diff options
Diffstat (limited to 'video/out/vo_gpu_next.c')
-rw-r--r-- | video/out/vo_gpu_next.c | 2284 |
1 files changed, 2284 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..67f7d5833a --- /dev/null +++ b/video/out/vo_gpu_next.c @@ -0,0 +1,2284 @@ +/* + * 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 <sys/stat.h> +#include <time.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_swapchain_colorspace_hint(p->sw, NULL); + } + |