Skip to main content

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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
88pub enum SurfaceTarget {
89    /// Wayland: `wl_display*` + `wl_surface*`, as returned by
90    /// `libwayland-client` (topic 15's WSI-ABI exception — the driver calls
91    /// libwayland functions on these).
92    Wayland {
93        /// `struct wl_display *`.
94        display: NonNull<c_void>,
95        /// `struct wl_surface *`.
96        surface: NonNull<c_void>,
97    },
98    /// X11 through XCB: `xcb_connection_t*` plus the window's XID.
99    Xcb {
100        /// `xcb_connection_t *`.
101        connection: NonNull<c_void>,
102        /// `xcb_window_t` — an XID, not a pointer.
103        window: u32,
104        /// `xcb_visualid_t` of the window, needed by some backends to pick a
105        /// compatible format.
106        visual_id: u32,
107    },
108    /// Windows: module instance + window handle.
109    Win32 {
110        /// `HINSTANCE` of the module that registered the window class.
111        hinstance: NonNull<c_void>,
112        /// `HWND`.
113        hwnd: NonNull<c_void>,
114    },
115    /// macOS/iOS: a `CAMetalLayer*`.
116    ///
117    /// The shell creates and owns the layer (attaching it to its `NSView`), so
118    /// backends never have to touch AppKit — Metal takes the layer directly and
119    /// `VK_EXT_metal_surface` takes it through MoltenVK.
120    AppKit {
121        /// `CAMetalLayer *`.
122        layer: NonNull<c_void>,
123    },
124    /// Web: an integer key into the shell's JS-side canvas registry.
125    ///
126    /// Topic 15 specifies a hand-rolled JS shim rather than `wasm-bindgen`, so
127    /// the canvas is identified by a number the shim assigns — no string
128    /// allocation crosses the wasm boundary, and this variant stays `Copy`.
129    Web {
130        /// Registry key handed out by the shell's JS shim.
131        canvas_id: u32,
132    },
133    /// No window at all: render into a backend-managed image ring and read
134    /// back.
135    ///
136    /// `HeadlessShell` (topic 15) and `crcbl screenshot` (topic 11) need a
137    /// target to hand the HAL even though there is nothing to present to.
138    /// Backends implement this as a swapchain-shaped rotation of offscreen
139    /// images, which is what makes the P1 golden-image e2e run through the
140    /// *same* acquire/present code path as a real window instead of a
141    /// second, less-tested one.
142    ///
143    /// # It deliberately carries no size
144    ///
145    /// It used to carry `width` and `height`, and that was a defect. Every
146    /// other variant names *handles*, which a resize does not touch — which is
147    /// exactly why `crcbl-shell` only requires a target to be re-queried after
148    /// a **mode** change. A size made this one variant change value on every
149    /// resize, so a surface created from an earlier target silently described
150    /// the wrong extent, and the seam had one rule for five variants and
151    /// another for the sixth.
152    ///
153    /// The extent comes from where it comes from for every other target: the
154    /// shell's `WindowState::size`, passed to the backend in the swapchain
155    /// descriptor. `crcbl-hal`'s `swapchain` module states that precedence as
156    /// a backend obligation. (No intra-doc link: this crate must not depend on
157    /// `crcbl-shell` — see the module docs.)
158    Offscreen,
159}
160
161impl SurfaceTarget {
162    /// A short, stable name for the platform this target names.
163    ///
164    /// For logs and error messages — `"wayland"`, `"xcb"`, `"win32"`,
165    /// `"appkit"`, `"web"`, `"offscreen"`.
166    #[must_use]
167    pub const fn platform_name(&self) -> &'static str {
168        match self {
169            Self::Wayland { .. } => "wayland",
170            Self::Xcb { .. } => "xcb",
171            Self::Win32 { .. } => "win32",
172            Self::AppKit { .. } => "appkit",
173            Self::Web { .. } => "web",
174            Self::Offscreen => "offscreen",
175        }
176    }
177
178    /// Whether this target presents to a real window server.
179    ///
180    /// `false` only for [`SurfaceTarget::Offscreen`]. Backends use it to skip
181    /// WSI extension checks and present-queue selection.
182    #[must_use]
183    pub const fn is_windowed(&self) -> bool {
184        !matches!(self, Self::Offscreen)
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    fn dangling() -> NonNull<c_void> {
193        NonNull::dangling()
194    }
195
196    #[test]
197    fn platform_names_are_distinct_and_stable() {
198        let targets = [
199            SurfaceTarget::Wayland {
200                display: dangling(),
201                surface: dangling(),
202            },
203            SurfaceTarget::Xcb {
204                connection: dangling(),
205                window: 1,
206                visual_id: 2,
207            },
208            SurfaceTarget::Win32 {
209                hinstance: dangling(),
210                hwnd: dangling(),
211            },
212            SurfaceTarget::AppKit { layer: dangling() },
213            SurfaceTarget::Web { canvas_id: 7 },
214            SurfaceTarget::Offscreen,
215        ];
216        let mut names: Vec<_> = targets.iter().map(SurfaceTarget::platform_name).collect();
217        let count = names.len();
218        names.sort_unstable();
219        names.dedup();
220        assert_eq!(names.len(), count, "platform names must be unique");
221    }
222
223    #[test]
224    fn only_offscreen_is_unwindowed() {
225        assert!(
226            SurfaceTarget::Web { canvas_id: 0 }.is_windowed(),
227            "a canvas is a real presentation target"
228        );
229        assert!(!SurfaceTarget::Offscreen.is_windowed());
230    }
231
232    #[test]
233    fn target_is_copy_and_comparable() {
234        let a = SurfaceTarget::Web { canvas_id: 3 };
235        let b = a;
236        assert_eq!(a, b);
237        assert_ne!(a, SurfaceTarget::Web { canvas_id: 4 });
238    }
239
240    // The deliberate non-`Send`/non-`Sync` property from the module docs is
241    // enforced structurally rather than by a test: [`NonNull`] is `!Send` and
242    // `!Sync`, so the only way to widen it is a hand-written `unsafe impl` in
243    // this file. Asserting the *absence* of an auto trait needs either
244    // specialization or a `trybuild` compile-fail fixture, and neither is worth
245    // a dependency to restate what the type system already guarantees. If you
246    // are here to add that `unsafe impl`, read the module docs first.
247}