From cc2d7a178970d1db01955572a6d350088d453f3c Mon Sep 17 00:00:00 2001 From: Matsumoto Kotaro Date: Sat, 30 May 2026 18:46:24 +0900 Subject: [PATCH] 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 }