From 241bfee7d4419db151765cc8053215323f87e89e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 24 Dec 2022 16:20:59 -0800 Subject: [PATCH] wasm: use shared, imported memory This switches our wasm build to use "shared" memory. Shared memory can be shared across multiple web workers, which is something we'll want to support for our multi-threaded behaviors later. Shared memory has a number of different restrictions so this updates zig-js to support it as well as updates some of our functions that need to be aware of it. --- build.zig | 32 ++++++++++++++++++++++++++++++-- example/app.ts | 8 +++++--- src/font/Atlas.zig | 14 +++++++++++--- src/os/wasm.zig | 5 +++++ vendor/zig-js | 2 +- 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/build.zig b/build.zig index b9f56e776..a9794aa13 100644 --- a/build.zig +++ b/build.zig @@ -112,16 +112,44 @@ pub fn build(b: *std.build.Builder) !void { // wasm { + // Build our Wasm target. + const wasm_target: std.zig.CrossTarget = .{ + .cpu_arch = .wasm32, + .os_tag = .freestanding, + .cpu_model = .{ .explicit = &std.Target.wasm.cpu.mvp }, + .cpu_features_add = std.Target.wasm.featureSet(&.{ + // We use this to explicitly request shared memory. + .atomics, + + // Not explicitly used but compiler could use them if they want. + .bulk_memory, + .reference_types, + .sign_ext, + }), + }; + + // Whether we're using wasm shared memory. Some behaviors change. + // For now we require this but I wanted to make the code handle both + // up front. + const wasm_shared: bool = true; + exe_options.addOption(bool, "wasm_shared", wasm_shared); + const wasm = b.addSharedLibrary( "ghostty-wasm", "src/main_wasm.zig", .{ .unversioned = {} }, ); - wasm.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); + wasm.setTarget(wasm_target); wasm.setBuildMode(mode); wasm.setOutputDir("zig-out"); wasm.addOptions("build_options", exe_options); + // So that we can use web workers with our wasm binary + wasm.import_memory = true; + wasm.initial_memory = 65536 * 25; + wasm.max_memory = 65536 * 65536; // Maximum number of pages in wasm32 + wasm.shared_memory = wasm_shared; + // Stack protector adds extern requirements that we don't satisfy. wasm.stack_protector = false; @@ -136,7 +164,7 @@ pub fn build(b: *std.build.Builder) !void { // 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.setTarget(wasm_target); main_test.addOptions("build_options", exe_options); try addDeps(b, main_test, true); test_step.dependOn(&main_test.step); diff --git a/example/app.ts b/example/app.ts index f66771371..40a81d2c2 100644 --- a/example/app.ts +++ b/example/app.ts @@ -4,9 +4,11 @@ const zjs = new ZigJS(); const importObject = { module: {}, env: { + memory: new WebAssembly.Memory({ initial: 25, maximum: 65536, shared: true }), log: (ptr: number, len: number) => { - const view = new DataView(zjs.memory.buffer, ptr, Number(len)); - const str = new TextDecoder('utf-8').decode(view); + const arr = new Uint8ClampedArray(zjs.memory.buffer, ptr, len); + const data = arr.slice(); + const str = new TextDecoder('utf-8').decode(data); console.log(str); }, }, @@ -20,8 +22,8 @@ fetch(url.href).then(response => ).then(bytes => WebAssembly.instantiate(bytes, importObject) ).then(results => { + const memory = importObject.env.memory; const { - memory, malloc, free, face_new, diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index f38af41df..30d0b5cba 100644 --- a/src/font/Atlas.zig +++ b/src/font/Atlas.zig @@ -423,9 +423,17 @@ pub const Wasm = struct { defer mem_buf.deinit(); // Create an array that points to our buffer - const Uint8ClampedArray = try js.global.get(js.Object, "Uint8ClampedArray"); - defer Uint8ClampedArray.deinit(); - const arr = try Uint8ClampedArray.new(.{ mem_buf, buf.ptr, buf.len }); + const arr = arr: { + const Uint8ClampedArray = try js.global.get(js.Object, "Uint8ClampedArray"); + defer Uint8ClampedArray.deinit(); + const arr = try Uint8ClampedArray.new(.{ mem_buf, buf.ptr, buf.len }); + if (!wasm.shared_mem) break :arr arr; + + // If we're sharing memory then we have to copy the data since + // we can't set ImageData directly using a SharedArrayBuffer. + defer arr.deinit(); + break :arr try arr.call(js.Object, "slice", .{}); + }; defer arr.deinit(); // Create the image data from our array diff --git a/src/os/wasm.zig b/src/os/wasm.zig index 5d3c65830..31435314b 100644 --- a/src/os/wasm.zig +++ b/src/os/wasm.zig @@ -1,6 +1,7 @@ //! This file contains helpers for wasm compilation. const std = @import("std"); const builtin = @import("builtin"); +const options = @import("build_options"); comptime { if (!builtin.target.isWasm()) { @@ -8,6 +9,10 @@ comptime { } } +/// True if we're in shared memory mode. If true, then the memory buffer +/// in JS will be backed by a SharedArrayBuffer and some behaviors change. +pub const shared_mem = options.wasm_shared; + /// The allocator to use in wasm environments. /// /// The return values of this should NOT be sent to the host environment diff --git a/vendor/zig-js b/vendor/zig-js index 5e3a5ce77..c89c1965c 160000 --- a/vendor/zig-js +++ b/vendor/zig-js @@ -1 +1 @@ -Subproject commit 5e3a5ce776f7b424022494a830f66ace224fe7ff +Subproject commit c89c1965cc6bf6ede97c1b891b624ce5282853d1