Skip to main content

astrelis_render/
framebuffer.rs

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