@bornengine/engine 0.4.16

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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/native/android/Cargo.lock +1848 -0
  4. package/native/android/Cargo.toml +24 -0
  5. package/native/android/src/lib.rs +702 -0
  6. package/native/ios/Cargo.lock +1690 -0
  7. package/native/ios/Cargo.toml +32 -0
  8. package/native/ios/src/lib.rs +1267 -0
  9. package/native/linux/Cargo.lock +3279 -0
  10. package/native/linux/Cargo.toml +29 -0
  11. package/native/linux/src/lib.rs +1331 -0
  12. package/native/macos/Cargo.lock +3310 -0
  13. package/native/macos/Cargo.toml +46 -0
  14. package/native/macos/src/lib.rs +1302 -0
  15. package/native/shared/Cargo.lock +1899 -0
  16. package/native/shared/Cargo.toml +62 -0
  17. package/native/shared/assets/default_font.ttf +0 -0
  18. package/native/shared/build.rs +270 -0
  19. package/native/shared/shaders/common/clouds.wgsl +122 -0
  20. package/native/shared/shaders/common/fog.wgsl +16 -0
  21. package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
  22. package/native/shared/shaders/common/imposter.wgsl +112 -0
  23. package/native/shared/shaders/common/pbr.wgsl +186 -0
  24. package/native/shared/shaders/common/shadows.wgsl +186 -0
  25. package/native/shared/shaders/common/sky.wgsl +8 -0
  26. package/native/shared/shaders/common/tonemap.wgsl +25 -0
  27. package/native/shared/shaders/impulse_field.wgsl +57 -0
  28. package/native/shared/shaders/material_abi.wgsl +383 -0
  29. package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
  30. package/native/shared/src/anim_mixer.rs +61 -0
  31. package/native/shared/src/attach.rs +263 -0
  32. package/native/shared/src/audio/decode.rs +123 -0
  33. package/native/shared/src/audio/mod.rs +863 -0
  34. package/native/shared/src/audio/render.rs +892 -0
  35. package/native/shared/src/audio/spsc.rs +156 -0
  36. package/native/shared/src/audio/stream.rs +226 -0
  37. package/native/shared/src/custom_shaders.rs +104 -0
  38. package/native/shared/src/decals.rs +245 -0
  39. package/native/shared/src/drs.rs +211 -0
  40. package/native/shared/src/engine.rs +261 -0
  41. package/native/shared/src/ffi.rs +116 -0
  42. package/native/shared/src/ffi_core/assets.rs +388 -0
  43. package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
  44. package/native/shared/src/ffi_core/draw.rs +334 -0
  45. package/native/shared/src/ffi_core/game_loop.rs +577 -0
  46. package/native/shared/src/ffi_core/input.rs +234 -0
  47. package/native/shared/src/ffi_core/mod.rs +127 -0
  48. package/native/shared/src/ffi_core/models.rs +1154 -0
  49. package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
  50. package/native/shared/src/ffi_core/scene.rs +626 -0
  51. package/native/shared/src/ffi_core/vfx.rs +212 -0
  52. package/native/shared/src/ffi_core/visual.rs +691 -0
  53. package/native/shared/src/frame_callbacks.rs +122 -0
  54. package/native/shared/src/geometry.rs +236 -0
  55. package/native/shared/src/handles.rs +182 -0
  56. package/native/shared/src/input.rs +448 -0
  57. package/native/shared/src/jolt_sys.rs +822 -0
  58. package/native/shared/src/lib.rs +55 -0
  59. package/native/shared/src/models.rs +1093 -0
  60. package/native/shared/src/models_gltf.rs +1280 -0
  61. package/native/shared/src/particles.rs +391 -0
  62. package/native/shared/src/physics_jolt.rs +1908 -0
  63. package/native/shared/src/picking.rs +298 -0
  64. package/native/shared/src/postfx.rs +345 -0
  65. package/native/shared/src/profiler.rs +492 -0
  66. package/native/shared/src/ragdoll.rs +474 -0
  67. package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
  68. package/native/shared/src/renderer/brdf_lut.rs +154 -0
  69. package/native/shared/src/renderer/draw2d.rs +143 -0
  70. package/native/shared/src/renderer/formats.rs +822 -0
  71. package/native/shared/src/renderer/froxel.rs +421 -0
  72. package/native/shared/src/renderer/gi_bake.rs +653 -0
  73. package/native/shared/src/renderer/graph.rs +462 -0
  74. package/native/shared/src/renderer/hiz.rs +269 -0
  75. package/native/shared/src/renderer/hot_reload.rs +390 -0
  76. package/native/shared/src/renderer/impulse_field.rs +456 -0
  77. package/native/shared/src/renderer/lighting.rs +154 -0
  78. package/native/shared/src/renderer/material_instancing.rs +171 -0
  79. package/native/shared/src/renderer/material_pipeline.rs +700 -0
  80. package/native/shared/src/renderer/material_system.rs +1996 -0
  81. package/native/shared/src/renderer/material_system_tests.rs +601 -0
  82. package/native/shared/src/renderer/material_system_wasm.rs +41 -0
  83. package/native/shared/src/renderer/mod.rs +12556 -0
  84. package/native/shared/src/renderer/model_draw.rs +641 -0
  85. package/native/shared/src/renderer/occlusion.rs +429 -0
  86. package/native/shared/src/renderer/planar_pass.rs +593 -0
  87. package/native/shared/src/renderer/planar_reflection.rs +499 -0
  88. package/native/shared/src/renderer/post_pass.rs +249 -0
  89. package/native/shared/src/renderer/postfx_chain.rs +728 -0
  90. package/native/shared/src/renderer/pt_pass.rs +577 -0
  91. package/native/shared/src/renderer/scene_pass.rs +607 -0
  92. package/native/shared/src/renderer/shader_include.rs +205 -0
  93. package/native/shared/src/renderer/shader_library.rs +135 -0
  94. package/native/shared/src/renderer/shaders/ao.rs +570 -0
  95. package/native/shared/src/renderer/shaders/core.rs +1243 -0
  96. package/native/shared/src/renderer/shaders/env.rs +907 -0
  97. package/native/shared/src/renderer/shaders/gi.rs +810 -0
  98. package/native/shared/src/renderer/shaders/mod.rs +19 -0
  99. package/native/shared/src/renderer/shaders/post.rs +1558 -0
  100. package/native/shared/src/renderer/shaders/pt.rs +1859 -0
  101. package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
  102. package/native/shared/src/renderer/shadow_pass.rs +731 -0
  103. package/native/shared/src/renderer/ssgi_pass.rs +392 -0
  104. package/native/shared/src/renderer/ssr_pass.rs +188 -0
  105. package/native/shared/src/renderer/texture_store.rs +473 -0
  106. package/native/shared/src/renderer/transient.rs +591 -0
  107. package/native/shared/src/renderer/types.rs +941 -0
  108. package/native/shared/src/renderer/util.rs +152 -0
  109. package/native/shared/src/scene.rs +1362 -0
  110. package/native/shared/src/sdf_cache.rs +274 -0
  111. package/native/shared/src/shadows.rs +1036 -0
  112. package/native/shared/src/staging.rs +102 -0
  113. package/native/shared/src/string_header.rs +266 -0
  114. package/native/shared/src/text_renderer.rs +502 -0
  115. package/native/shared/src/textures.rs +197 -0
  116. package/native/tvos/Cargo.lock +1693 -0
  117. package/native/tvos/Cargo.toml +36 -0
  118. package/native/tvos/metal-patched/Cargo.toml +178 -0
  119. package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
  120. package/native/tvos/metal-patched/LICENSE-MIT +25 -0
  121. package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
  122. package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
  123. package/native/tvos/metal-patched/src/argument.rs +366 -0
  124. package/native/tvos/metal-patched/src/blitpass.rs +102 -0
  125. package/native/tvos/metal-patched/src/buffer.rs +71 -0
  126. package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
  127. package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
  128. package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
  129. package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
  130. package/native/tvos/metal-patched/src/computepass.rs +107 -0
  131. package/native/tvos/metal-patched/src/constants.rs +152 -0
  132. package/native/tvos/metal-patched/src/counters.rs +119 -0
  133. package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
  134. package/native/tvos/metal-patched/src/device.rs +2134 -0
  135. package/native/tvos/metal-patched/src/drawable.rs +39 -0
  136. package/native/tvos/metal-patched/src/encoder.rs +2041 -0
  137. package/native/tvos/metal-patched/src/heap.rs +281 -0
  138. package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
  139. package/native/tvos/metal-patched/src/lib.rs +657 -0
  140. package/native/tvos/metal-patched/src/library.rs +902 -0
  141. package/native/tvos/metal-patched/src/mps.rs +575 -0
  142. package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
  143. package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
  144. package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
  145. package/native/tvos/metal-patched/src/renderpass.rs +443 -0
  146. package/native/tvos/metal-patched/src/resource.rs +182 -0
  147. package/native/tvos/metal-patched/src/sampler.rs +165 -0
  148. package/native/tvos/metal-patched/src/sync.rs +178 -0
  149. package/native/tvos/metal-patched/src/texture.rs +352 -0
  150. package/native/tvos/metal-patched/src/types.rs +90 -0
  151. package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
  152. package/native/tvos/src/audio_backend.rs +197 -0
  153. package/native/tvos/src/lib.rs +1891 -0
  154. package/native/visionos/Cargo.lock +1693 -0
  155. package/native/visionos/Cargo.toml +40 -0
  156. package/native/visionos/src/audio_backend.rs +197 -0
  157. package/native/visionos/src/lib.rs +1887 -0
  158. package/native/watchos/Cargo.lock +16 -0
  159. package/native/watchos/Cargo.toml +19 -0
  160. package/native/watchos/shaders/bloom_postfx.metal +99 -0
  161. package/native/watchos/src/BloomWatchApp.swift +1267 -0
  162. package/native/watchos/src/BloomWatchAudio.swift +179 -0
  163. package/native/watchos/src/audio.rs +55 -0
  164. package/native/watchos/src/draw_list.rs +229 -0
  165. package/native/watchos/src/ffi_stubs.rs +915 -0
  166. package/native/watchos/src/ffi_stubs_manual.rs +35 -0
  167. package/native/watchos/src/lib.rs +1124 -0
  168. package/native/watchos/src/models.rs +746 -0
  169. package/native/watchos/src/postfx.rs +95 -0
  170. package/native/watchos/src/scene.rs +534 -0
  171. package/native/watchos/src/textures.rs +184 -0
  172. package/native/web/Cargo.lock +1657 -0
  173. package/native/web/Cargo.toml +43 -0
  174. package/native/web/bloom_glue.js +695 -0
  175. package/native/web/build.sh +131 -0
  176. package/native/web/index.html +35 -0
  177. package/native/web/jolt_bridge.js +1519 -0
  178. package/native/web/src/input_ffi.rs +286 -0
  179. package/native/web/src/lib.rs +1796 -0
  180. package/native/web/src/material_ffi.rs +710 -0
  181. package/native/web/src/parity_ffi.rs +343 -0
  182. package/native/web/src/physics_ffi.rs +643 -0
  183. package/native/web/src/ragdoll_ffi.rs +250 -0
  184. package/native/web/src/render_settings.rs +98 -0
  185. package/native/windows/Cargo.lock +1815 -0
  186. package/native/windows/Cargo.toml +68 -0
  187. package/native/windows/src/lib.rs +1486 -0
  188. package/package.json +4279 -0
  189. package/src/audio/index.ts +315 -0
  190. package/src/core/colors.ts +63 -0
  191. package/src/core/index.ts +1206 -0
  192. package/src/core/keys.ts +63 -0
  193. package/src/core/types.ts +104 -0
  194. package/src/index.ts +171 -0
  195. package/src/math/index.ts +516 -0
  196. package/src/mobile/index.ts +294 -0
  197. package/src/models/index.ts +1258 -0
  198. package/src/physics/index.ts +1134 -0
  199. package/src/scene/index.ts +698 -0
  200. package/src/shapes/index.ts +120 -0
  201. package/src/text/index.ts +48 -0
  202. package/src/textures/index.ts +187 -0
  203. package/src/vfx/index.ts +191 -0
  204. package/src/world/index.ts +24 -0
  205. package/src/world/loader.ts +423 -0
  206. package/src/world/prefab.ts +217 -0
  207. package/src/world/render.ts +172 -0
  208. package/src/world/saver.ts +108 -0
  209. package/src/world/serialize.ts +301 -0
  210. package/src/world/terrain.ts +355 -0
  211. package/src/world/types.ts +160 -0
  212. package/src/world/validate.ts +319 -0
  213. package/src/world/version.ts +114 -0
@@ -0,0 +1,462 @@
1
+ // Render graph — Phase 2 of RFC 0001.
2
+ //
3
+ // Declarative pass scheduler. Each `PassNode` declares what it reads,
4
+ // what it writes, and a `run` closure that encodes its rendering work
5
+ // against an owning `Renderer`. The scheduler topologically sorts the
6
+ // nodes so a node that reads a resource always runs after the node
7
+ // that writes it; explicit `after` / `before` hints break ties.
8
+ //
9
+ // This module landed in two steps: Phase 2 brought the types, the
10
+ // scheduler, and ordering unit tests; Phase 2b (complete — see the
11
+ // frame-graph construction in `mod.rs`) ported the existing passes, so
12
+ // the graph now drives the real frame in `end_frame_with_scene`.
13
+
14
+ use std::collections::{HashMap, HashSet};
15
+
16
+ // =====================================================================
17
+ // Inputs / Outputs — the resource kinds the graph schedules around
18
+ // =====================================================================
19
+
20
+ /// Resources a pass may read. `Transient` is the open extension point
21
+ /// for Phase 3's resource pool; the named variants cover the
22
+ /// well-known render targets that the engine already owns.
23
+ #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
24
+ pub enum PassInput {
25
+ /// Snapshot of HDR colour before the current pass ran. Triggers
26
+ /// a `CopyToSample` synthetic node if no prior pass explicitly
27
+ /// produced it.
28
+ SceneColor,
29
+ /// Linearised scene depth, sampled. Same synthetic-copy semantics.
30
+ SceneDepth,
31
+ /// Cascaded shadow map depth at cascade index.
32
+ Shadow(u8),
33
+ EnvCubemap,
34
+ MotionVectors,
35
+ /// Phase 7 — world-space impulse texture (decals, splashes, wakes).
36
+ Impulse,
37
+ /// Phase-3-defined transient texture identified by the pool's
38
+ /// handle type. Represented as a u32 here so the graph module
39
+ /// doesn't depend on the pool implementation.
40
+ Transient(u32),
41
+ }
42
+
43
+ /// Resources a pass may write. The variants mirror `PassInput` where
44
+ /// it makes sense, plus some write-only targets (MRT G-buffer slots,
45
+ /// the shadow map, the swapchain).
46
+ #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
47
+ pub enum PassOutput {
48
+ HdrColor,
49
+ Depth,
50
+ MaterialRt,
51
+ VelocityRt,
52
+ AlbedoRt,
53
+ Shadow(u8),
54
+ Transient(u32),
55
+ /// Terminal node — the final surface presentation target. Every
56
+ /// frame's graph must end with exactly one node that writes
57
+ /// Swapchain.
58
+ Swapchain,
59
+ }
60
+
61
+ // =====================================================================
62
+ // PassNode — what each scheduler unit looks like
63
+ // =====================================================================
64
+
65
+ /// A single schedulable unit of GPU work. `run` is deliberately left
66
+ /// as a boxed closure so pass bodies own whatever captured state they
67
+ /// need; the scheduler doesn't know or care.
68
+ ///
69
+ /// `RunFn` takes a single untyped context argument because this module
70
+ /// must not depend on `Renderer` (would create a module cycle). The
71
+ /// `'a` lifetime lets closures borrow frame-scoped data (render
72
+ /// targets, uniform buffers) from the renderer — dropping the `Send`
73
+ /// bound is deliberate, since graph execution is single-threaded from
74
+ /// the caller's perspective.
75
+ pub type RunFn<'a, Ctx> = Box<dyn FnOnce(&mut Ctx) + 'a>;
76
+
77
+ pub struct PassNode<'a, Ctx> {
78
+ pub name: &'static str,
79
+ pub reads: Vec<PassInput>,
80
+ pub writes: Vec<PassOutput>,
81
+ /// Hard "run this node after X" hints — used for ordering that the
82
+ /// data dependencies alone can't express (e.g. two passes that
83
+ /// share a read target but where one conceptually follows the
84
+ /// other for timing reasons).
85
+ pub after: Vec<&'static str>,
86
+ /// Hard "run this node before X" hints. Validator rejects cycles.
87
+ pub before: Vec<&'static str>,
88
+ pub run: RunFn<'a, Ctx>,
89
+ }
90
+
91
+ impl<'a, Ctx> PassNode<'a, Ctx> {
92
+ pub fn new(name: &'static str, run: RunFn<'a, Ctx>) -> Self {
93
+ Self {
94
+ name, reads: Vec::new(), writes: Vec::new(),
95
+ after: Vec::new(), before: Vec::new(),
96
+ run,
97
+ }
98
+ }
99
+ pub fn with_reads(mut self, reads: &[PassInput]) -> Self {
100
+ self.reads = reads.to_vec(); self
101
+ }
102
+ pub fn with_writes(mut self, writes: &[PassOutput]) -> Self {
103
+ self.writes = writes.to_vec(); self
104
+ }
105
+ pub fn with_after(mut self, after: &[&'static str]) -> Self {
106
+ self.after = after.to_vec(); self
107
+ }
108
+ pub fn with_before(mut self, before: &[&'static str]) -> Self {
109
+ self.before = before.to_vec(); self
110
+ }
111
+ }
112
+
113
+ // =====================================================================
114
+ // Graph — a container of nodes + schedule + execute
115
+ // =====================================================================
116
+
117
+ pub struct Graph<'a, Ctx> {
118
+ pub nodes: Vec<PassNode<'a, Ctx>>,
119
+ }
120
+
121
+ #[derive(Debug)]
122
+ pub enum GraphError {
123
+ /// `after` / `before` referenced a name that isn't in the graph.
124
+ UnknownNode { node: String, referenced: String },
125
+ /// Data dependencies + hints imply a cycle that can't be resolved.
126
+ Cycle(Vec<String>),
127
+ }
128
+
129
+ impl<'a, Ctx> Graph<'a, Ctx> {
130
+ pub fn new() -> Self { Self { nodes: Vec::new() } }
131
+ pub fn push(&mut self, node: PassNode<'a, Ctx>) -> &mut Self {
132
+ self.nodes.push(node); self
133
+ }
134
+
135
+ /// Compute execution order. Returns indices into `self.nodes`.
136
+ pub fn schedule(&self) -> Result<Vec<usize>, GraphError> {
137
+ schedule(&self.nodes)
138
+ }
139
+
140
+ /// Schedule + run all nodes in order. Consumes the graph because
141
+ /// each node's `run` closure is `FnOnce`.
142
+ pub fn execute(self, ctx: &mut Ctx) -> Result<(), GraphError> {
143
+ let order = schedule(&self.nodes)?;
144
+ // Drain in order. Using Option<PassNode> trick so we can take()
145
+ // nodes out by index without panicking the Vec's geometry.
146
+ let mut slots: Vec<Option<PassNode<'a, Ctx>>> = self.nodes.into_iter().map(Some).collect();
147
+ for i in order {
148
+ let node = slots[i].take().expect("each node scheduled exactly once");
149
+ (node.run)(ctx);
150
+ }
151
+ Ok(())
152
+ }
153
+ }
154
+
155
+ impl<'a, Ctx> Default for Graph<'a, Ctx> {
156
+ fn default() -> Self { Self::new() }
157
+ }
158
+
159
+ // =====================================================================
160
+ // Scheduler — topological sort (Kahn's algorithm) with hint tie-breaks
161
+ // =====================================================================
162
+
163
+ fn schedule<'a, Ctx>(nodes: &[PassNode<'a, Ctx>]) -> Result<Vec<usize>, GraphError> {
164
+ let n = nodes.len();
165
+ if n == 0 { return Ok(Vec::new()); }
166
+
167
+ // Name → index lookup for `after` / `before` hint resolution.
168
+ let mut name_to_idx: HashMap<&'static str, usize> = HashMap::new();
169
+ for (i, node) in nodes.iter().enumerate() {
170
+ name_to_idx.insert(node.name, i);
171
+ }
172
+
173
+ // Build the predecessor set for each node.
174
+ // Edge A → B means A must run before B.
175
+ let mut preds: Vec<HashSet<usize>> = vec![HashSet::new(); n];
176
+
177
+ // 1) Data-dependency edges: if B reads what A writes, A → B.
178
+ // Approximate "writes" as "any write matches any read" — the
179
+ // enum comparison treats PassOutput::HdrColor as matching
180
+ // PassInput::SceneColor via the `matches_write` helper below.
181
+ for b in 0..n {
182
+ for read in &nodes[b].reads {
183
+ for a in 0..n {
184
+ if a == b { continue; }
185
+ for write in &nodes[a].writes {
186
+ if input_matches_write(read, write) {
187
+ preds[b].insert(a);
188
+ }
189
+ }
190
+ }
191
+ }
192
+ }
193
+ // 2) Explicit `after` hints: node B.after = [A] → A → B.
194
+ for (b, node) in nodes.iter().enumerate() {
195
+ for name in &node.after {
196
+ let a = name_to_idx.get(name).ok_or_else(|| GraphError::UnknownNode {
197
+ node: node.name.to_string(),
198
+ referenced: name.to_string(),
199
+ })?;
200
+ if *a != b { preds[b].insert(*a); }
201
+ }
202
+ }
203
+ // 3) Explicit `before` hints: node A.before = [B] → A → B.
204
+ for (a, node) in nodes.iter().enumerate() {
205
+ for name in &node.before {
206
+ let b = name_to_idx.get(name).ok_or_else(|| GraphError::UnknownNode {
207
+ node: node.name.to_string(),
208
+ referenced: name.to_string(),
209
+ })?;
210
+ if *b != a { preds[*b].insert(a); }
211
+ }
212
+ }
213
+
214
+ // Kahn's algorithm: repeatedly pick a node whose `preds` is empty
215
+ // (or whose unsatisfied preds are already in the output), add it,
216
+ // strip it from every other preds set. Break ties by declaration
217
+ // order to keep the schedule deterministic.
218
+ let mut in_degree: Vec<usize> = preds.iter().map(|p| p.len()).collect();
219
+ let mut out: Vec<usize> = Vec::with_capacity(n);
220
+ loop {
221
+ let next = (0..n).find(|&i| !out.contains(&i) && in_degree[i] == 0);
222
+ match next {
223
+ Some(i) => {
224
+ out.push(i);
225
+ for j in 0..n {
226
+ if preds[j].remove(&i) { in_degree[j] -= 1; }
227
+ }
228
+ }
229
+ None => break,
230
+ }
231
+ }
232
+
233
+ if out.len() != n {
234
+ // Anything still in `preds` is part of a cycle.
235
+ let cycle: Vec<String> = (0..n)
236
+ .filter(|i| !out.contains(i))
237
+ .map(|i| nodes[i].name.to_string())
238
+ .collect();
239
+ return Err(GraphError::Cycle(cycle));
240
+ }
241
+ Ok(out)
242
+ }
243
+
244
+ /// A read-of-output match: SceneColor reads from HdrColor,
245
+ /// SceneDepth reads from Depth, etc. The synthetic copy-to-sample
246
+ /// node that Phase 2b introduces is the one that actually produces
247
+ /// the sampled snapshot; for scheduling purposes we treat them as
248
+ /// the same resource.
249
+ fn input_matches_write(input: &PassInput, output: &PassOutput) -> bool {
250
+ match (input, output) {
251
+ (PassInput::SceneColor, PassOutput::HdrColor) => true,
252
+ (PassInput::SceneDepth, PassOutput::Depth) => true,
253
+ (PassInput::Shadow(i), PassOutput::Shadow(j)) => i == j,
254
+ (PassInput::Transient(i), PassOutput::Transient(j)) => i == j,
255
+ // EnvCubemap, MotionVectors, Impulse are produced outside
256
+ // the render graph for now (env is loaded at startup; motion
257
+ // vectors are written as a G-buffer target but not individually
258
+ // tracked). No-op match.
259
+ _ => false,
260
+ }
261
+ }
262
+
263
+ // =====================================================================
264
+ // Tests
265
+ // =====================================================================
266
+
267
+ #[cfg(test)]
268
+ mod tests {
269
+ use super::*;
270
+ use std::sync::{Arc, Mutex};
271
+
272
+ /// Test context that records the order of pass executions.
273
+ type TestCtx = Vec<&'static str>;
274
+
275
+ fn record(name: &'static str) -> RunFn<'static, TestCtx> {
276
+ Box::new(move |ctx: &mut TestCtx| ctx.push(name))
277
+ }
278
+
279
+ #[test]
280
+ fn empty_graph_runs_clean() {
281
+ let graph: Graph<'_, TestCtx> = Graph::new();
282
+ let mut ctx = Vec::new();
283
+ graph.execute(&mut ctx).unwrap();
284
+ assert!(ctx.is_empty());
285
+ }
286
+
287
+ #[test]
288
+ fn single_node_runs() {
289
+ let mut graph = Graph::new();
290
+ graph.push(PassNode::new("a", record("a")));
291
+ let mut ctx = Vec::new();
292
+ graph.execute(&mut ctx).unwrap();
293
+ assert_eq!(ctx, vec!["a"]);
294
+ }
295
+
296
+ #[test]
297
+ fn data_dependency_orders_reads_after_writes() {
298
+ // A writes HdrColor, B reads SceneColor. B must run after A.
299
+ let mut graph = Graph::new();
300
+ graph.push(PassNode::new("B_reads_scene", record("B"))
301
+ .with_reads(&[PassInput::SceneColor]));
302
+ graph.push(PassNode::new("A_writes_hdr", record("A"))
303
+ .with_writes(&[PassOutput::HdrColor]));
304
+ let mut ctx = Vec::new();
305
+ graph.execute(&mut ctx).unwrap();
306
+ assert_eq!(ctx, vec!["A", "B"]);
307
+ }
308
+
309
+ #[test]
310
+ fn explicit_after_hint_orders_nodes() {
311
+ let mut graph = Graph::new();
312
+ graph.push(PassNode::new("later", record("later"))
313
+ .with_after(&["earlier"]));
314
+ graph.push(PassNode::new("earlier", record("earlier")));
315
+ let mut ctx = Vec::new();
316
+ graph.execute(&mut ctx).unwrap();
317
+ assert_eq!(ctx, vec!["earlier", "later"]);
318
+ }
319
+
320
+ #[test]
321
+ fn explicit_before_hint_orders_nodes() {
322
+ let mut graph = Graph::new();
323
+ graph.push(PassNode::new("second", record("second")));
324
+ graph.push(PassNode::new("first", record("first"))
325
+ .with_before(&["second"]));
326
+ let mut ctx = Vec::new();
327
+ graph.execute(&mut ctx).unwrap();
328
+ assert_eq!(ctx, vec!["first", "second"]);
329
+ }
330
+
331
+ #[test]
332
+ fn chain_of_three() {
333
+ // A -> B -> C via data dependencies.
334
+ let mut graph = Graph::new();
335
+ graph.push(PassNode::new("C", record("C"))
336
+ .with_reads(&[PassInput::Transient(1)]));
337
+ graph.push(PassNode::new("A", record("A"))
338
+ .with_writes(&[PassOutput::HdrColor]));
339
+ graph.push(PassNode::new("B", record("B"))
340
+ .with_reads(&[PassInput::SceneColor])
341
+ .with_writes(&[PassOutput::Transient(1)]));
342
+ let mut ctx = Vec::new();
343
+ graph.execute(&mut ctx).unwrap();
344
+ assert_eq!(ctx, vec!["A", "B", "C"]);
345
+ }
346
+
347
+ #[test]
348
+ fn parallel_branches_deterministic_by_declaration_order() {
349
+ // Both X and Y depend on ROOT, neither depends on the other.
350
+ // Order should be declaration order among the tied candidates.
351
+ let mut graph = Graph::new();
352
+ graph.push(PassNode::new("X", record("X"))
353
+ .with_reads(&[PassInput::SceneColor]));
354
+ graph.push(PassNode::new("Y", record("Y"))
355
+ .with_reads(&[PassInput::SceneColor]));
356
+ graph.push(PassNode::new("ROOT", record("ROOT"))
357
+ .with_writes(&[PassOutput::HdrColor]));
358
+ let mut ctx = Vec::new();
359
+ graph.execute(&mut ctx).unwrap();
360
+ assert_eq!(ctx, vec!["ROOT", "X", "Y"]);
361
+ }
362
+
363
+ #[test]
364
+ fn unknown_after_is_reported() {
365
+ let mut graph: Graph<'_, TestCtx> = Graph::new();
366
+ graph.push(PassNode::new("a", record("a"))
367
+ .with_after(&["does_not_exist"]));
368
+ let err = graph.schedule().unwrap_err();
369
+ match err {
370
+ GraphError::UnknownNode { node, referenced } => {
371
+ assert_eq!(node, "a");
372
+ assert_eq!(referenced, "does_not_exist");
373
+ }
374
+ _ => panic!("expected UnknownNode"),
375
+ }
376
+ }
377
+
378
+ #[test]
379
+ fn cycle_via_explicit_hints_is_rejected() {
380
+ let mut graph: Graph<'_, TestCtx> = Graph::new();
381
+ graph.push(PassNode::new("a", record("a"))
382
+ .with_after(&["b"]));
383
+ graph.push(PassNode::new("b", record("b"))
384
+ .with_after(&["a"]));
385
+ match graph.schedule() {
386
+ Err(GraphError::Cycle(names)) => {
387
+ assert!(names.contains(&"a".to_string()));
388
+ assert!(names.contains(&"b".to_string()));
389
+ }
390
+ other => panic!("expected Cycle, got {:?}", other),
391
+ }
392
+ }
393
+
394
+ #[test]
395
+ fn concrete_frame_graph_sketches_correctly() {
396
+ // A rough sketch of the Phase-2b target frame graph — shadow
397
+ // → main_hdr → ssao → translucent → composite → swapchain —
398
+ // proves the scheduler produces the expected order.
399
+ let mut graph: Graph<'_, TestCtx> = Graph::new();
400
+ graph.push(PassNode::new("composite", record("composite"))
401
+ .with_reads(&[PassInput::Transient(10)])
402
+ // Composite is the terminal pass — explicit `after` on
403
+ // every predecessor so tie-breaking (declaration order)
404
+ // doesn't float composite past a sibling that hasn't run.
405
+ .with_after(&["bloom", "translucent", "ssao"])
406
+ .with_writes(&[PassOutput::Swapchain]));
407
+ graph.push(PassNode::new("bloom", record("bloom"))
408
+ .with_reads(&[PassInput::SceneColor])
409
+ .with_writes(&[PassOutput::Transient(10)]));
410
+ graph.push(PassNode::new("translucent", record("translucent"))
411
+ .with_reads(&[PassInput::SceneColor, PassInput::SceneDepth])
412
+ .with_writes(&[PassOutput::HdrColor])
413
+ .with_after(&["main_hdr"])); // explicit so SceneColor is
414
+ // a snapshot, not a write-
415
+ // then-read loop
416
+ graph.push(PassNode::new("ssao", record("ssao"))
417
+ .with_reads(&[PassInput::SceneDepth])
418
+ .with_writes(&[PassOutput::Transient(20)]));
419
+ graph.push(PassNode::new("main_hdr", record("main_hdr"))
420
+ .with_reads(&[PassInput::Shadow(0)])
421
+ .with_writes(&[PassOutput::HdrColor, PassOutput::Depth,
422
+ PassOutput::MaterialRt, PassOutput::VelocityRt,
423
+ PassOutput::AlbedoRt]));
424
+ graph.push(PassNode::new("shadow_0", record("shadow_0"))
425
+ .with_writes(&[PassOutput::Shadow(0)]));
426
+
427
+ let mut ctx = Vec::new();
428
+ graph.execute(&mut ctx).unwrap();
429
+ // Invariants — strict total order isn't guaranteed, but these
430
+ // data / explicit-hint dependencies must hold:
431
+ let pos = |name: &str| ctx.iter().position(|&n| n == name).unwrap();
432
+ assert!(pos("shadow_0") < pos("main_hdr"));
433
+ assert!(pos("main_hdr") < pos("ssao"));
434
+ assert!(pos("main_hdr") < pos("translucent"));
435
+ assert!(pos("main_hdr") < pos("bloom"));
436
+ assert!(pos("bloom") < pos("composite"));
437
+ assert!(pos("translucent") < pos("composite"));
438
+ assert!(pos("ssao") < pos("composite"));
439
+ assert_eq!(pos("composite"), ctx.len() - 1);
440
+ }
441
+
442
+ #[test]
443
+ fn run_fn_captures_mutable_state() {
444
+ // Proves the RunFn / context model supports shared mutable
445
+ // data between passes — the real engine use case is writing
446
+ // to the encoder or the profiler.
447
+ let counter = Arc::new(Mutex::new(0));
448
+ let c1 = counter.clone();
449
+ let c2 = counter.clone();
450
+
451
+ let mut graph: Graph<'_, TestCtx> = Graph::new();
452
+ graph.push(PassNode::new("a", Box::new(move |_ctx| {
453
+ *c1.lock().unwrap() += 1;
454
+ })));
455
+ graph.push(PassNode::new("b", Box::new(move |_ctx| {
456
+ *c2.lock().unwrap() += 10;
457
+ })));
458
+ let mut ctx = Vec::new();
459
+ graph.execute(&mut ctx).unwrap();
460
+ assert_eq!(*counter.lock().unwrap(), 11);
461
+ }
462
+ }