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>;