summaryrefslogtreecommitdiffstats
path: root/video/out/vo_gpu_next.c
diff options
context:
space:
mode:
Diffstat (limited to 'video/out/vo_gpu_next.c')
-rw-r--r--video/out/vo_gpu_next.c2285
1 files changed, 2285 insertions, 0 deletions
diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c
new file mode 100644
index 0000000000..d68c836b52
--- /dev/null
+++ b/video/out/vo_gpu_next.c
@@ -0,0 +1,2285 @@
+/*
+ * 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 <dirent.h>
+#include <sys/stat.h>
+#include <time.h>
+#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 "misc/io_utils.h"
+#include "options/m_config.h"
+#include "options/options.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 cache {
+ struct mp_log *log;
+ struct mpv_global *global;
+ char *dir;
+ const char *name;
+ size_t size_limit;
+ pl_cache cache;
+};
+
+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;
+
+ 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 m_config_cache *next_opts_cache;
+ struct gl_next_opts *next_opts;
+ struct cache shader_cache, icc_cache;
+ struct mp_csp_equalizer_state *video_eq;
+ struct scaler_params scalers[SCALER_COUNT];
+ const struct pl_hook **hooks; // storage for `params.hooks`
+ enum pl_color_levels output_levels;
+
+ struct pl_icc_params icc_params;
+ char *icc_path;
+ pl_icc_object icc_profile;
+
+ // 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;
+
+ struct mp_image_params target_params;
+};
+
+static void update_render_options(struct vo *vo);
+static void update_lut(struct priv *p, struct user_lut *lut);
+
+struct gl_next_opts {
+ bool delayed_peak;
+ int border_background;
+ float corner_rounding;
+ bool inter_preserve;
+ struct user_lut lut;
+ struct user_lut image_lut;
+ struct user_lut target_lut;
+ bool target_hint;
+ char **raw_opts;
+};
+
+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}
+};
+
+#define OPT_BASE_STRUCT struct gl_next_opts
+const struct m_sub_options gl_next_conf = {
+ .opts = (const struct m_option[]) {
+ {"allow-delayed-peak-detect", OPT_BOOL(delayed_peak)},
+ {"border-background", OPT_CHOICE(border_background,
+ {"none", BACKGROUND_NONE},
+ {"color", BACKGROUND_COLOR},
+ {"tiles", BACKGROUND_TILES})},
+ {"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},
+ },
+ .defaults = &(struct gl_next_opts) {
+ .border_background = BACKGROUND_COLOR,
+ .inter_preserve = true,
+ },
+ .size = sizeof(struct gl_next_opts),
+ .change_flags = UPDATE_VIDEO,
+};
+
+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 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];
+ if (b->dw == 0 || b->dh == 0)
+ continue;
+ 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 = src->params.color;
+ // 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:
+ if (src && item->video_color_space && !pl_color_space_is_hdr(&src->params.color))
+ ol->color = src->params.color;
+ 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 bool hwdec_reconfig(struct priv *p, struct ra_hwdec *hwdec,
+ const struct mp_image_params *par)
+{
+ if (p->hwdec_mapper) {
+ if (mp_image_params_static_equal(par, &p->hwdec_mapper->src_params)) {
+ p->hwdec_mapper->src_params.repr.dovi = par->repr.dovi;
+ p->hwdec_mapper->dst_params.repr.dovi = par->repr.dovi;
+ p->hwdec_mapper->src_params.color.hdr = par->color.hdr;
+ p->hwdec_mapper->dst_params.color.hdr = par->color.hdr;
+ 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 = par->color,
+ .repr = par->repr,
+ .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 PL_COLOR_SYSTEM_RGB:
+ frame->repr.sys = PL_COLOR_SYSTEM_RGB;
+ frame->repr.levels = PL_COLOR_LEVELS_FULL;
+ break;
+ case PL_COLOR_SYSTEM_XYZ:
+ frame->repr.sys = PL_COLOR_SYSTEM_XYZ;
+ break;
+ case PL_COLOR_SYSTEM_UNKNOWN:
+ 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, par->chroma_location);
+
+ 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->next_opts->image_lut);
+ frame->lut = p->next_opts->image_lut.lut;
+ frame->lut_type = p->next_opts->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;
+ bool changed = m_config_cache_update(p->opts_cache);
+ changed = m_config_cache_update(p->next_opts_cache) || changed;
+ if (changed)
+ update_render_options(vo);
+
+ update_lut(p, &p->next_opts->lut);
+ pars->params.lut = p->next_opts->lut.lut;
+ pars->params.lut_type = p->next_opts->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->next_opts->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->next_opts->target_lut);
+ target->lut = p->next_opts->target_lut.lut;
+ target->lut_type = p->next_opts->target_lut.type;
+
+ // Colorspace overrides
+ const struct gl_video_opts *opts = p->opts_cache->opts;
+ if (p->output_levels)
+ target->repr.levels = p->output_levels;
+ if (opts->target_prim)
+ target->color.primaries = opts->target_prim;
+ if (opts->target_trc)
+ target->color.transfer = 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(opts->target_gamut);
+ container = pl_raw_primaries_get(target->color.primaries);
+ target->color.hdr.prim = pl_primaries_clip(gamut, container);
+ }
+ int dither_depth = opts->dither_depth;
+ if (dither_depth == 0) {
+ struct ra_swapchain *sw = p->ra_ctx->swapchain;
+ if (sw->fns->color_depth) {
+ dither_depth = sw->fns->color_depth(sw);
+ } else if (!pl_color_transfer_is_hdr(target->color.transfer)) {
+ dither_depth = 8;
+ }
+ }
+ if (dither_depth > 0) {
+ struct pl_bit_encoding *tbits = &target->repr.bits;
+ tbits->color_depth += dither_depth - tbits->sample_depth;
+ tbits->sample_depth = 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->next_opts->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->next_opts->target_hint && frame->current) {
+ struct pl_color_space hint = frame->current->params.color;
+ if (opts->target_prim)
+ hint.primaries = opts->target_prim;
+ if (opts->target_trc)
+ hint.transfer = 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->next_opts->target_hint) {
+ pl_swa