Merge branch 'wasm'

This commit is contained in:
Mitchell Hashimoto
2022-12-03 21:36:21 -08:00
10 changed files with 289 additions and 25 deletions

View File

@ -73,6 +73,9 @@ pub fn build(b: *std.build.Builder) !void {
"Build and install test executables with 'build'", "Build and install test executables with 'build'",
) orelse false; ) orelse false;
// We can use wasmtime to test wasm
b.enable_wasmtime = true;
// Add our benchmarks // Add our benchmarks
try benchSteps(b, target, mode); try benchSteps(b, target, mode);
@ -106,11 +109,11 @@ pub fn build(b: *std.build.Builder) !void {
b.installFile("dist/macos/Ghostty.icns", "Ghostty.app/Contents/Resources/Ghostty.icns"); b.installFile("dist/macos/Ghostty.icns", "Ghostty.app/Contents/Resources/Ghostty.icns");
} }
// term.wasm // wasm
{ {
const wasm = b.addSharedLibrary( const wasm = b.addSharedLibrary(
"ghostty-term", "ghostty-wasm",
"src/terminal/main_wasm.zig", "src/main_wasm.zig",
.{ .unversioned = {} }, .{ .unversioned = {} },
); );
wasm.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); wasm.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
@ -122,8 +125,17 @@ pub fn build(b: *std.build.Builder) !void {
wasm.addPackage(utf8proc.pkg); wasm.addPackage(utf8proc.pkg);
_ = try utf8proc.link(b, wasm); _ = try utf8proc.link(b, wasm);
const step = b.step("term-wasm", "Build the terminal.wasm library"); const step = b.step("wasm", "Build the wasm library");
step.dependOn(&wasm.step); step.dependOn(&wasm.step);
// We support tests via wasmtime. wasmtime uses WASI so this
// isn't an exact match to our freestanding target above but
// it lets us test some basic functionality.
const test_step = b.step("test-wasm", "Run all tests for wasm");
const main_test = b.addTest("src/main_wasm.zig");
main_test.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .wasi });
main_test.addOptions("build_options", exe_options);
test_step.dependOn(&main_test.step);
} }
// Run // Run

View File

@ -13,21 +13,23 @@
} }
}; };
fetch('ghostty-term.wasm').then(response => fetch('ghostty-wasm.wasm').then(response =>
response.arrayBuffer() response.arrayBuffer()
).then(bytes => ).then(bytes =>
WebAssembly.instantiate(bytes, importObject) WebAssembly.instantiate(bytes, importObject)
).then(results => { ).then(results => {
const { const {
terminal_new, atlas_new,
terminal_free, atlas_free,
terminal_print, atlas_reserve,
free,
memory,
} = results.instance.exports; } = results.instance.exports;
const term = terminal_new(80, 80); const atlas = atlas_new(512, 0);
console.log(term); const reg = atlas_reserve(atlas, 10, 10);
terminal_free(term); free(reg);
terminal_print('a'); atlas_free(atlas);
}); });
</script> </script>
</body> </body>

View File

@ -2,6 +2,7 @@
, gdb , gdb
, glxinfo , glxinfo
, nodejs
, parallel , parallel
, pkg-config , pkg-config
, python , python
@ -64,6 +65,9 @@ in mkShell rec {
zig zig
zip zip
# For web and wasm stuff
nodejs
# Testing # Testing
gdb gdb
parallel parallel

View File

@ -49,10 +49,10 @@ modified: bool = false,
/// updated in-place. /// updated in-place.
resized: bool = false, resized: bool = false,
pub const Format = enum { pub const Format = enum(u8) {
greyscale, greyscale = 0,
rgb, rgb = 1,
rgba, rgba = 2,
pub fn depth(self: Format) u8 { pub fn depth(self: Format) u8 {
return switch (self) { return switch (self) {
@ -76,7 +76,7 @@ pub const Error = error{
/// A region within the texture atlas. These can be acquired using the /// A region within the texture atlas. These can be acquired using the
/// "reserve" function. A region reservation is required to write data. /// "reserve" function. A region reservation is required to write data.
pub const Region = struct { pub const Region = extern struct {
x: u32, x: u32,
y: u32, y: u32,
width: u32, width: u32,
@ -298,6 +298,80 @@ pub fn clear(self: *Atlas) void {
self.nodes.appendAssumeCapacity(.{ .x = 1, .y = 1, .width = self.size - 2 }); self.nodes.appendAssumeCapacity(.{ .x = 1, .y = 1, .width = self.size - 2 });
} }
/// The wasm-compatible API. This lacks documentation unless the API differs
/// from the standard Zig API. To learn what a function does, just look one
/// level deeper to what Zig function is called and read the documentation there.
///
/// To export this from Zig, use `usingnamespace Wasm` in some top-level
/// space and it will be exported.
pub const Wasm = struct {
// If you're copying this file (Atlas.zig) out to a separate project,
// just replace this with the allocator you want to use.
const wasm = @import("../wasm.zig");
const alloc = wasm.alloc;
export fn atlas_new(size: u32, format: u8) ?*Atlas {
const atlas = init(
alloc,
size,
@intToEnum(Format, format),
) catch return null;
const result = alloc.create(Atlas) catch return null;
result.* = atlas;
return result;
}
/// The return value for this should be freed by the caller with "free".
export fn atlas_reserve(self: *Atlas, width: u32, height: u32) ?*Region {
return atlas_reserve_(self, width, height) catch return null;
}
fn atlas_reserve_(self: *Atlas, width: u32, height: u32) !*Region {
const reg = try self.reserve(alloc, width, height);
const result = try alloc.create(Region);
errdefer alloc.destroy(result);
_ = try wasm.toHostOwned(result);
result.* = reg;
return result;
}
export fn atlas_set(self: *Atlas, reg: *Region, data: [*]const u8, len: usize) void {
self.set(reg.*, data[0..len]);
}
export fn atlas_grow(self: *Atlas, size_new: u32) bool {
self.grow(alloc, size_new) catch return false;
return true;
}
export fn atlas_clear(self: *Atlas) void {
self.clear();
}
export fn atlas_free(ptr: ?*Atlas) void {
if (ptr) |v| {
v.deinit(alloc);
alloc.destroy(v);
}
}
test "happy path" {
var atlas = atlas_new(512, @enumToInt(Format.greyscale)).?;
defer atlas_free(atlas);
const reg = atlas_reserve(atlas, 2, 2).?;
defer alloc.destroy(reg);
try testing.expect(wasm.isHostOwned(reg));
defer wasm.toModuleOwned(reg);
try testing.expect(reg.width > 0);
const data = &[_]u8{ 1, 2, 3, 4 };
try testing.expect(!atlas.modified);
atlas_set(atlas, reg, data, data.len);
try testing.expect(atlas.modified);
}
};
test "exact fit" { test "exact fit" {
const alloc = testing.allocator; const alloc = testing.allocator;
var atlas = try init(alloc, 34, .greyscale); // +2 for 1px border var atlas = try init(alloc, 34, .greyscale); // +2 for 1px border

View File

@ -68,6 +68,8 @@ pub fn deinit(self: *DeferredFace) void {
.fontconfig_freetype => if (self.fc) |*fc| fc.deinit(), .fontconfig_freetype => if (self.fc) |*fc| fc.deinit(),
.coretext => if (self.ct) |*ct| ct.deinit(), .coretext => if (self.ct) |*ct| ct.deinit(),
.freetype => {}, .freetype => {},
// TODO
.web_canvas => unreachable,
} }
self.* = undefined; self.* = undefined;
} }
@ -90,6 +92,9 @@ pub fn name(self: DeferredFace) ![:0]const u8 {
}, },
.freetype => {}, .freetype => {},
// TODO
.web_canvas => unreachable,
} }
return "TODO: built-in font names"; return "TODO: built-in font names";
@ -122,6 +127,9 @@ pub fn load(
return; return;
}, },
// TODO
.web_canvas => unreachable,
// Unreachable because we must be already loaded or have the // Unreachable because we must be already loaded or have the
// proper configuration for one of the other deferred mechanisms. // proper configuration for one of the other deferred mechanisms.
.freetype => unreachable, .freetype => unreachable,
@ -245,6 +253,9 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
} }
}, },
// TODO
.web_canvas => unreachable,
.freetype => {}, .freetype => {},
} }

View File

@ -2,12 +2,14 @@ const builtin = @import("builtin");
const options = @import("main.zig").options; const options = @import("main.zig").options;
const freetype = @import("face/freetype.zig"); const freetype = @import("face/freetype.zig");
const coretext = @import("face/coretext.zig"); const coretext = @import("face/coretext.zig");
const web_canvas = @import("face/web_canvas.zig");
/// Face implementation for the compile options. /// Face implementation for the compile options.
pub const Face = switch (options.backend) { pub const Face = switch (options.backend) {
.fontconfig_freetype => freetype.Face, .fontconfig_freetype => freetype.Face,
.coretext => freetype.Face, .coretext => freetype.Face,
//.coretext => coretext.Face, //.coretext => coretext.Face,
.web_canvas => web_canvas.Face,
else => unreachable, else => unreachable,
}; };

View File

@ -0,0 +1,11 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const font = @import("../main.zig");
const log = std.log.scoped(.font_face);
pub const Face = struct {
// TODO
};

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin");
const build_options = @import("build_options"); const build_options = @import("build_options");
pub const Atlas = @import("Atlas.zig"); pub const Atlas = @import("Atlas.zig");
@ -16,16 +17,15 @@ pub const Sprite = sprite.Sprite;
pub const Descriptor = discovery.Descriptor; pub const Descriptor = discovery.Descriptor;
pub const Discover = discovery.Discover; pub const Discover = discovery.Discover;
pub usingnamespace if (builtin.target.isWasm()) struct {
pub usingnamespace Atlas.Wasm;
} else struct {};
/// Build options /// Build options
pub const options: struct { pub const options: struct {
backend: Backend, backend: Backend,
} = .{ } = .{
.backend = if (build_options.coretext) .backend = Backend.default(),
.coretext
else if (build_options.fontconfig)
.fontconfig_freetype
else
.freetype,
}; };
pub const Backend = enum { pub const Backend = enum {
@ -38,14 +38,37 @@ pub const Backend = enum {
/// CoreText for both font discovery and rendering (macOS). /// CoreText for both font discovery and rendering (macOS).
coretext, coretext,
/// Use the browser font system and the Canvas API (wasm). This limits
/// the available fonts to browser fonts (anything Canvas natively
/// supports).
web_canvas,
/// Returns the default backend for a build environment. This is
/// meant to be called at comptime.
pub fn default() Backend {
// Wasm only supports browser at the moment.
if (builtin.target.isWasm()) return .web_canvas;
return if (build_options.coretext)
.coretext
else if (build_options.fontconfig)
.fontconfig_freetype
else
.freetype;
}
/// Helper that just returns true if we should be using freetype. This /// Helper that just returns true if we should be using freetype. This
/// is used for tests. /// is used for tests.
pub fn freetype(self: Backend) bool { pub fn freetype(self: Backend) bool {
return switch (self) { return switch (self) {
.freetype, .fontconfig_freetype => true, .freetype, .fontconfig_freetype => true,
.coretext => false, .coretext, .web_canvas => false,
}; };
} }
test "default can run at comptime" {
_ = comptime default();
}
}; };
/// The styles that a family can take. /// The styles that a family can take.
@ -66,5 +89,11 @@ pub const Presentation = enum(u1) {
pub const sprite_index = Group.FontIndex.initSpecial(.sprite); pub const sprite_index = Group.FontIndex.initSpecial(.sprite);
test { test {
@import("std").testing.refAllDecls(@This()); // For non-wasm we want to test everything we can
if (!comptime builtin.target.isWasm()) {
@import("std").testing.refAllDecls(@This());
return;
}
_ = Atlas;
} }

5
src/main_wasm.zig Normal file
View File

@ -0,0 +1,5 @@
// This is the main file for the WASM module. The WASM module has to
// export a C ABI compatible API.
pub usingnamespace @import("wasm.zig");
pub usingnamespace @import("font/main.zig");

114
src/wasm.zig Normal file
View File

@ -0,0 +1,114 @@
//! This file contains helpers for wasm compilation.
const std = @import("std");
const builtin = @import("builtin");
comptime {
if (!builtin.target.isWasm()) {
@compileError("wasm.zig should only be analyzed for wasm32 builds");
}
}
/// The allocator to use in wasm environments.
///
/// The return values of this should NOT be sent to the host environment
/// unless toHostOwned is called on them. In this case, the caller is expected
/// to call free. If a pointer is NOT host-owned, then the wasm module is
/// expected to call the normal alloc.free/destroy functions.
///
/// TODO: we should NOT be using page_allocator because we're getting
/// full 64kb pages for every allocation. I plan on changing this to the
/// new stdlib wasm allocator once that is merged and available.
pub const alloc = if (builtin.is_test)
std.testing.allocator
else
std.heap.page_allocator;
/// For host-owned allocations:
/// We need to keep track of our own pointer lengths because Zig
/// allocators usually don't do this and we need to be able to send
/// a direct pointer back to the host system. A more appropriate thing
/// to do would be to probably make a custom allocator that keeps track
/// of size.
var allocs: std.AutoHashMapUnmanaged([*]u8, usize) = .{};
/// Allocate len bytes and return a pointer to the memory in the host.
/// The data is not zeroed.
pub export fn malloc(len: usize) ?[*]u8 {
return alloc_(len) catch return null;
}
/// Free an allocation from malloc.
pub export fn free(ptr: ?[*]u8) void {
if (ptr) |v| {
if (allocs.get(v)) |len| {
const slice = v[0..len];
alloc.free(slice);
_ = allocs.remove(v);
}
}
}
/// Convert an allocated pointer of any type to a host-owned pointer.
/// This pushes the responsibility to free it to the host. The returned
/// pointer will match the pointer but is typed correctly for returning
/// to the host.
pub fn toHostOwned(ptr: anytype) ![*]u8 {
// Convert our pointer to a byte array
const info = @typeInfo(@TypeOf(ptr)).Pointer;
const T = info.child;
const size = @sizeOf(T);
const casted = @intToPtr([*]u8, @ptrToInt(ptr));
// Store the information about it
try allocs.putNoClobber(alloc, casted, size);
errdefer _ = allocs.remove(casted);
return casted;
}
/// Returns true if the value is host owned.
pub fn isHostOwned(ptr: anytype) bool {
const casted = @intToPtr([*]u8, @ptrToInt(ptr));
return allocs.contains(casted);
}
/// Convert a pointer back to a module-owned value. The caller is expected
/// to cast or have the valid pointer for alloc calls.
pub fn toModuleOwned(ptr: anytype) void {
const casted = @intToPtr([*]u8, @ptrToInt(ptr));
_ = allocs.remove(casted);
}
fn alloc_(len: usize) ![*]u8 {
// Create the allocation
const slice = try alloc.alloc(u8, len);
errdefer alloc.free(slice);
// Store the size so we can deallocate later
try allocs.putNoClobber(alloc, slice.ptr, slice.len);
errdefer _ = allocs.remove(slice.ptr);
return slice.ptr;
}
test "basics" {
const testing = std.testing;
var buf = malloc(32).?;
try testing.expect(allocs.size == 1);
free(buf);
try testing.expect(allocs.size == 0);
}
test "toHostOwned" {
const testing = std.testing;
const Point = struct { x: u32 = 0, y: u32 = 0 };
var p = try alloc.create(Point);
errdefer alloc.destroy(p);
const ptr = try toHostOwned(p);
try testing.expect(allocs.size == 1);
try testing.expect(isHostOwned(p));
try testing.expect(isHostOwned(ptr));
free(ptr);
try testing.expect(allocs.size == 0);
}