summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMitya Selivanov <automainint@guattari.tech>2025-01-06 23:41:27 +0100
committerMitya Selivanov <automainint@guattari.tech>2025-01-06 23:41:27 +0100
commiteab2433a2daee814492548b95f16892adde0908d (patch)
treee08d2b82fc3525d937bb64475da5543b4628158c
parent3359f068dc9e8ac036f0f709aeccf11dfba3cf03 (diff)
downloadreduced_system_layer-eab2433a2daee814492548b95f16892adde0908d.zip
Impl rendering in AudioWorklet
-rw-r--r--Dockerfile2
-rwxr-xr-xexamples/graph.c2
-rw-r--r--index.htm470
-rw-r--r--index.htm.bak415
-rwxr-xr-xreduced_system_layer.c180
5 files changed, 763 insertions, 306 deletions
diff --git a/Dockerfile b/Dockerfile
index 7054730..bbe548b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,7 +5,7 @@ RUN apk add clang lld
COPY examples /usr/examples
COPY reduced_system_layer.c /usr/reduced_system_layer.c
COPY graphics.c /usr/graphics.c
-RUN clang --target=wasm32 -nostdlib -fno-builtin -Wl,--no-entry,--allow-undefined -o /usr/index.wasm /usr/examples/graph.c
+RUN clang --target=wasm32 -nostdlib -fno-builtin -mbulk-memory -Wl,--no-entry,--allow-undefined -o /usr/index.wasm /usr/examples/graph.c
FROM nginx:alpine
EXPOSE 80
diff --git a/examples/graph.c b/examples/graph.c
index dc4b226..6ad5fdc 100755
--- a/examples/graph.c
+++ b/examples/graph.c
@@ -531,7 +531,7 @@ void update_and_render_frame(void) {
// Render
- fill_rectangle(OP_SET, 0xffffff, 0, 0, platform.frame_width, platform.frame_height);
+ fill_rectangle(OP_SET, 0xa0b0c0, 0, 0, platform.frame_width, platform.frame_height);
if (adding_edge) {
f64 x0 = world.nodes[adding_src].x;
diff --git a/index.htm b/index.htm
index cb1f474..7f82840 100644
--- a/index.htm
+++ b/index.htm
@@ -10,46 +10,149 @@
<canvas style="margin: 0; width: 100%; height: 100%;" id="frame" oncontextmenu="event.preventDefault()"></canvas>
<script type="worklet">
registerProcessor(
- 'Sound_Ring',
+ 'Sound_Node',
class Sound_Ring extends AudioWorkletProcessor {
constructor(options) {
super();
- this.ring = options.processorOptions.ring;
+ this.frame_width = 0;
+ this.frame_height = 0;
+
+ let event_handlers = {
+ wasm : (ev) => {
+ WebAssembly.instantiate(
+ ev.wasm,
+ {
+ wasi_snapshot_preview1 : {
+ clock_time_get : () => { return Date.now(); },
+ args_sizes_get : () => { throw new Error("Unexpected args_sizes_get call"); },
+ args_get : () => { throw new Error("Unexpected args_get call"); },
+ proc_exit : () => { throw new Error("Unexpected proc_exit call"); },
+ fd_close : () => { throw new Error("Unexpected fd_close call"); },
+ fd_write : () => { throw new Error("Unexpected fd_write call"); },
+ fd_seek : () => { throw new Error("Unexpected fd_seek call"); },
+ },
+ env : {
+ p_clipboard_write : (size, text) => {
+ this.port.postMessage({
+ id : "clipboard",
+ text : new TextDecoder("utf8").decode(new Uint8Array(this.memory_buffer, text, size)), // FIXME
+ });
+ },
+ p_time_impl : Date.now,
+
+ floor : Math.floor,
+ ceil : Math.ceil,
+ sqrt : Math.sqrt,
+ pow : Math.pow,
+ log : Math.log,
+ log2 : Math.log2,
+ log10 : Math.log10,
+ exp : Math.exp,
+ sin : Math.sin,
+ cos : Math.cos,
+ tan : Math.tan,
+ asin : Math.asin,
+ acos : Math.acos,
+ atan : Math.atan,
+ atan2 : Math.atan2,
+ },
+ }
+ ).then((program) => {
+ program.exports.js_main(options.processorOptions.href);
+
+ this.memory_buffer = program.exports.memory.buffer;
+ this.sound_buffer_address = program.exports.js_sound_buffer();
+ this.pixels_address = program.exports.js_pixels();
+ this.frames = new Float32Array(this.memory_buffer, this.sound_buffer_address, program.exports.js_max_num_audio_frames());
+
+ let bytes = new Uint8Array(this.memory_buffer);
+ let title_address = program.exports.js_title();
+ let len = 0;
+ while (bytes[title_address + len] != 0)
+ ++len;
+ let title_buffer = new ArrayBuffer(len);
+ new Uint8Array(title_buffer).set(bytes.subarray(title_address, title_address + len));
+
+ this.port.postMessage({
+ id : "init",
+ title_buffer : title_buffer,
+ });
+
+ this.program = program;
+ });
+ },
+
+ resize : (ev) => {
+ this.frame_width = ev.width;
+ this.frame_height = ev.height;
+ },
+
+ mousedown : (ev) => {
+ if (this.program === undefined)
+ return;
+
+ this.program.exports.js_mousedown(ev.buttons);
+ },
- this.position = 0;
- this.max_num_frames = this.ring.byteLength / 4;
+ mouseup : (ev) => {
+ if (this.program === undefined)
+ return;
+
+ this.program.exports.js_mouseup(ev.buttons);
+ },
+
+ mousemove : (ev) => {
+ if (this.program === undefined)
+ return;
+
+ this.program.exports.js_mousemove(ev.x, ev.y);
+ },
+
+ keydown : (ev) => {
+ if (this.program === undefined)
+ return;
+
+ this.program.exports.js_keydown(ev.key, ev.mod);
+ },
+
+ keyup : (ev) => {
+ if (this.program === undefined)
+ return;
+
+ this.program.exports.js_keyup(ev.key, ev.mod);
+ },
+ };
+
+ this.port.onmessage = (ev) => {
+ if (ev.data.id in event_handlers)
+ event_handlers[ev.data.id](ev.data);
+ };
}
process(input, output, parameters) {
+ if (this.program === undefined)
+ return true;
+
let num_channels = output[0].length;
let num_samples = output[0][0].length;
let num_frames = num_samples * num_channels;
- if (num_frames > this.max_num_frames) {
- console.error("Sound buffer overflow");
- return true;
- }
+ this.program.exports.js_frame(this.frame_width, this.frame_height, num_samples);
- let frames = new Float32Array(this.ring);
-
- if (num_frames <= this.max_num_frames - this.position)
- for (let j = 0; j < num_channels; ++j)
- for (let i = 0; i < num_samples; ++i)
- output[0][j][i] = frames[this.position + i * num_channels + j];
- else {
- let part_one = this.max_num_frames - this.position;
- let part_two = num_frames - part_one;
-
- for (let j = 0; j < num_channels; ++j) {
- for (let i = 0; i < part_one; ++i)
- output[0][j][i] = frames[this.position + i * num_channels + j];
- for (let i = 0; i < part_two; ++i)
- output[0][j][part_one + i] = frames[i * num_channels + j];
- }
- }
+ let pixels_size = this.frame_width * this.frame_height * 4;
+ let pixels_buffer = new ArrayBuffer(pixels_size);
+ new Uint8Array(pixels_buffer).set(new Uint8Array(this.memory_buffer, this.pixels_address, pixels_size));
+
+ this.port.postMessage({
+ id : "frame",
+ pixels_buffer : pixels_buffer,
+ });
+
+ for (let j = 0; j < num_channels; ++j)
+ for (let i = 0; i < num_samples; ++i)
+ output[0][j][i] = this.frames[j * num_samples + i];
- this.position = (this.position + num_frames) % this.max_num_frames;
return true;
}
}
@@ -170,44 +273,75 @@
return mod;
};
- function string_from_memory(bytes, offset) {
- let len = 0;
- while (bytes[offset + len] != 0)
- ++len;
- return new TextDecoder("utf8").decode(bytes.subarray(offset, offset + len));
- }
-
async function run(attrs) {
- let wait_for_events = false;
- let sleep_duration = 0;
+ let sound_sample_rate = 44100; // TODO
+ let sound_num_channels = 2; // TODO
+
+ let frame_width = 0;
+ let frame_height = 0;
let canvas;
- let program;
- let memory;
- let pixels_address;
let context;
+ let pixels_buffer;
let animation_frame;
let sound_ready = false;
- let sound_context;
let sound_init;
+ let sound_context;
let sound_node;
- let sound_num_channels;
- let sound_buffer_address;
- let sound_max_num_frames;
-
- let sound_shared_ring;
- let sound_position;
canvas = attrs.canvas;
context = canvas.getContext("2d");
- sound_init = async () => {
- if (sound_ready || sound_context !== undefined)
- return;
+ context.fillStyle = "#000";
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ context.fillStyle = "#fff";
+ context.font = "1.7em serif";
+ context.textAlign = "center";
+ context.textBaseline = "middle";
+ context.fillText("Click to run", canvas.width / 2.0, canvas.height / 2.0);
+
+ animation_frame = () => {
+ if (attrs.fit_window) {
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+ }
+
+ if (frame_width != canvas.width || frame_height != canvas.height) {
+ frame_width = canvas.width;
+ frame_height = canvas.height;
+
+ sound_node.port.postMessage({
+ id : "resize",
+ width : frame_width,
+ height : frame_height,
+ });
+ }
+
+ let num_bytes = 4 * canvas.width * canvas.height;
+
+ if (pixels_buffer !== undefined && pixels_buffer.byteLength >= num_bytes) {
+ let pixels = new Uint8ClampedArray(pixels_buffer, 0, num_bytes);
+
+ let data = new ImageData(
+ pixels,
+ canvas.width,
+ canvas.height
+ );
+
+ context.putImageData(data, 0, 0);
+ } else {
+ context.fillStyle = "#000";
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ }
+
+ window.requestAnimationFrame(animation_frame);
+ };
+
+ sound_init = async () => {
sound_context = new AudioContext({
- sampleRate : program.instance.exports.js_sample_rate(),
+ sampleRate : sound_sample_rate,
});
let blob = new Blob(
@@ -217,192 +351,114 @@
await sound_context.audioWorklet.addModule(URL.createObjectURL(blob));
- sound_num_channels = program.instance.exports.js_num_channels();
- sound_max_num_frames = program.instance.exports.js_max_num_audio_frames();
- sound_buffer_address = program.instance.exports.js_sound_buffer();
-
- sound_shared_ring = new SharedArrayBuffer(sound_max_num_frames * 4);
- sound_position = 0;
-
sound_node = new AudioWorkletNode(
sound_context,
- "Sound_Ring",
- { numberOfInputs : 0,
- outputChannelCount : [ sound_num_channels ],
- processorOptions : { ring : sound_shared_ring, }, }
+ "Sound_Node",
+ { numberOfInputs : 0,
+ outputChannelCount : [ sound_num_channels ],
+ processorOptions : { href : document.location.href }, }
);
sound_node.connect(sound_context.destination);
- program.instance.exports.js_sound_sync();
-
- sound_ready = true;
- };
+ let message_handlers = {
+ init : (ev) => {
+ document.title = new TextDecoder("utf8").decode(new Uint8Array(ev.title_buffer));
+ sound_ready = true;
- program = await WebAssembly.instantiateStreaming(
- attrs.wasm,
- {
- "wasi_snapshot_preview1" : {
- clock_time_get : () => { return Date.now(); },
- args_sizes_get : () => { throw new Error("Unexpected args_sizes_get call"); },
- args_get : () => { throw new Error("Unexpected args_get call"); },
- proc_exit : () => { throw new Error("Unexpected proc_exit call"); },
- fd_close : () => { throw new Error("Unexpected fd_close call"); },
- fd_write : () => { throw new Error("Unexpected fd_write call"); },
- fd_seek : () => { throw new Error("Unexpected fd_seek call"); },
+ window.requestAnimationFrame(animation_frame);
},
- "env" : {
- p_init : () => {
- program.instance.exports.js_init();
- memory = new Uint8Array(program.instance.exports.memory.buffer);
- pixels_address = program.instance.exports.js_pixels();
- document.title = string_from_memory(memory, program.instance.exports.js_title());
- },
- p_render_frame_impl : () => {
- let data = new ImageData(
- new Uint8ClampedArray(
- memory.subarray(
- pixels_address,
- pixels_address + 4 * canvas.width * canvas.height
- )
- ),
- canvas.width,
- canvas.height
- );
-
- context.putImageData(data, 0, 0);
- },
- p_clipboard_write : (size, text) => {
- navigator.clipboard.writeText(
- new TextDecoder().decode(memory.subarray(text, text + size))
- ).catch((e) => { console.error(e); });
- },
- p_cleanup : () => {},
- p_wait_events_impl : () => { wait_for_events = true; },
- p_sleep_for_impl : (time) => { sleep_duration += time; },
- p_time_impl : Date.now,
-
- p_handle_audio_impl : (samples_elapsed) => {
- if (!sound_ready)
- return;
-
- let num_frames = samples_elapsed * sound_num_channels;
-
- if (num_frames > sound_max_num_frames) {
- console.error("Sound buffer overflow");
- return;
- }
-
- let dst = new Float32Array(sound_shared_ring);
- let src = new Float32Array(memory.buffer, sound_buffer_address, sound_max_num_frames);
-
- if (num_frames <= sound_max_num_frames - sound_position)
- for (let i = 0; i < num_frames; ++i) {
- dst[sound_position + i] = src[sound_position + i];
- src[sound_position + i] = 0.0;
- }
- else {
- let part_one = sound_max_num_frames - sound_position;
- let part_two = num_frames - part_one;
- for (let i = 0; i < part_one; ++i) {
- dst[sound_position + i] = src[sound_position + i];
- src[sound_position + i] = 0.0;
- }
- for (let i = 0; i < part_two; ++i) {
- dst[i] = src[i];
- src[i] = 0.0;
- }
- }
-
- sound_position = (sound_position + samples_elapsed * sound_num_channels) % sound_max_num_frames;
- },
-
- floor : Math.floor,
- ceil : Math.ceil,
- sqrt : Math.sqrt,
- pow : Math.pow,
- log : Math.log,
- log2 : Math.log2,
- log10 : Math.log10,
- exp : Math.exp,
- sin : Math.sin,
- cos : Math.cos,
- tan : Math.tan,
- asin : Math.asin,
- acos : Math.acos,
- atan : Math.atan,
- atan2 : Math.atan2,
+ clipboard : (ev) => {
+ navigator.clipboard.writeText(ev.text);
},
- }
- );
- program.instance.exports.js_main(document.location.href);
+ frame : (ev) => {
+ pixels_buffer = ev.pixels_buffer;
+ let pixels = new Uint8Array(pixels_buffer);
+ }
+ };
- animation_frame = (time) => {
- if (attrs.fit_window) {
- canvas.width = window.innerWidth;
- canvas.height = window.innerHeight;
- }
+ sound_node.port.onmessage = (ev) => {
+ if (ev.data.id in message_handlers)
+ message_handlers[ev.data.id](ev.data);
+ };
- wait_for_events = false;
- program.instance.exports.js_frame(canvas.width, canvas.height);
-
- if (!wait_for_events) {
- if (sleep_duration > 0) {
- let timeout = sleep_duration;
- sleep_duration = 0;
- setTimeout(() => {
- window.requestAnimationFrame(animation_frame);
- }, timeout);
- } else
- window.requestAnimationFrame(animation_frame);
- }
+ sound_node.port.postMessage({
+ id : "wasm",
+ wasm : await WebAssembly.compileStreaming(attrs.wasm),
+ });
};
- window.addEventListener("resize", (ev) => {
- window.requestAnimationFrame(animation_frame);
- });
-
canvas.addEventListener("mousedown", (ev) => {
- sound_init();
- program.instance.exports.js_mousedown(ev.buttons);
- window.requestAnimationFrame(animation_frame);
+ if (!sound_ready) {
+ sound_init();
+ sound_init = () => {};
+ return;
+ }
+
+ sound_node.port.postMessage({
+ id : "mousedown",
+ buttons : ev.buttons,
+ });
});
canvas.addEventListener("mouseup", (ev) => {
- program.instance.exports.js_mouseup(ev.buttons);
- window.requestAnimationFrame(animation_frame);
+ if (!sound_ready)
+ return;
+
+ sound_node.port.postMessage({
+ id : "mouseup",
+ buttons : ev.buttons,
+ });
});
canvas.addEventListener("mousemove", (ev) => {
- program.instance.exports.js_mousemove(ev.clientX, ev.clientY);
- window.requestAnimationFrame(animation_frame);
+ if (!sound_ready)
+ return;
+
+ sound_node.port.postMessage({
+ id : "mousemove",
+ x : ev.clientX,
+ y : ev.clientY,
+ });
});
- window.addEventListener("keydown", (ev) => {
+ canvas.addEventListener("keydown", (ev) => {
ev.preventDefault();
- if (!ev.repeat) {
- let mod = mod_from_event(ev);
- let key = key_from_code(ev.code);
- if (key != 0)
- program.instance.exports.js_keydown(key, mod);
- window.requestAnimationFrame(animation_frame);
- }
+ if (!sound_ready)
+ return;
+ if (ev.repeat)
+ return;
+ let mod = mod_from_event(ev);
+ let key = key_from_code(ev.code);
+ if (key == 0)
+ return;
+
+ sound_node.port.postMessage({
+ id : "keydown",
+ mod : mod,
+ key : key,
+ });
});
- window.addEventListener("keyup", (ev) => {
+ canvas.addEventListener("keyup", (ev) => {
ev.preventDefault();
- if (!ev.repeat) {
- let mod = mod_from_event(ev);
- let key = key_from_code(ev.code);
- if (key != 0)
- program.instance.exports.js_keyup(key, mod);
- window.requestAnimationFrame(animation_frame);
- }
- });
+ if (!sound_ready)
+ return;
+ if (ev.repeat)
+ return;
+ let mod = mod_from_event(ev);
+ let key = key_from_code(ev.code);
+ if (key == 0)
+ return;
- window.requestAnimationFrame(animation_frame);
+ sound_node.port.postMessage({
+ id : "keyup",
+ mod : mod,
+ key : key,
+ });
+ });
}
run({
@@ -412,4 +468,4 @@
}).catch((e) => console.error(e));
</script>
</body>
-</html>
+</html>
diff --git a/index.htm.bak b/index.htm.bak
new file mode 100644
index 0000000..cb1f474
--- /dev/null
+++ b/index.htm.bak
@@ -0,0 +1,415 @@
+<!DOCTYPE html>
+<html style="height: 100%; overflow: hidden; overflow-x: hidden;">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <link rel='icon' type='image/gif' href='/favicon.gif'>
+ <title> Reduced System Layer </title>
+ </head>
+ <body style="margin: 0; height: 100%; overflow: hidden; overflow-x: hidden;">
+ <canvas style="margin: 0; width: 100%; height: 100%;" id="frame" oncontextmenu="event.preventDefault()"></canvas>
+ <script type="worklet">
+ registerProcessor(
+ 'Sound_Ring',
+ class Sound_Ring extends AudioWorkletProcessor {
+ constructor(options) {
+ super();
+
+ this.ring = options.processorOptions.ring;
+
+ this.position = 0;
+ this.max_num_frames = this.ring.byteLength / 4;
+ }
+
+ process(input, output, parameters) {
+ let num_channels = output[0].length;
+ let num_samples = output[0][0].length;
+ let num_frames = num_samples * num_channels;
+
+ if (num_frames > this.max_num_frames) {
+ console.error("Sound buffer overflow");
+ return true;
+ }
+
+ let frames = new Float32Array(this.ring);
+
+ if (num_frames <= this.max_num_frames - this.position)
+ for (let j = 0; j < num_channels; ++j)
+ for (let i = 0; i < num_samples; ++i)
+ output[0][j][i] = frames[this.position + i * num_channels + j];
+ else {
+ let part_one = this.max_num_frames - this.position;
+ let part_two = num_frames - part_one;
+
+ for (let j = 0; j < num_channels; ++j) {
+ for (let i = 0; i < part_one; ++i)
+ output[0][j][i] = frames[this.position + i * num_channels + j];
+ for (let i = 0; i < part_two; ++i)
+ output[0][j][part_one + i] = frames[i * num_channels + j];
+ }
+ }
+
+ this.position = (this.position + num_frames) % this.max_num_frames;
+ return true;
+ }
+ }
+ );
+ </script>
+ <script type="text/javascript">
+ let key_map = {
+ "Backspace" : 8,
+ "Tab" : 9,
+ "Enter" : 10,
+ "ControlLeft" : 11,
+ "ControlRight" : 12,
+ "ShiftLeft" : 13,
+ "ShiftRight" : 14,
+ "AltLeft" : 15,
+ "AltRight" : 16,
+ "ArrowLeft" : 17,
+ "ArrowRight" : 18,
+ "ArrowUp" : 19,
+ "ArrowDown" : 20,
+ "Pause" : 21,
+ "Insert" : 22,
+ "Home" : 23,
+ "End" : 24,
+ "PageUp" : 25,
+ "PageDown" : 26,
+ "Escape" : 27,
+ "PrintScreen" : 28,
+ "Space" : 32,
+ "MetaLeft" : 33,
+ "MetaRight" : 34,
+ "Quote" : 39,
+ "Comma" : 44,
+ "Minus" : 45,
+ "Period" : 46,
+ "Slash" : 47,
+ "Digit0" : 48,
+ "Digit1" : 49,
+ "Digit2" : 50,
+ "Digit3" : 51,
+ "Digit4" : 52,
+ "Digit5" : 53,
+ "Digit6" : 54,
+ "Digit7" : 55,
+ "Digit8" : 56,
+ "Digit9" : 57,
+ "Semicolon" : 59,
+ "Equal" : 61,
+ "BracketLeft" : 91,
+ "Backslash" : 92,
+ "BracketRight" : 93,
+ "Backquote" : 96,
+ "KeyA" : 97,
+ "KeyB" : 98,
+ "KeyC" : 99,
+ "KeyD" : 100,
+ "KeyE" : 101,
+ "KeyF" : 102,
+ "KeyG" : 103,
+ "KeyH" : 104,
+ "KeyI" : 105,
+ "KeyJ" : 106,
+ "KeyK" : 107,
+ "KeyL" : 108,
+ "KeyM" : 109,
+ "KeyN" : 110,
+ "KeyO" : 111,
+ "KeyP" : 112,
+ "KeyQ" : 113,
+ "KeyR" : 114,
+ "KeyS" : 115,
+ "KeyT" : 116,
+ "KeyU" : 117,
+ "KeyV" : 118,
+ "KeyW" : 119,
+ "KeyX" : 120,
+ "KeyY" : 121,
+ "KeyZ" : 122,
+ "Delete" : 127,
+ "F1" : 145,
+ "F2" : 146,
+ "F3" : 147,
+ "F4" : 148,
+ "F5" : 149,
+ "F6" : 150,
+ "F7" : 151,
+ "F8" : 152,
+ "F9" : 153,
+ "F10" : 154,
+ "F11" : 155,
+ "F12" : 156,
+ "F13" : 157,
+ "F14" : 158,
+ "F15" : 159,
+ "F16" : 160,
+ "F17" : 161,
+ "F18" : 162,
+ "F19" : 163,
+ "F20" : 164,
+ "F21" : 165,
+ "F22" : 166,
+ "F23" : 167,
+ "F24" : 168,
+ };
+
+ function key_from_code(code) {
+ if (code in key_map)
+ return key_map[code];
+ return 0;
+ };
+
+ function mod_from_event(ev) {
+ let mod = 0;
+ if (ev.ctrlKey) mod |= 1;
+ if (ev.shiftKey) mod |= 2;
+ if (ev.altKey) mod |= 4;
+ if (ev.metaKey) mod |= 8;
+ return mod;
+ };
+
+ function string_from_memory(bytes, offset) {
+ let len = 0;
+ while (bytes[offset + len] != 0)
+ ++len;
+ return new TextDecoder("utf8").decode(bytes.subarray(offset, offset + len));
+ }
+
+ async function run(attrs) {
+ let wait_for_events = false;
+ let sleep_duration = 0;
+
+ let canvas;
+ let program;
+ let memory;
+ let pixels_address;
+ let context;
+ let animation_frame;
+
+ let sound_ready = false;
+ let sound_context;
+ let sound_init;
+ let sound_node;
+ let sound_num_channels;
+ let sound_buffer_address;
+ let sound_max_num_frames;
+
+ let sound_shared_ring;
+ let sound_position;
+
+ canvas = attrs.canvas;
+ context = canvas.getContext("2d");
+
+ sound_init = async () => {
+ if (sound_ready || sound_context !== undefined)
+ return;
+
+ sound_context = new AudioContext({
+ sampleRate : program.instance.exports.js_sample_rate(),
+ });
+
+ let blob = new Blob(
+ [ document.querySelector("script[type=worklet]").innerText ],
+ { type : "application/javascript", }
+ );
+
+ await sound_context.audioWorklet.addModule(URL.createObjectURL(blob));
+
+ sound_num_channels = program.instance.exports.js_num_channels();
+ sound_max_num_frames = program.instance.exports.js_max_num_audio_frames();
+ sound_buffer_address = program.instance.exports.js_sound_buffer();
+
+ sound_shared_ring = new SharedArrayBuffer(sound_max_num_frames * 4);
+ sound_position = 0;
+
+ sound_node = new AudioWorkletNode(
+ sound_context,
+ "Sound_Ring",
+ { numberOfInputs : 0,
+ outputChannelCount : [ sound_num_channels ],
+ processorOptions : { ring : sound_shared_ring, }, }
+ );
+
+ sound_node.connect(sound_context.destination);
+
+ program.instance.exports.js_sound_sync();
+
+ sound_ready = true;
+ };
+
+ program = await WebAssembly.instantiateStreaming(
+ attrs.wasm,
+ {
+ "wasi_snapshot_preview1" : {
+ clock_time_get : () => { return Date.now(); },
+ args_sizes_get : () => { throw new Error("Unexpected args_sizes_get call"); },
+ args_get : () => { throw new Error("Unexpected args_get call"); },
+ proc_exit : () => { throw new Error("Unexpected proc_exit call"); },
+ fd_close : () => { throw new Error("Unexpected fd_close call"); },
+ fd_write : () => { throw new Error("Unexpected fd_write call"); },
+ fd_seek : () => { throw new Error("Unexpected fd_seek call"); },
+ },
+ "env" : {
+ p_init : () => {
+ program.instance.exports.js_init();
+ memory = new Uint8Array(program.instance.exports.memory.buffer);
+ pixels_address = program.instance.exports.js_pixels();
+ document.title = string_from_memory(memory, program.instance.exports.js_title());
+ },
+ p_render_frame_impl : () => {
+ let data = new ImageData(
+ new Uint8ClampedArray(
+ memory.subarray(
+ pixels_address,
+ pixels_address + 4 * canvas.width * canvas.height
+ )
+ ),
+ canvas.width,
+ canvas.height
+ );
+
+ context.putImageData(data, 0, 0);
+ },
+ p_clipboard_write : (size, text) => {
+ navigator.clipboard.writeText(
+ new TextDecoder().decode(memory.subarray(text, text + size))
+ ).catch((e) => { console.error(e); });
+ },
+ p_cleanup : () => {},
+ p_wait_events_impl : () => { wait_for_events = true; },
+ p_sleep_for_impl : (time) => { sleep_duration += time; },
+ p_time_impl : Date.now,
+
+ p_handle_audio_impl : (samples_elapsed) => {
+ if (!sound_ready)
+ return;
+
+ let num_frames = samples_elapsed * sound_num_channels;
+
+ if (num_frames > sound_max_num_frames) {
+ console.error("Sound buffer overflow");
+ return;
+ }
+
+ let dst = new Float32Array(sound_shared_ring);
+ let src = new Float32Array(memory.buffer, sound_buffer_address, sound_max_num_frames);
+
+ if (num_frames <= sound_max_num_frames - sound_position)
+ for (let i = 0; i < num_frames; ++i) {
+ dst[sound_position + i] = src[sound_position + i];
+ src[sound_position + i] = 0.0;
+ }
+ else {
+ let part_one = sound_max_num_frames - sound_position;
+ let part_two = num_frames - part_one;
+
+ for (let i = 0; i < part_one; ++i) {
+ dst[sound_position + i] = src[sound_position + i];
+ src[sound_position + i] = 0.0;
+ }
+ for (let i = 0; i < part_two; ++i) {
+ dst[i] = src[i];
+ src[i] = 0.0;
+ }
+ }
+
+ sound_position = (sound_position + samples_elapsed * sound_num_channels) % sound_max_num_frames;
+ },
+
+ floor : Math.floor,
+ ceil : Math.ceil,
+ sqrt : Math.sqrt,
+ pow : Math.pow,
+ log : Math.log,
+ log2 : Math.log2,
+ log10 : Math.log10,
+ exp : Math.exp,
+ sin : Math.sin,
+ cos : Math.cos,
+ tan : Math.tan,
+ asin : Math.asin,
+ acos : Math.acos,
+ atan : Math.atan,
+ atan2 : Math.atan2,
+ },
+ }
+ );
+
+ program.instance.exports.js_main(document.location.href);
+
+ animation_frame = (time) => {
+ if (attrs.fit_window) {
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+ }
+
+ wait_for_events = false;
+ program.instance.exports.js_frame(canvas.width, canvas.height);
+
+ if (!wait_for_events) {
+ if (sleep_duration > 0) {
+ let timeout = sleep_duration;
+ sleep_duration = 0;
+ setTimeout(() => {
+ window.requestAnimationFrame(animation_frame);
+ }, timeout);
+ } else
+ window.requestAnimationFrame(animation_frame);
+ }
+ };
+
+ window.addEventListener("resize", (ev) => {
+ window.requestAnimationFrame(animation_frame);
+ });
+
+ canvas.addEventListener("mousedown", (ev) => {
+ sound_init();
+ program.instance.exports.js_mousedown(ev.buttons);
+ window.requestAnimationFrame(animation_frame);
+ });
+
+ canvas.addEventListener("mouseup", (ev) => {
+ program.instance.exports.js_mouseup(ev.buttons);
+ window.requestAnimationFrame(animation_frame);
+ });
+
+ canvas.addEventListener("mousemove", (ev) => {
+ program.instance.exports.js_mousemove(ev.clientX, ev.clientY);
+ window.requestAnimationFrame(animation_frame);
+ });
+
+ window.addEventListener("keydown", (ev) => {
+ ev.preventDefault();
+ if (!ev.repeat) {
+ let mod = mod_from_event(ev);
+ let key = key_from_code(ev.code);
+ if (key != 0)
+ program.instance.exports.js_keydown(key, mod);
+ window.requestAnimationFrame(animation_frame);
+ }
+ });
+
+ window.addEventListener("keyup", (ev) => {
+ ev.preventDefault();
+ if (!ev.repeat) {
+ let mod = mod_from_event(ev);
+ let key = key_from_code(ev.code);
+ if (key != 0)
+ program.instance.exports.js_keyup(key, mod);
+ window.requestAnimationFrame(animation_frame);
+ }
+ });
+
+ window.requestAnimationFrame(animation_frame);
+ }
+
+ run({
+ canvas : document.getElementById("frame"),
+ wasm : fetch("index.wasm"),
+ fit_window : true,
+ }).catch((e) => console.error(e));
+ </script>
+ </body>
+</html>
diff --git a/reduced_system_layer.c b/reduced_system_layer.c
index a5e63e0..587b319 100755
--- a/reduced_system_layer.c
+++ b/reduced_system_layer.c
@@ -77,7 +77,7 @@
#/
#/ ----------------------------------------------------------------
#/
-#/ (C) 2024 Mitya Selivanov <guattari.tech>
+#/ (C) 2025 Mitya Selivanov <guattari.tech>
#/
#/ ================================================================
#/
@@ -1559,17 +1559,22 @@ void p_clipboard_write(i64 size, c8 *data) {
// ================================================================
//
-// Web canvas
+// WebAssembly
//
// ================================================================
#ifdef __wasm__
-static u32 _buffer[MAX_NUM_PIXELS] = {0};
-static Input_Key _input[MAX_INPUT_SIZE] = {0};
-static i32 _num_events = 0;
-static i32 _input_size = 0;
-static b8 _key_pressed[MAX_NUM_KEYS] = {0};
+static u32 _buffer[MAX_NUM_PIXELS] = {0};
+static Input_Key _input[MAX_INPUT_SIZE] = {0};
+static i32 _num_events = 0;
+static i32 _input_size = 0;
+static b8 _key_pressed[MAX_NUM_KEYS] = {0};
+static b8 _wait_events = 0;
+i64 _sound_position = 0;
+i64 _sound_read = 0;
+f32 _sound_ring[MAX_NUM_AUDIO_FRAMES] = {0};
+f32 _sound_buffer[MAX_NUM_AUDIO_FRAMES] = {0};
i32 p_time_impl(void);
@@ -1577,10 +1582,17 @@ i64 p_time(void) {
return p_time_impl();
}
-void p_sleep_for_impl(i32 duration);
-
void p_sleep_for(i64 duration) {
- p_sleep_for_impl((i32) duration);
+ // TODO
+}
+
+void p_init(void) {
+ ++_num_events;
+ platform.pixels = _buffer;
+ platform.input = _input;
+ platform.done = 1;
+ _sound_position = AUDIO_AVAIL_MIN % MAX_NUM_AUDIO_FRAMES;
+ _sound_read = 0;
}
i32 p_handle_events(void) {
@@ -1595,33 +1607,56 @@ i32 p_handle_events(void) {
return n;
}
-i32 p_wait_events_impl(void);
-
i32 p_wait_events(void) {
- p_wait_events_impl();
+ _wait_events = 1;
+
return p_handle_events();
}
-void p_render_frame_impl(void);
+void p_render_frame(void) { }
-void p_render_frame(void) {
- // Make canvas pixels opaque.
- i64 size = platform.frame_width * platform.frame_height;
- for (i64 i = 0; i < size; ++i)
- platform.pixels[i] |= 0xff000000;
+void p_handle_audio(i64 samples_elapsed) {
+ if (samples_elapsed <= 0)
+ return;
- p_render_frame_impl();
+ _sound_position = (_sound_position + samples_elapsed * AUDIO_NUM_CHANNELS) % MAX_NUM_AUDIO_FRAMES;
}
-__attribute__((export_name("js_main"))) void js_main(c8 *href) {
- main(1, &href);
+void p_queue_sound(i64 delay_in_samples, i64 num_samples, f32 *frames) {
+ if (frames == NULL)
+ return;
+
+ if (delay_in_samples < 0) {
+ frames += -delay_in_samples * AUDIO_NUM_CHANNELS;
+ num_samples -= delay_in_samples;
+ delay_in_samples = 0;
+ }
+
+ if (num_samples <= 0)
+ return;
+
+ i64 num_frames = num_samples * AUDIO_NUM_CHANNELS;
+ if (num_frames > MAX_NUM_AUDIO_FRAMES)
+ return;
+
+ i64 begin = (_sound_position + delay_in_samples * AUDIO_NUM_CHANNELS) % MAX_NUM_AUDIO_FRAMES;
+
+ if (num_frames <= MAX_NUM_AUDIO_FRAMES - begin)
+ for (i64 i = 0; i < num_frames; ++i)
+ _sound_ring[begin + i] += frames[i];
+ else {
+ i64 part_one = MAX_NUM_AUDIO_FRAMES - begin;
+ i64 part_two = num_frames - part_one;
+
+ for (i64 i = 0; i < part_one; ++i)
+ _sound_ring[begin + i] += frames[i];
+ for (i64 i = 0; i < part_two; ++i)
+ _sound_ring[i] += frames[part_one + i];
+ }
}
-__attribute__((export_name("js_init"))) void js_init(void) {
- ++_num_events;
- platform.pixels = _buffer;
- platform.input = _input;
- platform.done = 1;
+__attribute__((export_name("js_main"))) void js_main(c8 *href) {
+ main(1, &href);
}
__attribute__((export_name("js_title"))) void *js_title(void) {
@@ -1632,7 +1667,7 @@ __attribute__((export_name("js_pixels"))) void *js_pixels(void) {
return platform.pixels;
}
-__attribute__((export_name("js_frame"))) void js_frame(i32 frame_width, i32 frame_height) {
+__attribute__((export_name("js_frame"))) void js_frame(i32 frame_width, i32 frame_height, i32 num_samples) {
if (platform.frame_width != frame_width || platform.frame_height != frame_height) {
++_num_events;
platform.frame_width = frame_width;
@@ -1641,7 +1676,25 @@ __attribute__((export_name("js_frame"))) void js_frame(i32 frame_width, i32 fram
platform.done = 0;
- update_and_render_frame();
+ if (_num_events > 0 || !_wait_events) {
+ _wait_events = 0;
+ update_and_render_frame();
+
+ // Make canvas pixels opaque.
+ i64 size = platform.frame_width * platform.frame_height;
+ for (i64 i = 0; i < size; ++i)
+ platform.pixels[i] |= 0xff000000;
+ }
+
+ // Convert interleaved frames to non-interleaved.
+ for (i64 j = 0; j < AUDIO_NUM_CHANNELS; ++j)
+ for (i64 i = 0; i < num_samples; ++i) {
+ i64 n = (_sound_read + i * AUDIO_NUM_CHANNELS + j) % MAX_NUM_AUDIO_FRAMES;
+ _sound_buffer[j * num_samples + i] = _sound_ring[n];
+ _sound_ring [n] = 0.f;
+ }
+
+ _sound_read = (_sound_read + num_samples * AUDIO_NUM_CHANNELS) % MAX_NUM_AUDIO_FRAMES;
}
__attribute__((export_name("js_mousemove"))) void js_mousemove(i32 x, i32 y) {
@@ -1685,67 +1738,6 @@ __attribute__((export_name("js_keyup"))) void js_keyup(u32 key, u32 mod) {
platform.key_down[key] = 0;
}
-#endif // __wasm__
-
-// ================================================================
-//
-// Web audio
-//
-// TODO: Shared memory buffer
-// https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Memory
-// https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletNode/port
-//
-// ================================================================
-
-#ifdef __wasm__
-
-i64 _sound_position = 0;
-f32 _sound_ring[MAX_NUM_AUDIO_FRAMES] = {0};
-
-void p_handle_audio_impl(i32 samples_elapsed);
-
-void p_handle_audio(i64 samples_elapsed) {
- if (samples_elapsed <= 0)
- return;
-
- p_handle_audio_impl(samples_elapsed);
-
- _sound_position = (_sound_position + samples_elapsed * AUDIO_NUM_CHANNELS) % MAX_NUM_AUDIO_FRAMES;
-}
-
-void p_queue_sound(i64 delay_in_samples, i64 num_samples, f32 *frames) {
- if (frames == NULL)
- return;
-
- if (delay_in_samples < 0) {
- frames += -delay_in_samples * AUDIO_NUM_CHANNELS;
- num_samples -= delay_in_samples;
- delay_in_samples = 0;
- }
-
- if (num_samples <= 0)
- return;
-
- i64 num_frames = num_samples * AUDIO_NUM_CHANNELS;
- if (num_frames > MAX_NUM_AUDIO_FRAMES)
- return;
-
- i64 begin = (_sound_position + delay_in_samples * AUDIO_NUM_CHANNELS) % MAX_NUM_AUDIO_FRAMES;
-
- if (num_frames <= MAX_NUM_AUDIO_FRAMES - begin)
- for (i64 i = 0; i < num_frames; ++i)
- _sound_ring[begin + i] += frames[i];
- else {
- i64 part_one = MAX_NUM_AUDIO_FRAMES - begin;
- i64 part_two = num_frames - part_one;
-
- for (i64 i = 0; i < part_one; ++i)
- _sound_ring[begin + i] += frames[i];
- for (i64 i = 0; i < part_two; ++i)
- _sound_ring[i] += frames[part_one + i];
- }
-}
-
__attribute__((export_name("js_sample_rate"))) f64 js_sample_rate(void) {
return (f64) AUDIO_SAMPLE_RATE;
}
@@ -1759,13 +1751,7 @@ __attribute__((export_name("js_max_num_audio_frames"))) i32 js_max_num_audio_fra
}
__attribute__((export_name("js_sound_buffer"))) void *js_sound_buffer(void) {
- return _sound_ring;
-}
-
-__attribute__((export_name("js_sound_sync"))) void js_sound_sync(void) {
- _sound_position = AUDIO_AVAIL_MIN;
- for (i64 i = 0; i < MAX_NUM_AUDIO_FRAMES; ++i)
- _sound_ring[i] = 0.f;
+ return _sound_buffer;
}
#endif // __wasm__