Add animation script

Allow the definition of customizable animations using expressions and
transition curves.

Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
This commit is contained in:
Yuxuan Shui 2024-04-20 12:10:56 +01:00
parent f7425e87ed
commit d731d8ede6
No known key found for this signature in database
GPG Key ID: D3A4405BE6CC17F4
4 changed files with 950 additions and 1 deletions

View File

@ -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') ]
'renderer/renderer.c', 'renderer/damage.c', 'script.c') ]
picom_inc = include_directories('.')
cflags = []

881
src/script.c Normal file
View File

@ -0,0 +1,881 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Yuxuan Shui <yshuiv7@gmail.com>
#include "script.h"
#include <libconfig.h>
#include <stdbool.h>
#include <stddef.h>
#include <uthash.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,
OP_EXP,
};
enum instruction_type {
INST_IMM = 0,
INST_OP,
INST_VAR,
INST_CTX,
};
struct instruction {
enum instruction_type type;
union {
double imm;
enum op op;
unsigned var_ref;
ptrdiff_t ctx;
};
};
struct expression {
unsigned len;
struct instruction instrs[];
};
enum variable_type {
VAR_TYPE_TRANSITION,
VAR_TYPE_IMM,
VAR_TYPE_EXPR,
};
struct non_curve_variable {
bool is_imm;
union {
struct expression *expr;
double imm;
};
};
struct variable {
enum variable_type type;
union {
struct {
const struct curve *curve;
double duration;
double delay;
struct non_curve_variable start, end;
bool reset;
} transition;
struct expression *expr;
double imm;
};
};
struct variable_index {
UT_hash_handle hh;
char *name;
unsigned index;
};
struct script {
unsigned len;
unsigned max_expr_len;
double max_transition_duration;
struct variable_index *indices;
struct variable vars[];
};
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 **out_end, char **err) {
const char *str = input_str;
const char *end;
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 (str == end) {
asprintf(err, "Invalid curve definition %s (invalid time unit at \"%s\")",
input_str, str);
return NAN;
}
*out_end = end;
return number * unit;
}
/// Parse a timing function.
///
/// Syntax for this is the same as CSS transitions:
/// <duration> <timing-function> <delay>
/// 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;
}
*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 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 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];
}
static struct instruction
parse_operand(const char *str, const char **end, const struct variable_index *indices,
const struct script_context_info *ctx_info, char **err) {
double number = strtod_simple(str, end);
*err = NULL;
if (*end != str) {
return (struct instruction){
.type = INST_IMM,
.imm = number,
};
}
while (**end) {
if (isalnum(**end) || **end == '-' || **end == '_') {
(*end)++;
} else {
break;
}
}
if (*end == str) {
asprintf(err, "Failed to parse a number or a variable name %s", str);
return (struct instruction){};
}
struct variable_index *var = NULL;
HASH_FIND(hh, indices, str, (unsigned long)(*end - str), var);
if (var != NULL) {
return (struct instruction){
.type = INST_VAR,
.var_ref = var->index,
};
}
struct script_context_info *ctx = NULL;
HASH_FIND(hh, ctx_info, str, (unsigned long)(*end - str), ctx);
if (ctx != NULL) {
return (struct instruction){
.type = INST_CTX,
.ctx = ctx->offset,
};
}
asprintf(err, "variable name \"%.*s\" is not defined", (int)(*end - str), str);
*end = str;
return (struct instruction){};
}
struct expression_parser_context {
char *op_stack;
struct instruction *instrs;
size_t len;
size_t op_top, operand_top, ninstrs;
};
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);
}
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;
}
// Both operands are immediates, do constant propagation.
if (ctx->instrs[ctx->ninstrs - 1].type == INST_IMM &&
ctx->instrs[ctx->ninstrs - 2].type == INST_IMM) {
double imm = op_eval(ctx->instrs[ctx->ninstrs - 1].imm,
char_to_op(ctx->op_stack[ctx->op_top]),
ctx->instrs[ctx->ninstrs - 2].imm);
ctx->operand_top -= 1;
ctx->instrs[ctx->ninstrs - 2].imm = imm;
ctx->ninstrs -= 1;
ctx->op_top -= 1;
return true;
}
ctx->instrs[ctx->ninstrs].type = INST_OP;
ctx->instrs[ctx->ninstrs].op = char_to_op(ctx->op_stack[ctx->op_top - 1]);
ctx->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, const struct variable_index *indices,
const struct script_context_info *ctx_info, char **err) {
const char *str = input_str;
while (*str == '(') {
str = skip_space(str + 1);
ctx->op_stack[ctx->op_top++] = '(';
}
ctx->instrs[ctx->ninstrs++] = parse_operand(str, end, indices, ctx_info, err);
if (str == *end) {
return false;
}
str = skip_space(*end);
ctx->operand_top += 1;
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 %s", input_str);
return false;
}
ctx->op_top -= 1;
str = skip_space(str + 1);
}
*end = str;
return true;
}
/// Precedence based expression parser.
static struct expression *
parse_expression(const char *input_str, const struct variable_index *indices,
const struct script_context_info *ctx_info, char **err) {
const char *str = skip_space(input_str);
const size_t len = strlen(str);
struct expression *expr = NULL;
if (len == 0) {
return NULL;
}
// At most each character in `input_str` could map to an individual instruction
struct expression_parser_context ctx = {
.op_stack = ccalloc(len, char),
.instrs = ccalloc(len, struct instruction),
.op_top = 0,
.operand_top = 0,
.ninstrs = 0,
};
const char *end = NULL;
if (!parse_operand_or_paren(&ctx, str, &end, indices, ctx_info, 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, indices, ctx_info, 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;
}
expr = malloc(sizeof(struct expression) + sizeof(struct instruction[ctx.ninstrs]));
BUG_ON(ctx.ninstrs > UINT_MAX);
expr->len = (unsigned)ctx.ninstrs;
memcpy(expr->instrs, ctx.instrs, sizeof(struct instruction[ctx.ninstrs]));
end:
free(ctx.op_stack);
free(ctx.instrs);
return expr;
}
static void expression_free(struct expression *expr) {
free(expr);
}
static void variable_deinit(struct variable *var) {
switch (var->type) {
case VAR_TYPE_TRANSITION:
var->transition.curve->free(var->transition.curve);
if (!var->transition.start.is_imm) {
expression_free(var->transition.start.expr);
}
if (!var->transition.end.is_imm) {
expression_free(var->transition.end.expr);
}
break;
case VAR_TYPE_EXPR: expression_free(var->expr); break;
case VAR_TYPE_IMM: break;
}
}
static bool parse_transition(config_setting_t *setting, struct variable *var,
const struct variable_index *indices,
const struct script_context_info *ctx_info, char **err) {
const char *str = NULL;
int boolean = 0;
double number = 0;
if (!config_setting_lookup_string(setting, "timing", &str)) {
asprintf(err, "transition section must contain a timing function");
return false;
}
var->transition.curve = parse_timing_function(str, &var->transition.duration,
&var->transition.delay, err);
if (!var->transition.curve) {
return false;
}
if (!config_setting_lookup_bool(setting, "reset", &boolean)) {
var->transition.reset = false;
} else {
var->transition.reset = boolean;
}
// set start/end.is_imm to true so `variable_deinit` will handle them properly.
var->transition.start.is_imm = true;
var->transition.end.is_imm = true;
if (config_setting_lookup_float(setting, "start", &number)) {
var->transition.start.imm = number;
} else if (!config_setting_lookup_string(setting, "start", &str)) {
asprintf(err, "transition definition must contain a start value or "
"expression");
variable_deinit(var);
return false;
} else {
var->transition.start.expr = parse_expression(str, indices, ctx_info, err);
if (!var->transition.start.expr) {
variable_deinit(var);
return false;
}
var->transition.start.is_imm = false;
}
if (config_setting_lookup_float(setting, "end", &number)) {
var->transition.end.imm = number;
} else if (!config_setting_lookup_string(setting, "end", &str)) {
asprintf(err, "transition definition must contain a end value or "
"expression");
return false;
} else {
var->transition.end.expr = parse_expression(str, indices, ctx_info, err);
if (!var->transition.end.expr) {
variable_deinit(var);
return false;
}
var->transition.end.is_imm = false;
}
return true;
}
static inline void variable_indices_free(struct variable_index **indices) {
struct variable_index *index, *next_index;
HASH_ITER(hh, *indices, index, next_index) {
HASH_DEL(*indices, index);
free(index->name);
free(index);
}
}
void script_free(struct script *script) {
for (unsigned i = 0; i < script->len; i++) {
variable_deinit(&script->vars[i]);
}
variable_indices_free(&script->indices);
free(script);
}
struct script *script_parse(config_setting_t *setting,
const struct script_context_info *ctx_info, char **out_err) {
if (!config_setting_is_group(setting)) {
return NULL;
}
struct variable_index *indices = NULL;
const uint32_t n = to_u32_checked(config_setting_length(setting));
for (uint32_t i = 0; i < n; i++) {
auto var = config_setting_get_elem(setting, i);
struct variable_index *new_index = ccalloc(1, struct variable_index);
new_index->name = strdup(config_setting_name(var));
new_index->index = i;
HASH_ADD_STR(indices, name, new_index);
}
struct script *script = malloc(sizeof(struct script) + sizeof(struct variable[n]));
script->len = 0;
script->max_expr_len = 0;
script->max_transition_duration = 0;
script->indices = NULL;
for (uint32_t i = 0; i < n; i++, script->len++) {
auto var = config_setting_get_elem(setting, i);
if (config_setting_is_number(var)) {
script->vars[i].type = VAR_TYPE_IMM;
script->vars[i].imm = config_setting_get_float(var);
continue;
}
char *err = NULL;
const char *str = config_setting_get_string(var);
if (str != NULL) {
script->vars[i].expr = parse_expression(str, indices, ctx_info, &err);
if (!script->vars[i].expr) {
asprintf(out_err,
"Cannot parse expression %s = \"%s\" (error %s)",
config_setting_name(var), str, err);
free(err);
break;
}
script->vars[i].type = VAR_TYPE_EXPR;
if (script->vars[i].expr->len > script->max_expr_len) {
script->max_expr_len = script->vars[i].expr->len;
}
continue;
}
if (!config_setting_is_group(var)) {
asprintf(out_err,
"Invalid variable \"%s\", it must be either a number, "
"a string, or a config group defining a transition.",
config_setting_name(var));
break;
}
if (!parse_transition(var, &script->vars[i], indices, ctx_info, &err)) {
asprintf(out_err, "Cannot parse member %s as a transition (error %s)",
config_setting_name(var), err);
free(err);
break;
}
script->vars[i].type = VAR_TYPE_TRANSITION;
double transition_duration =
script->vars[i].transition.delay + script->vars[i].transition.duration;
if (transition_duration > script->max_transition_duration) {
script->max_transition_duration = transition_duration;
}
}
if (script->len < n) {
script_free(script);
return NULL;
}
script->indices = indices;
return script;
}
struct script_instance *script_instance_new(const struct script *script) {
struct script_instance *instance =
malloc(offsetof(struct script_instance, vars[script->len]) +
sizeof(double[script->max_expr_len]) + sizeof(unsigned[script->len * 2]));
instance->script = script;
instance->elapsed = 0;
for (unsigned i = 0; i < script->len; i++) {
auto var = &script->vars[i];
instance->vars[i].evaluated = 0;
switch (var->type) {
case VAR_TYPE_IMM:
instance->vars[i].evaluated = 1;
instance->vars[i].result = var->imm;
break;
case VAR_TYPE_TRANSITION:
instance->vars[i].start =
var->transition.start.is_imm ? var->transition.start.imm : NAN;
instance->vars[i].end =
var->transition.end.is_imm ? var->transition.end.imm : NAN;
instance->vars[i].current = NAN;
break;
case VAR_TYPE_EXPR: instance->vars[i].result = NAN; break;
}
}
return instance;
}
struct script_instance *
script_instance_steer(struct script_instance *old, struct script *script) {
auto instance = script_instance_new(script);
HASH_ITER2(script->indices, var) {
if (script->vars[var->index].type != VAR_TYPE_TRANSITION) {
continue;
}
if (script->vars[var->index].transition.reset) {
continue;
}
struct variable_index *old_index = NULL;
HASH_FIND_STR(old->script->indices, var->name, old_index);
if (old_index == NULL) {
continue;
}
BUG_ON(old->vars[old_index->index].evaluated != 1);
if (old->script->vars[old_index->index].type == VAR_TYPE_TRANSITION) {
instance->vars[var->index].start = old->vars[old_index->index].current;
} else {
instance->vars[var->index].start = old->vars[old_index->index].result;
}
}
return instance;
}
void script_instance_advance(struct script_instance *instance, double elapsed) {
instance->elapsed += elapsed;
auto script = instance->script;
for (unsigned i = 0; i < script->len; i++) {
switch (script->vars[i].type) {
case VAR_TYPE_TRANSITION:
instance->vars[i].current = NAN;
instance->vars[i].evaluated = 0;
break;
case VAR_TYPE_EXPR:
instance->vars[i].result = NAN;
instance->vars[i].evaluated = 0;
break;
case VAR_TYPE_IMM: break;
}
}
}
static double script_instance_evaluate_one_expression(struct script_instance *instance,
struct expression *expr, void *context) {
auto script = instance->script;
auto eval_stack = (double *)&instance->vars[script->len];
unsigned stack_top = 0;
for (unsigned i = 0; i < expr->len; i++) {
switch (expr->instrs[i].type) {
case INST_CTX:
BUG_ON(context == NULL);
eval_stack[stack_top++] = *(double *)(context + expr->instrs[i].ctx);
break;
case INST_IMM: eval_stack[stack_top++] = expr->instrs[i].imm; break;
case INST_VAR:;
auto inst_var = &instance->vars[expr->instrs[i].var_ref];
auto script_var = &script->vars[expr->instrs[i].var_ref];
BUG_ON(inst_var->evaluated != 1);
if (script_var->type == VAR_TYPE_TRANSITION) {
if (instance->elapsed == 0) {
eval_stack[stack_top++] = inst_var->start;
} else {
eval_stack[stack_top++] = inst_var->current;
}
} else {
eval_stack[stack_top++] = inst_var->result;
}
break;
case INST_OP:
BUG_ON(stack_top < 2);
eval_stack[stack_top - 2] =
op_eval(eval_stack[stack_top - 2], expr->instrs[i].op,
eval_stack[stack_top - 1]);
stack_top -= 1;
break;
}
if (safe_isnan(eval_stack[stack_top - 1])) {
return NAN;
}
}
BUG_ON(stack_top != 1);
return eval_stack[stack_top - 1];
}
static inline enum script_evaluation_result
script_instance_evaluate_one_transition(struct script_instance *instance, unsigned i) {
auto script = instance->script;
if (instance->elapsed == 0) {
BUG_ON(safe_isnan(instance->vars[i].start));
instance->vars[i].current = instance->vars[i].start;
instance->vars[i].evaluated = 1;
return SCRIPT_EVAL_OK;
}
double progress = (instance->elapsed - script->vars[i].transition.delay) /
script->vars[i].transition.duration;
progress = min2(max2(progress, 0.0), 1.0);
double t = script->vars[i].transition.curve->sample(
script->vars[i].transition.curve, progress);
instance->vars[i].current =
t * instance->vars[i].end + (1 - t) * instance->vars[i].start;
if (safe_isnan(instance->vars[i].current)) {
return SCRIPT_EVAL_ERROR_NAN;
}
if (safe_isinf(instance->vars[i].current)) {
return SCRIPT_EVAL_ERROR_INF;
}
instance->vars[i].evaluated = 1;
return SCRIPT_EVAL_OK;
}
static enum script_evaluation_result
script_instance_evaluate_one(struct script_instance *instance, unsigned var_index,
unsigned *stack, unsigned *scan_index, void *context,
unsigned **cycle) {
unsigned stack_top = 0;
auto script = instance->script;
stack[stack_top] = var_index;
scan_index[stack_top++] = 0;
instance->vars[var_index].evaluated = 2;
while (stack_top) {
auto curr = stack[stack_top - 1];
struct expression *expr;
if (script->vars[curr].type == VAR_TYPE_TRANSITION) {
// If a transition type is on stack, that means its start value
// is wanted. This has to be the first time this instance is
// evaluated.
BUG_ON(instance->elapsed > 0);
BUG_ON(script->vars[curr].transition.start.is_imm);
expr = script->vars[curr].transition.start.expr;
} else {
BUG_ON(script->vars[curr].type == VAR_TYPE_IMM);
expr = script->vars[curr].expr;
}
// First, make sure all referenced variables are evaluated.
for (; scan_index[stack_top - 1] < expr->len; scan_index[stack_top - 1]++) {
auto i = scan_index[stack_top - 1];
if (expr->instrs[i].type != INST_VAR) {
continue;
}
auto var_ref = expr->instrs[i].var_ref;
if (instance->vars[var_ref].evaluated == 1) {
continue;
}
if (instance->vars[var_ref].evaluated == 2) {
// Reference cycle
unsigned cycle_length = 1;
for (unsigned j = 1; j < stack_top; j++) {
if (stack[stack_top - j] == var_ref) {
break;
}
cycle_length += 1;
}
*cycle = ccalloc(cycle_length + 1, unsigned);
for (unsigned j = 0; j < cycle_length; j++) {
(*cycle)[j] = stack[stack_top - cycle_length + j];
}
(*cycle)[cycle_length] = var_ref;
return SCRIPT_EVAL_ERROR_CYCLIC;
}
auto script_var = &script->vars[var_ref];
auto inst_var = &instance->vars[var_ref];
BUG_ON(script_var->type == VAR_TYPE_IMM);
if ((script_var->type == VAR_TYPE_TRANSITION &&
safe_isnan(inst_var->start)) ||
script_var->type == VAR_TYPE_EXPR) {
// This var_ref needs to be evaluated first, push it on to
// the stack.
stack[stack_top] = var_ref;
scan_index[stack_top++] = 0;
inst_var->evaluated = 2;
goto stack_next;
}
// Here script_var->type must be VAR_TYPE_TRANSITION
auto ret = script_instance_evaluate_one_transition(instance, var_ref);
if (ret != SCRIPT_EVAL_OK) {
return ret;
}
}
double result =
script_instance_evaluate_one_expression(instance, expr, context);
if (safe_isinf(result)) {
return SCRIPT_EVAL_ERROR_INF;
}
if (safe_isnan(result)) {
return SCRIPT_EVAL_ERROR_NAN;
}
if (script->vars[curr].type == VAR_TYPE_TRANSITION) {
instance->vars[curr].start = result;
} else {
instance->vars[curr].result = result;
}
instance->vars[curr].evaluated = 1;
stack_top -= 1;
stack_next:;
}
return SCRIPT_EVAL_OK;
}
bool script_instance_is_finished(const struct script_instance *instance) {
return instance->elapsed >= instance->script->max_transition_duration;
}
enum script_evaluation_result
script_instance_evaluate(struct script_instance *instance, void *context, unsigned **cycle) {
auto script = instance->script;
auto eval_stack = (double *)&instance->vars[script->len];
auto stack = (unsigned *)(eval_stack + script->max_expr_len);
auto scan_index = stack + script->len;
for (unsigned i = 0; i < script->len; i++) {
if (instance->vars[i].evaluated || script->vars[i].type != VAR_TYPE_EXPR) {
continue;
}
auto result = script_instance_evaluate_one(instance, i, stack, scan_index,
context, cycle);
if (result != SCRIPT_EVAL_OK) {
return result;
}
}
for (unsigned i = 0; i < script->len; i++) {
if (script->vars[i].type != VAR_TYPE_TRANSITION) {
continue;
}
if (safe_isnan(instance->vars[i].start)) {
BUG_ON(instance->elapsed > 0);
instance->vars[i].start = script_instance_evaluate_one_expression(
instance, script->vars[i].transition.start.expr, context);
}
if (safe_isnan(instance->vars[i].end)) {
BUG_ON(instance->elapsed > 0);
instance->vars[i].end = script_instance_evaluate_one_expression(
instance, script->vars[i].transition.end.expr, context);
}
if (instance->vars[i].evaluated) {
continue;
}
auto ret = script_instance_evaluate_one_transition(instance, i);
if (ret != SCRIPT_EVAL_OK) {
return ret;
}
}
return SCRIPT_EVAL_OK;
}
TEST_CASE(scripts) {
config_t cfg;
config_init(&cfg);
config_set_auto_convert(&cfg, 1);
int ret = config_read_string(&cfg, "cfg: { \
a = 10; \
b = \"a * 2\";\
c = \"(b - 1) * (a+1)\";\
d = \"e - 1\"; \
e : { \
timing = \"10s cubic-bezier(0.1,0.2, 0.3, 0.4) 0.5s\"; \
start = 0; \
end = \"2 * c\"; \
}; \
}");
TEST_EQUAL(ret, CONFIG_TRUE);
config_setting_t *setting = config_lookup(&cfg, "cfg");
TEST_NOTEQUAL(setting, NULL);
char *err = NULL;
auto script = script_parse(setting, NULL, &err);
TEST_NOTEQUAL(script, NULL);
TEST_EQUAL(err, NULL);
if (err) {
TEST_STREQUAL(err, "");
free(err);
}
if (script) {
struct variable_index *c;
HASH_FIND_STR(script->indices, "c", c);
TEST_NOTEQUAL(c, NULL);
TEST_EQUAL(script->vars[c->index].type, VAR_TYPE_EXPR);
struct expression *c_expr = script->vars[c->index].expr;
TEST_EQUAL(c_expr->len, 7);
enum instruction_type expected_type[] = {
INST_VAR, INST_IMM, INST_OP, INST_VAR, INST_IMM, INST_OP, INST_OP};
for (size_t i = 0; i < ARR_SIZE(expected_type); i++) {
TEST_EQUAL(c_expr->instrs[i].type, expected_type[i]);
}
struct script_instance *instance = script_instance_new(script);
unsigned *cycle;
auto result = script_instance_evaluate(instance, NULL, &cycle);
TEST_EQUAL(result, SCRIPT_EVAL_OK);
TEST_EQUAL(instance->vars[0].result, 10);
TEST_EQUAL(instance->vars[1].result, 20);
TEST_EQUAL(instance->vars[2].result, 209);
TEST_EQUAL(instance->vars[3].result, -1);
TEST_EQUAL(instance->vars[4].current, 0);
TEST_EQUAL(instance->vars[4].start, 0);
TEST_EQUAL(instance->vars[4].end, 418);
script_instance_advance(instance, 11);
result = script_instance_evaluate(instance, NULL, &cycle);
TEST_EQUAL(result, SCRIPT_EVAL_OK);
TEST_EQUAL(instance->vars[0].result, 10);
TEST_EQUAL(instance->vars[1].result, 20);
TEST_EQUAL(instance->vars[2].result, 209);
TEST_EQUAL(instance->vars[3].result, 417);
TEST_EQUAL(instance->vars[4].current, 418);
TEST_EQUAL(instance->vars[4].start, 0);
TEST_EQUAL(instance->vars[4].end, 418);
TEST_TRUE(script_instance_is_finished(instance));
free(instance);
script_free(script);
}
config_destroy(&cfg);
}

58
src/script.h Normal file
View File

@ -0,0 +1,58 @@
#include <assert.h>
#include <stdalign.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <uthash.h>
#include "compiler.h"
struct script_context_info {
UT_hash_handle hh;
const char *name;
ptrdiff_t offset;
};
struct script;
struct script_instance {
const struct script *script;
double elapsed;
struct {
union {
/// For a expression or immediate variable
double result;
/// For a transition variable
struct {
double start;
double end;
double current;
};
};
uint8_t evaluated;
} vars[];
};
enum script_evaluation_result {
/// Script contains cyclic references
SCRIPT_EVAL_ERROR_CYCLIC,
/// +/-inf in results
SCRIPT_EVAL_ERROR_INF,
/// NaN in results
SCRIPT_EVAL_ERROR_NAN,
/// OK
SCRIPT_EVAL_OK,
};
static_assert(alignof(typeof(((struct script_instance *)NULL)->vars)) >= alignof(double),
"vars has unexpected alignment");
static_assert(alignof(double) > alignof(unsigned), "double/unsigned has unexpected "
"alignment");
struct script_instance *script_instance_new(const struct script *script);
enum script_evaluation_result
script_instance_evaluate(struct script_instance *instance, void *context, unsigned **cycle);
void script_instance_reset(struct script_instance *instance);
void script_instance_advance(struct script_instance *instance, double elapsed);
bool script_instance_is_finished(const struct script_instance *instance);
/// Interrupt a script that is currently running, and "steer" it towards a new script.
/// For transition functions in `new_` that has a match in `old`, the current value of
/// that function in `old` will be used as the start value in `new_`. Unless that
/// transition function has the `reset` property enabled.
struct script_instance *
script_instance_steer(struct script_instance *old, struct script *script);

View File

@ -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() \