astrelis_render/
framebuffer.rs

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