From 9dba86f3cacb951630c7729ab0144680aae66b4e Mon Sep 17 00:00:00 2001 From: Asmir A Date: Sun, 2 Aug 2026 21:30:41 +0200 Subject: [PATCH] add workaround for river wm rotation issue --- CLAUDE.md | 26 +++++++++ src/att_lock.h | 17 ++++++ src/main.c | 156 +++++++++++++++++++++++++++++++++++++++++++++++-- src/render.c | 10 +++- 4 files changed, 202 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index efbcf88..d6796ee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,6 +76,32 @@ pixels. `render_output()` allocates a device-pixel buffer of `width * scale` and shorter output dimension (`att_grid()`), which is what makes rotation and arbitrary aspect ratios work with no special cases. +**Output resize / rotation** — the surface size comes from `configure` and *only* from `configure`: +ext-session-lock makes a buffer that does not match the acked size a `dimensions_mismatch` error, +which kills the client and leaves the session locked with nothing drawing on it. So the locker can +never resize itself on its own initiative. + +That matters because not every compositor reconfigures. River 0.4.5 configures a lock surface once, +when it is created, and never again (`LockSurface.create()` is the only `configure()` call site, and +nothing in `Output.zig` re-issues one), so rotating or re-moding an output used to leave the locker +painting at the old size, clipped to the overlap. `att_lock` therefore watches `wl_output` +(`mode`/`geometry`/`scale`), and if the output changed shape but no configure arrives within +`RESIZE_GRACE_MS`, `recreate_lock_surface()` destroys the lock surface and makes a new one for the +same output, which forces a fresh configure at the current size. The lock object is untouched, so +the session stays locked across the swap. + +Two guards keep this from misfiring, and both matter: + +- The change detector compares `wl_output` values against a snapshot of `wl_output` values taken at + the last configure (`cfg_*`), never against the configured surface size. On a fractionally scaled + output the integer `wl_output.scale` cannot reproduce the logical size, so those two would never + match and every idle moment would look like a pending resize. +- `retry_*` records the geometry a recreate was already attempted for, so a compositor that ignores + the recreate is nagged once instead of being put in a destroy/create loop. + +A compositor that does send a configure never reaches any of this: by the time the grace period +expires the snapshot matches again and `outputs_recheck_geometry()` does nothing. + **Pattern model** (`src/pattern.c`) — a sequence of dot indices (`row * 4 + col`). `pattern_add()` implements the Android rule that unvisited dots lying exactly on the straight line between the previous and new dot get captured first, using the gcd of the row/col delta. `pattern_to_string()` diff --git a/src/att_lock.h b/src/att_lock.h index 9ca0007..dbecb80 100644 --- a/src/att_lock.h +++ b/src/att_lock.h @@ -63,6 +63,20 @@ struct att_output { int32_t width, height; /* logical size from configure */ bool configured; struct att_buffer buffers[2]; + + /* Output geometry as last advertised by wl_output. Only ever compared + * against the `cfg_*` snapshot below, never used to size a buffer: the + * buffer size must come from configure alone, and with fractional scale + * the integer wl_output.scale cannot reproduce the logical size anyway. */ + int32_t mode_w, mode_h, transform; + + /* Same four values as of the most recent configure. A difference means + * the output changed and the compositor has not reconfigured us. */ + int32_t cfg_mode_w, cfg_mode_h, cfg_transform, cfg_scale; + + /* Geometry we last recreated the lock surface for, so a compositor that + * ignores the recreate is nagged once rather than forever. */ + int32_t retry_mode_w, retry_mode_h, retry_transform, retry_scale; }; struct att_seat { @@ -111,6 +125,9 @@ struct att { int fail_count; /* failed unlock attempts, pattern or password */ int64_t lockout_until; /* CLOCK_MONOTONIC ms; 0 when not locked out */ + /* CLOCK_MONOTONIC ms at which to re-examine output geometry, or 0. */ + int64_t resize_recheck_at; + /* configuration */ const char *image_path; const char *pam_service; diff --git a/src/main.c b/src/main.c index 4883c99..982c699 100644 --- a/src/main.c +++ b/src/main.c @@ -20,6 +20,12 @@ static void schedule_redraw(struct att *app) app->dirty = true; } +/* How long to wait for a compositor to send the configure that an output + * geometry change should produce, before forcing one. Long enough not to race a + * compositor that reconfigures in a later event batch, short enough that a + * rotated screen is not left unusable. */ +#define RESIZE_GRACE_MS 250 + /* --- failed-attempt throttling -------------------------------------------- */ /* The pattern is checked against a local hash and never goes through PAM, so it @@ -522,15 +528,26 @@ static const struct wl_seat_listener seat_listener = { /* --- output --------------------------------------------------------------- */ -static void out_geometry(void *d, struct wl_output *o, int32_t x, int32_t y, +static void out_geometry(void *data, struct wl_output *o, int32_t x, int32_t y, int32_t pw, int32_t ph, int32_t subpixel, const char *make, const char *model, int32_t transform) -{ (void)d; (void)o; (void)x; (void)y; (void)pw; (void)ph; (void)subpixel; - (void)make; (void)model; (void)transform; } +{ + (void)o; (void)x; (void)y; (void)pw; (void)ph; (void)subpixel; + (void)make; (void)model; + struct att_output *out = data; + out->transform = transform; +} -static void out_mode(void *d, struct wl_output *o, uint32_t flags, +static void out_mode(void *data, struct wl_output *o, uint32_t flags, int32_t w, int32_t h, int32_t refresh) -{ (void)d; (void)o; (void)flags; (void)w; (void)h; (void)refresh; } +{ + (void)o; (void)refresh; + struct att_output *out = data; + if (!(flags & WL_OUTPUT_MODE_CURRENT)) + return; + out->mode_w = w; + out->mode_h = h; +} static void out_scale(void *data, struct wl_output *o, int32_t factor) { @@ -539,7 +556,47 @@ static void out_scale(void *data, struct wl_output *o, int32_t factor) out->scale = factor > 0 ? factor : 1; } -static void out_done(void *d, struct wl_output *o) { (void)d; (void)o; } +/* Record the output geometry that the current configure corresponds to. */ +static void out_snapshot(struct att_output *o) +{ + o->cfg_mode_w = o->mode_w; + o->cfg_mode_h = o->mode_h; + o->cfg_transform = o->transform; + o->cfg_scale = o->scale; +} + +/* True if the output changed shape since the configure we are drawing to. + * Deliberately compares wl_output values against wl_output values rather than + * against the configured surface size: on a fractionally scaled output the two + * never match, and that must not be mistaken for a pending resize. */ +static bool out_geometry_changed(const struct att_output *o) +{ + return o->mode_w != o->cfg_mode_w || + o->mode_h != o->cfg_mode_h || + o->transform != o->cfg_transform || + o->scale != o->cfg_scale; +} + +static void out_done(void *data, struct wl_output *wl_out) +{ + (void)wl_out; + struct att_output *o = data; + + if (!o->configured) + return; /* nothing to compare against yet */ + if (o->cfg_mode_w <= 0) { + /* First configure landed before wl_output described the output; + * adopt this geometry as the baseline instead of treating it as + * a change. */ + out_snapshot(o); + return; + } + if (out_geometry_changed(o)) { + /* Give a well-behaved compositor a moment to send the configure + * this change should produce; only step in if it never does. */ + o->app->resize_recheck_at = att_now_ms() + RESIZE_GRACE_MS; + } +} static void out_name(void *d, struct wl_output *o, const char *name) { (void)d; (void)o; (void)name; } static void out_description(void *d, struct wl_output *o, const char *desc) @@ -562,9 +619,22 @@ static void lock_surface_configure(void *data, { struct att_output *o = data; ext_session_lock_surface_v1_ack_configure(surf, serial); + + bool resized = o->configured && + ((int32_t)width != o->width || (int32_t)height != o->height); + o->width = (int32_t)width; o->height = (int32_t)height; o->configured = true; + out_snapshot(o); + + /* The grid moves with the surface, so a stroke started before the resize + * would carry its already-captured dots onto a grid that is no longer + * under the user's finger -- and completing it would count as a failed + * attempt. Drop it and let them start over. */ + if (resized && o->app->pattern.drawing && o->app->active_output == o) + pattern_reset(&o->app->pattern); + render_output(o); /* must commit a buffer for the compositor to lock */ } @@ -583,6 +653,66 @@ static void create_lock_surface(struct att *app, struct att_output *o) &lock_surface_listener, o); } +/* Tear the lock surface down and build a fresh one for the same output. + * + * Some compositors (river 0.4.5, for one) configure a lock surface exactly once + * when it is created and never again, so rotating or re-moding an output leaves + * us committing buffers at the old size -- the surface is then clipped to the + * overlap and the unlock UI ends up half off-screen. We cannot simply commit a + * correctly sized buffer instead: ext-session-lock requires the buffer to match + * the acked configure exactly, and a `dimensions_mismatch` error kills the + * client, which leaves the session locked with nothing drawing on it. Creating + * a new lock surface is in-spec and makes the compositor issue a configure with + * the output's current size. + * + * The lock object itself is untouched, so the session stays locked throughout. */ +static void recreate_lock_surface(struct att *app, struct att_output *o) +{ + if (!app->lock) + return; + + if (o->lock_surface) { + ext_session_lock_surface_v1_destroy(o->lock_surface); + o->lock_surface = NULL; + } + if (o->surface) { + wl_surface_destroy(o->surface); + o->surface = NULL; + } + /* The buffers outlive the surface on purpose: the compositor may still be + * scanning one out, and it releases them in its own time. buffer_get() + * reaps them once they come back at the wrong size. */ + o->configured = false; + + create_lock_surface(app, o); +} + +static void outputs_recheck_geometry(struct att *app) +{ + struct att_output *o; + + wl_list_for_each(o, &app->outputs, link) { + if (!o->configured || o->cfg_mode_w <= 0) + continue; + if (!out_geometry_changed(o)) + continue; /* the compositor reconfigured us after all */ + + /* Recreated for this exact geometry already and still nothing: + * stop, rather than loop on a compositor we cannot satisfy. */ + if (o->retry_mode_w == o->mode_w && + o->retry_mode_h == o->mode_h && + o->retry_transform == o->transform && + o->retry_scale == o->scale) + continue; + + o->retry_mode_w = o->mode_w; + o->retry_mode_h = o->mode_h; + o->retry_transform = o->transform; + o->retry_scale = o->scale; + recreate_lock_surface(app, o); + } +} + static void output_destroy(struct att_output *o) { wl_list_remove(&o->link); @@ -814,6 +944,20 @@ int main(int argc, char **argv) } } + /* Same trick as the countdown: an output that changed shape has + * to wake us even with no input pending, or a rotation made + * while idle would stay broken until the user touched something. */ + if (app.resize_recheck_at) { + int64_t left = app.resize_recheck_at - att_now_ms(); + if (left > 0) { + if (timeout < 0 || left < timeout) + timeout = (int)left; + } else { + app.resize_recheck_at = 0; + outputs_recheck_geometry(&app); + } + } + if (app.dirty) { app.dirty = false; render_all(&app); diff --git a/src/render.c b/src/render.c index bcdce31..1da0a7e 100644 --- a/src/render.c +++ b/src/render.c @@ -245,8 +245,16 @@ void render_output(struct att_output *o) int ph = o->height * scale; struct att_buffer *b = buffer_get(app, o, pw, ph); - if (!b) + if (!b) { + /* Both buffers are still held by the compositor. Ask for another + * pass instead of dropping the frame: with nothing else pending + * the loop blocks indefinitely, so a frame lost here -- the one + * right after a resize, most of all -- would never be repainted + * and the surface would keep showing its pre-resize contents. + * The pending wl_buffer.release wakes the poll. */ + app->dirty = true; return; + } cairo_surface_t *surface = cairo_image_surface_create_for_data( b->data, CAIRO_FORMAT_ARGB32, pw, ph, pw * 4);