@forgeax/engine-render 0.1.6 → 0.1.7
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 +139 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts.map +1 -1
- package/dist/assembly/factory.d.ts +7 -0
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/authoring.d.ts +1 -0
- package/dist/authoring.d.ts.map +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-QC4VW3XJ.mjs → chunk-HCOGPHKS.mjs} +17 -3
- package/dist/chunk-HCOGPHKS.mjs.map +1 -0
- package/dist/chunk-YEJIUW7D.mjs +1152 -0
- package/dist/chunk-YEJIUW7D.mjs.map +1 -0
- package/dist/components/__tests__/points-lines-admission.unit.test.d.ts +2 -0
- package/dist/components/__tests__/points-lines-admission.unit.test.d.ts.map +1 -0
- package/dist/components/__tests__/points-lines-schema.unit.test.d.ts +2 -0
- package/dist/components/__tests__/points-lines-schema.unit.test.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/lines.d.ts +5 -0
- package/dist/components/lines.d.ts.map +1 -0
- package/dist/components/points.d.ts +15 -0
- package/dist/components/points.d.ts.map +1 -0
- package/dist/construct-renderer.mjs +952 -497
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/errors/render.d.ts +82 -1
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/features/render-graph-raster.d.ts +5 -1
- package/dist/features/render-graph-raster.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +4 -2
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +3 -0
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/pbr-pipeline.d.ts +2 -0
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/points-lines/__tests__/bounds.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/bounds.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts +2 -0
- package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/inspection.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/inspection.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts +2 -0
- package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/points-lines.bench.d.ts +2 -0
- package/dist/points-lines/__tests__/points-lines.bench.d.ts.map +1 -0
- package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts +2 -0
- package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/snapshot.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/snapshot.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts.map +1 -0
- package/dist/points-lines/admission.d.ts +36 -0
- package/dist/points-lines/admission.d.ts.map +1 -0
- package/dist/points-lines/bounds.d.ts +8 -0
- package/dist/points-lines/bounds.d.ts.map +1 -0
- package/dist/points-lines/expansion-cache.d.ts +28 -0
- package/dist/points-lines/expansion-cache.d.ts.map +1 -0
- package/dist/points-lines/inspection.d.ts +53 -0
- package/dist/points-lines/inspection.d.ts.map +1 -0
- package/dist/points-lines/prepare.d.ts +67 -0
- package/dist/points-lines/prepare.d.ts.map +1 -0
- package/dist/points-lines/record.d.ts +60 -0
- package/dist/points-lines/record.d.ts.map +1 -0
- package/dist/points-lines/snapshot.d.ts +49 -0
- package/dist/points-lines/snapshot.d.ts.map +1 -0
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame.d.ts +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +7 -0
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +17 -0
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/record/view-ubo.d.ts +6 -0
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/render-contract.d.ts +10 -2
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +3 -0
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +4 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene-types.d.ts +3 -0
- package/dist/scene/render-scene-types.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +7 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/package.json +19 -19
- package/src/__tests__/authoring-path.unit.test.ts +9 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +12 -2
- package/src/__tests__/material-cooked-projection.unit.test.ts +29 -7
- package/src/__tests__/material-static-dynamic-route.unit.test.ts +28 -28
- package/src/__tests__/render-error-code-owner.test-d.ts +6 -0
- package/src/__tests__/render-error-exhaustive.test-d.ts +19 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +38 -0
- package/src/assembly/factory.ts +72 -3
- package/src/assembly/material/assembly.ts +2 -5
- package/src/authoring.ts +1 -0
- package/src/components/__tests__/points-lines-admission.unit.test.ts +219 -0
- package/src/components/__tests__/points-lines-schema.unit.test.ts +56 -0
- package/src/components/index.ts +7 -0
- package/src/components/lines.ts +18 -0
- package/src/components/points.ts +40 -0
- package/src/errors/render.ts +144 -1
- package/src/features/render-graph-raster.ts +22 -3
- package/src/gpu-driven/production-raster.ts +6 -1
- package/src/index.ts +17 -0
- package/src/inspection-types.ts +4 -0
- package/src/pbr-pipeline.ts +8 -1
- package/src/plugin.ts +4 -0
- package/src/points-lines/__tests__/bounds.unit.test.ts +21 -0
- package/src/points-lines/__tests__/cache-recovery.unit.test.ts +157 -0
- package/src/points-lines/__tests__/inspection-lanes.integration.test.ts +146 -0
- package/src/points-lines/__tests__/inspection.unit.test.ts +173 -0
- package/src/points-lines/__tests__/lane-structural.integration.test.ts +122 -0
- package/src/points-lines/__tests__/points-lines.bench.ts +400 -0
- package/src/points-lines/__tests__/raster-lane.integration.test.ts +44 -0
- package/src/points-lines/__tests__/snapshot.unit.test.ts +101 -0
- package/src/points-lines/__tests__/view-ubo.unit.test.ts +73 -0
- package/src/points-lines/admission.ts +238 -0
- package/src/points-lines/bounds.ts +23 -0
- package/src/points-lines/expansion-cache.ts +215 -0
- package/src/points-lines/inspection.ts +97 -0
- package/src/points-lines/prepare.ts +177 -0
- package/src/points-lines/record.ts +125 -0
- package/src/points-lines/snapshot.ts +130 -0
- package/src/record/frame-lighting.ts +14 -2
- package/src/record/frame.ts +12 -1
- package/src/record/main-pass-geometry.ts +92 -0
- package/src/record/main-pass-sprite-draws.ts +1 -1
- package/src/record/main-pass.ts +6 -3
- package/src/record/render-context.ts +20 -0
- package/src/record/shadow-pass.ts +16 -5
- package/src/record/skybox-post-pass.ts +2 -1
- package/src/record/typed-frame-graph.ts +17 -16
- package/src/record/view-ubo.ts +47 -0
- package/src/render-contract.ts +13 -2
- package/src/render-system-extract.ts +71 -8
- package/src/render-system.ts +304 -2
- package/src/scene/render-scene-types.ts +3 -0
- package/src/scene/render-scene.ts +57 -2
- package/dist/chunk-QC4VW3XJ.mjs.map +0 -1
- package/dist/chunk-SCRSAK5B.mjs +0 -480
- package/dist/chunk-SCRSAK5B.mjs.map +0 -1
package/src/render-system.ts
CHANGED
|
@@ -27,12 +27,18 @@ import {
|
|
|
27
27
|
type Handle,
|
|
28
28
|
type MaterialAsset,
|
|
29
29
|
type MaterialTextureValue,
|
|
30
|
+
type MeshAsset,
|
|
30
31
|
type RenderPipelineAsset,
|
|
31
32
|
RenderQueue,
|
|
32
33
|
type SamplerAsset,
|
|
34
|
+
toShared,
|
|
33
35
|
} from '@forgeax/engine-types';
|
|
34
36
|
import { createClusterBinScratch } from './cluster-binner';
|
|
35
|
-
import
|
|
37
|
+
import {
|
|
38
|
+
type ObservationUnavailableError,
|
|
39
|
+
PointsLinesMaterialUnsupportedError,
|
|
40
|
+
type RenderError,
|
|
41
|
+
} from './errors/render';
|
|
36
42
|
import {
|
|
37
43
|
type RenderFeaturePreparedGraphicsResolverInput,
|
|
38
44
|
runRenderFeatureFrame,
|
|
@@ -51,10 +57,30 @@ import {
|
|
|
51
57
|
} from './fullscreen-post-process-pass';
|
|
52
58
|
import { GpuDrivenProduction } from './gpu-driven/production-raster';
|
|
53
59
|
import type { RenderSceneInspection } from './inspection-types';
|
|
60
|
+
import { admitPointsLines } from './points-lines/admission';
|
|
61
|
+
import { PointsLinesExpansionCache } from './points-lines/expansion-cache';
|
|
62
|
+
import {
|
|
63
|
+
inspectPointsLines,
|
|
64
|
+
type PointsLinesInspection,
|
|
65
|
+
type PointsLinesSourceError,
|
|
66
|
+
} from './points-lines/inspection';
|
|
67
|
+
import {
|
|
68
|
+
createPointsLinesLanePreparationAdapter,
|
|
69
|
+
type PointsLinesLanePreparationAdapter,
|
|
70
|
+
} from './points-lines/prepare';
|
|
71
|
+
import { createPointsLinesLaneAdapter, type PointsLinesBackend } from './points-lines/record';
|
|
72
|
+
import type { PointsLinesRetainedSnapshot } from './points-lines/snapshot';
|
|
73
|
+
import type { PointsLinesRecordOwner, PointsLinesRecordSubmission } from './record/render-context';
|
|
54
74
|
|
|
55
75
|
export type { RenderSceneInspection } from './inspection-types';
|
|
56
76
|
|
|
57
|
-
import {
|
|
77
|
+
import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
|
|
78
|
+
import {
|
|
79
|
+
GPU_BUFFER_USAGE_COPY_DST,
|
|
80
|
+
GPU_BUFFER_USAGE_INDEX,
|
|
81
|
+
GPU_BUFFER_USAGE_UNIFORM,
|
|
82
|
+
GPU_BUFFER_USAGE_VERTEX,
|
|
83
|
+
} from './gpu-usage';
|
|
58
84
|
import { assembleMaterialWithSkylightEntries } from './ibl/skylight-bind-group';
|
|
59
85
|
import { disposeInstanceBuffers, disposeTransientInstanceBuffers } from './instance-buffer-cache';
|
|
60
86
|
import type { MeshMaterialBindingObservation } from './mesh-material-bindings';
|
|
@@ -283,6 +309,8 @@ export interface RenderSystem {
|
|
|
283
309
|
readonly visibilityStats: { explicitlyHidden: number };
|
|
284
310
|
/** Persistent scene maintenance evidence from the ordinary single-World path. */
|
|
285
311
|
readonly renderScene: RenderSceneInspection;
|
|
312
|
+
/** Retained Points/Lines authoring facts from the single scene projection. */
|
|
313
|
+
readonly pointsLinesSnapshots: readonly PointsLinesRetainedSnapshot[];
|
|
286
314
|
/** Current mesh-slot provenance and active diagnostics from the last frame. */
|
|
287
315
|
readonly meshMaterialBindings: readonly MeshMaterialBindingObservation[];
|
|
288
316
|
/**
|
|
@@ -398,6 +426,273 @@ function makePreparedPipelinePendingError(): RhiError {
|
|
|
398
426
|
});
|
|
399
427
|
}
|
|
400
428
|
|
|
429
|
+
type PointsLinesGpuResources = {
|
|
430
|
+
readonly vertexBuffer: Buffer;
|
|
431
|
+
readonly indexBuffer: Buffer;
|
|
432
|
+
};
|
|
433
|
+
type PointsLinesPreparationAdapter = PointsLinesLanePreparationAdapter<PointsLinesGpuResources>;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* The Standard renderer's single Points/Lines prepare owner.
|
|
437
|
+
*
|
|
438
|
+
* The retained snapshot is the identity boundary; this owner only resolves
|
|
439
|
+
* the source assets, runs admission, and publishes the existing preparation
|
|
440
|
+
* and record contracts to the main geometry loop. The expansion cache is
|
|
441
|
+
* shared across lanes and this owner keeps the one prepared vertex/index
|
|
442
|
+
* resource pair for each retained identity.
|
|
443
|
+
*/
|
|
444
|
+
class StandardPointsLinesOwner implements PointsLinesRecordOwner {
|
|
445
|
+
private readonly cache = new PointsLinesExpansionCache();
|
|
446
|
+
private readonly preparations = new Map<string, PointsLinesPreparationAdapter>();
|
|
447
|
+
private readonly active = new Map<string, PointsLinesInspection>();
|
|
448
|
+
private readonly layoutProjection = deriveVertexLayoutProjection({
|
|
449
|
+
position: new Float32Array(0),
|
|
450
|
+
normal: new Float32Array(0),
|
|
451
|
+
uv: new Float32Array(0),
|
|
452
|
+
tangent: new Float32Array(0),
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
constructor(private readonly internals: RenderSystemInternals) {}
|
|
456
|
+
|
|
457
|
+
beginFrame(): void {
|
|
458
|
+
this.active.clear();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
prepare(
|
|
462
|
+
entry: import('./record/frame-snapshot').ValidatedRenderable,
|
|
463
|
+
clustered: boolean,
|
|
464
|
+
): PointsLinesRecordSubmission | undefined {
|
|
465
|
+
const snapshot = entry.source.pointsLines;
|
|
466
|
+
if (snapshot === undefined || snapshot.component === undefined || entry.world === undefined) {
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
const key = `${snapshot.worldId}:${snapshot.entityKey}`;
|
|
470
|
+
const topology = snapshot.component === 'Points' ? 'point-list' : 'line-list';
|
|
471
|
+
const backend = this.backend();
|
|
472
|
+
const lane = backend === 'wgpu-webgl2' ? 'cpu-webgl2' : clustered ? 'clustered' : 'direct';
|
|
473
|
+
const meshResult = resolveAssetHandle<MeshAsset>(
|
|
474
|
+
entry.world,
|
|
475
|
+
toShared<'MeshAsset'>(entry.source.assetHandle),
|
|
476
|
+
);
|
|
477
|
+
const materialResult = resolveAssetHandle<MaterialAsset>(
|
|
478
|
+
entry.world,
|
|
479
|
+
toShared<'MaterialAsset'>(snapshot.materialHandle),
|
|
480
|
+
);
|
|
481
|
+
if (!meshResult.ok || !materialResult.ok) {
|
|
482
|
+
this.publishRefusal(
|
|
483
|
+
snapshot,
|
|
484
|
+
new PointsLinesMaterialUnsupportedError({
|
|
485
|
+
entity: snapshot.entityKey,
|
|
486
|
+
material: 'unresolved',
|
|
487
|
+
pass: 'forward',
|
|
488
|
+
module: 'asset-resolution',
|
|
489
|
+
reason: 'source MeshAsset or MaterialAsset could not be resolved',
|
|
490
|
+
}),
|
|
491
|
+
);
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
const admission = admitPointsLines({
|
|
495
|
+
entity: snapshot.entityKey,
|
|
496
|
+
mesh: meshResult.value,
|
|
497
|
+
material: materialResult.value,
|
|
498
|
+
...(snapshot.style?.kind === 'points'
|
|
499
|
+
? {
|
|
500
|
+
points: {
|
|
501
|
+
sizePx: snapshot.style.sizePx,
|
|
502
|
+
shape: snapshot.style.shape === 'circle' ? 1 : 0,
|
|
503
|
+
},
|
|
504
|
+
}
|
|
505
|
+
: { lines: { widthPx: snapshot.style?.widthPx ?? 1 } }),
|
|
506
|
+
});
|
|
507
|
+
if (!admission.ok) {
|
|
508
|
+
this.publishRefusal(snapshot, admission.error);
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
const geometryKey = this.cache.getOrCreate(snapshot, meshResult.value).key;
|
|
512
|
+
const prepKey = `${geometryKey}:${lane}:${backend}`;
|
|
513
|
+
let preparation = this.preparations.get(prepKey);
|
|
514
|
+
if (preparation === undefined) {
|
|
515
|
+
preparation = createPointsLinesLanePreparationAdapter(lane, backend, {
|
|
516
|
+
cache: this.cache,
|
|
517
|
+
adapter: {
|
|
518
|
+
create: (geometry) => {
|
|
519
|
+
const vertex = this.internals.device.createBuffer({
|
|
520
|
+
label: `points-lines-vertices:${geometryKey}`,
|
|
521
|
+
size: Math.max(4, geometry.vertices.byteLength),
|
|
522
|
+
usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
523
|
+
mappedAtCreation: false,
|
|
524
|
+
});
|
|
525
|
+
if (!vertex.ok) return err(vertex.error);
|
|
526
|
+
const index = this.internals.device.createBuffer({
|
|
527
|
+
label: `points-lines-indices:${geometryKey}`,
|
|
528
|
+
size: Math.max(4, geometry.indices.byteLength),
|
|
529
|
+
usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
530
|
+
mappedAtCreation: false,
|
|
531
|
+
});
|
|
532
|
+
if (!index.ok) {
|
|
533
|
+
this.internals.device.destroyBuffer(vertex.value);
|
|
534
|
+
return err(index.error);
|
|
535
|
+
}
|
|
536
|
+
return ok({ vertexBuffer: vertex.value, indexBuffer: index.value });
|
|
537
|
+
},
|
|
538
|
+
upload: (resource, geometry) => {
|
|
539
|
+
if (geometry.vertices.byteLength > 0) {
|
|
540
|
+
const vertexWrite = this.internals.device.queue.writeBuffer(
|
|
541
|
+
resource.vertexBuffer,
|
|
542
|
+
0,
|
|
543
|
+
geometry.vertices,
|
|
544
|
+
);
|
|
545
|
+
if (!vertexWrite.ok) return err(vertexWrite.error);
|
|
546
|
+
}
|
|
547
|
+
if (geometry.indices.byteLength > 0) {
|
|
548
|
+
const indexWrite = this.internals.device.queue.writeBuffer(
|
|
549
|
+
resource.indexBuffer,
|
|
550
|
+
0,
|
|
551
|
+
geometry.indices,
|
|
552
|
+
);
|
|
553
|
+
if (!indexWrite.ok) return err(indexWrite.error);
|
|
554
|
+
}
|
|
555
|
+
return ok(geometry.derivedBytes);
|
|
556
|
+
},
|
|
557
|
+
validate: (_resource, geometry) =>
|
|
558
|
+
geometry.expandedVertexCount > 0 && geometry.expandedIndexCount > 0
|
|
559
|
+
? ok(undefined)
|
|
560
|
+
: err(new Error('Points/Lines expansion contains no drawable triangles')),
|
|
561
|
+
destroy: (resource) => {
|
|
562
|
+
this.internals.device.destroyBuffer(resource.vertexBuffer);
|
|
563
|
+
this.internals.device.destroyBuffer(resource.indexBuffer);
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
this.preparations.set(prepKey, preparation);
|
|
568
|
+
}
|
|
569
|
+
const prepared = preparation.prepare(snapshot, meshResult.value);
|
|
570
|
+
if (!prepared.ok) {
|
|
571
|
+
const lkg = preparation.lastKnownGood();
|
|
572
|
+
if (lkg !== undefined) {
|
|
573
|
+
const lkgPlan = createPointsLinesLaneAdapter(lane, backend).createRecordPlan(
|
|
574
|
+
lkg.snapshot,
|
|
575
|
+
lkg.geometry,
|
|
576
|
+
);
|
|
577
|
+
const state = preparation.inspect();
|
|
578
|
+
this.active.set(
|
|
579
|
+
key,
|
|
580
|
+
inspectPointsLines({
|
|
581
|
+
snapshot,
|
|
582
|
+
topology,
|
|
583
|
+
lane,
|
|
584
|
+
pointCount: lkg.geometry.pointCount,
|
|
585
|
+
segmentCount: lkg.geometry.segmentCount,
|
|
586
|
+
sourceBytes: lkg.geometry.sourceBytes,
|
|
587
|
+
derivedBytes: lkg.geometry.derivedBytes,
|
|
588
|
+
cache: { hit: true, rebuilds: state.rebuilds, evictions: 0 },
|
|
589
|
+
drawCount: lkgPlan.drawCount,
|
|
590
|
+
uploadBytes: 0,
|
|
591
|
+
lastKnownGood: true,
|
|
592
|
+
refusal: {
|
|
593
|
+
code: prepared.error.code,
|
|
594
|
+
expected: prepared.error.expected,
|
|
595
|
+
hint: prepared.error.hint,
|
|
596
|
+
detail: prepared.error.detail,
|
|
597
|
+
generation: prepared.error.detail.generation,
|
|
598
|
+
lastKnownGood: true,
|
|
599
|
+
},
|
|
600
|
+
}),
|
|
601
|
+
);
|
|
602
|
+
return {
|
|
603
|
+
plan: lkgPlan,
|
|
604
|
+
vertexBuffer: lkg.resource.vertexBuffer,
|
|
605
|
+
indexBuffer: lkg.resource.indexBuffer,
|
|
606
|
+
layoutProjection: this.layoutProjection,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
this.publishRefusal(snapshot, prepared.error);
|
|
610
|
+
return undefined;
|
|
611
|
+
}
|
|
612
|
+
const plan = createPointsLinesLaneAdapter(lane, backend).createRecordPlan(
|
|
613
|
+
snapshot,
|
|
614
|
+
prepared.value.geometry,
|
|
615
|
+
);
|
|
616
|
+
const state = preparation.inspect();
|
|
617
|
+
this.active.set(
|
|
618
|
+
key,
|
|
619
|
+
inspectPointsLines({
|
|
620
|
+
snapshot,
|
|
621
|
+
topology,
|
|
622
|
+
lane,
|
|
623
|
+
pointCount: prepared.value.geometry.pointCount,
|
|
624
|
+
segmentCount: prepared.value.geometry.segmentCount,
|
|
625
|
+
sourceBytes: prepared.value.geometry.sourceBytes,
|
|
626
|
+
derivedBytes: prepared.value.geometry.derivedBytes,
|
|
627
|
+
cache: {
|
|
628
|
+
hit: prepared.value.uploadedBytes === 0,
|
|
629
|
+
rebuilds: state.rebuilds,
|
|
630
|
+
evictions: 0,
|
|
631
|
+
},
|
|
632
|
+
drawCount: plan.drawCount,
|
|
633
|
+
uploadBytes: prepared.value.uploadedBytes,
|
|
634
|
+
lastKnownGood: prepared.value.lastKnownGood,
|
|
635
|
+
}),
|
|
636
|
+
);
|
|
637
|
+
return {
|
|
638
|
+
plan,
|
|
639
|
+
vertexBuffer: prepared.value.resource.vertexBuffer,
|
|
640
|
+
indexBuffer: prepared.value.resource.indexBuffer,
|
|
641
|
+
layoutProjection: this.layoutProjection,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
resetForDeviceLoss(): void {
|
|
646
|
+
for (const preparation of this.preparations.values()) preparation.resetForDeviceLoss();
|
|
647
|
+
this.active.clear();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
inspections(): readonly PointsLinesInspection[] {
|
|
651
|
+
return [...this.active.values()];
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private backend(): PointsLinesBackend {
|
|
655
|
+
switch (this.internals.device.caps.backendKind) {
|
|
656
|
+
case 'wgpu-webgl2':
|
|
657
|
+
return 'wgpu-webgl2';
|
|
658
|
+
case 'null':
|
|
659
|
+
return 'null';
|
|
660
|
+
default:
|
|
661
|
+
return 'webgpu';
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private publishRefusal(
|
|
666
|
+
snapshot: PointsLinesRetainedSnapshot,
|
|
667
|
+
error: PointsLinesSourceError,
|
|
668
|
+
): void {
|
|
669
|
+
this.active.set(
|
|
670
|
+
`${snapshot.worldId}:${snapshot.entityKey}`,
|
|
671
|
+
inspectPointsLines({
|
|
672
|
+
snapshot,
|
|
673
|
+
topology: snapshot.component === 'Points' ? 'point-list' : 'line-list',
|
|
674
|
+
lane: 'refused',
|
|
675
|
+
pointCount: 0,
|
|
676
|
+
segmentCount: 0,
|
|
677
|
+
sourceBytes: 0,
|
|
678
|
+
derivedBytes: 0,
|
|
679
|
+
cache: { hit: false, rebuilds: 0, evictions: 0 },
|
|
680
|
+
drawCount: 0,
|
|
681
|
+
uploadBytes: 0,
|
|
682
|
+
lastKnownGood: false,
|
|
683
|
+
refusal: {
|
|
684
|
+
code: error.code,
|
|
685
|
+
expected: error.expected,
|
|
686
|
+
hint: error.hint,
|
|
687
|
+
detail: error.detail,
|
|
688
|
+
generation: snapshot.meshGeneration,
|
|
689
|
+
lastKnownGood: false,
|
|
690
|
+
},
|
|
691
|
+
}),
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
401
696
|
export function createRenderSystem(internals: RenderSystemInternals): RenderSystem {
|
|
402
697
|
const phaseCatalogRegistration = internals.profiler?.registerPhaseCatalog(
|
|
403
698
|
'render',
|
|
@@ -406,6 +701,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
|
|
|
406
701
|
let releaseProfilerCatalog =
|
|
407
702
|
phaseCatalogRegistration?.ok === true ? phaseCatalogRegistration.value : undefined;
|
|
408
703
|
let preparedWorlds: readonly World[] = [];
|
|
704
|
+
const pointsLinesOwner = new StandardPointsLinesOwner(internals);
|
|
409
705
|
const persistentRenderScene = new PersistentRenderScene({
|
|
410
706
|
getDevice: () => internals.device,
|
|
411
707
|
onGpuError: (error) => internals.errorRegistry.fire(error),
|
|
@@ -1071,6 +1367,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
|
|
|
1071
1367
|
gpuDriven: gpuDrivenProduction.inspect(),
|
|
1072
1368
|
};
|
|
1073
1369
|
},
|
|
1370
|
+
get pointsLinesSnapshots(): readonly PointsLinesRetainedSnapshot[] {
|
|
1371
|
+
return persistentRenderScene.pointsLinesSnapshots();
|
|
1372
|
+
},
|
|
1074
1373
|
detachScene(world: World): void {
|
|
1075
1374
|
persistentRenderScene.detach(world);
|
|
1076
1375
|
},
|
|
@@ -1091,6 +1390,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
|
|
|
1091
1390
|
}
|
|
1092
1391
|
}
|
|
1093
1392
|
try {
|
|
1393
|
+
pointsLinesOwner.beginFrame();
|
|
1094
1394
|
// cameraOwner drives the surfaced cameras + frustum
|
|
1095
1395
|
// cull; resourceOwner drives skylight/skybox/postProcess + per-world
|
|
1096
1396
|
// record config.
|
|
@@ -1333,8 +1633,10 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
|
|
|
1333
1633
|
},
|
|
1334
1634
|
renderReadLeases,
|
|
1335
1635
|
featureGraphCandidate,
|
|
1636
|
+
pointsLinesOwner,
|
|
1336
1637
|
),
|
|
1337
1638
|
);
|
|
1639
|
+
persistentRenderScene.setPointsLinesInspections(pointsLinesOwner.inspections());
|
|
1338
1640
|
if (internals.featureHost !== undefined && preparedResourceBatches.length > 0) {
|
|
1339
1641
|
const batches = preparedResourceBatches;
|
|
1340
1642
|
if (submitted) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RenderSceneResyncReason } from '../inspection-types';
|
|
2
|
+
import type { PointsLinesRetainedSnapshot } from '../points-lines/snapshot';
|
|
2
3
|
import type { RenderableSnapshot } from '../render-system-extract';
|
|
3
4
|
|
|
4
5
|
export type { RenderSceneResyncReason } from '../inspection-types';
|
|
@@ -23,6 +24,8 @@ export interface RenderSceneRecord extends RenderSceneIdentity {
|
|
|
23
24
|
|
|
24
25
|
export interface RenderSceneSlot extends RenderSceneRecord {
|
|
25
26
|
readonly snapshot: RenderableSnapshot;
|
|
27
|
+
/** Optional detached projection carried with the retained render slot. */
|
|
28
|
+
readonly pointsLines?: PointsLinesRetainedSnapshot;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export interface RenderSceneApplyResult {
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
} from '../gpu-driven/batch-topology';
|
|
30
30
|
import { GpuScene } from '../gpu-scene';
|
|
31
31
|
import type { PersistentRenderSceneInspection } from '../inspection-types';
|
|
32
|
+
import type { PointsLinesInspection } from '../points-lines/inspection';
|
|
33
|
+
import type { PointsLinesRetainedSnapshot } from '../points-lines/snapshot';
|
|
32
34
|
import type { CameraSnapshot } from '../render-contract';
|
|
33
35
|
import type {
|
|
34
36
|
DispatchEntry,
|
|
@@ -95,6 +97,21 @@ function ownSnapshot(snapshot: RenderableSnapshot): RenderableSnapshot {
|
|
|
95
97
|
regions: new Float32Array(snapshot.spriteInstances.regions),
|
|
96
98
|
},
|
|
97
99
|
}),
|
|
100
|
+
...(snapshot.pointsLines === undefined
|
|
101
|
+
? {}
|
|
102
|
+
: { pointsLines: ownPointsLinesSnapshot(snapshot.pointsLines) }),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ownPointsLinesSnapshot(
|
|
107
|
+
snapshot: PointsLinesRetainedSnapshot,
|
|
108
|
+
): PointsLinesRetainedSnapshot {
|
|
109
|
+
return {
|
|
110
|
+
...snapshot,
|
|
111
|
+
style: snapshot.style === undefined ? undefined : { ...snapshot.style },
|
|
112
|
+
sourceBounds: new Float32Array(snapshot.sourceBounds),
|
|
113
|
+
viewport: { ...snapshot.viewport },
|
|
114
|
+
projection: new Float32Array(snapshot.projection),
|
|
98
115
|
};
|
|
99
116
|
}
|
|
100
117
|
|
|
@@ -156,6 +173,10 @@ export class RenderScene {
|
|
|
156
173
|
private readonly freeSlots: number[] = [];
|
|
157
174
|
private readonly slotsByWorld = new Map<number, Map<number, number>>();
|
|
158
175
|
private readonly slotsByMaterial = new Map<number, Set<number>>();
|
|
176
|
+
// World bounds are a pure projection of each immutable snapshot. Cache the
|
|
177
|
+
// derived value by snapshot identity so repeated view queries do not redo
|
|
178
|
+
// the eight-corner transform for every view in the same frame.
|
|
179
|
+
private readonly worldBoundsCache = new WeakMap<RenderableSnapshot, RenderSceneBounds | null>();
|
|
159
180
|
private orderedSlots: number[] = [];
|
|
160
181
|
private materialized: readonly RenderableSnapshot[] | undefined;
|
|
161
182
|
private slotSnapshot: readonly RenderSceneSlot[] | undefined;
|
|
@@ -365,6 +386,18 @@ export class RenderScene {
|
|
|
365
386
|
return this.lookup(worldId, entityKey)?.snapshot;
|
|
366
387
|
}
|
|
367
388
|
|
|
389
|
+
pointsLinesSnapshots(): readonly PointsLinesRetainedSnapshot[] {
|
|
390
|
+
const snapshots: PointsLinesRetainedSnapshot[] = [];
|
|
391
|
+
for (const slot of this.orderedSlots) {
|
|
392
|
+
const record = this.slots[slot];
|
|
393
|
+
const snapshot = record?.snapshot.pointsLines;
|
|
394
|
+
if (record !== undefined && snapshot !== undefined) {
|
|
395
|
+
snapshots.push(ownPointsLinesSnapshot({ ...snapshot, worldId: record.worldId }));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return Object.freeze(snapshots);
|
|
399
|
+
}
|
|
400
|
+
|
|
368
401
|
slotsForMaterial(materialHandle: number): readonly RenderSceneSlot[] {
|
|
369
402
|
const slots = this.slotsByMaterial.get(materialHandle);
|
|
370
403
|
if (slots === undefined) return [];
|
|
@@ -381,8 +414,14 @@ export class RenderScene {
|
|
|
381
414
|
for (const slot of this.orderedSlots) {
|
|
382
415
|
const record = this.slots[slot];
|
|
383
416
|
if (record === undefined) continue;
|
|
384
|
-
const
|
|
385
|
-
|
|
417
|
+
const snapshot = record.snapshot;
|
|
418
|
+
let candidate = this.worldBoundsCache.get(snapshot);
|
|
419
|
+
if (candidate === undefined && !this.worldBoundsCache.has(snapshot)) {
|
|
420
|
+
candidate = worldBounds(snapshot) ?? null;
|
|
421
|
+
this.worldBoundsCache.set(snapshot, candidate);
|
|
422
|
+
}
|
|
423
|
+
if (candidate === null || candidate === undefined) continue;
|
|
424
|
+
if (intersects(candidate, bounds)) records.push(record);
|
|
386
425
|
}
|
|
387
426
|
return records;
|
|
388
427
|
}
|
|
@@ -654,6 +693,7 @@ export class PersistentRenderScene {
|
|
|
654
693
|
private gpuDevice: RhiDevice | undefined;
|
|
655
694
|
private gpuStatus: 'inactive' | 'unsupported' | 'resident' | 'rebuild-pending' | 'error' =
|
|
656
695
|
'inactive';
|
|
696
|
+
private pointsLinesInspections: readonly PointsLinesInspection[] = [];
|
|
657
697
|
|
|
658
698
|
constructor(private readonly options: PersistentRenderSceneOptions = {}) {}
|
|
659
699
|
|
|
@@ -841,6 +881,14 @@ export class PersistentRenderScene {
|
|
|
841
881
|
this.gpuStatus = this.options.getDevice === undefined ? 'inactive' : 'rebuild-pending';
|
|
842
882
|
}
|
|
843
883
|
|
|
884
|
+
setPointsLinesInspections(inspections: readonly PointsLinesInspection[]): void {
|
|
885
|
+
this.pointsLinesInspections = inspections.map((inspection) => ({
|
|
886
|
+
...inspection,
|
|
887
|
+
cache: { ...inspection.cache },
|
|
888
|
+
...(inspection.refusal === undefined ? {} : { refusal: { ...inspection.refusal } }),
|
|
889
|
+
}));
|
|
890
|
+
}
|
|
891
|
+
|
|
844
892
|
dispose(): void {
|
|
845
893
|
this.gpuScene?.dispose();
|
|
846
894
|
this.gpuScene = undefined;
|
|
@@ -848,10 +896,12 @@ export class PersistentRenderScene {
|
|
|
848
896
|
this.gpuDevice = undefined;
|
|
849
897
|
this.gpuStatus = 'inactive';
|
|
850
898
|
this.composition = undefined;
|
|
899
|
+
this.pointsLinesInspections = [];
|
|
851
900
|
}
|
|
852
901
|
|
|
853
902
|
inspect(): PersistentRenderSceneInspection {
|
|
854
903
|
const entry = this.composition;
|
|
904
|
+
const pointsLines = this.pointsLinesInspections;
|
|
855
905
|
return {
|
|
856
906
|
worldEntitiesScanned: this.worldEntitiesScanned,
|
|
857
907
|
fullRebuilds: this.fullRebuilds,
|
|
@@ -871,6 +921,7 @@ export class PersistentRenderScene {
|
|
|
871
921
|
ineligible: 0,
|
|
872
922
|
} satisfies BatchTopologyInspection),
|
|
873
923
|
gpu: this.inspectGpu(),
|
|
924
|
+
pointsLines,
|
|
874
925
|
};
|
|
875
926
|
}
|
|
876
927
|
|
|
@@ -886,6 +937,10 @@ export class PersistentRenderScene {
|
|
|
886
937
|
};
|
|
887
938
|
}
|
|
888
939
|
|
|
940
|
+
pointsLinesSnapshots(): readonly PointsLinesRetainedSnapshot[] {
|
|
941
|
+
return this.composition?.projection.pointsLinesSnapshots() ?? [];
|
|
942
|
+
}
|
|
943
|
+
|
|
889
944
|
private inspectGpu(): PersistentGpuSceneInspection {
|
|
890
945
|
if (this.gpuStatus === 'resident' && this.gpuScene !== undefined) {
|
|
891
946
|
return { status: 'resident', ...this.gpuScene.inspect() };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/glyph-text.ts","../src/components/sprite-playback-mode.ts","../src/systems/transparent-sort-config.ts"],"names":[],"mappings":";;;;;AAkDO,IAAM,SAAA,GAAY,gBAAgB,WAAA,EAAa;AAAA;AAAA;AAAA,EAGpD,UAAA,EAAY;AAAA,IACV,IAAA,EAAM,mBAAA;AAAA,IACN,OAAA,EAAS,CAAA;AAAA,IACT,mBAAA,EAAqB;AAAA,GACvB;AAAA,EACA,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,SAAS,EAAA,EAAG;AAAA,EACpC,QAAA,EAAU,EAAE,IAAA,EAAM,KAAA,EAAO,SAAS,EAAA,EAAG;AAAA;AAAA;AAAA,EAGrC,KAAA,EAAO,EAAE,IAAA,EAAM,eAAA,EAAiB,OAAA,EAAS,IAAI,YAAA,CAAa,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAC,CAAA;AACxE,CAAC;;;ACXM,IAAM,yBAAA,GAA4B,CAAA;AAGlC,IAAM,0BAAA,GAA6B,CAAA;AAGnC,IAAM,cAAA,GAAiB,OAAO,MAAA,CAAO;AAAA,EAC1C,IAAA,EAAM,yBAAA;AAAA,EACN,KAAA,EAAO;AACT,CAAU;AAeH,SAAS,0BAA0B,KAAA,EAAmC;AAC3E,EAAA,OAAO,KAAA,KAAU,6BAA6B,OAAA,GAAU,MAAA;AAC1D;ACoBO,IAAM,2BAAA,GAA8B,uBAAA;AAGpC,IAAM,6BAAA,GAAgC;AAGtC,IAAM,6BAAA,GAAgC;AAGtC,IAAM,8BAAA,GAAiC;AAGvC,IAAM,8BAAA,GAAiC;AAGvC,IAAM,eAAA,GAAkB,OAAO,MAAA,CAAO;AAAA,EAC3C,MAAA,EAAQ,6BAAA;AAAA,EACR,MAAA,EAAQ,6BAAA;AAAA,EACR,OAAA,EAAS,8BAAA;AAAA,EACT,QAAA,EAAU,8BAAA;AAAA,EACV,SAAA,EAAW;AACb,CAAU;AAMV,IAAM,cAAA,GAAwC,OAAO,MAAA,CAAO;AAAA,EAC1D,IAAA,EAAM,6BAAA;AAAA,EACN,OAAA,EAAS;AACX,CAAC,CAAA;AAED,IAAM,WAAA,uBAAuC,GAAA,CAAY;AAAA,EACvD,6BAAA;AAAA,EACA,6BAAA;AAAA,EACA,8BAAA;AAAA,EACA;AACF,CAAC,CAAA;AAOD,IAAM,aAAA,GAAgB,0BAAA;AACtB,IAAM,SAAA,GAAY,8CAAA;AA2BX,SAAS,yBAAyB,KAAA,EAAqC;AAC5E,EAAA,IAAI,CAAC,KAAA,CAAM,WAAA,CAAY,2BAA2B,CAAA,EAAG;AACnD,IAAA,OAAO,cAAA;AAAA,EACT;AACA,EAAA,OAAO,KAAA,CAAM,YAAmC,2BAA2B,CAAA;AAC7E;AAwCO,SAAS,wBAAA,CACd,OACA,GAAA,EACyC;AACzC,EAAA,IAAI,CAAC,WAAA,CAAY,GAAA,CAAI,GAAA,CAAI,IAAI,CAAA,EAAG;AAC9B,IAAA,OAAO,GAAA;AAAA,MACL,IAAI,yBAAA,CAA0B,aAAA,EAAe,SAAA,EAAW;AAAA,QACtD,cAAc,GAAA,CAAI,IAAA;AAAA,QAClB,WAAA,EAAa;AAAA,OACd;AAAA,KACH;AAAA,EACF;AACA,EAAA,KAAA,CAAM,eAAsC,2BAAA,EAA6B;AAAA,IACvE,MAAM,GAAA,CAAI,IAAA;AAAA,IACV,SAAS,GAAA,CAAI;AAAA,GACd,CAAA;AACD,EAAA,OAAO,GAAG,MAAS,CAAA;AACrB","file":"chunk-QC4VW3XJ.mjs","sourcesContent":["// @forgeax/engine-runtime - GlyphText component (feat-20260531-world-space-msdf-text-rendering M4 / w14).\n//\n// `GlyphText` is the authoring source component for world-space MSDF text\n// (requirements AC-06 / §domain model). It carries ONLY authoring data; the\n// glyph quad baking + MeshFilter / MeshRenderer attachment is the job of the\n// `glyphTextLayoutSystem` (plan-strategy D-2: GlyphText is pure authoring\n// data, baking is a system responsibility). There is NO `TextLayoutAsset`\n// intermediate (OOS-5) -- layout output lives directly in a baked MeshAsset.\n//\n// Naming: single-semantic component drops the `Component` suffix\n// (AGENTS.md §Component naming rule #1 -- Transform / Camera / GlyphText).\n//\n// Schema vocab:\n// - `fontHandle: 'shared<FontAsset>'` -> `Handle<'FontAsset', 'shared'>`\n// (u32-stored; AssetRegistry owns the FontAsset lifecycle). AI users\n// obtain the handle via `assets.loadByGuid<FontAsset>(guid)`.\n// - `text: 'string'` -> native JS string (UniqueRefStore-backed, same\n// dispatch as `Name.value`).\n// - `fontSize: 'f32'` -> layout scale applied to the FontAsset metrics.\n// - `color: 'array<f32, 4>'` -> linear-space rgba tint (feat-20260709 M3:\n// collapsed from four `colorR/G/B/A` scalar columns into one inline\n// stride-4 SoA column, mirroring the DirectionalLight direction/color\n// idiom). Explicit layer-2 default [1,1,1,1] (opaque white).\n//\n// charter mapping: P1 (single import surface from `@forgeax/engine-render`,\n// co-located with `glyphTextLayoutSystem` that consumes it) + P3\n// (machine-readable schema literal) + P5 (consistent abstraction: same\n// `'shared<T>'` idiom as MeshFilter.assetHandle / MeshRenderer.materials).\n\nimport { defineComponent } from '@forgeax/engine-ecs';\n\n/**\n * Glyph text authoring component (world-space MSDF text).\n *\n * Spawn an entity with a `GlyphText` and the `glyphTextLayoutSystem`\n * (auto-wired by `createRenderer` / `createApp`) lays out the glyph quads,\n * bakes a `MeshAsset`, and attaches `MeshFilter` + `MeshRenderer` on the\n * next frame (Commands-deferred). Mutating `text` / `fontSize` / `color`\n * re-bakes the mesh in place (plan-strategy D-1 updateMesh; registry size\n * unchanged, AC-08).\n *\n * @example Spawn a world-space label:\n * import { GlyphText } from '@forgeax/engine-render/authoring';\n * const font = (await assets.loadByGuid(fontGuid)).unwrap();\n * world.spawn({\n * component: GlyphText,\n * data: { fontHandle: font, text: 'Hello', fontSize: 32,\n * color: [1, 1, 1, 1] },\n * });\n */\nexport const GlyphText = defineComponent('GlyphText', {\n // The layout/render owner re-resolves the font asset on the target world;\n // authoring text and style remain portable simulation state.\n fontHandle: {\n type: 'shared<FontAsset>',\n default: 0 as never,\n simulationTransient: true,\n },\n text: { type: 'string', default: '' },\n fontSize: { type: 'f32', default: 16 },\n // color carries an explicit layer-2 default [1,1,1,1] (opaque white); the\n // array layer-3 fallback is all-zero, so the default MUST be explicit (D-5).\n color: { type: 'array<f32, 4>', default: new Float32Array([1, 1, 1, 1]) },\n});\n","// @forgeax/engine-runtime - SpritePlaybackMode (u32 column encoding + mapper).\n//\n// SSOT for the SpriteAnimation.playbackMode ECS column (M2 T-12) and the\n// runtime tick-system branch selector (M4 T-23). The shape mirrors the M1\n// `Tonemap` block in `./camera.ts:72-90` (TONEMAP_NONE = 0 /\n// TONEMAP_REINHARD_EXTENDED = 1 + `type Tonemap = 'none' |\n// 'reinhard-extended'` + `tonemapFromF32`) for charter P4 consistent\n// abstraction — AI users keep one mental model across all u32-encoded\n// closed-union schema columns.\n//\n// Why `'u32'` + constant + mapper instead of a string-literal column?\n// ECS schema whitelist `SchemaFieldType` (packages/ecs/src/component.ts\n// section schema-field-type) does not accept string-literal unions\n// (research F-2 + F-5). Storing a u32 + translating to a closed\n// `'loop' | 'clamp'` literal union at the tick-system seam preserves\n// AI-user-facing narrowing (`switch (mode) { case 'loop': ... }`)\n// without forcing the ECS column to learn a new field-type vocabulary.\n//\n// Naming convention (plan-strategy section 8.command naming):\n// SPRITE_PLAYBACK_MODE_LOOP / SPRITE_PLAYBACK_MODE_CLAMP mirror M1\n// TONEMAP_* / TRANSPARENT_SORT_MODE_*. `spritePlaybackModeFromU32`\n// mirrors `tonemapFromF32` / `cameraProjectionFromF32`. AI users\n// discover the trio via single-import barrel re-export from\n// `@forgeax/engine-runtime` (wired in by M2 T-13).\n//\n// Anchors: plan-strategy section 2 D-5 + section 3.1 PR block SPM +\n// section 4 risk R-SCHEMA-1 reaction; research F-2 + F-5;\n// requirements section AC-02 + section 2.3 playbackMode row;\n// plan-tasks.json T-07 acceptanceCheck (rg\n// \"SPRITE_PLAYBACK_MODE_LOOP|SPRITE_PLAYBACK_MODE_CLAMP|\n// spritePlaybackModeFromU32\" >= 3 hits in this file).\n\n/**\n * Playback mode discriminator literal union (requirements section AC-02 +\n * section 2.3). Two members for the MVP:\n *\n * `'loop'` — `currentFrame = (currentFrame + 1) % frameCount`; the\n * sprite-animation-tick system wraps the index when\n * `accumDt >= frameDuration` (default for hello-sprite-\n * atlas demo \"100 sprites synchronised walk cycle\").\n * `'clamp'` — `currentFrame = min(currentFrame + 1, frameCount - 1)`;\n * holds the last frame for death / terminator-animation\n * style sequences (requirements section 2.5 q8 lock).\n *\n * Future modes (`'pingpong'`, reverse playback, arbitrary frame index\n * jumps) are deferred per requirements OOS-03. The closed union shape\n * leaves room for additive growth without breaking the u32 enum encoding\n * (plan-strategy section 2 D-5).\n */\nexport type SpritePlaybackMode = 'loop' | 'clamp';\n\n/** Numeric encoding of the loop playback mode (schema value for `playbackMode`). */\nexport const SPRITE_PLAYBACK_MODE_LOOP = 0 as const;\n\n/** Numeric encoding of the clamp playback mode (schema value for `playbackMode`). */\nexport const SPRITE_PLAYBACK_MODE_CLAMP = 1 as const;\n\n/** Grouped authoring values; the numeric ECS encoding stays owner-local. */\nexport const SpritePlayback = Object.freeze({\n loop: SPRITE_PLAYBACK_MODE_LOOP,\n clamp: SPRITE_PLAYBACK_MODE_CLAMP,\n} as const);\n\n/**\n * Map a `SpriteAnimation.playbackMode` numeric column value to the closed\n * `SpritePlaybackMode` string-literal union. The defensive fallback mirrors\n * `cameraProjectionFromF32` / `tonemapFromF32` precedent — a value other\n * than `SPRITE_PLAYBACK_MODE_CLAMP` (1) maps to `'loop'`, so stale or\n * uninitialised entities surface a predictable playback shape through the\n * tick-system query (rather than throwing or returning `undefined`).\n * Validation of `playbackMode` happens at schema-write time, not here.\n *\n * The tick-system in M4 T-23 (`spriteAnimationTickSystem`) consumes the\n * return value via `switch (mode) { case 'loop': ...; case 'clamp': ... }`\n * to pick the per-branch frame-advance arithmetic.\n */\nexport function spritePlaybackModeFromU32(value: number): SpritePlaybackMode {\n return value === SPRITE_PLAYBACK_MODE_CLAMP ? 'clamp' : 'loop';\n}\n","// @forgeax/engine-runtime - TransparentSortConfig KV resource + helpers.\n//\n// feat-20260520-2d-sprite-layer-mvp M-2 w14 / requirements AC-08 + AC-18\n// path (2)/(3) + AC-19 derivation audit (5).\n//\n// Surface:\n// - interface TransparentSortConfig { mode: number; yzAlpha: number }\n// - constant TRANSPARENT_SORT_CONFIG_KEY = 'TransparentSortConfig'\n// - constants TRANSPARENT_SORT_MODE_LAYER_Z = 0 (horizontal-z)\n// TRANSPARENT_SORT_MODE_LAYER_Y = 1 (Y-sort)\n// TRANSPARENT_SORT_MODE_LAYER_YZ = 2 (Y-Z blend)\n// - helper getTransparentSortConfig(world): TransparentSortConfig\n// - helper setTransparentSortConfig(world, cfg):\n// Result<void, ResourceInvalidValueError>\n//\n// 5-view selection table (charter F1 progressive disclosure for AI users\n// reading IDE hover on this header; same SSOT mirrored in\n// apps/hello/sprite/README.md M-4 w33):\n//\n// | view | mode | yzAlpha | sortValue formula |\n// |:------------------|:----:|:-------:|:--------------------------------|\n// | horizontal (side) | 0 | --- | posZ |\n// | top-down | 1 | --- | -(posY - pivot.y * size.y) |\n// | Don't Starve | 2 | 1.0 | (posY - pivot.y * size.y) + posZ|\n// | isometric | 2 | 0.5 | (posY - pivot.y * size.y) + 0.5*posZ |\n// | JRPG (foot pivot) | 1 | --- | -(posY - pivot.y * size.y) |\n//\n// AC-18 path (2): @fallback getTransparentSortConfig KV missing returns\n// default; no warn; no throw (legal default state, not an error state).\n// AC-18 path (3): @fallback setTransparentSortConfig mode out-of-range\n// does NOT silently coerce to mode=0 — returns Result.err with the\n// 4 SSOT fields locked by plan-strategy D-4 so AI users can self-repair\n// via .code / .expected / .hint / .detail property access (charter P3).\n// AC-18 path (3) carve-out: yzAlpha range is intentionally NOT validated;\n// mode=0 / mode=1 ignore the field entirely, mode=2 reads it, mode=3\n// (distance) also reads it for compat but ignores it (squared-distance\n// formula does not use yzAlpha). The silent-ignore is the documented\n// charter P3 \"legal default\" boundary.\n//\n// @new-surface KV resource: ECS has no defineResource factory at this\n// feat's commit time; the TS POD interface + string KV key form is the\n// minimum-new-surface route. The world resource store\n// (insertResource / getResource / hasResource) is reused unchanged\n// (requirements §2.2.G derivation row 5).\n// @derives world.hasResource / world.insertResource / world.getResource\n// KV API (packages/ecs/src/resource.ts + world.ts: insertResource l.490,\n// getResource l.498, hasResource l.503).\n// @fallback getTransparentSortConfig KV missing returns\n// { mode: 0, yzAlpha: 1.0 }; no warn; no throw.\n// @fallback setTransparentSortConfig mode out-of-range returns Result.err\n// with code='resource-invalid-value'; never silently coerced.\n//\n// charter mapping: F1 (single-import barrel + 5-view JSDoc table on\n// hover) + P3 (structured failure on mode out-of-range; legal-default\n// fallback distinguished from error fallback) + P4 (consistent\n// abstraction — same world.{has,get,insert}Resource KV API as every\n// other engine resource consumer).\n\nimport type { World } from '@forgeax/engine-ecs';\nimport { ResourceInvalidValueError } from '@forgeax/engine-ecs/projection';\nimport { err, ok, type Result } from '@forgeax/engine-types';\n\n// ────────────────────────────────────────────────────────────────────────────\n// POD interface + KV key + 3 named mode constants\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * Transparent-bucket sort configuration (plain-data POD). Lives as a\n * world-level resource keyed by `TRANSPARENT_SORT_CONFIG_KEY`.\n *\n * Fields:\n * - `mode \\u2208 {0, 1, 2, 3}` selects the sort formula\n * (`TRANSPARENT_SORT_MODE_LAYER_Z` / `_LAYER_Y` / `_LAYER_YZ` / `_DISTANCE`).\n * - `yzAlpha` (mode=2 only) blends Y-axis into Z-axis: 1.0 weights\n * posZ equally with `posY - pivot.y * size.y` (Don't-Starve style),\n * 0.5 weights posZ at half (isometric). mode=0 / mode=1 ignore the\n * field (documented charter P3 legal-default boundary).\n */\nexport interface TransparentSortConfig {\n readonly mode: number;\n readonly yzAlpha: number;\n}\n\n/** Extract-stage POD consumed by the transparent-sort owner. */\nexport interface TransparentEntry {\n readonly entityIndex: number;\n readonly materialHandle: number;\n readonly layer: number;\n readonly posX: number;\n readonly posY: number;\n readonly posZ: number;\n readonly pivotY: number;\n readonly sizeY: number;\n readonly sortKey?: number | undefined;\n readonly renderableIndex?: number | undefined;\n}\n\n/** World resource key for `TransparentSortConfig` KV entry. */\nexport const TRANSPARENT_SORT_CONFIG_KEY = 'TransparentSortConfig' as const;\n\n/** horizontal / side-scroller: sortValue = posZ. */\nexport const TRANSPARENT_SORT_MODE_LAYER_Z = 0;\n\n/** top-down / JRPG: sortValue = -(posY - pivot.y * size.y) (foot pivot Y-sort). */\nexport const TRANSPARENT_SORT_MODE_LAYER_Y = 1;\n\n/** Don't-Starve / isometric: sortValue = (posY - pivot.y * size.y) + yzAlpha * posZ. */\nexport const TRANSPARENT_SORT_MODE_LAYER_YZ = 2;\n\n/** 3D distance: sortValue = -(dist^2) to cameraPos, back-to-front (far first). */\nexport const TRANSPARENT_SORT_MODE_DISTANCE = 3;\n\n/** Grouped authoring values and the single configuration operation. */\nexport const TransparentSort = Object.freeze({\n layerZ: TRANSPARENT_SORT_MODE_LAYER_Z,\n layerY: TRANSPARENT_SORT_MODE_LAYER_Y,\n layerYZ: TRANSPARENT_SORT_MODE_LAYER_YZ,\n distance: TRANSPARENT_SORT_MODE_DISTANCE,\n configure: setTransparentSortConfig,\n} as const);\n\n// ────────────────────────────────────────────────────────────────────────────\n// Internal: silent default + mode validity check\n// ────────────────────────────────────────────────────────────────────────────\n\nconst DEFAULT_CONFIG: TransparentSortConfig = Object.freeze({\n mode: TRANSPARENT_SORT_MODE_LAYER_Z,\n yzAlpha: 1.0,\n});\n\nconst VALID_MODES: ReadonlySet<number> = new Set<number>([\n TRANSPARENT_SORT_MODE_LAYER_Z,\n TRANSPARENT_SORT_MODE_LAYER_Y,\n TRANSPARENT_SORT_MODE_LAYER_YZ,\n TRANSPARENT_SORT_MODE_DISTANCE,\n]);\n\n// The expected / hint literals are the SSOT locked by plan-strategy D-4.\n// They MUST round-trip byte-for-byte into ResourceInvalidValueError so AI\n// users can read err.expected / err.hint for self-repair (charter P3).\n// `\\u2208` is the math \"is-element-of\" symbol; the ASCII source escape\n// keeps the file English-only per AGENTS.md §Conventions.\nconst EXPECTED_MODE = 'mode \\u2208 {0, 1, 2, 3}';\nconst HINT_MODE = '0=layer-z, 1=layer-y, 2=layer-yz, 3=distance';\n\n// ────────────────────────────────────────────────────────────────────────────\n// Helpers\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * Read the world's `TransparentSortConfig` resource.\n *\n * @fallback KV missing returns `{ mode: 0, yzAlpha: 1.0 }`\n * (`TRANSPARENT_SORT_MODE_LAYER_Z`, horizontal-z safe default).\n * NO warn, NO throw — KV missing is a legal state (not an error state),\n * mirroring how a sprite-free scene is a legal state for the renderer\n * even though render-system has a transparent bucket. plan-strategy D-2:\n * `hasResource` guard precedes the read so this helper never trips\n * `ResourceNotFoundError` from `world.getResource`.\n *\n * @example\n * const cfg = getTransparentSortConfig(world);\n * // cfg.mode === 0, cfg.yzAlpha === 1.0 when no resource inserted.\n *\n * @example\n * world.insertResource(TRANSPARENT_SORT_CONFIG_KEY,\n * { mode: TRANSPARENT_SORT_MODE_LAYER_Y, yzAlpha: 1.0 });\n * const cfg = getTransparentSortConfig(world);\n * // cfg.mode === 1, cfg.yzAlpha === 1.0 (yzAlpha ignored by mode=1).\n */\nexport function getTransparentSortConfig(world: World): TransparentSortConfig {\n if (!world.hasResource(TRANSPARENT_SORT_CONFIG_KEY)) {\n return DEFAULT_CONFIG;\n }\n return world.getResource<TransparentSortConfig>(TRANSPARENT_SORT_CONFIG_KEY);\n}\n\n/**\n * Write the world's `TransparentSortConfig` resource, validating `mode`.\n *\n * Returns `Result<void, ResourceInvalidValueError>`:\n *\n * - `ok` — mode is in `{0, 1, 2, 3}`; the resource is inserted /\n * overwritten via `world.insertResource`.\n * - `err` — mode is out of range; the resource is NOT mutated;\n * `err.error` is a `ResourceInvalidValueError` instance carrying:\n * `.code === 'resource-invalid-value'`\n * `.expected === 'mode \\u2208 {0, 1, 2, 3}'`\n * `.hint === '0=layer-z, 1=layer-y, 2=layer-yz, 3=distance'`\n * `.detail.receivedMode === <the rejected mode>`\n *\n * @fallback mode out-of-range is NEVER silently coerced to mode=0 —\n * `Result.err` is the only fail-mode (charter P3 structured failure).\n * AI users read `.code / .expected / .hint / .detail` properties to\n * self-repair, NOT the human-readable `.message` string.\n *\n * @fallback yzAlpha range is intentionally not validated; mode=0 / mode=1\n * ignore the field, mode=2 reads it. The silent-ignore is the\n * documented charter P3 \"legal default\" boundary (plan-strategy D-4 +\n * AC-18 path (3) carve-out).\n *\n * @example success path:\n * const r = setTransparentSortConfig(world,\n * { mode: TRANSPARENT_SORT_MODE_LAYER_YZ, yzAlpha: 0.5 });\n * if (!r.ok) return r;\n *\n * @example failure path:\n * const r = setTransparentSortConfig(world, { mode: 99, yzAlpha: 1.0 });\n * if (!r.ok) {\n * // r.error.code === 'resource-invalid-value'\n * // r.error.expected === 'mode \\u2208 {0, 1, 2, 3}'\n * // r.error.hint === '0=layer-z, 1=layer-y, 2=layer-yz, 3=distance'\n * // r.error.detail.receivedMode === 99\n * }\n */\nexport function setTransparentSortConfig(\n world: World,\n cfg: TransparentSortConfig,\n): Result<void, ResourceInvalidValueError> {\n if (!VALID_MODES.has(cfg.mode)) {\n return err(\n new ResourceInvalidValueError(EXPECTED_MODE, HINT_MODE, {\n receivedMode: cfg.mode,\n receivedKey: TRANSPARENT_SORT_CONFIG_KEY,\n }),\n );\n }\n world.insertResource<TransparentSortConfig>(TRANSPARENT_SORT_CONFIG_KEY, {\n mode: cfg.mode,\n yzAlpha: cfg.yzAlpha,\n });\n return ok(undefined);\n}\n"]}
|