/*
* 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 "common.h"
#include "filters.h"
#include "hash.h"
#include "shaders.h"
#include "dispatch.h"
#include
struct cached_frame {
uint64_t signature;
uint64_t params_hash; // for detecting `pl_render_params` changes
struct pl_color_space color;
struct pl_icc_profile profile;
pl_rect2df crop;
pl_tex tex;
int comps;
bool evict; // for garbage collection
};
struct sampler {
pl_shader_obj upscaler_state;
pl_shader_obj downscaler_state;
};
struct osd_vertex {
float pos[2];
float coord[2];
float color[4];
};
struct icc_state {
pl_icc_object icc;
uint64_t error; // set to profile signature on failure
};
struct pl_renderer_t {
pl_gpu gpu;
pl_dispatch dp;
pl_log log;
// Cached feature checks (inverted)
enum pl_render_error errors;
// List containing signatures of disabled hooks
PL_ARRAY(uint64_t) disabled_hooks;
// Shader resource objects and intermediate textures (FBOs)
pl_shader_obj tone_map_state;
pl_shader_obj dither_state;
pl_shader_obj grain_state[4];
pl_shader_obj lut_state[3];
pl_shader_obj icc_state[2];
PL_ARRAY(pl_tex) fbos;
struct sampler sampler_main;
struct sampler sampler_contrast;
struct sampler samplers_src[4];
struct sampler samplers_dst[4];
// Temporary storage for vertex/index data
PL_ARRAY(struct osd_vertex) osd_vertices;
PL_ARRAY(uint16_t) osd_indices;
struct pl_vertex_attrib osd_attribs[3];
// Frame cache (for frame mixing / interpolation)
PL_ARRAY(struct cached_frame) frames;
PL_ARRAY(pl_tex) frame_fbos;
// For debugging / logging purposes
int prev_dither;
// For backwards compatibility
struct icc_state icc_fallback[2];
};
enum {
// Index into `lut_state`
LUT_IMAGE,
LUT_TARGET,
LUT_PARAMS,
};
enum {
// Index into `icc_state`
ICC_IMAGE,
ICC_TARGET
};
pl_renderer pl_renderer_create(pl_log log, pl_gpu gpu)
{
pl_renderer rr = pl_alloc_ptr(NULL, rr);
*rr = (struct pl_renderer_t) {
.gpu = gpu,
.log = log,
.dp = pl_dispatch_create(log, gpu),
.osd_attribs = {
{
.name = "pos",
.offset = offsetof(struct osd_vertex, pos),
.fmt = pl_find_vertex_fmt(gpu, PL_FMT_FLOAT, 2),
}, {
.name = "coord",
.offset = offsetof(struct osd_vertex, coord),
.fmt = pl_find_vertex_fmt(gpu, PL_FMT_FLOAT, 2),
}, {
.name = "osd_color",
.offset = offsetof(struct osd_vertex, color),
.fmt = pl_find_vertex_fmt(gpu, PL_FMT_FLOAT, 4),
}
},
};
assert(rr->dp);
return rr;
}
static void sampler_destroy(pl_renderer rr, struct sampler *sampler)
{
pl_shader_obj_destroy(&sampler->upscaler_state);
pl_shader_obj_destroy(&sampler->downscaler_state);
}
void pl_renderer_destroy(pl_renderer *p_rr)
{
pl_renderer rr = *p_rr;
if (!rr)
return;
// Free all intermediate FBOs
for (int i = 0; i < rr->fbos.num; i++)
pl_tex_destroy(rr->gpu, &rr->fbos.elem[i]);
for (int i = 0; i < rr->frames.num; i++)
pl_tex_destroy(rr->gpu, &rr->frames.elem[i].tex);
for (int i = 0; i < rr->frame_fbos.num; i++)
pl_tex_destroy(rr->gpu, &rr->frame_fbos.elem[i]);
// Free all shader resource objects
pl_shader_obj_destroy(&rr->tone_map_state);
pl_shader_obj_destroy(&rr->dither_state);
for (int i = 0; i < PL_ARRAY_SIZE(rr->lut_state); i++)
pl_shader_obj_destroy(&rr->lut_state[i]);
for (int i = 0; i < PL_ARRAY_SIZE(rr->grain_state); i++)
pl_shader_obj_destroy(&rr->grain_state[i]);
for (int i = 0; i < PL_ARRAY_SIZE(rr->icc_state); i++)
pl_shader_obj_destroy(&rr->icc_state[i]);
// Free all samplers
sampler_destroy(rr, &rr->sampler_main);
sampler_destroy(rr, &rr->sampler_contrast);
for (int i = 0; i < PL_ARRAY_SIZE(rr->samplers_src); i++)
sampler_destroy(rr, &rr->samplers_src[i]);
for (int i = 0; i < PL_ARRAY_SIZE(rr->samplers_dst); i++)
sampler_destroy(rr, &rr->samplers_dst[i]);
// Free fallback ICC profiles
for (int i = 0; i < PL_ARRAY_SIZE(rr->icc_fallback); i++)
pl_icc_close(&rr->icc_fallback[i].icc);
pl_dispatch_destroy(&rr->dp);
pl_free_ptr(p_rr);
}
size_t pl_renderer_save(pl_renderer rr, uint8_t *out)
{
return pl_cache_save(pl_gpu_cache(rr->gpu), out, out ? SIZE_MAX : 0);
}
void pl_renderer_load(pl_renderer rr, const uint8_t *cache)
{
pl_cache_load(pl_gpu_cache(rr->gpu), cache, SIZE_MAX);
}
void pl_renderer_flush_cache(pl_renderer rr)
{
for (int i = 0; i < rr->frames.num; i++)
pl_tex_destroy(rr->gpu, &rr->frames.elem[i].tex);
rr->frames.num = 0;
pl_reset_detected_peak(rr->tone_map_state);
}
const struct pl_render_params pl_render_fast_params = { PL_RENDER_DEFAULTS };
const struct pl_render_params pl_render_default_params = {
PL_RENDER_DEFAULTS
.upscaler = &pl_filter_lanczos,
.downscaler = &pl_filter_hermite,
.frame_mixer = &pl_filter_oversample,
.sigmoid_params = &pl_sigmoid_default_params,
.dither_params = &pl_dither_default_params,
.peak_detect_params = &pl_peak_detect_default_params,
};
const struct pl_render_params pl_render_high_quality_params = {
PL_RENDER_DEFAULTS
.upscaler = &pl_filter_ewa_lanczossharp,
.downscaler = &pl_filter_hermite,
.frame_mixer = &pl_filter_oversample,
.sigmoid_params = &pl_sigmoid_default_params,
.peak_detect_params = &pl_peak_detect_high_quality_params,
.color_map_params = &pl_color_map_high_quality_params,
.dither_params = &pl_dither_default_params,
.deband_params = &pl_deband_default_params,
};
const struct pl_filter_preset pl_frame_mixers[] = {
{ "none", NULL, "No frame mixing" },
{ "linear", &pl_filter_bilinear, "Linear frame mixing" },
{ "oversample", &pl_filter_oversample, "Oversample (AKA SmoothMotion)" },
{ "mitchell_clamp", &pl_filter_mitchell_clamp, "Clamped Mitchell spline" },
{ "hermite", &pl_filter_hermite, "Cubic spline (Hermite)" },
{0}
};
const int pl_num_frame_mixers = PL_ARRAY_SIZE(pl_frame_mixers) - 1;
const struct pl_filter_preset pl_scale_filters[] = {
{"none", NULL, "Built-in sampling"},
{"oversample", &pl_filter_oversample, "Oversample (Aspect-preserving NN)"},
COMMON_FILTER_PRESETS,
{0}
};
const int pl_num_scale_filters = PL_ARRAY_SIZE(pl_scale_filters) - 1;
// Represents a "in-flight" image, which is either a shader that's in the
// process of producing some sort of image, or a texture that needs to be
// sampled from
struct img {
// Effective texture size, always set
int w, h;
// Recommended format (falls back to fbofmt otherwise), only for shaders
pl_fmt fmt;
// Exactly *one* of these two is set:
pl_shader sh;
pl_tex tex;
// If true, created shaders will be set to unique
bool unique;
// Information about what to log/disable/fallback to if the shader fails
const char *err_msg;
enum pl_render_error err_enum;
pl_tex err_tex;
// Current effective source area, will be sampled by the main scaler
pl_rect2df rect;
// The current effective colorspace
struct pl_color_repr repr;
struct pl_color_space color;
int comps;
};
// Plane 'type', ordered by incrementing priority
enum plane_type {
PLANE_INVALID = 0,
PLANE_ALPHA,
PLANE_CHROMA,
PLANE_LUMA,
PLANE_RGB,
PLANE_XYZ,
};
static inline enum plane_type detect_plane_type(const struct pl_plane *plane,
const struct pl_color_repr *repr)
{
if (pl_color_system_is_ycbcr_like(repr->sys)) {
int t = PLANE_INVALID;
for (int c = 0; c < plane->components; c++) {
switch (plane->component_mapping[c]) {
case PL_CHANNEL_Y: t = PL_MAX(t, PLANE_LUMA); continue;
case PL_CHANNEL_A: t = PL_MAX(t, PLANE_ALPHA); continue;
case PL_CHANNEL_CB:
case PL_CHANNEL_CR:
t = PL_MAX(t, PLANE_CHROMA);
continue;
default: continue;
}
}
pl_assert(t);
return t;
}
// Extra test for exclusive / separated alpha plane
if (plane->components == 1 && plane->component_mapping[0] == PL_CHANNEL_A)
return PLANE_ALPHA;
switch (repr->sys) {
case PL_COLOR_SYSTEM_UNKNOWN: // fall through to RGB
case PL_COLOR_SYSTEM_RGB: return PLANE_RGB;
case PL_COLOR_SYSTEM_XYZ: return PLANE_XYZ;
// For the switch completeness check
case PL_COLOR_SYSTEM_BT_601:
case PL_COLOR_SYSTEM_BT_709:
case PL_COLOR_SYSTEM_SMPTE_240M:
case PL_COLOR_SYSTEM_BT_2020_NC:
case PL_COLOR_SYSTEM_BT_2020_C:
case PL_COLOR_SYSTEM_BT_2100_PQ:
case PL_COLOR_SYSTEM_BT_2100_HLG:
case PL_COLOR_SYSTEM_DOLBYVISION:
case PL_COLOR_SYSTEM_YCGCO:
case PL_COLOR_SYSTEM_COUNT:
break;
}
pl_unreachable();
}
struct pass_state {
void *tmp;
pl_renderer rr;
const struct pl_render_params *params;
struct pl_render_info info; // for info callback
// Represents the "current" image which we're in the process of rendering.
// This is initially set by pass_read_image, and all of the subsequent
// rendering steps will mutate this in-place.
struct img img;
// Represents the "reference rect". Canonically, this is functionally
// equivalent to `image.crop`, but also updates as the refplane evolves
// (e.g. due to user hook prescalers)
pl_rect2df ref_rect;
// Integer version of `target.crop`. Semantically identical.
pl_rect2d dst_rect;
// Logical end-to-end rotation
pl_rotation rotation;
// Cached copies of the `image` / `target` for this rendering pass,
// corrected to make sure all rects etc. are properly defaulted/inferred.
struct pl_frame image;
struct pl_frame target;
// Cached copies of the `prev` / `next` frames, for deinterlacing.
struct pl_frame prev, next;
// Some extra plane metadata, inferred from `planes`
enum plane_type src_type[4];
int src_ref, dst_ref; // index into `planes`
// Metadata for `rr->fbos`
pl_fmt fbofmt[5];
bool *fbos_used;
bool need_peak_fbo; // need indirection for peak detection
// Map of acquired frames
struct {
bool target, image, prev, next;
} acquired;
};
static void find_fbo_format(struct pass_state *pass)
{
const struct pl_render_params *params = pass->params;
pl_renderer rr = pass->rr;
if (params->disable_fbos || (rr->errors & PL_RENDER_ERR_FBO) || pass->fbofmt[4])
return;
struct {
enum pl_fmt_type type;
int depth;
enum pl_fmt_caps caps;
} configs[] = {
// Prefer floating point formats first
{PL_FMT_FLOAT, 16, PL_FMT_CAP_LINEAR},
{PL_FMT_FLOAT, 16, PL_FMT_CAP_SAMPLEABLE},
// Otherwise, fall back to unorm/snorm, preferring linearly sampleable
{PL_FMT_UNORM, 16, PL_FMT_CAP_LINEAR},
{PL_FMT_SNORM, 16, PL_FMT_CAP_LINEAR},
{PL_FMT_UNORM, 16, PL_FMT_CAP_SAMPLEABLE},
{PL_FMT_SNORM, 16, PL_FMT_CAP_SAMPLEABLE},
// As a final fallback, allow 8-bit FBO formats (for UNORM only)
{PL_FMT_UNORM, 8, PL_FMT_CAP_LINEAR},
{PL_FMT_UNORM, 8, PL_FMT_CAP_SAMPLEABLE},
};
pl_fmt fmt = NULL;
for (int i = 0; i < PL_ARRAY_SIZE(configs); i++) {
if (params->force_low_bit_depth_fbos && configs[i].depth > 8)
continue;
fmt = pl_find_fmt(rr->gpu, configs[i].type, 4, configs[i].depth, 0,
PL_FMT_CAP_RENDERABLE | configs[i].caps);
if (!fmt)
continue;
pass->fbofmt[4] = fmt;
// Probe the right variant for each number of channels, falling
// back to the next biggest format
for (int c = 1; c < 4; c++) {
pass->fbofmt[c] = pl_find_fmt(rr->gpu, configs[i].type, c,
configs[i].depth, 0, fmt->caps);
pass->fbofmt[c] = PL_DEF(pass->fbofmt[c], pass->fbofmt[c+1]);
}
return;
}
PL_WARN(rr, "Found no renderable FBO format! Most features disabled");
rr->errors |= PL_RENDER_ERR_FBO;
}
static void info_callback(void *priv, const struct pl_dispatch_info *dinfo)
{
struct pass_state *pass = priv;
const struct pl_render_params *params = pass->params;
if (!params->info_callback)
return;
pass->info.pass = dinfo;
params->info_callback(params->info_priv, &pass->info);
pass->info.index++;
}
static pl_tex get_fbo(struct pass_state *pass, int w, int h, pl_fmt fmt,
int comps, pl_debug_tag debug_tag)
{
pl_renderer rr = pass->rr;
comps = PL_DEF(comps, 4);
fmt = PL_DEF(fmt, pass->fbofmt[comps]);
if (!fmt)
return NULL;
struct pl_tex_params params = {
.w = w,
.h = h,
.format = fmt,
.sampleable = true,
.renderable = true,
.blit_src = fmt->caps & PL_FMT_CAP_BLITTABLE,
.storable = fmt->caps & PL_FMT_CAP_STORABLE,
.debug_tag = debug_tag,
};
int best_idx = -1;
int best_diff = 0;
// Find the best-fitting texture out of rr->fbos
for (int i = 0; i < rr->fbos.num; i++) {
if (pass->fbos_used[i])
continue;
// Orthogonal distance, with penalty for format mismatches
int diff = abs(rr->fbos.elem[i]->params.w - w) +
abs(rr->fbos.elem[i]->params.h - h) +
((rr->fbos.elem[i]->params.format != fmt) ? 1000 : 0);
if (best_idx < 0 || diff < best_diff) {
best_idx = i;
best_diff = diff;
}
}
// No texture found at all, add a new one
if (best_idx < 0) {
best_idx = rr->fbos.num;
PL_ARRAY_APPEND(rr, rr->fbos, NULL);
pl_grow(pass->tmp, &pass->fbos_used, rr->fbos.num * sizeof(bool));
pass->fbos_used[best_idx] = false;
}
if (!pl_tex_recreate(rr->gpu, &rr->fbos.elem[best_idx], ¶ms))
return NULL;
pass->fbos_used[best_idx] = true;
return rr->fbos.elem[best_idx];
}
// Forcibly convert an img to `tex`, dispatching where necessary
static pl_tex _img_tex(struct pass_state *pass, struct img *img, pl_debug_tag tag)
{
if (img->tex) {
pl_assert(!img->sh);
return img->tex;
}
pl_renderer rr = pass->rr;
pl_tex tex = get_fbo(pass, img->w, img->h, img->fmt, img->comps, tag);
img->fmt = NULL;
if (!tex) {
PL_ERR(rr, "Failed creating FBO texture! Disabling advanced rendering..");
memset(pass->fbofmt, 0, sizeof(pass->fbofmt));
pl_dispatch_abort(rr->dp, &img->sh);
rr->errors |= PL_RENDER_ERR_FBO;
return img->err_tex;
}
pl_assert(img->sh);
bool ok = pl_dispatch_finish(rr->dp, pl_dispatch_params(
.shader = &img->sh,
.target = tex,
));
const char *err_msg = img->err_msg;
enum pl_render_error err_enum = img->err_enum;
pl_tex err_tex = img->err_tex;
img->err_msg = NULL;
img->err_enum = PL_RENDER_ERR_NONE;
img->err_tex = NULL;
if (!ok) {
PL_ERR(rr, "%s", PL_DEF(err_msg, "Failed dispatching intermediate pass!"));
rr->errors |= err_enum;
img->sh = pl_dispatch_begin(rr->dp);
img->tex = err_tex;
return img->tex;
}
img->tex = tex;
return img->tex;
}
#define img_tex(pass, img) _img_tex(pass, img, PL_DEBUG_TAG)
// Forcibly convert an img to `sh`, sampling where necessary
static pl_shader img_sh(struct pass_state *pass, struct img *img)
{
if (img->sh) {
pl_assert(!img->tex);
return img->sh;
}
pl_assert(img->tex);
img->sh = pl_dispatch_begin_ex(pass->rr->dp, img->unique);
pl_shader_sample_direct(img->sh, pl_sample_src( .tex = img->tex ));
img->tex = NULL;
return img->sh;
}
enum sampler_type {
SAMPLER_DIRECT, // pick based on texture caps
SAMPLER_NEAREST, // direct sampling, force nearest
SAMPLER_BICUBIC, // fast bicubic scaling
SAMPLER_HERMITE, // fast hermite scaling
SAMPLER_GAUSSIAN, // fast gaussian scaling
SAMPLER_COMPLEX, // complex custom filters
SAMPLER_OVERSAMPLE,
};
enum sampler_dir {
SAMPLER_NOOP, // 1:1 scaling
SAMPLER_UP, // upscaling
SAMPLER_DOWN, // downscaling
};
enum sampler_usage {
SAMPLER_MAIN,
SAMPLER_PLANE,
SAMPLER_CONTRAST,
};
struct sampler_info {
const struct pl_filter_config *config; // if applicable
enum sampler_usage usage;
enum sampler_type type;
enum sampler_dir dir;
enum sampler_dir dir_sep[2];
};
static struct sampler_info sample_src_info(struct pass_state *pass,
const struct pl_sample_src *src,
enum sampler_usage usage)
{
const struct pl_render_params *params = pass->params;
struct sampler_info info = { .usage = usage };
pl_renderer rr = pass->rr;
float rx = src->new_w / fabsf(pl_rect_w(src->rect));
if (rx < 1.0 - 1e-6) {
info.dir_sep[0] = SAMPLER_DOWN;
} else if (rx > 1.0 + 1e-6) {
info.dir_sep[0] = SAMPLER_UP;
}
float ry = src->new_h / fabsf(pl_rect_h(src->rect));
if (ry < 1.0 - 1e-6) {
info.dir_sep[1] = SAMPLER_DOWN;
} else if (ry > 1.0 + 1e-6) {
info.dir_sep[1] = SAMPLER_UP;
}
if (params->correct_subpixel_offsets) {
if (!info.dir_sep[0] && fabsf(src->rect.x0) > 1e-6f)
info.dir_sep[0] = SAMPLER_UP;
if (!info.dir_sep[1] && fabsf(src->rect.y0) > 1e-6f)
info.dir_sep[1] = SAMPLER_UP;
}
// We use PL_MAX so downscaling overrides upscaling when choosing scalers
info.dir = PL_MAX(info.dir_sep[0], info.dir_sep[1]);
switch (info.dir) {
case SAMPLER_DOWN:
if (usage == SAMPLER_CONTRAST) {
info.config = &pl_filter_bicubic;
} else if (usage == SAMPLER_PLANE && params->plane_downscaler) {
info.config = params->plane_downscaler;
} else {
info.config = params->downscaler;
}
break;
case SAMPLER_UP:
if (usage == SAMPLER_PLANE && params->plane_upscaler) {
info.config = params->plane_upscaler;
} else {
pl_assert(usage != SAMPLER_CONTRAST);
info.config = params->upscaler;
}
break;
case SAMPLER_NOOP:
info.type = SAMPLER_NEAREST;
return info;
}
if ((rr->errors & PL_RENDER_ERR_SAMPLING) || !info.config) {
info.type = SAMPLER_DIRECT;
} else if (info.config->kernel == &pl_filter_function_oversample) {
info.type = SAMPLER_OVERSAMPLE;
} else {
info.type = SAMPLER_COMPLEX;
// Try using faster replacements for GPU built-in scalers
pl_fmt texfmt = src->tex ? src->tex->params.format : pass->fbofmt[4];
bool can_linear = texfmt->caps & PL_FMT_CAP_LINEAR;
bool can_fast = info.dir == SAMPLER_UP || params->skip_anti_aliasing;
if (can_fast && !params->disable_builtin_scalers) {
if (can_linear && info.config == &pl_filter_bicubic)
info.type = SAMPLER_BICUBIC;
if (can_linear && info.config == &pl_filter_hermite)
info.type = SAMPLER_HERMITE;
if (can_linear && info.config == &pl_filter_gaussian)
info.type = SAMPLER_GAUSSIAN;
if (can_linear && info.config == &pl_filter_bilinear)
info.type = SAMPLER_DIRECT;
if (info.config == &pl_filter_nearest)
info.type = can_linear ? SAMPLER_NEAREST : SAMPLER_DIRECT;
}
}
// Disable advanced scaling without FBOs
if (!pass->fbofmt[4] && info.type == SAMPLER_COMPLEX)
info.type = SAMPLER_DIRECT;
return info;
}
static void dispatch_sampler(struct pass_state *pass, pl_shader sh,
struct sampler *sampler, enum sampler_usage usage,
pl_tex target_tex, const struct pl_sample_src *src)
{
const struct pl_render_params *params = pass->params;
if (!sampler)
goto fallback;
pl_renderer rr = pass->rr;
struct sampler_info info = sample_src_info(pass, src, usage);
pl_shader_obj *lut = NULL;
switch (info.dir) {
case SAMPLER_NOOP:
goto fallback;
case SAMPLER_DOWN:
lut = &sampler->downscaler_state;
break;
case SAMPLER_UP:
lut = &sampler->upscaler_state;
break;
}
switch (info.type) {
case SAMPLER_DIRECT:
goto fallback;
case SAMPLER_NEAREST:
pl_shader_sample_nearest(sh, src);
return;
case SAMPLER_OVERSAMPLE:
pl_shader_sample_oversample(sh, src, info.config->kernel->params[0]);
return;
case SAMPLER_BICUBIC:
pl_shader_sample_bicubic(sh, src);
return;
case SAMPLER_HERMITE:
pl_shader_sample_hermite(sh, src);
return;
case SAMPLER_GAUSSIAN:
pl_shader_sample_gaussian(sh, src);
return;
case SAMPLER_COMPLEX:
break; // continue below
}
pl_assert(lut);
struct pl_sample_filter_params fparams = {
.filter = *info.config,
.antiring = params->antiringing_strength,
.no_widening = params->skip_anti_aliasing && usage != SAMPLER_CONTRAST,
.lut = lut,
};
if (target_tex) {
fparams.no_compute = !target_tex->params.storable;
} else {
fparams.no_compute = !(pass->fbofmt[4]->caps & PL_FMT_CAP_STORABLE);
}
bool ok;
if (info.config->polar) {
// Polar samplers are always a single function call
ok = pl_shader_sample_polar(sh, src, &fparams);
} else if (info.dir_sep[0] && info.dir_sep[1]) {
// Scaling is needed in both directions
struct pl_sample_src src1 = *src, src2 = *src;
src1.new_w = src->tex->params.w;
src1.rect.x0 = 0;
src1.rect.x1 = src1.new_w;;
src2.rect.y0 = 0;
src2.rect.y1 = src1.new_h;
pl_shader tsh = pl_dispatch_begin(rr->dp);
ok = pl_shader_sample_ortho2(tsh, &src1, &fparams);
if (!ok) {
pl_dispatch_abort(rr->dp, &tsh);
goto done;
}
struct img img = {
.sh = tsh,
.w = src1.new_w,
.h = src1.new_h,
.comps = src->components,
};
src2.tex = img_tex(pass, &img);
src2.scale = 1.0;
ok = src2.tex && pl_shader_sample_ortho2(sh, &src2, &fparams);
} else {
// Scaling is needed only in one direction
ok = pl_shader_sample_ortho2(sh, src, &fparams);
}
done:
if (!ok) {
PL_ERR(rr, "Failed dispatching scaler.. disabling");
rr->errors |= PL_RENDER_ERR_SAMPLING;
goto fallback;
}
return;
fallback:
// If all else fails, fall back to auto sampling
pl_shader_sample_direct(sh, src);
}
static void swizzle_color(pl_shader sh, int comps, const int comp_map[4],
bool force_alpha)
{
ident_t orig = sh_fresh(sh, "orig_color");
GLSL("vec4 "$" = color; \n"
"color = vec4(0.0, 0.0, 0.0, 1.0); \n", orig);
static const int def_map[4] = {0, 1, 2, 3};
comp_map = PL_DEF(comp_map, def_map);
for (int c = 0; c < comps; c++) {
if (comp_map[c] >= 0)
GLSL("color[%d] = "$"[%d]; \n", c, orig, comp_map[c]);
}
if (force_alpha)
GLSL("color.a = "$".a; \n", orig);
}
// `scale` adapts from `pass->dst_rect` to the plane being rendered to
static void draw_overlays(struct pass_state *pass, pl_tex fbo,
int comps, const int comp_map[4],
const struct pl_overlay *overlays, int num,
struct pl_color_space color, struct pl_color_repr repr,
const pl_transform2x2 *output_shift)
{
pl_renderer rr = pass->rr;
if (num <= 0 || (rr->errors & PL_RENDER_ERR_OVERLAY))
return;
enum pl_fmt_caps caps = fbo->params.format->caps;
if (!(rr->errors & PL_RENDER_ERR_BLENDING) &&
!(caps & PL_FMT_CAP_BLENDABLE))
{
PL_WARN(rr, "Trying to draw an overlay to a non-blendable target. "
"Alpha blending is disabled, results may be incorrect!");
rr->errors |= PL_RENDER_ERR_BLENDING;
}
const struct pl_frame *image = pass->src_ref >= 0 ? &pass->image : NULL;
pl_transform2x2 src_to_dst;
if (image) {
float rx = pl_rect_w(pass->dst_rect) / pl_rect_w(image->crop);
float ry = pl_rect_h(pass->dst_rect) / pl_rect_h(image->crop);
src_to_dst = (pl_transform2x2) {
.mat.m = {{ rx, 0 }, { 0, ry }},
.c = {
pass->dst_rect.x0 - rx * image->crop.x0,
pass->dst_rect.y0 - ry * image->crop.y0,
},
};
if (pass->rotation % PL_ROTATION_180 == PL_ROTATION_90) {
PL_SWAP(src_to_dst.c[0], src_to_dst.c[1]);
src_to_dst.mat = (pl_matrix2x2) {{{ 0, ry }, { rx, 0 }}};
}
}
const struct pl_frame *target = &pass->target;
pl_rect2df dst_crop = target->crop;
pl_rect2df_rotate(&dst_crop, -pass->rotation);
pl_rect2df_normalize(&dst_crop);
for (int n = 0; n < num; n++) {
struct pl_overlay ol = overlays[n];
if (!ol.num_parts)
continue;
if (!ol.coords) {
ol.coords = overlays == target->overlays
? PL_OVERLAY_COORDS_DST_FRAME
: PL_OVERLAY_COORDS_SRC_FRAME;
}
pl_transform2x2 tf = pl_transform2x2_identity;
switch (ol.coords) {
case PL_OVERLAY_COORDS_SRC_CROP:
if (!image)
continue;
tf.c[0] = image->crop.x0;
tf.c[1] = image->crop.y0;
// fall through
case PL_OVERLAY_COORDS_SRC_FRAME:
if (!image)
continue;
pl_transform2x2_rmul(&src_to_dst, &tf);
break;
case PL_OVERLAY_COORDS_DST_CROP:
tf.c[0] = dst_crop.x0;
tf.c[1] = dst_crop.y0;
break;
case PL_OVERLAY_COORDS_DST_FRAME:
break;
case PL_OVERLAY_COORDS_AUTO:
case PL_OVERLAY_COORDS_COUNT:
pl_unreachable();
}
if (output_shift)
pl_transform2x2_rmul(output_shift, &tf);
// Construct vertex/index buffers
rr->osd_vertices.num = 0;
rr->osd_indices.num = 0;
for (int i = 0; i < ol.num_parts; i++) {
const struct pl_overlay_part *part = &ol.parts[i];
#define EMIT_VERT(x, y) \
do { \
float pos[2] = { part->dst.x, part->dst.y }; \
pl_transform2x2_apply(&tf, pos); \
PL_ARRAY_APPEND(rr, rr->osd_vertices, (struct osd_vertex) { \
.pos = { \
2.0 * (pos[0] / fbo->params.w) - 1.0, \
2.0 * (pos[1] / fbo->params.h) - 1.0, \
}, \
.coord = { \
part->src.x / ol.tex->params.w, \
part->src.y / ol.tex->params.h, \
}, \
.color = { \
part->color[0], part->color[1], \
part->color[2], part->color[3], \
}, \
}); \
} while (0)
int idx_base = rr->osd_vertices.num;
EMIT_VERT(x0, y0); // idx 0: top left
EMIT_VERT(x1, y0); // idx 1: top right
EMIT_VERT(x0, y1); // idx 2: bottom left
EMIT_VERT(x1, y1); // idx 3: bottom right
PL_ARRAY_APPEND(rr, rr->osd_indices, idx_base + 0);
PL_ARRAY_APPEND(rr, rr->osd_indices, idx_base + 1);
PL_ARRAY_APPEND(rr, rr->osd_indices, idx_base + 2);
PL_ARRAY_APPEND(rr, rr->osd_indices, idx_base + 2);
PL_ARRAY_APPEND(rr, rr->osd_indices, idx_base + 1);
PL_ARRAY_APPEND(rr, rr->osd_indices, idx_base + 3);
}
// Draw parts
pl_shader sh = pl_dispatch_begin(rr->dp);
ident_t tex = sh_desc(sh, (struct pl_shader_desc) {
.desc = {
.name = "osd_tex",
.type = PL_DESC_SAMPLED_TEX,
},
.binding = {
.object = ol.tex,
.sample_mode = (ol.tex->params.format->caps & PL_FMT_CAP_LINEAR)
? PL_TEX_SAMPLE_LINEAR
: PL_TEX_SAMPLE_NEAREST,
},
});
sh_describe(sh, "overlay");
GLSL("// overlay \n");
switch (ol.mode) {
case PL_OVERLAY_NORMAL:
GLSL("vec4 color = textureLod("$", coord, 0.0); \n", tex);
break;
case PL_OVERLAY_MONOCHROME:
GLSL("vec4 color = osd_color; \n");
break;
case PL_OVERLAY_MODE_COUNT:
pl_unreachable();
};
static const struct pl_color_map_params osd_params = {
PL_COLOR_MAP_DEFAULTS
.tone_mapping_function = &pl_tone_map_linear,
.gamut_mapping = &pl_gamut_map_saturation,
};
sh->output = PL_SHADER_SIG_COLOR;
pl_shader_decode_color(sh, &ol.repr, NULL);
if (target->icc)
color.transfer = PL_COLOR_TRC_LINEAR;
pl_shader_color_map_ex(sh, &osd_params, pl_color_map_args(ol.color, color));
if (target->icc)
pl_icc_encode(sh, target->icc, &rr->icc_state[ICC_TARGET]);
bool premul = repr.alpha == PL_ALPHA_PREMULTIPLIED;
pl_shader_encode_color(sh, &repr);
if (ol.mode == PL_OVERLAY_MONOCHROME) {
GLSL("color.%s *= textureLod("$", coord, 0.0).r; \n",
premul ? "rgba" : "a", tex);
}
swizzle_color(sh, comps, comp_map, true);
struct pl_blend_params blend_params = {
.src_rgb = premul ? PL_BLEND_ONE : PL_BLEND_SRC_ALPHA,
.src_alpha = PL_BLEND_ONE,
.dst_rgb = PL_BLEND_ONE_MINUS_SRC_ALPHA,
.dst_alpha = PL_BLEND_ONE_MINUS_SRC_ALPHA,
};
bool ok = pl_dispatch_vertex(rr->dp, pl_dispatch_vertex_params(
.shader = &sh,
.target = fbo,
.blend_params = (rr->errors & PL_RENDER_ERR_BLENDING)
? NULL : &blend_params,
.vertex_stride = sizeof(struct osd_vertex),
.num_vertex_attribs = ol.mode == PL_OVERLAY_NORMAL ? 2 : 3,
.vertex_attribs = rr->osd_attribs,
.vertex_position_idx = 0,
.vertex_coords = PL_COORDS_NORMALIZED,
.vertex_type = PL_PRIM_TRIANGLE_LIST,
.vertex_count = rr->osd_indices.num,
.vertex_data = rr->osd_vertices.elem,
.index_data = rr->osd_indices.elem,
));
if (!ok) {
PL_ERR(rr, "Failed rendering overlays!");
rr->errors |= PL_RENDER_ERR_OVERLAY;
return;
}
}
}
static pl_tex get_hook_tex(void *priv, int width, int height)
{
struct pass_state *pass = priv;
return get_fbo(pass, width, height, NULL, 4, PL_DEBUG_TAG);
}
// Returns if any hook was applied (even if there were errors)
static bool pass_hook(struct pass_state *pass, struct img *img,
enum pl_hook_stage stage)
{
const struct pl_render_params *params = pass->params;
pl_renderer rr = pass->rr;
if (!pass->fbofmt[4] || !stage)
return false;
bool ret = false;
for (int n = 0; n < params->num_hooks; n++) {
const struct pl_hook *hook = params->hooks[n];
if (!(hook->stages & stage))
continue;
// Hopefully the list of disabled hooks is small, search linearly.
for (int i = 0; i < rr->disabled_hooks.num; i++) {
if (rr->disabled_hooks.elem[i] != hook->signature)
continue;
PL_TRACE(rr, "Skipping hook %d (0x%"PRIx64") stage 0x%x",
n, hook->signature, stage);
goto hook_skip;
}
PL_TRACE(rr, "Dispatching hook %d (0x%"PRIx64") stage 0x%x",
n, hook->signature, stage);
struct pl_hook_params hparams = {
.gpu = rr->gpu,
.dispatch = rr->dp,
.get_tex = get_hook_tex,
.priv = pass,
.stage = stage,
.rect = img->rect,
.repr = img->repr,
.color = img->color,
.orig_repr = &pass->image.repr,
.orig_color = &pass->image.color,
.components = img->comps,
.src_rect = pass->ref_rect,
.dst_rect = pass->dst_rect,
};
// TODO: Add some sort of `test` API function to the hooks that allows
// us to skip having to touch the `img` state at all for no-ops
switch (hook->input) {
case PL_HOOK_SIG_NONE:
break;
case PL_HOOK_SIG_TEX: {
hparams.tex = img_tex(pass, img);
if (!hparams.tex) {
PL_ERR(rr, "Failed dispatching shader prior to hook!");
goto hook_error;
}
break;
}
case PL_HOOK_SIG_COLOR:
hparams.sh = img_sh(pass, img);
break;
case PL_HOOK_SIG_COUNT:
pl_unreachable();
}
struct pl_hook_res res = hook->hook(hook->priv, &hparams);
if (res.failed) {
PL_ERR(rr, "Failed executing hook, disabling");
goto hook_error;
}
bool resizable = pl_hook_stage_resizable(stage);
switch (res.output) {
case PL_HOOK_SIG_NONE:
break;
case PL_HOOK_SIG_TEX:
if (!resizable) {
if (res.tex->params.w != img->w ||
res.tex->params.h != img->h ||
!pl_rect2d_eq(res.rect, img->rect))
{
PL_ERR(rr, "User hook tried resizing non-resizable stage!");
goto hook_error;
}
}
*img = (struct img) {
.tex = res.tex,
.repr = res.repr,
.color = res.color,
.comps = res.components,
.rect = res.rect,
.w = res.tex->params.w,
.h = res.tex->params.h,
.unique = img->unique,
};
break;
case PL_HOOK_SIG_COLOR:
if (!resizable) {
if (res.sh->output_w != img->w ||
res.sh->output_h != img->h ||
!pl_rect2d_eq(res.rect, img->rect))
{
PL_ERR(rr, "User hook tried resizing non-resizable stage!");
goto hook_error;
}
}
*img = (struct img) {
.sh = res.sh,
.repr = res.repr,
.color = res.color,
.comps = res.components,
.rect = res.rect,
.w = res.sh->output_w,
.h = res.sh->output_h,
.unique = img->unique,
.err_enum = PL_RENDER_ERR_HOOKS,
.err_msg = "Failed applying user hook",
.err_tex = hparams.tex, // if any
};
break;
case PL_HOOK_SIG_COUNT:
pl_unreachable();
}
// a hook was performed successfully
ret = true;
hook_skip:
continue;
hook_error:
PL_ARRAY_APPEND(rr, rr->disabled_hooks, hook->signature);
rr->errors |= PL_RENDER_ERR_HOOKS;
}
// Make sure the state remains as valid as possible, even if the resulting
// shaders might end up nonsensical, to prevent segfaults
if (!img->tex && !img->sh)
img->sh = pl_dispatch_begin(rr->dp);
return ret;
}
static void hdr_update_peak(struct pass_state *pass)
{
const struct pl_render_params *params = pass->params;
pl_renderer rr = pass->rr;
if (!params->peak_detect_params || !pl_color_space_is_hdr(&pass->img.color))
goto cleanup;
if (rr->errors & PL_RENDER_ERR_PEAK_DETECT)
goto cleanup;
if (pass->fbofmt[4] && !(pass->fbofmt[4]->caps & PL_FMT_CAP_STORABLE))
goto cleanup;
if (!rr->gpu->limits.max_ssbo_size)
goto cleanup;
float max_peak = pl_color_transfer_nominal_peak(pass->img.color.transfer) *
PL_COLOR_SDR_WHITE;
if (pass->img.color.transfer == PL_COLOR_TRC_HLG)
max_peak = pass->img.color.hdr.max_luma;
if (max_peak <= pass->target.color.hdr.max_luma + 1e-6)
goto cleanup; // no adaptation needed
if (pass->img.color.hdr.avg_pq_y)
goto cleanup; // DV metadata already present
enum pl_hdr_metadata_type metadata = PL_HDR_METADATA_ANY;
if (params->color_map_params)
metadata = params->color_map_params->metadata;
if (metadata && metadata != PL_HDR_METADATA_CIE_Y)
goto cleanup; // metadata will be unused
const struct pl_color_map_params *cpars = params->color_map_params;
bool uses_ootf = cpars && cpars->tone_mapping_function == &pl_tone_map_st2094_40;
if (uses_ootf && pass->img.color.hdr.ootf.num_anchors)
goto cleanup; // HDR10+ OOTF is being used
if (params->lut && params->lut_type == PL_LUT_CONVERSION)
goto cleanup; // LUT handles tone mapping
if (!pass->fbofmt[4] && !params->peak_detect_params->allow_delayed) {
PL_WARN(rr, "Disabling peak detection because "
"`pl_peak_detect_params.allow_delayed` is false, but lack of "
"FBOs forces the result to be delayed.");
rr->errors |= PL_RENDER_ERR_PEAK_DETECT;
goto cleanup;
}
bool ok = pl_shader_detect_peak(img_sh(pass, &pass->img), pass->img.color,
&rr->tone_map_state, params->peak_detect_params);
if (!ok) {
PL_WARN(rr, "Failed creating HDR peak detection shader.. disabling");
rr->errors |= PL_RENDER_ERR_PEAK_DETECT;
goto cleanup;
}
pass->need_peak_fbo = !params->peak_detect_params->allow_delayed;
return;
cleanup:
// No peak detection required or supported, so clean up the state to avoid
// confusing it with later frames where peak detection is enabled again
pl_reset_detected_peak(rr->tone_map_state);
}
bool pl_renderer_get_hdr_metadata(pl_renderer rr,
struct pl_hdr_metadata *metadata)
{
return pl_get_detected_hdr_metadata(rr->tone_map_state, metadata);
}
struct plane_state {
enum plane_type type;
struct pl_plane plane;
struct img img; // for per-plane shaders
float plane_w, plane_h; // logical plane dimensions
};
static const char *plane_type_names[] = {
[PLANE_INVALID] = "invalid",
[PLANE_ALPHA] = "alpha",
[PLANE_CHROMA] = "chroma",
[PLANE_LUMA] = "luma",
[PLANE_RGB] = "rgb",
[PLANE_XYZ] = "xyz",
};
static void log_plane_info(pl_renderer rr, const struct plane_state *st)
{
const struct pl_plane *plane = &st->plane;
PL_TRACE(rr, " Type: %s", plane_type_names[st->type]);
switch (plane->components) {
case 0:
PL_TRACE(rr, " Components: (none)");
break;
case 1:
PL_TRACE(rr, " Components: {%d}",
plane->component_mapping[0]);
break;
case 2:
PL_TRACE(rr, " Components: {%d %d}",
plane->component_mapping[0],
plane->component_mapping[1]);
break;
case 3:
PL_TRACE(rr, " Components: {%d %d %d}",
plane->component_mapping[0],
plane->component_mapping[1],
plane->component_mapping[2]);
break;
case 4:
PL_TRACE(rr, " Components: {%d %d %d %d}",
plane->component_mapping[0],
plane->component_mapping[1],
plane->component_mapping[2],
plane->component_mapping[3]);
break;
}
PL_TRACE(rr, " Rect: {%f %f} -> {%f %f}",
st->img.rect.x0, st->img.rect.y0, st->img.rect.x1, st->img.rect.y1);
PL_TRACE(rr, " Bits: %d (used) / %d (sampled), shift %d",
st->img.repr.bits.color_depth,
st->img.repr.bits.sample_depth,
st->img.repr.bits.bit_shift);
}
// Returns true if debanding was applied
static bool plane_deband(struct pass_state *pass, struct img *img, float neutral[3])
{
const struct pl_render_params *params = pass->params;
const struct pl_frame *image = &pass->image;
pl_renderer rr = pass->rr;
if ((rr->errors & PL_RENDER_ERR_DEBANDING) ||
!params->deband_params || !pass->fbofmt[4])
{
return false;
}
struct pl_color_repr repr = img->repr;
struct pl_sample_src src = {
.tex = img_tex(pass, img),
.components = img->comps,
.scale = pl_color_repr_normalize(&repr),
};
if (!(src.tex->params.format->caps & PL_FMT_CAP_LINEAR)) {
PL_WARN(rr, "Debanding requires uploaded textures to be linearly "
"sampleable (params.sample_mode = PL_TEX_SAMPLE_LINEAR)! "
"Disabling debanding..");
rr->errors |= PL_RENDER_ERR_DEBANDING;
return false;
}
// Divide the deband grain scale by the effective current colorspace nominal
// peak, to make sure the output intensity of the grain is as independent
// of the source as possible, even though it happens this early in the
// process (well before any linearization / output adaptation)
struct pl_deband_params dparams = *params->deband_params;
dparams.grain /= image->color.hdr.max_luma / PL_COLOR_SDR_WHITE;
memcpy(dparams.grain_neutral, neutral, sizeof(dparams.grain_neutral));
img->tex = NULL;
img->sh = pl_dispatch_begin_ex(rr->dp, true);
pl_shader_deband(img->sh, &src, &dparams);
img->err_msg = "Failed applying debanding... disabling!";
img->err_enum = PL_RENDER_ERR_DEBANDING;
img->err_tex = src.tex;
img->repr = repr;
return true;
}
// Returns true if grain was applied
static bool plane_film_grain(struct pass_state *pass, int plane_idx,
struct plane_state *st,
const struct plane_state *ref)
{
const struct pl_frame *image = &pass->image;
pl_renderer rr = pass->rr;
if (rr->errors & PL_RENDER_ERR_FILM_GRAIN)
return false;
struct img *img = &st->img;
struct pl_plane *plane = &st->plane;
struct pl_color_repr repr = image->repr;
bool is_orig_repr = pl_color_repr_equal(&st->img.repr, &image->repr);
if (!is_orig_repr) {
// Propagate the original color depth to the film grain algorithm, but
// update the sample depth and effective bit shift based on the state
// of the current texture, which is guaranteed to already be
// normalized.
pl_assert(st->img.repr.bits.bit_shift == 0);
repr.bits.sample_depth = st->img.repr.bits.sample_depth;
repr.bits.bit_shift = repr.bits.sample_depth - repr.bits.color_depth;
}
struct pl_film_grain_params grain_params = {
.data = image->film_grain,
.luma_tex = ref->plane.texture,
.repr = &repr,
.components = plane->components,
};
switch (image->film_grain.type) {
case PL_FILM_GRAIN_NONE: return false;
case PL_FILM_GRAIN_H274: break;
case PL_FILM_GRAIN_AV1:
grain_params.luma_tex = ref->plane.texture;
for (int c = 0; c < ref->plane.components; c++) {
if (ref->plane.component_mapping[c] == PL_CHANNEL_Y)
grain_params.luma_comp = c;
}
break;
default: pl_unreachable();
}
for (int c = 0; c < plane->components; c++)
grain_params.component_mapping[c] = plane->component_mapping[c];
if (!pl_needs_film_grain(&grain_params))
return false;
if (!pass->fbofmt[plane->components]) {
PL_ERR(rr, "Film grain required but no renderable format available.. "
"disabling!");
rr->errors |= PL_RENDER_ERR_FILM_GRAIN;
return false;
}
grain_params.tex = img_tex(pass, img);
if (!grain_params.tex)
return false;
img->sh = pl_dispatch_begin_ex(rr->dp, true);
if (!pl_shader_film_grain(img->sh, &rr->grain_state[plane_idx], &grain_params)) {
pl_dispatch_abort(rr->dp, &img->sh);
rr->errors |= PL_RENDER_ERR_FILM_GRAIN;
return false;
}
img->tex = NULL;
img->err_msg = "Failed applying film grain.. disabling!";
img->err_enum = PL_RENDER_ERR_FILM_GRAIN;
img->err_tex = grain_params.tex;
if (is_orig_repr)
img->repr = repr;
return true;
}
static const enum pl_hook_stage plane_hook_stages[] = {
[PLANE_ALPHA] = PL_HOOK_ALPHA_INPUT,
[PLANE_CHROMA] = PL_HOOK_CHROMA_INPUT,
[PLANE_LUMA] = PL_HOOK_LUMA_INPUT,
[PLANE_RGB] = PL_HOOK_RGB_INPUT,
[PLANE_XYZ] = PL_HOOK_XYZ_INPUT,
};
static const enum pl_hook_stage plane_scaled_hook_stages[] = {
[PLANE_ALPHA] = PL_HOOK_ALPHA_SCALED,
[PLANE_CHROMA] = PL_HOOK_CHROMA_SCALED,
[PLANE_LUMA] = 0, // never hooked
[PLANE_RGB] = 0,
[PLANE_XYZ] = 0,
};
static enum pl_lut_type guess_frame_lut_type(const struct pl_frame *frame,
bool reversed)
{
if (!frame->lut)
return PL_LUT_UNKNOWN;
if (frame->lut_type)
return frame->lut_type;
enum pl_color_system sys_in = frame->lut->repr_in.sys;
enum pl_color_system sys_out = frame->lut->repr_out.sys;
if (reversed)
PL_SWAP(sys_in, sys_out);
if (sys_in == PL_COLOR_SYSTEM_RGB && sys_out == sys_in)
return PL_LUT_NORMALIZED;
if (sys_in == frame->repr.sys && sys_out == PL_COLOR_SYSTEM_RGB)
return PL_LUT_CONVERSION;
// Unknown, just fall back to the default
return PL_LUT_NATIVE;
}
static pl_fmt merge_fmt(struct pass_state *pass, const struct img *a,
const struct img *b)
{
pl_renderer rr = pass->rr;
pl_fmt fmta = a->tex ? a->tex->params.format : PL_DEF(a->fmt, pass->fbofmt[a->comps]);
pl_fmt fmtb = b->tex ? b->tex->params.format : PL_DEF(b->fmt, pass->fbofmt[b->comps]);
pl_assert(fmta && fmtb);
if (fmta->type != fmtb->type)
return NULL;
int num_comps = PL_MIN(4, a->comps + b->comps);
int min_depth = PL_MAX(a->repr.bits.sample_depth, b->repr.bits.sample_depth);
// Only return formats that support all relevant caps of both formats
const enum pl_fmt_caps mask = PL_FMT_CAP_SAMPLEABLE | PL_FMT_CAP_LINEAR;
enum pl_fmt_caps req_caps = (fmta->caps & mask) | (fmtb->caps & mask);
return pl_find_fmt(rr->gpu, fmta->type, num_comps, min_depth, 0, req_caps);
}
// Applies a series of rough heuristics to figure out whether we expect any
// performance gains from plane merging. This is basically a series of checks
// for operations that we *know* benefit from merged planes
static bool want_merge(struct pass_state *pass,
const struct plane_state *st,
const struct plane_state *ref)
{
const struct pl_render_params *params = pass->params;
const pl_renderer rr = pass->rr;
if (!pass->fbofmt[4])
return false;
// Debanding
if (!(rr->errors & PL_RENDER_ERR_DEBANDING) && params->deband_params)
return true;
// Other plane hooks, which are generally nontrivial
enum pl_hook_stage stage = plane_hook_stages[st->type];
for (int i = 0; i < params->num_hooks; i++) {
if (params->hooks[i]->stages & stage)
return true;
}
// Non-trivial scaling
struct pl_sample_src src = {
.new_w = ref->img.w,
.new_h = ref->img.h,
.rect = {
.x1 = st->img.w,
.y1 = st->img.h,
},
};
struct sampler_info info = sample_src_info(pass, &src, SAMPLER_PLANE);
if (info.type == SAMPLER_COMPLEX)
return true;
// Film grain synthesis, can be merged for compatible channels, saving on
// redundant sampling of the grain/offset textures
struct pl_film_grain_params grain_params = {
.data = pass->image.film_grain,
.repr = (struct pl_color_repr *) &st->img.repr,
.components = st->plane.components,
};
for (int c = 0; c < st->plane.components; c++)
grain_params.component_mapping[c] = st->plane.component_mapping[c];
if (!(rr->errors & PL_RENDER_ERR_FILM_GRAIN) &&
pl_needs_film_grain(&grain_params))
{
return true;
}
return false;
}
// This scales and merges all of the source images, and initializes pass->img.
static bool pass_read_image(struct pass_state *pass)
{
const struct pl_render_params *params = pass->params;
struct pl_frame *image = &pass->image;
pl_renderer rr = pass->rr;
struct plane_state planes[4];
struct plane_state *ref = &planes[pass->src_ref];
pl_assert(pass->src_ref >= 0 && pass->src_ref < image->num_planes);
for (int i = 0; i < image->num_planes; i++) {
planes[i] = (struct plane_state) {
.type = detect_plane_type(&image->planes[i], &image->repr),
.plane = image->planes[i],
.img = {
.w = image->planes[i].texture->params.w,
.h = image->planes[i].texture->params.h,
.tex = image->planes[i].texture,
.repr = image->repr,
.color = image->color,
.comps = image->planes[i].components,
},
};
// Deinterlace plane if needed
if (image->field != PL_FIELD_NONE && params->deinterlace_params &&
pass->fbofmt[4] && !(rr->errors & PL_RENDER_ERR_DEINTERLACING))
{
struct img *img = &planes[i].img;
struct pl_deinterlace_source src = {
.cur.top = img->tex,
.prev.top = image->prev ? image->prev->planes[i].texture : NULL,
.next.top = image->next ? image->next->planes[i].texture : NULL,
.field = image->field,
.first_field = image->first_field,
.component_mask = (1 << img->comps) - 1,
};
img->tex = NULL;
img->sh = pl_dispatch_begin_ex(pass->rr->dp, true);
pl_shader_deinterlace(img->sh, &src, params->deinterlace_params);
img->err_msg = "Failed deinterlacing plane.. disabling!";
img->err_enum = PL_RENDER_ERR_DEINTERLACING;
img->err_tex = planes[i].plane.texture;
}
}
// Original ref texture, even after preprocessing
pl_tex ref_tex = ref->plane.texture;
// Merge all compatible planes into 'combined' shaders
for (int i = 0; i < image->num_planes; i++) {
struct plane_state *sti = &planes[i];
if (!sti->type)
continue;
if (!want_merge(pass, sti, ref))
continue;
bool did_merge = false;
for (int j = i+1; j < image->num_planes; j++) {
struct plane_state *stj = &planes[j];
bool merge = sti->type == stj->type &&
sti->img.w == stj->img.w &&
sti->img.h == stj->img.h &&
sti->plane.shift_x == stj->plane.shift_x &&
sti->plane.shift_y == stj->plane.shift_y;
if (!merge)
continue;
pl_fmt fmt = merge_fmt(pass, &sti->img, &stj->img);
if (!fmt)
continue;
PL_TRACE(rr, "Merging plane %d into plane %d", j, i);
pl_shader sh = sti->img.sh;
if (!sh) {
sh = sti->img.sh = pl_dispatch_begin_ex(pass->rr->dp, true);
pl_shader_sample_direct(sh, pl_sample_src( .tex = sti->img.tex ));
sti->img.tex = NULL;
}
pl_shader psh = NULL;
if (!stj->img.sh) {
psh = pl_dispatch_begin_ex(pass->rr->dp, true);
pl_shader_sample_direct(psh, pl_sample_src( .tex = stj->img.tex ));
}
ident_t sub = sh_subpass(sh, psh ? psh : stj->img.sh);
pl_dispatch_abort(rr->dp, &psh);
if (!sub)
break; // skip merging
sh_describe(sh, "merging planes");
GLSL("{ \n"
"vec4 tmp = "$"(); \n", sub);
for (int jc = 0; jc < stj->img.comps; jc++) {
int map = stj->plane.component_mapping[jc];
if (map == PL_CHANNEL_NONE)
continue;
int ic = sti->img.comps++;
pl_assert(ic < 4);
GLSL("color[%d] = tmp[%d]; \n", ic, jc);
sti->plane.components = sti->img.comps;
sti->plane.component_mapping[ic] = map;
}
GLSL("} \n");
sti->img.fmt = fmt;
pl_dispatch_abort(rr->dp, &stj->img.sh);
*stj = (struct plane_state) {0};
did_merge = true;
}
if (!did_merge)
continue;
if (!img_tex(pass, &sti->img)) {
PL_ERR(rr, "Failed dispatching plane merging shader, disabling FBOs!");
memset(pass->fbofmt, 0, sizeof(pass->fbofmt));
rr->errors |= PL_RENDER_ERR_FBO;
return false;
}
}
int bits = image->repr.bits.sample_depth;
float out_scale = bits ? (1llu << bits) / ((1llu << bits) - 1.0f) : 1.0f;
float neutral_luma = 0.0, neutral_chroma = 0.5f * out_scale;
if (pl_color_levels_guess(&image->repr) == PL_COLOR_LEVELS_LIMITED)
neutral_luma = 16 / 256.0f * out_scale;
if (!pl_color_system_is_ycbcr_like(image->repr.sys))
neutral_chroma = neutral_luma;
// Compute the sampling rc of each plane
for (int i = 0; i < image->num_planes; i++) {
struct plane_state *st = &planes[i];
if (!st->type)
continue;
float rx = (float) st->plane.texture->params.w / ref_tex->params.w,
ry = (float) st->plane.texture->params.h / ref_tex->params.h;
// Only accept integer scaling ratios. This accounts for the fact that
// fractionally subsampled planes get rounded up to the nearest integer
// size, which we want to discard.
float rrx = rx >= 1 ? roundf(rx) : 1.0 / roundf(1.0 / rx),
rry = ry >= 1 ? roundf(ry) : 1.0 / roundf(1.0 / ry);
float sx = st->plane.shift_x,
sy = st->plane.shift_y;
st->img.rect = (pl_rect2df) {
.x0 = (image->crop.x0 - sx) * rrx,
.y0 = (image->crop.y0 - sy) * rry,
.x1 = (image->crop.x1 - sx) * rrx,
.y1 = (image->crop.y1 - sy) * rry,
};
st->plane_w = ref_tex->params.w * rrx;
st->plane_h = ref_tex->params.h * rry;
PL_TRACE(rr, "Plane %d:", i);
log_plane_info(rr, st);
float neutral[3] = {0.0};
for (int c = 0, idx = 0; c < st->plane.components; c++) {
switch (st->plane.component_mapping[c]) {
case PL_CHANNEL_Y: neutral[idx++] = neutral_luma; break;
case PL_CHANNEL_U: // fall through
case PL_CHANNEL_V: neutral[idx++] = neutral_chroma; break;
}
}
// The order of operations (deband -> film grain -> user hooks) is
// chosen to maximize quality. Note that film grain requires unmodified
// plane sizes, so it has to be before user hooks. As for debanding,
// it's reduced in quality after e.g. plane scalers as well. It's also
// made less effective by performing film grain synthesis first.
if (plane_deband(pass, &st->img, neutral)) {
PL_TRACE(rr, "After debanding:");
log_plane_info(rr, st);
}
if (plane_film_grain(pass, i, st, ref)) {
PL_TRACE(rr, "After film grain:");
log_plane_info(rr, st);
}
if (pass_hook(pass, &st->img, plane_hook_stages[st->type])) {
PL_TRACE(rr, "After user hooks:");
log_plane_info(rr, st);
}
}
pl_shader sh = pl_dispatch_begin_ex(rr->dp, true);
sh_require(sh, PL_SHADER_SIG_NONE, 0, 0);
// Initialize the color to black
GLSL("vec4 color = vec4("$", vec2("$"), 1.0); \n"
"// pass_read_image \n"
"{ \n"
"vec4 tmp; \n",
SH_FLOAT(neutral_luma), SH_FLOAT(neutral_chroma));
// For quality reasons, explicitly drop subpixel offsets from the ref rect
// and re-add them as part of `pass->img.rect`, always rounding towards 0.
// Additionally, drop anamorphic subpixel mismatches.
pl_rect2d ref_rounded;
ref_rounded.x0 = truncf(ref->img.rect.x0);
ref_rounded.y0 = truncf(ref->img.rect.y0);
ref_rounded.x1 = ref_rounded.x0 + roundf(pl_rect_w(ref->img.rect));
ref_rounded.y1 = ref_rounded.y0 + roundf(pl_rect_h(ref->img.rect));
PL_TRACE(rr, "Rounded reference rect: {%d %d %d %d}",
ref_rounded.x0, ref_rounded.y0,
ref_rounded.x1, ref_rounded.y1);
float off_x = ref->img.rect.x0 - ref_rounded.x0,
off_y = ref->img.rect.y0 - ref_rounded.y0,
stretch_x = pl_rect_w(ref_rounded) / pl_rect_w(ref->img.rect),
stretch_y = pl_rect_h(ref_rounded) / pl_rect_h(ref->img.rect);
for (int i = 0; i < image->num_planes; i++) {
struct plane_state *st = &planes[i];
const struct pl_plane *plane = &st->plane;
if (!st->type)
continue;
float scale_x = pl_rect_w(st->img.rect) / pl_rect_w(ref->img.rect),
scale_y = pl_rect_h(st->img.rect) / pl_rect_h(ref->img.rect),
base_x = st->img.rect.x0 - scale_x * off_x,
base_y = st->img.rect.y0 - scale_y * off_y;
struct pl_sample_src src = {
.components = plane->components,
.address_mode = plane->address_mode,
.scale = pl_color_repr_normalize(&st->img.repr),
.new_w = pl_rect_w(ref_rounded),
.new_h = pl_rect_h(ref_rounded),
.rect = {
base_x,
base_y,
base_x + stretch_x * pl_rect_w(st->img.rect),
base_y + stretch_y * pl_rect_h(st->img.rect),
},
};
if (plane->flipped) {
src.rect.y0 = st->plane_h - src.rect.y0;
src.rect.y1 = st->plane_h - src.rect.y1;
}
PL_TRACE(rr, "Aligning plane %d: {%f %f %f %f} -> {%f %f %f %f}%s",
i, st->img.rect.x0, st->img.rect.y0,
st->img.rect.x1, st->img.rect.y1,
src.rect.x0, src.rect.y0,
src.rect.x1, src.rect.y1,
plane->flipped ? " (flipped) " : "");
st->img.unique = true;
pl_rect2d unscaled = { .x1 = src.new_w, .y1 = src.new_h };
if (st->img.sh && st->img.w == src.new_w && st->img.h == src.new_h &&
pl_rect2d_eq(src.rect, unscaled))
{
// Image rects are already equal, no indirect scaling needed
} else {
src.tex = img_tex(pass, &st->img);
st->img.tex = NULL;
st->img.sh = pl_dispatch_begin_ex(rr->dp, true);
dispatch_sampler(pass, st->img.sh, &rr->samplers_src[i],
SAMPLER_PLANE, NULL, &src);
st->img.err_enum |= PL_RENDER_ERR_SAMPLING;
st->img.rect.x0 = st->img.rect.y0 = 0.0f;
st->img.w = st->img.rect.x1 = src.new_w;
st->img.h = st->img.rect.y1 = src.new_h;
}
pass_hook(pass, &st->img, plane_scaled_hook_stages[st->type]);
ident_t sub = sh_subpass(sh, img_sh(pass, &st->img));
if (!sub) {
if (!img_tex(pass, &st->img)) {
pl_dispatch_abort(rr->dp, &sh);
return false;
}
sub = sh_subpass(sh, img_sh(pass, &st->img));
pl_assert(sub);
}
GLSL("tmp = "$"(); \n", sub);
for (int c = 0; c < src.components; c++) {
if (plane->component_mapping[c] < 0)
continue;
GLSL("color[%d] = tmp[%d];\n", plane->component_mapping[c], c);
}
// we don't need it anymore
pl_dispatch_abort(rr->dp, &st->img.sh);
}
GLSL("}\n");
pass->img = (struct img) {
.sh = sh,
.w = pl_rect_w(ref_rounded),
.h = pl_rect_h(ref_rounded),
.repr = ref->img.repr,
.color = image->color,
.comps = ref->img.repr.alpha ? 4 : 3,
.rect = {
off_x,
off_y,
off_x + pl_rect_w(ref->img.rect),
off_y + pl_rect_h(ref->img.rect),
},
};
// Update the reference rect to our adjusted image coordinates
pass->ref_rect = pass->img.rect;
pass_hook(pass, &pass->img, PL_HOOK_NATIVE);
// Apply LUT logic and colorspace conversion
enum pl_lut_type lut_type = guess_frame_lut_type(image, false);
sh = img_sh(pass, &pass->img);
bool needs_conversion = true;
if (lut_type == PL_LUT_NATIVE || lut_type == PL_LUT_CONVERSION) {
// Fix bit depth normalization before applying LUT
float scale = pl_color_repr_normalize(&pass->img.repr);
GLSL("color *= vec4("$"); \n", SH_FLOAT(scale));
pl_shader_set_alpha(sh, &pass->img.repr, PL_ALPHA_INDEPENDENT);
pl_shader_custom_lut(sh, image->lut, &rr->lut_state[LUT_IMAGE]);
if (lut_type == PL_LUT_CONVERSION) {
pass->img.repr.sys = PL_COLOR_SYSTEM_RGB;
pass->img.repr.levels = PL_COLOR_LEVELS_FULL;
needs_conversion = false;
}
}
if (needs_conversion) {
if (pass->img.repr.sys == PL_COLOR_SYSTEM_XYZ)
pass->img.color.transfer = PL_COLOR_TRC_LINEAR;
pl_shader_decode_color(sh, &pass->img.repr, params->color_adjustment);
}
if (lut_type == PL_LUT_NORMALIZED)
pl_shader_custom_lut(sh, image->lut, &rr->lut_state[LUT_IMAGE]);
// A main PL_LUT_CONVERSION LUT overrides ICC profiles
bool main_lut_override = params->lut && params->lut_type == PL_LUT_CONVERSION;
if (image->icc && !main_lut_override) {
pl_shader_set_alpha(sh, &pass->img.repr, PL_ALPHA_INDEPENDENT);
pl_icc_decode(sh, image->icc, &rr->icc_state[ICC_IMAGE], &pass->img.color);
}
// Pre-multiply alpha channel before the rest of the pipeline, to avoid
// bleeding colors from transparent regions into non-transparent regions
pl_shader_set_alpha(sh, &pass->img.repr, PL_ALPHA_PREMULTIPLIED);
pass_hook(pass, &pass->img, PL_HOOK_RGB);
sh = NULL;
return true;
}
static bool pass_scale_main(struct pass_state *pass)
{
const struct pl_render_params *params = pass->params;
pl_renderer rr = pass->rr;
pl_fmt fbofmt = pass->fbofmt[pass->img.comps];
if (!fbofmt) {
PL_TRACE(rr, "Skipping main scaler (no FBOs)");
return true;
}
const pl_rect2df new_rect = {
.x1 = abs(pl_rect_w(pass->dst_rect)),
.y1 = abs(pl_rect_h(pass->dst_rect)),
};
struct img *img = &pass->img;
struct pl_sample_src src = {
.components = img->comps,
.new_w = pl_rect_w(new_rect),
.new_h = pl_rect_h(new_rect),
.rect = img->rect,
};
const struct pl_frame *image = &pass->image;
bool need_fbo = false;
// Force FBO indirection if this shader is non-resizable
int out_w, out_h;
if (img->sh && pl_shader_output_size(img->sh, &out_w, &out_h))
need_fbo |= out_w != src.new_w || out_h != src.new_h;
struct sampler_info info = sample_src_info(pass, &src, SAMPLER_MAIN);
bool use_sigmoid = info.dir == SAMPLER_UP && params->sigmoid_params;
bool use_linear = info.dir == SAMPLER_DOWN;
// Opportunistically update peak here if it would save performance
if (info.dir == SAMPLER_UP)
hdr_update_peak(pass);
// We need to enable the full rendering pipeline if there are any user
// shaders / hooks that might depend on it.
uint64_t scaling_hooks = PL_HOOK_PRE_KERNEL | PL_HOOK_POST_KERNEL;
uint64_t linear_hooks = PL_HOOK_LINEAR | PL_HOOK_SIGMOID;
for (int i = 0; i < params->num_hooks; i++) {
if (params->hooks[i]->stages & (scaling_hooks | linear_hooks)) {
need_fbo = true;
if (params->hooks[i]->stages & linear_hooks)
use_linear = true;
if (params->hooks[i]->stages & PL_HOOK_SIGMOID)
use_sigmoid = true;
}
}
if (info.dir == SAMPLER_NOOP && !need_fbo) {
pl_assert(src.new_w == img->w && src.new_h == img->h);
PL_TRACE(rr, "Skipping main scaler (would be no-op)");
goto done;
}
if (info.type == SAMPLER_DIRECT && !need_fbo) {
img->w = src.new_w;
img->h = src.new_h;
img->rect = new_rect;
PL_TRACE(rr, "Skipping main scaler (free sampling)");
goto done;
}
// Hard-disable both sigmoidization and linearization when required
if (params->disable_linear_scaling || fbofmt->component_depth[0] < 16)
use_sigmoid = use_linear = false;
// Avoid sigmoidization for HDR content because it clips to [0,1], and
// linearization because it causes very nasty ringing artefacts.
if (pl_color_space_is_hdr(&img->color))
use_sigmoid = use_linear = false;
if (!(use_linear || use_sigmoid) && img->color.transfer == PL_COLOR_TRC_LINEAR) {
img->color.transfer = image->color.transfer;
if (image->color.transfer == PL_COLOR_TRC_LINEAR)
img->color.transfer = PL_COLOR_TRC_GAMMA22; // arbitrary fallback
pl_shader_delinearize(img_sh(pass, img), &img->color);
}
if (use_linear || use_sigmoid) {
pl_shader_linearize(img_sh(pass, img), &img->color);
img->color.transfer = PL_COLOR_TRC_LINEAR;
pass_hook(pass, img, PL_HOOK_LINEAR);
}
if (use_sigmoid) {
pl_shader_sigmoidize(img_sh(pass, img), params->sigmoid_params);
pass_hook(pass, img, PL_HOOK_SIGMOID);
}
pass_hook(pass, img, PL_HOOK_PRE_KERNEL);
src.tex = img_tex(pass, img);
if (!src.tex)
return false;
pass->need_peak_fbo = false;
pl_shader sh = pl_dispatch_begin_ex(rr->dp, true);
dispatch_sampler(pass, sh, &rr->sampler_main, SAMPLER_MAIN, NULL, &src);
img->tex = NULL;
img->sh = sh;
img->w = src.new_w;
img->h = src.new_h;
img->rect = new_rect;
pass_hook(pass, img, PL_HOOK_POST_KERNEL);
if (use_sigmoid)
pl_shader_unsigmoidize(img_sh(pass, img), params->sigmoid_params);
done:
if (info.dir != SAMPLER_UP)
hdr_update_peak(pass);
pass_hook(pass, img, PL_HOOK_SCALED);
return true;
}
static pl_tex get_feature_map(struct pass_state *pass)
{
const struct pl_render_params *params = pass->params;
pl_renderer rr = pass->rr;
const struct pl_color_map_params *cparams = params->color_map_params;
cparams = PL_DEF(cparams, &pl_color_map_default_params);
if (!cparams->contrast_recovery || cparams->contrast_smoothness <= 1)
return NULL;
if (!pass->fbofmt[4])
return NULL;
if (!pl_color_space_is_hdr(&pass->img.color))
return NULL;
if (rr->errors & (PL_RENDER_ERR_SAMPLING | PL_RENDER_ERR_CONTRAST_RECOVERY))
return NULL;
if (pass->img.color.hdr.max_luma <= pass->target.color.hdr.max_luma + 1e-6)
return NULL; // no adaptation needed
if (params->lut && params->lut_type == PL_LUT_CONVERSION)
return NULL; // LUT handles tone mapping
struct img *img = &pass->img;
if (!img_tex(pass, img))
return NULL;
const float ratio = cparams->contrast_smoothness;
const int cr_w = ceilf(abs(pl_rect_w(pass->dst_rect)) / ratio);
const int cr_h = ceilf(abs(pl_rect_h(pass->dst_rect)) / ratio);
pl_tex inter_tex = get_fbo(pass, img->w, img->h, NULL, 1, PL_DEBUG_TAG);
pl_tex out_tex = get_fbo(pass, cr_w, cr_h, NULL, 1, PL_DEBUG_TAG);
if (!inter_tex || !out_tex)
goto error;
pl_shader sh = pl_dispatch_begin(rr->dp);
pl_shader_sample_direct(sh, pl_sample_src( .tex = img->tex ));
pl_shader_extract_features(sh, img->color);
bool ok = pl_dispatch_finish(rr->dp, pl_dispatch_params(
.shader = &sh,
.target = inter_tex,
));
if (!ok)
goto error;
const struct pl_sample_src src = {
.tex = inter_tex,
.rect = img->rect,
.address_mode = PL_TEX_ADDRESS_MIRROR,
.components = 1,
.new_w = cr_w,
.new_h = cr_h,
};
sh = pl_dispatch_begin(rr->dp);
dispatch_sampler(pass, sh, &rr->sampler_contrast, SAMPLER_CONTRAST, out_tex, &src);
ok = pl_dispatch_finish(rr->dp, pl_dispatch_params(
.shader = &sh,
.target = out_tex,
));
if (!ok)
goto error;
return out_tex;
error:
PL_ERR(rr, "Failed extracting luma for contrast recovery, disabling");
rr->errors |= PL_RENDER_ERR_CONTRAST_RECOVERY;
return NULL;
}
// Transforms image into the output color space (tone-mapping, ICC 3DLUT, etc)
static void pass_convert_colors(struct pass_state *pass)
{
const struct pl_render_params *params = pass->params;
const struct pl_frame *image = &pass->image;
const struct pl_frame *target = &pass->target;
pl_renderer rr = pass->rr;
struct img *img = &pass->img;
pl_shader sh = img_sh(pass, img);
bool prelinearized = false;
bool need_conversion = true;
assert(image->color.primaries == img->color.primaries);
if (img->color.transfer == PL_COLOR_TRC_LINEAR) {
if (img->repr.alpha == PL_ALPHA_PREMULTIPLIED) {
// Very annoying edge case: since prelinerization happens with
// premultiplied alpha, but color mapping happens with independent
// alpha, we need to go back to non-linear representation *before*
// alpha mode conversion, to avoid distortion
img->color.transfer = image->color.transfer;
pl_shader_delinearize(sh, &img->color);
} else {
prelinearized = true;
}
} else if (img->color.transfer != image->color.transfer) {
if (image->color.transfer == PL_COLOR_TRC_LINEAR) {
// Another annoying edge case: if the input is linear light, but we
// decide to un-linearize it for scaling purposes, we need to
// re-linearize before passing it into `pl_shader_color_map`
pl_shader_linearize(sh, &img->color);
img->color.transfer = PL_COLOR_TRC_LINEAR;
}
}
// Do all processing in independent alpha, to avoid nonlinear distortions
pl_shader_set_alpha(sh, &img->repr, PL_ALPHA_INDEPENDENT);
// Apply color blindness simulation if requested
if (params->cone_params)
pl_shader_cone_distort(sh, img->color, params->cone_params);
if (params->lut) {
struct pl_color_space lut_in = params->lut->color_in;
struct pl_color_space lut_out = params->lut->color_out;
switch (params->lut_type) {
case PL_LUT_UNKNOWN:
case PL_LUT_NATIVE:
pl_color_space_merge(&lut_in, &image->color);
pl_color_space_merge(&lut_out, &image->color);
break;
case PL_LUT_CONVERSION:
pl_color_space_merge(&lut_in, &image->color);
need_conversion = false; // conversion LUT the highest priority
break;
case PL_LUT_NORMALIZED:
if (!prelinearized) {
// PL_LUT_NORMALIZED wants linear input data
pl_shader_linearize(sh, &img->color);
img->color.transfer = PL_COLOR_TRC_LINEAR;
prelinearized = true;
}
pl_color_space_merge(&lut_in, &img->color);
pl_color_space_merge(&lut_out, &img->color);
break;
}
pl_shader_color_map_ex(sh, params->color_map_params, pl_color_map_args(
.src = image->color,
.dst = lut_in,
.prelinearized = prelinearized,
));
if (params->lut_type == PL_LUT_NORMALIZED) {
GLSLF("color.rgb *= vec3(1.0/"$"); \n",
SH_FLOAT(pl_color_transfer_nominal_peak(lut_in.transfer)));
}
pl_shader_custom_lut(sh, params->lut, &rr->lut_state[LUT_PARAMS]);
if (params->lut_type == PL_LUT_NORMALIZED) {
GLSLF("color.rgb *= vec3("$"); \n",
SH_FLOAT(pl_color_transfer_nominal_peak(lut_out.transfer)));
}
if (params->lut_type != PL_LUT_CONVERSION) {
pl_shader_color_map_ex(sh, params->color_map_params, pl_color_map_args(
.src = lut_out,
.dst = img->color,
));
}
}
if (need_conversion) {
struct pl_color_space target_csp = target->color;
if (target->icc)
target_csp.transfer = PL_COLOR_TRC_LINEAR;
if (pass->need_peak_fbo && !img_tex(pass, img))
return;
// generate HDR feature map if required
pl_tex feature_map = get_feature_map(pass);
sh = img_sh(pass, img); // `get_feature_map` dispatches previous shader
// current -> target
pl_shader_color_map_ex(sh, params->color_map_params, pl_color_map_args(
.src = image->color,
.dst = target_csp,
.prelinearized = prelinearized,
.state = &rr->tone_map_state,
.feature_map = feature_map,
));
if (target->icc)
pl_icc_encode(sh, target->icc, &rr->icc_state[ICC_TARGET]);
}
enum pl_lut_type lut_type = guess_frame_lut_type(target, true);
if (lut_type == PL_LUT_NORMALIZED || lut_type == PL_LUT_CONVERSION)
pl_shader_custom_lut(sh, target->lut, &rr->lut_state[LUT_TARGET]);
img->color = target->color;
}
// Returns true if error diffusion was successfully performed
static bool pass_error_diffusion(struct pass_state *pass, pl_shader *sh,
int new_depth, int comps, int out_w, int out_h)
{
const struct pl_render_params *params = pass->params;
pl_renderer rr = pass->rr;
if (!params->error_diffusion || (rr->errors & PL_RENDER_ERR_ERROR_DIFFUSION))
return false;
size_t shmem_req = pl_error_diffusion_shmem_req(params->error_diffusion, out_h);
if (shmem_req > rr->gpu->glsl.max_shmem_size) {
PL_TRACE(rr, "Disabling error diffusion due to shmem requirements (%zu) "
"exceeding capabilities (%zu)", shmem_req, rr->gpu->glsl.max_shmem_size);
return false;
}
pl_fmt fmt = pass->fbofmt[comps];
if (!fmt || !(fmt->caps & PL_FMT_CAP_STORABLE)) {
PL_ERR(rr, "Error diffusion requires storable FBOs but GPU does not "
"provide them... disabling!");
goto error;
}
struct pl_error_diffusion_params edpars = {
.new_depth = new_depth,
.kernel = params->error_diffusion,
};
// Create temporary framebuffers
edpars.input_tex = get_fbo(pass, out_w, out_h, fmt, comps, PL_DEBUG_TAG);
edpars.output_tex = get_fbo(pass, out_w, out_h, fmt, comps, PL_DEBUG_TAG);
if (!edpars.input_tex || !edpars.output_tex)
goto error;
pl_shader dsh = pl_dispatch_begin(rr->dp);
if (!pl_shader_error_diffusion(dsh, &edpars)) {
pl_dispatch_abort(rr->dp, &dsh);
goto error;
}
// Everything was okay, run the shaders
bool ok = pl_dispatch_finish(rr->dp, pl_dispatch_params(
.shader = sh,
.target = edpars.input_tex,
));
if (ok) {
ok = pl_dispatch_compute(rr->dp, pl_dispatch_compute_params(
.shader = &dsh,
.dispatch_size = {1, 1, 1},
));
}
*sh = pl_dispatch_begin(rr->dp);
pl_shader_sample_direct(*sh, pl_sample_src(
.tex = ok ? edpars.output_tex : edpars.input_tex,
));
return ok;
error:
rr->errors |= PL_RENDER_ERR_ERROR_DIFFUSION;
return false;
}
#define CLEAR_COL(params) \
(float[4]) { \
(params)->background_color[0], \
(params)->background_color[1], \
(params)->background_color[2], \
1.0 - (params)->background_transparency, \
}
static bool pass_output_target(struct pass_state *pass)
{
const struct pl_render_params *params = pass->params;
const struct pl_frame *image = &pass->image;
const struct pl_frame *target = &pass->target;
pl_renderer rr = pass->rr;
struct img *img = &pass->img;
pl_shader sh = img_sh(pass, img);
if (params->corner_rounding > 0.0f) {
const float out_w2 = fabsf(pl_rect_w(target->crop)) / 2.0f;
const float out_h2 = fabsf(pl_rect_h(target->crop)) / 2.0f;
const float radius = fminf(params->corner_rounding, 1.0f) *
fminf(out_w2, out_h2);
const struct pl_rect2df relpos = {
.x0 = -out_w2, .y0 = -out_h2,
.x1 = out_w2, .y1 = out_h2,
};
GLSL("float radius = "$"; \n"
"vec2 size2 = vec2("$", "$"); \n"
"vec2 relpos = "$"; \n"
"vec2 rd = abs(relpos) - size2 + vec2(radius); \n"
"float rdist = length(max(rd, 0.0)) - radius; \n"
"float border = smoothstep(2.0f, 0.0f, rdist); \n",
SH_FLOAT_DYN(radius),
SH_FLOAT_DYN(out_w2), SH_FLOAT_DYN(out_h2),
sh_attr_vec2(sh, "relpos", &relpos));
switch (img->repr.alpha) {
case PL_ALPHA_UNKNOWN:
GLSL("color.a = border; \n");
img->repr.alpha = PL_ALPHA_INDEPENDENT;
img->comps = 4;
break;
case PL_ALPHA_INDEPENDENT:
GLSL("color.a *= border; \n");
break;
case PL_ALPHA_PREMULTIPLIED:
GLSL("color *= border; \n");
break;
case PL_ALPHA_MODE_COUNT:
pl_unreachable();
}
}
const struct pl_plane *ref = &target->planes[pass->dst_ref];
pl_rect2d dst_rect = pass->dst_rect;
if (params->distort_params) {
struct pl_distort_params dpars = *params->distort_params;
if (dpars.alpha_mode) {
pl_shader_set_alpha(sh, &img->repr, dpars.alpha_mode);
img->repr.alpha = dpars.alpha_mode;
img->comps = 4;
}
pl_tex tex = img_tex(pass, img);
if (!tex)
return false;
// Expand canvas to fit result of distortion
const float ar = pl_rect2df_aspect(&target->crop);
const float sx = fminf(ar, 1.0f);
const float sy = fminf(1.0f / ar, 1.0f);
pl_rect2df bb = pl_transform2x2_bounds(&dpars.transform, &(pl_rect2df) {
.x0 = -sx, .x1 = sx,
.y0 = -sy, .y1 = sy,
});
// Clamp to output size and adjust as needed when constraining output
pl_rect2df tmp = target->crop;
pl_rect2df_stretch(&tmp, pl_rect_w(bb) / (2*sx), pl_rect_h(bb) / (2*sy));
const float tmp_w = pl_rect_w(tmp), tmp_h = pl_rect_h(tmp);
int canvas_w = ref->texture->params.w,
canvas_h = ref->texture->params.h;
if (pass->rotation % PL_ROTATION_180 == PL_ROTATION_90)
PL_SWAP(canvas_w, canvas_h);
tmp.x0 = PL_CLAMP(tmp.x0, 0.0f, canvas_w);
tmp.x1 = PL_CLAMP(tmp.x1, 0.0f, canvas_w);
tmp.y0 = PL_CLAMP(tmp.y0, 0.0f, canvas_h);
tmp.y1 = PL_CLAMP(tmp.y1, 0.0f, canvas_h);
if (dpars.constrain) {
const float rx = pl_rect_w(tmp) / tmp_w;
const float ry = pl_rect_h(tmp) / tmp_h;
pl_rect2df_stretch(&tmp, fminf(ry / rx, 1.0f), fminf(rx / ry, 1.0f));
}
dst_rect.x0 = roundf(tmp.x0);
dst_rect.x1 = roundf(tmp.x1);
dst_rect.y0 = roundf(tmp.y0);
dst_rect.y1 = roundf(tmp.y1);
dpars.unscaled = true;
img->w = abs(pl_rect_w(dst_rect));
img->h = abs(pl_rect_h(dst_rect));
img->tex = NULL;
img->sh = sh = pl_dispatch_begin(rr->dp);
pl_shader_distort(sh, tex, img->w, img->h, &dpars);
}
pass_hook(pass, img, PL_HOOK_PRE_OUTPUT);
bool need_blend = params->blend_against_tiles ||
(!target->repr.alpha && !params->blend_params);
if (img->comps == 4 && need_blend) {
if (params->blend_against_tiles) {
static const float zero[2][3] = {0};
const float (*color)[3] = params->tile_colors;
if (memcmp(color, zero, sizeof(zero)) == 0)
color = pl_render_default_params.tile_colors;
int size = PL_DEF(params->tile_size, pl_render_default_params.tile_size);
GLSLH("#define bg_tile_a vec3("$", "$", "$") \n",
SH_FLOAT(color[0][0]), SH_FLOAT(color[0][1]), SH_FLOAT(color[0][2]));
GLSLH("#define bg_tile_b vec3("$", "$", "$") \n",
SH_FLOAT(color[1][0]), SH_FLOAT(color[1][1]), SH_FLOAT(color[1][2]));
GLSL("vec2 outcoord = gl_FragCoord.xy * "$"; \n"
"bvec2 tile = lessThan(fract(outcoord), vec2(0.5)); \n"
"vec3 bg_color = tile.x == tile.y ? bg_tile_a : bg_tile_b; \n",
SH_FLOAT(1.0 / size));
} else {
GLSLH("#define bg_color vec3("$", "$", "$") \n",
SH_FLOAT(params->background_color[0]),
SH_FLOAT(params->background_color[1]),
SH_FLOAT(params->background_color[2]));
}
pl_shader_set_alpha(sh, &img->repr, PL_ALPHA_PREMULTIPLIED);
GLSL("color = vec4(color.rgb + bg_color * (1.0 - color.a), 1.0); \n");
img->repr.alpha = PL_ALPHA_UNKNOWN;
img->comps = 3;
}
// Apply the color scale separately, after encoding is done, to make sure
// that the intermediate FBO (if any) has the correct precision.
struct pl_color_repr repr = target->repr;
float scale = pl_color_repr_normalize(&repr);
enum pl_lut_type lut_type = guess_frame_lut_type(target, true);
if (lut_type != PL_LUT_CONVERSION)
pl_shader_encode_color(sh, &repr);
if (lut_type == PL_LUT_NATIVE) {
pl_shader_set_alpha(sh, &img->repr, PL_ALPHA_INDEPENDENT);
pl_shader_custom_lut(sh, target->lut, &rr->lut_state[LUT_TARGET]);
pl_shader_set_alpha(sh, &img->repr, PL_ALPHA_PREMULTIPLIED);
}
// Rotation handling
if (pass->rotation % PL_ROTATION_180 == PL_ROTATION_90) {
PL_SWAP(dst_rect.x0, dst_rect.y0);
PL_SWAP(dst_rect.x1, dst_rect.y1);
PL_SWAP(img->w, img->h);
sh->transpose = true;
}
pass_hook(pass, img, PL_HOOK_OUTPUT);
sh = NULL;
bool flipped_x = dst_rect.x1 < dst_rect.x0,
flipped_y = dst_rect.y1 < dst_rect.y0;
if (!params->skip_target_clearing && pl_frame_is_cropped(target))
pl_frame_clear_rgba(rr->gpu, target, CLEAR_COL(params));
for (int p = 0; p < target->num_planes; p++) {
const struct pl_plane *plane = &target->planes[p];
float rx = (float) plane->texture->params.w / ref->texture->params.w,
ry = (float) plane->texture->params.h / ref->texture->params.h;
// Only accept integer scaling ratios. This accounts for the fact
// that fractionally subsampled planes get rounded up to the
// nearest integer size, which we want to over-render.
float rrx = rx >= 1 ? roundf(rx) : 1.0 / roundf(1.0 / rx),
rry = ry >= 1 ? roundf(ry) : 1.0 / roundf(1.0 / ry);
float sx = plane->shift_x, sy = plane->shift_y;
pl_rect2df plane_rectf = {
.x0 = (dst_rect.x0 - sx) * rrx,
.y0 = (dst_rect.y0 - sy) * rry,
.x1 = (dst_rect.x1 - sx) * rrx,
.y1 = (dst_rect.y1 - sy) * rry,
};
// Normalize to make the math easier
pl_rect2df_normalize(&plane_rectf);
// Round the output rect
int rx0 = floorf(plane_rectf.x0), ry0 = floorf(plane_rectf.y0),
rx1 = ceilf(plane_rectf.x1), ry1 = ceilf(plane_rectf.y1);
PL_TRACE(rr, "Subsampled target %d: {%f %f %f %f} -> {%d %d %d %d}",
p, plane_rectf.x0, plane_rectf.y0,
plane_rectf.x1, plane_rectf.y1,
rx0, ry0, rx1, ry1);
if (target->num_planes > 1) {
// Planar output, so we need to sample from an intermediate FBO
struct pl_sample_src src = {
.tex = img_tex(pass, img),
.new_w = rx1 - rx0,
.new_h = ry1 - ry0,
.rect = {
.x0 = (rx0 - plane_rectf.x0) / rrx,
.x1 = (rx1 - plane_rectf.x0) / rrx,
.y0 = (ry0 - plane_rectf.y0) / rry,
.y1 = (ry1 - plane_rectf.y0) / rry,
},
};
if (!src.tex) {
PL_ERR(rr, "Output requires multiple planes, but FBOs are "
"unavailable. This combination is unsupported.");
return false;
}
PL_TRACE(rr, "Sampling %dx%d img aligned from {%f %f %f %f}",
pass->img.w, pass->img.h,
src.rect.x0, src.rect.y0,
src.rect.x1, src.rect.y1);
for (int c = 0; c < plane->components; c++) {
if (plane->component_mapping[c] < 0)
continue;
src.component_mask |= 1 << plane->component_mapping[c];
}
sh = pl_dispatch_begin(rr->dp);
dispatch_sampler(pass, sh, &rr->samplers_dst[p], SAMPLER_PLANE,
plane->texture, &src);
} else {
// Single plane, so we can directly re-use the img shader unless
// it's incompatible with the FBO capabilities
bool is_comp = pl_shader_is_compute(img_sh(pass, img));
if (is_comp && !plane->texture->params.storable) {
if (!img_tex(pass, img)) {
PL_ERR(rr, "Rendering requires compute shaders, but output "
"is not storable, and FBOs are unavailable. This "
"combination is unsupported.");
return false;
}
}
sh = img_sh(pass, img);
img->sh = NULL;
}
// Ignore dithering for > 16-bit outputs by default, since it makes
// little sense to do so (and probably just adds errors)
int depth = target->repr.bits.color_depth, applied_dither = 0;
if (depth && (depth < 16 || params->force_dither)) {
if (pass_error_diffusion(pass, &sh, depth, plane->components,
rx1 - rx0, ry1 - ry0))
{
applied_dither = depth;
} else if (params->dither_params) {
struct pl_dither_params dparams = *params->dither_params;
if (!params->disable_dither_gamma_correction)
dparams.transfer = target->color.transfer;
pl_shader_dither(sh, depth, &rr->dither_state, &dparams);
applied_dither = depth;
}
}
if (applied_dither != rr->prev_dither) {
if (applied_dither) {
PL_INFO(rr, "Dithering to %d bit depth", applied_dither);
} else {
PL_INFO(rr, "Dithering disabled");
}
rr->prev_dither = applied_dither;
}
GLSL("color *= vec4(1.0 / "$"); \n", SH_FLOAT(scale));
swizzle_color(sh, plane->components, plane->component_mapping,
params->blend_params);
pl_rect2d plane_rect = {
.x0 = flipped_x ? rx1 : rx0,
.x1 = flipped_x ? rx0 : rx1,
.y0 = flipped_y ? ry1 : ry0,
.y1 = flipped_y ? ry0 : ry1,
};
pl_transform2x2 tscale = {
.mat = {{{ rrx, 0.0 }, { 0.0, rry }}},
.c = { -sx, -sy },
};
if (plane->flipped) {
int plane_h = rry * ref->texture->params.h;
plane_rect.y0 = plane_h - plane_rect.y0;
plane_rect.y1 = plane_h - plane_rect.y1;
tscale.mat.m[1][1] = -tscale.mat.m[1][1];
tscale.c[1] += plane->texture->params.h;
}
bool ok = pl_dispatch_finish(rr->dp, pl_dispatch_params(
.shader = &sh,
.target = plane->texture,
.blend_params = params->blend_params,
.rect = plane_rect,
));
if (!ok)
return false;
if (pass->info.stage != PL_RENDER_STAGE_BLEND) {
draw_overlays(pass, plane->texture, plane->components,
plane->component_mapping, image->overlays,
image->num_overlays, target->color, target->repr,
&tscale);
}
draw_overlays(pass, plane->texture, plane->components,
plane->component_mapping, target->overlays,
target->num_overlays, target->color, target->repr,
&tscale);
}
*img = (struct img) {0};
return true;
}
#define require(expr) pl_require(rr, expr)
#define validate_plane(plane, param) \
do { \
require((plane).texture); \
require((plane).texture->params.param); \
require((plane).components > 0 && (plane).components <= 4); \
for (int c = 0; c < (plane).components; c++) { \
require((plane).component_mapping[c] >= PL_CHANNEL_NONE && \
(plane).component_mapping[c] <= PL_CHANNEL_A); \
} \
} while (0)
#define validate_overlay(overlay) \
do { \
require((overlay).tex); \
require((overlay).tex->params.sampleable); \
require((overlay).num_parts >= 0); \
for (int n = 0; n < (overlay).num_parts; n++) { \
const struct pl_overlay_part *p = &(overlay).parts[n]; \
require(pl_rect_w(p->dst) && pl_rect_h(p->dst)); \
} \
} while (0)
#define validate_deinterlace_ref(image, ref) \
do { \
require((image)->num_planes == (ref)->num_planes); \
const struct pl_tex_params *imgp, *refp; \
for (int p = 0; p < (image)->num_planes; p++) { \
validate_plane((ref)->planes[p], sampleable); \
imgp = &(image)->planes[p].texture->params; \
refp = &(ref)->planes[p].texture->params; \
require(imgp->w == refp->w); \
require(imgp->h == refp->h); \
require(imgp->format->num_components == refp->format->num_components);\
} \
} while (0)
// Perform some basic validity checks on incoming structs to help catch invalid
// API usage. This is not an exhaustive check. In particular, enums are not
// bounds checked. This is because most functions accepting enums already
// abort() in the default case, and because it's not the intent of this check
// to catch all instances of memory corruption - just common logic bugs.
static bool validate_structs(pl_renderer rr,
const struct pl_frame *image,
const struct pl_frame *target)
{
// Rendering to/from a frame with no planes is technically allowed, but so
// pointless that it's more likely to be a user error worth catching.
require(target->num_planes > 0 && target->num_planes <= PL_MAX_PLANES);
for (int i = 0; i < target->num_planes; i++)
validate_plane(target->planes[i], renderable);
require(!pl_rect_w(target->crop) == !pl_rect_h(target->crop));
require(target->num_overlays >= 0);
for (int i = 0; i < target->num_overlays; i++)
validate_overlay(target->overlays[i]);
if (!image)
return true;
require(image->num_planes > 0 && image->num_planes <= PL_MAX_PLANES);
for (int i = 0; i < image->num_planes; i++)
validate_plane(image->planes[i], sampleable);
require(!pl_rect_w(image->crop) == !pl_rect_h(image->crop));
require(image->num_overlays >= 0);
for (int i = 0; i < image->num_overlays; i++)
validate_overlay(image->overlays[i]);
if (image->field != PL_FIELD_NONE) {
require(image->first_field != PL_FIELD_NONE);
if (image->prev)
validate_deinterlace_ref(image, image->prev);
if (image->next)
validate_deinterlace_ref(image, image->next);
}
return true;
error:
return false;
}
// returns index
static int frame_ref(const struct pl_frame *frame)
{
pl_assert(frame->num_planes);
for (int i = 0; i < frame->num_planes; i++) {
switch (detect_plane_type(&frame->planes[i], &frame->repr)) {
case PLANE_RGB:
case PLANE_LUMA:
case PLANE_XYZ:
return i;
case PLANE_CHROMA:
case PLANE_ALPHA:
continue;
case PLANE_INVALID:
pl_unreachable();
}
}
return 0;
}
static void fix_refs_and_rects(struct pass_state *pass)
{
struct pl_frame *target = &pass->target;
pl_rect2df *dst = &target->crop;
pass->dst_ref = frame_ref(target);
pl_tex dst_ref = target->planes[pass->dst_ref].texture;
int dst_w = dst_ref->params.w, dst_h = dst_ref->params.h;
if ((!dst->x0 && !dst->x1) || (!dst->y0 && !dst->y1)) {
dst->x1 = dst_w;
dst->y1 = dst_h;
}
if (pass->src_ref < 0) {
// Simplified version of the below code which only rounds the target
// rect but doesn't retroactively apply the crop to the image
pass->rotation = pl_rotation_normalize(-target->rotation);
pl_rect2df_rotate(dst, -pass->rotation);
if (pass->rotation % PL_ROTATION_180 == PL_ROTATION_90)
PL_SWAP(dst_w, dst_h);
*dst = (pl_rect2df) {
.x0 = roundf(PL_CLAMP(dst->x0, 0.0, dst_w)),
.y0 = roundf(PL_CLAMP(dst->y0, 0.0, dst_h)),
.x1 = roundf(PL_CLAMP(dst->x1, 0.0, dst_w)),
.y1 = roundf(PL_CLAMP(dst->y1, 0.0, dst_h)),
};
pass->dst_rect = (pl_rect2d) {
dst->x0, dst->y0, dst->x1, dst->y1,
};
return;
}
struct pl_frame *image = &pass->image;
pl_rect2df *src = &image->crop;
pass->src_ref = frame_ref(image);
pl_tex src_ref = image->planes[pass->src_ref].texture;
if ((!src->x0 && !src->x1) || (!src->y0 && !src->y1)) {
src->x1 = src_ref->params.w;
src->y1 = src_ref->params.h;
};
// Compute end-to-end rotation
pass->rotation = pl_rotation_normalize(image->rotation - target->rotation);
pl_rect2df_rotate(dst, -pass->rotation); // normalize by counter-rotating
if (pass->rotation % PL_ROTATION_180 == PL_ROTATION_90)
PL_SWAP(dst_w, dst_h);
// Keep track of whether the end-to-end rendering is flipped
bool flipped_x = (src->x0 > src->x1) != (dst->x0 > dst->x1),
flipped_y = (src->y0 > src->y1) != (dst->y0 > dst->y1);
// Normalize both rects to make the math easier
pl_rect2df_normalize(src);
pl_rect2df_normalize(dst);
// Round the output rect and clip it to the framebuffer dimensions
float rx0 = roundf(PL_CLAMP(dst->x0, 0.0, dst_w)),
ry0 = roundf(PL_CLAMP(dst->y0, 0.0, dst_h)),
rx1 = roundf(PL_CLAMP(dst->x1, 0.0, dst_w)),
ry1 = roundf(PL_CLAMP(dst->y1, 0.0, dst_h));
// Adjust the src rect corresponding to the rounded crop
float scale_x = pl_rect_w(*src) / pl_rect_w(*dst),
scale_y = pl_rect_h(*src) / pl_rect_h(*dst),
base_x = src->x0,
base_y = src->y0;
src->x0 = base_x + (rx0 - dst->x0) * scale_x;
src->x1 = base_x + (rx1 - dst->x0) * scale_x;
src->y0 = base_y + (ry0 - dst->y0) * scale_y;
src->y1 = base_y + (ry1 - dst->y0) * scale_y;
// Update dst_rect to the rounded values and re-apply flip if needed. We
// always do this in the `dst` rather than the `src`` because this allows
// e.g. polar sampling compute shaders to work.
*dst = (pl_rect2df) {
.x0 = flipped_x ? rx1 : rx0,
.y0 = flipped_y ? ry1 : ry0,
.x1 = flipped_x ? rx0 : rx1,
.y1 = flipped_y ? ry0 : ry1,
};
// Copies of the above, for convenience
pass->ref_rect = *src;
pass->dst_rect = (pl_rect2d) {
dst->x0, dst->y0, dst->x1, dst->y1,
};
}
static void fix_frame(struct pl_frame *frame)
{
pl_tex tex = frame->planes[frame_ref(frame)].texture;
if (frame->repr.sys == PL_COLOR_SYSTEM_XYZ) {
// XYZ is implicity converted to linear DCI-P3 in pl_color_repr_decode
frame->color.primaries = PL_COLOR_PRIM_DCI_P3;
frame->color.transfer = PL_COLOR_TRC_ST428;
}
// If the primaries are not known, guess them based on the resolution
if (tex && !frame->color.primaries)
frame->color.primaries = pl_color_primaries_guess(tex->params.w, tex->params.h);
// For UNORM formats, we can infer the sampled bit depth from the texture
// itself. This is ignored for other format types, because the logic
// doesn't really work out for them anyways, and it's best not to do
// anything too crazy unless the user provides explicit details.
struct pl_bit_encoding *bits = &frame->repr.bits;
if (!bits->sample_depth && tex && tex->params.format->type == PL_FMT_UNORM) {
// Just assume the first component's depth is canonical. This works in
// practice, since for cases like rgb565 we want to use the lower depth
// anyway. Plus, every format has at least one component.
bits->sample_depth = tex->params.format->component_depth[0];
// If we don't know the color depth, assume it spans the full range of
// the texture. Otherwise, clamp it to the texture depth.
bits->color_depth = PL_DEF(bits->color_depth, bits->sample_depth);
bits->color_depth = PL_MIN(bits->color_depth, bits->sample_depth);
// If the texture depth is higher than the known color depth, assume
// the colors were left-shifted.
bits->bit_shift += bits->sample_depth - bits->color_depth;
}
}
static bool acquire_frame(struct pass_state *pass, struct pl_frame *frame,
bool *acquired)
{
if (!frame || !frame->acquire || *acquired)
return true;
*acquired = true;
return frame->acquire(pass->rr->gpu, frame);
}
static void release_frame(struct pass_state *pass, struct pl_frame *frame,
bool *acquired)
{
if (frame && frame->release && *acquired)
frame->release(pass->rr->gpu, frame);
*acquired = false;
}
static void pass_uninit(struct pass_state *pass)
{
pl_renderer rr = pass->rr;
pl_dispatch_abort(rr->dp, &pass->img.sh);
release_frame(pass, &pass->next, &pass->acquired.next);
release_frame(pass, &pass->prev, &pass->acquired.prev);
release_frame(pass, &pass->image, &pass->acquired.image);
release_frame(pass, &pass->target, &pass->acquired.target);
pl_free_ptr(&pass->tmp);
}
static void icc_fallback(struct pass_state *pass, struct pl_frame *frame,
struct icc_state *fallback)
{
if (!frame || frame->icc || !frame->profile.data)
return;
// Don't re-attempt opening already failed profiles
if (fallback->error && fallback->error == frame->profile.signature)
return;
#ifdef PL_HAVE_LCMS
pl_renderer rr = pass->rr;
if (pl_icc_update(rr->log, &fallback->icc, &frame->profile, NULL)) {
frame->icc = fallback->icc;
} else {
PL_WARN(rr, "Failed opening ICC profile... ignoring");
fallback->error = frame->profile.signature;
}
#endif
}
static void pass_fix_frames(struct pass_state *pass)
{
pl_renderer rr = pass->rr;
struct pl_frame *image = pass->src_ref < 0 ? NULL : &pass->image;
struct pl_frame *target = &pass->target;
fix_refs_and_rects(pass);
// Fallback for older ICC profile API
icc_fallback(pass, image, &rr->icc_fallback[ICC_IMAGE]);
icc_fallback(pass, target, &rr->icc_fallback[ICC_TARGET]);
// Force colorspace metadata to ICC profile values, if present
if (image && image->icc) {
image->color.primaries = image->icc->containing_primaries;
image->color.hdr = image->icc->csp.hdr;
}
if (target->icc) {
target->color.primaries = target->icc->containing_primaries;
target->color.hdr = target->icc->csp.hdr;
}
// Infer the target color space info based on the image's
if (image) {
fix_frame(image);
pl_color_space_infer_map(&image->color, &target->color);
fix_frame(target); // do this only after infer_map
} else {
fix_frame(target);
pl_color_space_infer(&target->color);
}
// Detect the presence of an alpha channel in the frames and explicitly
// default the alpha mode in this case, so we can use it to detect whether
// or not to strip the alpha channel during rendering.
//
// Note the different defaults for the image and target, because files
// are usually independent but windowing systems usually expect
// premultiplied. (We also premultiply for internal rendering, so this
// way of doing it avoids a possible division-by-zero path!)
if (image && !image->repr.alpha) {
for (int i = 0; i < image->num_planes; i++) {
const struct pl_plane *plane = &image->planes[i];
for (int c = 0; c < plane->components; c++) {
if (plane->component_mapping[c] == PL_CHANNEL_A)
image->repr.alpha = PL_ALPHA_INDEPENDENT;
}
}
}
if (!target->repr.alpha) {
for (int i = 0; i < target->num_planes; i++) {
const struct pl_plane *plane = &target->planes[i];
for (int c = 0; c < plane->components; c++) {
if (plane->component_mapping[c] == PL_CHANNEL_A)
target->repr.alpha = PL_ALPHA_PREMULTIPLIED;
}
}
}
}
void pl_frames_infer(pl_renderer rr, struct pl_frame *image,
struct pl_frame *target)
{
struct pass_state pass = {
.rr = rr,
.image = *image,
.target = *target,
};
pass_fix_frames(&pass);
*image = pass.image;
*target = pass.target;
}
static bool pass_init(struct pass_state *pass, bool acquire_image)
{
struct pl_frame *image = pass->src_ref < 0 ? NULL : &pass->image;
struct pl_frame *target = &pass->target;
if (!acquire_frame(pass, target, &pass->acquired.target))
goto error;
if (acquire_image && image) {
if (!acquire_frame(pass, image, &pass->acquired.image))
goto error;
const struct pl_render_params *params = pass->params;
const struct pl_deinterlace_params *deint = params->deinterlace_params;
bool needs_refs = image->field != PL_FIELD_NONE && deint &&
pl_deinterlace_needs_refs(deint->algo);
if (image->prev && needs_refs) {
// Move into local copy so we can acquire/release it
pass->prev = *image->prev;
image->prev = &pass->prev;
if (!acquire_frame(pass, &pass->prev, &pass->acquired.prev))
goto error;
}
if (image->next && needs_refs) {
pass->next = *image->next;
image->next = &pass->next;
if (!acquire_frame(pass, &pass->next, &pass->acquired.next))
goto error;
}
}
if (!validate_structs(pass->rr, acquire_image ? image : NULL, target))
goto error;
find_fbo_format(pass);
pass_fix_frames(pass);
pass->tmp = pl_tmp(NULL);
return true;
error:
pass_uninit(pass);
return false;
}
static void pass_begin_frame(struct pass_state *pass)
{
pl_renderer rr = pass->rr;
const struct pl_render_params *params = pass->params;
pl_dispatch_callback(rr->dp, pass, info_callback);
pl_dispatch_reset_frame(rr->dp);
for (int i = 0; i < params->num_hooks; i++) {
if (params->hooks[i]->reset)
params->hooks[i]->reset(params->hooks[i]->priv);
}
size_t size = rr->fbos.num * sizeof(bool);
pass->fbos_used = pl_realloc(pass->tmp, pass->fbos_used, size);
memset(pass->fbos_used, 0, size);
}
static bool draw_empty_overlays(pl_renderer rr,
const struct pl_frame *ptarget,
const struct pl_render_params *params)
{
if (!params->skip_target_clearing)
pl_frame_clear_rgba(rr->gpu, ptarget, CLEAR_COL(params));
if (!ptarget->num_overlays)
return true;
struct pass_state pass = {
.rr = rr,
.params = params,
.src_ref = -1,
.target = *ptarget,
.info.stage = PL_RENDER_STAGE_BLEND,
.info.count = 0,
};
if (!pass_init(&pass, false))
return false;
pass_begin_frame(&pass);
struct pl_frame *target = &pass.target;
pl_tex ref = target->planes[pass.dst_ref].texture;
for (int p = 0; p < target->num_planes; p++) {
const struct pl_plane *plane = &target->planes[p];
// Math replicated from `pass_output_target`
float rx = (float) plane->texture->params.w / ref->params.w,
ry = (float) plane->texture->params.h / ref->params.h;
float rrx = rx >= 1 ? roundf(rx) : 1.0 / roundf(1.0 / rx),
rry = ry >= 1 ? roundf(ry) : 1.0 / roundf(1.0 / ry);
float sx = plane->shift_x, sy = plane->shift_y;
pl_transform2x2 tscale = {
.mat = {{{ rrx, 0.0 }, { 0.0, rry }}},
.c = { -sx, -sy },
};
if (plane->flipped) {
tscale.mat.m[1][1] = -tscale.mat.m[1][1];
tscale.c[1] += plane->texture->params.h;
}
draw_overlays(&pass, plane->texture, plane->components,
plane->component_mapping, target->overlays,
target->num_overlays, target->color, target->repr,
&tscale);
}
pass_uninit(&pass);
return true;
}
bool pl_render_image(pl_renderer rr, const struct pl_frame *pimage,
const struct pl_frame *ptarget,
const struct pl_render_params *params)
{
params = PL_DEF(params, &pl_render_default_params);
pl_dispatch_mark_dynamic(rr->dp, params->dynamic_constants);
if (!pimage)
return draw_empty_overlays(rr, ptarget, params);
struct pass_state pass = {
.rr = rr,
.params = params,
.image = *pimage,
.target = *ptarget,
.info.stage = PL_RENDER_STAGE_FRAME,
};
if (!pass_init(&pass, true))
return false;
// No-op (empty crop)
if (!pl_rect_w(pass.dst_rect) || !pl_rect_h(pass.dst_rect)) {
pass_uninit(&pass);
return draw_empty_overlays(rr, ptarget, params);
}
pass_begin_frame(&pass);
if (!pass_read_image(&pass))
goto error;
if (!pass_scale_main(&pass))
goto error;
pass_convert_colors(&pass);
if (!pass_output_target(&pass))
goto error;
pass_uninit(&pass);
return true;
error:
PL_ERR(rr, "Failed rendering image!");
pass_uninit(&pass);
return false;
}
const struct pl_frame *pl_frame_mix_current(const struct pl_frame_mix *mix)
{
const struct pl_frame *cur = NULL;
for (int i = 0; i < mix->num_frames; i++) {
if (mix->timestamps[i] > 0.0f)
break;
cur = mix->frames[i];
}
return cur;
}
const struct pl_frame *pl_frame_mix_nearest(const struct pl_frame_mix *mix)
{
if (!mix->num_frames)
return NULL;
const struct pl_frame *best = mix->frames[0];
float best_dist = fabsf(mix->timestamps[0]);
for (int i = 1; i < mix->num_frames; i++) {
float dist = fabsf(mix->timestamps[i]);
if (dist < best_dist) {
best = mix->frames[i];
best_dist = dist;
continue;
} else {
break;
}
}
return best;
}
struct params_info {
uint64_t hash;
bool trivial;
};
static struct params_info render_params_info(const struct pl_render_params *params_orig)
{
struct pl_render_params params = *params_orig;
struct params_info info = {
.trivial = true,
.hash = 0,
};
#define HASH_PTR(ptr, def, ptr_trivial) \
do { \
if (ptr) { \
pl_hash_merge(&info.hash, pl_mem_hash(ptr, sizeof(*ptr))); \
info.trivial &= (ptr_trivial); \
ptr = NULL; \
} else if ((def) != NULL) { \
pl_hash_merge(&info.hash, pl_mem_hash(def, sizeof(*ptr))); \
} \
} while (0)
#define HASH_FILTER(scaler) \
do { \
if ((scaler == &pl_filter_bilinear || scaler == &pl_filter_nearest) && \
params.skip_anti_aliasing) \
{ \
/* treat as NULL */ \
} else if (scaler) { \
struct pl_filter_config filter = *scaler; \
HASH_PTR(filter.kernel, NULL, false); \
HASH_PTR(filter.window, NULL, false); \
pl_hash_merge(&info.hash, pl_var_hash(filter)); \
scaler = NULL; \
} \
} while (0)
HASH_FILTER(params.upscaler);
HASH_FILTER(params.downscaler);
HASH_PTR(params.deband_params, NULL, false);
HASH_PTR(params.sigmoid_params, NULL, false);
HASH_PTR(params.deinterlace_params, NULL, false);
HASH_PTR(params.cone_params, NULL, true);
HASH_PTR(params.icc_params, &pl_icc_default_params, true);
HASH_PTR(params.color_adjustment, &pl_color_adjustment_neutral, true);
HASH_PTR(params.color_map_params, &pl_color_map_default_params, true);
HASH_PTR(params.peak_detect_params, NULL, false);
// Hash all hooks
for (int i = 0; i < params.num_hooks; i++) {
const struct pl_hook *hook = params.hooks[i];
if (hook->stages == PL_HOOK_OUTPUT)
continue; // ignore hooks only relevant to pass_output_target
pl_hash_merge(&info.hash, pl_var_hash(*hook));
info.trivial = false;
}
params.hooks = NULL;
// Hash the LUT by only looking at the signature
if (params.lut) {
pl_hash_merge(&info.hash, params.lut->signature);
info.trivial = false;
params.lut = NULL;
}
#define CLEAR(field) field = (__typeof__(field)) {0}
// Clear out fields only relevant to pl_render_image_mix
CLEAR(params.frame_mixer);
CLEAR(params.preserve_mixing_cache);
CLEAR(params.skip_caching_single_frame);
memset(params.background_color, 0, sizeof(params.background_color));
CLEAR(params.background_transparency);
CLEAR(params.skip_target_clearing);
CLEAR(params.blend_against_tiles);
memset(params.tile_colors, 0, sizeof(params.tile_colors));
CLEAR(params.tile_size);
// Clear out fields only relevant to pass_output_target
CLEAR(params.blend_params);
CLEAR(params.distort_params);
CLEAR(params.dither_params);
CLEAR(params.error_diffusion);
CLEAR(params.force_dither);
CLEAR(params.corner_rounding);
// Clear out other irrelevant fields
CLEAR(params.dynamic_constants);
CLEAR(params.info_callback);
CLEAR(params.info_priv);
pl_hash_merge(&info.hash, pl_var_hash(params));
return info;
}
#define MAX_MIX_FRAMES 16
bool pl_render_image_mix(pl_renderer rr, const struct pl_frame_mix *images,
const struct pl_frame *ptarget,
const struct pl_render_params *params)
{
if (!images->num_frames)
return pl_render_image(rr, NULL, ptarget, params);
params = PL_DEF(params, &pl_render_default_params);
struct params_info par_info = render_params_info(params);
pl_dispatch_mark_dynamic(rr->dp, params->dynamic_constants);
require(images->num_frames >= 1);
require(images->vsync_duration > 0.0);
for (int i = 0; i < images->num_frames - 1; i++)
require(images->timestamps[i] <= images->timestamps[i+1]);
const struct pl_frame *refimg = pl_frame_mix_nearest(images);
struct pass_state pass = {
.rr = rr,
.params = params,
.image = *refimg,
.target = *ptarget,
.info.stage = PL_RENDER_STAGE_BLEND,
};
if (rr->errors & PL_RENDER_ERR_FRAME_MIXING)
goto fallback;
if (!pass_init(&pass, false))
return false;
if (!pass.fbofmt[4])
goto fallback;
const struct pl_frame *target = &pass.target;
int out_w = abs(pl_rect_w(pass.dst_rect)),
out_h = abs(pl_rect_h(pass.dst_rect));
if (!out_w || !out_h)
goto fallback;
int fidx = 0;
struct cached_frame frames[MAX_MIX_FRAMES];
float weights[MAX_MIX_FRAMES];
float wsum = 0.0;
// Garbage collect the cache by evicting all frames from the cache that are
// not determined to still be required
for (int i = 0; i < rr->frames.num; i++)
rr->frames.elem[i].evict = true;
// Blur frame mixer according to vsync ratio (source / display)
struct pl_filter_config mixer;
if (params->frame_mixer) {
mixer = *params->frame_mixer;
mixer.blur = PL_DEF(mixer.blur, 1.0);
for (int i = 1; i < images->num_frames; i++) {
if (images->timestamps[i] >= 0.0 && images->timestamps[i - 1] < 0) {
float frame_dur = images->timestamps[i] - images->timestamps[i - 1];
if (images->vsync_duration > frame_dur && !params->skip_anti_aliasing)
mixer.blur *= images->vsync_duration / frame_dur;
break;
}
}
}
// Traverse the input frames and determine/prepare the ones we need
bool single_frame = !params->frame_mixer || images->num_frames == 1;
retry:
for (int i = 0; i < images->num_frames; i++) {
uint64_t sig = images->signatures[i];
float rts = images->timestamps[i];
const struct pl_frame *img = images->frames[i];
PL_TRACE(rr, "Considering image with signature 0x%llx, rts %f",
(unsigned long long) sig, rts);
// Combining images with different rotations is basically unfeasible
if (pl_rotation_normalize(img->rotation - refimg->rotation)) {
PL_TRACE(rr, " -> Skipping: incompatible rotation");
continue;
}
float weight;
if (single_frame) {
// Only render the refimg, ignore others
if (img == refimg) {
weight = 1.0;
} else {
PL_TRACE(rr, " -> Skipping: no frame mixer");
continue;
}
// For backwards compatibility, treat !kernel as oversample
} else if (!mixer.kernel || mixer.kernel == &pl_filter_function_oversample) {
// Compute the visible interval [rts, end] of this frame
float end = i+1 < images->num_frames ? images->timestamps[i+1] : INFINITY;
if (rts > images->vsync_duration || end < 0.0) {
PL_TRACE(rr, " -> Skipping: no intersection with vsync");
continue;
} else {
rts = PL_MAX(rts, 0.0);
end = PL_MIN(end, images->vsync_duration);
pl_assert(end >= rts);
}
// Weight is the fraction of vsync interval that frame is visible
weight = (end - rts) / images->vsync_duration;
PL_TRACE(rr, " -> Frame [%f, %f] intersects [%f, %f] = weight %f",
rts, end, 0.0, images->vsync_duration, weight);
if (weight < mixer.kernel->params[0]) {
PL_TRACE(rr, " (culling due to threshold)");
weight = 0.0;
}
} else {
const float radius = pl_filter_radius_bound(&mixer);
if (fabsf(rts) >= radius) {
PL_TRACE(rr, " -> Skipping: outside filter radius (%f)", radius);
continue;
}
// Weight is directly sampled from the filter
weight = pl_filter_sample(&mixer, rts);
PL_TRACE(rr, " -> Filter offset %f = weight %f", rts, weight);
}
struct cached_frame *f = NULL;
for (int j = 0; j < rr->frames.num; j++) {
if (rr->frames.elem[j].signature == sig) {
f = &rr->frames.elem[j];
f->evict = false;
break;
}
}
// Skip frames with negligible contributions. Do this after the loop
// above to make sure these frames don't get evicted just yet, and
// also exclude the reference image from this optimization to ensure
// that we always have at least one frame.
const float cutoff = 1e-3;
if (fabsf(weight) <= cutoff && img != refimg) {
PL_TRACE(rr, " -> Skipping: weight (%f) below threshold (%f)",
weight, cutoff);
continue;
}
bool skip_cache = single_frame && (params->skip_caching_single_frame || par_info.trivial);
if (!f && skip_cache) {
PL_TRACE(rr, "Single frame not found in cache, bypassing");
goto fallback;
}
if (!f) {
// Signature does not exist in the cache at all yet,
// so grow the cache by this entry.
PL_ARRAY_GROW(rr, rr->frames);
f = &rr->frames.elem[rr->frames.num++];
*f = (struct cached_frame) {
.signature = sig,
};
}
// Check to see if we can blindly reuse this cache entry. This is the
// case if either the params are compatible, or the user doesn't care
bool can_reuse = f->tex;
bool strict_reuse = skip_cache || single_frame ||
!params->preserve_mixing_cache;
if (can_reuse && strict_reuse) {
can_reuse = f->tex->params.w == out_w &&
f->tex->params.h == out_h &&
pl_rect2d_eq(f->crop, img->crop) &&
f->params_hash == par_info.hash &&
pl_color_space_equal(&f->color, &target->color) &&
pl_icc_profile_equal(&f->profile, &target->profile);
}
if (!can_reuse && skip_cache) {
PL_TRACE(rr, "Single frame cache entry invalid, bypassing");
goto fallback;
}
if (!can_reuse) {
// If we can't reuse the entry, we need to re-render this frame
PL_TRACE(rr, " -> Cached texture missing or invalid.. (re)creating");
if (!f->tex) {
if (PL_ARRAY_POP(rr->frame_fbos, &f->tex))
pl_tex_invalidate(rr->gpu, f->tex);
}
bool ok = pl_tex_recreate(rr->gpu, &f->tex, pl_tex_params(
.w = out_w,
.h = out_h,
.format = pass.fbofmt[4],
.sampleable = true,
.renderable = true,
.blit_dst = pass.fbofmt[4]->caps & PL_FMT_CAP_BLITTABLE,
.storable = pass.fbofmt[4]->caps & PL_FMT_CAP_STORABLE,
));
if (!ok) {
PL_ERR(rr, "Could not create intermediate texture for "
"frame mixing.. disabling!");
rr->errors |= PL_RENDER_ERR_FRAME_MIXING;
goto fallback;
}
struct pass_state inter_pass = {
.rr = rr,
.params = pass.params,
.image = *img,
.target = *ptarget,
.info.stage = PL_RENDER_STAGE_FRAME,
.acquired = pass.acquired,
};
// Render a single frame up to `pass_output_target`
memcpy(inter_pass.fbofmt, pass.fbofmt, sizeof(pass.fbofmt));
if (!pass_init(&inter_pass, true))
goto fail;
pass_begin_frame(&inter_pass);
if (!(ok = pass_read_image(&inter_pass)))
goto inter_pass_error;
if (!(ok = pass_scale_main(&inter_pass)))
goto inter_pass_error;
pass_convert_colors(&inter_pass);
pl_assert(inter_pass.img.sh); // guaranteed by `pass_convert_colors`
pl_shader_set_alpha(inter_pass.img.sh, &inter_pass.img.repr,
PL_ALPHA_PREMULTIPLIED); // for frame mixing
pl_assert(inter_pass.img.w == out_w &&
inter_pass.img.h == out_h);
ok = pl_dispatch_finish(rr->dp, pl_dispatch_params(
.shader = &inter_pass.img.sh,
.target = f->tex,
));
if (!ok)
goto inter_pass_error;
float sx = out_w / pl_rect_w(inter_pass.dst_rect),
sy = out_h / pl_rect_h(inter_pass.dst_rect);
pl_transform2x2 shift = {
.mat.m = {{ sx, 0, }, { 0, sy, }},
.c = {
-sx * inter_pass.dst_rect.x0,
-sy * inter_pass.dst_rect.y0
},
};
if (inter_pass.rotation % PL_ROTATION_180 == PL_ROTATION_90) {
PL_SWAP(shift.mat.m[0][0], shift.mat.m[0][1]);
PL_SWAP(shift.mat.m[1][0], shift.mat.m[1][1]);
}
draw_overlays(&inter_pass, f->tex, inter_pass.img.comps, NULL,
inter_pass.image.overlays,
inter_pass.image.num_overlays,
inter_pass.img.color,
inter_pass.img.repr,
&shift);
f->params_hash = par_info.hash;
f->crop = img->crop;
f->color = inter_pass.img.color;
f->comps = inter_pass.img.comps;
f->profile = target->profile;
// fall through
inter_pass_error:
inter_pass.acquired.target = false; // don't release target
pass_uninit(&inter_pass);
if (!ok)
goto fail;
}
pl_assert(fidx < MAX_MIX_FRAMES);
frames[fidx] = *f;
weights[fidx] = weight;
wsum += weight;
fidx++;
}
// Evict the frames we *don't* need
for (int i = 0; i < rr->frames.num; ) {
if (rr->frames.elem[i].evict) {
PL_TRACE(rr, "Evicting frame with signature %llx from cache",
(unsigned long long) rr->frames.elem[i].signature);
PL_ARRAY_APPEND(rr, rr->frame_fbos, rr->frames.elem[i].tex);
PL_ARRAY_REMOVE_AT(rr->frames, i);
continue;
} else {
i++;
}
}
// If we got back no frames, retry with ZOH semantics
if (!fidx) {
pl_assert(!single_frame);
single_frame = true;
goto retry;
}
// Sample and mix the output color
pass_begin_frame(&pass);
pass.info.count = fidx;
pl_assert(fidx > 0);
pl_shader sh = pl_dispatch_begin(rr->dp);
sh_describef(sh, "frame mixing (%d frame%s)", fidx, fidx > 1 ? "s" : "");
sh->output = PL_SHADER_SIG_COLOR;
sh->output_w = out_w;
sh->output_h = out_h;
GLSL("vec4 color; \n"
"// pl_render_image_mix \n"
"{ \n"
"vec4 mix_color = vec4(0.0); \n");
int comps = 0;
for (int i = 0; i < fidx; i++) {
const struct pl_tex_params *tpars = &frames[i].tex->params;
// Use linear sampling if desired and possible
enum pl_tex_sample_mode sample_mode = PL_TEX_SAMPLE_NEAREST;
if ((tpars->w != out_w || tpars->h != out_h) &&
(tpars->format->caps & PL_FMT_CAP_LINEAR))
{
sample_mode = PL_TEX_SAMPLE_LINEAR;
}
ident_t pos, tex = sh_bind(sh, frames[i].tex, PL_TEX_ADDRESS_CLAMP,
sample_mode, "frame", NULL, &pos, NULL);
GLSL("color = textureLod("$", "$", 0.0); \n", tex, pos);
// Note: This ignores differences in ICC profile, which we decide to
// just simply not care about. Doing that properly would require
// converting between different image profiles, and the headache of
// finagling that state is just not worth it because this is an
// exceptionally unlikely hypothetical.
//
// This also ignores differences in HDR metadata, which we deliberately
// ignore because it causes aggressive shader recompilation.
struct pl_color_space frame_csp = frames[i].color;
struct pl_color_space mix_csp = target->color;
frame_csp.hdr = mix_csp.hdr = (struct pl_hdr_metadata) {0};
pl_shader_color_map_ex(sh, NULL, pl_color_map_args(frame_csp, mix_csp));
float weight = weights[i] / wsum;
GLSL("mix_color += vec4("$") * color; \n", SH_FLOAT_DYN(weight));
comps = PL_MAX(comps, frames[i].comps);
}
GLSL("color = mix_color; \n"
"} \n");
// Dispatch this to the destination
pass.img = (struct img) {
.sh = sh,
.w = out_w,
.h = out_h,
.comps = comps,
.color = target->color,
.repr = {
.sys = PL_COLOR_SYSTEM_RGB,
.levels = PL_COLOR_LEVELS_PC,
.alpha = comps >= 4 ? PL_ALPHA_PREMULTIPLIED : PL_ALPHA_UNKNOWN,
},
};
if (!pass_output_target(&pass))
goto fallback;
pass_uninit(&pass);
return true;
fail:
PL_ERR(rr, "Could not render image for frame mixing.. disabling!");
rr->errors |= PL_RENDER_ERR_FRAME_MIXING;
// fall through
fallback:
pass_uninit(&pass);
return pl_render_image(rr, refimg, ptarget, params);
error: // for parameter validation failures
return false;
}
void pl_frames_infer_mix(pl_renderer rr, const struct pl_frame_mix *mix,
struct pl_frame *target, struct pl_frame *out_ref)
{
struct pass_state pass = {
.rr = rr,
.target = *target,
};
const struct pl_frame *refimg = pl_frame_mix_nearest(mix);
if (refimg) {
pass.image = *refimg;
} else {
pass.src_ref = -1;
}
pass_fix_frames(&pass);
*target = pass.target;
if (out_ref)
*out_ref = pass.image;
}
void pl_frame_set_chroma_location(struct pl_frame *frame,
enum pl_chroma_location chroma_loc)
{
pl_tex ref = frame->planes[frame_ref(frame)].texture;
if (ref) {
// Texture dimensions are already known, so apply the chroma location
// only to subsampled planes
int ref_w = ref->params.w, ref_h = ref->params.h;
for (int i = 0; i < frame->num_planes; i++) {
struct pl_plane *plane = &frame->planes[i];
pl_tex tex = plane->texture;
bool subsampled = tex->params.w < ref_w || tex->params.h < ref_h;
if (subsampled)
pl_chroma_location_offset(chroma_loc, &plane->shift_x, &plane->shift_y);
}
} else {
// Texture dimensions are not yet known, so apply the chroma location
// to all chroma planes, regardless of subsampling
for (int i = 0; i < frame->num_planes; i++) {
struct pl_plane *plane = &frame->planes[i];
if (detect_plane_type(plane, &frame->repr) == PLANE_CHROMA)
pl_chroma_location_offset(chroma_loc, &plane->shift_x, &plane->shift_y);
}
}
}
void pl_frame_from_swapchain(struct pl_frame *out_frame,
const struct pl_swapchain_frame *frame)
{
pl_tex fbo = frame->fbo;
int num_comps = fbo->params.format->num_components;
if (!frame->color_repr.alpha)
num_comps = PL_MIN(num_comps, 3);
*out_frame = (struct pl_frame) {
.num_planes = 1,
.planes = {{
.texture = fbo,
.flipped = frame->flipped,
.components = num_comps,
.component_mapping = {0, 1, 2, 3},
}},
.crop = { 0, 0, fbo->params.w, fbo->params.h },
.repr = frame->color_repr,
.color = frame->color_space,
};
}
bool pl_frame_is_cropped(const struct pl_frame *frame)
{
int x0 = roundf(PL_MIN(frame->crop.x0, frame->crop.x1)),
y0 = roundf(PL_MIN(frame->crop.y0, frame->crop.y1)),
x1 = roundf(PL_MAX(frame->crop.x0, frame->crop.x1)),
y1 = roundf(PL_MAX(frame->crop.y0, frame->crop.y1));
pl_tex ref = frame->planes[frame_ref(frame)].texture;
pl_assert(ref);
if (!x0 && !x1)
x1 = ref->params.w;
if (!y0 && !y1)
y1 = ref->params.h;
return x0 > 0 || y0 > 0 || x1 < ref->params.w || y1 < ref->params.h;
}
void pl_frame_clear_rgba(pl_gpu gpu, const struct pl_frame *frame,
const float rgba[4])
{
struct pl_color_repr repr = frame->repr;
pl_transform3x3 tr = pl_color_repr_decode(&repr, NULL);
pl_transform3x3_invert(&tr);
float encoded[3] = { rgba[0], rgba[1], rgba[2] };
pl_transform3x3_apply(&tr, encoded);
float mult = frame->repr.alpha == PL_ALPHA_PREMULTIPLIED ? rgba[3] : 1.0;
for (int p = 0; p < frame->num_planes; p++) {
const struct pl_plane *plane = &frame->planes[p];
float clear[4] = { 0.0, 0.0, 0.0, rgba[3] };
for (int c = 0; c < plane->components; c++) {
int ch = plane->component_mapping[c];
if (ch >= 0 && ch < 3)
clear[c] = mult * encoded[plane->component_mapping[c]];
}
pl_tex_clear(gpu, plane->texture, clear);
}
}
struct pl_render_errors pl_renderer_get_errors(pl_renderer rr)
{
return (struct pl_render_errors) {
.errors = rr->errors,
.disabled_hooks = rr->disabled_hooks.elem,
.num_disabled_hooks = rr->disabled_hooks.num,
};
}
void pl_renderer_reset_errors(pl_renderer rr,
const struct pl_render_errors *errors)
{
if (!errors) {
// Reset everything
rr->errors = PL_RENDER_ERR_NONE;
rr->disabled_hooks.num = 0;
return;
}
// Reset only requested errors
rr->errors &= ~errors->errors;
// Not clearing hooks
if (!(errors->errors & PL_RENDER_ERR_HOOKS))
goto done;
// Remove all hook signatures
if (!errors->num_disabled_hooks) {
rr->disabled_hooks.num = 0;
goto done;
}
// At this point we require valid array of hooks
if (!errors->disabled_hooks) {
assert(errors->disabled_hooks);
goto done;
}
for (int i = 0; i < errors->num_disabled_hooks; i++) {
for (int j = 0; j < rr->disabled_hooks.num; j++) {
// Remove only requested hook signatures
if (rr->disabled_hooks.elem[j] == errors->disabled_hooks[i]) {
PL_ARRAY_REMOVE_AT(rr->disabled_hooks, j);
break;
}
}
}
done:
if (rr->disabled_hooks.num)
rr->errors |= PL_RENDER_ERR_HOOKS;
return;
}