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