astrelis_render/
frame.rs

1use std::sync::Arc;
2
3use astrelis_core::profiling::{profile_function, profile_scope};
4use astrelis_winit::window::WinitWindow;
5
6use crate::context::GraphicsContext;
7use crate::target::RenderTarget;
8
9/// Statistics for a rendered frame.
10pub struct FrameStats {
11    pub passes: usize,
12    pub draw_calls: usize,
13}
14
15impl FrameStats {
16    pub(crate) fn new() -> Self {
17        Self {
18            passes: 0,
19            draw_calls: 0,
20        }
21    }
22}
23
24/// Surface texture and view for rendering.
25pub struct Surface {
26    pub(crate) texture: wgpu::SurfaceTexture,
27    pub(crate) view: wgpu::TextureView,
28}
29
30impl Surface {
31    pub fn texture(&self) -> &wgpu::Texture {
32        &self.texture.texture
33    }
34
35    pub fn view(&self) -> &wgpu::TextureView {
36        &self.view
37    }
38}
39
40/// Context for a single frame of rendering.
41pub struct FrameContext {
42    pub(crate) stats: FrameStats,
43    pub(crate) surface: Option<Surface>,
44    pub(crate) encoder: Option<wgpu::CommandEncoder>,
45    pub(crate) context: &'static GraphicsContext,
46    pub(crate) window: Arc<WinitWindow>,
47    pub(crate) surface_format: wgpu::TextureFormat,
48}
49
50impl FrameContext {
51    pub fn surface(&self) -> &Surface {
52        self.surface.as_ref().unwrap()
53    }
54
55    pub fn surface_format(&self) -> wgpu::TextureFormat {
56        self.surface_format
57    }
58
59    pub fn increment_passes(&mut self) {
60        self.stats.passes += 1;
61    }
62
63    pub fn increment_draw_calls(&mut self) {
64        self.stats.draw_calls += 1;
65    }
66
67    pub fn stats(&self) -> &FrameStats {
68        &self.stats
69    }
70
71    pub fn graphics_context(&self) -> &'static GraphicsContext {
72        self.context
73    }
74
75    pub fn encoder(&mut self) -> &mut wgpu::CommandEncoder {
76        self.encoder.as_mut().expect("Encoder already taken")
77    }
78
79    pub fn encoder_and_surface(&mut self) -> (&mut wgpu::CommandEncoder, &Surface) {
80        (
81            self.encoder.as_mut().expect("Encoder already taken"),
82            self.surface.as_ref().unwrap(),
83        )
84    }
85
86    pub fn finish(self) {
87        drop(self);
88    }
89}
90
91impl Drop for FrameContext {
92    fn drop(&mut self) {
93        profile_function!();
94
95        if self.stats.passes == 0 {
96            tracing::error!("No render passes were executed for this frame!");
97            return;
98        }
99
100        if let Some(encoder) = self.encoder.take() {
101            profile_scope!("submit_commands");
102            self.context.queue.submit(std::iter::once(encoder.finish()));
103        }
104
105        if let Some(surface) = self.surface.take() {
106            profile_scope!("present_surface");
107            surface.texture.present();
108        }
109
110        // Request redraw for next frame
111        self.window.request_redraw();
112    }
113}
114
115/// Clear operation for a render pass.
116#[derive(Debug, Clone, Copy)]
117pub enum ClearOp {
118    /// Load existing contents (no clear).
119    Load,
120    /// Clear to the specified color.
121    Clear(wgpu::Color),
122}
123
124impl Default for ClearOp {
125    fn default() -> Self {
126        ClearOp::Load
127    }
128}
129
130impl From<wgpu::Color> for ClearOp {
131    fn from(color: wgpu::Color) -> Self {
132        ClearOp::Clear(color)
133    }
134}
135
136impl From<crate::Color> for ClearOp {
137    fn from(color: crate::Color) -> Self {
138        ClearOp::Clear(color.to_wgpu())
139    }
140}
141
142/// Depth clear operation for a render pass.
143#[derive(Debug, Clone, Copy)]
144pub enum DepthClearOp {
145    /// Load existing depth values.
146    Load,
147    /// Clear to the specified depth value (typically 1.0).
148    Clear(f32),
149}
150
151impl Default for DepthClearOp {
152    fn default() -> Self {
153        DepthClearOp::Clear(1.0)
154    }
155}
156
157/// Builder for creating render passes.
158pub struct RenderPassBuilder<'a> {
159    label: Option<&'a str>,
160    // New simplified API
161    target: Option<RenderTarget<'a>>,
162    clear_op: ClearOp,
163    depth_clear_op: DepthClearOp,
164    // Legacy API for advanced use
165    color_attachments: Vec<Option<wgpu::RenderPassColorAttachment<'a>>>,
166    surface_attachment_ops: Option<(wgpu::Operations<wgpu::Color>, Option<&'a wgpu::TextureView>)>,
167    depth_stencil_attachment: Option<wgpu::RenderPassDepthStencilAttachment<'a>>,
168}
169
170impl<'a> RenderPassBuilder<'a> {
171    pub fn new() -> Self {
172        Self {
173            label: None,
174            target: None,
175            clear_op: ClearOp::Load,
176            depth_clear_op: DepthClearOp::default(),
177            color_attachments: Vec::new(),
178            surface_attachment_ops: None,
179            depth_stencil_attachment: None,
180        }
181    }
182
183    /// Set a debug label for the render pass.
184    pub fn label(mut self, label: &'a str) -> Self {
185        self.label = Some(label);
186        self
187    }
188
189    /// Set the render target (Surface or Framebuffer).
190    ///
191    /// This is the simplified API - use this instead of manual color_attachment calls.
192    pub fn target(mut self, target: RenderTarget<'a>) -> Self {
193        self.target = Some(target);
194        self
195    }
196
197    /// Set clear color for the render target.
198    ///
199    /// Pass a wgpu::Color or use ClearOp::Load to preserve existing contents.
200    pub fn clear_color(mut self, color: impl Into<ClearOp>) -> Self {
201        self.clear_op = color.into();
202        self
203    }
204
205    /// Set depth clear operation.
206    pub fn clear_depth(mut self, depth: f32) -> Self {
207        self.depth_clear_op = DepthClearOp::Clear(depth);
208        self
209    }
210
211    /// Load existing depth values instead of clearing.
212    pub fn load_depth(mut self) -> Self {
213        self.depth_clear_op = DepthClearOp::Load;
214        self
215    }
216
217    // Legacy API for advanced use cases
218
219    /// Add a color attachment manually (advanced API).
220    ///
221    /// For most cases, use `.target()` instead.
222    pub fn color_attachment(
223        mut self,
224        view: Option<&'a wgpu::TextureView>,
225        resolve_target: Option<&'a wgpu::TextureView>,
226        ops: wgpu::Operations<wgpu::Color>,
227    ) -> Self {
228        if let Some(view) = view {
229            self.color_attachments
230                .push(Some(wgpu::RenderPassColorAttachment {
231                    view,
232                    resolve_target,
233                    ops,
234                    depth_slice: None,
235                }));
236        } else {
237            // Store ops for later - will be filled with surface view in build()
238            self.surface_attachment_ops = Some((ops, resolve_target));
239        }
240        self
241    }
242
243    /// Add a depth-stencil attachment manually (advanced API).
244    ///
245    /// For framebuffers with depth, the depth attachment is handled automatically
246    /// when using `.target()`.
247    pub fn depth_stencil_attachment(
248        mut self,
249        view: &'a wgpu::TextureView,
250        depth_ops: Option<wgpu::Operations<f32>>,
251        stencil_ops: Option<wgpu::Operations<u32>>,
252    ) -> Self {
253        self.depth_stencil_attachment = Some(wgpu::RenderPassDepthStencilAttachment {
254            view,
255            depth_ops,
256            stencil_ops,
257        });
258        self
259    }
260
261    /// Builds the render pass and begins it on the provided frame context.
262    ///
263    /// This takes ownership of the CommandEncoder from the FrameContext, and releases it
264    /// back to the FrameContext when the RenderPass is dropped or [`finish`](RenderPass::finish)
265    /// is called.
266    pub fn build(self, frame_context: &'a mut FrameContext) -> RenderPass<'a> {
267        let mut encoder = frame_context.encoder.take().unwrap();
268
269        // Build color attachments based on target or legacy API
270        let mut all_attachments = Vec::new();
271
272        if let Some(target) = &self.target {
273            // New simplified API
274            let color_ops = match self.clear_op {
275                ClearOp::Load => wgpu::Operations {
276                    load: wgpu::LoadOp::Load,
277                    store: wgpu::StoreOp::Store,
278                },
279                ClearOp::Clear(color) => wgpu::Operations {
280                    load: wgpu::LoadOp::Clear(color),
281                    store: wgpu::StoreOp::Store,
282                },
283            };
284
285            match target {
286                RenderTarget::Surface => {
287                    let surface_view = frame_context.surface().view();
288                    all_attachments.push(Some(wgpu::RenderPassColorAttachment {
289                        view: surface_view,
290                        resolve_target: None,
291                        ops: color_ops,
292                        depth_slice: None,
293                    }));
294                }
295                RenderTarget::Framebuffer(fb) => {
296                    all_attachments.push(Some(wgpu::RenderPassColorAttachment {
297                        view: fb.render_view(),
298                        resolve_target: fb.resolve_target(),
299                        ops: color_ops,
300                        depth_slice: None,
301                    }));
302                }
303            }
304        } else {
305            // Legacy API
306            if let Some((ops, resolve_target)) = self.surface_attachment_ops {
307                let surface_view = frame_context.surface().view();
308                all_attachments.push(Some(wgpu::RenderPassColorAttachment {
309                    view: surface_view,
310                    resolve_target,
311                    ops,
312                    depth_slice: None,
313                }));
314            }
315            all_attachments.extend(self.color_attachments);
316        }
317
318        // Build depth attachment
319        let depth_attachment = if let Some(attachment) = self.depth_stencil_attachment {
320            Some(attachment)
321        } else if let Some(RenderTarget::Framebuffer(fb)) = &self.target {
322            fb.depth_view().map(|view| {
323                let depth_ops = match self.depth_clear_op {
324                    DepthClearOp::Load => wgpu::Operations {
325                        load: wgpu::LoadOp::Load,
326                        store: wgpu::StoreOp::Store,
327                    },
328                    DepthClearOp::Clear(depth) => wgpu::Operations {
329                        load: wgpu::LoadOp::Clear(depth),
330                        store: wgpu::StoreOp::Store,
331                    },
332                };
333                wgpu::RenderPassDepthStencilAttachment {
334                    view,
335                    depth_ops: Some(depth_ops),
336                    stencil_ops: None,
337                }
338            })
339        } else {
340            None
341        };
342
343        let descriptor = wgpu::RenderPassDescriptor {
344            label: self.label,
345            color_attachments: &all_attachments,
346            depth_stencil_attachment: depth_attachment,
347            occlusion_query_set: None,
348            timestamp_writes: None,
349        };
350
351        let render_pass = encoder.begin_render_pass(&descriptor).forget_lifetime();
352
353        frame_context.increment_passes();
354
355        RenderPass {
356            context: frame_context,
357            encoder: Some(encoder),
358            descriptor: Some(render_pass),
359        }
360    }
361}
362
363impl Default for RenderPassBuilder<'_> {
364    fn default() -> Self {
365        Self::new()
366    }
367}
368
369/// A render pass wrapper that automatically returns the encoder to the frame context.
370pub struct RenderPass<'a> {
371    pub context: &'a mut FrameContext,
372    pub(crate) encoder: Option<wgpu::CommandEncoder>,
373    pub(crate) descriptor: Option<wgpu::RenderPass<'static>>,
374}
375
376impl<'a> RenderPass<'a> {
377    pub fn descriptor(&mut self) -> &mut wgpu::RenderPass<'static> {
378        self.descriptor.as_mut().unwrap()
379    }
380
381    pub fn finish(self) {
382        drop(self);
383    }
384}
385
386impl Drop for RenderPass<'_> {
387    fn drop(&mut self) {
388        profile_function!();
389
390        drop(self.descriptor.take());
391
392        // Return the encoder to the frame context
393        self.context.encoder = self.encoder.take();
394    }
395}
396
397/// Helper trait for creating render passes with common configurations.
398pub trait RenderPassExt {
399    /// Create a render pass that clears to the given color.
400    fn clear_pass<'a>(
401        &'a mut self,
402        target: RenderTarget<'a>,
403        clear_color: wgpu::Color,
404    ) -> RenderPass<'a>;
405
406    /// Create a render pass that loads existing content.
407    fn load_pass<'a>(&'a mut self, target: RenderTarget<'a>) -> RenderPass<'a>;
408}
409
410impl RenderPassExt for FrameContext {
411    fn clear_pass<'a>(
412        &'a mut self,
413        target: RenderTarget<'a>,
414        clear_color: wgpu::Color,
415    ) -> RenderPass<'a> {
416        RenderPassBuilder::new()
417            .target(target)
418            .clear_color(clear_color)
419            .build(self)
420    }
421
422    fn load_pass<'a>(&'a mut self, target: RenderTarget<'a>) -> RenderPass<'a> {
423        RenderPassBuilder::new().target(target).build(self)
424    }
425}