Merge branch 'feat/10090-graphics-cache-dir' of https://github.com/Mekann2904/kitty

This commit is contained in:
Kovid Goyal
2026-06-19 10:16:10 +05:30
10 changed files with 92 additions and 21 deletions

View File

@@ -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**
-----------------------------------------------------------

View File

@@ -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'),
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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",

View File

@@ -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)

View File

@@ -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')

View File

@@ -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
}