Quick reference for key smithay APIs used in driftwm. See the source at
~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/smithay-0.7.0/.
Source: src/input/pointer/grab.rs
13-method trait for intercepting pointer events during a grab:
trait PointerGrab<D: SeatHandler>: Send + Downcast {
fn motion(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>,
focus: Option<(PointerFocus, Point<f64, Logical>)>, event: &MotionEvent);
fn relative_motion(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>,
focus: Option<(PointerFocus, Point<f64, Logical>)>, event: &RelativeMotionEvent);
fn button(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>, event: &ButtonEvent);
fn axis(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>, details: AxisFrame);
fn frame(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>);
fn gesture_swipe_begin/update/end(...); // 3 methods
fn gesture_pinch_begin/update/end(...); // 3 methods
fn gesture_hold_begin/end(...); // 2 methods
fn start_data(&self) -> &GrabStartData<D>;
fn unset(&mut self, data: &mut D);
}pub struct GrabStartData<D: SeatHandler> {
pub focus: Option<(<D as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
pub button: u32,
pub location: Point<f64, Logical>,
}impl PointerHandle<D> {
fn set_grab(&self, data: &mut D, grab: G, serial: Serial, focus: Focus);
fn unset_grab(&self, data: &mut D, serial: Serial, time: u32);
fn button(&self, data: &mut D, event: &ButtonEvent);
// button() updates pressed_buttons BEFORE calling grab.button()
fn grab_start_data(&self) -> Option<GrabStartData<D>>;
fn current_location(&self) -> Point<f64, Logical>;
}impl PointerInnerHandle<'_, D> {
fn motion(&mut self, data: &mut D, focus: Option<(Focus, Point)>, event: &MotionEvent);
fn button(&mut self, data: &mut D, event: &ButtonEvent);
fn axis(&mut self, data: &mut D, details: AxisFrame);
fn frame(&mut self, data: &mut D);
fn unset_grab(&mut self, handler: &mut dyn PointerGrab<D>, data: &mut D,
serial: Serial, time: u32, restore_focus: bool);
fn current_pressed(&self) -> &[u32];
fn current_focus(&self) -> Option<(PointerFocus, Point<f64, Logical>)>;
fn current_location(&self) -> Point<f64, Logical>;
// + gesture forwarding methods
}pub enum Focus { Keep, Clear }Source: src/utils/user_data.rs
// get_or_insert returns &T (immutable!) — use RefCell for mutation
states.data_map.get_or_insert(|| RefCell::new(MyState::default())).borrow() // read
states.data_map.get_or_insert(|| RefCell::new(MyState::default())).replace(val) // writePlain enum (NOT bitflags). Values: None=0, Top=1, Bottom=2, Left=4, Right=8,
TopLeft=5, TopRight=9, BottomLeft=6, BottomRight=10.
Use (edge as u32) & bit for component checks.
toplevel.with_pending_state(|state| {
state.size = Some(new_size);
state.states.set(xdg_toplevel::State::Resizing);
});
toplevel.send_pending_configure();XdgToplevelSurfaceRoleAttributes::pending_configures() -> &[ToplevelConfigure]
is public, reached via with_states(surface, |s| s.data_map.get::<XdgToplevelSurfaceData>())
(XdgToplevelSurfaceData = Mutex<XdgToplevelSurfaceRoleAttributes>, so
.lock().unwrap().pending_configures()). Entries are pruned in ack_configure
with retain(|c| c.serial > serial), so non-empty ⇔ the latest configure is not
yet acked. Treat a missing data entry as "no pending" (unwrap_or(false)).
Non-empty does not imply a pending resize: a compositor queues size-less
(state.size == None/(0,0), "client picks") configures too, so to detect an
owed resize inspect each ToplevelConfigure's state.size for a real
(non-zero) size differing from the committed geometry, not just list length.
Pruning is latched to the ack_configure request (the acked configure moves
to last_acked), not to the commit that applies it — and real clients (GTK4)
ack as soon as they process the event, then keep committing old-size frames
until their next resized render. So "no pending configures" must not be
read as "committed geometry is current". To gate on a size transition actually
completing, track it compositor-side off the committed geometry changing (see
pending_recenter), not off ack state.
ToplevelSurface::send_pending_configure() gates on has_pending_changes(),
which is !initial_configure_sent || <server_pending differs>. So it does not
no-op before the initial configure — it forces one out. To flush a mid-session
change (e.g. an Activated flip on focus change) without prematurely sending, and
thereby fragmenting, a batched first-commit configure, guard on
ToplevelSurface::is_initial_configure_sent() first. Window::set_activated(bool) -> bool returns whether the hint actually changed, so pair the two: flush only
when set_activated reports a change and the initial configure is already out.
let modifiers = self.seat.get_keyboard().unwrap().modifier_state();
if modifiers.alt { ... }Source: src/input/pointer/cursor_image.rs
pub enum CursorImageStatus {
Hidden,
Named(CursorIcon), // CursorIcon from cursor_icon crate
Surface(WlSurface), // client-provided cursor
}
impl CursorImageStatus {
pub fn default_named() -> Self { Self::Named(CursorIcon::Default) }
}CursorIcon::name() returns CSS cursor names: "default", "pointer", "grabbing", etc.
Source: src/wayland/cursor_shape.rs
// Init: requires TabletSeatHandler impl (even empty)
let state = CursorShapeManagerState::new::<DriftWm>(&display_handle);
delegate_cursor_shape!(DriftWm);
// Also need: impl TabletSeatHandler for DriftWm {}Source: src/backend/renderer/element/memory.rs
// Create from pixel data:
let buffer = MemoryRenderBuffer::from_slice(
&pixels_rgba, // &[u8]
Fourcc::Abgr8888, // format (xcursor pixels_rgba is ABGR)
(width, height), // impl Into<Size<i32, Buffer>>
1, // scale
Transform::Normal,
None, // opaque_regions
);
// Create render element:
let elem = MemoryRenderBufferRenderElement::from_buffer(
renderer, // &mut R where R: ImportMem
location, // impl Into<Point<f64, Physical>> — PHYSICAL coords!
&buffer,
None, // alpha: Option<f32>
None, // src: Option<Rectangle<f64, Logical>>
None, // size: Option<Size<i32, Logical>>
Kind::Cursor, // Kind enum
)?;Source: src/desktop/space/mod.rs
pub fn render_output<R, C, E, S>(
output: &Output,
renderer: &mut R,
framebuffer: &mut R::Framebuffer<'_>,
alpha: f32,
age: usize,
spaces: S,
custom_elements: &[C], // C: RenderElement<R> — rendered ON TOP of space
damage_tracker: &mut OutputDamageTracker,
clear_color: impl Into<Color32F>,
) -> Result<RenderOutputResult, OutputDamageTrackerError<R::Error>>Source: src/wayland/shell/xdg/mod.rs
impl PopupSurface {
pub fn send_configure(&self) -> Result<Serial, PopupConfigureError>;
pub fn send_repositioned(&self, token: u32);
pub fn with_pending_state<F, T>(&self, f: F) -> T
where F: FnOnce(&mut PopupState) -> T;
}Must call send_configure() in new_popup — client won't commit until it receives this.
Set geometry first: surface.with_pending_state(|s| s.geometry = positioner.get_geometry()).
Source: src/desktop/wayland/popup/manager.rs
impl PopupManager {
pub fn track_popup(&mut self, kind: PopupKind) -> Result<(), ...>;
pub fn commit(surface: &WlSurface); // call in CompositorHandler::commit()
pub fn cleanup(&mut self); // call each frame
// Static — used internally by Window::render_elements()
pub fn popups_for_surface(surface: &WlSurface)
-> impl Iterator<Item = (PopupKind, Point<i32, Logical>)>;
}render_output() → Window::render_elements() → PopupManager::popups_for_surface() →
render_elements_from_surface_tree() per popup. Fully automatic — no compositor render code needed.
The protocol flow for a popup on a layer surface is xdg_surface.get_popup(None, positioner) then
zwlr_layer_surface_v1.get_popup(xdg_popup). The first call fires XdgShellHandler::new_popup with
parent = None, and track_popup queues a parentless popup into unmapped_popups; the popup's
first commit drains that entry and inserts it into the popup tree (the parent is set by then).
Calling track_popup again from the layer handler inserts a second tree node — PopupTree::insert
never dedupes — and popups_for_surface then yields the popup twice, so it renders twice
(double-alpha on translucent pixels, doubled per-popup render work). The layer handler should only
unconstrain_popup; the xdg-path unmapped entry handles tracking. find_popup searches
unmapped_popups too, so the popup is resolvable in the pre-commit window.
Window::bbox() / LayerSurface::bbox() cover the toplevel and its subsurfaces but not popups;
the _with_popups variants merge in every popup from PopupManager::popups_for_surface.
SpaceElement::bbox for Window is bbox_with_popups() — Space never used the popup-less box
(source: src/desktop/space/wayland/window.rs). Window::send_frame and LayerSurface::send_frame
both send frame callbacks to popup surface trees too, so throttling decisions keyed on a popup-less
bbox starve visible popups.
SpaceElement::is_in_input_region for Window is literally
self.surface_under(*point, WindowSurfaceType::ALL).is_some()
(source: src/desktop/space/wayland/window.rs) — the "cheap predicate" and the "full lookup" are the
same traversal, so a bbox test in front of it is the only actual early-out.
WindowSurfaceType::ALL = TOPLEVEL | SUBSURFACE | POPUP, and Window::surface_under tests
popups first (in PopupManager::popups_for_surface order, each offset by
geometry().loc + location - popup.geometry().loc) before descending the toplevel's surface tree.
Per-surface, contains_point (src/desktop/wayland/utils.rs) rejects points outside the buffer
rect, then returns true when input_region.is_none() — a surface that declares no input region
is input across its whole buffer. A CSD client's shadow margin is usually exactly that, so it
hit-tests as content, not as empty space.
Window::bbox() is a commit-time cache: on_commit recomputes it from the surface tree, and
driftwm only calls on_commit for non-sync-subsurface commits (src/handlers/compositor.rs). The
popup half of bbox_with_popups() is computed live. So the box and surface_under can disagree
transiently — treat the bbox as a conservative filter, not as an equivalent test.
Source: src/wayland/selection/data_device/mod.rs
pub fn set_data_device_focus<D>(dh: &DisplayHandle, seat: &Seat<D>, client: Option<Client>)
where D: SeatHandler + DataDeviceHandler + 'static;Sends wl_data_device.selection to newly focused client. Call in SeatHandler::focus_changed().
Source: src/wayland/selection/primary_selection/mod.rs
pub fn set_primary_focus<D>(dh: &DisplayHandle, seat: &Seat<D>, client: Option<Client>)
where D: SeatHandler + PrimarySelectionHandler + 'static;Same pattern — call alongside set_data_device_focus.
fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&Self::KeyboardFocus>) {
let dh = &self.display_handle;
let client = focused.and_then(|f| dh.get_client(f.0.id()).ok());
set_data_device_focus(dh, seat, client.clone());
set_primary_focus(dh, seat, client);
}Startup-notification / focus-request tokens. XdgActivationState owns a
HashMap<XdgActivationToken, XdgActivationTokenData>. XdgActivationToken is a
newtype over a random 32-char alphanumeric String (Deref<Target = str>,
as_str(), From<String>).
// Compositor-minted token, no client association. Does NOT call token_created.
// Returns refs borrowed from the &mut XdgActivationState.
pub fn create_external_token(
&mut self,
data: impl Into<Option<XdgActivationTokenData>>,
) -> (&XdgActivationToken, &XdgActivationTokenData);
pub fn remove_token(&mut self, token: &XdgActivationToken) -> bool;
pub fn data_for_token(&self, token: &XdgActivationToken) -> Option<&XdgActivationTokenData>;
pub fn retain_tokens<F: FnMut(&XdgActivationToken, &XdgActivationTokenData) -> bool>(&mut self, f: F);XdgActivationTokenData fields: client_id, serial: Option<(Serial, WlSeat)>
(the input serial — None for a compositor-minted or spontaneous token),
app_id, surface (the requesting surface, not the one to activate),
timestamp: Instant, and user_data: Arc<UserDataMap>.
Attaching custom data: stamp user_data (an Arc<UserDataMap>) right after
minting via the returned data ref:
let (token, data) = state.create_external_token(None);
data.user_data.insert_if_missing_threadsafe(|| MyMarker(id)); // T: Send + Sync + 'static
let token = token.clone();UserDataMap::get::<T>() returns Option<&T>; a value inserted with the
non-threadsafe insert_if_missing is only visible from the thread it was
inserted on (use insert_if_missing_threadsafe to be thread-agnostic).
Round-trip: on a client xdg_activation_v1.activate { token, surface }, the
dispatch looks up known_tokens.get(&token).cloned() and calls
XdgActivationHandler::request_activation(token, token_data, surface). The
token_data is a clone, but user_data is an Arc, so a stamped marker is
visible there. An unknown token string is silently dropped (no handler call).
The token stays in the pool after request_activation until remove_token /
retain_tokens. token_created fires only for client-built tokens (the
.commit() path), never for create_external_token.
let theme = xcursor::CursorTheme::load("default"); // respects XCURSOR_PATH
let path = theme.load_icon("default")?; // -> PathBuf
let images = xcursor::parser::parse_xcursor(&std::fs::read(path)?)?;
// Image { width, height, xhot, yhot, pixels_rgba: Vec<u8>, pixels_argb: Vec<u8>, size, delay }Source: src/backend/drm/compositor/mod.rs (use_mode), src/backend/drm/surface/atomic.rs (AtomicDrmSurface::use_mode); git checkout under ~/.cargo/git/checkouts/smithay-*/.
use_mode does not modeset immediately:
AtomicDrmSurface::use_modecreates a mode property blob + a throwaway test buffer, submits aTEST_ONLYatomic commit to validate, and on success just stores the mode in the surface'spendingstate.DrmCompositor::use_modethen resizes the swapchain to the new dimensions.- The real modeset lands with the next frame commit (
queue_framepicks up the pending state, commits withALLOW_MODESET).
So it never races an in-flight page flip — the kernel serializes atomic commits per CRTC, and the pending frame's fb holds its own reference. niri calls use_mode unconditionally at config-apply time with no deferral (tty.rs, on_output_config_changed) and only handles the Err. Deferring/queueing around frames_pending before calling it is unnecessary.
Source: src/backend/input/mod.rs, src/backend/input/tablet.rs.
InputBackend is 25 associated types and zero methods — a pure type-level
description of what a backend can emit. It is declared pub trait InputBackend: Sized, so it is never object-safe: backends are selected with a type parameter
(process_input_event::<I>), never a dyn. Implementing it costs nothing but
the event types you actually produce:
pub enum UnusedEvent {}is uninhabited and carries a blanket impl of every event trait (each method ismatch *self {}). Any associated type you don't emit is= UnusedEventwith no code written.SpecialEventhas no trait bound at all.Device: PartialEq + Eq + Hash—id() -> String,name() -> String,has_capability(DeviceCapability) -> bool,usb_id() -> Option<(u32, u32)>,syspath() -> Option<PathBuf>.DeviceCapabilityisCopy + Eqbut notHash, so a device holding a capability list can't deriveHash— hash the id instead.Event<B>::time()is MICROseconds;time_msec()is provided and divides by 1000.device()returnsB::Deviceby value, so the device type is usuallyClone.- Marker traits over shared supertraits:
PointerMotionAbsoluteEvent<B>andTouchDownEvent<B>add nothing toAbsolutePositionEvent<B>(x,y,x_transformed(width),y_transformed(height), plus providedposition/position_transformed) andTouchEvent<B>(slot() -> TouchSlot) — impl the supertrait, thenimpl Marker for T {}. PointerButtonEvent<B>needs onlybutton_code() -> u32andstate() -> ButtonState;button() -> Option<MouseButton>is provided.InputEvent<B>derivesDebug, which bounds onB: Debug— a backend that isn'tDebugstill works, its events just can't be printed.
driftwm's synthetic backend for tests lives in src/tests/input_backend.rs.
- Must call
on_commit_buffer_handler::<DriftWm>(surface)inCompositorHandler::commit()— NOT done bydelegate_compositor!. Without it,RendererSurfaceStateis never populated,surface_viewstays None,bbox_from_surface_tree()returns 0x0, windows invisible. - Must call
output.create_global::<DriftWm>(&display_handle)—space.map_output()is internal only; clients need awl_outputglobal to see monitors. ToplevelSurface::send_configure()must be called innew_toplevel— clients won't render until they receive an initial configure.PopupSurface::send_configure()must be called innew_popup— same as toplevels. Also set geometry from positioner:surface.with_pending_state(|s| s.geometry = positioner.get_geometry()).- Cross-app clipboard requires
set_data_device_focus+set_primary_focusinSeatHandler::focus_changed(). Without this, newly focused clients don't receivewl_data_device.selectionevents and can't paste from other apps. Extract client viadh.get_client(surface.id()).ok().
post_erroron an already-destroyed resource kills the client WITHOUT deliveringwl_display.error.send_eventresolves the object argument, getsErr(InvalidId)because the destructor already removed it from the object map, and the send result is discarded ("errors are ignored, as the client will be killed anyway") — wayland-backendrs/server_impl/client.rs. The socket just EOFs, so client-side it isWaylandError::Io, andConnection::protocol_error()mapsIo(_) => None. Tests must not assert onprotocol_error()for these; the observable symptom is a bareBroken pipe (os error 32).
- Winit backend needs
Transform::Flipped180on the output — EGL Y-axis is inverted relative to Wayland coordinates. Transform::Normalfor udev — DRM handles orientation natively.- WAYLAND_DISPLAY must NOT be set before
winit::init()— winit connects to the parent compositor; setting our socket first causes a deadlock. - Backend on state — winit backend stored as
Option<WinitGraphicsBackend<GlesRenderer>>on DriftWm. Timer closure uses take/put pattern to split borrows. Required for DmabufHandler to access renderer. - DMA-BUF v3 (create_global) sufficient for winit backend — advertises formats, no device info. v4 (create_global_with_default_feedback) adds render device hints for multi-GPU.
ImportDma::dmabuf_formats()on GlesRenderer gets formats from EGL. - Benign
EGL BAD_SURFACEerror on first frame is frombuffer_age()before the surface is ready;unwrap_or(0)handles it.
- Don't add +8 to keycodes from smithay input events — they're already XKB keycodes. Adding 8 double-offsets every key.
- Mouse wheel vs trackpad scroll —
PointerAxisEvent::amount()returnsNonefor discrete mouse wheels. Useamount_v120()(120 = one notch) as fallback:event.amount(axis).or_else(|| event.amount_v120(axis).map(|v| v * 15.0 / 120.0)).
- DataMap::get_or_insert returns
&T(immutable) — wrap state inRefCellfor mutation. Use.borrow()/.replace(). insert_if_missing_threadsaferequiresSync—RefCellis notSync, useMutexfor data stored in smithay'sUserDataMap(e.g.AppliedWindowRuleon surface data_map).xdg_toplevel::ResizeEdgeis an enum, NOT bitflags — use(edge as u32) & bitfor component checks.- Resize position adjustment must be absolute, not incremental — store
initial_window_locationinResizeStateand computenew_loc = initial_loc + (initial_size - current_size). Incrementalloc += deltacauses cumulative drift.
- Popup grabs require
FocusTargetwrapper —PopupGrabneedsKeyboardFocus: From<PopupKind>. Can't implFrom<PopupKind> for WlSurface(orphan rule), so use aFocusTarget(WlSurface)newtype that implsFrom<PopupKind>,WaylandFocus,KeyboardTarget,PointerTarget,TouchTarget. - PopupKeyboardGrab/PopupPointerGrab at
smithay::desktop::{PopupKeyboardGrab, PopupPointerGrab}. - PopupUngrabStrategy at
smithay::desktop::PopupUngrabStrategy, NOTsmithay::wayland::shell::xdg. - Never call
data.seat.get_pointer()insidePointerGrab::unset()—unset()runs while smithay's internal pointer mutex is held; re-entering it viaget_pointer()deadlocks. Do all side-effects inbutton()beforehandle.unset_grab(). LayerMapguard (MutexGuard) must be dropped before callingkeyboard.set_focus()—set_focustriggersSeatHandler::focus_changed()which may need&mut self.- Layer surface exclusive focus must be guarded — only grab keyboard focus when it's not already on this surface. Otherwise every commit from an Exclusive layer surface steals focus back.
pointer_over_layermust be reset on layer destroy and fullscreen enter — stale flag breaks all input until next motion event.TouchHandle::is_grabbed()is true after anydown(), andgrab_start_data()is no better — smithay's touchDefaultGrab::down(input/touch/grab.rs) installs its ownTouchDownGrabunconditionally, sois_grabbed()can't distinguish "the compositor set a grab" from "a finger is down".grab_start_data()is the same tautology under another name:set_grabstores every grab asGrabStatus::Active(input/touch/mod.rs) andgrab_start_data()returnsSomefor anyActive, so it isSomeafter anydown()too — it reports the start data of whichever grab is installed, not who installed it. Nothing onTouchHandleanswers the question; assert on the compositor's own grab bookkeeping instead.
- WlSurface protocol methods clash with trait methods —
WlSurface::enter()is thewl_surface.enter(output)protocol method. When delegatingKeyboardTarget::enteretc. from a newtype, use fully-qualified syntax:KT::<D>::enter(&self.0, ...). - TouchTarget has 7 methods, not 5 —
down,up,motion,frame,cancelplusshapeandorientation.
- Custom shader background via
PixelShaderElement—GlesRenderer::compile_custom_pixel_shader(src, &[UniformName])compiles GLSL (auto-prepends#version 100).PixelShaderElement::new(shader, area, opaque_regions, alpha, uniforms, kind)creates a render element. Built-in varyings:v_coords(0-1),size(output pixels),alpha. Custom uniforms viaUniform::new(name, value). render_elements!macro<=form for concrete renderer:render_elements! { pub Name<=GlesRenderer>; Variant=Type, }— generates non-generic enum withimpl RenderElement<GlesRenderer>.space_render_elements()is public atsmithay::desktop::space::space_render_elements. ReturnsVec<SpaceRenderElements<R, E>>.- Element ordering in damage tracker: first element = topmost (drawn last), last element = bottommost (drawn first). For background behind windows: use
damage_tracker.render_output()directly with [cursor, space_elements, background] ordering. Element::opaque_regions()anddamage_since()must return ELEMENT-LOCAL coords (relative togeometry().loc).OutputDamageTrackertranslates them byelement_loc = geometry(scale).locitself (damage/mod.rs:region.loc += element_loc).geometry()itself is absolute. Returning absolute opaque regions double-offsets them to2 × geometry.loc— the renderer then skips clearing under that phantom rect and nothing draws there, leaving unpainted holes (black live / uninitialized-magenta in a fresh capture buffer). A full-screen element at origin(0,0)hides this (local == absolute); elements at non-zero positions (e.g. tiled-bg chunks) expose it. The defaultdamage_since()returnsRectangle::from_size(geometry(scale).size)at(0,0)— already local — so custom elements only need to fixopaque_regions().gles::uniformmodule is private — types (Uniform,UniformName,UniformType) re-exported atsmithay::backend::renderer::gles::{Uniform, UniformName, UniformType}.RescaleRenderElementatsmithay::backend::renderer::element::utils::RescaleRenderElement—from_element(elem, physical_origin, scale)scales position+size. Used for zoom.Space::render_elements_for_region()— returnsVec<WaylandSurfaceRenderElement<R>>for an arbitrary rectangle. Positions offset by-region.loc(camera). Doesn't clip to output geometry — essential for zoom < 1.0.
WlrLayerShellHandler::new_layer_surfacetakeswlr_layer::LayerSurface(protocol type), NOTdesktop::LayerSurface— wrap withdesktop::LayerSurface::new(surface, namespace)before passing tolayer_map_for_output().map_layer().- Pointer must ALWAYS stay in canvas coords — even when over a layer surface.
layer_surface_under()returns an adjusted focus location so smithay computes correct surface-local coords. - The layer-shell validation
pre_commit_hookis registered once perwl_surfaceand NEVER removed (wlr_layer/handlers.rs, gated onif initial; smithay does not expose itsHookId). Its error ladder, in order (wlr_layer/mod.rs), each posting onrole.surfaceand returning:InvalidSizeforsize.w == 0 && !anchor.anchored_horizontally(),InvalidSizefor the vertical twin,InvalidExclusiveEdgefor anexclusive_edgeoutsideanchor, thenInvalidSurfaceState("must ack the initial configure before attaching buffer") whenhas_buffer && role.last_acked.is_none().has_buffercomes fromSurfaceAttributes::pending().buffer:NewBuffer→ true,Removed→ false,None→had_buffer_before— and that one is a differentpending(),LayerSurfaceCachedState::pending().last_acked.is_some(), not anything onSurfaceAttributes. zwlr_layer_surface_v1destroy does not unregister that hook, anddestroyed(wlr_layer/handlers.rs) zeroes what it validates against —LayerSurfaceAttributes::reset()plus*pending() = *current() = Default::default()onLayerSurfaceCachedState. Every later commit on thatwl_surfacetherefore re-runs the ladder against zeroed state and posts on the destroyed proxy. Upstream bug; driftwm neutralises it from an earlier hook (handlers/compositor.rs+LayerDestroyedMarker).destroyedremoves the entry fromknown_layersand drops the lock BEFORE callingWlrLayerShellHandler::layer_destroyed— soWlrLayerShellState::layer_surfaces()(impl DoubleEndedIterator<Item = LayerSurface>, clones the list) sees only survivors from inside that callback.PrivateSurfaceData::set_rolepermits taking the same role twice (compositor/tree.rserrors only when the new role differs), so onewl_surfacecan carry two liveZwlrLayerSurfaceV1— and sinceLayerSurfaceAttributes.surfaceis private,!role.surface.alive()can't be used to tell them apart.LayerSurfaceCachedState'sCacheable::commitis*selfandCachedState::commitnever resetspending(compositor/cache.rs) — so anything written intopending()survives a commit and survives into the next role taken on thatwl_surface.get_layer_surfaceonly setspending().layer; thedestroyedwipe has already run by then.- A pre-commit-hook write to
LayerSurfaceCachedState::pending()lands incurrent()too — andcurrent()is the half that gets read.commitis*selfandmerge_intois*into = self(wlr_layer/mod.rs:191-198), so the commit that follows the hook copies the poisonedpendingwholesale overcurrent.LayerMap::arrangereadsLayerSurface::cached_state()(desktop/wayland/layer.rs:309) →with_cached_state→f(guard.current())(wlr_layer/mod.rs:454-462). Anything a hook forces in must therefore be undone in both halves; undoing onlypending()passes tests only by accident ofarrangerefusing to configure beforeinitial_configure_sent. - A future
DrmSyncobjStatewould reintroduce this crash on a different object. Its pre-commit hook is registered atwp_linux_drm_syncobj_manager_v1.get_surface(drm_syncobj/mod.rs), i.e. after driftwm'snew_surfacehooks, so it would see the stripped buffer and postNoBuffer("acquire point without buffer") on a still-livewp_linux_drm_syncobj_surface_v1. On top of that,RendererSurfaceState::update_buffer'sRemovedarm callsreset()and returns before signallingrelease_point, so the client's release fence would never fire for the stripped commit.
- The lock validation
pre_commit_hookis registered unconditionally atget_lock_surface(session_lock/lock.rs) — unlike layer shell'sif initial, so a lock → unlock → lock cycle on the samewl_surfacestacks a second hook. TheHookIdis discarded, so neither can be removed. ExtSessionLockSurfaceV1::destroyedcalls noSessionLockHandlermethod (session_lock/surface.rs) — there is no per-role destroy seam to hook. It runsLockSurfaceAttributes::reset()(server_pending,pending_configures,last_ackedall cleared) and zeroes both halves ofLockSurfaceCachedState, while leavingattributes.surfacepointing at the dead proxy. Every later commit on thatwl_surfacetherefore posts on a destroyed object.- Its error ladder is asymmetric with layer shell's: the first check is unconditional.
last_acked.is_none()→CommitBeforeFirstAck, before anything is read about the buffer — so a barecommit()on an orphaned role kills the client with no buffer involved at all. Then, onSurfaceAttributes::pending().buffer:NewBufferwhose logical size (viewportdstif set, else buffer size / scale / transform) differs from the ackedstate.size→DimensionsMismatch;Removed→NullBufferoutright — lock surfaces are not allowed to unmap, so the layer trick of neutralising a commit by forcingRemovedis itself fatal here.Nonealways passes (it falls through tohad_buffer_before), and so does aNewBufferwhosebuffer_dimensions()isNone: the size comparison sits insideif let Some(buf_size). pending_configuresempty andlast_ackedunset is reachable only throughdestroyed— the invariant driftwm'sneutralise_orphaned_lock_commitdetects on.send_configurepushes ontopending_configuresbefore the event goes out, andack_configureretains onlyserial > ackedwhile settinglast_acked = Some, which then staysSome. So live-and-unacked still holds a pending configure, and live-and-acked holds the ack. A role the compositor never configured shows the same shape — hence the second, "we configured this" half of the gate.- A
wl_surfacecan carry two live lock roles at once, and destroying the stale one resets the live one's state.give_rolesucceeds when the role string matches (compositor/tree.rs), and theDuplicateOutputguard compares per-clientwl_outputresources (session_lock/lock.rs), so a client that bindswl_outputtwice can callget_lock_surfaceon the samewl_surfacetwice. The second call repointsattributes.surfaceand resets nothing; bothExtSessionLockSurfaceV1s stay live over one sharedLockSurfaceAttributes. Destroying the stale proxy then runsdestroyed's reset while the current role is live and acked — producing the empty shape on a live role. Nothing downstream can tell them apart:LockSurfaceAttributes::surfaceispub(crate),LockSurface's own handle is private, andLockSurface::alive()reports thewl_surface, not the role. driftwm's gate therefore latches that surface as orphaned; every commit after that also has its pending buffer taken, so a live lock surface freezes at its last frame (the buffer itself is still released, so the client doesn't stall on it) — which fails safe (session stays locked, nothing leaks, no client is killed) and needs a client-side protocol violation smithay declines to reject. get_lock_surfacere-takes cleanly on awl_surfacethat already had the role.give_roleonly errors on a different role, and theAlreadyConstructedcheck readsSurfaceAttributespending().bufferandcurrent().buffer— bothNoneon a settled surface, sinceRendererSurfaceState::update_buffertakes the current one andCacheable::committakes the pending one. The re-take only repointsattributes.surface; it resets nothing, so anything written intoLockSurfaceAttributessurvives into the new role.- smithay sends its own initial configure after
SessionLockHandler::new_surfacereturns (lock.rs), but only if the handler leftserver_pendingset to a state the role has not already been told.get_pending_statestarts withserver_pending.take()?and then drops it anyway when it equalscurrent_server_state()— the last pending configure, else the last ack. So a handler that returns early without callingwith_pending_stateleaves the role live and permanently unconfigured, and so does one that re-requests the state a surviving ack already carries. - A future
DrmSyncobjStatewould reintroduce the kill on a different object, exactly as it would for layer shell: its hook registers atwp_linux_drm_syncobj_manager_v1.get_surface, i.e. after driftwm'snew_surfacehooks, so it would find the stripped buffer and postNoBufferon a still-livewp_linux_drm_syncobj_surface_v1. Nodrm_syncobjtoday.
- Release-on-replace happens in exactly one place,
SurfaceAttributes::merge_into(compositor/handlers.rs): replacing a pending buffer during the cache merge releases the displaced one unless it's the sameWlBuffer(if Some(&buffer) != new_buffer { buffer.release(); }).Cacheable::commitisbuffer: self.buffer.take()— a plain move. So overwritingpending().bufferfrom a pre-commit hook sends no release at all; do it explicitly or a client recycling a small shm pool stalls forever. - Release-on-drop is the separate second path:
InnerBuffer::dropcallsbuffer.release()(backend/renderer/utils/wayland.rs) — which is whyBufferAssignment::Removedreleases the previously committed buffer andNonedoes not:RendererSurfaceState::update_buffermapsRemoved→reset()→self.buffer = None, dropping the lastArc<InnerBuffer>. Read the currently held buffer viastates.data_map.get::<RendererSurfaceStateUserData>()→RendererSurfaceState::buffer() -> Option<&Buffer>(Buffer: PartialEq<WlBuffer>) to avoid double-releasing one a client re-attached.
XdgDecorationStateatsmithay::wayland::shell::xdg::decoration::XdgDecorationState. Mode enum atwayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode(ServerSide/ClientSide).- Minimal windows: force
Mode::ServerSideon the toplevel — client removes its CSD. Compositor draws no titlebar (still draws shadow + corner clip).
drm_fourcc::DrmFormatre-exported assmithay::backend::allocator::Format— smithay renames it.- Connector subpixel is
connector::SubPixel(notSubpixelOrder). Fromsmithay::reexports::drm::control::connector. DrmDevice::create_surface()needs&mut self.RegistrationTokenhas noDefault— calloop tokens can only be created byinsert_source().smithay-drm-extrasfrom git (not crates.io 0.1) — needed for libdisplay-info 0.3.0 compat (Arch Linux).DrmCompositor::frame_submitted()must be called on VBlank — otherwise buffers aren't recycled and rendering stalls.- LibSeatSession needs
mut—session.open()requires mutable reference. - Libinput
suspend()/resume()take&self(not&mut self) — clone is fine. - Both Backend enum variants should be
Boxed —WinitGraphicsBackend~6.5KB,GlesRenderer~6.3KB. Clippy warns about variant size differences.
- Temporaries in
if letlive until end of block — separatelet x = expr.cloned(); if let Some(x) = x {when needing&mut selfinside the block. - DMA-BUF blocker uses let-chains —
if let Some(dmabuf) = ... && let Ok((blocker, source)) = ... && let Some(client) = ... { }is idiomatic Rust 2024.
driftwm drives per-window wl_surface.enter/leave itself (DriftWm::refresh_window_outputs) instead of Space::refresh. The relevant SpaceElement methods on Window (smithay::desktop::space::SpaceElement; call fully-qualified — Window has inherent methods with clashing names):
SpaceElement::output_enter(&self, output, overlap)— insertsoverlap(relative to the window origin) into theWindow's privateWindowOutputUserDataoverlap map (keyed by a downgradedOutput) and callsrefresh()to push the enter to the toplevel + its popups. Idempotent per output; re-sends on a changed overlap.SpaceElement::output_leave(&self, output)— dropsoutputfrom that map and sendswl_surface.leavefor the toplevel and every popup. No-op afterOutput::leave_all().SpaceElement::refresh(&self)— re-runs the per-surfaceoutput_updatefor the toplevel and popups from theWindow's own overlap map; keeps popup enter/leave fresh without a full diff.SpaceElement::bbox(&self)=Window::bbox_with_popups();geometry()= the window geometry (decoration-excluded).Space::refreshtranslated the bbox bylocation - geometry().locbefore intersecting each output — replicate this when computing overlaps.Space::refreshsemantics (replaced) — retained alive elements, diffed each element's bbox against every mapped output's geometry (output_geometry(o).unwrap_or_else(Rectangle::zero)), sentoutput_enter/output_leaveon change, calledSpaceElement::refreshper element, thenOutput::cleanupper output.Output::cleanup(&self)— prunes dead surfaces from the output's own enter tracking.Output::leave_all(&self)— sendswl_surface.leavefor every surface currently entered on the output (used on placeholder-output teardown).UserDataMapis a lock-free append-only boxed list: a&Tfromget/get_or_insertstays valid across a laterinsert_if_missingof a different type, so holding one userdata borrow while aSpaceElementmethod touches another userdata entry is sound.