Skip to main content

pebble/
macros.rs

1/// Unwraps an `Option`, returning from the enclosing function if it's
2/// `None` — collapses the `let Some(x) = expr else { return };` pattern
3/// that shows up constantly in systems (which return `()`, so the `?`
4/// operator isn't an option the way it would be in a function returning
5/// `Option`/`Result`).
6///
7/// ```rust,ignore
8/// let material = or_return!(materials.get(handle));
9/// let material = or_return!(materials.get(handle), return None); // custom return value
10/// ```
11#[macro_export]
12macro_rules! or_return {
13    ($expr:expr) => {
14        match $expr {
15            ::core::option::Option::Some(value) => value,
16            ::core::option::Option::None => return,
17        }
18    };
19    ($expr:expr, $ret:expr) => {
20        match $expr {
21            ::core::option::Option::Some(value) => value,
22            ::core::option::Option::None => return $ret,
23        }
24    };
25}
26
27/// The "material instance → material" chain every draw call repeats: look up
28/// `$instances.get($handle)`, then its material via the instance's own
29/// `target` handle, then `set_pipeline` + `set_bind_group(0, ...)` on
30/// `$pass`. Returns from the enclosing function (via [`or_return!`]) if
31/// either lookup isn't ready yet — an asset that hasn't finished uploading
32/// is a normal, common case (a couple frames on load), not a bug.
33///
34/// Evaluates to the looked-up `&GPUMaterialInstance`, so callers that also
35/// need `.update(name, data)` on it (e.g. a per-frame camera uniform) can
36/// still bind that:
37///
38/// ```rust,ignore
39/// let instance = bind_mat!(render_pass, materials, instances, instance_handle);
40/// ```
41#[macro_export]
42macro_rules! bind_mat {
43    ($pass:expr, $materials:expr, $instances:expr, $handle:expr) => {{
44        let instance = $crate::or_return!($instances.get($handle));
45        let material = $crate::or_return!($materials.get(instance.target));
46        $pass.set_pipeline(&material.pipeline);
47        $pass.set_bind_group(0, &instance.bind_group, &[]);
48        instance
49    }};
50}
51
52/// Same as [`bind_mat!`], for a `ComputePass` + `Compute`/`ComputeInstance`
53/// instead of a `RenderPass` + `Material`/`MaterialInstance`.
54///
55/// ```rust,ignore
56/// let instance = bind_comp!(compute_pass, computes, instances, instance_handle);
57/// ```
58#[macro_export]
59macro_rules! bind_comp {
60    ($pass:expr, $computes:expr, $instances:expr, $handle:expr) => {{
61        let instance = $crate::or_return!($instances.get($handle));
62        let compute = $crate::or_return!($computes.get(instance.target));
63        $pass.set_pipeline(&compute.pipeline);
64        $pass.set_bind_group(0, &instance.bind_group, &[]);
65        instance
66    }};
67}
68
69/// Looks up `$meshes.get($handle)` and draws it — sets the vertex/index
70/// buffers and calls `draw_indexed`, defaulting the instance range to `0..1`
71/// (pass a fourth argument for instanced draws). Returns from the enclosing
72/// function (via [`or_return!`]) if the mesh isn't ready yet.
73///
74/// ```rust,ignore
75/// draw_mesh!(render_pass, meshes, mesh_handle);
76/// draw_mesh!(render_pass, meshes, mesh_handle, 0..enemy_count);
77/// ```
78#[macro_export]
79macro_rules! draw_mesh {
80    ($pass:expr, $meshes:expr, $handle:expr) => {
81        $crate::draw_mesh!($pass, $meshes, $handle, 0..1)
82    };
83    ($pass:expr, $meshes:expr, $handle:expr, $instances:expr) => {{
84        let mesh = $crate::or_return!($meshes.get($handle));
85        $pass.set_vertex_buffer(0, &mesh.vertex_buffer);
86        $pass.set_index_buffer(&mesh.index_buffer, $crate::graphics::types::IndexFormat::Uint32);
87        $pass.draw_indexed(0..mesh.index_count, 0, $instances);
88    }};
89}
90
91#[cfg(test)]
92mod tests {
93    use crate::graphics::types::IndexFormat;
94
95    struct Assets<T>(Option<T>);
96
97    impl<T> Assets<T> {
98        fn get(&self, _handle: u32) -> Option<&T> {
99            self.0.as_ref()
100        }
101    }
102
103    struct GPUMaterial {
104        pipeline: &'static str,
105    }
106
107    struct GPUMaterialInstance {
108        target: u32,
109        bind_group: &'static str,
110    }
111
112    #[derive(Default)]
113    struct RecordingPass {
114        pipeline: Option<&'static str>,
115        bind_group: Option<&'static str>,
116        vertex_buffer: Option<&'static str>,
117        index_buffer: Option<(&'static str, IndexFormat)>,
118        drawn: Option<(std::ops::Range<u32>, i32, std::ops::Range<u32>)>,
119    }
120
121    impl RecordingPass {
122        fn set_pipeline(&mut self, pipeline: &&'static str) {
123            self.pipeline = Some(pipeline);
124        }
125
126        fn set_bind_group(&mut self, _index: u32, bind_group: &&'static str, _offsets: &[u32]) {
127            self.bind_group = Some(bind_group);
128        }
129
130        fn set_vertex_buffer(&mut self, _slot: u32, buffer: &&'static str) {
131            self.vertex_buffer = Some(buffer);
132        }
133
134        fn set_index_buffer(&mut self, buffer: &&'static str, format: IndexFormat) {
135            self.index_buffer = Some((buffer, format));
136        }
137
138        fn draw_indexed(&mut self, indices: std::ops::Range<u32>, base_vertex: i32, instances: std::ops::Range<u32>) {
139            self.drawn = Some((indices, base_vertex, instances));
140        }
141    }
142
143    fn bind_missing(pass: &mut RecordingPass) {
144        let materials: Assets<GPUMaterial> = Assets(None);
145        let instances: Assets<GPUMaterialInstance> = Assets(None);
146        bind_mat!(pass, materials, instances, 0u32);
147    }
148
149    #[test]
150    fn bind_mat_returns_early_when_instance_missing() {
151        let mut pass = RecordingPass::default();
152        bind_missing(&mut pass);
153        assert!(pass.pipeline.is_none());
154        assert!(pass.bind_group.is_none());
155    }
156
157    fn bind_present(pass: &mut RecordingPass) {
158        let materials = Assets(Some(GPUMaterial { pipeline: "pipeline" }));
159        let instances = Assets(Some(GPUMaterialInstance { target: 0, bind_group: "bind_group" }));
160        let instance = bind_mat!(pass, materials, instances, 0u32);
161        assert_eq!(instance.bind_group, "bind_group");
162    }
163
164    #[test]
165    fn bind_mat_sets_pipeline_and_bind_group() {
166        let mut pass = RecordingPass::default();
167        bind_present(&mut pass);
168        assert_eq!(pass.pipeline, Some("pipeline"));
169        assert_eq!(pass.bind_group, Some("bind_group"));
170    }
171
172    struct GPUMesh {
173        vertex_buffer: &'static str,
174        index_buffer: &'static str,
175        index_count: u32,
176    }
177
178    #[test]
179    fn draw_mesh_defaults_to_a_single_instance() {
180        let mut pass = RecordingPass::default();
181        let meshes = Assets(Some(GPUMesh { vertex_buffer: "vbo", index_buffer: "ibo", index_count: 6 }));
182        draw_mesh!(pass, meshes, 0u32);
183
184        assert_eq!(pass.vertex_buffer, Some("vbo"));
185        assert_eq!(pass.index_buffer, Some(("ibo", IndexFormat::Uint32)));
186        assert_eq!(pass.drawn, Some((0..6, 0, 0..1)));
187    }
188
189    #[test]
190    fn draw_mesh_accepts_an_explicit_instance_range() {
191        let mut pass = RecordingPass::default();
192        let meshes = Assets(Some(GPUMesh { vertex_buffer: "vbo", index_buffer: "ibo", index_count: 6 }));
193        draw_mesh!(pass, meshes, 0u32, 0..12);
194
195        assert_eq!(pass.drawn, Some((0..6, 0, 0..12)));
196    }
197
198    #[test]
199    fn draw_mesh_returns_early_when_missing() {
200        let mut pass = RecordingPass::default();
201        let meshes: Assets<GPUMesh> = Assets(None);
202        draw_mesh!(pass, meshes, 0u32);
203
204        assert!(pass.drawn.is_none());
205    }
206}