@forgeax/engine-runtime 0.1.19 → 0.1.21
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 +102 -0
- package/dist/.tsbuildinfo +1 -1
- 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/tilemap-assets.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__/material-texture-dimension-mismatch.unit.test.d.ts +2 -0
- package/dist/__tests__/material-texture-dimension-mismatch.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/__tests__/texture-dimensions.browser.test.d.ts +2 -0
- package/dist/__tests__/texture-dimensions.browser.test.d.ts.map +1 -0
- package/dist/__tests__/volumetric-fog-record-recovery.integration.test.d.ts +2 -0
- package/dist/__tests__/volumetric-fog-record-recovery.integration.test.d.ts.map +1 -0
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +30 -30
- package/src/__tests__/active-camera.unit.test.ts +6 -8
- package/src/__tests__/asset-registry.recursive.spec.ts +13 -19
- package/src/__tests__/asset.unit.test.ts +28 -25
- package/src/__tests__/basis-catalog-dispatch.integration.test.ts +10 -9
- package/src/__tests__/clamp-to-last.e2e.browser.test.ts +2 -2
- package/src/__tests__/components.test-d.ts +10 -3
- package/src/__tests__/components.unit.test.ts +13 -7
- package/src/__tests__/compressed-texture-physical-extent.integration.test.ts +12 -10
- 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 +11 -4
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +15 -2
- package/src/__tests__/dawn/material-alpha.dawn.test.ts +2 -3
- package/src/__tests__/dawn/mipmap-srgb-linear.dawn.test.ts +2 -3
- package/src/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.ts +2 -3
- package/src/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.ts +2 -3
- package/src/__tests__/dawn/upload-texture.dawn.test.ts +4 -6
- package/src/__tests__/derive-bgl-integration.test.ts +11 -0
- package/src/__tests__/equirect-bc6h.integration.test.ts +6 -4
- package/src/__tests__/errors.unit.test.ts +71 -20
- package/src/__tests__/extract-frames-merge.test.ts +48 -40
- package/src/__tests__/extract-skin-mismatch.unit.test.ts +6 -2
- package/src/__tests__/four-path-compression.integration.test.ts +2 -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__/gpu-resource-cow-survivor.test.ts +2 -3
- package/src/__tests__/helpers/tilemap-assets.ts +3 -4
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +6 -3
- package/src/__tests__/light-snapshot.test-d.ts +8 -0
- package/src/__tests__/lights-preservation.fixup.test.ts +127 -0
- package/src/__tests__/lights.unit.test.ts +75 -2214
- package/src/__tests__/loadbyguid-texture-intern.unit.test.ts +3 -4
- package/src/__tests__/loader-name-survival.test.ts +2 -3
- package/src/__tests__/material-texture-dimension-mismatch.unit.test.ts +18 -0
- package/src/__tests__/material-texture-uv-scale.unit.test.ts +10 -2
- package/src/__tests__/mip-gate.unit.test.ts +11 -8
- package/src/__tests__/msaa-sprite-pixel-diff.dawn.test.ts +2 -3
- package/src/__tests__/new-kind-refs-contract.test.ts +2 -3
- package/src/__tests__/pbr-pipeline.unit.test.ts +29 -5
- package/src/__tests__/pipeline.unit.test.ts +66 -221
- package/src/__tests__/point-light-shadow.browser.test.ts +208 -83
- package/src/__tests__/point-light-shadow.unit.test.ts +67 -9
- package/src/__tests__/render-error-exhaustive.test-d.ts +92 -9
- package/src/__tests__/render-system-extract.test.ts +4 -6
- package/src/__tests__/render-system-mega.test.ts +30 -41
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +8 -7
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +6 -4
- 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 -3
- package/src/__tests__/shadow-csm-atlas.test.ts +41 -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-ubo.test.ts +41 -45
- package/src/__tests__/shadow-csm-viewport.browser.test.ts +29 -0
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +140 -58
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +6 -2
- package/src/__tests__/skylight-lazy-projection.test.ts +8 -5
- package/src/__tests__/sprite-cullmode-flip.dawn.test.ts +4 -6
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +4 -2
- package/src/__tests__/ssao-bgl.test.ts +10 -16
- package/src/__tests__/ssao-passes.test.ts +19 -2
- package/src/__tests__/systems.unit.test.ts +59 -33
- package/src/__tests__/texture-dimensions.browser.test.ts +127 -0
- package/src/__tests__/transcode-fallback.unit.test.ts +5 -1
- package/src/__tests__/view-ubo-layout.browser.test.ts +20 -10
- package/src/__tests__/volumetric-fog-record-recovery.integration.test.ts +180 -0
- package/src/createRenderer.ts +4 -0
- package/src/index.ts +1 -2
|
@@ -131,7 +131,7 @@ function makeMockRuntime(capsOverride: Partial<RhiCaps> = {}): {
|
|
|
131
131
|
|
|
132
132
|
function fakeHdrpBuffers(): HdrpBuffers {
|
|
133
133
|
return {
|
|
134
|
-
|
|
134
|
+
device: {} as HdrpBuffers['device'],
|
|
135
135
|
lightDataBuffer: mockBuffer('hdrp-light-data'),
|
|
136
136
|
lightDataBytes: 16384,
|
|
137
137
|
clusterGridBuffer: mockBuffer('hdrp-cluster-grid'),
|
|
@@ -150,15 +150,9 @@ function fakeHdrpBuffers(): HdrpBuffers {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
describe('HDRP GPU membership producer bind-group contract', () => {
|
|
153
|
-
it('preserves the 256-light
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
);
|
|
157
|
-
const storagePayload = new Uint32Array(
|
|
158
|
-
packClusterUniform({ x: 16, y: 9, z: 24 }, 0.1, 50, 0, 256, 256),
|
|
159
|
-
);
|
|
160
|
-
expect(uniformPayload[3]).toBe(128);
|
|
161
|
-
expect(storagePayload[3]).toBe(256);
|
|
153
|
+
it('preserves the full 256-light Standard count in the cluster UBO', () => {
|
|
154
|
+
const payload = new Uint32Array(packClusterUniform({ x: 16, y: 9, z: 24 }, 0.1, 50, 0, 256));
|
|
155
|
+
expect(payload[3]).toBe(256);
|
|
162
156
|
});
|
|
163
157
|
|
|
164
158
|
it('keeps enabled SSAO intensity in the zero-punctual-light uniform path', () => {
|
|
@@ -209,12 +203,12 @@ describe('HDRP GPU membership producer bind-group contract', () => {
|
|
|
209
203
|
|
|
210
204
|
describe('w28 — HDRP unified BGL SSAO entries (binding 7/8)', () => {
|
|
211
205
|
it('descriptor entries.length === 7 (5 cluster + 2 ssao)', () => {
|
|
212
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
206
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
213
207
|
expect(desc.entries?.length).toBe(7);
|
|
214
208
|
});
|
|
215
209
|
|
|
216
210
|
it('binding 7 is texture_2d<f32> with FRAGMENT visibility (ssaoBlurred)', () => {
|
|
217
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
211
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
218
212
|
const b7 = desc.entries?.find((e) => e.binding === 7);
|
|
219
213
|
expect(b7).toBeDefined();
|
|
220
214
|
expect(b7?.visibility).toBe(FRAGMENT_VISIBILITY);
|
|
@@ -223,7 +217,7 @@ describe('w28 — HDRP unified BGL SSAO entries (binding 7/8)', () => {
|
|
|
223
217
|
});
|
|
224
218
|
|
|
225
219
|
it('binding 8 is sampler with FRAGMENT visibility (ssaoSampler)', () => {
|
|
226
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
220
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
227
221
|
const b8 = desc.entries?.find((e) => e.binding === 8);
|
|
228
222
|
expect(b8).toBeDefined();
|
|
229
223
|
expect(b8?.visibility).toBe(FRAGMENT_VISIBILITY);
|
|
@@ -231,13 +225,13 @@ describe('w28 — HDRP unified BGL SSAO entries (binding 7/8)', () => {
|
|
|
231
225
|
});
|
|
232
226
|
|
|
233
227
|
it('binding 9 is absent (scope-amend-webgl2-ubo: intensity folded into binding 6)', () => {
|
|
234
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
228
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
235
229
|
const bindings = desc.entries?.map((e) => e.binding) ?? [];
|
|
236
230
|
expect(bindings).not.toContain(9);
|
|
237
231
|
});
|
|
238
232
|
|
|
239
233
|
it('existing bindings 0/3/4/5/6 remain unchanged', () => {
|
|
240
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
234
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
241
235
|
const b0 = desc.entries?.find((e) => e.binding === 0);
|
|
242
236
|
expect(b0?.buffer?.hasDynamicOffset).toBe(true);
|
|
243
237
|
for (const b of [3, 4, 5, 6]) {
|
|
@@ -247,7 +241,7 @@ describe('w28 — HDRP unified BGL SSAO entries (binding 7/8)', () => {
|
|
|
247
241
|
});
|
|
248
242
|
|
|
249
243
|
it('all bindings exactly { 0, 3, 4, 5, 6, 7, 8 }', () => {
|
|
250
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
244
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
251
245
|
const bindings = new Set(desc.entries?.map((e) => e.binding));
|
|
252
246
|
expect(bindings).toEqual(new Set([0, 3, 4, 5, 6, 7, 8]));
|
|
253
247
|
});
|
|
@@ -63,7 +63,14 @@ interface SpyCtx {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function makeDispatchSpyCtx(
|
|
66
|
-
opts: {
|
|
66
|
+
opts: {
|
|
67
|
+
ssaoCalcPipeline?: unknown;
|
|
68
|
+
ssaoBlurPipeline?: unknown;
|
|
69
|
+
ssaoBgl?: unknown;
|
|
70
|
+
fxaaPipeline?: unknown;
|
|
71
|
+
fxaaBindGroupLayout?: unknown;
|
|
72
|
+
fxaaSampler?: unknown;
|
|
73
|
+
} = {},
|
|
67
74
|
): SpyCtx {
|
|
68
75
|
const spy: DispatchSpy = {
|
|
69
76
|
beginRenderPassCalls: [],
|
|
@@ -138,10 +145,12 @@ function makeDispatchSpyCtx(
|
|
|
138
145
|
removeEventListener: vi.fn(),
|
|
139
146
|
};
|
|
140
147
|
|
|
148
|
+
const fxaaParamsBuffer = { __label: 'fxaa-params' };
|
|
141
149
|
const runtime = {
|
|
142
150
|
device,
|
|
143
151
|
errorRegistry,
|
|
144
152
|
shaderCache: { get: vi.fn() },
|
|
153
|
+
getPostProcessParamsBuffer: vi.fn(() => fxaaParamsBuffer),
|
|
145
154
|
} as unknown as RenderSystemRuntime;
|
|
146
155
|
|
|
147
156
|
// Pre-seed ssao-buffers cache so the record closure resolves a stable
|
|
@@ -162,6 +171,13 @@ function makeDispatchSpyCtx(
|
|
|
162
171
|
ssaoFilteringSampler: null,
|
|
163
172
|
ssaoDepthSampler: null,
|
|
164
173
|
ssaoFallbackRawView: null,
|
|
174
|
+
// FXAA is part of the same Standard post chain. Keep the fixture's
|
|
175
|
+
// fullscreen resources complete so recordAll exercises the real FXAA
|
|
176
|
+
// bind-group path instead of returning early at its capability gate.
|
|
177
|
+
fxaaPipeline: 'fxaaPipeline' in opts ? opts.fxaaPipeline : { __label: 'fxaa-pipeline' },
|
|
178
|
+
fxaaBindGroupLayout:
|
|
179
|
+
'fxaaBindGroupLayout' in opts ? opts.fxaaBindGroupLayout : { __label: 'fxaa-bgl' },
|
|
180
|
+
fxaaSampler: 'fxaaSampler' in opts ? opts.fxaaSampler : { __label: 'fxaa-sampler' },
|
|
165
181
|
},
|
|
166
182
|
};
|
|
167
183
|
|
|
@@ -215,7 +231,6 @@ function makeDispatchSpyCtx(
|
|
|
215
231
|
meshBindGroup: null,
|
|
216
232
|
frameState: {
|
|
217
233
|
perFrameGraph: mockPerFrameGraph,
|
|
218
|
-
isHdrpActive: true,
|
|
219
234
|
installedPipelineConfig: { ssao: { enabled: true, intensity: 1.0 } },
|
|
220
235
|
// Post-process bind group identity cache (bloom / fxaa / ssao). Keyed on
|
|
221
236
|
// the graph-resolved views so resize rebuilds automatically.
|
|
@@ -673,6 +688,7 @@ function makePostProcessRecordCtx(): {
|
|
|
673
688
|
draw: vi.fn(),
|
|
674
689
|
end: vi.fn(),
|
|
675
690
|
};
|
|
691
|
+
const fxaaParamsBuffer = { label: 'fxaa-params' };
|
|
676
692
|
const device = {
|
|
677
693
|
createBindGroup: vi.fn((desc: PostProcessBindGroupCreate) => {
|
|
678
694
|
bindGroupCreates.push(desc);
|
|
@@ -711,6 +727,7 @@ function makePostProcessRecordCtx(): {
|
|
|
711
727
|
runtime: {
|
|
712
728
|
device: { ...device, caps: { storageBuffer: true } },
|
|
713
729
|
errorRegistry: { fire: vi.fn() },
|
|
730
|
+
getPostProcessParamsBuffer: vi.fn(() => fxaaParamsBuffer),
|
|
714
731
|
},
|
|
715
732
|
store: { getCubemapGpuView: vi.fn() },
|
|
716
733
|
encoder: {
|
|
@@ -120,6 +120,7 @@ import { INSTANCE_STORAGE_STRIDE_FLOATS } from '../../../render/src/record/mesh-
|
|
|
120
120
|
import { selectSwapChainFormat } from '../../../render/src/render-system';
|
|
121
121
|
import { createSkinPaletteAllocator } from '../../../render/src/systems/skin-palette-allocator';
|
|
122
122
|
import type { TransparentEntry } from '../../../render/src/systems/transparent-sort-config';
|
|
123
|
+
import { createTemporalFrameTransaction } from '../../../render/src/temporal/frame';
|
|
123
124
|
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
124
125
|
import { drawWithOwners } from './renderer-test-utils';
|
|
125
126
|
|
|
@@ -447,9 +448,9 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
447
448
|
];
|
|
448
449
|
}
|
|
449
450
|
|
|
450
|
-
describe('Camera schema (
|
|
451
|
-
it('Camera.schema has
|
|
452
|
-
expect(Object.keys(Camera.fields).length).toBe(
|
|
451
|
+
describe('Camera schema (21 fields: 17 f32 + clearColor array + autoAspect bool + target after render-target extension)', () => {
|
|
452
|
+
it('Camera.schema has 21 fields (17 f32 + clearColor array + autoAspect bool + target: perspective quartet + projection + ortho quartet + tonemap trio + antialias + bloom quartet + clearColor + autoAspect)', () => {
|
|
453
|
+
expect(Object.keys(Camera.fields).length).toBe(21);
|
|
453
454
|
expect(componentFieldType(Camera, 'fov')).toBe('f32');
|
|
454
455
|
expect(componentFieldType(Camera, 'aspect')).toBe('f32');
|
|
455
456
|
expect(componentFieldType(Camera, 'near')).toBe('f32');
|
|
@@ -475,6 +476,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
475
476
|
// feat-20260617-host-engine-contract-and-video-cutscene / M3: aspect-sync
|
|
476
477
|
// opt-out flag (bool column tier).
|
|
477
478
|
expect(componentFieldType(Camera, 'autoAspect')).toBe('bool');
|
|
479
|
+
expect(componentFieldType(Camera, 'target')).toBe('shared<RenderTarget>');
|
|
478
480
|
});
|
|
479
481
|
});
|
|
480
482
|
|
|
@@ -573,6 +575,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
573
575
|
directionalCount: 0,
|
|
574
576
|
point: [],
|
|
575
577
|
spot: [],
|
|
578
|
+
directionalShadowError: undefined,
|
|
576
579
|
lightSpaceMatrix: undefined,
|
|
577
580
|
shadowMapSize: undefined,
|
|
578
581
|
pointShadow: [],
|
|
@@ -762,8 +765,11 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
762
765
|
[],
|
|
763
766
|
{
|
|
764
767
|
frameNumber: 0,
|
|
768
|
+
temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
|
|
769
|
+
temporalFrameInput: undefined,
|
|
765
770
|
compiledFrameGraph: null,
|
|
766
771
|
compiledFrameGraphTopologyKey: null,
|
|
772
|
+
compiledFrameGraphGeneration: 0,
|
|
767
773
|
retiredCompiledFrameGraphs: new Set(),
|
|
768
774
|
instanceBuffers: new Map(),
|
|
769
775
|
transientInstanceBuffers: [],
|
|
@@ -784,8 +790,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
784
790
|
installedPipelineHandle: 0,
|
|
785
791
|
activePipeline: urpPipeline,
|
|
786
792
|
installedPipelineConfig: undefined,
|
|
787
|
-
|
|
788
|
-
hdrpOncePerFrameFired: new Set(),
|
|
793
|
+
standardOncePerFrameFired: new Set(),
|
|
789
794
|
},
|
|
790
795
|
{ unlit: 0 },
|
|
791
796
|
{ createBindGroup: 0, keys: [] },
|
|
@@ -817,8 +822,11 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
817
822
|
[],
|
|
818
823
|
{
|
|
819
824
|
frameNumber: 0,
|
|
825
|
+
temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
|
|
826
|
+
temporalFrameInput: undefined,
|
|
820
827
|
compiledFrameGraph: null,
|
|
821
828
|
compiledFrameGraphTopologyKey: null,
|
|
829
|
+
compiledFrameGraphGeneration: 0,
|
|
822
830
|
retiredCompiledFrameGraphs: new Set(),
|
|
823
831
|
instanceBuffers: new Map(),
|
|
824
832
|
transientInstanceBuffers: [],
|
|
@@ -839,8 +847,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
839
847
|
installedPipelineHandle: 0,
|
|
840
848
|
activePipeline: urpPipeline,
|
|
841
849
|
installedPipelineConfig: undefined,
|
|
842
|
-
|
|
843
|
-
hdrpOncePerFrameFired: new Set(),
|
|
850
|
+
standardOncePerFrameFired: new Set(),
|
|
844
851
|
},
|
|
845
852
|
{ unlit: 0 },
|
|
846
853
|
{ createBindGroup: 0, keys: [] },
|
|
@@ -909,8 +916,9 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
909
916
|
// --- from fxaa-pipeline.test.ts ---
|
|
910
917
|
|
|
911
918
|
describe('feat-20260528-fxaa-post-processing M2 w8: FXAA pipeline construction (success path)', () => {
|
|
912
|
-
it('D-2: FXAA BGL has exactly
|
|
913
|
-
//
|
|
919
|
+
it('D-2: FXAA BGL has exactly 3 entries: texture + sampler + dither policy UBO', () => {
|
|
920
|
+
// FXAA owns a per-pass output-policy UBO so pipeline asset config can
|
|
921
|
+
// disable dither without rebuilding the shader or graph topology.
|
|
914
922
|
// The sampler entry uses { type: 'filtering' } (linear sampling).
|
|
915
923
|
const bglEntries = [
|
|
916
924
|
{
|
|
@@ -923,15 +931,21 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
923
931
|
visibility: 2, // GPU_SHADER_STAGE_FRAGMENT = 0x2
|
|
924
932
|
sampler: { type: 'filtering' },
|
|
925
933
|
},
|
|
934
|
+
{
|
|
935
|
+
binding: 2,
|
|
936
|
+
visibility: 2,
|
|
937
|
+
buffer: { type: 'uniform' },
|
|
938
|
+
},
|
|
926
939
|
];
|
|
927
|
-
expect(bglEntries).toHaveLength(
|
|
940
|
+
expect(bglEntries).toHaveLength(3);
|
|
928
941
|
expect(bglEntries[0]?.binding).toBe(0);
|
|
929
942
|
expect(bglEntries[0]?.texture).toBeDefined();
|
|
930
943
|
expect(bglEntries[1]?.binding).toBe(1);
|
|
931
944
|
expect(bglEntries[1]?.sampler).toBeDefined();
|
|
932
|
-
|
|
945
|
+
expect(bglEntries[2]?.binding).toBe(2);
|
|
946
|
+
expect(bglEntries[2]?.buffer).toEqual({ type: 'uniform' });
|
|
933
947
|
const hasBuffer = bglEntries.some((e) => 'buffer' in (e as Record<string, unknown>));
|
|
934
|
-
expect(hasBuffer).toBe(
|
|
948
|
+
expect(hasBuffer).toBe(true);
|
|
935
949
|
});
|
|
936
950
|
|
|
937
951
|
it('D-3: FXAA pipeline target format = swap-chain storage format (non-srgb, helper Channel 2 truth)', () => {
|
|
@@ -1915,10 +1929,10 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
1915
1929
|
return 'mesh-ssbo-capacity';
|
|
1916
1930
|
case 'mesh-ssbo-ceiling-reached':
|
|
1917
1931
|
return 'mesh-ssbo-ceiling';
|
|
1918
|
-
case '
|
|
1919
|
-
return '
|
|
1920
|
-
case '
|
|
1921
|
-
return '
|
|
1932
|
+
case 'standard-light-budget-exceeded':
|
|
1933
|
+
return 'standard-budget';
|
|
1934
|
+
case 'standard-cluster-index-overflow':
|
|
1935
|
+
return 'standard-overflow';
|
|
1922
1936
|
}
|
|
1923
1937
|
}
|
|
1924
1938
|
expect(exhaustive('skin-joint-count-exceeded')).toBe('joint-count');
|
|
@@ -2081,8 +2095,10 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2081
2095
|
);
|
|
2082
2096
|
});
|
|
2083
2097
|
|
|
2084
|
-
it('imports View from forgeax_view::common', () => {
|
|
2085
|
-
expect(source).toEqual(expect.stringContaining('#import forgeax_view::common::
|
|
2098
|
+
it('imports View from forgeax_view::common as the sole view ABI', () => {
|
|
2099
|
+
expect(source).toEqual(expect.stringContaining('#import forgeax_view::common::View'));
|
|
2100
|
+
expect(source).not.toContain('FogRay');
|
|
2101
|
+
expect(source).not.toContain('fogRayFromNdc');
|
|
2086
2102
|
});
|
|
2087
2103
|
|
|
2088
2104
|
it('imports fullscreen_triangle from forgeax_view::common', () => {
|
|
@@ -3179,6 +3195,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3179
3195
|
directionalCount: 0,
|
|
3180
3196
|
point: [],
|
|
3181
3197
|
spot: [],
|
|
3198
|
+
directionalShadowError: undefined,
|
|
3182
3199
|
lightSpaceMatrix: undefined,
|
|
3183
3200
|
shadowMapSize: undefined,
|
|
3184
3201
|
pointShadow: [],
|
|
@@ -3361,11 +3378,12 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3361
3378
|
}
|
|
3362
3379
|
|
|
3363
3380
|
function makePipelineState(
|
|
3364
|
-
internals: { _fakes: Record<string, unknown
|
|
3381
|
+
internals: { _fakes: Record<string, unknown>; device?: unknown },
|
|
3365
3382
|
hdrAlreadyAllocated: boolean,
|
|
3366
3383
|
): unknown {
|
|
3367
3384
|
const f = internals._fakes;
|
|
3368
3385
|
return {
|
|
3386
|
+
device: internals.device,
|
|
3369
3387
|
meshes: new Map(),
|
|
3370
3388
|
format: 'bgra8unorm',
|
|
3371
3389
|
colorAttachmentFormat: 'bgra8unorm-srgb',
|
|
@@ -3450,8 +3468,11 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3450
3468
|
[],
|
|
3451
3469
|
{
|
|
3452
3470
|
frameNumber: 0,
|
|
3471
|
+
temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
|
|
3472
|
+
temporalFrameInput: undefined,
|
|
3453
3473
|
compiledFrameGraph: null,
|
|
3454
3474
|
compiledFrameGraphTopologyKey: null,
|
|
3475
|
+
compiledFrameGraphGeneration: 0,
|
|
3455
3476
|
retiredCompiledFrameGraphs: new Set(),
|
|
3456
3477
|
instanceBuffers: new Map(),
|
|
3457
3478
|
transientInstanceBuffers: [],
|
|
@@ -3472,8 +3493,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3472
3493
|
installedPipelineHandle: 0,
|
|
3473
3494
|
activePipeline: urpPipeline,
|
|
3474
3495
|
installedPipelineConfig: undefined,
|
|
3475
|
-
|
|
3476
|
-
hdrpOncePerFrameFired: new Set(),
|
|
3496
|
+
standardOncePerFrameFired: new Set(),
|
|
3477
3497
|
},
|
|
3478
3498
|
{ unlit: 0 },
|
|
3479
3499
|
{ createBindGroup: 0, keys: [] },
|
|
@@ -3569,8 +3589,11 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3569
3589
|
[],
|
|
3570
3590
|
{
|
|
3571
3591
|
frameNumber: 0,
|
|
3592
|
+
temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
|
|
3593
|
+
temporalFrameInput: undefined,
|
|
3572
3594
|
compiledFrameGraph: null,
|
|
3573
3595
|
compiledFrameGraphTopologyKey: null,
|
|
3596
|
+
compiledFrameGraphGeneration: 0,
|
|
3574
3597
|
retiredCompiledFrameGraphs: new Set(),
|
|
3575
3598
|
instanceBuffers,
|
|
3576
3599
|
warnedZeroLightStandard: false,
|
|
@@ -3590,8 +3613,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3590
3613
|
installedPipelineHandle: 0,
|
|
3591
3614
|
activePipeline: urpPipeline,
|
|
3592
3615
|
installedPipelineConfig: undefined,
|
|
3593
|
-
|
|
3594
|
-
hdrpOncePerFrameFired: new Set(),
|
|
3616
|
+
standardOncePerFrameFired: new Set(),
|
|
3595
3617
|
},
|
|
3596
3618
|
{ unlit: 0 },
|
|
3597
3619
|
{ createBindGroup: 0, keys: [] },
|
|
@@ -3642,8 +3664,11 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3642
3664
|
[],
|
|
3643
3665
|
{
|
|
3644
3666
|
frameNumber: 0,
|
|
3667
|
+
temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
|
|
3668
|
+
temporalFrameInput: undefined,
|
|
3645
3669
|
compiledFrameGraph: null,
|
|
3646
3670
|
compiledFrameGraphTopologyKey: null,
|
|
3671
|
+
compiledFrameGraphGeneration: 0,
|
|
3647
3672
|
retiredCompiledFrameGraphs: new Set(),
|
|
3648
3673
|
instanceBuffers,
|
|
3649
3674
|
warnedZeroLightStandard: false,
|
|
@@ -3663,8 +3688,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3663
3688
|
installedPipelineHandle: 0,
|
|
3664
3689
|
activePipeline: urpPipeline,
|
|
3665
3690
|
installedPipelineConfig: undefined,
|
|
3666
|
-
|
|
3667
|
-
hdrpOncePerFrameFired: new Set(),
|
|
3691
|
+
standardOncePerFrameFired: new Set(),
|
|
3668
3692
|
},
|
|
3669
3693
|
{ unlit: 0 },
|
|
3670
3694
|
{ createBindGroup: 0, keys: [] },
|
|
@@ -8323,10 +8347,10 @@ type ExtractFramesWithPipeline = (
|
|
|
8323
8347
|
}
|
|
8324
8348
|
|
|
8325
8349
|
// ── feat-20260612-skin-palette-per-frame-upload M3 / m3-1: record-stage ──
|
|
8326
|
-
// dynOffset[1] truth-value + skin BG cache miss=1 + hit=N-1 stats counter.
|
|
8350
|
+
// dynOffset[1]/[2] truth-value + skin BG cache miss=1 + hit=N-1 stats counter.
|
|
8327
8351
|
//
|
|
8328
8352
|
// Why this block exists:
|
|
8329
|
-
// M3 lifts the record-stage `group2DynamicOffsets`
|
|
8353
|
+
// M3 lifts the record-stage `group2DynamicOffsets` palette slots from a
|
|
8330
8354
|
// hard-coded `0` (PR #353 stub) to the per-entity `entry.source.skin
|
|
8331
8355
|
// .byteOffset` cursor that M2 m2-6 wired into the extract output. The
|
|
8332
8356
|
// acceptanceCheck calls for the literal text `group2DynamicOffsets[1]
|
|
@@ -8338,8 +8362,8 @@ type ExtractFramesWithPipeline = (
|
|
|
8338
8362
|
// What the assertions cover:
|
|
8339
8363
|
// (a) The test-local offset oracle mirrors the production main-pass
|
|
8340
8364
|
// geometry contract. With skinByteOffset !== undefined the returned
|
|
8341
|
-
// tuple's [1]
|
|
8342
|
-
//
|
|
8365
|
+
// tuple's [1] and [2] slots equal the supplied byteOffset (the
|
|
8366
|
+
// temporal contract); with skinByteOffset ===
|
|
8343
8367
|
// undefined it returns a length-1 tuple (non-skin paths preserved).
|
|
8344
8368
|
// (b) N=3 sequential lookups
|
|
8345
8369
|
// against the same `(meshStorageBuffer, skinPaletteAllocator
|
|
@@ -8366,7 +8390,9 @@ type ExtractFramesWithPipeline = (
|
|
|
8366
8390
|
skinByteOffset: number | undefined,
|
|
8367
8391
|
): readonly number[] => {
|
|
8368
8392
|
const meshOffset = meshSlotIdx * MESH_PER_ENTITY_STRIDE;
|
|
8369
|
-
return skinByteOffset === undefined
|
|
8393
|
+
return skinByteOffset === undefined
|
|
8394
|
+
? [meshOffset]
|
|
8395
|
+
: [meshOffset, skinByteOffset, skinByteOffset];
|
|
8370
8396
|
};
|
|
8371
8397
|
|
|
8372
8398
|
// Skin path: second slot must equal the per-entity byteOffset cursor
|
|
@@ -8375,12 +8401,12 @@ type ExtractFramesWithPipeline = (
|
|
|
8375
8401
|
// (first allocateSlice), cursor=256 (second 1-joint slice), cursor
|
|
8376
8402
|
// =16320 (the worst-case 255-joint slice end).
|
|
8377
8403
|
const skinAt0 = computeSkinGroup2DynOffsets(0, 0);
|
|
8378
|
-
expect(skinAt0).toHaveLength(
|
|
8404
|
+
expect(skinAt0).toHaveLength(3);
|
|
8379
8405
|
// group2DynamicOffsets[1] === byteOffset (literal assertion text per acceptanceCheck)
|
|
8380
8406
|
expect(skinAt0[1]).toBe(0);
|
|
8381
8407
|
|
|
8382
8408
|
const skinAt256 = computeSkinGroup2DynOffsets(1, 256);
|
|
8383
|
-
expect(skinAt256).toHaveLength(
|
|
8409
|
+
expect(skinAt256).toHaveLength(3);
|
|
8384
8410
|
// group2DynamicOffsets[1] === byteOffset
|
|
8385
8411
|
expect(skinAt256[1]).toBe(256);
|
|
8386
8412
|
// mesh slot continues to follow the 256-byte stride: meshSlotIdx * 256.
|
|
@@ -8388,7 +8414,7 @@ type ExtractFramesWithPipeline = (
|
|
|
8388
8414
|
|
|
8389
8415
|
const byteOffsetWorst = 254 * 256;
|
|
8390
8416
|
const skinAtEnd = computeSkinGroup2DynOffsets(2, byteOffsetWorst);
|
|
8391
|
-
expect(skinAtEnd).toHaveLength(
|
|
8417
|
+
expect(skinAtEnd).toHaveLength(3);
|
|
8392
8418
|
// group2DynamicOffsets[1] === byteOffset
|
|
8393
8419
|
expect(skinAtEnd[1]).toBe(byteOffsetWorst);
|
|
8394
8420
|
expect(skinAtEnd[0]).toBe(2 * 256);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const TEXTURE_BINDING = 0x04;
|
|
4
|
+
const COPY_DST = 0x08;
|
|
5
|
+
const COPY_SRC = 0x01;
|
|
6
|
+
const STORAGE_BINDING = 0x80;
|
|
7
|
+
const MAP_READ = 0x0001;
|
|
8
|
+
const COMPUTE_STAGE = 1;
|
|
9
|
+
|
|
10
|
+
type ProbeResult =
|
|
11
|
+
| { readonly status: 'ready'; readonly samples: readonly (readonly number[])[] }
|
|
12
|
+
| { readonly status: 'unavailable'; readonly reason: string };
|
|
13
|
+
|
|
14
|
+
async function probe(): Promise<ProbeResult> {
|
|
15
|
+
if (!('gpu' in navigator)) return { status: 'unavailable', reason: 'webgpu-unavailable' };
|
|
16
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
17
|
+
if (adapter === null) return { status: 'unavailable', reason: 'adapter-unavailable' };
|
|
18
|
+
const device = await adapter.requestDevice();
|
|
19
|
+
const texture = device.createTexture({
|
|
20
|
+
size: { width: 2, height: 2, depthOrArrayLayers: 2 },
|
|
21
|
+
dimension: '3d',
|
|
22
|
+
format: 'rgba8unorm',
|
|
23
|
+
usage: TEXTURE_BINDING | COPY_DST,
|
|
24
|
+
});
|
|
25
|
+
const arrayTexture = device.createTexture({
|
|
26
|
+
size: { width: 2, height: 2, depthOrArrayLayers: 2 },
|
|
27
|
+
dimension: '2d',
|
|
28
|
+
format: 'rgba8unorm',
|
|
29
|
+
usage: TEXTURE_BINDING | COPY_DST,
|
|
30
|
+
});
|
|
31
|
+
const samples = new Uint8Array(256 * 2 * 2);
|
|
32
|
+
for (let point = 0; point < 8; point += 1) {
|
|
33
|
+
const z = Math.floor(point / 4);
|
|
34
|
+
const y = Math.floor((point % 4) / 2);
|
|
35
|
+
const x = point % 2;
|
|
36
|
+
const offset = z * 512 + y * 256 + x * 4;
|
|
37
|
+
samples[offset] = point + 1;
|
|
38
|
+
samples[offset + 3] = 255;
|
|
39
|
+
}
|
|
40
|
+
const layout = { offset: 0, bytesPerRow: 256, rowsPerImage: 2 };
|
|
41
|
+
const extent = { width: 2, height: 2, depthOrArrayLayers: 2 };
|
|
42
|
+
device.queue.writeTexture({ texture: arrayTexture }, samples, layout, extent);
|
|
43
|
+
device.queue.writeTexture({ texture }, samples, layout, extent);
|
|
44
|
+
const output = device.createBuffer({ size: 256, usage: STORAGE_BINDING | COPY_SRC });
|
|
45
|
+
const readback = device.createBuffer({ size: 256, usage: MAP_READ | COPY_DST });
|
|
46
|
+
const module = device.createShaderModule({
|
|
47
|
+
code: `
|
|
48
|
+
@group(0) @binding(0) var array_tex: texture_2d_array<f32>;
|
|
49
|
+
@group(0) @binding(1) var volume_tex: texture_3d<f32>;
|
|
50
|
+
@group(0) @binding(2) var<storage, read_write> output: array<vec4<u32>>;
|
|
51
|
+
fn to_byte(value: f32) -> u32 {
|
|
52
|
+
return u32(round(value * 255.0));
|
|
53
|
+
}
|
|
54
|
+
@compute @workgroup_size(1)
|
|
55
|
+
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
56
|
+
let point = id.x;
|
|
57
|
+
let coord = vec3<i32>(i32(point % 2u), i32((point / 2u) % 2u), i32(point / 4u));
|
|
58
|
+
let array_value = textureLoad(array_tex, coord.xy, coord.z, 0);
|
|
59
|
+
let volume_value = textureLoad(volume_tex, coord, 0);
|
|
60
|
+
output[point] = vec4<u32>(to_byte(array_value.r), 0u, 0u, 0u);
|
|
61
|
+
output[point + 8u] = vec4<u32>(to_byte(volume_value.r), 0u, 0u, 0u);
|
|
62
|
+
}
|
|
63
|
+
`,
|
|
64
|
+
});
|
|
65
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
66
|
+
entries: [
|
|
67
|
+
{ binding: 0, visibility: COMPUTE_STAGE, texture: { viewDimension: '2d-array' } },
|
|
68
|
+
{ binding: 1, visibility: COMPUTE_STAGE, texture: { viewDimension: '3d' } },
|
|
69
|
+
{
|
|
70
|
+
binding: 2,
|
|
71
|
+
visibility: COMPUTE_STAGE,
|
|
72
|
+
buffer: { type: 'storage' },
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
const pipeline = device.createComputePipeline({
|
|
77
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
|
|
78
|
+
compute: { module, entryPoint: 'main' },
|
|
79
|
+
});
|
|
80
|
+
const bindGroup = device.createBindGroup({
|
|
81
|
+
layout: bindGroupLayout,
|
|
82
|
+
entries: [
|
|
83
|
+
{ binding: 0, resource: arrayTexture.createView({ dimension: '2d-array' }) },
|
|
84
|
+
{ binding: 1, resource: texture.createView({ dimension: '3d' }) },
|
|
85
|
+
{ binding: 2, resource: { buffer: output } },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
const encoder = device.createCommandEncoder();
|
|
89
|
+
const pass = encoder.beginComputePass();
|
|
90
|
+
pass.setPipeline(pipeline);
|
|
91
|
+
pass.setBindGroup(0, bindGroup);
|
|
92
|
+
pass.dispatchWorkgroups(8);
|
|
93
|
+
pass.end();
|
|
94
|
+
encoder.copyBufferToBuffer(output, 0, readback, 0, 256);
|
|
95
|
+
device.queue.submit([encoder.finish()]);
|
|
96
|
+
await readback.mapAsync(MAP_READ);
|
|
97
|
+
const actual = new Uint8Array(readback.getMappedRange()).slice();
|
|
98
|
+
readback.unmap();
|
|
99
|
+
const observed = Array.from({ length: 8 }, (_, point) => [
|
|
100
|
+
actual[point * 16] ?? 0,
|
|
101
|
+
actual[(point + 8) * 16] ?? 0,
|
|
102
|
+
]);
|
|
103
|
+
const expected = Array.from({ length: 8 }, (_, point) => point + 1);
|
|
104
|
+
if (
|
|
105
|
+
observed.some(
|
|
106
|
+
([arraySample, volumeSample], point) =>
|
|
107
|
+
arraySample !== expected[point] || volumeSample !== expected[point],
|
|
108
|
+
)
|
|
109
|
+
) {
|
|
110
|
+
return { status: 'unavailable', reason: 'gpu-readback-mismatch' };
|
|
111
|
+
}
|
|
112
|
+
return { status: 'ready', samples: observed };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe('browser texture dimensions', () => {
|
|
116
|
+
it('returns an observable WebGPU status instead of silently accepting a bad view', async () => {
|
|
117
|
+
const result = await probe();
|
|
118
|
+
expect(['ready', 'unavailable']).toContain(result.status);
|
|
119
|
+
if (result.status === 'unavailable') expect(result.reason.length).toBeGreaterThan(0);
|
|
120
|
+
if (result.status === 'ready') {
|
|
121
|
+
expect(result.samples).toHaveLength(8);
|
|
122
|
+
expect(
|
|
123
|
+
result.samples.map(([arraySample, volumeSample]) => [arraySample, volumeSample]),
|
|
124
|
+
).toEqual(Array.from({ length: 8 }, (_, point) => [point + 1, point + 1]));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -126,7 +126,11 @@ function wireFetch(
|
|
|
126
126
|
{
|
|
127
127
|
guid: GUID_TEX,
|
|
128
128
|
kind: 'texture',
|
|
129
|
-
payload: {
|
|
129
|
+
payload: {
|
|
130
|
+
shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
|
|
131
|
+
colorSpace,
|
|
132
|
+
mips: { kind: 'none' },
|
|
133
|
+
},
|
|
130
134
|
refs: [],
|
|
131
135
|
artifacts: {
|
|
132
136
|
body: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// view-ubo-layout.browser.test.ts - feat-20260518-pbr-direct-lighting-mvp
|
|
2
|
-
// feat-
|
|
3
|
-
//
|
|
2
|
+
// feat-20260827-directional-csm-pcss-quality M1: VIEW_UBO_BYTES remains 960
|
|
3
|
+
// while the View slot remains 1024 B. The prefix layout is unchanged and the
|
|
4
|
+
// directional shadow carrier reuses the existing tail pad.
|
|
4
5
|
// 16 B + cameraPos 16 B + lightSpaceMatrix 64 B + inverseViewProj 64 B;
|
|
5
6
|
// std140 vec3 padded to 16-byte boundaries).
|
|
6
7
|
//
|
|
@@ -44,14 +45,16 @@ const VIEW_UBO_OFFSETS = {
|
|
|
44
45
|
lightSpaceMatrix: 112,
|
|
45
46
|
inverseViewProj: 176,
|
|
46
47
|
} as const;
|
|
47
|
-
const VIEW_UBO_BYTES =
|
|
48
|
+
const VIEW_UBO_BYTES = 960;
|
|
49
|
+
const DIRECTIONAL_FILTER_OFFSET = 512;
|
|
50
|
+
const SPOT_LIGHT_VIEW_PROJ_OFFSET = 528;
|
|
48
51
|
|
|
49
52
|
describe('w4 view UBO 240 B std140 layout (AC-06, browser)', () => {
|
|
50
|
-
it.skipIf(!browserReady)('host write payload is exactly
|
|
51
|
-
// The host (
|
|
53
|
+
it.skipIf(!browserReady)('host write payload is exactly 960 bytes (240 floats)', () => {
|
|
54
|
+
// The host (view-ubo.ts) builds a Float32Array(240) and
|
|
52
55
|
// emits a single queue.writeBuffer. Asserting the size lock here keeps
|
|
53
56
|
// the host shape and shader ABI in agreement before the GPU sees it.
|
|
54
|
-
const payload = new Float32Array(
|
|
57
|
+
const payload = new Float32Array(240);
|
|
55
58
|
expect(payload.byteLength).toBe(VIEW_UBO_BYTES);
|
|
56
59
|
});
|
|
57
60
|
|
|
@@ -63,17 +66,17 @@ describe('w4 view UBO 240 B std140 layout (AC-06, browser)', () => {
|
|
|
63
66
|
expect(VIEW_UBO_OFFSETS.lightSpaceMatrix).toBe(112);
|
|
64
67
|
expect(VIEW_UBO_OFFSETS.inverseViewProj).toBe(176);
|
|
65
68
|
// Buffer total = inverseViewProj offset + mat4 (64 B) = 240.
|
|
66
|
-
expect(VIEW_UBO_OFFSETS.inverseViewProj + 64).toBe(
|
|
69
|
+
expect(VIEW_UBO_OFFSETS.inverseViewProj + 64).toBe(240);
|
|
67
70
|
});
|
|
68
71
|
|
|
69
72
|
it.skipIf(!browserReady)(
|
|
70
73
|
'host packs lightDir / lightColor / cameraPos at canonical std140 slots',
|
|
71
74
|
() => {
|
|
72
75
|
// Canonical packing order documented in plan-strategy D-4: the host
|
|
73
|
-
// builds a
|
|
76
|
+
// builds a 240-float buffer mirroring the shader-side `View` struct
|
|
74
77
|
// field order. AC-06: existing field offsets unchanged -- tail append
|
|
75
78
|
// only.
|
|
76
|
-
const payload = new Float32Array(
|
|
79
|
+
const payload = new Float32Array(240);
|
|
77
80
|
// worldViewProj = identity for this assertion (mat4 in [0..16))
|
|
78
81
|
payload[0] = 1; // m00
|
|
79
82
|
payload[5] = 1; // m11
|
|
@@ -133,7 +136,7 @@ describe('w4 view UBO 240 B std140 layout (AC-06, browser)', () => {
|
|
|
133
136
|
// calculation.
|
|
134
137
|
const inverseProjF32Index = VIEW_UBO_OFFSETS.inverseViewProj / 4;
|
|
135
138
|
expect(inverseProjF32Index).toBe(44);
|
|
136
|
-
const payload = new Float32Array(
|
|
139
|
+
const payload = new Float32Array(240);
|
|
137
140
|
// Place an identity mat4 at inverseViewProj slot [44..59]
|
|
138
141
|
payload[44] = 1;
|
|
139
142
|
payload[49] = 1;
|
|
@@ -145,6 +148,13 @@ describe('w4 view UBO 240 B std140 layout (AC-06, browser)', () => {
|
|
|
145
148
|
expect(payload[59]).toBe(1);
|
|
146
149
|
});
|
|
147
150
|
|
|
151
|
+
it.skipIf(!browserReady)('shadow tail keeps the directional carrier before spot matrices', () => {
|
|
152
|
+
expect(DIRECTIONAL_FILTER_OFFSET).toBe(126 * 4 + 8);
|
|
153
|
+
expect(SPOT_LIGHT_VIEW_PROJ_OFFSET).toBe(132 * 4);
|
|
154
|
+
expect(SPOT_LIGHT_VIEW_PROJ_OFFSET - DIRECTIONAL_FILTER_OFFSET).toBe(16);
|
|
155
|
+
expect(SPOT_LIGHT_VIEW_PROJ_OFFSET + 4 * 4 * 16).toBe(VIEW_UBO_BYTES - 176);
|
|
156
|
+
});
|
|
157
|
+
|
|
148
158
|
it.skipIf(!browserReady)(
|
|
149
159
|
'existing field offsets unchanged by tail append (AC-06 rear-append guarantee)',
|
|
150
160
|
() => {
|