astrelis_render/
framebuffer.rs

1//! Framebuffer abstraction for offscreen rendering.
2
3use crate::context::GraphicsContext;
4
5/// Depth format used by framebuffers.
6pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
7
8/// An offscreen render target with optional depth and MSAA attachments.
9#[derive(Debug)]
10pub struct Framebuffer {
11    color_texture: wgpu::Texture,
12    color_view: wgpu::TextureView,
13    depth_texture: Option<wgpu::Texture>,
14    depth_view: Option<wgpu::TextureView>,
15    msaa_texture: Option<wgpu::Texture>,
16    msaa_view: Option<wgpu::TextureView>,
17    width: u32,
18    height: u32,
19    format: wgpu::TextureFormat,
20    sample_count: u32,
21}
22
23impl Framebuffer {
24    /// Create a new framebuffer builder.
25    pub fn builder(width: u32, height: u32) -> FramebufferBuilder {
26        FramebufferBuilder::new(width, height)
27    }
28
29    /// Get the color texture (resolved, non-MSAA).
30    pub fn color_texture(&self) -> &wgpu::Texture {
31        &self.color_texture
32    }
33
34    /// Get the color texture view (resolved, non-MSAA).
35    pub fn color_view(&self) -> &wgpu::TextureView {
36        &self.color_view
37    }
38
39    /// Get the depth texture, if present.
40    pub fn depth_texture(&self) -> Option<&wgpu::Texture> {
41        self.depth_texture.as_ref()
42    }
43
44    /// Get the depth texture view, if present.
45    pub fn depth_view(&self) -> Option<&wgpu::TextureView> {
46        self.depth_view.as_ref()
47    }
48
49    /// Get the MSAA texture (render target when MSAA enabled).
50    pub fn msaa_texture(&self) -> Option<&wgpu::Texture> {
51        self.msaa_texture.as_ref()
52    }
53
54    /// Get the MSAA texture view (render target when MSAA enabled).
55    pub fn msaa_view(&self) -> Option<&wgpu::TextureView> {
56        self.msaa_view.as_ref()
57    }
58
59    /// Get the view to render to (MSAA view if enabled, otherwise color view).
60    pub fn render_view(&self) -> &wgpu::TextureView {
61        self.msaa_view.as_ref().unwrap_or(&self.color_view)
62    }
63
64    /// Get the resolve target (color view if MSAA enabled, None otherwise).
65    pub fn resolve_target(&self) -> Option<&wgpu::TextureView> {
66        if self.msaa_view.is_some() {
67            Some(&self.color_view)
68        } else {
69            None
70        }
71    }
72
73    /// Get the framebuffer width.
74    pub fn width(&self) -> u32 {
75        self.width
76    }
77
78    /// Get the framebuffer height.
79    pub fn height(&self) -> u32 {
80        self.height
81    }
82
83    /// Get the framebuffer size as (width, height).
84    pub fn size(&self) -> (u32, u32) {
85        (self.width, self.height)
86    }
87
88    /// Get the color format.
89    pub fn format(&self) -> wgpu::TextureFormat {
90        self.format
91    }
92
93    /// Get the sample count (1 if no MSAA).
94    pub fn sample_count(&self) -> u32 {
95        self.sample_count
96    }
97
98    /// Check if MSAA is enabled.
99    pub fn has_msaa(&self) -> bool {
100        self.sample_count > 1
101    }
102
103    /// Check if depth buffer is enabled.
104    pub fn has_depth(&self) -> bool {
105        self.depth_texture.is_some()
106    }
107
108    /// Resize the framebuffer, recreating all textures.
109    pub fn resize(&mut self, context: &GraphicsContext, width: u32, height: u32) {
110        if self.width == width && self.height == height {
111            return;
112        }
113
114        let new_fb = FramebufferBuilder::new(width, height)
115            .format(self.format)
116            .sample_count_if(self.sample_count > 1, self.sample_count)
117            .depth_if(self.depth_texture.is_some())
118            .build(context);
119
120        *self = new_fb;
121    }
122}
123
124/// Builder for creating framebuffers with optional attachments.
125pub struct FramebufferBuilder {
126    width: u32,
127    height: u32,
128    format: wgpu::TextureFormat,
129    sample_count: u32,
130    with_depth: bool,
131    label: Option<&'static str>,
132}
133
134impl FramebufferBuilder {
135    /// Create a new framebuffer builder with the given dimensions.
136    pub fn new(width: u32, height: u32) -> Self {
137        Self {
138            width,
139            height,
140            format: wgpu::TextureFormat::Rgba8UnormSrgb,
141            sample_count: 1,
142            with_depth: false,
143            label: None,
144        }
145    }
146
147    /// Set the color format.
148    pub fn format(mut self, format: wgpu::TextureFormat) -> Self {
149        self.format = format;
150        self
151    }
152
153    /// Enable MSAA with the given sample count (typically 4).
154    pub fn with_msaa(mut self, sample_count: u32) -> Self {
155        self.sample_count = sample_count;
156        self
157    }
158
159    /// Conditionally set sample count.
160    pub fn sample_count_if(mut self, condition: bool, sample_count: u32) -> Self {
161        if condition {
162            self.sample_count = sample_count;
163        }
164        self
165    }
166
167    /// Enable depth buffer.
168    pub fn with_depth(mut self) -> Self {
169        self.with_depth = true;
170        self
171    }
172
173    /// Conditionally enable depth buffer.
174    pub fn depth_if(mut self, condition: bool) -> Self {
175        self.with_depth = condition;
176        self
177    }
178
179    /// Set a debug label for the framebuffer textures.
180    pub fn label(mut self, label: &'static str) -> Self {
181        self.label = Some(label);
182        self
183    }
184
185    /// Build the framebuffer.
186    pub fn build(self, context: &GraphicsContext) -> Framebuffer {
187        let label_prefix = self.label.unwrap_or("Framebuffer");
188
189        let size = wgpu::Extent3d {
190            width: self.width,
191            height: self.height,
192            depth_or_array_layers: 1,
193        };
194
195        // Create color texture (always sample_count=1, used as resolve target or direct render)
196        let color_texture = context.device.create_texture(&wgpu::TextureDescriptor {
197            label: Some(&format!("{} Color", label_prefix)),
198            size,
199            mip_level_count: 1,
200            sample_count: 1,
201            dimension: wgpu::TextureDimension::D2,
202            format: self.format,
203            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
204                | wgpu::TextureUsages::TEXTURE_BINDING
205                | wgpu::TextureUsages::COPY_SRC,
206            view_formats: &[],
207        });
208
209        let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
210
211        // Create MSAA texture if sample_count > 1
212        let (msaa_texture, msaa_view) = if self.sample_count > 1 {
213            let texture = context.device.create_texture(&wgpu::TextureDescriptor {
214                label: Some(&format!("{} MSAA", label_prefix)),
215                size,
216                mip_level_count: 1,
217                sample_count: self.sample_count,
218                dimension: wgpu::TextureDimension::D2,
219                format: self.format,
220                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
221                view_formats: &[],
222            });
223            let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
224            (Some(texture), Some(view))
225        } else {
226            (None, None)
227        };
228
229        // Create depth texture if requested
230        let (depth_texture, depth_view) = if self.with_depth {
231            let depth_sample_count = if self.sample_count > 1 {
232                self.sample_count
233            } else {
234                1
235            };
236
237            let texture = context.device.create_texture(&wgpu::TextureDescriptor {
238                label: Some(&format!("{} Depth", label_prefix)),
239                size,
240                mip_level_count: 1,
241                sample_count: depth_sample_count,
242                dimension: wgpu::TextureDimension::D2,
243                format: DEPTH_FORMAT,
244                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
245                    | wgpu::TextureUsages::TEXTURE_BINDING,
246                view_formats: &[],
247            });
248            let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
249            (Some(texture), Some(view))
250        } else {
251            (None, None)
252        };
253
254        Framebuffer {
255            color_texture,
256            color_view,
257            depth_texture,
258            depth_view,
259            msaa_texture,
260            msaa_view,
261            width: self.width,
262            height: self.height,
263            format: self.format,
264            sample_count: self.sample_count,
265        }
266    }
267}