crcbl_core/surface.rs
1//! [`SurfaceTarget`] — the one sanctioned platform leak in the engine.
2//!
3//! # Why this lives in `crcbl-core`
4//!
5//! The windowing rules in `docs/notes/backends.md` describe `SurfaceTarget` as
6//! "the *single* sanctioned platform leak": the shell produces it, a HAL backend consumes it,
7//! and nothing in between is allowed to look inside. That makes it **shared
8//! vocabulary between two crates that must not depend on each other**:
9//!
10//! ```text
11//! crcbl-core (SurfaceTarget lives here)
12//! ^ ^
13//! | |
14//! crcbl-shell crcbl-hal
15//! ^
16//! |
17//! crcbl-vk / crcbl-webgpu / …
18//! ```
19//!
20//! * `crcbl-shell` must not depend on `crcbl-hal` — a windowing library that
21//! needs a graphics HAL to compile has the dependency backwards, and
22//! `HeadlessShell` would drag a backend seam into CI-only builds.
23//! * `crcbl-hal` must not depend on `crcbl-shell` — backends are used offscreen
24//! (`crcbl screenshot`, golden-image e2e) with no window anywhere in the
25//! process, and `crcbl-shell` will grow real platform code that a headless
26//! render test has no business linking.
27//!
28//! Both already depend on `crcbl-core`, so putting the type here is the only
29//! placement that does not create a cycle or a spurious dependency. It costs
30//! `crcbl-core` nothing: the type is a plain enum of integers and raw pointers
31//! with no methods that touch them.
32//!
33//! # Safety model
34//!
35//! Constructing a `SurfaceTarget` is **safe**. Nothing here dereferences a
36//! pointer, and a wrong value is inert until a backend uses it. The obligation
37//! sits at the single point that can act on it — `crcbl-hal`'s
38//! `Instance::create_surface`, which is `unsafe` and documents the full
39//! contract (handles are live objects of the stated kind, and they outlive the
40//! surface created from them).
41//!
42//! This mirrors `raw-window-handle` 0.6 and `wgpu::Instance::create_surface_unsafe`,
43//! and it means this module — the one place platform pointers enter the engine
44//! — contains no `unsafe` at all.
45//!
46//! # Thread safety
47//!
48//! `SurfaceTarget` is deliberately **not** `Send`/`Sync` (it holds
49//! [`NonNull`]). Several of these handles are thread-affine in ways we cannot
50//! promise away — a Win32 `HWND` belongs to the thread that pumps its message
51//! queue, and an `NSView`/`CAMetalLayer` is main-thread-only on macOS. The
52//! practical consequence is the one every engine already lives with: create the
53//! HAL surface on the thread that owns the window. `raw-window-handle` reached
54//! the same conclusion in 0.6 after shipping the unsound `Send` version.
55
56use core::ffi::c_void;
57use core::ptr::NonNull;
58
59/// A window's native presentation handles, in the form a HAL backend needs to
60/// create a surface.
61///
62/// Produced by `crcbl-shell`, destructured *only* by HAL backends. Engine code,
63/// samples and the editor pass it through opaquely; a `#[cfg]` on a platform in
64/// a consumer crate is a regression.
65///
66/// # Exhaustiveness
67///
68/// This enum is intentionally **not** `#[non_exhaustive]`. Adding a platform
69/// should break every backend's `match` loudly at compile time — the whole
70/// engine is one workspace, and a silently-ignored new platform is exactly the
71/// failure this type exists to prevent.
72///
73/// # Variants and their producers
74///
75/// | Variant | Shell backend | Vulkan entry point |
76/// | --- | --- | --- |
77/// | [`Wayland`](Self::Wayland) | Wayland (P0) | `vkCreateWaylandSurfaceKHR` |
78/// | [`Xcb`](Self::Xcb) | X11 (P0) | `vkCreateXcbSurfaceKHR` |
79/// | [`Web`](Self::Web) | canvas (P5) | — (WebGPU `GPUCanvasContext`) |
80/// | [`Win32`](Self::Win32) | Win32 (P14) | `vkCreateWin32SurfaceKHR` |
81/// | [`AppKit`](Self::AppKit) | AppKit (P14) | `vkCreateMetalSurfaceEXT` |
82/// | [`Offscreen`](Self::Offscreen) | `HeadlessShell` | — (image ring, no WSI) |
83///
84/// Xlib is absent on purpose: topic 15 links `libxcb`, not `libX11`, so no
85/// shell backend can produce an `Xlib` target and a variant nobody constructs
86/// is a variant every backend still has to handle.
87///
88/// # Transparency travels here
89///
90/// Every variant that names a desktop window carries `transparent`: the shell
91/// sets it from the window's `WindowDesc::transparent` (`crcbl-shell`, named
92/// rather than linked, see the module docs), and
93/// [`transparent`](Self::transparent) reads it whatever the platform. The
94/// window is what knows it was made transparent, so the renderer learns it
95/// from the window's own handles rather than from a second statement that
96/// could disagree. A presenter that sees it asks for premultiplied alpha, and
97/// one whose surface cannot composite that way refuses rather than drawing the
98/// window opaque.
99///
100/// [`Web`](Self::Web) and [`Offscreen`](Self::Offscreen) carry no flag. The web
101/// shell refuses a transparent window, since what shows through a canvas is the
102/// page's business, so a canvas target is never one; an offscreen ring has no
103/// compositor to blend with, and its readback holds whatever alpha the frame
104/// wrote either way.
105#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
106pub enum SurfaceTarget {
107 /// Wayland: `wl_display*` + `wl_surface*`, as returned by
108 /// `libwayland-client` (topic 15's WSI-ABI exception — the driver calls
109 /// libwayland functions on these).
110 Wayland {
111 /// `struct wl_display *`.
112 display: NonNull<c_void>,
113 /// `struct wl_surface *`.
114 surface: NonNull<c_void>,
115 /// The window was created transparent. The compositor blends the
116 /// surface by its buffer's alpha, so a presenter asks for
117 /// premultiplied alpha; the surface itself is made the same way.
118 transparent: bool,
119 /// The `dev_t` of the DRM device the compositor composites on, as
120 /// `zwp_linux_dmabuf_feedback_v1.main_device` names it: the GPU a
121 /// buffer reaches without a copy between devices, which a backend
122 /// matches to one of its adapters for `Instance::display_adapter`
123 /// (`crcbl-hal`, named rather than linked, see the module docs).
124 /// `None` when the compositor offers no `zwp_linux_dmabuf_v1` at
125 /// version 4 or had not answered when the target was read.
126 main_device: Option<u64>,
127 },
128 /// X11 through XCB: `xcb_connection_t*` plus the window's XID.
129 Xcb {
130 /// `xcb_connection_t *`.
131 connection: NonNull<c_void>,
132 /// `xcb_window_t` — an XID, not a pointer.
133 window: u32,
134 /// `xcb_visualid_t` of the window, needed by some backends to pick a
135 /// compatible format.
136 visual_id: u32,
137 /// The window was created transparent. The X11 shell makes every
138 /// window on the root visual, which has no alpha, and refuses a
139 /// transparent one, so it always sets this `false`; the field is here
140 /// so that a backend reads transparency the same way on every window
141 /// system.
142 transparent: bool,
143 /// The `dev_t` of the DRM device the X server renders on, from DRI3
144 /// `Open` — the counterpart of
145 /// [`Wayland`](Self::Wayland)'s `main_device`, and matched to an
146 /// adapter the same way. `None` without DRI3 (Xvfb has none).
147 main_device: Option<u64>,
148 },
149 /// Windows: module instance + window handle, and whether the window was
150 /// made for per-pixel transparency.
151 Win32 {
152 /// `HINSTANCE` of the module that registered the window class.
153 hinstance: NonNull<c_void>,
154 /// `HWND`.
155 hwnd: NonNull<c_void>,
156 /// The window was created transparent (`WS_EX_NOREDIRECTIONBITMAP`):
157 /// it has no redirection surface, so nothing presented to the `HWND`
158 /// itself is shown, and a backend must present through
159 /// DirectComposition with premultiplied alpha.
160 ///
161 /// Windows is the one window system where this changes how a backend
162 /// makes the *surface*, not only which alpha its swapchain asks for.
163 /// A backend that cannot present this way refuses the surface by name
164 /// rather than drawing into a window that shows nothing.
165 transparent: bool,
166 },
167 /// macOS/iOS: a `CAMetalLayer*`.
168 ///
169 /// The shell creates and owns the layer (attaching it to its `NSView`), so
170 /// backends never have to touch AppKit — Metal takes the layer directly and
171 /// `VK_EXT_metal_surface` takes it through MoltenVK.
172 AppKit {
173 /// `CAMetalLayer *`.
174 layer: NonNull<c_void>,
175 /// The window was created transparent: the shell made the window and
176 /// this layer non-opaque. A presenter keeps the layer non-opaque and
177 /// asks for premultiplied alpha, which is how Core Animation blends a
178 /// layer's contents.
179 transparent: bool,
180 },
181 /// Web: an integer key into the shell's JS-side canvas registry.
182 ///
183 /// Topic 15 specifies a hand-rolled JS shim rather than `wasm-bindgen`, so
184 /// the canvas is identified by a number the shim assigns — no string
185 /// allocation crosses the wasm boundary, and this variant stays `Copy`.
186 Web {
187 /// Registry key handed out by the shell's JS shim.
188 canvas_id: u32,
189 },
190 /// No window at all: render into a backend-managed image ring and read
191 /// back.
192 ///
193 /// `HeadlessShell` (topic 15) and `crcbl screenshot` (topic 11) need a
194 /// target to hand the HAL even though there is nothing to present to.
195 /// Backends implement this as a swapchain-shaped rotation of offscreen
196 /// images, which is what makes the P1 golden-image e2e run through the
197 /// *same* acquire/present code path as a real window instead of a
198 /// second, less-tested one.
199 ///
200 /// # It deliberately carries no size
201 ///
202 /// It used to carry `width` and `height`, and that was a defect. Every
203 /// other variant names *handles*, which a resize does not touch — which is
204 /// exactly why `crcbl-shell` only requires a target to be re-queried after
205 /// a **mode** change. A size made this one variant change value on every
206 /// resize, so a surface created from an earlier target silently described
207 /// the wrong extent, and the seam had one rule for five variants and
208 /// another for the sixth.
209 ///
210 /// The extent comes from where it comes from for every other target: the
211 /// shell's `WindowState::size`, passed to the backend in the swapchain
212 /// descriptor. `crcbl-hal`'s `swapchain` module states that precedence as
213 /// a backend obligation. (No intra-doc link: this crate must not depend on
214 /// `crcbl-shell` — see the module docs.)
215 Offscreen,
216}
217
218impl SurfaceTarget {
219 /// A short, stable name for the platform this target names.
220 ///
221 /// For logs and error messages — `"wayland"`, `"xcb"`, `"win32"`,
222 /// `"appkit"`, `"web"`, `"offscreen"`.
223 #[must_use]
224 pub const fn platform_name(&self) -> &'static str {
225 match self {
226 Self::Wayland { .. } => "wayland",
227 Self::Xcb { .. } => "xcb",
228 Self::Win32 { .. } => "win32",
229 Self::AppKit { .. } => "appkit",
230 Self::Web { .. } => "web",
231 Self::Offscreen => "offscreen",
232 }
233 }
234
235 /// Whether the window behind this target was created transparent, so the
236 /// desktop should show through where the frame's alpha is below one.
237 ///
238 /// Always `false` for [`Web`](Self::Web) and
239 /// [`Offscreen`](Self::Offscreen); see the type's docs for why they carry
240 /// no flag.
241 #[must_use]
242 pub const fn transparent(&self) -> bool {
243 match *self {
244 Self::Wayland { transparent, .. }
245 | Self::Xcb { transparent, .. }
246 | Self::Win32 { transparent, .. }
247 | Self::AppKit { transparent, .. } => transparent,
248 Self::Web { .. } | Self::Offscreen => false,
249 }
250 }
251
252 /// Whether this target presents to a real window server.
253 ///
254 /// `false` only for [`SurfaceTarget::Offscreen`]. Backends use it to skip
255 /// WSI extension checks and present-queue selection.
256 #[must_use]
257 pub const fn is_windowed(&self) -> bool {
258 !matches!(self, Self::Offscreen)
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 fn dangling() -> NonNull<c_void> {
267 NonNull::dangling()
268 }
269
270 #[test]
271 fn platform_names_are_distinct_and_stable() {
272 let targets = [
273 SurfaceTarget::Wayland {
274 display: dangling(),
275 surface: dangling(),
276 transparent: false,
277 main_device: None,
278 },
279 SurfaceTarget::Xcb {
280 connection: dangling(),
281 window: 1,
282 visual_id: 2,
283 transparent: false,
284 main_device: None,
285 },
286 SurfaceTarget::Win32 {
287 hinstance: dangling(),
288 hwnd: dangling(),
289 transparent: false,
290 },
291 SurfaceTarget::AppKit {
292 layer: dangling(),
293 transparent: false,
294 },
295 SurfaceTarget::Web { canvas_id: 7 },
296 SurfaceTarget::Offscreen,
297 ];
298 let mut names: Vec<_> = targets.iter().map(SurfaceTarget::platform_name).collect();
299 let count = names.len();
300 names.sort_unstable();
301 names.dedup();
302 assert_eq!(names.len(), count, "platform names must be unique");
303 }
304
305 #[test]
306 fn only_offscreen_is_unwindowed() {
307 assert!(
308 SurfaceTarget::Web { canvas_id: 0 }.is_windowed(),
309 "a canvas is a real presentation target"
310 );
311 assert!(!SurfaceTarget::Offscreen.is_windowed());
312 }
313
314 /// Every windowed variant answers its own flag, both ways, and the two
315 /// that carry none answer `false`.
316 #[test]
317 fn transparent_reads_each_windowed_variants_own_flag() {
318 for flag in [false, true] {
319 let targets = [
320 SurfaceTarget::Wayland {
321 display: dangling(),
322 surface: dangling(),
323 transparent: flag,
324 main_device: None,
325 },
326 SurfaceTarget::Xcb {
327 connection: dangling(),
328 window: 1,
329 visual_id: 2,
330 transparent: flag,
331 main_device: None,
332 },
333 SurfaceTarget::Win32 {
334 hinstance: dangling(),
335 hwnd: dangling(),
336 transparent: flag,
337 },
338 SurfaceTarget::AppKit {
339 layer: dangling(),
340 transparent: flag,
341 },
342 ];
343 for target in targets {
344 assert_eq!(target.transparent(), flag, "{target:?}");
345 }
346 }
347 assert!(!SurfaceTarget::Web { canvas_id: 0 }.transparent());
348 assert!(!SurfaceTarget::Offscreen.transparent());
349 }
350
351 #[test]
352 fn target_is_copy_and_comparable() {
353 let a = SurfaceTarget::Web { canvas_id: 3 };
354 let b = a;
355 assert_eq!(a, b);
356 assert_ne!(a, SurfaceTarget::Web { canvas_id: 4 });
357 }
358
359 // The deliberate non-`Send`/non-`Sync` property from the module docs is
360 // enforced structurally rather than by a test: [`NonNull`] is `!Send` and
361 // `!Sync`, so the only way to widen it is a hand-written `unsafe impl` in
362 // this file. Asserting the *absence* of an auto trait needs either
363 // specialization or a `trybuild` compile-fail fixture, and neither is worth
364 // a dependency to restate what the type system already guarantees. If you
365 // are here to add that `unsafe impl`, read the module docs first.
366}