Skip to main content

oximedia_gpu/
render_pass.rs

1//! Render pass configuration and builder for `oximedia-gpu`.
2//!
3//! Provides load/store operation enums, per-attachment configuration, and a
4//! builder pattern for constructing `RenderPassConfig` descriptors.
5
6#![allow(dead_code)]
7
8/// Specifies what happens to an attachment's contents at the start of a pass.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum LoadOp {
11    /// Clear the attachment to a specified value.
12    Clear,
13    /// Load the existing contents.
14    Load,
15    /// Don't care about existing contents (undefined after load).
16    DontCare,
17}
18
19impl LoadOp {
20    /// Returns `true` when existing data will be preserved (Load).
21    #[must_use]
22    pub fn preserves_content(&self) -> bool {
23        matches!(self, Self::Load)
24    }
25
26    /// Returns a human-readable label.
27    #[must_use]
28    pub fn label(&self) -> &'static str {
29        match self {
30            Self::Clear => "clear",
31            Self::Load => "load",
32            Self::DontCare => "dont_care",
33        }
34    }
35}
36
37/// Specifies what happens to an attachment's contents at the end of a pass.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub enum StoreOp {
40    /// Store the results back to the attachment.
41    Store,
42    /// Discard the results (transient attachment).
43    Discard,
44    /// Don't care about the result.
45    DontCare,
46}
47
48impl StoreOp {
49    /// Returns `true` when the pass output will be written back.
50    #[must_use]
51    pub fn writes_output(&self) -> bool {
52        matches!(self, Self::Store)
53    }
54
55    /// Returns a human-readable label.
56    #[must_use]
57    pub fn label(&self) -> &'static str {
58        match self {
59            Self::Store => "store",
60            Self::Discard => "discard",
61            Self::DontCare => "dont_care",
62        }
63    }
64}
65
66/// Pixel format of a render attachment.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub enum AttachmentFormat {
69    /// 8-bit per channel RGBA.
70    Rgba8Unorm,
71    /// 16-bit per channel RGBA (HDR).
72    Rgba16Float,
73    /// 32-bit per channel RGBA (HDR).
74    Rgba32Float,
75    /// 32-bit depth, 8-bit stencil.
76    Depth32FloatStencil8,
77    /// 32-bit float depth only.
78    Depth32Float,
79    /// 24-bit depth, 8-bit stencil.
80    Depth24PlusStencil8,
81}
82
83impl AttachmentFormat {
84    /// Returns `true` when this format contains a depth component.
85    #[must_use]
86    pub fn has_depth(&self) -> bool {
87        matches!(
88            self,
89            Self::Depth32FloatStencil8 | Self::Depth32Float | Self::Depth24PlusStencil8
90        )
91    }
92
93    /// Returns `true` when this format contains a stencil component.
94    #[must_use]
95    pub fn has_stencil(&self) -> bool {
96        matches!(self, Self::Depth32FloatStencil8 | Self::Depth24PlusStencil8)
97    }
98
99    /// Returns the number of bytes per texel.
100    #[must_use]
101    pub fn bytes_per_texel(&self) -> u32 {
102        match self {
103            Self::Rgba8Unorm => 4,
104            Self::Rgba16Float => 8,
105            Self::Rgba32Float => 16,
106            Self::Depth32FloatStencil8 => 5,
107            Self::Depth32Float => 4,
108            Self::Depth24PlusStencil8 => 4,
109        }
110    }
111}
112
113/// Configuration for a single attachment within a render pass.
114#[derive(Debug, Clone)]
115pub struct AttachmentConfig {
116    /// Pixel format.
117    pub format: AttachmentFormat,
118    /// Operation at the start of the pass.
119    pub load_op: LoadOp,
120    /// Operation at the end of the pass.
121    pub store_op: StoreOp,
122    /// Optional MSAA sample count (1 = no MSAA).
123    pub sample_count: u32,
124    /// Debug label.
125    pub label: Option<String>,
126}
127
128impl AttachmentConfig {
129    /// Creates an attachment configuration.
130    #[must_use]
131    pub fn new(format: AttachmentFormat, load_op: LoadOp, store_op: StoreOp) -> Self {
132        Self {
133            format,
134            load_op,
135            store_op,
136            sample_count: 1,
137            label: None,
138        }
139    }
140
141    /// Returns `true` when this attachment is a depth (or depth/stencil) attachment.
142    #[must_use]
143    pub fn has_depth(&self) -> bool {
144        self.format.has_depth()
145    }
146
147    /// Returns `true` when MSAA is enabled (sample count > 1).
148    #[must_use]
149    pub fn is_multisampled(&self) -> bool {
150        self.sample_count > 1
151    }
152
153    /// Sets the MSAA sample count.
154    #[must_use]
155    pub fn with_sample_count(mut self, count: u32) -> Self {
156        self.sample_count = count;
157        self
158    }
159
160    /// Attaches a debug label.
161    #[must_use]
162    pub fn with_label(mut self, label: impl Into<String>) -> Self {
163        self.label = Some(label.into());
164        self
165    }
166}
167
168/// Describes all attachments used in a single render pass.
169#[derive(Debug, Clone)]
170pub struct RenderPassConfig {
171    /// Color attachments (up to 8 on most hardware).
172    pub color_attachments: Vec<AttachmentConfig>,
173    /// Optional depth/stencil attachment.
174    pub depth_attachment: Option<AttachmentConfig>,
175    /// Debug label for the render pass.
176    pub label: Option<String>,
177}
178
179impl RenderPassConfig {
180    /// Returns the total number of attachments (color + optional depth).
181    #[must_use]
182    pub fn attachment_count(&self) -> usize {
183        self.color_attachments.len() + usize::from(self.depth_attachment.is_some())
184    }
185
186    /// Returns `true` when a depth attachment is present.
187    #[must_use]
188    pub fn has_depth_attachment(&self) -> bool {
189        self.depth_attachment.is_some()
190    }
191
192    /// Returns `true` when all color attachments use the same format.
193    #[must_use]
194    pub fn has_uniform_color_format(&self) -> bool {
195        let mut iter = self.color_attachments.iter().map(|a| a.format);
196        match iter.next() {
197            None => true,
198            Some(first) => iter.all(|f| f == first),
199        }
200    }
201}
202
203/// Fluent builder for [`RenderPassConfig`].
204#[derive(Debug, Default)]
205pub struct RenderPassBuilder {
206    color_attachments: Vec<AttachmentConfig>,
207    depth_attachment: Option<AttachmentConfig>,
208    label: Option<String>,
209}
210
211impl RenderPassBuilder {
212    /// Creates a new builder.
213    #[must_use]
214    pub fn new() -> Self {
215        Self::default()
216    }
217
218    /// Appends a color attachment.
219    #[must_use]
220    pub fn add_color_attachment(mut self, attachment: AttachmentConfig) -> Self {
221        self.color_attachments.push(attachment);
222        self
223    }
224
225    /// Sets the depth/stencil attachment.
226    ///
227    /// Returns an error string if the attachment format does not contain depth.
228    pub fn set_depth_attachment(mut self, attachment: AttachmentConfig) -> Result<Self, String> {
229        if !attachment.has_depth() {
230            return Err(format!(
231                "Format {:?} does not contain a depth component",
232                attachment.format
233            ));
234        }
235        self.depth_attachment = Some(attachment);
236        Ok(self)
237    }
238
239    /// Sets a debug label for the render pass.
240    #[must_use]
241    pub fn with_label(mut self, label: impl Into<String>) -> Self {
242        self.label = Some(label.into());
243        self
244    }
245
246    /// Consumes the builder and returns the [`RenderPassConfig`].
247    ///
248    /// Returns an error if no color attachments have been added.
249    pub fn build(self) -> Result<RenderPassConfig, String> {
250        if self.color_attachments.is_empty() {
251            return Err("RenderPassConfig requires at least one color attachment".into());
252        }
253        Ok(RenderPassConfig {
254            color_attachments: self.color_attachments,
255            depth_attachment: self.depth_attachment,
256            label: self.label,
257        })
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_load_op_preserves_content_load() {
267        assert!(LoadOp::Load.preserves_content());
268    }
269
270    #[test]
271    fn test_load_op_clear_does_not_preserve() {
272        assert!(!LoadOp::Clear.preserves_content());
273    }
274
275    #[test]
276    fn test_store_op_writes_output_store() {
277        assert!(StoreOp::Store.writes_output());
278    }
279
280    #[test]
281    fn test_store_op_discard_no_write() {
282        assert!(!StoreOp::Discard.writes_output());
283    }
284
285    #[test]
286    fn test_attachment_format_has_depth_depth32() {
287        assert!(AttachmentFormat::Depth32Float.has_depth());
288    }
289
290    #[test]
291    fn test_attachment_format_rgba8_no_depth() {
292        assert!(!AttachmentFormat::Rgba8Unorm.has_depth());
293    }
294
295    #[test]
296    fn test_attachment_format_has_stencil_depth24() {
297        assert!(AttachmentFormat::Depth24PlusStencil8.has_stencil());
298    }
299
300    #[test]
301    fn test_attachment_format_bytes_per_texel_rgba32() {
302        assert_eq!(AttachmentFormat::Rgba32Float.bytes_per_texel(), 16);
303    }
304
305    #[test]
306    fn test_attachment_config_has_depth_true() {
307        let a = AttachmentConfig::new(
308            AttachmentFormat::Depth32Float,
309            LoadOp::Clear,
310            StoreOp::Store,
311        );
312        assert!(a.has_depth());
313    }
314
315    #[test]
316    fn test_attachment_config_not_multisampled_by_default() {
317        let a = AttachmentConfig::new(AttachmentFormat::Rgba8Unorm, LoadOp::Clear, StoreOp::Store);
318        assert!(!a.is_multisampled());
319    }
320
321    #[test]
322    fn test_attachment_config_with_sample_count() {
323        let a = AttachmentConfig::new(AttachmentFormat::Rgba8Unorm, LoadOp::Clear, StoreOp::Store)
324            .with_sample_count(4);
325        assert!(a.is_multisampled());
326        assert_eq!(a.sample_count, 4);
327    }
328
329    #[test]
330    fn test_render_pass_builder_build_ok() {
331        let color =
332            AttachmentConfig::new(AttachmentFormat::Rgba8Unorm, LoadOp::Clear, StoreOp::Store);
333        let config = RenderPassBuilder::new()
334            .add_color_attachment(color)
335            .build()
336            .expect("operation should succeed in test");
337        assert_eq!(config.attachment_count(), 1);
338    }
339
340    #[test]
341    fn test_render_pass_builder_build_no_color_err() {
342        let result = RenderPassBuilder::new().build();
343        assert!(result.is_err());
344    }
345
346    #[test]
347    fn test_render_pass_builder_with_depth() {
348        let color =
349            AttachmentConfig::new(AttachmentFormat::Rgba8Unorm, LoadOp::Clear, StoreOp::Store);
350        let depth = AttachmentConfig::new(
351            AttachmentFormat::Depth32Float,
352            LoadOp::Clear,
353            StoreOp::Discard,
354        );
355        let config = RenderPassBuilder::new()
356            .add_color_attachment(color)
357            .set_depth_attachment(depth)
358            .expect("operation should succeed in test")
359            .build()
360            .expect("operation should succeed in test");
361        assert!(config.has_depth_attachment());
362        assert_eq!(config.attachment_count(), 2);
363    }
364
365    #[test]
366    fn test_render_pass_builder_depth_non_depth_format_err() {
367        let bad =
368            AttachmentConfig::new(AttachmentFormat::Rgba8Unorm, LoadOp::Clear, StoreOp::Store);
369        let result = RenderPassBuilder::new().set_depth_attachment(bad);
370        assert!(result.is_err());
371    }
372
373    #[test]
374    fn test_render_pass_uniform_color_format_true_single() {
375        let color =
376            AttachmentConfig::new(AttachmentFormat::Rgba8Unorm, LoadOp::Clear, StoreOp::Store);
377        let config = RenderPassBuilder::new()
378            .add_color_attachment(color)
379            .build()
380            .expect("operation should succeed in test");
381        assert!(config.has_uniform_color_format());
382    }
383}