@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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { UIInputRouter } from "../input/ui-router";
|
|
10
10
|
import type { RendererModule } from "../module/renderer-module";
|
|
11
11
|
import type { RenderSurface, RenderSurfaceContext } from "../platform/render-surface";
|
|
12
|
-
import {
|
|
12
|
+
import { getNativeHost } from "../platform/runtime";
|
|
13
13
|
import { TelemetryCollector } from "../telemetry/collector";
|
|
14
14
|
import type { GPUAdapterInfo as GPUAdapterInfoData } from "../telemetry/gpu-profiler";
|
|
15
15
|
import { GPUProfiler, type FrameGraphData, type GPUInfo } from "../telemetry/gpu-profiler";
|
|
@@ -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);
|
|
@@ -605,17 +630,15 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
605
630
|
*/
|
|
606
631
|
setResolutionScale(scale: number): void {
|
|
607
632
|
this.resolutionScale = Math.max(0.25, Math.min(1, scale));
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
log.warn("game-renderer", "resolutionScale is not supported on the native surface (swapchain must match the window size) — ignoring");
|
|
618
|
-
}
|
|
633
|
+
// On the native host the swapchain extent must equal the window's
|
|
634
|
+
// real size — wgpu compares the configured size against the surface's
|
|
635
|
+
// currentExtent at acquire time and returns Outdated forever when
|
|
636
|
+
// they differ. Shrinking canvas.width/height would also corrupt
|
|
637
|
+
// clientWidth (UI layout + input coords). Resolution scaling needs
|
|
638
|
+
// an offscreen-render + upscale path to work here; for now it is a
|
|
639
|
+
// no-op rather than a brick.
|
|
640
|
+
if (this.resolutionScale < 1) {
|
|
641
|
+
log.warn("game-renderer", "resolutionScale is not supported on the native surface (swapchain must match the window size) — ignoring");
|
|
619
642
|
}
|
|
620
643
|
if (this.lastCssW > 0) this.onResize(this.lastCssW, this.lastCssH, this.lastRawDpr);
|
|
621
644
|
}
|
|
@@ -632,7 +655,7 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
632
655
|
// performance knob when fill rate is a concern. On native the
|
|
633
656
|
// swapchain is window-sized (see setResolutionScale) so the scale is
|
|
634
657
|
// never applied to the backing store.
|
|
635
|
-
const cappedDpr = dpr
|
|
658
|
+
const cappedDpr = dpr;
|
|
636
659
|
this.dpr = cappedDpr;
|
|
637
660
|
const w = Math.round(cssWidth * cappedDpr);
|
|
638
661
|
const h = Math.round(cssHeight * cappedDpr);
|
|
@@ -798,15 +821,39 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
798
821
|
this.renderFrame();
|
|
799
822
|
} catch (err) {
|
|
800
823
|
log.error("game-renderer", `Render loop error: ${(err as Error).message}\n${(err as Error).stack}`);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
}
|
|
824
|
+
// No per-error device.lost.then here: the device already has exactly
|
|
825
|
+
// one lost subscription (init + reinit paths), and on native each
|
|
826
|
+
// .lost access mints a fresh promise — per-error attaches accumulated
|
|
827
|
+
// duplicate watchers during an error storm.
|
|
806
828
|
}
|
|
807
829
|
this.scheduleRender();
|
|
808
830
|
};
|
|
809
831
|
|
|
832
|
+
/**
|
|
833
|
+
* Redirect queue.submit() into frameSubmitSink while a frame is in
|
|
834
|
+
* flight. Submission order is preserved — buffers land in the sink in
|
|
835
|
+
* the order submit() was called — so mid-frame submits (uploads in
|
|
836
|
+
* beforeFrame, a game's afterFrame pass, endFrame hooks) still execute
|
|
837
|
+
* ahead of work recorded later, just inside the single end-of-frame
|
|
838
|
+
* queue.submit rather than as extra queue-lock + driver calls.
|
|
839
|
+
*/
|
|
840
|
+
private installQueueSubmitInterceptor(device: GPUDevice): void {
|
|
841
|
+
if (this.submitInterceptorDevice === device) return;
|
|
842
|
+
this.submitInterceptorDevice = device;
|
|
843
|
+
const queue = device.queue as any;
|
|
844
|
+
if (queue.__ddSubmitIntercepted) return;
|
|
845
|
+
queue.__ddSubmitIntercepted = true;
|
|
846
|
+
const origSubmit: GPUQueue["submit"] = queue.submit.bind(queue);
|
|
847
|
+
queue.submit = (commandBuffers: Iterable<GPUCommandBuffer>) => {
|
|
848
|
+
const sink = this.frameSubmitSink;
|
|
849
|
+
if (!sink) {
|
|
850
|
+
origSubmit(commandBuffers);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
for (const cb of commandBuffers) sink.push(cb);
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
810
857
|
private renderFrame(): void {
|
|
811
858
|
const now = performance.now();
|
|
812
859
|
|
|
@@ -856,6 +903,15 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
856
903
|
// recreates the invalid resources.
|
|
857
904
|
const gpuError = this.gpuProfiler?.hasUncapturedError() ?? false;
|
|
858
905
|
|
|
906
|
+
// Collect command buffers from all phases and submit once at the end
|
|
907
|
+
// of the frame — one queue.submit per frame instead of 2-4. The sink
|
|
908
|
+
// is armed BEFORE beginFrame so submit() calls from hooks and game
|
|
909
|
+
// callbacks (beforeFrame uploads, afterFrame render passes, the XR
|
|
910
|
+
// endFrame encoder) are captured in call order into the same batch
|
|
911
|
+
// rather than crossing into wgpu individually.
|
|
912
|
+
const frameCommandBuffers: GPUCommandBuffer[] = [];
|
|
913
|
+
this.frameSubmitSink = frameCommandBuffers;
|
|
914
|
+
|
|
859
915
|
// XR render target provider beginFrame hook
|
|
860
916
|
if (this.renderTargetProvider && !gpuError) {
|
|
861
917
|
this.renderTargetProvider.beginFrame();
|
|
@@ -869,9 +925,6 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
869
925
|
offscreen.ensureTargets(this.canvas.width, this.canvas.height);
|
|
870
926
|
}
|
|
871
927
|
|
|
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
928
|
if (!gpuError) {
|
|
876
929
|
// 2D mode with viewportCount=0: clear the canvas if a clearColor is
|
|
877
930
|
// configured, then let the afterFrame callback do custom rendering.
|
|
@@ -922,21 +975,6 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
922
975
|
frameCommandBuffers.push(...passMailbox.drain("preSubmit"));
|
|
923
976
|
}
|
|
924
977
|
|
|
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
978
|
// Record telemetry
|
|
941
979
|
if (this.telemetryCollector) {
|
|
942
980
|
this.telemetryCollector.recordFrame(dt * 1000);
|
|
@@ -975,43 +1013,72 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
975
1013
|
this.frameTriangles = 0;
|
|
976
1014
|
}
|
|
977
1015
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1016
|
+
try {
|
|
1017
|
+
// XR render target provider endFrame hook (skip on GPU error). Its
|
|
1018
|
+
// submits are captured by frameSubmitSink and join the batch.
|
|
1019
|
+
if (!gpuError && this.renderTargetProvider && this.device) {
|
|
1020
|
+
this.renderTargetProvider.endFrame(this.device.createCommandEncoder());
|
|
1021
|
+
}
|
|
982
1022
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
.
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1023
|
+
// After frame callback — game submits here append to the batch in
|
|
1024
|
+
// order, still ahead of the UI composite below.
|
|
1025
|
+
this.callbacks.afterFrame?.(dt, this.elapsedTime);
|
|
1026
|
+
this.rendererModuleHost?.dispatchFrame("afterFrame", dt, this.elapsedTime);
|
|
1027
|
+
|
|
1028
|
+
// Composite registered screen-space UI (html-ui panels) on top of the
|
|
1029
|
+
// final image (skip on GPU error to avoid cascade). This must run AFTER
|
|
1030
|
+
// afterFrame — 2D games (viewportCount=0) do their custom rendering in
|
|
1031
|
+
// the afterFrame callback, so drawing UI earlier would put it underneath
|
|
1032
|
+
// their clear pass. The texture comes from peek: a frame the game
|
|
1033
|
+
// skipped must not be started just to draw UI on a blank swapchain.
|
|
1034
|
+
if (!gpuError && this.device && this.context) {
|
|
1035
|
+
const compositors = this.uiCompositorOrder.filter((c) => c.hasContent());
|
|
1036
|
+
const uiCanvasView = compositors.length > 0
|
|
1037
|
+
? this.getFrameSurfaceTexture()?.createView()
|
|
1038
|
+
: undefined;
|
|
1039
|
+
if (uiCanvasView) {
|
|
1040
|
+
const uiEncoder = this.device.createCommandEncoder();
|
|
1041
|
+
const uiPass = uiEncoder.beginRenderPass({
|
|
1042
|
+
colorAttachments: [{
|
|
1043
|
+
view: uiCanvasView,
|
|
1044
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1045
|
+
loadOp: "load" as GPULoadOp,
|
|
1046
|
+
storeOp: "store" as GPUStoreOp,
|
|
1047
|
+
}],
|
|
1048
|
+
});
|
|
1049
|
+
// Panel rects are CSS px — pass the CSS surface size so
|
|
1050
|
+
// rect/surface maps correctly on HiDPI displays (canvas.width is
|
|
1051
|
+
// physical px; identical where scaleFactor=1).
|
|
1052
|
+
compositors.forEach((c) => { c.render(uiPass, this.canvas.clientWidth, this.canvas.clientHeight); });
|
|
1053
|
+
uiPass.end();
|
|
1054
|
+
// "preUi" slot — worker passes ordered ahead of the UI-composite
|
|
1055
|
+
// buffer inside the end-of-frame submit.
|
|
1056
|
+
frameCommandBuffers.push(...(passMailbox?.drain("preUi") ?? []), uiEncoder.finish());
|
|
1057
|
+
}
|
|
1014
1058
|
}
|
|
1059
|
+
} finally {
|
|
1060
|
+
// End of frame: disarm the submit sink and flush everything recorded
|
|
1061
|
+
// this frame — engine passes, captured callback submits, mailbox
|
|
1062
|
+
// passes, the UI composite — in a single queue.submit(). Runs even on
|
|
1063
|
+
// a callback throw so already-recorded work still lands.
|
|
1064
|
+
this.frameSubmitSink = null;
|
|
1065
|
+
if (frameCommandBuffers.length > 0 && this.device) {
|
|
1066
|
+
try {
|
|
1067
|
+
this.device.queue.submit(frameCommandBuffers);
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
log.error("game-renderer", `queue.submit failed: ${(err as Error).message ?? err}`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Read GPU timer results asynchronously (1-frame latency).
|
|
1075
|
+
// Must be called AFTER queue.submit() — readGpuTimers() calls mapAsync on
|
|
1076
|
+
// the read buffer, and a mapped/mapping-pending buffer cannot be used in a
|
|
1077
|
+
// submitted command buffer (the copy-to-readBuffer was encoded above).
|
|
1078
|
+
if (this.gpuProfiler) {
|
|
1079
|
+
this.gpuProfiler.readGpuTimers().then(() => {
|
|
1080
|
+
// Results available for next frame
|
|
1081
|
+
}).catch(() => {});
|
|
1015
1082
|
}
|
|
1016
1083
|
|
|
1017
1084
|
// Present the surface (native wgpu requires explicit presentation;
|
|
@@ -1043,6 +1110,25 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
1043
1110
|
}
|
|
1044
1111
|
}
|
|
1045
1112
|
|
|
1113
|
+
/**
|
|
1114
|
+
* The surface texture already acquired for the in-flight frame, or null
|
|
1115
|
+
* when nothing has acquired one yet. End-of-frame overlays (the UI
|
|
1116
|
+
* composite) use this so they join a frame rather than start one: on
|
|
1117
|
+
* hosts with frame retention (native), acquiring here would fabricate a
|
|
1118
|
+
* frame the game deliberately skipped (dirty-tracking) and present the
|
|
1119
|
+
* overlay over a blank swapchain image — a full-screen flash. Falls
|
|
1120
|
+
* back to acquiring on contexts without peek (DOM semantics:
|
|
1121
|
+
* getCurrentTexture is idempotent within a frame and present is
|
|
1122
|
+
* implicit, so there is nothing to defeat).
|
|
1123
|
+
*/
|
|
1124
|
+
private getFrameSurfaceTexture(): GPUTexture | null {
|
|
1125
|
+
const peek = (this.context as { peekCurrentTexture?: () => GPUTexture | null } | null)?.peekCurrentTexture;
|
|
1126
|
+
if (typeof peek === "function") {
|
|
1127
|
+
try { return peek.call(this.context) ?? null; } catch { return null; }
|
|
1128
|
+
}
|
|
1129
|
+
return this.getSurfaceTexture();
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1046
1132
|
private renderViewport(viewportIdx: number, dt: number, offscreen: OffscreenMode | null): GPUCommandBuffer | null {
|
|
1047
1133
|
if (!this.device || !this.context) return null;
|
|
1048
1134
|
|
|
@@ -1220,7 +1306,7 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
1220
1306
|
|
|
1221
1307
|
private createDepthTexture(w: number, h: number): GPUTextureView {
|
|
1222
1308
|
if (!this.device) throw new Error("No device");
|
|
1223
|
-
const key =
|
|
1309
|
+
const key = w * 65536 + h;
|
|
1224
1310
|
let tex = this.depthTextures.get(key);
|
|
1225
1311
|
if (tex) {
|
|
1226
1312
|
// LRU: move to most-recently-used by re-inserting.
|
|
@@ -1314,11 +1400,9 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
1314
1400
|
* this helper only runs the compositors.
|
|
1315
1401
|
*/
|
|
1316
1402
|
protected renderScreenUiCompositors(): void {
|
|
1317
|
-
const compositors =
|
|
1318
|
-
.filter((c) => c.hasContent())
|
|
1319
|
-
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
1403
|
+
const compositors = this.uiCompositorOrder.filter((c) => c.hasContent());
|
|
1320
1404
|
if (compositors.length === 0 || !this.device || !this.context) return;
|
|
1321
|
-
const canvasView = this.
|
|
1405
|
+
const canvasView = this.getFrameSurfaceTexture()?.createView();
|
|
1322
1406
|
if (!canvasView) return;
|
|
1323
1407
|
const uiEncoder = this.device.createCommandEncoder();
|
|
1324
1408
|
const uiPass = uiEncoder.beginRenderPass({
|
|
@@ -1329,7 +1413,8 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
1329
1413
|
storeOp: "store" as GPUStoreOp,
|
|
1330
1414
|
}],
|
|
1331
1415
|
});
|
|
1332
|
-
|
|
1416
|
+
// Panel rects are CSS px — see the main UI composite block above.
|
|
1417
|
+
compositors.forEach((c) => { c.render(uiPass, this.canvas.clientWidth, this.canvas.clientHeight); });
|
|
1333
1418
|
uiPass.end();
|
|
1334
1419
|
this.device.queue.submit([uiEncoder.finish()]);
|
|
1335
1420
|
}
|
package/core/src/render/index.ts
CHANGED
|
@@ -111,8 +111,6 @@ export type { FrameGraphBuilder } from "./render-pass";
|
|
|
111
111
|
export { RendererInputBusImpl } from "./renderer-input-bus";
|
|
112
112
|
export { RendererModuleHost } from "./renderer-module-host";
|
|
113
113
|
export type { RendererModuleHostCallbacks } from "./renderer-module-host";
|
|
114
|
-
export { SabCanvasOverlay } from "./sab-canvas-overlay";
|
|
115
|
-
export type { SabCanvasOverlayOptions } from "./sab-canvas-overlay";
|
|
116
114
|
export { clearShaderValidationDedup, createValidatedShaderModule, createValidatedShaderModuleAsync, installShaderValidationGuard } from "./shader-validator";
|
|
117
115
|
export { CSM_SHADER_CHUNK, POINT_SHADOW_SHADER_CHUNK, SPOT_SHADOW_SHADER_CHUNK } from "./shaders/shadow-chunks";
|
|
118
116
|
export { SkyboxRenderer } from "./skybox";
|
|
@@ -79,9 +79,9 @@ export class InputManager {
|
|
|
79
79
|
}
|
|
80
80
|
}) as EventListener);
|
|
81
81
|
|
|
82
|
-
add(
|
|
82
|
+
add(this.canvas, "pointerlockchange", (() => {
|
|
83
83
|
const wasLocked = this.pointerLocked;
|
|
84
|
-
this.pointerLocked =
|
|
84
|
+
this.pointerLocked = this.canvas.pointerLocked === true;
|
|
85
85
|
if (this.pointerLocked) {
|
|
86
86
|
this.pointerLockRetryCount = 0;
|
|
87
87
|
if (this.pointerLockRetryTimer) {
|
|
@@ -97,7 +97,7 @@ export class InputManager {
|
|
|
97
97
|
}
|
|
98
98
|
}) as EventListener);
|
|
99
99
|
|
|
100
|
-
add(
|
|
100
|
+
add(this.canvas, "pointerlockerror", (() => {
|
|
101
101
|
// No action needed — tryLockPointer's fallback timer will retry.
|
|
102
102
|
}) as EventListener);
|
|
103
103
|
}
|
|
@@ -180,10 +180,10 @@ export class InputManager {
|
|
|
180
180
|
}
|
|
181
181
|
}) as EventListener, { passive: false });
|
|
182
182
|
|
|
183
|
-
//
|
|
184
|
-
// sandjongg's tile canvas) would otherwise trigger the canvas's
|
|
185
|
-
// mouseleave and break UI hover/pressed state.
|
|
186
|
-
add(
|
|
183
|
+
// Window-level mouseleave: a second interactive canvas stacked above
|
|
184
|
+
// (e.g. sandjongg's tile canvas) would otherwise trigger the canvas's
|
|
185
|
+
// own mouseleave and break UI hover/pressed state.
|
|
186
|
+
add(window, "mouseleave", (() => {
|
|
187
187
|
this.uiInputRouter?.handlePointerLeave();
|
|
188
188
|
}) as EventListener);
|
|
189
189
|
|
|
@@ -116,8 +116,8 @@ export class RendererModuleHost {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* Inject the DevTools API so renderer plugins can self-register
|
|
120
|
-
*
|
|
119
|
+
* Inject the DevTools API so renderer plugins can self-register data
|
|
120
|
+
* feeds via `ctx.devtools.registerDataFeed(...)` during their `register()`.
|
|
121
121
|
*/
|
|
122
122
|
setDevToolsAPI(api: ModuleDevToolsAPI): void {
|
|
123
123
|
this._devtools = api;
|
|
@@ -635,8 +635,6 @@ export class RendererModuleHost {
|
|
|
635
635
|
|
|
636
636
|
// No-op DevTools API stub — used when setDevToolsAPI() hasn't been called.
|
|
637
637
|
const NoopRendererDevToolsAPI: ModuleDevToolsAPI = {
|
|
638
|
-
registerPanel: () => {},
|
|
639
|
-
registerOverlayToggle: () => {},
|
|
640
638
|
registerDataFeed: () => {},
|
|
641
639
|
registerCommand: () => {},
|
|
642
640
|
registerSABStat: () => {},
|
|
@@ -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 {
|