1
0
Fork 0
mirror of https://github.com/yshui/picom.git synced 2025-02-10 15:45:57 -05:00

Merge pull request #1305 from yshui/animation-presets

Animation part 4:  Presets
This commit is contained in:
Yuxuan Shui 2024-08-06 05:45:52 +01:00 committed by GitHub
commit 1cfd2a0d98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 2864 additions and 471 deletions

View file

@ -40,6 +40,9 @@ jobs:
steps:
- build:
build-config: -Dwith_docs=true -Db_coverage=true
- run:
name: build animgen
command: ninja -vC build tools/animgen
- persist_to_workspace:
root: .
paths:
@ -54,6 +57,10 @@ jobs:
command: |
ulimit -c unlimited
printf "\n::: test animgen :::\n"
build/tools/animgen data/animation_presets.conf >/dev/null 2>error.log
[ -s error.log ] && cat error.log && exit 1
printf "\n::: Unit tests :::\n"
ninja -vC build test

2
.gitignore vendored
View file

@ -51,8 +51,10 @@ target
!/tests/testcases/*.py
# Misc files
.vscode
*.conf
!/tests/configs/*.conf
!/data/*.conf
perf.data
perf.data.old
core.*

165
data/animation_presets.conf Normal file
View file

@ -0,0 +1,165 @@
disappear = {
opacity = {
duration = "placeholder0";
start = "window-raw-opacity-before";
end = "window-raw-opacity";
};
blur-opacity = "opacity";
shadow-opacity = "opacity";
offset-x = "(1 - scale-x) / 2 * window-width";
offset-y = "(1 - scale-y) / 2 * window-height";
scale-x = {
curve = "cubic-bezier(0.21, 0.02, 0.76, 0.36)";
duration = "placeholder0";
start = 1;
end = "placeholder1";
};
scale-y = "scale-x";
shadow-scale-x = "scale-x";
shadow-scale-y = "scale-y";
shadow-offset-x = "offset-x";
shadow-offset-y = "offset-y";
# See comments in tools/animgen.c for syntax
*knobs = {
scale = 0.95;
duration = 0.2;
};
*placeholders = ((0, "duration"),(1, "scale"));
};
appear = {
opacity = {
duration = "placeholder0";
start = "window-raw-opacity-before";
end = "window-raw-opacity";
};
blur-opacity = "opacity";
shadow-opacity = "opacity";
offset-x = "(1 - scale-x) / 2 * window-width";
offset-y = "(1 - scale-y) / 2 * window-height";
scale-x = {
curve = "cubic-bezier(0.24, 0.64, 0.79, 0.98)";
duration = "placeholder0";
start = "placeholder1";
end = 1;
};
scale-y = "scale-x";
shadow-scale-x = "scale-x";
shadow-scale-y = "scale-y";
shadow-offset-x = "offset-x";
shadow-offset-y = "offset-y";
*knobs = {
scale = 0.95;
duration = 0.2;
};
*placeholders = ((0, "duration"),(1, "scale"));
};
slide-out = {
v-timing = {
curve = "cubic-bezier(0.21, 0.02, 0.76, 0.36)";
start = 0;
duration = "placeholder0";
end = "window-width * placeholder1 + window-height * placeholder2";
};
offset-x = "v-timing * placeholder3";
offset-y = "v-timing * (1 - placeholder3)";
shadow-offset-x = "offset-x";
shadow-offset-y = "offset-y";
crop-x = "window-x";
crop-y = "window-y";
crop-width = "window-width";
crop-height = "window-height";
opacity = 1;
blur-opacity = "opacity";
shadow-opacity = "opacity";
*knobs = {
duration = 0.2;
direction = (2, ["up", "down", "left", "right"]);
};
*placeholders = (
(0, "duration"),
(1, "direction", [0, 0, -1, 1]),
(2, "direction", [-1, 1, 0, 0]),
(3, "direction", [0, 0, 1, 1]),
);
};
slide-in = {
v-timing = {
curve = "cubic-bezier(0.24, 0.64, 0.79, 0.98)";
start = "window-width * placeholder1 + window-height * placeholder2";
duration = "placeholder0";
end = 0;
};
offset-x = "v-timing * placeholder3";
offset-y = "v-timing * (1 - placeholder3)";
shadow-offset-x = "offset-x";
shadow-offset-y = "offset-y";
crop-x = "window-x";
crop-y = "window-y";
crop-width = "window-width";
crop-height = "window-height";
*knobs = {
duration = 0.2;
direction = (2, ["up", "down", "left", "right"]);
};
*placeholders = (
(0, "duration"),
(1, "direction", [0, 0, -1, 1]),
(2, "direction", [-1, 1, 0, 0]),
(3, "direction", [0, 0, 1, 1]),
);
};
fly-out = {
v-timing = {
curve = "cubic-bezier(0.05, 0, 0.69, -0.05)";
duration = "placeholder0";
start = 0;
end = "(window-height + window-y) * placeholder2 + (window-width + window-x) * placeholder1";
};
offset-x = "v-timing * placeholder3";
offset-y = "v-timing * (1 - placeholder3)";
shadow-offset-x = "offset-x";
shadow-offset-y = "offset-y";
opacity = 1;
shadow-opacity = 1;
blur-opacity = 1;
*knobs = {
duration = 0.2;
direction = (0, ["up", "down", "left", "right"]);
};
*placeholders = (
(0, "duration"),
(1, "direction", [0, 0, -1, 1]),
(2, "direction", [-1, 1, 0, 0]),
(3, "direction", [0, 0, 1, 1]),
);
};
fly-in = {
v-timing = {
curve = "cubic-bezier(0.17, 0.67, 0.68, 1.03)";
end = 0;
duration = "placeholder0";
start = "- window-height - window-y";
};
offset-x = "v-timing * placeholder3";
offset-y = "v-timing * (1 - placeholder3)";
shadow-offset-x = "offset-x";
shadow-offset-y = "offset-y";
opacity = 1;
shadow-opacity = 1;
blur-opacity = 1;
*knobs = {
duration = 0.2;
direction = (0, ["up", "down", "left", "right"]);
};
*placeholders = (
(0, "duration"),
(1, "direction", [0, 0, -1, 1]),
(2, "direction", [-1, 1, 0, 0]),
(3, "direction", [0, 0, 1, 1]),
);
}

View file

@ -92,6 +92,7 @@ test_h_dep = subproject('test.h').get_variable('test_h_dep')
subdir('src')
subdir('man')
subdir('tools')
install_data('bin/picom-trans', install_dir: get_option('bindir'))
install_data('picom.desktop', install_dir: 'share/applications')

View file

@ -16,6 +16,7 @@
#include "common.h"
#include "config.h"
#include "log.h"
#include "transition/preset.h"
#include "transition/script.h"
#include "utils/dynarr.h"
#include "utils/misc.h"
@ -233,8 +234,14 @@ static enum animation_trigger parse_animation_trigger(const char *trigger) {
return ANIMATION_TRIGGER_INVALID;
}
static struct script *
compile_win_script(config_setting_t *setting, int *output_indices, char **err) {
/// Compile a script from `setting` into `result`, return false on failure.
/// Only the `script` and `output_indices` fields of `result` will be modified.
static bool
compile_win_script(struct win_script *result, config_setting_t *setting, char **err) {
if (config_setting_lookup(setting, "preset")) {
return win_script_parse_preset(result, setting);
}
struct script_output_info outputs[ARR_SIZE(win_script_outputs)];
memcpy(outputs, win_script_outputs, sizeof(win_script_outputs));
@ -242,20 +249,19 @@ compile_win_script(config_setting_t *setting, int *output_indices, char **err) {
.context_info = win_script_context_info,
.output_info = outputs,
};
auto script = script_compile(setting, parse_config, err);
if (script == NULL) {
return script;
result->script = script_compile(setting, parse_config, err);
if (result->script == NULL) {
return false;
}
for (int i = 0; i < NUM_OF_WIN_SCRIPT_OUTPUTS; i++) {
output_indices[i] = outputs[i].slot;
result->output_indices[i] = outputs[i].slot;
}
return script;
return true;
}
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 is_generated) {
int number_of_triggers, struct win_script animation, unsigned line) {
bool needed = false;
for (int i = 0; i < number_of_triggers; i++) {
if (triggers[i] == ANIMATION_TRIGGER_INVALID) {
@ -268,42 +274,39 @@ set_animation(struct win_script *animations, const enum animation_trigger *trigg
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;
animations[triggers[i]].suppressions = suppressions;
animations[triggers[i]].is_generated = is_generated;
animations[triggers[i]] = animation;
needed = true;
}
return needed;
}
static struct script *
parse_animation_one(struct win_script *animations, config_setting_t *setting) {
static bool parse_animation_one(struct win_script *animations,
struct script ***all_scripts, config_setting_t *setting) {
struct win_script result = {};
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;
return false;
}
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;
return false;
}
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;
return false;
}
if (number_of_triggers == 0) {
log_error("Trigger list is empty in animation defined at line %d",
config_setting_source_line(triggers));
return NULL;
return false;
}
enum animation_trigger *trigger_types =
alloca(sizeof(enum animation_trigger[number_of_triggers]));
@ -322,7 +325,6 @@ 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);
@ -332,16 +334,16 @@ parse_animation_one(struct win_script *animations, config_setting_t *setting) {
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;
return false;
}
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;
return false;
}
suppressions = 1 << suppression;
result.suppressions = 1 << suppression;
} else {
auto len = config_setting_length(suppressions_setting);
for (int i = 0; i < len; i++) {
@ -353,39 +355,37 @@ parse_animation_one(struct win_script *animations, config_setting_t *setting) {
"contain strings, but one of them is not at "
"line %d",
config_setting_source_line(suppressions_setting));
return NULL;
return false;
}
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;
return false;
}
suppressions |= 1U << suppression;
result.suppressions |= 1U << 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);
if (!script) {
if (!compile_win_script(&result, setting, &err)) {
log_error("Failed to parse animation script at line %d: %s",
config_setting_source_line(setting), err);
free(err);
return NULL;
return false;
}
bool needed = set_animation(animations, trigger_types, number_of_triggers, script,
output_indices, suppressions,
config_setting_source_line(setting), false);
bool needed = set_animation(animations, trigger_types, number_of_triggers, result,
config_setting_source_line(setting));
if (!needed) {
script_free(script);
script = NULL;
script_free(result.script);
} else {
dynarr_push(*all_scripts, result.script);
}
return script;
return true;
}
/// `out_scripts`: all the script objects created, this is a dynarr.
@ -394,27 +394,24 @@ static void parse_animations(struct win_script *animations, config_setting_t *se
auto number_of_animations = (unsigned)config_setting_length(setting);
for (unsigned 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) {
dynarr_push(*out_scripts, script);
}
parse_animation_one(animations, out_scripts, sub);
}
}
#define FADING_TEMPLATE_1 \
"opacity = { " \
" timing = \"%sms linear\"; " \
" duration = %s; " \
" start = \"window-raw-opacity-before\"; " \
" end = \"window-raw-opacity\"; " \
"};" \
"shadow-opacity = \"opacity\";"
#define FADING_TEMPLATE_2 \
"blur-opacity = { " \
" timing = \"%sms linear\"; " \
" duration = %s; " \
" start = %d; end = %d; " \
"};"
static struct script *compile_win_script_from_string(const char *input, int *output_indices) {
static bool compile_win_script_from_string(struct win_script *result, const char *input) {
config_t tmp_config;
config_setting_t *setting;
config_init(&tmp_config);
@ -424,11 +421,11 @@ static struct script *compile_win_script_from_string(const char *input, int *out
// Since we are compiling scripts we generated, it can't fail.
char *err = NULL;
auto script = compile_win_script(setting, output_indices, &err);
bool succeeded = compile_win_script(result, setting, &err);
config_destroy(&tmp_config);
BUG_ON(err != NULL);
return script;
return succeeded;
}
void generate_fading_config(struct options *opt) {
@ -441,8 +438,7 @@ void generate_fading_config(struct options *opt) {
unsigned 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;
double duration = 1.0 / opt->fade_in_step * opt->fade_delta / 1000.0;
if (!safe_isinf(duration) && !safe_isnan(duration) && duration > 0) {
scoped_charp duration_str = NULL;
dtostr(duration, &duration_str);
@ -451,7 +447,8 @@ void generate_fading_config(struct options *opt) {
asnprintf(&str, &len, FADING_TEMPLATE_1 FADING_TEMPLATE_2, duration_str,
duration_str, 0, 1);
auto fade_in1 = compile_win_script_from_string(str, output_indices);
struct win_script fade_in1 = {.is_generated = true};
BUG_ON(!compile_win_script_from_string(&fade_in1, str));
if (opt->animations[ANIMATION_TRIGGER_OPEN].script == NULL &&
!opt->no_fading_openclose) {
trigger[number_of_triggers++] = ANIMATION_TRIGGER_OPEN;
@ -459,25 +456,24 @@ void generate_fading_config(struct options *opt) {
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, 0, true)) {
scripts[number_of_scripts++] = fade_in1;
if (set_animation(opt->animations, trigger, number_of_triggers, fade_in1, 0)) {
scripts[number_of_scripts++] = fade_in1.script;
} else {
script_free(fade_in1);
script_free(fade_in1.script);
}
// Fading for opacity change, for these, the blur opacity doesn't change.
asnprintf(&str, &len, FADING_TEMPLATE_1, duration_str);
auto fade_in2 = compile_win_script_from_string(str, output_indices);
struct win_script fade_in2 = {.is_generated = true};
BUG_ON(!compile_win_script_from_string(&fade_in2, str));
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, 0, true)) {
scripts[number_of_scripts++] = fade_in2;
if (set_animation(opt->animations, trigger, number_of_triggers, fade_in2, 0)) {
scripts[number_of_scripts++] = fade_in2.script;
} else {
script_free(fade_in2);
script_free(fade_in2.script);
}
} else {
log_error("Invalid fade-in setting (step: %f, delta: %d), ignoring.",
@ -492,7 +488,8 @@ void generate_fading_config(struct options *opt) {
// Fading out to nothing, i.e. `hide` and `close`
asnprintf(&str, &len, FADING_TEMPLATE_1 FADING_TEMPLATE_2, duration_str,
duration_str, 1, 0);
auto fade_out1 = compile_win_script_from_string(str, output_indices);
struct win_script fade_out1 = {.is_generated = true};
BUG_ON(!compile_win_script_from_string(&fade_out1, str));
number_of_triggers = 0;
if (opt->animations[ANIMATION_TRIGGER_CLOSE].script == NULL &&
!opt->no_fading_openclose) {
@ -501,25 +498,24 @@ void generate_fading_config(struct options *opt) {
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, 0, true)) {
scripts[number_of_scripts++] = fade_out1;
if (set_animation(opt->animations, trigger, number_of_triggers, fade_out1, 0)) {
scripts[number_of_scripts++] = fade_out1.script;
} else {
script_free(fade_out1);
script_free(fade_out1.script);
}
// Fading for opacity change
asnprintf(&str, &len, FADING_TEMPLATE_1, duration_str);
auto fade_out2 = compile_win_script_from_string(str, output_indices);
struct win_script fade_out2 = {.is_generated = true};
BUG_ON(!compile_win_script_from_string(&fade_out2, str));
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, 0, true)) {
scripts[number_of_scripts++] = fade_out2;
if (set_animation(opt->animations, trigger, number_of_triggers, fade_out2, 0)) {
scripts[number_of_scripts++] = fade_out2.script;
} else {
script_free(fade_out2);
script_free(fade_out2.script);
}
} else {
log_error("Invalid fade-out setting (step: %f, delta: %d), ignoring.",

View file

@ -2,27 +2,52 @@ libev = dependency('libev', required: false)
if not libev.found()
libev = cc.find_library('ev')
endif
base_deps = [
cc.find_library('m'),
libev
]
base_deps = [cc.find_library('m'), libev]
srcs = [ files('picom.c', 'c2.c', 'x.c', 'config.c', 'vsync.c',
'diagnostic.c', 'render.c', 'log.c',
'options.c', 'event.c', 'atom.c',
'vblank.c','config_libconfig.c', 'inspect.c', 'api.c') ]
srcs = [
files(
'api.c',
'atom.c',
'c2.c',
'config.c',
'config_libconfig.c',
'diagnostic.c',
'event.c',
'inspect.c',
'log.c',
'options.c',
'picom.c',
'render.c',
'vblank.c',
'vsync.c',
'x.c',
),
]
picom_inc = include_directories(['.', '../include'])
cflags = []
required_xcb_packages = [
'xcb', 'xcb-composite', 'xcb-damage', 'xcb-glx', 'xcb-present', 'xcb-randr',
'xcb-render', 'xcb-shape', 'xcb-sync', 'xcb-xfixes'
'xcb',
'xcb-composite',
'xcb-damage',
'xcb-glx',
'xcb-present',
'xcb-randr',
'xcb-render',
'xcb-shape',
'xcb-sync',
'xcb-xfixes',
]
# Some XCB packages are here because their versioning differs (see check below).
required_packages = [
'pixman-1', 'x11', 'x11-xcb', 'xcb-image', 'xcb-renderutil', 'xcb-util',
'pixman-1',
'x11',
'x11-xcb',
'xcb-image',
'xcb-renderutil',
'xcb-util',
'threads',
]
@ -41,9 +66,11 @@ if not libconfig_dep.found()
cmake = import('cmake')
sub_libconfig_opts = cmake.subproject_options()
sub_libconfig_opts.add_cmake_defines({
'BUILD_SHARED_LIBS': false,
})
sub_libconfig_opts.add_cmake_defines(
{
'BUILD_SHARED_LIBS': false,
},
)
sub_libconfig_opts.set_install(false)
sub_libconfig = cmake.subproject('libconfig', options: sub_libconfig_opts)
base_deps += [sub_libconfig.dependency('config')]
@ -52,7 +79,7 @@ else
endif
if not cc.has_header('uthash.h')
error('Dependency uthash not found')
error('Dependency uthash not found')
endif
deps = []
@ -71,18 +98,18 @@ endif
if get_option('opengl')
cflags += ['-DCONFIG_OPENGL']
deps += [dependency('epoxy', required: true)]
srcs += [ 'opengl.c' ]
srcs += ['opengl.c']
endif
if get_option('dbus')
cflags += ['-DCONFIG_DBUS']
deps += [dependency('dbus-1', required: true)]
srcs += [ 'dbus.c', 'rtkit.c' ]
srcs += ['dbus.c', 'rtkit.c']
endif
if get_option('xrescheck')
cflags += ['-DDEBUG_XRC']
srcs += [ 'xrescheck.c' ]
srcs += ['xrescheck.c']
endif
if get_option('unittest')
@ -92,8 +119,12 @@ endif
host_system = host_machine.system()
if host_system == 'linux'
cflags += ['-DHAS_INOTIFY']
elif (host_system == 'freebsd' or host_system == 'netbsd' or
host_system == 'dragonfly' or host_system == 'openbsd')
elif (
host_system == 'freebsd'
or host_system == 'netbsd'
or host_system == 'dragonfly'
or host_system == 'openbsd'
)
cflags += ['-DHAS_KQUEUE']
endif
@ -105,26 +136,51 @@ subdir('utils')
dl_dep = []
if not cc.has_function('dlopen')
dl_dep = [ cc.find_library('dl', required: true) ]
dl_dep = [cc.find_library('dl', required: true)]
endif
picom = executable('picom', srcs, c_args: cflags,
dependencies: [ base_deps, deps, test_h_dep ] + dl_dep,
install: true, include_directories: picom_inc,
export_dynamic: true, gnu_symbol_visibility: 'hidden')
libtools = static_library(
'libtools',
[
'log.c',
'utils/dynarr.c',
'utils/misc.c',
'utils/str.c',
'transition/curve.c',
'transition/script.c',
],
include_directories: picom_inc,
dependencies: [test_h_dep],
install: false,
build_by_default: false,
)
picom = executable(
'picom',
srcs,
c_args: cflags,
dependencies: [base_deps, deps, test_h_dep] + dl_dep,
install: true,
include_directories: picom_inc,
export_dynamic: true,
gnu_symbol_visibility: 'hidden',
)
if get_option('unittest')
test('picom unittest', picom, args: [ '--unittest' ])
test('picom unittest', picom, args: ['--unittest'])
endif
install_symlink('picom-inspect', install_dir: 'bin', pointing_to: 'picom')
if cc.has_argument('-fsanitize=fuzzer')
c2_fuzz = executable('c2_fuzz', srcs + ['fuzzer/c2.c'],
c_args: cflags + ['-fsanitize=fuzzer', '-DCONFIG_FUZZER'],
link_args: ['-fsanitize=fuzzer'],
dependencies: [ base_deps, deps, test_h_dep ],
build_by_default: false,
install: false, include_directories: picom_inc
)
c2_fuzz = executable(
'c2_fuzz',
srcs + ['fuzzer/c2.c'],
c_args: cflags + ['-fsanitize=fuzzer', '-DCONFIG_FUZZER'],
link_args: ['-fsanitize=fuzzer'],
dependencies: [base_deps, deps, test_h_dep],
build_by_default: false,
install: false,
include_directories: picom_inc,
)
endif

View file

@ -15,46 +15,30 @@ static double curve_sample_linear(const struct curve *this attr_unused, double p
return progress;
}
static void noop_free(const struct curve *this attr_unused) {
static char *curve_linear_to_c(const struct curve * /*this*/) {
return strdup("{.type = CURVE_LINEAR},");
}
static void trivial_free(const struct curve *this) {
free((void *)this);
}
// Cubic bezier interpolator.
//
// Stolen from servo:
// https://searchfox.org/mozilla-central/rev/5da2d56d12/servo/components/style/bezier.rs
static const struct curve static_linear_curve = {
.sample = curve_sample_linear,
.free = noop_free,
};
const struct curve *curve_new_linear(void) {
return &static_linear_curve;
}
/// 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) {
static inline double cubic_bezier_sample_x(const struct curve_cubic_bezier *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) {
static inline double cubic_bezier_sample_y(const struct curve_cubic_bezier *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) {
cubic_bezier_sample_derivative_x(const struct curve_cubic_bezier *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 inline double cubic_bezier_solve_x(const struct curve_cubic_bezier *this, double x) {
static const int NEWTON_METHOD_ITERATIONS = 8;
double t = x;
// Fast path: try Newton's method.
@ -88,47 +72,26 @@ static inline double cubic_bezier_solve_x(const struct cubic_bezier_curve *this,
return t;
}
static double curve_sample_cubic_bezier(const struct curve *base, double progress) {
auto this = (struct cubic_bezier_curve *)base;
static double
curve_sample_cubic_bezier(const struct curve_cubic_bezier *curve, double progress) {
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);
double t = cubic_bezier_solve_x(curve, progress);
return cubic_bezier_sample_y(curve, 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;
static char *curve_cubic_bezier_to_c(const struct curve_cubic_bezier *curve) {
char *buf = NULL;
casprintf(&buf,
"{.type = CURVE_CUBIC_BEZIER, .bezier = { .ax = %a, .bx = %a, "
".cx = %a, .ay = %a, .by = %a, .cy = %a }},",
curve->ax, curve->bx, curve->cx, curve->ay, curve->by, curve->cy);
return buf;
}
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;
static double curve_sample_step(const struct curve_step *this, double progress) {
double y_steps = this->steps - 1 + this->jump_end + this->jump_start,
x_steps = this->steps;
if (progress == 1) {
@ -143,29 +106,28 @@ static double curve_sample_step(const struct curve *base, double progress) {
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;
static char *curve_step_to_c(const struct curve_step *this) {
char *buf = NULL;
casprintf(&buf,
"{.type = CURVE_STEP, .step = { .steps = %d, .jump_start = %s, "
".jump_end = %s }},",
this->steps, this->jump_start ? "true" : "false",
this->jump_end ? "true" : "false");
return buf;
}
const struct curve *parse_linear(const char *str, const char **end, char **err) {
struct curve parse_linear(const char *str, const char **end, char **err) {
*end = str;
*err = NULL;
return &static_linear_curve;
return CURVE_LINEAR_INIT;
}
const struct curve *parse_steps(const char *input_str, const char **out_end, char **err) {
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;
return CURVE_INVALID_INIT;
}
str += 1;
str = skip_space(str);
@ -173,12 +135,12 @@ const struct curve *parse_steps(const char *input_str, const char **out_end, cha
auto steps = strtol(str, &end, 10);
if (end == str || steps > INT_MAX) {
asprintf(err, "Invalid step count at \"%s\".", str);
return NULL;
return CURVE_INVALID_INIT;
}
str = skip_space(end);
if (*str != ',') {
asprintf(err, "Invalid steps argument list \"%s\".", input_str);
return NULL;
return CURVE_INVALID_INIT;
}
str = skip_space(str + 1);
bool jump_start =
@ -187,25 +149,24 @@ const struct curve *parse_steps(const char *input_str, const char **out_end, cha
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;
return CURVE_INVALID_INIT;
}
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;
return CURVE_INVALID_INIT;
}
*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) {
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;
return CURVE_INVALID_INIT;
}
str += 1;
for (int i = 0; i < 4; i++) {
@ -215,13 +176,13 @@ parse_cubic_bezier(const char *input_str, const char **out_end, char **err) {
numbers[i] = strtod_simple(str, &end);
if (end == str) {
asprintf(err, "Invalid number %s.", str);
return NULL;
return CURVE_INVALID_INIT;
}
str = skip_space(end);
const char expected = i == 3 ? ')' : ',';
if (*str != expected) {
asprintf(err, "Invalid cubic-bazier argument list %s.", input_str);
return NULL;
return CURVE_INVALID_INIT;
}
str += 1;
}
@ -229,7 +190,7 @@ parse_cubic_bezier(const char *input_str, const char **out_end, char **err) {
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);
typedef struct curve (*curve_parser)(const char *str, const char **end, char **err);
static const struct {
curve_parser parse;
@ -240,7 +201,7 @@ static const struct {
{parse_steps, "steps"},
};
const struct curve *curve_parse(const char *str, const char **end, char **err) {
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);
@ -249,5 +210,26 @@ const struct curve *curve_parse(const char *str, const char **end, char **err) {
}
}
asprintf(err, "Unknown curve type \"%s\".", str);
return NULL;
return CURVE_INVALID_INIT;
}
double curve_sample(const struct curve *curve, double progress) {
switch (curve->type) {
case CURVE_LINEAR: return curve_sample_linear(curve, progress);
case CURVE_STEP: return curve_sample_step(&curve->step, progress);
case CURVE_CUBIC_BEZIER:
return curve_sample_cubic_bezier(&curve->bezier, progress);
case CURVE_INVALID:
default: unreachable();
}
}
char *curve_to_c(const struct curve *curve) {
switch (curve->type) {
case CURVE_LINEAR: return curve_linear_to_c(curve);
case CURVE_STEP: return curve_step_to_c(&curve->step);
case CURVE_CUBIC_BEZIER: return curve_cubic_bezier_to_c(&curve->bezier);
case CURVE_INVALID:
default: unreachable();
}
}

View file

@ -2,20 +2,51 @@
// Copyright (c) Yuxuan Shui <yshuiv7@gmail.com>
#pragma once
#include <assert.h>
#include <stdbool.h>
// ========================== Interpolators ==========================
struct curve {
/// The interpolator function for an animatable. This function should calculate
/// the current value of the `animatable` based on its `start`, `target`,
/// `duration` and `progress`.
double (*sample)(const struct curve *this, double progress);
/// Free the interpolator.
void (*free)(const struct curve *this);
enum curve_type {
CURVE_LINEAR,
CURVE_CUBIC_BEZIER,
CURVE_STEP,
CURVE_INVALID,
};
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);
const struct curve *curve_parse(const char *str, const char **end, char **err);
struct curve {
enum curve_type type;
union {
struct curve_cubic_bezier {
double ax, bx, cx;
double ay, by, cy;
} bezier;
struct curve_step {
int steps;
bool jump_start, jump_end;
} step;
};
};
static const struct curve CURVE_LINEAR_INIT = {.type = CURVE_LINEAR};
static const struct curve CURVE_INVALID_INIT = {.type = CURVE_INVALID};
static inline struct curve curve_new_cubic_bezier(double x1, double y1, double x2, double y2) {
double cx = 3. * x1;
double bx = 3. * (x2 - x1) - cx;
double cy = 3. * y1;
double by = 3. * (y2 - y1) - cy;
return (struct curve){
.type = CURVE_CUBIC_BEZIER,
.bezier = {.ax = 1. - cx - bx, .bx = bx, .cx = cx, .ay = 1. - cy - by, .by = by, .cy = cy},
};
}
static inline struct curve curve_new_step(int steps, bool jump_start, bool jump_end) {
assert(steps > 0);
return (struct curve){
.type = CURVE_STEP,
.step = {.steps = steps, .jump_start = jump_start, .jump_end = jump_end},
};
}
struct curve curve_parse(const char *str, const char **end, char **err);
/// Calculate the value of the curve at `progress`.
double curve_sample(const struct curve *curve, double progress);
char *curve_to_c(const struct curve *curve);

File diff suppressed because it is too large Load diff

View file

@ -1 +1 @@
srcs += [ files('curve.c', 'script.c') ]
srcs += [files('generated/script_templates.c', 'curve.c', 'preset.c', 'script.c')]

30
src/transition/preset.c Normal file
View file

@ -0,0 +1,30 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Yuxuan Shui <yshuiv7@gmail.com>
#include <libconfig.h>
#include <stdbool.h>
#include "config.h"
#include "preset.h"
#include "script.h"
extern struct {
const char *name;
bool (*func)(struct win_script *output, config_setting_t *setting);
} win_script_presets[];
bool win_script_parse_preset(struct win_script *output, config_setting_t *setting) {
const char *preset = NULL;
if (!config_setting_lookup_string(setting, "preset", &preset)) {
log_error("Missing preset name in script");
return false;
}
for (unsigned i = 0; win_script_presets[i].name; i++) {
if (strcmp(preset, win_script_presets[i].name) == 0) {
log_debug("Using animation preset: %s", preset);
return win_script_presets[i].func(output, setting);
}
}
log_error("Unknown preset: %s", preset);
return false;
}

12
src/transition/preset.h Normal file
View file

@ -0,0 +1,12 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Yuxuan Shui <yshuiv7@gmail.com>
#pragma once
#include <stdbool.h>
typedef struct config_setting_t config_setting_t;
struct win_script;
/// Parse a animation preset definition into a win_script.
bool win_script_parse_preset(struct win_script *output, config_setting_t *setting);

View file

@ -7,69 +7,18 @@
#include <libconfig.h>
#include <uthash.h>
#include "utils/dynarr.h"
#include "utils/list.h"
#include "utils/str.h"
#include "utils/uthash_extra.h"
#include "curve.h"
#include "script.h"
#include "script_internal.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;
};
};
};
#define X(x) [x] = #x,
static const char *op_names[] = {OPERATORS};
#undef X
struct fragment {
struct list_node siblings;
@ -104,50 +53,15 @@ struct compilation_stack {
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;
/// The memory slot for storing the elapsed time.
/// The next slot after this is used for storing the total duration of the script.
unsigned elapsed_slot;
unsigned allocated_slots;
unsigned max_stack;
double max_duration;
const char *current_variable_name;
int *compiled;
struct list_node all_fragments;
@ -175,7 +89,7 @@ static void log_instruction_(enum log_level level, const char *func, unsigned in
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_OP: logv("op %s", op_names[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;
@ -186,78 +100,39 @@ static void log_instruction_(enum log_level level, const char *func, unsigned in
#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;
char *instruction_to_c(struct instruction i) {
char *buf = NULL;
switch (i.type) {
case INST_IMM: casprintf(&buf, "{.type = INST_IMM, .imm = %a},", i.imm); break;
case INST_BRANCH:
casprintf(&buf, "{.type = INST_BRANCH, .rel = %d},", i.rel);
break;
case INST_BRANCH_ONCE:
casprintf(&buf, "{.type = INST_BRANCH_ONCE, .rel = %d},", i.rel);
break;
case INST_HALT: casprintf(&buf, "{.type = INST_HALT},"); break;
case INST_CURVE:;
char *curve = curve_to_c(&i.curve);
casprintf(&buf, "{.type = INST_CURVE, .curve = %s},", curve);
free(curve);
break;
case INST_OP:
casprintf(&buf, "{.type = INST_OP, .op = %s},", op_names[i.op]);
break;
case INST_LOAD:
casprintf(&buf, "{.type = INST_LOAD, .slot = %u},", i.slot);
break;
case INST_STORE:
casprintf(&buf, "{.type = INST_STORE, .slot = %u},", i.slot);
break;
case INST_STORE_OVER_NAN:
casprintf(&buf, "{.type = INST_STORE_OVER_NAN, .slot = %u},", i.slot);
break;
case INST_LOAD_CTX:
casprintf(&buf, "{.type = INST_LOAD_CTX, .ctx = %ld},", i.ctx);
break;
}
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:
/// <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;
}
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;
return buf;
}
static char parse_op(const char *input_str, const char **end, char **err) {
@ -364,6 +239,7 @@ static inline double op_eval(double l, enum op op, double r) {
case OP_MUL: return l * r;
case OP_EXP: return pow(l, r);
case OP_NEG: return -l;
case OP_MAX: return max2(l, r);
}
unreachable();
}
@ -440,7 +316,8 @@ static struct fragment *fragment_new(struct script_compile_context *ctx, unsigne
return fragment;
}
/// Precedence based expression parser.
/// Precedence based expression parser. Prepend fragments to `stack_entry`, or allocate a
/// new one if `stack_entry` is NULL.
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) {
@ -452,9 +329,14 @@ static bool expression_compile(struct compilation_stack **stack_entry, const cha
}
// 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]));
if (!*stack_entry) {
*stack_entry =
calloc(1, sizeof(struct compilation_stack) + sizeof(unsigned[len]));
(*stack_entry)->exit = &fragment->next;
} else {
fragment->next = (*stack_entry)->entry_point;
}
(*stack_entry)->entry_point = fragment;
(*stack_entry)->exit = &fragment->next;
struct expression_parser_context ctx = {
.op_stack = ccalloc(len, char),
@ -513,6 +395,7 @@ end:
free(ctx.op_stack);
if (!succeeded) {
free(*stack_entry);
*stack_entry = NULL;
}
return succeeded;
}
@ -539,45 +422,48 @@ make_imm_stack_entry(struct script_compile_context *ctx, double imm, unsigned sl
return entry;
}
static void compilation_stack_cleanup(struct compilation_stack **stack_entry) {
free(*stack_entry);
*stack_entry = NULL;
}
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;
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;
const char *str = NULL;
if (config_setting_lookup_string(setting, "curve", &str)) {
curve = curve_parse(str, &str, &err);
if (curve.type == CURVE_INVALID) {
asprintf(out_err, "Cannot parse curve at line %d: %s",
config_setting_source_line(setting), err);
free(err);
return false;
}
} else {
curve = CURVE_LINEAR_INIT;
}
if (config_setting_lookup_bool(setting, "reset", &boolean)) {
reset = boolean;
}
BUG_ON(ctx->allocated_slots > UINT_MAX - 1);
// The start value must take a slot, because it's overridable.
auto start_slot = ctx->allocated_slots;
auto end_slot = ctx->allocated_slots + 1;
ctx->allocated_slots += 2;
ctx->allocated_slots += 1;
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;
cleanup(compilation_stack_cleanup) 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)) {
@ -593,35 +479,124 @@ transition_compile(struct compilation_stack **stack_entry, config_setting_t *set
return false;
}
// 0 = end, 1 = duration, 2 = delay
struct instruction load_parameters[3];
if (config_setting_lookup_float(setting, "end", &number)) {
end = make_imm_stack_entry(ctx, number, end_slot, false);
load_parameters[0] = (struct instruction){
.type = INST_IMM,
.imm = number,
};
} 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;
} else {
BUG_ON(ctx->allocated_slots > UINT_MAX - 1);
auto end_slot = ctx->allocated_slots++;
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;
}
load_parameters[0] = (struct instruction){
.type = INST_LOAD,
.slot = end_slot,
};
}
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;
if (config_setting_lookup_float(setting, "duration", &number)) {
if (number == 0) {
asprintf(out_err, "Duration must be greater than 0. Line %d.",
config_setting_source_line(setting));
return false;
}
load_parameters[1] = (struct instruction){
.type = INST_IMM,
.imm = number,
};
} else if (!config_setting_lookup_string(setting, "duration", &str)) {
asprintf(out_err,
"Transition definition does not contain a duration value or "
"expression. Line %d.",
config_setting_source_line(setting));
return false;
} else {
BUG_ON(ctx->allocated_slots > UINT_MAX - 1);
auto duration_slot = ctx->allocated_slots++;
if (!expression_compile(&end, str, ctx, duration_slot, false, &err)) {
asprintf(out_err, "Transition has an invalid duration expression: %s. Line %d",
err, config_setting_source_line(setting));
free(err);
return false;
}
load_parameters[1] = (struct instruction){
.type = INST_LOAD,
.slot = duration_slot,
};
}
if (config_setting_lookup_float(setting, "delay", &number)) {
load_parameters[2] = (struct instruction){
.type = INST_IMM,
.imm = number,
};
} else if (!config_setting_lookup_string(setting, "delay", &str)) {
load_parameters[2] = (struct instruction){
.type = INST_IMM,
.imm = 0,
};
} else {
BUG_ON(ctx->allocated_slots > UINT_MAX - 1);
auto delay_slot = ctx->allocated_slots++;
if (!expression_compile(&end, str, ctx, delay_slot, false, &err)) {
asprintf(out_err, "Transition has an invalid delay expression: %s. Line %d",
err, config_setting_source_line(setting));
free(err);
return false;
}
load_parameters[2] = (struct instruction){
.type = INST_LOAD,
.slot = delay_slot,
};
}
// clang-format off
struct instruction instrs[] = {
load_parameters[0],
{.type = INST_LOAD, .slot = start_slot},
{.type = INST_OP, .op = OP_SUB}, // v0 = end - start
{.type = INST_LOAD, .slot = ctx->elapsed_slot},
load_parameters[2],
{.type = INST_OP, .op = OP_SUB}, // v1 = elapsed - delay
load_parameters[1],
{.type = INST_OP, .op = OP_DIV}, // v2 = v1 / duration
{.type = INST_CURVE, .curve = curve}, // v3 = curve(v2)
{.type = INST_OP, .op = OP_MUL}, // v4 = v0 * v3
{.type = INST_LOAD, .slot = start_slot},
{.type = INST_OP, .op = OP_ADD}, // v5 = v4 + start
{.type = INST_STORE, .slot = slot}, // memory[slot] = v5
};
// Instructs for calculating the total duration of the transition
struct instruction total_duration_instrs[] = {
load_parameters[1],
load_parameters[2],
{.type = INST_OP, .op = OP_ADD}, // v0 = duration + delay
{.type = INST_LOAD, .slot = ctx->elapsed_slot + 1},
{.type = INST_OP, .op = OP_MAX}, // v1 = max(v0, total_duration)
{.type = INST_STORE, .slot = ctx->elapsed_slot + 1},
};
// clang-format on
if (ctx->max_stack < 3) {
// The list of instructions above needs 3 stack slots
ctx->max_stack = 3;
}
struct fragment *fragment = fragment_new(ctx, ARR_SIZE(instrs));
memcpy(fragment->instrs, instrs, sizeof(instrs));
fragment->ninstrs = ARR_SIZE(instrs);
@ -653,13 +628,14 @@ transition_compile(struct compilation_stack **stack_entry, config_setting_t *set
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.
// The `end` block includes `end`, `duration`, and `delay` values.
if (end != NULL && end->ndeps > 0) {
// If we get here, the end/duration/delay values are 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 these values, 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;
@ -681,25 +657,31 @@ transition_compile(struct compilation_stack **stack_entry, config_setting_t *set
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;
if (end != NULL) {
// 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);
// This must happen _after_ the `end` block.
struct fragment *total_duration_fragment = fragment_new(ctx, ARR_SIZE(instrs));
memcpy(total_duration_fragment->instrs, total_duration_instrs,
sizeof(total_duration_instrs));
total_duration_fragment->ninstrs = ARR_SIZE(total_duration_instrs);
*ctx->once_end_tail = total_duration_fragment;
ctx->once_end_tail = &total_duration_fragment->next;
return true;
}
static void instruction_deinit(struct instruction *instr) {
if (instr->type == INST_CURVE) {
instr->curve->free(instr->curve);
}
static void instruction_deinit(struct instruction * /*instr*/) {
}
static void fragment_free(struct fragment *frag) {
@ -945,20 +927,40 @@ script_compile_context_init(struct script_compile_context *ctx, config_setting_t
HASH_ADD_STR(ctx->vars, name, alloc);
}
ctx->allocated_slots = n;
ctx->allocated_slots = n + 2;
ctx->elapsed_slot = 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;
ctx->once_end_head = fragment_new(ctx, 2);
ctx->once_end_head->instrs[0] = (struct instruction){
.type = INST_IMM,
.imm = 0,
};
ctx->once_end_head->instrs[1] = (struct instruction){
.type = INST_STORE,
.slot = ctx->elapsed_slot + 1,
};
ctx->once_end_head->ninstrs = 2;
ctx->once_end_tail = &ctx->once_end_head->next;
ctx->max_stack = 1;
}
unsigned script_elapsed_slot(const struct script *script) {
return script->elapsed_slot;
}
unsigned script_total_duration_slot(const struct script *script) {
return script->elapsed_slot + 1;
}
struct script *
script_compile(config_setting_t *setting, struct script_parse_config cfg, char **out_err) {
if (!config_setting_is_group(setting)) {
casprintf(out_err, "Script setting must be a group");
return NULL;
}
struct script_context_info_internal *context_table = NULL;
@ -1033,13 +1035,14 @@ script_compile(config_setting_t *setting, struct script_parse_config cfg, char *
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->elapsed_slot = ctx.elapsed_slot;
script->n_slots = 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);
log_debug("Compiled script at line %d, total instructions: %d, "
"slots: %d, stack size: %d, memory[%u] = total duration, memory[%u] = "
"elapsed",
config_setting_source_line(setting), script->len, script->n_slots,
script->stack_size, script->elapsed_slot + 1, script->elapsed_slot);
if (log_get_level_tls() <= LOG_LEVEL_DEBUG) {
log_debug("Output mapping:");
HASH_ITER2(ctx.vars, var) {
@ -1057,17 +1060,100 @@ script_compile(config_setting_t *setting, struct script_parse_config cfg, char *
return script;
}
char *script_to_c(const struct script *script, const struct script_output_info *outputs) {
char **buf = dynarr_new(char *, script->len * 40);
char *tmp = NULL;
casprintf(&tmp, "{\n"
" static const struct instruction instrs[] = {\n");
dynarr_push(buf, tmp);
for (unsigned i = 0; i < script->len; i++) {
dynarr_push(buf, instruction_to_c(script->instrs[i]));
}
casprintf(&tmp,
" };\n struct script *ret = \n"
" malloc(offsetof(struct script, instrs) + sizeof(instrs));\n"
" ret->len = ARR_SIZE(instrs); ret->elapsed_slot = %u;"
" ret->n_slots = %u; ret->stack_size = %u;\n"
" ret->vars = NULL; ret->overrides = NULL;\n"
" memcpy(ret->instrs, instrs, sizeof(instrs));\n",
script->elapsed_slot, script->n_slots, script->stack_size);
dynarr_push(buf, tmp);
struct variable_allocation *var, *next_var;
HASH_ITER(hh, script->vars, var, next_var) {
char *var_str = NULL;
casprintf(&var_str,
" {\n"
" struct variable_allocation *var = \n"
" malloc(sizeof(*var));\n"
" *var = (struct variable_allocation){\n"
" .name = strdup(\"%s\"), .slot = %u, .index = %u\n"
" };\n"
" HASH_ADD_STR(ret->vars, name, var);\n"
" }\n",
var->name, var->slot, var->index);
dynarr_push(buf, var_str);
}
struct overridable_slot *override, *next_override;
HASH_ITER(hh, script->overrides, override, next_override) {
char *override_str = NULL;
casprintf(&override_str,
" {\n"
" struct overridable_slot *override = \n"
" malloc(sizeof(*override));\n"
" *override = (struct overridable_slot){\n"
" .name = strdup(\"%s\"), .slot = %u\n"
" };\n"
" HASH_ADD_STR(ret->overrides, name, override);\n"
" }\n",
override->name, override->slot);
dynarr_push(buf, override_str);
}
for (size_t i = 0; outputs && outputs[i].name; i++) {
struct variable_allocation *alloc = NULL;
HASH_FIND_STR(script->vars, outputs[i].name, alloc);
if (alloc) {
casprintf(&tmp, " output_slots[%zu] = %u;\n", i, alloc->slot);
} else {
casprintf(&tmp, " output_slots[%zu] = -1;\n", i);
}
dynarr_push(buf, tmp);
}
casprintf(&tmp, " return ret;\n}\n");
dynarr_push(buf, tmp);
return dynarr_join(buf, "");
}
void script_specialize(struct script *script,
const struct script_specialization_context *spec, unsigned n_context) {
for (unsigned i = 0; i < script->len; i++) {
if (script->instrs[i].type != INST_LOAD_CTX) {
continue;
}
for (unsigned j = 0; j < n_context; j++) {
if (script->instrs[i].ctx == spec[j].offset) {
script->instrs[i].type = INST_IMM;
script->instrs[i].imm = spec[j].value;
break;
}
}
}
}
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);
unsigned memory_size = max2(1, script->n_slots + 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++) {
for (unsigned i = 0; i < script->n_slots; i++) {
instance->memory[i] = NAN;
}
instance->memory[script->elapsed_slot] = 0;
return instance;
}
@ -1084,17 +1170,13 @@ void script_instance_resume_from(struct script_instance *old, struct script_inst
}
}
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];
auto stack = (double *)&instance->memory[script->n_slots];
unsigned top = 0;
double l, r;
bool do_branch_once = instance->elapsed == 0;
bool do_branch_once = instance->memory[script->elapsed_slot] == 0;
for (auto i = script->instrs;; i++) {
switch (i->type) {
case INST_IMM: stack[top++] = i->imm; break;
@ -1132,9 +1214,10 @@ script_instance_evaluate(struct script_instance *instance, void *context) {
}
break;
case INST_CURVE:
l = (instance->elapsed - i->delay) / i->duration;
BUG_ON(top < 1);
l = stack[top - 1];
l = min2(max2(0, l), 1);
stack[top++] = i->curve->sample(i->curve, l);
stack[top - 1] = curve_sample(&i->curve, l);
break;
}
if (top && safe_isnan(stack[top - 1])) {
@ -1171,23 +1254,28 @@ TEST_CASE(scripts_1) {
c = \"(b - 1) * (a+1)\";\
d = \"- e - 1\"; \
e : { \
timing = \"10s cubic-bezier(0.5,0.5, 0.5, 0.5) 0.5s\"; \
curve = \"cubic-bezier(0.5,0.5, 0.5, 0.5)\"; \
duration = \"a\"; \
delay = 0.5; \
start = 10; \
end = \"2 * c\"; \
}; \
f : { \
timing = \"10s cubic-bezier(0.1,0.2, 0.3, 0.4) 0.5s\"; \
curve = \"cubic-bezier(0.1,0.2, 0.3, 0.4)\"; \
duration = 10; \
delay = 0.5; \
start = \"e + 1\"; \
end = \"f - 1\"; \
}; \
neg = \"-a\"; \
timing1 : { \
timing = \"10s\"; \
duration = 10; \
start = 1; \
end = 0; \
};\
timing2 : { \
timing = \"10s steps(1, jump-start)\"; \
curve = \"steps(1, jump-start)\"; \
duration = 10; \
start = 1; \
end = 0; \
};";
@ -1209,6 +1297,7 @@ TEST_CASE(scripts_1) {
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[script->elapsed_slot + 1], 10.5);
TEST_EQUAL(instance->memory[outputs[0].slot], 10);
TEST_EQUAL(instance->memory[outputs[1].slot], 20);
TEST_EQUAL(instance->memory[outputs[2].slot], 209);
@ -1216,12 +1305,12 @@ TEST_CASE(scripts_1) {
TEST_EQUAL(instance->memory[outputs[4].slot], 10);
TEST_TRUE(!script_instance_is_finished(instance));
instance->elapsed += 5.5;
instance->memory[instance->script->elapsed_slot] += 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;
instance->memory[instance->script->elapsed_slot] += 5.5;
result = script_instance_evaluate(instance, NULL);
TEST_EQUAL(result, SCRIPT_EVAL_OK);
TEST_EQUAL(instance->memory[outputs[0].slot], 10);
@ -1252,21 +1341,20 @@ 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 = { curve = \"asdf\";};", "Cannot parse curve at line 1: Unknown curve "
"type \"asdf\"."},
{"a = { curve = \"steps(a)\";};", "Cannot parse curve at line 1: Invalid "
"step count at \"a)\"."},
{"a = { curve = \"steps(1)\";};", "Cannot parse curve at line 1: Invalid "
"steps argument list \"(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."},
{"a = {};", "Transition definition does not contain a start value or "
"expression. Line 1."},
{"a = { duration = 0; start = 0; end = 0; };", "Duration must be greater "
"than 0. Line 1."},
};
char *err = NULL;
struct script *script = NULL;

View file

@ -14,6 +14,11 @@ struct script_context_info {
ptrdiff_t offset;
};
struct script_specialization_context {
ptrdiff_t offset;
double value;
};
struct script_output_info {
const char *name;
/// Slot for this variable, -1 if this variable doesn't exist.
@ -29,7 +34,6 @@ struct script_parse_config {
struct script;
struct script_instance {
const struct script *script;
double elapsed;
double memory[];
};
enum script_evaluation_result {
@ -44,12 +48,13 @@ typedef struct config_setting_t config_setting_t;
static_assert(alignof(double) > alignof(unsigned), "double/unsigned has unexpected "
"alignment");
#define SCRIPT_CTX_PLACEHOLDER_BASE (0x40000000)
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
@ -62,3 +67,29 @@ bool script_instance_is_finished(const struct script_instance *instance);
/// 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);
/// Get the total duration slot of a script.
unsigned script_total_duration_slot(const struct script *script);
unsigned script_elapsed_slot(const struct script *script);
/// Specialize a script instance with a context. During evaluation of the resulting
/// script, what would have been read from the context will be replaced with the hardcoded
/// value in the specialization context.
void script_specialize(struct script *instance,
const struct script_specialization_context *context,
unsigned n_context);
/// Check if a script instance has finished. The script instance must have been evaluated
/// at least once.
static inline bool script_instance_is_finished(const struct script_instance *instance) {
return instance->memory[script_elapsed_slot(instance->script)] >=
instance->memory[script_total_duration_slot(instance->script)];
}
/// Generate code for a C function that will return a script identical to `script` when
/// called. The generated function will take a `int *output_slots` parameter, which it
/// will fill in, based on `outputs` passed to this function. Specifically, the generated
/// function will fill in `output_slots[i]` with the slot number of the output variable
/// named `outputs[i].name`. The generated function will return a pointer to the script.
/// This function only generates the function body, you need to provide the function
/// signature and the function name yourself.
char *script_to_c(const struct script *script, const struct script_output_info *outputs);

View file

@ -0,0 +1,98 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Yuxuan Shui <yshuiv7@gmail.com>
#pragma once
#include <uthash.h>
#include "curve.h"
#define OPERATORS \
X(OP_ADD) \
X(OP_SUB) \
X(OP_MUL) \
X(OP_DIV) \
/* Exponent */ \
X(OP_EXP) \
/* Negation */ \
X(OP_NEG) \
X(OP_MAX)
#define X(x) x,
enum op { OPERATORS };
#undef X
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,
/// Pop a value from the top of the stack, clamp its value to [0, 1], then
/// evaluate a curve at that point, 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,
};
/// 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;
};
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 curve curve;
};
};
/// 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 n_slots;
/// The memory slot for storing the elapsed time.
/// The next slot after this is used for storing the total duration of the script.
unsigned elapsed_slot;
unsigned stack_size;
struct variable_allocation *vars;
struct overridable_slot *overrides;
struct instruction instrs[];
};

26
src/utils/dynarr.c Normal file
View file

@ -0,0 +1,26 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Yuxuan Shui <yshuiv7@gmail.com>
#include "dynarr.h"
char *dynarr_join(char **arr, const char *sep) {
size_t total_len = 0;
dynarr_foreach(arr, i) {
total_len += strlen(*i);
}
char *ret = malloc(total_len + strlen(sep) * (dynarr_len(arr) - 1) + 1);
size_t pos = 0;
allocchk(ret);
dynarr_foreach(arr, i) {
if (i != arr) {
strcpy(ret + pos, sep);
pos += strlen(sep);
}
strcpy(ret + pos, *i);
pos += strlen(*i);
free(*i);
}
dynarr_free_pod(arr);
ret[pos] = '\0';
return ret;
}

View file

@ -72,7 +72,7 @@ static inline void dynarr_remove_swap_impl(size_t size, void *arr, size_t idx) {
#define dynarr_free(arr, dtor) \
do { \
dynarr_clear(arr, dtor); \
free((struct dynarr_header *)(arr)-1); \
free((struct dynarr_header *)(arr) - 1); \
(arr) = NULL; \
} while (0)
#define dynarr_free_pod(arr) dynarr_free(arr, (void (*)(typeof(arr)))NULL)
@ -86,7 +86,7 @@ static inline void dynarr_remove_swap_impl(size_t size, void *arr, size_t idx) {
#define dynarr_resize(arr, newlen, init, dtor) \
do { \
BUG_ON((arr) == NULL); \
dynarr_reserve((arr), (newlen)-dynarr_len(arr)); \
dynarr_reserve((arr), (newlen) - dynarr_len(arr)); \
if ((init) != NULL) { \
for (size_t i = dynarr_len(arr); i < (newlen); i++) { \
(init)((arr) + i); \
@ -121,9 +121,9 @@ static inline void dynarr_remove_swap_impl(size_t size, void *arr, size_t idx) {
#define dynarr_remove_swap(arr, idx) \
dynarr_remove_swap_impl(sizeof(typeof(*(arr))), (void *)(arr), idx)
/// Return the length of the array
#define dynarr_len(arr) (((struct dynarr_header *)(arr)-1)->len)
#define dynarr_len(arr) (((struct dynarr_header *)(arr) - 1)->len)
/// Return the capacity of the array
#define dynarr_cap(arr) (((struct dynarr_header *)(arr)-1)->cap)
#define dynarr_cap(arr) (((struct dynarr_header *)(arr) - 1)->cap)
/// Return the last element of the array
#define dynarr_last(arr) ((arr)[dynarr_len(arr) - 1])
/// Return the pointer just past the last element of the array
@ -183,3 +183,7 @@ static inline void dynarr_remove_swap_impl(size_t size, void *arr, size_t idx) {
} \
dynarr_find_ret; \
})
/// Concatenate a dynarr of strings into a single string, separated by `sep`. The array
/// will be freed by this function.
char *dynarr_join(char **arr, const char *sep);

View file

@ -1 +1,11 @@
srcs += [ files('cache.c', 'file_watch.c', 'kernel.c', 'statistics.c', 'str.c', 'misc.c') ]
srcs += [
files(
'cache.c',
'dynarr.c',
'file_watch.c',
'kernel.c',
'misc.c',
'statistics.c',
'str.c',
),
]

View file

@ -4,6 +4,7 @@
#pragma once
#include <ctype.h>
#include <math.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
@ -93,3 +94,15 @@ static inline bool starts_with(const char *str, const char *needle, bool ignore_
/// reallocates it if it's not big enough.
int asnprintf(char **strp, size_t *capacity, const char *fmt, ...)
__attribute__((format(printf, 3, 4)));
/// Like `asprintf`, but it aborts the program if memory allocation fails.
static inline size_t __attribute__((format(printf, 2, 3)))
casprintf(char **strp, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
int ret = vasprintf(strp, fmt, ap);
va_end(ap);
BUG_ON(ret < 0);
return (size_t)ret;
}

View file

@ -1773,7 +1773,9 @@ bool win_process_animation_and_state_change(struct session *ps, struct win *w, d
log_verbose("Advance animation for %#010x (%s) %f seconds", win_id(w),
w->name, delta_t);
if (!script_instance_is_finished(w->running_animation_instance)) {
w->running_animation_instance->elapsed += delta_t;
auto elapsed_slot =
script_elapsed_slot(w->running_animation_instance->script);
w->running_animation_instance->memory[elapsed_slot] += delta_t;
auto result = script_instance_evaluate(
w->running_animation_instance, &win_ctx);
if (result != SCRIPT_EVAL_OK) {

View file

@ -261,6 +261,8 @@ struct win_script_context {
double monitor_x, monitor_y;
double monitor_width, monitor_height;
};
static_assert(SCRIPT_CTX_PLACEHOLDER_BASE > sizeof(struct win_script_context),
"win_script_context too large");
static const struct script_context_info win_script_context_info[] = {
{"window-x", offsetof(struct win_script_context, x)},

View file

@ -427,9 +427,33 @@ window-shader-fg-rule =
animations = ({
triggers = ["close", "hide"];
offset-y = {
timing = "0.2s linear";
start = 0;
duration = 0.2;
end = "- window-height - window-y";
};
opacity = 1;
})
}, {
triggers = ["open"];
preset = "slide-in";
duration = 1;
}, {
triggers = ["open"];
preset = "slide-out";
duration = 1;
}, {
triggers = ["open"];
preset = "fly-in";
duration = 1;
}, {
triggers = ["open"];
preset = "fly-out";
duration = 1;
}, {
triggers = ["open"];
preset = "appear";
duration = 1;
}, {
triggers = ["open"];
preset = "disappear";
duration = 1;
});

540
tools/animgen.c Normal file
View file

@ -0,0 +1,540 @@
#include <libconfig.h>
#include <uthash.h>
#include "compiler.h" // IWYU pragma: keep
#include "transition/script.h"
#include "transition/script_internal.h"
#include "utils/dynarr.h"
#include "wm/win.h"
enum knob_type {
KNOB_NUMBER,
KNOB_CHOICE,
};
struct knob {
UT_hash_handle hh;
const char *name;
enum knob_type type;
union {
struct {
double default_value;
} number;
struct {
char **choices;
unsigned n_choices;
unsigned default_choice;
};
};
bool emitted;
};
struct placeholder {
struct knob *source;
double *value_for_choices;
};
bool config_extra_get_float(config_setting_t *setting, double *value) {
if (config_setting_type(setting) != CONFIG_TYPE_FLOAT &&
config_setting_type(setting) != CONFIG_TYPE_INT &&
config_setting_type(setting) != CONFIG_TYPE_INT64) {
return false;
}
*value = config_setting_get_float(setting);
return true;
}
bool config_extra_get_int(config_setting_t *setting, int *value) {
if (config_setting_type(setting) != CONFIG_TYPE_INT &&
config_setting_type(setting) != CONFIG_TYPE_INT64) {
return false;
}
*value = config_setting_get_int(setting);
return true;
}
char *sanitized_name(const char *name) {
char *ret = strdup(name);
for (char *p = ret; *p; p++) {
if (*p == '-') {
*p = '_';
}
}
return ret;
}
void free_charp(void *p) {
free(*(char **)p);
}
#define scopedp(type) cleanup(free_##type##p) type *
#define MAX_PLACEHOLDERS 10
void codegen(const char *name, const char *body, const struct placeholder *placeholders) {
auto ident = sanitized_name(name);
printf("static struct script *script_template__%s(int *output_slots)\n%s\n",
ident, body);
printf("static bool win_script_preset__%s(struct win_script *output, "
"config_setting_t *setting) {\n",
ident);
printf(" output->script = script_template__%s(output->output_indices);\n", ident);
for (size_t i = 0; i < MAX_PLACEHOLDERS; i++) {
if (placeholders[i].source == NULL || placeholders[i].source->emitted) {
continue;
}
auto knob = placeholders[i].source;
scopedp(char) knob_ident = sanitized_name(knob->name);
knob->emitted = true;
if (knob->type == KNOB_NUMBER) {
printf(" double knob_%s = %a;\n", knob_ident,
knob->number.default_value);
printf(" config_setting_lookup_float(setting, \"%s\", "
"&knob_%s);\n",
knob->name, knob_ident);
continue;
}
printf(" const char *knob_%s = \"%s\";\n", knob_ident,
knob->choices[knob->default_choice]);
printf(" config_setting_lookup_string(setting, \"%s\", &knob_%s);\n",
knob->name, knob_ident);
for (unsigned j = 0; j < MAX_PLACEHOLDERS; j++) {
if (placeholders[j].source != knob) {
continue;
}
printf(" double placeholder%u_%s;\n", j, knob_ident);
}
for (unsigned j = 0; j < knob->n_choices; j++) {
printf(" if (strcmp(knob_%s, \"%s\") == 0) {\n", knob_ident,
knob->choices[j]);
for (unsigned k = 0; k < MAX_PLACEHOLDERS; k++) {
if (placeholders[k].source != knob) {
continue;
}
printf(" placeholder%u_%s = %a;\n", k, knob_ident,
placeholders[k].value_for_choices[j]);
}
printf(" } else ");
}
printf("{\n");
printf(" log_error(\"Invalid choice \\\"%%s\\\" for "
"option \\\"%s\\\". Line %%d.\", knob_%s, "
"config_setting_source_line(config_setting_get_member(setting, "
"\"%s\")));\n",
knob->name, knob_ident, knob->name);
printf(" log_error(\" Valid ones are: ");
for (unsigned j = 0; j < knob->n_choices; j++) {
printf("%s\\\"%s\\\"", j ? ", " : "", knob->choices[j]);
}
printf("\");\n");
printf(" script_free(output->script);\n");
printf(" output->script = NULL;\n");
printf(" return false;\n");
printf(" }\n");
}
printf(" struct script_specialization_context spec[] = {\n");
for (size_t i = 0; i < 10; i++) {
if (placeholders[i].source == NULL) {
continue;
}
auto knob = placeholders[i].source;
auto knob_ident = sanitized_name(knob->name);
if (knob->type == KNOB_NUMBER) {
printf(" {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + %zu, "
".value = knob_%s},\n",
i * 4, knob_ident);
} else {
printf(" {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + %zu, "
".value = placeholder%zu_%s},\n",
i * 4, i, knob_ident);
}
free(knob_ident);
}
printf(" };\n");
printf(" script_specialize(output->script, spec, ARR_SIZE(spec));\n");
printf(" return true;\n");
printf("}\n");
free(ident);
}
/// Syntax for defining knobs and placeholders:
///
/// {
/// # other settings...
/// # ....
///
/// __knobs = {
/// knob1 = 0.5; # knob1 is a number, default value 0.5
///
/// # knob2 is a choice, default choice is "default_choice" (index 2)
/// # ┌----- index of the default choice
/// # v
/// knob2 = (2, ["choice1", "choice2", "default_choice"]);
/// };
/// __placeholders = (
/// #┌----- index of the placeholder
/// #v
/// (1, "knob1"), # placeholder1 takes value from knob1
///
/// # placeholder2 takes value from knob2. Because knob2 is a choice,
/// # we need to provide a mapping from choice to value.
/// (2, "knob2", [1, 2, 0]);
/// );
/// }
static bool parse_knobs(const char *preset_name, config_setting_t *knob_settings,
config_setting_t *placeholder_settings, struct knob *knobs,
struct placeholder *placeholders) {
struct knob *knobs_by_name = NULL;
if (config_setting_length(knob_settings) > MAX_PLACEHOLDERS) {
fprintf(stderr, "Too many knobs in %s, max %d allowed\n", preset_name,
MAX_PLACEHOLDERS);
return false;
}
if (config_setting_length(placeholder_settings) > MAX_PLACEHOLDERS) {
fprintf(stderr, "Too many placeholders in %s, max %d allowed\n",
preset_name, MAX_PLACEHOLDERS);
return false;
}
unsigned n_knobs = 0;
for (unsigned i = 0; i < (unsigned)config_setting_length(knob_settings); i++) {
auto config = config_setting_get_elem(knob_settings, i);
const char *name = config_setting_name(config);
double default_value;
auto knob = &knobs[n_knobs++];
knob->name = strdup(name);
if (config_extra_get_float(config, &default_value)) {
knob->type = KNOB_NUMBER;
knob->number.default_value = default_value;
HASH_ADD_STR(knobs_by_name, name, knob);
n_knobs++;
continue;
}
if (!config_setting_is_list(config) || config_setting_length(config) != 2) {
fprintf(stderr,
"Invalid placeholder %s in %s, line %d. It must be a "
"list of length 2.\n",
name, preset_name, config_setting_source_line(config));
continue;
}
int default_choice;
config_setting_t *choices = config_setting_get_elem(config, 1);
if (!config_extra_get_int(config_setting_get_elem(config, 0), &default_choice) ||
choices == NULL || !config_setting_is_array(choices)) {
fprintf(stderr,
"Invalid placeholder %s in %s, line %d. Failed to get "
"elements.\n",
name, preset_name, config_setting_source_line(config));
continue;
}
auto n_choices = (unsigned)config_setting_length(choices);
if (default_choice < 0 || (unsigned)default_choice >= n_choices) {
fprintf(stderr,
"Invalid knob choice in %s, knob %s line %d. Default "
"choice out of range.\n",
preset_name, name, config_setting_source_line(config));
continue;
}
knob->type = KNOB_CHOICE;
knob->n_choices = 0;
knob->choices = malloc(n_choices * sizeof(char *));
knob->default_choice = (unsigned)default_choice;
bool has_error = false;
for (unsigned j = 0; j < n_choices; j++) {
auto choice =
config_setting_get_string(config_setting_get_elem(choices, j));
if (choice == NULL) {
fprintf(stderr,
"Invalid knob choice in %s, knob %s line %d. "
"Failed to get choice.\n",
preset_name, name,
config_setting_source_line(config));
has_error = true;
break;
}
for (unsigned k = 0; k < j; k++) {
if (strcmp(knob->choices[k], choice) == 0) {
fprintf(stderr,
"Invalid knob choice in %s, knob %s line "
"%d. Duplicate choice %s.\n",
preset_name, name,
config_setting_source_line(config), choice);
has_error = true;
break;
}
}
if (has_error) {
break;
}
knob->choices[knob->n_choices++] = strdup(choice);
}
if (has_error) {
for (unsigned j = 0; j < knob->n_choices; j++) {
free(knob->choices[j]);
}
free(knob->choices);
free((void *)knob->name);
knob->choices = NULL;
knob->name = NULL;
continue;
}
HASH_ADD_STR(knobs_by_name, name, knob);
n_knobs++;
}
for (unsigned i = 0; i < (unsigned)config_setting_length(placeholder_settings); i++) {
auto config = config_setting_get_elem(placeholder_settings, i);
if (!config_setting_is_list(config) || config_setting_length(config) < 2) {
fprintf(stderr,
"Invalid placeholder in preset %s, line %d. Must be a "
"non-empty list.\n",
preset_name, config_setting_source_line(config));
continue;
}
int index;
if (!config_extra_get_int(config_setting_get_elem(config, 0), &index)) {
fprintf(stderr,
"Invalid placeholder in preset %s, line %d. Index must "
"be an integer.\n",
preset_name, config_setting_source_line(config));
continue;
}
auto placeholder = &placeholders[index];
if (placeholder->source) {
fprintf(stderr,
"Invalid placeholder in preset %s, line %d. Placeholder "
"with index %d already defined.\n",
preset_name, config_setting_source_line(config), index);
continue;
}
BUG_ON(placeholder->value_for_choices != NULL);
const char *source =
config_setting_get_string(config_setting_get_elem(config, 1));
struct knob *knob;
HASH_FIND_STR(knobs_by_name, source, knob);
if (!knob) {
fprintf(stderr,
"Invalid placeholder%d definition in %s, line "
"%d. Source knob %s not found.\n",
index, preset_name, config_setting_source_line(config),
source);
continue;
}
if (config_setting_length(config) == 2) {
if (knob->type != KNOB_NUMBER) {
fprintf(stderr,
"Invalid placeholder%d definition in %s, line "
"%d. Source knob %s is not a number.\n",
index, preset_name,
config_setting_source_line(config), source);
continue;
}
placeholder->source = knob;
} else if (config_setting_length(config) == 3) {
config_setting_t *value_for_choices =
config_setting_get_elem(config, 2);
if (value_for_choices == NULL ||
!config_setting_is_array(value_for_choices)) {
fprintf(stderr,
"Invalid placeholder%d definition in %s, line "
"%d. Failed to get elements.\n",
index, preset_name,
config_setting_source_line(config));
continue;
}
if (knob->type != KNOB_CHOICE) {
fprintf(stderr,
"Invalid placeholder%d definition in %s, line "
"%d. Source knob %s is not a choice.\n",
index, preset_name,
config_setting_source_line(config), source);
continue;
}
if (knob->n_choices !=
(unsigned)config_setting_length(value_for_choices)) {
fprintf(stderr,
"Invalid placeholder%d definition in %s, line "
"%d. Number of choices doesn't match.\n",
index, preset_name,
config_setting_source_line(config));
continue;
}
placeholder->value_for_choices =
malloc(sizeof(double) * knob->n_choices);
for (unsigned j = 0; j < knob->n_choices; j++) {
double value;
if (!config_extra_get_float(
config_setting_get_elem(value_for_choices, j), &value)) {
fprintf(stderr,
"Invalid placeholder%d definition in %s, "
"line %d. Failed to get value.\n",
index, preset_name,
config_setting_source_line(config));
free(placeholder->value_for_choices);
placeholder->value_for_choices = NULL;
break;
}
placeholder->value_for_choices[j] = value;
}
if (placeholder->value_for_choices == NULL) {
continue;
}
placeholder->source = knob;
} else {
fprintf(stderr,
"Invalid placeholder%d definition in %s, line %d. "
"Excessive elements.\n",
index, preset_name, config_setting_source_line(config));
continue;
}
}
struct knob *k, *nk;
HASH_ITER(hh, knobs_by_name, k, nk) {
HASH_DEL(knobs_by_name, k);
}
return true;
}
int main(int argc, const char **argv) {
if (argc != 2) {
return 1;
}
log_init_tls();
char **presets = dynarr_new(char *, 10);
config_t cfg;
config_init(&cfg);
config_set_auto_convert(&cfg, 1);
if (!config_read_file(&cfg, argv[1])) {
fprintf(stderr, "Failed to read config file %s: %s\n", argv[1],
config_error_text(&cfg));
config_destroy(&cfg);
return 1;
}
auto settings = config_root_setting(&cfg);
// win_script_context_info and 10 extra placeholder contexts, for
// script_specialize()
static const ptrdiff_t base = SCRIPT_CTX_PLACEHOLDER_BASE;
struct script_context_info context_info[ARR_SIZE(win_script_context_info) + MAX_PLACEHOLDERS] = {
{"placeholder0", base}, {"placeholder1", base + 4},
{"placeholder2", base + 8}, {"placeholder3", base + 12},
{"placeholder4", base + 16}, {"placeholder5", base + 20},
{"placeholder6", base + 24}, {"placeholder7", base + 28},
{"placeholder8", base + 32}, {"placeholder9", base + 36},
};
memcpy(context_info + 10, win_script_context_info, sizeof(win_script_context_info));
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 = context_info,
.output_info = NULL,
};
printf("// This file is generated by tools/animgen.c from %s\n", argv[1]);
printf("// This file is included in git repository for "
"convenience only.\n");
printf("// DO NOT EDIT THIS FILE!\n\n");
printf("#include <libconfig.h>\n");
printf("#include \"../script.h\"\n");
printf("#include \"../curve.h\"\n");
printf("#include \"../script_internal.h\"\n");
printf("#include \"utils/misc.h\"\n");
printf("#include \"config.h\"\n");
for (unsigned i = 0; i < (unsigned)config_setting_length(settings); i++) {
auto sub = config_setting_get_elem(settings, i);
auto name = config_setting_name(sub);
struct knob knobs[MAX_PLACEHOLDERS] = {};
struct placeholder placeholders[MAX_PLACEHOLDERS] = {};
auto knob_settings = config_setting_get_member(sub, "*knobs");
if (knob_settings) {
auto placeholder_settings =
config_setting_get_member(sub, "*placeholders");
BUG_ON(!placeholder_settings);
parse_knobs(name, knob_settings, placeholder_settings, knobs,
placeholders);
config_setting_remove(sub, "*knobs");
config_setting_remove(sub, "*placeholders");
knob_settings = NULL;
}
char *err = NULL;
auto script = script_compile(sub, parse_config, &err);
if (!script) {
fprintf(stderr, "Failed to compile script %s: %s\n", name, err);
free(err);
continue;
}
bool has_err = false;
for (size_t j = 0; j < script->len; j++) {
if (script->instrs[j].type != INST_LOAD_CTX) {
continue;
}
if (script->instrs[j].ctx < base) {
continue;
}
size_t index = (size_t)(script->instrs[j].ctx - base) / 4;
BUG_ON(index >= ARR_SIZE(knobs));
if (placeholders[index].source == NULL) {
fprintf(stderr, "Placeholder %zu used, but not defined\n",
index);
has_err = true;
break;
}
}
if (!has_err) {
char *code = script_to_c(script, outputs);
codegen(name, code, placeholders);
free(code);
dynarr_push(presets, strdup(name));
}
for (size_t j = 0; j < MAX_PLACEHOLDERS; j++) {
if (placeholders[j].value_for_choices) {
free(placeholders[j].value_for_choices);
}
}
for (size_t j = 0; j < MAX_PLACEHOLDERS; j++) {
if (knobs[j].type == KNOB_CHOICE) {
for (unsigned k = 0; k < knobs[j].n_choices; k++) {
free(knobs[j].choices[k]);
}
free(knobs[j].choices);
}
free((void *)knobs[j].name);
}
script_free(script);
}
config_destroy(&cfg);
printf("struct {\n"
" const char *name;\n"
" bool (*func)(struct win_script *output, "
"config_setting_t *setting);\n"
"} win_script_presets[] = {\n");
dynarr_foreach(presets, p) {
auto ident = sanitized_name(*p);
printf(" {\"%s\", win_script_preset__%s},\n", *p, ident);
free(*p);
free(ident);
}
printf(" {NULL, NULL},\n};\n");
dynarr_free_pod(presets);
return 0;
}

8
tools/meson.build Normal file
View file

@ -0,0 +1,8 @@
executable(
'animgen',
'animgen.c',
dependencies: [ base_deps, libconfig_dep, test_h_dep, cc.find_library('m')],
link_with: [libtools],
build_by_default: false,
include_directories: picom_inc,
)