Skip to main content

astrelis_render/
window.rs

1//! Window and surface management for rendering.
2//!
3//! This module provides [`RenderableWindow`], which wraps a [`Window`] and manages
4//! its GPU surface for rendering. It handles surface configuration, frame presentation,
5//! and surface loss recovery.
6//!
7//! # Lifecycle
8//!
9//! 1. Create with [`RenderableWindow::new()`] passing a window and graphics context
10//! 2. Call [`begin_drawing()`](RenderableWindow::begin_drawing) to start a frame
11//! 3. Use the returned [`FrameContext`] for rendering
12//! 4. Drop the frame context to submit commands and present
13//!
14//! # Example
15//!
16//! ```rust,no_run
17//! use astrelis_render::{GraphicsContext, RenderableWindow, Color};
18//! use astrelis_winit::WindowDescriptor;
19//! # use std::sync::Arc;
20//!
21//! # fn example(window: astrelis_winit::window::Window, graphics: Arc<GraphicsContext>) {
22//! let mut renderable = RenderableWindow::new(window, graphics);
23//!
24//! // In render loop:
25//! let mut frame = renderable.begin_drawing();
26//! frame.clear_and_render(
27//!     astrelis_render::RenderTarget::Surface,
28//!     Color::BLACK,
29//!     |pass| {
30//!         // Rendering commands
31//!     },
32//! );
33//! frame.finish();
34//! # }
35//! ```
36//!
37//! # Surface Loss
38//!
39//! The surface can be lost due to window minimization, GPU driver resets, or other
40//! platform events. [`RenderableWindow`] handles this automatically by recreating
41//! the surface when [`begin_drawing()`](RenderableWindow::begin_drawing) is called.
42
43use astrelis_core::{
44    geometry::{LogicalSize, PhysicalPosition, PhysicalSize, ScaleFactor},
45    profiling::profile_function,
46};
47use astrelis_winit::{
48    WindowId,
49    window::{Window, WindowBackend},
50};
51use std::sync::Arc;
52
53use crate::{
54    context::{GraphicsContext, GraphicsError},
55    frame::{FrameContext, FrameStats, Surface},
56    gpu_profiling::GpuFrameProfiler,
57};
58
59/// Viewport definition for rendering.
60///
61/// A viewport represents the renderable area of a window in physical coordinates,
62/// along with the scale factor for coordinate conversions.
63#[derive(Debug, Clone, Copy)]
64pub struct Viewport {
65    /// Position in physical coordinates (pixels).
66    pub position: PhysicalPosition<f32>,
67    /// Size in physical coordinates (pixels).
68    pub size: PhysicalSize<f32>,
69    /// Scale factor for logical/physical conversion.
70    pub scale_factor: ScaleFactor,
71}
72
73impl Default for Viewport {
74    fn default() -> Self {
75        Self {
76            position: PhysicalPosition::new(0.0, 0.0),
77            size: PhysicalSize::new(800.0, 600.0),
78            // it needs to be 1.0 to avoid division by zero and other issues
79            scale_factor: ScaleFactor(1.0),
80        }
81    }
82}
83
84impl Viewport {
85    /// Create a new viewport with the given physical size and scale factor.
86    pub fn new(width: f32, height: f32, scale_factor: ScaleFactor) -> Self {
87        Self {
88            position: PhysicalPosition::new(0.0, 0.0),
89            size: PhysicalSize::new(width, height),
90            scale_factor,
91        }
92    }
93
94    /// Create a viewport from physical size.
95    pub fn from_physical_size(size: PhysicalSize<u32>, scale_factor: ScaleFactor) -> Self {
96        Self {
97            position: PhysicalPosition::new(0.0, 0.0),
98            size: PhysicalSize::new(size.width as f32, size.height as f32),
99            scale_factor,
100        }
101    }
102
103    /// Check if the viewport is valid (has positive dimensions).
104    pub fn is_valid(&self) -> bool {
105        self.size.width > 0.0 && self.size.height > 0.0 && self.scale_factor.0 > 0.0
106    }
107
108    /// Get the size in logical pixels.
109    pub fn to_logical(&self) -> LogicalSize<f32> {
110        self.size.to_logical(self.scale_factor)
111    }
112
113    /// Get the width in physical pixels.
114    pub fn width(&self) -> f32 {
115        self.size.width
116    }
117
118    /// Get the height in physical pixels.
119    pub fn height(&self) -> f32 {
120        self.size.height
121    }
122
123    /// Get the x position in physical pixels.
124    pub fn x(&self) -> f32 {
125        self.position.x
126    }
127
128    /// Get the y position in physical pixels.
129    pub fn y(&self) -> f32 {
130        self.position.y
131    }
132}
133
134/// Descriptor for configuring a window's rendering context.
135#[derive(Default)]
136pub struct WindowContextDescriptor {
137    /// The surface texture format. If None, uses the default format for the surface.
138    pub format: Option<wgpu::TextureFormat>,
139    /// Present mode for the surface.
140    pub present_mode: Option<wgpu::PresentMode>,
141    /// Alpha mode for the surface.
142    pub alpha_mode: Option<wgpu::CompositeAlphaMode>,
143}
144
145pub(crate) struct PendingReconfigure {
146    pub(crate) resize: Option<PhysicalSize<u32>>,
147}
148
149impl PendingReconfigure {
150    const fn new() -> Self {
151        Self { resize: None }
152    }
153}
154
155/// Manages a wgpu [`Surface`](wgpu::Surface) and its configuration for a single window.
156///
157/// Handles surface creation, reconfiguration on resize, and frame acquisition.
158/// Most users should interact with [`RenderableWindow`] instead, which wraps
159/// this type and adds convenience methods.
160pub struct WindowContext {
161    pub(crate) window: Window,
162    pub(crate) context: Arc<GraphicsContext>,
163    pub(crate) surface: wgpu::Surface<'static>,
164    pub(crate) config: wgpu::SurfaceConfiguration,
165    pub(crate) reconfigure: PendingReconfigure,
166}
167
168impl WindowContext {
169    pub fn new(
170        window: Window,
171        context: Arc<GraphicsContext>,
172        descriptor: WindowContextDescriptor,
173    ) -> Result<Self, GraphicsError> {
174        profile_function!();
175        let scale_factor = window.scale_factor();
176        let logical_size = window.logical_size();
177        let physical_size = logical_size.to_physical(scale_factor);
178
179        let surface = context
180            .instance()
181            .create_surface(window.window.clone())
182            .map_err(|e| GraphicsError::SurfaceCreationFailed(e.to_string()))?;
183
184        let mut config = surface
185            .get_default_config(context.adapter(), physical_size.width, physical_size.height)
186            .ok_or_else(|| GraphicsError::SurfaceConfigurationFailed(
187                "No suitable surface configuration found".to_string()
188            ))?;
189
190        if let Some(format) = descriptor.format {
191            config.format = format;
192        }
193        if let Some(present_mode) = descriptor.present_mode {
194            config.present_mode = present_mode;
195        }
196        if let Some(alpha_mode) = descriptor.alpha_mode {
197            config.alpha_mode = alpha_mode;
198        }
199
200        surface.configure(context.device(), &config);
201
202        Ok(Self {
203            window,
204            surface,
205            config,
206            reconfigure: PendingReconfigure::new(),
207            context,
208        })
209    }
210
211    /// Handle window resize event (logical size).
212    pub fn resized(&mut self, new_size: LogicalSize<u32>) {
213        let scale_factor = self.window.scale_factor();
214        let physical_size = new_size.to_physical(scale_factor);
215        self.reconfigure.resize = Some(physical_size);
216    }
217
218    /// Handle window resize event (physical size).
219    pub fn resized_physical(&mut self, new_size: PhysicalSize<u32>) {
220        self.reconfigure.resize = Some(new_size);
221    }
222
223    pub fn window(&self) -> &Window {
224        &self.window
225    }
226
227    pub fn graphics_context(&self) -> &GraphicsContext {
228        &self.context
229    }
230
231    pub fn surface(&self) -> &wgpu::Surface<'static> {
232        &self.surface
233    }
234
235    pub fn surface_config(&self) -> &wgpu::SurfaceConfiguration {
236        &self.config
237    }
238
239    /// Get the surface texture format.
240    ///
241    /// This is the format that render pipelines must use when rendering to this
242    /// window's surface. Pass this to renderer constructors like
243    /// [`LineRenderer::new`](crate::LineRenderer::new).
244    pub fn surface_format(&self) -> wgpu::TextureFormat {
245        self.config.format
246    }
247
248    /// Get the logical size of the window.
249    pub fn logical_size(&self) -> LogicalSize<u32> {
250        self.window.logical_size()
251    }
252
253    /// Get the physical size of the window.
254    pub fn physical_size(&self) -> PhysicalSize<u32> {
255        self.window.physical_size()
256    }
257
258    /// Get the logical size as f32.
259    pub fn logical_size_f32(&self) -> LogicalSize<f32> {
260        let size = self.logical_size();
261        LogicalSize::new(size.width as f32, size.height as f32)
262    }
263
264    /// Get the physical size as f32.
265    pub fn physical_size_f32(&self) -> PhysicalSize<f32> {
266        let size = self.physical_size();
267        PhysicalSize::new(size.width as f32, size.height as f32)
268    }
269
270    /// Reconfigure the surface with a new configuration.
271    pub fn reconfigure_surface(&mut self, config: wgpu::SurfaceConfiguration) {
272        self.config = config;
273        self.surface.configure(self.context.device(), &self.config);
274    }
275}
276
277impl WindowContext {
278    /// Try to acquire a surface texture, handling recoverable errors by reconfiguring.
279    ///
280    /// This method will attempt to reconfigure the surface if it's lost or outdated,
281    /// providing automatic recovery for common scenarios like window minimize/restore.
282    fn try_acquire_surface_texture(&mut self) -> Result<wgpu::SurfaceTexture, GraphicsError> {
283        // First attempt
284        match self.surface.get_current_texture() {
285            Ok(frame) => return Ok(frame),
286            Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
287                // Surface needs reconfiguration - try to recover
288                tracing::debug!("Surface lost/outdated, reconfiguring...");
289                self.surface.configure(self.context.device(), &self.config);
290            }
291            Err(wgpu::SurfaceError::OutOfMemory) => {
292                return Err(GraphicsError::SurfaceOutOfMemory);
293            }
294            Err(wgpu::SurfaceError::Timeout) => {
295                return Err(GraphicsError::SurfaceTimeout);
296            }
297            Err(e) => {
298                return Err(GraphicsError::SurfaceTextureAcquisitionFailed(e.to_string()));
299            }
300        }
301
302        // Second attempt after reconfiguration
303        match self.surface.get_current_texture() {
304            Ok(frame) => Ok(frame),
305            Err(wgpu::SurfaceError::Lost) => Err(GraphicsError::SurfaceLost),
306            Err(wgpu::SurfaceError::Outdated) => Err(GraphicsError::SurfaceOutdated),
307            Err(wgpu::SurfaceError::OutOfMemory) => Err(GraphicsError::SurfaceOutOfMemory),
308            Err(wgpu::SurfaceError::Timeout) => Err(GraphicsError::SurfaceTimeout),
309            Err(e) => Err(GraphicsError::SurfaceTextureAcquisitionFailed(e.to_string())),
310        }
311    }
312}
313
314impl WindowContext {
315    /// Begin drawing a frame, optionally with a GPU profiler attached.
316    ///
317    /// This is the internal implementation used by both `WindowBackend::try_begin_drawing`
318    /// and `RenderableWindow::begin_drawing`.
319    pub(crate) fn try_begin_drawing_with_profiler(
320        &mut self,
321        gpu_profiler: Option<Arc<GpuFrameProfiler>>,
322    ) -> Result<FrameContext, GraphicsError> {
323        profile_function!();
324
325        let mut configure_needed = false;
326        if let Some(new_size) = self.reconfigure.resize.take() {
327            self.config.width = new_size.width;
328            self.config.height = new_size.height;
329            configure_needed = true;
330        }
331
332        if configure_needed {
333            self.surface.configure(self.context.device(), &self.config);
334        }
335
336        let frame = self.try_acquire_surface_texture()?;
337        let view = frame
338            .texture
339            .create_view(&wgpu::TextureViewDescriptor::default());
340
341        let encoder = self
342            .context
343            .device()
344            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
345                label: Some("Frame Encoder"),
346            });
347
348        Ok(FrameContext {
349            surface: Some(Surface {
350                texture: frame,
351                view,
352            }),
353            encoder: Some(encoder),
354            context: self.context.clone(),
355            stats: FrameStats::new(),
356            window: self.window.window.clone(),
357            surface_format: self.config.format,
358            gpu_profiler,
359        })
360    }
361}
362
363impl WindowBackend for WindowContext {
364    type FrameContext = FrameContext;
365    type Error = GraphicsError;
366
367    fn try_begin_drawing(&mut self) -> Result<Self::FrameContext, Self::Error> {
368        self.try_begin_drawing_with_profiler(None)
369    }
370}
371
372/// A renderable window that combines a winit [`Window`] with a [`WindowContext`].
373///
374/// This is the primary type for rendering to a window. It implements
375/// `Deref<Target = WindowContext>`, so all `WindowContext` methods are
376/// available directly.
377///
378/// # GPU Profiling
379///
380/// Attach a [`GpuFrameProfiler`] via [`set_gpu_profiler`](Self::set_gpu_profiler)
381/// to automatically profile render passes. Once attached, all frames created via
382/// [`begin_drawing`](WindowBackend::begin_drawing) will include GPU profiling:
383/// - Render passes in `with_pass()` / `clear_and_render()` get automatic GPU scopes
384/// - Queries are resolved and the profiler frame is ended in `FrameContext::Drop`
385pub struct RenderableWindow {
386    pub(crate) context: WindowContext,
387    pub(crate) gpu_profiler: Option<Arc<GpuFrameProfiler>>,
388}
389
390impl RenderableWindow {
391    pub fn new(window: Window, context: Arc<GraphicsContext>) -> Result<Self, GraphicsError> {
392        Self::new_with_descriptor(window, context, WindowContextDescriptor::default())
393    }
394
395    pub fn new_with_descriptor(
396        window: Window,
397        context: Arc<GraphicsContext>,
398        descriptor: WindowContextDescriptor,
399    ) -> Result<Self, GraphicsError> {
400        profile_function!();
401        let context = WindowContext::new(window, context, descriptor)?;
402        Ok(Self {
403            context,
404            gpu_profiler: None,
405        })
406    }
407
408    pub fn id(&self) -> WindowId {
409        self.context.window.id()
410    }
411
412    pub fn window(&self) -> &Window {
413        &self.context.window
414    }
415
416    pub fn context(&self) -> &WindowContext {
417        &self.context
418    }
419
420    pub fn context_mut(&mut self) -> &mut WindowContext {
421        &mut self.context
422    }
423
424    /// Get the surface texture format.
425    ///
426    /// This is the format that render pipelines must use when rendering to this
427    /// window's surface. Pass this to renderer constructors like
428    /// [`LineRenderer::new`](crate::LineRenderer::new).
429    pub fn surface_format(&self) -> wgpu::TextureFormat {
430        self.context.surface_format()
431    }
432
433    /// Handle window resize event (logical size).
434    pub fn resized(&mut self, new_size: LogicalSize<u32>) {
435        self.context.resized(new_size);
436    }
437
438    /// Handle window resize event (physical size).
439    pub fn resized_physical(&mut self, new_size: PhysicalSize<u32>) {
440        self.context.resized_physical(new_size);
441    }
442
443    /// Get the physical size of the window.
444    pub fn physical_size(&self) -> PhysicalSize<u32> {
445        self.context.physical_size()
446    }
447
448    /// Get the scale factor.
449    pub fn scale_factor(&self) -> ScaleFactor {
450        self.window().scale_factor()
451    }
452
453    /// Get the viewport for this window.
454    pub fn viewport(&self) -> Viewport {
455        let physical_size = self.physical_size();
456        let scale_factor = self.scale_factor();
457
458        Viewport {
459            position: PhysicalPosition::new(0.0, 0.0),
460            size: PhysicalSize::new(physical_size.width as f32, physical_size.height as f32),
461            scale_factor,
462        }
463    }
464
465    /// Attach a GPU profiler to this window for automatic render pass profiling.
466    ///
467    /// Once set, all frames created via [`begin_drawing`](WindowBackend::begin_drawing)
468    /// will automatically:
469    /// - Create GPU profiling scopes around render passes
470    /// - Resolve timestamp queries before command submission
471    /// - End the profiler frame after queue submit
472    ///
473    /// # Example
474    ///
475    /// ```ignore
476    /// let profiler = Arc::new(GpuFrameProfiler::new(&graphics_ctx)?);
477    /// window.set_gpu_profiler(profiler);
478    ///
479    /// // Now all frames are automatically profiled:
480    /// let mut frame = window.begin_drawing();
481    /// frame.clear_and_render(RenderTarget::Surface, Color::BLACK, |pass| {
482    ///     // GPU scope "main_pass" is automatically active
483    /// });
484    /// frame.finish(); // auto: resolve_queries -> submit -> end_frame
485    /// ```
486    pub fn set_gpu_profiler(&mut self, profiler: Arc<GpuFrameProfiler>) {
487        self.gpu_profiler = Some(profiler);
488    }
489
490    /// Remove the GPU profiler from this window.
491    ///
492    /// Returns the profiler if one was attached.
493    pub fn remove_gpu_profiler(&mut self) -> Option<Arc<GpuFrameProfiler>> {
494        self.gpu_profiler.take()
495    }
496
497    /// Get a reference to the GPU profiler, if one is attached.
498    pub fn gpu_profiler(&self) -> Option<&Arc<GpuFrameProfiler>> {
499        self.gpu_profiler.as_ref()
500    }
501}
502
503impl std::ops::Deref for RenderableWindow {
504    type Target = WindowContext;
505
506    fn deref(&self) -> &Self::Target {
507        &self.context
508    }
509}
510
511impl std::ops::DerefMut for RenderableWindow {
512    fn deref_mut(&mut self) -> &mut Self::Target {
513        &mut self.context
514    }
515}
516
517impl WindowBackend for RenderableWindow {
518    type FrameContext = FrameContext;
519    type Error = GraphicsError;
520
521    fn try_begin_drawing(&mut self) -> Result<Self::FrameContext, Self::Error> {
522        self.context
523            .try_begin_drawing_with_profiler(self.gpu_profiler.clone())
524    }
525}