diff --git a/kitty/graphics.c b/kitty/graphics.c index 48562db55..c619018f4 100644 --- a/kitty/graphics.c +++ b/kitty/graphics.c @@ -28,7 +28,7 @@ PyTypeObject GraphicsManager_Type; static inline size_t cache_key(const ImageAndFrame x, char *key) { - if (x.frame_idx) return snprintf(key, CACHE_KEY_BUFFER_SIZE, "%llx:%x", x.image_id, x.frame_idx); + if (x.frame_id) return snprintf(key, CACHE_KEY_BUFFER_SIZE, "%llx:%x", x.image_id, x.frame_id); return snprintf(key, CACHE_KEY_BUFFER_SIZE, "%llx:", x.image_id); } #define CK(x) key, cache_key(x, key) @@ -97,7 +97,9 @@ static inline void free_image(GraphicsManager *self, Image *img) { if (img->texture_id) free_texture(&img->texture_id); ImageAndFrame key = { .image_id=img->internal_id }; - for (key.frame_idx = 0; key.frame_idx <= img->extra_framecnt; key.frame_idx++) { + if (!remove_from_cache(self, key) && PyErr_Occurred()) PyErr_Print(); + for (unsigned i = 0; i < img->extra_framecnt; i++) { + key.frame_id = img->extra_frames[i].id; if (!remove_from_cache(self, key) && PyErr_Occurred()) PyErr_Print(); } if (img->extra_frames) { @@ -483,7 +485,7 @@ process_image_data(GraphicsManager *self, Image* img, const GraphicsCommand *g, } static Image* -initialize_load_data(GraphicsManager *self, const GraphicsCommand *g, Image *img, const unsigned char transmission_type, const uint32_t data_fmt, const uint32_t frame_idx) { +initialize_load_data(GraphicsManager *self, const GraphicsCommand *g, Image *img, const unsigned char transmission_type, const uint32_t data_fmt, const uint32_t frame_id) { img->load_data = (const LoadData){0}; switch(data_fmt) { case PNG: @@ -503,7 +505,7 @@ initialize_load_data(GraphicsManager *self, const GraphicsCommand *g, Image *img ABRT("EINVAL", "Unknown image format: %u", data_fmt); } if (transmission_type == 'd') { - if (g->more) self->currently_loading_data_for = (ImageAndFrame){.image_id = img->internal_id, .frame_idx = frame_idx}; + if (g->more) self->currently_loading_data_for = (ImageAndFrame){.image_id = img->internal_id, .frame_id = frame_id}; img->load_data.buf_capacity = img->load_data.data_sz + (g->compressed ? 1024 : 10); // compression header img->load_data.buf = malloc(img->load_data.buf_capacity); img->load_data.buf_used = 0; @@ -781,7 +783,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I g->num_lines = frame_number; unsigned char tt = g->transmission_type ? g->transmission_type : 'd'; size_t w = img->width, h = img->height; - if (tt == 'd' && self->currently_loading_data_for.image_id == img->internal_id && self->currently_loading_data_for.frame_idx == frame_number - 1) { + if (tt == 'd' && self->currently_loading_data_for.image_id == img->internal_id) { INIT_CHUNKED_LOAD; } else { self->last_transmit_graphics_command = *g; @@ -800,7 +802,6 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I if (img->data_loaded) { const unsigned bytes_per_pixel = img->is_opaque ? 3 : 4; const size_t expected_data_sz = img->width * img->height * bytes_per_pixel; - const ImageAndFrame key = { .image_id = img->internal_id, .frame_idx = frame_number - 1 }; if (img->load_data.is_opaque != img->is_opaque) FAIL("EINVAL", "Transparency for frames must match that of the base image"); @@ -816,9 +817,19 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I void *base_data = NULL; size_t data_sz = 0; + ImageAndFrame key = { .image_id = img->internal_id }; if (is_new_frame) { + key.frame_id = ++img->frame_id_counter; + if (!key.frame_id) key.frame_id = ++img->frame_id_counter; if (g->num_cells) { - const ImageAndFrame other = { .image_id = img->internal_id, .frame_idx = g->num_cells - 1 }; + ImageAndFrame other = { .image_id = img->internal_id }; + if (g->num_cells > 1) { + other.frame_id = g->num_cells - 2; + if (other.frame_id >= img->extra_framecnt) { + FAIL("ENODATA", "No data for frame with number: %u found in image: %u", g->num_cells, img->client_id); + } + other.frame_id = img->extra_frames[other.frame_id].id; + } if (!read_from_cache(self, other, &base_data, &data_sz)) { FAIL("ENODATA", "No data for frame with number: %u found in image: %u", g->num_cells, img->client_id); } @@ -828,6 +839,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I data_sz = expected_data_sz; } } else { + if (frame_number > 1) key.frame_id = img->extra_frames[frame_number - 2].id; if (!read_from_cache(self, key, &base_data, &data_sz)) { FAIL("ENODATA", "No data for frame with number: %u found in image: %u", frame_number, img->client_id); } @@ -865,6 +877,7 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I img->extra_frames = frames; img->extra_framecnt++; img->extra_frames[frame_number - 2].gap = DEFAULT_GAP; + img->extra_frames[frame_number - 2].id = key.frame_id; } if (g->z_index > 0) img->extra_frames[frame_number - 2].gap = g->z_index; } @@ -886,8 +899,24 @@ handle_delete_frame_command(GraphicsManager *self, const GraphicsCommand *g, boo } uint32_t frame_number = MIN(img->extra_framecnt + 1, g->num_lines); if (!frame_number) frame_number = 1; - if (!img->extra_framecnt) return g->action == 'F' ? img : NULL; - + if (!img->extra_framecnt) return g->delete_action == 'F' ? img : NULL; + ImageAndFrame key = {.image_id=img->internal_id}; + if (frame_number == 1) { + key.frame_id = img->extra_frames[0].id; + img->loop_delay = img->extra_frames[0].gap; + void *data; size_t sz; + if (read_from_cache(self, key, &data, &sz)) { + key.frame_id = 0; + add_to_cache(self, key, data, sz); + } + if (PyErr_Occurred()) PyErr_Print(); + frame_number = 2; + } + unsigned idx = frame_number - 2; + key.frame_id = img->extra_frames[idx].id; + remove_from_cache(self, key); + if (idx < img->extra_framecnt - 1) memmove(img->extra_frames + idx, img->extra_frames + idx + 1, sizeof(img->extra_frames[0]) * img->extra_framecnt - 1 - idx); + img->extra_framecnt--; return NULL; } // }}} @@ -1187,12 +1216,12 @@ image_as_dict(GraphicsManager *self, Image *img) { ImageAndFrame key = {.image_id = img->internal_id}; PyObject *frames = PyTuple_New(img->extra_framecnt); for (unsigned i = 0; i < img->extra_framecnt; i++) { - key.frame_idx = i + 1; + key.frame_id = img->extra_frames[i].id; PyTuple_SET_ITEM(frames, i, Py_BuildValue( - "{sI sN}", "gap", img->extra_frames[i].gap, "data", read_from_cache_python(self, key))); + "{sI sI sN}", "gap", img->extra_frames[i].gap, "id", key.frame_id, "data", read_from_cache_python(self, key))); if (PyErr_Occurred()) return NULL; } - key.frame_idx = 0; + key.frame_id = 0; return Py_BuildValue("{sI sI sI sI sK sI sI sO sO sI sI sO sN sN}", U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt), U(client_number), "data_loaded", img->data_loaded ? Py_True : Py_False, diff --git a/kitty/graphics.h b/kitty/graphics.h index cff6b36ad..5b09a2f19 100644 --- a/kitty/graphics.h +++ b/kitty/graphics.h @@ -44,6 +44,7 @@ typedef struct { typedef struct { uint32_t gap; + uint32_t id; } Frame; @@ -56,7 +57,7 @@ typedef struct { ImageRef *refs; Frame *extra_frames; - uint32_t loop_delay, current_frame_index; + uint32_t loop_delay, current_frame_index, frame_id_counter; size_t refcnt, refcap, extra_framecnt; monotonic_t atime; size_t used_storage; @@ -79,7 +80,7 @@ typedef struct { typedef struct { id_type image_id; - uint32_t frame_idx; + uint32_t frame_id; } ImageAndFrame; typedef struct { diff --git a/kitty_tests/graphics.py b/kitty_tests/graphics.py index 27514a3e8..fff816a44 100644 --- a/kitty_tests/graphics.py +++ b/kitty_tests/graphics.py @@ -613,29 +613,56 @@ class TestGraphics(BaseTest): # simple new frame t(payload='2' * 36) img = g.image_for_client_id(1) - self.assertEqual(img['extra_frames'], ({'gap': 40, 'data': b'2' * 36},)) + self.assertEqual(img['extra_frames'], ({'gap': 40, 'id': 1, 'data': b'2' * 36},)) # test editing a frame t(payload='3' * 36, r=2) img = g.image_for_client_id(1) - self.assertEqual(img['extra_frames'], ({'gap': 40, 'data': b'3' * 36},)) + self.assertEqual(img['extra_frames'], ({'gap': 40, 'id': 1, 'data': b'3' * 36},)) # test editing part of a frame t(payload='4' * 12, r=2, s=2, v=2) img = g.image_for_client_id(1) - self.assertEqual(img['extra_frames'], ({'gap': 40, 'data': b'444444333333444444333333333333333333'},)) + self.assertEqual(img['extra_frames'], ({'gap': 40, 'id': 1, 'data': b'444444333333444444333333333333333333'},)) t(payload='5' * 12, r=2, s=2, v=2, x=1, y=1) img = g.image_for_client_id(1) - self.assertEqual(img['extra_frames'], ({'gap': 40, 'data': b'444444333555555444555555333333333333'},)) + self.assertEqual(img['extra_frames'], ({'gap': 40, 'id': 1, 'data': b'444444333555555444555555333333333333'},)) t(payload='3' * 36, r=2) img = g.image_for_client_id(1) - self.assertEqual(img['extra_frames'], ({'gap': 40, 'data': b'3' * 36},)) + self.assertEqual(img['extra_frames'], ({'gap': 40, 'id': 1, 'data': b'3' * 36},)) # test loading from previous frame t(payload='4' * 12, c=2, s=2, v=2, z=101, frame_number=3) img = g.image_for_client_id(1) self.assertEqual(img['extra_frames'], ( - {'gap': 40, 'data': b'3' * 36}, - {'gap': 101, 'data': b'444444333333444444333333333333333333'}, + {'gap': 40, 'id': 1, 'data': b'3' * 36}, + {'gap': 101, 'id': 2, 'data': b'444444333333444444333333333333333333'}, )) - - li(a='d', d='A') + # test delete of frames + t(payload='5' * 36, frame_number=4) + img = g.image_for_client_id(1) + self.assertEqual(img['extra_frames'], ( + {'gap': 40, 'id': 1, 'data': b'3' * 36}, + {'gap': 101, 'id': 2, 'data': b'444444333333444444333333333333333333'}, + {'gap': 40, 'id': 3, 'data': b'5' * 36}, + )) + self.assertIsNone(li(a='d', d='f', i=1, r=1)) + img = g.image_for_client_id(1) + self.assertEqual(img['data'], b'3' * 36) + self.assertEqual(img['extra_frames'], ( + {'gap': 101, 'id': 2, 'data': b'444444333333444444333333333333333333'}, + {'gap': 40, 'id': 3, 'data': b'5' * 36}, + )) + self.assertIsNone(li(a='d', d='f', i=1, r=2)) + img = g.image_for_client_id(1) + self.assertEqual(img['data'], b'3' * 36) + self.assertEqual(img['extra_frames'], ( + {'gap': 40, 'id': 3, 'data': b'5' * 36}, + )) + self.assertIsNone(li(a='d', d='f', i=1)) + img = g.image_for_client_id(1) + self.assertEqual(img['data'], b'5' * 36) + self.assertFalse(img['extra_frames']) + self.assertIsNone(li(a='d', d='f', i=1)) + img = g.image_for_client_id(1) + self.assertEqual(img['data'], b'5' * 36) + self.assertIsNone(li(a='d', d='F', i=1)) self.ae(g.image_count, 0) self.assertEqual(g.disk_cache.total_size, 0)