@forgeax/engine-runtime 0.1.21 → 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 +3 -3
- 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__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/index.mjs +3 -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__/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.unit.test.ts +5 -2
- package/src/__tests__/collect-transient-roundtrip.test.ts +14 -3
- package/src/__tests__/components.test-d.ts +9 -6
- package/src/__tests__/components.unit.test.ts +23 -19
- package/src/__tests__/errors.unit.test.ts +3 -0
- package/src/__tests__/extract-frames-merge.test.ts +3 -0
- 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 +8 -10
- package/src/__tests__/light-snapshot.test-d.ts +27 -1
- package/src/__tests__/lights-preservation.fixup.test.ts +19 -21
- package/src/__tests__/lights.unit.test.ts +2076 -1
- 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 +16 -16
- package/src/__tests__/pipeline.unit.test.ts +20 -24
- package/src/__tests__/point-light-shadow.browser.test.ts +1 -0
- package/src/__tests__/point-light-shadow.unit.test.ts +18 -98
- 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 +10 -0
- package/src/__tests__/render-system-extract.test.ts +5 -5
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +1 -0
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +1 -0
- package/src/__tests__/renderer-surface.unit.test.ts +0 -2
- package/src/__tests__/resolveName-unified-entry.test.ts +6 -6
- package/src/__tests__/resolveName.test.ts +14 -2
- package/src/__tests__/shadow-csm-cascade-loadop.test.ts +1 -0
- package/src/__tests__/shadow-csm-tile-consistency.test.ts +1 -0
- package/src/__tests__/shadow-csm-ubo.test.ts +3 -1
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +15 -4
- 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 +1 -6
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -13
- package/src/__tests__/ssao-bgl.test.ts +2 -1
- package/src/__tests__/systems.unit.test.ts +25 -174
- 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/collect-scene-asset.ts +1 -1
- package/src/components/__tests__/transform-vec-schema.test.ts +2 -2
- package/src/createRenderer.ts +1 -0
- package/src/sprite-param-values.ts +1 -1
- package/dist/.tsbuildinfo +0 -1
|
@@ -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)', () => {
|
|
@@ -269,6 +269,8 @@ describe('directional filter carrier tail-pad wiring (M1-T05)', () => {
|
|
|
269
269
|
|
|
270
270
|
it('keeps one View binding and removes the retired spot matrix binding', () => {
|
|
271
271
|
expect(wgslSrc.match(/@group\(0\) @binding\(0\) var<uniform> view/g)).toHaveLength(1);
|
|
272
|
-
expect(wgslSrc).not.toMatch(
|
|
272
|
+
expect(wgslSrc).not.toMatch(
|
|
273
|
+
/@group\(0\)\s*@binding\(9\)\s*var<uniform>\s+spotLightViewProj\s*:/,
|
|
274
|
+
);
|
|
273
275
|
});
|
|
274
276
|
});
|
|
@@ -29,11 +29,18 @@ import { validateDirectionalLightData } from '../../../render/src/components/lig
|
|
|
29
29
|
import { constructRuntimeRendererHost } from '../renderer-host';
|
|
30
30
|
import { drawPublished } from './draw-published';
|
|
31
31
|
|
|
32
|
-
const
|
|
33
|
-
|
|
32
|
+
const LIGHTWEIGHT_DAWN = process.env.FORGEAX_DAWN_LIGHTWEIGHT === '1';
|
|
33
|
+
// The CI observability lane only needs enough settled frames to prove the
|
|
34
|
+
// field-owned pixel/UBO deltas. Keep the full 300-frame, 256x256 window for
|
|
35
|
+
// local and nightly runs, while the isolated PR process uses a bounded 60-frame
|
|
36
|
+
// 128x128 target to avoid retaining a large lavapipe command backlog.
|
|
37
|
+
const SHADOW_FIELDS_RENDER_FRAMES = LIGHTWEIGHT_DAWN ? 60 : 300;
|
|
38
|
+
const WIDTH = LIGHTWEIGHT_DAWN ? 128 : 256;
|
|
39
|
+
const HEIGHT = LIGHTWEIGHT_DAWN ? 128 : 256;
|
|
34
40
|
const BPR = Math.ceil((WIDTH * 4) / 256) * 256;
|
|
35
41
|
|
|
36
|
-
// Each falsifier performs four fresh
|
|
42
|
+
// Each falsifier performs four fresh Dawn readbacks at the active target size
|
|
43
|
+
// (256x256 by default, 128x128 in the lightweight CI lane). Windows' Dawn
|
|
37
44
|
// prebuild can exceed Vitest's 90-second bound for that sequence while still
|
|
38
45
|
// completing well inside the nightly platform job's 45-minute budget.
|
|
39
46
|
const SHADOW_FIELDS_FALSIFIER_TIMEOUT_MS = 300_000;
|
|
@@ -355,9 +362,13 @@ async function renderConfig(
|
|
|
355
362
|
const w = new World();
|
|
356
363
|
spawnScene(w, castShadow, depthBias, normalBias, pcf, mapSize);
|
|
357
364
|
let de = 0;
|
|
358
|
-
for (let i = 0; i <
|
|
365
|
+
for (let i = 0; i < SHADOW_FIELDS_RENDER_FRAMES; i++) {
|
|
359
366
|
const r = drawPublished(renderer, w);
|
|
360
367
|
if (!r.ok) de++;
|
|
368
|
+
// Keep the native Dawn carrier paced like a real frame loop. Without a
|
|
369
|
+
// bounded drain, a long shadow-map leg can retain an unbounded lavapipe
|
|
370
|
+
// command/resource backlog before the final fence below.
|
|
371
|
+
if (i % 16 === 15) await dev.queue.onSubmittedWorkDone();
|
|
361
372
|
}
|
|
362
373
|
if (de > 0) throw new Error(`draw errors: ${de}`);
|
|
363
374
|
// rt is created lazily by the canvas configure()/getCurrentTexture path during
|
|
@@ -15,7 +15,7 @@ const extractPath = (() => {
|
|
|
15
15
|
describe('_getArrayView count (AC-03 gate)', () => {
|
|
16
16
|
it('keeps skin and instance hot fields on column views', () => {
|
|
17
17
|
const src = readFileSync(extractPath, 'utf8');
|
|
18
|
-
expect(src).toContain("_getArrayView(jointEntity,
|
|
18
|
+
expect(src).toContain("_getArrayView(jointEntity, GlobalTransform, 'world')");
|
|
19
19
|
expect(src).toContain("_getArrayView(entity, Instances, 'transforms')");
|
|
20
20
|
expect(src).toContain("_getArrayView(entity, SpriteInstances, 'transforms')");
|
|
21
21
|
expect(src).toContain("_getArrayView(entity, SpriteInstances, 'regions')");
|
|
@@ -34,7 +34,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
34
34
|
import { World } from '@forgeax/engine-ecs';
|
|
35
35
|
import type { Mat4, Vec3 } from '@forgeax/engine-math';
|
|
36
36
|
import { mat4, vec3 } from '@forgeax/engine-math';
|
|
37
|
-
import { ChildOf, propagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
37
|
+
import { ChildOf, GlobalTransform, propagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
38
38
|
import { Skin } from '@forgeax/engine-skinning';
|
|
39
39
|
import { describe, expect, it } from 'vitest';
|
|
40
40
|
|
|
@@ -125,13 +125,13 @@ describe('skin parented double transform (AC-01)', () => {
|
|
|
125
125
|
const r = propagateTransforms(world);
|
|
126
126
|
expect(r.ok).toBe(true);
|
|
127
127
|
|
|
128
|
-
// Read
|
|
129
|
-
const st = world.get(skinEntity,
|
|
128
|
+
// Read derived GlobalTransform.world for skin entity and joint entity.
|
|
129
|
+
const st = world.get(skinEntity, GlobalTransform);
|
|
130
130
|
expect(st.ok).toBe(true);
|
|
131
131
|
if (!st.ok) return;
|
|
132
132
|
const meshWorldMat = mat4FromFloat32Array(st.value.world as unknown as Float32Array);
|
|
133
133
|
|
|
134
|
-
const jt = world.get(jointEntity,
|
|
134
|
+
const jt = world.get(jointEntity, GlobalTransform);
|
|
135
135
|
expect(jt.ok).toBe(true);
|
|
136
136
|
if (!jt.ok) return;
|
|
137
137
|
const jointWorldMat = mat4FromFloat32Array(jt.value.world as unknown as Float32Array);
|
|
@@ -202,7 +202,7 @@ describe('skin parented double transform (AC-01)', () => {
|
|
|
202
202
|
const r = propagateTransforms(world);
|
|
203
203
|
expect(r.ok).toBe(true);
|
|
204
204
|
|
|
205
|
-
const jt = world.get(jointEntity,
|
|
205
|
+
const jt = world.get(jointEntity, GlobalTransform);
|
|
206
206
|
expect(jt.ok).toBe(true);
|
|
207
207
|
if (!jt.ok) return;
|
|
208
208
|
const jointWorldMat = mat4FromFloat32Array(jt.value.world as unknown as Float32Array);
|
|
@@ -218,12 +218,7 @@ function spawnMixedScene(world: World, castShadow: boolean): void {
|
|
|
218
218
|
};
|
|
219
219
|
// Entity order is intentional: static A -> skinned B -> static C.
|
|
220
220
|
spawnStaticCaster(-3);
|
|
221
|
-
const joint = world
|
|
222
|
-
.spawn({
|
|
223
|
-
component: Transform,
|
|
224
|
-
data: identityTransform([0, 7, 5]),
|
|
225
|
-
})
|
|
226
|
-
.unwrap();
|
|
221
|
+
const joint = world.spawn({ component: Transform, data: identityTransform([0, 7, 5]) }).unwrap();
|
|
227
222
|
world.spawn(
|
|
228
223
|
{ component: Transform, data: identityTransform() },
|
|
229
224
|
{ component: MeshFilter, data: { assetHandle: skinMesh } },
|
|
@@ -46,22 +46,22 @@ function makeSpec(shaderId: string): PipelineSpec {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
|
|
49
|
-
describe('pbr-view BGL (
|
|
50
|
-
it('sprite-lit vs sprite share the same
|
|
49
|
+
describe('pbr-view BGL (Cluster lights live in group(2))', () => {
|
|
50
|
+
it('sprite-lit vs sprite share the same 10-entry pbr-view BGL under storage-buffer caps', () => {
|
|
51
51
|
const spriteSpec = makeSpec('forgeax::sprite');
|
|
52
52
|
const spriteLitSpec = makeSpec('forgeax::sprite-lit');
|
|
53
53
|
const sprite = buildBindGroupLayoutDescriptor(spriteSpec, {
|
|
54
54
|
kind: 'pbr-view',
|
|
55
|
-
caps: { storageBuffer: true },
|
|
55
|
+
caps: { storageBuffer: true, extendedLighting: false },
|
|
56
56
|
});
|
|
57
57
|
const spriteLit = buildBindGroupLayoutDescriptor(spriteLitSpec, {
|
|
58
58
|
kind: 'pbr-view',
|
|
59
|
-
caps: { storageBuffer: true },
|
|
59
|
+
caps: { storageBuffer: true, extendedLighting: false },
|
|
60
60
|
});
|
|
61
61
|
expect(JSON.stringify(spriteLit)).toBe(JSON.stringify(sprite));
|
|
62
62
|
// binding 10 is the shared Points/Lines view UBO; bindings 11/12 are the
|
|
63
63
|
// optional SpotLight projector texture and sampler on capable devices.
|
|
64
|
-
expect(spriteLit.entries.length).toBe(
|
|
64
|
+
expect(spriteLit.entries.length).toBe(10);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
it('sprite-lit vs sprite share the same 10-entry pbr-view BGL under uniform fallback caps (AC-10)', () => {
|
|
@@ -69,25 +69,23 @@ describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
|
|
|
69
69
|
const spriteLitSpec = makeSpec('forgeax::sprite-lit');
|
|
70
70
|
const sprite = buildBindGroupLayoutDescriptor(spriteSpec, {
|
|
71
71
|
kind: 'pbr-view',
|
|
72
|
-
caps: { storageBuffer: false },
|
|
72
|
+
caps: { storageBuffer: false, extendedLighting: false },
|
|
73
73
|
});
|
|
74
74
|
const spriteLit = buildBindGroupLayoutDescriptor(spriteLitSpec, {
|
|
75
75
|
kind: 'pbr-view',
|
|
76
|
-
caps: { storageBuffer: false },
|
|
76
|
+
caps: { storageBuffer: false, extendedLighting: false },
|
|
77
77
|
});
|
|
78
78
|
expect(JSON.stringify(spriteLit)).toBe(JSON.stringify(sprite));
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
it('
|
|
81
|
+
it('does not expose point/spot light bindings on storageBuffer=true', () => {
|
|
82
82
|
const entries = buildPbrViewBglEntries({ storageBuffer: true });
|
|
83
|
-
expect(entries
|
|
84
|
-
expect(entries[2]?.buffer?.type).toBe('read-only-storage');
|
|
83
|
+
expect(entries.some((entry) => entry.binding === 1 || entry.binding === 2)).toBe(false);
|
|
85
84
|
});
|
|
86
85
|
|
|
87
|
-
it('
|
|
86
|
+
it('does not expose point/spot light bindings on storageBuffer=false', () => {
|
|
88
87
|
const entries = buildPbrViewBglEntries({ storageBuffer: false });
|
|
89
|
-
expect(entries
|
|
90
|
-
expect(entries[2]?.buffer?.type).toBe('uniform');
|
|
88
|
+
expect(entries.some((entry) => entry.binding === 1 || entry.binding === 2)).toBe(false);
|
|
91
89
|
});
|
|
92
90
|
});
|
|
93
91
|
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
getOrCreateSsaoFallbackTexture,
|
|
40
40
|
packClusterUniform,
|
|
41
41
|
} from '../../../render/src/hdrp-buffers';
|
|
42
|
+
import { BYTES_PER_DIRECT_LIGHT_SLOT } from '../../../render/src/light-buffer-layout';
|
|
42
43
|
import { createHdrpBindGroupLayoutDescriptor } from '../../../render/src/pipeline-spec';
|
|
43
44
|
import type { RenderSystemRuntime } from '../../../render/src/render-system';
|
|
44
45
|
|
|
@@ -133,7 +134,7 @@ function fakeHdrpBuffers(): HdrpBuffers {
|
|
|
133
134
|
return {
|
|
134
135
|
device: {} as HdrpBuffers['device'],
|
|
135
136
|
lightDataBuffer: mockBuffer('hdrp-light-data'),
|
|
136
|
-
lightDataBytes:
|
|
137
|
+
lightDataBytes: 256 * BYTES_PER_DIRECT_LIGHT_SLOT,
|
|
137
138
|
clusterGridBuffer: mockBuffer('hdrp-cluster-grid'),
|
|
138
139
|
clusterGridBytes: 1024,
|
|
139
140
|
lightIndexListBuffer: mockBuffer('hdrp-light-index-list'),
|
|
@@ -80,7 +80,13 @@ import {
|
|
|
80
80
|
} from '@forgeax/engine-render';
|
|
81
81
|
import type { BindGroupEntry, Buffer, Sampler, Texture, TextureView } from '@forgeax/engine-rhi';
|
|
82
82
|
import { ok as rhiOk } from '@forgeax/engine-rhi';
|
|
83
|
-
import {
|
|
83
|
+
import {
|
|
84
|
+
ChildOf,
|
|
85
|
+
GlobalTransform,
|
|
86
|
+
Name,
|
|
87
|
+
propagateTransforms,
|
|
88
|
+
Transform,
|
|
89
|
+
} from '@forgeax/engine-scene';
|
|
84
90
|
import { TONEMAP_LUMINANCE_EPSILON } from '@forgeax/engine-shader';
|
|
85
91
|
import type { SkinErrorCode } from '@forgeax/engine-skinning';
|
|
86
92
|
import {
|
|
@@ -206,11 +212,7 @@ import {
|
|
|
206
212
|
} from '../../../render/src/instance-buffer-cache';
|
|
207
213
|
import { standardPipeline as urpPipeline } from '../../../render/src/pipeline/standard-pipeline';
|
|
208
214
|
import { ZERO_CAMERA_CLEAR_FALLBACK } from '../../../render/src/record/frame-snapshot';
|
|
209
|
-
import {
|
|
210
|
-
warnMultiLightDirectional,
|
|
211
|
-
warnMultiLightPoint,
|
|
212
|
-
warnMultiLightSpot,
|
|
213
|
-
} from '../../../render/src/record/helpers';
|
|
215
|
+
import { warnMultiLightDirectional } from '../../../render/src/record/helpers';
|
|
214
216
|
import type { CameraSnapshot } from '../../../render/src/render-contract';
|
|
215
217
|
import {
|
|
216
218
|
type ExtractedLights,
|
|
@@ -575,6 +577,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
575
577
|
directionalCount: 0,
|
|
576
578
|
point: [],
|
|
577
579
|
spot: [],
|
|
580
|
+
rect: [],
|
|
578
581
|
directionalShadowError: undefined,
|
|
579
582
|
lightSpaceMatrix: undefined,
|
|
580
583
|
shadowMapSize: undefined,
|
|
@@ -690,8 +693,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
690
693
|
unlitPipelineHdr: null,
|
|
691
694
|
shadowFallbackTextureView: { __label: 'shadow-fallback-view' },
|
|
692
695
|
skylightFallback: null,
|
|
693
|
-
pointLightsBuffer: { __label: 'point-lights-buf' },
|
|
694
|
-
spotLightsBuffer: { __label: 'spot-lights-buf' },
|
|
695
696
|
pbrPipelineLayout: { __label: 'pbr-pipeline-layout' },
|
|
696
697
|
defaultNormalTextureView: { __label: 'default-normal-view' },
|
|
697
698
|
perPassResources: {
|
|
@@ -776,8 +777,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
776
777
|
warnedZeroLightStandard: false,
|
|
777
778
|
warnedShadowDisabled: false,
|
|
778
779
|
warnedMultiLightDirectional: false,
|
|
779
|
-
warnedMultiLightPoint: false,
|
|
780
|
-
warnedMultiLightSpot: false,
|
|
781
780
|
warnedSkyboxTonemapNone: false,
|
|
782
781
|
warnedMissingBaseColorTextureHandles: new Set<number>(),
|
|
783
782
|
warnedNineSliceScaleEntities: new Set<number>(),
|
|
@@ -833,8 +832,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
833
832
|
warnedZeroLightStandard: false,
|
|
834
833
|
warnedShadowDisabled: false,
|
|
835
834
|
warnedMultiLightDirectional: false,
|
|
836
|
-
warnedMultiLightPoint: false,
|
|
837
|
-
warnedMultiLightSpot: false,
|
|
838
835
|
warnedSkyboxTonemapNone: false,
|
|
839
836
|
warnedMissingBaseColorTextureHandles: new Set<number>(),
|
|
840
837
|
warnedNineSliceScaleEntities: new Set<number>(),
|
|
@@ -1318,8 +1315,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
1318
1315
|
function makeFrameState() {
|
|
1319
1316
|
return {
|
|
1320
1317
|
warnedMultiLightDirectional: false,
|
|
1321
|
-
warnedMultiLightPoint: false,
|
|
1322
|
-
warnedMultiLightSpot: false,
|
|
1323
1318
|
};
|
|
1324
1319
|
}
|
|
1325
1320
|
|
|
@@ -1388,151 +1383,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
1388
1383
|
}
|
|
1389
1384
|
});
|
|
1390
1385
|
});
|
|
1391
|
-
|
|
1392
|
-
describe('warnMultiLightPoint (M3 / AC-04 / AC-05 (c))', () => {
|
|
1393
|
-
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
1394
|
-
|
|
1395
|
-
beforeEach(() => {
|
|
1396
|
-
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1397
|
-
});
|
|
1398
|
-
|
|
1399
|
-
afterEach(() => {
|
|
1400
|
-
warnSpy.mockRestore();
|
|
1401
|
-
});
|
|
1402
|
-
|
|
1403
|
-
it('first hit fires console.warn (N>4)', () => {
|
|
1404
|
-
const fs = makeFrameState();
|
|
1405
|
-
warnMultiLightPoint(fs, 5);
|
|
1406
|
-
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
1407
|
-
expect(fs.warnedMultiLightPoint).toBe(true);
|
|
1408
|
-
const call = warnSpy.mock.calls[0];
|
|
1409
|
-
expect(call[1]).toMatchObject({
|
|
1410
|
-
code: 'render-system-multi-light',
|
|
1411
|
-
expected: 'at most 4 point',
|
|
1412
|
-
detail: { type: 'point', got: 5 },
|
|
1413
|
-
});
|
|
1414
|
-
});
|
|
1415
|
-
|
|
1416
|
-
it('no-op when pointCount <= 4', () => {
|
|
1417
|
-
const fs = makeFrameState();
|
|
1418
|
-
warnMultiLightPoint(fs, 4);
|
|
1419
|
-
warnMultiLightPoint(fs, 0);
|
|
1420
|
-
expect(warnSpy).toHaveBeenCalledTimes(0);
|
|
1421
|
-
expect(fs.warnedMultiLightPoint).toBe(false);
|
|
1422
|
-
});
|
|
1423
|
-
|
|
1424
|
-
it('warn-once across 6 frames', () => {
|
|
1425
|
-
const fs = makeFrameState();
|
|
1426
|
-
warnMultiLightPoint(fs, 5);
|
|
1427
|
-
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
1428
|
-
for (let i = 0; i < 5; i++) {
|
|
1429
|
-
warnMultiLightPoint(fs, 5);
|
|
1430
|
-
}
|
|
1431
|
-
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
1432
|
-
});
|
|
1433
|
-
|
|
1434
|
-
it('production gate suppresses output but flips latch', () => {
|
|
1435
|
-
const fs = makeFrameState();
|
|
1436
|
-
warnMultiLightPoint(fs, 5, { env: { NODE_ENV: 'production' } });
|
|
1437
|
-
expect(warnSpy).toHaveBeenCalledTimes(0);
|
|
1438
|
-
expect(fs.warnedMultiLightPoint).toBe(true);
|
|
1439
|
-
});
|
|
1440
|
-
});
|
|
1441
|
-
|
|
1442
|
-
describe('warnMultiLightSpot (M3 / AC-04 / AC-05 (c))', () => {
|
|
1443
|
-
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
1444
|
-
|
|
1445
|
-
beforeEach(() => {
|
|
1446
|
-
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1447
|
-
});
|
|
1448
|
-
|
|
1449
|
-
afterEach(() => {
|
|
1450
|
-
warnSpy.mockRestore();
|
|
1451
|
-
});
|
|
1452
|
-
|
|
1453
|
-
it('first hit fires console.warn (N>4)', () => {
|
|
1454
|
-
const fs = makeFrameState();
|
|
1455
|
-
warnMultiLightSpot(fs, 5);
|
|
1456
|
-
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
1457
|
-
expect(fs.warnedMultiLightSpot).toBe(true);
|
|
1458
|
-
const call = warnSpy.mock.calls[0];
|
|
1459
|
-
expect(call[1]).toMatchObject({
|
|
1460
|
-
code: 'render-system-multi-light',
|
|
1461
|
-
expected: 'at most 4 spot',
|
|
1462
|
-
detail: { type: 'spot', got: 5 },
|
|
1463
|
-
});
|
|
1464
|
-
});
|
|
1465
|
-
|
|
1466
|
-
it('no-op when spotCount <= 4', () => {
|
|
1467
|
-
const fs = makeFrameState();
|
|
1468
|
-
warnMultiLightSpot(fs, 4);
|
|
1469
|
-
warnMultiLightSpot(fs, 0);
|
|
1470
|
-
expect(warnSpy).toHaveBeenCalledTimes(0);
|
|
1471
|
-
expect(fs.warnedMultiLightSpot).toBe(false);
|
|
1472
|
-
});
|
|
1473
|
-
|
|
1474
|
-
it('warn-once across 6 frames', () => {
|
|
1475
|
-
const fs = makeFrameState();
|
|
1476
|
-
warnMultiLightSpot(fs, 5);
|
|
1477
|
-
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
1478
|
-
for (let i = 0; i < 5; i++) {
|
|
1479
|
-
warnMultiLightSpot(fs, 5);
|
|
1480
|
-
}
|
|
1481
|
-
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
1482
|
-
});
|
|
1483
|
-
|
|
1484
|
-
it('production gate suppresses output but flips latch', () => {
|
|
1485
|
-
const fs = makeFrameState();
|
|
1486
|
-
warnMultiLightSpot(fs, 5, { env: { NODE_ENV: 'production' } });
|
|
1487
|
-
expect(warnSpy).toHaveBeenCalledTimes(0);
|
|
1488
|
-
expect(fs.warnedMultiLightSpot).toBe(true);
|
|
1489
|
-
});
|
|
1490
|
-
});
|
|
1491
|
-
|
|
1492
|
-
describe('independent latch isolation (M3 / AC-04 / user Step 5.a)', () => {
|
|
1493
|
-
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
1494
|
-
|
|
1495
|
-
beforeEach(() => {
|
|
1496
|
-
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1497
|
-
});
|
|
1498
|
-
|
|
1499
|
-
afterEach(() => {
|
|
1500
|
-
warnSpy.mockRestore();
|
|
1501
|
-
});
|
|
1502
|
-
|
|
1503
|
-
it('firing directional does not silence point', () => {
|
|
1504
|
-
const fs = makeFrameState();
|
|
1505
|
-
warnMultiLightDirectional(fs, 2);
|
|
1506
|
-
expect(fs.warnedMultiLightDirectional).toBe(true);
|
|
1507
|
-
expect(fs.warnedMultiLightPoint).toBe(false);
|
|
1508
|
-
expect(fs.warnedMultiLightSpot).toBe(false);
|
|
1509
|
-
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
1510
|
-
warnMultiLightPoint(fs, 5);
|
|
1511
|
-
expect(warnSpy).toHaveBeenCalledTimes(2);
|
|
1512
|
-
expect(fs.warnedMultiLightPoint).toBe(true);
|
|
1513
|
-
});
|
|
1514
|
-
|
|
1515
|
-
it('firing point does not silence spot', () => {
|
|
1516
|
-
const fs = makeFrameState();
|
|
1517
|
-
warnMultiLightPoint(fs, 5);
|
|
1518
|
-
expect(fs.warnedMultiLightPoint).toBe(true);
|
|
1519
|
-
expect(fs.warnedMultiLightSpot).toBe(false);
|
|
1520
|
-
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
1521
|
-
warnMultiLightSpot(fs, 5);
|
|
1522
|
-
expect(warnSpy).toHaveBeenCalledTimes(2);
|
|
1523
|
-
expect(fs.warnedMultiLightSpot).toBe(true);
|
|
1524
|
-
});
|
|
1525
|
-
|
|
1526
|
-
it('pre-set directional latch does not prevent point/spot first fire', () => {
|
|
1527
|
-
const fs = makeFrameState();
|
|
1528
|
-
fs.warnedMultiLightDirectional = true;
|
|
1529
|
-
warnMultiLightPoint(fs, 5);
|
|
1530
|
-
warnMultiLightSpot(fs, 5);
|
|
1531
|
-
expect(warnSpy).toHaveBeenCalledTimes(2);
|
|
1532
|
-
expect(fs.warnedMultiLightPoint).toBe(true);
|
|
1533
|
-
expect(fs.warnedMultiLightSpot).toBe(true);
|
|
1534
|
-
});
|
|
1535
|
-
});
|
|
1536
1386
|
}
|
|
1537
1387
|
|
|
1538
1388
|
{
|
|
@@ -1703,6 +1553,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
1703
1553
|
|
|
1704
1554
|
async function importComponents(): Promise<{
|
|
1705
1555
|
Transform: unknown;
|
|
1556
|
+
GlobalTransform: unknown;
|
|
1706
1557
|
MeshFilter: unknown;
|
|
1707
1558
|
MeshRenderer: unknown;
|
|
1708
1559
|
Camera: unknown;
|
|
@@ -2770,8 +2621,10 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2770
2621
|
const result = buildPbrPipelineLayouts(device as any, STORAGE_CAPS);
|
|
2771
2622
|
expect(result.bindGroupLayouts).toHaveLength(4);
|
|
2772
2623
|
// And the captured pipeline layout descriptor matches.
|
|
2773
|
-
expect(device.capturedPipelineLayouts).toHaveLength(
|
|
2774
|
-
const layout = device.capturedPipelineLayouts
|
|
2624
|
+
expect(device.capturedPipelineLayouts).toHaveLength(2);
|
|
2625
|
+
const layout = device.capturedPipelineLayouts.find(
|
|
2626
|
+
(candidate) => candidate.label === 'pbr-pl',
|
|
2627
|
+
);
|
|
2775
2628
|
expect(layout?.bindGroupLayouts).toHaveLength(4);
|
|
2776
2629
|
expect(layout?.label).toBe('pbr-pl');
|
|
2777
2630
|
});
|
|
@@ -3195,6 +3048,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3195
3048
|
directionalCount: 0,
|
|
3196
3049
|
point: [],
|
|
3197
3050
|
spot: [],
|
|
3051
|
+
rect: [],
|
|
3198
3052
|
directionalShadowError: undefined,
|
|
3199
3053
|
lightSpaceMatrix: undefined,
|
|
3200
3054
|
shadowMapSize: undefined,
|
|
@@ -3479,8 +3333,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3479
3333
|
warnedZeroLightStandard: false,
|
|
3480
3334
|
warnedShadowDisabled: false,
|
|
3481
3335
|
warnedMultiLightDirectional: false,
|
|
3482
|
-
warnedMultiLightPoint: false,
|
|
3483
|
-
warnedMultiLightSpot: false,
|
|
3484
3336
|
warnedSkyboxTonemapNone: false,
|
|
3485
3337
|
warnedMissingBaseColorTextureHandles: new Set<number>(),
|
|
3486
3338
|
warnedNineSliceScaleEntities: new Set<number>(),
|
|
@@ -3599,8 +3451,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3599
3451
|
warnedZeroLightStandard: false,
|
|
3600
3452
|
warnedShadowDisabled: false,
|
|
3601
3453
|
warnedMultiLightDirectional: false,
|
|
3602
|
-
warnedMultiLightPoint: false,
|
|
3603
|
-
warnedMultiLightSpot: false,
|
|
3604
3454
|
warnedSkyboxTonemapNone: false,
|
|
3605
3455
|
warnedMissingBaseColorTextureHandles: new Set<number>(),
|
|
3606
3456
|
warnedNineSliceScaleEntities: new Set<number>(),
|
|
@@ -3674,8 +3524,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3674
3524
|
warnedZeroLightStandard: false,
|
|
3675
3525
|
warnedShadowDisabled: false,
|
|
3676
3526
|
warnedMultiLightDirectional: false,
|
|
3677
|
-
warnedMultiLightPoint: false,
|
|
3678
|
-
warnedMultiLightSpot: false,
|
|
3679
3527
|
warnedSkyboxTonemapNone: false,
|
|
3680
3528
|
warnedMissingBaseColorTextureHandles: new Set<number>(),
|
|
3681
3529
|
warnedNineSliceScaleEntities: new Set<number>(),
|
|
@@ -5148,7 +4996,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
5148
4996
|
}
|
|
5149
4997
|
|
|
5150
4998
|
function readWorld(world: World, entity: EntityHandle): Float32Array {
|
|
5151
|
-
return world.get(entity,
|
|
4999
|
+
return world.get(entity, GlobalTransform).unwrap().world as Float32Array;
|
|
5152
5000
|
}
|
|
5153
5001
|
|
|
5154
5002
|
function expectMatClose(actual: Float32Array, expected: Float32Array): void {
|
|
@@ -5157,7 +5005,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
5157
5005
|
}
|
|
5158
5006
|
}
|
|
5159
5007
|
|
|
5160
|
-
describe('w7 - propagate writes
|
|
5008
|
+
describe('w7 - propagate writes GlobalTransform.world mat4 (AC-05 / AC-06)', () => {
|
|
5161
5009
|
it('root: world == compose(local.TRS) element-wise epsilon<=1e-5 (AC-06)', () => {
|
|
5162
5010
|
const world = new World();
|
|
5163
5011
|
const local = trs({ pos: [3, 4, 5], scale: [2, 2, 2] });
|
|
@@ -5550,17 +5398,17 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
5550
5398
|
});
|
|
5551
5399
|
|
|
5552
5400
|
// feat-20260601 w12/w13: skin joint world matrices now flow from the resolved
|
|
5553
|
-
// `
|
|
5401
|
+
// `GlobalTransform.world` column array view (a raw 16-float `Float32Array`, written
|
|
5554
5402
|
// by propagateTransforms) straight into the allocator's `readonly Mat4[]`
|
|
5555
5403
|
// jointWorlds parameter -- zero recompose from decomposed TRS. The allocator
|
|
5556
|
-
// contract is unchanged; this guards that a bare
|
|
5404
|
+
// contract is unchanged; this guards that a bare GlobalTransform.world-shaped
|
|
5557
5405
|
// Float32Array premultiplies against the IBM exactly like a `mat4.create()`.
|
|
5558
|
-
it('accepts a
|
|
5406
|
+
it('accepts a GlobalTransform.world-shaped Float32Array as a joint world matrix', () => {
|
|
5559
5407
|
const { device, written } = mockDevice();
|
|
5560
5408
|
const alloc = createSkinPaletteAllocator(device, MAX_BINDING);
|
|
5561
|
-
// A
|
|
5409
|
+
// A GlobalTransform.world view is a plain 16-float column-major Float32Array.
|
|
5562
5410
|
// Here: translation (0, 3, 0) -- the shape a `world._getArrayView(joint,
|
|
5563
|
-
//
|
|
5411
|
+
// GlobalTransform, 'world')` read returns after propagate.
|
|
5564
5412
|
const jointWorldView = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 3, 0, 1]);
|
|
5565
5413
|
const ibm = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
5566
5414
|
const slice = alloc.allocateSlice(1);
|
|
@@ -7381,6 +7229,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
7381
7229
|
|
|
7382
7230
|
async function importComponents(): Promise<{
|
|
7383
7231
|
Transform: unknown;
|
|
7232
|
+
GlobalTransform: unknown;
|
|
7384
7233
|
MeshFilter: unknown;
|
|
7385
7234
|
MeshRenderer: unknown;
|
|
7386
7235
|
Camera: unknown;
|
|
@@ -7698,6 +7547,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
7698
7547
|
|
|
7699
7548
|
async function importComponents(): Promise<{
|
|
7700
7549
|
Transform: unknown;
|
|
7550
|
+
GlobalTransform: unknown;
|
|
7701
7551
|
MeshFilter: unknown;
|
|
7702
7552
|
MeshRenderer: unknown;
|
|
7703
7553
|
Camera: unknown;
|
|
@@ -8777,6 +8627,7 @@ type ExtractFramesWithPipeline = (
|
|
|
8777
8627
|
|
|
8778
8628
|
interface IntegrationComponents {
|
|
8779
8629
|
Transform: unknown;
|
|
8630
|
+
GlobalTransform: unknown;
|
|
8780
8631
|
MeshFilter: unknown;
|
|
8781
8632
|
MeshRenderer: unknown;
|
|
8782
8633
|
Camera: unknown;
|