diff --git a/local/recipes/wayland/redbear-compositor/source/src/bin/redbear-compositor-check.rs b/local/recipes/wayland/redbear-compositor/source/src/bin/redbear-compositor-check.rs index d6e0a58c0..4838d88d8 100644 --- a/local/recipes/wayland/redbear-compositor/source/src/bin/redbear-compositor-check.rs +++ b/local/recipes/wayland/redbear-compositor/source/src/bin/redbear-compositor-check.rs @@ -14,17 +14,36 @@ const WL_DISPLAY_GET_REGISTRY: u16 = 1; const WL_REGISTRY_BIND: u16 = 0; const WL_REGISTRY_GLOBAL: u16 = 0; const WL_COMPOSITOR_CREATE_SURFACE: u16 = 0; +const WL_COMPOSITOR_CREATE_REGION: u16 = 1; +const WL_FIXES_DESTROY_REGISTRY: u16 = 1; +const WL_FIXES_ACK_GLOBAL_REMOVE: u16 = 2; +const WL_SEAT_CAPABILITIES: u16 = 0; +const WL_SEAT_NAME: u16 = 1; const WL_SHM_CREATE_POOL: u16 = 0; const WL_SHM_FORMAT: u16 = 0; const WL_SHM_POOL_CREATE_BUFFER: u16 = 0; -const WL_SURFACE_ATTACH: u16 = 0; -const WL_SURFACE_COMMIT: u16 = 5; +const WL_OUTPUT_GEOMETRY: u16 = 0; +const WL_OUTPUT_MODE: u16 = 1; +const WL_OUTPUT_DONE: u16 = 2; +const WL_OUTPUT_SCALE: u16 = 3; +const WL_OUTPUT_NAME: u16 = 4; +const WL_OUTPUT_DESCRIPTION: u16 = 5; +const WL_SURFACE_ATTACH: u16 = 1; +const WL_SURFACE_SET_OPAQUE_REGION: u16 = 4; +const WL_SURFACE_COMMIT: u16 = 6; +const WL_REGION_DESTROY: u16 = 0; const WL_CALLBACK_DONE: u16 = 0; +const XDG_WM_BASE_CREATE_POSITIONER: u16 = 1; const XDG_WM_BASE_GET_XDG_SURFACE: u16 = 2; const XDG_SURFACE_GET_TOPLEVEL: u16 = 1; +const XDG_SURFACE_GET_POPUP: u16 = 2; +const XDG_POSITIONER_DESTROY: u16 = 0; +const XDG_POSITIONER_SET_SIZE: u16 = 1; +const XDG_POPUP_DESTROY: u16 = 0; const XDG_SURFACE_ACK_CONFIGURE: u16 = 4; const XDG_SURFACE_CONFIGURE: u16 = 0; const XDG_TOPLEVEL_CONFIGURE: u16 = 0; +const XDG_POPUP_CONFIGURE: u16 = 0; const WL_SHM_FORMAT_XRGB8888: u32 = 1; fn push_u32(buf: &mut Vec, value: u32) { @@ -61,7 +80,18 @@ fn read_u32(data: &[u8], cursor: &mut usize) -> Result { } fn read_wayland_string(data: &[u8], cursor: &mut usize) -> Result { - let length = read_u32(data, cursor)? as usize; + if *cursor + 4 > data.len() { + return Err(String::from( + "unexpected end of message while reading string length", + )); + } + let length = u32::from_le_bytes([ + data[*cursor], + data[*cursor + 1], + data[*cursor + 2], + data[*cursor + 3], + ]) as usize; + *cursor += 4; if length == 0 { return Ok(String::new()); } @@ -238,11 +268,11 @@ impl WaylandProbe { } } -fn collect_globals(probe: &mut WaylandProbe) -> Result, String> { +fn collect_globals(probe: &mut WaylandProbe) -> Result, String> { let registry_id = probe.get_registry()?; let mut globals = HashMap::new(); - for _ in 0..6 { + for _ in 0..9 { let (object_id, opcode, payload) = probe.read_message()?; if object_id != registry_id || opcode != WL_REGISTRY_GLOBAL { return Err(format!( @@ -254,13 +284,124 @@ fn collect_globals(probe: &mut WaylandProbe) -> Result, Str let mut cursor = 0; let name = read_u32(&payload, &mut cursor)?; let interface = read_wayland_string(&payload, &mut cursor)?; - let _version = read_u32(&payload, &mut cursor)?; - globals.insert(interface, name); + let version = read_u32(&payload, &mut cursor)?; + globals.insert(interface, (name, version)); } Ok(globals) } +fn expect_output_metadata(probe: &mut WaylandProbe, output_id: u32) -> Result<(), String> { + let mut saw_geometry = false; + let mut saw_mode = false; + let mut saw_scale = false; + let mut saw_name = false; + let mut saw_description = false; + let mut saw_done = false; + + for _ in 0..6 { + let (object_id, opcode, payload) = probe.read_message()?; + if object_id != output_id { + return Err(format!( + "unexpected wl_output event object: expected {}, got {}", + output_id, object_id + )); + } + + match opcode { + WL_OUTPUT_GEOMETRY => { + if payload.len() < 28 { + return Err(format!( + "short wl_output.geometry payload: {}", + payload.len() + )); + } + saw_geometry = true; + } + WL_OUTPUT_MODE => { + if payload.len() != 16 { + return Err(format!("invalid wl_output.mode payload: {}", payload.len())); + } + saw_mode = true; + } + WL_OUTPUT_SCALE => { + if payload != 1i32.to_le_bytes() { + return Err(format!("unexpected wl_output.scale payload: {payload:?}")); + } + saw_scale = true; + } + WL_OUTPUT_NAME => { + let mut cursor = 0; + let name = read_wayland_string(&payload, &mut cursor)?; + if name != "RedBear-0" { + return Err(format!("unexpected wl_output.name: {name}")); + } + saw_name = true; + } + WL_OUTPUT_DESCRIPTION => { + let mut cursor = 0; + let description = read_wayland_string(&payload, &mut cursor)?; + if description != "Red Bear OS framebuffer output" { + return Err(format!("unexpected wl_output.description: {description}")); + } + saw_description = true; + } + WL_OUTPUT_DONE => saw_done = true, + _ => return Err(format!("unexpected wl_output opcode: {opcode}")), + } + } + + if saw_geometry && saw_mode && saw_scale && saw_name && saw_description && saw_done { + Ok(()) + } else { + Err(format!( + "incomplete wl_output metadata: geometry={saw_geometry} mode={saw_mode} scale={saw_scale} name={saw_name} description={saw_description} done={saw_done}" + )) + } +} + +fn expect_seat_metadata(probe: &mut WaylandProbe, seat_id: u32) -> Result<(), String> { + let mut saw_capabilities = false; + let mut saw_name = false; + + for _ in 0..2 { + let (object_id, opcode, payload) = probe.read_message()?; + if object_id != seat_id { + return Err(format!( + "unexpected wl_seat event object: expected {}, got {}", + seat_id, object_id + )); + } + match opcode { + WL_SEAT_CAPABILITIES => { + if payload != 1u32.to_le_bytes() { + return Err(format!( + "unexpected wl_seat.capabilities payload: {payload:?}" + )); + } + saw_capabilities = true; + } + WL_SEAT_NAME => { + let mut cursor = 0; + let name = read_wayland_string(&payload, &mut cursor)?; + if name != "seat0" { + return Err(format!("unexpected wl_seat.name: {name}")); + } + saw_name = true; + } + _ => return Err(format!("unexpected wl_seat opcode: {opcode}")), + } + } + + if saw_capabilities && saw_name { + Ok(()) + } else { + Err(format!( + "incomplete wl_seat metadata: capabilities={saw_capabilities} name={saw_name}" + )) + } +} + fn expect_shm_formats(probe: &mut WaylandProbe, shm_id: u32) -> Result<(), String> { let mut formats = Vec::new(); @@ -336,6 +477,36 @@ fn expect_xdg_configure( ])) } +fn expect_xdg_popup_configure( + probe: &mut WaylandProbe, + popup_id: u32, + xdg_surface_id: u32, +) -> Result { + let (object_id, opcode, payload) = probe.read_message()?; + if object_id != popup_id || opcode != XDG_POPUP_CONFIGURE || payload.len() != 16 { + return Err(format!( + "unexpected xdg_popup event: object={} opcode={} payload_len={}", + object_id, + opcode, + payload.len() + )); + } + + let (object_id, opcode, payload) = probe.read_message()?; + if object_id != xdg_surface_id || opcode != XDG_SURFACE_CONFIGURE || payload.len() != 4 { + return Err(format!( + "unexpected popup xdg_surface event: object={} opcode={} payload_len={}", + object_id, + opcode, + payload.len() + )); + } + + Ok(u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ])) +} + fn exercise_shm_pool(probe: &mut WaylandProbe, shm_id: u32, surface_id: u32) -> Result<(), String> { let temp_path = std::env::temp_dir().join(format!( "redbear-compositor-check-{}-{}.shm", @@ -408,21 +579,50 @@ fn check_wayland_socket() -> Result<(), String> { let globals = collect_globals(&mut probe)?; let registry_id = 2; - let compositor_name = *globals + let (compositor_name, _) = *globals .get("wl_compositor") .ok_or_else(|| String::from("wl_compositor global missing"))?; - let shm_name = *globals + let (shm_name, _) = *globals .get("wl_shm") .ok_or_else(|| String::from("wl_shm global missing"))?; - let xdg_name = *globals + let (xdg_name, _) = *globals .get("xdg_wm_base") .ok_or_else(|| String::from("xdg_wm_base global missing"))?; + let (fixes_name, _) = *globals + .get("wl_fixes") + .ok_or_else(|| String::from("wl_fixes global missing"))?; + let (output_name, output_version) = *globals + .get("wl_output") + .ok_or_else(|| String::from("wl_output global missing"))?; + let (seat_name, seat_version) = *globals + .get("wl_seat") + .ok_or_else(|| String::from("wl_seat global missing"))?; + + if output_version < 4 { + return Err(format!( + "wl_output advertised v{output_version}; v4 required for name/description metadata" + )); + } + if seat_version < 2 { + return Err(format!( + "wl_seat advertised v{seat_version}; v2 required for name metadata" + )); + } let compositor_id = probe.bind(registry_id, compositor_name, "wl_compositor", 4)?; - let shm_id = probe.bind(registry_id, shm_name, "wl_shm", 1)?; + let shm_id = probe.bind(registry_id, shm_name, "wl_shm", 2)?; let xdg_wm_base_id = probe.bind(registry_id, xdg_name, "xdg_wm_base", 1)?; + let fixes_id = probe.bind(registry_id, fixes_name, "wl_fixes", 2)?; + let output_id = probe.bind(registry_id, output_name, "wl_output", 4)?; + let seat_id = probe.bind(registry_id, seat_name, "wl_seat", 5)?; + + let mut payload = Vec::new(); + push_u32(&mut payload, output_name); + probe.send_message(fixes_id, WL_FIXES_ACK_GLOBAL_REMOVE, &payload)?; expect_shm_formats(&mut probe, shm_id)?; + expect_output_metadata(&mut probe, output_id)?; + expect_seat_metadata(&mut probe, seat_id)?; let surface_id = probe.alloc_id(); probe.send_message( @@ -431,6 +631,18 @@ fn check_wayland_socket() -> Result<(), String> { &surface_id.to_le_bytes(), )?; + let region_id = probe.alloc_id(); + probe.send_message( + compositor_id, + WL_COMPOSITOR_CREATE_REGION, + ®ion_id.to_le_bytes(), + )?; + let mut payload = Vec::new(); + push_u32(&mut payload, region_id); + probe.send_message(surface_id, WL_SURFACE_SET_OPAQUE_REGION, &payload)?; + probe.send_message(region_id, WL_REGION_DESTROY, &[])?; + let _ = probe.read_message()?; + let xdg_surface_id = probe.alloc_id(); let mut payload = Vec::new(); push_u32(&mut payload, xdg_surface_id); @@ -450,7 +662,53 @@ fn check_wayland_socket() -> Result<(), String> { &serial.to_le_bytes(), )?; - exercise_shm_pool(&mut probe, shm_id, surface_id) + let positioner_id = probe.alloc_id(); + probe.send_message( + xdg_wm_base_id, + XDG_WM_BASE_CREATE_POSITIONER, + &positioner_id.to_le_bytes(), + )?; + let mut payload = Vec::new(); + push_i32(&mut payload, 64); + push_i32(&mut payload, 32); + probe.send_message(positioner_id, XDG_POSITIONER_SET_SIZE, &payload)?; + + let popup_surface_id = probe.alloc_id(); + probe.send_message( + compositor_id, + WL_COMPOSITOR_CREATE_SURFACE, + &popup_surface_id.to_le_bytes(), + )?; + + let popup_xdg_surface_id = probe.alloc_id(); + let mut payload = Vec::new(); + push_u32(&mut payload, popup_xdg_surface_id); + push_u32(&mut payload, popup_surface_id); + probe.send_message(xdg_wm_base_id, XDG_WM_BASE_GET_XDG_SURFACE, &payload)?; + + let popup_id = probe.alloc_id(); + let mut payload = Vec::new(); + push_u32(&mut payload, popup_id); + push_u32(&mut payload, xdg_surface_id); + push_u32(&mut payload, positioner_id); + probe.send_message(popup_xdg_surface_id, XDG_SURFACE_GET_POPUP, &payload)?; + let popup_serial = expect_xdg_popup_configure(&mut probe, popup_id, popup_xdg_surface_id)?; + probe.send_message( + popup_xdg_surface_id, + XDG_SURFACE_ACK_CONFIGURE, + &popup_serial.to_le_bytes(), + )?; + + probe.send_message(popup_id, XDG_POPUP_DESTROY, &[])?; + let _ = probe.read_message()?; + probe.send_message(positioner_id, XDG_POSITIONER_DESTROY, &[])?; + let _ = probe.read_message()?; + + exercise_shm_pool(&mut probe, shm_id, surface_id)?; + + let mut payload = Vec::new(); + push_u32(&mut payload, registry_id); + probe.send_message(fixes_id, WL_FIXES_DESTROY_REGISTRY, &payload) } fn check_binaries() -> Result<(), Vec> { diff --git a/local/recipes/wayland/redbear-compositor/source/src/main.rs b/local/recipes/wayland/redbear-compositor/source/src/main.rs index 8ad06db2e..156908c14 100644 --- a/local/recipes/wayland/redbear-compositor/source/src/main.rs +++ b/local/recipes/wayland/redbear-compositor/source/src/main.rs @@ -11,11 +11,12 @@ // scanout. // // Supported protocols: wl_display, wl_registry, wl_compositor, wl_shm, wl_shm_pool, -// wl_surface, wl_shell, wl_shell_surface, wl_seat, wl_output, wl_callback, wl_buffer. +// wl_surface, wl_shell, wl_shell_surface, wl_seat, wl_output, wl_callback, wl_buffer, +// wl_fixes. // // Wire format: [sender:u32] [msg_size:u16|opcode:u16] [args...] -use std::collections::{HashMap, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; use std::io::{Read, Seek, SeekFrom, Write}; use std::mem; use std::os::fd::{AsRawFd, FromRawFd, RawFd}; @@ -60,18 +61,67 @@ mod drm_backend { } #[repr(C)] - struct DrmConnector { connector_id: u32, connection: u32, connector_type: u32, mm_width: u32, mm_height: u32, encoder_id: u32, mode_count: u32, } + struct DrmConnector { + connector_id: u32, + connection: u32, + connector_type: u32, + mm_width: u32, + mm_height: u32, + encoder_id: u32, + mode_count: u32, + } #[repr(C)] #[derive(Clone, Copy)] - struct DrmModeInfo { clock: u32, hdisplay: u16, hsync_start: u16, hsync_end: u16, htotal: u16, hskew: u16, vdisplay: u16, vsync_start: u16, vsync_end: u16, vtotal: u16, vscan: u16, vrefresh: u32, flags: u32, type_: u32, } + struct DrmModeInfo { + clock: u32, + hdisplay: u16, + hsync_start: u16, + hsync_end: u16, + htotal: u16, + hskew: u16, + vdisplay: u16, + vsync_start: u16, + vsync_end: u16, + vtotal: u16, + vscan: u16, + vrefresh: u32, + flags: u32, + type_: u32, + } #[repr(C)] - struct DrmCreateDumb { height: u32, width: u32, bpp: u32, flags: u32, handle: u32, pitch: u32, size: u64, } + struct DrmCreateDumb { + height: u32, + width: u32, + bpp: u32, + flags: u32, + handle: u32, + pitch: u32, + size: u64, + } #[repr(C)] - struct DrmMapDumb { handle: u32, pad: u32, offset: u64, } + struct DrmMapDumb { + handle: u32, + pad: u32, + offset: u64, + } #[repr(C)] - struct DrmAddFb { width: u32, height: u32, pitch: u32, bpp: u32, depth: u32, handle: u32, fb_id: u32, } + struct DrmAddFb { + width: u32, + height: u32, + pitch: u32, + bpp: u32, + depth: u32, + handle: u32, + fb_id: u32, + } #[repr(C)] - struct DrmSetCrtc { crtc_id: u32, fb_handle: u32, connector_count: u32, connectors: [u32; 8], mode: DrmModeInfo, } + struct DrmSetCrtc { + crtc_id: u32, + fb_handle: u32, + connector_count: u32, + connectors: [u32; 8], + mode: DrmModeInfo, + } pub struct DrmOutput { pub width: u32, @@ -80,7 +130,7 @@ mod drm_backend { pub buffers: Vec>, fb_ids: Vec, pub current: AtomicUsize, - file: File, + _file: File, } impl DrmOutput { @@ -89,19 +139,35 @@ mod drm_backend { eprintln!("redbear-compositor: opened /scheme/drm/card0"); // Get connector info - let conn_bytes = vec![0u8; std::mem::size_of::()]; let mut conn: DrmConnector = unsafe { std::mem::zeroed() }; conn.connector_id = 0; conn.mode_count = 1; let mut req = vec![0u8; std::mem::size_of::()]; - unsafe { std::ptr::copy_nonoverlapping(&conn as *const DrmConnector as *const u8, req.as_mut_ptr(), req.len()); } - let mut resp = vec![0u8; std::mem::size_of::() + std::mem::size_of::()]; + unsafe { + std::ptr::copy_nonoverlapping( + &conn as *const DrmConnector as *const u8, + req.as_mut_ptr(), + req.len(), + ); + } + let mut resp = + vec![0u8; std::mem::size_of::() + std::mem::size_of::()]; if drm_ioctl(&mut file, DRM_IOCTL_MODE_GETCONNECTOR, &req, &mut resp).is_err() { return None; } - unsafe { std::ptr::copy_nonoverlapping(resp.as_ptr(), &mut conn as *mut DrmConnector as *mut u8, std::mem::size_of::()); } - if conn.mode_count == 0 { return None; } - let mode = unsafe { &*(resp.as_ptr().add(std::mem::size_of::()) as *const DrmModeInfo) }; + unsafe { + std::ptr::copy_nonoverlapping( + resp.as_ptr(), + &mut conn as *mut DrmConnector as *mut u8, + std::mem::size_of::(), + ); + } + if conn.mode_count == 0 { + return None; + } + let mode = unsafe { + &*(resp.as_ptr().add(std::mem::size_of::()) as *const DrmModeInfo) + }; let width = mode.hdisplay as u32; let height = mode.vdisplay as u32; eprintln!("redbear-compositor: DRM mode {}x{}", width, height); @@ -116,30 +182,90 @@ mod drm_backend { dumb.width = width; dumb.bpp = 32; let mut dumb_req = vec![0u8; std::mem::size_of::()]; - unsafe { std::ptr::copy_nonoverlapping(&dumb as *const DrmCreateDumb as *const u8, dumb_req.as_mut_ptr(), dumb_req.len()); } + unsafe { + std::ptr::copy_nonoverlapping( + &dumb as *const DrmCreateDumb as *const u8, + dumb_req.as_mut_ptr(), + dumb_req.len(), + ); + } let mut dumb_resp = vec![0u8; std::mem::size_of::()]; - if drm_ioctl(&mut file, DRM_IOCTL_MODE_CREATE_DUMB, &dumb_req, &mut dumb_resp).is_err() { return None; } - unsafe { std::ptr::copy_nonoverlapping(dumb_resp.as_ptr(), &mut dumb as *mut DrmCreateDumb as *mut u8, std::mem::size_of::()); } - if dumb.handle == 0 { return None; } + if drm_ioctl( + &mut file, + DRM_IOCTL_MODE_CREATE_DUMB, + &dumb_req, + &mut dumb_resp, + ) + .is_err() + { + return None; + } + unsafe { + std::ptr::copy_nonoverlapping( + dumb_resp.as_ptr(), + &mut dumb as *mut DrmCreateDumb as *mut u8, + std::mem::size_of::(), + ); + } + if dumb.handle == 0 { + return None; + } stride = dumb.pitch; // Map dumb buffer - let mut map = DrmMapDumb { handle: dumb.handle, pad: 0, offset: 0 }; + let map = DrmMapDumb { + handle: dumb.handle, + pad: 0, + offset: 0, + }; let mut map_req = vec![0u8; std::mem::size_of::()]; - unsafe { std::ptr::copy_nonoverlapping(&map as *const DrmMapDumb as *const u8, map_req.as_mut_ptr(), map_req.len()); } + unsafe { + std::ptr::copy_nonoverlapping( + &map as *const DrmMapDumb as *const u8, + map_req.as_mut_ptr(), + map_req.len(), + ); + } let mut map_resp = vec![0u8; std::mem::size_of::()]; - if drm_ioctl(&mut file, DRM_IOCTL_MODE_MAP_DUMB, &map_req, &mut map_resp).is_err() { return None; } + if drm_ioctl(&mut file, DRM_IOCTL_MODE_MAP_DUMB, &map_req, &mut map_resp).is_err() { + return None; + } let buf_size = dumb.size as usize; buffers.push(vec![0u8; buf_size]); // Add framebuffer - let mut addfb = DrmAddFb { width, height, pitch: stride, bpp: 32, depth: 24, handle: dumb.handle, fb_id: 0 }; + let mut addfb = DrmAddFb { + width, + height, + pitch: stride, + bpp: 32, + depth: 24, + handle: dumb.handle, + fb_id: 0, + }; let mut addfb_req = vec![0u8; std::mem::size_of::()]; - unsafe { std::ptr::copy_nonoverlapping(&addfb as *const DrmAddFb as *const u8, addfb_req.as_mut_ptr(), addfb_req.len()); } + unsafe { + std::ptr::copy_nonoverlapping( + &addfb as *const DrmAddFb as *const u8, + addfb_req.as_mut_ptr(), + addfb_req.len(), + ); + } let mut addfb_resp = vec![0u8; std::mem::size_of::()]; - if drm_ioctl(&mut file, DRM_IOCTL_MODE_ADDFB, &addfb_req, &mut addfb_resp).is_err() { return None; } - unsafe { std::ptr::copy_nonoverlapping(addfb_resp.as_ptr(), &mut addfb as *mut DrmAddFb as *mut u8, std::mem::size_of::()); } - if addfb.fb_id == 0 { return None; } + if drm_ioctl(&mut file, DRM_IOCTL_MODE_ADDFB, &addfb_req, &mut addfb_resp).is_err() + { + return None; + } + unsafe { + std::ptr::copy_nonoverlapping( + addfb_resp.as_ptr(), + &mut addfb as *mut DrmAddFb as *mut u8, + std::mem::size_of::(), + ); + } + if addfb.fb_id == 0 { + return None; + } fb_ids.push(addfb.fb_id); } @@ -151,15 +277,36 @@ mod drm_backend { setcrtc.connectors[0] = 0; // connector 0 setcrtc.mode = *mode; let mut setcrtc_req = vec![0u8; std::mem::size_of::()]; - unsafe { std::ptr::copy_nonoverlapping(&setcrtc as *const DrmSetCrtc as *const u8, setcrtc_req.as_mut_ptr(), setcrtc_req.len()); } - if drm_ioctl(&mut file, DRM_IOCTL_MODE_SETCRTC, &setcrtc_req, &mut []).is_err() { return None; } + unsafe { + std::ptr::copy_nonoverlapping( + &setcrtc as *const DrmSetCrtc as *const u8, + setcrtc_req.as_mut_ptr(), + setcrtc_req.len(), + ); + } + if drm_ioctl(&mut file, DRM_IOCTL_MODE_SETCRTC, &setcrtc_req, &mut []).is_err() { + return None; + } - eprintln!("redbear-compositor: DRM output {}x{} stride={}", width, height, stride); - Some(DrmOutput { width, height, stride, buffers, fb_ids, current: AtomicUsize::new(0), file }) + eprintln!( + "redbear-compositor: DRM output {}x{} stride={}", + width, height, stride + ); + Some(DrmOutput { + width, + height, + stride, + buffers, + fb_ids, + current: AtomicUsize::new(0), + _file: file, + }) } pub fn flip(&self) { - if self.fb_ids.len() < 2 { return; } + if self.fb_ids.len() < 2 { + return; + } let cur = self.current.load(Ordering::Relaxed); let next = (cur + 1) % self.fb_ids.len(); let fb_id = self.fb_ids[next]; @@ -179,9 +326,22 @@ mod drm_backend { } #[cfg(not(target_os = "redox"))] mod drm_backend { - pub struct DrmOutput; + use std::sync::atomic::AtomicUsize; + + pub struct DrmOutput { + pub width: u32, + pub height: u32, + pub stride: u32, + pub buffers: Vec>, + pub current: AtomicUsize, + } + impl DrmOutput { - pub fn open() -> Option { None } + pub fn open() -> Option { + None + } + + pub fn flip(&self) {} } } @@ -229,7 +389,18 @@ fn read_u32(data: &[u8], cursor: &mut usize) -> Result { } fn read_wayland_string(data: &[u8], cursor: &mut usize) -> Result { - let length = read_u32(data, cursor)? as usize; + if *cursor + 4 > data.len() { + return Err(String::from( + "unexpected end of message while reading string length", + )); + } + let length = u32::from_le_bytes([ + data[*cursor], + data[*cursor + 1], + data[*cursor + 2], + data[*cursor + 3], + ]) as usize; + *cursor += 4; if length == 0 { return Ok(String::new()); } @@ -308,31 +479,36 @@ const WL_DISPLAY_DELETE_ID: u16 = 2; const WL_REGISTRY_BIND: u16 = 0; const WL_REGISTRY_GLOBAL: u16 = 0; +const WL_REGISTRY_GLOBAL_REMOVE: u16 = 1; + +const WL_FIXES_DESTROY: u16 = 0; +const WL_FIXES_DESTROY_REGISTRY: u16 = 1; +const WL_FIXES_ACK_GLOBAL_REMOVE: u16 = 2; const WL_COMPOSITOR_CREATE_SURFACE: u16 = 0; -// Protocol constant: reserved for future implementation. -#[allow(dead_code)] const WL_COMPOSITOR_CREATE_REGION: u16 = 1; const WL_SHM_CREATE_POOL: u16 = 0; +const WL_SHM_RELEASE: u16 = 1; const WL_SHM_FORMAT: u16 = 0; const WL_SHM_POOL_CREATE_BUFFER: u16 = 0; -// Protocol constant: reserved for future implementation. -#[allow(dead_code)] -const WL_SHM_POOL_RESIZE: u16 = 1; +const WL_SHM_POOL_DESTROY: u16 = 1; +const WL_SHM_POOL_RESIZE: u16 = 2; +const WL_BUFFER_DESTROY: u16 = 0; const WL_BUFFER_RELEASE: u16 = 0; -const WL_SURFACE_ATTACH: u16 = 0; -const WL_SURFACE_DAMAGE: u16 = 1; -const WL_SURFACE_COMMIT: u16 = 5; -// Protocol constant: reserved for future implementation. -#[allow(dead_code)] -const WL_SURFACE_ENTER: u16 = 0; -// Protocol constant: reserved for future implementation. -#[allow(dead_code)] -const WL_SURFACE_LEAVE: u16 = 1; +const WL_SURFACE_DESTROY: u16 = 0; +const WL_SURFACE_ATTACH: u16 = 1; +const WL_SURFACE_DAMAGE: u16 = 2; +const WL_SURFACE_FRAME: u16 = 3; +const WL_SURFACE_SET_OPAQUE_REGION: u16 = 4; +const WL_SURFACE_SET_INPUT_REGION: u16 = 5; +const WL_SURFACE_COMMIT: u16 = 6; +const WL_REGION_DESTROY: u16 = 0; +const WL_REGION_ADD: u16 = 1; +const WL_REGION_SUBTRACT: u16 = 2; const WL_SHELL_GET_SHELL_SURFACE: u16 = 0; @@ -346,19 +522,52 @@ const WL_SHELL_SURFACE_PING: u16 = 0; const WL_SHELL_SURFACE_CONFIGURE: u16 = 1; const XDG_WM_BASE_DESTROY: u16 = 0; +const XDG_WM_BASE_CREATE_POSITIONER: u16 = 1; const XDG_WM_BASE_GET_XDG_SURFACE: u16 = 2; const XDG_WM_BASE_PONG: u16 = 3; const XDG_SURFACE_DESTROY: u16 = 0; const XDG_SURFACE_GET_TOPLEVEL: u16 = 1; +const XDG_SURFACE_GET_POPUP: u16 = 2; +const XDG_SURFACE_SET_WINDOW_GEOMETRY: u16 = 3; const XDG_SURFACE_ACK_CONFIGURE: u16 = 4; const XDG_SURFACE_CONFIGURE: u16 = 0; const XDG_TOPLEVEL_CONFIGURE: u16 = 0; +const XDG_TOPLEVEL_DESTROY: u16 = 0; + +const XDG_POSITIONER_DESTROY: u16 = 0; +const XDG_POSITIONER_SET_SIZE: u16 = 1; +const XDG_POSITIONER_SET_ANCHOR_RECT: u16 = 2; +const XDG_POSITIONER_SET_ANCHOR: u16 = 3; +const XDG_POSITIONER_SET_GRAVITY: u16 = 4; +const XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT: u16 = 5; +const XDG_POSITIONER_SET_OFFSET: u16 = 6; + +const XDG_POPUP_DESTROY: u16 = 0; +const XDG_POPUP_GRAB: u16 = 1; +const XDG_POPUP_REPOSITION: u16 = 2; +const XDG_POPUP_CONFIGURE: u16 = 0; const WL_SEAT_GET_POINTER: u16 = 0; const WL_SEAT_GET_KEYBOARD: u16 = 1; +const WL_SEAT_GET_TOUCH: u16 = 2; +const WL_SEAT_RELEASE: u16 = 3; const WL_SEAT_CAPABILITIES: u16 = 0; +const WL_SEAT_NAME: u16 = 1; + +const WL_POINTER_RELEASE: u16 = 0; +const WL_KEYBOARD_RELEASE: u16 = 0; +const WL_TOUCH_RELEASE: u16 = 0; + +const WL_DATA_DEVICE_MANAGER_CREATE_DATA_SOURCE: u16 = 0; +const WL_DATA_DEVICE_MANAGER_GET_DATA_DEVICE: u16 = 1; +const WL_DATA_SOURCE_OFFER: u16 = 0; +const WL_DATA_SOURCE_DESTROY: u16 = 1; +const WL_DATA_SOURCE_SET_ACTIONS: u16 = 2; +const WL_DATA_DEVICE_START_DRAG: u16 = 0; +const WL_DATA_DEVICE_SET_SELECTION: u16 = 1; +const WL_DATA_DEVICE_RELEASE: u16 = 2; // Protocol constant: reserved for future implementation. #[allow(dead_code)] @@ -377,6 +586,9 @@ const WL_OUTPUT_GEOMETRY: u16 = 0; const WL_OUTPUT_MODE: u16 = 1; const WL_OUTPUT_DONE: u16 = 2; const WL_OUTPUT_SCALE: u16 = 3; +const WL_OUTPUT_RELEASE: u16 = 0; +const WL_OUTPUT_NAME: u16 = 4; +const WL_OUTPUT_DESCRIPTION: u16 = 5; const WL_CALLBACK_DONE: u16 = 0; @@ -403,9 +615,13 @@ const OBJECT_TYPE_WL_DATA_DEVICE_MANAGER: u32 = 17; const OBJECT_TYPE_WL_SUBCOMPOSITOR: u32 = 18; const OBJECT_TYPE_WL_DATA_DEVICE: u32 = 19; const OBJECT_TYPE_WL_SUBSURFACE: u32 = 20; +const OBJECT_TYPE_WL_FIXES: u32 = 21; +const OBJECT_TYPE_WL_REGION: u32 = 22; +const OBJECT_TYPE_WL_TOUCH: u32 = 23; +const OBJECT_TYPE_WL_DATA_SOURCE: u32 = 24; +const OBJECT_TYPE_XDG_POSITIONER: u32 = 25; +const OBJECT_TYPE_XDG_POPUP: u32 = 26; -// wl_data_device_manager opcodes -const WL_DATA_DEVICE_MANAGER_GET_DATA_DEVICE: u16 = 1; // wl_subcompositor opcodes const WL_SUBCOMPOSITOR_GET_SUBSURFACE: u16 = 1; @@ -433,6 +649,7 @@ struct Buffer { #[derive(Clone)] struct Surface { buffer: Option, + pending_buffer_id: Option, committed_buffer_id: Option, x: u32, y: u32, @@ -442,9 +659,11 @@ struct Surface { struct ClientState { objects: HashMap, + object_versions: HashMap, surfaces: HashMap, buffers: HashMap, shm_pools: HashMap, + acked_global_removals: HashSet, _next_id: u32, } @@ -468,7 +687,7 @@ impl Compositor { fb_width: u32, fb_height: u32, fb_stride: u32, - drm: Mutex>, + drm: Mutex>, ) -> std::io::Result { let _ = std::fs::remove_file(socket_path); let listener = UnixListener::bind(socket_path)?; @@ -494,7 +713,7 @@ impl Compositor { Global { name: 2, interface: "wl_shm".into(), - version: 1, + version: 2, }, Global { name: 3, @@ -509,7 +728,7 @@ impl Compositor { Global { name: 5, interface: "wl_output".into(), - version: 3, + version: 4, }, Global { name: 6, @@ -526,6 +745,11 @@ impl Compositor { interface: "wl_subcompositor".into(), version: 1, }, + Global { + name: 9, + interface: "wl_fixes".into(), + version: 2, + }, ]; Ok(Self { @@ -566,9 +790,11 @@ impl Compositor { client_id, ClientState { objects: HashMap::new(), + object_versions: HashMap::new(), surfaces: HashMap::new(), buffers: HashMap::new(), shm_pools: HashMap::new(), + acked_global_removals: HashSet::new(), _next_id: 1, }, ); @@ -602,6 +828,21 @@ impl Compositor { let _ = stream.write_all(&msg); } + fn send_delete_id(&self, stream: &mut UnixStream, deleted_id: u32) { + let mut msg = Vec::with_capacity(12); + push_header(&mut msg, 1, WL_DISPLAY_DELETE_ID, 4); + push_u32(&mut msg, deleted_id); + let _ = stream.write_all(&msg); + } + + #[allow(dead_code)] + fn send_global_remove(&self, stream: &mut UnixStream, registry_id: u32, name: u32) { + let mut msg = Vec::with_capacity(12); + push_header(&mut msg, registry_id, WL_REGISTRY_GLOBAL_REMOVE, 4); + push_u32(&mut msg, name); + let _ = stream.write_all(&msg); + } + fn handle_client(&self, client_id: u32, mut stream: UnixStream) { let mut buf = [0u8; 4096]; loop { @@ -690,6 +931,7 @@ impl Compositor { let mut clients = self.clients.lock().unwrap(); if let Some(client) = clients.get_mut(&client_id) { client.objects.remove(&obj_id); + client.object_versions.remove(&obj_id); client.surfaces.remove(&obj_id); client.buffers.remove(&obj_id); client.shm_pools.remove(&obj_id); @@ -723,9 +965,9 @@ impl Compositor { OBJECT_TYPE_WL_REGISTRY => match opcode { WL_REGISTRY_BIND => { let mut cursor = 0; - let _name = read_u32(payload, &mut cursor)?; + let name = read_u32(payload, &mut cursor)?; let iface = read_wayland_string(payload, &mut cursor)?; - let _version = read_u32(payload, &mut cursor)?; + let requested_version = read_u32(payload, &mut cursor)?; let new_id = read_u32(payload, &mut cursor)?; eprintln!( @@ -735,6 +977,13 @@ impl Compositor { let mut clients = self.clients.lock().unwrap(); if let Some(client) = clients.get_mut(&client_id) { + let global_version = self + .globals + .iter() + .find(|global| global.name == name && global.interface == iface) + .map(|global| global.version) + .unwrap_or(requested_version); + let object_version = requested_version.min(global_version); let type_id = match iface.as_str() { "wl_compositor" => OBJECT_TYPE_WL_COMPOSITOR, "wl_shm" => OBJECT_TYPE_WL_SHM, @@ -744,18 +993,20 @@ impl Compositor { "xdg_wm_base" => OBJECT_TYPE_XDG_WM_BASE, "wl_data_device_manager" => OBJECT_TYPE_WL_DATA_DEVICE_MANAGER, "wl_subcompositor" => OBJECT_TYPE_WL_SUBCOMPOSITOR, + "wl_fixes" => OBJECT_TYPE_WL_FIXES, _ => 0, }; client.objects.insert(new_id, type_id); + client.object_versions.insert(new_id, object_version); if iface == "wl_shm" { self.send_shm_format(stream, new_id, WL_SHM_FORMAT_ARGB8888); self.send_shm_format(stream, new_id, WL_SHM_FORMAT_XRGB8888); } if iface == "wl_output" { - self.send_output_info(stream, new_id); + self.send_output_info(stream, new_id, object_version); } if iface == "wl_seat" { - self.send_seat_capabilities(stream, new_id); + self.send_seat_capabilities(stream, new_id, object_version); } } } @@ -779,6 +1030,7 @@ impl Compositor { surface_id, Surface { buffer: None, + pending_buffer_id: None, committed_buffer_id: None, x: 0, y: 0, @@ -789,6 +1041,17 @@ impl Compositor { } } } + WL_COMPOSITOR_CREATE_REGION => { + if payload.len() >= 4 { + let region_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(region_id, OBJECT_TYPE_WL_REGION); + } + } + } _ => { eprintln!( "redbear-compositor: unhandled opcode {} on object {}", @@ -826,6 +1089,14 @@ impl Compositor { } } } + WL_SHM_RELEASE => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } _ => { eprintln!( "redbear-compositor: unhandled opcode {} on object {}", @@ -891,6 +1162,30 @@ impl Compositor { } } } + WL_SHM_POOL_DESTROY => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.shm_pools.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + WL_SHM_POOL_RESIZE => { + if payload.len() >= 4 { + let size = i32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + if size > 0 { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + if let Some(pool) = client.shm_pools.get_mut(&object_id) { + pool.size = pool.size.max(size as usize); + } + } + } + } + } _ => { eprintln!( "redbear-compositor: unhandled opcode {} on object {}", @@ -899,6 +1194,15 @@ impl Compositor { } }, OBJECT_TYPE_WL_SURFACE => match opcode { + WL_SURFACE_DESTROY => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.surfaces.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } WL_SURFACE_ATTACH => { if payload.len() >= 12 { let buffer_id = u32::from_le_bytes([ @@ -916,11 +1220,18 @@ impl Compositor { let mut clients = self.clients.lock().unwrap(); if let Some(client) = clients.get_mut(&client_id) { - if let Some((pool_id, buffer)) = + let attached_buffer = if buffer_id == 0 { + None + } else { client.buffers.get(&buffer_id).cloned() - { - if let Some(surface) = client.surfaces.get_mut(&object_id) { + }; + if let Some(surface) = client.surfaces.get_mut(&object_id) { + if buffer_id == 0 { + surface.buffer = None; + surface.pending_buffer_id = None; + } else if let Some((pool_id, buffer)) = attached_buffer { surface.buffer = Some(Buffer { pool_id, ..buffer }); + surface.pending_buffer_id = Some(buffer_id); } } } @@ -931,18 +1242,8 @@ impl Compositor { let mut clients = self.clients.lock().unwrap(); if let Some(client) = clients.get_mut(&client_id) { if let Some(surface) = client.surfaces.get_mut(&object_id) { - let old_buffer = surface.committed_buffer_id.take(); - surface.committed_buffer_id = - surface.buffer.as_ref().map(|b| { - client - .buffers - .iter() - .find(|(_, (_, buf))| { - buf.offset == b.offset && buf.width == b.width - }) - .map(|(id, _)| *id) - .unwrap_or(0) - }); + let release_buffer = surface.pending_buffer_id; + surface.committed_buffer_id = release_buffer; let surface_snapshot = surface.clone(); if let Some(ref buffer) = surface_snapshot.buffer { @@ -952,7 +1253,7 @@ impl Compositor { self.composite_buffer(pool, buffer, &surface_snapshot); } } - old_buffer + release_buffer } else { None } @@ -970,6 +1271,18 @@ impl Compositor { WL_SURFACE_DAMAGE => { // No-op — we don't need damage tracking for a single-client greeter. } + WL_SURFACE_FRAME => { + if payload.len() >= 4 { + let callback_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + self.send_callback_done(stream, callback_id, self.next_serial()); + } + } + WL_SURFACE_SET_OPAQUE_REGION | WL_SURFACE_SET_INPUT_REGION => { + // Region state is tracked as accepted but inert for the greeter's + // single full-screen composition surface. + } _ => { eprintln!( "redbear-compositor: unhandled opcode {} on object {}", @@ -1008,7 +1321,7 @@ impl Compositor { } }, OBJECT_TYPE_WL_SEAT => match opcode { - WL_SEAT_GET_POINTER | WL_SEAT_GET_KEYBOARD => { + WL_SEAT_GET_POINTER | WL_SEAT_GET_KEYBOARD | WL_SEAT_GET_TOUCH => { if payload.len() >= 4 { let new_id = u32::from_le_bytes([ payload[0], payload[1], payload[2], payload[3], @@ -1016,6 +1329,7 @@ impl Compositor { let object_type = match opcode { WL_SEAT_GET_POINTER => OBJECT_TYPE_WL_POINTER, WL_SEAT_GET_KEYBOARD => OBJECT_TYPE_WL_KEYBOARD, + WL_SEAT_GET_TOUCH => OBJECT_TYPE_WL_TOUCH, _ => unreachable!(), }; let mut clients = self.clients.lock().unwrap(); @@ -1024,6 +1338,15 @@ impl Compositor { } } } + WL_SEAT_RELEASE => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } _ => { eprintln!( "redbear-compositor: unhandled opcode {} on object {}", @@ -1032,6 +1355,17 @@ impl Compositor { } }, OBJECT_TYPE_XDG_WM_BASE => match opcode { + XDG_WM_BASE_CREATE_POSITIONER => { + if payload.len() >= 4 { + let new_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_XDG_POSITIONER); + } + } + } XDG_WM_BASE_GET_XDG_SURFACE => { if payload.len() >= 4 { let new_id = u32::from_le_bytes([ @@ -1043,8 +1377,17 @@ impl Compositor { } } } - XDG_WM_BASE_DESTROY | XDG_WM_BASE_PONG => { - // No-op — the greeter keeps the shell global alive for the client lifetime. + XDG_WM_BASE_DESTROY => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + XDG_WM_BASE_PONG => { + // The compositor does not currently send xdg_wm_base.ping, but accepting + // pong keeps clients tolerant if a future watchdog starts doing so. } _ => { eprintln!( @@ -1059,6 +1402,8 @@ impl Compositor { if let Some(client) = clients.get_mut(&client_id) { client.objects.remove(&object_id); } + drop(clients); + self.send_delete_id(stream, object_id); } XDG_SURFACE_GET_TOPLEVEL => { if payload.len() >= 4 { @@ -1075,6 +1420,25 @@ impl Compositor { self.send_xdg_surface_configure(stream, object_id, serial); } } + XDG_SURFACE_GET_POPUP => { + if payload.len() >= 12 { + let popup_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(popup_id, OBJECT_TYPE_XDG_POPUP); + } + drop(clients); + let serial = self.next_serial(); + self.send_xdg_popup_configure(stream, popup_id); + self.send_xdg_surface_configure(stream, object_id, serial); + } + } + XDG_SURFACE_SET_WINDOW_GEOMETRY => { + // Geometry is accepted for Qt/KDE bookkeeping. The bounded greeter + // compositor still maps every surface into its own fullscreen plane. + } XDG_SURFACE_ACK_CONFIGURE => { // Client acknowledged — ready for first commit. } @@ -1085,12 +1449,153 @@ impl Compositor { ); } }, - OBJECT_TYPE_WL_OUTPUT - | OBJECT_TYPE_WL_BUFFER - | OBJECT_TYPE_XDG_TOPLEVEL - | OBJECT_TYPE_WL_POINTER - | OBJECT_TYPE_WL_KEYBOARD - | OBJECT_TYPE_WL_DATA_DEVICE_MANAGER => match opcode { + OBJECT_TYPE_WL_OUTPUT => match opcode { + WL_OUTPUT_RELEASE => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + _ => { + eprintln!( + "redbear-compositor: unhandled wl_output opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_BUFFER => match opcode { + WL_BUFFER_DESTROY => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + client.buffers.remove(&object_id); + for surface in client.surfaces.values_mut() { + if surface.committed_buffer_id == Some(object_id) { + surface.committed_buffer_id = None; + } + if surface.pending_buffer_id == Some(object_id) { + surface.pending_buffer_id = None; + surface.buffer = None; + } + } + } + drop(clients); + self.send_delete_id(stream, object_id); + } + _ => { + eprintln!( + "redbear-compositor: unhandled wl_buffer opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_XDG_TOPLEVEL => match opcode { + XDG_TOPLEVEL_DESTROY => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + _ => { + // Accept title, app-id, size, maximize/fullscreen, and related state + // requests. The greeter compositor maps every toplevel to one bounded + // full-screen surface, so no extra state is needed yet. + } + }, + OBJECT_TYPE_XDG_POSITIONER => match opcode { + XDG_POSITIONER_DESTROY => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + XDG_POSITIONER_SET_SIZE + | XDG_POSITIONER_SET_ANCHOR_RECT + | XDG_POSITIONER_SET_ANCHOR + | XDG_POSITIONER_SET_GRAVITY + | XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT + | XDG_POSITIONER_SET_OFFSET => {} + _ => { + eprintln!( + "redbear-compositor: unhandled xdg_positioner opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_XDG_POPUP => match opcode { + XDG_POPUP_DESTROY => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + XDG_POPUP_GRAB | XDG_POPUP_REPOSITION => {} + _ => { + eprintln!( + "redbear-compositor: unhandled xdg_popup opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_POINTER => match opcode { + WL_POINTER_RELEASE => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + _ => {} + }, + OBJECT_TYPE_WL_KEYBOARD => match opcode { + WL_KEYBOARD_RELEASE => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + _ => {} + }, + OBJECT_TYPE_WL_TOUCH => match opcode { + WL_TOUCH_RELEASE => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + _ => {} + }, + OBJECT_TYPE_WL_DATA_DEVICE_MANAGER => match opcode { + WL_DATA_DEVICE_MANAGER_CREATE_DATA_SOURCE => { + if payload.len() >= 4 { + let new_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_WL_DATA_SOURCE); + } + } + } WL_DATA_DEVICE_MANAGER_GET_DATA_DEVICE => { if payload.len() >= 4 { let new_id = u32::from_le_bytes([ @@ -1109,6 +1614,40 @@ impl Compositor { ); } }, + OBJECT_TYPE_WL_DATA_SOURCE => match opcode { + WL_DATA_SOURCE_DESTROY => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + WL_DATA_SOURCE_OFFER | WL_DATA_SOURCE_SET_ACTIONS => {} + _ => { + eprintln!( + "redbear-compositor: unhandled data_source opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_DATA_DEVICE => match opcode { + WL_DATA_DEVICE_RELEASE => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + WL_DATA_DEVICE_START_DRAG | WL_DATA_DEVICE_SET_SELECTION => {} + _ => { + eprintln!( + "redbear-compositor: unhandled data_device opcode {} on object {}", + opcode, object_id + ); + } + }, OBJECT_TYPE_WL_SUBCOMPOSITOR => match opcode { WL_SUBCOMPOSITOR_GET_SUBSURFACE => { if payload.len() >= 4 { @@ -1128,10 +1667,78 @@ impl Compositor { ); } }, - OBJECT_TYPE_WL_DATA_DEVICE | OBJECT_TYPE_WL_SUBSURFACE => { - // Accept all requests silently — Qt6 needs these proxies - // to exist for initialization but we don't implement - // clipboard or subsurface compositing yet. + OBJECT_TYPE_WL_SUBSURFACE => { + // Accept set_position/place_above/place_below/set_sync/set_desync/destroy. + // The greeter path has one fullscreen surface, so subsurface state is inert. + if opcode == 0 { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + } + OBJECT_TYPE_WL_REGION => match opcode { + WL_REGION_DESTROY => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + WL_REGION_ADD | WL_REGION_SUBTRACT => {} + _ => { + eprintln!( + "redbear-compositor: unhandled wl_region opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_FIXES => match opcode { + WL_FIXES_DESTROY => { + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(stream, object_id); + } + WL_FIXES_DESTROY_REGISTRY => { + if payload.len() >= 4 { + let registry_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + if client.objects.get(®istry_id).copied() + == Some(OBJECT_TYPE_WL_REGISTRY) + { + client.objects.remove(®istry_id); + } + } + drop(clients); + self.send_delete_id(stream, registry_id); + } + } + WL_FIXES_ACK_GLOBAL_REMOVE => { + if payload.len() >= 4 { + let name = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + client.acked_global_removals.insert(name); + } + } + } + _ => { + eprintln!( + "redbear-compositor: unhandled wl_fixes opcode {} on object {}", + opcode, object_id + ); + } }, _ => { eprintln!( @@ -1152,11 +1759,19 @@ impl Compositor { // obtained from Vec::as_mut_ptr() remain valid after the guard is // dropped because no other thread can mutate the buffers. let byte_count = buffer.height as usize * buffer.stride as usize; - if buffer.offset as usize + byte_count > pool.size { return; } + if buffer.offset as usize + byte_count > pool.size { + return; + } let mut src = vec![0u8; byte_count]; - if pool.file.seek(SeekFrom::Start(buffer.offset as u64)).is_err() - || pool.file.read_exact(&mut src).is_err() { return; } + if pool + .file + .seek(SeekFrom::Start(buffer.offset as u64)) + .is_err() + || pool.file.read_exact(&mut src).is_err() + { + return; + } let fb_stride; let fb_ptr: *mut u8; @@ -1164,7 +1779,8 @@ impl Compositor { if let Ok(mut drm_guard) = self.drm.lock() { if let Some(ref mut drm) = *drm_guard { fb_stride = drm.stride as usize; - let idx = (drm.current.load(std::sync::atomic::Ordering::Relaxed) + 1) % drm.buffers.len().max(1); + let idx = (drm.current.load(std::sync::atomic::Ordering::Relaxed) + 1) + % drm.buffers.len().max(1); fb_ptr = drm.buffers[idx].as_mut_ptr(); } else { let mut fb = self.fb_data.lock().unwrap(); @@ -1187,7 +1803,8 @@ impl Compositor { let src_row = row * buffer.stride as usize; let dst_row = (dst_y + row) * fb_stride + dst_x * 4; if dst_row + buffer.width as usize * 4 <= fb_len - && src_row + buffer.width as usize * 4 <= src.len() { + && src_row + buffer.width as usize * 4 <= src.len() + { for col in 0..buffer.width as usize { let s = src_row + col * 4; let d = dst_row + col * 4; @@ -1203,7 +1820,7 @@ impl Compositor { } // Page flip after compositing to DRM - if let Ok(mut drm_guard) = self.drm.lock() { + if let Ok(drm_guard) = self.drm.lock() { if let Some(ref drm) = *drm_guard { drm.flip(); } @@ -1234,6 +1851,16 @@ impl Compositor { let _ = stream.write_all(&msg); } + fn send_xdg_popup_configure(&self, stream: &mut UnixStream, popup_id: u32) { + let mut msg = Vec::with_capacity(24); + push_header(&mut msg, popup_id, XDG_POPUP_CONFIGURE, 16); + push_i32(&mut msg, 0); + push_i32(&mut msg, 0); + push_i32(&mut msg, self.fb_width as i32); + push_i32(&mut msg, self.fb_height as i32); + let _ = stream.write_all(&msg); + } + fn send_shm_format(&self, stream: &mut UnixStream, shm_id: u32, format: u32) { let mut msg = Vec::with_capacity(12); push_header(&mut msg, shm_id, WL_SHM_FORMAT, 4); @@ -1241,7 +1868,7 @@ impl Compositor { let _ = stream.write_all(&msg); } - fn send_output_info(&self, stream: &mut UnixStream, output_id: u32) { + fn send_output_info(&self, stream: &mut UnixStream, output_id: u32, version: u32) { // wl_output.geometry { let mut payload = Vec::new(); @@ -1263,37 +1890,44 @@ impl Compositor { { let mut msg = Vec::with_capacity(24); push_header(&mut msg, output_id, WL_OUTPUT_MODE, 16); - push_u32(&mut msg, 0x2); + push_u32(&mut msg, 0x3); push_i32(&mut msg, self.fb_width as i32); push_i32(&mut msg, self.fb_height as i32); - push_i32(&mut msg, 60); + push_i32(&mut msg, 60_000); let _ = stream.write_all(&msg); } - // wl_output.scale and wl_output.done are required for wl_output v2+ clients to - // treat the output as fully initialized. - { + // wl_output.scale is v2+; wl_output.name/description are v4+. Only send events the + // client version made legal, because Qt binds older versions in some fallback paths. + if version >= 2 { let mut msg = Vec::with_capacity(12); push_header(&mut msg, output_id, WL_OUTPUT_SCALE, 4); push_i32(&mut msg, 1); let _ = stream.write_all(&msg); } - { + if version >= 4 { + let mut payload = Vec::new(); + push_wayland_string(&mut payload, "RedBear-0"); + let mut msg = Vec::with_capacity(8 + payload.len()); + push_header(&mut msg, output_id, WL_OUTPUT_NAME, payload.len()); + msg.extend_from_slice(&payload); + let _ = stream.write_all(&msg); + } + if version >= 4 { + let mut payload = Vec::new(); + push_wayland_string(&mut payload, "Red Bear OS framebuffer output"); + let mut msg = Vec::with_capacity(8 + payload.len()); + push_header(&mut msg, output_id, WL_OUTPUT_DESCRIPTION, payload.len()); + msg.extend_from_slice(&payload); + let _ = stream.write_all(&msg); + } + if version >= 2 { let mut msg = Vec::with_capacity(8); push_header(&mut msg, output_id, WL_OUTPUT_DONE, 0); let _ = stream.write_all(&msg); } } - fn send_seat_capabilities(&self, stream: &mut UnixStream, seat_id: u32) { - // wl_seat.name (v2) — required by Qt6 to fully initialize the seat - { - let mut payload = Vec::new(); - push_wayland_string(&mut payload, "seat0"); - let mut msg = Vec::with_capacity(8 + payload.len()); - push_header(&mut msg, seat_id, 1, payload.len()); // opcode 1 = wl_seat.name - msg.extend_from_slice(&payload); - let _ = stream.write_all(&msg); - } + fn send_seat_capabilities(&self, stream: &mut UnixStream, seat_id: u32, version: u32) { // wl_seat.capabilities — advertise pointer so Qt creates a wl_pointer proxy { let mut msg = Vec::with_capacity(12); @@ -1301,6 +1935,15 @@ impl Compositor { push_u32(&mut msg, 0x1); // WL_SEAT_CAPABILITY_POINTER let _ = stream.write_all(&msg); } + // wl_seat.name is v2+ and is required by Qt6 to fully initialize the seat. + if version >= 2 { + let mut payload = Vec::new(); + push_wayland_string(&mut payload, "seat0"); + let mut msg = Vec::with_capacity(8 + payload.len()); + push_header(&mut msg, seat_id, WL_SEAT_NAME, payload.len()); + msg.extend_from_slice(&payload); + let _ = stream.write_all(&msg); + } } } @@ -1314,17 +1957,27 @@ fn main() { // Fall back to VESA framebuffer parameters from environment. let drm = drm_backend::DrmOutput::open(); let (fb_width, fb_height, fb_stride, fb_phys) = if let Some(ref d) = drm { - eprintln!("redbear-compositor: using DRM/KMS output {}x{}", d.width, d.height); + eprintln!( + "redbear-compositor: using DRM/KMS output {}x{}", + d.width, d.height + ); (d.width, d.height, d.stride, 0) } else { let fb_width: u32 = std::env::var("FRAMEBUFFER_WIDTH") - .unwrap_or_else(|_| "1280".into()).parse().unwrap_or(1280); + .unwrap_or_else(|_| "1280".into()) + .parse() + .unwrap_or(1280); let fb_height: u32 = std::env::var("FRAMEBUFFER_HEIGHT") - .unwrap_or_else(|_| "720".into()).parse().unwrap_or(720); + .unwrap_or_else(|_| "720".into()) + .parse() + .unwrap_or(720); let fb_stride: u32 = std::env::var("FRAMEBUFFER_STRIDE") - .unwrap_or_else(|_| (fb_width * 4).to_string()).parse().unwrap_or(fb_width * 4); + .unwrap_or_else(|_| (fb_width * 4).to_string()) + .parse() + .unwrap_or(fb_width * 4); let fb_phys_str = std::env::var("FRAMEBUFFER_ADDR").unwrap_or_else(|_| "0x80000000".into()); - let fb_phys = usize::from_str_radix(fb_phys_str.trim_start_matches("0x"), 16).unwrap_or(0x80000000); + let fb_phys = + usize::from_str_radix(fb_phys_str.trim_start_matches("0x"), 16).unwrap_or(0x80000000); eprintln!( "redbear-compositor: fb {}x{} stride {} phys 0x{:X}", fb_width, fb_height, fb_stride, fb_phys @@ -1333,7 +1986,14 @@ fn main() { }; let socket_path_clone = socket_path.clone(); - match Compositor::new(&socket_path, fb_phys, fb_width, fb_height, fb_stride, Mutex::new(drm)) { + match Compositor::new( + &socket_path, + fb_phys, + fb_width, + fb_height, + fb_stride, + Mutex::new(drm), + ) { Ok(mut compositor) => { if let Err(e) = compositor.run() { eprintln!("redbear-compositor: {}", e); diff --git a/local/recipes/wayland/redbear-compositor/source/tests/integration_test.rs b/local/recipes/wayland/redbear-compositor/source/tests/integration_test.rs index bce1863b4..ab974aa20 100644 --- a/local/recipes/wayland/redbear-compositor/source/tests/integration_test.rs +++ b/local/recipes/wayland/redbear-compositor/source/tests/integration_test.rs @@ -1,6 +1,7 @@ // Integration test: verifies the compositor's Wayland protocol implementation // by starting a real compositor instance and connecting as a client. +use std::collections::HashMap; use std::io::{Read, Write}; use std::os::unix::net::UnixStream; use std::process::{Child, Command}; @@ -39,6 +40,26 @@ fn read_wayland_string(payload: &[u8], cursor: &mut usize) -> String { .to_string() } +fn collect_globals(client: &mut WaylandClient, registry: u32) -> HashMap { + let mut globals = HashMap::new(); + for _ in 0..9 { + let (object_id, opcode, payload) = client.read_message().expect("read global failed"); + assert_eq!(object_id, registry); + assert_eq!(opcode, 0); // wl_registry.global + let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let mut cursor = 4; + let iface = read_wayland_string(&payload, &mut cursor); + let version = u32::from_le_bytes([ + payload[cursor], + payload[cursor + 1], + payload[cursor + 2], + payload[cursor + 3], + ]); + globals.insert(iface, (name, version)); + } + globals +} + struct WaylandClient { stream: UnixStream, next_id: u32, @@ -155,7 +176,7 @@ fn test_compositor_globals() { // Read global events let mut globals = Vec::new(); - for _ in 0..6 { + for _ in 0..9 { match client.read_message() { Ok((_obj_id, opcode, payload)) => { assert_eq!(opcode, 0); // wl_registry.global @@ -192,6 +213,10 @@ fn test_compositor_globals() { globals.iter().any(|(_, i)| i == "xdg_wm_base"), "xdg_wm_base missing" ); + assert!( + globals.iter().any(|(_, i)| i == "wl_fixes"), + "wl_fixes missing" + ); compositor.kill().ok(); let _ = std::fs::remove_file(socket); @@ -209,7 +234,7 @@ fn test_compositor_shm_formats() { // Read globals to find wl_shm name let mut shm_name = 0u32; - for _ in 0..6 { + for _ in 0..9 { let (_, _, payload) = client.read_message().expect("read failed"); let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); let mut cursor = 4; @@ -248,6 +273,327 @@ fn test_compositor_shm_formats() { let _ = std::fs::remove_file(socket); } +#[test] +fn test_compositor_wl_fixes_destroy_registry() { + let socket = "/tmp/test-redbear-compositor-wl-fixes.sock"; + let _ = std::fs::remove_file(socket); + + let mut compositor = start_compositor(socket); + let mut client = WaylandClient::connect(socket).expect("failed to connect"); + + let registry = client.get_registry().expect("get_registry failed"); + + let mut fixes_name = 0u32; + for _ in 0..9 { + let (_, opcode, payload) = client.read_message().expect("read failed"); + assert_eq!(opcode, 0); // wl_registry.global + let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let mut cursor = 4; + let iface = read_wayland_string(&payload, &mut cursor); + if iface == "wl_fixes" { + fixes_name = name; + } + } + + assert_ne!(fixes_name, 0, "wl_fixes global not found"); + + let fixes = client + .bind(registry, fixes_name, "wl_fixes", 2) + .expect("bind wl_fixes failed"); + + let removed_global_name = 5u32; // wl_output in the compositor's fixed global table. + client + .send_message(fixes, 2, &removed_global_name.to_le_bytes()) + .expect("wl_fixes.ack_global_remove failed"); + client + .send_message(fixes, 1, ®istry.to_le_bytes()) + .expect("wl_fixes.destroy_registry failed"); + + let (object_id, opcode, payload) = client.read_message().expect("read delete_id failed"); + assert_eq!(object_id, 1, "delete_id must be sent by wl_display"); + assert_eq!(opcode, 2, "expected wl_display.delete_id"); + assert_eq!(payload, registry.to_le_bytes()); + + compositor.kill().ok(); + let _ = std::fs::remove_file(socket); +} + +#[test] +fn test_compositor_output_and_seat_metadata() { + let socket = "/tmp/test-redbear-compositor-output-seat.sock"; + let _ = std::fs::remove_file(socket); + + let mut compositor = start_compositor(socket); + let mut client = WaylandClient::connect(socket).expect("failed to connect"); + + let registry = client.get_registry().expect("get_registry failed"); + let globals = collect_globals(&mut client, registry); + + let (output_name, output_version) = *globals.get("wl_output").expect("wl_output missing"); + assert!( + output_version >= 4, + "wl_output must advertise metadata-capable v4" + ); + let output_id = client + .bind(registry, output_name, "wl_output", 4) + .expect("bind wl_output failed"); + + let mut saw_geometry = false; + let mut saw_mode = false; + let mut saw_scale = false; + let mut saw_name = false; + let mut saw_description = false; + let mut saw_done = false; + for _ in 0..6 { + let (object_id, opcode, payload) = client.read_message().expect("read output event failed"); + assert_eq!(object_id, output_id); + match opcode { + 0 => saw_geometry = true, + 1 => { + assert_eq!(payload.len(), 16); + saw_mode = true; + } + 2 => saw_done = true, + 3 => { + assert_eq!(payload, 1i32.to_le_bytes()); + saw_scale = true; + } + 4 => { + let mut cursor = 0; + assert_eq!(read_wayland_string(&payload, &mut cursor), "RedBear-0"); + saw_name = true; + } + 5 => { + let mut cursor = 0; + assert_eq!( + read_wayland_string(&payload, &mut cursor), + "Red Bear OS framebuffer output" + ); + saw_description = true; + } + _ => panic!("unexpected wl_output opcode {opcode}"), + } + } + assert!(saw_geometry && saw_mode && saw_scale && saw_name && saw_description && saw_done); + + let (seat_name, seat_version) = *globals.get("wl_seat").expect("wl_seat missing"); + assert!(seat_version >= 2, "wl_seat must advertise name-capable v2+"); + let seat_id = client + .bind(registry, seat_name, "wl_seat", 5) + .expect("bind wl_seat failed"); + + let mut saw_capabilities = false; + let mut saw_seat_name = false; + for _ in 0..2 { + let (object_id, opcode, payload) = client.read_message().expect("read seat event failed"); + assert_eq!(object_id, seat_id); + match opcode { + 0 => { + assert_eq!(payload, 1u32.to_le_bytes()); + saw_capabilities = true; + } + 1 => { + let mut cursor = 0; + assert_eq!(read_wayland_string(&payload, &mut cursor), "seat0"); + saw_seat_name = true; + } + _ => panic!("unexpected wl_seat opcode {opcode}"), + } + } + assert!(saw_capabilities && saw_seat_name); + + client + .send_message(output_id, 0, &[]) + .expect("wl_output.release failed"); + let (object_id, opcode, payload) = client.read_message().expect("read output delete_id failed"); + assert_eq!(object_id, 1); + assert_eq!(opcode, 2); + assert_eq!(payload, output_id.to_le_bytes()); + + compositor.kill().ok(); + let _ = std::fs::remove_file(socket); +} + +#[test] +fn test_compositor_real_surface_opcodes() { + let socket = "/tmp/test-redbear-compositor-real-surface.sock"; + let _ = std::fs::remove_file(socket); + + let mut compositor = start_compositor(socket); + let mut client = WaylandClient::connect(socket).expect("failed to connect"); + + let registry = client.get_registry().expect("get_registry failed"); + let mut globals = HashMap::new(); + for _ in 0..9 { + let (_, opcode, payload) = client.read_message().expect("read failed"); + assert_eq!(opcode, 0); // wl_registry.global + let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let mut cursor = 4; + let iface = read_wayland_string(&payload, &mut cursor); + globals.insert(iface, name); + } + + let compositor_name = *globals + .get("wl_compositor") + .expect("wl_compositor global missing"); + let compositor_id = client + .bind(registry, compositor_name, "wl_compositor", 4) + .expect("bind wl_compositor failed"); + + let surface_id = client.alloc_id(); + client + .send_message(compositor_id, 0, &surface_id.to_le_bytes()) + .expect("wl_compositor.create_surface failed"); + + let region_id = client.alloc_id(); + client + .send_message(compositor_id, 1, ®ion_id.to_le_bytes()) + .expect("wl_compositor.create_region failed"); + + client + .send_message(surface_id, 4, ®ion_id.to_le_bytes()) + .expect("wl_surface.set_opaque_region failed"); + client + .send_message(region_id, 0, &[]) + .expect("wl_region.destroy failed"); + let (object_id, opcode, payload) = client.read_message().expect("read region delete_id failed"); + assert_eq!(object_id, 1); + assert_eq!(opcode, 2); + assert_eq!(payload, region_id.to_le_bytes()); + + let callback_id = client.alloc_id(); + client + .send_message(surface_id, 3, &callback_id.to_le_bytes()) + .expect("wl_surface.frame failed"); + let (object_id, opcode, payload) = client.read_message().expect("read frame callback failed"); + assert_eq!(object_id, callback_id); + assert_eq!(opcode, 0); + assert_eq!(payload.len(), 4); + + client + .send_message(surface_id, 6, &[]) + .expect("wl_surface.commit failed"); + client + .send_message(surface_id, 0, &[]) + .expect("wl_surface.destroy failed"); + let (object_id, opcode, payload) = client + .read_message() + .expect("read surface delete_id failed"); + assert_eq!(object_id, 1); + assert_eq!(opcode, 2); + assert_eq!(payload, surface_id.to_le_bytes()); + + compositor.kill().ok(); + let _ = std::fs::remove_file(socket); +} + +#[test] +fn test_compositor_xdg_popup_lifecycle() { + let socket = "/tmp/test-redbear-compositor-xdg-popup.sock"; + let _ = std::fs::remove_file(socket); + + let mut compositor = start_compositor(socket); + let mut client = WaylandClient::connect(socket).expect("failed to connect"); + + let registry = client.get_registry().expect("get_registry failed"); + let mut globals = HashMap::new(); + for _ in 0..9 { + let (_, opcode, payload) = client.read_message().expect("read failed"); + assert_eq!(opcode, 0); // wl_registry.global + let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let mut cursor = 4; + let iface = read_wayland_string(&payload, &mut cursor); + globals.insert(iface, name); + } + + let compositor_name = *globals + .get("wl_compositor") + .expect("wl_compositor global missing"); + let xdg_name = *globals.get("xdg_wm_base").expect("xdg_wm_base missing"); + let compositor_id = client + .bind(registry, compositor_name, "wl_compositor", 4) + .expect("bind wl_compositor failed"); + let xdg_wm_base_id = client + .bind(registry, xdg_name, "xdg_wm_base", 1) + .expect("bind xdg_wm_base failed"); + + let parent_surface = client.alloc_id(); + client + .send_message(compositor_id, 0, &parent_surface.to_le_bytes()) + .expect("create parent surface failed"); + let parent_xdg = client.alloc_id(); + let mut payload = Vec::new(); + payload.extend_from_slice(&parent_xdg.to_le_bytes()); + payload.extend_from_slice(&parent_surface.to_le_bytes()); + client + .send_message(xdg_wm_base_id, 2, &payload) + .expect("get parent xdg_surface failed"); + + let positioner = client.alloc_id(); + client + .send_message(xdg_wm_base_id, 1, &positioner.to_le_bytes()) + .expect("create_positioner failed"); + let mut payload = Vec::new(); + payload.extend_from_slice(&64i32.to_le_bytes()); + payload.extend_from_slice(&32i32.to_le_bytes()); + client + .send_message(positioner, 1, &payload) + .expect("positioner.set_size failed"); + + let popup_surface = client.alloc_id(); + client + .send_message(compositor_id, 0, &popup_surface.to_le_bytes()) + .expect("create popup surface failed"); + let popup_xdg = client.alloc_id(); + let mut payload = Vec::new(); + payload.extend_from_slice(&popup_xdg.to_le_bytes()); + payload.extend_from_slice(&popup_surface.to_le_bytes()); + client + .send_message(xdg_wm_base_id, 2, &payload) + .expect("get popup xdg_surface failed"); + + let popup = client.alloc_id(); + let mut payload = Vec::new(); + payload.extend_from_slice(&popup.to_le_bytes()); + payload.extend_from_slice(&parent_xdg.to_le_bytes()); + payload.extend_from_slice(&positioner.to_le_bytes()); + client + .send_message(popup_xdg, 2, &payload) + .expect("xdg_surface.get_popup failed"); + + let (object_id, opcode, payload) = client.read_message().expect("read popup configure failed"); + assert_eq!(object_id, popup); + assert_eq!(opcode, 0); + assert_eq!(payload.len(), 16); + let (object_id, opcode, payload) = client + .read_message() + .expect("read popup surface configure failed"); + assert_eq!(object_id, popup_xdg); + assert_eq!(opcode, 0); + assert_eq!(payload.len(), 4); + + client + .send_message(popup, 0, &[]) + .expect("xdg_popup.destroy failed"); + let (object_id, opcode, payload) = client.read_message().expect("read popup delete_id failed"); + assert_eq!(object_id, 1); + assert_eq!(opcode, 2); + assert_eq!(payload, popup.to_le_bytes()); + + client + .send_message(positioner, 0, &[]) + .expect("xdg_positioner.destroy failed"); + let (object_id, opcode, payload) = client + .read_message() + .expect("read positioner delete_id failed"); + assert_eq!(object_id, 1); + assert_eq!(opcode, 2); + assert_eq!(payload, positioner.to_le_bytes()); + + compositor.kill().ok(); + let _ = std::fs::remove_file(socket); +} + #[test] fn test_compositor_sync_roundtrip() { let socket = "/tmp/test-redbear-compositor-sync.sock";