<feed xmlns='http://www.w3.org/2005/Atom'>
<title>srdwm/crates/wayland/src/udev/mod.rs, branch main</title>
<subtitle>Cross-platform window manager written in Rust.
</subtitle>
<id>https://srdusr.com/git/srdwm/atom?h=main</id>
<link rel='self' href='https://srdusr.com/git/srdwm/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/'/>
<updated>2026-08-30T18:41:00+00:00</updated>
<entry>
<title>Indent doc list continuations</title>
<updated>2026-08-30T18:41:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-08-30T18:41:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=f2f9ed1b3f9c49b323ce591eb72a406058d324a4'/>
<id>urn:sha1:f2f9ed1b3f9c49b323ce591eb72a406058d324a4</id>
<content type='text'>
</content>
</entry>
<entry>
<title>Confirm Nemo's popup works; fix the two bugs that hid it, and shadow bleed across a monitor seam</title>
<updated>2026-04-19T22:48:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-04-19T22:48:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=436d42da6ef61a5ea20d5102c4baed7bf0993606'/>
<id>urn:sha1:436d42da6ef61a5ea20d5102c4baed7bf0993606</id>
<content type='text'>
Nemo's right-click context menu was the last open punch-list item, parked
twice as untestable. It works: verified end to end in a throwaway nested
compositor, menu and submenu both, at the correct position and stacking.
The popup path itself needed no fix, so the POPUP-GEOM-DIAG/POPUP-GRAB-DIAG
diagnostics are removed.

Two real bugs turned up in the way of testing it.

zwlr_virtual_pointer was a silent no-op on the winit backend. Every
Motion/MotionAbsolute handler read UdevState::bounds() behind an early
return when state.udev was None, and that field is Some only for the DRM
backend. The protocol advertised its global, accepted create_virtual_pointer
and accepted every request, then discarded all motion with no error and no
log. That is the backend a nested instance runs on, so the only safe way to
drive a throwaway compositor - a Wayland client of that compositor, which
cannot reach any other session, unlike ydotool's /dev/uinput writes - did
not work at all. Bounds now come from WindowManager::monitors() when udev is
absent; both backends fill that list from Platform::monitors().

The winit backend's screencopy pass rendered no popups and no shadows. It
re-renders the scene offscreen, and that second scene was missing tiers, so
grim on a nested instance reported the opposite of the truth: a menu drawing
perfectly on screen photographed as absent. The DRM backend never had this,
since it serves screencopy from the on-screen frame it just drew. Border
strips are still missing from that pass, called out in the code rather than
left silent.

Also fixed, from the "windows show a bit in the other monitor" report:
shadow_rect expanded by SHADOW_SIZE on every side with no monitor-boundary
awareness, so a window flush against a seam put its 24px shadow strip on the
neighbouring screen. shadow_rect_clipped clips to the bounding box of the
monitors the window's geometry actually touches - not just its assigned
one, since a window straddling a seam really does occupy both and clipping
there would cut its shadow off mid-body. The bitmap's own extent stays
unclipped, because the src rectangle indexes into it; only the fragment list
is clipped. Six tests on the incident's own numbers. Not confirmed on
screen: the nested backend cannot produce a second monitor.

New tool: tools/virtual-pointer-click, a scriptable virtual-pointer driver
that acknowledges each command after its round-trip, so a test script can
put a screenshot between a move and the click that follows it.

489 tests pass, clippy clean.
</content>
</entry>
<entry>
<title>Fix intermittent cursor ghosting when crossing between monitors</title>
<updated>2025-10-07T20:20:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-10-07T20:20:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=9f07715b469a481849ee35ffa33d6dab34356c7d'/>
<id>urn:sha1:9f07715b469a481849ee35ffa33d6dab34356c7d</id>
<content type='text'>
Live report: the real cursor sometimes leaves a brief ghost behind right
after moving between monitors. The bare-metal render loop already forces
a full repaint (ages = [0, 0]) on a workspace switch or any window move/
resize/open/close/restack, both added earlier for the same underlying gap:
the damage tracker's own element diffing doesn't always catch a vacated
region on its own. Neither reset noticed the pointer leaving one monitor
for another - no window moved, no workspace changed - so that head's own
vacated cursor-sized region was left entirely to the tracker's diffing,
intermittently.

Adds UdevState::last_cursor_head, compared each frame the same way the
other two resets are; only the head the pointer just left gets forced back
to ages = [0, 0] (the newly-entered head draws a genuinely new element
there and diffs correctly on its own).
</content>
</entry>
<entry>
<title>Make the secondary-cursor sprite opt-in and expire stale entries</title>
<updated>2025-10-03T20:59:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-10-03T20:59:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=4eb36873d4ca4b0faf0e54dbeab8f5f6450dd704'/>
<id>urn:sha1:4eb36873d4ca4b0faf0e54dbeab8f5f6450dd704</id>
<content type='text'>
Live report: a second cursor appeared uninvited and unusably (frozen,
uncontrollable) on screen. Multi-cursor Phase 1 rendered one sprite per
physical libinput pointer device that had ever reported a position, with
no way to turn it off and no expiry - so a phantom device (a real mouse's
side-button/scroll cluster enumerating as its own HID path is a common
case) that reports once and never moves again left a frozen ghost sprite
with nothing to control or dismiss it.

Adds general.multi_cursor (default false, live-settable via
`srd set multi_cursor &lt;bool&gt;`) and keys secondary_cursors to
(Point, Instant) so both the recording side (udev/session.rs) and the
render side (udev/render.rs) drop any entry older than
SECONDARY_CURSOR_TIMEOUT (1.5s). The "agent controls a window without
interrupting the user" use case this report also raised was never gated
on this flag - that's Multi-cursor Phase 2's pinned virtual-pointer
delivery, which never shows a visible cursor at all.
</content>
</entry>
<entry>
<title>Fake (headless) monitors, and fix new windows all opening in one spot</title>
<updated>2025-09-26T20:07:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-09-26T20:07:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=9a9aa8fe9d4f4f9b15860b45244e695942c90afd'/>
<id>urn:sha1:9a9aa8fe9d4f4f9b15860b45244e695942c90afd</id>
<content type='text'>
Two independent pieces landed together this pass - both real, both
scoped, see docs/TODO.md for the full narrative on each:

Fake monitors: a genuinely independent, additional wl_output with no DRM
connector/CRTC behind it at all - distinct from srd.monitor.split
(divides one real output's own placement rectangle). Researched niri's
own Headless backend first (cloned at ~/reference-wms/niri): its render()
never actually composites anything, a no-render stub for that project's
test suite only. This one is real: it renders whatever is placed on it,
on demand, whenever a zwlr_screencopy_manager_v1 client asks for a frame.
New crates/wayland/src/udev/virtual_heads.rs (create/remove a real Output
+ global, render-on-demand for screencopy, integrated into platform.rs's
monitors() as a genuine srdwm_core::Monitor so core placement/workspace
code needs zero special-casing). New IPC/CLI: srd dispatch create
fake-monitor &lt;name&gt; &lt;w&gt;x&lt;h&gt; / remove fake-monitor &lt;name&gt;. Core-side
request queue in crates/core/src/manager/fake_monitor.rs.

Placement bug, root-caused and fixed: every new window opened alone
landed in the exact same spot, "not at all like Windows" (reported live).
SmartPlacement::place tried a grid cell first, and grid's own cell count
is existing.len() + 1 - with nothing else open (opening one app at a
time, the ordinary case), that's always 1, so a 1x1 grid returns the same
single cell forever regardless of session history. Cascade had the same
bug in a second form (its own step was existing.len() % max_steps, also
always 0 with nothing open). Fixed: WindowManager::next_cascade_step (a
Cell - add_window's own target_monitor stays borrowed across the call)
advances on every real placement and is never reset by a window closing;
place() now skips grid entirely when nothing else is open, going
straight to cascade, since grid's real job (dividing space among
concurrent windows) has nothing to divide when there's no concurrency.

Full workspace build/test/clippy clean (223 core / 141 wayland / 29
platform / 24 ctl / 28 config / 10 x11 tests, 0 failed, 0 clippy
warnings), built and installed.
</content>
</entry>
<entry>
<title>Wayland backend: desktop icons v2, menu rebuild, layer-shell/scale fixes,</title>
<updated>2025-09-10T09:24:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-09-10T09:24:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=5ee627928d0cb017e59d796c71793c35d5b65d33'/>
<id>urn:sha1:5ee627928d0cb017e59d796c71793c35d5b65d33</id>
<content type='text'>
XWayland stability, GPU rendering, and multi-cursor Phase 2

The bulk of a multi-session shift's real work landed in crates/wayland.
Full root-cause/verification narrative for every item below lives in
docs/TODO.md (each has its own dated entry); this is the summary:

Desktop shell:
- Real desktop icons v2 (state/desktop_icons.rs, desktop_icons.rs):
  fixed origin-baked-before-the-bar-connects, fixed-icon sort order, and
  a proper Rename/Delete-to-Trash menu (window_memory.rs backs the
  rename-persistence side). Rubber-band marquee multi-select.
- icon_theme.rs: real freedesktop icon-theme lookup (inherits chain,
  hicolor fallback) rendering actual theme SVGs via resvg/tiny-skia,
  replacing the hand-drawn placeholder glyphs.
- Context/desktop menus (decoration.rs, desktop_menu.rs, state/menu.rs)
  rebuilt to match the project's own AGS panel styling: rounded floating
  panel, tinted-fill row highlight, real separators, a much fuller
  titlebar window-menu action set.

Layer-shell / multi-monitor:
- Layer-shell hit-testing and render positioning (input/pointer.rs,
  udev/render.rs's element placement) now correctly convert LayerMap's
  logical geometry into physical pixels on a fractionally-scaled output
  - root cause of a bottom-anchored dock being unclickable and
  unpainted while a top-anchored bar on the same output worked.
  udev/outputs.rs's relayout_outputs gained the same physical/logical
  split for cross-output positioning, now backed by a real unit test
  (next_logical_x) built from the original measured incident numbers.
- state/geometry.rs: a window's border/decoration no longer briefly
  clips when moved between differently-scaled monitors mid-drag.

XWayland / stability:
- xwayland.rs, udev/session.rs, udev/platform.rs: fixed a 100%-
  reproducible cold-start XKEYBOARD crash-loop (XWayland's own stdin
  inherited a real, already-owned VT; env passthrough and idle-callback
  spawn timing were both real, independent gaps) that had silently taken
  down all X11-app support and the global-menu registrar every session.
- state/toplevel.rs, state/lifecycle.rs: XWayland dialog detection via
  WM_TRANSIENT_FOR, not just a native xdg_toplevel parent.

Rendering:
- udev/render.rs, decoration.rs: real GPU (GBM+EGL+DrmCompositor)
  window-content and cursor rendering on the udev backend, falling back
  to the untouched Pixman path automatically on any init failure.
- decoration/tests.rs, state/mod.rs: rounded-corner/border fixes for
  interactive resize lag and cross-monitor moves.

Multi-cursor Phase 2 (virtual_pointer.rs, new; state/mod.rs, udev/
platform.rs, winit/nested_platform.rs): pins a zwlr_virtual_pointer_
unstable_v1 object to a specific window, bypassing the shared seat/
focus/pointer_pos path entirely via hand-rolled wl_pointer.enter/motion/
button/frame/leave against every WlPointer the target client has bound
(PointerHandle::client_pointers). Lets an agent operate one window while
a human uses another, genuinely simultaneously, with zero client
cooperation and no second wl_seat (confirmed a dead end: real clients
only ever bind the first seat advertised).

Full workspace build/test/clippy clean.
</content>
</entry>
<entry>
<title>Wire real GBM+EGL+DrmCompositor rendering for one head (GPU Phase 2)</title>
<updated>2025-03-25T22:26:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-03-25T22:26:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=0d3c7ab727b321a36ffea66c461221b14bbc48d8'/>
<id>urn:sha1:0d3c7ab727b321a36ffea66c461221b14bbc48d8</id>
<content type='text'>
Extends the SRDWM_GPU=1 opt-in path (Phase 1, fa0c7f1) from a
capability probe into an actual, working GPU render pipeline for
exactly one head, per the plan this was built from
the plan file.

gpu.rs: probe now goes all the way through EGLContext, GlesRenderer,
DrmDevice (real DRM device, separate duped fd from the existing legacy
Card), and DrmOutputManager construction, returning both a GpuContext
and its DrmDeviceNotifier on success. GpuContext::initialize_output
drives one crtc/mode/connector through DrmOutputManager, storing the
resulting GpuOutput for the render loop to find.

Confirmed while reading smithay's own source directly (not assumed):
DrmDevice::new's disable_connectors parameter is not an atomic-vs-
legacy switch - DrmDevice::create_internal tries atomic capability
first and falls back to a Legacy internal variant automatically,
exposed via DrmDevice::is_atomic(), logged here rather than assumed.

platform.rs: probes at startup, calls initialize_output for the first
head only (Phase 2's deliberate scope - see the plan), registers the
DrmDeviceNotifier as its own calloop event source alongside (not
replacing) the existing legacy DRM-fd registration.

render.rs: render_udev_frame's per-head loop checks, before any of the
existing Pixman-specific element-building logic runs, whether this
head's crtc matches the GPU context's initialized output; if so,
renders a plain clear color through render_frame/queue_frame and
continues to the next head, completely bypassing the Pixman path for
that head. Every other head, and this same head whenever the GPU
context or its output is absent, is entirely unaffected.

session.rs: register_gpu_drm_notifier handles DrmEvent::VBlank by
calling frame_submitted() on the matching GpuOutput (required per
queue_frame's own doc comment, or the swapchain runs out of buffers)
and logs DrmEvent::Error without treating it as fatal.

Deliberately out of scope for this phase (documented in the plan):
window content/decorations/cursor on the GPU head (clear color only),
multi-monitor GPU rendering (one head only), and VT-switch pause/
resume for the GPU head specifically (DrmOutputManager's own pause()/
activate() calls are a different API surface from the
existing manual set_crtc+DPMS reassertion, and porting that pairing
correctly needs its own isolated verification pass).

SRDWM_GPU unset (the default) is unaffected: every step above only
runs when it's set to "1", and every failure at any step falls back
to the existing, untouched Pixman path with a logged reason, same
fallback contract Phase 1 already established.
</content>
</entry>
<entry>
<title>Add opt-in GBM+EGL capability probe (Phase 1 of GPU rendering)</title>
<updated>2025-03-21T21:23:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-03-21T21:23:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=ef4bc112b8507880ddfa53ffce45505762384612'/>
<id>urn:sha1:ef4bc112b8507880ddfa53ffce45505762384612</id>
<content type='text'>
The udev backend is, by explicit design, 100% software: PixmanRenderer
compositing into legacy KMS dumb buffers. That was a deliberate choice
for portability (dumb buffers work on essentially any DRM driver,
including a VM with no GBM/3D support), not an oversight - but this
machine's real hardware (Intel UHD 620, i915) should support real
GBM+EGL rendering, and the user has asked for a genuine GPU-accelerated
path, GPU preferred with CPU fallback, built as a separate track that
doesn't risk the working software path.

Adds `udev/gpu.rs::probe`: gated behind `SRDWM_GPU=1` (unset by
default - a no-op, zero behavior change for every session that
doesn't set it), attempts GBM device creation on a duped DRM fd, EGL
display/device creation, and a software-rasterizer check, logging
exactly which step failed if any and falling back silently. Wired in
at udev backend startup, right after the DRM fd is opened.

Deliberately does not yet create an EGLContext, a GlesRenderer, or
touch scanout at all. Reading smithay's own reference compositor
(anvil/src/udev.rs) confirmed it wires GBM+EGL rendering together with
atomic-KMS scanout as one unit via DrmCompositor, not as a renderer
swapped into the existing legacy set_crtc/page_flip flip loop this
backend uses today. Adopting DrmCompositor is separate, larger-scoped
work than "swap the renderer" - it replaces the same UdevHead
mode-set/flip machinery the VT-switch fixes
(register_session_notifier's ActivateSession arm, copy_and_flip's
retry backoff) live in, and needs its own plan. This probe answers the
first question - does the hardware even support it at all - safely,
before that larger integration is scoped and attempted.

Cargo.toml: added backend_egl/backend_gbm smithay features, additive
to the existing renderer_pixman path (unchanged, still the default).
</content>
</entry>
<entry>
<title>Fix stale ghost content after window move/resize/close on real hardware</title>
<updated>2025-03-07T20:07:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-03-07T20:07:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=9667308e6def22e9b1fa352357a788749ad92c74'/>
<id>urn:sha1:9667308e6def22e9b1fa352357a788749ad92c74</id>
<content type='text'>
The udev backend's damage tracking only forced a full repaint (ages =
[0, 0]) on a workspace switch or a VT-switch resume. An ordinary
move/resize/open/close/restack within the same workspace relied
entirely on OutputDamageTracker's own per-element diffing to compute
correct damage for the region a window vacated - which doesn't always
hold: maximizing a window over a second one, then un-maximizing, left
a persistent ghost of the second window's old titlebar/status text
sitting in the vacated corner, unchanged across multiple otherwise-
idle frames.

render_udev_frame now also hashes every visible window's id and rect
each frame and forces the same full-repaint reset whenever that
signature changes - the same "defensive, not a fix for a proven bug
in the diffing itself" reasoning the existing workspace-switch reset
already uses, just triggered by a second, complementary condition.
</content>
</entry>
<entry>
<title>Fix clippy warnings surfaced by the merge</title>
<updated>2025-02-18T19:15:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-02-18T19:15:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/srdwm/commit/?id=681a023b04b513b3283d85fe157d6b38987bc42e'/>
<id>urn:sha1:681a023b04b513b3283d85fe157d6b38987bc42e</id>
<content type='text'>
Pre-existing issues in the uncommitted rust-rewrite work (unused
imports, over-arity glyph-drawing functions, a couple of complex inline
types, or_insert_with(T::default) instead of or_default(), and one
unsimplified test-only arithmetic expression), plus two imports left
unused by switching to or_default(). None of these are behavior
changes. Full workspace build + clippy -D warnings + test suite (378
tests) all green after this.
</content>
</entry>
</feed>
