diff --git a/docs/graphics-protocol.rst b/docs/graphics-protocol.rst index 227e994c1..925beb688 100644 --- a/docs/graphics-protocol.rst +++ b/docs/graphics-protocol.rst @@ -1051,6 +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`` 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 51666f4a7..a980638eb 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': ('usage_hints', '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 a15d886d3..2a1d8e2d3 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 3b5a971b1..c19c67812 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..9705ea8a4 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, + .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)) { + 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 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; @@ -1354,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 @@ -1450,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; @@ -1513,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; } @@ -1553,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; @@ -1595,6 +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, + .transient = (g->usage_hints & GRAPHICS_USAGE_HINT_TRANSIENT) != 0, }; Frame *frame; if (is_new_frame) { @@ -1630,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)) { + 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"); @@ -1651,6 +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->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; @@ -1664,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); + 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; @@ -1867,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)) { + 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 disk cache"); + 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 0d348abc1..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; + 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; + 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 092740f80..68bf70b0d 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', + usage_hints = '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 usage_hints: + 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(usage_hints); 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", @@ -378,7 +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, + "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 eeaafd23b..74cab1c00 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_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') + 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/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 fb2e485fa..c3e57141e 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,15 @@ func (self *GraphicsCommand) SetFrameToMakeCurrent(c uint64) *GraphicsCommand { return self } +func (self *GraphicsCommand) UsageHints() uint64 { + return self.N +} + +func (self *GraphicsCommand) SetUsageHints(hints uint64) *GraphicsCommand { + self.N = hints + return self +} + func (self *GraphicsCommand) ImageId() uint32 { return self.i }