@zephyr3d/scene 0.9.22 → 0.9.24

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 (234) hide show
  1. package/dist/animation/joint_dynamics/collision.js +1 -7
  2. package/dist/animation/joint_dynamics/collision.js.map +1 -1
  3. package/dist/animation/joint_dynamics/constraints.js +2 -17
  4. package/dist/animation/joint_dynamics/constraints.js.map +1 -1
  5. package/dist/animation/joint_dynamics/controller.js +2 -20
  6. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  7. package/dist/animation/joint_dynamics/joint_dynamics_system.js +1 -3
  8. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  9. package/dist/animation/joint_dynamics/solver.js +14 -60
  10. package/dist/animation/joint_dynamics/solver.js.map +1 -1
  11. package/dist/animation/joint_dynamics/types.js +3 -3
  12. package/dist/animation/joint_dynamics/types.js.map +1 -1
  13. package/dist/app/app.js +23 -0
  14. package/dist/app/app.js.map +1 -1
  15. package/dist/app/scriptregistry.js +16 -53
  16. package/dist/app/scriptregistry.js.map +1 -1
  17. package/dist/asset/assetmanager.js +2 -0
  18. package/dist/asset/assetmanager.js.map +1 -1
  19. package/dist/asset/model.js +22 -0
  20. package/dist/asset/model.js.map +1 -1
  21. package/dist/blitter/bilateralblur.js +3 -1
  22. package/dist/blitter/bilateralblur.js.map +1 -1
  23. package/dist/blitter/blitter.js +3 -3
  24. package/dist/blitter/blitter.js.map +1 -1
  25. package/dist/camera/camera.js +398 -45
  26. package/dist/camera/camera.js.map +1 -1
  27. package/dist/camera/perspectivecamera.js +69 -2
  28. package/dist/camera/perspectivecamera.js.map +1 -1
  29. package/dist/index.d.ts +9113 -8740
  30. package/dist/index.js +10 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/material/blinn.js +1 -1
  33. package/dist/material/blinn.js.map +1 -1
  34. package/dist/material/lambert.js +3 -3
  35. package/dist/material/lambert.js.map +1 -1
  36. package/dist/material/material.js +8 -5
  37. package/dist/material/material.js.map +1 -1
  38. package/dist/material/meshmaterial.js +17 -10
  39. package/dist/material/meshmaterial.js.map +1 -1
  40. package/dist/material/mixins/lightmodel/blinnphong.js +1 -1
  41. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  42. package/dist/material/mixins/lightmodel/lambert.js +1 -1
  43. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  44. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +21 -6
  45. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  46. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +1 -1
  47. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  48. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +1 -1
  49. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  50. package/dist/material/mixins/lit.js +33 -11
  51. package/dist/material/mixins/lit.js.map +1 -1
  52. package/dist/material/mixins/pbr/brdf.js +44 -12
  53. package/dist/material/mixins/pbr/brdf.js.map +1 -1
  54. package/dist/material/mixins/pbr/common.js +149 -14
  55. package/dist/material/mixins/pbr/common.js.map +1 -1
  56. package/dist/material/mtoon.js +7 -4
  57. package/dist/material/mtoon.js.map +1 -1
  58. package/dist/material/pbrblueprint.js +5 -1
  59. package/dist/material/pbrblueprint.js.map +1 -1
  60. package/dist/material/pbrmr.js +1 -1
  61. package/dist/material/pbrmr.js.map +1 -1
  62. package/dist/material/pbrsg.js +1 -1
  63. package/dist/material/pbrsg.js.map +1 -1
  64. package/dist/material/shader/helper.js +512 -334
  65. package/dist/material/shader/helper.js.map +1 -1
  66. package/dist/material/skin.js +2 -2
  67. package/dist/material/skin.js.map +1 -1
  68. package/dist/material/terrain-cm.js +5 -5
  69. package/dist/material/terrain-cm.js.map +1 -1
  70. package/dist/material/water.js +6 -4
  71. package/dist/material/water.js.map +1 -1
  72. package/dist/posteffect/bloom.js +54 -4
  73. package/dist/posteffect/bloom.js.map +1 -1
  74. package/dist/posteffect/compositor.js +41 -0
  75. package/dist/posteffect/compositor.js.map +1 -1
  76. package/dist/posteffect/posteffect.js +35 -0
  77. package/dist/posteffect/posteffect.js.map +1 -1
  78. package/dist/posteffect/sao.js +7 -6
  79. package/dist/posteffect/sao.js.map +1 -1
  80. package/dist/posteffect/skinsss.js +3 -3
  81. package/dist/posteffect/skinsss.js.map +1 -1
  82. package/dist/posteffect/ssgi.js +988 -436
  83. package/dist/posteffect/ssgi.js.map +1 -1
  84. package/dist/posteffect/ssr.js +83 -45
  85. package/dist/posteffect/ssr.js.map +1 -1
  86. package/dist/posteffect/sss.js +18 -15
  87. package/dist/posteffect/sss.js.map +1 -1
  88. package/dist/posteffect/taa.js +5 -4
  89. package/dist/posteffect/taa.js.map +1 -1
  90. package/dist/posteffect/tonemap.js +15 -3
  91. package/dist/posteffect/tonemap.js.map +1 -1
  92. package/dist/render/abuffer_oit.js +2 -2
  93. package/dist/render/abuffer_oit.js.map +1 -1
  94. package/dist/render/cluster_light.js +22 -16
  95. package/dist/render/cluster_light.js.map +1 -1
  96. package/dist/render/drawable_mixin.js +8 -5
  97. package/dist/render/drawable_mixin.js.map +1 -1
  98. package/dist/render/dualdepthpeeling_oit.js +10 -5
  99. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  100. package/dist/render/envlight.js +298 -5
  101. package/dist/render/envlight.js.map +1 -1
  102. package/dist/render/gpu_picking.js +7 -5
  103. package/dist/render/gpu_picking.js.map +1 -1
  104. package/dist/render/hzb.js +28 -19
  105. package/dist/render/hzb.js.map +1 -1
  106. package/dist/render/lightpass.js +18 -4
  107. package/dist/render/lightpass.js.map +1 -1
  108. package/dist/render/render_queue.js +37 -4
  109. package/dist/render/render_queue.js.map +1 -1
  110. package/dist/render/renderer.js +8 -3
  111. package/dist/render/renderer.js.map +1 -1
  112. package/dist/render/rendergraph/blackboard.js +20 -48
  113. package/dist/render/rendergraph/blackboard.js.map +1 -1
  114. package/dist/render/rendergraph/device_pool_allocator.js +18 -58
  115. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  116. package/dist/render/rendergraph/executor.js +89 -115
  117. package/dist/render/rendergraph/executor.js.map +1 -1
  118. package/dist/render/rendergraph/forward_plus_builder.js +1003 -715
  119. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  120. package/dist/render/rendergraph/frame_graph_context.js +1 -1
  121. package/dist/render/rendergraph/frame_graph_context.js.map +1 -1
  122. package/dist/render/rendergraph/frame_resource_requirements.js +14 -0
  123. package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -0
  124. package/dist/render/rendergraph/history_resource_manager.js +74 -140
  125. package/dist/render/rendergraph/history_resource_manager.js.map +1 -1
  126. package/dist/render/rendergraph/history_resources.js +7 -5
  127. package/dist/render/rendergraph/history_resources.js.map +1 -1
  128. package/dist/render/rendergraph/render_context.js +19 -0
  129. package/dist/render/rendergraph/render_context.js.map +1 -0
  130. package/dist/render/rendergraph/render_pipeline.js +315 -0
  131. package/dist/render/rendergraph/render_pipeline.js.map +1 -0
  132. package/dist/render/rendergraph/rendergraph.js +158 -141
  133. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  134. package/dist/render/rendergraph/scene_render_context.js +267 -0
  135. package/dist/render/rendergraph/scene_render_context.js.map +1 -0
  136. package/dist/render/rendergraph/texture_affinity_cache.js +19 -0
  137. package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -0
  138. package/dist/render/rendergraph/types.js +19 -31
  139. package/dist/render/rendergraph/types.js.map +1 -1
  140. package/dist/render/renderpass.js +3 -3
  141. package/dist/render/renderpass.js.map +1 -1
  142. package/dist/render/shadow_mask_pass.js +7 -6
  143. package/dist/render/shadow_mask_pass.js.map +1 -1
  144. package/dist/render/sky.js +257 -95
  145. package/dist/render/sky.js.map +1 -1
  146. package/dist/render/weightedblended_oit.js +4 -3
  147. package/dist/render/weightedblended_oit.js.map +1 -1
  148. package/dist/scene/basesprite.js +2 -0
  149. package/dist/scene/basesprite.js.map +1 -1
  150. package/dist/scene/batchgroup.js +10 -0
  151. package/dist/scene/batchgroup.js.map +1 -1
  152. package/dist/scene/environment.js +61 -2
  153. package/dist/scene/environment.js.map +1 -1
  154. package/dist/scene/light.js +178 -10
  155. package/dist/scene/light.js.map +1 -1
  156. package/dist/scene/meshdrawable.js +89 -2
  157. package/dist/scene/meshdrawable.js.map +1 -1
  158. package/dist/scene/msdftext.js +2 -0
  159. package/dist/scene/msdftext.js.map +1 -1
  160. package/dist/scene/msdftextsprite.js +2 -0
  161. package/dist/scene/msdftextsprite.js.map +1 -1
  162. package/dist/scene/particlesys.js +2 -0
  163. package/dist/scene/particlesys.js.map +1 -1
  164. package/dist/scene/scene.js +34 -1
  165. package/dist/scene/scene.js.map +1 -1
  166. package/dist/scene/terrain-cm/grass.js +2 -1
  167. package/dist/scene/terrain-cm/grass.js.map +1 -1
  168. package/dist/scene/terrain-cm/terrain-cm.js +2 -0
  169. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  170. package/dist/scene/water.js +2 -0
  171. package/dist/scene/water.js.map +1 -1
  172. package/dist/shaders/atmosphere.js +5 -4
  173. package/dist/shaders/atmosphere.js.map +1 -1
  174. package/dist/shaders/fog.js +10 -3
  175. package/dist/shaders/fog.js.map +1 -1
  176. package/dist/shaders/shadow.js +149 -40
  177. package/dist/shaders/shadow.js.map +1 -1
  178. package/dist/shaders/ssgi_reprojection.js +118 -0
  179. package/dist/shaders/ssgi_reprojection.js.map +1 -0
  180. package/dist/shaders/ssr.js +322 -154
  181. package/dist/shaders/ssr.js.map +1 -1
  182. package/dist/shadow/esm.js +10 -7
  183. package/dist/shadow/esm.js.map +1 -1
  184. package/dist/shadow/pcf_opt.js +12 -12
  185. package/dist/shadow/pcf_opt.js.map +1 -1
  186. package/dist/shadow/pcf_pd.js +12 -12
  187. package/dist/shadow/pcf_pd.js.map +1 -1
  188. package/dist/shadow/pcss.js +16 -11
  189. package/dist/shadow/pcss.js.map +1 -1
  190. package/dist/shadow/shadow_impl.js +29 -3
  191. package/dist/shadow/shadow_impl.js.map +1 -1
  192. package/dist/shadow/shadowmapper.js +5 -2
  193. package/dist/shadow/shadowmapper.js.map +1 -1
  194. package/dist/shadow/ssm.js +13 -13
  195. package/dist/shadow/ssm.js.map +1 -1
  196. package/dist/shadow/vsm.js +7 -7
  197. package/dist/shadow/vsm.js.map +1 -1
  198. package/dist/utility/physical.js +74 -0
  199. package/dist/utility/physical.js.map +1 -0
  200. package/dist/utility/rendermipmap.js +2 -2
  201. package/dist/utility/rendermipmap.js.map +1 -1
  202. package/dist/utility/serialization/manager.js +3 -1
  203. package/dist/utility/serialization/manager.js.map +1 -1
  204. package/dist/utility/serialization/scene/animation.js +3 -1
  205. package/dist/utility/serialization/scene/animation.js.map +1 -1
  206. package/dist/utility/serialization/scene/batch.js +3 -1
  207. package/dist/utility/serialization/scene/batch.js.map +1 -1
  208. package/dist/utility/serialization/scene/camera.js +598 -2
  209. package/dist/utility/serialization/scene/camera.js.map +1 -1
  210. package/dist/utility/serialization/scene/light.js +180 -9
  211. package/dist/utility/serialization/scene/light.js.map +1 -1
  212. package/dist/utility/serialization/scene/material.js +43 -3
  213. package/dist/utility/serialization/scene/material.js.map +1 -1
  214. package/dist/utility/serialization/scene/mesh.js +3 -1
  215. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  216. package/dist/utility/serialization/scene/node.js +3 -1
  217. package/dist/utility/serialization/scene/node.js.map +1 -1
  218. package/dist/utility/serialization/scene/particle.js +3 -1
  219. package/dist/utility/serialization/scene/particle.js.map +1 -1
  220. package/dist/utility/serialization/scene/primitive.js +2 -0
  221. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  222. package/dist/utility/serialization/scene/scene.js +70 -0
  223. package/dist/utility/serialization/scene/scene.js.map +1 -1
  224. package/dist/utility/serialization/scene/sprite.js +3 -1
  225. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  226. package/dist/utility/serialization/scene/terrain.js +3 -1
  227. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  228. package/dist/utility/serialization/scene/text.js +3 -0
  229. package/dist/utility/serialization/scene/text.js.map +1 -1
  230. package/dist/utility/serialization/scene/water.js +3 -1
  231. package/dist/utility/serialization/scene/water.js.map +1 -1
  232. package/dist/values.js +59 -65
  233. package/dist/values.js.map +1 -1
  234. package/package.json +3 -3
@@ -1,103 +1,86 @@
1
1
  import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
2
2
 
3
- /**
4
- * Render Graph — declarative, configurable render pipeline.
5
- *
6
- * Usage:
7
- * ```ts
8
- * const graph = new RenderGraph();
9
- * let backbuffer = graph.importTexture('backbuffer');
10
- *
11
- * let linearDepth: RGHandle;
12
- * graph.addPass('DepthPrepass', (builder) => {
13
- * linearDepth = builder.createTexture({ format: 'r32f', label: 'linearDepth' });
14
- * builder.setExecute(() => { ... });
15
- * });
16
- *
17
- * graph.addPass('LightPass', (builder) => {
18
- * builder.read(linearDepth);
19
- * backbuffer = builder.write(backbuffer);
20
- * builder.setExecute(() => { ... });
21
- * });
22
- *
23
- * const compiled = graph.compile([backbuffer]);
24
- * graph.execute(compiled);
25
- * graph.reset();
26
- * ```
27
- *
28
- * @public
29
- */ class RenderGraph {
30
- /** @internal */ _nextResourceId = 0;
31
- /** @internal */ _resources = new Map();
32
- /** @internal */ _passes = [];
33
- /** @internal */ _compiled = null;
34
- // ─── Graph Building ─────────────────────────────────────────────────
35
- /**
36
- * Import an external (persistent) texture into the graph.
37
- *
38
- * Imported resources are not allocated or released by the graph.
39
- * Typically used for the backbuffer or any texture that outlives a single frame.
40
- *
41
- * @param name - Debug label for the imported resource.
42
- * @returns A handle referencing the imported resource.
43
- */ importTexture(name) {
3
+ /** Declarative render pass and resource dependency graph. @public */ class RenderGraph {
4
+ _nextResourceId = 0;
5
+ _resources = new Map();
6
+ _passes = [];
7
+ _compiled = null;
8
+ _lastWriterByPhysicalId = new Map();
9
+ /** Deduplicates warnings across per-frame graph instances. */ static _warnedCulledPasses = new Set();
10
+ /** Import an external texture that the graph does not own. */ importTexture(name) {
44
11
  const id = this._nextResourceId++;
45
12
  const resource = new RGResource(id, name, 'imported', null);
46
13
  this._resources.set(id, resource);
47
14
  this._compiled = null;
48
- return new RGHandle(id, name);
15
+ return new RGHandle(id, name, 'texture');
49
16
  }
50
17
  /**
51
- * Add a render pass to the graph.
52
- *
53
- * The setup callback receives a {@link RGPassBuilder} to declare resource
54
- * dependencies. Call `builder.setExecute(fn)` inside setup to provide the
55
- * execution callback.
56
- *
57
- * @param name - Debug label for the pass.
58
- * @param setup - Setup callback that declares resources and sets the execute function.
18
+ * Add a pass. The setup callback declares dependencies and execution.
59
19
  */ addPass(name, setup) {
60
- const pass = new RGPass(this._passes.length, name);
20
+ let nameOccurrence = 0;
21
+ for (const existing of this._passes){
22
+ if (existing.name === name) {
23
+ nameOccurrence++;
24
+ }
25
+ }
26
+ const pass = new RGPass(this._passes.length, name, nameOccurrence);
61
27
  const builder = this._createBuilder(pass);
62
- const data = setup(builder);
63
- pass.data = data;
64
- this._passes.push(pass);
65
- this._compiled = null;
66
- return data;
28
+ const initialResourceIds = new Set(this._resources.keys());
29
+ try {
30
+ const data = setup(builder);
31
+ pass.data = data;
32
+ this._passes.push(pass);
33
+ this._compiled = null;
34
+ return data;
35
+ } catch (error) {
36
+ for (const id of this._resources.keys()){
37
+ if (!initialResourceIds.has(id)) {
38
+ this._resources.delete(id);
39
+ }
40
+ }
41
+ for (const resource of this._resources.values()){
42
+ for(let i = resource.consumers.length - 1; i >= 0; i--){
43
+ if (resource.consumers[i] === pass) {
44
+ resource.consumers.splice(i, 1);
45
+ }
46
+ }
47
+ if (resource.nextWriter === pass) {
48
+ resource.nextWriter = null;
49
+ }
50
+ }
51
+ // Remove WAR edges added to existing passes before setup failed.
52
+ for (const existing of this._passes){
53
+ for(let i = existing.warDependencies.length - 1; i >= 0; i--){
54
+ if (existing.warDependencies[i] === pass) {
55
+ existing.warDependencies.splice(i, 1);
56
+ }
57
+ }
58
+ }
59
+ this._lastWriterByPhysicalId.clear();
60
+ for (const resource of this._resources.values()){
61
+ if (resource.producer) {
62
+ this._lastWriterByPhysicalId.set(resource.physicalId, resource.producer);
63
+ }
64
+ }
65
+ throw error;
66
+ }
67
67
  }
68
- // ─── Compilation ────────────────────────────────────────────────────
69
68
  /**
70
- * Compile the render graph.
71
- *
72
- * Performs dead-pass culling, topological sorting, and resource lifetime analysis.
73
- *
74
- * @param outputs - Handles of resources that must be produced (graph sinks).
75
- * If a resource was passed to {@link RGPassBuilder.write}, use the returned
76
- * post-write handle here, not the original handle.
77
- * Passes that do not contribute to these outputs (directly or transitively)
78
- * are culled, unless marked as side-effect passes.
79
- * @returns The compiled graph ready for execution.
69
+ * Cull dead passes, sort dependencies, and calculate resource lifetimes.
70
+ * Outputs must be their latest written versions.
80
71
  */ compile(outputs) {
81
72
  this._validateOutputs(outputs);
82
- // 1. Mark alive passes via backward traversal from outputs + side-effect passes
83
73
  this._cullDeadPasses(outputs);
84
- // 2. Topological sort of alive passes
85
74
  const ordered = this._topologicalSort();
86
- // 3. Resource lifetime analysis
87
75
  const lifetimes = this._analyzeLifetimes(ordered);
76
+ this._validateAllocationKeys(lifetimes);
88
77
  this._compiled = {
89
78
  orderedPasses: ordered,
90
79
  lifetimes
91
80
  };
92
81
  return this._compiled;
93
82
  }
94
- /**
95
- * Execute a compiled render graph (simple mode, no resource management).
96
- *
97
- * For automatic resource allocation/release, use {@link RenderGraphExecutor} instead.
98
- *
99
- * @param compiled - The compiled graph from {@link RenderGraph.compile}.
100
- */ execute(compiled) {
83
+ /** @internal Test-only callback runner; resource-managed code uses RenderGraphExecutor. */ execute(compiled) {
101
84
  const noopCtx = {
102
85
  getTexture () {
103
86
  throw new Error('RenderGraph.execute(): resource resolution not available. Use RenderGraphExecutor for managed execution.');
@@ -108,9 +91,7 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
108
91
  createFramebuffer () {
109
92
  throw new Error('RenderGraph.execute(): framebuffer allocation not available. Use RenderGraphExecutor for managed execution.');
110
93
  },
111
- deferCleanup () {
112
- // No-op in unmanaged execution mode.
113
- }
94
+ deferCleanup () {}
114
95
  };
115
96
  for (const pass of compiled.orderedPasses){
116
97
  if (pass.subpasses.length > 0) {
@@ -122,18 +103,13 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
122
103
  }
123
104
  }
124
105
  }
125
- /**
126
- * Reset the graph for the next frame.
127
- *
128
- * Clears all passes, transient resources, and compiled state.
129
- * Imported resources are also cleared — re-import them each frame.
130
- */ reset() {
106
+ /** Clear all passes, resources, and compiled state. */ reset() {
131
107
  this._passes.length = 0;
132
108
  this._resources.clear();
133
109
  this._nextResourceId = 0;
134
110
  this._compiled = null;
111
+ this._lastWriterByPhysicalId.clear();
135
112
  }
136
- // ─── Accessors (for testing / debugging) ────────────────────────────
137
113
  /** @internal */ getResource(handle) {
138
114
  return this._resources.get(handle._id);
139
115
  }
@@ -143,27 +119,18 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
143
119
  /** @internal */ get resources() {
144
120
  return this._resources;
145
121
  }
146
- // ─── Private: Builder ───────────────────────────────────────────────
147
- /** @internal */ _createBuilder(pass) {
122
+ _createBuilder(pass) {
148
123
  const graph = this;
124
+ const textureLabelOccurrences = new Map();
149
125
  return {
150
126
  read (handle) {
151
127
  const res = graph._resources.get(handle._id);
152
128
  if (!res) {
153
129
  throw new Error(`RenderGraph: unknown resource "${handle.name}" (id=${handle._id})`);
154
130
  }
155
- // Validate that transient resources have a producer
156
- if (res.kind === 'transient' && !res.producer) {
157
- throw new Error(`RenderGraph: pass "${pass.name}" attempts to read transient resource "${res.name}" ` + `which has no producer. Ensure the resource is created before being read.`);
158
- }
159
- if (!pass.reads.includes(res)) {
160
- pass.reads.push(res);
161
- }
162
- if (!res.consumers.includes(pass)) {
163
- res.consumers.push(pass);
164
- }
131
+ graph._declareRead(pass, res);
165
132
  },
166
- write (handle) {
133
+ write (handle, options) {
167
134
  const res = graph._resources.get(handle._id);
168
135
  if (!res) {
169
136
  throw new Error(`RenderGraph: unknown resource "${handle.name}" (id=${handle._id})`);
@@ -174,32 +141,62 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
174
141
  if (res.kind === 'framebuffer') {
175
142
  throw new Error(`RenderGraph: pass "${pass.name}" attempts to write framebuffer "${res.name}". ` + `Create a new framebuffer view instead.`);
176
143
  }
144
+ const discard = options?.load === 'discard';
145
+ const previousWriter = graph._lastWriterByPhysicalId.get(res.physicalId);
146
+ if (previousWriter && previousWriter !== pass) {
147
+ const dependencies = discard ? pass.orderingDependencies : pass.dependencies;
148
+ if (!dependencies.includes(previousWriter)) {
149
+ dependencies.push(previousWriter);
150
+ }
151
+ }
152
+ // WAR orders live readers before the overwrite without retaining them.
177
153
  for (const consumer of res.consumers){
178
- if (consumer !== pass && !pass.dependencies.includes(consumer)) {
179
- pass.dependencies.push(consumer);
154
+ if (consumer !== pass && !pass.warDependencies.includes(consumer)) {
155
+ pass.warDependencies.push(consumer);
180
156
  }
181
157
  }
182
- if (res.producer && res.producer !== pass && !pass.dependencies.includes(res.producer)) {
183
- pass.dependencies.push(res.producer);
158
+ if (res.producer && res.producer !== pass) {
159
+ const dependencies = discard ? pass.orderingDependencies : pass.dependencies;
160
+ if (!dependencies.includes(res.producer)) {
161
+ dependencies.push(res.producer);
162
+ }
184
163
  }
185
164
  const id = graph._nextResourceId++;
186
165
  const versionName = `${res.name}@${pass.name}`;
187
- const version = new RGResource(id, versionName, res.kind, res.desc, res.physicalId);
166
+ const version = new RGResource(id, versionName, res.kind, res.desc, res.physicalId, res.allocationKey);
188
167
  version.producer = pass;
168
+ // WAW edges make the first overwrite sufficient for stale readers.
169
+ if (!res.nextWriter) {
170
+ res.nextWriter = pass;
171
+ }
189
172
  graph._resources.set(id, version);
173
+ graph._lastWriterByPhysicalId.set(res.physicalId, pass);
190
174
  if (!pass.writes.includes(version)) {
191
175
  pass.writes.push(version);
192
176
  }
193
- return new RGHandle(id, versionName);
177
+ return new RGHandle(id, versionName, 'texture');
194
178
  },
195
179
  createTexture (desc) {
180
+ if (desc.allocationKey !== undefined && desc.allocationKey.length === 0) {
181
+ throw new Error(`RenderGraph: pass "${pass.name}" specified an empty texture allocationKey.`);
182
+ }
196
183
  const id = graph._nextResourceId++;
197
184
  const name = desc.label ?? `_tex_${id}`;
198
- const res = new RGResource(id, name, 'transient', desc);
185
+ const labelKey = desc.label ?? '<unnamed>';
186
+ const labelOccurrence = textureLabelOccurrences.get(labelKey) ?? 0;
187
+ textureLabelOccurrences.set(labelKey, labelOccurrence + 1);
188
+ const allocationKey = desc.allocationKey ?? `rg:${JSON.stringify([
189
+ pass.name,
190
+ pass.nameOccurrence,
191
+ desc.label ?? null,
192
+ labelOccurrence
193
+ ])}`;
194
+ const res = new RGResource(id, name, 'transient', desc, id, allocationKey);
199
195
  res.producer = pass;
200
196
  graph._resources.set(id, res);
197
+ graph._lastWriterByPhysicalId.set(res.physicalId, pass);
201
198
  pass.writes.push(res);
202
- return new RGHandle(id, name);
199
+ return new RGHandle(id, name, 'texture');
203
200
  },
204
201
  createToken (name) {
205
202
  const id = graph._nextResourceId++;
@@ -208,7 +205,7 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
208
205
  res.producer = pass;
209
206
  graph._resources.set(id, res);
210
207
  pass.writes.push(res);
211
- return new RGHandle(id, tokenName);
208
+ return new RGHandle(id, tokenName, 'token');
212
209
  },
213
210
  createFramebuffer (desc) {
214
211
  const id = graph._nextResourceId++;
@@ -216,9 +213,10 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
216
213
  const res = new RGResource(id, name, 'framebuffer', desc);
217
214
  res.producer = pass;
218
215
  graph._resources.set(id, res);
216
+ graph._lastWriterByPhysicalId.set(res.physicalId, pass);
219
217
  pass.writes.push(res);
220
218
  graph._declareFramebufferAttachmentDeps(pass, desc);
221
- return new RGHandle(id, name);
219
+ return new RGHandle(id, name, 'framebuffer');
222
220
  },
223
221
  sideEffect () {
224
222
  pass.hasSideEffect = true;
@@ -237,8 +235,21 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
237
235
  }
238
236
  };
239
237
  }
240
- // ─── Private: Dead Pass Culling ─────────────────────────────────────
241
- /** @internal */ _validateOutputs(outputs) {
238
+ _validateAllocationKeys(lifetimes) {
239
+ const physicalByKey = new Map();
240
+ for (const lifetime of lifetimes.values()){
241
+ const resource = lifetime.resource;
242
+ if (resource.kind !== 'transient' || !resource.allocationKey) {
243
+ continue;
244
+ }
245
+ const existingPhysicalId = physicalByKey.get(resource.allocationKey);
246
+ if (existingPhysicalId !== undefined && existingPhysicalId !== resource.physicalId) {
247
+ throw new Error(`RenderGraph: transient allocationKey "${resource.allocationKey}" is used by multiple resources. ` + 'Use a unique allocationKey for each logical texture.');
248
+ }
249
+ physicalByKey.set(resource.allocationKey, resource.physicalId);
250
+ }
251
+ }
252
+ _validateOutputs(outputs) {
242
253
  const latestVersions = new Map();
243
254
  for (const res of this._resources.values()){
244
255
  const current = latestVersions.get(res.physicalId);
@@ -257,12 +268,10 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
257
268
  }
258
269
  }
259
270
  }
260
- /** @internal */ _cullDeadPasses(outputs) {
261
- // Start with all passes marked dead
271
+ _cullDeadPasses(outputs) {
262
272
  for (const pass of this._passes){
263
273
  pass.alive = false;
264
274
  }
265
- // Seed: resources that are requested outputs
266
275
  const neededResources = new Set();
267
276
  const stack = [];
268
277
  const markResourceNeeded = (res)=>{
@@ -279,6 +288,7 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
279
288
  for (const dep of pass.reads){
280
289
  markResourceNeeded(dep);
281
290
  }
291
+ // WAR dependencies order passes but do not propagate liveness.
282
292
  for (const dependency of pass.dependencies){
283
293
  markPassAlive(dependency);
284
294
  }
@@ -290,14 +300,11 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
290
300
  }
291
301
  markResourceNeeded(res);
292
302
  }
293
- // Seed: side-effect passes (always alive) — push their read dependencies
294
303
  for (const pass of this._passes){
295
304
  if (pass.hasSideEffect) {
296
305
  markPassAlive(pass);
297
306
  }
298
307
  }
299
- // Backward traversal: for each needed resource, mark its producer alive
300
- // and recursively mark the producer's read dependencies as needed
301
308
  while(stack.length > 0){
302
309
  const res = stack.pop();
303
310
  const producer = res.producer;
@@ -305,8 +312,30 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
305
312
  markPassAlive(producer);
306
313
  }
307
314
  }
315
+ // Warn when an ordering-only pass may have omitted sideEffect or an output.
316
+ for (const pass of this._passes){
317
+ if (!pass.alive && !pass.hasSideEffect && pass.warDependencies.length > 0 && !RenderGraph._warnedCulledPasses.has(pass.name)) {
318
+ RenderGraph._warnedCulledPasses.add(pass.name);
319
+ console.warn(`RenderGraph: pass "${pass.name}" was culled but is referenced by WAR ` + `ordering edges. If its work must run, call sideEffect() or route its ` + `output into a compile() sink.`);
320
+ }
321
+ }
308
322
  }
309
- /** @internal */ _declareFramebufferAttachmentDeps(pass, desc) {
323
+ _declareRead(pass, res) {
324
+ if (res.kind === 'transient' && !res.producer) {
325
+ throw new Error(`RenderGraph: pass "${pass.name}" attempts to read transient resource "${res.name}" ` + `which has no producer. Ensure the resource is created before being read.`);
326
+ }
327
+ if (!pass.reads.includes(res)) {
328
+ pass.reads.push(res);
329
+ }
330
+ if (!res.consumers.includes(pass)) {
331
+ res.consumers.push(pass);
332
+ }
333
+ // A stale-version read must precede its first overwrite.
334
+ if (res.nextWriter && res.nextWriter !== pass && !res.nextWriter.warDependencies.includes(pass)) {
335
+ res.nextWriter.warDependencies.push(pass);
336
+ }
337
+ }
338
+ _declareFramebufferAttachmentDeps(pass, desc) {
310
339
  const declare = (attachment)=>{
311
340
  if (attachment instanceof RGHandle) {
312
341
  const res = this._resources.get(attachment._id);
@@ -317,12 +346,7 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
317
346
  throw new Error(`RenderGraph: framebuffer attachment "${res.name}" must be a texture resource, got ${res.kind}.`);
318
347
  }
319
348
  if (res.producer !== pass) {
320
- if (!pass.reads.includes(res)) {
321
- pass.reads.push(res);
322
- }
323
- if (!res.consumers.includes(pass)) {
324
- res.consumers.push(pass);
325
- }
349
+ this._declareRead(pass, res);
326
350
  }
327
351
  }
328
352
  };
@@ -334,13 +358,11 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
334
358
  }
335
359
  declare(desc.depthAttachment);
336
360
  }
337
- // ─── Private: Topological Sort (Kahn's Algorithm) ───────────────────
338
- /** @internal */ _topologicalSort() {
361
+ _topologicalSort() {
339
362
  const alivePasses = this._passes.filter((p)=>p.alive);
340
363
  if (alivePasses.length === 0) {
341
364
  return [];
342
365
  }
343
- // Build adjacency: producer -> consumers (only among alive passes)
344
366
  const aliveSet = new Set(alivePasses);
345
367
  const inDegree = new Map();
346
368
  const adjacency = new Map();
@@ -362,8 +384,13 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
362
384
  for (const dependency of pass.dependencies){
363
385
  addEdge(dependency, pass);
364
386
  }
387
+ for (const dependency of pass.orderingDependencies){
388
+ addEdge(dependency, pass);
389
+ }
390
+ for (const dependency of pass.warDependencies){
391
+ addEdge(dependency, pass);
392
+ }
365
393
  }
366
- // For each resource, its producer has an edge to each of its consumers
367
394
  for (const res of this._resources.values()){
368
395
  if (!res.producer || !aliveSet.has(res.producer)) {
369
396
  continue;
@@ -372,7 +399,6 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
372
399
  addEdge(res.producer, consumer);
373
400
  }
374
401
  }
375
- // Kahn's algorithm
376
402
  const queue = [];
377
403
  for (const pass of alivePasses){
378
404
  if (inDegree.get(pass) === 0) {
@@ -392,7 +418,6 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
392
418
  }
393
419
  }
394
420
  if (result.length !== alivePasses.length) {
395
- // Find passes that are part of the cycle (those with non-zero in-degree)
396
421
  const cycleParticipants = [];
397
422
  for (const [pass, degree] of inDegree){
398
423
  if (degree > 0) {
@@ -403,10 +428,8 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
403
428
  }
404
429
  return result;
405
430
  }
406
- // ─── Private: Resource Lifetime Analysis ────────────────────────────
407
- /** @internal */ _analyzeLifetimes(orderedPasses) {
431
+ _analyzeLifetimes(orderedPasses) {
408
432
  const lifetimes = new Map();
409
- // Build pass -> order index map
410
433
  const orderMap = new Map();
411
434
  for(let i = 0; i < orderedPasses.length; i++){
412
435
  orderMap.set(orderedPasses[i], i);
@@ -414,13 +437,11 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
414
437
  for (const res of this._resources.values()){
415
438
  let first = Infinity;
416
439
  let last = -Infinity;
417
- // Producer
418
440
  if (res.producer && orderMap.has(res.producer)) {
419
441
  const idx = orderMap.get(res.producer);
420
442
  first = Math.min(first, idx);
421
443
  last = Math.max(last, idx);
422
444
  }
423
- // Consumers
424
445
  for (const consumer of res.consumers){
425
446
  if (orderMap.has(consumer)) {
426
447
  const idx = orderMap.get(consumer);
@@ -436,11 +457,7 @@ import { RGHandle, RGPass, RGResource, RGSubpass } from './types.js';
436
457
  });
437
458
  }
438
459
  }
439
- // A pass that reads a framebuffer implicitly reads its attachment textures:
440
- // extend each RGHandle attachment's lifetime to cover the framebuffer's
441
- // lifetime so the executor never releases a backing texture while a later
442
- // pass can still render through the framebuffer. Framebuffers cannot be
443
- // attachments of other framebuffers, so a single propagation pass suffices.
460
+ // Attachment textures must outlive every framebuffer use.
444
461
  for (const lifetime of lifetimes.values()){
445
462
  const res = lifetime.resource;
446
463
  if (res.kind !== 'framebuffer' || !res.desc) {