astrelis_render/
frame.rs

1use std::sync::Arc;
2
3use astrelis_core::profiling::{profile_function, profile_scope};
4use astrelis_winit::window::WinitWindow;
5
6use crate::context::GraphicsContext;
7use crate::target::RenderTarget;
8
9/// Statistics for a rendered frame.
10pub struct FrameStats {
11    pub passes: usize,
12    pub draw_calls: usize,
13}
14
15impl FrameStats {
16    pub(crate) fn new() -> Self {
17        Self {
18            passes: 0,
19            draw_calls: 0,
20        }
21    }
22}
23
24/// Surface texture and view for rendering.
25pub struct Surface {
26    pub(crate) texture: wgpu::SurfaceTexture,
27    pub(crate) view: wgpu::TextureView,
28}
29
30impl Surface {
31    pub fn texture(&self) -> &wgpu::Texture {
32        &self.texture.texture
33    }
34
35    pub fn view(&self) -> &wgpu::TextureView {
36        &self.view
37    }
38}
39
40/// Context for a single frame of rendering.
41pub struct FrameContext {
42    pub(crate) stats: FrameStats,
43    pub(crate) surface: Option<Surface>,
44    pub(crate) encoder: Option<wgpu::CommandEncoder>,
45    pub context: Arc<GraphicsContext>,
46    pub(crate) window: Arc<WinitWindow>,
47    pub(crate) surface_format: wgpu::TextureFormat,
48}
49
50impl FrameContext {
51    pub fn surface(&self) -> &Surface {
52        self.surface.as_ref().unwrap()
53    }
54
55    pub fn surface_format(&self) -> wgpu::TextureFormat {
56        self.surface_format
57    }
58
59    pub fn increment_passes(&mut self) {
60        self.stats.passes += 1;
61    }
62
63    pub fn increment_draw_calls(&mut self) {
64        self.stats.draw_calls += 1;
65    }
66
67    pub fn stats(&self) -> &FrameStats {
68        &self.stats
69    }
70
71    pub fn graphics_context(&self) -> &GraphicsContext {
72        &self.context
73    }
74
75    pub fn encoder(&mut self) -> &mut wgpu::CommandEncoder {
76        self.encoder.as_mut().expect("Encoder already taken")
77    }
78
79    pub fn encoder_and_surface(&mut self) -> (&mut wgpu::CommandEncoder, &Surface) {
80        (
81            self.encoder.as_mut().expect("Encoder already taken"),
82            self.surface.as_ref().unwrap(),
83        )
84    }
85
86    pub fn finish(self) {
87        drop(self);
88    }
89
90    /// Execute a closure with a render pass, automatically handling scoping.
91    ///
92    /// This is the ergonomic RAII pattern that eliminates the need for manual `{ }` blocks.
93    /// The render pass is automatically dropped after the closure completes.
94    ///
95    /// # Example
96    /// ```rust,no_run
97    /// # use astrelis_render::*;
98    /// # let mut frame: FrameContext = todo!();
99    /// frame.with_pass(
100    ///     RenderPassBuilder::new()
101    ///         .target(RenderTarget::Surface)
102    ///         .clear_color(Color::BLACK),
103    ///     |pass| {
104    ///         // Render commands here
105    ///         // pass automatically drops when closure ends
106    ///     }
107    /// );
108    /// frame.finish();
109    /// ```
110    pub fn with_pass<'a, F>(&'a mut self, builder: RenderPassBuilder<'a>, f: F)
111    where
112        F: FnOnce(&mut RenderPass<'a>),
113    {
114        let mut pass = builder.build(self);
115        f(&mut pass);
116        // pass drops here automatically
117    }
118
119    /// Convenience method to clear to a color and execute rendering commands.
120    ///
121    /// This is the most common pattern - clear the surface and render.
122    ///
123    /// # Example
124    /// ```rust,no_run
125    /// # use astrelis_render::*;
126    /// # let mut frame: FrameContext = todo!();
127    /// frame.clear_and_render(
128    ///     RenderTarget::Surface,
129    ///     Color::BLACK,
130    ///     |pass| {
131    ///         // Render your content here
132    ///         // Example: ui.render(pass.descriptor());
133    ///     }
134    /// );
135    /// frame.finish();
136    /// ```
137    pub fn clear_and_render<'a, F>(
138        &'a mut self,
139        target: RenderTarget<'a>,
140        clear_color: impl Into<crate::Color>,
141        f: F,
142    ) where
143        F: FnOnce(&mut RenderPass<'a>),
144    {
145        self.with_pass(
146            RenderPassBuilder::new()
147                .target(target)
148                .clear_color(clear_color.into()),
149            f,
150        );
151    }
152}
153
154impl Drop for FrameContext {
155    fn drop(&mut self) {
156        profile_function!();
157
158        if self.stats.passes == 0 {
159            tracing::error!("No render passes were executed for this frame!");
160            return;
161        }
162
163        if let Some(encoder) = self.encoder.take() {
164            profile_scope!("submit_commands");
165            self.context.queue.submit(std::iter::once(encoder.finish()));
166        }
167
168        if let Some(surface) = self.surface.take() {
169            profile_scope!("present_surface");
170            surface.texture.present();
171        }
172
173        // Request redraw for next frame
174        self.window.request_redraw();
175    }
176}
177
178/// Clear operation for a render pass.
179#[derive(Debug, Clone, Copy)]
180#[derive(Default)]
181pub enum ClearOp {
182    /// Load existing contents (no clear).
183    #[default]
184    Load,
185    /// Clear to the specified color.
186    Clear(wgpu::Color),
187}
188
189
190impl From<wgpu::Color> for ClearOp {
191    fn from(color: wgpu::Color) -> Self {
192        ClearOp::Clear(color)
193    }
194}
195
196impl From<crate::Color> for ClearOp {
197    fn from(color: crate::Color) -> Self {
198        ClearOp::Clear(color.to_wgpu())
199    }
200}
201
202/// Depth clear operation for a render pass.
203#[derive(Debug, Clone, Copy)]
204pub enum DepthClearOp {
205    /// Load existing depth values.
206    Load,
207    /// Clear to the specified depth value (typically 1.0).
208    Clear(f32),
209}
210
211impl Default for DepthClearOp {
212    fn default() -> Self {
213        DepthClearOp::Clear(1.0)
214    }
215}
216
217/// Builder for creating render passes.
218pub struct RenderPassBuilder<'a> {
219    label: Option<&'a str>,
220    // New simplified API
221    target: Option<RenderTarget<'a>>,
222    clear_op: ClearOp,
223    depth_clear_op: DepthClearOp,
224    // Legacy API for advanced use
225    color_attachments: Vec<Option<wgpu::RenderPassColorAttachment<'a>>>,
226    surface_attachment_ops: Option<(wgpu::Operations<wgpu::Color>, Option<&'a wgpu::TextureView>)>,
227    depth_stencil_attachment: Option<wgpu::RenderPassDepthStencilAttachment<'a>>,
228}
229
230impl<'a> RenderPassBuilder<'a> {
231    pub fn new() -> Self {
232        Self {
233            label: None,
234            target: None,
235            clear_op: ClearOp::Load,
236            depth_clear_op: DepthClearOp::default(),
237            color_attachments: Vec::new(),
238            surface_attachment_ops: None,
239            depth_stencil_attachment: None,
240        }
241    }
242
243    /// Set a debug label for the render pass.
244    pub fn label(mut self, label: &'a str) -> Self {
245        self.label = Some(label);
246        self
247    }
248
249    /// Set the render target (Surface or Framebuffer).
250    ///
251    /// This is the simplified API - use this instead of manual color_attachment calls.
252    pub fn target(mut self, target: RenderTarget<'a>) -> Self {
253        self.target = Some(target);
254        self
255    }
256
257    /// Set clear color for the render target.
258    ///
259    /// Pass a wgpu::Color or use ClearOp::Load to preserve existing contents.
260    pub fn clear_color(mut self, color: impl Into<ClearOp>) -> Self {
261        self.clear_op = color.into();
262        self
263    }
264
265    /// Set depth clear operation.
266    pub fn clear_depth(mut self, depth: f32) -> Self {
267        self.depth_clear_op = DepthClearOp::Clear(depth);
268        self
269    }
270
271    /// Load existing depth values instead of clearing.
272    pub fn load_depth(mut self) -> Self {
273        self.depth_clear_op = DepthClearOp::Load;
274        self
275    }
276
277    // Legacy API for advanced use cases
278
279    /// Add a color attachment manually (advanced API).
280    ///
281    /// For most cases, use `.target()` instead.
282    pub fn color_attachment(
283        mut self,
284        view: Option<&'a wgpu::TextureView>,
285        resolve_target: Option<&'a wgpu::TextureView>,
286        ops: wgpu::Operations<wgpu::Color>,
287    ) -> Self {
288        if let Some(view) = view {
289            self.color_attachments
290                .push(Some(wgpu::RenderPassColorAttachment {
291                    view,
292                    resolve_target,
293                    ops,
294                    depth_slice: None,
295                }));
296        } else {
297            // Store ops for later - will be filled with surface view in build()
298            self.surface_attachment_ops = Some((ops, resolve_target));
299        }
300        self
301    }
302
303    /// Add a depth-stencil attachment manually (advanced API).
304    ///
305    /// For framebuffers with depth, the depth attachment is handled automatically
306    /// when using `.target()`.
307    pub fn depth_stencil_attachment(
308        mut self,
309        view: &'a wgpu::TextureView,
310        depth_ops: Option<wgpu::Operations<f32>>,
311        stencil_ops: Option<wgpu::Operations<u32>>,
312    ) -> Self {
313        self.depth_stencil_attachment = Some(wgpu::RenderPassDepthStencilAttachment {
314            view,
315            depth_ops,
316            stencil_ops,
317        });
318        self
319    }
320
321    /// Builds the render pass and begins it on the provided frame context.
322    ///
323    /// This takes ownership of the CommandEncoder from the FrameContext, and releases it
324    /// back to the FrameContext when the RenderPass is dropped or [`finish`](RenderPass::finish)
325    /// is called.
326    pub fn build(self, frame_context: &'a mut FrameContext) -> RenderPass<'a> {
327        let mut encoder = frame_context.encoder.take().unwrap();
328
329        // Build color attachments based on target or legacy API
330        let mut all_attachments = Vec::new();
331
332        if let Some(target) = &self.target {
333            // New simplified API
334            let color_ops = match self.clear_op {
335                ClearOp::Load => wgpu::Operations {
336                    load: wgpu::LoadOp::Load,
337                    store: wgpu::StoreOp::Store,
338                },
339                ClearOp::Clear(color) => wgpu::Operations {
340                    load: wgpu::LoadOp::Clear(color),
341                    store: wgpu::StoreOp::Store,
342                },
343            };
344
345            match target {
346                RenderTarget::Surface => {
347                    let surface_view = frame_context.surface().view();
348                    all_attachments.push(Some(wgpu::RenderPassColorAttachment {
349                        view: surface_view,
350                        resolve_target: None,
351                        ops: color_ops,
352                        depth_slice: None,
353                    }));
354                }
355                RenderTarget::Framebuffer(fb) => {
356                    all_attachments.push(Some(wgpu::RenderPassColorAttachment {
357                        view: fb.render_view(),
358                        resolve_target: fb.resolve_target(),
359                        ops: color_ops,
360                        depth_slice: None,
361                    }));
362                }
363            }
364        } else {
365            // Legacy API
366            if let Some((ops, resolve_target)) = self.surface_attachment_ops {
367                let surface_view = frame_context.surface().view();
368                all_attachments.push(Some(wgpu::RenderPassColorAttachment {
369                    view: surface_view,
370                    resolve_target,
371                    ops,
372                    depth_slice: None,
373                }));
374            }
375            all_attachments.extend(self.color_attachments);
376        }
377
378        // Build depth attachment
379        let depth_attachment = if let Some(attachment) = self.depth_stencil_attachment {
380            Some(attachment)
381        } else if let Some(RenderTarget::Framebuffer(fb)) = &self.target {
382            fb.depth_view().map(|view| {
383                let depth_ops = match self.depth_clear_op {
384                    DepthClearOp::Load => wgpu::Operations {
385                        load: wgpu::LoadOp::Load,
386                        store: wgpu::StoreOp::Store,
387                    },
388                    DepthClearOp::Clear(depth) => wgpu::Operations {
389                        load: wgpu::LoadOp::Clear(depth),
390                        store: wgpu::StoreOp::Store,
391                    },
392                };
393                wgpu::RenderPassDepthStencilAttachment {
394                    view,
395                    depth_ops: Some(depth_ops),
396                    stencil_ops: None,
397                }
398            })
399        } else {
400            None
401        };
402
403        let descriptor = wgpu::RenderPassDescriptor {
404            label: self.label,
405            color_attachments: &all_attachments,
406            depth_stencil_attachment: depth_attachment,
407            occlusion_query_set: None,
408            timestamp_writes: None,
409        };
410
411        let render_pass = encoder.begin_render_pass(&descriptor).forget_lifetime();
412
413        frame_context.increment_passes();
414
415        RenderPass {
416            context: frame_context,
417            encoder: Some(encoder),
418            descriptor: Some(render_pass),
419        }
420    }
421}
422
423impl Default for RenderPassBuilder<'_> {
424    fn default() -> Self {
425        Self::new()
426    }
427}
428
429/// A render pass wrapper that automatically returns the encoder to the frame context.
430pub struct RenderPass<'a> {
431    pub context: &'a mut FrameContext,
432    pub(crate) encoder: Option<wgpu::CommandEncoder>,
433    pub(crate) descriptor: Option<wgpu::RenderPass<'static>>,
434}
435
436impl<'a> RenderPass<'a> {
437    pub fn descriptor(&mut self) -> &mut wgpu::RenderPass<'static> {
438        self.descriptor.as_mut().unwrap()
439    }
440
441    pub fn finish(self) {
442        drop(self);
443    }
444
445    // =========================================================================
446    // Viewport/Scissor Methods
447    // =========================================================================
448
449    /// Set the viewport using physical coordinates.
450    ///
451    /// The viewport defines the transformation from normalized device coordinates
452    /// to window coordinates.
453    ///
454    /// # Arguments
455    ///
456    /// * `rect` - The viewport rectangle in physical (pixel) coordinates
457    /// * `min_depth` - Minimum depth value (typically 0.0)
458    /// * `max_depth` - Maximum depth value (typically 1.0)
459    pub fn set_viewport_physical(
460        &mut self,
461        rect: astrelis_core::geometry::PhysicalRect<f32>,
462        min_depth: f32,
463        max_depth: f32,
464    ) {
465        self.descriptor().set_viewport(
466            rect.x,
467            rect.y,
468            rect.width,
469            rect.height,
470            min_depth,
471            max_depth,
472        );
473    }
474
475    /// Set the viewport using logical coordinates (converts with scale factor).
476    ///
477    /// # Arguments
478    ///
479    /// * `rect` - The viewport rectangle in logical coordinates
480    /// * `min_depth` - Minimum depth value (typically 0.0)
481    /// * `max_depth` - Maximum depth value (typically 1.0)
482    /// * `scale` - Scale factor for logical to physical conversion
483    pub fn set_viewport_logical(
484        &mut self,
485        rect: astrelis_core::geometry::LogicalRect<f32>,
486        min_depth: f32,
487        max_depth: f32,
488        scale: astrelis_core::geometry::ScaleFactor,
489    ) {
490        let physical = rect.to_physical_f32(scale);
491        self.set_viewport_physical(physical, min_depth, max_depth);
492    }
493
494    /// Set the viewport from a Viewport struct.
495    ///
496    /// Uses the viewport's position and size, with depth range 0.0 to 1.0.
497    pub fn set_viewport(&mut self, viewport: &crate::Viewport) {
498        self.descriptor().set_viewport(
499            viewport.position.x,
500            viewport.position.y,
501            viewport.size.width,
502            viewport.size.height,
503            0.0,
504            1.0,
505        );
506    }
507
508    /// Set the scissor rectangle using physical coordinates.
509    ///
510    /// The scissor rectangle defines the area of the render target that
511    /// can be modified by drawing commands.
512    ///
513    /// # Arguments
514    ///
515    /// * `rect` - The scissor rectangle in physical (pixel) coordinates
516    pub fn set_scissor_physical(&mut self, rect: astrelis_core::geometry::PhysicalRect<u32>) {
517        self.descriptor()
518            .set_scissor_rect(rect.x, rect.y, rect.width, rect.height);
519    }
520
521    /// Set the scissor rectangle using logical coordinates.
522    ///
523    /// # Arguments
524    ///
525    /// * `rect` - The scissor rectangle in logical coordinates
526    /// * `scale` - Scale factor for logical to physical conversion
527    pub fn set_scissor_logical(
528        &mut self,
529        rect: astrelis_core::geometry::LogicalRect<f32>,
530        scale: astrelis_core::geometry::ScaleFactor,
531    ) {
532        let physical = rect.to_physical(scale);
533        self.set_scissor_physical(physical);
534    }
535
536    // =========================================================================
537    // Convenience Drawing Methods
538    // =========================================================================
539
540    /// Set the pipeline for this render pass.
541    pub fn set_pipeline(&mut self, pipeline: &'a wgpu::RenderPipeline) {
542        self.descriptor().set_pipeline(pipeline);
543    }
544
545    /// Set a bind group for this render pass.
546    pub fn set_bind_group(
547        &mut self,
548        index: u32,
549        bind_group: &'a wgpu::BindGroup,
550        offsets: &[u32],
551    ) {
552        self.descriptor().set_bind_group(index, bind_group, offsets);
553    }
554
555    /// Set the vertex buffer for this render pass.
556    pub fn set_vertex_buffer(&mut self, slot: u32, buffer_slice: wgpu::BufferSlice<'a>) {
557        self.descriptor().set_vertex_buffer(slot, buffer_slice);
558    }
559
560    /// Set the index buffer for this render pass.
561    pub fn set_index_buffer(&mut self, buffer_slice: wgpu::BufferSlice<'a>, format: wgpu::IndexFormat) {
562        self.descriptor().set_index_buffer(buffer_slice, format);
563    }
564
565    /// Draw primitives.
566    pub fn draw(&mut self, vertices: std::ops::Range<u32>, instances: std::ops::Range<u32>) {
567        self.descriptor().draw(vertices, instances);
568        self.context.increment_draw_calls();
569    }
570
571    /// Draw indexed primitives.
572    pub fn draw_indexed(
573        &mut self,
574        indices: std::ops::Range<u32>,
575        base_vertex: i32,
576        instances: std::ops::Range<u32>,
577    ) {
578        self.descriptor().draw_indexed(indices, base_vertex, instances);
579        self.context.increment_draw_calls();
580    }
581
582    /// Insert a debug marker.
583    pub fn insert_debug_marker(&mut self, label: &str) {
584        self.descriptor().insert_debug_marker(label);
585    }
586
587    /// Push a debug group.
588    pub fn push_debug_group(&mut self, label: &str) {
589        self.descriptor().push_debug_group(label);
590    }
591
592    /// Pop a debug group.
593    pub fn pop_debug_group(&mut self) {
594        self.descriptor().pop_debug_group();
595    }
596
597    // =========================================================================
598    // Push Constants
599    // =========================================================================
600
601    /// Set push constants for a range of shader stages.
602    ///
603    /// Push constants are a fast way to pass small amounts of data to shaders
604    /// without the overhead of buffer updates. They are limited in size
605    /// (typically 128-256 bytes depending on the GPU).
606    ///
607    /// **Requires the `PUSH_CONSTANTS` feature to be enabled.**
608    ///
609    /// # Arguments
610    ///
611    /// * `stages` - Which shader stages can access this data
612    /// * `offset` - Byte offset within the push constant range
613    /// * `data` - The data to set (must be Pod)
614    ///
615    /// # Example
616    ///
617    /// ```ignore
618    /// #[repr(C)]
619    /// #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
620    /// struct PushConstants {
621    ///     transform: [[f32; 4]; 4],
622    ///     color: [f32; 4],
623    /// }
624    ///
625    /// let constants = PushConstants {
626    ///     transform: /* ... */,
627    ///     color: [1.0, 0.0, 0.0, 1.0],
628    /// };
629    ///
630    /// pass.set_push_constants(
631    ///     wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
632    ///     0,
633    ///     &constants,
634    /// );
635    /// ```
636    pub fn set_push_constants<T: bytemuck::Pod>(
637        &mut self,
638        stages: wgpu::ShaderStages,
639        offset: u32,
640        data: &T,
641    ) {
642        self.descriptor()
643            .set_push_constants(stages, offset, bytemuck::bytes_of(data));
644    }
645
646    /// Set push constants from raw bytes.
647    ///
648    /// Use this when you need more control over the data layout.
649    pub fn set_push_constants_raw(
650        &mut self,
651        stages: wgpu::ShaderStages,
652        offset: u32,
653        data: &[u8],
654    ) {
655        self.descriptor().set_push_constants(stages, offset, data);
656    }
657}
658
659impl Drop for RenderPass<'_> {
660    fn drop(&mut self) {
661        profile_function!();
662
663        drop(self.descriptor.take());
664
665        // Return the encoder to the frame context
666        self.context.encoder = self.encoder.take();
667    }
668}
669
670/// Helper trait for creating render passes with common configurations.
671pub trait RenderPassExt {
672    /// Create a render pass that clears to the given color.
673    fn clear_pass<'a>(
674        &'a mut self,
675        target: RenderTarget<'a>,
676        clear_color: wgpu::Color,
677    ) -> RenderPass<'a>;
678
679    /// Create a render pass that loads existing content.
680    fn load_pass<'a>(&'a mut self, target: RenderTarget<'a>) -> RenderPass<'a>;
681}
682
683impl RenderPassExt for FrameContext {
684    fn clear_pass<'a>(
685        &'a mut self,
686        target: RenderTarget<'a>,
687        clear_color: wgpu::Color,
688    ) -> RenderPass<'a> {
689        RenderPassBuilder::new()
690            .target(target)
691            .clear_color(clear_color)
692            .build(self)
693    }
694
695    fn load_pass<'a>(&'a mut self, target: RenderTarget<'a>) -> RenderPass<'a> {
696        RenderPassBuilder::new().target(target).build(self)
697    }
698}