@forgeax/engine-runtime 0.1.20 → 0.1.23
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/README.md +105 -3
- package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts.map +1 -0
- package/dist/__tests__/helpers/register-runtime-components.d.ts.map +1 -1
- package/dist/__tests__/helpers/register-scene-components.d.ts.map +1 -1
- package/dist/__tests__/lights-preservation.fixup.test.d.ts +2 -0
- package/dist/__tests__/lights-preservation.fixup.test.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +4 -2
- package/dist/index.mjs.map +1 -1
- package/dist/sprite-param-values.d.ts +1 -1
- package/package.json +30 -30
- package/src/__tests__/active-camera.unit.test.ts +6 -8
- package/src/__tests__/animation-graph-asset-union.test-d.ts +2 -0
- package/src/__tests__/asset-brand-and-name-retirement.test.ts +4 -3
- package/src/__tests__/asset-registry-scene.test-d.ts +1 -0
- package/src/__tests__/asset-registry.recursive.spec.ts +1 -1
- package/src/__tests__/asset.unit.test.ts +5 -2
- package/src/__tests__/clamp-to-last.e2e.browser.test.ts +2 -2
- package/src/__tests__/collect-transient-roundtrip.test.ts +14 -3
- package/src/__tests__/components.test-d.ts +19 -9
- package/src/__tests__/components.unit.test.ts +36 -26
- package/src/__tests__/create-renderer-assembly.integration.test.ts +24 -0
- package/src/__tests__/create-renderer-gpu-pass-timing.unit.test.ts +32 -0
- package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +66 -0
- package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +9 -1
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +15 -2
- package/src/__tests__/errors.unit.test.ts +54 -20
- package/src/__tests__/extract-frames-merge.test.ts +51 -40
- package/src/__tests__/extract-skin-mismatch.unit.test.ts +6 -2
- package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +13 -0
- package/src/__tests__/geometry.unit.test.ts +7 -10
- package/src/__tests__/helpers/register-runtime-components.ts +9 -1
- package/src/__tests__/helpers/register-scene-components.ts +9 -2
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +10 -11
- package/src/__tests__/light-snapshot.test-d.ts +35 -1
- package/src/__tests__/lights-preservation.fixup.test.ts +125 -0
- package/src/__tests__/lights.unit.test.ts +223 -308
- package/src/__tests__/loader-name-survival.test.ts +8 -9
- package/src/__tests__/package-promotion.test.ts +7 -11
- package/src/__tests__/pbr-pipeline.unit.test.ts +22 -17
- package/src/__tests__/pipeline.unit.test.ts +68 -226
- package/src/__tests__/point-light-shadow.browser.test.ts +209 -83
- package/src/__tests__/point-light-shadow.unit.test.ts +24 -47
- package/src/__tests__/propagate-transforms.dawn.test.ts +15 -16
- package/src/__tests__/record-worldid-isolation.test.ts +16 -32
- package/src/__tests__/render-error-exhaustive.test-d.ts +73 -9
- package/src/__tests__/render-system-extract.test.ts +5 -5
- package/src/__tests__/render-system-mega.test.ts +30 -41
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +5 -1
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +5 -1
- package/src/__tests__/render-system-skin-bg.unit.test.ts +14 -9
- package/src/__tests__/renderer-recover.unit.test.ts +8 -0
- package/src/__tests__/renderer-surface.unit.test.ts +229 -5
- package/src/__tests__/resolveName-unified-entry.test.ts +6 -6
- package/src/__tests__/resolveName.test.ts +14 -2
- package/src/__tests__/shadow-csm-atlas.test.ts +41 -0
- package/src/__tests__/shadow-csm-cascade-loadop.test.ts +1 -0
- package/src/__tests__/shadow-csm-component.test.ts +31 -24
- package/src/__tests__/shadow-csm-extract.test.ts +156 -47
- package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +72 -0
- package/src/__tests__/shadow-csm-shader.dawn.test.ts +19 -0
- package/src/__tests__/shadow-csm-tile-consistency.test.ts +1 -0
- package/src/__tests__/shadow-csm-ubo.test.ts +43 -45
- package/src/__tests__/shadow-csm-viewport.browser.test.ts +29 -0
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +100 -24
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +1 -1
- package/src/__tests__/skin-parented-double-transform.test.ts +5 -5
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +4 -7
- package/src/__tests__/skylight-lazy-projection.test.ts +8 -5
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -13
- package/src/__tests__/ssao-bgl.test.ts +12 -17
- package/src/__tests__/ssao-passes.test.ts +19 -2
- package/src/__tests__/systems.unit.test.ts +69 -205
- package/src/__tests__/template-game-default-api-contract.test.ts +45 -23
- package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +10 -4
- package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +2 -2
- package/src/__tests__/view-ubo-layout.browser.test.ts +20 -10
- package/src/collect-scene-asset.ts +1 -1
- package/src/components/__tests__/transform-vec-schema.test.ts +2 -2
- package/src/createRenderer.ts +2 -0
- package/src/index.ts +1 -2
- package/src/sprite-param-values.ts +1 -1
- package/dist/.tsbuildinfo +0 -1
|
@@ -40,6 +40,8 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
|
|
|
40
40
|
return code;
|
|
41
41
|
case 'taa-unavailable':
|
|
42
42
|
return code;
|
|
43
|
+
case 'standard-profile-invalid':
|
|
44
|
+
return code;
|
|
43
45
|
case 'frame-receipt-stale':
|
|
44
46
|
return code;
|
|
45
47
|
case 'renderer-contract-failed':
|
|
@@ -64,11 +66,23 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
|
|
|
64
66
|
return code;
|
|
65
67
|
case 'equirect-projection-failed':
|
|
66
68
|
return code;
|
|
67
|
-
case '
|
|
69
|
+
case 'standard-light-budget-exceeded':
|
|
70
|
+
return code;
|
|
71
|
+
case 'standard-cluster-index-overflow':
|
|
72
|
+
return code;
|
|
73
|
+
case 'standard-cluster-transport-unavailable':
|
|
74
|
+
return code;
|
|
75
|
+
case 'render-target-descriptor-invalid':
|
|
76
|
+
return code;
|
|
77
|
+
case 'render-target-capability-missing':
|
|
78
|
+
return code;
|
|
79
|
+
case 'render-target-state-invalid':
|
|
68
80
|
return code;
|
|
69
|
-
case '
|
|
81
|
+
case 'render-target-operation-failed':
|
|
70
82
|
return code;
|
|
71
|
-
case '
|
|
83
|
+
case 'reflection-probe-budget-exceeded':
|
|
84
|
+
return code;
|
|
85
|
+
case 'render-intent-invalid':
|
|
72
86
|
return code;
|
|
73
87
|
case 'point-shadow-atlas-uninitialized':
|
|
74
88
|
return code;
|
|
@@ -114,6 +128,7 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
|
|
|
114
128
|
return code;
|
|
115
129
|
case 'points-lines-prepare-failed':
|
|
116
130
|
return code;
|
|
131
|
+
case 'light-resource-unavailable':
|
|
117
132
|
case 'projector-binding-failed':
|
|
118
133
|
return code;
|
|
119
134
|
case 'volume-owner-conflict':
|
|
@@ -186,6 +201,10 @@ function narrowRenderError(err: RenderError): void {
|
|
|
186
201
|
void err.detail.operation;
|
|
187
202
|
void err.detail.causes;
|
|
188
203
|
break;
|
|
204
|
+
case 'standard-profile-invalid':
|
|
205
|
+
void err.detail.field;
|
|
206
|
+
void err.detail.actual;
|
|
207
|
+
break;
|
|
189
208
|
case 'frame-receipt-stale':
|
|
190
209
|
void err.detail.frameId;
|
|
191
210
|
void err.detail.receiptGeneration;
|
|
@@ -210,7 +229,9 @@ function narrowRenderError(err: RenderError): void {
|
|
|
210
229
|
break;
|
|
211
230
|
case 'shadow-invalid-config':
|
|
212
231
|
void err.detail.field; // string
|
|
213
|
-
void err.detail.
|
|
232
|
+
void err.detail.actual; // number
|
|
233
|
+
void err.detail.bound;
|
|
234
|
+
void err.detail.reason; // string
|
|
214
235
|
break;
|
|
215
236
|
case 'environment-source-conflict':
|
|
216
237
|
void err.detail.owners;
|
|
@@ -237,17 +258,51 @@ function narrowRenderError(err: RenderError): void {
|
|
|
237
258
|
case 'equirect-projection-failed':
|
|
238
259
|
void err.detail.handle; // number
|
|
239
260
|
break;
|
|
240
|
-
case '
|
|
261
|
+
case 'standard-light-budget-exceeded':
|
|
241
262
|
void err.detail.actual; // number
|
|
242
263
|
void err.detail.budget; // number
|
|
243
264
|
break;
|
|
244
|
-
case '
|
|
265
|
+
case 'standard-cluster-index-overflow':
|
|
245
266
|
void err.detail.actual; // number
|
|
246
267
|
void err.detail.capacity; // number
|
|
247
268
|
break;
|
|
248
|
-
case '
|
|
249
|
-
void err.detail.
|
|
250
|
-
void err.detail.
|
|
269
|
+
case 'standard-cluster-transport-unavailable':
|
|
270
|
+
void err.detail.requested; // number
|
|
271
|
+
void err.detail.admitted; // 0
|
|
272
|
+
break;
|
|
273
|
+
case 'render-target-descriptor-invalid':
|
|
274
|
+
void err.detail.field;
|
|
275
|
+
void err.detail.value;
|
|
276
|
+
void err.detail.expected;
|
|
277
|
+
break;
|
|
278
|
+
case 'render-target-capability-missing':
|
|
279
|
+
void err.detail.operation;
|
|
280
|
+
void err.detail.requested;
|
|
281
|
+
void err.detail.capability;
|
|
282
|
+
void err.detail.actual;
|
|
283
|
+
break;
|
|
284
|
+
case 'render-target-state-invalid':
|
|
285
|
+
void err.detail.operation;
|
|
286
|
+
void err.detail.reason;
|
|
287
|
+
void err.detail.state;
|
|
288
|
+
void err.detail.generation;
|
|
289
|
+
break;
|
|
290
|
+
case 'render-target-operation-failed':
|
|
291
|
+
void err.detail.operation;
|
|
292
|
+
void err.detail.stage;
|
|
293
|
+
void err.detail.generation;
|
|
294
|
+
void err.detail.cause;
|
|
295
|
+
void err.detail.recovery;
|
|
296
|
+
break;
|
|
297
|
+
case 'reflection-probe-budget-exceeded':
|
|
298
|
+
void err.detail.actual;
|
|
299
|
+
void err.detail.budget;
|
|
300
|
+
break;
|
|
301
|
+
case 'render-intent-invalid':
|
|
302
|
+
void err.detail.component;
|
|
303
|
+
void err.detail.field;
|
|
304
|
+
void err.detail.value;
|
|
305
|
+
void err.detail.allowed;
|
|
251
306
|
break;
|
|
252
307
|
case 'point-shadow-atlas-uninitialized':
|
|
253
308
|
// No detail on this class.
|
|
@@ -338,6 +393,15 @@ function narrowRenderError(err: RenderError): void {
|
|
|
338
393
|
void err.detail.owner;
|
|
339
394
|
void err.detail.generation;
|
|
340
395
|
break;
|
|
396
|
+
case 'light-resource-unavailable':
|
|
397
|
+
void err.detail.entity;
|
|
398
|
+
void err.detail.feature;
|
|
399
|
+
void err.detail.generation;
|
|
400
|
+
void err.detail.sourceKey;
|
|
401
|
+
void err.detail.reason;
|
|
402
|
+
void err.expected;
|
|
403
|
+
void err.hint;
|
|
404
|
+
break;
|
|
341
405
|
case 'transmission-capability-missing':
|
|
342
406
|
void err.detail.material;
|
|
343
407
|
void err.detail.capability;
|
|
@@ -40,22 +40,22 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
40
40
|
{
|
|
41
41
|
// w9 -- render-system-extract consumer migration unit tests (M3, AC-07 / AC-15).
|
|
42
42
|
//
|
|
43
|
-
// Post-migration extract reads the single resolved `
|
|
43
|
+
// Post-migration extract reads the single resolved `GlobalTransform.world` mat4
|
|
44
44
|
// (written by propagateTransforms) for every world-space consumer; the legacy
|
|
45
45
|
// GlobalTransform-column-switch + per-snapshot `mat4.compose` are gone.
|
|
46
46
|
//
|
|
47
47
|
// Five consumer classes covered:
|
|
48
48
|
// 1. mesh-walk -> RenderableSnapshot.transform.world is the 16-float
|
|
49
49
|
// column-major world mat4 (parent x child), copied straight from the
|
|
50
|
-
//
|
|
50
|
+
// GlobalTransform.world view; no per-snapshot compose.
|
|
51
51
|
// 2. frustum cull -> the cull AABB follows the world mat4 (a child placed
|
|
52
52
|
// off-screen by its parent is culled; in-screen is kept) -- same-source
|
|
53
53
|
// with the rendered world (AC-05 carried forward).
|
|
54
54
|
// 3. camera view -> CameraSnapshot.position is the world-space translation
|
|
55
|
-
// (mat4.getTranslation of
|
|
55
|
+
// (mat4.getTranslation of GlobalTransform.world); a child camera reflects
|
|
56
56
|
// parent x child.
|
|
57
57
|
// 4. light position -> point / spot light position is the world-space
|
|
58
|
-
// translation extracted from
|
|
58
|
+
// translation extracted from GlobalTransform.world (mat4.getTranslation).
|
|
59
59
|
// 5. AC-15 zero-materialization -> the mesh-walk reads world through the
|
|
60
60
|
// column-level array view; it does NOT call `world.get` per renderable
|
|
61
61
|
// to materialize a `{}` whole-component object.
|
|
@@ -209,7 +209,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
209
209
|
expect(w?.[12]).toBeCloseTo(12, 5);
|
|
210
210
|
expect(w?.[13]).toBeCloseTo(3, 5);
|
|
211
211
|
expect(w?.[14]).toBeCloseTo(0, 5);
|
|
212
|
-
// The snapshot world must equal the entity's
|
|
212
|
+
// The snapshot world must equal the entity's GlobalTransform.world column byte-for-byte.
|
|
213
213
|
});
|
|
214
214
|
|
|
215
215
|
it('mesh-walk: flat root world mat4 equals compose(local)', () => {
|
|
@@ -436,10 +436,11 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
436
436
|
|
|
437
437
|
// DirectionalLight with merged shadow fields.
|
|
438
438
|
expect(errors).toHaveLength(0);
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
expect(log.
|
|
439
|
+
// Standard Cluster owns the shadow, G-buffer, lighting, forward, and
|
|
440
|
+
// output topology stages. Keep this structural rather than freezing a
|
|
441
|
+
// pass-count literal that changes when a named stage is split.
|
|
442
|
+
expect(log.beginRenderPassCount).toBeGreaterThan(log.queueSubmitCount);
|
|
443
|
+
expect(log.setPipelineCount).toBeGreaterThan(0);
|
|
443
444
|
expect(log.drawIndexedCount).toBe(1);
|
|
444
445
|
expect(log.encoderFinishCount).toBe(1); // one typed graph command encoder
|
|
445
446
|
expect(log.queueSubmitCount).toBe(1); // one submission for the frame graph
|
|
@@ -592,9 +593,13 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
592
593
|
|
|
593
594
|
// Soft path: no error fired (D-Q7 mirroring; LO §1.1 minimum semantic).
|
|
594
595
|
expect(errors).toHaveLength(0);
|
|
595
|
-
// Empty geometry does not remove graph-owned
|
|
596
|
+
// Empty geometry does not remove graph-owned attachment and output passes.
|
|
597
|
+
// With no DirectionalLight, exact-zero topology omits the directional
|
|
598
|
+
// shadow target and pass while retaining the spot/point attachments.
|
|
599
|
+
// The graph-owned final output transform is recorded after the frame
|
|
600
|
+
// attachment pass even when the scene has no geometry.
|
|
596
601
|
expect(log.beginRenderPassCount).toBe(12);
|
|
597
|
-
expect(log.queueSubmitCount).toBe(8); //
|
|
602
|
+
expect(log.queueSubmitCount).toBe(8); // spot + 6 cube faces + frame
|
|
598
603
|
// No geometry submitted.
|
|
599
604
|
expect(log.drawIndexedCount).toBe(0);
|
|
600
605
|
});
|
|
@@ -703,7 +708,7 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
703
708
|
Object.assign(log, makeLog());
|
|
704
709
|
drawPublished(renderer, world as WorldType);
|
|
705
710
|
|
|
706
|
-
expect(errors.some((e) => e.code === 'render-system-multi-camera')).toBe(
|
|
711
|
+
expect(errors.some((e) => e.code === 'render-system-multi-camera')).toBe(false);
|
|
707
712
|
// First archetype hit still rendered.
|
|
708
713
|
expect(log.drawIndexedCount).toBeGreaterThanOrEqual(1);
|
|
709
714
|
});
|
|
@@ -822,45 +827,28 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
822
827
|
// registration, the clear pass still runs (so visual debugging shows
|
|
823
828
|
// the cleared canvas rather than a stale frame).
|
|
824
829
|
// The invalid entity is skipped, while graph-owned attachment passes are
|
|
825
|
-
// still recorded after the
|
|
830
|
+
// still recorded after the spot and six cube-face attachment passes.
|
|
831
|
+
// The graph-owned final output transform still runs after an invalid
|
|
832
|
+
// renderable is skipped, preserving a visible cleared frame.
|
|
826
833
|
expect(log.beginRenderPassCount).toBe(12);
|
|
827
|
-
expect(log.queueSubmitCount).toBe(8); //
|
|
834
|
+
expect(log.queueSubmitCount).toBe(8); // spot + 6 cube faces + frame
|
|
828
835
|
});
|
|
829
836
|
|
|
830
837
|
it('internal exception fires webgpu-runtime-error with .detail = { error: string } and skips frame', async () => {
|
|
831
|
-
// Inject a
|
|
832
|
-
//
|
|
838
|
+
// Inject a raw command-encoder failure only after renderer construction
|
|
839
|
+
// has completed. RHI device creation intentionally leaves this raw
|
|
840
|
+
// boundary synchronous, so RenderSystem's outer try/catch must surface
|
|
841
|
+
// the failure as webgpu-runtime-error instead of a ready-time failure.
|
|
833
842
|
const log = makeLog();
|
|
834
843
|
const { device } = makeMockGPUDevice(log);
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
// instance fallback storage buffer seed (feat-20260513-instanced-
|
|
844
|
-
// mesh M3 T-M3-2) + 1 for the skin-palette-identity seed
|
|
845
|
-
// (feat-20260611 R2 / M8 / w28 IS-14, 255 identity mat4 = 16320 B)
|
|
846
|
-
// + 1 for the built-in tonemap params UBO defaultValue seed at register
|
|
847
|
-
// (feat-20260621 M-A3 / D-5: postProcess.register('forgeax::tonemap')
|
|
848
|
-
// eager-writes its 16 B defaultValue during ready).
|
|
849
|
-
// feat-20260625-spot-light-shadow-mapping w25 (scope-amend webkit-
|
|
850
|
-
// fallback): the boot-time spot lightViewProj UBO 256 B zero-init seed
|
|
851
|
-
// is GONE — the spot matrices folded into the View UBO tail (no standalone
|
|
852
|
-
// buffer to zero-init), so the boot writeBuffer count dropped 14 -> 13.
|
|
853
|
-
// HANDLE_QUAD joined the builtin upload loop in
|
|
854
|
-
// feat-20260520-2d-sprite-layer-mvp post-merge fix; HANDLE_SPHERE
|
|
855
|
-
// (id=4) joined in feat-20260529-fxaa-runtime-toggle;
|
|
856
|
-
// HANDLE_NINESLICE_QUAD (id=5) joined in feat-20260527-sprite-
|
|
857
|
-
// nineslice M2 / w12.
|
|
858
|
-
// Only fail after ready completes (call 14+) so the failure
|
|
859
|
-
// surfaces in RenderSystem rather than the ready Promise.
|
|
860
|
-
if (writeCallCount > 13) {
|
|
861
|
-
throw new Error('mock: writeBuffer NaN payload');
|
|
862
|
-
}
|
|
863
|
-
return originalWrite.call(baseQueue, ...args);
|
|
844
|
+
const raw = device as {
|
|
845
|
+
createCommandEncoder: (...args: unknown[]) => unknown;
|
|
846
|
+
};
|
|
847
|
+
const originalCreateCommandEncoder = raw.createCommandEncoder;
|
|
848
|
+
let failRecord = false;
|
|
849
|
+
raw.createCommandEncoder = (...args: unknown[]) => {
|
|
850
|
+
if (failRecord) throw new Error('mock: createCommandEncoder failure');
|
|
851
|
+
return originalCreateCommandEncoder(...args);
|
|
864
852
|
};
|
|
865
853
|
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
866
854
|
const engine = await importEngine();
|
|
@@ -868,6 +856,7 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
868
856
|
const renderer = (
|
|
869
857
|
await engine.createRenderer(canvas, {}, { shaderManifestUrl: buildManifestDataUrl() })
|
|
870
858
|
).unwrap();
|
|
859
|
+
failRecord = true;
|
|
871
860
|
|
|
872
861
|
const { World } = await importEcs();
|
|
873
862
|
const C = await importComponents();
|
|
@@ -907,7 +896,7 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
907
896
|
| undefined;
|
|
908
897
|
expect(runtimeDetail?.error).toBeDefined();
|
|
909
898
|
expect(typeof runtimeDetail?.error?.message).toBe('string');
|
|
910
|
-
expect(runtimeDetail?.error?.message).toContain('
|
|
899
|
+
expect(runtimeDetail?.error?.message).toContain('createCommandEncoder');
|
|
911
900
|
});
|
|
912
901
|
});
|
|
913
902
|
|
|
@@ -116,6 +116,7 @@ function makeMockGPUDevice(spies: DeviceSpies, maxTextureDimension2D = 8192): {
|
|
|
116
116
|
limits: { maxTextureDimension2D },
|
|
117
117
|
queue: {
|
|
118
118
|
submit: () => undefined,
|
|
119
|
+
onSubmittedWorkDone: async () => undefined,
|
|
119
120
|
writeBuffer: () => undefined,
|
|
120
121
|
writeTexture: () => undefined,
|
|
121
122
|
},
|
|
@@ -290,6 +291,7 @@ async function importEcs(): Promise<{
|
|
|
290
291
|
|
|
291
292
|
async function importComponents(): Promise<{
|
|
292
293
|
Transform: unknown;
|
|
294
|
+
GlobalTransform: unknown;
|
|
293
295
|
MeshFilter: unknown;
|
|
294
296
|
MeshRenderer: unknown;
|
|
295
297
|
Camera: unknown;
|
|
@@ -486,7 +488,9 @@ describe('record: per-submesh PBR material BG textureView (bug-20260610 D2 regre
|
|
|
486
488
|
const { renderer } = await setupRenderer(spies);
|
|
487
489
|
const errors: string[] = [];
|
|
488
490
|
renderer.subscribe((event) => {
|
|
489
|
-
if (event.kind === 'error')
|
|
491
|
+
if (event.kind === 'error') {
|
|
492
|
+
errors.push(event.error.code);
|
|
493
|
+
}
|
|
490
494
|
});
|
|
491
495
|
|
|
492
496
|
const world = await spawnPbrMultiMaterialScene();
|
|
@@ -96,6 +96,7 @@ function makeMockGPUDevice(spies: DeviceSpies): { device: unknown } {
|
|
|
96
96
|
limits: {},
|
|
97
97
|
queue: {
|
|
98
98
|
submit: () => undefined,
|
|
99
|
+
onSubmittedWorkDone: async () => undefined,
|
|
99
100
|
writeBuffer: () => undefined,
|
|
100
101
|
writeTexture: () => undefined,
|
|
101
102
|
},
|
|
@@ -231,6 +232,7 @@ async function importEcs(): Promise<{
|
|
|
231
232
|
}
|
|
232
233
|
async function importComponents(): Promise<{
|
|
233
234
|
Transform: unknown;
|
|
235
|
+
GlobalTransform: unknown;
|
|
234
236
|
MeshFilter: unknown;
|
|
235
237
|
MeshRenderer: unknown;
|
|
236
238
|
Camera: unknown;
|
|
@@ -413,7 +415,9 @@ describe('record: per-submesh transparency (feat-city-glb Bug 5)', () => {
|
|
|
413
415
|
const { renderer } = await setupRenderer(spies);
|
|
414
416
|
const errors: string[] = [];
|
|
415
417
|
renderer.subscribe((event) => {
|
|
416
|
-
if (event.kind === 'error')
|
|
418
|
+
if (event.kind === 'error') {
|
|
419
|
+
errors.push(event.error.code);
|
|
420
|
+
}
|
|
417
421
|
});
|
|
418
422
|
|
|
419
423
|
const { world } = await spawnMixedTransparentScene();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// feat-20260611 R2 / M8 / w29 (IS-14): record-stage skin BindGroup unit test.
|
|
2
2
|
//
|
|
3
3
|
// Why this file exists:
|
|
4
|
-
// M8 wires the record stage to bind a
|
|
5
|
-
// (binding 0 mesh-array UBO + binding 1 palette UBO
|
|
4
|
+
// M8 wires the record stage to bind a 3-binding `pbr-skin-mesh-array-bgl`
|
|
5
|
+
// (binding 0 mesh-array UBO + binding 1 current palette UBO + binding 2
|
|
6
|
+
// previous palette UBO) at group(2) when
|
|
6
7
|
// the per-entry skin discriminator (`entry.source.skin !== undefined`) is
|
|
7
8
|
// set. R1 of this loop discovered the prior 1-binding `pbr-mesh-bg`
|
|
8
9
|
// build path triggered every-frame BGL-mismatch device errors against
|
|
@@ -13,8 +14,8 @@
|
|
|
13
14
|
// fast in CI rather than at frame N.
|
|
14
15
|
//
|
|
15
16
|
// What this test asserts:
|
|
16
|
-
// (1) `buildPbrSkinLayouts` returns a
|
|
17
|
-
// `pbr-skin-mesh-array-bgl` with bindings 0 + 1
|
|
17
|
+
// (1) `buildPbrSkinLayouts` returns a 3-entry meshArrayBgl labeled
|
|
18
|
+
// `pbr-skin-mesh-array-bgl` with bindings 0 + 1 + 2 all
|
|
18
19
|
// `hasDynamicOffset: true`. This is the BGL the record stage
|
|
19
20
|
// must use to build a BG matching `pbr-skin-pl`.
|
|
20
21
|
// (2) The skin pipeline layout `pbr-skin-pl` references the SAME
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
// contract that R1 violated.
|
|
24
25
|
// (3) Regression guard: `buildPbrPipelineLayouts` (URP) returns a
|
|
25
26
|
// 1-entry meshArrayBgl labeled `pbr-mesh-array-bgl`. M8 must
|
|
26
|
-
// NOT leak the
|
|
27
|
+
// NOT leak the 3-entry skin BGL into the URP path.
|
|
27
28
|
//
|
|
28
29
|
// What this test deliberately does NOT cover:
|
|
29
30
|
// - The actual `pass.setBindGroup(2, ...)` dispatch site is exercised
|
|
@@ -83,7 +84,7 @@ function makeMockDevice(): { device: PbrPipelineDevice; capture: MockDeviceCaptu
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
describe('feat-20260611 R2 / M8 / w29 — record-stage skin BindGroup BGL contract', () => {
|
|
86
|
-
it('(1) buildPbrSkinLayouts returns a
|
|
87
|
+
it('(1) buildPbrSkinLayouts returns a 3-entry meshArrayBgl labeled pbr-skin-mesh-array-bgl with bindings 0 + 1 + 2 all hasDynamicOffset:true (storage path)', () => {
|
|
87
88
|
const { device, capture } = makeMockDevice();
|
|
88
89
|
const pbr = buildPbrPipelineLayouts(device, { storageBuffer: true });
|
|
89
90
|
const skin = buildPbrSkinLayouts(device, { storageBuffer: true }, pbr);
|
|
@@ -91,16 +92,19 @@ describe('feat-20260611 R2 / M8 / w29 — record-stage skin BindGroup BGL contra
|
|
|
91
92
|
const skinDesc = capture.bglByHandle.get(skin.meshArrayBgl);
|
|
92
93
|
expect(skinDesc).toBeDefined();
|
|
93
94
|
expect(skinDesc?.label).toBe('pbr-skin-mesh-array-bgl');
|
|
94
|
-
expect(skinDesc?.entries).toHaveLength(
|
|
95
|
+
expect(skinDesc?.entries).toHaveLength(3);
|
|
95
96
|
expect(skinDesc?.entries[0]?.binding).toBe(0);
|
|
96
97
|
expect(skinDesc?.entries[0]?.buffer?.hasDynamicOffset).toBe(true);
|
|
97
98
|
expect(skinDesc?.entries[0]?.buffer?.type).toBe('read-only-storage');
|
|
98
99
|
expect(skinDesc?.entries[1]?.binding).toBe(1);
|
|
99
100
|
expect(skinDesc?.entries[1]?.buffer?.hasDynamicOffset).toBe(true);
|
|
100
101
|
expect(skinDesc?.entries[1]?.buffer?.type).toBe('read-only-storage');
|
|
102
|
+
expect(skinDesc?.entries[2]?.binding).toBe(2);
|
|
103
|
+
expect(skinDesc?.entries[2]?.buffer?.hasDynamicOffset).toBe(true);
|
|
104
|
+
expect(skinDesc?.entries[2]?.buffer?.type).toBe('read-only-storage');
|
|
101
105
|
});
|
|
102
106
|
|
|
103
|
-
it('(1b) uniform fallback path: buildPbrSkinLayouts BGL bindings 0 + 1
|
|
107
|
+
it('(1b) uniform fallback path: buildPbrSkinLayouts BGL bindings 0 + 1 + 2 all type=uniform', () => {
|
|
104
108
|
const { device, capture } = makeMockDevice();
|
|
105
109
|
const pbr = buildPbrPipelineLayouts(device, { storageBuffer: false });
|
|
106
110
|
const skin = buildPbrSkinLayouts(device, { storageBuffer: false }, pbr);
|
|
@@ -108,6 +112,7 @@ describe('feat-20260611 R2 / M8 / w29 — record-stage skin BindGroup BGL contra
|
|
|
108
112
|
const skinDesc = capture.bglByHandle.get(skin.meshArrayBgl);
|
|
109
113
|
expect(skinDesc?.entries[0]?.buffer?.type).toBe('uniform');
|
|
110
114
|
expect(skinDesc?.entries[1]?.buffer?.type).toBe('uniform');
|
|
115
|
+
expect(skinDesc?.entries[2]?.buffer?.type).toBe('uniform');
|
|
111
116
|
});
|
|
112
117
|
|
|
113
118
|
it('(2) pbr-skin-pl slot 2 === skin meshArrayBgl handle (NOT the standard pbr-mesh-array-bgl) — pins the BG vs PipelineLayout contract R1 violated', () => {
|
|
@@ -129,7 +134,7 @@ describe('feat-20260611 R2 / M8 / w29 — record-stage skin BindGroup BGL contra
|
|
|
129
134
|
expect(skinPlDesc?.bindGroupLayouts[3]).toBe(pbr.instancesBgl);
|
|
130
135
|
});
|
|
131
136
|
|
|
132
|
-
it('(3) regression guard: buildPbrPipelineLayouts (URP) returns 1-entry meshArrayBgl labeled pbr-mesh-array-bgl — M8 must NOT leak the
|
|
137
|
+
it('(3) regression guard: buildPbrPipelineLayouts (URP) returns 1-entry meshArrayBgl labeled pbr-mesh-array-bgl — M8 must NOT leak the 3-entry skin BGL into URP', () => {
|
|
133
138
|
const { device, capture } = makeMockDevice();
|
|
134
139
|
const pbr = buildPbrPipelineLayouts(device, { storageBuffer: true });
|
|
135
140
|
|
|
@@ -20,6 +20,7 @@ describe('renderer recovery and receipt contract', () => {
|
|
|
20
20
|
expect(result.error.code).toBe('renderer-state-invalid');
|
|
21
21
|
expect(result.error.hint.length).toBeGreaterThan(0);
|
|
22
22
|
}
|
|
23
|
+
expect(renderer.inspect().state).toBe('alive');
|
|
23
24
|
await renderer.dispose();
|
|
24
25
|
});
|
|
25
26
|
|
|
@@ -38,6 +39,13 @@ describe('renderer recovery and receipt contract', () => {
|
|
|
38
39
|
expect(frame.ok).toBe(true);
|
|
39
40
|
if (!frame.ok) return;
|
|
40
41
|
const receipt: FrameReceipt = frame.value;
|
|
42
|
+
expect(world.update().ok).toBe(true);
|
|
43
|
+
const nextFrame = renderer.draw({
|
|
44
|
+
leases: [attached.value],
|
|
45
|
+
camera: { lease: attached.value },
|
|
46
|
+
environment: { lease: attached.value },
|
|
47
|
+
});
|
|
48
|
+
expect(nextFrame.ok).toBe(true);
|
|
41
49
|
const observed = await renderer.observe(receipt, { include: ['draws', 'bindings'] });
|
|
42
50
|
expect(observed.ok).toBe(true);
|
|
43
51
|
if (observed.ok) expect(observed.value.frameId).toBe(receipt.frameId);
|