Commit 88008255 authored by Niklas Haas's avatar Niklas Haas

(WIP) add custom shader support

TODO:
- come up with a clean interface that hides away the messy bstr/texture
  business from the mpv universe
- integrate into the renderer
parent 4214a660
Pipeline #6399 failed with stages
in 31 seconds
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
#include <libplacebo/gpu.h> #include <libplacebo/gpu.h>
#include <libplacebo/shaders/colorspace.h> #include <libplacebo/shaders/colorspace.h>
#include <libplacebo/shaders/sampling.h> #include <libplacebo/shaders/sampling.h>
#include <libplacebo/shaders/custom.h>
#include <libplacebo/swapchain.h> #include <libplacebo/swapchain.h>
struct pl_renderer; struct pl_renderer;
...@@ -114,6 +115,11 @@ struct pl_render_params { ...@@ -114,6 +115,11 @@ struct pl_render_params {
// If NULL, this feature is disabled. // If NULL, this feature is disabled.
const struct pl_cone_params *cone_params; const struct pl_cone_params *cone_params;
// List of custom user shaders / hooks.
// See <libplacebo/shaders/custom.h> for more information.
const struct pl_hook *hooks;
int num_hooks;
// --- Performance / quality trade-off options: // --- Performance / quality trade-off options:
// These should generally be left off where quality is desired, as they can // These should generally be left off where quality is desired, as they can
// degrade the result quite noticeably; but may be useful for older or // degrade the result quite noticeably; but may be useful for older or
......
/*
* 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/>.
*/
#ifndef LIBPLACEBO_SHADERS_CUSTOM_H_
#define LIBPLACEBO_SHADERS_CUSTOM_H_
// Framework for enabling custom user shader hooks, as well as compatibility
// functions for parsing shaders in mpv format.
#include <libplacebo/shaders.h>
// Which "rendering stages" are available for user shader hooking purposes.
// Except where otherwise noted, all stages are "non-resizable", i.e. the
// shaders already have specific output size requirements.
enum pl_hook_stage {
// Hook stages for the untouched planes, as made available by the source.
// These are all resizable, i.e. there are no specific output stage
// requirements.
PL_HOOK_RGB_INPUT = 1 << 0,
PL_HOOK_LUMA_INPUT = 1 << 1,
PL_HOOK_CHROMA_INPUT = 1 << 2,
PL_HOOK_ALPHA_INPUT = 1 << 3,
PL_HOOK_XYZ_INPUT = 1 << 4,
// Hook stages for the scaled/aligned planes
PL_HOOK_CHROMA_SCALED = 1 << 5,
PL_HOOK_ALPHA_SCALED = 1 << 6,
PL_HOOK_NATIVE = 1 << 7, // Combined image in its native color space
PL_HOOK_RGB = 1 << 8, // After conversion to RGB, before overlays (resizable)
PL_HOOK_RGB_OVERLAY = 1 << 9, // After conversion to RGB, with overlays (resizable)
PL_HOOK_LINEAR = 1 << 10, // After linearization but before scaling
PL_HOOK_SIGMOID = 1 << 11, // After sigmoidization
PL_HOOK_PREKERNEL = 1 << 12, // Immediately before the main scaler kernel
PL_HOOK_POSTKERNEL = 1 << 13, // Immediately after the main scaler kernel
PL_HOOK_SCALED = 1 << 14, // After scaling, before color management
PL_HOOK_OUTPUT = 1 << 15, // After color management, before dithering
};
struct pl_hook_params {
const struct pl_gpu *gpu;
enum pl_hook_stage stage; // Which stage triggered the hook
// The shader object, which the user may modify. The shader is guaranteed
// to have the current signature requested by the user in `pl_hook.input`.
// Note that this shader might have specific output size requirements,
// depending on the exact shader stage hooked by the user.
struct pl_shader *sh;
// When the signature is `PL_SHADER_SIG_NONE`, the user may instead sample
// from this texture.
const struct pl_tex *tex;
// The current effective colorspace and representation, of either the
// pre-sampled color (in `sh`), or the contents of `tex`, respectively.
struct pl_color_repr repr;
struct pl_color_space color;
int components;
};
struct pl_hook {
enum pl_hook_stage stages; // Which stages to hook on
enum pl_shader_sig input; // Which input signature this hook expects
// The hook function, which is run by the renderer.
void (*hook)(const struct pl_hook_params *params);
};
// AV1 film grain parameters. For the exact meaning of these, see the
// specification (section 6.8.20).
//
// NOTE: These parameters are currently *not* sanity checked. Calling these
// functions with e.g. too large `num_points_y` or negative width/height is UB!
// Please make sure to sanity check the grain parameters signalled by the file
// before calling into these functions.
struct pl_grain_params {
int width, height; // dimensions of the image (luma)
int sub_x, sub_y; // subsampling shifts for the chroma planes
struct pl_color_repr repr; // underlying color system
// Some notes apply to `repr`:
// - repr.bits affects the rounding for grain generation
// - repr.levels affects whether or not we clip to full range or not
// - repr.sys affects whether channels 1 and 2 are treated like chroma
uint16_t grain_seed;
int num_points_y;
uint8_t points_y[14][2]; // [n][0] = value, [n][1] = scaling
bool chroma_scaling_from_luma;
int num_points_uv[2]; // should be {0} for grayscale images
uint8_t points_uv[2][10][2]; // like points_y for points_uv[0, 1] = u, v
int scaling_shift;
int ar_coeff_lag;
int8_t ar_coeffs_y[24];
int8_t ar_coeffs_uv[2][25];
int ar_coeff_shift;
int grain_scale_shift;
int8_t uv_mult[2];
int8_t uv_mult_luma[2];
int16_t uv_offset[2]; // 9-bit value, range [-256, 255]
bool overlap;
};
// Apply AV1 film grain to the channels given in `channel_map`, which maps
// from the component index of the vec4 color to the channel contained in that
// index, or PL_CHANNEL_NONE for unused channels.
//
// For example, if this is the pass for the subsampled Cb and Cr planes, which
// are currently available in color.xy, then `channels` would be:
// {PL_CHANNEL_CB, PL_CHANNEL_CR, PL_CHANNEL_NONE} = {1, 2, -1}
//
// When applying grain to the channels 1 and 2 channels, access to information
// from channel 0 is needed. It's important to take this information from the
// undistorted plane (before applying grain), and must be passed as the texture
// `luma_tex` - unless the channel map already includes channel 0 channel.
//
// So for example, for planar YCbCr content, grain must be added to the chroma
// channels first, then followed by the luma channels. (For packed content like
// rgb24 where all channels are part of the same pass, this is unnecessary)
//
// Note: all of this applies even if params->repr.sys == PL_COLOR_SYSTEM_RGB (!)
void pl_shader_av1_grain(struct pl_shader *sh,
struct pl_shader_obj **grain_state,
const enum pl_channel channels[3],
const struct pl_tex *luma_tex,
const struct pl_grain_params *params);
#endif // LIBPLACEBO_SHADERS_AV1_H_
...@@ -107,6 +107,7 @@ sources = [ ...@@ -107,6 +107,7 @@ sources = [
'shaders.c', 'shaders.c',
'shaders/av1.c', 'shaders/av1.c',
'shaders/colorspace.c', 'shaders/colorspace.c',
'shaders/custom.c',
'shaders/sampling.c', 'shaders/sampling.c',
'spirv.c', 'spirv.c',
'swapchain.c', 'swapchain.c',
......
/*
* 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 "gpu.h"
#include "shaders.h"
#include "shaders/custom.h"
static bool parse_rpn_szexpr(struct bstr line, struct szexp out[MAX_SZEXP_SIZE])
{
int pos = 0;
while (line.len > 0) {
struct bstr word = bstr_strip(bstr_splitchar(line, &line, ' '));
if (word.len == 0)
continue;
if (pos >= MAX_SZEXP_SIZE)
return false;
struct szexp *exp = &out[pos++];
if (bstr_eatend0(&word, ".w") || bstr_eatend0(&word, ".width")) {
exp->tag = SZEXP_VAR_W;
exp->val.varname = word;
continue;
}
if (bstr_eatend0(&word, ".h") || bstr_eatend0(&word, ".height")) {
exp->tag = SZEXP_VAR_H;
exp->val.varname = word;
continue;
}
switch (word.start[0]) {
case '+': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_ADD; continue;
case '-': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_SUB; continue;
case '*': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_MUL; continue;
case '/': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_DIV; continue;
case '!': exp->tag = SZEXP_OP1; exp->val.op = SZEXP_OP_NOT; continue;
case '>': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_GT; continue;
case '<': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_LT; continue;
}
if (word.start[0] >= '0' && word.start[0] <= '9') {
exp->tag = SZEXP_CONST;
if (bstr_sscanf(word, "%f", &exp->val.cval) != 1)
return false;
continue;
}
// Some sort of illegal expression
return false;
}
return true;
}
// Returns whether successful. 'result' is left untouched on failure
bool pl_eval_szexpr(struct pl_context *ctx, void *priv,
bool (*lookup)(void *priv, struct bstr var, float size[2]),
struct szexp expr[MAX_SZEXP_SIZE],
float *result)
{
float stack[MAX_SZEXP_SIZE] = {0};
int idx = 0; // points to next element to push
for (int i = 0; i < MAX_SZEXP_SIZE; i++) {
switch (expr[i].tag) {
case SZEXP_END:
goto done;
case SZEXP_CONST:
// Since our SZEXPs are bound by MAX_SZEXP_SIZE, it should be
// impossible to overflow the stack
assert(idx < MAX_SZEXP_SIZE);
stack[idx++] = expr[i].val.cval;
continue;
case SZEXP_OP1:
if (idx < 1) {
pl_warn(ctx, "Stack underflow in RPN expression!");
return false;
}
switch (expr[i].val.op) {
case SZEXP_OP_NOT: stack[idx-1] = !stack[idx-1]; break;
default: abort();
}
continue;
case SZEXP_OP2:
if (idx < 2) {
pl_warn(ctx, "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 SZEXP_OP_ADD: res = op1 + op2; break;
case SZEXP_OP_SUB: res = op1 - op2; break;
case SZEXP_OP_MUL: res = op1 * op2; break;
case SZEXP_OP_DIV: res = op1 / op2; break;
case SZEXP_OP_GT: res = op1 > op2; break;
case SZEXP_OP_LT: res = op1 < op2; break;
default: abort();
}
if (!isfinite(res)) {
pl_warn(ctx, "Illegal operation in RPN expression!");
return false;
}
stack[idx++] = res;
continue;
case SZEXP_VAR_W:
case SZEXP_VAR_H: {
struct bstr name = expr[i].val.varname;
float size[2];
if (!lookup(priv, name, size)) {
pl_warn(ctx, "Variable '%.*s' not found in RPN expression!",
BSTR_P(name));
return false;
}
stack[idx++] = (expr[i].tag == SZEXP_VAR_W) ? size[0] : size[1];
continue;
}
}
}
done:
// Return the single stack element
if (idx != 1) {
pl_warn(ctx, "Malformed stack after RPN expression!");
return false;
}
*result = stack[0];
return true;
}
static bool parse_hook(struct pl_context *ctx, struct bstr *body,
struct custom_shader_hook *out)
{
*out = (struct custom_shader_hook){
.pass_desc = bstr0("(unknown)"),
.offset = pl_transform2x2_identity,
.width = {{ SZEXP_VAR_W, { .varname = bstr0("HOOKED") }}},
.height = {{ SZEXP_VAR_H, { .varname = bstr0("HOOKED") }}},
.cond = {{ SZEXP_CONST, { .cval = 1.0 }}},
};
int hook_idx = 0;
int bind_idx = 0;
// Parse all headers
while (true) {
struct bstr rest;
struct bstr line = bstr_strip(bstr_getline(*body, &rest));
// Check for the presence of the magic line beginning
if (!bstr_eatstart0(&line, "//!"))
break;
*body = rest;
// Parse the supported commands
if (bstr_eatstart0(&line, "HOOK")) {
if (hook_idx == SHADER_MAX_HOOKS) {
pl_err(ctx, "Passes may only hook up to %d textures!",
SHADER_MAX_HOOKS);
return false;
}
out->hook_tex[hook_idx++] = bstr_strip(line);
continue;
}
if (bstr_eatstart0(&line, "BIND")) {
if (bind_idx == SHADER_MAX_BINDS) {
pl_err(ctx, "Passes may only bind up to %d textures!",
SHADER_MAX_BINDS);
return false;
}
out->bind_tex[bind_idx++] = bstr_strip(line);
continue;
}
if (bstr_eatstart0(&line, "SAVE")) {
out->save_tex = bstr_strip(line);
continue;
}
if (bstr_eatstart0(&line, "DESC")) {
out->pass_desc = bstr_strip(line);
continue;
}
if (bstr_eatstart0(&line, "OFFSET")) {
float ox, oy;
if (bstr_sscanf(line, "%f %f", &ox, &oy) != 2) {
pl_err(ctx, "Error while parsing OFFSET!");
return false;
}
out->offset.c[0] = ox;
out->offset.c[1] = oy;
continue;
}
if (bstr_eatstart0(&line, "WIDTH")) {
if (!parse_rpn_szexpr(line, out->width)) {
pl_err(ctx, "Error while parsing WIDTH!");
return false;
}
continue;
}
if (bstr_eatstart0(&line, "HEIGHT")) {
if (!parse_rpn_szexpr(line, out->height)) {
pl_err(ctx, "Error while parsing HEIGHT!");
return false;
}
continue;
}
if (bstr_eatstart0(&line, "WHEN")) {
if (!parse_rpn_szexpr(line, out->cond)) {
pl_err(ctx, "Error while parsing WHEN!");
return false;
}
continue;
}
if (bstr_eatstart0(&line, "COMPONENTS")) {
if (bstr_sscanf(line, "%d", &out->components) != 1) {
pl_err(ctx, "Error while parsing COMPONENTS!");
return false;
}
continue;
}
if (bstr_eatstart0(&line, "COMPUTE")) {
int num = bstr_sscanf(line, "%d %d %d %d",
&out->block_w, &out->block_h,
&out->threads_w, &out->threads_h);
if (num == 2 || num == 4) {
out->is_compute = true;
} else {
pl_err(log, "Error while parsing COMPUTE!");
return false;
}
continue;
}
// Unknown command type
pl_err(ctx, "Unrecognized command '%.*s'!", BSTR_P(line));
return false;
}
// The rest of the file up until the next magic line beginning (if any)
// shall be the shader body
if (bstr_split_tok(*body, "//!", &out->pass_body, body)) {
// Make sure the magic line is part of the rest
body->start -= 3;
body->len += 3;
}
// Sanity checking
if (hook_idx == 0)
pl_warn(ctx, "Pass has no hooked textures (will be ignored)!");
return true;
}
static bool parse_tex(const struct pl_gpu *gpu, struct bstr *body,
struct custom_shader_tex *out)
{
*out = (struct custom_shader_tex){
.name = bstr0("USER_TEX"),
.params = {
.w = 1, .h = 1, .d = 0,
.sampleable = true,
},
};
struct pl_tex_params *p = &out->params;
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 dims = bstr_sscanf(line, "%d %d %d", &p->w, &p->h, &p->d);
int 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:
if (p->d < 1 || p->d > lim) {
PL_ERR(gpu, "SIZE %d exceeds GPU's texture size limits (%d)!",
p->d, lim);
return false;
}
// fall through
case 2:
if (p->h < 1 || p->h > lim) {
PL_ERR(gpu, "SIZE %d exceeds GPU's texture size limits (%d)!",
p->h, lim);
return false;
}
// fall through
case 1:
if (p->w < 1 || p->w > lim) {
PL_ERR(gpu, "SIZE %d exceeds GPU's texture size limits (%d)!",
p->w, lim);
return false;
}
break;
default:
PL_ERR(gpu, "Error while parsing SIZE!");
return false;
};
// Clear out the superfluous components
if (dims < 3)
p->d = 0;
if (dims < 2)
p->h = 0;
continue;
}
if (bstr_eatstart0(&line, "FORMAT ")) {
p->format = NULL;
for (int n = 0; n < gpu->num_formats; n++) {
const struct pl_fmt *fmt = gpu->formats[n];
if (bstr_equals0(line, fmt->name)) {
p->format = fmt;
break;
}
}
if (!p->format || !p->format->opaque) {
PL_ERR(gpu, "Unrecognized/unavailable FORMAT name: '%.*s'!",
BSTR_P(line));
return false;
}
if (!(p->format->caps & PL_FMT_CAP_SAMPLEABLE)) {
PL_ERR(gpu, "Chosen FORMAT '%.*s' is not sampleable!",
BSTR_P(line));
return false;
}
continue;
}
if (bstr_eatstart0(&line, "FILTER")) {
line = bstr_strip(line);
if (bstr_equals0(line, "LINEAR")) {
p->sample_mode = PL_TEX_SAMPLE_LINEAR;
} else if (bstr_equals0(line, "NEAREST")) {
p->sample_mode = PL_TEX_SAMPLE_NEAREST;
} else {
PL_ERR(gpu, "Unrecognized FILTER: '%.*s'!", BSTR_P(line));
return false;
}
continue;
}
if (bstr_eatstart0(&line, "BORDER")) {
line = bstr_strip(line);
if (bstr_equals0(line, "CLAMP")) {
p->address_mode = PL_TEX_ADDRESS_CLAMP;
} else if (bstr_equals0(line, "REPEAT")) {
p->address_mode = PL_TEX_ADDRESS_REPEAT;
} else if (bstr_equals0(line, "MIRROR")) {
p->address_mode = PL_TEX_ADDRESS_MIRROR;
} else {
PL_ERR(gpu, "Unrecognized BORDER: '%.*s'!", BSTR_P(line));
return false;
}
continue;
}
PL_ERR(gpu, "Unrecognized command '%.*s'!", BSTR_P(line));
return false;
}
if (!p->format) {
PL_ERR(gpu, "No FORMAT specified!");
return false;
}
int caps = p->format->caps;
if (p->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
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)) {
PL_ERR(gpu, "Error while parsing TEXTURE body: must be a valid "
"hexadecimal sequence, on a single line!");
return false;
}
int texels = p->w * PL_DEF(p->h, 1) * PL_DEF(p->d, 1);
size_t expected_len = texels * p->format->texel_size;
if (tex.len != expected_len) {
PL_ERR(gpu, "Shader TEXTURE size mismatch: got %zu bytes, expected %zu!",
tex.len, expected_len);
talloc_free(tex.start);
return false;
}
p->initial_data = tex.start;
return true;
}
void pl_parse_user_shader(const struct pl_gpu *gpu, struct bstr shader, void *priv,
bool (*dohook)(void *p, struct custom_shader_hook hook),
bool (*dotex)(void *p, struct custom_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) {
PL_WARN(gpu, "Shader appears to contain no headers!");
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 custom_shader_tex t;
if (!parse_tex(gpu, &shader, &t) || !dotex(priv, t))
return;
continue;
}
struct custom_shader_hook h;
if (!parse_hook(gpu->ctx, &shader, &h) || !dohook(priv, h))
return;
}
}
/*
* 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/>.
*/
#pragma once
#include "common.h"
#include "shaders.h"
// Hard-coded size limits, mainly for convenience (to avoid dynamic memory)
#define SHADER_MAX_HOOKS 16
#define SHADER_MAX_BINDS 16
#define MAX_SZEXP_SIZE 32
enum szexp_op {
SZEXP_OP_ADD,
SZEXP_OP_SUB,
SZEXP_OP_MUL,
SZEXP_OP_DIV,
SZEXP_OP_NOT,
SZEXP_OP_GT,
SZEXP_OP_LT,
};
enum szexp_tag {
SZEXP_END = 0, // End of an RPN expression
SZEXP_CONST, // Push a constant value onto the stack
SZEXP_VAR_W, // Get the width/height of a named texture (variable)
SZEXP_VAR_H,
SZEXP_OP2, // Pop two elements and push the result of a dyadic operation
SZEXP_OP1, // Pop one element and push the result of a monadic operation
};
struct szexp {
enum szexp_tag tag;
union {
float cval;
struct bstr varname;
enum szexp_op op;
} val;
};
struct custom_shader_hook {
// Variable/literal names of textures
struct bstr pass_desc;
struct bstr hook_tex[SHADER_MAX_HOOKS];
struct bstr bind_tex[SHADER_MAX_BINDS];
struct bstr save_tex;
// Shader body itself + metadata
struct bstr pass_body;
struct pl_transform2x2 offset;
int components;
// Special expressions governing the output size and execution conditions