Skip to main content

astrelis_render/
frame.rs

1//! Frame lifecycle and RAII rendering context.
2//!
3//! This module provides [`Frame`], 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//! # Architecture
8//!
9//! The render system follows a clear ownership hierarchy:
10//!
11//! ```text
12//! GraphicsContext (Global, Arc<Self>)
13//!     └─▶ RenderWindow (Per-window, persistent)
14//!             └─▶ Frame (Per-frame, temporary)
15//!                     └─▶ RenderPass (Per-pass, temporary, owns encoder)
16//! ```
17//!
18//! Key design decisions:
19//! - **Each pass owns its encoder** - No encoder movement, no borrow conflicts
20//! - **Frame collects command buffers** - Via `RefCell<Vec<CommandBuffer>>`
21//! - **Immutable frame reference** - RenderPass takes `&'f Frame`, not `&'f mut Frame`
22//! - **Atomic stats** - Thread-safe counting via `Arc<AtomicFrameStats>`
23//! - **No unsafe code** - Clean ownership, no pointer casts
24//!
25//! # RAII Pattern
26//!
27//! ```rust,no_run
28//! # use astrelis_render::RenderWindow;
29//! # let mut window: RenderWindow = todo!();
30//! // New API - each pass owns its encoder
31//! let frame = window.begin_frame().expect("Surface available");
32//! {
33//!     let mut pass = frame.render_pass()
34//!         .clear_color(astrelis_render::Color::BLACK)
35//!         .clear_depth(0.0)
36//!         .label("main")
37//!         .build();
38//!
39//!     // Render commands here
40//!     // pass.wgpu_pass().draw(...);
41//! } // pass drops: ends pass → finishes encoder → pushes command buffer to frame
42//!
43//! frame.submit(); // Or let it drop - auto-submits
44//! ```
45//!
46//! # Important
47//!
48//! - Render passes own their encoder and push command buffers to the frame on drop
49//! - Multiple passes can be created sequentially within a frame
50//! - Frame auto-submits on drop if not explicitly submitted
51
52use std::cell::{Cell, RefCell};
53use std::sync::Arc;
54use std::sync::atomic::{AtomicU32, Ordering};
55
56use astrelis_core::profiling::{profile_function, profile_scope};
57use astrelis_winit::window::WinitWindow;
58
59use crate::Color;
60use crate::context::GraphicsContext;
61use crate::framebuffer::Framebuffer;
62use crate::gpu_profiling::GpuFrameProfiler;
63use crate::target::RenderTarget;
64
65/// Per-frame rendering statistics.
66///
67/// Tracks the number of render passes and draw calls executed during a single frame.
68#[derive(Debug, Clone, Copy, Default)]
69pub struct FrameStats {
70    /// Number of render passes begun this frame.
71    pub passes: usize,
72    /// Total number of draw calls issued across all passes.
73    pub draw_calls: usize,
74}
75
76/// Thread-safe atomic frame statistics.
77///
78/// Used to eliminate borrow conflicts in GPU profiling code by allowing
79/// stats updates through an Arc without needing mutable access to Frame.
80pub struct AtomicFrameStats {
81    passes: AtomicU32,
82    draw_calls: AtomicU32,
83}
84
85impl AtomicFrameStats {
86    /// Create new atomic stats initialized to zero.
87    pub fn new() -> Self {
88        Self {
89            passes: AtomicU32::new(0),
90            draw_calls: AtomicU32::new(0),
91        }
92    }
93
94    /// Increment the pass count.
95    pub fn increment_passes(&self) {
96        self.passes.fetch_add(1, Ordering::Relaxed);
97    }
98
99    /// Increment the draw call count.
100    pub fn increment_draw_calls(&self) {
101        self.draw_calls.fetch_add(1, Ordering::Relaxed);
102    }
103
104    /// Get the current pass count.
105    pub fn passes(&self) -> u32 {
106        self.passes.load(Ordering::Relaxed)
107    }
108
109    /// Get the current draw call count.
110    pub fn draw_calls(&self) -> u32 {
111        self.draw_calls.load(Ordering::Relaxed)
112    }
113
114    /// Convert to non-atomic FrameStats for final reporting.
115    pub fn to_frame_stats(&self) -> FrameStats {
116        FrameStats {
117            passes: self.passes() as usize,
118            draw_calls: self.draw_calls() as usize,
119        }
120    }
121}
122
123impl Default for AtomicFrameStats {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129/// The acquired surface texture and its view for the current frame.
130///
131/// Wraps a [`wgpu::SurfaceTexture`] together with a pre-created
132/// [`wgpu::TextureView`] so that render passes can bind it directly.
133pub struct Surface {
134    pub(crate) texture: wgpu::SurfaceTexture,
135    pub(crate) view: wgpu::TextureView,
136}
137
138impl Surface {
139    /// Get the underlying texture.
140    pub fn texture(&self) -> &wgpu::Texture {
141        &self.texture.texture
142    }
143
144    /// Get the texture view.
145    pub fn view(&self) -> &wgpu::TextureView {
146        &self.view
147    }
148}
149
150/// Context for a single frame of rendering.
151///
152/// Frame represents a single frame being rendered. It holds the acquired surface
153/// texture and collects command buffers from render passes. When dropped, it
154/// automatically submits all command buffers and presents the surface.
155///
156/// # Key Design Points
157///
158/// - **Immutable reference**: RenderPasses take `&Frame`, not `&mut Frame`
159/// - **RefCell for command buffers**: Allows multiple passes without mutable borrow
160/// - **Atomic stats**: Thread-safe pass/draw counting
161/// - **RAII cleanup**: Drop handles submit and present
162///
163/// # Example
164///
165/// ```rust,no_run
166/// # use astrelis_render::{RenderWindow, Color};
167/// # let mut window: RenderWindow = todo!();
168/// let frame = window.begin_frame().expect("Surface available");
169///
170/// // Create first pass
171/// {
172///     let mut pass = frame.render_pass()
173///         .clear_color(Color::BLACK)
174///         .build();
175///     // Render background
176/// }
177///
178/// // Create second pass (different encoder)
179/// {
180///     let mut pass = frame.render_pass()
181///         .load_color()
182///         .build();
183///     // Render UI overlay
184/// }
185///
186/// // Auto-submits on drop
187/// ```
188pub struct Frame<'w> {
189    /// Reference to the window (provides graphics context, depth view, etc.)
190    pub(crate) window: &'w crate::window::RenderWindow,
191    /// Acquired surface texture for this frame.
192    pub(crate) surface: Option<Surface>,
193    /// Collected command buffers from render passes.
194    pub(crate) command_buffers: RefCell<Vec<wgpu::CommandBuffer>>,
195    /// Atomic stats for thread-safe counting.
196    pub(crate) stats: Arc<AtomicFrameStats>,
197    /// Whether submit has been called.
198    pub(crate) submitted: Cell<bool>,
199    /// Surface texture format.
200    pub(crate) surface_format: wgpu::TextureFormat,
201    /// Optional GPU profiler.
202    pub(crate) gpu_profiler: Option<Arc<GpuFrameProfiler>>,
203    /// Window handle for redraw requests.
204    pub(crate) winit_window: Arc<WinitWindow>,
205}
206
207impl<'w> Frame<'w> {
208    /// Get the surface texture view for this frame.
209    ///
210    /// # Panics
211    /// Panics if the surface has been consumed. Use `try_surface_view()` for fallible access.
212    pub fn surface_view(&self) -> &wgpu::TextureView {
213        self.surface
214            .as_ref()
215            .expect("Surface already consumed")
216            .view()
217    }
218
219    /// Try to get the surface texture view for this frame.
220    pub fn try_surface_view(&self) -> Option<&wgpu::TextureView> {
221        self.surface.as_ref().map(|s| s.view())
222    }
223
224    /// Get the window's depth texture view, if the window was created with depth.
225    ///
226    /// This provides access to the window-owned depth buffer for render passes
227    /// that need depth testing.
228    pub fn depth_view(&self) -> Option<&wgpu::TextureView> {
229        self.window.depth_view_ref()
230    }
231
232    /// Get the surface texture format.
233    pub fn surface_format(&self) -> wgpu::TextureFormat {
234        self.surface_format
235    }
236
237    /// Get the frame size in physical pixels.
238    pub fn size(&self) -> (u32, u32) {
239        self.window.size()
240    }
241
242    /// Get the graphics context.
243    pub fn graphics(&self) -> &GraphicsContext {
244        self.window.graphics()
245    }
246
247    /// Get the wgpu device.
248    pub fn device(&self) -> &wgpu::Device {
249        self.window.graphics().device()
250    }
251
252    /// Get the wgpu queue.
253    pub fn queue(&self) -> &wgpu::Queue {
254        self.window.graphics().queue()
255    }
256
257    /// Get frame statistics.
258    pub fn stats(&self) -> FrameStats {
259        self.stats.to_frame_stats()
260    }
261
262    /// Get the atomic stats for direct access (used by RenderPass).
263    pub(crate) fn atomic_stats(&self) -> &Arc<AtomicFrameStats> {
264        &self.stats
265    }
266
267    /// Get the GPU profiler if attached.
268    pub fn gpu_profiler(&self) -> Option<&GpuFrameProfiler> {
269        self.gpu_profiler.as_deref()
270    }
271
272    /// Check if GPU profiling is active.
273    pub fn has_gpu_profiler(&self) -> bool {
274        self.gpu_profiler.is_some()
275    }
276
277    /// Create a command encoder for custom command recording.
278    ///
279    /// Use this for operations that don't fit the render pass model,
280    /// like buffer copies or texture uploads.
281    pub fn create_encoder(&self, label: Option<&str>) -> wgpu::CommandEncoder {
282        self.device()
283            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label })
284    }
285
286    /// Add a pre-built command buffer to the frame.
287    ///
288    /// Use this when you have custom command recording logic.
289    pub fn add_command_buffer(&self, buffer: wgpu::CommandBuffer) {
290        self.command_buffers.borrow_mut().push(buffer);
291    }
292
293    /// Start building a render pass.
294    ///
295    /// Returns a builder that can be configured with target, clear operations,
296    /// and depth settings before building the actual pass.
297    pub fn render_pass(&self) -> RenderPassBuilder<'_, 'w> {
298        RenderPassBuilder::new(self)
299    }
300
301    /// Start building a compute pass.
302    pub fn compute_pass(&self) -> crate::compute::ComputePassBuilder<'_, 'w> {
303        crate::compute::ComputePassBuilder::new(self)
304    }
305
306    /// Submit all collected command buffers and present the surface.
307    ///
308    /// This is called automatically on drop, but can be called explicitly
309    /// for more control over timing.
310    pub fn submit(self) {
311        // Move self to trigger drop which handles submission
312        drop(self);
313    }
314
315    /// Internal submit implementation called by Drop.
316    fn submit_inner(&self) {
317        profile_function!();
318
319        if self.stats.passes() == 0 {
320            tracing::warn!("No render passes were executed for this frame");
321        }
322
323        // Resolve GPU profiler queries before submitting
324        if let Some(ref profiler) = self.gpu_profiler {
325            // Create a dedicated encoder for query resolution
326            let mut resolve_encoder = self.create_encoder(Some("Profiler Resolve"));
327            profiler.resolve_queries(&mut resolve_encoder);
328            self.command_buffers
329                .borrow_mut()
330                .push(resolve_encoder.finish());
331        }
332
333        // Take all command buffers
334        let buffers = std::mem::take(&mut *self.command_buffers.borrow_mut());
335
336        if !buffers.is_empty() {
337            profile_scope!("submit_commands");
338            self.queue().submit(buffers);
339        }
340
341        // Present surface
342        if let Some(surface) = self.surface.as_ref() {
343            profile_scope!("present_surface");
344            // Note: We can't take() the surface since self is borrowed, but present
345            // doesn't consume it - it just signals we're done with this frame
346        }
347
348        // End GPU profiler frame
349        if let Some(ref profiler) = self.gpu_profiler
350            && let Err(e) = profiler.end_frame()
351        {
352            tracing::warn!("GPU profiler end_frame error: {e:?}");
353        }
354    }
355
356    // =========================================================================
357    // Backwards Compatibility Methods
358    // =========================================================================
359
360    /// Convenience method to clear to a color and execute rendering commands.
361    ///
362    /// This is the most common pattern - clear the surface and render.
363    ///
364    /// # Deprecated
365    ///
366    /// Prefer using the builder pattern:
367    /// ```ignore
368    /// let mut pass = frame.render_pass()
369    ///     .clear_color(Color::BLACK)
370    ///     .build();
371    /// // render
372    /// ```
373    #[deprecated(
374        since = "0.2.0",
375        note = "Use frame.render_pass().clear_color().build() instead"
376    )]
377    pub fn clear_and_render<F>(&self, target: RenderTarget<'_>, clear_color: Color, f: F)
378    where
379        F: FnOnce(&mut RenderPass<'_>),
380    {
381        profile_scope!("clear_and_render");
382        let mut pass = self
383            .render_pass()
384            .target(target)
385            .clear_color(clear_color)
386            .label("main_pass")
387            .build();
388        f(&mut pass);
389    }
390
391    /// Clear the target and render with depth testing enabled.
392    ///
393    /// # Deprecated
394    ///
395    /// Prefer using the builder pattern:
396    /// ```ignore
397    /// let mut pass = frame.render_pass()
398    ///     .clear_color(Color::BLACK)
399    ///     .clear_depth(0.0)
400    ///     .build();
401    /// ```
402    #[deprecated(
403        since = "0.2.0",
404        note = "Use frame.render_pass().clear_color().clear_depth().build() instead"
405    )]
406    pub fn clear_and_render_with_depth<'a, F>(
407        &'a self,
408        target: RenderTarget<'a>,
409        clear_color: Color,
410        depth_view: &'a wgpu::TextureView,
411        depth_clear_value: f32,
412        f: F,
413    ) where
414        F: FnOnce(&mut RenderPass<'a>),
415    {
416        profile_scope!("clear_and_render_with_depth");
417        let mut pass = self
418            .render_pass()
419            .target(target)
420            .clear_color(clear_color)
421            .depth_attachment(depth_view)
422            .clear_depth(depth_clear_value)
423            .label("main_pass_with_depth")
424            .build();
425        f(&mut pass);
426    }
427}
428
429impl Drop for Frame<'_> {
430    fn drop(&mut self) {
431        if !self.submitted.get() {
432            self.submitted.set(true);
433            self.submit_inner();
434        }
435
436        // Present surface
437        if let Some(surface) = self.surface.take() {
438            profile_scope!("present_surface");
439            surface.texture.present();
440        }
441
442        // Request redraw
443        self.winit_window.request_redraw();
444    }
445}
446
447// ============================================================================
448// RenderPassBuilder
449// ============================================================================
450
451/// Target for color attachment in render passes.
452#[derive(Debug, Clone, Copy, Default)]
453pub enum ColorTarget<'a> {
454    /// Render to the window surface.
455    #[default]
456    Surface,
457    /// Render to a custom texture view.
458    Custom(&'a wgpu::TextureView),
459    /// Render to a framebuffer.
460    Framebuffer(&'a Framebuffer),
461}
462
463/// Color operation for render pass.
464#[derive(Debug, Clone, Copy, Default)]
465pub enum ColorOp {
466    /// Clear to the specified color.
467    Clear(wgpu::Color),
468    /// Load existing contents.
469    #[default]
470    Load,
471}
472
473impl From<Color> for ColorOp {
474    fn from(color: Color) -> Self {
475        Self::Clear(color.to_wgpu())
476    }
477}
478
479impl From<wgpu::Color> for ColorOp {
480    fn from(color: wgpu::Color) -> Self {
481        Self::Clear(color)
482    }
483}
484
485/// Depth operation for render pass.
486#[derive(Debug, Clone, Copy)]
487pub enum DepthOp {
488    /// Clear to the specified value.
489    Clear(f32),
490    /// Load existing values.
491    Load,
492    /// Read-only depth (no writes).
493    ReadOnly,
494}
495
496impl Default for DepthOp {
497    fn default() -> Self {
498        Self::Clear(1.0)
499    }
500}
501
502/// Builder for creating render passes with fluent API.
503///
504/// # Example
505///
506/// ```rust,no_run
507/// # use astrelis_render::{Frame, Color};
508/// # let frame: &Frame = todo!();
509/// let mut pass = frame.render_pass()
510///     .clear_color(Color::BLACK)
511///     .clear_depth(0.0)
512///     .label("main")
513///     .build();
514///
515/// // Use pass.wgpu_pass() for rendering
516/// ```
517pub struct RenderPassBuilder<'f, 'w> {
518    frame: &'f Frame<'w>,
519    color_target: ColorTarget<'f>,
520    color_op: ColorOp,
521    depth_view: Option<&'f wgpu::TextureView>,
522    depth_op: DepthOp,
523    label: Option<String>,
524}
525
526impl<'f, 'w> RenderPassBuilder<'f, 'w> {
527    /// Create a new render pass builder.
528    pub(crate) fn new(frame: &'f Frame<'w>) -> Self {
529        Self {
530            frame,
531            color_target: ColorTarget::Surface,
532            color_op: ColorOp::Load,
533            depth_view: None,
534            depth_op: DepthOp::default(),
535            label: None,
536        }
537    }
538
539    /// Set the render target (for backwards compatibility).
540    pub fn target(mut self, target: RenderTarget<'f>) -> Self {
541        match target {
542            RenderTarget::Surface => {
543                self.color_target = ColorTarget::Surface;
544            }
545            RenderTarget::SurfaceWithDepth {
546                depth_view,
547                clear_value,
548            } => {
549                self.color_target = ColorTarget::Surface;
550                self.depth_view = Some(depth_view);
551                if let Some(v) = clear_value {
552                    self.depth_op = DepthOp::Clear(v);
553                } else {
554                    self.depth_op = DepthOp::Load;
555                }
556            }
557            RenderTarget::Framebuffer(fb) => {
558                self.color_target = ColorTarget::Framebuffer(fb);
559                if let Some(dv) = fb.depth_view() {
560                    self.depth_view = Some(dv);
561                }
562            }
563        }
564        self
565    }
566
567    /// Render to the window surface (default).
568    pub fn to_surface(mut self) -> Self {
569        self.color_target = ColorTarget::Surface;
570        self
571    }
572
573    /// Render to a framebuffer.
574    pub fn to_framebuffer(mut self, fb: &'f Framebuffer) -> Self {
575        self.color_target = ColorTarget::Framebuffer(fb);
576        if let Some(dv) = fb.depth_view() {
577            self.depth_view = Some(dv);
578        }
579        self
580    }
581
582    /// Render to a custom texture view.
583    pub fn to_texture(mut self, view: &'f wgpu::TextureView) -> Self {
584        self.color_target = ColorTarget::Custom(view);
585        self
586    }
587
588    /// Clear the color target to the specified color.
589    pub fn clear_color(mut self, color: impl Into<ColorOp>) -> Self {
590        self.color_op = color.into();
591        self
592    }
593
594    /// Load existing color contents (default).
595    pub fn load_color(mut self) -> Self {
596        self.color_op = ColorOp::Load;
597        self
598    }
599
600    /// Set the depth attachment.
601    pub fn depth_attachment(mut self, view: &'f wgpu::TextureView) -> Self {
602        self.depth_view = Some(view);
603        self
604    }
605
606    /// Use the window's depth buffer automatically.
607    ///
608    /// # Panics
609    /// Panics if the window doesn't have a depth buffer.
610    pub fn with_window_depth(mut self) -> Self {
611        self.depth_view = Some(
612            self.frame
613                .depth_view()
614                .expect("Window must have depth buffer for with_window_depth()"),
615        );
616        self
617    }
618
619    /// Use the window's depth buffer if available.
620    pub fn with_window_depth_if_available(mut self) -> Self {
621        if let Some(dv) = self.frame.depth_view() {
622            self.depth_view = Some(dv);
623        }
624        self
625    }
626
627    /// Clear the depth buffer to the specified value.
628    pub fn clear_depth(mut self, value: f32) -> Self {
629        self.depth_op = DepthOp::Clear(value);
630        self
631    }
632
633    /// Load existing depth values.
634    pub fn load_depth(mut self) -> Self {
635        self.depth_op = DepthOp::Load;
636        self
637    }
638
639    /// Use depth in read-only mode (no writes).
640    pub fn depth_readonly(mut self) -> Self {
641        self.depth_op = DepthOp::ReadOnly;
642        self
643    }
644
645    /// Set a debug label for the render pass.
646    pub fn label(mut self, name: impl Into<String>) -> Self {
647        self.label = Some(name.into());
648        self
649    }
650
651    /// Build and return the render pass.
652    ///
653    /// The pass owns its encoder. When dropped, it ends the pass,
654    /// finishes the encoder, and adds the command buffer to the frame.
655    pub fn build(self) -> RenderPass<'f> {
656        profile_function!();
657
658        let label = self.label.clone();
659        let label_str = label.as_deref();
660
661        // Create encoder for this pass
662        let encoder = self
663            .frame
664            .device()
665            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: label_str });
666
667        // Build color attachment
668        let color_view = match self.color_target {
669            ColorTarget::Surface => self.frame.surface_view(),
670            ColorTarget::Custom(v) => v,
671            ColorTarget::Framebuffer(fb) => fb.render_view(),
672        };
673
674        let color_ops = match self.color_op {
675            ColorOp::Clear(color) => wgpu::Operations {
676                load: wgpu::LoadOp::Clear(color),
677                store: wgpu::StoreOp::Store,
678            },
679            ColorOp::Load => wgpu::Operations {
680                load: wgpu::LoadOp::Load,
681                store: wgpu::StoreOp::Store,
682            },
683        };
684
685        let resolve_target = match self.color_target {
686            ColorTarget::Framebuffer(fb) => fb.resolve_target(),
687            _ => None,
688        };
689
690        let color_attachments = [Some(wgpu::RenderPassColorAttachment {
691            view: color_view,
692            resolve_target,
693            ops: color_ops,
694            depth_slice: None,
695        })];
696
697        // Build depth attachment
698        let depth_attachment = self.depth_view.map(|view| {
699            let (depth_ops, read_only) = match self.depth_op {
700                DepthOp::Clear(value) => (
701                    Some(wgpu::Operations {
702                        load: wgpu::LoadOp::Clear(value),
703                        store: wgpu::StoreOp::Store,
704                    }),
705                    false,
706                ),
707                DepthOp::Load => (
708                    Some(wgpu::Operations {
709                        load: wgpu::LoadOp::Load,
710                        store: wgpu::StoreOp::Store,
711                    }),
712                    false,
713                ),
714                DepthOp::ReadOnly => (
715                    Some(wgpu::Operations {
716                        load: wgpu::LoadOp::Load,
717                        store: wgpu::StoreOp::Discard,
718                    }),
719                    true,
720                ),
721            };
722
723            wgpu::RenderPassDepthStencilAttachment {
724                view,
725                depth_ops: if read_only { None } else { depth_ops },
726                stencil_ops: None,
727            }
728        });
729
730        // Increment pass count
731        self.frame.stats.increment_passes();
732
733        // Create the wgpu render pass
734        // We need to keep encoder alive, so we create pass from a separate borrowed encoder
735        let mut encoder = encoder;
736        let pass = encoder
737            .begin_render_pass(&wgpu::RenderPassDescriptor {
738                label: label_str,
739                color_attachments: &color_attachments,
740                depth_stencil_attachment: depth_attachment,
741                timestamp_writes: None,
742                occlusion_query_set: None,
743            })
744            .forget_lifetime();
745
746        RenderPass {
747            frame: self.frame,
748            encoder: Some(encoder),
749            pass: Some(pass),
750            stats: self.frame.stats.clone(),
751            #[cfg(feature = "gpu-profiling")]
752            profiler_scope: None,
753        }
754    }
755}
756
757// ============================================================================
758// RenderPass
759// ============================================================================
760
761/// A render pass that owns its encoder.
762///
763/// When dropped, the render pass:
764/// 1. Ends the wgpu render pass
765/// 2. Finishes the command encoder
766/// 3. Pushes the command buffer to the frame
767///
768/// This design eliminates encoder movement and borrow conflicts.
769pub struct RenderPass<'f> {
770    /// Reference to the frame (for pushing command buffer on drop).
771    frame: &'f Frame<'f>,
772    /// The command encoder (owned by this pass).
773    encoder: Option<wgpu::CommandEncoder>,
774    /// The active wgpu render pass.
775    pass: Option<wgpu::RenderPass<'static>>,
776    /// Atomic stats for draw call counting.
777    stats: Arc<AtomicFrameStats>,
778    /// GPU profiler scope (when gpu-profiling feature is enabled).
779    #[cfg(feature = "gpu-profiling")]
780    profiler_scope: Option<wgpu_profiler::scope::OwningScope>,
781}
782
783impl<'f> RenderPass<'f> {
784    /// Get the underlying wgpu RenderPass (mutable).
785    ///
786    /// # Panics
787    /// Panics if the render pass has already been consumed.
788    pub fn wgpu_pass(&mut self) -> &mut wgpu::RenderPass<'static> {
789        self.pass.as_mut().expect("RenderPass already consumed")
790    }
791
792    /// Get the underlying wgpu RenderPass (immutable).
793    ///
794    /// # Panics
795    /// Panics if the render pass has already been consumed.
796    pub fn wgpu_pass_ref(&self) -> &wgpu::RenderPass<'static> {
797        self.pass.as_ref().expect("RenderPass already consumed")
798    }
799
800    /// Try to get the underlying wgpu RenderPass.
801    pub fn try_wgpu_pass(&mut self) -> Option<&mut wgpu::RenderPass<'static>> {
802        self.pass.as_mut()
803    }
804
805    /// Check if the render pass is still valid.
806    pub fn is_valid(&self) -> bool {
807        self.pass.is_some()
808    }
809
810    /// Get raw access to the pass (alias for wgpu_pass).
811    pub fn raw_pass(&mut self) -> &mut wgpu::RenderPass<'static> {
812        self.wgpu_pass()
813    }
814
815    /// Get the command encoder.
816    pub fn encoder(&self) -> Option<&wgpu::CommandEncoder> {
817        self.encoder.as_ref()
818    }
819
820    /// Get mutable access to the command encoder.
821    pub fn encoder_mut(&mut self) -> Option<&mut wgpu::CommandEncoder> {
822        self.encoder.as_mut()
823    }
824
825    /// Get the graphics context.
826    pub fn graphics(&self) -> &GraphicsContext {
827        self.frame.graphics()
828    }
829
830    /// Record a draw call for statistics.
831    pub fn record_draw_call(&self) {
832        self.stats.increment_draw_calls();
833    }
834
835    /// Consume the pass early and return the encoder for further use.
836    ///
837    /// This ends the render pass but allows the encoder to be used
838    /// for additional commands before submission.
839    pub fn into_encoder(mut self) -> wgpu::CommandEncoder {
840        // End the render pass
841        drop(self.pass.take());
842
843        // Take and return the encoder (skip normal Drop logic)
844        self.encoder.take().expect("Encoder already taken")
845    }
846
847    /// Finish the render pass (called automatically on drop).
848    pub fn finish(self) {
849        drop(self);
850    }
851
852    // =========================================================================
853    // Viewport/Scissor Methods
854    // =========================================================================
855
856    /// Set the viewport using physical coordinates.
857    pub fn set_viewport_physical(
858        &mut self,
859        rect: astrelis_core::geometry::PhysicalRect<f32>,
860        min_depth: f32,
861        max_depth: f32,
862    ) {
863        self.wgpu_pass().set_viewport(
864            rect.x,
865            rect.y,
866            rect.width,
867            rect.height,
868            min_depth,
869            max_depth,
870        );
871    }
872
873    /// Set the viewport using logical coordinates.
874    pub fn set_viewport_logical(
875        &mut self,
876        rect: astrelis_core::geometry::LogicalRect<f32>,
877        min_depth: f32,
878        max_depth: f32,
879        scale: astrelis_core::geometry::ScaleFactor,
880    ) {
881        let physical = rect.to_physical_f32(scale);
882        self.set_viewport_physical(physical, min_depth, max_depth);
883    }
884
885    /// Set the viewport from a Viewport struct.
886    pub fn set_viewport(&mut self, viewport: &crate::Viewport) {
887        self.wgpu_pass().set_viewport(
888            viewport.position.x,
889            viewport.position.y,
890            viewport.size.width,
891            viewport.size.height,
892            0.0,
893            1.0,
894        );
895    }
896
897    /// Set the scissor rectangle using physical coordinates.
898    pub fn set_scissor_physical(&mut self, rect: astrelis_core::geometry::PhysicalRect<u32>) {
899        self.wgpu_pass()
900            .set_scissor_rect(rect.x, rect.y, rect.width, rect.height);
901    }
902
903    /// Set the scissor rectangle using logical coordinates.
904    pub fn set_scissor_logical(
905        &mut self,
906        rect: astrelis_core::geometry::LogicalRect<f32>,
907        scale: astrelis_core::geometry::ScaleFactor,
908    ) {
909        let physical = rect.to_physical(scale);
910        self.set_scissor_physical(physical);
911    }
912
913    // =========================================================================
914    // Drawing Methods
915    // =========================================================================
916
917    /// Set the pipeline.
918    pub fn set_pipeline(&mut self, pipeline: &wgpu::RenderPipeline) {
919        self.wgpu_pass().set_pipeline(pipeline);
920    }
921
922    /// Set a bind group.
923    pub fn set_bind_group(&mut self, index: u32, bind_group: &wgpu::BindGroup, offsets: &[u32]) {
924        self.wgpu_pass().set_bind_group(index, bind_group, offsets);
925    }
926
927    /// Set a vertex buffer.
928    pub fn set_vertex_buffer(&mut self, slot: u32, buffer_slice: wgpu::BufferSlice<'_>) {
929        self.wgpu_pass().set_vertex_buffer(slot, buffer_slice);
930    }
931
932    /// Set the index buffer.
933    pub fn set_index_buffer(
934        &mut self,
935        buffer_slice: wgpu::BufferSlice<'_>,
936        format: wgpu::IndexFormat,
937    ) {
938        self.wgpu_pass().set_index_buffer(buffer_slice, format);
939    }
940
941    /// Draw primitives.
942    pub fn draw(&mut self, vertices: std::ops::Range<u32>, instances: std::ops::Range<u32>) {
943        self.wgpu_pass().draw(vertices, instances);
944        self.stats.increment_draw_calls();
945    }
946
947    /// Draw indexed primitives.
948    pub fn draw_indexed(
949        &mut self,
950        indices: std::ops::Range<u32>,
951        base_vertex: i32,
952        instances: std::ops::Range<u32>,
953    ) {
954        self.wgpu_pass()
955            .draw_indexed(indices, base_vertex, instances);
956        self.stats.increment_draw_calls();
957    }
958
959    /// Insert a debug marker.
960    pub fn insert_debug_marker(&mut self, label: &str) {
961        self.wgpu_pass().insert_debug_marker(label);
962    }
963
964    /// Push a debug group.
965    pub fn push_debug_group(&mut self, label: &str) {
966        self.wgpu_pass().push_debug_group(label);
967    }
968
969    /// Pop a debug group.
970    pub fn pop_debug_group(&mut self) {
971        self.wgpu_pass().pop_debug_group();
972    }
973
974    // =========================================================================
975    // Push Constants
976    // =========================================================================
977
978    /// Set push constants.
979    pub fn set_push_constants<T: bytemuck::Pod>(
980        &mut self,
981        stages: wgpu::ShaderStages,
982        offset: u32,
983        data: &T,
984    ) {
985        self.wgpu_pass()
986            .set_push_constants(stages, offset, bytemuck::bytes_of(data));
987    }
988
989    /// Set push constants from raw bytes.
990    pub fn set_push_constants_raw(&mut self, stages: wgpu::ShaderStages, offset: u32, data: &[u8]) {
991        self.wgpu_pass().set_push_constants(stages, offset, data);
992    }
993}
994
995impl Drop for RenderPass<'_> {
996    fn drop(&mut self) {
997        profile_function!();
998
999        // Drop GPU profiler scope first (ends timing)
1000        #[cfg(feature = "gpu-profiling")]
1001        drop(self.profiler_scope.take());
1002
1003        // End the render pass
1004        drop(self.pass.take());
1005
1006        // Finish encoder and push command buffer to frame
1007        if let Some(encoder) = self.encoder.take() {
1008            let command_buffer = encoder.finish();
1009            self.frame.command_buffers.borrow_mut().push(command_buffer);
1010        }
1011    }
1012}
1013
1014// ============================================================================
1015// Backwards Compatibility Types
1016// ============================================================================
1017
1018/// Clear operation for a render pass.
1019#[derive(Debug, Clone, Copy, Default)]
1020pub enum ClearOp {
1021    /// Load existing contents (no clear).
1022    #[default]
1023    Load,
1024    /// Clear to the specified color.
1025    Clear(wgpu::Color),
1026}
1027
1028impl From<wgpu::Color> for ClearOp {
1029    fn from(color: wgpu::Color) -> Self {
1030        ClearOp::Clear(color)
1031    }
1032}
1033
1034impl From<Color> for ClearOp {
1035    fn from(color: Color) -> Self {
1036        ClearOp::Clear(color.to_wgpu())
1037    }
1038}
1039
1040/// Depth clear operation for a render pass.
1041#[derive(Debug, Clone, Copy)]
1042pub enum DepthClearOp {
1043    /// Load existing depth values.
1044    Load,
1045    /// Clear to the specified depth value (typically 1.0).
1046    Clear(f32),
1047}
1048
1049impl Default for DepthClearOp {
1050    fn default() -> Self {
1051        DepthClearOp::Clear(1.0)
1052    }
1053}
1054
1055// ============================================================================
1056// Legacy Compatibility
1057// ============================================================================
1058
1059/// Deprecated alias for backwards compatibility.
1060#[deprecated(since = "0.2.0", note = "Use Frame instead")]
1061pub type FrameContext = Frame<'static>;