vo_opengl: support loading custom user textures

Parsing the texture data as raw strings makes the textures the most
portable and self-contained. In order to facilitate different types of
shaders, the parse_user_shader interaction has been changed to instead
have it loop through blocks and call the passed functions for each valid
block parsed. This is more modular and also cleaner, with better code
separation.

Closes #4586.
This commit is contained in:
Niklas Haas 2017-07-11 01:59:21 +02:00
parent f1af6e53f0
commit 345bb193fe
No known key found for this signature in database
GPG Key ID: 9A09076581B27402
7 changed files with 418 additions and 117 deletions

View File

@ -4239,24 +4239,59 @@ The following video options are currently all specific to ``--vo=opengl`` and
...
Each block of metadata, along with the non-metadata lines after it, defines
a single pass. Each pass can set the following metadata:
Each section of metadata, along with the non-metadata lines after it,
defines a single block. There are currently two types of blocks, HOOKs and
TEXTUREs.
DESC <title>
User-friendly description of the pass. This is the name used when
representing this shader in the list of passes for property
`vo-passes`.
A ``TEXTURE`` block can set the following options:
TEXTURE <name> (required)
The name of this texture. Hooks can then bind the texture under this
name using BIND. This must be the first option of the texture block.
SIZE <width> [<height>] [<depth>]
The dimensions of the texture. The height and depth are optional. The
type of texture (1D, 2D or 3D) depends on the number of components
specified.
COMPONENTS <n>
The number of components per texel contained in the texture. Defaults
to 1.
FORMAT <spec>
The texture format for the samples. A valid texture specification is
the number of bits followed by a single letter which is either ``f``
(for float), ``i`` (for uint) or ``u`` (for unorm), for example
``32f``. Defaults to ``8i``.
FILTER <LINEAR|NEAREST>
The min/magnification filter used when sampling from this texture.
BORDER <CLAMP|REPEAT|MIRROR>
The border wrapping mode used when sampling from this texture.
Following the metadata is a string of bytes in hexadecimal notation that
define the raw texture data, corresponding to the format specified by
`FORMAT`, on a single line with no extra whitespace.
A ``HOOK`` block can set the following options:
HOOK <name> (required)
The texture which to hook into. May occur multiple times within a
metadata block, up to a predetermined limit. See below for a list of
hookable textures.
DESC <title>
User-friendly description of the pass. This is the name used when
representing this shader in the list of passes for property
`vo-passes`.
BIND <name>
Loads a texture and makes it available to the pass, and sets up macros
to enable accessing it. See below for a list of set macros. By default,
no textures are bound. The special name HOOKED can be used to refer to
the texture that triggered this pass.
Loads a texture (either coming from mpv or from a ``TEXTURE`` block)
and makes it available to the pass. When binding textures from mpv,
this will also set up macros to facilitate accessing it properly. See
below for a list. By default, no textures are bound. The special name
HOOKED can be used to refer to the texture that triggered this pass.
SAVE <name>
Gives the name of the texture to save the result of this pass into. By
@ -4280,14 +4315,14 @@ The following video options are currently all specific to ``--vo=opengl`` and
hook point can still cause that hook point to be saved, which has some
minor overhead)
OFFSET ox oy
OFFSET <ox> <oy>
Indicates a pixel shift (offset) introduced by this pass. These pixel
offsets will be accumulated and corrected during the next scaling pass
(``cscale`` or ``scale``). The default values are 0 0 which correspond
to no shift. Note that offsets are ignored when not overwriting the
hooked texture.
COMPONENTS n
COMPONENTS <n>
Specifies how many components of this pass's output are relevant and
should be stored in the texture, up to 4 (rgba). By default, this value
is equal to the number of components in HOOKED.
@ -4303,7 +4338,7 @@ The following video options are currently all specific to ``--vo=opengl`` and
difference is the fact that you can use shared memory inside compute
shaders.
Each bound texture (via ``BIND``) will make available the following
Each bound mpv texture (via ``BIND``) will make available the following
definitions to that shader pass, where NAME is the name of the bound
texture:

View File

@ -454,3 +454,40 @@ struct bstr bstr_get_ext(struct bstr s)
return (struct bstr){NULL, 0};
return bstr_splice(s, dotpos + 1, s.len);
}
static int h_to_i(unsigned char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
return -1; // invalid char
}
bool bstr_decode_hex(void *talloc_ctx, struct bstr hex, struct bstr *out)
{
if (!out)
return false;
char *arr = talloc_array(talloc_ctx, char, hex.len / 2);
int len = 0;
while (hex.len >= 2) {
int a = h_to_i(hex.start[0]);
int b = h_to_i(hex.start[1]);
hex = bstr_splice(hex, 2, hex.len);
if (a < 0 || b < 0) {
talloc_free(arr);
return false;
}
arr[len++] = (a << 4) | b;
}
*out = (struct bstr){ .start = arr, .len = len };
return true;
}

View File

@ -80,6 +80,10 @@ double bstrtod(struct bstr str, struct bstr *rest);
void bstr_lower(struct bstr str);
int bstr_sscanf(struct bstr str, const char *format, ...);
// Decode a string containing hexadecimal data. All whitespace will be silently
// ignored. When successful, this allocates a new array to store the output.
bool bstr_decode_hex(void *talloc_ctx, struct bstr hex, struct bstr *out);
// Decode the UTF-8 code point at the start of the string, and return the
// character.
// After calling this function, *out_next will point to the next character.

View File

@ -55,32 +55,6 @@ static int control(stream_t *s, int cmd, void *arg)
return STREAM_UNSUPPORTED;
}
static int h_to_i(unsigned char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
return -1;
}
static bool bstr_to_hex_inplace(bstr *h)
{
if (h->len % 2)
return false;
for (int n = 0; n < h->len / 2; n++) {
int hi = h_to_i(h->start[n * 2 + 0]);
int lo = h_to_i(h->start[n * 2 + 1]);
if (hi < 0 || lo < 0)
return false;
h->start[n] = (hi << 4) | lo;
}
h->len /= 2;
return true;
}
static int open_f(stream_t *stream)
{
stream->fill_buffer = fill_buffer;
@ -100,7 +74,7 @@ static int open_f(stream_t *stream)
bstr_eatstart0(&data, "memory://");
stream_control(stream, STREAM_CTRL_SET_CONTENTS, &data);
if (use_hex && !bstr_to_hex_inplace(&p->data)) {
if (use_hex && !bstr_decode_hex(stream, p->data, &p->data)) {
MP_FATAL(stream, "Invalid data.\n");
return STREAM_ERROR;
}

View File

@ -19,6 +19,7 @@
#include "misc/ctype.h"
#include "user_shaders.h"
#include "formats.h"
static bool parse_rpn_szexpr(struct bstr line, struct szexp out[MAX_SZEXP_SIZE])
{
@ -158,13 +159,10 @@ done:
return true;
}
bool parse_user_shader_pass(struct mp_log *log, struct bstr *body,
struct gl_user_shader *out)
static bool parse_hook(struct mp_log *log, struct bstr *body,
struct gl_user_shader_hook *out)
{
if (!body || !out || !body->start || body->len == 0)
return false;
*out = (struct gl_user_shader){
*out = (struct gl_user_shader_hook){
.pass_desc = bstr0("(unknown)"),
.offset = identity_trans,
.width = {{ SZEXP_VAR_W, { .varname = bstr0("HOOKED") }}},
@ -175,14 +173,6 @@ bool parse_user_shader_pass(struct mp_log *log, struct bstr *body,
int hook_idx = 0;
int bind_idx = 0;
// Skip all garbage (e.g. comments) before the first header
int pos = bstr_find(*body, bstr0("//!"));
if (pos < 0) {
mp_warn(log, "Shader appears to contain no headers!\n");
return false;
}
*body = bstr_cut(*body, pos);
// Parse all headers
while (true) {
struct bstr rest;
@ -295,3 +285,162 @@ bool parse_user_shader_pass(struct mp_log *log, struct bstr *body,
return true;
}
static bool parse_tex(struct mp_log *log, struct bstr *body,
struct gl_user_shader_tex *out)
{
*out = (struct gl_user_shader_tex){
.name = bstr0("USER_TEX"),
.w = 1, .h = 1, .d = 1,
.components = 1,
.bytes = 1,
.mpgl_type = MPGL_TYPE_UINT,
.gl_filter = GL_LINEAR,
.gl_target = GL_TEXTURE_1D,
.gl_border = GL_CLAMP_TO_EDGE,
};
while (true) {
struct bstr rest;
struct bstr line = bstr_strip(bstr_getline(*body, &rest));
if (!bstr_eatstart0(&line, "//!"))
break;
*body = rest;
if (bstr_eatstart0(&line, "TEXTURE")) {
out->name = bstr_strip(line);
continue;
}
if (bstr_eatstart0(&line, "SIZE")) {
int num = bstr_sscanf(line, "%d %d %d", &out->w, &out->h, &out->d);
if (num < 1 || num > 3 || out->w < 1 || out->h < 1 || out->d < 1) {
mp_err(log, "Error while parsing SIZE!\n");
return false;
}
static GLenum tgt[] = {GL_TEXTURE_1D, GL_TEXTURE_2D, GL_TEXTURE_3D};
out->gl_target = tgt[num - 1];
continue;
}
if (bstr_eatstart0(&line, "COMPONENTS")) {
if (bstr_sscanf(line, "%d", &out->components) != 1) {
mp_err(log, "Error while parsing COMPONENTS!\n");
return false;
}
continue;
}
if (bstr_eatstart0(&line, "FORMAT")) {
int bits;
char fmt;
if (bstr_sscanf(line, "%d%c", &bits, &fmt) != 2) {
mp_err(log, "Error while parsing FORMAT!\n");
return false;
}
out->bytes = bits / 8;
switch (fmt) {
case 'f': out->mpgl_type = MPGL_TYPE_FLOAT; break;
case 'i': out->mpgl_type = MPGL_TYPE_UINT; break;
case 'u': out->mpgl_type = MPGL_TYPE_UNORM; break;
default:
mp_err(log, "Unrecognized FORMAT description: '%c'!\n", fmt);
return false;
}
continue;
}
if (bstr_eatstart0(&line, "FILTER")) {
line = bstr_strip(line);
if (bstr_equals0(line, "LINEAR")) {
out->gl_filter = GL_LINEAR;
} else if (bstr_equals0(line, "NEAREST")) {
out->gl_filter = GL_NEAREST;
} else {
mp_err(log, "Unrecognized FILTER: '%.*s'!\n", BSTR_P(line));
return false;
}
continue;
}
if (bstr_eatstart0(&line, "BORDER")) {
line = bstr_strip(line);
if (bstr_equals0(line, "CLAMP")) {
out->gl_border = GL_CLAMP_TO_EDGE;
} else if (bstr_equals0(line, "REPEAT")) {
out->gl_border = GL_REPEAT;
} else if (bstr_equals0(line, "MIRROR")) {
out->gl_border = GL_MIRRORED_REPEAT;
} else {
mp_err(log, "Unrecognized BORDER: '%.*s'!\n", BSTR_P(line));
return false;
}
continue;
}
mp_err(log, "Unrecognized command '%.*s'!\n", BSTR_P(line));
return false;
}
// Decode the rest of the section (up to the next //! marker) as raw hex
// data for the texture
struct bstr hexdata;
if (bstr_split_tok(*body, "//!", &hexdata, body)) {
// Make sure the magic line is part of the rest
body->start -= 3;
body->len += 3;
}
struct bstr tex;
if (!bstr_decode_hex(NULL, bstr_strip(hexdata), &tex)) {
mp_err(log, "Error while parsing TEXTURE body: must be a valid "
"hexadecimal sequence, on a single line!\n");
return false;
}
int expected_len = out->w * out->h * out->d * out->components * out->bytes;
if (tex.len != expected_len) {
mp_err(log, "Shader TEXTURE size mismatch: got %zd bytes, expected %d!\n",
tex.len, expected_len);
talloc_free(tex.start);
return false;
}
out->texdata = tex.start;
return true;
}
void parse_user_shader(struct mp_log *log, struct bstr shader, void *priv,
bool (*dohook)(void *p, struct gl_user_shader_hook hook),
bool (*dotex)(void *p, struct gl_user_shader_tex tex))
{
if (!dohook || !dotex || !shader.len)
return;
// Skip all garbage (e.g. comments) before the first header
int pos = bstr_find(shader, bstr0("//!"));
if (pos < 0) {
mp_warn(log, "Shader appears to contain no headers!\n");
return;
}
shader = bstr_cut(shader, pos);
// Loop over the file
while (shader.len > 0)
{
// Peek at the first header to dispatch the right type
if (bstr_startswith0(shader, "//!TEXTURE")) {
struct gl_user_shader_tex t;
if (!parse_tex(log, &shader, &t) || !dotex(priv, t))
return;
continue;
}
struct gl_user_shader_hook h;
if (!parse_hook(log, &shader, &h) || !dohook(priv, h))
return;
}
}

View File

@ -55,7 +55,7 @@ struct szexp {
} val;
};
struct gl_user_shader {
struct gl_user_shader_hook {
struct bstr pass_desc;
struct bstr hook_tex[SHADER_MAX_HOOKS];
struct bstr bind_tex[SHADER_MAX_BINDS];
@ -70,10 +70,25 @@ struct gl_user_shader {
int compute_h;
};
// Parse the next shader pass from `body`. The `struct bstr` is modified by the
// function. Returns false if the end of the string was reached (or on error).
bool parse_user_shader_pass(struct mp_log *log, struct bstr *body,
struct gl_user_shader *out);
struct gl_user_shader_tex {
struct bstr name;
int w, h, d;
int components;
int bytes;
int mpgl_type;
GLenum gl_target;
GLenum gl_filter;
GLenum gl_border;
void *texdata;
// for video.c
GLenum gl_tex;
};
// Parse the next shader block from `body`. The callbacks are invoked on every
// valid shader block parsed.
void parse_user_shader(struct mp_log *log, struct bstr shader, void *priv,
bool (*dohook)(void *p, struct gl_user_shader_hook hook),
bool (*dotex)(void *p, struct gl_user_shader_tex tex));
// Evaluate a szexp, given a lookup function for named textures
bool eval_szexpr(struct mp_log *log, void *priv,

View File

@ -240,6 +240,12 @@ struct gl_video {
struct fbotex vdpau_deinterleave_fbo[2];
GLuint hdr_peak_ssbo;
// user pass descriptions and textures
struct tex_hook tex_hooks[SHADER_MAX_PASSES];
int tex_hook_num;
struct gl_user_shader_tex user_textures[SHADER_MAX_PASSES];
int user_tex_num;
int surface_idx;
int surface_now;
int frames_drawn;
@ -274,9 +280,7 @@ struct gl_video {
struct gl_timer *upload_timer;
struct gl_timer *blit_timer;
// hooks and saved textures
struct tex_hook tex_hooks[SHADER_MAX_PASSES];
int tex_hook_num;
// intermediate textures
struct saved_tex saved_tex[SHADER_MAX_SAVED];
int saved_tex_num;
struct fbotex hook_fbos[SHADER_MAX_SAVED];
@ -503,7 +507,11 @@ static void gl_video_reset_hooks(struct gl_video *p)
for (int i = 0; i < p->tex_hook_num; i++)
talloc_free(p->tex_hooks[i].priv);
for (int i = 0; i < p->user_tex_num; i++)
p->gl->DeleteTextures(1, &p->user_textures[i].gl_tex);
p->tex_hook_num = 0;
p->user_tex_num = 0;
}
static inline int fbosurface_wrap(int id)
@ -1378,6 +1386,51 @@ static void saved_tex_store(struct gl_video *p, const char *name,
};
}
static bool pass_hook_setup_binds(struct gl_video *p, const char *name,
struct img_tex tex, struct tex_hook *hook)
{
for (int t = 0; t < TEXUNIT_VIDEO_NUM; t++) {
char *bind_name = (char *)hook->bind_tex[t];
if (!bind_name)
continue;
// This is a special name that means "currently hooked texture"
if (strcmp(bind_name, "HOOKED") == 0) {
int id = pass_bind(p, tex);
hook_prelude(p, "HOOKED", id, tex);
hook_prelude(p, name, id, tex);
continue;
}
// BIND can also be used to load user-defined textures, in which
// case we will directly load them as a uniform instead of
// generating the hook_prelude boilerplate
for (int u = 0; u < p->user_tex_num; u++) {
struct gl_user_shader_tex *utex = &p->user_textures[u];
if (bstr_equals0(utex->name, bind_name)) {
gl_sc_uniform_tex(p->sc, bind_name, utex->gl_target, utex->gl_tex);
goto next_bind;
}
}
struct img_tex bind_tex;
if (!saved_tex_find(p, bind_name, &bind_tex)) {
// Clean up texture bindings and move on to the next hook
MP_DBG(p, "Skipping hook on %s due to no texture named %s.\n",
name, bind_name);
p->pass_tex_num -= t;
return false;
}
hook_prelude(p, bind_name, pass_bind(p, bind_tex), bind_tex);
next_bind: ;
}
return true;
}
// Process hooks for a plane, saving the result and returning a new img_tex
// If 'trans' is NULL, the shader is forbidden from transforming tex
static struct img_tex pass_hook(struct gl_video *p, const char *name,
@ -1407,32 +1460,8 @@ found:
continue;
}
// Bind all necessary textures and add them to the prelude
for (int t = 0; t < TEXUNIT_VIDEO_NUM; t++) {
const char *bind_name = hook->bind_tex[t];
struct img_tex bind_tex;
if (!bind_name)
continue;
// This is a special name that means "currently hooked texture"
if (strcmp(bind_name, "HOOKED") == 0) {
int id = pass_bind(p, tex);
hook_prelude(p, "HOOKED", id, tex);
hook_prelude(p, name, id, tex);
continue;
}
if (!saved_tex_find(p, bind_name, &bind_tex)) {
// Clean up texture bindings and move on to the next hook
MP_DBG(p, "Skipping hook on %s due to no texture named %s.\n",
name, bind_name);
p->pass_tex_num -= t;
goto next_hook;
}
hook_prelude(p, bind_name, pass_bind(p, bind_tex), bind_tex);
}
if (!pass_hook_setup_binds(p, name, tex, hook))
continue;
// Run the actual hook. This generates a series of GLSL shader
// instructions sufficient for drawing the hook's output
@ -1471,8 +1500,6 @@ found:
}
saved_tex_store(p, store_name, saved_tex);
next_hook: ;
}
return tex;
@ -1825,13 +1852,15 @@ static bool img_tex_equiv(struct img_tex a, struct img_tex b)
gl_transform_eq(a.transform, b.transform);
}
static void pass_add_hook(struct gl_video *p, struct tex_hook hook)
static bool add_hook(struct gl_video *p, struct tex_hook hook)
{
if (p->tex_hook_num < SHADER_MAX_PASSES) {
p->tex_hooks[p->tex_hook_num++] = hook;
return true;
} else {
MP_ERR(p, "Too many passes! Limit is %d.\n", SHADER_MAX_PASSES);
talloc_free(hook.priv);
return false;
}
}
@ -1896,7 +1925,7 @@ static bool szexp_lookup(void *priv, struct bstr var, float size[2])
static bool user_hook_cond(struct gl_video *p, struct img_tex tex, void *priv)
{
struct gl_user_shader *shader = priv;
struct gl_user_shader_hook *shader = priv;
assert(shader);
float res = false;
@ -1907,7 +1936,7 @@ static bool user_hook_cond(struct gl_video *p, struct img_tex tex, void *priv)
static void user_hook(struct gl_video *p, struct img_tex tex,
struct gl_transform *trans, void *priv)
{
struct gl_user_shader *shader = priv;
struct gl_user_shader_hook *shader = priv;
assert(shader);
pass_describe(p, "user shader: %.*s (%s)", BSTR_P(shader->pass_desc),
@ -1928,33 +1957,91 @@ static void user_hook(struct gl_video *p, struct img_tex tex,
gl_transform_trans(shader->offset, trans);
}
static void pass_hook_user_shaders(struct gl_video *p, char **shaders)
static bool add_user_hook(void *priv, struct gl_user_shader_hook hook)
{
struct gl_video *p = priv;
struct gl_user_shader_hook *copy = talloc_ptrtype(p, copy);
*copy = hook;
struct tex_hook texhook = {
.save_tex = bstrdup0(copy, hook.save_tex),
.components = hook.components,
.hook = user_hook,
.cond = user_hook_cond,
.priv = copy,
};
for (int h = 0; h < SHADER_MAX_HOOKS; h++)
texhook.hook_tex[h] = bstrdup0(copy, hook.hook_tex[h]);
for (int h = 0; h < SHADER_MAX_BINDS; h++)
texhook.bind_tex[h] = bstrdup0(copy, hook.bind_tex[h]);
return add_hook(p, texhook);
}
static bool add_user_tex(void *priv, struct gl_user_shader_tex tex)
{
struct gl_video *p = priv;
GL *gl = p->gl;
if (p->user_tex_num == SHADER_MAX_PASSES) {
MP_ERR(p, "Too many textures! Limit is %d.\n", SHADER_MAX_PASSES);
goto err;
}
const struct gl_format *format = gl_find_format(gl, tex.mpgl_type,
tex.gl_filter == GL_LINEAR ? F_TF : 0, tex.bytes, tex.components);
if (!format) {
MP_ERR(p, "Could not satisfy format requirements for user "
"shader texture '%.*s'!\n", BSTR_P(tex.name));
goto err;
}
GLenum type = format->type,
ifmt = format->internal_format,
fmt = format->format;
GLenum tgt = tex.gl_target;
gl->GenTextures(1, &tex.gl_tex);
gl->BindTexture(tgt, tex.gl_tex);
gl->PixelStorei(GL_UNPACK_ALIGNMENT, 1);
if (tgt == GL_TEXTURE_3D) {
gl->TexImage3D(tgt, 0, ifmt, tex.w, tex.h, tex.d, 0, fmt, type, tex.texdata);
gl->TexParameteri(tgt, GL_TEXTURE_WRAP_S, tex.gl_border);
gl->TexParameteri(tgt, GL_TEXTURE_WRAP_T, tex.gl_border);
gl->TexParameteri(tgt, GL_TEXTURE_WRAP_R, tex.gl_border);
} else if (tgt == GL_TEXTURE_2D) {
gl->TexImage2D(tgt, 0, ifmt, tex.w, tex.h, 0, fmt, type, tex.texdata);
gl->TexParameteri(tgt, GL_TEXTURE_WRAP_S, tex.gl_border);
gl->TexParameteri(tgt, GL_TEXTURE_WRAP_T, tex.gl_border);
} else {
gl->TexImage1D(tgt, 0, ifmt, tex.w, 0, fmt, type, tex.texdata);
gl->TexParameteri(tgt, GL_TEXTURE_WRAP_S, tex.gl_border);
}
talloc_free(tex.texdata);
gl->TexParameteri(tgt, GL_TEXTURE_MIN_FILTER, tex.gl_filter);
gl->TexParameteri(tgt, GL_TEXTURE_MAG_FILTER, tex.gl_filter);
gl->BindTexture(tgt, 0);
p->user_textures[p->user_tex_num++] = tex;
return true;
err:
talloc_free(tex.texdata);
return false;
}
static void load_user_shaders(struct gl_video *p, char **shaders)
{
if (!shaders)
return;
for (int n = 0; shaders[n] != NULL; n++) {
struct bstr file = load_cached_file(p, shaders[n]);
struct gl_user_shader out;
while (parse_user_shader_pass(p->log, &file, &out)) {
struct gl_user_shader *hook = talloc_ptrtype(p, hook);
*hook = out;
struct tex_hook texhook = {
.save_tex = bstrdup0(hook, hook->save_tex),
.components = hook->components,
.hook = user_hook,
.cond = user_hook_cond,
.priv = hook,
};
for (int h = 0; h < SHADER_MAX_HOOKS; h++)
texhook.hook_tex[h] = bstrdup0(hook, hook->hook_tex[h]);
for (int h = 0; h < SHADER_MAX_BINDS; h++)
texhook.bind_tex[h] = bstrdup0(hook, hook->bind_tex[h]);
pass_add_hook(p, texhook);
}
parse_user_shader(p->log, file, p, add_user_hook, add_user_tex);
}
}
@ -1963,7 +2050,7 @@ static void gl_video_setup_hooks(struct gl_video *p)
gl_video_reset_hooks(p);
if (p->opts.deband) {
pass_add_hook(p, (struct tex_hook) {
add_hook(p, (struct tex_hook) {
.hook_tex = {"LUMA", "CHROMA", "RGB", "XYZ"},
.bind_tex = {"HOOKED"},
.hook = deband_hook,
@ -1971,14 +2058,14 @@ static void gl_video_setup_hooks(struct gl_video *p)
}
if (p->opts.unsharp != 0.0) {
pass_add_hook(p, (struct tex_hook) {
add_hook(p, (struct tex_hook) {
.hook_tex = {"MAIN"},
.bind_tex = {"HOOKED"},
.hook = unsharp_hook,
});
}
pass_hook_user_shaders(p, p->opts.user_shaders);
load_user_shaders(p, p->opts.user_shaders);
}
// sample from video textures, set "color" variable to yuv value