srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev/render.rs
AgeCommit message (Collapse)AuthorFilesLines
2026-08-30Use as_chunks for fixed-size pixel iterationHEADmainsrdusr1-1/+1
2026-07-27Hide a window only when srdwm knows it has not drawn, not when a lookup says sosrdusr1-2/+2
Regression I introduced two commits ago, reported live: "I can click close where the button would normally be and it does close, but it is still invisible." The gate that stops an empty frame being drawn before a client paints asked the renderer, from inside the render loop, whether a window's surface had a buffer attached right now - and treated "no" as "do not draw". That question is only meaningful for a native xdg-shell toplevel. An XWayland window's surface state does not describe it the same way, so the answer came back no on every frame and the window was never drawn again, while srdwm's own hit-testing carried on working perfectly: an invisible window that still takes clicks, which is a worse failure than the empty frame it was meant to prevent. Inverted to the fail-safe direction. `new_managed_window` - the one path that creates a native toplevel - puts the window into `awaiting_first_buffer`, and `commit` takes it out on the first commit that carries a buffer. The render and capture paths test that set and nothing else. A window is now hidden only when srdwm itself put it there, so no window whose plumbing works differently can be hidden by a lookup that did not apply to it: the XWayland map path never touches the set, and neither can anything else. The buffer question still gets asked, but only in `commit`, about a surface it was just handed, where it is the right question. Verified both halves: an ordinary spawn still shows no frame before content (26 captured frames with content, 0 without), and the only way into the set is one line in one function.
2026-07-26Draw a window's content from the same rect as the frame around itsrdusr1-2/+24
Reported live: "resizing shrinks/grows only the right side", and "the titlebar seems separate when resizing, it doesn't size at the same time". Both are one bug. Every decoration - border strips, titlebar, shadow -- is drawn from `frame`: the client's last committed size, anchored to whichever edge the drag is not holding. The content was drawn from `geom`: this compositor's live drag target, which moves on the same frame the pointer does. A client is always at least one commit behind a drag, so for that whole interval the two rects disagree, and the window is drawn as two pieces that move independently - the stale buffer sliding left with the pointer, carrying its old width, while the border it belongs in stays where the committed size puts it. Dragging a left edge therefore looked like it moved the right one. All three content paths now position from `frame` (both udev render loops and the winit one). Outside a resize this changes nothing at all: `committed_frame` only ever corrects the far edge, so `frame.x`/`frame.y` and `geom.x`/`geom.y` are the same value. Measured in a nested compositor, driving a real left-edge drag with the virtual-pointer tool and sampling both the model's target rect and the rendered pixels at the same moments: the content sits exactly one border width inside the border on both sides in every frame, the right edge holds at 830 throughout, and the left edge tracks the pointer (305, then 405). The earlier decorated-window run measured the same thing for the titlebar: its left edge moved with the window, its right edge did not move at all.
2026-07-23Do not draw a window before its client has painted anythingsrdusr1-0/+13
Reported as "before a window spawns, the border corners look funny". A toplevel is placed, sized and decorated the moment its role is created, which is well before the client draws. srdwm was rendering it from that moment, so what appeared first was an empty frame: border, titlebar and shadow standing around bare desktop, at the guessed 800x600 placeholder size, with nothing inside. When the real buffer arrived the frame snapped to the real size. Measured in a nested session, capturing a cold terminal's spawn with grim: four consecutive captured frames spanning 540ms showed a complete red border with zero client content inside it, at 642px outer height, which then settled at 610 - a jump of exactly one TITLEBAR_HEIGHT. After this change the same capture has no such frame at all: every frame that shows a border shows content in it, and the height does not change afterward. Two parts: - Nothing is drawn for a window that has never committed a buffer. All five paths that draw a frame agree on this - both udev render loops (Pixman and GPU), the winit render loop, and both screencopy paths, so a screenshot cannot show a frame the screen does not. - The open-slide starts at the first commit that carries a buffer rather than at role creation. A cold terminal took ~800ms to paint, long enough for the whole tween to finish against the empty frame, so the window simply appeared, already at rest, with no animation at all. It now animates where it can actually be seen. The answer latches once true (windows_shown_once), so a window that has legitimately shown something is never hidden again by this however its buffer state changes. A window that cannot be resolved to a surface counts as drawable, deliberately: this hides a window only on positive evidence that it has never drawn, so nothing whose surface plumbing works differently - an XWayland window - can be hidden by a lookup that did not apply to it. Same shape, and the same reason, as sync_layer_visibility's own has_buffer branch, which layer surfaces have had all along. 533 tests pass, clippy clean.
2026-05-15Fix the maximize border on the path that actually runs, and three spawn faultssrdusr1-2/+9
The maximize border was still drawn because the earlier fix landed on the wrong branch. udev/render.rs has three border blocks: the SRDWM_GPU=1 path at the top and two Pixman ones below. The patch replaced the first match in the file, which is the GPU branch a real DRM session never runs. All four sites across both backends are now gated on !maximized. The verification had failed twice for a separate reason: winit/capture.rs did not draw border strips at all, so a screenshot could never answer "is there a border here" and the control passed for the wrong reason. Border strips are now drawn into that pass as solid fills - corner rounding is not reproduced, so a capture is not pixel-exact at the corners, but presence, position, thickness and colour are. With that closed the test has a real control: unmaximized gives 6 accent pixels at x=800..805, exactly the configured border_width, and maximized gives none at the right edge or along the top row. That proves the winit path; the Pixman path is the same change at two more sites and is not separately confirmed on screen. Windows spawning as squares, partly off-screen, and always on the left were all SmartPlacement::grid. It returned size.min(cell), shrinking every window to its grid cell whatever size it asked for; it scanned cells in reading order and took the first free one, which is the leftmost; and nothing clamped the result, so a window larger than its cell could hang off the edge with its border out of view. The cell now decides only where a window goes, the scan starts from a rotating cell, and both grid and cascade clamp into the usable area. Four tests, one per reported symptom. 525 tests pass, clippy clean.
2026-05-11Fix spawn placement under the top bar, add per-window minimum sizes, and ↵srdusr1-1/+10
clean up maximize Four reports after restarting into today's build, with a screenshot. The screenshot was measured, not eyeballed: top bar y=0..29, a 4px accent border at y=30..33, titlebar from y=34, bottom border at y=1027..1030, and 49px of bare desktop below it. Windows spawning too close to the top bar. A remembered position was validated only by asking whether it landed on some monitor's full_geometry, which includes the strip a top bar reserves, so an app whose remembered y was small reopened with its titlebar under the bar. That is why it was "sometimes": it depended on the stored value, and the live store holds wezterm at y=44 and firefox at y=69 against a 30px bar. Remembered positions are now clamped into the monitor's usable area. Placement not surviving a logout. Window memory does persist, but five of the eleven entries in the live store were saved with a second monitor attached, at x >= 2000. Those points match no current monitor and were discarded outright, falling back to a fresh cascade, so those apps appeared to remember nothing. Such a position is now clamped onto a monitor that exists instead. Per-window minimum sizes. One global floor is wrong in both directions. Three sources now, in increasing precedence: the global floor, the client's own declared minimum (xdg_toplevel.set_min_size or ICCCM hints), and a min_width/min_height window rule overriding both. A rule wins permanently -- the backend refreshes the client's declared minimum on every decoration redraw and must not undo a deliberate override. Maximize, three faults in one report. A maximized window now draws no border: its edges are the screen's edges, and the only place maximize stops short is the bar strip, which is exactly where the measured line was. maximize_geometry_for no longer subtracts a bottom-anchored dock's exclusive zone, so maximize runs to the bottom of the screen and the dock floats over it; top, left and right are still honoured. general.maximize_covers_dock = false restores the old behaviour. With the border gone the window sits flush under the bar instead of with an accent line crowding it. Verified: seven new tests on the real numbers from the live store, and maximize geometry measured live in a nested instance (a window maximized on a split half reports exactly that half's rect). NOT confirmed on screen: the border removal and the dock behaviour - the nested backend has no bar or dock to reserve a zone, and an attempt to check the border produced a failing control, since srd set border_width only affects windows created after it. 515 tests pass, clippy clean.
2026-05-11Three live bug reports after a restart: icon drag, lock cursor, lock boxsrdusr1-2/+24
All three reported directly after the owner restarted into today's build. Desktop icons could not be dragged at all in single-click mode. The press handler opened the icon immediately when general.desktop_icon_single_click was on, so the branch that starts a drag was unreachable and an icon could never be moved. Deciding activation on press cannot distinguish a click from the first instant of a drag. Every press on an icon now starts a potential drag and release decides which it was, using a 4px movement threshold that latches once exceeded. Double-click mode goes through the same path, so both modes now drag identically. The lock screen drew no cursor. The cursor push in the udev render loop sits inside `if !locked`, and a locked head renders only the lock element list, so nothing drew a pointer - and on a bare TTY nothing else does. The on-screen keyboard's clicks were being handled correctly the whole time (native_lock_click); they simply could not be aimed. The pointer is now prepended to the lock element list, above the UI it is used to click. The password field's opaque panel is gone. New LockConfig::box_opacity, default 0.0: no fill, no border, no rounded rectangle, just the dots and status text over the blurred background. Raising it restores the panel at that opacity for anyone who wants a solid field. Drawing text on a transparent surface needed a new blit_glyph_over: the existing blit_glyph blends against one flat opaque colour and writes alpha 255, which would have turned every glyph into a block of the assumed background - the same box with its middle removed. VERIFICATION STATUS, stated plainly: all three are code-complete and the suite passes, but none is confirmed on screen. The nested backend's capture pass does not draw the desktop icon grid (a gap already recorded in winit/capture.rs), so the icon drag cannot be checked by screenshot there, and aiming blind is what this project's own rules forbid. The two lock changes were not visually checked either. 515 tests pass, clippy clean.
2026-04-27Build the eight asks recovered from the previous session's transcriptsrdusr1-0/+15
The punch list was not the whole record. These are the owner's own typed requests, read back out of the previous session's transcript rather than guessed at, then each checked against the code before being treated as open. Three they suspected were already done really were: dialogs already had a Close-only titlebar, inactive dimming already existed, and the corner resize hitbox had already been tuned. Snap layouts on drag, asked for twice. Edge snapping worked but committed silently on release with nothing shown first, so there was no way to know it would happen or where. A translucent drop-target preview now follows the drag, and throwing the pointer at a monitor's top edge drops down the existing six-cell grid to aim at. The preview calls the same snap_zone that end_drag does, so the two cannot disagree. Two defects found by screenshot before landing: moving down onto the flyout closed it, and its labels overflowed at a fixed cell width - the same "text goes out of view" fault already fixed once for the context menu. New File now offers real types, chosen by extension, with the de-duplication counter placed before the extension so the file stays what it says it is. Refresh re-reads init.lua and fires a new srd.on("refresh") handler instead of only re-scanning the icon grid. What refresh means beyond srdwm's own config stays the config's decision. general.config_reload_on_write (default on) applies an edited config on save, via an mtime sweep rather than an inotify watch: no new dependency, same behaviour on every target, and unaffected by editors that write through a temp file. A real bug behind "what happens when our config fails": you lost every keybinding. do_reload cleared the binding, handler and repeat tables before re-executing and never restored them, so a syntax error left neither the old config nor the new one, and the only key still working was the reload combo nobody thinks to press. The tables are now restored on any failure and config errors reach notify-send, not just the log. srd.lock() and a default Mod4+Ctrl+l binding: the built-in lock screen could not be reached from Lua at all. Native rather than shelling out, because a lock key that shells out fails silently when the binary is not on PATH. Default bindings added for srd.window.move and a dynamic/tiling toggle, both of which existed with no way to reach them, plus srd.layout.get() so the toggle reads the live workspace rather than the configured default. Dialogs open centred, and are excluded from remembered geometry in both directions - that table is keyed by app_id, which a dialog shares with the window that spawned it, so dialogs inherited an unrelated position and size and then overwrote it with their own. theme.decorations.title_bar.button_mode (dynamic by default, or fixed) drops the Maximize button on a window whose client pinned min == max size, where pressing it can do nothing. Maximize is removed from the slot list rather than skipped in place on both the render and hit-test sides, so the remaining buttons close the gap identically; three tests pin that agreement, which is what fails silently when it drifts. Also: the nested backend's screencopy pass now draws both menus, the flyout and the drag preview. Four investigations in one day started from a screenshot missing a tier, so that pass carries an explicit list of what it still omits and the on-screen loop points at it. 512 tests pass, clippy clean.
2026-04-20Confirm Nemo's popup works; fix the two bugs that hid it, and shadow bleed ↵srdusr1-2/+13
across a monitor seam 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.
2025-11-20Redesign the native lock screen: clock/avatar header, on-screen keyboard, ↵srdusr1-1/+13
wrong-password shake The native lock UI was a flat bordered rectangle with three left-aligned text lines and no shadow, clock, or identity marker - reported directly as looking unfinished. Splits the redesign across a new transparent-canvas header (time, date, circular avatar, username) above a redesigned, centered password box with a real drop shadow and a dimmed placeholder prompt, plus a genuine on-screen QWERTY-shaped keyboard with working Shift/Backspace/Return/Space and real click hit-testing shared with the render path via one `lock_stack_layout` function, and a damped-sine shake on a failed attempt. LockConfig gains show_clock/show_keyboard/avatar_bg, each independently srd.set-able and documented in a new theme.lock.* section in DEFAULTS.md. native_lock_render_elements now takes one NativeLockFrame struct instead of positional buffer arguments now that it composites five optional layers instead of two. Full workspace build/test/clippy clean (152 wayland tests, +6 new).
2025-11-20Add border/titlebar decoration rendering to the GPU render pathsrdusr1-0/+58
The GPU (SRDWM_GPU=1/general.gpu) path had real window content but square corners and no border/titlebar. A prior pass investigated a full port of the Pixman path's decoration rendering and deliberately did not attempt it blind, given no working GPU-capable hardware on this machine to verify a single pixel of it against. Asked directly, twice, to build it anyway rather than leave it. Scoped smaller than a full port: border top/bottom strips and the titlebar bitmap now render, reusing the exact cached MemoryRenderBuffers the Pixman path already builds (renderer-agnostic pixel buffers, imported for GlesRenderer the same generic way cursor::render_elements already does for either renderer). Left out on purpose: occlusion- fragment clipping against overlapping windows, and the left/right border side strips plus the drop shadow. Full workspace build/test/clippy clean. Explicitly not visually verified - same reason as before, no GPU-capable hardware on this machine.
2025-10-07Fix intermittent cursor ghosting when crossing between monitorssrdusr1-0/+18
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).
2025-10-03Make the secondary-cursor sprite opt-in and expire stale entriessrdusr1-6/+21
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 <bool>`) 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.
2025-09-10Wayland backend: desktop icons v2, menu rebuild, layer-shell/scale fixes,srdusr1-1/+69
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.
2025-08-17Fix desktop icons v1 regressions: bar overlap, wrong order; add proper menussrdusr1-2/+2
Live testing found v1 genuinely broken, not just rough: 1. Icons weren't rendering reliably at all - ensure_desktop_icons only ever computed the grid's origin once, on whichever render pass happened to be first. AGS's own top bar registers its exclusive zone after that first pass, so origin got permanently baked in at the pre-bar geometry. Confirmed live via a temporary diagnostic log. Fixed by re-deriving origin from the primary monitor's current geometry on every call instead of just the first. 2. Fixed icons (Home/Computer/Trash) always sorted before real files -- confirmed wrong via direct question. The whole list now sorts alphabetically by label, case-insensitive, fixed icons included. 3. "Set as Wallpaper" was the wrong feature: removed entirely (DesktopMenuAction::SetWallpaper, general.wallpaper_command, is_image_path). The user wants that handled by their real file manager once opened, not reimplemented here. Also adds real menu functionality per "where are all the options": Rename (inline text edit, new CompState::renaming_icon field and keyboard redirect mirroring NativeLock::password's existing precedent), Delete (moves to ~/.local/share/Trash per the freedesktop.org spec, new trash.rs module, same-filesystem case, no confirmation - this is the reversible move-to-trash, not a permanent delete), Empty Trash on the Trash icon, and Open Terminal Here / Open in File Manager on the bare-desktop menu (new general.terminal config key). 133 wayland-crate tests (up from 106), full workspace build and clippy clean.
2025-08-15Add real desktop icons plus right-click desktop/icon context menussrdusr1-0/+28
Closes "right-click on bare desktop" - previously a true no-op, nothing rendered above the wallpaper at all. Requested directly: a real desktop "just like windows does" - Home/Computer/Trash plus one icon per real ~/Desktop entry, individually draggable with persisted grid positions, double-click to open, right-click menus (per-icon "Open" plus "Set as Wallpaper" for image files when general.wallpaper_command is set; bare desktop "New Folder"/"Refresh"). Architecture mirrors the existing context_menu.rs/snap_flyout.rs "compositor-owned floating UI" pattern: desktop_icons.rs (data model, grid layout, filesystem scan, hit-test), desktop_icons_state.rs (JSON persistence, same shape as monitor_layout.rs), desktop_menu.rs (the new right-click menu, reusing decoration::render_context_menu's existing rasterizer), state/desktop_icons.rs (CompState glue: rescan/select/drag/ open/persist), and a new decoration::render_desktop_icon rasterizer -- hand-drawn glyphs, since no PNG/SVG decoding capability exists anywhere in this workspace. Wired into both render loops (udev and winit) above the wallpaper and below every window, and into input/pointer.rs's button/ motion handlers for selection, drag, double-click, and both menus. Four new config keys: general.desktop_icons (default true - a directly requested, purely visual feature, unlike the opt-in-while-experimental general.gpu), general.file_manager, general.desktop_icon_single_click, general.wallpaper_command (all default off/empty). Deliberately out of scope for this pass, stated up front: move-to-trash and "Empty Trash" (destructive, no confirmation-dialog primitive to gate them on yet), filesystem watching, multi-select, per-mimetype icon art, icons on any monitor but the primary one. 124 wayland-crate tests (up from 106), full workspace build and clippy clean.
2025-08-09Fix interactive-resize border/shadow lag without the OOB risk that sank the ↵srdusr1-3/+35
first attempt effective_frame_of now returns the live drag target while a window is being interactively resized (same change as the reverted first attempt), but two things make it safe this time instead of reintroducing the out-of-bounds texture sample that reversion was for: - Every src crop rect built from a window's frame width in udev/render.rs and winit/render.rs (titlebar, top border strip, bottom border strip) is now clamped against DecorationSignature's own recorded width/ border_width - the bitmap's actual last-built size - before reaching MemoryRenderBufferRenderElement::from_buffer, which does not itself validate src against the real texture size. This is a structural floor independent of timing, not a repeat of the previous unsafe approach. - handle_pointer_position now calls redraw_decoration_buffer once per resize motion event (throttled to 60Hz via a new CompState::resize_redraw_at), closing the lag at its source instead of only catching up on the next real client commit. This also fixes the shadow bitmap's identical commit-vs-live-position gap for free, since redraw_decoration_buffer rebuilds all three bitmaps together. Updates the TODO.md entry for this bug with the full before/after.
2025-08-08Render real window content on the GPU render pathsrdusr1-15/+30
Past clear-color + cursor: a GPU-driven head now renders every visible window's real content too, via surface_content_elements (the same generic-over-renderer helper the Pixman path uses, unmodified against gpu.renderer instead of udev.renderer). Content pushed after the cursor (so the cursor stays on top), in the same front-to-back `ids` order the Pixman path's own custom_elements already relies on for correct occlusion between windows - plain painter's-algorithm draw order, no separate clip needed since content is window-shaped. Deliberately the *unrounded* path: no corner masking (that's built against PixmanRenderer specifically on this backend) and no decorations (border, titlebar) - a GPU-driven head now shows real window content, square corners, no chrome. Decorations are the remaining real gap before this path has parity with the software one. Per-window geometry/position math (geom from window_anims or w.geometry, band for a decorated window's titlebar reservation, content_offset clamped non-negative) mirrors the Pixman path's own content push exactly, including an earlier double- subtraction and negative-margin fixes - so a CSD client with a real shadow margin positions the same way on either render path. Untested on real GPU-enabled hardware as of this writing: builds, passes clippy, full test suite green, and matches the existing Pixman path's geometry logic by inspection, but SRDWM_GPU/general.gpu were both unset on the machine this was built on - noted honestly in gpu.rs's own module doc comment, DEFAULTS.md, and IMPLEMENTATION_STATUS.md.
2025-05-26Render the real cursor on the GPU render path, not just a clear colorsrdusr1-11/+24
Past clear-color-only (Phase 2): the GPU-driven head now shows the same moving cursor the Pixman path renders, via cursor::render_elements - already generic over the renderer (R: Renderer + ImportAll + ImportMem), so it works unmodified against gpu.renderer (GlesRenderer) instead of udev.renderer (PixmanRenderer). GpuElement (gpu.rs) widened from a bare MemoryRenderBufferRenderElement to crate::elements::OverlayElement<GlesRenderer> - the same Surface/Memory/Solid enum the Pixman path's own custom_elements already uses, needed once the element list stopped always being empty. Window content and decorations remain a real gap - a GPU-driven head still shows no windows, just its own clear color and cursor. Untested on real GPU-enabled hardware this work (SRDWM_GPU unset on this machine's live session); builds, passes clippy, full test suite green.
2025-05-15Extend GPU rendering to every head, not just the firstsrdusr1-22/+28
Phase 2 of the GPU-rendering plan deliberately targeted a single head (GpuContext::output: Option<(crtc::Handle, GpuOutput)>) as a narrow proof that GBM+EGL+DrmCompositor rendering works at all on this hardware. DrmOutputManager already supports driving several crtcs at once - initialize_output is a per-crtc call on one shared manager, the same way anvil drives multiple outputs - so this was purely an unexercised restriction, not an architectural limit. GpuContext::output is now GpuContext::outputs: Vec<(crtc::Handle, GpuOutput)>, and udev/platform.rs calls initialize_output for every connected head in its own bring-up loop instead of only the first after that loop finishes. A head this fails for individually (already logged, not fatal) still just has no entry and falls back to the existing legacy Pixman path, unchanged from before. session.rs's VBlank handler and its VT-switch resume path (which excludes GPU-driven crtcs from the legacy set_crtc reassert loop, a different device fd that must never issue mode-set commands against a crtc DrmOutputManager already owns) both now look a crtc up in the Vec instead of comparing against a single stored one. render.rs's own render-loop lookup uses direct field access (gpu.outputs.iter_mut().find(...)) rather than an equivalent &mut self method: the borrow checker treats a method call as borrowing all of GpuContext, including gpu.renderer needed a few lines later for the same head, where direct field access lets it see the two borrows are disjoint. Still gated behind SRDWM_GPU=1 (unset by default) and untested on real multi-monitor hardware with the flag on - this machine has one display, so the actual multi-head path itself only gets exercised whenever it's set on hardware that has more than one.
2025-04-29Fix border/content misalignment from a stale and double-applied CSD marginsrdusr1-4/+63
Two bugs in how a CSD client's own declared shadow-margin offset (dwindow.geometry().loc) drives where its content actually renders, both surfaced by a real Firefox window: 1. A live Firefox window was observed reporting loc = (-10, -10) -- negative, despite sync_geometry's tiled-state hint telling it to reserve no margin at all. effective_frame_of already clamps this value to non-negative for its own size calc; the content-position code (both the masked-content-buffer test and the real content push) didn't, so a negative margin shifted content the wrong way -- away from the border, not toward it. Clamped to match. 2. Separately, and the actual cause of a later "border isn't over the window" report on the same Firefox window (this time reporting loc = (10, 10)): the masked/rounded content buffer's own build step already renders the client's surface tree shifted by -content_offset so the buffer's own (0, 0) lands exactly on the real, margin- excluded content top-left (see rounded_content_buffer's own loc parameter). Placing that already-compensated buffer on screen at a *second* content_offset-shifted position double-applied the correction, landing it content_offset pixels too far up and left of the border wrapping it. Confirmed live via pixel sampling: Firefox's own chrome rendered starting 10px above the border's nominal top edge, fully exposed, square, with no border over it at all. Split the single `pos` into `content_pos` (unshifted - what the already-compensated masked buffer uses) and `pos` (content_pos further shifted by content_offset - kept only for the surface_content_elements fallback, which renders the client's raw surface tree with no prior compensation of its own and still needs the shift applied once). The winit/GLES backend's equivalent path doesn't have this bug: its rounded_content_element renders the client's live texture directly at `location` with no separate pre-shifted buffer, so a single content_offset-adjusted position there was already correct.
2025-03-26Wire real GBM+EGL+DrmCompositor rendering for one head (GPU Phase 2)srdusr1-0/+29
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.
2025-03-07Fix stale ghost content after window move/resize/close on real hardwaresrdusr1-1/+43
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.
2025-02-21Remove resolved corner-rounding diagnosticssrdusr1-29/+0
The top-rounds-but-bottom-doesn't investigation these were tracking is closed: live-verified via a real screenshot (pixel-level, not eyeballed) that both corners round correctly on both a decorated window and an undecorated/CSD one relying on content masking. Removes three log::debug! blocks (corner-mask state, TOP/BOTTOM border strip position dumps, and a raw alpha-byte dump of the bottom border buffer) that were firing on every single render pass regardless of whether anything changed, adding real per-frame overhead for output no longer needed. Build + clippy + test (378 passing) all still green.
2025-02-18Fix clippy warnings surfaced by the mergesrdusr1-1/+2
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.
2025-02-17Merge branch 'main' into rust-rewritesrdusr1-12/+46
# Conflicts: # crates/config/src/engine/general.rs # crates/wayland/src/udev/drm.rs # crates/wayland/src/udev/mod.rs # crates/wayland/src/udev/render.rs # crates/wayland/src/winit/render.rs
2025-02-17Checkpoint: today's fixes before reconciling with the rust-rewrite worktreesrdusr1-12/+46
Fixes a live-reproduced VT-switch busy loop (failed page_flip retried with no backoff), shadow rendering bleeding onto occluding windows unclipped, and a winit-backend buffer-age correctness bug that left stale cross-window pixels on screen. Committing before merging in the much larger uncommitted rust-rewrite worktree, which independently touches several of the same files - this is the pre-merge baseline to diff against, not a claim that these are the final versions of these fixes.
2025-02-15Checkpoint: preserve all uncommitted rust-rewrite worktree worksrdusr1-75/+409
Safety commit before reconciling this worktree with main, which has diverged with its own separate fixes today. Nothing here is reviewed or curated yet - this exists purely so none of this work can be lost to a git operation, disk issue, or worktree cleanup while that reconciliation happens.
2025-02-04Fix CSD windows rendering with a wallpaper-visible gap at their cornersrdusr1-1/+24
srdwm never read xdg_surface.set_window_geometry anywhere. A CSD client (GTK4/Firefox) declares its real visible content as a sub-rect inset within a larger buffer that also reserves an invisible shadow margin - that margin stays reserved in the buffer even once the tiled-state hint tells the client to stop drawing the shadow itself. Every render path was positioning content at the client's raw buffer origin instead of subtracting that declared offset, leaving the margin's width/height as a gap with wallpaper visible through it at the window's top-left corner. Confirmed by pixel-diffing the gap against the real wallpaper at that exact screen position: an exact match, ruling out "just the client's own dark theme." Fixed in three places that have to move together: udev/render.rs and winit/render.rs's content-positioning code, and sync_geometry's space.map_element call. That last one matters as much as the other two - it's what smithay's Space (and therefore click hit-testing) reads, so a render-only fix would have traded a visible gap for an invisible, same-size hit-test offset in the other direction. Also applied to the new capture-workspace off-screen render for the same reason. This likely also explains real dropdown/context-menu misplacement (and the clicks landing on the wrong spot) in CSD apps: popup positioning anchors against the same window position this fix corrects.
2024-12-24Accumulate wayland-crate additions: capture rendering, focus/Space sync, VT ↵srdusr1-6/+78
resume, plumbing Bundles the remaining wayland-crate changes built up here, touching both backends (udev and winit) and the shared input/rendering code: - udev/capture.rs: off-screen Pixman render of an arbitrary (not necessarily on-screen) workspace's window content to a PPM file -- what crates/core's capture-request queue drives, for a workspace switcher's thumbnail previews. wlr-screencopy structurally can't do this (it can only see what an output is presenting), which is why this exists as a separate render path rather than reusing it. - input::focus_window now also raises the window in smithay's own Space, not just core's stacking order - Space is what actually renders on top and what pointer hit-testing reads, so any focus path that skipped this (an IPC "focus" dispatch, concretely) left a window genuinely focused while still rendering, and receiving clicks, underneath whatever was already topmost. Both backends' poll loops now re-sync this after any IPC mutation. - udev/session.rs's VT-switch resume fix (drains a stale pending page flip before reasserting CRTCs) already has its own earlier, cleanly isolated commit - not duplicated here. - Assorted decoration/cursor/rounded-corners/output-management/ screencopy/XWayland changes and their cross-backend wiring. Coarser than the repo's usual one-purpose-per-commit convention, deliberately - see the core-crate sweep commit's own message for why.
2024-08-23Round the bottom border strip's corners to match the topsrdusr1-8/+21
MISSING.md listed the border frame's bottom/left/right strips as staying square while the top one (and the titlebar above it) rounds -- "unrelated to client content rounding... not attempted." The left/ right strips genuinely can't participate (border_strips' geometry has them span only the height between the top and bottom strips, no corner to round), but the bottom strip is exactly the same shape as the top one and had no reason left to stay square. decoration::render_border_bottom mirrors render_border_top exactly (round_bottom_corners mirrors round_top_corners), cached the same way in a new border_bottom_decorations map, and drawn in both render loops via the same all-or-nothing occlusion check the top strip already uses - pulled out of the left/right strips' per-fragment occlusion splitting into its own dedicated bitmap path, matching top's existing trade-off (cropping a rounded bitmap's source rect per fragment is real extra work for a strip this thin) rather than inventing a new one. One real bug caught before it shipped: round_bottom_corners' corner- centre math (height - r - 1) panics on unsigned underflow whenever the radius clamp lands on the strip's own full height (a real, common case - a 2px-thick test strip hits it immediately). Fixed by computing the centre as a signed offset instead, mirroring how the existing dx/dy distance math already avoids the same class of issue.
2024-08-03Fix doc-comment file references left stale by today's six module splitssrdusr1-2/+2
~17 comments across the codebase still pointed at udev.rs/winit.rs/ state.rs by their old flat-file names after those became udev/, winit/, state/ directories - found while auditing what this work rushed, since the split verification (function/struct-name diffing, full test suite) checked structural correctness but never comment accuracy. Updated each to either the specific new file (e.g. "see state.rs's REPEAT_DELAY" -> "see state/mod.rs's REPEAT_DELAY", "udev.rs's monitors()" -> "udev/platform.rs's monitors()") or the bare module name where the reference was already generic ("the udev/winit backends", not a specific location).
2024-07-16Split crates/wayland/src/udev.rs (1643 lines) into udev/srdusr1-0/+600
Pure reorganization, no behavior change - verified by diffing the function-name and struct-name sets before/after (both identical) plus a full cargo test pass. Struct definitions (Card, DrmBuffer, UdevHead, UdevState) and their small impls stay in mod.rs, since default (crate- scoped) privacy there is visible to every descendant submodule without further changes. The rest splits by concern: - render.rs: the per-frame impl CompState block (render_udev_frame and the gamma/output-power methods) - still one ~520-line function, left intact rather than decomposed, given how much of its structure (the self.udev.as_mut() disjoint-borrow pattern threaded through it) is deliberate and already documented inline. - outputs.rs: hotplug reprobe/relayout (impl CompState). - platform.rs: UdevPlatform's struct/connect logic and its `impl Platform for UdevPlatform`, previously split apart in the flat file by ~250 lines of unrelated DRM/session code sitting between them. - drm.rs: mode/CRTC/framebuffer probing and setup. - session.rs: libseat/libinput/udev-monitor calloop registration and the libinput event handler. A few free functions and one struct (ConnectorProbe, bring_up_head, probe_connected, pick_crtc, the register_* functions) went from module-private to pub(crate): called across what are now sibling submodules, which - unlike a defining module's own descendants -- Rust's privacy model doesn't let see each other's private items. Matches this crate's existing pub(crate) convention rather than introducing pub(super), which crates/core's manager/ split used instead to match *that* crate's plain-private convention.