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