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.as_ref().map(|m| m.view()).unwrap_or(self.color.view())
78    }
79
80    /// Get the resolve target (color view if MSAA enabled, None otherwise).
81    pub fn resolve_target(&self) -> Option<&wgpu::TextureView> {
82        if self.msaa.is_some() {
83            Some(self.color.view())
84        } else {
85            None
86        }
87    }
88
89    /// Get the framebuffer width.
90    pub fn width(&self) -> u32 {
91        self.color.width()
92    }
93
94    /// Get the framebuffer height.
95    pub fn height(&self) -> u32 {
96        self.color.height()
97    }
98
99    /// Get the framebuffer size as (width, height).
100    pub fn size(&self) -> (u32, u32) {
101        (self.color.width(), self.color.height())
102    }
103
104    /// Get the color format.
105    pub fn format(&self) -> wgpu::TextureFormat {
106        self.color.format()
107    }
108
109    /// Get the sample count (1 if no MSAA).
110    pub fn sample_count(&self) -> u32 {
111        self.sample_count
112    }
113
114    /// Check if MSAA is enabled.
115    pub fn has_msaa(&self) -> bool {
116        self.sample_count > 1
117    }
118
119    /// Check if depth buffer is enabled.
120    pub fn has_depth(&self) -> bool {
121        self.depth.is_some()
122    }
123
124    /// Resize the framebuffer, recreating all textures.
125    pub fn resize(&mut self, context: &GraphicsContext, width: u32, height: u32) {
126        if self.color.width() == width && self.color.height() == height {
127            return;
128        }
129
130        let new_fb = FramebufferBuilder::new(width, height)
131            .format(self.color.format())
132            .sample_count_if(self.sample_count > 1, self.sample_count)
133            .depth_if(self.depth.is_some())
134            .build(context);
135
136        *self = new_fb;
137    }
138}
139
140/// Builder for creating framebuffers with optional attachments.
141pub struct FramebufferBuilder {
142    width: u32,
143    height: u32,
144    format: wgpu::TextureFormat,
145    sample_count: u32,
146    with_depth: bool,
147    label: Option<&'static str>,
148}
149
150impl FramebufferBuilder {
151    /// Create a new framebuffer builder with the given dimensions.
152    pub fn new(width: u32, height: u32) -> Self {
153        Self {
154            width,
155            height,
156            format: wgpu::TextureFormat::Rgba8UnormSrgb,
157            sample_count: 1,
158            with_depth: false,
159            label: None,
160        }
161    }
162
163    /// Set the color format.
164    pub fn format(mut self, format: wgpu::TextureFormat) -> Self {
165        self.format = format;
166        self
167    }
168
169    /// Enable MSAA with the given sample count (typically 4).
170    pub fn with_msaa(mut self, sample_count: u32) -> Self {
171        self.sample_count = sample_count;
172        self
173    }
174
175    /// Conditionally set sample count.
176    pub fn sample_count_if(mut self, condition: bool, sample_count: u32) -> Self {
177        if condition {
178            self.sample_count = sample_count;
179        }
180        self
181    }
182
183    /// Enable depth buffer.
184    pub fn with_depth(mut self) -> Self {
185        self.with_depth = true;
186        self
187    }
188
189    /// Conditionally enable depth buffer.
190    pub fn depth_if(mut self, condition: bool) -> Self {
191        self.with_depth = condition;
192        self
193    }
194
195    /// Set a debug label for the framebuffer textures.
196    pub fn label(mut self, label: &'static str) -> Self {
197        self.label = Some(label);
198        self
199    }
200
201    /// Build the framebuffer.
202    pub fn build(self, context: &GraphicsContext) -> Framebuffer {
203        profile_function!();
204        let label_prefix = self.label.unwrap_or("Framebuffer");
205
206        let size = wgpu::Extent3d {
207            width: self.width,
208            height: self.height,
209            depth_or_array_layers: 1,
210        };
211
212        // Create color texture (always sample_count=1, used as resolve target or direct render)
213        let color = GpuTexture::new(
214            context.device(),
215            &wgpu::TextureDescriptor {
216                label: Some(&format!("{} Color", label_prefix)),
217                size,
218                mip_level_count: 1,
219                sample_count: 1,
220                dimension: wgpu::TextureDimension::D2,
221                format: self.format,
222                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
223                    | wgpu::TextureUsages::TEXTURE_BINDING
224                    | wgpu::TextureUsages::COPY_SRC,
225                view_formats: &[],
226            },
227        );
228
229        // Create MSAA texture if sample_count > 1
230        let msaa = if self.sample_count > 1 {
231            Some(GpuTexture::new(
232                context.device(),
233                &wgpu::TextureDescriptor {
234                    label: Some(&format!("{} MSAA", label_prefix)),
235                    size,
236                    mip_level_count: 1,
237                    sample_count: self.sample_count,
238                    dimension: wgpu::TextureDimension::D2,
239                    format: self.format,
240                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
241                    view_formats: &[],
242                },
243            ))
244        } else {
245            None
246        };
247
248        // Create depth texture if requested
249        let depth = if self.with_depth {
250            let depth_sample_count = if self.sample_count > 1 {
251                self.sample_count
252            } else {
253                1
254            };
255
256            Some(GpuTexture::new(
257                context.device(),
258                &wgpu::TextureDescriptor {
259                    label: Some(&format!("{} Depth", label_prefix)),
260                    size,
261                    mip_level_count: 1,
262                    sample_count: depth_sample_count,
263                    dimension: wgpu::TextureDimension::D2,
264                    format: DEPTH_FORMAT,
265                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
266                        | wgpu::TextureUsages::TEXTURE_BINDING,
267                    view_formats: &[],
268                },
269            ))
270        } else {
271            None
272        };
273
274        Framebuffer {
275            color,
276            depth,
277            msaa,
278            sample_count: self.sample_count,
279        }
280    }
281}