@downdraft/engine 0.1.2-alpha.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/src/renderer/bootstrap.ts +1 -1
- package/app/src/renderer/feature-log.ts +1 -1
- package/core/src/index.ts +2 -2
- package/core/src/input/local-player-manager.ts +2 -2
- package/core/src/platform/rpc.ts +4 -1
- package/core/src/render/game-renderer.ts +127 -64
- package/core/src/sab/grid-buffer.ts +55 -2
- package/core/src/sab/record-buffer.ts +48 -5
- package/core/src/sab/sim-channel.ts +181 -17
- package/core/src/sab/slot-buffer.ts +55 -1
- package/core/src/sab/types.ts +11 -0
- package/core/src/telemetry/gpu-resource-tracker.ts +24 -0
- package/core/src/util/index.ts +1 -0
- package/core/src/util/logger.ts +2 -2
- package/core/src/util/session-timer.ts +23 -0
- package/core/src/worker/base-worker-host.ts +2 -1
- package/core/src/worker/buffer-sync.ts +20 -13
- package/core/src/worker/rpc.ts +54 -5
- package/core/src/worker/sim-worker-base.ts +6 -3
- package/core/src/worker/task-pool.ts +4 -0
- package/dist/.dd-manifest.json +879 -869
- package/dist/app/src/renderer/bootstrap.js +1 -1
- package/dist/app/src/renderer/feature-log.js +1 -1
- package/dist/core/src/index.js +2 -2
- package/dist/core/src/input/local-player-manager.js +2 -2
- package/dist/core/src/platform/rpc.js +2 -1
- package/dist/core/src/render/game-renderer.js +125 -59
- package/dist/core/src/sab/grid-buffer.js +53 -1
- package/dist/core/src/sab/record-buffer.js +49 -5
- package/dist/core/src/sab/sim-channel.js +173 -17
- package/dist/core/src/sab/slot-buffer.js +54 -1
- package/dist/core/src/telemetry/gpu-resource-tracker.js +26 -0
- package/dist/core/src/util/index.js +1 -0
- package/dist/core/src/util/logger.js +2 -2
- package/dist/core/src/util/session-timer.js +22 -0
- package/dist/core/src/worker/base-worker-host.js +2 -1
- package/dist/core/src/worker/buffer-sync.js +16 -9
- package/dist/core/src/worker/rpc.js +46 -8
- package/dist/core/src/worker/sim-worker-base.js +6 -3
- package/dist/core/src/worker/task-pool.js +5 -0
- package/dist/libraries/devtools/src/backend.js +35 -9
- package/dist/libraries/devtools/src/collectors.js +73 -22
- package/dist/libraries/gamepad/src/feed-input.js +1 -1
- package/dist/libraries/gamepad/src/hub.js +2 -2
- package/dist/libraries/gamepad/src/source-sab.js +2 -2
- package/dist/libraries/gamepad/src/sysfs-enrich.js +2 -2
- package/dist/libraries/gpu-kernels/src/kernel.js +1 -1
- package/dist/libraries/gpu-kernels/src/transpile.js +1 -1
- package/dist/libraries/html-ui-kit/src/nav/nav-controller.js +1 -1
- package/dist/libraries/html-ui-kit/src/nav/nav-input.js +2 -2
- package/dist/libraries/networking/src/discovery.js +8 -8
- package/dist/libraries/persistence/src/file-save-store.js +5 -5
- package/dist/libraries/persistence/src/opfs-save-store.js +2 -2
- package/dist/modules/controller-ui/src/index.js +1 -1
- package/dist/modules/devtools/src/blitz/fonts.js +75 -0
- package/dist/modules/devtools/src/blitz/host.js +127 -33
- package/dist/modules/devtools/src/blitz/panels/console.js +19 -7
- package/dist/modules/devtools/src/blitz/panels/elements.js +217 -28
- package/dist/modules/devtools/src/blitz/panels/gpu.js +5 -2
- package/dist/modules/devtools/src/blitz/panels/performance.js +28 -14
- package/dist/modules/devtools/src/blitz/panels/snapshot.js +31 -8
- package/dist/modules/devtools/src/blitz/panels/types.js +2 -1
- package/dist/modules/devtools/src/blitz/theme.js +26 -4
- package/dist/modules/html-ui/src/doc-backend.js +52 -3
- package/dist/modules/html-ui/src/host.js +6 -3
- package/dist/modules/html-ui/src/jsx-runtime.js +1 -1
- package/libraries/blitz-ui/native/Cargo.toml +6 -0
- package/libraries/blitz-ui/native-osr/Cargo.toml +3 -1
- package/libraries/devtools/src/backend.ts +38 -12
- package/libraries/devtools/src/collectors.ts +75 -22
- package/libraries/gamepad/src/feed-input.ts +1 -1
- package/libraries/gamepad/src/hub.ts +2 -2
- package/libraries/gamepad/src/source-sab.ts +2 -2
- package/libraries/gamepad/src/sysfs-enrich.ts +2 -2
- package/libraries/gpu-kernels/src/kernel.ts +2 -2
- package/libraries/gpu-kernels/src/transpile.ts +1 -1
- package/libraries/html-ui-kit/src/nav/nav-controller.ts +1 -1
- package/libraries/html-ui-kit/src/nav/nav-input.ts +2 -2
- package/libraries/networking/src/discovery.ts +8 -8
- package/libraries/persistence/src/file-save-store.ts +5 -5
- package/libraries/persistence/src/opfs-save-store.ts +2 -2
- package/libraries/physics-rapier/src/realm-worker-script.ts +1 -1
- package/modules/controller-ui/src/index.ts +1 -1
- package/modules/devtools/src/blitz/fonts.ts +78 -0
- package/modules/devtools/src/blitz/host.ts +110 -34
- package/modules/devtools/src/blitz/panels/console.ts +20 -8
- package/modules/devtools/src/blitz/panels/elements.ts +212 -31
- package/modules/devtools/src/blitz/panels/gpu.ts +4 -2
- package/modules/devtools/src/blitz/panels/performance.ts +26 -14
- package/modules/devtools/src/blitz/panels/snapshot.ts +30 -8
- package/modules/devtools/src/blitz/panels/types.ts +2 -1
- package/modules/devtools/src/blitz/theme.ts +26 -4
- package/modules/html-ui/src/doc-backend.ts +67 -4
- package/modules/html-ui/src/host.ts +11 -3
- package/modules/html-ui/src/jsx-runtime.ts +1 -1
- package/modules/html-ui/src/protocol.ts +1 -1
- package/package.json +2 -2
|
@@ -529,7 +529,7 @@ async function wireDevtoolsFrontend(
|
|
|
529
529
|
}
|
|
530
530
|
});
|
|
531
531
|
const evalNames: string[] = ["main"];
|
|
532
|
-
for (const wh of workerHosts ?? []) {
|
|
532
|
+
for (const wh of (workerHosts ?? []).values()) {
|
|
533
533
|
const evalFn = wh?.proxy?.__devtoolsEval;
|
|
534
534
|
if (typeof evalFn === "function" && wh.prefix) {
|
|
535
535
|
host.registerThreadEval(wh.prefix, (expr) => evalFn.call(wh.proxy, expr));
|
|
@@ -151,7 +151,7 @@ export async function getCombinedFeatureLog(): Promise<CombinedFeatureLog> {
|
|
|
151
151
|
// Single process — merge render fields (wgpu/disp/sab/wk/plug) into the
|
|
152
152
|
// host line. Host fields win on overlap (v, mode, cpuCores).
|
|
153
153
|
const host: FeatureLogData | null = hostLog || render
|
|
154
|
-
? { ...
|
|
154
|
+
? { ...render, ...hostLog, scope: "host" } as FeatureLogData
|
|
155
155
|
: null;
|
|
156
156
|
return {
|
|
157
157
|
host,
|
package/core/src/index.ts
CHANGED
|
@@ -56,7 +56,7 @@ export type { MultiInputChannelInstance, MultiInputChannelOptions } from "./sab/
|
|
|
56
56
|
export { INP, INP_FLAG, INP_HDR, INPUT_MAGIC, INPUT_VERSION, InputBufferReader, InputBufferWriter, InputChannel, KEY, MAX_INPUT_PLAYERS } from "./sab/game-input";
|
|
57
57
|
|
|
58
58
|
// Sim SAB Channel
|
|
59
|
-
export { allocateInputBuffer, allocateSimBuffer, ENT, MAX_ENTITIES, MAX_PLAYERS, PLR, PLR_FLAG, SIM_ENTITY_SLOT_SIZE, SIM_HDR, SIM_MAGIC, SIM_PLAYER_SLOT_SIZE, SIM_VERSION, SimBufferReader, SimBufferWriter, SimChannel } from "./sab/sim-channel";
|
|
59
|
+
export { allocateInputBuffer, allocateSimBuffer, ENT, MAX_ENTITIES, MAX_PLAYERS, PLR, PLR_FLAG, PREV, SIM_ENTITY_SLOT_SIZE, SIM_HDR, SIM_MAGIC, SIM_PLAYER_SLOT_SIZE, SIM_VERSION, SimBufferReader, SimBufferWriter, SimChannel } from "./sab/sim-channel";
|
|
60
60
|
export type { PhysicsTimingData } from "./sab/sim-channel";
|
|
61
61
|
|
|
62
62
|
// SAB Framework
|
|
@@ -90,7 +90,7 @@ export { EntitySimWorkerHost } from "./worker/entity-sim-worker-host";
|
|
|
90
90
|
export type { EntitySimApi, EntitySimHostOptions } from "./worker/entity-sim-worker-host";
|
|
91
91
|
export { exposeProfilingApi, InstrumentedWorkerHost } from "./worker/instrumented-worker-host";
|
|
92
92
|
export type { InstrumentedWorkerHostOptions } from "./worker/instrumented-worker-host";
|
|
93
|
-
export { expose, exposeEvents, getWorkerHost, wrap } from "./worker/rpc";
|
|
93
|
+
export { expose, exposeBatchedEvents, exposeEvents, getWorkerHost, wrap } from "./worker/rpc";
|
|
94
94
|
export type { ExposeOptions, HostMessageHandler, WorkerApi, WorkerEventEmitter, WorkerHost, WorkerProxy } from "./worker/rpc";
|
|
95
95
|
export { createSimWorker } from "./worker/sim-worker-base";
|
|
96
96
|
export type { CreateSimWorkerOptions, SimAfterTicksContext, SimTickContext, SimWorkerControl, SimWorkerStats } from "./worker/sim-worker-base";
|
|
@@ -140,11 +140,11 @@ export class LocalPlayerManager {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
private emitConnect(device: InputDevice): void {
|
|
143
|
-
for (const cb of this.connectCallbacks) cb(device);
|
|
143
|
+
for (const cb of this.connectCallbacks.values()) cb(device);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
private emitDisconnect(device: InputDevice): void {
|
|
147
|
-
for (const cb of this.disconnectCallbacks) cb(device);
|
|
147
|
+
for (const cb of this.disconnectCallbacks.values()) cb(device);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
startListening(): void {
|
package/core/src/platform/rpc.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
|
|
2
|
+
import { untrackedTimeout } from "../util/session-timer";
|
|
3
|
+
|
|
1
4
|
export type RPCMessageType = "request" | "response" | "event";
|
|
2
5
|
|
|
3
6
|
export interface RPCMessage {
|
|
@@ -34,7 +37,7 @@ export class RPC {
|
|
|
34
37
|
}
|
|
35
38
|
const id = this.messageIdCounter = (this.messageIdCounter + 1) % 0xFFFFFFFF;
|
|
36
39
|
return new Promise((resolve, reject) => {
|
|
37
|
-
const timer =
|
|
40
|
+
const timer = untrackedTimeout(() => {
|
|
38
41
|
if (this.pending.has(id)) {
|
|
39
42
|
this.pending.delete(id);
|
|
40
43
|
reject(new Error(`[RPC] Call to "${channel}" timed out after ${timeoutMs}ms`));
|
|
@@ -233,6 +233,9 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
233
233
|
uiInputRouter: UIInputRouter | null = null;
|
|
234
234
|
/** Screen-space compositors (html-ui panels etc.) drawn in the UI pass. */
|
|
235
235
|
private uiCompositors = new Set<import("../module/renderer-module").ScreenUiCompositor>();
|
|
236
|
+
/** uiCompositors as a pre-sorted array — rebuilt on add/remove so the
|
|
237
|
+
* frame loop doesn't spread+sort the set every frame. */
|
|
238
|
+
private uiCompositorOrder: import("../module/renderer-module").ScreenUiCompositor[] = [];
|
|
236
239
|
|
|
237
240
|
// Render loop state
|
|
238
241
|
private running = false;
|
|
@@ -272,13 +275,25 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
272
275
|
|
|
273
276
|
// Depth texture cache (used when the graph does not own depth)
|
|
274
277
|
// LRU: Map insertion order = access order (delete + re-set on access).
|
|
275
|
-
|
|
278
|
+
// Numeric key packs (w,h) — a `${w}x${h}` string key allocated a fresh
|
|
279
|
+
// string per lookup per viewport per frame.
|
|
280
|
+
private depthTextures = new Map<number, GPUTexture>();
|
|
276
281
|
private depthTextureCacheSize: number;
|
|
277
282
|
|
|
278
283
|
// Frame stats
|
|
279
284
|
private frameDrawCalls = 0;
|
|
280
285
|
private frameTriangles = 0;
|
|
281
286
|
|
|
287
|
+
// Per-frame submit interception: while non-null, queue.submit() calls
|
|
288
|
+
// (game callbacks, render-target providers, helpers) append their buffers
|
|
289
|
+
// here in call order instead of crossing into wgpu — the whole frame then
|
|
290
|
+
// lands in ONE queue.submit at end-of-frame. Games legitimately submit
|
|
291
|
+
// inside afterFrame callbacks (2D renderers, overlays), so folding the UI
|
|
292
|
+
// composite into the main batch requires capturing those submits rather
|
|
293
|
+
// than submitting early or reordering.
|
|
294
|
+
private frameSubmitSink: GPUCommandBuffer[] | null = null;
|
|
295
|
+
private submitInterceptorDevice: GPUDevice | null = null;
|
|
296
|
+
|
|
282
297
|
// Callbacks
|
|
283
298
|
private callbacks: FrameCallbacks = {};
|
|
284
299
|
|
|
@@ -354,6 +369,7 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
354
369
|
return false;
|
|
355
370
|
}
|
|
356
371
|
this.device = borrowedDevice ?? await this.requestDeviceFromAdapter(adapter!);
|
|
372
|
+
this.installQueueSubmitInterceptor(this.device);
|
|
357
373
|
this.adapter = adapter;
|
|
358
374
|
// The host configured the surface with its own usage set at creation —
|
|
359
375
|
// reconfiguring here would tear down the swapchain and race the host's
|
|
@@ -426,8 +442,16 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
426
442
|
setViewportCount: (count) => this.setViewportCount(count),
|
|
427
443
|
getUIInputRouter: () => this.uiInputRouter,
|
|
428
444
|
registerUiCompositor: (c) => {
|
|
445
|
+
const isNew = !this.uiCompositors.has(c);
|
|
429
446
|
this.uiCompositors.add(c);
|
|
430
|
-
|
|
447
|
+
if (isNew) {
|
|
448
|
+
this.uiCompositorOrder = [...this.uiCompositors].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
449
|
+
}
|
|
450
|
+
return () => {
|
|
451
|
+
if (this.uiCompositors.delete(c)) {
|
|
452
|
+
this.uiCompositorOrder = [...this.uiCompositors].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
453
|
+
}
|
|
454
|
+
};
|
|
431
455
|
},
|
|
432
456
|
});
|
|
433
457
|
|
|
@@ -543,6 +567,7 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
543
567
|
const device = await this.requestDeviceFromAdapter(adapter);
|
|
544
568
|
device.lost.then((info) => this.handleDeviceLost(info));
|
|
545
569
|
this.device = device;
|
|
570
|
+
this.installQueueSubmitInterceptor(device);
|
|
546
571
|
this.adapter = adapter;
|
|
547
572
|
|
|
548
573
|
installShaderValidationGuard(device);
|
|
@@ -798,15 +823,39 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
798
823
|
this.renderFrame();
|
|
799
824
|
} catch (err) {
|
|
800
825
|
log.error("game-renderer", `Render loop error: ${(err as Error).message}\n${(err as Error).stack}`);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
}
|
|
826
|
+
// No per-error device.lost.then here: the device already has exactly
|
|
827
|
+
// one lost subscription (init + reinit paths), and on native each
|
|
828
|
+
// .lost access mints a fresh promise — per-error attaches accumulated
|
|
829
|
+
// duplicate watchers during an error storm.
|
|
806
830
|
}
|
|
807
831
|
this.scheduleRender();
|
|
808
832
|
};
|
|
809
833
|
|
|
834
|
+
/**
|
|
835
|
+
* Redirect queue.submit() into frameSubmitSink while a frame is in
|
|
836
|
+
* flight. Submission order is preserved — buffers land in the sink in
|
|
837
|
+
* the order submit() was called — so mid-frame submits (uploads in
|
|
838
|
+
* beforeFrame, a game's afterFrame pass, endFrame hooks) still execute
|
|
839
|
+
* ahead of work recorded later, just inside the single end-of-frame
|
|
840
|
+
* queue.submit rather than as extra queue-lock + driver calls.
|
|
841
|
+
*/
|
|
842
|
+
private installQueueSubmitInterceptor(device: GPUDevice): void {
|
|
843
|
+
if (this.submitInterceptorDevice === device) return;
|
|
844
|
+
this.submitInterceptorDevice = device;
|
|
845
|
+
const queue = device.queue as any;
|
|
846
|
+
if (queue.__ddSubmitIntercepted) return;
|
|
847
|
+
queue.__ddSubmitIntercepted = true;
|
|
848
|
+
const origSubmit: GPUQueue["submit"] = queue.submit.bind(queue);
|
|
849
|
+
queue.submit = (commandBuffers: Iterable<GPUCommandBuffer>) => {
|
|
850
|
+
const sink = this.frameSubmitSink;
|
|
851
|
+
if (!sink) {
|
|
852
|
+
origSubmit(commandBuffers);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
for (const cb of commandBuffers) sink.push(cb);
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
810
859
|
private renderFrame(): void {
|
|
811
860
|
const now = performance.now();
|
|
812
861
|
|
|
@@ -856,6 +905,15 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
856
905
|
// recreates the invalid resources.
|
|
857
906
|
const gpuError = this.gpuProfiler?.hasUncapturedError() ?? false;
|
|
858
907
|
|
|
908
|
+
// Collect command buffers from all phases and submit once at the end
|
|
909
|
+
// of the frame — one queue.submit per frame instead of 2-4. The sink
|
|
910
|
+
// is armed BEFORE beginFrame so submit() calls from hooks and game
|
|
911
|
+
// callbacks (beforeFrame uploads, afterFrame render passes, the XR
|
|
912
|
+
// endFrame encoder) are captured in call order into the same batch
|
|
913
|
+
// rather than crossing into wgpu individually.
|
|
914
|
+
const frameCommandBuffers: GPUCommandBuffer[] = [];
|
|
915
|
+
this.frameSubmitSink = frameCommandBuffers;
|
|
916
|
+
|
|
859
917
|
// XR render target provider beginFrame hook
|
|
860
918
|
if (this.renderTargetProvider && !gpuError) {
|
|
861
919
|
this.renderTargetProvider.beginFrame();
|
|
@@ -869,9 +927,6 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
869
927
|
offscreen.ensureTargets(this.canvas.width, this.canvas.height);
|
|
870
928
|
}
|
|
871
929
|
|
|
872
|
-
// Collect command buffers from all phases and submit once at the end
|
|
873
|
-
// (reduces CPU→GPU sync points from 3+ per frame to 1).
|
|
874
|
-
const frameCommandBuffers: GPUCommandBuffer[] = [];
|
|
875
930
|
if (!gpuError) {
|
|
876
931
|
// 2D mode with viewportCount=0: clear the canvas if a clearColor is
|
|
877
932
|
// configured, then let the afterFrame callback do custom rendering.
|
|
@@ -922,21 +977,6 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
922
977
|
frameCommandBuffers.push(...passMailbox.drain("preSubmit"));
|
|
923
978
|
}
|
|
924
979
|
|
|
925
|
-
// Submit all command buffers for this frame in a single queue.submit() call
|
|
926
|
-
if (frameCommandBuffers.length > 0) {
|
|
927
|
-
this.device!.queue.submit(frameCommandBuffers);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// Read GPU timer results asynchronously (1-frame latency).
|
|
931
|
-
// Must be called AFTER queue.submit() — readGpuTimers() calls mapAsync on
|
|
932
|
-
// the read buffer, and a mapped/mapping-pending buffer cannot be used in a
|
|
933
|
-
// submitted command buffer (the copy-to-readBuffer was encoded above).
|
|
934
|
-
if (this.gpuProfiler) {
|
|
935
|
-
this.gpuProfiler.readGpuTimers().then(() => {
|
|
936
|
-
// Results available for next frame
|
|
937
|
-
}).catch(() => {});
|
|
938
|
-
}
|
|
939
|
-
|
|
940
980
|
// Record telemetry
|
|
941
981
|
if (this.telemetryCollector) {
|
|
942
982
|
this.telemetryCollector.recordFrame(dt * 1000);
|
|
@@ -975,43 +1015,68 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
975
1015
|
this.frameTriangles = 0;
|
|
976
1016
|
}
|
|
977
1017
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1018
|
+
try {
|
|
1019
|
+
// XR render target provider endFrame hook (skip on GPU error). Its
|
|
1020
|
+
// submits are captured by frameSubmitSink and join the batch.
|
|
1021
|
+
if (!gpuError && this.renderTargetProvider && this.device) {
|
|
1022
|
+
this.renderTargetProvider.endFrame(this.device.createCommandEncoder());
|
|
1023
|
+
}
|
|
982
1024
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
.filter((c) => c.hasContent())
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1025
|
+
// After frame callback — game submits here append to the batch in
|
|
1026
|
+
// order, still ahead of the UI composite below.
|
|
1027
|
+
this.callbacks.afterFrame?.(dt, this.elapsedTime);
|
|
1028
|
+
this.rendererModuleHost?.dispatchFrame("afterFrame", dt, this.elapsedTime);
|
|
1029
|
+
|
|
1030
|
+
// Composite registered screen-space UI (html-ui panels) on top of the
|
|
1031
|
+
// final image (skip on GPU error to avoid cascade). This must run AFTER
|
|
1032
|
+
// afterFrame — 2D games (viewportCount=0) do their custom rendering in
|
|
1033
|
+
// the afterFrame callback, so drawing UI earlier would put it underneath
|
|
1034
|
+
// their clear pass.
|
|
1035
|
+
if (!gpuError && this.device && this.context) {
|
|
1036
|
+
const compositors = this.uiCompositorOrder.filter((c) => c.hasContent());
|
|
1037
|
+
const uiCanvasView = compositors.length > 0
|
|
1038
|
+
? this.getSurfaceTexture()?.createView()
|
|
1039
|
+
: undefined;
|
|
1040
|
+
if (uiCanvasView) {
|
|
1041
|
+
const uiEncoder = this.device.createCommandEncoder();
|
|
1042
|
+
const uiPass = uiEncoder.beginRenderPass({
|
|
1043
|
+
colorAttachments: [{
|
|
1044
|
+
view: uiCanvasView,
|
|
1045
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1046
|
+
loadOp: "load" as GPULoadOp,
|
|
1047
|
+
storeOp: "store" as GPUStoreOp,
|
|
1048
|
+
}],
|
|
1049
|
+
});
|
|
1050
|
+
compositors.forEach((c) => { c.render(uiPass, this.canvas.width, this.canvas.height); });
|
|
1051
|
+
uiPass.end();
|
|
1052
|
+
// "preUi" slot — worker passes ordered ahead of the UI-composite
|
|
1053
|
+
// buffer inside the end-of-frame submit.
|
|
1054
|
+
frameCommandBuffers.push(...(passMailbox?.drain("preUi") ?? []), uiEncoder.finish());
|
|
1055
|
+
}
|
|
1014
1056
|
}
|
|
1057
|
+
} finally {
|
|
1058
|
+
// End of frame: disarm the submit sink and flush everything recorded
|
|
1059
|
+
// this frame — engine passes, captured callback submits, mailbox
|
|
1060
|
+
// passes, the UI composite — in a single queue.submit(). Runs even on
|
|
1061
|
+
// a callback throw so already-recorded work still lands.
|
|
1062
|
+
this.frameSubmitSink = null;
|
|
1063
|
+
if (frameCommandBuffers.length > 0 && this.device) {
|
|
1064
|
+
try {
|
|
1065
|
+
this.device.queue.submit(frameCommandBuffers);
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
log.error("game-renderer", `queue.submit failed: ${(err as Error).message ?? err}`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Read GPU timer results asynchronously (1-frame latency).
|
|
1073
|
+
// Must be called AFTER queue.submit() — readGpuTimers() calls mapAsync on
|
|
1074
|
+
// the read buffer, and a mapped/mapping-pending buffer cannot be used in a
|
|
1075
|
+
// submitted command buffer (the copy-to-readBuffer was encoded above).
|
|
1076
|
+
if (this.gpuProfiler) {
|
|
1077
|
+
this.gpuProfiler.readGpuTimers().then(() => {
|
|
1078
|
+
// Results available for next frame
|
|
1079
|
+
}).catch(() => {});
|
|
1015
1080
|
}
|
|
1016
1081
|
|
|
1017
1082
|
// Present the surface (native wgpu requires explicit presentation;
|
|
@@ -1220,7 +1285,7 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
1220
1285
|
|
|
1221
1286
|
private createDepthTexture(w: number, h: number): GPUTextureView {
|
|
1222
1287
|
if (!this.device) throw new Error("No device");
|
|
1223
|
-
const key =
|
|
1288
|
+
const key = w * 65536 + h;
|
|
1224
1289
|
let tex = this.depthTextures.get(key);
|
|
1225
1290
|
if (tex) {
|
|
1226
1291
|
// LRU: move to most-recently-used by re-inserting.
|
|
@@ -1314,9 +1379,7 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
1314
1379
|
* this helper only runs the compositors.
|
|
1315
1380
|
*/
|
|
1316
1381
|
protected renderScreenUiCompositors(): void {
|
|
1317
|
-
const compositors =
|
|
1318
|
-
.filter((c) => c.hasContent())
|
|
1319
|
-
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
1382
|
+
const compositors = this.uiCompositorOrder.filter((c) => c.hasContent());
|
|
1320
1383
|
if (compositors.length === 0 || !this.device || !this.context) return;
|
|
1321
1384
|
const canvasView = this.getSurfaceTexture()?.createView();
|
|
1322
1385
|
if (!canvasView) return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ChannelLayout, GridLayerLayout, HeaderViews } from "./types";
|
|
2
1
|
import { describeHeaderValidation } from "./errors";
|
|
2
|
+
import type { ChannelLayout, GridLayerLayout, HeaderViews } from "./types";
|
|
3
3
|
|
|
4
4
|
function createHeaderViews(sab: SharedArrayBuffer, headerSize: number): HeaderViews {
|
|
5
5
|
const u32Length = headerSize / 4;
|
|
@@ -66,6 +66,32 @@ export class GridReader {
|
|
|
66
66
|
return this.getSequence() !== lastSeen;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* True while the writer holds an open write window (sequence is odd).
|
|
71
|
+
* Readers can use this to cheaply defer sync work past a publish.
|
|
72
|
+
*/
|
|
73
|
+
isWriteInProgress(): boolean {
|
|
74
|
+
return (Atomics.load(this.seqArr, 0) & 1) !== 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Run `fn` against a consistent snapshot of the channel. The sequence
|
|
79
|
+
* word is a seqlock: odd while the writer is publishing, even when the
|
|
80
|
+
* data is stable. The read is retried while the word is odd or changes
|
|
81
|
+
* mid-read; after maxRetries it returns whatever the last attempt read
|
|
82
|
+
* rather than starving the reader (writers publish every tick).
|
|
83
|
+
*/
|
|
84
|
+
readConsistent<T>(fn: () => T, maxRetries = 64): T {
|
|
85
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
86
|
+
const s1 = Atomics.load(this.seqArr, 0);
|
|
87
|
+
if ((s1 & 1) === 0) {
|
|
88
|
+
const out = fn();
|
|
89
|
+
if (Atomics.load(this.seqArr, 0) === s1) return out;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return fn();
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
snapshot(): Record<string, unknown> {
|
|
70
96
|
const data: Record<string, unknown> = {};
|
|
71
97
|
data["sequence"] = this.getSequence();
|
|
@@ -88,6 +114,8 @@ export class GridWriter {
|
|
|
88
114
|
layers: Record<string, Float32Array | Int32Array | Uint32Array | Float64Array>;
|
|
89
115
|
private seqArr: Int32Array;
|
|
90
116
|
private layout: ChannelLayout;
|
|
117
|
+
/** True while a beginWrite() window is open (sequence is odd). */
|
|
118
|
+
private writeOpen = false;
|
|
91
119
|
|
|
92
120
|
constructor(sab: SharedArrayBuffer, layout: ChannelLayout) {
|
|
93
121
|
this.layout = layout;
|
|
@@ -107,10 +135,35 @@ export class GridWriter {
|
|
|
107
135
|
this.header.u32[this.layout.header.versionIndex] = this.layout.version;
|
|
108
136
|
}
|
|
109
137
|
|
|
110
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Open a write window — marks the sequence word odd so readers know a
|
|
140
|
+
* publish is in progress. Idempotent so mutating paths can call it
|
|
141
|
+
* unconditionally.
|
|
142
|
+
*/
|
|
143
|
+
beginWrite(): void {
|
|
144
|
+
if (this.writeOpen) return;
|
|
145
|
+
this.writeOpen = true;
|
|
146
|
+
Atomics.add(this.seqArr, 0, 1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Close the write window opened by beginWrite() — sequence back to even. */
|
|
150
|
+
endWrite(): void {
|
|
151
|
+
if (!this.writeOpen) return;
|
|
152
|
+
this.writeOpen = false;
|
|
111
153
|
Atomics.add(this.seqArr, 0, 1);
|
|
112
154
|
}
|
|
113
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Publish a commit. Without an open write window it steps +2 (even→even)
|
|
158
|
+
* so the sequence always lands on a stable value while still advancing
|
|
159
|
+
* once per publish; inside a window it closes it (+1, odd→even).
|
|
160
|
+
*/
|
|
161
|
+
bumpSequence(): void {
|
|
162
|
+
const step = this.writeOpen ? 1 : 2;
|
|
163
|
+
this.writeOpen = false;
|
|
164
|
+
Atomics.add(this.seqArr, 0, step);
|
|
165
|
+
}
|
|
166
|
+
|
|
114
167
|
getSequence(): number {
|
|
115
168
|
return Atomics.load(this.seqArr, 0);
|
|
116
169
|
}
|
|
@@ -67,6 +67,32 @@ export class RecordReader {
|
|
|
67
67
|
return this.getSequence() !== lastSeen;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* True while the writer holds an open write window (sequence is odd).
|
|
72
|
+
* Readers can use this to cheaply defer sync work past a publish.
|
|
73
|
+
*/
|
|
74
|
+
isWriteInProgress(): boolean {
|
|
75
|
+
return (Atomics.load(this.seqArr, 0) & 1) !== 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Run `fn` against a consistent snapshot of the channel. The sequence
|
|
80
|
+
* word is a seqlock: odd while the writer is publishing, even when the
|
|
81
|
+
* data is stable. The read is retried while the word is odd or changes
|
|
82
|
+
* mid-read; after maxRetries it returns whatever the last attempt read
|
|
83
|
+
* rather than starving the reader (writers publish every tick).
|
|
84
|
+
*/
|
|
85
|
+
readConsistent<T>(fn: () => T, maxRetries = 64): T {
|
|
86
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
87
|
+
const s1 = Atomics.load(this.seqArr, 0);
|
|
88
|
+
if ((s1 & 1) === 0) {
|
|
89
|
+
const out = fn();
|
|
90
|
+
if (Atomics.load(this.seqArr, 0) === s1) return out;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return fn();
|
|
94
|
+
}
|
|
95
|
+
|
|
70
96
|
snapshot(): Record<string, unknown> {
|
|
71
97
|
const data: Record<string, unknown> = {};
|
|
72
98
|
if (this.layout.fields) {
|
|
@@ -93,6 +119,8 @@ export class RecordWriter {
|
|
|
93
119
|
fields: Record<string, Float32Array | Int32Array | Uint32Array | Float64Array>;
|
|
94
120
|
private seqArr: Int32Array;
|
|
95
121
|
private layout: ChannelLayout;
|
|
122
|
+
/** True while a beginWrite() window is open (sequence is odd). */
|
|
123
|
+
private writeOpen = false;
|
|
96
124
|
|
|
97
125
|
constructor(sab: SharedArrayBuffer, layout: ChannelLayout) {
|
|
98
126
|
this.layout = layout;
|
|
@@ -112,18 +140,33 @@ export class RecordWriter {
|
|
|
112
140
|
this.header.u32[this.layout.header.versionIndex] = this.layout.version;
|
|
113
141
|
}
|
|
114
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Open a write window — marks the sequence word odd so readers know a
|
|
145
|
+
* publish is in progress. Idempotent so mutating paths can call it
|
|
146
|
+
* unconditionally.
|
|
147
|
+
*/
|
|
115
148
|
beginWrite(): void {
|
|
116
|
-
|
|
117
|
-
|
|
149
|
+
if (this.writeOpen) return;
|
|
150
|
+
this.writeOpen = true;
|
|
151
|
+
Atomics.add(this.seqArr, 0, 1);
|
|
118
152
|
}
|
|
119
153
|
|
|
154
|
+
/** Close the write window opened by beginWrite() — sequence back to even. */
|
|
120
155
|
endWrite(): void {
|
|
121
|
-
|
|
122
|
-
|
|
156
|
+
if (!this.writeOpen) return;
|
|
157
|
+
this.writeOpen = false;
|
|
158
|
+
Atomics.add(this.seqArr, 0, 1);
|
|
123
159
|
}
|
|
124
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Publish a commit. Without an open write window it steps +2 (even→even)
|
|
163
|
+
* so the sequence always lands on a stable value while still advancing
|
|
164
|
+
* once per publish; inside a window it closes it (+1, odd→even).
|
|
165
|
+
*/
|
|
125
166
|
bumpSequence(): void {
|
|
126
|
-
|
|
167
|
+
const step = this.writeOpen ? 1 : 2;
|
|
168
|
+
this.writeOpen = false;
|
|
169
|
+
Atomics.add(this.seqArr, 0, step);
|
|
127
170
|
}
|
|
128
171
|
|
|
129
172
|
getSequence(): number {
|