@downdraft/engine 0.2.0 → 0.3.1
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 +0 -6
- package/app/src/renderer/feature-log.ts +1 -9
- package/app/src/renderer/mcp-harness.ts +8 -11
- package/app/src/renderer/standard-automation-tools.ts +8 -145
- package/core/src/index.ts +2 -2
- package/core/src/module/host.ts +3 -6
- package/core/src/module/index.ts +1 -1
- package/core/src/module/module.ts +4 -47
- package/core/src/physics/interface.ts +5 -0
- package/core/src/platform/render-surface.ts +13 -0
- package/core/src/platform/rpc.ts +4 -1
- package/core/src/render/canvas-resize-watcher.ts +1 -10
- package/core/src/render/game-renderer.ts +164 -79
- package/core/src/render/index.ts +0 -2
- package/core/src/render/input-manager.ts +7 -7
- package/core/src/render/renderer-module-host.ts +2 -4
- 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/ui-font-scale.ts +5 -38
- package/core/src/util/index.ts +1 -0
- 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 +119 -5
- package/core/src/worker/sim-worker-base.ts +5 -2
- package/core/src/worker/task-pool.ts +4 -0
- package/dist/.dd-manifest.json +893 -908
- package/dist/app/src/renderer/bootstrap.js +0 -4
- package/dist/app/src/renderer/feature-log.js +1 -10
- package/dist/app/src/renderer/mcp-harness.js +8 -11
- package/dist/app/src/renderer/standard-automation-tools.js +6 -138
- package/dist/core/src/index.js +2 -2
- package/dist/core/src/module/host.js +3 -6
- package/dist/core/src/platform/rpc.js +2 -1
- package/dist/core/src/render/canvas-resize-watcher.js +1 -10
- package/dist/core/src/render/game-renderer.js +166 -74
- package/dist/core/src/render/index.js +0 -1
- package/dist/core/src/render/input-manager.js +7 -7
- package/dist/core/src/render/renderer-module-host.js +2 -4
- 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/ui-font-scale.js +5 -38
- package/dist/core/src/util/index.js +1 -0
- 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 +105 -8
- package/dist/core/src/worker/sim-worker-base.js +5 -2
- package/dist/core/src/worker/task-pool.js +5 -0
- package/dist/libraries/blitz-ui/src/native-module.js +8 -1
- package/dist/libraries/devtools/src/backend.js +35 -9
- package/dist/libraries/devtools/src/collectors.js +73 -22
- package/dist/libraries/devtools/src/native-providers.js +0 -2
- package/dist/libraries/entities/src/model-renderer.js +87 -27
- package/dist/libraries/entities/src/shaders/model.wgsl +13 -4
- package/dist/libraries/networking/src/index.js +1 -0
- package/dist/libraries/networking/src/ws-server.js +387 -0
- package/dist/libraries/physics-rapier/src/api.js +15 -0
- package/dist/libraries/physics-rapier/src/backend.js +15 -0
- package/dist/libraries/physics-rapier/src/rapier-backend.js +5 -0
- package/dist/libraries/postfx/src/post-process-stack.js +1 -3
- package/dist/libraries/sand/src/materials.js +2 -32
- package/dist/libraries/sand/src/sand-world.js +0 -3
- package/dist/modules/devtools/src/api.js +8 -23
- package/dist/modules/devtools/src/blitz/fonts.js +75 -0
- package/dist/modules/devtools/src/blitz/host.js +124 -30
- 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 +26 -12
- 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/devtools/src/data-bridge.js +0 -11
- package/dist/modules/devtools/src/index.js +0 -6
- package/dist/modules/devtools/src/init.js +8 -42
- package/dist/modules/devtools/src/scene-inspector.js +1 -4
- package/dist/modules/devtools/src/worker-sync.js +3 -11
- package/dist/modules/html-ui/src/doc-backend.js +52 -3
- package/dist/modules/html-ui/src/host.js +6 -3
- package/dist/test/src/vision-test.js +1 -3
- package/libraries/blitz-ui/native/Cargo.toml +6 -0
- package/libraries/blitz-ui/native-osr/Cargo.toml +3 -1
- package/libraries/blitz-ui/src/native-module.ts +8 -1
- package/libraries/devtools/src/backend.ts +38 -12
- package/libraries/devtools/src/collectors.ts +75 -22
- package/libraries/devtools/src/native-providers.ts +0 -2
- package/libraries/entities/src/model-renderer.ts +88 -27
- package/libraries/entities/src/shaders/model.wgsl +13 -4
- package/libraries/networking/src/index.ts +4 -0
- package/libraries/networking/src/ws-server.ts +397 -0
- package/libraries/physics-rapier/src/api.ts +15 -0
- package/libraries/physics-rapier/src/backend.ts +15 -0
- package/libraries/physics-rapier/src/rapier-backend.ts +6 -0
- package/libraries/postfx/src/post-process-stack.ts +1 -2
- package/libraries/sand/src/materials.ts +2 -32
- package/libraries/sand/src/sand-world.ts +0 -4
- package/modules/devtools/README.md +1 -3
- package/modules/devtools/src/api.ts +10 -34
- package/modules/devtools/src/blitz/fonts.ts +78 -0
- package/modules/devtools/src/blitz/host.ts +107 -31
- 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 +24 -12
- 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/devtools/src/data-bridge.ts +0 -16
- package/modules/devtools/src/index.ts +1 -11
- package/modules/devtools/src/init.ts +8 -51
- package/modules/devtools/src/scene-inspector.ts +2 -6
- package/modules/devtools/src/types.ts +5 -90
- package/modules/devtools/src/worker-sync.ts +3 -13
- package/modules/html-ui/src/doc-backend.ts +67 -4
- package/modules/html-ui/src/host.ts +11 -3
- package/modules/html-ui/src/protocol.ts +1 -1
- package/package.json +4 -5
- package/test/src/vision-test.ts +2 -4
- package/core/src/render/sab-canvas-overlay.ts +0 -122
- package/dist/core/src/render/sab-canvas-overlay.js +0 -83
- package/dist/modules/devtools/src/debug-overlay.js +0 -181
- package/dist/modules/devtools/src/doctor-panel.js +0 -163
- package/dist/modules/devtools/src/label-overlay.js +0 -129
- package/dist/modules/devtools/src/material-stats-panel.js +0 -171
- package/dist/modules/devtools/src/sim-stats-panel.js +0 -250
- package/modules/devtools/src/debug-overlay.ts +0 -238
- package/modules/devtools/src/doctor-panel.ts +0 -182
- package/modules/devtools/src/label-overlay.ts +0 -154
- package/modules/devtools/src/material-stats-panel.ts +0 -188
- package/modules/devtools/src/sim-stats-panel.ts +0 -272
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// logic through callback hooks.
|
|
7
7
|
// ============================================================================
|
|
8
8
|
import { UIInputRouter } from "../input/ui-router.js";
|
|
9
|
-
import {
|
|
9
|
+
import { getNativeHost } from "../platform/runtime.js";
|
|
10
10
|
import { TelemetryCollector } from "../telemetry/collector.js";
|
|
11
11
|
import { GPUProfiler } from "../telemetry/gpu-profiler.js";
|
|
12
12
|
import { GPUResourceTracker } from "../telemetry/gpu-resource-tracker.js";
|
|
@@ -42,6 +42,9 @@ export class GameRenderer {
|
|
|
42
42
|
uiInputRouter = null;
|
|
43
43
|
/** Screen-space compositors (html-ui panels etc.) drawn in the UI pass. */
|
|
44
44
|
uiCompositors = new Set();
|
|
45
|
+
/** uiCompositors as a pre-sorted array — rebuilt on add/remove so the
|
|
46
|
+
* frame loop doesn't spread+sort the set every frame. */
|
|
47
|
+
uiCompositorOrder = [];
|
|
45
48
|
// Render loop state
|
|
46
49
|
running = false;
|
|
47
50
|
deviceLost = false;
|
|
@@ -75,11 +78,22 @@ export class GameRenderer {
|
|
|
75
78
|
frameCtx = null;
|
|
76
79
|
// Depth texture cache (used when the graph does not own depth)
|
|
77
80
|
// LRU: Map insertion order = access order (delete + re-set on access).
|
|
81
|
+
// Numeric key packs (w,h) — a `${w}x${h}` string key allocated a fresh
|
|
82
|
+
// string per lookup per viewport per frame.
|
|
78
83
|
depthTextures = new Map();
|
|
79
84
|
depthTextureCacheSize;
|
|
80
85
|
// Frame stats
|
|
81
86
|
frameDrawCalls = 0;
|
|
82
87
|
frameTriangles = 0;
|
|
88
|
+
// Per-frame submit interception: while non-null, queue.submit() calls
|
|
89
|
+
// (game callbacks, render-target providers, helpers) append their buffers
|
|
90
|
+
// here in call order instead of crossing into wgpu — the whole frame then
|
|
91
|
+
// lands in ONE queue.submit at end-of-frame. Games legitimately submit
|
|
92
|
+
// inside afterFrame callbacks (2D renderers, overlays), so folding the UI
|
|
93
|
+
// composite into the main batch requires capturing those submits rather
|
|
94
|
+
// than submitting early or reordering.
|
|
95
|
+
frameSubmitSink = null;
|
|
96
|
+
submitInterceptorDevice = null;
|
|
83
97
|
// Callbacks
|
|
84
98
|
callbacks = {};
|
|
85
99
|
// Offscreen mode (set by game for postprocessing)
|
|
@@ -150,6 +164,7 @@ export class GameRenderer {
|
|
|
150
164
|
return false;
|
|
151
165
|
}
|
|
152
166
|
this.device = borrowedDevice ?? await this.requestDeviceFromAdapter(adapter);
|
|
167
|
+
this.installQueueSubmitInterceptor(this.device);
|
|
153
168
|
this.adapter = adapter;
|
|
154
169
|
// The host configured the surface with its own usage set at creation —
|
|
155
170
|
// reconfiguring here would tear down the swapchain and race the host's
|
|
@@ -214,8 +229,16 @@ export class GameRenderer {
|
|
|
214
229
|
setViewportCount: (count) => this.setViewportCount(count),
|
|
215
230
|
getUIInputRouter: () => this.uiInputRouter,
|
|
216
231
|
registerUiCompositor: (c) => {
|
|
232
|
+
const isNew = !this.uiCompositors.has(c);
|
|
217
233
|
this.uiCompositors.add(c);
|
|
218
|
-
|
|
234
|
+
if (isNew) {
|
|
235
|
+
this.uiCompositorOrder = [...this.uiCompositors].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
236
|
+
}
|
|
237
|
+
return () => {
|
|
238
|
+
if (this.uiCompositors.delete(c)) {
|
|
239
|
+
this.uiCompositorOrder = [...this.uiCompositors].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
240
|
+
}
|
|
241
|
+
};
|
|
219
242
|
},
|
|
220
243
|
});
|
|
221
244
|
// Initial viewport layout
|
|
@@ -328,6 +351,7 @@ export class GameRenderer {
|
|
|
328
351
|
const device = await this.requestDeviceFromAdapter(adapter);
|
|
329
352
|
device.lost.then((info) => this.handleDeviceLost(info));
|
|
330
353
|
this.device = device;
|
|
354
|
+
this.installQueueSubmitInterceptor(device);
|
|
331
355
|
this.adapter = adapter;
|
|
332
356
|
installShaderValidationGuard(device);
|
|
333
357
|
this.gpuResourceTracker = new GPUResourceTracker();
|
|
@@ -381,17 +405,15 @@ export class GameRenderer {
|
|
|
381
405
|
*/
|
|
382
406
|
setResolutionScale(scale) {
|
|
383
407
|
this.resolutionScale = Math.max(0.25, Math.min(1, scale));
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
log.warn("game-renderer", "resolutionScale is not supported on the native surface (swapchain must match the window size) — ignoring");
|
|
394
|
-
}
|
|
408
|
+
// On the native host the swapchain extent must equal the window's
|
|
409
|
+
// real size — wgpu compares the configured size against the surface's
|
|
410
|
+
// currentExtent at acquire time and returns Outdated forever when
|
|
411
|
+
// they differ. Shrinking canvas.width/height would also corrupt
|
|
412
|
+
// clientWidth (UI layout + input coords). Resolution scaling needs
|
|
413
|
+
// an offscreen-render + upscale path to work here; for now it is a
|
|
414
|
+
// no-op rather than a brick.
|
|
415
|
+
if (this.resolutionScale < 1) {
|
|
416
|
+
log.warn("game-renderer", "resolutionScale is not supported on the native surface (swapchain must match the window size) — ignoring");
|
|
395
417
|
}
|
|
396
418
|
if (this.lastCssW > 0)
|
|
397
419
|
this.onResize(this.lastCssW, this.lastCssH, this.lastRawDpr);
|
|
@@ -407,7 +429,7 @@ export class GameRenderer {
|
|
|
407
429
|
// performance knob when fill rate is a concern. On native the
|
|
408
430
|
// swapchain is window-sized (see setResolutionScale) so the scale is
|
|
409
431
|
// never applied to the backing store.
|
|
410
|
-
const cappedDpr = dpr
|
|
432
|
+
const cappedDpr = dpr;
|
|
411
433
|
this.dpr = cappedDpr;
|
|
412
434
|
const w = Math.round(cssWidth * cappedDpr);
|
|
413
435
|
const h = Math.round(cssHeight * cappedDpr);
|
|
@@ -563,14 +585,40 @@ export class GameRenderer {
|
|
|
563
585
|
}
|
|
564
586
|
catch (err) {
|
|
565
587
|
log.error("game-renderer", `Render loop error: ${err.message}\n${err.stack}`);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
588
|
+
// No per-error device.lost.then here: the device already has exactly
|
|
589
|
+
// one lost subscription (init + reinit paths), and on native each
|
|
590
|
+
// .lost access mints a fresh promise — per-error attaches accumulated
|
|
591
|
+
// duplicate watchers during an error storm.
|
|
571
592
|
}
|
|
572
593
|
this.scheduleRender();
|
|
573
594
|
};
|
|
595
|
+
/**
|
|
596
|
+
* Redirect queue.submit() into frameSubmitSink while a frame is in
|
|
597
|
+
* flight. Submission order is preserved — buffers land in the sink in
|
|
598
|
+
* the order submit() was called — so mid-frame submits (uploads in
|
|
599
|
+
* beforeFrame, a game's afterFrame pass, endFrame hooks) still execute
|
|
600
|
+
* ahead of work recorded later, just inside the single end-of-frame
|
|
601
|
+
* queue.submit rather than as extra queue-lock + driver calls.
|
|
602
|
+
*/
|
|
603
|
+
installQueueSubmitInterceptor(device) {
|
|
604
|
+
if (this.submitInterceptorDevice === device)
|
|
605
|
+
return;
|
|
606
|
+
this.submitInterceptorDevice = device;
|
|
607
|
+
const queue = device.queue;
|
|
608
|
+
if (queue.__ddSubmitIntercepted)
|
|
609
|
+
return;
|
|
610
|
+
queue.__ddSubmitIntercepted = true;
|
|
611
|
+
const origSubmit = queue.submit.bind(queue);
|
|
612
|
+
queue.submit = (commandBuffers) => {
|
|
613
|
+
const sink = this.frameSubmitSink;
|
|
614
|
+
if (!sink) {
|
|
615
|
+
origSubmit(commandBuffers);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
for (const cb of commandBuffers)
|
|
619
|
+
sink.push(cb);
|
|
620
|
+
};
|
|
621
|
+
}
|
|
574
622
|
renderFrame() {
|
|
575
623
|
const now = performance.now();
|
|
576
624
|
// Frame rate limiter: phase accumulator (skip during XR — XR drives its own cadence)
|
|
@@ -615,6 +663,14 @@ export class GameRenderer {
|
|
|
615
663
|
// simulation stays responsive. The flag is cleared by recovery code that
|
|
616
664
|
// recreates the invalid resources.
|
|
617
665
|
const gpuError = this.gpuProfiler?.hasUncapturedError() ?? false;
|
|
666
|
+
// Collect command buffers from all phases and submit once at the end
|
|
667
|
+
// of the frame — one queue.submit per frame instead of 2-4. The sink
|
|
668
|
+
// is armed BEFORE beginFrame so submit() calls from hooks and game
|
|
669
|
+
// callbacks (beforeFrame uploads, afterFrame render passes, the XR
|
|
670
|
+
// endFrame encoder) are captured in call order into the same batch
|
|
671
|
+
// rather than crossing into wgpu individually.
|
|
672
|
+
const frameCommandBuffers = [];
|
|
673
|
+
this.frameSubmitSink = frameCommandBuffers;
|
|
618
674
|
// XR render target provider beginFrame hook
|
|
619
675
|
if (this.renderTargetProvider && !gpuError) {
|
|
620
676
|
this.renderTargetProvider.beginFrame();
|
|
@@ -625,9 +681,6 @@ export class GameRenderer {
|
|
|
625
681
|
if (offscreen && useOffscreen && offscreen.ensureTargets && !gpuError) {
|
|
626
682
|
offscreen.ensureTargets(this.canvas.width, this.canvas.height);
|
|
627
683
|
}
|
|
628
|
-
// Collect command buffers from all phases and submit once at the end
|
|
629
|
-
// (reduces CPU→GPU sync points from 3+ per frame to 1).
|
|
630
|
-
const frameCommandBuffers = [];
|
|
631
684
|
if (!gpuError) {
|
|
632
685
|
// 2D mode with viewportCount=0: clear the canvas if a clearColor is
|
|
633
686
|
// configured, then let the afterFrame callback do custom rendering.
|
|
@@ -674,19 +727,6 @@ export class GameRenderer {
|
|
|
674
727
|
if (passMailbox) {
|
|
675
728
|
frameCommandBuffers.push(...passMailbox.drain("preSubmit"));
|
|
676
729
|
}
|
|
677
|
-
// Submit all command buffers for this frame in a single queue.submit() call
|
|
678
|
-
if (frameCommandBuffers.length > 0) {
|
|
679
|
-
this.device.queue.submit(frameCommandBuffers);
|
|
680
|
-
}
|
|
681
|
-
// Read GPU timer results asynchronously (1-frame latency).
|
|
682
|
-
// Must be called AFTER queue.submit() — readGpuTimers() calls mapAsync on
|
|
683
|
-
// the read buffer, and a mapped/mapping-pending buffer cannot be used in a
|
|
684
|
-
// submitted command buffer (the copy-to-readBuffer was encoded above).
|
|
685
|
-
if (this.gpuProfiler) {
|
|
686
|
-
this.gpuProfiler.readGpuTimers().then(() => {
|
|
687
|
-
// Results available for next frame
|
|
688
|
-
}).catch(() => { });
|
|
689
|
-
}
|
|
690
730
|
// Record telemetry
|
|
691
731
|
if (this.telemetryCollector) {
|
|
692
732
|
this.telemetryCollector.recordFrame(dt * 1000);
|
|
@@ -721,42 +761,72 @@ export class GameRenderer {
|
|
|
721
761
|
this.frameDrawCalls = 0;
|
|
722
762
|
this.frameTriangles = 0;
|
|
723
763
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
764
|
+
try {
|
|
765
|
+
// XR render target provider endFrame hook (skip on GPU error). Its
|
|
766
|
+
// submits are captured by frameSubmitSink and join the batch.
|
|
767
|
+
if (!gpuError && this.renderTargetProvider && this.device) {
|
|
768
|
+
this.renderTargetProvider.endFrame(this.device.createCommandEncoder());
|
|
769
|
+
}
|
|
770
|
+
// After frame callback — game submits here append to the batch in
|
|
771
|
+
// order, still ahead of the UI composite below.
|
|
772
|
+
this.callbacks.afterFrame?.(dt, this.elapsedTime);
|
|
773
|
+
this.rendererModuleHost?.dispatchFrame("afterFrame", dt, this.elapsedTime);
|
|
774
|
+
// Composite registered screen-space UI (html-ui panels) on top of the
|
|
775
|
+
// final image (skip on GPU error to avoid cascade). This must run AFTER
|
|
776
|
+
// afterFrame — 2D games (viewportCount=0) do their custom rendering in
|
|
777
|
+
// the afterFrame callback, so drawing UI earlier would put it underneath
|
|
778
|
+
// their clear pass. The texture comes from peek: a frame the game
|
|
779
|
+
// skipped must not be started just to draw UI on a blank swapchain.
|
|
780
|
+
if (!gpuError && this.device && this.context) {
|
|
781
|
+
const compositors = this.uiCompositorOrder.filter((c) => c.hasContent());
|
|
782
|
+
const uiCanvasView = compositors.length > 0
|
|
783
|
+
? this.getFrameSurfaceTexture()?.createView()
|
|
784
|
+
: undefined;
|
|
785
|
+
if (uiCanvasView) {
|
|
786
|
+
const uiEncoder = this.device.createCommandEncoder();
|
|
787
|
+
const uiPass = uiEncoder.beginRenderPass({
|
|
788
|
+
colorAttachments: [{
|
|
789
|
+
view: uiCanvasView,
|
|
790
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
791
|
+
loadOp: "load",
|
|
792
|
+
storeOp: "store",
|
|
793
|
+
}],
|
|
794
|
+
});
|
|
795
|
+
// Panel rects are CSS px — pass the CSS surface size so
|
|
796
|
+
// rect/surface maps correctly on HiDPI displays (canvas.width is
|
|
797
|
+
// physical px; identical where scaleFactor=1).
|
|
798
|
+
compositors.forEach((c) => { c.render(uiPass, this.canvas.clientWidth, this.canvas.clientHeight); });
|
|
799
|
+
uiPass.end();
|
|
800
|
+
// "preUi" slot — worker passes ordered ahead of the UI-composite
|
|
801
|
+
// buffer inside the end-of-frame submit.
|
|
802
|
+
frameCommandBuffers.push(...(passMailbox?.drain("preUi") ?? []), uiEncoder.finish());
|
|
803
|
+
}
|
|
804
|
+
}
|
|
727
805
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
? this.getSurfaceTexture()?.createView()
|
|
742
|
-
: undefined;
|
|
743
|
-
if (uiCanvasView) {
|
|
744
|
-
const uiEncoder = this.device.createCommandEncoder();
|
|
745
|
-
const uiPass = uiEncoder.beginRenderPass({
|
|
746
|
-
colorAttachments: [{
|
|
747
|
-
view: uiCanvasView,
|
|
748
|
-
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
749
|
-
loadOp: "load",
|
|
750
|
-
storeOp: "store",
|
|
751
|
-
}],
|
|
752
|
-
});
|
|
753
|
-
compositors.forEach((c) => { c.render(uiPass, this.canvas.width, this.canvas.height); });
|
|
754
|
-
uiPass.end();
|
|
755
|
-
// "preUi" slot — worker passes ordered ahead of the UI-composite
|
|
756
|
-
// submit so their output is visible to the compositor this frame.
|
|
757
|
-
this.device.queue.submit([...(passMailbox?.drain("preUi") ?? []), uiEncoder.finish()]);
|
|
806
|
+
finally {
|
|
807
|
+
// End of frame: disarm the submit sink and flush everything recorded
|
|
808
|
+
// this frame — engine passes, captured callback submits, mailbox
|
|
809
|
+
// passes, the UI composite — in a single queue.submit(). Runs even on
|
|
810
|
+
// a callback throw so already-recorded work still lands.
|
|
811
|
+
this.frameSubmitSink = null;
|
|
812
|
+
if (frameCommandBuffers.length > 0 && this.device) {
|
|
813
|
+
try {
|
|
814
|
+
this.device.queue.submit(frameCommandBuffers);
|
|
815
|
+
}
|
|
816
|
+
catch (err) {
|
|
817
|
+
log.error("game-renderer", `queue.submit failed: ${err.message ?? err}`);
|
|
818
|
+
}
|
|
758
819
|
}
|
|
759
820
|
}
|
|
821
|
+
// Read GPU timer results asynchronously (1-frame latency).
|
|
822
|
+
// Must be called AFTER queue.submit() — readGpuTimers() calls mapAsync on
|
|
823
|
+
// the read buffer, and a mapped/mapping-pending buffer cannot be used in a
|
|
824
|
+
// submitted command buffer (the copy-to-readBuffer was encoded above).
|
|
825
|
+
if (this.gpuProfiler) {
|
|
826
|
+
this.gpuProfiler.readGpuTimers().then(() => {
|
|
827
|
+
// Results available for next frame
|
|
828
|
+
}).catch(() => { });
|
|
829
|
+
}
|
|
760
830
|
// Present the surface (native wgpu requires explicit presentation;
|
|
761
831
|
// in browsers this is automatic at the end of the frame). Must be last —
|
|
762
832
|
// all submissions targeting the surface texture happen above.
|
|
@@ -785,6 +855,29 @@ export class GameRenderer {
|
|
|
785
855
|
return null;
|
|
786
856
|
}
|
|
787
857
|
}
|
|
858
|
+
/**
|
|
859
|
+
* The surface texture already acquired for the in-flight frame, or null
|
|
860
|
+
* when nothing has acquired one yet. End-of-frame overlays (the UI
|
|
861
|
+
* composite) use this so they join a frame rather than start one: on
|
|
862
|
+
* hosts with frame retention (native), acquiring here would fabricate a
|
|
863
|
+
* frame the game deliberately skipped (dirty-tracking) and present the
|
|
864
|
+
* overlay over a blank swapchain image — a full-screen flash. Falls
|
|
865
|
+
* back to acquiring on contexts without peek (DOM semantics:
|
|
866
|
+
* getCurrentTexture is idempotent within a frame and present is
|
|
867
|
+
* implicit, so there is nothing to defeat).
|
|
868
|
+
*/
|
|
869
|
+
getFrameSurfaceTexture() {
|
|
870
|
+
const peek = this.context?.peekCurrentTexture;
|
|
871
|
+
if (typeof peek === "function") {
|
|
872
|
+
try {
|
|
873
|
+
return peek.call(this.context) ?? null;
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return this.getSurfaceTexture();
|
|
880
|
+
}
|
|
788
881
|
renderViewport(viewportIdx, dt, offscreen) {
|
|
789
882
|
if (!this.device || !this.context)
|
|
790
883
|
return null;
|
|
@@ -951,7 +1044,7 @@ export class GameRenderer {
|
|
|
951
1044
|
createDepthTexture(w, h) {
|
|
952
1045
|
if (!this.device)
|
|
953
1046
|
throw new Error("No device");
|
|
954
|
-
const key =
|
|
1047
|
+
const key = w * 65536 + h;
|
|
955
1048
|
let tex = this.depthTextures.get(key);
|
|
956
1049
|
if (tex) {
|
|
957
1050
|
// LRU: move to most-recently-used by re-inserting.
|
|
@@ -1030,12 +1123,10 @@ export class GameRenderer {
|
|
|
1030
1123
|
* this helper only runs the compositors.
|
|
1031
1124
|
*/
|
|
1032
1125
|
renderScreenUiCompositors() {
|
|
1033
|
-
const compositors =
|
|
1034
|
-
.filter((c) => c.hasContent())
|
|
1035
|
-
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
1126
|
+
const compositors = this.uiCompositorOrder.filter((c) => c.hasContent());
|
|
1036
1127
|
if (compositors.length === 0 || !this.device || !this.context)
|
|
1037
1128
|
return;
|
|
1038
|
-
const canvasView = this.
|
|
1129
|
+
const canvasView = this.getFrameSurfaceTexture()?.createView();
|
|
1039
1130
|
if (!canvasView)
|
|
1040
1131
|
return;
|
|
1041
1132
|
const uiEncoder = this.device.createCommandEncoder();
|
|
@@ -1047,7 +1138,8 @@ export class GameRenderer {
|
|
|
1047
1138
|
storeOp: "store",
|
|
1048
1139
|
}],
|
|
1049
1140
|
});
|
|
1050
|
-
|
|
1141
|
+
// Panel rects are CSS px — see the main UI composite block above.
|
|
1142
|
+
compositors.forEach((c) => { c.render(uiPass, this.canvas.clientWidth, this.canvas.clientHeight); });
|
|
1051
1143
|
uiPass.end();
|
|
1052
1144
|
this.device.queue.submit([uiEncoder.finish()]);
|
|
1053
1145
|
}
|
|
@@ -57,7 +57,6 @@ export { PipelineCache } from "./pipeline.js";
|
|
|
57
57
|
export { RenderPass } from "./render-pass.js";
|
|
58
58
|
export { RendererInputBusImpl } from "./renderer-input-bus.js";
|
|
59
59
|
export { RendererModuleHost } from "./renderer-module-host.js";
|
|
60
|
-
export { SabCanvasOverlay } from "./sab-canvas-overlay.js";
|
|
61
60
|
export { clearShaderValidationDedup, createValidatedShaderModule, createValidatedShaderModuleAsync, installShaderValidationGuard } from "./shader-validator.js";
|
|
62
61
|
export { CSM_SHADER_CHUNK, POINT_SHADOW_SHADER_CHUNK, SPOT_SHADOW_SHADER_CHUNK } from "./shaders/shadow-chunks.js";
|
|
63
62
|
export { SkyboxRenderer } from "./skybox.js";
|
|
@@ -58,9 +58,9 @@ export class InputManager {
|
|
|
58
58
|
this.tryLockPointer();
|
|
59
59
|
}
|
|
60
60
|
}));
|
|
61
|
-
add(
|
|
61
|
+
add(this.canvas, "pointerlockchange", (() => {
|
|
62
62
|
const wasLocked = this.pointerLocked;
|
|
63
|
-
this.pointerLocked =
|
|
63
|
+
this.pointerLocked = this.canvas.pointerLocked === true;
|
|
64
64
|
if (this.pointerLocked) {
|
|
65
65
|
this.pointerLockRetryCount = 0;
|
|
66
66
|
if (this.pointerLockRetryTimer) {
|
|
@@ -76,7 +76,7 @@ export class InputManager {
|
|
|
76
76
|
this.mouseDelta.dy = 0;
|
|
77
77
|
}
|
|
78
78
|
}));
|
|
79
|
-
add(
|
|
79
|
+
add(this.canvas, "pointerlockerror", (() => {
|
|
80
80
|
// No action needed — tryLockPointer's fallback timer will retry.
|
|
81
81
|
}));
|
|
82
82
|
}
|
|
@@ -150,10 +150,10 @@ export class InputManager {
|
|
|
150
150
|
e.preventDefault();
|
|
151
151
|
}
|
|
152
152
|
}), { passive: false });
|
|
153
|
-
//
|
|
154
|
-
// sandjongg's tile canvas) would otherwise trigger the canvas's
|
|
155
|
-
// mouseleave and break UI hover/pressed state.
|
|
156
|
-
add(
|
|
153
|
+
// Window-level mouseleave: a second interactive canvas stacked above
|
|
154
|
+
// (e.g. sandjongg's tile canvas) would otherwise trigger the canvas's
|
|
155
|
+
// own mouseleave and break UI hover/pressed state.
|
|
156
|
+
add(window, "mouseleave", (() => {
|
|
157
157
|
this.uiInputRouter?.handlePointerLeave();
|
|
158
158
|
}));
|
|
159
159
|
add(this.canvas, "contextmenu", ((e) => {
|
|
@@ -50,8 +50,8 @@ export class RendererModuleHost {
|
|
|
50
50
|
this.inputBus = new RendererInputBusImpl(canvas);
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* Inject the DevTools API so renderer plugins can self-register
|
|
54
|
-
*
|
|
53
|
+
* Inject the DevTools API so renderer plugins can self-register data
|
|
54
|
+
* feeds via `ctx.devtools.registerDataFeed(...)` during their `register()`.
|
|
55
55
|
*/
|
|
56
56
|
setDevToolsAPI(api) {
|
|
57
57
|
this._devtools = api;
|
|
@@ -531,8 +531,6 @@ export class RendererModuleHost {
|
|
|
531
531
|
}
|
|
532
532
|
// No-op DevTools API stub — used when setDevToolsAPI() hasn't been called.
|
|
533
533
|
const NoopRendererDevToolsAPI = {
|
|
534
|
-
registerPanel: () => { },
|
|
535
|
-
registerOverlayToggle: () => { },
|
|
536
534
|
registerDataFeed: () => { },
|
|
537
535
|
registerCommand: () => { },
|
|
538
536
|
registerSABStat: () => { },
|
|
@@ -46,6 +46,31 @@ export class GridReader {
|
|
|
46
46
|
hasChanged(lastSeen) {
|
|
47
47
|
return this.getSequence() !== lastSeen;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* True while the writer holds an open write window (sequence is odd).
|
|
51
|
+
* Readers can use this to cheaply defer sync work past a publish.
|
|
52
|
+
*/
|
|
53
|
+
isWriteInProgress() {
|
|
54
|
+
return (Atomics.load(this.seqArr, 0) & 1) !== 0;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Run `fn` against a consistent snapshot of the channel. The sequence
|
|
58
|
+
* word is a seqlock: odd while the writer is publishing, even when the
|
|
59
|
+
* data is stable. The read is retried while the word is odd or changes
|
|
60
|
+
* mid-read; after maxRetries it returns whatever the last attempt read
|
|
61
|
+
* rather than starving the reader (writers publish every tick).
|
|
62
|
+
*/
|
|
63
|
+
readConsistent(fn, maxRetries = 64) {
|
|
64
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
65
|
+
const s1 = Atomics.load(this.seqArr, 0);
|
|
66
|
+
if ((s1 & 1) === 0) {
|
|
67
|
+
const out = fn();
|
|
68
|
+
if (Atomics.load(this.seqArr, 0) === s1)
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return fn();
|
|
73
|
+
}
|
|
49
74
|
snapshot() {
|
|
50
75
|
const data = {};
|
|
51
76
|
data["sequence"] = this.getSequence();
|
|
@@ -67,6 +92,8 @@ export class GridWriter {
|
|
|
67
92
|
layers;
|
|
68
93
|
seqArr;
|
|
69
94
|
layout;
|
|
95
|
+
/** True while a beginWrite() window is open (sequence is odd). */
|
|
96
|
+
writeOpen = false;
|
|
70
97
|
constructor(sab, layout) {
|
|
71
98
|
this.layout = layout;
|
|
72
99
|
this.header = createHeaderViews(sab, layout.header.size);
|
|
@@ -82,9 +109,34 @@ export class GridWriter {
|
|
|
82
109
|
this.header.u32[this.layout.header.magicIndex] = this.layout.magic;
|
|
83
110
|
this.header.u32[this.layout.header.versionIndex] = this.layout.version;
|
|
84
111
|
}
|
|
85
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Open a write window — marks the sequence word odd so readers know a
|
|
114
|
+
* publish is in progress. Idempotent so mutating paths can call it
|
|
115
|
+
* unconditionally.
|
|
116
|
+
*/
|
|
117
|
+
beginWrite() {
|
|
118
|
+
if (this.writeOpen)
|
|
119
|
+
return;
|
|
120
|
+
this.writeOpen = true;
|
|
86
121
|
Atomics.add(this.seqArr, 0, 1);
|
|
87
122
|
}
|
|
123
|
+
/** Close the write window opened by beginWrite() — sequence back to even. */
|
|
124
|
+
endWrite() {
|
|
125
|
+
if (!this.writeOpen)
|
|
126
|
+
return;
|
|
127
|
+
this.writeOpen = false;
|
|
128
|
+
Atomics.add(this.seqArr, 0, 1);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Publish a commit. Without an open write window it steps +2 (even→even)
|
|
132
|
+
* so the sequence always lands on a stable value while still advancing
|
|
133
|
+
* once per publish; inside a window it closes it (+1, odd→even).
|
|
134
|
+
*/
|
|
135
|
+
bumpSequence() {
|
|
136
|
+
const step = this.writeOpen ? 1 : 2;
|
|
137
|
+
this.writeOpen = false;
|
|
138
|
+
Atomics.add(this.seqArr, 0, step);
|
|
139
|
+
}
|
|
88
140
|
getSequence() {
|
|
89
141
|
return Atomics.load(this.seqArr, 0);
|
|
90
142
|
}
|
|
@@ -47,6 +47,31 @@ export class RecordReader {
|
|
|
47
47
|
hasChanged(lastSeen) {
|
|
48
48
|
return this.getSequence() !== lastSeen;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* True while the writer holds an open write window (sequence is odd).
|
|
52
|
+
* Readers can use this to cheaply defer sync work past a publish.
|
|
53
|
+
*/
|
|
54
|
+
isWriteInProgress() {
|
|
55
|
+
return (Atomics.load(this.seqArr, 0) & 1) !== 0;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run `fn` against a consistent snapshot of the channel. The sequence
|
|
59
|
+
* word is a seqlock: odd while the writer is publishing, even when the
|
|
60
|
+
* data is stable. The read is retried while the word is odd or changes
|
|
61
|
+
* mid-read; after maxRetries it returns whatever the last attempt read
|
|
62
|
+
* rather than starving the reader (writers publish every tick).
|
|
63
|
+
*/
|
|
64
|
+
readConsistent(fn, maxRetries = 64) {
|
|
65
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
66
|
+
const s1 = Atomics.load(this.seqArr, 0);
|
|
67
|
+
if ((s1 & 1) === 0) {
|
|
68
|
+
const out = fn();
|
|
69
|
+
if (Atomics.load(this.seqArr, 0) === s1)
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return fn();
|
|
74
|
+
}
|
|
50
75
|
snapshot() {
|
|
51
76
|
const data = {};
|
|
52
77
|
if (this.layout.fields) {
|
|
@@ -73,6 +98,8 @@ export class RecordWriter {
|
|
|
73
98
|
fields;
|
|
74
99
|
seqArr;
|
|
75
100
|
layout;
|
|
101
|
+
/** True while a beginWrite() window is open (sequence is odd). */
|
|
102
|
+
writeOpen = false;
|
|
76
103
|
constructor(sab, layout) {
|
|
77
104
|
this.layout = layout;
|
|
78
105
|
this.header = createHeaderViews(sab, layout.header.size);
|
|
@@ -88,16 +115,33 @@ export class RecordWriter {
|
|
|
88
115
|
this.header.u32[this.layout.header.magicIndex] = this.layout.magic;
|
|
89
116
|
this.header.u32[this.layout.header.versionIndex] = this.layout.version;
|
|
90
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Open a write window — marks the sequence word odd so readers know a
|
|
120
|
+
* publish is in progress. Idempotent so mutating paths can call it
|
|
121
|
+
* unconditionally.
|
|
122
|
+
*/
|
|
91
123
|
beginWrite() {
|
|
92
|
-
|
|
93
|
-
|
|
124
|
+
if (this.writeOpen)
|
|
125
|
+
return;
|
|
126
|
+
this.writeOpen = true;
|
|
127
|
+
Atomics.add(this.seqArr, 0, 1);
|
|
94
128
|
}
|
|
129
|
+
/** Close the write window opened by beginWrite() — sequence back to even. */
|
|
95
130
|
endWrite() {
|
|
96
|
-
|
|
97
|
-
|
|
131
|
+
if (!this.writeOpen)
|
|
132
|
+
return;
|
|
133
|
+
this.writeOpen = false;
|
|
134
|
+
Atomics.add(this.seqArr, 0, 1);
|
|
98
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Publish a commit. Without an open write window it steps +2 (even→even)
|
|
138
|
+
* so the sequence always lands on a stable value while still advancing
|
|
139
|
+
* once per publish; inside a window it closes it (+1, odd→even).
|
|
140
|
+
*/
|
|
99
141
|
bumpSequence() {
|
|
100
|
-
|
|
142
|
+
const step = this.writeOpen ? 1 : 2;
|
|
143
|
+
this.writeOpen = false;
|
|
144
|
+
Atomics.add(this.seqArr, 0, step);
|
|
101
145
|
}
|
|
102
146
|
getSequence() {
|
|
103
147
|
return Atomics.load(this.seqArr, 0);
|