From 61507b5abad025a6e2f1193297389c5239c96bdf Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Sat, 13 Apr 2024 17:21:45 +0100 Subject: [PATCH 01/23] transition: add step and cubic bezier interpolator Signed-off-by: Yuxuan Shui --- src/transition.c | 128 +++++++++++++++++++++++++++++++++++++++++++++++ src/transition.h | 5 +- 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/src/transition.c b/src/transition.c index fc03142709..9591fd1f57 100644 --- a/src/transition.c +++ b/src/transition.c @@ -142,6 +142,10 @@ static double curve_sample_linear(const struct curve *this attr_unused, double p static void noop_free(const struct curve *this attr_unused) { } +static void trivial_free(const struct curve *this) { + free((void *)this); +} + const struct curve *curve_new_linear(void) { static const struct curve ret = { .sample = curve_sample_linear, @@ -149,3 +153,127 @@ const struct curve *curve_new_linear(void) { }; return &ret; } + +/// Cubic bezier interpolator. +/// +/// Stolen from servo: +/// https://searchfox.org/mozilla-central/rev/5da2d56d12/servo/components/style/bezier.rs +struct cubic_bezier_curve { + struct curve base; + double ax, bx, cx; + double ay, by, cy; +}; + +static inline double cubic_bezier_sample_x(const struct cubic_bezier_curve *self, double t) { + return ((self->ax * t + self->bx) * t + self->cx) * t; +} + +static inline double cubic_bezier_sample_y(const struct cubic_bezier_curve *self, double t) { + return ((self->ay * t + self->by) * t + self->cy) * t; +} + +static inline double +cubic_bezier_sample_derivative_x(const struct cubic_bezier_curve *self, double t) { + return (3.0 * self->ax * t + 2.0 * self->bx) * t + self->cx; +} + +// Solve for the `t` in cubic bezier function that corresponds to `x` +static inline double cubic_bezier_solve_x(const struct cubic_bezier_curve *this, double x) { + static const int NEWTON_METHOD_ITERATIONS = 8; + double t = x; + // Fast path: try Newton's method. + for (int i = 0; i < NEWTON_METHOD_ITERATIONS; i++) { + double x2 = cubic_bezier_sample_x(this, t); + if (fabs(x2 - x) < 1e-7) { + return t; + } + double dx = cubic_bezier_sample_derivative_x(this, t); + if (fabs(dx) < 1e-6) { + break; + } + t -= (x2 - x) / dx; + } + + // Slow path: Use bisection. + double low = 0.0, high = 1.0; + t = x; + while (high - low > 1e-7) { + double x2 = cubic_bezier_sample_x(this, t); + if (fabs(x2 - x) < 1e-7) { + return t; + } + if (x > x2) { + low = t; + } else { + high = t; + } + t = (high - low) / 2.0 + low; + } + return t; +} + +static double curve_sample_cubic_bezier(const struct curve *base, double progress) { + auto this = (struct cubic_bezier_curve *)base; + assert(progress >= 0 && progress <= 1); + if (progress == 0 || progress == 1) { + return progress; + } + double t = cubic_bezier_solve_x(this, progress); + return cubic_bezier_sample_y(this, t); +} + +const struct curve *curve_new_cubic_bezier(double x1, double y1, double x2, double y2) { + if (x1 == y1 && x2 == y2) { + return curve_new_linear(); + } + + assert(x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1); + auto ret = ccalloc(1, struct cubic_bezier_curve); + ret->base.sample = curve_sample_cubic_bezier; + ret->base.free = trivial_free; + + double cx = 3. * x1; + double bx = 3. * (x2 - x1) - cx; + double cy = 3. * y1; + double by = 3. * (y2 - y1) - cy; + ret->ax = 1. - cx - bx; + ret->bx = bx; + ret->cx = cx; + ret->ay = 1. - cy - by; + ret->by = by; + ret->cy = cy; + return &ret->base; +} + +struct step_curve { + struct curve base; + int steps; + bool jump_start, jump_end; +}; + +static double curve_sample_step(const struct curve *base, double progress) { + auto this = (struct step_curve *)base; + double y_steps = this->steps - 1 + this->jump_end + this->jump_start, + x_steps = this->steps; + if (progress == 1) { + return 1; + } + if (progress == 0) { + return this->jump_start ? 1 / y_steps : 0; + } + + double scaled = progress * x_steps; + double quantized = this->jump_start ? ceil(scaled) : floor(scaled); + return quantized / y_steps; +} + +const struct curve *curve_new_step(int steps, bool jump_start, bool jump_end) { + assert(steps > 0); + auto ret = ccalloc(1, struct step_curve); + ret->base.sample = curve_sample_step; + ret->base.free = trivial_free; + ret->steps = steps; + ret->jump_start = jump_start; + ret->jump_end = jump_end; + return &ret->base; +} diff --git a/src/transition.h b/src/transition.h index e7c20efc20..e52de4e550 100644 --- a/src/transition.h +++ b/src/transition.h @@ -96,11 +96,12 @@ bool animatable_skip(struct animatable *a); /// /// Returns if the animatable is now animated. bool animatable_set_target(struct animatable *a, double target, double duration, - const struct curve *curve, - transition_callback_fn cb, void *data); + const struct curve *curve, transition_callback_fn cb, void *data); /// Create a new animatable. struct animatable animatable_new(double value); // ========================== Interpolators ========================== const struct curve *curve_new_linear(void); +const struct curve *curve_new_cubic_bezier(double x1, double y1, double x2, double y2); +const struct curve *curve_new_step(int steps, bool jump_start, bool jump_end); \ No newline at end of file From 614ebbbd43c2bcc162341b5b286d13a331a8068d Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Sat, 20 Apr 2024 12:10:31 +0100 Subject: [PATCH 02/23] transition: parse curve from string Signed-off-by: Yuxuan Shui --- src/transition.c | 109 ++++++++++++++++++++++++++++++++++++++++++++--- src/transition.h | 3 +- 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/src/transition.c b/src/transition.c index 9591fd1f57..6521a612c8 100644 --- a/src/transition.c +++ b/src/transition.c @@ -6,6 +6,7 @@ #include #include "compiler.h" +#include "string_utils.h" #include "transition.h" #include "utils.h" @@ -146,12 +147,12 @@ static void trivial_free(const struct curve *this) { free((void *)this); } +static const struct curve static_linear_curve = { + .sample = curve_sample_linear, + .free = noop_free, +}; const struct curve *curve_new_linear(void) { - static const struct curve ret = { - .sample = curve_sample_linear, - .free = noop_free, - }; - return &ret; + return &static_linear_curve; } /// Cubic bezier interpolator. @@ -277,3 +278,101 @@ const struct curve *curve_new_step(int steps, bool jump_start, bool jump_end) { ret->jump_end = jump_end; return &ret->base; } + +const struct curve *parse_linear(const char *str, const char **end, char **err) { + *end = str; + *err = NULL; + return &static_linear_curve; +} + +const struct curve *parse_steps(const char *input_str, const char **out_end, char **err) { + const char *str = input_str; + *err = NULL; + if (*str != '(') { + asprintf(err, "Invalid steps %s.", str); + return NULL; + } + str += 1; + str = skip_space(str); + char *end; + auto steps = strtol(str, &end, 10); + if (end == str || steps > INT_MAX) { + asprintf(err, "Invalid step count at \"%s\".", str); + return NULL; + } + str = skip_space(end); + if (*str != ',') { + asprintf(err, "Invalid steps argument list \"%s\".", input_str); + return NULL; + } + str = skip_space(str + 1); + bool jump_start = + starts_with(str, "jump-start", true) || starts_with(str, "jump-both", true); + bool jump_end = + starts_with(str, "jump-end", true) || starts_with(str, "jump-both", true); + if (!jump_start && !jump_end && !starts_with(str, "jump-none", true)) { + asprintf(err, "Invalid jump setting for steps \"%s\".", str); + return NULL; + } + str += jump_start ? (jump_end ? 9 : 10) : (jump_end ? 8 : 9); + str = skip_space(str); + if (*str != ')') { + asprintf(err, "Invalid steps argument list \"%s\".", input_str); + return NULL; + } + *out_end = str + 1; + return curve_new_step((int)steps, jump_start, jump_end); +} + +const struct curve * +parse_cubic_bezier(const char *input_str, const char **out_end, char **err) { + double numbers[4]; + const char *str = input_str; + if (*str != '(') { + asprintf(err, "Invalid cubic-bazier %s.", str); + return NULL; + } + str += 1; + for (int i = 0; i < 4; i++) { + str = skip_space(str); + + const char *end = NULL; + numbers[i] = strtod_simple(str, &end); + if (end == str) { + asprintf(err, "Invalid number %s.", str); + return NULL; + } + str = skip_space(end); + const char expected = i == 3 ? ')' : ','; + if (*str != expected) { + asprintf(err, "Invalid cubic-bazier argument list %s.", input_str); + return NULL; + } + str += 1; + } + *out_end = str; + return curve_new_cubic_bezier(numbers[0], numbers[1], numbers[2], numbers[3]); +} + +typedef const struct curve *(*curve_parser)(const char *str, const char **end, char **err); + +static const struct { + curve_parser parse; + const char *name; +} curve_parsers[] = { + {parse_cubic_bezier, "cubic-bezier"}, + {parse_linear, "linear"}, + {parse_steps, "steps"}, +}; + +const struct curve *curve_parse(const char *str, const char **end, char **err) { + str = skip_space(str); + for (size_t i = 0; i < ARR_SIZE(curve_parsers); i++) { + auto name_len = strlen(curve_parsers[i].name); + if (strncasecmp(str, curve_parsers[i].name, name_len) == 0) { + return curve_parsers[i].parse(str + name_len, end, err); + } + } + asprintf(err, "Unknown curve type \"%s\".", str); + return NULL; +} diff --git a/src/transition.h b/src/transition.h index e52de4e550..b5131f4b7f 100644 --- a/src/transition.h +++ b/src/transition.h @@ -104,4 +104,5 @@ struct animatable animatable_new(double value); const struct curve *curve_new_linear(void); const struct curve *curve_new_cubic_bezier(double x1, double y1, double x2, double y2); -const struct curve *curve_new_step(int steps, bool jump_start, bool jump_end); \ No newline at end of file +const struct curve *curve_new_step(int steps, bool jump_start, bool jump_end); +const struct curve *curve_parse(const char *str, const char **end, char **err); From 0f82266ecb6b2b4643f3a2a3f953d0ba9863f908 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Sat, 20 Apr 2024 12:10:56 +0100 Subject: [PATCH 03/23] Add animation script Allow the definition of customizable animations using expressions and transition curves. Signed-off-by: Yuxuan Shui --- src/meson.build | 2 +- src/script.c | 1279 +++++++++++++++++++++++++++++++++++++++++++++++ src/script.h | 60 +++ src/utils.h | 10 + 4 files changed, 1350 insertions(+), 1 deletion(-) create mode 100644 src/script.c create mode 100644 src/script.h diff --git a/src/meson.build b/src/meson.build index 0513e09613..1a52240456 100644 --- a/src/meson.build +++ b/src/meson.build @@ -11,7 +11,7 @@ srcs = [ files('picom.c', 'win.c', 'c2.c', 'x.c', 'config.c', 'vsync.c', 'utils. 'diagnostic.c', 'string_utils.c', 'render.c', 'kernel.c', 'log.c', 'options.c', 'event.c', 'cache.c', 'atom.c', 'file_watch.c', 'statistics.c', 'vblank.c', 'transition.c', 'wm.c', 'renderer/layout.c', 'renderer/command_builder.c', - 'renderer/renderer.c', 'renderer/damage.c', 'config_libconfig.c', 'inspect.c') ] + 'renderer/renderer.c', 'renderer/damage.c', 'config_libconfig.c', 'inspect.c', 'script.c') ] picom_inc = include_directories('.') cflags = [] diff --git a/src/script.c b/src/script.c new file mode 100644 index 0000000000..69d87d72f6 --- /dev/null +++ b/src/script.c @@ -0,0 +1,1279 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui +#include "script.h" +#include +#include +#include +#include +#include "list.h" +#include "string_utils.h" +#include "transition.h" +#include "uthash_extra.h" +#include "utils.h" + +enum op { + OP_ADD = 0, + OP_SUB, + OP_MUL, + OP_DIV, + /// Exponent + OP_EXP, + /// Negation + OP_NEG, +}; + +enum instruction_type { + /// Push an immediate value to the top of the stack + INST_IMM = 0, + /// Pop two values from the top of the stack, apply operator, + /// and push the result to the top of the stack + INST_OP, + /// Load a memory slot and push its value to the top of the stack. + INST_LOAD, + /// Load from evaluation context and push the value to the top of the stack. + INST_LOAD_CTX, + /// Pop one value from the top of the stack, and store it into a memory slot. + INST_STORE, + /// Pop one value from the top of the stack, if the memory slot contains NaN, + /// store it into the memory slot; otherwise discard the value. + INST_STORE_OVER_NAN, + /// Evaluate a curve at the given point of elapsed time, push the result to + /// the top of the stack. + INST_CURVE, + /// Jump to the branch target only when the script is evaluated for the first + /// time. Used to perform initialization and such. + INST_BRANCH_ONCE, + /// Unconditional branch + INST_BRANCH, + INST_HALT, +}; + +struct instruction { + enum instruction_type type; + union { + double imm; + enum op op; + /// Memory slot for load and store + unsigned slot; + /// Context offset for load_ctx + ptrdiff_t ctx; + /// Relative PC change for branching + int rel; + /// The curve + struct { + const struct curve *curve; + double duration; + double delay; + }; + }; +}; + +struct fragment { + struct list_node siblings; + /// If `once` is true, this is the succeeding fragment if + /// this fragment is executed. + struct fragment *once_next; + /// The succeeding fragment of this fragment. If `once` is true, this is the + /// succeeding fragment if this fragment is NOT executed. + struct fragment *next; + /// Number of instructions + unsigned ninstrs; + /// The address of the first instruction of this fragment in compiled script. + /// For `once` fragments, this is the address of the branch instruction. + unsigned addr; + /// Whether code is generated for this fragment. 0 = not emitted, 1 = emitted, 2 = + /// in queue + bool emitted; + struct instruction instrs[]; +}; + +/// Represent a variable during compilation. Contains a sequence of fragments, and +/// dependency of this variable. +struct compilation_stack { + struct fragment *entry_point; + struct fragment **exit; + unsigned index; + /// Number of dependencies + unsigned ndeps; + /// Whether the fragments loads from execution context + bool need_context; + /// Dependencies + unsigned deps[]; +}; + +enum variable_type { + VAR_TYPE_TRANSITION, + VAR_TYPE_IMM, + VAR_TYPE_EXPR, +}; + +/// Store metadata about where the result of a variable is stored +struct variable_allocation { + UT_hash_handle hh; + char *name; + unsigned index; + /// The memory slot for variable named `name` + unsigned slot; +}; + +/// When interrupting an already executing script and starting a new script, +/// we might want to inherit some of the existing values of variables as starting points, +/// i.e. we want to "resume" animation for the current state. This is configurable, and +/// can be disabled by enabling the `reset` property on a transition. This struct store +/// where the `start` variables of those "resumable" transition variables, which can be +/// overridden at the start of execution for this use case. +struct overridable_slot { + UT_hash_handle hh; + char *name; + unsigned slot; +}; + +struct script { + unsigned len; + unsigned nslots; + unsigned stack_size; + double max_duration; + struct variable_allocation *vars; + struct overridable_slot *overrides; + struct instruction instrs[]; +}; + +struct script_compile_context { + struct script_context_info_internal *context_info; + struct variable_allocation *vars; + struct overridable_slot *overrides; + unsigned allocated_slots; + unsigned max_stack; + double max_duration; + const char *current_variable_name; + int *compiled; + struct list_node all_fragments; + struct fragment **tail, *head; + /// Fragments that can be executed once at the beginning of the execution. + /// For example, storing imms into memory slots. + struct fragment **once_tail; + /// Fragments that should be executed once at the end of the first execution. + struct fragment **once_end_tail, *once_end_head; +}; + +static const char operators[] = "+-*/^"; +static const enum op operator_types[] = {OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_EXP}; +static const int operator_pre[] = {0, 0, 1, 1, 2}; + +static void log_instruction_(enum log_level level, const char *func, unsigned index, + const struct instruction *inst) { + if (log_get_level_tls() > level) { + return; + } +#define logv(fmt, ...) log_printf(tls_logger, level, func, "%u: " fmt, index, __VA_ARGS__) + switch (inst->type) { + case INST_IMM: logv("imm %f", inst->imm); break; + case INST_BRANCH: logv("br %d", inst->rel); break; + case INST_BRANCH_ONCE: logv("br_once %d", inst->rel); break; + case INST_HALT: log_printf(tls_logger, level, func, "%u: halt", index); break; + case INST_CURVE: log_printf(tls_logger, level, func, "%u: curve", index); break; + case INST_OP: logv("op %d (%c)", inst->op, operators[inst->op]); break; + case INST_LOAD: logv("load %u", inst->slot); break; + case INST_STORE: logv("store %u", inst->slot); break; + case INST_STORE_OVER_NAN: logv("store/nan %u", inst->slot); break; + case INST_LOAD_CTX: logv("load_ctx *(%ld)", inst->ctx); break; + } +#undef logv +} +#define log_instruction(level, i, inst) \ + log_instruction_(LOG_LEVEL_##level, __func__, i, &(inst)) + +static double parse_time_unit(const char *str, const char **end) { + if (strncasecmp(str, "s", 1) == 0) { + *end = str + 1; + return 1; + } + if (strncasecmp(str, "ms", 2) == 0) { + *end = str + 2; + return 1e-3; + } + return NAN; +} + +static double parse_duration(const char *input_str, const char **end, char **err) { + const char *str = input_str; + double number = strtod_simple(str, end); + if (*end == str) { + asprintf(err, "Invalid curve definition \"%s\".", input_str); + return NAN; + } + str = *end; + double unit = parse_time_unit(str, end); + if (*end == str) { + asprintf(err, "Invalid curve definition \"%s\" (invalid time unit at \"%s\").", + input_str, str); + *end = input_str; + return NAN; + } + return number * unit; +} + +/// Parse a timing function. +/// +/// Syntax for this is the same as CSS transitions: +/// +/// Examples: +/// 1s cubic-bezier(0.1, 0.2, 0.3, 0.4) 0.4s +/// 2s steps(5, jump-end) +static const struct curve * +parse_timing_function(const char *input_str, double *duration, double *delay, char **err) { + const char *str = skip_space(input_str); + const char *end = NULL; + *duration = parse_duration(str, &end, err); + if (str == end) { + return NULL; + } + + if (*duration == 0) { + asprintf(err, "Timing function cannot have a zero duration."); + return NULL; + } + + *delay = 0; + str = skip_space(end); + if (!*str) { + return curve_new_linear(); + } + + auto curve = curve_parse(str, &end, err); + if (!curve) { + return NULL; + } + + str = skip_space(end); + if (!*str) { + return curve; + } + *delay = parse_duration(str, &end, err); + if (str == end) { + curve->free(curve); + return NULL; + } + return curve; +} + +static char parse_op(const char *input_str, const char **end, char **err) { + char *op = strchr(operators, input_str[0]); + *err = NULL; + if (op != NULL) { + *end = input_str + 1; + return input_str[0]; + } + + asprintf(err, "Expected one of \"%s\", got '%c'.", operators, input_str[0]); + *end = input_str; + return 0; +} + +static enum op char_to_op(char ch) { + char *op = strchr(operators, ch); + BUG_ON(op == NULL); + return operator_types[op - operators]; +} + +struct script_context_info_internal { + UT_hash_handle hh; + struct script_context_info info; +}; + +struct expression_parser_context { + char *op_stack; + struct compilation_stack *entry; + size_t len; + + unsigned op_top, operand_top; + bool need_context; +}; + +/// Parse a number or a variable. Variable can optionally be prefixed with a minus sign. +static void +parse_raw_operand(struct expression_parser_context *ctx, const char *str, const char **end, + struct script_compile_context *script_ctx, char **err) { + double number = strtod_simple(str, end); + auto expr = ctx->entry->entry_point; + *err = NULL; + if (*end != str) { + expr->instrs[expr->ninstrs++] = (struct instruction){ + .type = INST_IMM, + .imm = number, + }; + return; + } + bool neg = false; + bool succeeded = false; + if (**end == '-') { + neg = true; + str = skip_space(str + 1); + *end = str; + } + while (**end) { + if (isalnum(**end) || **end == '-' || **end == '_') { + succeeded = true; + (*end)++; + } else { + break; + } + } + if (!succeeded) { + asprintf(err, "Expected a number or a variable name, got \"%s\".", str); + *end = str; + return; + } + struct variable_allocation *var = NULL; + struct script_context_info_internal *exe_ctx = NULL; + HASH_FIND(hh, script_ctx->vars, str, (unsigned long)(*end - str), var); + HASH_FIND(hh, script_ctx->context_info, str, (unsigned long)(*end - str), exe_ctx); + if (var != NULL) { + expr->instrs[expr->ninstrs++] = (struct instruction){ + .type = INST_LOAD, + .slot = var->slot, + }; + ctx->entry->deps[ctx->entry->ndeps++] = var->index; + } else if (exe_ctx != NULL) { + expr->instrs[expr->ninstrs++] = (struct instruction){ + .type = INST_LOAD_CTX, + .ctx = exe_ctx->info.offset, + }; + ctx->need_context = true; + } else { + asprintf(err, "variable name \"%.*s\" is not defined", (int)(*end - str), str); + *end = str; + return; + } + + if (neg) { + expr->instrs[expr->ninstrs++] = (struct instruction){ + .type = INST_OP, + .op = OP_NEG, + }; + } +} +static inline double op_eval(double l, enum op op, double r) { + switch (op) { + case OP_ADD: return l + r; + case OP_SUB: return l - r; + case OP_DIV: return l / r; + case OP_MUL: return l * r; + case OP_EXP: return pow(l, r); + case OP_NEG: return -l; + } + unreachable(); +} +static bool pop_op(const char *input_str, struct expression_parser_context *ctx, char **err) { + if (ctx->operand_top < 2) { + asprintf(err, "Missing operand for operator %c, in expression %s", + ctx->op_stack[ctx->op_top - 1], input_str); + return false; + } + auto f = ctx->entry->entry_point; + // Both operands are immediates, do constant propagation. + if (f->instrs[f->ninstrs - 1].type == INST_IMM && + f->instrs[f->ninstrs - 2].type == INST_IMM) { + double imm = op_eval(f->instrs[f->ninstrs - 1].imm, + char_to_op(ctx->op_stack[ctx->op_top]), + f->instrs[f->ninstrs - 2].imm); + ctx->operand_top -= 1; + f->instrs[f->ninstrs - 2].imm = imm; + f->ninstrs -= 1; + ctx->op_top -= 1; + return true; + } + f->instrs[f->ninstrs].type = INST_OP; + f->instrs[f->ninstrs].op = char_to_op(ctx->op_stack[ctx->op_top - 1]); + f->ninstrs += 1; + ctx->op_top -= 1; + ctx->operand_top -= 1; + return true; +} + +/// Parse an operand surrounded by some parenthesis: +/// `(((((var))` or `(((var` or `var)))` +static bool parse_operand_or_paren(struct expression_parser_context *ctx, + const char *input_str, const char **end, + struct script_compile_context *script_ctx, char **err) { + const char *str = input_str; + while (*str == '(') { + str = skip_space(str + 1); + ctx->op_stack[ctx->op_top++] = '('; + } + + parse_raw_operand(ctx, str, end, script_ctx, err); + if (str == *end) { + return false; + } + str = skip_space(*end); + ctx->operand_top += 1; + if (ctx->operand_top > script_ctx->max_stack) { + script_ctx->max_stack = ctx->operand_top; + } + + while (*str == ')') { + while (ctx->op_top > 0 && ctx->op_stack[ctx->op_top - 1] != '(') { + if (!pop_op(str, ctx, err)) { + return false; + } + } + if (ctx->op_top == 0) { + asprintf(err, "Unmatched ')' in expression \"%s\"", input_str); + return false; + } + ctx->op_top -= 1; + str = skip_space(str + 1); + } + *end = str; + return true; +} + +static struct fragment *fragment_new(struct script_compile_context *ctx, unsigned ninstrs) { + struct fragment *fragment = calloc( + 1, sizeof(struct fragment) + sizeof(struct instruction[max2(1, ninstrs)])); + allocchk(fragment); + list_insert_after(&ctx->all_fragments, &fragment->siblings); + return fragment; +} + +/// Precedence based expression parser. +static bool expression_compile(struct compilation_stack **stack_entry, const char *input_str, + struct script_compile_context *script_ctx, unsigned slot, + bool allow_override, char **err) { + const char *str = skip_space(input_str); + const size_t len = strlen(str); + BUG_ON(len > UINT_MAX); + if (len == 0) { + return false; + } + // At most each character in `input_str` could map to an individual instruction + auto fragment = fragment_new(script_ctx, (unsigned)len + 1); + *stack_entry = calloc(1, sizeof(struct compilation_stack) + sizeof(unsigned[len])); + (*stack_entry)->entry_point = fragment; + (*stack_entry)->exit = &fragment->next; + + struct expression_parser_context ctx = { + .op_stack = ccalloc(len, char), + .entry = *stack_entry, + .op_top = 0, + .operand_top = 0, + }; + const char *end = NULL; + bool succeeded = false; + if (!parse_operand_or_paren(&ctx, str, &end, script_ctx, err)) { + goto end; + } + + str = end; + while (*str) { + str = skip_space(str); + + char new_op = parse_op(str, &end, err); + if (str == end) { + goto end; + } + str = skip_space(end); + + int pre = operator_pre[char_to_op(new_op)]; + while (ctx.op_top > 0 && ctx.op_stack[ctx.op_top - 1] != '(' && + pre <= operator_pre[char_to_op(ctx.op_stack[ctx.op_top - 1])]) { + if (!pop_op(input_str, &ctx, err)) { + goto end; + } + } + ctx.op_stack[ctx.op_top++] = new_op; + if (!parse_operand_or_paren(&ctx, str, &end, script_ctx, err)) { + goto end; + } + str = end; + } + while (ctx.op_top != 0) { + if (!pop_op(input_str, &ctx, err)) { + goto end; + } + } + if (ctx.operand_top != 1) { + asprintf(err, "excessive operand on stack %s", input_str); + goto end; + } + succeeded = true; + // Save the value of the expression + // For overridable variables, we use store/nan, so caller can "pre-fill" the + // variable to override it. + fragment->instrs[fragment->ninstrs].type = + allow_override ? INST_STORE_OVER_NAN : INST_STORE; + fragment->instrs[fragment->ninstrs++].slot = slot; + (*stack_entry)->need_context = ctx.need_context; + +end: + free(ctx.op_stack); + if (!succeeded) { + free(*stack_entry); + } + return succeeded; +} + +static struct compilation_stack * +make_imm_stack_entry(struct script_compile_context *ctx, double imm, unsigned slot, + bool allow_override) { + auto fragment = fragment_new(ctx, 2); + fragment->ninstrs = 2; + fragment->instrs[0].type = INST_IMM; + fragment->instrs[0].imm = imm; + fragment->instrs[1].type = allow_override ? INST_STORE_OVER_NAN : INST_STORE; + fragment->instrs[1].slot = slot; + *ctx->once_tail = fragment; + ctx->once_tail = &fragment->next; + + // Insert an empty fragment for the stack entry + fragment = fragment_new(ctx, 0); + + struct compilation_stack *entry = ccalloc(1, struct compilation_stack); + allocchk(entry); + entry->entry_point = fragment; + entry->exit = &fragment->next; + return entry; +} + +static bool +transition_compile(struct compilation_stack **stack_entry, config_setting_t *setting, + struct script_compile_context *ctx, unsigned slot, char **out_err) { + const char *str = NULL; + int boolean = 0; + double number = 0; + double duration, delay; + const struct curve *curve; + bool reset = false; + char *err = NULL; + if (!config_setting_lookup_string(setting, "timing", &str)) { + asprintf(out_err, "Transition section does not contain a timing function. Line %d.", + config_setting_source_line(setting)); + return false; + } + curve = parse_timing_function(str, &duration, &delay, &err); + if (curve == NULL) { + asprintf(out_err, "%s Line %d.", err, config_setting_source_line(setting)); + free(err); + return false; + } + if (duration > ctx->max_duration) { + ctx->max_duration = duration; + } + + if (config_setting_lookup_bool(setting, "reset", &boolean)) { + reset = boolean; + } + + auto start_slot = ctx->allocated_slots; + auto end_slot = ctx->allocated_slots + 1; + ctx->allocated_slots += 2; + if (!reset) { + auto override = ccalloc(1, struct overridable_slot); + override->name = strdup(ctx->current_variable_name); + override->slot = start_slot; + HASH_ADD_STR(ctx->overrides, name, override); + } + struct compilation_stack *start = NULL, *end = NULL; + if (config_setting_lookup_float(setting, "start", &number)) { + start = make_imm_stack_entry(ctx, number, start_slot, true); + } else if (!config_setting_lookup_string(setting, "start", &str)) { + asprintf(out_err, + "Transition definition does not contain a start value or " + "expression. Line %d.", + config_setting_source_line(setting)); + return false; + } else if (!expression_compile(&start, str, ctx, start_slot, !reset, &err)) { + asprintf(out_err, "transition has an invalid start expression: %s Line %d.", + err, config_setting_source_line(setting)); + free(err); + return false; + } + + if (config_setting_lookup_float(setting, "end", &number)) { + end = make_imm_stack_entry(ctx, number, end_slot, false); + } else if (!config_setting_lookup_string(setting, "end", &str)) { + asprintf(out_err, + "Transition definition does not contain a end value or " + "expression. Line %d.", + config_setting_source_line(setting)); + return false; + } else if (!expression_compile(&end, str, ctx, end_slot, false, &err)) { + asprintf(out_err, "Transition has an invalid end expression: %s. Line %d", + err, config_setting_source_line(setting)); + free(err); + return false; + } + + struct instruction instrs[] = { + {.type = INST_LOAD, .slot = end_slot}, + {.type = INST_LOAD, .slot = start_slot}, + {.type = INST_OP, .op = OP_SUB}, // v1 = end - start + {.type = INST_CURVE, .curve = curve, .duration = duration, .delay = delay}, + {.type = INST_OP, .op = OP_MUL}, // v2 = v1 * curve + {.type = INST_LOAD, .slot = start_slot}, + {.type = INST_OP, .op = OP_ADD}, // v3 = v2 + start + {.type = INST_STORE, .slot = slot}, + }; + if (ctx->max_stack < 2) { + // The list of instructions above needs 2 stack slots + ctx->max_stack = 2; + } + struct fragment *fragment = fragment_new(ctx, ARR_SIZE(instrs)); + memcpy(fragment->instrs, instrs, sizeof(instrs)); + fragment->ninstrs = ARR_SIZE(instrs); + + *stack_entry = calloc( + 1, sizeof(struct compilation_stack) + sizeof(unsigned[max2(1, start->ndeps)])); + allocchk(*stack_entry); + struct fragment **next = &(*stack_entry)->entry_point; + + // Dependencies of the start value is the real dependencies of this transition + // variable. + (*stack_entry)->ndeps = start->ndeps; + // If start value has dependencies, we calculate it like this: + // if (first_evaluation) mem[start_slot] = ; + // Otherwise, it's put into the "evaluation once" block. + if (start->ndeps > 0) { + memcpy((*stack_entry)->deps, start->deps, sizeof(unsigned[start->ndeps])); + + auto branch = fragment_new(ctx, 0); + *next = branch; + branch->once_next = start->entry_point; + + auto phi = fragment_new(ctx, 0); + *start->exit = phi; + branch->next = phi; + next = &phi->next; + } else { + *ctx->once_tail = start->entry_point; + ctx->once_tail = start->exit; + } + + if (end->ndeps > 0) { + // Otherwise, the end value is not static, luckily we can still just + // calculate it at the end of the first evaluation, since at that point + // nothing can depends on a transition's end value. However, for the + // calculation of this transition curve, we don't yet have the end value. + // So we do this: `mem[output_slot] = mem[start_slot]`, instead of compute + // it normally. + *ctx->once_end_tail = end->entry_point; + ctx->once_end_tail = end->exit; + + const struct instruction load_store_instrs[] = { + {.type = INST_LOAD, .slot = start_slot}, + {.type = INST_STORE, .slot = slot}, + }; + auto load_store = fragment_new(ctx, ARR_SIZE(load_store_instrs)); + load_store->ninstrs = ARR_SIZE(load_store_instrs); + memcpy(load_store->instrs, load_store_instrs, sizeof(load_store_instrs)); + + auto branch = fragment_new(ctx, 0); + *next = branch; + branch->once_next = load_store; + branch->next = fragment; + + auto phi = fragment_new(ctx, 0); + load_store->next = phi; + fragment->next = phi; + (*stack_entry)->exit = &phi->next; + } else { + // The end value has no dependencies, so it only needs to be evaluated + // once at the start of the first evaluation. And therefore we can + // evaluate the curve like normal even for the first evaluation. + *ctx->once_tail = end->entry_point; + ctx->once_tail = end->exit; + + *next = fragment; + (*stack_entry)->exit = &fragment->next; + } + + free(end); + free(start); + return true; +} + +static void instruction_deinit(struct instruction *instr) { + if (instr->type == INST_CURVE) { + instr->curve->free(instr->curve); + } +} + +static void fragment_free(struct fragment *frag) { + list_remove(&frag->siblings); + for (unsigned i = 0; i < frag->ninstrs; i++) { + instruction_deinit(&frag->instrs[i]); + } + free(frag); +} + +#define free_hash_table(head) \ + do { \ + typeof(head) i, ni; \ + HASH_ITER(hh, head, i, ni) { \ + HASH_DEL(head, i); \ + free(i->name); \ + free(i); \ + } \ + } while (0) + +void script_free(struct script *script) { + for (unsigned i = 0; i < script->len; i++) { + instruction_deinit(&script->instrs[i]); + } + free_hash_table(script->vars); + free_hash_table(script->overrides); + free(script); +} + +static bool script_compile_one(struct compilation_stack **stack_entry, config_setting_t *var, + struct script_compile_context *ctx, char **err) { + ctx->current_variable_name = config_setting_name(var); + + struct variable_allocation *alloc = NULL; + HASH_FIND_STR(ctx->vars, ctx->current_variable_name, alloc); + BUG_ON(!alloc); + + if (config_setting_is_number(var)) { + *stack_entry = make_imm_stack_entry(ctx, config_setting_get_float(var), + alloc->slot, false); + return true; + } + const char *str = config_setting_get_string(var); + if (str != NULL) { + char *tmp_err = NULL; + bool succeeded = + expression_compile(stack_entry, str, ctx, alloc->slot, false, &tmp_err); + if (!succeeded) { + asprintf(err, "Failed to parse expression at line %d. %s", + config_setting_source_line(var), tmp_err); + free(tmp_err); + } + return succeeded; + } + + if (!config_setting_is_group(var)) { + asprintf(err, + "Invalid variable \"%s\", it must be either a number, a string, " + "or a config group defining a transition.", + config_setting_name(var)); + return false; + } + return transition_compile(stack_entry, var, ctx, alloc->slot, err); +} + +static void report_cycle(struct compilation_stack **stack, unsigned top, unsigned index, + config_setting_t *setting, char **err) { + unsigned start = top - 1; + while (stack[start]->index != index) { + start -= 1; + } + auto last_var = config_setting_get_elem(setting, index); + auto last_name = config_setting_name(last_var); + auto len = (size_t)(top - start) * 4 /* " -> " */ + strlen(last_name); + for (unsigned i = start; i < top; i++) { + auto var = config_setting_get_elem(setting, stack[i]->index); + len += strlen(config_setting_name(var)); + } + auto buf = ccalloc(len + 1, char); + auto pos = buf; + for (unsigned i = start; i < top; i++) { + auto var = config_setting_get_elem(setting, stack[i]->index); + auto name = config_setting_name(var); + strcpy(pos, name); + pos += strlen(name); + strcpy(pos, " -> "); + pos += 4; + } + strcpy(pos, last_name); + + asprintf(err, "Cyclic references detected in animation script defined at line %d: %s", + config_setting_source_line(setting), buf); + free(buf); +} + +static bool script_compile_one_recursive(struct compilation_stack **stack, + config_setting_t *setting, unsigned index, + struct script_compile_context *ctx, char **err) { + unsigned stack_top = 1; + if (!script_compile_one(&stack[0], config_setting_get_elem(setting, index), ctx, err)) { + return false; + } + stack[0]->index = index; + ctx->compiled[index] = 2; + while (stack_top) { + auto stack_entry = stack[stack_top - 1]; + while (stack_entry->ndeps) { + auto dep = stack_entry->deps[--stack_entry->ndeps]; + if (ctx->compiled[dep] == 1) { + continue; + } + if (ctx->compiled[dep] == 2) { + report_cycle(stack, stack_top, dep, setting, err); + goto out; + } + + auto dep_setting = config_setting_get_elem(setting, dep); + if (!script_compile_one(&stack[stack_top], dep_setting, ctx, err)) { + goto out; + } + stack[stack_top]->index = dep; + ctx->compiled[dep] = 2; + stack_top += 1; + goto next; + } + // Top of the stack has all of its dependencies resolved, we can emit + // its fragment. + *ctx->tail = stack_entry->entry_point; + ctx->tail = stack_entry->exit; + ctx->compiled[stack_entry->index] = 1; + stack[--stack_top] = NULL; + free(stack_entry); + next:; + } +out: + for (unsigned i = 0; i < stack_top; i++) { + free(stack[i]); + } + return stack_top == 0; +} + +static void prune_fragments(struct list_node *head) { + bool changed = true; + while (changed) { + changed = false; + list_foreach(struct fragment, i, head, siblings) { + if (i->once_next == i->next && i->next != NULL) { + i->once_next = NULL; + changed = true; + } + } + list_foreach(struct fragment, i, head, siblings) { + struct fragment *non_empty = i->next; + while (non_empty && non_empty->ninstrs == 0 && + non_empty->once_next == NULL) { + non_empty = non_empty->next; + } + changed |= (non_empty != i->next); + i->next = non_empty; + non_empty = i->once_next; + while (non_empty && non_empty->ninstrs == 0 && + non_empty->once_next == NULL) { + non_empty = non_empty->next; + } + changed |= (non_empty != i->once_next); + i->once_next = non_empty; + } + } +} + +static struct script *script_codegen(struct list_node *all_fragments, struct fragment *head) { + unsigned nfragments = 0; + list_foreach(struct fragment, i, all_fragments, siblings) { + nfragments += 1; + } + auto queue = ccalloc(nfragments, struct fragment *); + unsigned pos = 0, h = 0, t = 1; + queue[0] = head; + head->emitted = true; + // First, layout the fragments in the output + while (h != t) { + auto curr = queue[h]; + while (curr) { + curr->addr = pos; + curr->emitted = true; + pos += curr->ninstrs; + if (curr->once_next) { + pos += 1; // For branch_once + if (!curr->once_next->emitted) { + queue[t++] = curr->once_next; + curr->once_next->emitted = true; + } + } + if ((curr->next && curr->next->emitted) || !curr->next) { + pos += 1; // For branch or halt + break; + } + curr = curr->next; + } + h += 1; + } + struct script *script = + calloc(1, sizeof(struct script) + sizeof(struct instruction[pos])); + script->len = pos; + free(queue); + + list_foreach(const struct fragment, i, all_fragments, siblings) { + if (i->ninstrs) { + memcpy(&script->instrs[i->addr], i->instrs, + sizeof(struct instruction[i->ninstrs])); + } + + auto ninstrs = i->ninstrs; + if (i->once_next) { + script->instrs[i->addr + ninstrs].type = INST_BRANCH_ONCE; + script->instrs[i->addr + ninstrs].rel = + (int)i->once_next->addr - (int)(i->addr + ninstrs); + ninstrs += 1; + } + if (i->next && i->next->addr != i->addr + ninstrs) { + script->instrs[i->addr + ninstrs].type = INST_BRANCH; + script->instrs[i->addr + ninstrs].rel = + (int)i->next->addr - (int)(i->addr + ninstrs); + } else if (!i->next) { + script->instrs[i->addr + ninstrs].type = INST_HALT; + } + } + return script; +} + +static void +script_compile_context_init(struct script_compile_context *ctx, config_setting_t *setting) { + list_init_head(&ctx->all_fragments); + const uint32_t n = to_u32_checked(config_setting_length(setting)); + ctx->compiled = ccalloc(n, int); + for (uint32_t i = 0; i < n; i++) { + auto var = config_setting_get_elem(setting, i); + const char *var_name = config_setting_name(var); + auto alloc = ccalloc(1, struct variable_allocation); + alloc->name = strdup(var_name); + alloc->index = i; + alloc->slot = i; + HASH_ADD_STR(ctx->vars, name, alloc); + } + + ctx->allocated_slots = n; + + auto head = fragment_new(ctx, 0); + ctx->head = head; + ctx->once_tail = &head->once_next; + ctx->tail = &head->next; + + ctx->once_end_head = NULL; + ctx->once_end_tail = &ctx->once_end_head; +} + +struct script * +script_compile(config_setting_t *setting, struct script_parse_config cfg, char **out_err) { + if (!config_setting_is_group(setting)) { + return NULL; + } + struct script_context_info_internal *context_table = NULL; + if (cfg.context_info) { + for (unsigned i = 0; cfg.context_info[i].name; i++) { + struct script_context_info_internal *new_ctx = + ccalloc(1, struct script_context_info_internal); + new_ctx->info = cfg.context_info[i]; + HASH_ADD_STR(context_table, info.name, new_ctx); + } + } + + struct script_compile_context ctx = {}; + script_compile_context_init(&ctx, setting); + ctx.context_info = context_table; + const uint32_t n = to_u32_checked(config_setting_length(setting)); + auto stack = ccalloc(n, struct compilation_stack *); + for (uint32_t i = 0; i < n; i++) { + if (ctx.compiled[i]) { + continue; + } + if (!script_compile_one_recursive(stack, setting, i, &ctx, out_err)) { + break; + } + } + + { + struct script_context_info_internal *info, *next_info; + HASH_ITER(hh, context_table, info, next_info) { + HASH_DEL(context_table, info); + free(info); + } + } + for (int i = 0; cfg.output_info && cfg.output_info[i].name; i++) { + struct variable_allocation *alloc = NULL; + HASH_FIND_STR(ctx.vars, cfg.output_info[i].name, alloc); + if (alloc) { + cfg.output_info[i].slot = to_int_checked(alloc->slot); + } else { + cfg.output_info[i].slot = -1; + } + } + + bool succeeded = true; + for (unsigned i = 0; i < n; i++) { + if (ctx.compiled[i] != 1) { + succeeded = false; + break; + } + } + free(stack); + free(ctx.compiled); + if (!succeeded) { + free_hash_table(ctx.vars); + free_hash_table(ctx.overrides); + list_foreach_safe(struct fragment, i, &ctx.all_fragments, siblings) { + fragment_free(i); + } + return NULL; + } + + // Connect everything together + if (ctx.once_end_head) { + auto once_end = fragment_new(&ctx, 0); + *ctx.tail = once_end; + once_end->once_next = ctx.once_end_head; + } + *ctx.once_tail = ctx.head->next; + + prune_fragments(&ctx.all_fragments); + + auto script = script_codegen(&ctx.all_fragments, ctx.head); + script->vars = ctx.vars; + script->overrides = ctx.overrides; + script->max_duration = ctx.max_duration; + script->nslots = ctx.allocated_slots; + script->stack_size = ctx.max_stack; + log_debug("Compiled script at line %d, total instructions: %d, max duration: %f, " + "slots: %d, stack size: %d\n", + config_setting_source_line(setting), script->len, script->max_duration, + script->nslots, script->stack_size); + if (log_get_level_tls() <= LOG_LEVEL_DEBUG) { + log_debug("Output mapping:"); + HASH_ITER2(ctx.vars, var) { + log_debug(" %s -> %d", var->name, var->slot); + } + } + if (log_get_level_tls() <= LOG_LEVEL_TRACE) { + for (unsigned i = 0; i < script->len; i++) { + log_instruction(TRACE, i, script->instrs[i]); + } + } + list_foreach_safe(struct fragment, i, &ctx.all_fragments, siblings) { + free(i); + } + return script; +} + +struct script_instance *script_instance_new(const struct script *script) { + // allocate no space for the variable length array is UB. + unsigned memory_size = max2(1, script->nslots + script->stack_size); + struct script_instance *instance = + calloc(1, sizeof(struct script_instance) + sizeof(double[memory_size])); + allocchk(instance); + instance->script = script; + instance->elapsed = 0; + for (unsigned i = 0; i < script->nslots; i++) { + instance->memory[i] = NAN; + } + return instance; +} + +void script_instance_resume_from(struct script_instance *old, struct script_instance *new_) { + // todo: proper steer logic + struct overridable_slot *i, *next; + HASH_ITER(hh, new_->script->overrides, i, next) { + struct variable_allocation *src_alloc = NULL; + HASH_FIND_STR(old->script->vars, i->name, src_alloc); + if (!src_alloc) { + continue; + } + new_->memory[i->slot] = old->memory[src_alloc->slot]; + } +} + +bool script_instance_is_finished(const struct script_instance *instance) { + return instance->elapsed >= instance->script->max_duration; +} + +enum script_evaluation_result +script_instance_evaluate(struct script_instance *instance, void *context) { + auto script = instance->script; + auto stack = (double *)&instance->memory[script->nslots]; + unsigned top = 0; + double l, r; + bool do_branch_once = instance->elapsed == 0; + for (auto i = script->instrs;; i++) { + switch (i->type) { + case INST_IMM: stack[top++] = i->imm; break; + case INST_LOAD: stack[top++] = instance->memory[i->slot]; break; + case INST_LOAD_CTX: stack[top++] = *(double *)(context + i->ctx); break; + case INST_STORE: + BUG_ON(top < 1); + instance->memory[i->slot] = stack[--top]; + break; + case INST_STORE_OVER_NAN: + BUG_ON(top < 1); + top -= 1; + if (safe_isnan(instance->memory[i->slot])) { + instance->memory[i->slot] = stack[top]; + } + break; + case INST_BRANCH: i += i->rel - 1; break; + case INST_BRANCH_ONCE: + if (do_branch_once) { + i += i->rel - 1; + } + break; + case INST_HALT: return SCRIPT_EVAL_OK; + case INST_OP: + if (i->op == OP_NEG) { + BUG_ON(top < 1); + l = stack[top - 1]; + stack[top - 1] = -l; + } else { + BUG_ON(top < 2); + l = stack[top - 2]; + r = stack[top - 1]; + stack[top - 2] = op_eval(l, i->op, r); + top -= 1; + } + break; + case INST_CURVE: + l = (instance->elapsed - i->delay) / i->duration; + l = min2(max2(0, l), 1); + stack[top++] = i->curve->sample(i->curve, l); + break; + } + if (top && safe_isnan(stack[top - 1])) { + return SCRIPT_EVAL_ERROR_NAN; + } + if (top && safe_isinf(stack[top - 1])) { + return SCRIPT_EVAL_ERROR_INF; + } + } + unreachable(); +} + +#ifdef UNIT_TEST +static inline void +script_compile_str(struct test_case_metadata *metadata, const char *str, + struct script_output_info *outputs, char **err, struct script **out) { + config_t cfg; + config_init(&cfg); + config_set_auto_convert(&cfg, 1); + int ret = config_read_string(&cfg, str); + TEST_EQUAL(ret, CONFIG_TRUE); + + config_setting_t *setting = config_root_setting(&cfg); + TEST_NOTEQUAL(setting, NULL); + *out = script_compile(setting, + (struct script_parse_config){.output_info = outputs}, err); + config_destroy(&cfg); +} + +TEST_CASE(scripts_1) { + static const char *str = "\ + a = 10; \ + b = \"a * 2\";\ + c = \"(b - 1) * (a+1)\";\ + d = \"- e - 1\"; \ + e : { \ + timing = \"10s cubic-bezier(0.5,0.5, 0.5, 0.5) 0.5s\"; \ + start = 10; \ + end = \"2 * c\"; \ + }; \ + f : { \ + timing = \"10s cubic-bezier(0.1,0.2, 0.3, 0.4) 0.5s\"; \ + start = \"e + 1\"; \ + end = \"f - 1\"; \ + }; \ + neg = \"-a\"; \ + timing1 : { \ + timing = \"10s\"; \ + start = 1; \ + end = 0; \ + };\ + timing2 : { \ + timing = \"10s steps(1, jump-start)\"; \ + start = 1; \ + end = 0; \ + };"; + struct script_output_info outputs[] = {{"a"}, {"b"}, {"c"}, {"d"}, {"e"}, {NULL}}; + char *err = NULL; + struct script *script = NULL; + script_compile_str(metadata, str, outputs, &err, &script); + if (err) { + log_error("err: %s\n", err); + free(err); + } + TEST_NOTEQUAL(script, NULL); + TEST_EQUAL(err, NULL); + if (script) { + struct variable_allocation *c; + HASH_FIND_STR(script->vars, "c", c); + TEST_NOTEQUAL(c, NULL); + + struct script_instance *instance = script_instance_new(script); + auto result = script_instance_evaluate(instance, NULL); + TEST_EQUAL(result, SCRIPT_EVAL_OK); + TEST_EQUAL(instance->memory[outputs[0].slot], 10); + TEST_EQUAL(instance->memory[outputs[1].slot], 20); + TEST_EQUAL(instance->memory[outputs[2].slot], 209); + TEST_EQUAL(instance->memory[outputs[3].slot], -11); + TEST_EQUAL(instance->memory[outputs[4].slot], 10); + TEST_TRUE(!script_instance_is_finished(instance)); + + instance->elapsed += 5.5; + result = script_instance_evaluate(instance, NULL); + TEST_EQUAL(result, SCRIPT_EVAL_OK); + TEST_EQUAL(instance->memory[outputs[4].slot], 214); + + instance->elapsed += 5.5; + result = script_instance_evaluate(instance, NULL); + TEST_EQUAL(result, SCRIPT_EVAL_OK); + TEST_EQUAL(instance->memory[outputs[0].slot], 10); + TEST_EQUAL(instance->memory[outputs[1].slot], 20); + TEST_EQUAL(instance->memory[outputs[2].slot], 209); + TEST_EQUAL(instance->memory[outputs[3].slot], -419); + TEST_EQUAL(instance->memory[outputs[4].slot], 418); + TEST_TRUE(script_instance_is_finished(instance)); + free(instance); + script_free(script); + } +} +TEST_CASE(scripts_report_cycle) { + static const char *str = "\ + a = \"c\"; \ + b = \"a * 2\";\ + c = \"b + 1\";"; + char *err = NULL; + struct script *script = NULL; + script_compile_str(metadata, str, NULL, &err, &script); + TEST_EQUAL(script, NULL); + TEST_NOTEQUAL(err, NULL); + TEST_STREQUAL(err, "Cyclic references detected in animation script defined at " + "line 0: a -> c -> b -> a"); + free(err); +} +TEST_CASE(script_errors) { + static const char *cases[][2] = { + {"a = \"1 @ 2 \";", "Failed to parse expression at line 1. Expected one of " + "\"+-*/^\", got '@'."}, + {"a = { timing = \"1 asdf\";};", "Invalid curve definition \"1 asdf\" " + "(invalid time unit at \" asdf\"). Line 1."}, + {"a = { timing = \"1s asdf\";};", "Unknown curve type \"asdf\". Line 1."}, + {"a = { timing = \"1s steps(a)\";};", "Invalid step count at \"a)\". Line " + "1."}, + {"a = { timing = \"1s steps(1)\";};", "Invalid steps argument list \"(1)\". " + "Line 1."}, + {"a = \"1 + +\";", "Failed to parse expression at line 1. Expected a number " + "or a variable name, got \"+\"."}, + {"a = \"1)\";", "Failed to parse expression at line 1. Unmatched ')' in " + "expression \"1)\""}, + {"a = {};", "Transition section does not contain a timing function. Line 1."}, + {"a = { timing = \"0s\"; start = 0; end = 0; };", "Timing function cannot " + "have a zero duration. " + "Line 1."}, + }; + char *err = NULL; + struct script *script = NULL; + for (size_t i = 0; i < ARR_SIZE(cases); i++) { + script_compile_str(metadata, cases[i][0], NULL, &err, &script); + TEST_EQUAL(script, NULL); + TEST_NOTEQUAL(err, NULL); + TEST_STREQUAL(err, cases[i][1]); + free(err); + err = NULL; + } +} +#endif diff --git a/src/script.h b/src/script.h new file mode 100644 index 0000000000..8d9e9d4c09 --- /dev/null +++ b/src/script.h @@ -0,0 +1,60 @@ +#pragma once +#include +#include +#include +#include +#include + +struct script_context_info { + const char *name; + ptrdiff_t offset; +}; + +struct script_output_info { + const char *name; + /// Slot for this variable, -1 if this variable doesn't exist. + int slot; +}; + +struct script_parse_config { + const struct script_context_info *context_info; + /// Set the output variables of this script, also used to receive the slot number + /// for those variables. + struct script_output_info *output_info; +}; +struct script; +struct script_instance { + const struct script *script; + double elapsed; + double memory[]; +}; +enum script_evaluation_result { + /// +/-inf in results + SCRIPT_EVAL_ERROR_INF, + /// NaN in results + SCRIPT_EVAL_ERROR_NAN, + /// OK + SCRIPT_EVAL_OK, +}; +typedef struct config_setting_t config_setting_t; +static_assert(alignof(double) > alignof(unsigned), "double/unsigned has unexpected " + "alignment"); + +struct script * +script_compile(config_setting_t *setting, struct script_parse_config cfg, char **out_err); +void script_free(struct script *script); +enum script_evaluation_result +script_instance_evaluate(struct script_instance *instance, void *context); +bool script_instance_is_finished(const struct script_instance *instance); +/// Resume the script instance from another script instance that's currently running. +/// The script doesn't have to be the same. For resumable (explained later) transitions, +/// if matching variables exist in the `old` script, their starting point will be +/// overridden with the current value of matching variables from `old`. A resumable +/// transition is a transition that will "resume" from wherever its current value is. +/// Imagine a window flying off the screen, for some reason you decided to bring it back +/// when it's just halfway cross. It would be jarring if the window jumps, so you would +/// want it to fly back in from where it currently is, instead from out of the screen. +/// This resuming behavior can be turned off by setting `reset = true;` in the transition +/// configuration, in which case the user defined `start` value will always be used. +void script_instance_resume_from(struct script_instance *old, struct script_instance *new_); +struct script_instance *script_instance_new(const struct script *script); diff --git a/src/utils.h b/src/utils.h index 60ad46e9b4..ef536f2703 100644 --- a/src/utils.h +++ b/src/utils.h @@ -37,6 +37,16 @@ safe_isnan(double a) { return __builtin_isnan(a); } +#ifdef __clang__ +__attribute__((optnone)) +#else +__attribute__((optimize("-fno-fast-math"))) +#endif +static inline bool +safe_isinf(double a) { + return __builtin_isinf(a); +} + /// Same as assert(false), but make sure we abort _even in release builds_. /// Silence compiler warning caused by release builds making some code paths reachable. #define BUG() \ From e12e102c187b5b3f7ef11de4eab5a9aa4e9de03b Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Sat, 20 Apr 2024 13:51:59 +0100 Subject: [PATCH 04/23] config_libconfig: parse animation scripts Signed-off-by: Yuxuan Shui --- src/config.h | 37 +++++++++++ src/config_libconfig.c | 142 +++++++++++++++++++++++++++++++++++++++++ src/options.c | 7 ++ src/win.h | 27 ++++++++ src/win_defs.h | 11 ++++ 5 files changed, 224 insertions(+) diff --git a/src/config.h b/src/config.h index 68aef3b3cd..16d7ce3eda 100644 --- a/src/config.h +++ b/src/config.h @@ -81,6 +81,38 @@ enum vblank_scheduler_type { LAST_VBLANK_SCHEDULER, }; +enum animation_trigger { + ANIMATION_TRIGGER_INVALID = -1, + /// When a hidden window is shown + ANIMATION_TRIGGER_SHOW = 0, + /// When a window is hidden + ANIMATION_TRIGGER_HIDE, + /// When window opacity is increased + ANIMATION_TRIGGER_INCREASE_OPACITY, + /// When window opacity is decreased + ANIMATION_TRIGGER_DECREASE_OPACITY, + /// When a new window opens + ANIMATION_TRIGGER_OPEN, + /// When a window is closed + ANIMATION_TRIGGER_CLOSE, + ANIMATION_TRIGGER_LAST = ANIMATION_TRIGGER_CLOSE, +}; + +static const char *animation_trigger_names[] attr_unused = { + [ANIMATION_TRIGGER_SHOW] = "show", + [ANIMATION_TRIGGER_HIDE] = "hide", + [ANIMATION_TRIGGER_INCREASE_OPACITY] = "increase-opacity", + [ANIMATION_TRIGGER_DECREASE_OPACITY] = "decrease-opacity", + [ANIMATION_TRIGGER_OPEN] = "open", + [ANIMATION_TRIGGER_CLOSE] = "close", +}; + +struct script; +struct win_script { + int output_indices[NUM_OF_WIN_SCRIPT_OUTPUTS]; + struct script *script; +}; + extern const char *vblank_scheduler_str[]; /// Internal, private options for debugging and development use. @@ -292,6 +324,11 @@ typedef struct options { c2_lptr_t *transparent_clipping_blacklist; bool dithered_present; + // === Animation === + struct win_script animations[ANIMATION_TRIGGER_LAST + 1]; + /// Array of all the scripts used in `animations`. + struct script **all_scripts; + int number_of_scripts; } options_t; extern const char *const BACKEND_STRS[NUM_BKEND + 1]; diff --git a/src/config_libconfig.c b/src/config_libconfig.c index df9c4961a6..008b021531 100644 --- a/src/config_libconfig.c +++ b/src/config_libconfig.c @@ -15,8 +15,10 @@ #include "config.h" #include "err.h" #include "log.h" +#include "script.h" #include "string_utils.h" #include "utils.h" +#include "win.h" #pragma GCC diagnostic error "-Wunused-parameter" @@ -220,6 +222,140 @@ static inline void parse_wintype_config(const config_t *cfg, const char *member_ } } +static enum animation_trigger parse_animation_trigger(const char *trigger) { + for (int i = 0; i <= ANIMATION_TRIGGER_LAST; i++) { + if (strcasecmp(trigger, animation_trigger_names[i]) == 0) { + return i; + } + } + return ANIMATION_TRIGGER_INVALID; +} + +static struct script * +compile_win_script(config_setting_t *setting, int *output_indices, char **err) { + struct script_output_info outputs[ARR_SIZE(win_script_outputs)]; + memcpy(outputs, win_script_outputs, sizeof(win_script_outputs)); + + struct script_parse_config parse_config = { + .context_info = win_script_context_info, + .output_info = outputs, + }; + auto script = script_compile(setting, parse_config, err); + if (script == NULL) { + return script; + } + for (int i = 0; i < NUM_OF_WIN_SCRIPT_OUTPUTS; i++) { + output_indices[i] = outputs[i].slot; + } + return script; +} + +static bool set_animation(struct win_script *animations, + const enum animation_trigger *triggers, int number_of_triggers, + struct script *script, const int *output_indices, unsigned line) { + bool needed = false; + for (int i = 0; i < number_of_triggers; i++) { + if (triggers[i] == ANIMATION_TRIGGER_INVALID) { + log_error("Invalid trigger defined at line %d", line); + continue; + } + if (animations[triggers[i]].script != NULL) { + log_error("Duplicate animation defined for trigger %s at line " + "%d, it will be ignored.", + animation_trigger_names[triggers[i]], line); + continue; + } + memcpy(animations[triggers[i]].output_indices, output_indices, + sizeof(int[NUM_OF_WIN_SCRIPT_OUTPUTS])); + animations[triggers[i]].script = script; + needed = true; + } + return needed; +} + +static struct script * +parse_animation_one(struct win_script *animations, config_setting_t *setting) { + auto triggers = config_setting_lookup(setting, "triggers"); + if (!triggers) { + log_error("Missing triggers in animation script, at line %d", + config_setting_source_line(setting)); + return NULL; + } + if (!config_setting_is_list(triggers) && !config_setting_is_array(triggers) && + config_setting_get_string(triggers) == NULL) { + log_error("The \"triggers\" option must either be a string, a list, or " + "an array, but is none of those at line %d", + config_setting_source_line(triggers)); + return NULL; + } + auto number_of_triggers = + config_setting_get_string(triggers) == NULL ? config_setting_length(triggers) : 1; + if (number_of_triggers > ANIMATION_TRIGGER_LAST) { + log_error("Too many triggers in animation defined at line %d", + config_setting_source_line(triggers)); + return NULL; + } + if (number_of_triggers == 0) { + log_error("Trigger list is empty in animation defined at line %d", + config_setting_source_line(triggers)); + return NULL; + } + enum animation_trigger *trigger_types = + alloca(sizeof(enum animation_trigger[number_of_triggers])); + const char *trigger0 = config_setting_get_string(triggers); + if (trigger0 == NULL) { + for (int i = 0; i < number_of_triggers; i++) { + auto trigger_i = config_setting_get_string_elem(triggers, i); + trigger_types[i] = trigger_i == NULL + ? ANIMATION_TRIGGER_INVALID + : parse_animation_trigger(trigger_i); + } + } else { + trigger_types[0] = parse_animation_trigger(trigger0); + } + + // script parser shouldn't see this. + config_setting_remove(setting, "triggers"); + + int output_indices[NUM_OF_WIN_SCRIPT_OUTPUTS]; + char *err; + auto script = compile_win_script(setting, output_indices, &err); + if (!script) { + log_error("Failed to parse animation script at line %d: %s", + config_setting_source_line(setting), err); + free(err); + return NULL; + } + + bool needed = set_animation(animations, trigger_types, number_of_triggers, script, + output_indices, config_setting_source_line(setting)); + if (!needed) { + script_free(script); + script = NULL; + } + return script; +} + +static struct script **parse_animations(struct win_script *animations, + config_setting_t *setting, int *number_of_scripts) { + auto number_of_animations = config_setting_length(setting); + auto all_scripts = ccalloc(number_of_animations + 1, struct script *); + auto len = 0; + for (int i = 0; i < number_of_animations; i++) { + auto sub = config_setting_get_elem(setting, (unsigned)i); + auto script = parse_animation_one(animations, sub); + if (script != NULL) { + all_scripts[len++] = script; + } + } + if (len == 0) { + free(all_scripts); + all_scripts = NULL; + } + *number_of_scripts = len; + return all_scripts; +} + /** * Parse a configuration file from default location. * @@ -607,6 +743,12 @@ char *parse_config_libconfig(options_t *opt, const char *config_file) { parse_wintype_config(&cfg, "notify", &opt->wintype_option[WINTYPE_NOTIFICATION], &opt->wintype_option_mask[WINTYPE_NOTIFICATION]); + config_setting_t *animations = config_lookup(&cfg, "animations"); + if (animations) { + opt->all_scripts = + parse_animations(opt->animations, animations, &opt->number_of_scripts); + } + config_destroy(&cfg); return path; diff --git a/src/options.c b/src/options.c index 7a502f0f78..d6fcf68647 100644 --- a/src/options.c +++ b/src/options.c @@ -930,6 +930,13 @@ void options_destroy(struct options *options) { } free(options->blur_kerns); free(options->glx_fshader_win_str); + + for (int i = 0; i < options->number_of_scripts; i++) { + script_free(options->all_scripts[i]); + options->all_scripts[i] = NULL; + } + free(options->all_scripts); + memset(options->animations, 0, sizeof(options->animations)); } // vim: set noet sw=8 ts=8 : diff --git a/src/win.h b/src/win.h index 58c094d675..66da22c4cc 100644 --- a/src/win.h +++ b/src/win.h @@ -16,6 +16,7 @@ #include "list.h" #include "region.h" #include "render.h" +#include "script.h" #include "transition.h" #include "types.h" #include "utils.h" @@ -305,6 +306,32 @@ struct managed_win { region_t damaged; }; +struct win_script_context { + double x, y, width, height; + double opacity_before, opacity; +}; + +static const struct script_context_info win_script_context_info[] = { + {"window-x", offsetof(struct win_script_context, x)}, + {"window-y", offsetof(struct win_script_context, y)}, + {"window-width", offsetof(struct win_script_context, width)}, + {"window-height", offsetof(struct win_script_context, height)}, + {"window-raw-opacity-before", offsetof(struct win_script_context, opacity_before)}, + {"window-raw-opacity", offsetof(struct win_script_context, opacity)}, + {NULL, 0} // +}; + +static const struct script_output_info win_script_outputs[] = { + [WIN_SCRIPT_OFFSET_X] = {"offset-x"}, + [WIN_SCRIPT_OFFSET_Y] = {"offset-y"}, + [WIN_SCRIPT_SHADOW_OFFSET_X] = {"shadow-offset-x"}, + [WIN_SCRIPT_SHADOW_OFFSET_Y] = {"shadow-offset-y"}, + [WIN_SCRIPT_OPACITY] = {"opacity"}, + [WIN_SCRIPT_BLUR_OPACITY] = {"blur-opacity"}, + [WIN_SCRIPT_SHADOW_OPACITY] = {"shadow-opacity"}, + [NUM_OF_WIN_SCRIPT_OUTPUTS] = {NULL}, +}; + /// Process pending updates/images flags on a window. Has to be called in X critical /// section void win_process_update_flags(session_t *ps, struct managed_win *w); diff --git a/src/win_defs.h b/src/win_defs.h index a9d3b5fd4d..aca62a8677 100644 --- a/src/win_defs.h +++ b/src/win_defs.h @@ -65,3 +65,14 @@ enum win_flags { /// need better name for this, is set when some aspects of the window changed WIN_FLAGS_FACTOR_CHANGED = 1024, }; + +enum win_script_output { + WIN_SCRIPT_OFFSET_X = 0, + WIN_SCRIPT_OFFSET_Y, + WIN_SCRIPT_SHADOW_OFFSET_X, + WIN_SCRIPT_SHADOW_OFFSET_Y, + WIN_SCRIPT_OPACITY, + WIN_SCRIPT_BLUR_OPACITY, + WIN_SCRIPT_SHADOW_OPACITY, +}; +#define NUM_OF_WIN_SCRIPT_OUTPUTS (WIN_SCRIPT_SHADOW_OPACITY + 1) From 2f9e0921acc81e4520f248d3d2079473fc39ac3e Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Sat, 20 Apr 2024 16:19:21 +0100 Subject: [PATCH 05/23] config_libconfig: generate animation for legacy fading options Signed-off-by: Yuxuan Shui --- src/config.h | 3 + src/config_libconfig.c | 121 +++++++++++++++++++++++++++++++++++++++++ src/options.c | 1 + 3 files changed, 125 insertions(+) diff --git a/src/config.h b/src/config.h index 16d7ce3eda..31dc0e4b4a 100644 --- a/src/config.h +++ b/src/config.h @@ -403,4 +403,7 @@ static inline bool parse_vsync(const char *str) { return true; } +/// Generate animation script for legacy fading options +void generate_fading_config(struct options *opt); + // vim: set noet sw=8 ts=8 : diff --git a/src/config_libconfig.c b/src/config_libconfig.c index 008b021531..1250c6c772 100644 --- a/src/config_libconfig.c +++ b/src/config_libconfig.c @@ -356,6 +356,127 @@ static struct script **parse_animations(struct win_script *animations, return all_scripts; } +#define FADING_TEMPLATE_1 \ + "opacity = { " \ + " timing = \"%fms linear\"; " \ + " start = \"window-raw-opacity-before\"; " \ + " end = \"window-raw-opacity\"; " \ + "};" \ + "shadow-opacity = \"opacity\";" +#define FADING_TEMPLATE_2 \ + "blur-opacity = { " \ + " timing = \"%fms linear\"; " \ + " start = %f; end = %f; " \ + "};" + +static struct script *compile_win_script_from_string(const char *input, int *output_indices) { + config_t tmp_config; + config_setting_t *setting; + config_init(&tmp_config); + config_read_string(&tmp_config, input); + setting = config_root_setting(&tmp_config); + + // Since we are compiling scripts we generated, it can't fail. + char *err = NULL; + auto script = compile_win_script(setting, output_indices, &err); + config_destroy(&tmp_config); + BUG_ON(err != NULL); + + return script; +} + +void generate_fading_config(struct options *opt) { + // We create stand-in animations for fade-in/fade-out if they haven't be + // overwritten + char *str = NULL; + size_t len = 0; + enum animation_trigger trigger[2]; + struct script *scripts[4]; + int number_of_scripts = 0; + int number_of_triggers = 0; + + int output_indices[NUM_OF_WIN_SCRIPT_OUTPUTS]; + double duration = 1.0 / opt->fade_in_step * opt->fade_delta; + // Fading in from nothing, i.e. `open` and `show` + asnprintf(&str, &len, FADING_TEMPLATE_1 FADING_TEMPLATE_2, duration, duration, + 0.F, 1.F); + + auto fade_in1 = compile_win_script_from_string(str, output_indices); + if (opt->animations[ANIMATION_TRIGGER_OPEN].script == NULL && !opt->no_fading_openclose) { + trigger[number_of_triggers++] = ANIMATION_TRIGGER_OPEN; + } + if (opt->animations[ANIMATION_TRIGGER_SHOW].script == NULL) { + trigger[number_of_triggers++] = ANIMATION_TRIGGER_SHOW; + } + if (set_animation(opt->animations, trigger, number_of_triggers, fade_in1, + output_indices, 0)) { + scripts[number_of_scripts++] = fade_in1; + } else { + script_free(fade_in1); + } + + // Fading for opacity change, for these, the blur opacity doesn't change. + asnprintf(&str, &len, FADING_TEMPLATE_1, duration); + auto fade_in2 = compile_win_script_from_string(str, output_indices); + number_of_triggers = 0; + if (opt->animations[ANIMATION_TRIGGER_INCREASE_OPACITY].script == NULL) { + trigger[number_of_triggers++] = ANIMATION_TRIGGER_INCREASE_OPACITY; + } + if (set_animation(opt->animations, trigger, number_of_triggers, fade_in2, + output_indices, 0)) { + scripts[number_of_scripts++] = fade_in2; + } else { + script_free(fade_in2); + } + + duration = 1.0 / opt->fade_out_step * opt->fade_delta; + // Fading out to nothing, i.e. `hide` and `close` + asnprintf(&str, &len, FADING_TEMPLATE_1 FADING_TEMPLATE_2, duration, duration, + 1.F, 0.F); + auto fade_out1 = compile_win_script_from_string(str, output_indices); + number_of_triggers = 0; + if (opt->animations[ANIMATION_TRIGGER_CLOSE].script == NULL && + !opt->no_fading_openclose) { + trigger[number_of_triggers++] = ANIMATION_TRIGGER_CLOSE; + } + if (opt->animations[ANIMATION_TRIGGER_HIDE].script == NULL) { + trigger[number_of_triggers++] = ANIMATION_TRIGGER_HIDE; + } + if (set_animation(opt->animations, trigger, number_of_triggers, fade_out1, + output_indices, 0)) { + scripts[number_of_scripts++] = fade_out1; + } else { + script_free(fade_out1); + } + + // Fading for opacity change + asnprintf(&str, &len, FADING_TEMPLATE_1, duration); + auto fade_out2 = compile_win_script_from_string(str, output_indices); + number_of_triggers = 0; + if (opt->animations[ANIMATION_TRIGGER_DECREASE_OPACITY].script == NULL) { + trigger[number_of_triggers++] = ANIMATION_TRIGGER_DECREASE_OPACITY; + } + if (set_animation(opt->animations, trigger, number_of_triggers, fade_out2, + output_indices, 0)) { + scripts[number_of_scripts++] = fade_out2; + } else { + script_free(fade_out2); + } + free(str); + + log_debug("Generated %d scripts for fading.", number_of_scripts); + if (number_of_scripts) { + auto ptr = realloc( + opt->all_scripts, + sizeof(struct scripts * [number_of_scripts + opt->number_of_scripts])); + allocchk(ptr); + opt->all_scripts = ptr; + memcpy(&opt->all_scripts[opt->number_of_scripts], scripts, + sizeof(struct script *[number_of_scripts])); + opt->number_of_scripts += number_of_scripts; + } +} + /** * Parse a configuration file from default location. * diff --git a/src/options.c b/src/options.c index d6fcf68647..7043a9f330 100644 --- a/src/options.c +++ b/src/options.c @@ -883,6 +883,7 @@ bool get_cfg(options_t *opt, int argc, char *const *argv) { check_end:; } + generate_fading_config(opt); return true; } From 6140ae80ea9e1d9ceededfd87463325d662a42e4 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Wed, 8 May 2024 19:25:38 +0100 Subject: [PATCH 06/23] core: catch up with the X server always We should always process all the X events in libxcb's queue with the server grabbed, not just when we have `pending_updates` flag set. This is the only way to make sure we always render with up-to-date server states. Signed-off-by: Yuxuan Shui --- src/picom.c | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/picom.c b/src/picom.c index a8afdcdcaa..4325b38e76 100644 --- a/src/picom.c +++ b/src/picom.c @@ -1683,20 +1683,19 @@ static void fade_timer_callback(EV_P attr_unused, ev_timer *w, int revents attr_ } static void handle_pending_updates(EV_P_ struct session *ps) { - if (ps->pending_updates) { - log_debug("Delayed handling of events, entering critical section"); - auto e = xcb_request_check(ps->c.c, xcb_grab_server_checked(ps->c.c)); - if (e) { - log_fatal_x_error(e, "failed to grab x server"); - free(e); - return quit(ps); - } - - ps->server_grabbed = true; + auto e = xcb_request_check(ps->c.c, xcb_grab_server_checked(ps->c.c)); + if (e) { + log_fatal_x_error(e, "failed to grab x server"); + free(e); + return quit(ps); + } - // Catching up with X server - handle_queued_x_events(EV_A_ & ps->event_check, 0); + ps->server_grabbed = true; + // Catching up with X server + handle_queued_x_events(EV_A, &ps->event_check, 0); + if (ps->pending_updates) { + log_debug("Delayed handling of events, entering critical section"); // Process new windows, and maybe allocate struct managed_win for them handle_new_windows(ps); @@ -1713,18 +1712,17 @@ static void handle_pending_updates(EV_P_ struct session *ps) { // Process window flags (stale images) refresh_images(ps); - - e = xcb_request_check(ps->c.c, xcb_ungrab_server_checked(ps->c.c)); - if (e) { - log_fatal_x_error(e, "failed to ungrab x server"); - free(e); - return quit(ps); - } - - ps->server_grabbed = false; - ps->pending_updates = false; - log_debug("Exited critical section"); } + e = xcb_request_check(ps->c.c, xcb_ungrab_server_checked(ps->c.c)); + if (e) { + log_fatal_x_error(e, "failed to ungrab x server"); + free(e); + return quit(ps); + } + + ps->server_grabbed = false; + ps->pending_updates = false; + log_trace("Exited critical section"); } static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { From 49670be0d4269aa0d3714b7293808a6a368605c6 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Wed, 15 May 2024 03:32:12 +0100 Subject: [PATCH 07/23] win: switch to use the new animation system Remove the old fading machinary. As a side-effect, animation and time based shaders are now unified. Signed-off-by: Yuxuan Shui --- src/dbus.c | 4 +- src/event.c | 22 +-- src/picom.c | 153 ++++++------------ src/render.c | 10 +- src/renderer/layout.c | 8 +- src/types.h | 10 ++ src/win.c | 349 +++++++++++++++++++++--------------------- src/win.h | 38 ++--- src/win_defs.h | 10 +- 9 files changed, 283 insertions(+), 321 deletions(-) diff --git a/src/dbus.c b/src/dbus.c index f242622bcf..e60aceb282 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -708,13 +708,13 @@ cdbus_process_win_get(session_t *ps, DBusMessage *msg, DBusMessage *reply, DBusE append(map_state, boolean, w->a.map_state); append(wmwin, boolean, win_is_wmwin(w)); append(focused_raw, boolean, win_is_focused_raw(w)); - append(opacity, double, animatable_get(&w->opacity)); append(left_width, int32, w->frame_extents.left); append(right_width, int32, w->frame_extents.right); append(top_width, int32, w->frame_extents.top); append(bottom_width, int32, w->frame_extents.bottom); append_win_property(mode, enum); + append_win_property(opacity, double); append_win_property(client_win, wid); append_win_property(ever_damaged, boolean); append_win_property(window_type, enum); @@ -727,7 +727,7 @@ cdbus_process_win_get(session_t *ps, DBusMessage *msg, DBusMessage *reply, DBusE append_win_property(class_instance, string); append_win_property(class_general, string); append_win_property(role, string); - append_win_property(opacity.target, double); + append_win_property(opacity, double); append_win_property(has_opacity_prop, boolean); append_win_property(opacity_prop, uint32); append_win_property(opacity_is_set, boolean); diff --git a/src/event.c b/src/event.c index fd671acf49..d7b193bc5b 100644 --- a/src/event.c +++ b/src/event.c @@ -306,12 +306,8 @@ static inline void ev_destroy_notify(session_t *ps, xcb_destroy_notify_event_t * if (w != NULL) { destroy_win_start(ps, w); - if (!w->managed || !win_as_managed(w)->to_paint) { - // If the window wasn't managed, or was already not rendered, - // we don't need to fade it out. - if (w->managed) { - win_skip_fading(win_as_managed(w)); - } + if (!w->managed) { + // If the window wasn't managed, we can release it immediately destroy_win_finish(ps, w); } return; @@ -362,7 +358,7 @@ static inline void ev_map_notify(session_t *ps, xcb_map_notify_event_t *ev) { static inline void ev_unmap_notify(session_t *ps, xcb_unmap_notify_event_t *ev) { auto w = wm_find_managed(ps->wm, ev->window); if (w) { - unmap_win_start(ps, w); + unmap_win_start(w); } } @@ -441,13 +437,15 @@ static inline void ev_reparent_notify(session_t *ps, xcb_reparent_notify_event_t // Emulating what X server does: a destroyed // window is always unmapped first. if (mw->state == WSTATE_MAPPED) { - unmap_win_start(ps, mw); + unmap_win_start(mw); } + + // If an animation is running, the best we could do is stopping + // it. + free(mw->running_animation); + mw->running_animation = NULL; } destroy_win_start(ps, old_w); - if (old_w->managed) { - win_skip_fading(win_as_managed(old_w)); - } destroy_win_finish(ps, old_w); } @@ -677,6 +675,8 @@ static inline void repair_win(session_t *ps, struct managed_win *w) { free(e); } win_extents(w, &parts); + log_debug("Window %#010x (%s) has been damaged the first time", + w->base.id, w->name); } else { auto cookie = xcb_damage_subtract(ps->c.c, w->damage, XCB_NONE, ps->damage_ring.x_region); diff --git a/src/picom.c b/src/picom.c index 4325b38e76..23f063f502 100644 --- a/src/picom.c +++ b/src/picom.c @@ -140,12 +140,10 @@ void quit(session_t *ps) { } /** - * Get current system clock in milliseconds. + * Convert struct timespec to milliseconds. */ -static inline int64_t get_time_ms(void) { - struct timespec tp; - clock_gettime(CLOCK_MONOTONIC, &tp); - return (int64_t)tp.tv_sec * 1000 + (int64_t)tp.tv_nsec / 1000000; +static inline int64_t timespec_ms(struct timespec ts) { + return (int64_t)ts.tv_sec * 1000 + (int64_t)ts.tv_nsec / 1000000; } enum vblank_callback_action check_render_finish(struct vblank_event *e attr_unused, void *ud) { @@ -452,55 +450,6 @@ void add_damage(session_t *ps, const region_t *damage) { pixman_region32_union(cursor, cursor, (region_t *)damage); } -// === Fading === - -/** - * Get the time left before next fading point. - * - * In milliseconds. - */ -static double fade_timeout(session_t *ps) { - auto now = get_time_ms(); - if (ps->o.fade_delta + ps->fade_time < now) { - return 0; - } - - auto diff = ps->o.fade_delta + ps->fade_time - now; - - diff = clamp(diff, 0, ps->o.fade_delta * 2); - - return (double)diff / 1000.0; -} - -/** - * Run fading on a window. - * - * @param steps steps of fading - * @return whether we are still in fading mode - */ -static bool run_fade(struct managed_win **_w, double delta_sec) { - auto w = *_w; - log_trace("Process fading for window %s (%#010x), ΔT: %fs", w->name, w->base.id, - delta_sec); - if (w->number_of_animations == 0) { - // We have reached target opacity. - // We don't call win_check_fade_finished here because that could destroy - // the window, but we still need the damage info from this window - log_trace("|- was fading but finished"); - return false; - } - - log_trace("|- fading, opacity: %lf", animatable_get(&w->opacity)); - animatable_advance(&w->opacity, delta_sec); - animatable_advance(&w->blur_opacity, delta_sec); - log_trace("|- opacity updated: %lf", animatable_get(&w->opacity)); - - // Note even if the animatable is not animating anymore at this point, we still - // want to run preprocess one last time to finish state transition. So return true - // in that case too. - return true; -} - // === Windows === /** @@ -597,13 +546,19 @@ static void rebuild_screen_reg(session_t *ps) { /// Free up all the images and deinit the backend static void destroy_backend(session_t *ps) { win_stack_foreach_managed_safe(w, wm_stack_end(ps->wm)) { - // Wrapping up fading in progress - win_skip_fading(w); + // An unmapped window shouldn't have a pixmap, unless it has animation + // running. (`w->previous.state != w->state` means there might be + // animation but we haven't had a chance to start it because + // `win_process_animation_and_state_change` hasn't been called.) + // TBH, this assertion is probably too complex than what it's worth. + assert(!w->win_image || w->state == WSTATE_MAPPED || + w->running_animation != NULL || w->previous.state != w->state); + // Wrapping up animation in progress + free(w->running_animation); + w->running_animation = NULL; if (ps->backend_data) { - // Unmapped windows could still have shadow images, but not pixmap - // images - assert(!w->win_image || w->state != WSTATE_UNMAPPED); + // Unmapped windows could still have shadow images. // In some cases, the window might have PIXMAP_STALE flag set: // 1. If the window is unmapped. Their stale flags won't be // handled until they are mapped. @@ -867,24 +822,13 @@ static void handle_root_flags(session_t *ps) { * * @return whether the operation succeeded */ -static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, - struct managed_win **out_bottom) { +static bool paint_preprocess(session_t *ps, bool *animation, struct managed_win **out_bottom) { // XXX need better, more general name for `fade_running`. It really // means if fade is still ongoing after the current frame is rendered struct managed_win *bottom = NULL; - *fade_running = false; *animation = false; *out_bottom = NULL; - // Fading step calculation - int64_t delta_ms = 0L; - auto now = get_time_ms(); - if (ps->fade_time) { - assert(now >= ps->fade_time); - delta_ms = now - ps->fade_time; - } - ps->fade_time = now; - // First, let's process fading, and animated shaders // TODO(yshui) check if a window is fully obscured, and if we don't need to // process fading or animation for it. @@ -901,20 +845,18 @@ static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, add_damage_from_win(ps, w); *animation = true; } + if (w->running_animation != NULL) { + *animation = true; + } // Add window to damaged area if its opacity changes // If was_painted == false, and to_paint is also false, we don't care // If was_painted == false, but to_paint is true, damage will be added in // the loop below - if (was_painted && w->number_of_animations != 0) { + if (was_painted && w->running_animation != NULL) { add_damage_from_win(ps, w); } - // Run fading - if (run_fade(&w, (double)delta_ms / 1000.0)) { - *fade_running = true; - } - if (win_has_frame(w)) { w->frame_opacity = ps->o.frame_opacity; } else { @@ -943,8 +885,8 @@ static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, bool to_paint = true; // w->to_paint remembers whether this window is painted last time const bool was_painted = w->to_paint; - const double window_opacity = animatable_get(&w->opacity); - const double blur_opacity = animatable_get(&w->blur_opacity); + const double window_opacity = win_animatable_get(w, WIN_SCRIPT_OPACITY); + const double blur_opacity = win_animatable_get(w, WIN_SCRIPT_BLUR_OPACITY); // Destroy reg_ignore if some window above us invalidated it if (!reg_ignore_valid) { @@ -959,12 +901,7 @@ static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, // pixmap is gone (for example due to a ConfigureNotify), or when it's // excluded if ((w->state == WSTATE_UNMAPPED || w->state == WSTATE_DESTROYED) && - w->number_of_animations == 0) { - if (window_opacity != 0 || blur_opacity != 0) { - log_warn("Window %#010x (%s) is unmapped but still has " - "opacity", - w->base.id, w->name); - } + w->running_animation == NULL) { log_trace("|- is unmapped"); to_paint = false; } else if (unlikely(ps->debug_window != XCB_NONE) && @@ -1080,7 +1017,7 @@ static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, reg_ignore_valid = reg_ignore_valid && w->reg_ignore_valid; w->reg_ignore_valid = true; - if (w->state == WSTATE_DESTROYED && w->number_of_animations == 0) { + if (w->state == WSTATE_DESTROYED && w->running_animation == NULL) { // the window should be destroyed because it was destroyed // by X server and now its animations are finished destroy_win_finish(ps, &w->base); @@ -1682,7 +1619,8 @@ static void fade_timer_callback(EV_P attr_unused, ev_timer *w, int revents attr_ queue_redraw(ps); } -static void handle_pending_updates(EV_P_ struct session *ps) { +static void handle_pending_updates(EV_P_ struct session *ps, double delta_t) { + log_trace("Delayed handling of events, entering critical section"); auto e = xcb_request_check(ps->c.c, xcb_grab_server_checked(ps->c.c)); if (e) { log_fatal_x_error(e, "failed to grab x server"); @@ -1723,6 +1661,12 @@ static void handle_pending_updates(EV_P_ struct session *ps) { ps->server_grabbed = false; ps->pending_updates = false; log_trace("Exited critical section"); + + win_stack_foreach_managed_safe(w, wm_stack_end(ps->wm)) { + // Window might be freed by this function, if it's destroyed and its + // animation finished + win_process_animation_and_state_change(ps, w, delta_t); + } } static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { @@ -1733,13 +1677,22 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { int64_t draw_callback_enter_us; clock_gettime(CLOCK_MONOTONIC, &now); + // Fading step calculation + auto now_ms = timespec_ms(now); + int64_t delta_ms = 0L; + if (ps->fade_time) { + assert(now_ms >= ps->fade_time); + delta_ms = now_ms - ps->fade_time; + } + ps->fade_time = now_ms; + draw_callback_enter_us = (now.tv_sec * 1000000LL + now.tv_nsec / 1000); if (ps->next_render != 0) { log_trace("Schedule delay: %" PRIi64 " us", draw_callback_enter_us - (int64_t)ps->next_render); } - handle_pending_updates(EV_A_ ps); + handle_pending_updates(EV_A_ ps, (double)delta_ms / 1000.0); int64_t after_handle_pending_updates_us; clock_gettime(CLOCK_MONOTONIC, &now); @@ -1756,8 +1709,12 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { // // Using foreach_safe here since skipping fading can cause window to be // freed if it's destroyed. + // + // TODO(yshui) I think maybe we don't need this anymore, since now we + // immediate acquire pixmap right after `map_win_start`. win_stack_foreach_managed_safe(w, wm_stack_end(ps->wm)) { - win_skip_fading(w); + free(w->running_animation); + w->running_animation = NULL; if (w->state == WSTATE_DESTROYED) { destroy_win_finish(ps, &w->base); } @@ -1780,11 +1737,10 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { /* TODO(yshui) Have a stripped down version of paint_preprocess that is used when * screen is not redirected. its sole purpose should be to decide whether the * screen should be redirected. */ - bool fade_running = false; bool animation = false; bool was_redirected = ps->redirected; struct managed_win *bottom = NULL; - if (!paint_preprocess(ps, &fade_running, &animation, &bottom)) { + if (!paint_preprocess(ps, &animation, &bottom)) { log_fatal("Pre-render preparation has failed, exiting..."); exit(1); } @@ -1803,14 +1759,6 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { return draw_callback_impl(EV_A_ ps, revents); } - // Start/stop fade timer depends on whether window are fading - if (!fade_running && ev_is_active(&ps->fade_timer)) { - ev_timer_stop(EV_A_ & ps->fade_timer); - } else if (fade_running && !ev_is_active(&ps->fade_timer)) { - ev_timer_set(&ps->fade_timer, fade_timeout(ps), 0); - ev_timer_start(EV_A_ & ps->fade_timer); - } - int64_t after_preprocess_us; clock_gettime(CLOCK_MONOTONIC, &now); after_preprocess_us = (now.tv_sec * 1000000LL + now.tv_nsec / 1000); @@ -1879,11 +1827,6 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { // false. ps->backend_busy = (ps->frame_pacing && did_render); ps->next_render = 0; - - if (!fade_running) { - ps->fade_time = 0L; - } - ps->render_queued = false; // TODO(yshui) Investigate how big the X critical section needs to be. There are @@ -1893,6 +1836,8 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { // event. if (animation) { queue_redraw(ps); + } else { + ps->fade_time = 0L; } if (ps->vblank_scheduler) { // Even if we might not want to render during next vblank, we want to keep diff --git a/src/render.c b/src/render.c index f434361233..77a85a2e1f 100644 --- a/src/render.c +++ b/src/render.c @@ -437,7 +437,7 @@ void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) const int y = w->g.y; const uint16_t wid = to_u16_checked(w->widthb); const uint16_t hei = to_u16_checked(w->heightb); - const double window_opacity = animatable_get(&w->opacity); + const double window_opacity = win_animatable_get(w, WIN_SCRIPT_OPACITY); xcb_render_picture_t pict = w->paint.pict; @@ -808,8 +808,8 @@ win_paint_shadow(session_t *ps, struct managed_win *w, region_t *reg_paint) { .x = -(w->shadow_dx), .y = -(w->shadow_dy), }; - double shadow_opacity = - animatable_get(&w->opacity) * ps->o.shadow_opacity * ps->o.frame_opacity; + double shadow_opacity = win_animatable_get(w, WIN_SCRIPT_SHADOW_OPACITY) * + ps->o.shadow_opacity * ps->o.frame_opacity; render(ps, 0, 0, w->g.x + w->shadow_dx, w->g.y + w->shadow_dy, w->shadow_width, w->shadow_height, w->widthb, w->heightb, shadow_opacity, true, false, 0, w->shadow_paint.pict, w->shadow_paint.ptex, reg_paint, NULL, @@ -903,7 +903,7 @@ win_blur_background(session_t *ps, struct managed_win *w, xcb_render_picture_t t auto const wid = to_u16_checked(w->widthb); auto const hei = to_u16_checked(w->heightb); const int cr = w ? w->corner_radius : 0; - const double window_opacity = animatable_get(&w->opacity); + const double window_opacity = win_animatable_get(w, WIN_SCRIPT_OPACITY); double factor_center = 1.0; // Adjust blur strength according to window opacity, to make it appear @@ -1143,7 +1143,7 @@ void paint_all(session_t *ps, struct managed_win *t) { } // Only clip shadows above visible windows - if (animatable_get(&w->opacity) * MAX_ALPHA >= 1) { + if (win_animatable_get(w, WIN_SCRIPT_OPACITY) * MAX_ALPHA >= 1) { if (w->clip_shadow_above) { // Add window bounds to shadow-clip region pixman_region32_union(®_shadow_clip, ®_shadow_clip, diff --git a/src/renderer/layout.c b/src/renderer/layout.c index d8a3eece53..37b27feae1 100644 --- a/src/renderer/layout.c +++ b/src/renderer/layout.c @@ -62,10 +62,10 @@ static bool layer_from_window(struct layer *out_layer, struct managed_win *w, iv goto out; } - out_layer->opacity = (float)animatable_get(&w->opacity); - out_layer->blur_opacity = (float)animatable_get(&w->blur_opacity); - out_layer->shadow_opacity = - (float)(out_layer->opacity * w->shadow_opacity * w->frame_opacity); + out_layer->opacity = (float)win_animatable_get(w, WIN_SCRIPT_OPACITY); + out_layer->blur_opacity = (float)win_animatable_get(w, WIN_SCRIPT_BLUR_OPACITY); + out_layer->shadow_opacity = (float)(win_animatable_get(w, WIN_SCRIPT_SHADOW_OPACITY) * + w->shadow_opacity * w->frame_opacity); if (out_layer->opacity == 0 && out_layer->blur_opacity == 0) { goto out; } diff --git a/src/types.h b/src/types.h index ffcf3f2a64..4050f8065e 100644 --- a/src/types.h +++ b/src/types.h @@ -5,6 +5,8 @@ /// Some common types +#include +#include #include #include @@ -78,5 +80,13 @@ static inline ivec2 ivec2_neg(ivec2 a) { }; } +/// Saturating cast from a vec2 to a ivec2 +static inline ivec2 vec2_as(vec2 a) { + return (ivec2){ + .x = (int)fmin(fmax(a.x, INT_MIN), INT_MAX), + .y = (int)fmin(fmax(a.y, INT_MIN), INT_MAX), + }; +} + #define MARGIN_INIT \ { 0, 0, 0, 0 } diff --git a/src/win.c b/src/win.c index 0ec166bf0e..0aa012249e 100644 --- a/src/win.c +++ b/src/win.c @@ -82,7 +82,6 @@ static const double ROUNDED_PERCENT = 0.05; */ static void win_update_opacity_prop(struct x_connection *c, struct atom *atoms, struct managed_win *w, bool detect_client_opacity); -static void win_update_opacity_target(session_t *ps, struct managed_win *w); static void win_update_prop_shadow_raw(struct x_connection *c, struct atom *atoms, struct managed_win *w); static bool @@ -431,7 +430,6 @@ static void win_update_properties(session_t *ps, struct managed_win *w) { if (win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_WM_WINDOW_OPACITY)) { win_update_opacity_prop(&ps->c, ps->atoms, w, ps->o.detect_client_opacity); - win_update_opacity_target(ps, w); } if (win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_FRAME_EXTENTS)) { @@ -485,6 +483,7 @@ static void win_update_properties(session_t *ps, struct managed_win *w) { win_clear_all_properties_stale(w); } +static void map_win_start(struct managed_win *w); /// Handle non-image flags. This phase might set IMAGES_STALE flags void win_process_update_flags(session_t *ps, struct managed_win *w) { log_trace("Processing flags for window %#010x (%s), was rendered: %d, flags: " @@ -492,7 +491,7 @@ void win_process_update_flags(session_t *ps, struct managed_win *w) { w->base.id, w->name, w->to_paint, w->flags); if (win_check_flags_all(w, WIN_FLAGS_MAPPED)) { - map_win_start(ps, w); + map_win_start(w); win_clear_flags(w, WIN_FLAGS_MAPPED); } @@ -822,7 +821,7 @@ winmode_t win_calc_mode_raw(const struct managed_win *w) { } winmode_t win_calc_mode(const struct managed_win *w) { - if (animatable_get(&w->opacity) < 1.0) { + if (win_animatable_get(w, WIN_SCRIPT_OPACITY) < 1.0) { return WMODE_TRANS; } return win_calc_mode_raw(w); @@ -879,7 +878,6 @@ static double win_calc_opacity_target(session_t *ps, const struct managed_win *w /// Doesn't free `w` static void unmap_win_finish(session_t *ps, struct managed_win *w) { w->reg_ignore_valid = false; - w->state = WSTATE_UNMAPPED; // We are in unmap_win, this window definitely was viewable if (ps->backend_data) { @@ -897,8 +895,10 @@ static void unmap_win_finish(session_t *ps, struct managed_win *w) { free_paint(ps, &w->shadow_paint); // Try again at binding images when the window is mapped next time - win_clear_flags(w, WIN_FLAGS_IMAGE_ERROR); - assert(w->number_of_animations == 0); + if (w->state != WSTATE_DESTROYED) { + win_clear_flags(w, WIN_FLAGS_IMAGE_ERROR); + } + assert(w->running_animation == NULL); } struct window_transition_data { @@ -909,90 +909,6 @@ struct window_transition_data { uint64_t refcount; }; -static void win_transition_callback(enum transition_event event, void *data_) { - auto data = (struct window_transition_data *)data_; - auto w = data->w; - w->number_of_animations--; - if (w->number_of_animations == 0 && event != TRANSITION_INTERRUPTED) { - if (w->state == WSTATE_DESTROYED || w->state == WSTATE_UNMAPPED) { - if (animatable_get(&w->opacity) != 0) { - log_warn("Window %#010x (%s) has finished fading out but " - "its opacity is not 0", - w->base.id, w->name); - } - } - if (w->state == WSTATE_UNMAPPED) { - unmap_win_finish(data->ps, data->w); - } - // Destroyed windows are freed in paint_preprocess, this makes managing - // the lifetime of windows easier. - w->in_openclose = false; - } - data->refcount--; - if (data->refcount == 0) { - free(data); - } -} - -/** - * Determine if a window should fade on opacity change. - */ -bool win_should_fade(session_t *ps, const struct managed_win *w) { - // To prevent it from being overwritten by last-paint value if the window - // is - if (w->fade_force != UNSET) { - return w->fade_force; - } - if (ps->o.no_fading_openclose && w->in_openclose) { - return false; - } - if (ps->o.no_fading_destroyed_argb && w->state == WSTATE_DESTROYED && - win_has_alpha(w) && w->client_win && w->client_win != w->base.id) { - // deprecated - return false; - } - if (w->fade_excluded) { - return false; - } - return ps->o.wintype_option[w->window_type].fade; -} - -/// Call `animatable_set_target` on the opacity of a window, with appropriate -/// target opacity and duration. -static inline void -win_start_fade(session_t *ps, struct managed_win *w, double target_blur_opacity) { - double current_opacity = animatable_get(&w->opacity), - target_opacity = win_calc_opacity_target(ps, w); - double step_size = - target_opacity > current_opacity ? ps->o.fade_in_step : ps->o.fade_out_step; - double duration = (fabs(target_opacity - current_opacity) / step_size) * - (double)ps->o.fade_delta / 1000.0; - if (!win_should_fade(ps, w)) { - duration = 0; - } - - auto data = ccalloc(1, struct window_transition_data); - data->ps = ps; - data->w = w; - data->refcount = 0; - if (animatable_set_target(&w->opacity, target_opacity, duration, - curve_new_linear(), win_transition_callback, data)) { - data->refcount++; - w->number_of_animations++; - } - if (animatable_set_target(&w->blur_opacity, target_blur_opacity, duration, - curve_new_linear(), win_transition_callback, data)) { - data->refcount++; - w->number_of_animations++; - } - if (!data->refcount) { - free(data); - } - if (w->number_of_animations == 0 && w->state == WSTATE_UNMAPPED) { - unmap_win_finish(ps, w); - } -} - /** * Determine whether a window is to be dimmed. */ @@ -1373,8 +1289,6 @@ void win_on_factor_change(session_t *ps, struct managed_win *w) { ps->o.transparent_clipping && !c2_match(ps->c2_state, w, ps->o.transparent_clipping_blacklist, NULL); - win_update_opacity_target(ps, w); - w->reg_ignore_valid = false; if (ps->debug_window != XCB_NONE && (w->base.id == ps->debug_window || w->client_win == ps->debug_window)) { @@ -1648,6 +1562,7 @@ struct win *attr_ret_nonnull maybe_allocate_managed_win(session_t *ps, struct wi .opacity_prop = OPAQUE, .opacity_is_set = false, .opacity_set = 1, + .opacity = 0, .frame_extents = MARGIN_INIT, // in win_mark_client .bounding_shaped = false, .bounding_shape = {0}, @@ -1724,8 +1639,6 @@ struct win *attr_ret_nonnull maybe_allocate_managed_win(session_t *ps, struct wi new->base = *w; new->base.managed = true; new->a = *a; - new->opacity = animatable_new(0); - new->blur_opacity = animatable_new(0); new->shadow_opacity = ps->o.shadow_opacity; pixman_region32_init(&new->bounding_shape); @@ -2133,7 +2046,7 @@ bool win_is_region_ignore_valid(session_t *ps, const struct managed_win *w) { /// Finish the destruction of a window (e.g. after fading has finished). /// Frees `w` void destroy_win_finish(session_t *ps, struct win *w) { - log_verbose("Trying to finish destroying (%#010x)", w->id); + log_debug("Trying to finish destroying (%#010x)", w->id); auto next_w = wm_stack_next_managed(ps->wm, &w->stack_neighbour); list_remove(&w->stack_neighbour); @@ -2248,29 +2161,10 @@ void destroy_win_start(session_t *ps, struct win *w) { mw->state = WSTATE_DESTROYED; mw->a.map_state = XCB_MAP_STATE_UNMAPPED; mw->in_openclose = true; - - // We don't initiate animation here, because it should already have been - // started by unmap_win_start, because X automatically unmaps windows - // before destroying them. But we do need to stop animation if - // no_fading_destroyed_windows, or no_fading_openclose is enabled. - if (!win_should_fade(ps, mw)) { - win_skip_fading(mw); - } - } - - // don't need win_ev_stop because the window is gone anyway - // Send D-Bus signal - if (ps->o.dbus) { - cdbus_ev_win_destroyed(session_get_cdbus(ps), w); - } - - if (!ps->redirected && w->managed) { - // Skip transition if we are not rendering - win_skip_fading(mw); } } -void unmap_win_start(session_t *ps, struct managed_win *w) { +void unmap_win_start(struct managed_win *w) { assert(w); assert(w->base.managed); assert(w->a._class != XCB_WINDOW_CLASS_INPUT_ONLY); @@ -2292,33 +2186,170 @@ void unmap_win_start(session_t *ps, struct managed_win *w) { w->a.map_state = XCB_MAP_STATE_UNMAPPED; w->state = WSTATE_UNMAPPED; - win_start_fade(ps, w, 0); +} - // Send D-Bus signal - if (ps->o.dbus) { - cdbus_ev_win_unmapped(session_get_cdbus(ps), &w->base); +struct win_script_context +win_script_context_prepare(struct session *ps, struct managed_win *w) { + struct win_script_context ret = { + .x = w->g.x, + .y = w->g.y, + .width = w->widthb, + .height = w->heightb, + .opacity = win_calc_opacity_target(ps, w), + .opacity_before = w->opacity, + }; + return ret; +} + +double win_animatable_get(const struct managed_win *w, enum win_script_output output) { + if (w->running_animation && w->running_animation_outputs[output] >= 0) { + return w->running_animation->memory[w->running_animation_outputs[output]]; + } + switch (output) { + case WIN_SCRIPT_BLUR_OPACITY: return w->state == WSTATE_MAPPED ? 1.0 : 0.0; + case WIN_SCRIPT_OPACITY: + case WIN_SCRIPT_SHADOW_OPACITY: return w->opacity; + case WIN_SCRIPT_OFFSET_X: + case WIN_SCRIPT_OFFSET_Y: + case WIN_SCRIPT_SHADOW_OFFSET_X: + case WIN_SCRIPT_SHADOW_OFFSET_Y: return 0; + } + unreachable(); +} + +#define WSTATE_PAIR(a, b) ((int)(a) * NUM_OF_WSTATES + (int)(b)) + +void win_process_animation_and_state_change(struct session *ps, struct managed_win *w, + double delta_t) { + // If the window hasn't ever been damaged yet, it won't be rendered in this frame. + // And if it is also unmapped/destroyed, it won't receive damage events. In this + // case we can skip its animation. For mapped windows, we need to provisionally + // start animation, because its first damage event might come a bit late. + bool will_never_render = !w->ever_damaged && w->state != WSTATE_MAPPED; + if (!ps->redirected || will_never_render) { + // This window won't be rendered, so we don't need to run the animations. + free(w->running_animation); + w->running_animation = NULL; + w->previous.state = w->state; + w->opacity = win_calc_opacity_target(ps, w); + return; } - if (!ps->redirected || !w->ever_damaged) { - // If we are not redirected, we skip fading because we aren't - // rendering anything anyway. If the window wasn't ever damaged, - // it shouldn't be painted either. But a fading out window is - // always painted, so we have to skip fading here. - win_skip_fading(w); + auto win_ctx = win_script_context_prepare(ps, w); + w->opacity = win_ctx.opacity; + if (w->previous.state == w->state && win_ctx.opacity_before == win_ctx.opacity) { + // No state changes, if there's a animation running, we just continue it. + if (w->running_animation == NULL) { + return; + } + log_debug("Advance animation for %#010x (%s) %f seconds", w->base.id, + w->name, delta_t); + if (!script_instance_is_finished(w->running_animation)) { + w->running_animation->elapsed += delta_t; + auto result = + script_instance_evaluate(w->running_animation, &win_ctx); + if (result != SCRIPT_EVAL_OK) { + log_error("Failed to run animation script: %d", result); + } + return; + } + free(w->running_animation); + w->running_animation = NULL; + w->in_openclose = false; + if (w->state == WSTATE_UNMAPPED) { + unmap_win_finish(ps, w); + } else if (w->state == WSTATE_DESTROYED) { + destroy_win_finish(ps, &w->base); + } + return; } -} -/// Skip the current in progress fading of window, -/// transition the window straight to its end state -void win_skip_fading(struct managed_win *w) { - if (w->number_of_animations == 0) { + // Try to determine the right animation trigger based on state changes. Note there + // is some complications here. X automatically unmaps windows before destroying + // them. So a "close" trigger will also be fired from a UNMAPPED -> DESTROYED + // transition, besides the more obvious MAPPED -> DESTROYED transition. But this + // also means, if the user didn't configure a animation for "hide", but did + // for "close", there is a chance this animation won't be triggered, if there is a + // gap between the UnmapNotify and DestroyNotify. There is no way on our end of + // fixing this without using hacks. + enum animation_trigger trigger = ANIMATION_TRIGGER_INVALID; + if (w->previous.state == w->state) { + // Only opacity changed + assert(w->state == WSTATE_MAPPED); + trigger = win_ctx.opacity > win_ctx.opacity_before + ? ANIMATION_TRIGGER_INCREASE_OPACITY + : ANIMATION_TRIGGER_DECREASE_OPACITY; + } else { + // Send D-Bus signal + if (ps->o.dbus) { + switch (w->state) { + case WSTATE_UNMAPPED: + cdbus_ev_win_unmapped(session_get_cdbus(ps), &w->base); + break; + case WSTATE_MAPPED: + cdbus_ev_win_mapped(session_get_cdbus(ps), &w->base); + break; + case WSTATE_DESTROYED: + cdbus_ev_win_destroyed(session_get_cdbus(ps), &w->base); + break; + } + } + + auto old_state = w->previous.state; + w->previous.state = w->state; + switch (WSTATE_PAIR(old_state, w->state)) { + case WSTATE_PAIR(WSTATE_UNMAPPED, WSTATE_MAPPED): + trigger = w->in_openclose ? ANIMATION_TRIGGER_OPEN + : ANIMATION_TRIGGER_SHOW; + break; + case WSTATE_PAIR(WSTATE_UNMAPPED, WSTATE_DESTROYED): + if ((!ps->o.no_fading_destroyed_argb || !win_has_alpha(w)) && + w->running_animation != NULL) { + trigger = ANIMATION_TRIGGER_CLOSE; + } + break; + case WSTATE_PAIR(WSTATE_MAPPED, WSTATE_DESTROYED): + // TODO(yshui) we should deprecate "no-fading-destroyed-argb" and + // ask user to write fading rules (after we have added such + // rules). Ditto below. + if (!ps->o.no_fading_destroyed_argb || !win_has_alpha(w)) { + trigger = ANIMATION_TRIGGER_CLOSE; + } + break; + case WSTATE_PAIR(WSTATE_MAPPED, WSTATE_UNMAPPED): + trigger = ANIMATION_TRIGGER_HIDE; + break; + default: + log_error("Impossible state transition from %d to %d", old_state, + w->state); + assert(false); + free(w->running_animation); + w->running_animation = NULL; + return; + } + } + + if (trigger == ANIMATION_TRIGGER_INVALID || ps->o.animations[trigger].script == NULL) { + free(w->running_animation); + w->running_animation = NULL; return; } - log_debug("Skipping fading process of window %#010x (%s)", w->base.id, w->name); - animatable_skip(&w->opacity); - animatable_skip(&w->blur_opacity); + + log_debug("Starting animation %s for window %#010x (%s)", + animation_trigger_names[trigger], w->base.id, w->name); + + auto new_animation = script_instance_new(ps->o.animations[trigger].script); + if (w->running_animation) { + script_instance_resume_from(w->running_animation, new_animation); + free(w->running_animation); + } + w->running_animation = new_animation; + w->running_animation_outputs = ps->o.animations[trigger].output_indices; + script_instance_evaluate(w->running_animation, &win_ctx); } +#undef WSTATE_PAIR + // TODO(absolutelynothelix): rename to x_update_win_(randr_?)monitor and move to // the x.c. void win_update_monitor(struct x_monitors *monitors, struct managed_win *mw) { @@ -2339,9 +2370,9 @@ void win_update_monitor(struct x_monitors *monitors, struct managed_win *mw) { mw->base.id, mw->name, mw->g.x, mw->g.y, mw->widthb, mw->heightb); } -/// Map an already registered window -void map_win_start(session_t *ps, struct managed_win *w) { - assert(ps->server_grabbed); +/// Start the mapping of a window. We cannot map immediately since we might need to fade +/// the window in. +static void map_win_start(struct managed_win *w) { assert(w); // Don't care about window mapping if it's an InputOnly window @@ -2350,7 +2381,7 @@ void map_win_start(session_t *ps, struct managed_win *w) { return; } - log_debug("Mapping (%#010x \"%s\")", w->base.id, w->name); + log_debug("Mapping (%#010x \"%s\"), old state %d", w->base.id, w->name, w->state); assert(w->state != WSTATE_DESTROYED); if (w->state == WSTATE_MAPPED) { @@ -2375,44 +2406,12 @@ void map_win_start(session_t *ps, struct managed_win *w) { log_debug("Window (%#010x) has type %s", w->base.id, WINTYPES[w->window_type].name); w->state = WSTATE_MAPPED; - win_start_fade(ps, w, 1); win_set_flags(w, WIN_FLAGS_PIXMAP_STALE); - - log_debug("Window %#010x has opacity %f, opacity target is %f", w->base.id, - animatable_get(&w->opacity), w->opacity.target); - - // Send D-Bus signal - if (ps->o.dbus) { - cdbus_ev_win_mapped(session_get_cdbus(ps), &w->base); - } - - if (!ps->redirected) { - win_skip_fading(w); - } -} - -/** - * Update target window opacity depending on the current state. - */ -void win_update_opacity_target(session_t *ps, struct managed_win *w) { - win_start_fade(ps, w, w->blur_opacity.target); // We don't want to change - // blur_opacity target - - if (w->number_of_animations == 0) { - return; - } - - log_debug("Window %#010x (%s) opacity %f, opacity target %f, start %f", w->base.id, - w->name, animatable_get(&w->opacity), w->opacity.target, w->opacity.start); - - if (!ps->redirected) { - win_skip_fading(w); - } } /// Set flags on a window. Some sanity checks are performed void win_set_flags(struct managed_win *w, uint64_t flags) { - log_debug("Set flags %" PRIu64 " to window %#010x (%s)", flags, w->base.id, w->name); + log_verbose("Set flags %" PRIu64 " to window %#010x (%s)", flags, w->base.id, w->name); if (unlikely(w->state == WSTATE_DESTROYED)) { log_error("Flags set on a destroyed window %#010x (%s)", w->base.id, w->name); return; @@ -2423,11 +2422,11 @@ void win_set_flags(struct managed_win *w, uint64_t flags) { /// Clear flags on a window. Some sanity checks are performed void win_clear_flags(struct managed_win *w, uint64_t flags) { - log_debug("Clear flags %" PRIu64 " from window %#010x (%s)", flags, w->base.id, - w->name); + log_verbose("Clear flags %" PRIu64 " from window %#010x (%s)", flags, w->base.id, + w->name); if (unlikely(w->state == WSTATE_DESTROYED)) { - log_warn("Flags cleared on a destroyed window %#010x (%s)", w->base.id, - w->name); + log_warn("Flags %" PRIu64 " cleared on a destroyed window %#010x (%s)", + flags, w->base.id, w->name); return; } diff --git a/src/win.h b/src/win.h index 66da22c4cc..5343185427 100644 --- a/src/win.h +++ b/src/win.h @@ -101,6 +101,12 @@ struct win_geometry { uint16_t border_width; }; +/// These are changes of window state that might trigger an animation. We separate them +/// out and delay their application so determine which animation to run is easier. +struct win_state_change { + winstate_t state; +}; + struct managed_win { struct win base; /// backend data attached to this window. Only available when @@ -131,7 +137,7 @@ struct managed_win { /// Client window visual pict format const xcb_render_pictforminfo_t *client_pictfmt; /// Window painting mode. - winmode_t mode; + winmode_t mode; // TODO(yshui) only used by legacy backends, remove. /// Whether the window has been damaged at least once since it /// was mapped. Unmapped windows that were previously mapped /// retain their `ever_damaged` state. Mapping a window resets @@ -214,11 +220,7 @@ struct managed_win { // Opacity-related members /// Window opacity - struct animatable opacity; - /// Opacity of the window's background blur - /// Used to gracefully fade in/out the window, otherwise the blur - /// would be at full/zero intensity immediately which will be jarring. - struct animatable blur_opacity; + double opacity; /// true if window (or client window, for broken window managers /// not transferring client window's _NET_WM_WINDOW_OPACITY value) has opacity /// prop @@ -292,8 +294,6 @@ struct managed_win { struct c2_window_state c2_state; // Animation related - /// Number of animations currently in progress - unsigned int number_of_animations; #ifdef CONFIG_OPENGL /// Textures and FBO background blur use. @@ -304,6 +304,13 @@ struct managed_win { /// The damaged region of the window, in window local coordinates. region_t damaged; + + /// Previous state of the window before state changed. This is used + /// by `win_process_animation_and_state_change` to trigger appropriate + /// animations. + struct win_state_change previous; + struct script_instance *running_animation; + const int *running_animation_outputs; }; struct win_script_context { @@ -334,17 +341,15 @@ static const struct script_output_info win_script_outputs[] = { /// Process pending updates/images flags on a window. Has to be called in X critical /// section +void win_process_animation_and_state_change(struct session *ps, struct managed_win *w, + double delta_t); +double win_animatable_get(const struct managed_win *w, enum win_script_output output); void win_process_update_flags(session_t *ps, struct managed_win *w); void win_process_image_flags(session_t *ps, struct managed_win *w); /// Start the unmap of a window. We cannot unmap immediately since we might need to fade /// the window out. -void unmap_win_start(struct session *, struct managed_win *); - -/// Start the mapping of a window. We cannot map immediately since we might need to fade -/// the window in. -void map_win_start(struct session *, struct managed_win *); - +void unmap_win_start(struct managed_win *); /// Start the destroying of a window. Windows cannot always be destroyed immediately /// because of fading and such. void destroy_win_start(session_t *ps, struct win *w); @@ -363,7 +368,6 @@ void win_set_invert_color_force(session_t *ps, struct managed_win *w, switch_t v * Set real focused state of a window. */ void win_set_focused(session_t *ps, struct managed_win *w); -bool attr_pure win_should_fade(session_t *ps, const struct managed_win *w); void win_on_factor_change(session_t *ps, struct managed_win *w); void win_unmark_client(struct managed_win *w); @@ -414,10 +418,6 @@ struct win *attr_ret_nonnull maybe_allocate_managed_win(session_t *ps, struct wi */ void destroy_win_finish(session_t *ps, struct win *w); -/// Skip the current in progress fading of window, -/// transition the window straight to its end state -void win_skip_fading(struct managed_win *w); - /** * Check if a window is focused, without using any focus rules or forced focus settings */ diff --git a/src/win_defs.h b/src/win_defs.h index aca62a8677..04455de2d2 100644 --- a/src/win_defs.h +++ b/src/win_defs.h @@ -36,11 +36,12 @@ typedef enum { /// The window is mapped and viewable. Equivalent to map-state == /// XCB_MAP_STATE_VIEWABLE WSTATE_MAPPED, - // XCB_MAP_STATE_UNVIEWABLE is not represented here because it should not be // possible for top-level windows. } winstate_t; +#define NUM_OF_WSTATES (WSTATE_MAPPED + 1) + enum win_flags { // Note: *_NONE flags are mostly redundant and meant for detecting logical errors // in the code @@ -67,12 +68,19 @@ enum win_flags { }; enum win_script_output { + /// Additional X offset of the window. WIN_SCRIPT_OFFSET_X = 0, + /// Additional Y offset of the window. WIN_SCRIPT_OFFSET_Y, + /// Additional X offset of the shadow. WIN_SCRIPT_SHADOW_OFFSET_X, + /// Additional Y offset of the shadow. WIN_SCRIPT_SHADOW_OFFSET_Y, + /// Opacity of the window. WIN_SCRIPT_OPACITY, + /// Opacity of the blurred background of the window. WIN_SCRIPT_BLUR_OPACITY, + /// Opacity of the shadow. WIN_SCRIPT_SHADOW_OPACITY, }; #define NUM_OF_WIN_SCRIPT_OUTPUTS (WIN_SCRIPT_SHADOW_OPACITY + 1) From 39a9a71e050f81fed0c0fc226e4384b3ac5471dd Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Thu, 16 May 2024 19:01:21 +0100 Subject: [PATCH 08/23] win: make window and shadow offsets animatable Signed-off-by: Yuxuan Shui --- src/renderer/layout.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/renderer/layout.c b/src/renderer/layout.c index 37b27feae1..815d59db73 100644 --- a/src/renderer/layout.c +++ b/src/renderer/layout.c @@ -42,11 +42,16 @@ static bool layer_from_window(struct layer *out_layer, struct managed_win *w, iv goto out; } - out_layer->origin = (ivec2){.x = w->g.x, .y = w->g.y}; + out_layer->origin = + vec2_as((vec2){.x = w->g.x + win_animatable_get(w, WIN_SCRIPT_OFFSET_X), + .y = w->g.y + win_animatable_get(w, WIN_SCRIPT_OFFSET_Y)}); out_layer->size = (ivec2){.width = w->widthb, .height = w->heightb}; if (w->shadow) { out_layer->shadow_origin = - (ivec2){.x = w->g.x + w->shadow_dx, .y = w->g.y + w->shadow_dy}; + vec2_as((vec2){.x = w->g.x + w->shadow_dx + + win_animatable_get(w, WIN_SCRIPT_SHADOW_OFFSET_X), + .y = w->g.y + w->shadow_dy + + win_animatable_get(w, WIN_SCRIPT_SHADOW_OFFSET_Y)}); out_layer->shadow_size = (ivec2){.width = w->shadow_width, .height = w->shadow_height}; } else { From 1e32ced5d37cad883fac868e24adcc566f6c16b6 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Wed, 8 May 2024 19:55:45 +0100 Subject: [PATCH 09/23] transition: remove unused functions Signed-off-by: Yuxuan Shui --- src/transition.c | 126 ----------------------------------------------- src/transition.h | 88 +-------------------------------- 2 files changed, 1 insertion(+), 213 deletions(-) diff --git a/src/transition.c b/src/transition.c index 6521a612c8..56c11ac4d2 100644 --- a/src/transition.c +++ b/src/transition.c @@ -10,132 +10,6 @@ #include "transition.h" #include "utils.h" -double animatable_get_progress(const struct animatable *a) { - if (a->duration > 0) { - return a->elapsed / a->duration; - } - return 1; -} - -/// Get the current value of an `animatable`. -double animatable_get(const struct animatable *a) { - if (a->duration > 0) { - assert(a->elapsed < a->duration); - double t = a->curve->sample(a->curve, animatable_get_progress(a)); - return (1 - t) * a->start + t * a->target; - } - return a->target; -} - -/// Advance the animation by a given number of steps. -void animatable_advance(struct animatable *a, double elapsed) { - if (a->duration == 0 || elapsed <= 0) { - return; - } - - assert(a->elapsed < a->duration); - if (elapsed >= a->duration - a->elapsed) { - a->elapsed = a->duration; - } else { - a->elapsed += elapsed; - } - - if (a->elapsed == a->duration) { - a->start = a->target; - a->duration = 0; - a->elapsed = 0; - a->curve->free(a->curve); - a->curve = NULL; - if (a->callback) { - a->callback(TRANSITION_COMPLETED, a->callback_data); - a->callback = NULL; - a->callback_data = NULL; - } - } -} - -/// Returns whether an `animatable` is currently animating. -bool animatable_is_animating(const struct animatable *a) { - assert(a->duration == 0 || a->elapsed < a->duration); - return a->duration != 0; -} - -/// Cancel the current animation of an `animatable`. This stops the animation and -/// the `animatable` will retain its current value. -/// -/// Returns true if the `animatable` was animated before this function is called. -bool animatable_interrupt(struct animatable *a) { - if (a->duration == 0) { - return false; - } - - a->start = animatable_get(a); - a->target = a->start; - a->duration = 0; - a->elapsed = 0; - a->curve->free(a->curve); - a->curve = NULL; - if (a->callback) { - a->callback(TRANSITION_INTERRUPTED, a->callback_data); - a->callback = NULL; - a->callback_data = NULL; - } - return true; -} - -/// Cancel the current animation of an `animatable` and set its value to its target. -/// -/// Returns true if the `animatable` was animated before this function is called. -bool animatable_skip(struct animatable *a) { - if (a->duration == 0) { - return false; - } - - a->start = a->target; - a->duration = 0; - a->elapsed = 0; - a->curve->free(a->curve); - a->curve = NULL; - if (a->callback) { - a->callback(TRANSITION_SKIPPED, a->callback_data); - a->callback = NULL; - a->callback_data = NULL; - } - return true; -} - -/// Change the target value of an `animatable`. -/// If the `animatable` is already animating, the animation will be canceled first. -bool animatable_set_target(struct animatable *a, double target, double duration, - const struct curve *curve, transition_callback_fn cb, void *data) { - animatable_interrupt(a); - if (duration == 0 || a->start == target) { - a->start = target; - a->target = target; - curve->free(curve); - return false; - } - - a->target = target; - a->duration = duration; - a->elapsed = 0; - a->callback = cb; - a->callback_data = data; - a->curve = curve; - return true; -} - -/// Create a new animatable. -struct animatable animatable_new(double value) { - struct animatable ret = { - .start = value, - .target = value, - .duration = 0, - .elapsed = 0, - }; - return ret; -} - static double curve_sample_linear(const struct curve *this attr_unused, double progress) { return progress; } diff --git a/src/transition.h b/src/transition.h index b5131f4b7f..d6bcab299f 100644 --- a/src/transition.h +++ b/src/transition.h @@ -5,31 +5,7 @@ #include #include "compiler.h" -struct animatable; -enum transition_event; - -/// Callback when the transition state changes. Callback might be called by: -/// - `animatable_set_target` generates TRANSITION_COMPLETED when the specified duration -/// is 0. also generates TRANSITION_CANCELLED if the animatable was already animating. -/// - `animatable_cancel` generates TRANSITION_CANCELED -/// - `animatable_early_stop` generates TRANSITION_STOPPED_EARLY -/// - `animatable_step` generates TRANSITION_COMPLETED when the animation is completed. -/// Callback is guaranteed to be called exactly once for each `animatable_set_target` -/// call, unless an animatable is freed before the transition is completed. -typedef void (*transition_callback_fn)(enum transition_event event, void *data); - -enum transition_event { - TRANSITION_COMPLETED, - TRANSITION_INTERRUPTED, - TRANSITION_SKIPPED, -}; - -/// The base type for step_state. -struct step_state_base { - /// The current value of the `animatable`. - /// If the `animatable` is not animated, this equals to `animatable->target`. - double current; -}; +// ========================== Interpolators ========================== struct curve { /// The interpolator function for an animatable. This function should calculate @@ -40,68 +16,6 @@ struct curve { void (*free)(const struct curve *this); }; -/// An animatable value -struct animatable { - /// The starting value. - /// When this `animatable` is not animated, this is the current value. - double start; - /// The target value. - /// If the `animatable` is not animated, this equals to `start`. - double target; - /// The animation duration in unspecified units. - /// If the `animatable` is not animated, this is 0. - double duration; - /// The current progress of the animation in the same units as `duration`. - /// If the `animatable` is not animated, this is 0. - double elapsed; - - transition_callback_fn callback; - void *callback_data; - - /// The function for calculating the current value. If - /// `step_state` is not NULL, the `step` function is used; - /// otherwise, the `interpolator` function is used. - /// The interpolator function. - const struct curve *curve; -}; - -// =============================== API =============================== - -/// Get the current value of an `animatable`. -double animatable_get(const struct animatable *a); -/// Get the animation progress as a percentage of the total duration. -double animatable_get_progress(const struct animatable *a); -/// Advance the animation by a given amount. `elapsed` cannot be negative. -void animatable_advance(struct animatable *a, double elapsed); -/// Returns whether an `animatable` is currently animating. -bool animatable_is_animating(const struct animatable *a); -/// Interrupt the current animation of an `animatable`. This stops the animation and -/// the `animatable` will retain its current value. -/// -/// Returns true if the `animatable` was animated before this function is called. -bool animatable_interrupt(struct animatable *a); -/// Skip the current animation of an `animatable` and set its value to its target. -/// -/// Returns true if the `animatable` was animated before this function is called. -bool animatable_skip(struct animatable *a); -/// Change the target value of an `animatable`. Specify a duration, an interpolator -/// function, and a callback function. -/// -/// If the `animatable` is already animating, the animation will be canceled first. -/// -/// Note, In some cases this function does not start the animation, for example, if the -/// target value is the same as the current value of the animatable, or if the duration is -/// 0. If the animation is not started, the callback function will not be called. The -/// animatable's current animation, if it has one, will be canceled regardless. -/// -/// Returns if the animatable is now animated. -bool animatable_set_target(struct animatable *a, double target, double duration, - const struct curve *curve, transition_callback_fn cb, void *data); -/// Create a new animatable. -struct animatable animatable_new(double value); - -// ========================== Interpolators ========================== - const struct curve *curve_new_linear(void); const struct curve *curve_new_cubic_bezier(double x1, double y1, double x2, double y2); const struct curve *curve_new_step(int steps, bool jump_start, bool jump_end); From 05e5f48b49c5fa0ae174adf90250eba836549648 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Thu, 9 May 2024 03:14:55 +0100 Subject: [PATCH 10/23] core: remove fade_timer from session Signed-off-by: Yuxuan Shui --- src/common.h | 2 -- src/picom.c | 10 ---------- 2 files changed, 12 deletions(-) diff --git a/src/common.h b/src/common.h index a612f63f95..b7b220e930 100644 --- a/src/common.h +++ b/src/common.h @@ -150,8 +150,6 @@ typedef struct session { ev_io xiow; /// Timeout for delayed unredirection. ev_timer unredir_timer; - /// Timer for fading - ev_timer fade_timer; /// Use an ev_timer callback for drawing ev_timer draw_timer; /// Called every time we have timeouts or new data on socket, diff --git a/src/picom.c b/src/picom.c index 23f063f502..0c194ba1bb 100644 --- a/src/picom.c +++ b/src/picom.c @@ -1612,13 +1612,6 @@ static void tmout_unredir_callback(EV_P attr_unused, ev_timer *w, int revents at queue_redraw(ps); } -static void fade_timer_callback(EV_P attr_unused, ev_timer *w, int revents attr_unused) { - // TODO(yshui): do we still need the fade timer? we queue redraw automatically in - // draw_callback_impl if animation is running. - session_t *ps = session_ptr(w, fade_timer); - queue_redraw(ps); -} - static void handle_pending_updates(EV_P_ struct session *ps, double delta_t) { log_trace("Delayed handling of events, entering critical section"); auto e = xcb_request_check(ps->c.c, xcb_grab_server_checked(ps->c.c)); @@ -2401,8 +2394,6 @@ static session_t *session_init(int argc, char **argv, Display *dpy, ev_init(&ps->unredir_timer, tmout_unredir_callback); ev_init(&ps->draw_timer, draw_callback); - ev_init(&ps->fade_timer, fade_timer_callback); - // Set up SIGUSR1 signal handler to reset program ev_signal_init(&ps->usr1_signal, reset_enable, SIGUSR1); ev_signal_init(&ps->int_signal, exit_enable, SIGINT); @@ -2639,7 +2630,6 @@ static void session_destroy(session_t *ps) { // Stop libev event handlers ev_timer_stop(ps->loop, &ps->unredir_timer); - ev_timer_stop(ps->loop, &ps->fade_timer); ev_timer_stop(ps->loop, &ps->draw_timer); ev_prepare_stop(ps->loop, &ps->event_check); ev_signal_stop(ps->loop, &ps->usr1_signal); From b7becd92a95520827efbfc596f81abb62bc9c0ee Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Thu, 9 May 2024 05:15:03 +0100 Subject: [PATCH 11/23] tests: add animations to base config parsing test Signed-off-by: Yuxuan Shui --- tests/configs/parsing_test.conf | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/configs/parsing_test.conf b/tests/configs/parsing_test.conf index a30886800a..f27cd9aa8d 100644 --- a/tests/configs/parsing_test.conf +++ b/tests/configs/parsing_test.conf @@ -423,3 +423,13 @@ window-shader-fg-rule = " shader.frag :name = 'a'", "default:name = 'b'" ] + +animations = ({ + triggers = ["close", "hide"]; + offset-y = { + timing = "0.2s linear"; + start = 0; + end = "- window-height - window-y"; + }; + opacity = 1; +}) From 69835cb29ae795c01677fdf6161a3ccff9223b5f Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Sat, 11 May 2024 17:54:15 +0100 Subject: [PATCH 12/23] options: disable animations on legacy backends Signed-off-by: Yuxuan Shui --- src/options.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/options.c b/src/options.c index 7043a9f330..a70523f5db 100644 --- a/src/options.c +++ b/src/options.c @@ -883,6 +883,20 @@ bool get_cfg(options_t *opt, int argc, char *const *argv) { check_end:; } + if (opt->legacy_backends && opt->number_of_scripts > 0) { + log_warn("Custom animations are not supported by the legacy " + "backends. Disabling animations."); + for (size_t i = 0; i < ARR_SIZE(opt->animations); i++) { + opt->animations[i].script = NULL; + } + for (int i = 0; i < opt->number_of_scripts; i++) { + script_free(opt->all_scripts[i]); + } + free(opt->all_scripts); + opt->all_scripts = NULL; + opt->number_of_scripts = 0; + } + generate_fading_config(opt); return true; } From a2525fb2f73b9b13db2dc3bbb9cf267f2f5afee0 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Sat, 11 May 2024 01:45:07 +0100 Subject: [PATCH 13/23] backend: add scale to blit parameters Signed-off-by: Yuxuan Shui --- src/backend/backend.h | 5 +++++ src/renderer/command_builder.c | 2 ++ src/renderer/renderer.c | 3 +++ 3 files changed, 10 insertions(+) diff --git a/src/backend/backend.h b/src/backend/backend.h index 10bcd14332..3d9f443a7f 100644 --- a/src/backend/backend.h +++ b/src/backend/backend.h @@ -125,6 +125,9 @@ struct backend_blit_args { /// will be normalized so that the maximum brightness is /// this value. double max_brightness; + /// Scale factor for the horizontal and vertical direction (X for horizontal, + /// Y for vertical). + vec2 scale; /// Corner radius of the source image. The corners of /// the source image will be rounded. double corner_radius; @@ -139,6 +142,8 @@ struct backend_blit_args { bool color_inverted; }; +static const vec2 SCALE_IDENTITY = {1.0, 1.0}; + enum backend_image_format { /// A format that can be used for normal rendering, and binding /// X pixmaps. diff --git a/src/renderer/command_builder.c b/src/renderer/command_builder.c index 0dc8067c1e..3950c4190b 100644 --- a/src/renderer/command_builder.c +++ b/src/renderer/command_builder.c @@ -58,6 +58,7 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, .corner_radius = w->corner_radius, .opacity = layer->opacity, .dim = dim, + .scale = SCALE_IDENTITY, .effective_size = raw_size, .shader = w->fg_shader ? w->fg_shader->backend_shader : NULL, .color_inverted = w->invert_color, @@ -138,6 +139,7 @@ command_for_shadow(struct layer *layer, struct backend_command *cmd, .opacity = layer->shadow_opacity, .max_brightness = 1, .source_mask = w->corner_radius > 0 ? &cmd->source_mask : NULL, + .scale = SCALE_IDENTITY, .effective_size = layer->shadow_size, .target_mask = &cmd->target_mask, }; diff --git a/src/renderer/renderer.c b/src/renderer/renderer.c index 68130a9c12..6e333a8a6a 100644 --- a/src/renderer/renderer.c +++ b/src/renderer/renderer.c @@ -259,6 +259,7 @@ image_handle renderer_shadow_from_mask(struct renderer *r, struct backend_base * .color_inverted = false, .effective_size = mask_size, .dim = 0, + .scale = SCALE_IDENTITY, .corner_radius = 0, .border_width = 0, .max_brightness = 1, @@ -334,6 +335,7 @@ image_handle renderer_shadow_from_mask(struct renderer *r, struct backend_base * .corner_radius = 0, .border_width = 0, .max_brightness = 1, + .scale = SCALE_IDENTITY, }; pixman_region32_init_rect(&target_mask, 0, 0, (unsigned)shadow_size.width, (unsigned)shadow_size.height); @@ -596,6 +598,7 @@ bool renderer_render(struct renderer *r, struct backend_base *backend, .effective_size = r->canvas_size, .source_mask = NULL, .target_mask = &damage_region, + .scale = SCALE_IDENTITY, }; log_trace("Blit for monitor repaint"); backend->ops->blit(backend, (ivec2){}, r->back_image, &blit); From d894e90850213ef31563a7546cd1a2353bd5e19e Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Sat, 11 May 2024 01:45:36 +0100 Subject: [PATCH 14/23] backend: gl: support the scale parameter of blit Signed-off-by: Yuxuan Shui --- src/backend/gl/blur.c | 22 ++++----- src/backend/gl/gl_common.c | 94 +++++++++++++++++++++----------------- src/backend/gl/gl_common.h | 19 ++++---- src/types.h | 4 ++ 4 files changed, 79 insertions(+), 60 deletions(-) diff --git a/src/backend/gl/blur.c b/src/backend/gl/blur.c index b61eaa450a..c9d85fa0eb 100644 --- a/src/backend/gl/blur.c +++ b/src/backend/gl/blur.c @@ -344,18 +344,18 @@ bool gl_blur(struct backend_base *base, ivec2 origin, image_handle target_, } // Original region for the final compositing step from blur result to target. - auto coord = ccalloc(nrects * 16, GLint); + auto coord = ccalloc(nrects * 16, GLfloat); auto indices = ccalloc(nrects * 6, GLuint); - gl_mask_rects_to_coords(origin, nrects, rects, coord, indices); + gl_mask_rects_to_coords(origin, nrects, rects, SCALE_IDENTITY, coord, indices); if (!target->y_inverted) { gl_y_flip_target(nrects, coord, target->height); } // Resize region for sampling from source texture, and for blur passes - auto coord_resized = ccalloc(nrects_resized * 16, GLint); + auto coord_resized = ccalloc(nrects_resized * 16, GLfloat); auto indices_resized = ccalloc(nrects_resized * 6, GLuint); - gl_mask_rects_to_coords(origin, nrects_resized, rects_resized, coord_resized, - indices_resized); + gl_mask_rects_to_coords(origin, nrects_resized, rects_resized, SCALE_IDENTITY, + coord_resized, indices_resized); pixman_region32_fini(®_blur_resized); // FIXME(yshui) In theory we should handle blurring a non-y-inverted source, but // we never actually use that capability anywhere. @@ -375,9 +375,9 @@ bool gl_blur(struct backend_base *base, ivec2 origin, image_handle target_, indices, GL_STREAM_DRAW); glEnableVertexAttribArray(vert_coord_loc); glEnableVertexAttribArray(vert_in_texcoord_loc); - glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 4, NULL); - glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_INT, GL_FALSE, - sizeof(GLint) * 4, (void *)(sizeof(GLint) * 2)); + glVertexAttribPointer(vert_coord_loc, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 4, NULL); + glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_FLOAT, GL_FALSE, + sizeof(GLfloat) * 4, (void *)(sizeof(GLfloat) * 2)); glBindVertexArray(vao[1]); glBindBuffer(GL_ARRAY_BUFFER, bo[2]); @@ -389,9 +389,9 @@ bool gl_blur(struct backend_base *base, ivec2 origin, image_handle target_, GL_STREAM_DRAW); glEnableVertexAttribArray(vert_coord_loc); glEnableVertexAttribArray(vert_in_texcoord_loc); - glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 4, NULL); - glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_INT, GL_FALSE, - sizeof(GLint) * 4, (void *)(sizeof(GLint) * 2)); + glVertexAttribPointer(vert_coord_loc, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 4, NULL); + glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_FLOAT, GL_FALSE, + sizeof(GLfloat) * 4, (void *)(sizeof(GLfloat) * 2)); int vao_nelems[2] = {nrects * 6, nrects_resized * 6}; diff --git a/src/backend/gl/gl_common.c b/src/backend/gl/gl_common.c index 7d543561f7..e9ab9f3c66 100644 --- a/src/backend/gl/gl_common.c +++ b/src/backend/gl/gl_common.c @@ -366,10 +366,10 @@ struct gl_vertex_attribs_definition { }; static const struct gl_vertex_attribs_definition gl_blit_vertex_attribs = { - .stride = sizeof(GLint) * 4, + .stride = sizeof(GLfloat) * 4, .count = 2, - .attribs = {{GL_INT, vert_coord_loc, NULL}, - {GL_INT, vert_in_texcoord_loc, ((GLint *)NULL) + 2}}, + .attribs = {{GL_FLOAT, vert_coord_loc, NULL}, + {GL_FLOAT, vert_in_texcoord_loc, ((GLfloat *)NULL) + 2}}, }; /** @@ -384,7 +384,7 @@ static const struct gl_vertex_attribs_definition gl_blit_vertex_attribs = { * @param nuniforms number of uniforms for `shader` * @param uniforms uniforms for `shader` */ -static void gl_blit_inner(GLuint target_fbo, int nrects, GLint *coord, GLuint *indices, +static void gl_blit_inner(GLuint target_fbo, int nrects, GLfloat *coord, GLuint *indices, const struct gl_vertex_attribs_definition *vert_attribs, const struct gl_shader *shader, int nuniforms, struct gl_uniform_value *uniforms) { @@ -475,26 +475,29 @@ static void gl_blit_inner(GLuint target_fbo, int nrects, GLint *coord, GLuint *i gl_check_err(); } -void gl_mask_rects_to_coords(ivec2 origin, int nrects, const rect_t *rects, GLint *coord, - GLuint *indices) { +void gl_mask_rects_to_coords(ivec2 origin, int nrects, const rect_t *rects, vec2 scale, + GLfloat *coord, GLuint *indices) { for (ptrdiff_t i = 0; i < nrects; i++) { // Rectangle in source image coordinates rect_t rect_src = region_translate_rect(rects[i], ivec2_neg(origin)); // Rectangle in target image coordinates rect_t rect_dst = rects[i]; + // clang-format off memcpy(&coord[i * 16], - ((GLint[][2]){ - {rect_dst.x1, rect_dst.y1}, // Vertex, bottom-left - {rect_src.x1, rect_src.y1}, // Texture - {rect_dst.x2, rect_dst.y1}, // Vertex, bottom-right - {rect_src.x2, rect_src.y1}, // Texture - {rect_dst.x2, rect_dst.y2}, // Vertex, top-right - {rect_src.x2, rect_src.y2}, // Texture - {rect_dst.x1, rect_dst.y2}, // Vertex, top-left - {rect_src.x1, rect_src.y2}, // Texture + ((GLfloat[][2]){ + // Interleaved vertex and texture coordinates, starting with vertex. + {(GLfloat)rect_dst.x1, (GLfloat)rect_dst.y1}, // bottom-left + {(GLfloat)(rect_src.x1 / scale.x), (GLfloat)(rect_src.y1 / scale.y)}, + {(GLfloat)rect_dst.x2, (GLfloat)rect_dst.y1}, // bottom-right + {(GLfloat)(rect_src.x2 / scale.x), (GLfloat)(rect_src.y1 / scale.y)}, + {(GLfloat)rect_dst.x2, (GLfloat)rect_dst.y2}, // top-right + {(GLfloat)(rect_src.x2 / scale.x), (GLfloat)(rect_src.y2 / scale.y)}, + {(GLfloat)rect_dst.x1, (GLfloat)rect_dst.y2}, // top-left + {(GLfloat)(rect_src.x1 / scale.x), (GLfloat)(rect_src.y2 / scale.y)}, }), sizeof(GLint[2]) * 8); + // clang-format on GLuint u = (GLuint)(i * 4); memcpy(&indices[i * 6], @@ -509,13 +512,13 @@ void gl_mask_rects_to_coords(ivec2 origin, int nrects, const rect_t *rects, GLin /// @param[in] nrects number of rectangles /// @param[in] coord OpenGL vertex coordinates /// @param[in] texture_height height of the source image -static inline void gl_y_flip_texture(int nrects, GLint *coord, GLint texture_height) { +static inline void gl_y_flip_texture(int nrects, GLfloat *coord, GLint texture_height) { for (ptrdiff_t i = 0; i < nrects; i++) { auto current_rect = &coord[i * 16]; // 16 numbers per rectangle for (ptrdiff_t j = 0; j < 4; j++) { // 4 numbers per vertex, texture coordinates are the last two auto current_vertex = ¤t_rect[j * 4 + 2]; - current_vertex[1] = texture_height - current_vertex[1]; + current_vertex[1] = (GLfloat)texture_height - current_vertex[1]; } } } @@ -524,7 +527,7 @@ static inline void gl_y_flip_texture(int nrects, GLint *coord, GLint texture_hei /// shader, and uniforms. static int gl_lower_blit_args(struct gl_data *gd, ivec2 origin, const struct backend_blit_args *args, - GLint **coord, GLuint **indices, struct gl_shader **shader, + GLfloat **coord, GLuint **indices, struct gl_shader **shader, struct gl_uniform_value *uniforms) { auto img = (struct gl_texture *)args->source_image; int nrects; @@ -534,9 +537,9 @@ gl_lower_blit_args(struct gl_data *gd, ivec2 origin, const struct backend_blit_a // Nothing to paint return 0; } - *coord = ccalloc(nrects * 16, GLint); + *coord = ccalloc(nrects * 16, GLfloat); *indices = ccalloc(nrects * 6, GLuint); - gl_mask_rects_to_coords(origin, nrects, rects, *coord, *indices); + gl_mask_rects_to_coords(origin, nrects, rects, args->scale, *coord, *indices); if (!img->y_inverted) { gl_y_flip_texture(nrects, *coord, img->height); } @@ -558,11 +561,13 @@ gl_lower_blit_args(struct gl_data *gd, ivec2 origin, const struct backend_blit_a border_width = 0; } // clang-format off + auto tex_sampler = vec2_eq(args->scale, SCALE_IDENTITY) ? + gd->samplers[GL_SAMPLER_REPEAT] : gd->samplers[GL_SAMPLER_REPEAT_SCALE]; struct gl_uniform_value from_uniforms[] = { [UNIFORM_OPACITY_LOC] = {.type = GL_FLOAT, .f = (float)args->opacity}, [UNIFORM_INVERT_COLOR_LOC] = {.type = GL_INT, .i = args->color_inverted}, [UNIFORM_TEX_LOC] = {.type = GL_TEXTURE_2D, - .tu = {img->texture, gd->samplers[GL_SAMPLER_REPEAT]}}, + .tu = {img->texture, tex_sampler}}, [UNIFORM_EFFECTIVE_SIZE_LOC] = {.type = GL_FLOAT_VEC2, .f2 = {(float)args->effective_size.width, (float)args->effective_size.height}}, @@ -613,7 +618,7 @@ bool gl_blit(backend_t *base, ivec2 origin, image_handle target_, return false; } - GLint *coord; + GLfloat *coord; GLuint *indices; struct gl_shader *shader; struct gl_uniform_value uniforms[NUMBER_OF_UNIFORMS] = {}; @@ -678,9 +683,9 @@ static bool gl_copy_area_draw(struct gl_data *gd, ivec2 origin, return true; } - auto coord = ccalloc(16 * nrects, GLint); + auto coord = ccalloc(16 * nrects, GLfloat); auto indices = ccalloc(6 * nrects, GLuint); - gl_mask_rects_to_coords(origin, nrects, rects, coord, indices); + gl_mask_rects_to_coords(origin, nrects, rects, SCALE_IDENTITY, coord, indices); if (!target->y_inverted) { gl_y_flip_target(nrects, coord, target->height); } @@ -856,6 +861,17 @@ uint64_t gl_get_shader_attributes(backend_t *backend_data attr_unused, void *sha return ret; } +static const struct { + GLint filter; + GLint wrap; +} gl_sampler_params[] = { + [GL_SAMPLER_REPEAT] = {GL_NEAREST, GL_REPEAT}, + [GL_SAMPLER_REPEAT_SCALE] = {GL_LINEAR, GL_REPEAT}, + [GL_SAMPLER_BLUR] = {GL_LINEAR, GL_CLAMP_TO_EDGE}, + [GL_SAMPLER_EDGE] = {GL_NEAREST, GL_CLAMP_TO_EDGE}, + [GL_SAMPLER_BORDER] = {GL_NEAREST, GL_CLAMP_TO_BORDER}, +}; + bool gl_init(struct gl_data *gd, session_t *ps) { if (!epoxy_has_gl_extension("GL_ARB_explicit_uniform_location")) { log_error("GL_ARB_explicit_uniform_location support is required but " @@ -965,20 +981,16 @@ bool gl_init(struct gl_data *gd, session_t *ps) { gd->back_image.height = ps->root_height; glGenSamplers(GL_MAX_SAMPLERS, gd->samplers); - for (int i = 0; i < GL_MAX_SAMPLERS; i++) { - glSamplerParameteri(gd->samplers[i], GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glSamplerParameteri(gd->samplers[i], GL_TEXTURE_MAG_FILTER, GL_NEAREST); - } - glSamplerParameterf(gd->samplers[GL_SAMPLER_EDGE], GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glSamplerParameterf(gd->samplers[GL_SAMPLER_EDGE], GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glSamplerParameterf(gd->samplers[GL_SAMPLER_BLUR], GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glSamplerParameterf(gd->samplers[GL_SAMPLER_BLUR], GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glSamplerParameterf(gd->samplers[GL_SAMPLER_BLUR], GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glSamplerParameterf(gd->samplers[GL_SAMPLER_BLUR], GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glSamplerParameterf(gd->samplers[GL_SAMPLER_BORDER], GL_TEXTURE_WRAP_S, - GL_CLAMP_TO_BORDER); - glSamplerParameterf(gd->samplers[GL_SAMPLER_BORDER], GL_TEXTURE_WRAP_T, - GL_CLAMP_TO_BORDER); + for (size_t i = 0; i < ARR_SIZE(gl_sampler_params); i++) { + glSamplerParameteri(gd->samplers[i], GL_TEXTURE_MIN_FILTER, + gl_sampler_params[i].filter); + glSamplerParameteri(gd->samplers[i], GL_TEXTURE_MAG_FILTER, + gl_sampler_params[i].filter); + glSamplerParameteri(gd->samplers[i], GL_TEXTURE_WRAP_S, + gl_sampler_params[i].wrap); + glSamplerParameteri(gd->samplers[i], GL_TEXTURE_WRAP_T, + gl_sampler_params[i].wrap); + } gd->logger = gl_string_marker_logger_new(); if (gd->logger) { @@ -1100,9 +1112,9 @@ image_handle gl_new_image(backend_t *backend_data attr_unused, bool gl_apply_alpha(backend_t *base, image_handle target, double alpha, const region_t *reg_op) { auto gd = (struct gl_data *)base; static const struct gl_vertex_attribs_definition vertex_attribs = { - .stride = sizeof(GLint) * 4, + .stride = sizeof(GLfloat) * 4, .count = 1, - .attribs = {{GL_INT, vert_coord_loc, NULL}}, + .attribs = {{GL_FLOAT, vert_coord_loc, NULL}}, }; if (alpha == 1.0 || !pixman_region32_not_empty(reg_op)) { return true; @@ -1115,7 +1127,7 @@ bool gl_apply_alpha(backend_t *base, image_handle target, double alpha, const re int nrects; const rect_t *rect = pixman_region32_rectangles(reg_op, &nrects); - auto coord = ccalloc(nrects * 16, GLint); + auto coord = ccalloc(nrects * 16, GLfloat); auto indices = ccalloc(nrects * 6, GLuint); struct gl_uniform_value uniforms[] = { diff --git a/src/backend/gl/gl_common.h b/src/backend/gl/gl_common.h index 3a679c757e..6e388834a8 100644 --- a/src/backend/gl/gl_common.h +++ b/src/backend/gl/gl_common.h @@ -66,7 +66,10 @@ struct gl_texture { }; enum gl_sampler { + /// A sampler that repeats the texture, with nearest filtering. GL_SAMPLER_REPEAT = 0, + /// A sampler that repeats the texture, with linear filtering. + GL_SAMPLER_REPEAT_SCALE, /// Clamp to edge GL_SAMPLER_EDGE, /// Clamp to border, border color will be (0, 0, 0, 0) @@ -126,13 +129,13 @@ void gl_prepare(backend_t *base, const region_t *reg); /// @param[in] rects mask rectangles, in mask coordinates /// @param[out] coord OpenGL vertex coordinates, suitable for creating VAO/VBO /// @param[out] indices OpenGL vertex indices, suitable for creating VAO/VBO -void gl_mask_rects_to_coords(ivec2 origin, int nrects, const rect_t *rects, GLint *coord, - GLuint *indices); -/// Like `gl_mask_rects_to_coords`, but with `origin` and `mask_origin` set to 0. i.e. all -/// coordinates are in the same space. +void gl_mask_rects_to_coords(ivec2 origin, int nrects, const rect_t *rects, vec2 scale, + GLfloat *coord, GLuint *indices); +/// Like `gl_mask_rects_to_coords`, but with `origin` is (0, 0). static inline void gl_mask_rects_to_coords_simple(int nrects, const rect_t *rects, - GLint *coord, GLuint *indices) { - return gl_mask_rects_to_coords((ivec2){0, 0}, nrects, rects, coord, indices); + GLfloat *coord, GLuint *indices) { + return gl_mask_rects_to_coords((ivec2){0, 0}, nrects, rects, SCALE_IDENTITY, + coord, indices); } GLuint gl_create_shader(GLenum shader_type, const char *shader_str); @@ -210,13 +213,13 @@ static inline GLuint gl_bind_image_to_fbo(struct gl_data *gd, image_handle image /// @param[in] nrects number of rectangles /// @param[in] coord OpenGL vertex coordinates /// @param[in] target_height height of the target image -static inline void gl_y_flip_target(int nrects, GLint *coord, GLint target_height) { +static inline void gl_y_flip_target(int nrects, GLfloat *coord, GLint target_height) { for (ptrdiff_t i = 0; i < nrects; i++) { auto current_rect = &coord[i * 16]; // 16 numbers per rectangle for (ptrdiff_t j = 0; j < 4; j++) { // 4 numbers per vertex, target coordinates are the first two auto current_vertex = ¤t_rect[j * 4]; - current_vertex[1] = target_height - current_vertex[1]; + current_vertex[1] = (GLfloat)target_height - current_vertex[1]; } } } diff --git a/src/types.h b/src/types.h index 4050f8065e..dd88ff6d1d 100644 --- a/src/types.h +++ b/src/types.h @@ -88,5 +88,9 @@ static inline ivec2 vec2_as(vec2 a) { }; } +static inline bool vec2_eq(vec2 a, vec2 b) { + return a.x == b.x && a.y == b.y; +} + #define MARGIN_INIT \ { 0, 0, 0, 0 } From a0b5d066c683014905f5bcca83aa224651e32013 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Thu, 16 May 2024 11:47:11 +0100 Subject: [PATCH 15/23] config: add debug option "consistent_buffer_age" An attempt to make rendering with damage deterministic even when replayed from a trace. Signed-off-by: Yuxuan Shui --- src/config.c | 7 ++-- src/config.h | 8 ++++ src/renderer/renderer.c | 87 ++++++++++++++++++++++++++++++----------- 3 files changed, 76 insertions(+), 26 deletions(-) diff --git a/src/config.c b/src/config.c index 59b1398e1a..a94ac5ee90 100644 --- a/src/config.c +++ b/src/config.c @@ -523,9 +523,10 @@ const char *vblank_scheduler_str[] = { [LAST_VBLANK_SCHEDULER] = NULL }; static const struct debug_options_entry debug_options_entries[] = { - {"always_rebind_pixmap", NULL , offsetof(struct debug_options, always_rebind_pixmap)}, - {"smart_frame_pacing" , NULL , offsetof(struct debug_options, smart_frame_pacing)}, - {"force_vblank_sched" , vblank_scheduler_str, offsetof(struct debug_options, force_vblank_scheduler)}, + {"always_rebind_pixmap" , NULL , offsetof(struct debug_options, always_rebind_pixmap)}, + {"smart_frame_pacing" , NULL , offsetof(struct debug_options, smart_frame_pacing)}, + {"force_vblank_sched" , vblank_scheduler_str, offsetof(struct debug_options, force_vblank_scheduler)}, + {"consistent_buffer_age", NULL , offsetof(struct debug_options, consistent_buffer_age)}, }; // clang-format on diff --git a/src/config.h b/src/config.h index 31dc0e4b4a..9fd0d5d20e 100644 --- a/src/config.h +++ b/src/config.h @@ -126,6 +126,14 @@ struct debug_options { /// Useful when being traced under apitrace, to force it to pick up /// updated contents. WARNING, extremely slow. int always_rebind_pixmap; + /// When using damage, replaying an apitrace becomes non-deterministic, because + /// the buffer age we got when we rendered will be different from the buffer age + /// apitrace gets when it replays. When this option is enabled, we saves the + /// contents of each rendered frame, and at the beginning of each render, we + /// restore the content of the back buffer based on the buffer age we get, + /// ensuring no matter what buffer age apitrace gets during replay, the result + /// will be the same. + int consistent_buffer_age; }; extern struct debug_options global_debug_options; diff --git a/src/renderer/renderer.c b/src/renderer/renderer.c index 6e333a8a6a..679a336f64 100644 --- a/src/renderer/renderer.c +++ b/src/renderer/renderer.c @@ -26,6 +26,8 @@ struct renderer { image_handle *monitor_repaint_copy; /// Regions painted over by monitor repaint region_t *monitor_repaint_region; + /// Copy of the entire back buffer + image_handle *back_buffer_copy; /// Current frame index in ring buffer int frame_index; int max_buffer_age; @@ -176,6 +178,13 @@ renderer_set_root_size(struct renderer *r, struct backend_base *backend, ivec2 r if (r->back_image) { backend->ops->release_image(backend, r->back_image); } + if (r->back_buffer_copy) { + for (int i = 0; i < r->max_buffer_age; i++) { + backend->ops->release_image(backend, r->back_buffer_copy[i]); + } + free(r->back_buffer_copy); + r->back_buffer_copy = NULL; + } if (r->monitor_repaint_copy) { for (int i = 0; i < r->max_buffer_age; i++) { backend->ops->release_image(backend, r->monitor_repaint_copy[i]); @@ -454,28 +463,41 @@ static bool renderer_prepare_commands(struct renderer *r, struct backend_base *b return true; } -void renderer_ensure_monitor_repaint_ready(struct renderer *r, struct backend_base *backend) { - if (!r->monitor_repaint_pixel) { - r->monitor_repaint_pixel = backend->ops->new_image( - backend, BACKEND_IMAGE_FORMAT_PIXMAP, (ivec2){1, 1}); - BUG_ON(!r->monitor_repaint_pixel); - backend->ops->clear(backend, r->monitor_repaint_pixel, - (struct color){.alpha = 0.5, .red = 0.5}); +void renderer_ensure_images_ready(struct renderer *r, struct backend_base *backend, + bool monitor_repaint) { + if (monitor_repaint) { + if (!r->monitor_repaint_pixel) { + r->monitor_repaint_pixel = backend->ops->new_image( + backend, BACKEND_IMAGE_FORMAT_PIXMAP, (ivec2){1, 1}); + BUG_ON(!r->monitor_repaint_pixel); + backend->ops->clear(backend, r->monitor_repaint_pixel, + (struct color){.alpha = 0.5, .red = 0.5}); + } + if (!r->monitor_repaint_copy) { + r->monitor_repaint_copy = ccalloc(r->max_buffer_age, image_handle); + for (int i = 0; i < r->max_buffer_age; i++) { + r->monitor_repaint_copy[i] = backend->ops->new_image( + backend, BACKEND_IMAGE_FORMAT_PIXMAP, + (ivec2){.width = r->canvas_size.width, + .height = r->canvas_size.height}); + BUG_ON(!r->monitor_repaint_copy[i]); + } + } + if (!r->monitor_repaint_region) { + r->monitor_repaint_region = ccalloc(r->max_buffer_age, region_t); + for (int i = 0; i < r->max_buffer_age; i++) { + pixman_region32_init(&r->monitor_repaint_region[i]); + } + } } - if (!r->monitor_repaint_copy) { - r->monitor_repaint_copy = ccalloc(r->max_buffer_age, image_handle); + if (global_debug_options.consistent_buffer_age && !r->back_buffer_copy) { + r->back_buffer_copy = ccalloc(r->max_buffer_age, image_handle); for (int i = 0; i < r->max_buffer_age; i++) { - r->monitor_repaint_copy[i] = backend->ops->new_image( + r->back_buffer_copy[i] = backend->ops->new_image( backend, BACKEND_IMAGE_FORMAT_PIXMAP, (ivec2){.width = r->canvas_size.width, .height = r->canvas_size.height}); - BUG_ON(!r->monitor_repaint_copy[i]); - } - } - if (!r->monitor_repaint_region) { - r->monitor_repaint_region = ccalloc(r->max_buffer_age, region_t); - for (int i = 0; i < r->max_buffer_age; i++) { - pixman_region32_init(&r->monitor_repaint_region[i]); + BUG_ON(!r->back_buffer_copy[i]); } } } @@ -506,9 +528,7 @@ bool renderer_render(struct renderer *r, struct backend_base *backend, return false; } - if (monitor_repaint) { - renderer_ensure_monitor_repaint_ready(r, backend); - } + renderer_ensure_images_ready(r, backend, monitor_repaint); command_builder_build(cb, layout, force_blend, blur_frame, inactive_dim_fixed, max_brightness, inactive_dim, monitors, wintype_options); @@ -539,6 +559,16 @@ bool renderer_render(struct renderer *r, struct backend_base *backend, } auto buffer_age = (use_damage || monitor_repaint) ? backend->ops->buffer_age(backend) : 0; + if (buffer_age > 0 && global_debug_options.consistent_buffer_age) { + int past_frame = + (r->frame_index + r->max_buffer_age - buffer_age) % r->max_buffer_age; + region_t region; + pixman_region32_init_rect(®ion, 0, 0, (unsigned)r->canvas_size.width, + (unsigned)r->canvas_size.height); + backend->ops->copy_area(backend, (ivec2){}, backend->ops->back_buffer(backend), + r->back_buffer_copy[past_frame], ®ion); + pixman_region32_fini(®ion); + } if (buffer_age > 0 && (unsigned)buffer_age <= layout_manager_max_buffer_age(lm)) { layout_manager_damage(lm, (unsigned)buffer_age, blur_size, &damage_region); } @@ -604,10 +634,21 @@ bool renderer_render(struct renderer *r, struct backend_base *backend, backend->ops->blit(backend, (ivec2){}, r->back_image, &blit); } + backend->ops->copy_area_quantize(backend, (ivec2){}, + backend->ops->back_buffer(backend), + r->back_image, &damage_region); + + if (global_debug_options.consistent_buffer_age) { + region_t region; + pixman_region32_init_rect(®ion, 0, 0, (unsigned)r->canvas_size.width, + (unsigned)r->canvas_size.height); + backend->ops->copy_area(backend, (ivec2){}, + r->back_buffer_copy[r->frame_index], + backend->ops->back_buffer(backend), ®ion); + pixman_region32_fini(®ion); + } + if (backend->ops->present) { - backend->ops->copy_area_quantize(backend, (ivec2){}, - backend->ops->back_buffer(backend), - r->back_image, &damage_region); backend->ops->present(backend); } From be82ec0ca872a18ab3114058d9497d80eb25f1a0 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Thu, 16 May 2024 19:25:51 +0100 Subject: [PATCH 16/23] types: introduce a type to represent a simple rectangle Signed-off-by: Yuxuan Shui --- src/renderer/command_builder.c | 35 ++++++++++++++++++---------------- src/renderer/damage.c | 24 ++++++++++------------- src/renderer/layout.c | 25 +++++++++++------------- src/renderer/layout.h | 12 ++++-------- src/types.h | 27 ++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 52 deletions(-) diff --git a/src/renderer/command_builder.c b/src/renderer/command_builder.c index 3950c4190b..1840ad50f9 100644 --- a/src/renderer/command_builder.c +++ b/src/renderer/command_builder.c @@ -34,7 +34,8 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, } ivec2 raw_size = {.width = w->widthb, .height = w->heightb}; pixman_region32_copy(&cmd->target_mask, &w->bounding_shape); - pixman_region32_translate(&cmd->target_mask, layer->origin.x, layer->origin.y); + pixman_region32_translate(&cmd->target_mask, layer->window.origin.x, + layer->window.origin.y); if (w->frame_opacity < 1) { pixman_region32_subtract(&cmd->target_mask, &cmd->target_mask, frame_region); } @@ -47,11 +48,11 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, } } if (w->corner_radius > 0) { - win_region_remove_corners(w, layer->origin, &cmd->opaque_region); + win_region_remove_corners(w, layer->window.origin, &cmd->opaque_region); } cmd->op = BACKEND_COMMAND_BLIT; cmd->source = BACKEND_COMMAND_SOURCE_WINDOW; - cmd->origin = layer->origin; + cmd->origin = layer->window.origin; cmd->blit = (struct backend_blit_args){ .border_width = border_width, .target_mask = &cmd->target_mask, @@ -73,7 +74,7 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, pixman_region32_copy(&cmd->target_mask, frame_region); pixman_region32_init(&cmd->opaque_region); cmd->op = BACKEND_COMMAND_BLIT; - cmd->origin = layer->origin; + cmd->origin = layer->window.origin; cmd->source = BACKEND_COMMAND_SOURCE_WINDOW; cmd->blit = cmd[1].blit; cmd->blit.target_mask = &cmd->target_mask; @@ -93,13 +94,13 @@ command_for_shadow(struct layer *layer, struct backend_command *cmd, return 0; } cmd->op = BACKEND_COMMAND_BLIT; - cmd->origin = layer->shadow_origin; + cmd->origin = layer->shadow.origin; cmd->source = BACKEND_COMMAND_SOURCE_SHADOW; pixman_region32_clear(&cmd->target_mask); pixman_region32_union_rect(&cmd->target_mask, &cmd->target_mask, - layer->shadow_origin.x, layer->shadow_origin.y, - (unsigned)layer->shadow_size.width, - (unsigned)layer->shadow_size.height); + layer->shadow.origin.x, layer->shadow.origin.y, + (unsigned)layer->shadow.size.width, + (unsigned)layer->shadow.size.height); log_trace("Calculate shadow for %#010x (%s)", w->base.id, w->name); log_region(TRACE, &cmd->target_mask); if (!wintype_options[w->window_type].full_shadow) { @@ -133,14 +134,15 @@ command_for_shadow(struct layer *layer, struct backend_command *cmd, if (w->corner_radius > 0) { cmd->source_mask.corner_radius = w->corner_radius; cmd->source_mask.inverted = true; - cmd->source_mask.origin = ivec2_sub(layer->origin, layer->shadow_origin); + cmd->source_mask.origin = + ivec2_sub(layer->window.origin, layer->shadow.origin); } cmd->blit = (struct backend_blit_args){ .opacity = layer->shadow_opacity, .max_brightness = 1, .source_mask = w->corner_radius > 0 ? &cmd->source_mask : NULL, .scale = SCALE_IDENTITY, - .effective_size = layer->shadow_size, + .effective_size = layer->shadow.size, .target_mask = &cmd->target_mask, }; pixman_region32_init(&cmd->opaque_region); @@ -157,8 +159,8 @@ command_for_blur(struct layer *layer, struct backend_command *cmd, } if (force_blend || mode == WMODE_TRANS || layer->opacity < 1.0) { pixman_region32_copy(&cmd->target_mask, &w->bounding_shape); - pixman_region32_translate(&cmd->target_mask, layer->origin.x, - layer->origin.y); + pixman_region32_translate(&cmd->target_mask, layer->window.origin.x, + layer->window.origin.y); } else if (blur_frame && mode == WMODE_FRAME_TRANS) { pixman_region32_copy(&cmd->target_mask, frame_region); } else { @@ -167,7 +169,7 @@ command_for_blur(struct layer *layer, struct backend_command *cmd, cmd->op = BACKEND_COMMAND_BLUR; cmd->origin = (ivec2){}; if (w->corner_radius > 0) { - cmd->source_mask.origin = (ivec2){.x = layer->origin.x, .y = layer->origin.y}; + cmd->source_mask.origin = layer->window.origin; cmd->source_mask.corner_radius = w->corner_radius; cmd->source_mask.inverted = false; } @@ -204,8 +206,8 @@ command_builder_apply_transparent_clipping(struct layout *layout, region_t *scra } else if (mode == WMODE_FRAME_TRANS) { win_get_region_frame_local(win, &tmp); } - pixman_region32_translate(&tmp, layer->origin.x, - layer->origin.y); + pixman_region32_translate(&tmp, layer->window.origin.x, + layer->window.origin.y); pixman_region32_union(scratch_region, scratch_region, &tmp); pixman_region32_fini(&tmp); } @@ -372,7 +374,8 @@ void command_builder_build(struct command_builder *cb, struct layout *layout, bo auto layer = &layout->layers[i]; auto last = cmd; auto frame_region = win_get_region_frame_local_by_val(layer->win); - pixman_region32_translate(&frame_region, layer->origin.x, layer->origin.y); + pixman_region32_translate(&frame_region, layer->window.origin.x, + layer->window.origin.y); // Add window body cmd -= commands_for_window_body(layer, cmd, &frame_region, inactive_dim_fixed, diff --git a/src/renderer/damage.c b/src/renderer/damage.c index dd17efd693..78b30144fa 100644 --- a/src/renderer/damage.c +++ b/src/renderer/damage.c @@ -17,18 +17,12 @@ static inline bool attr_unused layer_key_eq(const struct layer_key *a, static bool layer_compare(const struct layer *past_layer, const struct backend_command *past_layer_cmd, const struct layer *curr_layer, const struct backend_command *curr_layer_cmd) { - if (past_layer->origin.x != curr_layer->origin.x || - past_layer->origin.y != curr_layer->origin.y || - past_layer->size.width != curr_layer->size.width || - past_layer->size.height != curr_layer->size.height) { + if (!ibox_eq(past_layer->window, curr_layer->window)) { // Window moved or size changed return false; } - if (past_layer->shadow_origin.x != curr_layer->shadow_origin.x || - past_layer->shadow_origin.y != curr_layer->shadow_origin.y || - past_layer->shadow_size.width != curr_layer->shadow_size.width || - past_layer->shadow_size.height != curr_layer->shadow_size.height) { + if (!ibox_eq(past_layer->shadow, curr_layer->shadow)) { // Shadow moved or size changed return false; } @@ -156,12 +150,14 @@ void layout_manager_damage(struct layout_manager *lm, unsigned buffer_age, log_trace("Layout[%d]: ", -l); auto layout = layout_manager_layout(lm, l); for (unsigned i = 0; i < layout->len; i++) { - log_trace( - "\t%#010x %dx%d+%dx%d (prev %d, next %d)", - layout->layers[i].key.window, layout->layers[i].size.width, - layout->layers[i].size.height, - layout->layers[i].origin.x, layout->layers[i].origin.y, - layout->layers[i].prev_rank, layout->layers[i].next_rank); + log_trace("\t%#010x %dx%d+%dx%d (prev %d, next %d)", + layout->layers[i].key.window, + layout->layers[i].window.size.width, + layout->layers[i].window.size.height, + layout->layers[i].window.origin.x, + layout->layers[i].window.origin.y, + layout->layers[i].prev_rank, + layout->layers[i].next_rank); } } } diff --git a/src/renderer/layout.c b/src/renderer/layout.c index 815d59db73..7095deea6a 100644 --- a/src/renderer/layout.c +++ b/src/renderer/layout.c @@ -42,28 +42,25 @@ static bool layer_from_window(struct layer *out_layer, struct managed_win *w, iv goto out; } - out_layer->origin = + out_layer->window.origin = vec2_as((vec2){.x = w->g.x + win_animatable_get(w, WIN_SCRIPT_OFFSET_X), .y = w->g.y + win_animatable_get(w, WIN_SCRIPT_OFFSET_Y)}); - out_layer->size = (ivec2){.width = w->widthb, .height = w->heightb}; + out_layer->window.size = (ivec2){.width = w->widthb, .height = w->heightb}; if (w->shadow) { - out_layer->shadow_origin = + out_layer->shadow.origin = vec2_as((vec2){.x = w->g.x + w->shadow_dx + win_animatable_get(w, WIN_SCRIPT_SHADOW_OFFSET_X), .y = w->g.y + w->shadow_dy + win_animatable_get(w, WIN_SCRIPT_SHADOW_OFFSET_Y)}); - out_layer->shadow_size = + out_layer->shadow.size = (ivec2){.width = w->shadow_width, .height = w->shadow_height}; } else { - out_layer->shadow_origin = (ivec2){}; - out_layer->shadow_size = (ivec2){}; + out_layer->shadow.origin = (ivec2){}; + out_layer->shadow.size = (ivec2){}; } - if (out_layer->size.width <= 0 || out_layer->size.height <= 0) { - goto out; - } - if (out_layer->size.height + out_layer->origin.y <= 0 || - out_layer->size.width + out_layer->origin.x <= 0 || - out_layer->origin.y >= size.height || out_layer->origin.x >= size.width) { + + struct ibox screen = {.origin = {0, 0}, .size = size}; + if (!ibox_overlap(out_layer->window, screen)) { goto out; } @@ -76,8 +73,8 @@ static bool layer_from_window(struct layer *out_layer, struct managed_win *w, iv } pixman_region32_copy(&out_layer->damaged, &w->damaged); - pixman_region32_translate(&out_layer->damaged, out_layer->origin.x, - out_layer->origin.y); + pixman_region32_translate(&out_layer->damaged, out_layer->window.origin.x, + out_layer->window.origin.y); // TODO(yshui) Is there a better way to handle shaped windows? Shaped windows can // have a very large number of rectangles in their shape, we don't want to handle // that and slow ourselves down. so we treat them as transparent and just use diff --git a/src/renderer/layout.h b/src/renderer/layout.h index a58849e901..9c5d76822d 100644 --- a/src/renderer/layout.h +++ b/src/renderer/layout.h @@ -29,14 +29,10 @@ struct layer { struct managed_win *win; /// Damaged region of this layer, in screen coordinates region_t damaged; - /// Origin (the top left outmost corner) of the window in screen coordinates - ivec2 origin; - /// Size of the window - ivec2 size; - /// Origin of the shadow in screen coordinates - ivec2 shadow_origin; - /// Size of the shadow - ivec2 shadow_size; + /// Window rectangle in screen coordinates. + struct ibox window; + /// Shadow rectangle in screen coordinates. + struct ibox shadow; /// Opacity of this window float opacity; /// Opacity of the background blur of this window diff --git a/src/types.h b/src/types.h index dd88ff6d1d..68f21ca667 100644 --- a/src/types.h +++ b/src/types.h @@ -55,6 +55,11 @@ typedef struct ivec2 { }; } ivec2; +struct ibox { + ivec2 origin; + ivec2 size; +}; + static inline ivec2 ivec2_add(ivec2 a, ivec2 b) { return (ivec2){ .x = a.x + b.x, @@ -92,5 +97,27 @@ static inline bool vec2_eq(vec2 a, vec2 b) { return a.x == b.x && a.y == b.y; } +/// Check if two boxes have a non-zero intersection area. +static inline bool ibox_overlap(struct ibox a, struct ibox b) { + if (a.size.width <= 0 || a.size.height <= 0 || b.size.width <= 0 || b.size.height <= 0) { + return false; + } + if (a.origin.x <= INT_MAX - a.size.width && a.origin.y <= INT_MAX - a.size.height && + (a.origin.x + a.size.width <= b.origin.x || + a.origin.y + a.size.height <= b.origin.y)) { + return false; + } + if (b.origin.x <= INT_MAX - b.size.width && b.origin.y <= INT_MAX - b.size.height && + (b.origin.x + b.size.width <= a.origin.x || + b.origin.y + b.size.height <= a.origin.y)) { + return false; + } + return true; +} + +static inline bool ibox_eq(struct ibox a, struct ibox b) { + return ivec2_eq(a.origin, b.origin) && ivec2_eq(a.size, b.size); +} + #define MARGIN_INIT \ { 0, 0, 0, 0 } From 659f009828480d2c35a5b7643444524844015896 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Thu, 16 May 2024 19:38:04 +0100 Subject: [PATCH 17/23] win: make window and shadow scales animatable Signed-off-by: Yuxuan Shui --- src/region.h | 23 +++++++++++++++++++++++ src/renderer/command_builder.c | 12 ++++++++---- src/renderer/damage.c | 8 ++++++++ src/renderer/layout.c | 15 +++++++++++++-- src/renderer/layout.h | 6 ++++++ src/types.h | 7 +++++++ src/win.c | 4 ++++ src/win.h | 4 ++++ src/win_defs.h | 10 +++++++++- 9 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/region.h b/src/region.h index 96b9780e91..0e641890ab 100644 --- a/src/region.h +++ b/src/region.h @@ -154,6 +154,29 @@ static inline void region_intersect(region_t *region, ivec2 origin, const region pixman_region32_translate(region, origin.x, origin.y); } +/// Scale the `region` by `scale`. The origin of scaling is `origin`. +static inline void region_scale(region_t *region, ivec2 origin, vec2 scale) { + static const vec2 SCALE_IDENTITY = {.x = 1.0, .y = 1.0}; + if (vec2_eq(scale, SCALE_IDENTITY)) { + return; + } + + int n; + region_t tmp = *region; + auto r = pixman_region32_rectangles(&tmp, &n); + for (int i = 0; i < n; i++) { + r[i].x1 = (int32_t)((r[i].x1 - origin.x) * scale.x + origin.x); + r[i].y1 = (int32_t)((r[i].y1 - origin.y) * scale.y + origin.y); + r[i].x2 = (int32_t)((r[i].x2 - origin.x) * scale.x + origin.x); + r[i].y2 = (int32_t)((r[i].y2 - origin.y) * scale.y + origin.y); + } + + // Manipulating the rectangles could break assumptions made internally by pixman, + // so we recreate the region with the rectangles to let pixman fix them. + pixman_region32_init_rects(region, r, n); + pixman_region32_fini(&tmp); +} + /// Calculate the symmetric difference of `region1`, and `region2`, and union the result /// into `result`. The two input regions has to be in the same coordinate space. /// diff --git a/src/renderer/command_builder.c b/src/renderer/command_builder.c index 1840ad50f9..426597083b 100644 --- a/src/renderer/command_builder.c +++ b/src/renderer/command_builder.c @@ -32,7 +32,6 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, border_width = min3(w->frame_extents.left, w->frame_extents.right, w->frame_extents.bottom); } - ivec2 raw_size = {.width = w->widthb, .height = w->heightb}; pixman_region32_copy(&cmd->target_mask, &w->bounding_shape); pixman_region32_translate(&cmd->target_mask, layer->window.origin.x, layer->window.origin.y); @@ -50,6 +49,8 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, if (w->corner_radius > 0) { win_region_remove_corners(w, layer->window.origin, &cmd->opaque_region); } + region_scale(&cmd->target_mask, layer->window.origin, layer->scale); + region_scale(&cmd->opaque_region, layer->window.origin, layer->scale); cmd->op = BACKEND_COMMAND_BLIT; cmd->source = BACKEND_COMMAND_SOURCE_WINDOW; cmd->origin = layer->window.origin; @@ -59,8 +60,8 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, .corner_radius = w->corner_radius, .opacity = layer->opacity, .dim = dim, - .scale = SCALE_IDENTITY, - .effective_size = raw_size, + .scale = layer->scale, + .effective_size = layer->window.size, .shader = w->fg_shader ? w->fg_shader->backend_shader : NULL, .color_inverted = w->invert_color, .source_mask = NULL, @@ -72,6 +73,7 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, cmd -= 1; pixman_region32_copy(&cmd->target_mask, frame_region); + region_scale(&cmd->target_mask, cmd->origin, layer->scale); pixman_region32_init(&cmd->opaque_region); cmd->op = BACKEND_COMMAND_BLIT; cmd->origin = layer->window.origin; @@ -137,11 +139,12 @@ command_for_shadow(struct layer *layer, struct backend_command *cmd, cmd->source_mask.origin = ivec2_sub(layer->window.origin, layer->shadow.origin); } + cmd->blit = (struct backend_blit_args){ .opacity = layer->shadow_opacity, .max_brightness = 1, .source_mask = w->corner_radius > 0 ? &cmd->source_mask : NULL, - .scale = SCALE_IDENTITY, + .scale = layer->shadow_scale, .effective_size = layer->shadow.size, .target_mask = &cmd->target_mask, }; @@ -166,6 +169,7 @@ command_for_blur(struct layer *layer, struct backend_command *cmd, } else { return 0; } + region_scale(&cmd->target_mask, layer->window.origin, layer->scale); cmd->op = BACKEND_COMMAND_BLUR; cmd->origin = (ivec2){}; if (w->corner_radius > 0) { diff --git a/src/renderer/damage.c b/src/renderer/damage.c index 78b30144fa..073aaeb835 100644 --- a/src/renderer/damage.c +++ b/src/renderer/damage.c @@ -22,6 +22,13 @@ layer_compare(const struct layer *past_layer, const struct backend_command *past return false; } + // TODO(yshui) consider window body and shadow separately. + if (!vec2_eq(past_layer->scale, curr_layer->scale) || + !vec2_eq(past_layer->shadow_scale, curr_layer->shadow_scale)) { + // Window or shadow scale changed + return false; + } + if (!ibox_eq(past_layer->shadow, curr_layer->shadow)) { // Shadow moved or size changed return false; @@ -93,6 +100,7 @@ command_blit_damage(region_t *damage, region_t *scratch_region, struct backend_c if (cmd1->source == BACKEND_COMMAND_SOURCE_WINDOW) { layout_manager_collect_window_damage(lm, layer_index, buffer_age, scratch_region); + region_scale(scratch_region, cmd2->origin, cmd2->blit.scale); pixman_region32_intersect(scratch_region, scratch_region, &cmd1->target_mask); pixman_region32_intersect(scratch_region, scratch_region, &cmd2->target_mask); pixman_region32_union(damage, damage, scratch_region); diff --git a/src/renderer/layout.c b/src/renderer/layout.c index 7095deea6a..da25e4b509 100644 --- a/src/renderer/layout.c +++ b/src/renderer/layout.c @@ -42,21 +42,32 @@ static bool layer_from_window(struct layer *out_layer, struct managed_win *w, iv goto out; } + out_layer->scale = (vec2){ + .x = win_animatable_get(w, WIN_SCRIPT_SCALE_X), + .y = win_animatable_get(w, WIN_SCRIPT_SCALE_Y), + }; out_layer->window.origin = vec2_as((vec2){.x = w->g.x + win_animatable_get(w, WIN_SCRIPT_OFFSET_X), .y = w->g.y + win_animatable_get(w, WIN_SCRIPT_OFFSET_Y)}); - out_layer->window.size = (ivec2){.width = w->widthb, .height = w->heightb}; + out_layer->window.size = vec2_as((vec2){.width = w->widthb * out_layer->scale.x, + .height = w->heightb * out_layer->scale.y}); if (w->shadow) { + out_layer->shadow_scale = (vec2){ + .x = win_animatable_get(w, WIN_SCRIPT_SHADOW_SCALE_X), + .y = win_animatable_get(w, WIN_SCRIPT_SHADOW_SCALE_Y), + }; out_layer->shadow.origin = vec2_as((vec2){.x = w->g.x + w->shadow_dx + win_animatable_get(w, WIN_SCRIPT_SHADOW_OFFSET_X), .y = w->g.y + w->shadow_dy + win_animatable_get(w, WIN_SCRIPT_SHADOW_OFFSET_Y)}); out_layer->shadow.size = - (ivec2){.width = w->shadow_width, .height = w->shadow_height}; + vec2_as((vec2){.width = w->shadow_width * out_layer->shadow_scale.x, + .height = w->shadow_height * out_layer->shadow_scale.y}); } else { out_layer->shadow.origin = (ivec2){}; out_layer->shadow.size = (ivec2){}; + out_layer->shadow_scale = SCALE_IDENTITY; } struct ibox screen = {.origin = {0, 0}, .size = size}; diff --git a/src/renderer/layout.h b/src/renderer/layout.h index 9c5d76822d..c2ea02680a 100644 --- a/src/renderer/layout.h +++ b/src/renderer/layout.h @@ -33,6 +33,12 @@ struct layer { struct ibox window; /// Shadow rectangle in screen coordinates. struct ibox shadow; + /// Scale of the window. The origin of scaling is the top left corner of the + /// window. + vec2 scale; + /// Scale of the shadow. The origin of scaling is the top left corner of the + /// shadow. + vec2 shadow_scale; /// Opacity of this window float opacity; /// Opacity of the background blur of this window diff --git a/src/types.h b/src/types.h index 68f21ca667..234c498f60 100644 --- a/src/types.h +++ b/src/types.h @@ -97,6 +97,13 @@ static inline bool vec2_eq(vec2 a, vec2 b) { return a.x == b.x && a.y == b.y; } +static inline vec2 vec2_scale(vec2 a, vec2 scale) { + return (vec2){ + .x = a.x * scale.x, + .y = a.y * scale.y, + }; +} + /// Check if two boxes have a non-zero intersection area. static inline bool ibox_overlap(struct ibox a, struct ibox b) { if (a.size.width <= 0 || a.size.height <= 0 || b.size.width <= 0 || b.size.height <= 0) { diff --git a/src/win.c b/src/win.c index 0aa012249e..2e80c57ef1 100644 --- a/src/win.c +++ b/src/win.c @@ -2213,6 +2213,10 @@ double win_animatable_get(const struct managed_win *w, enum win_script_output ou case WIN_SCRIPT_OFFSET_Y: case WIN_SCRIPT_SHADOW_OFFSET_X: case WIN_SCRIPT_SHADOW_OFFSET_Y: return 0; + case WIN_SCRIPT_SCALE_X: + case WIN_SCRIPT_SCALE_Y: + case WIN_SCRIPT_SHADOW_SCALE_X: + case WIN_SCRIPT_SHADOW_SCALE_Y: return 1; } unreachable(); } diff --git a/src/win.h b/src/win.h index 5343185427..4c1b630ba4 100644 --- a/src/win.h +++ b/src/win.h @@ -336,6 +336,10 @@ static const struct script_output_info win_script_outputs[] = { [WIN_SCRIPT_OPACITY] = {"opacity"}, [WIN_SCRIPT_BLUR_OPACITY] = {"blur-opacity"}, [WIN_SCRIPT_SHADOW_OPACITY] = {"shadow-opacity"}, + [WIN_SCRIPT_SCALE_X] = {"scale-x"}, + [WIN_SCRIPT_SCALE_Y] = {"scale-y"}, + [WIN_SCRIPT_SHADOW_SCALE_X] = {"shadow-scale-x"}, + [WIN_SCRIPT_SHADOW_SCALE_Y] = {"shadow-scale-y"}, [NUM_OF_WIN_SCRIPT_OUTPUTS] = {NULL}, }; diff --git a/src/win_defs.h b/src/win_defs.h index 04455de2d2..96aa3fba65 100644 --- a/src/win_defs.h +++ b/src/win_defs.h @@ -82,5 +82,13 @@ enum win_script_output { WIN_SCRIPT_BLUR_OPACITY, /// Opacity of the shadow. WIN_SCRIPT_SHADOW_OPACITY, + /// Horizontal scale + WIN_SCRIPT_SCALE_X, + /// Vertical scale + WIN_SCRIPT_SCALE_Y, + /// Horizontal scale of the shadow + WIN_SCRIPT_SHADOW_SCALE_X, + /// Vertical scale of the shadow + WIN_SCRIPT_SHADOW_SCALE_Y, }; -#define NUM_OF_WIN_SCRIPT_OUTPUTS (WIN_SCRIPT_SHADOW_OPACITY + 1) +#define NUM_OF_WIN_SCRIPT_OUTPUTS (WIN_SCRIPT_SHADOW_SCALE_Y + 1) From cf98d95d938bf7b9510bcffb3d79b14cd2c4aa88 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Thu, 16 May 2024 20:07:25 +0100 Subject: [PATCH 18/23] types: add a scoped region_t Signed-off-by: Yuxuan Shui --- src/region.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/region.h b/src/region.h index 0e641890ab..9b0dda33b5 100644 --- a/src/region.h +++ b/src/region.h @@ -17,6 +17,14 @@ typedef pixman_box32_t rect_t; RC_TYPE(region_t, rc_region, pixman_region32_init, pixman_region32_fini, static inline) +static inline void region_free(region_t *region) { + if (region) { + pixman_region32_fini(region); + } +} + +#define scoped_region_t cleanup(region_free) region_t + static inline void dump_region(const region_t *x) { if (log_get_level_tls() > LOG_LEVEL_TRACE) { return; From 811681a69fc52be8fb600e17fc51db404cdb6407 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Thu, 16 May 2024 20:08:05 +0100 Subject: [PATCH 19/23] win: support animatable cropping Signed-off-by: Yuxuan Shui --- src/region.h | 8 ++++++++ src/renderer/command_builder.c | 11 +++++++++++ src/renderer/layout.c | 10 +++++++++- src/renderer/layout.h | 7 ++----- src/win.c | 4 ++++ src/win.h | 4 ++++ src/win_defs.h | 10 +++++++++- 7 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/region.h b/src/region.h index 9b0dda33b5..cdd092dd62 100644 --- a/src/region.h +++ b/src/region.h @@ -200,3 +200,11 @@ region_symmetric_difference_local(region_t *result, region_t *scratch, pixman_region32_subtract(scratch, scratch, region1); pixman_region32_union(result, result, scratch); } + +static inline region_t region_from_box(struct ibox a) { + region_t ret; + unsigned width = (unsigned)(min2(INT_MAX - a.origin.x, a.size.width)), + height = (unsigned)(min2(INT_MAX - a.origin.y, a.size.height)); + pixman_region32_init_rect(&ret, a.origin.x, a.origin.y, width, height); + return ret; +} diff --git a/src/renderer/command_builder.c b/src/renderer/command_builder.c index 426597083b..d0879030a1 100644 --- a/src/renderer/command_builder.c +++ b/src/renderer/command_builder.c @@ -18,6 +18,7 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, const region_t *frame_region, bool inactive_dim_fixed, double inactive_dim, double max_brightness) { auto w = layer->win; + scoped_region_t crop = region_from_box(layer->crop); auto mode = win_calc_mode_raw(layer->win); int border_width = w->g.border_width; double dim = 0; @@ -51,6 +52,8 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, } region_scale(&cmd->target_mask, layer->window.origin, layer->scale); region_scale(&cmd->opaque_region, layer->window.origin, layer->scale); + pixman_region32_intersect(&cmd->target_mask, &cmd->target_mask, &crop); + pixman_region32_intersect(&cmd->opaque_region, &cmd->opaque_region, &crop); cmd->op = BACKEND_COMMAND_BLIT; cmd->source = BACKEND_COMMAND_SOURCE_WINDOW; cmd->origin = layer->window.origin; @@ -74,6 +77,7 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, pixman_region32_copy(&cmd->target_mask, frame_region); region_scale(&cmd->target_mask, cmd->origin, layer->scale); + pixman_region32_intersect(&cmd->target_mask, &cmd->target_mask, &crop); pixman_region32_init(&cmd->opaque_region); cmd->op = BACKEND_COMMAND_BLIT; cmd->origin = layer->window.origin; @@ -140,6 +144,9 @@ command_for_shadow(struct layer *layer, struct backend_command *cmd, ivec2_sub(layer->window.origin, layer->shadow.origin); } + scoped_region_t crop = region_from_box(layer->crop); + pixman_region32_intersect(&cmd->target_mask, &cmd->target_mask, &crop); + cmd->blit = (struct backend_blit_args){ .opacity = layer->shadow_opacity, .max_brightness = 1, @@ -170,6 +177,10 @@ command_for_blur(struct layer *layer, struct backend_command *cmd, return 0; } region_scale(&cmd->target_mask, layer->window.origin, layer->scale); + + scoped_region_t crop = region_from_box(layer->crop); + pixman_region32_intersect(&cmd->target_mask, &cmd->target_mask, &crop); + cmd->op = BACKEND_COMMAND_BLUR; cmd->origin = (ivec2){}; if (w->corner_radius > 0) { diff --git a/src/renderer/layout.c b/src/renderer/layout.c index da25e4b509..1083935b46 100644 --- a/src/renderer/layout.c +++ b/src/renderer/layout.c @@ -51,6 +51,14 @@ static bool layer_from_window(struct layer *out_layer, struct managed_win *w, iv .y = w->g.y + win_animatable_get(w, WIN_SCRIPT_OFFSET_Y)}); out_layer->window.size = vec2_as((vec2){.width = w->widthb * out_layer->scale.x, .height = w->heightb * out_layer->scale.y}); + out_layer->crop.origin = vec2_as((vec2){ + .x = win_animatable_get(w, WIN_SCRIPT_CROP_X), + .y = win_animatable_get(w, WIN_SCRIPT_CROP_Y), + }); + out_layer->crop.size = vec2_as((vec2){ + .x = win_animatable_get(w, WIN_SCRIPT_CROP_WIDTH), + .y = win_animatable_get(w, WIN_SCRIPT_CROP_HEIGHT), + }); if (w->shadow) { out_layer->shadow_scale = (vec2){ .x = win_animatable_get(w, WIN_SCRIPT_SHADOW_SCALE_X), @@ -71,7 +79,7 @@ static bool layer_from_window(struct layer *out_layer, struct managed_win *w, iv } struct ibox screen = {.origin = {0, 0}, .size = size}; - if (!ibox_overlap(out_layer->window, screen)) { + if (!ibox_overlap(out_layer->window, screen) || !ibox_overlap(out_layer->crop, screen)) { goto out; } diff --git a/src/renderer/layout.h b/src/renderer/layout.h index c2ea02680a..c0295cd679 100644 --- a/src/renderer/layout.h +++ b/src/renderer/layout.h @@ -45,6 +45,8 @@ struct layer { float blur_opacity; /// Opacity of this window's shadow float shadow_opacity; + /// Crop the content of this layer to this box, in screen coordinates. + struct ibox crop; /// How many commands are needed to render this layer unsigned number_of_commands; @@ -65,11 +67,6 @@ struct layer { // things like blur-background-frame // region_t opaque_region; // region_t blur_region; - - // TODO(yshui) support cropping - /// x and y offset for cropping. Anything to the top or - /// left of the crop point will be cropped out. - // uint32_t crop_x, crop_y; }; /// Layout of windows at a specific frame diff --git a/src/win.c b/src/win.c index 2e80c57ef1..85c5dd0ac8 100644 --- a/src/win.c +++ b/src/win.c @@ -2209,6 +2209,8 @@ double win_animatable_get(const struct managed_win *w, enum win_script_output ou case WIN_SCRIPT_BLUR_OPACITY: return w->state == WSTATE_MAPPED ? 1.0 : 0.0; case WIN_SCRIPT_OPACITY: case WIN_SCRIPT_SHADOW_OPACITY: return w->opacity; + case WIN_SCRIPT_CROP_X: + case WIN_SCRIPT_CROP_Y: case WIN_SCRIPT_OFFSET_X: case WIN_SCRIPT_OFFSET_Y: case WIN_SCRIPT_SHADOW_OFFSET_X: @@ -2217,6 +2219,8 @@ double win_animatable_get(const struct managed_win *w, enum win_script_output ou case WIN_SCRIPT_SCALE_Y: case WIN_SCRIPT_SHADOW_SCALE_X: case WIN_SCRIPT_SHADOW_SCALE_Y: return 1; + case WIN_SCRIPT_CROP_WIDTH: + case WIN_SCRIPT_CROP_HEIGHT: return INFINITY; } unreachable(); } diff --git a/src/win.h b/src/win.h index 4c1b630ba4..cc0c3be614 100644 --- a/src/win.h +++ b/src/win.h @@ -340,6 +340,10 @@ static const struct script_output_info win_script_outputs[] = { [WIN_SCRIPT_SCALE_Y] = {"scale-y"}, [WIN_SCRIPT_SHADOW_SCALE_X] = {"shadow-scale-x"}, [WIN_SCRIPT_SHADOW_SCALE_Y] = {"shadow-scale-y"}, + [WIN_SCRIPT_CROP_X] = {"crop-x"}, + [WIN_SCRIPT_CROP_Y] = {"crop-y"}, + [WIN_SCRIPT_CROP_WIDTH] = {"crop-width"}, + [WIN_SCRIPT_CROP_HEIGHT] = {"crop-height"}, [NUM_OF_WIN_SCRIPT_OUTPUTS] = {NULL}, }; diff --git a/src/win_defs.h b/src/win_defs.h index 96aa3fba65..2f46ef5b21 100644 --- a/src/win_defs.h +++ b/src/win_defs.h @@ -90,5 +90,13 @@ enum win_script_output { WIN_SCRIPT_SHADOW_SCALE_X, /// Vertical scale of the shadow WIN_SCRIPT_SHADOW_SCALE_Y, + /// X coordinate of the origin of the crop box + WIN_SCRIPT_CROP_X, + /// Y coordinate of the origin of the crop box + WIN_SCRIPT_CROP_Y, + /// Width of the crop box + WIN_SCRIPT_CROP_WIDTH, + /// Height of the crop box + WIN_SCRIPT_CROP_HEIGHT, }; -#define NUM_OF_WIN_SCRIPT_OUTPUTS (WIN_SCRIPT_SHADOW_SCALE_Y + 1) +#define NUM_OF_WIN_SCRIPT_OUTPUTS (WIN_SCRIPT_CROP_HEIGHT + 1) From bf6fc478c07c1f16763ac398c432bf04fec6684b Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Fri, 17 May 2024 11:04:12 +0100 Subject: [PATCH 20/23] backend/xrender: implement support for scale Signed-off-by: Yuxuan Shui --- src/backend/xrender/xrender.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/backend/xrender/xrender.c b/src/backend/xrender/xrender.c index 11decd2e9f..ffe936c414 100644 --- a/src/backend/xrender/xrender.c +++ b/src/backend/xrender/xrender.c @@ -103,6 +103,16 @@ struct xrender_rounded_rectangle_cache { int radius; }; +static void +set_picture_scale(struct x_connection *c, xcb_render_picture_t picture, vec2 scale) { + xcb_render_transform_t transform = { + .matrix11 = DOUBLE_TO_XFIXED(1.0 / scale.x), + .matrix22 = DOUBLE_TO_XFIXED(1.0 / scale.y), + .matrix33 = DOUBLE_TO_XFIXED(1.0), + }; + set_cant_fail_cookie(c, xcb_render_set_picture_transform(c->c, picture, transform)); +} + /// Make a picture of size width x height, which has a rounded rectangle of corner_radius /// rendered in it. struct xrender_rounded_rectangle_cache * @@ -316,6 +326,9 @@ static bool xrender_blit(struct backend_base *base, ivec2 origin, inner->size.height, (int)args->corner_radius); } } + + set_picture_scale(xd->base.c, mask_pict, args->scale); + if (((args->color_inverted || args->dim != 0) && has_alpha) || args->corner_radius != 0) { // Apply image properties using a temporary image, because the source @@ -384,6 +397,8 @@ static bool xrender_blit(struct backend_base *base, ivec2 origin, tmp_pict, 0, 0, 0, 0, 0, 0, tmpw, tmph); } + set_picture_scale(xd->base.c, tmp_pict, args->scale); + xcb_render_composite(xd->base.c->c, XCB_RENDER_PICT_OP_OVER, tmp_pict, mask_pict, target->pict, 0, 0, mask_pict_dst_x, mask_pict_dst_y, to_i16_checked(origin.x), @@ -392,6 +407,8 @@ static bool xrender_blit(struct backend_base *base, ivec2 origin, } else { uint8_t op = (has_alpha ? XCB_RENDER_PICT_OP_OVER : XCB_RENDER_PICT_OP_SRC); + set_picture_scale(xd->base.c, inner->pict, args->scale); + xcb_render_composite(xd->base.c->c, op, inner->pict, mask_pict, target->pict, 0, 0, mask_pict_dst_x, mask_pict_dst_y, to_i16_checked(origin.x), to_i16_checked(origin.y), From 2899c789371e8cfd321a19762a1667d9dfb195f5 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Fri, 17 May 2024 13:09:41 +0100 Subject: [PATCH 21/23] win: add animation suppressions setting This can be used to prevent another animation from interrupting the current running animation. Signed-off-by: Yuxuan Shui --- src/config.h | 3 ++ src/config_libconfig.c | 65 ++++++++++++++++++++++++++++++++++++------ src/win.c | 8 ++++++ src/win.h | 1 + 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/config.h b/src/config.h index 9fd0d5d20e..2297428ae4 100644 --- a/src/config.h +++ b/src/config.h @@ -110,6 +110,9 @@ static const char *animation_trigger_names[] attr_unused = { struct script; struct win_script { int output_indices[NUM_OF_WIN_SCRIPT_OUTPUTS]; + /// A running animation can be configured to prevent other animations from + /// starting. + uint64_t suppressions; struct script *script; }; diff --git a/src/config_libconfig.c b/src/config_libconfig.c index 1250c6c772..8867635a71 100644 --- a/src/config_libconfig.c +++ b/src/config_libconfig.c @@ -250,9 +250,10 @@ compile_win_script(config_setting_t *setting, int *output_indices, char **err) { return script; } -static bool set_animation(struct win_script *animations, - const enum animation_trigger *triggers, int number_of_triggers, - struct script *script, const int *output_indices, unsigned line) { +static bool +set_animation(struct win_script *animations, const enum animation_trigger *triggers, + int number_of_triggers, struct script *script, const int *output_indices, + uint64_t suppressions, unsigned line) { bool needed = false; for (int i = 0; i < number_of_triggers; i++) { if (triggers[i] == ANIMATION_TRIGGER_INVALID) { @@ -268,6 +269,7 @@ static bool set_animation(struct win_script *animations, memcpy(animations[triggers[i]].output_indices, output_indices, sizeof(int[NUM_OF_WIN_SCRIPT_OUTPUTS])); animations[triggers[i]].script = script; + animations[triggers[i]].suppressions = suppressions; needed = true; } return needed; @@ -317,6 +319,52 @@ parse_animation_one(struct win_script *animations, config_setting_t *setting) { // script parser shouldn't see this. config_setting_remove(setting, "triggers"); + uint64_t suppressions = 0; + auto suppressions_setting = config_setting_lookup(setting, "suppressions"); + if (suppressions_setting != NULL) { + auto single_suppression = config_setting_get_string(suppressions_setting); + if (!config_setting_is_list(suppressions_setting) && + !config_setting_is_array(suppressions_setting) && + single_suppression == NULL) { + log_error("The \"suppressions\" option must either be a string, " + "a list, or an array, but is none of those at line %d", + config_setting_source_line(suppressions_setting)); + return NULL; + } + if (single_suppression != NULL) { + auto suppression = parse_animation_trigger(single_suppression); + if (suppression == ANIMATION_TRIGGER_INVALID) { + log_error("Invalid suppression defined at line %d", + config_setting_source_line(suppressions_setting)); + return NULL; + } + suppressions = 1 << suppression; + } else { + auto len = config_setting_length(suppressions_setting); + for (int i = 0; i < len; i++) { + auto suppression_str = + config_setting_get_string_elem(suppressions_setting, i); + if (suppression_str == NULL) { + log_error( + "The \"suppressions\" option must only " + "contain strings, but one of them is not at " + "line %d", + config_setting_source_line(suppressions_setting)); + return NULL; + } + auto suppression = parse_animation_trigger(suppression_str); + if (suppression == ANIMATION_TRIGGER_INVALID) { + log_error( + "Invalid suppression defined at line %d", + config_setting_source_line(suppressions_setting)); + return NULL; + } + suppressions |= 1 << suppression; + } + } + config_setting_remove(setting, "suppressions"); + } + int output_indices[NUM_OF_WIN_SCRIPT_OUTPUTS]; char *err; auto script = compile_win_script(setting, output_indices, &err); @@ -328,7 +376,8 @@ parse_animation_one(struct win_script *animations, config_setting_t *setting) { } bool needed = set_animation(animations, trigger_types, number_of_triggers, script, - output_indices, config_setting_source_line(setting)); + output_indices, suppressions, + config_setting_source_line(setting)); if (!needed) { script_free(script); script = NULL; @@ -409,7 +458,7 @@ void generate_fading_config(struct options *opt) { trigger[number_of_triggers++] = ANIMATION_TRIGGER_SHOW; } if (set_animation(opt->animations, trigger, number_of_triggers, fade_in1, - output_indices, 0)) { + output_indices, 0, 0)) { scripts[number_of_scripts++] = fade_in1; } else { script_free(fade_in1); @@ -423,7 +472,7 @@ void generate_fading_config(struct options *opt) { trigger[number_of_triggers++] = ANIMATION_TRIGGER_INCREASE_OPACITY; } if (set_animation(opt->animations, trigger, number_of_triggers, fade_in2, - output_indices, 0)) { + output_indices, 0, 0)) { scripts[number_of_scripts++] = fade_in2; } else { script_free(fade_in2); @@ -443,7 +492,7 @@ void generate_fading_config(struct options *opt) { trigger[number_of_triggers++] = ANIMATION_TRIGGER_HIDE; } if (set_animation(opt->animations, trigger, number_of_triggers, fade_out1, - output_indices, 0)) { + output_indices, 0, 0)) { scripts[number_of_scripts++] = fade_out1; } else { script_free(fade_out1); @@ -457,7 +506,7 @@ void generate_fading_config(struct options *opt) { trigger[number_of_triggers++] = ANIMATION_TRIGGER_DECREASE_OPACITY; } if (set_animation(opt->animations, trigger, number_of_triggers, fade_out2, - output_indices, 0)) { + output_indices, 0, 0)) { scripts[number_of_scripts++] = fade_out2; } else { script_free(fade_out2); diff --git a/src/win.c b/src/win.c index 85c5dd0ac8..a25d7c5f1d 100644 --- a/src/win.c +++ b/src/win.c @@ -2343,6 +2343,13 @@ void win_process_animation_and_state_change(struct session *ps, struct managed_w return; } + if (w->running_animation && (w->running_animation_suppressions & (1 << trigger)) != 0) { + log_debug("Not starting animation %s for window %#010x (%s) because it " + "is being suppressed.", + animation_trigger_names[trigger], w->base.id, w->name); + return; + } + log_debug("Starting animation %s for window %#010x (%s)", animation_trigger_names[trigger], w->base.id, w->name); @@ -2353,6 +2360,7 @@ void win_process_animation_and_state_change(struct session *ps, struct managed_w } w->running_animation = new_animation; w->running_animation_outputs = ps->o.animations[trigger].output_indices; + w->running_animation_suppressions = ps->o.animations[trigger].suppressions; script_instance_evaluate(w->running_animation, &win_ctx); } diff --git a/src/win.h b/src/win.h index cc0c3be614..231efed1a3 100644 --- a/src/win.h +++ b/src/win.h @@ -311,6 +311,7 @@ struct managed_win { struct win_state_change previous; struct script_instance *running_animation; const int *running_animation_outputs; + uint64_t running_animation_suppressions; }; struct win_script_context { From df6ccb5e0ffc73416401684c8ddad6b47a5812f0 Mon Sep 17 00:00:00 2001 From: Istvan Petres Date: Sat, 18 May 2024 16:36:17 +0300 Subject: [PATCH 22/23] Added Desktop Switch Triggers for different animations This adds support for desktop switching animations by keeping track of _NET_CURRENT_DESKTOP atom on the root window. As far as I understand this atom is set by window managers and so if it changes we can know that it's a desktop switch happening. Unfortunately window manager may need to set this BEFORE hiding/showing windows so we can animate correctly, me personally using FluxBox and this quick change https://github.com/pijulius/fluxbox/commit/83ee4dbee67a68fe38b0f96431473719e165bf1e makes it work just fine. It adds the following animation triggers: * workspace-out * workspace-out-inverse * workspace-in * workspace-in-inverse Unfortunately had to add inverse variables too as you may navigate to the next workspace from for e.g. 1st to 2nd but you may also go to 2nd from 1st and in that case the animations have to be totally different. Here is a config example for switching workspace: animations = ({ triggers = ["workspace-out"]; offset-y = { timing = "0.2s cubic-bezier(0.21, 0.02, 0.76, 0.36)"; start = "0"; end = "-window-height"; }; shadow-offset-y = "offset-y"; opacity = { timing = "0.2s linear"; start = "window-raw-opacity-before"; end = "window-raw-opacity"; }; blur-opacity = "opacity"; shadow-opacity = "opacity"; }, { triggers = ["workspace-out-inverse"]; offset-y = { timing = "0.2s cubic-bezier(0.21, 0.02, 0.76, 0.36)"; start = "0"; end = "window-height + window-y"; }; shadow-offset-y = "offset-y"; opacity = { timing = "0.2s linear"; start = "window-raw-opacity-before"; end = "window-raw-opacity"; }; blur-opacity = "opacity"; shadow-opacity = "opacity"; }, { triggers = ["workspace-in"]; offset-y = { timing = "0.2s cubic-bezier(0.24, 0.64, 0.79, 0.98)"; start = "window-height + window-y"; end = "0"; }; shadow-offset-y = "offset-y"; opacity = { timing = "0.2s linear"; start = "0"; end = "window-raw-opacity"; }; blur-opacity = "opacity"; shadow-opacity = "opacity"; }, { triggers = ["workspace-in-inverse"]; offset-y = { timing = "0.2s cubic-bezier(0.24, 0.64, 0.79, 0.98)"; start = "-window-height"; end = "0"; }; shadow-offset-y = "offset-y"; opacity = { timing = "0.2s linear"; start = "0"; end = "window-raw-opacity"; }; blur-opacity = "opacity"; shadow-opacity = "opacity"; }) --- src/atom.h | 3 ++- src/common.h | 4 ++++ src/config.h | 10 +++++++++- src/event.c | 11 +++++++++++ src/picom.c | 13 +++++++++++++ src/win.c | 18 ++++++++++++++++++ 6 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/atom.h b/src/atom.h index a24dcd4425..5b94294b07 100644 --- a/src/atom.h +++ b/src/atom.h @@ -25,7 +25,8 @@ _NET_WM_WINDOW_TYPE, \ _XROOTPMAP_ID, \ ESETROOT_PMAP_ID, \ - _XSETROOT_ID + _XSETROOT_ID, \ + _NET_CURRENT_DESKTOP #define ATOM_LIST2 \ _NET_WM_WINDOW_TYPE_DESKTOP, \ diff --git a/src/common.h b/src/common.h index b7b220e930..2dc27b2e24 100644 --- a/src/common.h +++ b/src/common.h @@ -184,6 +184,10 @@ typedef struct session { int root_width; /// Height of root window. int root_height; + /// Current desktop number of root window + int root_desktop_num; + /// Desktop switch direction + int root_desktop_switch_direction; /// X Composite overlay window. xcb_window_t overlay; /// The target window for debug mode diff --git a/src/config.h b/src/config.h index 2297428ae4..814a9426ae 100644 --- a/src/config.h +++ b/src/config.h @@ -95,7 +95,11 @@ enum animation_trigger { ANIMATION_TRIGGER_OPEN, /// When a window is closed ANIMATION_TRIGGER_CLOSE, - ANIMATION_TRIGGER_LAST = ANIMATION_TRIGGER_CLOSE, + ANIMATION_TRIGGER_WORKSPACE_IN, + ANIMATION_TRIGGER_WORKSPACE_IN_INVERSE, + ANIMATION_TRIGGER_WORKSPACE_OUT, + ANIMATION_TRIGGER_WORKSPACE_OUT_INVERSE, + ANIMATION_TRIGGER_LAST = ANIMATION_TRIGGER_WORKSPACE_OUT_INVERSE, }; static const char *animation_trigger_names[] attr_unused = { @@ -105,6 +109,10 @@ static const char *animation_trigger_names[] attr_unused = { [ANIMATION_TRIGGER_DECREASE_OPACITY] = "decrease-opacity", [ANIMATION_TRIGGER_OPEN] = "open", [ANIMATION_TRIGGER_CLOSE] = "close", + [ANIMATION_TRIGGER_WORKSPACE_IN] = "workspace-in", + [ANIMATION_TRIGGER_WORKSPACE_IN_INVERSE] = "workspace-in-inverse", + [ANIMATION_TRIGGER_WORKSPACE_OUT] = "workspace-out", + [ANIMATION_TRIGGER_WORKSPACE_OUT_INVERSE] = "workspace-out-inverse", }; struct script; diff --git a/src/event.c b/src/event.c index d7b193bc5b..6166bb6249 100644 --- a/src/event.c +++ b/src/event.c @@ -591,6 +591,17 @@ static inline void ev_property_notify(session_t *ps, xcb_property_notify_event_t } if (ps->c.screen_info->root == ev->window) { + // If desktop number property changes + if (ev->atom == ps->atoms->a_NET_CURRENT_DESKTOP) { + auto prop = x_get_prop(&ps->c, ps->c.screen_info->root, ps->atoms->a_NET_CURRENT_DESKTOP, + 1L, XCB_ATOM_CARDINAL, 32); + + if (prop.nitems) { + ps->root_desktop_switch_direction = ((int)*prop.c32) - ps->root_desktop_num; + ps->root_desktop_num = (int)*prop.c32; + } + } + if (ps->o.use_ewmh_active_win && ps->atoms->a_NET_ACTIVE_WINDOW == ev->atom) { // to update focus ps->pending_updates = true; diff --git a/src/picom.c b/src/picom.c index 0c194ba1bb..e4dfc3c1d5 100644 --- a/src/picom.c +++ b/src/picom.c @@ -1822,6 +1822,9 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { ps->next_render = 0; ps->render_queued = false; + // Reset workspace switching so correct animations are used afterward + ps->root_desktop_switch_direction = 0; + // TODO(yshui) Investigate how big the X critical section needs to be. There are // suggestions that rendering should be in the critical section as well. @@ -2281,6 +2284,16 @@ static session_t *session_init(int argc, char **argv, Display *dpy, goto err; } + // Initiate current workspace num so we correctly animate if picom + // started/restarted on different desktop number than 0 + auto prop = x_get_prop(&ps->c, ps->c.screen_info->root, ps->atoms->a_NET_CURRENT_DESKTOP, + 1L, XCB_ATOM_CARDINAL, 32); + + ps->root_desktop_switch_direction = 0; + if (prop.nitems) { + ps->root_desktop_num = (int)*prop.c32; + } + rebuild_screen_reg(ps); bool compositor_running = false; diff --git a/src/win.c b/src/win.c index a25d7c5f1d..91077af3db 100644 --- a/src/win.c +++ b/src/win.c @@ -2309,6 +2309,15 @@ void win_process_animation_and_state_change(struct session *ps, struct managed_w case WSTATE_PAIR(WSTATE_UNMAPPED, WSTATE_MAPPED): trigger = w->in_openclose ? ANIMATION_TRIGGER_OPEN : ANIMATION_TRIGGER_SHOW; + + if (ps->root_desktop_switch_direction != 0) { + trigger = + (ps->root_desktop_switch_direction < 0 && + ps->root_desktop_switch_direction >= -1) || + ps->root_desktop_switch_direction > 1 ? + ANIMATION_TRIGGER_WORKSPACE_IN_INVERSE : + ANIMATION_TRIGGER_WORKSPACE_IN; + } break; case WSTATE_PAIR(WSTATE_UNMAPPED, WSTATE_DESTROYED): if ((!ps->o.no_fading_destroyed_argb || !win_has_alpha(w)) && @@ -2326,6 +2335,15 @@ void win_process_animation_and_state_change(struct session *ps, struct managed_w break; case WSTATE_PAIR(WSTATE_MAPPED, WSTATE_UNMAPPED): trigger = ANIMATION_TRIGGER_HIDE; + + if (ps->root_desktop_switch_direction != 0) { + trigger = + (ps->root_desktop_switch_direction < 0 && + ps->root_desktop_switch_direction >= -1) || + ps->root_desktop_switch_direction > 1 ? + ANIMATION_TRIGGER_WORKSPACE_OUT_INVERSE : + ANIMATION_TRIGGER_WORKSPACE_OUT; + } break; default: log_error("Impossible state transition from %d to %d", old_state, From 6624f0991d4c79bd317b94ce0d93956cb43c0bc5 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Sun, 19 May 2024 08:48:44 +0100 Subject: [PATCH 23/23] win: expose information about monitors to animation script Define script context variables describing the monitor a window is on. Signed-off-by: Yuxuan Shui --- src/win.c | 9 +++++++++ src/win.h | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/src/win.c b/src/win.c index a25d7c5f1d..b0ed94df61 100644 --- a/src/win.c +++ b/src/win.c @@ -2190,6 +2190,11 @@ void unmap_win_start(struct managed_win *w) { struct win_script_context win_script_context_prepare(struct session *ps, struct managed_win *w) { + auto monitor = + (w->randr_monitor >= 0 && w->randr_monitor < ps->monitors.count) + ? *pixman_region32_extents(&ps->monitors.regions[w->randr_monitor]) + : (pixman_box32_t){ + .x1 = 0, .y1 = 0, .x2 = ps->root_width, .y2 = ps->root_height}; struct win_script_context ret = { .x = w->g.x, .y = w->g.y, @@ -2197,6 +2202,10 @@ win_script_context_prepare(struct session *ps, struct managed_win *w) { .height = w->heightb, .opacity = win_calc_opacity_target(ps, w), .opacity_before = w->opacity, + .monitor_x = monitor.x1, + .monitor_y = monitor.y1, + .monitor_width = monitor.x2 - monitor.x1, + .monitor_height = monitor.y2 - monitor.y1, }; return ret; } diff --git a/src/win.h b/src/win.h index 231efed1a3..73d22ea301 100644 --- a/src/win.h +++ b/src/win.h @@ -317,6 +317,8 @@ struct managed_win { struct win_script_context { double x, y, width, height; double opacity_before, opacity; + double monitor_x, monitor_y; + double monitor_width, monitor_height; }; static const struct script_context_info win_script_context_info[] = { @@ -326,6 +328,10 @@ static const struct script_context_info win_script_context_info[] = { {"window-height", offsetof(struct win_script_context, height)}, {"window-raw-opacity-before", offsetof(struct win_script_context, opacity_before)}, {"window-raw-opacity", offsetof(struct win_script_context, opacity)}, + {"window-monitor-x", offsetof(struct win_script_context, monitor_x)}, + {"window-monitor-y", offsetof(struct win_script_context, monitor_y)}, + {"window-monitor-width", offsetof(struct win_script_context, monitor_width)}, + {"window-monitor-height", offsetof(struct win_script_context, monitor_height)}, {NULL, 0} // };