@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
|
@@ -164,7 +164,7 @@ function makeLights(): MockObj {
|
|
|
164
164
|
shadowMapSize: 0,
|
|
165
165
|
depthBias: 0,
|
|
166
166
|
normalBias: 0,
|
|
167
|
-
|
|
167
|
+
directionalShadowQuality: undefined,
|
|
168
168
|
pointShadow: [],
|
|
169
169
|
};
|
|
170
170
|
}
|
|
@@ -174,13 +174,12 @@ function makeFrameState(): MockObj {
|
|
|
174
174
|
frameNumber: 1,
|
|
175
175
|
compiledFrameGraph: null,
|
|
176
176
|
compiledFrameGraphTopologyKey: null,
|
|
177
|
+
compiledFrameGraphGeneration: 0,
|
|
177
178
|
retiredCompiledFrameGraphs: new Set(),
|
|
178
179
|
instanceBuffers: new Map(),
|
|
179
180
|
transientInstanceBuffers: [],
|
|
180
181
|
warnedZeroLightStandard: false,
|
|
181
182
|
warnedMultiLightDirectional: false,
|
|
182
|
-
warnedMultiLightPoint: false,
|
|
183
|
-
warnedMultiLightSpot: false,
|
|
184
183
|
warnedSkyboxTonemapNone: false,
|
|
185
184
|
warnedMissingBaseColorTextureHandles: new Set<number>(),
|
|
186
185
|
warnedNineSliceScaleEntities: new Set<number>(),
|
|
@@ -193,8 +192,7 @@ function makeFrameState(): MockObj {
|
|
|
193
192
|
installedPipelineHandle: 0,
|
|
194
193
|
activePipeline: { buildGraph: () => null, execute: () => undefined },
|
|
195
194
|
installedPipelineConfig: undefined,
|
|
196
|
-
|
|
197
|
-
hdrpOncePerFrameFired: new Set(),
|
|
195
|
+
standardOncePerFrameFired: new Set(),
|
|
198
196
|
pointShadowAtlas: null,
|
|
199
197
|
pointShadowSnapshots: [],
|
|
200
198
|
lastFoldBucketCount: 0,
|
|
@@ -231,6 +229,232 @@ function callRecordFrame(
|
|
|
231
229
|
acquireSwapChainTarget(internals, internals.getPipelineState());
|
|
232
230
|
}
|
|
233
231
|
|
|
232
|
+
type DirectionalShadowAdmission = {
|
|
233
|
+
readonly requested: 'off' | 'pcf1' | 'pcf3' | 'pcf5' | 'pcssMedium' | 'pcssHigh';
|
|
234
|
+
readonly effective:
|
|
235
|
+
| 'off'
|
|
236
|
+
| 'pcf1'
|
|
237
|
+
| 'pcf3'
|
|
238
|
+
| 'pcf5'
|
|
239
|
+
| 'pcssMedium'
|
|
240
|
+
| 'pcssHigh'
|
|
241
|
+
| 'rhi-null-structural';
|
|
242
|
+
readonly status: 'accepted' | 'fallback' | 'rejected';
|
|
243
|
+
readonly fallbackReason?: 'webgl2-unsupported' | 'rhi-null-structural' | 'candidate-failed';
|
|
244
|
+
readonly lastKnownGood: boolean;
|
|
245
|
+
readonly pixelEvidence: 'available' | 'not-available';
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
async function resolveDirectionalShadowAdmission(input: {
|
|
249
|
+
readonly backendKind: 'webgpu' | 'wgpu-webgl2' | 'null';
|
|
250
|
+
readonly requested: DirectionalShadowAdmission['requested'];
|
|
251
|
+
readonly candidate: 'accepted' | 'failed';
|
|
252
|
+
readonly lastKnownGood?: Exclude<
|
|
253
|
+
DirectionalShadowAdmission['effective'],
|
|
254
|
+
'off' | 'rhi-null-structural'
|
|
255
|
+
>;
|
|
256
|
+
}): Promise<DirectionalShadowAdmission> {
|
|
257
|
+
const module = (await import('../../../render/src/render-pipeline')) as Record<string, unknown>;
|
|
258
|
+
const resolver = module.resolveDirectionalShadowBackendAdmission;
|
|
259
|
+
expect(typeof resolver).toBe('function');
|
|
260
|
+
return (resolver as (value: typeof input) => DirectionalShadowAdmission)(input);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
describe('M3 backend admission and fallback truthfulness', () => {
|
|
264
|
+
it('keeps an explicitly disabled Directional shadow off without fabricating a candidate', async () => {
|
|
265
|
+
await expect(
|
|
266
|
+
resolveDirectionalShadowAdmission({
|
|
267
|
+
backendKind: 'webgpu',
|
|
268
|
+
requested: 'off',
|
|
269
|
+
candidate: 'failed',
|
|
270
|
+
}),
|
|
271
|
+
).resolves.toMatchObject({
|
|
272
|
+
requested: 'off',
|
|
273
|
+
effective: 'off',
|
|
274
|
+
status: 'accepted',
|
|
275
|
+
lastKnownGood: false,
|
|
276
|
+
pixelEvidence: 'not-available',
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('admits a production WebGPU/Dawn candidate without converting PCSS to PCF', async () => {
|
|
281
|
+
await expect(
|
|
282
|
+
resolveDirectionalShadowAdmission({
|
|
283
|
+
backendKind: 'webgpu',
|
|
284
|
+
requested: 'pcssMedium',
|
|
285
|
+
candidate: 'accepted',
|
|
286
|
+
}),
|
|
287
|
+
).resolves.toMatchObject({
|
|
288
|
+
requested: 'pcssMedium',
|
|
289
|
+
effective: 'pcssMedium',
|
|
290
|
+
status: 'accepted',
|
|
291
|
+
lastKnownGood: false,
|
|
292
|
+
pixelEvidence: 'available',
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it.each([
|
|
297
|
+
['pcssMedium', 'pcf3'],
|
|
298
|
+
['pcssHigh', 'pcf5'],
|
|
299
|
+
] as const)('maps WebGL2 %s to fixed effective %s', async (requested, effective) => {
|
|
300
|
+
await expect(
|
|
301
|
+
resolveDirectionalShadowAdmission({
|
|
302
|
+
backendKind: 'wgpu-webgl2',
|
|
303
|
+
requested,
|
|
304
|
+
candidate: 'accepted',
|
|
305
|
+
}),
|
|
306
|
+
).resolves.toMatchObject({
|
|
307
|
+
requested,
|
|
308
|
+
effective,
|
|
309
|
+
status: 'fallback',
|
|
310
|
+
fallbackReason: 'webgl2-unsupported',
|
|
311
|
+
pixelEvidence: 'available',
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('projects RhiNull as structural evidence and never as pixel evidence', async () => {
|
|
316
|
+
await expect(
|
|
317
|
+
resolveDirectionalShadowAdmission({
|
|
318
|
+
backendKind: 'null',
|
|
319
|
+
requested: 'pcssHigh',
|
|
320
|
+
candidate: 'accepted',
|
|
321
|
+
}),
|
|
322
|
+
).resolves.toMatchObject({
|
|
323
|
+
requested: 'pcssHigh',
|
|
324
|
+
effective: 'rhi-null-structural',
|
|
325
|
+
status: 'fallback',
|
|
326
|
+
fallbackReason: 'rhi-null-structural',
|
|
327
|
+
pixelEvidence: 'not-available',
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('rejects a capable candidate failure while retaining compatible LKG', async () => {
|
|
332
|
+
const result = await resolveDirectionalShadowAdmission({
|
|
333
|
+
backendKind: 'webgpu',
|
|
334
|
+
requested: 'pcssHigh',
|
|
335
|
+
candidate: 'failed',
|
|
336
|
+
lastKnownGood: 'pcf3',
|
|
337
|
+
});
|
|
338
|
+
expect(result).toMatchObject({
|
|
339
|
+
requested: 'pcssHigh',
|
|
340
|
+
effective: 'pcf3',
|
|
341
|
+
status: 'rejected',
|
|
342
|
+
fallbackReason: 'candidate-failed',
|
|
343
|
+
lastKnownGood: true,
|
|
344
|
+
pixelEvidence: 'available',
|
|
345
|
+
});
|
|
346
|
+
expect(result.effective).not.toBe('pcf5');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('keeps falsifiers observable instead of allowing fake effective mappings', async () => {
|
|
350
|
+
const result = await resolveDirectionalShadowAdmission({
|
|
351
|
+
backendKind: 'wgpu-webgl2',
|
|
352
|
+
requested: 'pcssMedium',
|
|
353
|
+
candidate: 'accepted',
|
|
354
|
+
});
|
|
355
|
+
expect(result.fallbackReason).toBe('webgl2-unsupported');
|
|
356
|
+
expect(result.effective).toBe('pcf3');
|
|
357
|
+
expect(result).not.toMatchObject({ effective: 'pcssMedium' });
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe('M3 bounded Directional shadow inspection', () => {
|
|
362
|
+
it('requires one detached projection with bounded tap and generation facts', async () => {
|
|
363
|
+
const module = (await import(
|
|
364
|
+
'../../../render/src/assembly/directional-shadow-inspection'
|
|
365
|
+
)) as Record<string, unknown>;
|
|
366
|
+
const project = module.projectDirectionalShadowInspection;
|
|
367
|
+
expect(typeof project).toBe('function');
|
|
368
|
+
const result = (project as (input: Record<string, unknown>) => Record<string, unknown>)({
|
|
369
|
+
admission: {
|
|
370
|
+
requested: 'pcssHigh',
|
|
371
|
+
effective: 'pcssHigh',
|
|
372
|
+
status: 'accepted',
|
|
373
|
+
pixelEvidence: 'available',
|
|
374
|
+
},
|
|
375
|
+
cascadeCount: 4,
|
|
376
|
+
mapSize: 2048,
|
|
377
|
+
atlasBytes: 67_108_864,
|
|
378
|
+
writerPasses: 4,
|
|
379
|
+
blockerTaps: 16,
|
|
380
|
+
filterTapUpperBound: 32,
|
|
381
|
+
seamTapUpperBound: 96,
|
|
382
|
+
deviceGeneration: 2,
|
|
383
|
+
graphGeneration: 7,
|
|
384
|
+
});
|
|
385
|
+
expect(result).toMatchObject({
|
|
386
|
+
requested: 'pcssHigh',
|
|
387
|
+
effective: 'pcssHigh',
|
|
388
|
+
cascadeCount: 4,
|
|
389
|
+
mapSize: 2048,
|
|
390
|
+
blockerTaps: 16,
|
|
391
|
+
filterTapUpperBound: 32,
|
|
392
|
+
seamTapUpperBound: 96,
|
|
393
|
+
deviceGeneration: 2,
|
|
394
|
+
graphGeneration: 7,
|
|
395
|
+
});
|
|
396
|
+
expect(Object.keys(result)).not.toContain('perPixel');
|
|
397
|
+
expect(Object.isFrozen(result)).toBe(true);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('projects production facts instead of fixed request, taps, or generations', async () => {
|
|
401
|
+
const module = (await import(
|
|
402
|
+
'../../../render/src/assembly/directional-shadow-inspection'
|
|
403
|
+
)) as Record<string, unknown>;
|
|
404
|
+
const projectSource = module.projectDirectionalShadowInspectionSource;
|
|
405
|
+
expect(typeof projectSource).toBe('function');
|
|
406
|
+
const project = projectSource as (input: Record<string, unknown>) => Record<string, unknown>;
|
|
407
|
+
const first = project({
|
|
408
|
+
requested: 'pcf3',
|
|
409
|
+
candidate: 'accepted',
|
|
410
|
+
cascadeCount: 3,
|
|
411
|
+
mapSize: 1024,
|
|
412
|
+
atlasBytes: 12_582_912,
|
|
413
|
+
writerPasses: 3,
|
|
414
|
+
deviceGeneration: 4,
|
|
415
|
+
graphGeneration: 11,
|
|
416
|
+
shadowReady: true,
|
|
417
|
+
});
|
|
418
|
+
const second = project({
|
|
419
|
+
requested: 'pcssHigh',
|
|
420
|
+
candidate: 'failed',
|
|
421
|
+
lastKnownGood: 'pcf3',
|
|
422
|
+
cascadeCount: 4,
|
|
423
|
+
mapSize: 2048,
|
|
424
|
+
atlasBytes: 67_108_864,
|
|
425
|
+
writerPasses: 4,
|
|
426
|
+
deviceGeneration: 5,
|
|
427
|
+
graphGeneration: 12,
|
|
428
|
+
shadowReady: false,
|
|
429
|
+
});
|
|
430
|
+
expect(first).toMatchObject({
|
|
431
|
+
requested: 'pcf3',
|
|
432
|
+
effective: 'pcf3',
|
|
433
|
+
status: 'accepted',
|
|
434
|
+
filterTapUpperBound: 9,
|
|
435
|
+
seamTapUpperBound: 9,
|
|
436
|
+
cascadeCount: 3,
|
|
437
|
+
mapSize: 1024,
|
|
438
|
+
deviceGeneration: 4,
|
|
439
|
+
graphGeneration: 11,
|
|
440
|
+
});
|
|
441
|
+
expect(second).toMatchObject({
|
|
442
|
+
requested: 'pcssHigh',
|
|
443
|
+
effective: 'pcf3',
|
|
444
|
+
status: 'rejected',
|
|
445
|
+
lastKnownGood: true,
|
|
446
|
+
blockerTaps: 16,
|
|
447
|
+
filterTapUpperBound: 32,
|
|
448
|
+
seamTapUpperBound: 96,
|
|
449
|
+
cascadeCount: 4,
|
|
450
|
+
mapSize: 2048,
|
|
451
|
+
deviceGeneration: 5,
|
|
452
|
+
graphGeneration: 12,
|
|
453
|
+
});
|
|
454
|
+
expect(second).not.toMatchObject({ requested: 'pcf3', status: 'accepted', atlasBytes: 0 });
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
234
458
|
// ── w5: surface retry — reconfigure + retry once (AC-03) ─────────────────────
|
|
235
459
|
|
|
236
460
|
describe('Surface retry (w5)', () => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// resolveName is the one runtime name-truth function and it delegates to the
|
|
4
4
|
// stateless deriveAssetName pure function (D-6). Every other name consumer reads
|
|
5
|
-
// resolveName or that same deriveAssetName -- none re-implements the
|
|
5
|
+
// resolveName or that same deriveAssetName -- none re-implements the display-name rule.
|
|
6
6
|
//
|
|
7
7
|
// This is a grep/fixture test, not a pure runtime assertion: it reads the source
|
|
8
8
|
// trees and asserts structural unification.
|
|
@@ -27,11 +27,11 @@ const buildCatalogSrc = readFileSync(
|
|
|
27
27
|
);
|
|
28
28
|
|
|
29
29
|
describe('unified name resolution (AC-04)', () => {
|
|
30
|
-
it('deriveAssetName is the single
|
|
30
|
+
it('deriveAssetName is the single display-name rule definition', () => {
|
|
31
31
|
expect(deriveSrc).toContain('export function deriveAssetName');
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
it('resolveName delegates to deriveAssetName (no inline
|
|
34
|
+
it('resolveName delegates to deriveAssetName (no inline re-implementation)', () => {
|
|
35
35
|
expect(registrySrc).toContain("import { deriveAssetName } from '@forgeax/engine-pack/name'");
|
|
36
36
|
// resolveName returns the result of deriveAssetName, not a hand-rolled rule.
|
|
37
37
|
const body = registrySrc.slice(
|
|
@@ -55,11 +55,11 @@ describe('unified name resolution (AC-04)', () => {
|
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
// M4 expansion (w22 landed): catalog-builder routes through deriveAssetName
|
|
58
|
-
it('catalog-builder imports and calls deriveAssetName (no inline
|
|
58
|
+
it('catalog-builder imports and calls deriveAssetName (no inline display-name rule)', () => {
|
|
59
59
|
expect(buildCatalogSrc).toContain("import { deriveAssetName } from './deriveAssetName.js'");
|
|
60
60
|
expect(buildCatalogSrc).toContain('deriveAssetName(');
|
|
61
|
-
// No inline re-implementation
|
|
62
|
-
//
|
|
61
|
+
// No inline re-implementation (no hand-rolled basename); the single
|
|
62
|
+
// deriveAssetName pure function handles the display-name rule.
|
|
63
63
|
expect(buildCatalogSrc.match(/deriveAssetName\(/g)?.length ?? 0).toBeGreaterThanOrEqual(2);
|
|
64
64
|
});
|
|
65
65
|
});
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// state directly through registerPackage (Risk-2: no pack-index fetch needed).
|
|
5
5
|
//
|
|
6
6
|
// Coverage:
|
|
7
|
-
// AC-01 — single-asset package:
|
|
8
|
-
// Asset POD carries no `name` property
|
|
7
|
+
// AC-01 — single-asset package: an explicit stored name wins; basename(path)
|
|
8
|
+
// remains the fallback. The Asset POD carries no `name` property.
|
|
9
9
|
// AC-02 — multi-asset package: resolveName returns each entry's stored name.
|
|
10
10
|
|
|
11
11
|
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
@@ -42,6 +42,18 @@ describe('resolveName single-asset package (AC-01)', () => {
|
|
|
42
42
|
expect(stored).toBeDefined();
|
|
43
43
|
expect(Object.hasOwn(stored as object, 'name')).toBe(false);
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
it('preserves an explicit authored name for a single-member pack', () => {
|
|
47
|
+
const reg = makeRegistry();
|
|
48
|
+
reg.catalog(parseGuid(SINGLE_GUID), sampler);
|
|
49
|
+
reg._registerPackage(
|
|
50
|
+
'assets/Materials.pack.json',
|
|
51
|
+
[SINGLE_GUID],
|
|
52
|
+
new Map([[SINGLE_GUID, 'NewMaterial']]),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(reg.resolveName(SINGLE_GUID)).toBe('NewMaterial');
|
|
56
|
+
});
|
|
45
57
|
});
|
|
46
58
|
|
|
47
59
|
describe('resolveName multi-asset package (AC-02)', () => {
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// cascade tile: col = i % tilesPerSide, row = floor(i / tilesPerSide).
|
|
7
7
|
|
|
8
8
|
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { projectDirectionalShadowInspection } from '../../../render/src/assembly/directional-shadow-inspection';
|
|
10
|
+
import { resolveDirectionalShadowBackendAdmission } from '../../../render/src/render-pipeline';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Pure helper mirroring urp-pipeline.ts atlas sizing logic.
|
|
@@ -176,3 +178,42 @@ describe('CSM atlas layout (w13)', () => {
|
|
|
176
178
|
});
|
|
177
179
|
});
|
|
178
180
|
});
|
|
181
|
+
|
|
182
|
+
describe('M3 inspection zero-delta structural budget', () => {
|
|
183
|
+
it('keeps PCF and PCSS on the same atlas and writer budget', () => {
|
|
184
|
+
const common = {
|
|
185
|
+
cascadeCount: 4,
|
|
186
|
+
mapSize: 2048,
|
|
187
|
+
atlasBytes: 67_108_864,
|
|
188
|
+
writerPasses: 4,
|
|
189
|
+
deviceGeneration: 1,
|
|
190
|
+
graphGeneration: 2,
|
|
191
|
+
} as const;
|
|
192
|
+
const pcf = projectDirectionalShadowInspection({
|
|
193
|
+
admission: resolveDirectionalShadowBackendAdmission({
|
|
194
|
+
backendKind: 'webgpu',
|
|
195
|
+
requested: 'pcf5',
|
|
196
|
+
candidate: 'accepted',
|
|
197
|
+
}),
|
|
198
|
+
...common,
|
|
199
|
+
blockerTaps: 0,
|
|
200
|
+
filterTapUpperBound: 25,
|
|
201
|
+
seamTapUpperBound: 25,
|
|
202
|
+
});
|
|
203
|
+
const pcss = projectDirectionalShadowInspection({
|
|
204
|
+
admission: resolveDirectionalShadowBackendAdmission({
|
|
205
|
+
backendKind: 'webgpu',
|
|
206
|
+
requested: 'pcssHigh',
|
|
207
|
+
candidate: 'accepted',
|
|
208
|
+
}),
|
|
209
|
+
...common,
|
|
210
|
+
blockerTaps: 16,
|
|
211
|
+
filterTapUpperBound: 32,
|
|
212
|
+
seamTapUpperBound: 96,
|
|
213
|
+
});
|
|
214
|
+
expect(pcss).toMatchObject({ effective: 'pcssHigh', cascadeCount: 4, mapSize: 2048 });
|
|
215
|
+
expect(pcf.writerPasses).toBe(pcss.writerPasses);
|
|
216
|
+
expect(pcf.atlasBytes).toBe(pcss.atlasBytes);
|
|
217
|
+
expect(pcf.graphGeneration).toBe(pcss.graphGeneration);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -83,7 +83,11 @@ describe('CSM component schema (w1)', () => {
|
|
|
83
83
|
error: {
|
|
84
84
|
code?: string;
|
|
85
85
|
hint?: string;
|
|
86
|
-
detail?: {
|
|
86
|
+
detail?: {
|
|
87
|
+
field?: string;
|
|
88
|
+
actual?: number;
|
|
89
|
+
bound?: { kind?: string; min?: number; max?: number };
|
|
90
|
+
};
|
|
87
91
|
};
|
|
88
92
|
};
|
|
89
93
|
|
|
@@ -97,7 +101,7 @@ describe('CSM component schema (w1)', () => {
|
|
|
97
101
|
expect(r.error.code).toBe('shadow-invalid-config');
|
|
98
102
|
expect(r.error.hint).toContain('cascadeCount');
|
|
99
103
|
expect(r.error.detail?.field).toBe('cascadeCount');
|
|
100
|
-
expect(r.error.detail?.
|
|
104
|
+
expect(r.error.detail?.actual).toBe(0);
|
|
101
105
|
});
|
|
102
106
|
|
|
103
107
|
it('cascadeCount=5 -> ShadowInvalidConfigError (max=4)', () => {
|
|
@@ -110,8 +114,8 @@ describe('CSM component schema (w1)', () => {
|
|
|
110
114
|
expect(r.error.code).toBe('shadow-invalid-config');
|
|
111
115
|
expect(r.error.hint).toContain('cascadeCount');
|
|
112
116
|
expect(r.error.detail?.field).toBe('cascadeCount');
|
|
113
|
-
expect(r.error.detail?.
|
|
114
|
-
expect(r.error.detail?.
|
|
117
|
+
expect(r.error.detail?.actual).toBe(5);
|
|
118
|
+
expect(r.error.detail?.bound).toEqual({ kind: 'range', min: 1, max: 4 });
|
|
115
119
|
});
|
|
116
120
|
|
|
117
121
|
it('splitLambda=-0.1 -> ShadowInvalidConfigError (min=0)', () => {
|
|
@@ -124,7 +128,8 @@ describe('CSM component schema (w1)', () => {
|
|
|
124
128
|
expect(r.error.code).toBe('shadow-invalid-config');
|
|
125
129
|
expect(r.error.hint).toContain('splitLambda');
|
|
126
130
|
expect(r.error.detail?.field).toBe('splitLambda');
|
|
127
|
-
expect(r.error.detail?.
|
|
131
|
+
expect(r.error.detail?.actual).toBeCloseTo(-0.1, 5);
|
|
132
|
+
expect(r.error.detail?.bound).toEqual({ kind: 'range', min: 0, max: 1 });
|
|
128
133
|
});
|
|
129
134
|
|
|
130
135
|
it('splitLambda=1.1 -> ShadowInvalidConfigError (max=1)', () => {
|
|
@@ -137,8 +142,8 @@ describe('CSM component schema (w1)', () => {
|
|
|
137
142
|
expect(r.error.code).toBe('shadow-invalid-config');
|
|
138
143
|
expect(r.error.hint).toContain('splitLambda');
|
|
139
144
|
expect(r.error.detail?.field).toBe('splitLambda');
|
|
140
|
-
expect(r.error.detail?.
|
|
141
|
-
expect(r.error.detail?.
|
|
145
|
+
expect(r.error.detail?.actual).toBeCloseTo(1.1, 5);
|
|
146
|
+
expect(r.error.detail?.bound).toEqual({ kind: 'range', min: 0, max: 1 });
|
|
142
147
|
});
|
|
143
148
|
|
|
144
149
|
it('cascadeBlend=-0.1 -> ShadowInvalidConfigError (min=0)', () => {
|
|
@@ -151,7 +156,8 @@ describe('CSM component schema (w1)', () => {
|
|
|
151
156
|
expect(r.error.code).toBe('shadow-invalid-config');
|
|
152
157
|
expect(r.error.hint).toContain('cascadeBlend');
|
|
153
158
|
expect(r.error.detail?.field).toBe('cascadeBlend');
|
|
154
|
-
expect(r.error.detail?.
|
|
159
|
+
expect(r.error.detail?.actual).toBeCloseTo(-0.1, 5);
|
|
160
|
+
expect(r.error.detail?.bound).toEqual({ kind: 'range', min: 0, max: 0.5 });
|
|
155
161
|
});
|
|
156
162
|
|
|
157
163
|
it('cascadeBlend=0.6 -> ShadowInvalidConfigError (max=0.5)', () => {
|
|
@@ -164,8 +170,8 @@ describe('CSM component schema (w1)', () => {
|
|
|
164
170
|
expect(r.error.code).toBe('shadow-invalid-config');
|
|
165
171
|
expect(r.error.hint).toContain('cascadeBlend');
|
|
166
172
|
expect(r.error.detail?.field).toBe('cascadeBlend');
|
|
167
|
-
expect(r.error.detail?.
|
|
168
|
-
expect(r.error.detail?.
|
|
173
|
+
expect(r.error.detail?.actual).toBeCloseTo(0.6, 5);
|
|
174
|
+
expect(r.error.detail?.bound).toEqual({ kind: 'range', min: 0, max: 0.5 });
|
|
169
175
|
});
|
|
170
176
|
|
|
171
177
|
it('all three new fields within valid range spawn succeeds', () => {
|
|
@@ -180,29 +186,28 @@ describe('CSM component schema (w1)', () => {
|
|
|
180
186
|
|
|
181
187
|
// ── w2: ShadowInvalidConfigDetail.max field constructor tests ──────────
|
|
182
188
|
|
|
183
|
-
describe('w2: ShadowInvalidConfigDetail.
|
|
184
|
-
it('cascadeCount=5
|
|
189
|
+
describe('w2: ShadowInvalidConfigDetail.bound constructor', () => {
|
|
190
|
+
it('cascadeCount=5 bound.max=4 via constructor', () => {
|
|
185
191
|
// TDD red: max param not yet on constructor.
|
|
186
192
|
const err = new (ShadowInvalidConfigError as any)('cascadeCount', 5, 1, 4);
|
|
187
193
|
expect(err.code).toBe('shadow-invalid-config');
|
|
188
194
|
expect(err.detail.field).toBe('cascadeCount');
|
|
189
|
-
expect(err.detail.
|
|
190
|
-
expect(err.detail.
|
|
191
|
-
expect(err.detail.max).toBe(4);
|
|
195
|
+
expect(err.detail.actual).toBe(5);
|
|
196
|
+
expect(err.detail.bound).toEqual({ kind: 'range', min: 1, max: 4 });
|
|
192
197
|
expect(err.hint).toContain('[1, 4]');
|
|
193
198
|
});
|
|
194
199
|
|
|
195
|
-
it('cascadeBlend=0.6
|
|
200
|
+
it('cascadeBlend=0.6 bound.max=0.5 via constructor', () => {
|
|
196
201
|
const err = new (ShadowInvalidConfigError as any)('cascadeBlend', 0.6, 0, 0.5);
|
|
197
202
|
expect(err.detail.field).toBe('cascadeBlend');
|
|
198
|
-
expect(err.detail.
|
|
203
|
+
expect(err.detail.bound).toEqual({ kind: 'range', min: 0, max: 0.5 });
|
|
199
204
|
expect(err.hint).toContain('[0, 0.5]');
|
|
200
205
|
});
|
|
201
206
|
|
|
202
|
-
it('splitLambda=1.1
|
|
207
|
+
it('splitLambda=1.1 bound.max=1 via constructor', () => {
|
|
203
208
|
const err = new (ShadowInvalidConfigError as any)('splitLambda', 1.1, 0, 1);
|
|
204
209
|
expect(err.detail.field).toBe('splitLambda');
|
|
205
|
-
expect(err.detail.
|
|
210
|
+
expect(err.detail.bound).toEqual({ kind: 'range', min: 0, max: 1 });
|
|
206
211
|
expect(err.hint).toContain('[0, 1]');
|
|
207
212
|
});
|
|
208
213
|
});
|
|
@@ -260,15 +265,17 @@ describe('CSM component schema (w1)', () => {
|
|
|
260
265
|
const err = r.error as {
|
|
261
266
|
code?: string;
|
|
262
267
|
hint?: string;
|
|
263
|
-
detail?: {
|
|
268
|
+
detail?: {
|
|
269
|
+
field?: string;
|
|
270
|
+
actual?: number;
|
|
271
|
+
bound?: { kind?: string; min?: number; max?: number };
|
|
272
|
+
};
|
|
264
273
|
};
|
|
265
274
|
expect(err.code).toBe('shadow-invalid-config');
|
|
266
275
|
expect(err.hint).toContain('mapSize');
|
|
267
276
|
expect(err.detail?.field).toBe('mapSize');
|
|
268
|
-
expect(err.detail?.
|
|
269
|
-
expect(err.detail?.
|
|
270
|
-
// mapSize < 1 has no max (lower-bound only), compatible with old callers.
|
|
271
|
-
expect(err.detail?.max).toBeUndefined();
|
|
277
|
+
expect(err.detail?.actual).toBe(0);
|
|
278
|
+
expect(err.detail?.bound).toEqual({ kind: 'lower-bound', operator: '>=', value: 1 });
|
|
272
279
|
}
|
|
273
280
|
});
|
|
274
281
|
|