diff options
| author | user <user@clank> | 2026-09-07 15:52:00 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-07 15:52:00 +0200 |
| commit | e9147fba11b735259a90089dd7207853ff49a3ad (patch) | |
| tree | 6dc86b56987c7bb2e2dba4a3151cf4576efab2b0 /src/graphics.c | |
| parent | screen: commit a wide cell's tail to prev, or the diff leaves a hole (diff) | |
| parent | app: cut the modals, toasts and splash out of the images under them (diff) | |
| download | slosh-e9147fba11b735259a90089dd7207853ff49a3ad.tar.gz slosh-e9147fba11b735259a90089dd7207853ff49a3ad.tar.xz slosh-e9147fba11b735259a90089dd7207853ff49a3ad.zip | |
Merge branch 'graphics-fixes' (PR #2 from iksteen): five fixes to the kitty passthrough, found through a mirror
One synchronized message per frame with cells and images together, a
placement said once rather than every frame, z=0 stated so a mirror
knows where the picture goes, a float cutting an image into pieces
instead of erasing it, and overlays cut out of the images under them.
Found running slosh inside shellglass, real on a terminal too.
Diffstat (limited to 'src/graphics.c')
| -rw-r--r-- | src/graphics.c | 96 |
1 files changed, 84 insertions, 12 deletions
diff --git a/src/graphics.c b/src/graphics.c index ac8c244..77b3b7f 100644 --- a/src/graphics.c +++ b/src/graphics.c @@ -18,7 +18,7 @@ * pinned to a spot on the screen no scrolling will move. The queue survives * until gfx_commit() says the client actually heard it. */ typedef struct { - uint32_t out_id, place_id; + uint32_t out_id, wire_id; } gfx_dead_t; struct graphics { @@ -27,11 +27,16 @@ struct graphics { gfx_place_t *places; size_t nplaces, placecap; uint32_t next_out_id; + uint32_t next_wire_id; /* placement ids we hand out; see gfx_place_t */ gfx_dead_t *dead; /* deletions owed to the client */ size_t ndead, deadcap; size_t *txed; /* imgs[] indices transmitted in the frame being built */ size_t ntxed, txedcap; + /* places[] indices emitted in the frame being built, for the same reason: + * a placement the client never heard is one it is not holding. */ + size_t *placed; + size_t nplaced, placedcap; char *out; size_t len, cap; @@ -81,6 +86,7 @@ static void out_b64(graphics_t *g, const uint8_t *in, size_t len) { graphics_t *gfx_new(void) { graphics_t *g = calloc(1, sizeof *g); g->next_out_id = OUT_ID_BASE; + g->next_wire_id = 1; return g; } @@ -90,6 +96,7 @@ void gfx_free(graphics_t *g) { free(g->places); free(g->dead); free(g->txed); + free(g->placed); free(g->out); free(g); } @@ -100,6 +107,16 @@ void gfx_reset(graphics_t *g) { /* A fresh client has no placements to delete and has seen no frame. */ g->ndead = 0; g->ntxed = 0; + g->nplaced = 0; +} + +void gfx_repaint(graphics_t *g) { + /* The client's screen was cleared, so it is holding no placements: say them + * all again. Not gfx_reset(): the terminal keeps the pixels it was sent + * (an id-keyed image survives a clear; only its placements go), and a + * deletion still owed from a dropped frame is still owed -- forgetting one + * here is a picture parked on the client's screen for good. */ + for (size_t i = 0; i < g->nplaces; i++) g->places[i].shown = false; } void gfx_forget_pane(graphics_t *g, uint32_t pane) { @@ -113,7 +130,7 @@ void gfx_begin(graphics_t *g) { /* A flushed frame nobody committed was never delivered: the `graphics` * control command renders the stream for a CLI, not for the client, and * anything it claimed to transmit the client still has not seen. */ - if (g->ntxed) gfx_commit(g, false); + if (g->ntxed || g->nplaced) gfx_commit(g, false); for (size_t i = 0; i < g->nplaces; i++) g->places[i].live = false; g->len = 0; if (g->out) g->out[0] = 0; @@ -198,14 +215,18 @@ void gfx_place(graphics_t *g, const gfx_req_t *req) { img->gen = gen; img->sent = false; } - if (!img->sent) + bool transmitted = false; + if (!img->sent) { transmit(g, img, req->px_w, req->px_h, req->format, req->compression, req->data, req->data_len); + transmitted = img->sent; + } uint32_t pid = req->place_id ? req->place_id : 1; gfx_place_t *slot = NULL; for (size_t i = 0; i < g->nplaces; i++) - if (g->places[i].out_id == img->out_id && g->places[i].place_id == pid) + if (g->places[i].out_id == img->out_id && g->places[i].place_id == pid && + g->places[i].piece == req->piece) slot = &g->places[i]; if (!slot) { if (g->nplaces == g->placecap) { @@ -216,7 +237,25 @@ void gfx_place(graphics_t *g, const gfx_req_t *req) { memset(slot, 0, sizeof *slot); slot->out_id = img->out_id; slot->place_id = pid; + slot->piece = req->piece; + slot->wire_id = g->next_wire_id++; } + /* What the client is holding, against what it should be holding. A + * placement that has not moved, resized or re-cropped is left alone: see + * gfx_flush() for why saying it again is not free. A retransmit invalidates + * it too -- kitty deletes an image's placements when its data is replaced, + * so the picture is gone until we place it once more. + * + * Field by field rather than a memcmp of the struct: the padding a compiler + * is free to leave between these members is not initialised by an + * assignment, so a memcmp compares it and reports a change that isn't one -- + * which here would mean re-placing every image every frame, silently, again. */ + bool same = slot->shown && !transmitted && slot->col == req->col && + slot->row == req->row && slot->cols == req->cols && + slot->rows == req->rows && slot->x_off == req->x_off && + slot->y_off == req->y_off && slot->scale_cols == req->scale_cols && + slot->scale_rows == req->scale_rows && slot->sx == req->sx && + slot->sy == req->sy && slot->sw == req->sw && slot->sh == req->sh; slot->col = req->col; slot->row = req->row; slot->cols = req->cols; @@ -230,6 +269,7 @@ void gfx_place(graphics_t *g, const gfx_req_t *req) { slot->sw = req->sw; slot->sh = req->sh; slot->live = true; + slot->shown = same; } char *gfx_flush(graphics_t *g, size_t *out_len) { @@ -264,14 +304,14 @@ char *gfx_flush(graphics_t *g, size_t *out_len) { } bool queued = false; for (size_t j = 0; j < g->ndead; j++) - if (g->dead[j].out_id == p->out_id && g->dead[j].place_id == p->place_id) + if (g->dead[j].out_id == p->out_id && g->dead[j].wire_id == p->wire_id) queued = true; if (!queued) { if (g->ndead == g->deadcap) { g->deadcap = g->deadcap ? g->deadcap * 2 : 8; g->dead = realloc(g->dead, g->deadcap * sizeof *g->dead); } - g->dead[g->ndead++] = (gfx_dead_t){p->out_id, p->place_id}; + g->dead[g->ndead++] = (gfx_dead_t){p->out_id, p->wire_id}; } } g->nplaces = keep; @@ -280,14 +320,42 @@ char *gfx_flush(graphics_t *g, size_t *out_len) { * came back is deleted and re-placed in the same stream, which nets out. */ for (size_t i = 0; i < g->ndead; i++) out_fmt(g, "\x1b_Ga=d,d=i,q=2,i=%u,p=%u\x1b\\", g->dead[i].out_id, - g->dead[i].place_id); + g->dead[i].wire_id); - /* Then (re)place everything that is. C=1 keeps the cursor where the text - * renderer left it, which matters because we are interleaving with a diff. */ + /* Then place what the client is not already holding. C=1 keeps the cursor + * where the text renderer left it, which matters because we are + * interleaving with a diff. + * + * Only the placements that changed. A kitty placement is a standing + * instruction, not a per-frame one: "the other commands to erase text must + * have no effect on graphics", so a repaint of the cells under a picture + * does not disturb it and re-placing it says nothing new. It is not free, + * though. A terminal mirroring the stream cannot know that the identical + * placement is identical -- it re-decodes the image and holds the picture + * back until that finishes, so re-placing at frame rate meant the mirrored + * copy was mid-decode nearly always and flickered or vanished, while the + * local terminal (which keeps the pixels) looked fine. That was the bug + * this diff exists for, and it costs nothing on the terminal path either: + * a still picture on a busy screen now costs zero bytes. */ for (size_t i = 0; i < g->nplaces; i++) { gfx_place_t *p = &g->places[i]; + if (p->shown) continue; + if (g->nplaced == g->placedcap) { + g->placedcap = g->placedcap ? g->placedcap * 2 : 8; + g->placed = realloc(g->placed, g->placedcap * sizeof *g->placed); + } + g->placed[g->nplaced++] = i; + p->shown = true; out_fmt(g, "\x1b[%u;%uH", p->row + 1, p->col + 1); - out_fmt(g, "\x1b_Ga=p,q=2,C=1,i=%u,p=%u", p->out_id, p->place_id); + /* z=0 is kitty's own default, so saying it changes nothing on a terminal + * -- and it is the whole picture for anything mirroring the stream, which + * cannot otherwise tell "the emitter wants this over the text" from "the + * emitter said nothing". Left unsaid, a mirror is free to paint the image + * under the cells, and the first pane-wide background to land on those + * cells (dim_unfocused paints one over every cell it touches) hides the + * picture completely -- on the mirrored copy only, while the terminal, + * which took the default, still shows it. */ + out_fmt(g, "\x1b_Ga=p,q=2,C=1,z=0,i=%u,p=%u", p->out_id, p->wire_id); /* c=/r= mean *scale into this many cells*, so they are passed on only * when the program asked for them. Sending the cell count a natural-size * image happens to cover looks identical in a still picture and makes a @@ -326,12 +394,16 @@ void gfx_commit(graphics_t *g, bool delivered) { } else { /* The transmissions in that frame never happened as far as the client * is concerned; forget we made them so the next frame sends them again. - * Deletions stay queued for the same reason. Placements need nothing: - * every live one is re-emitted every frame anyway. */ + * Deletions stay queued for the same reason, and so do the placements: + * now that a placement is said once rather than every frame, a placement + * that left with a dropped frame is a picture the client never draws. */ for (size_t i = 0; i < g->ntxed; i++) if (g->txed[i] < g->nimgs) g->imgs[g->txed[i]].sent = false; + for (size_t i = 0; i < g->nplaced; i++) + if (g->placed[i] < g->nplaces) g->places[g->placed[i]].shown = false; } g->ntxed = 0; + g->nplaced = 0; } size_t gfx_placements(const graphics_t *g, const gfx_place_t **out) { |
