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