@forgeax/engine-render 0.1.19 → 0.1.20

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 (174) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/assembly/factory.d.ts +16 -5
  3. package/dist/assembly/factory.d.ts.map +1 -1
  4. package/dist/{chunk-RJRUS55L.mjs → chunk-4XINDLAY.mjs} +267 -186
  5. package/dist/chunk-4XINDLAY.mjs.map +1 -0
  6. package/dist/{chunk-YTQ5C7YQ.mjs → chunk-CDTWTHZR.mjs} +176 -8
  7. package/dist/chunk-CDTWTHZR.mjs.map +1 -0
  8. package/dist/{chunk-IPXCABEP.mjs → chunk-M2MP4ZOU.mjs} +2 -2
  9. package/dist/chunk-M2MP4ZOU.mjs.map +1 -0
  10. package/dist/{chunk-LOSO4CTV.mjs → chunk-TBQJOK2L.mjs} +67 -3
  11. package/dist/chunk-TBQJOK2L.mjs.map +1 -0
  12. package/dist/components/light-helpers.d.ts.map +1 -1
  13. package/dist/components/spot-light.d.ts +12 -0
  14. package/dist/components/spot-light.d.ts.map +1 -1
  15. package/dist/construct-renderer.mjs +2281 -164
  16. package/dist/construct-renderer.mjs.map +1 -1
  17. package/dist/device/gpu-residency.d.ts +19 -2
  18. package/dist/device/gpu-residency.d.ts.map +1 -1
  19. package/dist/errors/render.d.ts +69 -1
  20. package/dist/errors/render.d.ts.map +1 -1
  21. package/dist/gpu-usage.d.ts +2 -0
  22. package/dist/gpu-usage.d.ts.map +1 -1
  23. package/dist/index.d.ts +7 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.mjs +3 -3
  26. package/dist/internal.mjs +1 -1
  27. package/dist/light-buffer-layout.d.ts +20 -12
  28. package/dist/light-buffer-layout.d.ts.map +1 -1
  29. package/dist/pbr-pipeline.d.ts +6 -3
  30. package/dist/pbr-pipeline.d.ts.map +1 -1
  31. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  32. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  33. package/dist/pipeline/standard-profile.d.ts +13 -0
  34. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  35. package/dist/record/frame-lighting.d.ts +2 -0
  36. package/dist/record/frame-lighting.d.ts.map +1 -1
  37. package/dist/record/frame-snapshot.d.ts +37 -1
  38. package/dist/record/frame-snapshot.d.ts.map +1 -1
  39. package/dist/record/frame-targets.d.ts.map +1 -1
  40. package/dist/record/frame.d.ts +3 -2
  41. package/dist/record/frame.d.ts.map +1 -1
  42. package/dist/record/gpu-timing.d.ts +32 -0
  43. package/dist/record/gpu-timing.d.ts.map +1 -0
  44. package/dist/record/main-pass-material.d.ts +1 -1
  45. package/dist/record/main-pass-material.d.ts.map +1 -1
  46. package/dist/record/render-context.d.ts +5 -1
  47. package/dist/record/render-context.d.ts.map +1 -1
  48. package/dist/record/shadow-pass.d.ts.map +1 -1
  49. package/dist/record/typed-frame-graph.d.ts +11 -3
  50. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  51. package/dist/render-contract.d.ts +66 -1
  52. package/dist/render-contract.d.ts.map +1 -1
  53. package/dist/render-data.d.ts +10 -6
  54. package/dist/render-data.d.ts.map +1 -1
  55. package/dist/render-pipeline.d.ts +36 -0
  56. package/dist/render-pipeline.d.ts.map +1 -1
  57. package/dist/render-system-extract.d.ts +55 -3
  58. package/dist/render-system-extract.d.ts.map +1 -1
  59. package/dist/render-system.d.ts +7 -1
  60. package/dist/render-system.d.ts.map +1 -1
  61. package/dist/renderer-inspect.d.ts +29 -0
  62. package/dist/renderer-inspect.d.ts.map +1 -0
  63. package/dist/scene/render-scene.d.ts.map +1 -1
  64. package/dist/temporal/frame.d.ts +44 -0
  65. package/dist/temporal/frame.d.ts.map +1 -0
  66. package/dist/temporal/index.mjs +3 -3
  67. package/dist/typed-render-graph-primitives.d.ts +6 -1
  68. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  69. package/dist/typed-shadow-atlas.d.ts +10 -0
  70. package/dist/typed-shadow-atlas.d.ts.map +1 -0
  71. package/dist/typed-shadow-passes.d.ts.map +1 -1
  72. package/dist/volume/capability.d.ts +57 -0
  73. package/dist/volume/capability.d.ts.map +1 -0
  74. package/dist/volume/component.d.ts +61 -0
  75. package/dist/volume/component.d.ts.map +1 -0
  76. package/dist/volume/extract.d.ts +11 -0
  77. package/dist/volume/extract.d.ts.map +1 -0
  78. package/dist/volume/inspection.d.ts +38 -0
  79. package/dist/volume/inspection.d.ts.map +1 -0
  80. package/dist/volume/passes.d.ts +38 -0
  81. package/dist/volume/passes.d.ts.map +1 -0
  82. package/dist/volume/recovery.d.ts +38 -0
  83. package/dist/volume/recovery.d.ts.map +1 -0
  84. package/dist/volume/resources.d.ts +57 -0
  85. package/dist/volume/resources.d.ts.map +1 -0
  86. package/dist/volume/temporal.d.ts +35 -0
  87. package/dist/volume/temporal.d.ts.map +1 -0
  88. package/package.json +20 -20
  89. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -14
  90. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  91. package/src/__tests__/render-error-code-owner.test-d.ts +5 -0
  92. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +28 -0
  93. package/src/__tests__/renderer-frame-transaction.integration.test.ts +1 -1
  94. package/src/__tests__/skinned-shadow-caster.test.ts +21 -0
  95. package/src/__tests__/standard-profile-oracle.unit.test.ts +7 -5
  96. package/src/__tests__/volume-optics-oracle.unit.test.ts +85 -0
  97. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +655 -0
  98. package/src/__tests__/volumetric-fog-component.unit.test.ts +71 -0
  99. package/src/__tests__/volumetric-fog-composite-stage-readback.dawn.test.ts +376 -0
  100. package/src/__tests__/volumetric-fog-errors.unit.test.ts +51 -0
  101. package/src/__tests__/volumetric-fog-inspection.unit.test.ts +77 -0
  102. package/src/__tests__/volumetric-fog-light-capability.unit.test.ts +19 -0
  103. package/src/__tests__/volumetric-fog-light-consumers.integration.test.ts +18 -0
  104. package/src/__tests__/volumetric-fog-light-errors.unit.test.ts +17 -0
  105. package/src/__tests__/volumetric-fog-light-topology.unit.test.ts +21 -0
  106. package/src/__tests__/volumetric-fog-light-transaction.integration.test.ts +53 -0
  107. package/src/__tests__/volumetric-fog-m9-watcher.integration.test.ts +27 -0
  108. package/src/__tests__/volumetric-fog-point-light-owner.integration.test.ts +44 -0
  109. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +49 -0
  110. package/src/__tests__/volumetric-fog-point-spot-contract.unit.test.ts +28 -0
  111. package/src/__tests__/volumetric-fog-point-spot-lkg.integration.test.ts +65 -0
  112. package/src/__tests__/volumetric-fog-point-spot-selection.integration.test.ts +49 -0
  113. package/src/__tests__/volumetric-fog-point-spot-temporal.unit.test.ts +55 -0
  114. package/src/__tests__/volumetric-fog-projector-contract.unit.test.ts +22 -0
  115. package/src/__tests__/volumetric-fog-projector-inspection.unit.test.ts +12 -0
  116. package/src/__tests__/volumetric-fog-projector-surface-volume.integration.test.ts +11 -0
  117. package/src/__tests__/volumetric-fog-projector-transaction.integration.test.ts +26 -0
  118. package/src/__tests__/volumetric-fog-recovery.integration.test.ts +47 -0
  119. package/src/__tests__/volumetric-fog-selected-light-resolution.integration.test.ts +63 -0
  120. package/src/__tests__/volumetric-fog-spot-closure.integration.test.ts +26 -0
  121. package/src/__tests__/volumetric-fog-spot-contract.test-d.ts +21 -0
  122. package/src/__tests__/volumetric-fog-spot-contract.unit.test.ts +27 -0
  123. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +14 -0
  124. package/src/__tests__/volumetric-fog-spot-optics.integration.test.ts +29 -0
  125. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +550 -0
  126. package/src/__tests__/volumetric-fog-temporal-selected-light.unit.test.ts +33 -0
  127. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +45 -0
  128. package/src/__tests__/volumetric-fog-topology.unit.test.ts +237 -0
  129. package/src/__tests__/volumetric-fog-transparent.integration.test.ts +27 -0
  130. package/src/assembly/factory.ts +271 -15
  131. package/src/components/light-helpers.ts +4 -0
  132. package/src/components/spot-light.ts +22 -3
  133. package/src/device/gpu-residency.ts +168 -58
  134. package/src/errors/render.ts +119 -1
  135. package/src/gpu-usage.ts +2 -0
  136. package/src/index.ts +38 -1
  137. package/src/light-buffer-layout.ts +59 -26
  138. package/src/pbr-pipeline.ts +33 -10
  139. package/src/pipeline/standard-forward-lane.ts +20 -0
  140. package/src/pipeline/standard-pipeline.ts +20 -0
  141. package/src/pipeline/standard-profile.ts +23 -0
  142. package/src/record/frame-lighting.ts +103 -89
  143. package/src/record/frame-snapshot.ts +41 -1
  144. package/src/record/frame-targets.ts +6 -2
  145. package/src/record/frame.ts +908 -5
  146. package/src/record/gpu-timing.ts +224 -0
  147. package/src/record/main-pass-material.ts +6 -2
  148. package/src/record/render-context.ts +5 -0
  149. package/src/record/shadow-pass.ts +87 -38
  150. package/src/record/typed-frame-graph.ts +143 -8
  151. package/src/render-contract.ts +72 -0
  152. package/src/render-data.ts +50 -22
  153. package/src/render-pipeline.ts +44 -0
  154. package/src/render-system-extract.ts +329 -3
  155. package/src/render-system.ts +79 -0
  156. package/src/renderer-inspect.ts +66 -0
  157. package/src/scene/render-scene.ts +2 -0
  158. package/src/temporal/frame.ts +128 -0
  159. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +2 -3
  160. package/src/typed-render-graph-primitives.ts +32 -2
  161. package/src/typed-shadow-atlas.ts +17 -0
  162. package/src/typed-shadow-passes.ts +6 -5
  163. package/src/volume/capability.ts +173 -0
  164. package/src/volume/component.ts +110 -0
  165. package/src/volume/extract.ts +21 -0
  166. package/src/volume/inspection.ts +78 -0
  167. package/src/volume/passes.ts +680 -0
  168. package/src/volume/recovery.ts +86 -0
  169. package/src/volume/resources.ts +236 -0
  170. package/src/volume/temporal.ts +89 -0
  171. package/dist/chunk-IPXCABEP.mjs.map +0 -1
  172. package/dist/chunk-LOSO4CTV.mjs.map +0 -1
  173. package/dist/chunk-RJRUS55L.mjs.map +0 -1
  174. package/dist/chunk-YTQ5C7YQ.mjs.map +0 -1
@@ -142,6 +142,7 @@ import type {
142
142
  ParamSchemaEntry,
143
143
  PrimitiveTopology,
144
144
  SkeletonAsset,
145
+ TextureAsset,
145
146
  } from '@forgeax/engine-types';
146
147
  import {
147
148
  ASSET_ERROR_HINTS,
@@ -209,6 +210,15 @@ export { extractCameraSnapshots } from './extract/camera';
209
210
 
210
211
  import type { SkinPaletteAllocator } from './systems/skin-palette-allocator';
211
212
  import type { SkinPaletteSlice } from './systems/skin-palette-types';
213
+ import { resolveSelectedVolumetricLight } from './volume/capability';
214
+ import {
215
+ type ValidatedVolumetricFog,
216
+ type VolumeDensityBinding,
217
+ VolumetricFog,
218
+ type VolumetricFogAuthoring,
219
+ } from './volume/component';
220
+ import { extractVolumetricFog } from './volume/extract';
221
+ import type { VolumeProjectorTuple } from './volume/temporal';
212
222
 
213
223
  /**
214
224
  * DirectionalLightSnapshot — sun-like infinite light variant of the
@@ -221,6 +231,7 @@ import type { SkinPaletteSlice } from './systems/skin-palette-types';
221
231
  */
222
232
  export interface DirectionalLightSnapshot {
223
233
  readonly kind: 'directional';
234
+ readonly entity?: EntityHandle;
224
235
  readonly direction: Vec3;
225
236
  readonly color: Vec3;
226
237
  readonly intensity: number;
@@ -234,6 +245,7 @@ export interface DirectionalLightSnapshot {
234
245
  */
235
246
  export interface PointLightSnapshot {
236
247
  readonly kind: 'point';
248
+ readonly entity?: EntityHandle | number;
237
249
  readonly position: Vec3;
238
250
  readonly color: Vec3;
239
251
  readonly intensity: number;
@@ -317,10 +329,15 @@ export interface PointShadowSnapshot {
317
329
  * is false, dir degenerates, or the light is clipped). shadowAtlasTile
318
330
  * (i32 sentinel -1) is the allocated tile index 0..3 or -1 for unassigned
319
331
  * (plan-strategy D-4). mapSize / nearPlane / farPlane are the source
320
- * component shadow parameters carried through to the record stage.
332
+ * component shadow parameters carried through to the record stage. The
333
+ * author-facing `shadowIntensity` remains in [0,1] and is copied unchanged;
334
+ * shader consumers apply it only to shadow visibility, never to candela
335
+ * radiance.
321
336
  */
322
337
  export interface SpotLightSnapshot {
323
338
  readonly kind: 'spot';
339
+ /** Entity identity is branded in extracted frames; numeric fixtures remain compatible. */
340
+ readonly entity?: EntityHandle | number;
324
341
  readonly position: Vec3;
325
342
  readonly direction: Vec3;
326
343
  readonly color: Vec3;
@@ -335,6 +352,17 @@ export interface SpotLightSnapshot {
335
352
  readonly nearPlane: number;
336
353
  readonly farPlane: number;
337
354
  readonly shadowAtlasTile: number;
355
+ /** Fraction of sampled shadow visibility to retain; 1 = fully opaque shadow. */
356
+ readonly shadowIntensity?: number;
357
+ readonly depthBias?: number;
358
+ readonly normalBias?: number;
359
+ readonly pcfKernelSize?: number;
360
+ /** Authoritative TextureAsset projector resolved from the SpotLight handle. */
361
+ readonly projectorHandle?: Handle<'TextureAsset', 'shared'>;
362
+ readonly projectorAsset?: TextureAsset;
363
+ readonly projectorGuid?: string;
364
+ readonly projectorGeneration?: number;
365
+ readonly projectorRevision?: number;
338
366
  }
339
367
 
340
368
  /**
@@ -505,6 +533,37 @@ export interface SkyboxSnapshot {
505
533
  readonly entityHandle: number;
506
534
  }
507
535
 
536
+ /**
537
+ * Resource-owner projection for the single ECS VolumetricFog singleton.
538
+ *
539
+ * The renderer carries the validated authoring facts and the source POD all
540
+ * the way to the record stage. No profile flag or source/catalog byte is
541
+ * allowed to enable the graph: `status: 'available'` is produced only after
542
+ * the ECS component, its shared TextureAsset, and the volume validator all
543
+ * agree on one linear 3D density owner.
544
+ */
545
+ export interface ExtractedVolumetricFog {
546
+ readonly status: 'off' | 'available' | 'degraded';
547
+ readonly fog?: ValidatedVolumetricFog;
548
+ readonly densityHandle?: Handle<'TextureAsset', 'shared'>;
549
+ readonly densityAsset?: TextureAsset;
550
+ readonly guid?: string;
551
+ readonly generation?: number;
552
+ readonly digest?: string;
553
+ readonly worldId?: number;
554
+ /** Required same-World light selected by the authoring component. */
555
+ readonly lightEntity?: EntityHandle;
556
+ readonly lightKind?: 'directional' | 'point' | 'spot';
557
+ /** Optional Point+Spot pair identities consumed by the volume integrator. */
558
+ readonly pointLightEntity?: EntityHandle;
559
+ readonly spotLightEntity?: EntityHandle;
560
+ /** Accepted projector tuple shared by surface and volume when authored. */
561
+ readonly projector?: VolumeProjectorTuple;
562
+ /** The resolved projector payload shared with the surface light snapshot. */
563
+ readonly projectorHandle?: Handle<'TextureAsset', 'shared'>;
564
+ readonly projectorAsset?: TextureAsset;
565
+ }
566
+
508
567
  export interface RenderableSnapshot {
509
568
  readonly assetHandle: number;
510
569
  readonly transform: TransformSnapshot;
@@ -1006,6 +1065,8 @@ export interface ExtractedFrame {
1006
1065
  readonly environment: EnvironmentFrame | undefined;
1007
1066
  /** False when the resource-owner world failed environment/fog selection. */
1008
1067
  readonly environmentReady: boolean;
1068
+ /** One resource-owner volume projection; absent means no authored fog. */
1069
+ readonly volumetricFog?: ExtractedVolumetricFog;
1009
1070
  readonly renderables: RenderableSnapshot[];
1010
1071
  /**
1011
1072
  * Single dispatch list sorted by queue value (ascending, stable sort).
@@ -2308,6 +2369,178 @@ function selectFogFrame(world: World): FogFrame | undefined {
2308
2369
  throw fogSelection.error;
2309
2370
  }
2310
2371
 
2372
+ function textureDigest(texture: TextureAsset): string {
2373
+ // The render inspection needs a stable identity token, not a second byte
2374
+ // store. FNV-1a is deterministic, synchronous, and deliberately labelled
2375
+ // as a non-cryptographic digest so it cannot be mistaken for Catalog proof.
2376
+ let hash = 2166136261;
2377
+ for (const byte of texture.data) hash = Math.imul(hash ^ byte, 16777619) >>> 0;
2378
+ return `fnv1a:${hash.toString(16).padStart(8, '0')}`;
2379
+ }
2380
+
2381
+ function extractVolumeSnapshot(
2382
+ world: World,
2383
+ assets: AssetRegistry | null | undefined,
2384
+ worldInternal: WorldInternalView,
2385
+ ): ExtractedVolumetricFog | undefined {
2386
+ const query = world.query({ read: [VolumetricFog] }).unwrap();
2387
+ const authorings: VolumetricFogAuthoring[] = [];
2388
+ let selectedLight:
2389
+ | {
2390
+ readonly entity: EntityHandle;
2391
+ readonly kind: 'directional' | 'point' | 'spot';
2392
+ readonly pointLightEntity?: EntityHandle;
2393
+ readonly spotLightEntity?: EntityHandle;
2394
+ }
2395
+ | undefined;
2396
+ let first:
2397
+ | {
2398
+ readonly densityHandle: Handle<'TextureAsset', 'shared'>;
2399
+ readonly densityAsset: TextureAsset;
2400
+ readonly guid: string;
2401
+ }
2402
+ | undefined;
2403
+
2404
+ for (const row of query) {
2405
+ const source = row.get(VolumetricFog);
2406
+ if (source.light === null) continue;
2407
+ const selected = resolveSelectedVolumetricLight(world, source.light);
2408
+ if (selected.status === 'available') {
2409
+ const pairSpot = source.spotLight === null ? undefined : source.spotLight;
2410
+ selectedLight ??= {
2411
+ entity: selected.entity,
2412
+ kind: selected.kind,
2413
+ ...(selected.kind === 'point' ? { pointLightEntity: selected.entity } : {}),
2414
+ ...(pairSpot === undefined ? {} : { spotLightEntity: pairSpot }),
2415
+ };
2416
+ }
2417
+ const densityHandle = toShared<'TextureAsset'>(Math.round(Number(source.density)));
2418
+ const resolved = resolveAssetHandle<TextureAsset>(world, densityHandle);
2419
+ if (!resolved.ok || resolved.value.kind !== 'texture') {
2420
+ worldInternal._routeError(
2421
+ resolved.ok
2422
+ ? new RhiError({
2423
+ code: 'asset-not-registered',
2424
+ expected: 'VolumetricFog.density resolves to a TextureAsset',
2425
+ hint: 'load a TextureAsset before assigning it to VolumetricFog.density',
2426
+ detail: { assetHandle: Number(densityHandle) },
2427
+ })
2428
+ : resolved.error,
2429
+ { severity: Severity.Error, systemName: 'RenderSystem.extract (volumetric-density)' },
2430
+ );
2431
+ return { status: 'degraded' };
2432
+ }
2433
+ const densityAsset = resolved.value;
2434
+ const guid = assets?.guidOf(densityAsset) ?? `handle:${Number(densityHandle)}`;
2435
+ const shape = densityAsset.shape;
2436
+ const density: VolumeDensityBinding =
2437
+ shape.viewDimension === '3d'
2438
+ ? {
2439
+ guid,
2440
+ generation: 1,
2441
+ shape: {
2442
+ viewDimension: '3d',
2443
+ extent: {
2444
+ width: shape.extent.width,
2445
+ height: shape.extent.height,
2446
+ depth: shape.extent.depth,
2447
+ },
2448
+ },
2449
+ format: densityAsset.format,
2450
+ colorSpace: densityAsset.colorSpace,
2451
+ }
2452
+ : {
2453
+ guid,
2454
+ generation: 1,
2455
+ shape: {
2456
+ viewDimension: shape.viewDimension,
2457
+ extent:
2458
+ shape.viewDimension === '2d-array'
2459
+ ? {
2460
+ width: shape.extent.width,
2461
+ height: shape.extent.height,
2462
+ layers: shape.extent.layers,
2463
+ }
2464
+ : { width: shape.extent.width, height: shape.extent.height },
2465
+ },
2466
+ format: densityAsset.format,
2467
+ colorSpace: densityAsset.colorSpace,
2468
+ };
2469
+ const authoring: VolumetricFogAuthoring = {
2470
+ light: source.light,
2471
+ density,
2472
+ bounds: {
2473
+ min: [source.boundsMin[0] ?? 0, source.boundsMin[1] ?? 0, source.boundsMin[2] ?? 0],
2474
+ max: [source.boundsMax[0] ?? 0, source.boundsMax[1] ?? 0, source.boundsMax[2] ?? 0],
2475
+ },
2476
+ extinction: [source.extinction[0] ?? 0, source.extinction[1] ?? 0, source.extinction[2] ?? 0],
2477
+ albedo: [source.albedo[0] ?? 0, source.albedo[1] ?? 0, source.albedo[2] ?? 0],
2478
+ emission: [source.emission[0] ?? 0, source.emission[1] ?? 0, source.emission[2] ?? 0],
2479
+ anisotropy: source.anisotropy,
2480
+ maxDistance: source.maxDistance,
2481
+ };
2482
+ authorings.push(authoring);
2483
+ first ??= { densityHandle, densityAsset, guid };
2484
+ }
2485
+
2486
+ if (authorings.length === 0) return undefined;
2487
+ const extracted = extractVolumetricFog(authorings);
2488
+ if (!extracted.ok) {
2489
+ worldInternal._routeError(extracted.error, {
2490
+ severity: Severity.Error,
2491
+ systemName: 'RenderSystem.extract (volumetric-fog)',
2492
+ });
2493
+ return {
2494
+ status: 'degraded',
2495
+ ...(first === undefined
2496
+ ? {}
2497
+ : {
2498
+ densityHandle: first.densityHandle,
2499
+ densityAsset: first.densityAsset,
2500
+ guid: first.guid,
2501
+ generation: 1,
2502
+ digest: textureDigest(first.densityAsset),
2503
+ ...(selectedLight === undefined
2504
+ ? {}
2505
+ : {
2506
+ lightEntity: selectedLight.entity,
2507
+ lightKind: selectedLight.kind,
2508
+ ...(selectedLight.pointLightEntity === undefined
2509
+ ? {}
2510
+ : { pointLightEntity: selectedLight.pointLightEntity }),
2511
+ ...(selectedLight.spotLightEntity === undefined
2512
+ ? {}
2513
+ : { spotLightEntity: selectedLight.spotLightEntity }),
2514
+ }),
2515
+ }),
2516
+ };
2517
+ }
2518
+ if (extracted.value.status === 'off' || first === undefined) return { status: 'off' };
2519
+ const selectedLightFields =
2520
+ selectedLight === undefined
2521
+ ? {}
2522
+ : {
2523
+ lightEntity: selectedLight.entity,
2524
+ lightKind: selectedLight.kind,
2525
+ ...(selectedLight.pointLightEntity === undefined
2526
+ ? {}
2527
+ : { pointLightEntity: selectedLight.pointLightEntity }),
2528
+ ...(selectedLight.spotLightEntity === undefined
2529
+ ? {}
2530
+ : { spotLightEntity: selectedLight.spotLightEntity }),
2531
+ };
2532
+ return {
2533
+ status: 'available',
2534
+ fog: extracted.value.fog,
2535
+ densityHandle: first.densityHandle,
2536
+ densityAsset: first.densityAsset,
2537
+ guid: first.guid,
2538
+ generation: 1,
2539
+ digest: textureDigest(first.densityAsset),
2540
+ ...selectedLightFields,
2541
+ };
2542
+ }
2543
+
2311
2544
  export function extractFrames(
2312
2545
  worlds: readonly World[],
2313
2546
  owner: number | ExtractFramesOwner,
@@ -2565,6 +2798,7 @@ export function extractFrames(
2565
2798
  directionalCsmConfig,
2566
2799
  directionalCsmDirection,
2567
2800
  };
2801
+ const volumetricFog = resourceOwnerFrame?.volumetricFog;
2568
2802
  const skylight = resourceOwnerFrame?.skylight;
2569
2803
  const skylightCount = resourceOwnerFrame?.skylightCount ?? 0;
2570
2804
  const skybox = resourceOwnerFrame?.skybox;
@@ -2613,6 +2847,9 @@ export function extractFrames(
2613
2847
  return {
2614
2848
  cameras,
2615
2849
  lights,
2850
+ ...(volumetricFog === undefined
2851
+ ? {}
2852
+ : { volumetricFog: { ...volumetricFog, worldId: resourceOwner } }),
2616
2853
  renderables,
2617
2854
  dispatch: dispatchEntries,
2618
2855
  skylight,
@@ -2732,6 +2969,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2732
2969
  // `row.get(X) !== undefined`); K-3 invariant preserved (`_getArrayView`
2733
2970
  // calls survive untouched, only the entity source changes).
2734
2971
  const worldInternal = createWorldInternalView(world);
2972
+ let volumetricFog = extractVolumeSnapshot(world, assets, worldInternal);
2735
2973
 
2736
2974
  const cameras = extractCameraSnapshots(world);
2737
2975
 
@@ -2763,6 +3001,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2763
3001
  const intensity = l.intensity;
2764
3002
  const snapshot: DirectionalLightSnapshot = {
2765
3003
  kind: 'directional',
3004
+ entity: row.entity,
2766
3005
  direction: vec3.create(l.direction[0] ?? 0, l.direction[1] ?? -1, l.direction[2] ?? 0),
2767
3006
  color: vec3.create(
2768
3007
  (l.color[0] ?? 1) * intensity,
@@ -2818,6 +3057,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2818
3057
  worldMat !== undefined ? mat4.getTranslation(vec3.create(), worldMat) : vec3.create(0, 0, 0);
2819
3058
  pointSnapshots.push({
2820
3059
  kind: 'point',
3060
+ entity: entityId,
2821
3061
  position,
2822
3062
  color: vec3.create(
2823
3063
  (p.color[0] ?? 1) * intensity,
@@ -2889,11 +3129,20 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2889
3129
  const sMapSize = s.mapSize;
2890
3130
  const sNearPlane = s.nearPlane;
2891
3131
  const sFarPlane = s.farPlane;
3132
+ const shadowIntensity = s.shadowIntensity;
3133
+ // `shared<T>` columns are u32-backed and zero-filled when the optional
3134
+ // field is absent. Treat only a positive handle as an authored projector;
3135
+ // checking nullish values alone would turn the zero sentinel into a
3136
+ // phantom projector matrix and an avoidable asset-not-found diagnostic.
3137
+ const hasProjector = Number(s.projector) > 0;
2892
3138
 
2893
3139
  let lightViewProj: Float32Array | undefined;
2894
3140
  let shadowAtlasTile = -1;
2895
3141
 
2896
- if (castShadow && hasValidDirection) {
3142
+ // A projector needs the same perspective transform even when its SpotLight
3143
+ // has no shadow atlas tile. The matrix remains a derived light fact; the
3144
+ // shadow tile stays -1 so shadow visibility is still independently gated.
3145
+ if ((castShadow || hasProjector) && hasValidDirection) {
2897
3146
  const target = vec3.create(
2898
3147
  (position[0] ?? 0) + (dirN[0] ?? 0),
2899
3148
  (position[1] ?? 0) + (dirN[1] ?? 0),
@@ -2912,14 +3161,40 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2912
3161
  mat4.multiply(lightViewProj as Mat4, proj, view);
2913
3162
 
2914
3163
  // D-4: allocate tile 0..3; 5th+ = -1 sentinel.
2915
- if (spotTileNext < 4) {
3164
+ if (castShadow && spotTileNext < 4) {
2916
3165
  shadowAtlasTile = spotTileNext;
2917
3166
  spotTileNext += 1;
2918
3167
  }
2919
3168
  }
2920
3169
 
3170
+ let projectorHandle: Handle<'TextureAsset', 'shared'> | undefined;
3171
+ let projectorAsset: TextureAsset | undefined;
3172
+ let projectorGuid: string | undefined;
3173
+ if (hasProjector) {
3174
+ const candidate = toShared<'TextureAsset'>(Math.round(Number(s.projector)));
3175
+ const resolvedProjector = resolveAssetHandle<TextureAsset>(world, candidate);
3176
+ if (!resolvedProjector.ok || resolvedProjector.value.kind !== 'texture') {
3177
+ worldInternal._routeError(
3178
+ resolvedProjector.ok
3179
+ ? new RhiError({
3180
+ code: 'asset-not-registered',
3181
+ expected: 'SpotLight.projector resolves to a TextureAsset',
3182
+ hint: 'load the authored projector TextureAsset before assigning SpotLight.projector',
3183
+ detail: { assetHandle: Number(candidate) },
3184
+ })
3185
+ : resolvedProjector.error,
3186
+ { severity: Severity.Error, systemName: 'RenderSystem.extract (spot-projector)' },
3187
+ );
3188
+ } else {
3189
+ projectorHandle = candidate;
3190
+ projectorAsset = resolvedProjector.value;
3191
+ projectorGuid = assets?.guidOf(projectorAsset) ?? `handle:${Number(candidate)}`;
3192
+ }
3193
+ }
3194
+
2921
3195
  spotSnapshots.push({
2922
3196
  kind: 'spot',
3197
+ entity: row.entity,
2923
3198
  // D-6: position reflects world transform; direction stays sourced
2924
3199
  // from SpotLight.direction (NOT rotated by the parent).
2925
3200
  position,
@@ -2940,9 +3215,59 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2940
3215
  nearPlane: sNearPlane,
2941
3216
  farPlane: sFarPlane,
2942
3217
  shadowAtlasTile,
3218
+ shadowIntensity,
3219
+ depthBias: s.depthBias,
3220
+ normalBias: s.normalBias,
3221
+ pcfKernelSize: s.pcfKernelSize,
3222
+ ...(projectorHandle === undefined ||
3223
+ projectorAsset === undefined ||
3224
+ projectorGuid === undefined
3225
+ ? {}
3226
+ : {
3227
+ projectorHandle,
3228
+ projectorAsset,
3229
+ projectorGuid,
3230
+ projectorGeneration: 1,
3231
+ projectorRevision: 1,
3232
+ }),
2943
3233
  });
2944
3234
  }
2945
3235
 
3236
+ // The SpotLight is the sole projector owner. Once all light snapshots have
3237
+ // been extracted, publish its accepted TextureAsset tuple on the volume
3238
+ // snapshot so surface and volume consume the same identity and revision.
3239
+ let volumeSpot: SpotLightSnapshot | undefined;
3240
+ const extractedFog = volumetricFog;
3241
+ if (extractedFog !== undefined) {
3242
+ if (extractedFog.spotLightEntity !== undefined) {
3243
+ volumeSpot = spotSnapshots.find((light) => light.entity === extractedFog.spotLightEntity);
3244
+ } else if (extractedFog.lightKind === 'spot') {
3245
+ volumeSpot = spotSnapshots.find((light) => light.entity === extractedFog.lightEntity);
3246
+ }
3247
+ }
3248
+ if (
3249
+ volumetricFog !== undefined &&
3250
+ volumetricFog.status === 'available' &&
3251
+ volumeSpot?.projectorAsset !== undefined &&
3252
+ volumeSpot.projectorHandle !== undefined
3253
+ ) {
3254
+ const projectorHandle = volumeSpot.projectorHandle;
3255
+ const projectorAsset = volumeSpot.projectorAsset;
3256
+ volumetricFog = {
3257
+ ...volumetricFog,
3258
+ projector: {
3259
+ guid: volumeSpot.projectorGuid ?? `handle:${Number(volumeSpot.projectorHandle)}`,
3260
+ generation: volumeSpot.projectorGeneration ?? 1,
3261
+ view: '2d',
3262
+ sampler: 'linear-clamp-to-edge',
3263
+ projection: 'spot-projection',
3264
+ revision: volumeSpot.projectorRevision ?? 1,
3265
+ },
3266
+ projectorHandle,
3267
+ projectorAsset,
3268
+ };
3269
+ }
3270
+
2946
3271
  // bug-20260710-editor-cross-world-shadow: CSM matrices are computed by the
2947
3272
  // shared pure {@link computeDirectionalCsm}, called here per-world with THIS
2948
3273
  // world's own `cameras[0]`. In a single-world app the light and camera share
@@ -4670,6 +4995,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4670
4995
  lights,
4671
4996
  environment,
4672
4997
  environmentReady: true,
4998
+ ...(volumetricFog === undefined ? {} : { volumetricFog }),
4673
4999
  renderables,
4674
5000
  dispatch,
4675
5001
  skylight,
@@ -80,6 +80,7 @@ import {
80
80
  import { createPointsLinesLaneAdapter, type PointsLinesBackend } from './points-lines/record';
81
81
  import type { PointsLinesRetainedSnapshot } from './points-lines/snapshot';
82
82
  import type { PointsLinesRecordOwner, PointsLinesRecordSubmission } from './record/render-context';
83
+ import { inspectVolumetricFog } from './volume/inspection';
83
84
 
84
85
  export type { RenderSceneInspection } from './inspection-types';
85
86
 
@@ -120,11 +121,13 @@ import type {
120
121
  RenderFrameState,
121
122
  } from './record/frame-snapshot';
122
123
  import { getTextureIdentity } from './record/frame-snapshot';
124
+ import type { GpuTimingCapture } from './record/gpu-timing';
123
125
  import { applyParamSnapshotToUbo, residentTextureView } from './record/main-pass-material';
124
126
  import type { RenderSystemInternals } from './record/render-context';
125
127
  import {
126
128
  type RenderFeatureGraphCandidate,
127
129
  resetRenderFeatureGraphState,
130
+ settleVolumetricFogGraphCandidate,
128
131
  } from './record/typed-frame-graph';
129
132
  import {
130
133
  type CameraSnapshot,
@@ -145,6 +148,7 @@ import {
145
148
  TRANSPARENT_SORT_MODE_LAYER_YZ,
146
149
  TRANSPARENT_SORT_MODE_LAYER_Z,
147
150
  } from './systems/transparent-sort-config';
151
+ import { createTemporalFrameTransaction, type TemporalFrame } from './temporal/frame';
148
152
  import { retireTemporalGpuState } from './temporal/gpu';
149
153
  import { inspectTemporal, type TemporalInspection } from './temporal/inspection';
150
154
  import {
@@ -325,6 +329,7 @@ export interface RenderSystem {
325
329
  worlds: readonly World[],
326
330
  opts: DrawOwnerOptions,
327
331
  renderReadLeases?: readonly RenderReadLease[],
332
+ timingCapture?: GpuTimingCapture,
328
333
  ): boolean;
329
334
  /** Release renderer-owned persistent state for one detached World. */
330
335
  detachScene(world: World): void;
@@ -392,6 +397,10 @@ export interface RenderSystem {
392
397
  readonly createBindGroup: number;
393
398
  readonly keys: readonly string[];
394
399
  };
400
+ /** Latest accepted-submit temporal POD; absent until the first accepted frame. */
401
+ readonly temporalFrame: TemporalFrame | undefined;
402
+ /** Current renderer-owned volumetric fog inspection snapshot. */
403
+ readonly volumetricFog: import('./volume/inspection').VolumetricFogInspection;
395
404
  /** Configure the sole Standard graph owner before the first frame. */
396
405
  configureStandard(config: RenderPipelineAsset['config']): void;
397
406
  /** Register one engine-owned post-process shader used by the Standard lane. */
@@ -949,10 +958,26 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
949
958
  graphGeneration: 0,
950
959
  successfulTemporalFrameIndex: 0,
951
960
  lastSuccessfulBloom: 'off',
961
+ temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
962
+ temporalFrameInput: undefined,
952
963
  directionalShadowCache: null,
953
964
  directionalShadowCacheRecorded: false,
954
965
  compiledFrameGraph: null,
955
966
  compiledFrameGraphTopologyKey: null,
967
+ volumetricFogPreviousGraph: null,
968
+ volumetricFogPreviousGraphKey: null,
969
+ volumetricFogCandidateGraph: null,
970
+ volumetricFogParamsBuffers: [null, null],
971
+ volumetricFogParamsPendingSlot: null,
972
+ volumetricFogParamsAcceptedSlot: null,
973
+ volumetricFogAcceptedParams: undefined,
974
+ volumetricFogPendingParams: undefined,
975
+ volumetricFogInspection: inspectVolumetricFog({ authored: false, capability: 'available' }),
976
+ volumetricFogAccepted: undefined,
977
+ volumetricFogAcceptedContext: undefined,
978
+ volumetricFogHistoryGraph: null,
979
+ volumetricFogHistorySlot: null,
980
+ volumetricFogHistorySignature: null,
956
981
  retiredCompiledFrameGraphs: new Set(),
957
982
  currentFrameObservationSource: undefined,
958
983
  lastSuccessfulCameraAntialias: undefined,
@@ -1657,6 +1682,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1657
1682
  worlds: readonly World[],
1658
1683
  opts: DrawOwnerOptions,
1659
1684
  renderReadLeases?: readonly RenderReadLease[],
1685
+ timingCapture?: GpuTimingCapture,
1660
1686
  ): boolean {
1661
1687
  preparedWorlds = worlds;
1662
1688
  const profileSession = internals.profiler?.activeSession();
@@ -1732,6 +1758,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1732
1758
  lights,
1733
1759
  environment,
1734
1760
  environmentReady,
1761
+ volumetricFog,
1735
1762
  renderables,
1736
1763
  dispatch,
1737
1764
  skylight,
@@ -1961,6 +1988,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1961
1988
  environment?.fogSignature ?? '',
1962
1989
  environmentReady && environment !== undefined,
1963
1990
  persistentRenderScene.transmissionTopologyDemand(),
1991
+ volumetricFog,
1992
+ timingCapture,
1964
1993
  ),
1965
1994
  );
1966
1995
  if (submitted) {
@@ -2061,6 +2090,20 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2061
2090
  frameState.graphTargetCapture = request;
2062
2091
  },
2063
2092
  bindGroupCounts: bindGroupCounts,
2093
+ get temporalFrame(): TemporalFrame | undefined {
2094
+ const accepted = frameState.temporalFrame;
2095
+ if (accepted === undefined) return undefined;
2096
+ return {
2097
+ ...accepted,
2098
+ currentViewProjection: new Float32Array(accepted.currentViewProjection),
2099
+ previousViewProjection:
2100
+ accepted.previousViewProjection === undefined
2101
+ ? undefined
2102
+ : new Float32Array(accepted.previousViewProjection),
2103
+ jitter: [accepted.jitter[0], accepted.jitter[1]],
2104
+ viewport: { ...accepted.viewport },
2105
+ };
2106
+ },
2064
2107
  frustumStats: lastFrustumStats,
2065
2108
  visibilityStats: lastVisibilityStats,
2066
2109
  get meshMaterialBindings(): readonly MeshMaterialBindingObservation[] {
@@ -2075,6 +2118,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2075
2118
  get perFrameGraphInfo(): CompiledRenderGraphInfo | undefined {
2076
2119
  return frameState.compiledFrameGraph?.inspect();
2077
2120
  },
2121
+ get volumetricFog(): import('./volume/inspection').VolumetricFogInspection {
2122
+ return frameState.volumetricFogInspection;
2123
+ },
2078
2124
  configureStandard(config: RenderPipelineAsset['config']): void {
2079
2125
  const profileConfig = internals.standardProfile;
2080
2126
  const resolvedConfig =
@@ -2214,6 +2260,22 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2214
2260
  retireTemporalGpuState(retiring);
2215
2261
  }
2216
2262
  frameState.retiringTemporalGpuStates.clear();
2263
+ settleVolumetricFogGraphCandidate(frameState, false);
2264
+ for (const buffer of frameState.volumetricFogParamsBuffers) {
2265
+ if (buffer === null) continue;
2266
+ const destroyed = internals.device.destroyBuffer(buffer);
2267
+ if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
2268
+ }
2269
+ frameState.volumetricFogParamsBuffers = [null, null];
2270
+ frameState.volumetricFogParamsPendingSlot = null;
2271
+ frameState.volumetricFogParamsAcceptedSlot = null;
2272
+ frameState.volumetricFogAcceptedParams = undefined;
2273
+ frameState.volumetricFogPendingParams = undefined;
2274
+ frameState.volumetricFogAccepted = undefined;
2275
+ frameState.volumetricFogAcceptedContext = undefined;
2276
+ frameState.volumetricFogHistoryGraph = null;
2277
+ frameState.volumetricFogHistorySlot = null;
2278
+ frameState.volumetricFogHistorySignature = null;
2217
2279
  const compiled = frameState.compiledFrameGraph;
2218
2280
  frameState.compiledFrameGraph = null;
2219
2281
  frameState.compiledFrameGraphTopologyKey = null;
@@ -2269,9 +2331,26 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2269
2331
  // must be discarded, not merely marked for destruction: their opaque
2270
2332
  // handles cannot be used on the fresh device and the next draw must
2271
2333
  // lazily build a new graph from the preserved ECS / asset POD caches.
2334
+ settleVolumetricFogGraphCandidate(frameState, false);
2272
2335
  frameState.compiledFrameGraph?.retire().catch(() => undefined);
2273
2336
  frameState.compiledFrameGraph = null;
2274
2337
  frameState.compiledFrameGraphTopologyKey = null;
2338
+ frameState.volumetricFogPreviousGraph = null;
2339
+ frameState.volumetricFogPreviousGraphKey = null;
2340
+ frameState.volumetricFogCandidateGraph = null;
2341
+ frameState.volumetricFogAccepted = undefined;
2342
+ frameState.volumetricFogAcceptedContext = undefined;
2343
+ frameState.volumetricFogHistoryGraph = null;
2344
+ frameState.volumetricFogHistorySlot = null;
2345
+ frameState.volumetricFogHistorySignature = null;
2346
+ // The old device owns these transient params buffers. Discard their
2347
+ // handles without calling destroy() on the lost device; the next frame
2348
+ // must allocate fresh buffers on the replacement device.
2349
+ frameState.volumetricFogParamsBuffers = [null, null];
2350
+ frameState.volumetricFogParamsPendingSlot = null;
2351
+ frameState.volumetricFogParamsAcceptedSlot = null;
2352
+ frameState.volumetricFogAcceptedParams = undefined;
2353
+ frameState.volumetricFogPendingParams = undefined;
2275
2354
  frameState.directionalShadowCache = null;
2276
2355
  frameState.directionalShadowCacheRecorded = false;
2277
2356
  frameState.currentFrameObservationSource = undefined;