From 379ff668a310b70353347dd9e81e32066a403a3a Mon Sep 17 00:00:00 2001 From: AntonPieper Date: Wed, 21 Feb 2024 11:45:30 +0100 Subject: [PATCH 1/7] Implement async using workers --- EventWorker.js | 91 ++++++++ examples/shapes_colors_palette_async.c | 104 +++++++++ index.html | 4 +- nob.c | 5 + raylib-wrapper.js | 238 +++++++++++++++++++++ raylib.js | 281 +++++++++---------------- server.py | 12 ++ wasm/shapes_colors_palette_async.wasm | Bin 0 -> 5620 bytes 8 files changed, 556 insertions(+), 179 deletions(-) create mode 100644 EventWorker.js create mode 100644 examples/shapes_colors_palette_async.c create mode 100644 raylib-wrapper.js create mode 100755 server.py create mode 100755 wasm/shapes_colors_palette_async.wasm diff --git a/EventWorker.js b/EventWorker.js new file mode 100644 index 0000000..c898dc2 --- /dev/null +++ b/EventWorker.js @@ -0,0 +1,91 @@ +// Adapted from https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#passing_data_examples +export default class EventWorker { + #worker; + #listeners; + + constructor(scriptUrl, options) { + this.#worker = new Worker(scriptUrl, options); + this.#listeners = {}; + this.#worker.onmessage = (event) => { + if ( + event.data instanceof Object && + Object.hasOwn(event.data, "event") && + Object.hasOwn(event.data, "message") + ) { + this.#listeners[event.data.event].apply( + this, + event.data.message, + ); + } else { + console.error(event); + throw new TypeError("EventWorker got illegal event"); + } + }; + this.#worker.onmessageerror = (event) => { + console.error("[MAIN] onmessageerror:", event); + throw new Error(event); + } + this.#worker.onerror = (event) => { + console.error("[MAIN] onerror:", event); + throw new Error(event); + } + } + + terminate() { + this.#worker.terminate(); + this.#worker = null; + } + + setListener(name, handler) { + this.#listeners[name] = handler; + } + + removeListener(name) { + delete this.#listeners[name]; + } + + send(event, ...message) { + if (!event) { + throw new TypeError("EventWorker.send takes at least one argument"); + } + this.#worker.postMessage({ + event, + message, + }); + } +} + +/** + * @param {Record} handlers + * @typedef {(this: typeof self, ...message: any[])} WorkerHandler + */ +export function registerWorkerEvents(handlers) { + onmessage = (event) => { + if ( + event.data instanceof Object && + Object.hasOwn(event.data, "event") && + Object.hasOwn(event.data, "message") + ) { + handlers[event.data.event].apply(self, event.data.message); + } else { + console.error(event); + throw new TypeError("Illegal postMessage!"); + } + }; + onmessageerror = (event) => { + console.error("[WORKER] onmessageerror:", event); + }; + onerror = (event) => { + console.error("[WORKER] error:", event); + }; +} + +export function reply(event, ...message) { + if (!event) { + throw new TypeError("reply - not enough arguments"); + } + postMessage({ + event, + message, + }); +} diff --git a/examples/shapes_colors_palette_async.c b/examples/shapes_colors_palette_async.c new file mode 100644 index 0000000..8d05266 --- /dev/null +++ b/examples/shapes_colors_palette_async.c @@ -0,0 +1,104 @@ +/******************************************************************************************* +* +* raylib [shapes] example - Colors palette +* +* Example originally created with raylib 1.0, last time updated with raylib 2.5 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2014-2024 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +#include "raylib.h" + +#define MAX_COLORS_COUNT 21 // Number of colors available + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 800; + const int screenHeight = 450; + + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - colors palette"); + + Color colors[MAX_COLORS_COUNT] = { + DARKGRAY, MAROON, ORANGE, DARKGREEN, DARKBLUE, DARKPURPLE, DARKBROWN, + GRAY, RED, GOLD, LIME, BLUE, VIOLET, BROWN, LIGHTGRAY, PINK, YELLOW, + GREEN, SKYBLUE, PURPLE, BEIGE }; + + const char *colorNames[MAX_COLORS_COUNT] = { + "DARKGRAY", "MAROON", "ORANGE", "DARKGREEN", "DARKBLUE", "DARKPURPLE", + "DARKBROWN", "GRAY", "RED", "GOLD", "LIME", "BLUE", "VIOLET", "BROWN", + "LIGHTGRAY", "PINK", "YELLOW", "GREEN", "SKYBLUE", "PURPLE", "BEIGE" }; + + Rectangle colorsRecs[MAX_COLORS_COUNT] = { 0 }; // Rectangles array + + // Fills colorsRecs data (for every rectangle) + for (int i = 0; i < MAX_COLORS_COUNT; i++) + { + colorsRecs[i].x = 20.0f + 100.0f *(i%7) + 10.0f *(i%7); + colorsRecs[i].y = 80.0f + 100.0f *(i/7) + 10.0f *(i/7); + colorsRecs[i].width = 100.0f; + colorsRecs[i].height = 100.0f; + } + + int colorState[MAX_COLORS_COUNT] = { 0 }; // Color state: 0-DEFAULT, 1-MOUSE_HOVER + + Vector2 mousePoint = { 0.0f, 0.0f }; + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + mousePoint = GetMousePosition(); + + for (int i = 0; i < MAX_COLORS_COUNT; i++) + { + if (CheckCollisionPointRec(mousePoint, colorsRecs[i])) colorState[i] = 1; + else colorState[i] = 0; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(RAYWHITE); + + DrawText("raylib colors palette", 28, 42, 20, BLACK); + DrawText("press SPACE to see all colors", GetScreenWidth() - 180, GetScreenHeight() - 40, 10, GRAY); + + for (int i = 0; i < MAX_COLORS_COUNT; i++) // Draw all rectangles + { + DrawRectangleRec(colorsRecs[i], Fade(colors[i], colorState[i]? 0.6f : 1.0f)); + + if (IsKeyDown(KEY_SPACE) || colorState[i]) + { + DrawRectangle((int)colorsRecs[i].x, (int)(colorsRecs[i].y + colorsRecs[i].height - 26), (int)colorsRecs[i].width, 20, BLACK); + DrawRectangleLinesEx(colorsRecs[i], 6, Fade(BLACK, 0.3f)); + DrawText(colorNames[i], (int)(colorsRecs[i].x + colorsRecs[i].width - MeasureText(colorNames[i], 10) - 12), + (int)(colorsRecs[i].y + colorsRecs[i].height - 20), 10, colors[i]); + } + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} diff --git a/index.html b/index.html index 960a2cd..9f22da0 100644 --- a/index.html +++ b/index.html @@ -57,7 +57,7 @@ src: url(fonts/acme_7_wide_xtnd.woff); } - + @@ -69,7 +69,7 @@ const wasmPaths = { "tsoding": ["tsoding_ball", "tsoding_snake",], "core": ["core_basic_window", "core_basic_screen_manager", "core_input_keys", "core_input_mouse_wheel",], - "shapes": ["shapes_colors_palette"], + "shapes": ["shapes_colors_palette", "shapes_colors_palette_async"], "text": ["text_writing_anim"], "textures": ["textures_logo_raylib"], } diff --git a/nob.c b/nob.c index 15ef36d..6ea0882 100644 --- a/nob.c +++ b/nob.c @@ -28,6 +28,11 @@ Example examples[] = { .bin_path = "./build/shapes_colors_palette", .wasm_path = "./wasm/shapes_colors_palette.wasm", }, + { + .src_path = "./examples/shapes_colors_palette_async.c", + .bin_path = "./build/shapes_colors_palette_async", + .wasm_path = "./wasm/shapes_colors_palette_async.wasm", + }, { .src_path = "./examples/tsoding_ball.c", .bin_path = "./build/tsoding_ball", diff --git a/raylib-wrapper.js b/raylib-wrapper.js new file mode 100644 index 0000000..be65ec1 --- /dev/null +++ b/raylib-wrapper.js @@ -0,0 +1,238 @@ +let frameCounter = 0; +function setListeners(eventWorker, handlers, ctx = handlers) { + for (const prop in handlers) { + eventWorker.setListener(prop, handlers[prop].bind(ctx)); + } +} +class RaylibJs { + async start({ wasmPath, canvasId }) { + /** @type {HTMLCanvasElement} */ + const canvas = document.getElementById(canvasId); + if (canvas == null) { + throw new Error( + `raylib.js could not find canvas with ID ${canvasId}`, + ); + } + this.ctx = canvas.getContext("bitmaprenderer"); + if (this.worker === undefined) { + this.worker = new (await import("./EventWorker.js")).default( + "./raylib.js", + { type: "module" }, + ); + } else { + throw new Error("raylib.js worker already exists!"); + } + + this.buffer = new SharedArrayBuffer(20); + const keys = new SharedArrayBuffer(GLFW_KEY_LAST + 1); + const mouse = new SharedArrayBuffer(4 * 3); + const boundingRect = new SharedArrayBuffer(4 * 4); + this.keys = new Uint8Array(keys); + this.mouse = new Float32Array(mouse); + this.boundingRect = new Float32Array(boundingRect); + // bind listeners + setListeners( + this.worker, + { + frame: this.#onFrame, + window: this.#onWindow, + requestAnimationFrame: this.#onRequestAnimationFrame, + }, + this, + ); + window.addEventListener("keydown", (e) => { + const key = glfwKeyMapping[e.code]; + this.keys[~~(key / 8)] |= 1 << key % 8; + }); + window.addEventListener("keyup", (e) => { + const key = glfwKeyMapping[e.code]; + this.keys[~~(key / 8)] &= ~(1 << key % 8); + }); + window.addEventListener("mousemove", (e) => { + this.mouse[0] = e.clientX; + this.mouse[1] = e.clientY; + }); + window.addEventListener("wheel", (e) => { + this.mouse[2] = e.deltaY; + }); + + // Initialize raylib.js worker + return new Promise((resolve) => { + this.worker.setListener("initialized", () => { + this.worker.removeListener("initialized"); + // TODO: listen to real changss in boundingClientRect + console.log("initialized"); + this.#setBoundingRect(); + resolve(); + }); + this.worker.send("init", { + wasmPath, + buffer: this.buffer, + keys, + mouse, + boundingRect, + }); + }); + } + + stop() { + new Uint8Array(this.buffer, this.buffer.byteLength - 1, 1).set([1]); + // TODO: gracefully shut down + this.worker.terminate(); + } + + #onFrame(frame) { + this.ctx.transferFromImageBitmap(frame); + } + + #onWindow(width, height, title) { + this.ctx.canvas.width = width; + this.ctx.canvas.height = height; + // TODO: listen to bounding client rect changes + this.#setBoundingRect(); + document.title = title; + } + + #onRequestAnimationFrame() { + requestAnimationFrame((timestamp) => { + new Float64Array(this.buffer, 8, 1).set([timestamp]); + this.#wake(); + }); + } + + #wake() { + const flag = new Int32Array(this.buffer, 0, 1); + Atomics.store(flag, 0, 1); + Atomics.notify(flag, 0); + } + + #setBoundingRect() { + const rect = this.ctx.canvas.getBoundingClientRect(); + this.boundingRect.set([rect.left, rect.top, rect.width, rect.height]); + } +} + +const glfwKeyMapping = { + Space: 32, + 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, + KeyA: 65, + KeyB: 66, + KeyC: 67, + KeyD: 68, + KeyE: 69, + KeyF: 70, + KeyG: 71, + KeyH: 72, + KeyI: 73, + KeyJ: 74, + KeyK: 75, + KeyL: 76, + KeyM: 77, + KeyN: 78, + KeyO: 79, + KeyP: 80, + KeyQ: 81, + KeyR: 82, + KeyS: 83, + KeyT: 84, + KeyU: 85, + KeyV: 86, + KeyW: 87, + KeyX: 88, + KeyY: 89, + KeyZ: 90, + BracketLeft: 91, + Backslash: 92, + BracketRight: 93, + Backquote: 96, + // GLFW_KEY_WORLD_1 161 /* non-US #1 */ + // GLFW_KEY_WORLD_2 162 /* non-US #2 */ + Escape: 256, + Enter: 257, + Tab: 258, + Backspace: 259, + Insert: 260, + Delete: 261, + ArrowRight: 262, + ArrowLeft: 263, + ArrowDown: 264, + ArrowUp: 265, + PageUp: 266, + PageDown: 267, + Home: 268, + End: 269, + CapsLock: 280, + ScrollLock: 281, + NumLock: 282, + PrintScreen: 283, + Pause: 284, + F1: 290, + F2: 291, + F3: 292, + F4: 293, + F5: 294, + F6: 295, + F7: 296, + F8: 297, + F9: 298, + F10: 299, + F11: 300, + F12: 301, + F13: 302, + F14: 303, + F15: 304, + F16: 305, + F17: 306, + F18: 307, + F19: 308, + F20: 309, + F21: 310, + F22: 311, + F23: 312, + F24: 313, + F25: 314, + NumPad0: 320, + NumPad1: 321, + NumPad2: 322, + NumPad3: 323, + NumPad4: 324, + NumPad5: 325, + NumPad6: 326, + NumPad7: 327, + NumPad8: 328, + NumPad9: 329, + NumpadDecimal: 330, + NumpadDivide: 331, + NumpadMultiply: 332, + NumpadSubtract: 333, + NumpadAdd: 334, + NumpadEnter: 335, + NumpadEqual: 336, + ShiftLeft: 340, + ControlLeft: 341, + AltLeft: 342, + MetaLeft: 343, + ShiftRight: 344, + ControlRight: 345, + AltRight: 346, + MetaRight: 347, + ContextMenu: 348, + // GLFW_KEY_LAST GLFW_KEY_MENU +}; +const GLFW_KEY_LAST = 348; diff --git a/raylib.js b/raylib.js index a6a0154..833a452 100644 --- a/raylib.js +++ b/raylib.js @@ -1,3 +1,9 @@ +import { registerWorkerEvents, reply } from "./EventWorker.js"; + +if (typeof importScripts !== "function") { + throw new Error("raylib.js should be run in a Worker!"); +} + function make_environment(env) { return new Proxy(env, { get(target, prop, receiver) { @@ -38,29 +44,37 @@ class RaylibJs { this.dt = undefined; this.targetFPS = 60; this.entryFunction = undefined; - this.prevPressedKeyState = new Set(); - this.currentPressedKeyState = new Set(); - this.currentMouseWheelMoveState = 0; - this.currentMousePosition = {x: 0, y: 0}; + this.prevKeys = new Uint8Array(0); + this.keys = new Uint8Array(0); + this.mouse = new Float32Array(3); this.images = []; - this.quit = false; + this.boundingRect = new Float32Array(4); } constructor() { this.#reset(); } - - stop() { - this.quit = true; + + get quit() { + return new Uint8Array(this.buffer, this.buffer.byteLength - 1, 1)[0] !== 0; } - async start({ wasmPath, canvasId }) { + async start({wasmPath, buffer, keys, mouse, boundingRect}) { if (this.wasm !== undefined) { console.error("The game is already running. Please stop() it first."); return; } + /** @type {SharedArrayBuffer} */ + this.buffer = buffer; - const canvas = document.getElementById(canvasId); + this.keys = new Uint8Array(keys); + this.prevKeys = new Uint8Array(this.keys.length); + this.mouse = new Float32Array(mouse); + this.boundingRect = new Float32Array(boundingRect); + this.asyncFlag = new Int32Array(this.buffer, 0, 1); + this.timeBuffer = new Float64Array(this.buffer, 8, 1); + + const canvas = new OffscreenCanvas(0, 0); this.ctx = canvas.getContext("2d"); if (this.ctx === null) { throw new Error("Could not create 2d canvas context"); @@ -69,52 +83,52 @@ class RaylibJs { this.wasm = await WebAssembly.instantiateStreaming(fetch(wasmPath), { env: make_environment(this) }); + + // Load default font + const grixel = new FontFace("grixel", "url(./fonts/acme_7_wide_xtnd.woff)"); + self.fonts.add(grixel); + await grixel.load(); - const keyDown = (e) => { - this.currentPressedKeyState.add(glfwKeyMapping[e.code]); - }; - const keyUp = (e) => { - this.currentPressedKeyState.delete(glfwKeyMapping[e.code]); - }; - const wheelMove = (e) => { - this.currentMouseWheelMoveState = Math.sign(-e.deltaY); - }; - const mouseMove = (e) => { - this.currentMousePosition = {x: e.clientX, y: e.clientY}; - }; - window.addEventListener("keydown", keyDown); - window.addEventListener("keyup", keyUp); - window.addEventListener("wheel", wheelMove); - window.addEventListener("mousemove", mouseMove); + this.#wait(() => reply("requestAnimationFrame")); + this.previous = this.timeBuffer[0]; + this.#wait(() => reply("requestAnimationFrame")); + reply("initialized"); this.wasm.instance.exports.main(); - const next = (timestamp) => { - if (this.quit) { - this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); - window.removeEventListener("keydown", keyDown); - this.#reset() - return; - } - this.dt = (timestamp - this.previous)/1000.0; - this.previous = timestamp; + // backwards compatibility + while (!this.WindowShouldClose()) { this.entryFunction(); - window.requestAnimationFrame(next); - }; - window.requestAnimationFrame((timestamp) => { - this.previous = timestamp; - window.requestAnimationFrame(next); - }); + } + this.CloseWindow(); + } + + get currentMouseWheelMoveState() { + return Math.sign(-this.mouse[2]); + } + + set currentMouseWheelMoveState(v) { + this.mouse[2] = v; + } + + get currentMousePosition() { + return { x: this.mouse[0], y: this.mouse[1] }; } InitWindow(width, height, title_ptr) { this.ctx.canvas.width = width; this.ctx.canvas.height = height; const buffer = this.wasm.instance.exports.memory.buffer; - document.title = cstr_by_ptr(buffer, title_ptr); + reply("window", width, height, cstr_by_ptr(buffer, title_ptr)); } - WindowShouldClose(){ - return false; + WindowShouldClose() { + return this.quit; + } + + CloseWindow() { + this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); + this.#wait(() => reply("requestAnimationFrame")); + this.#reset(); } SetTargetFPS(fps) { @@ -136,12 +150,19 @@ class RaylibJs { return Math.min(this.dt, 1.0/this.targetFPS); } - BeginDrawing() {} + BeginDrawing() { + const timestamp = this.timeBuffer[0]; + this.dt = (timestamp - this.previous)/1000.0; + this.previous = timestamp; + } EndDrawing() { - this.prevPressedKeyState.clear(); - this.prevPressedKeyState = new Set(this.currentPressedKeyState); + this.prevKeys.set(this.keys); this.currentMouseWheelMoveState = 0.0; + const img = this.ctx.canvas.transferToImageBitmap(); + this.ctx.drawImage(img, 0, 0); + reply("frame", img); + this.#wait(() => reply("requestAnimationFrame")); } DrawCircleV(center_ptr, radius, color_ptr) { @@ -185,11 +206,17 @@ class RaylibJs { } IsKeyPressed(key) { - return !this.prevPressedKeyState.has(key) && this.currentPressedKeyState.has(key); + return !this.#isBitSet(this.prevKeys, key) && this.#isBitSet(this.keys, key); + } + + #isBitSet(arr, place) { + return arr[~~(place / 8)] & (1 << (place % 8)) !== 0; } + IsKeyDown(key) { - return this.currentPressedKeyState.has(key); + return this.#isBitSet(this.keys, key); } + GetMouseWheelMove() { return this.currentMouseWheelMoveState; } @@ -219,9 +246,9 @@ class RaylibJs { } GetMousePosition(result_ptr) { - const bcrect = this.ctx.canvas.getBoundingClientRect(); - const x = this.currentMousePosition.x - bcrect.left; - const y = this.currentMousePosition.y - bcrect.top; + const bcrect = this.boundingRect; + const x = this.currentMousePosition.x - bcrect[0]; + const y = this.currentMousePosition.y - bcrect[1]; const buffer = this.wasm.instance.exports.memory.buffer; new Float32Array(buffer, result_ptr, 2).set([x, y]); @@ -317,7 +344,7 @@ class RaylibJs { // TODO: dynamically generate the name for the font // Support more than one custom font const font = new FontFace("myfont", `url(${fileName})`); - document.fonts.add(font); + self.fonts.add(font); font.load(); } @@ -347,130 +374,24 @@ class RaylibJs { raylib_js_set_entry(entry) { this.entryFunction = this.wasm.instance.exports.__indirect_function_table.get(entry); } -} - -const glfwKeyMapping = { - "Space": 32, - "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, - "KeyA": 65, - "KeyB": 66, - "KeyC": 67, - "KeyD": 68, - "KeyE": 69, - "KeyF": 70, - "KeyG": 71, - "KeyH": 72, - "KeyI": 73, - "KeyJ": 74, - "KeyK": 75, - "KeyL": 76, - "KeyM": 77, - "KeyN": 78, - "KeyO": 79, - "KeyP": 80, - "KeyQ": 81, - "KeyR": 82, - "KeyS": 83, - "KeyT": 84, - "KeyU": 85, - "KeyV": 86, - "KeyW": 87, - "KeyX": 88, - "KeyY": 89, - "KeyZ": 90, - "BracketLeft": 91, - "Backslash": 92, - "BracketRight": 93, - "Backquote": 96, - // GLFW_KEY_WORLD_1 161 /* non-US #1 */ - // GLFW_KEY_WORLD_2 162 /* non-US #2 */ - "Escape": 256, - "Enter": 257, - "Tab": 258, - "Backspace": 259, - "Insert": 260, - "Delete": 261, - "ArrowRight": 262, - "ArrowLeft": 263, - "ArrowDown": 264, - "ArrowUp": 265, - "PageUp": 266, - "PageDown": 267, - "Home": 268, - "End": 269, - "CapsLock": 280, - "ScrollLock": 281, - "NumLock": 282, - "PrintScreen": 283, - "Pause": 284, - "F1": 290, - "F2": 291, - "F3": 292, - "F4": 293, - "F5": 294, - "F6": 295, - "F7": 296, - "F8": 297, - "F9": 298, - "F10": 299, - "F11": 300, - "F12": 301, - "F13": 302, - "F14": 303, - "F15": 304, - "F16": 305, - "F17": 306, - "F18": 307, - "F19": 308, - "F20": 309, - "F21": 310, - "F22": 311, - "F23": 312, - "F24": 313, - "F25": 314, - "NumPad0": 320, - "NumPad1": 321, - "NumPad2": 322, - "NumPad3": 323, - "NumPad4": 324, - "NumPad5": 325, - "NumPad6": 326, - "NumPad7": 327, - "NumPad8": 328, - "NumPad9": 329, - "NumpadDecimal": 330, - "NumpadDivide": 331, - "NumpadMultiply": 332, - "NumpadSubtract": 333, - "NumpadAdd": 334, - "NumpadEnter": 335, - "NumpadEqual": 336, - "ShiftLeft": 340, - "ControlLeft" : 341, - "AltLeft": 342, - "MetaLeft": 343, - "ShiftRight": 344, - "ControlRight": 345, - "AltRight": 346, - "MetaRight": 347, - "ContextMenu": 348, - // GLFW_KEY_LAST GLFW_KEY_MENU + + #wait(cmd) { + Atomics.store(this.asyncFlag, 0, 0); + cmd?.(); + Atomics.wait(this.asyncFlag, 0, 0); + } + + memcpy(dest_ptr, src_ptr, count) { + const buffer = this.wasm.instance.exports.memory.buffer; + new Uint8Array(buffer, dest_ptr, count).set(new Uint8Array(buffer, src_ptr, count)); + return dest_ptr; + } + + memset(ptr, value, num) { + const buffer = this.wasm.instance.exports.memory.buffer; + new Uint8Array(buffer, ptr, num).fill(value); + return ptr; + } } function cstrlen(mem, ptr) { @@ -509,3 +430,9 @@ function getColorFromMemory(buffer, color_ptr) { const [r, g, b, a] = new Uint8Array(buffer, color_ptr, 4); return color_hex_unpacked(r, g, b, a); } + +registerWorkerEvents({ + init(options) { + new RaylibJs().start(options); + } +}); diff --git a/server.py b/server.py new file mode 100755 index 0000000..21570ef --- /dev/null +++ b/server.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +from http.server import HTTPServer, SimpleHTTPRequestHandler, test +import sys + +class CORSRequestHandler (SimpleHTTPRequestHandler): + def end_headers (self): + self.send_header('Cross-Origin-Opener-Policy', 'same-origin') + self.send_header('Cross-Origin-Embedder-Policy', 'require-corp') + SimpleHTTPRequestHandler.end_headers(self) + +if __name__ == '__main__': + test(CORSRequestHandler, HTTPServer, port=int(sys.argv[1]) if len(sys.argv) > 1 else 8000) diff --git a/wasm/shapes_colors_palette_async.wasm b/wasm/shapes_colors_palette_async.wasm new file mode 100755 index 0000000000000000000000000000000000000000..57fc55a57337198cdc1f8267ed38ea6b2b6a6f7c GIT binary patch literal 5620 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 Date: Wed, 21 Feb 2024 12:03:09 +0100 Subject: [PATCH 2/7] Add additional cross origin isolation message --- index.html | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 9f22da0..67a0afd 100644 --- a/index.html +++ b/index.html @@ -39,7 +39,8 @@ max-width: 8rem; } - .not-hosted-msg { + .not-hosted-msg, + .no-coi-msg { text-align: center; position: absolute; @@ -87,6 +88,7 @@ const { protocol } = window.location; const isHosted = protocol !== "file:"; + const isCOI = crossOriginIsolated ?? false; let raylibJs = undefined; function startRaylib(selectedWasm){ @@ -96,6 +98,21 @@ raylibExampleSelect.value = selectedWasm; if (isHosted) { + if (!isCOI) { + window.addEventListener("load", () => { + document.body.innerHTML = ` +
+
+

Unfortunately, due to security restrictions, SharedArrayBuffer cannot be used.

+

Please navigate to this location using a web server that has the required headers set.

+

If you have Python 3 on your system you can just do:

+
+ $ python3 ./server.py 6969 +
+ `; + }); + return; + } if (raylibJs !== undefined) { raylibJs.stop(); } @@ -113,7 +130,7 @@

Please navigate to this location using a web server.

If you have Python 3 on your system you can just do:

- $ python3 -m http.server 6969 + $ python3 ./server.py 6969 `; }); From 480b69152d6ffe82f90de18d1d7ca2e647822236 Mon Sep 17 00:00:00 2001 From: AntonPieper Date: Wed, 21 Feb 2024 12:25:39 +0100 Subject: [PATCH 3/7] Add COI hack for GitHub pages --- enable-threads.js | 75 +++++++++++++++++++++++++++++++++++++++++++++++ index.html | 2 ++ 2 files changed, 77 insertions(+) create mode 100644 enable-threads.js diff --git a/enable-threads.js b/enable-threads.js new file mode 100644 index 0000000..955e31f --- /dev/null +++ b/enable-threads.js @@ -0,0 +1,75 @@ +// NOTE: This file creates a service worker that cross-origin-isolates the page (read more here: https://web.dev/coop-coep/) which allows us to use wasm threads. +// Normally you would set the COOP and COEP headers on the server to do this, but Github Pages doesn't allow this, so this is a hack to do that. + +/* Edited version of: coi-serviceworker v0.1.6 - Guido Zuidhof, licensed under MIT */ +// From here: https://github.com/gzuidhof/coi-serviceworker +if(typeof window === 'undefined') { + self.addEventListener("install", () => self.skipWaiting()); + self.addEventListener("activate", e => e.waitUntil(self.clients.claim())); + + async function handleFetch(request) { + if(request.cache === "only-if-cached" && request.mode !== "same-origin") { + return; + } + + if(request.mode === "no-cors") { // We need to set `credentials` to "omit" for no-cors requests, per this comment: https://bugs.chromium.org/p/chromium/issues/detail?id=1309901#c7 + request = new Request(request.url, { + cache: request.cache, + credentials: "omit", + headers: request.headers, + integrity: request.integrity, + destination: request.destination, + keepalive: request.keepalive, + method: request.method, + mode: request.mode, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + signal: request.signal, + }); + } + + let r = await fetch(request).catch(e => console.error(e)); + + if(r.status === 0) { + return r; + } + + const headers = new Headers(r.headers); + headers.set("Cross-Origin-Embedder-Policy", "credentialless"); // or: require-corp + headers.set("Cross-Origin-Opener-Policy", "same-origin"); + + return new Response(r.body, { status: r.status, statusText: r.statusText, headers }); + } + + self.addEventListener("fetch", function(e) { + e.respondWith(handleFetch(e.request)); // respondWith must be executed synchonously (but can be passed a Promise) + }); + +} else { + (async function() { + if(window.crossOriginIsolated !== false) return; + + let registration = await navigator.serviceWorker.register(window.document.currentScript.src).catch(e => console.error("COOP/COEP Service Worker failed to register:", e)); + if(registration) { + console.log("COOP/COEP Service Worker registered", registration.scope); + + registration.addEventListener("updatefound", () => { + console.log("Reloading page to make use of updated COOP/COEP Service Worker."); + window.location.reload(); + }); + + // If the registration is active, but it's not controlling the page + if(registration.active && !navigator.serviceWorker.controller) { + console.log("Reloading page to make use of COOP/COEP Service Worker."); + window.location.reload(); + } + } + })(); +} + +// Code to deregister: +// let registrations = await navigator.serviceWorker.getRegistrations(); +// for(let registration of registrations) { +// await registration.unregister(); +// } diff --git a/index.html b/index.html index 67a0afd..afe1dd6 100644 --- a/index.html +++ b/index.html @@ -58,6 +58,8 @@ src: url(fonts/acme_7_wide_xtnd.woff); } + + From cf05fa83a96f5d178deeab24e6d3ecf275c68292 Mon Sep 17 00:00:00 2001 From: AntonPieper Date: Fri, 23 Feb 2024 11:47:13 +0100 Subject: [PATCH 4/7] Add `SharedData` to share data more easily - automatically creates properties on both sides - types are synchronized for IDE users --- SharedData.js | 95 +++++++++++++++++++++++++++++++++++++++++++++++ index.html | 7 ++-- raylib-wrapper.js | 63 ++++++++++++++++--------------- raylib.js | 43 +++++++++------------ 4 files changed, 147 insertions(+), 61 deletions(-) create mode 100644 SharedData.js diff --git a/SharedData.js b/SharedData.js new file mode 100644 index 0000000..42944a3 --- /dev/null +++ b/SharedData.js @@ -0,0 +1,95 @@ +/** + * @typedef {keyof typeof SchemaType} SchemaKey + * @typedef {(typeof SchemaType)[SchemaKey]} SchemaType + */ +export const SchemaType = { + u8: Uint8Array, + i32: Int32Array, + u32: Uint32Array, + f32: Float32Array, + f64: Float64Array, +}; + +/** + * @typedef {Record { + const bytes = SchemaType[el.type].BYTES_PER_ELEMENT; + const mod = acc % bytes; + if (mod !== 0) { + acc += bytes - mod; + } + acc += bytes * el.count; + return acc; + }, 0); + } + + /** + * @returns {SharedBufferData} The data to be sahred across workers. + */ + build() { + return { + schema: this.schema, + buffer: new SharedArrayBuffer(this.byteSize()), + }; + } + + /** + * Build a schema. + * @param {T} schema + * @template {Record} T + * @returns {{[P in keyof T]: T[P] extends string ? {type: T[P], count: 1} : {type: T[P][0], count: T[P][1]}} + */ + static buildSchema(schema) { + const result = {}; + for (const key in schema) { + if (typeof schema[key] === "string") { + result[key] = { type: schema[key], count: 1 }; + } else { + result[key] = { type: schema[key][0], count: schema[key][1] }; + } + } + return result; + } + /** + * @param {T} schema + * @template {Parameters[0]} T + * @returns {SharedBufferData>} + */ + static build(schema) { + const s = this.buildSchema(schema); + return { buffer: new SharedArrayBuffer(this.byteSize(s)), schema: s }; + } + + /** + * Build views around a schema. + * @param {{schema: T, buffer: SharedArrayBuffer}} data + * @template {Schema} T + * @returns {{[P in keyof T]: InstanceType<(typeof SchemaType)[T[P]["type"]]>}} + */ + static view(data) { + let offset = 0; + const views = {}; + for (const [name, el] of Object.entries(data.schema)) { + const constructor = SchemaType[el.type]; + const bytes = constructor.BYTES_PER_ELEMENT; + const mod = offset % bytes; + if (mod !== 0) { + offset += bytes - mod; + } + views[name] = new constructor(data.buffer, offset, el.count); + offset += views[name].byteLength; + } + return views; + } +} diff --git a/index.html b/index.html index afe1dd6..07ce4a5 100644 --- a/index.html +++ b/index.html @@ -60,15 +60,15 @@ - - -