@forgeax/engine-assets-runtime 0.1.3 → 0.1.6
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 +442 -175
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/aabb.unit.test.d.ts +2 -0
- package/dist/__tests__/aabb.unit.test.d.ts.map +1 -0
- package/dist/__tests__/artifact-integrity.unit.test.d.ts +2 -0
- package/dist/__tests__/artifact-integrity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/artifact-io.unit.test.d.ts +2 -0
- package/dist/__tests__/artifact-io.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-kind.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-kind.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-catalog.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-catalog.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.pack-cache.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.pack-cache.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts +2 -0
- package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/catalog-replica.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-replica.unit.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-source-v2.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-source-v2.unit.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-source.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-source.unit.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-subscription-order.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-subscription-order.unit.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-subscription.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-subscription.unit.test.d.ts.map +1 -0
- package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts +2 -0
- package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts.map +1 -0
- package/dist/__tests__/decode-image-bytes.browser.test.d.ts +2 -0
- package/dist/__tests__/decode-image-bytes.browser.test.d.ts.map +1 -0
- package/dist/__tests__/decode-image-bytes.test-d.d.ts +2 -0
- package/dist/__tests__/decode-image-bytes.test-d.d.ts.map +1 -0
- package/dist/__tests__/decode-image-bytes.test.d.ts +2 -0
- package/dist/__tests__/decode-image-bytes.test.d.ts.map +1 -0
- package/dist/__tests__/dynamic-texture-store.unit.test.d.ts +2 -0
- package/dist/__tests__/dynamic-texture-store.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors-asset.unit.test.d.ts +2 -0
- package/dist/__tests__/errors-asset.unit.test.d.ts.map +1 -0
- package/dist/__tests__/inline-pack-loaders.unit.test.d.ts +2 -0
- package/dist/__tests__/inline-pack-loaders.unit.test.d.ts.map +1 -0
- package/dist/__tests__/instantiate-context.unit.test.d.ts +2 -0
- package/dist/__tests__/instantiate-context.unit.test.d.ts.map +1 -0
- package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts +2 -0
- package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts.map +1 -0
- package/dist/__tests__/instantiate.unit.test.d.ts +2 -0
- package/dist/__tests__/instantiate.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loader-input-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/loader-input-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loader-registry.unit.test.d.ts +2 -0
- package/dist/__tests__/loader-registry.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts +2 -0
- package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-generation-cache.unit.test.d.ts +2 -0
- package/dist/__tests__/material-generation-cache.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-load-consumer.test-d.d.ts +2 -0
- package/dist/__tests__/material-load-consumer.test-d.d.ts.map +1 -0
- package/dist/__tests__/material-ready.integration.test.d.ts +2 -0
- package/dist/__tests__/material-ready.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-stale-generation.unit.test.d.ts +2 -0
- package/dist/__tests__/material-stale-generation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mipmap-generator.unit.test.d.ts +2 -0
- package/dist/__tests__/mipmap-generator.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts +2 -0
- package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts.map +1 -0
- package/dist/__tests__/pack-basis-load.integration.test.d.ts +2 -0
- package/dist/__tests__/pack-basis-load.integration.test.d.ts.map +1 -0
- package/dist/__tests__/payload-validate.unit.test.d.ts +2 -0
- package/dist/__tests__/payload-validate.unit.test.d.ts.map +1 -0
- package/dist/__tests__/plugin.test.d.ts +2 -0
- package/dist/__tests__/plugin.test.d.ts.map +1 -0
- package/dist/__tests__/registry-concurrency.bench.test.d.ts +2 -0
- package/dist/__tests__/registry-concurrency.bench.test.d.ts.map +1 -0
- package/dist/__tests__/registry-failure-retry.integration.test.d.ts +2 -0
- package/dist/__tests__/registry-failure-retry.integration.test.d.ts.map +1 -0
- package/dist/__tests__/registry-load-graph.integration.test.d.ts +2 -0
- package/dist/__tests__/registry-load-graph.integration.test.d.ts.map +1 -0
- package/dist/__tests__/registry-load-state.unit.test.d.ts +2 -0
- package/dist/__tests__/registry-load-state.unit.test.d.ts.map +1 -0
- package/dist/__tests__/registry-package-cache.unit.test.d.ts +2 -0
- package/dist/__tests__/registry-package-cache.unit.test.d.ts.map +1 -0
- package/dist/__tests__/registry-public-ready.unit.test.d.ts +2 -0
- package/dist/__tests__/registry-public-ready.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-asset-handle.unit.test.d.ts +2 -0
- package/dist/__tests__/resolve-asset-handle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-handle-fields.unit.test.d.ts +2 -0
- package/dist/__tests__/scene-handle-fields.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-payload.unit.test.d.ts +2 -0
- package/dist/__tests__/scene-payload.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-publication-fence.test.d.ts +2 -0
- package/dist/__tests__/scene-publication-fence.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts.map +1 -0
- package/dist/__tests__/static-rig.integration.test.d.ts +2 -0
- package/dist/__tests__/static-rig.integration.test.d.ts.map +1 -0
- package/dist/__tests__/validate-material.unit.test.d.ts +2 -0
- package/dist/__tests__/validate-material.unit.test.d.ts.map +1 -0
- package/dist/__tests__/video-pack-loader.integration.test.d.ts +2 -0
- package/dist/__tests__/video-pack-loader.integration.test.d.ts.map +1 -0
- package/dist/aabb.d.ts +3 -0
- package/dist/aabb.d.ts.map +1 -0
- package/dist/asset-registry.d.ts +552 -0
- package/dist/asset-registry.d.ts.map +1 -0
- package/dist/builtin-asset-registry.d.ts +22 -0
- package/dist/builtin-asset-registry.d.ts.map +1 -0
- package/dist/catalog-source.d.ts +2 -1
- package/dist/catalog-source.d.ts.map +1 -1
- package/dist/decode-image-bytes.d.ts +57 -0
- package/dist/decode-image-bytes.d.ts.map +1 -0
- package/dist/dynamic-texture-store.d.ts +89 -0
- package/dist/dynamic-texture-store.d.ts.map +1 -0
- package/dist/errors/asset.d.ts +201 -0
- package/dist/errors/asset.d.ts.map +1 -0
- package/dist/handles.d.ts +141 -0
- package/dist/handles.d.ts.map +1 -0
- package/dist/image-error.d.ts +6 -0
- package/dist/image-error.d.ts.map +1 -0
- package/dist/index.d.ts +32 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +7218 -114
- package/dist/index.mjs.map +1 -1
- package/dist/internal.d.ts +2 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.mjs +317 -3
- package/dist/internal.mjs.map +1 -1
- package/dist/loader-registry.d.ts +63 -0
- package/dist/loader-registry.d.ts.map +1 -0
- package/dist/loaders/inline-pack.d.ts +72 -0
- package/dist/loaders/inline-pack.d.ts.map +1 -0
- package/dist/loaders/mesh-bin.d.ts +21 -0
- package/dist/loaders/mesh-bin.d.ts.map +1 -0
- package/dist/loaders/pack-artifact.d.ts +20 -0
- package/dist/loaders/pack-artifact.d.ts.map +1 -0
- package/dist/material/dependency-graph.d.ts +9 -0
- package/dist/material/dependency-graph.d.ts.map +1 -0
- package/dist/material/generation-cache.d.ts +24 -0
- package/dist/material/generation-cache.d.ts.map +1 -0
- package/dist/material/inspection.d.ts +24 -0
- package/dist/material/inspection.d.ts.map +1 -0
- package/dist/material/loader.d.ts +59 -0
- package/dist/material/loader.d.ts.map +1 -0
- package/dist/material/runtime-shader.d.ts +14 -0
- package/dist/material/runtime-shader.d.ts.map +1 -0
- package/dist/mesh-bin.d.ts +3 -0
- package/dist/mesh-bin.d.ts.map +1 -0
- package/dist/mipmap-generator.d.ts +132 -0
- package/dist/mipmap-generator.d.ts.map +1 -0
- package/dist/payload-validate.d.ts +60 -0
- package/dist/payload-validate.d.ts.map +1 -0
- package/dist/plugin.d.ts +16 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/registry/artifact-io.d.ts +16 -0
- package/dist/registry/artifact-io.d.ts.map +1 -0
- package/dist/registry/asset-evidence.d.ts +13 -0
- package/dist/registry/asset-evidence.d.ts.map +1 -0
- package/dist/registry/catalog-state.d.ts +47 -0
- package/dist/registry/catalog-state.d.ts.map +1 -0
- package/dist/registry/catalog.d.ts +48 -0
- package/dist/registry/catalog.d.ts.map +1 -0
- package/dist/registry/instantiate.d.ts +153 -0
- package/dist/registry/instantiate.d.ts.map +1 -0
- package/dist/registry/load-by-guid.d.ts +247 -0
- package/dist/registry/load-by-guid.d.ts.map +1 -0
- package/dist/registry/load-state.d.ts +26 -0
- package/dist/registry/load-state.d.ts.map +1 -0
- package/dist/registry/runtime-projection.d.ts +16 -0
- package/dist/registry/runtime-projection.d.ts.map +1 -0
- package/dist/registry/scene-publication-fence.d.ts +38 -0
- package/dist/registry/scene-publication-fence.d.ts.map +1 -0
- package/dist/registry/validate-material.d.ts +77 -0
- package/dist/registry/validate-material.d.ts.map +1 -0
- package/dist/resolve-asset-handle.d.ts +43 -0
- package/dist/resolve-asset-handle.d.ts.map +1 -0
- package/dist/resolve-tileset-runtime.d.ts +23 -0
- package/dist/resolve-tileset-runtime.d.ts.map +1 -0
- package/dist/scene-handle-fields.d.ts +88 -0
- package/dist/scene-handle-fields.d.ts.map +1 -0
- package/dist/scene-payload.d.ts +15 -0
- package/dist/scene-payload.d.ts.map +1 -0
- package/dist/wire-default-loaders.d.ts +29 -0
- package/dist/wire-default-loaders.d.ts.map +1 -0
- package/package.json +22 -9
- package/src/__tests__/aabb.unit.test.ts +97 -0
- package/src/__tests__/artifact-integrity.unit.test.ts +99 -0
- package/src/__tests__/artifact-io.unit.test.ts +127 -0
- package/src/__tests__/asset-evidence-sdk.integration.test.ts +32 -0
- package/src/__tests__/asset-evidence.unit.test.ts +32 -0
- package/src/__tests__/{asset-kind.test.ts → asset-kind.unit.test.ts} +1 -1
- package/src/__tests__/asset-registry-catalog-reconcile.integration.test.ts +74 -0
- package/src/__tests__/asset-registry-catalog-reconcile.unit.test.ts +89 -0
- package/src/__tests__/asset-registry-catalog.unit.test.ts +170 -0
- package/src/__tests__/asset-registry.pack-cache.test.ts +96 -0
- package/src/__tests__/asset-registry.unit.test.ts +511 -0
- package/src/__tests__/asset-runtime-error-code-owner.test-d.ts +63 -0
- package/src/__tests__/catalog-replica.unit.test.ts +196 -0
- package/src/__tests__/catalog-source-v2.unit.test.ts +158 -0
- package/src/__tests__/catalog-source.unit.test.ts +126 -0
- package/src/__tests__/catalog-subscription-order.unit.test.ts +65 -0
- package/src/__tests__/catalog-subscription.unit.test.ts +28 -0
- package/src/__tests__/decode-image-bytes-pass-through.test.ts +34 -0
- package/src/__tests__/decode-image-bytes.browser.test.ts +102 -0
- package/src/__tests__/decode-image-bytes.test-d.ts +108 -0
- package/src/__tests__/decode-image-bytes.test.ts +90 -0
- package/src/__tests__/decode-image-mime-owner.test.ts +2 -5
- package/src/__tests__/dynamic-texture-store.unit.test.ts +115 -0
- package/src/__tests__/errors-asset.unit.test.ts +92 -0
- package/src/__tests__/inline-pack-loaders.unit.test.ts +548 -0
- package/src/__tests__/instantiate-context.unit.test.ts +65 -0
- package/src/__tests__/instantiate-publication-fence.unit.test.ts +158 -0
- package/src/__tests__/instantiate.unit.test.ts +684 -0
- package/src/__tests__/loader-input-contract.unit.test.ts +89 -0
- package/src/__tests__/loader-registry.unit.test.ts +101 -0
- package/src/__tests__/material-dependency-invalidation.unit.test.ts +14 -0
- package/src/__tests__/material-generation-cache.unit.test.ts +46 -0
- package/src/__tests__/material-load-consumer.test-d.ts +42 -0
- package/src/__tests__/material-ready.integration.test.ts +492 -0
- package/src/__tests__/material-stale-generation.unit.test.ts +152 -0
- package/src/__tests__/mesh-bin-morph-v3.test.ts +22 -0
- package/src/__tests__/mesh-bin.unit.test.ts +152 -0
- package/src/__tests__/mipmap-generator.unit.test.ts +202 -0
- package/src/__tests__/pack-artifact-transcode-model-owner.test.ts +32 -0
- package/src/__tests__/pack-basis-load.integration.test.ts +168 -0
- package/src/__tests__/payload-validate.unit.test.ts +285 -0
- package/src/__tests__/plugin.test.ts +55 -0
- package/src/__tests__/registry-concurrency.bench.test.ts +40 -0
- package/src/__tests__/registry-failure-retry.integration.test.ts +69 -0
- package/src/__tests__/registry-load-graph.integration.test.ts +246 -0
- package/src/__tests__/registry-load-state.unit.test.ts +35 -0
- package/src/__tests__/registry-package-cache.unit.test.ts +53 -0
- package/src/__tests__/registry-public-ready.unit.test.ts +38 -0
- package/src/__tests__/resolve-asset-handle.unit.test.ts +238 -0
- package/src/__tests__/scene-handle-fields.unit.test.ts +129 -0
- package/src/__tests__/scene-payload.unit.test.ts +151 -0
- package/src/__tests__/scene-publication-fence.test.ts +129 -0
- package/src/__tests__/scriptable-pack-public-surface.test-d.ts +16 -0
- package/src/__tests__/static-rig.integration.test.ts +8 -0
- package/src/__tests__/validate-material.unit.test.ts +359 -0
- package/src/__tests__/video-pack-loader.integration.test.ts +83 -0
- package/src/aabb.ts +107 -0
- package/src/asset-registry.ts +2040 -0
- package/src/builtin-asset-registry.ts +164 -0
- package/src/catalog-source.ts +14 -70
- package/src/decode-image-bytes.ts +126 -0
- package/src/dynamic-texture-store.ts +234 -0
- package/src/errors/asset.ts +347 -0
- package/src/handles.ts +180 -0
- package/src/image-error.ts +45 -0
- package/src/index.ts +177 -19
- package/src/internal.ts +2 -0
- package/src/loader-registry.ts +157 -0
- package/src/loaders/inline-pack.ts +837 -0
- package/src/loaders/mesh-bin.ts +304 -0
- package/src/loaders/pack-artifact.ts +665 -0
- package/src/material/dependency-graph.ts +46 -0
- package/src/material/generation-cache.ts +177 -0
- package/src/material/inspection.ts +46 -0
- package/src/material/loader.ts +326 -0
- package/src/material/runtime-shader.ts +133 -0
- package/src/mesh-bin.ts +5 -0
- package/src/mipmap-generator.ts +450 -0
- package/src/payload-validate.ts +573 -0
- package/src/plugin.ts +48 -0
- package/src/registry/artifact-io.ts +259 -0
- package/src/registry/asset-evidence.ts +51 -0
- package/src/registry/catalog-state.ts +409 -0
- package/src/registry/catalog.ts +369 -0
- package/src/registry/instantiate.ts +1056 -0
- package/src/registry/load-by-guid.ts +1657 -0
- package/src/registry/load-state.ts +115 -0
- package/src/registry/runtime-projection.ts +63 -0
- package/src/registry/scene-publication-fence.ts +256 -0
- package/src/registry/validate-material.ts +386 -0
- package/src/resolve-asset-handle.ts +147 -0
- package/src/resolve-tileset-runtime.ts +72 -0
- package/src/scene-handle-fields.ts +221 -0
- package/src/scene-payload.ts +207 -0
- package/src/wire-default-loaders.ts +79 -0
- package/dist/__tests__/asset-graph-red.integration.test.d.ts +0 -2
- package/dist/__tests__/asset-graph-red.integration.test.d.ts.map +0 -1
- package/dist/__tests__/asset-kind.test.d.ts +0 -2
- package/dist/__tests__/asset-kind.test.d.ts.map +0 -1
- package/dist/__tests__/asset-registry-core.integration.test.d.ts +0 -2
- package/dist/__tests__/asset-registry-core.integration.test.d.ts.map +0 -1
- package/dist/__tests__/asset-registry-public-api.test-d.d.ts +0 -2
- package/dist/__tests__/asset-registry-public-api.test-d.d.ts.map +0 -1
- package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts +0 -2
- package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts.map +0 -1
- package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts +0 -2
- package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts.map +0 -1
- package/dist/__tests__/catalog-session-red.unit.test.d.ts +0 -2
- package/dist/__tests__/catalog-session-red.unit.test.d.ts.map +0 -1
- package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts +0 -2
- package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts.map +0 -1
- package/src/__tests__/asset-graph-red.integration.test.ts +0 -113
- package/src/__tests__/asset-registry-core.integration.test.ts +0 -161
- package/src/__tests__/asset-registry-public-api.test-d.ts +0 -31
- package/src/__tests__/asset-runtime-core-lifecycle.integration.test.ts +0 -79
- package/src/__tests__/asset-runtime-snapshot.unit.test.ts +0 -23
- package/src/__tests__/catalog-session-red.unit.test.ts +0 -276
- package/src/__tests__/registry-lifecycle-red.integration.test.ts +0 -80
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// ─── BuiltinAssetRegistry (D-15 process-static tier) ────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// D-15 two-tier asset resolution. Builtin payloads (the 5 procedural meshes
|
|
4
|
+
// the engine ships with) are PROCESS-STATIC: a frozen const keyed by a fixed
|
|
5
|
+
// slot u32 in [1, BUILTIN_BASE). They never enter a World's SharedRefStore,
|
|
6
|
+
// never participate in reference counting, and resolve without any World
|
|
7
|
+
// reference — so a builtin handle (HANDLE_CUBE etc.) is transparent across
|
|
8
|
+
// every World and every renderer.
|
|
9
|
+
//
|
|
10
|
+
// R-13 (module init-order): all five payload consts are constructed
|
|
11
|
+
// synchronously at module load, are module-level, and have no side effect.
|
|
12
|
+
// Importing this module then calling resolve() observes fully-constructed
|
|
13
|
+
// frozen payloads — there is no lazy getter and no circular-dependency
|
|
14
|
+
// init hazard.
|
|
15
|
+
//
|
|
16
|
+
// The companion AssetRegistry (asset-registry.ts) retreats to a thin
|
|
17
|
+
// guid<->handle index; user-tier payloads live in World.sharedRefs at
|
|
18
|
+
// slots >= BUILTIN_BASE. AssetRegistry.get(handle) dispatches by slot range:
|
|
19
|
+
// slot < BUILTIN_BASE -> BuiltinAssetRegistry.resolve, else world.sharedRefs.
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
createBoxGeometry,
|
|
23
|
+
createCylinderGeometry,
|
|
24
|
+
createPlaneGeometry,
|
|
25
|
+
createSphereGeometry,
|
|
26
|
+
meshFromInterleaved,
|
|
27
|
+
} from '@forgeax/engine-geometry';
|
|
28
|
+
import type { Handle, MeshAsset as TypesMeshAsset } from '@forgeax/engine-types';
|
|
29
|
+
import { BUILTIN_BASE, handleSlot } from '@forgeax/engine-types';
|
|
30
|
+
|
|
31
|
+
// D-16: BUILTIN_BASE is the slot boundary [1, BUILTIN_BASE) builtin /
|
|
32
|
+
// [BUILTIN_BASE, +inf) user-tier. It lives in @forgeax/engine-types (the
|
|
33
|
+
// single shared dependency of ecs + runtime) to avoid a cross-package cycle;
|
|
34
|
+
// re-exported from the runtime barrel here for AI-user discoverability.
|
|
35
|
+
export { BUILTIN_BASE };
|
|
36
|
+
|
|
37
|
+
// ─── Builtin geometry data (12F: position + normal + uv + tangent) ──────────
|
|
38
|
+
// bug-20260709-builtin-quad-withoutaabb-disables-sprite-frustum-cu: every
|
|
39
|
+
// builtin geometry payload keeps the local-space AABB produced by its geometry
|
|
40
|
+
// factory. Builtins are ordinary MeshAssets at the render boundary, so removing
|
|
41
|
+
// their bounds silently turns them into always-visible renderables and defeats
|
|
42
|
+
// the same frustum-culling path used by imported meshes.
|
|
43
|
+
|
|
44
|
+
// BUILTIN_CUBE is synthesized from `createBoxGeometry(1, 1, 1)` so the cube
|
|
45
|
+
// inherits Three.js-aligned per-face UV unwrap and per-vertex tangent vec4.
|
|
46
|
+
// The procedural factory always returns Result.ok for valid (>0) extents,
|
|
47
|
+
// hence the unwrap-with-throw is safe at module init.
|
|
48
|
+
const builtinCubeRes = createBoxGeometry(1, 1, 1);
|
|
49
|
+
if (!builtinCubeRes.ok) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`[builtin-asset-registry] createBoxGeometry(1,1,1) failed: ${builtinCubeRes.error.code}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
export const BUILTIN_CUBE: TypesMeshAsset = Object.freeze(builtinCubeRes.value);
|
|
55
|
+
|
|
56
|
+
// BUILTIN_TRIANGLE: 3 vertices in the XY plane facing +Z, with a [0..1]² UV
|
|
57
|
+
// triangle so a textured triangle samples the texture (apex = top-centre,
|
|
58
|
+
// base = bottom-left / bottom-right). meshFromInterleaved expands the
|
|
59
|
+
// 8-floats interleaved input (pos + normal + uv) to the runtime 12-floats
|
|
60
|
+
// stride (adds tangent vec4 per `geometry/tangent.ts` path A).
|
|
61
|
+
const builtinTriangleRes = meshFromInterleaved(
|
|
62
|
+
new Float32Array([
|
|
63
|
+
// pos.xyz normal.xyz uv.xy
|
|
64
|
+
0, 0.7, 0, 0, 0, 1, 0.5, 1, -0.7, -0.6, 0, 0, 0, 1, 0, 0, 0.7, -0.6, 0, 0, 0, 1, 1, 0,
|
|
65
|
+
]),
|
|
66
|
+
new Uint16Array([0, 1, 2]),
|
|
67
|
+
);
|
|
68
|
+
if (!builtinTriangleRes.ok) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`[builtin-asset-registry] builtin triangle geometry failed: ${builtinTriangleRes.error.code}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
export const BUILTIN_TRIANGLE: TypesMeshAsset = Object.freeze(builtinTriangleRes.value);
|
|
74
|
+
|
|
75
|
+
// BUILTIN_QUAD: unit-size plane on XY facing +Z (4 vertices, 2 triangles,
|
|
76
|
+
// 6 indices). Synthesised from `createPlaneGeometry(1, 1)` (which itself
|
|
77
|
+
// chains through `meshFromInterleaved`) so the resulting MeshAsset is
|
|
78
|
+
// byte-identical to procedural plane output — zero new layout
|
|
79
|
+
// discriminator, AI users reason about UV / pivot semantics by reading
|
|
80
|
+
// `packages/runtime/src/geometry/plane.ts` (charter P4 consistent
|
|
81
|
+
// abstraction; feat-20260520 M-1 / w06).
|
|
82
|
+
// bug-20260709-builtin-quad-withoutaabb-disables-sprite-frustum-cu: this
|
|
83
|
+
// payload follows the same local-AABB contract as every other builtin.
|
|
84
|
+
const builtinQuadRes = createPlaneGeometry(1, 1);
|
|
85
|
+
if (!builtinQuadRes.ok) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`[builtin-asset-registry] createPlaneGeometry(1,1) failed: ${builtinQuadRes.error.code}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
export const BUILTIN_QUAD: TypesMeshAsset = Object.freeze(builtinQuadRes.value);
|
|
91
|
+
|
|
92
|
+
// BUILTIN_SPHERE: UV-sphere synthesised from createSphereGeometry(1, 16, 12).
|
|
93
|
+
// Vertices are at exact radius-1 positions (sphere.ts:40-45) so the
|
|
94
|
+
// |hypot(pos)-1| < 1e-6 radius invariant holds by construction. Index
|
|
95
|
+
// buffer is Uint32Array — downstream consumers (step-3 upload loop,
|
|
96
|
+
// createRenderer.ts) auto-select 'uint32' indexFormat via
|
|
97
|
+
// `instanceof Uint32Array`.
|
|
98
|
+
const builtinSphereRes = createSphereGeometry(1, 16, 12);
|
|
99
|
+
if (!builtinSphereRes.ok) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`[builtin-asset-registry] createSphereGeometry(1,16,12) failed: ${builtinSphereRes.error.code}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
export const BUILTIN_SPHERE: TypesMeshAsset = Object.freeze(builtinSphereRes.value);
|
|
105
|
+
|
|
106
|
+
// BUILTIN_CYLINDER: procedural cylinder synthesised from createCylinderGeometry(0.5, 0.5, 1, 16, 1).
|
|
107
|
+
// Radius-top=radius-bottom=0.5, height=1, 16 radial segments, 1 height segment — a
|
|
108
|
+
// unit-height open cylinder (no caps). meshFromInterleaved expands the 8-floats
|
|
109
|
+
// interleaved input (pos + normal + uv) to the runtime 12-floats stride (adds
|
|
110
|
+
// tangent vec4 per geometry/tangent.ts path A).
|
|
111
|
+
// feat-20260701-editor-world-container-doc-ecs-collapse M0 / AC-16:
|
|
112
|
+
// handle=6, GUID = deriveBuiltin('HANDLE_CYLINDER') UUIDv5
|
|
113
|
+
// (plan-strategy §2 D-6 + §5.6 builtin-guid-ssot gate)
|
|
114
|
+
const builtinCylinderRes = createCylinderGeometry(0.5, 0.5, 1, 16, 1);
|
|
115
|
+
if (!builtinCylinderRes.ok) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`[builtin-asset-registry] createCylinderGeometry(0.5,0.5,1,16,1) failed: ${builtinCylinderRes.error.code}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
export const BUILTIN_CYLINDER: TypesMeshAsset = Object.freeze(builtinCylinderRes.value);
|
|
121
|
+
|
|
122
|
+
// BUILTIN_NINESLICE_QUAD: 4×4 grid plane synthesised from
|
|
123
|
+
// createPlaneGeometry(1, 1, 3, 3) — 16 vertices, 9 sub-quads × 6 indices = 54.
|
|
124
|
+
// Reuses the unit-quad vertex layout (12F: pos + normal + uv + tangent) so
|
|
125
|
+
// HANDLE_NINESLICE_QUAD funnels through the same sprite-pipeline binding
|
|
126
|
+
// table as HANDLE_QUAD; only the vertex_index → (i, j) decomposition in
|
|
127
|
+
// sprite.wgsl branches on slices presence (feat-20260527-sprite-nineslice
|
|
128
|
+
// M3, plan-strategy §D-2 + §D-4).
|
|
129
|
+
const builtinNineSliceQuadRes = createPlaneGeometry(1, 1, 3, 3);
|
|
130
|
+
if (!builtinNineSliceQuadRes.ok) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`[builtin-asset-registry] createPlaneGeometry(1,1,3,3) failed: ${builtinNineSliceQuadRes.error.code}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
export const BUILTIN_NINESLICE_QUAD: TypesMeshAsset = Object.freeze(builtinNineSliceQuadRes.value);
|
|
136
|
+
|
|
137
|
+
// Slot u32 -> frozen payload. Slot ids 1..5 are the fixed builtin handle
|
|
138
|
+
// values (HANDLE_CUBE=1 .. HANDLE_NINESLICE_QUAD=5) defined in asset-registry.
|
|
139
|
+
const BUILTIN_BY_SLOT: ReadonlyMap<number, TypesMeshAsset> = new Map([
|
|
140
|
+
[1, BUILTIN_CUBE],
|
|
141
|
+
[2, BUILTIN_TRIANGLE],
|
|
142
|
+
[3, BUILTIN_QUAD],
|
|
143
|
+
[4, BUILTIN_SPHERE],
|
|
144
|
+
[5, BUILTIN_NINESLICE_QUAD],
|
|
145
|
+
[6, BUILTIN_CYLINDER],
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Process-static builtin asset resolver (D-15). `resolve(handle)` returns the
|
|
150
|
+
* frozen builtin payload when `slot < BUILTIN_BASE`, else `null` (the slot
|
|
151
|
+
* belongs to the user tier — resolve it through `world.sharedRefs.resolve`).
|
|
152
|
+
*
|
|
153
|
+
* Read-only: there is no register / mint surface. The five builtin payloads
|
|
154
|
+
* are fixed at module load. AI users `import { BuiltinAssetRegistry } from
|
|
155
|
+
* '@forgeax/engine-runtime'` and resolve a builtin handle without a World.
|
|
156
|
+
*/
|
|
157
|
+
export const BuiltinAssetRegistry = Object.freeze({
|
|
158
|
+
resolve<T extends TypesMeshAsset>(handle: Handle<string, 'shared'>): T | null {
|
|
159
|
+
const slot = handleSlot(handle);
|
|
160
|
+
if (slot >= BUILTIN_BASE) return null;
|
|
161
|
+
const payload = BUILTIN_BY_SLOT.get(slot);
|
|
162
|
+
return payload === undefined ? null : (payload as T);
|
|
163
|
+
},
|
|
164
|
+
});
|
package/src/catalog-source.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import { err, ok, type Result } from '@forgeax/engine-rhi';
|
|
1
2
|
import {
|
|
2
3
|
ASSET_ERROR_HINTS,
|
|
3
4
|
AssetError,
|
|
4
5
|
type CatalogDelta,
|
|
5
6
|
type CatalogEntry,
|
|
6
|
-
err,
|
|
7
|
-
ok,
|
|
8
7
|
type ResourceRevision,
|
|
9
|
-
type Result,
|
|
10
8
|
type RuntimeAssetBinding,
|
|
11
9
|
} from '@forgeax/engine-types';
|
|
10
|
+
import { fetchCatalog } from './registry/catalog';
|
|
11
|
+
|
|
12
12
|
export type CatalogListener = (delta: CatalogDelta) => void;
|
|
13
13
|
|
|
14
14
|
/** Read-only catalog source backed by static entries or a canonical URL. */
|
|
@@ -69,33 +69,17 @@ export function createCatalogSource(options: {
|
|
|
69
69
|
}),
|
|
70
70
|
);
|
|
71
71
|
}
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
let raw: unknown;
|
|
84
|
-
try {
|
|
85
|
-
raw = await response.json();
|
|
86
|
-
} catch {
|
|
87
|
-
return err(
|
|
88
|
-
new AssetError({
|
|
89
|
-
code: 'asset-parse-failed',
|
|
90
|
-
expected: 'the configured Catalog URL to return JSON',
|
|
91
|
-
hint: 'repair the producer Catalog before loading the current publication',
|
|
92
|
-
}),
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
const rows = scopedRows(raw, options.expectedScope);
|
|
96
|
-
if (!rows.ok) return rows;
|
|
97
|
-
const result = rows.value.map((row) => ({ ...row }));
|
|
98
|
-
return ok(result);
|
|
72
|
+
const result = await fetchCatalog(
|
|
73
|
+
options.url,
|
|
74
|
+
options.fetch ?? globalThis.fetch,
|
|
75
|
+
undefined,
|
|
76
|
+
options.expectedRevision,
|
|
77
|
+
options.expectedScope,
|
|
78
|
+
);
|
|
79
|
+
if (!result.ok) return result;
|
|
80
|
+
return ok(
|
|
81
|
+
[...result.value].map(([guid, entry]) => ({ guid, ...entry })) as readonly CatalogEntry[],
|
|
82
|
+
);
|
|
99
83
|
},
|
|
100
84
|
subscribe(listener) {
|
|
101
85
|
return options.subscribe?.(listener) ?? (() => {});
|
|
@@ -103,43 +87,3 @@ export function createCatalogSource(options: {
|
|
|
103
87
|
...(options.expectedScope === undefined ? {} : { expectedScope: options.expectedScope }),
|
|
104
88
|
};
|
|
105
89
|
}
|
|
106
|
-
|
|
107
|
-
function scopedRows(
|
|
108
|
-
raw: unknown,
|
|
109
|
-
expectedScope: Pick<RuntimeAssetBinding, 'scopeId' | 'generation'> | undefined,
|
|
110
|
-
): Result<readonly CatalogEntry[], AssetError> {
|
|
111
|
-
if (Array.isArray(raw)) return ok(raw as readonly CatalogEntry[]);
|
|
112
|
-
if (raw === null || typeof raw !== 'object') return parseRowsError('Catalog JSON rows');
|
|
113
|
-
const snapshot = raw as Record<string, unknown>;
|
|
114
|
-
if (!Array.isArray(snapshot.entries)) return parseRowsError('Catalog JSON entries');
|
|
115
|
-
if (expectedScope !== undefined) {
|
|
116
|
-
if (
|
|
117
|
-
snapshot.scopeId !== expectedScope.scopeId ||
|
|
118
|
-
snapshot.generation !== expectedScope.generation ||
|
|
119
|
-
snapshot.authority !== 'authoritative'
|
|
120
|
-
) {
|
|
121
|
-
return err(
|
|
122
|
-
new AssetError({
|
|
123
|
-
code: 'asset-parse-failed',
|
|
124
|
-
expected: 'the Catalog snapshot to match the active scope and generation',
|
|
125
|
-
hint: 'restore the authoritative producer Catalog for the active runtime scope',
|
|
126
|
-
detail: {
|
|
127
|
-
expectedScope,
|
|
128
|
-
actualScope: { scopeId: snapshot.scopeId, generation: snapshot.generation },
|
|
129
|
-
} as never,
|
|
130
|
-
}),
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return ok(snapshot.entries as readonly CatalogEntry[]);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function parseRowsError(expected: string): Result<never, AssetError> {
|
|
138
|
-
return err(
|
|
139
|
-
new AssetError({
|
|
140
|
-
code: 'asset-parse-failed',
|
|
141
|
-
expected,
|
|
142
|
-
hint: 'repair the producer Catalog before loading the current publication',
|
|
143
|
-
}),
|
|
144
|
-
);
|
|
145
|
-
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// @forgeax/engine-assets-runtime -- runtime image byte decoder
|
|
2
|
+
// (tweak-20260714-runtime-image-bytes-decoder-add-decodeimagebytes M2 / m2-1).
|
|
3
|
+
//
|
|
4
|
+
// M1 established the signature + TSDoc contract + red test skeleton. M2
|
|
5
|
+
// fills the body:
|
|
6
|
+
// 1. mime whitelist pre-check (PNG / JPEG); non-match -> structured
|
|
7
|
+
// `image-format-unsupported` err (charter P3 explicit failure).
|
|
8
|
+
// 2. Delegate to `@forgeax/engine-image::decodeImageInBrowser` (SSOT
|
|
9
|
+
// browser-safe decoder; charter architecture principle 1 SSOT).
|
|
10
|
+
// 3. Map `DecodedImage` -> `TextureAsset` POD; `format` derives from
|
|
11
|
+
// `opts.colorSpace` (srgb -> 'rgba8unorm-srgb' / linear ->
|
|
12
|
+
// 'rgba8unorm', mirrors `image-importer.ts` colorSpaceToFormat); when
|
|
13
|
+
// `opts.mipmap !== false`, `mipLevelCount` derives from
|
|
14
|
+
// `numMipLevels({width,height})` (charter Derive-Don't-Duplicate).
|
|
15
|
+
//
|
|
16
|
+
// decode-image-bytes.ts is the SINGLE assets-runtime file allowed to
|
|
17
|
+
// statically import @forgeax/engine-image (charter D-2; the
|
|
18
|
+
// scripts/check-image-pipeline-isolation.mjs Path (a.2-anti) whitelist
|
|
19
|
+
// row anchors this exact path). The gate's (a.1) forbidden-symbol regex
|
|
20
|
+
// `\basync\s+function\s+decodeImage\b` does NOT match `decodeImageBytes`
|
|
21
|
+
// because `\b` between `e` and `B` fails (both are word chars) --
|
|
22
|
+
// verified at M2 by running the gate locally.
|
|
23
|
+
import { decodeImageInBrowser } from '@forgeax/engine-image';
|
|
24
|
+
import type { ImageError, Result, TextureAsset } from '@forgeax/engine-types';
|
|
25
|
+
import { err, ok } from '@forgeax/engine-types';
|
|
26
|
+
import { makeImageError } from './image-error';
|
|
27
|
+
import { numMipLevels } from './mipmap-generator';
|
|
28
|
+
|
|
29
|
+
/** v1 mime whitelist -- PNG / JPEG only (charter P1 progressive disclosure). */
|
|
30
|
+
const SUPPORTED_MIMES = ['image/png', 'image/jpeg'] as const;
|
|
31
|
+
type SupportedMime = (typeof SUPPORTED_MIMES)[number];
|
|
32
|
+
|
|
33
|
+
function isSupportedMime(mime: string): mime is SupportedMime {
|
|
34
|
+
return SUPPORTED_MIMES.some((supportedMime) => supportedMime === mime);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Decode a PNG / JPEG byte stream into a `TextureAsset` POD.
|
|
39
|
+
*
|
|
40
|
+
* Runtime SDK entry for AI users who hold image bytes (fetched over the
|
|
41
|
+
* network, embedded as base64, produced by an out-of-tree decoder, etc.) and
|
|
42
|
+
* want to feed them into `world.allocSharedRef('TextureAsset', pod)` +
|
|
43
|
+
* `GpuResourceStore.ensureResident` without the disk-side importer / build
|
|
44
|
+
* pipeline in the loop (charter P1 progressive disclosure: one-step "bytes
|
|
45
|
+
* in, POD out").
|
|
46
|
+
*
|
|
47
|
+
* v1 supports the two universally-decodable mime types: `image/png` and
|
|
48
|
+
* `image/jpeg`. Failure surfaces are all structured via the closed
|
|
49
|
+
* `ImageError` union (`.code` / `.expected` / `.hint` / `.detail`); the
|
|
50
|
+
* function never throws for expected failures (AGENTS.md Error model +
|
|
51
|
+
* charter P3 explicit failure).
|
|
52
|
+
*
|
|
53
|
+
* Error codes possible (subset of the closed `ImageErrorCode` union):
|
|
54
|
+
* - `image-format-unsupported` -- mime not in the v1 whitelist (`detail.actualMime`)
|
|
55
|
+
* - `image-decode-failed` -- decoder rejected the bytes, or the environment
|
|
56
|
+
* lacks `createImageBitmap` (`detail.reason`)
|
|
57
|
+
*
|
|
58
|
+
* @param bytes - Encoded image byte stream (`Uint8Array` or `ArrayBuffer`).
|
|
59
|
+
* @param mime - Byte-stream mime type. v1 whitelist: `'image/png' | 'image/jpeg'`.
|
|
60
|
+
* @param opts - Optional decode overrides:
|
|
61
|
+
* - `colorSpace`: `'srgb'` (default) or `'linear'`. Derives POD `format`:
|
|
62
|
+
* `srgb -> 'rgba8unorm-srgb'`, `linear -> 'rgba8unorm'`.
|
|
63
|
+
* - `mipmap`: `true` (default) or `false`. When true, `mipLevelCount` is
|
|
64
|
+
* computed via `numMipLevels({ width, height })`; when false, forced 1.
|
|
65
|
+
* @returns `ok(TextureAsset)` on success; `err(ImageError)` on failure.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* import { decodeImageBytes } from '@forgeax/engine-assets-runtime';
|
|
70
|
+
*
|
|
71
|
+
* const bytes = new Uint8Array(await (await fetch(url)).arrayBuffer());
|
|
72
|
+
* const result = await decodeImageBytes(bytes, 'image/png');
|
|
73
|
+
* if (!result.ok) {
|
|
74
|
+
* // The ImageError code and detail are one correlated discriminated union.
|
|
75
|
+
* switch (result.error.code) {
|
|
76
|
+
* case 'image-format-unsupported':
|
|
77
|
+
* console.error('bad mime:', result.error.detail.actualMime);
|
|
78
|
+
* break;
|
|
79
|
+
* case 'image-decode-failed':
|
|
80
|
+
* console.error('decode reason:', result.error.detail.reason);
|
|
81
|
+
* break;
|
|
82
|
+
* }
|
|
83
|
+
* return;
|
|
84
|
+
* }
|
|
85
|
+
* const handle = world.allocSharedRef('TextureAsset', result.value);
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export async function decodeImageBytes(
|
|
89
|
+
bytes: Uint8Array | ArrayBuffer,
|
|
90
|
+
mime: string,
|
|
91
|
+
opts: { colorSpace?: 'srgb' | 'linear'; mipmap?: boolean } = {},
|
|
92
|
+
): Promise<Result<TextureAsset, ImageError>> {
|
|
93
|
+
if (!isSupportedMime(mime)) {
|
|
94
|
+
return err(
|
|
95
|
+
makeImageError({
|
|
96
|
+
code: 'image-format-unsupported',
|
|
97
|
+
actualMime: mime,
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const colorSpace = opts.colorSpace ?? 'srgb';
|
|
103
|
+
const mipmap = opts.mipmap ?? true;
|
|
104
|
+
|
|
105
|
+
const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
106
|
+
const decoded = await decodeImageInBrowser(u8, mime, { colorSpace, mipmap });
|
|
107
|
+
if (!decoded.ok) {
|
|
108
|
+
return err(decoded.error);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const dec = decoded.value;
|
|
112
|
+
const format: GPUTextureFormat = colorSpace === 'srgb' ? 'rgba8unorm-srgb' : 'rgba8unorm';
|
|
113
|
+
const mipLevelCount = mipmap ? numMipLevels({ width: dec.width, height: dec.height }) : 1;
|
|
114
|
+
|
|
115
|
+
const pod: TextureAsset = {
|
|
116
|
+
kind: 'texture',
|
|
117
|
+
width: dec.width,
|
|
118
|
+
height: dec.height,
|
|
119
|
+
format,
|
|
120
|
+
data: dec.bytes,
|
|
121
|
+
colorSpace,
|
|
122
|
+
mipmap,
|
|
123
|
+
mipLevelCount,
|
|
124
|
+
};
|
|
125
|
+
return ok(pod);
|
|
126
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// @forgeax/engine-assets-runtime — DynamicTextureStore: transient per-frame GPU
|
|
2
|
+
// textures for video sources (feat-20260623-world-space-video-asset M4 / w15).
|
|
3
|
+
//
|
|
4
|
+
// D-3: video frames are re-uploaded every frame — the opposite of the static
|
|
5
|
+
// "upload once / cache forever" semantics GpuResourceStore.ensureResident
|
|
6
|
+
// implements. Routing video through ensureResident would either poison its
|
|
7
|
+
// permanent cache (AC-08) or crash it (its `switch (pod.kind)` has no `video`
|
|
8
|
+
// arm). So video gets its OWN store with a transient lifecycle, exactly the way
|
|
9
|
+
// cube-texture upload is an eager independent path that never enters the
|
|
10
|
+
// ensureResident switch (research Finding 7 precedent).
|
|
11
|
+
//
|
|
12
|
+
// This store does NOT import GpuResourceStore and accepts NO static texture
|
|
13
|
+
// handle — it is keyed solely by the VideoAsset clip handle. Each key owns a
|
|
14
|
+
// single GPUTexture sized to the source video; the texture is recreated only
|
|
15
|
+
// when the source dimensions change (a steady-size clip allocates once and is
|
|
16
|
+
// re-written in place every frame via copyExternalImageToTexture). The view is
|
|
17
|
+
// re-fetched after each upload so the bind group always samples the latest
|
|
18
|
+
// frame.
|
|
19
|
+
//
|
|
20
|
+
// Charter P3: every failure returns a structured Result (never throws for an
|
|
21
|
+
// expected GPU failure); the per-frame caller (record stage videoTextureView)
|
|
22
|
+
// fires the RhiError on the engine error channel and falls back to the default
|
|
23
|
+
// view for that frame.
|
|
24
|
+
|
|
25
|
+
import type { Result, RhiDevice, RhiError, Texture, TextureView } from '@forgeax/engine-rhi';
|
|
26
|
+
import { ok } from '@forgeax/engine-rhi';
|
|
27
|
+
import type { Handle } from '@forgeax/engine-types';
|
|
28
|
+
import { handleSlot } from '@forgeax/engine-types';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The host-owned external image the engine copies a video frame from
|
|
32
|
+
* (HTMLVideoElement / VideoFrame / ImageBitmap / canvas). Mirrors the `source`
|
|
33
|
+
* member of the WebGPU `GPUCopyExternalImageSourceInfo` the RHI
|
|
34
|
+
* `copyExternalImageToTexture` consumes; aliased here so the store does not pull
|
|
35
|
+
* the whole webgpu descriptor type into its surface.
|
|
36
|
+
*/
|
|
37
|
+
export type CopyExternalImageSource = GPUCopyExternalImageSourceInfo['source'];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* GPUTextureUsage bits a video destination texture needs: COPY_DST (the
|
|
41
|
+
* copyExternalImageToTexture write target), TEXTURE_BINDING (sampled in the
|
|
42
|
+
* material bind group), and RENDER_ATTACHMENT (required by the WebGPU spec for
|
|
43
|
+
* a copyExternalImageToTexture destination). Mirrors the literals
|
|
44
|
+
* gpu-resource-store.ts uses for its upload textures (no shared const exists to
|
|
45
|
+
* import without coupling the two stores — D-3 keeps them independent).
|
|
46
|
+
*/
|
|
47
|
+
const VIDEO_TEXTURE_USAGE = 0x2 | 0x4 | 0x10;
|
|
48
|
+
|
|
49
|
+
/** rgba8unorm-srgb: a video frame decodes to sRGB; sampling returns linear. */
|
|
50
|
+
const VIDEO_TEXTURE_FORMAT = 'rgba8unorm-srgb' as const;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The minimal RHI device surface DynamicTextureStore needs. Declared structurally
|
|
54
|
+
* (not the full RhiDevice) so unit tests drive it with a small mock and the store
|
|
55
|
+
* stays decoupled from the rest of the device surface (Pipeline Isolation).
|
|
56
|
+
*/
|
|
57
|
+
export interface DynamicTextureDevice {
|
|
58
|
+
createTexture(desc: {
|
|
59
|
+
readonly size: {
|
|
60
|
+
readonly width: number;
|
|
61
|
+
readonly height: number;
|
|
62
|
+
readonly depthOrArrayLayers: number;
|
|
63
|
+
};
|
|
64
|
+
readonly format: typeof VIDEO_TEXTURE_FORMAT;
|
|
65
|
+
readonly usage: number;
|
|
66
|
+
readonly label?: string;
|
|
67
|
+
}): Result<Texture, RhiError>;
|
|
68
|
+
createTextureView(texture: Texture, desc: Record<string, never>): Result<TextureView, RhiError>;
|
|
69
|
+
destroyTexture(texture: Texture): Result<void, RhiError>;
|
|
70
|
+
readonly queue: {
|
|
71
|
+
copyExternalImageToTexture(
|
|
72
|
+
source: { readonly source: CopyExternalImageSource; readonly flipY?: boolean },
|
|
73
|
+
destination: { readonly texture: Texture },
|
|
74
|
+
copySize: {
|
|
75
|
+
readonly width: number;
|
|
76
|
+
readonly height: number;
|
|
77
|
+
readonly depthOrArrayLayers: number;
|
|
78
|
+
},
|
|
79
|
+
): Result<void, RhiError>;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Adapt the opaque RHI device to the store's intentionally small upload seam. */
|
|
84
|
+
export function adaptDynamicTextureDevice(device: RhiDevice): DynamicTextureDevice {
|
|
85
|
+
return {
|
|
86
|
+
createTexture: (descriptor) =>
|
|
87
|
+
device.createTexture({
|
|
88
|
+
...descriptor,
|
|
89
|
+
mipLevelCount: undefined,
|
|
90
|
+
sampleCount: undefined,
|
|
91
|
+
dimension: undefined,
|
|
92
|
+
viewFormats: undefined,
|
|
93
|
+
textureBindingViewDimension: undefined,
|
|
94
|
+
}),
|
|
95
|
+
createTextureView: (texture) =>
|
|
96
|
+
device.createTextureView(texture, {
|
|
97
|
+
label: undefined,
|
|
98
|
+
format: undefined,
|
|
99
|
+
dimension: undefined,
|
|
100
|
+
usage: undefined,
|
|
101
|
+
aspect: undefined,
|
|
102
|
+
baseMipLevel: undefined,
|
|
103
|
+
mipLevelCount: undefined,
|
|
104
|
+
baseArrayLayer: undefined,
|
|
105
|
+
arrayLayerCount: undefined,
|
|
106
|
+
}),
|
|
107
|
+
destroyTexture: (texture) => device.destroyTexture(texture),
|
|
108
|
+
queue: {
|
|
109
|
+
copyExternalImageToTexture: (source, destination, copySize) =>
|
|
110
|
+
device.queue.copyExternalImageToTexture(
|
|
111
|
+
{ source: source.source, origin: [0, 0], flipY: source.flipY ?? false },
|
|
112
|
+
{ texture: destination.texture, origin: [0, 0, 0] },
|
|
113
|
+
copySize,
|
|
114
|
+
),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface TransientEntry {
|
|
120
|
+
texture: Texture;
|
|
121
|
+
view: TextureView;
|
|
122
|
+
width: number;
|
|
123
|
+
height: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Transient per-frame texture store for video sources. Independent of
|
|
128
|
+
* GpuResourceStore: it neither imports nor reaches into the static residency
|
|
129
|
+
* cache (AC-08 / D-3).
|
|
130
|
+
*/
|
|
131
|
+
export class DynamicTextureStore {
|
|
132
|
+
private device: DynamicTextureDevice | undefined = undefined;
|
|
133
|
+
private readonly entries = new Map<number, TransientEntry>();
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Wire the GPU device the store uploads through. Called once after the
|
|
137
|
+
* renderer captures its device (mirrors GpuResourceStore.configureGpuDevice).
|
|
138
|
+
* A replacement device invalidates every cached texture: the handles in the
|
|
139
|
+
* map belong to the old device and cannot be reused after renderer recovery.
|
|
140
|
+
*/
|
|
141
|
+
configureGpuDevice(device: DynamicTextureDevice): void {
|
|
142
|
+
if (this.device !== undefined && this.device !== device) {
|
|
143
|
+
this.destroyAll();
|
|
144
|
+
}
|
|
145
|
+
this.device = device;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Upload one video frame for `clip` from the host-owned source image
|
|
150
|
+
* (HTMLVideoElement / VideoFrame / ImageBitmap), (re)allocating the transient
|
|
151
|
+
* texture when its size changes, and return the current-frame view to bind.
|
|
152
|
+
*
|
|
153
|
+
* Returns `undefined` (not an error) when the device is not yet wired or the
|
|
154
|
+
* source has no decodable dimensions yet (metadata pending) — the caller binds
|
|
155
|
+
* the default view that frame. A structured RhiError surfaces only when a wired
|
|
156
|
+
* device rejects the allocation or the copy (charter P3).
|
|
157
|
+
*/
|
|
158
|
+
uploadFrame(
|
|
159
|
+
clip: Handle<'VideoAsset', 'shared'>,
|
|
160
|
+
source: CopyExternalImageSource,
|
|
161
|
+
width: number,
|
|
162
|
+
height: number,
|
|
163
|
+
): Result<TextureView, RhiError> | undefined {
|
|
164
|
+
const device = this.device;
|
|
165
|
+
if (device === undefined) return undefined;
|
|
166
|
+
if (width <= 0 || height <= 0) return undefined;
|
|
167
|
+
|
|
168
|
+
const id = handleSlot(clip);
|
|
169
|
+
const ensured = this.ensureEntry(device, id, width, height);
|
|
170
|
+
if (!ensured.ok) return ensured;
|
|
171
|
+
const entry = ensured.value;
|
|
172
|
+
|
|
173
|
+
const copyRes = device.queue.copyExternalImageToTexture(
|
|
174
|
+
{ source, flipY: true },
|
|
175
|
+
{ texture: entry.texture },
|
|
176
|
+
{ width, height, depthOrArrayLayers: 1 },
|
|
177
|
+
);
|
|
178
|
+
if (!copyRes.ok) return copyRes;
|
|
179
|
+
return ok(entry.view);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The current-frame view for a clip, if one has been uploaded this session,
|
|
184
|
+
* else undefined. The record stage reads this when assembling the bind group
|
|
185
|
+
* (a frame that has not uploaded yet falls back to the default view).
|
|
186
|
+
*/
|
|
187
|
+
getView(clip: Handle<'VideoAsset', 'shared'>): TextureView | undefined {
|
|
188
|
+
return this.entries.get(handleSlot(clip))?.view;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Destroy every transient texture + drop the map (renderer teardown). */
|
|
192
|
+
destroyAll(): void {
|
|
193
|
+
const device = this.device;
|
|
194
|
+
for (const entry of this.entries.values()) {
|
|
195
|
+
device?.destroyTexture(entry.texture);
|
|
196
|
+
}
|
|
197
|
+
this.entries.clear();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get the entry for `id`, (re)allocating its texture + view when absent or
|
|
202
|
+
* when the source dimensions changed. A same-size re-upload reuses the
|
|
203
|
+
* existing texture (allocate-once for a steady clip; the per-frame cost is the
|
|
204
|
+
* copyExternalImageToTexture write, not a texture create).
|
|
205
|
+
*/
|
|
206
|
+
private ensureEntry(
|
|
207
|
+
device: DynamicTextureDevice,
|
|
208
|
+
id: number,
|
|
209
|
+
width: number,
|
|
210
|
+
height: number,
|
|
211
|
+
): Result<TransientEntry, RhiError> {
|
|
212
|
+
const existing = this.entries.get(id);
|
|
213
|
+
if (existing !== undefined && existing.width === width && existing.height === height) {
|
|
214
|
+
return ok(existing);
|
|
215
|
+
}
|
|
216
|
+
if (existing !== undefined) device.destroyTexture(existing.texture);
|
|
217
|
+
|
|
218
|
+
const texRes = device.createTexture({
|
|
219
|
+
size: { width, height, depthOrArrayLayers: 1 },
|
|
220
|
+
format: VIDEO_TEXTURE_FORMAT,
|
|
221
|
+
usage: VIDEO_TEXTURE_USAGE,
|
|
222
|
+
label: `video-transient-${id}`,
|
|
223
|
+
});
|
|
224
|
+
if (!texRes.ok) return texRes;
|
|
225
|
+
const viewRes = device.createTextureView(texRes.value, {});
|
|
226
|
+
if (!viewRes.ok) {
|
|
227
|
+
device.destroyTexture(texRes.value);
|
|
228
|
+
return viewRes;
|
|
229
|
+
}
|
|
230
|
+
const entry: TransientEntry = { texture: texRes.value, view: viewRes.value, width, height };
|
|
231
|
+
this.entries.set(id, entry);
|
|
232
|
+
return ok(entry);
|
|
233
|
+
}
|
|
234
|
+
}
|