@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.
Files changed (151) hide show
  1. package/README.md +139 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts.map +1 -1
  4. package/dist/assembly/factory.d.ts +7 -0
  5. package/dist/assembly/factory.d.ts.map +1 -1
  6. package/dist/assembly/material/assembly.d.ts.map +1 -1
  7. package/dist/authoring.d.ts +1 -0
  8. package/dist/authoring.d.ts.map +1 -1
  9. package/dist/authoring.mjs +1 -1
  10. package/dist/{chunk-QC4VW3XJ.mjs → chunk-HCOGPHKS.mjs} +17 -3
  11. package/dist/chunk-HCOGPHKS.mjs.map +1 -0
  12. package/dist/chunk-YEJIUW7D.mjs +1152 -0
  13. package/dist/chunk-YEJIUW7D.mjs.map +1 -0
  14. package/dist/components/__tests__/points-lines-admission.unit.test.d.ts +2 -0
  15. package/dist/components/__tests__/points-lines-admission.unit.test.d.ts.map +1 -0
  16. package/dist/components/__tests__/points-lines-schema.unit.test.d.ts +2 -0
  17. package/dist/components/__tests__/points-lines-schema.unit.test.d.ts.map +1 -0
  18. package/dist/components/index.d.ts +2 -0
  19. package/dist/components/index.d.ts.map +1 -1
  20. package/dist/components/lines.d.ts +5 -0
  21. package/dist/components/lines.d.ts.map +1 -0
  22. package/dist/components/points.d.ts +15 -0
  23. package/dist/components/points.d.ts.map +1 -0
  24. package/dist/construct-renderer.mjs +952 -497
  25. package/dist/construct-renderer.mjs.map +1 -1
  26. package/dist/errors/render.d.ts +82 -1
  27. package/dist/errors/render.d.ts.map +1 -1
  28. package/dist/features/render-graph-raster.d.ts +5 -1
  29. package/dist/features/render-graph-raster.d.ts.map +1 -1
  30. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  31. package/dist/index.d.ts +4 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.mjs +4 -2
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/inspection-types.d.ts +3 -0
  36. package/dist/inspection-types.d.ts.map +1 -1
  37. package/dist/pbr-pipeline.d.ts +2 -0
  38. package/dist/pbr-pipeline.d.ts.map +1 -1
  39. package/dist/plugin.d.ts.map +1 -1
  40. package/dist/points-lines/__tests__/bounds.unit.test.d.ts +2 -0
  41. package/dist/points-lines/__tests__/bounds.unit.test.d.ts.map +1 -0
  42. package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts +2 -0
  43. package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts.map +1 -0
  44. package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts +2 -0
  45. package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts.map +1 -0
  46. package/dist/points-lines/__tests__/inspection.unit.test.d.ts +2 -0
  47. package/dist/points-lines/__tests__/inspection.unit.test.d.ts.map +1 -0
  48. package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts +2 -0
  49. package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts.map +1 -0
  50. package/dist/points-lines/__tests__/points-lines.bench.d.ts +2 -0
  51. package/dist/points-lines/__tests__/points-lines.bench.d.ts.map +1 -0
  52. package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts +2 -0
  53. package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts.map +1 -0
  54. package/dist/points-lines/__tests__/snapshot.unit.test.d.ts +2 -0
  55. package/dist/points-lines/__tests__/snapshot.unit.test.d.ts.map +1 -0
  56. package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts +2 -0
  57. package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts.map +1 -0
  58. package/dist/points-lines/admission.d.ts +36 -0
  59. package/dist/points-lines/admission.d.ts.map +1 -0
  60. package/dist/points-lines/bounds.d.ts +8 -0
  61. package/dist/points-lines/bounds.d.ts.map +1 -0
  62. package/dist/points-lines/expansion-cache.d.ts +28 -0
  63. package/dist/points-lines/expansion-cache.d.ts.map +1 -0
  64. package/dist/points-lines/inspection.d.ts +53 -0
  65. package/dist/points-lines/inspection.d.ts.map +1 -0
  66. package/dist/points-lines/prepare.d.ts +67 -0
  67. package/dist/points-lines/prepare.d.ts.map +1 -0
  68. package/dist/points-lines/record.d.ts +60 -0
  69. package/dist/points-lines/record.d.ts.map +1 -0
  70. package/dist/points-lines/snapshot.d.ts +49 -0
  71. package/dist/points-lines/snapshot.d.ts.map +1 -0
  72. package/dist/record/frame-lighting.d.ts.map +1 -1
  73. package/dist/record/frame.d.ts +1 -1
  74. package/dist/record/frame.d.ts.map +1 -1
  75. package/dist/record/main-pass-geometry.d.ts +7 -0
  76. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  77. package/dist/record/main-pass.d.ts.map +1 -1
  78. package/dist/record/render-context.d.ts +17 -0
  79. package/dist/record/render-context.d.ts.map +1 -1
  80. package/dist/record/shadow-pass.d.ts.map +1 -1
  81. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  82. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  83. package/dist/record/view-ubo.d.ts +6 -0
  84. package/dist/record/view-ubo.d.ts.map +1 -1
  85. package/dist/render-contract.d.ts +10 -2
  86. package/dist/render-contract.d.ts.map +1 -1
  87. package/dist/render-system-extract.d.ts +3 -0
  88. package/dist/render-system-extract.d.ts.map +1 -1
  89. package/dist/render-system.d.ts +4 -1
  90. package/dist/render-system.d.ts.map +1 -1
  91. package/dist/scene/render-scene-types.d.ts +3 -0
  92. package/dist/scene/render-scene-types.d.ts.map +1 -1
  93. package/dist/scene/render-scene.d.ts +7 -0
  94. package/dist/scene/render-scene.d.ts.map +1 -1
  95. package/package.json +19 -19
  96. package/src/__tests__/authoring-path.unit.test.ts +9 -0
  97. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +12 -2
  98. package/src/__tests__/material-cooked-projection.unit.test.ts +29 -7
  99. package/src/__tests__/material-static-dynamic-route.unit.test.ts +28 -28
  100. package/src/__tests__/render-error-code-owner.test-d.ts +6 -0
  101. package/src/__tests__/render-error-exhaustive.test-d.ts +19 -0
  102. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +38 -0
  103. package/src/assembly/factory.ts +72 -3
  104. package/src/assembly/material/assembly.ts +2 -5
  105. package/src/authoring.ts +1 -0
  106. package/src/components/__tests__/points-lines-admission.unit.test.ts +219 -0
  107. package/src/components/__tests__/points-lines-schema.unit.test.ts +56 -0
  108. package/src/components/index.ts +7 -0
  109. package/src/components/lines.ts +18 -0
  110. package/src/components/points.ts +40 -0
  111. package/src/errors/render.ts +144 -1
  112. package/src/features/render-graph-raster.ts +22 -3
  113. package/src/gpu-driven/production-raster.ts +6 -1
  114. package/src/index.ts +17 -0
  115. package/src/inspection-types.ts +4 -0
  116. package/src/pbr-pipeline.ts +8 -1
  117. package/src/plugin.ts +4 -0
  118. package/src/points-lines/__tests__/bounds.unit.test.ts +21 -0
  119. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +157 -0
  120. package/src/points-lines/__tests__/inspection-lanes.integration.test.ts +146 -0
  121. package/src/points-lines/__tests__/inspection.unit.test.ts +173 -0
  122. package/src/points-lines/__tests__/lane-structural.integration.test.ts +122 -0
  123. package/src/points-lines/__tests__/points-lines.bench.ts +400 -0
  124. package/src/points-lines/__tests__/raster-lane.integration.test.ts +44 -0
  125. package/src/points-lines/__tests__/snapshot.unit.test.ts +101 -0
  126. package/src/points-lines/__tests__/view-ubo.unit.test.ts +73 -0
  127. package/src/points-lines/admission.ts +238 -0
  128. package/src/points-lines/bounds.ts +23 -0
  129. package/src/points-lines/expansion-cache.ts +215 -0
  130. package/src/points-lines/inspection.ts +97 -0
  131. package/src/points-lines/prepare.ts +177 -0
  132. package/src/points-lines/record.ts +125 -0
  133. package/src/points-lines/snapshot.ts +130 -0
  134. package/src/record/frame-lighting.ts +14 -2
  135. package/src/record/frame.ts +12 -1
  136. package/src/record/main-pass-geometry.ts +92 -0
  137. package/src/record/main-pass-sprite-draws.ts +1 -1
  138. package/src/record/main-pass.ts +6 -3
  139. package/src/record/render-context.ts +20 -0
  140. package/src/record/shadow-pass.ts +16 -5
  141. package/src/record/skybox-post-pass.ts +2 -1
  142. package/src/record/typed-frame-graph.ts +17 -16
  143. package/src/record/view-ubo.ts +47 -0
  144. package/src/render-contract.ts +13 -2
  145. package/src/render-system-extract.ts +71 -8
  146. package/src/render-system.ts +304 -2
  147. package/src/scene/render-scene-types.ts +3 -0
  148. package/src/scene/render-scene.ts +57 -2
  149. package/dist/chunk-QC4VW3XJ.mjs.map +0 -1
  150. package/dist/chunk-SCRSAK5B.mjs +0 -480
  151. package/dist/chunk-SCRSAK5B.mjs.map +0 -1
@@ -0,0 +1,177 @@
1
+ import type { MeshAsset } from '@forgeax/engine-types';
2
+ import { err, ok, type Result } from '@forgeax/engine-types';
3
+ import { PointsLinesPrepareFailedError } from '../errors/render';
4
+ import type { PointsLinesExpandedGeometry, PointsLinesExpansionCache } from './expansion-cache';
5
+ import {
6
+ createPointsLinesLaneContract,
7
+ type PointsLinesBackend,
8
+ type PointsLinesLane,
9
+ type PointsLinesLaneContract,
10
+ } from './record';
11
+ import type { PointsLinesRetainedSnapshot } from './snapshot';
12
+
13
+ export interface PointsLinesGpuResourceAdapter<Resource> {
14
+ create(geometry: PointsLinesExpandedGeometry): Result<Resource, Error>;
15
+ upload(resource: Resource, geometry: PointsLinesExpandedGeometry): Result<number, Error>;
16
+ validate(resource: Resource, geometry: PointsLinesExpandedGeometry): Result<void, Error>;
17
+ destroy(resource: Resource): void;
18
+ }
19
+
20
+ export interface PointsLinesPreparedResource<Resource> {
21
+ readonly resource: Resource;
22
+ readonly geometry: PointsLinesExpandedGeometry;
23
+ readonly snapshot: PointsLinesRetainedSnapshot;
24
+ readonly uploadedBytes: number;
25
+ readonly lastKnownGood: true;
26
+ }
27
+
28
+ export interface PointsLinesPreparationOptions<Resource> {
29
+ readonly cache: PointsLinesExpansionCache;
30
+ readonly adapter: PointsLinesGpuResourceAdapter<Resource>;
31
+ }
32
+
33
+ export interface PointsLinesLanePreparationAdapter<Resource> {
34
+ readonly contract: PointsLinesLaneContract;
35
+ prepare(
36
+ snapshot: PointsLinesRetainedSnapshot,
37
+ mesh: MeshAsset,
38
+ ): Result<PointsLinesPreparedResource<Resource>, PointsLinesPrepareFailedError>;
39
+ resetForDeviceLoss(): void;
40
+ lastKnownGood(): PointsLinesPreparedResource<Resource> | undefined;
41
+ inspect(): PointsLinesPreparationInspection<Resource>;
42
+ }
43
+
44
+ export interface PointsLinesPreparationInspection<Resource> {
45
+ readonly status: 'empty' | 'resident' | 'rebuild-pending';
46
+ readonly lastKnownGood: boolean;
47
+ readonly liveResource: Resource | undefined;
48
+ readonly liveBytes: number;
49
+ readonly candidateBytes: number;
50
+ readonly cacheEntries: number;
51
+ readonly uploads: number;
52
+ readonly rebuilds: number;
53
+ readonly failures: number;
54
+ }
55
+
56
+ /**
57
+ * Owns one atomic candidate/LKG pair for a Points/Lines render projection.
58
+ * GPU resources are supplied by the existing DeviceScope adapter; this class
59
+ * keeps no backend handle or second resource ledger.
60
+ */
61
+ export class PointsLinesPreparation<Resource> {
62
+ private live: PointsLinesPreparedResource<Resource> | undefined;
63
+ private pendingCandidateBytes = 0;
64
+ private status: 'empty' | 'resident' | 'rebuild-pending' = 'empty';
65
+ private uploads = 0;
66
+ private rebuilds = 0;
67
+ private failures = 0;
68
+
69
+ constructor(private readonly options: PointsLinesPreparationOptions<Resource>) {}
70
+
71
+ prepare(
72
+ snapshot: PointsLinesRetainedSnapshot,
73
+ mesh: MeshAsset,
74
+ ): Result<PointsLinesPreparedResource<Resource>, PointsLinesPrepareFailedError> {
75
+ const geometry = this.options.cache.getOrCreate(snapshot, mesh);
76
+ if (this.live?.geometry.key === geometry.key) {
77
+ this.live = {
78
+ ...this.live,
79
+ snapshot,
80
+ uploadedBytes: 0,
81
+ };
82
+ this.status = 'resident';
83
+ return ok(this.live);
84
+ }
85
+
86
+ this.pendingCandidateBytes = geometry.derivedBytes;
87
+ const created = this.options.adapter.create(geometry);
88
+ if (!created.ok) return this.fail(snapshot, created.error);
89
+ const candidate = created.value;
90
+ const uploaded = this.options.adapter.upload(candidate, geometry);
91
+ if (!uploaded.ok) {
92
+ this.options.adapter.destroy(candidate);
93
+ return this.fail(snapshot, uploaded.error);
94
+ }
95
+ this.uploads += 1;
96
+ const validated = this.options.adapter.validate(candidate, geometry);
97
+ if (!validated.ok) {
98
+ this.options.adapter.destroy(candidate);
99
+ return this.fail(snapshot, validated.error);
100
+ }
101
+
102
+ const previous = this.live;
103
+ this.live = {
104
+ resource: candidate,
105
+ geometry,
106
+ snapshot,
107
+ uploadedBytes: uploaded.value,
108
+ lastKnownGood: true,
109
+ };
110
+ this.pendingCandidateBytes = 0;
111
+ this.status = 'resident';
112
+ this.rebuilds += 1;
113
+ if (previous !== undefined) this.options.adapter.destroy(previous.resource);
114
+ return ok(this.live);
115
+ }
116
+
117
+ resetForDeviceLoss(): void {
118
+ if (this.live !== undefined) this.options.adapter.destroy(this.live.resource);
119
+ this.live = undefined;
120
+ this.pendingCandidateBytes = 0;
121
+ this.status = 'rebuild-pending';
122
+ }
123
+
124
+ lastKnownGood(): PointsLinesPreparedResource<Resource> | undefined {
125
+ return this.live;
126
+ }
127
+
128
+ inspect(): PointsLinesPreparationInspection<Resource> {
129
+ return {
130
+ status: this.status,
131
+ lastKnownGood: this.live?.lastKnownGood ?? false,
132
+ liveResource: this.live?.resource,
133
+ liveBytes: this.live?.geometry.derivedBytes ?? 0,
134
+ candidateBytes: this.pendingCandidateBytes,
135
+ cacheEntries: this.options.cache.size,
136
+ uploads: this.uploads,
137
+ rebuilds: this.rebuilds,
138
+ failures: this.failures,
139
+ };
140
+ }
141
+
142
+ private fail(
143
+ snapshot: PointsLinesRetainedSnapshot,
144
+ cause: Error,
145
+ ): Result<never, PointsLinesPrepareFailedError> {
146
+ this.pendingCandidateBytes = 0;
147
+ this.failures += 1;
148
+ return err(
149
+ new PointsLinesPrepareFailedError({
150
+ owner: 'points-lines',
151
+ generation: snapshot.meshGeneration,
152
+ stage: 'prepare',
153
+ cause: cause.message,
154
+ lastKnownGood: this.live !== undefined,
155
+ }),
156
+ );
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Binds the existing atomic preparation owner to a lane contract. It carries
162
+ * no RHI handle and does not add a second cache or recovery owner.
163
+ */
164
+ export function createPointsLinesLanePreparationAdapter<Resource>(
165
+ lane: PointsLinesLane,
166
+ backend: PointsLinesBackend,
167
+ options: PointsLinesPreparationOptions<Resource>,
168
+ ): PointsLinesLanePreparationAdapter<Resource> {
169
+ const preparation = new PointsLinesPreparation(options);
170
+ return {
171
+ contract: createPointsLinesLaneContract(lane, backend),
172
+ prepare: (snapshot, mesh) => preparation.prepare(snapshot, mesh),
173
+ resetForDeviceLoss: () => preparation.resetForDeviceLoss(),
174
+ lastKnownGood: () => preparation.lastKnownGood(),
175
+ inspect: () => preparation.inspect(),
176
+ };
177
+ }
@@ -0,0 +1,125 @@
1
+ import type { PointsLinesExpandedGeometry } from './expansion-cache';
2
+ import type { PointsLinesRetainedSnapshot } from './snapshot';
3
+
4
+ export const POINTS_LINES_MATERIAL_SHADER_ID = 'forgeax::points-lines';
5
+
6
+ export type PointsLinesLane = 'direct' | 'clustered' | 'cpu-webgl2';
7
+ export type PointsLinesBackend = 'webgpu' | 'wgpu-webgl2' | 'null';
8
+
9
+ export interface PointsLinesLaneContract {
10
+ readonly lane: PointsLinesLane;
11
+ readonly backend: PointsLinesBackend;
12
+ readonly authoring: 'shared';
13
+ readonly evidence: 'runtime' | 'structural-only';
14
+ readonly material: {
15
+ readonly source: 'MaterialAsset';
16
+ readonly shaderId: typeof POINTS_LINES_MATERIAL_SHADER_ID;
17
+ readonly shadingModel: 'unlit';
18
+ readonly clusteredLighting: false;
19
+ readonly lit: false;
20
+ };
21
+ readonly graph: {
22
+ readonly additionalAttachments: 0;
23
+ readonly additionalPasses: 0;
24
+ };
25
+ readonly shadowDrawCount: 0;
26
+ readonly capabilities: {
27
+ readonly compute: false;
28
+ readonly storage: false;
29
+ readonly indirect: false;
30
+ };
31
+ }
32
+
33
+ export interface PointsLinesRecordPlan {
34
+ readonly lane: PointsLinesLane;
35
+ readonly backend: PointsLinesBackend;
36
+ readonly materialShaderId: typeof POINTS_LINES_MATERIAL_SHADER_ID;
37
+ readonly component: 'Points' | 'Lines';
38
+ readonly meshGeneration: number;
39
+ readonly materialGeneration: number;
40
+ readonly vertexCount: number;
41
+ readonly indexCount: number;
42
+ readonly drawCount: 0 | 1;
43
+ readonly shadowDrawCount: 0;
44
+ readonly conservativeMarginPx: number;
45
+ readonly graph: PointsLinesLaneContract['graph'];
46
+ }
47
+
48
+ /**
49
+ * Describes the shared lane boundary without selecting a backend in authoring.
50
+ * Every lane uses CPU-uploaded triangle expansion; capability flags therefore
51
+ * stay false, including on WebGPU, and the graph remains the existing pass.
52
+ */
53
+ export function createPointsLinesLaneContract(
54
+ lane: PointsLinesLane,
55
+ backend: PointsLinesBackend,
56
+ ): PointsLinesLaneContract {
57
+ return {
58
+ lane,
59
+ backend,
60
+ authoring: 'shared',
61
+ evidence: backend === 'null' ? 'structural-only' : 'runtime',
62
+ material: {
63
+ source: 'MaterialAsset',
64
+ shaderId: POINTS_LINES_MATERIAL_SHADER_ID,
65
+ shadingModel: 'unlit',
66
+ clusteredLighting: false,
67
+ lit: false,
68
+ },
69
+ graph: { additionalAttachments: 0, additionalPasses: 0 },
70
+ shadowDrawCount: 0,
71
+ capabilities: { compute: false, storage: false, indirect: false },
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Turns one retained expansion into the bounded draw facts consumed by the
77
+ * existing record pass. The shader expands one quad per point or segment,
78
+ * so the limits are exactly four vertices and six indices per primitive.
79
+ */
80
+ export function createPointsLinesRecordPlan(
81
+ snapshot: PointsLinesRetainedSnapshot,
82
+ geometry: PointsLinesExpandedGeometry,
83
+ contract: PointsLinesLaneContract,
84
+ ): PointsLinesRecordPlan {
85
+ const primitiveCount = geometry.pointCount + geometry.segmentCount;
86
+ const visible = snapshot.visible && primitiveCount > 0;
87
+ const conservativeMarginPx =
88
+ snapshot.style?.kind === 'points'
89
+ ? snapshot.style.sizePx * 0.5
90
+ : (snapshot.style?.widthPx ?? 0) * 0.5;
91
+ return {
92
+ lane: contract.lane,
93
+ backend: contract.backend,
94
+ materialShaderId: POINTS_LINES_MATERIAL_SHADER_ID,
95
+ component: geometry.component,
96
+ meshGeneration: snapshot.meshGeneration,
97
+ materialGeneration: snapshot.materialGeneration,
98
+ vertexCount: primitiveCount * 4,
99
+ indexCount: primitiveCount * 6,
100
+ drawCount: visible ? 1 : 0,
101
+ shadowDrawCount: 0,
102
+ conservativeMarginPx,
103
+ graph: contract.graph,
104
+ };
105
+ }
106
+
107
+ export interface PointsLinesLaneAdapter {
108
+ readonly contract: PointsLinesLaneContract;
109
+ createRecordPlan(
110
+ snapshot: PointsLinesRetainedSnapshot,
111
+ geometry: PointsLinesExpandedGeometry,
112
+ ): PointsLinesRecordPlan;
113
+ }
114
+
115
+ export function createPointsLinesLaneAdapter(
116
+ lane: PointsLinesLane,
117
+ backend: PointsLinesBackend,
118
+ ): PointsLinesLaneAdapter {
119
+ const contract = createPointsLinesLaneContract(lane, backend);
120
+ return {
121
+ contract,
122
+ createRecordPlan: (snapshot, geometry) =>
123
+ createPointsLinesRecordPlan(snapshot, geometry, contract),
124
+ };
125
+ }
@@ -0,0 +1,130 @@
1
+ import type { PointShape } from '../components/points';
2
+
3
+ export interface PointsLinesViewport {
4
+ readonly width: number;
5
+ readonly height: number;
6
+ readonly dpr: number;
7
+ }
8
+
9
+ export type PointsLinesStyle =
10
+ | { readonly kind: 'points'; readonly sizePx: number; readonly shape: PointShape }
11
+ | { readonly kind: 'lines'; readonly widthPx: number };
12
+
13
+ export type PointsLinesInvalidation = 'none' | 'mesh' | 'material' | 'style' | 'view' | 'scene';
14
+
15
+ /**
16
+ * Detached facts retained between frames for the Points/Lines owner.
17
+ *
18
+ * The snapshot deliberately contains no World, ECS row, AssetRegistry, GPU
19
+ * handle, or derived triangle data. Mesh and material generations are the
20
+ * producer-owned fences used by prepare; viewport and projection are the
21
+ * only camera-owned facts in this projection.
22
+ */
23
+ export interface PointsLinesRetainedSnapshot {
24
+ readonly worldId: number;
25
+ readonly entityKey: number;
26
+ readonly component: 'Points' | 'Lines' | undefined;
27
+ readonly meshHandle: number;
28
+ readonly meshGeneration: number;
29
+ readonly materialHandle: number;
30
+ readonly materialGeneration: number;
31
+ readonly style: PointsLinesStyle | undefined;
32
+ readonly layer: number;
33
+ readonly sortKey: number | undefined;
34
+ readonly visible: boolean;
35
+ readonly sourceBounds: Float32Array;
36
+ readonly viewport: PointsLinesViewport;
37
+ readonly projection: Float32Array;
38
+ readonly dedicatedResourceBytes?: number;
39
+ }
40
+
41
+ export interface PointsLinesSnapshotInput
42
+ extends Omit<
43
+ PointsLinesRetainedSnapshot,
44
+ 'sourceBounds' | 'viewport' | 'projection' | 'sortKey'
45
+ > {
46
+ readonly sortKey?: number | undefined;
47
+ readonly sourceBounds: ArrayLike<number>;
48
+ readonly viewport: PointsLinesViewport;
49
+ readonly projection: ArrayLike<number>;
50
+ }
51
+
52
+ export function createPointsLinesSnapshot(
53
+ input: PointsLinesSnapshotInput,
54
+ ): PointsLinesRetainedSnapshot {
55
+ return {
56
+ worldId: input.worldId,
57
+ entityKey: input.entityKey,
58
+ component: input.component,
59
+ meshHandle: input.meshHandle,
60
+ meshGeneration: input.meshGeneration,
61
+ materialHandle: input.materialHandle,
62
+ materialGeneration: input.materialGeneration,
63
+ style: input.style === undefined ? undefined : { ...input.style },
64
+ layer: input.layer,
65
+ sortKey: input.sortKey,
66
+ visible: input.visible,
67
+ sourceBounds: new Float32Array(input.sourceBounds),
68
+ viewport: { ...input.viewport },
69
+ projection: new Float32Array(input.projection),
70
+ ...(input.component === undefined ? { dedicatedResourceBytes: 0 } : {}),
71
+ };
72
+ }
73
+
74
+ export function comparePointsLinesSnapshots(
75
+ previous: PointsLinesRetainedSnapshot,
76
+ next: PointsLinesRetainedSnapshot,
77
+ ): PointsLinesInvalidation {
78
+ if (
79
+ previous.meshHandle !== next.meshHandle ||
80
+ previous.meshGeneration !== next.meshGeneration ||
81
+ !sameArray(previous.sourceBounds, next.sourceBounds)
82
+ ) {
83
+ return 'mesh';
84
+ }
85
+ if (
86
+ previous.materialHandle !== next.materialHandle ||
87
+ previous.materialGeneration !== next.materialGeneration
88
+ ) {
89
+ return 'material';
90
+ }
91
+ if (!sameValue(previous.style, next.style)) return 'style';
92
+ if (
93
+ previous.viewport.width !== next.viewport.width ||
94
+ previous.viewport.height !== next.viewport.height ||
95
+ previous.viewport.dpr !== next.viewport.dpr ||
96
+ !sameArray(previous.projection, next.projection)
97
+ ) {
98
+ return 'view';
99
+ }
100
+ if (
101
+ previous.worldId !== next.worldId ||
102
+ previous.entityKey !== next.entityKey ||
103
+ previous.layer !== next.layer ||
104
+ previous.sortKey !== next.sortKey ||
105
+ previous.visible !== next.visible
106
+ ) {
107
+ return 'scene';
108
+ }
109
+ return 'none';
110
+ }
111
+
112
+ function sameArray(left: ArrayLike<number>, right: ArrayLike<number>): boolean {
113
+ if (left.length !== right.length) return false;
114
+ for (let index = 0; index < left.length; index += 1) {
115
+ if (left[index] !== right[index]) return false;
116
+ }
117
+ return true;
118
+ }
119
+
120
+ function sameValue(
121
+ left: PointsLinesStyle | undefined,
122
+ right: PointsLinesStyle | undefined,
123
+ ): boolean {
124
+ if (left?.kind !== right?.kind) return false;
125
+ if (left === undefined || right === undefined) return left === right;
126
+ if (left.kind === 'points' && right.kind === 'points') {
127
+ return left.sizePx === right.sizePx && left.shape === right.shape;
128
+ }
129
+ return left.kind === 'lines' && right.kind === 'lines' && left.widthPx === right.widthPx;
130
+ }
@@ -57,7 +57,7 @@ import {
57
57
  } from './helpers';
58
58
  import { getOrCreateFromChain, MESH_SSBO_BYTES, MESH_UBO_FULL_ARRAY_BYTES } from './mesh-ssbo';
59
59
  import type { PipelineState, RecordProfileRunner, RenderSystemInternals } from './render-context';
60
- import { VIEW_UNIFORM_BYTES } from './view-ubo';
60
+ import { POINTS_LINES_VIEW_BYTES, VIEW_UNIFORM_BYTES } from './view-ubo';
61
61
 
62
62
  function runHdrpClusterProfilePhase<T>(
63
63
  runner: RecordProfileRunner | undefined,
@@ -161,6 +161,7 @@ export function buildPerFrameBindGroups(
161
161
  b5View,
162
162
  pipelineState.shadowParamsBuffer,
163
163
  b8View,
164
+ pipelineState.pointsLinesViewBuffer ?? pipelineState.viewUniformBuffer,
164
165
  ],
165
166
  'view-main',
166
167
  () => {
@@ -248,10 +249,21 @@ export function buildPerFrameBindGroups(
248
249
  value: b8View,
249
250
  },
250
251
  },
252
+ {
253
+ binding: 10,
254
+ resource: {
255
+ kind: 'buffer',
256
+ value: {
257
+ buffer: pipelineState.pointsLinesViewBuffer ?? pipelineState.viewUniformBuffer,
258
+ size: POINTS_LINES_VIEW_BYTES,
259
+ },
260
+ },
261
+ },
251
262
  // feat-20260625-spot-light-shadow-mapping w25: the per-spot
252
263
  // fragment-read lightViewProj matrices fold into the View UBO
253
264
  // (binding 0, `view.spotLightViewProj`) — no standalone binding 9
254
- // (WebGL2 fragment uniform-buffer budget). binding 8 is the last.
265
+ // (WebGL2 fragment uniform-buffer budget). binding 10 is the
266
+ // dedicated vertex-only Points/Lines viewport UBO.
255
267
  ],
256
268
  });
257
269
  if (!viewBindGroupResult.ok) throw viewBindGroupResult.error;
@@ -206,7 +206,7 @@ import {
206
206
  executeCompiledFrameGraph,
207
207
  type RenderFeatureGraphCandidate,
208
208
  } from './typed-frame-graph';
209
- import { writeViewUbo } from './view-ubo';
209
+ import { writePointsLinesViewUbo, writeViewUbo } from './view-ubo';
210
210
 
211
211
  /**
212
212
  * A frame-local material table. Repeated snapshot objects share one UBO slot;
@@ -320,6 +320,7 @@ export function recordFrame(
320
320
  },
321
321
  renderReadLeases?: readonly RenderReadLease[],
322
322
  featureGraphCandidate?: RenderFeatureGraphCandidate,
323
+ pointsLinesOwner?: import('./render-context').PointsLinesRecordOwner,
323
324
  ): boolean {
324
325
  // The `try / finally` wrapper advances `frameState.frameNumber` exactly
325
326
  // once per `recordFrame` invocation regardless of which early-return
@@ -699,6 +700,15 @@ export function recordFrame(
699
700
  lights,
700
701
  frameState.spotShadowSnapshots,
701
702
  );
703
+ if (pipelineState.pointsLinesViewBuffer !== undefined) {
704
+ writePointsLinesViewUbo(
705
+ internals.device.queue,
706
+ pipelineState.pointsLinesViewBuffer,
707
+ camera,
708
+ targetW,
709
+ targetH,
710
+ );
711
+ }
702
712
  writeShadowCasterUniforms(
703
713
  internals.device.queue,
704
714
  pipelineState.shadowCasterCascadeBuffer,
@@ -816,6 +826,7 @@ export function recordFrame(
816
826
  materialSlots,
817
827
  materialSlotOwners,
818
828
  materialSlotCount,
829
+ pointsLines: pointsLinesOwner,
819
830
  materialBgAssemblyCache: frameState.materialBgAssemblyCache,
820
831
  directionalShadowCacheReuse: directionalShadowCache.reuse,
821
832
  ...(profilePhase !== undefined ? { profilePhase } : {}),
@@ -21,6 +21,8 @@ import type { InstanceBufferCacheEntry } from '../instance-buffer-cache';
21
21
  import { SPRITE_PREMULTIPLIED_ALPHA_BLEND } from '../materials';
22
22
  import { SKIN_MATERIAL_SHADER_ID } from '../pbr-pipeline';
23
23
  import { renderStateHash, variantSetFromVertexLayoutProjection } from '../pipeline-spec';
24
+ import type { PointsLinesRecordPlan } from '../points-lines/record';
25
+ import { POINTS_LINES_MATERIAL_SHADER_ID } from '../points-lines/record';
24
26
  import type { RenderRecordPhase } from '../render-contract';
25
27
  import type { MaterialSnapshot } from '../render-system-extract';
26
28
  import { worldEntityKey } from './frame-snapshot';
@@ -40,6 +42,7 @@ import {
40
42
  } from './mesh-ssbo';
41
43
  import type { _InternalRenderPipelineContext } from './render-context';
42
44
  import { MATERIAL_PER_ENTITY_STRIDE } from './render-context';
45
+ import { POINTS_LINES_VIEW_SLOT_STRIDE, writePointsLinesViewUbo } from './view-ubo';
43
46
 
44
47
  type GeometryInstanceDraw = {
45
48
  readonly instanceBuffer: Buffer;
@@ -115,6 +118,23 @@ function submitSubmeshDraws(
115
118
  }
116
119
  }
117
120
 
121
+ /**
122
+ * Issues a prepared Points/Lines draw into the already-open geometry pass.
123
+ * Pass lifetime, attachments, bind groups, and submission remain owned by the
124
+ * existing main-pass interpreter.
125
+ */
126
+ export function recordPointsLinesDraw(
127
+ pass: RhiRenderPassEncoder,
128
+ plan: PointsLinesRecordPlan,
129
+ ): void {
130
+ if (plan.drawCount === 0) return;
131
+ if (plan.indexCount > 0) {
132
+ pass.drawIndexed(plan.indexCount, 1, 0, 0, 0);
133
+ return;
134
+ }
135
+ pass.draw(plan.vertexCount, 1, 0, 0);
136
+ }
137
+
118
138
  /**
119
139
  * feat-20260704 M3/w19: per-entity geometry (main colour) draw loop, extracted
120
140
  * verbatim from recordMainPass. Walks `c.validatedOrdered`, selects the
@@ -155,6 +175,7 @@ export function recordGeometryDraws(
155
175
  msaaActive,
156
176
  validatedOrdered,
157
177
  splitLdrSprite,
178
+ viewBindGroup,
158
179
  } = c;
159
180
  const colorFormatOverride = c.transparentColorFormat as GPUTextureFormat | undefined;
160
181
  let lastVertexBuffer: GpuBuffer | null = null;
@@ -276,6 +297,13 @@ export function recordGeometryDraws(
276
297
  const entry = validatedOrdered[i];
277
298
  if (entry === undefined) continue;
278
299
 
300
+ // Points/Lines share the Standard geometry pass. Their retained source
301
+ // is prepared once by the renderer owner, while this loop remains the
302
+ // sole pass/draw submission owner and preserves the authored topology.
303
+ const pointsLinesSubmission = c.pointsLines?.prepare(entry, frameState.isHdrpActive);
304
+ if (entry.source.pointsLines !== undefined && pointsLinesSubmission?.plan.drawCount !== 1)
305
+ continue;
306
+
279
307
  if (
280
308
  passKind === 'forward' &&
281
309
  c.gpuDrivenEntityKeys.has(worldEntityKey(entry.source.worldId, entry.source.entityKey))
@@ -316,6 +344,70 @@ export function recordGeometryDraws(
316
344
  lastStencilReference = stencilReference;
317
345
  }
318
346
 
347
+ if (pointsLinesSubmission !== undefined && entry.source.pointsLines !== undefined) {
348
+ const pointsLinesMaterial: MaterialSnapshot = {
349
+ ...entry.source.material,
350
+ materialShaderId: POINTS_LINES_MATERIAL_SHADER_ID,
351
+ materialParamSchema:
352
+ runtime.getParamSchema?.(POINTS_LINES_MATERIAL_SHADER_ID) ??
353
+ entry.source.material.materialParamSchema,
354
+ };
355
+ const materialSlot = materialSlotIndices[i]?.[0] ?? 0;
356
+ const pointsLinesBindGroup = resolveMaterialBindGroup(
357
+ materialSlot,
358
+ pointsLinesMaterial,
359
+ entry.source.entityKey,
360
+ entry.world ?? c.world,
361
+ );
362
+ const pointsLinesPipeline = resolveMaterialPipeline(
363
+ POINTS_LINES_MATERIAL_SHADER_ID,
364
+ {
365
+ ...pointsLinesMaterial.renderState,
366
+ cullMode: 'none',
367
+ },
368
+ 'triangle-list',
369
+ 'uint32',
370
+ undefined,
371
+ colorFormatOverride,
372
+ pointsLinesSubmission.layoutProjection,
373
+ );
374
+ if (pointsLinesPipeline === null) continue;
375
+ if (pipelineState.pointsLinesViewBuffer === undefined) continue;
376
+ writePointsLinesViewUbo(
377
+ runtime.device.queue,
378
+ pipelineState.pointsLinesViewBuffer,
379
+ c.camera,
380
+ c.targetW,
381
+ c.targetH,
382
+ entry.source.transform.world,
383
+ entry.source.pointsLines.style,
384
+ i * POINTS_LINES_VIEW_SLOT_STRIDE,
385
+ );
386
+ pass.setBindGroup(
387
+ 0,
388
+ viewBindGroup as BindGroup,
389
+ new Uint32Array([i * POINTS_LINES_VIEW_SLOT_STRIDE]),
390
+ 0,
391
+ 1,
392
+ );
393
+ pass.setPipeline(pointsLinesPipeline);
394
+ pass.setVertexBuffer(0, pointsLinesSubmission.vertexBuffer);
395
+ pass.setIndexBuffer(pointsLinesSubmission.indexBuffer, 'uint32');
396
+ materialGroup1DynamicOffsets[0] = materialSlot * MATERIAL_PER_ENTITY_STRIDE;
397
+ pass.setBindGroup(1, pointsLinesBindGroup, materialGroup1DynamicOffsets, 0, 1);
398
+ const pointsLinesMeshGroup = meshBindGroup ?? meshGroup2;
399
+ if (pointsLinesMeshGroup === null) continue;
400
+ pass.setBindGroup(2, pointsLinesMeshGroup, meshGroup2DynamicOffsets);
401
+ pass.setBindGroup(
402
+ 3,
403
+ identityInstanceDraws[0]?.instanceBindGroup ??
404
+ resolveGeometryInstancesBindGroup(c, identityInstanceBuffer),
405
+ );
406
+ recordPointsLinesDraw(pass, pointsLinesSubmission.plan);
407
+ pass.setBindGroup(0, viewBindGroup as BindGroup, [0]);
408
+ continue;
409
+ }
410
+
319
411
  if (entry.mesh.vertexBuffer !== lastVertexBuffer) {
320
412
  pass.setVertexBuffer(0, entry.mesh.vertexBuffer.handle);
321
413
  lastVertexBuffer = entry.mesh.vertexBuffer;
@@ -203,7 +203,7 @@ export function recordSpritePass(
203
203
  ) as never,
204
204
  );
205
205
 
206
- spritePass.setBindGroup(0, viewBindGroup as BindGroup);
206
+ spritePass.setBindGroup(0, viewBindGroup as BindGroup, [0]);
207
207
 
208
208
  // feat-20260625-refactor-sprite-as-transparent-mesh M3 / w14 (D-7):
209
209
  // sprite PSO resolution migrated from the dedicated boot-time PSO
@@ -10,6 +10,7 @@ import type { Handle, PassKind, PassSelector } from '@forgeax/engine-types';
10
10
  import { createHdrpUnifiedBindGroup, getOrCreateHdrpBuffers } from '../hdrp-buffers';
11
11
  import { getOrCreateIblCache } from '../ibl/IblPipelineCache';
12
12
  import { buildBeginRenderPassDescriptor } from '../pipeline-spec';
13
+ import { POINTS_LINES_MATERIAL_SHADER_ID } from '../points-lines/record';
13
14
  import type { RenderRecordPhase } from '../render-contract';
14
15
  import type { MaterialSnapshot } from '../render-system-extract';
15
16
  import { recordGeometryDraws } from './main-pass-geometry';
@@ -535,10 +536,12 @@ export function recordMainPass(
535
536
  entityKey: number,
536
537
  materialWorld: World = world,
537
538
  ): BindGroup =>
538
- preparedMaterialBindGroups[materialSlot] ??
539
- buildPerSubmeshMaterialBg(material, entityKey, materialWorld);
539
+ material.materialShaderId !== POINTS_LINES_MATERIAL_SHADER_ID
540
+ ? (preparedMaterialBindGroups[materialSlot] ??
541
+ buildPerSubmeshMaterialBg(material, entityKey, materialWorld))
542
+ : buildPerSubmeshMaterialBg(material, entityKey, materialWorld);
540
543
 
541
- pass.setBindGroup(0, viewBindGroup as BindGroup);
544
+ pass.setBindGroup(0, viewBindGroup as BindGroup, [0]);
542
545
 
543
546
  // Track which (mesh-vertex-buffer, mesh-index-buffer, pipeline) combo
544
547
  // was last bound so consecutive entities sharing the same combo skip