diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index fb6d3d28b..47e8cddb1 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -828,7 +828,7 @@ prepare_to_render_os_window( bool was_previously_rendered_with_layers = os_window->needs_layers; os_window->needs_layers = (!global_state.supports_framebuffer_srgb || effective_os_window_alpha(os_window) < 1.f || os_window->live_resize.in_progress || - (background_image_for_os_window(os_window) != NULL) || os_window->has_active_custom_shaders); + (background_image_for_os_window(os_window) != NULL) || os_window->shader_anim.has_active_shaders); if (TD.screen && os_window->num_tabs && !os_window->has_too_few_tabs) { if (!os_window->tab_bar_data_updated) { call_boss(update_tab_bar_data, "K", os_window->id); @@ -945,7 +945,7 @@ prepare_to_render_os_window( if (blink_has_ceased && !os_window->user_is_idle) { os_window->user_is_idle = true; os_window->shader_anim_event_registry |= (1u << SHADER_ANIM_EVENT_USER_IDLE); - } else if (!blink_has_ceased && OPT(cursor_blink_interval) <= 0 && os_window->has_active_custom_shaders) { + } else if (!blink_has_ceased && OPT(cursor_blink_interval) <= 0 && os_window->shader_anim.has_active_shaders) { // cursor blinking is disabled so collect_cursor_info won't schedule a wakeup for // this deadline; do it here so user-idle fires on time set_maximum_wait(OPT(cursor_stop_blinking_after) - time_since_last_activity); @@ -957,18 +957,16 @@ 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; + const ShaderAnimState before = os_window->shader_anim; 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; - 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 (os_window->shader_anim.has_active_shaders) os_window->needs_layers = true; + if (custom_shader_needs_render(&before, &os_window->shader_anim, events, now)) needs_render = true; + // Wake up for the next animation frame and for the moment a duration + // bounded animation expires. Static groups (animation_step 0) have + // min_step == MONOTONIC_T_MAX and so schedule no periodic wakeup. 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); + 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/fast_data_types.pyi b/kitty/fast_data_types.pyi index 9ccac6462..7788531c6 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -532,6 +532,8 @@ def compile_program( ) -> int: pass +def custom_shader_needs_render(before: Tuple[bool, int, int], after: Tuple[bool, int, int], event_mask: int, now: int) -> bool: ... +def simulate_custom_shader_render_ticks(num_ticks: int, event_mask: int = 0, initialize: bool = False) -> list[bool]: ... def set_os_window_chrome(os_window_id: int) -> bool: pass diff --git a/kitty/shaders.c b/kitty/shaders.c index e069bd4a7..25e3e2fdd 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -501,6 +501,24 @@ init_cell_program(void) { bind_shader_globals_to_current_context(); } +// Non-animated, non-attached groups are active from the moment the pipeline +// loads; animated groups start inactive and attached groups inherit the state +// of their predecessor. +static bool +group_is_initially_active(const CustomShaderGroup *cg, bool predecessor_is_active) { + return cg->attached ? predecessor_is_active : (cg->animation_start_events == 0); +} + +static bool +pipeline_has_initially_active_group(void) { + bool prev = false; + for (size_t i = 0; i < custom_shaders.end.num_groups; i++) { + prev = group_is_initially_active(custom_shaders.end.groups + i, prev); + if (prev) return true; + } + return false; +} + static void init_custom_programs(void) { custom_shaders.count = 0; @@ -541,33 +559,9 @@ init_custom_programs(void) { if (global_state.layers_render_texture.texture_b_fbo_id) free_framebuffer(&global_state.layers_render_texture.texture_b_fbo_id); } // persist is per-window and freed lazily in start_os_window_rendering - // Non-animated, non-attached groups are always active; animated groups start inactive. - // Attached groups inherit their initial active state from their predecessor. - bool any_initially_active = false; - { - bool prev_init = false; - for (size_t i = 0; i < custom_shaders.end.num_groups; i++) { - const CustomShaderGroup *cg = &custom_shaders.end.groups[i]; - bool this_init = cg->attached ? prev_init : (cg->animation_start_events == 0); - if (this_init) any_initially_active = true; - prev_init = this_init; - } - } - bool initially_active = custom_shaders.count > 0 && any_initially_active; for (size_t w = 0; w < global_state.num_os_windows; w++) { OSWindow *osw = global_state.os_windows + w; - zero_at_ptr_count(osw->shader_group_anim, custom_shaders.end.num_groups); - // Initialize active=true for attached groups that follow always-active predecessors - bool prev_init = false; - for (size_t i = 0; i < custom_shaders.end.num_groups; i++) { - const CustomShaderGroup *cg = &custom_shaders.end.groups[i]; - bool this_init = cg->attached ? prev_init : (cg->animation_start_events == 0); - if (cg->attached && this_init) osw->shader_group_anim[i].active = true; - prev_init = this_init; - } - osw->has_active_custom_shaders = initially_active; - osw->shader_anim_min_step = MONOTONIC_T_MAX; - osw->shader_anim_next_end_at = MONOTONIC_T_MAX; + init_shader_animation_state(osw); // Synthesize focus events for windows that already have focus so that // animations triggered by os-window-focus-in start on the first render // after shader compilation (the focus-in event fired before shaders were @@ -579,7 +573,31 @@ init_custom_programs(void) { custom_shaders.count, custom_shaders.end.num_groups, custom_shaders.end.textures, - initially_active); + custom_shaders.count > 0 && pipeline_has_initially_active_group()); +} + +// Reset the per-OS-window animation state to what it should be immediately +// after the current pipeline was loaded. Called both when the pipeline changes +// and when an OS window is created, so that the MONOTONIC_T_MAX sentinels in +// ShaderAnimState are never left at their zero initialized values -- a zero +// min_step/next_end_at reads as "an animation is running and its deadline has +// already passed", which would make the renderer redraw on every single tick. +void +init_shader_animation_state(OSWindow *osw) { + zero_at_ptr_count(osw->shader_group_anim, custom_shaders.end.num_groups); + bool prev_init = false, any_initially_active = false; + for (size_t i = 0; i < custom_shaders.end.num_groups; i++) { + const CustomShaderGroup *cg = &custom_shaders.end.groups[i]; + const bool this_init = group_is_initially_active(cg, prev_init); + // Only attached groups are tracked by the state machine when initially + // active; always-active non-attached groups never enter it at all. + if (cg->attached && this_init) osw->shader_group_anim[i].active = true; + if (this_init) any_initially_active = true; + prev_init = this_init; + } + osw->shader_anim.has_active_shaders = custom_shaders.count > 0 && any_initially_active; + osw->shader_anim.min_step = MONOTONIC_T_MAX; + osw->shader_anim.next_end_at = MONOTONIC_T_MAX; } static const char * @@ -624,12 +642,12 @@ shader_anim_event_mask_str(unsigned mask) { monotonic_t update_custom_shader_animations(unsigned event_mask, monotonic_t now, OSWindow *os_window) { if (!custom_shaders.count) { - os_window->has_active_custom_shaders = false; + os_window->shader_anim = (ShaderAnimState){.min_step = MONOTONIC_T_MAX, .next_end_at = MONOTONIC_T_MAX}; return MONOTONIC_T_MAX; } // Fast path: no events and no duration-bounded animation expiring this frame. // Cached state is still valid — skip group iteration entirely. - if (!event_mask && now < os_window->shader_anim_next_end_at) return os_window->shader_anim_min_step; + if (!event_mask && now < os_window->shader_anim.next_end_at) return os_window->shader_anim.min_step; // Slow path: update per-group state and recompute cached values in one pass. CustomShaderPipeline *p = &custom_shaders.end; @@ -694,14 +712,35 @@ update_custom_shader_animations(unsigned event_mask, monotonic_t now, OSWindow * } prev_active = this_active; } - bool prev_has_active = os_window->has_active_custom_shaders; - os_window->has_active_custom_shaders = any_active; - if (prev_has_active != any_active) debug_rendering("has_active_custom_shaders: %d -> %d\n", prev_has_active, any_active); - os_window->shader_anim_min_step = min_step; - os_window->shader_anim_next_end_at = next_end; + bool prev_has_active = os_window->shader_anim.has_active_shaders; + if (prev_has_active != any_active) debug_rendering("has_active_shaders: %d -> %d\n", prev_has_active, any_active); + os_window->shader_anim.has_active_shaders = any_active; + os_window->shader_anim.min_step = min_step; + os_window->shader_anim.next_end_at = next_end; return min_step; } +// Decide whether the custom shader layer needs a frame drawn this tick, given +// the cached state from the previous tick and the state just recomputed by +// update_custom_shader_animations(). Kept separate from the renderer so it can +// be unit tested -- see test_custom_shader_redraw_logic. +bool +custom_shader_needs_render(const ShaderAnimState *before, const ShaderAnimState *after, unsigned event_mask, monotonic_t now) { + // Groups turned on or off: draw once so the change becomes visible. This is + // what produces the final frame that clears a finished effect. + if (before->has_active_shaders != after->has_active_shaders) return true; + // A duration bounded animation expires on this tick. + if (now >= before->next_end_at) return true; + // Something was animating last tick, so it either wants its next frame or + // has just stopped and wants one last one. + if (before->min_step < MONOTONIC_T_MAX) return true; + if (!after->has_active_shaders) return false; + // Animated groups need periodic frames. Static groups (animation_step 0) + // only need one when an event may have changed what they draw; everything + // else that changes the screen requests a redraw through other paths. + return event_mask != 0 || after->min_step < MONOTONIC_T_MAX; +} + void bind_shader_globals_to_current_context(void) { if (shader_globals_vao_idx == -1) return; @@ -2750,7 +2789,7 @@ stop_os_window_rendering(OSWindow *os_window, Tab *tab, Window *active_window, m if (os_window->needs_layers) { float sx = global_state.layers_render_texture.width > 0 ? (float)os_window->viewport_width / (float)global_state.layers_render_texture.width : 1.f; float sy = global_state.layers_render_texture.height > 0 ? (float)os_window->viewport_height / (float)global_state.layers_render_texture.height : 1.f; - if (custom_shaders.end.active && os_window->has_active_custom_shaders) { + if (custom_shaders.end.active && os_window->shader_anim.has_active_shaders) { // debug_rendering("Custom end shader running (groups=%zu)\n", custom_shaders.end.num_groups); restore_viewport(); if (os_window->live_resize.in_progress) @@ -3155,6 +3194,55 @@ sprite_map_set_limits(PyObject UNUSED *self, PyObject *args) { Py_RETURN_NONE; } +// Test only. Wraps custom_shader_needs_render() so the redraw decision can be +// exercised from the Python test suite without a GPU context. Each state is a +// (has_active_shaders, min_step, next_end_at) tuple. +static bool +parse_shader_anim_state(PyObject *obj, ShaderAnimState *ans) { + int has_active; + long long min_step, next_end_at; + if (!PyArg_ParseTuple(obj, "pLL", &has_active, &min_step, &next_end_at)) return false; + *ans = (ShaderAnimState){.has_active_shaders = has_active, .min_step = min_step, .next_end_at = next_end_at}; + return true; +} + +static PyObject * +pycustom_shader_needs_render(PyObject *self UNUSED, PyObject *args) { + PyObject *b, *a; + unsigned int event_mask; + long long now; + ShaderAnimState before, after; + if (!PyArg_ParseTuple(args, "O!O!IL", &PyTuple_Type, &b, &PyTuple_Type, &a, &event_mask, &now)) return NULL; + if (!parse_shader_anim_state(b, &before) || !parse_shader_anim_state(a, &after)) return NULL; + if (custom_shader_needs_render(&before, &after, event_mask, now)) Py_RETURN_TRUE; + Py_RETURN_FALSE; +} + +// Test only. Runs the renderer's per-frame custom shader bookkeeping (the same +// sequence as prepare_to_render_os_window) against a freshly allocated, zero +// initialized OSWindow and returns whether each tick asked for a redraw. Used +// to assert that an idle OS window with no shader work pending stays idle. +static PyObject * +pysimulate_custom_shader_render_ticks(PyObject *self UNUSED, PyObject *args) { + unsigned long num_ticks; + unsigned int event_mask = 0; + int initialize = 0; + if (!PyArg_ParseTuple(args, "k|Ip", &num_ticks, &event_mask, &initialize)) return NULL; + RAII_ALLOC(OSWindow, osw, calloc(1, sizeof(OSWindow))); + if (!osw) return PyErr_NoMemory(); + if (initialize) init_shader_animation_state(osw); + RAII_PyObject(ans, PyList_New(0)); + if (!ans) return NULL; + monotonic_t now = monotonic(); + for (unsigned long i = 0; i < num_ticks; i++, now += ms_to_monotonic_t(10ll)) { + const ShaderAnimState before = osw->shader_anim; + update_custom_shader_animations(event_mask, now, osw); + PyObject *v = custom_shader_needs_render(&before, &osw->shader_anim, event_mask, now) ? Py_True : Py_False; + if (PyList_Append(ans, v) != 0) return NULL; + } + return Py_NewRef(ans); +} + #define M(name, arg_type) {#name, (PyCFunction)name, arg_type, NULL} #define MW(name, arg_type) {#name, (PyCFunction)py##name, arg_type, NULL} static PyMethodDef module_methods[] = { @@ -3167,6 +3255,8 @@ static PyMethodDef module_methods[] = { MW(unmap_vao_buffer, METH_VARARGS), MW(bind_program, METH_O), MW(unbind_program, METH_NOARGS), + MW(custom_shader_needs_render, METH_VARARGS), + MW(simulate_custom_shader_render_ticks, METH_VARARGS), {NULL, NULL, 0, NULL} /* Sentinel */ }; diff --git a/kitty/shaders/custom/dim-inactive-windows.pipeline b/kitty/shaders/custom/dim-inactive-windows.pipeline index 5e2f448bf..c50c953d3 100644 --- a/kitty/shaders/custom/dim-inactive-windows.pipeline +++ b/kitty/shaders/custom/dim-inactive-windows.pipeline @@ -1,4 +1,6 @@ startgroup + # static effect, redraw only when the active window changes + animation_step 0 var float HIGHLIGHT_INTENSITY = 0 var float BORDER_WIDTH = 0 shaders focus-highlight diff --git a/kitty/shaders/slang.py b/kitty/shaders/slang.py index 72e258538..8a155f0a2 100644 --- a/kitty/shaders/slang.py +++ b/kitty/shaders/slang.py @@ -184,10 +184,14 @@ class LoadShaderPrograms: text_old_gamma: bool = False custom_shaders: tuple[str, ...] = () force_recompile_of_custom_shaders: bool = False - last_built_custom_shaders: dict[int, Any] = {} opts: Options | None = None + def __init__(self) -> None: + # Per-instance because it is mutable: the sources most recently compiled + # into each program, used to skip redundant recompiles. + self.last_built_custom_shaders: dict[int, tuple[str, str, dict[str, Any]]] = {} + def get_options(self) -> Options: try: return self.opts or get_options() @@ -270,11 +274,17 @@ class LoadShaderPrograms: continue pmap.setdefault(d['slot'], []).append(d) + def disable(prog: int) -> None: + # Forget the cached sources as well, otherwise re-enabling the same + # shader later is a no-op because the sources compare equal while the + # program itself is empty. + compile_program(prog, (), (), {}, allow_recompile) + self.last_built_custom_shaders.pop(prog, None) + def do(prog: int, slot: str) -> None: slot_pipelines = pmap.get(slot) if not slot_pipelines: - compile_program(prog, (), (), {}, allow_recompile) - self.last_built_custom_shaders.pop(prog, None) + disable(prog) else: try: pipeline = merge_pipelines(slot_pipelines) @@ -283,8 +293,7 @@ class LoadShaderPrograms: # print(frag, file=open('/tmp/sample.frag', 'w')) except Exception as e: log_error(f'Failed to build custom shader for slot {slot} with error: {e}') - compile_program(prog, (), (), {}, allow_recompile) - self.last_built_custom_shaders.pop(prog, None) + disable(prog) else: try: if self.last_built_custom_shaders.get(prog) != (vert, frag, metadata): @@ -292,8 +301,7 @@ class LoadShaderPrograms: self.last_built_custom_shaders[prog] = vert, frag, metadata except Exception as e: log_error(f'Failed to load custom shader for slot {slot} with error: {e}') - compile_program(prog, (), (), {}, allow_recompile) - self.last_built_custom_shaders.pop(prog, None) + disable(prog) do(CUSTOM_END_PROGRAM, 'end') compile_program(-2, (), (), {}) # initialize programs @@ -1304,7 +1312,10 @@ def parse_pipeline_definition(lines: Iterable[str], pipeline_name: str, pipeline case 'animation_step': if len(parts) < 2: raise ValueError('animation_step requires a millisecond value') - current_group['animation_step'] = int(parts[1]) * 1_000_000 + step_ms = int(parts[1]) + if step_ms < 0: + raise ValueError(f'animation_step must be non-negative, not: {step_ms}') + current_group['animation_step'] = step_ms * 1_000_000 case 'animation_stop': val = ''.join(parts[1:]) if len(parts) > 1 else 'never' if val == 'never': diff --git a/kitty/state.c b/kitty/state.c index f701afede..eb85a168e 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -313,6 +313,7 @@ add_os_window(void) { ans->tab_bar_render_data.vao_idx = create_cell_vao(); ans->background_opacity.alpha = OPT(background_opacity); ans->created_at = monotonic(); + init_shader_animation_state(ans); END_WITH_OS_WINDOW_REFS return ans; } diff --git a/kitty/state.h b/kitty/state.h index e9d109eb6..08aafba8b 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -484,6 +484,17 @@ typedef struct BackgroundImageRenderSettings { #define MAX_CUSTOM_SHADER_GROUPS 16 +// Summary of the custom shader animation state machine, cached once per frame +// so the renderer can decide whether a new frame is needed without walking the +// pipeline. Both deadlines use MONOTONIC_T_MAX as their "nothing to do" +// sentinel, so this must never be left zero initialized -- see +// init_shader_animation_state(). +typedef struct ShaderAnimState { + bool has_active_shaders; // at least one group will draw this frame + monotonic_t min_step; // smallest animation_step across active groups, MONOTONIC_T_MAX if none want periodic redraws + monotonic_t next_end_at; // earliest expiry of a duration bounded active animation, MONOTONIC_T_MAX if none +} ShaderAnimState; + typedef enum { SHADER_ANIM_EVENT_POINTER_LEFT_BUTTON_PRESS, SHADER_ANIM_EVENT_OS_WINDOW_FOCUS_IN, @@ -575,9 +586,7 @@ typedef struct OSWindow { bool active; monotonic_t started_at; } shader_group_anim[MAX_CUSTOM_SHADER_GROUPS]; - bool has_active_custom_shaders; - monotonic_t shader_anim_min_step; // cached min animation_step across active animated groups - monotonic_t shader_anim_next_end_at; // earliest expiry of a duration-bounded active animation + ShaderAnimState shader_anim; } OSWindow; static inline float @@ -769,6 +778,8 @@ void dispatch_buffered_keys(Window *w); bool screen_needs_rendering_in_layers(OSWindow *os_window, Window *w, Screen *screen); void setup_os_window_for_rendering(OSWindow *, Tab *, Window *, bool, monotonic_t); monotonic_t update_custom_shader_animations(unsigned event_mask, monotonic_t now, OSWindow *os_window); +void init_shader_animation_state(OSWindow *os_window); +bool custom_shader_needs_render(const ShaderAnimState *before, const ShaderAnimState *after, unsigned event_mask, monotonic_t now); void swap_window_buffers(OSWindow *w); void take_screenshot_of_rectangular_region(OSWindow *os_window, Region region, unsigned char *dst_buf, unsigned *thumb_w, unsigned *thumb_h, bool no_scaling); bool current_framebuffer_is_ok(void); diff --git a/kitty_tests/slang.py b/kitty_tests/slang.py index 360863371..e844b9d4c 100644 --- a/kitty_tests/slang.py +++ b/kitty_tests/slang.py @@ -35,41 +35,66 @@ _SUPPORT_SHADER_NAMES = frozenset(('types', 'pipeline')) class TestSlang(BaseTest): - def test_inactive_window_tint_pipeline(self): - if not shutil.which(slangc()[0]): - self.skipTest(f'slangc ({slangc()[0]}) not found in PATH') + def test_animation_step_parsing(self): + def step(val): + return parse_pipeline_definition(['startgroup', f'animation_step {val}', 'shaders focus-highlight', 'endgroup'], 'test')['groups'][0][ + 'animation_step' + ] - pipeline = parse_pipeline_definition( - """ - startgroup - animation_step 0 - var float HIGHLIGHT_INTENSITY = 0 - var float BORDER_WIDTH = 0 - var float INACTIVE_DIM = 0.74 - var float3 INACTIVE_DIM_COLOR = float3(0.2158605) - var bool DIM_CENTRAL_AREA_ONLY = true - var bool INCLUDE_WINDOW_PADDING = true - shaders focus-highlight - endgroup - """.splitlines(), - 'dim-gray', - ) - with tempfile.TemporaryDirectory() as cache_dir: - clear_caches() - try: - 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) + # 0 means "static effect, no periodic redraws" and must survive as 0 + self.ae(step(0), 0) + self.ae(step(16), 16_000_000) + self.assertRaises(ValueError, step, -1) + + def test_custom_shader_redraw_logic(self): + from kitty.fast_data_types import custom_shader_needs_render, simulate_custom_shader_render_ticks + + NEVER = 2**63 - 1 # MONOTONIC_T_MAX, the "nothing scheduled" sentinel + STEP = 50_000_000 # an animated group redrawing every 50ms + NOW = 1_000_000_000 + + def needs_render(before, after, events=0, now=NOW): + return custom_shader_needs_render(before, after, events, now) + + idle = (False, NEVER, NEVER) + static = (True, NEVER, NEVER) + animating = (True, STEP, NEVER) + + # Nothing active and nothing pending: the shader layer must not ask for frames. + self.assertFalse(needs_render(idle, idle)) + self.assertFalse(needs_render(idle, idle, events=1)) + # A static group only redraws when an event may have changed its output. + self.assertFalse(needs_render(static, static)) + self.assertTrue(needs_render(static, static, events=1)) + # An animated group redraws every tick... + self.assertTrue(needs_render(animating, animating)) + # ...and gets one final frame after it stops, so the effect is cleared. + self.assertTrue(needs_render(animating, static)) + self.assertTrue(needs_render(animating, idle)) + # Groups turning on or off is always a visible change. + self.assertTrue(needs_render(idle, static)) + self.assertTrue(needs_render(static, idle)) + # The tick on which a duration bounded animation expires needs a frame, + # earlier ticks do not. + self.assertTrue(needs_render((True, NEVER, NOW), static)) + self.assertTrue(needs_render((True, NEVER, NOW - 1), static)) + self.assertFalse(needs_render((True, NEVER, NOW + 1), static)) + + # A window with no custom shaders configured must settle into asking for + # no frames at all, otherwise an idle kitty redraws forever. Test both a + # zero initialized window and one initialized the way add_os_window() + # does it, since zero is not the MONOTONIC_T_MAX sentinel. + self.ae(simulate_custom_shader_render_ticks(8, 0, True), [False] * 8) + self.ae(simulate_custom_shader_render_ticks(8)[1:], [False] * 7) + # Even a steady stream of shader events must not produce frames when + # there are no shaders to draw. + self.ae(simulate_custom_shader_render_ticks(8, 0xFF, True), [False] * 8) def test_custom_shader_reenable(self): from kitty.fast_data_types import CUSTOM_END_PROGRAM from kitty.options.types import defaults loader = LoadShaderPrograms() - loader.last_built_custom_shaders = {} pipeline = parse_pipeline_definition(['startgroup', 'animation_step 0', 'shaders focus-highlight', 'endgroup'], 'test') self.ae(pipeline['groups'][0]['animation_step'], 0) with (