diff options
author | Mitya Selivanov <automainint@guattari.tech> | 2025-01-06 23:41:27 +0100 |
---|---|---|
committer | Mitya Selivanov <automainint@guattari.tech> | 2025-01-06 23:41:27 +0100 |
commit | eab2433a2daee814492548b95f16892adde0908d (patch) | |
tree | e08d2b82fc3525d937bb64475da5543b4628158c | |
parent | 3359f068dc9e8ac036f0f709aeccf11dfba3cf03 (diff) | |
download | reduced_system_layer-eab2433a2daee814492548b95f16892adde0908d.zip |
Impl rendering in AudioWorklet
-rw-r--r-- | Dockerfile | 2 | ||||
-rwxr-xr-x | examples/graph.c | 2 | ||||
-rw-r--r-- | index.htm | 470 | ||||
-rw-r--r-- | index.htm.bak | 415 | ||||
-rwxr-xr-x | reduced_system_layer.c | 180 |
5 files changed, 763 insertions, 306 deletions
@@ -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; @@ -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__ |