@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
|
@@ -147,16 +147,14 @@ describe('w7 — getActiveCamera / setActiveCamera', () => {
|
|
|
147
147
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
148
148
|
|
|
149
149
|
describe('w7 — extractFrame ActiveCamera selection', () => {
|
|
150
|
-
it('(2) no ActiveCamera + multiple cameras ->
|
|
150
|
+
it('(2) no ActiveCamera + multiple display cameras -> first display camera surfaces', () => {
|
|
151
151
|
const world = new World();
|
|
152
152
|
const assets = makeAssets();
|
|
153
153
|
spawnCamera(world, 1.0);
|
|
154
154
|
spawnCamera(world, 2.0);
|
|
155
155
|
const frame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
156
|
-
// No selection ->
|
|
157
|
-
|
|
158
|
-
// does NOT prune when there is no ActiveCamera.
|
|
159
|
-
expect(frame.cameras.length).toBe(2);
|
|
156
|
+
// No selection -> the first display camera remains the display authority.
|
|
157
|
+
expect(frame.cameras.length).toBe(1);
|
|
160
158
|
});
|
|
161
159
|
|
|
162
160
|
it('(1) ActiveCamera pointing at 2nd camera -> only that camera surfaces', () => {
|
|
@@ -171,15 +169,15 @@ describe('w7 — extractFrame ActiveCamera selection', () => {
|
|
|
171
169
|
expect(frame.cameras[0]?.fov).toBeCloseTo(2.0, 5);
|
|
172
170
|
});
|
|
173
171
|
|
|
174
|
-
it('(3) ActiveCamera pointing at non-existent entity -> fallback to first
|
|
172
|
+
it('(3) ActiveCamera pointing at non-existent entity -> fallback to first display camera', () => {
|
|
175
173
|
const world = new World();
|
|
176
174
|
const assets = makeAssets();
|
|
177
175
|
spawnCamera(world, 1.0);
|
|
178
176
|
spawnCamera(world, 2.0);
|
|
179
177
|
setActiveCamera(world, 999999);
|
|
180
178
|
const frame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
181
|
-
// Unresolvable -> behave as if no ActiveCamera
|
|
182
|
-
expect(frame.cameras.length).toBe(
|
|
179
|
+
// Unresolvable -> behave as if no ActiveCamera.
|
|
180
|
+
expect(frame.cameras.length).toBe(1);
|
|
183
181
|
});
|
|
184
182
|
|
|
185
183
|
it('(4) single camera + ActiveCamera pointing at it -> behavior unchanged (one camera)', () => {
|
|
@@ -68,6 +68,8 @@ describe('AnimationGraph POD in the Asset union (M2 / w12)', () => {
|
|
|
68
68
|
return 'video';
|
|
69
69
|
case 'particle-effect':
|
|
70
70
|
return 'particle-effect';
|
|
71
|
+
case 'ies-profile':
|
|
72
|
+
return 'ies-profile';
|
|
71
73
|
default: {
|
|
72
74
|
const _exhaustive: never = a;
|
|
73
75
|
return _exhaustive;
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// AC-05: storedNameOf retired; per-GUID stored name lives on the envelope and
|
|
4
4
|
// feeds resolveName's `storedName` argument. resolveName's three-arg
|
|
5
|
-
//
|
|
6
|
-
// no-package '' fallback /
|
|
5
|
+
// display-name derivation (stored-name precedence / package basename
|
|
6
|
+
// fallback / no-package '' fallback / promotion freeze) must remain
|
|
7
|
+
// coherent.
|
|
7
8
|
// AC-06: the 14-arm assetBrand switch retired for the ASSET_BRAND Record
|
|
8
9
|
// table. The table maps every Asset.kind to its brand, and the two
|
|
9
10
|
// consumption points (instantiate -> allocSharedRef brand arg;
|
|
@@ -19,7 +20,7 @@ function makeReg(): AssetRegistry {
|
|
|
19
20
|
|
|
20
21
|
const sampler = { kind: 'sampler' as const };
|
|
21
22
|
|
|
22
|
-
// ── AC-05: resolveName
|
|
23
|
+
// ── AC-05: resolveName remains coherent after storedNameOf retirement ──
|
|
23
24
|
|
|
24
25
|
describe('AC-05 resolveName regression (storedNameOf retired, name on envelope)', () => {
|
|
25
26
|
it('single-asset package -> basename(path), no stored name needed', () => {
|
|
@@ -1166,7 +1166,7 @@ const LEAF_FIXTURES: readonly LeafFixture[] = [
|
|
|
1166
1166
|
kind: 'render-pipeline',
|
|
1167
1167
|
makeAsset: () => ({
|
|
1168
1168
|
kind: 'render-pipeline' as const,
|
|
1169
|
-
pipelineId: 'forgeax::
|
|
1169
|
+
pipelineId: 'forgeax::standard' as const,
|
|
1170
1170
|
}),
|
|
1171
1171
|
},
|
|
1172
1172
|
];
|
|
@@ -784,6 +784,7 @@ function _makeStubGPU(): unknown {
|
|
|
784
784
|
'ui',
|
|
785
785
|
'audio',
|
|
786
786
|
'particle-effect',
|
|
787
|
+
'ies-profile',
|
|
787
788
|
] as const;
|
|
788
789
|
|
|
789
790
|
const UNREGISTERED_STUBS = ['shader'] as const;
|
|
@@ -795,13 +796,15 @@ function _makeStubGPU(): unknown {
|
|
|
795
796
|
for (const kind of REGISTERED_KINDS) {
|
|
796
797
|
expect(reg.get(kind), `expected loader for kind '${kind}'`).toBeDefined();
|
|
797
798
|
}
|
|
798
|
-
expect(reg.registeredKinds()).toHaveLength(
|
|
799
|
+
expect(reg.registeredKinds()).toHaveLength(18);
|
|
800
|
+
expect(reg.get('ies-profile'), 'ies-profile is wired internally').toBeDefined();
|
|
799
801
|
});
|
|
800
802
|
|
|
801
803
|
it('default set includes audio, video, and UI', () => {
|
|
802
804
|
const reg = new LoaderRegistry();
|
|
803
805
|
wireDefaultLoaders(reg);
|
|
804
|
-
expect(reg.registeredKinds()).toHaveLength(
|
|
806
|
+
expect(reg.registeredKinds()).toHaveLength(18);
|
|
807
|
+
expect(reg.get('ies-profile'), 'ies-profile is wired internally').toBeDefined();
|
|
805
808
|
expect(reg.get('video'), 'video is wired internally (graphics-extras)').toBeDefined();
|
|
806
809
|
expect(reg.get('ui'), 'ui is wired internally (engine-ui)').toBeDefined();
|
|
807
810
|
expect(reg.get('audio'), 'audio is an ordinary default asset kind').toBeDefined();
|
|
@@ -61,8 +61,8 @@ const SUT_ATTRIBUTABLE_RENDER_CODES: ReadonlySet<string> = new Set([
|
|
|
61
61
|
'vertex-storage-buffer-unavailable',
|
|
62
62
|
'mesh-ssbo-capacity-exceeded',
|
|
63
63
|
'mesh-ssbo-ceiling-reached',
|
|
64
|
-
'
|
|
65
|
-
'
|
|
64
|
+
'standard-light-budget-exceeded',
|
|
65
|
+
'standard-cluster-index-overflow',
|
|
66
66
|
]);
|
|
67
67
|
|
|
68
68
|
// Suppress WebGPU teardown race: chromium fires unhandled OperationError
|
|
@@ -14,9 +14,15 @@ import * as SceneOwner from '@forgeax/engine-scene';
|
|
|
14
14
|
|
|
15
15
|
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
16
16
|
import type { EntityHandle } from '@forgeax/engine-ecs';
|
|
17
|
-
import { World } from '@forgeax/engine-ecs';
|
|
17
|
+
import { componentDefinition, World } from '@forgeax/engine-ecs';
|
|
18
18
|
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
ChildOf,
|
|
21
|
+
Children,
|
|
22
|
+
GlobalTransform,
|
|
23
|
+
propagateTransforms,
|
|
24
|
+
Transform,
|
|
25
|
+
} from '@forgeax/engine-scene';
|
|
20
26
|
import type { LocalEntityId, SceneAsset } from '@forgeax/engine-types';
|
|
21
27
|
import { describe, expect, it } from 'vitest';
|
|
22
28
|
import { rootsToSceneAsset } from '../collect-scene-asset';
|
|
@@ -26,7 +32,7 @@ import { registerSceneComponents } from './helpers/register-scene-components';
|
|
|
26
32
|
// Read the resolved world mat4 (column-major 16 floats) from the Transform
|
|
27
33
|
// world column array view.
|
|
28
34
|
function worldOf(world: World, entity: EntityHandle): Float32Array {
|
|
29
|
-
return world.get(entity,
|
|
35
|
+
return world.get(entity, GlobalTransform).unwrap().world;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
function makeRegistry(): AssetRegistry {
|
|
@@ -210,6 +216,7 @@ describe('w1 — AC-04 transient world roundtrip numeric equivalence', () => {
|
|
|
210
216
|
for (const ent of collected.value.entities) {
|
|
211
217
|
const t = (ent.components as Record<string, Record<string, unknown>>).Transform;
|
|
212
218
|
if (t !== undefined) expect('world' in t).toBe(false);
|
|
219
|
+
expect((ent.components as Record<string, unknown>).GlobalTransform).toBeUndefined();
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
// Round-trip into a fresh world.
|
|
@@ -266,4 +273,8 @@ describe('w1 — AC-04 transient world roundtrip numeric equivalence', () => {
|
|
|
266
273
|
expect(wDst[13] as number).toBeCloseTo(4, 5);
|
|
267
274
|
expect(wDst[14] as number).toBeCloseTo(5, 5);
|
|
268
275
|
});
|
|
276
|
+
|
|
277
|
+
it('declares GlobalTransform transient and reconstructs it from the authored pair', () => {
|
|
278
|
+
expect(componentDefinition(GlobalTransform).policy.transient).toBe(true);
|
|
279
|
+
});
|
|
269
280
|
});
|
|
@@ -16,18 +16,20 @@
|
|
|
16
16
|
|
|
17
17
|
import type { InputShapeOf, SchemaOf, ShapeOf } from '@forgeax/engine-ecs';
|
|
18
18
|
import type { Camera, DirectionalLight, MeshFilter, MeshRenderer } from '@forgeax/engine-render';
|
|
19
|
-
import type { ChildOf, Transform } from '@forgeax/engine-scene';
|
|
19
|
+
import type { ChildOf, GlobalTransform, Transform } from '@forgeax/engine-scene';
|
|
20
20
|
import type { Handle } from '@forgeax/engine-types';
|
|
21
21
|
import { describe, expectTypeOf, it } from 'vitest';
|
|
22
22
|
|
|
23
23
|
describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeOf', () => {
|
|
24
|
-
it('Transform data shape has 3 local inline-array fields
|
|
24
|
+
it('Transform data shape has 3 local inline-array fields', () => {
|
|
25
25
|
type Data = ShapeOf<SchemaOf<typeof Transform>>;
|
|
26
|
-
expectTypeOf<keyof Data>().toEqualTypeOf<'pos' | 'quat' | 'scale'
|
|
26
|
+
expectTypeOf<keyof Data>().toEqualTypeOf<'pos' | 'quat' | 'scale'>();
|
|
27
27
|
expectTypeOf<Data['pos']>().toEqualTypeOf<Float32Array>();
|
|
28
28
|
expectTypeOf<Data['quat']>().toEqualTypeOf<Float32Array>();
|
|
29
29
|
expectTypeOf<Data['scale']>().toEqualTypeOf<Float32Array>();
|
|
30
|
-
|
|
30
|
+
type GlobalData = ShapeOf<SchemaOf<typeof GlobalTransform>>;
|
|
31
|
+
expectTypeOf<keyof GlobalData>().toEqualTypeOf<'world'>();
|
|
32
|
+
expectTypeOf<GlobalData['world']>().toEqualTypeOf<Float32Array>();
|
|
31
33
|
// Write side widens `array<f32, N>` to also accept plain number[] literals
|
|
32
34
|
// (InputShapeOf asymmetry): `pos: [1, 2, 3]` spawns without a
|
|
33
35
|
// Float32Array wrapper at the call site.
|
|
@@ -67,7 +69,7 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
|
|
|
67
69
|
expectTypeOf<{ materials: readonly [0] }>().not.toMatchTypeOf<SpawnData>();
|
|
68
70
|
});
|
|
69
71
|
|
|
70
|
-
it('Camera data shape has
|
|
72
|
+
it('Camera data shape has 21 fields (18 number + clearColor array + autoAspect boolean + target)', () => {
|
|
71
73
|
type Data = ShapeOf<SchemaOf<typeof Camera>>;
|
|
72
74
|
expectTypeOf<keyof Data>().toEqualTypeOf<
|
|
73
75
|
| 'fov'
|
|
@@ -90,6 +92,7 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
|
|
|
90
92
|
| 'bloomBlurRadius'
|
|
91
93
|
| 'clearColor'
|
|
92
94
|
| 'autoAspect'
|
|
95
|
+
| 'target'
|
|
93
96
|
>();
|
|
94
97
|
expectTypeOf<Data['fov']>().toEqualTypeOf<number>();
|
|
95
98
|
expectTypeOf<Data['far']>().toEqualTypeOf<number>();
|
|
@@ -107,9 +110,10 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
|
|
|
107
110
|
expectTypeOf<Data['clearColor']>().toEqualTypeOf<Float32Array>();
|
|
108
111
|
// feat-20260617 / M3: AC-09 -- bool column narrows to boolean, not number.
|
|
109
112
|
expectTypeOf<Data['autoAspect']>().toEqualTypeOf<boolean>();
|
|
113
|
+
expectTypeOf<Data['target']>().toEqualTypeOf<Handle<'RenderTarget', 'shared'>>();
|
|
110
114
|
});
|
|
111
115
|
|
|
112
|
-
it('DirectionalLight data shape
|
|
116
|
+
it('DirectionalLight data shape includes closed shadow quality fields', () => {
|
|
113
117
|
// feat-20260621: DirectionalLightShadow merged into DirectionalLight via castShadow toggle.
|
|
114
118
|
// shadowDistance replaced the nearPlane/farPlane pair (near derives from camera).
|
|
115
119
|
// feat-20260709 M2: direction/color collapsed to array<f32,3> columns.
|
|
@@ -126,13 +130,18 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
|
|
|
126
130
|
| 'depthBias'
|
|
127
131
|
| 'normalBias'
|
|
128
132
|
| 'shadowDistance'
|
|
129
|
-
| '
|
|
133
|
+
| 'shadowFilter'
|
|
134
|
+
| 'shadowAngularRadius'
|
|
135
|
+
| 'maxPenumbraTexels'
|
|
130
136
|
>();
|
|
131
137
|
expectTypeOf<Data['direction']>().toEqualTypeOf<Float32Array>();
|
|
132
138
|
expectTypeOf<Data['color']>().toEqualTypeOf<Float32Array>();
|
|
133
139
|
expectTypeOf<Data['intensity']>().toEqualTypeOf<number>();
|
|
134
140
|
// bool column narrows to boolean, not number.
|
|
135
141
|
expectTypeOf<Data['castShadow']>().toEqualTypeOf<boolean>();
|
|
142
|
+
expectTypeOf<Data['shadowFilter']>().toEqualTypeOf<number>();
|
|
143
|
+
expectTypeOf<Data['shadowAngularRadius']>().toEqualTypeOf<number>();
|
|
144
|
+
expectTypeOf<Data['maxPenumbraTexels']>().toEqualTypeOf<number>();
|
|
136
145
|
});
|
|
137
146
|
});
|
|
138
147
|
|
|
@@ -183,12 +192,13 @@ describe('w7 type-level - component name literal types are preserved', () => {
|
|
|
183
192
|
// ────────────────────────────────────────────────────────────────────────────
|
|
184
193
|
|
|
185
194
|
describe('w15 AC-04 type constraint — ShapeOf derivation from field-payload', () => {
|
|
186
|
-
it(
|
|
195
|
+
it('ShapeOf<SchemaOf<typeof Transform>> yields local Float32Array views', () => {
|
|
187
196
|
type T = ShapeOf<SchemaOf<typeof Transform>>;
|
|
188
197
|
expectTypeOf<T['pos']>().toEqualTypeOf<Float32Array>();
|
|
189
198
|
expectTypeOf<T['quat']>().toEqualTypeOf<Float32Array>();
|
|
190
199
|
expectTypeOf<T['scale']>().toEqualTypeOf<Float32Array>();
|
|
191
|
-
|
|
200
|
+
type GlobalData = ShapeOf<SchemaOf<typeof GlobalTransform>>;
|
|
201
|
+
expectTypeOf<GlobalData['world']>().toEqualTypeOf<Float32Array>();
|
|
192
202
|
});
|
|
193
203
|
|
|
194
204
|
it("ShapeOf<SchemaOf<typeof Camera>> yields 21 number fields ('f32' -> number) + autoAspect boolean ('bool' -> boolean)", () => {
|
|
@@ -65,7 +65,13 @@ import {
|
|
|
65
65
|
Tilemap,
|
|
66
66
|
} from '@forgeax/engine-render/authoring';
|
|
67
67
|
import * as SceneOwner from '@forgeax/engine-scene';
|
|
68
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
ChildOf,
|
|
70
|
+
Children,
|
|
71
|
+
GlobalTransform,
|
|
72
|
+
propagateTransforms,
|
|
73
|
+
Transform,
|
|
74
|
+
} from '@forgeax/engine-scene';
|
|
69
75
|
import type { Handle, LocalEntityId, SceneAsset, SceneEntity } from '@forgeax/engine-types';
|
|
70
76
|
import { toShared } from '@forgeax/engine-types';
|
|
71
77
|
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
@@ -150,17 +156,17 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
150
156
|
// abstraction: components are flat-scalar SoA columns, not nested objects).
|
|
151
157
|
|
|
152
158
|
describe('w7 - 5 component schemas register through defineComponent', () => {
|
|
153
|
-
it('Transform has
|
|
159
|
+
it('Transform has the three authored local array fields', () => {
|
|
154
160
|
expect(Transform.name).toBe('Transform');
|
|
155
|
-
expect(Object.keys(componentSchema(Transform)).length).toBe(
|
|
161
|
+
expect(Object.keys(componentSchema(Transform)).length).toBe(3);
|
|
156
162
|
// pos: inline [x, y, z]
|
|
157
163
|
expect(componentSchema(Transform).pos).toBe('array<f32, 3>');
|
|
158
164
|
// quat: inline [x, y, z, w] quaternion
|
|
159
165
|
expect(componentSchema(Transform).quat).toBe('array<f32, 4>');
|
|
160
166
|
// scale: inline [x, y, z]
|
|
161
167
|
expect(componentSchema(Transform).scale).toBe('array<f32, 3>');
|
|
162
|
-
|
|
163
|
-
expect(componentSchema(
|
|
168
|
+
expect(GlobalTransform.name).toBe('GlobalTransform');
|
|
169
|
+
expect(componentSchema(GlobalTransform).world).toBe('array<f32, 16>');
|
|
164
170
|
});
|
|
165
171
|
|
|
166
172
|
it('MeshFilter has 1 shared<MeshAsset> field (assetHandle; M5 / w14)', () => {
|
|
@@ -176,9 +182,9 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
176
182
|
expect(schemaRecord.materials).toBe('array<shared<MaterialAsset>>');
|
|
177
183
|
});
|
|
178
184
|
|
|
179
|
-
it('Camera has
|
|
185
|
+
it('Camera has 21 fields (17 f32 + historyVersion u32 + clearColor array<f32,4> + autoAspect bool + target)', () => {
|
|
180
186
|
expect(Camera.name).toBe('Camera');
|
|
181
|
-
expect(Object.keys(componentSchema(Camera)).length).toBe(
|
|
187
|
+
expect(Object.keys(componentSchema(Camera)).length).toBe(21);
|
|
182
188
|
expect(componentSchema(Camera).fov).toBe('f32');
|
|
183
189
|
expect(componentSchema(Camera).aspect).toBe('f32');
|
|
184
190
|
expect(componentSchema(Camera).near).toBe('f32');
|
|
@@ -211,13 +217,13 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
211
217
|
expect(componentSchema(Camera).autoAspect).toBe('bool');
|
|
212
218
|
});
|
|
213
219
|
|
|
214
|
-
it('DirectionalLight has
|
|
220
|
+
it('DirectionalLight has 14 fields: 3 light + castShadow bool + closed shadow quality fields', () => {
|
|
215
221
|
// feat-20260621: DirectionalLightShadow merged into DirectionalLight via castShadow toggle.
|
|
216
222
|
// shadowDistance replaced the nearPlane/farPlane pair (near derives from camera).
|
|
217
223
|
// feat-20260709 M2: direction/color collapsed from 6 per-axis scalars to
|
|
218
224
|
// two array<f32,3> columns (3 light fields: direction + color + intensity).
|
|
219
225
|
expect(DirectionalLight.name).toBe('DirectionalLight');
|
|
220
|
-
expect(Object.keys(componentSchema(DirectionalLight)).length).toBe(
|
|
226
|
+
expect(Object.keys(componentSchema(DirectionalLight)).length).toBe(14);
|
|
221
227
|
// 3 light fields
|
|
222
228
|
expect(componentSchema(DirectionalLight).direction).toBe('array<f32, 3>');
|
|
223
229
|
expect(componentSchema(DirectionalLight).color).toBe('array<f32, 3>');
|
|
@@ -231,7 +237,10 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
231
237
|
expect(componentSchema(DirectionalLight).depthBias).toBe('f32');
|
|
232
238
|
expect(componentSchema(DirectionalLight).normalBias).toBe('f32');
|
|
233
239
|
expect(componentSchema(DirectionalLight).shadowDistance).toBe('f32');
|
|
234
|
-
expect(componentSchema(DirectionalLight).
|
|
240
|
+
expect(componentSchema(DirectionalLight).shadowFilter).toBe('enum');
|
|
241
|
+
expect(componentSchema(DirectionalLight).shadowAngularRadius).toBe('f32');
|
|
242
|
+
expect(componentSchema(DirectionalLight).maxPenumbraTexels).toBe('f32');
|
|
243
|
+
expect('pcfKernelSize' in componentSchema(DirectionalLight)).toBe(false);
|
|
235
244
|
});
|
|
236
245
|
|
|
237
246
|
it('all 5 components are frozen tokens with owner-assigned identities', () => {
|
|
@@ -411,9 +420,10 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
411
420
|
// file keeps an integration-shape spawn smoke for the
|
|
412
421
|
// archetype-with-Children path.
|
|
413
422
|
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
423
|
+
// The resolved world transform is the `GlobalTransform.world` mat4 column;
|
|
424
|
+
// Transform's generic ECS requirement materializes that carrier on normal
|
|
425
|
+
// spawn/add paths. Propagation + hierarchy compose coverage lives in
|
|
426
|
+
// render-system-extract.test.ts (GlobalTransform.world parent x child).
|
|
417
427
|
//
|
|
418
428
|
// charter mapping: proposition 2 (Bevy ChildOf/Children industry analog) +
|
|
419
429
|
// proposition 3 (machine-readable schema > prose: grep componentSchema(ChildOf) /
|
|
@@ -432,14 +442,10 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
432
442
|
expect((componentSchema(Children) as Record<string, unknown>).entities).toBe('array<entity>');
|
|
433
443
|
});
|
|
434
444
|
|
|
435
|
-
it('
|
|
436
|
-
|
|
437
|
-
// separate GlobalTransform component. The 3 local-TRS inline array
|
|
438
|
-
// columns (pos/quat/scale) plus the `world: array<f32, 16>` resolved
|
|
439
|
-
// mat4 are the full shape.
|
|
440
|
-
const tKeys = Object.keys(componentSchema(Transform));
|
|
445
|
+
it('GlobalTransform carries the resolved world mat4 column', () => {
|
|
446
|
+
const tKeys = Object.keys(componentSchema(GlobalTransform));
|
|
441
447
|
expect(tKeys).toContain('world');
|
|
442
|
-
expect(componentSchema(
|
|
448
|
+
expect(componentSchema(GlobalTransform).world).toBe('array<f32, 16>');
|
|
443
449
|
expect(componentSchema(Transform).pos).toBe('array<f32, 3>');
|
|
444
450
|
expect(componentSchema(Transform).quat).toBe('array<f32, 4>');
|
|
445
451
|
expect(componentSchema(Transform).scale).toBe('array<f32, 3>');
|
|
@@ -602,7 +608,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
602
608
|
//
|
|
603
609
|
// Two orthogonal guarantees after ChildOf gained its `relationship` block:
|
|
604
610
|
// AC-25 reader path unchanged -- propagateTransforms still walks ChildOf.parent
|
|
605
|
-
// upward and composes child.
|
|
611
|
+
// upward and composes child.GlobalTransform.world = parent.world x
|
|
606
612
|
// compose(child local). The migration must NOT alter this output.
|
|
607
613
|
// AC-25 mirror auto-maintained -- spawning / removing / reparenting ChildOf
|
|
608
614
|
// now auto-updates the parent's Children.entities list (the OOS-10 manual-
|
|
@@ -634,9 +640,9 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
634
640
|
const r = propagateTransforms(world);
|
|
635
641
|
expect(r.ok).toBe(true);
|
|
636
642
|
|
|
637
|
-
// feat-20260601: the resolved world transform lives on
|
|
643
|
+
// feat-20260601: the resolved world transform lives on GlobalTransform.world
|
|
638
644
|
// (a 16-float column-major mat4); the translation column is m[12,13,14].
|
|
639
|
-
const t = world.get(child,
|
|
645
|
+
const t = world.get(child, GlobalTransform);
|
|
640
646
|
expect(t.ok).toBe(true);
|
|
641
647
|
if (!t.ok) return;
|
|
642
648
|
const w = t.value.world;
|
|
@@ -1408,7 +1414,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1408
1414
|
// ────────────────────────────────────────────────────────────────────────────
|
|
1409
1415
|
|
|
1410
1416
|
describe('Camera.fields reflection (w14 AC-07 SSOT)', () => {
|
|
1411
|
-
it('Camera.fields has exactly
|
|
1417
|
+
it('Camera.fields has exactly 21 keys matching the Camera column set', () => {
|
|
1412
1418
|
const keys = Object.keys(Camera.fields).sort();
|
|
1413
1419
|
expect(keys).toEqual([
|
|
1414
1420
|
'antialias',
|
|
@@ -1428,6 +1434,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1428
1434
|
'near',
|
|
1429
1435
|
'projection',
|
|
1430
1436
|
'right',
|
|
1437
|
+
'target',
|
|
1431
1438
|
'tonemap',
|
|
1432
1439
|
'top',
|
|
1433
1440
|
'whitePoint',
|
|
@@ -1443,7 +1450,9 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1443
1450
|
? 'array<f32, 4>'
|
|
1444
1451
|
: key === 'historyVersion'
|
|
1445
1452
|
? 'u32'
|
|
1446
|
-
: '
|
|
1453
|
+
: key === 'target'
|
|
1454
|
+
? 'shared<RenderTarget>'
|
|
1455
|
+
: 'f32';
|
|
1447
1456
|
expect(Camera.fields[key].type).toBe(expected);
|
|
1448
1457
|
}
|
|
1449
1458
|
});
|
|
@@ -2409,6 +2418,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
2409
2418
|
const world = new World();
|
|
2410
2419
|
const e = world.spawn({ component: Transform, data: {} }).unwrap();
|
|
2411
2420
|
const row = world.get(e, Transform).unwrap();
|
|
2421
|
+
const global = world.get(e, GlobalTransform).unwrap();
|
|
2412
2422
|
|
|
2413
2423
|
// local pos/quat/scale inline array columns at identity.
|
|
2414
2424
|
expect(Array.from(row.pos)).toEqual([0, 0, 0]);
|
|
@@ -2417,7 +2427,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
2417
2427
|
|
|
2418
2428
|
// world array<f32,16> resolves to a Float32Array view of 16 contiguous
|
|
2419
2429
|
// floats. Default fill is the identity mat4 (column-major).
|
|
2420
|
-
const w =
|
|
2430
|
+
const w = global.world as Float32Array;
|
|
2421
2431
|
expect(w).toBeInstanceOf(Float32Array);
|
|
2422
2432
|
expect(w.length).toBe(16);
|
|
2423
2433
|
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
@@ -36,6 +36,30 @@ describe('runtime renderer host assembly', () => {
|
|
|
36
36
|
host.value.renderer.dispose();
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
it('repeats null-device assembly and frame observation without device loss', async () => {
|
|
40
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
41
|
+
const host = await constructRendererHost(canvas(), { rhi }, { shaderManifestUrl: manifest });
|
|
42
|
+
expect(host.ok).toBe(true);
|
|
43
|
+
if (!host.ok) continue;
|
|
44
|
+
const world = new World();
|
|
45
|
+
const attached = host.value.renderer.attach(world);
|
|
46
|
+
expect(attached.ok).toBe(true);
|
|
47
|
+
if (!attached.ok) {
|
|
48
|
+
host.value.renderer.dispose();
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
expect(world.update().ok).toBe(true);
|
|
52
|
+
const frame = host.value.renderer.draw(frameRequest(attached.value));
|
|
53
|
+
expect(frame.ok).toBe(true);
|
|
54
|
+
if (frame.ok) {
|
|
55
|
+
expect((await host.value.renderer.observe(frame.value, { include: ['timings'] })).ok).toBe(
|
|
56
|
+
true,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
host.value.renderer.dispose();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
39
63
|
it('keeps runtime construction errors structured', async () => {
|
|
40
64
|
const result = await createRenderer(canvas(), { rhi: undefined });
|
|
41
65
|
expect(result.ok).toBe(false);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import type { GpuPassTimingOptions, RendererOptions } from '@forgeax/engine-render';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
describe('runtime GPU pass timing forwarding', () => {
|
|
7
|
+
it('keeps one option object and projects the exact field to Render', () => {
|
|
8
|
+
const gpuPassTiming = { maxPassesPerFrame: 8 } satisfies GpuPassTimingOptions;
|
|
9
|
+
const options = { gpuPassTiming } satisfies RendererOptions;
|
|
10
|
+
expect(options.gpuPassTiming).toBe(gpuPassTiming);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('forwards without owning admission, sessions, handles, or CPU capture', () => {
|
|
14
|
+
const source = readFileSync(
|
|
15
|
+
fileURLToPath(new URL('../createRenderer.ts', import.meta.url)),
|
|
16
|
+
'utf8',
|
|
17
|
+
);
|
|
18
|
+
expect(source).toContain('gpuPassTiming: options.gpuPassTiming');
|
|
19
|
+
expect(source).not.toContain('createGpuPassTimingSession');
|
|
20
|
+
expect(source).not.toContain('ProfileCapture');
|
|
21
|
+
expect(source).not.toContain('passCatalog');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('keeps the no-option path free of a timing field', () => {
|
|
25
|
+
const source = readFileSync(
|
|
26
|
+
fileURLToPath(new URL('../createRenderer.ts', import.meta.url)),
|
|
27
|
+
'utf8',
|
|
28
|
+
);
|
|
29
|
+
expect(source).toContain('options === undefined');
|
|
30
|
+
expect(source).toContain('gpuPassTiming: options.gpuPassTiming');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -13,8 +13,74 @@
|
|
|
13
13
|
|
|
14
14
|
import { createRenderer, EngineEnvironmentError } from '@forgeax/engine-runtime';
|
|
15
15
|
import { describe, expect, it } from 'vitest';
|
|
16
|
+
import { projectDirectionalShadowInspection } from '../../../render/src/assembly/directional-shadow-inspection';
|
|
17
|
+
import { resolveDirectionalShadowBackendAdmission } from '../../../render/src/render-pipeline';
|
|
16
18
|
|
|
17
19
|
describe('createRenderer without navigator.gpu (browser-no-webgpu)', () => {
|
|
20
|
+
it('requires the shared backend admission truth table for WebGL2 fallback and capable failure', async () => {
|
|
21
|
+
const module = (await import('../../../render/src/render-pipeline')) as Record<string, unknown>;
|
|
22
|
+
const resolver = module.resolveDirectionalShadowBackendAdmission;
|
|
23
|
+
expect(typeof resolver).toBe('function');
|
|
24
|
+
const resolve = resolver as (input: {
|
|
25
|
+
backendKind: 'webgpu' | 'wgpu-webgl2';
|
|
26
|
+
requested: 'pcssMedium' | 'pcssHigh';
|
|
27
|
+
candidate: 'accepted' | 'failed';
|
|
28
|
+
lastKnownGood?: 'pcf3' | 'pcf5';
|
|
29
|
+
}) => {
|
|
30
|
+
effective: string;
|
|
31
|
+
status: string;
|
|
32
|
+
fallbackReason?: string;
|
|
33
|
+
};
|
|
34
|
+
expect(
|
|
35
|
+
resolve({ backendKind: 'wgpu-webgl2', requested: 'pcssMedium', candidate: 'accepted' }),
|
|
36
|
+
).toMatchObject({
|
|
37
|
+
effective: 'pcf3',
|
|
38
|
+
status: 'fallback',
|
|
39
|
+
fallbackReason: 'webgl2-unsupported',
|
|
40
|
+
});
|
|
41
|
+
expect(
|
|
42
|
+
resolve({
|
|
43
|
+
backendKind: 'webgpu',
|
|
44
|
+
requested: 'pcssHigh',
|
|
45
|
+
candidate: 'failed',
|
|
46
|
+
lastKnownGood: 'pcf3',
|
|
47
|
+
}),
|
|
48
|
+
).toMatchObject({ effective: 'pcf3', status: 'rejected', fallbackReason: 'candidate-failed' });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('keeps capable candidate failure explicit while retaining the LKG inspection facts', () => {
|
|
52
|
+
const inspection = projectDirectionalShadowInspection({
|
|
53
|
+
admission: resolveDirectionalShadowBackendAdmission({
|
|
54
|
+
backendKind: 'webgpu',
|
|
55
|
+
requested: 'pcssHigh',
|
|
56
|
+
candidate: 'failed',
|
|
57
|
+
lastKnownGood: 'pcf3',
|
|
58
|
+
}),
|
|
59
|
+
cascadeCount: 4,
|
|
60
|
+
mapSize: 2048,
|
|
61
|
+
atlasBytes: 67_108_864,
|
|
62
|
+
writerPasses: 4,
|
|
63
|
+
blockerTaps: 0,
|
|
64
|
+
filterTapUpperBound: 9,
|
|
65
|
+
seamTapUpperBound: 9,
|
|
66
|
+
deviceGeneration: 2,
|
|
67
|
+
graphGeneration: 6,
|
|
68
|
+
error: {
|
|
69
|
+
code: 'shadow-candidate-failed',
|
|
70
|
+
expected: 'the capable backend candidate compiles and validates',
|
|
71
|
+
hint: 'fix the authoring input, then retry or recover',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
expect(inspection).toMatchObject({
|
|
75
|
+
effective: 'pcf3',
|
|
76
|
+
status: 'rejected',
|
|
77
|
+
fallbackReason: 'candidate-failed',
|
|
78
|
+
lastKnownGood: true,
|
|
79
|
+
graphGeneration: 6,
|
|
80
|
+
error: { hint: expect.stringContaining('retry') },
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
18
84
|
it('navigator.gpu is absent or non-functional in this browser environment', async () => {
|
|
19
85
|
const nav = globalThis.navigator as { gpu?: GPU };
|
|
20
86
|
if (nav.gpu === undefined) return;
|
|
@@ -62,6 +62,12 @@ const BUFFER_USAGE_MAP_READ = 0x0001;
|
|
|
62
62
|
const BUFFER_USAGE_COPY_DST = 0x0008;
|
|
63
63
|
const MAP_MODE_READ = 0x0001;
|
|
64
64
|
|
|
65
|
+
// Windows Dawn first-touch construction for the two renderer/readback probes
|
|
66
|
+
// can exceed Vitest's project-wide 30-second timeout while still completing
|
|
67
|
+
// well inside the nightly platform budget. Keep this owner-specific gate
|
|
68
|
+
// bounded so a valid cold-start probe is not retried as a false failure.
|
|
69
|
+
const SPRITE_NINESLICE_DAWN_TEST_TIMEOUT_MS = 120_000;
|
|
70
|
+
|
|
65
71
|
// Distinctive 4x4 texture: corners RED (cells (0,0)/(3,0)/(0,3)/(3,3)),
|
|
66
72
|
// remaining cells BLUE. With slices=[0.25,0.25,0.25,0.25] in atlas-uv space
|
|
67
73
|
// the 4 corners line up with the 4 corner texels -- making the central band
|
|
@@ -325,7 +331,9 @@ function midBandDistance(a: CapturedFrame, b: CapturedFrame): number {
|
|
|
325
331
|
}
|
|
326
332
|
|
|
327
333
|
describe('feat-20260527-sprite-nineslice w14 dawn smoke (stretch vs tile divergence)', () => {
|
|
328
|
-
it('stretch + tile sprite frames diverge in the mid-band after w15 (red until w15 lands)',
|
|
334
|
+
it('stretch + tile sprite frames diverge in the mid-band after w15 (red until w15 lands)', {
|
|
335
|
+
timeout: SPRITE_NINESLICE_DAWN_TEST_TIMEOUT_MS,
|
|
336
|
+
}, async () => {
|
|
329
337
|
// Asymmetric scale: x=4 vs y=1. With slices=[0.25,0.25,0.25,0.25] and the
|
|
330
338
|
// 4x4 corner-checker texture, the post-w15 9-slice path keeps corner
|
|
331
339
|
// zones at fixed world dimensions while the legacy pre-w15 path stretches
|
|
@@ -55,6 +55,17 @@ const MAP_MODE_READ = 0x0001;
|
|
|
55
55
|
const INSTANCE_COUNT = 5;
|
|
56
56
|
const SPACING = 2.5;
|
|
57
57
|
|
|
58
|
+
const SHADOW_FILTER = (() => {
|
|
59
|
+
const value = (
|
|
60
|
+
globalThis as unknown as { process?: { env?: Record<string, string | undefined> } }
|
|
61
|
+
).process?.env?.FORGEAX_SHADOW_FILTER;
|
|
62
|
+
if (value === 'pcssMedium') return 4;
|
|
63
|
+
if (value === 'pcssHigh') return 5;
|
|
64
|
+
if (value === 'pcf1') return 1;
|
|
65
|
+
if (value === 'pcf5') return 3;
|
|
66
|
+
return 2;
|
|
67
|
+
})();
|
|
68
|
+
|
|
58
69
|
function buildTranslationGrid(): Float32Array {
|
|
59
70
|
const out = new Float32Array(INSTANCE_COUNT * 16);
|
|
60
71
|
const half = ((INSTANCE_COUNT - 1) * SPACING) / 2;
|
|
@@ -263,7 +274,9 @@ describe('w10 -- shadow instanced dawn smoke (AC-05 behavioral)', () => {
|
|
|
263
274
|
depthBias: 0.005,
|
|
264
275
|
normalBias: 0.05,
|
|
265
276
|
shadowDistance: 50,
|
|
266
|
-
|
|
277
|
+
shadowFilter: SHADOW_FILTER,
|
|
278
|
+
shadowAngularRadius: 0.00465,
|
|
279
|
+
maxPenumbraTexels: 32,
|
|
267
280
|
},
|
|
268
281
|
});
|
|
269
282
|
|
|
@@ -285,7 +298,7 @@ describe('w10 -- shadow instanced dawn smoke (AC-05 behavioral)', () => {
|
|
|
285
298
|
{ component: Instances, data: { transforms: instanceTransforms } },
|
|
286
299
|
);
|
|
287
300
|
|
|
288
|
-
// Render 300 frames for temporal stability (PCF shadow, light accumulation)
|
|
301
|
+
// Render 300 frames for temporal stability (PCF or PCSS shadow, light accumulation)
|
|
289
302
|
const attachment = renderer.attach(world);
|
|
290
303
|
expect(attachment.ok).toBe(true);
|
|
291
304
|
if (!attachment.ok) throw attachment.error;
|