From cc2d7a178970d1db01955572a6d350088d453f3c Mon Sep 17 00:00:00 2001 From: Matsumoto Kotaro Date: Sat, 30 May 2026 18:46:24 +0900 Subject: [PATCH 1/2] graphics: add memory-only storage for graphics data Add a new graphics protocol key, N=1, to request that transmitted image/frame data is kept only in memory and not written to the graphics disk cache file. This is useful for transient high-frequency updates such as video-like streams, where the latest frame is the only useful data and persisting each frame to the disk cache causes unnecessary write traffic. The implementation keeps the existing graphics cache abstraction intact: memory-only entries can still be read back by animation, composition, and frame coalescing paths. Only persistence to the disk cache file is skipped. The default behavior is unchanged when N is omitted or set to zero. --- docs/graphics-protocol.rst | 2 ++ gen/apc_parsers.py | 1 + kitty/disk-cache.c | 13 ++++++++----- kitty/disk-cache.h | 2 +- kitty/graphics.c | 19 +++++++++++-------- kitty/graphics.h | 4 ++-- kitty/parse-graphics-command.h | 8 +++++++- kitty_tests/graphics.py | 14 ++++++++++++++ tools/tui/graphics/command.go | 18 +++++++++++++++++- 9 files changed, 63 insertions(+), 18 deletions(-) diff --git a/docs/graphics-protocol.rst b/docs/graphics-protocol.rst index 227e994c1..5b212acb2 100644 --- a/docs/graphics-protocol.rst +++ b/docs/graphics-protocol.rst @@ -1051,6 +1051,8 @@ Key Value Default Description ``o`` Single character. ``null`` The type of data compression. ``only z`` ``m`` zero or one ``0`` Whether there is more chunked data available. +``N`` zero or one ``0`` If set to ``1``, keep the transmitted image or frame data in memory only, + without writing it to the graphics disk cache. **Keys for image display** ----------------------------------------------------------- diff --git a/gen/apc_parsers.py b/gen/apc_parsers.py index 51666f4a7..0d23cca6e 100755 --- a/gen/apc_parsers.py +++ b/gen/apc_parsers.py @@ -313,6 +313,7 @@ def parsers() -> None: 'U': ('unicode_placement', 'uint'), 'P': ('parent_id', 'uint'), 'Q': ('parent_placement_id', 'uint'), + 'N': ('no_disk_cache', 'uint'), 'H': ('offset_from_parent_x', 'int'), 'V': ('offset_from_parent_y', 'int'), } diff --git a/kitty/disk-cache.c b/kitty/disk-cache.c index a32c85835..088ef79f8 100644 --- a/kitty/disk-cache.c +++ b/kitty/disk-cache.c @@ -33,7 +33,7 @@ typedef struct CacheKey { typedef struct { uint8_t *data; size_t data_sz; - bool written_to_disk, uses_encryption; + bool written_to_disk, uses_encryption, memory_only; off_t pos_in_cache_file; uint8_t encryption_key[64]; } CacheValue; @@ -593,7 +593,7 @@ create_cache_entry(void) { } bool -add_to_disk_cache(PyObject *self_, const void *key, size_t key_sz, const void *data, size_t data_sz) { +add_to_disk_cache(PyObject *self_, const void *key, size_t key_sz, const void *data, size_t data_sz, bool memory_only) { DiskCache *self = (DiskCache*)self_; if (!ensure_state(self)) return false; if (key_sz > MAX_KEY_SIZE) { PyErr_SetString(PyExc_KeyError, "cache key is too long"); return false; } @@ -618,11 +618,14 @@ add_to_disk_cache(PyObject *self_, const void *key, size_t key_sz, const void *d if (s->data) free(s->data); } s->data = copied_data; s->data_sz = data_sz; copied_data = NULL; + s->memory_only = memory_only; + s->written_to_disk = memory_only; + if (memory_only) s->pos_in_cache_file = -1; self->total_size += s->data_sz; end: mutex(unlock); if (PyErr_Occurred()) return false; - wakeup_write_loop(self); + if (!memory_only) wakeup_write_loop(self); return true; } @@ -740,7 +743,7 @@ disk_cache_clear_from_ram(PyObject *self_, bool(matches)(void*, void *key, unsig mutex(lock); cache_map_for_loop(i) { CacheValue *s = i.data->val; - if (s->written_to_disk && s->data && matches(data, i.data->key.hash_key, i.data->key.hash_keylen)) { + if (s->written_to_disk && !s->memory_only && s->data && matches(data, i.data->key.hash_key, i.data->key.hash_keylen)) { free(s->data); s->data = NULL; ans++; } @@ -848,7 +851,7 @@ add(PyObject *self, PyObject *args) { const char *key, *data; Py_ssize_t keylen, datalen; PA("y#y#", &key, &keylen, &data, &datalen); - if (!add_to_disk_cache(self, key, keylen, data, datalen)) return NULL; + if (!add_to_disk_cache(self, key, keylen, data, datalen, false)) return NULL; Py_RETURN_NONE; } diff --git a/kitty/disk-cache.h b/kitty/disk-cache.h index d6c1ea26f..17538fe8c 100644 --- a/kitty/disk-cache.h +++ b/kitty/disk-cache.h @@ -9,7 +9,7 @@ #include "data-types.h" PyObject* create_disk_cache(void); -bool add_to_disk_cache(PyObject *self, const void *key, size_t key_sz, const void *data, size_t data_sz); +bool add_to_disk_cache(PyObject *self, const void *key, size_t key_sz, const void *data, size_t data_sz, bool memory_only); bool remove_from_disk_cache(PyObject *self_, const void *key, size_t key_sz); void* read_from_disk_cache(PyObject *self_, const void *key, size_t key_sz, void*(allocator)(void*, size_t), void*, bool); PyObject* read_from_disk_cache_python(PyObject *self_, const void *key, size_t key_sz, bool); diff --git a/kitty/graphics.c b/kitty/graphics.c index 7365a0223..b56481e21 100644 --- a/kitty/graphics.c +++ b/kitty/graphics.c @@ -41,9 +41,9 @@ cache_key(const ImageAndFrame x, char *key) { #define CK(x) key, cache_key(x, key) static bool -add_to_cache(GraphicsManager *self, const ImageAndFrame x, const void *data, const size_t sz) { +add_to_cache(GraphicsManager *self, const ImageAndFrame x, const void *data, const size_t sz, bool memory_only) { char key[CACHE_KEY_BUFFER_SIZE]; - return add_to_disk_cache(self->disk_cache, CK(x), data, sz); + return add_to_disk_cache(self->disk_cache, CK(x), data, sz, memory_only); } static bool @@ -768,11 +768,12 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_ .is_opaque = self->currently_loading.is_opaque, .is_4byte_aligned = self->currently_loading.is_4byte_aligned, .width = img->width, .height = img->height, + .memory_only = !!g->no_disk_cache, }; if (!is_query) { - if (!add_to_cache(self, (const ImageAndFrame){.image_id = img->internal_id, .frame_id=img->root_frame.id}, self->currently_loading.data, self->currently_loading.data_sz)) { + if (!add_to_cache(self, (const ImageAndFrame){.image_id = img->internal_id, .frame_id=img->root_frame.id}, self->currently_loading.data, self->currently_loading.data_sz, img->root_frame.memory_only)) { if (PyErr_Occurred()) PyErr_Print(); - ABRT("ENOSPC", "Failed to store image data in disk cache"); + ABRT("ENOSPC", "Failed to store image data in cache"); } upload_to_gpu(self, img, img->root_frame.is_opaque, img->root_frame.is_4byte_aligned, self->currently_loading.data); self->used_storage += required_sz; @@ -1595,6 +1596,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I .alpha_blend = g->compose_mode != 1 && !load_data->is_opaque, .gap = g->gap > 0 ? g->gap : (g->gap < 0) ? 0 : DEFAULT_GAP, .bgcolor = g->bgcolor, + .memory_only = !!g->no_disk_cache, }; Frame *frame; if (is_new_frame) { @@ -1635,7 +1637,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I } } *frame = transmitted_frame; - if (!add_to_cache(self, key, load_data->data, load_data->data_sz)) { + if (!add_to_cache(self, key, load_data->data, load_data->data_sz, frame->memory_only)) { img->extra_framecnt--; if (PyErr_Occurred()) PyErr_Print(); ABRT("ENOSPC", "Failed to cache data for image frame"); @@ -1651,6 +1653,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I if (g->gap != 0) change_gap(img, frame, transmitted_frame.gap); CoalescedFrameData cfd = get_coalesced_frame_data(self, img, frame); if (!cfd.buf) ABRT("EINVAL", "No data associated with frame number: %u", frame_number); + frame->memory_only = transmitted_frame.memory_only; frame->alpha_blend = false; frame->base_frame_id = 0; frame->bgcolor = 0; frame->is_opaque = cfd.is_opaque; frame->is_4byte_aligned = cfd.is_4byte_aligned; frame->x = 0; frame->y = 0; frame->width = img->width; frame->height = img->height; @@ -1664,7 +1667,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I }; compose(d, cfd.buf, load_data->data); const ImageAndFrame key = { .image_id = img->internal_id, .frame_id = frame->id }; - bool added = add_to_cache(self, key, cfd.buf, (size_t)bytes_per_pixel * frame->width * frame->height); + bool added = add_to_cache(self, key, cfd.buf, (size_t)bytes_per_pixel * frame->width * frame->height, frame->memory_only); if (added && frame == current_frame(img)) { update_current_frame(self, img, &cfd); *is_dirty = true; @@ -1868,9 +1871,9 @@ handle_compose_command(GraphicsManager *self, bool *is_dirty, const GraphicsComm }; compose_rectangles(d, dest_data.buf, src_data.buf); const ImageAndFrame key = { .image_id = img->internal_id, .frame_id = dest_frame->id }; - if (!add_to_cache(self, key, dest_data.buf, ((size_t)(dest_data.is_opaque ? 3 : 4)) * img->width * img->height)) { + if (!add_to_cache(self, key, dest_data.buf, ((size_t)(dest_data.is_opaque ? 3 : 4)) * img->width * img->height, dest_frame->memory_only)) { if (PyErr_Occurred()) PyErr_Print(); - set_command_failed_response("ENOSPC", "Failed to store image data in disk cache"); + set_command_failed_response("ENOSPC", "Failed to store image data in cache"); } // frame is now a fully coalesced frame dest_frame->x = 0; dest_frame->y = 0; dest_frame->width = img->width; dest_frame->height = img->height; diff --git a/kitty/graphics.h b/kitty/graphics.h index 0d348abc1..36d2b303f 100644 --- a/kitty/graphics.h +++ b/kitty/graphics.h @@ -10,7 +10,7 @@ typedef struct { unsigned char action, transmission_type, compressed, delete_action; - uint32_t format, more, id, image_number, data_sz, data_offset, placement_id, quiet, parent_id, parent_placement_id; + uint32_t format, more, id, image_number, data_sz, data_offset, placement_id, quiet, parent_id, parent_placement_id, no_disk_cache; uint32_t width, height, x_offset, y_offset; union { uint32_t cursor_movement, compose_mode; }; union { uint32_t cell_x_offset; }; @@ -71,7 +71,7 @@ typedef struct { typedef struct { uint32_t gap, id, width, height, x, y, base_frame_id, bgcolor; - bool is_opaque, is_4byte_aligned, alpha_blend; + bool is_opaque, is_4byte_aligned, alpha_blend, memory_only; } Frame; typedef enum { ANIMATION_STOPPED = 0, ANIMATION_LOADING = 1, ANIMATION_RUNNING = 2} AnimationState; diff --git a/kitty/parse-graphics-command.h b/kitty/parse-graphics-command.h index 092740f80..0bd92181d 100644 --- a/kitty/parse-graphics-command.h +++ b/kitty/parse-graphics-command.h @@ -46,6 +46,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf, unicode_placement = 'U', parent_id = 'P', parent_placement_id = 'Q', + no_disk_cache = 'N', offset_from_parent_x = 'H', offset_from_parent_y = 'V' }; @@ -141,6 +142,9 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf, case parent_placement_id: value_state = UINT; break; + case no_disk_cache: + value_state = UINT; + break; case offset_from_parent_x: value_state = INT; break; @@ -299,6 +303,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf, U(unicode_placement); U(parent_id); U(parent_placement_id); + U(no_disk_cache); default: break; } @@ -359,7 +364,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf, REPORT_VA_COMMAND( "K s {sc sc sc sc sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI " - "sI sI sI sI si si si ss#}", + "sI sI sI sI sI si si si ss#}", self->window_id, "graphics_command", "action", g.action, "delete_action", g.delete_action, "transmission_type", @@ -379,6 +384,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf, (unsigned int)g.cursor_movement, "unicode_placement", (unsigned int)g.unicode_placement, "parent_id", (unsigned int)g.parent_id, "parent_placement_id", (unsigned int)g.parent_placement_id, + "no_disk_cache", (unsigned int)g.no_disk_cache, "z_index", (int)g.z_index, "offset_from_parent_x", (int)g.offset_from_parent_x, "offset_from_parent_y", diff --git a/kitty_tests/graphics.py b/kitty_tests/graphics.py index eeaafd23b..ce8f1202f 100644 --- a/kitty_tests/graphics.py +++ b/kitty_tests/graphics.py @@ -386,6 +386,20 @@ class TestGraphics(BaseTest): self.assertIsNone(li(payload='2' * 12, z=77, m=1, q=2)) self.assertIsNone(li(payload='2' * 12)) + def test_no_disk_cache_graphics_image(self): + s, g, pl, sl = load_helpers(self) + self.assertEqual(g.disk_cache.end_of_data_offset(), 0) + self.ae(pl('abc', s=1, v=1, f=24, N=1), 'OK') + self.assertTrue(g.disk_cache.wait_for_write()) + self.assertEqual(g.disk_cache.end_of_data_offset(), 0) + img = g.image_for_client_id(1) + self.assertIsNotNone(img) + self.ae(img['data'], b'abc') + + self.ae(pl('def', s=1, v=1, f=24, i=2), 'OK') + self.assertTrue(g.disk_cache.wait_for_write()) + self.assertGreater(g.disk_cache.end_of_data_offset(), 0) + def test_load_images(self): s, g, pl, sl = load_helpers(self) self.assertEqual(g.disk_cache.total_size, 0) diff --git a/tools/tui/graphics/command.go b/tools/tui/graphics/command.go index fb2e485fa..247d60782 100644 --- a/tools/tui/graphics/command.go +++ b/tools/tui/graphics/command.go @@ -143,7 +143,7 @@ type GraphicsCommand struct { d GRT_d U GRT_U - s, v, S, O, x, y, w, h, X, Y, c, r uint64 + s, v, S, O, x, y, w, h, X, Y, c, r, N uint64 i, I, p uint32 @@ -176,6 +176,7 @@ func (self *GraphicsCommand) serialize_non_default_fields() (ans []string) { write_key('U', self.U, null.U) write_key('d', self.d, null.d) + write_key('N', self.N, null.N) write_key('s', self.s, null.s) write_key('v', self.v, null.v) write_key('S', self.S, null.S) @@ -376,6 +377,8 @@ func (self *GraphicsCommand) SetString(key byte, value string) (err error) { err = set_val(&self.U, GRT_U_from_string, value) case 'd': err = set_val(&self.d, GRT_d_from_string, value) + case 'N': + err = set_uval(&self.N, value) case 's': err = set_uval(&self.s, value) case 'v': @@ -753,6 +756,19 @@ func (self *GraphicsCommand) SetFrameToMakeCurrent(c uint64) *GraphicsCommand { return self } +func (self *GraphicsCommand) NoDiskCache() bool { + return self.N != 0 +} + +func (self *GraphicsCommand) SetNoDiskCache(noDiskCache bool) *GraphicsCommand { + if noDiskCache { + self.N = 1 + } else { + self.N = 0 + } + return self +} + func (self *GraphicsCommand) ImageId() uint32 { return self.i } From 89946ebc078a79656d37b5424b8387e44dd2a908 Mon Sep 17 00:00:00 2001 From: Matsumoto Kotaro Date: Fri, 19 Jun 2026 13:18:41 +0900 Subject: [PATCH 2/2] graphics: make N a transient usage-hints bitmask Change the graphics protocol N key from a boolean into a usage-hints bitmask. Define the first bit as a transient hint, allowing the terminal to treat the image data as short-lived and apply optimizations such as skipping disk cache writes. Propagate the transient hint through frame coalescing and composition, so a composed frame is transient if any contributing frame is transient. --- docs/graphics-protocol.rst | 12 ++++++++++-- gen/apc_parsers.py | 2 +- kitty/graphics.c | 34 ++++++++++++++++++++++++++-------- kitty/graphics.h | 7 +++++-- kitty/parse-graphics-command.h | 10 +++++----- kitty_tests/graphics.py | 2 +- kitty_tests/parser.py | 3 ++- tools/tui/graphics/command.go | 12 ++++-------- 8 files changed, 54 insertions(+), 28 deletions(-) diff --git a/docs/graphics-protocol.rst b/docs/graphics-protocol.rst index 5b212acb2..925beb688 100644 --- a/docs/graphics-protocol.rst +++ b/docs/graphics-protocol.rst @@ -1051,8 +1051,16 @@ Key Value Default Description ``o`` Single character. ``null`` The type of data compression. ``only z`` ``m`` zero or one ``0`` Whether there is more chunked data available. -``N`` zero or one ``0`` If set to ``1``, keep the transmitted image or frame data in memory only, - without writing it to the graphics disk cache. +``N`` bitmask ``0`` Usage hints from the client to the terminal about the intended use of + the image. Only one hint is currently defined, the ``1`` bit which means + *transient*. The terminal is free to assume that an image with this hint + will be used for only a short time, and so may, for example, evict its + data before other images when the image is soft deleted, has no visible + placements and the terminal is under storage pressure, or skip writing + its data to disk. The terminal is also free to ignore the hint. If an + animation frame with the *transient* hint is composited onto another + frame, and any of the involved frames have the hint, the resulting + composited frame also has the hint. **Keys for image display** ----------------------------------------------------------- diff --git a/gen/apc_parsers.py b/gen/apc_parsers.py index 0d23cca6e..a980638eb 100755 --- a/gen/apc_parsers.py +++ b/gen/apc_parsers.py @@ -313,7 +313,7 @@ def parsers() -> None: 'U': ('unicode_placement', 'uint'), 'P': ('parent_id', 'uint'), 'Q': ('parent_placement_id', 'uint'), - 'N': ('no_disk_cache', 'uint'), + 'N': ('usage_hints', 'uint'), 'H': ('offset_from_parent_x', 'int'), 'V': ('offset_from_parent_y', 'int'), } diff --git a/kitty/graphics.c b/kitty/graphics.c index b56481e21..9705ea8a4 100644 --- a/kitty/graphics.c +++ b/kitty/graphics.c @@ -768,10 +768,10 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_ .is_opaque = self->currently_loading.is_opaque, .is_4byte_aligned = self->currently_loading.is_4byte_aligned, .width = img->width, .height = img->height, - .memory_only = !!g->no_disk_cache, + .transient = (g->usage_hints & GRAPHICS_USAGE_HINT_TRANSIENT) != 0, }; if (!is_query) { - if (!add_to_cache(self, (const ImageAndFrame){.image_id = img->internal_id, .frame_id=img->root_frame.id}, self->currently_loading.data, self->currently_loading.data_sz, img->root_frame.memory_only)) { + if (!add_to_cache(self, (const ImageAndFrame){.image_id = img->internal_id, .frame_id=img->root_frame.id}, self->currently_loading.data, self->currently_loading.data_sz, img->root_frame.transient)) { if (PyErr_Occurred()) PyErr_Print(); ABRT("ENOSPC", "Failed to store image data in cache"); } @@ -1355,7 +1355,7 @@ change_gap(Image *img, Frame *f, int32_t gap) { typedef struct { uint8_t *buf; - bool is_4byte_aligned, is_opaque; + bool is_4byte_aligned, is_opaque, transient; } CoalescedFrameData; static void @@ -1451,6 +1451,7 @@ compose(const ComposeData d, uint8_t *under_data, const uint8_t *over_data) { static CoalescedFrameData get_coalesced_frame_data_standalone(const Image *img, const Frame *f, uint8_t *frame_data) { CoalescedFrameData ans = {0}; + ans.transient = f->transient; bool is_full_frame = f->width == img->width && f->height == img->height && !f->x && !f->y; if (is_full_frame) { ans.buf = frame_data; @@ -1514,6 +1515,7 @@ get_coalesced_frame_data_impl(GraphicsManager *self, Image *img, const Frame *f, }; compose(d, base_data.buf, frame_data); free(frame_data); + base_data.transient = base_data.transient || f->transient; return base_data; } @@ -1554,6 +1556,17 @@ reference_chain_too_large(Image *img, const Frame *frame) { return num >= 5 || drawn_area >= limit; } +static bool +frame_chain_is_transient(Image *img, const Frame *frame) { + // matches the recursion depth limit in get_coalesced_frame_data_impl + unsigned num = 0; + while (frame) { + if (frame->transient) return true; + if (!frame->base_frame_id || ++num > 32 || !(frame = frame_for_id(img, frame->base_frame_id))) break; + } + return false; +} + static Image* handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, Image *img, const uint8_t *payload, bool *is_dirty) { uint32_t frame_number = g->frame_number, fmt = g->format ? g->format : RGBA; @@ -1596,7 +1609,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I .alpha_blend = g->compose_mode != 1 && !load_data->is_opaque, .gap = g->gap > 0 ? g->gap : (g->gap < 0) ? 0 : DEFAULT_GAP, .bgcolor = g->bgcolor, - .memory_only = !!g->no_disk_cache, + .transient = (g->usage_hints & GRAPHICS_USAGE_HINT_TRANSIENT) != 0, }; Frame *frame; if (is_new_frame) { @@ -1632,12 +1645,14 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I transmitted_frame.x = 0; transmitted_frame.y = 0; transmitted_frame.is_4byte_aligned = cfd.is_4byte_aligned; transmitted_frame.is_opaque = cfd.is_opaque; + transmitted_frame.transient = transmitted_frame.transient || cfd.transient; } else { transmitted_frame.base_frame_id = other_frame->id; + transmitted_frame.transient = transmitted_frame.transient || frame_chain_is_transient(img, other_frame); } } *frame = transmitted_frame; - if (!add_to_cache(self, key, load_data->data, load_data->data_sz, frame->memory_only)) { + if (!add_to_cache(self, key, load_data->data, load_data->data_sz, frame->transient)) { img->extra_framecnt--; if (PyErr_Occurred()) PyErr_Print(); ABRT("ENOSPC", "Failed to cache data for image frame"); @@ -1653,7 +1668,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I if (g->gap != 0) change_gap(img, frame, transmitted_frame.gap); CoalescedFrameData cfd = get_coalesced_frame_data(self, img, frame); if (!cfd.buf) ABRT("EINVAL", "No data associated with frame number: %u", frame_number); - frame->memory_only = transmitted_frame.memory_only; + frame->transient = cfd.transient || transmitted_frame.transient; frame->alpha_blend = false; frame->base_frame_id = 0; frame->bgcolor = 0; frame->is_opaque = cfd.is_opaque; frame->is_4byte_aligned = cfd.is_4byte_aligned; frame->x = 0; frame->y = 0; frame->width = img->width; frame->height = img->height; @@ -1667,7 +1682,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I }; compose(d, cfd.buf, load_data->data); const ImageAndFrame key = { .image_id = img->internal_id, .frame_id = frame->id }; - bool added = add_to_cache(self, key, cfd.buf, (size_t)bytes_per_pixel * frame->width * frame->height, frame->memory_only); + bool added = add_to_cache(self, key, cfd.buf, (size_t)bytes_per_pixel * frame->width * frame->height, frame->transient); if (added && frame == current_frame(img)) { update_current_frame(self, img, &cfd); *is_dirty = true; @@ -1870,10 +1885,13 @@ handle_compose_command(GraphicsManager *self, bool *is_dirty, const GraphicsComm .stride = img->width }; compose_rectangles(d, dest_data.buf, src_data.buf); + bool transient = src_data.transient || dest_data.transient; const ImageAndFrame key = { .image_id = img->internal_id, .frame_id = dest_frame->id }; - if (!add_to_cache(self, key, dest_data.buf, ((size_t)(dest_data.is_opaque ? 3 : 4)) * img->width * img->height, dest_frame->memory_only)) { + if (!add_to_cache(self, key, dest_data.buf, ((size_t)(dest_data.is_opaque ? 3 : 4)) * img->width * img->height, transient)) { if (PyErr_Occurred()) PyErr_Print(); set_command_failed_response("ENOSPC", "Failed to store image data in cache"); + } else { + dest_frame->transient = transient; } // frame is now a fully coalesced frame dest_frame->x = 0; dest_frame->y = 0; dest_frame->width = img->width; dest_frame->height = img->height; diff --git a/kitty/graphics.h b/kitty/graphics.h index 36d2b303f..90a6834ba 100644 --- a/kitty/graphics.h +++ b/kitty/graphics.h @@ -8,9 +8,12 @@ #include "data-types.h" #include "monotonic.h" +// Bitmask values for GraphicsCommand.usage_hints +#define GRAPHICS_USAGE_HINT_TRANSIENT 1u + typedef struct { unsigned char action, transmission_type, compressed, delete_action; - uint32_t format, more, id, image_number, data_sz, data_offset, placement_id, quiet, parent_id, parent_placement_id, no_disk_cache; + uint32_t format, more, id, image_number, data_sz, data_offset, placement_id, quiet, parent_id, parent_placement_id, usage_hints; uint32_t width, height, x_offset, y_offset; union { uint32_t cursor_movement, compose_mode; }; union { uint32_t cell_x_offset; }; @@ -71,7 +74,7 @@ typedef struct { typedef struct { uint32_t gap, id, width, height, x, y, base_frame_id, bgcolor; - bool is_opaque, is_4byte_aligned, alpha_blend, memory_only; + bool is_opaque, is_4byte_aligned, alpha_blend, transient; } Frame; typedef enum { ANIMATION_STOPPED = 0, ANIMATION_LOADING = 1, ANIMATION_RUNNING = 2} AnimationState; diff --git a/kitty/parse-graphics-command.h b/kitty/parse-graphics-command.h index 0bd92181d..68bf70b0d 100644 --- a/kitty/parse-graphics-command.h +++ b/kitty/parse-graphics-command.h @@ -46,7 +46,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf, unicode_placement = 'U', parent_id = 'P', parent_placement_id = 'Q', - no_disk_cache = 'N', + usage_hints = 'N', offset_from_parent_x = 'H', offset_from_parent_y = 'V' }; @@ -142,7 +142,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf, case parent_placement_id: value_state = UINT; break; - case no_disk_cache: + case usage_hints: value_state = UINT; break; case offset_from_parent_x: @@ -303,7 +303,7 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf, U(unicode_placement); U(parent_id); U(parent_placement_id); - U(no_disk_cache); + U(usage_hints); default: break; } @@ -383,8 +383,8 @@ static inline void parse_graphics_code(PS *self, uint8_t *parser_buf, "cell_y_offset", (unsigned int)g.cell_y_offset, "cursor_movement", (unsigned int)g.cursor_movement, "unicode_placement", (unsigned int)g.unicode_placement, "parent_id", (unsigned int)g.parent_id, - "parent_placement_id", (unsigned int)g.parent_placement_id, - "no_disk_cache", (unsigned int)g.no_disk_cache, + "parent_placement_id", (unsigned int)g.parent_placement_id, "usage_hints", + (unsigned int)g.usage_hints, "z_index", (int)g.z_index, "offset_from_parent_x", (int)g.offset_from_parent_x, "offset_from_parent_y", diff --git a/kitty_tests/graphics.py b/kitty_tests/graphics.py index ce8f1202f..74cab1c00 100644 --- a/kitty_tests/graphics.py +++ b/kitty_tests/graphics.py @@ -386,7 +386,7 @@ class TestGraphics(BaseTest): self.assertIsNone(li(payload='2' * 12, z=77, m=1, q=2)) self.assertIsNone(li(payload='2' * 12)) - def test_no_disk_cache_graphics_image(self): + def test_transient_graphics_image(self): s, g, pl, sl = load_helpers(self) self.assertEqual(g.disk_cache.end_of_data_offset(), 0) self.ae(pl('abc', s=1, v=1, f=24, N=1), 'OK') diff --git a/kitty_tests/parser.py b/kitty_tests/parser.py index 45e025e54..8685621c3 100644 --- a/kitty_tests/parser.py +++ b/kitty_tests/parser.py @@ -920,7 +920,7 @@ class TestParser(BaseTest): k.setdefault(f, b'\0') for f in ('format more id data_sz data_offset width height x_offset y_offset data_height data_width cursor_movement' ' num_cells num_lines cell_x_offset cell_y_offset z_index placement_id image_number quiet unicode_placement' - ' parent_id parent_placement_id offset_from_parent_x offset_from_parent_y' + ' parent_id parent_placement_id usage_hints offset_from_parent_x offset_from_parent_y' ).split(): k.setdefault(f, 0) p = k.pop('payload', '') @@ -943,6 +943,7 @@ class TestParser(BaseTest): t('a=t,t=d,s=100,z=-9', payload='X', action='t', transmission_type='d', data_width=100, z_index=-9) t('a=t,t=d,s=100,z=9', payload='payload', action='t', transmission_type='d', data_width=100, z_index=9) t('a=t,t=d,s=100,z=9,q=2', action='t', transmission_type='d', data_width=100, z_index=9, quiet=2) + t('N=1', usage_hints=1) e(',s=1', 'Malformed GraphicsCommand control block, invalid key character: 0x2c') e('W=1', 'Malformed GraphicsCommand control block, invalid key character: 0x57') e('1=1', 'Malformed GraphicsCommand control block, invalid key character: 0x31') diff --git a/tools/tui/graphics/command.go b/tools/tui/graphics/command.go index 247d60782..c3e57141e 100644 --- a/tools/tui/graphics/command.go +++ b/tools/tui/graphics/command.go @@ -756,16 +756,12 @@ func (self *GraphicsCommand) SetFrameToMakeCurrent(c uint64) *GraphicsCommand { return self } -func (self *GraphicsCommand) NoDiskCache() bool { - return self.N != 0 +func (self *GraphicsCommand) UsageHints() uint64 { + return self.N } -func (self *GraphicsCommand) SetNoDiskCache(noDiskCache bool) *GraphicsCommand { - if noDiskCache { - self.N = 1 - } else { - self.N = 0 - } +func (self *GraphicsCommand) SetUsageHints(hints uint64) *GraphicsCommand { + self.N = hints return self }