@flighthq/scene3d-wgpu 0.3.0 → 0.3.1-next.1041.35b950c
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/dist/customShaderWgpuMeshMaterialRenderer.d.ts.map +1 -1
- package/dist/customShaderWgpuMeshMaterialRenderer.js +6 -9
- package/dist/customShaderWgpuMeshMaterialRenderer.js.map +1 -1
- package/dist/drawWgpuScene3D.d.ts.map +1 -1
- package/dist/drawWgpuScene3D.js +4 -2
- package/dist/drawWgpuScene3D.js.map +1 -1
- package/dist/explainWgpuScene3DCoverage.d.ts +2 -2
- package/dist/explainWgpuScene3DCoverage.d.ts.map +1 -1
- package/dist/explainWgpuScene3DCoverage.js +62 -26
- package/dist/explainWgpuScene3DCoverage.js.map +1 -1
- package/dist/wgpuClassicPrelude.js +2 -2
- package/dist/wgpuClassicPrelude.js.map +1 -1
- package/dist/wgpuMeshMaterialRegistry.d.ts.map +1 -1
- package/dist/wgpuMeshMaterialRegistry.js +15 -11
- package/dist/wgpuMeshMaterialRegistry.js.map +1 -1
- package/dist/wgpuMeshPipeline.d.ts +1 -1
- package/dist/wgpuMeshPipeline.d.ts.map +1 -1
- package/dist/wgpuMeshPipeline.js +39 -12
- package/dist/wgpuMeshPipeline.js.map +1 -1
- package/dist/wgpuMeshUpload.d.ts +1 -1
- package/dist/wgpuMeshUpload.d.ts.map +1 -1
- package/dist/wgpuMeshUpload.js +37 -12
- package/dist/wgpuMeshUpload.js.map +1 -1
- package/dist/wgpuParticleEmitter3D.d.ts.map +1 -1
- package/dist/wgpuParticleEmitter3D.js +9 -6
- package/dist/wgpuParticleEmitter3D.js.map +1 -1
- package/dist/wgpuPbrPipelineCache.d.ts.map +1 -1
- package/dist/wgpuPbrPipelineCache.js +2 -2
- package/dist/wgpuPbrPipelineCache.js.map +1 -1
- package/dist/wgpuScene3DRuntime.d.ts.map +1 -1
- package/dist/wgpuScene3DRuntime.js +18 -10
- package/dist/wgpuScene3DRuntime.js.map +1 -1
- package/dist/wgpuScene3DTestHelper.d.ts.map +1 -1
- package/dist/wgpuScene3DTestHelper.js +126 -6
- package/dist/wgpuScene3DTestHelper.js.map +1 -1
- package/dist/wgpuShadedModifierSnippet.d.ts.map +1 -1
- package/dist/wgpuShadedModifierSnippet.js +8 -11
- package/dist/wgpuShadedModifierSnippet.js.map +1 -1
- package/dist/wgpuShadedPrelude.d.ts +5 -3
- package/dist/wgpuShadedPrelude.d.ts.map +1 -1
- package/dist/wgpuShadedPrelude.js +34 -13
- package/dist/wgpuShadedPrelude.js.map +1 -1
- package/dist/wgpuShadowMap.d.ts.map +1 -1
- package/dist/wgpuShadowMap.js +22 -6
- package/dist/wgpuShadowMap.js.map +1 -1
- package/dist/wgpuSkinPalette.d.ts +4 -1
- package/dist/wgpuSkinPalette.d.ts.map +1 -1
- package/dist/wgpuSkinPalette.js +227 -27
- package/dist/wgpuSkinPalette.js.map +1 -1
- package/dist/wgpuWireframeUpload.d.ts.map +1 -1
- package/dist/wgpuWireframeUpload.js +12 -6
- package/dist/wgpuWireframeUpload.js.map +1 -1
- package/package.json +20 -15
- package/src/customShaderWgpuMeshMaterialRenderer.test.ts +25 -1
- package/src/explainWgpuScene3DCoverage.test.ts +44 -10
- package/src/wgpuMeshMaterialRegistry.test.ts +22 -0
- package/src/wgpuMeshPipeline.test.ts +34 -1
- package/src/wgpuMeshUpload.test.ts +65 -3
- package/src/wgpuParticleEmitter3D.test.ts +23 -0
- package/src/wgpuScene3DRuntime.test.ts +8 -4
- package/src/wgpuScene3DTestHelper.test.ts +84 -0
- package/src/wgpuShadedModifierSnippet.test.ts +20 -6
- package/src/wgpuShadedPrelude.test.ts +6 -6
- package/src/wgpuShadowMap.test.ts +30 -0
- package/src/wgpuSkinPalette.test.ts +134 -19
- package/src/wgpuWireframeUpload.test.ts +29 -1
|
@@ -6,12 +6,14 @@ import { createTexture } from '@flighthq/texture/contract';
|
|
|
6
6
|
import type {
|
|
7
7
|
ImageResourceReference,
|
|
8
8
|
Scene3DKindUsage,
|
|
9
|
+
SceneCoverageCatalog,
|
|
9
10
|
SceneCoverageEntry,
|
|
10
11
|
WgpuRenderState,
|
|
11
12
|
} from '@flighthq/types/contract';
|
|
12
13
|
import {
|
|
13
14
|
ImageResourceReferenceKind,
|
|
14
15
|
RenderRegistry,
|
|
16
|
+
RequirementFacet,
|
|
15
17
|
ResourceResolutionState,
|
|
16
18
|
SceneCoverage,
|
|
17
19
|
StandardMaterialKind,
|
|
@@ -22,8 +24,27 @@ import { explainWgpuScene3DCoverage, hasWgpuScene3DCoverage } from './explainWgp
|
|
|
22
24
|
import { registerWgpuMeshMaterialRenderer } from './wgpuMeshMaterialRegistry';
|
|
23
25
|
import { makeWgpuScene3DState } from './wgpuScene3DTestHelper';
|
|
24
26
|
|
|
27
|
+
const coverageCatalog: SceneCoverageCatalog = [
|
|
28
|
+
{
|
|
29
|
+
kind: 'ShadedMaterial',
|
|
30
|
+
registrations: [{ module: '@flighthq/scene3d-wgpu', registrar: 'registerWgpuShadedMaterial' }],
|
|
31
|
+
registry: RenderRegistry.MaterialRenderer,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
kind: 'image',
|
|
35
|
+
registrations: [{ module: '@flighthq/scene3d-wgpu', registrar: 'registerWgpuImageTextureResolver' }],
|
|
36
|
+
registry: RenderRegistry.TextureResolver,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
kind: 'RimModifier',
|
|
40
|
+
registrations: [{ module: '@flighthq/scene3d-wgpu', registrar: 'registerWgpuRimModifier' }],
|
|
41
|
+
registry: RenderRegistry.ModifierSnippet,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
25
45
|
function shadedUsage(withModifier = false): Scene3DKindUsage {
|
|
26
46
|
const ref: ImageResourceReference = {
|
|
47
|
+
alphaType: 'straight',
|
|
27
48
|
bytes: new Uint8Array([1, 2, 3]),
|
|
28
49
|
failure: null,
|
|
29
50
|
kind: ImageResourceReferenceKind.Embedded,
|
|
@@ -42,23 +63,29 @@ function shadedUsage(withModifier = false): Scene3DKindUsage {
|
|
|
42
63
|
|
|
43
64
|
function entries(state: WgpuRenderState, withModifier = false): SceneCoverageEntry[] {
|
|
44
65
|
const out: SceneCoverageEntry[] = [];
|
|
45
|
-
explainWgpuScene3DCoverage(out, state, shadedUsage(withModifier));
|
|
66
|
+
explainWgpuScene3DCoverage(out, state, shadedUsage(withModifier), coverageCatalog);
|
|
46
67
|
return out;
|
|
47
68
|
}
|
|
48
69
|
|
|
49
70
|
const renderer = { bind() {}, draw() {} } as never;
|
|
50
71
|
|
|
51
72
|
describe('explainWgpuScene3DCoverage', () => {
|
|
52
|
-
it('reports a bare state
|
|
73
|
+
it('reports a bare state with remedies for both unregistered requirements', () => {
|
|
53
74
|
const found = entries(makeWgpuScene3DState().state);
|
|
54
75
|
expect(found).toContainEqual({
|
|
55
|
-
coverage: SceneCoverage.
|
|
76
|
+
coverage: SceneCoverage.Unregistered,
|
|
77
|
+
facet: RequirementFacet.SceneMaterialKind,
|
|
56
78
|
kind: 'ShadedMaterial',
|
|
79
|
+
module: '@flighthq/scene3d-wgpu',
|
|
80
|
+
registrar: 'registerWgpuShadedMaterial',
|
|
57
81
|
registry: RenderRegistry.MaterialRenderer,
|
|
58
82
|
});
|
|
59
83
|
expect(found).toContainEqual({
|
|
60
|
-
coverage: SceneCoverage.
|
|
84
|
+
coverage: SceneCoverage.Unregistered,
|
|
85
|
+
facet: RequirementFacet.SceneTextureSourceKind,
|
|
61
86
|
kind: 'image',
|
|
87
|
+
module: '@flighthq/scene3d-wgpu',
|
|
88
|
+
registrar: 'registerWgpuImageTextureResolver',
|
|
62
89
|
registry: RenderRegistry.TextureResolver,
|
|
63
90
|
});
|
|
64
91
|
});
|
|
@@ -67,8 +94,11 @@ describe('explainWgpuScene3DCoverage', () => {
|
|
|
67
94
|
const { state } = makeWgpuScene3DState();
|
|
68
95
|
registerWgpuMeshMaterialRenderer(state, StandardMaterialKind, renderer);
|
|
69
96
|
expect(entries(state)).toContainEqual({
|
|
70
|
-
coverage: SceneCoverage.
|
|
97
|
+
coverage: SceneCoverage.FallbackRemediable,
|
|
98
|
+
facet: RequirementFacet.SceneMaterialKind,
|
|
71
99
|
kind: 'ShadedMaterial',
|
|
100
|
+
module: '@flighthq/scene3d-wgpu',
|
|
101
|
+
registrar: 'registerWgpuShadedMaterial',
|
|
72
102
|
registry: RenderRegistry.MaterialRenderer,
|
|
73
103
|
});
|
|
74
104
|
});
|
|
@@ -78,17 +108,21 @@ describe('explainWgpuScene3DCoverage', () => {
|
|
|
78
108
|
registerWgpuMeshMaterialRenderer(state, 'ShadedMaterial', renderer);
|
|
79
109
|
expect(entries(state)).toContainEqual({
|
|
80
110
|
coverage: SceneCoverage.Satisfied,
|
|
111
|
+
facet: RequirementFacet.SceneMaterialKind,
|
|
81
112
|
kind: 'ShadedMaterial',
|
|
82
113
|
registry: RenderRegistry.MaterialRenderer,
|
|
83
114
|
});
|
|
84
115
|
});
|
|
85
116
|
|
|
86
|
-
it('reports an unregistered modifier snippet as
|
|
117
|
+
it('reports an unregistered modifier snippet as actionable, never a fallback', () => {
|
|
87
118
|
// The shaded compiler assembles base + modifiers into one program, so an absent snippet fails the
|
|
88
119
|
// whole material rather than degrading it.
|
|
89
120
|
expect(entries(makeWgpuScene3DState().state, true)).toContainEqual({
|
|
90
|
-
coverage: SceneCoverage.
|
|
121
|
+
coverage: SceneCoverage.Unregistered,
|
|
122
|
+
facet: RequirementFacet.SceneModifierKind,
|
|
91
123
|
kind: 'RimModifier',
|
|
124
|
+
module: '@flighthq/scene3d-wgpu',
|
|
125
|
+
registrar: 'registerWgpuRimModifier',
|
|
92
126
|
registry: RenderRegistry.ModifierSnippet,
|
|
93
127
|
});
|
|
94
128
|
});
|
|
@@ -101,9 +135,9 @@ describe('explainWgpuScene3DCoverage', () => {
|
|
|
101
135
|
const { state } = makeWgpuScene3DState();
|
|
102
136
|
const usage = shadedUsage();
|
|
103
137
|
const out: SceneCoverageEntry[] = [];
|
|
104
|
-
explainWgpuScene3DCoverage(out, state, usage);
|
|
138
|
+
explainWgpuScene3DCoverage(out, state, usage, coverageCatalog);
|
|
105
139
|
const first = out.length;
|
|
106
|
-
explainWgpuScene3DCoverage(out, state, usage);
|
|
140
|
+
explainWgpuScene3DCoverage(out, state, usage, coverageCatalog);
|
|
107
141
|
expect(out).toHaveLength(first);
|
|
108
142
|
});
|
|
109
143
|
});
|
|
@@ -124,7 +158,7 @@ describe('hasWgpuScene3DCoverage', () => {
|
|
|
124
158
|
registerWgpuMeshMaterialRenderer(state, 'ShadedMaterial', renderer);
|
|
125
159
|
const usage = shadedUsage();
|
|
126
160
|
const out: SceneCoverageEntry[] = [];
|
|
127
|
-
explainWgpuScene3DCoverage(out, state, usage);
|
|
161
|
+
explainWgpuScene3DCoverage(out, state, usage, coverageCatalog);
|
|
128
162
|
const gaps = out.filter((e) => e.coverage !== SceneCoverage.Satisfied);
|
|
129
163
|
expect(hasWgpuScene3DCoverage(state, usage)).toBe(gaps.length === 0);
|
|
130
164
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createStandardPbrMaterial } from '@flighthq/materials/contract';
|
|
2
|
+
import { getRegistryTableEntry } from '@flighthq/registry/contract';
|
|
3
|
+
import { copyWgpuRenderStateRegistrations, getWgpuRenderStateRuntime } from '@flighthq/render-wgpu/contract';
|
|
2
4
|
import type { WgpuMeshMaterialRenderer } from '@flighthq/types/contract';
|
|
3
5
|
import { StandardMaterialKind, StandardPbrMaterialKind } from '@flighthq/types/contract';
|
|
4
6
|
|
|
@@ -7,6 +9,7 @@ import {
|
|
|
7
9
|
registerWgpuMeshMaterialRenderer,
|
|
8
10
|
resolveWgpuMeshMaterialRenderer,
|
|
9
11
|
} from './wgpuMeshMaterialRegistry';
|
|
12
|
+
import { getWgpuScene3DRuntime } from './wgpuScene3DRuntime';
|
|
10
13
|
import { makeWgpuScene3DState } from './wgpuScene3DTestHelper';
|
|
11
14
|
|
|
12
15
|
function makeRenderer(): WgpuMeshMaterialRenderer {
|
|
@@ -30,6 +33,25 @@ describe('registerWgpuMeshMaterialRenderer', () => {
|
|
|
30
33
|
registerWgpuMeshMaterialRenderer(state, StandardPbrMaterialKind, renderer);
|
|
31
34
|
expect(getWgpuMeshMaterialRenderer(state, StandardPbrMaterialKind)).toBe(renderer);
|
|
32
35
|
});
|
|
36
|
+
|
|
37
|
+
it('replaces the persistent table while an explicitly copied state retains its snapshot', () => {
|
|
38
|
+
const { state: screen } = makeWgpuScene3DState();
|
|
39
|
+
const { state: derived } = makeWgpuScene3DState();
|
|
40
|
+
const renderer = makeRenderer();
|
|
41
|
+
const replacement = makeRenderer();
|
|
42
|
+
registerWgpuMeshMaterialRenderer(screen, StandardPbrMaterialKind, renderer);
|
|
43
|
+
const snapshot = getWgpuRenderStateRuntime(screen).registries.meshMaterialRenderers;
|
|
44
|
+
|
|
45
|
+
copyWgpuRenderStateRegistrations(derived, screen);
|
|
46
|
+
getWgpuScene3DRuntime(derived);
|
|
47
|
+
registerWgpuMeshMaterialRenderer(screen, StandardPbrMaterialKind, replacement);
|
|
48
|
+
|
|
49
|
+
expect(getWgpuRenderStateRuntime(derived).registries.meshMaterialRenderers).toBe(snapshot);
|
|
50
|
+
expect(getWgpuRenderStateRuntime(screen).registries.meshMaterialRenderers).not.toBe(snapshot);
|
|
51
|
+
expect(getRegistryTableEntry(snapshot, StandardPbrMaterialKind)).toBe(renderer);
|
|
52
|
+
expect(getWgpuMeshMaterialRenderer(derived, StandardPbrMaterialKind)).toBe(renderer);
|
|
53
|
+
expect(getWgpuMeshMaterialRenderer(screen, StandardPbrMaterialKind)).toBe(replacement);
|
|
54
|
+
});
|
|
33
55
|
});
|
|
34
56
|
|
|
35
57
|
describe('resolveWgpuMeshMaterialRenderer', () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createCamera3D } from '@flighthq/camera/contract';
|
|
2
2
|
import { createMatrix3, createMatrix4 } from '@flighthq/geometry/contract';
|
|
3
3
|
import { createStandardPbrMaterial } from '@flighthq/materials/contract';
|
|
4
|
-
import { createBoxMeshGeometry } from '@flighthq/mesh/contract';
|
|
4
|
+
import { createBoxMeshGeometry, createMeshGeometry } from '@flighthq/mesh/contract';
|
|
5
5
|
import { getWgpuRenderStateRuntime } from '@flighthq/render-wgpu/contract';
|
|
6
6
|
import { createTexture, setTextureUvOffset, setTextureUvScale } from '@flighthq/texture/contract';
|
|
7
7
|
import type {
|
|
@@ -277,6 +277,39 @@ describe('drawWgpuMeshSubset', () => {
|
|
|
277
277
|
expect(draw!.args[0]).toBe(proxy.subset.indexCount);
|
|
278
278
|
});
|
|
279
279
|
|
|
280
|
+
// The defect this covers: non-indexed geometry issued NO draw call at all, so a valid imported mesh
|
|
281
|
+
// rendered nothing and reported nothing. A backend that silently drops geometry is worse than one
|
|
282
|
+
// that draws it wrong, because nothing announces the loss.
|
|
283
|
+
it('issues a non-indexed draw for geometry without indices', () => {
|
|
284
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
285
|
+
ensureWgpuFrameBindGroup(state);
|
|
286
|
+
beginWgpuMeshDraw(state, makePipeline(state));
|
|
287
|
+
const geometry = createMeshGeometry({
|
|
288
|
+
indices: null,
|
|
289
|
+
layout: {
|
|
290
|
+
attributes: [{ byteOffset: 0, format: 'float32x3', semantic: 'position' }],
|
|
291
|
+
stride: 12,
|
|
292
|
+
},
|
|
293
|
+
vertices: new Float32Array(9),
|
|
294
|
+
});
|
|
295
|
+
const proxy: Scene3DRenderProxy = {
|
|
296
|
+
material: createStandardPbrMaterial(),
|
|
297
|
+
normalMatrix: createMatrix3(),
|
|
298
|
+
subset: geometry.subsets[0],
|
|
299
|
+
worldMatrix: createMatrix4(),
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
drawWgpuMeshSubset(state, proxy, geometry);
|
|
303
|
+
|
|
304
|
+
const draw = fake.calls.find((c) => c.name === 'draw');
|
|
305
|
+
expect(draw).toBeDefined();
|
|
306
|
+
expect(draw!.args[0]).toBe(proxy.subset.indexCount);
|
|
307
|
+
expect(draw!.args[2]).toBe(proxy.subset.indexOffset);
|
|
308
|
+
// It must not try to bind an index buffer it does not have.
|
|
309
|
+
expect(fake.calls.some((c) => c.name === 'setIndexBuffer')).toBe(false);
|
|
310
|
+
expect(fake.calls.some((c) => c.name === 'drawIndexed')).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
|
|
280
313
|
it('is a no-op when no pipeline is active', () => {
|
|
281
314
|
const { fake, state } = makeWgpuScene3DState();
|
|
282
315
|
drawWgpuMeshSubset(state, makeProxy(), createBoxMeshGeometry());
|
|
@@ -44,6 +44,28 @@ describe('ensureWgpuMeshUpload', () => {
|
|
|
44
44
|
expect(fake.calls.filter((c) => c.name === 'writeBuffer').length).toBeGreaterThanOrEqual(2);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
it('pads an odd-length Uint16 index upload to four-byte alignment without changing the index count', () => {
|
|
48
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
49
|
+
const geometry = createMeshGeometry({
|
|
50
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
51
|
+
layout: POSITION_LAYOUT,
|
|
52
|
+
vertices: new Float32Array(9),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const upload = ensureWgpuMeshUpload(state, geometry);
|
|
56
|
+
const writes = fake.calls.filter((call) => call.name === 'writeBuffer');
|
|
57
|
+
const indexWrite = writes[writes.length - 1]!;
|
|
58
|
+
const uploadedIndices = new Uint16Array(
|
|
59
|
+
indexWrite.args[2] as ArrayBuffer,
|
|
60
|
+
indexWrite.args[3] as number,
|
|
61
|
+
(indexWrite.args[4] as number) / Uint16Array.BYTES_PER_ELEMENT,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(indexWrite.args[4]).toBe(8);
|
|
65
|
+
expect(Array.from(uploadedIndices.subarray(0, 3))).toEqual([0, 1, 2]);
|
|
66
|
+
expect(upload.indexCount).toBe(3);
|
|
67
|
+
});
|
|
68
|
+
|
|
47
69
|
it('returns the cached upload without re-uploading when version is unchanged', () => {
|
|
48
70
|
const { fake, state } = makeWgpuScene3DState();
|
|
49
71
|
const geometry = createBoxMeshGeometry();
|
|
@@ -80,6 +102,8 @@ describe('ensureWgpuMeshUpload', () => {
|
|
|
80
102
|
positions: new Float32Array([1, 2, 3]),
|
|
81
103
|
skinnedNormals: new Float32Array(3),
|
|
82
104
|
skinnedPositions: new Float32Array(3),
|
|
105
|
+
skinnedTangents: new Float32Array(0),
|
|
106
|
+
tangents: new Float32Array(0),
|
|
83
107
|
weights: new Float32Array([1, 0, 0, 0]),
|
|
84
108
|
});
|
|
85
109
|
|
|
@@ -135,6 +159,8 @@ describe('ensureWgpuMeshUpload', () => {
|
|
|
135
159
|
positions: new Float32Array([1, 0, 0]),
|
|
136
160
|
skinnedNormals: new Float32Array(3),
|
|
137
161
|
skinnedPositions: new Float32Array(3),
|
|
162
|
+
skinnedTangents: new Float32Array(0),
|
|
163
|
+
tangents: new Float32Array(0),
|
|
138
164
|
weights: new Float32Array([1, 0, 0, 0]),
|
|
139
165
|
});
|
|
140
166
|
const morph: MeshMorph = {
|
|
@@ -162,7 +188,32 @@ describe('ensureWgpuMeshUpload', () => {
|
|
|
162
188
|
expect(meshRuntime.webgpuData as unknown).toBe(upload);
|
|
163
189
|
});
|
|
164
190
|
|
|
165
|
-
|
|
191
|
+
// glTF primitives may legitimately omit indices and the importer preserves that, so refusing to
|
|
192
|
+
// upload them made valid meshes vanish on this backend with nothing rendered and nothing thrown.
|
|
193
|
+
// The GL sibling has always uploaded them; this mirrors it.
|
|
194
|
+
it('uploads non-indexed geometry with the vertex count in indexCount', () => {
|
|
195
|
+
const { state } = makeWgpuScene3DState();
|
|
196
|
+
const geometry = createMeshGeometry({
|
|
197
|
+
indices: null,
|
|
198
|
+
layout: {
|
|
199
|
+
attributes: [{ byteOffset: 0, format: 'float32x3', semantic: 'position' }],
|
|
200
|
+
stride: 12,
|
|
201
|
+
},
|
|
202
|
+
vertices: new Float32Array(9),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const upload = ensureWgpuMeshUpload(state, geometry);
|
|
206
|
+
|
|
207
|
+
expect(upload.vertexBuffer).toBeDefined();
|
|
208
|
+
expect(upload.indexBuffer).toBeNull();
|
|
209
|
+
// 9 floats / 3 per vertex = 3 vertices, the count a non-indexed draw needs.
|
|
210
|
+
expect(upload.indexCount).toBe(3);
|
|
211
|
+
expect(getWgpuScene3DRuntime(state).uploadCache.get(geometry)).toBe(upload);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// No index buffer exists, so no element format describes one. Null says that; a stand-in 'uint16'
|
|
215
|
+
// would read as a fact about a buffer that is not there.
|
|
216
|
+
it('reports a null index format for non-indexed geometry', () => {
|
|
166
217
|
const { state } = makeWgpuScene3DState();
|
|
167
218
|
const geometry = createMeshGeometry({
|
|
168
219
|
indices: null,
|
|
@@ -172,7 +223,18 @@ describe('ensureWgpuMeshUpload', () => {
|
|
|
172
223
|
},
|
|
173
224
|
vertices: new Float32Array(9),
|
|
174
225
|
});
|
|
175
|
-
|
|
176
|
-
expect(
|
|
226
|
+
|
|
227
|
+
expect(ensureWgpuMeshUpload(state, geometry).indexFormat).toBeNull();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('reports zero vertices when the layout has no stride to divide by', () => {
|
|
231
|
+
const { state } = makeWgpuScene3DState();
|
|
232
|
+
const geometry = createMeshGeometry({
|
|
233
|
+
indices: null,
|
|
234
|
+
layout: { attributes: [], stride: 0 },
|
|
235
|
+
vertices: new Float32Array(9),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(ensureWgpuMeshUpload(state, geometry).indexCount).toBe(0);
|
|
177
239
|
});
|
|
178
240
|
});
|
|
@@ -157,6 +157,29 @@ describe('drawWgpuScene3DParticleEmitter3Ds', () => {
|
|
|
157
157
|
expect(pipelineDescriptors(fake.calls).some((d) => hasTextureConstant(d) === 1)).toBe(true);
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
+
it('copies atlas pixels with straight alpha for post-decode shader premultiplication', () => {
|
|
161
|
+
const { state, fake } = makeAtlasWgpuScene3DState();
|
|
162
|
+
const scene = createNode3D(Node3DKind);
|
|
163
|
+
addNodeChild(scene, makeAtlasEmitter(64, 64));
|
|
164
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
165
|
+
const copy = fake.calls.find((c) => c.name === 'copyExternalImageToTexture');
|
|
166
|
+
expect(copy).toBeDefined();
|
|
167
|
+
expect((copy!.args[1] as { premultipliedAlpha?: boolean }).premultipliedAlpha).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('premultiplies sampled atlas color after decode in the fragment shader', () => {
|
|
171
|
+
const { state, fake } = makeAtlasWgpuScene3DState();
|
|
172
|
+
const scene = createNode3D(Node3DKind);
|
|
173
|
+
addNodeChild(scene, makeAtlasEmitter(64, 64));
|
|
174
|
+
drawWgpuScene3DParticleEmitter3Ds(state, scene, makeCamera(), makeLights());
|
|
175
|
+
const shaderSource = fake.calls
|
|
176
|
+
.filter((c) => c.name === 'createShaderModule')
|
|
177
|
+
.map((c) => String((c.args[0] as { code: string }).code))
|
|
178
|
+
.find((source) => source.includes('@fragment fn fs_main'));
|
|
179
|
+
expect(shaderSource).toContain('let alpha = tex.a * in.color.a;');
|
|
180
|
+
expect(shaderSource).toContain('vec4f(tex.rgb * in.color.rgb * alpha, alpha)');
|
|
181
|
+
});
|
|
182
|
+
|
|
160
183
|
it('compiles the untextured pipeline variant for an atlas-less emitter', () => {
|
|
161
184
|
const { state, fake } = makeWgpuScene3DState();
|
|
162
185
|
const scene = createNode3D(Node3DKind);
|
|
@@ -11,17 +11,21 @@ describe('getWgpuScene3DRuntime', () => {
|
|
|
11
11
|
const a = getWgpuScene3DRuntime(state);
|
|
12
12
|
const b = getWgpuScene3DRuntime(state);
|
|
13
13
|
expect(a).toBe(b);
|
|
14
|
-
expect(a.materialRegistry).toBeInstanceOf(Map);
|
|
15
14
|
expect(a.pipelineCache).toBeInstanceOf(Map);
|
|
16
15
|
expect(a.activeBlendMode).toBeNull();
|
|
17
16
|
expect(a.activeMeshPipeline).toBeNull();
|
|
18
17
|
});
|
|
19
18
|
|
|
20
|
-
it('surfaces
|
|
19
|
+
it('surfaces its upload cache without replacing persistent material dispatch policy', () => {
|
|
21
20
|
const { state } = makeWgpuScene3DState();
|
|
22
|
-
const scene = getWgpuScene3DRuntime(state);
|
|
23
21
|
const stateRuntime = state[EntityRuntimeKey] as WgpuRenderStateRuntime;
|
|
24
|
-
|
|
22
|
+
const materials = stateRuntime.registries.meshMaterialRenderers;
|
|
23
|
+
const modifierSnippets = stateRuntime.registries.modifierSnippets;
|
|
24
|
+
const modifierSnippetRevision = stateRuntime.registries.modifierSnippetRevision;
|
|
25
|
+
const scene = getWgpuScene3DRuntime(state);
|
|
26
|
+
expect(stateRuntime.registries.meshMaterialRenderers).toBe(materials);
|
|
27
|
+
expect(stateRuntime.registries.modifierSnippets).toBe(modifierSnippets);
|
|
28
|
+
expect(stateRuntime.registries.modifierSnippetRevision).toBe(modifierSnippetRevision);
|
|
25
29
|
expect(stateRuntime.sceneMeshUploadCache).toBe(scene.uploadCache);
|
|
26
30
|
});
|
|
27
31
|
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { getWgpuRenderStateRuntime } from '@flighthq/render-wgpu/contract';
|
|
2
|
+
|
|
3
|
+
import { makeWgpuScene3DState } from './wgpuScene3DTestHelper';
|
|
4
|
+
|
|
5
|
+
describe('makeWgpuScene3DState', () => {
|
|
6
|
+
it('records aligned writeBuffer data using typed-array element units', () => {
|
|
7
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
8
|
+
|
|
9
|
+
state.device.queue.writeBuffer({} as GPUBuffer, 0, new Uint16Array([0, 1, 2]), 0, 2);
|
|
10
|
+
|
|
11
|
+
expect(fake.calls.filter((call) => call.name === 'writeBuffer')).toHaveLength(1);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('rejects a writeBuffer ArrayBuffer byte size that is not a multiple of four', () => {
|
|
15
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
16
|
+
const data = new Uint16Array([0, 1, 2]);
|
|
17
|
+
|
|
18
|
+
expect(() =>
|
|
19
|
+
state.device.queue.writeBuffer({} as GPUBuffer, 0, data.buffer, data.byteOffset, data.byteLength),
|
|
20
|
+
).toThrow(expect.objectContaining({ name: 'OperationError' }));
|
|
21
|
+
expect(fake.calls.some((call) => call.name === 'writeBuffer')).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('rejects a writeBuffer typed-array element count whose bytes are not a multiple of four', () => {
|
|
25
|
+
const { state } = makeWgpuScene3DState();
|
|
26
|
+
|
|
27
|
+
expect(() => state.device.queue.writeBuffer({} as GPUBuffer, 0, new Uint16Array([0, 1, 2]))).toThrow(
|
|
28
|
+
expect.objectContaining({ name: 'OperationError' }),
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('rejects a writeBuffer destination offset that is not aligned to four bytes', () => {
|
|
33
|
+
const { state } = makeWgpuScene3DState();
|
|
34
|
+
|
|
35
|
+
expect(() => state.device.queue.writeBuffer({} as GPUBuffer, 2, new Uint32Array([0]))).toThrow(
|
|
36
|
+
/destination offset must be aligned to 4 bytes/,
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rejects misaligned or out-of-range index buffer bindings', () => {
|
|
41
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
42
|
+
const buffer = state.device.createBuffer({ size: 8, usage: GPUBufferUsage.INDEX });
|
|
43
|
+
const pass = getWgpuRenderStateRuntime(state).renderPass!;
|
|
44
|
+
|
|
45
|
+
expect(() => pass.setIndexBuffer(buffer, 'uint16', -2)).toThrow(/offset and size must be non-negative/);
|
|
46
|
+
expect(() => pass.setIndexBuffer(buffer, 'uint32', 2)).toThrow(/offset must be aligned to 4 bytes/);
|
|
47
|
+
expect(() => pass.setIndexBuffer(buffer, 'uint16', 6, 4)).toThrow(/range exceeds the buffer size/);
|
|
48
|
+
expect(fake.calls.some((call) => call.name === 'setIndexBuffer')).toBe(false);
|
|
49
|
+
|
|
50
|
+
pass.setIndexBuffer(buffer, 'uint16', 2, 4);
|
|
51
|
+
expect(fake.calls.filter((call) => call.name === 'setIndexBuffer')).toHaveLength(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('rejects misaligned or out-of-range vertex buffer bindings', () => {
|
|
55
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
56
|
+
const buffer = state.device.createBuffer({ size: 8, usage: GPUBufferUsage.VERTEX });
|
|
57
|
+
const pass = getWgpuRenderStateRuntime(state).renderPass!;
|
|
58
|
+
|
|
59
|
+
expect(() => pass.setVertexBuffer(0, buffer, 0, -4)).toThrow(/offset and size must be non-negative/);
|
|
60
|
+
expect(() => pass.setVertexBuffer(0, buffer, 2)).toThrow(/offset must be aligned to 4 bytes/);
|
|
61
|
+
expect(() => pass.setVertexBuffer(0, buffer, 4, 8)).toThrow(/range exceeds the buffer size/);
|
|
62
|
+
expect(fake.calls.some((call) => call.name === 'setVertexBuffer')).toBe(false);
|
|
63
|
+
|
|
64
|
+
pass.setVertexBuffer(0, buffer, 4, 4);
|
|
65
|
+
expect(fake.calls.filter((call) => call.name === 'setVertexBuffer')).toHaveLength(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('rejects writeTexture data smaller than an rgba8 copy requires', () => {
|
|
69
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
70
|
+
const texture = state.device.createTexture({
|
|
71
|
+
size: [2, 2, 1],
|
|
72
|
+
format: 'rgba8unorm',
|
|
73
|
+
usage: GPUTextureUsage.COPY_DST,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(() =>
|
|
77
|
+
state.device.queue.writeTexture({ texture }, new Uint8Array(15), { bytesPerRow: 8, rowsPerImage: 2 }, [2, 2, 1]),
|
|
78
|
+
).toThrow(expect.objectContaining({ name: 'OperationError' }));
|
|
79
|
+
expect(fake.calls.some((call) => call.name === 'writeTexture')).toBe(false);
|
|
80
|
+
|
|
81
|
+
state.device.queue.writeTexture({ texture }, new Uint8Array(16), { bytesPerRow: 8, rowsPerImage: 2 }, [2, 2, 1]);
|
|
82
|
+
expect(fake.calls.filter((call) => call.name === 'writeTexture')).toHaveLength(1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -15,13 +15,25 @@ function makeSnippet(overrides?: Partial<WgpuModifierSnippet>): WgpuModifierSnip
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
describe('registerWgpuModifierSnippet', () => {
|
|
18
|
-
it('
|
|
19
|
-
const { state } = makeWgpuScene3DState();
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
it('replaces the table while an explicitly copied state retains its snapshot through lazy scene init', () => {
|
|
19
|
+
const { state: screen } = makeWgpuScene3DState();
|
|
20
|
+
const { state: derived } = makeWgpuScene3DState();
|
|
21
|
+
const initial = makeSnippet();
|
|
22
22
|
const override = makeSnippet({ contribution: () => ({ source: '// override' }) });
|
|
23
|
-
registerWgpuModifierSnippet(
|
|
24
|
-
|
|
23
|
+
registerWgpuModifierSnippet(screen, initial);
|
|
24
|
+
const snapshot = getWgpuRenderStateRuntime(screen).registries.modifierSnippets;
|
|
25
|
+
|
|
26
|
+
copyWgpuRenderStateRegistrations(derived, screen);
|
|
27
|
+
getWgpuScene3DRuntime(derived);
|
|
28
|
+
registerWgpuModifierSnippet(screen, override);
|
|
29
|
+
|
|
30
|
+
expect(getWgpuRenderStateRuntime(derived).registries.modifierSnippets).toBe(snapshot);
|
|
31
|
+
expect(getWgpuRenderStateRuntime(derived).registries.modifierSnippetRevision).toBe(1);
|
|
32
|
+
expect(getWgpuRenderStateRuntime(screen).registries.modifierSnippets).not.toBe(snapshot);
|
|
33
|
+
expect(getWgpuRenderStateRuntime(screen).registries.modifierSnippetRevision).toBe(2);
|
|
34
|
+
expect(getRegistryTableEntry(snapshot, 'acme.Test')).toBe(initial);
|
|
35
|
+
expect(resolveWgpuModifierSnippet(derived, 'acme.Test')).toBe(initial);
|
|
36
|
+
expect(resolveWgpuModifierSnippet(screen, 'acme.Test')).toBe(override);
|
|
25
37
|
});
|
|
26
38
|
});
|
|
27
39
|
|
|
@@ -31,3 +43,5 @@ describe('resolveWgpuModifierSnippet', () => {
|
|
|
31
43
|
expect(resolveWgpuModifierSnippet(state, 'acme.Missing')).toBeNull();
|
|
32
44
|
});
|
|
33
45
|
});
|
|
46
|
+
import { getRegistryTableEntry } from '@flighthq/registry/contract';
|
|
47
|
+
import { copyWgpuRenderStateRegistrations, getWgpuRenderStateRuntime } from '@flighthq/render-wgpu/contract';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { registerWgpuImageTextureResolver } from '@flighthq/render-wgpu/contract';
|
|
1
|
+
import { getWgpuRenderStateRuntime, registerWgpuImageTextureResolver } from '@flighthq/render-wgpu/contract';
|
|
2
2
|
import {
|
|
3
3
|
createAnimatedNormalModifier,
|
|
4
4
|
createDissolveModifier,
|
|
@@ -173,7 +173,7 @@ describe('getWgpuShadedModuleSource', () => {
|
|
|
173
173
|
createToonModifier({ steps: 3 }),
|
|
174
174
|
],
|
|
175
175
|
});
|
|
176
|
-
const source = getWgpuShadedModuleSource(material,
|
|
176
|
+
const source = getWgpuShadedModuleSource(material, getWgpuRenderStateRuntime(state).registries.modifierSnippets);
|
|
177
177
|
expect(source).toContain('animatedNormal');
|
|
178
178
|
expect(source).toContain('emissiveTerm');
|
|
179
179
|
expect(source).toContain('rimFactor');
|
|
@@ -195,7 +195,7 @@ describe('getWgpuShadedModuleSource', () => {
|
|
|
195
195
|
}),
|
|
196
196
|
],
|
|
197
197
|
});
|
|
198
|
-
const source = getWgpuShadedModuleSource(material,
|
|
198
|
+
const source = getWgpuShadedModuleSource(material, getWgpuRenderStateRuntime(state).registries.modifierSnippets);
|
|
199
199
|
expect(source.indexOf('localPosition = vec4f(localPosition.xyz')).toBeLessThan(
|
|
200
200
|
source.indexOf('let world = draw.world * localPosition'),
|
|
201
201
|
);
|
|
@@ -212,7 +212,7 @@ describe('getWgpuShadedModuleSource', () => {
|
|
|
212
212
|
const registered = { kind: 'acme.Glow', slot: ModifierSlot.Effect } as Modifier;
|
|
213
213
|
const missing = { kind: 'acme.Missing', slot: ModifierSlot.Effect } as Modifier;
|
|
214
214
|
const material = createShadedMaterial({ modifiers: [registered, missing] });
|
|
215
|
-
const registry =
|
|
215
|
+
const registry = getWgpuRenderStateRuntime(state).registries.modifierSnippets;
|
|
216
216
|
const source = getWgpuShadedModuleSource(material, registry);
|
|
217
217
|
expect(source).toContain('radiance = radiance + vec3f(0.125)');
|
|
218
218
|
expect(buildWgpuShadedCacheKey(material, registry)).toContain('acme.Missing');
|
|
@@ -249,7 +249,7 @@ describe('getWgpuShadedModuleSource', () => {
|
|
|
249
249
|
});
|
|
250
250
|
modifiers.push({ kind, slot: slots[i] });
|
|
251
251
|
}
|
|
252
|
-
const registry =
|
|
252
|
+
const registry = getWgpuRenderStateRuntime(state).registries.modifierSnippets;
|
|
253
253
|
const source = getWgpuShadedModuleSource(createShadedMaterial({ modifiers }), registry);
|
|
254
254
|
for (let i = 0; i < slots.length; i++) expect(source).toContain(`slot_marker_${i}`);
|
|
255
255
|
expect(source).toMatch(/\/\/ slot_marker_1\s+diffuse = diffuse;\s+if \(ALPHA_MASK/);
|
|
@@ -277,7 +277,7 @@ describe('getWgpuShadedModuleSource', () => {
|
|
|
277
277
|
{ kind: 'acme.DeclarationB', slot: ModifierSlot.Effect },
|
|
278
278
|
],
|
|
279
279
|
});
|
|
280
|
-
const source = getWgpuShadedModuleSource(material,
|
|
280
|
+
const source = getWgpuShadedModuleSource(material, getWgpuRenderStateRuntime(state).registries.modifierSnippets);
|
|
281
281
|
expect(source.match(/fn shadedValueNoise/g)).toHaveLength(1);
|
|
282
282
|
expect(source).toContain('fn vendorA() {}\nfn vendorB() {}');
|
|
283
283
|
});
|
|
@@ -214,6 +214,36 @@ describe('drawWgpuScene3DShadowMap', () => {
|
|
|
214
214
|
expect(fake.calls.filter((call) => call.name === 'beginRenderPass')).toHaveLength(passCount);
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
+
// The sibling of the forward-pass defect: the shadow pass also bound an index buffer and issued
|
|
218
|
+
// drawIndexed unconditionally, so a non-indexed caster rendered (once the forward path was fixed)
|
|
219
|
+
// but cast no shadow. glShadowMap has always branched to drawArrays for this case.
|
|
220
|
+
it('issues a non-indexed draw for a caster without indices', () => {
|
|
221
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
222
|
+
const scene = createNode3D(Node3DKind);
|
|
223
|
+
addNodeChild(
|
|
224
|
+
scene,
|
|
225
|
+
createMesh(
|
|
226
|
+
createMeshGeometry({
|
|
227
|
+
indices: null,
|
|
228
|
+
layout: {
|
|
229
|
+
attributes: [{ byteOffset: 0, format: 'float32x3', semantic: 'position' }],
|
|
230
|
+
stride: 12,
|
|
231
|
+
},
|
|
232
|
+
vertices: new Float32Array(9),
|
|
233
|
+
}),
|
|
234
|
+
[],
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
drawWgpuScene3DShadowMap(state, scene, makeShadowCamera(), SHADOW_LIGHT);
|
|
239
|
+
|
|
240
|
+
const draw = fake.calls.find((c) => c.name === 'draw');
|
|
241
|
+
expect(draw).toBeDefined();
|
|
242
|
+
expect(draw!.args[0]).toBe(3);
|
|
243
|
+
expect(fake.calls.some((c) => c.name === 'drawIndexed')).toBe(false);
|
|
244
|
+
expect(fake.calls.some((c) => c.name === 'setIndexBuffer')).toBe(false);
|
|
245
|
+
});
|
|
246
|
+
|
|
217
247
|
it('opens a depth-only pass and renders each caster mesh depth', () => {
|
|
218
248
|
const { fake, state } = makeWgpuScene3DState();
|
|
219
249
|
drawWgpuScene3DShadowMap(state, makeShadowScene3D(), makeShadowCamera(), SHADOW_LIGHT);
|