diff --git a/docs/wayland-port.md b/docs/wayland-port.md index 58bdfa743..6c452d293 100644 --- a/docs/wayland-port.md +++ b/docs/wayland-port.md @@ -266,3 +266,14 @@ spaces in one process, and which one applies depends on which side of the bridge **THE PROBE ASKS "does the bridge work" FIRST**, with `wl_list_init`, which writes two pointers into a struct the guest owns. That is an observable effect rather than an inference, and until it holds every other result is noise. + +## The headless compositor needs a RENDERER, and its default is a no-op + +Measured while chasing a buffer that was attached and never released: weston's headless backend +selects the **no-op renderer** by default, and prints it as `no-op renderer SHM seed: 0` among a +page of capability lines nobody reads. A no-op renderer never reads a client buffer, so it never +releases one, and a client waiting for the release waits forever with nothing in any log. + +The checks pass `--renderer=pixman` so the compositor actually composites in software. Worth +knowing generally: a headless compositor that "runs fine" can still be doing nothing at all, and +that will look exactly like a client bug. diff --git a/scripts/checks/buck-wayland-check.nu b/scripts/checks/buck-wayland-check.nu index 51a763343..8cb5e3e3d 100755 --- a/scripts/checks/buck-wayland-check.nu +++ b/scripts/checks/buck-wayland-check.nu @@ -93,7 +93,7 @@ def main [scratch?: string] { say "== starting weston, headless ==" let weston = (job spawn { with-env {XDG_RUNTIME_DIR: $xdg} { - do -i { ^weston --backend=headless --socket=cider-wl --width=1024 --height=768 out+err> $"($root)/weston.log" } + do -i { ^weston --backend=headless --renderer=pixman --socket=cider-wl --width=1024 --height=768 out+err> $"($root)/weston.log" } } }) # The socket appears a moment after the process does, and connecting before it exists looks @@ -190,6 +190,15 @@ def main [scratch?: string] { bad "no xdg_surface configure arrived, so the surface was never mapped" $failed = $failed + 1 } + # PIXELS, asserted on the FRAME CALLBACK rather than the buffer release. A compositor may + # legitimately hold a buffer after drawing with it, so demanding a release asks for more than + # the protocol promises; a frame callback is the compositor saying it drew. + if ($out | str contains "pixels=presented") { + ok "a wl_shm buffer was attached and the compositor presented it" + } else { + bad "the surface was never presented, so no pixels reached the compositor" + $failed = $failed + 1 + } print -e "" print -e ($out | lines | where {|l| $l =~ 'cider-wayland-probe' } | str join "\n") diff --git a/src/darwin/wayland/probe.rs b/src/darwin/wayland/probe.rs index 8e4b180f0..0a31d76cf 100644 --- a/src/darwin/wayland/probe.rs +++ b/src/darwin/wayland/probe.rs @@ -241,6 +241,147 @@ fn open_a_window( break; } } - CONFIGURED + if !CONFIGURED { + return false; + } + + // NOW THE PIXELS, which is the part a CGSSurface exists to do. + let shm = wl::cider_wl_registry_bind_shm(registry, globals.bound.shm_name, globals.bound.shm_version); + if shm.is_null() { + println!("cider-wayland-probe shm-bind=FAILED"); + return false; + } + match present_a_buffer(display, shm, surface) { + Ok(()) => true, + Err(why) => { + println!("cider-wayland-probe pixels=FAILED {why}"); + false + } + } + } +} + +static mut RELEASED: bool = false; +static mut PRESENTED: bool = false; + +extern "C" fn on_buffer_release(_data: *mut c_void, _buffer: *mut wl::WlBuffer) { + unsafe { RELEASED = true }; +} + +extern "C" fn on_frame_done(_data: *mut c_void, _cb: *mut wl::WlCallback, _time: u32) { + unsafe { PRESENTED = true }; +} + +/// Fill a buffer, attach it, and wait for the compositor to RELEASE it. +/// +/// The release is the assertion. Attaching proves nothing on its own: the client can hand over a +/// buffer the compositor never reads. A release event means it finished with those pages, so the +/// pixels crossed the socket, the fd survived the trip and the mapping was valid on both sides. +/// +/// # Safety +/// Called with a configured surface and a bound wl_shm. +unsafe fn present_a_buffer( + display: *mut wl::WlDisplay, + shm: *mut wl::WlShm, + surface: *mut wl::WlSurface, +) -> Result<(), String> { + use std::io::{Seek, SeekFrom, Write}; + use std::os::unix::io::AsRawFd; + + const W: i32 = 64; + const H: i32 = 64; + let stride = W * 4; + let size = (stride * H) as usize; + + // A PLAIN FILE, not shm_open. The compositor receives the DESCRIPTOR over the socket and mmaps + // that, so the file only has to be mmap-able and the right size; where it lives does not + // travel with it. This also sidesteps the question of whether the guest has a working + // /dev/shm, which it does not need to have. + let mut file = tempfile_in_guest()?; + let pixel = 0xffu32 << 24 | 0x30u32 << 16 | 0x60u32 << 8 | 0x90u32; // opaque, a flat colour + let row: Vec = std::iter::repeat(pixel.to_ne_bytes()).take(W as usize).flatten().collect(); + for _ in 0..H { + file.write_all(&row).map_err(|e| format!("write {e}"))?; } + file.flush().map_err(|e| format!("flush {e}"))?; + file.seek(SeekFrom::Start(0)).map_err(|e| format!("seek {e}"))?; + + let pool = unsafe { wl::cider_wl_shm_create_pool(shm, file.as_raw_fd(), size as i32) }; + if pool.is_null() { + return Err("create_pool returned null".into()); + } + let format = unsafe { wl::cider_wl_shm_format_xrgb8888() }; + let buffer = unsafe { wl::cider_wl_shm_pool_create_buffer(pool, 0, W, H, stride, format) }; + if buffer.is_null() { + return Err("create_buffer returned null".into()); + } + let listener = wl::WlBufferListener { release: on_buffer_release }; + unsafe { + wl::cider_wl_buffer_add_listener(buffer, &listener, std::ptr::null_mut()); + // ASK FOR A FRAME TOO, before the commit that carries the buffer: the callback is + // delivered when the compositor has PRESENTED, which separates "never drawn" from + // "drawn, buffer still held". + let frame = wl::cider_wl_surface_frame(surface); + let frame_listener = wl::WlCallbackListener { done: on_frame_done }; + if !frame.is_null() { + wl::cider_wl_callback_add_listener(frame, &frame_listener, std::ptr::null_mut()); + } + wl::cider_wl_surface_attach(surface, buffer, 0, 0); + wl::cider_wl_surface_damage(surface, 0, 0, W, H); + wl::cider_wl_surface_commit(surface); + wl::wl_display_flush(display); + // A ROUNDTRIP IS NOT A FRAME, and a headless compositor is SLOW to start repainting. + // Measured on weston 15 with the pixman renderer: the first frame callback after a + // surface is mapped took well over two seconds, so a 2 second budget reported "never + // presented" for a surface that was in the scene graph the whole time. Ten seconds. + for _ in 0..200 { + wl::wl_display_roundtrip(display); + if RELEASED && PRESENTED { + break; + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + wl::cider_wl_shm_pool_destroy(pool); + // ASK THE CONNECTION WHETHER IT IS STILL ALIVE. A protocol error is invisible from + // here otherwise: events simply stop arriving, which looks exactly like a compositor + // that has not got round to compositing yet. + println!("cider-wayland-probe display_error={}", wl::wl_display_get_error(display)); + // HOLD THE SURFACE OPEN when asked, so a scene-graph dump has something to look at. + // Without it the probe exits in about two seconds and every inspection races it. + if let Ok(secs) = std::env::var("CIDER_WAYLAND_HOLD") { + if let Ok(n) = secs.parse::() { + println!("cider-wayland-probe holding={n}s"); + for _ in 0..(n * 10) { + wl::wl_display_roundtrip(display); + std::thread::sleep(std::time::Duration::from_millis(100)); + } + } + } + println!("cider-wayland-probe presented={PRESENTED} released={RELEASED}"); + // PRESENTED IS THE ASSERTION, not the release. A compositor may legitimately hold a + // buffer after drawing with it, so demanding a release asks for more than the protocol + // promises; a frame callback is the compositor stating it drew. + if PRESENTED { + println!("cider-wayland-probe pixels=presented size={W}x{H}"); + Ok(()) + } else { + Err("no frame callback arrived, so the surface was never presented".into()) + } + } +} + +/// A file the guest can create and mmap. /tmp inside the container is a tmpfs, which is exactly +/// what this wants. +fn tempfile_in_guest() -> Result { + let path = format!("/tmp/cider-wayland-probe-{}.shm", std::process::id()); + let file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(&path) + .map_err(|e| format!("open {path}: {e}"))?; + // Unlinked immediately: the descriptor keeps it alive and nothing is left behind. + let _ = std::fs::remove_file(&path); + Ok(file) } diff --git a/src/darwin/wayland/shim.c b/src/darwin/wayland/shim.c index 2b317ef87..fae3bc9ec 100644 --- a/src/darwin/wayland/shim.c +++ b/src/darwin/wayland/shim.c @@ -96,3 +96,52 @@ int cider_xdg_wm_base_add_listener(struct xdg_wm_base *base, const struct xdg_wm_base_listener *listener, void *data) { return xdg_wm_base_add_listener(base, listener, data); } + +// --------------------------------------------------------------------------------------------- +// PIXELS. wl_shm hands the compositor a file descriptor and it mmaps the same pages the client +// wrote, which is how a CGSSurface will present a bitmap. All of this is inline upstream too. + +struct wl_shm_pool *cider_wl_shm_create_pool(struct wl_shm *shm, int32_t fd, int32_t size) { + return wl_shm_create_pool(shm, fd, size); +} + +struct wl_buffer *cider_wl_shm_pool_create_buffer(struct wl_shm_pool *pool, int32_t offset, + int32_t width, int32_t height, int32_t stride, + uint32_t format) { + return wl_shm_pool_create_buffer(pool, offset, width, height, stride, format); +} + +void cider_wl_shm_pool_destroy(struct wl_shm_pool *pool) { wl_shm_pool_destroy(pool); } + +void cider_wl_surface_attach(struct wl_surface *surface, struct wl_buffer *buffer, int32_t x, + int32_t y) { + wl_surface_attach(surface, buffer, x, y); +} + +void cider_wl_surface_damage(struct wl_surface *surface, int32_t x, int32_t y, int32_t width, + int32_t height) { + wl_surface_damage(surface, x, y, width, height); +} + +// THE RELEASE EVENT IS THE PROOF. A compositor releases a buffer once it has finished reading it, +// so a release means our pixels were actually consumed rather than merely offered. +int cider_wl_buffer_add_listener(struct wl_buffer *buffer, const struct wl_buffer_listener *listener, + void *data) { + return wl_buffer_add_listener(buffer, listener, data); +} + +// WL_SHM_FORMAT_XRGB8888 is guaranteed by the protocol, unlike most formats, so the probe uses it +// rather than asking which are supported. +uint32_t cider_wl_shm_format_xrgb8888(void) { return WL_SHM_FORMAT_XRGB8888; } + +// A FRAME CALLBACK FIRES WHEN THE COMPOSITOR HAS PRESENTED, which is a different claim from a +// buffer release: release is about buffer LIFETIME and can be deferred, while a frame callback is +// the compositor saying it drew. Asking for both means a failure says which half is missing. +struct wl_callback *cider_wl_surface_frame(struct wl_surface *surface) { + return wl_surface_frame(surface); +} + +int cider_wl_callback_add_listener(struct wl_callback *callback, + const struct wl_callback_listener *listener, void *data) { + return wl_callback_add_listener(callback, listener, data); +} diff --git a/src/darwin/wayland/wl.rs b/src/darwin/wayland/wl.rs index fd87a4a12..968c92ef0 100644 --- a/src/darwin/wayland/wl.rs +++ b/src/darwin/wayland/wl.rs @@ -20,6 +20,9 @@ pub enum WlSurface {} pub enum XdgWmBase {} pub enum XdgSurface {} pub enum XdgToplevel {} +pub enum WlShmPool {} +pub enum WlBuffer {} +pub enum WlCallback {} /// libwayland's intrusive list head: two pointers, and wl_list_init makes both point at it. #[repr(C)] @@ -66,6 +69,23 @@ unsafe extern "C" { pub fn cider_xdg_surface_add_listener(s: *mut XdgSurface, l: *const XdgSurfaceListener, data: *mut c_void) -> c_int; pub fn cider_xdg_wm_base_pong(b: *mut XdgWmBase, serial: u32); pub fn cider_xdg_wm_base_add_listener(b: *mut XdgWmBase, l: *const XdgWmBaseListener, data: *mut c_void) -> c_int; + + // Pixels. + pub fn cider_wl_shm_create_pool(shm: *mut WlShm, fd: c_int, size: i32) -> *mut WlShmPool; + pub fn cider_wl_shm_pool_create_buffer(pool: *mut WlShmPool, offset: i32, width: i32, height: i32, stride: i32, format: u32) -> *mut WlBuffer; + pub fn cider_wl_shm_pool_destroy(pool: *mut WlShmPool); + pub fn cider_wl_surface_attach(s: *mut WlSurface, b: *mut WlBuffer, x: i32, y: i32); + pub fn cider_wl_surface_damage(s: *mut WlSurface, x: i32, y: i32, w: i32, h: i32); + pub fn cider_wl_buffer_add_listener(b: *mut WlBuffer, l: *const WlBufferListener, data: *mut c_void) -> c_int; + pub fn cider_wl_shm_format_xrgb8888() -> u32; + + /// Nonzero once the connection has failed. A protocol error kills the connection silently + /// from the client's point of view, so without asking, a missing event and a dead socket look + /// the same. + pub fn wl_display_get_error(display: *mut WlDisplay) -> c_int; + pub fn wl_display_flush(display: *mut WlDisplay) -> c_int; + pub fn cider_wl_surface_frame(s: *mut WlSurface) -> *mut WlCallback; + pub fn cider_wl_callback_add_listener(c: *mut WlCallback, l: *const WlCallbackListener, data: *mut c_void) -> c_int; } /// The layout libwayland expects: two function pointers, in this order. It is passed by pointer @@ -155,3 +175,17 @@ pub struct Bound { pub xdg_name: u32, pub xdg_version: u32, } + +/// One callback: the compositor has finished with the buffer. That event is the only honest +/// evidence from the client side that the pixels were CONSUMED and not merely handed over. +#[repr(C)] +pub struct WlBufferListener { + pub release: extern "C" fn(data: *mut c_void, buffer: *mut WlBuffer), +} + +/// The compositor calls this once it has PRESENTED the surface. Independent of buffer lifetime, +/// so it distinguishes "never drawn" from "drawn but the buffer is still held". +#[repr(C)] +pub struct WlCallbackListener { + pub done: extern "C" fn(data: *mut c_void, callback: *mut WlCallback, time: u32), +}