summaryrefslogtreecommitdiffstats
path: root/src/shaders/custom_mpv.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/shaders/custom_mpv.c')
-rw-r--r--src/shaders/custom_mpv.c1768
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, &params);
+ 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;
+}