@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.
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/native/android/Cargo.lock +1848 -0
- package/native/android/Cargo.toml +24 -0
- package/native/android/src/lib.rs +702 -0
- package/native/ios/Cargo.lock +1690 -0
- package/native/ios/Cargo.toml +32 -0
- package/native/ios/src/lib.rs +1267 -0
- package/native/linux/Cargo.lock +3279 -0
- package/native/linux/Cargo.toml +29 -0
- package/native/linux/src/lib.rs +1331 -0
- package/native/macos/Cargo.lock +3310 -0
- package/native/macos/Cargo.toml +46 -0
- package/native/macos/src/lib.rs +1302 -0
- package/native/shared/Cargo.lock +1899 -0
- package/native/shared/Cargo.toml +62 -0
- package/native/shared/assets/default_font.ttf +0 -0
- package/native/shared/build.rs +270 -0
- package/native/shared/shaders/common/clouds.wgsl +122 -0
- package/native/shared/shaders/common/fog.wgsl +16 -0
- package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
- package/native/shared/shaders/common/imposter.wgsl +112 -0
- package/native/shared/shaders/common/pbr.wgsl +186 -0
- package/native/shared/shaders/common/shadows.wgsl +186 -0
- package/native/shared/shaders/common/sky.wgsl +8 -0
- package/native/shared/shaders/common/tonemap.wgsl +25 -0
- package/native/shared/shaders/impulse_field.wgsl +57 -0
- package/native/shared/shaders/material_abi.wgsl +383 -0
- package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
- package/native/shared/src/anim_mixer.rs +61 -0
- package/native/shared/src/attach.rs +263 -0
- package/native/shared/src/audio/decode.rs +123 -0
- package/native/shared/src/audio/mod.rs +863 -0
- package/native/shared/src/audio/render.rs +892 -0
- package/native/shared/src/audio/spsc.rs +156 -0
- package/native/shared/src/audio/stream.rs +226 -0
- package/native/shared/src/custom_shaders.rs +104 -0
- package/native/shared/src/decals.rs +245 -0
- package/native/shared/src/drs.rs +211 -0
- package/native/shared/src/engine.rs +261 -0
- package/native/shared/src/ffi.rs +116 -0
- package/native/shared/src/ffi_core/assets.rs +388 -0
- package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
- package/native/shared/src/ffi_core/draw.rs +334 -0
- package/native/shared/src/ffi_core/game_loop.rs +577 -0
- package/native/shared/src/ffi_core/input.rs +234 -0
- package/native/shared/src/ffi_core/mod.rs +127 -0
- package/native/shared/src/ffi_core/models.rs +1154 -0
- package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
- package/native/shared/src/ffi_core/scene.rs +626 -0
- package/native/shared/src/ffi_core/vfx.rs +212 -0
- package/native/shared/src/ffi_core/visual.rs +691 -0
- package/native/shared/src/frame_callbacks.rs +122 -0
- package/native/shared/src/geometry.rs +236 -0
- package/native/shared/src/handles.rs +182 -0
- package/native/shared/src/input.rs +448 -0
- package/native/shared/src/jolt_sys.rs +822 -0
- package/native/shared/src/lib.rs +55 -0
- package/native/shared/src/models.rs +1093 -0
- package/native/shared/src/models_gltf.rs +1280 -0
- package/native/shared/src/particles.rs +391 -0
- package/native/shared/src/physics_jolt.rs +1908 -0
- package/native/shared/src/picking.rs +298 -0
- package/native/shared/src/postfx.rs +345 -0
- package/native/shared/src/profiler.rs +492 -0
- package/native/shared/src/ragdoll.rs +474 -0
- package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
- package/native/shared/src/renderer/brdf_lut.rs +154 -0
- package/native/shared/src/renderer/draw2d.rs +143 -0
- package/native/shared/src/renderer/formats.rs +822 -0
- package/native/shared/src/renderer/froxel.rs +421 -0
- package/native/shared/src/renderer/gi_bake.rs +653 -0
- package/native/shared/src/renderer/graph.rs +462 -0
- package/native/shared/src/renderer/hiz.rs +269 -0
- package/native/shared/src/renderer/hot_reload.rs +390 -0
- package/native/shared/src/renderer/impulse_field.rs +456 -0
- package/native/shared/src/renderer/lighting.rs +154 -0
- package/native/shared/src/renderer/material_instancing.rs +171 -0
- package/native/shared/src/renderer/material_pipeline.rs +700 -0
- package/native/shared/src/renderer/material_system.rs +1996 -0
- package/native/shared/src/renderer/material_system_tests.rs +601 -0
- package/native/shared/src/renderer/material_system_wasm.rs +41 -0
- package/native/shared/src/renderer/mod.rs +12556 -0
- package/native/shared/src/renderer/model_draw.rs +641 -0
- package/native/shared/src/renderer/occlusion.rs +429 -0
- package/native/shared/src/renderer/planar_pass.rs +593 -0
- package/native/shared/src/renderer/planar_reflection.rs +499 -0
- package/native/shared/src/renderer/post_pass.rs +249 -0
- package/native/shared/src/renderer/postfx_chain.rs +728 -0
- package/native/shared/src/renderer/pt_pass.rs +577 -0
- package/native/shared/src/renderer/scene_pass.rs +607 -0
- package/native/shared/src/renderer/shader_include.rs +205 -0
- package/native/shared/src/renderer/shader_library.rs +135 -0
- package/native/shared/src/renderer/shaders/ao.rs +570 -0
- package/native/shared/src/renderer/shaders/core.rs +1243 -0
- package/native/shared/src/renderer/shaders/env.rs +907 -0
- package/native/shared/src/renderer/shaders/gi.rs +810 -0
- package/native/shared/src/renderer/shaders/mod.rs +19 -0
- package/native/shared/src/renderer/shaders/post.rs +1558 -0
- package/native/shared/src/renderer/shaders/pt.rs +1859 -0
- package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
- package/native/shared/src/renderer/shadow_pass.rs +731 -0
- package/native/shared/src/renderer/ssgi_pass.rs +392 -0
- package/native/shared/src/renderer/ssr_pass.rs +188 -0
- package/native/shared/src/renderer/texture_store.rs +473 -0
- package/native/shared/src/renderer/transient.rs +591 -0
- package/native/shared/src/renderer/types.rs +941 -0
- package/native/shared/src/renderer/util.rs +152 -0
- package/native/shared/src/scene.rs +1362 -0
- package/native/shared/src/sdf_cache.rs +274 -0
- package/native/shared/src/shadows.rs +1036 -0
- package/native/shared/src/staging.rs +102 -0
- package/native/shared/src/string_header.rs +266 -0
- package/native/shared/src/text_renderer.rs +502 -0
- package/native/shared/src/textures.rs +197 -0
- package/native/tvos/Cargo.lock +1693 -0
- package/native/tvos/Cargo.toml +36 -0
- package/native/tvos/metal-patched/Cargo.toml +178 -0
- package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
- package/native/tvos/metal-patched/LICENSE-MIT +25 -0
- package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
- package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
- package/native/tvos/metal-patched/src/argument.rs +366 -0
- package/native/tvos/metal-patched/src/blitpass.rs +102 -0
- package/native/tvos/metal-patched/src/buffer.rs +71 -0
- package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
- package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
- package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
- package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
- package/native/tvos/metal-patched/src/computepass.rs +107 -0
- package/native/tvos/metal-patched/src/constants.rs +152 -0
- package/native/tvos/metal-patched/src/counters.rs +119 -0
- package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
- package/native/tvos/metal-patched/src/device.rs +2134 -0
- package/native/tvos/metal-patched/src/drawable.rs +39 -0
- package/native/tvos/metal-patched/src/encoder.rs +2041 -0
- package/native/tvos/metal-patched/src/heap.rs +281 -0
- package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
- package/native/tvos/metal-patched/src/lib.rs +657 -0
- package/native/tvos/metal-patched/src/library.rs +902 -0
- package/native/tvos/metal-patched/src/mps.rs +575 -0
- package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
- package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
- package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
- package/native/tvos/metal-patched/src/renderpass.rs +443 -0
- package/native/tvos/metal-patched/src/resource.rs +182 -0
- package/native/tvos/metal-patched/src/sampler.rs +165 -0
- package/native/tvos/metal-patched/src/sync.rs +178 -0
- package/native/tvos/metal-patched/src/texture.rs +352 -0
- package/native/tvos/metal-patched/src/types.rs +90 -0
- package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
- package/native/tvos/src/audio_backend.rs +197 -0
- package/native/tvos/src/lib.rs +1891 -0
- package/native/visionos/Cargo.lock +1693 -0
- package/native/visionos/Cargo.toml +40 -0
- package/native/visionos/src/audio_backend.rs +197 -0
- package/native/visionos/src/lib.rs +1887 -0
- package/native/watchos/Cargo.lock +16 -0
- package/native/watchos/Cargo.toml +19 -0
- package/native/watchos/shaders/bloom_postfx.metal +99 -0
- package/native/watchos/src/BloomWatchApp.swift +1267 -0
- package/native/watchos/src/BloomWatchAudio.swift +179 -0
- package/native/watchos/src/audio.rs +55 -0
- package/native/watchos/src/draw_list.rs +229 -0
- package/native/watchos/src/ffi_stubs.rs +915 -0
- package/native/watchos/src/ffi_stubs_manual.rs +35 -0
- package/native/watchos/src/lib.rs +1124 -0
- package/native/watchos/src/models.rs +746 -0
- package/native/watchos/src/postfx.rs +95 -0
- package/native/watchos/src/scene.rs +534 -0
- package/native/watchos/src/textures.rs +184 -0
- package/native/web/Cargo.lock +1657 -0
- package/native/web/Cargo.toml +43 -0
- package/native/web/bloom_glue.js +695 -0
- package/native/web/build.sh +131 -0
- package/native/web/index.html +35 -0
- package/native/web/jolt_bridge.js +1519 -0
- package/native/web/src/input_ffi.rs +286 -0
- package/native/web/src/lib.rs +1796 -0
- package/native/web/src/material_ffi.rs +710 -0
- package/native/web/src/parity_ffi.rs +343 -0
- package/native/web/src/physics_ffi.rs +643 -0
- package/native/web/src/ragdoll_ffi.rs +250 -0
- package/native/web/src/render_settings.rs +98 -0
- package/native/windows/Cargo.lock +1815 -0
- package/native/windows/Cargo.toml +68 -0
- package/native/windows/src/lib.rs +1486 -0
- package/package.json +4279 -0
- package/src/audio/index.ts +315 -0
- package/src/core/colors.ts +63 -0
- package/src/core/index.ts +1206 -0
- package/src/core/keys.ts +63 -0
- package/src/core/types.ts +104 -0
- package/src/index.ts +171 -0
- package/src/math/index.ts +516 -0
- package/src/mobile/index.ts +294 -0
- package/src/models/index.ts +1258 -0
- package/src/physics/index.ts +1134 -0
- package/src/scene/index.ts +698 -0
- package/src/shapes/index.ts +120 -0
- package/src/text/index.ts +48 -0
- package/src/textures/index.ts +187 -0
- package/src/vfx/index.ts +191 -0
- package/src/world/index.ts +24 -0
- package/src/world/loader.ts +423 -0
- package/src/world/prefab.ts +217 -0
- package/src/world/render.ts +172 -0
- package/src/world/saver.ts +108 -0
- package/src/world/serialize.ts +301 -0
- package/src/world/terrain.ts +355 -0
- package/src/world/types.ts +160 -0
- package/src/world/validate.ts +319 -0
- 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
|
+
}
|