Skip to content

fix(kitty-graphics): stop inline images blanking in panes (re-emit eviction + large-frame compression)#1175

Closed
abhi-easygo wants to merge 1 commit into
ogulcancelik:masterfrom
abhi-easygo:fix/kitty-graphics-reemit-blank
Closed

fix(kitty-graphics): stop inline images blanking in panes (re-emit eviction + large-frame compression)#1175
abhi-easygo wants to merge 1 commit into
ogulcancelik:masterfrom
abhi-easygo:fix/kitty-graphics-reemit-blank

Conversation

@abhi-easygo

Copy link
Copy Markdown

Summary

Fixes two bugs in the kitty_graphics virtual-placeholder re-emit path that caused inline images to go blank in herdr panes while the same images render correctly under tmux (allow-passthrough) and in bare Ghostty. Both live in src/kitty_graphics.rs; the display-placement and delete paths and the FFI/vendored lib are untouched.

Fixes #1167.

Bug 1 — mid-frame eviction of a still-referenced image (the #1167 blank-after-churn)

release_superseded_source_image (and the replaced-image branch in encode_graphics_update) could emit a=d,d=I on a host image mid-frame while a later placement in the same frame still referenced it. The guard only checked sources recorded so far (sources.values()), not all placements in the current frame. Combined with the frame-start data-filter (which leaves a later placement's data empty because the image was already uploaded), the later placement then couldn't re-upload (encode_upload_image refuses empty data → continue), leaving blank placeholder cells that persisted until the pane was recreated.

Two content-hash-identical sources are enough to trigger it: when one moves to fresh content (superseding the shared image) and the other adopts the shared content in the same frame, the shared image gets deleted before the second placement claims it.

Fix: compute frame_host_ids (host-ids referenced by all current-frame placements) up front, and never delete a host id still in that set. Applied to release_superseded_source_image; the replaced-image branch (which deletes and re-uploads its own id) instead gets an empty-data early-continue so it can't delete-then-fail-to-upload.

Bug 2 — uncompressed re-emit inflates large frames ~96×

herdr installs a PNG decoder into the vendored lib, so stored images are decoded to raw RGBA. encode_upload_image then re-transmitted that raw RGBA uncompressed (f=32). A ~278 KB PNG diagram becomes ~25.6 MB of RGBA (~34 MB base64) on every fresh upload — small diagrams stay small and render, but large ones overwhelm the outer terminal's per-transmission handling and silently drop. tmux and bare Ghostty forward the original compact PNG untouched, so they never hit this.

Fix: re-encode raw pixels back to PNG with the png crate (already a direct dependency, used for decode) and upload with f=100 — the exact compact form bare Ghostty/tmux already deliver. Non-RGBA formats and encode failures fall back to the existing raw path.

Isolation evidence

Same Neovim + snacks.nvim image module + Ghostty for all three:

Surface Bug 1 (churn) Bug 2 (large diagram)
bare Ghostty renders renders
tmux (allow-passthrough on) renders renders
herdr kitty_graphics=true (before) blanks after churn blanks
herdr (this PR) renders renders

The re-emit round-trip is the only path that touches image bytes, which is why both bugs are herdr-specific.

Verification

  • cargo build --release (zig@0.15): success.
  • cargo test --bin herdr kitty_graphics: 23 passed, 0 failed, including:
    • superseded_image_survives_when_later_placement_adopts_it_same_frame (new, Bug 1 regression)
    • encode_upload_image_reencodes_rgba_as_png (new, Bug 2: asserts f=100 control, no f=32/s=, payload < 1/10 raw size)
    • the existing lifecycle tests (shared_host_image_survives_..., replaced_image_content_deletes_..., etc.)

Confirmed end-to-end on macOS 26 / Ghostty 1.3.1: a large flowchart that previously blanked in a herdr pane now renders, and small diagrams survive edit/reload/float-open-close/buffer-switch churn.

Notes

Two bugs in the virtual-placeholder re-emit path blanked inline images in
herdr panes while the same images rendered fine under tmux and bare Ghostty.

1. Mid-frame eviction of a still-referenced image (ogulcancelik#1167). A source
   processed earlier in encode_graphics_update could delete a host image
   that a later same-frame placement still referenced, because the guard
   only checked already-recorded sources. With the frame-start data filter
   leaving the later placement's data empty, it then failed to re-upload and
   the placeholder cells stayed blank until the pane was recreated. Guard
   both deletion paths with a whole-frame reference set (frame_host_ids) and
   add an empty-data early-continue on the replaced-image branch.

2. Uncompressed re-emit inflated large frames ~96x. Stored images are
   decoded to raw RGBA and were re-transmitted uncompressed (f=32), turning
   a ~278 KB PNG into ~25.6 MB and overwhelming the outer terminal on large
   diagrams. Re-encode raw pixels back to PNG (f=100) via the png crate, the
   compact form bare Ghostty and tmux already deliver.

Fixes ogulcancelik#1167.
@kangal-bot

Copy link
Copy Markdown
Collaborator

Hi @abhi-easygo, thanks for your interest in contributing!

New contributors need maintainer approval before opening PRs. This keeps review time focused on accepted work and avoids wasted effort.

Herdr is opinionated about how it should look, feel, and work. Feature requests, ideas, questions, contribution proposals, and product-direction checks belong in Discussions, not issues.

Next steps:

  1. If this is a feature or behavior change, open a Discussion describing what you want to change and why
  2. If the work is accepted, a maintainer may convert the discussion into an issue or create a new issue for it
  3. If this is a reproducible bug, open a bug report using the issue template
  4. Wait for maintainer approval on an accepted issue before opening a PR
    Maintainers approve first-time PR paths with /approve @your-github-username on the accepted issue.
  5. Keep it concise and write in your own voice

A discussion, issue, branch, or proposed implementation does not reserve the work and does not mean the PR path is approved.

This PR will be closed automatically. See https://github.com/ogulcancelik/herdr/blob/master/CONTRIBUTING.md for more details.

@kangal-bot kangal-bot closed this Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants