Skip to main content

oxiui_render_wgpu/
surface.rs

1//! Windowed wgpu surface — swap-chain context for real-time rendering.
2//!
3//! [`SurfaceContext`] wraps a `wgpu::Surface` created from a raw OS window
4//! handle alongside the `Device`, `Queue`, and swap-chain configuration
5//! needed for real-time frame presentation.
6//!
7//! # Safety
8//!
9//! Creating a [`SurfaceContext`] requires raw window/display handles whose
10//! lifetime the caller must manage.  See [`SurfaceContext::from_raw_handles`].
11
12use oxiui_core::UiError;
13use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
14
15// ── SurfaceConfig ─────────────────────────────────────────────────────────────
16
17/// Configuration for a [`SurfaceContext`] swap-chain.
18#[derive(Clone, Debug)]
19pub struct SurfaceConfig {
20    /// Width of the swap-chain in physical pixels.
21    pub width: u32,
22    /// Height of the swap-chain in physical pixels.
23    pub height: u32,
24    /// Controls how the surface is presented to the display.
25    pub present_mode: wgpu::PresentMode,
26    /// Compositing alpha mode for the surface.
27    pub alpha_mode: wgpu::CompositeAlphaMode,
28    /// Preferred texture format.  When `None` the first format reported by
29    /// `surface.get_capabilities(&adapter)` is used (preferring sRGB).
30    pub desired_format: Option<wgpu::TextureFormat>,
31}
32
33impl Default for SurfaceConfig {
34    fn default() -> Self {
35        Self {
36            width: 800,
37            height: 600,
38            present_mode: wgpu::PresentMode::Fifo,
39            alpha_mode: wgpu::CompositeAlphaMode::Auto,
40            desired_format: None,
41        }
42    }
43}
44
45// ── SurfaceContext ────────────────────────────────────────────────────────────
46
47/// A windowed GPU context: device, queue, surface, and swap-chain config.
48///
49/// Unlike [`crate::GpuContext`] (headless), `SurfaceContext` presents frames
50/// to an OS window via a wgpu swap-chain.
51pub struct SurfaceContext {
52    /// The logical GPU device.
53    pub device: wgpu::Device,
54    /// The command queue for the device.
55    pub queue: wgpu::Queue,
56    /// The wgpu surface attached to the OS window.
57    pub surface: wgpu::Surface<'static>,
58    /// The texture format selected for the swap-chain.
59    pub surface_format: wgpu::TextureFormat,
60    /// Current swap-chain configuration.
61    config: wgpu::SurfaceConfiguration,
62}
63
64impl SurfaceContext {
65    /// Create a windowed surface context from raw OS window/display handles.
66    ///
67    /// # Safety
68    ///
69    /// The caller must ensure that both `window_handle` and `display_handle`
70    /// refer to valid OS objects and **remain valid** for the entire lifetime
71    /// of the returned `SurfaceContext`.  Dropping the underlying window while
72    /// a `SurfaceContext` is live is undefined behaviour.
73    pub unsafe fn from_raw_handles(
74        window_handle: RawWindowHandle,
75        display_handle: RawDisplayHandle,
76        config: SurfaceConfig,
77    ) -> Result<Self, UiError> {
78        let instance = wgpu::Instance::default();
79
80        // Build the unsafe surface target from the raw handles.
81        let surface_target = wgpu::SurfaceTargetUnsafe::RawHandle {
82            raw_window_handle: window_handle,
83            raw_display_handle: Some(display_handle),
84        };
85
86        // SAFETY: caller guarantees that the raw handles remain valid.
87        let surface = unsafe { instance.create_surface_unsafe(surface_target) }
88            .map_err(|e| UiError::Backend(format!("wgpu surface creation failed: {e}")))?;
89
90        // Request an adapter that is compatible with the surface.
91        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
92            power_preference: wgpu::PowerPreference::default(),
93            force_fallback_adapter: false,
94            compatible_surface: Some(&surface),
95        }))
96        .map_err(|e| UiError::Unsupported(format!("no GPU adapter for surface: {e}")))?;
97
98        let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
99            label: Some("oxiui-render-wgpu windowed device"),
100            required_features: wgpu::Features::empty(),
101            required_limits: wgpu::Limits::default(),
102            memory_hints: wgpu::MemoryHints::Performance,
103            experimental_features: wgpu::ExperimentalFeatures::disabled(),
104            trace: wgpu::Trace::Off,
105        }))
106        .map_err(|e| UiError::Backend(format!("GPU device request failed: {e}")))?;
107
108        // Choose the swap-chain format: honour `desired_format` when set,
109        // otherwise pick the first supported format preferring sRGB variants.
110        let surface_format = if let Some(fmt) = config.desired_format {
111            fmt
112        } else {
113            let caps = surface.get_capabilities(&adapter);
114            caps.formats
115                .iter()
116                .copied()
117                .find(|f| f.is_srgb())
118                .unwrap_or_else(|| {
119                    *caps
120                        .formats
121                        .first()
122                        .unwrap_or(&wgpu::TextureFormat::Bgra8Unorm)
123                })
124        };
125
126        let sc_config = wgpu::SurfaceConfiguration {
127            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
128            format: surface_format,
129            width: config.width.max(1),
130            height: config.height.max(1),
131            present_mode: config.present_mode,
132            alpha_mode: config.alpha_mode,
133            view_formats: vec![],
134            desired_maximum_frame_latency: 2,
135        };
136
137        surface.configure(&device, &sc_config);
138
139        Ok(Self {
140            device,
141            queue,
142            surface,
143            surface_format,
144            config: sc_config,
145        })
146    }
147
148    /// Resize the swap-chain to new dimensions.
149    ///
150    /// Dimensions are clamped to a minimum of 1×1 to avoid a wgpu validation error.
151    pub fn resize(&mut self, width: u32, height: u32) {
152        self.config.width = width.max(1);
153        self.config.height = height.max(1);
154        self.surface.configure(&self.device, &self.config);
155    }
156
157    /// Acquire the next swap-chain frame.
158    ///
159    /// Returns `None` on surface timeout, occlusion, or any other transient
160    /// condition — the caller should skip the frame and try again next tick.
161    /// Permanent errors (surface lost / outdated) are logged to `stderr`; the
162    /// caller can recreate the `SurfaceContext` if needed.
163    pub fn acquire_frame(&self) -> Option<wgpu::SurfaceTexture> {
164        match self.surface.get_current_texture() {
165            wgpu::CurrentSurfaceTexture::Success(frame) => Some(frame),
166            wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
167                // Suboptimal but still usable; caller should reconfigure soon.
168                Some(frame)
169            }
170            wgpu::CurrentSurfaceTexture::Timeout => None,
171            wgpu::CurrentSurfaceTexture::Occluded => None,
172            other => {
173                eprintln!("[oxiui-render-wgpu] surface error: {other:?} — frame skipped");
174                None
175            }
176        }
177    }
178
179    /// Present a previously acquired frame to the display.
180    pub fn present_frame(&self, frame: wgpu::SurfaceTexture) {
181        frame.present();
182    }
183
184    /// Current swap-chain width in physical pixels.
185    pub fn width(&self) -> u32 {
186        self.config.width
187    }
188
189    /// Current swap-chain height in physical pixels.
190    pub fn height(&self) -> u32 {
191        self.config.height
192    }
193
194    /// The texture format used by the swap-chain.
195    pub fn format(&self) -> wgpu::TextureFormat {
196        self.surface_format
197    }
198}
199
200// ── Tests ─────────────────────────────────────────────────────────────────────
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn surface_config_default_is_sane() {
208        let c = SurfaceConfig::default();
209        assert!(c.width > 0 && c.height > 0);
210        assert_eq!(c.width, 800);
211        assert_eq!(c.height, 600);
212    }
213
214    #[test]
215    fn surface_config_clone_and_debug() {
216        let c = SurfaceConfig::default();
217        let c2 = c.clone();
218        assert_eq!(c2.width, c.width);
219        // Ensure Debug impl compiles.
220        let _ = format!("{c:?}");
221    }
222}