Skip to content

Commit cf05fa8

Browse files
committed
Add SharedData to share data more easily
- automatically creates properties on both sides - types are synchronized for IDE users
1 parent 480b691 commit cf05fa8

File tree

4 files changed

+147
-61
lines changed

4 files changed

+147
-61
lines changed

SharedData.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* @typedef {keyof typeof SchemaType} SchemaKey
3+
* @typedef {(typeof SchemaType)[SchemaKey]} SchemaType
4+
*/
5+
export const SchemaType = {
6+
u8: Uint8Array,
7+
i32: Int32Array,
8+
u32: Uint32Array,
9+
f32: Float32Array,
10+
f64: Float64Array,
11+
};
12+
13+
/**
14+
* @typedef {Record<string, { type: SchemaKey, count: number}} Schema
15+
*//**
16+
* @typedef {{schema: T, buffer: SharedArrayBuffer}} SharedBufferData
17+
* @template {Schema} T
18+
*/
19+
20+
/**
21+
* Schema builder.
22+
*/
23+
export class DataSchema {
24+
/** @param {Schema} schema */
25+
static byteSize(schema) {
26+
return Object.values(schema).reduce((acc, el) => {
27+
const bytes = SchemaType[el.type].BYTES_PER_ELEMENT;
28+
const mod = acc % bytes;
29+
if (mod !== 0) {
30+
acc += bytes - mod;
31+
}
32+
acc += bytes * el.count;
33+
return acc;
34+
}, 0);
35+
}
36+
37+
/**
38+
* @returns {SharedBufferData} The data to be sahred across workers.
39+
*/
40+
build() {
41+
return {
42+
schema: this.schema,
43+
buffer: new SharedArrayBuffer(this.byteSize()),
44+
};
45+
}
46+
47+
/**
48+
* Build a schema.
49+
* @param {T} schema
50+
* @template {Record<string, [SchemaKey, number] | SchemaKey>} T
51+
* @returns {{[P in keyof T]: T[P] extends string ? {type: T[P], count: 1} : {type: T[P][0], count: T[P][1]}}
52+
*/
53+
static buildSchema(schema) {
54+
const result = {};
55+
for (const key in schema) {
56+
if (typeof schema[key] === "string") {
57+
result[key] = { type: schema[key], count: 1 };
58+
} else {
59+
result[key] = { type: schema[key][0], count: schema[key][1] };
60+
}
61+
}
62+
return result;
63+
}
64+
/**
65+
* @param {T} schema
66+
* @template {Parameters<typeof DataSchema.buildSchema>[0]} T
67+
* @returns {SharedBufferData<ReturnType<typeof DataSchema.buildSchema<T>>}
68+
*/
69+
static build(schema) {
70+
const s = this.buildSchema(schema);
71+
return { buffer: new SharedArrayBuffer(this.byteSize(s)), schema: s };
72+
}
73+
74+
/**
75+
* Build views around a schema.
76+
* @param {{schema: T, buffer: SharedArrayBuffer}} data
77+
* @template {Schema} T
78+
* @returns {{[P in keyof T]: InstanceType<(typeof SchemaType)[T[P]["type"]]>}}
79+
*/
80+
static view(data) {
81+
let offset = 0;
82+
const views = {};
83+
for (const [name, el] of Object.entries(data.schema)) {
84+
const constructor = SchemaType[el.type];
85+
const bytes = constructor.BYTES_PER_ELEMENT;
86+
const mod = offset % bytes;
87+
if (mod !== 0) {
88+
offset += bytes - mod;
89+
}
90+
views[name] = new constructor(data.buffer, offset, el.count);
91+
offset += views[name].byteLength;
92+
}
93+
return views;
94+
}
95+
}

index.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,15 @@
6060
</style>
6161
<!-- Hack for GitHub Pages found on https://github.com/orgs/community/discussions/13309#discussioncomment-3844940 -->
6262
<script src="enable-threads.js"></script>
63-
<script src="raylib-wrapper.js"></script>
6463
</head>
6564
<body>
6665
<label for="raylib-example-select">Choose an Example:</label>
67-
<select id="raylib-example-select" onchange="startRaylib(this.value)">
66+
<select id="raylib-example-select">
6867
<!-- This is populated by js -->
6968
</select>
7069
<canvas id="game"></canvas>
71-
<script>
70+
<script type="module">
71+
import RaylibJs from "./raylib-wrapper.js";
7272
const wasmPaths = {
7373
"tsoding": ["tsoding_ball", "tsoding_snake",],
7474
"core": ["core_basic_window", "core_basic_screen_manager", "core_input_keys", "core_input_mouse_wheel",],
@@ -87,6 +87,7 @@
8787
}
8888
raylibExampleSelect.innerHTML += "</optgroup>"
8989
}
90+
raylibExampleSelect.onchange = (ev) => startRaylib(ev.currentTarget.value);
9091

9192
const { protocol } = window.location;
9293
const isHosted = protocol !== "file:";

raylib-wrapper.js

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
let frameCounter = 0;
1+
import EventWorker from "./EventWorker.js";
2+
import { DataSchema } from "./SharedData.js";
3+
24
function setListeners(eventWorker, handlers, ctx = handlers) {
35
for (const prop in handlers) {
46
eventWorker.setListener(prop, handlers[prop].bind(ctx));
57
}
68
}
7-
class RaylibJs {
9+
10+
export default class RaylibJs {
811
async start({ wasmPath, canvasId }) {
912
/** @type {HTMLCanvasElement} */
1013
const canvas = document.getElementById(canvasId);
@@ -15,21 +18,19 @@ class RaylibJs {
1518
}
1619
this.ctx = canvas.getContext("bitmaprenderer");
1720
if (this.worker === undefined) {
18-
this.worker = new (await import("./EventWorker.js")).default(
19-
"./raylib.js",
20-
{ type: "module" },
21-
);
21+
this.worker = new EventWorker("./raylib.js", { type: "module" });
2222
} else {
2323
throw new Error("raylib.js worker already exists!");
2424
}
25-
26-
this.buffer = new SharedArrayBuffer(20);
27-
const keys = new SharedArrayBuffer(GLFW_KEY_LAST + 1);
28-
const mouse = new SharedArrayBuffer(4 * 3);
29-
const boundingRect = new SharedArrayBuffer(4 * 4);
30-
this.keys = new Uint8Array(keys);
31-
this.mouse = new Float32Array(mouse);
32-
this.boundingRect = new Float32Array(boundingRect);
25+
const shared = DataSchema.build({
26+
asyncFlag: "i32",
27+
time: "f64",
28+
stop: "u8",
29+
keys: ["u8", GLFW_KEY_LAST + 1],
30+
mouse: ["f32", 3],
31+
boundingRect: ["f32", 4],
32+
});
33+
this.views = DataSchema.view(shared);
3334
// bind listeners
3435
setListeners(
3536
this.worker,
@@ -42,18 +43,18 @@ class RaylibJs {
4243
);
4344
window.addEventListener("keydown", (e) => {
4445
const key = glfwKeyMapping[e.code];
45-
this.keys[~~(key / 8)] |= 1 << key % 8;
46+
this.views.keys[~~(key / 8)] |= 1 << key % 8;
4647
});
4748
window.addEventListener("keyup", (e) => {
4849
const key = glfwKeyMapping[e.code];
49-
this.keys[~~(key / 8)] &= ~(1 << key % 8);
50+
this.views.keys[~~(key / 8)] &= ~(1 << key % 8);
5051
});
5152
window.addEventListener("mousemove", (e) => {
52-
this.mouse[0] = e.clientX;
53-
this.mouse[1] = e.clientY;
53+
this.views.mouse[0] = e.clientX;
54+
this.views.mouse[1] = e.clientY;
5455
});
5556
window.addEventListener("wheel", (e) => {
56-
this.mouse[2] = e.deltaY;
57+
this.views.mouse[2] = e.deltaY;
5758
});
5859

5960
// Initialize raylib.js worker
@@ -65,18 +66,12 @@ class RaylibJs {
6566
this.#setBoundingRect();
6667
resolve();
6768
});
68-
this.worker.send("init", {
69-
wasmPath,
70-
buffer: this.buffer,
71-
keys,
72-
mouse,
73-
boundingRect,
74-
});
69+
this.worker.send("init", { wasmPath, shared });
7570
});
7671
}
7772

7873
stop() {
79-
new Uint8Array(this.buffer, this.buffer.byteLength - 1, 1).set([1]);
74+
this.views.stop.set([1]);
8075
// TODO: gracefully shut down
8176
this.worker.terminate();
8277
}
@@ -95,20 +90,24 @@ class RaylibJs {
9590

9691
#onRequestAnimationFrame() {
9792
requestAnimationFrame((timestamp) => {
98-
new Float64Array(this.buffer, 8, 1).set([timestamp]);
93+
this.views.time.set([timestamp]);
9994
this.#wake();
10095
});
10196
}
10297

10398
#wake() {
104-
const flag = new Int32Array(this.buffer, 0, 1);
105-
Atomics.store(flag, 0, 1);
106-
Atomics.notify(flag, 0);
99+
Atomics.store(this.views.asyncFlag, 0, 1);
100+
Atomics.notify(this.views.asyncFlag, 0);
107101
}
108102

109103
#setBoundingRect() {
110104
const rect = this.ctx.canvas.getBoundingClientRect();
111-
this.boundingRect.set([rect.left, rect.top, rect.width, rect.height]);
105+
this.views.boundingRect.set([
106+
rect.left,
107+
rect.top,
108+
rect.width,
109+
rect.height,
110+
]);
112111
}
113112
}
114113

raylib.js

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { registerWorkerEvents, reply } from "./EventWorker.js";
2+
import { DataSchema } from "./SharedData.js";
23

34
if (typeof importScripts !== "function") {
45
throw new Error("raylib.js should be run in a Worker!");
@@ -44,35 +45,25 @@ class RaylibJs {
4445
this.dt = undefined;
4546
this.targetFPS = 60;
4647
this.entryFunction = undefined;
47-
this.prevKeys = new Uint8Array(0);
48-
this.keys = new Uint8Array(0);
49-
this.mouse = new Float32Array(3);
5048
this.images = [];
51-
this.boundingRect = new Float32Array(4);
5249
}
5350

5451
constructor() {
5552
this.#reset();
5653
}
5754

5855
get quit() {
59-
return new Uint8Array(this.buffer, this.buffer.byteLength - 1, 1)[0] !== 0;
56+
return this.views.stop[0] !== 0;
6057
}
6158

62-
async start({wasmPath, buffer, keys, mouse, boundingRect}) {
59+
async start({wasmPath, shared}) {
6360
if (this.wasm !== undefined) {
6461
console.error("The game is already running. Please stop() it first.");
6562
return;
6663
}
67-
/** @type {SharedArrayBuffer} */
68-
this.buffer = buffer;
69-
70-
this.keys = new Uint8Array(keys);
71-
this.prevKeys = new Uint8Array(this.keys.length);
72-
this.mouse = new Float32Array(mouse);
73-
this.boundingRect = new Float32Array(boundingRect);
74-
this.asyncFlag = new Int32Array(this.buffer, 0, 1);
75-
this.timeBuffer = new Float64Array(this.buffer, 8, 1);
64+
/** @type {import("./raylib-wrapper.js").default["views"]} */
65+
this.views = DataSchema.view(shared);
66+
this.prevKeys = new Uint8Array(this.views.keys);
7667

7768
const canvas = new OffscreenCanvas(0, 0);
7869
this.ctx = canvas.getContext("2d");
@@ -90,7 +81,7 @@ class RaylibJs {
9081
await grixel.load();
9182

9283
this.#wait(() => reply("requestAnimationFrame"));
93-
this.previous = this.timeBuffer[0];
84+
this.previous = this.views.time[0];
9485
this.#wait(() => reply("requestAnimationFrame"));
9586
reply("initialized");
9687

@@ -103,15 +94,15 @@ class RaylibJs {
10394
}
10495

10596
get currentMouseWheelMoveState() {
106-
return Math.sign(-this.mouse[2]);
97+
return Math.sign(-this.views.mouse[2]);
10798
}
10899

109100
set currentMouseWheelMoveState(v) {
110-
this.mouse[2] = v;
101+
this.views.mouse[2] = v;
111102
}
112103

113104
get currentMousePosition() {
114-
return { x: this.mouse[0], y: this.mouse[1] };
105+
return { x: this.views.mouse[0], y: this.views.mouse[1] };
115106
}
116107

117108
InitWindow(width, height, title_ptr) {
@@ -151,13 +142,13 @@ class RaylibJs {
151142
}
152143

153144
BeginDrawing() {
154-
const timestamp = this.timeBuffer[0];
145+
const timestamp = this.views.time[0];
155146
this.dt = (timestamp - this.previous)/1000.0;
156147
this.previous = timestamp;
157148
}
158149

159150
EndDrawing() {
160-
this.prevKeys.set(this.keys);
151+
this.prevKeys.set(this.views.keys);
161152
this.currentMouseWheelMoveState = 0.0;
162153
const img = this.ctx.canvas.transferToImageBitmap();
163154
this.ctx.drawImage(img, 0, 0);
@@ -206,15 +197,15 @@ class RaylibJs {
206197
}
207198

208199
IsKeyPressed(key) {
209-
return !this.#isBitSet(this.prevKeys, key) && this.#isBitSet(this.keys, key);
200+
return !this.#isBitSet(this.prevKeys, key) && this.IsKeyPressed(key);
210201
}
211202

212203
#isBitSet(arr, place) {
213204
return arr[~~(place / 8)] & (1 << (place % 8)) !== 0;
214205
}
215206

216207
IsKeyDown(key) {
217-
return this.#isBitSet(this.keys, key);
208+
return this.#isBitSet(this.views.keys, key);
218209
}
219210

220211
GetMouseWheelMove() {
@@ -246,7 +237,7 @@ class RaylibJs {
246237
}
247238

248239
GetMousePosition(result_ptr) {
249-
const bcrect = this.boundingRect;
240+
const bcrect = this.views.boundingRect;
250241
const x = this.currentMousePosition.x - bcrect[0];
251242
const y = this.currentMousePosition.y - bcrect[1];
252243

@@ -376,9 +367,9 @@ class RaylibJs {
376367
}
377368

378369
#wait(cmd) {
379-
Atomics.store(this.asyncFlag, 0, 0);
370+
Atomics.store(this.views.asyncFlag, 0, 0);
380371
cmd?.();
381-
Atomics.wait(this.asyncFlag, 0, 0);
372+
Atomics.wait(this.views.asyncFlag, 0, 0);
382373
}
383374

384375
memcpy(dest_ptr, src_ptr, count) {

0 commit comments

Comments
 (0)