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 }