diff --git a/include/mpv/render_placebo.h b/include/mpv/render_placebo.h new file mode 100644 index 0000000000000..1850fc03f0d91 --- /dev/null +++ b/include/mpv/render_placebo.h @@ -0,0 +1,139 @@ +/* Copyright (C) 2024 the mpv developers + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef MPV_CLIENT_API_RENDER_LIBPLACEBO_H_ +#define MPV_CLIENT_API_RENDER_LIBPLACEBO_H_ + +#include "render.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Libplacebo backend + * ------------------------------------ + * + * This header contains definitions for using the libplacebo renderer with + * the render.h API. The libplacebo renderer allows the user to use the + * libplacebo library directly. + * + * The client is responsible for: + * - Creating and managing the graphics context (Vulkan instance/device or + * OpenGL context) + * - Creating and providing a pl_swapchain from libplacebo + * + * Basic usage: + * + * 1. Create pl_log using pl_log_create() + * 2. Create a Vulkan instance using pl_vk_inst_create() if using Vulkan + * 3. Create a Vulkan or OpenGL context using + * pl_vk_create()/pl_vulkan_import() or pl_opengl_create() + * 4. Create a pl_swapchain using pl_opengl_create_swapchain() or + * pl_vulkan_create_swapchain() + * 5. Call mpv_render_context_create() with: + * - MPV_RENDER_PARAM_API_TYPE = MPV_RENDER_API_TYPE_LIBPLACEBO + * - MPV_RENDER_PARAM_LIBPLACEBO_SWAPCHAIN = pl_swapchain + * - MPV_RENDER_PARAM_LIBPLACEBO_PL_LOG = pl_log + * 6. Create mpv_render_param array with the following parameters + * (optional): + * - MPV_RENDER_PARAM_LIBPLACEBO_OPTIONS = pl_options + * - MPV_RENDER_PARAM_LIBPLACEBO_FRAME = pl_swapchain_frame + * - MPV_RENDER_PARAM_LIBPLACEBO_VIEWPORT = mpv_render_rect + * 7. Render loop: mpv_render_context_render() then + * pl_swapchain_submit_frame() and pl_swapchain_swap_buffers() if a + * frame is provided + * + * Main notes: + * - Make sure to keep the swapchain valid for the lifetime of the render + * context + * - Make sure to use the same libplacebo library version that the mpv + * library has been compiled with. + */ + +typedef struct mpv_render_rect_t +{ +int x0, y0; +int x1, y1; +} *mpv_render_rect; + + enum + { + + /** + * Required for mpv_render_context_create(). + * Type: pl_swapchain (from libplacebo) + * + * The swapchain that mpv will render to. The client is responsible for + * creating this swapchain and calling pl_swapchain_swap_buffers() after + * mpv_render_context_render() returns. + * + * The swapchain must remain valid for the lifetime of the render context. + */ + MPV_RENDER_PARAM_LIBPLACEBO_SWAPCHAIN = 101, + + /** + * Optional for mpv_render_context_create(). + * Type: pl_log (from libplacebo) + * + * A libplacebo log context for receiving log messages. If not provided, + * mpv will create its own pl_log instance. + */ + MPV_RENDER_PARAM_LIBPLACEBO_PL_LOG = 102, + /** + * Optional for mpv_render_context_render(). + * Type: pl_options (from libplacebo) + * + * A libplacebo options context for passing rendering options. If not + * provided, mpv will use the default options. + */ + MPV_RENDER_PARAM_LIBPLACEBO_OPTIONS = 103, + + /** + * Optional for mpv_render_context_render(). + * Type: pl_swapchain_frame (from libplacebo) + * + * A libplacebo frame to render to. If not provided, mpv will start a + * new swapchain frame and render to it, submit the frame to the swapchain + * and call pl_swapchain_swap_buffers(). The user MUST NOT call + * pl_swapchain_submit_frame() and pl_swapchain_swap_buffers() after + * rendering. If the frame is provided, mpv will not submit the frame to + * the swapchain and will not call pl_swapchain_swap_buffers(), and the + * user will be responsible to call pl_swapchain_submit_frame() and + * pl_swapchain_swap_buffers(). The frame must be valid for the lifetime of + * the render context. + */ + MPV_RENDER_PARAM_LIBPLACEBO_FRAME = 104, + /** + * Optional for mpv_render_context_render(). + * Type: mpv_render_rect + * + * The viewport rectangle to render to. If not provided, mpv will render to + * the entire viewport. + */ + MPV_RENDER_PARAM_LIBPLACEBO_VIEWPORT = 105, + }; + + /** + * API type string for libplacebo backend. + * Use with MPV_RENDER_PARAM_API_TYPE. + */ + #define MPV_RENDER_API_TYPE_LIBPLACEBO "libplacebo" + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/meson.build b/meson.build index d7bfd5f3a3311..193c536c184d7 100644 --- a/meson.build +++ b/meson.build @@ -258,6 +258,7 @@ sources = files( 'video/out/placebo/utils.c', 'video/out/vo_gpu_next.c', 'video/out/gpu_next/context.c', + 'video/out/placebo/libmpv_placebo.c', ## osdep 'osdep/io.c', @@ -1814,7 +1815,7 @@ if get_option('libmpv') description: 'mpv media player client library') headers = ['include/mpv/client.h', 'include/mpv/render.h', - 'include/mpv/render_gl.h', 'include/mpv/stream_cb.h'] + 'include/mpv/render_gl.h', 'include/mpv/stream_cb.h', 'include/mpv/render_placebo.h'] install_headers(headers, subdir: 'mpv') # Allow projects to build with libmpv by cloning into ./subprojects/mpv diff --git a/video/out/libmpv.h b/video/out/libmpv.h index 02efefb1016f6..8edce4ea5e3a1 100644 --- a/video/out/libmpv.h +++ b/video/out/libmpv.h @@ -80,4 +80,5 @@ struct render_backend_fns { }; extern const struct render_backend_fns render_backend_gpu; +extern const struct render_backend_fns render_backend_libplacebo; extern const struct render_backend_fns render_backend_sw; diff --git a/video/out/placebo/libmpv_placebo.c b/video/out/placebo/libmpv_placebo.c new file mode 100644 index 0000000000000..f4c2f4127d643 --- /dev/null +++ b/video/out/placebo/libmpv_placebo.c @@ -0,0 +1,951 @@ +/* + * 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 . + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "osdep/threads.h" +#include "misc/mp_assert.h" +#include "common/common.h" +#include "common/msg.h" +#include "options/m_config.h" +#include "video/out/libmpv.h" +#include "video/out/gpu/video.h" +#include "video/out/gpu/video_shaders.h" +#include "video/out/placebo/utils.h" +#include "video/mp_image.h" + +#include "sub/osd.h" +#include "sub/draw_bmp.h" + +#include "mpv/render_placebo.h" + + + +struct osd_entry { + pl_tex tex; + struct pl_overlay_part *parts; + int num_parts; +}; + +struct overlay_state { + struct osd_entry entries[MAX_OSD_PARTS]; + struct pl_overlay overlays[MAX_OSD_PARTS]; +}; + +struct frame_priv { + struct priv *p; // Back-reference to main priv + struct overlay_state subs; + uint64_t osd_sync; +}; + +struct priv { + struct mp_log *log; + struct mpv_global *global; + + // libplacebo objects + pl_log pllog; + pl_gpu gpu; + pl_renderer rr; + pl_queue queue; + pl_swapchain swapchain; + bool external_pllog; + + // Allocated DR buffers + mp_mutex dr_lock; + pl_buf *dr_buffers; + int num_dr_buffers; + + // OSD state + pl_fmt osd_fmt[SUBBITMAP_COUNT]; + pl_tex *sub_tex; + int num_sub_tex; + struct overlay_state osd_state; + uint64_t osd_sync; + + // Frame state + struct mp_rect src, dst; + struct mp_osd_res osd_res; + struct mp_image_params img_params; + struct osd_state *osd; // OSD source from vo + uint64_t last_id; + double last_pts; + bool want_reset; + + // Rendering options + pl_options pars; + struct m_config_cache *opts_cache; + + int last_w; + int last_h; +}; + +static void update_options(struct priv *p); + +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 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; + + if (!(desc.flags & MP_IMGFLAG_NE)) + return 0; + + if (desc.flags & MP_IMGFLAG_PAL) + return 0; + + if ((desc.flags & MP_IMGFLAG_TYPE_FLOAT) && (desc.flags & MP_IMGFLAG_YUV)) + return 0; + + bool has_bits = 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; + + 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++; + + 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; + 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]; + + 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)) { + *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; + } + + return desc.num_planes; +} + +static bool format_supported(struct priv *p, int format) +{ + 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 int init(struct render_backend *ctx, mpv_render_param *params) +{ + ctx->priv = talloc_zero(NULL, struct priv); + struct priv *p = ctx->priv; + p->log = ctx->log; + p->global = ctx->global; + + char *api = get_mpv_render_param(params, MPV_RENDER_PARAM_API_TYPE, NULL); + if (!api || strcmp(api, MPV_RENDER_API_TYPE_LIBPLACEBO) != 0) + return MPV_ERROR_NOT_IMPLEMENTED; + + // Get optional pl_log + pl_log external_log = get_mpv_render_param(params, (mpv_render_param_type) MPV_RENDER_PARAM_LIBPLACEBO_PL_LOG, NULL); + + // Get required swapchain + p->swapchain = get_mpv_render_param(params, (mpv_render_param_type) MPV_RENDER_PARAM_LIBPLACEBO_SWAPCHAIN, NULL); + if (!p->swapchain) { + MP_ERR(p, "MPV_RENDER_PARAM_LIBPLACEBO_SWAPCHAIN is required\n"); + return MPV_ERROR_INVALID_PARAMETER; + } + + // Create or use pl_log + if (external_log) { + p->pllog = external_log; + p->external_pllog = true; + } else { + p->pllog = mppl_log_create(p, p->log); + if (!p->pllog) { + MP_ERR(p, "Failed to create libplacebo log\n"); + return MPV_ERROR_GENERIC; + } + p->external_pllog = false; + } + + p->gpu = p->swapchain->gpu; + if (!p->gpu) { + MP_ERR(p, "Failed to obtain GPU context\n"); + return MPV_ERROR_GENERIC; + } + + // Create renderer and queue + p->rr = pl_renderer_create(p->pllog, p->gpu); + if (!p->rr) { + MP_ERR(p, "Failed to create libplacebo renderer\n"); + return MPV_ERROR_GENERIC; + } + + p->queue = pl_queue_create(p->gpu); + if (!p->queue) { + MP_ERR(p, "Failed to create frame queue\n"); + return MPV_ERROR_GENERIC; + } + + // Initialize OSD formats + 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; + + // Initialize render options + p->pars = pl_options_alloc(p->pllog); + p->opts_cache = m_config_cache_alloc(p, p->global, &gl_video_conf); + + // Note: Hardware decoding not supported by this backend. + // libmpv render API with external swapchain doesn't have ra_ctx, + // required by libmpv's hwdec structure. + // TODO: Implement an alternative to ra_hwdec_driver for this backend. + // Or find some other way. + ctx->hwdec_devs = NULL; + + // Set capabilities + ctx->driver_caps = VO_CAP_ROTATE90 | VO_CAP_FILM_GRAIN | VO_CAP_VFLIP; + + // Initialize DR buffer mutex + mp_mutex_init(&p->dr_lock); + + p->last_h = p->last_w = 0; + return 0; +} + +static bool check_format(struct render_backend *ctx, int imgfmt) +{ + struct priv *p = ctx->priv; + + return format_supported(p, imgfmt); +} + +static int set_parameter(struct render_backend *ctx, mpv_render_param param) +{ + // struct priv *p = ctx->priv; + (void)ctx; + + switch (param.type) { + case MPV_RENDER_PARAM_ICC_PROFILE: { + // ICC profile handling would go here + // For now, we don't support dynamic ICC profile changes + return MPV_ERROR_NOT_IMPLEMENTED; + } + default: + return MPV_ERROR_NOT_IMPLEMENTED; + } +} + +static void reconfig(struct render_backend *ctx, struct mp_image_params *params) +{ + struct priv *p = ctx->priv; + p->img_params = *params; + p->want_reset = true; +} + +static void reset(struct render_backend *ctx) +{ + struct priv *p = ctx->priv; + p->want_reset = true; +} + +static void update_external(struct render_backend *ctx, struct vo *vo) +{ + struct priv *p = ctx->priv; + p->osd = vo ? vo->osd : NULL; +} + +static void resize(struct render_backend *ctx, struct mp_rect *src, + struct mp_rect *dst, struct mp_osd_res *osd) +{ + struct priv *p = ctx->priv; + + // Store the positioning info - dst contains the centered position + // calculated by mp_get_src_dst_rects based on video alignment options + p->src = *src; + p->dst = *dst; + p->osd_res = *osd; +} + +static int get_target_size(struct render_backend *ctx, mpv_render_param *params, + int *out_w, int *out_h) +{ + struct priv *p = ctx->priv; + + *out_w = p->last_w; + *out_h = p->last_h; + return 0; +} + +static void update_overlays(struct priv *p, struct mp_osd_res res, + struct pl_frame *frame, struct overlay_state *state, + struct osd_state *osd, double pts) +{ + if (!osd) + return; + + struct sub_bitmap_list *subs = osd_render(osd, res, pts, 0, mp_draw_sub_formats); + + 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 = item->packed_w, + .h = item->packed_h, + .host_writable = true, + .sampleable = true, + }); + + if (!ok) { + MP_ERR(p, "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(p, "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.0f, + ((c >> 16) & 0xFF) / 255.0f, + ((c >> 8) & 0xFF) / 255.0f, + (255 - (c & 0xFF)) / 255.0f, + } + }; + 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 = PL_OVERLAY_COORDS_DST_FRAME, + }; + + switch (item->format) { + case SUBBITMAP_BGRA: + ol->mode = PL_OVERLAY_NORMAL; + ol->repr.alpha = PL_ALPHA_PREMULTIPLIED; + break; + case SUBBITMAP_LIBASS: + ol->mode = PL_OVERLAY_MONOCHROME; + ol->repr.alpha = PL_ALPHA_INDEPENDENT; + break; + } + } + + talloc_free(subs); +} + +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; + if (!mpi) + return false; + + struct frame_priv *fp = mpi->priv; + struct priv *p = fp->p; + + struct mp_image_params par = mpi->params; + mp_image_params_guess_csp(&par); + + *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, + }; + + // Software decoding path (hwdec not supported in this backend) + struct pl_plane_data data[4] = {0}; + frame->num_planes = plane_data_from_imgfmt(data, &frame->repr.bits, mpi->imgfmt); + + if (!frame->num_planes) { + // Unsupported format (likely hwdec frame - use --hwdec=no) + MP_ERR(p, "Unsupported frame format: %s (use --hwdec=no)\n", mp_imgfmt_to_name(mpi->imgfmt)); + talloc_free(mpi); + return false; + } + + 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]; + } + + // Check if frame is in a DR buffer (zero-copy path) + 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(p, "Failed uploading frame!\n"); + talloc_free(mpi); + return false; + } + } + + 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); + + 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->p; + + // Save OSD textures for reuse + 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 int render(struct render_backend *ctx, mpv_render_param *params, + struct vo_frame *frame) +{ + struct priv *p = ctx->priv; + mpv_render_rect viewport = get_mpv_render_param( + params, (mpv_render_param_type)MPV_RENDER_PARAM_LIBPLACEBO_VIEWPORT, + NULL); + update_options(p); + + const struct gl_video_opts *opts = p->opts_cache->opts; + bool can_interpolate = opts->interpolation && frame->display_synced && + !frame->still && frame->num_frames > 1; + double pts_offset = can_interpolate ? frame->ideal_frame_vsync : 0; + + struct pl_source_frame vpts; + if (frame->current && !p->want_reset) { + // Check for backward PTS jump + if (pl_queue_peek(p->queue, 0, &vpts) && + frame->current->pts + MPMAX(0, pts_offset) < vpts.pts) + { + MP_VERBOSE(p, "PTS discontinuity (backward): current=%f queue=%f\n", + frame->current->pts, vpts.pts); + p->want_reset = true; + } + // Check for large forward PTS jump (common in streams) + if (p->last_pts > 0 && frame->current->pts > p->last_pts + 5.0) { + MP_VERBOSE(p, "PTS discontinuity (forward): current=%f last=%f\n", + frame->current->pts, p->last_pts); + p->want_reset = true; + } + } + + // Push incoming frames to queue + for (int n = 0; n < frame->num_frames; n++) { + int id = frame->frame_id + n; + + // Handle reset inside the loop to properly handle discontinuities + 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; + + struct mp_image *mpi = mp_image_new_ref(frame->frames[n]); + if (!mpi) { + MP_ERR(p, "Failed to ref frame\n"); + continue; + } + struct frame_priv *fp = talloc_zero(mpi, struct frame_priv); + fp->p = p; + mpi->priv = fp; + + 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; + } + + struct pl_swapchain_frame* swframe = NULL; + struct pl_swapchain_frame internal_swframe; + bool use_application_swframe = false; + { + struct pl_swapchain_frame *application_swframe = get_mpv_render_param( + params, (mpv_render_param_type)MPV_RENDER_PARAM_LIBPLACEBO_FRAME, + NULL); + if (!application_swframe) { + if (!pl_swapchain_start_frame(p->swapchain, &internal_swframe)) { + if (frame->current) { + struct pl_queue_params qparams = *pl_queue_params( + .pts = frame->current->pts + pts_offset, + .radius = pl_frame_mix_radius(&p->pars->params), + ); + pl_queue_update(p->queue, NULL, &qparams); + } + return MPV_ERROR_GENERIC; + } + swframe = &internal_swframe; + } else { + swframe = application_swframe; + use_application_swframe = true; + } + } + + p->last_w = swframe->fbo->params.w; + p->last_h = swframe->fbo->params.h; + + if (viewport) { + // Get video dimensions + int video_w = p->src.x1 - p->src.x0; + int video_h = p->src.y1 - p->src.y0; + if (video_w <= 0 || video_h <= 0) { + // Fallback to image params if src not set + video_w = swframe->fbo->params.w; + video_h = swframe->fbo->params.h; + } + + // Get viewport dimensions + double vp_w = viewport->x1 - viewport->x0; + double vp_h = viewport->y1 - viewport->y0; + + // Calculate aspect ratios + double video_aspect = (double)video_w / video_h; + double viewport_aspect = vp_w / vp_h; + + // Calculate destination rect that fits video in viewport with aspect ratio + double dst_w, dst_h; + if (video_aspect > viewport_aspect) { + // Video is wider - fit to width + dst_w = vp_w; + dst_h = vp_w / video_aspect; + } else { + // Video is taller - fit to height + dst_h = vp_h; + dst_w = vp_h * video_aspect; + } + + // Center in viewport + double offset_x = (vp_w - dst_w) / 2.0; + double offset_y = (vp_h - dst_h) / 2.0; + + // Set destination crop + p->dst.x0 = viewport->x0 + offset_x; + p->dst.y0 = viewport->y0 + offset_y; + p->dst.x1 = viewport->x0 + offset_x + dst_w; + p->dst.y1 = viewport->y0 + offset_y + dst_h; + } + + + struct pl_frame target; + pl_frame_from_swapchain(&target, swframe); + target.overlays = NULL; + target.num_overlays = 0; + + // Set up target crop + if (p->dst.x1 > p->dst.x0 && p->dst.y1 > p->dst.y0) { + target.crop = (struct pl_rect2df) { + .x0 = p->dst.x0, + .y0 = p->dst.y0, + .x1 = p->dst.x1, + .y1 = p->dst.y1, + }; + } else { + target.crop = (struct pl_rect2df) { + .x0 = 0, + .y0 = 0, + .x1 = swframe->fbo->params.w, + .y1 = swframe->fbo->params.h, + }; + } + + // Get frame mix from queue + struct pl_frame_mix mix = {0}; + if (frame->current) { + struct pl_queue_params qparams = *pl_queue_params( + .pts = frame->current->pts + pts_offset, + .radius = pl_frame_mix_radius(&p->pars->params), + .vsync_duration = can_interpolate ? frame->ideal_frame_vsync_duration : 0, + .interpolation_threshold = opts->interpolation_threshold, + ); +#if PL_API_VER >= 340 + qparams.drift_compensation = 0; +#endif + + struct pl_source_frame first; + if (pl_queue_peek(p->queue, 0, &first) && 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(p, "Failed updating frame queue!\n"); + pl_tex_clear(p->gpu, swframe->fbo, (float[4]){ 0.5, 0.0, 1.0, 1.0 }); + goto submit; + case PL_QUEUE_EOF: + case PL_QUEUE_MORE: + case PL_QUEUE_OK: + break; + } + + // Apply source crop to all 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; + if (p->src.x1 > p->src.x0 && p->src.y1 > p->src.y0) { + image->crop = (struct pl_rect2df) { + .x0 = p->src.x0, + .y0 = p->src.y0, + .x1 = p->src.x1, + .y1 = p->src.y1, + }; + } else if (mpi) { + image->crop = (struct pl_rect2df) { + .x0 = 0, + .y0 = 0, + .x1 = mpi->params.w, + .y1 = mpi->params.h, + }; + } + } + } + // Update OSD overlays on target frame + double pts = frame->current ? frame->current->pts : 0; + update_overlays(p, p->osd_res, &target, &p->osd_state, p->osd, pts); + + // Render + struct pl_render_params render_params = p->pars->params; + + // Only enable frame mixing when we can actually interpolate + if (!can_interpolate || mix.num_frames < 2) + render_params.frame_mixer = NULL; + + if (mix.num_frames > 0) { + if (!pl_render_image_mix(p->rr, &mix, &target, &render_params)) { + MP_ERR(p, "Failed rendering frame!\n"); + pl_tex_clear(p->gpu, swframe->fbo, (float[4]){ 0.5, 0.0, 1.0, 1.0 }); + } + } else { + pl_tex_clear(p->gpu, swframe->fbo, (float[4]){ 0.0, 0.0, 0.0, 1.0 }); + } + + // Flush GPU commands + pl_gpu_flush(p->gpu); + +submit: + if (!use_application_swframe) { + if (!pl_swapchain_submit_frame(p->swapchain)) { + MP_ERR(p, "Failed submitting frame to swapchain!\n"); + return MPV_ERROR_GENERIC; + } + + pl_swapchain_swap_buffers(p->swapchain); + } + + return 0; +} + +static struct mp_image *get_image(struct render_backend *ctx, int imgfmt, + int w, int h, int stride_align, int flags) +{ + struct priv *p = ctx->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 screenshot(struct render_backend *ctx, struct vo_frame *frame, + struct voctrl_screenshot *args) +{ + // Not implemented + args->res = NULL; +} + +static void perfdata(struct render_backend *ctx, + struct voctrl_performance_data *out) +{ + // Not implemented + memset(out, 0, sizeof(*out)); +} + +static void destroy(struct render_backend *ctx) +{ + struct priv *p = ctx->priv; + + if (!p) + return; + + // Destroy queue first + pl_queue_destroy(&p->queue); + + // Destroy OSD textures + 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]); + + // Destroy hwdec + mp_assert(p->num_dr_buffers == 0); + mp_mutex_destroy(&p->dr_lock); + + // Destroy renderer + pl_renderer_destroy(&p->rr); + + // Destroy options + pl_options_free(&p->pars); + + // Destroy pl_log if we created it + if (!p->external_pllog && p->pllog) + pl_log_destroy(&p->pllog); + + talloc_free(p); + ctx->priv = NULL; +} + +static void update_options(struct priv *p) +{ + m_config_cache_update(p->opts_cache); + + // Apply basic render options from config + pl_options pars = p->pars; + const struct gl_video_opts *opts = p->opts_cache->opts; + + pars->params.background_color[0] = opts->background_color.r / 255.0; + pars->params.background_color[1] = opts->background_color.g / 255.0; + pars->params.background_color[2] = opts->background_color.b / 255.0; + pars->params.background_transparency = 1 - opts->background_color.a / 255.0; + + // Deband params + pars->params.deband_params = opts->deband ? &pars->deband_params : NULL; + if (opts->deband && opts->deband_opts) { + 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; + } + + // Sigmoid params + pars->params.sigmoid_params = opts->sigmoid_upscaling ? &pars->sigmoid_params : NULL; + if (opts->sigmoid_upscaling) { + pars->sigmoid_params.center = opts->sigmoid_center; + pars->sigmoid_params.slope = opts->sigmoid_slope; + } +} + +const struct render_backend_fns render_backend_libplacebo = { + .init = init, + .check_format = check_format, + .set_parameter = set_parameter, + .reconfig = reconfig, + .reset = reset, + .update_external = update_external, + .resize = resize, + .get_target_size = get_target_size, + .render = render, + .get_image = get_image, + .screenshot = screenshot, + .perfdata = perfdata, + .destroy = destroy, +}; diff --git a/video/out/vo_libmpv.c b/video/out/vo_libmpv.c index a91016df4f4d6..db5de9cdfac1d 100644 --- a/video/out/vo_libmpv.c +++ b/video/out/vo_libmpv.c @@ -113,6 +113,7 @@ struct mpv_render_context { const struct render_backend_fns *render_backends[] = { &render_backend_gpu, + &render_backend_libplacebo, &render_backend_sw, NULL };