@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
@@ -40,14 +40,14 @@ import {
40
40
  GPU_TEXTURE_USAGE_RENDER_ATTACHMENT,
41
41
  GPU_TEXTURE_USAGE_TEXTURE_BINDING,
42
42
  } from '../gpu-texture-usage';
43
- import { STANDARD_PIPELINE_ID } from '../pipeline/standard-profile';
43
+ import { resolveVolumetricFogProfile, STANDARD_PIPELINE_ID } from '../pipeline/standard-profile';
44
44
  import type { CameraSnapshot } from '../render-contract';
45
45
  import type {
46
46
  RenderPipelineFeatureTarget,
47
47
  RenderPipelineFrame,
48
48
  RenderPipelineTopology,
49
49
  } from '../render-pipeline';
50
- import type { ExtractedLights } from '../render-system-extract';
50
+ import type { ExtractedLights, ExtractedVolumetricFog } from '../render-system-extract';
51
51
  import {
52
52
  abortTemporalGpuSubmit,
53
53
  commitTemporalGpuSubmit,
@@ -60,8 +60,11 @@ import {
60
60
  } from '../temporal/gpu';
61
61
  import { isSceneDataTarget } from '../temporal/scene-data';
62
62
  import type { TransmissionDemand } from '../transmission/projection';
63
+ import { hasVolumetricFogCapability } from '../volume/capability';
64
+ import { deriveVolumetricFogExtent, deriveVolumetricFogResolvedExtent } from '../volume/resources';
63
65
  import { buildPerFrameBindGroups } from './frame-lighting';
64
66
  import { getTextureIdentity, type RenderFrameState } from './frame-snapshot';
67
+ import type { GpuTimingCapture } from './gpu-timing';
65
68
  import type { PipelineState, RenderSystemInternals } from './render-context';
66
69
  import { resolveSurfaceFormatPair, resolveSurfaceProfile } from './render-context';
67
70
 
@@ -329,6 +332,7 @@ function topologyOf(
329
332
  gpuDriven: PreparedGpuDrivenFrame | undefined,
330
333
  featureGraphCandidate: RenderFeatureGraphCandidate | undefined,
331
334
  transmissionDemand: TransmissionDemand | undefined,
335
+ volumetricFog: ExtractedVolumetricFog | undefined,
332
336
  ): Result<RenderPipelineTopology, RhiError> {
333
337
  const cascadeCount = Math.max(1, Math.min(4, Math.round(lights.cascadeCount ?? 1))) as
334
338
  | 1
@@ -365,6 +369,21 @@ function topologyOf(
365
369
  ]),
366
370
  ],
367
371
  };
372
+ const volumeCapability = hasVolumetricFogCapability(
373
+ internals.device.caps,
374
+ internals.volumetricFogShaders,
375
+ );
376
+ const selectedVolumeLight =
377
+ volumetricFog?.lightKind === 'spot'
378
+ ? lights.spot.find((light) => light.entity === volumetricFog.lightEntity)
379
+ : volumetricFog?.lightKind === 'point'
380
+ ? lights.point.find((light) => light.entity === volumetricFog.lightEntity)
381
+ : volumetricFog?.lightKind === 'directional' &&
382
+ lights.directional?.entity === volumetricFog.lightEntity
383
+ ? lights.directional
384
+ : volumetricFog?.lightKind === undefined
385
+ ? lights.directional
386
+ : undefined;
368
387
  return ok({
369
388
  pipelineId: STANDARD_PIPELINE_ID,
370
389
  standardProfile: internals.standardProfile,
@@ -398,6 +417,47 @@ function topologyOf(
398
417
  ).length,
399
418
  ),
400
419
  },
420
+ volumetricFog:
421
+ volumetricFog?.status === 'available' &&
422
+ volumetricFog.densityAsset?.shape.viewDimension === '3d' &&
423
+ camera.tonemap !== 'none' &&
424
+ selectedVolumeLight !== undefined &&
425
+ volumeCapability
426
+ ? (() => {
427
+ const volumeProfile = resolveVolumetricFogProfile(internals.standardProfile ?? {});
428
+ return {
429
+ enabled: true,
430
+ format: volumetricFog.densityAsset.format,
431
+ extent: {
432
+ width: volumetricFog.densityAsset.shape.extent.width,
433
+ height: volumetricFog.densityAsset.shape.extent.height,
434
+ depth: volumetricFog.densityAsset.shape.extent.depth,
435
+ },
436
+ froxelExtent: deriveVolumetricFogExtent(
437
+ { width, height },
438
+ volumeProfile.depth,
439
+ volumeProfile.tileSize,
440
+ ),
441
+ resolvedExtent: deriveVolumetricFogResolvedExtent(
442
+ { width, height },
443
+ volumeProfile.tileSize,
444
+ ),
445
+ lightKind: volumetricFog.lightKind,
446
+ lightEntity: volumetricFog.lightEntity,
447
+ pointLightEntity: volumetricFog.pointLightEntity,
448
+ spotLightEntity: volumetricFog.spotLightEntity,
449
+ ...(volumetricFog.projector === undefined
450
+ ? {}
451
+ : {
452
+ projector: {
453
+ guid: volumetricFog.projector.guid,
454
+ generation: volumetricFog.projector.generation,
455
+ revision: volumetricFog.projector.revision,
456
+ },
457
+ }),
458
+ };
459
+ })()
460
+ : { enabled: false },
401
461
  lane: {
402
462
  compute: internals.device.caps.compute,
403
463
  storageBuffer: internals.device.caps.storageBuffer,
@@ -610,6 +670,32 @@ function retire(
610
670
  .catch(() => undefined);
611
671
  }
612
672
 
673
+ /**
674
+ * Settle a volume topology candidate after the command buffer outcome is
675
+ * known. A graph that failed to encode, finish, or submit must never become
676
+ * the next frame's accepted graph: retire it and restore the prior graph (or
677
+ * leave the renderer without a graph when this was the first attempt).
678
+ */
679
+ export function settleVolumetricFogGraphCandidate(
680
+ frameState: RenderFrameState,
681
+ submitted: boolean,
682
+ ): void {
683
+ const candidate = frameState.volumetricFogCandidateGraph ?? null;
684
+ if (candidate === null) return;
685
+ const previous = frameState.volumetricFogPreviousGraph ?? null;
686
+ const previousKey = frameState.volumetricFogPreviousGraphKey ?? null;
687
+ if (submitted) {
688
+ if (previous !== null && previous !== candidate) retire(frameState, previous);
689
+ } else {
690
+ if (candidate !== previous) retire(frameState, candidate);
691
+ frameState.compiledFrameGraph = previous;
692
+ frameState.compiledFrameGraphTopologyKey = previousKey;
693
+ }
694
+ frameState.volumetricFogPreviousGraph = null;
695
+ frameState.volumetricFogPreviousGraphKey = null;
696
+ frameState.volumetricFogCandidateGraph = null;
697
+ }
698
+
613
699
  export function ensureCompiledFrameGraph(
614
700
  internals: RenderSystemInternals,
615
701
  frameState: RenderFrameState,
@@ -623,6 +709,8 @@ export function ensureCompiledFrameGraph(
623
709
  featureGraphCandidate?: RenderFeatureGraphCandidate,
624
710
  clearOnly = false,
625
711
  transmissionDemand?: TransmissionDemand,
712
+ volumetricFog?: ExtractedVolumetricFog,
713
+ volumeTopologyCandidate = false,
626
714
  ): CompiledRenderGraph<RenderPipelineFrame> | null {
627
715
  const surfaceWidth = Math.max(1, width);
628
716
  const surfaceHeight = Math.max(1, height);
@@ -631,7 +719,7 @@ export function ensureCompiledFrameGraph(
631
719
  if (!projectedFeaturePlans.ok) {
632
720
  reportRenderFeatureGraphError(internals, projectedFeaturePlans.error);
633
721
  featureGraphCandidate?.onRejected?.();
634
- return frameState.compiledFrameGraph;
722
+ return volumeTopologyCandidate ? null : frameState.compiledFrameGraph;
635
723
  }
636
724
  const topologyResult = topologyOf(
637
725
  internals,
@@ -646,11 +734,12 @@ export function ensureCompiledFrameGraph(
646
734
  gpuDriven,
647
735
  featureGraphCandidate,
648
736
  transmissionDemand,
737
+ volumetricFog,
649
738
  );
650
739
  if (!topologyResult.ok) {
651
740
  internals.errorRegistry.fire(topologyResult.error);
652
741
  featureGraphCandidate?.onRejected?.();
653
- return frameState.compiledFrameGraph;
742
+ return volumeTopologyCandidate ? null : frameState.compiledFrameGraph;
654
743
  }
655
744
  const topology = topologyResult.value;
656
745
  const key = JSON.stringify(topology);
@@ -658,6 +747,10 @@ export function ensureCompiledFrameGraph(
658
747
  if (featureGraphCandidate !== undefined) {
659
748
  commitFeatureCandidate(internals, featureGraphCandidate);
660
749
  }
750
+ // A candidate flag only describes a possible topology replacement. When
751
+ // the derived key is unchanged there is no replacement to stage: reuse the
752
+ // accepted graph so dynamic volume history/imported views can advance and
753
+ // a queue-submit failure can still publish the LKG diagnostic.
661
754
  return frameState.compiledFrameGraph;
662
755
  }
663
756
 
@@ -701,7 +794,7 @@ export function ensureCompiledFrameGraph(
701
794
  if (!built.ok) {
702
795
  internals.errorRegistry.fire(built.error);
703
796
  featureGraphCandidate?.onRejected?.();
704
- return frameState.compiledFrameGraph;
797
+ return volumeTopologyCandidate ? null : frameState.compiledFrameGraph;
705
798
  }
706
799
  const compiled = builder.compile({
707
800
  device: internals.device,
@@ -713,6 +806,11 @@ export function ensureCompiledFrameGraph(
713
806
  return frameState.compiledFrameGraph;
714
807
  }
715
808
  const previous = frameState.compiledFrameGraph;
809
+ if (volumeTopologyCandidate) {
810
+ frameState.volumetricFogPreviousGraph = previous;
811
+ frameState.volumetricFogPreviousGraphKey = frameState.compiledFrameGraphTopologyKey;
812
+ frameState.volumetricFogCandidateGraph = compiled.value;
813
+ }
716
814
  frameState.compiledFrameGraph = compiled.value;
717
815
  frameState.compiledFrameGraphTopologyKey = key;
718
816
  const compiledTargets = compiled.value as CompiledGraphTargetAccess;
@@ -727,7 +825,7 @@ export function ensureCompiledFrameGraph(
727
825
  if (featureGraphCandidate !== undefined) {
728
826
  commitFeatureCandidate(internals, featureGraphCandidate);
729
827
  }
730
- if (previous !== null) retire(frameState, previous);
828
+ if (!volumeTopologyCandidate && previous !== null) retire(frameState, previous);
731
829
  return compiled.value;
732
830
  }
733
831
 
@@ -737,15 +835,41 @@ export function executeCompiledFrameGraph(
737
835
  frame: RenderPipelineFrame,
738
836
  encoder: RhiCommandEncoder,
739
837
  runPass?: import('@forgeax/engine-render-graph').RenderGraphPassRunner,
838
+ timingCapture?: GpuTimingCapture,
740
839
  ): boolean {
741
840
  const graph = frameState.compiledFrameGraph;
742
- if (graph === null) return false;
841
+ const rejectTemporalFrame = (): void => {
842
+ if (frameState.temporalFrameInput === undefined) return;
843
+ frameState.temporalFrameTransaction.commit({ accepted: false });
844
+ frameState.temporalFrameInput = undefined;
845
+ };
846
+ if (graph === null) {
847
+ timingCapture?.discard();
848
+ settleVolumetricFogGraphCandidate(frameState, false);
849
+ rejectTemporalFrame();
850
+ return false;
851
+ }
852
+ const timedRunPass =
853
+ timingCapture === undefined
854
+ ? runPass
855
+ : (
856
+ pass: import('@forgeax/engine-render-graph').RenderGraphPassExecution,
857
+ encode: () => void,
858
+ ) => {
859
+ timingCapture.beforePass(pass.name, encoder);
860
+ try {
861
+ if (runPass === undefined) encode();
862
+ else runPass(pass, encode);
863
+ } finally {
864
+ timingCapture.afterPass(pass.name, encoder);
865
+ }
866
+ };
743
867
  type PreparedCommand = { readonly command: CommandBuffer };
744
868
  let preparedCommand: PreparedCommand = { command: undefined as never };
745
869
  const transaction = executeRendererFrameTransaction<PreparedCommand>({
746
870
  build: () => ({ ok: true, value: { command: undefined as never } }),
747
871
  execute: () => {
748
- const executed = graph.execute(frame, runPass);
872
+ const executed = graph.execute(frame, timedRunPass);
749
873
  if (!executed.ok) {
750
874
  internals.errorRegistry.fire(executed.error);
751
875
  return { ok: false, stage: 'execute' };
@@ -887,8 +1011,14 @@ export function executeCompiledFrameGraph(
887
1011
  );
888
1012
  return { ok: false, stage: 'finish' };
889
1013
  }
1014
+ const resolved = timingCapture?.resolve(encoder);
1015
+ if (resolved !== undefined && !resolved.ok) {
1016
+ timingCapture?.discard();
1017
+ return { ok: false, stage: 'finish' };
1018
+ }
890
1019
  const finished = encoder.finish();
891
1020
  if (!finished.ok) {
1021
+ timingCapture?.discard();
892
1022
  internals.errorRegistry.fire(finished.error);
893
1023
  return { ok: false, stage: 'finish' };
894
1024
  }
@@ -898,9 +1028,11 @@ export function executeCompiledFrameGraph(
898
1028
  submit: () => {
899
1029
  const submitted = internals.device.queue.submit([preparedCommand.command]);
900
1030
  if (!submitted.ok) {
1031
+ timingCapture?.discard();
901
1032
  internals.errorRegistry.fire(submitted.error);
902
1033
  return { ok: false, stage: 'submit' };
903
1034
  }
1035
+ timingCapture?.markSubmitted();
904
1036
  return { ok: true, value: undefined };
905
1037
  },
906
1038
  commit: () => {
@@ -974,6 +1106,7 @@ export function executeCompiledFrameGraph(
974
1106
  }
975
1107
  },
976
1108
  abort: (failure) => {
1109
+ timingCapture?.discard();
977
1110
  frameState.bloomFrameReceipts = undefined;
978
1111
  if (frameState.environmentGeneration !== undefined) {
979
1112
  frameState.environmentLifecycle?.recordStageFailure(
@@ -996,5 +1129,7 @@ export function executeCompiledFrameGraph(
996
1129
  }
997
1130
  },
998
1131
  });
1132
+ settleVolumetricFogGraphCandidate(frameState, transaction.ok);
1133
+ if (!transaction.ok) rejectTemporalFrame();
999
1134
  return transaction.ok;
1000
1135
  }
@@ -16,6 +16,7 @@ import type {
16
16
  RhiInstance,
17
17
  RhiRenderPassEncoder,
18
18
  RenderPipeline as RhiRenderPipeline,
19
+ Sampler,
19
20
  Texture,
20
21
  TextureFormat,
21
22
  TextureView,
@@ -43,7 +44,9 @@ export type { PointsLinesInspection } from './points-lines/inspection';
43
44
  import type { MeshMaterialBindingObservation } from './mesh-material-bindings';
44
45
  import type { StandardProfile } from './pipeline/standard-profile';
45
46
  import type { PostProcessError } from './post-process-errors';
47
+ import type { VolumeTimingObservation } from './record/gpu-timing';
46
48
  import type { TemporalView } from './temporal/view';
49
+ import type { VolumetricFogInspection } from './volume/inspection';
47
50
 
48
51
  export type RenderResult<T, E> =
49
52
  | { readonly ok: true; readonly value: T }
@@ -118,6 +121,14 @@ export type { TemporalView } from './temporal/view';
118
121
  interface RenderPipelineStateView {
119
122
  readonly format: TextureFormat;
120
123
  readonly colorAttachmentFormat: TextureFormat;
124
+ /** Renderer-owned View UBO shared by Standard lighting, shadows, and fog. */
125
+ readonly viewUniformBuffer: Buffer;
126
+ /** Renderer-owned SpotLight std430 buffer shared by Standard and volume. */
127
+ readonly pointLightsBuffer: Buffer;
128
+ readonly spotLightsBuffer: Buffer;
129
+ /** Fallback resources keep the shared view layout valid when no projector is authored. */
130
+ readonly defaultWhiteTextureView: TextureView;
131
+ readonly defaultSampler: Sampler;
121
132
  }
122
133
 
123
134
  /**
@@ -127,6 +138,16 @@ interface RenderPipelineStateView {
127
138
  */
128
139
  interface RenderPipelineRuntime {
129
140
  readonly device: RhiDevice;
141
+ /** Runtime-owned shader module factory used by built-in graph producers. */
142
+ readonly shaderModuleFactory?: {
143
+ createShaderModule(input: {
144
+ readonly code: string;
145
+ readonly label?: string | undefined;
146
+ }): import('@forgeax/engine-types').Result<
147
+ import('@forgeax/engine-rhi').ShaderModule,
148
+ RhiError
149
+ >;
150
+ };
130
151
  readonly errorRegistry: { fire(error: RendererError): void };
131
152
  readonly debugOverlay?: RenderDebugOverlay | undefined;
132
153
  readonly lookupPostProcess?: (id: string) => PostProcessShaderEntry | undefined;
@@ -170,6 +191,52 @@ export interface RenderPipelineContext {
170
191
  readonly msaaActive: boolean;
171
192
  readonly geometryColorResolveView: TextureView | null;
172
193
  readonly ldrSpriteColorView: TextureView | null;
194
+ /** Renderer-owned volume resources prepared from the authored ECS snapshot. */
195
+ readonly volumetricFog?: VolumetricFogFrameContext;
196
+ /** Renderer-owned SpotLight projector resources, independent of fog. */
197
+ readonly spotLightProjector?: SpotLightProjectorFrameContext;
198
+ /** Build-time composed utility sources for the renderer-owned volume passes. */
199
+ readonly volumetricFogShaders?: VolumetricFogShaderSources;
200
+ }
201
+
202
+ /** Stable utility entry-point sources emitted by the shader manifest. */
203
+ export interface VolumetricFogShaderSources {
204
+ readonly inject: string;
205
+ readonly temporal: string;
206
+ readonly integrate: string;
207
+ readonly composite: string;
208
+ }
209
+
210
+ /**
211
+ * GPU resources for one validated volumetric-fog candidate. The record stage
212
+ * owns residency and parameter uploads; graph passes consume this narrow view
213
+ * and never reach into AssetRegistry or World.
214
+ */
215
+ export interface VolumetricFogFrameContext {
216
+ readonly densityTexture: Texture;
217
+ readonly densityView: TextureView;
218
+ readonly paramsBuffer: Buffer;
219
+ readonly densityGeneration: number;
220
+ /** Accepted graph-owned history slot; null means first frame/reset. */
221
+ readonly historyReadSlot: 0 | 1 | null;
222
+ /** Pending graph-owned history slot; promoted only after queue.submit succeeds. */
223
+ readonly historyWriteSlot: 0 | 1;
224
+ readonly historyValid: boolean;
225
+ /** Accepted SpotLight projector resources resolved by the GPU residency owner. */
226
+ readonly projectorTexture?: Texture;
227
+ readonly projectorView?: TextureView;
228
+ readonly projectorSampler?: Sampler;
229
+ }
230
+
231
+ /**
232
+ * GPU resources for the single surface SpotLight projector binding. The
233
+ * projector is a SpotLight-owned cookie and therefore remains available when
234
+ * volumetric fog is disabled; volume integration may reuse the same tuple.
235
+ */
236
+ export interface SpotLightProjectorFrameContext {
237
+ readonly texture: Texture;
238
+ readonly view: TextureView;
239
+ readonly sampler: Sampler;
173
240
  }
174
241
 
175
242
  /**
@@ -274,6 +341,7 @@ export interface FrameReceiptObservation {
274
341
  readonly frameId: number;
275
342
  readonly deviceGeneration: number;
276
343
  readonly include: readonly ('timings' | 'draws' | 'bindings')[];
344
+ readonly timings?: VolumeTimingObservation;
277
345
  }
278
346
 
279
347
  /** Stable backend facts for the Standard output transform projection. */
@@ -342,6 +410,8 @@ export interface RenderInspection {
342
410
  readonly frameId: number;
343
411
  readonly deviceGeneration: number;
344
412
  };
413
+ /** Renderer-owned authored volume projection and submit/LKG state. */
414
+ readonly volumetricFog?: VolumetricFogInspection;
345
415
  readonly features: readonly string[];
346
416
  /** Truthful lifecycle facts for every registered declarative feature. */
347
417
  readonly featureDiagnostics: readonly RenderFeatureDiagnostics[];
@@ -577,6 +647,8 @@ export interface RendererOptions {
577
647
  readonly standardProfile?: RenderProfile | undefined;
578
648
  /** Explicit profiler capability shared by App and Render. */
579
649
  readonly profiler?: Profiler | undefined;
650
+ /** Explicitly request timestamp-query for receipt-bound GPU profiling. */
651
+ readonly captureGpuTimings?: boolean | undefined;
580
652
  /** Optional host-owned RHI lifecycle instrumentation, such as recording. */
581
653
  readonly rhiInstrumentation?: RhiBackendInstrumentation | undefined;
582
654
  // feat-20260608-create-app-param-surface-trim / M1 / AC-02: `clearColor`
@@ -15,7 +15,6 @@
15
15
  // GPU-resource kind would surface as a `tsc -b` exhaustiveness error at the
16
16
  // switch rather than a silent fallthrough (AC-06).
17
17
 
18
- import { numMipLevels } from '@forgeax/engine-assets-runtime';
19
18
  import {
20
19
  bytesPerRow as blockBytesPerRow,
21
20
  blockParamsForFormat,
@@ -29,9 +28,11 @@ import { err, ok, type Result } from '@forgeax/engine-rhi';
29
28
  import {
30
29
  ASSET_ERROR_HINTS,
31
30
  type AssetError,
31
+ deriveTextureLayout,
32
32
  type MeshAsset,
33
33
  type Submesh,
34
34
  type TextureAsset,
35
+ type TextureShape,
35
36
  } from '@forgeax/engine-types';
36
37
  import {
37
38
  GPU_TEXTURE_USAGE_COPY_DST,
@@ -88,12 +89,16 @@ export interface MeshRenderData {
88
89
  readonly submeshes: readonly Submesh[];
89
90
  }
90
91
 
91
- /** Descriptor for a 2D texture's GPU resource (projected from POD). */
92
+ /** Descriptor for a sampled texture's GPU resource (projected from POD). */
92
93
  export interface TextureRenderData {
93
94
  /** Logical width retained for existing projection consumers. */
94
95
  readonly width: number;
95
96
  /** Logical height retained for existing projection consumers. */
96
97
  readonly height: number;
98
+ /** Authored shape; the residency layer must preserve this view dimension. */
99
+ readonly shape: TextureShape;
100
+ /** Array layers for `2d-array`, depth slices for `3d`, otherwise one. */
101
+ readonly depthOrArrayLayers: number;
97
102
  /** Authored content extent; TextureAsset remains the logical asset SSOT. */
98
103
  readonly logicalExtent: TextureExtent;
99
104
  /** GPU allocation extent, derived from format + logicalExtent. */
@@ -105,10 +110,10 @@ export interface TextureRenderData {
105
110
  readonly usage: number;
106
111
  /**
107
112
  * Row pitch for the BASE mip (level 0) the store passes to `writeTexture`.
108
- * Uncompressed: `width * 4` (RGBA8). Block-compressed: the block-padded
109
- * `ceil(width / blockW) * bytesPerBlock` (feat-20260707 M5 / w35, AC-08).
110
- * The per-mip layout for a multi-level compressed upload comes from
111
- * {@link deriveMipUploadLayout}.
113
+ * Derived from the canonical texture layout, so format-specific uncompressed
114
+ * widths (for example R8 density) and block-compressed widths stay aligned
115
+ * with the upload contract. The per-mip layout for a multi-level compressed
116
+ * upload comes from {@link deriveMipUploadLayout}.
112
117
  */
113
118
  readonly bytesPerRow: number;
114
119
  /** True iff `format` is a block-compressed format (drives the store's upload arm). */
@@ -262,6 +267,8 @@ export function deriveRenderDataMesh(mesh: MeshAsset): Result<MeshRenderData, As
262
267
  * inconsistent (a `-srgb` format requires an `srgb` colorSpace and vice versa).
263
268
  */
264
269
  export function deriveRenderDataTexture(tex: TextureAsset): Result<TextureRenderData, AssetError> {
270
+ const width = tex.shape.extent.width;
271
+ const height = tex.shape.extent.height;
265
272
  const isSrgbFormat = tex.format.endsWith('-srgb');
266
273
  const expectedColorSpace: 'srgb' | 'linear' = isSrgbFormat ? 'srgb' : 'linear';
267
274
  if (tex.colorSpace !== expectedColorSpace) {
@@ -275,14 +282,23 @@ export function deriveRenderDataTexture(tex: TextureAsset): Result<TextureRender
275
282
  }
276
283
 
277
284
  const compressed = isCompressedFormat(tex.format);
285
+ const layout = deriveTextureLayout({ shape: tex.shape, format: tex.format, mips: tex.mips });
286
+ if (!layout.ok) {
287
+ return err(
288
+ projectionError({
289
+ code: 'invalid-source-format',
290
+ expected: layout.error.expected,
291
+ hint: layout.error.hint,
292
+ }),
293
+ );
294
+ }
278
295
  // feat-20260707 M5 / w35 (AC-09, D-9): a block-compressed texture cannot have
279
296
  // its mips GPU-generated (F-7). An OFFLINE chain baked into `data`
280
297
  // (mipLevelCount > 1, e.g. a KTX2 level chain) is the normal compressed path
281
298
  // and passes; requesting RUNTIME mip generation (`mipmap:true` with no offline
282
299
  // chain) fails fast with a self-recovery hint (fail-fast at the projection
283
300
  // layer -- producer self-validate).
284
- const hasOfflineMipChain = (tex.mipLevelCount ?? 1) > 1;
285
- if (compressed && tex.mipmap === true && !hasOfflineMipChain) {
301
+ if (compressed && tex.mips.kind === 'generate') {
286
302
  return err(
287
303
  projectionError({
288
304
  code: 'mipgen-unsupported-compressed-format',
@@ -296,15 +312,19 @@ export function deriveRenderDataTexture(tex: TextureAsset): Result<TextureRender
296
312
  // Uncompressed: runtime mip-gen count from the pixel size. Compressed: the
297
313
  // level count is authored (offline chain) -- honour the POD's mipLevelCount,
298
314
  // never re-derive it from the size (a compressed chain may stop early).
299
- const mipLevelCount = compressed
300
- ? Math.max(1, tex.mipLevelCount ?? 1)
301
- : tex.mipmap
302
- ? numMipLevels(tex)
303
- : 1;
315
+ const mipLevelCount = layout.value.levels.length;
304
316
 
305
- const uncompressedBytesPerTexel =
306
- tex.format === 'rgba32float' ? 16 : tex.format === 'rgba16float' ? 8 : 4;
307
- if (!compressed && tex.data.byteLength < tex.width * tex.height * uncompressedBytesPerTexel) {
317
+ const baseLevel = layout.value.levels[0];
318
+ if (baseLevel === undefined) {
319
+ return err(
320
+ projectionError({
321
+ code: 'invalid-source-format',
322
+ expected: 'texture layout contains a base mip level',
323
+ hint: ASSET_ERROR_HINTS['invalid-source-format'],
324
+ }),
325
+ );
326
+ }
327
+ if (!compressed && tex.data.byteLength < baseLevel.byteLength) {
308
328
  return err(
309
329
  projectionError({
310
330
  code: 'invalid-source-format',
@@ -318,8 +338,8 @@ export function deriveRenderDataTexture(tex: TextureAsset): Result<TextureRender
318
338
  // from the format, not aggregate payload length: imported images may retain
319
339
  // trailing decoder bytes and offline mip chains contain more than the base mip.
320
340
  const bytesPerRow = compressed
321
- ? (blockBytesPerRow(tex.format, tex.width) ?? tex.width * 4)
322
- : tex.width * uncompressedBytesPerTexel;
341
+ ? (blockBytesPerRow(tex.format, width) ?? baseLevel.bytesPerRow)
342
+ : baseLevel.bytesPerRow;
323
343
 
324
344
  // Compressed formats are not renderable -- RENDER_ATTACHMENT would fail
325
345
  // createTexture validation, and the runtime mipmap blit (which needs it) never
@@ -333,9 +353,16 @@ export function deriveRenderDataTexture(tex: TextureAsset): Result<TextureRender
333
353
  GPU_TEXTURE_USAGE_COPY_SRC;
334
354
 
335
355
  return ok({
336
- width: tex.width,
337
- height: tex.height,
338
- ...deriveTextureExtent(tex.format, tex.width, tex.height),
356
+ width,
357
+ height,
358
+ shape: tex.shape,
359
+ depthOrArrayLayers:
360
+ tex.shape.viewDimension === '2d'
361
+ ? 1
362
+ : tex.shape.viewDimension === '2d-array'
363
+ ? tex.shape.extent.layers
364
+ : tex.shape.extent.depth,
365
+ ...deriveTextureExtent(tex.format, width, height),
339
366
  format: tex.format,
340
367
  mipLevelCount,
341
368
  usage,
@@ -373,8 +400,9 @@ export function deriveRenderDataCubemap(source: TextureAsset): Result<CubeRender
373
400
  }),
374
401
  );
375
402
  }
403
+ const sourceHeight = source.shape.viewDimension === '2d' ? source.shape.extent.height : 0;
376
404
  return ok({
377
- cubeFaceSize: source.height,
405
+ cubeFaceSize: sourceHeight,
378
406
  outputFormat: 'rgba16float',
379
407
  needsHalfConversion: source.format === 'rgba32float',
380
408
  cubeUsage:
@@ -141,6 +141,48 @@ export interface RenderPipelineTopology {
141
141
  readonly pointFaceSize: number;
142
142
  readonly spotCount: number;
143
143
  };
144
+ /**
145
+ * Authored volume topology facts. Identity/generation/digest are runtime
146
+ * residency facts and deliberately do not participate in graph topology.
147
+ */
148
+ readonly volumetricFog?: {
149
+ readonly enabled: boolean;
150
+ readonly lightKind?: 'directional' | 'point' | 'spot' | undefined;
151
+ readonly lightEntity?: number | undefined;
152
+ readonly pointLightEntity?: number | undefined;
153
+ readonly spotLightEntity?: number | undefined;
154
+ readonly projector?:
155
+ | {
156
+ readonly guid: string;
157
+ readonly generation: number;
158
+ readonly revision: number;
159
+ }
160
+ | undefined;
161
+ readonly format?: TextureFormat | undefined;
162
+ readonly extent?:
163
+ | {
164
+ readonly width: number;
165
+ readonly height: number;
166
+ readonly depth: number;
167
+ }
168
+ | undefined;
169
+ /** Renderer-owned froxel grid derived from the compiled surface extent. */
170
+ readonly froxelExtent?:
171
+ | {
172
+ readonly width: number;
173
+ readonly height: number;
174
+ readonly depth: number;
175
+ }
176
+ | undefined;
177
+ /** Renderer-owned 2D resolve/history extent derived from the same profile. */
178
+ readonly resolvedExtent?:
179
+ | {
180
+ readonly width: number;
181
+ readonly height: number;
182
+ readonly depth: number;
183
+ }
184
+ | undefined;
185
+ };
144
186
  readonly lane: {
145
187
  readonly compute: boolean;
146
188
  readonly storageBuffer: boolean;
@@ -257,6 +299,8 @@ export function importRenderPipelineSurface(
257
299
  }
258
300
 
259
301
  export interface RenderPipelineFeatureTarget {
302
+ /** Optional semantic alias when a pipeline exposes multiple color roles. */
303
+ readonly name?: string;
260
304
  readonly kind: RenderFeatureTargetKind;
261
305
  readonly texture: GraphTexture;
262
306
  readonly view: GraphTextureView;