Skip to content

Commit

Permalink
wasm: Reduce CPU usage using Atomics.wait
Browse files Browse the repository at this point in the history
  • Loading branch information
zenith391 committed Oct 26, 2023
1 parent fe92930 commit b8c7de8
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 89 deletions.
6 changes: 6 additions & 0 deletions src/backends/wasm/backend.zig
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,11 @@ pub const Canvas = struct {
js.rectPath(self.ctx, x, y, w, h);
}

pub fn roundedRectangleEx(self: *DrawContext, x: i32, y: i32, w: u32, h: u32, cornerRadiuses: [4]f32) void {
_ = cornerRadiuses;
self.rectangle(x, y, w, h);
}

pub fn text(self: *DrawContext, x: i32, y: i32, layout: TextLayout, str: []const u8) void {
// TODO: layout
_ = layout;
Expand Down Expand Up @@ -517,6 +522,7 @@ var stopExecution = false;
// Temporary execution until async is added back in Zig
pub fn runStep(step: shared.EventLoopStep) bool {
_ = step;
js.yield();
while (js.hasEvent()) {
const eventId = js.popEvent();
switch (js.getEventType(eventId)) {
Expand Down
120 changes: 42 additions & 78 deletions src/backends/wasm/capy-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,40 +26,45 @@ function readString(addr, len) {
}

// 1 byte for making and 8 bytes for data (64 bits)
let pendingAnswer = new SharedArrayBuffer(9);
let pendingAnswer = new SharedArrayBuffer(12);
/**
@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);
if (type == "bool") {
return waitForAnswer("int") != 0;
}

const view = new Int32Array(pendingAnswer);
view[0] = WAITING;
// while (view.getUint8(0) != DONE) {
// wait(10);
// }
Atomics.wait(view, 0, WAITING);

switch (type) {
case "int":
const int = view.getUint32(5, true) << 32 | view.getUint32(1, true);
console.log("Received answer " + int);
const int = view[1] << 32 | view[2];
return int;
}

throw Error("Type invalid (" + type + ")");
}

/**
Shared array buffer used for sleeping with Atomics.wait()
**/
const AB = new Int32Array(new SharedArrayBuffer(4));
/**
@param {int} msecs Time to wait in milliseconds
**/
function wait(msecs) {
const start = Date.now();
while (Date.now() >= start + msecs) {
// Wait.
while (Date.now() <= start + msecs) {
Atomics.wait(AB, 0, 0, msecs - (Date.now() - start));
}

return;
}

const memory = new WebAssembly.Memory({
Expand Down Expand Up @@ -114,99 +119,54 @@ const env = {
return Date.now();
},
hasEvent: function() {
return pendingEvents.length > 0;
self.postMessage(["hasEvent"]);
return waitForAnswer("bool");
},
popEvent: function() {
if (pendingEvents.length > 0) {
return pendingEvents.shift();
} else {
console.error("Popping event even though none is available!");
}
self.postMessage(["popEvent"]);
return waitForAnswer("int");
},
getEventType: function(event) {
return events[event].type;
self.postMessage(["getEventType", event]);
return waitForAnswer("int");
},
getEventTarget: function(event) {
if (events[event].target === undefined) {
console.error("Tried getting the target of a global event");
}
return events[event].target;
self.postMessage(["getEventTarget", event]);
return waitForAnswer("int");
},
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];
self.postMessage(["getEventArg", event, idx]);
return waitForAnswer("int");
},

// 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;
self.postMessage(["openContext", element]);
return waitForAnswer("int");
},
setColor: function(ctx, r, g, b, a) {
canvasContexts[ctx].fillStyle = "rgba(" + r + "," + g + "," + b + "," + a + ")";
canvasContexts[ctx].strokeStyle = canvasContexts[ctx].fillStyle;
self.postMessage(["setColor", ctx, r, g, b, a]);
},
rectPath: function(ctx, x, y, w, h) {
canvasContexts[ctx].rect(x, y, w, h);
self.postMessage(["rectPath", ctx, x, y, w, h]);
},
moveTo: function(ctx, x, y) {
canvasContexts[ctx].moveTo(x, y);
self.postMessage(["moveTo", ctx, x, y]);
},
lineTo: function(ctx, x, y) {
canvasContexts[ctx].lineTo(x, y);
self.postMessage(["lineTo", ctx, 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);
throw new Error("TODO: fill text");
},
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);
self.postMessage(["fillImage", ctx, img, x, y]);
},
fill: function(ctx) {
canvasContexts[ctx].fill();
canvasContexts[ctx].beginPath();
self.postMessage(["fill", ctx]);
},
stroke: function(ctx) {
canvasContexts[ctx].stroke();
canvasContexts[ctx].beginPath();
self.postMessage(["stroke", ctx]);
},

// Resources
Expand Down Expand Up @@ -256,7 +216,11 @@ const env = {

return slice.length;
},

yield: function() {
// TODO: use Atomics.wait to wait until there is an event (if step is Blocking)
// or to wait until requestAnimationFrame is called (if step is Asynchronous)
wait(32);
},
stopExecution: function() {
postMessage(["stopExecution"]);
},
Expand Down
28 changes: 17 additions & 11 deletions src/backends/wasm/capy.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,31 @@ function pushEvent(evt) {
}

async function pushAnswer(type, value) {
// Convert booleans to integers
if (value === true) value = 1;
if (value === false) value = 0;

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) {
const view = new Int32Array(pendingAnswer);
while (view[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);
view[1] = left;
view[2] = right;
view[0] = DONE;
if (Atomics.notify(view, 0) != 1) {
throw new Error("Expected 1 agent to be awoken.");
}
}

async function wait(msecs) {
Expand Down Expand Up @@ -158,12 +165,12 @@ const env = {
domObjects[root].style.height = "100%";
rootElementId = root;
},
setText: function(element, textPtr, textLen) {
setText: function(element, text) {
const elem = domObjects[element];
if (elem.nodeName === "INPUT") {
elem.value = readString(textPtr, textLen);
elem.value = text;
} else {
elem.innerText = readString(textPtr, textLen);
elem.innerText = text;
}
},
getTextLen: function(element) {
Expand Down Expand Up @@ -243,10 +250,10 @@ const env = {
canvas.width = window.devicePixelRatio * canvas.clientWidth;
canvas.height = window.devicePixelRatio * canvas.clientHeight;

for (ctxId in canvasContexts) {
for (let ctxId in canvasContexts) {
if (canvasContexts[ctxId].owner === element) {
canvasContexts[ctxId].clearRect(0, 0, canvas.width, canvas.height);
return ctxId;
return Number.parseInt(ctxId);
}
}
const ctx = canvas.getContext("2d");
Expand Down Expand Up @@ -369,7 +376,6 @@ const env = {
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];
Expand Down
1 change: 1 addition & 0 deletions src/backends/wasm/js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub extern fn getEventType(event: EventId) EventType;
pub extern fn getEventTarget(event: EventId) ElementId;
pub extern fn getEventArg(event: EventId, argIdx: usize) usize;
pub extern fn stopExecution() noreturn;
pub extern fn yield() void;

// Canvas related
pub extern fn openContext(element: ElementId) CanvasContextId;
Expand Down

0 comments on commit b8c7de8

Please sign in to comment.