diff options
Diffstat (limited to 'src/shaders/custom_mpv.c')
-rw-r--r-- | src/shaders/custom_mpv.c | 1768 |
1 files changed, 1768 insertions, 0 deletions
diff --git a/src/shaders/custom_mpv.c b/src/shaders/custom_mpv.c new file mode 100644 index 0000000..4ef0817 --- /dev/null +++ b/src/shaders/custom_mpv.c @@ -0,0 +1,1768 @@ +/* + * This file is part of libplacebo. + * + * libplacebo is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * libplacebo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with libplacebo. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <math.h> +#include <limits.h> + +#include "gpu.h" +#include "shaders.h" + +#include <libplacebo/shaders/colorspace.h> +#include <libplacebo/shaders/custom.h> + +// Hard-coded size limits, mainly for convenience (to avoid dynamic memory) +#define SHADER_MAX_HOOKS 16 +#define SHADER_MAX_BINDS 16 +#define MAX_SHEXP_SIZE 32 + +enum shexp_op { + SHEXP_OP_ADD, + SHEXP_OP_SUB, + SHEXP_OP_MUL, + SHEXP_OP_DIV, + SHEXP_OP_MOD, + SHEXP_OP_NOT, + SHEXP_OP_GT, + SHEXP_OP_LT, + SHEXP_OP_EQ, +}; + +enum shexp_tag { + SHEXP_END = 0, // End of an RPN expression + SHEXP_CONST, // Push a constant value onto the stack + SHEXP_TEX_W, // Get the width/height of a named texture (variable) + SHEXP_TEX_H, + SHEXP_OP2, // Pop two elements and push the result of a dyadic operation + SHEXP_OP1, // Pop one element and push the result of a monadic operation + SHEXP_VAR, // Arbitrary variable (e.g. shader parameters) +}; + +struct shexp { + enum shexp_tag tag; + union { + float cval; + pl_str varname; + enum shexp_op op; + } val; +}; + +struct custom_shader_hook { + // Variable/literal names of textures + pl_str pass_desc; + pl_str hook_tex[SHADER_MAX_HOOKS]; + pl_str bind_tex[SHADER_MAX_BINDS]; + pl_str save_tex; + + // Shader body itself + metadata + pl_str pass_body; + float offset[2]; + bool offset_align; + int comps; + + // Special expressions governing the output size and execution conditions + struct shexp width[MAX_SHEXP_SIZE]; + struct shexp height[MAX_SHEXP_SIZE]; + struct shexp cond[MAX_SHEXP_SIZE]; + + // Special metadata for compute shaders + bool is_compute; + int block_w, block_h; // Block size (each block corresponds to one WG) + int threads_w, threads_h; // How many threads form a WG +}; + +static bool parse_rpn_shexpr(pl_str line, struct shexp out[MAX_SHEXP_SIZE]) +{ + int pos = 0; + + while (line.len > 0) { + pl_str word = pl_str_split_char(line, ' ', &line); + if (word.len == 0) + continue; + + if (pos >= MAX_SHEXP_SIZE) + return false; + + struct shexp *exp = &out[pos++]; + + if (pl_str_eatend0(&word, ".w") || pl_str_eatend0(&word, ".width")) { + exp->tag = SHEXP_TEX_W; + exp->val.varname = word; + continue; + } + + if (pl_str_eatend0(&word, ".h") || pl_str_eatend0(&word, ".height")) { + exp->tag = SHEXP_TEX_H; + exp->val.varname = word; + continue; + } + + switch (word.buf[0]) { + case '+': exp->tag = SHEXP_OP2; exp->val.op = SHEXP_OP_ADD; continue; + case '-': exp->tag = SHEXP_OP2; exp->val.op = SHEXP_OP_SUB; continue; + case '*': exp->tag = SHEXP_OP2; exp->val.op = SHEXP_OP_MUL; continue; + case '/': exp->tag = SHEXP_OP2; exp->val.op = SHEXP_OP_DIV; continue; + case '%': exp->tag = SHEXP_OP2; exp->val.op = SHEXP_OP_MOD; continue; + case '!': exp->tag = SHEXP_OP1; exp->val.op = SHEXP_OP_NOT; continue; + case '>': exp->tag = SHEXP_OP2; exp->val.op = SHEXP_OP_GT; continue; + case '<': exp->tag = SHEXP_OP2; exp->val.op = SHEXP_OP_LT; continue; + case '=': exp->tag = SHEXP_OP2; exp->val.op = SHEXP_OP_EQ; continue; + } + + if (word.buf[0] >= '0' && word.buf[0] <= '9') { + exp->tag = SHEXP_CONST; + if (!pl_str_parse_float(word, &exp->val.cval)) + return false; + continue; + } + + // Treat as generic variable + exp->tag = SHEXP_VAR; + exp->val.varname = word; + } + + return true; +} + +static inline pl_str split_magic(pl_str *body) +{ + pl_str ret = pl_str_split_str0(*body, "//!", body); + if (body->len) { + // Make sure the separator is included in the remainder + body->buf -= 3; + body->len += 3; + } + + return ret; +} + +static bool parse_hook(pl_log log, pl_str *body, struct custom_shader_hook *out) +{ + *out = (struct custom_shader_hook){ + .pass_desc = pl_str0("unknown user shader"), + .width = {{ SHEXP_TEX_W, { .varname = pl_str0("HOOKED") }}}, + .height = {{ SHEXP_TEX_H, { .varname = pl_str0("HOOKED") }}}, + .cond = {{ SHEXP_CONST, { .cval = 1.0 }}}, + }; + + int hook_idx = 0; + int bind_idx = 0; + + // Parse all headers + while (true) { + pl_str rest; + pl_str line = pl_str_strip(pl_str_getline(*body, &rest)); + + // Check for the presence of the magic line beginning + if (!pl_str_eatstart0(&line, "//!")) + break; + + *body = rest; + + // Parse the supported commands + if (pl_str_eatstart0(&line, "HOOK")) { + if (hook_idx == SHADER_MAX_HOOKS) { + pl_err(log, "Passes may only hook up to %d textures!", + SHADER_MAX_HOOKS); + return false; + } + out->hook_tex[hook_idx++] = pl_str_strip(line); + continue; + } + + if (pl_str_eatstart0(&line, "BIND")) { + if (bind_idx == SHADER_MAX_BINDS) { + pl_err(log, "Passes may only bind up to %d textures!", + SHADER_MAX_BINDS); + return false; + } + out->bind_tex[bind_idx++] = pl_str_strip(line); + continue; + } + + if (pl_str_eatstart0(&line, "SAVE")) { + pl_str save_tex = pl_str_strip(line); + if (pl_str_equals0(save_tex, "HOOKED")) { + // This is a special name that means "overwrite existing" + // texture, which we just signal by not having any `save_tex` + // name set. + out->save_tex = (pl_str) {0}; + } else if (pl_str_equals0(save_tex, "MAIN")) { + // Compatibility alias + out->save_tex = pl_str0("MAINPRESUB"); + } else { + out->save_tex = save_tex; + }; + continue; + } + + if (pl_str_eatstart0(&line, "DESC")) { + out->pass_desc = pl_str_strip(line); + continue; + } + + if (pl_str_eatstart0(&line, "OFFSET")) { + line = pl_str_strip(line); + if (pl_str_equals0(line, "ALIGN")) { + out->offset_align = true; + } else { + if (!pl_str_parse_float(pl_str_split_char(line, ' ', &line), &out->offset[0]) || + !pl_str_parse_float(pl_str_split_char(line, ' ', &line), &out->offset[1]) || + line.len) + { + pl_err(log, "Error while parsing OFFSET!"); + return false; + } + } + continue; + } + + if (pl_str_eatstart0(&line, "WIDTH")) { + if (!parse_rpn_shexpr(line, out->width)) { + pl_err(log, "Error while parsing WIDTH!"); + return false; + } + continue; + } + + if (pl_str_eatstart0(&line, "HEIGHT")) { + if (!parse_rpn_shexpr(line, out->height)) { + pl_err(log, "Error while parsing HEIGHT!"); + return false; + } + continue; + } + + if (pl_str_eatstart0(&line, "WHEN")) { + if (!parse_rpn_shexpr(line, out->cond)) { + pl_err(log, "Error while parsing WHEN!"); + return false; + } + continue; + } + + if (pl_str_eatstart0(&line, "COMPONENTS")) { + if (!pl_str_parse_int(pl_str_strip(line), &out->comps)) { + pl_err(log, "Error parsing COMPONENTS: '%.*s'", PL_STR_FMT(line)); + return false; + } + continue; + } + + if (pl_str_eatstart0(&line, "COMPUTE")) { + line = pl_str_strip(line); + bool ok = pl_str_parse_int(pl_str_split_char(line, ' ', &line), &out->block_w) && + pl_str_parse_int(pl_str_split_char(line, ' ', &line), &out->block_h); + + line = pl_str_strip(line); + if (ok && line.len) { + ok = pl_str_parse_int(pl_str_split_char(line, ' ', &line), &out->threads_w) && + pl_str_parse_int(pl_str_split_char(line, ' ', &line), &out->threads_h) && + !line.len; + } else { + out->threads_w = out->block_w; + out->threads_h = out->block_h; + } + + if (!ok) { + pl_err(log, "Error while parsing COMPUTE!"); + return false; + } + + out->is_compute = true; + continue; + } + + // Unknown command type + pl_err(log, "Unrecognized command '%.*s'!", PL_STR_FMT(line)); + return false; + } + + // The rest of the file up until the next magic line beginning (if any) + // shall be the shader body + out->pass_body = split_magic(body); + + // Sanity checking + if (hook_idx == 0) + pl_warn(log, "Pass has no hooked textures (will be ignored)!"); + + return true; +} + +static bool parse_tex(pl_gpu gpu, void *alloc, pl_str *body, + struct pl_shader_desc *out) +{ + *out = (struct pl_shader_desc) { + .desc = { + .name = "USER_TEX", + .type = PL_DESC_SAMPLED_TEX, + }, + }; + + struct pl_tex_params params = { + .w = 1, .h = 1, .d = 0, + .sampleable = true, + .debug_tag = PL_DEBUG_TAG, + }; + + while (true) { + pl_str rest; + pl_str line = pl_str_strip(pl_str_getline(*body, &rest)); + + if (!pl_str_eatstart0(&line, "//!")) + break; + + *body = rest; + + if (pl_str_eatstart0(&line, "TEXTURE")) { + out->desc.name = pl_strdup0(alloc, pl_str_strip(line)); + continue; + } + + if (pl_str_eatstart0(&line, "SIZE")) { + line = pl_str_strip(line); + int dims = 0; + int dim[4]; // extra space to catch invalid extra entries + while (line.len && dims < PL_ARRAY_SIZE(dim)) { + if (!pl_str_parse_int(pl_str_split_char(line, ' ', &line), &dim[dims++])) { + PL_ERR(gpu, "Error while parsing SIZE!"); + return false; + } + } + + uint32_t lim = dims == 1 ? gpu->limits.max_tex_1d_dim + : dims == 2 ? gpu->limits.max_tex_2d_dim + : dims == 3 ? gpu->limits.max_tex_3d_dim + : 0; + + // Sanity check against GPU size limits + switch (dims) { + case 3: + params.d = dim[2]; + if (params.d < 1 || params.d > lim) { + PL_ERR(gpu, "SIZE %d exceeds GPU's texture size limits (%d)!", + params.d, lim); + return false; + } + // fall through + case 2: + params.h = dim[1]; + if (params.h < 1 || params.h > lim) { + PL_ERR(gpu, "SIZE %d exceeds GPU's texture size limits (%d)!", + params.h, lim); + return false; + } + // fall through + case 1: + params.w = dim[0]; + if (params.w < 1 || params.w > lim) { + PL_ERR(gpu, "SIZE %d exceeds GPU's texture size limits (%d)!", + params.w, lim); + return false; + } + break; + + default: + PL_ERR(gpu, "Invalid number of texture dimensions!"); + return false; + }; + + // Clear out the superfluous components + if (dims < 3) + params.d = 0; + if (dims < 2) + params.h = 0; + continue; + } + + if (pl_str_eatstart0(&line, "FORMAT")) { + line = pl_str_strip(line); + params.format = NULL; + for (int n = 0; n < gpu->num_formats; n++) { + pl_fmt fmt = gpu->formats[n]; + if (pl_str_equals0(line, fmt->name)) { + params.format = fmt; + break; + } + } + + if (!params.format || params.format->opaque) { + PL_ERR(gpu, "Unrecognized/unavailable FORMAT name: '%.*s'!", + PL_STR_FMT(line)); + return false; + } + + if (!(params.format->caps & PL_FMT_CAP_SAMPLEABLE)) { + PL_ERR(gpu, "Chosen FORMAT '%.*s' is not sampleable!", + PL_STR_FMT(line)); + return false; + } + continue; + } + + if (pl_str_eatstart0(&line, "FILTER")) { + line = pl_str_strip(line); + if (pl_str_equals0(line, "LINEAR")) { + out->binding.sample_mode = PL_TEX_SAMPLE_LINEAR; + } else if (pl_str_equals0(line, "NEAREST")) { + out->binding.sample_mode = PL_TEX_SAMPLE_NEAREST; + } else { + PL_ERR(gpu, "Unrecognized FILTER: '%.*s'!", PL_STR_FMT(line)); + return false; + } + continue; + } + + if (pl_str_eatstart0(&line, "BORDER")) { + line = pl_str_strip(line); + if (pl_str_equals0(line, "CLAMP")) { + out->binding.address_mode = PL_TEX_ADDRESS_CLAMP; + } else if (pl_str_equals0(line, "REPEAT")) { + out->binding.address_mode = PL_TEX_ADDRESS_REPEAT; + } else if (pl_str_equals0(line, "MIRROR")) { + out->binding.address_mode = PL_TEX_ADDRESS_MIRROR; + } else { + PL_ERR(gpu, "Unrecognized BORDER: '%.*s'!", PL_STR_FMT(line)); + return false; + } + continue; + } + + if (pl_str_eatstart0(&line, "STORAGE")) { + params.storable = true; + out->desc.type = PL_DESC_STORAGE_IMG; + out->desc.access = PL_DESC_ACCESS_READWRITE; + out->memory = PL_MEMORY_COHERENT; + continue; + } + + PL_ERR(gpu, "Unrecognized command '%.*s'!", PL_STR_FMT(line)); + return false; + } + + if (!params.format) { + PL_ERR(gpu, "No FORMAT specified!"); + return false; + } + + int caps = params.format->caps; + if (out->binding.sample_mode == PL_TEX_SAMPLE_LINEAR && !(caps & PL_FMT_CAP_LINEAR)) { + PL_ERR(gpu, "The specified texture format cannot be linear filtered!"); + return false; + } + + // Decode the rest of the section (up to the next //! marker) as raw hex + // data for the texture + pl_str tex, hexdata = split_magic(body); + if (!pl_str_decode_hex(NULL, pl_str_strip(hexdata), &tex)) { + PL_ERR(gpu, "Error while parsing TEXTURE body: must be a valid " + "hexadecimal sequence!"); + return false; + } + + int texels = params.w * PL_DEF(params.h, 1) * PL_DEF(params.d, 1); + size_t expected_len = texels * params.format->texel_size; + if (tex.len == 0 && params.storable) { + // In this case, it's okay that the texture has no initial data + pl_free_ptr(&tex.buf); + } else if (tex.len != expected_len) { + PL_ERR(gpu, "Shader TEXTURE size mismatch: got %zu bytes, expected %zu!", + tex.len, expected_len); + pl_free(tex.buf); + return false; + } + + params.initial_data = tex.buf; + out->binding.object = pl_tex_create(gpu, ¶ms); + pl_free(tex.buf); + + if (!out->binding.object) { + PL_ERR(gpu, "Failed creating custom texture!"); + return false; + } + + return true; +} + +static bool parse_buf(pl_gpu gpu, void *alloc, pl_str *body, + struct pl_shader_desc *out) +{ + *out = (struct pl_shader_desc) { + .desc = { + .name = "USER_BUF", + .type = PL_DESC_BUF_UNIFORM, + }, + }; + + // Temporary, to allow deferring variable placement until all headers + // have been processed (in order to e.g. determine buffer type) + void *tmp = pl_tmp(alloc); // will be freed automatically on failure + PL_ARRAY(struct pl_var) vars = {0}; + + while (true) { + pl_str rest; + pl_str line = pl_str_strip(pl_str_getline(*body, &rest)); + + if (!pl_str_eatstart0(&line, "//!")) + break; + + *body = rest; + + if (pl_str_eatstart0(&line, "BUFFER")) { + out->desc.name = pl_strdup0(alloc, pl_str_strip(line)); + continue; + } + + if (pl_str_eatstart0(&line, "STORAGE")) { + out->desc.type = PL_DESC_BUF_STORAGE; + out->desc.access = PL_DESC_ACCESS_READWRITE; + out->memory = PL_MEMORY_COHERENT; + continue; + } + + if (pl_str_eatstart0(&line, "VAR")) { + pl_str type_name = pl_str_split_char(pl_str_strip(line), ' ', &line); + struct pl_var var = {0}; + for (const struct pl_named_var *nv = pl_var_glsl_types; nv->glsl_name; nv++) { + if (pl_str_equals0(type_name, nv->glsl_name)) { + var = nv->var; + break; + } + } + + if (!var.type) { + // No type found + PL_ERR(gpu, "Unrecognized GLSL type '%.*s'!", PL_STR_FMT(type_name)); + return false; + } + + pl_str var_name = pl_str_split_char(line, '[', &line); + if (line.len > 0) { + // Parse array dimension + if (!pl_str_parse_int(pl_str_split_char(line, ']', NULL), &var.dim_a)) { + PL_ERR(gpu, "Failed parsing array dimension from [%.*s!", + PL_STR_FMT(line)); + return false; + } + + if (var.dim_a < 1) { + PL_ERR(gpu, "Invalid array dimension %d!", var.dim_a); + return false; + } + } + + var.name = pl_strdup0(alloc, pl_str_strip(var_name)); + PL_ARRAY_APPEND(tmp, vars, var); + continue; + } + + PL_ERR(gpu, "Unrecognized command '%.*s'!", PL_STR_FMT(line)); + return false; + } + + // Try placing all of the buffer variables + for (int i = 0; i < vars.num; i++) { + if (!sh_buf_desc_append(alloc, gpu, out, NULL, vars.elem[i])) { + PL_ERR(gpu, "Custom buffer exceeds GPU limitations!"); + return false; + } + } + + // Decode the rest of the section (up to the next //! marker) as raw hex + // data for the buffer + pl_str data, hexdata = split_magic(body); + if (!pl_str_decode_hex(tmp, pl_str_strip(hexdata), &data)) { + PL_ERR(gpu, "Error while parsing BUFFER body: must be a valid " + "hexadecimal sequence!"); + return false; + } + + size_t buf_size = sh_buf_desc_size(out); + if (data.len == 0 && out->desc.type == PL_DESC_BUF_STORAGE) { + // In this case, it's okay that the buffer has no initial data + } else if (data.len != buf_size) { + PL_ERR(gpu, "Shader BUFFER size mismatch: got %zu bytes, expected %zu!", + data.len, buf_size); + return false; + } + + out->binding.object = pl_buf_create(gpu, pl_buf_params( + .size = buf_size, + .uniform = out->desc.type == PL_DESC_BUF_UNIFORM, + .storable = out->desc.type == PL_DESC_BUF_STORAGE, + .initial_data = data.len ? data.buf : NULL, + )); + + if (!out->binding.object) { + PL_ERR(gpu, "Failed creating custom buffer!"); + return false; + } + + pl_free(tmp); + return true; +} + +static bool parse_var(pl_log log, pl_str str, enum pl_var_type type, pl_var_data *out) +{ + if (!str.len) + return true; + + pl_str buf = str; + bool ok = false; + switch (type) { + case PL_VAR_SINT: + ok = pl_str_parse_int(pl_str_split_char(buf, ' ', &buf), &out->i); + break; + case PL_VAR_UINT: + ok = pl_str_parse_uint(pl_str_split_char(buf, ' ', &buf), &out->u); + break; + case PL_VAR_FLOAT: + ok = pl_str_parse_float(pl_str_split_char(buf, ' ', &buf), &out->f); + break; + case PL_VAR_INVALID: + case PL_VAR_TYPE_COUNT: + pl_unreachable(); + } + + if (pl_str_strip(buf).len > 0) + ok = false; // left-over garbage + + if (!ok) { + pl_err(log, "Failed parsing variable data: %.*s", PL_STR_FMT(str)); + return false; + } + + return true; +} + +static bool check_bounds(pl_log log, enum pl_var_type type, const pl_var_data data, + const pl_var_data minimum, const pl_var_data maximum) +{ +#define CHECK_BOUNDS(v, fmt) do \ +{ \ + if (data.v < minimum.v) { \ + pl_err(log, "Initial value "fmt" below declared minimum "fmt"!", \ + data.v, minimum.v); \ + return false; \ + } \ + if (data.v > maximum.v) { \ + pl_err(log, "Initial value "fmt" above declared maximum "fmt"!", \ + data.v, maximum.v); \ + return false; \ + } \ +} while (0) + + switch (type) { + case PL_VAR_SINT: + CHECK_BOUNDS(i, "%d"); + break; + case PL_VAR_UINT: + CHECK_BOUNDS(u, "%u"); + break; + case PL_VAR_FLOAT: + CHECK_BOUNDS(f, "%f"); + break; + case PL_VAR_INVALID: + case PL_VAR_TYPE_COUNT: + pl_unreachable(); + } + +#undef CHECK_BOUNDS + return true; +} + +static bool parse_param(pl_log log, void *alloc, pl_str *body, + struct pl_hook_par *out) +{ + *out = (struct pl_hook_par) {0}; + pl_str minimum = {0}; + pl_str maximum = {0}; + bool is_enum = false; + + while (true) { + pl_str rest; + pl_str line = pl_str_strip(pl_str_getline(*body, &rest)); + + if (!pl_str_eatstart0(&line, "//!")) + break; + + *body = rest; + + if (pl_str_eatstart0(&line, "PARAM")) { + out->name = pl_strdup0(alloc, pl_str_strip(line)); + continue; + } + + if (pl_str_eatstart0(&line, "DESC")) { + out->description = pl_strdup0(alloc, pl_str_strip(line)); + continue; + } + + if (pl_str_eatstart0(&line, "MINIMUM")) { + minimum = pl_str_strip(line); + continue; + } + + if (pl_str_eatstart0(&line, "MAXIMUM")) { + maximum = pl_str_strip(line); + continue; + } + + if (pl_str_eatstart0(&line, "TYPE")) { + line = pl_str_strip(line); + is_enum = pl_str_eatstart0(&line, "ENUM"); + line = pl_str_strip(line); + if (pl_str_eatstart0(&line, "DYNAMIC")) { + out->mode = PL_HOOK_PAR_DYNAMIC; + } else if (pl_str_eatstart0(&line, "CONSTANT")) { + out->mode = PL_HOOK_PAR_CONSTANT; + } else if (pl_str_eatstart0(&line, "DEFINE")) { + out->mode = PL_HOOK_PAR_DEFINE; + out->type = PL_VAR_SINT; + if (pl_str_strip(line).len > 0) { + pl_err(log, "TYPE DEFINE does not take any extra arguments, " + "unexpected: '%.*s'", PL_STR_FMT(line)); + return false; + } + continue; + } else { + out->mode = PL_HOOK_PAR_VARIABLE; + } + + line = pl_str_strip(line); + for (const struct pl_named_var *nv = pl_var_glsl_types; + nv->glsl_name; nv++) + { + if (pl_str_equals0(line, nv->glsl_name)) { + if (nv->var.dim_v > 1 || nv->var.dim_m > 1) { + pl_err(log, "GLSL type '%s' is incompatible with " + "shader parameters, must be scalar type!", + nv->glsl_name); + return false; + } + + out->type = nv->var.type; + if (is_enum && out->type != PL_VAR_SINT) { + pl_err(log, "ENUM is only compatible with type int/DEFINE!"); + return false; + } + goto next; + } + } + + pl_err(log, "Unrecognized GLSL type '%.*s'!", PL_STR_FMT(line)); + return false; + } + + pl_err(log, "Unrecognized command '%.*s'!", PL_STR_FMT(line)); + return false; + +next: ; + } + + switch (out->type) { + case PL_VAR_INVALID: + pl_err(log, "Missing variable type!"); + return false; + case PL_VAR_SINT: + out->minimum.i = INT_MIN; + out->maximum.i = INT_MAX; + break; + case PL_VAR_UINT: + out->minimum.u = 0; + out->maximum.u = UINT_MAX; + break; + case PL_VAR_FLOAT: + out->minimum.f = -INFINITY; + out->maximum.f = INFINITY; + break; + case PL_VAR_TYPE_COUNT: + pl_unreachable(); + } + + pl_str initial = pl_str_strip(split_magic(body)); + if (!initial.len) { + pl_err(log, "Missing initial parameter value!"); + return false; + } + + if (is_enum) { + PL_ARRAY(const char *) names = {0}; + pl_assert(out->type == PL_VAR_SINT); + do { + pl_str line = pl_str_strip(pl_str_getline(initial, &initial)); + if (!line.len) + continue; + PL_ARRAY_APPEND(alloc, names, pl_strdup0(alloc, line)); + } while (initial.len); + + pl_assert(names.num >= 1); + out->initial.i = 0; + out->minimum.i = 0; + out->maximum.i = names.num - 1; + out->names = names.elem; + } else { + if (!parse_var(log, initial, out->type, &out->initial)) + return false; + if (!parse_var(log, minimum, out->type, &out->minimum)) + return false; + if (!parse_var(log, maximum, out->type, &out->maximum)) + return false; + if (!check_bounds(log, out->type, out->initial, out->minimum, out->maximum)) + return false; + } + + out->data = pl_memdup(alloc, &out->initial, sizeof(out->initial)); + return true; +} + +static enum pl_hook_stage mp_stage_to_pl(pl_str stage) +{ + if (pl_str_equals0(stage, "RGB")) + return PL_HOOK_RGB_INPUT; + if (pl_str_equals0(stage, "LUMA")) + return PL_HOOK_LUMA_INPUT; + if (pl_str_equals0(stage, "CHROMA")) + return PL_HOOK_CHROMA_INPUT; + if (pl_str_equals0(stage, "ALPHA")) + return PL_HOOK_ALPHA_INPUT; + if (pl_str_equals0(stage, "XYZ")) + return PL_HOOK_XYZ_INPUT; + + if (pl_str_equals0(stage, "CHROMA_SCALED")) + return PL_HOOK_CHROMA_SCALED; + if (pl_str_equals0(stage, "ALPHA_SCALED")) + return PL_HOOK_ALPHA_SCALED; + + if (pl_str_equals0(stage, "NATIVE")) + return PL_HOOK_NATIVE; + if (pl_str_equals0(stage, "MAINPRESUB")) + return PL_HOOK_RGB; + if (pl_str_equals0(stage, "MAIN")) + return PL_HOOK_RGB; // Note: conflicts with above! + + if (pl_str_equals0(stage, "LINEAR")) + return PL_HOOK_LINEAR; + if (pl_str_equals0(stage, "SIGMOID")) + return PL_HOOK_SIGMOID; + if (pl_str_equals0(stage, "PREKERNEL")) + return PL_HOOK_PRE_KERNEL; + if (pl_str_equals0(stage, "POSTKERNEL")) + return PL_HOOK_POST_KERNEL; + + if (pl_str_equals0(stage, "SCALED")) + return PL_HOOK_SCALED; + if (pl_str_equals0(stage, "PREOUTPUT")) + return PL_HOOK_PRE_OUTPUT; + if (pl_str_equals0(stage, "OUTPUT")) + return PL_HOOK_OUTPUT; + + return 0; +} + +static pl_str pl_stage_to_mp(enum pl_hook_stage stage) +{ + switch (stage) { + case PL_HOOK_RGB_INPUT: return pl_str0("RGB"); + case PL_HOOK_LUMA_INPUT: return pl_str0("LUMA"); + case PL_HOOK_CHROMA_INPUT: return pl_str0("CHROMA"); + case PL_HOOK_ALPHA_INPUT: return pl_str0("ALPHA"); + case PL_HOOK_XYZ_INPUT: return pl_str0("XYZ"); + + case PL_HOOK_CHROMA_SCALED: return pl_str0("CHROMA_SCALED"); + case PL_HOOK_ALPHA_SCALED: return pl_str0("ALPHA_SCALED"); + + case PL_HOOK_NATIVE: return pl_str0("NATIVE"); + case PL_HOOK_RGB: return pl_str0("MAINPRESUB"); + + case PL_HOOK_LINEAR: return pl_str0("LINEAR"); + case PL_HOOK_SIGMOID: return pl_str0("SIGMOID"); + case PL_HOOK_PRE_KERNEL: return pl_str0("PREKERNEL"); + case PL_HOOK_POST_KERNEL: return pl_str0("POSTKERNEL"); + + case PL_HOOK_SCALED: return pl_str0("SCALED"); + case PL_HOOK_PRE_OUTPUT: return pl_str0("PREOUTPUT"); + case PL_HOOK_OUTPUT: return pl_str0("OUTPUT"); + }; + + pl_unreachable(); +} + +struct hook_pass { + enum pl_hook_stage exec_stages; + struct custom_shader_hook hook; +}; + +struct pass_tex { + pl_str name; + pl_tex tex; + + // Metadata + pl_rect2df rect; + struct pl_color_repr repr; + struct pl_color_space color; + int comps; +}; + +struct hook_priv { + pl_log log; + pl_gpu gpu; + void *alloc; + + PL_ARRAY(struct hook_pass) hook_passes; + PL_ARRAY(struct pl_hook_par) hook_params; + + // Fixed (for shader-local resources) + PL_ARRAY(struct pl_shader_desc) descriptors; + + // Dynamic per pass + enum pl_hook_stage save_stages; + PL_ARRAY(struct pass_tex) pass_textures; + pl_shader trc_helper; + + // State for PRNG/frame count + int frame_count; + uint64_t prng_state[4]; +}; + +static void hook_reset(void *priv) +{ + struct hook_priv *p = priv; + p->pass_textures.num = 0; +} + +// Context during execution of a hook +struct hook_ctx { + struct hook_priv *priv; + const struct pl_hook_params *params; + struct pass_tex hooked; +}; + +static bool lookup_tex(struct hook_ctx *ctx, pl_str var, float size[2]) +{ + struct hook_priv *p = ctx->priv; + const struct pl_hook_params *params = ctx->params; + + if (pl_str_equals0(var, "HOOKED")) { + pl_assert(ctx->hooked.tex); + size[0] = ctx->hooked.tex->params.w; + size[1] = ctx->hooked.tex->params.h; + return true; + } + + if (pl_str_equals0(var, "NATIVE_CROPPED")) { + size[0] = fabs(pl_rect_w(params->src_rect)); + size[1] = fabs(pl_rect_h(params->src_rect)); + return true; + } + + if (pl_str_equals0(var, "OUTPUT")) { + size[0] = abs(pl_rect_w(params->dst_rect)); + size[1] = abs(pl_rect_h(params->dst_rect)); + return true; + } + + if (pl_str_equals0(var, "MAIN")) + var = pl_str0("MAINPRESUB"); + + for (int i = 0; i < p->pass_textures.num; i++) { + if (pl_str_equals(var, p->pass_textures.elem[i].name)) { + pl_tex tex = p->pass_textures.elem[i].tex; + size[0] = tex->params.w; + size[1] = tex->params.h; + return true; + } + } + + return false; +} + +static bool lookup_var(struct hook_ctx *ctx, pl_str var, float *val) +{ + struct hook_priv *p = ctx->priv; + for (int i = 0; i < p->hook_params.num; i++) { + const struct pl_hook_par *hp = &p->hook_params.elem[i]; + if (pl_str_equals0(var, hp->name)) { + switch (hp->type) { + case PL_VAR_SINT: *val = hp->data->i; return true; + case PL_VAR_UINT: *val = hp->data->u; return true; + case PL_VAR_FLOAT: *val = hp->data->f; return true; + case PL_VAR_INVALID: + case PL_VAR_TYPE_COUNT: + break; + } + + pl_unreachable(); + } + + if (hp->names) { + for (int j = hp->minimum.i; j <= hp->maximum.i; j++) { + if (pl_str_equals0(var, hp->names[j])) { + *val = j; + return true; + } + } + } + } + + PL_WARN(p, "Variable '%.*s' not found in RPN expression!", PL_STR_FMT(var)); + return false; +} + +// Returns whether successful. 'result' is left untouched on failure +static bool eval_shexpr(struct hook_ctx *ctx, + const struct shexp expr[MAX_SHEXP_SIZE], + float *result) +{ + struct hook_priv *p = ctx->priv; + float stack[MAX_SHEXP_SIZE] = {0}; + int idx = 0; // points to next element to push + + for (int i = 0; i < MAX_SHEXP_SIZE; i++) { + switch (expr[i].tag) { + case SHEXP_END: + goto done; + + case SHEXP_CONST: + // Since our SHEXPs are bound by MAX_SHEXP_SIZE, it should be + // impossible to overflow the stack + assert(idx < MAX_SHEXP_SIZE); + stack[idx++] = expr[i].val.cval; + continue; + + case SHEXP_OP1: + if (idx < 1) { + PL_WARN(p, "Stack underflow in RPN expression!"); + return false; + } + + switch (expr[i].val.op) { + case SHEXP_OP_NOT: stack[idx-1] = !stack[idx-1]; break; + default: pl_unreachable(); + } + continue; + + case SHEXP_OP2: + if (idx < 2) { + PL_WARN(p, "Stack underflow in RPN expression!"); + return false; + } + + // Pop the operands in reverse order + float op2 = stack[--idx]; + float op1 = stack[--idx]; + float res = 0.0; + switch (expr[i].val.op) { + case SHEXP_OP_ADD: res = op1 + op2; break; + case SHEXP_OP_SUB: res = op1 - op2; break; + case SHEXP_OP_MUL: res = op1 * op2; break; + case SHEXP_OP_DIV: res = op1 / op2; break; + case SHEXP_OP_MOD: res = fmodf(op1, op2); break; + case SHEXP_OP_GT: res = op1 > op2; break; + case SHEXP_OP_LT: res = op1 < op2; break; + case SHEXP_OP_EQ: res = fabsf(op1 - op2) <= 1e-6 * fmaxf(op1, op2); break; + case SHEXP_OP_NOT: pl_unreachable(); + } + + if (!isfinite(res)) { + PL_WARN(p, "Illegal operation in RPN expression!"); + return false; + } + + stack[idx++] = res; + continue; + + case SHEXP_TEX_W: + case SHEXP_TEX_H: { + pl_str name = expr[i].val.varname; + float size[2]; + + if (!lookup_tex(ctx, name, size)) { + PL_WARN(p, "Variable '%.*s' not found in RPN expression!", + PL_STR_FMT(name)); + return false; + } + + stack[idx++] = (expr[i].tag == SHEXP_TEX_W) ? size[0] : size[1]; + continue; + } + + case SHEXP_VAR: { + pl_str name = expr[i].val.varname; + float val; + if (!lookup_var(ctx, name, &val)) + return false; + stack[idx++] = val; + continue; + } + } + } + +done: + // Return the single stack element + if (idx != 1) { + PL_WARN(p, "Malformed stack after RPN expression!"); + return false; + } + + *result = stack[0]; + return true; +} + +static double prng_step(uint64_t s[4]) +{ + const uint64_t result = s[0] + s[3]; + const uint64_t t = s[1] << 17; + + s[2] ^= s[0]; + s[3] ^= s[1]; + s[1] ^= s[2]; + s[0] ^= s[3]; + + s[2] ^= t; + s[3] = (s[3] << 45) | (s[3] >> (64 - 45)); + return (result >> 11) * 0x1.0p-53; +} + +static bool bind_pass_tex(pl_shader sh, pl_str name, + const struct pass_tex *ptex, + const pl_rect2df *rect, + bool hooked, bool mainpresub) +{ + ident_t id, pos, pt; + + // Compatibility with mpv texture binding semantics + id = sh_bind(sh, ptex->tex, PL_TEX_ADDRESS_CLAMP, PL_TEX_SAMPLE_LINEAR, + "hook_tex", rect, &pos, &pt); + if (!id) + return false; + + GLSLH("#define %.*s_raw "$" \n", PL_STR_FMT(name), id); + GLSLH("#define %.*s_pos "$" \n", PL_STR_FMT(name), pos); + GLSLH("#define %.*s_map "$"_map \n", PL_STR_FMT(name), pos); + GLSLH("#define %.*s_size vec2(textureSize("$", 0)) \n", PL_STR_FMT(name), id); + GLSLH("#define %.*s_pt "$" \n", PL_STR_FMT(name), pt); + + float off[2] = { ptex->rect.x0, ptex->rect.y0 }; + GLSLH("#define %.*s_off "$" \n", PL_STR_FMT(name), + sh_var(sh, (struct pl_shader_var) { + .var = pl_var_vec2("offset"), + .data = off, + })); + + struct pl_color_repr repr = ptex->repr; + ident_t scale = SH_FLOAT(pl_color_repr_normalize(&repr)); + GLSLH("#define %.*s_mul "$" \n", PL_STR_FMT(name), scale); + + // Compatibility with mpv + GLSLH("#define %.*s_rot mat2(1.0, 0.0, 0.0, 1.0) \n", PL_STR_FMT(name)); + + // Sampling function boilerplate + GLSLH("#define %.*s_tex(pos) ("$" * vec4(textureLod("$", pos, 0.0))) \n", + PL_STR_FMT(name), scale, id); + GLSLH("#define %.*s_texOff(off) (%.*s_tex("$" + "$" * vec2(off))) \n", + PL_STR_FMT(name), PL_STR_FMT(name), pos, pt); + + bool can_gather = ptex->tex->params.format->gatherable; + if (can_gather) { + GLSLH("#define %.*s_gather(pos, c) ("$" * vec4(textureGather("$", pos, c))) \n", + PL_STR_FMT(name), scale, id); + } + + if (hooked) { + GLSLH("#define HOOKED_raw %.*s_raw \n", PL_STR_FMT(name)); + GLSLH("#define HOOKED_pos %.*s_pos \n", PL_STR_FMT(name)); + GLSLH("#define HOOKED_size %.*s_size \n", PL_STR_FMT(name)); + GLSLH("#define HOOKED_rot %.*s_rot \n", PL_STR_FMT(name)); + GLSLH("#define HOOKED_off %.*s_off \n", PL_STR_FMT(name)); + GLSLH("#define HOOKED_pt %.*s_pt \n", PL_STR_FMT(name)); + GLSLH("#define HOOKED_map %.*s_map \n", PL_STR_FMT(name)); + GLSLH("#define HOOKED_mul %.*s_mul \n", PL_STR_FMT(name)); + GLSLH("#define HOOKED_tex %.*s_tex \n", PL_STR_FMT(name)); + GLSLH("#define HOOKED_texOff %.*s_texOff \n", PL_STR_FMT(name)); + if (can_gather) + GLSLH("#define HOOKED_gather %.*s_gather \n", PL_STR_FMT(name)); + } + + if (mainpresub) { + GLSLH("#define MAIN_raw MAINPRESUB_raw \n"); + GLSLH("#define MAIN_pos MAINPRESUB_pos \n"); + GLSLH("#define MAIN_size MAINPRESUB_size \n"); + GLSLH("#define MAIN_rot MAINPRESUB_rot \n"); + GLSLH("#define MAIN_off MAINPRESUB_off \n"); + GLSLH("#define MAIN_pt MAINPRESUB_pt \n"); + GLSLH("#define MAIN_map MAINPRESUB_map \n"); + GLSLH("#define MAIN_mul MAINPRESUB_mul \n"); + GLSLH("#define MAIN_tex MAINPRESUB_tex \n"); + GLSLH("#define MAIN_texOff MAINPRESUB_texOff \n"); + if (can_gather) + GLSLH("#define MAIN_gather MAINPRESUB_gather \n"); + } + + return true; +} + +static void save_pass_tex(struct hook_priv *p, struct pass_tex ptex) +{ + + for (int i = 0; i < p->pass_textures.num; i++) { + if (!pl_str_equals(p->pass_textures.elem[i].name, ptex.name)) + continue; + + p->pass_textures.elem[i] = ptex; + return; + } + + // No texture with this name yet, append new one + PL_ARRAY_APPEND(p->alloc, p->pass_textures, ptex); +} + +static struct pl_hook_res hook_hook(void *priv, const struct pl_hook_params *params) +{ + struct hook_priv *p = priv; + pl_str stage = pl_stage_to_mp(params->stage); + struct pl_hook_res res = {0}; + + pl_shader sh = NULL; + struct hook_ctx ctx = { + .priv = p, + .params = params, + .hooked = { + .name = stage, + .tex = params->tex, + .rect = params->rect, + .repr = params->repr, + .color = params->color, + .comps = params->components, + }, + }; + + // Save the input texture if needed + if (p->save_stages & params->stage) { + PL_TRACE(p, "Saving input texture '%.*s' for binding", + PL_STR_FMT(ctx.hooked.name)); + save_pass_tex(p, ctx.hooked); + } + + for (int n = 0; n < p->hook_passes.num; n++) { + const struct hook_pass *pass = &p->hook_passes.elem[n]; + if (!(pass->exec_stages & params->stage)) + continue; + + const struct custom_shader_hook *hook = &pass->hook; + PL_TRACE(p, "Executing hook pass %d on stage '%.*s': %.*s", + n, PL_STR_FMT(stage), PL_STR_FMT(hook->pass_desc)); + + // Test for execution condition + float run = 0; + if (!eval_shexpr(&ctx, hook->cond, &run)) + goto error; + + if (!run) { + PL_TRACE(p, "Skipping hook due to condition"); + continue; + } + + // Generate a new shader object + sh = pl_dispatch_begin(params->dispatch); + + // Bind all necessary input textures + for (int i = 0; i < PL_ARRAY_SIZE(hook->bind_tex); i++) { + pl_str texname = hook->bind_tex[i]; + if (!texname.len) + break; + + // Convenience alias, to allow writing shaders that are oblivious + // of the exact stage they hooked. This simply translates to + // whatever stage actually fired the hook. + bool hooked = false, mainpresub = false; + if (pl_str_equals0(texname, "HOOKED")) { + // Continue with binding this, under the new name + texname = stage; + hooked = true; + } + + // Compatibility alias, because MAIN and MAINPRESUB mean the same + // thing to libplacebo, but user shaders are still written as + // though they can be different concepts. + if (pl_str_equals0(texname, "MAIN") || + pl_str_equals0(texname, "MAINPRESUB")) + { + texname = pl_str0("MAINPRESUB"); + mainpresub = true; + } + + for (int j = 0; j < p->descriptors.num; j++) { + if (pl_str_equals0(texname, p->descriptors.elem[j].desc.name)) { + // Directly bind this, no need to bother with all the + // `bind_pass_tex` boilerplate + ident_t id = sh_desc(sh, p->descriptors.elem[j]); + GLSLH("#define %.*s "$" \n", PL_STR_FMT(texname), id); + + if (p->descriptors.elem[j].desc.type == PL_DESC_SAMPLED_TEX) { + GLSLH("#define %.*s_tex(pos) (textureLod("$", pos, 0.0)) \n", + PL_STR_FMT(texname), id); + } + goto next_bind; + } + } + + for (int j = 0; j < p->pass_textures.num; j++) { + if (pl_str_equals(texname, p->pass_textures.elem[j].name)) { + // Note: We bind the whole texture, rather than + // hooked.rect, because user shaders in general are not + // designed to handle cropped input textures. + const struct pass_tex *ptex = &p->pass_textures.elem[j]; + pl_rect2df rect = { + 0, 0, ptex->tex->params.w, ptex->tex->params.h, + }; + + if (hook->offset_align && pl_str_equals(texname, stage)) { + float sx = pl_rect_w(ctx.hooked.rect) / pl_rect_w(params->src_rect), + sy = pl_rect_h(ctx.hooked.rect) / pl_rect_h(params->src_rect), + ox = ctx.hooked.rect.x0 - sx * params->src_rect.x0, + oy = ctx.hooked.rect.y0 - sy * params->src_rect.y0; + + PL_TRACE(p, "Aligning plane with ref: %f %f", ox, oy); + pl_rect2df_offset(&rect, ox, oy); + } + + if (!bind_pass_tex(sh, texname, &p->pass_textures.elem[j], + &rect, hooked, mainpresub)) + { + goto error; + } + goto next_bind; + } + } + + // If none of the above matched, this is an unknown texture name, + // so silently ignore this pass to match the mpv behavior + PL_TRACE(p, "Skipping hook due to no texture named '%.*s'.", + PL_STR_FMT(texname)); + pl_dispatch_abort(params->dispatch, &sh); + goto next_pass; + + next_bind: ; // outer 'continue' + } + + // Set up the input variables + p->frame_count++; + GLSLH("#define frame "$" \n", sh_var(sh, (struct pl_shader_var) { + .var = pl_var_int("frame"), + .data = &p->frame_count, + .dynamic = true, + })); + + float random = prng_step(p->prng_state); + GLSLH("#define random "$" \n", sh_var(sh, (struct pl_shader_var) { + .var = pl_var_float("random"), + .data = &random, + .dynamic = true, + })); + + float src_size[2] = { pl_rect_w(params->src_rect), pl_rect_h(params->src_rect) }; + GLSLH("#define input_size "$" \n", sh_var(sh, (struct pl_shader_var) { + .var = pl_var_vec2("input_size"), + .data = src_size, + })); + + float dst_size[2] = { pl_rect_w(params->dst_rect), pl_rect_h(params->dst_rect) }; + GLSLH("#define target_size "$" \n", sh_var(sh, (struct pl_shader_var) { + .var = pl_var_vec2("target_size"), + .data = dst_size, + })); + + float tex_off[2] = { params->src_rect.x0, params->src_rect.y0 }; + GLSLH("#define tex_offset "$" \n", sh_var(sh, (struct pl_shader_var) { + .var = pl_var_vec2("tex_offset"), + .data = tex_off, + })); + + // Custom parameters + for (int i = 0; i < p->hook_params.num; i++) { + const struct pl_hook_par *hp = &p->hook_params.elem[i]; + switch (hp->mode) { + case PL_HOOK_PAR_VARIABLE: + case PL_HOOK_PAR_DYNAMIC: + GLSLH("#define %s "$" \n", hp->name, + sh_var(sh, (struct pl_shader_var) { + .var = { + .name = hp->name, + .type = hp->type, + .dim_v = 1, + .dim_m = 1, + .dim_a = 1, + }, + .data = hp->data, + .dynamic = hp->mode == PL_HOOK_PAR_DYNAMIC, + })); + break; + + case PL_HOOK_PAR_CONSTANT: + GLSLH("#define %s "$" \n", hp->name, + sh_const(sh, (struct pl_shader_const) { + .name = hp->name, + .type = hp->type, + .data = hp->data, + .compile_time = true, + })); + break; + + case PL_HOOK_PAR_DEFINE: + GLSLH("#define %s %d \n", hp->name, hp->data->i); + break; + + case PL_HOOK_PAR_MODE_COUNT: + pl_unreachable(); + } + + if (hp->names) { + for (int j = hp->minimum.i; j <= hp->maximum.i; j++) + GLSLH("#define %s %d \n", hp->names[j], j); + } + } + + // Helper sub-shaders + uint64_t sh_id = SH_PARAMS(sh).id; + pl_shader_reset(p->trc_helper, pl_shader_params( + .id = ++sh_id, + .gpu = p->gpu, + )); + pl_shader_linearize(p->trc_helper, params->orig_color); + GLSLH("#define linearize "$" \n", sh_subpass(sh, p->trc_helper)); + + pl_shader_reset(p->trc_helper, pl_shader_params( + .id = ++sh_id, + .gpu = p->gpu, + )); + pl_shader_delinearize(p->trc_helper, params->orig_color); + GLSLH("#define delinearize "$" \n", sh_subpass(sh, p->trc_helper)); + + // Load and run the user shader itself + sh_append_str(sh, SH_BUF_HEADER, hook->pass_body); + sh_describef(sh, "%.*s", PL_STR_FMT(hook->pass_desc)); + + // Resolve output size and create framebuffer + float out_size[2] = {0}; + if (!eval_shexpr(&ctx, hook->width, &out_size[0]) || + !eval_shexpr(&ctx, hook->height, &out_size[1])) + { + goto error; + } + + int out_w = roundf(out_size[0]), + out_h = roundf(out_size[1]); + + if (!sh_require(sh, PL_SHADER_SIG_NONE, out_w, out_h)) + goto error; + + // Generate a new texture to store the render result + pl_tex fbo; + fbo = params->get_tex(params->priv, out_w, out_h); + if (!fbo) { + PL_ERR(p, "Failed dispatching hook: `get_tex` callback failed?"); + goto error; + } + + bool ok; + if (hook->is_compute) { + + if (!sh_try_compute(sh, hook->threads_w, hook->threads_h, false, 0) || + !fbo->params.storable) + { + PL_ERR(p, "Failed dispatching COMPUTE shader"); + goto error; + } + + GLSLP("#define out_image "$" \n", sh_desc(sh, (struct pl_shader_desc) { + .binding.object = fbo, + .desc = { + .name = "out_image", + .type = PL_DESC_STORAGE_IMG, + .access = PL_DESC_ACCESS_WRITEONLY, + }, + })); + + sh->output = PL_SHADER_SIG_NONE; + + GLSL("hook(); \n"); + ok = pl_dispatch_compute(params->dispatch, pl_dispatch_compute_params( + .shader = &sh, + .dispatch_size = { + // Round up as many blocks as are needed to cover the image + PL_DIV_UP(out_w, hook->block_w), + PL_DIV_UP(out_h, hook->block_h), + 1, + }, + .width = out_w, + .height = out_h, + )); + + } else { + + // Default non-COMPUTE shaders to explicitly use fragment shaders + // only, to avoid breaking things like fwidth() + sh->type = PL_DEF(sh->type, SH_FRAGMENT); + + GLSL("vec4 color = hook(); \n"); + ok = pl_dispatch_finish(params->dispatch, pl_dispatch_params( + .shader = &sh, + .target = fbo, + )); + + } + + if (!ok) + goto error; + + float sx = (float) out_w / ctx.hooked.tex->params.w, + sy = (float) out_h / ctx.hooked.tex->params.h, + x0 = sx * ctx.hooked.rect.x0 + hook->offset[0], + y0 = sy * ctx.hooked.rect.y0 + hook->offset[1]; + + pl_rect2df new_rect = { + x0, + y0, + x0 + sx * pl_rect_w(ctx.hooked.rect), + y0 + sy * pl_rect_h(ctx.hooked.rect), + }; + + if (hook->offset_align) { + float rx = pl_rect_w(new_rect) / pl_rect_w(params->src_rect), + ry = pl_rect_h(new_rect) / pl_rect_h(params->src_rect), + ox = rx * params->src_rect.x0 - sx * ctx.hooked.rect.x0, + oy = ry * params->src_rect.y0 - sy * ctx.hooked.rect.y0; + + pl_rect2df_offset(&new_rect, ox, oy); + } + + // Save the result of this shader invocation + struct pass_tex ptex = { + .name = hook->save_tex.len ? hook->save_tex : stage, + .tex = fbo, + .repr = ctx.hooked.repr, + .color = ctx.hooked.color, + .comps = PL_DEF(hook->comps, ctx.hooked.comps), + .rect = new_rect, + }; + + // It's assumed that users will correctly normalize the input + pl_color_repr_normalize(&ptex.repr); + + PL_TRACE(p, "Saving output texture '%.*s' from hook execution on '%.*s'", + PL_STR_FMT(ptex.name), PL_STR_FMT(stage)); + + save_pass_tex(p, ptex); + + // Update the result object, unless we saved to a different name + if (pl_str_equals(ptex.name, stage)) { + ctx.hooked = ptex; + res = (struct pl_hook_res) { + .output = PL_HOOK_SIG_TEX, + .tex = fbo, + .repr = ptex.repr, + .color = ptex.color, + .components = ptex.comps, + .rect = new_rect, + }; + } + +next_pass: ; + } + + return res; + +error: + pl_dispatch_abort(params->dispatch, &sh); + return (struct pl_hook_res) { .failed = true }; +} + +const struct pl_hook *pl_mpv_user_shader_parse(pl_gpu gpu, + const char *shader_text, + size_t shader_len) +{ + if (!shader_len) + return NULL; + + pl_str shader = { (uint8_t *) shader_text, shader_len }; + + struct pl_hook *hook = pl_zalloc_obj(NULL, hook, struct hook_priv); + struct hook_priv *p = PL_PRIV(hook); + + *hook = (struct pl_hook) { + .input = PL_HOOK_SIG_TEX, + .priv = p, + .reset = hook_reset, + .hook = hook_hook, + .signature = pl_str_hash(shader), + }; + + *p = (struct hook_priv) { + .log = gpu->log, + .gpu = gpu, + .alloc = hook, + .trc_helper = pl_shader_alloc(gpu->log, NULL), + .prng_state = { + // Determined by fair die roll + 0xb76d71f9443c228allu, 0x93a02092fc4807e8llu, + 0x06d81748f838bd07llu, 0x9381ee129dddce6cllu, + }, + }; + + shader = pl_strdup(hook, shader); + + // Skip all garbage (e.g. comments) before the first header + int pos = pl_str_find(shader, pl_str0("//!")); + if (pos < 0) { + PL_ERR(gpu, "Shader appears to contain no headers?"); + goto error; + } + shader = pl_str_drop(shader, pos); + + // Loop over the file + while (shader.len > 0) + { + // Peek at the first header to dispatch the right type + if (pl_str_startswith0(shader, "//!TEXTURE")) { + struct pl_shader_desc sd; + if (!parse_tex(gpu, hook, &shader, &sd)) + goto error; + + PL_INFO(gpu, "Registering named texture '%s'", sd.desc.name); + PL_ARRAY_APPEND(hook, p->descriptors, sd); + continue; + } + + if (pl_str_startswith0(shader, "//!BUFFER")) { + struct pl_shader_desc sd; + if (!parse_buf(gpu, hook, &shader, &sd)) + goto error; + + PL_INFO(gpu, "Registering named buffer '%s'", sd.desc.name); + PL_ARRAY_APPEND(hook, p->descriptors, sd); + continue; + } + + if (pl_str_startswith0(shader, "//!PARAM")) { + struct pl_hook_par hp; + if (!parse_param(gpu->log, hook, &shader, &hp)) + goto error; + + PL_INFO(gpu, "Registering named parameter '%s'", hp.name); + PL_ARRAY_APPEND(hook, p->hook_params, hp); + continue; + } + + struct custom_shader_hook h; + if (!parse_hook(gpu->log, &shader, &h)) + goto error; + + struct hook_pass pass = { + .exec_stages = 0, + .hook = h, + }; + + for (int i = 0; i < PL_ARRAY_SIZE(h.hook_tex); i++) + pass.exec_stages |= mp_stage_to_pl(h.hook_tex[i]); + for (int i = 0; i < PL_ARRAY_SIZE(h.bind_tex); i++) { + p->save_stages |= mp_stage_to_pl(h.bind_tex[i]); + if (pl_str_equals0(h.bind_tex[i], "HOOKED")) + p->save_stages |= pass.exec_stages; + } + + // As an extra precaution, this avoids errors when trying to run + // conditions against planes that were never hooked. As a sole + // exception, OUTPUT is special because it's hard-coded to return the + // dst_rect even before it was hooked. (This is an apparently + // undocumented mpv quirk, but shaders rely on it in practice) + enum pl_hook_stage rpn_stages = 0; + for (int i = 0; i < PL_ARRAY_SIZE(h.width); i++) { + if (h.width[i].tag == SHEXP_TEX_W || h.width[i].tag == SHEXP_TEX_H) + rpn_stages |= mp_stage_to_pl(h.width[i].val.varname); + } + for (int i = 0; i < PL_ARRAY_SIZE(h.height); i++) { + if (h.height[i].tag == SHEXP_TEX_W || h.height[i].tag == SHEXP_TEX_H) + rpn_stages |= mp_stage_to_pl(h.height[i].val.varname); + } + for (int i = 0; i < PL_ARRAY_SIZE(h.cond); i++) { + if (h.cond[i].tag == SHEXP_TEX_W || h.cond[i].tag == SHEXP_TEX_H) + rpn_stages |= mp_stage_to_pl(h.cond[i].val.varname); + } + + p->save_stages |= rpn_stages & ~PL_HOOK_OUTPUT; + + PL_INFO(gpu, "Registering hook pass: %.*s", PL_STR_FMT(h.pass_desc)); + PL_ARRAY_APPEND(hook, p->hook_passes, pass); + } + + // We need to hook on both the exec and save stages, so that we can keep + // track of any textures we might need + hook->stages |= p->save_stages; + for (int i = 0; i < p->hook_passes.num; i++) + hook->stages |= p->hook_passes.elem[i].exec_stages; + + hook->parameters = p->hook_params.elem; + hook->num_parameters = p->hook_params.num; + + PL_MSG(gpu, PL_LOG_DEBUG, "Loaded user shader:"); + pl_msg_source(gpu->log, PL_LOG_DEBUG, shader_text); + + return hook; + +error: + pl_mpv_user_shader_destroy((const struct pl_hook **) &hook); + PL_MSG(gpu, PL_LOG_ERR, "Failed to parse user shader:"); + pl_msg_source(gpu->log, PL_LOG_ERR, shader_text); + pl_log_stack_trace(gpu->log, PL_LOG_ERR); + return NULL; +} + +void pl_mpv_user_shader_destroy(const struct pl_hook **hookp) +{ + const struct pl_hook *hook = *hookp; + if (!hook) + return; + + struct hook_priv *p = PL_PRIV(hook); + for (int i = 0; i < p->descriptors.num; i++) { + switch (p->descriptors.elem[i].desc.type) { + case PL_DESC_BUF_UNIFORM: + case PL_DESC_BUF_STORAGE: + case PL_DESC_BUF_TEXEL_UNIFORM: + case PL_DESC_BUF_TEXEL_STORAGE: { + pl_buf buf = p->descriptors.elem[i].binding.object; + pl_buf_destroy(p->gpu, &buf); + break; + } + + case PL_DESC_SAMPLED_TEX: + case PL_DESC_STORAGE_IMG: { + pl_tex tex = p->descriptors.elem[i].binding.object; + pl_tex_destroy(p->gpu, &tex); + break; + + case PL_DESC_INVALID: + case PL_DESC_TYPE_COUNT: + pl_unreachable(); + } + } + } + + pl_shader_free(&p->trc_helper); + pl_free((void *) hook); + *hookp = NULL; +} |