From 7b41e333990e42aa4ff1a078e8155a97de97d4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E9=B8=BD?= <18717730+xiaobai050@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:41:26 +0800 Subject: [PATCH] Custom shaders: Redraw static effects only on demand --- docs/changelog.rst | 3 ++- docs/custom-shaders.rst | 8 ++++++-- kitty/child-monitor.c | 7 ++++++- kitty/shaders.c | 2 +- kitty_tests/slang.py | 4 +++- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 20b28288f..85d91e2ce 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -205,7 +205,8 @@ Detailed list of changes - Support for :doc:`/custom-shaders` for adding various graphical effects (:iss:`10344`) - Custom shaders: Allow tinting inactive windows toward a configurable color, - preserving the tab bar and active window padding. + preserving the tab bar and active window padding. Support static effects + without periodic redraws. - Various throughput performance improvements for a 15-35% real world improvement depending on workload diff --git a/docs/custom-shaders.rst b/docs/custom-shaders.rst index 8b82c1dae..9a2ee0b3b 100644 --- a/docs/custom-shaders.rst +++ b/docs/custom-shaders.rst @@ -84,6 +84,7 @@ following as :file:`shaders/dim-gray.pipeline` in your kitty config directory an set ``custom_shaders dim-gray`` and ``inactive_text_alpha 1`` in :file:`kitty.conf`:: startgroup + animation_step 0 var float HIGHLIGHT_INTENSITY = 0 var float BORDER_WIDTH = 0 var float INACTIVE_DIM = 0.74 @@ -100,7 +101,8 @@ sRGB ``#808080``. The default target is black, preserving the existing dimming effect. The two optional flags preserve the tab bar and include the active window's padding in its unchanged area. Explicit cell backgrounds and images are blended together with text, while transparency and colors reported to -terminal applications are preserved. +terminal applications are preserved. ``animation_step 0`` makes this static +effect redraw only when content or shader events change. Anatomy of a custom shader @@ -269,7 +271,9 @@ Directives inside a group: How many milliseconds between animation frames. Lower values produce smoother animation at the cost of more GPU draws. Defaults to ``50`` ms (20 fps). The value is clamped to be no smaller than - :opt:`repaint_delay`. + :opt:`repaint_delay`. Set this to ``0`` for static effects that should + redraw only when content or shader events change, without periodic frames. + Animation stop events and duration limits still apply. ``attach`` Mark this group as *attached* to the preceding group. An attached group diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 6ac645b38..fb6d3d28b 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -957,13 +957,18 @@ prepare_to_render_os_window( events |= (1u << SHADER_ANIM_EVENT_TAB_CHANGE) | (1u << SHADER_ANIM_EVENT_WINDOW_FOCUS_IN) | (1u << SHADER_ANIM_EVENT_WINDOW_FOCUS_OUT); if (*active_window_id && *active_window_id != os_window->last_active_window_id) events |= (1u << SHADER_ANIM_EVENT_WINDOW_FOCUS_IN) | (1u << SHADER_ANIM_EVENT_WINDOW_FOCUS_OUT); + const bool was_active = os_window->has_active_custom_shaders; + const bool was_animating = os_window->shader_anim_min_step < MONOTONIC_T_MAX; + const bool animation_ended = now >= os_window->shader_anim_next_end_at; monotonic_t min_step = update_custom_shader_animations(events, now, os_window); os_window->shader_anim_event_registry = 0; if (os_window->has_active_custom_shaders) { os_window->needs_layers = true; - needs_render = true; + if (events || min_step < MONOTONIC_T_MAX) needs_render = true; } + if (was_active != os_window->has_active_custom_shaders || was_animating || animation_ended) needs_render = true; if (min_step < MONOTONIC_T_MAX) set_maximum_wait(min_step); + if (os_window->shader_anim_next_end_at < MONOTONIC_T_MAX) set_maximum_wait(os_window->shader_anim_next_end_at - now); } return needs_render || was_previously_rendered_with_layers != os_window->needs_layers; } diff --git a/kitty/shaders.c b/kitty/shaders.c index faafde04e..e069bd4a7 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -2998,7 +2998,7 @@ transfer_pipeline_to_struct(PyObject *pg, CustomShaderPipeline *p) { PyObject *astep = PyDict_GetItemString(g, "animation_step"); cg->animation_step = (astep && PyLong_Check(astep)) ? (monotonic_t)PyLong_AsLong(astep) : ANIMATION_SAMPLE_WAIT; - cg->animation_step = MAX(cg->animation_step, OPT(repaint_delay)); + cg->animation_step = cg->animation_step == 0 ? MONOTONIC_T_MAX : MAX(cg->animation_step, OPT(repaint_delay)); PyObject *ae_events = PyDict_GetItemString(g, "animation_end_events"); if (is_seq(ae_events)) cg->animation_end_events = parse_anim_event_sequence(ae_events); diff --git a/kitty_tests/slang.py b/kitty_tests/slang.py index 1a9fcb26f..41948dab0 100644 --- a/kitty_tests/slang.py +++ b/kitty_tests/slang.py @@ -40,6 +40,7 @@ class TestSlang(BaseTest): pipeline = parse_pipeline_definition( """ startgroup + animation_step 0 var float HIGHLIGHT_INTENSITY = 0 var float BORDER_WIDTH = 0 var float INACTIVE_DIM = 0.74 @@ -54,11 +55,12 @@ class TestSlang(BaseTest): with tempfile.TemporaryDirectory() as cache_dir: clear_caches() try: - vertex, fragment, _ = build_custom_shader_pipeline_glsl(pipeline, cache_dir=cache_dir) + vertex, fragment, metadata = build_custom_shader_pipeline_glsl(pipeline, cache_dir=cache_dir) finally: clear_caches() self.assertTrue(vertex) self.assertTrue(fragment) + self.ae(metadata['pipeline']['groups'][0]['animation_step'], 0) def test_slang_parser(self): def check(src: str, expected: SlangFile) -> None: