diff options
| author | Ingmar Steen <iksteen@gmail.com> | 2026-09-06 19:39:57 +0200 |
|---|---|---|
| committer | Ingmar Steen <iksteen@gmail.com> | 2026-09-06 19:39:57 +0200 |
| commit | de3dacc0d23b6ca75fbe63370510f101c1e6a9e7 (patch) | |
| tree | f624e1362d461dcb55e31ad15902ecf9981e32c0 /src/server.c | |
| parent | pane: drop the render snapshot of panes on hidden tabs -- 154K -> 66K each (diff) | |
| download | slosh-de3dacc0d23b6ca75fbe63370510f101c1e6a9e7.tar.gz slosh-de3dacc0d23b6ca75fbe63370510f101c1e6a9e7.tar.xz slosh-de3dacc0d23b6ca75fbe63370510f101c1e6a9e7.zip | |
server: one synchronized message per frame, cells and images together
A frame went out as two messages: the cell diff, then the kitty
placements that belong on top of it. The client wrote each to the
terminal with its own write, and directly on a tty the two land in one
read often enough that nobody saw the gap. Put anything that re-chunks
the stream in between -- a mirror like shellglass, which reads the pty
8KB at a time and writes each read straight through -- and the gap
becomes a screen refresh of its own: the cells where the picture is,
repainted, with no picture on them, and the placement arriving a moment
later to put it back. Every image on screen blinks once per frame, which
is how it was reported: unstable, as if constantly redrawn.
So the two halves travel as one message now, and the whole frame is
wrapped in synchronized output (DEC private mode 2026), which a terminal
buffers and presents as one update. That covers the case the single
message cannot: a frame larger than one read downstream is still split,
and the markers are what stop the halves being presented separately.
Terminals that do not implement 2026 ignore the private mode, so it goes
out unconditionally -- the same thing tmux does, and the reason a redraw
under tmux never showed this.
The live check asserts what the fix is: no image command reaches the
client outside a synchronized update.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRz3zwZJvBvzsR9k4cj6dd
Diffstat (limited to 'src/server.c')
| -rw-r--r-- | src/server.c | 105 |
1 files changed, 74 insertions, 31 deletions
diff --git a/src/server.c b/src/server.c index 387fa7d..bb233ca 100644 --- a/src/server.c +++ b/src/server.c @@ -110,6 +110,11 @@ typedef struct { uint16_t fallback_cell_w, fallback_cell_h; bool pending_paint; int64_t next_frame; + /* One frame, assembled for whichever client is being pushed to: cells, + * images and the synchronized-update markers around them. Server-wide + * rather than per-connection because it never outlives the send. */ + char *frame; + size_t frame_cap; } server_t; static conn_t *active_conn(server_t *s) { @@ -247,29 +252,6 @@ static char *b64(const char *in, size_t len, size_t *out_len) { return out; } -/* Images go out after the cell diff, so a repainted cell cannot land on top - * of a placement we just made. - * - * Returns whether the client is caught up. The stream is indivisible -- half - * a frame is a dangling APC that eats whatever follows it -- so a full outbox - * drops the whole frame, the model is told (deletions stay owed, transmits - * are re-sent), and the caller keeps a repaint pending until it lands. Losing - * a frame here silently was how a scrolled-away screenshot stayed parked on - * the screen: its deletion left with the dropped bytes and was never owed - * again. */ -static bool push_graphics(server_t *s, conn_t *c) { - size_t len = 0; - const char *bytes = app_graphics_view(s->app, c->gfx, c->view_x, c->view_y, - c->cols, c->rows, &len); - if (!len) { - app_graphics_view_commit(c->gfx, true); - return true; - } - bool ok = conn_send(c, MSG_OUTPUT, bytes, len); - app_graphics_view_commit(c->gfx, ok); - return ok; -} - /* The clipboard lives on the client's machine, not ours, so a copy travels as * OSC 52 for the outer terminal to honour. Input-driven copies name their * originating connection; asynchronous pane copies use the active one. */ @@ -331,15 +313,77 @@ static void stamp_indicator(server_t *s, conn_t *c) { screen_put_utf8(&c->view, (uint16_t)(x0 + i), 0, &tag[i], 1, fg, bg, 0); } -static bool push_frame(server_t *s, conn_t *c) { +/* Synchronized output (DEC private mode 2026): the terminal buffers + * everything between the two markers and presents it as one update. + * + * A frame is a cell diff and then the image placements that belong on top of + * it -- and a terminal that draws the diff *before* the placements arrive + * shows, for one refresh, the cells where the picture is with no picture on + * them. Direct on a tty the two halves usually land in one read and nobody + * sees the gap; put anything that re-chunks the stream in between (a mirror + * like shellglass reads the pty 8KB at a time and writes each read straight + * through) and the gap becomes a refresh of its own. Every image on screen + * then blinks once per frame, which is how it was reported: "unstable, as if + * constantly redrawn". + * + * Terminals that do not implement 2026 ignore the private mode, which is why + * this can go out unconditionally. */ +#define SYNC_BEGIN "\x1b[?2026h" +#define SYNC_END "\x1b[?2026l" + +/* One client's whole frame: the cell diff, then the images that belong on top + * of it, in a single message. + * + * One message rather than the two this used to be, because the two are one + * picture: split across messages they are split across writes at the client, + * and everything downstream is then free to present the halves separately. + * Images last within it, so a repainted cell cannot land on top of a + * placement we just made. + * + * Returns whether the client is caught up. The message is indivisible -- half + * a frame is a dangling APC that eats whatever follows it -- so a full outbox + * drops the whole frame, the model is told (deletions stay owed, transmits + * are re-sent), and the caller keeps a repaint pending until it lands. Losing + * a frame here silently was how a scrolled-away screenshot stayed parked on + * the screen: its deletion left with the dropped bytes and was never owed + * again. */ +static bool push_display(server_t *s, conn_t *c) { uint16_t old_x = c->view_x, old_y = c->view_y; uint16_t old_cols = c->shown_cols, old_rows = c->shown_rows; update_viewport(c, &s->screen); screen_project(&c->view, &s->screen, c->view_x, c->view_y); stamp_indicator(s, c); screen_render(&c->view); - if (!c->view.out_len) return true; - if (!conn_send(c, MSG_OUTPUT, c->view.out, c->view.out_len)) { + + size_t glen = 0; + const char *gfx = app_graphics_view(s->app, c->gfx, c->view_x, c->view_y, + c->cols, c->rows, &glen); + if (!c->view.out_len && !glen) { /* nothing changed: not one byte */ + app_graphics_view_commit(c->gfx, true); + return true; + } + + const size_t pre = sizeof SYNC_BEGIN - 1, post = sizeof SYNC_END - 1; + size_t len = pre + c->view.out_len + glen + post; + if (len > s->frame_cap) { + size_t cap = s->frame_cap ? s->frame_cap : 8192; + while (cap < len) cap *= 2; + char *grown = realloc(s->frame, cap); + if (!grown) return false; + s->frame = grown; + s->frame_cap = cap; + } + size_t off = 0; + memcpy(s->frame + off, SYNC_BEGIN, pre); + off += pre; + memcpy(s->frame + off, c->view.out, c->view.out_len); + off += c->view.out_len; + memcpy(s->frame + off, gfx, glen); + off += glen; + memcpy(s->frame + off, SYNC_END, post); + + bool ok = conn_send(c, MSG_OUTPUT, s->frame, len); + if (!ok) { /* The projection never entered this client's stream. Keep mouse mapping * tied to the last viewport we did enqueue, then retry this one in full. */ c->view_x = old_x; @@ -347,9 +391,9 @@ static bool push_frame(server_t *s, conn_t *c) { c->shown_cols = old_cols; c->shown_rows = old_rows; c->view.force_full = true; - return false; } - return true; + app_graphics_view_commit(c->gfx, ok); + return ok; } static bool push_clients(server_t *s) { @@ -368,9 +412,7 @@ static bool push_clients(server_t *s) { for (size_t i = 0; i < s->nconns; i++) { conn_t *c = &s->conns[i]; if (!c->display) continue; - bool frame_ok = push_frame(s, c); - bool gfx_ok = push_graphics(s, c); - if (!(frame_ok && gfx_ok)) ok = false; + if (!push_display(s, c)) ok = false; } return ok; } @@ -1099,6 +1141,7 @@ int server_run(const char *name, const char *const argv[], uint16_t cols, close(lfd); input_free(s.cmd_in); screen_free(&s.screen); + free(s.frame); app_free(s.app); return 0; } |
