From fe9293046a3c022b4f1111646232ed2e058dd48a Mon Sep 17 00:00:00 2001 From: zenith391 <39484230+zenith391@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:19:40 +0200 Subject: [PATCH] work on wasm without async It might be a dead end as it just maxes out the CPU at 100% (so a big no-no for mobile) and is hard to work around --- build_capy.zig | 30 +++- src/backends/wasm/backend.zig | 142 +++++++++++----- src/backends/wasm/capy-worker.js | 281 +++++++++++++++++++++++++++++++ src/backends/wasm/capy.js | 75 +++++++-- src/internal.zig | 1 - 5 files changed, 466 insertions(+), 63 deletions(-) create mode 100644 src/backends/wasm/capy-worker.js diff --git a/build_capy.zig b/build_capy.zig index e46dbea5..a6688a48 100644 --- a/build_capy.zig +++ b/build_capy.zig @@ -87,24 +87,42 @@ const WebServerStep = struct { } else if (std.mem.eql(u8, path, "/capy.js")) { file_path = try std.fs.path.join(req_allocator, &.{ build_root, "src/backends/wasm/capy.js" }); content_type = "application/javascript"; + } else if (std.mem.eql(u8, path, "/capy-worker.js")) { + file_path = try std.fs.path.join(req_allocator, &.{ build_root, "src/backends/wasm/capy-worker.js" }); + content_type = "application/javascript"; } else if (std.mem.eql(u8, path, "/zig-app.wasm")) { file_path = self.exe.getOutputSource().getPath2(build, &self.step); content_type = "application/wasm"; } - var file = try std.fs.openFileAbsolute(file_path, .{ .mode = .read_only }); - defer file.close(); - const content = try file.readToEndAlloc(req_allocator, std.math.maxInt(usize)); + std.log.info("{s} -> {s}", .{ path, file_path }); + const file: ?std.fs.File = std.fs.cwd().openFile(file_path, .{ .mode = .read_only }) catch |err| blk: { + switch (err) { + error.FileNotFound => break :blk null, + else => return err, + } + }; + const content = blk: { + if (file) |f| { + defer f.close(); + break :blk try f.readToEndAlloc(req_allocator, std.math.maxInt(usize)); + } else { + break :blk "404 Not Found"; + } + }; res.transfer_encoding = .{ .content_length = content.len }; - try res.headers.append("Connection", res.request.headers.getFirstValue("Connection") orelse "close"); + // try res.headers.append("Connection", res.request.headers.getFirstValue("Connection") orelse "close"); + try res.headers.append("Connection", "close"); try res.headers.append("Content-Type", content_type); + try res.headers.append("Cross-Origin-Opener-Policy", "same-origin"); + try res.headers.append("Cross-Origin-Embedder-Policy", "require-corp"); try res.do(); try res.writer().writeAll(content); try res.finish(); - if (res.connection.closing) break; + if (res.connection.closing or true) break; } } }; @@ -280,6 +298,8 @@ pub fn install(step: *std.Build.CompileStep, options: CapyBuildOptions) !*std.Bu if (step.optimize == .ReleaseSmall) { step.strip = true; } + step.export_symbol_names = &.{"_start"}; + step.import_memory = true; const serve = WebServerStep.create(b, step); const install_step = b.addInstallArtifact(step, .{}); diff --git a/src/backends/wasm/backend.zig b/src/backends/wasm/backend.zig index 0662122e..62468e23 100644 --- a/src/backends/wasm/backend.zig +++ b/src/backends/wasm/backend.zig @@ -46,11 +46,16 @@ pub fn init() !void { var globalWindow: ?*Window = null; pub const Window = struct { + peer: *GuiWidget, child: ?PeerType = null, scale: f32 = 1.0, + pub usingnamespace Events(Window); + pub fn create() !Window { - return Window{}; + return Window{ + .peer = try GuiWidget.init(Window, lasting_allocator, "div", "window"), + }; } pub fn show(self: *Window) void { @@ -119,6 +124,7 @@ pub fn Events(comptime T: type) type { }, .KeyType => self.peer.user.keyTypeHandler = cb, .KeyPress => self.peer.user.keyPressHandler = cb, + .PropertyChange => self.peer.user.propertyChangeHandler = cb, } } @@ -188,11 +194,11 @@ pub fn Events(comptime T: type) type { } pub fn getWidth(self: *const T) c_int { - return std.math.max(10, js.getWidth(self.peer.element)); + return @max(10, js.getWidth(self.peer.element)); } pub fn getHeight(self: *const T) c_int { - return std.math.max(10, js.getHeight(self.peer.element)); + return @max(10, js.getHeight(self.peer.element)); } pub fn getPreferredSize(self: *const T) lib.Size { @@ -241,7 +247,7 @@ pub const TextField = struct { pub const Label = struct { peer: *GuiWidget, /// The text returned by getText(), it's invalidated everytime setText is called - temp_text: ?[:0]const u8 = null, + temp_text: ?[]const u8 = null, pub usingnamespace Events(Label); @@ -251,7 +257,7 @@ pub const Label = struct { pub fn setAlignment(_: *Label, _: f32) void {} - pub fn setText(self: *Label, text: [:0]const u8) void { + pub fn setText(self: *Label, text: []const u8) void { js.setText(self.peer.element, text.ptr, text.len); if (self.temp_text) |slice| { lasting_allocator.free(slice); @@ -259,7 +265,7 @@ pub const Label = struct { } } - pub fn getText(self: *Label) [:0]const u8 { + pub fn getText(self: *Label) []const u8 { if (self.temp_text) |text| { return text; } else { @@ -463,6 +469,16 @@ pub const Container = struct { self.children.append(peer) catch unreachable; } + pub fn remove(self: *const Container, peer: PeerType) void { + _ = peer; + _ = self; + } + + pub fn setTabOrder(self: *const Container, peers: []const PeerType) void { + _ = peers; + _ = self; + } + pub fn move(self: *const Container, peer: PeerType, x: u32, y: u32) void { _ = self; js.setPos(peer.element, x, y); @@ -496,9 +512,27 @@ pub const HttpResponse = struct { } }; -// Execution +var stopExecution = false; -fn executeMain() callconv(.Async) void { +// Temporary execution until async is added back in Zig +pub fn runStep(step: shared.EventLoopStep) bool { + _ = step; + while (js.hasEvent()) { + const eventId = js.popEvent(); + switch (js.getEventType(eventId)) { + else => { + if (globalWindow) |window| { + if (window.child) |child| { + child.processEventFn(child.object, eventId); + } + } + }, + } + } + return !stopExecution; +} + +fn executeMain() void { const mainFn = @import("root").main; const ReturnType = @typeInfo(@TypeOf(mainFn)).Fn.return_type.?; if (ReturnType == void) { @@ -507,13 +541,28 @@ fn executeMain() callconv(.Async) void { mainFn() catch |err| @panic(@errorName(err)); } js.stopExecution(); + stopExecution = true; } -var frame: @Frame(executeMain) = undefined; -var result: void = {}; -var suspending: bool = false; +// Execution +// TODO: reuse the old system when async is finally reimplemented in the zig compiler + +// fn executeMain() callconv(.Async) void { +// const mainFn = @import("root").main; +// const ReturnType = @typeInfo(@TypeOf(mainFn)).Fn.return_type.?; +// if (ReturnType == void) { +// mainFn(); +// } else { +// mainFn() catch |err| @panic(@errorName(err)); +// } +// js.stopExecution(); +// } -var resumePtr: anyframe = undefined; +// var frame: @Frame(executeMain) = undefined; +// var result: void = {}; +// var suspending: bool = false; + +// var resumePtr: anyframe = undefined; fn milliTimestamp() i64 { return @as(i64, @intFromFloat(js.now())); @@ -556,10 +605,11 @@ pub const backendExport = struct { const start = milliTimestamp(); while (milliTimestamp() < start + @as(i64, @intCast(duration))) { - suspending = true; - suspend { - resumePtr = @frame(); - } + // suspending = true; + // suspend { + // resumePtr = @frame(); + // } + // TODO: better way to sleep like calling a jS function for sleep } return 0; } @@ -600,37 +650,43 @@ pub const backendExport = struct { //@breakpoint(); js.stopExecution(); + stopExecution = true; + while (true) {} } pub export fn _start() callconv(.C) void { - _ = @asyncCall(&frame, &result, executeMain, .{}); + executeMain(); } - pub export fn _zgtContinue() callconv(.C) void { - if (suspending) { - suspending = false; - resume resumePtr; - } - } + // pub export fn _start() callconv(.C) void { + // _ = @asyncCall(&frame, &result, executeMain, .{}); + // } + + // pub export fn _zgtContinue() callconv(.C) void { + // if (suspending) { + // suspending = false; + // resume resumePtr; + // } + // } }; -pub fn runStep(step: shared.EventLoopStep) callconv(.Async) bool { - _ = step; - while (js.hasEvent()) { - const eventId = js.popEvent(); - switch (js.getEventType(eventId)) { - else => { - if (globalWindow) |window| { - if (window.child) |child| { - child.processEventFn(child.object, eventId); - } - } - }, - } - } - suspending = true; - suspend { - resumePtr = @frame(); - } - return true; -} +// pub fn runStep(step: shared.EventLoopStep) callconv(.Async) bool { +// _ = step; +// while (js.hasEvent()) { +// const eventId = js.popEvent(); +// switch (js.getEventType(eventId)) { +// else => { +// if (globalWindow) |window| { +// if (window.child) |child| { +// child.processEventFn(child.object, eventId); +// } +// } +// }, +// } +// } +// suspending = true; +// suspend { +// resumePtr = @frame(); +// } +// return true; +// } diff --git a/src/backends/wasm/capy-worker.js b/src/backends/wasm/capy-worker.js new file mode 100644 index 00000000..ffdf1f08 --- /dev/null +++ b/src/backends/wasm/capy-worker.js @@ -0,0 +1,281 @@ +let obj = null; +let pendingEvents = []; +let networkRequests = []; +let networkRequestsCompletion = []; +let networkRequestsReadIdx = []; +let resources = []; +let events = []; +let executeProgram = true; +let rootElementId = -1; + +function pushEvent(evt) { + const eventId = events.push(evt); + pendingEvents.push(eventId - 1); +} + +function readString(addr, len) { + addr = addr >>> 0; // convert from i32 to u32 + len = len >>> 0; + + let utf8Decoder = new TextDecoder(); + // let view = new Uint8Array(obj.instance.exports.memory.buffer); + let view = new Uint8Array(env.memory.buffer); + // console.debug("read string @ " + addr + " for " + len + " bytes"); + + return utf8Decoder.decode(view.slice(addr, addr + len)); +} + +// 1 byte for making and 8 bytes for data (64 bits) +let pendingAnswer = new SharedArrayBuffer(9); +/** + @param {string} type The type of the answer, can only be "int" +**/ +function waitForAnswer(type) { + const WAITING = 0; + const DONE = 1; + + const view = new DataView(pendingAnswer); + view.setUint8(0, WAITING); + while (view.getUint8(0) != DONE) { + wait(10); + } + + switch (type) { + case "int": + const int = view.getUint32(5, true) << 32 | view.getUint32(1, true); + console.log("Received answer " + int); + return int; + } + + throw Error("Type invalid (" + type + ")"); +} + +/** + @param {int} msecs Time to wait in milliseconds +**/ +function wait(msecs) { + const start = Date.now(); + while (Date.now() >= start + msecs) { + // Wait. + } + + return; +} + +const memory = new WebAssembly.Memory({ + initial: 20, + maximum: 100, + // shared: true, // NOT SUPPORTED ON FIREFOX +}); +const env = { + memory: memory, + jsPrint: function(arg, len) { + console.log(readString(arg, len)); + }, + jsCreateElement: function(name, nameLen, elementType, elementTypeLen) { + self.postMessage(["jsCreateElement", name, nameLen, elementType, elementTypeLen]); + const a = waitForAnswer("int"); + return a; + }, + appendElement: function(parent, child) { + self.postMessage(["appendElement", parent, child]); + }, + /** + * @param {int} root + **/ + setRoot: function(root) { + self.postMessage(["setRoot", root]); + }, + setText: function(element, textPtr, textLen) { + self.postMessage(["setText", element, readString(textPtr, textLen)]); + }, + getTextLen: function(element) { + self.postMessage(["getTextLen", element]); + return waitForAnswer("int"); + }, + getText: function(element, textPtr) { + self.postMessage(["getText", element, textPtr]); + }, + setPos: function(element, x, y) { + self.postMessage(["setPos", element, x, y]) + }, + setSize: function(element, w, h) { + self.postMessage(["setSize", element, w, h]); + }, + getWidth: function(element) { + self.postMessage(["getWidth", element]); + return waitForAnswer("int"); + }, + getHeight: function(element) { + self.postMessage(["getHeight", element]); + return waitForAnswer("int"); + }, + now: function() { + return Date.now(); + }, + hasEvent: function() { + return pendingEvents.length > 0; + }, + popEvent: function() { + if (pendingEvents.length > 0) { + return pendingEvents.shift(); + } else { + console.error("Popping event even though none is available!"); + } + }, + getEventType: function(event) { + return events[event].type; + }, + getEventTarget: function(event) { + if (events[event].target === undefined) { + console.error("Tried getting the target of a global event"); + } + return events[event].target; + }, + getEventArg: function(event, idx) { + if (events[event].args === undefined || events[event].args[idx] === undefined) { + console.error("Tried getting non-existent arg:" + idx); + } + return events[event].args[idx]; + }, + + // Canvas + openContext: function(element) { + const canvas = domObjects[element]; + canvas.width = window.devicePixelRatio * canvas.clientWidth; + canvas.height = window.devicePixelRatio * canvas.clientHeight; + + for (ctxId in canvasContexts) { + if (canvasContexts[ctxId].owner === element) { + canvasContexts[ctxId].clearRect(0, 0, canvas.width, canvas.height); + return ctxId; + } + } + const ctx = canvas.getContext("2d"); + ctx.owner = element; + ctx.lineWidth = 2.5; + ctx.beginPath(); + return canvasContexts.push(ctx) - 1; + }, + setColor: function(ctx, r, g, b, a) { + canvasContexts[ctx].fillStyle = "rgba(" + r + "," + g + "," + b + "," + a + ")"; + canvasContexts[ctx].strokeStyle = canvasContexts[ctx].fillStyle; + }, + rectPath: function(ctx, x, y, w, h) { + canvasContexts[ctx].rect(x, y, w, h); + }, + moveTo: function(ctx, x, y) { + canvasContexts[ctx].moveTo(x, y); + }, + lineTo: function(ctx, x, y) { + canvasContexts[ctx].lineTo(x, y); + }, + fillText: function(ctx, textPtr, textLen, x, y) { + const text = readString(textPtr, textLen); + canvasContexts[ctx].textAlign = "left"; + canvasContexts[ctx].textBaseline = "top"; + canvasContexts[ctx].fillText(text, x, y); + }, + fillImage: function(ctx, img, x, y) { + const canvas = canvasContexts[ctx]; + const image = resources[img]; + if (!image.imageDatas[ctx]) { + image.imageDatas[ctx] = canvas.createImageData(image.width, image.height); + const data = image.imageDatas[ctx].data; + const Bpp = image.stride / image.width; // source bytes per pixel + for (let y = 0; y < image.height; y++) { + for (let x = 0; x < image.width; x++) { + data[y*image.width*4+x*4+0] = image.bytes[y*image.stride+x*Bpp+0]; + data[y*image.width*4+x*4+1] = image.bytes[y*image.stride+x*Bpp+1]; + data[y*image.width*4+x*4+2] = image.bytes[y*image.stride+x*Bpp+2]; + if (!image.isRgb) { + data[y*image.width*4+x*4+3] = image.bytes[y*image.stride+x*Bpp+3]; + } else { + data[y*image.width*4+x*4+3] = 0xFF; + } + } + } + image.bytes = undefined; // try to free up some space + resources[img] = image; + } + canvas.putImageData(image.imageDatas[ctx], x, y); + }, + fill: function(ctx) { + canvasContexts[ctx].fill(); + canvasContexts[ctx].beginPath(); + }, + stroke: function(ctx) { + canvasContexts[ctx].stroke(); + canvasContexts[ctx].beginPath(); + }, + + // Resources + uploadImage: function(width, height, stride, isRgb, bytesPtr) { + const size = stride * height; + let view = new Uint8Array(obj.instance.exports.memory.buffer); + let data = Uint8ClampedArray.from(view.slice(bytesPtr, bytesPtr + size)); + return resources.push({ + type: 'image', + width: width, + height: height, + stride: stride, + rgb: isRgb != 0, + bytes: data, + imageDatas: {}, + }) - 1; + }, + + // Network + fetchHttp: function(urlPtr, urlLen) { + const url = readString(urlPtr, urlLen); + const id = networkRequests.length; + const promise = fetch(url).then(response => response.arrayBuffer()).then(response => { + networkRequestsCompletion[id] = true; + networkRequests[id] = response; + }); + networkRequestsCompletion.push(false); + networkRequestsReadIdx.push(0); + return networkRequests.push(promise) - 1; + }, + isRequestReady: function(id) { + return networkRequestsCompletion[id]; + }, + readRequest: function(id, bufPtr, bufLen) { + if (networkRequestsCompletion[id] === false) return 0; + + const buffer = networkRequests[id]; + const idx = networkRequestsReadIdx[id]; + + const view = new Uint8Array(buffer); + const slice = view.slice(idx, idx + bufLen); + const memoryView = new Uint8Array(obj.instance.exports.memory.buffer); + for (let i = 0; i < slice.length; i++) { + memoryView[bufPtr + i] = slice[i]; + } + networkRequestsReadIdx[id] += slice.length; + + return slice.length; + }, + + stopExecution: function() { + postMessage(["stopExecution"]); + }, + }; + +console.log("WEB WORKER RUN"); + +(async function() { + const importObject = { + env: env, + }; + if (WebAssembly.instantiateStreaming) { + obj = await WebAssembly.instantiateStreaming(fetch("zig-app.wasm"), importObject); + } else { + const response = await fetch("zig-app.wasm"); + obj = await WebAssembly.instantiate(await response.arrayBuffer(), importObject); + } + + // const buffer = obj.instance.exports.memory.buffer; + self.postMessage(["setBuffer", memory.buffer, pendingAnswer]); + obj.instance.exports._start(); +})(); diff --git a/src/backends/wasm/capy.js b/src/backends/wasm/capy.js index ed30684f..eeb379aa 100644 --- a/src/backends/wasm/capy.js +++ b/src/backends/wasm/capy.js @@ -1,4 +1,3 @@ -let obj = null; let domObjects = []; let canvasContexts = []; let pendingEvents = []; @@ -10,23 +9,58 @@ let events = []; let executeProgram = true; let rootElementId = -1; +/** + @type SharedArrayBuffer +**/ +let arrayBuffer = undefined; +/** + @type SharedArrayBuffer +**/ +let pendingAnswer = undefined; + function pushEvent(evt) { const eventId = events.push(evt); pendingEvents.push(eventId - 1); } +async function pushAnswer(type, value) { + if (type == "int" && typeof value !== "number") { + throw Error("Type mismatch, got " + (typeof value)); + } + + const WAITING = 0; + const DONE = 1; + const view = new DataView(pendingAnswer); + while (view.getUint8(0) != WAITING) { + // throw Error("Expected waiting state"); + await wait(100); + console.log("Await waiting state"); + } + + const left = value & 0xFFFFFFFF; + const right = value >> 32; + view.setUint32(1, left, true); + view.setUint32(5, right, true); + view.setInt8(0, DONE); +} + +async function wait(msecs) { + const promise = new Promise((resolve, reject) => { + setTimeout(resolve, msecs); + }); + return promise; +} + function readString(addr, len) { addr = addr >>> 0; // convert from i32 to u32 len = len >>> 0; let utf8Decoder = new TextDecoder(); - let view = new Uint8Array(obj.instance.exports.memory.buffer); - // console.debug("read string @ " + addr + " for " + len + " bytes"); + let view = new Uint8Array(arrayBuffer); return utf8Decoder.decode(view.slice(addr, addr + len)); } -const importObj = { - env: { +const env = { jsPrint: function(arg, len) { console.log(readString(arg, len)); }, @@ -325,23 +359,36 @@ const importObj = { executeProgram = false; console.error("STOP EXECUTION!"); }, - } }; + (async function() { - if (WebAssembly.instantiateStreaming) { - obj = await WebAssembly.instantiateStreaming(fetch("zig-app.wasm"), importObj); - } else { - const response = await fetch("zig-app.wasm"); - obj = await WebAssembly.instantiate(await response.arrayBuffer(), importObj); + if (!window.Worker) { + alert("Capy requires Web Workers until Zig supports async"); } - obj.instance.exports._start(); + const wasmWorker = new Worker("capy-worker.js"); + wasmWorker.postMessage("test"); + wasmWorker.onmessage = (e) => { + console.log("message", e.data); + const name = e.data[0]; + if (name === "setBuffer") { + arrayBuffer = e.data[1]; + pendingAnswer = e.data[2]; + } else if (name === "stopExecution") { + wasmWorker.terminate(); + } else { + const value = env[name].apply(null, e.data.slice(1)); + if (value !== undefined) { + pushAnswer("int", value); + } + } + }; // TODO: when we're in blocking mode, avoid updating so often function update() { if (executeProgram) { - obj.instance.exports._zgtContinue(); - requestAnimationFrame(update); + // obj.instance.exports._zgtContinue(); + // requestAnimationFrame(update); } } //setInterval(update, 32); diff --git a/src/internal.zig b/src/internal.zig index 240bcaea..81dffb37 100644 --- a/src/internal.zig +++ b/src/internal.zig @@ -578,7 +578,6 @@ pub fn Events(comptime T: type) type { /// When the value is changed in the opacity data wrapper fn opacityChanged(newValue: f32, userdata: usize) void { const widget = @as(*T, @ptrFromInt(userdata)); - std.log.info("opaccity changed to {d}", .{newValue}); if (widget.peer) |*peer| { peer.setOpacity(newValue); }