From bb5c5a8e4ff8cdba387aeb67723d7bf3fb9db41e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Feb 2021 16:22:36 +0530 Subject: [PATCH] Allow gapless frames --- docs/graphics-protocol.rst | 9 +++++---- kitty/graphics.c | 29 ++++++++++++++++++++++------- kitty/graphics.h | 1 + kitty_tests/graphics.py | 2 +- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/graphics-protocol.rst b/docs/graphics-protocol.rst index a5131b8fd..64b0b0e65 100644 --- a/docs/graphics-protocol.rst +++ b/docs/graphics-protocol.rst @@ -551,15 +551,16 @@ Key Value Default Description ``c`` Positive integer ``0`` The 1-based frame number of the frame whose image data serves as the base data when creating a new frame, by default the base data is black, fully transparent pixels ``r`` Positive integer ``0`` The 1-based frame number of the frame that is being edited. By default, a new frame is created -``z`` 32-bit integer ``0`` The gap (in milliseconds) of this frame from the previous one. Values less than - one are ignored, new frames are given a default gap of ``40ms`` if not specified. +``z`` 32-bit integer ``0`` The gap (in milliseconds) of this frame from the previous one. A value of + zero is ignored. Negative values create a *gapless* frame. If not specified, + frames have a default gap of ``40ms``. The root frame defaults to zero gap. **Keys for animation control** ----------------------------------------------------------- ``s`` Positive integer ``0`` ``1`` - start animation, ``>1`` - stop animation ``r`` Positive integer ``0`` The 1-based frame number of the frame that is being affected -``z`` 32-bit integer ``0`` The gap (in milliseconds) of this frame from the previous one. Values less than - one are ignored +``z`` 32-bit integer ``0`` The gap (in milliseconds) of this frame from the previous one. A value of + zero is ignored. Negative values create a *gapless* frame. ``c`` Positive integer ``0`` The 1-based frame number of the frame that should be made the current frame diff --git a/kitty/graphics.c b/kitty/graphics.c index 530056810..a2723e6ab 100644 --- a/kitty/graphics.c +++ b/kitty/graphics.c @@ -766,7 +766,7 @@ grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float scree rd->texture_id = img->texture_id; img->is_drawn = true; } - if (img->is_drawn && !was_drawn && img->animation_enabled && img->extra_framecnt) { + if (img->is_drawn && !was_drawn && img->animation_enabled && img->extra_framecnt && img->animation_duration) { self->has_images_needing_animation = true; global_state.has_active_animated_images = true; } @@ -821,6 +821,14 @@ update_current_frame(GraphicsManager *self, Image *img, void *data) { img->current_frame_shown_at = monotonic(); } +static inline void +change_gap(Image *img, Frame *f, int32_t gap) { + uint32_t prev_gap = f->gap; + f->gap = MAX(0, gap); + img->animation_duration = prev_gap < img->animation_duration ? img->animation_duration - prev_gap : 0; + img->animation_duration += f->gap; +} + 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; @@ -925,15 +933,15 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I ABRT("ENOSPC", "Failed to cache data for image frame"); } if (is_new_frame) { - if (!img->extra_framecnt) img->root_frame.gap = g->_gap > 0 ? g->_gap : DEFAULT_GAP; Frame *frames = realloc(img->extra_frames, sizeof(img->extra_frames[0]) * img->extra_framecnt + 1); if (!frames) ABRT("ENOMEM", "Out of memory"); 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; + img->animation_duration += DEFAULT_GAP; } - if (g->_gap > 0) img->extra_frames[frame_number - 2].gap = g->_gap; + if (g->_gap > 0) change_gap(img, img->extra_frames + frame_number - 2, g->_gap); } return img; } @@ -957,17 +965,21 @@ handle_delete_frame_command(GraphicsManager *self, const GraphicsCommand *g, boo *is_dirty = true; ImageAndFrame key = {.image_id=img->internal_id}; bool remove_root = frame_number == 1; + uint32_t removed_gap = 0; if (remove_root) { key.frame_id = img->root_frame.id; remove_from_cache(self, key); if (PyErr_Occurred()) PyErr_Print(); + removed_gap = img->root_frame.gap; img->root_frame = img->extra_frames[0]; } unsigned removed_idx = remove_root ? 0 : frame_number - 2; if (!remove_root) { key.frame_id = img->extra_frames[removed_idx].id; + removed_gap = img->extra_frames[removed_idx].gap; remove_from_cache(self, key); } + img->animation_duration = removed_gap < img->animation_duration ? img->animation_duration - removed_gap : 0; if (PyErr_Occurred()) PyErr_Print(); if (removed_idx < img->extra_framecnt - 1) memmove(img->extra_frames + removed_idx, img->extra_frames + removed_idx + 1, sizeof(img->extra_frames[0]) * img->extra_framecnt - 1 - removed_idx); img->extra_framecnt--; @@ -987,7 +999,7 @@ handle_animation_control_command(GraphicsManager *self, bool *is_dirty, const Gr uint32_t frame_idx = g->_frame_number - 1; if (frame_idx <= img->extra_framecnt) { Frame *f = frame_idx ? img->extra_frames + frame_idx - 1 : &img->root_frame; - if (g->_gap > 0) f->gap = g->_gap; + if (g->_gap) change_gap(img, f, g->_gap); } } if (g->_other_frame_number) { @@ -1018,13 +1030,15 @@ scan_active_animations(GraphicsManager *self, const monotonic_t now, monotonic_t self->context_made_current_for_this_command = true; for (size_t i = self->image_count; i-- > 0;) { Image *img = self->images + i; - if (img->animation_enabled && img->extra_framecnt && img->is_drawn) { + if (img->animation_enabled && img->extra_framecnt && img->is_drawn && img->animation_duration) { self->has_images_needing_animation = true; Frame *next_frame = img->current_frame_index + 1 < img->extra_framecnt ? img->extra_frames + img->current_frame_index + 1 : &img->root_frame; monotonic_t next_frame_at = img->current_frame_shown_at + ms_to_monotonic_t(next_frame->gap); if (now >= next_frame_at) { dirtied = true; - img->current_frame_index = (img->current_frame_index + 1) % (img->extra_framecnt + 1); + do { + img->current_frame_index = (img->current_frame_index + 1) % (img->extra_framecnt + 1); + } while(!current_frame(img)->gap); update_current_frame(self, img, NULL); next_frame = img->current_frame_index + 1 < img->extra_framecnt ? img->extra_frames + img->current_frame_index + 1 : &img->root_frame; } @@ -1341,10 +1355,11 @@ image_as_dict(GraphicsManager *self, Image *img) { if (PyErr_Occurred()) { Py_CLEAR(frames); return NULL; } } key.frame_id = img->root_frame.id; - return Py_BuildValue("{sI sI sI sI sK sI sI sO sO sO sI sI sI sN sN}", + return Py_BuildValue("{sI sI sI sI sK sI sI sO sO sO sI sI sI sI sN sN}", U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt), U(client_number), B(data_loaded), B(is_4byte_aligned), B(animation_enabled), U(current_frame_index), "root_frame_gap", img->root_frame.gap, U(current_frame_index), + U(animation_duration), "data", read_from_cache_python(self, key), "extra_frames", frames ); #undef B diff --git a/kitty/graphics.h b/kitty/graphics.h index 099b3b7ee..eb5909935 100644 --- a/kitty/graphics.h +++ b/kitty/graphics.h @@ -57,6 +57,7 @@ typedef struct { ImageRef *refs; Frame *extra_frames, root_frame; uint32_t current_frame_index, frame_id_counter; + uint64_t animation_duration; size_t refcnt, refcap, extra_framecnt; monotonic_t atime; size_t used_storage; diff --git a/kitty_tests/graphics.py b/kitty_tests/graphics.py index 778af6930..b944b7ff1 100644 --- a/kitty_tests/graphics.py +++ b/kitty_tests/graphics.py @@ -638,7 +638,7 @@ class TestGraphics(BaseTest): )) # test changing gaps img = g.image_for_client_id(1) - self.assertEqual(img['root_frame_gap'], 77) + self.assertEqual(img['root_frame_gap'], 0) self.assertIsNone(li(a='a', i=1, r=1, z=13)) img = g.image_for_client_id(1) self.assertEqual(img['root_frame_gap'], 13)