truce_gui/platform.rs
1//! Platform window bridging for baseview.
2//!
3//! Bridges truce's `RawWindowHandle` to baseview's `HasRawWindowHandle`
4//! (raw-window-handle 0.5), and provides scale factor querying and
5//! wgpu surface creation.
6
7// `HasRawDisplayHandle` / `RwhRawDisplayHandle` are only touched on
8// the Linux (X11) arm of `HasRawWindowHandle for ParentWindow`;
9// silence the macOS/Windows dead-import warning.
10#[allow(unused_imports)]
11use raw_window_handle::{
12 HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle as RwhRawDisplayHandle,
13 RawWindowHandle as RwhRawWindowHandle,
14};
15use truce_core::editor::RawWindowHandle;
16
17use std::sync::Arc;
18use std::sync::atomic::{AtomicU64, Ordering};
19
20/// Newtype bridging truce's `RawWindowHandle` to baseview's
21/// `HasRawWindowHandle` (raw-window-handle 0.5).
22pub struct ParentWindow(pub RawWindowHandle);
23
24unsafe impl HasRawWindowHandle for ParentWindow {
25 fn raw_window_handle(&self) -> RwhRawWindowHandle {
26 match self.0 {
27 RawWindowHandle::AppKit(ptr) => {
28 let mut handle = raw_window_handle::AppKitWindowHandle::empty();
29 handle.ns_view = ptr;
30 RwhRawWindowHandle::AppKit(handle)
31 }
32 RawWindowHandle::UiKit(ptr) => {
33 // baseview doesn't host on iOS - the iOS editor
34 // path attaches a UIView directly without going
35 // through this bridge. We surface the handle for
36 // completeness (and so future iOS-aware backends
37 // can read it) but in practice no caller on iOS
38 // reaches this arm.
39 let mut handle = raw_window_handle::UiKitWindowHandle::empty();
40 handle.ui_view = ptr;
41 RwhRawWindowHandle::UiKit(handle)
42 }
43 RawWindowHandle::Win32(ptr) => {
44 let mut handle = raw_window_handle::Win32WindowHandle::empty();
45 handle.hwnd = ptr;
46 RwhRawWindowHandle::Win32(handle)
47 }
48 RawWindowHandle::X11(window_id) => {
49 let mut handle = raw_window_handle::XlibWindowHandle::empty();
50 // rwh 0.5 field type is c_ulong: u64 on Linux/macOS, u32 on Windows.
51 // The Windows narrowing is the lossy edge - `XID` is 32-bit there.
52 #[allow(clippy::cast_possible_truncation)]
53 {
54 handle.window = window_id as _;
55 }
56 RwhRawWindowHandle::Xlib(handle)
57 }
58 }
59 }
60}
61
62/// Query the backing scale factor from the parent `NSView`'s window.
63#[cfg(target_os = "macos")]
64#[must_use]
65pub fn query_backing_scale(parent: &RawWindowHandle) -> f64 {
66 use objc::{msg_send, sel, sel_impl};
67
68 let ns_view_ptr = match parent {
69 RawWindowHandle::AppKit(ptr) => *ptr,
70 _ => return 1.0,
71 };
72
73 if ns_view_ptr.is_null() {
74 return 1.0;
75 }
76
77 unsafe {
78 let ns_view = ns_view_ptr.cast::<objc::runtime::Object>();
79 let window: *mut objc::runtime::Object = msg_send![ns_view, window];
80 let scale: f64 = if window.is_null() {
81 let screen: *mut objc::runtime::Object = msg_send![objc::class!(NSScreen), mainScreen];
82 if screen.is_null() {
83 2.0
84 } else {
85 msg_send![screen, backingScaleFactor]
86 }
87 } else {
88 msg_send![window, backingScaleFactor]
89 };
90 if scale < 1.0 { 1.0 } else { scale }
91 }
92}
93
94#[cfg(target_os = "windows")]
95#[must_use]
96pub fn query_backing_scale(parent: &RawWindowHandle) -> f64 {
97 let hwnd = match parent {
98 RawWindowHandle::Win32(ptr) => *ptr,
99 _ => return 1.0,
100 };
101 win32_dpi_scale(hwnd)
102}
103
104#[cfg(target_os = "linux")]
105#[must_use]
106pub fn query_backing_scale(_parent: &RawWindowHandle) -> f64 {
107 main_screen_scale()
108}
109
110#[cfg(target_os = "ios")]
111#[must_use]
112pub fn query_backing_scale(parent: &RawWindowHandle) -> f64 {
113 use objc2::msg_send;
114 use objc2::runtime::AnyObject;
115
116 let ui_view_ptr = match parent {
117 RawWindowHandle::UiKit(ptr) => *ptr,
118 _ => return 1.0,
119 };
120 if ui_view_ptr.is_null() {
121 return main_screen_scale();
122 }
123 // SAFETY: UIView is a UIKit class; `contentScaleFactor` is a
124 // public Objective-C property returning CGFloat (= f64 on
125 // arm64). Called on the main thread per UIKit's threading
126 // rule, which is also where AUv3 view controllers live.
127 unsafe {
128 let ui_view: *mut AnyObject = ui_view_ptr.cast();
129 let scale: f64 = msg_send![ui_view, contentScaleFactor];
130 if scale > 0.0 { scale } else { 1.0 }
131 }
132}
133
134#[cfg(target_os = "ios")]
135#[must_use]
136pub fn main_screen_scale() -> f64 {
137 use objc2::msg_send;
138 use objc2::runtime::{AnyClass, AnyObject};
139 // SAFETY: `+[UIScreen mainScreen]` is documented to return the
140 // process's primary screen on the main thread.
141 unsafe {
142 let Some(cls) = AnyClass::get(c"UIScreen") else {
143 return 1.0;
144 };
145 let screen: *mut AnyObject = msg_send![cls, mainScreen];
146 if screen.is_null() {
147 return 1.0;
148 }
149 let scale: f64 = msg_send![screen, scale];
150 if scale > 0.0 { scale } else { 1.0 }
151 }
152}
153
154/// Query the main screen's backing scale factor (no parent window needed).
155#[cfg(target_os = "macos")]
156#[must_use]
157pub fn main_screen_scale() -> f64 {
158 use objc::{msg_send, sel, sel_impl};
159 unsafe {
160 let screen: *mut objc::runtime::Object = msg_send![objc::class!(NSScreen), mainScreen];
161 if screen.is_null() {
162 1.0
163 } else {
164 let scale: f64 = msg_send![screen, backingScaleFactor];
165 if scale < 1.0 { 1.0 } else { scale }
166 }
167 }
168}
169
170#[cfg(target_os = "windows")]
171#[must_use]
172pub fn main_screen_scale() -> f64 {
173 win32_dpi_scale(std::ptr::null_mut())
174}
175
176// `reanchor_to_superview_top` and `reanchor_all_children_to_top`
177// live in the `truce-gui-utils` crate so backends that don't pull
178// `truce-gui` (vizia) can still get at them. Re-exported here for
179// existing `truce_gui::platform::...` call sites.
180pub use truce_gui_utils::{
181 reanchor_all_children_to_top, reanchor_to_superview_top, should_skip_frame,
182};
183
184/// Shared, mutable editor scale factor.
185///
186/// Single source of truth for the live content-scale of an open plugin
187/// window. Each GUI backend (egui / iced / slint) constructs one in
188/// `Editor::open`, stores it on the editor for `set_scale_factor` to
189/// write through, and hands a clone to its baseview `WindowHandler` so
190/// the render thread can pick up changes between frames.
191///
192/// Two writers, one reader-per-frame:
193/// - `Editor::set_scale_factor` (host → editor, e.g. CLAP `set_scale`,
194/// VST3 Windows `IPlugViewContentScaleSupport`).
195/// - `WindowEvent::Resized` (baseview → handler, fired when the OS
196/// reports a new content scale, e.g. dragging the window across
197/// monitors with different DPIs).
198///
199/// Most-recent-write wins. The handler tracks a `last_applied_scale`
200/// alongside its `EditorScale` clone and, when it observes a divergence
201/// at frame start, recomputes physical sizes and reconfigures its
202/// surface / renderer.
203/// Paces editor paints to the compositor's measured consumption rate.
204///
205/// A child-window swapchain acquire (`get_current_texture`) blocks
206/// until the compositor frees a frame slot, and a host may compose an
207/// embedded editor window far below the editor's tick rate (REAPER on
208/// Windows composes FX child windows at ~13 Hz). Painting every tick
209/// then parks the host's GUI thread in that wait - measured at 74 ms
210/// of every 77 ms frame, which saturates the host's whole UI and reads
211/// as the DAW hanging. Editors feed each paint's measured acquire wait
212/// into [`Self::record_acquire`] and skip the tick while
213/// [`Self::should_hold`] is true, converging on the rate the
214/// compositor actually displays (which is all the user ever saw).
215///
216/// The pace is a decayed maximum: a successfully paced paint measures
217/// a ~0 ms acquire, so pacing by the last wait alone oscillates
218/// (paced, unpaced, paced, ...). Holding the estimate through the
219/// zeros and shrinking it ~15% per paint lets it track a compositor
220/// that genuinely speeds up within roughly a second.
221#[derive(Debug, Default)]
222pub struct PaintPacer {
223 not_before: Option<std::time::Instant>,
224 pace: std::time::Duration,
225}
226
227impl PaintPacer {
228 /// Acquire waits below this are treated as "compositor keeps up" -
229 /// no pacing.
230 const MIN_PACE: std::time::Duration = std::time::Duration::from_millis(8);
231 /// Ceiling on the hold so a pathological acquire (wgpu's internal
232 /// timeout is ~1 s) can't park the editor for a full second.
233 const MAX_PACE: std::time::Duration = std::time::Duration::from_millis(250);
234
235 /// Whether this tick's paint should be skipped: the previous
236 /// acquire showed the compositor hasn't caught up yet.
237 #[must_use]
238 pub fn should_hold(&self) -> bool {
239 self.not_before
240 .is_some_and(|t| std::time::Instant::now() < t)
241 }
242
243 /// Record how long a paint's swapchain acquire blocked, scheduling
244 /// the earliest next paint accordingly.
245 pub fn record_acquire(&mut self, wait: std::time::Duration) {
246 self.pace = wait.max(self.pace.mul_f32(0.85));
247 self.not_before = (self.pace >= Self::MIN_PACE)
248 .then(|| std::time::Instant::now() + self.pace.min(Self::MAX_PACE));
249 }
250}
251
252#[derive(Clone)]
253pub struct EditorScale {
254 inner: Arc<AtomicU64>,
255}
256
257impl EditorScale {
258 /// Construct with an initial scale. Non-finite or non-positive
259 /// values clamp to 1.0 so callers never have to defend against
260 /// `0.0 * size` collapsing the surface.
261 #[must_use]
262 pub fn new(initial: f64) -> Self {
263 let v = if initial.is_finite() && initial > 0.0 {
264 initial
265 } else {
266 1.0
267 };
268 Self {
269 inner: Arc::new(AtomicU64::new(v.to_bits())),
270 }
271 }
272
273 /// Read the current scale.
274 #[must_use]
275 pub fn get(&self) -> f64 {
276 f64::from_bits(self.inner.load(Ordering::Relaxed))
277 }
278
279 /// Read the current scale, narrowed to `f32` for renderer / DSP
280 /// use. Display scales never exceed 4.0 in practice, so the f64
281 /// → f32 narrowing is invisible.
282 #[allow(clippy::cast_possible_truncation)]
283 #[must_use]
284 pub fn get_f32(&self) -> f32 {
285 self.get() as f32
286 }
287
288 /// Update the current scale. Non-finite or non-positive values are
289 /// silently dropped - callers are forwarding numbers from hosts /
290 /// `info.scale()` where a bad value is a host bug, not something
291 /// to propagate into the surface config.
292 pub fn set(&self, scale: f64) {
293 if scale.is_finite() && scale > 0.0 {
294 self.inner.store(scale.to_bits(), Ordering::Relaxed);
295 } else {
296 // Surface the upstream bug at least in debug builds so a
297 // host that's emitting bad scales doesn't get silently
298 // ignored. Production builds drop quietly to keep the
299 // editor running.
300 log::warn!(
301 "EditorScale::set ignored a bad value ({scale}); \
302 expected finite, positive f64",
303 );
304 }
305 }
306
307 /// Pick up a host-driven scale change since the last frame.
308 ///
309 /// Reads the current scale (narrowed to `f32`) and compares it
310 /// bit-identically against `last`. When the value moved, updates
311 /// `last` and returns `Some(cur)`; otherwise returns `None`.
312 ///
313 /// Used by every editor backend's per-frame loop to gate surface /
314 /// renderer reconfiguration on actual host scale events. Bit-equality
315 /// is the correct semantics - the cell is written verbatim from
316 /// host callbacks, never through accumulating arithmetic, so an
317 /// epsilon-based check would either thrash on noise (there is
318 /// none) or miss a legitimate `1.0 → 1.0001` host signal.
319 #[allow(clippy::cast_possible_truncation, clippy::float_cmp)]
320 pub fn take_change(&self, last: &mut f32) -> Option<f32> {
321 let cur = self.get() as f32;
322 if cur == *last {
323 None
324 } else {
325 *last = cur;
326 Some(cur)
327 }
328 }
329}
330
331/// Convert a logical extent (in points) to physical pixels.
332///
333/// Standardised rounding policy across every truce GUI backend:
334/// round to nearest, then clamp the result to `1` so a degenerate
335/// `0 × scale` doesn't collapse a wgpu surface (`width: 0` is a
336/// validation error). The `logical.max(1)` guard handles the
337/// converse - a zero-logical caller can't multiply through to `0`
338/// before the round.
339///
340/// Canonical definition lives in `truce-gui-types` so `truce-gpu`'s
341/// `WgpuBackend` can call it without a `truce-gui` dep (cycle); the
342/// re-export below preserves the historical `truce_gui::to_physical_px`
343/// path.
344pub use truce_gui_types::to_physical_px;
345
346/// Cached display scale factor on Linux, stored as f64 bits. Zero means unset.
347///
348/// Linux has no safe synchronous DPI query from plugin code - the authoritative
349/// value is read by baseview internally (from `Xft.dpi` with a screen-geometry
350/// fallback) and delivered via `WindowEvent::Resized::info.scale()` once the
351/// window is live. We cache the first value an editor sees there so that later
352/// pre-window `main_screen_scale()` calls (e.g. the next editor's `::new`)
353/// return something useful instead of 1.0.
354#[cfg(target_os = "linux")]
355static LINUX_SCALE_BITS: AtomicU64 = AtomicU64::new(0);
356
357/// Record the display scale factor observed from baseview on Linux. Editors
358/// should call this from their `WindowEvent::Resized` handlers so subsequent
359/// pre-window queries match what baseview is delivering. No-op on non-Linux.
360pub fn note_linux_scale_factor(scale: f64) {
361 #[cfg(target_os = "linux")]
362 {
363 if scale.is_finite() && scale > 0.0 {
364 LINUX_SCALE_BITS.store(scale.to_bits(), Ordering::Relaxed);
365 }
366 }
367 #[cfg(not(target_os = "linux"))]
368 {
369 let _ = scale;
370 }
371}
372
373/// Decide the content-scale policy for an editor's baseview child window.
374///
375/// Returns `Some(scale)` when the caller should open with
376/// `WindowScalePolicy::ScaleFactor(scale)` (and render at `scale`), or
377/// `None` when it should keep `WindowScalePolicy::SystemScaleFactor` and
378/// query the OS backing/DPI scale as usual.
379///
380/// The distinction only bites on Linux. There `SystemScaleFactor` reads
381/// `Xft.dpi` - the *desktop* scale - which is the right signal for the
382/// standalone app's own top-level window but wrong for a plugin embedded
383/// in a host: a non-DPI-aware host (Bitwig on X11) runs at 1x and
384/// allocates a 1x container regardless of desktop scaling, so honoring
385/// `Xft.dpi` builds a window twice the size of its allocated rect. For an
386/// embedded editor we therefore drive scale from the host's content-scale
387/// callback (default `1.0`; `host_scale_set` flips true once the host
388/// announces one via `set_scale_factor`) rather than the desktop.
389/// macOS/Windows always keep `SystemScaleFactor`: the OS reports a
390/// reliable per-window scale there.
391#[must_use]
392pub fn editor_window_scale(
393 uses_system_scale: bool,
394 host_scale_set: bool,
395 host_scale: f64,
396) -> Option<f64> {
397 #[cfg(target_os = "linux")]
398 {
399 if uses_system_scale {
400 None
401 } else if host_scale_set && host_scale.is_finite() && host_scale > 0.0 {
402 Some(host_scale)
403 } else {
404 Some(1.0)
405 }
406 }
407 #[cfg(not(target_os = "linux"))]
408 {
409 let _ = (uses_system_scale, host_scale_set, host_scale);
410 None
411 }
412}
413
414#[cfg(target_os = "linux")]
415pub fn main_screen_scale() -> f64 {
416 // Priority: TRUCE_SCALE env var (dev/test override) → cached scale
417 // observed from baseview → 1.0 fallback. No side-channel Xlib calls -
418 // those crashed inside NVIDIA's Vulkan driver when invoked from the
419 // render thread.
420 if let Ok(s) = std::env::var("TRUCE_SCALE")
421 && let Ok(v) = s.parse::<f64>()
422 && v.is_finite()
423 && v > 0.0
424 {
425 return v;
426 }
427 let bits = LINUX_SCALE_BITS.load(Ordering::Relaxed);
428 if bits == 0 {
429 return 1.0;
430 }
431 let v = f64::from_bits(bits);
432 if v.is_finite() && v > 0.0 { v } else { 1.0 }
433}
434
435/// Query the DPI scale factor on Windows.
436/// If `hwnd` is non-null, queries per-window DPI; otherwise queries the system DPI.
437#[cfg(target_os = "windows")]
438fn win32_dpi_scale(hwnd: *mut std::ffi::c_void) -> f64 {
439 // Default DPI is 96; scale = actual_dpi / 96.
440 const DEFAULT_DPI: u32 = 96;
441
442 unsafe extern "system" {
443 fn GetDpiForWindow(hwnd: *mut std::ffi::c_void) -> u32;
444 fn GetDpiForSystem() -> u32;
445 }
446
447 let dpi = if hwnd.is_null() {
448 unsafe { GetDpiForSystem() }
449 } else {
450 let d = unsafe { GetDpiForWindow(hwnd) };
451 if d == 0 {
452 unsafe { GetDpiForSystem() }
453 } else {
454 d
455 }
456 };
457
458 if dpi == 0 {
459 1.0
460 } else {
461 f64::from(dpi) / f64::from(DEFAULT_DPI)
462 }
463}
464
465/// Physical client-area size of a Win32 window, straight from
466/// `GetClientRect`. The authoritative answer to "how many pixels does
467/// the swapchain have to cover" - unlike `to_physical_px(logical,
468/// scale)`, which is a prediction that can diverge from what the host
469/// actually sized the child window to. `None` for non-Win32 handles,
470/// a null/dead HWND, or an empty rect.
471#[cfg(target_os = "windows")]
472#[must_use]
473pub fn win32_client_size(handle: RwhRawWindowHandle) -> Option<(u32, u32)> {
474 #[repr(C)]
475 struct Rect {
476 left: i32,
477 top: i32,
478 right: i32,
479 bottom: i32,
480 }
481 unsafe extern "system" {
482 fn GetClientRect(hwnd: *mut std::ffi::c_void, rect: *mut Rect) -> i32;
483 }
484
485 let RwhRawWindowHandle::Win32(h) = handle else {
486 return None;
487 };
488 if h.hwnd.is_null() {
489 return None;
490 }
491 let mut rect = Rect {
492 left: 0,
493 top: 0,
494 right: 0,
495 bottom: 0,
496 };
497 // SAFETY: pure state query on a window handle baseview owns for
498 // the editor's lifetime, called from the GUI thread that owns the
499 // HWND.
500 if unsafe { GetClientRect(h.hwnd, &raw mut rect) } == 0 {
501 return None;
502 }
503 // Client coordinates put left/top at 0; right/bottom are the size.
504 let w = u32::try_from(rect.right).ok()?;
505 let hgt = u32::try_from(rect.bottom).ok()?;
506 if w == 0 || hgt == 0 {
507 return None;
508 }
509 Some((w, hgt))
510}
511
512#[cfg(target_os = "windows")]
513fn current_module_hinstance() -> Option<std::num::NonZeroIsize> {
514 unsafe extern "system" {
515 fn GetModuleHandleW(lpModuleName: *const u16) -> isize;
516 }
517 // SAFETY: `GetModuleHandleW(NULL)` is documented to return the running
518 // EXE's HMODULE without acquiring a refcount; no threading or aliasing
519 // concerns. Returns 0 only in pathological cases (kernel32 missing).
520 let hmodule = unsafe { GetModuleHandleW(std::ptr::null()) };
521 std::num::NonZeroIsize::new(hmodule)
522}
523
524/// wgpu backends to use for an editor that presents into a
525/// host-owned child window. macOS is Metal-only; Windows is DX12
526/// (the only backend feature truce-gui/truce-gpu compile in on
527/// Windows - see their `Cargo.toml`); Linux keeps `PRIMARY`.
528#[must_use]
529pub fn editor_wgpu_backends() -> wgpu::Backends {
530 #[cfg(target_os = "windows")]
531 {
532 wgpu::Backends::DX12
533 }
534 #[cfg(target_os = "macos")]
535 {
536 wgpu::Backends::METAL
537 }
538 #[cfg(not(any(target_os = "windows", target_os = "macos")))]
539 {
540 wgpu::Backends::PRIMARY
541 }
542}
543
544/// `wgpu::InstanceDescriptor` for editor surfaces, with the DX12
545/// shader compiler pinned to **FXC**.
546///
547/// wgpu 29 defaults DX12 to a dynamically-loaded **DXC**
548/// (`dxcompiler.dll`). When a host process has already loaded its own
549/// incompatible `dxcompiler.dll` - Pro Tools does - wgpu's
550/// `DxcCreateInstance` returns `E_NOINTERFACE` and the *entire* DX12
551/// backend fails to initialise, leaving the instance with zero
552/// adapters: a blank editor (egui / built-in) or a panic on the
553/// `.expect` (slint). FXC (`d3dcompiler_47.dll`, always present on
554/// Windows, never conflicts) sidesteps it. wgpu 0.19 (iced)
555/// defaulted to FXC, which is why iced was never affected.
556#[must_use]
557pub fn editor_instance_descriptor() -> wgpu::InstanceDescriptor {
558 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
559 desc.backends = editor_wgpu_backends();
560 desc.backend_options.dx12.shader_compiler = wgpu::Dx12Compiler::Fxc;
561 desc
562}
563
564/// Bridge a baseview raw-window-handle 0.5 to a wgpu-compatible
565/// `SurfaceTargetUnsafe` using rwh 0.6 types.
566///
567/// Both `truce-gui`'s blit pipeline (cpu mode) and
568/// `truce_gpu::WgpuBackend::from_window` (gpu mode, used by
569/// `GpuEditor`) need this bridge; the two crates can't share a
570/// canonical copy without forming a dep cycle, so each carries its
571/// own ~100 LOC version. The two are kept in sync by inspection.
572///
573/// # Safety
574/// The window handle must be valid for the lifetime of the returned surface.
575#[cfg(not(target_os = "ios"))]
576#[must_use]
577pub unsafe fn create_wgpu_surface(
578 instance: &wgpu::Instance,
579 window: &baseview::Window,
580) -> Option<wgpu::Surface<'static>> {
581 #[cfg(target_os = "windows")]
582 {
583 let RwhRawWindowHandle::Win32(handle) = window.raw_window_handle() else {
584 return None;
585 };
586 unsafe { create_wgpu_surface_from_hwnd(instance, handle.hwnd as isize) }
587 }
588 #[cfg(not(target_os = "windows"))]
589 unsafe {
590 let rwh = window.raw_window_handle();
591 let surface_target = match rwh {
592 #[cfg(target_os = "macos")]
593 RwhRawWindowHandle::AppKit(handle) => {
594 let ns_view = handle.ns_view;
595 if ns_view.is_null() {
596 return None;
597 }
598 let rwh6_window = wgpu::rwh::RawWindowHandle::AppKit(
599 wgpu::rwh::AppKitWindowHandle::new(std::ptr::NonNull::new(ns_view)?),
600 );
601 let rwh6_display =
602 wgpu::rwh::RawDisplayHandle::AppKit(wgpu::rwh::AppKitDisplayHandle::new());
603 wgpu::SurfaceTargetUnsafe::RawHandle {
604 raw_display_handle: Some(rwh6_display),
605 raw_window_handle: rwh6_window,
606 }
607 }
608 #[cfg(target_os = "linux")]
609 RwhRawWindowHandle::Xlib(handle) => {
610 let RwhRawDisplayHandle::Xlib(display_handle) = window.raw_display_handle() else {
611 return None;
612 };
613 let display_ptr = std::ptr::NonNull::new(display_handle.display);
614 let rwh6_window = wgpu::rwh::RawWindowHandle::Xlib(
615 wgpu::rwh::XlibWindowHandle::new(handle.window),
616 );
617 let rwh6_display = wgpu::rwh::RawDisplayHandle::Xlib(
618 wgpu::rwh::XlibDisplayHandle::new(display_ptr, display_handle.screen),
619 );
620 wgpu::SurfaceTargetUnsafe::RawHandle {
621 raw_display_handle: Some(rwh6_display),
622 raw_window_handle: rwh6_window,
623 }
624 }
625 _ => return None,
626 };
627
628 instance.create_surface_unsafe(surface_target).ok()
629 }
630}
631
632/// Windows-only variant of [`create_wgpu_surface`] that takes the raw
633/// HWND value instead of a `&baseview::Window`. An `isize` is `Send`,
634/// so callers can create the surface on a worker thread and keep the
635/// host's GUI thread free while the graphics driver initializes (a
636/// wedged driver can block device/surface creation indefinitely).
637///
638/// # Safety
639/// `hwnd` must be a live window handle that outlives the returned
640/// surface. If the window is destroyed first, swapchain creation fails
641/// with a driver error (DXGI validates the HWND) rather than UB.
642#[cfg(target_os = "windows")]
643#[must_use]
644pub unsafe fn create_wgpu_surface_from_hwnd(
645 instance: &wgpu::Instance,
646 hwnd: isize,
647) -> Option<wgpu::Surface<'static>> {
648 let mut win32 = wgpu::rwh::Win32WindowHandle::new(std::num::NonZeroIsize::new(hwnd)?);
649 // wgpu's Vulkan backend requires `hinstance` to be set
650 // (`vkCreateWin32SurfaceKHR` rejects a null HINSTANCE).
651 // baseview leaves the rwh 0.5 `hinstance` field at null,
652 // so populate it here with the running module's HMODULE.
653 win32.hinstance = current_module_hinstance();
654 let surface_target = wgpu::SurfaceTargetUnsafe::RawHandle {
655 raw_display_handle: Some(wgpu::rwh::RawDisplayHandle::Windows(
656 wgpu::rwh::WindowsDisplayHandle::new(),
657 )),
658 raw_window_handle: wgpu::rwh::RawWindowHandle::Win32(win32),
659 };
660 unsafe { instance.create_surface_unsafe(surface_target).ok() }
661}