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