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 GPU profiler if attached.
263    pub fn gpu_profiler(&self) -> Option<&GpuFrameProfiler> {
264        self.gpu_profiler.as_deref()
265    }
266
267    /// Check if GPU profiling is active.
268    pub fn has_gpu_profiler(&self) -> bool {
269        self.gpu_profiler.is_some()
270    }
271
272    /// Create a command encoder for custom command recording.
273    ///
274    /// Use this for operations that don't fit the render pass model,
275    /// like buffer copies or texture uploads.
276    pub fn create_encoder(&self, label: Option<&str>) -> wgpu::CommandEncoder {
277        self.device()
278            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label })
279    }
280
281    /// Add a pre-built command buffer to the frame.
282    ///
283    /// Use this when you have custom command recording logic.
284    pub fn add_command_buffer(&self, buffer: wgpu::CommandBuffer) {
285        self.command_buffers.borrow_mut().push(buffer);
286    }
287
288    /// Start building a render pass.
289    ///
290    /// Returns a builder that can be configured with target, clear operations,
291    /// and depth settings before building the actual pass.
292    pub fn render_pass(&self) -> RenderPassBuilder<'_, 'w> {
293        RenderPassBuilder::new(self)
294    }
295
296    /// Start building a compute pass.
297    pub fn compute_pass(&self) -> crate::compute::ComputePassBuilder<'_, 'w> {
298        crate::compute::ComputePassBuilder::new(self)
299    }
300
301    /// Submit all collected command buffers and present the surface.
302    ///
303    /// This is called automatically on drop, but can be called explicitly
304    /// for more control over timing.
305    pub fn submit(self) {
306        // Move self to trigger drop which handles submission
307        drop(self);
308    }
309
310    /// Internal submit implementation called by Drop.
311    fn submit_inner(&self) {
312        profile_function!();
313
314        if self.stats.passes() == 0 {
315            tracing::warn!("No render passes were executed for this frame");
316        }
317
318        // Resolve GPU profiler queries before submitting
319        if let Some(ref profiler) = self.gpu_profiler {
320            // Create a dedicated encoder for query resolution
321            let mut resolve_encoder = self.create_encoder(Some("Profiler Resolve"));
322            profiler.resolve_queries(&mut resolve_encoder);
323            self.command_buffers
324                .borrow_mut()
325                .push(resolve_encoder.finish());
326        }
327
328        // Take all command buffers
329        let buffers = std::mem::take(&mut *self.command_buffers.borrow_mut());
330
331        if !buffers.is_empty() {
332            profile_scope!("submit_commands");
333            self.queue().submit(buffers);
334        }
335
336        // Present surface
337        if let Some(_surface) = self.surface.as_ref() {
338            profile_scope!("present_surface");
339            // Note: We can't take() the surface since self is borrowed, but present
340            // doesn't consume it - it just signals we're done with this frame
341        }
342
343        // End GPU profiler frame
344        if let Some(ref profiler) = self.gpu_profiler
345            && let Err(e) = profiler.end_frame()
346        {
347            tracing::warn!("GPU profiler end_frame error: {e:?}");
348        }
349    }
350}
351
352impl Drop for Frame<'_> {
353    fn drop(&mut self) {
354        if !self.submitted.get() {
355            self.submitted.set(true);
356            self.submit_inner();
357        }
358
359        // Present surface
360        if let Some(surface) = self.surface.take() {
361            profile_scope!("present_surface");
362            surface.texture.present();
363        }
364
365        // Request redraw
366        self.winit_window.request_redraw();
367    }
368}
369
370// ============================================================================
371// RenderPassBuilder
372// ============================================================================
373
374/// Target for color attachment in render passes.
375#[derive(Debug, Clone, Copy, Default)]
376pub enum ColorTarget<'a> {
377    /// Render to the window surface.
378    #[default]
379    Surface,
380    /// Render to a custom texture view.
381    Custom(&'a wgpu::TextureView),
382    /// Render to a framebuffer.
383    Framebuffer(&'a Framebuffer),
384}
385
386/// Color operation for render pass.
387#[derive(Debug, Clone, Copy, Default)]
388pub enum ColorOp {
389    /// Clear to the specified color.
390    Clear(wgpu::Color),
391    /// Load existing contents.
392    #[default]
393    Load,
394}
395
396impl From<Color> for ColorOp {
397    fn from(color: Color) -> Self {
398        Self::Clear(color.to_wgpu())
399    }
400}
401
402impl From<wgpu::Color> for ColorOp {
403    fn from(color: wgpu::Color) -> Self {
404        Self::Clear(color)
405    }
406}
407
408/// Depth operation for render pass.
409#[derive(Debug, Clone, Copy)]
410pub enum DepthOp {
411    /// Clear to the specified value.
412    Clear(f32),
413    /// Load existing values.
414    Load,
415    /// Read-only depth (no writes).
416    ReadOnly,
417}
418
419impl Default for DepthOp {
420    fn default() -> Self {
421        Self::Clear(1.0)
422    }
423}
424
425/// Builder for creating render passes with fluent API.
426///
427/// # Example
428///
429/// ```rust,no_run
430/// # use astrelis_render::{Frame, Color};
431/// # let frame: &Frame = todo!();
432/// let mut pass = frame.render_pass()
433///     .clear_color(Color::BLACK)
434///     .clear_depth(0.0)
435///     .label("main")
436///     .build();
437///
438/// // Use pass.wgpu_pass() for rendering
439/// ```
440pub struct RenderPassBuilder<'f, 'w> {
441    frame: &'f Frame<'w>,
442    color_target: ColorTarget<'f>,
443    color_op: ColorOp,
444    depth_view: Option<&'f wgpu::TextureView>,
445    depth_op: DepthOp,
446    label: Option<String>,
447}
448
449impl<'f, 'w> RenderPassBuilder<'f, 'w> {
450    /// Create a new render pass builder.
451    pub(crate) fn new(frame: &'f Frame<'w>) -> Self {
452        Self {
453            frame,
454            color_target: ColorTarget::Surface,
455            color_op: ColorOp::Load,
456            depth_view: None,
457            depth_op: DepthOp::default(),
458            label: None,
459        }
460    }
461
462    /// Set the render target (for backwards compatibility).
463    pub fn target(mut self, target: RenderTarget<'f>) -> Self {
464        match target {
465            RenderTarget::Surface => {
466                self.color_target = ColorTarget::Surface;
467            }
468            RenderTarget::SurfaceWithDepth {
469                depth_view,
470                clear_value,
471            } => {
472                self.color_target = ColorTarget::Surface;
473                self.depth_view = Some(depth_view);
474                if let Some(v) = clear_value {
475                    self.depth_op = DepthOp::Clear(v);
476                } else {
477                    self.depth_op = DepthOp::Load;
478                }
479            }
480            RenderTarget::Framebuffer(fb) => {
481                self.color_target = ColorTarget::Framebuffer(fb);
482                if let Some(dv) = fb.depth_view() {
483                    self.depth_view = Some(dv);
484                }
485            }
486        }
487        self
488    }
489
490    /// Render to the window surface (default).
491    pub fn to_surface(mut self) -> Self {
492        self.color_target = ColorTarget::Surface;
493        self
494    }
495
496    /// Render to a framebuffer.
497    pub fn to_framebuffer(mut self, fb: &'f Framebuffer) -> Self {
498        self.color_target = ColorTarget::Framebuffer(fb);
499        if let Some(dv) = fb.depth_view() {
500            self.depth_view = Some(dv);
501        }
502        self
503    }
504
505    /// Render to a custom texture view.
506    pub fn to_texture(mut self, view: &'f wgpu::TextureView) -> Self {
507        self.color_target = ColorTarget::Custom(view);
508        self
509    }
510
511    /// Clear the color target to the specified color.
512    pub fn clear_color(mut self, color: impl Into<ColorOp>) -> Self {
513        self.color_op = color.into();
514        self
515    }
516
517    /// Load existing color contents (default).
518    pub fn load_color(mut self) -> Self {
519        self.color_op = ColorOp::Load;
520        self
521    }
522
523    /// Set the depth attachment.
524    pub fn depth_attachment(mut self, view: &'f wgpu::TextureView) -> Self {
525        self.depth_view = Some(view);
526        self
527    }
528
529    /// Use the window's depth buffer automatically.
530    ///
531    /// # Panics
532    /// Panics if the window doesn't have a depth buffer.
533    pub fn with_window_depth(mut self) -> Self {
534        self.depth_view = Some(
535            self.frame
536                .depth_view()
537                .expect("Window must have depth buffer for with_window_depth()"),
538        );
539        self
540    }
541
542    /// Use the window's depth buffer if available.
543    pub fn with_window_depth_if_available(mut self) -> Self {
544        if let Some(dv) = self.frame.depth_view() {
545            self.depth_view = Some(dv);
546        }
547        self
548    }
549
550    /// Clear the depth buffer to the specified value.
551    pub fn clear_depth(mut self, value: f32) -> Self {
552        self.depth_op = DepthOp::Clear(value);
553        self
554    }
555
556    /// Load existing depth values.
557    pub fn load_depth(mut self) -> Self {
558        self.depth_op = DepthOp::Load;
559        self
560    }
561
562    /// Use depth in read-only mode (no writes).
563    pub fn depth_readonly(mut self) -> Self {
564        self.depth_op = DepthOp::ReadOnly;
565        self
566    }
567
568    /// Set a debug label for the render pass.
569    pub fn label(mut self, name: impl Into<String>) -> Self {
570        self.label = Some(name.into());
571        self
572    }
573
574    /// Build and return the render pass.
575    ///
576    /// The pass owns its encoder. When dropped, it ends the pass,
577    /// finishes the encoder, and adds the command buffer to the frame.
578    pub fn build(self) -> RenderPass<'f> {
579        profile_function!();
580
581        let label = self.label.clone();
582        let label_str = label.as_deref();
583
584        // Create encoder for this pass
585        let encoder = self
586            .frame
587            .device()
588            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: label_str });
589
590        // Build color attachment
591        let color_view = match self.color_target {
592            ColorTarget::Surface => self.frame.surface_view(),
593            ColorTarget::Custom(v) => v,
594            ColorTarget::Framebuffer(fb) => fb.render_view(),
595        };
596
597        let color_ops = match self.color_op {
598            ColorOp::Clear(color) => wgpu::Operations {
599                load: wgpu::LoadOp::Clear(color),
600                store: wgpu::StoreOp::Store,
601            },
602            ColorOp::Load => wgpu::Operations {
603                load: wgpu::LoadOp::Load,
604                store: wgpu::StoreOp::Store,
605            },
606        };
607
608        let resolve_target = match self.color_target {
609            ColorTarget::Framebuffer(fb) => fb.resolve_target(),
610            _ => None,
611        };
612
613        let color_attachments = [Some(wgpu::RenderPassColorAttachment {
614            view: color_view,
615            resolve_target,
616            ops: color_ops,
617            depth_slice: None,
618        })];
619
620        // Build depth attachment
621        let depth_attachment = self.depth_view.map(|view| {
622            let (depth_ops, read_only) = match self.depth_op {
623                DepthOp::Clear(value) => (
624                    Some(wgpu::Operations {
625                        load: wgpu::LoadOp::Clear(value),
626                        store: wgpu::StoreOp::Store,
627                    }),
628                    false,
629                ),
630                DepthOp::Load => (
631                    Some(wgpu::Operations {
632                        load: wgpu::LoadOp::Load,
633                        store: wgpu::StoreOp::Store,
634                    }),
635                    false,
636                ),
637                DepthOp::ReadOnly => (
638                    Some(wgpu::Operations {
639                        load: wgpu::LoadOp::Load,
640                        store: wgpu::StoreOp::Discard,
641                    }),
642                    true,
643                ),
644            };
645
646            wgpu::RenderPassDepthStencilAttachment {
647                view,
648                depth_ops: if read_only { None } else { depth_ops },
649                stencil_ops: None,
650            }
651        });
652
653        // Increment pass count
654        self.frame.stats.increment_passes();
655
656        // Create the wgpu render pass
657        // We need to keep encoder alive, so we create pass from a separate borrowed encoder
658        let mut encoder = encoder;
659        let pass = encoder
660            .begin_render_pass(&wgpu::RenderPassDescriptor {
661                label: label_str,
662                color_attachments: &color_attachments,
663                depth_stencil_attachment: depth_attachment,
664                timestamp_writes: None,
665                occlusion_query_set: None,
666            })
667            .forget_lifetime();
668
669        RenderPass {
670            frame: self.frame,
671            encoder: Some(encoder),
672            pass: Some(pass),
673            stats: self.frame.stats.clone(),
674            #[cfg(feature = "gpu-profiling")]
675            profiler_scope: None,
676        }
677    }
678}
679
680// ============================================================================
681// RenderPass
682// ============================================================================
683
684/// A render pass that owns its encoder.
685///
686/// When dropped, the render pass:
687/// 1. Ends the wgpu render pass
688/// 2. Finishes the command encoder
689/// 3. Pushes the command buffer to the frame
690///
691/// This design eliminates encoder movement and borrow conflicts.
692pub struct RenderPass<'f> {
693    /// Reference to the frame (for pushing command buffer on drop).
694    frame: &'f Frame<'f>,
695    /// The command encoder (owned by this pass).
696    encoder: Option<wgpu::CommandEncoder>,
697    /// The active wgpu render pass.
698    pass: Option<wgpu::RenderPass<'static>>,
699    /// Atomic stats for draw call counting.
700    stats: Arc<AtomicFrameStats>,
701    /// GPU profiler scope (when gpu-profiling feature is enabled).
702    #[cfg(feature = "gpu-profiling")]
703    profiler_scope: Option<wgpu_profiler::scope::OwningScope>,
704}
705
706impl<'f> RenderPass<'f> {
707    /// Get the underlying wgpu RenderPass (mutable).
708    ///
709    /// # Panics
710    /// Panics if the render pass has already been consumed.
711    pub fn wgpu_pass(&mut self) -> &mut wgpu::RenderPass<'static> {
712        self.pass.as_mut().expect("RenderPass already consumed")
713    }
714
715    /// Get the underlying wgpu RenderPass (immutable).
716    ///
717    /// # Panics
718    /// Panics if the render pass has already been consumed.
719    pub fn wgpu_pass_ref(&self) -> &wgpu::RenderPass<'static> {
720        self.pass.as_ref().expect("RenderPass already consumed")
721    }
722
723    /// Try to get the underlying wgpu RenderPass.
724    pub fn try_wgpu_pass(&mut self) -> Option<&mut wgpu::RenderPass<'static>> {
725        self.pass.as_mut()
726    }
727
728    /// Check if the render pass is still valid.
729    pub fn is_valid(&self) -> bool {
730        self.pass.is_some()
731    }
732
733    /// Get raw access to the pass (alias for wgpu_pass).
734    pub fn raw_pass(&mut self) -> &mut wgpu::RenderPass<'static> {
735        self.wgpu_pass()
736    }
737
738    /// Get the command encoder.
739    pub fn encoder(&self) -> Option<&wgpu::CommandEncoder> {
740        self.encoder.as_ref()
741    }
742
743    /// Get mutable access to the command encoder.
744    pub fn encoder_mut(&mut self) -> Option<&mut wgpu::CommandEncoder> {
745        self.encoder.as_mut()
746    }
747
748    /// Get the graphics context.
749    pub fn graphics(&self) -> &GraphicsContext {
750        self.frame.graphics()
751    }
752
753    /// Record a draw call for statistics.
754    pub fn record_draw_call(&self) {
755        self.stats.increment_draw_calls();
756    }
757
758    /// Consume the pass early and return the encoder for further use.
759    ///
760    /// This ends the render pass but allows the encoder to be used
761    /// for additional commands before submission.
762    pub fn into_encoder(mut self) -> wgpu::CommandEncoder {
763        // End the render pass
764        drop(self.pass.take());
765
766        // Take and return the encoder (skip normal Drop logic)
767        self.encoder.take().expect("Encoder already taken")
768    }
769
770    /// Finish the render pass (called automatically on drop).
771    pub fn finish(self) {
772        drop(self);
773    }
774
775    // =========================================================================
776    // Viewport/Scissor Methods
777    // =========================================================================
778
779    /// Set the viewport using physical coordinates.
780    pub fn set_viewport_physical(
781        &mut self,
782        rect: astrelis_core::geometry::PhysicalRect<f32>,
783        min_depth: f32,
784        max_depth: f32,
785    ) {
786        self.wgpu_pass().set_viewport(
787            rect.x,
788            rect.y,
789            rect.width,
790            rect.height,
791            min_depth,
792            max_depth,
793        );
794    }
795
796    /// Set the viewport using logical coordinates.
797    pub fn set_viewport_logical(
798        &mut self,
799        rect: astrelis_core::geometry::LogicalRect<f32>,
800        min_depth: f32,
801        max_depth: f32,
802        scale: astrelis_core::geometry::ScaleFactor,
803    ) {
804        let physical = rect.to_physical_f32(scale);
805        self.set_viewport_physical(physical, min_depth, max_depth);
806    }
807
808    /// Set the viewport from a Viewport struct.
809    pub fn set_viewport(&mut self, viewport: &crate::Viewport) {
810        self.wgpu_pass().set_viewport(
811            viewport.position.x,
812            viewport.position.y,
813            viewport.size.width,
814            viewport.size.height,
815            0.0,
816            1.0,
817        );
818    }
819
820    /// Set the scissor rectangle using physical coordinates.
821    pub fn set_scissor_physical(&mut self, rect: astrelis_core::geometry::PhysicalRect<u32>) {
822        self.wgpu_pass()
823            .set_scissor_rect(rect.x, rect.y, rect.width, rect.height);
824    }
825
826    /// Set the scissor rectangle using logical coordinates.
827    pub fn set_scissor_logical(
828        &mut self,
829        rect: astrelis_core::geometry::LogicalRect<f32>,
830        scale: astrelis_core::geometry::ScaleFactor,
831    ) {
832        let physical = rect.to_physical(scale);
833        self.set_scissor_physical(physical);
834    }
835
836    // =========================================================================
837    // Drawing Methods
838    // =========================================================================
839
840    /// Set the pipeline.
841    pub fn set_pipeline(&mut self, pipeline: &wgpu::RenderPipeline) {
842        self.wgpu_pass().set_pipeline(pipeline);
843    }
844
845    /// Set a bind group.
846    pub fn set_bind_group(&mut self, index: u32, bind_group: &wgpu::BindGroup, offsets: &[u32]) {
847        self.wgpu_pass().set_bind_group(index, bind_group, offsets);
848    }
849
850    /// Set a vertex buffer.
851    pub fn set_vertex_buffer(&mut self, slot: u32, buffer_slice: wgpu::BufferSlice<'_>) {
852        self.wgpu_pass().set_vertex_buffer(slot, buffer_slice);
853    }
854
855    /// Set the index buffer.
856    pub fn set_index_buffer(
857        &mut self,
858        buffer_slice: wgpu::BufferSlice<'_>,
859        format: wgpu::IndexFormat,
860    ) {
861        self.wgpu_pass().set_index_buffer(buffer_slice, format);
862    }
863
864    /// Draw primitives.
865    pub fn draw(&mut self, vertices: std::ops::Range<u32>, instances: std::ops::Range<u32>) {
866        self.wgpu_pass().draw(vertices, instances);
867        self.stats.increment_draw_calls();
868    }
869
870    /// Draw indexed primitives.
871    pub fn draw_indexed(
872        &mut self,
873        indices: std::ops::Range<u32>,
874        base_vertex: i32,
875        instances: std::ops::Range<u32>,
876    ) {
877        self.wgpu_pass()
878            .draw_indexed(indices, base_vertex, instances);
879        self.stats.increment_draw_calls();
880    }
881
882    /// Insert a debug marker.
883    pub fn insert_debug_marker(&mut self, label: &str) {
884        self.wgpu_pass().insert_debug_marker(label);
885    }
886
887    /// Push a debug group.
888    pub fn push_debug_group(&mut self, label: &str) {
889        self.wgpu_pass().push_debug_group(label);
890    }
891
892    /// Pop a debug group.
893    pub fn pop_debug_group(&mut self) {
894        self.wgpu_pass().pop_debug_group();
895    }
896
897    // =========================================================================
898    // Push Constants
899    // =========================================================================
900
901    /// Set push constants.
902    pub fn set_push_constants<T: bytemuck::Pod>(
903        &mut self,
904        stages: wgpu::ShaderStages,
905        offset: u32,
906        data: &T,
907    ) {
908        self.wgpu_pass()
909            .set_push_constants(stages, offset, bytemuck::bytes_of(data));
910    }
911
912    /// Set push constants from raw bytes.
913    pub fn set_push_constants_raw(&mut self, stages: wgpu::ShaderStages, offset: u32, data: &[u8]) {
914        self.wgpu_pass().set_push_constants(stages, offset, data);
915    }
916}
917
918impl Drop for RenderPass<'_> {
919    fn drop(&mut self) {
920        profile_function!();
921
922        // Drop GPU profiler scope first (ends timing)
923        #[cfg(feature = "gpu-profiling")]
924        drop(self.profiler_scope.take());
925
926        // End the render pass
927        drop(self.pass.take());
928
929        // Finish encoder and push command buffer to frame
930        if let Some(encoder) = self.encoder.take() {
931            let command_buffer = encoder.finish();
932            self.frame.command_buffers.borrow_mut().push(command_buffer);
933        }
934    }
935}
936
937// ============================================================================
938// Backwards Compatibility Types
939// ============================================================================
940
941/// Clear operation for a render pass.
942#[derive(Debug, Clone, Copy, Default)]
943pub enum ClearOp {
944    /// Load existing contents (no clear).
945    #[default]
946    Load,
947    /// Clear to the specified color.
948    Clear(wgpu::Color),
949}
950
951impl From<wgpu::Color> for ClearOp {
952    fn from(color: wgpu::Color) -> Self {
953        ClearOp::Clear(color)
954    }
955}
956
957impl From<Color> for ClearOp {
958    fn from(color: Color) -> Self {
959        ClearOp::Clear(color.to_wgpu())
960    }
961}
962
963/// Depth clear operation for a render pass.
964#[derive(Debug, Clone, Copy)]
965pub enum DepthClearOp {
966    /// Load existing depth values.
967    Load,
968    /// Clear to the specified depth value (typically 1.0).
969    Clear(f32),
970}
971
972impl Default for DepthClearOp {
973    fn default() -> Self {
974        DepthClearOp::Clear(1.0)
975    }
976}