Skip to main content

astrelis_render/
frame.rs

1//! Frame lifecycle and RAII rendering context.
2//!
3//! This module provides [`FrameContext`], which manages the lifecycle of a single
4//! rendering frame using RAII patterns. The frame automatically submits GPU commands
5//! and presents the surface when dropped.
6//!
7//! # RAII Pattern
8//!
9//! ```rust,no_run
10//! # use astrelis_render::RenderableWindow;
11//! # use astrelis_winit::window::WindowBackend;
12//! # let mut renderable_window: RenderableWindow = todo!();
13//! {
14//!     let mut frame = renderable_window.begin_drawing();
15//!
16//!     // Render passes must be dropped before frame.finish()
17//!     frame.clear_and_render(
18//!         astrelis_render::RenderTarget::Surface,
19//!         astrelis_render::Color::BLACK,
20//!         |pass| {
21//!             // Rendering commands
22//!             // Pass is automatically dropped here
23//!         },
24//!     );
25//!
26//!     frame.finish(); // Submits commands and presents surface
27//! } // FrameContext drops here if .finish() not called
28//! ```
29//!
30//! # Important
31//!
32//! - Render passes MUST be dropped before calling `frame.finish()`
33//! - Use `clear_and_render()` for automatic pass scoping
34//! - Forgetting `frame.finish()` will still submit via Drop, but explicitly calling it is recommended
35
36use std::sync::Arc;
37
38use astrelis_core::profiling::{profile_function, profile_scope};
39use astrelis_winit::window::WinitWindow;
40
41use crate::context::GraphicsContext;
42use crate::gpu_profiling::GpuFrameProfiler;
43use crate::target::RenderTarget;
44
45/// Per-frame rendering statistics.
46///
47/// Tracks the number of render passes and draw calls executed during a single frame.
48pub struct FrameStats {
49    /// Number of render passes begun this frame.
50    pub passes: usize,
51    /// Total number of draw calls issued across all passes.
52    pub draw_calls: usize,
53}
54
55impl FrameStats {
56    pub(crate) fn new() -> Self {
57        Self {
58            passes: 0,
59            draw_calls: 0,
60        }
61    }
62}
63
64/// The acquired surface texture and its view for the current frame.
65///
66/// Wraps a [`wgpu::SurfaceTexture`] together with a pre-created
67/// [`wgpu::TextureView`] so that render passes can bind it directly.
68pub struct Surface {
69    pub(crate) texture: wgpu::SurfaceTexture,
70    pub(crate) view: wgpu::TextureView,
71}
72
73impl Surface {
74    pub fn texture(&self) -> &wgpu::Texture {
75        &self.texture.texture
76    }
77
78    pub fn view(&self) -> &wgpu::TextureView {
79        &self.view
80    }
81}
82
83/// Context for a single frame of rendering.
84///
85/// When a [`GpuFrameProfiler`] is attached (via [`RenderableWindow::set_gpu_profiler`]),
86/// GPU profiling scopes are automatically created around render passes in
87/// [`with_pass`](Self::with_pass) and [`clear_and_render`](Self::clear_and_render).
88/// Queries are resolved and the profiler frame is ended in the `Drop` implementation.
89pub struct FrameContext {
90    pub(crate) stats: FrameStats,
91    pub(crate) surface: Option<Surface>,
92    pub(crate) encoder: Option<wgpu::CommandEncoder>,
93    pub(crate) context: Arc<GraphicsContext>,
94    pub(crate) window: Arc<WinitWindow>,
95    pub(crate) surface_format: wgpu::TextureFormat,
96    /// Optional GPU profiler for automatic render pass profiling.
97    pub(crate) gpu_profiler: Option<Arc<GpuFrameProfiler>>,
98}
99
100impl FrameContext {
101    /// Get the surface for this frame.
102    ///
103    /// # Panics
104    /// Panics if the surface has already been consumed. Use `try_surface()` for fallible access.
105    pub fn surface(&self) -> &Surface {
106        self.surface.as_ref().expect("Surface already consumed or not acquired")
107    }
108
109    /// Try to get the surface for this frame.
110    ///
111    /// Returns `None` if the surface has already been consumed.
112    pub fn try_surface(&self) -> Option<&Surface> {
113        self.surface.as_ref()
114    }
115
116    /// Check if the surface is available.
117    pub fn has_surface(&self) -> bool {
118        self.surface.is_some()
119    }
120
121    pub fn surface_format(&self) -> wgpu::TextureFormat {
122        self.surface_format
123    }
124
125    pub fn increment_passes(&mut self) {
126        self.stats.passes += 1;
127    }
128
129    pub fn increment_draw_calls(&mut self) {
130        self.stats.draw_calls += 1;
131    }
132
133    pub fn stats(&self) -> &FrameStats {
134        &self.stats
135    }
136
137    pub fn graphics_context(&self) -> &GraphicsContext {
138        &self.context
139    }
140
141    /// Get a cloneable Arc reference to the graphics context.
142    pub fn graphics_context_arc(&self) -> &Arc<GraphicsContext> {
143        &self.context
144    }
145
146    /// Get the command encoder for this frame.
147    ///
148    /// # Panics
149    /// Panics if the encoder has already been taken. Use `try_encoder()` for fallible access.
150    pub fn encoder(&mut self) -> &mut wgpu::CommandEncoder {
151        self.encoder.as_mut().expect("Encoder already taken")
152    }
153
154    /// Try to get the command encoder for this frame.
155    ///
156    /// Returns `None` if the encoder has already been taken.
157    pub fn try_encoder(&mut self) -> Option<&mut wgpu::CommandEncoder> {
158        self.encoder.as_mut()
159    }
160
161    /// Check if the encoder is available.
162    pub fn has_encoder(&self) -> bool {
163        self.encoder.is_some()
164    }
165
166    /// Get the encoder and surface together.
167    ///
168    /// # Panics
169    /// Panics if either the encoder or surface has been consumed.
170    /// Use `try_encoder_and_surface()` for fallible access.
171    pub fn encoder_and_surface(&mut self) -> (&mut wgpu::CommandEncoder, &Surface) {
172        (
173            self.encoder.as_mut().expect("Encoder already taken"),
174            self.surface.as_ref().expect("Surface already consumed"),
175        )
176    }
177
178    /// Try to get the encoder and surface together.
179    ///
180    /// Returns `None` if either has been consumed.
181    pub fn try_encoder_and_surface(&mut self) -> Option<(&mut wgpu::CommandEncoder, &Surface)> {
182        match (self.encoder.as_mut(), self.surface.as_ref()) {
183            (Some(encoder), Some(surface)) => Some((encoder, surface)),
184            _ => None,
185        }
186    }
187
188    /// Get direct access to the command encoder (immutable).
189    pub fn encoder_ref(&self) -> Option<&wgpu::CommandEncoder> {
190        self.encoder.as_ref()
191    }
192
193    /// Get mutable access to the command encoder.
194    pub fn encoder_mut(&mut self) -> Option<&mut wgpu::CommandEncoder> {
195        self.encoder.as_mut()
196    }
197
198    /// Get the surface texture view for this frame.
199    ///
200    /// # Panics
201    /// Panics if the surface has been consumed. Use `try_surface_view()` for fallible access.
202    pub fn surface_view(&self) -> &wgpu::TextureView {
203        self.surface().view()
204    }
205
206    /// Try to get the surface texture view for this frame.
207    pub fn try_surface_view(&self) -> Option<&wgpu::TextureView> {
208        self.try_surface().map(|s| s.view())
209    }
210
211    /// Get the surface texture for this frame.
212    ///
213    /// # Panics
214    /// Panics if the surface has been consumed. Use `try_surface_texture()` for fallible access.
215    pub fn surface_texture(&self) -> &wgpu::Texture {
216        self.surface().texture()
217    }
218
219    /// Try to get the surface texture for this frame.
220    pub fn try_surface_texture(&self) -> Option<&wgpu::Texture> {
221        self.try_surface().map(|s| s.texture())
222    }
223
224    pub fn finish(self) {
225        drop(self);
226    }
227
228    /// Get the GPU profiler attached to this frame, if any.
229    pub fn gpu_profiler(&self) -> Option<&GpuFrameProfiler> {
230        self.gpu_profiler.as_deref()
231    }
232
233    /// Check if GPU profiling is active for this frame.
234    pub fn has_gpu_profiler(&self) -> bool {
235        self.gpu_profiler.is_some()
236    }
237
238    /// Execute a closure with a render pass, automatically handling scoping.
239    ///
240    /// This is the ergonomic RAII pattern that eliminates the need for manual `{ }` blocks.
241    /// The render pass is automatically dropped after the closure completes.
242    ///
243    /// When a GPU profiler is attached to this frame (via [`RenderableWindow::set_gpu_profiler`]),
244    /// a GPU profiling scope is automatically created around the render pass, using the
245    /// pass label (or `"render_pass"` as default) as the scope name.
246    ///
247    /// # Example
248    /// ```rust,no_run
249    /// # use astrelis_render::*;
250    /// # let mut frame: FrameContext = todo!();
251    /// frame.with_pass(
252    ///     RenderPassBuilder::new()
253    ///         .target(RenderTarget::Surface)
254    ///         .clear_color(Color::BLACK),
255    ///     |pass| {
256    ///         // Render commands here
257    ///         // pass automatically drops when closure ends
258    ///     }
259    /// );
260    /// frame.finish();
261    /// ```
262    pub fn with_pass<'a, F>(&'a mut self, builder: RenderPassBuilder<'a>, f: F)
263    where
264        F: FnOnce(&mut RenderPass<'a>),
265    {
266        profile_scope!("with_pass");
267
268        #[cfg(feature = "gpu-profiling")]
269        {
270            if self.gpu_profiler.is_some() {
271                self.with_pass_profiled_inner(builder, f);
272                return;
273            }
274        }
275
276        let mut pass = builder.build(self);
277        f(&mut pass);
278        // pass drops here automatically
279    }
280
281    /// Internal: execute a render pass with GPU profiling scope.
282    ///
283    /// This method creates a GPU timing scope around the render pass using
284    /// the profiler attached to this frame. The encoder is temporarily moved
285    /// out, wrapped in a profiling scope, and returned after the closure completes.
286    ///
287    /// # Safety rationale for the unsafe block:
288    ///
289    /// The `RenderPass` created here borrows `self` (for draw call counting),
290    /// but the encoder is held by the profiling scope, not by the `RenderPass`.
291    /// After the closure and scope are dropped, we need to write the encoder back
292    /// to `self.encoder`. The borrow checker cannot see that the `RenderPass` is
293    /// fully dropped before the write, so we use a raw pointer to reborrow `self`.
294    /// This is safe because:
295    /// 1. The `RenderPass` has `encoder: None` - it never touches `self.encoder`
296    /// 2. The `pass` (wgpu_pass) is taken before the `RenderPass` is dropped
297    /// 3. The `RenderPass::Drop` returns early when `encoder` is `None`
298    /// 4. The encoder write happens strictly after all borrows are released
299    #[cfg(feature = "gpu-profiling")]
300    fn with_pass_profiled_inner<'a, F>(&'a mut self, builder: RenderPassBuilder<'a>, f: F)
301    where
302        F: FnOnce(&mut RenderPass<'a>),
303    {
304        let label = builder.label_or("render_pass").to_string();
305        let profiler = self.gpu_profiler.clone().unwrap();
306        let mut encoder = self.encoder.take().expect("Encoder already taken");
307
308        // Build attachments (borrows self immutably for surface view access).
309        // We must finish using the attachments before mutating self.
310        let (all_attachments, depth_attachment) = builder.build_attachments(self);
311
312        {
313            let mut scope = profiler.scope(&label, &mut encoder);
314
315            let descriptor = wgpu::RenderPassDescriptor {
316                label: Some(&label),
317                color_attachments: &all_attachments,
318                depth_stencil_attachment: depth_attachment,
319                occlusion_query_set: None,
320                timestamp_writes: None,
321            };
322
323            let wgpu_pass = scope.begin_render_pass(&descriptor).forget_lifetime();
324
325            // SAFETY: We need to create a RenderPass that borrows self for 'a
326            // (for draw call counting via frame.increment_draw_calls()), but
327            // we also need to reassign self.encoder after the scope drops.
328            // The RenderPass created here has encoder=None, so its Drop impl
329            // will NOT write to self.encoder. We use a raw pointer to get a
330            // second mutable reference, which is safe because:
331            // - The RenderPass only accesses self.stats (via increment_draw_calls)
332            // - The encoder reassignment only accesses self.encoder
333            // - These are disjoint fields
334            // - The RenderPass is fully dropped before the encoder reassignment
335            let self_ptr = self as *mut FrameContext;
336            let frame_ref: &'a mut FrameContext = unsafe { &mut *self_ptr };
337            frame_ref.stats.passes += 1;
338
339            let mut pass = RenderPass {
340                frame: frame_ref,
341                encoder: None, // encoder held by scope
342                pass: Some(wgpu_pass),
343            };
344
345            f(&mut pass);
346
347            // End the render pass before the scope closes.
348            pass.pass.take();
349            // RenderPass is dropped here - its Drop impl skips encoder return (encoder is None)
350        }
351        // scope dropped here -- end GPU timestamp
352
353        // SAFETY: All borrows from the RenderPass and scope are now released.
354        // The attachments from build_attachments are also dropped.
355        self.encoder = Some(encoder);
356    }
357
358    /// Convenience method to clear to a color and execute rendering commands.
359    ///
360    /// This is the most common pattern - clear the surface and render.
361    /// When a GPU profiler is attached, a scope named `"main_pass"` is automatically
362    /// created around the render pass.
363    ///
364    /// # Example
365    /// ```rust,no_run
366    /// # use astrelis_render::*;
367    /// # let mut frame: FrameContext = todo!();
368    /// frame.clear_and_render(
369    ///     RenderTarget::Surface,
370    ///     Color::BLACK,
371    ///     |pass| {
372    ///         // Render your content here
373    ///         // Example: ui.render(pass.wgpu_pass());
374    ///     }
375    /// );
376    /// frame.finish();
377    /// ```
378    pub fn clear_and_render<'a, F>(
379        &'a mut self,
380        target: RenderTarget<'a>,
381        clear_color: impl Into<crate::Color>,
382        f: F,
383    ) where
384        F: FnOnce(&mut RenderPass<'a>),
385    {
386        profile_scope!("clear_and_render");
387        self.with_pass(
388            RenderPassBuilder::new()
389                .label("main_pass")
390                .target(target)
391                .clear_color(clear_color.into()),
392            f,
393        );
394    }
395
396    /// Clear the target and render with depth testing enabled.
397    ///
398    /// This is the same as `clear_and_render` but also attaches a depth buffer
399    /// and clears it to the specified value (typically 0.0 for reverse-Z depth).
400    ///
401    /// # Arguments
402    /// - `target`: The render target (Surface or Framebuffer)
403    /// - `clear_color`: The color to clear to
404    /// - `depth_view`: The depth texture view for depth testing
405    /// - `depth_clear_value`: The value to clear the depth buffer to (0.0 for reverse-Z)
406    /// - `f`: The closure to execute rendering commands
407    ///
408    /// # Example
409    ///
410    /// ```ignore
411    /// let ui_depth_view = ui_renderer.depth_view();
412    /// frame.clear_and_render_with_depth(
413    ///     RenderTarget::Surface,
414    ///     Color::BLACK,
415    ///     ui_depth_view,
416    ///     0.0, // Clear to 0.0 for reverse-Z
417    ///     |pass| {
418    ///         ui_system.render(pass.wgpu_pass());
419    ///     },
420    /// );
421    /// ```
422    pub fn clear_and_render_with_depth<'a, F>(
423        &'a mut self,
424        target: RenderTarget<'a>,
425        clear_color: impl Into<crate::Color>,
426        depth_view: &'a wgpu::TextureView,
427        depth_clear_value: f32,
428        f: F,
429    ) where
430        F: FnOnce(&mut RenderPass<'a>),
431    {
432        profile_scope!("clear_and_render_with_depth");
433        self.with_pass(
434            RenderPassBuilder::new()
435                .label("main_pass_with_depth")
436                .target(target)
437                .clear_color(clear_color.into())
438                .depth_stencil_attachment(
439                    depth_view,
440                    Some(wgpu::Operations {
441                        load: wgpu::LoadOp::Clear(depth_clear_value),
442                        store: wgpu::StoreOp::Store,
443                    }),
444                    None,
445                ),
446            f,
447        );
448    }
449
450    /// Create a render pass that clears to the given color.
451    pub fn clear_pass<'a>(
452        &'a mut self,
453        target: RenderTarget<'a>,
454        clear_color: wgpu::Color,
455    ) -> RenderPass<'a> {
456        RenderPassBuilder::new()
457            .target(target)
458            .clear_color(clear_color)
459            .build(self)
460    }
461
462    /// Create a render pass that loads existing content.
463    pub fn load_pass<'a>(&'a mut self, target: RenderTarget<'a>) -> RenderPass<'a> {
464        RenderPassBuilder::new().target(target).build(self)
465    }
466
467    /// Execute a closure with a GPU profiling scope on the command encoder.
468    ///
469    /// If no GPU profiler is attached, the closure is called directly with the encoder.
470    /// When a profiler is present, a GPU timing scope with the given label wraps the closure.
471    ///
472    /// This is useful for profiling non-render-pass work like buffer copies, texture uploads,
473    /// or compute dispatches that happen outside of `with_pass()`.
474    ///
475    /// # Example
476    ///
477    /// ```ignore
478    /// frame.with_gpu_scope("upload_data", |encoder| {
479    ///     encoder.copy_buffer_to_buffer(&src, 0, &dst, 0, size);
480    /// });
481    /// ```
482    #[cfg(feature = "gpu-profiling")]
483    pub fn with_gpu_scope<F>(&mut self, label: &str, f: F)
484    where
485        F: FnOnce(&mut wgpu::CommandEncoder),
486    {
487        if let Some(profiler) = self.gpu_profiler.clone() {
488            let mut encoder = self.encoder.take().expect("Encoder already taken");
489            {
490                let mut scope = profiler.scope(label, &mut encoder);
491                f(&mut scope);
492            }
493            self.encoder = Some(encoder);
494        } else {
495            f(self.encoder());
496        }
497    }
498
499    /// Execute a closure with a GPU profiling scope on the command encoder.
500    ///
501    /// When `gpu-profiling` feature is disabled, this simply calls the closure with the encoder.
502    #[cfg(not(feature = "gpu-profiling"))]
503    pub fn with_gpu_scope<F>(&mut self, _label: &str, f: F)
504    where
505        F: FnOnce(&mut wgpu::CommandEncoder),
506    {
507        f(self.encoder());
508    }
509}
510
511impl Drop for FrameContext {
512    fn drop(&mut self) {
513        profile_function!();
514
515        if self.stats.passes == 0 {
516            tracing::error!("No render passes were executed for this frame!");
517            return;
518        }
519
520        // Resolve GPU profiler queries before submitting commands
521        if let Some(ref profiler) = self.gpu_profiler
522            && let Some(encoder) = self.encoder.as_mut() {
523                profiler.resolve_queries(encoder);
524            }
525
526        if let Some(encoder) = self.encoder.take() {
527            {
528                profile_scope!("submit_commands");
529                self.context.queue().submit(std::iter::once(encoder.finish()));
530            }
531        }
532
533        if let Some(surface) = self.surface.take() {
534            profile_scope!("present_surface");
535            surface.texture.present();
536        }
537
538        // End GPU profiler frame (after submit, before next frame)
539        if let Some(ref profiler) = self.gpu_profiler
540            && let Err(e) = profiler.end_frame() {
541                tracing::warn!("GPU profiler end_frame error: {e:?}");
542            }
543
544        // Request redraw for next frame
545        self.window.request_redraw();
546    }
547}
548
549/// Clear operation for a render pass.
550#[derive(Debug, Clone, Copy)]
551#[derive(Default)]
552pub enum ClearOp {
553    /// Load existing contents (no clear).
554    #[default]
555    Load,
556    /// Clear to the specified color.
557    Clear(wgpu::Color),
558}
559
560
561impl From<wgpu::Color> for ClearOp {
562    fn from(color: wgpu::Color) -> Self {
563        ClearOp::Clear(color)
564    }
565}
566
567impl From<crate::Color> for ClearOp {
568    fn from(color: crate::Color) -> Self {
569        ClearOp::Clear(color.to_wgpu())
570    }
571}
572
573/// Depth clear operation for a render pass.
574#[derive(Debug, Clone, Copy)]
575pub enum DepthClearOp {
576    /// Load existing depth values.
577    Load,
578    /// Clear to the specified depth value (typically 1.0).
579    Clear(f32),
580}
581
582impl Default for DepthClearOp {
583    fn default() -> Self {
584        DepthClearOp::Clear(1.0)
585    }
586}
587
588/// Builder for creating render passes.
589pub struct RenderPassBuilder<'a> {
590    label: Option<&'a str>,
591    // New simplified API
592    target: Option<RenderTarget<'a>>,
593    clear_op: ClearOp,
594    depth_clear_op: DepthClearOp,
595    // Legacy API for advanced use
596    color_attachments: Vec<Option<wgpu::RenderPassColorAttachment<'a>>>,
597    surface_attachment_ops: Option<(wgpu::Operations<wgpu::Color>, Option<&'a wgpu::TextureView>)>,
598    depth_stencil_attachment: Option<wgpu::RenderPassDepthStencilAttachment<'a>>,
599    // GPU profiling support
600    #[cfg(feature = "gpu-profiling")]
601    timestamp_writes: Option<wgpu::RenderPassTimestampWrites<'a>>,
602}
603
604impl<'a> RenderPassBuilder<'a> {
605    pub fn new() -> Self {
606        Self {
607            label: None,
608            target: None,
609            clear_op: ClearOp::Load,
610            depth_clear_op: DepthClearOp::default(),
611            color_attachments: Vec::new(),
612            surface_attachment_ops: None,
613            depth_stencil_attachment: None,
614            #[cfg(feature = "gpu-profiling")]
615            timestamp_writes: None,
616        }
617    }
618
619    /// Set a debug label for the render pass.
620    pub fn label(mut self, label: &'a str) -> Self {
621        self.label = Some(label);
622        self
623    }
624
625    /// Get the label, or a default fallback.
626    #[allow(dead_code)]
627    pub(crate) fn label_or<'b>(&'b self, default: &'b str) -> &'b str {
628        self.label.unwrap_or(default)
629    }
630
631    /// Set timestamp writes for GPU profiling.
632    #[cfg(feature = "gpu-profiling")]
633    #[allow(dead_code)]
634    pub(crate) fn timestamp_writes(mut self, tw: wgpu::RenderPassTimestampWrites<'a>) -> Self {
635        self.timestamp_writes = Some(tw);
636        self
637    }
638
639    /// Set the render target (Surface or Framebuffer).
640    ///
641    /// This is the simplified API - use this instead of manual color_attachment calls.
642    pub fn target(mut self, target: RenderTarget<'a>) -> Self {
643        self.target = Some(target);
644        self
645    }
646
647    /// Set clear color for the render target.
648    ///
649    /// Pass a wgpu::Color or use ClearOp::Load to preserve existing contents.
650    pub fn clear_color(mut self, color: impl Into<ClearOp>) -> Self {
651        self.clear_op = color.into();
652        self
653    }
654
655    /// Set depth clear operation.
656    pub fn clear_depth(mut self, depth: f32) -> Self {
657        self.depth_clear_op = DepthClearOp::Clear(depth);
658        self
659    }
660
661    /// Load existing depth values instead of clearing.
662    pub fn load_depth(mut self) -> Self {
663        self.depth_clear_op = DepthClearOp::Load;
664        self
665    }
666
667    // Legacy API for advanced use cases
668
669    /// Add a color attachment manually (advanced API).
670    ///
671    /// For most cases, use `.target()` instead.
672    pub fn color_attachment(
673        mut self,
674        view: Option<&'a wgpu::TextureView>,
675        resolve_target: Option<&'a wgpu::TextureView>,
676        ops: wgpu::Operations<wgpu::Color>,
677    ) -> Self {
678        if let Some(view) = view {
679            self.color_attachments
680                .push(Some(wgpu::RenderPassColorAttachment {
681                    view,
682                    resolve_target,
683                    ops,
684                    depth_slice: None,
685                }));
686        } else {
687            // Store ops for later - will be filled with surface view in build()
688            self.surface_attachment_ops = Some((ops, resolve_target));
689        }
690        self
691    }
692
693    /// Add a depth-stencil attachment manually (advanced API).
694    ///
695    /// For framebuffers with depth, the depth attachment is handled automatically
696    /// when using `.target()`.
697    pub fn depth_stencil_attachment(
698        mut self,
699        view: &'a wgpu::TextureView,
700        depth_ops: Option<wgpu::Operations<f32>>,
701        stencil_ops: Option<wgpu::Operations<u32>>,
702    ) -> Self {
703        self.depth_stencil_attachment = Some(wgpu::RenderPassDepthStencilAttachment {
704            view,
705            depth_ops,
706            stencil_ops,
707        });
708        self
709    }
710
711    /// Build the color and depth attachments without creating the render pass.
712    ///
713    /// Used internally by `build()` and by `with_pass_profiled_inner()` to build
714    /// attachments before creating the GPU profiling scope.
715    pub(crate) fn build_attachments(
716        &self,
717        frame_context: &'a FrameContext,
718    ) -> (
719        Vec<Option<wgpu::RenderPassColorAttachment<'a>>>,
720        Option<wgpu::RenderPassDepthStencilAttachment<'a>>,
721    ) {
722        let mut all_attachments = Vec::new();
723
724        if let Some(target) = &self.target {
725            let color_ops = match self.clear_op {
726                ClearOp::Load => wgpu::Operations {
727                    load: wgpu::LoadOp::Load,
728                    store: wgpu::StoreOp::Store,
729                },
730                ClearOp::Clear(color) => wgpu::Operations {
731                    load: wgpu::LoadOp::Clear(color),
732                    store: wgpu::StoreOp::Store,
733                },
734            };
735
736            match target {
737                RenderTarget::Surface => {
738                    let surface_view = frame_context.surface().view();
739                    all_attachments.push(Some(wgpu::RenderPassColorAttachment {
740                        view: surface_view,
741                        resolve_target: None,
742                        ops: color_ops,
743                        depth_slice: None,
744                    }));
745                }
746                RenderTarget::Framebuffer(fb) => {
747                    all_attachments.push(Some(wgpu::RenderPassColorAttachment {
748                        view: fb.render_view(),
749                        resolve_target: fb.resolve_target(),
750                        ops: color_ops,
751                        depth_slice: None,
752                    }));
753                }
754            }
755        } else {
756            if let Some((ops, resolve_target)) = self.surface_attachment_ops {
757                let surface_view = frame_context.surface().view();
758                all_attachments.push(Some(wgpu::RenderPassColorAttachment {
759                    view: surface_view,
760                    resolve_target,
761                    ops,
762                    depth_slice: None,
763                }));
764            }
765            all_attachments.extend(self.color_attachments.iter().cloned());
766        }
767
768        let depth_attachment = if let Some(ref attachment) = self.depth_stencil_attachment {
769            Some(attachment.clone())
770        } else if let Some(RenderTarget::Framebuffer(fb)) = &self.target {
771            fb.depth_view().map(|view| {
772                let depth_ops = match self.depth_clear_op {
773                    DepthClearOp::Load => wgpu::Operations {
774                        load: wgpu::LoadOp::Load,
775                        store: wgpu::StoreOp::Store,
776                    },
777                    DepthClearOp::Clear(depth) => wgpu::Operations {
778                        load: wgpu::LoadOp::Clear(depth),
779                        store: wgpu::StoreOp::Store,
780                    },
781                };
782                wgpu::RenderPassDepthStencilAttachment {
783                    view,
784                    depth_ops: Some(depth_ops),
785                    stencil_ops: None,
786                }
787            })
788        } else {
789            None
790        };
791
792        (all_attachments, depth_attachment)
793    }
794
795    /// Builds the render pass and begins it on the provided frame context.
796    ///
797    /// This takes ownership of the CommandEncoder from the FrameContext, and releases it
798    /// back to the FrameContext when the RenderPass is dropped or [`finish`](RenderPass::finish)
799    /// is called.
800    pub fn build(self, frame_context: &'a mut FrameContext) -> RenderPass<'a> {
801        profile_function!();
802        let mut encoder = frame_context.encoder.take().unwrap();
803
804        let (all_attachments, depth_attachment) = self.build_attachments(frame_context);
805
806        #[cfg(feature = "gpu-profiling")]
807        let ts_writes = self.timestamp_writes;
808        #[cfg(not(feature = "gpu-profiling"))]
809        let ts_writes: Option<wgpu::RenderPassTimestampWrites<'_>> = None;
810
811        let descriptor = wgpu::RenderPassDescriptor {
812            label: self.label,
813            color_attachments: &all_attachments,
814            depth_stencil_attachment: depth_attachment,
815            occlusion_query_set: None,
816            timestamp_writes: ts_writes,
817        };
818
819        let render_pass = encoder.begin_render_pass(&descriptor).forget_lifetime();
820
821        frame_context.increment_passes();
822
823        RenderPass {
824            frame: frame_context,
825            encoder: Some(encoder),
826            pass: Some(render_pass),
827        }
828    }
829}
830
831impl Default for RenderPassBuilder<'_> {
832    fn default() -> Self {
833        Self::new()
834    }
835}
836
837/// A render pass wrapper that automatically returns the encoder to the frame context.
838pub struct RenderPass<'a> {
839    pub(crate) frame: &'a mut FrameContext,
840    pub(crate) encoder: Option<wgpu::CommandEncoder>,
841    pub(crate) pass: Option<wgpu::RenderPass<'static>>,
842}
843
844impl<'a> RenderPass<'a> {
845    /// Get the underlying wgpu RenderPass.
846    ///
847    /// # Panics
848    /// Panics if the render pass has already been consumed (dropped or finished).
849    /// Use `try_wgpu_pass()` for fallible access.
850    pub fn wgpu_pass(&mut self) -> &mut wgpu::RenderPass<'static> {
851        self.pass.as_mut()
852            .expect("RenderPass already consumed - ensure it wasn't dropped or finished early")
853    }
854
855    /// Try to get the underlying wgpu RenderPass.
856    ///
857    /// Returns `None` if the render pass has already been consumed.
858    pub fn try_wgpu_pass(&mut self) -> Option<&mut wgpu::RenderPass<'static>> {
859        self.pass.as_mut()
860    }
861
862    /// Check if the render pass is still valid and can be used.
863    pub fn is_valid(&self) -> bool {
864        self.pass.is_some()
865    }
866
867    /// Get raw access to the underlying wgpu render pass.
868    pub fn raw_pass(&mut self) -> &mut wgpu::RenderPass<'static> {
869        self.pass.as_mut().unwrap()
870    }
871
872    /// Get the graphics context.
873    pub fn graphics_context(&self) -> &GraphicsContext {
874        &self.frame.context
875    }
876
877    /// Get the frame context.
878    pub fn frame_context(&self) -> &FrameContext {
879        self.frame
880    }
881
882    pub fn finish(self) {
883        drop(self);
884    }
885
886    // =========================================================================
887    // Viewport/Scissor Methods
888    // =========================================================================
889
890    /// Set the viewport using physical coordinates.
891    ///
892    /// The viewport defines the transformation from normalized device coordinates
893    /// to window coordinates.
894    ///
895    /// # Arguments
896    ///
897    /// * `rect` - The viewport rectangle in physical (pixel) coordinates
898    /// * `min_depth` - Minimum depth value (typically 0.0)
899    /// * `max_depth` - Maximum depth value (typically 1.0)
900    pub fn set_viewport_physical(
901        &mut self,
902        rect: astrelis_core::geometry::PhysicalRect<f32>,
903        min_depth: f32,
904        max_depth: f32,
905    ) {
906        self.wgpu_pass().set_viewport(
907            rect.x,
908            rect.y,
909            rect.width,
910            rect.height,
911            min_depth,
912            max_depth,
913        );
914    }
915
916    /// Set the viewport using logical coordinates (converts with scale factor).
917    ///
918    /// # Arguments
919    ///
920    /// * `rect` - The viewport rectangle in logical coordinates
921    /// * `min_depth` - Minimum depth value (typically 0.0)
922    /// * `max_depth` - Maximum depth value (typically 1.0)
923    /// * `scale` - Scale factor for logical to physical conversion
924    pub fn set_viewport_logical(
925        &mut self,
926        rect: astrelis_core::geometry::LogicalRect<f32>,
927        min_depth: f32,
928        max_depth: f32,
929        scale: astrelis_core::geometry::ScaleFactor,
930    ) {
931        let physical = rect.to_physical_f32(scale);
932        self.set_viewport_physical(physical, min_depth, max_depth);
933    }
934
935    /// Set the viewport from a Viewport struct.
936    ///
937    /// Uses the viewport's position and size, with depth range 0.0 to 1.0.
938    pub fn set_viewport(&mut self, viewport: &crate::Viewport) {
939        self.wgpu_pass().set_viewport(
940            viewport.position.x,
941            viewport.position.y,
942            viewport.size.width,
943            viewport.size.height,
944            0.0,
945            1.0,
946        );
947    }
948
949    /// Set the scissor rectangle using physical coordinates.
950    ///
951    /// The scissor rectangle defines the area of the render target that
952    /// can be modified by drawing commands.
953    ///
954    /// # Arguments
955    ///
956    /// * `rect` - The scissor rectangle in physical (pixel) coordinates
957    pub fn set_scissor_physical(&mut self, rect: astrelis_core::geometry::PhysicalRect<u32>) {
958        self.wgpu_pass()
959            .set_scissor_rect(rect.x, rect.y, rect.width, rect.height);
960    }
961
962    /// Set the scissor rectangle using logical coordinates.
963    ///
964    /// # Arguments
965    ///
966    /// * `rect` - The scissor rectangle in logical coordinates
967    /// * `scale` - Scale factor for logical to physical conversion
968    pub fn set_scissor_logical(
969        &mut self,
970        rect: astrelis_core::geometry::LogicalRect<f32>,
971        scale: astrelis_core::geometry::ScaleFactor,
972    ) {
973        let physical = rect.to_physical(scale);
974        self.set_scissor_physical(physical);
975    }
976
977    // =========================================================================
978    // Convenience Drawing Methods
979    // =========================================================================
980
981    /// Set the pipeline for this render pass.
982    pub fn set_pipeline(&mut self, pipeline: &'a wgpu::RenderPipeline) {
983        self.wgpu_pass().set_pipeline(pipeline);
984    }
985
986    /// Set a bind group for this render pass.
987    pub fn set_bind_group(
988        &mut self,
989        index: u32,
990        bind_group: &'a wgpu::BindGroup,
991        offsets: &[u32],
992    ) {
993        self.wgpu_pass().set_bind_group(index, bind_group, offsets);
994    }
995
996    /// Set the vertex buffer for this render pass.
997    pub fn set_vertex_buffer(&mut self, slot: u32, buffer_slice: wgpu::BufferSlice<'a>) {
998        self.wgpu_pass().set_vertex_buffer(slot, buffer_slice);
999    }
1000
1001    /// Set the index buffer for this render pass.
1002    pub fn set_index_buffer(&mut self, buffer_slice: wgpu::BufferSlice<'a>, format: wgpu::IndexFormat) {
1003        self.wgpu_pass().set_index_buffer(buffer_slice, format);
1004    }
1005
1006    /// Draw primitives.
1007    pub fn draw(&mut self, vertices: std::ops::Range<u32>, instances: std::ops::Range<u32>) {
1008        self.wgpu_pass().draw(vertices, instances);
1009        self.frame.increment_draw_calls();
1010    }
1011
1012    /// Draw indexed primitives.
1013    pub fn draw_indexed(
1014        &mut self,
1015        indices: std::ops::Range<u32>,
1016        base_vertex: i32,
1017        instances: std::ops::Range<u32>,
1018    ) {
1019        self.wgpu_pass().draw_indexed(indices, base_vertex, instances);
1020        self.frame.increment_draw_calls();
1021    }
1022
1023    /// Insert a debug marker.
1024    pub fn insert_debug_marker(&mut self, label: &str) {
1025        self.wgpu_pass().insert_debug_marker(label);
1026    }
1027
1028    /// Push a debug group.
1029    pub fn push_debug_group(&mut self, label: &str) {
1030        self.wgpu_pass().push_debug_group(label);
1031    }
1032
1033    /// Pop a debug group.
1034    pub fn pop_debug_group(&mut self) {
1035        self.wgpu_pass().pop_debug_group();
1036    }
1037
1038    // =========================================================================
1039    // Push Constants
1040    // =========================================================================
1041
1042    /// Set push constants for a range of shader stages.
1043    ///
1044    /// Push constants are a fast way to pass small amounts of data to shaders
1045    /// without the overhead of buffer updates. They are limited in size
1046    /// (typically 128-256 bytes depending on the GPU).
1047    ///
1048    /// **Requires the `PUSH_CONSTANTS` feature to be enabled.**
1049    ///
1050    /// # Arguments
1051    ///
1052    /// * `stages` - Which shader stages can access this data
1053    /// * `offset` - Byte offset within the push constant range
1054    /// * `data` - The data to set (must be Pod)
1055    ///
1056    /// # Example
1057    ///
1058    /// ```ignore
1059    /// #[repr(C)]
1060    /// #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1061    /// struct PushConstants {
1062    ///     transform: [[f32; 4]; 4],
1063    ///     color: [f32; 4],
1064    /// }
1065    ///
1066    /// let constants = PushConstants {
1067    ///     transform: /* ... */,
1068    ///     color: [1.0, 0.0, 0.0, 1.0],
1069    /// };
1070    ///
1071    /// pass.set_push_constants(
1072    ///     wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
1073    ///     0,
1074    ///     &constants,
1075    /// );
1076    /// ```
1077    pub fn set_push_constants<T: bytemuck::Pod>(
1078        &mut self,
1079        stages: wgpu::ShaderStages,
1080        offset: u32,
1081        data: &T,
1082    ) {
1083        self.wgpu_pass()
1084            .set_push_constants(stages, offset, bytemuck::bytes_of(data));
1085    }
1086
1087    /// Set push constants from raw bytes.
1088    ///
1089    /// Use this when you need more control over the data layout.
1090    pub fn set_push_constants_raw(
1091        &mut self,
1092        stages: wgpu::ShaderStages,
1093        offset: u32,
1094        data: &[u8],
1095    ) {
1096        self.wgpu_pass().set_push_constants(stages, offset, data);
1097    }
1098}
1099
1100impl Drop for RenderPass<'_> {
1101    fn drop(&mut self) {
1102        profile_function!();
1103
1104        drop(self.pass.take());
1105
1106        // Return the encoder to the frame context.
1107        // When used within a GPU profiling scope (with_pass_profiled_inner),
1108        // encoder is None because the encoder is held by the profiling scope — skip in that case.
1109        if let Some(encoder) = self.encoder.take() {
1110            self.frame.encoder = Some(encoder);
1111        }
1112    }
1113}
1114