Skip to main content

celestialsim/chunk_nodes/
mod.rs

1//! Chunk pipeline nodes as self-contained units behind the [`ChunkNode`] trait.
2//!
3//! Mirrors `nodes/mod.rs` for the chunk (grass/foliage) pipeline. Two nodes:
4//! `chunk-upload` (CPU → GPU descriptor + instance buffers) and `chunk-realize`
5//! (compute dispatch into the chunk multimesh). The graph is assembled from the
6//! data-driven registry in [`build_chunk_pipeline`].
7//!
8//! GPU bodies are stubs; Task 10 fleshes them out.
9
10use celestial_graph::{GraphNode, NodeDesc, NodeId, Graph};
11
12use crate::chunk_pipeline::ChunkCtx;
13
14mod upload;
15mod surface_custom;
16mod realize;
17mod bake;
18mod scatter_place;
19mod scatter_compact;
20
21pub use upload::ChunkUpload;
22pub use surface_custom::ChunkSurfaceCustom;
23pub use realize::ChunkRealize;
24pub use bake::ChunkBake;
25pub use scatter_place::ChunkScatterPlace;
26pub use scatter_compact::ChunkScatterCompact;
27
28/// A schedulable chunk operation. Knows its static name and how to record
29/// itself; resource reads/writes are owned by the registry, not the node.
30pub trait ChunkNode {
31    /// Static node name (used for GPU timestamps and logs).
32    fn name(&self) -> &'static str;
33    /// Hash of the node's own parameters; a change marks the node dirty even
34    /// if no upstream resource changed.
35    fn params_hash(&self) -> u64 {
36        0
37    }
38    /// Record this node's GPU work. Only called when the node is dirty.
39    fn record(&mut self, ctx: &mut ChunkCtx<'_>);
40}
41
42/// Bridge the trait objects to the `celestial-graph` executor.
43impl<'a> GraphNode<ChunkCtx<'a>> for Box<dyn ChunkNode> {
44    fn params_hash(&self) -> u64 {
45        (**self).params_hash()
46    }
47    fn record(&mut self, ctx: &mut ChunkCtx<'a>) {
48        (**self).record(ctx)
49    }
50}
51
52// Resource slots, allocated in this order by `build_chunk_pipeline`. Plain
53// indices so the wiring table is testable without a `Graph`/GPU.
54const R_CHUNK_DESC: usize = 0;
55const R_INSTANCES: usize = 1;
56const R_VERTS_TEX: usize = 2;
57const R_ATLAS: usize = 3;
58/// CEL-73 scatter: the visible `{slot, depth}` gather list (upload → compact).
59const R_SCATTER_VIS: usize = 4;
60/// CEL-73 scatter: the cached per-slot candidate pool (place → compact).
61const R_SCATTER_POOL: usize = 5;
62/// CEL-73 scatter: the external per-layer MultiMesh buffers compact writes.
63const R_SCATTER_OUT: usize = 6;
64/// Custom GPU surface: the per-slot `surface_color/height/normal` buffers the
65/// custom node writes and realize/bake read (ordering handle for the node).
66const R_SURFACE: usize = 7;
67const RESOURCE_COUNT: usize = 8;
68
69/// One row of the chunk pipeline registry: how to build a node plus the
70/// resource slots it reads/writes.
71struct NodeSpec {
72    make: fn() -> Box<dyn ChunkNode>,
73    reads: Vec<usize>,
74    writes: Vec<usize>,
75}
76
77/// Single source of truth for chunk pipeline topology.
78fn pipeline_specs() -> Vec<NodeSpec> {
79    vec![
80        NodeSpec {
81            make: || Box::new(ChunkUpload),
82            reads: vec![],
83            writes: vec![R_CHUNK_DESC, R_INSTANCES, R_SCATTER_VIS],
84        },
85        // Runs after upload (reads desc) and before realize/bake (writes the
86        // surface buffers they read). Earlier registry index => earlier in the
87        // stable topo order, same as bake sitting after realize.
88        NodeSpec {
89            make: || Box::new(ChunkSurfaceCustom),
90            reads: vec![R_CHUNK_DESC],
91            writes: vec![R_SURFACE],
92        },
93        NodeSpec {
94            make: || Box::new(ChunkRealize),
95            reads: vec![R_CHUNK_DESC, R_SURFACE],
96            writes: vec![R_VERTS_TEX],
97        },
98        NodeSpec {
99            make: || Box::new(ChunkBake),
100            reads: vec![R_CHUNK_DESC, R_SURFACE],
101            writes: vec![R_ATLAS],
102        },
103        NodeSpec {
104            make: || Box::new(ChunkScatterPlace),
105            // Reads the CPU-surface heightmap too: on the provider route the
106            // instances are displaced by the SAME baked elevations as realize.
107            reads: vec![R_CHUNK_DESC, R_SURFACE],
108            writes: vec![R_SCATTER_POOL],
109        },
110        NodeSpec {
111            make: || Box::new(ChunkScatterCompact),
112            reads: vec![R_SCATTER_POOL, R_SCATTER_VIS],
113            writes: vec![R_SCATTER_OUT],
114        },
115    ]
116}
117
118/// Node ids the job needs to address after the graph is built.
119pub struct ChunkRegistry {
120    /// Drives the CPU descriptor + instance upload; marked dirty each frame.
121    pub upload: NodeId,
122    /// Realize compute dispatch; marked dirty when upload runs.
123    pub realize: NodeId,
124}
125
126/// Build the chunk graph from the registry. Allocates resources, adds every
127/// node in registry order, and returns the ids the job addresses.
128pub fn build_chunk_pipeline(graph: &mut Graph<Box<dyn ChunkNode>>) -> ChunkRegistry {
129    let resources: Vec<_> = (0..RESOURCE_COUNT).map(|_| graph.add_resource()).collect();
130    let mut by_name: std::collections::HashMap<&'static str, NodeId> = Default::default();
131    for spec in pipeline_specs() {
132        let node = (spec.make)();
133        let name = node.name();
134        let id = graph.add_node(
135            NodeDesc {
136                name,
137                reads: spec.reads.iter().map(|&s| resources[s]).collect(),
138                writes: spec.writes.iter().map(|&s| resources[s]).collect(),
139            },
140            node,
141        );
142        by_name.insert(name, id);
143    }
144    ChunkRegistry {
145        upload: by_name["celestial/chunk-upload"],
146        realize: by_name["celestial/chunk-realize"],
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    /// Lock the chunk pipeline topology: node order/names and per-node
155    /// read/write resource slots. Pure data — no GPU.
156    #[test]
157    fn chunk_registry_topology_is_locked() {
158        let specs = pipeline_specs();
159        let by_name: std::collections::HashMap<&'static str, &NodeSpec> =
160            specs.iter().map(|s| ((s.make)().name(), s)).collect();
161        let names: Vec<&'static str> = specs.iter().map(|s| (s.make)().name()).collect();
162        assert_eq!(
163            names,
164            [
165                "celestial/chunk-upload",
166                "celestial/chunk-surface-custom",
167                "celestial/chunk-realize",
168                "celestial/chunk-bake",
169                "celestial/chunk-scatter-place",
170                "celestial/chunk-scatter-compact",
171            ]
172        );
173
174        let n = |name| by_name[name];
175        // chunk-upload: writes desc + instances + the scatter visible list.
176        assert_eq!(n("celestial/chunk-upload").reads, Vec::<usize>::new());
177        assert_eq!(
178            n("celestial/chunk-upload").writes,
179            vec![R_CHUNK_DESC, R_INSTANCES, R_SCATTER_VIS]
180        );
181        // chunk-surface-custom: reads desc, writes the surface buffers realize/
182        // bake read. Between upload and realize (earlier registry index).
183        assert_eq!(n("celestial/chunk-surface-custom").reads, vec![R_CHUNK_DESC]);
184        assert_eq!(n("celestial/chunk-surface-custom").writes, vec![R_SURFACE]);
185        // chunk-realize: reads desc + surface, writes verts/tex output.
186        assert_eq!(n("celestial/chunk-realize").reads, vec![R_CHUNK_DESC, R_SURFACE]);
187        assert_eq!(n("celestial/chunk-realize").writes, vec![R_VERTS_TEX]);
188        // chunk-bake: reads desc + surface, writes the colour/normal detail atlas.
189        // Runs after realize (later registry index => later in stable topo order).
190        assert_eq!(n("celestial/chunk-bake").reads, vec![R_CHUNK_DESC, R_SURFACE]);
191        assert_eq!(n("celestial/chunk-bake").writes, vec![R_ATLAS]);
192        // scatter-place (CEL-73): reads desc + the CPU-surface heightmap (so
193        // instances sit on the provider's terrain), writes the candidate pool.
194        assert_eq!(
195            n("celestial/chunk-scatter-place").reads,
196            vec![R_CHUNK_DESC, R_SURFACE]
197        );
198        assert_eq!(n("celestial/chunk-scatter-place").writes, vec![R_SCATTER_POOL]);
199        // scatter-compact: reads pool + visible list, writes the external
200        // multimesh buffers. Last in registry order => runs after place.
201        assert_eq!(
202            n("celestial/chunk-scatter-compact").reads,
203            vec![R_SCATTER_POOL, R_SCATTER_VIS]
204        );
205        assert_eq!(n("celestial/chunk-scatter-compact").writes, vec![R_SCATTER_OUT]);
206    }
207}