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