mpv/video/out/vo_gpu_next.c

2134 lines
70 KiB
C

/*
* Copyright (C) 2021 Niklas Haas
*
* This file is part of mpv.
*
* mpv 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.
*
* mpv 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 mpv. If not, see <http://www.gnu.org/licenses/>.
*/
#include <unistd.h>
#include <libplacebo/colorspace.h>
#include <libplacebo/options.h>
#include <libplacebo/renderer.h>
#include <libplacebo/shaders/lut.h>
#include <libplacebo/shaders/icc.h>
#include <libplacebo/utils/libav.h>
#include <libplacebo/utils/frame_queue.h>
#include "config.h"
#include "common/common.h"
#include "options/m_config.h"
#include "options/path.h"
#include "osdep/io.h"
#include "osdep/threads.h"
#include "stream/stream.h"
#include "video/fmt-conversion.h"
#include "video/mp_image.h"
#include "video/out/placebo/ra_pl.h"
#include "placebo/utils.h"
#include "gpu/context.h"
#include "gpu/hwdec.h"
#include "gpu/video.h"
#include "gpu/video_shaders.h"
#include "sub/osd.h"
#include "gpu_next/context.h"
#if HAVE_GL && defined(PL_HAVE_OPENGL)
#include <libplacebo/opengl.h>
#include "video/out/opengl/ra_gl.h"
#endif
#if HAVE_D3D11 && defined(PL_HAVE_D3D11)
#include <libplacebo/d3d11.h>
#include "video/out/d3d11/ra_d3d11.h"
#include "osdep/windows_utils.h"
#endif
struct osd_entry {
pl_tex tex;
struct pl_overlay_part *parts;
int num_parts;
};
struct osd_state {
struct osd_entry entries[MAX_OSD_PARTS];
struct pl_overlay overlays[MAX_OSD_PARTS];
};
struct scaler_params {
struct pl_filter_config config;
};
struct user_hook {
char *path;
const struct pl_hook *hook;
};
struct user_lut {
char *opt;
char *path;
int type;
struct pl_custom_lut *lut;
};
struct frame_info {
int count;
struct pl_dispatch_info info[VO_PASS_PERF_MAX];
};
struct priv {
struct mp_log *log;
struct mpv_global *global;
struct ra_ctx *ra_ctx;
struct gpu_ctx *context;
struct ra_hwdec_ctx hwdec_ctx;
struct ra_hwdec_mapper *hwdec_mapper;
// Allocated DR buffers
mp_mutex dr_lock;
pl_buf *dr_buffers;
int num_dr_buffers;
pl_log pllog;
pl_gpu gpu;
pl_renderer rr;
pl_queue queue;
pl_swapchain sw;
pl_fmt osd_fmt[SUBBITMAP_COUNT];
pl_tex *sub_tex;
int num_sub_tex;
pl_cache shader_cache;
pl_cache icc_cache;
struct mp_rect src, dst;
struct mp_osd_res osd_res;
struct osd_state osd_state;
uint64_t last_id;
uint64_t osd_sync;
double last_pts;
bool is_interpolated;
bool want_reset;
bool frame_pending;
bool redraw;
pl_options pars;
struct m_config_cache *opts_cache;
struct mp_csp_equalizer_state *video_eq;
struct scaler_params scalers[SCALER_COUNT];
const struct pl_hook **hooks; // storage for `params.hooks`
enum mp_csp_levels output_levels;
char **raw_opts;
struct pl_icc_params icc_params;
char *icc_path;
pl_icc_object icc_profile;
struct user_lut image_lut;
struct user_lut target_lut;
struct user_lut lut;
// Cached shaders, preserved across options updates
struct user_hook *user_hooks;
int num_user_hooks;
// Performance data of last frame
struct frame_info perf_fresh;
struct frame_info perf_redraw;
bool delayed_peak;
bool inter_preserve;
bool target_hint;
float corner_rounding;
};
static void update_render_options(struct vo *vo);
static void update_lut(struct priv *p, struct user_lut *lut);
static pl_buf get_dr_buf(struct priv *p, const uint8_t *ptr)
{
mp_mutex_lock(&p->dr_lock);
for (int i = 0; i < p->num_dr_buffers; i++) {
pl_buf buf = p->dr_buffers[i];
if (ptr >= buf->data && ptr < buf->data + buf->params.size) {
mp_mutex_unlock(&p->dr_lock);
return buf;
}
}
mp_mutex_unlock(&p->dr_lock);
return NULL;
}
static void free_dr_buf(void *opaque, uint8_t *data)
{
struct priv *p = opaque;
mp_mutex_lock(&p->dr_lock);
for (int i = 0; i < p->num_dr_buffers; i++) {
if (p->dr_buffers[i]->data == data) {
pl_buf_destroy(p->gpu, &p->dr_buffers[i]);
MP_TARRAY_REMOVE_AT(p->dr_buffers, p->num_dr_buffers, i);
mp_mutex_unlock(&p->dr_lock);
return;
}
}
MP_ASSERT_UNREACHABLE();
}
static struct mp_image *get_image(struct vo *vo, int imgfmt, int w, int h,
int stride_align, int flags)
{
struct priv *p = vo->priv;
pl_gpu gpu = p->gpu;
if (!gpu->limits.thread_safe || !gpu->limits.max_mapped_size)
return NULL;
if ((flags & VO_DR_FLAG_HOST_CACHED) && !gpu->limits.host_cached)
return NULL;
stride_align = mp_lcm(stride_align, gpu->limits.align_tex_xfer_pitch);
stride_align = mp_lcm(stride_align, gpu->limits.align_tex_xfer_offset);
int size = mp_image_get_alloc_size(imgfmt, w, h, stride_align);
if (size < 0)
return NULL;
pl_buf buf = pl_buf_create(gpu, &(struct pl_buf_params) {
.memory_type = PL_BUF_MEM_HOST,
.host_mapped = true,
.size = size + stride_align,
});
if (!buf)
return NULL;
struct mp_image *mpi = mp_image_from_buffer(imgfmt, w, h, stride_align,
buf->data, buf->params.size,
p, free_dr_buf);
if (!mpi) {
pl_buf_destroy(gpu, &buf);
return NULL;
}
mp_mutex_lock(&p->dr_lock);
MP_TARRAY_APPEND(p, p->dr_buffers, p->num_dr_buffers, buf);
mp_mutex_unlock(&p->dr_lock);
return mpi;
}
static struct pl_color_space get_mpi_csp(struct vo *vo, struct mp_image *mpi);
static void update_overlays(struct vo *vo, struct mp_osd_res res,
int flags, enum pl_overlay_coords coords,
struct osd_state *state, struct pl_frame *frame,
struct mp_image *src)
{
struct priv *p = vo->priv;
static const bool subfmt_all[SUBBITMAP_COUNT] = {
[SUBBITMAP_LIBASS] = true,
[SUBBITMAP_BGRA] = true,
};
double pts = src ? src->pts : 0;
struct sub_bitmap_list *subs = osd_render(vo->osd, res, pts, flags, subfmt_all);
frame->overlays = state->overlays;
frame->num_overlays = 0;
for (int n = 0; n < subs->num_items; n++) {
const struct sub_bitmaps *item = subs->items[n];
if (!item->num_parts || !item->packed)
continue;
struct osd_entry *entry = &state->entries[item->render_index];
pl_fmt tex_fmt = p->osd_fmt[item->format];
if (!entry->tex)
MP_TARRAY_POP(p->sub_tex, p->num_sub_tex, &entry->tex);
bool ok = pl_tex_recreate(p->gpu, &entry->tex, &(struct pl_tex_params) {
.format = tex_fmt,
.w = MPMAX(item->packed_w, entry->tex ? entry->tex->params.w : 0),
.h = MPMAX(item->packed_h, entry->tex ? entry->tex->params.h : 0),
.host_writable = true,
.sampleable = true,
});
if (!ok) {
MP_ERR(vo, "Failed recreating OSD texture!\n");
break;
}
ok = pl_tex_upload(p->gpu, &(struct pl_tex_transfer_params) {
.tex = entry->tex,
.rc = { .x1 = item->packed_w, .y1 = item->packed_h, },
.row_pitch = item->packed->stride[0],
.ptr = item->packed->planes[0],
});
if (!ok) {
MP_ERR(vo, "Failed uploading OSD texture!\n");
break;
}
entry->num_parts = 0;
for (int i = 0; i < item->num_parts; i++) {
const struct sub_bitmap *b = &item->parts[i];
uint32_t c = b->libass.color;
struct pl_overlay_part part = {
.src = { b->src_x, b->src_y, b->src_x + b->w, b->src_y + b->h },
.dst = { b->x, b->y, b->x + b->dw, b->y + b->dh },
.color = {
(c >> 24) / 255.0,
((c >> 16) & 0xFF) / 255.0,
((c >> 8) & 0xFF) / 255.0,
1.0 - (c & 0xFF) / 255.0,
}
};
MP_TARRAY_APPEND(p, entry->parts, entry->num_parts, part);
}
struct pl_overlay *ol = &state->overlays[frame->num_overlays++];
*ol = (struct pl_overlay) {
.tex = entry->tex,
.parts = entry->parts,
.num_parts = entry->num_parts,
.color = {
.primaries = PL_COLOR_PRIM_BT_709,
.transfer = PL_COLOR_TRC_SRGB,
},
.coords = coords,
};
switch (item->format) {
case SUBBITMAP_BGRA:
ol->mode = PL_OVERLAY_NORMAL;
ol->repr.alpha = PL_ALPHA_PREMULTIPLIED;
// Infer bitmap colorspace from source
if (src) {
ol->color = get_mpi_csp(vo, src);
// Seems like HDR subtitles are targeting SDR white
if (pl_color_transfer_is_hdr(ol->color.transfer)) {
ol->color.hdr = (struct pl_hdr_metadata) {
.max_luma = PL_COLOR_SDR_WHITE,
};
}
}
break;
case SUBBITMAP_LIBASS:
ol->mode = PL_OVERLAY_MONOCHROME;
ol->repr.alpha = PL_ALPHA_INDEPENDENT;
break;
}
}
talloc_free(subs);
}
struct frame_priv {
struct vo *vo;
struct osd_state subs;
uint64_t osd_sync;
struct ra_hwdec *hwdec;
};
static int plane_data_from_imgfmt(struct pl_plane_data out_data[4],
struct pl_bit_encoding *out_bits,
enum mp_imgfmt imgfmt)
{
struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(imgfmt);
if (!desc.num_planes || !(desc.flags & MP_IMGFLAG_HAS_COMPS))
return 0;
if (desc.flags & MP_IMGFLAG_HWACCEL)
return 0; // HW-accelerated frames need to be mapped differently
if (!(desc.flags & MP_IMGFLAG_NE))
return 0; // GPU endianness follows the host's
if (desc.flags & MP_IMGFLAG_PAL)
return 0; // Palette formats (currently) not supported in libplacebo
if ((desc.flags & MP_IMGFLAG_TYPE_FLOAT) && (desc.flags & MP_IMGFLAG_YUV))
return 0; // Floating-point YUV (currently) unsupported
bool has_bits = false;
bool any_padded = false;
for (int p = 0; p < desc.num_planes; p++) {
struct pl_plane_data *data = &out_data[p];
struct mp_imgfmt_comp_desc sorted[MP_NUM_COMPONENTS];
int num_comps = 0;
if (desc.bpp[p] % 8)
return 0; // Pixel size is not byte-aligned
for (int c = 0; c < mp_imgfmt_desc_get_num_comps(&desc); c++) {
if (desc.comps[c].plane != p)
continue;
data->component_map[num_comps] = c;
sorted[num_comps] = desc.comps[c];
num_comps++;
// Sort components by offset order, while keeping track of the
// semantic mapping in `data->component_map`
for (int i = num_comps - 1; i > 0; i--) {
if (sorted[i].offset >= sorted[i - 1].offset)
break;
MPSWAP(struct mp_imgfmt_comp_desc, sorted[i], sorted[i - 1]);
MPSWAP(int, data->component_map[i], data->component_map[i - 1]);
}
}
uint64_t total_bits = 0;
// Fill in the pl_plane_data fields for each component
memset(data->component_size, 0, sizeof(data->component_size));
for (int c = 0; c < num_comps; c++) {
data->component_size[c] = sorted[c].size;
data->component_pad[c] = sorted[c].offset - total_bits;
total_bits += data->component_pad[c] + data->component_size[c];
any_padded |= sorted[c].pad;
// Ignore bit encoding of alpha channel
if (!out_bits || data->component_map[c] == PL_CHANNEL_A)
continue;
struct pl_bit_encoding bits = {
.sample_depth = data->component_size[c],
.color_depth = sorted[c].size - abs(sorted[c].pad),
.bit_shift = MPMAX(sorted[c].pad, 0),
};
if (!has_bits) {
*out_bits = bits;
has_bits = true;
} else {
if (!pl_bit_encoding_equal(out_bits, &bits)) {
// Bit encoding differs between components/planes,
// cannot handle this
*out_bits = (struct pl_bit_encoding) {0};
out_bits = NULL;
}
}
}
data->pixel_stride = desc.bpp[p] / 8;
data->type = (desc.flags & MP_IMGFLAG_TYPE_FLOAT)
? PL_FMT_FLOAT
: PL_FMT_UNORM;
}
if (any_padded && !out_bits)
return 0; // can't handle padded components without `pl_bit_encoding`
return desc.num_planes;
}
static struct pl_color_space get_mpi_csp(struct vo *vo, struct mp_image *mpi)
{
struct pl_color_space csp = {
.primaries = mp_prim_to_pl(mpi->params.color.primaries),
.transfer = mp_trc_to_pl(mpi->params.color.gamma),
.hdr = mpi->params.color.hdr,
};
return csp;
}
static bool hwdec_reconfig(struct priv *p, struct ra_hwdec *hwdec,
const struct mp_image_params *par)
{
if (p->hwdec_mapper) {
if (mp_image_params_equal(par, &p->hwdec_mapper->src_params)) {
return p->hwdec_mapper;
} else {
ra_hwdec_mapper_free(&p->hwdec_mapper);
}
}
p->hwdec_mapper = ra_hwdec_mapper_create(hwdec, par);
if (!p->hwdec_mapper) {
MP_ERR(p, "Initializing texture for hardware decoding failed.\n");
return NULL;
}
return p->hwdec_mapper;
}
// For RAs not based on ra_pl, this creates a new pl_tex wrapper
static pl_tex hwdec_get_tex(struct priv *p, int n)
{
struct ra_tex *ratex = p->hwdec_mapper->tex[n];
struct ra *ra = p->hwdec_mapper->ra;
if (ra_pl_get(ra))
return (pl_tex) ratex->priv;
#if HAVE_GL && defined(PL_HAVE_OPENGL)
if (ra_is_gl(ra) && pl_opengl_get(p->gpu)) {
struct pl_opengl_wrap_params par = {
.width = ratex->params.w,
.height = ratex->params.h,
};
ra_gl_get_format(ratex->params.format, &par.iformat,
&(GLenum){0}, &(GLenum){0});
ra_gl_get_raw_tex(ra, ratex, &par.texture, &par.target);
return pl_opengl_wrap(p->gpu, &par);
}
#endif
#if HAVE_D3D11 && defined(PL_HAVE_D3D11)
if (ra_is_d3d11(ra)) {
int array_slice = 0;
ID3D11Resource *res = ra_d3d11_get_raw_tex(ra, ratex, &array_slice);
pl_tex tex = pl_d3d11_wrap(p->gpu, pl_d3d11_wrap_params(
.tex = res,
.array_slice = array_slice,
.fmt = ra_d3d11_get_format(ratex->params.format),
.w = ratex->params.w,
.h = ratex->params.h,
));
SAFE_RELEASE(res);
return tex;
}
#endif
MP_ERR(p, "Failed mapping hwdec frame? Open a bug!\n");
return false;
}
static bool hwdec_acquire(pl_gpu gpu, struct pl_frame *frame)
{
struct mp_image *mpi = frame->user_data;
struct frame_priv *fp = mpi->priv;
struct priv *p = fp->vo->priv;
if (!hwdec_reconfig(p, fp->hwdec, &mpi->params))
return false;
if (ra_hwdec_mapper_map(p->hwdec_mapper, mpi) < 0) {
MP_ERR(p, "Mapping hardware decoded surface failed.\n");
return false;
}
for (int n = 0; n < frame->num_planes; n++) {
if (!(frame->planes[n].texture = hwdec_get_tex(p, n)))
return false;
}
return true;
}
static void hwdec_release(pl_gpu gpu, struct pl_frame *frame)
{
struct mp_image *mpi = frame->user_data;
struct frame_priv *fp = mpi->priv;
struct priv *p = fp->vo->priv;
if (!ra_pl_get(p->hwdec_mapper->ra)) {
for (int n = 0; n < frame->num_planes; n++)
pl_tex_destroy(p->gpu, &frame->planes[n].texture);
}
ra_hwdec_mapper_unmap(p->hwdec_mapper);
}
static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src,
struct pl_frame *frame)
{
struct mp_image *mpi = src->frame_data;
const struct mp_image_params *par = &mpi->params;
struct frame_priv *fp = mpi->priv;
struct vo *vo = fp->vo;
struct priv *p = vo->priv;
fp->hwdec = ra_hwdec_get(&p->hwdec_ctx, mpi->imgfmt);
if (fp->hwdec) {
// Note: We don't actually need the mapper to map the frame yet, we
// only reconfig the mapper here (potentially creating it) to access
// `dst_params`. In practice, though, this should not matter unless the
// image format changes mid-stream.
if (!hwdec_reconfig(p, fp->hwdec, &mpi->params)) {
talloc_free(mpi);
return false;
}
par = &p->hwdec_mapper->dst_params;
}
*frame = (struct pl_frame) {
.color = get_mpi_csp(vo, mpi),
.repr = {
.sys = mp_csp_to_pl(par->color.space),
.levels = mp_levels_to_pl(par->color.levels),
.alpha = mp_alpha_to_pl(par->alpha),
},
.profile = {
.data = mpi->icc_profile ? mpi->icc_profile->data : NULL,
.len = mpi->icc_profile ? mpi->icc_profile->size : 0,
},
.rotation = par->rotate / 90,
.user_data = mpi,
};
// mp_image, like AVFrame, likes communicating RGB/XYZ/YCbCr status
// implicitly via the image format, rather than the actual tagging.
switch (mp_imgfmt_get_forced_csp(par->imgfmt)) {
case MP_CSP_RGB:
frame->repr.sys = PL_COLOR_SYSTEM_RGB;
frame->repr.levels = PL_COLOR_LEVELS_FULL;
break;
case MP_CSP_XYZ:
frame->repr.sys = PL_COLOR_SYSTEM_XYZ;
break;
case MP_CSP_AUTO:
if (!frame->repr.sys)
frame->repr.sys = pl_color_system_guess_ycbcr(par->w, par->h);
break;
default: break;
}
if (fp->hwdec) {
struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(par->imgfmt);
frame->acquire = hwdec_acquire;
frame->release = hwdec_release;
frame->num_planes = desc.num_planes;
for (int n = 0; n < frame->num_planes; n++) {
struct pl_plane *plane = &frame->planes[n];
int *map = plane->component_mapping;
for (int c = 0; c < mp_imgfmt_desc_get_num_comps(&desc); c++) {
if (desc.comps[c].plane != n)
continue;
// Sort by component offset
uint8_t offset = desc.comps[c].offset;
int index = plane->components++;
while (index > 0 && desc.comps[map[index - 1]].offset > offset) {
map[index] = map[index - 1];
index--;
}
map[index] = c;
}
}
} else { // swdec
struct pl_plane_data data[4] = {0};
frame->num_planes = plane_data_from_imgfmt(data, &frame->repr.bits, mpi->imgfmt);
for (int n = 0; n < frame->num_planes; n++) {
struct pl_plane *plane = &frame->planes[n];
data[n].width = mp_image_plane_w(mpi, n);
data[n].height = mp_image_plane_h(mpi, n);
if (mpi->stride[n] < 0) {
data[n].pixels = mpi->planes[n] + (data[n].height - 1) * mpi->stride[n];
data[n].row_stride = -mpi->stride[n];
plane->flipped = true;
} else {
data[n].pixels = mpi->planes[n];
data[n].row_stride = mpi->stride[n];
}
pl_buf buf = get_dr_buf(p, data[n].pixels);
if (buf) {
data[n].buf = buf;
data[n].buf_offset = (uint8_t *) data[n].pixels - buf->data;
data[n].pixels = NULL;
} else if (gpu->limits.callbacks) {
data[n].callback = talloc_free;
data[n].priv = mp_image_new_ref(mpi);
}
if (!pl_upload_plane(gpu, plane, &tex[n], &data[n])) {
MP_ERR(vo, "Failed uploading frame!\n");
talloc_free(data[n].priv);
talloc_free(mpi);
return false;
}
}
}
// Update chroma location, must be done after initializing planes
pl_frame_set_chroma_location(frame, mp_chroma_to_pl(par->chroma_location));
// Set the frame DOVI metadata
mp_map_dovi_metadata_to_pl(mpi, frame);
if (mpi->film_grain)
pl_film_grain_from_av(&frame->film_grain, (AVFilmGrainParams *) mpi->film_grain->data);
// Compute a unique signature for any attached ICC profile. Wasteful in
// theory if the ICC profile is the same for multiple frames, but in
// practice ICC profiles are overwhelmingly going to be attached to
// still images so it shouldn't matter.
pl_icc_profile_compute_signature(&frame->profile);
// Update LUT attached to this frame
update_lut(p, &p->image_lut);
frame->lut = p->image_lut.lut;
frame->lut_type = p->image_lut.type;
return true;
}
static void unmap_frame(pl_gpu gpu, struct pl_frame *frame,
const struct pl_source_frame *src)
{
struct mp_image *mpi = src->frame_data;
struct frame_priv *fp = mpi->priv;
struct priv *p = fp->vo->priv;
for (int i = 0; i < MP_ARRAY_SIZE(fp->subs.entries); i++) {
pl_tex tex = fp->subs.entries[i].tex;
if (tex)
MP_TARRAY_APPEND(p, p->sub_tex, p->num_sub_tex, tex);
}
talloc_free(mpi);
}
static void discard_frame(const struct pl_source_frame *src)
{
struct mp_image *mpi = src->frame_data;
talloc_free(mpi);
}
static void info_callback(void *priv, const struct pl_render_info *info)
{
struct vo *vo = priv;
struct priv *p = vo->priv;
if (info->index >= VO_PASS_PERF_MAX)
return; // silently ignore clipped passes, whatever
struct frame_info *frame;
switch (info->stage) {
case PL_RENDER_STAGE_FRAME: frame = &p->perf_fresh; break;
case PL_RENDER_STAGE_BLEND: frame = &p->perf_redraw; break;
default: abort();
}
frame->count = info->index + 1;
pl_dispatch_info_move(&frame->info[info->index], info->pass);
}
static void update_options(struct vo *vo)
{
struct priv *p = vo->priv;
pl_options pars = p->pars;
if (m_config_cache_update(p->opts_cache))
update_render_options(vo);
update_lut(p, &p->lut);
pars->params.lut = p->lut.lut;
pars->params.lut_type = p->lut.type;
// Update equalizer state
struct mp_csp_params cparams = MP_CSP_PARAMS_DEFAULTS;
mp_csp_equalizer_state_get(p->video_eq, &cparams);
pars->color_adjustment.brightness = cparams.brightness;
pars->color_adjustment.contrast = cparams.contrast;
pars->color_adjustment.hue = cparams.hue;
pars->color_adjustment.saturation = cparams.saturation;
pars->color_adjustment.gamma = cparams.gamma;
p->output_levels = cparams.levels_out;
for (char **kv = p->raw_opts; kv && kv[0]; kv += 2)
pl_options_set_str(pars, kv[0], kv[1]);
}
static void apply_target_contrast(struct priv *p, struct pl_color_space *color)
{
const struct gl_video_opts *opts = p->opts_cache->opts;
// Auto mode, leave as is
if (!opts->target_contrast)
return;
// Infinite contrast
if (opts->target_contrast == -1) {
color->hdr.min_luma = 1e-7;
return;
}
// Infer max_luma for current pl_color_space
pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
.color = color,
// with HDR10 meta to respect value if already set
.metadata = PL_HDR_METADATA_HDR10,
.scaling = PL_HDR_NITS,
.out_max = &color->hdr.max_luma
));
color->hdr.min_luma = color->hdr.max_luma / opts->target_contrast;
}
static void apply_target_options(struct priv *p, struct pl_frame *target)
{
update_lut(p, &p->target_lut);
target->lut = p->target_lut.lut;
target->lut_type = p->target_lut.type;
// Colorspace overrides
const struct gl_video_opts *opts = p->opts_cache->opts;
if (p->output_levels)
target->repr.levels = mp_levels_to_pl(p->output_levels);
if (opts->target_prim)
target->color.primaries = mp_prim_to_pl(opts->target_prim);
if (opts->target_trc)
target->color.transfer = mp_trc_to_pl(opts->target_trc);
// If swapchain returned a value use this, override is used in hint
if (opts->target_peak && !target->color.hdr.max_luma)
target->color.hdr.max_luma = opts->target_peak;
if (!target->color.hdr.min_luma)
apply_target_contrast(p, &target->color);
if (opts->target_gamut) {
// Ensure resulting gamut still fits inside container
const struct pl_raw_primaries *gamut, *container;
gamut = pl_raw_primaries_get(mp_prim_to_pl(opts->target_gamut));
container = pl_raw_primaries_get(target->color.primaries);
target->color.hdr.prim = pl_primaries_clip(gamut, container);
}
if (opts->dither_depth > 0) {
struct pl_bit_encoding *tbits = &target->repr.bits;
tbits->color_depth += opts->dither_depth - tbits->sample_depth;
tbits->sample_depth = opts->dither_depth;
}
if (opts->icc_opts->icc_use_luma) {
p->icc_params.max_luma = 0.0f;
} else {
pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
.color = &target->color,
.metadata = PL_HDR_METADATA_HDR10, // use only static HDR nits
.scaling = PL_HDR_NITS,
.out_max = &p->icc_params.max_luma,
));
}
pl_icc_update(p->pllog, &p->icc_profile, NULL, &p->icc_params);
target->icc = p->icc_profile;
}
static void apply_crop(struct pl_frame *frame, struct mp_rect crop,
int width, int height)
{
frame->crop = (struct pl_rect2df) {
.x0 = crop.x0,
.y0 = crop.y0,
.x1 = crop.x1,
.y1 = crop.y1,
};
// mpv gives us rotated/flipped rects, libplacebo expects unrotated
pl_rect2df_rotate(&frame->crop, -frame->rotation);
if (frame->crop.x1 < frame->crop.x0) {
frame->crop.x0 = width - frame->crop.x0;
frame->crop.x1 = width - frame->crop.x1;
}
if (frame->crop.y1 < frame->crop.y0) {
frame->crop.y0 = height - frame->crop.y0;
frame->crop.y1 = height - frame->crop.y1;
}
}
static void update_tm_viz(struct pl_color_map_params *params,
const struct pl_frame *target)
{
if (!params->visualize_lut)
return;
// Use right half of sceen for TM visualization, constrain to 1:1 AR
const float out_w = fabsf(pl_rect_w(target->crop));
const float out_h = fabsf(pl_rect_h(target->crop));
const float size = MPMIN(out_w / 2.0f, out_h);
params->visualize_rect = (pl_rect2df) {
.x0 = 1.0f - size / out_w,
.x1 = 1.0f,
.y0 = 0.0f,
.y1 = size / out_h,
};
// Visualize red-blue plane
params->visualize_hue = M_PI / 4.0;
}
static void draw_frame(struct vo *vo, struct vo_frame *frame)
{
struct priv *p = vo->priv;
pl_options pars = p->pars;
pl_gpu gpu = p->gpu;
update_options(vo);
struct pl_render_params params = pars->params;
const struct gl_video_opts *opts = p->opts_cache->opts;
bool will_redraw = frame->display_synced && frame->num_vsyncs > 1;
bool cache_frame = will_redraw || frame->still;
bool can_interpolate = opts->interpolation && frame->display_synced &&
!frame->still && frame->num_frames > 1;
double pts_offset = can_interpolate ? frame->ideal_frame_vsync : 0;
params.info_callback = info_callback;
params.info_priv = vo;
params.skip_caching_single_frame = !cache_frame;
params.preserve_mixing_cache = p->inter_preserve && !frame->still;
if (frame->still)
params.frame_mixer = NULL;
// pl_queue advances its internal virtual PTS and culls available frames
// based on this value and the VPS/FPS ratio. Requesting a non-monotonic PTS
// is an invalid use of pl_queue. Reset it if this happens in an attempt to
// recover as much as possible. Ideally, this should never occur, and if it
// does, it should be corrected. The ideal_frame_vsync may be negative if
// the last draw did not align perfectly with the vsync. In this case, we
// should have the previous frame available in pl_queue, or a reset is
// already requested. Clamp the check to 0, as we don't have the previous
// frame in vo_frame anyway.
struct pl_source_frame vpts;
if (frame->current && !p->want_reset) {
if (pl_queue_peek(p->queue, 0, &vpts) &&
frame->current->pts + MPMAX(0, pts_offset) < vpts.pts)
{
MP_VERBOSE(vo, "Forcing queue refill, PTS(%f + %f | %f) < VPTS(%f)\n",
frame->current->pts, pts_offset,
frame->ideal_frame_vsync_duration, vpts.pts);
p->want_reset = true;
}
}
// Push all incoming frames into the frame queue
for (int n = 0; n < frame->num_frames; n++) {
int id = frame->frame_id + n;
if (p->want_reset) {
pl_renderer_flush_cache(p->rr);
pl_queue_reset(p->queue);
p->last_pts = 0.0;
p->last_id = 0;
p->want_reset = false;
}
if (id <= p->last_id)
continue; // ignore already seen frames
struct mp_image *mpi = mp_image_new_ref(frame->frames[n]);
struct frame_priv *fp = talloc_zero(mpi, struct frame_priv);
mpi->priv = fp;
fp->vo = vo;
pl_queue_push(p->queue, &(struct pl_source_frame) {
.pts = mpi->pts,
.duration = can_interpolate ? frame->approx_duration : 0,
.frame_data = mpi,
.map = map_frame,
.unmap = unmap_frame,
.discard = discard_frame,
});
p->last_id = id;
}
if (p->target_hint && frame->current) {
struct pl_color_space hint = get_mpi_csp(vo, frame->current);
if (opts->target_prim)
hint.primaries = mp_prim_to_pl(opts->target_prim);
if (opts->target_trc)
hint.transfer = mp_trc_to_pl(opts->target_trc);
if (opts->target_peak)
hint.hdr.max_luma = opts->target_peak;
apply_target_contrast(p, &hint);
pl_swapchain_colorspace_hint(p->sw, &hint);
} else if (!p->target_hint) {
pl_swapchain_colorspace_hint(p->sw, NULL);
}
struct pl_swapchain_frame swframe;
struct ra_swapchain *sw = p->ra_ctx->swapchain;
bool should_draw = sw->fns->start_frame(sw, NULL); // for wayland logic
if (!should_draw || !pl_swapchain_start_frame(p->sw, &swframe)) {
if (frame->current) {
// Advance the queue state to the current PTS to discard unused frames
pl_queue_update(p->queue, NULL, pl_queue_params(
.pts = frame->current->pts + pts_offset,
.radius = pl_frame_mix_radius(&params),
.vsync_duration = can_interpolate ? frame->ideal_frame_vsync_duration : 0,
#if PL_API_VER >= 340
.drift_compensation = 0,
#endif
));
}
return;
}
bool valid = false;
p->is_interpolated = false;
// Calculate target
struct pl_frame target;
pl_frame_from_swapchain(&target, &swframe);
apply_target_options(p, &target);
update_overlays(vo, p->osd_res,
(frame->current && opts->blend_subs) ? OSD_DRAW_OSD_ONLY : 0,
PL_OVERLAY_COORDS_DST_FRAME, &p->osd_state, &target, frame->current);
apply_crop(&target, p->dst, swframe.fbo->params.w, swframe.fbo->params.h);
update_tm_viz(&pars->color_map_params, &target);
struct pl_frame_mix mix = {0};
if (frame->current) {
// Update queue state
struct pl_queue_params qparams = *pl_queue_params(
.pts = frame->current->pts + pts_offset,
.radius = pl_frame_mix_radius(&params),
.vsync_duration = can_interpolate ? frame->ideal_frame_vsync_duration : 0,
.interpolation_threshold = opts->interpolation_threshold,
#if PL_API_VER >= 340
.drift_compensation = 0,
#endif
);
// Depending on the vsync ratio, we may be up to half of the vsync
// duration before the current frame time. This works fine because
// pl_queue will have this frame, unless it's after a reset event. In
// this case, start from the first available frame.
struct pl_source_frame first;
if (pl_queue_peek(p->queue, 0, &first) && qparams.pts < first.pts) {
if (first.pts != frame->current->pts)
MP_VERBOSE(vo, "Current PTS(%f) != VPTS(%f)\n", frame->current->pts, first.pts);
MP_VERBOSE(vo, "Clamping first frame PTS from %f to %f\n", qparams.pts, first.pts);
qparams.pts = first.pts;
}
p->last_pts = qparams.pts;
switch (pl_queue_update(p->queue, &mix, &qparams)) {
case PL_QUEUE_ERR:
MP_ERR(vo, "Failed updating frames!\n");
goto done;
case PL_QUEUE_EOF:
abort(); // we never signal EOF
case PL_QUEUE_MORE:
// This is expected to happen semi-frequently near the start and
// end of a file, so only log it at high verbosity and move on.
MP_DBG(vo, "Render queue underrun.\n");
break;
case PL_QUEUE_OK:
break;
}
// Update source crop and overlays on all existing frames. We
// technically own the `pl_frame` struct so this is kosher. This could
// be partially avoided by instead flushing the queue on resizes, but
// doing it this way avoids unnecessarily re-uploading frames.
for (int i = 0; i < mix.num_frames; i++) {
struct pl_frame *image = (struct pl_frame *) mix.frames[i];
struct mp_image *mpi = image->user_data;
struct frame_priv *fp = mpi->priv;
apply_crop(image, p->src, vo->params->w, vo->params->h);
if (opts->blend_subs) {
if (frame->redraw || fp->osd_sync < p->osd_sync) {
float rx = pl_rect_w(p->dst) / pl_rect_w(image->crop);
float ry = pl_rect_h(p->dst) / pl_rect_h(image->crop);
struct mp_osd_res res = {
.w = pl_rect_w(p->dst),
.h = pl_rect_h(p->dst),
.ml = -image->crop.x0 * rx,
.mr = (image->crop.x1 - vo->params->w) * rx,
.mt = -image->crop.y0 * ry,
.mb = (image->crop.y1 - vo->params->h) * ry,
.display_par = 1.0,
};
// TODO: fix this doing pointless updates
if (frame->redraw)
p->osd_sync++;
update_overlays(vo, res, OSD_DRAW_SUB_ONLY,
PL_OVERLAY_COORDS_DST_CROP,
&fp->subs, image, mpi);
fp->osd_sync = p->osd_sync;
}
} else {
// Disable overlays when blend_subs is disabled
image->num_overlays = 0;
fp->osd_sync = 0;
}
// Update the frame signature to include the current OSD sync
// value, in order to disambiguate between identical frames with
// modified OSD. Shift the OSD sync value by a lot to avoid
// collisions with low signature values.
//
// This is safe to do because `pl_frame_mix.signature` lives in
// temporary memory that is only valid for this `pl_queue_update`.
((uint64_t *) mix.signatures)[i] ^= fp->osd_sync << 48;
}
}
// Render frame
if (!pl_render_image_mix(p->rr, &mix, &target, &params)) {
MP_ERR(vo, "Failed rendering frame!\n");
goto done;
}
const struct pl_frame *cur_frame = pl_frame_mix_nearest(&mix);
if (cur_frame && vo->params) {
vo->params->color.hdr = cur_frame->color.hdr;
// Augment metadata with peak detection max_pq_y / avg_pq_y
pl_renderer_get_hdr_metadata(p->rr, &vo->params->color.hdr);
}
p->is_interpolated = pts_offset != 0 && mix.num_frames > 1;
valid = true;
// fall through
done:
if (!valid) // clear with purple to indicate error
pl_tex_clear(gpu, swframe.fbo, (float[4]){ 0.5, 0.0, 1.0, 1.0 });
pl_gpu_flush(gpu);
p->frame_pending = true;
}
static void flip_page(struct vo *vo)
{
struct priv *p = vo->priv;
struct ra_swapchain *sw = p->ra_ctx->swapchain;
if (p->frame_pending) {
if (!pl_swapchain_submit_frame(p->sw))
MP_ERR(vo, "Failed presenting frame!\n");
p->frame_pending = false;
}
sw->fns->swap_buffers(sw);
}
static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
{
struct priv *p = vo->priv;
struct ra_swapchain *sw = p->ra_ctx->swapchain;
if (sw->fns->get_vsync)
sw->fns->get_vsync(sw, info);
}
static int query_format(struct vo *vo, int format)
{
struct priv *p = vo->priv;
if (ra_hwdec_get(&p->hwdec_ctx, format))
return true;
struct pl_bit_encoding bits;
struct pl_plane_data data[4] = {0};
int planes = plane_data_from_imgfmt(data, &bits, format);
if (!planes)
return false;
for (int i = 0; i < planes; i++) {
if (!pl_plane_find_fmt(p->gpu, NULL, &data[i]))
return false;
}
return true;
}
static void resize(struct vo *vo)
{
struct priv *p = vo->priv;
struct mp_rect src, dst;
struct mp_osd_res osd;
vo_get_src_dst_rects(vo, &src, &dst, &osd);
if (vo->dwidth && vo->dheight) {
gpu_ctx_resize(p->context, vo->dwidth, vo->dheight);
vo->want_redraw = true;
}
if (mp_rect_equals(&p->src, &src) &&
mp_rect_equals(&p->dst, &dst) &&
osd_res_equals(p->osd_res, osd))
return;
p->osd_sync++;
p->osd_res = osd;
p->src = src;
p->dst = dst;
}
static int reconfig(struct vo *vo, struct mp_image_params *params)
{
struct priv *p = vo->priv;
if (!p->ra_ctx->fns->reconfig(p->ra_ctx))
return -1;
resize(vo);
return 0;
}
// Takes over ownership of `icc`. Can be used to unload profile (icc.len == 0)
static bool update_icc(struct priv *p, struct bstr icc)
{
struct pl_icc_profile profile = {
.data = icc.start,
.len = icc.len,
};
pl_icc_profile_compute_signature(&profile);
bool ok = pl_icc_update(p->pllog, &p->icc_profile, &profile, &p->icc_params);
talloc_free(icc.start);
return ok;
}
// Returns whether the ICC profile was updated (even on failure)
static bool update_auto_profile(struct priv *p, int *events)
{
const struct gl_video_opts *opts = p->opts_cache->opts;
if (!opts->icc_opts || !opts->icc_opts->profile_auto || p->icc_path)
return false;
MP_VERBOSE(p, "Querying ICC profile...\n");
bstr icc = {0};
int r = p->ra_ctx->fns->control(p->ra_ctx, events, VOCTRL_GET_ICC_PROFILE, &icc);
if (r != VO_NOTAVAIL) {
if (r == VO_FALSE) {
MP_WARN(p, "Could not retrieve an ICC profile.\n");
} else if (r == VO_NOTIMPL) {
MP_ERR(p, "icc-profile-auto not implemented on this platform.\n");
}
update_icc(p, icc);
return true;
}
return false;
}
static void video_screenshot(struct vo *vo, struct voctrl_screenshot *args)
{
struct priv *p = vo->priv;
pl_options pars = p->pars;
pl_gpu gpu = p->gpu;
pl_tex fbo = NULL;
args->res = NULL;
update_options(vo);
struct pl_render_params params = pars->params;
params.info_callback = NULL;
params.skip_caching_single_frame = true;
params.preserve_mixing_cache = false;
params.frame_mixer = NULL;
struct pl_peak_detect_params peak_params;
if (params.peak_detect_params) {
peak_params = *params.peak_detect_params;
params.peak_detect_params = &peak_params;
peak_params.allow_delayed = false;
}
// Retrieve the current frame from the frame queue
struct pl_frame_mix mix;
enum pl_queue_status status;
status = pl_queue_update(p->queue, &mix, pl_queue_params(
.pts = p->last_pts,
#if PL_API_VER >= 340
.drift_compensation = 0,
#endif
));
assert(status != PL_QUEUE_EOF);
if (status == PL_QUEUE_ERR) {
MP_ERR(vo, "Unknown error occurred while trying to take screenshot!\n");
return;
}
if (!mix.num_frames) {
MP_ERR(vo, "No frames available to take screenshot of, is a file loaded?\n");
return;
}
// Passing an interpolation radius of 0 guarantees that the first frame in
// the resulting mix is the correct frame for this PTS
struct pl_frame image = *(struct pl_frame *) mix.frames[0];
struct mp_image *mpi = image.user_data;
struct mp_rect src = p->src, dst = p->dst;
struct mp_osd_res osd = p->osd_res;
if (!args->scaled) {
int w, h;
mp_image_params_get_dsize(&mpi->params, &w, &h);
if (w < 1 || h < 1)
return;
int src_w = mpi->params.w;
int src_h = mpi->params.h;
src = (struct mp_rect) {0, 0, src_w, src_h};
dst = (struct mp_rect) {0, 0, w, h};
if (mp_image_crop_valid(&mpi->params))
src = mpi->params.crop;
if (mpi->params.rotate % 180 == 90) {
MPSWAP(int, w, h);
MPSWAP(int, src_w, src_h);
}
mp_rect_rotate(&src, src_w, src_h, mpi->params.rotate);
mp_rect_rotate(&dst, w, h, mpi->params.rotate);
osd = (struct mp_osd_res) {
.display_par = 1.0,
.w = mp_rect_w(dst),
.h = mp_rect_h(dst),
};
}
// Create target FBO, try high bit depth first
int mpfmt;
for (int depth = args->high_bit_depth ? 16 : 8; depth; depth -= 8) {
if (depth == 16) {
mpfmt = IMGFMT_RGBA64;
} else {
mpfmt = p->ra_ctx->opts.want_alpha ? IMGFMT_RGBA : IMGFMT_RGB0;
}
pl_fmt fmt = pl_find_fmt(gpu, PL_FMT_UNORM, 4, depth, depth,
PL_FMT_CAP_RENDERABLE | PL_FMT_CAP_HOST_READABLE);
if (!fmt)
continue;
fbo = pl_tex_create(gpu, pl_tex_params(
.w = osd.w,
.h = osd.h,
.format = fmt,
.blit_dst = true,
.renderable = true,
.host_readable = true,
.storable = fmt->caps & PL_FMT_CAP_STORABLE,
));
if (fbo)
break;
}
if (!fbo) {
MP_ERR(vo, "Failed creating target FBO for screenshot!\n");
return;
}
struct pl_frame target = {
.repr = pl_color_repr_rgb,
.num_planes = 1,
.planes[0] = {
.texture = fbo,
.components = 4,
.component_mapping = {0, 1, 2, 3},
},
};
if (args->scaled) {
// Apply target LUT, ICC profile and CSP override only in window mode
apply_target_options(p, &target);
} else if (args->native_csp) {
target.color = image.color;
} else {
target.color = pl_color_space_srgb;
}
apply_crop(&image, src, mpi->params.w, mpi->params.h);
apply_crop(&target, dst, fbo->params.w, fbo->params.h);
update_tm_viz(&pars->color_map_params, &target);
int osd_flags = 0;
if (!args->subs)
osd_flags |= OSD_DRAW_OSD_ONLY;
if (!args->osd)
osd_flags |= OSD_DRAW_SUB_ONLY;
const struct gl_video_opts *opts = p->opts_cache->opts;
struct frame_priv *fp = mpi->priv;
if (opts->blend_subs) {
float rx = pl_rect_w(dst) / pl_rect_w(image.crop);
float ry = pl_rect_h(dst) / pl_rect_h(image.crop);
struct mp_osd_res res = {
.w = pl_rect_w(dst),
.h = pl_rect_h(dst),
.ml = -image.crop.x0 * rx,
.mr = (image.crop.x1 - vo->params->w) * rx,
.mt = -image.crop.y0 * ry,
.mb = (image.crop.y1 - vo->params->h) * ry,
.display_par = 1.0,
};
update_overlays(vo, res, osd_flags,
PL_OVERLAY_COORDS_DST_CROP,
&fp->subs, &image, mpi);
} else {
// Disable overlays when blend_subs is disabled
update_overlays(vo, osd, osd_flags, PL_OVERLAY_COORDS_DST_FRAME,
&p->osd_state, &target, mpi);
image.num_overlays = 0;
}
if (!pl_render_image(p->rr, &image, &target, &params)) {
MP_ERR(vo, "Failed rendering frame!\n");
goto done;
}
args->res = mp_image_alloc(mpfmt, fbo->params.w, fbo->params.h);
if (!args->res)
goto done;
args->res->params.color.primaries = mp_prim_from_pl(target.color.primaries);
args->res->params.color.gamma = mp_trc_from_pl(target.color.transfer);
args->res->params.color.levels = mp_levels_from_pl(target.repr.levels);
args->res->params.color.hdr = target.color.hdr;
if (args->scaled)
args->res->params.p_w = args->res->params.p_h = 1;
bool ok = pl_tex_download(gpu, pl_tex_transfer_params(
.tex = fbo,
.ptr = args->res->planes[0],
.row_pitch = args->res->stride[0],
));
if (!ok)
TA_FREEP(&args->res);
// fall through
done:
pl_tex_destroy(gpu, &fbo);
}
static inline void copy_frame_info_to_mp(struct frame_info *pl,
struct mp_frame_perf *mp) {
static_assert(MP_ARRAY_SIZE(pl->info) == MP_ARRAY_SIZE(mp->perf), "");
assert(pl->count <= VO_PASS_PERF_MAX);
mp->count = MPMIN(pl->count, VO_PASS_PERF_MAX);
for (int i = 0; i < mp->count; ++i) {
const struct pl_dispatch_info *pass = &pl->info[i];
static_assert(VO_PERF_SAMPLE_COUNT >= MP_ARRAY_SIZE(pass->samples), "");
assert(pass->num_samples <= MP_ARRAY_SIZE(pass->samples));
struct mp_pass_perf *perf = &mp->perf[i];
perf->count = MPMIN(pass->num_samples, VO_PERF_SAMPLE_COUNT);
memcpy(perf->samples, pass->samples, perf->count * sizeof(pass->samples[0]));
perf->last = pass->last;
perf->peak = pass->peak;
perf->avg = pass->average;
strncpy(mp->desc[i], pass->shader->description, sizeof(mp->desc[i]) - 1);
mp->desc[i][sizeof(mp->desc[i]) - 1] = '\0';
}
}
static int control(struct vo *vo, uint32_t request, void *data)
{
struct priv *p = vo->priv;
switch (request) {
case VOCTRL_SET_PANSCAN:
resize(vo);
return VO_TRUE;
case VOCTRL_SET_EQUALIZER:
case VOCTRL_PAUSE:
if (p->is_interpolated)
vo->want_redraw = true;
return VO_TRUE;
case VOCTRL_UPDATE_RENDER_OPTS: {
m_config_cache_update(p->opts_cache);
const struct gl_video_opts *opts = p->opts_cache->opts;
p->ra_ctx->opts.want_alpha = opts->alpha_mode == ALPHA_YES;
if (p->ra_ctx->fns->update_render_opts)
p->ra_ctx->fns->update_render_opts(p->ra_ctx);
update_render_options(vo);
vo->want_redraw = true;
// Also re-query the auto profile, in case `update_render_options`
// unloaded a manually specified icc profile in favor of
// icc-profile-auto
int events = 0;
update_auto_profile(p, &events);
vo_event(vo, events);
return VO_TRUE;
}
case VOCTRL_RESET:
// Defer until the first new frame (unique ID) actually arrives
p->want_reset = true;
return VO_TRUE;
case VOCTRL_PERFORMANCE_DATA: {
struct voctrl_performance_data *perf = data;
copy_frame_info_to_mp(&p->perf_fresh, &perf->fresh);
copy_frame_info_to_mp(&p->perf_redraw, &perf->redraw);
return true;
}
case VOCTRL_SCREENSHOT:
video_screenshot(vo, data);
return true;
case VOCTRL_EXTERNAL_RESIZE:
reconfig(vo, NULL);
return true;
case VOCTRL_LOAD_HWDEC_API:
ra_hwdec_ctx_load_fmt(&p->hwdec_ctx, vo->hwdec_devs, data);
return true;
}
int events = 0;
int r = p->ra_ctx->fns->control(p->ra_ctx, &events, request, data);
if (events & VO_EVENT_ICC_PROFILE_CHANGED) {
if (update_auto_profile(p, &events))
vo->want_redraw = true;
}
if (events & VO_EVENT_RESIZE)
resize(vo);
if (events & VO_EVENT_EXPOSE)
vo->want_redraw = true;
vo_event(vo, events);
return r;
}
static void wakeup(struct vo *vo)
{
struct priv *p = vo->priv;
if (p->ra_ctx && p->ra_ctx->fns->wakeup)
p->ra_ctx->fns->wakeup(p->ra_ctx);
}
static void wait_events(struct vo *vo, int64_t until_time_ns)
{
struct priv *p = vo->priv;
if (p->ra_ctx && p->ra_ctx->fns->wait_events) {
p->ra_ctx->fns->wait_events(p->ra_ctx, until_time_ns);
} else {
vo_wait_default(vo, until_time_ns);
}
}
static char *get_cache_file(struct priv *p, char *type)
{
char *file = NULL;
char *dir = NULL;
struct gl_video_opts *opts = p->opts_cache->opts;
if (strcmp(type, "shader") == 0) {
if (!opts->shader_cache)
goto done;
dir = opts->shader_cache_dir;
} else if (strcmp(type, "icc") == 0) {
if (!opts->icc_opts->cache)
goto done;
dir = opts->icc_opts->cache_dir;
} else {
goto done;
}
if (dir && dir[0]) {
dir = mp_get_user_path(NULL, p->global, dir);
} else {
dir = mp_find_user_file(NULL, p->global, "cache", "");
}
if (dir && dir[0]) {
file = mp_path_join(NULL, dir, "libplacebo.cache");
mp_mkdirp(dir);
}
talloc_free(dir);
done:
return file;
}
static void load_cache_files(struct priv *p)
{
char *icc_cache = get_cache_file(p, "icc");
char *shader_cache = get_cache_file(p, "shader");
bool same_cache = false;
if (icc_cache && shader_cache)
same_cache = strcmp(icc_cache, shader_cache) == 0;
if (shader_cache) {
FILE *cache = fopen(shader_cache, "rb");
if (cache) {
int ret = pl_cache_load_file(p->shader_cache, cache);
if (same_cache)
pl_cache_load_file(p->icc_cache, cache);
fclose(cache);
if (ret < 0)
MP_WARN(p, "Failed loading cache from %s\n", shader_cache);
}
talloc_free(shader_cache);
}
if (icc_cache && !same_cache) {
FILE *cache = fopen(icc_cache, "rb");
if (cache) {
int ret = pl_cache_load_file(p->icc_cache, cache);
fclose(cache);
if (ret < 0)
MP_WARN(p, "Failed loading cache from %s\n", icc_cache);
}
}
talloc_free(icc_cache);
}
static void save_cache_files(struct priv *p)
{
void *ta_ctx = talloc_new(NULL);
char *icc_cache = get_cache_file(p, "icc");
char *shader_cache = get_cache_file(p, "shader");
talloc_steal(ta_ctx, icc_cache);
talloc_steal(ta_ctx, shader_cache);
bool same_cache = false;
if (icc_cache && shader_cache)
same_cache = strcmp(icc_cache, shader_cache) == 0;
for (int i = 0; i < 2; i++) {
const char *target_file = i == 0 ? shader_cache : icc_cache;
pl_cache target_cache = i == 0 ? p->shader_cache : p->icc_cache;
if (!target_file)
continue;
char *tmp = talloc_asprintf(ta_ctx, "%sXXXXXX", target_file);
int fd = mkstemp(tmp);
if (fd < 0)
continue;
FILE *cache = fdopen(fd, "wb");
if (!cache) {
close(fd);
continue;
}
int ret = pl_cache_save_file(target_cache, cache);
if (same_cache)
ret += pl_cache_save_file(p->icc_cache, cache);
fclose(cache);
if (ret >= 0)
ret = rename(tmp, target_file);
if (ret < 0) {
MP_WARN(p, "Failed saving cache to %s\n", target_file);
unlink(tmp);
}
if (same_cache)
break;
}
talloc_free(ta_ctx);
}
static void uninit(struct vo *vo)
{
struct priv *p = vo->priv;
pl_queue_destroy(&p->queue); // destroy this first
for (int i = 0; i < MP_ARRAY_SIZE(p->osd_state.entries); i++)
pl_tex_destroy(p->gpu, &p->osd_state.entries[i].tex);
for (int i = 0; i < p->num_sub_tex; i++)
pl_tex_destroy(p->gpu, &p->sub_tex[i]);
for (int i = 0; i < p->num_user_hooks; i++)
pl_mpv_user_shader_destroy(&p->user_hooks[i].hook);
if (vo->hwdec_devs) {
ra_hwdec_mapper_free(&p->hwdec_mapper);
ra_hwdec_ctx_uninit(&p->hwdec_ctx);
hwdec_devices_set_loader(vo->hwdec_devs, NULL, NULL);
hwdec_devices_destroy(vo->hwdec_devs);
}
assert(p->num_dr_buffers == 0);
mp_mutex_destroy(&p->dr_lock);
save_cache_files(p);
pl_cache_destroy(&p->shader_cache);
pl_cache_destroy(&p->icc_cache);
pl_icc_close(&p->icc_profile);
pl_renderer_destroy(&p->rr);
for (int i = 0; i < VO_PASS_PERF_MAX; ++i) {
pl_shader_info_deref(&p->perf_fresh.info[i].shader);
pl_shader_info_deref(&p->perf_redraw.info[i].shader);
}
pl_options_free(&p->pars);
p->ra_ctx = NULL;
p->pllog = NULL;
p->gpu = NULL;
p->sw = NULL;
gpu_ctx_destroy(&p->context);
}
static void load_hwdec_api(void *ctx, struct hwdec_imgfmt_request *params)
{
vo_control(ctx, VOCTRL_LOAD_HWDEC_API, params);
}
static int preinit(struct vo *vo)
{
struct priv *p = vo->priv;
p->opts_cache = m_config_cache_alloc(p, vo->global, &gl_video_conf);
p->video_eq = mp_csp_equalizer_create(p, vo->global);
p->global = vo->global;
p->log = vo->log;
struct gl_video_opts *gl_opts = p->opts_cache->opts;
p->context = gpu_ctx_create(vo, gl_opts);
if (!p->context)
goto err_out;
// For the time being
p->ra_ctx = p->context->ra_ctx;
p->pllog = p->context->pllog;
p->gpu = p->context->gpu;
p->sw = p->context->swapchain;
p->hwdec_ctx = (struct ra_hwdec_ctx) {
.log = p->log,
.global = p->global,
.ra_ctx = p->ra_ctx,
};
vo->hwdec_devs = hwdec_devices_create();
hwdec_devices_set_loader(vo->hwdec_devs, load_hwdec_api, vo);
ra_hwdec_ctx_init(&p->hwdec_ctx, vo->hwdec_devs, gl_opts->hwdec_interop, false);
mp_mutex_init(&p->dr_lock);
p->shader_cache = pl_cache_create(pl_cache_params(
.log = p->pllog,
.max_total_size = 10 << 20, // 10 MiB
));
p->icc_cache = pl_cache_create(pl_cache_params(
.log = p->pllog,
.max_total_size = 10 << 20, // 10 MiB
));
pl_gpu_set_cache(p->gpu, p->shader_cache);
load_cache_files(p);
p->rr = pl_renderer_create(p->pllog, p->gpu);
p->queue = pl_queue_create(p->gpu);
p->osd_fmt[SUBBITMAP_LIBASS] = pl_find_named_fmt(p->gpu, "r8");
p->osd_fmt[SUBBITMAP_BGRA] = pl_find_named_fmt(p->gpu, "bgra8");
p->osd_sync = 1;
p->pars = pl_options_alloc(p->pllog);
update_render_options(vo);
return 0;
err_out:
uninit(vo);
return -1;
}
static const struct pl_filter_config *map_scaler(struct priv *p,
enum scaler_unit unit)
{
const struct pl_filter_preset fixed_scalers[] = {
{ "bilinear", &pl_filter_bilinear },
{ "bicubic_fast", &pl_filter_bicubic },
{ "nearest", &pl_filter_nearest },
{ "oversample", &pl_filter_oversample },
{0},
};
const struct pl_filter_preset fixed_frame_mixers[] = {
{ "linear", &pl_filter_bilinear },
{ "oversample", &pl_filter_oversample },
{0},
};
const struct pl_filter_preset *fixed_presets =
unit == SCALER_TSCALE ? fixed_frame_mixers : fixed_scalers;
const struct gl_video_opts *opts = p->opts_cache->opts;
const struct scaler_config *cfg = &opts->scaler[unit];
if (unit == SCALER_DSCALE && (!cfg->kernel.name || !cfg->kernel.name[0]))
cfg = &opts->scaler[SCALER_SCALE];
if (unit == SCALER_CSCALE && (!cfg->kernel.name || !cfg->kernel.name[0]))
cfg = &opts->scaler[SCALER_SCALE];
for (int i = 0; fixed_presets[i].name; i++) {
if (strcmp(cfg->kernel.name, fixed_presets[i].name) == 0)
return fixed_presets[i].filter;
}
// Attempt loading filter preset first, fall back to raw filter function
struct scaler_params *par = &p->scalers[unit];
const struct pl_filter_preset *preset;
const struct pl_filter_function_preset *fpreset;
if ((preset = pl_find_filter_preset(cfg->kernel.name))) {
par->config = *preset->filter;
} else if ((fpreset = pl_find_filter_function_preset(cfg->kernel.name))) {
par->config = (struct pl_filter_config) {
.kernel = fpreset->function,
.params[0] = fpreset->function->params[0],
.params[1] = fpreset->function->params[1],
};
} else {
MP_ERR(p, "Failed mapping filter function '%s', no libplacebo analog?\n",
cfg->kernel.name);
return &pl_filter_bilinear;
}
const struct pl_filter_function_preset *wpreset;
if ((wpreset = pl_find_filter_function_preset(cfg->window.name))) {
par->config.window = wpreset->function;
par->config.wparams[0] = wpreset->function->params[0];
par->config.wparams[1] = wpreset->function->params[1];
}
for (int i = 0; i < 2; i++) {
if (!isnan(cfg->kernel.params[i]))
par->config.params[i] = cfg->kernel.params[i];
if (!isnan(cfg->window.params[i]))
par->config.wparams[i] = cfg->window.params[i];
}
par->config.clamp = cfg->clamp;
if (cfg->kernel.blur > 0.0)
par->config.blur = cfg->kernel.blur;
if (cfg->kernel.taper > 0.0)
par->config.taper = cfg->kernel.taper;
if (cfg->radius > 0.0) {
if (par->config.kernel->resizable) {
par->config.radius = cfg->radius;
} else {
MP_WARN(p, "Filter radius specified but filter '%s' is not "
"resizable, ignoring\n", cfg->kernel.name);
}
}
return &par->config;
}
static const struct pl_hook *load_hook(struct priv *p, const char *path)
{
if (!path || !path[0])
return NULL;
for (int i = 0; i < p->num_user_hooks; i++) {
if (strcmp(p->user_hooks[i].path, path) == 0)
return p->user_hooks[i].hook;
}
char *fname = mp_get_user_path(NULL, p->global, path);
bstr shader = stream_read_file(fname, p, p->global, 1000000000); // 1GB
talloc_free(fname);
const struct pl_hook *hook = NULL;
if (shader.len)
hook = pl_mpv_user_shader_parse(p->gpu, shader.start, shader.len);
MP_TARRAY_APPEND(p, p->user_hooks, p->num_user_hooks, (struct user_hook) {
.path = talloc_strdup(p, path),
.hook = hook,
});
return hook;
}
static void update_icc_opts(struct priv *p, const struct mp_icc_opts *opts)
{
if (!opts)
return;
if (!opts->profile_auto && !p->icc_path) {
// Un-set any auto-loaded profiles if icc-profile-auto was disabled
update_icc(p, (bstr) {0});
}
int s_r = 0, s_g = 0, s_b = 0;
gl_parse_3dlut_size(opts->size_str, &s_r, &s_g, &s_b);
p->icc_params = pl_icc_default_params;
p->icc_params.intent = opts->intent;
p->icc_params.size_r = s_r;
p->icc_params.size_g = s_g;
p->icc_params.size_b = s_b;
p->icc_params.cache = p->icc_cache;
if (!opts->profile || !opts->profile[0]) {
// No profile enabled, un-load any existing profiles
update_icc(p, (bstr) {0});
TA_FREEP(&p->icc_path);
return;
}
if (p->icc_path && strcmp(opts->profile, p->icc_path) == 0)
return; // ICC profile hasn't changed
char *fname = mp_get_user_path(NULL, p->global, opts->profile);
MP_VERBOSE(p, "Opening ICC profile '%s'\n", fname);
struct bstr icc = stream_read_file(fname, p, p->global, 100000000); // 100 MB
talloc_free(fname);
update_icc(p, icc);
// Update cached path
talloc_free(p->icc_path);
p->icc_path = talloc_strdup(p, opts->profile);
}
static void update_lut(struct priv *p, struct user_lut *lut)
{
if (!lut->opt) {
pl_lut_free(&lut->lut);
TA_FREEP(&lut->path);
return;
}
if (lut->path && strcmp(lut->path, lut->opt) == 0)
return; // no change
// Update cached path
pl_lut_free(&lut->lut);
talloc_free(lut->path);
lut->path = talloc_strdup(p, lut->opt);
// Load LUT file
char *fname = mp_get_user_path(NULL, p->global, lut->path);
MP_VERBOSE(p, "Loading custom LUT '%s'\n", fname);
struct bstr lutdata = stream_read_file(fname, p, p->global, 100000000); // 100 MB
lut->lut = pl_lut_parse_cube(p->pllog, lutdata.start, lutdata.len);
talloc_free(lutdata.start);
}
static void update_hook_opts(struct priv *p, char **opts, const char *shaderpath,
const struct pl_hook *hook)
{
if (!opts)
return;
const char *basename = mp_basename(shaderpath);
struct bstr shadername;
if (!mp_splitext(basename, &shadername))
shadername = bstr0(basename);
for (int n = 0; opts[n * 2]; n++) {
struct bstr k = bstr0(opts[n * 2 + 0]);
struct bstr v = bstr0(opts[n * 2 + 1]);
int pos;
if ((pos = bstrchr(k, '/')) >= 0) {
if (!bstr_equals(bstr_splice(k, 0, pos), shadername))
continue;
k = bstr_cut(k, pos + 1);
}
for (int i = 0; i < hook->num_parameters; i++) {
const struct pl_hook_par *hp = &hook->parameters[i];
if (!bstr_equals0(k, hp->name) != 0)
continue;
m_option_t opt = {
.name = hp->name,
};
if (hp->names) {
for (int j = hp->minimum.i; j <= hp->maximum.i; j++) {
if (bstr_equals0(v, hp->names[j])) {
hp->data->i = j;
goto next_hook;
}
}
}
switch (hp->type) {
case PL_VAR_FLOAT:
opt.type = &m_option_type_float;
opt.min = hp->minimum.f;
opt.max = hp->maximum.f;
break;
case PL_VAR_SINT:
opt.type = &m_option_type_int;
opt.min = hp->minimum.i;
opt.max = hp->maximum.i;
break;
case PL_VAR_UINT:
opt.type = &m_option_type_int;
opt.min = MPMIN(hp->minimum.u, INT_MAX);
opt.max = MPMIN(hp->maximum.u, INT_MAX);
break;
}
if (!opt.type)
goto next_hook;
opt.type->parse(p->log, &opt, k, v, hp->data);
goto next_hook;
}
next_hook:;
}
}
static void update_render_options(struct vo *vo)
{
struct priv *p = vo->priv;
pl_options pars = p->pars;
const struct gl_video_opts *opts = p->opts_cache->opts;
pars->params.antiringing_strength = opts->scaler[0].antiring;
pars->params.background_color[0] = opts->background.r / 255.0;
pars->params.background_color[1] = opts->background.g / 255.0;
pars->params.background_color[2] = opts->background.b / 255.0;
pars->params.background_transparency = 1.0 - opts->background.a / 255.0;
pars->params.skip_anti_aliasing = !opts->correct_downscaling;
pars->params.disable_linear_scaling = !opts->linear_downscaling && !opts->linear_upscaling;
pars->params.disable_fbos = opts->dumb_mode == 1;
pars->params.blend_against_tiles = opts->alpha_mode == ALPHA_BLEND_TILES;
pars->params.corner_rounding = p->corner_rounding;
pars->params.correct_subpixel_offsets = !opts->scaler_resizes_only;
// Map scaler options as best we can
pars->params.upscaler = map_scaler(p, SCALER_SCALE);
pars->params.downscaler = map_scaler(p, SCALER_DSCALE);
pars->params.plane_upscaler = map_scaler(p, SCALER_CSCALE);
pars->params.frame_mixer = opts->interpolation ? map_scaler(p, SCALER_TSCALE) : NULL;
// Request as many frames as required from the decoder, depending on the
// speed VPS/FPS ratio libplacebo may need more frames. Request frames up to
// ratio of 1/4, but only if anti aliasing is enabled.
int req_frames = 2;
if (pars->params.frame_mixer) {
req_frames += ceilf(pars->params.frame_mixer->kernel->radius) *
(pars->params.skip_anti_aliasing ? 1 : 4);
}
vo_set_queue_params(vo, 0, MPMIN(VO_MAX_REQ_FRAMES, req_frames));
pars->params.deband_params = opts->deband ? &pars->deband_params : NULL;
pars->deband_params.iterations = opts->deband_opts->iterations;
pars->deband_params.radius = opts->deband_opts->range;
pars->deband_params.threshold = opts->deband_opts->threshold / 16.384;
pars->deband_params.grain = opts->deband_opts->grain / 8.192;
pars->params.sigmoid_params = opts->sigmoid_upscaling ? &pars->sigmoid_params : NULL;
pars->sigmoid_params.center = opts->sigmoid_center;
pars->sigmoid_params.slope = opts->sigmoid_slope;
pars->params.peak_detect_params = opts->tone_map.compute_peak >= 0 ? &pars->peak_detect_params : NULL;
pars->peak_detect_params.smoothing_period = opts->tone_map.decay_rate;
pars->peak_detect_params.scene_threshold_low = opts->tone_map.scene_threshold_low;
pars->peak_detect_params.scene_threshold_high = opts->tone_map.scene_threshold_high;
pars->peak_detect_params.percentile = opts->tone_map.peak_percentile;
pars->peak_detect_params.allow_delayed = p->delayed_peak;
const struct pl_tone_map_function * const tone_map_funs[] = {
[TONE_MAPPING_AUTO] = &pl_tone_map_auto,
[TONE_MAPPING_CLIP] = &pl_tone_map_clip,
[TONE_MAPPING_MOBIUS] = &pl_tone_map_mobius,
[TONE_MAPPING_REINHARD] = &pl_tone_map_reinhard,
[TONE_MAPPING_HABLE] = &pl_tone_map_hable,
[TONE_MAPPING_GAMMA] = &pl_tone_map_gamma,
[TONE_MAPPING_LINEAR] = &pl_tone_map_linear,
[TONE_MAPPING_SPLINE] = &pl_tone_map_spline,
[TONE_MAPPING_BT_2390] = &pl_tone_map_bt2390,
[TONE_MAPPING_BT_2446A] = &pl_tone_map_bt2446a,
[TONE_MAPPING_ST2094_40] = &pl_tone_map_st2094_40,
[TONE_MAPPING_ST2094_10] = &pl_tone_map_st2094_10,
};
const struct pl_gamut_map_function * const gamut_modes[] = {
[GAMUT_AUTO] = pl_color_map_default_params.gamut_mapping,
[GAMUT_CLIP] = &pl_gamut_map_clip,
[GAMUT_PERCEPTUAL] = &pl_gamut_map_perceptual,
[GAMUT_RELATIVE] = &pl_gamut_map_relative,
[GAMUT_SATURATION] = &pl_gamut_map_saturation,
[GAMUT_ABSOLUTE] = &pl_gamut_map_absolute,
[GAMUT_DESATURATE] = &pl_gamut_map_desaturate,
[GAMUT_DARKEN] = &pl_gamut_map_darken,
[GAMUT_WARN] = &pl_gamut_map_highlight,
[GAMUT_LINEAR] = &pl_gamut_map_linear,
};
pars->color_map_params.tone_mapping_function = tone_map_funs[opts->tone_map.curve];
pars->color_map_params.tone_mapping_param = opts->tone_map.curve_param;
if (isnan(pars->color_map_params.tone_mapping_param)) // vo_gpu compatibility
pars->color_map_params.tone_mapping_param = 0.0;
pars->color_map_params.inverse_tone_mapping = opts->tone_map.inverse;
pars->color_map_params.contrast_recovery = opts->tone_map.contrast_recovery;
pars->color_map_params.visualize_lut = opts->tone_map.visualize;
pars->color_map_params.contrast_smoothness = opts->tone_map.contrast_smoothness;
pars->color_map_params.gamut_mapping = gamut_modes[opts->tone_map.gamut_mode];
switch (opts->dither_algo) {
case DITHER_NONE:
pars->params.dither_params = NULL;
break;
case DITHER_ERROR_DIFFUSION:
pars->params.error_diffusion = pl_find_error_diffusion_kernel(opts->error_diffusion);
if (!pars->params.error_diffusion) {
MP_WARN(p, "Could not find error diffusion kernel '%s', falling "
"back to fruit.\n", opts->error_diffusion);
}
MP_FALLTHROUGH;
case DITHER_ORDERED:
case DITHER_FRUIT:
pars->params.dither_params = &pars->dither_params;
pars->dither_params.method = opts->dither_algo == DITHER_ORDERED
? PL_DITHER_ORDERED_FIXED
: PL_DITHER_BLUE_NOISE;
pars->dither_params.lut_size = opts->dither_size;
pars->dither_params.temporal = opts->temporal_dither;
break;
}
if (opts->dither_depth < 0)
pars->params.dither_params = NULL;
update_icc_opts(p, opts->icc_opts);
pars->params.num_hooks = 0;
const struct pl_hook *hook;
for (int i = 0; opts->user_shaders && opts->user_shaders[i]; i++) {
if ((hook = load_hook(p, opts->user_shaders[i]))) {
MP_TARRAY_APPEND(p, p->hooks, pars->params.num_hooks, hook);
update_hook_opts(p, opts->user_shader_opts, opts->user_shaders[i], hook);
}
}
pars->params.hooks = p->hooks;
}
#define OPT_BASE_STRUCT struct priv
const struct m_opt_choice_alternatives lut_types[] = {
{"auto", PL_LUT_UNKNOWN},
{"native", PL_LUT_NATIVE},
{"normalized", PL_LUT_NORMALIZED},
{"conversion", PL_LUT_CONVERSION},
{0}
};
const struct vo_driver video_out_gpu_next = {
.description = "Video output based on libplacebo",
.name = "gpu-next",
.caps = VO_CAP_ROTATE90 |
VO_CAP_FILM_GRAIN |
0x0,
.preinit = preinit,
.query_format = query_format,
.reconfig = reconfig,
.control = control,
.get_image_ts = get_image,
.draw_frame = draw_frame,
.flip_page = flip_page,
.get_vsync = get_vsync,
.wait_events = wait_events,
.wakeup = wakeup,
.uninit = uninit,
.priv_size = sizeof(struct priv),
.priv_defaults = &(const struct priv) {
.inter_preserve = true,
},
.options = (const struct m_option[]) {
{"allow-delayed-peak-detect", OPT_BOOL(delayed_peak)},
{"corner-rounding", OPT_FLOAT(corner_rounding), M_RANGE(0, 1)},
{"interpolation-preserve", OPT_BOOL(inter_preserve)},
{"lut", OPT_STRING(lut.opt), .flags = M_OPT_FILE},
{"lut-type", OPT_CHOICE_C(lut.type, lut_types)},
{"image-lut", OPT_STRING(image_lut.opt), .flags = M_OPT_FILE},
{"image-lut-type", OPT_CHOICE_C(image_lut.type, lut_types)},
{"target-lut", OPT_STRING(target_lut.opt), .flags = M_OPT_FILE},
{"target-colorspace-hint", OPT_BOOL(target_hint)},
// No `target-lut-type` because we don't support non-RGB targets
{"libplacebo-opts", OPT_KEYVALUELIST(raw_opts)},
{0}
},
};