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