@forgeax/engine-render 0.1.34 → 0.1.35
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 +52 -3
- package/dist/assembly/device-feature-admission.d.ts +2 -2
- package/dist/assembly/host-contract.d.ts +3 -2
- package/dist/assembly/host-contract.d.ts.map +1 -1
- package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
- package/dist/assembly/render-target-host.d.ts +5 -0
- package/dist/assembly/render-target-host.d.ts.map +1 -1
- package/dist/assembly/renderer-facade.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts +1 -0
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/authoring.mjs +2 -4
- package/dist/authoring.mjs.map +1 -1
- package/dist/chunk-3XUBWRY2.mjs +1842 -0
- package/dist/chunk-3XUBWRY2.mjs.map +1 -0
- package/dist/{chunk-UI5SFODX.mjs → chunk-G6HDNGY7.mjs} +2329 -1776
- package/dist/chunk-G6HDNGY7.mjs.map +1 -0
- package/dist/{chunk-HEB564WF.mjs → chunk-LM5IE7GF.mjs} +4 -4
- package/dist/{chunk-HEB564WF.mjs.map → chunk-LM5IE7GF.mjs.map} +1 -1
- package/dist/{chunk-MKDIT2EM.mjs → chunk-LNIUIKQ7.mjs} +143 -246
- package/dist/chunk-LNIUIKQ7.mjs.map +1 -0
- package/dist/chunk-MXTWKZO4.mjs +3268 -0
- package/dist/chunk-MXTWKZO4.mjs.map +1 -0
- package/dist/{chunk-ODZWR6JQ.mjs → chunk-MZ4C7EHF.mjs} +2042 -27
- package/dist/chunk-MZ4C7EHF.mjs.map +1 -0
- package/dist/chunk-RIPAEGI6.mjs +48 -0
- package/dist/chunk-RIPAEGI6.mjs.map +1 -0
- package/dist/{chunk-OCDLEI6I.mjs → chunk-SGPUJW3F.mjs} +117 -5
- package/dist/chunk-SGPUJW3F.mjs.map +1 -0
- package/dist/{chunk-ELJFJ2BU.mjs → chunk-X2KA6WHM.mjs} +29 -4
- package/dist/chunk-X2KA6WHM.mjs.map +1 -0
- package/dist/{chunk-BLXGONR2.mjs → chunk-XSFB7XXQ.mjs} +51 -4
- package/dist/chunk-XSFB7XXQ.mjs.map +1 -0
- package/dist/cloud/frame-context.d.ts +2 -2
- package/dist/cloud/frame-context.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +810 -2073
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +19 -19
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/errors/render.d.ts +3 -2
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/material-context.d.ts +7 -0
- package/dist/extract/material-context.d.ts.map +1 -0
- package/dist/features/host.d.ts +5 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/plan.d.ts +2 -0
- package/dist/features/plan.d.ts.map +1 -1
- package/dist/features/types.d.ts +4 -0
- package/dist/features/types.d.ts.map +1 -1
- package/dist/glyph-text-layout-system.d.ts +1 -1
- package/dist/glyph-text-layout-system.d.ts.map +1 -1
- package/dist/gpu-driven/shadow-ownership.d.ts +3 -3
- package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -1
- package/dist/ibl/frame-resources.d.ts.map +1 -1
- package/dist/ibl/skylight-bind-group.d.ts +4 -4
- package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +530 -9
- package/dist/index.mjs.map +1 -1
- package/dist/instances.d.ts +3 -4
- package/dist/instances.d.ts.map +1 -1
- package/dist/internal.mjs +7 -8
- package/dist/internal.mjs.map +1 -1
- package/dist/pbr-pipeline.d.ts +3 -3
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-output/lut-gpu.d.ts +2 -2
- package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -1
- package/dist/pipeline/standard-output/resources.d.ts +2 -2
- package/dist/pipeline/standard-output/resources.d.ts.map +1 -1
- package/dist/publication/contract.d.ts +77 -0
- package/dist/publication/contract.d.ts.map +1 -0
- package/dist/publication/dependencies.d.ts +7 -0
- package/dist/publication/dependencies.d.ts.map +1 -0
- package/dist/publication/prepare-geometry.d.ts +9 -0
- package/dist/publication/prepare-geometry.d.ts.map +1 -0
- package/dist/publication/programs.d.ts +5 -0
- package/dist/publication/programs.d.ts.map +1 -0
- package/dist/publication/publisher.d.ts +33 -0
- package/dist/publication/publisher.d.ts.map +1 -0
- package/dist/publication/receiver.d.ts +29 -0
- package/dist/publication/receiver.d.ts.map +1 -0
- package/dist/publication/resource-scope.d.ts +18 -0
- package/dist/publication/resource-scope.d.ts.map +1 -0
- package/dist/publication/targets.d.ts +33 -0
- package/dist/publication/targets.d.ts.map +1 -0
- package/dist/publication/video.d.ts +5 -0
- package/dist/publication/video.d.ts.map +1 -0
- package/dist/record/fold-buckets.d.ts +3 -3
- package/dist/record/fold-buckets.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +6 -6
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame.d.ts +5 -4
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/helpers.d.ts +2 -2
- package/dist/record/helpers.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +2 -2
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +7 -9
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass-sprite-draws.d.ts +3 -16
- package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/prepared-material-bindings.d.ts +3 -3
- package/dist/record/prepared-material-bindings.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +2 -2
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/sprite-instance-buffer.d.ts +31 -0
- package/dist/record/sprite-instance-buffer.d.ts.map +1 -0
- package/dist/record/target-capture-graph.d.ts +19 -0
- package/dist/record/target-capture-graph.d.ts.map +1 -0
- package/dist/record/typed-frame-graph.d.ts +2 -13
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/record/volume-params.d.ts +2 -2
- package/dist/record/volume-params.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts +5 -5
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-contract.d.ts +4 -1
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +2 -0
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system-presentation.d.ts +2 -2
- package/dist/render-system-presentation.d.ts.map +1 -1
- package/dist/render-system-projections.d.ts +3 -2
- package/dist/render-system-projections.d.ts.map +1 -1
- package/dist/render-system-residency.d.ts +2 -2
- package/dist/render-system-residency.d.ts.map +1 -1
- package/dist/render-system.d.ts +3 -7
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +8 -4
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/scene/render-source.d.ts +42 -0
- package/dist/scene/render-source.d.ts.map +1 -0
- package/dist/scene/source-systems.d.ts +6 -0
- package/dist/scene/source-systems.d.ts.map +1 -0
- package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
- package/dist/systems/skin-palette-types.d.ts +10 -0
- package/dist/systems/skin-palette-types.d.ts.map +1 -1
- package/dist/systems/transparent-dispatch.d.ts +2 -2
- package/dist/systems/transparent-dispatch.d.ts.map +1 -1
- package/dist/systems/transparent-sort-config.d.ts +2 -1
- package/dist/systems/transparent-sort-config.d.ts.map +1 -1
- package/dist/temporal/index.mjs +6 -7
- package/package.json +21 -21
- package/src/__tests__/auto-exposure-transaction.integration.test.ts +4 -9
- package/src/__tests__/device-feature-admission.unit.test.ts +3 -2
- package/src/__tests__/gpu-driven-pbr.dawn.test.ts +3 -4
- package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +4 -1
- package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +0 -1
- package/src/__tests__/materials-standard-contract.unit.test.ts +0 -1
- package/src/__tests__/physical-clearcoat.integration.test.ts +23 -23
- package/src/__tests__/physical-material-budget.unit.test.ts +40 -0
- package/src/__tests__/render-error-code-owner.test-d.ts +1 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +1 -1
- package/src/assembly/device-feature-admission.ts +2 -2
- package/src/assembly/host-contract.ts +5 -2
- package/src/assembly/recovery/renderer-recover.ts +9 -0
- package/src/assembly/render-target-host.ts +57 -47
- package/src/assembly/renderer-facade.ts +3 -1
- package/src/assembly/webgpu-ready.ts +1 -1
- package/src/assembly/webgpu-renderer.ts +89 -61
- package/src/cloud/frame-context.ts +2 -2
- package/src/device/gpu-residency.ts +26 -25
- package/src/errors/render.ts +3 -1
- package/src/extract/material-context.ts +21 -0
- package/src/features/host.ts +13 -5
- package/src/features/plan.ts +2 -0
- package/src/features/types.ts +4 -0
- package/src/glyph-text-layout-system.ts +2 -2
- package/src/gpu-driven/shadow-ownership.ts +3 -3
- package/src/ibl/frame-resources.ts +0 -1
- package/src/ibl/skylight-bind-group.ts +13 -25
- package/src/index.ts +9 -0
- package/src/instances.ts +9 -6
- package/src/pbr-pipeline.ts +11 -17
- package/src/pipeline/standard-output/lut-gpu.ts +3 -3
- package/src/pipeline/standard-output/resources.ts +2 -2
- package/src/publication/__tests__/derived-geometry.integration.test.ts +177 -0
- package/src/publication/__tests__/features.integration.test.ts +122 -0
- package/src/publication/__tests__/programs.unit.test.ts +25 -0
- package/src/publication/__tests__/publication.integration.test.ts +370 -0
- package/src/publication/__tests__/targets.integration.test.ts +120 -0
- package/src/publication/__tests__/video.browser.test.ts +50 -0
- package/src/publication/contract.ts +104 -0
- package/src/publication/dependencies.ts +54 -0
- package/src/publication/prepare-geometry.ts +94 -0
- package/src/publication/programs.ts +45 -0
- package/src/publication/publisher.ts +618 -0
- package/src/publication/receiver.ts +297 -0
- package/src/publication/resource-scope.ts +31 -0
- package/src/publication/targets.ts +182 -0
- package/src/publication/video.ts +33 -0
- package/src/record/__tests__/material-bg-cache.unit.test.ts +15 -1
- package/src/record/__tests__/sprite-material-abi.unit.test.ts +8 -10
- package/src/record/__tests__/video-texture-view.unit.test.ts +9 -9
- package/src/record/fold-buckets.ts +8 -5
- package/src/record/frame-snapshot.ts +6 -6
- package/src/record/frame.ts +42 -22
- package/src/record/helpers.ts +2 -2
- package/src/record/main-pass-geometry.ts +21 -2
- package/src/record/main-pass-material.ts +37 -33
- package/src/record/main-pass-sprite-draws.ts +6 -152
- package/src/record/main-pass.ts +8 -7
- package/src/record/prepared-material-bindings.ts +11 -6
- package/src/record/render-context.ts +2 -2
- package/src/record/shadow-pass.ts +0 -3
- package/src/record/sprite-instance-buffer.ts +155 -0
- package/src/record/target-capture-graph.ts +258 -0
- package/src/record/typed-frame-graph.ts +6 -206
- package/src/record/view-ubo.ts +1 -1
- package/src/record/volume-params.ts +6 -6
- package/src/recovery/render-system-candidate.ts +10 -10
- package/src/render-contract.ts +6 -1
- package/src/render-system-extract-tail.ts +33 -17
- package/src/render-system-extract.ts +2 -0
- package/src/render-system-presentation.ts +2 -2
- package/src/render-system-projections.ts +3 -2
- package/src/render-system-residency.ts +2 -2
- package/src/render-system.ts +82 -53
- package/src/scene/render-scene.ts +110 -100
- package/src/scene/render-source.ts +90 -0
- package/src/scene/source-systems.ts +33 -0
- package/src/systems/skin-palette-allocator.ts +1 -0
- package/src/systems/skin-palette-types.ts +11 -0
- package/src/systems/transparent-dispatch.ts +2 -2
- package/src/systems/transparent-sort-config.ts +3 -1
- package/dist/chunk-BLXGONR2.mjs.map +0 -1
- package/dist/chunk-ELJFJ2BU.mjs.map +0 -1
- package/dist/chunk-ILQ5MEZF.mjs +0 -32
- package/dist/chunk-ILQ5MEZF.mjs.map +0 -1
- package/dist/chunk-MKDIT2EM.mjs.map +0 -1
- package/dist/chunk-OCDLEI6I.mjs.map +0 -1
- package/dist/chunk-ODZWR6JQ.mjs.map +0 -1
- package/dist/chunk-RXZ3RGOU.mjs +0 -53
- package/dist/chunk-RXZ3RGOU.mjs.map +0 -1
- package/dist/chunk-SHFUFJ5N.mjs +0 -1781
- package/dist/chunk-SHFUFJ5N.mjs.map +0 -1
- package/dist/chunk-UI5SFODX.mjs.map +0 -1
- package/dist/chunk-UINHLCZA.mjs +0 -2389
- package/dist/chunk-UINHLCZA.mjs.map +0 -1
- package/dist/chunk-WEX45HKH.mjs +0 -1524
- package/dist/chunk-WEX45HKH.mjs.map +0 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { AssetRegistry, HANDLE_CUBE, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
|
|
2
|
+
import { World } from '@forgeax/engine-ecs';
|
|
3
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
4
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
5
|
+
import { ShaderRegistry } from '@forgeax/engine-shader';
|
|
6
|
+
import { type MeshAsset, ok } from '@forgeax/engine-types';
|
|
7
|
+
import { expect, it } from 'vitest';
|
|
8
|
+
import { MeshFilter, MeshRenderer } from '../../components';
|
|
9
|
+
import type { RenderFeature } from '../../features/types';
|
|
10
|
+
import { renderPublicationTransfers } from '../contract';
|
|
11
|
+
import { createRenderPublisher } from '../publisher';
|
|
12
|
+
import { RenderPublicationReceiver } from '../receiver';
|
|
13
|
+
|
|
14
|
+
it('acknowledges only submitted source data, rejecting stale and duplicate receipts atomically', () => {
|
|
15
|
+
const world = new World();
|
|
16
|
+
const assets = new AssetRegistry(new ShaderRegistry({ manifestUrl: undefined }));
|
|
17
|
+
const consumed: unknown[] = [];
|
|
18
|
+
let value = 1;
|
|
19
|
+
const feature: RenderFeature<unknown> = {
|
|
20
|
+
identity: 'source-events',
|
|
21
|
+
extract: () => ok({ value }),
|
|
22
|
+
plan: () => ok({ resources: [], passes: [] }),
|
|
23
|
+
onSourceFrameSubmitted: (data, feedback) => consumed.push({ data, feedback }),
|
|
24
|
+
};
|
|
25
|
+
const publisher = createRenderPublisher(world, assets, { source: 'ack', epoch: 1 }, undefined, [
|
|
26
|
+
feature,
|
|
27
|
+
]);
|
|
28
|
+
const candidate = publisher.prepare(0).unwrap();
|
|
29
|
+
const packet = structuredClone(candidate.packet, {
|
|
30
|
+
transfer: renderPublicationTransfers(candidate.packet),
|
|
31
|
+
});
|
|
32
|
+
candidate.accept();
|
|
33
|
+
value = 2;
|
|
34
|
+
expect(consumed).toEqual([]);
|
|
35
|
+
expect(publisher.acknowledgeFeatures(0, [{ identity: feature.identity, feedback: 1 }]).ok).toBe(
|
|
36
|
+
false,
|
|
37
|
+
);
|
|
38
|
+
expect(
|
|
39
|
+
publisher.acknowledgeFeatures(1, [
|
|
40
|
+
{ identity: feature.identity, feedback: 1 },
|
|
41
|
+
{ identity: 'unknown', feedback: 1 },
|
|
42
|
+
]).ok,
|
|
43
|
+
).toBe(false);
|
|
44
|
+
expect(consumed).toEqual([]);
|
|
45
|
+
publisher.acknowledgeFeatures(1, [{ identity: feature.identity, feedback: 1 }]).unwrap();
|
|
46
|
+
expect(consumed).toEqual([{ data: { value: 1 }, feedback: 1 }]);
|
|
47
|
+
expect(publisher.acknowledgeFeatures(1, [{ identity: feature.identity, feedback: 1 }]).ok).toBe(
|
|
48
|
+
false,
|
|
49
|
+
);
|
|
50
|
+
publisher.recycle(1, renderPublicationTransfers(packet)).unwrap();
|
|
51
|
+
const next = publisher.prepare(1).unwrap();
|
|
52
|
+
next.discard();
|
|
53
|
+
expect(consumed).toHaveLength(1);
|
|
54
|
+
publisher.dispose();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('publishes feature-only GUID resources and retires them when the producer drops the root', () => {
|
|
58
|
+
const world = new World();
|
|
59
|
+
const assets = new AssetRegistry(new ShaderRegistry({ manifestUrl: undefined }));
|
|
60
|
+
const guid = '12345678-1234-1234-1234-123456789abc';
|
|
61
|
+
const mesh = resolveAssetHandle<MeshAsset>(world, HANDLE_CUBE).unwrap();
|
|
62
|
+
assets.catalog(guid, mesh);
|
|
63
|
+
let roots = [guid];
|
|
64
|
+
const feature: RenderFeature<unknown> = {
|
|
65
|
+
identity: 'guid-consumer',
|
|
66
|
+
extract: () => ok({}),
|
|
67
|
+
assetDependencies: () => roots,
|
|
68
|
+
plan: () => ok({ resources: [], passes: [] }),
|
|
69
|
+
};
|
|
70
|
+
const identity = { source: 'feature-assets', epoch: 1 };
|
|
71
|
+
const publisher = createRenderPublisher(world, assets, identity, undefined, [feature]);
|
|
72
|
+
const receiver = new RenderPublicationReceiver(identity);
|
|
73
|
+
const first = publisher.prepare(0).unwrap();
|
|
74
|
+
const packet = structuredClone(first.packet);
|
|
75
|
+
first.accept();
|
|
76
|
+
const accepted = receiver.accept(packet).unwrap();
|
|
77
|
+
expect(accepted.resources.lookupAsset(guid)).toEqual(mesh);
|
|
78
|
+
expect(packet.assets.filter((row) => row.guid === guid)).toHaveLength(1);
|
|
79
|
+
publisher.recycle(1, renderPublicationTransfers(packet)).unwrap();
|
|
80
|
+
roots = [];
|
|
81
|
+
const next = publisher.prepare(1).unwrap();
|
|
82
|
+
expect(next.packet.retiredAssets).toContain(accepted.resources.handleForGuid(guid));
|
|
83
|
+
receiver.accept(next.packet).unwrap();
|
|
84
|
+
expect(accepted.resources.lookupAsset(guid)).toBeUndefined();
|
|
85
|
+
next.discard();
|
|
86
|
+
publisher.dispose();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('publishes LOD GUID dependencies and rejects a missing lower-detail asset before acceptance', () => {
|
|
90
|
+
const world = new World();
|
|
91
|
+
const assets = new AssetRegistry(new ShaderRegistry({ manifestUrl: undefined }));
|
|
92
|
+
const guid = '12345678-1234-1234-1234-123456789abc';
|
|
93
|
+
const mesh = resolveAssetHandle<MeshAsset>(world, HANDLE_CUBE).unwrap();
|
|
94
|
+
assets.catalog(guid, mesh);
|
|
95
|
+
const parsed = AssetGuid.parse(guid);
|
|
96
|
+
if (!parsed.ok) throw parsed.error;
|
|
97
|
+
const assetHandle = world.allocSharedRef('MeshAsset', {
|
|
98
|
+
...mesh,
|
|
99
|
+
lods: [{ mesh: parsed.value, screenCoverage: 0.5 }],
|
|
100
|
+
});
|
|
101
|
+
world
|
|
102
|
+
.spawn(
|
|
103
|
+
{ component: Transform, data: {} },
|
|
104
|
+
{ component: MeshFilter, data: { assetHandle } },
|
|
105
|
+
{ component: MeshRenderer, data: {} },
|
|
106
|
+
)
|
|
107
|
+
.unwrap();
|
|
108
|
+
const identity = { source: 'lod-assets', epoch: 1 };
|
|
109
|
+
const publisher = createRenderPublisher(world, assets, identity);
|
|
110
|
+
const receiver = new RenderPublicationReceiver(identity);
|
|
111
|
+
const candidate = publisher.prepare(0).unwrap();
|
|
112
|
+
expect(candidate.packet.assets.some((row) => row.guid === guid)).toBe(true);
|
|
113
|
+
const missing = {
|
|
114
|
+
...candidate.packet,
|
|
115
|
+
assets: candidate.packet.assets.filter((row) => row.guid !== guid),
|
|
116
|
+
};
|
|
117
|
+
expect(receiver.accept(missing).ok).toBe(false);
|
|
118
|
+
expect(receiver.acceptedRevision).toBe(0);
|
|
119
|
+
expect(receiver.accept(structuredClone(candidate.packet)).ok).toBe(true);
|
|
120
|
+
candidate.discard();
|
|
121
|
+
publisher.dispose();
|
|
122
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
2
|
+
import { ShaderRegistry } from '@forgeax/engine-shader';
|
|
3
|
+
import { expect, it } from 'vitest';
|
|
4
|
+
import { installPublicationPrograms } from '../programs';
|
|
5
|
+
|
|
6
|
+
it('rejects conflicting programs atomically and preserves the installed shader', () => {
|
|
7
|
+
const shaders = new ShaderRegistry({ manifestUrl: undefined });
|
|
8
|
+
const assets = new AssetRegistry(shaders);
|
|
9
|
+
const first = { key: 'forgeax::publication-test', shader: { source: 'first', paramSchema: [] } };
|
|
10
|
+
installPublicationPrograms(assets, [first]);
|
|
11
|
+
expect(() =>
|
|
12
|
+
installPublicationPrograms(assets, [
|
|
13
|
+
{ key: 'forgeax::new-test', shader: { source: 'new', paramSchema: [] } },
|
|
14
|
+
{ ...first, shader: { ...first.shader, source: 'conflict' } },
|
|
15
|
+
]),
|
|
16
|
+
).toThrow('conflicting immutable shader');
|
|
17
|
+
expect(shaders.findMaterialArtifact('forgeax::new-test').ok).toBe(false);
|
|
18
|
+
expect(shaders.findMaterialArtifact(first.key).unwrap().source).toBe('first');
|
|
19
|
+
expect(() => installPublicationPrograms(assets, [first, first])).toThrow('duplicate');
|
|
20
|
+
expect(() =>
|
|
21
|
+
installPublicationPrograms(assets, [
|
|
22
|
+
{ ...first, artifact: { key: 'wrong', bytes: new Uint8Array() } },
|
|
23
|
+
]),
|
|
24
|
+
).toThrow('mismatched');
|
|
25
|
+
});
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssetRegistry,
|
|
3
|
+
HANDLE_CUBE,
|
|
4
|
+
RuntimeMaterialValue,
|
|
5
|
+
RuntimeMeshVertices,
|
|
6
|
+
resolveAssetHandle,
|
|
7
|
+
} from '@forgeax/engine-assets-runtime';
|
|
8
|
+
import { Time, World } from '@forgeax/engine-ecs';
|
|
9
|
+
import { ChildOf, GlobalTransform, Transform } from '@forgeax/engine-scene';
|
|
10
|
+
import { ShaderRegistry } from '@forgeax/engine-shader';
|
|
11
|
+
import { Skin } from '@forgeax/engine-skinning';
|
|
12
|
+
import type { MaterialAsset, MeshAsset } from '@forgeax/engine-types';
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import {
|
|
15
|
+
Camera,
|
|
16
|
+
Instances,
|
|
17
|
+
MeshFilter,
|
|
18
|
+
MeshRenderer,
|
|
19
|
+
Points,
|
|
20
|
+
PostProcessParams,
|
|
21
|
+
Skylight,
|
|
22
|
+
Visibility,
|
|
23
|
+
VisibilityStateValue,
|
|
24
|
+
} from '../../components';
|
|
25
|
+
import { PersistentRenderScene } from '../../scene/render-scene';
|
|
26
|
+
import { renderPublicationTransfers } from '../contract';
|
|
27
|
+
import { createRenderPublisher } from '../publisher';
|
|
28
|
+
import { RenderPublicationReceiver } from '../receiver';
|
|
29
|
+
|
|
30
|
+
function fixture() {
|
|
31
|
+
const world = new World();
|
|
32
|
+
const assets = new AssetRegistry(new ShaderRegistry({ manifestUrl: undefined }));
|
|
33
|
+
const identity = { source: 'publication-test', epoch: 1 };
|
|
34
|
+
const publisher = createRenderPublisher(world, assets, identity);
|
|
35
|
+
world
|
|
36
|
+
.spawn(
|
|
37
|
+
{ component: Transform, data: { pos: [0, 0, 5] } },
|
|
38
|
+
{ component: Camera, data: { fov: Math.PI / 3 } },
|
|
39
|
+
)
|
|
40
|
+
.unwrap();
|
|
41
|
+
const spawn = () =>
|
|
42
|
+
world
|
|
43
|
+
.spawn(
|
|
44
|
+
{ component: Transform, data: {} },
|
|
45
|
+
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
46
|
+
{ component: MeshRenderer, data: {} },
|
|
47
|
+
)
|
|
48
|
+
.unwrap();
|
|
49
|
+
const first = spawn(),
|
|
50
|
+
second = spawn();
|
|
51
|
+
world.update(0).unwrap();
|
|
52
|
+
const receiver = new RenderPublicationReceiver(identity),
|
|
53
|
+
scene = new PersistentRenderScene();
|
|
54
|
+
const publish = () => {
|
|
55
|
+
const candidate = publisher.prepare(world.getResource(Time).elapsed).unwrap();
|
|
56
|
+
const packet = structuredClone(candidate.packet, {
|
|
57
|
+
transfer: renderPublicationTransfers(candidate.packet),
|
|
58
|
+
});
|
|
59
|
+
candidate.accept();
|
|
60
|
+
const saved = structuredClone(packet);
|
|
61
|
+
const accepted = receiver.accept(packet).unwrap();
|
|
62
|
+
const frame = scene.consumePublication(accepted);
|
|
63
|
+
const returned = structuredClone(renderPublicationTransfers(packet), {
|
|
64
|
+
transfer: renderPublicationTransfers(packet),
|
|
65
|
+
});
|
|
66
|
+
publisher.recycle(packet.revision, returned).unwrap();
|
|
67
|
+
// Preserve logical rows independently from the returned transport storage.
|
|
68
|
+
return { packet: saved, frame };
|
|
69
|
+
};
|
|
70
|
+
return { world, publisher, receiver, scene, first, second, spawn, publish };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('native render publication', () => {
|
|
74
|
+
it('publishes a numeric baseline, observes no static delta and retains the same CPU slots', () => {
|
|
75
|
+
const f = fixture();
|
|
76
|
+
const initial = f.publish();
|
|
77
|
+
expect(initial.frame.renderables).toHaveLength(2);
|
|
78
|
+
expect(initial.packet.templates).toHaveLength(1);
|
|
79
|
+
const slots = f.scene.compositionSlots().map((slot) => [slot.slot, slot.generation]);
|
|
80
|
+
const next = f.publish();
|
|
81
|
+
expect(
|
|
82
|
+
next.packet.upserts.length + next.packet.transforms.length + next.packet.removed.length,
|
|
83
|
+
).toBe(0);
|
|
84
|
+
expect(next.packet.assets).toHaveLength(0);
|
|
85
|
+
expect(f.scene.compositionSlots().map((slot) => [slot.slot, slot.generation])).toEqual(slots);
|
|
86
|
+
f.publisher.dispose();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('accumulates changes across a slow consumer without transferring ECS columns', () => {
|
|
90
|
+
const f = fixture();
|
|
91
|
+
f.publish();
|
|
92
|
+
f.world.set(f.first, Transform, { pos: [1, 0, 0] }).unwrap();
|
|
93
|
+
f.world.update(1 / 60).unwrap();
|
|
94
|
+
f.world.set(f.second, Transform, { pos: [2, 0, 0] }).unwrap();
|
|
95
|
+
f.world.update(1 / 60).unwrap();
|
|
96
|
+
const next = f.publish();
|
|
97
|
+
expect(next.packet.upserts).toHaveLength(0);
|
|
98
|
+
expect(new Set(next.packet.transformEntities)).toEqual(new Set([f.first, f.second]));
|
|
99
|
+
for (const entity of [f.first, f.second]) {
|
|
100
|
+
const matrix = f.world.get(entity, GlobalTransform).unwrap().world;
|
|
101
|
+
expect(matrix.byteLength).toBe(64);
|
|
102
|
+
expect([
|
|
103
|
+
...(next.frame.renderables.find((row) => row.entityKey === entity)?.transform.world ?? []),
|
|
104
|
+
]).toEqual([...matrix]);
|
|
105
|
+
}
|
|
106
|
+
f.publisher.dispose();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('reconciles destruction and generation reuse without reviving the old render identity', () => {
|
|
110
|
+
const f = fixture();
|
|
111
|
+
f.publish();
|
|
112
|
+
f.world.despawn(f.first).unwrap();
|
|
113
|
+
const replacement = f.spawn();
|
|
114
|
+
f.world.update(0).unwrap();
|
|
115
|
+
const next = f.publish();
|
|
116
|
+
expect(next.packet.removed).toContain(f.first);
|
|
117
|
+
expect(next.frame.renderables.map((row) => row.entityKey)).toEqual(
|
|
118
|
+
expect.arrayContaining([replacement, f.second]),
|
|
119
|
+
);
|
|
120
|
+
expect(next.frame.renderables.some((row) => row.entityKey === f.first)).toBe(false);
|
|
121
|
+
f.publisher.dispose();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('rejects old epochs before the first baseline and wrong revisions without advancing acceptance', () => {
|
|
125
|
+
const f = fixture();
|
|
126
|
+
const candidate = f.publisher.prepare(0).unwrap();
|
|
127
|
+
expect(f.receiver.accept({ ...candidate.packet, epoch: 0 })).toMatchObject({
|
|
128
|
+
ok: false,
|
|
129
|
+
error: { detail: { reason: 'identity' } },
|
|
130
|
+
});
|
|
131
|
+
expect(f.receiver.accept({ ...candidate.packet, base: 1 })).toMatchObject({
|
|
132
|
+
ok: false,
|
|
133
|
+
error: { detail: { reason: 'base' } },
|
|
134
|
+
});
|
|
135
|
+
expect(f.receiver.accept({ ...candidate.packet, revision: 2 })).toMatchObject({
|
|
136
|
+
ok: false,
|
|
137
|
+
error: { detail: { reason: 'revision' } },
|
|
138
|
+
});
|
|
139
|
+
expect(f.receiver.acceptedRevision).toBe(0);
|
|
140
|
+
candidate.discard();
|
|
141
|
+
f.publish();
|
|
142
|
+
expect(f.receiver.acceptedRevision).toBe(1);
|
|
143
|
+
f.publisher.dispose();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('rejects malformed columns and missing dependencies without consuming the baseline', () => {
|
|
147
|
+
const f = fixture();
|
|
148
|
+
const candidate = f.publisher.prepare(0).unwrap();
|
|
149
|
+
for (const packet of [
|
|
150
|
+
{ ...candidate.packet, transforms: new Float32Array(1) },
|
|
151
|
+
{ ...candidate.packet, assets: [] },
|
|
152
|
+
{ ...candidate.packet, upserts: new Uint32Array([f.first, 99]) },
|
|
153
|
+
{ ...candidate.packet, removed: new Uint32Array([f.first]) },
|
|
154
|
+
]) {
|
|
155
|
+
expect(f.receiver.accept(packet).ok).toBe(false);
|
|
156
|
+
expect(f.receiver.acceptedRevision).toBe(0);
|
|
157
|
+
}
|
|
158
|
+
candidate.discard();
|
|
159
|
+
expect(f.publish().frame.renderables).toHaveLength(2);
|
|
160
|
+
f.publisher.dispose();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('retries observed transforms after a discarded synchronous publication', () => {
|
|
164
|
+
const f = fixture();
|
|
165
|
+
f.publish();
|
|
166
|
+
f.world.set(f.first, Transform, { pos: [3, 0, 0] }).unwrap();
|
|
167
|
+
f.world.update(0).unwrap();
|
|
168
|
+
const discarded = f.publisher.prepare(0).unwrap();
|
|
169
|
+
discarded.discard();
|
|
170
|
+
const next = f.publish();
|
|
171
|
+
expect(
|
|
172
|
+
next.frame.renderables.find((row) => row.entityKey === f.first)?.transform.world[12],
|
|
173
|
+
).toBe(3);
|
|
174
|
+
f.publisher.dispose();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('publishes shared material changes and their removal, and retires detached dependencies', () => {
|
|
179
|
+
const f = fixture();
|
|
180
|
+
const asset: MaterialAsset = {
|
|
181
|
+
kind: 'material',
|
|
182
|
+
passes: [
|
|
183
|
+
{
|
|
184
|
+
name: 'Forward',
|
|
185
|
+
program: { module: 'forgeax::default-standard-pbr' },
|
|
186
|
+
renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
values: { baseColor: [1, 0, 0, 1] },
|
|
190
|
+
};
|
|
191
|
+
const handle = f.world.allocSharedRef('MaterialAsset', asset);
|
|
192
|
+
f.world.set(f.first, MeshRenderer, { materials: [handle] }).unwrap();
|
|
193
|
+
f.world.set(f.second, MeshRenderer, { materials: [handle] }).unwrap();
|
|
194
|
+
f.publish();
|
|
195
|
+
const content = f.world
|
|
196
|
+
.spawn({
|
|
197
|
+
component: RuntimeMaterialValue,
|
|
198
|
+
data: { asset: handle, parameter: 'baseColor', kind: 2, value: [0, 1, 0, 1] },
|
|
199
|
+
})
|
|
200
|
+
.unwrap();
|
|
201
|
+
const changed = f.publish();
|
|
202
|
+
expect(changed.packet.upserts.length).toBe(4);
|
|
203
|
+
expect(changed.packet.invalidatedAssets).toContain(handle);
|
|
204
|
+
expect(changed.frame.renderables.every((row) => row.material.baseColor[1] === 1)).toBe(true);
|
|
205
|
+
f.world.despawn(content).unwrap();
|
|
206
|
+
expect(f.publish().frame.renderables.every((row) => row.material.baseColor[0] === 1)).toBe(true);
|
|
207
|
+
f.world.despawn(f.first).unwrap();
|
|
208
|
+
f.world.despawn(f.second).unwrap();
|
|
209
|
+
expect(f.publish().packet.retiredAssets).toContain(handle);
|
|
210
|
+
f.publisher.dispose();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('publishes projected mesh buffers and restores their base payload after content removal', () => {
|
|
214
|
+
const f = fixture();
|
|
215
|
+
f.publish();
|
|
216
|
+
const original = resolveAssetHandle<MeshAsset>(f.world, HANDLE_CUBE).unwrap();
|
|
217
|
+
const vertices = new Float32Array(original.vertices);
|
|
218
|
+
vertices[0] = Number(vertices[0]) + 0.25;
|
|
219
|
+
const content = f.world
|
|
220
|
+
.spawn({ component: RuntimeMeshVertices, data: { asset: HANDLE_CUBE, vertices } })
|
|
221
|
+
.unwrap();
|
|
222
|
+
const changed = f.publish();
|
|
223
|
+
expect(changed.packet.invalidatedAssets).toContain(HANDLE_CUBE);
|
|
224
|
+
expect(
|
|
225
|
+
(changed.packet.assets.find((row) => row.handle === HANDLE_CUBE)?.value as MeshAsset)
|
|
226
|
+
.vertices[0],
|
|
227
|
+
).toBe(vertices[0]);
|
|
228
|
+
f.world.despawn(content).unwrap();
|
|
229
|
+
expect(
|
|
230
|
+
(f.publish().packet.assets.find((row) => row.handle === HANDLE_CUBE)?.value as MeshAsset)
|
|
231
|
+
.vertices[0],
|
|
232
|
+
).toBe(original.vertices[0]);
|
|
233
|
+
f.publisher.dispose();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('propagates parent visibility and reuses returned storage without allocation growth', () => {
|
|
237
|
+
const f = fixture();
|
|
238
|
+
f.publish();
|
|
239
|
+
const parent = f.world
|
|
240
|
+
.spawn(
|
|
241
|
+
{ component: Transform, data: {} },
|
|
242
|
+
{ component: Visibility, data: { state: VisibilityStateValue.visible } },
|
|
243
|
+
)
|
|
244
|
+
.unwrap();
|
|
245
|
+
f.world.addComponent(f.first, { component: ChildOf, data: { parent } }).unwrap();
|
|
246
|
+
f.world.update(0).unwrap();
|
|
247
|
+
f.publish();
|
|
248
|
+
f.world.set(parent, Visibility, { state: VisibilityStateValue.hidden }).unwrap();
|
|
249
|
+
expect(f.publish().frame.renderables.some((row) => row.entityKey === f.first)).toBe(false);
|
|
250
|
+
f.world.set(parent, Visibility, { state: VisibilityStateValue.visible }).unwrap();
|
|
251
|
+
f.publish();
|
|
252
|
+
const allocations = f.publisher.inspect().allocations;
|
|
253
|
+
for (let i = 0; i < 12; i++) {
|
|
254
|
+
f.world.set(f.second, Transform, { pos: [i, 0, 0] }).unwrap();
|
|
255
|
+
f.world.update(0).unwrap();
|
|
256
|
+
f.publish();
|
|
257
|
+
}
|
|
258
|
+
expect(f.publisher.inspect().allocations).toBe(allocations);
|
|
259
|
+
f.publisher.dispose();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('ignores unbound geometry vocabulary just like local extraction', () => {
|
|
263
|
+
const f = fixture();
|
|
264
|
+
f.publish();
|
|
265
|
+
f.world.spawn({ component: Points, data: {} }).unwrap();
|
|
266
|
+
const result = f.publisher.prepare(0);
|
|
267
|
+
expect(result.ok).toBe(true);
|
|
268
|
+
if (result.ok) result.value.discard();
|
|
269
|
+
f.publisher.dispose();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it.each([
|
|
273
|
+
10_000, 50_000,
|
|
274
|
+
])('keeps static publication delta and native scanned rows at zero for %i entities', (count) => {
|
|
275
|
+
const f = fixture();
|
|
276
|
+
for (let i = 2; i < count; i++) f.spawn();
|
|
277
|
+
f.world.update(0).unwrap();
|
|
278
|
+
const baseline = f.publish();
|
|
279
|
+
expect(baseline.packet.upserts.length).toBe(count * 2);
|
|
280
|
+
const allocations = f.publisher.inspect().allocations;
|
|
281
|
+
for (let i = 0; i < 3; i++) {
|
|
282
|
+
const next = f.publish();
|
|
283
|
+
expect(
|
|
284
|
+
next.packet.upserts.length +
|
|
285
|
+
next.packet.transforms.length +
|
|
286
|
+
next.packet.removed.length +
|
|
287
|
+
next.packet.assets.length,
|
|
288
|
+
).toBe(0);
|
|
289
|
+
expect(f.publisher.inspect().scannedRows).toBe(0);
|
|
290
|
+
expect(f.publisher.inspect().allocations).toBe(allocations);
|
|
291
|
+
}
|
|
292
|
+
f.publisher.dispose();
|
|
293
|
+
}, 60_000);
|
|
294
|
+
|
|
295
|
+
it('publishes instance poses without detaching source storage and accumulates skipped updates', () => {
|
|
296
|
+
const f = fixture();
|
|
297
|
+
const matrices = new Float32Array(32);
|
|
298
|
+
for (let i = 0; i < 2; i++) for (const j of [0, 5, 10, 15]) matrices[i * 16 + j] = 1;
|
|
299
|
+
f.world.addComponent(f.first, { component: Instances, data: { transforms: matrices } }).unwrap();
|
|
300
|
+
const initial = f.publish();
|
|
301
|
+
expect(
|
|
302
|
+
initial.frame.renderables.find((row) => row.entityKey === f.first)?.instances?.transforms,
|
|
303
|
+
).toEqual(matrices);
|
|
304
|
+
matrices[12] = 0.5;
|
|
305
|
+
f.world.set(f.first, Instances, { transforms: matrices }).unwrap();
|
|
306
|
+
matrices[28] = -0.5;
|
|
307
|
+
f.world.set(f.first, Instances, { transforms: matrices }).unwrap();
|
|
308
|
+
const next = f.publish();
|
|
309
|
+
expect(
|
|
310
|
+
next.frame.renderables.find((row) => row.entityKey === f.first)?.instances?.transforms,
|
|
311
|
+
).toEqual(matrices);
|
|
312
|
+
expect(f.world.get(f.first, Instances).unwrap().transforms.byteLength).toBe(128);
|
|
313
|
+
expect(f.publish().packet.upserts).toHaveLength(0);
|
|
314
|
+
f.publisher.dispose();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('publishes frame-owned environment and post parameters without geometry churn', () => {
|
|
318
|
+
const f = fixture();
|
|
319
|
+
f.publish();
|
|
320
|
+
f.world.spawn({ component: Skylight, data: { intensity: 2 } }).unwrap();
|
|
321
|
+
f.world
|
|
322
|
+
.spawn({
|
|
323
|
+
component: PostProcessParams,
|
|
324
|
+
data: { shader: 'test-post', data: new Uint8Array([1, 2, 3, 4]) },
|
|
325
|
+
})
|
|
326
|
+
.unwrap();
|
|
327
|
+
const next = f.publish();
|
|
328
|
+
expect(next.frame.skylight?.intensity).toBe(2);
|
|
329
|
+
expect(next.frame.postProcessParams.get('test-post')).toEqual(new Uint8Array([1, 2, 3, 4]));
|
|
330
|
+
expect(next.packet.upserts).toHaveLength(0);
|
|
331
|
+
f.publisher.dispose();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('publishes detached skin poses and follows joint changes across skipped source frames', () => {
|
|
335
|
+
const f = fixture();
|
|
336
|
+
const identity = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
337
|
+
const skeleton = f.world.allocSharedRef('SkeletonAsset', {
|
|
338
|
+
kind: 'skeleton',
|
|
339
|
+
jointCount: 1,
|
|
340
|
+
inverseBindMatrices: identity,
|
|
341
|
+
});
|
|
342
|
+
const joint = f.world.spawn({ component: Transform, data: {} }).unwrap();
|
|
343
|
+
f.world
|
|
344
|
+
.addComponent(f.first, {
|
|
345
|
+
component: Skin,
|
|
346
|
+
data: { skeleton, joints: new Uint32Array([joint]) },
|
|
347
|
+
})
|
|
348
|
+
.unwrap();
|
|
349
|
+
f.world.update(0).unwrap();
|
|
350
|
+
const initial = f.publish();
|
|
351
|
+
const pose = initial.packet.templates.find((t) => t.snapshot.skinPose !== undefined)?.snapshot
|
|
352
|
+
.skinPose;
|
|
353
|
+
expect(pose?.jointCount).toBe(1);
|
|
354
|
+
expect(initial.packet.templates.every((t) => t.snapshot.skin === undefined)).toBe(true);
|
|
355
|
+
f.world.set(joint, Transform, { pos: [0.5, 0, 0] }).unwrap();
|
|
356
|
+
f.world.update(1 / 60).unwrap();
|
|
357
|
+
f.world.set(joint, Transform, { pos: [1, 0, 0] }).unwrap();
|
|
358
|
+
f.world.update(1 / 60).unwrap();
|
|
359
|
+
const next = f.publish();
|
|
360
|
+
const nextPose = next.packet.templates.find((t) => t.snapshot.skinPose !== undefined)?.snapshot
|
|
361
|
+
.skinPose;
|
|
362
|
+
expect(nextPose?.jointWorlds[0]?.[12]).toBe(1);
|
|
363
|
+
expect(pose?.jointWorlds[0]?.[12]).toBe(0);
|
|
364
|
+
expect(f.publish().packet.upserts).toHaveLength(0);
|
|
365
|
+
f.world.removeComponent(f.first, Skin).unwrap();
|
|
366
|
+
expect(
|
|
367
|
+
f.publish().frame.renderables.find((row) => row.entityKey === f.first)?.skinPose,
|
|
368
|
+
).toBeUndefined();
|
|
369
|
+
f.publisher.dispose();
|
|
370
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { AssetRegistry, HANDLE_CUBE } from '@forgeax/engine-assets-runtime';
|
|
2
|
+
import { World } from '@forgeax/engine-ecs';
|
|
3
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
4
|
+
import { ShaderRegistry } from '@forgeax/engine-shader';
|
|
5
|
+
import { expect, it } from 'vitest';
|
|
6
|
+
import { createRenderTargetHost } from '../../assembly/render-target-host';
|
|
7
|
+
import { Camera, CubeCamera, MeshFilter, MeshRenderer } from '../../components';
|
|
8
|
+
import type { RenderResult } from '../../render-contract';
|
|
9
|
+
import type { RenderTargetDescriptor } from '../../targets/contracts';
|
|
10
|
+
import { resolveRenderTargetMaterialSource } from '../../targets/material-source';
|
|
11
|
+
import { renderPublicationTransfers } from '../contract';
|
|
12
|
+
import { createRenderPublisher } from '../publisher';
|
|
13
|
+
import { RenderPublicationReceiver } from '../receiver';
|
|
14
|
+
import { RenderPublicationTargetOwner, RenderPublicationTargetReceiver } from '../targets';
|
|
15
|
+
|
|
16
|
+
function unwrap<T, E>(result: RenderResult<T, E>): T {
|
|
17
|
+
if (!result.ok) throw result.error;
|
|
18
|
+
return result.value;
|
|
19
|
+
}
|
|
20
|
+
const descriptor: RenderTargetDescriptor = {
|
|
21
|
+
shape: '2d',
|
|
22
|
+
width: 32,
|
|
23
|
+
height: 32,
|
|
24
|
+
format: 'rgba8unorm-srgb',
|
|
25
|
+
mipLevels: 1,
|
|
26
|
+
sampleCount: 1,
|
|
27
|
+
sampled: true,
|
|
28
|
+
readback: false,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
it('maps auxiliary/cube cameras and distinct material targets into the receiver owner, including resize and retirement', () => {
|
|
32
|
+
const world = new World();
|
|
33
|
+
const assets = new AssetRegistry(new ShaderRegistry({ manifestUrl: undefined }));
|
|
34
|
+
const source = new RenderPublicationTargetOwner();
|
|
35
|
+
const host = createRenderTargetHost();
|
|
36
|
+
const projection = new RenderPublicationTargetReceiver(host);
|
|
37
|
+
const identity = { source: 'target-publication', epoch: 1 };
|
|
38
|
+
const publisher = createRenderPublisher(world, assets, identity, undefined, [], source);
|
|
39
|
+
const receiver = new RenderPublicationReceiver(identity);
|
|
40
|
+
const first = unwrap(source.authoring.createRenderTarget(descriptor));
|
|
41
|
+
const second = unwrap(source.authoring.createRenderTarget(descriptor));
|
|
42
|
+
const cube = unwrap(source.authoring.createRenderTarget({ ...descriptor, shape: 'cube' }));
|
|
43
|
+
const target = world.allocSharedRef('RenderTarget', first);
|
|
44
|
+
const camera = world
|
|
45
|
+
.spawn(
|
|
46
|
+
{ component: Transform, data: { pos: [0, 0, 5] } },
|
|
47
|
+
{ component: Camera, data: { target } },
|
|
48
|
+
)
|
|
49
|
+
.unwrap();
|
|
50
|
+
const cubeEntity = world
|
|
51
|
+
.spawn(
|
|
52
|
+
{ component: Transform, data: {} },
|
|
53
|
+
{ component: CubeCamera, data: { target: world.allocSharedRef('RenderTarget', cube) } },
|
|
54
|
+
)
|
|
55
|
+
.unwrap();
|
|
56
|
+
for (const target of [first, second]) {
|
|
57
|
+
const texture = unwrap(
|
|
58
|
+
source.authoring.createRenderTargetTextureSource(target, {
|
|
59
|
+
aspect: 'color',
|
|
60
|
+
dimension: '2d',
|
|
61
|
+
mipLevel: 0,
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
const textureRef = world.allocSharedRef('RenderTargetTextureSource', texture);
|
|
65
|
+
const material = world.allocSharedRef('MaterialAsset', {
|
|
66
|
+
kind: 'material',
|
|
67
|
+
passes: [
|
|
68
|
+
{
|
|
69
|
+
name: 'Forward',
|
|
70
|
+
program: { module: 'forgeax::default-standard-pbr' },
|
|
71
|
+
renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
values: { baseColorTexture: textureRef },
|
|
75
|
+
});
|
|
76
|
+
world
|
|
77
|
+
.spawn(
|
|
78
|
+
{ component: Transform, data: {} },
|
|
79
|
+
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
80
|
+
{ component: MeshRenderer, data: { materials: [material] } },
|
|
81
|
+
)
|
|
82
|
+
.unwrap();
|
|
83
|
+
}
|
|
84
|
+
world.update(0).unwrap();
|
|
85
|
+
const publish = () => {
|
|
86
|
+
const candidate = publisher.prepare(0).unwrap();
|
|
87
|
+
const packet = structuredClone(candidate.packet);
|
|
88
|
+
candidate.accept();
|
|
89
|
+
const result = projection.apply(receiver.accept(packet).unwrap());
|
|
90
|
+
publisher.recycle(packet.revision, renderPublicationTransfers(packet)).unwrap();
|
|
91
|
+
return result;
|
|
92
|
+
};
|
|
93
|
+
const initial = publish();
|
|
94
|
+
expect(initial.packet.templates).toHaveLength(2);
|
|
95
|
+
expect(initial.packet.targetSources).toHaveLength(2);
|
|
96
|
+
const remote = initial.frame.auxiliaryCameras[0]?.target;
|
|
97
|
+
expect(remote).toBeDefined();
|
|
98
|
+
expect(remote).not.toBe(first);
|
|
99
|
+
expect(host.descriptions().find((row) => row.target === remote)?.descriptor).toEqual(descriptor);
|
|
100
|
+
expect(initial.frame.cubeCameras).toHaveLength(1);
|
|
101
|
+
const bindings = initial.frame.renderables.map((row) => {
|
|
102
|
+
const source = row.material.textureSources?.get('baseColorTexture');
|
|
103
|
+
if (source === undefined) throw new Error('Missing published target source');
|
|
104
|
+
return resolveRenderTargetMaterialSource(source);
|
|
105
|
+
});
|
|
106
|
+
expect(bindings.every((binding) => binding !== undefined)).toBe(true);
|
|
107
|
+
expect(bindings[0]?.target).not.toBe(bindings[1]?.target);
|
|
108
|
+
unwrap(source.authoring.resizeRenderTarget(first, { ...descriptor, width: 64 }));
|
|
109
|
+
const resized = publish();
|
|
110
|
+
expect(resized.frame.auxiliaryCameras[0]?.target).toBe(remote);
|
|
111
|
+
expect(host.descriptions().find((row) => row.target === remote)?.descriptor.width).toBe(64);
|
|
112
|
+
world.despawn(camera).unwrap();
|
|
113
|
+
world.despawn(cubeEntity).unwrap();
|
|
114
|
+
unwrap(source.authoring.destroyRenderTarget(cube));
|
|
115
|
+
publish();
|
|
116
|
+
expect(host.descriptions()).toHaveLength(2);
|
|
117
|
+
publisher.dispose();
|
|
118
|
+
source.dispose();
|
|
119
|
+
host.dispose();
|
|
120
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
2
|
+
import { World } from '@forgeax/engine-ecs';
|
|
3
|
+
import { ShaderRegistry } from '@forgeax/engine-shader';
|
|
4
|
+
import { expect, it } from 'vitest';
|
|
5
|
+
import { createRenderPublisher } from '../publisher';
|
|
6
|
+
import { RenderPublicationReceiver } from '../receiver';
|
|
7
|
+
|
|
8
|
+
it('owns accepted native frames, rejecting duplicates and closed frames before changing revision', () => {
|
|
9
|
+
const identity = { source: 'native-video-lifetime', epoch: 1 };
|
|
10
|
+
const publisher = createRenderPublisher(
|
|
11
|
+
new World(),
|
|
12
|
+
new AssetRegistry(new ShaderRegistry({ manifestUrl: undefined })),
|
|
13
|
+
identity,
|
|
14
|
+
);
|
|
15
|
+
const candidate = publisher.prepare(0).unwrap();
|
|
16
|
+
const receiver = new RenderPublicationReceiver(identity);
|
|
17
|
+
const canvas = new OffscreenCanvas(4, 4);
|
|
18
|
+
canvas.getContext('2d')?.fillRect(0, 0, 4, 4);
|
|
19
|
+
const source = new VideoFrame(canvas, { timestamp: 0 });
|
|
20
|
+
const first = source.clone(),
|
|
21
|
+
next = source.clone(),
|
|
22
|
+
closed = source.clone();
|
|
23
|
+
closed.close();
|
|
24
|
+
const row = { entity: 1, clip: 2, frame: first };
|
|
25
|
+
try {
|
|
26
|
+
expect(
|
|
27
|
+
receiver.accept({ ...candidate.packet, videoFrames: [row, { ...row, frame: next }] }).ok,
|
|
28
|
+
).toBe(false);
|
|
29
|
+
expect(receiver.acceptedRevision).toBe(0);
|
|
30
|
+
const accepted = receiver.accept({ ...candidate.packet, videoFrames: [row] }).unwrap();
|
|
31
|
+
expect(accepted.resources.videoFrame(1, 2)).toBe(first);
|
|
32
|
+
const second = { ...candidate.packet, baseline: false, revision: 2, base: 1 };
|
|
33
|
+
expect(receiver.accept({ ...second, videoFrames: [{ ...row, frame: closed }] }).ok).toBe(false);
|
|
34
|
+
expect(receiver.acceptedRevision).toBe(1);
|
|
35
|
+
expect(first.displayWidth).toBe(4);
|
|
36
|
+
receiver.accept({ ...second, videoFrames: [{ ...row, frame: next }] }).unwrap();
|
|
37
|
+
expect(first.displayWidth).toBe(0);
|
|
38
|
+
receiver.dispose();
|
|
39
|
+
expect(next.displayWidth).toBe(0);
|
|
40
|
+
expect(source.displayWidth).toBe(4);
|
|
41
|
+
} finally {
|
|
42
|
+
receiver.dispose();
|
|
43
|
+
candidate.discard();
|
|
44
|
+
publisher.dispose();
|
|
45
|
+
source.close();
|
|
46
|
+
first.close();
|
|
47
|
+
next.close();
|
|
48
|
+
closed.close();
|
|
49
|
+
}
|
|
50
|
+
});
|