add workaround for river wm rotation issue

This commit is contained in:
2026-08-02 21:30:41 +02:00
parent 44aca6d476
commit 9dba86f3ca
4 changed files with 202 additions and 7 deletions
+26
View File
@@ -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()`
+17
View File
@@ -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;
+150 -6
View File
@@ -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);
+9 -1
View File
@@ -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);