Skip to content

Commit b8c7de8

Browse files
committed
wasm: Reduce CPU usage using Atomics.wait
1 parent fe92930 commit b8c7de8

File tree

4 files changed

+66
-89
lines changed

4 files changed

+66
-89
lines changed

src/backends/wasm/backend.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,11 @@ pub const Canvas = struct {
381381
js.rectPath(self.ctx, x, y, w, h);
382382
}
383383

384+
pub fn roundedRectangleEx(self: *DrawContext, x: i32, y: i32, w: u32, h: u32, cornerRadiuses: [4]f32) void {
385+
_ = cornerRadiuses;
386+
self.rectangle(x, y, w, h);
387+
}
388+
384389
pub fn text(self: *DrawContext, x: i32, y: i32, layout: TextLayout, str: []const u8) void {
385390
// TODO: layout
386391
_ = layout;
@@ -517,6 +522,7 @@ var stopExecution = false;
517522
// Temporary execution until async is added back in Zig
518523
pub fn runStep(step: shared.EventLoopStep) bool {
519524
_ = step;
525+
js.yield();
520526
while (js.hasEvent()) {
521527
const eventId = js.popEvent();
522528
switch (js.getEventType(eventId)) {

src/backends/wasm/capy-worker.js

Lines changed: 42 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -26,40 +26,45 @@ function readString(addr, len) {
2626
}
2727

2828
// 1 byte for making and 8 bytes for data (64 bits)
29-
let pendingAnswer = new SharedArrayBuffer(9);
29+
let pendingAnswer = new SharedArrayBuffer(12);
3030
/**
3131
@param {string} type The type of the answer, can only be "int"
3232
**/
3333
function waitForAnswer(type) {
3434
const WAITING = 0;
3535
const DONE = 1;
36-
37-
const view = new DataView(pendingAnswer);
38-
view.setUint8(0, WAITING);
39-
while (view.getUint8(0) != DONE) {
40-
wait(10);
36+
if (type == "bool") {
37+
return waitForAnswer("int") != 0;
4138
}
39+
40+
const view = new Int32Array(pendingAnswer);
41+
view[0] = WAITING;
42+
// while (view.getUint8(0) != DONE) {
43+
// wait(10);
44+
// }
45+
Atomics.wait(view, 0, WAITING);
4246

4347
switch (type) {
4448
case "int":
45-
const int = view.getUint32(5, true) << 32 | view.getUint32(1, true);
46-
console.log("Received answer " + int);
49+
const int = view[1] << 32 | view[2];
4750
return int;
4851
}
4952

5053
throw Error("Type invalid (" + type + ")");
5154
}
5255

56+
/**
57+
Shared array buffer used for sleeping with Atomics.wait()
58+
**/
59+
const AB = new Int32Array(new SharedArrayBuffer(4));
5360
/**
5461
@param {int} msecs Time to wait in milliseconds
5562
**/
5663
function wait(msecs) {
5764
const start = Date.now();
58-
while (Date.now() >= start + msecs) {
59-
// Wait.
65+
while (Date.now() <= start + msecs) {
66+
Atomics.wait(AB, 0, 0, msecs - (Date.now() - start));
6067
}
61-
62-
return;
6368
}
6469

6570
const memory = new WebAssembly.Memory({
@@ -114,99 +119,54 @@ const env = {
114119
return Date.now();
115120
},
116121
hasEvent: function() {
117-
return pendingEvents.length > 0;
122+
self.postMessage(["hasEvent"]);
123+
return waitForAnswer("bool");
118124
},
119125
popEvent: function() {
120-
if (pendingEvents.length > 0) {
121-
return pendingEvents.shift();
122-
} else {
123-
console.error("Popping event even though none is available!");
124-
}
126+
self.postMessage(["popEvent"]);
127+
return waitForAnswer("int");
125128
},
126129
getEventType: function(event) {
127-
return events[event].type;
130+
self.postMessage(["getEventType", event]);
131+
return waitForAnswer("int");
128132
},
129133
getEventTarget: function(event) {
130-
if (events[event].target === undefined) {
131-
console.error("Tried getting the target of a global event");
132-
}
133-
return events[event].target;
134+
self.postMessage(["getEventTarget", event]);
135+
return waitForAnswer("int");
134136
},
135137
getEventArg: function(event, idx) {
136-
if (events[event].args === undefined || events[event].args[idx] === undefined) {
137-
console.error("Tried getting non-existent arg:" + idx);
138-
}
139-
return events[event].args[idx];
138+
self.postMessage(["getEventArg", event, idx]);
139+
return waitForAnswer("int");
140140
},
141141

142142
// Canvas
143143
openContext: function(element) {
144-
const canvas = domObjects[element];
145-
canvas.width = window.devicePixelRatio * canvas.clientWidth;
146-
canvas.height = window.devicePixelRatio * canvas.clientHeight;
147-
148-
for (ctxId in canvasContexts) {
149-
if (canvasContexts[ctxId].owner === element) {
150-
canvasContexts[ctxId].clearRect(0, 0, canvas.width, canvas.height);
151-
return ctxId;
152-
}
153-
}
154-
const ctx = canvas.getContext("2d");
155-
ctx.owner = element;
156-
ctx.lineWidth = 2.5;
157-
ctx.beginPath();
158-
return canvasContexts.push(ctx) - 1;
144+
self.postMessage(["openContext", element]);
145+
return waitForAnswer("int");
159146
},
160147
setColor: function(ctx, r, g, b, a) {
161-
canvasContexts[ctx].fillStyle = "rgba(" + r + "," + g + "," + b + "," + a + ")";
162-
canvasContexts[ctx].strokeStyle = canvasContexts[ctx].fillStyle;
148+
self.postMessage(["setColor", ctx, r, g, b, a]);
163149
},
164150
rectPath: function(ctx, x, y, w, h) {
165-
canvasContexts[ctx].rect(x, y, w, h);
151+
self.postMessage(["rectPath", ctx, x, y, w, h]);
166152
},
167153
moveTo: function(ctx, x, y) {
168-
canvasContexts[ctx].moveTo(x, y);
154+
self.postMessage(["moveTo", ctx, x, y]);
169155
},
170156
lineTo: function(ctx, x, y) {
171-
canvasContexts[ctx].lineTo(x, y);
157+
self.postMessage(["lineTo", ctx, x, y]);
172158
},
173159
fillText: function(ctx, textPtr, textLen, x, y) {
174-
const text = readString(textPtr, textLen);
175-
canvasContexts[ctx].textAlign = "left";
176-
canvasContexts[ctx].textBaseline = "top";
177-
canvasContexts[ctx].fillText(text, x, y);
160+
throw new Error("TODO: fill text");
178161
},
179162
fillImage: function(ctx, img, x, y) {
180-
const canvas = canvasContexts[ctx];
181-
const image = resources[img];
182-
if (!image.imageDatas[ctx]) {
183-
image.imageDatas[ctx] = canvas.createImageData(image.width, image.height);
184-
const data = image.imageDatas[ctx].data;
185-
const Bpp = image.stride / image.width; // source bytes per pixel
186-
for (let y = 0; y < image.height; y++) {
187-
for (let x = 0; x < image.width; x++) {
188-
data[y*image.width*4+x*4+0] = image.bytes[y*image.stride+x*Bpp+0];
189-
data[y*image.width*4+x*4+1] = image.bytes[y*image.stride+x*Bpp+1];
190-
data[y*image.width*4+x*4+2] = image.bytes[y*image.stride+x*Bpp+2];
191-
if (!image.isRgb) {
192-
data[y*image.width*4+x*4+3] = image.bytes[y*image.stride+x*Bpp+3];
193-
} else {
194-
data[y*image.width*4+x*4+3] = 0xFF;
195-
}
196-
}
197-
}
198-
image.bytes = undefined; // try to free up some space
199-
resources[img] = image;
200-
}
201-
canvas.putImageData(image.imageDatas[ctx], x, y);
163+
self.postMessage(["fillImage", ctx, img, x, y]);
202164
},
203165
fill: function(ctx) {
204-
canvasContexts[ctx].fill();
205-
canvasContexts[ctx].beginPath();
166+
self.postMessage(["fill", ctx]);
206167
},
207168
stroke: function(ctx) {
208-
canvasContexts[ctx].stroke();
209-
canvasContexts[ctx].beginPath();
169+
self.postMessage(["stroke", ctx]);
210170
},
211171

212172
// Resources
@@ -256,7 +216,11 @@ const env = {
256216

257217
return slice.length;
258218
},
259-
219+
yield: function() {
220+
// TODO: use Atomics.wait to wait until there is an event (if step is Blocking)
221+
// or to wait until requestAnimationFrame is called (if step is Asynchronous)
222+
wait(32);
223+
},
260224
stopExecution: function() {
261225
postMessage(["stopExecution"]);
262226
},

src/backends/wasm/capy.js

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,31 @@ function pushEvent(evt) {
2424
}
2525

2626
async function pushAnswer(type, value) {
27+
// Convert booleans to integers
28+
if (value === true) value = 1;
29+
if (value === false) value = 0;
30+
2731
if (type == "int" && typeof value !== "number") {
2832
throw Error("Type mismatch, got " + (typeof value));
2933
}
3034

3135
const WAITING = 0;
3236
const DONE = 1;
33-
const view = new DataView(pendingAnswer);
34-
while (view.getUint8(0) != WAITING) {
37+
const view = new Int32Array(pendingAnswer);
38+
while (view[0] != WAITING) {
3539
// throw Error("Expected waiting state");
3640
await wait(100);
3741
console.log("Await waiting state");
3842
}
3943

4044
const left = value & 0xFFFFFFFF;
4145
const right = value >> 32;
42-
view.setUint32(1, left, true);
43-
view.setUint32(5, right, true);
44-
view.setInt8(0, DONE);
46+
view[1] = left;
47+
view[2] = right;
48+
view[0] = DONE;
49+
if (Atomics.notify(view, 0) != 1) {
50+
throw new Error("Expected 1 agent to be awoken.");
51+
}
4552
}
4653

4754
async function wait(msecs) {
@@ -158,12 +165,12 @@ const env = {
158165
domObjects[root].style.height = "100%";
159166
rootElementId = root;
160167
},
161-
setText: function(element, textPtr, textLen) {
168+
setText: function(element, text) {
162169
const elem = domObjects[element];
163170
if (elem.nodeName === "INPUT") {
164-
elem.value = readString(textPtr, textLen);
171+
elem.value = text;
165172
} else {
166-
elem.innerText = readString(textPtr, textLen);
173+
elem.innerText = text;
167174
}
168175
},
169176
getTextLen: function(element) {
@@ -243,10 +250,10 @@ const env = {
243250
canvas.width = window.devicePixelRatio * canvas.clientWidth;
244251
canvas.height = window.devicePixelRatio * canvas.clientHeight;
245252

246-
for (ctxId in canvasContexts) {
253+
for (let ctxId in canvasContexts) {
247254
if (canvasContexts[ctxId].owner === element) {
248255
canvasContexts[ctxId].clearRect(0, 0, canvas.width, canvas.height);
249-
return ctxId;
256+
return Number.parseInt(ctxId);
250257
}
251258
}
252259
const ctx = canvas.getContext("2d");
@@ -369,7 +376,6 @@ const env = {
369376
const wasmWorker = new Worker("capy-worker.js");
370377
wasmWorker.postMessage("test");
371378
wasmWorker.onmessage = (e) => {
372-
console.log("message", e.data);
373379
const name = e.data[0];
374380
if (name === "setBuffer") {
375381
arrayBuffer = e.data[1];

src/backends/wasm/js.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub extern fn getEventType(event: EventId) EventType;
3131
pub extern fn getEventTarget(event: EventId) ElementId;
3232
pub extern fn getEventArg(event: EventId, argIdx: usize) usize;
3333
pub extern fn stopExecution() noreturn;
34+
pub extern fn yield() void;
3435

3536
// Canvas related
3637
pub extern fn openContext(element: ElementId) CanvasContextId;

0 commit comments

Comments
 (0)