| Age | Commit message (Collapse) | Author | Files | Lines |
|
carve_inner_corner_pixel (an earlier ring fix) cut the
border strip's own disk using an inner circle at the *same* centre as
the outer cut, radius - border_width - correct when a titlebar sits
underneath (it shares that exact circle, by construction), but wrong
whenever client content sits underneath instead: content's own
rounded-corner mask (rounded_corners_pixman.rs's apply_corner_mask) is
centred radius from *its own* buffer's edges, and that buffer starts
border_width rows/columns inside the border strip's - a same-radius
circle offset (border_width, border_width) diagonally from the
border's own outer one, not a smaller concentric one. Reusing the
titlebar-style ring left a real, if very small, gap along part of the
seam and a thin sliver of double coverage along the rest - reported
live, at extreme zoom against a solid-colour wallpaper (chosen
specifically to make a sub-pixel gap easy to spot against, unlike the
usual desktop image): "very tiny gaps... corner radius does not
match."
round_top_corners/round_bottom_corners's own `inner_radius: Option<u32>`
parameter is now `inner: Option<InnerRing>`, an explicit (center_row,
center_col, radius) rather than an implicit "same centre, smaller
radius" - InnerRing's own doc comment has the full geometry for both
cases. render_border_top gained a `decorated` parameter to pick the
right one (titlebar-aligned when true, content-aligned - centre
shifted by border_width on both axes, radius unchanged - when false);
render_border_bottom always uses the content-aligned ring, since this
compositor never draws a bottom titlebar.
apply_corner_mask (rounded_corners_pixman.rs) made pub(crate) so the
new regression test can build a real masked content buffer via the
actual production function, not a reimplementation of its math.
border_top_and_content_mask_have_no_gap_along_the_corner_diagonal_when_undecorated
checks coverage along the diagonal ray between the two circles'
centres - the direction they're actually offset along, and so the
worst case for a gap opening up between them - against the real
render_border_top/apply_corner_mask output, not a hand-rederived
formula.
|
|
Same fix as the udev backend's matching content-position code: a real
CSD shadow margin (dwindow.geometry().loc) is never negative, but a
live Firefox window was observed reporting loc = (-10, -10) despite
sync_geometry's tiled-state hint telling it to reserve no margin at
all. This path (rounded_content_element, a live GLES shader rendering
the client's own texture directly, not a separate pre-shifted buffer)
doesn't have the udev backend's double-application bug, but it shares
the same single-subtraction call and so needed the same clamp.
|
|
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.
|
|
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.
|
|
round_top_corners/round_bottom_corners only ever cut pixels *outside*
the shared corner radius (the rounded outer silhouette). Nothing cut
anything *inside* it, so a border strip's own "extra" rows (present
whenever corner_radius > border_width) stayed a solid filled disk out
to the centre column/row, then hit clip_middle_beyond_thickness's
hard, unblended rectangular cut right at the disk's own most opaque
point - a clean right-angle step, not a curve. Confirmed live,
zoomed: a real square notch bitten into an otherwise smooth arc,
reported as "squares on the inside corners of each vertex."
Added carve_inner_corner_pixel, the same smoothstep falloff as the
existing outer cut but inverted (cuts near the centre instead of far
from it), applied at radius - border_width so the corner becomes a
genuine ring of ~border_width visible thickness tapering smoothly to
transparent, instead of a filled wedge. Only render_border_top/
render_border_bottom pass an inner_radius; the titlebar's own corner
and the lock-screen box keep their existing solid-disk behaviour,
which is correct for a single flat-coloured panel with nothing of a
different colour underneath needing to show through.
Also generalizes round_top_corners with an explicit center_col
parameter, mirroring the existing center_row shift: the titlebar's own
corner circle was never shifted horizontally to match the border
strip's (only vertically), leaving a border_width-wide sliver of the
titlebar's own misaligned curve poking through at the seam.
border_top_and_titlebar_corners_meet_without_a_seam and
border_top_curve_actually_closes_within_the_side_strips_own_width
updated to match: both now compare the correct corresponding columns
(the titlebar's own buffer starts border_width columns inside the
border strip's), and the latter no longer demands exact 255 opacity at
a point that legitimately sits within the new inner cut's own
antialiasing band.
|
|
Phase 2 (0274273) deliberately shipped without VT-switch support for
the GPU-driven head, documented as an explicit gap rather than a
silent risk. Before testing it live, wire the real fix instead of
finding out empirically - this already burned three real
reboots getting the *legacy* VT-switch path right, and DrmOutputManager
uses a genuinely different API surface (pause()/activate(), calling
through to DrmDevice's own master-lock acquire/release) than the
manual set_crtc+DPMS reassertion register_session_notifier already
does for legacy heads.
PauseSession now also calls DrmOutputManager::pause() when SRDWM_GPU=1
and a GPU context exists - a separate device/fd from the legacy Card,
so purely additive. ActivateSession calls DrmOutputManager::activate
(false), then deliberately does *not* also force a fresh render for
that head specifically: DrmCompositor::render_frame always issues a
full state commit (atomic or legacy, whichever this device negotiated
- see DrmDevice::is_atomic()), not just a buffer swap, so the
existing data.render_udev_frame() call at the end of this handler
already reasserts mode-set and CRTC-active state together for the GPU
head via render.rs's own GPU branch, the same way it always does.
Also fixed a real conflict Phase 2 introduced: the existing legacy
crtc-reassert loop (explicit set_crtc through the legacy Card/fd) used
to run for every head unconditionally, including one now driven by the
GPU path through a completely different DrmDeviceFd - two separate
fds issuing mode-set commands against the same physical CRTC, exactly
the kind of conflict that produced the worst VT-switch
incidents (EBUSY loops) when it was really one fd racing itself. The
GPU-owned crtc (if any) is now excluded from that loop.
|
|
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.
|
|
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).
|
|
apply_geometry and restore - the Platform callbacks core's toggle_
maximize/apply_snap_zone/restore_window drive for a pure geometry
change - only called sync_geometry, never redraw_decoration_buffer.
The cached border-strip/titlebar bitmaps (self.border_top_decorations,
self.border_bottom_decorations, self.decorations) size themselves from
effective_frame, which can differ from w.geometry alone once a CSD
client's own invisible shadow margin is involved (see that function's
own doc comment) - but nothing here rebuilt them right when this
callback changed w.geometry. The next rebuild only happened whenever
this window's client next committed a frame or some other, unrelated
trigger reached redraw_decoration_buffer, not reliably right away.
Confirmed live: maximizing then restoring a Chrome window left its
border strips sized for the maximized frame while its real content had
already settled back to the smaller restored size, immediately and
permanently until some later trigger happened to catch it up - a
real, visible gap between content and border on the far (east/south)
edges, a different bug from the half-pixel corner seam fixed
separately in blend_corner_pixel.
Both apply_geometry and restore now also call redraw_decoration_buffer
right after sync_geometry, in both the udev and winit backends.
|
|
The border-strip bitmap's own corner rounding (corners.rs's blend_
corner_pixel) and the client-content mask's corner rounding
(rounded_corners_pixman.rs's apply_corner_mask) are two independent
implementations that need to trace the exact same circle where a
window's border meets its own rounded content. Their falloff math was
already identical (same smoothstep construction over the same
radius-1..radius+1 band), but apply_corner_mask samples each pixel at
its own center (x as f32 + 0.5) - the standard rasterization
convention, matching the GLES shader used for the winit backend --
while blend_corner_pixel sampled at the raw integer coordinate (x as
f32), a systematic half-pixel offset between the two curves.
round_top_corners/round_bottom_corners had a compensating "- 1" baked
into their own right-edge/bottom-edge center calculation, tuned
against the old, uncentered convention. Confirmed live at extreme
zoom: a small but real right-angle step partway along an otherwise
smooth arc, right where the two curves are supposed to meet --
reported as "squares on the inside corners of each vertex."
Added the missing + 0.5 to blend_corner_pixel and removed the two
compensating "- 1"s (both call sites), which now line up exactly with
apply_corner_mask's own clamp-derived center for the same corner
(width - r / height - r, not width - r - 1 / height - r - 1). All 378
existing tests pass unchanged - none of them assert exact pixel
positions this shifts by half a pixel.
|
|
The previous fix for a stale border size (switching effective_frame_of
from dwindow.geometry() to raw dwindow.bbox()) traded one bug for
another. bbox() is the window's entire committed buffer; geometry() is
that buffer intersected with the client's own xdg_surface::
set_window_geometry hint, which excludes any invisible CSD shadow
margin the client reserves around its real visible content. The
assumption behind the switch - that sync_geometry's unconditional
tiled-state bits make every compliant client reserve no such margin,
so nothing would be lost - was wrong: confirmed live via temporary
diagnostic logging, Chrome reserves a real, correctly-current 10px
margin on all four sides regardless of the tiled hint (Firefox, the
window that exposed the original staleness bug, does not - the two
disagree on this).
Raw bbox() therefore handed the border/content mask Chrome's entire
buffer, margin included - 20px wider and taller than its real visible
chrome on each axis, with no compensating position shift - so the
rounded border curve traced a rectangle Chrome's real content never
reached, and its true, still-square corner poked straight through the
curve instead of being hidden by it. Reported live as a border not
lining up with a window's content and a hard block cutting through an
otherwise-rounded corner.
Fixed by keeping both properties at once: dwindow.geometry().loc as
the margin - assumed symmetric (left == right, top == bottom), which
holds for every real CSD shadow margin observed here, since it's a
fixed design constant that doesn't scale with window size and so has
no equivalent staleness window even while the hint's absolute size
does - subtracted from the always-fresh bbox(). Current size, correct
visible-content bounds, for a client that reserves a margin (Chrome)
and one that doesn't (Firefox, whose hint .loc is always (0, 0), where
this reduces to plain bbox()) alike.
|
|
effective_frame_of sized a window's border/shadow/titlebar from
dwindow.geometry() - xdg_surface::set_window_geometry. Per smithay's
own implementation that value is the client's cached hint intersected
with bbox(), falling back to bbox() only if never set. Nothing in the
protocol obliges a client to resend the hint on every resize, and
intersection() can never return something larger than its smaller
operand - so once a client's cached hint is smaller than its current
real buffer, geometry() stays clamped there permanently.
Confirmed live: after a passive tiling reflow (this window resized
only as a side effect of a sibling window moving, no direct action on
this window itself), Firefox's real content filled the correct, much
larger area immediately, but its border/decoration stayed rendered at
a small fraction of that - unchanged for several seconds, well past
both animation settling and any reasonable commit-throttle window --
until an unrelated maximize/restore cycle happened to prompt Firefox
into resending a fresh hint and self-correcting.
Switched to bbox(): the real bounding box of the window's current
surface tree, which updates on every commit unconditionally. This
gives up excluding a CSD client's own invisible drop-shadow margin,
but sync_geometry already unconditionally sends all four tiled state
bits specifically so a compliant client (GTK4/Firefox) reserves no
such margin at all, so a compliant client loses nothing. Both the
decoration-bitmap sizing (redraw_decoration_buffer) and the render
loops' own border/shadow/occlusion positioning funnel through this one
function, so they stay consistent with each other - avoiding the
out-of-bounds texture-crop regression a previous, different attempt at
this same lag hit (see this function's own doc comment history).
|
|
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.
|
|
The kernel revokes every input device fd across a VT switch away.
libinput has a documented pair of calls for this exact case,
suspend()/resume() (libinput_suspend/libinput_resume), which reopen
every device through the session once it is reactivated. This
codebase never called either one, so after switching back to the
compositor's VT, libinput's device list stayed pointed at fds the
kernel had already revoked - reads on them don't error, they just
silently stop producing events, forever. Rendering, DRM/KMS, and
libseat's own session activation all recovered on their own, which is
what made this look like a display bug rather than an input one; it
took three real forced reboots today, with no visible input from
keyboard or mouse for 30+ minutes after switching back to tty1 each
time, to isolate it as this specific missing call.
register_libinput now returns the Libinput context (a clone of the
one already handed to LibinputInputBackend - it's a reference-counted
handle, not a deep copy, and LibinputInputBackend only exposes an
immutable accessor once it's moved into the calloop event source).
register_session_notifier takes that handle and calls suspend() on
PauseSession, resume() on ActivateSession.
|
|
VT-switch resume: reasserting the CRTC's mode-setting state was never
enough on its own - display power (DPMS) is separate KMS state, and
nothing here ever touched it after a real switch-away-and-back. Page
flips kept succeeding with zero errors logged for the rest of a real
30+ minute session while the panel itself simply stayed dark, which is
what actually explains a user report of the screen and keyboard input
never recovering after one VT switch. Sets DPMS-on unconditionally on
every resume now, the same property zwlr_output_power_v1 already
writes for an explicit client request.
Corner rendering: both backends' side-strip crop (the fix that keeps a
window's flat left/right border strips from poking a solid-coloured
square through the top/bottom strip's own rounded curve) only ever
activated for `w.decorated` windows. An undecorated/CSD window's crop
depends on its *content* actually getting masked to match - the
winit backend never checked that at all, so every CSD window with a
nonzero border_width got the uncropped, "staircase" artifact
unconditionally, confirmed live via a highlighted border colour and
raw pixel sampling. Ported the udev backend's own `border_curve_is_safe`
check (decorated OR content-will-be-masked) into winit, using the
cheap "does this surface have subsurface children" test both content-
masking code paths already gate success on, rather than duplicating
either one's real (comparatively expensive) rendering work just to
probe it.
Full workspace build + clippy -D warnings + test suite (378 tests) green.
|
|
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.
|
|
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.
|
|
# 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
|
|
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.
|
|
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.
|
|
sync()'s per-tick platform.focus(id) re-assertion (added to keep real
Wayland/X11 keyboard focus following core's own bookkeeping) ran
unconditionally on every dirty tick, including when nothing about focus
had actually changed. focus_window (core) has its own, separate side
effect of switching to the focused window's workspace when it differs
from the current one - correct when focus genuinely moves to a window
on another workspace, but this call was never gated on focus having
changed at all: switching workspace via activate_workspace left the
still-focused window's own workspace field untouched, so the very next
dirty tick's blind re-assertion of that same focus saw a mismatch against
the just-changed current_workspace and switched straight back.
Confirmed live via temporary core-side logging: two switch_workspace
calls a few milliseconds apart, the second one undoing the first every
single time, for every workspace switch that didn't also change which
window was focused.
Gated the re-assertion on the focused id actually changing since the
last sync() call. Real focus-follows-real-platform-focus still happens
on every genuine change, which is all the original fix needed.
|
|
Every pointer motion event re-ran the same popup/layer/content hit-test
from scratch and delivered focus to whatever it found right now - there
was no notion of "a button is held, keep delivering to the surface that
received the press" at all, which is standard, expected Wayland
compositor behavior (every real compositor does this; it's how dragging,
text selection, and scrollbar-thumb dragging all stay coherent even when
the pointer briefly leaves the widget's bounds mid-gesture).
Without it, a real human's hand drifting even slightly outside the
pressed surface mid-drag - trivially easy during a fast, non-perfectly-
straight mouse motion - sent that client an unrequested `leave` event in
the middle of its own gesture. GTK's drag recognizers (a GtkHeaderBar's
move-the-window gesture, concretely) treat a mid-gesture leave as "this
isn't coherent, abort," which reads as "dragging this window by its
title bar does nothing at all" - live-reproduced this work on Nemo,
and consistent with move_request never having fired once all session
despite real attempts.
pointer_button_grab captures the (surface, origin) resolved on a button
press once the held-button count goes from 0 to 1, and every event under
the grab - motion or button, this press's or a later one overlapping
it - is delivered there instead of wherever a fresh hit-test lands,
until every held button is back up.
|
|
1. A popup's xdg_popup.grab (Firefox's own right-click menu, concretely)
receiving zero pointer input at all - no hover highlight, no click
effect, not even dismiss-on-miss - logs whether grab_popup actually
succeeds, since a silent failure there would explain exactly this.
2. srd dispatch activate_workspace returning {"ok":true} without ever
changing the current workspace, confirmed via a raw socket request
bypassing the CLI entirely. switch_workspace's own logic reads
correct; logs its actual inputs/state to find out why the real
process disagrees with it.
Remove once both are resolved.
|
|
Every srd.spawn/Command::spawn call fires and forgets its Child handle
by design (a compositor's main loop can't block waiting on an arbitrary
launched command), but nothing else was reaping them either, so every
one that exited stayed a zombie for the rest of the session. Confirmed
live via an AGS peer session's own ps: six zombies from four different
programs, spread across half an hour of ordinary use.
Explicitly ignoring SIGCHLD is the standard fix for exactly this case --
the kernel reaps exited children itself, no waitpid loop needed.
|
|
popup_targets (used for both drawing and hit-testing xdg_popup surfaces)
never subtracted the xdg_surface::set_window_geometry content offset the
rest of the codebase already accounts for - a CSD window's popups were
placed relative to its raw, unshifted buffer origin instead of its real
visible content, self-consistently wrong in both rendering and click
routing.
|
|
sync_layer_visibility could not tell a real hide (null-buffer commit on
an already-visible surface) apart from a layer-shell client's ordinary
realization sequence (commit with no buffer -> configure -> ack-commit
with no buffer again -> attach real content): both look like "committed,
no buffer" from has_buffer alone. Every layer surface's first realization
was spuriously unmapped and immediately remapped, doubling LayerMap
arrange() passes on every single popup open.
Live-reproduced via an AGS peer session: a full-monitor click-outside-to-
close popup surface came back from a hit-test with geometry wider than
the real output after several open/close cycles on a wl_surface GTK had
reused across role destroy/recreate, and sat in the Top layer above every
real window with no input region set - silently swallowing clicks meant
for windows, dropdowns, and CSD title bars alike.
layer_surfaces_shown_once now gates the hide path on a surface having
actually shown a buffer at least once, and is cleared in layer_destroyed
so a reused wl_surface's next role starts clean rather than inheriting
the previous role's flag.
|
|
Logs the layer kind, namespace, arranged geometry, surface-local hit
point, and the surface's actual committed input region for every
candidate the hit-test walks. Answers two open questions from a live
report of a dock receiving zero pointer input despite a correctly-set
input region as measured from the client side: whether arrange() is
giving the surface its full requested size or clamping it to the
exclusive zone, and whether this compositor's own view of the
committed input region actually matches what the client set. Remove
once that's settled.
|
|
layer_surface_under_layers used smithay's LayerMap::layer_under(),
which returns only the single topmost surface (by z-order) whose
*bounding box* contains the point - not its real input region. If
that one surface's input region excluded the point, the old code gave
up on the whole layer-kind instead of falling through to whatever real
surface is stacked underneath it.
Concretely: any other surface on the same layer-kind with a bbox
overlapping the target - a mapped-but-mostly-transparent
backdrop/dismiss popup, concretely - would silently swallow every
hover and click meant for whatever's underneath, with no way to reach
it at all. Same failure shape as an already-fixed AGS-side bug
(Overview's own bbox-wide input-region fallback), just compositor-side
and not limited to that one instance.
Now walks every candidate on a layer-kind topmost-first and tries the
next one down when a candidate's actual input region doesn't cover the
point, instead of stopping at the first bounding-box match.
|
|
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.
|
|
focus_window marked the target focused without clearing minimized, so
a dock icon's Activate (or the plain "focus" IPC command) on a
minimized window left it focused but still excluded from
visible_windows/rendering - reads exactly like the click did nothing,
since the window never actually reappears. Every focus_window caller
gets the restore for free now. Added a Super+n keybind for minimize
too, since none existed at all before this.
|
|
Remaining files from the backlog: Lua config engine
additions (general/register/support/window), workspace Cargo.toml/
Cargo.lock churn from the new dependencies added elsewhere in this
sweep, srdwm/main.rs wiring, and docs (DEFAULTS.md plus a new
SESSION_HANDOFF.md written mid-session for continuity across a restart
- see that file's own header for what it is and isn't).
|
|
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.
|
|
CLI verbs
Bundles related IPC-surface and CLI additions built up over this
session:
- capture_workspace IPC command + srd capture workspace CLI verb (see
the wayland-crate commit for the off-screen render this drives).
- Expanded IPC response payloads (monitors, client events, global menu
info) and their matching CLI plumbing.
Same reasoning as the core-crate sweep commit for why this is coarser
than the repo's usual convention: too entangled to split safely without
a dedicated review pass.
|
|
test coverage
Bundles several related changes to crates/core built up over this
session rather than committed incrementally:
- WindowManager::request_capture_workspace/drain_capture_requests (new
manager/capture.rs) - backend-agnostic queuing for an off-screen
workspace render, see the wayland-side commit for why this exists.
- focus_window now switches workspace as a side effect when the target
isn't on the current one, matching Hyprland's focuswindow convention
(manager/focus.rs).
- Assorted window/rules/theme field additions and their test coverage.
Left less granular than the repo's usual one-purpose-per-commit
convention deliberately: these accumulated across a long session
without being committed as they landed, and are too entangled
line-by-line to safely split apart now without risking mis-attributing
changes to the wrong commit message.
|
|
toggle_maximize previously targeted full_geometry outright (past every
reserved zone), on an earlier request specifically about the dock -
which also silently let it extend behind a top bar's zone, reported
back as its own bug once live-tested. maximize_geometry is a third
rect distinct from geometry (every zone) and full_geometry (none):
full_geometry with only a top-anchored bar's exclusive zone subtracted
back out. New test locks in dock-covered/bar-respected together.
|
|
Right-clicking (or hovering) the maximize button opens a flyout of
fixed half/quarter positions (snap_flyout.rs renders it); picking one
applies that zone via WindowManager::apply_snap_zone, the click-driven
equivalent of dragging a window to that edge and releasing.
|
|
Exposes each window's application menu (Firefox/GTK's dbusmenu export,
X11's _GTK_APPLICATION_OBJECT_PATH-style menus via global_menu.rs) so
an external panel can render it as a system menu bar rather than each
window drawing its own, the same convention appmenu.rs/gtk_shell.rs
and appmenu_registrar.rs wire up across both backends.
|
|
A real ext-session-lock-v1 implementation drawn by the compositor
itself, not a hand-off to an external locker process: a live-blurred
background, PAM authentication on a background thread, and its own
config surface (lock_config.rs) rather than hardcoded appearance.
|
|
A flip issued right before a VT switch away could still be undelivered
when the session resumed - the kernel refuses a new page flip on a CRTC
with one already outstanding, which showed up live as a black screen
that never recovered across two switch attempts, with a rapid repeating
"Device or resource busy" loop in the log. Drain and apply any pending
DRM events before reasserting CRTCs and rendering again on resume, so a
stale flip from before the switch can't collide with the fresh one.
|
|
read_global_menu unconditionally preferred MenuSource::Gtk whenever a
GTK menubar path resolved - but appmenu-gtk-module exports a plain
Gtk.Window's menu (no GtkApplication, so no _GTK_APPLICATION_OBJECT_
PATH/_GTK_WINDOW_OBJECT_PATH) through its Unity-compatibility shim,
unity.-prefixed actions and all, while still setting _GTK_MENUBAR_
OBJECT_PATH. Labeling that Gtk meant a consumer inserted app/win
action groups the app never populated instead of a unity one it did --
every menu item rendered, but permanently insensitive, since none of
them resolved against a group that existed. This was flagged as a
known risk in this function's own doc comment when `source` was first
added, but the priority logic itself never got the fix.
Root-caused live by an AGS peer session: read the actual exported menu
content off the bus for a real appmenu-gtk-module app and found
unity.-prefixed actions at the GTK atom's own path, with app_path/
window_path both empty - confirming that emptiness is the reliable
tell, not which atom happened to resolve.
Fixed by preferring Unity whenever app_path/window_path are both
absent, even if a GTK menubar path resolved - the path itself doesn't
change, only the label. Pulled the decision out into a standalone
classify_menu_source function so it's unit-testable without a real X
connection, with the exact live case as a regression test.
|
|
Auditing "clicking behavior and basics": general.focus_follows_mouse,
general.mouse_follows_focus, general.auto_raise, general.auto_focus,
the entire window.* namespace (8 more keys, a full duplicate of the
same four plus remember_position/size/state), and general.
smart_placement/border_width were all seeded into default_config() and
documented in DEFAULTS.md, but none were read anywhere - srd.set()/
srd.get() on any of them silently succeeded while doing nothing.
focus_follows_mouse is real, well-defined, and directly relevant to
clicking basics - implemented it plus auto_raise (raise, not just
focus, on hover) rather than just deleting the promise like the
others. WindowManager gained focus_follows_mouse/auto_raise bools,
wired from apply_general_settings the same way every other general.*
flag is. handle_pointer_position now tracks whichever window (content
or decoration) is under the pointer and, when the setting is on and
that differs from the currently-focused window, focuses it through the
same focus_window() free function every click-driven focus change
already uses (real keyboard focus, not just core state) - skipped
entirely while dragging/resizing or over a layer-shell surface, so the
pointer sweeping over other windows mid-drag or hovering a bar can't
steal focus from what's actually being manipulated.
mouse_follows_focus (pointer warp on keybinding-driven focus change)
and auto_focus (no clear distinct meaning beyond click-to-focus) stay
unimplemented and are now undocumented rather than promised.
|
|
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.
|
|
MISSING.md had listed this as needing to touch srdwm_core::window::
hit_test's signature at every call site, including the honest-stub
Windows/macOS backends - overstated on closer inspection: that shared
function already takes a plain resize_margin: i32 parameter, agnostic
to where the value comes from. The only real change needed was reading
Window.resize_margin.unwrap_or(wm-wide default) instead of always the
WM-wide value, at the single call site inside WindowManager::hit_test
in core - no backend touched at all.
Window.resize_margin: Option<i32>, WindowRuleActions.resize_margin to
match, applied in add_window/reapply_rules_if_pending the same way
opacity already is. Settable via a rule action or
srd.window.set_resize_margin(n) on the focused window.
|
|
Same shape as the existing toggle_visibility/focus/close dispatch
actions - lets an external script (or a live diagnostic check)
drive either without a keybinding already existing to trigger it.
Immediately useful for verifying the maximize/fullscreen contract on
a live session without synthesizing any input at all.
|
|
No configure this compositor ever sent set any xdg_toplevel::State bit
at all - confirmed by grepping the whole crate, zero hits before this.
GTK4 (Firefox concretely) reads the tiled bits to decide whether to
reserve an invisible client-side shadow margin around its own content,
independent of server- vs client-side decoration; with none ever sent
it always assumed "floating, might need a shadow" and kept reserving
one. That margin sits inside the committed buffer but is functionally
invisible, so this compositor's own border - drawn at the full
geometry, margin included, since nothing here knew the margin existed
- ended up visibly offset from where the client's real chrome began.
Root-caused from a live screenshot: Firefox's srdwm-drawn border sat
clearly up-and-left of its actual toolbar, not framing it. Reported as
"border is not with the window at start" and, more generally, borders
never feeling like part of the window they're drawn around - which
this is: decoration and content genuinely disagreeing about where the
window's edge is.
Sets all four Tiled* bits unconditionally on every xdg_toplevel
configure - the same technique river/dwl use, telling every window
it's flush against something and should skip its own shadow regardless
of whether it's in a literal tiling layout, which is the outcome
actually wanted here: this compositor draws the frame, nothing else
should also be reserving room for one.
Not visually verified against a live client yet - this needs an
actual GTK app rendering under a restarted session to confirm the
shadow margin is really gone, which no offline test can substitute
for.
|
|
Reported live, repeatedly: corners felt like they had no priority over
sides. They didn't, structurally - a corner only ever registered in
the exact pixel square where both edges' own resize_margin zones
happened to overlap (6x6px at the default 6px margin), nothing wider.
A click a little further along either axis, still clearly aiming for
the corner, fell back to a single straight-edge resize instead.
resize_edge_at now checks a separate, wider corner zone
(CORNER_MARGIN * resize_margin) first, so a corner claims a real,
deliberately larger target the way GNOME/KDE already do - diagonal
resize is a harder grab than a straight edge and deserves more room,
not the same or less.
Also fixed a related, previously-unreachable case: a *decorated*
window's whole titlebar band returned Drag/Close/Maximize/Minimize
unconditionally, so top-left/top-right diagonal resize never had a
code path at all, even at the titlebar's own corner pixels. Added
top-left resize as a small, genuine corner square (both axes, not just
one) checked before drag/buttons. Deliberately did NOT add the
matching top-right case: that corner is the close button on every
mainstream desktop, and trading a well-known, expected target for a
rarely-wanted one at exactly the spot a miss costs the most isn't a
trade worth making.
Four new regression tests cover: reaching a corner past the old tight
margin, falling back to a plain edge just past the new wider one, the
newly-reachable decorated top-left corner, and confirming top-right
still closes rather than competing with resize.
|
|
A peer session pointed out srd clients reports a decorated window's
rect 30px taller than X11 reports the client's own content window --
correct (Window.geometry is the frame rect, TITLEBAR_HEIGHT included
on top, the same convention hit-testing/rendering already use
internally), but genuinely undocumented from an external IPC
consumer's point of view. Made the frame-vs-content distinction
explicit on the field itself rather than leaving it to be reverse-
engineered from a pixel diff.
|
|
map_window_request only ever read window.title()/.class() once, at
MapRequest - for a client whose managed window doesn't carry
WM_NAME/WM_CLASS at that exact moment (or the properties simply arrive
later), Window.title/app_id stayed permanently empty. That reaches
srd.rule's class matching, this compositor's own titlebar text, and
every wlr-foreign-toplevel-management listener (a dock's running
indicator, an app switcher, icon lookup) - confirmed live via a peer
session's AGS instance rendering blank rows for Spotify and OpenSnitch.
Implements XwmHandler::property_notify (previously unhandled, a
no-op default) to re-read title/class on WmWindowProperty::Title/Class
and update Window plus notify rule/decoration/foreign-toplevel
listeners on an actual change - the XWayland-side mirror of
sync_toplevel_metadata, which already exists for exactly this problem
on the native xdg-shell path (see its own doc comment).
Not fully verified against the specific live case that surfaced this:
whether XWayland's X11Wm delivers property_notify for a *managed*
window whose real WM_NAME/WM_CLASS live only on an unmanaged sibling/
child (rather than arriving late on the same window) is still an open
question - this fixes the well-documented "arrives late" case with
certainty, and may or may not cover that harder case too.
|
|
Platform::close only ever called w.toplevel(), which is None for an
XWayland window - closing one (the WM's own close binding, or `srd
dispatch close`) silently did nothing at all, on both udev and winit.
Found live: a leftover untitled fullscreen window wouldn't close via
srd dispatch close even after several seconds, tracing back to this.
Fixed by falling back to X11Surface::close() when there's no xdg
toplevel - it already handles both cases smithay-side (a polite
WM_DELETE_WINDOW for a cooperating client, outright destroy_window for
one that doesn't support it), so no new logic was needed, just calling
it.
|
|
visible_windows' doc comment claimed windows show "on the active
workspace of whichever monitor they're assigned to" - the code never
reads w.monitor at all; current_workspace is one flat value shared by
every monitor, not per-output. Documented that explicitly on both the
field and the method, since this is a real behavioral difference from
Hyprland worth a reader actually seeing, not just an inaccurate comment
to fix quietly.
monitor.primary_workspace/monitor.workspace_count describe a per-
monitor-workspace design that doesn't exist; workspace.auto_switch/
workspace.persistent were never wired to any behavior. All four were
seeded into default_config() and documented in DEFAULTS.md, so
srd.set()/srd.get() on them silently succeeded while doing nothing --
removed from both, matching the precedent already set by general.
rounded_corners' deliberate absence from default_config for a
different reason (backend-dependent default rather than unbuilt).
|