mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-10-11 16:32:50 +00:00
cleanup previous PR fixing one regression and doing code cleanups
This commit is contained in:
parent
5d58f2f4cc
commit
d37f845293
8 changed files with 224 additions and 84 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
158
kitty/shaders.c
158
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 */
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue