/* * 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 . */ #include #include #include "gpu.h" #include "shaders.h" #include #include // 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; }