summaryrefslogtreecommitdiffstats
path: root/video/out
diff options
context:
space:
mode:
Diffstat (limited to 'video/out')
-rw-r--r--video/out/android_common.c99
-rw-r--r--video/out/android_common.h29
-rw-r--r--video/out/aspect.c216
-rw-r--r--video/out/aspect.h33
-rw-r--r--video/out/bitmap_packer.c197
-rw-r--r--video/out/bitmap_packer.h51
-rw-r--r--video/out/cocoa_cb_common.swift230
-rw-r--r--video/out/d3d11/context.c566
-rw-r--r--video/out/d3d11/context.h9
-rw-r--r--video/out/d3d11/hwdec_d3d11va.c258
-rw-r--r--video/out/d3d11/hwdec_dxva2dxgi.c478
-rw-r--r--video/out/d3d11/ra_d3d11.c2544
-rw-r--r--video/out/d3d11/ra_d3d11.h47
-rw-r--r--video/out/dither.c175
-rw-r--r--video/out/dither.h2
-rw-r--r--video/out/dr_helper.c162
-rw-r--r--video/out/dr_helper.h37
-rw-r--r--video/out/drm_atomic.c458
-rw-r--r--video/out/drm_atomic.h100
-rw-r--r--video/out/drm_common.c1289
-rw-r--r--video/out/drm_common.h108
-rw-r--r--video/out/drm_prime.c160
-rw-r--r--video/out/drm_prime.h45
-rw-r--r--video/out/filter_kernels.c411
-rw-r--r--video/out/filter_kernels.h56
-rw-r--r--video/out/gpu/context.c277
-rw-r--r--video/out/gpu/context.h107
-rw-r--r--video/out/gpu/d3d11_helpers.c966
-rw-r--r--video/out/gpu/d3d11_helpers.h103
-rw-r--r--video/out/gpu/error_diffusion.c316
-rw-r--r--video/out/gpu/error_diffusion.h48
-rw-r--r--video/out/gpu/hwdec.c358
-rw-r--r--video/out/gpu/hwdec.h156
-rw-r--r--video/out/gpu/lcms.c526
-rw-r--r--video/out/gpu/lcms.h61
-rw-r--r--video/out/gpu/libmpv_gpu.c248
-rw-r--r--video/out/gpu/libmpv_gpu.h40
-rw-r--r--video/out/gpu/osd.c363
-rw-r--r--video/out/gpu/osd.h25
-rw-r--r--video/out/gpu/ra.c424
-rw-r--r--video/out/gpu/ra.h559
-rw-r--r--video/out/gpu/shader_cache.c1056
-rw-r--r--video/out/gpu/shader_cache.h66
-rw-r--r--video/out/gpu/spirv.c70
-rw-r--r--video/out/gpu/spirv.h41
-rw-r--r--video/out/gpu/spirv_shaderc.c125
-rw-r--r--video/out/gpu/user_shaders.c463
-rw-r--r--video/out/gpu/user_shaders.h99
-rw-r--r--video/out/gpu/utils.c349
-rw-r--r--video/out/gpu/utils.h108
-rw-r--r--video/out/gpu/video.c4364
-rw-r--r--video/out/gpu/video.h238
-rw-r--r--video/out/gpu/video_shaders.c1033
-rw-r--r--video/out/gpu/video_shaders.h59
-rw-r--r--video/out/gpu_next/context.c240
-rw-r--r--video/out/gpu_next/context.h40
-rw-r--r--video/out/hwdec/dmabuf_interop.h57
-rw-r--r--video/out/hwdec/dmabuf_interop_gl.c311
-rw-r--r--video/out/hwdec/dmabuf_interop_pl.c138
-rw-r--r--video/out/hwdec/dmabuf_interop_wl.c83
-rw-r--r--video/out/hwdec/hwdec_aimagereader.c402
-rw-r--r--video/out/hwdec/hwdec_cuda.c286
-rw-r--r--video/out/hwdec/hwdec_cuda.h59
-rw-r--r--video/out/hwdec/hwdec_cuda_gl.c174
-rw-r--r--video/out/hwdec/hwdec_cuda_vk.c344
-rw-r--r--video/out/hwdec/hwdec_drmprime.c294
-rw-r--r--video/out/hwdec/hwdec_drmprime_overlay.c334
-rw-r--r--video/out/hwdec/hwdec_ios_gl.m222
-rw-r--r--video/out/hwdec/hwdec_mac_gl.c169
-rw-r--r--video/out/hwdec/hwdec_vaapi.c557
-rw-r--r--video/out/hwdec/hwdec_vt.c141
-rw-r--r--video/out/hwdec/hwdec_vt.h63
-rw-r--r--video/out/hwdec/hwdec_vt_pl.m312
-rw-r--r--video/out/hwdec/hwdec_vulkan.c333
-rw-r--r--video/out/libmpv.h83
-rw-r--r--video/out/libmpv_sw.c208
-rw-r--r--video/out/mac/common.swift691
-rw-r--r--video/out/mac/gl_layer.swift322
-rw-r--r--video/out/mac/metal_layer.swift43
-rw-r--r--video/out/mac/title_bar.swift229
-rw-r--r--video/out/mac/view.swift297
-rw-r--r--video/out/mac/window.swift593
-rw-r--r--video/out/mac_common.swift174
-rw-r--r--video/out/meson.build51
-rw-r--r--video/out/opengl/angle_dynamic.c39
-rw-r--r--video/out/opengl/angle_dynamic.h89
-rw-r--r--video/out/opengl/common.c694
-rw-r--r--video/out/opengl/common.h258
-rw-r--r--video/out/opengl/context.c324
-rw-r--r--video/out/opengl/context.h58
-rw-r--r--video/out/opengl/context_android.c130
-rw-r--r--video/out/opengl/context_angle.c653
-rw-r--r--video/out/opengl/context_drm_egl.c744
-rw-r--r--video/out/opengl/context_dxinterop.c605
-rw-r--r--video/out/opengl/context_glx.c351
-rw-r--r--video/out/opengl/context_rpi.c327
-rw-r--r--video/out/opengl/context_wayland.c230
-rw-r--r--video/out/opengl/context_win.c378
-rw-r--r--video/out/opengl/context_x11egl.c225
-rw-r--r--video/out/opengl/egl_helpers.c381
-rw-r--r--video/out/opengl/egl_helpers.h38
-rw-r--r--video/out/opengl/formats.c196
-rw-r--r--video/out/opengl/formats.h51
-rw-r--r--video/out/opengl/gl_headers.h799
-rw-r--r--video/out/opengl/hwdec_d3d11egl.c363
-rw-r--r--video/out/opengl/hwdec_dxva2egl.c384
-rw-r--r--video/out/opengl/hwdec_dxva2gldx.c247
-rw-r--r--video/out/opengl/hwdec_rpi.c384
-rw-r--r--video/out/opengl/hwdec_vdpau.c251
-rw-r--r--video/out/opengl/libmpv_gl.c114
-rw-r--r--video/out/opengl/ra_gl.c1208
-rw-r--r--video/out/opengl/ra_gl.h17
-rw-r--r--video/out/opengl/utils.c282
-rw-r--r--video/out/opengl/utils.h57
-rw-r--r--video/out/placebo/ra_pl.c677
-rw-r--r--video/out/placebo/ra_pl.h16
-rw-r--r--video/out/placebo/utils.c263
-rw-r--r--video/out/placebo/utils.h41
-rw-r--r--video/out/present_sync.c126
-rw-r--r--video/out/present_sync.h57
-rw-r--r--video/out/vo.c1441
-rw-r--r--video/out/vo.h544
-rw-r--r--video/out/vo_caca.c314
-rw-r--r--video/out/vo_direct3d.c1247
-rw-r--r--video/out/vo_dmabuf_wayland.c872
-rw-r--r--video/out/vo_drm.c458
-rw-r--r--video/out/vo_gpu.c336
-rw-r--r--video/out/vo_gpu_next.c2104
-rw-r--r--video/out/vo_image.c165
-rw-r--r--video/out/vo_kitty.c433
-rw-r--r--video/out/vo_lavc.c262
-rw-r--r--video/out/vo_libmpv.c748
-rw-r--r--video/out/vo_mediacodec_embed.c127
-rw-r--r--video/out/vo_null.c104
-rw-r--r--video/out/vo_rpi.c938
-rw-r--r--video/out/vo_sdl.c992
-rw-r--r--video/out/vo_sixel.c627
-rw-r--r--video/out/vo_tct.c347
-rw-r--r--video/out/vo_vaapi.c877
-rw-r--r--video/out/vo_vdpau.c1139
-rw-r--r--video/out/vo_wlshm.c324
-rw-r--r--video/out/vo_x11.c447
-rw-r--r--video/out/vo_xv.c921
-rw-r--r--video/out/vulkan/common.h40
-rw-r--r--video/out/vulkan/context.c372
-rw-r--r--video/out/vulkan/context.h31
-rw-r--r--video/out/vulkan/context_android.c96
-rw-r--r--video/out/vulkan/context_display.c491
-rw-r--r--video/out/vulkan/context_mac.m119
-rw-r--r--video/out/vulkan/context_wayland.c167
-rw-r--r--video/out/vulkan/context_win.c106
-rw-r--r--video/out/vulkan/context_xlib.c143
-rw-r--r--video/out/vulkan/utils.c42
-rw-r--r--video/out/vulkan/utils.h6
-rw-r--r--video/out/w32_common.c2144
-rw-r--r--video/out/w32_common.h36
-rw-r--r--video/out/wayland_common.c2629
-rw-r--r--video/out/wayland_common.h189
-rw-r--r--video/out/win32/displayconfig.c140
-rw-r--r--video/out/win32/displayconfig.h27
-rw-r--r--video/out/win32/droptarget.c227
-rw-r--r--video/out/win32/droptarget.h35
-rw-r--r--video/out/win_state.c155
-rw-r--r--video/out/win_state.h35
-rw-r--r--video/out/wldmabuf/context_wldmabuf.c43
-rw-r--r--video/out/wldmabuf/ra_wldmabuf.c66
-rw-r--r--video/out/wldmabuf/ra_wldmabuf.h23
-rw-r--r--video/out/x11_common.c2291
-rw-r--r--video/out/x11_common.h164
169 files changed, 64299 insertions, 0 deletions
diff --git a/video/out/android_common.c b/video/out/android_common.c
new file mode 100644
index 0000000..27e7b5b
--- /dev/null
+++ b/video/out/android_common.c
@@ -0,0 +1,99 @@
+/*
+ * 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 <libavcodec/jni.h>
+#include <android/native_window_jni.h>
+
+#include "android_common.h"
+#include "common/msg.h"
+#include "misc/jni.h"
+#include "options/m_config.h"
+#include "vo.h"
+
+struct vo_android_state {
+ struct mp_log *log;
+ ANativeWindow *native_window;
+};
+
+bool vo_android_init(struct vo *vo)
+{
+ vo->android = talloc_zero(vo, struct vo_android_state);
+ struct vo_android_state *ctx = vo->android;
+
+ *ctx = (struct vo_android_state){
+ .log = mp_log_new(ctx, vo->log, "android"),
+ };
+
+ JNIEnv *env = MP_JNI_GET_ENV(ctx);
+ if (!env) {
+ MP_FATAL(ctx, "Could not attach java VM.\n");
+ goto fail;
+ }
+
+ assert(vo->opts->WinID != 0 && vo->opts->WinID != -1);
+ jobject surface = (jobject)(intptr_t)vo->opts->WinID;
+ ctx->native_window = ANativeWindow_fromSurface(env, surface);
+ if (!ctx->native_window) {
+ MP_FATAL(ctx, "Failed to create ANativeWindow\n");
+ goto fail;
+ }
+
+ return true;
+fail:
+ talloc_free(ctx);
+ vo->android = NULL;
+ return false;
+}
+
+void vo_android_uninit(struct vo *vo)
+{
+ struct vo_android_state *ctx = vo->android;
+ if (!ctx)
+ return;
+
+ if (ctx->native_window)
+ ANativeWindow_release(ctx->native_window);
+
+ talloc_free(ctx);
+ vo->android = NULL;
+}
+
+ANativeWindow *vo_android_native_window(struct vo *vo)
+{
+ struct vo_android_state *ctx = vo->android;
+ return ctx->native_window;
+}
+
+bool vo_android_surface_size(struct vo *vo, int *out_w, int *out_h)
+{
+ struct vo_android_state *ctx = vo->android;
+
+ int w = vo->opts->android_surface_size.w,
+ h = vo->opts->android_surface_size.h;
+ if (!w)
+ w = ANativeWindow_getWidth(ctx->native_window);
+ if (!h)
+ h = ANativeWindow_getHeight(ctx->native_window);
+
+ if (w <= 0 || h <= 0) {
+ MP_ERR(ctx, "Failed to get height and width.\n");
+ return false;
+ }
+ *out_w = w;
+ *out_h = h;
+ return true;
+}
diff --git a/video/out/android_common.h b/video/out/android_common.h
new file mode 100644
index 0000000..7f075ea
--- /dev/null
+++ b/video/out/android_common.h
@@ -0,0 +1,29 @@
+/*
+ * 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/>.
+ */
+
+#pragma once
+
+#include <android/native_window_jni.h>
+
+#include "common/common.h"
+
+struct vo;
+
+bool vo_android_init(struct vo *vo);
+void vo_android_uninit(struct vo *vo);
+ANativeWindow *vo_android_native_window(struct vo *vo);
+bool vo_android_surface_size(struct vo *vo, int *w, int *h);
diff --git a/video/out/aspect.c b/video/out/aspect.c
new file mode 100644
index 0000000..6e1cd63
--- /dev/null
+++ b/video/out/aspect.c
@@ -0,0 +1,216 @@
+/*
+ * 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/>.
+ */
+
+/* Stuff for correct aspect scaling. */
+#include "aspect.h"
+#include "math.h"
+#include "vo.h"
+#include "common/msg.h"
+#include "options/options.h"
+#include "video/mp_image.h"
+
+#include "vo.h"
+#include "sub/osd.h"
+
+static void aspect_calc_panscan(struct mp_vo_opts *opts,
+ int w, int h, int d_w, int d_h, int unscaled,
+ int window_w, int window_h, double monitor_par,
+ int *out_w, int *out_h)
+{
+ int fwidth = window_w;
+ int fheight = (float)window_w / d_w * d_h / monitor_par;
+ if (fheight > window_h || fheight < h) {
+ int tmpw = (float)window_h / d_h * d_w * monitor_par;
+ if (tmpw <= window_w) {
+ fheight = window_h;
+ fwidth = tmpw;
+ }
+ }
+
+ int vo_panscan_area = window_h - fheight;
+ double f_w = fwidth / (double)MPMAX(fheight, 1);
+ double f_h = 1;
+ if (vo_panscan_area == 0) {
+ vo_panscan_area = window_w - fwidth;
+ f_w = 1;
+ f_h = fheight / (double)MPMAX(fwidth, 1);
+ }
+
+ if (unscaled) {
+ vo_panscan_area = 0;
+ if (unscaled != 2 || (d_w <= window_w && d_h <= window_h)) {
+ fwidth = d_w * monitor_par;
+ fheight = d_h;
+ }
+ }
+
+ *out_w = fwidth + vo_panscan_area * opts->panscan * f_w;
+ *out_h = fheight + vo_panscan_area * opts->panscan * f_h;
+}
+
+// Clamp [start, end) to range [0, size) with various fallbacks.
+static void clamp_size(int size, int *start, int *end)
+{
+ *start = MPMAX(0, *start);
+ *end = MPMIN(size, *end);
+ if (*start >= *end) {
+ *start = 0;
+ *end = 1;
+ }
+}
+
+static void src_dst_split_scaling(int src_size, int dst_size,
+ int scaled_src_size,
+ float zoom, float align, float pan, float scale,
+ int *src_start, int *src_end,
+ int *dst_start, int *dst_end,
+ int *osd_margin_a, int *osd_margin_b)
+{
+ scaled_src_size *= powf(2, zoom) * scale;
+ scaled_src_size = MPMAX(scaled_src_size, 1);
+ align = (align + 1) / 2;
+
+ *dst_start = (dst_size - scaled_src_size) * align + pan * scaled_src_size;
+ *dst_end = *dst_start + scaled_src_size;
+
+ // Distance of screen frame to video
+ *osd_margin_a = *dst_start;
+ *osd_margin_b = dst_size - *dst_end;
+
+ // Clip to screen
+ int s_src = *src_end - *src_start;
+ int s_dst = *dst_end - *dst_start;
+ if (*dst_start < 0) {
+ int border = -(*dst_start) * s_src / s_dst;
+ *src_start += border;
+ *dst_start = 0;
+ }
+ if (*dst_end > dst_size) {
+ int border = (*dst_end - dst_size) * s_src / s_dst;
+ *src_end -= border;
+ *dst_end = dst_size;
+ }
+
+ // For sanity: avoid bothering VOs with corner cases
+ clamp_size(src_size, src_start, src_end);
+ clamp_size(dst_size, dst_start, dst_end);
+}
+
+static void calc_margin(float opts[2], int out[2], int size)
+{
+ out[0] = MPCLAMP((int)(opts[0] * size), 0, size);
+ out[1] = MPCLAMP((int)(opts[1] * size), 0, size);
+
+ if (out[0] + out[1] >= size) {
+ // This case is not really supported. Show an error by 1 pixel.
+ out[0] = 0;
+ out[1] = MPMAX(0, size - 1);
+ }
+}
+
+void mp_get_src_dst_rects(struct mp_log *log, struct mp_vo_opts *opts,
+ int vo_caps, struct mp_image_params *video,
+ int window_w, int window_h, double monitor_par,
+ struct mp_rect *out_src,
+ struct mp_rect *out_dst,
+ struct mp_osd_res *out_osd)
+{
+ int src_w = video->w;
+ int src_h = video->h;
+ int src_dw, src_dh;
+
+ mp_image_params_get_dsize(video, &src_dw, &src_dh);
+ window_w = MPMAX(1, window_w);
+ window_h = MPMAX(1, window_h);
+
+ int margin_x[2] = {0};
+ int margin_y[2] = {0};
+ if (opts->keepaspect) {
+ calc_margin(opts->margin_x, margin_x, window_w);
+ calc_margin(opts->margin_y, margin_y, window_h);
+ }
+
+ int vid_window_w = window_w - margin_x[0] - margin_x[1];
+ int vid_window_h = window_h - margin_y[0] - margin_y[1];
+
+ struct mp_rect dst = {0, 0, window_w, window_h};
+ struct mp_rect src = {0, 0, src_w, src_h};
+ if (mp_image_crop_valid(video))
+ src = video->crop;
+
+ if (vo_caps & VO_CAP_ROTATE90) {
+ if (video->rotate % 180 == 90) {
+ MPSWAP(int, src_w, src_h);
+ MPSWAP(int, src_dw, src_dh);
+ }
+ mp_rect_rotate(&src, src_w, src_h, video->rotate);
+ }
+
+ struct mp_osd_res osd = {
+ .w = window_w,
+ .h = window_h,
+ .display_par = monitor_par,
+ };
+
+ if (opts->keepaspect) {
+ int scaled_width, scaled_height;
+ aspect_calc_panscan(opts, src_w, src_h, src_dw, src_dh, opts->unscaled,
+ vid_window_w, vid_window_h, monitor_par,
+ &scaled_width, &scaled_height);
+ src_dst_split_scaling(src_w, vid_window_w, scaled_width,
+ opts->zoom, opts->align_x, opts->pan_x, opts->scale_x,
+ &src.x0, &src.x1, &dst.x0, &dst.x1,
+ &osd.ml, &osd.mr);
+ src_dst_split_scaling(src_h, vid_window_h, scaled_height,
+ opts->zoom, opts->align_y, opts->pan_y, opts->scale_y,
+ &src.y0, &src.y1, &dst.y0, &dst.y1,
+ &osd.mt, &osd.mb);
+ }
+
+ dst.x0 += margin_x[0];
+ dst.y0 += margin_y[0];
+ dst.x1 += margin_x[0];
+ dst.y1 += margin_y[0];
+
+ // OSD really uses the full window, but was computed on the margin-cut
+ // video sub-window. Correct it to the full window.
+ osd.ml += margin_x[0];
+ osd.mr += margin_x[1];
+ osd.mt += margin_y[0];
+ osd.mb += margin_y[1];
+
+ *out_src = src;
+ *out_dst = dst;
+ *out_osd = osd;
+
+ int sw = src.x1 - src.x0, sh = src.y1 - src.y0;
+ int dw = dst.x1 - dst.x0, dh = dst.y1 - dst.y0;
+
+ mp_verbose(log, "Window size: %dx%d (Borders: l=%d t=%d r=%d b=%d)\n",
+ window_w, window_h,
+ margin_x[0], margin_y[0], margin_x[1], margin_y[1]);
+ mp_verbose(log, "Video source: %dx%d (%d:%d)\n",
+ video->w, video->h, video->p_w, video->p_h);
+ mp_verbose(log, "Video display: (%d, %d) %dx%d -> (%d, %d) %dx%d\n",
+ src.x0, src.y0, sw, sh, dst.x0, dst.y0, dw, dh);
+ mp_verbose(log, "Video scale: %f/%f\n",
+ (double)dw / sw, (double)dh / sh);
+ mp_verbose(log, "OSD borders: l=%d t=%d r=%d b=%d\n",
+ osd.ml, osd.mt, osd.mr, osd.mb);
+ mp_verbose(log, "Video borders: l=%d t=%d r=%d b=%d\n",
+ dst.x0, dst.y0, window_w - dst.x1, window_h - dst.y1);
+}
diff --git a/video/out/aspect.h b/video/out/aspect.h
new file mode 100644
index 0000000..4123311
--- /dev/null
+++ b/video/out/aspect.h
@@ -0,0 +1,33 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MPLAYER_ASPECT_H
+#define MPLAYER_ASPECT_H
+
+struct mp_log;
+struct mp_vo_opts;
+struct mp_image_params;
+struct mp_rect;
+struct mp_osd_res;
+void mp_get_src_dst_rects(struct mp_log *log, struct mp_vo_opts *opts,
+ int vo_caps, struct mp_image_params *video,
+ int window_w, int window_h, double monitor_par,
+ struct mp_rect *out_src,
+ struct mp_rect *out_dst,
+ struct mp_osd_res *out_osd);
+
+#endif /* MPLAYER_ASPECT_H */
diff --git a/video/out/bitmap_packer.c b/video/out/bitmap_packer.c
new file mode 100644
index 0000000..5ef090b
--- /dev/null
+++ b/video/out/bitmap_packer.c
@@ -0,0 +1,197 @@
+/*
+ * Calculate how to pack bitmap rectangles into a larger surface
+ *
+ * Copyright 2009, 2012 Uoti Urpala
+ *
+ * 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 <stdlib.h>
+#include <assert.h>
+#include <stdio.h>
+#include <limits.h>
+
+#include "mpv_talloc.h"
+#include "bitmap_packer.h"
+#include "common/common.h"
+
+#define IS_POWER_OF_2(x) (((x) > 0) && !(((x) - 1) & (x)))
+
+void packer_reset(struct bitmap_packer *packer)
+{
+ struct bitmap_packer old = *packer;
+ *packer = (struct bitmap_packer) {
+ .w_max = old.w_max,
+ .h_max = old.h_max,
+ };
+ talloc_free_children(packer);
+}
+
+void packer_get_bb(struct bitmap_packer *packer, struct pos out_bb[2])
+{
+ out_bb[0] = (struct pos) {0};
+ out_bb[1] = (struct pos) {packer->used_width, packer->used_height};
+}
+
+#define HEIGHT_SORT_BITS 4
+static int size_index(int s)
+{
+ int n = mp_log2(s);
+ return (n << HEIGHT_SORT_BITS)
+ + ((- 1 - (s << HEIGHT_SORT_BITS >> n)) & ((1 << HEIGHT_SORT_BITS) - 1));
+}
+
+/* Pack the given rectangles into an area of size w * h.
+ * The size of each rectangle is read from in[i].x / in[i].y.
+ * The height of each rectangle must be less than 65536.
+ * 'scratch' must point to work memory for num_rects+16 ints.
+ * The packed position for rectangle number i is set in out[i].
+ * Return 0 on success, -1 if the rectangles did not fit in w*h.
+ *
+ * The rectangles are placed in rows in order approximately sorted by
+ * height (the approximate sorting is simpler than a full one would be,
+ * and allows the algorithm to work in linear time). Additionally, to
+ * reduce wasted space when there are a few tall rectangles, empty
+ * lower-right parts of rows are filled recursively when the size of
+ * rectangles in the row drops past a power-of-two threshold. So if a
+ * row starts with rectangles of size 3x50, 10x40 and 5x20 then the
+ * free rectangle with corners (13, 20)-(w, 50) is filled recursively.
+ */
+static int pack_rectangles(struct pos *in, struct pos *out, int num_rects,
+ int w, int h, int *scratch, int *used_width)
+{
+ int bins[16 << HEIGHT_SORT_BITS];
+ int sizes[16 << HEIGHT_SORT_BITS] = { 0 };
+ for (int i = 0; i < num_rects; i++)
+ sizes[size_index(in[i].y)]++;
+ int idx = 0;
+ for (int i = 0; i < 16 << HEIGHT_SORT_BITS; i += 1 << HEIGHT_SORT_BITS) {
+ for (int j = 0; j < 1 << HEIGHT_SORT_BITS; j++) {
+ bins[i + j] = idx;
+ idx += sizes[i + j];
+ }
+ scratch[idx++] = -1;
+ }
+ for (int i = 0; i < num_rects; i++)
+ scratch[bins[size_index(in[i].y)]++] = i;
+ for (int i = 0; i < 16; i++)
+ bins[i] = bins[i << HEIGHT_SORT_BITS] - sizes[i << HEIGHT_SORT_BITS];
+ struct {
+ int size, x, bottom;
+ } stack[16] = {{15, 0, h}}, s = {0};
+ int stackpos = 1;
+ int y;
+ while (stackpos) {
+ y = s.bottom;
+ s = stack[--stackpos];
+ s.size++;
+ while (s.size--) {
+ int maxy = -1;
+ int obj;
+ while ((obj = scratch[bins[s.size]]) >= 0) {
+ int bottom = y + in[obj].y;
+ if (bottom > s.bottom)
+ break;
+ int right = s.x + in[obj].x;
+ if (right > w)
+ break;
+ bins[s.size]++;
+ out[obj] = (struct pos){s.x, y};
+ num_rects--;
+ if (maxy < 0)
+ stack[stackpos++] = s;
+ s.x = right;
+ maxy = MPMAX(maxy, bottom);
+ }
+ *used_width = MPMAX(*used_width, s.x);
+ if (maxy > 0)
+ s.bottom = maxy;
+ }
+ }
+ return num_rects ? -1 : y;
+}
+
+int packer_pack(struct bitmap_packer *packer)
+{
+ if (packer->count == 0)
+ return 0;
+ int w_orig = packer->w, h_orig = packer->h;
+ struct pos *in = packer->in;
+ int xmax = 0, ymax = 0;
+ for (int i = 0; i < packer->count; i++) {
+ if (in[i].x <= 0 || in[i].y <= 0) {
+ in[i] = (struct pos){0, 0};
+ } else {
+ in[i].x += packer->padding * 2;
+ in[i].y += packer->padding * 2;
+ }
+ if (in[i].x < 0 || in [i].x > 65535 || in[i].y < 0 || in[i].y > 65535) {
+ fprintf(stderr, "Invalid OSD / subtitle bitmap size\n");
+ abort();
+ }
+ xmax = MPMAX(xmax, in[i].x);
+ ymax = MPMAX(ymax, in[i].y);
+ }
+ if (xmax > packer->w)
+ packer->w = 1 << (mp_log2(xmax - 1) + 1);
+ if (ymax > packer->h)
+ packer->h = 1 << (mp_log2(ymax - 1) + 1);
+ while (1) {
+ int used_width = 0;
+ int y = pack_rectangles(in, packer->result, packer->count,
+ packer->w, packer->h,
+ packer->scratch, &used_width);
+ if (y >= 0) {
+ packer->used_width = MPMIN(used_width, packer->w);
+ packer->used_height = MPMIN(y, packer->h);
+ assert(packer->w == 0 || IS_POWER_OF_2(packer->w));
+ assert(packer->h == 0 || IS_POWER_OF_2(packer->h));
+ if (packer->padding) {
+ for (int i = 0; i < packer->count; i++) {
+ packer->result[i].x += packer->padding;
+ packer->result[i].y += packer->padding;
+ }
+ }
+ return packer->w != w_orig || packer->h != h_orig;
+ }
+ int w_max = packer->w_max > 0 ? packer->w_max : INT_MAX;
+ int h_max = packer->h_max > 0 ? packer->h_max : INT_MAX;
+ if (packer->w <= packer->h && packer->w != w_max)
+ packer->w = MPMIN(packer->w * 2, w_max);
+ else if (packer->h != h_max)
+ packer->h = MPMIN(packer->h * 2, h_max);
+ else {
+ packer->w = w_orig;
+ packer->h = h_orig;
+ return -1;
+ }
+ }
+}
+
+void packer_set_size(struct bitmap_packer *packer, int size)
+{
+ packer->count = size;
+ if (size <= packer->asize)
+ return;
+ packer->asize = MPMAX(packer->asize * 2, size);
+ talloc_free(packer->result);
+ talloc_free(packer->scratch);
+ packer->in = talloc_realloc(packer, packer->in, struct pos, packer->asize);
+ packer->result = talloc_array_ptrtype(packer, packer->result,
+ packer->asize);
+ packer->scratch = talloc_array_ptrtype(packer, packer->scratch,
+ packer->asize + 16);
+}
diff --git a/video/out/bitmap_packer.h b/video/out/bitmap_packer.h
new file mode 100644
index 0000000..97bf88f
--- /dev/null
+++ b/video/out/bitmap_packer.h
@@ -0,0 +1,51 @@
+#ifndef MPLAYER_PACK_RECTANGLES_H
+#define MPLAYER_PACK_RECTANGLES_H
+
+struct pos {
+ int x;
+ int y;
+};
+
+struct bitmap_packer {
+ int w;
+ int h;
+ int w_max;
+ int h_max;
+ int padding;
+ int count;
+ struct pos *in;
+ struct pos *result;
+ int used_width;
+ int used_height;
+
+ // internal
+ int *scratch;
+ int asize;
+};
+
+struct sub_bitmaps;
+
+// Clear all internal state. Leave the following fields: w_max, h_max
+void packer_reset(struct bitmap_packer *packer);
+
+// Get the bounding box used for bitmap data (including padding).
+// The bounding box doesn't exceed (0,0)-(packer->w,packer->h).
+void packer_get_bb(struct bitmap_packer *packer, struct pos out_bb[2]);
+
+/* Reallocate packer->in for at least to desired number of items.
+ * Also sets packer->count to the same value.
+ */
+void packer_set_size(struct bitmap_packer *packer, int size);
+
+/* To use this, set packer->count to number of rectangles, w_max and h_max
+ * to maximum output rectangle size, and w and h to start size (may be 0).
+ * Write input sizes in packer->in.
+ * Resulting packing will be written in packer->result.
+ * w and h will be increased if necessary for successful packing.
+ * There is a strong guarantee that w and h will be powers of 2 (or set to 0).
+ * Return value is -1 if packing failed because w and h were set to max
+ * values but that wasn't enough, 1 if w or h was increased, and 0 otherwise.
+ */
+int packer_pack(struct bitmap_packer *packer);
+
+#endif
diff --git a/video/out/cocoa_cb_common.swift b/video/out/cocoa_cb_common.swift
new file mode 100644
index 0000000..9c0054a
--- /dev/null
+++ b/video/out/cocoa_cb_common.swift
@@ -0,0 +1,230 @@
+/*
+ * 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/>.
+ */
+
+import Cocoa
+
+class CocoaCB: Common {
+ var libmpv: LibmpvHelper
+ var layer: GLLayer?
+
+ @objc var isShuttingDown: Bool = false
+
+ enum State {
+ case uninitialized
+ case needsInit
+ case initialized
+ }
+ var backendState: State = .uninitialized
+
+
+ @objc init(_ mpvHandle: OpaquePointer) {
+ let newlog = mp_log_new(UnsafeMutablePointer<MPContext>(mpvHandle), mp_client_get_log(mpvHandle), "cocoacb")
+ libmpv = LibmpvHelper(mpvHandle, newlog)
+ super.init(newlog)
+ layer = GLLayer(cocoaCB: self)
+ }
+
+ func preinit(_ vo: UnsafeMutablePointer<vo>) {
+ mpv = MPVHelper(vo, log)
+
+ if backendState == .uninitialized {
+ backendState = .needsInit
+
+ guard let layer = self.layer else {
+ log.sendError("Something went wrong, no GLLayer was initialized")
+ exit(1)
+ }
+
+ initView(vo, layer)
+ initMisc(vo)
+ }
+ }
+
+ func uninit() {
+ window?.orderOut(nil)
+ window?.close()
+ mpv = nil
+ }
+
+ func reconfig(_ vo: UnsafeMutablePointer<vo>) {
+ mpv?.vo = vo
+ if backendState == .needsInit {
+ DispatchQueue.main.sync { self.initBackend(vo) }
+ } else {
+ DispatchQueue.main.async {
+ self.updateWindowSize(vo)
+ self.layer?.update(force: true)
+ }
+ }
+ }
+
+ func initBackend(_ vo: UnsafeMutablePointer<vo>) {
+ let previousActiveApp = getActiveApp()
+ initApp()
+ initWindow(vo, previousActiveApp)
+ updateICCProfile()
+ initWindowState()
+
+ backendState = .initialized
+ }
+
+ func updateWindowSize(_ vo: UnsafeMutablePointer<vo>) {
+ guard let targetScreen = getTargetScreen(forFullscreen: false) ?? NSScreen.main else
+ {
+ log.sendWarning("Couldn't update Window size, no Screen available")
+ return
+ }
+
+ let wr = getWindowGeometry(forScreen: targetScreen, videoOut: vo)
+ if !(window?.isVisible ?? false) &&
+ !(window?.isMiniaturized ?? false) &&
+ !NSApp.isHidden
+ {
+ window?.makeKeyAndOrderFront(nil)
+ }
+ layer?.atomicDrawingStart()
+ window?.updateSize(wr.size)
+ }
+
+ override func displayLinkCallback(_ displayLink: CVDisplayLink,
+ _ inNow: UnsafePointer<CVTimeStamp>,
+ _ inOutputTime: UnsafePointer<CVTimeStamp>,
+ _ flagsIn: CVOptionFlags,
+ _ flagsOut: UnsafeMutablePointer<CVOptionFlags>) -> CVReturn
+ {
+ libmpv.reportRenderFlip()
+ return kCVReturnSuccess
+ }
+
+ override func lightSensorUpdate() {
+ libmpv.setRenderLux(lmuToLux(lastLmu))
+ }
+
+ override func updateICCProfile() {
+ guard let colorSpace = window?.screen?.colorSpace else {
+ log.sendWarning("Couldn't update ICC Profile, no color space available")
+ return
+ }
+
+ libmpv.setRenderICCProfile(colorSpace)
+ layer?.colorspace = colorSpace.cgColorSpace
+ }
+
+ override func windowDidEndAnimation() {
+ layer?.update()
+ checkShutdown()
+ }
+
+ override func windowSetToFullScreen() {
+ layer?.update(force: true)
+ }
+
+ override func windowSetToWindow() {
+ layer?.update(force: true)
+ }
+
+ override func windowDidUpdateFrame() {
+ layer?.update(force: true)
+ }
+
+ override func windowDidChangeScreen() {
+ layer?.update(force: true)
+ }
+
+ override func windowDidChangeScreenProfile() {
+ layer?.needsICCUpdate = true
+ }
+
+ override func windowDidChangeBackingProperties() {
+ layer?.contentsScale = window?.backingScaleFactor ?? 1
+ }
+
+ override func windowWillStartLiveResize() {
+ layer?.inLiveResize = true
+ }
+
+ override func windowDidEndLiveResize() {
+ layer?.inLiveResize = false
+ }
+
+ override func windowDidChangeOcclusionState() {
+ layer?.update(force: true)
+ }
+
+ var controlCallback: mp_render_cb_control_fn = { ( v, ctx, e, request, data ) -> Int32 in
+ let ccb = unsafeBitCast(ctx, to: CocoaCB.self)
+
+ guard let vo = v, let events = e else {
+ ccb.log.sendWarning("Unexpected nil value in Control Callback")
+ return VO_FALSE
+ }
+
+ return ccb.control(vo, events: events, request: request, data: data)
+ }
+
+ override func control(_ vo: UnsafeMutablePointer<vo>,
+ events: UnsafeMutablePointer<Int32>,
+ request: UInt32,
+ data: UnsafeMutableRawPointer?) -> Int32
+ {
+ switch mp_voctrl(request) {
+ case VOCTRL_PREINIT:
+ DispatchQueue.main.sync { self.preinit(vo) }
+ return VO_TRUE
+ case VOCTRL_UNINIT:
+ DispatchQueue.main.async { self.uninit() }
+ return VO_TRUE
+ case VOCTRL_RECONFIG:
+ reconfig(vo)
+ return VO_TRUE
+ default:
+ break
+ }
+
+ return super.control(vo, events: events, request: request, data: data)
+ }
+
+ func shutdown(_ destroy: Bool = false) {
+ isShuttingDown = window?.isAnimating ?? false ||
+ window?.isInFullscreen ?? false && mpv?.opts.native_fs ?? true
+ if window?.isInFullscreen ?? false && !(window?.isAnimating ?? false) {
+ window?.close()
+ }
+ if isShuttingDown { return }
+
+ uninit()
+ uninitCommon()
+
+ libmpv.deinitRender()
+ libmpv.deinitMPV(destroy)
+ }
+
+ func checkShutdown() {
+ if isShuttingDown {
+ shutdown(true)
+ }
+ }
+
+ @objc func processEvent(_ event: UnsafePointer<mpv_event>) {
+ switch event.pointee.event_id {
+ case MPV_EVENT_SHUTDOWN:
+ shutdown()
+ default:
+ break
+ }
+ }
+}
diff --git a/video/out/d3d11/context.c b/video/out/d3d11/context.c
new file mode 100644
index 0000000..05f04fd
--- /dev/null
+++ b/video/out/d3d11/context.c
@@ -0,0 +1,566 @@
+/*
+ * 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 "common/msg.h"
+#include "options/m_config.h"
+#include "osdep/timer.h"
+#include "osdep/windows_utils.h"
+
+#include "video/out/gpu/context.h"
+#include "video/out/gpu/d3d11_helpers.h"
+#include "video/out/gpu/spirv.h"
+#include "video/out/w32_common.h"
+#include "context.h"
+#include "ra_d3d11.h"
+
+static int d3d11_validate_adapter(struct mp_log *log,
+ const struct m_option *opt,
+ struct bstr name, const char **value);
+
+struct d3d11_opts {
+ int feature_level;
+ int warp;
+ bool flip;
+ int sync_interval;
+ char *adapter_name;
+ int output_format;
+ int color_space;
+ bool exclusive_fs;
+};
+
+#define OPT_BASE_STRUCT struct d3d11_opts
+const struct m_sub_options d3d11_conf = {
+ .opts = (const struct m_option[]) {
+ {"d3d11-warp", OPT_CHOICE(warp,
+ {"auto", -1},
+ {"no", 0},
+ {"yes", 1})},
+ {"d3d11-feature-level", OPT_CHOICE(feature_level,
+ {"12_1", D3D_FEATURE_LEVEL_12_1},
+ {"12_0", D3D_FEATURE_LEVEL_12_0},
+ {"11_1", D3D_FEATURE_LEVEL_11_1},
+ {"11_0", D3D_FEATURE_LEVEL_11_0},
+ {"10_1", D3D_FEATURE_LEVEL_10_1},
+ {"10_0", D3D_FEATURE_LEVEL_10_0},
+ {"9_3", D3D_FEATURE_LEVEL_9_3},
+ {"9_2", D3D_FEATURE_LEVEL_9_2},
+ {"9_1", D3D_FEATURE_LEVEL_9_1})},
+ {"d3d11-flip", OPT_BOOL(flip)},
+ {"d3d11-sync-interval", OPT_INT(sync_interval), M_RANGE(0, 4)},
+ {"d3d11-adapter", OPT_STRING_VALIDATE(adapter_name,
+ d3d11_validate_adapter)},
+ {"d3d11-output-format", OPT_CHOICE(output_format,
+ {"auto", DXGI_FORMAT_UNKNOWN},
+ {"rgba8", DXGI_FORMAT_R8G8B8A8_UNORM},
+ {"bgra8", DXGI_FORMAT_B8G8R8A8_UNORM},
+ {"rgb10_a2", DXGI_FORMAT_R10G10B10A2_UNORM},
+ {"rgba16f", DXGI_FORMAT_R16G16B16A16_FLOAT})},
+ {"d3d11-output-csp", OPT_CHOICE(color_space,
+ {"auto", -1},
+ {"srgb", DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709},
+ {"linear", DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709},
+ {"pq", DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020},
+ {"bt.2020", DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020})},
+ {"d3d11-exclusive-fs", OPT_BOOL(exclusive_fs)},
+ {0}
+ },
+ .defaults = &(const struct d3d11_opts) {
+ .feature_level = D3D_FEATURE_LEVEL_12_1,
+ .warp = -1,
+ .flip = true,
+ .sync_interval = 1,
+ .adapter_name = NULL,
+ .output_format = DXGI_FORMAT_UNKNOWN,
+ .color_space = -1,
+ },
+ .size = sizeof(struct d3d11_opts)
+};
+
+struct priv {
+ struct d3d11_opts *opts;
+ struct m_config_cache *opts_cache;
+
+ struct mp_vo_opts *vo_opts;
+ struct m_config_cache *vo_opts_cache;
+
+ struct ra_tex *backbuffer;
+ ID3D11Device *device;
+ IDXGISwapChain *swapchain;
+ struct mp_colorspace swapchain_csp;
+
+ int64_t perf_freq;
+ unsigned sync_refresh_count;
+ int64_t sync_qpc_time;
+ int64_t vsync_duration_qpc;
+ int64_t last_submit_qpc;
+};
+
+static int d3d11_validate_adapter(struct mp_log *log,
+ const struct m_option *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ bool help = bstr_equals0(param, "help");
+ bool adapter_matched = false;
+ struct bstr listing = { 0 };
+
+ if (bstr_equals0(param, "")) {
+ return 0;
+ }
+
+ adapter_matched = mp_d3d11_list_or_verify_adapters(log,
+ help ? bstr0(NULL) : param,
+ help ? &listing : NULL);
+
+ if (help) {
+ mp_info(log, "Available D3D11 adapters:\n%.*s",
+ BSTR_P(listing));
+ talloc_free(listing.start);
+ return M_OPT_EXIT;
+ }
+
+ if (!adapter_matched) {
+ mp_err(log, "No adapter matching '%.*s'!\n", BSTR_P(param));
+ }
+
+ return adapter_matched ? 0 : M_OPT_INVALID;
+}
+
+static struct ra_tex *get_backbuffer(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ID3D11Texture2D *backbuffer = NULL;
+ struct ra_tex *tex = NULL;
+ HRESULT hr;
+
+ hr = IDXGISwapChain_GetBuffer(p->swapchain, 0, &IID_ID3D11Texture2D,
+ (void**)&backbuffer);
+ if (FAILED(hr)) {
+ MP_ERR(ctx, "Couldn't get swapchain image\n");
+ goto done;
+ }
+
+ tex = ra_d3d11_wrap_tex(ctx->ra, (ID3D11Resource *)backbuffer);
+done:
+ SAFE_RELEASE(backbuffer);
+ return tex;
+}
+
+static bool resize(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ HRESULT hr;
+
+ if (p->backbuffer) {
+ MP_ERR(ctx, "Attempt at resizing while a frame was in progress!\n");
+ return false;
+ }
+
+ hr = IDXGISwapChain_ResizeBuffers(p->swapchain, 0, ctx->vo->dwidth,
+ ctx->vo->dheight, DXGI_FORMAT_UNKNOWN, 0);
+ if (FAILED(hr)) {
+ MP_FATAL(ctx, "Couldn't resize swapchain: %s\n", mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ return true;
+}
+
+static bool d3d11_reconfig(struct ra_ctx *ctx)
+{
+ vo_w32_config(ctx->vo);
+ return resize(ctx);
+}
+
+static int d3d11_color_depth(struct ra_swapchain *sw)
+{
+ struct priv *p = sw->priv;
+ DXGI_SWAP_CHAIN_DESC desc;
+
+ HRESULT hr = IDXGISwapChain_GetDesc(p->swapchain, &desc);
+ if (FAILED(hr)) {
+ MP_ERR(sw->ctx, "Failed to query swap chain description: %s!\n",
+ mp_HRESULT_to_str(hr));
+ return 0;
+ }
+
+ const struct ra_format *ra_fmt =
+ ra_d3d11_get_ra_format(sw->ctx->ra, desc.BufferDesc.Format);
+ if (!ra_fmt)
+ return 0;
+
+ return ra_fmt->component_depth[0];
+}
+
+static bool d3d11_start_frame(struct ra_swapchain *sw, struct ra_fbo *out_fbo)
+{
+ struct priv *p = sw->priv;
+
+ if (!out_fbo)
+ return true;
+
+ assert(!p->backbuffer);
+
+ p->backbuffer = get_backbuffer(sw->ctx);
+ if (!p->backbuffer)
+ return false;
+
+ *out_fbo = (struct ra_fbo) {
+ .tex = p->backbuffer,
+ .flip = false,
+ .color_space = p->swapchain_csp
+ };
+ return true;
+}
+
+static bool d3d11_submit_frame(struct ra_swapchain *sw,
+ const struct vo_frame *frame)
+{
+ struct priv *p = sw->priv;
+
+ ra_d3d11_flush(sw->ctx->ra);
+ ra_tex_free(sw->ctx->ra, &p->backbuffer);
+ return true;
+}
+
+static int64_t qpc_to_ns(struct ra_swapchain *sw, int64_t qpc)
+{
+ struct priv *p = sw->priv;
+
+ // Convert QPC units (1/perf_freq seconds) to nanoseconds. This will work
+ // without overflow because the QPC value is guaranteed not to roll-over
+ // within 100 years, so perf_freq must be less than 2.9*10^9.
+ return qpc / p->perf_freq * INT64_C(1000000000) +
+ qpc % p->perf_freq * INT64_C(1000000000) / p->perf_freq;
+}
+
+static int64_t qpc_ns_now(struct ra_swapchain *sw)
+{
+ LARGE_INTEGER perf_count;
+ QueryPerformanceCounter(&perf_count);
+ return qpc_to_ns(sw, perf_count.QuadPart);
+}
+
+static void d3d11_swap_buffers(struct ra_swapchain *sw)
+{
+ struct priv *p = sw->priv;
+
+ m_config_cache_update(p->opts_cache);
+
+ LARGE_INTEGER perf_count;
+ QueryPerformanceCounter(&perf_count);
+ p->last_submit_qpc = perf_count.QuadPart;
+
+ IDXGISwapChain_Present(p->swapchain, p->opts->sync_interval, 0);
+}
+
+static void d3d11_get_vsync(struct ra_swapchain *sw, struct vo_vsync_info *info)
+{
+ struct priv *p = sw->priv;
+ HRESULT hr;
+
+ m_config_cache_update(p->opts_cache);
+
+ // The calculations below are only valid if mpv presents on every vsync
+ if (p->opts->sync_interval != 1)
+ return;
+
+ // They're also only valid for flip model swapchains
+ DXGI_SWAP_CHAIN_DESC desc;
+ hr = IDXGISwapChain_GetDesc(p->swapchain, &desc);
+ if (FAILED(hr) || (desc.SwapEffect != DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL &&
+ desc.SwapEffect != DXGI_SWAP_EFFECT_FLIP_DISCARD))
+ {
+ return;
+ }
+
+ // GetLastPresentCount returns a sequential ID for the frame submitted by
+ // the last call to IDXGISwapChain::Present()
+ UINT submit_count;
+ hr = IDXGISwapChain_GetLastPresentCount(p->swapchain, &submit_count);
+ if (FAILED(hr))
+ return;
+
+ // GetFrameStatistics returns two pairs. The first is (PresentCount,
+ // PresentRefreshCount) which relates a present ID (on the same timeline as
+ // GetLastPresentCount) to the physical vsync it was displayed on. The
+ // second is (SyncRefreshCount, SyncQPCTime), which relates a physical vsync
+ // to a timestamp on the same clock as QueryPerformanceCounter.
+ DXGI_FRAME_STATISTICS stats;
+ hr = IDXGISwapChain_GetFrameStatistics(p->swapchain, &stats);
+ if (hr == DXGI_ERROR_FRAME_STATISTICS_DISJOINT) {
+ p->sync_refresh_count = 0;
+ p->sync_qpc_time = 0;
+ }
+ if (FAILED(hr))
+ return;
+
+ info->last_queue_display_time = 0;
+ info->vsync_duration = 0;
+ // Detecting skipped vsyncs is possible but not supported yet
+ info->skipped_vsyncs = -1;
+
+ // Get the number of physical vsyncs that have passed since the start of the
+ // playback or disjoint event.
+ // Check for 0 here, since sometimes GetFrameStatistics returns S_OK but
+ // with 0s in some (all?) members of DXGI_FRAME_STATISTICS.
+ unsigned src_passed = 0;
+ if (stats.SyncRefreshCount && p->sync_refresh_count)
+ src_passed = stats.SyncRefreshCount - p->sync_refresh_count;
+ if (p->sync_refresh_count == 0)
+ p->sync_refresh_count = stats.SyncRefreshCount;
+
+ // Get the elapsed time passed between the above vsyncs
+ unsigned sqt_passed = 0;
+ if (stats.SyncQPCTime.QuadPart && p->sync_qpc_time)
+ sqt_passed = stats.SyncQPCTime.QuadPart - p->sync_qpc_time;
+ if (p->sync_qpc_time == 0)
+ p->sync_qpc_time = stats.SyncQPCTime.QuadPart;
+
+ // If any vsyncs have passed, estimate the physical frame rate
+ if (src_passed && sqt_passed)
+ p->vsync_duration_qpc = sqt_passed / src_passed;
+ if (p->vsync_duration_qpc)
+ info->vsync_duration = qpc_to_ns(sw, p->vsync_duration_qpc);
+
+ // If the physical frame rate is known and the other members of
+ // DXGI_FRAME_STATISTICS are non-0, estimate the timing of the next frame
+ if (p->vsync_duration_qpc && stats.PresentCount &&
+ stats.PresentRefreshCount && stats.SyncRefreshCount &&
+ stats.SyncQPCTime.QuadPart)
+ {
+ // It's not clear if PresentRefreshCount and SyncRefreshCount can refer
+ // to different frames, but in case they can, assuming mpv presents on
+ // every frame, guess the present count that relates to SyncRefreshCount.
+ unsigned expected_sync_pc = stats.PresentCount +
+ (stats.SyncRefreshCount - stats.PresentRefreshCount);
+
+ // Now guess the timestamp of the last submitted frame based on the
+ // timestamp of the frame at SyncRefreshCount and the frame rate
+ int queued_frames = submit_count - expected_sync_pc;
+ int64_t last_queue_display_time_qpc = stats.SyncQPCTime.QuadPart +
+ queued_frames * p->vsync_duration_qpc;
+
+ // Only set the estimated display time if it's after the last submission
+ // time. It could be before if mpv skips a lot of frames.
+ if (last_queue_display_time_qpc >= p->last_submit_qpc) {
+ info->last_queue_display_time = mp_time_ns() +
+ (qpc_to_ns(sw, last_queue_display_time_qpc) - qpc_ns_now(sw));
+ }
+ }
+}
+
+static bool d3d11_set_fullscreen(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ HRESULT hr;
+
+ m_config_cache_update(p->opts_cache);
+
+ if (!p->swapchain) {
+ MP_ERR(ctx, "Full screen configuration was requested before D3D11 "
+ "swap chain was ready!");
+ return false;
+ }
+
+ // we only want exclusive FS if we are entering FS and
+ // exclusive FS is enabled. Otherwise disable exclusive FS.
+ bool enable_exclusive_fs = p->vo_opts->fullscreen &&
+ p->opts->exclusive_fs;
+
+ MP_VERBOSE(ctx, "%s full-screen exclusive mode while %s fullscreen\n",
+ enable_exclusive_fs ? "Enabling" : "Disabling",
+ ctx->vo->opts->fullscreen ? "entering" : "leaving");
+
+ hr = IDXGISwapChain_SetFullscreenState(p->swapchain,
+ enable_exclusive_fs, NULL);
+ if (FAILED(hr))
+ return false;
+
+ if (!resize(ctx))
+ return false;
+
+ return true;
+}
+
+static int d3d11_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ struct priv *p = ctx->priv;
+ int ret = -1;
+ bool fullscreen_switch_needed = false;
+
+ switch (request) {
+ case VOCTRL_VO_OPTS_CHANGED: {
+ void *changed_option;
+
+ while (m_config_cache_get_next_changed(p->vo_opts_cache,
+ &changed_option))
+ {
+ struct mp_vo_opts *vo_opts = p->vo_opts_cache->opts;
+
+ if (changed_option == &vo_opts->fullscreen) {
+ fullscreen_switch_needed = true;
+ }
+ }
+
+ break;
+ }
+ default:
+ break;
+ }
+
+ // if leaving full screen, handle d3d11 stuff first, then general
+ // windowing
+ if (fullscreen_switch_needed && !p->vo_opts->fullscreen) {
+ if (!d3d11_set_fullscreen(ctx))
+ return VO_FALSE;
+
+ fullscreen_switch_needed = false;
+ }
+
+ ret = vo_w32_control(ctx->vo, events, request, arg);
+
+ // if entering full screen, handle d3d11 after general windowing stuff
+ if (fullscreen_switch_needed && p->vo_opts->fullscreen) {
+ if (!d3d11_set_fullscreen(ctx))
+ return VO_FALSE;
+
+ fullscreen_switch_needed = false;
+ }
+
+ if (*events & VO_EVENT_RESIZE) {
+ if (!resize(ctx))
+ return VO_ERROR;
+ }
+ return ret;
+}
+
+static void d3d11_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->swapchain)
+ IDXGISwapChain_SetFullscreenState(p->swapchain, FALSE, NULL);
+
+ if (ctx->ra)
+ ra_tex_free(ctx->ra, &p->backbuffer);
+ SAFE_RELEASE(p->swapchain);
+ vo_w32_uninit(ctx->vo);
+ SAFE_RELEASE(p->device);
+
+ // Destroy the RA last to prevent objects we hold from showing up in D3D's
+ // leak checker
+ if (ctx->ra)
+ ctx->ra->fns->destroy(ctx->ra);
+}
+
+static const struct ra_swapchain_fns d3d11_swapchain = {
+ .color_depth = d3d11_color_depth,
+ .start_frame = d3d11_start_frame,
+ .submit_frame = d3d11_submit_frame,
+ .swap_buffers = d3d11_swap_buffers,
+ .get_vsync = d3d11_get_vsync,
+};
+
+static bool d3d11_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ p->opts_cache = m_config_cache_alloc(ctx, ctx->global, &d3d11_conf);
+ p->opts = p->opts_cache->opts;
+
+ p->vo_opts_cache = m_config_cache_alloc(ctx, ctx->vo->global, &vo_sub_opts);
+ p->vo_opts = p->vo_opts_cache->opts;
+
+ LARGE_INTEGER perf_freq;
+ QueryPerformanceFrequency(&perf_freq);
+ p->perf_freq = perf_freq.QuadPart;
+
+ struct ra_swapchain *sw = ctx->swapchain = talloc_zero(ctx, struct ra_swapchain);
+ sw->priv = p;
+ sw->ctx = ctx;
+ sw->fns = &d3d11_swapchain;
+
+ struct d3d11_device_opts dopts = {
+ .debug = ctx->opts.debug,
+ .allow_warp = p->opts->warp != 0,
+ .force_warp = p->opts->warp == 1,
+ .max_feature_level = p->opts->feature_level,
+ .max_frame_latency = ctx->vo->opts->swapchain_depth,
+ .adapter_name = p->opts->adapter_name,
+ };
+ if (!mp_d3d11_create_present_device(ctx->log, &dopts, &p->device))
+ goto error;
+
+ if (!spirv_compiler_init(ctx))
+ goto error;
+ ctx->ra = ra_d3d11_create(p->device, ctx->log, ctx->spirv);
+ if (!ctx->ra)
+ goto error;
+
+ if (!vo_w32_init(ctx->vo))
+ goto error;
+
+ UINT usage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT;
+ if (ID3D11Device_GetFeatureLevel(p->device) >= D3D_FEATURE_LEVEL_11_0 &&
+ p->opts->output_format != DXGI_FORMAT_B8G8R8A8_UNORM)
+ {
+ usage |= DXGI_USAGE_UNORDERED_ACCESS;
+ }
+
+ struct d3d11_swapchain_opts scopts = {
+ .window = vo_w32_hwnd(ctx->vo),
+ .width = ctx->vo->dwidth,
+ .height = ctx->vo->dheight,
+ .format = p->opts->output_format,
+ .color_space = p->opts->color_space,
+ .configured_csp = &p->swapchain_csp,
+ .flip = p->opts->flip,
+ // Add one frame for the backbuffer and one frame of "slack" to reduce
+ // contention with the window manager when acquiring the backbuffer
+ .length = ctx->vo->opts->swapchain_depth + 2,
+ .usage = usage,
+ };
+ if (!mp_d3d11_create_swapchain(p->device, ctx->log, &scopts, &p->swapchain))
+ goto error;
+
+ return true;
+
+error:
+ d3d11_uninit(ctx);
+ return false;
+}
+
+IDXGISwapChain *ra_d3d11_ctx_get_swapchain(struct ra_ctx *ra)
+{
+ if (ra->swapchain->fns != &d3d11_swapchain)
+ return NULL;
+
+ struct priv *p = ra->priv;
+
+ IDXGISwapChain_AddRef(p->swapchain);
+
+ return p->swapchain;
+}
+
+const struct ra_ctx_fns ra_ctx_d3d11 = {
+ .type = "d3d11",
+ .name = "d3d11",
+ .reconfig = d3d11_reconfig,
+ .control = d3d11_control,
+ .init = d3d11_init,
+ .uninit = d3d11_uninit,
+};
diff --git a/video/out/d3d11/context.h b/video/out/d3d11/context.h
new file mode 100644
index 0000000..8a9ef4c
--- /dev/null
+++ b/video/out/d3d11/context.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include <dxgi.h>
+
+#include "video/out/gpu/context.h"
+
+// Get the underlying D3D11 swap chain from an RA context. The returned swap chain is
+// refcounted and must be released by the caller.
+IDXGISwapChain *ra_d3d11_ctx_get_swapchain(struct ra_ctx *ra);
diff --git a/video/out/d3d11/hwdec_d3d11va.c b/video/out/d3d11/hwdec_d3d11va.c
new file mode 100644
index 0000000..6aaa12b
--- /dev/null
+++ b/video/out/d3d11/hwdec_d3d11va.c
@@ -0,0 +1,258 @@
+/*
+ * 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 <windows.h>
+#include <d3d11.h>
+#include <d3d11_1.h>
+
+#include "common/common.h"
+#include "options/m_config.h"
+#include "osdep/windows_utils.h"
+#include "video/hwdec.h"
+#include "video/d3d.h"
+#include "video/out/d3d11/ra_d3d11.h"
+#include "video/out/gpu/hwdec.h"
+
+struct d3d11va_opts {
+ bool zero_copy;
+};
+
+#define OPT_BASE_STRUCT struct d3d11va_opts
+const struct m_sub_options d3d11va_conf = {
+ .opts = (const struct m_option[]) {
+ {"d3d11va-zero-copy", OPT_BOOL(zero_copy)},
+ {0}
+ },
+ .defaults = &(const struct d3d11va_opts) {0},
+ .size = sizeof(struct d3d11va_opts)
+};
+
+struct priv_owner {
+ struct d3d11va_opts *opts;
+
+ struct mp_hwdec_ctx hwctx;
+ ID3D11Device *device;
+ ID3D11Device1 *device1;
+};
+
+struct priv {
+ // 1-copy path
+ ID3D11DeviceContext1 *ctx;
+ ID3D11Texture2D *copy_tex;
+
+ // zero-copy path
+ int num_planes;
+ const struct ra_format *fmt[4];
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+ SAFE_RELEASE(p->device);
+ SAFE_RELEASE(p->device1);
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ HRESULT hr;
+
+ if (!ra_is_d3d11(hw->ra_ctx->ra))
+ return -1;
+ p->device = ra_d3d11_get_device(hw->ra_ctx->ra);
+ if (!p->device)
+ return -1;
+
+ p->opts = mp_get_config_group(hw->priv, hw->global, &d3d11va_conf);
+
+ // D3D11VA requires Direct3D 11.1, so this should always succeed
+ hr = ID3D11Device_QueryInterface(p->device, &IID_ID3D11Device1,
+ (void**)&p->device1);
+ if (FAILED(hr)) {
+ MP_ERR(hw, "Failed to get D3D11.1 interface: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ ID3D10Multithread *multithread;
+ hr = ID3D11Device_QueryInterface(p->device, &IID_ID3D10Multithread,
+ (void **)&multithread);
+ if (FAILED(hr)) {
+ MP_ERR(hw, "Failed to get Multithread interface: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+ ID3D10Multithread_SetMultithreadProtected(multithread, TRUE);
+ ID3D10Multithread_Release(multithread);
+
+ static const int subfmts[] = {IMGFMT_NV12, IMGFMT_P010, 0};
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = hw->driver->name,
+ .av_device_ref = d3d11_wrap_device_ref(p->device),
+ .supported_formats = subfmts,
+ .hw_imgfmt = IMGFMT_D3D11,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx\n");
+ return -1;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ for (int i = 0; i < 4; i++)
+ ra_tex_free(mapper->ra, &mapper->tex[i]);
+ SAFE_RELEASE(p->copy_tex);
+ SAFE_RELEASE(p->ctx);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *o = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+ HRESULT hr;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ struct ra_imgfmt_desc desc = {0};
+
+ if (!ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &desc))
+ return -1;
+
+ if (o->opts->zero_copy) {
+ // In the zero-copy path, we create the ra_tex objects in the map
+ // operation, so we just need to store the format of each plane
+ p->num_planes = desc.num_planes;
+ for (int i = 0; i < desc.num_planes; i++)
+ p->fmt[i] = desc.planes[i];
+ } else {
+ struct mp_image layout = {0};
+ mp_image_set_params(&layout, &mapper->dst_params);
+
+ DXGI_FORMAT copy_fmt;
+ switch (mapper->dst_params.imgfmt) {
+ case IMGFMT_NV12: copy_fmt = DXGI_FORMAT_NV12; break;
+ case IMGFMT_P010: copy_fmt = DXGI_FORMAT_P010; break;
+ default: return -1;
+ }
+
+ D3D11_TEXTURE2D_DESC copy_desc = {
+ .Width = mapper->dst_params.w,
+ .Height = mapper->dst_params.h,
+ .MipLevels = 1,
+ .ArraySize = 1,
+ .SampleDesc.Count = 1,
+ .Format = copy_fmt,
+ .BindFlags = D3D11_BIND_SHADER_RESOURCE,
+ };
+ hr = ID3D11Device_CreateTexture2D(o->device, &copy_desc, NULL,
+ &p->copy_tex);
+ if (FAILED(hr)) {
+ MP_FATAL(mapper, "Could not create shader resource texture\n");
+ return -1;
+ }
+
+ for (int i = 0; i < desc.num_planes; i++) {
+ mapper->tex[i] = ra_d3d11_wrap_tex_video(mapper->ra, p->copy_tex,
+ mp_image_plane_w(&layout, i), mp_image_plane_h(&layout, i), 0,
+ desc.planes[i]);
+ if (!mapper->tex[i]) {
+ MP_FATAL(mapper, "Could not create RA texture view\n");
+ return -1;
+ }
+ }
+
+ // A ref to the immediate context is needed for CopySubresourceRegion
+ ID3D11Device1_GetImmediateContext1(o->device1, &p->ctx);
+ }
+
+ return 0;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ ID3D11Texture2D *tex = (void *)mapper->src->planes[0];
+ int subresource = (intptr_t)mapper->src->planes[1];
+
+ if (p->copy_tex) {
+ ID3D11DeviceContext1_CopySubresourceRegion1(p->ctx,
+ (ID3D11Resource *)p->copy_tex, 0, 0, 0, 0,
+ (ID3D11Resource *)tex, subresource, (&(D3D11_BOX) {
+ .left = 0,
+ .top = 0,
+ .front = 0,
+ .right = mapper->dst_params.w,
+ .bottom = mapper->dst_params.h,
+ .back = 1,
+ }), D3D11_COPY_DISCARD);
+
+ // We no longer need the original texture after copying it.
+ mp_image_unrefp(&mapper->src);
+ } else {
+ D3D11_TEXTURE2D_DESC desc2d;
+ ID3D11Texture2D_GetDesc(tex, &desc2d);
+
+ for (int i = 0; i < p->num_planes; i++) {
+ // The video decode texture may include padding, so the size of the
+ // ra_tex needs to be determined by the actual size of the Tex2D
+ bool chroma = i >= 1;
+ int w = desc2d.Width / (chroma ? 2 : 1);
+ int h = desc2d.Height / (chroma ? 2 : 1);
+
+ mapper->tex[i] = ra_d3d11_wrap_tex_video(mapper->ra, tex,
+ w, h, subresource, p->fmt[i]);
+ if (!mapper->tex[i])
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ if (p->copy_tex)
+ return;
+ for (int i = 0; i < 4; i++)
+ ra_tex_free(mapper->ra, &mapper->tex[i]);
+}
+
+const struct ra_hwdec_driver ra_hwdec_d3d11va = {
+ .name = "d3d11va",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_D3D11, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/d3d11/hwdec_dxva2dxgi.c b/video/out/d3d11/hwdec_dxva2dxgi.c
new file mode 100644
index 0000000..62158d4
--- /dev/null
+++ b/video/out/d3d11/hwdec_dxva2dxgi.c
@@ -0,0 +1,478 @@
+/*
+ * 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 <windows.h>
+#include <d3d9.h>
+#include <d3d11.h>
+#include <dxva2api.h>
+
+#include "common/common.h"
+#include "osdep/windows_utils.h"
+#include "video/hwdec.h"
+#include "video/d3d.h"
+#include "video/out/d3d11/ra_d3d11.h"
+#include "video/out/gpu/hwdec.h"
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+ ID3D11Device *dev11;
+ IDirect3DDevice9Ex *dev9;
+};
+
+struct queue_surf {
+ ID3D11Texture2D *tex11;
+ ID3D11Query *idle11;
+ ID3D11Texture2D *stage11;
+ IDirect3DTexture9 *tex9;
+ IDirect3DSurface9 *surf9;
+ IDirect3DSurface9 *stage9;
+ struct ra_tex *tex;
+
+ bool busy11; // The surface is currently being used by D3D11
+};
+
+struct priv {
+ ID3D11Device *dev11;
+ ID3D11DeviceContext *ctx11;
+ IDirect3DDevice9Ex *dev9;
+
+ // Surface queue stuff. Following Microsoft recommendations, a queue of
+ // surfaces is used to share images between D3D9 and D3D11. This allows
+ // multiple D3D11 frames to be in-flight at once.
+ struct queue_surf **queue;
+ int queue_len;
+ int queue_pos;
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+ SAFE_RELEASE(p->dev11);
+ SAFE_RELEASE(p->dev9);
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ IDirect3D9Ex *d3d9ex = NULL;
+ int ret = -1;
+ HRESULT hr;
+
+ if (!ra_is_d3d11(hw->ra_ctx->ra))
+ goto done;
+ p->dev11 = ra_d3d11_get_device(hw->ra_ctx->ra);
+ if (!p->dev11)
+ goto done;
+
+ d3d_load_dlls();
+ if (!d3d9_dll) {
+ MP_FATAL(hw, "Failed to load \"d3d9.dll\": %s\n", mp_LastError_to_str());
+ goto done;
+ }
+ if (!dxva2_dll) {
+ MP_FATAL(hw, "Failed to load \"dxva2.dll\": %s\n", mp_LastError_to_str());
+ goto done;
+ }
+
+ HRESULT (WINAPI *Direct3DCreate9Ex)(UINT SDKVersion, IDirect3D9Ex **ppD3D);
+ Direct3DCreate9Ex = (void *)GetProcAddress(d3d9_dll, "Direct3DCreate9Ex");
+ if (!Direct3DCreate9Ex) {
+ MP_FATAL(hw, "Direct3D 9Ex not supported\n");
+ goto done;
+ }
+
+ hr = Direct3DCreate9Ex(D3D_SDK_VERSION, &d3d9ex);
+ if (FAILED(hr)) {
+ MP_FATAL(hw, "Couldn't create Direct3D9Ex: %s\n", mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ D3DPRESENT_PARAMETERS pparams = {
+ .BackBufferWidth = 16,
+ .BackBufferHeight = 16,
+ .BackBufferCount = 1,
+ .SwapEffect = D3DSWAPEFFECT_DISCARD,
+ .hDeviceWindow = GetDesktopWindow(),
+ .Windowed = TRUE,
+ .Flags = D3DPRESENTFLAG_VIDEO,
+ };
+ hr = IDirect3D9Ex_CreateDeviceEx(d3d9ex, D3DADAPTER_DEFAULT,
+ D3DDEVTYPE_HAL, GetDesktopWindow(), D3DCREATE_NOWINDOWCHANGES |
+ D3DCREATE_FPU_PRESERVE | D3DCREATE_HARDWARE_VERTEXPROCESSING |
+ D3DCREATE_DISABLE_PSGP_THREADING | D3DCREATE_MULTITHREADED, &pparams,
+ NULL, &p->dev9);
+ if (FAILED(hr)) {
+ MP_FATAL(hw, "Failed to create Direct3D9Ex device: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ // Check if it's possible to StretchRect() from NV12 to XRGB surfaces
+ hr = IDirect3D9Ex_CheckDeviceFormatConversion(d3d9ex, D3DADAPTER_DEFAULT,
+ D3DDEVTYPE_HAL, MAKEFOURCC('N', 'V', '1', '2'), D3DFMT_X8R8G8B8);
+ if (hr != S_OK) {
+ MP_FATAL(hw, "Can't StretchRect from NV12 to XRGB surfaces\n");
+ goto done;
+ }
+
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = hw->driver->name,
+ .av_device_ref = d3d9_wrap_device_ref((IDirect3DDevice9 *)p->dev9),
+ .hw_imgfmt = IMGFMT_DXVA2,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx\n");
+ goto done;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ ret = 0;
+done:
+ SAFE_RELEASE(d3d9ex);
+ return ret;
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *o = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+
+ ID3D11Device_AddRef(o->dev11);
+ p->dev11 = o->dev11;
+ IDirect3DDevice9Ex_AddRef(o->dev9);
+ p->dev9 = o->dev9;
+ ID3D11Device_GetImmediateContext(o->dev11, &p->ctx11);
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = IMGFMT_RGB0;
+ mapper->dst_params.hw_subfmt = 0;
+ return 0;
+}
+
+static void surf_destroy(struct ra_hwdec_mapper *mapper,
+ struct queue_surf *surf)
+{
+ if (!surf)
+ return;
+ SAFE_RELEASE(surf->tex11);
+ SAFE_RELEASE(surf->idle11);
+ SAFE_RELEASE(surf->stage11);
+ SAFE_RELEASE(surf->tex9);
+ SAFE_RELEASE(surf->surf9);
+ SAFE_RELEASE(surf->stage9);
+ ra_tex_free(mapper->ra, &surf->tex);
+ talloc_free(surf);
+}
+
+static struct queue_surf *surf_create(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ IDXGIResource *res11 = NULL;
+ bool success = false;
+ HRESULT hr;
+
+ struct queue_surf *surf = talloc_ptrtype(p, surf);
+
+ D3D11_TEXTURE2D_DESC desc11 = {
+ .Width = mapper->src->w,
+ .Height = mapper->src->h,
+ .MipLevels = 1,
+ .ArraySize = 1,
+ .Format = DXGI_FORMAT_B8G8R8X8_UNORM,
+ .SampleDesc.Count = 1,
+ .Usage = D3D11_USAGE_DEFAULT,
+ .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET,
+ .MiscFlags = D3D11_RESOURCE_MISC_SHARED,
+ };
+ hr = ID3D11Device_CreateTexture2D(p->dev11, &desc11, NULL, &surf->tex11);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to create D3D11 texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ // Try to use a 16x16 staging texture, unless the source surface is
+ // smaller. Ideally, a 1x1 texture would be sufficient, but Microsoft's
+ // D3D9ExDXGISharedSurf example uses 16x16 to avoid driver bugs.
+ D3D11_TEXTURE2D_DESC sdesc11 = {
+ .Width = MPMIN(16, desc11.Width),
+ .Height = MPMIN(16, desc11.Height),
+ .MipLevels = 1,
+ .ArraySize = 1,
+ .Format = DXGI_FORMAT_B8G8R8X8_UNORM,
+ .SampleDesc.Count = 1,
+ .Usage = D3D11_USAGE_STAGING,
+ .CPUAccessFlags = D3D11_CPU_ACCESS_READ,
+ };
+ hr = ID3D11Device_CreateTexture2D(p->dev11, &sdesc11, NULL, &surf->stage11);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to create D3D11 staging texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ hr = ID3D11Texture2D_QueryInterface(surf->tex11, &IID_IDXGIResource,
+ (void**)&res11);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to get share handle: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ HANDLE share_handle;
+ hr = IDXGIResource_GetSharedHandle(res11, &share_handle);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to get share handle: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ hr = ID3D11Device_CreateQuery(p->dev11,
+ &(D3D11_QUERY_DESC) { D3D11_QUERY_EVENT }, &surf->idle11);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to create D3D11 query: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ // Share the D3D11 texture with D3D9Ex
+ hr = IDirect3DDevice9Ex_CreateTexture(p->dev9, desc11.Width, desc11.Height,
+ 1, D3DUSAGE_RENDERTARGET, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT,
+ &surf->tex9, &share_handle);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to create D3D9 texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ hr = IDirect3DTexture9_GetSurfaceLevel(surf->tex9, 0, &surf->surf9);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to get D3D9 surface: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ // As above, try to use a 16x16 staging texture to avoid driver bugs
+ hr = IDirect3DDevice9Ex_CreateRenderTarget(p->dev9,
+ MPMIN(16, desc11.Width), MPMIN(16, desc11.Height), D3DFMT_X8R8G8B8,
+ D3DMULTISAMPLE_NONE, 0, TRUE, &surf->stage9, NULL);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to create D3D9 staging surface: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ surf->tex = ra_d3d11_wrap_tex(mapper->ra, (ID3D11Resource *)surf->tex11);
+ if (!surf->tex)
+ goto done;
+
+ success = true;
+done:
+ if (!success)
+ surf_destroy(mapper, surf);
+ SAFE_RELEASE(res11);
+ return success ? surf : NULL;
+}
+
+// true if the surface is currently in-use by the D3D11 graphics pipeline
+static bool surf_is_idle11(struct ra_hwdec_mapper *mapper,
+ struct queue_surf *surf)
+{
+ struct priv *p = mapper->priv;
+ HRESULT hr;
+ BOOL idle;
+
+ if (!surf->busy11)
+ return true;
+
+ hr = ID3D11DeviceContext_GetData(p->ctx11,
+ (ID3D11Asynchronous *)surf->idle11, &idle, sizeof(idle),
+ D3D11_ASYNC_GETDATA_DONOTFLUSH);
+ if (FAILED(hr) || hr == S_FALSE || !idle)
+ return false;
+
+ surf->busy11 = false;
+ return true;
+}
+
+// If the surface is currently in-use by the D3D11 graphics pipeline, wait for
+// it to become idle. Should only be called in the queue-underflow case.
+static bool surf_wait_idle11(struct ra_hwdec_mapper *mapper,
+ struct queue_surf *surf)
+{
+ struct priv *p = mapper->priv;
+ HRESULT hr;
+
+ ID3D11DeviceContext_CopySubresourceRegion(p->ctx11,
+ (ID3D11Resource *)surf->stage11, 0, 0, 0, 0,
+ (ID3D11Resource *)surf->tex11, 0, (&(D3D11_BOX){
+ .right = MPMIN(16, mapper->src->w),
+ .bottom = MPMIN(16, mapper->src->h),
+ .back = 1,
+ }));
+
+ // Block until the surface becomes idle (see surf_wait_idle9())
+ D3D11_MAPPED_SUBRESOURCE map = {0};
+ hr = ID3D11DeviceContext_Map(p->ctx11, (ID3D11Resource *)surf->stage11, 0,
+ D3D11_MAP_READ, 0, &map);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Couldn't map D3D11 staging texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ ID3D11DeviceContext_Unmap(p->ctx11, (ID3D11Resource *)surf->stage11, 0);
+ surf->busy11 = false;
+ return true;
+}
+
+static bool surf_wait_idle9(struct ra_hwdec_mapper *mapper,
+ struct queue_surf *surf)
+{
+ struct priv *p = mapper->priv;
+ HRESULT hr;
+
+ // Rather than polling for the surface to become idle, copy part of the
+ // surface to a staging texture and map it. This should block until the
+ // surface becomes idle. Microsoft's ISurfaceQueue does this as well.
+ RECT rc = {0, 0, MPMIN(16, mapper->src->w), MPMIN(16, mapper->src->h)};
+ hr = IDirect3DDevice9Ex_StretchRect(p->dev9, surf->surf9, &rc, surf->stage9,
+ &rc, D3DTEXF_NONE);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Couldn't copy to D3D9 staging texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ D3DLOCKED_RECT lock;
+ hr = IDirect3DSurface9_LockRect(surf->stage9, &lock, NULL, D3DLOCK_READONLY);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Couldn't map D3D9 staging texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ IDirect3DSurface9_UnlockRect(surf->stage9);
+ p->queue[p->queue_pos]->busy11 = true;
+ return true;
+}
+
+static struct queue_surf *surf_acquire(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ if (!p->queue_len || !surf_is_idle11(mapper, p->queue[p->queue_pos])) {
+ if (p->queue_len < 16) {
+ struct queue_surf *surf = surf_create(mapper);
+ if (!surf)
+ return NULL;
+
+ // The next surface is busy, so grow the queue
+ MP_TARRAY_INSERT_AT(p, p->queue, p->queue_len, p->queue_pos, surf);
+ MP_DBG(mapper, "Queue grew to %d surfaces\n", p->queue_len);
+ } else {
+ // For sanity, don't let the queue grow beyond 16 surfaces. It
+ // should never get this big. If it does, wait for the surface to
+ // become idle rather than polling it.
+ if (!surf_wait_idle11(mapper, p->queue[p->queue_pos]))
+ return NULL;
+ MP_WARN(mapper, "Queue underflow!\n");
+ }
+ }
+ return p->queue[p->queue_pos];
+}
+
+static void surf_release(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ ID3D11DeviceContext_End(p->ctx11,
+ (ID3D11Asynchronous *)p->queue[p->queue_pos]->idle11);
+
+ // The current surface is now in-flight, move to the next surface
+ p->queue_pos++;
+ if (p->queue_pos >= p->queue_len)
+ p->queue_pos = 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ for (int i = 0; i < p->queue_len; i++)
+ surf_destroy(mapper, p->queue[i]);
+
+ SAFE_RELEASE(p->ctx11);
+ SAFE_RELEASE(p->dev9);
+ SAFE_RELEASE(p->dev11);
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ HRESULT hr;
+
+ struct queue_surf *surf = surf_acquire(mapper);
+ if (!surf)
+ return -1;
+
+ RECT rc = {0, 0, mapper->src->w, mapper->src->h};
+ IDirect3DSurface9* hw_surface = (IDirect3DSurface9 *)mapper->src->planes[3];
+
+ hr = IDirect3DDevice9Ex_StretchRect(p->dev9, hw_surface, &rc, surf->surf9,
+ &rc, D3DTEXF_NONE);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "StretchRect() failed: %s\n", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ if (!surf_wait_idle9(mapper, surf))
+ return -1;
+
+ mapper->tex[0] = surf->tex;
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ if (p->queue_pos < p->queue_len &&
+ p->queue[p->queue_pos]->tex == mapper->tex[0])
+ {
+ surf_release(mapper);
+ mapper->tex[0] = NULL;
+ }
+}
+
+const struct ra_hwdec_driver ra_hwdec_dxva2dxgi = {
+ .name = "dxva2-dxgi",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_DXVA2, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/d3d11/ra_d3d11.c b/video/out/d3d11/ra_d3d11.c
new file mode 100644
index 0000000..84fd004
--- /dev/null
+++ b/video/out/d3d11/ra_d3d11.c
@@ -0,0 +1,2544 @@
+#include <windows.h>
+#include <versionhelpers.h>
+#include <d3d11_1.h>
+#include <d3d11sdklayers.h>
+#include <dxgi1_2.h>
+#include <d3dcompiler.h>
+#include <spirv_cross_c.h>
+
+#include "common/msg.h"
+#include "osdep/io.h"
+#include "osdep/subprocess.h"
+#include "osdep/timer.h"
+#include "osdep/windows_utils.h"
+#include "video/out/gpu/spirv.h"
+#include "video/out/gpu/utils.h"
+
+#include "ra_d3d11.h"
+
+#ifndef D3D11_1_UAV_SLOT_COUNT
+#define D3D11_1_UAV_SLOT_COUNT (64)
+#endif
+#define D3D11_FORMAT_SUPPORT2_UAV_TYPED_STORE (0x80)
+
+// D3D11.3 message IDs, not present in mingw-w64 v9
+#define D3D11_MESSAGE_ID_CREATE_FENCE ((D3D11_MESSAGE_ID)0x300209)
+#define D3D11_MESSAGE_ID_DESTROY_FENCE ((D3D11_MESSAGE_ID)0x30020b)
+
+struct dll_version {
+ uint16_t major;
+ uint16_t minor;
+ uint16_t build;
+ uint16_t revision;
+};
+
+struct ra_d3d11 {
+ struct spirv_compiler *spirv;
+
+ ID3D11Device *dev;
+ ID3D11Device1 *dev1;
+ ID3D11DeviceContext *ctx;
+ ID3D11DeviceContext1 *ctx1;
+ pD3DCompile D3DCompile;
+
+ struct dll_version d3d_compiler_ver;
+
+ // Debug interfaces (--gpu-debug)
+ ID3D11Debug *debug;
+ ID3D11InfoQueue *iqueue;
+
+ // Device capabilities
+ D3D_FEATURE_LEVEL fl;
+ bool has_clear_view;
+ bool has_timestamp_queries;
+ int max_uavs;
+
+ // Streaming dynamic vertex buffer, which is used for all renderpasses
+ ID3D11Buffer *vbuf;
+ size_t vbuf_size;
+ size_t vbuf_used;
+
+ // clear() renderpass resources (only used when has_clear_view is false)
+ ID3D11PixelShader *clear_ps;
+ ID3D11VertexShader *clear_vs;
+ ID3D11InputLayout *clear_layout;
+ ID3D11Buffer *clear_vbuf;
+ ID3D11Buffer *clear_cbuf;
+
+ // blit() renderpass resources
+ ID3D11PixelShader *blit_float_ps;
+ ID3D11VertexShader *blit_vs;
+ ID3D11InputLayout *blit_layout;
+ ID3D11Buffer *blit_vbuf;
+ ID3D11SamplerState *blit_sampler;
+};
+
+struct d3d_tex {
+ // res mirrors one of tex1d, tex2d or tex3d for convenience. It does not
+ // hold an additional reference to the texture object.
+ ID3D11Resource *res;
+
+ ID3D11Texture1D *tex1d;
+ ID3D11Texture2D *tex2d;
+ ID3D11Texture3D *tex3d;
+ int array_slice;
+
+ // Staging texture for tex_download(), 2D only
+ ID3D11Texture2D *staging;
+
+ ID3D11ShaderResourceView *srv;
+ ID3D11RenderTargetView *rtv;
+ ID3D11UnorderedAccessView *uav;
+ ID3D11SamplerState *sampler;
+};
+
+struct d3d_buf {
+ ID3D11Buffer *buf;
+ ID3D11UnorderedAccessView *uav;
+ void *data; // System-memory mirror of the data in buf
+ bool dirty; // Is buf out of date?
+};
+
+struct d3d_rpass {
+ ID3D11PixelShader *ps;
+ ID3D11VertexShader *vs;
+ ID3D11ComputeShader *cs;
+ ID3D11InputLayout *layout;
+ ID3D11BlendState *bstate;
+};
+
+struct d3d_timer {
+ ID3D11Query *ts_start;
+ ID3D11Query *ts_end;
+ ID3D11Query *disjoint;
+ uint64_t result; // Latches the result from the previous use of the timer
+};
+
+struct d3d_fmt {
+ const char *name;
+ int components;
+ int bytes;
+ int bits[4];
+ DXGI_FORMAT fmt;
+ enum ra_ctype ctype;
+ bool unordered;
+};
+
+static const char clear_vs[] = "\
+float4 main(float2 pos : POSITION) : SV_Position\n\
+{\n\
+ return float4(pos, 0.0, 1.0);\n\
+}\n\
+";
+
+static const char clear_ps[] = "\
+cbuffer ps_cbuf : register(b0) {\n\
+ float4 color : packoffset(c0);\n\
+}\n\
+\n\
+float4 main(float4 pos : SV_Position) : SV_Target\n\
+{\n\
+ return color;\n\
+}\n\
+";
+
+struct blit_vert {
+ float x, y, u, v;
+};
+
+static const char blit_vs[] = "\
+void main(float2 pos : POSITION, float2 coord : TEXCOORD0,\n\
+ out float4 out_pos : SV_Position, out float2 out_coord : TEXCOORD0)\n\
+{\n\
+ out_pos = float4(pos, 0.0, 1.0);\n\
+ out_coord = coord;\n\
+}\n\
+";
+
+static const char blit_float_ps[] = "\
+Texture2D<float4> tex : register(t0);\n\
+SamplerState samp : register(s0);\n\
+\n\
+float4 main(float4 pos : SV_Position, float2 coord : TEXCOORD0) : SV_Target\n\
+{\n\
+ return tex.Sample(samp, coord);\n\
+}\n\
+";
+
+#define DXFMT(f, t) .fmt = DXGI_FORMAT_##f##_##t, .ctype = RA_CTYPE_##t
+static struct d3d_fmt formats[] = {
+ { "r8", 1, 1, { 8}, DXFMT(R8, UNORM) },
+ { "rg8", 2, 2, { 8, 8}, DXFMT(R8G8, UNORM) },
+ { "rgba8", 4, 4, { 8, 8, 8, 8}, DXFMT(R8G8B8A8, UNORM) },
+ { "r16", 1, 2, {16}, DXFMT(R16, UNORM) },
+ { "rg16", 2, 4, {16, 16}, DXFMT(R16G16, UNORM) },
+ { "rgba16", 4, 8, {16, 16, 16, 16}, DXFMT(R16G16B16A16, UNORM) },
+
+ { "r32ui", 1, 4, {32}, DXFMT(R32, UINT) },
+ { "rg32ui", 2, 8, {32, 32}, DXFMT(R32G32, UINT) },
+ { "rgb32ui", 3, 12, {32, 32, 32}, DXFMT(R32G32B32, UINT) },
+ { "rgba32ui", 4, 16, {32, 32, 32, 32}, DXFMT(R32G32B32A32, UINT) },
+
+ { "r16hf", 1, 2, {16}, DXFMT(R16, FLOAT) },
+ { "rg16hf", 2, 4, {16, 16}, DXFMT(R16G16, FLOAT) },
+ { "rgba16hf", 4, 8, {16, 16, 16, 16}, DXFMT(R16G16B16A16, FLOAT) },
+ { "r32f", 1, 4, {32}, DXFMT(R32, FLOAT) },
+ { "rg32f", 2, 8, {32, 32}, DXFMT(R32G32, FLOAT) },
+ { "rgb32f", 3, 12, {32, 32, 32}, DXFMT(R32G32B32, FLOAT) },
+ { "rgba32f", 4, 16, {32, 32, 32, 32}, DXFMT(R32G32B32A32, FLOAT) },
+
+ { "rgb10_a2", 4, 4, {10, 10, 10, 2}, DXFMT(R10G10B10A2, UNORM) },
+ { "bgra8", 4, 4, { 8, 8, 8, 8}, DXFMT(B8G8R8A8, UNORM), .unordered = true },
+ { "bgrx8", 3, 4, { 8, 8, 8}, DXFMT(B8G8R8X8, UNORM), .unordered = true },
+};
+
+static bool dll_version_equal(struct dll_version a, struct dll_version b)
+{
+ return a.major == b.major &&
+ a.minor == b.minor &&
+ a.build == b.build &&
+ a.revision == b.revision;
+}
+
+DXGI_FORMAT ra_d3d11_get_format(const struct ra_format *fmt)
+{
+ struct d3d_fmt *d3d = fmt->priv;
+ return d3d->fmt;
+}
+
+const struct ra_format *ra_d3d11_get_ra_format(struct ra *ra, DXGI_FORMAT fmt)
+{
+ for (int i = 0; i < ra->num_formats; i++) {
+ struct ra_format *ra_fmt = ra->formats[i];
+
+ if (ra_d3d11_get_format(ra_fmt) == fmt)
+ return ra_fmt;
+ }
+
+ return NULL;
+}
+
+static void setup_formats(struct ra *ra)
+{
+ // All formats must be usable as a 2D texture
+ static const UINT sup_basic = D3D11_FORMAT_SUPPORT_TEXTURE2D;
+ // SHADER_SAMPLE indicates support for linear sampling, point always works
+ static const UINT sup_filter = D3D11_FORMAT_SUPPORT_SHADER_SAMPLE;
+ // RA requires renderable surfaces to be blendable as well
+ static const UINT sup_render = D3D11_FORMAT_SUPPORT_RENDER_TARGET |
+ D3D11_FORMAT_SUPPORT_BLENDABLE;
+ // Typed UAVs are equivalent to images. RA only cares if they're storable.
+ static const UINT sup_store = D3D11_FORMAT_SUPPORT_TYPED_UNORDERED_ACCESS_VIEW;
+ static const UINT sup2_store = D3D11_FORMAT_SUPPORT2_UAV_TYPED_STORE;
+
+ struct ra_d3d11 *p = ra->priv;
+ HRESULT hr;
+
+ for (int i = 0; i < MP_ARRAY_SIZE(formats); i++) {
+ struct d3d_fmt *d3dfmt = &formats[i];
+ UINT support = 0;
+ hr = ID3D11Device_CheckFormatSupport(p->dev, d3dfmt->fmt, &support);
+ if (FAILED(hr))
+ continue;
+ if ((support & sup_basic) != sup_basic)
+ continue;
+
+ D3D11_FEATURE_DATA_FORMAT_SUPPORT2 sup2 = { .InFormat = d3dfmt->fmt };
+ ID3D11Device_CheckFeatureSupport(p->dev, D3D11_FEATURE_FORMAT_SUPPORT2,
+ &sup2, sizeof(sup2));
+ UINT support2 = sup2.OutFormatSupport2;
+
+ struct ra_format *fmt = talloc_zero(ra, struct ra_format);
+ *fmt = (struct ra_format) {
+ .name = d3dfmt->name,
+ .priv = d3dfmt,
+ .ctype = d3dfmt->ctype,
+ .ordered = !d3dfmt->unordered,
+ .num_components = d3dfmt->components,
+ .pixel_size = d3dfmt->bytes,
+ .linear_filter = (support & sup_filter) == sup_filter,
+ .renderable = (support & sup_render) == sup_render,
+ .storable = p->fl >= D3D_FEATURE_LEVEL_11_0 &&
+ (support & sup_store) == sup_store &&
+ (support2 & sup2_store) == sup2_store,
+ };
+
+ if (support & D3D11_FORMAT_SUPPORT_TEXTURE1D)
+ ra->caps |= RA_CAP_TEX_1D;
+
+ for (int j = 0; j < d3dfmt->components; j++)
+ fmt->component_size[j] = fmt->component_depth[j] = d3dfmt->bits[j];
+
+ fmt->glsl_format = ra_fmt_glsl_format(fmt);
+
+ MP_TARRAY_APPEND(ra, ra->formats, ra->num_formats, fmt);
+ }
+}
+
+static bool tex_init(struct ra *ra, struct ra_tex *tex)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_tex *tex_p = tex->priv;
+ struct ra_tex_params *params = &tex->params;
+ HRESULT hr;
+
+ // A SRV is required for renderpasses and blitting, since blitting can use
+ // a renderpass internally
+ if (params->render_src || params->blit_src) {
+ // Always specify the SRV format for simplicity. This will match the
+ // texture format for textures created with tex_create, but it can be
+ // different for wrapped planar video textures.
+ D3D11_SHADER_RESOURCE_VIEW_DESC srvdesc = {
+ .Format = ra_d3d11_get_format(params->format),
+ };
+ switch (params->dimensions) {
+ case 1:
+ if (tex_p->array_slice >= 0) {
+ srvdesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE1DARRAY;
+ srvdesc.Texture1DArray.MipLevels = 1;
+ srvdesc.Texture1DArray.FirstArraySlice = tex_p->array_slice;
+ srvdesc.Texture1DArray.ArraySize = 1;
+ } else {
+ srvdesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE1D;
+ srvdesc.Texture1D.MipLevels = 1;
+ }
+ break;
+ case 2:
+ if (tex_p->array_slice >= 0) {
+ srvdesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
+ srvdesc.Texture2DArray.MipLevels = 1;
+ srvdesc.Texture2DArray.FirstArraySlice = tex_p->array_slice;
+ srvdesc.Texture2DArray.ArraySize = 1;
+ } else {
+ srvdesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
+ srvdesc.Texture2D.MipLevels = 1;
+ }
+ break;
+ case 3:
+ // D3D11 does not have Texture3D arrays
+ srvdesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE3D;
+ srvdesc.Texture3D.MipLevels = 1;
+ break;
+ }
+ hr = ID3D11Device_CreateShaderResourceView(p->dev, tex_p->res, &srvdesc,
+ &tex_p->srv);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create SRV: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ }
+
+ // Samplers are required for renderpasses, but not blitting, since the blit
+ // code uses its own point sampler
+ if (params->render_src) {
+ D3D11_SAMPLER_DESC sdesc = {
+ .AddressU = D3D11_TEXTURE_ADDRESS_CLAMP,
+ .AddressV = D3D11_TEXTURE_ADDRESS_CLAMP,
+ .AddressW = D3D11_TEXTURE_ADDRESS_CLAMP,
+ .ComparisonFunc = D3D11_COMPARISON_NEVER,
+ .MinLOD = 0,
+ .MaxLOD = D3D11_FLOAT32_MAX,
+ .MaxAnisotropy = 1,
+ };
+ if (params->src_linear)
+ sdesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
+ if (params->src_repeat) {
+ sdesc.AddressU = sdesc.AddressV = sdesc.AddressW =
+ D3D11_TEXTURE_ADDRESS_WRAP;
+ }
+ // The runtime pools sampler state objects internally, so we don't have
+ // to worry about resource usage when creating one for every ra_tex
+ hr = ID3D11Device_CreateSamplerState(p->dev, &sdesc, &tex_p->sampler);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create sampler: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ }
+
+ // Like SRVs, an RTV is required for renderpass output and blitting
+ if (params->render_dst || params->blit_dst) {
+ hr = ID3D11Device_CreateRenderTargetView(p->dev, tex_p->res, NULL,
+ &tex_p->rtv);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create RTV: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ }
+
+ if (p->fl >= D3D_FEATURE_LEVEL_11_0 && params->storage_dst) {
+ hr = ID3D11Device_CreateUnorderedAccessView(p->dev, tex_p->res, NULL,
+ &tex_p->uav);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create UAV: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ }
+
+ return true;
+error:
+ return false;
+}
+
+static void tex_destroy(struct ra *ra, struct ra_tex *tex)
+{
+ if (!tex)
+ return;
+ struct d3d_tex *tex_p = tex->priv;
+
+ SAFE_RELEASE(tex_p->srv);
+ SAFE_RELEASE(tex_p->rtv);
+ SAFE_RELEASE(tex_p->uav);
+ SAFE_RELEASE(tex_p->sampler);
+ SAFE_RELEASE(tex_p->res);
+ SAFE_RELEASE(tex_p->staging);
+ talloc_free(tex);
+}
+
+static struct ra_tex *tex_create(struct ra *ra,
+ const struct ra_tex_params *params)
+{
+ // Only 2D textures may be downloaded for now
+ if (params->downloadable && params->dimensions != 2)
+ return NULL;
+
+ struct ra_d3d11 *p = ra->priv;
+ HRESULT hr;
+
+ struct ra_tex *tex = talloc_zero(NULL, struct ra_tex);
+ tex->params = *params;
+ tex->params.initial_data = NULL;
+
+ struct d3d_tex *tex_p = tex->priv = talloc_zero(tex, struct d3d_tex);
+ DXGI_FORMAT fmt = ra_d3d11_get_format(params->format);
+
+ D3D11_SUBRESOURCE_DATA data;
+ D3D11_SUBRESOURCE_DATA *pdata = NULL;
+ if (params->initial_data) {
+ data = (D3D11_SUBRESOURCE_DATA) {
+ .pSysMem = params->initial_data,
+ .SysMemPitch = params->w * params->format->pixel_size,
+ };
+ if (params->dimensions >= 3)
+ data.SysMemSlicePitch = data.SysMemPitch * params->h;
+ pdata = &data;
+ }
+
+ D3D11_USAGE usage = D3D11_USAGE_DEFAULT;
+ D3D11_BIND_FLAG bind_flags = 0;
+
+ if (params->render_src || params->blit_src)
+ bind_flags |= D3D11_BIND_SHADER_RESOURCE;
+ if (params->render_dst || params->blit_dst)
+ bind_flags |= D3D11_BIND_RENDER_TARGET;
+ if (p->fl >= D3D_FEATURE_LEVEL_11_0 && params->storage_dst)
+ bind_flags |= D3D11_BIND_UNORDERED_ACCESS;
+
+ // Apparently IMMUTABLE textures are efficient, so try to infer whether we
+ // can use one
+ if (params->initial_data && !params->render_dst && !params->storage_dst &&
+ !params->blit_dst && !params->host_mutable)
+ usage = D3D11_USAGE_IMMUTABLE;
+
+ switch (params->dimensions) {
+ case 1:;
+ D3D11_TEXTURE1D_DESC desc1d = {
+ .Width = params->w,
+ .MipLevels = 1,
+ .ArraySize = 1,
+ .Format = fmt,
+ .Usage = usage,
+ .BindFlags = bind_flags,
+ };
+ hr = ID3D11Device_CreateTexture1D(p->dev, &desc1d, pdata, &tex_p->tex1d);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create Texture1D: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ tex_p->res = (ID3D11Resource *)tex_p->tex1d;
+ break;
+ case 2:;
+ D3D11_TEXTURE2D_DESC desc2d = {
+ .Width = params->w,
+ .Height = params->h,
+ .MipLevels = 1,
+ .ArraySize = 1,
+ .SampleDesc.Count = 1,
+ .Format = fmt,
+ .Usage = usage,
+ .BindFlags = bind_flags,
+ };
+ hr = ID3D11Device_CreateTexture2D(p->dev, &desc2d, pdata, &tex_p->tex2d);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create Texture2D: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ tex_p->res = (ID3D11Resource *)tex_p->tex2d;
+
+ // Create a staging texture with CPU access for tex_download()
+ if (params->downloadable) {
+ desc2d.BindFlags = 0;
+ desc2d.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
+ desc2d.Usage = D3D11_USAGE_STAGING;
+
+ hr = ID3D11Device_CreateTexture2D(p->dev, &desc2d, NULL,
+ &tex_p->staging);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to staging texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ }
+ break;
+ case 3:;
+ D3D11_TEXTURE3D_DESC desc3d = {
+ .Width = params->w,
+ .Height = params->h,
+ .Depth = params->d,
+ .MipLevels = 1,
+ .Format = fmt,
+ .Usage = usage,
+ .BindFlags = bind_flags,
+ };
+ hr = ID3D11Device_CreateTexture3D(p->dev, &desc3d, pdata, &tex_p->tex3d);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create Texture3D: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ tex_p->res = (ID3D11Resource *)tex_p->tex3d;
+ break;
+ default:
+ MP_ASSERT_UNREACHABLE();
+ }
+
+ tex_p->array_slice = -1;
+
+ if (!tex_init(ra, tex))
+ goto error;
+
+ return tex;
+
+error:
+ tex_destroy(ra, tex);
+ return NULL;
+}
+
+struct ra_tex *ra_d3d11_wrap_tex(struct ra *ra, ID3D11Resource *res)
+{
+ HRESULT hr;
+
+ struct ra_tex *tex = talloc_zero(NULL, struct ra_tex);
+ struct ra_tex_params *params = &tex->params;
+ struct d3d_tex *tex_p = tex->priv = talloc_zero(tex, struct d3d_tex);
+
+ DXGI_FORMAT fmt = DXGI_FORMAT_UNKNOWN;
+ D3D11_USAGE usage = D3D11_USAGE_DEFAULT;
+ D3D11_BIND_FLAG bind_flags = 0;
+
+ D3D11_RESOURCE_DIMENSION type;
+ ID3D11Resource_GetType(res, &type);
+ switch (type) {
+ case D3D11_RESOURCE_DIMENSION_TEXTURE2D:
+ hr = ID3D11Resource_QueryInterface(res, &IID_ID3D11Texture2D,
+ (void**)&tex_p->tex2d);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Resource is not a ID3D11Texture2D\n");
+ goto error;
+ }
+ tex_p->res = (ID3D11Resource *)tex_p->tex2d;
+
+ D3D11_TEXTURE2D_DESC desc2d;
+ ID3D11Texture2D_GetDesc(tex_p->tex2d, &desc2d);
+ if (desc2d.MipLevels != 1) {
+ MP_ERR(ra, "Mipmapped textures not supported for wrapping\n");
+ goto error;
+ }
+ if (desc2d.ArraySize != 1) {
+ MP_ERR(ra, "Texture arrays not supported for wrapping\n");
+ goto error;
+ }
+ if (desc2d.SampleDesc.Count != 1) {
+ MP_ERR(ra, "Multisampled textures not supported for wrapping\n");
+ goto error;
+ }
+
+ params->dimensions = 2;
+ params->w = desc2d.Width;
+ params->h = desc2d.Height;
+ params->d = 1;
+ usage = desc2d.Usage;
+ bind_flags = desc2d.BindFlags;
+ fmt = desc2d.Format;
+ break;
+ default:
+ // We could wrap Texture1D/3D as well, but keep it simple, since this
+ // function is only used for swapchain backbuffers at the moment
+ MP_ERR(ra, "Resource is not suitable to wrap\n");
+ goto error;
+ }
+
+ for (int i = 0; i < ra->num_formats; i++) {
+ DXGI_FORMAT target_fmt = ra_d3d11_get_format(ra->formats[i]);
+ if (fmt == target_fmt) {
+ params->format = ra->formats[i];
+ break;
+ }
+ }
+ if (!params->format) {
+ MP_ERR(ra, "Could not find a suitable RA format for wrapped resource\n");
+ goto error;
+ }
+
+ if (bind_flags & D3D11_BIND_SHADER_RESOURCE) {
+ params->render_src = params->blit_src = true;
+ params->src_linear = params->format->linear_filter;
+ }
+ if (bind_flags & D3D11_BIND_RENDER_TARGET)
+ params->render_dst = params->blit_dst = true;
+ if (bind_flags & D3D11_BIND_UNORDERED_ACCESS)
+ params->storage_dst = true;
+
+ if (usage != D3D11_USAGE_DEFAULT) {
+ MP_ERR(ra, "Resource is not D3D11_USAGE_DEFAULT\n");
+ goto error;
+ }
+
+ tex_p->array_slice = -1;
+
+ if (!tex_init(ra, tex))
+ goto error;
+
+ return tex;
+error:
+ tex_destroy(ra, tex);
+ return NULL;
+}
+
+struct ra_tex *ra_d3d11_wrap_tex_video(struct ra *ra, ID3D11Texture2D *res,
+ int w, int h, int array_slice,
+ const struct ra_format *fmt)
+{
+ struct ra_tex *tex = talloc_zero(NULL, struct ra_tex);
+ struct ra_tex_params *params = &tex->params;
+ struct d3d_tex *tex_p = tex->priv = talloc_zero(tex, struct d3d_tex);
+
+ tex_p->tex2d = res;
+ tex_p->res = (ID3D11Resource *)tex_p->tex2d;
+ ID3D11Texture2D_AddRef(res);
+
+ D3D11_TEXTURE2D_DESC desc2d;
+ ID3D11Texture2D_GetDesc(tex_p->tex2d, &desc2d);
+ if (!(desc2d.BindFlags & D3D11_BIND_SHADER_RESOURCE)) {
+ MP_ERR(ra, "Video resource is not bindable\n");
+ goto error;
+ }
+
+ params->dimensions = 2;
+ params->w = w;
+ params->h = h;
+ params->d = 1;
+ params->render_src = true;
+ params->src_linear = true;
+ // fmt can be different to the texture format for planar video textures
+ params->format = fmt;
+
+ if (desc2d.ArraySize > 1) {
+ tex_p->array_slice = array_slice;
+ } else {
+ tex_p->array_slice = -1;
+ }
+
+ if (!tex_init(ra, tex))
+ goto error;
+
+ return tex;
+error:
+ tex_destroy(ra, tex);
+ return NULL;
+}
+
+ID3D11Resource *ra_d3d11_get_raw_tex(struct ra *ra, struct ra_tex *tex,
+ int *array_slice)
+{
+ struct d3d_tex *tex_p = tex->priv;
+
+ ID3D11Resource_AddRef(tex_p->res);
+ if (array_slice)
+ *array_slice = tex_p->array_slice;
+ return tex_p->res;
+}
+
+static bool tex_upload(struct ra *ra, const struct ra_tex_upload_params *params)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct ra_tex *tex = params->tex;
+ struct d3d_tex *tex_p = tex->priv;
+
+ if (!params->src) {
+ MP_ERR(ra, "Pixel buffers are not supported\n");
+ return false;
+ }
+
+ const char *src = params->src;
+ ptrdiff_t stride = tex->params.dimensions >= 2 ? tex->params.w : 0;
+ ptrdiff_t pitch = tex->params.dimensions >= 3 ? stride * tex->params.h : 0;
+ bool invalidate = true;
+ D3D11_BOX rc;
+ D3D11_BOX *prc = NULL;
+
+ if (tex->params.dimensions == 2) {
+ stride = params->stride;
+
+ if (params->rc && (params->rc->x0 != 0 || params->rc->y0 != 0 ||
+ params->rc->x1 != tex->params.w || params->rc->y1 != tex->params.h))
+ {
+ rc = (D3D11_BOX) {
+ .left = params->rc->x0,
+ .top = params->rc->y0,
+ .front = 0,
+ .right = params->rc->x1,
+ .bottom = params->rc->y1,
+ .back = 1,
+ };
+ prc = &rc;
+ invalidate = params->invalidate;
+ }
+ }
+
+ int subresource = tex_p->array_slice >= 0 ? tex_p->array_slice : 0;
+ if (p->ctx1) {
+ ID3D11DeviceContext1_UpdateSubresource1(p->ctx1, tex_p->res,
+ subresource, prc, src, stride, pitch,
+ invalidate ? D3D11_COPY_DISCARD : 0);
+ } else {
+ ID3D11DeviceContext_UpdateSubresource(p->ctx, tex_p->res, subresource,
+ prc, src, stride, pitch);
+ }
+
+ return true;
+}
+
+static bool tex_download(struct ra *ra, struct ra_tex_download_params *params)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct ra_tex *tex = params->tex;
+ struct d3d_tex *tex_p = tex->priv;
+ HRESULT hr;
+
+ if (!tex_p->staging)
+ return false;
+
+ ID3D11DeviceContext_CopyResource(p->ctx, (ID3D11Resource*)tex_p->staging,
+ tex_p->res);
+
+ D3D11_MAPPED_SUBRESOURCE lock;
+ hr = ID3D11DeviceContext_Map(p->ctx, (ID3D11Resource*)tex_p->staging, 0,
+ D3D11_MAP_READ, 0, &lock);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to map staging texture: %s\n", mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ char *cdst = params->dst;
+ char *csrc = lock.pData;
+ for (int y = 0; y < tex->params.h; y++) {
+ memcpy(cdst + y * params->stride, csrc + y * lock.RowPitch,
+ MPMIN(params->stride, lock.RowPitch));
+ }
+
+ ID3D11DeviceContext_Unmap(p->ctx, (ID3D11Resource*)tex_p->staging, 0);
+
+ return true;
+}
+
+static void buf_destroy(struct ra *ra, struct ra_buf *buf)
+{
+ if (!buf)
+ return;
+ struct d3d_buf *buf_p = buf->priv;
+ SAFE_RELEASE(buf_p->buf);
+ SAFE_RELEASE(buf_p->uav);
+ talloc_free(buf);
+}
+
+static struct ra_buf *buf_create(struct ra *ra,
+ const struct ra_buf_params *params)
+{
+ // D3D11 does not support permanent mapping or pixel buffers
+ if (params->host_mapped || params->type == RA_BUF_TYPE_TEX_UPLOAD)
+ return NULL;
+
+ struct ra_d3d11 *p = ra->priv;
+ HRESULT hr;
+
+ struct ra_buf *buf = talloc_zero(NULL, struct ra_buf);
+ buf->params = *params;
+ buf->params.initial_data = NULL;
+
+ struct d3d_buf *buf_p = buf->priv = talloc_zero(buf, struct d3d_buf);
+
+ D3D11_SUBRESOURCE_DATA data;
+ D3D11_SUBRESOURCE_DATA *pdata = NULL;
+ if (params->initial_data) {
+ data = (D3D11_SUBRESOURCE_DATA) { .pSysMem = params->initial_data };
+ pdata = &data;
+ }
+
+ D3D11_BUFFER_DESC desc = { .ByteWidth = params->size };
+ switch (params->type) {
+ case RA_BUF_TYPE_SHADER_STORAGE:
+ desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
+ desc.ByteWidth = MP_ALIGN_UP(desc.ByteWidth, sizeof(float));
+ desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;
+ break;
+ case RA_BUF_TYPE_UNIFORM:
+ desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
+ desc.ByteWidth = MP_ALIGN_UP(desc.ByteWidth, sizeof(float[4]));
+ break;
+ }
+
+ hr = ID3D11Device_CreateBuffer(p->dev, &desc, pdata, &buf_p->buf);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create buffer: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ // D3D11 doesn't allow constant buffer updates that aren't aligned to a
+ // full constant boundary (vec4,) and some drivers don't allow partial
+ // constant buffer updates at all. To support partial buffer updates, keep
+ // a mirror of the buffer data in system memory and upload the whole thing
+ // before the buffer is used.
+ if (params->host_mutable)
+ buf_p->data = talloc_zero_size(buf, desc.ByteWidth);
+
+ if (params->type == RA_BUF_TYPE_SHADER_STORAGE) {
+ D3D11_UNORDERED_ACCESS_VIEW_DESC udesc = {
+ .Format = DXGI_FORMAT_R32_TYPELESS,
+ .ViewDimension = D3D11_UAV_DIMENSION_BUFFER,
+ .Buffer = {
+ .NumElements = desc.ByteWidth / sizeof(float),
+ .Flags = D3D11_BUFFER_UAV_FLAG_RAW,
+ },
+ };
+ hr = ID3D11Device_CreateUnorderedAccessView(p->dev,
+ (ID3D11Resource *)buf_p->buf, &udesc, &buf_p->uav);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create UAV: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ }
+
+ return buf;
+error:
+ buf_destroy(ra, buf);
+ return NULL;
+}
+
+static void buf_resolve(struct ra *ra, struct ra_buf *buf)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_buf *buf_p = buf->priv;
+
+ if (!buf->params.host_mutable || !buf_p->dirty)
+ return;
+
+ // Synchronize the GPU buffer with the system-memory copy
+ ID3D11DeviceContext_UpdateSubresource(p->ctx, (ID3D11Resource *)buf_p->buf,
+ 0, NULL, buf_p->data, 0, 0);
+ buf_p->dirty = false;
+}
+
+static void buf_update(struct ra *ra, struct ra_buf *buf, ptrdiff_t offset,
+ const void *data, size_t size)
+{
+ struct d3d_buf *buf_p = buf->priv;
+
+ char *cdata = buf_p->data;
+ memcpy(cdata + offset, data, size);
+ buf_p->dirty = true;
+}
+
+static const char *get_shader_target(struct ra *ra, enum glsl_shader type)
+{
+ struct ra_d3d11 *p = ra->priv;
+ switch (p->fl) {
+ default:
+ switch (type) {
+ case GLSL_SHADER_VERTEX: return "vs_5_0";
+ case GLSL_SHADER_FRAGMENT: return "ps_5_0";
+ case GLSL_SHADER_COMPUTE: return "cs_5_0";
+ }
+ break;
+ case D3D_FEATURE_LEVEL_10_1:
+ switch (type) {
+ case GLSL_SHADER_VERTEX: return "vs_4_1";
+ case GLSL_SHADER_FRAGMENT: return "ps_4_1";
+ case GLSL_SHADER_COMPUTE: return "cs_4_1";
+ }
+ break;
+ case D3D_FEATURE_LEVEL_10_0:
+ switch (type) {
+ case GLSL_SHADER_VERTEX: return "vs_4_0";
+ case GLSL_SHADER_FRAGMENT: return "ps_4_0";
+ case GLSL_SHADER_COMPUTE: return "cs_4_0";
+ }
+ break;
+ case D3D_FEATURE_LEVEL_9_3:
+ switch (type) {
+ case GLSL_SHADER_VERTEX: return "vs_4_0_level_9_3";
+ case GLSL_SHADER_FRAGMENT: return "ps_4_0_level_9_3";
+ }
+ break;
+ case D3D_FEATURE_LEVEL_9_2:
+ case D3D_FEATURE_LEVEL_9_1:
+ switch (type) {
+ case GLSL_SHADER_VERTEX: return "vs_4_0_level_9_1";
+ case GLSL_SHADER_FRAGMENT: return "ps_4_0_level_9_1";
+ }
+ break;
+ }
+ return NULL;
+}
+
+static const char *shader_type_name(enum glsl_shader type)
+{
+ switch (type) {
+ case GLSL_SHADER_VERTEX: return "vertex";
+ case GLSL_SHADER_FRAGMENT: return "fragment";
+ case GLSL_SHADER_COMPUTE: return "compute";
+ default: return "unknown";
+ }
+}
+
+static bool setup_clear_rpass(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ ID3DBlob *vs_blob = NULL;
+ ID3DBlob *ps_blob = NULL;
+ HRESULT hr;
+
+ hr = p->D3DCompile(clear_vs, sizeof(clear_vs), NULL, NULL, NULL, "main",
+ get_shader_target(ra, GLSL_SHADER_VERTEX),
+ D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vs_blob, NULL);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to compile clear() vertex shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = ID3D11Device_CreateVertexShader(p->dev,
+ ID3D10Blob_GetBufferPointer(vs_blob), ID3D10Blob_GetBufferSize(vs_blob),
+ NULL, &p->clear_vs);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create clear() vertex shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = p->D3DCompile(clear_ps, sizeof(clear_ps), NULL, NULL, NULL, "main",
+ get_shader_target(ra, GLSL_SHADER_FRAGMENT),
+ D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &ps_blob, NULL);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to compile clear() pixel shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = ID3D11Device_CreatePixelShader(p->dev,
+ ID3D10Blob_GetBufferPointer(ps_blob), ID3D10Blob_GetBufferSize(ps_blob),
+ NULL, &p->clear_ps);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create clear() pixel shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ D3D11_INPUT_ELEMENT_DESC in_descs[] = {
+ { "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0 },
+ };
+ hr = ID3D11Device_CreateInputLayout(p->dev, in_descs,
+ MP_ARRAY_SIZE(in_descs), ID3D10Blob_GetBufferPointer(vs_blob),
+ ID3D10Blob_GetBufferSize(vs_blob), &p->clear_layout);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create clear() IA layout: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ // clear() always draws to a quad covering the whole viewport
+ static const float verts[] = {
+ -1, -1,
+ 1, -1,
+ 1, 1,
+ -1, 1,
+ -1, -1,
+ 1, 1,
+ };
+ D3D11_BUFFER_DESC vdesc = {
+ .ByteWidth = sizeof(verts),
+ .Usage = D3D11_USAGE_IMMUTABLE,
+ .BindFlags = D3D11_BIND_VERTEX_BUFFER,
+ };
+ D3D11_SUBRESOURCE_DATA vdata = {
+ .pSysMem = verts,
+ };
+ hr = ID3D11Device_CreateBuffer(p->dev, &vdesc, &vdata, &p->clear_vbuf);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create clear() vertex buffer: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ D3D11_BUFFER_DESC cdesc = {
+ .ByteWidth = sizeof(float[4]),
+ .BindFlags = D3D11_BIND_CONSTANT_BUFFER,
+ };
+ hr = ID3D11Device_CreateBuffer(p->dev, &cdesc, NULL, &p->clear_cbuf);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create clear() constant buffer: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ SAFE_RELEASE(vs_blob);
+ SAFE_RELEASE(ps_blob);
+ return true;
+error:
+ SAFE_RELEASE(vs_blob);
+ SAFE_RELEASE(ps_blob);
+ return false;
+}
+
+static void clear_rpass(struct ra *ra, struct ra_tex *tex, float color[4],
+ struct mp_rect *rc)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_tex *tex_p = tex->priv;
+ struct ra_tex_params *params = &tex->params;
+
+ ID3D11DeviceContext_UpdateSubresource(p->ctx,
+ (ID3D11Resource *)p->clear_cbuf, 0, NULL, color, 0, 0);
+
+ ID3D11DeviceContext_IASetInputLayout(p->ctx, p->clear_layout);
+ ID3D11DeviceContext_IASetVertexBuffers(p->ctx, 0, 1, &p->clear_vbuf,
+ &(UINT) { sizeof(float[2]) }, &(UINT) { 0 });
+ ID3D11DeviceContext_IASetPrimitiveTopology(p->ctx,
+ D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+
+ ID3D11DeviceContext_VSSetShader(p->ctx, p->clear_vs, NULL, 0);
+
+ ID3D11DeviceContext_RSSetViewports(p->ctx, 1, (&(D3D11_VIEWPORT) {
+ .Width = params->w,
+ .Height = params->h,
+ .MinDepth = 0,
+ .MaxDepth = 1,
+ }));
+ ID3D11DeviceContext_RSSetScissorRects(p->ctx, 1, (&(D3D11_RECT) {
+ .left = rc->x0,
+ .top = rc->y0,
+ .right = rc->x1,
+ .bottom = rc->y1,
+ }));
+ ID3D11DeviceContext_PSSetShader(p->ctx, p->clear_ps, NULL, 0);
+ ID3D11DeviceContext_PSSetConstantBuffers(p->ctx, 0, 1, &p->clear_cbuf);
+
+ ID3D11DeviceContext_OMSetRenderTargets(p->ctx, 1, &tex_p->rtv, NULL);
+ ID3D11DeviceContext_OMSetBlendState(p->ctx, NULL, NULL,
+ D3D11_DEFAULT_SAMPLE_MASK);
+
+ ID3D11DeviceContext_Draw(p->ctx, 6, 0);
+
+ ID3D11DeviceContext_PSSetConstantBuffers(p->ctx, 0, 1,
+ &(ID3D11Buffer *){ NULL });
+ ID3D11DeviceContext_OMSetRenderTargets(p->ctx, 0, NULL, NULL);
+}
+
+static void clear(struct ra *ra, struct ra_tex *tex, float color[4],
+ struct mp_rect *rc)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_tex *tex_p = tex->priv;
+ struct ra_tex_params *params = &tex->params;
+
+ if (!tex_p->rtv)
+ return;
+
+ if (rc->x0 || rc->y0 || rc->x1 != params->w || rc->y1 != params->h) {
+ if (p->has_clear_view) {
+ ID3D11DeviceContext1_ClearView(p->ctx1, (ID3D11View *)tex_p->rtv,
+ color, (&(D3D11_RECT) {
+ .left = rc->x0,
+ .top = rc->y0,
+ .right = rc->x1,
+ .bottom = rc->y1,
+ }), 1);
+ } else {
+ clear_rpass(ra, tex, color, rc);
+ }
+ } else {
+ ID3D11DeviceContext_ClearRenderTargetView(p->ctx, tex_p->rtv, color);
+ }
+}
+
+static bool setup_blit_rpass(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ ID3DBlob *vs_blob = NULL;
+ ID3DBlob *float_ps_blob = NULL;
+ HRESULT hr;
+
+ hr = p->D3DCompile(blit_vs, sizeof(blit_vs), NULL, NULL, NULL, "main",
+ get_shader_target(ra, GLSL_SHADER_VERTEX),
+ D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vs_blob, NULL);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to compile blit() vertex shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = ID3D11Device_CreateVertexShader(p->dev,
+ ID3D10Blob_GetBufferPointer(vs_blob), ID3D10Blob_GetBufferSize(vs_blob),
+ NULL, &p->blit_vs);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create blit() vertex shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = p->D3DCompile(blit_float_ps, sizeof(blit_float_ps), NULL, NULL, NULL,
+ "main", get_shader_target(ra, GLSL_SHADER_FRAGMENT),
+ D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &float_ps_blob, NULL);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to compile blit() pixel shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = ID3D11Device_CreatePixelShader(p->dev,
+ ID3D10Blob_GetBufferPointer(float_ps_blob),
+ ID3D10Blob_GetBufferSize(float_ps_blob),
+ NULL, &p->blit_float_ps);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create blit() pixel shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ D3D11_INPUT_ELEMENT_DESC in_descs[] = {
+ { "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0 },
+ { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 8 },
+ };
+ hr = ID3D11Device_CreateInputLayout(p->dev, in_descs,
+ MP_ARRAY_SIZE(in_descs), ID3D10Blob_GetBufferPointer(vs_blob),
+ ID3D10Blob_GetBufferSize(vs_blob), &p->blit_layout);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create blit() IA layout: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ D3D11_BUFFER_DESC vdesc = {
+ .ByteWidth = sizeof(struct blit_vert[6]),
+ .Usage = D3D11_USAGE_DEFAULT,
+ .BindFlags = D3D11_BIND_VERTEX_BUFFER,
+ };
+ hr = ID3D11Device_CreateBuffer(p->dev, &vdesc, NULL, &p->blit_vbuf);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create blit() vertex buffer: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ // Blit always uses point sampling, regardless of the source texture
+ D3D11_SAMPLER_DESC sdesc = {
+ .AddressU = D3D11_TEXTURE_ADDRESS_CLAMP,
+ .AddressV = D3D11_TEXTURE_ADDRESS_CLAMP,
+ .AddressW = D3D11_TEXTURE_ADDRESS_CLAMP,
+ .ComparisonFunc = D3D11_COMPARISON_NEVER,
+ .MinLOD = 0,
+ .MaxLOD = D3D11_FLOAT32_MAX,
+ .MaxAnisotropy = 1,
+ };
+ hr = ID3D11Device_CreateSamplerState(p->dev, &sdesc, &p->blit_sampler);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create blit() sampler: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ SAFE_RELEASE(vs_blob);
+ SAFE_RELEASE(float_ps_blob);
+ return true;
+error:
+ SAFE_RELEASE(vs_blob);
+ SAFE_RELEASE(float_ps_blob);
+ return false;
+}
+
+static void blit_rpass(struct ra *ra, struct ra_tex *dst, struct ra_tex *src,
+ struct mp_rect *dst_rc, struct mp_rect *src_rc)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_tex *dst_p = dst->priv;
+ struct d3d_tex *src_p = src->priv;
+
+ float u_min = (double)src_rc->x0 / src->params.w;
+ float u_max = (double)src_rc->x1 / src->params.w;
+ float v_min = (double)src_rc->y0 / src->params.h;
+ float v_max = (double)src_rc->y1 / src->params.h;
+
+ struct blit_vert verts[6] = {
+ { .x = -1, .y = -1, .u = u_min, .v = v_max },
+ { .x = 1, .y = -1, .u = u_max, .v = v_max },
+ { .x = 1, .y = 1, .u = u_max, .v = v_min },
+ { .x = -1, .y = 1, .u = u_min, .v = v_min },
+ };
+ verts[4] = verts[0];
+ verts[5] = verts[2];
+ ID3D11DeviceContext_UpdateSubresource(p->ctx,
+ (ID3D11Resource *)p->blit_vbuf, 0, NULL, verts, 0, 0);
+
+ ID3D11DeviceContext_IASetInputLayout(p->ctx, p->blit_layout);
+ ID3D11DeviceContext_IASetVertexBuffers(p->ctx, 0, 1, &p->blit_vbuf,
+ &(UINT) { sizeof(verts[0]) }, &(UINT) { 0 });
+ ID3D11DeviceContext_IASetPrimitiveTopology(p->ctx,
+ D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+
+ ID3D11DeviceContext_VSSetShader(p->ctx, p->blit_vs, NULL, 0);
+
+ ID3D11DeviceContext_RSSetViewports(p->ctx, 1, (&(D3D11_VIEWPORT) {
+ .TopLeftX = dst_rc->x0,
+ .TopLeftY = dst_rc->y0,
+ .Width = mp_rect_w(*dst_rc),
+ .Height = mp_rect_h(*dst_rc),
+ .MinDepth = 0,
+ .MaxDepth = 1,
+ }));
+ ID3D11DeviceContext_RSSetScissorRects(p->ctx, 1, (&(D3D11_RECT) {
+ .left = dst_rc->x0,
+ .top = dst_rc->y0,
+ .right = dst_rc->x1,
+ .bottom = dst_rc->y1,
+ }));
+
+ ID3D11DeviceContext_PSSetShader(p->ctx, p->blit_float_ps, NULL, 0);
+ ID3D11DeviceContext_PSSetShaderResources(p->ctx, 0, 1, &src_p->srv);
+ ID3D11DeviceContext_PSSetSamplers(p->ctx, 0, 1, &p->blit_sampler);
+
+ ID3D11DeviceContext_OMSetRenderTargets(p->ctx, 1, &dst_p->rtv, NULL);
+ ID3D11DeviceContext_OMSetBlendState(p->ctx, NULL, NULL,
+ D3D11_DEFAULT_SAMPLE_MASK);
+
+ ID3D11DeviceContext_Draw(p->ctx, 6, 0);
+
+ ID3D11DeviceContext_PSSetShaderResources(p->ctx, 0, 1,
+ &(ID3D11ShaderResourceView *) { NULL });
+ ID3D11DeviceContext_PSSetSamplers(p->ctx, 0, 1,
+ &(ID3D11SamplerState *) { NULL });
+ ID3D11DeviceContext_OMSetRenderTargets(p->ctx, 0, NULL, NULL);
+}
+
+static void blit(struct ra *ra, struct ra_tex *dst, struct ra_tex *src,
+ struct mp_rect *dst_rc_ptr, struct mp_rect *src_rc_ptr)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_tex *dst_p = dst->priv;
+ struct d3d_tex *src_p = src->priv;
+ struct mp_rect dst_rc = *dst_rc_ptr;
+ struct mp_rect src_rc = *src_rc_ptr;
+
+ assert(dst->params.dimensions == 2);
+ assert(src->params.dimensions == 2);
+
+ // A zero-sized target rectangle is a no-op
+ if (!mp_rect_w(dst_rc) || !mp_rect_h(dst_rc))
+ return;
+
+ // ra.h seems to imply that both dst_rc and src_rc can be flipped, but it's
+ // easier for blit_rpass() if only src_rc can be flipped, so unflip dst_rc.
+ if (dst_rc.x0 > dst_rc.x1) {
+ MPSWAP(int, dst_rc.x0, dst_rc.x1);
+ MPSWAP(int, src_rc.x0, src_rc.x1);
+ }
+ if (dst_rc.y0 > dst_rc.y1) {
+ MPSWAP(int, dst_rc.y0, dst_rc.y1);
+ MPSWAP(int, src_rc.y0, src_rc.y1);
+ }
+
+ // If format conversion, stretching or flipping is required, a renderpass
+ // must be used
+ if (dst->params.format != src->params.format ||
+ mp_rect_w(dst_rc) != mp_rect_w(src_rc) ||
+ mp_rect_h(dst_rc) != mp_rect_h(src_rc))
+ {
+ blit_rpass(ra, dst, src, &dst_rc, &src_rc);
+ } else {
+ int dst_sr = dst_p->array_slice >= 0 ? dst_p->array_slice : 0;
+ int src_sr = src_p->array_slice >= 0 ? src_p->array_slice : 0;
+ ID3D11DeviceContext_CopySubresourceRegion(p->ctx, dst_p->res, dst_sr,
+ dst_rc.x0, dst_rc.y0, 0, src_p->res, src_sr, (&(D3D11_BOX) {
+ .left = src_rc.x0,
+ .top = src_rc.y0,
+ .front = 0,
+ .right = src_rc.x1,
+ .bottom = src_rc.y1,
+ .back = 1,
+ }));
+ }
+}
+
+static int desc_namespace(struct ra *ra, enum ra_vartype type)
+{
+ // Images and SSBOs both use UAV bindings
+ if (type == RA_VARTYPE_IMG_W)
+ type = RA_VARTYPE_BUF_RW;
+ return type;
+}
+
+static bool compile_glsl(struct ra *ra, enum glsl_shader type,
+ const char *glsl, ID3DBlob **out)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct spirv_compiler *spirv = p->spirv;
+ void *ta_ctx = talloc_new(NULL);
+ spvc_result sc_res = SPVC_SUCCESS;
+ spvc_context sc_ctx = NULL;
+ spvc_parsed_ir sc_ir = NULL;
+ spvc_compiler sc_compiler = NULL;
+ spvc_compiler_options sc_opts = NULL;
+ const char *hlsl = NULL;
+ ID3DBlob *errors = NULL;
+ bool success = false;
+ HRESULT hr;
+
+ int sc_shader_model;
+ if (p->fl >= D3D_FEATURE_LEVEL_11_0) {
+ sc_shader_model = 50;
+ } else if (p->fl >= D3D_FEATURE_LEVEL_10_1) {
+ sc_shader_model = 41;
+ } else {
+ sc_shader_model = 40;
+ }
+
+ int64_t start_ns = mp_time_ns();
+
+ bstr spv_module;
+ if (!spirv->fns->compile_glsl(spirv, ta_ctx, type, glsl, &spv_module))
+ goto done;
+
+ int64_t shaderc_ns = mp_time_ns();
+
+ sc_res = spvc_context_create(&sc_ctx);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+
+ sc_res = spvc_context_parse_spirv(sc_ctx, (SpvId *)spv_module.start,
+ spv_module.len / sizeof(SpvId), &sc_ir);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+
+ sc_res = spvc_context_create_compiler(sc_ctx, SPVC_BACKEND_HLSL, sc_ir,
+ SPVC_CAPTURE_MODE_TAKE_OWNERSHIP,
+ &sc_compiler);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+
+ sc_res = spvc_compiler_create_compiler_options(sc_compiler, &sc_opts);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+ sc_res = spvc_compiler_options_set_uint(sc_opts,
+ SPVC_COMPILER_OPTION_HLSL_SHADER_MODEL, sc_shader_model);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+ if (type == GLSL_SHADER_VERTEX) {
+ // FLIP_VERTEX_Y is only valid for vertex shaders
+ sc_res = spvc_compiler_options_set_bool(sc_opts,
+ SPVC_COMPILER_OPTION_FLIP_VERTEX_Y, SPVC_TRUE);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+ }
+ sc_res = spvc_compiler_install_compiler_options(sc_compiler, sc_opts);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+
+ sc_res = spvc_compiler_compile(sc_compiler, &hlsl);
+ if (sc_res != SPVC_SUCCESS)
+ goto done;
+
+ int64_t cross_ns = mp_time_ns();
+
+ hr = p->D3DCompile(hlsl, strlen(hlsl), NULL, NULL, NULL, "main",
+ get_shader_target(ra, type), D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, out,
+ &errors);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "D3DCompile failed: %s\n%.*s", mp_HRESULT_to_str(hr),
+ (int)ID3D10Blob_GetBufferSize(errors),
+ (char*)ID3D10Blob_GetBufferPointer(errors));
+ goto done;
+ }
+
+ int64_t d3dcompile_ns = mp_time_ns();
+
+ MP_VERBOSE(ra, "Compiled a %s shader in %lldns\n", shader_type_name(type),
+ d3dcompile_ns - start_ns);
+ MP_VERBOSE(ra, "shaderc: %lldns, SPIRV-Cross: %lldns, D3DCompile: %lldns\n",
+ shaderc_ns - start_ns,
+ cross_ns - shaderc_ns,
+ d3dcompile_ns - cross_ns);
+
+ success = true;
+done:
+ if (sc_res != SPVC_SUCCESS) {
+ MP_MSG(ra, MSGL_ERR, "SPIRV-Cross failed: %s\n",
+ spvc_context_get_last_error_string(sc_ctx));
+ }
+ int level = success ? MSGL_DEBUG : MSGL_ERR;
+ MP_MSG(ra, level, "GLSL source:\n");
+ mp_log_source(ra->log, level, glsl);
+ if (hlsl) {
+ MP_MSG(ra, level, "HLSL source:\n");
+ mp_log_source(ra->log, level, hlsl);
+ }
+ SAFE_RELEASE(errors);
+ if (sc_ctx)
+ spvc_context_destroy(sc_ctx);
+ talloc_free(ta_ctx);
+ return success;
+}
+
+static void renderpass_destroy(struct ra *ra, struct ra_renderpass *pass)
+{
+ if (!pass)
+ return;
+ struct d3d_rpass *pass_p = pass->priv;
+
+ SAFE_RELEASE(pass_p->vs);
+ SAFE_RELEASE(pass_p->ps);
+ SAFE_RELEASE(pass_p->cs);
+ SAFE_RELEASE(pass_p->layout);
+ SAFE_RELEASE(pass_p->bstate);
+ talloc_free(pass);
+}
+
+static D3D11_BLEND map_ra_blend(enum ra_blend blend)
+{
+ switch (blend) {
+ default:
+ case RA_BLEND_ZERO: return D3D11_BLEND_ZERO;
+ case RA_BLEND_ONE: return D3D11_BLEND_ONE;
+ case RA_BLEND_SRC_ALPHA: return D3D11_BLEND_SRC_ALPHA;
+ case RA_BLEND_ONE_MINUS_SRC_ALPHA: return D3D11_BLEND_INV_SRC_ALPHA;
+ };
+}
+
+static size_t vbuf_upload(struct ra *ra, void *data, size_t size)
+{
+ struct ra_d3d11 *p = ra->priv;
+ HRESULT hr;
+
+ // Arbitrary size limit in case there is an insane number of vertices
+ if (size > 1e9) {
+ MP_ERR(ra, "Vertex buffer is too large\n");
+ return -1;
+ }
+
+ // If the vertex data doesn't fit, realloc the vertex buffer
+ if (size > p->vbuf_size) {
+ size_t new_size = p->vbuf_size;
+ // Arbitrary base size
+ if (!new_size)
+ new_size = 64 * 1024;
+ while (new_size < size)
+ new_size *= 2;
+
+ ID3D11Buffer *new_buf;
+ D3D11_BUFFER_DESC vbuf_desc = {
+ .ByteWidth = new_size,
+ .Usage = D3D11_USAGE_DYNAMIC,
+ .BindFlags = D3D11_BIND_VERTEX_BUFFER,
+ .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE,
+ };
+ hr = ID3D11Device_CreateBuffer(p->dev, &vbuf_desc, NULL, &new_buf);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create vertex buffer: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ SAFE_RELEASE(p->vbuf);
+ p->vbuf = new_buf;
+ p->vbuf_size = new_size;
+ p->vbuf_used = 0;
+ }
+
+ bool discard = false;
+ size_t offset = p->vbuf_used;
+ if (offset + size > p->vbuf_size) {
+ // We reached the end of the buffer, so discard and wrap around
+ discard = true;
+ offset = 0;
+ }
+
+ D3D11_MAPPED_SUBRESOURCE map = { 0 };
+ hr = ID3D11DeviceContext_Map(p->ctx, (ID3D11Resource *)p->vbuf, 0,
+ discard ? D3D11_MAP_WRITE_DISCARD : D3D11_MAP_WRITE_NO_OVERWRITE,
+ 0, &map);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to map vertex buffer: %s\n", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ char *cdata = map.pData;
+ memcpy(cdata + offset, data, size);
+
+ ID3D11DeviceContext_Unmap(p->ctx, (ID3D11Resource *)p->vbuf, 0);
+
+ p->vbuf_used = offset + size;
+ return offset;
+}
+
+static const char cache_magic[4] = "RD11";
+static const int cache_version = 3;
+
+struct cache_header {
+ char magic[sizeof(cache_magic)];
+ int cache_version;
+ char compiler[SPIRV_NAME_MAX_LEN];
+ int spv_compiler_version;
+ unsigned spvc_compiler_major;
+ unsigned spvc_compiler_minor;
+ unsigned spvc_compiler_patch;
+ struct dll_version d3d_compiler_version;
+ int feature_level;
+ size_t vert_bytecode_len;
+ size_t frag_bytecode_len;
+ size_t comp_bytecode_len;
+};
+
+static void load_cached_program(struct ra *ra,
+ const struct ra_renderpass_params *params,
+ bstr *vert_bc,
+ bstr *frag_bc,
+ bstr *comp_bc)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct spirv_compiler *spirv = p->spirv;
+ bstr cache = params->cached_program;
+
+ if (cache.len < sizeof(struct cache_header))
+ return;
+
+ struct cache_header *header = (struct cache_header *)cache.start;
+ cache = bstr_cut(cache, sizeof(*header));
+
+ unsigned spvc_major, spvc_minor, spvc_patch;
+ spvc_get_version(&spvc_major, &spvc_minor, &spvc_patch);
+
+ if (strncmp(header->magic, cache_magic, sizeof(cache_magic)) != 0)
+ return;
+ if (header->cache_version != cache_version)
+ return;
+ if (strncmp(header->compiler, spirv->name, sizeof(header->compiler)) != 0)
+ return;
+ if (header->spv_compiler_version != spirv->compiler_version)
+ return;
+ if (header->spvc_compiler_major != spvc_major)
+ return;
+ if (header->spvc_compiler_minor != spvc_minor)
+ return;
+ if (header->spvc_compiler_patch != spvc_patch)
+ return;
+ if (!dll_version_equal(header->d3d_compiler_version, p->d3d_compiler_ver))
+ return;
+ if (header->feature_level != p->fl)
+ return;
+
+ if (header->vert_bytecode_len && vert_bc) {
+ *vert_bc = bstr_splice(cache, 0, header->vert_bytecode_len);
+ MP_VERBOSE(ra, "Using cached vertex shader\n");
+ }
+ cache = bstr_cut(cache, header->vert_bytecode_len);
+
+ if (header->frag_bytecode_len && frag_bc) {
+ *frag_bc = bstr_splice(cache, 0, header->frag_bytecode_len);
+ MP_VERBOSE(ra, "Using cached fragment shader\n");
+ }
+ cache = bstr_cut(cache, header->frag_bytecode_len);
+
+ if (header->comp_bytecode_len && comp_bc) {
+ *comp_bc = bstr_splice(cache, 0, header->comp_bytecode_len);
+ MP_VERBOSE(ra, "Using cached compute shader\n");
+ }
+ cache = bstr_cut(cache, header->comp_bytecode_len);
+}
+
+static void save_cached_program(struct ra *ra, struct ra_renderpass *pass,
+ bstr vert_bc,
+ bstr frag_bc,
+ bstr comp_bc)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct spirv_compiler *spirv = p->spirv;
+
+ unsigned spvc_major, spvc_minor, spvc_patch;
+ spvc_get_version(&spvc_major, &spvc_minor, &spvc_patch);
+
+ struct cache_header header = {
+ .cache_version = cache_version,
+ .spv_compiler_version = p->spirv->compiler_version,
+ .spvc_compiler_major = spvc_major,
+ .spvc_compiler_minor = spvc_minor,
+ .spvc_compiler_patch = spvc_patch,
+ .d3d_compiler_version = p->d3d_compiler_ver,
+ .feature_level = p->fl,
+ .vert_bytecode_len = vert_bc.len,
+ .frag_bytecode_len = frag_bc.len,
+ .comp_bytecode_len = comp_bc.len,
+ };
+ memcpy(header.magic, cache_magic, sizeof(header.magic));
+ strncpy(header.compiler, spirv->name, sizeof(header.compiler));
+
+ struct bstr *prog = &pass->params.cached_program;
+ bstr_xappend(pass, prog, (bstr){ (char *) &header, sizeof(header) });
+ bstr_xappend(pass, prog, vert_bc);
+ bstr_xappend(pass, prog, frag_bc);
+ bstr_xappend(pass, prog, comp_bc);
+}
+
+static struct ra_renderpass *renderpass_create_raster(struct ra *ra,
+ struct ra_renderpass *pass, const struct ra_renderpass_params *params)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_rpass *pass_p = pass->priv;
+ ID3DBlob *vs_blob = NULL;
+ ID3DBlob *ps_blob = NULL;
+ HRESULT hr;
+
+ // load_cached_program will load compiled shader bytecode into vert_bc and
+ // frag_bc if the cache is valid. If not, vert_bc/frag_bc will remain NULL.
+ bstr vert_bc = {0};
+ bstr frag_bc = {0};
+ load_cached_program(ra, params, &vert_bc, &frag_bc, NULL);
+
+ if (!vert_bc.start) {
+ if (!compile_glsl(ra, GLSL_SHADER_VERTEX, params->vertex_shader,
+ &vs_blob))
+ goto error;
+ vert_bc = (bstr){
+ ID3D10Blob_GetBufferPointer(vs_blob),
+ ID3D10Blob_GetBufferSize(vs_blob),
+ };
+ }
+
+ hr = ID3D11Device_CreateVertexShader(p->dev, vert_bc.start, vert_bc.len,
+ NULL, &pass_p->vs);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create vertex shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ if (!frag_bc.start) {
+ if (!compile_glsl(ra, GLSL_SHADER_FRAGMENT, params->frag_shader,
+ &ps_blob))
+ goto error;
+ frag_bc = (bstr){
+ ID3D10Blob_GetBufferPointer(ps_blob),
+ ID3D10Blob_GetBufferSize(ps_blob),
+ };
+ }
+
+ hr = ID3D11Device_CreatePixelShader(p->dev, frag_bc.start, frag_bc.len,
+ NULL, &pass_p->ps);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create pixel shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ D3D11_INPUT_ELEMENT_DESC *in_descs = talloc_array(pass,
+ D3D11_INPUT_ELEMENT_DESC, params->num_vertex_attribs);
+ for (int i = 0; i < params->num_vertex_attribs; i++) {
+ struct ra_renderpass_input *inp = &params->vertex_attribs[i];
+
+ DXGI_FORMAT fmt = DXGI_FORMAT_UNKNOWN;
+ switch (inp->type) {
+ case RA_VARTYPE_FLOAT:
+ switch (inp->dim_v) {
+ case 1: fmt = DXGI_FORMAT_R32_FLOAT; break;
+ case 2: fmt = DXGI_FORMAT_R32G32_FLOAT; break;
+ case 3: fmt = DXGI_FORMAT_R32G32B32_FLOAT; break;
+ case 4: fmt = DXGI_FORMAT_R32G32B32A32_FLOAT; break;
+ }
+ break;
+ case RA_VARTYPE_BYTE_UNORM:
+ switch (inp->dim_v) {
+ case 1: fmt = DXGI_FORMAT_R8_UNORM; break;
+ case 2: fmt = DXGI_FORMAT_R8G8_UNORM; break;
+ // There is no 3-component 8-bit DXGI format
+ case 4: fmt = DXGI_FORMAT_R8G8B8A8_UNORM; break;
+ }
+ break;
+ }
+ if (fmt == DXGI_FORMAT_UNKNOWN) {
+ MP_ERR(ra, "Could not find suitable vertex input format\n");
+ goto error;
+ }
+
+ in_descs[i] = (D3D11_INPUT_ELEMENT_DESC) {
+ // The semantic name doesn't mean much and is just used to verify
+ // the input description matches the shader. SPIRV-Cross always
+ // uses TEXCOORD, so we should too.
+ .SemanticName = "TEXCOORD",
+ .SemanticIndex = i,
+ .AlignedByteOffset = inp->offset,
+ .Format = fmt,
+ };
+ }
+
+ hr = ID3D11Device_CreateInputLayout(p->dev, in_descs,
+ params->num_vertex_attribs, vert_bc.start, vert_bc.len,
+ &pass_p->layout);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create IA layout: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ talloc_free(in_descs);
+ in_descs = NULL;
+
+ D3D11_BLEND_DESC bdesc = {
+ .RenderTarget[0] = {
+ .BlendEnable = params->enable_blend,
+ .SrcBlend = map_ra_blend(params->blend_src_rgb),
+ .DestBlend = map_ra_blend(params->blend_dst_rgb),
+ .BlendOp = D3D11_BLEND_OP_ADD,
+ .SrcBlendAlpha = map_ra_blend(params->blend_src_alpha),
+ .DestBlendAlpha = map_ra_blend(params->blend_dst_alpha),
+ .BlendOpAlpha = D3D11_BLEND_OP_ADD,
+ .RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL,
+ },
+ };
+ hr = ID3D11Device_CreateBlendState(p->dev, &bdesc, &pass_p->bstate);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create blend state: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ save_cached_program(ra, pass, vert_bc, frag_bc, (bstr){0});
+
+ SAFE_RELEASE(vs_blob);
+ SAFE_RELEASE(ps_blob);
+ return pass;
+
+error:
+ renderpass_destroy(ra, pass);
+ SAFE_RELEASE(vs_blob);
+ SAFE_RELEASE(ps_blob);
+ return NULL;
+}
+
+static struct ra_renderpass *renderpass_create_compute(struct ra *ra,
+ struct ra_renderpass *pass, const struct ra_renderpass_params *params)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_rpass *pass_p = pass->priv;
+ ID3DBlob *cs_blob = NULL;
+ HRESULT hr;
+
+ bstr comp_bc = {0};
+ load_cached_program(ra, params, NULL, NULL, &comp_bc);
+
+ if (!comp_bc.start) {
+ if (!compile_glsl(ra, GLSL_SHADER_COMPUTE, params->compute_shader,
+ &cs_blob))
+ goto error;
+ comp_bc = (bstr){
+ ID3D10Blob_GetBufferPointer(cs_blob),
+ ID3D10Blob_GetBufferSize(cs_blob),
+ };
+ }
+ hr = ID3D11Device_CreateComputeShader(p->dev, comp_bc.start, comp_bc.len,
+ NULL, &pass_p->cs);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create compute shader: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ save_cached_program(ra, pass, (bstr){0}, (bstr){0}, comp_bc);
+
+ SAFE_RELEASE(cs_blob);
+ return pass;
+error:
+ renderpass_destroy(ra, pass);
+ SAFE_RELEASE(cs_blob);
+ return NULL;
+}
+
+static struct ra_renderpass *renderpass_create(struct ra *ra,
+ const struct ra_renderpass_params *params)
+{
+ struct ra_renderpass *pass = talloc_zero(NULL, struct ra_renderpass);
+ pass->params = *ra_renderpass_params_copy(pass, params);
+ pass->params.cached_program = (bstr){0};
+ pass->priv = talloc_zero(pass, struct d3d_rpass);
+
+ if (params->type == RA_RENDERPASS_TYPE_COMPUTE) {
+ return renderpass_create_compute(ra, pass, params);
+ } else {
+ return renderpass_create_raster(ra, pass, params);
+ }
+}
+
+static void renderpass_run_raster(struct ra *ra,
+ const struct ra_renderpass_run_params *params,
+ ID3D11Buffer *ubos[], int ubos_len,
+ ID3D11SamplerState *samplers[],
+ ID3D11ShaderResourceView *srvs[],
+ int samplers_len,
+ ID3D11UnorderedAccessView *uavs[],
+ int uavs_len)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct ra_renderpass *pass = params->pass;
+ struct d3d_rpass *pass_p = pass->priv;
+
+ UINT vbuf_offset = vbuf_upload(ra, params->vertex_data,
+ pass->params.vertex_stride * params->vertex_count);
+ if (vbuf_offset == (UINT)-1)
+ return;
+
+ ID3D11DeviceContext_IASetInputLayout(p->ctx, pass_p->layout);
+ ID3D11DeviceContext_IASetVertexBuffers(p->ctx, 0, 1, &p->vbuf,
+ &pass->params.vertex_stride, &vbuf_offset);
+ ID3D11DeviceContext_IASetPrimitiveTopology(p->ctx,
+ D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+
+ ID3D11DeviceContext_VSSetShader(p->ctx, pass_p->vs, NULL, 0);
+
+ ID3D11DeviceContext_RSSetViewports(p->ctx, 1, (&(D3D11_VIEWPORT) {
+ .TopLeftX = params->viewport.x0,
+ .TopLeftY = params->viewport.y0,
+ .Width = mp_rect_w(params->viewport),
+ .Height = mp_rect_h(params->viewport),
+ .MinDepth = 0,
+ .MaxDepth = 1,
+ }));
+ ID3D11DeviceContext_RSSetScissorRects(p->ctx, 1, (&(D3D11_RECT) {
+ .left = params->scissors.x0,
+ .top = params->scissors.y0,
+ .right = params->scissors.x1,
+ .bottom = params->scissors.y1,
+ }));
+ ID3D11DeviceContext_PSSetShader(p->ctx, pass_p->ps, NULL, 0);
+ ID3D11DeviceContext_PSSetConstantBuffers(p->ctx, 0, ubos_len, ubos);
+ ID3D11DeviceContext_PSSetShaderResources(p->ctx, 0, samplers_len, srvs);
+ ID3D11DeviceContext_PSSetSamplers(p->ctx, 0, samplers_len, samplers);
+
+ struct ra_tex *target = params->target;
+ struct d3d_tex *target_p = target->priv;
+ ID3D11DeviceContext_OMSetRenderTargetsAndUnorderedAccessViews(p->ctx, 1,
+ &target_p->rtv, NULL, 1, uavs_len, uavs, NULL);
+ ID3D11DeviceContext_OMSetBlendState(p->ctx, pass_p->bstate, NULL,
+ D3D11_DEFAULT_SAMPLE_MASK);
+
+ ID3D11DeviceContext_Draw(p->ctx, params->vertex_count, 0);
+
+ // Unbind everything. It's easier to do this than to actually track state,
+ // and if we leave the RTV bound, it could trip up D3D's conflict checker.
+ for (int i = 0; i < ubos_len; i++)
+ ubos[i] = NULL;
+ for (int i = 0; i < samplers_len; i++) {
+ samplers[i] = NULL;
+ srvs[i] = NULL;
+ }
+ for (int i = 0; i < uavs_len; i++)
+ uavs[i] = NULL;
+ ID3D11DeviceContext_PSSetConstantBuffers(p->ctx, 0, ubos_len, ubos);
+ ID3D11DeviceContext_PSSetShaderResources(p->ctx, 0, samplers_len, srvs);
+ ID3D11DeviceContext_PSSetSamplers(p->ctx, 0, samplers_len, samplers);
+ ID3D11DeviceContext_OMSetRenderTargetsAndUnorderedAccessViews(p->ctx, 0,
+ NULL, NULL, 1, uavs_len, uavs, NULL);
+}
+
+static void renderpass_run_compute(struct ra *ra,
+ const struct ra_renderpass_run_params *params,
+ ID3D11Buffer *ubos[], int ubos_len,
+ ID3D11SamplerState *samplers[],
+ ID3D11ShaderResourceView *srvs[],
+ int samplers_len,
+ ID3D11UnorderedAccessView *uavs[],
+ int uavs_len)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct ra_renderpass *pass = params->pass;
+ struct d3d_rpass *pass_p = pass->priv;
+
+ ID3D11DeviceContext_CSSetShader(p->ctx, pass_p->cs, NULL, 0);
+ ID3D11DeviceContext_CSSetConstantBuffers(p->ctx, 0, ubos_len, ubos);
+ ID3D11DeviceContext_CSSetShaderResources(p->ctx, 0, samplers_len, srvs);
+ ID3D11DeviceContext_CSSetSamplers(p->ctx, 0, samplers_len, samplers);
+ ID3D11DeviceContext_CSSetUnorderedAccessViews(p->ctx, 0, uavs_len, uavs,
+ NULL);
+
+ ID3D11DeviceContext_Dispatch(p->ctx, params->compute_groups[0],
+ params->compute_groups[1],
+ params->compute_groups[2]);
+
+ for (int i = 0; i < ubos_len; i++)
+ ubos[i] = NULL;
+ for (int i = 0; i < samplers_len; i++) {
+ samplers[i] = NULL;
+ srvs[i] = NULL;
+ }
+ for (int i = 0; i < uavs_len; i++)
+ uavs[i] = NULL;
+ ID3D11DeviceContext_CSSetConstantBuffers(p->ctx, 0, ubos_len, ubos);
+ ID3D11DeviceContext_CSSetShaderResources(p->ctx, 0, samplers_len, srvs);
+ ID3D11DeviceContext_CSSetSamplers(p->ctx, 0, samplers_len, samplers);
+ ID3D11DeviceContext_CSSetUnorderedAccessViews(p->ctx, 0, uavs_len, uavs,
+ NULL);
+}
+
+static void renderpass_run(struct ra *ra,
+ const struct ra_renderpass_run_params *params)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct ra_renderpass *pass = params->pass;
+ enum ra_renderpass_type type = pass->params.type;
+
+ ID3D11Buffer *ubos[D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT] = {0};
+ int ubos_len = 0;
+
+ ID3D11SamplerState *samplers[D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT] = {0};
+ ID3D11ShaderResourceView *srvs[D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT] = {0};
+ int samplers_len = 0;
+
+ ID3D11UnorderedAccessView *uavs[D3D11_1_UAV_SLOT_COUNT] = {0};
+ int uavs_len = 0;
+
+ // In a raster pass, one of the UAV slots is used by the runtime for the RTV
+ int uavs_max = type == RA_RENDERPASS_TYPE_COMPUTE ? p->max_uavs
+ : p->max_uavs - 1;
+
+ // Gather the input variables used in this pass. These will be mapped to
+ // HLSL registers.
+ for (int i = 0; i < params->num_values; i++) {
+ struct ra_renderpass_input_val *val = &params->values[i];
+ int binding = pass->params.inputs[val->index].binding;
+ switch (pass->params.inputs[val->index].type) {
+ case RA_VARTYPE_BUF_RO:
+ if (binding >= MP_ARRAY_SIZE(ubos)) {
+ MP_ERR(ra, "Too many constant buffers in pass\n");
+ return;
+ }
+ struct ra_buf *buf_ro = *(struct ra_buf **)val->data;
+ buf_resolve(ra, buf_ro);
+ struct d3d_buf *buf_ro_p = buf_ro->priv;
+ ubos[binding] = buf_ro_p->buf;
+ ubos_len = MPMAX(ubos_len, binding + 1);
+ break;
+ case RA_VARTYPE_BUF_RW:
+ if (binding > uavs_max) {
+ MP_ERR(ra, "Too many UAVs in pass\n");
+ return;
+ }
+ struct ra_buf *buf_rw = *(struct ra_buf **)val->data;
+ buf_resolve(ra, buf_rw);
+ struct d3d_buf *buf_rw_p = buf_rw->priv;
+ uavs[binding] = buf_rw_p->uav;
+ uavs_len = MPMAX(uavs_len, binding + 1);
+ break;
+ case RA_VARTYPE_TEX:
+ if (binding >= MP_ARRAY_SIZE(samplers)) {
+ MP_ERR(ra, "Too many textures in pass\n");
+ return;
+ }
+ struct ra_tex *tex = *(struct ra_tex **)val->data;
+ struct d3d_tex *tex_p = tex->priv;
+ samplers[binding] = tex_p->sampler;
+ srvs[binding] = tex_p->srv;
+ samplers_len = MPMAX(samplers_len, binding + 1);
+ break;
+ case RA_VARTYPE_IMG_W:
+ if (binding > uavs_max) {
+ MP_ERR(ra, "Too many UAVs in pass\n");
+ return;
+ }
+ struct ra_tex *img = *(struct ra_tex **)val->data;
+ struct d3d_tex *img_p = img->priv;
+ uavs[binding] = img_p->uav;
+ uavs_len = MPMAX(uavs_len, binding + 1);
+ break;
+ }
+ }
+
+ if (type == RA_RENDERPASS_TYPE_COMPUTE) {
+ renderpass_run_compute(ra, params, ubos, ubos_len, samplers, srvs,
+ samplers_len, uavs, uavs_len);
+ } else {
+ renderpass_run_raster(ra, params, ubos, ubos_len, samplers, srvs,
+ samplers_len, uavs, uavs_len);
+ }
+}
+
+static void timer_destroy(struct ra *ra, ra_timer *ratimer)
+{
+ if (!ratimer)
+ return;
+ struct d3d_timer *timer = ratimer;
+
+ SAFE_RELEASE(timer->ts_start);
+ SAFE_RELEASE(timer->ts_end);
+ SAFE_RELEASE(timer->disjoint);
+ talloc_free(timer);
+}
+
+static ra_timer *timer_create(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ if (!p->has_timestamp_queries)
+ return NULL;
+
+ struct d3d_timer *timer = talloc_zero(NULL, struct d3d_timer);
+ HRESULT hr;
+
+ hr = ID3D11Device_CreateQuery(p->dev,
+ &(D3D11_QUERY_DESC) { D3D11_QUERY_TIMESTAMP }, &timer->ts_start);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create start query: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ hr = ID3D11Device_CreateQuery(p->dev,
+ &(D3D11_QUERY_DESC) { D3D11_QUERY_TIMESTAMP }, &timer->ts_end);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create end query: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ // Measuring duration in D3D11 requires three queries: start and end
+ // timestamps, and a disjoint query containing a flag which says whether
+ // the timestamps are usable or if a discontinuity occurred between them,
+ // like a change in power state or clock speed. The disjoint query also
+ // contains the timer frequency, so the timestamps are useless without it.
+ hr = ID3D11Device_CreateQuery(p->dev,
+ &(D3D11_QUERY_DESC) { D3D11_QUERY_TIMESTAMP_DISJOINT }, &timer->disjoint);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create timer query: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+
+ return timer;
+error:
+ timer_destroy(ra, timer);
+ return NULL;
+}
+
+static uint64_t timestamp_to_ns(uint64_t timestamp, uint64_t freq)
+{
+ static const uint64_t ns_per_s = 1000000000llu;
+ return timestamp / freq * ns_per_s + timestamp % freq * ns_per_s / freq;
+}
+
+static uint64_t timer_get_result(struct ra *ra, ra_timer *ratimer)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_timer *timer = ratimer;
+ HRESULT hr;
+
+ UINT64 start, end;
+ D3D11_QUERY_DATA_TIMESTAMP_DISJOINT dj;
+
+ hr = ID3D11DeviceContext_GetData(p->ctx,
+ (ID3D11Asynchronous *)timer->ts_end, &end, sizeof(end),
+ D3D11_ASYNC_GETDATA_DONOTFLUSH);
+ if (FAILED(hr) || hr == S_FALSE)
+ return 0;
+ hr = ID3D11DeviceContext_GetData(p->ctx,
+ (ID3D11Asynchronous *)timer->ts_start, &start, sizeof(start),
+ D3D11_ASYNC_GETDATA_DONOTFLUSH);
+ if (FAILED(hr) || hr == S_FALSE)
+ return 0;
+ hr = ID3D11DeviceContext_GetData(p->ctx,
+ (ID3D11Asynchronous *)timer->disjoint, &dj, sizeof(dj),
+ D3D11_ASYNC_GETDATA_DONOTFLUSH);
+ if (FAILED(hr) || hr == S_FALSE || dj.Disjoint || !dj.Frequency)
+ return 0;
+
+ return timestamp_to_ns(end - start, dj.Frequency);
+}
+
+static void timer_start(struct ra *ra, ra_timer *ratimer)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_timer *timer = ratimer;
+
+ // Latch the last result of this ra_timer (returned by timer_stop)
+ timer->result = timer_get_result(ra, ratimer);
+
+ ID3D11DeviceContext_Begin(p->ctx, (ID3D11Asynchronous *)timer->disjoint);
+ ID3D11DeviceContext_End(p->ctx, (ID3D11Asynchronous *)timer->ts_start);
+}
+
+static uint64_t timer_stop(struct ra *ra, ra_timer *ratimer)
+{
+ struct ra_d3d11 *p = ra->priv;
+ struct d3d_timer *timer = ratimer;
+
+ ID3D11DeviceContext_End(p->ctx, (ID3D11Asynchronous *)timer->ts_end);
+ ID3D11DeviceContext_End(p->ctx, (ID3D11Asynchronous *)timer->disjoint);
+
+ return timer->result;
+}
+
+static int map_msg_severity(D3D11_MESSAGE_SEVERITY sev)
+{
+ switch (sev) {
+ case D3D11_MESSAGE_SEVERITY_CORRUPTION:
+ return MSGL_FATAL;
+ case D3D11_MESSAGE_SEVERITY_ERROR:
+ return MSGL_ERR;
+ case D3D11_MESSAGE_SEVERITY_WARNING:
+ return MSGL_WARN;
+ default:
+ case D3D11_MESSAGE_SEVERITY_INFO:
+ case D3D11_MESSAGE_SEVERITY_MESSAGE:
+ return MSGL_DEBUG;
+ }
+}
+
+static int map_msg_severity_by_id(D3D11_MESSAGE_ID id,
+ D3D11_MESSAGE_SEVERITY sev)
+{
+ switch (id) {
+ // These are normal. The RA timer queue habitually reuses timer objects
+ // without retrieving the results.
+ case D3D11_MESSAGE_ID_QUERY_BEGIN_ABANDONING_PREVIOUS_RESULTS:
+ case D3D11_MESSAGE_ID_QUERY_END_ABANDONING_PREVIOUS_RESULTS:
+ return MSGL_TRACE;
+
+ // D3D11 writes log messages every time an object is created or
+ // destroyed. That results in a lot of log spam, so force MSGL_TRACE.
+#define OBJ_LIFETIME_MESSAGES(obj) \
+ case D3D11_MESSAGE_ID_CREATE_ ## obj: \
+ case D3D11_MESSAGE_ID_DESTROY_ ## obj
+
+ OBJ_LIFETIME_MESSAGES(CONTEXT):
+ OBJ_LIFETIME_MESSAGES(BUFFER):
+ OBJ_LIFETIME_MESSAGES(TEXTURE1D):
+ OBJ_LIFETIME_MESSAGES(TEXTURE2D):
+ OBJ_LIFETIME_MESSAGES(TEXTURE3D):
+ OBJ_LIFETIME_MESSAGES(SHADERRESOURCEVIEW):
+ OBJ_LIFETIME_MESSAGES(RENDERTARGETVIEW):
+ OBJ_LIFETIME_MESSAGES(DEPTHSTENCILVIEW):
+ OBJ_LIFETIME_MESSAGES(VERTEXSHADER):
+ OBJ_LIFETIME_MESSAGES(HULLSHADER):
+ OBJ_LIFETIME_MESSAGES(DOMAINSHADER):
+ OBJ_LIFETIME_MESSAGES(GEOMETRYSHADER):
+ OBJ_LIFETIME_MESSAGES(PIXELSHADER):
+ OBJ_LIFETIME_MESSAGES(INPUTLAYOUT):
+ OBJ_LIFETIME_MESSAGES(SAMPLER):
+ OBJ_LIFETIME_MESSAGES(BLENDSTATE):
+ OBJ_LIFETIME_MESSAGES(DEPTHSTENCILSTATE):
+ OBJ_LIFETIME_MESSAGES(RASTERIZERSTATE):
+ OBJ_LIFETIME_MESSAGES(QUERY):
+ OBJ_LIFETIME_MESSAGES(PREDICATE):
+ OBJ_LIFETIME_MESSAGES(COUNTER):
+ OBJ_LIFETIME_MESSAGES(COMMANDLIST):
+ OBJ_LIFETIME_MESSAGES(CLASSINSTANCE):
+ OBJ_LIFETIME_MESSAGES(CLASSLINKAGE):
+ OBJ_LIFETIME_MESSAGES(COMPUTESHADER):
+ OBJ_LIFETIME_MESSAGES(UNORDEREDACCESSVIEW):
+ OBJ_LIFETIME_MESSAGES(VIDEODECODER):
+ OBJ_LIFETIME_MESSAGES(VIDEOPROCESSORENUM):
+ OBJ_LIFETIME_MESSAGES(VIDEOPROCESSOR):
+ OBJ_LIFETIME_MESSAGES(DECODEROUTPUTVIEW):
+ OBJ_LIFETIME_MESSAGES(PROCESSORINPUTVIEW):
+ OBJ_LIFETIME_MESSAGES(PROCESSOROUTPUTVIEW):
+ OBJ_LIFETIME_MESSAGES(DEVICECONTEXTSTATE):
+ OBJ_LIFETIME_MESSAGES(FENCE):
+ return MSGL_TRACE;
+
+#undef OBJ_LIFETIME_MESSAGES
+
+ default:
+ return map_msg_severity(sev);
+ }
+}
+
+static void debug_marker(struct ra *ra, const char *msg)
+{
+ struct ra_d3d11 *p = ra->priv;
+ void *talloc_ctx = talloc_new(NULL);
+ HRESULT hr;
+
+ if (!p->iqueue)
+ goto done;
+
+ // Copy debug-layer messages to mpv's log output
+ bool printed_header = false;
+ uint64_t messages = ID3D11InfoQueue_GetNumStoredMessages(p->iqueue);
+ for (uint64_t i = 0; i < messages; i++) {
+ SIZE_T len;
+ hr = ID3D11InfoQueue_GetMessage(p->iqueue, i, NULL, &len);
+ if (FAILED(hr) || !len)
+ goto done;
+
+ D3D11_MESSAGE *d3dmsg = talloc_size(talloc_ctx, len);
+ hr = ID3D11InfoQueue_GetMessage(p->iqueue, i, d3dmsg, &len);
+ if (FAILED(hr))
+ goto done;
+
+ int msgl = map_msg_severity_by_id(d3dmsg->ID, d3dmsg->Severity);
+ if (mp_msg_test(ra->log, msgl)) {
+ if (!printed_header)
+ MP_INFO(ra, "%s:\n", msg);
+ printed_header = true;
+
+ MP_MSG(ra, msgl, "%d: %.*s\n", (int)d3dmsg->ID,
+ (int)d3dmsg->DescriptionByteLength, d3dmsg->pDescription);
+ talloc_free(d3dmsg);
+ }
+ }
+
+ ID3D11InfoQueue_ClearStoredMessages(p->iqueue);
+done:
+ talloc_free(talloc_ctx);
+}
+
+static void destroy(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+
+ // Release everything except the interfaces needed to perform leak checking
+ SAFE_RELEASE(p->clear_ps);
+ SAFE_RELEASE(p->clear_vs);
+ SAFE_RELEASE(p->clear_layout);
+ SAFE_RELEASE(p->clear_vbuf);
+ SAFE_RELEASE(p->clear_cbuf);
+ SAFE_RELEASE(p->blit_float_ps);
+ SAFE_RELEASE(p->blit_vs);
+ SAFE_RELEASE(p->blit_layout);
+ SAFE_RELEASE(p->blit_vbuf);
+ SAFE_RELEASE(p->blit_sampler);
+ SAFE_RELEASE(p->vbuf);
+ SAFE_RELEASE(p->ctx1);
+ SAFE_RELEASE(p->dev1);
+ SAFE_RELEASE(p->dev);
+
+ if (p->ctx) {
+ // Destroy the device context synchronously so referenced objects don't
+ // show up in the leak check
+ ID3D11DeviceContext_ClearState(p->ctx);
+ ID3D11DeviceContext_Flush(p->ctx);
+ }
+ SAFE_RELEASE(p->ctx);
+
+ if (p->debug) {
+ // Report any leaked objects
+ debug_marker(ra, "after destroy");
+ ID3D11Debug_ReportLiveDeviceObjects(p->debug, D3D11_RLDO_DETAIL);
+ debug_marker(ra, "after leak check");
+ ID3D11Debug_ReportLiveDeviceObjects(p->debug, D3D11_RLDO_SUMMARY);
+ debug_marker(ra, "after leak summary");
+ }
+ SAFE_RELEASE(p->debug);
+ SAFE_RELEASE(p->iqueue);
+
+ talloc_free(ra);
+}
+
+static struct ra_fns ra_fns_d3d11 = {
+ .destroy = destroy,
+ .tex_create = tex_create,
+ .tex_destroy = tex_destroy,
+ .tex_upload = tex_upload,
+ .tex_download = tex_download,
+ .buf_create = buf_create,
+ .buf_destroy = buf_destroy,
+ .buf_update = buf_update,
+ .clear = clear,
+ .blit = blit,
+ .uniform_layout = std140_layout,
+ .desc_namespace = desc_namespace,
+ .renderpass_create = renderpass_create,
+ .renderpass_destroy = renderpass_destroy,
+ .renderpass_run = renderpass_run,
+ .timer_create = timer_create,
+ .timer_destroy = timer_destroy,
+ .timer_start = timer_start,
+ .timer_stop = timer_stop,
+ .debug_marker = debug_marker,
+};
+
+void ra_d3d11_flush(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ ID3D11DeviceContext_Flush(p->ctx);
+}
+
+static void init_debug_layer(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ HRESULT hr;
+
+ hr = ID3D11Device_QueryInterface(p->dev, &IID_ID3D11Debug,
+ (void**)&p->debug);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to get debug device: %s\n", mp_HRESULT_to_str(hr));
+ return;
+ }
+
+ hr = ID3D11Device_QueryInterface(p->dev, &IID_ID3D11InfoQueue,
+ (void**)&p->iqueue);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to get info queue: %s\n", mp_HRESULT_to_str(hr));
+ return;
+ }
+
+ // Store an unlimited amount of messages in the buffer. This is fine
+ // because we flush stored messages regularly (in debug_marker.)
+ ID3D11InfoQueue_SetMessageCountLimit(p->iqueue, -1);
+
+ // Push empty filter to get everything
+ D3D11_INFO_QUEUE_FILTER filter = {0};
+ ID3D11InfoQueue_PushStorageFilter(p->iqueue, &filter);
+}
+
+static struct dll_version get_dll_version(HMODULE dll)
+{
+ void *ctx = talloc_new(NULL);
+ struct dll_version ret = { 0 };
+
+ HRSRC rsrc = FindResourceW(dll, MAKEINTRESOURCEW(VS_VERSION_INFO),
+ VS_FILE_INFO);
+ if (!rsrc)
+ goto done;
+ DWORD size = SizeofResource(dll, rsrc);
+ HGLOBAL res = LoadResource(dll, rsrc);
+ if (!res)
+ goto done;
+ void *ptr = LockResource(res);
+ if (!ptr)
+ goto done;
+ void *copy = talloc_memdup(ctx, ptr, size);
+
+ VS_FIXEDFILEINFO *ffi;
+ UINT ffi_len;
+ if (!VerQueryValueW(copy, L"\\", (void**)&ffi, &ffi_len))
+ goto done;
+ if (ffi_len < sizeof(*ffi))
+ goto done;
+
+ ret.major = HIWORD(ffi->dwFileVersionMS);
+ ret.minor = LOWORD(ffi->dwFileVersionMS);
+ ret.build = HIWORD(ffi->dwFileVersionLS);
+ ret.revision = LOWORD(ffi->dwFileVersionLS);
+
+done:
+ talloc_free(ctx);
+ return ret;
+}
+
+static bool load_d3d_compiler(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ HMODULE d3dcompiler = NULL;
+
+ // Try the inbox D3DCompiler first (Windows 8.1 and up)
+ if (IsWindows8Point1OrGreater()) {
+ d3dcompiler = LoadLibraryExW(L"d3dcompiler_47.dll", NULL,
+ LOAD_LIBRARY_SEARCH_SYSTEM32);
+ }
+ // Check for a packaged version of d3dcompiler_47.dll
+ if (!d3dcompiler)
+ d3dcompiler = LoadLibraryW(L"d3dcompiler_47.dll");
+ // Try d3dcompiler_46.dll from the Windows 8 SDK
+ if (!d3dcompiler)
+ d3dcompiler = LoadLibraryW(L"d3dcompiler_46.dll");
+ // Try d3dcompiler_43.dll from the June 2010 DirectX SDK
+ if (!d3dcompiler)
+ d3dcompiler = LoadLibraryW(L"d3dcompiler_43.dll");
+ // Can't find any compiler DLL, so give up
+ if (!d3dcompiler)
+ return false;
+
+ p->d3d_compiler_ver = get_dll_version(d3dcompiler);
+
+ p->D3DCompile = (pD3DCompile)GetProcAddress(d3dcompiler, "D3DCompile");
+ if (!p->D3DCompile)
+ return false;
+ return true;
+}
+
+static void find_max_texture_dimension(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+
+ D3D11_TEXTURE2D_DESC desc = {
+ .Width = ra->max_texture_wh,
+ .Height = ra->max_texture_wh,
+ .MipLevels = 1,
+ .ArraySize = 1,
+ .SampleDesc.Count = 1,
+ .Format = DXGI_FORMAT_R8_UNORM,
+ .BindFlags = D3D11_BIND_SHADER_RESOURCE,
+ };
+ while (true) {
+ desc.Height = desc.Width *= 2;
+ if (desc.Width >= 0x8000000u)
+ return;
+ if (FAILED(ID3D11Device_CreateTexture2D(p->dev, &desc, NULL, NULL)))
+ return;
+ ra->max_texture_wh = desc.Width;
+ }
+}
+
+struct ra *ra_d3d11_create(ID3D11Device *dev, struct mp_log *log,
+ struct spirv_compiler *spirv)
+{
+ HRESULT hr;
+
+ struct ra *ra = talloc_zero(NULL, struct ra);
+ ra->log = log;
+ ra->fns = &ra_fns_d3d11;
+
+ // Even Direct3D 10level9 supports 3D textures
+ ra->caps = RA_CAP_TEX_3D | RA_CAP_DIRECT_UPLOAD | RA_CAP_BUF_RO |
+ RA_CAP_BLIT | spirv->ra_caps;
+
+ ra->glsl_version = spirv->glsl_version;
+ ra->glsl_vulkan = true;
+
+ struct ra_d3d11 *p = ra->priv = talloc_zero(ra, struct ra_d3d11);
+ p->spirv = spirv;
+
+ int minor = 0;
+ ID3D11Device_AddRef(dev);
+ p->dev = dev;
+ ID3D11Device_GetImmediateContext(p->dev, &p->ctx);
+ hr = ID3D11Device_QueryInterface(p->dev, &IID_ID3D11Device1,
+ (void**)&p->dev1);
+ if (SUCCEEDED(hr)) {
+ minor = 1;
+ ID3D11Device1_GetImmediateContext1(p->dev1, &p->ctx1);
+
+ D3D11_FEATURE_DATA_D3D11_OPTIONS fopts = { 0 };
+ hr = ID3D11Device_CheckFeatureSupport(p->dev,
+ D3D11_FEATURE_D3D11_OPTIONS, &fopts, sizeof(fopts));
+ if (SUCCEEDED(hr)) {
+ p->has_clear_view = fopts.ClearView;
+ }
+ }
+
+ MP_VERBOSE(ra, "Using Direct3D 11.%d runtime\n", minor);
+
+ p->fl = ID3D11Device_GetFeatureLevel(p->dev);
+ if (p->fl >= D3D_FEATURE_LEVEL_11_0) {
+ ra->max_texture_wh = D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ } else if (p->fl >= D3D_FEATURE_LEVEL_10_0) {
+ ra->max_texture_wh = D3D10_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ } else if (p->fl >= D3D_FEATURE_LEVEL_9_3) {
+ ra->max_texture_wh = D3D_FL9_3_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ } else {
+ ra->max_texture_wh = D3D_FL9_1_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ }
+
+ if (p->fl >= D3D_FEATURE_LEVEL_11_0)
+ ra->caps |= RA_CAP_GATHER;
+ if (p->fl >= D3D_FEATURE_LEVEL_10_0)
+ ra->caps |= RA_CAP_FRAGCOORD;
+
+ // Some 10_0 hardware has compute shaders, but only 11_0 has image load/store
+ if (p->fl >= D3D_FEATURE_LEVEL_11_0) {
+ ra->caps |= RA_CAP_COMPUTE | RA_CAP_BUF_RW;
+ ra->max_shmem = 32 * 1024;
+ ra->max_compute_group_threads =
+ D3D11_CS_THREAD_GROUP_MAX_THREADS_PER_GROUP;
+ }
+
+ if (p->fl >= D3D_FEATURE_LEVEL_11_1) {
+ p->max_uavs = D3D11_1_UAV_SLOT_COUNT;
+ } else {
+ p->max_uavs = D3D11_PS_CS_UAV_REGISTER_COUNT;
+ }
+
+ if (ID3D11Device_GetCreationFlags(p->dev) & D3D11_CREATE_DEVICE_DEBUG)
+ init_debug_layer(ra);
+
+ // Some level 9_x devices don't have timestamp queries
+ hr = ID3D11Device_CreateQuery(p->dev,
+ &(D3D11_QUERY_DESC) { D3D11_QUERY_TIMESTAMP }, NULL);
+ p->has_timestamp_queries = SUCCEEDED(hr);
+
+ debug_marker(ra, "before maximum Texture2D size lookup");
+
+ // According to MSDN, the above texture sizes are just minimums and drivers
+ // may support larger textures. See:
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/ff476874.aspx
+ find_max_texture_dimension(ra);
+
+ // Ignore any messages during find_max_texture_dimension
+ if (p->iqueue)
+ ID3D11InfoQueue_ClearStoredMessages(p->iqueue);
+
+ MP_VERBOSE(ra, "Maximum Texture2D size: %dx%d\n", ra->max_texture_wh,
+ ra->max_texture_wh);
+
+ if (!load_d3d_compiler(ra)) {
+ MP_FATAL(ra, "Could not find D3DCompiler DLL\n");
+ goto error;
+ }
+
+ MP_VERBOSE(ra, "D3DCompiler version: %u.%u.%u.%u\n",
+ p->d3d_compiler_ver.major, p->d3d_compiler_ver.minor,
+ p->d3d_compiler_ver.build, p->d3d_compiler_ver.revision);
+
+ setup_formats(ra);
+
+ // The rasterizer state never changes, so set it up here
+ ID3D11RasterizerState *rstate;
+ D3D11_RASTERIZER_DESC rdesc = {
+ .FillMode = D3D11_FILL_SOLID,
+ .CullMode = D3D11_CULL_NONE,
+ .FrontCounterClockwise = FALSE,
+ .DepthClipEnable = TRUE, // Required for 10level9
+ .ScissorEnable = TRUE,
+ };
+ hr = ID3D11Device_CreateRasterizerState(p->dev, &rdesc, &rstate);
+ if (FAILED(hr)) {
+ MP_ERR(ra, "Failed to create rasterizer state: %s\n", mp_HRESULT_to_str(hr));
+ goto error;
+ }
+ ID3D11DeviceContext_RSSetState(p->ctx, rstate);
+ SAFE_RELEASE(rstate);
+
+ // If the device doesn't support ClearView, we have to set up a
+ // shader-based clear() implementation
+ if (!p->has_clear_view && !setup_clear_rpass(ra))
+ goto error;
+
+ if (!setup_blit_rpass(ra))
+ goto error;
+
+ return ra;
+
+error:
+ destroy(ra);
+ return NULL;
+}
+
+ID3D11Device *ra_d3d11_get_device(struct ra *ra)
+{
+ struct ra_d3d11 *p = ra->priv;
+ ID3D11Device_AddRef(p->dev);
+ return p->dev;
+}
+
+bool ra_is_d3d11(struct ra *ra)
+{
+ return ra->fns == &ra_fns_d3d11;
+}
diff --git a/video/out/d3d11/ra_d3d11.h b/video/out/d3d11/ra_d3d11.h
new file mode 100644
index 0000000..6f62a7f
--- /dev/null
+++ b/video/out/d3d11/ra_d3d11.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <stdbool.h>
+#include <windows.h>
+#include <d3d11.h>
+#include <dxgi1_2.h>
+
+#include "video/out/gpu/ra.h"
+#include "video/out/gpu/spirv.h"
+
+// Get the underlying DXGI format from an RA format
+DXGI_FORMAT ra_d3d11_get_format(const struct ra_format *fmt);
+
+// Gets the matching ra_format for a given DXGI format.
+// Returns a nullptr in case of no known match.
+const struct ra_format *ra_d3d11_get_ra_format(struct ra *ra, DXGI_FORMAT fmt);
+
+// Create an RA instance from a D3D11 device. This takes a reference to the
+// device, which is released when the RA instance is destroyed.
+struct ra *ra_d3d11_create(ID3D11Device *device, struct mp_log *log,
+ struct spirv_compiler *spirv);
+
+// Flush the immediate context of the wrapped D3D11 device
+void ra_d3d11_flush(struct ra *ra);
+
+// Create an RA texture from a D3D11 resource. This takes a reference to the
+// texture, which is released when the RA texture is destroyed.
+struct ra_tex *ra_d3d11_wrap_tex(struct ra *ra, ID3D11Resource *res);
+
+// As above, but for a D3D11VA video resource. The fmt parameter selects which
+// plane of a planar format will be mapped when the RA texture is used.
+// array_slice should be set for texture arrays and is ignored for non-arrays.
+struct ra_tex *ra_d3d11_wrap_tex_video(struct ra *ra, ID3D11Texture2D *res,
+ int w, int h, int array_slice,
+ const struct ra_format *fmt);
+
+// Get the underlying D3D11 resource from an RA texture. The returned resource
+// is refcounted and must be released by the caller.
+ID3D11Resource *ra_d3d11_get_raw_tex(struct ra *ra, struct ra_tex *tex,
+ int *array_slice);
+
+// Get the underlying D3D11 device from an RA instance. The returned device is
+// refcounted and must be released by the caller.
+ID3D11Device *ra_d3d11_get_device(struct ra *ra);
+
+// True if the RA instance was created with ra_d3d11_create()
+bool ra_is_d3d11(struct ra *ra);
diff --git a/video/out/dither.c b/video/out/dither.c
new file mode 100644
index 0000000..44558ba
--- /dev/null
+++ b/video/out/dither.c
@@ -0,0 +1,175 @@
+/*
+ * Generate a dithering matrix for downsampling images.
+ *
+ * Copyright © 2013 Wessel Dankers <wsl@fruit.je>
+ *
+ * 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 <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <inttypes.h>
+#include <string.h>
+#include <assert.h>
+#include <math.h>
+
+#include <libavutil/lfg.h>
+
+#include "mpv_talloc.h"
+#include "dither.h"
+
+#define MAX_SIZEB 8
+#define MAX_SIZE (1 << MAX_SIZEB)
+#define MAX_SIZE2 (MAX_SIZE * MAX_SIZE)
+
+#define WRAP_SIZE2(k, x) ((unsigned int)((unsigned int)(x) & ((k)->size2 - 1)))
+#define XY(k, x, y) ((unsigned int)(((x) | ((y) << (k)->sizeb))))
+
+struct ctx {
+ unsigned int sizeb, size, size2;
+ unsigned int gauss_radius;
+ unsigned int gauss_middle;
+ uint64_t gauss[MAX_SIZE2];
+ unsigned int randomat[MAX_SIZE2];
+ bool calcmat[MAX_SIZE2];
+ uint64_t gaussmat[MAX_SIZE2];
+ unsigned int unimat[MAX_SIZE2];
+ AVLFG avlfg;
+};
+
+static void makegauss(struct ctx *k, unsigned int sizeb)
+{
+ assert(sizeb >= 1 && sizeb <= MAX_SIZEB);
+
+ av_lfg_init(&k->avlfg, 123);
+
+ k->sizeb = sizeb;
+ k->size = 1 << k->sizeb;
+ k->size2 = k->size * k->size;
+
+ k->gauss_radius = k->size / 2 - 1;
+ k->gauss_middle = XY(k, k->gauss_radius, k->gauss_radius);
+
+ unsigned int gauss_size = k->gauss_radius * 2 + 1;
+ unsigned int gauss_size2 = gauss_size * gauss_size;
+
+ for (unsigned int c = 0; c < k->size2; c++)
+ k->gauss[c] = 0;
+
+ double sigma = -log(1.5 / (double) UINT64_MAX * gauss_size2) / k->gauss_radius;
+
+ for (unsigned int gy = 0; gy <= k->gauss_radius; gy++) {
+ for (unsigned int gx = 0; gx <= gy; gx++) {
+ int cx = (int)gx - k->gauss_radius;
+ int cy = (int)gy - k->gauss_radius;
+ int sq = cx * cx + cy * cy;
+ double e = exp(-sqrt(sq) * sigma);
+ uint64_t v = e / gauss_size2 * (double) UINT64_MAX;
+ k->gauss[XY(k, gx, gy)] =
+ k->gauss[XY(k, gy, gx)] =
+ k->gauss[XY(k, gx, gauss_size - 1 - gy)] =
+ k->gauss[XY(k, gy, gauss_size - 1 - gx)] =
+ k->gauss[XY(k, gauss_size - 1 - gx, gy)] =
+ k->gauss[XY(k, gauss_size - 1 - gy, gx)] =
+ k->gauss[XY(k, gauss_size - 1 - gx, gauss_size - 1 - gy)] =
+ k->gauss[XY(k, gauss_size - 1 - gy, gauss_size - 1 - gx)] = v;
+ }
+ }
+ uint64_t total = 0;
+ for (unsigned int c = 0; c < k->size2; c++) {
+ uint64_t oldtotal = total;
+ total += k->gauss[c];
+ assert(total >= oldtotal);
+ }
+}
+
+static void setbit(struct ctx *k, unsigned int c)
+{
+ if (k->calcmat[c])
+ return;
+ k->calcmat[c] = true;
+ uint64_t *m = k->gaussmat;
+ uint64_t *me = k->gaussmat + k->size2;
+ uint64_t *g = k->gauss + WRAP_SIZE2(k, k->gauss_middle + k->size2 - c);
+ uint64_t *ge = k->gauss + k->size2;
+ while (g < ge)
+ *m++ += *g++;
+ g = k->gauss;
+ while (m < me)
+ *m++ += *g++;
+}
+
+static unsigned int getmin(struct ctx *k)
+{
+ uint64_t min = UINT64_MAX;
+ unsigned int resnum = 0;
+ unsigned int size2 = k->size2;
+ for (unsigned int c = 0; c < size2; c++) {
+ if (k->calcmat[c])
+ continue;
+ uint64_t total = k->gaussmat[c];
+ if (total <= min) {
+ if (total != min) {
+ min = total;
+ resnum = 0;
+ }
+ k->randomat[resnum++] = c;
+ }
+ }
+ if (resnum == 1)
+ return k->randomat[0];
+ if (resnum == size2)
+ return size2 / 2;
+ return k->randomat[av_lfg_get(&k->avlfg) % resnum];
+}
+
+static void makeuniform(struct ctx *k)
+{
+ unsigned int size2 = k->size2;
+ for (unsigned int c = 0; c < size2; c++) {
+ unsigned int r = getmin(k);
+ setbit(k, r);
+ k->unimat[r] = c;
+ }
+}
+
+// out_matrix is a reactangular tsize * tsize array, where tsize = (1 << size).
+void mp_make_fruit_dither_matrix(float *out_matrix, int size)
+{
+ struct ctx *k = talloc_zero(NULL, struct ctx);
+ makegauss(k, size);
+ makeuniform(k);
+ float invscale = k->size2;
+ for(unsigned int y = 0; y < k->size; y++) {
+ for(unsigned int x = 0; x < k->size; x++)
+ out_matrix[x + y * k->size] = k->unimat[XY(k, x, y)] / invscale;
+ }
+ talloc_free(k);
+}
+
+void mp_make_ordered_dither_matrix(unsigned char *m, int size)
+{
+ m[0] = 0;
+ for (int sz = 1; sz < size; sz *= 2) {
+ int offset[] = {sz*size, sz, sz * (size+1), 0};
+ for (int i = 0; i < 4; i++)
+ for (int y = 0; y < sz * size; y += size)
+ for (int x = 0; x < sz; x++)
+ m[x+y+offset[i]] = m[x+y] * 4 + (3-i) * 256/size/size;
+ }
+}
diff --git a/video/out/dither.h b/video/out/dither.h
new file mode 100644
index 0000000..ca804e3
--- /dev/null
+++ b/video/out/dither.h
@@ -0,0 +1,2 @@
+void mp_make_fruit_dither_matrix(float *out_matrix, int size);
+void mp_make_ordered_dither_matrix(unsigned char *m, int size);
diff --git a/video/out/dr_helper.c b/video/out/dr_helper.c
new file mode 100644
index 0000000..ac440a7
--- /dev/null
+++ b/video/out/dr_helper.c
@@ -0,0 +1,162 @@
+#include <assert.h>
+#include <stdatomic.h>
+#include <stdlib.h>
+
+#include <libavutil/buffer.h>
+
+#include "misc/dispatch.h"
+#include "mpv_talloc.h"
+#include "osdep/threads.h"
+#include "video/mp_image.h"
+
+#include "dr_helper.h"
+
+struct dr_helper {
+ mp_mutex thread_lock;
+ mp_thread_id thread_id;
+ bool thread_valid; // (POSIX defines no "unset" mp_thread value yet)
+
+ struct mp_dispatch_queue *dispatch;
+ atomic_ullong dr_in_flight;
+
+ struct mp_image *(*get_image)(void *ctx, int imgfmt, int w, int h,
+ int stride_align, int flags);
+ void *get_image_ctx;
+};
+
+static void dr_helper_destroy(void *ptr)
+{
+ struct dr_helper *dr = ptr;
+
+ // All references must have been freed on destruction, or we'll have
+ // dangling pointers.
+ assert(atomic_load(&dr->dr_in_flight) == 0);
+
+ mp_mutex_destroy(&dr->thread_lock);
+}
+
+struct dr_helper *dr_helper_create(struct mp_dispatch_queue *dispatch,
+ struct mp_image *(*get_image)(void *ctx, int imgfmt, int w, int h,
+ int stride_align, int flags),
+ void *get_image_ctx)
+{
+ struct dr_helper *dr = talloc_ptrtype(NULL, dr);
+ talloc_set_destructor(dr, dr_helper_destroy);
+ *dr = (struct dr_helper){
+ .dispatch = dispatch,
+ .dr_in_flight = 0,
+ .get_image = get_image,
+ .get_image_ctx = get_image_ctx,
+ };
+ mp_mutex_init(&dr->thread_lock);
+ return dr;
+}
+
+void dr_helper_acquire_thread(struct dr_helper *dr)
+{
+ mp_mutex_lock(&dr->thread_lock);
+ assert(!dr->thread_valid); // fails on API user errors
+ dr->thread_valid = true;
+ dr->thread_id = mp_thread_current_id();
+ mp_mutex_unlock(&dr->thread_lock);
+}
+
+void dr_helper_release_thread(struct dr_helper *dr)
+{
+ mp_mutex_lock(&dr->thread_lock);
+ // Fails on API user errors.
+ assert(dr->thread_valid);
+ assert(mp_thread_id_equal(dr->thread_id, mp_thread_current_id()));
+ dr->thread_valid = false;
+ mp_mutex_unlock(&dr->thread_lock);
+}
+
+struct free_dr_context {
+ struct dr_helper *dr;
+ AVBufferRef *ref;
+};
+
+static void dr_thread_free(void *ptr)
+{
+ struct free_dr_context *ctx = ptr;
+
+ unsigned long long v = atomic_fetch_add(&ctx->dr->dr_in_flight, -1);
+ assert(v); // value before sub is 0 - unexpected underflow.
+
+ av_buffer_unref(&ctx->ref);
+ talloc_free(ctx);
+}
+
+static void free_dr_buffer_on_dr_thread(void *opaque, uint8_t *data)
+{
+ struct free_dr_context *ctx = opaque;
+ struct dr_helper *dr = ctx->dr;
+
+ mp_mutex_lock(&dr->thread_lock);
+ bool on_this_thread =
+ dr->thread_valid && mp_thread_id_equal(ctx->dr->thread_id, mp_thread_current_id());
+ mp_mutex_unlock(&dr->thread_lock);
+
+ // The image could be unreffed even on the DR thread. In practice, this
+ // matters most on DR destruction.
+ if (on_this_thread) {
+ dr_thread_free(ctx);
+ } else {
+ mp_dispatch_enqueue(dr->dispatch, dr_thread_free, ctx);
+ }
+}
+
+struct get_image_cmd {
+ struct dr_helper *dr;
+ int imgfmt, w, h, stride_align, flags;
+ struct mp_image *res;
+};
+
+static void sync_get_image(void *ptr)
+{
+ struct get_image_cmd *cmd = ptr;
+ struct dr_helper *dr = cmd->dr;
+
+ cmd->res = dr->get_image(dr->get_image_ctx, cmd->imgfmt, cmd->w, cmd->h,
+ cmd->stride_align, cmd->flags);
+ if (!cmd->res)
+ return;
+
+ // We require exactly 1 AVBufferRef.
+ assert(cmd->res->bufs[0]);
+ assert(!cmd->res->bufs[1]);
+
+ // Apply some magic to get it free'd on the DR thread as well. For this to
+ // work, we create a dummy-ref that aliases the original ref, which is why
+ // the original ref must be writable in the first place. (A newly allocated
+ // image should be always writable of course.)
+ assert(mp_image_is_writeable(cmd->res));
+
+ struct free_dr_context *ctx = talloc_zero(NULL, struct free_dr_context);
+ *ctx = (struct free_dr_context){
+ .dr = dr,
+ .ref = cmd->res->bufs[0],
+ };
+
+ AVBufferRef *new_ref = av_buffer_create(ctx->ref->data, ctx->ref->size,
+ free_dr_buffer_on_dr_thread, ctx, 0);
+ MP_HANDLE_OOM(new_ref);
+
+ cmd->res->bufs[0] = new_ref;
+
+ atomic_fetch_add(&dr->dr_in_flight, 1);
+}
+
+struct mp_image *dr_helper_get_image(struct dr_helper *dr, int imgfmt,
+ int w, int h, int stride_align, int flags)
+{
+ struct get_image_cmd cmd = {
+ .dr = dr,
+ .imgfmt = imgfmt,
+ .w = w, .h = h,
+ .stride_align = stride_align,
+ .flags = flags,
+ };
+ mp_dispatch_run(dr->dispatch, sync_get_image, &cmd);
+ return cmd.res;
+}
diff --git a/video/out/dr_helper.h b/video/out/dr_helper.h
new file mode 100644
index 0000000..cf2ed14
--- /dev/null
+++ b/video/out/dr_helper.h
@@ -0,0 +1,37 @@
+#pragma once
+
+// This is a helper for implementing thread-safety for DR callbacks. These need
+// to allocate GPU buffers on the GPU thread (e.g. OpenGL with its forced TLS),
+// and the buffers also need to be freed on the GPU thread.
+// This is not a helpful "Dr.", rather it represents Satan in form of C code.
+struct dr_helper;
+
+struct mp_image;
+struct mp_dispatch_queue;
+
+// dr_helper_get_image() calls will use the dispatch queue to run get_image on
+// a target thread, which processes the dispatch queue.
+// Note: the dispatch queue must process outstanding async. work before the
+// dr_helper instance can be destroyed.
+struct dr_helper *dr_helper_create(struct mp_dispatch_queue *dispatch,
+ struct mp_image *(*get_image)(void *ctx, int imgfmt, int w, int h,
+ int stride_align, int flags),
+ void *get_image_ctx);
+
+// Make DR release calls (freeing images) reentrant if they are called on current
+// thread. That means any free call will directly release the image as allocated
+// with get_image().
+// Only 1 thread can use this at a time. Note that it would make no sense to
+// call this on more than 1 thread, as get_image is assumed not thread-safe.
+void dr_helper_acquire_thread(struct dr_helper *dr);
+
+// This _must_ be called on the same thread as dr_helper_acquire_thread() was
+// called. Every release call must be paired with an acquire call.
+void dr_helper_release_thread(struct dr_helper *dr);
+
+// Allocate an image by running the get_image callback on the target thread.
+// Always blocks on dispatch queue processing. This implies there is no way to
+// allocate a DR'ed image on the render thread (at least not in a way which
+// actually works if you want foreign threads to be able to free them).
+struct mp_image *dr_helper_get_image(struct dr_helper *dr, int imgfmt,
+ int w, int h, int stride_align, int flags);
diff --git a/video/out/drm_atomic.c b/video/out/drm_atomic.c
new file mode 100644
index 0000000..5754504
--- /dev/null
+++ b/video/out/drm_atomic.c
@@ -0,0 +1,458 @@
+/*
+ * 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 <errno.h>
+#include <inttypes.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "drm_atomic.h"
+
+int drm_object_create_properties(struct mp_log *log, int fd,
+ struct drm_object *object)
+{
+ object->props = drmModeObjectGetProperties(fd, object->id, object->type);
+ if (object->props) {
+ object->props_info = talloc_zero_size(NULL, object->props->count_props
+ * sizeof(object->props_info));
+ if (object->props_info) {
+ for (int i = 0; i < object->props->count_props; i++)
+ object->props_info[i] = drmModeGetProperty(fd, object->props->props[i]);
+ } else {
+ mp_err(log, "Out of memory\n");
+ goto fail;
+ }
+ } else {
+ mp_err(log, "Failed to retrieve properties for object id %d\n", object->id);
+ goto fail;
+ }
+
+ return 0;
+
+ fail:
+ drm_object_free_properties(object);
+ return -1;
+}
+
+void drm_object_free_properties(struct drm_object *object)
+{
+ if (object->props) {
+ for (int i = 0; i < object->props->count_props; i++) {
+ if (object->props_info[i]) {
+ drmModeFreeProperty(object->props_info[i]);
+ object->props_info[i] = NULL;
+ }
+ }
+
+ talloc_free(object->props_info);
+ object->props_info = NULL;
+
+ drmModeFreeObjectProperties(object->props);
+ object->props = NULL;
+ }
+}
+
+int drm_object_get_property(struct drm_object *object, char *name, uint64_t *value)
+{
+ for (int i = 0; i < object->props->count_props; i++) {
+ if (strcasecmp(name, object->props_info[i]->name) == 0) {
+ *value = object->props->prop_values[i];
+ return 0;
+ }
+ }
+
+ return -EINVAL;
+}
+
+drmModePropertyBlobPtr drm_object_get_property_blob(struct drm_object *object, char *name)
+{
+ uint64_t blob_id;
+
+ if (!drm_object_get_property(object, name, &blob_id)) {
+ return drmModeGetPropertyBlob(object->fd, blob_id);
+ }
+
+ return NULL;
+}
+
+int drm_object_set_property(drmModeAtomicReq *request, struct drm_object *object,
+ char *name, uint64_t value)
+{
+ for (int i = 0; i < object->props->count_props; i++) {
+ if (strcasecmp(name, object->props_info[i]->name) == 0) {
+ if (object->props_info[i]->flags & DRM_MODE_PROP_IMMUTABLE) {
+ /* Do not try to set immutable values, as this might cause the
+ * atomic commit operation to fail. */
+ return -EINVAL;
+ }
+ return drmModeAtomicAddProperty(request, object->id,
+ object->props_info[i]->prop_id, value);
+ }
+ }
+
+ return -EINVAL;
+}
+
+struct drm_object *drm_object_create(struct mp_log *log, int fd,
+ uint32_t object_id, uint32_t type)
+{
+ struct drm_object *obj = NULL;
+ obj = talloc_zero(NULL, struct drm_object);
+ obj->fd = fd;
+ obj->id = object_id;
+ obj->type = type;
+
+ if (drm_object_create_properties(log, fd, obj)) {
+ talloc_free(obj);
+ return NULL;
+ }
+
+ return obj;
+}
+
+void drm_object_free(struct drm_object *object)
+{
+ if (object) {
+ drm_object_free_properties(object);
+ talloc_free(object);
+ }
+}
+
+void drm_object_print_info(struct mp_log *log, struct drm_object *object)
+{
+ mp_err(log, "Object ID = %d (type = %x) has %d properties\n",
+ object->id, object->type, object->props->count_props);
+
+ for (int i = 0; i < object->props->count_props; i++)
+ mp_err(log, " Property '%s' = %lld\n", object->props_info[i]->name,
+ (long long)object->props->prop_values[i]);
+}
+
+struct drm_atomic_context *drm_atomic_create_context(struct mp_log *log, int fd, int crtc_id,
+ int connector_id,
+ int draw_plane_idx, int drmprime_video_plane_idx)
+{
+ drmModePlaneRes *plane_res = NULL;
+ drmModeRes *res = NULL;
+ struct drm_object *plane = NULL;
+ struct drm_atomic_context *ctx;
+ int crtc_index = -1;
+ int layercount = -1;
+ int primary_id = 0;
+ int overlay_id = 0;
+
+ uint64_t value;
+
+ res = drmModeGetResources(fd);
+ if (!res) {
+ mp_err(log, "Cannot retrieve DRM resources: %s\n", mp_strerror(errno));
+ goto fail;
+ }
+
+ plane_res = drmModeGetPlaneResources(fd);
+ if (!plane_res) {
+ mp_err(log, "Cannot retrieve plane resources: %s\n", mp_strerror(errno));
+ goto fail;
+ }
+
+ ctx = talloc_zero(NULL, struct drm_atomic_context);
+ if (!ctx) {
+ mp_err(log, "Out of memory\n");
+ goto fail;
+ }
+
+ ctx->fd = fd;
+ ctx->crtc = drm_object_create(log, ctx->fd, crtc_id, DRM_MODE_OBJECT_CRTC);
+ if (!ctx->crtc) {
+ mp_err(log, "Failed to create CRTC object\n");
+ goto fail;
+ }
+
+ for (int i = 0; i < res->count_crtcs; i++) {
+ if (res->crtcs[i] == crtc_id) {
+ crtc_index = i;
+ break;
+ }
+ }
+
+ for (int i = 0; i < res->count_connectors; i++) {
+ drmModeConnector *connector = drmModeGetConnector(fd, res->connectors[i]);
+ if (connector) {
+ if (connector->connector_id == connector_id)
+ ctx->connector = drm_object_create(log, ctx->fd, connector->connector_id,
+ DRM_MODE_OBJECT_CONNECTOR);
+ drmModeFreeConnector(connector);
+ if (ctx->connector)
+ break;
+ }
+ }
+
+ for (unsigned int j = 0; j < plane_res->count_planes; j++) {
+
+ drmModePlane *drmplane = drmModeGetPlane(ctx->fd, plane_res->planes[j]);
+ const uint32_t possible_crtcs = drmplane->possible_crtcs;
+ const uint32_t plane_id = drmplane->plane_id;
+ drmModeFreePlane(drmplane);
+ drmplane = NULL;
+
+ if (possible_crtcs & (1 << crtc_index)) {
+ plane = drm_object_create(log, ctx->fd, plane_id, DRM_MODE_OBJECT_PLANE);
+
+ if (!plane) {
+ mp_err(log, "Failed to create Plane object from plane ID %d\n",
+ plane_id);
+ goto fail;
+ }
+
+ if (drm_object_get_property(plane, "TYPE", &value) == -EINVAL) {
+ mp_err(log, "Unable to retrieve type property from plane %d\n", j);
+ goto fail;
+ }
+
+ if (value != DRM_PLANE_TYPE_CURSOR) { // Skip cursor planes
+ layercount++;
+
+ if ((!primary_id) && (value == DRM_PLANE_TYPE_PRIMARY))
+ primary_id = plane_id;
+
+ if ((!overlay_id) && (value == DRM_PLANE_TYPE_OVERLAY))
+ overlay_id = plane_id;
+
+ if (layercount == draw_plane_idx) {
+ ctx->draw_plane = plane;
+ continue;
+ }
+
+ if (layercount == drmprime_video_plane_idx) {
+ ctx->drmprime_video_plane = plane;
+ continue;
+ }
+ }
+
+ drm_object_free(plane);
+ plane = NULL;
+ }
+ }
+
+ // draw plane was specified as either of the special options: any primary plane or any overlay plane
+ if (!ctx->draw_plane) {
+ const int draw_plane_id = (draw_plane_idx == DRM_OPTS_OVERLAY_PLANE) ? overlay_id : primary_id;
+ const char *plane_type = (draw_plane_idx == DRM_OPTS_OVERLAY_PLANE) ? "overlay" : "primary";
+ if (draw_plane_id) {
+ mp_verbose(log, "Using %s plane %d as draw plane\n", plane_type, draw_plane_id);
+ ctx->draw_plane = drm_object_create(log, ctx->fd, draw_plane_id, DRM_MODE_OBJECT_PLANE);
+ } else {
+ mp_err(log, "Failed to find draw plane with idx=%d\n", draw_plane_idx);
+ goto fail;
+ }
+ } else {
+ mp_verbose(log, "Found draw plane with ID %d\n", ctx->draw_plane->id);
+ }
+
+ // drmprime plane was specified as either of the special options: any primary plane or any overlay plane
+ if (!ctx->drmprime_video_plane) {
+ const int drmprime_video_plane_id = (drmprime_video_plane_idx == DRM_OPTS_PRIMARY_PLANE) ? primary_id : overlay_id;
+ const char *plane_type = (drmprime_video_plane_idx == DRM_OPTS_PRIMARY_PLANE) ? "primary" : "overlay";
+
+ if (drmprime_video_plane_id) {
+ mp_verbose(log, "Using %s plane %d as drmprime plane\n", plane_type, drmprime_video_plane_id);
+ ctx->drmprime_video_plane = drm_object_create(log, ctx->fd, drmprime_video_plane_id, DRM_MODE_OBJECT_PLANE);
+ } else {
+ mp_verbose(log, "Failed to find drmprime plane with idx=%d. drmprime-overlay hwdec interop will not work\n", drmprime_video_plane_idx);
+ }
+ } else {
+ mp_verbose(log, "Found drmprime plane with ID %d\n", ctx->drmprime_video_plane->id);
+ }
+
+ drmModeFreePlaneResources(plane_res);
+ drmModeFreeResources(res);
+ return ctx;
+
+fail:
+ if (res)
+ drmModeFreeResources(res);
+ if (plane_res)
+ drmModeFreePlaneResources(plane_res);
+ if (plane)
+ drm_object_free(plane);
+ return NULL;
+}
+
+void drm_atomic_destroy_context(struct drm_atomic_context *ctx)
+{
+ drm_mode_destroy_blob(ctx->fd, &ctx->old_state.crtc.mode);
+ drm_object_free(ctx->crtc);
+ drm_object_free(ctx->connector);
+ drm_object_free(ctx->draw_plane);
+ drm_object_free(ctx->drmprime_video_plane);
+ talloc_free(ctx);
+}
+
+static bool drm_atomic_save_plane_state(struct drm_object *plane,
+ struct drm_atomic_plane_state *plane_state)
+{
+ if (!plane)
+ return true;
+
+ bool ret = true;
+
+ if (0 > drm_object_get_property(plane, "FB_ID", &plane_state->fb_id))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "CRTC_ID", &plane_state->crtc_id))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "SRC_X", &plane_state->src_x))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "SRC_Y", &plane_state->src_y))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "SRC_W", &plane_state->src_w))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "SRC_H", &plane_state->src_h))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "CRTC_X", &plane_state->crtc_x))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "CRTC_Y", &plane_state->crtc_y))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "CRTC_W", &plane_state->crtc_w))
+ ret = false;
+ if (0 > drm_object_get_property(plane, "CRTC_H", &plane_state->crtc_h))
+ ret = false;
+ // ZPOS might not exist, so ignore whether or not this succeeds
+ drm_object_get_property(plane, "ZPOS", &plane_state->zpos);
+
+ return ret;
+}
+
+static bool drm_atomic_restore_plane_state(drmModeAtomicReq *request,
+ struct drm_object *plane,
+ const struct drm_atomic_plane_state *plane_state)
+{
+ if (!plane)
+ return true;
+
+ bool ret = true;
+
+ if (0 > drm_object_set_property(request, plane, "FB_ID", plane_state->fb_id))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "CRTC_ID", plane_state->crtc_id))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "SRC_X", plane_state->src_x))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "SRC_Y", plane_state->src_y))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "SRC_W", plane_state->src_w))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "SRC_H", plane_state->src_h))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "CRTC_X", plane_state->crtc_x))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "CRTC_Y", plane_state->crtc_y))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "CRTC_W", plane_state->crtc_w))
+ ret = false;
+ if (0 > drm_object_set_property(request, plane, "CRTC_H", plane_state->crtc_h))
+ ret = false;
+ // ZPOS might not exist, or be immutable, so ignore whether or not this succeeds
+ drm_object_set_property(request, plane, "ZPOS", plane_state->zpos);
+
+ return ret;
+}
+
+bool drm_atomic_save_old_state(struct drm_atomic_context *ctx)
+{
+ if (ctx->old_state.saved)
+ return false;
+
+ bool ret = true;
+
+ drmModeCrtc *crtc = drmModeGetCrtc(ctx->fd, ctx->crtc->id);
+ if (crtc == NULL)
+ return false;
+ ctx->old_state.crtc.mode.mode = crtc->mode;
+ drmModeFreeCrtc(crtc);
+
+ if (0 > drm_object_get_property(ctx->crtc, "ACTIVE", &ctx->old_state.crtc.active))
+ ret = false;
+
+ // This property was added in kernel 5.0. We will just ignore any errors.
+ drm_object_get_property(ctx->crtc, "VRR_ENABLED", &ctx->old_state.crtc.vrr_enabled);
+
+ if (0 > drm_object_get_property(ctx->connector, "CRTC_ID", &ctx->old_state.connector.crtc_id))
+ ret = false;
+
+ if (!drm_atomic_save_plane_state(ctx->draw_plane, &ctx->old_state.draw_plane))
+ ret = false;
+ if (!drm_atomic_save_plane_state(ctx->drmprime_video_plane, &ctx->old_state.drmprime_video_plane))
+ ret = false;
+
+ ctx->old_state.saved = true;
+
+ return ret;
+}
+
+bool drm_atomic_restore_old_state(drmModeAtomicReqPtr request, struct drm_atomic_context *ctx)
+{
+ if (!ctx->old_state.saved)
+ return false;
+
+ bool ret = true;
+
+ if (0 > drm_object_set_property(request, ctx->connector, "CRTC_ID", ctx->old_state.connector.crtc_id))
+ ret = false;
+
+ // This property was added in kernel 5.0. We will just ignore any errors.
+ drm_object_set_property(request, ctx->crtc, "VRR_ENABLED", ctx->old_state.crtc.vrr_enabled);
+
+ if (!drm_mode_ensure_blob(ctx->fd, &ctx->old_state.crtc.mode))
+ ret = false;
+ if (0 > drm_object_set_property(request, ctx->crtc, "MODE_ID", ctx->old_state.crtc.mode.blob_id))
+ ret = false;
+ if (0 > drm_object_set_property(request, ctx->crtc, "ACTIVE", ctx->old_state.crtc.active))
+ ret = false;
+
+ if (!drm_atomic_restore_plane_state(request, ctx->draw_plane, &ctx->old_state.draw_plane))
+ ret = false;
+ if (!drm_atomic_restore_plane_state(request, ctx->drmprime_video_plane, &ctx->old_state.drmprime_video_plane))
+ ret = false;
+
+ ctx->old_state.saved = false;
+
+ return ret;
+}
+
+bool drm_mode_ensure_blob(int fd, struct drm_mode *mode)
+{
+ int ret = 0;
+
+ if (!mode->blob_id) {
+ ret = drmModeCreatePropertyBlob(fd, &mode->mode, sizeof(drmModeModeInfo),
+ &mode->blob_id);
+ }
+
+ return (ret == 0);
+}
+
+bool drm_mode_destroy_blob(int fd, struct drm_mode *mode)
+{
+ int ret = 0;
+
+ if (mode->blob_id) {
+ ret = drmModeDestroyPropertyBlob(fd, mode->blob_id);
+ mode->blob_id = 0;
+ }
+
+ return (ret == 0);
+}
diff --git a/video/out/drm_atomic.h b/video/out/drm_atomic.h
new file mode 100644
index 0000000..499aa33
--- /dev/null
+++ b/video/out/drm_atomic.h
@@ -0,0 +1,100 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MP_DRMATOMIC_H
+#define MP_DRMATOMIC_H
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+
+#include "common/msg.h"
+#include "drm_common.h"
+
+#define DRM_OPTS_PRIMARY_PLANE -1
+#define DRM_OPTS_OVERLAY_PLANE -2
+
+struct drm_atomic_plane_state {
+ uint64_t fb_id;
+ uint64_t crtc_id;
+ uint64_t src_x;
+ uint64_t src_y;
+ uint64_t src_w;
+ uint64_t src_h;
+ uint64_t crtc_x;
+ uint64_t crtc_y;
+ uint64_t crtc_w;
+ uint64_t crtc_h;
+ uint64_t zpos;
+};
+
+// Used to store the restore state for VT switching and uninit
+struct drm_atomic_state {
+ bool saved;
+ struct {
+ uint64_t crtc_id;
+ } connector;
+ struct {
+ struct drm_mode mode;
+ uint64_t active;
+ uint64_t vrr_enabled;
+ } crtc;
+ struct drm_atomic_plane_state draw_plane;
+ struct drm_atomic_plane_state drmprime_video_plane;
+};
+
+struct drm_object {
+ int fd;
+ uint32_t id;
+ uint32_t type;
+ drmModeObjectProperties *props;
+ drmModePropertyRes **props_info;
+};
+
+struct drm_atomic_context {
+ int fd;
+
+ struct drm_object *crtc;
+ struct drm_object *connector;
+ struct drm_object *draw_plane;
+ struct drm_object *drmprime_video_plane;
+
+ drmModeAtomicReq *request;
+
+ struct drm_atomic_state old_state;
+};
+
+int drm_object_create_properties(struct mp_log *log, int fd, struct drm_object *object);
+void drm_object_free_properties(struct drm_object *object);
+int drm_object_get_property(struct drm_object *object, char *name, uint64_t *value);
+int drm_object_set_property(drmModeAtomicReq *request, struct drm_object *object, char *name, uint64_t value);
+drmModePropertyBlobPtr drm_object_get_property_blob(struct drm_object *object, char *name);
+struct drm_object *drm_object_create(struct mp_log *log, int fd, uint32_t object_id, uint32_t type);
+void drm_object_free(struct drm_object *object);
+void drm_object_print_info(struct mp_log *log, struct drm_object *object);
+struct drm_atomic_context *drm_atomic_create_context(struct mp_log *log, int fd, int crtc_id, int connector_id,
+ int draw_plane_idx, int drmprime_video_plane_idx);
+void drm_atomic_destroy_context(struct drm_atomic_context *ctx);
+
+bool drm_atomic_save_old_state(struct drm_atomic_context *ctx);
+bool drm_atomic_restore_old_state(drmModeAtomicReq *request, struct drm_atomic_context *ctx);
+
+bool drm_mode_ensure_blob(int fd, struct drm_mode *mode);
+bool drm_mode_destroy_blob(int fd, struct drm_mode *mode);
+
+#endif // MP_DRMATOMIC_H
diff --git a/video/out/drm_common.c b/video/out/drm_common.c
new file mode 100644
index 0000000..da45ca2
--- /dev/null
+++ b/video/out/drm_common.c
@@ -0,0 +1,1289 @@
+/*
+ * 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 <errno.h>
+#include <string.h>
+#include <signal.h>
+#include <sys/ioctl.h>
+#include <poll.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <limits.h>
+#include <math.h>
+#include <time.h>
+#include <drm_fourcc.h>
+
+#include "config.h"
+
+#if HAVE_CONSIO_H
+#include <sys/consio.h>
+#else
+#include <sys/vt.h>
+#endif
+
+#include "drm_atomic.h"
+#include "drm_common.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "misc/ctype.h"
+#include "options/m_config.h"
+#include "osdep/io.h"
+#include "osdep/poll_wrapper.h"
+#include "osdep/timer.h"
+#include "present_sync.h"
+#include "video/out/vo.h"
+
+#define EVT_RELEASE 1
+#define EVT_ACQUIRE 2
+#define EVT_INTERRUPT 255
+#define HANDLER_ACQUIRE 0
+#define HANDLER_RELEASE 1
+#define RELEASE_SIGNAL SIGUSR1
+#define ACQUIRE_SIGNAL SIGUSR2
+#define MAX_CONNECTOR_NAME_LEN 20
+
+static int vt_switcher_pipe[2];
+
+static int drm_connector_opt_help(struct mp_log *log, const struct m_option *opt,
+ struct bstr name);
+
+static int drm_mode_opt_help(struct mp_log *log, const struct m_option *opt,
+ struct bstr name);
+
+static int drm_validate_mode_opt(struct mp_log *log, const struct m_option *opt,
+ struct bstr name, const char **value);
+
+static void drm_show_available_modes(struct mp_log *log, const drmModeConnector *connector);
+
+static void drm_show_available_connectors(struct mp_log *log, int card_no,
+ const char *card_path);
+static double mode_get_Hz(const drmModeModeInfo *mode);
+
+#define OPT_BASE_STRUCT struct drm_opts
+const struct m_sub_options drm_conf = {
+ .opts = (const struct m_option[]) {
+ {"drm-device", OPT_STRING(device_path), .flags = M_OPT_FILE},
+ {"drm-connector", OPT_STRING(connector_spec),
+ .help = drm_connector_opt_help},
+ {"drm-mode", OPT_STRING_VALIDATE(mode_spec, drm_validate_mode_opt),
+ .help = drm_mode_opt_help},
+ {"drm-atomic", OPT_CHOICE(drm_atomic, {"no", 0}, {"auto", 1}),
+ .deprecation_message = "this option is deprecated: DRM Atomic is required"},
+ {"drm-draw-plane", OPT_CHOICE(draw_plane,
+ {"primary", DRM_OPTS_PRIMARY_PLANE},
+ {"overlay", DRM_OPTS_OVERLAY_PLANE}),
+ M_RANGE(0, INT_MAX)},
+ {"drm-drmprime-video-plane", OPT_CHOICE(drmprime_video_plane,
+ {"primary", DRM_OPTS_PRIMARY_PLANE},
+ {"overlay", DRM_OPTS_OVERLAY_PLANE}),
+ M_RANGE(0, INT_MAX)},
+ {"drm-format", OPT_CHOICE(drm_format,
+ {"xrgb8888", DRM_OPTS_FORMAT_XRGB8888},
+ {"xrgb2101010", DRM_OPTS_FORMAT_XRGB2101010},
+ {"xbgr8888", DRM_OPTS_FORMAT_XBGR8888},
+ {"xbgr2101010", DRM_OPTS_FORMAT_XBGR2101010})},
+ {"drm-draw-surface-size", OPT_SIZE_BOX(draw_surface_size)},
+ {"drm-vrr-enabled", OPT_CHOICE(vrr_enabled,
+ {"no", 0}, {"yes", 1}, {"auto", -1})},
+ {0},
+ },
+ .defaults = &(const struct drm_opts) {
+ .mode_spec = "preferred",
+ .drm_atomic = 1,
+ .draw_plane = DRM_OPTS_PRIMARY_PLANE,
+ .drmprime_video_plane = DRM_OPTS_OVERLAY_PLANE,
+ },
+ .size = sizeof(struct drm_opts),
+};
+
+static const char *connector_names[] = {
+ "Unknown", // DRM_MODE_CONNECTOR_Unknown
+ "VGA", // DRM_MODE_CONNECTOR_VGA
+ "DVI-I", // DRM_MODE_CONNECTOR_DVII
+ "DVI-D", // DRM_MODE_CONNECTOR_DVID
+ "DVI-A", // DRM_MODE_CONNECTOR_DVIA
+ "Composite", // DRM_MODE_CONNECTOR_Composite
+ "SVIDEO", // DRM_MODE_CONNECTOR_SVIDEO
+ "LVDS", // DRM_MODE_CONNECTOR_LVDS
+ "Component", // DRM_MODE_CONNECTOR_Component
+ "DIN", // DRM_MODE_CONNECTOR_9PinDIN
+ "DP", // DRM_MODE_CONNECTOR_DisplayPort
+ "HDMI-A", // DRM_MODE_CONNECTOR_HDMIA
+ "HDMI-B", // DRM_MODE_CONNECTOR_HDMIB
+ "TV", // DRM_MODE_CONNECTOR_TV
+ "eDP", // DRM_MODE_CONNECTOR_eDP
+ "Virtual", // DRM_MODE_CONNECTOR_VIRTUAL
+ "DSI", // DRM_MODE_CONNECTOR_DSI
+ "DPI", // DRM_MODE_CONNECTOR_DPI
+ "Writeback", // DRM_MODE_CONNECTOR_WRITEBACK
+ "SPI", // DRM_MODE_CONNECTOR_SPI
+ "USB", // DRM_MODE_CONNECTOR_USB
+};
+
+struct drm_mode_spec {
+ enum {
+ DRM_MODE_SPEC_BY_IDX, // Specified by idx
+ DRM_MODE_SPEC_BY_NUMBERS, // Specified by width, height and opt. refresh
+ DRM_MODE_SPEC_PREFERRED, // Select the preferred mode of the display
+ DRM_MODE_SPEC_HIGHEST, // Select the mode with the highest resolution
+ } type;
+ unsigned int idx;
+ unsigned int width;
+ unsigned int height;
+ double refresh;
+};
+
+/* VT Switcher */
+static void vt_switcher_sighandler(int sig)
+{
+ int saved_errno = errno;
+ unsigned char event = sig == RELEASE_SIGNAL ? EVT_RELEASE : EVT_ACQUIRE;
+ (void)write(vt_switcher_pipe[1], &event, sizeof(event));
+ errno = saved_errno;
+}
+
+static bool has_signal_installed(int signo)
+{
+ struct sigaction act = { 0 };
+ sigaction(signo, 0, &act);
+ return act.sa_handler != 0;
+}
+
+static int install_signal(int signo, void (*handler)(int))
+{
+ struct sigaction act = { 0 };
+ act.sa_handler = handler;
+ sigemptyset(&act.sa_mask);
+ act.sa_flags = SA_RESTART;
+ return sigaction(signo, &act, NULL);
+}
+
+static void release_vt(void *data)
+{
+ struct vo_drm_state *drm = data;
+ MP_VERBOSE(drm, "Releasing VT\n");
+ vo_drm_release_crtc(drm);
+}
+
+static void acquire_vt(void *data)
+{
+ struct vo_drm_state *drm = data;
+ MP_VERBOSE(drm, "Acquiring VT\n");
+ vo_drm_acquire_crtc(drm);
+}
+
+static void vt_switcher_acquire(struct vt_switcher *s,
+ void (*handler)(void*), void *user_data)
+{
+ s->handlers[HANDLER_ACQUIRE] = handler;
+ s->handler_data[HANDLER_ACQUIRE] = user_data;
+}
+
+static void vt_switcher_release(struct vt_switcher *s,
+ void (*handler)(void*), void *user_data)
+{
+ s->handlers[HANDLER_RELEASE] = handler;
+ s->handler_data[HANDLER_RELEASE] = user_data;
+}
+
+static bool vt_switcher_init(struct vt_switcher *s, struct mp_log *log)
+{
+ s->tty_fd = -1;
+ s->log = log;
+ vt_switcher_pipe[0] = -1;
+ vt_switcher_pipe[1] = -1;
+
+ if (mp_make_cloexec_pipe(vt_switcher_pipe)) {
+ mp_err(log, "Creating pipe failed: %s\n", mp_strerror(errno));
+ return false;
+ }
+
+ s->tty_fd = open("/dev/tty", O_RDWR | O_CLOEXEC);
+ if (s->tty_fd < 0) {
+ mp_err(log, "Can't open TTY for VT control: %s\n", mp_strerror(errno));
+ return false;
+ }
+
+ if (has_signal_installed(RELEASE_SIGNAL)) {
+ mp_err(log, "Can't handle VT release - signal already used\n");
+ return false;
+ }
+ if (has_signal_installed(ACQUIRE_SIGNAL)) {
+ mp_err(log, "Can't handle VT acquire - signal already used\n");
+ return false;
+ }
+
+ if (install_signal(RELEASE_SIGNAL, vt_switcher_sighandler)) {
+ mp_err(log, "Failed to install release signal: %s\n", mp_strerror(errno));
+ return false;
+ }
+ if (install_signal(ACQUIRE_SIGNAL, vt_switcher_sighandler)) {
+ mp_err(log, "Failed to install acquire signal: %s\n", mp_strerror(errno));
+ return false;
+ }
+
+ struct vt_mode vt_mode = { 0 };
+ if (ioctl(s->tty_fd, VT_GETMODE, &vt_mode) < 0) {
+ mp_err(log, "VT_GETMODE failed: %s\n", mp_strerror(errno));
+ return false;
+ }
+
+ vt_mode.mode = VT_PROCESS;
+ vt_mode.relsig = RELEASE_SIGNAL;
+ vt_mode.acqsig = ACQUIRE_SIGNAL;
+ // frsig is a signal for forced release. Not implemented on Linux,
+ // Solaris, BSDs but must be set to a valid signal on some of those.
+ vt_mode.frsig = SIGIO; // unused
+ if (ioctl(s->tty_fd, VT_SETMODE, &vt_mode) < 0) {
+ mp_err(log, "VT_SETMODE failed: %s\n", mp_strerror(errno));
+ return false;
+ }
+
+ // Block the VT switching signals from interrupting the VO thread (they will
+ // still be picked up by other threads, which will fill vt_switcher_pipe for us)
+ sigset_t set;
+ sigemptyset(&set);
+ sigaddset(&set, RELEASE_SIGNAL);
+ sigaddset(&set, ACQUIRE_SIGNAL);
+ pthread_sigmask(SIG_BLOCK, &set, NULL);
+
+ return true;
+}
+
+static void vt_switcher_interrupt_poll(struct vt_switcher *s)
+{
+ unsigned char event = EVT_INTERRUPT;
+ (void)write(vt_switcher_pipe[1], &event, sizeof(event));
+}
+
+static void vt_switcher_destroy(struct vt_switcher *s)
+{
+ struct vt_mode vt_mode = {0};
+ vt_mode.mode = VT_AUTO;
+ if (ioctl(s->tty_fd, VT_SETMODE, &vt_mode) < 0) {
+ MP_ERR(s, "VT_SETMODE failed: %s\n", mp_strerror(errno));
+ return;
+ }
+
+ install_signal(RELEASE_SIGNAL, SIG_DFL);
+ install_signal(ACQUIRE_SIGNAL, SIG_DFL);
+ close(s->tty_fd);
+ close(vt_switcher_pipe[0]);
+ close(vt_switcher_pipe[1]);
+}
+
+static void vt_switcher_poll(struct vt_switcher *s, int timeout_ns)
+{
+ struct pollfd fds[1] = {
+ { .events = POLLIN, .fd = vt_switcher_pipe[0] },
+ };
+ mp_poll(fds, 1, timeout_ns);
+ if (!fds[0].revents)
+ return;
+
+ unsigned char event;
+ if (read(fds[0].fd, &event, sizeof(event)) != sizeof(event))
+ return;
+
+ switch (event) {
+ case EVT_RELEASE:
+ s->handlers[HANDLER_RELEASE](s->handler_data[HANDLER_RELEASE]);
+ if (ioctl(s->tty_fd, VT_RELDISP, 1) < 0) {
+ MP_ERR(s, "Failed to release virtual terminal\n");
+ }
+ break;
+ case EVT_ACQUIRE:
+ s->handlers[HANDLER_ACQUIRE](s->handler_data[HANDLER_ACQUIRE]);
+ if (ioctl(s->tty_fd, VT_RELDISP, VT_ACKACQ) < 0) {
+ MP_ERR(s, "Failed to acquire virtual terminal\n");
+ }
+ break;
+ case EVT_INTERRUPT:
+ break;
+ }
+}
+
+bool vo_drm_acquire_crtc(struct vo_drm_state *drm)
+{
+ if (drm->active)
+ return true;
+ drm->active = true;
+
+ if (drmSetMaster(drm->fd)) {
+ MP_WARN(drm, "Failed to acquire DRM master: %s\n",
+ mp_strerror(errno));
+ }
+
+ struct drm_atomic_context *atomic_ctx = drm->atomic_context;
+
+ if (!drm_atomic_save_old_state(atomic_ctx))
+ MP_WARN(drm, "Failed to save old DRM atomic state\n");
+
+ drmModeAtomicReqPtr request = drmModeAtomicAlloc();
+ if (!request) {
+ MP_ERR(drm, "Failed to allocate drm atomic request\n");
+ goto err;
+ }
+
+ if (drm_object_set_property(request, atomic_ctx->connector, "CRTC_ID", drm->crtc_id) < 0) {
+ MP_ERR(drm, "Could not set CRTC_ID on connector\n");
+ goto err;
+ }
+
+ if (!drm_mode_ensure_blob(drm->fd, &drm->mode)) {
+ MP_ERR(drm, "Failed to create DRM mode blob\n");
+ goto err;
+ }
+ if (drm_object_set_property(request, atomic_ctx->crtc, "MODE_ID", drm->mode.blob_id) < 0) {
+ MP_ERR(drm, "Could not set MODE_ID on crtc\n");
+ goto err;
+ }
+ if (drm_object_set_property(request, atomic_ctx->crtc, "ACTIVE", 1) < 0) {
+ MP_ERR(drm, "Could not set ACTIVE on crtc\n");
+ goto err;
+ }
+
+ /*
+ * VRR related properties were added in kernel 5.0. We will not fail if we
+ * cannot query or set the value, but we will log as appropriate.
+ */
+ uint64_t vrr_capable = 0;
+ drm_object_get_property(atomic_ctx->connector, "VRR_CAPABLE", &vrr_capable);
+ MP_VERBOSE(drm, "crtc is%s VRR capable\n", vrr_capable ? "" : " not");
+
+ uint64_t vrr_requested = drm->opts->vrr_enabled;
+ if (vrr_requested == 1 || (vrr_capable && vrr_requested == -1)) {
+ if (drm_object_set_property(request, atomic_ctx->crtc, "VRR_ENABLED", 1) < 0) {
+ MP_WARN(drm, "Could not enable VRR on crtc\n");
+ } else {
+ MP_VERBOSE(drm, "Enabled VRR on crtc\n");
+ }
+ }
+
+ drm_object_set_property(request, atomic_ctx->draw_plane, "FB_ID", drm->fb->id);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "CRTC_ID", drm->crtc_id);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "SRC_X", 0);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "SRC_Y", 0);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "SRC_W", drm->width << 16);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "SRC_H", drm->height << 16);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "CRTC_X", 0);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "CRTC_Y", 0);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "CRTC_W", drm->mode.mode.hdisplay);
+ drm_object_set_property(request, atomic_ctx->draw_plane, "CRTC_H", drm->mode.mode.vdisplay);
+
+ if (drmModeAtomicCommit(drm->fd, request, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL)) {
+ MP_ERR(drm, "Failed to commit ModeSetting atomic request: %s\n", strerror(errno));
+ goto err;
+ }
+
+ drmModeAtomicFree(request);
+ return true;
+
+err:
+ drmModeAtomicFree(request);
+ return false;
+}
+
+
+void vo_drm_release_crtc(struct vo_drm_state *drm)
+{
+ if (!drm->active)
+ return;
+ drm->active = false;
+
+ if (!drm->atomic_context->old_state.saved)
+ return;
+
+ bool success = true;
+ struct drm_atomic_context *atomic_ctx = drm->atomic_context;
+ drmModeAtomicReqPtr request = drmModeAtomicAlloc();
+ if (!request) {
+ MP_ERR(drm, "Failed to allocate drm atomic request\n");
+ success = false;
+ }
+
+ if (request && !drm_atomic_restore_old_state(request, atomic_ctx)) {
+ MP_WARN(drm, "Got error while restoring old state\n");
+ success = false;
+ }
+
+ if (request) {
+ if (drmModeAtomicCommit(drm->fd, request, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL)) {
+ MP_WARN(drm, "Failed to commit ModeSetting atomic request: %s\n",
+ mp_strerror(errno));
+ success = false;
+ }
+ }
+
+ if (request)
+ drmModeAtomicFree(request);
+
+ if (!success)
+ MP_ERR(drm, "Failed to restore previous mode\n");
+
+ if (drmDropMaster(drm->fd)) {
+ MP_WARN(drm, "Failed to drop DRM master: %s\n",
+ mp_strerror(errno));
+ }
+}
+
+/* libdrm */
+static void get_connector_name(const drmModeConnector *connector,
+ char ret[MAX_CONNECTOR_NAME_LEN])
+{
+ const char *type_name;
+
+ if (connector->connector_type < MP_ARRAY_SIZE(connector_names)) {
+ type_name = connector_names[connector->connector_type];
+ } else {
+ type_name = "UNKNOWN";
+ }
+
+ snprintf(ret, MAX_CONNECTOR_NAME_LEN, "%s-%d", type_name,
+ connector->connector_type_id);
+}
+
+// Gets the first connector whose name matches the input parameter.
+// The returned connector may be disconnected.
+// Result must be freed with drmModeFreeConnector.
+static drmModeConnector *get_connector_by_name(const drmModeRes *res,
+ const char *connector_name,
+ int fd)
+{
+ for (int i = 0; i < res->count_connectors; i++) {
+ drmModeConnector *connector
+ = drmModeGetConnector(fd, res->connectors[i]);
+ if (!connector)
+ continue;
+ char other_connector_name[MAX_CONNECTOR_NAME_LEN];
+ get_connector_name(connector, other_connector_name);
+ if (!strcmp(connector_name, other_connector_name))
+ return connector;
+ drmModeFreeConnector(connector);
+ }
+ return NULL;
+}
+
+// Gets the first connected connector.
+// Result must be freed with drmModeFreeConnector.
+static drmModeConnector *get_first_connected_connector(const drmModeRes *res,
+ int fd)
+{
+ for (int i = 0; i < res->count_connectors; i++) {
+ drmModeConnector *connector = drmModeGetConnector(fd, res->connectors[i]);
+ if (!connector)
+ continue;
+ if (connector->connection == DRM_MODE_CONNECTED && connector->count_modes > 0) {
+ return connector;
+ }
+ drmModeFreeConnector(connector);
+ }
+ return NULL;
+}
+
+static bool setup_connector(struct vo_drm_state *drm, const drmModeRes *res,
+ const char *connector_name)
+{
+ drmModeConnector *connector;
+
+ if (connector_name && strcmp(connector_name, "") && strcmp(connector_name, "auto")) {
+ connector = get_connector_by_name(res, connector_name, drm->fd);
+ if (!connector) {
+ MP_ERR(drm, "No connector with name %s found\n", connector_name);
+ drm_show_available_connectors(drm->log, drm->card_no, drm->card_path);
+ return false;
+ }
+ } else {
+ connector = get_first_connected_connector(res, drm->fd);
+ if (!connector) {
+ MP_ERR(drm, "No connected connectors found\n");
+ return false;
+ }
+ }
+
+ if (connector->connection != DRM_MODE_CONNECTED) {
+ drmModeFreeConnector(connector);
+ MP_ERR(drm, "Chosen connector is disconnected\n");
+ return false;
+ }
+
+ if (connector->count_modes == 0) {
+ drmModeFreeConnector(connector);
+ MP_ERR(drm, "Chosen connector has no valid modes\n");
+ return false;
+ }
+
+ drm->connector = connector;
+ return true;
+}
+
+static bool setup_crtc(struct vo_drm_state *drm, const drmModeRes *res)
+{
+ // First try to find currently connected encoder and its current CRTC
+ for (unsigned int i = 0; i < res->count_encoders; i++) {
+ drmModeEncoder *encoder = drmModeGetEncoder(drm->fd, res->encoders[i]);
+ if (!encoder) {
+ MP_WARN(drm, "Cannot retrieve encoder %u:%u: %s\n",
+ i, res->encoders[i], mp_strerror(errno));
+ continue;
+ }
+
+ if (encoder->encoder_id == drm->connector->encoder_id && encoder->crtc_id != 0) {
+ MP_VERBOSE(drm, "Connector %u currently connected to encoder %u\n",
+ drm->connector->connector_id, drm->connector->encoder_id);
+ drm->encoder = encoder;
+ drm->crtc_id = encoder->crtc_id;
+ goto success;
+ }
+
+ drmModeFreeEncoder(encoder);
+ }
+
+ // Otherwise pick first legal encoder and CRTC combo for the connector
+ for (unsigned int i = 0; i < drm->connector->count_encoders; ++i) {
+ drmModeEncoder *encoder
+ = drmModeGetEncoder(drm->fd, drm->connector->encoders[i]);
+ if (!encoder) {
+ MP_WARN(drm, "Cannot retrieve encoder %u:%u: %s\n",
+ i, drm->connector->encoders[i], mp_strerror(errno));
+ continue;
+ }
+
+ // iterate all global CRTCs
+ for (unsigned int j = 0; j < res->count_crtcs; ++j) {
+ // check whether this CRTC works with the encoder
+ if (!(encoder->possible_crtcs & (1 << j)))
+ continue;
+
+ drm->encoder = encoder;
+ drm->crtc_id = res->crtcs[j];
+ goto success;
+ }
+
+ drmModeFreeEncoder(encoder);
+ }
+
+ MP_ERR(drm, "Connector %u has no suitable CRTC\n",
+ drm->connector->connector_id);
+ return false;
+
+ success:
+ MP_VERBOSE(drm, "Selected Encoder %u with CRTC %u\n",
+ drm->encoder->encoder_id, drm->crtc_id);
+ return true;
+}
+
+static bool all_digits(const char *str)
+{
+ if (str == NULL || str[0] == '\0') {
+ return false;
+ }
+
+ for (const char *c = str; *c != '\0'; ++c) {
+ if (!mp_isdigit(*c))
+ return false;
+ }
+ return true;
+}
+
+static bool parse_mode_spec(const char *spec, struct drm_mode_spec *parse_result)
+{
+ if (spec == NULL || spec[0] == '\0' || strcmp(spec, "preferred") == 0) {
+ if (parse_result) {
+ *parse_result =
+ (struct drm_mode_spec) { .type = DRM_MODE_SPEC_PREFERRED };
+ }
+ return true;
+ }
+
+ if (strcmp(spec, "highest") == 0) {
+ if (parse_result) {
+ *parse_result =
+ (struct drm_mode_spec) { .type = DRM_MODE_SPEC_HIGHEST };
+ }
+ return true;
+ }
+
+ // If the string is made up of only digits, it means that it is an index number
+ if (all_digits(spec)) {
+ if (parse_result) {
+ *parse_result = (struct drm_mode_spec) {
+ .type = DRM_MODE_SPEC_BY_IDX,
+ .idx = strtoul(spec, NULL, 10),
+ };
+ }
+ return true;
+ }
+
+ if (!mp_isdigit(spec[0]))
+ return false;
+ char *height_part, *refresh_part;
+ const unsigned int width = strtoul(spec, &height_part, 10);
+ if (spec == height_part || height_part[0] == '\0' || height_part[0] != 'x')
+ return false;
+
+ height_part += 1;
+ if (!mp_isdigit(height_part[0]))
+ return false;
+ const unsigned int height = strtoul(height_part, &refresh_part, 10);
+ if (height_part == refresh_part)
+ return false;
+
+ char *rest = NULL;
+ double refresh;
+ switch (refresh_part[0]) {
+ case '\0':
+ refresh = nan("");
+ break;
+ case '@':
+ refresh_part += 1;
+ if (!(mp_isdigit(refresh_part[0]) || refresh_part[0] == '.'))
+ return false;
+ refresh = strtod(refresh_part, &rest);
+ if (refresh_part == rest || rest[0] != '\0' || refresh < 0.0)
+ return false;
+ break;
+ default:
+ return false;
+ }
+
+ if (parse_result) {
+ *parse_result = (struct drm_mode_spec) {
+ .type = DRM_MODE_SPEC_BY_NUMBERS,
+ .width = width,
+ .height = height,
+ .refresh = refresh,
+ };
+ }
+ return true;
+}
+
+static bool setup_mode_by_idx(struct vo_drm_state *drm, unsigned int mode_idx)
+{
+ if (mode_idx >= drm->connector->count_modes) {
+ MP_ERR(drm, "Bad mode index (max = %d).\n",
+ drm->connector->count_modes - 1);
+ return false;
+ }
+
+ drm->mode.mode = drm->connector->modes[mode_idx];
+ return true;
+}
+
+static bool mode_match(const drmModeModeInfo *mode,
+ unsigned int width,
+ unsigned int height,
+ double refresh)
+{
+ if (isnan(refresh)) {
+ return
+ (mode->hdisplay == width) &&
+ (mode->vdisplay == height);
+ } else {
+ const double mode_refresh = mode_get_Hz(mode);
+ return
+ (mode->hdisplay == width) &&
+ (mode->vdisplay == height) &&
+ ((int)round(refresh*100) == (int)round(mode_refresh*100));
+ }
+}
+
+static bool setup_mode_by_numbers(struct vo_drm_state *drm,
+ unsigned int width,
+ unsigned int height,
+ double refresh)
+{
+ for (unsigned int i = 0; i < drm->connector->count_modes; ++i) {
+ drmModeModeInfo *current_mode = &drm->connector->modes[i];
+ if (mode_match(current_mode, width, height, refresh)) {
+ drm->mode.mode = *current_mode;
+ return true;
+ }
+ }
+
+ MP_ERR(drm, "Could not find mode matching %s\n", drm->opts->mode_spec);
+ return false;
+}
+
+static bool setup_mode_preferred(struct vo_drm_state *drm)
+{
+ for (unsigned int i = 0; i < drm->connector->count_modes; ++i) {
+ drmModeModeInfo *current_mode = &drm->connector->modes[i];
+ if (current_mode->type & DRM_MODE_TYPE_PREFERRED) {
+ drm->mode.mode = *current_mode;
+ return true;
+ }
+ }
+
+ // Fall back to first mode
+ MP_WARN(drm, "Could not find any preferred mode. Picking the first mode.\n");
+ drm->mode.mode = drm->connector->modes[0];
+ return true;
+}
+
+static bool setup_mode_highest(struct vo_drm_state *drm)
+{
+ unsigned int area = 0;
+ drmModeModeInfo *highest_resolution_mode = &drm->connector->modes[0];
+ for (unsigned int i = 0; i < drm->connector->count_modes; ++i) {
+ drmModeModeInfo *current_mode = &drm->connector->modes[i];
+
+ const unsigned int current_area =
+ current_mode->hdisplay * current_mode->vdisplay;
+ if (current_area > area) {
+ highest_resolution_mode = current_mode;
+ area = current_area;
+ }
+ }
+
+ drm->mode.mode = *highest_resolution_mode;
+ return true;
+}
+
+static bool setup_mode(struct vo_drm_state *drm)
+{
+ if (drm->connector->count_modes <= 0) {
+ MP_ERR(drm, "No available modes\n");
+ return false;
+ }
+
+ struct drm_mode_spec parsed;
+ if (!parse_mode_spec(drm->opts->mode_spec, &parsed)) {
+ MP_ERR(drm, "Parse error\n");
+ goto err;
+ }
+
+ switch (parsed.type) {
+ case DRM_MODE_SPEC_BY_IDX:
+ if (!setup_mode_by_idx(drm, parsed.idx))
+ goto err;
+ break;
+ case DRM_MODE_SPEC_BY_NUMBERS:
+ if (!setup_mode_by_numbers(drm, parsed.width, parsed.height, parsed.refresh))
+ goto err;
+ break;
+ case DRM_MODE_SPEC_PREFERRED:
+ if (!setup_mode_preferred(drm))
+ goto err;
+ break;
+ case DRM_MODE_SPEC_HIGHEST:
+ if (!setup_mode_highest(drm))
+ goto err;
+ break;
+ default:
+ MP_ERR(drm, "setup_mode: Internal error\n");
+ goto err;
+ }
+
+ drmModeModeInfo *mode = &drm->mode.mode;
+ MP_VERBOSE(drm, "Selected mode: %s (%dx%d@%.2fHz)\n",
+ mode->name, mode->hdisplay, mode->vdisplay, mode_get_Hz(mode));
+
+ return true;
+
+err:
+ MP_INFO(drm, "Available modes:\n");
+ drm_show_available_modes(drm->log, drm->connector);
+ return false;
+}
+
+static int open_card_path(const char *path)
+{
+ return open(path, O_RDWR | O_CLOEXEC);
+}
+
+static bool card_supports_kms(const char *path)
+{
+ int fd = open_card_path(path);
+ bool ret = fd != -1 && drmIsKMS(fd);
+ if (fd != -1)
+ close(fd);
+ return ret;
+}
+
+static bool card_has_connection(const char *path)
+{
+ int fd = open_card_path(path);
+ bool ret = false;
+ if (fd != -1) {
+ drmModeRes *res = drmModeGetResources(fd);
+ if (res) {
+ drmModeConnector *connector = get_first_connected_connector(res, fd);
+ if (connector)
+ ret = true;
+ drmModeFreeConnector(connector);
+ drmModeFreeResources(res);
+ }
+ close(fd);
+ }
+ return ret;
+}
+
+static void get_primary_device_path(struct vo_drm_state *drm)
+{
+ if (drm->opts->device_path) {
+ drm->card_path = talloc_strdup(drm, drm->opts->device_path);
+ return;
+ }
+
+ drmDevice *devices[DRM_MAX_MINOR] = { 0 };
+ int card_count = drmGetDevices2(0, devices, MP_ARRAY_SIZE(devices));
+ bool card_no_given = drm->card_no >= 0;
+
+ if (card_count < 0) {
+ MP_ERR(drm, "Listing DRM devices with drmGetDevices failed! (%s)\n",
+ mp_strerror(errno));
+ goto err;
+ }
+
+ if (card_no_given && drm->card_no > (card_count - 1)) {
+ MP_ERR(drm, "Card number %d given too high! %d devices located.\n",
+ drm->card_no, card_count);
+ goto err;
+ }
+
+ for (int i = card_no_given ? drm->card_no : 0; i < card_count; i++) {
+ drmDevice *dev = devices[i];
+
+ if (!(dev->available_nodes & (1 << DRM_NODE_PRIMARY))) {
+ if (card_no_given) {
+ MP_ERR(drm, "DRM card number %d given, but it does not have "
+ "a primary node!\n", i);
+ break;
+ }
+
+ continue;
+ }
+
+ const char *card_path = dev->nodes[DRM_NODE_PRIMARY];
+
+ if (!card_supports_kms(card_path)) {
+ if (card_no_given) {
+ MP_ERR(drm,
+ "DRM card number %d given, but it does not support "
+ "KMS!\n", i);
+ break;
+ }
+
+ continue;
+ }
+
+ if (!card_has_connection(card_path)) {
+ if (card_no_given) {
+ MP_ERR(drm,
+ "DRM card number %d given, but it does not have any "
+ "connected outputs.\n", i);
+ break;
+ }
+
+ continue;
+ }
+
+ MP_VERBOSE(drm, "Picked DRM card %d, primary node %s%s.\n",
+ i, card_path,
+ card_no_given ? "" : " as the default");
+
+ drm->card_path = talloc_strdup(drm, card_path);
+ drm->card_no = i;
+ break;
+ }
+
+ if (!drm->card_path)
+ MP_ERR(drm, "No primary DRM device could be picked!\n");
+
+err:
+ drmFreeDevices(devices, card_count);
+}
+
+static void drm_pflip_cb(int fd, unsigned int msc, unsigned int sec,
+ unsigned int usec, void *data)
+{
+ struct vo_drm_state *drm = data;
+
+ int64_t ust = MP_TIME_S_TO_NS(sec) + MP_TIME_US_TO_NS(usec);
+ present_sync_update_values(drm->present, ust, msc);
+ present_sync_swap(drm->present);
+ drm->waiting_for_flip = false;
+}
+
+int vo_drm_control(struct vo *vo, int *events, int request, void *arg)
+{
+ struct vo_drm_state *drm = vo->drm;
+ switch (request) {
+ case VOCTRL_GET_DISPLAY_FPS: {
+ double fps = vo_drm_get_display_fps(drm);
+ if (fps <= 0)
+ break;
+ *(double*)arg = fps;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_RES: {
+ ((int *)arg)[0] = drm->mode.mode.hdisplay;
+ ((int *)arg)[1] = drm->mode.mode.vdisplay;
+ return VO_TRUE;
+ }
+ case VOCTRL_PAUSE:
+ vo->want_redraw = true;
+ drm->paused = true;
+ return VO_TRUE;
+ case VOCTRL_RESUME:
+ drm->paused = false;
+ return VO_TRUE;
+ }
+ return VO_NOTIMPL;
+}
+
+bool vo_drm_init(struct vo *vo)
+{
+ vo->drm = talloc_zero(NULL, struct vo_drm_state);
+ struct vo_drm_state *drm = vo->drm;
+
+ *drm = (struct vo_drm_state) {
+ .vo = vo,
+ .log = mp_log_new(drm, vo->log, "drm"),
+ .mode = {{0}},
+ .crtc_id = -1,
+ .card_no = -1,
+ };
+
+ drm->vt_switcher_active = vt_switcher_init(&drm->vt_switcher, drm->log);
+ if (drm->vt_switcher_active) {
+ vt_switcher_acquire(&drm->vt_switcher, acquire_vt, drm);
+ vt_switcher_release(&drm->vt_switcher, release_vt, drm);
+ } else {
+ MP_WARN(drm, "Failed to set up VT switcher. Terminal switching will be unavailable.\n");
+ }
+
+ drm->opts = mp_get_config_group(drm, drm->vo->global, &drm_conf);
+
+ drmModeRes *res = NULL;
+ get_primary_device_path(drm);
+
+ if (!drm->card_path) {
+ MP_ERR(drm, "Failed to find a usable DRM primary node!\n");
+ goto err;
+ }
+
+ drm->fd = open_card_path(drm->card_path);
+ if (drm->fd < 0) {
+ MP_ERR(drm, "Cannot open card \"%d\": %s.\n", drm->card_no, mp_strerror(errno));
+ goto err;
+ }
+
+ drmVersionPtr ver = drmGetVersion(drm->fd);
+ if (ver) {
+ MP_VERBOSE(drm, "Driver: %s %d.%d.%d (%s)\n", ver->name, ver->version_major,
+ ver->version_minor, ver->version_patchlevel, ver->date);
+ drmFreeVersion(ver);
+ }
+
+ res = drmModeGetResources(drm->fd);
+ if (!res) {
+ MP_ERR(drm, "Cannot retrieve DRM resources: %s\n", mp_strerror(errno));
+ goto err;
+ }
+
+ if (!setup_connector(drm, res, drm->opts->connector_spec))
+ goto err;
+ if (!setup_crtc(drm, res))
+ goto err;
+ if (!setup_mode(drm))
+ goto err;
+
+ // Universal planes allows accessing all the planes (including primary)
+ if (drmSetClientCap(drm->fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)) {
+ MP_ERR(drm, "Failed to set Universal planes capability\n");
+ }
+
+ if (drmSetClientCap(drm->fd, DRM_CLIENT_CAP_ATOMIC, 1)) {
+ MP_ERR(drm, "Failed to create DRM atomic context, no DRM Atomic support\n");
+ goto err;
+ } else {
+ MP_VERBOSE(drm, "DRM Atomic support found\n");
+ drm->atomic_context = drm_atomic_create_context(drm->log, drm->fd, drm->crtc_id,
+ drm->connector->connector_id,
+ drm->opts->draw_plane,
+ drm->opts->drmprime_video_plane);
+ if (!drm->atomic_context) {
+ MP_ERR(drm, "Failed to create DRM atomic context\n");
+ goto err;
+ }
+ }
+
+ drmModeFreeResources(res);
+
+ drm->ev.version = DRM_EVENT_CONTEXT_VERSION;
+ drm->ev.page_flip_handler = &drm_pflip_cb;
+ drm->present = mp_present_initialize(drm, drm->vo->opts, VO_MAX_SWAPCHAIN_DEPTH);
+
+ return true;
+
+err:
+ if (res)
+ drmModeFreeResources(res);
+
+ vo_drm_uninit(vo);
+ return false;
+}
+
+void vo_drm_uninit(struct vo *vo)
+{
+ struct vo_drm_state *drm = vo->drm;
+ if (!drm)
+ return;
+
+ vo_drm_release_crtc(drm);
+ if (drm->vt_switcher_active)
+ vt_switcher_destroy(&drm->vt_switcher);
+
+ drm_mode_destroy_blob(drm->fd, &drm->mode);
+
+ if (drm->connector) {
+ drmModeFreeConnector(drm->connector);
+ drm->connector = NULL;
+ }
+ if (drm->encoder) {
+ drmModeFreeEncoder(drm->encoder);
+ drm->encoder = NULL;
+ }
+ if (drm->atomic_context) {
+ drm_atomic_destroy_context(drm->atomic_context);
+ }
+
+ close(drm->fd);
+ talloc_free(drm);
+ vo->drm = NULL;
+}
+
+static double mode_get_Hz(const drmModeModeInfo *mode)
+{
+ double rate = mode->clock * 1000.0 / mode->htotal / mode->vtotal;
+ if (mode->flags & DRM_MODE_FLAG_INTERLACE)
+ rate *= 2.0;
+ return rate;
+}
+
+static void drm_show_available_modes(struct mp_log *log,
+ const drmModeConnector *connector)
+{
+ for (unsigned int i = 0; i < connector->count_modes; i++) {
+ mp_info(log, " Mode %d: %s (%dx%d@%.2fHz)\n", i,
+ connector->modes[i].name,
+ connector->modes[i].hdisplay,
+ connector->modes[i].vdisplay,
+ mode_get_Hz(&connector->modes[i]));
+ }
+}
+
+static void drm_show_foreach_connector(struct mp_log *log, int card_no,
+ const char *card_path,
+ void (*show_fn)(struct mp_log*, int,
+ const drmModeConnector*))
+{
+ int fd = open_card_path(card_path);
+ if (fd < 0) {
+ mp_err(log, "Failed to open card %d (%s)\n", card_no, card_path);
+ return;
+ }
+
+ drmModeRes *res = drmModeGetResources(fd);
+ if (!res) {
+ mp_err(log, "Cannot retrieve DRM resources: %s\n", mp_strerror(errno));
+ goto err;
+ }
+
+ for (int i = 0; i < res->count_connectors; i++) {
+ drmModeConnector *connector = drmModeGetConnector(fd, res->connectors[i]);
+ if (!connector)
+ continue;
+ show_fn(log, card_no, connector);
+ drmModeFreeConnector(connector);
+ }
+
+err:
+ if (fd >= 0)
+ close(fd);
+ if (res)
+ drmModeFreeResources(res);
+}
+
+static void drm_show_connector_name_and_state_callback(struct mp_log *log, int card_no,
+ const drmModeConnector *connector)
+{
+ char other_connector_name[MAX_CONNECTOR_NAME_LEN];
+ get_connector_name(connector, other_connector_name);
+ const char *connection_str = (connector->connection == DRM_MODE_CONNECTED) ?
+ "connected" : "disconnected";
+ mp_info(log, " %s (%s)\n", other_connector_name, connection_str);
+}
+
+static void drm_show_available_connectors(struct mp_log *log, int card_no,
+ const char *card_path)
+{
+ mp_info(log, "Available connectors for card %d (%s):\n", card_no,
+ card_path);
+ drm_show_foreach_connector(log, card_no, card_path,
+ drm_show_connector_name_and_state_callback);
+ mp_info(log, "\n");
+}
+
+static void drm_show_connector_modes_callback(struct mp_log *log, int card_no,
+ const drmModeConnector *connector)
+{
+ if (connector->connection != DRM_MODE_CONNECTED)
+ return;
+
+ char other_connector_name[MAX_CONNECTOR_NAME_LEN];
+ get_connector_name(connector, other_connector_name);
+ mp_info(log, "Available modes for drm-connector=%d.%s\n",
+ card_no, other_connector_name);
+ drm_show_available_modes(log, connector);
+ mp_info(log, "\n");
+}
+
+static void drm_show_available_connectors_and_modes(struct mp_log *log,
+ int card_no,
+ const char *card_path)
+{
+ drm_show_foreach_connector(log, card_no, card_path,
+ drm_show_connector_modes_callback);
+}
+
+static void drm_show_foreach_card(struct mp_log *log,
+ void (*show_fn)(struct mp_log *, int,
+ const char *))
+{
+ drmDevice *devices[DRM_MAX_MINOR] = { 0 };
+ int card_count = drmGetDevices2(0, devices, MP_ARRAY_SIZE(devices));
+ if (card_count < 0) {
+ mp_err(log, "Listing DRM devices with drmGetDevices failed! (%s)\n",
+ mp_strerror(errno));
+ return;
+ }
+
+ for (int i = 0; i < card_count; i++) {
+ drmDevice *dev = devices[i];
+
+ if (!(dev->available_nodes & (1 << DRM_NODE_PRIMARY)))
+ continue;
+
+ const char *card_path = dev->nodes[DRM_NODE_PRIMARY];
+
+ int fd = open_card_path(card_path);
+ if (fd < 0) {
+ mp_err(log, "Failed to open primary DRM node path %s!\n",
+ card_path);
+ continue;
+ }
+
+ close(fd);
+ show_fn(log, i, card_path);
+ }
+
+ drmFreeDevices(devices, card_count);
+}
+
+static void drm_show_available_cards_and_connectors(struct mp_log *log)
+{
+ drm_show_foreach_card(log, drm_show_available_connectors);
+}
+
+static void drm_show_available_cards_connectors_and_modes(struct mp_log *log)
+{
+ drm_show_foreach_card(log, drm_show_available_connectors_and_modes);
+}
+
+static int drm_connector_opt_help(struct mp_log *log, const struct m_option *opt,
+ struct bstr name)
+{
+ drm_show_available_cards_and_connectors(log);
+ return M_OPT_EXIT;
+}
+
+static int drm_mode_opt_help(struct mp_log *log, const struct m_option *opt,
+ struct bstr name)
+{
+ drm_show_available_cards_connectors_and_modes(log);
+ return M_OPT_EXIT;
+}
+
+static int drm_validate_mode_opt(struct mp_log *log, const struct m_option *opt,
+ struct bstr name, const char **value)
+{
+ const char *param = *value;
+ if (!parse_mode_spec(param, NULL)) {
+ mp_fatal(log, "Invalid value for option drm-mode. Must be a positive number, a string of the format WxH[@R] or 'help'\n");
+ return M_OPT_INVALID;
+ }
+
+ return 1;
+}
+
+/* Helpers */
+double vo_drm_get_display_fps(struct vo_drm_state *drm)
+{
+ return mode_get_Hz(&drm->mode.mode);
+}
+
+void vo_drm_set_monitor_par(struct vo *vo)
+{
+ struct vo_drm_state *drm = vo->drm;
+ if (vo->opts->force_monitor_aspect != 0.0) {
+ vo->monitor_par = drm->fb->width / (double) drm->fb->height /
+ vo->opts->force_monitor_aspect;
+ } else {
+ vo->monitor_par = 1 / vo->opts->monitor_pixel_aspect;
+ }
+ MP_VERBOSE(drm, "Monitor pixel aspect: %g\n", vo->monitor_par);
+}
+
+void vo_drm_wait_events(struct vo *vo, int64_t until_time_ns)
+{
+ struct vo_drm_state *drm = vo->drm;
+ if (drm->vt_switcher_active) {
+ int64_t wait_ns = until_time_ns - mp_time_ns();
+ int64_t timeout_ns = MPCLAMP(wait_ns, 0, MP_TIME_S_TO_NS(10));
+ vt_switcher_poll(&drm->vt_switcher, timeout_ns);
+ } else {
+ vo_wait_default(vo, until_time_ns);
+ }
+}
+
+void vo_drm_wait_on_flip(struct vo_drm_state *drm)
+{
+ // poll page flip finish event
+ while (drm->waiting_for_flip) {
+ const int timeout_ms = 3000;
+ struct pollfd fds[1] = { { .events = POLLIN, .fd = drm->fd } };
+ poll(fds, 1, timeout_ms);
+ if (fds[0].revents & POLLIN) {
+ const int ret = drmHandleEvent(drm->fd, &drm->ev);
+ if (ret != 0) {
+ MP_ERR(drm, "drmHandleEvent failed: %i\n", ret);
+ return;
+ }
+ }
+ }
+}
+
+void vo_drm_wakeup(struct vo *vo)
+{
+ struct vo_drm_state *drm = vo->drm;
+ if (drm->vt_switcher_active)
+ vt_switcher_interrupt_poll(&drm->vt_switcher);
+}
diff --git a/video/out/drm_common.h b/video/out/drm_common.h
new file mode 100644
index 0000000..581151f
--- /dev/null
+++ b/video/out/drm_common.h
@@ -0,0 +1,108 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MP_VT_SWITCHER_H
+#define MP_VT_SWITCHER_H
+
+#include <stdbool.h>
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+#include "vo.h"
+
+#define DRM_OPTS_FORMAT_XRGB8888 0
+#define DRM_OPTS_FORMAT_XRGB2101010 1
+#define DRM_OPTS_FORMAT_XBGR8888 2
+#define DRM_OPTS_FORMAT_XBGR2101010 3
+
+struct framebuffer {
+ int fd;
+ uint32_t width;
+ uint32_t height;
+ uint32_t stride;
+ uint32_t size;
+ uint32_t handle;
+ uint8_t *map;
+ uint32_t id;
+};
+
+struct drm_mode {
+ drmModeModeInfo mode;
+ uint32_t blob_id;
+};
+
+struct drm_opts {
+ char *device_path;
+ char *connector_spec;
+ char *mode_spec;
+ int drm_atomic;
+ int draw_plane;
+ int drmprime_video_plane;
+ int drm_format;
+ struct m_geometry draw_surface_size;
+ int vrr_enabled;
+};
+
+struct vt_switcher {
+ int tty_fd;
+ struct mp_log *log;
+ void (*handlers[2])(void*);
+ void *handler_data[2];
+};
+
+struct vo_drm_state {
+ drmModeConnector *connector;
+ drmModeEncoder *encoder;
+ drmEventContext ev;
+
+ struct drm_atomic_context *atomic_context;
+ struct drm_mode mode;
+ struct drm_opts *opts;
+ struct framebuffer *fb;
+ struct mp_log *log;
+ struct mp_present *present;
+ struct vo *vo;
+ struct vt_switcher vt_switcher;
+
+ bool active;
+ bool paused;
+ bool still;
+ bool vt_switcher_active;
+ bool waiting_for_flip;
+
+ char *card_path;
+ int card_no;
+ int fd;
+
+ uint32_t crtc_id;
+ uint32_t height;
+ uint32_t width;
+};
+
+bool vo_drm_init(struct vo *vo);
+int vo_drm_control(struct vo *vo, int *events, int request, void *arg);
+
+double vo_drm_get_display_fps(struct vo_drm_state *drm);
+void vo_drm_set_monitor_par(struct vo *vo);
+void vo_drm_uninit(struct vo *vo);
+void vo_drm_wait_events(struct vo *vo, int64_t until_time_ns);
+void vo_drm_wait_on_flip(struct vo_drm_state *drm);
+void vo_drm_wakeup(struct vo *vo);
+
+bool vo_drm_acquire_crtc(struct vo_drm_state *drm);
+void vo_drm_release_crtc(struct vo_drm_state *drm);
+
+#endif
diff --git a/video/out/drm_prime.c b/video/out/drm_prime.c
new file mode 100644
index 0000000..9335fa8
--- /dev/null
+++ b/video/out/drm_prime.c
@@ -0,0 +1,160 @@
+/*
+ * 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 <errno.h>
+#include <unistd.h>
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+#include <drm_mode.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "drm_common.h"
+#include "drm_prime.h"
+
+int drm_prime_create_framebuffer(struct mp_log *log, int fd,
+ AVDRMFrameDescriptor *descriptor, int width,
+ int height, struct drm_prime_framebuffer *framebuffer,
+ struct drm_prime_handle_refs *handle_refs)
+{
+ AVDRMLayerDescriptor *layer = NULL;
+ uint32_t pitches[4] = { 0 };
+ uint32_t offsets[4] = { 0 };
+ uint32_t handles[4] = { 0 };
+ uint64_t modifiers[4] = { 0 };
+ int ret, layer_fd;
+
+ if (descriptor && descriptor->nb_layers) {
+ *framebuffer = (struct drm_prime_framebuffer){0};
+
+ for (int object = 0; object < descriptor->nb_objects; object++) {
+ ret = drmPrimeFDToHandle(fd, descriptor->objects[object].fd,
+ &framebuffer->gem_handles[object]);
+ if (ret < 0) {
+ mp_err(log, "Failed to retrieve the Prime Handle from handle %d (%d).\n",
+ object, descriptor->objects[object].fd);
+ goto fail;
+ }
+ modifiers[object] = descriptor->objects[object].format_modifier;
+ }
+
+ layer = &descriptor->layers[0];
+
+ for (int plane = 0; plane < AV_DRM_MAX_PLANES; plane++) {
+ layer_fd = framebuffer->gem_handles[layer->planes[plane].object_index];
+ if (layer_fd && layer->planes[plane].pitch) {
+ pitches[plane] = layer->planes[plane].pitch;
+ offsets[plane] = layer->planes[plane].offset;
+ handles[plane] = layer_fd;
+ } else {
+ pitches[plane] = 0;
+ offsets[plane] = 0;
+ handles[plane] = 0;
+ modifiers[plane] = 0;
+ }
+ }
+
+ ret = drmModeAddFB2WithModifiers(fd, width, height, layer->format,
+ handles, pitches, offsets,
+ modifiers, &framebuffer->fb_id,
+ DRM_MODE_FB_MODIFIERS);
+ if (ret < 0) {
+ ret = drmModeAddFB2(fd, width, height, layer->format,
+ handles, pitches, offsets,
+ &framebuffer->fb_id, 0);
+ if (ret < 0) {
+ mp_err(log, "Failed to create framebuffer with drmModeAddFB2 on layer %d: %s\n",
+ 0, mp_strerror(errno));
+ goto fail;
+ }
+ }
+
+ for (int plane = 0; plane < AV_DRM_MAX_PLANES; plane++) {
+ drm_prime_add_handle_ref(handle_refs, framebuffer->gem_handles[plane]);
+ }
+ }
+
+ return 0;
+
+fail:
+ memset(framebuffer, 0, sizeof(*framebuffer));
+ return -1;
+}
+
+void drm_prime_destroy_framebuffer(struct mp_log *log, int fd,
+ struct drm_prime_framebuffer *framebuffer,
+ struct drm_prime_handle_refs *handle_refs)
+{
+ if (framebuffer->fb_id)
+ drmModeRmFB(fd, framebuffer->fb_id);
+
+ for (int i = 0; i < AV_DRM_MAX_PLANES; i++) {
+ if (framebuffer->gem_handles[i]) {
+ drm_prime_remove_handle_ref(handle_refs,
+ framebuffer->gem_handles[i]);
+ if (!drm_prime_get_handle_ref_count(handle_refs,
+ framebuffer->gem_handles[i])) {
+ drmIoctl(fd, DRM_IOCTL_GEM_CLOSE, &framebuffer->gem_handles[i]);
+ }
+ }
+ }
+
+ memset(framebuffer, 0, sizeof(*framebuffer));
+}
+
+void drm_prime_init_handle_ref_count(void *talloc_parent,
+ struct drm_prime_handle_refs *handle_refs)
+{
+ handle_refs->handle_ref_count = talloc_zero(talloc_parent, uint32_t);
+ handle_refs->size = 1;
+ handle_refs->ctx = talloc_parent;
+}
+
+void drm_prime_add_handle_ref(struct drm_prime_handle_refs *handle_refs,
+ uint32_t handle)
+{
+ if (handle) {
+ if (handle > handle_refs->size) {
+ handle_refs->size = handle;
+ MP_TARRAY_GROW(handle_refs->ctx, handle_refs->handle_ref_count,
+ handle_refs->size);
+ }
+ handle_refs->handle_ref_count[handle - 1]++;
+ }
+}
+
+void drm_prime_remove_handle_ref(struct drm_prime_handle_refs *handle_refs,
+ uint32_t handle)
+{
+ if (handle) {
+ if (handle <= handle_refs->size &&
+ handle_refs->handle_ref_count[handle - 1])
+ {
+ handle_refs->handle_ref_count[handle - 1]--;
+ }
+ }
+}
+
+uint32_t drm_prime_get_handle_ref_count(struct drm_prime_handle_refs *handle_refs,
+ uint32_t handle)
+{
+ if (handle) {
+ if (handle <= handle_refs->size)
+ return handle_refs->handle_ref_count[handle - 1];
+ }
+ return 0;
+}
diff --git a/video/out/drm_prime.h b/video/out/drm_prime.h
new file mode 100644
index 0000000..69acba6
--- /dev/null
+++ b/video/out/drm_prime.h
@@ -0,0 +1,45 @@
+/*
+ * 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/>.
+ */
+
+#ifndef DRM_PRIME_H
+#define DRM_PRIME_H
+
+#include <libavutil/hwcontext_drm.h>
+
+#include "common/msg.h"
+
+struct drm_prime_framebuffer {
+ uint32_t fb_id;
+ uint32_t gem_handles[AV_DRM_MAX_PLANES];
+};
+
+struct drm_prime_handle_refs {
+ uint32_t *handle_ref_count;
+ size_t size;
+ void *ctx;
+};
+
+int drm_prime_create_framebuffer(struct mp_log *log, int fd, AVDRMFrameDescriptor *descriptor, int width, int height,
+ struct drm_prime_framebuffer *framebuffers,
+ struct drm_prime_handle_refs *handle_refs);
+void drm_prime_destroy_framebuffer(struct mp_log *log, int fd, struct drm_prime_framebuffer *framebuffers,
+ struct drm_prime_handle_refs *handle_refs);
+void drm_prime_init_handle_ref_count(void *talloc_parent, struct drm_prime_handle_refs *handle_refs);
+void drm_prime_add_handle_ref(struct drm_prime_handle_refs *handle_refs, uint32_t handle);
+void drm_prime_remove_handle_ref(struct drm_prime_handle_refs *handle_refs, uint32_t handle);
+uint32_t drm_prime_get_handle_ref_count(struct drm_prime_handle_refs *handle_refs, uint32_t handle);
+#endif // DRM_PRIME_H
diff --git a/video/out/filter_kernels.c b/video/out/filter_kernels.c
new file mode 100644
index 0000000..95d99ff
--- /dev/null
+++ b/video/out/filter_kernels.c
@@ -0,0 +1,411 @@
+/*
+ * Some of the filter code was taken from Glumpy:
+ * # Copyright (c) 2009-2016 Nicolas P. Rougier. All rights reserved.
+ * # Distributed under the (new) BSD License.
+ * (https://github.com/glumpy/glumpy/blob/master/glumpy/library/build-spatial-filters.py)
+ *
+ * Also see:
+ * - http://vector-agg.cvs.sourceforge.net/viewvc/vector-agg/agg-2.5/include/agg_image_filters.h
+ * - Vapoursynth plugin fmtconv (WTFPL Licensed), which is based on
+ * dither plugin for avisynth from the same author:
+ * https://github.com/vapoursynth/fmtconv/tree/master/src/fmtc
+ * - Paul Heckbert's "zoom"
+ * - XBMC: ConvolutionKernels.cpp etc.
+ *
+ * This file is part of mpv.
+ *
+ * This file can be distributed under the 3-clause license ("New BSD License").
+ *
+ * You can alternatively redistribute the non-Glumpy parts of this file 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.
+ */
+
+#include <stddef.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+
+#include "filter_kernels.h"
+#include "common/common.h"
+
+// NOTE: all filters are designed for discrete convolution
+
+const struct filter_window *mp_find_filter_window(const char *name)
+{
+ if (!name)
+ return NULL;
+ for (const struct filter_window *w = mp_filter_windows; w->name; w++) {
+ if (strcmp(w->name, name) == 0)
+ return w;
+ }
+ return NULL;
+}
+
+const struct filter_kernel *mp_find_filter_kernel(const char *name)
+{
+ if (!name)
+ return NULL;
+ for (const struct filter_kernel *k = mp_filter_kernels; k->f.name; k++) {
+ if (strcmp(k->f.name, name) == 0)
+ return k;
+ }
+ return NULL;
+}
+
+// sizes = sorted list of available filter sizes, terminated with size 0
+// inv_scale = source_size / dest_size
+bool mp_init_filter(struct filter_kernel *filter, const int *sizes,
+ double inv_scale)
+{
+ assert(filter->f.radius > 0);
+ double blur = filter->f.blur > 0.0 ? filter->f.blur : 1.0;
+ filter->radius = blur * filter->f.radius;
+
+ // Only downscaling requires widening the filter
+ filter->filter_scale = MPMAX(1.0, inv_scale);
+ double src_radius = filter->radius * filter->filter_scale;
+ // Polar filters are dependent solely on the radius
+ if (filter->polar) {
+ filter->size = 1; // Not meaningful for EWA/polar scalers.
+ // Safety precaution to avoid generating a gigantic shader
+ if (src_radius > 16.0) {
+ src_radius = 16.0;
+ filter->filter_scale = src_radius / filter->radius;
+ return false;
+ }
+ return true;
+ }
+ int size = ceil(2.0 * src_radius);
+ // round up to smallest available size that's still large enough
+ if (size < sizes[0])
+ size = sizes[0];
+ const int *cursize = sizes;
+ while (size > *cursize && *cursize)
+ cursize++;
+ if (*cursize) {
+ filter->size = *cursize;
+ return true;
+ } else {
+ // The filter doesn't fit - instead of failing completely, use the
+ // largest filter available. This is incorrect, but better than refusing
+ // to do anything.
+ filter->size = cursize[-1];
+ filter->filter_scale = (filter->size/2.0) / filter->radius;
+ return false;
+ }
+}
+
+// Sample from a blurred and tapered window
+static double sample_window(struct filter_window *kernel, double x)
+{
+ if (!kernel->weight)
+ return 1.0;
+
+ // All windows are symmetric, this makes life easier
+ x = fabs(x);
+
+ // Stretch and taper the window size as needed
+ x = kernel->blur > 0.0 ? x / kernel->blur : x;
+ x = x <= kernel->taper ? 0.0 : (x - kernel->taper) / (1 - kernel->taper);
+
+ if (x < kernel->radius)
+ return kernel->weight(kernel, x);
+ return 0.0;
+}
+
+// Evaluate a filter's kernel and window at a given absolute position
+static double sample_filter(struct filter_kernel *filter, double x)
+{
+ // The window is always stretched to the entire kernel
+ double w = sample_window(&filter->w, x / filter->radius * filter->w.radius);
+ double k = w * sample_window(&filter->f, x);
+ return k < 0 ? (1 - filter->clamp) * k : k;
+}
+
+// Calculate the 1D filtering kernel for N sample points.
+// N = number of samples, which is filter->size
+// The weights will be stored in out_w[0] to out_w[N - 1]
+// f = x0 - abs(x0), subpixel position in the range [0,1) or [0,1].
+static void mp_compute_weights(struct filter_kernel *filter, double f,
+ float *out_w)
+{
+ assert(filter->size > 0);
+ double sum = 0;
+ for (int n = 0; n < filter->size; n++) {
+ double x = f - (n - filter->size / 2 + 1);
+ double w = sample_filter(filter, x / filter->filter_scale);
+ out_w[n] = w;
+ sum += w;
+ }
+ // Normalize to preserve energy
+ for (int n = 0; n < filter->size; n++)
+ out_w[n] /= sum;
+}
+
+// Fill the given array with weights for the range [0.0, 1.0]. The array is
+// interpreted as rectangular array of count * filter->size items, with a
+// stride of `stride` floats in between each array element. (For polar filters,
+// the `count` indicates the row size and filter->size/stride are ignored)
+//
+// There will be slight sampling error if these weights are used in a OpenGL
+// texture as LUT directly. The sampling point of a texel is located at its
+// center, so out_array[0] will end up at 0.5 / count instead of 0.0.
+// Correct lookup requires a linear coordinate mapping from [0.0, 1.0] to
+// [0.5 / count, 1.0 - 0.5 / count].
+void mp_compute_lut(struct filter_kernel *filter, int count, int stride,
+ float *out_array)
+{
+ if (filter->polar) {
+ filter->radius_cutoff = 0.0;
+ // Compute a 1D array indexed by radius
+ for (int x = 0; x < count; x++) {
+ double r = x * filter->radius / (count - 1);
+ out_array[x] = sample_filter(filter, r);
+
+ if (fabs(out_array[x]) > 1e-3f)
+ filter->radius_cutoff = r;
+ }
+ } else {
+ // Compute a 2D array indexed by subpixel position
+ for (int n = 0; n < count; n++) {
+ mp_compute_weights(filter, n / (double)(count - 1),
+ out_array + stride * n);
+ }
+ }
+}
+
+typedef struct filter_window params;
+
+static double box(params *p, double x)
+{
+ // This is mathematically 1.0 everywhere, the clipping is done implicitly
+ // based on the radius.
+ return 1.0;
+}
+
+static double triangle(params *p, double x)
+{
+ return fmax(0.0, 1.0 - fabs(x / p->radius));
+}
+
+static double cosine(params *p, double x)
+{
+ return cos(x);
+}
+
+static double hanning(params *p, double x)
+{
+ return 0.5 + 0.5 * cos(M_PI * x);
+}
+
+static double hamming(params *p, double x)
+{
+ return 0.54 + 0.46 * cos(M_PI * x);
+}
+
+static double quadric(params *p, double x)
+{
+ if (x < 0.5) {
+ return 0.75 - x * x;
+ } else if (x < 1.5) {
+ double t = x - 1.5;
+ return 0.5 * t * t;
+ }
+ return 0.0;
+}
+
+static double bessel_i0(double x)
+{
+ double s = 1.0;
+ double y = x * x / 4.0;
+ double t = y;
+ int i = 2;
+ while (t > 1e-12) {
+ s += t;
+ t *= y / (i * i);
+ i += 1;
+ }
+ return s;
+}
+
+static double kaiser(params *p, double x)
+{
+ if (x > 1)
+ return 0;
+ double i0a = 1.0 / bessel_i0(p->params[0]);
+ return bessel_i0(p->params[0] * sqrt(1.0 - x * x)) * i0a;
+}
+
+static double blackman(params *p, double x)
+{
+ double a = p->params[0];
+ double a0 = (1-a)/2.0, a1 = 1/2.0, a2 = a/2.0;
+ double pix = M_PI * x;
+ return a0 + a1*cos(pix) + a2*cos(2 * pix);
+}
+
+static double welch(params *p, double x)
+{
+ return 1.0 - x*x;
+}
+
+// Family of cubic B/C splines
+static double cubic_bc(params *p, double x)
+{
+ double b = p->params[0],
+ c = p->params[1];
+ double p0 = (6.0 - 2.0 * b) / 6.0,
+ p2 = (-18.0 + 12.0 * b + 6.0 * c) / 6.0,
+ p3 = (12.0 - 9.0 * b - 6.0 * c) / 6.0,
+ q0 = (8.0 * b + 24.0 * c) / 6.0,
+ q1 = (-12.0 * b - 48.0 * c) / 6.0,
+ q2 = (6.0 * b + 30.0 * c) / 6.0,
+ q3 = (-b - 6.0 * c) / 6.0;
+
+ if (x < 1.0) {
+ return p0 + x * x * (p2 + x * p3);
+ } else if (x < 2.0) {
+ return q0 + x * (q1 + x * (q2 + x * q3));
+ }
+ return 0.0;
+}
+
+static double spline16(params *p, double x)
+{
+ if (x < 1.0) {
+ return ((x - 9.0/5.0 ) * x - 1.0/5.0 ) * x + 1.0;
+ } else {
+ return ((-1.0/3.0 * (x-1) + 4.0/5.0) * (x-1) - 7.0/15.0 ) * (x-1);
+ }
+}
+
+static double spline36(params *p, double x)
+{
+ if (x < 1.0) {
+ return ((13.0/11.0 * x - 453.0/209.0) * x - 3.0/209.0) * x + 1.0;
+ } else if (x < 2.0) {
+ return ((-6.0/11.0 * (x-1) + 270.0/209.0) * (x-1) - 156.0/ 209.0) * (x-1);
+ } else {
+ return ((1.0/11.0 * (x-2) - 45.0/209.0) * (x-2) + 26.0/209.0) * (x-2);
+ }
+}
+
+static double spline64(params *p, double x)
+{
+ if (x < 1.0) {
+ return ((49.0/41.0 * x - 6387.0/2911.0) * x - 3.0/2911.0) * x + 1.0;
+ } else if (x < 2.0) {
+ return ((-24.0/41.0 * (x-1) + 4032.0/2911.0) * (x-1) - 2328.0/2911.0) * (x-1);
+ } else if (x < 3.0) {
+ return ((6.0/41.0 * (x-2) - 1008.0/2911.0) * (x-2) + 582.0/2911.0) * (x-2);
+ } else {
+ return ((-1.0/41.0 * (x-3) + 168.0/2911.0) * (x-3) - 97.0/2911.0) * (x-3);
+ }
+}
+
+static double gaussian(params *p, double x)
+{
+ return exp(-2.0 * x * x / p->params[0]);
+}
+
+static double sinc(params *p, double x)
+{
+ if (fabs(x) < 1e-8)
+ return 1.0;
+ x *= M_PI;
+ return sin(x) / x;
+}
+
+static double jinc(params *p, double x)
+{
+ if (fabs(x) < 1e-8)
+ return 1.0;
+ x *= M_PI;
+ return 2.0 * j1(x) / x;
+}
+
+static double sphinx(params *p, double x)
+{
+ if (fabs(x) < 1e-8)
+ return 1.0;
+ x *= M_PI;
+ return 3.0 * (sin(x) - x * cos(x)) / (x * x * x);
+}
+
+const struct filter_window mp_filter_windows[] = {
+ {"box", 1, box},
+ {"triangle", 1, triangle},
+ {"bartlett", 1, triangle},
+ {"cosine", M_PI_2, cosine},
+ {"hanning", 1, hanning},
+ {"tukey", 1, hanning, .taper = 0.5},
+ {"hamming", 1, hamming},
+ {"quadric", 1.5, quadric},
+ {"welch", 1, welch},
+ {"kaiser", 1, kaiser, .params = {6.33, NAN} },
+ {"blackman", 1, blackman, .params = {0.16, NAN} },
+ {"gaussian", 2, gaussian, .params = {1.00, NAN} },
+ {"sinc", 1, sinc},
+ {"jinc", 1.2196698912665045, jinc},
+ {"sphinx", 1.4302966531242027, sphinx},
+ {0}
+};
+
+#define JINC_R3 3.2383154841662362
+#define JINC_R4 4.2410628637960699
+
+const struct filter_kernel mp_filter_kernels[] = {
+ // Spline filters
+ {{"spline16", 2, spline16}},
+ {{"spline36", 3, spline36}},
+ {{"spline64", 4, spline64}},
+ // Sinc filters
+ {{"sinc", 2, sinc, .resizable = true}},
+ {{"lanczos", 3, sinc, .resizable = true}, .window = "sinc"},
+ {{"ginseng", 3, sinc, .resizable = true}, .window = "jinc"},
+ // Jinc filters
+ {{"jinc", JINC_R3, jinc, .resizable = true}, .polar = true},
+ {{"ewa_lanczos", JINC_R3, jinc, .resizable = true}, .polar = true, .window = "jinc"},
+ {{"ewa_hanning", JINC_R3, jinc, .resizable = true}, .polar = true, .window = "hanning" },
+ {{"ewa_ginseng", JINC_R3, jinc, .resizable = true}, .polar = true, .window = "sinc"},
+ // Slightly sharpened to minimize the 1D step response error (to better
+ // preserve horizontal/vertical lines)
+ {{"ewa_lanczossharp", JINC_R3, jinc, .blur = 0.9812505837223707, .resizable = true},
+ .polar = true, .window = "jinc"},
+ // Similar to the above, but sharpened substantially to the point of
+ // minimizing the total impulse response error on an integer grid. Tends
+ // to preserve hash patterns well. Very sharp but rings a lot.
+ {{"ewa_lanczos4sharpest", JINC_R4, jinc, .blur = 0.8845120932605005, .resizable = true},
+ .polar = true, .window = "jinc"},
+ // Similar to the above, but softened instead, to make even/odd integer
+ // contributions exactly symmetrical. Designed to smooth out hash patterns.
+ {{"ewa_lanczossoft", JINC_R3, jinc, .blur = 1.0164667662867047, .resizable = true},
+ .polar = true, .window = "jinc"},
+ // Very soft (blurred) hanning-windowed jinc; removes almost all aliasing.
+ // Blur parameter picked to match orthogonal and diagonal contributions
+ {{"haasnsoft", JINC_R3, jinc, .blur = 1.11, .resizable = true},
+ .polar = true, .window = "hanning"},
+ // Cubic filters
+ {{"bicubic", 2, cubic_bc, .params = {1.0, 0.0} }},
+ {{"hermite", 1, cubic_bc, .params = {0.0, 0.0} }},
+ {{"catmull_rom", 2, cubic_bc, .params = {0.0, 0.5} }},
+ {{"mitchell", 2, cubic_bc, .params = {1.0/3.0, 1.0/3.0} }},
+ {{"robidoux", 2, cubic_bc, .params = {12 / (19 + 9 * M_SQRT2),
+ 113 / (58 + 216 * M_SQRT2)} }},
+ {{"robidouxsharp", 2, cubic_bc, .params = {6 / (13 + 7 * M_SQRT2),
+ 7 / (2 + 12 * M_SQRT2)} }},
+ {{"ewa_robidoux", 2, cubic_bc, .params = {12 / (19 + 9 * M_SQRT2),
+ 113 / (58 + 216 * M_SQRT2)}},
+ .polar = true},
+ {{"ewa_robidouxsharp", 2,cubic_bc, .params = {6 / (13 + 7 * M_SQRT2),
+ 7 / (2 + 12 * M_SQRT2)}},
+ .polar = true},
+ // Miscellaneous filters
+ {{"box", 1, box, .resizable = true}},
+ {{"nearest", 0.5, box}},
+ {{"triangle", 1, triangle, .resizable = true}},
+ {{"gaussian", 2, gaussian, .params = {1.0, NAN}, .resizable = true}},
+ {{0}}
+};
diff --git a/video/out/filter_kernels.h b/video/out/filter_kernels.h
new file mode 100644
index 0000000..b8b2f67
--- /dev/null
+++ b/video/out/filter_kernels.h
@@ -0,0 +1,56 @@
+/*
+ * This file is part of mpv.
+ *
+ * This file can be distributed under the 3-clause license ("New BSD License").
+ *
+ * You can alternatively redistribute the non-Glumpy parts of this file 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.
+ */
+
+#ifndef MPLAYER_FILTER_KERNELS_H
+#define MPLAYER_FILTER_KERNELS_H
+
+#include <stdbool.h>
+
+struct filter_window {
+ const char *name;
+ double radius; // Preferred radius, should only be changed if resizable
+ double (*weight)(struct filter_window *k, double x);
+ bool resizable; // Filter supports any given radius
+ double params[2]; // User-defined custom filter parameters. Not used by
+ // all filters
+ double blur; // Blur coefficient (sharpens or widens the filter)
+ double taper; // Taper coefficient (flattens the filter's center)
+};
+
+struct filter_kernel {
+ struct filter_window f; // the kernel itself
+ struct filter_window w; // window storage
+ double clamp; // clamping factor, affects negative weights
+ // Constant values
+ const char *window; // default window
+ bool polar; // whether or not the filter uses polar coordinates
+ // The following values are set by mp_init_filter() at runtime.
+ int size; // number of coefficients (may depend on radius)
+ double radius; // true filter radius, derived from f.radius and f.blur
+ double filter_scale; // Factor to convert the mathematical filter
+ // function radius to the possibly wider
+ // (in the case of downsampling) filter sample
+ // radius.
+ double radius_cutoff; // the radius at which we can cut off the filter
+};
+
+extern const struct filter_window mp_filter_windows[];
+extern const struct filter_kernel mp_filter_kernels[];
+
+const struct filter_window *mp_find_filter_window(const char *name);
+const struct filter_kernel *mp_find_filter_kernel(const char *name);
+
+bool mp_init_filter(struct filter_kernel *filter, const int *sizes,
+ double scale);
+void mp_compute_lut(struct filter_kernel *filter, int count, int stride,
+ float *out_array);
+
+#endif /* MPLAYER_FILTER_KERNELS_H */
diff --git a/video/out/gpu/context.c b/video/out/gpu/context.c
new file mode 100644
index 0000000..5ce18af
--- /dev/null
+++ b/video/out/gpu/context.c
@@ -0,0 +1,277 @@
+/*
+ * 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 <stddef.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdbool.h>
+#include <math.h>
+#include <assert.h>
+
+#include "config.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "options/options.h"
+#include "options/m_option.h"
+#include "video/out/vo.h"
+
+#include "context.h"
+#include "spirv.h"
+
+/* OpenGL */
+extern const struct ra_ctx_fns ra_ctx_glx;
+extern const struct ra_ctx_fns ra_ctx_x11_egl;
+extern const struct ra_ctx_fns ra_ctx_drm_egl;
+extern const struct ra_ctx_fns ra_ctx_wayland_egl;
+extern const struct ra_ctx_fns ra_ctx_wgl;
+extern const struct ra_ctx_fns ra_ctx_angle;
+extern const struct ra_ctx_fns ra_ctx_dxgl;
+extern const struct ra_ctx_fns ra_ctx_rpi;
+extern const struct ra_ctx_fns ra_ctx_android;
+
+/* Vulkan */
+extern const struct ra_ctx_fns ra_ctx_vulkan_wayland;
+extern const struct ra_ctx_fns ra_ctx_vulkan_win;
+extern const struct ra_ctx_fns ra_ctx_vulkan_xlib;
+extern const struct ra_ctx_fns ra_ctx_vulkan_android;
+extern const struct ra_ctx_fns ra_ctx_vulkan_display;
+extern const struct ra_ctx_fns ra_ctx_vulkan_mac;
+
+/* Direct3D 11 */
+extern const struct ra_ctx_fns ra_ctx_d3d11;
+
+/* No API */
+extern const struct ra_ctx_fns ra_ctx_wldmabuf;
+
+static const struct ra_ctx_fns *contexts[] = {
+#if HAVE_D3D11
+ &ra_ctx_d3d11,
+#endif
+
+// OpenGL contexts:
+#if HAVE_EGL_ANDROID
+ &ra_ctx_android,
+#endif
+#if HAVE_RPI
+ &ra_ctx_rpi,
+#endif
+#if HAVE_EGL_ANGLE_WIN32
+ &ra_ctx_angle,
+#endif
+#if HAVE_GL_WIN32
+ &ra_ctx_wgl,
+#endif
+#if HAVE_GL_DXINTEROP
+ &ra_ctx_dxgl,
+#endif
+#if HAVE_EGL_WAYLAND
+ &ra_ctx_wayland_egl,
+#endif
+#if HAVE_EGL_X11
+ &ra_ctx_x11_egl,
+#endif
+#if HAVE_GL_X11
+ &ra_ctx_glx,
+#endif
+#if HAVE_EGL_DRM
+ &ra_ctx_drm_egl,
+#endif
+
+// Vulkan contexts:
+#if HAVE_VULKAN
+
+#if HAVE_ANDROID
+ &ra_ctx_vulkan_android,
+#endif
+#if HAVE_WIN32_DESKTOP
+ &ra_ctx_vulkan_win,
+#endif
+#if HAVE_WAYLAND
+ &ra_ctx_vulkan_wayland,
+#endif
+#if HAVE_X11
+ &ra_ctx_vulkan_xlib,
+#endif
+#if HAVE_VK_KHR_DISPLAY
+ &ra_ctx_vulkan_display,
+#endif
+#if HAVE_COCOA && HAVE_SWIFT
+ &ra_ctx_vulkan_mac,
+#endif
+#endif
+
+/* No API contexts: */
+#if HAVE_DMABUF_WAYLAND
+ &ra_ctx_wldmabuf,
+#endif
+};
+
+static int ra_ctx_api_help(struct mp_log *log, const struct m_option *opt,
+ struct bstr name)
+{
+ mp_info(log, "GPU APIs (contexts):\n");
+ mp_info(log, " auto (autodetect)\n");
+ for (int n = 0; n < MP_ARRAY_SIZE(contexts); n++) {
+ if (!contexts[n]->hidden)
+ mp_info(log, " %s (%s)\n", contexts[n]->type, contexts[n]->name);
+ }
+ return M_OPT_EXIT;
+}
+
+static int ra_ctx_validate_api(struct mp_log *log, const struct m_option *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ if (bstr_equals0(param, "auto"))
+ return 1;
+ for (int i = 0; i < MP_ARRAY_SIZE(contexts); i++) {
+ if (bstr_equals0(param, contexts[i]->type) && !contexts[i]->hidden)
+ return 1;
+ }
+ return M_OPT_INVALID;
+}
+
+static int ra_ctx_context_help(struct mp_log *log, const struct m_option *opt,
+ struct bstr name)
+{
+ mp_info(log, "GPU contexts (APIs):\n");
+ mp_info(log, " auto (autodetect)\n");
+ for (int n = 0; n < MP_ARRAY_SIZE(contexts); n++) {
+ if (!contexts[n]->hidden)
+ mp_info(log, " %s (%s)\n", contexts[n]->name, contexts[n]->type);
+ }
+ return M_OPT_EXIT;
+}
+
+static int ra_ctx_validate_context(struct mp_log *log, const struct m_option *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ if (bstr_equals0(param, "auto"))
+ return 1;
+ for (int i = 0; i < MP_ARRAY_SIZE(contexts); i++) {
+ if (bstr_equals0(param, contexts[i]->name) && !contexts[i]->hidden)
+ return 1;
+ }
+ return M_OPT_INVALID;
+}
+
+// Create a VO window and create a RA context on it.
+// vo_flags: passed to the backend's create window function
+struct ra_ctx *ra_ctx_create(struct vo *vo, struct ra_ctx_opts opts)
+{
+ bool api_auto = !opts.context_type || strcmp(opts.context_type, "auto") == 0;
+ bool ctx_auto = !opts.context_name || strcmp(opts.context_name, "auto") == 0;
+
+ if (ctx_auto) {
+ MP_VERBOSE(vo, "Probing for best GPU context.\n");
+ opts.probing = true;
+ }
+
+ // Hack to silence backend (X11/Wayland/etc.) errors. Kill it once backends
+ // are separate from `struct vo`
+ bool old_probing = vo->probing;
+ vo->probing = opts.probing;
+
+ for (int i = 0; i < MP_ARRAY_SIZE(contexts); i++) {
+ if (contexts[i]->hidden)
+ continue;
+ if (!opts.probing && strcmp(contexts[i]->name, opts.context_name) != 0)
+ continue;
+ if (!api_auto && strcmp(contexts[i]->type, opts.context_type) != 0)
+ continue;
+
+ struct ra_ctx *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct ra_ctx) {
+ .vo = vo,
+ .global = vo->global,
+ .log = mp_log_new(ctx, vo->log, contexts[i]->type),
+ .opts = opts,
+ .fns = contexts[i],
+ };
+
+ MP_VERBOSE(ctx, "Initializing GPU context '%s'\n", ctx->fns->name);
+ if (contexts[i]->init(ctx)) {
+ vo->probing = old_probing;
+ return ctx;
+ }
+
+ talloc_free(ctx);
+ }
+
+ vo->probing = old_probing;
+
+ // If we've reached this point, then none of the contexts matched the name
+ // requested, or the backend creation failed for all of them.
+ if (!vo->probing)
+ MP_ERR(vo, "Failed initializing any suitable GPU context!\n");
+ return NULL;
+}
+
+struct ra_ctx *ra_ctx_create_by_name(struct vo *vo, const char *name)
+{
+ for (int i = 0; i < MP_ARRAY_SIZE(contexts); i++) {
+ if (strcmp(name, contexts[i]->name) != 0)
+ continue;
+
+ struct ra_ctx *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct ra_ctx) {
+ .vo = vo,
+ .global = vo->global,
+ .log = mp_log_new(ctx, vo->log, contexts[i]->type),
+ .fns = contexts[i],
+ };
+
+ MP_VERBOSE(ctx, "Initializing GPU context '%s'\n", ctx->fns->name);
+ if (contexts[i]->init(ctx))
+ return ctx;
+ talloc_free(ctx);
+ }
+ return NULL;
+}
+
+void ra_ctx_destroy(struct ra_ctx **ctx_ptr)
+{
+ struct ra_ctx *ctx = *ctx_ptr;
+ if (!ctx)
+ return;
+
+ if (ctx->spirv && ctx->spirv->fns->uninit)
+ ctx->spirv->fns->uninit(ctx);
+
+ ctx->fns->uninit(ctx);
+ talloc_free(ctx);
+
+ *ctx_ptr = NULL;
+}
+
+#define OPT_BASE_STRUCT struct ra_ctx_opts
+const struct m_sub_options ra_ctx_conf = {
+ .opts = (const m_option_t[]) {
+ {"gpu-context",
+ OPT_STRING_VALIDATE(context_name, ra_ctx_validate_context),
+ .help = ra_ctx_context_help},
+ {"gpu-api",
+ OPT_STRING_VALIDATE(context_type, ra_ctx_validate_api),
+ .help = ra_ctx_api_help},
+ {"gpu-debug", OPT_BOOL(debug)},
+ {"gpu-sw", OPT_BOOL(allow_sw)},
+ {0}
+ },
+ .size = sizeof(struct ra_ctx_opts),
+};
diff --git a/video/out/gpu/context.h b/video/out/gpu/context.h
new file mode 100644
index 0000000..6788e6f
--- /dev/null
+++ b/video/out/gpu/context.h
@@ -0,0 +1,107 @@
+#pragma once
+
+#include "video/out/vo.h"
+#include "video/csputils.h"
+
+#include "ra.h"
+
+struct ra_ctx_opts {
+ bool allow_sw; // allow software renderers
+ bool want_alpha; // create an alpha framebuffer if possible
+ bool debug; // enable debugging layers/callbacks etc.
+ bool probing; // the backend was auto-probed
+ char *context_name; // filter by `ra_ctx_fns.name`
+ char *context_type; // filter by `ra_ctx_fns.type`
+};
+
+extern const struct m_sub_options ra_ctx_conf;
+
+struct ra_ctx {
+ struct vo *vo;
+ struct ra *ra;
+ struct mpv_global *global;
+ struct mp_log *log;
+
+ struct ra_ctx_opts opts;
+ const struct ra_ctx_fns *fns;
+ struct ra_swapchain *swapchain;
+ struct spirv_compiler *spirv;
+
+ void *priv;
+};
+
+// The functions that make up a ra_ctx.
+struct ra_ctx_fns {
+ const char *type; // API type (for --gpu-api)
+ const char *name; // name (for --gpu-context)
+
+ bool hidden; // hide the ra_ctx from users
+
+ // Resize the window, or create a new window if there isn't one yet.
+ // Currently, there is an unfortunate interaction with ctx->vo, and
+ // display size etc. are determined by it.
+ bool (*reconfig)(struct ra_ctx *ctx);
+
+ // This behaves exactly like vo_driver.control().
+ int (*control)(struct ra_ctx *ctx, int *events, int request, void *arg);
+
+ // These behave exactly like vo_driver.wakeup/wait_events. They are
+ // optional.
+ void (*wakeup)(struct ra_ctx *ctx);
+ void (*wait_events)(struct ra_ctx *ctx, int64_t until_time_ns);
+ void (*update_render_opts)(struct ra_ctx *ctx);
+
+ // Initialize/destroy the 'struct ra' and possibly the underlying VO backend.
+ // Not normally called by the user of the ra_ctx.
+ bool (*init)(struct ra_ctx *ctx);
+ void (*uninit)(struct ra_ctx *ctx);
+};
+
+// Extra struct for the swapchain-related functions so they can be easily
+// inherited from helpers.
+struct ra_swapchain {
+ struct ra_ctx *ctx;
+ struct priv *priv;
+ const struct ra_swapchain_fns *fns;
+};
+
+// Represents a framebuffer / render target
+struct ra_fbo {
+ struct ra_tex *tex;
+ bool flip; // rendering needs to be inverted
+
+ // Host system's colorspace that it will be interpreting
+ // the frame buffer as.
+ struct mp_colorspace color_space;
+};
+
+struct ra_swapchain_fns {
+ // Gets the current framebuffer depth in bits (0 if unknown). Optional.
+ int (*color_depth)(struct ra_swapchain *sw);
+
+ // Called when rendering starts. Returns NULL on failure. This must be
+ // followed by submit_frame, to submit the rendered frame. This function
+ // can also fail sporadically, and such errors should be ignored unless
+ // they persist.
+ bool (*start_frame)(struct ra_swapchain *sw, struct ra_fbo *out_fbo);
+
+ // Present the frame. Issued in lockstep with start_frame, with rendering
+ // commands in between. The `frame` is just there for timing data, for
+ // swapchains smart enough to do something with it.
+ bool (*submit_frame)(struct ra_swapchain *sw, const struct vo_frame *frame);
+
+ // Performs a buffer swap. This blocks for as long as necessary to meet
+ // params.swapchain_depth, or until the next vblank (for vsynced contexts)
+ void (*swap_buffers)(struct ra_swapchain *sw);
+
+ // See vo. Usually called after swap_buffers().
+ void (*get_vsync)(struct ra_swapchain *sw, struct vo_vsync_info *info);
+};
+
+// Create and destroy a ra_ctx. This also takes care of creating and destroying
+// the underlying `struct ra`, and perhaps the underlying VO backend.
+struct ra_ctx *ra_ctx_create(struct vo *vo, struct ra_ctx_opts opts);
+void ra_ctx_destroy(struct ra_ctx **ctx);
+
+// Special case of creating a ra_ctx while specifying a specific context by name.
+struct ra_ctx *ra_ctx_create_by_name(struct vo *vo, const char *name);
diff --git a/video/out/gpu/d3d11_helpers.c b/video/out/gpu/d3d11_helpers.c
new file mode 100644
index 0000000..30d9eae
--- /dev/null
+++ b/video/out/gpu/d3d11_helpers.c
@@ -0,0 +1,966 @@
+/*
+ * 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 <windows.h>
+#include <d3d11.h>
+#include <dxgi1_6.h>
+#include <versionhelpers.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "misc/bstr.h"
+#include "osdep/io.h"
+#include "osdep/threads.h"
+#include "osdep/windows_utils.h"
+
+#include "d3d11_helpers.h"
+
+// Windows 8 enum value, not present in mingw-w64 headers
+#define DXGI_ADAPTER_FLAG_SOFTWARE (2)
+typedef HRESULT(WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory);
+
+static mp_once d3d11_once = MP_STATIC_ONCE_INITIALIZER;
+static PFN_D3D11_CREATE_DEVICE pD3D11CreateDevice = NULL;
+static PFN_CREATE_DXGI_FACTORY pCreateDXGIFactory1 = NULL;
+static void d3d11_load(void)
+{
+ HMODULE d3d11 = LoadLibraryW(L"d3d11.dll");
+ HMODULE dxgilib = LoadLibraryW(L"dxgi.dll");
+ if (!d3d11 || !dxgilib)
+ return;
+
+ pD3D11CreateDevice = (PFN_D3D11_CREATE_DEVICE)
+ GetProcAddress(d3d11, "D3D11CreateDevice");
+ pCreateDXGIFactory1 = (PFN_CREATE_DXGI_FACTORY)
+ GetProcAddress(dxgilib, "CreateDXGIFactory1");
+}
+
+static bool load_d3d11_functions(struct mp_log *log)
+{
+ mp_exec_once(&d3d11_once, d3d11_load);
+ if (!pD3D11CreateDevice || !pCreateDXGIFactory1) {
+ mp_fatal(log, "Failed to load base d3d11 functionality: "
+ "CreateDevice: %s, CreateDXGIFactory1: %s\n",
+ pD3D11CreateDevice ? "success" : "failure",
+ pCreateDXGIFactory1 ? "success": "failure");
+ return false;
+ }
+
+ return true;
+}
+
+#define D3D11_DXGI_ENUM(prefix, define) { case prefix ## define: return #define; }
+
+static const char *d3d11_get_format_name(DXGI_FORMAT fmt)
+{
+ switch (fmt) {
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, UNKNOWN);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32A32_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32A32_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32A32_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32A32_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32B32_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16B16A16_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16B16A16_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16B16A16_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16B16A16_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16B16A16_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16B16A16_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G32_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32G8X24_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, D32_FLOAT_S8X24_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32_FLOAT_X8X24_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, X32_TYPELESS_G8X24_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R10G10B10A2_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R10G10B10A2_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R10G10B10A2_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R11G11B10_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8B8A8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8B8A8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8B8A8_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8B8A8_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8B8A8_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8B8A8_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16G16_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, D32_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R32_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R24G8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, D24_UNORM_S8_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R24_UNORM_X8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, X24_TYPELESS_G8_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16_FLOAT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, D16_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R16_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8_UINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8_SINT);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, A8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R1_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R9G9B9E5_SHAREDEXP);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R8G8_B8G8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, G8R8_G8B8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC1_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC1_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC1_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC2_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC2_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC2_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC3_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC3_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC3_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC4_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC4_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC4_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC5_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC5_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC5_SNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B5G6R5_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B5G5R5A1_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B8G8R8A8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B8G8R8X8_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, R10G10B10_XR_BIAS_A2_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B8G8R8A8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B8G8R8A8_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B8G8R8X8_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B8G8R8X8_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC6H_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC6H_UF16);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC6H_SF16);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC7_TYPELESS);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC7_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, BC7_UNORM_SRGB);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, AYUV);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, Y410);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, Y416);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, NV12);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, P010);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, P016);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, 420_OPAQUE);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, YUY2);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, Y210);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, Y216);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, NV11);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, AI44);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, IA44);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, P8);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, A8P8);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, B4G4R4A4_UNORM);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, P208);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, V208);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, V408);
+ D3D11_DXGI_ENUM(DXGI_FORMAT_, FORCE_UINT);
+ default:
+ return "<Unknown>";
+ }
+}
+
+static const char *d3d11_get_csp_name(DXGI_COLOR_SPACE_TYPE csp)
+{
+ switch (csp) {
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_FULL_G22_NONE_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_FULL_G10_NONE_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_STUDIO_G22_NONE_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_STUDIO_G22_NONE_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RESERVED);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_FULL_G22_NONE_P709_X601);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G22_LEFT_P601);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_FULL_G22_LEFT_P601);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G22_LEFT_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_FULL_G22_LEFT_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G22_LEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_FULL_G22_LEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_FULL_G2084_NONE_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G2084_LEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_STUDIO_G2084_NONE_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G22_TOPLEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G2084_TOPLEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_FULL_G22_NONE_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_GHLG_TOPLEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_FULL_GHLG_TOPLEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_STUDIO_G24_NONE_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, RGB_STUDIO_G24_NONE_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G24_LEFT_P709);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G24_LEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, YCBCR_STUDIO_G24_TOPLEFT_P2020);
+ D3D11_DXGI_ENUM(DXGI_COLOR_SPACE_, CUSTOM);
+ default:
+ return "<Unknown>";
+ }
+}
+
+static bool d3d11_get_mp_csp(DXGI_COLOR_SPACE_TYPE csp,
+ struct mp_colorspace *mp_csp)
+{
+ if (!mp_csp)
+ return false;
+
+ // Colorspaces utilizing gamma 2.2 (G22) are set to
+ // AUTO as that keeps the current default flow regarding
+ // SDR transfer function handling.
+ // (no adjustment is done unless the user has a CMS LUT).
+ //
+ // Additionally, only set primary information with colorspaces
+ // utilizing non-709 primaries to keep the current behavior
+ // regarding not doing conversion from BT.601 to BT.709.
+ switch (csp) {
+ case DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709:
+ *mp_csp = (struct mp_colorspace){
+ .gamma = MP_CSP_TRC_AUTO,
+ .primaries = MP_CSP_PRIM_AUTO,
+ };
+ break;
+ case DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709:
+ *mp_csp = (struct mp_colorspace) {
+ .gamma = MP_CSP_TRC_LINEAR,
+ .primaries = MP_CSP_PRIM_AUTO,
+ };
+ break;
+ case DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020:
+ *mp_csp = (struct mp_colorspace) {
+ .gamma = MP_CSP_TRC_PQ,
+ .primaries = MP_CSP_PRIM_BT_2020,
+ };
+ break;
+ case DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020:
+ *mp_csp = (struct mp_colorspace) {
+ .gamma = MP_CSP_TRC_AUTO,
+ .primaries = MP_CSP_PRIM_BT_2020,
+ };
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+static bool query_output_format_and_colorspace(struct mp_log *log,
+ IDXGISwapChain *swapchain,
+ DXGI_FORMAT *out_fmt,
+ DXGI_COLOR_SPACE_TYPE *out_cspace)
+{
+ IDXGIOutput *output = NULL;
+ IDXGIOutput6 *output6 = NULL;
+ DXGI_OUTPUT_DESC1 desc = { 0 };
+ char *monitor_name = NULL;
+ bool success = false;
+
+ if (!out_fmt || !out_cspace)
+ return false;
+
+ HRESULT hr = IDXGISwapChain_GetContainingOutput(swapchain, &output);
+ if (FAILED(hr)) {
+ mp_err(log, "Failed to get swap chain's containing output: %s!\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ hr = IDXGIOutput_QueryInterface(output, &IID_IDXGIOutput6,
+ (void**)&output6);
+ if (FAILED(hr)) {
+ // point where systems older than Windows 10 would fail,
+ // thus utilizing error log level only with windows 10+
+ mp_msg(log, IsWindows10OrGreater() ? MSGL_ERR : MSGL_V,
+ "Failed to create a DXGI 1.6 output interface: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ hr = IDXGIOutput6_GetDesc1(output6, &desc);
+ if (FAILED(hr)) {
+ mp_err(log, "Failed to query swap chain's output information: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ monitor_name = mp_to_utf8(NULL, desc.DeviceName);
+
+ mp_verbose(log, "Queried output: %s, %ldx%ld @ %d bits, colorspace: %s (%d)\n",
+ monitor_name,
+ desc.DesktopCoordinates.right - desc.DesktopCoordinates.left,
+ desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top,
+ desc.BitsPerColor,
+ d3d11_get_csp_name(desc.ColorSpace),
+ desc.ColorSpace);
+
+ *out_cspace = desc.ColorSpace;
+
+ // limit ourselves to the 8bit and 10bit formats for now.
+ // while the 16bit float format would be preferable as something
+ // to default to, it seems to be hard-coded to linear transfer
+ // in windowed mode, and follows configured colorspace in full screen.
+ *out_fmt = desc.BitsPerColor > 8 ?
+ DXGI_FORMAT_R10G10B10A2_UNORM : DXGI_FORMAT_R8G8B8A8_UNORM;
+
+ success = true;
+
+done:
+ talloc_free(monitor_name);
+ SAFE_RELEASE(output6);
+ SAFE_RELEASE(output);
+ return success;
+}
+
+// Get a const array of D3D_FEATURE_LEVELs from max_fl to min_fl (inclusive)
+static int get_feature_levels(int max_fl, int min_fl,
+ const D3D_FEATURE_LEVEL **out)
+{
+ static const D3D_FEATURE_LEVEL levels[] = {
+ D3D_FEATURE_LEVEL_12_1,
+ D3D_FEATURE_LEVEL_12_0,
+ D3D_FEATURE_LEVEL_11_1,
+ D3D_FEATURE_LEVEL_11_0,
+ D3D_FEATURE_LEVEL_10_1,
+ D3D_FEATURE_LEVEL_10_0,
+ D3D_FEATURE_LEVEL_9_3,
+ D3D_FEATURE_LEVEL_9_2,
+ D3D_FEATURE_LEVEL_9_1,
+ };
+ static const int levels_len = MP_ARRAY_SIZE(levels);
+
+ int start = 0;
+ for (; start < levels_len; start++) {
+ if (levels[start] <= max_fl)
+ break;
+ }
+ int len = 0;
+ for (; start + len < levels_len; len++) {
+ if (levels[start + len] < min_fl)
+ break;
+ }
+ *out = &levels[start];
+ return len;
+}
+
+static IDXGIAdapter1 *get_d3d11_adapter(struct mp_log *log,
+ struct bstr requested_adapter_name,
+ struct bstr *listing)
+{
+ HRESULT hr = S_OK;
+ IDXGIFactory1 *factory;
+ IDXGIAdapter1 *picked_adapter = NULL;
+
+ hr = pCreateDXGIFactory1(&IID_IDXGIFactory1, (void **)&factory);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to create a DXGI factory: %s\n",
+ mp_HRESULT_to_str(hr));
+ return NULL;
+ }
+
+ for (unsigned int adapter_num = 0; hr != DXGI_ERROR_NOT_FOUND; adapter_num++)
+ {
+ IDXGIAdapter1 *adapter = NULL;
+ DXGI_ADAPTER_DESC1 desc = { 0 };
+ char *adapter_description = NULL;
+
+ hr = IDXGIFactory1_EnumAdapters1(factory, adapter_num, &adapter);
+ if (FAILED(hr)) {
+ if (hr != DXGI_ERROR_NOT_FOUND) {
+ mp_fatal(log, "Failed to enumerate at adapter %u\n",
+ adapter_num);
+ }
+ continue;
+ }
+
+ if (FAILED(IDXGIAdapter1_GetDesc1(adapter, &desc))) {
+ mp_fatal(log, "Failed to get adapter description when listing at adapter %u\n",
+ adapter_num);
+ continue;
+ }
+
+ adapter_description = mp_to_utf8(NULL, desc.Description);
+
+ if (listing) {
+ bstr_xappend_asprintf(NULL, listing,
+ "Adapter %u: vendor: %u, description: %s\n",
+ adapter_num, desc.VendorId,
+ adapter_description);
+ }
+
+ if (requested_adapter_name.len &&
+ bstr_case_startswith(bstr0(adapter_description),
+ requested_adapter_name))
+ {
+ picked_adapter = adapter;
+ }
+
+ talloc_free(adapter_description);
+
+ if (picked_adapter) {
+ break;
+ }
+
+ SAFE_RELEASE(adapter);
+ }
+
+ SAFE_RELEASE(factory);
+
+ return picked_adapter;
+}
+
+static HRESULT create_device(struct mp_log *log, IDXGIAdapter1 *adapter,
+ bool warp, bool debug, int max_fl, int min_fl,
+ ID3D11Device **dev)
+{
+ const D3D_FEATURE_LEVEL *levels;
+ int levels_len = get_feature_levels(max_fl, min_fl, &levels);
+ if (!levels_len) {
+ mp_fatal(log, "No suitable Direct3D feature level found\n");
+ return E_FAIL;
+ }
+
+ D3D_DRIVER_TYPE type = warp ? D3D_DRIVER_TYPE_WARP
+ : D3D_DRIVER_TYPE_HARDWARE;
+ UINT flags = debug ? D3D11_CREATE_DEVICE_DEBUG : 0;
+ return pD3D11CreateDevice((IDXGIAdapter *)adapter, adapter ? D3D_DRIVER_TYPE_UNKNOWN : type,
+ NULL, flags, levels, levels_len, D3D11_SDK_VERSION, dev, NULL, NULL);
+}
+
+bool mp_d3d11_list_or_verify_adapters(struct mp_log *log,
+ bstr adapter_name,
+ bstr *listing)
+{
+ IDXGIAdapter1 *picked_adapter = NULL;
+
+ if (!load_d3d11_functions(log)) {
+ return false;
+ }
+
+ if ((picked_adapter = get_d3d11_adapter(log, adapter_name, listing))) {
+ SAFE_RELEASE(picked_adapter);
+ return true;
+ }
+
+ return false;
+}
+
+// Create a Direct3D 11 device for rendering and presentation. This is meant to
+// reduce boilerplate in backends that D3D11, while also making sure they share
+// the same device creation logic and log the same information.
+bool mp_d3d11_create_present_device(struct mp_log *log,
+ struct d3d11_device_opts *opts,
+ ID3D11Device **dev_out)
+{
+ bool debug = opts->debug;
+ bool warp = opts->force_warp;
+ int max_fl = opts->max_feature_level;
+ int min_fl = opts->min_feature_level;
+ // Normalize nullptr and an empty string to nullptr to simplify handling.
+ char *adapter_name = (opts->adapter_name && *(opts->adapter_name)) ?
+ opts->adapter_name : NULL;
+ ID3D11Device *dev = NULL;
+ IDXGIDevice1 *dxgi_dev = NULL;
+ IDXGIAdapter1 *adapter = NULL;
+ bool success = false;
+ HRESULT hr;
+
+ if (!load_d3d11_functions(log)) {
+ goto done;
+ }
+
+ adapter = get_d3d11_adapter(log, bstr0(adapter_name), NULL);
+
+ if (adapter_name && !adapter) {
+ mp_warn(log, "Adapter matching '%s' was not found in the system! "
+ "Will fall back to the default adapter.\n",
+ adapter_name);
+ }
+
+ // Return here to retry creating the device
+ do {
+ // Use these default feature levels if they are not set
+ max_fl = max_fl ? max_fl : D3D_FEATURE_LEVEL_11_0;
+ min_fl = min_fl ? min_fl : D3D_FEATURE_LEVEL_9_1;
+
+ hr = create_device(log, adapter, warp, debug, max_fl, min_fl, &dev);
+
+ // Retry without debug, if SDK is not available
+ if (debug && hr == DXGI_ERROR_SDK_COMPONENT_MISSING) {
+ mp_warn(log, "gpu-debug disabled due to error: %s\n", mp_HRESULT_to_str(hr));
+ debug = false;
+ continue;
+ }
+
+ if (SUCCEEDED(hr))
+ break;
+
+ // Trying to create a D3D_FEATURE_LEVEL_12_0 device on Windows 8.1 or
+ // below will not succeed. Try an 11_1 device.
+ if (max_fl >= D3D_FEATURE_LEVEL_12_0 &&
+ min_fl <= D3D_FEATURE_LEVEL_11_1)
+ {
+ mp_dbg(log, "Failed to create 12_0+ device, trying 11_1\n");
+ max_fl = D3D_FEATURE_LEVEL_11_1;
+ continue;
+ }
+
+ // Trying to create a D3D_FEATURE_LEVEL_11_1 device on Windows 7
+ // without the platform update will not succeed. Try an 11_0 device.
+ if (max_fl >= D3D_FEATURE_LEVEL_11_1 &&
+ min_fl <= D3D_FEATURE_LEVEL_11_0)
+ {
+ mp_dbg(log, "Failed to create 11_1+ device, trying 11_0\n");
+ max_fl = D3D_FEATURE_LEVEL_11_0;
+ continue;
+ }
+
+ // Retry with WARP if allowed
+ if (!warp && opts->allow_warp) {
+ mp_dbg(log, "Failed to create hardware device, trying WARP\n");
+ warp = true;
+ max_fl = opts->max_feature_level;
+ min_fl = opts->min_feature_level;
+ continue;
+ }
+
+ mp_fatal(log, "Failed to create Direct3D 11 device: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ } while (true);
+
+ // if we picked an adapter, release it here - we're taking another
+ // from the device.
+ SAFE_RELEASE(adapter);
+
+ hr = ID3D11Device_QueryInterface(dev, &IID_IDXGIDevice1, (void**)&dxgi_dev);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to get DXGI device\n");
+ goto done;
+ }
+ hr = IDXGIDevice1_GetParent(dxgi_dev, &IID_IDXGIAdapter1, (void**)&adapter);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to get DXGI adapter\n");
+ goto done;
+ }
+
+ IDXGIDevice1_SetMaximumFrameLatency(dxgi_dev, opts->max_frame_latency);
+
+ DXGI_ADAPTER_DESC1 desc;
+ hr = IDXGIAdapter1_GetDesc1(adapter, &desc);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to get adapter description\n");
+ goto done;
+ }
+
+ D3D_FEATURE_LEVEL selected_level = ID3D11Device_GetFeatureLevel(dev);
+ mp_verbose(log, "Using Direct3D 11 feature level %u_%u\n",
+ ((unsigned)selected_level) >> 12,
+ (((unsigned)selected_level) >> 8) & 0xf);
+
+ char *dev_name = mp_to_utf8(NULL, desc.Description);
+ mp_verbose(log, "Device Name: %s\n"
+ "Device ID: %04x:%04x (rev %02x)\n"
+ "Subsystem ID: %04x:%04x\n"
+ "LUID: %08lx%08lx\n",
+ dev_name,
+ desc.VendorId, desc.DeviceId, desc.Revision,
+ LOWORD(desc.SubSysId), HIWORD(desc.SubSysId),
+ desc.AdapterLuid.HighPart, desc.AdapterLuid.LowPart);
+ talloc_free(dev_name);
+
+ if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
+ warp = true;
+ // If the primary display adapter is a software adapter, the
+ // DXGI_ADAPTER_FLAG_SOFTWARE flag won't be set, but the device IDs should
+ // still match the Microsoft Basic Render Driver
+ if (desc.VendorId == 0x1414 && desc.DeviceId == 0x8c)
+ warp = true;
+ if (warp) {
+ mp_msg(log, opts->force_warp ? MSGL_V : MSGL_WARN,
+ "Using a software adapter\n");
+ }
+
+ *dev_out = dev;
+ dev = NULL;
+ success = true;
+
+done:
+ SAFE_RELEASE(adapter);
+ SAFE_RELEASE(dxgi_dev);
+ SAFE_RELEASE(dev);
+ return success;
+}
+
+static HRESULT create_swapchain_1_2(ID3D11Device *dev, IDXGIFactory2 *factory,
+ struct mp_log *log,
+ struct d3d11_swapchain_opts *opts,
+ bool flip, DXGI_FORMAT format,
+ IDXGISwapChain **swapchain_out)
+{
+ IDXGISwapChain *swapchain = NULL;
+ IDXGISwapChain1 *swapchain1 = NULL;
+ HRESULT hr;
+
+ DXGI_SWAP_CHAIN_DESC1 desc = {
+ .Width = opts->width ? opts->width : 1,
+ .Height = opts->height ? opts->height : 1,
+ .Format = format,
+ .SampleDesc = { .Count = 1 },
+ .BufferUsage = opts->usage,
+ };
+
+ if (flip) {
+ // UNORDERED_ACCESS with FLIP_SEQUENTIAL seems to be buggy with
+ // Windows 7 drivers
+ if ((desc.BufferUsage & DXGI_USAGE_UNORDERED_ACCESS) &&
+ !IsWindows8OrGreater())
+ {
+ mp_verbose(log, "Disabling UNORDERED_ACCESS for flip-model "
+ "swapchain backbuffers in Windows 7\n");
+ desc.BufferUsage &= ~DXGI_USAGE_UNORDERED_ACCESS;
+ }
+
+ if (IsWindows10OrGreater()) {
+ desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
+ } else {
+ desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
+ }
+ desc.BufferCount = opts->length;
+ } else {
+ desc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
+ desc.BufferCount = 1;
+ }
+
+ hr = IDXGIFactory2_CreateSwapChainForHwnd(factory, (IUnknown*)dev,
+ opts->window, &desc, NULL, NULL, &swapchain1);
+ if (FAILED(hr))
+ goto done;
+ hr = IDXGISwapChain1_QueryInterface(swapchain1, &IID_IDXGISwapChain,
+ (void**)&swapchain);
+ if (FAILED(hr))
+ goto done;
+
+ *swapchain_out = swapchain;
+ swapchain = NULL;
+
+done:
+ SAFE_RELEASE(swapchain1);
+ SAFE_RELEASE(swapchain);
+ return hr;
+}
+
+static HRESULT create_swapchain_1_1(ID3D11Device *dev, IDXGIFactory1 *factory,
+ struct mp_log *log,
+ struct d3d11_swapchain_opts *opts,
+ DXGI_FORMAT format,
+ IDXGISwapChain **swapchain_out)
+{
+ DXGI_SWAP_CHAIN_DESC desc = {
+ .BufferDesc = {
+ .Width = opts->width ? opts->width : 1,
+ .Height = opts->height ? opts->height : 1,
+ .Format = format,
+ },
+ .SampleDesc = { .Count = 1 },
+ .BufferUsage = opts->usage,
+ .BufferCount = 1,
+ .OutputWindow = opts->window,
+ .Windowed = TRUE,
+ .SwapEffect = DXGI_SWAP_EFFECT_DISCARD,
+ };
+
+ return IDXGIFactory1_CreateSwapChain(factory, (IUnknown*)dev, &desc,
+ swapchain_out);
+}
+
+static bool update_swapchain_format(struct mp_log *log,
+ IDXGISwapChain *swapchain,
+ DXGI_FORMAT format)
+{
+ DXGI_SWAP_CHAIN_DESC desc;
+
+ HRESULT hr = IDXGISwapChain_GetDesc(swapchain, &desc);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to query swap chain's current state: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ hr = IDXGISwapChain_ResizeBuffers(swapchain, 0, desc.BufferDesc.Width,
+ desc.BufferDesc.Height,
+ format, 0);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Couldn't update swapchain format: %s\n",
+ mp_HRESULT_to_str(hr));
+ return false;
+ }
+
+ return true;
+}
+
+static bool update_swapchain_color_space(struct mp_log *log,
+ IDXGISwapChain *swapchain,
+ DXGI_COLOR_SPACE_TYPE color_space)
+{
+ IDXGISwapChain4 *swapchain4 = NULL;
+ const char *csp_name = d3d11_get_csp_name(color_space);
+ bool success = false;
+ HRESULT hr = E_FAIL;
+ unsigned int csp_support_flags;
+
+ hr = IDXGISwapChain_QueryInterface(swapchain, &IID_IDXGISwapChain4,
+ (void *)&(swapchain4));
+ if (FAILED(hr)) {
+ mp_err(log, "Failed to create v4 swapchain for color space "
+ "configuration (%s)!\n",
+ mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ hr = IDXGISwapChain4_CheckColorSpaceSupport(swapchain4,
+ color_space,
+ &csp_support_flags);
+ if (FAILED(hr)) {
+ mp_err(log, "Failed to check color space support for color space "
+ "%s (%d): %s!\n",
+ csp_name, color_space, mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ mp_verbose(log,
+ "Swapchain capabilities for color space %s (%d): "
+ "normal: %s, overlay: %s\n",
+ csp_name, color_space,
+ (csp_support_flags & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT) ?
+ "yes" : "no",
+ (csp_support_flags & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_OVERLAY_PRESENT) ?
+ "yes" : "no");
+
+ if (!(csp_support_flags & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT)) {
+ mp_err(log, "Color space %s (%d) is not supported by this swapchain!\n",
+ csp_name, color_space);
+ goto done;
+ }
+
+ hr = IDXGISwapChain4_SetColorSpace1(swapchain4, color_space);
+ if (FAILED(hr)) {
+ mp_err(log, "Failed to set color space %s (%d) for this swapchain "
+ "(%s)!\n",
+ csp_name, color_space, mp_HRESULT_to_str(hr));
+ goto done;
+ }
+
+ mp_verbose(log, "Swapchain successfully configured to color space %s (%d)!\n",
+ csp_name, color_space);
+
+ success = true;
+
+done:
+ SAFE_RELEASE(swapchain4);
+ return success;
+}
+
+static bool configure_created_swapchain(struct mp_log *log,
+ IDXGISwapChain *swapchain,
+ DXGI_FORMAT requested_format,
+ DXGI_COLOR_SPACE_TYPE requested_csp,
+ struct mp_colorspace *configured_csp)
+{
+ DXGI_FORMAT probed_format = DXGI_FORMAT_UNKNOWN;
+ DXGI_FORMAT selected_format = DXGI_FORMAT_UNKNOWN;
+ DXGI_COLOR_SPACE_TYPE probed_colorspace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
+ DXGI_COLOR_SPACE_TYPE selected_colorspace;
+ const char *format_name = NULL;
+ const char *csp_name = NULL;
+ struct mp_colorspace mp_csp = { 0 };
+ bool mp_csp_mapped = false;
+
+ query_output_format_and_colorspace(log, swapchain,
+ &probed_format,
+ &probed_colorspace);
+
+
+ selected_format = requested_format != DXGI_FORMAT_UNKNOWN ?
+ requested_format :
+ (probed_format != DXGI_FORMAT_UNKNOWN ?
+ probed_format : DXGI_FORMAT_R8G8B8A8_UNORM);
+ selected_colorspace = requested_csp != -1 ?
+ requested_csp : probed_colorspace;
+ format_name = d3d11_get_format_name(selected_format);
+ csp_name = d3d11_get_csp_name(selected_colorspace);
+ mp_csp_mapped = d3d11_get_mp_csp(selected_colorspace, &mp_csp);
+
+ mp_verbose(log, "Selected swapchain format %s (%d), attempting "
+ "to utilize it.\n",
+ format_name, selected_format);
+
+ if (!update_swapchain_format(log, swapchain, selected_format)) {
+ return false;
+ }
+
+ if (!IsWindows10OrGreater()) {
+ // On older than Windows 10, query_output_format_and_colorspace
+ // will not change probed_colorspace, and even if a user sets
+ // a colorspace it will not get applied. Thus warn user in case a
+ // value was specifically set and finish.
+ if (requested_csp != -1) {
+ mp_warn(log, "User selected a D3D11 color space %s (%d), "
+ "but configuration of color spaces is only supported"
+ "from Windows 10! The default configuration has been "
+ "left as-is.\n",
+ csp_name, selected_colorspace);
+ }
+
+ return true;
+ }
+
+ if (!mp_csp_mapped) {
+ mp_warn(log, "Color space %s (%d) does not have an mpv color space "
+ "mapping! Overriding to standard sRGB!\n",
+ csp_name, selected_colorspace);
+ selected_colorspace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
+ d3d11_get_mp_csp(selected_colorspace, &mp_csp);
+ }
+
+ mp_verbose(log, "Selected swapchain color space %s (%d), attempting to "
+ "utilize it.\n",
+ csp_name, selected_colorspace);
+
+ if (!update_swapchain_color_space(log, swapchain, selected_colorspace)) {
+ return false;
+ }
+
+ if (configured_csp) {
+ *configured_csp = mp_csp;
+ }
+
+ return true;
+}
+
+// Create a Direct3D 11 swapchain
+bool mp_d3d11_create_swapchain(ID3D11Device *dev, struct mp_log *log,
+ struct d3d11_swapchain_opts *opts,
+ IDXGISwapChain **swapchain_out)
+{
+ IDXGIDevice1 *dxgi_dev = NULL;
+ IDXGIAdapter1 *adapter = NULL;
+ IDXGIFactory1 *factory = NULL;
+ IDXGIFactory2 *factory2 = NULL;
+ IDXGISwapChain *swapchain = NULL;
+ bool success = false;
+ HRESULT hr;
+
+ hr = ID3D11Device_QueryInterface(dev, &IID_IDXGIDevice1, (void**)&dxgi_dev);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to get DXGI device\n");
+ goto done;
+ }
+ hr = IDXGIDevice1_GetParent(dxgi_dev, &IID_IDXGIAdapter1, (void**)&adapter);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to get DXGI adapter\n");
+ goto done;
+ }
+ hr = IDXGIAdapter1_GetParent(adapter, &IID_IDXGIFactory1, (void**)&factory);
+ if (FAILED(hr)) {
+ mp_fatal(log, "Failed to get DXGI factory\n");
+ goto done;
+ }
+ hr = IDXGIFactory1_QueryInterface(factory, &IID_IDXGIFactory2,
+ (void**)&factory2);
+ if (FAILED(hr))
+ factory2 = NULL;
+
+ bool flip = factory2 && opts->flip;
+
+ // Return here to retry creating the swapchain
+ do {
+ if (factory2) {
+ // Create a DXGI 1.2+ (Windows 8+) swap chain if possible
+ hr = create_swapchain_1_2(dev, factory2, log, opts, flip,
+ DXGI_FORMAT_R8G8B8A8_UNORM, &swapchain);
+ } else {
+ // Fall back to DXGI 1.1 (Windows 7)
+ hr = create_swapchain_1_1(dev, factory, log, opts,
+ DXGI_FORMAT_R8G8B8A8_UNORM, &swapchain);
+ }
+ if (SUCCEEDED(hr))
+ break;
+
+ if (flip) {
+ mp_dbg(log, "Failed to create flip-model swapchain, trying bitblt\n");
+ flip = false;
+ continue;
+ }
+
+ mp_fatal(log, "Failed to create swapchain: %s\n", mp_HRESULT_to_str(hr));
+ goto done;
+ } while (true);
+
+ // Prevent DXGI from making changes to the VO window, otherwise it will
+ // hook the Alt+Enter keystroke and make it trigger an ugly transition to
+ // exclusive fullscreen mode instead of running the user-set command.
+ IDXGIFactory_MakeWindowAssociation(factory, opts->window,
+ DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER |
+ DXGI_MWA_NO_PRINT_SCREEN);
+
+ if (factory2) {
+ mp_verbose(log, "Using DXGI 1.2+\n");
+ } else {
+ mp_verbose(log, "Using DXGI 1.1\n");
+ }
+
+ configure_created_swapchain(log, swapchain, opts->format,
+ opts->color_space,
+ opts->configured_csp);
+
+ DXGI_SWAP_CHAIN_DESC scd = {0};
+ IDXGISwapChain_GetDesc(swapchain, &scd);
+ if (scd.SwapEffect == DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL ||
+ scd.SwapEffect == DXGI_SWAP_EFFECT_FLIP_DISCARD)
+ {
+ mp_verbose(log, "Using flip-model presentation\n");
+ } else {
+ mp_verbose(log, "Using bitblt-model presentation\n");
+ }
+
+ *swapchain_out = swapchain;
+ swapchain = NULL;
+ success = true;
+
+done:
+ SAFE_RELEASE(swapchain);
+ SAFE_RELEASE(factory2);
+ SAFE_RELEASE(factory);
+ SAFE_RELEASE(adapter);
+ SAFE_RELEASE(dxgi_dev);
+ return success;
+}
diff --git a/video/out/gpu/d3d11_helpers.h b/video/out/gpu/d3d11_helpers.h
new file mode 100644
index 0000000..c115d33
--- /dev/null
+++ b/video/out/gpu/d3d11_helpers.h
@@ -0,0 +1,103 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MP_D3D11_HELPERS_H_
+#define MP_D3D11_HELPERS_H_
+
+#include <stdbool.h>
+#include <windows.h>
+#include <d3d11.h>
+#include <dxgi1_2.h>
+
+#include "video/mp_image.h"
+
+#define D3D_FEATURE_LEVEL_12_0 (0xc000)
+#define D3D_FEATURE_LEVEL_12_1 (0xc100)
+
+#define DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P709 ((DXGI_COLOR_SPACE_TYPE)20)
+#define DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P2020 ((DXGI_COLOR_SPACE_TYPE)21)
+#define DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P709 ((DXGI_COLOR_SPACE_TYPE)22)
+#define DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P2020 ((DXGI_COLOR_SPACE_TYPE)23)
+#define DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_TOPLEFT_P2020 ((DXGI_COLOR_SPACE_TYPE)24)
+
+struct d3d11_device_opts {
+ // Enable the debug layer (D3D11_CREATE_DEVICE_DEBUG)
+ bool debug;
+
+ // Allow a software (WARP) adapter. Note, sometimes a software adapter will
+ // be used even when allow_warp is false. This is because, on Windows 8 and
+ // up, if there are no hardware adapters, Windows will pretend the WARP
+ // adapter is the primary hardware adapter.
+ bool allow_warp;
+
+ // Always use a WARP adapter. This is mainly for testing purposes.
+ bool force_warp;
+
+ // The maximum number of pending frames allowed to be queued to a swapchain
+ int max_frame_latency;
+
+ // The maximum Direct3D 11 feature level to attempt to create
+ // If unset, defaults to D3D_FEATURE_LEVEL_11_0
+ int max_feature_level;
+
+ // The minimum Direct3D 11 feature level to attempt to create. If this is
+ // not supported, device creation will fail.
+ // If unset, defaults to D3D_FEATURE_LEVEL_9_1
+ int min_feature_level;
+
+ // The adapter name to utilize if a specific adapter is required
+ // If unset, the default adapter will be utilized when creating
+ // a device.
+ char *adapter_name;
+};
+
+bool mp_d3d11_list_or_verify_adapters(struct mp_log *log,
+ bstr adapter_name,
+ bstr *listing);
+
+bool mp_d3d11_create_present_device(struct mp_log *log,
+ struct d3d11_device_opts *opts,
+ ID3D11Device **dev_out);
+
+struct d3d11_swapchain_opts {
+ HWND window;
+ int width;
+ int height;
+ DXGI_FORMAT format;
+ DXGI_COLOR_SPACE_TYPE color_space;
+
+ // mp_colorspace mapping of the configured swapchain colorspace
+ // shall be written into this memory location if configuration
+ // succeeds. Will be ignored if NULL.
+ struct mp_colorspace *configured_csp;
+
+ // Use DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL if possible
+ bool flip;
+
+ // Number of surfaces in the swapchain
+ int length;
+
+ // The BufferUsage value for swapchain surfaces. This should probably
+ // contain DXGI_USAGE_RENDER_TARGET_OUTPUT.
+ DXGI_USAGE usage;
+};
+
+bool mp_d3d11_create_swapchain(ID3D11Device *dev, struct mp_log *log,
+ struct d3d11_swapchain_opts *opts,
+ IDXGISwapChain **swapchain_out);
+
+#endif
diff --git a/video/out/gpu/error_diffusion.c b/video/out/gpu/error_diffusion.c
new file mode 100644
index 0000000..c1ea542
--- /dev/null
+++ b/video/out/gpu/error_diffusion.c
@@ -0,0 +1,316 @@
+/*
+ * 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 <stdlib.h>
+
+#include "error_diffusion.h"
+
+#include "common/common.h"
+
+#define GLSL(...) gl_sc_addf(sc, __VA_ARGS__)
+#define GLSLH(...) gl_sc_haddf(sc, __VA_ARGS__)
+
+// After a (y, x) -> (y, x + y * shift) mapping, find the right most column that
+// will be affected by the current column.
+static int compute_rightmost_shifted_column(const struct error_diffusion_kernel *k)
+{
+ int ret = 0;
+ for (int y = 0; y <= EF_MAX_DELTA_Y; y++) {
+ for (int x = EF_MIN_DELTA_X; x <= EF_MAX_DELTA_X; x++) {
+ if (k->pattern[y][x - EF_MIN_DELTA_X] != 0) {
+ int shifted_x = x + y * k->shift;
+
+ // The shift mapping guarantees current column (or left of it)
+ // won't be affected by error diffusion.
+ assert(shifted_x > 0);
+
+ ret = MPMAX(ret, shifted_x);
+ }
+ }
+ }
+ return ret;
+}
+
+const struct error_diffusion_kernel *mp_find_error_diffusion_kernel(const char *name)
+{
+ if (!name)
+ return NULL;
+ for (const struct error_diffusion_kernel *k = mp_error_diffusion_kernels;
+ k->name;
+ k++) {
+ if (strcmp(k->name, name) == 0)
+ return k;
+ }
+ return NULL;
+}
+
+int mp_ef_compute_shared_memory_size(const struct error_diffusion_kernel *k,
+ int height)
+{
+ // We add EF_MAX_DELTA_Y empty lines on the bottom to handle errors
+ // propagated out from bottom side.
+ int rows = height + EF_MAX_DELTA_Y;
+ int shifted_columns = compute_rightmost_shifted_column(k) + 1;
+
+ // The shared memory is an array of size rows*shifted_columns. Each element
+ // is a single uint for three RGB component.
+ return rows * shifted_columns * 4;
+}
+
+void pass_error_diffusion(struct gl_shader_cache *sc,
+ const struct error_diffusion_kernel *k,
+ int tex, int width, int height, int depth, int block_size)
+{
+ assert(block_size <= height);
+
+ // The parallel error diffusion works by applying the shift mapping first.
+ // Taking the Floyd and Steinberg algorithm for example. After applying
+ // the (y, x) -> (y, x + y * shift) mapping (with shift=2), all errors are
+ // propagated into the next few columns, which makes parallel processing on
+ // the same column possible.
+ //
+ // X 7/16 X 7/16
+ // 3/16 5/16 1/16 ==> 0 0 3/16 5/16 1/16
+
+ // Figuring out the size of rectangle containing all shifted pixels.
+ // The rectangle height is not changed.
+ int shifted_width = width + (height - 1) * k->shift;
+
+ // We process all pixels from the shifted rectangles column by column, with
+ // a single global work group of size |block_size|.
+ // Figuring out how many block are required to process all pixels. We need
+ // this explicitly to make the number of barrier() calls match.
+ int blocks = (height * shifted_width + block_size - 1) / block_size;
+
+ // If we figure out how many of the next columns will be affected while the
+ // current columns is being processed. We can store errors of only a few
+ // columns in the shared memory. Using a ring buffer will further save the
+ // cost while iterating to next column.
+ int ring_buffer_rows = height + EF_MAX_DELTA_Y;
+ int ring_buffer_columns = compute_rightmost_shifted_column(k) + 1;
+ int ring_buffer_size = ring_buffer_rows * ring_buffer_columns;
+
+ // Defines the ring buffer in shared memory.
+ GLSLH("shared uint err_rgb8[%d];\n", ring_buffer_size);
+
+ // Initialize the ring buffer.
+ GLSL("for (int i = int(gl_LocalInvocationIndex); i < %d; i += %d) ",
+ ring_buffer_size, block_size);
+ GLSL("err_rgb8[i] = 0u;\n");
+
+ GLSL("for (int block_id = 0; block_id < %d; ++block_id) {\n", blocks);
+
+ // Add barrier here to have previous block all processed before starting
+ // the processing of the next.
+ GLSL("groupMemoryBarrier();\n");
+ GLSL("barrier();\n");
+
+ // Compute the coordinate of the pixel we are currently processing, both
+ // before and after the shift mapping.
+ GLSL("int id = int(gl_LocalInvocationIndex) + block_id * %d;\n", block_size);
+ GLSL("int y = id %% %d, x_shifted = id / %d;\n", height, height);
+ GLSL("int x = x_shifted - y * %d;\n", k->shift);
+
+ // Proceed only if we are processing a valid pixel.
+ GLSL("if (0 <= x && x < %d) {\n", width);
+
+ // The index that the current pixel have on the ring buffer.
+ GLSL("int idx = (x_shifted * %d + y) %% %d;\n", ring_buffer_rows, ring_buffer_size);
+
+ // Fetch the current pixel.
+ GLSL("vec3 pix = texelFetch(texture%d, ivec2(x, y), 0).rgb;\n", tex);
+
+ // The dithering will quantize pixel value into multiples of 1/dither_quant.
+ int dither_quant = (1 << depth) - 1;
+
+ // We encode errors in RGB components into a single 32-bit unsigned integer.
+ // The error we propagate from the current pixel is in range of
+ // [-0.5 / dither_quant, 0.5 / dither_quant]. While not quite obvious, the
+ // sum of all errors been propagated into a pixel is also in the same range.
+ // It's possible to map errors in this range into [-127, 127], and use an
+ // unsigned 8-bit integer to store it (using standard two's complement).
+ // The three 8-bit unsigned integers can then be encoded into a single
+ // 32-bit unsigned integer, with two 4-bit padding to prevent addition
+ // operation overflows affecting other component. There are at most 12
+ // addition operations on each pixel, so 4-bit padding should be enough.
+ // The overflow from R component will be discarded.
+ //
+ // The following figure is how the encoding looks like.
+ //
+ // +------------------------------------+
+ // |RRRRRRRR|0000|GGGGGGGG|0000|BBBBBBBB|
+ // +------------------------------------+
+ //
+
+ // The bitshift position for R and G component.
+ int bitshift_r = 24, bitshift_g = 12;
+ // The multiplier we use to map [-0.5, 0.5] to [-127, 127].
+ int uint8_mul = 127 * 2;
+
+ // Adding the error previously propagated into current pixel, and clear it
+ // in the buffer.
+ GLSL("uint err_u32 = err_rgb8[idx] + %uu;\n",
+ (128u << bitshift_r) | (128u << bitshift_g) | 128u);
+ GLSL("pix = pix * %d.0 + vec3("
+ "int((err_u32 >> %d) & 255u) - 128,"
+ "int((err_u32 >> %d) & 255u) - 128,"
+ "int( err_u32 & 255u) - 128"
+ ") / %d.0;\n", dither_quant, bitshift_r, bitshift_g, uint8_mul);
+ GLSL("err_rgb8[idx] = 0u;\n");
+
+ // Write the dithered pixel.
+ GLSL("vec3 dithered = round(pix);\n");
+ GLSL("imageStore(out_image, ivec2(x, y), vec4(dithered / %d.0, 0.0));\n",
+ dither_quant);
+
+ GLSL("vec3 err_divided = (pix - dithered) * %d.0 / %d.0;\n",
+ uint8_mul, k->divisor);
+ GLSL("ivec3 tmp;\n");
+
+ // Group error propagation with same weight factor together, in order to
+ // reduce the number of annoying error encoding.
+ for (int dividend = 1; dividend <= k->divisor; dividend++) {
+ bool err_assigned = false;
+
+ for (int y = 0; y <= EF_MAX_DELTA_Y; y++) {
+ for (int x = EF_MIN_DELTA_X; x <= EF_MAX_DELTA_X; x++) {
+ if (k->pattern[y][x - EF_MIN_DELTA_X] != dividend)
+ continue;
+
+ if (!err_assigned) {
+ err_assigned = true;
+
+ GLSL("tmp = ivec3(round(err_divided * %d.0));\n", dividend);
+
+ GLSL("err_u32 = "
+ "(uint(tmp.r & 255) << %d)|"
+ "(uint(tmp.g & 255) << %d)|"
+ " uint(tmp.b & 255);\n",
+ bitshift_r, bitshift_g);
+ }
+
+ int shifted_x = x + y * k->shift;
+
+ // Unlike the right border, errors propagated out from left
+ // border will remain in the ring buffer. This will produce
+ // visible artifacts near the left border, especially for
+ // shift=3 kernels.
+ if (x < 0)
+ GLSL("if (x >= %d) ", -x);
+
+ // Calculate the new position in the ring buffer to propagate
+ // the error into.
+ int ring_buffer_delta = shifted_x * ring_buffer_rows + y;
+ GLSL("atomicAdd(err_rgb8[(idx + %d) %% %d], err_u32);\n",
+ ring_buffer_delta, ring_buffer_size);
+ }
+ }
+ }
+
+ GLSL("}\n"); // if (0 <= x && x < width)
+
+ GLSL("}\n"); // block_id
+}
+
+// Different kernels for error diffusion.
+// Patterns are from http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT
+const struct error_diffusion_kernel mp_error_diffusion_kernels[] = {
+ {
+ .name = "simple",
+ .shift = 1,
+ .pattern = {{0, 0, 0, 1, 0},
+ {0, 0, 1, 0, 0},
+ {0, 0, 0, 0, 0}},
+ .divisor = 2
+ },
+ {
+ // The "false" Floyd-Steinberg kernel
+ .name = "false-fs",
+ .shift = 1,
+ .pattern = {{0, 0, 0, 3, 0},
+ {0, 0, 3, 2, 0},
+ {0, 0, 0, 0, 0}},
+ .divisor = 8
+ },
+ {
+ .name = "sierra-lite",
+ .shift = 2,
+ .pattern = {{0, 0, 0, 2, 0},
+ {0, 1, 1, 0, 0},
+ {0, 0, 0, 0, 0}},
+ .divisor = 4
+ },
+ {
+ .name = "floyd-steinberg",
+ .shift = 2,
+ .pattern = {{0, 0, 0, 7, 0},
+ {0, 3, 5, 1, 0},
+ {0, 0, 0, 0, 0}},
+ .divisor = 16
+ },
+ {
+ .name = "atkinson",
+ .shift = 2,
+ .pattern = {{0, 0, 0, 1, 1},
+ {0, 1, 1, 1, 0},
+ {0, 0, 1, 0, 0}},
+ .divisor = 8
+ },
+ // All kernels below have shift value of 3, and probably are too heavy for
+ // low end GPU.
+ {
+ .name = "jarvis-judice-ninke",
+ .shift = 3,
+ .pattern = {{0, 0, 0, 7, 5},
+ {3, 5, 7, 5, 3},
+ {1, 3, 5, 3, 1}},
+ .divisor = 48
+ },
+ {
+ .name = "stucki",
+ .shift = 3,
+ .pattern = {{0, 0, 0, 8, 4},
+ {2, 4, 8, 4, 2},
+ {1, 2, 4, 2, 1}},
+ .divisor = 42
+ },
+ {
+ .name = "burkes",
+ .shift = 3,
+ .pattern = {{0, 0, 0, 8, 4},
+ {2, 4, 8, 4, 2},
+ {0, 0, 0, 0, 0}},
+ .divisor = 32
+ },
+ {
+ .name = "sierra-3",
+ .shift = 3,
+ .pattern = {{0, 0, 0, 5, 3},
+ {2, 4, 5, 4, 2},
+ {0, 2, 3, 2, 0}},
+ .divisor = 32
+ },
+ {
+ .name = "sierra-2",
+ .shift = 3,
+ .pattern = {{0, 0, 0, 4, 3},
+ {1, 2, 3, 2, 1},
+ {0, 0, 0, 0, 0}},
+ .divisor = 16
+ },
+ {0}
+};
diff --git a/video/out/gpu/error_diffusion.h b/video/out/gpu/error_diffusion.h
new file mode 100644
index 0000000..6bdcea1
--- /dev/null
+++ b/video/out/gpu/error_diffusion.h
@@ -0,0 +1,48 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MP_GL_ERROR_DIFFUSION
+#define MP_GL_ERROR_DIFFUSION
+
+#include "shader_cache.h"
+
+// defines the border of all error diffusion kernels
+#define EF_MIN_DELTA_X (-2)
+#define EF_MAX_DELTA_X (2)
+#define EF_MAX_DELTA_Y (2)
+
+struct error_diffusion_kernel {
+ const char *name;
+
+ // The minimum value such that a (y, x) -> (y, x + y * shift) mapping will
+ // make all error pushing operations affect next column (and after it) only.
+ int shift;
+
+ // The diffusion factor for (y, x) is pattern[y][x - EF_MIN_DELTA_X] / divisor.
+ int pattern[EF_MAX_DELTA_Y + 1][EF_MAX_DELTA_X - EF_MIN_DELTA_X + 1];
+ int divisor;
+};
+
+extern const struct error_diffusion_kernel mp_error_diffusion_kernels[];
+
+const struct error_diffusion_kernel *mp_find_error_diffusion_kernel(const char *name);
+int mp_ef_compute_shared_memory_size(const struct error_diffusion_kernel *k, int height);
+void pass_error_diffusion(struct gl_shader_cache *sc,
+ const struct error_diffusion_kernel *k,
+ int tex, int width, int height, int depth, int block_size);
+
+#endif /* MP_GL_ERROR_DIFFUSION */
diff --git a/video/out/gpu/hwdec.c b/video/out/gpu/hwdec.c
new file mode 100644
index 0000000..c8098f3
--- /dev/null
+++ b/video/out/gpu/hwdec.c
@@ -0,0 +1,358 @@
+/*
+ * 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 <stddef.h>
+#include <string.h>
+
+#include "config.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "hwdec.h"
+
+extern const struct ra_hwdec_driver ra_hwdec_vaapi;
+extern const struct ra_hwdec_driver ra_hwdec_videotoolbox;
+extern const struct ra_hwdec_driver ra_hwdec_vdpau;
+extern const struct ra_hwdec_driver ra_hwdec_dxva2egl;
+extern const struct ra_hwdec_driver ra_hwdec_d3d11egl;
+extern const struct ra_hwdec_driver ra_hwdec_dxva2gldx;
+extern const struct ra_hwdec_driver ra_hwdec_d3d11va;
+extern const struct ra_hwdec_driver ra_hwdec_dxva2dxgi;
+extern const struct ra_hwdec_driver ra_hwdec_cuda;
+extern const struct ra_hwdec_driver ra_hwdec_rpi_overlay;
+extern const struct ra_hwdec_driver ra_hwdec_drmprime;
+extern const struct ra_hwdec_driver ra_hwdec_drmprime_overlay;
+extern const struct ra_hwdec_driver ra_hwdec_aimagereader;
+extern const struct ra_hwdec_driver ra_hwdec_vulkan;
+
+const struct ra_hwdec_driver *const ra_hwdec_drivers[] = {
+#if HAVE_VAAPI
+ &ra_hwdec_vaapi,
+#endif
+#if HAVE_VIDEOTOOLBOX_GL || HAVE_IOS_GL || HAVE_VIDEOTOOLBOX_PL
+ &ra_hwdec_videotoolbox,
+#endif
+#if HAVE_D3D_HWACCEL
+ #if HAVE_EGL_ANGLE
+ &ra_hwdec_d3d11egl,
+ #if HAVE_D3D9_HWACCEL
+ &ra_hwdec_dxva2egl,
+ #endif
+ #endif
+ #if HAVE_D3D11
+ &ra_hwdec_d3d11va,
+ #if HAVE_D3D9_HWACCEL
+ &ra_hwdec_dxva2dxgi,
+ #endif
+ #endif
+#endif
+#if HAVE_GL_DXINTEROP_D3D9
+ &ra_hwdec_dxva2gldx,
+#endif
+#if HAVE_CUDA_INTEROP
+ &ra_hwdec_cuda,
+#endif
+#if HAVE_VDPAU_GL_X11
+ &ra_hwdec_vdpau,
+#endif
+#if HAVE_RPI_MMAL
+ &ra_hwdec_rpi_overlay,
+#endif
+#if HAVE_DRM
+ &ra_hwdec_drmprime,
+ &ra_hwdec_drmprime_overlay,
+#endif
+#if HAVE_ANDROID_MEDIA_NDK
+ &ra_hwdec_aimagereader,
+#endif
+#if HAVE_VULKAN_INTEROP
+ &ra_hwdec_vulkan,
+#endif
+
+ NULL
+};
+
+struct ra_hwdec *ra_hwdec_load_driver(struct ra_ctx *ra_ctx,
+ struct mp_log *log,
+ struct mpv_global *global,
+ struct mp_hwdec_devices *devs,
+ const struct ra_hwdec_driver *drv,
+ bool is_auto)
+{
+ struct ra_hwdec *hwdec = talloc(NULL, struct ra_hwdec);
+ *hwdec = (struct ra_hwdec) {
+ .driver = drv,
+ .log = mp_log_new(hwdec, log, drv->name),
+ .global = global,
+ .ra_ctx = ra_ctx,
+ .devs = devs,
+ .probing = is_auto,
+ .priv = talloc_zero_size(hwdec, drv->priv_size),
+ };
+ mp_verbose(log, "Loading hwdec driver '%s'\n", drv->name);
+ if (hwdec->driver->init(hwdec) < 0) {
+ ra_hwdec_uninit(hwdec);
+ mp_verbose(log, "Loading failed.\n");
+ return NULL;
+ }
+ return hwdec;
+}
+
+void ra_hwdec_uninit(struct ra_hwdec *hwdec)
+{
+ if (hwdec)
+ hwdec->driver->uninit(hwdec);
+ talloc_free(hwdec);
+}
+
+bool ra_hwdec_test_format(struct ra_hwdec *hwdec, int imgfmt)
+{
+ for (int n = 0; hwdec->driver->imgfmts[n]; n++) {
+ if (hwdec->driver->imgfmts[n] == imgfmt)
+ return true;
+ }
+ return false;
+}
+
+struct ra_hwdec_mapper *ra_hwdec_mapper_create(struct ra_hwdec *hwdec,
+ const struct mp_image_params *params)
+{
+ assert(ra_hwdec_test_format(hwdec, params->imgfmt));
+
+ struct ra_hwdec_mapper *mapper = talloc_ptrtype(NULL, mapper);
+ *mapper = (struct ra_hwdec_mapper){
+ .owner = hwdec,
+ .driver = hwdec->driver->mapper,
+ .log = hwdec->log,
+ .ra = hwdec->ra_ctx->ra,
+ .priv = talloc_zero_size(mapper, hwdec->driver->mapper->priv_size),
+ .src_params = *params,
+ .dst_params = *params,
+ };
+ if (mapper->driver->init(mapper) < 0)
+ ra_hwdec_mapper_free(&mapper);
+ return mapper;
+}
+
+void ra_hwdec_mapper_free(struct ra_hwdec_mapper **mapper)
+{
+ struct ra_hwdec_mapper *p = *mapper;
+ if (p) {
+ ra_hwdec_mapper_unmap(p);
+ p->driver->uninit(p);
+ talloc_free(p);
+ }
+ *mapper = NULL;
+}
+
+void ra_hwdec_mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ if (mapper->driver->unmap)
+ mapper->driver->unmap(mapper);
+
+ // Clean up after the image if the mapper didn't already
+ mp_image_unrefp(&mapper->src);
+}
+
+int ra_hwdec_mapper_map(struct ra_hwdec_mapper *mapper, struct mp_image *img)
+{
+ ra_hwdec_mapper_unmap(mapper);
+ mp_image_setrefp(&mapper->src, img);
+ if (mapper->driver->map(mapper) < 0) {
+ ra_hwdec_mapper_unmap(mapper);
+ return -1;
+ }
+ return 0;
+}
+
+static int ra_hwdec_validate_opt_full(struct mp_log *log, bool include_modes,
+ const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ bool help = bstr_equals0(param, "help");
+ if (help)
+ mp_info(log, "Available hwdecs:\n");
+ for (int n = 0; ra_hwdec_drivers[n]; n++) {
+ const struct ra_hwdec_driver *drv = ra_hwdec_drivers[n];
+ if (help) {
+ mp_info(log, " %s\n", drv->name);
+ } else if (bstr_equals0(param, drv->name)) {
+ return 1;
+ }
+ }
+ if (help) {
+ if (include_modes) {
+ mp_info(log, " auto (behavior depends on context)\n"
+ " all (load all hwdecs)\n"
+ " no (do not load any and block loading on demand)\n");
+ }
+ return M_OPT_EXIT;
+ }
+ if (!param.len)
+ return 1; // "" is treated specially
+ if (include_modes &&
+ (bstr_equals0(param, "all") || bstr_equals0(param, "auto") ||
+ bstr_equals0(param, "no")))
+ return 1;
+ mp_fatal(log, "No hwdec backend named '%.*s' found!\n", BSTR_P(param));
+ return M_OPT_INVALID;
+}
+
+int ra_hwdec_validate_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ return ra_hwdec_validate_opt_full(log, true, opt, name, value);
+}
+
+int ra_hwdec_validate_drivers_only_opt(struct mp_log *log,
+ const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ return ra_hwdec_validate_opt_full(log, false, opt, name, value);
+}
+
+static void load_add_hwdec(struct ra_hwdec_ctx *ctx, struct mp_hwdec_devices *devs,
+ const struct ra_hwdec_driver *drv, bool is_auto)
+{
+ // Don't load duplicate hwdecs
+ for (int j = 0; j < ctx->num_hwdecs; j++) {
+ if (ctx->hwdecs[j]->driver == drv)
+ return;
+ }
+
+ struct ra_hwdec *hwdec =
+ ra_hwdec_load_driver(ctx->ra_ctx, ctx->log, ctx->global, devs, drv, is_auto);
+ if (hwdec)
+ MP_TARRAY_APPEND(NULL, ctx->hwdecs, ctx->num_hwdecs, hwdec);
+}
+
+static void load_hwdecs_all(struct ra_hwdec_ctx *ctx, struct mp_hwdec_devices *devs)
+{
+ if (!ctx->loading_done) {
+ for (int n = 0; ra_hwdec_drivers[n]; n++)
+ load_add_hwdec(ctx, devs, ra_hwdec_drivers[n], true);
+ ctx->loading_done = true;
+ }
+}
+
+void ra_hwdec_ctx_init(struct ra_hwdec_ctx *ctx, struct mp_hwdec_devices *devs,
+ const char *type, bool load_all_by_default)
+{
+ assert(ctx->ra_ctx);
+
+ /*
+ * By default, or if the option value is "auto", we will not pre-emptively
+ * load any interops, and instead allow them to be loaded on-demand.
+ *
+ * If the option value is "no", then no interops will be loaded now, and
+ * no interops will be loaded, even if requested later.
+ *
+ * If the option value is "all", then all interops will be loaded now, and
+ * obviously no interops will need to be loaded later.
+ *
+ * Finally, if a specific interop is requested, it will be loaded now, and
+ * other interops can be loaded, if requested later.
+ */
+ if (!type || !type[0] || strcmp(type, "auto") == 0) {
+ if (!load_all_by_default)
+ return;
+ type = "all";
+ }
+ if (strcmp(type, "no") == 0) {
+ // do nothing, just block further loading
+ } else if (strcmp(type, "all") == 0) {
+ load_hwdecs_all(ctx, devs);
+ } else {
+ for (int n = 0; ra_hwdec_drivers[n]; n++) {
+ const struct ra_hwdec_driver *drv = ra_hwdec_drivers[n];
+ if (strcmp(type, drv->name) == 0) {
+ load_add_hwdec(ctx, devs, drv, false);
+ break;
+ }
+ }
+ }
+ ctx->loading_done = true;
+}
+
+void ra_hwdec_ctx_uninit(struct ra_hwdec_ctx *ctx)
+{
+ for (int n = 0; n < ctx->num_hwdecs; n++)
+ ra_hwdec_uninit(ctx->hwdecs[n]);
+
+ talloc_free(ctx->hwdecs);
+ memset(ctx, 0, sizeof(*ctx));
+}
+
+void ra_hwdec_ctx_load_fmt(struct ra_hwdec_ctx *ctx, struct mp_hwdec_devices *devs,
+ struct hwdec_imgfmt_request *params)
+{
+ int imgfmt = params->imgfmt;
+ if (ctx->loading_done) {
+ /*
+ * If we previously marked interop loading as done (for reasons
+ * discussed above), then do not load any other interops regardless
+ * of imgfmt.
+ */
+ return;
+ }
+
+ if (imgfmt == IMGFMT_NONE) {
+ MP_VERBOSE(ctx, "Loading hwdec drivers for all formats\n");
+ load_hwdecs_all(ctx, devs);
+ return;
+ }
+
+ MP_VERBOSE(ctx, "Loading hwdec drivers for format: '%s'\n",
+ mp_imgfmt_to_name(imgfmt));
+ for (int i = 0; ra_hwdec_drivers[i]; i++) {
+ bool matched_fmt = false;
+ const struct ra_hwdec_driver *drv = ra_hwdec_drivers[i];
+ for (int j = 0; drv->imgfmts[j]; j++) {
+ if (imgfmt == drv->imgfmts[j]) {
+ matched_fmt = true;
+ break;
+ }
+ }
+ if (!matched_fmt) {
+ continue;
+ }
+
+ load_add_hwdec(ctx, devs, drv, params->probing);
+ }
+}
+
+struct ra_hwdec *ra_hwdec_get(struct ra_hwdec_ctx *ctx, int imgfmt)
+{
+ for (int n = 0; n < ctx->num_hwdecs; n++) {
+ if (ra_hwdec_test_format(ctx->hwdecs[n], imgfmt))
+ return ctx->hwdecs[n];
+ }
+
+ return NULL;
+}
+
+int ra_hwdec_driver_get_imgfmt_for_name(const char *name)
+{
+ for (int i = 0; ra_hwdec_drivers[i]; i++) {
+ if (!strcmp(ra_hwdec_drivers[i]->name, name)) {
+ return ra_hwdec_drivers[i]->imgfmts[0];
+ }
+ }
+ return IMGFMT_NONE;
+}
diff --git a/video/out/gpu/hwdec.h b/video/out/gpu/hwdec.h
new file mode 100644
index 0000000..7766073
--- /dev/null
+++ b/video/out/gpu/hwdec.h
@@ -0,0 +1,156 @@
+#ifndef MPGL_HWDEC_H_
+#define MPGL_HWDEC_H_
+
+#include "video/mp_image.h"
+#include "context.h"
+#include "ra.h"
+#include "video/hwdec.h"
+
+// Helper to organize/load hwdecs dynamically
+struct ra_hwdec_ctx {
+ // Set these before calling `ra_hwdec_ctx_init`
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct ra_ctx *ra_ctx;
+
+ bool loading_done;
+ struct ra_hwdec **hwdecs;
+ int num_hwdecs;
+};
+
+int ra_hwdec_validate_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value);
+
+int ra_hwdec_validate_drivers_only_opt(struct mp_log *log,
+ const m_option_t *opt,
+ struct bstr name, const char **value);
+
+void ra_hwdec_ctx_init(struct ra_hwdec_ctx *ctx, struct mp_hwdec_devices *devs,
+ const char *opt, bool load_all_by_default);
+void ra_hwdec_ctx_uninit(struct ra_hwdec_ctx *ctx);
+
+void ra_hwdec_ctx_load_fmt(struct ra_hwdec_ctx *ctx, struct mp_hwdec_devices *devs,
+ struct hwdec_imgfmt_request *params);
+
+// Gets the right `ra_hwdec` for a format, if any
+struct ra_hwdec *ra_hwdec_get(struct ra_hwdec_ctx *ctx, int imgfmt);
+
+struct ra_hwdec {
+ const struct ra_hwdec_driver *driver;
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct ra_ctx *ra_ctx;
+ struct mp_hwdec_devices *devs;
+ // GLSL extensions required to sample textures from this.
+ const char **glsl_extensions;
+ // For free use by hwdec driver
+ void *priv;
+ // For working around the vdpau vs. vaapi mess.
+ bool probing;
+ // Used in overlay mode only.
+ float overlay_colorkey[4];
+};
+
+struct ra_hwdec_mapper {
+ const struct ra_hwdec_mapper_driver *driver;
+ struct mp_log *log;
+ struct ra *ra;
+ void *priv;
+ struct ra_hwdec *owner;
+ // Input frame parameters. (Set before init(), immutable.)
+ struct mp_image_params src_params;
+ // Output frame parameters (represents the format the textures return). Must
+ // be set by init(), immutable afterwards,
+ struct mp_image_params dst_params;
+
+ // The currently mapped source image (or the image about to be mapped in
+ // ->map()). NULL if unmapped. The mapper can also clear this reference if
+ // the mapped textures contain a full copy.
+ struct mp_image *src;
+
+ // The mapped textures and metadata about them. These fields change if a
+ // new frame is mapped (or unmapped), but otherwise remain constant.
+ // The common code won't mess with these, so you can e.g. set them in the
+ // .init() callback.
+ struct ra_tex *tex[4];
+};
+
+// This can be used to map frames of a specific hw format as GL textures.
+struct ra_hwdec_mapper_driver {
+ // Used to create ra_hwdec_mapper.priv.
+ size_t priv_size;
+
+ // Init the mapper implementation. At this point, the field src_params,
+ // fns, devs, priv are initialized.
+ int (*init)(struct ra_hwdec_mapper *mapper);
+ // Destroy the mapper. unmap is called before this.
+ void (*uninit)(struct ra_hwdec_mapper *mapper);
+
+ // Map mapper->src as texture, and set mapper->frame to textures using it.
+ // It is expected that the textures remain valid until the next unmap
+ // or uninit call.
+ // The function is allowed to unref mapper->src if it's not needed (i.e.
+ // this function creates a copy).
+ // The underlying format can change, so you might need to do some form
+ // of change detection. You also must reject unsupported formats with an
+ // error.
+ // On error, returns negative value on error and remains unmapped.
+ int (*map)(struct ra_hwdec_mapper *mapper);
+ // Unmap the frame. Does nothing if already unmapped. Optional.
+ void (*unmap)(struct ra_hwdec_mapper *mapper);
+};
+
+struct ra_hwdec_driver {
+ // Name of the interop backend. This is used for informational purposes and
+ // for use with debugging options.
+ const char *name;
+ // Used to create ra_hwdec.priv.
+ size_t priv_size;
+ // One of the hardware surface IMGFMT_ that must be passed to map_image later.
+ // Terminated with a 0 entry. (Extend the array size as needed.)
+ const int imgfmts[3];
+
+ // Create the hwdec device. It must add it to hw->devs, if applicable.
+ int (*init)(struct ra_hwdec *hw);
+ void (*uninit)(struct ra_hwdec *hw);
+
+ // This will be used to create a ra_hwdec_mapper from ra_hwdec.
+ const struct ra_hwdec_mapper_driver *mapper;
+
+ // The following function provides an alternative API. Each ra_hwdec_driver
+ // must have either provide a mapper or overlay_frame (not both or none), and
+ // if overlay_frame is set, it operates in overlay mode. In this mode,
+ // OSD etc. is rendered via OpenGL, but the video is rendered as a separate
+ // layer below it.
+ // Non-overlay mode is strictly preferred, so try not to use overlay mode.
+ // Set the given frame as overlay, replacing the previous one. This can also
+ // just change the position of the overlay.
+ // hw_image==src==dst==NULL is passed to clear the overlay.
+ int (*overlay_frame)(struct ra_hwdec *hw, struct mp_image *hw_image,
+ struct mp_rect *src, struct mp_rect *dst, bool newframe);
+};
+
+extern const struct ra_hwdec_driver *const ra_hwdec_drivers[];
+
+struct ra_hwdec *ra_hwdec_load_driver(struct ra_ctx *ra_ctx,
+ struct mp_log *log,
+ struct mpv_global *global,
+ struct mp_hwdec_devices *devs,
+ const struct ra_hwdec_driver *drv,
+ bool is_auto);
+
+void ra_hwdec_uninit(struct ra_hwdec *hwdec);
+
+bool ra_hwdec_test_format(struct ra_hwdec *hwdec, int imgfmt);
+
+struct ra_hwdec_mapper *ra_hwdec_mapper_create(struct ra_hwdec *hwdec,
+ const struct mp_image_params *params);
+void ra_hwdec_mapper_free(struct ra_hwdec_mapper **mapper);
+void ra_hwdec_mapper_unmap(struct ra_hwdec_mapper *mapper);
+int ra_hwdec_mapper_map(struct ra_hwdec_mapper *mapper, struct mp_image *img);
+
+// Get the primary image format for the given driver name.
+// Returns IMGFMT_NONE if the name doesn't get matched.
+int ra_hwdec_driver_get_imgfmt_for_name(const char *name);
+
+#endif
diff --git a/video/out/gpu/lcms.c b/video/out/gpu/lcms.c
new file mode 100644
index 0000000..7006a96
--- /dev/null
+++ b/video/out/gpu/lcms.c
@@ -0,0 +1,526 @@
+/*
+ * 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 <string.h>
+#include <math.h>
+
+#include "mpv_talloc.h"
+
+#include "config.h"
+
+#include "stream/stream.h"
+#include "common/common.h"
+#include "misc/bstr.h"
+#include "common/msg.h"
+#include "options/m_option.h"
+#include "options/path.h"
+#include "video/csputils.h"
+#include "lcms.h"
+
+#include "osdep/io.h"
+
+#if HAVE_LCMS2
+
+#include <lcms2.h>
+#include <libavutil/sha.h>
+#include <libavutil/mem.h>
+
+struct gl_lcms {
+ void *icc_data;
+ size_t icc_size;
+ struct AVBufferRef *vid_profile;
+ char *current_profile;
+ bool using_memory_profile;
+ bool changed;
+ enum mp_csp_prim current_prim;
+ enum mp_csp_trc current_trc;
+
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct mp_icc_opts *opts;
+};
+
+static void lcms2_error_handler(cmsContext ctx, cmsUInt32Number code,
+ const char *msg)
+{
+ struct gl_lcms *p = cmsGetContextUserData(ctx);
+ MP_ERR(p, "lcms2: %s\n", msg);
+}
+
+static void load_profile(struct gl_lcms *p)
+{
+ talloc_free(p->icc_data);
+ p->icc_data = NULL;
+ p->icc_size = 0;
+ p->using_memory_profile = false;
+ talloc_free(p->current_profile);
+ p->current_profile = NULL;
+
+ if (!p->opts->profile || !p->opts->profile[0])
+ return;
+
+ char *fname = mp_get_user_path(NULL, p->global, p->opts->profile);
+ MP_VERBOSE(p, "Opening ICC profile '%s'\n", fname);
+ struct bstr iccdata = stream_read_file(fname, p, p->global,
+ 100000000); // 100 MB
+ talloc_free(fname);
+ if (!iccdata.len)
+ return;
+
+ talloc_free(p->icc_data);
+
+ p->icc_data = iccdata.start;
+ p->icc_size = iccdata.len;
+ p->current_profile = talloc_strdup(p, p->opts->profile);
+}
+
+static void gl_lcms_destructor(void *ptr)
+{
+ struct gl_lcms *p = ptr;
+ av_buffer_unref(&p->vid_profile);
+}
+
+struct gl_lcms *gl_lcms_init(void *talloc_ctx, struct mp_log *log,
+ struct mpv_global *global,
+ struct mp_icc_opts *opts)
+{
+ struct gl_lcms *p = talloc_ptrtype(talloc_ctx, p);
+ talloc_set_destructor(p, gl_lcms_destructor);
+ *p = (struct gl_lcms) {
+ .global = global,
+ .log = log,
+ .opts = opts,
+ };
+ gl_lcms_update_options(p);
+ return p;
+}
+
+void gl_lcms_update_options(struct gl_lcms *p)
+{
+ if ((p->using_memory_profile && !p->opts->profile_auto) ||
+ !bstr_equals(bstr0(p->opts->profile), bstr0(p->current_profile)))
+ {
+ load_profile(p);
+ }
+
+ p->changed = true; // probably
+}
+
+// Warning: profile.start must point to a ta allocation, and the function
+// takes over ownership.
+// Returns whether the internal profile was changed.
+bool gl_lcms_set_memory_profile(struct gl_lcms *p, bstr profile)
+{
+ if (!p->opts->profile_auto || (p->opts->profile && p->opts->profile[0])) {
+ talloc_free(profile.start);
+ return false;
+ }
+
+ if (p->using_memory_profile &&
+ p->icc_data && profile.start &&
+ profile.len == p->icc_size &&
+ memcmp(profile.start, p->icc_data, p->icc_size) == 0)
+ {
+ talloc_free(profile.start);
+ return false;
+ }
+
+ p->changed = true;
+ p->using_memory_profile = true;
+
+ talloc_free(p->icc_data);
+
+ p->icc_data = talloc_steal(p, profile.start);
+ p->icc_size = profile.len;
+
+ return true;
+}
+
+// Guards against NULL and uses bstr_equals to short-circuit some special cases
+static bool vid_profile_eq(struct AVBufferRef *a, struct AVBufferRef *b)
+{
+ if (!a || !b)
+ return a == b;
+
+ return bstr_equals((struct bstr){ a->data, a->size },
+ (struct bstr){ b->data, b->size });
+}
+
+// Return whether the profile or config has changed since the last time it was
+// retrieved. If it has changed, gl_lcms_get_lut3d() should be called.
+bool gl_lcms_has_changed(struct gl_lcms *p, enum mp_csp_prim prim,
+ enum mp_csp_trc trc, struct AVBufferRef *vid_profile)
+{
+ if (p->changed || p->current_prim != prim || p->current_trc != trc)
+ return true;
+
+ return !vid_profile_eq(p->vid_profile, vid_profile);
+}
+
+// Whether a profile is set. (gl_lcms_get_lut3d() is expected to return a lut,
+// but it could still fail due to runtime errors, such as invalid icc data.)
+bool gl_lcms_has_profile(struct gl_lcms *p)
+{
+ return p->icc_size > 0;
+}
+
+static cmsHPROFILE get_vid_profile(struct gl_lcms *p, cmsContext cms,
+ cmsHPROFILE disp_profile,
+ enum mp_csp_prim prim, enum mp_csp_trc trc)
+{
+ if (p->opts->use_embedded && p->vid_profile) {
+ // Try using the embedded ICC profile
+ cmsHPROFILE prof = cmsOpenProfileFromMemTHR(cms, p->vid_profile->data,
+ p->vid_profile->size);
+ if (prof) {
+ MP_VERBOSE(p, "Successfully opened embedded ICC profile\n");
+ return prof;
+ }
+
+ // Otherwise, warn the user and generate the profile as usual
+ MP_WARN(p, "Video contained an invalid ICC profile! Ignoring...\n");
+ }
+
+ // The input profile for the transformation is dependent on the video
+ // primaries and transfer characteristics
+ struct mp_csp_primaries csp = mp_get_csp_primaries(prim);
+ cmsCIExyY wp_xyY = {csp.white.x, csp.white.y, 1.0};
+ cmsCIExyYTRIPLE prim_xyY = {
+ .Red = {csp.red.x, csp.red.y, 1.0},
+ .Green = {csp.green.x, csp.green.y, 1.0},
+ .Blue = {csp.blue.x, csp.blue.y, 1.0},
+ };
+
+ cmsToneCurve *tonecurve[3] = {0};
+ switch (trc) {
+ case MP_CSP_TRC_LINEAR: tonecurve[0] = cmsBuildGamma(cms, 1.0); break;
+ case MP_CSP_TRC_GAMMA18: tonecurve[0] = cmsBuildGamma(cms, 1.8); break;
+ case MP_CSP_TRC_GAMMA20: tonecurve[0] = cmsBuildGamma(cms, 2.0); break;
+ case MP_CSP_TRC_GAMMA22: tonecurve[0] = cmsBuildGamma(cms, 2.2); break;
+ case MP_CSP_TRC_GAMMA24: tonecurve[0] = cmsBuildGamma(cms, 2.4); break;
+ case MP_CSP_TRC_GAMMA26: tonecurve[0] = cmsBuildGamma(cms, 2.6); break;
+ case MP_CSP_TRC_GAMMA28: tonecurve[0] = cmsBuildGamma(cms, 2.8); break;
+
+ case MP_CSP_TRC_SRGB:
+ // Values copied from Little-CMS
+ tonecurve[0] = cmsBuildParametricToneCurve(cms, 4,
+ (double[5]){2.40, 1/1.055, 0.055/1.055, 1/12.92, 0.04045});
+ break;
+
+ case MP_CSP_TRC_PRO_PHOTO:
+ tonecurve[0] = cmsBuildParametricToneCurve(cms, 4,
+ (double[5]){1.8, 1.0, 0.0, 1/16.0, 0.03125});
+ break;
+
+ case MP_CSP_TRC_BT_1886: {
+ double src_black[3];
+ if (p->opts->contrast < 0) {
+ // User requested infinite contrast, return 2.4 profile
+ tonecurve[0] = cmsBuildGamma(cms, 2.4);
+ break;
+ } else if (p->opts->contrast > 0) {
+ MP_VERBOSE(p, "Using specified contrast: %d\n", p->opts->contrast);
+ for (int i = 0; i < 3; i++)
+ src_black[i] = 1.0 / p->opts->contrast;
+ } else {
+ // To build an appropriate BT.1886 transformation we need access to
+ // the display's black point, so we use LittleCMS' detection
+ // function. Relative colorimetric is used since we want to
+ // approximate the BT.1886 to the target device's actual black
+ // point even in e.g. perceptual mode
+ const int intent = MP_INTENT_RELATIVE_COLORIMETRIC;
+ cmsCIEXYZ bp_XYZ;
+ if (!cmsDetectBlackPoint(&bp_XYZ, disp_profile, intent, 0))
+ return false;
+
+ // Map this XYZ value back into the (linear) source space
+ cmsHPROFILE rev_profile;
+ cmsToneCurve *linear = cmsBuildGamma(cms, 1.0);
+ rev_profile = cmsCreateRGBProfileTHR(cms, &wp_xyY, &prim_xyY,
+ (cmsToneCurve*[3]){linear, linear, linear});
+ cmsHPROFILE xyz_profile = cmsCreateXYZProfile();
+ cmsHTRANSFORM xyz2src = cmsCreateTransformTHR(cms,
+ xyz_profile, TYPE_XYZ_DBL, rev_profile, TYPE_RGB_DBL,
+ intent, cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE);
+ cmsFreeToneCurve(linear);
+ cmsCloseProfile(rev_profile);
+ cmsCloseProfile(xyz_profile);
+ if (!xyz2src)
+ return false;
+
+ cmsDoTransform(xyz2src, &bp_XYZ, src_black, 1);
+ cmsDeleteTransform(xyz2src);
+
+ double contrast = 3.0 / (src_black[0] + src_black[1] + src_black[2]);
+ MP_VERBOSE(p, "Detected ICC profile contrast: %f\n", contrast);
+ }
+
+ // Build the parametric BT.1886 transfer curve, one per channel
+ for (int i = 0; i < 3; i++) {
+ const double gamma = 2.40;
+ double binv = pow(src_black[i], 1.0/gamma);
+ tonecurve[i] = cmsBuildParametricToneCurve(cms, 6,
+ (double[4]){gamma, 1.0 - binv, binv, 0.0});
+ }
+ break;
+ }
+
+ default:
+ abort();
+ }
+
+ if (!tonecurve[0])
+ return false;
+
+ if (!tonecurve[1]) tonecurve[1] = tonecurve[0];
+ if (!tonecurve[2]) tonecurve[2] = tonecurve[0];
+
+ cmsHPROFILE *vid_profile = cmsCreateRGBProfileTHR(cms, &wp_xyY, &prim_xyY,
+ tonecurve);
+
+ if (tonecurve[2] != tonecurve[0]) cmsFreeToneCurve(tonecurve[2]);
+ if (tonecurve[1] != tonecurve[0]) cmsFreeToneCurve(tonecurve[1]);
+ cmsFreeToneCurve(tonecurve[0]);
+
+ return vid_profile;
+}
+
+bool gl_lcms_get_lut3d(struct gl_lcms *p, struct lut3d **result_lut3d,
+ enum mp_csp_prim prim, enum mp_csp_trc trc,
+ struct AVBufferRef *vid_profile)
+{
+ int s_r, s_g, s_b;
+ bool result = false;
+
+ p->changed = false;
+ p->current_prim = prim;
+ p->current_trc = trc;
+
+ // We need to hold on to a reference to the video's ICC profile for as long
+ // as we still need to perform equality checking, so generate a new
+ // reference here
+ av_buffer_unref(&p->vid_profile);
+ if (vid_profile) {
+ MP_VERBOSE(p, "Got an embedded ICC profile.\n");
+ p->vid_profile = av_buffer_ref(vid_profile);
+ MP_HANDLE_OOM(p->vid_profile);
+ }
+
+ if (!gl_parse_3dlut_size(p->opts->size_str, &s_r, &s_g, &s_b))
+ return false;
+
+ if (!gl_lcms_has_profile(p))
+ return false;
+
+ // For simplicity, default to 65x65x65, which is large enough to cover
+ // typical profiles with good accuracy while not being too wasteful
+ s_r = s_r ? s_r : 65;
+ s_g = s_g ? s_g : 65;
+ s_b = s_b ? s_b : 65;
+
+ void *tmp = talloc_new(NULL);
+ uint16_t *output = talloc_array(tmp, uint16_t, s_r * s_g * s_b * 4);
+ struct lut3d *lut = NULL;
+ cmsContext cms = NULL;
+
+ char *cache_file = NULL;
+ if (p->opts->cache) {
+ // Gamma is included in the header to help uniquely identify it,
+ // because we may change the parameter in the future or make it
+ // customizable, same for the primaries.
+ char *cache_info = talloc_asprintf(tmp,
+ "ver=1.4, intent=%d, size=%dx%dx%d, prim=%d, trc=%d, "
+ "contrast=%d\n",
+ p->opts->intent, s_r, s_g, s_b, prim, trc, p->opts->contrast);
+
+ uint8_t hash[32];
+ struct AVSHA *sha = av_sha_alloc();
+ MP_HANDLE_OOM(sha);
+ av_sha_init(sha, 256);
+ av_sha_update(sha, cache_info, strlen(cache_info));
+ if (vid_profile)
+ av_sha_update(sha, vid_profile->data, vid_profile->size);
+ av_sha_update(sha, p->icc_data, p->icc_size);
+ av_sha_final(sha, hash);
+ av_free(sha);
+
+ char *cache_dir = p->opts->cache_dir;
+ if (cache_dir && cache_dir[0]) {
+ cache_dir = mp_get_user_path(tmp, p->global, cache_dir);
+ } else {
+ cache_dir = mp_find_user_file(tmp, p->global, "cache", "");
+ }
+
+ if (cache_dir && cache_dir[0]) {
+ cache_file = talloc_strdup(tmp, "");
+ for (int i = 0; i < sizeof(hash); i++)
+ cache_file = talloc_asprintf_append(cache_file, "%02X", hash[i]);
+ cache_file = mp_path_join(tmp, cache_dir, cache_file);
+ mp_mkdirp(cache_dir);
+ }
+ }
+
+ // check cache
+ if (cache_file && stat(cache_file, &(struct stat){0}) == 0) {
+ MP_VERBOSE(p, "Opening 3D LUT cache in file '%s'.\n", cache_file);
+ struct bstr cachedata = stream_read_file(cache_file, tmp, p->global,
+ 1000000000); // 1 GB
+ if (cachedata.len == talloc_get_size(output)) {
+ memcpy(output, cachedata.start, cachedata.len);
+ goto done;
+ } else {
+ MP_WARN(p, "3D LUT cache invalid!\n");
+ }
+ }
+
+ cms = cmsCreateContext(NULL, p);
+ if (!cms)
+ goto error_exit;
+ cmsSetLogErrorHandlerTHR(cms, lcms2_error_handler);
+
+ cmsHPROFILE profile =
+ cmsOpenProfileFromMemTHR(cms, p->icc_data, p->icc_size);
+ if (!profile)
+ goto error_exit;
+
+ cmsHPROFILE vid_hprofile = get_vid_profile(p, cms, profile, prim, trc);
+ if (!vid_hprofile) {
+ cmsCloseProfile(profile);
+ goto error_exit;
+ }
+
+ cmsHTRANSFORM trafo = cmsCreateTransformTHR(cms, vid_hprofile, TYPE_RGB_16,
+ profile, TYPE_RGBA_16,
+ p->opts->intent,
+ cmsFLAGS_NOCACHE |
+ cmsFLAGS_NOOPTIMIZE |
+ cmsFLAGS_BLACKPOINTCOMPENSATION);
+ cmsCloseProfile(profile);
+ cmsCloseProfile(vid_hprofile);
+
+ if (!trafo)
+ goto error_exit;
+
+ // transform a (s_r)x(s_g)x(s_b) cube, with 3 components per channel
+ uint16_t *input = talloc_array(tmp, uint16_t, s_r * 3);
+ for (int b = 0; b < s_b; b++) {
+ for (int g = 0; g < s_g; g++) {
+ for (int r = 0; r < s_r; r++) {
+ input[r * 3 + 0] = r * 65535 / (s_r - 1);
+ input[r * 3 + 1] = g * 65535 / (s_g - 1);
+ input[r * 3 + 2] = b * 65535 / (s_b - 1);
+ }
+ size_t base = (b * s_r * s_g + g * s_r) * 4;
+ cmsDoTransform(trafo, input, output + base, s_r);
+ }
+ }
+
+ cmsDeleteTransform(trafo);
+
+ if (cache_file) {
+ FILE *out = fopen(cache_file, "wb");
+ if (out) {
+ fwrite(output, talloc_get_size(output), 1, out);
+ fclose(out);
+ }
+ }
+
+done: ;
+
+ lut = talloc_ptrtype(NULL, lut);
+ *lut = (struct lut3d) {
+ .data = talloc_steal(lut, output),
+ .size = {s_r, s_g, s_b},
+ };
+
+ *result_lut3d = lut;
+ result = true;
+
+error_exit:
+
+ if (cms)
+ cmsDeleteContext(cms);
+
+ if (!lut)
+ MP_FATAL(p, "Error loading ICC profile.\n");
+
+ talloc_free(tmp);
+ return result;
+}
+
+#else /* HAVE_LCMS2 */
+
+struct gl_lcms *gl_lcms_init(void *talloc_ctx, struct mp_log *log,
+ struct mpv_global *global,
+ struct mp_icc_opts *opts)
+{
+ return (struct gl_lcms *) talloc_new(talloc_ctx);
+}
+
+void gl_lcms_update_options(struct gl_lcms *p) { }
+bool gl_lcms_set_memory_profile(struct gl_lcms *p, bstr profile) {return false;}
+
+bool gl_lcms_has_changed(struct gl_lcms *p, enum mp_csp_prim prim,
+ enum mp_csp_trc trc, struct AVBufferRef *vid_profile)
+{
+ return false;
+}
+
+bool gl_lcms_has_profile(struct gl_lcms *p)
+{
+ return false;
+}
+
+bool gl_lcms_get_lut3d(struct gl_lcms *p, struct lut3d **result_lut3d,
+ enum mp_csp_prim prim, enum mp_csp_trc trc,
+ struct AVBufferRef *vid_profile)
+{
+ return false;
+}
+
+#endif
+
+static int validate_3dlut_size_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ int p1, p2, p3;
+ return gl_parse_3dlut_size(*value, &p1, &p2, &p3) ? 0 : M_OPT_INVALID;
+}
+
+#define OPT_BASE_STRUCT struct mp_icc_opts
+const struct m_sub_options mp_icc_conf = {
+ .opts = (const m_option_t[]) {
+ {"use-embedded-icc-profile", OPT_BOOL(use_embedded)},
+ {"icc-profile", OPT_STRING(profile), .flags = M_OPT_FILE},
+ {"icc-profile-auto", OPT_BOOL(profile_auto)},
+ {"icc-cache", OPT_BOOL(cache)},
+ {"icc-cache-dir", OPT_STRING(cache_dir), .flags = M_OPT_FILE},
+ {"icc-intent", OPT_INT(intent)},
+ {"icc-force-contrast", OPT_CHOICE(contrast, {"no", 0}, {"inf", -1}),
+ M_RANGE(0, 1000000)},
+ {"icc-3dlut-size", OPT_STRING_VALIDATE(size_str, validate_3dlut_size_opt)},
+ {"icc-use-luma", OPT_BOOL(icc_use_luma)},
+ {0}
+ },
+ .size = sizeof(struct mp_icc_opts),
+ .defaults = &(const struct mp_icc_opts) {
+ .size_str = "auto",
+ .intent = MP_INTENT_RELATIVE_COLORIMETRIC,
+ .use_embedded = true,
+ .cache = true,
+ },
+};
diff --git a/video/out/gpu/lcms.h b/video/out/gpu/lcms.h
new file mode 100644
index 0000000..607353a
--- /dev/null
+++ b/video/out/gpu/lcms.h
@@ -0,0 +1,61 @@
+#ifndef MP_GL_LCMS_H
+#define MP_GL_LCMS_H
+
+#include <stddef.h>
+#include <stdbool.h>
+#include "misc/bstr.h"
+#include "video/csputils.h"
+#include <libavutil/buffer.h>
+
+extern const struct m_sub_options mp_icc_conf;
+
+struct mp_icc_opts {
+ bool use_embedded;
+ char *profile;
+ bool profile_auto;
+ bool cache;
+ char *cache_dir;
+ char *size_str;
+ int intent;
+ int contrast;
+ bool icc_use_luma;
+};
+
+struct lut3d {
+ uint16_t *data;
+ int size[3];
+};
+
+struct mp_log;
+struct mpv_global;
+struct gl_lcms;
+
+struct gl_lcms *gl_lcms_init(void *talloc_ctx, struct mp_log *log,
+ struct mpv_global *global,
+ struct mp_icc_opts *opts);
+void gl_lcms_update_options(struct gl_lcms *p);
+bool gl_lcms_set_memory_profile(struct gl_lcms *p, bstr profile);
+bool gl_lcms_has_profile(struct gl_lcms *p);
+bool gl_lcms_get_lut3d(struct gl_lcms *p, struct lut3d **,
+ enum mp_csp_prim prim, enum mp_csp_trc trc,
+ struct AVBufferRef *vid_profile);
+bool gl_lcms_has_changed(struct gl_lcms *p, enum mp_csp_prim prim,
+ enum mp_csp_trc trc, struct AVBufferRef *vid_profile);
+
+static inline bool gl_parse_3dlut_size(const char *arg, int *p1, int *p2, int *p3)
+{
+ if (!strcmp(arg, "auto")) {
+ *p1 = *p2 = *p3 = 0;
+ return true;
+ }
+ if (sscanf(arg, "%dx%dx%d", p1, p2, p3) != 3)
+ return false;
+ for (int n = 0; n < 3; n++) {
+ int s = ((int[]) { *p1, *p2, *p3 })[n];
+ if (s < 2 || s > 512)
+ return false;
+ }
+ return true;
+}
+
+#endif
diff --git a/video/out/gpu/libmpv_gpu.c b/video/out/gpu/libmpv_gpu.c
new file mode 100644
index 0000000..aae1d18
--- /dev/null
+++ b/video/out/gpu/libmpv_gpu.c
@@ -0,0 +1,248 @@
+#include "config.h"
+#include "hwdec.h"
+#include "libmpv_gpu.h"
+#include "libmpv/render_gl.h"
+#include "video.h"
+#include "video/out/libmpv.h"
+
+static const struct libmpv_gpu_context_fns *context_backends[] = {
+#if HAVE_GL
+ &libmpv_gpu_context_gl,
+#endif
+ NULL
+};
+
+struct priv {
+ struct libmpv_gpu_context *context;
+
+ struct gl_video *renderer;
+};
+
+struct native_resource_entry {
+ const char *name; // ra_add_native_resource() internal name argument
+ size_t size; // size of struct pointed to (0 for no copy)
+};
+
+static const struct native_resource_entry native_resource_map[] = {
+ [MPV_RENDER_PARAM_X11_DISPLAY] = {
+ .name = "x11",
+ .size = 0,
+ },
+ [MPV_RENDER_PARAM_WL_DISPLAY] = {
+ .name = "wl",
+ .size = 0,
+ },
+ [MPV_RENDER_PARAM_DRM_DRAW_SURFACE_SIZE] = {
+ .name = "drm_draw_surface_size",
+ .size = sizeof (mpv_opengl_drm_draw_surface_size),
+ },
+ [MPV_RENDER_PARAM_DRM_DISPLAY_V2] = {
+ .name = "drm_params_v2",
+ .size = sizeof (mpv_opengl_drm_params_v2),
+ },
+};
+
+static int init(struct render_backend *ctx, mpv_render_param *params)
+{
+ ctx->priv = talloc_zero(NULL, struct priv);
+ struct priv *p = ctx->priv;
+
+ char *api = get_mpv_render_param(params, MPV_RENDER_PARAM_API_TYPE, NULL);
+ if (!api)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ for (int n = 0; context_backends[n]; n++) {
+ const struct libmpv_gpu_context_fns *backend = context_backends[n];
+ if (strcmp(backend->api_name, api) == 0) {
+ p->context = talloc_zero(NULL, struct libmpv_gpu_context);
+ *p->context = (struct libmpv_gpu_context){
+ .global = ctx->global,
+ .log = ctx->log,
+ .fns = backend,
+ };
+ break;
+ }
+ }
+
+ if (!p->context)
+ return MPV_ERROR_NOT_IMPLEMENTED;
+
+ int err = p->context->fns->init(p->context, params);
+ if (err < 0)
+ return err;
+
+ for (int n = 0; params && params[n].type; n++) {
+ if (params[n].type > 0 &&
+ params[n].type < MP_ARRAY_SIZE(native_resource_map) &&
+ native_resource_map[params[n].type].name)
+ {
+ const struct native_resource_entry *entry =
+ &native_resource_map[params[n].type];
+ void *data = params[n].data;
+ if (entry->size)
+ data = talloc_memdup(p, data, entry->size);
+ ra_add_native_resource(p->context->ra_ctx->ra, entry->name, data);
+ }
+ }
+
+ p->renderer = gl_video_init(p->context->ra_ctx->ra, ctx->log, ctx->global);
+
+ ctx->hwdec_devs = hwdec_devices_create();
+ gl_video_init_hwdecs(p->renderer, p->context->ra_ctx, ctx->hwdec_devs, true);
+ ctx->driver_caps = VO_CAP_ROTATE90;
+ return 0;
+}
+
+static bool check_format(struct render_backend *ctx, int imgfmt)
+{
+ struct priv *p = ctx->priv;
+
+ return gl_video_check_format(p->renderer, imgfmt);
+}
+
+static int set_parameter(struct render_backend *ctx, mpv_render_param param)
+{
+ struct priv *p = ctx->priv;
+
+ switch (param.type) {
+ case MPV_RENDER_PARAM_ICC_PROFILE: {
+ mpv_byte_array *data = param.data;
+ gl_video_set_icc_profile(p->renderer, (bstr){data->data, data->size});
+ return 0;
+ }
+ case MPV_RENDER_PARAM_AMBIENT_LIGHT: {
+ int lux = *(int *)param.data;
+ gl_video_set_ambient_lux(p->renderer, lux);
+ return 0;
+ }
+ default:
+ return MPV_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+static void reconfig(struct render_backend *ctx, struct mp_image_params *params)
+{
+ struct priv *p = ctx->priv;
+
+ gl_video_config(p->renderer, params);
+}
+
+static void reset(struct render_backend *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ gl_video_reset(p->renderer);
+}
+
+static void update_external(struct render_backend *ctx, struct vo *vo)
+{
+ struct priv *p = ctx->priv;
+
+ gl_video_set_osd_source(p->renderer, vo ? vo->osd : NULL);
+ if (vo)
+ gl_video_configure_queue(p->renderer, vo);
+}
+
+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;
+
+ gl_video_resize(p->renderer, src, dst, 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;
+
+ // Mapping the surface is cheap, better than adding new backend entrypoints.
+ struct ra_tex *tex;
+ int err = p->context->fns->wrap_fbo(p->context, params, &tex);
+ if (err < 0)
+ return err;
+ *out_w = tex->params.w;
+ *out_h = tex->params.h;
+ return 0;
+}
+
+static int render(struct render_backend *ctx, mpv_render_param *params,
+ struct vo_frame *frame)
+{
+ struct priv *p = ctx->priv;
+
+ // Mapping the surface is cheap, better than adding new backend entrypoints.
+ struct ra_tex *tex;
+ int err = p->context->fns->wrap_fbo(p->context, params, &tex);
+ if (err < 0)
+ return err;
+
+ int depth = *(int *)get_mpv_render_param(params, MPV_RENDER_PARAM_DEPTH,
+ &(int){0});
+ gl_video_set_fb_depth(p->renderer, depth);
+
+ bool flip = *(int *)get_mpv_render_param(params, MPV_RENDER_PARAM_FLIP_Y,
+ &(int){0});
+
+ struct ra_fbo target = {.tex = tex, .flip = flip};
+ gl_video_render_frame(p->renderer, frame, target, RENDER_FRAME_DEF);
+ p->context->fns->done_frame(p->context, frame->display_synced);
+
+ 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;
+
+ return gl_video_get_image(p->renderer, imgfmt, w, h, stride_align, flags);
+}
+
+static void screenshot(struct render_backend *ctx, struct vo_frame *frame,
+ struct voctrl_screenshot *args)
+{
+ struct priv *p = ctx->priv;
+
+ gl_video_screenshot(p->renderer, frame, args);
+}
+
+static void perfdata(struct render_backend *ctx,
+ struct voctrl_performance_data *out)
+{
+ struct priv *p = ctx->priv;
+
+ gl_video_perfdata(p->renderer, out);
+}
+
+static void destroy(struct render_backend *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->renderer)
+ gl_video_uninit(p->renderer);
+
+ hwdec_devices_destroy(ctx->hwdec_devs);
+
+ if (p->context) {
+ p->context->fns->destroy(p->context);
+ talloc_free(p->context->priv);
+ talloc_free(p->context);
+ }
+}
+
+const struct render_backend_fns render_backend_gpu = {
+ .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/gpu/libmpv_gpu.h b/video/out/gpu/libmpv_gpu.h
new file mode 100644
index 0000000..497dcc3
--- /dev/null
+++ b/video/out/gpu/libmpv_gpu.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "video/out/libmpv.h"
+
+struct ra_tex;
+
+struct libmpv_gpu_context {
+ struct mpv_global *global;
+ struct mp_log *log;
+ const struct libmpv_gpu_context_fns *fns;
+
+ struct ra_ctx *ra_ctx;
+ void *priv;
+};
+
+// Manage backend specific interaction between libmpv and ra backend, that can't
+// be managed by ra itself (initialization and passing FBOs).
+struct libmpv_gpu_context_fns {
+ // The libmpv API type name, see MPV_RENDER_PARAM_API_TYPE.
+ const char *api_name;
+ // Pretty much works like render_backend_fns.init, except that the
+ // API type is already checked by the caller.
+ // Successful init must set ctx->ra.
+ int (*init)(struct libmpv_gpu_context *ctx, mpv_render_param *params);
+ // Wrap the surface passed to mpv_render_context_render() (via the params
+ // array) into a ra_tex and return it. Returns a libmpv error code, and sets
+ // *out to a temporary object on success. The returned object is valid until
+ // another wrap_fbo() or done_frame() is called.
+ // This does not need to care about generic attributes, like flipping.
+ int (*wrap_fbo)(struct libmpv_gpu_context *ctx, mpv_render_param *params,
+ struct ra_tex **out);
+ // Signal that the ra_tex object obtained with wrap_fbo is no longer used.
+ // For certain backends, this might also be used to signal the end of
+ // rendering (like OpenGL doing weird crap).
+ void (*done_frame)(struct libmpv_gpu_context *ctx, bool ds);
+ // Free all data in ctx->priv.
+ void (*destroy)(struct libmpv_gpu_context *ctx);
+};
+
+extern const struct libmpv_gpu_context_fns libmpv_gpu_context_gl;
diff --git a/video/out/gpu/osd.c b/video/out/gpu/osd.c
new file mode 100644
index 0000000..91505a9
--- /dev/null
+++ b/video/out/gpu/osd.c
@@ -0,0 +1,363 @@
+/*
+ * 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 <stdlib.h>
+#include <assert.h>
+#include <limits.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "video/csputils.h"
+#include "video/mp_image.h"
+#include "osd.h"
+
+#define GLSL(x) gl_sc_add(sc, #x "\n");
+
+// glBlendFuncSeparate() arguments
+static const int blend_factors[SUBBITMAP_COUNT][4] = {
+ [SUBBITMAP_LIBASS] = {RA_BLEND_SRC_ALPHA, RA_BLEND_ONE_MINUS_SRC_ALPHA,
+ RA_BLEND_ONE, RA_BLEND_ONE_MINUS_SRC_ALPHA},
+ [SUBBITMAP_BGRA] = {RA_BLEND_ONE, RA_BLEND_ONE_MINUS_SRC_ALPHA,
+ RA_BLEND_ONE, RA_BLEND_ONE_MINUS_SRC_ALPHA},
+};
+
+struct vertex {
+ float position[2];
+ float texcoord[2];
+ uint8_t ass_color[4];
+};
+
+static const struct ra_renderpass_input vertex_vao[] = {
+ {"position", RA_VARTYPE_FLOAT, 2, 1, offsetof(struct vertex, position)},
+ {"texcoord" , RA_VARTYPE_FLOAT, 2, 1, offsetof(struct vertex, texcoord)},
+ {"ass_color", RA_VARTYPE_BYTE_UNORM, 4, 1, offsetof(struct vertex, ass_color)},
+};
+
+struct mpgl_osd_part {
+ enum sub_bitmap_format format;
+ int change_id;
+ struct ra_tex *texture;
+ int w, h;
+ int num_subparts;
+ int prev_num_subparts;
+ struct sub_bitmap *subparts;
+ int num_vertices;
+ struct vertex *vertices;
+};
+
+struct mpgl_osd {
+ struct mp_log *log;
+ struct osd_state *osd;
+ struct ra *ra;
+ struct mpgl_osd_part *parts[MAX_OSD_PARTS];
+ const struct ra_format *fmt_table[SUBBITMAP_COUNT];
+ bool formats[SUBBITMAP_COUNT];
+ bool change_flag; // for reporting to API user only
+ // temporary
+ int stereo_mode;
+ struct mp_osd_res osd_res;
+ void *scratch;
+};
+
+struct mpgl_osd *mpgl_osd_init(struct ra *ra, struct mp_log *log,
+ struct osd_state *osd)
+{
+ struct mpgl_osd *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct mpgl_osd) {
+ .log = log,
+ .osd = osd,
+ .ra = ra,
+ .change_flag = true,
+ .scratch = talloc_zero_size(ctx, 1),
+ };
+
+ ctx->fmt_table[SUBBITMAP_LIBASS] = ra_find_unorm_format(ra, 1, 1);
+ ctx->fmt_table[SUBBITMAP_BGRA] = ra_find_unorm_format(ra, 1, 4);
+
+ for (int n = 0; n < MAX_OSD_PARTS; n++)
+ ctx->parts[n] = talloc_zero(ctx, struct mpgl_osd_part);
+
+ for (int n = 0; n < SUBBITMAP_COUNT; n++)
+ ctx->formats[n] = !!ctx->fmt_table[n];
+
+ return ctx;
+}
+
+void mpgl_osd_destroy(struct mpgl_osd *ctx)
+{
+ if (!ctx)
+ return;
+
+ for (int n = 0; n < MAX_OSD_PARTS; n++) {
+ struct mpgl_osd_part *p = ctx->parts[n];
+ ra_tex_free(ctx->ra, &p->texture);
+ }
+ talloc_free(ctx);
+}
+
+static int next_pow2(int v)
+{
+ for (int x = 0; x < 30; x++) {
+ if ((1 << x) >= v)
+ return 1 << x;
+ }
+ return INT_MAX;
+}
+
+static bool upload_osd(struct mpgl_osd *ctx, struct mpgl_osd_part *osd,
+ struct sub_bitmaps *imgs)
+{
+ struct ra *ra = ctx->ra;
+ bool ok = false;
+
+ assert(imgs->packed);
+
+ int req_w = next_pow2(imgs->packed_w);
+ int req_h = next_pow2(imgs->packed_h);
+
+ const struct ra_format *fmt = ctx->fmt_table[imgs->format];
+ assert(fmt);
+
+ if (!osd->texture || req_w > osd->w || req_h > osd->h ||
+ osd->format != imgs->format)
+ {
+ ra_tex_free(ra, &osd->texture);
+
+ osd->format = imgs->format;
+ osd->w = MPMAX(32, req_w);
+ osd->h = MPMAX(32, req_h);
+
+ MP_VERBOSE(ctx, "Reallocating OSD texture to %dx%d.\n", osd->w, osd->h);
+
+ if (osd->w > ra->max_texture_wh || osd->h > ra->max_texture_wh) {
+ MP_ERR(ctx, "OSD bitmaps do not fit on a surface with the maximum "
+ "supported size %dx%d.\n", ra->max_texture_wh,
+ ra->max_texture_wh);
+ goto done;
+ }
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = osd->w,
+ .h = osd->h,
+ .d = 1,
+ .format = fmt,
+ .render_src = true,
+ .src_linear = true,
+ .host_mutable = true,
+ };
+ osd->texture = ra_tex_create(ra, &params);
+ if (!osd->texture)
+ goto done;
+ }
+
+ struct ra_tex_upload_params params = {
+ .tex = osd->texture,
+ .src = imgs->packed->planes[0],
+ .invalidate = true,
+ .rc = &(struct mp_rect){0, 0, imgs->packed_w, imgs->packed_h},
+ .stride = imgs->packed->stride[0],
+ };
+
+ ok = ra->fns->tex_upload(ra, &params);
+
+done:
+ return ok;
+}
+
+static void gen_osd_cb(void *pctx, struct sub_bitmaps *imgs)
+{
+ struct mpgl_osd *ctx = pctx;
+
+ if (imgs->num_parts == 0 || !ctx->formats[imgs->format])
+ return;
+
+ struct mpgl_osd_part *osd = ctx->parts[imgs->render_index];
+
+ bool ok = true;
+ if (imgs->change_id != osd->change_id) {
+ if (!upload_osd(ctx, osd, imgs))
+ ok = false;
+
+ osd->change_id = imgs->change_id;
+ ctx->change_flag = true;
+ }
+ osd->num_subparts = ok ? imgs->num_parts : 0;
+
+ MP_TARRAY_GROW(osd, osd->subparts, osd->num_subparts);
+ memcpy(osd->subparts, imgs->parts,
+ osd->num_subparts * sizeof(osd->subparts[0]));
+}
+
+bool mpgl_osd_draw_prepare(struct mpgl_osd *ctx, int index,
+ struct gl_shader_cache *sc)
+{
+ assert(index >= 0 && index < MAX_OSD_PARTS);
+ struct mpgl_osd_part *part = ctx->parts[index];
+
+ enum sub_bitmap_format fmt = part->format;
+ if (!fmt || !part->num_subparts || !part->texture)
+ return false;
+
+ gl_sc_uniform_texture(sc, "osdtex", part->texture);
+ switch (fmt) {
+ case SUBBITMAP_BGRA: {
+ GLSL(color = texture(osdtex, texcoord).bgra;)
+ break;
+ }
+ case SUBBITMAP_LIBASS: {
+ GLSL(color =
+ vec4(ass_color.rgb, ass_color.a * texture(osdtex, texcoord).r);)
+ break;
+ }
+ default:
+ MP_ASSERT_UNREACHABLE();
+ }
+
+ return true;
+}
+
+static void write_quad(struct vertex *va, struct gl_transform t,
+ float x0, float y0, float x1, float y1,
+ float tx0, float ty0, float tx1, float ty1,
+ float tex_w, float tex_h, const uint8_t color[4])
+{
+ gl_transform_vec(t, &x0, &y0);
+ gl_transform_vec(t, &x1, &y1);
+
+#define COLOR_INIT {color[0], color[1], color[2], color[3]}
+ va[0] = (struct vertex){ {x0, y0}, {tx0 / tex_w, ty0 / tex_h}, COLOR_INIT };
+ va[1] = (struct vertex){ {x0, y1}, {tx0 / tex_w, ty1 / tex_h}, COLOR_INIT };
+ va[2] = (struct vertex){ {x1, y0}, {tx1 / tex_w, ty0 / tex_h}, COLOR_INIT };
+ va[3] = (struct vertex){ {x1, y1}, {tx1 / tex_w, ty1 / tex_h}, COLOR_INIT };
+ va[4] = va[2];
+ va[5] = va[1];
+#undef COLOR_INIT
+}
+
+static void generate_verts(struct mpgl_osd_part *part, struct gl_transform t)
+{
+ MP_TARRAY_GROW(part, part->vertices,
+ part->num_vertices + part->num_subparts * 6);
+
+ for (int n = 0; n < part->num_subparts; n++) {
+ struct sub_bitmap *b = &part->subparts[n];
+ struct vertex *va = &part->vertices[part->num_vertices];
+
+ // NOTE: the blend color is used with SUBBITMAP_LIBASS only, so it
+ // doesn't matter that we upload garbage for the other formats
+ uint32_t c = b->libass.color;
+ uint8_t color[4] = { c >> 24, (c >> 16) & 0xff,
+ (c >> 8) & 0xff, 255 - (c & 0xff) };
+
+ write_quad(va, t,
+ b->x, b->y, b->x + b->dw, b->y + b->dh,
+ b->src_x, b->src_y, b->src_x + b->w, b->src_y + b->h,
+ part->w, part->h, color);
+
+ part->num_vertices += 6;
+ }
+}
+
+// number of screen divisions per axis (x=0, y=1) for the current 3D mode
+static void get_3d_side_by_side(int stereo_mode, int div[2])
+{
+ div[0] = div[1] = 1;
+ switch (stereo_mode) {
+ case MP_STEREO3D_SBS2L:
+ case MP_STEREO3D_SBS2R: div[0] = 2; break;
+ case MP_STEREO3D_AB2R:
+ case MP_STEREO3D_AB2L: div[1] = 2; break;
+ }
+}
+
+void mpgl_osd_draw_finish(struct mpgl_osd *ctx, int index,
+ struct gl_shader_cache *sc, struct ra_fbo fbo)
+{
+ struct mpgl_osd_part *part = ctx->parts[index];
+
+ int div[2];
+ get_3d_side_by_side(ctx->stereo_mode, div);
+
+ part->num_vertices = 0;
+
+ for (int x = 0; x < div[0]; x++) {
+ for (int y = 0; y < div[1]; y++) {
+ struct gl_transform t;
+ gl_transform_ortho_fbo(&t, fbo);
+
+ float a_x = ctx->osd_res.w * x;
+ float a_y = ctx->osd_res.h * y;
+ t.t[0] += a_x * t.m[0][0] + a_y * t.m[1][0];
+ t.t[1] += a_x * t.m[0][1] + a_y * t.m[1][1];
+
+ generate_verts(part, t);
+ }
+ }
+
+ const int *factors = &blend_factors[part->format][0];
+ gl_sc_blend(sc, factors[0], factors[1], factors[2], factors[3]);
+
+ gl_sc_dispatch_draw(sc, fbo.tex, false, vertex_vao, MP_ARRAY_SIZE(vertex_vao),
+ sizeof(struct vertex), part->vertices, part->num_vertices);
+}
+
+static void set_res(struct mpgl_osd *ctx, struct mp_osd_res res, int stereo_mode)
+{
+ int div[2];
+ get_3d_side_by_side(stereo_mode, div);
+
+ res.w /= div[0];
+ res.h /= div[1];
+ ctx->osd_res = res;
+}
+
+void mpgl_osd_generate(struct mpgl_osd *ctx, struct mp_osd_res res, double pts,
+ int stereo_mode, int draw_flags)
+{
+ for (int n = 0; n < MAX_OSD_PARTS; n++)
+ ctx->parts[n]->num_subparts = 0;
+
+ set_res(ctx, res, stereo_mode);
+
+ osd_draw(ctx->osd, ctx->osd_res, pts, draw_flags, ctx->formats, gen_osd_cb, ctx);
+ ctx->stereo_mode = stereo_mode;
+
+ // Parts going away does not necessarily result in gen_osd_cb() being called
+ // (not even with num_parts==0), so check this separately.
+ for (int n = 0; n < MAX_OSD_PARTS; n++) {
+ struct mpgl_osd_part *part = ctx->parts[n];
+ if (part->num_subparts != part->prev_num_subparts)
+ ctx->change_flag = true;
+ part->prev_num_subparts = part->num_subparts;
+ }
+}
+
+// See osd_resize() for remarks. This function is an optional optimization too.
+void mpgl_osd_resize(struct mpgl_osd *ctx, struct mp_osd_res res, int stereo_mode)
+{
+ set_res(ctx, res, stereo_mode);
+ osd_resize(ctx->osd, ctx->osd_res);
+}
+
+bool mpgl_osd_check_change(struct mpgl_osd *ctx, struct mp_osd_res *res,
+ double pts)
+{
+ ctx->change_flag = false;
+ mpgl_osd_generate(ctx, *res, pts, 0, 0);
+ return ctx->change_flag;
+}
diff --git a/video/out/gpu/osd.h b/video/out/gpu/osd.h
new file mode 100644
index 0000000..00fbc49
--- /dev/null
+++ b/video/out/gpu/osd.h
@@ -0,0 +1,25 @@
+#ifndef MPLAYER_GL_OSD_H
+#define MPLAYER_GL_OSD_H
+
+#include <stdbool.h>
+#include <inttypes.h>
+
+#include "utils.h"
+#include "shader_cache.h"
+#include "sub/osd.h"
+
+struct mpgl_osd *mpgl_osd_init(struct ra *ra, struct mp_log *log,
+ struct osd_state *osd);
+void mpgl_osd_destroy(struct mpgl_osd *ctx);
+
+void mpgl_osd_generate(struct mpgl_osd *ctx, struct mp_osd_res res, double pts,
+ int stereo_mode, int draw_flags);
+void mpgl_osd_resize(struct mpgl_osd *ctx, struct mp_osd_res res, int stereo_mode);
+bool mpgl_osd_draw_prepare(struct mpgl_osd *ctx, int index,
+ struct gl_shader_cache *sc);
+void mpgl_osd_draw_finish(struct mpgl_osd *ctx, int index,
+ struct gl_shader_cache *sc, struct ra_fbo fbo);
+bool mpgl_osd_check_change(struct mpgl_osd *ctx, struct mp_osd_res *res,
+ double pts);
+
+#endif
diff --git a/video/out/gpu/ra.c b/video/out/gpu/ra.c
new file mode 100644
index 0000000..855f9b6
--- /dev/null
+++ b/video/out/gpu/ra.c
@@ -0,0 +1,424 @@
+#include "common/common.h"
+#include "common/msg.h"
+#include "video/img_format.h"
+
+#include "ra.h"
+
+void ra_add_native_resource(struct ra *ra, const char *name, void *data)
+{
+ struct ra_native_resource r = {
+ .name = name,
+ .data = data,
+ };
+ MP_TARRAY_APPEND(ra, ra->native_resources, ra->num_native_resources, r);
+}
+
+void *ra_get_native_resource(struct ra *ra, const char *name)
+{
+ for (int n = 0; n < ra->num_native_resources; n++) {
+ struct ra_native_resource *r = &ra->native_resources[n];
+ if (strcmp(r->name, name) == 0)
+ return r->data;
+ }
+
+ return NULL;
+}
+
+struct ra_tex *ra_tex_create(struct ra *ra, const struct ra_tex_params *params)
+{
+ switch (params->dimensions) {
+ case 1:
+ assert(params->h == 1 && params->d == 1);
+ break;
+ case 2:
+ assert(params->d == 1);
+ break;
+ default:
+ assert(params->dimensions >= 1 && params->dimensions <= 3);
+ }
+ return ra->fns->tex_create(ra, params);
+}
+
+void ra_tex_free(struct ra *ra, struct ra_tex **tex)
+{
+ if (*tex)
+ ra->fns->tex_destroy(ra, *tex);
+ *tex = NULL;
+}
+
+struct ra_buf *ra_buf_create(struct ra *ra, const struct ra_buf_params *params)
+{
+ return ra->fns->buf_create(ra, params);
+}
+
+void ra_buf_free(struct ra *ra, struct ra_buf **buf)
+{
+ if (*buf)
+ ra->fns->buf_destroy(ra, *buf);
+ *buf = NULL;
+}
+
+void ra_free(struct ra **ra)
+{
+ if (*ra)
+ (*ra)->fns->destroy(*ra);
+ talloc_free(*ra);
+ *ra = NULL;
+}
+
+size_t ra_vartype_size(enum ra_vartype type)
+{
+ switch (type) {
+ case RA_VARTYPE_INT: return sizeof(int);
+ case RA_VARTYPE_FLOAT: return sizeof(float);
+ case RA_VARTYPE_BYTE_UNORM: return 1;
+ default: return 0;
+ }
+}
+
+struct ra_layout ra_renderpass_input_layout(struct ra_renderpass_input *input)
+{
+ size_t el_size = ra_vartype_size(input->type);
+ if (!el_size)
+ return (struct ra_layout){0};
+
+ // host data is always tightly packed
+ return (struct ra_layout) {
+ .align = 1,
+ .stride = el_size * input->dim_v,
+ .size = el_size * input->dim_v * input->dim_m,
+ };
+}
+
+static struct ra_renderpass_input *dup_inputs(void *ta_parent,
+ const struct ra_renderpass_input *inputs, int num_inputs)
+{
+ struct ra_renderpass_input *res =
+ talloc_memdup(ta_parent, (void *)inputs, num_inputs * sizeof(inputs[0]));
+ for (int n = 0; n < num_inputs; n++)
+ res[n].name = talloc_strdup(res, res[n].name);
+ return res;
+}
+
+// Return a newly allocated deep-copy of params.
+struct ra_renderpass_params *ra_renderpass_params_copy(void *ta_parent,
+ const struct ra_renderpass_params *params)
+{
+ struct ra_renderpass_params *res = talloc_ptrtype(ta_parent, res);
+ *res = *params;
+ res->inputs = dup_inputs(res, res->inputs, res->num_inputs);
+ res->vertex_attribs =
+ dup_inputs(res, res->vertex_attribs, res->num_vertex_attribs);
+ res->cached_program = bstrdup(res, res->cached_program);
+ res->vertex_shader = talloc_strdup(res, res->vertex_shader);
+ res->frag_shader = talloc_strdup(res, res->frag_shader);
+ res->compute_shader = talloc_strdup(res, res->compute_shader);
+ return res;
+}
+
+struct glsl_fmt {
+ enum ra_ctype ctype;
+ int num_components;
+ int component_depth[4];
+ const char *glsl_format;
+};
+
+// List taken from the GLSL specification, sans snorm and sint formats
+static const struct glsl_fmt ra_glsl_fmts[] = {
+ {RA_CTYPE_FLOAT, 1, {16}, "r16f"},
+ {RA_CTYPE_FLOAT, 1, {32}, "r32f"},
+ {RA_CTYPE_FLOAT, 2, {16, 16}, "rg16f"},
+ {RA_CTYPE_FLOAT, 2, {32, 32}, "rg32f"},
+ {RA_CTYPE_FLOAT, 4, {16, 16, 16, 16}, "rgba16f"},
+ {RA_CTYPE_FLOAT, 4, {32, 32, 32, 32}, "rgba32f"},
+ {RA_CTYPE_FLOAT, 3, {11, 11, 10}, "r11f_g11f_b10f"},
+
+ {RA_CTYPE_UNORM, 1, {8}, "r8"},
+ {RA_CTYPE_UNORM, 1, {16}, "r16"},
+ {RA_CTYPE_UNORM, 2, {8, 8}, "rg8"},
+ {RA_CTYPE_UNORM, 2, {16, 16}, "rg16"},
+ {RA_CTYPE_UNORM, 4, {8, 8, 8, 8}, "rgba8"},
+ {RA_CTYPE_UNORM, 4, {16, 16, 16, 16}, "rgba16"},
+ {RA_CTYPE_UNORM, 4, {10, 10, 10, 2}, "rgb10_a2"},
+
+ {RA_CTYPE_UINT, 1, {8}, "r8ui"},
+ {RA_CTYPE_UINT, 1, {16}, "r16ui"},
+ {RA_CTYPE_UINT, 1, {32}, "r32ui"},
+ {RA_CTYPE_UINT, 2, {8, 8}, "rg8ui"},
+ {RA_CTYPE_UINT, 2, {16, 16}, "rg16ui"},
+ {RA_CTYPE_UINT, 2, {32, 32}, "rg32ui"},
+ {RA_CTYPE_UINT, 4, {8, 8, 8, 8}, "rgba8ui"},
+ {RA_CTYPE_UINT, 4, {16, 16, 16, 16}, "rgba16ui"},
+ {RA_CTYPE_UINT, 4, {32, 32, 32, 32}, "rgba32ui"},
+ {RA_CTYPE_UINT, 4, {10, 10, 10, 2}, "rgb10_a2ui"},
+};
+
+const char *ra_fmt_glsl_format(const struct ra_format *fmt)
+{
+ for (int n = 0; n < MP_ARRAY_SIZE(ra_glsl_fmts); n++) {
+ const struct glsl_fmt *gfmt = &ra_glsl_fmts[n];
+
+ if (fmt->ctype != gfmt->ctype)
+ continue;
+ if (fmt->num_components != gfmt->num_components)
+ continue;
+
+ for (int i = 0; i < fmt->num_components; i++) {
+ if (fmt->component_depth[i] != gfmt->component_depth[i])
+ goto next_fmt;
+ }
+
+ return gfmt->glsl_format;
+
+next_fmt: ; // equivalent to `continue`
+ }
+
+ return NULL;
+}
+
+// Return whether this is a tightly packed format with no external padding and
+// with the same bit size/depth in all components, and the shader returns
+// components in the same order as in memory.
+static bool ra_format_is_regular(const struct ra_format *fmt)
+{
+ if (!fmt->pixel_size || !fmt->num_components || !fmt->ordered)
+ return false;
+ for (int n = 1; n < fmt->num_components; n++) {
+ if (fmt->component_size[n] != fmt->component_size[0] ||
+ fmt->component_depth[n] != fmt->component_depth[0])
+ return false;
+ }
+ if (fmt->component_size[0] * fmt->num_components != fmt->pixel_size * 8)
+ return false;
+ return true;
+}
+
+// Return a regular filterable format using RA_CTYPE_UNORM.
+const struct ra_format *ra_find_unorm_format(struct ra *ra,
+ int bytes_per_component,
+ int n_components)
+{
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt = ra->formats[n];
+ if (fmt->ctype == RA_CTYPE_UNORM && fmt->num_components == n_components &&
+ fmt->pixel_size == bytes_per_component * n_components &&
+ fmt->component_depth[0] == bytes_per_component * 8 &&
+ fmt->linear_filter && ra_format_is_regular(fmt))
+ return fmt;
+ }
+ return NULL;
+}
+
+// Return a regular format using RA_CTYPE_UINT.
+const struct ra_format *ra_find_uint_format(struct ra *ra,
+ int bytes_per_component,
+ int n_components)
+{
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt = ra->formats[n];
+ if (fmt->ctype == RA_CTYPE_UINT && fmt->num_components == n_components &&
+ fmt->pixel_size == bytes_per_component * n_components &&
+ fmt->component_depth[0] == bytes_per_component * 8 &&
+ ra_format_is_regular(fmt))
+ return fmt;
+ }
+ return NULL;
+}
+
+// Find a float format of any precision that matches the C type of the same
+// size for upload.
+// May drop bits from the mantissa (such as selecting float16 even if
+// bytes_per_component == 32); prefers possibly faster formats first.
+static const struct ra_format *ra_find_float_format(struct ra *ra,
+ int bytes_per_component,
+ int n_components)
+{
+ // Assumes ra_format are ordered by performance.
+ // The >=16 check is to avoid catching fringe formats.
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt = ra->formats[n];
+ if (fmt->ctype == RA_CTYPE_FLOAT && fmt->num_components == n_components &&
+ fmt->pixel_size == bytes_per_component * n_components &&
+ fmt->component_depth[0] >= 16 &&
+ fmt->linear_filter && ra_format_is_regular(fmt))
+ return fmt;
+ }
+ return NULL;
+}
+
+// Return a filterable regular format that uses at least float16 internally, and
+// uses a normal C float for transfer on the CPU side. (This is just so we don't
+// need 32->16 bit conversion on CPU, which would be messy.)
+const struct ra_format *ra_find_float16_format(struct ra *ra, int n_components)
+{
+ return ra_find_float_format(ra, sizeof(float), n_components);
+}
+
+const struct ra_format *ra_find_named_format(struct ra *ra, const char *name)
+{
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt = ra->formats[n];
+ if (strcmp(fmt->name, name) == 0)
+ return fmt;
+ }
+ return NULL;
+}
+
+// Like ra_find_unorm_format(), but if no fixed point format is available,
+// return an unsigned integer format.
+static const struct ra_format *find_plane_format(struct ra *ra, int bytes,
+ int n_channels,
+ enum mp_component_type ctype)
+{
+ switch (ctype) {
+ case MP_COMPONENT_TYPE_UINT: {
+ const struct ra_format *f = ra_find_unorm_format(ra, bytes, n_channels);
+ if (f)
+ return f;
+ return ra_find_uint_format(ra, bytes, n_channels);
+ }
+ case MP_COMPONENT_TYPE_FLOAT:
+ return ra_find_float_format(ra, bytes, n_channels);
+ default: return NULL;
+ }
+}
+
+// Put a mapping of imgfmt to texture formats into *out. Basically it selects
+// the correct texture formats needed to represent an imgfmt in a shader, with
+// textures using the same memory organization as on the CPU.
+// Each plane is represented by a texture, and each texture has a RGBA
+// component order. out->components describes the meaning of them.
+// May return integer formats for >8 bit formats, if the driver has no
+// normalized 16 bit formats.
+// Returns false (and *out is not touched) if no format found.
+bool ra_get_imgfmt_desc(struct ra *ra, int imgfmt, struct ra_imgfmt_desc *out)
+{
+ struct ra_imgfmt_desc res = {.component_type = RA_CTYPE_UNKNOWN};
+
+ struct mp_regular_imgfmt regfmt;
+ if (mp_get_regular_imgfmt(&regfmt, imgfmt)) {
+ res.num_planes = regfmt.num_planes;
+ res.component_bits = regfmt.component_size * 8;
+ res.component_pad = regfmt.component_pad;
+ for (int n = 0; n < regfmt.num_planes; n++) {
+ struct mp_regular_imgfmt_plane *plane = &regfmt.planes[n];
+ res.planes[n] = find_plane_format(ra, regfmt.component_size,
+ plane->num_components,
+ regfmt.component_type);
+ if (!res.planes[n])
+ return false;
+ for (int i = 0; i < plane->num_components; i++)
+ res.components[n][i] = plane->components[i];
+ // Dropping LSBs when shifting will lead to dropped MSBs.
+ if (res.component_bits > res.planes[n]->component_depth[0] &&
+ res.component_pad < 0)
+ return false;
+ // Renderer restriction, but actually an unwanted corner case.
+ if (res.component_type != RA_CTYPE_UNKNOWN &&
+ res.component_type != res.planes[n]->ctype)
+ return false;
+ res.component_type = res.planes[n]->ctype;
+ }
+ res.chroma_w = 1 << regfmt.chroma_xs;
+ res.chroma_h = 1 << regfmt.chroma_ys;
+ goto supported;
+ }
+
+ for (int n = 0; n < ra->num_formats; n++) {
+ if (imgfmt && ra->formats[n]->special_imgfmt == imgfmt) {
+ res = *ra->formats[n]->special_imgfmt_desc;
+ goto supported;
+ }
+ }
+
+ // Unsupported format
+ return false;
+
+supported:
+
+ *out = res;
+ return true;
+}
+
+static const char *ctype_to_str(enum ra_ctype ctype)
+{
+ switch (ctype) {
+ case RA_CTYPE_UNORM: return "unorm";
+ case RA_CTYPE_UINT: return "uint ";
+ case RA_CTYPE_FLOAT: return "float";
+ default: return "unknown";
+ }
+}
+
+void ra_dump_tex_formats(struct ra *ra, int msgl)
+{
+ if (!mp_msg_test(ra->log, msgl))
+ return;
+ MP_MSG(ra, msgl, "Texture formats:\n");
+ MP_MSG(ra, msgl, " NAME COMP*TYPE SIZE DEPTH PER COMP.\n");
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt = ra->formats[n];
+ const char *ctype = ctype_to_str(fmt->ctype);
+ char cl[40] = "";
+ for (int i = 0; i < fmt->num_components; i++) {
+ mp_snprintf_cat(cl, sizeof(cl), "%s%d", i ? " " : "",
+ fmt->component_size[i]);
+ if (fmt->component_size[i] != fmt->component_depth[i])
+ mp_snprintf_cat(cl, sizeof(cl), "/%d", fmt->component_depth[i]);
+ }
+ MP_MSG(ra, msgl, " %-10s %d*%s %3dB %s %s %s %s {%s}\n", fmt->name,
+ fmt->num_components, ctype, fmt->pixel_size,
+ fmt->luminance_alpha ? "LA" : " ",
+ fmt->linear_filter ? "LF" : " ",
+ fmt->renderable ? "CR" : " ",
+ fmt->storable ? "ST" : " ", cl);
+ }
+ MP_MSG(ra, msgl, " LA = LUMINANCE_ALPHA hack format\n");
+ MP_MSG(ra, msgl, " LF = linear filterable\n");
+ MP_MSG(ra, msgl, " CR = can be used for render targets\n");
+ MP_MSG(ra, msgl, " ST = can be used for storable images\n");
+}
+
+void ra_dump_imgfmt_desc(struct ra *ra, const struct ra_imgfmt_desc *desc,
+ int msgl)
+{
+ char pl[80] = "";
+ char pf[80] = "";
+ for (int n = 0; n < desc->num_planes; n++) {
+ if (n > 0) {
+ mp_snprintf_cat(pl, sizeof(pl), "/");
+ mp_snprintf_cat(pf, sizeof(pf), "/");
+ }
+ char t[5] = {0};
+ for (int i = 0; i < 4; i++)
+ t[i] = "_rgba"[desc->components[n][i]];
+ for (int i = 3; i > 0 && t[i] == '_'; i--)
+ t[i] = '\0';
+ mp_snprintf_cat(pl, sizeof(pl), "%s", t);
+ mp_snprintf_cat(pf, sizeof(pf), "%s", desc->planes[n]->name);
+ }
+ MP_MSG(ra, msgl, "%d planes %dx%d %d/%d [%s] (%s) [%s]\n",
+ desc->num_planes, desc->chroma_w, desc->chroma_h,
+ desc->component_bits, desc->component_pad, pf, pl,
+ ctype_to_str(desc->component_type));
+}
+
+void ra_dump_img_formats(struct ra *ra, int msgl)
+{
+ if (!mp_msg_test(ra->log, msgl))
+ return;
+ MP_MSG(ra, msgl, "Image formats:\n");
+ for (int imgfmt = IMGFMT_START; imgfmt < IMGFMT_END; imgfmt++) {
+ const char *name = mp_imgfmt_to_name(imgfmt);
+ if (strcmp(name, "unknown") == 0)
+ continue;
+ MP_MSG(ra, msgl, " %s", name);
+ struct ra_imgfmt_desc desc;
+ if (ra_get_imgfmt_desc(ra, imgfmt, &desc)) {
+ MP_MSG(ra, msgl, " => ");
+ ra_dump_imgfmt_desc(ra, &desc, msgl);
+ } else {
+ MP_MSG(ra, msgl, "\n");
+ }
+ }
+}
diff --git a/video/out/gpu/ra.h b/video/out/gpu/ra.h
new file mode 100644
index 0000000..5f229f8
--- /dev/null
+++ b/video/out/gpu/ra.h
@@ -0,0 +1,559 @@
+#pragma once
+
+#include "common/common.h"
+#include "misc/bstr.h"
+
+// Handle for a rendering API backend.
+struct ra {
+ struct ra_fns *fns;
+ void *priv;
+
+ int glsl_version; // GLSL version (e.g. 300 => 3.0)
+ bool glsl_es; // use ES dialect
+ bool glsl_vulkan; // use vulkan dialect
+
+ struct mp_log *log;
+
+ // RA_CAP_* bit field. The RA backend must set supported features at init
+ // time.
+ uint64_t caps;
+
+ // Maximum supported width and height of a 2D texture. Set by the RA backend
+ // at init time.
+ int max_texture_wh;
+
+ // Maximum shared memory for compute shaders. Set by the RA backend at init
+ // time.
+ size_t max_shmem;
+
+ // Maximum number of threads in a compute work group. Set by the RA backend
+ // at init time.
+ size_t max_compute_group_threads;
+
+ // Maximum push constant size. Set by the RA backend at init time.
+ size_t max_pushc_size;
+
+ // Set of supported texture formats. Must be added by RA backend at init time.
+ // If there are equivalent formats with different caveats, the preferred
+ // formats should have a lower index. (E.g. GLES3 should put rg8 before la.)
+ struct ra_format **formats;
+ int num_formats;
+
+ // Accelerate texture uploads via an extra PBO even when
+ // RA_CAP_DIRECT_UPLOAD is supported. This is basically only relevant for
+ // OpenGL. Set by the RA user.
+ bool use_pbo;
+
+ // Array of native resources. For the most part an "escape" mechanism, and
+ // usually does not contain parameters required for basic functionality.
+ struct ra_native_resource *native_resources;
+ int num_native_resources;
+};
+
+// For passing through windowing system specific parameters and such. The
+// names are always internal (the libmpv render API uses mpv_render_param_type
+// and maps them to names internally).
+// For example, a name="x11" entry has a X11 display as (Display*)data.
+struct ra_native_resource {
+ const char *name;
+ void *data;
+};
+
+// Add a ra_native_resource entry. Both name and data pointers must stay valid
+// until ra termination.
+void ra_add_native_resource(struct ra *ra, const char *name, void *data);
+
+// Search ra->native_resources, returns NULL on failure.
+void *ra_get_native_resource(struct ra *ra, const char *name);
+
+enum {
+ RA_CAP_TEX_1D = 1 << 0, // supports 1D textures (as shader inputs)
+ RA_CAP_TEX_3D = 1 << 1, // supports 3D textures (as shader inputs)
+ RA_CAP_BLIT = 1 << 2, // supports ra_fns.blit
+ RA_CAP_COMPUTE = 1 << 3, // supports compute shaders
+ RA_CAP_DIRECT_UPLOAD = 1 << 4, // supports tex_upload without ra_buf
+ RA_CAP_BUF_RO = 1 << 5, // supports RA_VARTYPE_BUF_RO
+ RA_CAP_BUF_RW = 1 << 6, // supports RA_VARTYPE_BUF_RW
+ RA_CAP_NESTED_ARRAY = 1 << 7, // supports nested arrays
+ RA_CAP_GLOBAL_UNIFORM = 1 << 8, // supports using "naked" uniforms (not UBO)
+ RA_CAP_GATHER = 1 << 9, // supports textureGather in GLSL
+ RA_CAP_FRAGCOORD = 1 << 10, // supports reading from gl_FragCoord
+ RA_CAP_PARALLEL_COMPUTE = 1 << 11, // supports parallel compute shaders
+ RA_CAP_NUM_GROUPS = 1 << 12, // supports gl_NumWorkGroups
+ RA_CAP_SLOW_DR = 1 << 13, // direct rendering is assumed to be slow
+};
+
+enum ra_ctype {
+ RA_CTYPE_UNKNOWN = 0, // also used for inconsistent multi-component formats
+ RA_CTYPE_UNORM, // unsigned normalized integer (fixed point) formats
+ RA_CTYPE_UINT, // full integer formats
+ RA_CTYPE_FLOAT, // float formats (signed, any bit size)
+};
+
+// All formats must be useable as texture formats. All formats must be byte
+// aligned (all pixels start and end on a byte boundary), at least as far CPU
+// transfers are concerned.
+struct ra_format {
+ // All fields are read-only after creation.
+ const char *name; // symbolic name for user interaction/debugging
+ void *priv;
+ enum ra_ctype ctype; // data type of each component
+ bool ordered; // components are sequential in memory, and returned
+ // by the shader in memory order (the shader can
+ // return arbitrary values for unused components)
+ int num_components; // component count, 0 if not applicable, max. 4
+ int component_size[4]; // in bits, all entries 0 if not applicable
+ int component_depth[4]; // bits in use for each component, 0 if not applicable
+ // (_must_ be set if component_size[] includes padding,
+ // and the real procession as seen by shader is lower)
+ int pixel_size; // in bytes, total pixel size (0 if opaque)
+ bool luminance_alpha; // pre-GL_ARB_texture_rg hack for 2 component textures
+ // if this is set, shader must use .ra instead of .rg
+ // only applies to 2-component textures
+ bool linear_filter; // linear filtering available from shader
+ bool renderable; // can be used for render targets
+ bool storable; // can be used for storage images
+ bool dummy_format; // is not a real ra_format but a fake one (e.g. FBO).
+ // dummy formats cannot be used to create textures
+
+ // If not 0, the format represents some sort of packed fringe format, whose
+ // shader representation is given by the special_imgfmt_desc pointer.
+ int special_imgfmt;
+ const struct ra_imgfmt_desc *special_imgfmt_desc;
+
+ // This gives the GLSL image format corresponding to the format, if any.
+ // (e.g. rgba16ui)
+ const char *glsl_format;
+};
+
+struct ra_tex_params {
+ int dimensions; // 1-3 for 1D-3D textures
+ // Size of the texture. 1D textures require h=d=1, 2D textures require d=1.
+ int w, h, d;
+ const struct ra_format *format;
+ bool render_src; // must be useable as source texture in a shader
+ bool render_dst; // must be useable as target texture in a shader
+ bool storage_dst; // must be usable as a storage image (RA_VARTYPE_IMG_W)
+ bool blit_src; // must be usable as a blit source
+ bool blit_dst; // must be usable as a blit destination
+ bool host_mutable; // texture may be updated with tex_upload
+ bool downloadable; // texture can be read with tex_download
+ // When used as render source texture.
+ bool src_linear; // if false, use nearest sampling (whether this can
+ // be true depends on ra_format.linear_filter)
+ bool src_repeat; // if false, clamp texture coordinates to edge
+ // if true, repeat texture coordinates
+ bool non_normalized; // hack for GL_TEXTURE_RECTANGLE OSX idiocy
+ // always set to false, except in OSX code
+ bool external_oes; // hack for GL_TEXTURE_EXTERNAL_OES idiocy
+ // If non-NULL, the texture will be created with these contents. Using
+ // this does *not* require setting host_mutable. Otherwise, the initial
+ // data is undefined.
+ void *initial_data;
+};
+
+// Conflates the following typical GPU API concepts:
+// - texture itself
+// - sampler state
+// - staging buffers for texture upload
+// - framebuffer objects
+// - wrappers for swapchain framebuffers
+// - synchronization needed for upload/rendering/etc.
+struct ra_tex {
+ // All fields are read-only after creation.
+ struct ra_tex_params params;
+ void *priv;
+};
+
+struct ra_tex_upload_params {
+ struct ra_tex *tex; // Texture to upload to
+ bool invalidate; // Discard pre-existing data not in the region uploaded
+ // Uploading from buffer:
+ struct ra_buf *buf; // Buffer to upload from (mutually exclusive with `src`)
+ size_t buf_offset; // Start of data within buffer (bytes)
+ // Uploading directly: (Note: If RA_CAP_DIRECT_UPLOAD is not set, then this
+ // will be internally translated to a tex_upload buffer by the RA)
+ const void *src; // Address of data
+ // For 2D textures only:
+ struct mp_rect *rc; // Region to upload. NULL means entire image
+ ptrdiff_t stride; // The size of a horizontal line in bytes (*not* texels!)
+};
+
+struct ra_tex_download_params {
+ struct ra_tex *tex; // Texture to download from
+ // Downloading directly (set by caller, data written to by callee):
+ void *dst; // Address of data (packed with no alignment)
+ ptrdiff_t stride; // The size of a horizontal line in bytes (*not* texels!)
+};
+
+// Buffer usage type. This restricts what types of operations may be performed
+// on a buffer.
+enum ra_buf_type {
+ RA_BUF_TYPE_INVALID,
+ RA_BUF_TYPE_TEX_UPLOAD, // texture upload buffer (pixel buffer object)
+ RA_BUF_TYPE_SHADER_STORAGE, // shader buffer (SSBO), for RA_VARTYPE_BUF_RW
+ RA_BUF_TYPE_UNIFORM, // uniform buffer (UBO), for RA_VARTYPE_BUF_RO
+ RA_BUF_TYPE_VERTEX, // not publicly usable (RA-internal usage)
+ RA_BUF_TYPE_SHARED_MEMORY, // device memory for sharing with external API
+};
+
+struct ra_buf_params {
+ enum ra_buf_type type;
+ size_t size;
+ bool host_mapped; // create a read-writable persistent mapping (ra_buf.data)
+ bool host_mutable; // contents may be updated via buf_update()
+ // If non-NULL, the buffer will be created with these contents. Otherwise,
+ // the initial data is undefined.
+ void *initial_data;
+};
+
+// A generic buffer, which can be used for many purposes (texture upload,
+// storage buffer, uniform buffer, etc.)
+struct ra_buf {
+ // All fields are read-only after creation.
+ struct ra_buf_params params;
+ void *data; // for persistently mapped buffers, points to the first byte
+ void *priv;
+};
+
+// Type of a shader uniform variable, or a vertex attribute. In all cases,
+// vectors are matrices are done by having more than 1 value.
+enum ra_vartype {
+ RA_VARTYPE_INVALID,
+ RA_VARTYPE_INT, // C: int, GLSL: int, ivec*
+ RA_VARTYPE_FLOAT, // C: float, GLSL: float, vec*, mat*
+ RA_VARTYPE_TEX, // C: ra_tex*, GLSL: various sampler types
+ // ra_tex.params.render_src must be true
+ RA_VARTYPE_IMG_W, // C: ra_tex*, GLSL: various image types
+ // write-only (W) image for compute shaders
+ // ra_tex.params.storage_dst must be true
+ RA_VARTYPE_BYTE_UNORM, // C: uint8_t, GLSL: int, vec* (vertex data only)
+ RA_VARTYPE_BUF_RO, // C: ra_buf*, GLSL: uniform buffer block
+ // buf type must be RA_BUF_TYPE_UNIFORM
+ RA_VARTYPE_BUF_RW, // C: ra_buf*, GLSL: shader storage buffer block
+ // buf type must be RA_BUF_TYPE_SHADER_STORAGE
+ RA_VARTYPE_COUNT
+};
+
+// Returns the host size of a ra_vartype, or 0 for abstract vartypes (e.g. tex)
+size_t ra_vartype_size(enum ra_vartype type);
+
+// Represents a uniform, texture input parameter, and similar things.
+struct ra_renderpass_input {
+ const char *name; // name as used in the shader
+ enum ra_vartype type;
+ // The total number of values is given by dim_v * dim_m.
+ int dim_v; // vector dimension (1 for non-vector and non-matrix)
+ int dim_m; // additional matrix dimension (dim_v x dim_m)
+ // Vertex data: byte offset of the attribute into the vertex struct
+ size_t offset;
+ // RA_VARTYPE_TEX: texture unit
+ // RA_VARTYPE_IMG_W: image unit
+ // RA_VARTYPE_BUF_* buffer binding point
+ // Other uniforms: unused
+ // Bindings must be unique within each namespace, as specified by
+ // desc_namespace()
+ int binding;
+};
+
+// Represents the layout requirements of an input value
+struct ra_layout {
+ size_t align; // the alignment requirements (always a power of two)
+ size_t stride; // the delta between two rows of an array/matrix
+ size_t size; // the total size of the input
+};
+
+// Returns the host layout of a render pass input. Returns {0} for renderpass
+// inputs without a corresponding host representation (e.g. textures/buffers)
+struct ra_layout ra_renderpass_input_layout(struct ra_renderpass_input *input);
+
+enum ra_blend {
+ RA_BLEND_ZERO,
+ RA_BLEND_ONE,
+ RA_BLEND_SRC_ALPHA,
+ RA_BLEND_ONE_MINUS_SRC_ALPHA,
+};
+
+enum ra_renderpass_type {
+ RA_RENDERPASS_TYPE_INVALID,
+ RA_RENDERPASS_TYPE_RASTER, // vertex+fragment shader
+ RA_RENDERPASS_TYPE_COMPUTE, // compute shader
+};
+
+// Static part of a rendering pass. It conflates the following:
+// - compiled shader and its list of uniforms
+// - vertex attributes and its shader mappings
+// - blending parameters
+// (For Vulkan, this would be shader module + pipeline state.)
+// Upon creation, the values of dynamic values such as uniform contents (whose
+// initial values are not provided here) are required to be 0.
+struct ra_renderpass_params {
+ enum ra_renderpass_type type;
+
+ // Uniforms, including texture/sampler inputs.
+ struct ra_renderpass_input *inputs;
+ int num_inputs;
+ size_t push_constants_size; // must be <= ra.max_pushc_size and a multiple of 4
+
+ // Highly implementation-specific byte array storing a compiled version
+ // of the program. Can be used to speed up shader compilation. A backend
+ // xan read this in renderpass_create, or set this on the newly created
+ // ra_renderpass params field.
+ bstr cached_program;
+
+ // --- type==RA_RENDERPASS_TYPE_RASTER only
+
+ // Describes the format of the vertex data. When using ra.glsl_vulkan,
+ // the order of this array must match the vertex attribute locations.
+ struct ra_renderpass_input *vertex_attribs;
+ int num_vertex_attribs;
+ int vertex_stride;
+
+ // Format of the target texture
+ const struct ra_format *target_format;
+
+ // Shader text, in GLSL. (Yes, you need a GLSL compiler.)
+ // These are complete shaders, including prelude and declarations.
+ const char *vertex_shader;
+ const char *frag_shader;
+
+ // Target blending mode. If enable_blend is false, the blend_ fields can
+ // be ignored.
+ bool enable_blend;
+ enum ra_blend blend_src_rgb;
+ enum ra_blend blend_dst_rgb;
+ enum ra_blend blend_src_alpha;
+ enum ra_blend blend_dst_alpha;
+
+ // If true, the contents of `target` not written to will become undefined
+ bool invalidate_target;
+
+ // --- type==RA_RENDERPASS_TYPE_COMPUTE only
+
+ // Shader text, like vertex_shader/frag_shader.
+ const char *compute_shader;
+};
+
+struct ra_renderpass_params *ra_renderpass_params_copy(void *ta_parent,
+ const struct ra_renderpass_params *params);
+
+// Conflates the following typical GPU API concepts:
+// - various kinds of shaders
+// - rendering pipelines
+// - descriptor sets, uniforms, other bindings
+// - all synchronization necessary
+// - the current values of all uniforms (this one makes it relatively stateful
+// from an API perspective)
+struct ra_renderpass {
+ // All fields are read-only after creation.
+ struct ra_renderpass_params params;
+ void *priv;
+};
+
+// An input value (see ra_renderpass_input).
+struct ra_renderpass_input_val {
+ int index; // index into ra_renderpass_params.inputs[]
+ void *data; // pointer to data according to ra_renderpass_input
+ // (e.g. type==RA_VARTYPE_FLOAT+dim_v=3,dim_m=3 => float[9])
+};
+
+// Parameters for performing a rendering pass (basically the dynamic params).
+// These change potentially every time.
+struct ra_renderpass_run_params {
+ struct ra_renderpass *pass;
+
+ // Generally this lists parameters only which changed since the last
+ // invocation and need to be updated. The ra_renderpass instance is
+ // supposed to keep unchanged values from the previous run.
+ // For non-primitive types like textures, these entries are always added,
+ // even if they do not change.
+ struct ra_renderpass_input_val *values;
+ int num_values;
+ void *push_constants; // must be set if params.push_constants_size > 0
+
+ // --- pass->params.type==RA_RENDERPASS_TYPE_RASTER only
+
+ // target->params.render_dst must be true, and target->params.format must
+ // match pass->params.target_format.
+ struct ra_tex *target;
+ struct mp_rect viewport;
+ struct mp_rect scissors;
+
+ // (The primitive type is always a triangle list.)
+ void *vertex_data;
+ int vertex_count; // number of vertex elements, not bytes
+
+ // --- pass->params.type==RA_RENDERPASS_TYPE_COMPUTE only
+
+ // Number of work groups to be run in X/Y/Z dimensions.
+ int compute_groups[3];
+};
+
+// This is an opaque type provided by the implementation, but we want to at
+// least give it a saner name than void* for code readability purposes.
+typedef void ra_timer;
+
+// Rendering API entrypoints. (Note: there are some additional hidden features
+// you need to take care of. For example, hwdec mapping will be provided
+// separately from ra, but might need to call into ra private code.)
+struct ra_fns {
+ void (*destroy)(struct ra *ra);
+
+ // Create a texture (with undefined contents). Return NULL on failure.
+ // This is a rare operation, and normally textures and even FBOs for
+ // temporary rendering intermediate data are cached.
+ struct ra_tex *(*tex_create)(struct ra *ra,
+ const struct ra_tex_params *params);
+
+ void (*tex_destroy)(struct ra *ra, struct ra_tex *tex);
+
+ // Upload data to a texture. This is an extremely common operation. When
+ // using a buffer, the contents of the buffer must exactly match the image
+ // - conversions between bit depth etc. are not supported. The buffer *may*
+ // be marked as "in use" while this operation is going on, and the contents
+ // must not be touched again by the API user until buf_poll returns true.
+ // Returns whether successful.
+ bool (*tex_upload)(struct ra *ra, const struct ra_tex_upload_params *params);
+
+ // Copy data from the texture to memory. ra_tex_params.downloadable must
+ // have been set to true on texture creation.
+ bool (*tex_download)(struct ra *ra, struct ra_tex_download_params *params);
+
+ // Create a buffer. This can be used as a persistently mapped buffer,
+ // a uniform buffer, a shader storage buffer or possibly others.
+ // Not all usage types must be supported; may return NULL if unavailable.
+ struct ra_buf *(*buf_create)(struct ra *ra,
+ const struct ra_buf_params *params);
+
+ void (*buf_destroy)(struct ra *ra, struct ra_buf *buf);
+
+ // Update the contents of a buffer, starting at a given offset (*must* be a
+ // multiple of 4) and up to a given size, with the contents of *data. This
+ // is an extremely common operation. Calling this while the buffer is
+ // considered "in use" is an error. (See: buf_poll)
+ void (*buf_update)(struct ra *ra, struct ra_buf *buf, ptrdiff_t offset,
+ const void *data, size_t size);
+
+ // Returns if a buffer is currently "in use" or not. Updating the contents
+ // of a buffer (via buf_update or writing to buf->data) while it is still
+ // in use is an error and may result in graphical corruption. Optional, if
+ // NULL then all buffers are always usable.
+ bool (*buf_poll)(struct ra *ra, struct ra_buf *buf);
+
+ // Returns the layout requirements of a uniform buffer element. Optional,
+ // but must be implemented if RA_CAP_BUF_RO is supported.
+ struct ra_layout (*uniform_layout)(struct ra_renderpass_input *inp);
+
+ // Returns the layout requirements of a push constant element. Optional,
+ // but must be implemented if ra.max_pushc_size > 0.
+ struct ra_layout (*push_constant_layout)(struct ra_renderpass_input *inp);
+
+ // Returns an abstract namespace index for a given renderpass input type.
+ // This will always be a value >= 0 and < RA_VARTYPE_COUNT. This is used to
+ // figure out which inputs may share the same value of `binding`.
+ int (*desc_namespace)(struct ra *ra, enum ra_vartype type);
+
+ // Clear the dst with the given color (rgba) and within the given scissor.
+ // dst must have dst->params.render_dst==true. Content outside of the
+ // scissor is preserved.
+ void (*clear)(struct ra *ra, struct ra_tex *dst, float color[4],
+ struct mp_rect *scissor);
+
+ // Copy a sub-rectangle from one texture to another. The source/dest region
+ // is always within the texture bounds. Areas outside the dest region are
+ // preserved. The formats of the textures must be loosely compatible. The
+ // dst texture can be a swapchain framebuffer, but src can not. Only 2D
+ // textures are supported.
+ // The textures must have blit_src and blit_dst set, respectively.
+ // Rectangles with negative width/height lead to flipping, different src/dst
+ // sizes lead to point scaling. Coordinates are always in pixels.
+ // Optional. Only available if RA_CAP_BLIT is set (if it's not set, it must
+ // not be called, even if it's non-NULL).
+ void (*blit)(struct ra *ra, struct ra_tex *dst, struct ra_tex *src,
+ struct mp_rect *dst_rc, struct mp_rect *src_rc);
+
+ // Compile a shader and create a pipeline. This is a rare operation.
+ // The params pointer and anything it points to must stay valid until
+ // renderpass_destroy.
+ struct ra_renderpass *(*renderpass_create)(struct ra *ra,
+ const struct ra_renderpass_params *params);
+
+ void (*renderpass_destroy)(struct ra *ra, struct ra_renderpass *pass);
+
+ // Perform a render pass, basically drawing a list of triangles to a FBO.
+ // This is an extremely common operation.
+ void (*renderpass_run)(struct ra *ra,
+ const struct ra_renderpass_run_params *params);
+
+ // Create a timer object. Returns NULL on failure, or if timers are
+ // unavailable for some reason. Optional.
+ ra_timer *(*timer_create)(struct ra *ra);
+
+ void (*timer_destroy)(struct ra *ra, ra_timer *timer);
+
+ // Start recording a timer. Note that valid usage requires you to pair
+ // every start with a stop. Trying to start a timer twice, or trying to
+ // stop a timer before having started it, consistutes invalid usage.
+ void (*timer_start)(struct ra *ra, ra_timer *timer);
+
+ // Stop recording a timer. This also returns any results that have been
+ // measured since the last usage of this ra_timer. It's important to note
+ // that GPU timer measurement are asynchronous, so this function does not
+ // always produce a value - and the values it does produce are typically
+ // delayed by a few frames. When no value is available, this returns 0.
+ uint64_t (*timer_stop)(struct ra *ra, ra_timer *timer);
+
+ // Associates a marker with any past error messages, for debugging
+ // purposes. Optional.
+ void (*debug_marker)(struct ra *ra, const char *msg);
+};
+
+struct ra_tex *ra_tex_create(struct ra *ra, const struct ra_tex_params *params);
+void ra_tex_free(struct ra *ra, struct ra_tex **tex);
+
+struct ra_buf *ra_buf_create(struct ra *ra, const struct ra_buf_params *params);
+void ra_buf_free(struct ra *ra, struct ra_buf **buf);
+
+void ra_free(struct ra **ra);
+
+const struct ra_format *ra_find_unorm_format(struct ra *ra,
+ int bytes_per_component,
+ int n_components);
+const struct ra_format *ra_find_uint_format(struct ra *ra,
+ int bytes_per_component,
+ int n_components);
+const struct ra_format *ra_find_float16_format(struct ra *ra, int n_components);
+const struct ra_format *ra_find_named_format(struct ra *ra, const char *name);
+
+struct ra_imgfmt_desc {
+ int num_planes;
+ const struct ra_format *planes[4];
+ // Chroma pixel size (1x1 is 4:4:4)
+ uint8_t chroma_w, chroma_h;
+ // Component storage size in bits (possibly padded). For formats with
+ // different sizes per component, this is arbitrary. For padded formats
+ // like P010 or YUV420P10, padding is included.
+ int component_bits;
+ // Like mp_regular_imgfmt.component_pad.
+ int component_pad;
+ // == planes[n].ctype (RA_CTYPE_UNKNOWN if not applicable)
+ enum ra_ctype component_type;
+ // For each texture and each texture output (rgba order) describe what
+ // component it returns.
+ // The values are like the values in mp_regular_imgfmt_plane.components[].
+ // Access as components[plane_nr][component_index]. Set unused items to 0.
+ // For ra_format.luminance_alpha, this returns 1/2 ("rg") instead of 1/4
+ // ("ra"). the logic is that the texture format has 2 channels, thus the
+ // data must be returned in the first two components. The renderer fixes
+ // this later.
+ uint8_t components[4][4];
+};
+
+const char *ra_fmt_glsl_format(const struct ra_format *fmt);
+
+bool ra_get_imgfmt_desc(struct ra *ra, int imgfmt, struct ra_imgfmt_desc *out);
+
+void ra_dump_tex_formats(struct ra *ra, int msgl);
+void ra_dump_imgfmt_desc(struct ra *ra, const struct ra_imgfmt_desc *desc,
+ int msgl);
+void ra_dump_img_formats(struct ra *ra, int msgl);
diff --git a/video/out/gpu/shader_cache.c b/video/out/gpu/shader_cache.c
new file mode 100644
index 0000000..3e05173
--- /dev/null
+++ b/video/out/gpu/shader_cache.c
@@ -0,0 +1,1056 @@
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <assert.h>
+
+#include <libavutil/sha.h>
+#include <libavutil/mem.h>
+
+#include "osdep/io.h"
+
+#include "common/common.h"
+#include "options/path.h"
+#include "stream/stream.h"
+#include "shader_cache.h"
+#include "utils.h"
+
+// Force cache flush if more than this number of shaders is created.
+#define SC_MAX_ENTRIES 256
+
+union uniform_val {
+ float f[9]; // RA_VARTYPE_FLOAT
+ int i[4]; // RA_VARTYPE_INT
+ struct ra_tex *tex; // RA_VARTYPE_TEX, RA_VARTYPE_IMG_*
+ struct ra_buf *buf; // RA_VARTYPE_BUF_*
+};
+
+enum sc_uniform_type {
+ SC_UNIFORM_TYPE_GLOBAL = 0, // global uniform (RA_CAP_GLOBAL_UNIFORM)
+ SC_UNIFORM_TYPE_UBO = 1, // uniform buffer (RA_CAP_BUF_RO)
+ SC_UNIFORM_TYPE_PUSHC = 2, // push constant (ra.max_pushc_size)
+};
+
+struct sc_uniform {
+ enum sc_uniform_type type;
+ struct ra_renderpass_input input;
+ const char *glsl_type;
+ union uniform_val v;
+ char *buffer_format;
+ // for SC_UNIFORM_TYPE_UBO/PUSHC:
+ struct ra_layout layout;
+ size_t offset; // byte offset within the buffer
+};
+
+struct sc_cached_uniform {
+ union uniform_val v;
+ int index; // for ra_renderpass_input_val
+ bool set; // whether the uniform has ever been set
+};
+
+struct sc_entry {
+ struct ra_renderpass *pass;
+ struct sc_cached_uniform *cached_uniforms;
+ int num_cached_uniforms;
+ bstr total;
+ struct timer_pool *timer;
+ struct ra_buf *ubo;
+ int ubo_index; // for ra_renderpass_input_val.index
+ void *pushc;
+};
+
+struct gl_shader_cache {
+ struct ra *ra;
+ struct mp_log *log;
+
+ // permanent
+ char **exts;
+ int num_exts;
+
+ // this is modified during use (gl_sc_add() etc.) and reset for each shader
+ bstr prelude_text;
+ bstr header_text;
+ bstr text;
+
+ // Next binding point (texture unit, image unit, buffer binding, etc.)
+ // In OpenGL these are separate for each input type
+ int next_binding[RA_VARTYPE_COUNT];
+ bool next_uniform_dynamic;
+
+ struct ra_renderpass_params params;
+
+ struct sc_entry **entries;
+ int num_entries;
+
+ struct sc_entry *current_shader; // set by gl_sc_generate()
+
+ struct sc_uniform *uniforms;
+ int num_uniforms;
+
+ int ubo_binding;
+ size_t ubo_size;
+ size_t pushc_size;
+
+ struct ra_renderpass_input_val *values;
+ int num_values;
+
+ // For checking that the user is calling gl_sc_reset() properly.
+ bool needs_reset;
+
+ bool error_state; // true if an error occurred
+
+ // temporary buffers (avoids frequent reallocations)
+ bstr tmp[6];
+
+ // For the disk-cache.
+ char *cache_dir;
+ struct mpv_global *global; // can be NULL
+};
+
+struct gl_shader_cache *gl_sc_create(struct ra *ra, struct mpv_global *global,
+ struct mp_log *log)
+{
+ struct gl_shader_cache *sc = talloc_ptrtype(NULL, sc);
+ *sc = (struct gl_shader_cache){
+ .ra = ra,
+ .global = global,
+ .log = log,
+ };
+ gl_sc_reset(sc);
+ return sc;
+}
+
+// Reset the previous pass. This must be called after gl_sc_generate and before
+// starting a new shader. It may also be called on errors.
+void gl_sc_reset(struct gl_shader_cache *sc)
+{
+ sc->prelude_text.len = 0;
+ sc->header_text.len = 0;
+ sc->text.len = 0;
+ for (int n = 0; n < sc->num_uniforms; n++)
+ talloc_free((void *)sc->uniforms[n].input.name);
+ sc->num_uniforms = 0;
+ sc->ubo_binding = 0;
+ sc->ubo_size = 0;
+ sc->pushc_size = 0;
+ for (int i = 0; i < RA_VARTYPE_COUNT; i++)
+ sc->next_binding[i] = 0;
+ sc->next_uniform_dynamic = false;
+ sc->current_shader = NULL;
+ sc->params = (struct ra_renderpass_params){0};
+ sc->needs_reset = false;
+}
+
+static void sc_flush_cache(struct gl_shader_cache *sc)
+{
+ MP_DBG(sc, "flushing shader cache\n");
+
+ for (int n = 0; n < sc->num_entries; n++) {
+ struct sc_entry *e = sc->entries[n];
+ ra_buf_free(sc->ra, &e->ubo);
+ if (e->pass)
+ sc->ra->fns->renderpass_destroy(sc->ra, e->pass);
+ timer_pool_destroy(e->timer);
+ talloc_free(e);
+ }
+ sc->num_entries = 0;
+}
+
+void gl_sc_destroy(struct gl_shader_cache *sc)
+{
+ if (!sc)
+ return;
+ gl_sc_reset(sc);
+ sc_flush_cache(sc);
+ talloc_free(sc);
+}
+
+bool gl_sc_error_state(struct gl_shader_cache *sc)
+{
+ return sc->error_state;
+}
+
+void gl_sc_reset_error(struct gl_shader_cache *sc)
+{
+ sc->error_state = false;
+}
+
+void gl_sc_enable_extension(struct gl_shader_cache *sc, char *name)
+{
+ for (int n = 0; n < sc->num_exts; n++) {
+ if (strcmp(sc->exts[n], name) == 0)
+ return;
+ }
+ MP_TARRAY_APPEND(sc, sc->exts, sc->num_exts, talloc_strdup(sc, name));
+}
+
+#define bstr_xappend0(sc, b, s) bstr_xappend(sc, b, bstr0(s))
+
+void gl_sc_add(struct gl_shader_cache *sc, const char *text)
+{
+ bstr_xappend0(sc, &sc->text, text);
+}
+
+void gl_sc_addf(struct gl_shader_cache *sc, const char *textf, ...)
+{
+ va_list ap;
+ va_start(ap, textf);
+ bstr_xappend_vasprintf(sc, &sc->text, textf, ap);
+ va_end(ap);
+}
+
+void gl_sc_hadd(struct gl_shader_cache *sc, const char *text)
+{
+ bstr_xappend0(sc, &sc->header_text, text);
+}
+
+void gl_sc_haddf(struct gl_shader_cache *sc, const char *textf, ...)
+{
+ va_list ap;
+ va_start(ap, textf);
+ bstr_xappend_vasprintf(sc, &sc->header_text, textf, ap);
+ va_end(ap);
+}
+
+void gl_sc_hadd_bstr(struct gl_shader_cache *sc, struct bstr text)
+{
+ bstr_xappend(sc, &sc->header_text, text);
+}
+
+void gl_sc_paddf(struct gl_shader_cache *sc, const char *textf, ...)
+{
+ va_list ap;
+ va_start(ap, textf);
+ bstr_xappend_vasprintf(sc, &sc->prelude_text, textf, ap);
+ va_end(ap);
+}
+
+static struct sc_uniform *find_uniform(struct gl_shader_cache *sc,
+ const char *name)
+{
+ struct sc_uniform new = {
+ .input = {
+ .dim_v = 1,
+ .dim_m = 1,
+ },
+ };
+
+ for (int n = 0; n < sc->num_uniforms; n++) {
+ struct sc_uniform *u = &sc->uniforms[n];
+ if (strcmp(u->input.name, name) == 0) {
+ const char *allocname = u->input.name;
+ *u = new;
+ u->input.name = allocname;
+ return u;
+ }
+ }
+
+ // not found -> add it
+ new.input.name = talloc_strdup(NULL, name);
+ MP_TARRAY_APPEND(sc, sc->uniforms, sc->num_uniforms, new);
+ return &sc->uniforms[sc->num_uniforms - 1];
+}
+
+static int gl_sc_next_binding(struct gl_shader_cache *sc, enum ra_vartype type)
+{
+ return sc->next_binding[sc->ra->fns->desc_namespace(sc->ra, type)]++;
+}
+
+void gl_sc_uniform_dynamic(struct gl_shader_cache *sc)
+{
+ sc->next_uniform_dynamic = true;
+}
+
+// Updates the metadata for the given sc_uniform. Assumes sc_uniform->input
+// and glsl_type/buffer_format are already set.
+static void update_uniform_params(struct gl_shader_cache *sc, struct sc_uniform *u)
+{
+ bool dynamic = sc->next_uniform_dynamic;
+ sc->next_uniform_dynamic = false;
+
+ // Try not using push constants for "large" values like matrices, since
+ // this is likely to both exceed the VGPR budget as well as the pushc size
+ // budget
+ bool try_pushc = u->input.dim_m == 1 || dynamic;
+
+ // Attempt using push constants first
+ if (try_pushc && sc->ra->glsl_vulkan && sc->ra->max_pushc_size) {
+ struct ra_layout layout = sc->ra->fns->push_constant_layout(&u->input);
+ size_t offset = MP_ALIGN_UP(sc->pushc_size, layout.align);
+ // Push constants have limited size, so make sure we don't exceed this
+ size_t new_size = offset + layout.size;
+ if (new_size <= sc->ra->max_pushc_size) {
+ u->type = SC_UNIFORM_TYPE_PUSHC;
+ u->layout = layout;
+ u->offset = offset;
+ sc->pushc_size = new_size;
+ return;
+ }
+ }
+
+ // Attempt using uniform buffer next. The GLSL version 440 check is due
+ // to explicit offsets on UBO entries. In theory we could leave away
+ // the offsets and support UBOs for older GL as well, but this is a nice
+ // safety net for driver bugs (and also rules out potentially buggy drivers)
+ // Also avoid UBOs for highly dynamic stuff since that requires synchronizing
+ // the UBO writes every frame
+ bool try_ubo = !(sc->ra->caps & RA_CAP_GLOBAL_UNIFORM) || !dynamic;
+ if (try_ubo && sc->ra->glsl_version >= 440 && (sc->ra->caps & RA_CAP_BUF_RO)) {
+ u->type = SC_UNIFORM_TYPE_UBO;
+ u->layout = sc->ra->fns->uniform_layout(&u->input);
+ u->offset = MP_ALIGN_UP(sc->ubo_size, u->layout.align);
+ sc->ubo_size = u->offset + u->layout.size;
+ return;
+ }
+
+ // If all else fails, use global uniforms
+ assert(sc->ra->caps & RA_CAP_GLOBAL_UNIFORM);
+ u->type = SC_UNIFORM_TYPE_GLOBAL;
+}
+
+void gl_sc_uniform_texture(struct gl_shader_cache *sc, char *name,
+ struct ra_tex *tex)
+{
+ const char *glsl_type = "sampler2D";
+ if (tex->params.dimensions == 1) {
+ glsl_type = "sampler1D";
+ } else if (tex->params.dimensions == 3) {
+ glsl_type = "sampler3D";
+ } else if (tex->params.non_normalized) {
+ glsl_type = "sampler2DRect";
+ } else if (tex->params.external_oes) {
+ glsl_type = "samplerExternalOES";
+ } else if (tex->params.format->ctype == RA_CTYPE_UINT) {
+ glsl_type = sc->ra->glsl_es ? "highp usampler2D" : "usampler2D";
+ }
+
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_TEX;
+ u->glsl_type = glsl_type;
+ u->input.binding = gl_sc_next_binding(sc, u->input.type);
+ u->v.tex = tex;
+}
+
+void gl_sc_uniform_image2D_wo(struct gl_shader_cache *sc, const char *name,
+ struct ra_tex *tex)
+{
+ gl_sc_enable_extension(sc, "GL_ARB_shader_image_load_store");
+
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_IMG_W;
+ u->glsl_type = sc->ra->glsl_es ? "writeonly highp image2D" : "writeonly image2D";
+ u->input.binding = gl_sc_next_binding(sc, u->input.type);
+ u->v.tex = tex;
+}
+
+void gl_sc_ssbo(struct gl_shader_cache *sc, char *name, struct ra_buf *buf,
+ char *format, ...)
+{
+ assert(sc->ra->caps & RA_CAP_BUF_RW);
+ gl_sc_enable_extension(sc, "GL_ARB_shader_storage_buffer_object");
+
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_BUF_RW;
+ u->glsl_type = "";
+ u->input.binding = gl_sc_next_binding(sc, u->input.type);
+ u->v.buf = buf;
+
+ va_list ap;
+ va_start(ap, format);
+ u->buffer_format = ta_vasprintf(sc, format, ap);
+ va_end(ap);
+}
+
+void gl_sc_uniform_f(struct gl_shader_cache *sc, char *name, float f)
+{
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_FLOAT;
+ u->glsl_type = "float";
+ update_uniform_params(sc, u);
+ u->v.f[0] = f;
+}
+
+void gl_sc_uniform_i(struct gl_shader_cache *sc, char *name, int i)
+{
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_INT;
+ u->glsl_type = "int";
+ update_uniform_params(sc, u);
+ u->v.i[0] = i;
+}
+
+void gl_sc_uniform_vec2(struct gl_shader_cache *sc, char *name, float f[2])
+{
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_FLOAT;
+ u->input.dim_v = 2;
+ u->glsl_type = "vec2";
+ update_uniform_params(sc, u);
+ u->v.f[0] = f[0];
+ u->v.f[1] = f[1];
+}
+
+void gl_sc_uniform_vec3(struct gl_shader_cache *sc, char *name, float f[3])
+{
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_FLOAT;
+ u->input.dim_v = 3;
+ u->glsl_type = "vec3";
+ update_uniform_params(sc, u);
+ u->v.f[0] = f[0];
+ u->v.f[1] = f[1];
+ u->v.f[2] = f[2];
+}
+
+static void transpose2x2(float r[2 * 2])
+{
+ MPSWAP(float, r[0+2*1], r[1+2*0]);
+}
+
+void gl_sc_uniform_mat2(struct gl_shader_cache *sc, char *name,
+ bool transpose, float *v)
+{
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_FLOAT;
+ u->input.dim_v = 2;
+ u->input.dim_m = 2;
+ u->glsl_type = "mat2";
+ update_uniform_params(sc, u);
+ for (int n = 0; n < 4; n++)
+ u->v.f[n] = v[n];
+ if (transpose)
+ transpose2x2(&u->v.f[0]);
+}
+
+static void transpose3x3(float r[3 * 3])
+{
+ MPSWAP(float, r[0+3*1], r[1+3*0]);
+ MPSWAP(float, r[0+3*2], r[2+3*0]);
+ MPSWAP(float, r[1+3*2], r[2+3*1]);
+}
+
+void gl_sc_uniform_mat3(struct gl_shader_cache *sc, char *name,
+ bool transpose, float *v)
+{
+ struct sc_uniform *u = find_uniform(sc, name);
+ u->input.type = RA_VARTYPE_FLOAT;
+ u->input.dim_v = 3;
+ u->input.dim_m = 3;
+ u->glsl_type = "mat3";
+ update_uniform_params(sc, u);
+ for (int n = 0; n < 9; n++)
+ u->v.f[n] = v[n];
+ if (transpose)
+ transpose3x3(&u->v.f[0]);
+}
+
+void gl_sc_blend(struct gl_shader_cache *sc,
+ enum ra_blend blend_src_rgb,
+ enum ra_blend blend_dst_rgb,
+ enum ra_blend blend_src_alpha,
+ enum ra_blend blend_dst_alpha)
+{
+ sc->params.enable_blend = true;
+ sc->params.blend_src_rgb = blend_src_rgb;
+ sc->params.blend_dst_rgb = blend_dst_rgb;
+ sc->params.blend_src_alpha = blend_src_alpha;
+ sc->params.blend_dst_alpha = blend_dst_alpha;
+}
+
+const char *gl_sc_bvec(struct gl_shader_cache *sc, int dims)
+{
+ static const char *bvecs[] = {
+ [1] = "bool",
+ [2] = "bvec2",
+ [3] = "bvec3",
+ [4] = "bvec4",
+ };
+
+ static const char *vecs[] = {
+ [1] = "float",
+ [2] = "vec2",
+ [3] = "vec3",
+ [4] = "vec4",
+ };
+
+ assert(dims > 0 && dims < MP_ARRAY_SIZE(bvecs));
+ return sc->ra->glsl_version >= 130 ? bvecs[dims] : vecs[dims];
+}
+
+static const char *vao_glsl_type(const struct ra_renderpass_input *e)
+{
+ // pretty dumb... too dumb, but works for us
+ switch (e->dim_v) {
+ case 1: return "float";
+ case 2: return "vec2";
+ case 3: return "vec3";
+ case 4: return "vec4";
+ default: MP_ASSERT_UNREACHABLE();
+ }
+}
+
+static void update_ubo(struct ra *ra, struct ra_buf *ubo, struct sc_uniform *u)
+{
+ uintptr_t src = (uintptr_t) &u->v;
+ size_t dst = u->offset;
+ struct ra_layout src_layout = ra_renderpass_input_layout(&u->input);
+ struct ra_layout dst_layout = u->layout;
+
+ for (int i = 0; i < u->input.dim_m; i++) {
+ ra->fns->buf_update(ra, ubo, dst, (void *)src, src_layout.stride);
+ src += src_layout.stride;
+ dst += dst_layout.stride;
+ }
+}
+
+static void update_pushc(struct ra *ra, void *pushc, struct sc_uniform *u)
+{
+ uintptr_t src = (uintptr_t) &u->v;
+ uintptr_t dst = (uintptr_t) pushc + (ptrdiff_t) u->offset;
+ struct ra_layout src_layout = ra_renderpass_input_layout(&u->input);
+ struct ra_layout dst_layout = u->layout;
+
+ for (int i = 0; i < u->input.dim_m; i++) {
+ memcpy((void *)dst, (void *)src, src_layout.stride);
+ src += src_layout.stride;
+ dst += dst_layout.stride;
+ }
+}
+
+static void update_uniform(struct gl_shader_cache *sc, struct sc_entry *e,
+ struct sc_uniform *u, int n)
+{
+ struct sc_cached_uniform *un = &e->cached_uniforms[n];
+ struct ra_layout layout = ra_renderpass_input_layout(&u->input);
+ if (layout.size > 0 && un->set && memcmp(&un->v, &u->v, layout.size) == 0)
+ return;
+
+ un->v = u->v;
+ un->set = true;
+
+ static const char *desc[] = {
+ [SC_UNIFORM_TYPE_UBO] = "UBO",
+ [SC_UNIFORM_TYPE_PUSHC] = "PC",
+ [SC_UNIFORM_TYPE_GLOBAL] = "global",
+ };
+ MP_TRACE(sc, "Updating %s uniform '%s'\n", desc[u->type], u->input.name);
+
+ switch (u->type) {
+ case SC_UNIFORM_TYPE_GLOBAL: {
+ struct ra_renderpass_input_val value = {
+ .index = un->index,
+ .data = &un->v,
+ };
+ MP_TARRAY_APPEND(sc, sc->values, sc->num_values, value);
+ break;
+ }
+ case SC_UNIFORM_TYPE_UBO:
+ assert(e->ubo);
+ update_ubo(sc->ra, e->ubo, u);
+ break;
+ case SC_UNIFORM_TYPE_PUSHC:
+ assert(e->pushc);
+ update_pushc(sc->ra, e->pushc, u);
+ break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+}
+
+void gl_sc_set_cache_dir(struct gl_shader_cache *sc, char *dir)
+{
+ talloc_free(sc->cache_dir);
+ if (dir && dir[0]) {
+ dir = mp_get_user_path(NULL, sc->global, dir);
+ } else {
+ dir = mp_find_user_file(NULL, sc->global, "cache", "");
+ }
+ sc->cache_dir = talloc_strdup(sc, dir);
+ talloc_free(dir);
+}
+
+static bool create_pass(struct gl_shader_cache *sc, struct sc_entry *entry)
+{
+ bool ret = false;
+
+ void *tmp = talloc_new(NULL);
+ struct ra_renderpass_params params = sc->params;
+
+ const char *cache_header = "mpv shader cache v1\n";
+ char *cache_filename = NULL;
+ char *cache_dir = NULL;
+
+ if (sc->cache_dir && sc->cache_dir[0]) {
+ // Try to load it from a disk cache.
+ cache_dir = mp_get_user_path(tmp, sc->global, sc->cache_dir);
+
+ struct AVSHA *sha = av_sha_alloc();
+ MP_HANDLE_OOM(sha);
+ av_sha_init(sha, 256);
+ av_sha_update(sha, entry->total.start, entry->total.len);
+
+ uint8_t hash[256 / 8];
+ av_sha_final(sha, hash);
+ av_free(sha);
+
+ char hashstr[256 / 8 * 2 + 1];
+ for (int n = 0; n < 256 / 8; n++)
+ snprintf(hashstr + n * 2, sizeof(hashstr) - n * 2, "%02X", hash[n]);
+
+ cache_filename = mp_path_join(tmp, cache_dir, hashstr);
+ if (stat(cache_filename, &(struct stat){0}) == 0) {
+ MP_DBG(sc, "Trying to load shader from disk...\n");
+ struct bstr cachedata =
+ stream_read_file(cache_filename, tmp, sc->global, 1000000000);
+ if (bstr_eatstart0(&cachedata, cache_header))
+ params.cached_program = cachedata;
+ }
+ }
+
+ // If using a UBO, also make sure to add it as an input value so the RA
+ // can see it
+ if (sc->ubo_size) {
+ entry->ubo_index = sc->params.num_inputs;
+ struct ra_renderpass_input ubo_input = {
+ .name = "UBO",
+ .type = RA_VARTYPE_BUF_RO,
+ .dim_v = 1,
+ .dim_m = 1,
+ .binding = sc->ubo_binding,
+ };
+ MP_TARRAY_APPEND(sc, params.inputs, params.num_inputs, ubo_input);
+ }
+
+ if (sc->pushc_size) {
+ params.push_constants_size = MP_ALIGN_UP(sc->pushc_size, 4);
+ entry->pushc = talloc_zero_size(entry, params.push_constants_size);
+ }
+
+ if (sc->ubo_size) {
+ struct ra_buf_params ubo_params = {
+ .type = RA_BUF_TYPE_UNIFORM,
+ .size = sc->ubo_size,
+ .host_mutable = true,
+ };
+
+ entry->ubo = ra_buf_create(sc->ra, &ubo_params);
+ if (!entry->ubo) {
+ MP_ERR(sc, "Failed creating uniform buffer!\n");
+ goto error;
+ }
+ }
+
+ entry->pass = sc->ra->fns->renderpass_create(sc->ra, &params);
+ if (!entry->pass)
+ goto error;
+
+ if (entry->pass && cache_filename) {
+ bstr nc = entry->pass->params.cached_program;
+ if (nc.len && !bstr_equals(params.cached_program, nc)) {
+ mp_mkdirp(cache_dir);
+
+ MP_DBG(sc, "Writing shader cache file: %s\n", cache_filename);
+ FILE *out = fopen(cache_filename, "wb");
+ if (out) {
+ fwrite(cache_header, strlen(cache_header), 1, out);
+ fwrite(nc.start, nc.len, 1, out);
+ fclose(out);
+ }
+ }
+ }
+
+ ret = true;
+
+error:
+ talloc_free(tmp);
+ return ret;
+}
+
+#define ADD(x, ...) bstr_xappend_asprintf(sc, (x), __VA_ARGS__)
+#define ADD_BSTR(x, s) bstr_xappend(sc, (x), (s))
+
+static void add_uniforms(struct gl_shader_cache *sc, bstr *dst)
+{
+ // Add all of the UBO entries separately as members of their own buffer
+ if (sc->ubo_size > 0) {
+ ADD(dst, "layout(std140, binding=%d) uniform UBO {\n", sc->ubo_binding);
+ for (int n = 0; n < sc->num_uniforms; n++) {
+ struct sc_uniform *u = &sc->uniforms[n];
+ if (u->type != SC_UNIFORM_TYPE_UBO)
+ continue;
+ ADD(dst, "layout(offset=%zu) %s %s;\n", u->offset, u->glsl_type,
+ u->input.name);
+ }
+ ADD(dst, "};\n");
+ }
+
+ // Ditto for push constants
+ if (sc->pushc_size > 0) {
+ ADD(dst, "layout(std430, push_constant) uniform PushC {\n");
+ for (int n = 0; n < sc->num_uniforms; n++) {
+ struct sc_uniform *u = &sc->uniforms[n];
+ if (u->type != SC_UNIFORM_TYPE_PUSHC)
+ continue;
+ ADD(dst, "layout(offset=%zu) %s %s;\n", u->offset, u->glsl_type,
+ u->input.name);
+ }
+ ADD(dst, "};\n");
+ }
+
+ for (int n = 0; n < sc->num_uniforms; n++) {
+ struct sc_uniform *u = &sc->uniforms[n];
+ if (u->type != SC_UNIFORM_TYPE_GLOBAL)
+ continue;
+ switch (u->input.type) {
+ case RA_VARTYPE_INT:
+ case RA_VARTYPE_FLOAT:
+ assert(sc->ra->caps & RA_CAP_GLOBAL_UNIFORM);
+ MP_FALLTHROUGH;
+ case RA_VARTYPE_TEX:
+ // Vulkan requires explicitly assigning the bindings in the shader
+ // source. For OpenGL it's optional, but requires higher GL version
+ // so we don't do it (and instead have ra_gl update the bindings
+ // after program creation).
+ if (sc->ra->glsl_vulkan)
+ ADD(dst, "layout(binding=%d) ", u->input.binding);
+ ADD(dst, "uniform %s %s;\n", u->glsl_type, u->input.name);
+ break;
+ case RA_VARTYPE_BUF_RO:
+ ADD(dst, "layout(std140, binding=%d) uniform %s { %s };\n",
+ u->input.binding, u->input.name, u->buffer_format);
+ break;
+ case RA_VARTYPE_BUF_RW:
+ ADD(dst, "layout(std430, binding=%d) restrict coherent buffer %s { %s };\n",
+ u->input.binding, u->input.name, u->buffer_format);
+ break;
+ case RA_VARTYPE_IMG_W: {
+ // For better compatibility, we have to explicitly label the
+ // type of data we will be reading/writing to this image.
+ const char *fmt = u->v.tex->params.format->glsl_format;
+
+ if (sc->ra->glsl_vulkan) {
+ if (fmt) {
+ ADD(dst, "layout(binding=%d, %s) ", u->input.binding, fmt);
+ } else {
+ ADD(dst, "layout(binding=%d) ", u->input.binding);
+ }
+ } else if (fmt) {
+ ADD(dst, "layout(%s) ", fmt);
+ }
+ ADD(dst, "uniform restrict %s %s;\n", u->glsl_type, u->input.name);
+ }
+ }
+ }
+}
+
+// 1. Generate vertex and fragment shaders from the fragment shader text added
+// with gl_sc_add(). The generated shader program is cached (based on the
+// text), so actual compilation happens only the first time.
+// 2. Update the uniforms and textures set with gl_sc_uniform_*.
+// 3. Make the new shader program current (glUseProgram()).
+// After that, you render, and then you call gc_sc_reset(), which does:
+// 1. Unbind the program and all textures.
+// 2. Reset the sc state and prepare for a new shader program. (All uniforms
+// and fragment operations needed for the next program have to be re-added.)
+static void gl_sc_generate(struct gl_shader_cache *sc,
+ enum ra_renderpass_type type,
+ const struct ra_format *target_format,
+ const struct ra_renderpass_input *vao,
+ int vao_len, size_t vertex_stride)
+{
+ int glsl_version = sc->ra->glsl_version;
+ int glsl_es = sc->ra->glsl_es ? glsl_version : 0;
+
+ sc->params.type = type;
+
+ // gl_sc_reset() must be called after ending the previous render process,
+ // and before starting a new one.
+ assert(!sc->needs_reset);
+ sc->needs_reset = true;
+
+ // If using a UBO, pick a binding (needed for shader generation)
+ if (sc->ubo_size)
+ sc->ubo_binding = gl_sc_next_binding(sc, RA_VARTYPE_BUF_RO);
+
+ for (int n = 0; n < MP_ARRAY_SIZE(sc->tmp); n++)
+ sc->tmp[n].len = 0;
+
+ // set up shader text (header + uniforms + body)
+ bstr *header = &sc->tmp[0];
+ ADD(header, "#version %d%s\n", glsl_version, glsl_es >= 300 ? " es" : "");
+ if (type == RA_RENDERPASS_TYPE_COMPUTE) {
+ // This extension cannot be enabled in fragment shader. Enable it as
+ // an exception for compute shader.
+ ADD(header, "#extension GL_ARB_compute_shader : enable\n");
+ }
+ for (int n = 0; n < sc->num_exts; n++)
+ ADD(header, "#extension %s : enable\n", sc->exts[n]);
+ if (glsl_es) {
+ ADD(header, "#ifdef GL_FRAGMENT_PRECISION_HIGH\n");
+ ADD(header, "precision highp float;\n");
+ ADD(header, "#else\n");
+ ADD(header, "precision mediump float;\n");
+ ADD(header, "#endif\n");
+
+ ADD(header, "precision mediump sampler2D;\n");
+ if (sc->ra->caps & RA_CAP_TEX_3D)
+ ADD(header, "precision mediump sampler3D;\n");
+ }
+
+ if (glsl_version >= 130) {
+ ADD(header, "#define tex1D texture\n");
+ ADD(header, "#define tex3D texture\n");
+ } else {
+ ADD(header, "#define tex1D texture1D\n");
+ ADD(header, "#define tex3D texture3D\n");
+ ADD(header, "#define texture texture2D\n");
+ }
+
+ // Additional helpers.
+ ADD(header, "#define LUT_POS(x, lut_size)"
+ " mix(0.5 / (lut_size), 1.0 - 0.5 / (lut_size), (x))\n");
+
+ char *vert_in = glsl_version >= 130 ? "in" : "attribute";
+ char *vert_out = glsl_version >= 130 ? "out" : "varying";
+ char *frag_in = glsl_version >= 130 ? "in" : "varying";
+
+ struct bstr *vert = NULL, *frag = NULL, *comp = NULL;
+
+ if (type == RA_RENDERPASS_TYPE_RASTER) {
+ // vertex shader: we don't use the vertex shader, so just setup a
+ // dummy, which passes through the vertex array attributes.
+ bstr *vert_head = &sc->tmp[1];
+ ADD_BSTR(vert_head, *header);
+ bstr *vert_body = &sc->tmp[2];
+ ADD(vert_body, "void main() {\n");
+ bstr *frag_vaos = &sc->tmp[3];
+ for (int n = 0; n < vao_len; n++) {
+ const struct ra_renderpass_input *e = &vao[n];
+ const char *glsl_type = vao_glsl_type(e);
+ char loc[32] = {0};
+ if (sc->ra->glsl_vulkan)
+ snprintf(loc, sizeof(loc), "layout(location=%d) ", n);
+ if (strcmp(e->name, "position") == 0) {
+ // setting raster pos. requires setting gl_Position magic variable
+ assert(e->dim_v == 2 && e->type == RA_VARTYPE_FLOAT);
+ ADD(vert_head, "%s%s vec2 vertex_position;\n", loc, vert_in);
+ ADD(vert_body, "gl_Position = vec4(vertex_position, 1.0, 1.0);\n");
+ } else {
+ ADD(vert_head, "%s%s %s vertex_%s;\n", loc, vert_in, glsl_type, e->name);
+ ADD(vert_head, "%s%s %s %s;\n", loc, vert_out, glsl_type, e->name);
+ ADD(vert_body, "%s = vertex_%s;\n", e->name, e->name);
+ ADD(frag_vaos, "%s%s %s %s;\n", loc, frag_in, glsl_type, e->name);
+ }
+ }
+ ADD(vert_body, "}\n");
+ vert = vert_head;
+ ADD_BSTR(vert, *vert_body);
+
+ // fragment shader; still requires adding used uniforms and VAO elements
+ frag = &sc->tmp[4];
+ ADD_BSTR(frag, *header);
+ if (glsl_version >= 130) {
+ ADD(frag, "%sout vec4 out_color;\n",
+ sc->ra->glsl_vulkan ? "layout(location=0) " : "");
+ }
+ ADD_BSTR(frag, *frag_vaos);
+ add_uniforms(sc, frag);
+
+ ADD_BSTR(frag, sc->prelude_text);
+ ADD_BSTR(frag, sc->header_text);
+
+ ADD(frag, "void main() {\n");
+ // we require _all_ frag shaders to write to a "vec4 color"
+ ADD(frag, "vec4 color = vec4(0.0, 0.0, 0.0, 1.0);\n");
+ ADD_BSTR(frag, sc->text);
+ if (glsl_version >= 130) {
+ ADD(frag, "out_color = color;\n");
+ } else {
+ ADD(frag, "gl_FragColor = color;\n");
+ }
+ ADD(frag, "}\n");
+
+ // We need to fix the format of the render dst at renderpass creation
+ // time
+ assert(target_format);
+ sc->params.target_format = target_format;
+ }
+
+ if (type == RA_RENDERPASS_TYPE_COMPUTE) {
+ comp = &sc->tmp[4];
+ ADD_BSTR(comp, *header);
+
+ add_uniforms(sc, comp);
+
+ ADD_BSTR(comp, sc->prelude_text);
+ ADD_BSTR(comp, sc->header_text);
+
+ ADD(comp, "void main() {\n");
+ ADD(comp, "vec4 color = vec4(0.0, 0.0, 0.0, 1.0);\n"); // convenience
+ ADD_BSTR(comp, sc->text);
+ ADD(comp, "}\n");
+ }
+
+ bstr *hash_total = &sc->tmp[5];
+
+ ADD(hash_total, "type %d\n", sc->params.type);
+
+ if (frag) {
+ ADD_BSTR(hash_total, *frag);
+ sc->params.frag_shader = frag->start;
+ }
+ ADD(hash_total, "\n");
+ if (vert) {
+ ADD_BSTR(hash_total, *vert);
+ sc->params.vertex_shader = vert->start;
+ }
+ ADD(hash_total, "\n");
+ if (comp) {
+ ADD_BSTR(hash_total, *comp);
+ sc->params.compute_shader = comp->start;
+ }
+ ADD(hash_total, "\n");
+
+ if (sc->params.enable_blend) {
+ ADD(hash_total, "blend %d %d %d %d\n",
+ sc->params.blend_src_rgb, sc->params.blend_dst_rgb,
+ sc->params.blend_src_alpha, sc->params.blend_dst_alpha);
+ }
+
+ if (sc->params.target_format)
+ ADD(hash_total, "format %s\n", sc->params.target_format->name);
+
+ struct sc_entry *entry = NULL;
+ for (int n = 0; n < sc->num_entries; n++) {
+ struct sc_entry *cur = sc->entries[n];
+ if (bstr_equals(cur->total, *hash_total)) {
+ entry = cur;
+ break;
+ }
+ }
+ if (!entry) {
+ if (sc->num_entries == SC_MAX_ENTRIES)
+ sc_flush_cache(sc);
+ entry = talloc_ptrtype(NULL, entry);
+ *entry = (struct sc_entry){
+ .total = bstrdup(entry, *hash_total),
+ .timer = timer_pool_create(sc->ra),
+ };
+
+ // The vertex shader uses mangled names for the vertex attributes, so
+ // that the fragment shader can use the "real" names. But the shader is
+ // expecting the vertex attribute names (at least with older GLSL
+ // targets for GL).
+ sc->params.vertex_stride = vertex_stride;
+ for (int n = 0; n < vao_len; n++) {
+ struct ra_renderpass_input attrib = vao[n];
+ attrib.name = talloc_asprintf(entry, "vertex_%s", attrib.name);
+ MP_TARRAY_APPEND(sc, sc->params.vertex_attribs,
+ sc->params.num_vertex_attribs, attrib);
+ }
+
+ for (int n = 0; n < sc->num_uniforms; n++) {
+ struct sc_cached_uniform u = {0};
+ if (sc->uniforms[n].type == SC_UNIFORM_TYPE_GLOBAL) {
+ // global uniforms need to be made visible to the ra_renderpass
+ u.index = sc->params.num_inputs;
+ MP_TARRAY_APPEND(sc, sc->params.inputs, sc->params.num_inputs,
+ sc->uniforms[n].input);
+ }
+ MP_TARRAY_APPEND(entry, entry->cached_uniforms,
+ entry->num_cached_uniforms, u);
+ }
+ if (!create_pass(sc, entry))
+ sc->error_state = true;
+ MP_TARRAY_APPEND(sc, sc->entries, sc->num_entries, entry);
+ }
+
+ if (!entry->pass) {
+ sc->current_shader = NULL;
+ return;
+ }
+
+ assert(sc->num_uniforms == entry->num_cached_uniforms);
+
+ sc->num_values = 0;
+ for (int n = 0; n < sc->num_uniforms; n++)
+ update_uniform(sc, entry, &sc->uniforms[n], n);
+
+ // If we're using a UBO, make sure to bind it as well
+ if (sc->ubo_size) {
+ struct ra_renderpass_input_val ubo_val = {
+ .index = entry->ubo_index,
+ .data = &entry->ubo,
+ };
+ MP_TARRAY_APPEND(sc, sc->values, sc->num_values, ubo_val);
+ }
+
+ sc->current_shader = entry;
+}
+
+struct mp_pass_perf gl_sc_dispatch_draw(struct gl_shader_cache *sc,
+ struct ra_tex *target, bool discard,
+ const struct ra_renderpass_input *vao,
+ int vao_len, size_t vertex_stride,
+ void *vertices, size_t num_vertices)
+{
+ struct timer_pool *timer = NULL;
+
+ sc->params.invalidate_target = discard;
+ gl_sc_generate(sc, RA_RENDERPASS_TYPE_RASTER, target->params.format,
+ vao, vao_len, vertex_stride);
+ if (!sc->current_shader)
+ goto error;
+
+ timer = sc->current_shader->timer;
+
+ struct mp_rect full_rc = {0, 0, target->params.w, target->params.h};
+
+ struct ra_renderpass_run_params run = {
+ .pass = sc->current_shader->pass,
+ .values = sc->values,
+ .num_values = sc->num_values,
+ .push_constants = sc->current_shader->pushc,
+ .target = target,
+ .vertex_data = vertices,
+ .vertex_count = num_vertices,
+ .viewport = full_rc,
+ .scissors = full_rc,
+ };
+
+ timer_pool_start(timer);
+ sc->ra->fns->renderpass_run(sc->ra, &run);
+ timer_pool_stop(timer);
+
+error:
+ gl_sc_reset(sc);
+ return timer_pool_measure(timer);
+}
+
+struct mp_pass_perf gl_sc_dispatch_compute(struct gl_shader_cache *sc,
+ int w, int h, int d)
+{
+ struct timer_pool *timer = NULL;
+
+ gl_sc_generate(sc, RA_RENDERPASS_TYPE_COMPUTE, NULL, NULL, 0, 0);
+ if (!sc->current_shader)
+ goto error;
+
+ timer = sc->current_shader->timer;
+
+ struct ra_renderpass_run_params run = {
+ .pass = sc->current_shader->pass,
+ .values = sc->values,
+ .num_values = sc->num_values,
+ .push_constants = sc->current_shader->pushc,
+ .compute_groups = {w, h, d},
+ };
+
+ timer_pool_start(timer);
+ sc->ra->fns->renderpass_run(sc->ra, &run);
+ timer_pool_stop(timer);
+
+error:
+ gl_sc_reset(sc);
+ return timer_pool_measure(timer);
+}
diff --git a/video/out/gpu/shader_cache.h b/video/out/gpu/shader_cache.h
new file mode 100644
index 0000000..7c51c7a
--- /dev/null
+++ b/video/out/gpu/shader_cache.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include "common/common.h"
+#include "misc/bstr.h"
+#include "ra.h"
+
+// For mp_pass_perf
+#include "video/out/vo.h"
+
+struct mp_log;
+struct mpv_global;
+struct gl_shader_cache;
+
+struct gl_shader_cache *gl_sc_create(struct ra *ra, struct mpv_global *global,
+ struct mp_log *log);
+void gl_sc_destroy(struct gl_shader_cache *sc);
+bool gl_sc_error_state(struct gl_shader_cache *sc);
+void gl_sc_reset_error(struct gl_shader_cache *sc);
+void gl_sc_add(struct gl_shader_cache *sc, const char *text);
+void gl_sc_addf(struct gl_shader_cache *sc, const char *textf, ...)
+ PRINTF_ATTRIBUTE(2, 3);
+void gl_sc_hadd(struct gl_shader_cache *sc, const char *text);
+void gl_sc_haddf(struct gl_shader_cache *sc, const char *textf, ...)
+ PRINTF_ATTRIBUTE(2, 3);
+void gl_sc_hadd_bstr(struct gl_shader_cache *sc, struct bstr text);
+void gl_sc_paddf(struct gl_shader_cache *sc, const char *textf, ...)
+ PRINTF_ATTRIBUTE(2, 3);
+
+// A hint that the next data-type (i.e. non-binding) uniform is expected to
+// change frequently. This refers to the _f, _i, _vecN etc. uniform types.
+void gl_sc_uniform_dynamic(struct gl_shader_cache *sc);
+void gl_sc_uniform_texture(struct gl_shader_cache *sc, char *name,
+ struct ra_tex *tex);
+void gl_sc_uniform_image2D_wo(struct gl_shader_cache *sc, const char *name,
+ struct ra_tex *tex);
+void gl_sc_ssbo(struct gl_shader_cache *sc, char *name, struct ra_buf *buf,
+ char *format, ...) PRINTF_ATTRIBUTE(4, 5);
+void gl_sc_uniform_f(struct gl_shader_cache *sc, char *name, float f);
+void gl_sc_uniform_i(struct gl_shader_cache *sc, char *name, int f);
+void gl_sc_uniform_vec2(struct gl_shader_cache *sc, char *name, float f[2]);
+void gl_sc_uniform_vec3(struct gl_shader_cache *sc, char *name, float f[3]);
+void gl_sc_uniform_mat2(struct gl_shader_cache *sc, char *name,
+ bool transpose, float *v);
+void gl_sc_uniform_mat3(struct gl_shader_cache *sc, char *name,
+ bool transpose, float *v);
+
+// Return the correct bvecN() variant for using mix() in this GLSL version
+const char *gl_sc_bvec(struct gl_shader_cache *sc, int dims);
+
+void gl_sc_blend(struct gl_shader_cache *sc,
+ enum ra_blend blend_src_rgb,
+ enum ra_blend blend_dst_rgb,
+ enum ra_blend blend_src_alpha,
+ enum ra_blend blend_dst_alpha);
+void gl_sc_enable_extension(struct gl_shader_cache *sc, char *name);
+struct mp_pass_perf gl_sc_dispatch_draw(struct gl_shader_cache *sc,
+ struct ra_tex *target, bool discard,
+ const struct ra_renderpass_input *vao,
+ int vao_len, size_t vertex_stride,
+ void *ptr, size_t num);
+struct mp_pass_perf gl_sc_dispatch_compute(struct gl_shader_cache *sc,
+ int w, int h, int d);
+// The application can call this on errors, to reset the current shader. This
+// is normally done implicitly by gl_sc_dispatch_*
+void gl_sc_reset(struct gl_shader_cache *sc);
+void gl_sc_set_cache_dir(struct gl_shader_cache *sc, char *dir);
diff --git a/video/out/gpu/spirv.c b/video/out/gpu/spirv.c
new file mode 100644
index 0000000..67088bc
--- /dev/null
+++ b/video/out/gpu/spirv.c
@@ -0,0 +1,70 @@
+#include "common/msg.h"
+#include "options/m_config.h"
+
+#include "spirv.h"
+#include "config.h"
+
+extern const struct spirv_compiler_fns spirv_shaderc;
+
+// in probe-order
+enum {
+ SPIRV_AUTO = 0,
+ SPIRV_SHADERC, // generally preferred, but not packaged everywhere
+};
+
+static const struct spirv_compiler_fns *compilers[] = {
+#if HAVE_SHADERC
+ [SPIRV_SHADERC] = &spirv_shaderc,
+#endif
+};
+
+static const struct m_opt_choice_alternatives compiler_choices[] = {
+ {"auto", SPIRV_AUTO},
+#if HAVE_SHADERC
+ {"shaderc", SPIRV_SHADERC},
+#endif
+ {0}
+};
+
+struct spirv_opts {
+ int compiler;
+};
+
+#define OPT_BASE_STRUCT struct spirv_opts
+const struct m_sub_options spirv_conf = {
+ .opts = (const struct m_option[]) {
+ {"spirv-compiler", OPT_CHOICE_C(compiler, compiler_choices)},
+ {0}
+ },
+ .size = sizeof(struct spirv_opts),
+};
+
+bool spirv_compiler_init(struct ra_ctx *ctx)
+{
+ void *tmp = talloc_new(NULL);
+ struct spirv_opts *opts = mp_get_config_group(tmp, ctx->global, &spirv_conf);
+ int compiler = opts->compiler;
+ talloc_free(tmp);
+
+ for (int i = SPIRV_AUTO+1; i < MP_ARRAY_SIZE(compilers); i++) {
+ if (compiler != SPIRV_AUTO && i != compiler)
+ continue;
+ if (!compilers[i])
+ continue;
+
+ ctx->spirv = talloc_zero(ctx, struct spirv_compiler);
+ ctx->spirv->log = ctx->log,
+ ctx->spirv->fns = compilers[i];
+
+ const char *name = m_opt_choice_str(compiler_choices, i);
+ strncpy(ctx->spirv->name, name, sizeof(ctx->spirv->name) - 1);
+ MP_VERBOSE(ctx, "Initializing SPIR-V compiler '%s'\n", name);
+ if (ctx->spirv->fns->init(ctx))
+ return true;
+ talloc_free(ctx->spirv);
+ ctx->spirv = NULL;
+ }
+
+ MP_ERR(ctx, "Failed initializing SPIR-V compiler!\n");
+ return false;
+}
diff --git a/video/out/gpu/spirv.h b/video/out/gpu/spirv.h
new file mode 100644
index 0000000..e3dbd4f
--- /dev/null
+++ b/video/out/gpu/spirv.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "common/msg.h"
+#include "common/common.h"
+#include "context.h"
+
+enum glsl_shader {
+ GLSL_SHADER_VERTEX,
+ GLSL_SHADER_FRAGMENT,
+ GLSL_SHADER_COMPUTE,
+};
+
+#define SPIRV_NAME_MAX_LEN 32
+
+struct spirv_compiler {
+ char name[SPIRV_NAME_MAX_LEN];
+ const struct spirv_compiler_fns *fns;
+ struct mp_log *log;
+ void *priv;
+
+ const char *required_ext; // or NULL
+ int glsl_version; // GLSL version supported
+ int compiler_version; // for cache invalidation, may be left as 0
+ int ra_caps; // RA_CAP_* provided by this implementation, if any
+};
+
+struct spirv_compiler_fns {
+ // Compile GLSL to SPIR-V, under GL_KHR_vulkan_glsl semantics.
+ bool (*compile_glsl)(struct spirv_compiler *spirv, void *tactx,
+ enum glsl_shader type, const char *glsl,
+ struct bstr *out_spirv);
+
+ // Called by spirv_compiler_init / ra_ctx_destroy. These don't need to
+ // allocate/free ctx->spirv, that is done by the caller
+ bool (*init)(struct ra_ctx *ctx);
+ void (*uninit)(struct ra_ctx *ctx); // optional
+};
+
+// Initializes ctx->spirv to a valid SPIR-V compiler, or returns false on
+// failure. Cleanup will be handled by ra_ctx_destroy.
+bool spirv_compiler_init(struct ra_ctx *ctx);
diff --git a/video/out/gpu/spirv_shaderc.c b/video/out/gpu/spirv_shaderc.c
new file mode 100644
index 0000000..f285631
--- /dev/null
+++ b/video/out/gpu/spirv_shaderc.c
@@ -0,0 +1,125 @@
+#include "common/msg.h"
+
+#include "context.h"
+#include "spirv.h"
+
+#include <shaderc/shaderc.h>
+
+struct priv {
+ shaderc_compiler_t compiler;
+ shaderc_compile_options_t opts;
+};
+
+static void shaderc_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->spirv->priv;
+ if (!p)
+ return;
+
+ shaderc_compile_options_release(p->opts);
+ shaderc_compiler_release(p->compiler);
+}
+
+static bool shaderc_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->spirv->priv = talloc_zero(ctx->spirv, struct priv);
+
+ p->compiler = shaderc_compiler_initialize();
+ if (!p->compiler)
+ goto error;
+ p->opts = shaderc_compile_options_initialize();
+ if (!p->opts)
+ goto error;
+
+ shaderc_compile_options_set_optimization_level(p->opts,
+ shaderc_optimization_level_performance);
+ if (ctx->opts.debug)
+ shaderc_compile_options_set_generate_debug_info(p->opts);
+
+ int ver, rev;
+ shaderc_get_spv_version(&ver, &rev);
+ ctx->spirv->compiler_version = ver * 100 + rev; // forwards compatibility
+ ctx->spirv->glsl_version = 450; // impossible to query?
+ return true;
+
+error:
+ shaderc_uninit(ctx);
+ return false;
+}
+
+static shaderc_compilation_result_t compile(struct priv *p,
+ enum glsl_shader type,
+ const char *glsl, bool debug)
+{
+ static const shaderc_shader_kind kinds[] = {
+ [GLSL_SHADER_VERTEX] = shaderc_glsl_vertex_shader,
+ [GLSL_SHADER_FRAGMENT] = shaderc_glsl_fragment_shader,
+ [GLSL_SHADER_COMPUTE] = shaderc_glsl_compute_shader,
+ };
+
+ if (debug) {
+ return shaderc_compile_into_spv_assembly(p->compiler, glsl, strlen(glsl),
+ kinds[type], "input", "main", p->opts);
+ } else {
+ return shaderc_compile_into_spv(p->compiler, glsl, strlen(glsl),
+ kinds[type], "input", "main", p->opts);
+ }
+}
+
+static bool shaderc_compile(struct spirv_compiler *spirv, void *tactx,
+ enum glsl_shader type, const char *glsl,
+ struct bstr *out_spirv)
+{
+ struct priv *p = spirv->priv;
+
+ shaderc_compilation_result_t res = compile(p, type, glsl, false);
+ int errs = shaderc_result_get_num_errors(res),
+ warn = shaderc_result_get_num_warnings(res),
+ msgl = errs ? MSGL_ERR : warn ? MSGL_WARN : MSGL_V;
+
+ const char *msg = shaderc_result_get_error_message(res);
+ if (msg[0])
+ MP_MSG(spirv, msgl, "shaderc output:\n%s", msg);
+
+ int s = shaderc_result_get_compilation_status(res);
+ bool success = s == shaderc_compilation_status_success;
+
+ static const char *results[] = {
+ [shaderc_compilation_status_success] = "success",
+ [shaderc_compilation_status_invalid_stage] = "invalid stage",
+ [shaderc_compilation_status_compilation_error] = "error",
+ [shaderc_compilation_status_internal_error] = "internal error",
+ [shaderc_compilation_status_null_result_object] = "no result",
+ [shaderc_compilation_status_invalid_assembly] = "invalid assembly",
+ };
+
+ const char *status = s < MP_ARRAY_SIZE(results) ? results[s] : "unknown";
+ MP_MSG(spirv, msgl, "shaderc compile status '%s' (%d errors, %d warnings)\n",
+ status, errs, warn);
+
+ if (success) {
+ void *bytes = (void *) shaderc_result_get_bytes(res);
+ out_spirv->len = shaderc_result_get_length(res);
+ out_spirv->start = talloc_memdup(tactx, bytes, out_spirv->len);
+ }
+
+ // Also print SPIR-V disassembly for debugging purposes. Unfortunately
+ // there doesn't seem to be a way to get this except compiling the shader
+ // a second time..
+ if (mp_msg_test(spirv->log, MSGL_TRACE)) {
+ shaderc_compilation_result_t dis = compile(p, type, glsl, true);
+ MP_TRACE(spirv, "Generated SPIR-V:\n%.*s",
+ (int)shaderc_result_get_length(dis),
+ shaderc_result_get_bytes(dis));
+ shaderc_result_release(dis);
+ }
+
+ shaderc_result_release(res);
+ return success;
+}
+
+const struct spirv_compiler_fns spirv_shaderc = {
+ .compile_glsl = shaderc_compile,
+ .init = shaderc_init,
+ .uninit = shaderc_uninit,
+};
diff --git a/video/out/gpu/user_shaders.c b/video/out/gpu/user_shaders.c
new file mode 100644
index 0000000..708de87
--- /dev/null
+++ b/video/out/gpu/user_shaders.c
@@ -0,0 +1,463 @@
+/*
+ * 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 <assert.h>
+#include <math.h>
+
+#include "common/msg.h"
+#include "misc/ctype.h"
+#include "user_shaders.h"
+
+static bool parse_rpn_szexpr(struct bstr line, struct szexp out[MAX_SZEXP_SIZE])
+{
+ int pos = 0;
+
+ while (line.len > 0) {
+ struct bstr word = bstr_strip(bstr_splitchar(line, &line, ' '));
+ if (word.len == 0)
+ continue;
+
+ if (pos >= MAX_SZEXP_SIZE)
+ return false;
+
+ struct szexp *exp = &out[pos++];
+
+ if (bstr_eatend0(&word, ".w") || bstr_eatend0(&word, ".width")) {
+ exp->tag = SZEXP_VAR_W;
+ exp->val.varname = word;
+ continue;
+ }
+
+ if (bstr_eatend0(&word, ".h") || bstr_eatend0(&word, ".height")) {
+ exp->tag = SZEXP_VAR_H;
+ exp->val.varname = word;
+ continue;
+ }
+
+ switch (word.start[0]) {
+ case '+': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_ADD; continue;
+ case '-': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_SUB; continue;
+ case '*': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_MUL; continue;
+ case '/': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_DIV; continue;
+ case '%': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_MOD; continue;
+ case '!': exp->tag = SZEXP_OP1; exp->val.op = SZEXP_OP_NOT; continue;
+ case '>': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_GT; continue;
+ case '<': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_LT; continue;
+ case '=': exp->tag = SZEXP_OP2; exp->val.op = SZEXP_OP_EQ; continue;
+ }
+
+ if (mp_isdigit(word.start[0])) {
+ exp->tag = SZEXP_CONST;
+ if (bstr_sscanf(word, "%f", &exp->val.cval) != 1)
+ return false;
+ continue;
+ }
+
+ // Some sort of illegal expression
+ return false;
+ }
+
+ return true;
+}
+
+// Returns whether successful. 'result' is left untouched on failure
+bool eval_szexpr(struct mp_log *log, void *priv,
+ bool (*lookup)(void *priv, struct bstr var, float size[2]),
+ struct szexp expr[MAX_SZEXP_SIZE], float *result)
+{
+ float stack[MAX_SZEXP_SIZE] = {0};
+ int idx = 0; // points to next element to push
+
+ for (int i = 0; i < MAX_SZEXP_SIZE; i++) {
+ switch (expr[i].tag) {
+ case SZEXP_END:
+ goto done;
+
+ case SZEXP_CONST:
+ // Since our SZEXPs are bound by MAX_SZEXP_SIZE, it should be
+ // impossible to overflow the stack
+ assert(idx < MAX_SZEXP_SIZE);
+ stack[idx++] = expr[i].val.cval;
+ continue;
+
+ case SZEXP_OP1:
+ if (idx < 1) {
+ mp_warn(log, "Stack underflow in RPN expression!\n");
+ return false;
+ }
+
+ switch (expr[i].val.op) {
+ case SZEXP_OP_NOT: stack[idx-1] = !stack[idx-1]; break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+ continue;
+
+ case SZEXP_OP2:
+ if (idx < 2) {
+ mp_warn(log, "Stack underflow in RPN expression!\n");
+ return false;
+ }
+
+ // Pop the operands in reverse order
+ float op2 = stack[--idx];
+ float op1 = stack[--idx];
+ float res = 0.0;
+ switch (expr[i].val.op) {
+ case SZEXP_OP_ADD: res = op1 + op2; break;
+ case SZEXP_OP_SUB: res = op1 - op2; break;
+ case SZEXP_OP_MUL: res = op1 * op2; break;
+ case SZEXP_OP_DIV: res = op1 / op2; break;
+ case SZEXP_OP_MOD: res = fmodf(op1, op2); break;
+ case SZEXP_OP_GT: res = op1 > op2; break;
+ case SZEXP_OP_LT: res = op1 < op2; break;
+ case SZEXP_OP_EQ: res = op1 == op2; break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+
+ if (!isfinite(res)) {
+ mp_warn(log, "Illegal operation in RPN expression!\n");
+ return false;
+ }
+
+ stack[idx++] = res;
+ continue;
+
+ case SZEXP_VAR_W:
+ case SZEXP_VAR_H: {
+ struct bstr name = expr[i].val.varname;
+ float size[2];
+
+ if (!lookup(priv, name, size)) {
+ mp_warn(log, "Variable %.*s not found in RPN expression!\n",
+ BSTR_P(name));
+ return false;
+ }
+
+ stack[idx++] = (expr[i].tag == SZEXP_VAR_W) ? size[0] : size[1];
+ continue;
+ }
+ }
+ }
+
+done:
+ // Return the single stack element
+ if (idx != 1) {
+ mp_warn(log, "Malformed stack after RPN expression!\n");
+ return false;
+ }
+
+ *result = stack[0];
+ return true;
+}
+
+static bool parse_hook(struct mp_log *log, struct bstr *body,
+ struct gl_user_shader_hook *out)
+{
+ *out = (struct gl_user_shader_hook){
+ .pass_desc = bstr0("(unknown)"),
+ .offset = identity_trans,
+ .align_offset = false,
+ .width = {{ SZEXP_VAR_W, { .varname = bstr0("HOOKED") }}},
+ .height = {{ SZEXP_VAR_H, { .varname = bstr0("HOOKED") }}},
+ .cond = {{ SZEXP_CONST, { .cval = 1.0 }}},
+ };
+
+ int hook_idx = 0;
+ int bind_idx = 0;
+
+ // Parse all headers
+ while (true) {
+ struct bstr rest;
+ struct bstr line = bstr_strip(bstr_getline(*body, &rest));
+
+ // Check for the presence of the magic line beginning
+ if (!bstr_eatstart0(&line, "//!"))
+ break;
+
+ *body = rest;
+
+ // Parse the supported commands
+ if (bstr_eatstart0(&line, "HOOK")) {
+ if (hook_idx == SHADER_MAX_HOOKS) {
+ mp_err(log, "Passes may only hook up to %d textures!\n",
+ SHADER_MAX_HOOKS);
+ return false;
+ }
+ out->hook_tex[hook_idx++] = bstr_strip(line);
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "BIND")) {
+ if (bind_idx == SHADER_MAX_BINDS) {
+ mp_err(log, "Passes may only bind up to %d textures!\n",
+ SHADER_MAX_BINDS);
+ return false;
+ }
+ out->bind_tex[bind_idx++] = bstr_strip(line);
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "SAVE")) {
+ out->save_tex = bstr_strip(line);
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "DESC")) {
+ out->pass_desc = bstr_strip(line);
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "OFFSET")) {
+ line = bstr_strip(line);
+ if (bstr_equals0(line, "ALIGN")) {
+ out->align_offset = true;
+ } else {
+ float ox, oy;
+ if (bstr_sscanf(line, "%f %f", &ox, &oy) != 2) {
+ mp_err(log, "Error while parsing OFFSET!\n");
+ return false;
+ }
+ out->offset.t[0] = ox;
+ out->offset.t[1] = oy;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "WIDTH")) {
+ if (!parse_rpn_szexpr(line, out->width)) {
+ mp_err(log, "Error while parsing WIDTH!\n");
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "HEIGHT")) {
+ if (!parse_rpn_szexpr(line, out->height)) {
+ mp_err(log, "Error while parsing HEIGHT!\n");
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "WHEN")) {
+ if (!parse_rpn_szexpr(line, out->cond)) {
+ mp_err(log, "Error while parsing WHEN!\n");
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "COMPONENTS")) {
+ if (bstr_sscanf(line, "%d", &out->components) != 1) {
+ mp_err(log, "Error while parsing COMPONENTS!\n");
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "COMPUTE")) {
+ struct compute_info *ci = &out->compute;
+ int num = bstr_sscanf(line, "%d %d %d %d", &ci->block_w, &ci->block_h,
+ &ci->threads_w, &ci->threads_h);
+
+ if (num == 2 || num == 4) {
+ ci->active = true;
+ ci->directly_writes = true;
+ } else {
+ mp_err(log, "Error while parsing COMPUTE!\n");
+ return false;
+ }
+ continue;
+ }
+
+ // Unknown command type
+ mp_err(log, "Unrecognized command '%.*s'!\n", BSTR_P(line));
+ return false;
+ }
+
+ // The rest of the file up until the next magic line beginning (if any)
+ // shall be the shader body
+ if (bstr_split_tok(*body, "//!", &out->pass_body, body)) {
+ // Make sure the magic line is part of the rest
+ body->start -= 3;
+ body->len += 3;
+ }
+
+ // Sanity checking
+ if (hook_idx == 0)
+ mp_warn(log, "Pass has no hooked textures (will be ignored)!\n");
+
+ return true;
+}
+
+static bool parse_tex(struct mp_log *log, struct ra *ra, struct bstr *body,
+ struct gl_user_shader_tex *out)
+{
+ *out = (struct gl_user_shader_tex){
+ .name = bstr0("USER_TEX"),
+ .params = {
+ .dimensions = 2,
+ .w = 1, .h = 1, .d = 1,
+ .render_src = true,
+ .src_linear = true,
+ },
+ };
+ struct ra_tex_params *p = &out->params;
+
+ while (true) {
+ struct bstr rest;
+ struct bstr line = bstr_strip(bstr_getline(*body, &rest));
+
+ if (!bstr_eatstart0(&line, "//!"))
+ break;
+
+ *body = rest;
+
+ if (bstr_eatstart0(&line, "TEXTURE")) {
+ out->name = bstr_strip(line);
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "SIZE")) {
+ p->dimensions = bstr_sscanf(line, "%d %d %d", &p->w, &p->h, &p->d);
+ if (p->dimensions < 1 || p->dimensions > 3 ||
+ p->w < 1 || p->h < 1 || p->d < 1)
+ {
+ mp_err(log, "Error while parsing SIZE!\n");
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "FORMAT ")) {
+ p->format = NULL;
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt = ra->formats[n];
+ if (bstr_equals0(line, fmt->name)) {
+ p->format = fmt;
+ break;
+ }
+ }
+ // (pixel_size==0 is for opaque formats)
+ if (!p->format || !p->format->pixel_size) {
+ mp_err(log, "Unrecognized/unavailable FORMAT name: '%.*s'!\n",
+ BSTR_P(line));
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "FILTER")) {
+ line = bstr_strip(line);
+ if (bstr_equals0(line, "LINEAR")) {
+ p->src_linear = true;
+ } else if (bstr_equals0(line, "NEAREST")) {
+ p->src_linear = false;
+ } else {
+ mp_err(log, "Unrecognized FILTER: '%.*s'!\n", BSTR_P(line));
+ return false;
+ }
+ continue;
+ }
+
+ if (bstr_eatstart0(&line, "BORDER")) {
+ line = bstr_strip(line);
+ if (bstr_equals0(line, "CLAMP")) {
+ p->src_repeat = false;
+ } else if (bstr_equals0(line, "REPEAT")) {
+ p->src_repeat = true;
+ } else {
+ mp_err(log, "Unrecognized BORDER: '%.*s'!\n", BSTR_P(line));
+ return false;
+ }
+ continue;
+ }
+
+ mp_err(log, "Unrecognized command '%.*s'!\n", BSTR_P(line));
+ return false;
+ }
+
+ if (!p->format) {
+ mp_err(log, "No FORMAT specified.\n");
+ return false;
+ }
+
+ if (p->src_linear && !p->format->linear_filter) {
+ mp_err(log, "The specified texture format cannot be filtered!\n");
+ return false;
+ }
+
+ // Decode the rest of the section (up to the next //! marker) as raw hex
+ // data for the texture
+ struct bstr hexdata;
+ if (bstr_split_tok(*body, "//!", &hexdata, body)) {
+ // Make sure the magic line is part of the rest
+ body->start -= 3;
+ body->len += 3;
+ }
+
+ struct bstr tex;
+ if (!bstr_decode_hex(NULL, bstr_strip(hexdata), &tex)) {
+ mp_err(log, "Error while parsing TEXTURE body: must be a valid "
+ "hexadecimal sequence, on a single line!\n");
+ return false;
+ }
+
+ int expected_len = p->w * p->h * p->d * p->format->pixel_size;
+ if (tex.len != expected_len) {
+ mp_err(log, "Shader TEXTURE size mismatch: got %zd bytes, expected %d!\n",
+ tex.len, expected_len);
+ talloc_free(tex.start);
+ return false;
+ }
+
+ p->initial_data = tex.start;
+ return true;
+}
+
+void parse_user_shader(struct mp_log *log, struct ra *ra, struct bstr shader,
+ void *priv,
+ bool (*dohook)(void *p, struct gl_user_shader_hook hook),
+ bool (*dotex)(void *p, struct gl_user_shader_tex tex))
+{
+ if (!dohook || !dotex || !shader.len)
+ return;
+
+ // Skip all garbage (e.g. comments) before the first header
+ int pos = bstr_find(shader, bstr0("//!"));
+ if (pos < 0) {
+ mp_warn(log, "Shader appears to contain no headers!\n");
+ return;
+ }
+ shader = bstr_cut(shader, pos);
+
+ // Loop over the file
+ while (shader.len > 0)
+ {
+ // Peek at the first header to dispatch the right type
+ if (bstr_startswith0(shader, "//!TEXTURE")) {
+ struct gl_user_shader_tex t;
+ if (!parse_tex(log, ra, &shader, &t) || !dotex(priv, t))
+ return;
+ continue;
+ }
+
+ struct gl_user_shader_hook h;
+ if (!parse_hook(log, &shader, &h) || !dohook(priv, h))
+ return;
+ }
+}
diff --git a/video/out/gpu/user_shaders.h b/video/out/gpu/user_shaders.h
new file mode 100644
index 0000000..4bb7c22
--- /dev/null
+++ b/video/out/gpu/user_shaders.h
@@ -0,0 +1,99 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MP_GL_USER_SHADERS_H
+#define MP_GL_USER_SHADERS_H
+
+#include "utils.h"
+#include "ra.h"
+
+#define SHADER_MAX_HOOKS 16
+#define SHADER_MAX_BINDS 16
+#define MAX_SZEXP_SIZE 32
+
+enum szexp_op {
+ SZEXP_OP_ADD,
+ SZEXP_OP_SUB,
+ SZEXP_OP_MUL,
+ SZEXP_OP_DIV,
+ SZEXP_OP_MOD,
+ SZEXP_OP_NOT,
+ SZEXP_OP_GT,
+ SZEXP_OP_LT,
+ SZEXP_OP_EQ,
+};
+
+enum szexp_tag {
+ SZEXP_END = 0, // End of an RPN expression
+ SZEXP_CONST, // Push a constant value onto the stack
+ SZEXP_VAR_W, // Get the width/height of a named texture (variable)
+ SZEXP_VAR_H,
+ SZEXP_OP2, // Pop two elements and push the result of a dyadic operation
+ SZEXP_OP1, // Pop one element and push the result of a monadic operation
+};
+
+struct szexp {
+ enum szexp_tag tag;
+ union {
+ float cval;
+ struct bstr varname;
+ enum szexp_op op;
+ } val;
+};
+
+struct compute_info {
+ bool active;
+ int block_w, block_h; // Block size (each block corresponds to one WG)
+ int threads_w, threads_h; // How many threads form a working group
+ bool directly_writes; // If true, shader is assumed to imageStore(out_image)
+};
+
+struct gl_user_shader_hook {
+ struct bstr pass_desc;
+ struct bstr hook_tex[SHADER_MAX_HOOKS];
+ struct bstr bind_tex[SHADER_MAX_BINDS];
+ struct bstr save_tex;
+ struct bstr pass_body;
+ struct gl_transform offset;
+ bool align_offset;
+ struct szexp width[MAX_SZEXP_SIZE];
+ struct szexp height[MAX_SZEXP_SIZE];
+ struct szexp cond[MAX_SZEXP_SIZE];
+ int components;
+ struct compute_info compute;
+};
+
+struct gl_user_shader_tex {
+ struct bstr name;
+ struct ra_tex_params params;
+ // for video.c
+ struct ra_tex *tex;
+};
+
+// Parse the next shader block from `body`. The callbacks are invoked on every
+// valid shader block parsed.
+void parse_user_shader(struct mp_log *log, struct ra *ra, struct bstr shader,
+ void *priv,
+ bool (*dohook)(void *p, struct gl_user_shader_hook hook),
+ bool (*dotex)(void *p, struct gl_user_shader_tex tex));
+
+// Evaluate a szexp, given a lookup function for named textures
+bool eval_szexpr(struct mp_log *log, void *priv,
+ bool (*lookup)(void *priv, struct bstr var, float size[2]),
+ struct szexp expr[MAX_SZEXP_SIZE], float *result);
+
+#endif
diff --git a/video/out/gpu/utils.c b/video/out/gpu/utils.c
new file mode 100644
index 0000000..8a1aacf
--- /dev/null
+++ b/video/out/gpu/utils.c
@@ -0,0 +1,349 @@
+#include "common/msg.h"
+#include "video/out/vo.h"
+#include "utils.h"
+
+// Standard parallel 2D projection, except y1 < y0 means that the coordinate
+// system is flipped, not the projection.
+void gl_transform_ortho(struct gl_transform *t, float x0, float x1,
+ float y0, float y1)
+{
+ if (y1 < y0) {
+ float tmp = y0;
+ y0 = tmp - y1;
+ y1 = tmp;
+ }
+
+ t->m[0][0] = 2.0f / (x1 - x0);
+ t->m[0][1] = 0.0f;
+ t->m[1][0] = 0.0f;
+ t->m[1][1] = 2.0f / (y1 - y0);
+ t->t[0] = -(x1 + x0) / (x1 - x0);
+ t->t[1] = -(y1 + y0) / (y1 - y0);
+}
+
+// Apply the effects of one transformation to another, transforming it in the
+// process. In other words: post-composes t onto x
+void gl_transform_trans(struct gl_transform t, struct gl_transform *x)
+{
+ struct gl_transform xt = *x;
+ x->m[0][0] = t.m[0][0] * xt.m[0][0] + t.m[0][1] * xt.m[1][0];
+ x->m[1][0] = t.m[1][0] * xt.m[0][0] + t.m[1][1] * xt.m[1][0];
+ x->m[0][1] = t.m[0][0] * xt.m[0][1] + t.m[0][1] * xt.m[1][1];
+ x->m[1][1] = t.m[1][0] * xt.m[0][1] + t.m[1][1] * xt.m[1][1];
+ gl_transform_vec(t, &x->t[0], &x->t[1]);
+}
+
+void gl_transform_ortho_fbo(struct gl_transform *t, struct ra_fbo fbo)
+{
+ int y_dir = fbo.flip ? -1 : 1;
+ gl_transform_ortho(t, 0, fbo.tex->params.w, 0, fbo.tex->params.h * y_dir);
+}
+
+float gl_video_scale_ambient_lux(float lmin, float lmax,
+ float rmin, float rmax, float lux)
+{
+ assert(lmax > lmin);
+
+ float num = (rmax - rmin) * (log10(lux) - log10(lmin));
+ float den = log10(lmax) - log10(lmin);
+ float result = num / den + rmin;
+
+ // clamp the result
+ float max = MPMAX(rmax, rmin);
+ float min = MPMIN(rmax, rmin);
+ return MPMAX(MPMIN(result, max), min);
+}
+
+void ra_buf_pool_uninit(struct ra *ra, struct ra_buf_pool *pool)
+{
+ for (int i = 0; i < pool->num_buffers; i++)
+ ra_buf_free(ra, &pool->buffers[i]);
+
+ talloc_free(pool->buffers);
+ *pool = (struct ra_buf_pool){0};
+}
+
+static bool ra_buf_params_compatible(const struct ra_buf_params *new,
+ const struct ra_buf_params *old)
+{
+ return new->type == old->type &&
+ new->size <= old->size &&
+ new->host_mapped == old->host_mapped &&
+ new->host_mutable == old->host_mutable;
+}
+
+static bool ra_buf_pool_grow(struct ra *ra, struct ra_buf_pool *pool)
+{
+ struct ra_buf *buf = ra_buf_create(ra, &pool->current_params);
+ if (!buf)
+ return false;
+
+ MP_TARRAY_INSERT_AT(NULL, pool->buffers, pool->num_buffers, pool->index, buf);
+ MP_VERBOSE(ra, "Resized buffer pool of type %u to size %d\n",
+ pool->current_params.type, pool->num_buffers);
+ return true;
+}
+
+struct ra_buf *ra_buf_pool_get(struct ra *ra, struct ra_buf_pool *pool,
+ const struct ra_buf_params *params)
+{
+ assert(!params->initial_data);
+
+ if (!ra_buf_params_compatible(params, &pool->current_params)) {
+ ra_buf_pool_uninit(ra, pool);
+ pool->current_params = *params;
+ }
+
+ // Make sure we have at least one buffer available
+ if (!pool->buffers && !ra_buf_pool_grow(ra, pool))
+ return NULL;
+
+ // Make sure the next buffer is available for use
+ if (!ra->fns->buf_poll(ra, pool->buffers[pool->index]) &&
+ !ra_buf_pool_grow(ra, pool))
+ {
+ return NULL;
+ }
+
+ struct ra_buf *buf = pool->buffers[pool->index++];
+ pool->index %= pool->num_buffers;
+
+ return buf;
+}
+
+bool ra_tex_upload_pbo(struct ra *ra, struct ra_buf_pool *pbo,
+ const struct ra_tex_upload_params *params)
+{
+ if (params->buf)
+ return ra->fns->tex_upload(ra, params);
+
+ struct ra_tex *tex = params->tex;
+ size_t row_size = tex->params.dimensions == 2 ? params->stride :
+ tex->params.w * tex->params.format->pixel_size;
+
+ int height = tex->params.h;
+ if (tex->params.dimensions == 2 && params->rc)
+ height = mp_rect_h(*params->rc);
+
+ struct ra_buf_params bufparams = {
+ .type = RA_BUF_TYPE_TEX_UPLOAD,
+ .size = row_size * height * tex->params.d,
+ .host_mutable = true,
+ };
+
+ struct ra_buf *buf = ra_buf_pool_get(ra, pbo, &bufparams);
+ if (!buf)
+ return false;
+
+ ra->fns->buf_update(ra, buf, 0, params->src, bufparams.size);
+
+ struct ra_tex_upload_params newparams = *params;
+ newparams.buf = buf;
+ newparams.src = NULL;
+
+ return ra->fns->tex_upload(ra, &newparams);
+}
+
+struct ra_layout std140_layout(struct ra_renderpass_input *inp)
+{
+ size_t el_size = ra_vartype_size(inp->type);
+
+ // std140 packing rules:
+ // 1. The alignment of generic values is their size in bytes
+ // 2. The alignment of vectors is the vector length * the base count, with
+ // the exception of vec3 which is always aligned like vec4
+ // 3. The alignment of arrays is that of the element size rounded up to
+ // the nearest multiple of vec4
+ // 4. Matrices are treated like arrays of vectors
+ // 5. Arrays/matrices are laid out with a stride equal to the alignment
+ size_t stride = el_size * inp->dim_v;
+ size_t align = stride;
+ if (inp->dim_v == 3)
+ align += el_size;
+ if (inp->dim_m > 1)
+ stride = align = MP_ALIGN_UP(stride, sizeof(float[4]));
+
+ return (struct ra_layout) {
+ .align = align,
+ .stride = stride,
+ .size = stride * inp->dim_m,
+ };
+}
+
+struct ra_layout std430_layout(struct ra_renderpass_input *inp)
+{
+ size_t el_size = ra_vartype_size(inp->type);
+
+ // std430 packing rules: like std140, except arrays/matrices are always
+ // "tightly" packed, even arrays/matrices of vec3s
+ size_t stride = el_size * inp->dim_v;
+ size_t align = stride;
+ if (inp->dim_v == 3 && inp->dim_m == 1)
+ align += el_size;
+
+ return (struct ra_layout) {
+ .align = align,
+ .stride = stride,
+ .size = stride * inp->dim_m,
+ };
+}
+
+// Resize a texture to a new desired size and format if necessary
+bool ra_tex_resize(struct ra *ra, struct mp_log *log, struct ra_tex **tex,
+ int w, int h, const struct ra_format *fmt)
+{
+ if (*tex) {
+ struct ra_tex_params cur_params = (*tex)->params;
+ if (cur_params.w == w && cur_params.h == h && cur_params.format == fmt)
+ return true;
+ }
+
+ mp_dbg(log, "Resizing texture: %dx%d\n", w, h);
+
+ if (!fmt || !fmt->renderable || !fmt->linear_filter) {
+ mp_err(log, "Format %s not supported.\n", fmt ? fmt->name : "(unset)");
+ return false;
+ }
+
+ ra_tex_free(ra, tex);
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = w,
+ .h = h,
+ .d = 1,
+ .format = fmt,
+ .src_linear = true,
+ .render_src = true,
+ .render_dst = true,
+ .storage_dst = fmt->storable,
+ .blit_src = true,
+ };
+
+ *tex = ra_tex_create(ra, &params);
+ if (!*tex)
+ mp_err(log, "Error: texture could not be created.\n");
+
+ return *tex;
+}
+
+struct timer_pool {
+ struct ra *ra;
+ ra_timer *timer;
+ bool running; // detect invalid usage
+
+ uint64_t samples[VO_PERF_SAMPLE_COUNT];
+ int sample_idx;
+ int sample_count;
+
+ uint64_t sum;
+ uint64_t peak;
+};
+
+struct timer_pool *timer_pool_create(struct ra *ra)
+{
+ if (!ra->fns->timer_create)
+ return NULL;
+
+ ra_timer *timer = ra->fns->timer_create(ra);
+ if (!timer)
+ return NULL;
+
+ struct timer_pool *pool = talloc(NULL, struct timer_pool);
+ if (!pool) {
+ ra->fns->timer_destroy(ra, timer);
+ return NULL;
+ }
+
+ *pool = (struct timer_pool){ .ra = ra, .timer = timer };
+ return pool;
+}
+
+void timer_pool_destroy(struct timer_pool *pool)
+{
+ if (!pool)
+ return;
+
+ pool->ra->fns->timer_destroy(pool->ra, pool->timer);
+ talloc_free(pool);
+}
+
+void timer_pool_start(struct timer_pool *pool)
+{
+ if (!pool)
+ return;
+
+ assert(!pool->running);
+ pool->ra->fns->timer_start(pool->ra, pool->timer);
+ pool->running = true;
+}
+
+void timer_pool_stop(struct timer_pool *pool)
+{
+ if (!pool)
+ return;
+
+ assert(pool->running);
+ uint64_t res = pool->ra->fns->timer_stop(pool->ra, pool->timer);
+ pool->running = false;
+
+ if (res) {
+ // Input res into the buffer and grab the previous value
+ uint64_t old = pool->samples[pool->sample_idx];
+ pool->sample_count = MPMIN(pool->sample_count + 1, VO_PERF_SAMPLE_COUNT);
+ pool->samples[pool->sample_idx++] = res;
+ pool->sample_idx %= VO_PERF_SAMPLE_COUNT;
+ pool->sum = pool->sum + res - old;
+
+ // Update peak if necessary
+ if (res >= pool->peak) {
+ pool->peak = res;
+ } else if (pool->peak == old) {
+ // It's possible that the last peak was the value we just removed,
+ // if so we need to scan for the new peak
+ uint64_t peak = res;
+ for (int i = 0; i < VO_PERF_SAMPLE_COUNT; i++)
+ peak = MPMAX(peak, pool->samples[i]);
+ pool->peak = peak;
+ }
+ }
+}
+
+struct mp_pass_perf timer_pool_measure(struct timer_pool *pool)
+{
+ if (!pool)
+ return (struct mp_pass_perf){0};
+
+ struct mp_pass_perf res = {
+ .peak = pool->peak,
+ .count = pool->sample_count,
+ };
+
+ int idx = pool->sample_idx - pool->sample_count + VO_PERF_SAMPLE_COUNT;
+ for (int i = 0; i < res.count; i++) {
+ idx %= VO_PERF_SAMPLE_COUNT;
+ res.samples[i] = pool->samples[idx++];
+ }
+
+ if (res.count > 0) {
+ res.last = res.samples[res.count - 1];
+ res.avg = pool->sum / res.count;
+ }
+
+ return res;
+}
+
+void mp_log_source(struct mp_log *log, int lev, const char *src)
+{
+ int line = 1;
+ if (!src)
+ return;
+ while (*src) {
+ const char *end = strchr(src, '\n');
+ const char *next = end + 1;
+ if (!end)
+ next = end = src + strlen(src);
+ mp_msg(log, lev, "[%3d] %.*s\n", line, (int)(end - src), src);
+ line++;
+ src = next;
+ }
+}
diff --git a/video/out/gpu/utils.h b/video/out/gpu/utils.h
new file mode 100644
index 0000000..215873e
--- /dev/null
+++ b/video/out/gpu/utils.h
@@ -0,0 +1,108 @@
+#pragma once
+
+#include <stdbool.h>
+#include <math.h>
+
+#include "ra.h"
+#include "context.h"
+
+// A 3x2 matrix, with the translation part separate.
+struct gl_transform {
+ // row-major, e.g. in mathematical notation:
+ // | m[0][0] m[0][1] |
+ // | m[1][0] m[1][1] |
+ float m[2][2];
+ float t[2];
+};
+
+static const struct gl_transform identity_trans = {
+ .m = {{1.0, 0.0}, {0.0, 1.0}},
+ .t = {0.0, 0.0},
+};
+
+void gl_transform_ortho(struct gl_transform *t, float x0, float x1,
+ float y0, float y1);
+
+// This treats m as an affine transformation, in other words m[2][n] gets
+// added to the output.
+static inline void gl_transform_vec(struct gl_transform t, float *x, float *y)
+{
+ float vx = *x, vy = *y;
+ *x = vx * t.m[0][0] + vy * t.m[0][1] + t.t[0];
+ *y = vx * t.m[1][0] + vy * t.m[1][1] + t.t[1];
+}
+
+struct mp_rect_f {
+ float x0, y0, x1, y1;
+};
+
+// Semantic equality (fuzzy comparison)
+static inline bool mp_rect_f_seq(struct mp_rect_f a, struct mp_rect_f b)
+{
+ return fabs(a.x0 - b.x0) < 1e-6 && fabs(a.x1 - b.x1) < 1e-6 &&
+ fabs(a.y0 - b.y0) < 1e-6 && fabs(a.y1 - b.y1) < 1e-6;
+}
+
+static inline void gl_transform_rect(struct gl_transform t, struct mp_rect_f *r)
+{
+ gl_transform_vec(t, &r->x0, &r->y0);
+ gl_transform_vec(t, &r->x1, &r->y1);
+}
+
+static inline bool gl_transform_eq(struct gl_transform a, struct gl_transform b)
+{
+ for (int x = 0; x < 2; x++) {
+ for (int y = 0; y < 2; y++) {
+ if (a.m[x][y] != b.m[x][y])
+ return false;
+ }
+ }
+
+ return a.t[0] == b.t[0] && a.t[1] == b.t[1];
+}
+
+void gl_transform_trans(struct gl_transform t, struct gl_transform *x);
+
+void gl_transform_ortho_fbo(struct gl_transform *t, struct ra_fbo fbo);
+
+float gl_video_scale_ambient_lux(float lmin, float lmax,
+ float rmin, float rmax, float lux);
+
+// A pool of buffers, which can grow as needed
+struct ra_buf_pool {
+ struct ra_buf_params current_params;
+ struct ra_buf **buffers;
+ int num_buffers;
+ int index;
+};
+
+void ra_buf_pool_uninit(struct ra *ra, struct ra_buf_pool *pool);
+
+// Note: params->initial_data is *not* supported
+struct ra_buf *ra_buf_pool_get(struct ra *ra, struct ra_buf_pool *pool,
+ const struct ra_buf_params *params);
+
+// Helper that wraps ra_tex_upload using texture upload buffers to ensure that
+// params->buf is always set. This is intended for RA-internal usage.
+bool ra_tex_upload_pbo(struct ra *ra, struct ra_buf_pool *pbo,
+ const struct ra_tex_upload_params *params);
+
+// Layout rules for GLSL's packing modes
+struct ra_layout std140_layout(struct ra_renderpass_input *inp);
+struct ra_layout std430_layout(struct ra_renderpass_input *inp);
+
+bool ra_tex_resize(struct ra *ra, struct mp_log *log, struct ra_tex **tex,
+ int w, int h, const struct ra_format *fmt);
+
+// A wrapper around ra_timer that does result pooling, averaging etc.
+struct timer_pool;
+
+struct timer_pool *timer_pool_create(struct ra *ra);
+void timer_pool_destroy(struct timer_pool *pool);
+void timer_pool_start(struct timer_pool *pool);
+void timer_pool_stop(struct timer_pool *pool);
+struct mp_pass_perf timer_pool_measure(struct timer_pool *pool);
+
+// print a multi line string with line numbers (e.g. for shader sources)
+// log, lev: module and log level, as in mp_msg()
+void mp_log_source(struct mp_log *log, int lev, const char *src);
diff --git a/video/out/gpu/video.c b/video/out/gpu/video.c
new file mode 100644
index 0000000..852ee78
--- /dev/null
+++ b/video/out/gpu/video.c
@@ -0,0 +1,4364 @@
+/*
+ * 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 <assert.h>
+#include <float.h>
+#include <math.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include <libavutil/common.h>
+#include <libavutil/lfg.h>
+
+#include "video.h"
+
+#include "misc/bstr.h"
+#include "options/m_config.h"
+#include "options/path.h"
+#include "common/global.h"
+#include "options/options.h"
+#include "utils.h"
+#include "hwdec.h"
+#include "osd.h"
+#include "ra.h"
+#include "stream/stream.h"
+#include "video_shaders.h"
+#include "user_shaders.h"
+#include "error_diffusion.h"
+#include "video/out/filter_kernels.h"
+#include "video/out/aspect.h"
+#include "video/out/dither.h"
+#include "video/out/vo.h"
+
+// scale/cscale arguments that map directly to shader filter routines.
+// Note that the convolution filters are not included in this list.
+static const char *const fixed_scale_filters[] = {
+ "bilinear",
+ "bicubic_fast",
+ "oversample",
+ NULL
+};
+static const char *const fixed_tscale_filters[] = {
+ "oversample",
+ "linear",
+ NULL
+};
+
+// must be sorted, and terminated with 0
+int filter_sizes[] =
+ {2, 4, 6, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 0};
+int tscale_sizes[] = {2, 4, 6, 8, 0};
+
+struct vertex_pt {
+ float x, y;
+};
+
+struct texplane {
+ struct ra_tex *tex;
+ int w, h;
+ bool flipped;
+};
+
+struct video_image {
+ struct texplane planes[4];
+ struct mp_image *mpi; // original input image
+ uint64_t id; // unique ID identifying mpi contents
+ bool hwdec_mapped;
+};
+
+enum plane_type {
+ PLANE_NONE = 0,
+ PLANE_RGB,
+ PLANE_LUMA,
+ PLANE_CHROMA,
+ PLANE_ALPHA,
+ PLANE_XYZ,
+};
+
+static const char *plane_names[] = {
+ [PLANE_NONE] = "unknown",
+ [PLANE_RGB] = "rgb",
+ [PLANE_LUMA] = "luma",
+ [PLANE_CHROMA] = "chroma",
+ [PLANE_ALPHA] = "alpha",
+ [PLANE_XYZ] = "xyz",
+};
+
+// A self-contained description of a source image which can be bound to a
+// texture unit and sampled from. Contains metadata about how it's to be used
+struct image {
+ enum plane_type type; // must be set to something non-zero
+ int components; // number of relevant coordinates
+ float multiplier; // multiplier to be used when sampling
+ struct ra_tex *tex;
+ int w, h; // logical size (after transformation)
+ struct gl_transform transform; // rendering transformation
+ int padding; // number of leading padding components (e.g. 2 = rg is padding)
+};
+
+// A named image, for user scripting purposes
+struct saved_img {
+ const char *name;
+ struct image img;
+};
+
+// A texture hook. This is some operation that transforms a named texture as
+// soon as it's generated
+struct tex_hook {
+ const char *save_tex;
+ const char *hook_tex[SHADER_MAX_HOOKS];
+ const char *bind_tex[SHADER_MAX_BINDS];
+ int components; // how many components are relevant (0 = same as input)
+ bool align_offset; // whether to align hooked tex with reference.
+ void *priv; // this gets talloc_freed when the tex_hook is removed
+ void (*hook)(struct gl_video *p, struct image img, // generates GLSL
+ struct gl_transform *trans, void *priv);
+ bool (*cond)(struct gl_video *p, struct image img, void *priv);
+};
+
+struct surface {
+ struct ra_tex *tex;
+ uint64_t id;
+ double pts;
+};
+
+#define SURFACES_MAX 10
+
+struct cached_file {
+ char *path;
+ struct bstr body;
+};
+
+struct pass_info {
+ struct bstr desc;
+ struct mp_pass_perf perf;
+};
+
+struct dr_buffer {
+ struct ra_buf *buf;
+ // The mpi reference will keep the data from being recycled (or from other
+ // references gaining write access) while the GPU is accessing the buffer.
+ struct mp_image *mpi;
+};
+
+struct gl_video {
+ struct ra *ra;
+
+ struct mpv_global *global;
+ struct mp_log *log;
+ struct gl_video_opts opts;
+ struct m_config_cache *opts_cache;
+ struct gl_lcms *cms;
+
+ int fb_depth; // actual bits available in GL main framebuffer
+ struct m_color clear_color;
+ bool force_clear_color;
+
+ struct gl_shader_cache *sc;
+
+ struct osd_state *osd_state;
+ struct mpgl_osd *osd;
+ double osd_pts;
+
+ struct ra_tex *lut_3d_texture;
+ bool use_lut_3d;
+ int lut_3d_size[3];
+
+ struct ra_tex *dither_texture;
+
+ struct mp_image_params real_image_params; // configured format
+ struct mp_image_params image_params; // texture format (mind hwdec case)
+ struct ra_imgfmt_desc ra_format; // texture format
+ int plane_count;
+
+ bool is_gray;
+ bool has_alpha;
+ char color_swizzle[5];
+ bool use_integer_conversion;
+
+ struct video_image image;
+
+ struct dr_buffer *dr_buffers;
+ int num_dr_buffers;
+
+ bool using_dr_path;
+
+ bool dumb_mode;
+ bool forced_dumb_mode;
+
+ // Cached vertex array, to avoid re-allocation per frame. For simplicity,
+ // our vertex format is simply a list of `vertex_pt`s, since this greatly
+ // simplifies offset calculation at the cost of (unneeded) flexibility.
+ struct vertex_pt *tmp_vertex;
+ struct ra_renderpass_input *vao;
+ int vao_len;
+
+ const struct ra_format *fbo_format;
+ struct ra_tex *merge_tex[4];
+ struct ra_tex *scale_tex[4];
+ struct ra_tex *integer_tex[4];
+ struct ra_tex *indirect_tex;
+ struct ra_tex *blend_subs_tex;
+ struct ra_tex *error_diffusion_tex[2];
+ struct ra_tex *screen_tex;
+ struct ra_tex *output_tex;
+ struct ra_tex **hook_textures;
+ int num_hook_textures;
+ int idx_hook_textures;
+
+ struct ra_buf *hdr_peak_ssbo;
+ struct surface surfaces[SURFACES_MAX];
+
+ // user pass descriptions and textures
+ struct tex_hook *tex_hooks;
+ int num_tex_hooks;
+ struct gl_user_shader_tex *user_textures;
+ int num_user_textures;
+
+ int surface_idx;
+ int surface_now;
+ int frames_drawn;
+ bool is_interpolated;
+ bool output_tex_valid;
+
+ // state for configured scalers
+ struct scaler scaler[SCALER_COUNT];
+
+ struct mp_csp_equalizer_state *video_eq;
+
+ struct mp_rect src_rect; // displayed part of the source video
+ struct mp_rect dst_rect; // video rectangle on output window
+ struct mp_osd_res osd_rect; // OSD size/margins
+
+ // temporary during rendering
+ struct compute_info pass_compute; // compute shader metadata for this pass
+ struct image *pass_imgs; // bound images for this pass
+ int num_pass_imgs;
+ struct saved_img *saved_imgs; // saved (named) images for this frame
+ int num_saved_imgs;
+
+ // effective current texture metadata - this will essentially affect the
+ // next render pass target, as well as implicitly tracking what needs to
+ // be done with the image
+ int texture_w, texture_h;
+ struct gl_transform texture_offset; // texture transform without rotation
+ int components;
+ bool use_linear;
+ float user_gamma;
+
+ // pass info / metrics
+ struct pass_info pass_fresh[VO_PASS_PERF_MAX];
+ struct pass_info pass_redraw[VO_PASS_PERF_MAX];
+ struct pass_info *pass;
+ int pass_idx;
+ struct timer_pool *upload_timer;
+ struct timer_pool *blit_timer;
+ struct timer_pool *osd_timer;
+
+ int frames_uploaded;
+ int frames_rendered;
+ AVLFG lfg;
+
+ // Cached because computing it can take relatively long
+ int last_dither_matrix_size;
+ float *last_dither_matrix;
+
+ struct cached_file *files;
+ int num_files;
+
+ struct ra_hwdec_ctx hwdec_ctx;
+ struct ra_hwdec_mapper *hwdec_mapper;
+ struct ra_hwdec *hwdec_overlay;
+ bool hwdec_active;
+
+ bool dsi_warned;
+ bool broken_frame; // temporary error state
+
+ bool colorspace_override_warned;
+ bool correct_downscaling_warned;
+};
+
+static const struct gl_video_opts gl_video_opts_def = {
+ .dither_algo = DITHER_FRUIT,
+ .dither_size = 6,
+ .temporal_dither_period = 1,
+ .error_diffusion = "sierra-lite",
+ .fbo_format = "auto",
+ .sigmoid_center = 0.75,
+ .sigmoid_slope = 6.5,
+ .scaler = {
+ {{"lanczos", .params={NAN, NAN}}, {.params = {NAN, NAN}}}, // scale
+ {{"hermite", .params={NAN, NAN}}, {.params = {NAN, NAN}}}, // dscale
+ {{NULL, .params={NAN, NAN}}, {.params = {NAN, NAN}}}, // cscale
+ {{"oversample", .params={NAN, NAN}}, {.params = {NAN, NAN}}}, // tscale
+ },
+ .scaler_resizes_only = true,
+ .correct_downscaling = true,
+ .linear_downscaling = true,
+ .sigmoid_upscaling = true,
+ .interpolation_threshold = 0.01,
+ .alpha_mode = ALPHA_BLEND_TILES,
+ .background = {0, 0, 0, 255},
+ .gamma = 1.0f,
+ .tone_map = {
+ .curve = TONE_MAPPING_AUTO,
+ .curve_param = NAN,
+ .max_boost = 1.0,
+ .decay_rate = 20.0,
+ .scene_threshold_low = 1.0,
+ .scene_threshold_high = 3.0,
+ .contrast_smoothness = 3.5,
+ },
+ .early_flush = -1,
+ .shader_cache = true,
+ .hwdec_interop = "auto",
+};
+
+static int validate_scaler_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value);
+
+static int validate_window_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value);
+
+static int validate_error_diffusion_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value);
+
+#define OPT_BASE_STRUCT struct gl_video_opts
+
+// Use for options which use NAN for defaults.
+#define OPT_FLOATDEF(field) \
+ OPT_FLOAT(field), \
+ .flags = M_OPT_DEFAULT_NAN
+
+#define SCALER_OPTS(n, i) \
+ {n, OPT_STRING_VALIDATE(scaler[i].kernel.name, validate_scaler_opt)}, \
+ {n"-param1", OPT_FLOATDEF(scaler[i].kernel.params[0])}, \
+ {n"-param2", OPT_FLOATDEF(scaler[i].kernel.params[1])}, \
+ {n"-blur", OPT_FLOAT(scaler[i].kernel.blur)}, \
+ {n"-cutoff", OPT_REMOVED("Hard-coded as 0.001")}, \
+ {n"-taper", OPT_FLOAT(scaler[i].kernel.taper), M_RANGE(0.0, 1.0)}, \
+ {n"-wparam", OPT_FLOATDEF(scaler[i].window.params[0])}, \
+ {n"-wblur", OPT_REMOVED("Just adjust filter radius directly")}, \
+ {n"-wtaper", OPT_FLOAT(scaler[i].window.taper), M_RANGE(0.0, 1.0)}, \
+ {n"-clamp", OPT_FLOAT(scaler[i].clamp), M_RANGE(0.0, 1.0)}, \
+ {n"-radius", OPT_FLOAT(scaler[i].radius), M_RANGE(0.5, 16.0)}, \
+ {n"-antiring", OPT_FLOAT(scaler[i].antiring), M_RANGE(0.0, 1.0)}, \
+ {n"-window", OPT_STRING_VALIDATE(scaler[i].window.name, validate_window_opt)}
+
+const struct m_sub_options gl_video_conf = {
+ .opts = (const m_option_t[]) {
+ {"gpu-dumb-mode", OPT_CHOICE(dumb_mode,
+ {"auto", 0}, {"yes", 1}, {"no", -1})},
+ {"gamma-factor", OPT_FLOAT(gamma), M_RANGE(0.1, 2.0),
+ .deprecation_message = "no replacement"},
+ {"gamma-auto", OPT_BOOL(gamma_auto),
+ .deprecation_message = "no replacement"},
+ {"target-prim", OPT_CHOICE_C(target_prim, mp_csp_prim_names)},
+ {"target-trc", OPT_CHOICE_C(target_trc, mp_csp_trc_names)},
+ {"target-peak", OPT_CHOICE(target_peak, {"auto", 0}),
+ M_RANGE(10, 10000)},
+ {"target-contrast", OPT_CHOICE(target_contrast, {"auto", 0}, {"inf", -1}),
+ M_RANGE(10, 1000000)},
+ {"target-gamut", OPT_CHOICE_C(target_gamut, mp_csp_prim_names)},
+ {"tone-mapping", OPT_CHOICE(tone_map.curve,
+ {"auto", TONE_MAPPING_AUTO},
+ {"clip", TONE_MAPPING_CLIP},
+ {"mobius", TONE_MAPPING_MOBIUS},
+ {"reinhard", TONE_MAPPING_REINHARD},
+ {"hable", TONE_MAPPING_HABLE},
+ {"gamma", TONE_MAPPING_GAMMA},
+ {"linear", TONE_MAPPING_LINEAR},
+ {"spline", TONE_MAPPING_SPLINE},
+ {"bt.2390", TONE_MAPPING_BT_2390},
+ {"bt.2446a", TONE_MAPPING_BT_2446A},
+ {"st2094-40", TONE_MAPPING_ST2094_40},
+ {"st2094-10", TONE_MAPPING_ST2094_10})},
+ {"tone-mapping-param", OPT_FLOATDEF(tone_map.curve_param)},
+ {"inverse-tone-mapping", OPT_BOOL(tone_map.inverse)},
+ {"tone-mapping-max-boost", OPT_FLOAT(tone_map.max_boost),
+ M_RANGE(1.0, 10.0)},
+ {"tone-mapping-visualize", OPT_BOOL(tone_map.visualize)},
+ {"gamut-mapping-mode", OPT_CHOICE(tone_map.gamut_mode,
+ {"auto", GAMUT_AUTO},
+ {"clip", GAMUT_CLIP},
+ {"perceptual", GAMUT_PERCEPTUAL},
+ {"relative", GAMUT_RELATIVE},
+ {"saturation", GAMUT_SATURATION},
+ {"absolute", GAMUT_ABSOLUTE},
+ {"desaturate", GAMUT_DESATURATE},
+ {"darken", GAMUT_DARKEN},
+ {"warn", GAMUT_WARN},
+ {"linear", GAMUT_LINEAR})},
+ {"hdr-compute-peak", OPT_CHOICE(tone_map.compute_peak,
+ {"auto", 0},
+ {"yes", 1},
+ {"no", -1})},
+ {"hdr-peak-percentile", OPT_FLOAT(tone_map.peak_percentile),
+ M_RANGE(0.0, 100.0)},
+ {"hdr-peak-decay-rate", OPT_FLOAT(tone_map.decay_rate),
+ M_RANGE(0.0, 1000.0)},
+ {"hdr-scene-threshold-low", OPT_FLOAT(tone_map.scene_threshold_low),
+ M_RANGE(0, 20.0)},
+ {"hdr-scene-threshold-high", OPT_FLOAT(tone_map.scene_threshold_high),
+ M_RANGE(0, 20.0)},
+ {"hdr-contrast-recovery", OPT_FLOAT(tone_map.contrast_recovery),
+ M_RANGE(0, 2.0)},
+ {"hdr-contrast-smoothness", OPT_FLOAT(tone_map.contrast_smoothness),
+ M_RANGE(1.0, 100.0)},
+ {"opengl-pbo", OPT_BOOL(pbo)},
+ SCALER_OPTS("scale", SCALER_SCALE),
+ SCALER_OPTS("dscale", SCALER_DSCALE),
+ SCALER_OPTS("cscale", SCALER_CSCALE),
+ SCALER_OPTS("tscale", SCALER_TSCALE),
+ {"scaler-lut-size", OPT_REMOVED("hard-coded as 8")},
+ {"scaler-resizes-only", OPT_BOOL(scaler_resizes_only)},
+ {"correct-downscaling", OPT_BOOL(correct_downscaling)},
+ {"linear-downscaling", OPT_BOOL(linear_downscaling)},
+ {"linear-upscaling", OPT_BOOL(linear_upscaling)},
+ {"sigmoid-upscaling", OPT_BOOL(sigmoid_upscaling)},
+ {"sigmoid-center", OPT_FLOAT(sigmoid_center), M_RANGE(0.0, 1.0)},
+ {"sigmoid-slope", OPT_FLOAT(sigmoid_slope), M_RANGE(1.0, 20.0)},
+ {"fbo-format", OPT_STRING(fbo_format)},
+ {"dither-depth", OPT_CHOICE(dither_depth, {"no", -1}, {"auto", 0}),
+ M_RANGE(-1, 16)},
+ {"dither", OPT_CHOICE(dither_algo,
+ {"fruit", DITHER_FRUIT},
+ {"ordered", DITHER_ORDERED},
+ {"error-diffusion", DITHER_ERROR_DIFFUSION},
+ {"no", DITHER_NONE})},
+ {"dither-size-fruit", OPT_INT(dither_size), M_RANGE(2, 8)},
+ {"temporal-dither", OPT_BOOL(temporal_dither)},
+ {"temporal-dither-period", OPT_INT(temporal_dither_period),
+ M_RANGE(1, 128)},
+ {"error-diffusion",
+ OPT_STRING_VALIDATE(error_diffusion, validate_error_diffusion_opt)},
+ {"alpha", OPT_CHOICE(alpha_mode,
+ {"no", ALPHA_NO},
+ {"yes", ALPHA_YES},
+ {"blend", ALPHA_BLEND},
+ {"blend-tiles", ALPHA_BLEND_TILES})},
+ {"opengl-rectangle-textures", OPT_BOOL(use_rectangle)},
+ {"background", OPT_COLOR(background)},
+ {"interpolation", OPT_BOOL(interpolation)},
+ {"interpolation-threshold", OPT_FLOAT(interpolation_threshold)},
+ {"blend-subtitles", OPT_CHOICE(blend_subs,
+ {"no", BLEND_SUBS_NO},
+ {"yes", BLEND_SUBS_YES},
+ {"video", BLEND_SUBS_VIDEO})},
+ {"glsl-shaders", OPT_PATHLIST(user_shaders), .flags = M_OPT_FILE},
+ {"glsl-shader", OPT_CLI_ALIAS("glsl-shaders-append")},
+ {"glsl-shader-opts", OPT_KEYVALUELIST(user_shader_opts)},
+ {"deband", OPT_BOOL(deband)},
+ {"deband", OPT_SUBSTRUCT(deband_opts, deband_conf)},
+ {"sharpen", OPT_FLOAT(unsharp)},
+ {"gpu-tex-pad-x", OPT_INT(tex_pad_x), M_RANGE(0, 4096)},
+ {"gpu-tex-pad-y", OPT_INT(tex_pad_y), M_RANGE(0, 4096)},
+ {"", OPT_SUBSTRUCT(icc_opts, mp_icc_conf)},
+ {"gpu-shader-cache", OPT_BOOL(shader_cache)},
+ {"gpu-shader-cache-dir", OPT_STRING(shader_cache_dir), .flags = M_OPT_FILE},
+ {"gpu-hwdec-interop",
+ OPT_STRING_VALIDATE(hwdec_interop, ra_hwdec_validate_opt)},
+ {"gamut-warning", OPT_REMOVED("Replaced by --gamut-mapping-mode=warn")},
+ {"gamut-clipping", OPT_REMOVED("Replaced by --gamut-mapping-mode=desaturate")},
+ {"tone-mapping-desaturate", OPT_REMOVED("Replaced by --tone-mapping-mode")},
+ {"tone-mapping-desaturate-exponent", OPT_REMOVED("Replaced by --tone-mapping-mode")},
+ {"tone-mapping-crosstalk", OPT_REMOVED("Hard-coded as 0.04")},
+ {"tone-mapping-mode", OPT_REMOVED("no replacement")},
+ {0}
+ },
+ .size = sizeof(struct gl_video_opts),
+ .defaults = &gl_video_opts_def,
+};
+
+static void uninit_rendering(struct gl_video *p);
+static void uninit_scaler(struct gl_video *p, struct scaler *scaler);
+static void check_gl_features(struct gl_video *p);
+static bool pass_upload_image(struct gl_video *p, struct mp_image *mpi, uint64_t id);
+static const char *handle_scaler_opt(const char *name, bool tscale);
+static void reinit_from_options(struct gl_video *p);
+static void get_scale_factors(struct gl_video *p, bool transpose_rot, double xy[2]);
+static void gl_video_setup_hooks(struct gl_video *p);
+static void gl_video_update_options(struct gl_video *p);
+
+#define GLSL(x) gl_sc_add(p->sc, #x "\n");
+#define GLSLF(...) gl_sc_addf(p->sc, __VA_ARGS__)
+#define GLSLHF(...) gl_sc_haddf(p->sc, __VA_ARGS__)
+#define PRELUDE(...) gl_sc_paddf(p->sc, __VA_ARGS__)
+
+static struct bstr load_cached_file(struct gl_video *p, const char *path)
+{
+ if (!path || !path[0])
+ return (struct bstr){0};
+ for (int n = 0; n < p->num_files; n++) {
+ if (strcmp(p->files[n].path, path) == 0)
+ return p->files[n].body;
+ }
+ // not found -> load it
+ char *fname = mp_get_user_path(NULL, p->global, path);
+ struct bstr s = stream_read_file(fname, p, p->global, 1000000000); // 1GB
+ talloc_free(fname);
+ if (s.len) {
+ struct cached_file new = {
+ .path = talloc_strdup(p, path),
+ .body = s,
+ };
+ MP_TARRAY_APPEND(p, p->files, p->num_files, new);
+ return new.body;
+ }
+ return (struct bstr){0};
+}
+
+static void debug_check_gl(struct gl_video *p, const char *msg)
+{
+ if (p->ra->fns->debug_marker)
+ p->ra->fns->debug_marker(p->ra, msg);
+}
+
+static void gl_video_reset_surfaces(struct gl_video *p)
+{
+ for (int i = 0; i < SURFACES_MAX; i++) {
+ p->surfaces[i].id = 0;
+ p->surfaces[i].pts = MP_NOPTS_VALUE;
+ }
+ p->surface_idx = 0;
+ p->surface_now = 0;
+ p->frames_drawn = 0;
+ p->output_tex_valid = false;
+}
+
+static void gl_video_reset_hooks(struct gl_video *p)
+{
+ for (int i = 0; i < p->num_tex_hooks; i++)
+ talloc_free(p->tex_hooks[i].priv);
+
+ for (int i = 0; i < p->num_user_textures; i++)
+ ra_tex_free(p->ra, &p->user_textures[i].tex);
+
+ p->num_tex_hooks = 0;
+ p->num_user_textures = 0;
+}
+
+static inline int surface_wrap(int id)
+{
+ id = id % SURFACES_MAX;
+ return id < 0 ? id + SURFACES_MAX : id;
+}
+
+static void reinit_osd(struct gl_video *p)
+{
+ mpgl_osd_destroy(p->osd);
+ p->osd = NULL;
+ if (p->osd_state)
+ p->osd = mpgl_osd_init(p->ra, p->log, p->osd_state);
+}
+
+static void uninit_rendering(struct gl_video *p)
+{
+ for (int n = 0; n < SCALER_COUNT; n++)
+ uninit_scaler(p, &p->scaler[n]);
+
+ ra_tex_free(p->ra, &p->dither_texture);
+
+ for (int n = 0; n < 4; n++) {
+ ra_tex_free(p->ra, &p->merge_tex[n]);
+ ra_tex_free(p->ra, &p->scale_tex[n]);
+ ra_tex_free(p->ra, &p->integer_tex[n]);
+ }
+
+ ra_tex_free(p->ra, &p->indirect_tex);
+ ra_tex_free(p->ra, &p->blend_subs_tex);
+ ra_tex_free(p->ra, &p->screen_tex);
+ ra_tex_free(p->ra, &p->output_tex);
+
+ for (int n = 0; n < 2; n++)
+ ra_tex_free(p->ra, &p->error_diffusion_tex[n]);
+
+ for (int n = 0; n < SURFACES_MAX; n++)
+ ra_tex_free(p->ra, &p->surfaces[n].tex);
+
+ for (int n = 0; n < p->num_hook_textures; n++)
+ ra_tex_free(p->ra, &p->hook_textures[n]);
+
+ gl_video_reset_surfaces(p);
+ gl_video_reset_hooks(p);
+
+ gl_sc_reset_error(p->sc);
+}
+
+bool gl_video_gamma_auto_enabled(struct gl_video *p)
+{
+ return p->opts.gamma_auto;
+}
+
+struct mp_colorspace gl_video_get_output_colorspace(struct gl_video *p)
+{
+ return (struct mp_colorspace) {
+ .primaries = p->opts.target_prim,
+ .gamma = p->opts.target_trc,
+ .hdr.max_luma = p->opts.target_peak,
+ };
+}
+
+// Warning: profile.start must point to a ta allocation, and the function
+// takes over ownership.
+void gl_video_set_icc_profile(struct gl_video *p, bstr icc_data)
+{
+ if (gl_lcms_set_memory_profile(p->cms, icc_data))
+ reinit_from_options(p);
+}
+
+bool gl_video_icc_auto_enabled(struct gl_video *p)
+{
+ return p->opts.icc_opts ? p->opts.icc_opts->profile_auto : false;
+}
+
+static bool gl_video_get_lut3d(struct gl_video *p, enum mp_csp_prim prim,
+ enum mp_csp_trc trc)
+{
+ if (!p->use_lut_3d)
+ return false;
+
+ struct AVBufferRef *icc = NULL;
+ if (p->image.mpi)
+ icc = p->image.mpi->icc_profile;
+
+ if (p->lut_3d_texture && !gl_lcms_has_changed(p->cms, prim, trc, icc))
+ return true;
+
+ // GLES3 doesn't provide filtered 16 bit integer textures
+ // GLES2 doesn't even provide 3D textures
+ const struct ra_format *fmt = ra_find_unorm_format(p->ra, 2, 4);
+ if (!fmt || !(p->ra->caps & RA_CAP_TEX_3D)) {
+ p->use_lut_3d = false;
+ MP_WARN(p, "Disabling color management (no RGBA16 3D textures).\n");
+ return false;
+ }
+
+ struct lut3d *lut3d = NULL;
+ if (!fmt || !gl_lcms_get_lut3d(p->cms, &lut3d, prim, trc, icc) || !lut3d) {
+ p->use_lut_3d = false;
+ return false;
+ }
+
+ ra_tex_free(p->ra, &p->lut_3d_texture);
+
+ struct ra_tex_params params = {
+ .dimensions = 3,
+ .w = lut3d->size[0],
+ .h = lut3d->size[1],
+ .d = lut3d->size[2],
+ .format = fmt,
+ .render_src = true,
+ .src_linear = true,
+ .initial_data = lut3d->data,
+ };
+ p->lut_3d_texture = ra_tex_create(p->ra, &params);
+
+ debug_check_gl(p, "after 3d lut creation");
+
+ for (int i = 0; i < 3; i++)
+ p->lut_3d_size[i] = lut3d->size[i];
+
+ talloc_free(lut3d);
+
+ if (!p->lut_3d_texture) {
+ p->use_lut_3d = false;
+ return false;
+ }
+
+ return true;
+}
+
+// Fill an image struct from a ra_tex + some metadata
+static struct image image_wrap(struct ra_tex *tex, enum plane_type type,
+ int components)
+{
+ assert(type != PLANE_NONE);
+ return (struct image){
+ .type = type,
+ .tex = tex,
+ .multiplier = 1.0,
+ .w = tex ? tex->params.w : 1,
+ .h = tex ? tex->params.h : 1,
+ .transform = identity_trans,
+ .components = components,
+ };
+}
+
+// Bind an image to a free texture unit and return its ID.
+static int pass_bind(struct gl_video *p, struct image img)
+{
+ int idx = p->num_pass_imgs;
+ MP_TARRAY_APPEND(p, p->pass_imgs, p->num_pass_imgs, img);
+ return idx;
+}
+
+// Rotation by 90° and flipping.
+// w/h is used for recentering.
+static void get_transform(float w, float h, int rotate, bool flip,
+ struct gl_transform *out_tr)
+{
+ int a = rotate % 90 ? 0 : rotate / 90;
+ int sin90[4] = {0, 1, 0, -1}; // just to avoid rounding issues etc.
+ int cos90[4] = {1, 0, -1, 0};
+ struct gl_transform tr = {{{ cos90[a], sin90[a]},
+ {-sin90[a], cos90[a]}}};
+
+ // basically, recenter to keep the whole image in view
+ float b[2] = {1, 1};
+ gl_transform_vec(tr, &b[0], &b[1]);
+ tr.t[0] += b[0] < 0 ? w : 0;
+ tr.t[1] += b[1] < 0 ? h : 0;
+
+ if (flip) {
+ struct gl_transform fliptr = {{{1, 0}, {0, -1}}, {0, h}};
+ gl_transform_trans(fliptr, &tr);
+ }
+
+ *out_tr = tr;
+}
+
+// Return the chroma plane upscaled to luma size, but with additional padding
+// for image sizes not aligned to subsampling.
+static int chroma_upsize(int size, int pixel)
+{
+ return (size + pixel - 1) / pixel * pixel;
+}
+
+// If a and b are on the same plane, return what plane type should be used.
+// If a or b are none, the other type always wins.
+// Usually: LUMA/RGB/XYZ > CHROMA > ALPHA
+static enum plane_type merge_plane_types(enum plane_type a, enum plane_type b)
+{
+ if (a == PLANE_NONE)
+ return b;
+ if (b == PLANE_LUMA || b == PLANE_RGB || b == PLANE_XYZ)
+ return b;
+ if (b != PLANE_NONE && a == PLANE_ALPHA)
+ return b;
+ return a;
+}
+
+// Places a video_image's image textures + associated metadata into img[]. The
+// number of textures is equal to p->plane_count. Any necessary plane offsets
+// are stored in off. (e.g. chroma position)
+static void pass_get_images(struct gl_video *p, struct video_image *vimg,
+ struct image img[4], struct gl_transform off[4])
+{
+ assert(vimg->mpi);
+
+ int w = p->image_params.w;
+ int h = p->image_params.h;
+
+ // Determine the chroma offset
+ float ls_w = 1.0 / p->ra_format.chroma_w;
+ float ls_h = 1.0 / p->ra_format.chroma_h;
+
+ struct gl_transform chroma = {{{ls_w, 0.0}, {0.0, ls_h}}};
+
+ if (p->image_params.chroma_location != MP_CHROMA_CENTER) {
+ int cx, cy;
+ mp_get_chroma_location(p->image_params.chroma_location, &cx, &cy);
+ // By default texture coordinates are such that chroma is centered with
+ // any chroma subsampling. If a specific direction is given, make it
+ // so that the luma and chroma sample line up exactly.
+ // For 4:4:4, setting chroma location should have no effect at all.
+ // luma sample size (in chroma coord. space)
+ chroma.t[0] = ls_w < 1 ? ls_w * -cx / 2 : 0;
+ chroma.t[1] = ls_h < 1 ? ls_h * -cy / 2 : 0;
+ }
+
+ memset(img, 0, 4 * sizeof(img[0]));
+ for (int n = 0; n < p->plane_count; n++) {
+ struct texplane *t = &vimg->planes[n];
+
+ enum plane_type type = PLANE_NONE;
+ int padding = 0;
+ for (int i = 0; i < 4; i++) {
+ int c = p->ra_format.components[n][i];
+ enum plane_type ctype;
+ if (c == 0) {
+ ctype = PLANE_NONE;
+ } else if (c == 4) {
+ ctype = PLANE_ALPHA;
+ } else if (p->image_params.color.space == MP_CSP_RGB) {
+ ctype = PLANE_RGB;
+ } else if (p->image_params.color.space == MP_CSP_XYZ) {
+ ctype = PLANE_XYZ;
+ } else {
+ ctype = c == 1 ? PLANE_LUMA : PLANE_CHROMA;
+ }
+ type = merge_plane_types(type, ctype);
+ if (!c && padding == i)
+ padding = i + 1;
+ }
+
+ int msb_valid_bits =
+ p->ra_format.component_bits + MPMIN(p->ra_format.component_pad, 0);
+ int csp = type == PLANE_ALPHA ? MP_CSP_RGB : p->image_params.color.space;
+ float tex_mul =
+ 1.0 / mp_get_csp_mul(csp, msb_valid_bits, p->ra_format.component_bits);
+ if (p->ra_format.component_type == RA_CTYPE_FLOAT)
+ tex_mul = 1.0;
+
+ img[n] = (struct image){
+ .type = type,
+ .tex = t->tex,
+ .multiplier = tex_mul,
+ .w = t->w,
+ .h = t->h,
+ .padding = padding,
+ };
+
+ for (int i = 0; i < 4; i++)
+ img[n].components += !!p->ra_format.components[n][i];
+
+ get_transform(t->w, t->h, p->image_params.rotate, t->flipped,
+ &img[n].transform);
+ if (p->image_params.rotate % 180 == 90)
+ MPSWAP(int, img[n].w, img[n].h);
+
+ off[n] = identity_trans;
+
+ if (type == PLANE_CHROMA) {
+ struct gl_transform rot;
+ get_transform(0, 0, p->image_params.rotate, true, &rot);
+
+ struct gl_transform tr = chroma;
+ gl_transform_vec(rot, &tr.t[0], &tr.t[1]);
+
+ float dx = (chroma_upsize(w, p->ra_format.chroma_w) - w) * ls_w;
+ float dy = (chroma_upsize(h, p->ra_format.chroma_h) - h) * ls_h;
+
+ // Adjust the chroma offset if the real chroma size is fractional
+ // due image sizes not aligned to chroma subsampling.
+ struct gl_transform rot2;
+ get_transform(0, 0, p->image_params.rotate, t->flipped, &rot2);
+ if (rot2.m[0][0] < 0)
+ tr.t[0] += dx;
+ if (rot2.m[1][0] < 0)
+ tr.t[0] += dy;
+ if (rot2.m[0][1] < 0)
+ tr.t[1] += dx;
+ if (rot2.m[1][1] < 0)
+ tr.t[1] += dy;
+
+ off[n] = tr;
+ }
+ }
+}
+
+// Return the index of the given component (assuming all non-padding components
+// of all planes are concatenated into a linear list).
+static int find_comp(struct ra_imgfmt_desc *desc, int component)
+{
+ int cur = 0;
+ for (int n = 0; n < desc->num_planes; n++) {
+ for (int i = 0; i < 4; i++) {
+ if (desc->components[n][i]) {
+ if (desc->components[n][i] == component)
+ return cur;
+ cur++;
+ }
+ }
+ }
+ return -1;
+}
+
+static void init_video(struct gl_video *p)
+{
+ p->use_integer_conversion = false;
+
+ struct ra_hwdec *hwdec = ra_hwdec_get(&p->hwdec_ctx, p->image_params.imgfmt);
+ if (hwdec) {
+ if (hwdec->driver->overlay_frame) {
+ MP_WARN(p, "Using HW-overlay mode. No GL filtering is performed "
+ "on the video!\n");
+ p->hwdec_overlay = hwdec;
+ } else {
+ p->hwdec_mapper = ra_hwdec_mapper_create(hwdec, &p->image_params);
+ if (!p->hwdec_mapper)
+ MP_ERR(p, "Initializing texture for hardware decoding failed.\n");
+ }
+ if (p->hwdec_mapper)
+ p->image_params = p->hwdec_mapper->dst_params;
+ const char **exts = hwdec->glsl_extensions;
+ for (int n = 0; exts && exts[n]; n++)
+ gl_sc_enable_extension(p->sc, (char *)exts[n]);
+ p->hwdec_active = true;
+ }
+
+ p->ra_format = (struct ra_imgfmt_desc){0};
+ ra_get_imgfmt_desc(p->ra, p->image_params.imgfmt, &p->ra_format);
+
+ p->plane_count = p->ra_format.num_planes;
+
+ p->has_alpha = false;
+ p->is_gray = true;
+
+ for (int n = 0; n < p->ra_format.num_planes; n++) {
+ for (int i = 0; i < 4; i++) {
+ if (p->ra_format.components[n][i]) {
+ p->has_alpha |= p->ra_format.components[n][i] == 4;
+ p->is_gray &= p->ra_format.components[n][i] == 1 ||
+ p->ra_format.components[n][i] == 4;
+ }
+ }
+ }
+
+ for (int c = 0; c < 4; c++) {
+ int loc = find_comp(&p->ra_format, c + 1);
+ p->color_swizzle[c] = "rgba"[loc >= 0 && loc < 4 ? loc : 0];
+ }
+ p->color_swizzle[4] = '\0';
+
+ mp_image_params_guess_csp(&p->image_params);
+
+ av_lfg_init(&p->lfg, 1);
+
+ debug_check_gl(p, "before video texture creation");
+
+ if (!p->hwdec_active) {
+ struct video_image *vimg = &p->image;
+
+ struct mp_image layout = {0};
+ mp_image_set_params(&layout, &p->image_params);
+
+ for (int n = 0; n < p->plane_count; n++) {
+ struct texplane *plane = &vimg->planes[n];
+ const struct ra_format *format = p->ra_format.planes[n];
+
+ plane->w = mp_image_plane_w(&layout, n);
+ plane->h = mp_image_plane_h(&layout, n);
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = plane->w + p->opts.tex_pad_x,
+ .h = plane->h + p->opts.tex_pad_y,
+ .d = 1,
+ .format = format,
+ .render_src = true,
+ .src_linear = format->linear_filter,
+ .non_normalized = p->opts.use_rectangle,
+ .host_mutable = true,
+ };
+
+ MP_VERBOSE(p, "Texture for plane %d: %dx%d\n", n,
+ params.w, params.h);
+
+ plane->tex = ra_tex_create(p->ra, &params);
+ p->use_integer_conversion |= format->ctype == RA_CTYPE_UINT;
+ }
+ }
+
+ debug_check_gl(p, "after video texture creation");
+
+ // Format-dependent checks.
+ check_gl_features(p);
+
+ gl_video_setup_hooks(p);
+}
+
+static struct dr_buffer *gl_find_dr_buffer(struct gl_video *p, uint8_t *ptr)
+{
+ for (int i = 0; i < p->num_dr_buffers; i++) {
+ struct dr_buffer *buffer = &p->dr_buffers[i];
+ uint8_t *bufptr = buffer->buf->data;
+ size_t size = buffer->buf->params.size;
+ if (ptr >= bufptr && ptr < bufptr + size)
+ return buffer;
+ }
+
+ return NULL;
+}
+
+static void gc_pending_dr_fences(struct gl_video *p, bool force)
+{
+again:;
+ for (int n = 0; n < p->num_dr_buffers; n++) {
+ struct dr_buffer *buffer = &p->dr_buffers[n];
+ if (!buffer->mpi)
+ continue;
+
+ bool res = p->ra->fns->buf_poll(p->ra, buffer->buf);
+ if (res || force) {
+ // Unreferencing the image could cause gl_video_dr_free_buffer()
+ // to be called by the talloc destructor (if it was the last
+ // reference). This will implicitly invalidate the buffer pointer
+ // and change the p->dr_buffers array. To make it worse, it could
+ // free multiple dr_buffers due to weird theoretical corner cases.
+ // This is also why we use the goto to iterate again from the
+ // start, because everything gets fucked up. Hail satan!
+ struct mp_image *ref = buffer->mpi;
+ buffer->mpi = NULL;
+ talloc_free(ref);
+ goto again;
+ }
+ }
+}
+
+static void unref_current_image(struct gl_video *p)
+{
+ struct video_image *vimg = &p->image;
+
+ if (vimg->hwdec_mapped) {
+ assert(p->hwdec_active && p->hwdec_mapper);
+ ra_hwdec_mapper_unmap(p->hwdec_mapper);
+ memset(vimg->planes, 0, sizeof(vimg->planes));
+ vimg->hwdec_mapped = false;
+ }
+
+ vimg->id = 0;
+
+ mp_image_unrefp(&vimg->mpi);
+
+ // While we're at it, also garbage collect pending fences in here to
+ // get it out of the way.
+ gc_pending_dr_fences(p, false);
+}
+
+// If overlay mode is used, make sure to remove the overlay.
+// Be careful with this. Removing the overlay and adding another one will
+// lead to flickering artifacts.
+static void unmap_overlay(struct gl_video *p)
+{
+ if (p->hwdec_overlay)
+ p->hwdec_overlay->driver->overlay_frame(p->hwdec_overlay, NULL, NULL, NULL, true);
+}
+
+static void uninit_video(struct gl_video *p)
+{
+ uninit_rendering(p);
+
+ struct video_image *vimg = &p->image;
+
+ unmap_overlay(p);
+ unref_current_image(p);
+
+ for (int n = 0; n < p->plane_count; n++) {
+ struct texplane *plane = &vimg->planes[n];
+ ra_tex_free(p->ra, &plane->tex);
+ }
+ *vimg = (struct video_image){0};
+
+ // Invalidate image_params to ensure that gl_video_config() will call
+ // init_video() on uninitialized gl_video.
+ p->real_image_params = (struct mp_image_params){0};
+ p->image_params = p->real_image_params;
+ p->hwdec_active = false;
+ p->hwdec_overlay = NULL;
+ ra_hwdec_mapper_free(&p->hwdec_mapper);
+}
+
+static void pass_record(struct gl_video *p, struct mp_pass_perf perf)
+{
+ if (!p->pass || p->pass_idx == VO_PASS_PERF_MAX)
+ return;
+
+ struct pass_info *pass = &p->pass[p->pass_idx];
+ pass->perf = perf;
+
+ if (pass->desc.len == 0)
+ bstr_xappend(p, &pass->desc, bstr0("(unknown)"));
+
+ p->pass_idx++;
+}
+
+PRINTF_ATTRIBUTE(2, 3)
+static void pass_describe(struct gl_video *p, const char *textf, ...)
+{
+ if (!p->pass || p->pass_idx == VO_PASS_PERF_MAX)
+ return;
+
+ struct pass_info *pass = &p->pass[p->pass_idx];
+
+ if (pass->desc.len > 0)
+ bstr_xappend(p, &pass->desc, bstr0(" + "));
+
+ va_list ap;
+ va_start(ap, textf);
+ bstr_xappend_vasprintf(p, &pass->desc, textf, ap);
+ va_end(ap);
+}
+
+static void pass_info_reset(struct gl_video *p, bool is_redraw)
+{
+ p->pass = is_redraw ? p->pass_redraw : p->pass_fresh;
+ p->pass_idx = 0;
+
+ for (int i = 0; i < VO_PASS_PERF_MAX; i++) {
+ p->pass[i].desc.len = 0;
+ p->pass[i].perf = (struct mp_pass_perf){0};
+ }
+}
+
+static void pass_report_performance(struct gl_video *p)
+{
+ if (!p->pass)
+ return;
+
+ for (int i = 0; i < VO_PASS_PERF_MAX; i++) {
+ struct pass_info *pass = &p->pass[i];
+ if (pass->desc.len) {
+ MP_TRACE(p, "pass '%.*s': last %dus avg %dus peak %dus\n",
+ BSTR_P(pass->desc),
+ (int)pass->perf.last/1000,
+ (int)pass->perf.avg/1000,
+ (int)pass->perf.peak/1000);
+ }
+ }
+}
+
+static void pass_prepare_src_tex(struct gl_video *p)
+{
+ struct gl_shader_cache *sc = p->sc;
+
+ for (int n = 0; n < p->num_pass_imgs; n++) {
+ struct image *s = &p->pass_imgs[n];
+ if (!s->tex)
+ continue;
+
+ char *texture_name = mp_tprintf(32, "texture%d", n);
+ char *texture_size = mp_tprintf(32, "texture_size%d", n);
+ char *texture_rot = mp_tprintf(32, "texture_rot%d", n);
+ char *texture_off = mp_tprintf(32, "texture_off%d", n);
+ char *pixel_size = mp_tprintf(32, "pixel_size%d", n);
+
+ gl_sc_uniform_texture(sc, texture_name, s->tex);
+ float f[2] = {1, 1};
+ if (!s->tex->params.non_normalized) {
+ f[0] = s->tex->params.w;
+ f[1] = s->tex->params.h;
+ }
+ gl_sc_uniform_vec2(sc, texture_size, f);
+ gl_sc_uniform_mat2(sc, texture_rot, true, (float *)s->transform.m);
+ gl_sc_uniform_vec2(sc, texture_off, (float *)s->transform.t);
+ gl_sc_uniform_vec2(sc, pixel_size, (float[]){1.0f / f[0],
+ 1.0f / f[1]});
+ }
+}
+
+static void cleanup_binds(struct gl_video *p)
+{
+ p->num_pass_imgs = 0;
+}
+
+// Sets the appropriate compute shader metadata for an implicit compute pass
+// bw/bh: block size
+static void pass_is_compute(struct gl_video *p, int bw, int bh, bool flexible)
+{
+ if (p->pass_compute.active && flexible) {
+ // Avoid overwriting existing block sizes when using a flexible pass
+ bw = p->pass_compute.block_w;
+ bh = p->pass_compute.block_h;
+ }
+
+ p->pass_compute = (struct compute_info){
+ .active = true,
+ .block_w = bw,
+ .block_h = bh,
+ };
+}
+
+// w/h: the width/height of the compute shader's operating domain (e.g. the
+// target target that needs to be written, or the source texture that needs to
+// be reduced)
+static void dispatch_compute(struct gl_video *p, int w, int h,
+ struct compute_info info)
+{
+ PRELUDE("layout (local_size_x = %d, local_size_y = %d) in;\n",
+ info.threads_w > 0 ? info.threads_w : info.block_w,
+ info.threads_h > 0 ? info.threads_h : info.block_h);
+
+ pass_prepare_src_tex(p);
+
+ // Since we don't actually have vertices, we pretend for convenience
+ // reasons that we do and calculate the right texture coordinates based on
+ // the output sample ID
+ gl_sc_uniform_vec2(p->sc, "out_scale", (float[2]){ 1.0 / w, 1.0 / h });
+ PRELUDE("#define outcoord(id) (out_scale * (vec2(id) + vec2(0.5)))\n");
+
+ for (int n = 0; n < p->num_pass_imgs; n++) {
+ struct image *s = &p->pass_imgs[n];
+ if (!s->tex)
+ continue;
+
+ PRELUDE("#define texmap%d(id) (texture_rot%d * outcoord(id) + "
+ "pixel_size%d * texture_off%d)\n", n, n, n, n);
+ PRELUDE("#define texcoord%d texmap%d(gl_GlobalInvocationID)\n", n, n);
+ }
+
+ // always round up when dividing to make sure we don't leave off a part of
+ // the image
+ int num_x = info.block_w > 0 ? (w + info.block_w - 1) / info.block_w : 1,
+ num_y = info.block_h > 0 ? (h + info.block_h - 1) / info.block_h : 1;
+
+ if (!(p->ra->caps & RA_CAP_NUM_GROUPS))
+ PRELUDE("#define gl_NumWorkGroups uvec3(%d, %d, 1)\n", num_x, num_y);
+
+ pass_record(p, gl_sc_dispatch_compute(p->sc, num_x, num_y, 1));
+ cleanup_binds(p);
+}
+
+static struct mp_pass_perf render_pass_quad(struct gl_video *p,
+ struct ra_fbo fbo, bool discard,
+ const struct mp_rect *dst)
+{
+ // The first element is reserved for `vec2 position`
+ int num_vertex_attribs = 1 + p->num_pass_imgs;
+ size_t vertex_stride = num_vertex_attribs * sizeof(struct vertex_pt);
+
+ // Expand the VAO if necessary
+ while (p->vao_len < num_vertex_attribs) {
+ MP_TARRAY_APPEND(p, p->vao, p->vao_len, (struct ra_renderpass_input) {
+ .name = talloc_asprintf(p, "texcoord%d", p->vao_len - 1),
+ .type = RA_VARTYPE_FLOAT,
+ .dim_v = 2,
+ .dim_m = 1,
+ .offset = p->vao_len * sizeof(struct vertex_pt),
+ });
+ }
+
+ int num_vertices = 6; // quad as triangle list
+ int num_attribs_total = num_vertices * num_vertex_attribs;
+ MP_TARRAY_GROW(p, p->tmp_vertex, num_attribs_total);
+
+ struct gl_transform t;
+ gl_transform_ortho_fbo(&t, fbo);
+
+ float x[2] = {dst->x0, dst->x1};
+ float y[2] = {dst->y0, dst->y1};
+ gl_transform_vec(t, &x[0], &y[0]);
+ gl_transform_vec(t, &x[1], &y[1]);
+
+ for (int n = 0; n < 4; n++) {
+ struct vertex_pt *vs = &p->tmp_vertex[num_vertex_attribs * n];
+ // vec2 position in idx 0
+ vs[0].x = x[n / 2];
+ vs[0].y = y[n % 2];
+ for (int i = 0; i < p->num_pass_imgs; i++) {
+ struct image *s = &p->pass_imgs[i];
+ if (!s->tex)
+ continue;
+ struct gl_transform tr = s->transform;
+ float tx = (n / 2) * s->w;
+ float ty = (n % 2) * s->h;
+ gl_transform_vec(tr, &tx, &ty);
+ bool rect = s->tex->params.non_normalized;
+ // vec2 texcoordN in idx N+1
+ vs[i + 1].x = tx / (rect ? 1 : s->tex->params.w);
+ vs[i + 1].y = ty / (rect ? 1 : s->tex->params.h);
+ }
+ }
+
+ memmove(&p->tmp_vertex[num_vertex_attribs * 4],
+ &p->tmp_vertex[num_vertex_attribs * 2],
+ vertex_stride);
+
+ memmove(&p->tmp_vertex[num_vertex_attribs * 5],
+ &p->tmp_vertex[num_vertex_attribs * 1],
+ vertex_stride);
+
+ return gl_sc_dispatch_draw(p->sc, fbo.tex, discard, p->vao, num_vertex_attribs,
+ vertex_stride, p->tmp_vertex, num_vertices);
+}
+
+static void finish_pass_fbo(struct gl_video *p, struct ra_fbo fbo,
+ bool discard, const struct mp_rect *dst)
+{
+ pass_prepare_src_tex(p);
+ pass_record(p, render_pass_quad(p, fbo, discard, dst));
+ debug_check_gl(p, "after rendering");
+ cleanup_binds(p);
+}
+
+// dst_fbo: this will be used for rendering; possibly reallocating the whole
+// FBO, if the required parameters have changed
+// w, h: required FBO target dimension, and also defines the target rectangle
+// used for rasterization
+static void finish_pass_tex(struct gl_video *p, struct ra_tex **dst_tex,
+ int w, int h)
+{
+ if (!ra_tex_resize(p->ra, p->log, dst_tex, w, h, p->fbo_format)) {
+ cleanup_binds(p);
+ gl_sc_reset(p->sc);
+ return;
+ }
+
+ // If RA_CAP_PARALLEL_COMPUTE is set, try to prefer compute shaders
+ // over fragment shaders wherever possible.
+ if (!p->pass_compute.active && (p->ra->caps & RA_CAP_PARALLEL_COMPUTE) &&
+ (*dst_tex)->params.storage_dst)
+ {
+ pass_is_compute(p, 16, 16, true);
+ }
+
+ if (p->pass_compute.active) {
+ gl_sc_uniform_image2D_wo(p->sc, "out_image", *dst_tex);
+ if (!p->pass_compute.directly_writes)
+ GLSL(imageStore(out_image, ivec2(gl_GlobalInvocationID), color);)
+
+ dispatch_compute(p, w, h, p->pass_compute);
+ p->pass_compute = (struct compute_info){0};
+
+ debug_check_gl(p, "after dispatching compute shader");
+ } else {
+ struct ra_fbo fbo = { .tex = *dst_tex, };
+ finish_pass_fbo(p, fbo, true, &(struct mp_rect){0, 0, w, h});
+ }
+}
+
+static const char *get_tex_swizzle(struct image *img)
+{
+ if (!img->tex)
+ return "rgba";
+ return img->tex->params.format->luminance_alpha ? "raaa" : "rgba";
+}
+
+// Copy a texture to the vec4 color, while increasing offset. Also applies
+// the texture multiplier to the sampled color
+static void copy_image(struct gl_video *p, unsigned int *offset, struct image img)
+{
+ const unsigned int count = img.components;
+ char src[5] = {0};
+ char dst[5] = {0};
+
+ assert(*offset + count < sizeof(dst));
+ assert(img.padding + count < sizeof(src));
+
+ int id = pass_bind(p, img);
+
+ const char *tex_fmt = get_tex_swizzle(&img);
+ const char *dst_fmt = "rgba";
+ for (unsigned int i = 0; i < count; i++) {
+ src[i] = tex_fmt[img.padding + i];
+ dst[i] = dst_fmt[*offset + i];
+ }
+
+ if (img.tex && img.tex->params.format->ctype == RA_CTYPE_UINT) {
+ uint64_t tex_max = 1ull << p->ra_format.component_bits;
+ img.multiplier *= 1.0 / (tex_max - 1);
+ }
+
+ GLSLF("color.%s = %f * vec4(texture(texture%d, texcoord%d)).%s;\n",
+ dst, img.multiplier, id, id, src);
+
+ *offset += count;
+}
+
+static void skip_unused(struct gl_video *p, int num_components)
+{
+ for (int i = num_components; i < 4; i++)
+ GLSLF("color.%c = %f;\n", "rgba"[i], i < 3 ? 0.0 : 1.0);
+}
+
+static void uninit_scaler(struct gl_video *p, struct scaler *scaler)
+{
+ ra_tex_free(p->ra, &scaler->sep_fbo);
+ ra_tex_free(p->ra, &scaler->lut);
+ scaler->kernel = NULL;
+ scaler->initialized = false;
+}
+
+static void hook_prelude(struct gl_video *p, const char *name, int id,
+ struct image img)
+{
+ GLSLHF("#define %s_raw texture%d\n", name, id);
+ GLSLHF("#define %s_pos texcoord%d\n", name, id);
+ GLSLHF("#define %s_size texture_size%d\n", name, id);
+ GLSLHF("#define %s_rot texture_rot%d\n", name, id);
+ GLSLHF("#define %s_off texture_off%d\n", name, id);
+ GLSLHF("#define %s_pt pixel_size%d\n", name, id);
+ GLSLHF("#define %s_map texmap%d\n", name, id);
+ GLSLHF("#define %s_mul %f\n", name, img.multiplier);
+
+ char crap[5] = "";
+ snprintf(crap, sizeof(crap), "%s", get_tex_swizzle(&img));
+
+ // Remove leading padding by rotating the swizzle mask.
+ int len = strlen(crap);
+ for (int n = 0; n < img.padding; n++) {
+ if (len) {
+ char f = crap[0];
+ memmove(crap, crap + 1, len - 1);
+ crap[len - 1] = f;
+ }
+ }
+
+ // Set up the sampling functions
+ GLSLHF("#define %s_tex(pos) (%s_mul * vec4(texture(%s_raw, pos)).%s)\n",
+ name, name, name, crap);
+
+ if (p->ra->caps & RA_CAP_GATHER) {
+ GLSLHF("#define %s_gather(pos, c) (%s_mul * vec4("
+ "textureGather(%s_raw, pos, c)))\n", name, name, name);
+ }
+
+ // Since the extra matrix multiplication impacts performance,
+ // skip it unless the texture was actually rotated
+ if (gl_transform_eq(img.transform, identity_trans)) {
+ GLSLHF("#define %s_texOff(off) %s_tex(%s_pos + %s_pt * vec2(off))\n",
+ name, name, name, name);
+ } else {
+ GLSLHF("#define %s_texOff(off) "
+ "%s_tex(%s_pos + %s_rot * vec2(off)/%s_size)\n",
+ name, name, name, name, name);
+ }
+}
+
+static bool saved_img_find(struct gl_video *p, const char *name,
+ struct image *out)
+{
+ if (!name || !out)
+ return false;
+
+ for (int i = 0; i < p->num_saved_imgs; i++) {
+ if (strcmp(p->saved_imgs[i].name, name) == 0) {
+ *out = p->saved_imgs[i].img;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static void saved_img_store(struct gl_video *p, const char *name,
+ struct image img)
+{
+ assert(name);
+
+ for (int i = 0; i < p->num_saved_imgs; i++) {
+ if (strcmp(p->saved_imgs[i].name, name) == 0) {
+ p->saved_imgs[i].img = img;
+ return;
+ }
+ }
+
+ MP_TARRAY_APPEND(p, p->saved_imgs, p->num_saved_imgs, (struct saved_img) {
+ .name = name,
+ .img = img
+ });
+}
+
+static bool pass_hook_setup_binds(struct gl_video *p, const char *name,
+ struct image img, struct tex_hook *hook)
+{
+ for (int t = 0; t < SHADER_MAX_BINDS; t++) {
+ char *bind_name = (char *)hook->bind_tex[t];
+
+ if (!bind_name)
+ continue;
+
+ // This is a special name that means "currently hooked texture"
+ if (strcmp(bind_name, "HOOKED") == 0) {
+ int id = pass_bind(p, img);
+ hook_prelude(p, "HOOKED", id, img);
+ hook_prelude(p, name, id, img);
+ continue;
+ }
+
+ // BIND can also be used to load user-defined textures, in which
+ // case we will directly load them as a uniform instead of
+ // generating the hook_prelude boilerplate
+ for (int u = 0; u < p->num_user_textures; u++) {
+ struct gl_user_shader_tex *utex = &p->user_textures[u];
+ if (bstr_equals0(utex->name, bind_name)) {
+ gl_sc_uniform_texture(p->sc, bind_name, utex->tex);
+ goto next_bind;
+ }
+ }
+
+ struct image bind_img;
+ if (!saved_img_find(p, bind_name, &bind_img)) {
+ // Clean up texture bindings and move on to the next hook
+ MP_TRACE(p, "Skipping hook on %s due to no texture named %s.\n",
+ name, bind_name);
+ p->num_pass_imgs -= t;
+ return false;
+ }
+
+ hook_prelude(p, bind_name, pass_bind(p, bind_img), bind_img);
+
+next_bind: ;
+ }
+
+ return true;
+}
+
+static struct ra_tex **next_hook_tex(struct gl_video *p)
+{
+ if (p->idx_hook_textures == p->num_hook_textures)
+ MP_TARRAY_APPEND(p, p->hook_textures, p->num_hook_textures, NULL);
+
+ return &p->hook_textures[p->idx_hook_textures++];
+}
+
+// Process hooks for a plane, saving the result and returning a new image
+// If 'trans' is NULL, the shader is forbidden from transforming img
+static struct image pass_hook(struct gl_video *p, const char *name,
+ struct image img, struct gl_transform *trans)
+{
+ if (!name)
+ return img;
+
+ saved_img_store(p, name, img);
+
+ MP_TRACE(p, "Running hooks for %s\n", name);
+ for (int i = 0; i < p->num_tex_hooks; i++) {
+ struct tex_hook *hook = &p->tex_hooks[i];
+
+ // Figure out if this pass hooks this texture
+ for (int h = 0; h < SHADER_MAX_HOOKS; h++) {
+ if (hook->hook_tex[h] && strcmp(hook->hook_tex[h], name) == 0)
+ goto found;
+ }
+
+ continue;
+
+found:
+ // Check the hook's condition
+ if (hook->cond && !hook->cond(p, img, hook->priv)) {
+ MP_TRACE(p, "Skipping hook on %s due to condition.\n", name);
+ continue;
+ }
+
+ const char *store_name = hook->save_tex ? hook->save_tex : name;
+ bool is_overwrite = strcmp(store_name, name) == 0;
+
+ // If user shader is set to align HOOKED with reference and fix its
+ // offset, it requires HOOKED to be resizable and overwrited.
+ if (is_overwrite && hook->align_offset) {
+ if (!trans) {
+ MP_ERR(p, "Hook tried to align unresizable texture %s!\n",
+ name);
+ return img;
+ }
+
+ struct gl_transform align_off = identity_trans;
+ align_off.t[0] = trans->t[0];
+ align_off.t[1] = trans->t[1];
+
+ gl_transform_trans(align_off, &img.transform);
+ }
+
+ if (!pass_hook_setup_binds(p, name, img, hook))
+ continue;
+
+ // Run the actual hook. This generates a series of GLSL shader
+ // instructions sufficient for drawing the hook's output
+ struct gl_transform hook_off = identity_trans;
+ hook->hook(p, img, &hook_off, hook->priv);
+
+ int comps = hook->components ? hook->components : img.components;
+ skip_unused(p, comps);
+
+ // Compute the updated FBO dimensions and store the result
+ struct mp_rect_f sz = {0, 0, img.w, img.h};
+ gl_transform_rect(hook_off, &sz);
+ int w = lroundf(fabs(sz.x1 - sz.x0));
+ int h = lroundf(fabs(sz.y1 - sz.y0));
+
+ struct ra_tex **tex = next_hook_tex(p);
+ finish_pass_tex(p, tex, w, h);
+ struct image saved_img = image_wrap(*tex, img.type, comps);
+
+ // If the texture we're saving overwrites the "current" texture, also
+ // update the tex parameter so that the future loop cycles will use the
+ // updated values, and export the offset
+ if (is_overwrite) {
+ if (!trans && !gl_transform_eq(hook_off, identity_trans)) {
+ MP_ERR(p, "Hook tried changing size of unscalable texture %s!\n",
+ name);
+ return img;
+ }
+
+ img = saved_img;
+ if (trans) {
+ gl_transform_trans(hook_off, trans);
+
+ // If user shader is set to align HOOKED, the offset it produces
+ // is dynamic (with static resizing factor though).
+ // Align it with reference manually to get offset fixed.
+ if (hook->align_offset) {
+ trans->t[0] = 0.0;
+ trans->t[1] = 0.0;
+ }
+ }
+ }
+
+ saved_img_store(p, store_name, saved_img);
+ }
+
+ return img;
+}
+
+// This can be used at any time in the middle of rendering to specify an
+// optional hook point, which if triggered will render out to a new FBO and
+// load the result back into vec4 color. Offsets applied by the hooks are
+// accumulated in tex_trans, and the FBO is dimensioned according
+// to p->texture_w/h
+static void pass_opt_hook_point(struct gl_video *p, const char *name,
+ struct gl_transform *tex_trans)
+{
+ if (!name)
+ return;
+
+ for (int i = 0; i < p->num_tex_hooks; i++) {
+ struct tex_hook *hook = &p->tex_hooks[i];
+
+ for (int h = 0; h < SHADER_MAX_HOOKS; h++) {
+ if (hook->hook_tex[h] && strcmp(hook->hook_tex[h], name) == 0)
+ goto found;
+ }
+
+ for (int b = 0; b < SHADER_MAX_BINDS; b++) {
+ if (hook->bind_tex[b] && strcmp(hook->bind_tex[b], name) == 0)
+ goto found;
+ }
+ }
+
+ // Nothing uses this texture, don't bother storing it
+ return;
+
+found: ;
+ struct ra_tex **tex = next_hook_tex(p);
+ finish_pass_tex(p, tex, p->texture_w, p->texture_h);
+ struct image img = image_wrap(*tex, PLANE_RGB, p->components);
+ img = pass_hook(p, name, img, tex_trans);
+ copy_image(p, &(int){0}, img);
+ p->texture_w = img.w;
+ p->texture_h = img.h;
+ p->components = img.components;
+ pass_describe(p, "(remainder pass)");
+}
+
+static void load_shader(struct gl_video *p, struct bstr body)
+{
+ gl_sc_hadd_bstr(p->sc, body);
+ gl_sc_uniform_dynamic(p->sc);
+ gl_sc_uniform_f(p->sc, "random", (double)av_lfg_get(&p->lfg) / UINT32_MAX);
+ gl_sc_uniform_dynamic(p->sc);
+ gl_sc_uniform_i(p->sc, "frame", p->frames_uploaded);
+ gl_sc_uniform_vec2(p->sc, "input_size",
+ (float[]){(p->src_rect.x1 - p->src_rect.x0) *
+ p->texture_offset.m[0][0],
+ (p->src_rect.y1 - p->src_rect.y0) *
+ p->texture_offset.m[1][1]});
+ gl_sc_uniform_vec2(p->sc, "target_size",
+ (float[]){p->dst_rect.x1 - p->dst_rect.x0,
+ p->dst_rect.y1 - p->dst_rect.y0});
+ gl_sc_uniform_vec2(p->sc, "tex_offset",
+ (float[]){p->src_rect.x0 * p->texture_offset.m[0][0] +
+ p->texture_offset.t[0],
+ p->src_rect.y0 * p->texture_offset.m[1][1] +
+ p->texture_offset.t[1]});
+}
+
+// Semantic equality
+static bool double_seq(double a, double b)
+{
+ return (isnan(a) && isnan(b)) || a == b;
+}
+
+static bool scaler_fun_eq(struct scaler_fun a, struct scaler_fun b)
+{
+ if ((a.name && !b.name) || (b.name && !a.name))
+ return false;
+
+ return ((!a.name && !b.name) || strcmp(a.name, b.name) == 0) &&
+ double_seq(a.params[0], b.params[0]) &&
+ double_seq(a.params[1], b.params[1]) &&
+ a.blur == b.blur &&
+ a.taper == b.taper;
+}
+
+static bool scaler_conf_eq(struct scaler_config a, struct scaler_config b)
+{
+ // Note: antiring isn't compared because it doesn't affect LUT
+ // generation
+ return scaler_fun_eq(a.kernel, b.kernel) &&
+ scaler_fun_eq(a.window, b.window) &&
+ a.radius == b.radius &&
+ a.clamp == b.clamp;
+}
+
+static void reinit_scaler(struct gl_video *p, struct scaler *scaler,
+ const struct scaler_config *conf,
+ double scale_factor,
+ int sizes[])
+{
+ assert(conf);
+ if (scaler_conf_eq(scaler->conf, *conf) &&
+ scaler->scale_factor == scale_factor &&
+ scaler->initialized)
+ return;
+
+ uninit_scaler(p, scaler);
+
+ if (scaler->index == SCALER_DSCALE && (!conf->kernel.name ||
+ !conf->kernel.name[0]))
+ {
+ conf = &p->opts.scaler[SCALER_SCALE];
+ }
+
+ if (scaler->index == SCALER_CSCALE && (!conf->kernel.name ||
+ !conf->kernel.name[0]))
+ {
+ conf = &p->opts.scaler[SCALER_SCALE];
+ }
+
+ struct filter_kernel bare_window;
+ const struct filter_kernel *t_kernel = mp_find_filter_kernel(conf->kernel.name);
+ const struct filter_window *t_window = mp_find_filter_window(conf->window.name);
+ bool is_tscale = scaler->index == SCALER_TSCALE;
+ if (!t_kernel) {
+ const struct filter_window *window = mp_find_filter_window(conf->kernel.name);
+ if (window) {
+ bare_window = (struct filter_kernel) { .f = *window };
+ t_kernel = &bare_window;
+ }
+ }
+
+ scaler->conf = *conf;
+ scaler->conf.kernel.name = (char *)handle_scaler_opt(conf->kernel.name, is_tscale);
+ scaler->conf.window.name = t_window ? (char *)t_window->name : NULL;
+ scaler->scale_factor = scale_factor;
+ scaler->insufficient = false;
+ scaler->initialized = true;
+ if (!t_kernel)
+ return;
+
+ scaler->kernel_storage = *t_kernel;
+ scaler->kernel = &scaler->kernel_storage;
+
+ if (!t_window) {
+ // fall back to the scaler's default window if available
+ t_window = mp_find_filter_window(t_kernel->window);
+ }
+ if (t_window)
+ scaler->kernel->w = *t_window;
+
+ for (int n = 0; n < 2; n++) {
+ if (!isnan(conf->kernel.params[n]))
+ scaler->kernel->f.params[n] = conf->kernel.params[n];
+ if (!isnan(conf->window.params[n]))
+ scaler->kernel->w.params[n] = conf->window.params[n];
+ }
+
+ if (conf->kernel.blur > 0.0)
+ scaler->kernel->f.blur = conf->kernel.blur;
+ if (conf->window.blur > 0.0)
+ scaler->kernel->w.blur = conf->window.blur;
+
+ if (conf->kernel.taper > 0.0)
+ scaler->kernel->f.taper = conf->kernel.taper;
+ if (conf->window.taper > 0.0)
+ scaler->kernel->w.taper = conf->window.taper;
+
+ if (scaler->kernel->f.resizable && conf->radius > 0.0)
+ scaler->kernel->f.radius = conf->radius;
+
+ scaler->kernel->clamp = conf->clamp;
+ scaler->insufficient = !mp_init_filter(scaler->kernel, sizes, scale_factor);
+
+ int size = scaler->kernel->size;
+ int num_components = size > 2 ? 4 : size;
+ const struct ra_format *fmt = ra_find_float16_format(p->ra, num_components);
+ assert(fmt);
+
+ int width = (size + num_components - 1) / num_components; // round up
+ int stride = width * num_components;
+ assert(size <= stride);
+
+ static const int lut_size = 256;
+ float *weights = talloc_array(NULL, float, lut_size * stride);
+ mp_compute_lut(scaler->kernel, lut_size, stride, weights);
+
+ bool use_1d = scaler->kernel->polar && (p->ra->caps & RA_CAP_TEX_1D);
+
+ struct ra_tex_params lut_params = {
+ .dimensions = use_1d ? 1 : 2,
+ .w = use_1d ? lut_size : width,
+ .h = use_1d ? 1 : lut_size,
+ .d = 1,
+ .format = fmt,
+ .render_src = true,
+ .src_linear = true,
+ .initial_data = weights,
+ };
+ scaler->lut = ra_tex_create(p->ra, &lut_params);
+
+ talloc_free(weights);
+
+ debug_check_gl(p, "after initializing scaler");
+}
+
+// Special helper for sampling from two separated stages
+static void pass_sample_separated(struct gl_video *p, struct image src,
+ struct scaler *scaler, int w, int h)
+{
+ // Separate the transformation into x and y components, per pass
+ struct gl_transform t_x = {
+ .m = {{src.transform.m[0][0], 0.0}, {src.transform.m[1][0], 1.0}},
+ .t = {src.transform.t[0], 0.0},
+ };
+ struct gl_transform t_y = {
+ .m = {{1.0, src.transform.m[0][1]}, {0.0, src.transform.m[1][1]}},
+ .t = {0.0, src.transform.t[1]},
+ };
+
+ // First pass (scale only in the y dir)
+ src.transform = t_y;
+ sampler_prelude(p->sc, pass_bind(p, src));
+ GLSLF("// first pass\n");
+ pass_sample_separated_gen(p->sc, scaler, 0, 1);
+ GLSLF("color *= %f;\n", src.multiplier);
+ finish_pass_tex(p, &scaler->sep_fbo, src.w, h);
+
+ // Second pass (scale only in the x dir)
+ src = image_wrap(scaler->sep_fbo, src.type, src.components);
+ src.transform = t_x;
+ pass_describe(p, "%s second pass", scaler->conf.kernel.name);
+ sampler_prelude(p->sc, pass_bind(p, src));
+ pass_sample_separated_gen(p->sc, scaler, 1, 0);
+}
+
+// Picks either the compute shader version or the regular sampler version
+// depending on hardware support
+static void pass_dispatch_sample_polar(struct gl_video *p, struct scaler *scaler,
+ struct image img, int w, int h)
+{
+ uint64_t reqs = RA_CAP_COMPUTE;
+ if ((p->ra->caps & reqs) != reqs)
+ goto fallback;
+
+ int bound = ceil(scaler->kernel->radius_cutoff);
+ int offset = bound - 1; // padding top/left
+ int padding = offset + bound; // total padding
+
+ float ratiox = (float)w / img.w,
+ ratioy = (float)h / img.h;
+
+ // For performance we want to load at least as many pixels
+ // horizontally as there are threads in a warp (32 for nvidia), as
+ // well as enough to take advantage of shmem parallelism
+ const int warp_size = 32, threads = 256;
+ int bw = warp_size;
+ int bh = threads / bw;
+
+ // We need to sample everything from base_min to base_max, so make sure
+ // we have enough room in shmem
+ int iw = (int)ceil(bw / ratiox) + padding + 1,
+ ih = (int)ceil(bh / ratioy) + padding + 1;
+
+ int shmem_req = iw * ih * img.components * sizeof(float);
+ if (shmem_req > p->ra->max_shmem)
+ goto fallback;
+
+ pass_is_compute(p, bw, bh, false);
+ pass_compute_polar(p->sc, scaler, img.components, bw, bh, iw, ih);
+ return;
+
+fallback:
+ // Fall back to regular polar shader when compute shaders are unsupported
+ // or the kernel is too big for shmem
+ pass_sample_polar(p->sc, scaler, img.components,
+ p->ra->caps & RA_CAP_GATHER);
+}
+
+// Sample from image, with the src rectangle given by it.
+// The dst rectangle is implicit by what the caller will do next, but w and h
+// must still be what is going to be used (to dimension FBOs correctly).
+// This will write the scaled contents to the vec4 "color".
+// The scaler unit is initialized by this function; in order to avoid cache
+// thrashing, the scaler unit should usually use the same parameters.
+static void pass_sample(struct gl_video *p, struct image img,
+ struct scaler *scaler, const struct scaler_config *conf,
+ double scale_factor, int w, int h)
+{
+ reinit_scaler(p, scaler, conf, scale_factor, filter_sizes);
+
+ // Describe scaler
+ const char *scaler_opt[] = {
+ [SCALER_SCALE] = "scale",
+ [SCALER_DSCALE] = "dscale",
+ [SCALER_CSCALE] = "cscale",
+ [SCALER_TSCALE] = "tscale",
+ };
+
+ pass_describe(p, "%s=%s (%s)", scaler_opt[scaler->index],
+ scaler->conf.kernel.name, plane_names[img.type]);
+
+ bool is_separated = scaler->kernel && !scaler->kernel->polar;
+
+ // Set up the transformation+prelude and bind the texture, for everything
+ // other than separated scaling (which does this in the subfunction)
+ if (!is_separated)
+ sampler_prelude(p->sc, pass_bind(p, img));
+
+ // Dispatch the scaler. They're all wildly different.
+ const char *name = scaler->conf.kernel.name;
+ if (strcmp(name, "bilinear") == 0) {
+ GLSL(color = texture(tex, pos);)
+ } else if (strcmp(name, "bicubic_fast") == 0) {
+ pass_sample_bicubic_fast(p->sc);
+ } else if (strcmp(name, "oversample") == 0) {
+ pass_sample_oversample(p->sc, scaler, w, h);
+ } else if (scaler->kernel && scaler->kernel->polar) {
+ pass_dispatch_sample_polar(p, scaler, img, w, h);
+ } else if (scaler->kernel) {
+ pass_sample_separated(p, img, scaler, w, h);
+ } else {
+ MP_ASSERT_UNREACHABLE(); // should never happen
+ }
+
+ // Apply any required multipliers. Separated scaling already does this in
+ // its first stage
+ if (!is_separated)
+ GLSLF("color *= %f;\n", img.multiplier);
+
+ // Micro-optimization: Avoid scaling unneeded channels
+ skip_unused(p, img.components);
+}
+
+// Returns true if two images are semantically equivalent (same metadata)
+static bool image_equiv(struct image a, struct image b)
+{
+ return a.type == b.type &&
+ a.components == b.components &&
+ a.multiplier == b.multiplier &&
+ a.tex->params.format == b.tex->params.format &&
+ a.tex->params.w == b.tex->params.w &&
+ a.tex->params.h == b.tex->params.h &&
+ a.w == b.w &&
+ a.h == b.h &&
+ gl_transform_eq(a.transform, b.transform);
+}
+
+static void deband_hook(struct gl_video *p, struct image img,
+ struct gl_transform *trans, void *priv)
+{
+ pass_describe(p, "debanding (%s)", plane_names[img.type]);
+ pass_sample_deband(p->sc, p->opts.deband_opts, &p->lfg,
+ p->image_params.color.gamma);
+}
+
+static void unsharp_hook(struct gl_video *p, struct image img,
+ struct gl_transform *trans, void *priv)
+{
+ pass_describe(p, "unsharp masking");
+ pass_sample_unsharp(p->sc, p->opts.unsharp);
+}
+
+struct szexp_ctx {
+ struct gl_video *p;
+ struct image img;
+};
+
+static bool szexp_lookup(void *priv, struct bstr var, float size[2])
+{
+ struct szexp_ctx *ctx = priv;
+ struct gl_video *p = ctx->p;
+
+ if (bstr_equals0(var, "NATIVE_CROPPED")) {
+ size[0] = (p->src_rect.x1 - p->src_rect.x0) * p->texture_offset.m[0][0];
+ size[1] = (p->src_rect.y1 - p->src_rect.y0) * p->texture_offset.m[1][1];
+ return true;
+ }
+
+ // The size of OUTPUT is determined. It could be useful for certain
+ // user shaders to skip passes.
+ if (bstr_equals0(var, "OUTPUT")) {
+ size[0] = p->dst_rect.x1 - p->dst_rect.x0;
+ size[1] = p->dst_rect.y1 - p->dst_rect.y0;
+ return true;
+ }
+
+ // HOOKED is a special case
+ if (bstr_equals0(var, "HOOKED")) {
+ size[0] = ctx->img.w;
+ size[1] = ctx->img.h;
+ return true;
+ }
+
+ for (int o = 0; o < p->num_saved_imgs; o++) {
+ if (bstr_equals0(var, p->saved_imgs[o].name)) {
+ size[0] = p->saved_imgs[o].img.w;
+ size[1] = p->saved_imgs[o].img.h;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static bool user_hook_cond(struct gl_video *p, struct image img, void *priv)
+{
+ struct gl_user_shader_hook *shader = priv;
+ assert(shader);
+
+ float res = false;
+ struct szexp_ctx ctx = {p, img};
+ eval_szexpr(p->log, &ctx, szexp_lookup, shader->cond, &res);
+ return res;
+}
+
+static void user_hook(struct gl_video *p, struct image img,
+ struct gl_transform *trans, void *priv)
+{
+ struct gl_user_shader_hook *shader = priv;
+ assert(shader);
+ load_shader(p, shader->pass_body);
+
+ pass_describe(p, "user shader: %.*s (%s)", BSTR_P(shader->pass_desc),
+ plane_names[img.type]);
+
+ if (shader->compute.active) {
+ p->pass_compute = shader->compute;
+ GLSLF("hook();\n");
+ } else {
+ GLSLF("color = hook();\n");
+ }
+
+ // Make sure we at least create a legal FBO on failure, since it's better
+ // to do this and display an error message than just crash OpenGL
+ float w = 1.0, h = 1.0;
+
+ eval_szexpr(p->log, &(struct szexp_ctx){p, img}, szexp_lookup, shader->width, &w);
+ eval_szexpr(p->log, &(struct szexp_ctx){p, img}, szexp_lookup, shader->height, &h);
+
+ *trans = (struct gl_transform){{{w / img.w, 0}, {0, h / img.h}}};
+ gl_transform_trans(shader->offset, trans);
+}
+
+static bool add_user_hook(void *priv, struct gl_user_shader_hook hook)
+{
+ struct gl_video *p = priv;
+ struct gl_user_shader_hook *copy = talloc_ptrtype(p, copy);
+ *copy = hook;
+
+ struct tex_hook texhook = {
+ .save_tex = bstrdup0(copy, hook.save_tex),
+ .components = hook.components,
+ .align_offset = hook.align_offset,
+ .hook = user_hook,
+ .cond = user_hook_cond,
+ .priv = copy,
+ };
+
+ for (int h = 0; h < SHADER_MAX_HOOKS; h++)
+ texhook.hook_tex[h] = bstrdup0(copy, hook.hook_tex[h]);
+ for (int h = 0; h < SHADER_MAX_BINDS; h++)
+ texhook.bind_tex[h] = bstrdup0(copy, hook.bind_tex[h]);
+
+ MP_TARRAY_APPEND(p, p->tex_hooks, p->num_tex_hooks, texhook);
+ return true;
+}
+
+static bool add_user_tex(void *priv, struct gl_user_shader_tex tex)
+{
+ struct gl_video *p = priv;
+
+ tex.tex = ra_tex_create(p->ra, &tex.params);
+ TA_FREEP(&tex.params.initial_data);
+
+ if (!tex.tex)
+ return false;
+
+ MP_TARRAY_APPEND(p, p->user_textures, p->num_user_textures, tex);
+ return true;
+}
+
+static void load_user_shaders(struct gl_video *p, char **shaders)
+{
+ if (!shaders)
+ return;
+
+ for (int n = 0; shaders[n] != NULL; n++) {
+ struct bstr file = load_cached_file(p, shaders[n]);
+ parse_user_shader(p->log, p->ra, file, p, add_user_hook, add_user_tex);
+ }
+}
+
+static void gl_video_setup_hooks(struct gl_video *p)
+{
+ gl_video_reset_hooks(p);
+
+ if (p->opts.deband) {
+ MP_TARRAY_APPEND(p, p->tex_hooks, p->num_tex_hooks, (struct tex_hook) {
+ .hook_tex = {"LUMA", "CHROMA", "RGB", "XYZ"},
+ .bind_tex = {"HOOKED"},
+ .hook = deband_hook,
+ });
+ }
+
+ if (p->opts.unsharp != 0.0) {
+ MP_TARRAY_APPEND(p, p->tex_hooks, p->num_tex_hooks, (struct tex_hook) {
+ .hook_tex = {"MAIN"},
+ .bind_tex = {"HOOKED"},
+ .hook = unsharp_hook,
+ });
+ }
+
+ load_user_shaders(p, p->opts.user_shaders);
+}
+
+// sample from video textures, set "color" variable to yuv value
+static void pass_read_video(struct gl_video *p)
+{
+ struct image img[4];
+ struct gl_transform offsets[4];
+ pass_get_images(p, &p->image, img, offsets);
+
+ // To keep the code as simple as possibly, we currently run all shader
+ // stages even if they would be unnecessary (e.g. no hooks for a texture).
+ // In the future, deferred image should optimize this away.
+
+ // Merge semantically identical textures. This loop is done from back
+ // to front so that merged textures end up in the right order while
+ // simultaneously allowing us to skip unnecessary merges
+ for (int n = 3; n >= 0; n--) {
+ if (img[n].type == PLANE_NONE)
+ continue;
+
+ int first = n;
+ int num = 0;
+
+ for (int i = 0; i < n; i++) {
+ if (image_equiv(img[n], img[i]) &&
+ gl_transform_eq(offsets[n], offsets[i]))
+ {
+ GLSLF("// merging plane %d ...\n", i);
+ copy_image(p, &num, img[i]);
+ first = MPMIN(first, i);
+ img[i] = (struct image){0};
+ }
+ }
+
+ if (num > 0) {
+ GLSLF("// merging plane %d ... into %d\n", n, first);
+ copy_image(p, &num, img[n]);
+ pass_describe(p, "merging planes");
+ finish_pass_tex(p, &p->merge_tex[n], img[n].w, img[n].h);
+ img[first] = image_wrap(p->merge_tex[n], img[n].type, num);
+ img[n] = (struct image){0};
+ }
+ }
+
+ // If any textures are still in integer format by this point, we need
+ // to introduce an explicit conversion pass to avoid breaking hooks/scaling
+ for (int n = 0; n < 4; n++) {
+ if (img[n].tex && img[n].tex->params.format->ctype == RA_CTYPE_UINT) {
+ GLSLF("// use_integer fix for plane %d\n", n);
+ copy_image(p, &(int){0}, img[n]);
+ pass_describe(p, "use_integer fix");
+ finish_pass_tex(p, &p->integer_tex[n], img[n].w, img[n].h);
+ img[n] = image_wrap(p->integer_tex[n], img[n].type,
+ img[n].components);
+ }
+ }
+
+ // The basic idea is we assume the rgb/luma texture is the "reference" and
+ // scale everything else to match, after all planes are finalized.
+ // We find the reference texture first, in order to maintain texture offset
+ // between hooks on different type of planes.
+ int reference_tex_num = 0;
+ for (int n = 0; n < 4; n++) {
+ switch (img[n].type) {
+ case PLANE_RGB:
+ case PLANE_XYZ:
+ case PLANE_LUMA: break;
+ default: continue;
+ }
+
+ reference_tex_num = n;
+ break;
+ }
+
+ // Dispatch the hooks for all of these textures, saving and perhaps
+ // modifying them in the process
+ for (int n = 0; n < 4; n++) {
+ const char *name;
+ switch (img[n].type) {
+ case PLANE_RGB: name = "RGB"; break;
+ case PLANE_LUMA: name = "LUMA"; break;
+ case PLANE_CHROMA: name = "CHROMA"; break;
+ case PLANE_ALPHA: name = "ALPHA"; break;
+ case PLANE_XYZ: name = "XYZ"; break;
+ default: continue;
+ }
+
+ img[n] = pass_hook(p, name, img[n], &offsets[n]);
+
+ if (reference_tex_num == n) {
+ // The reference texture is finalized now.
+ p->texture_w = img[n].w;
+ p->texture_h = img[n].h;
+ p->texture_offset = offsets[n];
+ }
+ }
+
+ // At this point all planes are finalized but they may not be at the
+ // required size yet. Furthermore, they may have texture offsets that
+ // require realignment.
+
+ // Compute the reference rect
+ struct mp_rect_f src = {0.0, 0.0, p->image_params.w, p->image_params.h};
+ struct mp_rect_f ref = src;
+ gl_transform_rect(p->texture_offset, &ref);
+
+ // Explicitly scale all of the textures that don't match
+ for (int n = 0; n < 4; n++) {
+ if (img[n].type == PLANE_NONE)
+ continue;
+
+ // If the planes are aligned identically, we will end up with the
+ // exact same source rectangle.
+ struct mp_rect_f rect = src;
+ gl_transform_rect(offsets[n], &rect);
+ if (mp_rect_f_seq(ref, rect))
+ continue;
+
+ // If the rectangles differ, then our planes have a different
+ // alignment and/or size. First of all, we have to compute the
+ // corrections required to meet the target rectangle
+ struct gl_transform fix = {
+ .m = {{(ref.x1 - ref.x0) / (rect.x1 - rect.x0), 0.0},
+ {0.0, (ref.y1 - ref.y0) / (rect.y1 - rect.y0)}},
+ .t = {ref.x0, ref.y0},
+ };
+
+ // Since the scale in texture space is different from the scale in
+ // absolute terms, we have to scale the coefficients down to be
+ // relative to the texture's physical dimensions and local offset
+ struct gl_transform scale = {
+ .m = {{(float)img[n].w / p->texture_w, 0.0},
+ {0.0, (float)img[n].h / p->texture_h}},
+ .t = {-rect.x0, -rect.y0},
+ };
+ if (p->image_params.rotate % 180 == 90)
+ MPSWAP(double, scale.m[0][0], scale.m[1][1]);
+
+ gl_transform_trans(scale, &fix);
+
+ // Since the texture transform is a function of the texture coordinates
+ // to texture space, rather than the other way around, we have to
+ // actually apply the *inverse* of this. Fortunately, calculating
+ // the inverse is relatively easy here.
+ fix.m[0][0] = 1.0 / fix.m[0][0];
+ fix.m[1][1] = 1.0 / fix.m[1][1];
+ fix.t[0] = fix.m[0][0] * -fix.t[0];
+ fix.t[1] = fix.m[1][1] * -fix.t[1];
+ gl_transform_trans(fix, &img[n].transform);
+
+ int scaler_id = -1;
+ const char *name = NULL;
+ switch (img[n].type) {
+ case PLANE_RGB:
+ case PLANE_LUMA:
+ case PLANE_XYZ:
+ scaler_id = SCALER_SCALE;
+ // these aren't worth hooking, fringe hypothetical cases only
+ break;
+ case PLANE_CHROMA:
+ scaler_id = SCALER_CSCALE;
+ name = "CHROMA_SCALED";
+ break;
+ case PLANE_ALPHA:
+ // alpha always uses bilinear
+ name = "ALPHA_SCALED";
+ }
+
+ if (scaler_id < 0)
+ continue;
+
+ const struct scaler_config *conf = &p->opts.scaler[scaler_id];
+
+ if (scaler_id == SCALER_CSCALE && (!conf->kernel.name ||
+ !conf->kernel.name[0]))
+ {
+ conf = &p->opts.scaler[SCALER_SCALE];
+ }
+
+ struct scaler *scaler = &p->scaler[scaler_id];
+
+ // bilinear scaling is a free no-op thanks to GPU sampling
+ if (strcmp(conf->kernel.name, "bilinear") != 0) {
+ GLSLF("// upscaling plane %d\n", n);
+ pass_sample(p, img[n], scaler, conf, 1.0, p->texture_w, p->texture_h);
+ finish_pass_tex(p, &p->scale_tex[n], p->texture_w, p->texture_h);
+ img[n] = image_wrap(p->scale_tex[n], img[n].type, img[n].components);
+ }
+
+ // Run any post-scaling hooks
+ img[n] = pass_hook(p, name, img[n], NULL);
+ }
+
+ // All planes are of the same size and properly aligned at this point
+ pass_describe(p, "combining planes");
+ int coord = 0;
+ for (int i = 0; i < 4; i++) {
+ if (img[i].type != PLANE_NONE)
+ copy_image(p, &coord, img[i]);
+ }
+ p->components = coord;
+}
+
+// Utility function that simply binds a texture and reads from it, without any
+// transformations.
+static void pass_read_tex(struct gl_video *p, struct ra_tex *tex)
+{
+ struct image img = image_wrap(tex, PLANE_RGB, p->components);
+ copy_image(p, &(int){0}, img);
+}
+
+// yuv conversion, and any other conversions before main up/down-scaling
+static void pass_convert_yuv(struct gl_video *p)
+{
+ struct gl_shader_cache *sc = p->sc;
+
+ struct mp_csp_params cparams = MP_CSP_PARAMS_DEFAULTS;
+ cparams.gray = p->is_gray;
+ cparams.is_float = p->ra_format.component_type == RA_CTYPE_FLOAT;
+ mp_csp_set_image_params(&cparams, &p->image_params);
+ mp_csp_equalizer_state_get(p->video_eq, &cparams);
+ p->user_gamma = 1.0 / (cparams.gamma * p->opts.gamma);
+
+ pass_describe(p, "color conversion");
+
+ if (p->color_swizzle[0])
+ GLSLF("color = color.%s;\n", p->color_swizzle);
+
+ // Pre-colormatrix input gamma correction
+ if (cparams.color.space == MP_CSP_XYZ)
+ pass_linearize(p->sc, p->image_params.color.gamma);
+
+ // We always explicitly normalize the range in pass_read_video
+ cparams.input_bits = cparams.texture_bits = 0;
+
+ // Conversion to RGB. For RGB itself, this still applies e.g. brightness
+ // and contrast controls, or expansion of e.g. LSB-packed 10 bit data.
+ struct mp_cmat m = {{{0}}};
+ mp_get_csp_matrix(&cparams, &m);
+ gl_sc_uniform_mat3(sc, "colormatrix", true, &m.m[0][0]);
+ gl_sc_uniform_vec3(sc, "colormatrix_c", m.c);
+
+ GLSL(color.rgb = mat3(colormatrix) * color.rgb + colormatrix_c;)
+
+ if (cparams.color.space == MP_CSP_XYZ) {
+ pass_delinearize(p->sc, p->image_params.color.gamma);
+ // mp_get_csp_matrix implicitly converts XYZ to DCI-P3
+ p->image_params.color.space = MP_CSP_RGB;
+ p->image_params.color.primaries = MP_CSP_PRIM_DCI_P3;
+ }
+
+ if (p->image_params.color.space == MP_CSP_BT_2020_C) {
+ // Conversion for C'rcY'cC'bc via the BT.2020 CL system:
+ // C'bc = (B'-Y'c) / 1.9404 | C'bc <= 0
+ // = (B'-Y'c) / 1.5816 | C'bc > 0
+ //
+ // C'rc = (R'-Y'c) / 1.7184 | C'rc <= 0
+ // = (R'-Y'c) / 0.9936 | C'rc > 0
+ //
+ // as per the BT.2020 specification, table 4. This is a non-linear
+ // transformation because (constant) luminance receives non-equal
+ // contributions from the three different channels.
+ GLSLF("// constant luminance conversion \n"
+ "color.br = color.br * mix(vec2(1.5816, 0.9936), \n"
+ " vec2(1.9404, 1.7184), \n"
+ " %s(lessThanEqual(color.br, vec2(0))))\n"
+ " + color.gg; \n",
+ gl_sc_bvec(p->sc, 2));
+ // Expand channels to camera-linear light. This shader currently just
+ // assumes everything uses the BT.2020 12-bit gamma function, since the
+ // difference between 10 and 12-bit is negligible for anything other
+ // than 12-bit content.
+ GLSLF("color.rgb = mix(color.rgb * vec3(1.0/4.5), \n"
+ " pow((color.rgb + vec3(0.0993))*vec3(1.0/1.0993), \n"
+ " vec3(1.0/0.45)), \n"
+ " %s(lessThanEqual(vec3(0.08145), color.rgb))); \n",
+ gl_sc_bvec(p->sc, 3));
+ // Calculate the green channel from the expanded RYcB
+ // The BT.2020 specification says Yc = 0.2627*R + 0.6780*G + 0.0593*B
+ GLSL(color.g = (color.g - 0.2627*color.r - 0.0593*color.b)*1.0/0.6780;)
+ // Recompress to receive the R'G'B' result, same as other systems
+ GLSLF("color.rgb = mix(color.rgb * vec3(4.5), \n"
+ " vec3(1.0993) * pow(color.rgb, vec3(0.45)) - vec3(0.0993), \n"
+ " %s(lessThanEqual(vec3(0.0181), color.rgb))); \n",
+ gl_sc_bvec(p->sc, 3));
+ }
+
+ p->components = 3;
+ if (!p->has_alpha || p->opts.alpha_mode == ALPHA_NO) {
+ GLSL(color.a = 1.0;)
+ } else if (p->image_params.alpha == MP_ALPHA_PREMUL) {
+ p->components = 4;
+ } else {
+ p->components = 4;
+ GLSL(color = vec4(color.rgb * color.a, color.a);) // straight -> premul
+ }
+}
+
+static void get_scale_factors(struct gl_video *p, bool transpose_rot, double xy[2])
+{
+ double target_w = p->src_rect.x1 - p->src_rect.x0;
+ double target_h = p->src_rect.y1 - p->src_rect.y0;
+ if (transpose_rot && p->image_params.rotate % 180 == 90)
+ MPSWAP(double, target_w, target_h);
+ xy[0] = (p->dst_rect.x1 - p->dst_rect.x0) / target_w;
+ xy[1] = (p->dst_rect.y1 - p->dst_rect.y0) / target_h;
+}
+
+// Cropping.
+static void compute_src_transform(struct gl_video *p, struct gl_transform *tr)
+{
+ float sx = (p->src_rect.x1 - p->src_rect.x0) / (float)p->texture_w,
+ sy = (p->src_rect.y1 - p->src_rect.y0) / (float)p->texture_h,
+ ox = p->src_rect.x0,
+ oy = p->src_rect.y0;
+ struct gl_transform transform = {{{sx, 0}, {0, sy}}, {ox, oy}};
+
+ gl_transform_trans(p->texture_offset, &transform);
+
+ *tr = transform;
+}
+
+// Takes care of the main scaling and pre/post-conversions
+static void pass_scale_main(struct gl_video *p)
+{
+ // Figure out the main scaler.
+ double xy[2];
+ get_scale_factors(p, true, xy);
+
+ // actual scale factor should be divided by the scale factor of prescaling.
+ xy[0] /= p->texture_offset.m[0][0];
+ xy[1] /= p->texture_offset.m[1][1];
+
+ // The calculation of scale factor involves 32-bit float(from gl_transform),
+ // use non-strict equality test to tolerate precision loss.
+ bool downscaling = xy[0] < 1.0 - FLT_EPSILON || xy[1] < 1.0 - FLT_EPSILON;
+ bool upscaling = !downscaling && (xy[0] > 1.0 + FLT_EPSILON ||
+ xy[1] > 1.0 + FLT_EPSILON);
+ double scale_factor = 1.0;
+
+ struct scaler *scaler = &p->scaler[SCALER_SCALE];
+ struct scaler_config scaler_conf = p->opts.scaler[SCALER_SCALE];
+ if (p->opts.scaler_resizes_only && !downscaling && !upscaling) {
+ scaler_conf.kernel.name = "bilinear";
+ // For scaler-resizes-only, we round the texture offset to
+ // the nearest round value in order to prevent ugly blurriness
+ // (in exchange for slightly shifting the image by up to half a
+ // subpixel)
+ p->texture_offset.t[0] = roundf(p->texture_offset.t[0]);
+ p->texture_offset.t[1] = roundf(p->texture_offset.t[1]);
+ }
+ if (downscaling && p->opts.scaler[SCALER_DSCALE].kernel.name) {
+ scaler_conf = p->opts.scaler[SCALER_DSCALE];
+ scaler = &p->scaler[SCALER_DSCALE];
+ }
+
+ // When requesting correct-downscaling and the clip is anamorphic, and
+ // because only a single scale factor is used for both axes, enable it only
+ // when both axes are downscaled, and use the milder of the factors to not
+ // end up with too much blur on one axis (even if we end up with sub-optimal
+ // scale factor on the other axis). This is better than not respecting
+ // correct scaling at all for anamorphic clips.
+ double f = MPMAX(xy[0], xy[1]);
+ if (p->opts.correct_downscaling && f < 1.0)
+ scale_factor = 1.0 / f;
+
+ // Pre-conversion, like linear light/sigmoidization
+ GLSLF("// scaler pre-conversion\n");
+ bool use_linear = false;
+ if (downscaling) {
+ use_linear = p->opts.linear_downscaling;
+
+ // Linear light downscaling results in nasty artifacts for HDR curves
+ // due to the potentially extreme brightness differences severely
+ // compounding any ringing. So just scale in gamma light instead.
+ if (mp_trc_is_hdr(p->image_params.color.gamma))
+ use_linear = false;
+ } else if (upscaling) {
+ use_linear = p->opts.linear_upscaling || p->opts.sigmoid_upscaling;
+ }
+
+ if (use_linear) {
+ p->use_linear = true;
+ pass_linearize(p->sc, p->image_params.color.gamma);
+ pass_opt_hook_point(p, "LINEAR", NULL);
+ }
+
+ bool use_sigmoid = use_linear && p->opts.sigmoid_upscaling && upscaling;
+ float sig_center, sig_slope, sig_offset, sig_scale;
+ if (use_sigmoid) {
+ // Coefficients for the sigmoidal transform are taken from the
+ // formula here: http://www.imagemagick.org/Usage/color_mods/#sigmoidal
+ sig_center = p->opts.sigmoid_center;
+ sig_slope = p->opts.sigmoid_slope;
+ // This function needs to go through (0,0) and (1,1) so we compute the
+ // values at 1 and 0, and then scale/shift them, respectively.
+ sig_offset = 1.0/(1+expf(sig_slope * sig_center));
+ sig_scale = 1.0/(1+expf(sig_slope * (sig_center-1))) - sig_offset;
+ GLSL(color.rgb = clamp(color.rgb, 0.0, 1.0);)
+ GLSLF("color.rgb = %f - log(1.0/(color.rgb * %f + %f) - 1.0) * 1.0/%f;\n",
+ sig_center, sig_scale, sig_offset, sig_slope);
+ pass_opt_hook_point(p, "SIGMOID", NULL);
+ }
+
+ pass_opt_hook_point(p, "PREKERNEL", NULL);
+
+ int vp_w = p->dst_rect.x1 - p->dst_rect.x0;
+ int vp_h = p->dst_rect.y1 - p->dst_rect.y0;
+ struct gl_transform transform;
+ compute_src_transform(p, &transform);
+
+ GLSLF("// main scaling\n");
+ finish_pass_tex(p, &p->indirect_tex, p->texture_w, p->texture_h);
+ struct image src = image_wrap(p->indirect_tex, PLANE_RGB, p->components);
+ gl_transform_trans(transform, &src.transform);
+ pass_sample(p, src, scaler, &scaler_conf, scale_factor, vp_w, vp_h);
+
+ // Changes the texture size to display size after main scaler.
+ p->texture_w = vp_w;
+ p->texture_h = vp_h;
+
+ pass_opt_hook_point(p, "POSTKERNEL", NULL);
+
+ GLSLF("// scaler post-conversion\n");
+ if (use_sigmoid) {
+ // Inverse of the transformation above
+ GLSL(color.rgb = clamp(color.rgb, 0.0, 1.0);)
+ GLSLF("color.rgb = (1.0/(1.0 + exp(%f * (%f - color.rgb))) - %f) * 1.0/%f;\n",
+ sig_slope, sig_center, sig_offset, sig_scale);
+ }
+}
+
+// Adapts the colors to the right output color space. (Final pass during
+// rendering)
+// If OSD is true, ignore any changes that may have been made to the video
+// by previous passes (i.e. linear scaling)
+static void pass_colormanage(struct gl_video *p, struct mp_colorspace src,
+ struct mp_colorspace fbo_csp, int flags, bool osd)
+{
+ struct ra *ra = p->ra;
+
+ // Configure the destination according to the FBO color space,
+ // unless specific transfer function, primaries or target peak
+ // is set. If values are set to _AUTO, the most likely intended
+ // values are guesstimated later in this function.
+ struct mp_colorspace dst = {
+ .gamma = p->opts.target_trc == MP_CSP_TRC_AUTO ?
+ fbo_csp.gamma : p->opts.target_trc,
+ .primaries = p->opts.target_prim == MP_CSP_PRIM_AUTO ?
+ fbo_csp.primaries : p->opts.target_prim,
+ .light = MP_CSP_LIGHT_DISPLAY,
+ .hdr.max_luma = !p->opts.target_peak ?
+ fbo_csp.hdr.max_luma : p->opts.target_peak,
+ };
+
+ if (!p->colorspace_override_warned &&
+ ((fbo_csp.gamma && dst.gamma != fbo_csp.gamma) ||
+ (fbo_csp.primaries && dst.primaries != fbo_csp.primaries)))
+ {
+ MP_WARN(p, "One or more colorspace value is being overridden "
+ "by user while the FBO provides colorspace information: "
+ "transfer function: (dst: %s, fbo: %s), "
+ "primaries: (dst: %s, fbo: %s). "
+ "Rendering can lead to incorrect results!\n",
+ m_opt_choice_str(mp_csp_trc_names, dst.gamma),
+ m_opt_choice_str(mp_csp_trc_names, fbo_csp.gamma),
+ m_opt_choice_str(mp_csp_prim_names, dst.primaries),
+ m_opt_choice_str(mp_csp_prim_names, fbo_csp.primaries));
+ p->colorspace_override_warned = true;
+ }
+
+ if (dst.gamma == MP_CSP_TRC_HLG)
+ dst.light = MP_CSP_LIGHT_SCENE_HLG;
+
+ if (p->use_lut_3d && (flags & RENDER_SCREEN_COLOR)) {
+ // The 3DLUT is always generated against the video's original source
+ // space, *not* the reference space. (To avoid having to regenerate
+ // the 3DLUT for the OSD on every frame)
+ enum mp_csp_prim prim_orig = p->image_params.color.primaries;
+ enum mp_csp_trc trc_orig = p->image_params.color.gamma;
+
+ // One exception: HDR is not implemented by LittleCMS for technical
+ // limitation reasons, so we use a gamma 2.2 input curve here instead.
+ // We could pick any value we want here, the difference is just coding
+ // efficiency.
+ if (mp_trc_is_hdr(trc_orig))
+ trc_orig = MP_CSP_TRC_GAMMA22;
+
+ if (gl_video_get_lut3d(p, prim_orig, trc_orig)) {
+ dst.primaries = prim_orig;
+ dst.gamma = trc_orig;
+ assert(dst.primaries && dst.gamma);
+ }
+ }
+
+ if (dst.primaries == MP_CSP_PRIM_AUTO) {
+ // The vast majority of people are on sRGB or BT.709 displays, so pick
+ // this as the default output color space.
+ dst.primaries = MP_CSP_PRIM_BT_709;
+
+ if (src.primaries == MP_CSP_PRIM_BT_601_525 ||
+ src.primaries == MP_CSP_PRIM_BT_601_625)
+ {
+ // Since we auto-pick BT.601 and BT.709 based on the dimensions,
+ // combined with the fact that they're very similar to begin with,
+ // and to avoid confusing the average user, just don't adapt BT.601
+ // content automatically at all.
+ dst.primaries = src.primaries;
+ }
+ }
+
+ if (dst.gamma == MP_CSP_TRC_AUTO) {
+ // Most people seem to complain when the image is darker or brighter
+ // than what they're "used to", so just avoid changing the gamma
+ // altogether by default. The only exceptions to this rule apply to
+ // very unusual TRCs, which even hardcode technoluddites would probably
+ // not enjoy viewing unaltered.
+ dst.gamma = src.gamma;
+
+ // Avoid outputting linear light or HDR content "by default". For these
+ // just pick gamma 2.2 as a default, since it's a good estimate for
+ // the response of typical displays
+ if (dst.gamma == MP_CSP_TRC_LINEAR || mp_trc_is_hdr(dst.gamma))
+ dst.gamma = MP_CSP_TRC_GAMMA22;
+ }
+
+ // If there's no specific signal peak known for the output display, infer
+ // it from the chosen transfer function. Also normalize the src peak, in
+ // case it was unknown
+ if (!dst.hdr.max_luma)
+ dst.hdr.max_luma = mp_trc_nom_peak(dst.gamma) * MP_REF_WHITE;
+ if (!src.hdr.max_luma)
+ src.hdr.max_luma = mp_trc_nom_peak(src.gamma) * MP_REF_WHITE;
+
+ // Whitelist supported modes
+ switch (p->opts.tone_map.curve) {
+ case TONE_MAPPING_AUTO:
+ case TONE_MAPPING_CLIP:
+ case TONE_MAPPING_MOBIUS:
+ case TONE_MAPPING_REINHARD:
+ case TONE_MAPPING_HABLE:
+ case TONE_MAPPING_GAMMA:
+ case TONE_MAPPING_LINEAR:
+ case TONE_MAPPING_BT_2390:
+ break;
+ default:
+ MP_WARN(p, "Tone mapping curve unsupported by vo_gpu, falling back.\n");
+ p->opts.tone_map.curve = TONE_MAPPING_AUTO;
+ break;
+ }
+
+ switch (p->opts.tone_map.gamut_mode) {
+ case GAMUT_AUTO:
+ case GAMUT_WARN:
+ case GAMUT_CLIP:
+ case GAMUT_DESATURATE:
+ break;
+ default:
+ MP_WARN(p, "Gamut mapping mode unsupported by vo_gpu, falling back.\n");
+ p->opts.tone_map.gamut_mode = GAMUT_AUTO;
+ break;
+ }
+
+ struct gl_tone_map_opts tone_map = p->opts.tone_map;
+ bool detect_peak = tone_map.compute_peak >= 0 && mp_trc_is_hdr(src.gamma)
+ && src.hdr.max_luma > dst.hdr.max_luma;
+
+ if (detect_peak && !p->hdr_peak_ssbo) {
+ struct {
+ float average[2];
+ int32_t frame_sum;
+ uint32_t frame_max;
+ uint32_t counter;
+ } peak_ssbo = {0};
+
+ struct ra_buf_params params = {
+ .type = RA_BUF_TYPE_SHADER_STORAGE,
+ .size = sizeof(peak_ssbo),
+ .initial_data = &peak_ssbo,
+ };
+
+ p->hdr_peak_ssbo = ra_buf_create(ra, &params);
+ if (!p->hdr_peak_ssbo) {
+ MP_WARN(p, "Failed to create HDR peak detection SSBO, disabling.\n");
+ tone_map.compute_peak = p->opts.tone_map.compute_peak = -1;
+ detect_peak = false;
+ }
+ }
+
+ if (detect_peak) {
+ pass_describe(p, "detect HDR peak");
+ pass_is_compute(p, 8, 8, true); // 8x8 is good for performance
+ gl_sc_ssbo(p->sc, "PeakDetect", p->hdr_peak_ssbo,
+ "vec2 average;"
+ "int frame_sum;"
+ "uint frame_max;"
+ "uint counter;"
+ );
+ } else {
+ tone_map.compute_peak = -1;
+ }
+
+ // Adapt from src to dst as necessary
+ pass_color_map(p->sc, p->use_linear && !osd, src, dst, &tone_map);
+
+ if (p->use_lut_3d && (flags & RENDER_SCREEN_COLOR)) {
+ gl_sc_uniform_texture(p->sc, "lut_3d", p->lut_3d_texture);
+ GLSL(vec3 cpos;)
+ for (int i = 0; i < 3; i++)
+ GLSLF("cpos[%d] = LUT_POS(color[%d], %d.0);\n", i, i, p->lut_3d_size[i]);
+ GLSL(color.rgb = tex3D(lut_3d, cpos).rgb;)
+ }
+}
+
+void gl_video_set_fb_depth(struct gl_video *p, int fb_depth)
+{
+ p->fb_depth = fb_depth;
+}
+
+static void pass_dither(struct gl_video *p)
+{
+ // Assume 8 bits per component if unknown.
+ int dst_depth = p->fb_depth > 0 ? p->fb_depth : 8;
+ if (p->opts.dither_depth > 0)
+ dst_depth = p->opts.dither_depth;
+
+ if (p->opts.dither_depth < 0 || p->opts.dither_algo == DITHER_NONE)
+ return;
+
+ if (p->opts.dither_algo == DITHER_ERROR_DIFFUSION) {
+ const struct error_diffusion_kernel *kernel =
+ mp_find_error_diffusion_kernel(p->opts.error_diffusion);
+ int o_w = p->dst_rect.x1 - p->dst_rect.x0,
+ o_h = p->dst_rect.y1 - p->dst_rect.y0;
+
+ int shmem_req = mp_ef_compute_shared_memory_size(kernel, o_h);
+ if (shmem_req > p->ra->max_shmem) {
+ MP_WARN(p, "Fallback to dither=fruit because there is no enough "
+ "shared memory (%d/%d).\n",
+ shmem_req, (int)p->ra->max_shmem);
+ p->opts.dither_algo = DITHER_FRUIT;
+ } else {
+ finish_pass_tex(p, &p->error_diffusion_tex[0], o_w, o_h);
+
+ struct image img = image_wrap(p->error_diffusion_tex[0], PLANE_RGB, p->components);
+
+ // Ensure the block size doesn't exceed the maximum of the
+ // implementation.
+ int block_size = MPMIN(p->ra->max_compute_group_threads, o_h);
+
+ pass_describe(p, "dither=error-diffusion (kernel=%s, depth=%d)",
+ kernel->name, dst_depth);
+
+ p->pass_compute = (struct compute_info) {
+ .active = true,
+ .threads_w = block_size,
+ .threads_h = 1,
+ .directly_writes = true
+ };
+
+ int tex_id = pass_bind(p, img);
+
+ pass_error_diffusion(p->sc, kernel, tex_id, o_w, o_h,
+ dst_depth, block_size);
+
+ finish_pass_tex(p, &p->error_diffusion_tex[1], o_w, o_h);
+
+ img = image_wrap(p->error_diffusion_tex[1], PLANE_RGB, p->components);
+ copy_image(p, &(int){0}, img);
+
+ return;
+ }
+ }
+
+ if (!p->dither_texture) {
+ MP_VERBOSE(p, "Dither to %d.\n", dst_depth);
+
+ int tex_size = 0;
+ void *tex_data = NULL;
+ const struct ra_format *fmt = NULL;
+ void *temp = NULL;
+
+ if (p->opts.dither_algo == DITHER_FRUIT) {
+ int sizeb = p->opts.dither_size;
+ int size = 1 << sizeb;
+
+ if (p->last_dither_matrix_size != size) {
+ p->last_dither_matrix = talloc_realloc(p, p->last_dither_matrix,
+ float, size * size);
+ mp_make_fruit_dither_matrix(p->last_dither_matrix, sizeb);
+ p->last_dither_matrix_size = size;
+ }
+
+ // Prefer R16 texture since they provide higher precision.
+ fmt = ra_find_unorm_format(p->ra, 2, 1);
+ if (!fmt)
+ fmt = ra_find_float16_format(p->ra, 1);
+ if (fmt) {
+ tex_size = size;
+ tex_data = p->last_dither_matrix;
+ if (fmt->ctype == RA_CTYPE_UNORM) {
+ uint16_t *t = temp = talloc_array(NULL, uint16_t, size * size);
+ for (int n = 0; n < size * size; n++)
+ t[n] = p->last_dither_matrix[n] * UINT16_MAX;
+ tex_data = t;
+ }
+ } else {
+ MP_VERBOSE(p, "GL too old. Falling back to ordered dither.\n");
+ p->opts.dither_algo = DITHER_ORDERED;
+ }
+ }
+
+ if (p->opts.dither_algo == DITHER_ORDERED) {
+ temp = talloc_array(NULL, char, 8 * 8);
+ mp_make_ordered_dither_matrix(temp, 8);
+
+ fmt = ra_find_unorm_format(p->ra, 1, 1);
+ tex_size = 8;
+ tex_data = temp;
+ }
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = tex_size,
+ .h = tex_size,
+ .d = 1,
+ .format = fmt,
+ .render_src = true,
+ .src_repeat = true,
+ .initial_data = tex_data,
+ };
+ p->dither_texture = ra_tex_create(p->ra, &params);
+
+ debug_check_gl(p, "dither setup");
+
+ talloc_free(temp);
+
+ if (!p->dither_texture)
+ return;
+ }
+
+ GLSLF("// dithering\n");
+
+ // This defines how many bits are considered significant for output on
+ // screen. The superfluous bits will be used for rounding according to the
+ // dither matrix. The precision of the source implicitly decides how many
+ // dither patterns can be visible.
+ int dither_quantization = (1 << dst_depth) - 1;
+ int dither_size = p->dither_texture->params.w;
+
+ gl_sc_uniform_texture(p->sc, "dither", p->dither_texture);
+
+ GLSLF("vec2 dither_pos = gl_FragCoord.xy * 1.0/%d.0;\n", dither_size);
+
+ if (p->opts.temporal_dither) {
+ int phase = (p->frames_rendered / p->opts.temporal_dither_period) % 8u;
+ float r = phase * (M_PI / 2); // rotate
+ float m = phase < 4 ? 1 : -1; // mirror
+
+ float matrix[2][2] = {{cos(r), -sin(r) },
+ {sin(r) * m, cos(r) * m}};
+ gl_sc_uniform_dynamic(p->sc);
+ gl_sc_uniform_mat2(p->sc, "dither_trafo", true, &matrix[0][0]);
+
+ GLSL(dither_pos = dither_trafo * dither_pos;)
+ }
+
+ GLSL(float dither_value = texture(dither, dither_pos).r;)
+ GLSLF("color = floor(color * %d.0 + dither_value + 0.5 / %d.0) * 1.0/%d.0;\n",
+ dither_quantization, dither_size * dither_size, dither_quantization);
+}
+
+// Draws the OSD, in scene-referred colors.. If cms is true, subtitles are
+// instead adapted to the display's gamut.
+static void pass_draw_osd(struct gl_video *p, int osd_flags, int frame_flags,
+ double pts, struct mp_osd_res rect, struct ra_fbo fbo,
+ bool cms)
+{
+ if (frame_flags & RENDER_FRAME_VF_SUBS)
+ osd_flags |= OSD_DRAW_SUB_FILTER;
+
+ if ((osd_flags & OSD_DRAW_SUB_ONLY) && (osd_flags & OSD_DRAW_OSD_ONLY))
+ return;
+
+ mpgl_osd_generate(p->osd, rect, pts, p->image_params.stereo3d, osd_flags);
+
+ timer_pool_start(p->osd_timer);
+ for (int n = 0; n < MAX_OSD_PARTS; n++) {
+ // (This returns false if this part is empty with nothing to draw.)
+ if (!mpgl_osd_draw_prepare(p->osd, n, p->sc))
+ continue;
+ // When subtitles need to be color managed, assume they're in sRGB
+ // (for lack of anything saner to do)
+ if (cms) {
+ static const struct mp_colorspace csp_srgb = {
+ .primaries = MP_CSP_PRIM_BT_709,
+ .gamma = MP_CSP_TRC_SRGB,
+ .light = MP_CSP_LIGHT_DISPLAY,
+ };
+
+ pass_colormanage(p, csp_srgb, fbo.color_space, frame_flags, true);
+ }
+ mpgl_osd_draw_finish(p->osd, n, p->sc, fbo);
+ }
+
+ timer_pool_stop(p->osd_timer);
+ pass_describe(p, "drawing osd");
+ pass_record(p, timer_pool_measure(p->osd_timer));
+}
+
+static float chroma_realign(int size, int pixel)
+{
+ return size / (float)chroma_upsize(size, pixel);
+}
+
+// Minimal rendering code path, for GLES or OpenGL 2.1 without proper FBOs.
+static void pass_render_frame_dumb(struct gl_video *p)
+{
+ struct image img[4];
+ struct gl_transform off[4];
+ pass_get_images(p, &p->image, img, off);
+
+ struct gl_transform transform;
+ compute_src_transform(p, &transform);
+
+ int index = 0;
+ for (int i = 0; i < p->plane_count; i++) {
+ int cw = img[i].type == PLANE_CHROMA ? p->ra_format.chroma_w : 1;
+ int ch = img[i].type == PLANE_CHROMA ? p->ra_format.chroma_h : 1;
+ if (p->image_params.rotate % 180 == 90)
+ MPSWAP(int, cw, ch);
+
+ struct gl_transform t = transform;
+ t.m[0][0] *= chroma_realign(p->texture_w, cw);
+ t.m[1][1] *= chroma_realign(p->texture_h, ch);
+
+ t.t[0] /= cw;
+ t.t[1] /= ch;
+
+ t.t[0] += off[i].t[0];
+ t.t[1] += off[i].t[1];
+
+ gl_transform_trans(img[i].transform, &t);
+ img[i].transform = t;
+
+ copy_image(p, &index, img[i]);
+ }
+
+ pass_convert_yuv(p);
+}
+
+// The main rendering function, takes care of everything up to and including
+// upscaling. p->image is rendered.
+// flags: bit set of RENDER_FRAME_* flags
+static bool pass_render_frame(struct gl_video *p, struct mp_image *mpi,
+ uint64_t id, int flags)
+{
+ // initialize the texture parameters and temporary variables
+ p->texture_w = p->image_params.w;
+ p->texture_h = p->image_params.h;
+ p->texture_offset = identity_trans;
+ p->components = 0;
+ p->num_saved_imgs = 0;
+ p->idx_hook_textures = 0;
+ p->use_linear = false;
+
+ // try uploading the frame
+ if (!pass_upload_image(p, mpi, id))
+ return false;
+
+ if (p->image_params.rotate % 180 == 90)
+ MPSWAP(int, p->texture_w, p->texture_h);
+
+ if (p->dumb_mode)
+ return true;
+
+ pass_read_video(p);
+ pass_opt_hook_point(p, "NATIVE", &p->texture_offset);
+ pass_convert_yuv(p);
+ pass_opt_hook_point(p, "MAINPRESUB", &p->texture_offset);
+
+ // For subtitles
+ double vpts = p->image.mpi->pts;
+ if (vpts == MP_NOPTS_VALUE)
+ vpts = p->osd_pts;
+
+ if (p->osd && p->opts.blend_subs == BLEND_SUBS_VIDEO &&
+ (flags & RENDER_FRAME_SUBS))
+ {
+ double scale[2];
+ get_scale_factors(p, false, scale);
+ struct mp_osd_res rect = {
+ .w = p->texture_w, .h = p->texture_h,
+ .display_par = scale[1] / scale[0], // counter compensate scaling
+ };
+ finish_pass_tex(p, &p->blend_subs_tex, rect.w, rect.h);
+ struct ra_fbo fbo = { p->blend_subs_tex };
+ pass_draw_osd(p, OSD_DRAW_SUB_ONLY, flags, vpts, rect, fbo, false);
+ pass_read_tex(p, p->blend_subs_tex);
+ pass_describe(p, "blend subs video");
+ }
+ pass_opt_hook_point(p, "MAIN", &p->texture_offset);
+
+ pass_scale_main(p);
+
+ int vp_w = p->dst_rect.x1 - p->dst_rect.x0,
+ vp_h = p->dst_rect.y1 - p->dst_rect.y0;
+ if (p->osd && p->opts.blend_subs == BLEND_SUBS_YES &&
+ (flags & RENDER_FRAME_SUBS))
+ {
+ // Recreate the real video size from the src/dst rects
+ struct mp_osd_res rect = {
+ .w = vp_w, .h = vp_h,
+ .ml = -p->src_rect.x0, .mr = p->src_rect.x1 - p->image_params.w,
+ .mt = -p->src_rect.y0, .mb = p->src_rect.y1 - p->image_params.h,
+ .display_par = 1.0,
+ };
+ // Adjust margins for scale
+ double scale[2];
+ get_scale_factors(p, true, scale);
+ rect.ml *= scale[0]; rect.mr *= scale[0];
+ rect.mt *= scale[1]; rect.mb *= scale[1];
+ // We should always blend subtitles in non-linear light
+ if (p->use_linear) {
+ pass_delinearize(p->sc, p->image_params.color.gamma);
+ p->use_linear = false;
+ }
+ finish_pass_tex(p, &p->blend_subs_tex, p->texture_w, p->texture_h);
+ struct ra_fbo fbo = { p->blend_subs_tex };
+ pass_draw_osd(p, OSD_DRAW_SUB_ONLY, flags, vpts, rect, fbo, false);
+ pass_read_tex(p, p->blend_subs_tex);
+ pass_describe(p, "blend subs");
+ }
+
+ pass_opt_hook_point(p, "SCALED", NULL);
+
+ return true;
+}
+
+static void pass_draw_to_screen(struct gl_video *p, struct ra_fbo fbo, int flags)
+{
+ if (p->dumb_mode)
+ pass_render_frame_dumb(p);
+
+ // Adjust the overall gamma before drawing to screen
+ if (p->user_gamma != 1) {
+ gl_sc_uniform_f(p->sc, "user_gamma", p->user_gamma);
+ GLSL(color.rgb = clamp(color.rgb, 0.0, 1.0);)
+ GLSL(color.rgb = pow(color.rgb, vec3(user_gamma));)
+ }
+
+ pass_colormanage(p, p->image_params.color, fbo.color_space, flags, false);
+
+ // Since finish_pass_fbo doesn't work with compute shaders, and neither
+ // does the checkerboard/dither code, we may need an indirection via
+ // p->screen_tex here.
+ if (p->pass_compute.active) {
+ int o_w = p->dst_rect.x1 - p->dst_rect.x0,
+ o_h = p->dst_rect.y1 - p->dst_rect.y0;
+ finish_pass_tex(p, &p->screen_tex, o_w, o_h);
+ struct image tmp = image_wrap(p->screen_tex, PLANE_RGB, p->components);
+ copy_image(p, &(int){0}, tmp);
+ }
+
+ if (p->has_alpha){
+ if (p->opts.alpha_mode == ALPHA_BLEND_TILES) {
+ // Draw checkerboard pattern to indicate transparency
+ GLSLF("// transparency checkerboard\n");
+ GLSL(bvec2 tile = lessThan(fract(gl_FragCoord.xy * 1.0/32.0), vec2(0.5));)
+ GLSL(vec3 background = vec3(tile.x == tile.y ? 0.93 : 0.87);)
+ GLSL(color.rgb += background.rgb * (1.0 - color.a);)
+ GLSL(color.a = 1.0;)
+ } else if (p->opts.alpha_mode == ALPHA_BLEND) {
+ // Blend into background color (usually black)
+ struct m_color c = p->opts.background;
+ GLSLF("vec4 background = vec4(%f, %f, %f, %f);\n",
+ c.r / 255.0, c.g / 255.0, c.b / 255.0, c.a / 255.0);
+ GLSL(color.rgb += background.rgb * (1.0 - color.a);)
+ GLSL(color.a = background.a;)
+ }
+ }
+
+ pass_opt_hook_point(p, "OUTPUT", NULL);
+
+ if (flags & RENDER_SCREEN_COLOR)
+ pass_dither(p);
+ pass_describe(p, "output to screen");
+ finish_pass_fbo(p, fbo, false, &p->dst_rect);
+}
+
+// flags: bit set of RENDER_FRAME_* flags
+static bool update_surface(struct gl_video *p, struct mp_image *mpi,
+ uint64_t id, struct surface *surf, int flags)
+{
+ int vp_w = p->dst_rect.x1 - p->dst_rect.x0,
+ vp_h = p->dst_rect.y1 - p->dst_rect.y0;
+
+ pass_info_reset(p, false);
+ if (!pass_render_frame(p, mpi, id, flags))
+ return false;
+
+ // Frame blending should always be done in linear light to preserve the
+ // overall brightness, otherwise this will result in flashing dark frames
+ // because mixing in compressed light artificially darkens the results
+ if (!p->use_linear) {
+ p->use_linear = true;
+ pass_linearize(p->sc, p->image_params.color.gamma);
+ }
+
+ finish_pass_tex(p, &surf->tex, vp_w, vp_h);
+ surf->id = id;
+ surf->pts = mpi->pts;
+ return true;
+}
+
+// Draws an interpolate frame to fbo, based on the frame timing in t
+// flags: bit set of RENDER_FRAME_* flags
+static void gl_video_interpolate_frame(struct gl_video *p, struct vo_frame *t,
+ struct ra_fbo fbo, int flags)
+{
+ bool is_new = false;
+
+ // Reset the queue completely if this is a still image, to avoid any
+ // interpolation artifacts from surrounding frames when unpausing or
+ // framestepping
+ if (t->still)
+ gl_video_reset_surfaces(p);
+
+ // First of all, figure out if we have a frame available at all, and draw
+ // it manually + reset the queue if not
+ if (p->surfaces[p->surface_now].id == 0) {
+ struct surface *now = &p->surfaces[p->surface_now];
+ if (!update_surface(p, t->current, t->frame_id, now, flags))
+ return;
+ p->surface_idx = p->surface_now;
+ is_new = true;
+ }
+
+ // Find the right frame for this instant
+ if (t->current) {
+ int next = surface_wrap(p->surface_now + 1);
+ while (p->surfaces[next].id &&
+ p->surfaces[next].id > p->surfaces[p->surface_now].id &&
+ p->surfaces[p->surface_now].id < t->frame_id)
+ {
+ p->surface_now = next;
+ next = surface_wrap(next + 1);
+ }
+ }
+
+ // Figure out the queue size. For illustration, a filter radius of 2 would
+ // look like this: _ A [B] C D _
+ // A is surface_bse, B is surface_now, C is surface_now+1 and D is
+ // surface_end.
+ struct scaler *tscale = &p->scaler[SCALER_TSCALE];
+ reinit_scaler(p, tscale, &p->opts.scaler[SCALER_TSCALE], 1, tscale_sizes);
+ bool oversample = strcmp(tscale->conf.kernel.name, "oversample") == 0;
+ bool linear = strcmp(tscale->conf.kernel.name, "linear") == 0;
+ int size;
+
+ if (oversample || linear) {
+ size = 2;
+ } else {
+ assert(tscale->kernel && !tscale->kernel->polar);
+ size = ceil(tscale->kernel->size);
+ }
+
+ int radius = size/2;
+ int surface_now = p->surface_now;
+ int surface_bse = surface_wrap(surface_now - (radius-1));
+ int surface_end = surface_wrap(surface_now + radius);
+ assert(surface_wrap(surface_bse + size-1) == surface_end);
+
+ // Render new frames while there's room in the queue. Note that technically,
+ // this should be done before the step where we find the right frame, but
+ // it only barely matters at the very beginning of playback, and this way
+ // makes the code much more linear.
+ int surface_dst = surface_wrap(p->surface_idx + 1);
+ for (int i = 0; i < t->num_frames; i++) {
+ // Avoid overwriting data we might still need
+ if (surface_dst == surface_bse - 1)
+ break;
+
+ struct mp_image *f = t->frames[i];
+ uint64_t f_id = t->frame_id + i;
+ if (!mp_image_params_equal(&f->params, &p->real_image_params))
+ continue;
+
+ if (f_id > p->surfaces[p->surface_idx].id) {
+ struct surface *dst = &p->surfaces[surface_dst];
+ if (!update_surface(p, f, f_id, dst, flags))
+ return;
+ p->surface_idx = surface_dst;
+ surface_dst = surface_wrap(surface_dst + 1);
+ is_new = true;
+ }
+ }
+
+ // Figure out whether the queue is "valid". A queue is invalid if the
+ // frames' PTS is not monotonically increasing. Anything else is invalid,
+ // so avoid blending incorrect data and just draw the latest frame as-is.
+ // Possible causes for failure of this condition include seeks, pausing,
+ // end of playback or start of playback.
+ bool valid = true;
+ for (int i = surface_bse, ii; valid && i != surface_end; i = ii) {
+ ii = surface_wrap(i + 1);
+ if (p->surfaces[i].id == 0 || p->surfaces[ii].id == 0) {
+ valid = false;
+ } else if (p->surfaces[ii].id < p->surfaces[i].id) {
+ valid = false;
+ MP_DBG(p, "interpolation queue underrun\n");
+ }
+ }
+
+ // Update OSD PTS to synchronize subtitles with the displayed frame
+ p->osd_pts = p->surfaces[surface_now].pts;
+
+ // Finally, draw the right mix of frames to the screen.
+ if (!is_new)
+ pass_info_reset(p, true);
+ pass_describe(p, "interpolation");
+ if (!valid || t->still) {
+ // surface_now is guaranteed to be valid, so we can safely use it.
+ pass_read_tex(p, p->surfaces[surface_now].tex);
+ p->is_interpolated = false;
+ } else {
+ double mix = t->vsync_offset / t->ideal_frame_duration;
+ // The scaler code always wants the fcoord to be between 0 and 1,
+ // so we try to adjust by using the previous set of N frames instead
+ // (which requires some extra checking to make sure it's valid)
+ if (mix < 0.0) {
+ int prev = surface_wrap(surface_bse - 1);
+ if (p->surfaces[prev].id != 0 &&
+ p->surfaces[prev].id < p->surfaces[surface_bse].id)
+ {
+ mix += 1.0;
+ surface_bse = prev;
+ } else {
+ mix = 0.0; // at least don't blow up, this should only
+ // ever happen at the start of playback
+ }
+ }
+
+ if (oversample) {
+ // Oversample uses the frame area as mix ratio, not the vsync
+ // position itself
+ double vsync_dist = t->vsync_interval / t->ideal_frame_duration,
+ threshold = tscale->conf.kernel.params[0];
+ threshold = isnan(threshold) ? 0.0 : threshold;
+ mix = (1 - mix) / vsync_dist;
+ mix = mix <= 0 + threshold ? 0 : mix;
+ mix = mix >= 1 - threshold ? 1 : mix;
+ mix = 1 - mix;
+ }
+
+ // Blend the frames together
+ if (oversample || linear) {
+ gl_sc_uniform_dynamic(p->sc);
+ gl_sc_uniform_f(p->sc, "inter_coeff", mix);
+ GLSL(color = mix(texture(texture0, texcoord0),
+ texture(texture1, texcoord1),
+ inter_coeff);)
+ } else {
+ gl_sc_uniform_dynamic(p->sc);
+ gl_sc_uniform_f(p->sc, "fcoord", mix);
+ pass_sample_separated_gen(p->sc, tscale, 0, 0);
+ }
+
+ // Load all the required frames
+ for (int i = 0; i < size; i++) {
+ struct image img =
+ image_wrap(p->surfaces[surface_wrap(surface_bse+i)].tex,
+ PLANE_RGB, p->components);
+ // Since the code in pass_sample_separated currently assumes
+ // the textures are bound in-order and starting at 0, we just
+ // assert to make sure this is the case (which it should always be)
+ int id = pass_bind(p, img);
+ assert(id == i);
+ }
+
+ MP_TRACE(p, "inter frame dur: %f vsync: %f, mix: %f\n",
+ t->ideal_frame_duration, t->vsync_interval, mix);
+ p->is_interpolated = true;
+ }
+ pass_draw_to_screen(p, fbo, flags);
+
+ p->frames_drawn += 1;
+}
+
+void gl_video_render_frame(struct gl_video *p, struct vo_frame *frame,
+ struct ra_fbo fbo, int flags)
+{
+ gl_video_update_options(p);
+
+ struct mp_rect target_rc = {0, 0, fbo.tex->params.w, fbo.tex->params.h};
+
+ p->broken_frame = false;
+
+ bool has_frame = !!frame->current;
+
+ struct m_color c = p->clear_color;
+ float clear_color[4] = {c.r / 255.0, c.g / 255.0, c.b / 255.0, c.a / 255.0};
+ p->ra->fns->clear(p->ra, fbo.tex, clear_color, &target_rc);
+
+ if (p->hwdec_overlay) {
+ if (has_frame) {
+ float *color = p->hwdec_overlay->overlay_colorkey;
+ p->ra->fns->clear(p->ra, fbo.tex, color, &p->dst_rect);
+ }
+
+ p->hwdec_overlay->driver->overlay_frame(p->hwdec_overlay, frame->current,
+ &p->src_rect, &p->dst_rect,
+ frame->frame_id != p->image.id);
+
+ if (frame->current)
+ p->osd_pts = frame->current->pts;
+
+ // Disable GL rendering
+ has_frame = false;
+ }
+
+ if (has_frame) {
+ bool interpolate = p->opts.interpolation && frame->display_synced &&
+ (p->frames_drawn || !frame->still);
+ if (interpolate) {
+ double ratio = frame->ideal_frame_duration / frame->vsync_interval;
+ if (fabs(ratio - 1.0) < p->opts.interpolation_threshold)
+ interpolate = false;
+ }
+
+ if (interpolate) {
+ gl_video_interpolate_frame(p, frame, fbo, flags);
+ } else {
+ bool is_new = frame->frame_id != p->image.id;
+
+ // Redrawing a frame might update subtitles.
+ if (frame->still && p->opts.blend_subs)
+ is_new = true;
+
+ if (is_new || !p->output_tex_valid) {
+ p->output_tex_valid = false;
+
+ pass_info_reset(p, !is_new);
+ if (!pass_render_frame(p, frame->current, frame->frame_id, flags))
+ goto done;
+
+ // For the non-interpolation case, we draw to a single "cache"
+ // texture to speed up subsequent re-draws (if any exist)
+ struct ra_fbo dest_fbo = fbo;
+ bool repeats = frame->num_vsyncs > 1 && frame->display_synced;
+ if ((repeats || frame->still) && !p->dumb_mode &&
+ (p->ra->caps & RA_CAP_BLIT) && fbo.tex->params.blit_dst)
+ {
+ // Attempt to use the same format as the destination FBO
+ // if possible. Some RAs use a wrapped dummy format here,
+ // so fall back to the fbo_format in that case.
+ const struct ra_format *fmt = fbo.tex->params.format;
+ if (fmt->dummy_format)
+ fmt = p->fbo_format;
+
+ bool r = ra_tex_resize(p->ra, p->log, &p->output_tex,
+ fbo.tex->params.w, fbo.tex->params.h,
+ fmt);
+ if (r) {
+ dest_fbo = (struct ra_fbo) { p->output_tex };
+ p->output_tex_valid = true;
+ }
+ }
+ pass_draw_to_screen(p, dest_fbo, flags);
+ }
+
+ // "output tex valid" and "output tex needed" are equivalent
+ if (p->output_tex_valid && fbo.tex->params.blit_dst) {
+ pass_info_reset(p, true);
+ pass_describe(p, "redraw cached frame");
+ struct mp_rect src = p->dst_rect;
+ struct mp_rect dst = src;
+ if (fbo.flip) {
+ dst.y0 = fbo.tex->params.h - src.y0;
+ dst.y1 = fbo.tex->params.h - src.y1;
+ }
+ timer_pool_start(p->blit_timer);
+ p->ra->fns->blit(p->ra, fbo.tex, p->output_tex, &dst, &src);
+ timer_pool_stop(p->blit_timer);
+ pass_record(p, timer_pool_measure(p->blit_timer));
+ }
+ }
+ }
+
+done:
+
+ debug_check_gl(p, "after video rendering");
+
+ if (p->osd && (flags & (RENDER_FRAME_SUBS | RENDER_FRAME_OSD))) {
+ // If we haven't actually drawn anything so far, then we technically
+ // need to consider this the start of a new pass. Let's call it a
+ // redraw just because, since it's basically a blank frame anyway
+ if (!has_frame)
+ pass_info_reset(p, true);
+
+ int osd_flags = p->opts.blend_subs ? OSD_DRAW_OSD_ONLY : 0;
+ if (!(flags & RENDER_FRAME_SUBS))
+ osd_flags |= OSD_DRAW_OSD_ONLY;
+ if (!(flags & RENDER_FRAME_OSD))
+ osd_flags |= OSD_DRAW_SUB_ONLY;
+
+ pass_draw_osd(p, osd_flags, flags, p->osd_pts, p->osd_rect, fbo, true);
+ debug_check_gl(p, "after OSD rendering");
+ }
+
+ p->broken_frame |= gl_sc_error_state(p->sc);
+ if (p->broken_frame) {
+ // Make the screen solid blue to make it visually clear that an
+ // error has occurred
+ float color[4] = {0.0, 0.05, 0.5, 1.0};
+ p->ra->fns->clear(p->ra, fbo.tex, color, &target_rc);
+ }
+
+ p->frames_rendered++;
+ pass_report_performance(p);
+}
+
+void gl_video_screenshot(struct gl_video *p, struct vo_frame *frame,
+ struct voctrl_screenshot *args)
+{
+ if (!p->ra->fns->tex_download)
+ return;
+
+ bool ok = false;
+ struct mp_image *res = NULL;
+ struct ra_tex *target = NULL;
+ struct mp_rect old_src = p->src_rect;
+ struct mp_rect old_dst = p->dst_rect;
+ struct mp_osd_res old_osd = p->osd_rect;
+ struct vo_frame *nframe = vo_frame_ref(frame);
+
+ // Disable interpolation and such.
+ nframe->redraw = true;
+ nframe->repeat = false;
+ nframe->still = true;
+ nframe->pts = 0;
+ nframe->duration = -1;
+
+ if (!args->scaled) {
+ int w, h;
+ mp_image_params_get_dsize(&p->image_params, &w, &h);
+ if (w < 1 || h < 1)
+ return;
+
+ int src_w = p->image_params.w;
+ int src_h = p->image_params.h;
+ struct mp_rect src = {0, 0, src_w, src_h};
+ struct mp_rect dst = {0, 0, w, h};
+
+ if (mp_image_crop_valid(&p->image_params))
+ src = p->image_params.crop;
+
+ if (p->image_params.rotate % 180 == 90) {
+ MPSWAP(int, w, h);
+ MPSWAP(int, src_w, src_h);
+ }
+ mp_rect_rotate(&src, src_w, src_h, p->image_params.rotate);
+ mp_rect_rotate(&dst, w, h, p->image_params.rotate);
+
+ struct mp_osd_res osd = {
+ .display_par = 1.0,
+ .w = mp_rect_w(dst),
+ .h = mp_rect_h(dst),
+ };
+ gl_video_resize(p, &src, &dst, &osd);
+ }
+
+ gl_video_reset_surfaces(p);
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .downloadable = true,
+ .w = p->osd_rect.w,
+ .h = p->osd_rect.h,
+ .d = 1,
+ .render_dst = true,
+ };
+
+ params.format = ra_find_unorm_format(p->ra, 1, 4);
+ int mpfmt = IMGFMT_RGB0;
+ if (args->high_bit_depth && p->ra_format.component_bits > 8) {
+ const struct ra_format *fmt = ra_find_unorm_format(p->ra, 2, 4);
+ if (fmt && fmt->renderable) {
+ params.format = fmt;
+ mpfmt = IMGFMT_RGBA64;
+ }
+ }
+
+ if (!params.format || !params.format->renderable)
+ goto done;
+ target = ra_tex_create(p->ra, &params);
+ if (!target)
+ goto done;
+
+ int flags = 0;
+ if (args->subs)
+ flags |= RENDER_FRAME_SUBS;
+ if (args->osd)
+ flags |= RENDER_FRAME_OSD;
+ if (args->scaled)
+ flags |= RENDER_SCREEN_COLOR;
+ gl_video_render_frame(p, nframe, (struct ra_fbo){target}, flags);
+
+ res = mp_image_alloc(mpfmt, params.w, params.h);
+ if (!res)
+ goto done;
+
+ struct ra_tex_download_params download_params = {
+ .tex = target,
+ .dst = res->planes[0],
+ .stride = res->stride[0],
+ };
+ if (!p->ra->fns->tex_download(p->ra, &download_params))
+ goto done;
+
+ if (p->broken_frame)
+ goto done;
+
+ ok = true;
+done:
+ talloc_free(nframe);
+ ra_tex_free(p->ra, &target);
+ gl_video_resize(p, &old_src, &old_dst, &old_osd);
+ gl_video_reset_surfaces(p);
+ if (!ok)
+ TA_FREEP(&res);
+ args->res = res;
+}
+
+// Use this color instead of the global option.
+void gl_video_set_clear_color(struct gl_video *p, struct m_color c)
+{
+ p->force_clear_color = true;
+ p->clear_color = c;
+}
+
+void gl_video_set_osd_pts(struct gl_video *p, double pts)
+{
+ p->osd_pts = pts;
+}
+
+bool gl_video_check_osd_change(struct gl_video *p, struct mp_osd_res *res,
+ double pts)
+{
+ return p->osd ? mpgl_osd_check_change(p->osd, res, pts) : false;
+}
+
+void gl_video_resize(struct gl_video *p,
+ struct mp_rect *src, struct mp_rect *dst,
+ struct mp_osd_res *osd)
+{
+ if (mp_rect_equals(&p->src_rect, src) &&
+ mp_rect_equals(&p->dst_rect, dst) &&
+ osd_res_equals(p->osd_rect, *osd))
+ return;
+
+ p->src_rect = *src;
+ p->dst_rect = *dst;
+ p->osd_rect = *osd;
+
+ gl_video_reset_surfaces(p);
+
+ if (p->osd)
+ mpgl_osd_resize(p->osd, p->osd_rect, p->image_params.stereo3d);
+}
+
+static void frame_perf_data(struct pass_info pass[], struct mp_frame_perf *out)
+{
+ for (int i = 0; i < VO_PASS_PERF_MAX; i++) {
+ if (!pass[i].desc.len)
+ break;
+ out->perf[out->count] = pass[i].perf;
+ strncpy(out->desc[out->count], pass[i].desc.start,
+ sizeof(out->desc[out->count]) - 1);
+ out->desc[out->count][sizeof(out->desc[out->count]) - 1] = '\0';
+ out->count++;
+ }
+}
+
+void gl_video_perfdata(struct gl_video *p, struct voctrl_performance_data *out)
+{
+ *out = (struct voctrl_performance_data){0};
+ frame_perf_data(p->pass_fresh, &out->fresh);
+ frame_perf_data(p->pass_redraw, &out->redraw);
+}
+
+// Returns false on failure.
+static bool pass_upload_image(struct gl_video *p, struct mp_image *mpi, uint64_t id)
+{
+ struct video_image *vimg = &p->image;
+
+ if (vimg->id == id)
+ return true;
+
+ unref_current_image(p);
+
+ mpi = mp_image_new_ref(mpi);
+ if (!mpi)
+ goto error;
+
+ vimg->mpi = mpi;
+ vimg->id = id;
+ p->osd_pts = mpi->pts;
+ p->frames_uploaded++;
+
+ if (p->hwdec_active) {
+ // Hardware decoding
+
+ if (!p->hwdec_mapper)
+ goto error;
+
+ pass_describe(p, "map frame (hwdec)");
+ timer_pool_start(p->upload_timer);
+ bool ok = ra_hwdec_mapper_map(p->hwdec_mapper, vimg->mpi) >= 0;
+ timer_pool_stop(p->upload_timer);
+ pass_record(p, timer_pool_measure(p->upload_timer));
+
+ vimg->hwdec_mapped = true;
+ if (ok) {
+ struct mp_image layout = {0};
+ mp_image_set_params(&layout, &p->image_params);
+ struct ra_tex **tex = p->hwdec_mapper->tex;
+ for (int n = 0; n < p->plane_count; n++) {
+ vimg->planes[n] = (struct texplane){
+ .w = mp_image_plane_w(&layout, n),
+ .h = mp_image_plane_h(&layout, n),
+ .tex = tex[n],
+ };
+ }
+ } else {
+ MP_FATAL(p, "Mapping hardware decoded surface failed.\n");
+ goto error;
+ }
+ return true;
+ }
+
+ // Software decoding
+ assert(mpi->num_planes == p->plane_count);
+
+ timer_pool_start(p->upload_timer);
+ for (int n = 0; n < p->plane_count; n++) {
+ struct texplane *plane = &vimg->planes[n];
+ if (!plane->tex) {
+ timer_pool_stop(p->upload_timer);
+ goto error;
+ }
+
+ struct ra_tex_upload_params params = {
+ .tex = plane->tex,
+ .src = mpi->planes[n],
+ .invalidate = true,
+ .stride = mpi->stride[n],
+ };
+
+ plane->flipped = params.stride < 0;
+ if (plane->flipped) {
+ int h = mp_image_plane_h(mpi, n);
+ params.src = (char *)params.src + (h - 1) * params.stride;
+ params.stride = -params.stride;
+ }
+
+ struct dr_buffer *mapped = gl_find_dr_buffer(p, mpi->planes[n]);
+ if (mapped) {
+ params.buf = mapped->buf;
+ params.buf_offset = (uintptr_t)params.src -
+ (uintptr_t)mapped->buf->data;
+ params.src = NULL;
+ }
+
+ if (p->using_dr_path != !!mapped) {
+ p->using_dr_path = !!mapped;
+ MP_VERBOSE(p, "DR enabled: %s\n", p->using_dr_path ? "yes" : "no");
+ }
+
+ if (!p->ra->fns->tex_upload(p->ra, &params)) {
+ timer_pool_stop(p->upload_timer);
+ goto error;
+ }
+
+ if (mapped && !mapped->mpi)
+ mapped->mpi = mp_image_new_ref(mpi);
+ }
+ timer_pool_stop(p->upload_timer);
+
+ bool using_pbo = p->ra->use_pbo || !(p->ra->caps & RA_CAP_DIRECT_UPLOAD);
+ const char *mode = p->using_dr_path ? "DR" : using_pbo ? "PBO" : "naive";
+ pass_describe(p, "upload frame (%s)", mode);
+ pass_record(p, timer_pool_measure(p->upload_timer));
+
+ return true;
+
+error:
+ unref_current_image(p);
+ p->broken_frame = true;
+ return false;
+}
+
+static bool test_fbo(struct gl_video *p, const struct ra_format *fmt)
+{
+ MP_VERBOSE(p, "Testing FBO format %s\n", fmt->name);
+ struct ra_tex *tex = NULL;
+ bool success = ra_tex_resize(p->ra, p->log, &tex, 16, 16, fmt);
+ ra_tex_free(p->ra, &tex);
+ return success;
+}
+
+// Return whether dumb-mode can be used without disabling any features.
+// Essentially, vo_gpu with mostly default settings will return true.
+static bool check_dumb_mode(struct gl_video *p)
+{
+ struct gl_video_opts *o = &p->opts;
+ if (p->use_integer_conversion)
+ return false;
+ if (o->dumb_mode > 0) // requested by user
+ return true;
+ if (o->dumb_mode < 0) // disabled by user
+ return false;
+
+ // otherwise, use auto-detection
+ if (o->correct_downscaling || o->linear_downscaling ||
+ o->linear_upscaling || o->sigmoid_upscaling || o->interpolation ||
+ o->blend_subs || o->deband || o->unsharp)
+ return false;
+ // check remaining scalers (tscale is already implicitly excluded above)
+ for (int i = 0; i < SCALER_COUNT; i++) {
+ if (i != SCALER_TSCALE) {
+ const char *name = o->scaler[i].kernel.name;
+ if (name && strcmp(name, "bilinear") != 0)
+ return false;
+ }
+ }
+ if (o->user_shaders && o->user_shaders[0])
+ return false;
+ return true;
+}
+
+// Disable features that are not supported with the current OpenGL version.
+static void check_gl_features(struct gl_video *p)
+{
+ struct ra *ra = p->ra;
+ bool have_float_tex = !!ra_find_float16_format(ra, 1);
+ bool have_mglsl = ra->glsl_version >= 130; // modern GLSL
+ const struct ra_format *rg_tex = ra_find_unorm_format(p->ra, 1, 2);
+ bool have_texrg = rg_tex && !rg_tex->luminance_alpha;
+ bool have_compute = ra->caps & RA_CAP_COMPUTE;
+ bool have_ssbo = ra->caps & RA_CAP_BUF_RW;
+ bool have_fragcoord = ra->caps & RA_CAP_FRAGCOORD;
+
+ const char *auto_fbo_fmts[] = {"rgba16f", "rgba16hf", "rgba16",
+ "rgb10_a2", "rgba8", 0};
+ const char *user_fbo_fmts[] = {p->opts.fbo_format, 0};
+ const char **fbo_fmts = user_fbo_fmts[0] && strcmp(user_fbo_fmts[0], "auto")
+ ? user_fbo_fmts : auto_fbo_fmts;
+ bool user_specified_fbo_fmt = fbo_fmts == user_fbo_fmts;
+ bool fbo_test_result = false;
+ bool have_fbo = false;
+ p->fbo_format = NULL;
+ for (int n = 0; fbo_fmts[n]; n++) {
+ const char *fmt = fbo_fmts[n];
+ const struct ra_format *f = ra_find_named_format(p->ra, fmt);
+ if (!f && user_specified_fbo_fmt)
+ MP_WARN(p, "FBO format '%s' not found!\n", fmt);
+ if (f && f->renderable && f->linear_filter &&
+ (fbo_test_result = test_fbo(p, f))) {
+ MP_VERBOSE(p, "Using FBO format %s.\n", f->name);
+ have_fbo = true;
+ p->fbo_format = f;
+ break;
+ }
+
+ if (user_specified_fbo_fmt) {
+ MP_WARN(p, "User-specified FBO format '%s' failed to initialize! "
+ "(exists=%d, renderable=%d, linear_filter=%d, "
+ "fbo_test_result=%d)\n",
+ fmt, !!f, f ? f->renderable : 0, f ? f->linear_filter : 0,
+ fbo_test_result);
+ }
+ }
+
+ if (!have_fragcoord && p->opts.dither_depth >= 0 &&
+ p->opts.dither_algo != DITHER_NONE)
+ {
+ p->opts.dither_algo = DITHER_NONE;
+ MP_WARN(p, "Disabling dithering (no gl_FragCoord).\n");
+ }
+ if (!have_fragcoord && p->opts.alpha_mode == ALPHA_BLEND_TILES) {
+ p->opts.alpha_mode = ALPHA_BLEND;
+ // Verbose, since this is the default setting
+ MP_VERBOSE(p, "Disabling alpha checkerboard (no gl_FragCoord).\n");
+ }
+ if (!have_fbo && have_compute) {
+ have_compute = false;
+ MP_WARN(p, "Force-disabling compute shaders as an FBO format was not "
+ "available! See your FBO format configuration!\n");
+ }
+
+ if (have_compute && have_fbo && !p->fbo_format->storable) {
+ have_compute = false;
+ MP_WARN(p, "Force-disabling compute shaders as the chosen FBO format "
+ "is not storable! See your FBO format configuration!\n");
+ }
+
+ if (!have_compute && p->opts.dither_algo == DITHER_ERROR_DIFFUSION) {
+ MP_WARN(p, "Disabling error diffusion dithering because compute shader "
+ "was not supported. Fallback to dither=fruit instead.\n");
+ p->opts.dither_algo = DITHER_FRUIT;
+ }
+
+ bool have_compute_peak = have_compute && have_ssbo;
+ if (!have_compute_peak && p->opts.tone_map.compute_peak >= 0) {
+ int msgl = p->opts.tone_map.compute_peak == 1 ? MSGL_WARN : MSGL_V;
+ MP_MSG(p, msgl, "Disabling HDR peak computation (one or more of the "
+ "following is not supported: compute shaders=%d, "
+ "SSBO=%d).\n", have_compute, have_ssbo);
+ p->opts.tone_map.compute_peak = -1;
+ }
+
+ p->forced_dumb_mode = p->opts.dumb_mode > 0 || !have_fbo || !have_texrg;
+ bool voluntarily_dumb = check_dumb_mode(p);
+ if (p->forced_dumb_mode || voluntarily_dumb) {
+ if (voluntarily_dumb) {
+ MP_VERBOSE(p, "No advanced processing required. Enabling dumb mode.\n");
+ } else if (p->opts.dumb_mode <= 0) {
+ MP_WARN(p, "High bit depth FBOs unsupported. Enabling dumb mode.\n"
+ "Most extended features will be disabled.\n");
+ }
+ p->dumb_mode = true;
+ static const struct scaler_config dumb_scaler_config = {
+ {"bilinear", .params = {NAN, NAN}},
+ {.params = {NAN, NAN}},
+ };
+ // Most things don't work, so whitelist all options that still work.
+ p->opts = (struct gl_video_opts){
+ .scaler = {
+ [SCALER_SCALE] = dumb_scaler_config,
+ [SCALER_DSCALE] = dumb_scaler_config,
+ [SCALER_CSCALE] = dumb_scaler_config,
+ [SCALER_TSCALE] = dumb_scaler_config,
+ },
+ .gamma = p->opts.gamma,
+ .gamma_auto = p->opts.gamma_auto,
+ .pbo = p->opts.pbo,
+ .fbo_format = p->opts.fbo_format,
+ .alpha_mode = p->opts.alpha_mode,
+ .use_rectangle = p->opts.use_rectangle,
+ .background = p->opts.background,
+ .dither_algo = p->opts.dither_algo,
+ .dither_depth = p->opts.dither_depth,
+ .dither_size = p->opts.dither_size,
+ .error_diffusion = p->opts.error_diffusion,
+ .temporal_dither = p->opts.temporal_dither,
+ .temporal_dither_period = p->opts.temporal_dither_period,
+ .tex_pad_x = p->opts.tex_pad_x,
+ .tex_pad_y = p->opts.tex_pad_y,
+ .tone_map = p->opts.tone_map,
+ .early_flush = p->opts.early_flush,
+ .icc_opts = p->opts.icc_opts,
+ .hwdec_interop = p->opts.hwdec_interop,
+ .target_trc = p->opts.target_trc,
+ .target_prim = p->opts.target_prim,
+ .target_peak = p->opts.target_peak,
+ };
+ if (!have_fbo)
+ p->use_lut_3d = false;
+ return;
+ }
+ p->dumb_mode = false;
+
+ // Normally, we want to disable them by default if FBOs are unavailable,
+ // because they will be slow (not critically slow, but still slower).
+ // Without FP textures, we must always disable them.
+ // I don't know if luminance alpha float textures exist, so disregard them.
+ for (int n = 0; n < SCALER_COUNT; n++) {
+ const struct filter_kernel *kernel =
+ mp_find_filter_kernel(p->opts.scaler[n].kernel.name);
+ if (kernel) {
+ char *reason = NULL;
+ if (!have_float_tex)
+ reason = "(float tex. missing)";
+ if (!have_mglsl)
+ reason = "(GLSL version too old)";
+ if (reason) {
+ MP_WARN(p, "Disabling scaler #%d %s %s.\n", n,
+ p->opts.scaler[n].kernel.name, reason);
+ // p->opts is a copy => we can just mess with it.
+ p->opts.scaler[n].kernel.name = "bilinear";
+ if (n == SCALER_TSCALE)
+ p->opts.interpolation = false;
+ }
+ }
+ }
+
+ int use_cms = p->opts.target_prim != MP_CSP_PRIM_AUTO ||
+ p->opts.target_trc != MP_CSP_TRC_AUTO || p->use_lut_3d;
+
+ // mix() is needed for some gamma functions
+ if (!have_mglsl && (p->opts.linear_downscaling ||
+ p->opts.linear_upscaling || p->opts.sigmoid_upscaling))
+ {
+ p->opts.linear_downscaling = false;
+ p->opts.linear_upscaling = false;
+ p->opts.sigmoid_upscaling = false;
+ MP_WARN(p, "Disabling linear/sigmoid scaling (GLSL version too old).\n");
+ }
+ if (!have_mglsl && use_cms) {
+ p->opts.target_prim = MP_CSP_PRIM_AUTO;
+ p->opts.target_trc = MP_CSP_TRC_AUTO;
+ p->use_lut_3d = false;
+ MP_WARN(p, "Disabling color management (GLSL version too old).\n");
+ }
+ if (!have_mglsl && p->opts.deband) {
+ p->opts.deband = false;
+ MP_WARN(p, "Disabling debanding (GLSL version too old).\n");
+ }
+}
+
+static void init_gl(struct gl_video *p)
+{
+ debug_check_gl(p, "before init_gl");
+
+ p->upload_timer = timer_pool_create(p->ra);
+ p->blit_timer = timer_pool_create(p->ra);
+ p->osd_timer = timer_pool_create(p->ra);
+
+ debug_check_gl(p, "after init_gl");
+
+ ra_dump_tex_formats(p->ra, MSGL_DEBUG);
+ ra_dump_img_formats(p->ra, MSGL_DEBUG);
+}
+
+void gl_video_uninit(struct gl_video *p)
+{
+ if (!p)
+ return;
+
+ uninit_video(p);
+ ra_hwdec_ctx_uninit(&p->hwdec_ctx);
+ gl_sc_destroy(p->sc);
+
+ ra_tex_free(p->ra, &p->lut_3d_texture);
+ ra_buf_free(p->ra, &p->hdr_peak_ssbo);
+
+ timer_pool_destroy(p->upload_timer);
+ timer_pool_destroy(p->blit_timer);
+ timer_pool_destroy(p->osd_timer);
+
+ for (int i = 0; i < VO_PASS_PERF_MAX; i++) {
+ talloc_free(p->pass_fresh[i].desc.start);
+ talloc_free(p->pass_redraw[i].desc.start);
+ }
+
+ mpgl_osd_destroy(p->osd);
+
+ // Forcibly destroy possibly remaining image references. This should also
+ // cause gl_video_dr_free_buffer() to be called for the remaining buffers.
+ gc_pending_dr_fences(p, true);
+
+ // Should all have been unreffed already.
+ assert(!p->num_dr_buffers);
+
+ talloc_free(p);
+}
+
+void gl_video_reset(struct gl_video *p)
+{
+ gl_video_reset_surfaces(p);
+}
+
+bool gl_video_showing_interpolated_frame(struct gl_video *p)
+{
+ return p->is_interpolated;
+}
+
+static bool is_imgfmt_desc_supported(struct gl_video *p,
+ const struct ra_imgfmt_desc *desc)
+{
+ if (!desc->num_planes)
+ return false;
+
+ if (desc->planes[0]->ctype == RA_CTYPE_UINT && p->forced_dumb_mode)
+ return false;
+
+ return true;
+}
+
+bool gl_video_check_format(struct gl_video *p, int mp_format)
+{
+ struct ra_imgfmt_desc desc;
+ if (ra_get_imgfmt_desc(p->ra, mp_format, &desc) &&
+ is_imgfmt_desc_supported(p, &desc))
+ return true;
+ if (ra_hwdec_get(&p->hwdec_ctx, mp_format))
+ return true;
+ return false;
+}
+
+void gl_video_config(struct gl_video *p, struct mp_image_params *params)
+{
+ unmap_overlay(p);
+ unref_current_image(p);
+
+ if (!mp_image_params_equal(&p->real_image_params, params)) {
+ uninit_video(p);
+ p->real_image_params = *params;
+ p->image_params = *params;
+ if (params->imgfmt)
+ init_video(p);
+ }
+
+ gl_video_reset_surfaces(p);
+}
+
+void gl_video_set_osd_source(struct gl_video *p, struct osd_state *osd)
+{
+ mpgl_osd_destroy(p->osd);
+ p->osd = NULL;
+ p->osd_state = osd;
+ reinit_osd(p);
+}
+
+struct gl_video *gl_video_init(struct ra *ra, struct mp_log *log,
+ struct mpv_global *g)
+{
+ struct gl_video *p = talloc_ptrtype(NULL, p);
+ *p = (struct gl_video) {
+ .ra = ra,
+ .global = g,
+ .log = log,
+ .sc = gl_sc_create(ra, g, log),
+ .video_eq = mp_csp_equalizer_create(p, g),
+ .opts_cache = m_config_cache_alloc(p, g, &gl_video_conf),
+ };
+ // make sure this variable is initialized to *something*
+ p->pass = p->pass_fresh;
+ struct gl_video_opts *opts = p->opts_cache->opts;
+ p->cms = gl_lcms_init(p, log, g, opts->icc_opts),
+ p->opts = *opts;
+ for (int n = 0; n < SCALER_COUNT; n++)
+ p->scaler[n] = (struct scaler){.index = n};
+ // our VAO always has the vec2 position as the first element
+ MP_TARRAY_APPEND(p, p->vao, p->vao_len, (struct ra_renderpass_input) {
+ .name = "position",
+ .type = RA_VARTYPE_FLOAT,
+ .dim_v = 2,
+ .dim_m = 1,
+ .offset = 0,
+ });
+ init_gl(p);
+ reinit_from_options(p);
+ return p;
+}
+
+// Get static string for scaler shader. If "tscale" is set to true, the
+// scaler must be a separable convolution filter.
+static const char *handle_scaler_opt(const char *name, bool tscale)
+{
+ if (name && name[0]) {
+ const struct filter_kernel *kernel = mp_find_filter_kernel(name);
+ if (kernel && (!tscale || !kernel->polar))
+ return kernel->f.name;
+
+ const struct filter_window *window = mp_find_filter_window(name);
+ if (window)
+ return window->name;
+
+ for (const char *const *filter = tscale ? fixed_tscale_filters
+ : fixed_scale_filters;
+ *filter; filter++) {
+ if (strcmp(*filter, name) == 0)
+ return *filter;
+ }
+ }
+ return NULL;
+}
+
+static void gl_video_update_options(struct gl_video *p)
+{
+ if (m_config_cache_update(p->opts_cache)) {
+ gl_lcms_update_options(p->cms);
+ reinit_from_options(p);
+ }
+
+ if (mp_csp_equalizer_state_changed(p->video_eq))
+ p->output_tex_valid = false;
+}
+
+static void reinit_from_options(struct gl_video *p)
+{
+ p->use_lut_3d = gl_lcms_has_profile(p->cms);
+
+ // Copy the option fields, so that check_gl_features() can mutate them.
+ // This works only for the fields themselves of course, not for any memory
+ // referenced by them.
+ p->opts = *(struct gl_video_opts *)p->opts_cache->opts;
+
+ if (!p->force_clear_color)
+ p->clear_color = p->opts.background;
+
+ check_gl_features(p);
+ uninit_rendering(p);
+ if (p->opts.shader_cache)
+ gl_sc_set_cache_dir(p->sc, p->opts.shader_cache_dir);
+ p->ra->use_pbo = p->opts.pbo;
+ gl_video_setup_hooks(p);
+ reinit_osd(p);
+
+ struct mp_vo_opts *vo_opts = mp_get_config_group(p, p->global, &vo_sub_opts);
+ if (p->opts.interpolation && !vo_opts->video_sync && !p->dsi_warned) {
+ MP_WARN(p, "Interpolation now requires enabling display-sync mode.\n"
+ "E.g.: --video-sync=display-resample\n");
+ p->dsi_warned = true;
+ }
+ talloc_free(vo_opts);
+
+ if (p->opts.correct_downscaling && !p->correct_downscaling_warned) {
+ const char *name = p->opts.scaler[SCALER_DSCALE].kernel.name;
+ if (!name)
+ name = p->opts.scaler[SCALER_SCALE].kernel.name;
+ if (!name || !strcmp(name, "bilinear")) {
+ MP_WARN(p, "correct-downscaling requires non-bilinear scaler.\n");
+ p->correct_downscaling_warned = true;
+ }
+ }
+}
+
+void gl_video_configure_queue(struct gl_video *p, struct vo *vo)
+{
+ gl_video_update_options(p);
+
+ int queue_size = 1;
+
+ // Figure out an adequate size for the interpolation queue. The larger
+ // the radius, the earlier we need to queue frames.
+ if (p->opts.interpolation) {
+ const struct filter_kernel *kernel =
+ mp_find_filter_kernel(p->opts.scaler[SCALER_TSCALE].kernel.name);
+ if (kernel) {
+ // filter_scale wouldn't be correctly initialized were we to use it here.
+ // This is fine since we're always upsampling, but beware if downsampling
+ // is added!
+ double radius = kernel->f.radius;
+ radius = radius > 0 ? radius : p->opts.scaler[SCALER_TSCALE].radius;
+ queue_size += 1 + ceil(radius);
+ } else {
+ // Oversample/linear case
+ queue_size += 2;
+ }
+ }
+
+ vo_set_queue_params(vo, 0, queue_size);
+}
+
+static int validate_scaler_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ char s[32] = {0};
+ int r = 1;
+ bool tscale = bstr_equals0(name, "tscale");
+ if (bstr_equals0(param, "help")) {
+ r = M_OPT_EXIT;
+ } else if (bstr_equals0(name, "dscale") && !param.len) {
+ return r; // empty dscale means "use same as upscaler"
+ } else if (bstr_equals0(name, "cscale") && !param.len) {
+ return r; // empty cscale means "use same as upscaler"
+ } else {
+ snprintf(s, sizeof(s), "%.*s", BSTR_P(param));
+ if (!handle_scaler_opt(s, tscale))
+ r = M_OPT_INVALID;
+ }
+ if (r < 1) {
+ mp_info(log, "Available scalers:\n");
+ for (const char *const *filter = tscale ? fixed_tscale_filters
+ : fixed_scale_filters;
+ *filter; filter++) {
+ mp_info(log, " %s\n", *filter);
+ }
+ for (int n = 0; mp_filter_kernels[n].f.name; n++) {
+ if (!tscale || !mp_filter_kernels[n].polar)
+ mp_info(log, " %s\n", mp_filter_kernels[n].f.name);
+ }
+ for (int n = 0; mp_filter_windows[n].name; n++) {
+ for (int m = 0; mp_filter_kernels[m].f.name; m++) {
+ if (!strcmp(mp_filter_windows[n].name, mp_filter_kernels[m].f.name))
+ goto next_window; // don't log duplicates
+ }
+ mp_info(log, " %s\n", mp_filter_windows[n].name);
+next_window: ;
+ }
+ if (s[0])
+ mp_fatal(log, "No scaler named '%s' found!\n", s);
+ }
+ return r;
+}
+
+static int validate_window_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ char s[32] = {0};
+ int r = 1;
+ if (bstr_equals0(param, "help")) {
+ r = M_OPT_EXIT;
+ } else if (!param.len) {
+ return r; // empty string means "use preferred window"
+ } else {
+ snprintf(s, sizeof(s), "%.*s", BSTR_P(param));
+ const struct filter_window *window = mp_find_filter_window(s);
+ if (!window)
+ r = M_OPT_INVALID;
+ }
+ if (r < 1) {
+ mp_info(log, "Available windows:\n");
+ for (int n = 0; mp_filter_windows[n].name; n++)
+ mp_info(log, " %s\n", mp_filter_windows[n].name);
+ if (s[0])
+ mp_fatal(log, "No window named '%s' found!\n", s);
+ }
+ return r;
+}
+
+static int validate_error_diffusion_opt(struct mp_log *log, const m_option_t *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ char s[32] = {0};
+ int r = 1;
+ if (bstr_equals0(param, "help")) {
+ r = M_OPT_EXIT;
+ } else {
+ snprintf(s, sizeof(s), "%.*s", BSTR_P(param));
+ const struct error_diffusion_kernel *k = mp_find_error_diffusion_kernel(s);
+ if (!k)
+ r = M_OPT_INVALID;
+ }
+ if (r < 1) {
+ mp_info(log, "Available error diffusion kernels:\n");
+ for (int n = 0; mp_error_diffusion_kernels[n].name; n++)
+ mp_info(log, " %s\n", mp_error_diffusion_kernels[n].name);
+ if (s[0])
+ mp_fatal(log, "No error diffusion kernel named '%s' found!\n", s);
+ }
+ return r;
+}
+
+void gl_video_set_ambient_lux(struct gl_video *p, int lux)
+{
+ if (p->opts.gamma_auto) {
+ p->opts.gamma = gl_video_scale_ambient_lux(16.0, 256.0, 1.0, 1.2, lux);
+ MP_TRACE(p, "ambient light changed: %d lux (gamma: %f)\n", lux,
+ p->opts.gamma);
+ }
+}
+
+static void *gl_video_dr_alloc_buffer(struct gl_video *p, size_t size)
+{
+ struct ra_buf_params params = {
+ .type = RA_BUF_TYPE_TEX_UPLOAD,
+ .host_mapped = true,
+ .size = size,
+ };
+
+ struct ra_buf *buf = ra_buf_create(p->ra, &params);
+ if (!buf)
+ return NULL;
+
+ MP_TARRAY_GROW(p, p->dr_buffers, p->num_dr_buffers);
+ p->dr_buffers[p->num_dr_buffers++] = (struct dr_buffer){ .buf = buf };
+
+ return buf->data;
+}
+
+static void gl_video_dr_free_buffer(void *opaque, uint8_t *data)
+{
+ struct gl_video *p = opaque;
+
+ for (int n = 0; n < p->num_dr_buffers; n++) {
+ struct dr_buffer *buffer = &p->dr_buffers[n];
+ if (buffer->buf->data == data) {
+ assert(!buffer->mpi); // can't be freed while it has a ref
+ ra_buf_free(p->ra, &buffer->buf);
+ MP_TARRAY_REMOVE_AT(p->dr_buffers, p->num_dr_buffers, n);
+ return;
+ }
+ }
+ // not found - must not happen
+ MP_ASSERT_UNREACHABLE();
+}
+
+struct mp_image *gl_video_get_image(struct gl_video *p, int imgfmt, int w, int h,
+ int stride_align, int flags)
+{
+ if (flags & VO_DR_FLAG_HOST_CACHED) {
+ if (p->ra->caps & RA_CAP_SLOW_DR) {
+ MP_VERBOSE(p, "DR path suspected slow/uncached, disabling.\n");
+ return NULL;
+ }
+ }
+
+ if (!gl_video_check_format(p, imgfmt))
+ return NULL;
+
+ int size = mp_image_get_alloc_size(imgfmt, w, h, stride_align);
+ if (size < 0)
+ return NULL;
+
+ int alloc_size = size + stride_align;
+ void *ptr = gl_video_dr_alloc_buffer(p, alloc_size);
+ if (!ptr)
+ return NULL;
+
+ // (we expect vo.c to proxy the free callback, so it happens in the same
+ // thread it was allocated in, removing the need for synchronization)
+ struct mp_image *res = mp_image_from_buffer(imgfmt, w, h, stride_align,
+ ptr, alloc_size, p,
+ gl_video_dr_free_buffer);
+ if (!res)
+ gl_video_dr_free_buffer(p, ptr);
+ return res;
+}
+
+void gl_video_init_hwdecs(struct gl_video *p, struct ra_ctx *ra_ctx,
+ struct mp_hwdec_devices *devs,
+ bool load_all_by_default)
+{
+ assert(!p->hwdec_ctx.ra_ctx);
+ p->hwdec_ctx = (struct ra_hwdec_ctx) {
+ .log = p->log,
+ .global = p->global,
+ .ra_ctx = ra_ctx,
+ };
+
+ ra_hwdec_ctx_init(&p->hwdec_ctx, devs, p->opts.hwdec_interop, load_all_by_default);
+}
+
+void gl_video_load_hwdecs_for_img_fmt(struct gl_video *p, struct mp_hwdec_devices *devs,
+ struct hwdec_imgfmt_request *params)
+{
+ assert(p->hwdec_ctx.ra_ctx);
+ ra_hwdec_ctx_load_fmt(&p->hwdec_ctx, devs, params);
+}
diff --git a/video/out/gpu/video.h b/video/out/gpu/video.h
new file mode 100644
index 0000000..411d336
--- /dev/null
+++ b/video/out/gpu/video.h
@@ -0,0 +1,238 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MP_GL_VIDEO_H
+#define MP_GL_VIDEO_H
+
+#include <stdbool.h>
+
+#include "options/m_option.h"
+#include "sub/osd.h"
+#include "utils.h"
+#include "lcms.h"
+#include "shader_cache.h"
+#include "video/csputils.h"
+#include "video/out/filter_kernels.h"
+
+struct scaler_fun {
+ char *name;
+ float params[2];
+ float blur;
+ float taper;
+};
+
+struct scaler_config {
+ struct scaler_fun kernel;
+ struct scaler_fun window;
+ float radius;
+ float antiring;
+ float clamp;
+};
+
+struct scaler {
+ int index;
+ struct scaler_config conf;
+ double scale_factor;
+ bool initialized;
+ struct filter_kernel *kernel;
+ struct ra_tex *lut;
+ struct ra_tex *sep_fbo;
+ bool insufficient;
+
+ // kernel points here
+ struct filter_kernel kernel_storage;
+};
+
+enum scaler_unit {
+ SCALER_SCALE, // luma/video
+ SCALER_DSCALE, // luma-video downscaling
+ SCALER_CSCALE, // chroma upscaling
+ SCALER_TSCALE, // temporal scaling (interpolation)
+ SCALER_COUNT
+};
+
+enum dither_algo {
+ DITHER_NONE = 0,
+ DITHER_FRUIT,
+ DITHER_ORDERED,
+ DITHER_ERROR_DIFFUSION,
+};
+
+enum alpha_mode {
+ ALPHA_NO = 0,
+ ALPHA_YES,
+ ALPHA_BLEND,
+ ALPHA_BLEND_TILES,
+};
+
+enum blend_subs_mode {
+ BLEND_SUBS_NO = 0,
+ BLEND_SUBS_YES,
+ BLEND_SUBS_VIDEO,
+};
+
+enum tone_mapping {
+ TONE_MAPPING_AUTO,
+ TONE_MAPPING_CLIP,
+ TONE_MAPPING_MOBIUS,
+ TONE_MAPPING_REINHARD,
+ TONE_MAPPING_HABLE,
+ TONE_MAPPING_GAMMA,
+ TONE_MAPPING_LINEAR,
+ TONE_MAPPING_SPLINE,
+ TONE_MAPPING_BT_2390,
+ TONE_MAPPING_BT_2446A,
+ TONE_MAPPING_ST2094_40,
+ TONE_MAPPING_ST2094_10,
+};
+
+enum gamut_mode {
+ GAMUT_AUTO,
+ GAMUT_CLIP,
+ GAMUT_PERCEPTUAL,
+ GAMUT_RELATIVE,
+ GAMUT_SATURATION,
+ GAMUT_ABSOLUTE,
+ GAMUT_DESATURATE,
+ GAMUT_DARKEN,
+ GAMUT_WARN,
+ GAMUT_LINEAR,
+};
+
+struct gl_tone_map_opts {
+ int curve;
+ float curve_param;
+ float max_boost;
+ bool inverse;
+ int compute_peak;
+ float decay_rate;
+ float scene_threshold_low;
+ float scene_threshold_high;
+ float peak_percentile;
+ float contrast_recovery;
+ float contrast_smoothness;
+ int gamut_mode;
+ bool visualize;
+};
+
+struct gl_video_opts {
+ int dumb_mode;
+ struct scaler_config scaler[4];
+ float gamma;
+ bool gamma_auto;
+ int target_prim;
+ int target_trc;
+ int target_peak;
+ int target_contrast;
+ int target_gamut;
+ struct gl_tone_map_opts tone_map;
+ bool correct_downscaling;
+ bool linear_downscaling;
+ bool linear_upscaling;
+ bool sigmoid_upscaling;
+ float sigmoid_center;
+ float sigmoid_slope;
+ bool scaler_resizes_only;
+ bool pbo;
+ int dither_depth;
+ int dither_algo;
+ int dither_size;
+ bool temporal_dither;
+ int temporal_dither_period;
+ char *error_diffusion;
+ char *fbo_format;
+ int alpha_mode;
+ bool use_rectangle;
+ struct m_color background;
+ bool interpolation;
+ float interpolation_threshold;
+ int blend_subs;
+ char **user_shaders;
+ char **user_shader_opts;
+ bool deband;
+ struct deband_opts *deband_opts;
+ float unsharp;
+ int tex_pad_x, tex_pad_y;
+ struct mp_icc_opts *icc_opts;
+ bool shader_cache;
+ int early_flush;
+ char *shader_cache_dir;
+ char *hwdec_interop;
+};
+
+extern const struct m_sub_options gl_video_conf;
+
+struct gl_video;
+struct vo_frame;
+struct voctrl_screenshot;
+
+enum {
+ RENDER_FRAME_SUBS = 1 << 0,
+ RENDER_FRAME_OSD = 1 << 1,
+ RENDER_FRAME_VF_SUBS = 1 << 2,
+ RENDER_SCREEN_COLOR = 1 << 3, // 3D LUT and dithering
+ RENDER_FRAME_DEF = RENDER_FRAME_SUBS | RENDER_FRAME_OSD | RENDER_SCREEN_COLOR,
+};
+
+struct gl_video *gl_video_init(struct ra *ra, struct mp_log *log,
+ struct mpv_global *g);
+void gl_video_uninit(struct gl_video *p);
+void gl_video_set_osd_source(struct gl_video *p, struct osd_state *osd);
+bool gl_video_check_format(struct gl_video *p, int mp_format);
+void gl_video_config(struct gl_video *p, struct mp_image_params *params);
+void gl_video_render_frame(struct gl_video *p, struct vo_frame *frame,
+ struct ra_fbo fbo, int flags);
+void gl_video_resize(struct gl_video *p,
+ struct mp_rect *src, struct mp_rect *dst,
+ struct mp_osd_res *osd);
+void gl_video_set_fb_depth(struct gl_video *p, int fb_depth);
+void gl_video_perfdata(struct gl_video *p, struct voctrl_performance_data *out);
+void gl_video_set_clear_color(struct gl_video *p, struct m_color color);
+void gl_video_set_osd_pts(struct gl_video *p, double pts);
+bool gl_video_check_osd_change(struct gl_video *p, struct mp_osd_res *osd,
+ double pts);
+
+void gl_video_screenshot(struct gl_video *p, struct vo_frame *frame,
+ struct voctrl_screenshot *args);
+
+float gl_video_scale_ambient_lux(float lmin, float lmax,
+ float rmin, float rmax, float lux);
+void gl_video_set_ambient_lux(struct gl_video *p, int lux);
+void gl_video_set_icc_profile(struct gl_video *p, bstr icc_data);
+bool gl_video_icc_auto_enabled(struct gl_video *p);
+bool gl_video_gamma_auto_enabled(struct gl_video *p);
+struct mp_colorspace gl_video_get_output_colorspace(struct gl_video *p);
+
+void gl_video_reset(struct gl_video *p);
+bool gl_video_showing_interpolated_frame(struct gl_video *p);
+
+struct mp_hwdec_devices;
+void gl_video_init_hwdecs(struct gl_video *p, struct ra_ctx *ra_ctx,
+ struct mp_hwdec_devices *devs,
+ bool load_all_by_default);
+struct hwdec_imgfmt_request;
+void gl_video_load_hwdecs_for_img_fmt(struct gl_video *p, struct mp_hwdec_devices *devs,
+ struct hwdec_imgfmt_request *params);
+
+struct vo;
+void gl_video_configure_queue(struct gl_video *p, struct vo *vo);
+
+struct mp_image *gl_video_get_image(struct gl_video *p, int imgfmt, int w, int h,
+ int stride_align, int flags);
+
+
+#endif
diff --git a/video/out/gpu/video_shaders.c b/video/out/gpu/video_shaders.c
new file mode 100644
index 0000000..6c0e8a8
--- /dev/null
+++ b/video/out/gpu/video_shaders.c
@@ -0,0 +1,1033 @@
+/*
+ * 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 <math.h>
+
+#include "video_shaders.h"
+#include "video.h"
+
+#define GLSL(x) gl_sc_add(sc, #x "\n");
+#define GLSLF(...) gl_sc_addf(sc, __VA_ARGS__)
+#define GLSLH(x) gl_sc_hadd(sc, #x "\n");
+#define GLSLHF(...) gl_sc_haddf(sc, __VA_ARGS__)
+
+// Set up shared/commonly used variables and macros
+void sampler_prelude(struct gl_shader_cache *sc, int tex_num)
+{
+ GLSLF("#undef tex\n");
+ GLSLF("#undef texmap\n");
+ GLSLF("#define tex texture%d\n", tex_num);
+ GLSLF("#define texmap texmap%d\n", tex_num);
+ GLSLF("vec2 pos = texcoord%d;\n", tex_num);
+ GLSLF("vec2 size = texture_size%d;\n", tex_num);
+ GLSLF("vec2 pt = pixel_size%d;\n", tex_num);
+}
+
+static void pass_sample_separated_get_weights(struct gl_shader_cache *sc,
+ struct scaler *scaler)
+{
+ gl_sc_uniform_texture(sc, "lut", scaler->lut);
+ GLSLF("float ypos = LUT_POS(fcoord, %d.0);\n", scaler->lut->params.h);
+
+ int N = scaler->kernel->size;
+ int width = (N + 3) / 4; // round up
+
+ GLSLF("float weights[%d];\n", N);
+ for (int i = 0; i < N; i++) {
+ if (i % 4 == 0)
+ GLSLF("c = texture(lut, vec2(%f, ypos));\n", (i / 4 + 0.5) / width);
+ GLSLF("weights[%d] = c[%d];\n", i, i % 4);
+ }
+}
+
+// Handle a single pass (either vertical or horizontal). The direction is given
+// by the vector (d_x, d_y). If the vector is 0, then planar interpolation is
+// used instead (samples from texture0 through textureN)
+void pass_sample_separated_gen(struct gl_shader_cache *sc, struct scaler *scaler,
+ int d_x, int d_y)
+{
+ int N = scaler->kernel->size;
+ bool use_ar = scaler->conf.antiring > 0;
+ bool planar = d_x == 0 && d_y == 0;
+ GLSL(color = vec4(0.0);)
+ GLSLF("{\n");
+ if (!planar) {
+ GLSLF("vec2 dir = vec2(%d.0, %d.0);\n", d_x, d_y);
+ GLSL(pt *= dir;)
+ GLSL(float fcoord = dot(fract(pos * size - vec2(0.5)), dir);)
+ GLSLF("vec2 base = pos - fcoord * pt - pt * vec2(%d.0);\n", N / 2 - 1);
+ }
+ GLSL(vec4 c;)
+ if (use_ar) {
+ GLSL(vec4 hi = vec4(0.0);)
+ GLSL(vec4 lo = vec4(1.0);)
+ }
+ pass_sample_separated_get_weights(sc, scaler);
+ GLSLF("// scaler samples\n");
+ for (int n = 0; n < N; n++) {
+ if (planar) {
+ GLSLF("c = texture(texture%d, texcoord%d);\n", n, n);
+ } else {
+ GLSLF("c = texture(tex, base + pt * vec2(%d.0));\n", n);
+ }
+ GLSLF("color += vec4(weights[%d]) * c;\n", n);
+ if (use_ar && (n == N/2-1 || n == N/2)) {
+ GLSL(lo = min(lo, c);)
+ GLSL(hi = max(hi, c);)
+ }
+ }
+ if (use_ar)
+ GLSLF("color = mix(color, clamp(color, lo, hi), %f);\n",
+ scaler->conf.antiring);
+ GLSLF("}\n");
+}
+
+// Subroutine for computing and adding an individual texel contribution
+// If planar is false, samples directly
+// If planar is true, takes the pixel from inX[idx] where X is the component and
+// `idx` must be defined by the caller
+static void polar_sample(struct gl_shader_cache *sc, struct scaler *scaler,
+ int x, int y, int components, bool planar)
+{
+ double radius = scaler->kernel->radius * scaler->kernel->filter_scale;
+ double radius_cutoff = scaler->kernel->radius_cutoff;
+
+ // Since we can't know the subpixel position in advance, assume a
+ // worst case scenario
+ int yy = y > 0 ? y-1 : y;
+ int xx = x > 0 ? x-1 : x;
+ double dmax = sqrt(xx*xx + yy*yy);
+ // Skip samples definitely outside the radius
+ if (dmax >= radius_cutoff)
+ return;
+ GLSLF("d = length(vec2(%d.0, %d.0) - fcoord);\n", x, y);
+ // Check for samples that might be skippable
+ bool maybe_skippable = dmax >= radius_cutoff - M_SQRT2;
+ if (maybe_skippable)
+ GLSLF("if (d < %f) {\n", radius_cutoff);
+
+ // get the weight for this pixel
+ if (scaler->lut->params.dimensions == 1) {
+ GLSLF("w = tex1D(lut, LUT_POS(d * 1.0/%f, %d.0)).r;\n",
+ radius, scaler->lut->params.w);
+ } else {
+ GLSLF("w = texture(lut, vec2(0.5, LUT_POS(d * 1.0/%f, %d.0))).r;\n",
+ radius, scaler->lut->params.h);
+ }
+ GLSL(wsum += w;)
+
+ if (planar) {
+ for (int n = 0; n < components; n++)
+ GLSLF("color[%d] += w * in%d[idx];\n", n, n);
+ } else {
+ GLSLF("in0 = texture(tex, base + pt * vec2(%d.0, %d.0));\n", x, y);
+ GLSL(color += vec4(w) * in0;)
+ }
+
+ if (maybe_skippable)
+ GLSLF("}\n");
+}
+
+void pass_sample_polar(struct gl_shader_cache *sc, struct scaler *scaler,
+ int components, bool sup_gather)
+{
+ GLSL(color = vec4(0.0);)
+ GLSLF("{\n");
+ GLSL(vec2 fcoord = fract(pos * size - vec2(0.5));)
+ GLSL(vec2 base = pos - fcoord * pt;)
+ GLSLF("float w, d, wsum = 0.0;\n");
+ for (int n = 0; n < components; n++)
+ GLSLF("vec4 in%d;\n", n);
+ GLSL(int idx;)
+
+ gl_sc_uniform_texture(sc, "lut", scaler->lut);
+
+ GLSLF("// scaler samples\n");
+ int bound = ceil(scaler->kernel->radius_cutoff);
+ for (int y = 1-bound; y <= bound; y += 2) {
+ for (int x = 1-bound; x <= bound; x += 2) {
+ // First we figure out whether it's more efficient to use direct
+ // sampling or gathering. The problem is that gathering 4 texels
+ // only to discard some of them is very wasteful, so only do it if
+ // we suspect it will be a win rather than a loss. This is the case
+ // exactly when all four texels are within bounds
+ bool use_gather = sqrt(x*x + y*y) < scaler->kernel->radius_cutoff;
+
+ if (!sup_gather)
+ use_gather = false;
+
+ if (use_gather) {
+ // Gather the four surrounding texels simultaneously
+ for (int n = 0; n < components; n++) {
+ GLSLF("in%d = textureGatherOffset(tex, base, "
+ "ivec2(%d, %d), %d);\n", n, x, y, n);
+ }
+
+ // Mix in all of the points with their weights
+ for (int p = 0; p < 4; p++) {
+ // The four texels are gathered counterclockwise starting
+ // from the bottom left
+ static const int xo[4] = {0, 1, 1, 0};
+ static const int yo[4] = {1, 1, 0, 0};
+ if (x+xo[p] > bound || y+yo[p] > bound)
+ continue;
+ GLSLF("idx = %d;\n", p);
+ polar_sample(sc, scaler, x+xo[p], y+yo[p], components, true);
+ }
+ } else {
+ // switch to direct sampling instead, for efficiency/compatibility
+ for (int yy = y; yy <= bound && yy <= y+1; yy++) {
+ for (int xx = x; xx <= bound && xx <= x+1; xx++)
+ polar_sample(sc, scaler, xx, yy, components, false);
+ }
+ }
+ }
+ }
+
+ GLSL(color = color / vec4(wsum);)
+ GLSLF("}\n");
+}
+
+// bw/bh: block size
+// iw/ih: input size (pre-calculated to fit all required texels)
+void pass_compute_polar(struct gl_shader_cache *sc, struct scaler *scaler,
+ int components, int bw, int bh, int iw, int ih)
+{
+ int bound = ceil(scaler->kernel->radius_cutoff);
+ int offset = bound - 1; // padding top/left
+
+ GLSL(color = vec4(0.0);)
+ GLSLF("{\n");
+ GLSL(vec2 wpos = texmap(gl_WorkGroupID * gl_WorkGroupSize);)
+ GLSL(vec2 wbase = wpos - pt * fract(wpos * size - vec2(0.5));)
+ GLSL(vec2 fcoord = fract(pos * size - vec2(0.5));)
+ GLSL(vec2 base = pos - pt * fcoord;)
+ GLSL(ivec2 rel = ivec2(round((base - wbase) * size));)
+ GLSL(int idx;)
+ GLSLF("float w, d, wsum = 0.0;\n");
+ gl_sc_uniform_texture(sc, "lut", scaler->lut);
+
+ // Load all relevant texels into shmem
+ for (int c = 0; c < components; c++)
+ GLSLHF("shared float in%d[%d];\n", c, ih * iw);
+
+ GLSL(vec4 c;)
+ GLSLF("for (int y = int(gl_LocalInvocationID.y); y < %d; y += %d) {\n", ih, bh);
+ GLSLF("for (int x = int(gl_LocalInvocationID.x); x < %d; x += %d) {\n", iw, bw);
+ GLSLF("c = texture(tex, wbase + pt * vec2(x - %d, y - %d));\n", offset, offset);
+ for (int c = 0; c < components; c++)
+ GLSLF("in%d[%d * y + x] = c[%d];\n", c, iw, c);
+ GLSLF("}}\n");
+ GLSL(groupMemoryBarrier();)
+ GLSL(barrier();)
+
+ // Dispatch the actual samples
+ GLSLF("// scaler samples\n");
+ for (int y = 1-bound; y <= bound; y++) {
+ for (int x = 1-bound; x <= bound; x++) {
+ GLSLF("idx = %d * rel.y + rel.x + %d;\n", iw,
+ iw * (y + offset) + x + offset);
+ polar_sample(sc, scaler, x, y, components, true);
+ }
+ }
+
+ GLSL(color = color / vec4(wsum);)
+ GLSLF("}\n");
+}
+
+static void bicubic_calcweights(struct gl_shader_cache *sc, const char *t, const char *s)
+{
+ // Explanation of how bicubic scaling with only 4 texel fetches is done:
+ // http://www.mate.tue.nl/mate/pdfs/10318.pdf
+ // 'Efficient GPU-Based Texture Interpolation using Uniform B-Splines'
+ // Explanation why this algorithm normally always blurs, even with unit
+ // scaling:
+ // http://bigwww.epfl.ch/preprints/ruijters1001p.pdf
+ // 'GPU Prefilter for Accurate Cubic B-spline Interpolation'
+ GLSLF("vec4 %s = vec4(-0.5, 0.1666, 0.3333, -0.3333) * %s"
+ " + vec4(1, 0, -0.5, 0.5);\n", t, s);
+ GLSLF("%s = %s * %s + vec4(0, 0, -0.5, 0.5);\n", t, t, s);
+ GLSLF("%s = %s * %s + vec4(-0.6666, 0, 0.8333, 0.1666);\n", t, t, s);
+ GLSLF("%s.xy *= vec2(1, 1) / vec2(%s.z, %s.w);\n", t, t, t);
+ GLSLF("%s.xy += vec2(1.0 + %s, 1.0 - %s);\n", t, s, s);
+}
+
+void pass_sample_bicubic_fast(struct gl_shader_cache *sc)
+{
+ GLSLF("{\n");
+ GLSL(vec2 fcoord = fract(pos * size + vec2(0.5, 0.5));)
+ bicubic_calcweights(sc, "parmx", "fcoord.x");
+ bicubic_calcweights(sc, "parmy", "fcoord.y");
+ GLSL(vec4 cdelta;)
+ GLSL(cdelta.xz = parmx.rg * vec2(-pt.x, pt.x);)
+ GLSL(cdelta.yw = parmy.rg * vec2(-pt.y, pt.y);)
+ // first y-interpolation
+ GLSL(vec4 ar = texture(tex, pos + cdelta.xy);)
+ GLSL(vec4 ag = texture(tex, pos + cdelta.xw);)
+ GLSL(vec4 ab = mix(ag, ar, parmy.b);)
+ // second y-interpolation
+ GLSL(vec4 br = texture(tex, pos + cdelta.zy);)
+ GLSL(vec4 bg = texture(tex, pos + cdelta.zw);)
+ GLSL(vec4 aa = mix(bg, br, parmy.b);)
+ // x-interpolation
+ GLSL(color = mix(aa, ab, parmx.b);)
+ GLSLF("}\n");
+}
+
+void pass_sample_oversample(struct gl_shader_cache *sc, struct scaler *scaler,
+ int w, int h)
+{
+ GLSLF("{\n");
+ GLSL(vec2 pos = pos - vec2(0.5) * pt;) // round to nearest
+ GLSL(vec2 fcoord = fract(pos * size - vec2(0.5));)
+ // Determine the mixing coefficient vector
+ gl_sc_uniform_vec2(sc, "output_size", (float[2]){w, h});
+ GLSL(vec2 coeff = fcoord * output_size/size;)
+ float threshold = scaler->conf.kernel.params[0];
+ threshold = isnan(threshold) ? 0.0 : threshold;
+ GLSLF("coeff = (coeff - %f) * 1.0/%f;\n", threshold, 1.0 - 2 * threshold);
+ GLSL(coeff = clamp(coeff, 0.0, 1.0);)
+ // Compute the right blend of colors
+ GLSL(color = texture(tex, pos + pt * (coeff - fcoord));)
+ GLSLF("}\n");
+}
+
+// Common constants for SMPTE ST.2084 (HDR)
+static const float PQ_M1 = 2610./4096 * 1./4,
+ PQ_M2 = 2523./4096 * 128,
+ PQ_C1 = 3424./4096,
+ PQ_C2 = 2413./4096 * 32,
+ PQ_C3 = 2392./4096 * 32;
+
+// Common constants for ARIB STD-B67 (HLG)
+static const float HLG_A = 0.17883277,
+ HLG_B = 0.28466892,
+ HLG_C = 0.55991073;
+
+// Common constants for Panasonic V-Log
+static const float VLOG_B = 0.00873,
+ VLOG_C = 0.241514,
+ VLOG_D = 0.598206;
+
+// Common constants for Sony S-Log
+static const float SLOG_A = 0.432699,
+ SLOG_B = 0.037584,
+ SLOG_C = 0.616596 + 0.03,
+ SLOG_P = 3.538813,
+ SLOG_Q = 0.030001,
+ SLOG_K2 = 155.0 / 219.0;
+
+// Linearize (expand), given a TRC as input. In essence, this is the ITU-R
+// EOTF, calculated on an idealized (reference) monitor with a white point of
+// MP_REF_WHITE and infinite contrast.
+//
+// These functions always output to a normalized scale of [0,1], for
+// convenience of the video.c code that calls it. To get the values in an
+// absolute scale, multiply the result by `mp_trc_nom_peak(trc)`
+void pass_linearize(struct gl_shader_cache *sc, enum mp_csp_trc trc)
+{
+ if (trc == MP_CSP_TRC_LINEAR)
+ return;
+
+ GLSLF("// linearize\n");
+
+ // Note that this clamp may technically violate the definition of
+ // ITU-R BT.2100, which allows for sub-blacks and super-whites to be
+ // displayed on the display where such would be possible. That said, the
+ // problem is that not all gamma curves are well-defined on the values
+ // outside this range, so we ignore it and just clip anyway for sanity.
+ GLSL(color.rgb = clamp(color.rgb, 0.0, 1.0);)
+
+ switch (trc) {
+ case MP_CSP_TRC_SRGB:
+ GLSLF("color.rgb = mix(color.rgb * vec3(1.0/12.92), \n"
+ " pow((color.rgb + vec3(0.055))/vec3(1.055), vec3(2.4)), \n"
+ " %s(lessThan(vec3(0.04045), color.rgb))); \n",
+ gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_BT_1886:
+ GLSL(color.rgb = pow(color.rgb, vec3(2.4));)
+ break;
+ case MP_CSP_TRC_GAMMA18:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.8));)
+ break;
+ case MP_CSP_TRC_GAMMA20:
+ GLSL(color.rgb = pow(color.rgb, vec3(2.0));)
+ break;
+ case MP_CSP_TRC_GAMMA22:
+ GLSL(color.rgb = pow(color.rgb, vec3(2.2));)
+ break;
+ case MP_CSP_TRC_GAMMA24:
+ GLSL(color.rgb = pow(color.rgb, vec3(2.4));)
+ break;
+ case MP_CSP_TRC_GAMMA26:
+ GLSL(color.rgb = pow(color.rgb, vec3(2.6));)
+ break;
+ case MP_CSP_TRC_GAMMA28:
+ GLSL(color.rgb = pow(color.rgb, vec3(2.8));)
+ break;
+ case MP_CSP_TRC_PRO_PHOTO:
+ GLSLF("color.rgb = mix(color.rgb * vec3(1.0/16.0), \n"
+ " pow(color.rgb, vec3(1.8)), \n"
+ " %s(lessThan(vec3(0.03125), color.rgb))); \n",
+ gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_PQ:
+ GLSLF("color.rgb = pow(color.rgb, vec3(1.0/%f));\n", PQ_M2);
+ GLSLF("color.rgb = max(color.rgb - vec3(%f), vec3(0.0)) \n"
+ " / (vec3(%f) - vec3(%f) * color.rgb);\n",
+ PQ_C1, PQ_C2, PQ_C3);
+ GLSLF("color.rgb = pow(color.rgb, vec3(%f));\n", 1.0 / PQ_M1);
+ // PQ's output range is 0-10000, but we need it to be relative to
+ // MP_REF_WHITE instead, so rescale
+ GLSLF("color.rgb *= vec3(%f);\n", 10000 / MP_REF_WHITE);
+ break;
+ case MP_CSP_TRC_HLG:
+ GLSLF("color.rgb = mix(vec3(4.0) * color.rgb * color.rgb,\n"
+ " exp((color.rgb - vec3(%f)) * vec3(1.0/%f)) + vec3(%f),\n"
+ " %s(lessThan(vec3(0.5), color.rgb)));\n",
+ HLG_C, HLG_A, HLG_B, gl_sc_bvec(sc, 3));
+ GLSLF("color.rgb *= vec3(1.0/%f);\n", MP_REF_WHITE_HLG);
+ break;
+ case MP_CSP_TRC_V_LOG:
+ GLSLF("color.rgb = mix((color.rgb - vec3(0.125)) * vec3(1.0/5.6), \n"
+ " pow(vec3(10.0), (color.rgb - vec3(%f)) * vec3(1.0/%f)) \n"
+ " - vec3(%f), \n"
+ " %s(lessThanEqual(vec3(0.181), color.rgb))); \n",
+ VLOG_D, VLOG_C, VLOG_B, gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_S_LOG1:
+ GLSLF("color.rgb = pow(vec3(10.0), (color.rgb - vec3(%f)) * vec3(1.0/%f))\n"
+ " - vec3(%f);\n",
+ SLOG_C, SLOG_A, SLOG_B);
+ break;
+ case MP_CSP_TRC_S_LOG2:
+ GLSLF("color.rgb = mix((color.rgb - vec3(%f)) * vec3(1.0/%f), \n"
+ " (pow(vec3(10.0), (color.rgb - vec3(%f)) * vec3(1.0/%f)) \n"
+ " - vec3(%f)) * vec3(1.0/%f), \n"
+ " %s(lessThanEqual(vec3(%f), color.rgb))); \n",
+ SLOG_Q, SLOG_P, SLOG_C, SLOG_A, SLOG_B, SLOG_K2, gl_sc_bvec(sc, 3), SLOG_Q);
+ break;
+ case MP_CSP_TRC_ST428:
+ GLSL(color.rgb = vec3(52.37/48.0) * pow(color.rgb, vec3(2.6)););
+ break;
+ default:
+ abort();
+ }
+
+ // Rescale to prevent clipping on non-float textures
+ GLSLF("color.rgb *= vec3(1.0/%f);\n", mp_trc_nom_peak(trc));
+}
+
+// Delinearize (compress), given a TRC as output. This corresponds to the
+// inverse EOTF (not the OETF) in ITU-R terminology, again assuming a
+// reference monitor.
+//
+// Like pass_linearize, this functions ingests values on an normalized scale
+void pass_delinearize(struct gl_shader_cache *sc, enum mp_csp_trc trc)
+{
+ if (trc == MP_CSP_TRC_LINEAR)
+ return;
+
+ GLSLF("// delinearize\n");
+ GLSL(color.rgb = clamp(color.rgb, 0.0, 1.0);)
+ GLSLF("color.rgb *= vec3(%f);\n", mp_trc_nom_peak(trc));
+
+ switch (trc) {
+ case MP_CSP_TRC_SRGB:
+ GLSLF("color.rgb = mix(color.rgb * vec3(12.92), \n"
+ " vec3(1.055) * pow(color.rgb, vec3(1.0/2.4)) \n"
+ " - vec3(0.055), \n"
+ " %s(lessThanEqual(vec3(0.0031308), color.rgb))); \n",
+ gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_BT_1886:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.4));)
+ break;
+ case MP_CSP_TRC_GAMMA18:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/1.8));)
+ break;
+ case MP_CSP_TRC_GAMMA20:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.0));)
+ break;
+ case MP_CSP_TRC_GAMMA22:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.2));)
+ break;
+ case MP_CSP_TRC_GAMMA24:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.4));)
+ break;
+ case MP_CSP_TRC_GAMMA26:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.6));)
+ break;
+ case MP_CSP_TRC_GAMMA28:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.8));)
+ break;
+ case MP_CSP_TRC_PRO_PHOTO:
+ GLSLF("color.rgb = mix(color.rgb * vec3(16.0), \n"
+ " pow(color.rgb, vec3(1.0/1.8)), \n"
+ " %s(lessThanEqual(vec3(0.001953), color.rgb))); \n",
+ gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_PQ:
+ GLSLF("color.rgb *= vec3(1.0/%f);\n", 10000 / MP_REF_WHITE);
+ GLSLF("color.rgb = pow(color.rgb, vec3(%f));\n", PQ_M1);
+ GLSLF("color.rgb = (vec3(%f) + vec3(%f) * color.rgb) \n"
+ " / (vec3(1.0) + vec3(%f) * color.rgb);\n",
+ PQ_C1, PQ_C2, PQ_C3);
+ GLSLF("color.rgb = pow(color.rgb, vec3(%f));\n", PQ_M2);
+ break;
+ case MP_CSP_TRC_HLG:
+ GLSLF("color.rgb *= vec3(%f);\n", MP_REF_WHITE_HLG);
+ GLSLF("color.rgb = mix(vec3(0.5) * sqrt(color.rgb),\n"
+ " vec3(%f) * log(color.rgb - vec3(%f)) + vec3(%f),\n"
+ " %s(lessThan(vec3(1.0), color.rgb)));\n",
+ HLG_A, HLG_B, HLG_C, gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_V_LOG:
+ GLSLF("color.rgb = mix(vec3(5.6) * color.rgb + vec3(0.125), \n"
+ " vec3(%f) * log(color.rgb + vec3(%f)) \n"
+ " + vec3(%f), \n"
+ " %s(lessThanEqual(vec3(0.01), color.rgb))); \n",
+ VLOG_C / M_LN10, VLOG_B, VLOG_D, gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_S_LOG1:
+ GLSLF("color.rgb = vec3(%f) * log(color.rgb + vec3(%f)) + vec3(%f);\n",
+ SLOG_A / M_LN10, SLOG_B, SLOG_C);
+ break;
+ case MP_CSP_TRC_S_LOG2:
+ GLSLF("color.rgb = mix(vec3(%f) * color.rgb + vec3(%f), \n"
+ " vec3(%f) * log(vec3(%f) * color.rgb + vec3(%f)) \n"
+ " + vec3(%f), \n"
+ " %s(lessThanEqual(vec3(0.0), color.rgb))); \n",
+ SLOG_P, SLOG_Q, SLOG_A / M_LN10, SLOG_K2, SLOG_B, SLOG_C, gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_TRC_ST428:
+ GLSL(color.rgb = pow(color.rgb * vec3(48.0/52.37), vec3(1.0/2.6)););
+ break;
+ default:
+ abort();
+ }
+}
+
+// Apply the OOTF mapping from a given light type to display-referred light.
+// Assumes absolute scale values. `peak` is used to tune the OOTF where
+// applicable (currently only HLG).
+static void pass_ootf(struct gl_shader_cache *sc, enum mp_csp_light light,
+ float peak)
+{
+ if (light == MP_CSP_LIGHT_DISPLAY)
+ return;
+
+ GLSLF("// apply ootf\n");
+
+ switch (light)
+ {
+ case MP_CSP_LIGHT_SCENE_HLG: {
+ // HLG OOTF from BT.2100, scaled to the chosen display peak
+ float gamma = MPMAX(1.0, 1.2 + 0.42 * log10(peak * MP_REF_WHITE / 1000.0));
+ GLSLF("color.rgb *= vec3(%f * pow(dot(src_luma, color.rgb), %f));\n",
+ peak / pow(12.0 / MP_REF_WHITE_HLG, gamma), gamma - 1.0);
+ break;
+ }
+ case MP_CSP_LIGHT_SCENE_709_1886:
+ // This OOTF is defined by encoding the result as 709 and then decoding
+ // it as 1886; although this is called 709_1886 we actually use the
+ // more precise (by one decimal) values from BT.2020 instead
+ GLSLF("color.rgb = mix(color.rgb * vec3(4.5), \n"
+ " vec3(1.0993) * pow(color.rgb, vec3(0.45)) - vec3(0.0993), \n"
+ " %s(lessThan(vec3(0.0181), color.rgb))); \n",
+ gl_sc_bvec(sc, 3));
+ GLSL(color.rgb = pow(color.rgb, vec3(2.4));)
+ break;
+ case MP_CSP_LIGHT_SCENE_1_2:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.2));)
+ break;
+ default:
+ abort();
+ }
+}
+
+// Inverse of the function pass_ootf, for completeness' sake.
+static void pass_inverse_ootf(struct gl_shader_cache *sc, enum mp_csp_light light,
+ float peak)
+{
+ if (light == MP_CSP_LIGHT_DISPLAY)
+ return;
+
+ GLSLF("// apply inverse ootf\n");
+
+ switch (light)
+ {
+ case MP_CSP_LIGHT_SCENE_HLG: {
+ float gamma = MPMAX(1.0, 1.2 + 0.42 * log10(peak * MP_REF_WHITE / 1000.0));
+ GLSLF("color.rgb *= vec3(1.0/%f);\n", peak / pow(12.0 / MP_REF_WHITE_HLG, gamma));
+ GLSLF("color.rgb /= vec3(max(1e-6, pow(dot(src_luma, color.rgb), %f)));\n",
+ (gamma - 1.0) / gamma);
+ break;
+ }
+ case MP_CSP_LIGHT_SCENE_709_1886:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.4));)
+ GLSLF("color.rgb = mix(color.rgb * vec3(1.0/4.5), \n"
+ " pow((color.rgb + vec3(0.0993)) * vec3(1.0/1.0993), \n"
+ " vec3(1/0.45)), \n"
+ " %s(lessThan(vec3(0.08145), color.rgb))); \n",
+ gl_sc_bvec(sc, 3));
+ break;
+ case MP_CSP_LIGHT_SCENE_1_2:
+ GLSL(color.rgb = pow(color.rgb, vec3(1.0/1.2));)
+ break;
+ default:
+ abort();
+ }
+}
+
+// Average light level for SDR signals. This is equal to a signal level of 0.5
+// under a typical presentation gamma of about 2.0.
+static const float sdr_avg = 0.25;
+
+static void hdr_update_peak(struct gl_shader_cache *sc,
+ const struct gl_tone_map_opts *opts)
+{
+ // Update the sig_peak/sig_avg from the old SSBO state
+ GLSL(if (average.y > 0.0) {)
+ GLSL( sig_avg = max(1e-3, average.x);)
+ GLSL( sig_peak = max(1.00, average.y);)
+ GLSL(})
+
+ // Chosen to avoid overflowing on an 8K buffer
+ const float log_min = 1e-3, log_scale = 400.0, sig_scale = 10000.0;
+
+ // For performance, and to avoid overflows, we tally up the sub-results per
+ // pixel using shared memory first
+ GLSLH(shared int wg_sum;)
+ GLSLH(shared uint wg_max;)
+ GLSL(wg_sum = 0; wg_max = 0u;)
+ GLSL(barrier();)
+ GLSLF("float sig_log = log(max(sig_max, %f));\n", log_min);
+ GLSLF("atomicAdd(wg_sum, int(sig_log * %f));\n", log_scale);
+ GLSLF("atomicMax(wg_max, uint(sig_max * %f));\n", sig_scale);
+
+ // Have one thread per work group update the global atomics
+ GLSL(memoryBarrierShared();)
+ GLSL(barrier();)
+ GLSL(if (gl_LocalInvocationIndex == 0u) {)
+ GLSL( int wg_avg = wg_sum / int(gl_WorkGroupSize.x * gl_WorkGroupSize.y);)
+ GLSL( atomicAdd(frame_sum, wg_avg);)
+ GLSL( atomicMax(frame_max, wg_max);)
+ GLSL( memoryBarrierBuffer();)
+ GLSL(})
+ GLSL(barrier();)
+
+ // Finally, to update the global state, we increment a counter per dispatch
+ GLSL(uint num_wg = gl_NumWorkGroups.x * gl_NumWorkGroups.y;)
+ GLSL(if (gl_LocalInvocationIndex == 0u && atomicAdd(counter, 1u) == num_wg - 1u) {)
+ GLSL( counter = 0u;)
+ GLSL( vec2 cur = vec2(float(frame_sum) / float(num_wg), frame_max);)
+ GLSLF(" cur *= vec2(1.0/%f, 1.0/%f);\n", log_scale, sig_scale);
+ GLSL( cur.x = exp(cur.x);)
+ GLSL( if (average.y == 0.0))
+ GLSL( average = cur;)
+
+ // Use an IIR low-pass filter to smooth out the detected values, with a
+ // configurable decay rate based on the desired time constant (tau)
+ if (opts->decay_rate) {
+ float decay = 1.0f - expf(-1.0f / opts->decay_rate);
+ GLSLF(" average += %f * (cur - average);\n", decay);
+ } else {
+ GLSLF(" average = cur;\n");
+ }
+
+ // Scene change hysteresis
+ float log_db = 10.0 / log(10.0);
+ GLSLF(" float weight = smoothstep(%f, %f, abs(log(cur.x / average.x)));\n",
+ opts->scene_threshold_low / log_db,
+ opts->scene_threshold_high / log_db);
+ GLSL( average = mix(average, cur, weight);)
+
+ // Reset SSBO state for the next frame
+ GLSL( frame_sum = 0; frame_max = 0u;)
+ GLSL( memoryBarrierBuffer();)
+ GLSL(})
+}
+
+static inline float pq_delinearize(float x)
+{
+ x *= MP_REF_WHITE / 10000.0;
+ x = powf(x, PQ_M1);
+ x = (PQ_C1 + PQ_C2 * x) / (1.0 + PQ_C3 * x);
+ x = pow(x, PQ_M2);
+ return x;
+}
+
+// Tone map from a known peak brightness to the range [0,1]. If ref_peak
+// is 0, we will use peak detection instead
+static void pass_tone_map(struct gl_shader_cache *sc,
+ float src_peak, float dst_peak,
+ const struct gl_tone_map_opts *opts)
+{
+ GLSLF("// HDR tone mapping\n");
+
+ // To prevent discoloration due to out-of-bounds clipping, we need to make
+ // sure to reduce the value range as far as necessary to keep the entire
+ // signal in range, so tone map based on the brightest component.
+ GLSL(int sig_idx = 0;)
+ GLSL(if (color[1] > color[sig_idx]) sig_idx = 1;)
+ GLSL(if (color[2] > color[sig_idx]) sig_idx = 2;)
+ GLSL(float sig_max = color[sig_idx];)
+ GLSLF("float sig_peak = %f;\n", src_peak);
+ GLSLF("float sig_avg = %f;\n", sdr_avg);
+
+ if (opts->compute_peak >= 0)
+ hdr_update_peak(sc, opts);
+
+ // Always hard-clip the upper bound of the signal range to avoid functions
+ // exploding on inputs greater than 1.0
+ GLSLF("vec3 sig = min(color.rgb, sig_peak);\n");
+
+ // This function always operates on an absolute scale, so ignore the
+ // dst_peak normalization for it
+ float dst_scale = dst_peak;
+ enum tone_mapping curve = opts->curve ? opts->curve : TONE_MAPPING_BT_2390;
+ if (curve == TONE_MAPPING_BT_2390)
+ dst_scale = 1.0;
+
+ // Rescale the variables in order to bring it into a representation where
+ // 1.0 represents the dst_peak. This is because all of the tone mapping
+ // algorithms are defined in such a way that they map to the range [0.0, 1.0].
+ if (dst_scale > 1.0) {
+ GLSLF("sig *= 1.0/%f;\n", dst_scale);
+ GLSLF("sig_peak *= 1.0/%f;\n", dst_scale);
+ }
+
+ GLSL(float sig_orig = sig[sig_idx];)
+ GLSLF("float slope = min(%f, %f / sig_avg);\n", opts->max_boost, sdr_avg);
+ GLSL(sig *= slope;)
+ GLSL(sig_peak *= slope;)
+
+ float param = opts->curve_param;
+ switch (curve) {
+ case TONE_MAPPING_CLIP:
+ GLSLF("sig = min(%f * sig, 1.0);\n", isnan(param) ? 1.0 : param);
+ break;
+
+ case TONE_MAPPING_MOBIUS:
+ GLSLF("if (sig_peak > (1.0 + 1e-6)) {\n");
+ GLSLF("const float j = %f;\n", isnan(param) ? 0.3 : param);
+ // solve for M(j) = j; M(sig_peak) = 1.0; M'(j) = 1.0
+ // where M(x) = scale * (x+a)/(x+b)
+ GLSLF("float a = -j*j * (sig_peak - 1.0) / (j*j - 2.0*j + sig_peak);\n");
+ GLSLF("float b = (j*j - 2.0*j*sig_peak + sig_peak) / "
+ "max(1e-6, sig_peak - 1.0);\n");
+ GLSLF("float scale = (b*b + 2.0*b*j + j*j) / (b-a);\n");
+ GLSLF("sig = mix(sig, scale * (sig + vec3(a)) / (sig + vec3(b)),"
+ " %s(greaterThan(sig, vec3(j))));\n",
+ gl_sc_bvec(sc, 3));
+ GLSLF("}\n");
+ break;
+
+ case TONE_MAPPING_REINHARD: {
+ float contrast = isnan(param) ? 0.5 : param,
+ offset = (1.0 - contrast) / contrast;
+ GLSLF("sig = sig / (sig + vec3(%f));\n", offset);
+ GLSLF("float scale = (sig_peak + %f) / sig_peak;\n", offset);
+ GLSL(sig *= scale;)
+ break;
+ }
+
+ case TONE_MAPPING_HABLE: {
+ float A = 0.15, B = 0.50, C = 0.10, D = 0.20, E = 0.02, F = 0.30;
+ GLSLHF("vec3 hable(vec3 x) {\n");
+ GLSLHF("return (x * (%f*x + vec3(%f)) + vec3(%f)) / "
+ " (x * (%f*x + vec3(%f)) + vec3(%f)) "
+ " - vec3(%f);\n",
+ A, C*B, D*E,
+ A, B, D*F,
+ E/F);
+ GLSLHF("}\n");
+ GLSLF("sig = hable(max(vec3(0.0), sig)) / hable(vec3(sig_peak)).x;\n");
+ break;
+ }
+
+ case TONE_MAPPING_GAMMA: {
+ float gamma = isnan(param) ? 1.8 : param;
+ GLSLF("const float cutoff = 0.05, gamma = 1.0/%f;\n", gamma);
+ GLSL(float scale = pow(cutoff / sig_peak, gamma.x) / cutoff;)
+ GLSLF("sig = mix(scale * sig,"
+ " pow(sig / sig_peak, vec3(gamma)),"
+ " %s(greaterThan(sig, vec3(cutoff))));\n",
+ gl_sc_bvec(sc, 3));
+ break;
+ }
+
+ case TONE_MAPPING_LINEAR: {
+ float coeff = isnan(param) ? 1.0 : param;
+ GLSLF("sig = min(%f / sig_peak, 1.0) * sig;\n", coeff);
+ break;
+ }
+
+ case TONE_MAPPING_BT_2390:
+ // We first need to encode both sig and sig_peak into PQ space
+ GLSLF("vec4 sig_pq = vec4(sig.rgb, sig_peak); \n"
+ "sig_pq *= vec4(1.0/%f); \n"
+ "sig_pq = pow(sig_pq, vec4(%f)); \n"
+ "sig_pq = (vec4(%f) + vec4(%f) * sig_pq) \n"
+ " / (vec4(1.0) + vec4(%f) * sig_pq); \n"
+ "sig_pq = pow(sig_pq, vec4(%f)); \n",
+ 10000.0 / MP_REF_WHITE, PQ_M1, PQ_C1, PQ_C2, PQ_C3, PQ_M2);
+ // Encode both the signal and the target brightness to be relative to
+ // the source peak brightness, and figure out the target peak in this space
+ GLSLF("float scale = 1.0 / sig_pq.a; \n"
+ "sig_pq.rgb *= vec3(scale); \n"
+ "float maxLum = %f * scale; \n",
+ pq_delinearize(dst_peak));
+ // Apply piece-wise hermite spline
+ GLSLF("float ks = 1.5 * maxLum - 0.5; \n"
+ "vec3 tb = (sig_pq.rgb - vec3(ks)) / vec3(1.0 - ks); \n"
+ "vec3 tb2 = tb * tb; \n"
+ "vec3 tb3 = tb2 * tb; \n"
+ "vec3 pb = (2.0 * tb3 - 3.0 * tb2 + vec3(1.0)) * vec3(ks) + \n"
+ " (tb3 - 2.0 * tb2 + tb) * vec3(1.0 - ks) + \n"
+ " (-2.0 * tb3 + 3.0 * tb2) * vec3(maxLum); \n"
+ "sig = mix(pb, sig_pq.rgb, %s(lessThan(sig_pq.rgb, vec3(ks)))); \n",
+ gl_sc_bvec(sc, 3));
+ // Convert back from PQ space to linear light
+ GLSLF("sig *= vec3(sig_pq.a); \n"
+ "sig = pow(sig, vec3(1.0/%f)); \n"
+ "sig = max(sig - vec3(%f), 0.0) / \n"
+ " (vec3(%f) - vec3(%f) * sig); \n"
+ "sig = pow(sig, vec3(1.0/%f)); \n"
+ "sig *= vec3(%f); \n",
+ PQ_M2, PQ_C1, PQ_C2, PQ_C3, PQ_M1, 10000.0 / MP_REF_WHITE);
+ break;
+
+ default:
+ abort();
+ }
+
+ GLSLF("float coeff = max(sig[sig_idx] - %f, 1e-6) / \n"
+ " max(sig[sig_idx], 1.0); \n"
+ "coeff = %f * pow(coeff / %f, %f); \n"
+ "color.rgb *= sig[sig_idx] / sig_orig; \n"
+ "color.rgb = mix(color.rgb, %f * sig, coeff); \n",
+ 0.18 / dst_scale, 0.90, dst_scale, 0.20, dst_scale);
+}
+
+// Map colors from one source space to another. These source spaces must be
+// known (i.e. not MP_CSP_*_AUTO), as this function won't perform any
+// auto-guessing. If is_linear is true, we assume the input has already been
+// linearized (e.g. for linear-scaling). If `opts->compute_peak` is true, we
+// will detect the peak instead of relying on metadata. Note that this requires
+// the caller to have already bound the appropriate SSBO and set up the compute
+// shader metadata
+void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
+ struct mp_colorspace src, struct mp_colorspace dst,
+ const struct gl_tone_map_opts *opts)
+{
+ GLSLF("// color mapping\n");
+
+ // Some operations need access to the video's luma coefficients, so make
+ // them available
+ float rgb2xyz[3][3];
+ mp_get_rgb2xyz_matrix(mp_get_csp_primaries(src.primaries), rgb2xyz);
+ gl_sc_uniform_vec3(sc, "src_luma", rgb2xyz[1]);
+ mp_get_rgb2xyz_matrix(mp_get_csp_primaries(dst.primaries), rgb2xyz);
+ gl_sc_uniform_vec3(sc, "dst_luma", rgb2xyz[1]);
+
+ bool need_ootf = src.light != dst.light;
+ if (src.light == MP_CSP_LIGHT_SCENE_HLG && src.hdr.max_luma != dst.hdr.max_luma)
+ need_ootf = true;
+
+ // All operations from here on require linear light as a starting point,
+ // so we linearize even if src.gamma == dst.gamma when one of the other
+ // operations needs it
+ bool need_linear = src.gamma != dst.gamma ||
+ src.primaries != dst.primaries ||
+ src.hdr.max_luma != dst.hdr.max_luma ||
+ need_ootf;
+
+ if (need_linear && !is_linear) {
+ // We also pull it up so that 1.0 is the reference white
+ pass_linearize(sc, src.gamma);
+ is_linear = true;
+ }
+
+ // Pre-scale the incoming values into an absolute scale
+ GLSLF("color.rgb *= vec3(%f);\n", mp_trc_nom_peak(src.gamma));
+
+ if (need_ootf)
+ pass_ootf(sc, src.light, src.hdr.max_luma / MP_REF_WHITE);
+
+ // Tone map to prevent clipping due to excessive brightness
+ if (src.hdr.max_luma > dst.hdr.max_luma) {
+ pass_tone_map(sc, src.hdr.max_luma / MP_REF_WHITE,
+ dst.hdr.max_luma / MP_REF_WHITE, opts);
+ }
+
+ // Adapt to the right colorspace if necessary
+ if (src.primaries != dst.primaries) {
+ struct mp_csp_primaries csp_src = mp_get_csp_primaries(src.primaries),
+ csp_dst = mp_get_csp_primaries(dst.primaries);
+ float m[3][3] = {{0}};
+ mp_get_cms_matrix(csp_src, csp_dst, MP_INTENT_RELATIVE_COLORIMETRIC, m);
+ gl_sc_uniform_mat3(sc, "cms_matrix", true, &m[0][0]);
+ GLSL(color.rgb = cms_matrix * color.rgb;)
+
+ if (!opts->gamut_mode || opts->gamut_mode == GAMUT_DESATURATE) {
+ GLSL(float cmin = min(min(color.r, color.g), color.b);)
+ GLSL(if (cmin < 0.0) {
+ float luma = dot(dst_luma, color.rgb);
+ float coeff = cmin / (cmin - luma);
+ color.rgb = mix(color.rgb, vec3(luma), coeff);
+ })
+ GLSLF("float cmax = 1.0/%f * max(max(color.r, color.g), color.b);\n",
+ dst.hdr.max_luma / MP_REF_WHITE);
+ GLSL(if (cmax > 1.0) color.rgb /= cmax;)
+ }
+ }
+
+ if (need_ootf)
+ pass_inverse_ootf(sc, dst.light, dst.hdr.max_luma / MP_REF_WHITE);
+
+ // Post-scale the outgoing values from absolute scale to normalized.
+ // For SDR, we normalize to the chosen signal peak. For HDR, we normalize
+ // to the encoding range of the transfer function.
+ float dst_range = dst.hdr.max_luma / MP_REF_WHITE;
+ if (mp_trc_is_hdr(dst.gamma))
+ dst_range = mp_trc_nom_peak(dst.gamma);
+
+ GLSLF("color.rgb *= vec3(%f);\n", 1.0 / dst_range);
+
+ // Warn for remaining out-of-gamut colors if enabled
+ if (opts->gamut_mode == GAMUT_WARN) {
+ GLSL(if (any(greaterThan(color.rgb, vec3(1.005))) ||
+ any(lessThan(color.rgb, vec3(-0.005)))))
+ GLSL(color.rgb = vec3(1.0) - color.rgb;) // invert
+ }
+
+ if (is_linear)
+ pass_delinearize(sc, dst.gamma);
+}
+
+// Wide usage friendly PRNG, shamelessly stolen from a GLSL tricks forum post.
+// Obtain random numbers by calling rand(h), followed by h = permute(h) to
+// update the state. Assumes the texture was hooked.
+// permute() was modified from the original to avoid "large" numbers in
+// calculations, since low-end mobile GPUs choke on them (overflow).
+static void prng_init(struct gl_shader_cache *sc, AVLFG *lfg)
+{
+ GLSLH(float mod289(float x) { return x - floor(x * 1.0/289.0) * 289.0; })
+ GLSLHF("float permute(float x) {\n");
+ GLSLH(return mod289( mod289(34.0*x + 1.0) * (fract(x) + 1.0) );)
+ GLSLHF("}\n");
+ GLSLH(float rand(float x) { return fract(x * 1.0/41.0); })
+
+ // Initialize the PRNG by hashing the position + a random uniform
+ GLSL(vec3 _m = vec3(HOOKED_pos, random) + vec3(1.0);)
+ GLSL(float h = permute(permute(permute(_m.x)+_m.y)+_m.z);)
+ gl_sc_uniform_dynamic(sc);
+ gl_sc_uniform_f(sc, "random", (double)av_lfg_get(lfg) / UINT32_MAX);
+}
+
+const struct deband_opts deband_opts_def = {
+ .iterations = 1,
+ .threshold = 48.0,
+ .range = 16.0,
+ .grain = 32.0,
+};
+
+#define OPT_BASE_STRUCT struct deband_opts
+const struct m_sub_options deband_conf = {
+ .opts = (const m_option_t[]) {
+ {"iterations", OPT_INT(iterations), M_RANGE(0, 16)},
+ {"threshold", OPT_FLOAT(threshold), M_RANGE(0.0, 4096.0)},
+ {"range", OPT_FLOAT(range), M_RANGE(1.0, 64.0)},
+ {"grain", OPT_FLOAT(grain), M_RANGE(0.0, 4096.0)},
+ {0}
+ },
+ .size = sizeof(struct deband_opts),
+ .defaults = &deband_opts_def,
+};
+
+// Stochastically sample a debanded result from a hooked texture.
+void pass_sample_deband(struct gl_shader_cache *sc, struct deband_opts *opts,
+ AVLFG *lfg, enum mp_csp_trc trc)
+{
+ // Initialize the PRNG
+ GLSLF("{\n");
+ prng_init(sc, lfg);
+
+ // Helper: Compute a stochastic approximation of the avg color around a
+ // pixel
+ GLSLHF("vec4 average(float range, inout float h) {\n");
+ // Compute a random rangle and distance
+ GLSLH(float dist = rand(h) * range; h = permute(h);)
+ GLSLH(float dir = rand(h) * 6.2831853; h = permute(h);)
+ GLSLH(vec2 o = dist * vec2(cos(dir), sin(dir));)
+
+ // Sample at quarter-turn intervals around the source pixel
+ GLSLH(vec4 ref[4];)
+ GLSLH(ref[0] = HOOKED_texOff(vec2( o.x, o.y));)
+ GLSLH(ref[1] = HOOKED_texOff(vec2(-o.y, o.x));)
+ GLSLH(ref[2] = HOOKED_texOff(vec2(-o.x, -o.y));)
+ GLSLH(ref[3] = HOOKED_texOff(vec2( o.y, -o.x));)
+
+ // Return the (normalized) average
+ GLSLH(return (ref[0] + ref[1] + ref[2] + ref[3])*0.25;)
+ GLSLHF("}\n");
+
+ // Sample the source pixel
+ GLSL(color = HOOKED_tex(HOOKED_pos);)
+ GLSLF("vec4 avg, diff;\n");
+ for (int i = 1; i <= opts->iterations; i++) {
+ // Sample the average pixel and use it instead of the original if
+ // the difference is below the given threshold
+ GLSLF("avg = average(%f, h);\n", i * opts->range);
+ GLSL(diff = abs(color - avg);)
+ GLSLF("color = mix(avg, color, %s(greaterThan(diff, vec4(%f))));\n",
+ gl_sc_bvec(sc, 4), opts->threshold / (i * 16384.0));
+ }
+
+ // Add some random noise to smooth out residual differences
+ GLSL(vec3 noise;)
+ GLSL(noise.x = rand(h); h = permute(h);)
+ GLSL(noise.y = rand(h); h = permute(h);)
+ GLSL(noise.z = rand(h); h = permute(h);)
+
+ // Noise is scaled to the signal level to prevent extreme noise for HDR
+ float gain = opts->grain/8192.0 / mp_trc_nom_peak(trc);
+ GLSLF("color.xyz += %f * (noise - vec3(0.5));\n", gain);
+ GLSLF("}\n");
+}
+
+// Assumes the texture was hooked
+void pass_sample_unsharp(struct gl_shader_cache *sc, float param) {
+ GLSLF("{\n");
+ GLSL(float st1 = 1.2;)
+ GLSL(vec4 p = HOOKED_tex(HOOKED_pos);)
+ GLSL(vec4 sum1 = HOOKED_texOff(st1 * vec2(+1, +1))
+ + HOOKED_texOff(st1 * vec2(+1, -1))
+ + HOOKED_texOff(st1 * vec2(-1, +1))
+ + HOOKED_texOff(st1 * vec2(-1, -1));)
+ GLSL(float st2 = 1.5;)
+ GLSL(vec4 sum2 = HOOKED_texOff(st2 * vec2(+1, 0))
+ + HOOKED_texOff(st2 * vec2( 0, +1))
+ + HOOKED_texOff(st2 * vec2(-1, 0))
+ + HOOKED_texOff(st2 * vec2( 0, -1));)
+ GLSL(vec4 t = p * 0.859375 + sum2 * -0.1171875 + sum1 * -0.09765625;)
+ GLSLF("color = p + t * %f;\n", param);
+ GLSLF("}\n");
+}
diff --git a/video/out/gpu/video_shaders.h b/video/out/gpu/video_shaders.h
new file mode 100644
index 0000000..27e7874
--- /dev/null
+++ b/video/out/gpu/video_shaders.h
@@ -0,0 +1,59 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MP_GL_VIDEO_SHADERS_H
+#define MP_GL_VIDEO_SHADERS_H
+
+#include <libavutil/lfg.h>
+
+#include "utils.h"
+#include "video.h"
+
+struct deband_opts {
+ int iterations;
+ float threshold;
+ float range;
+ float grain;
+};
+
+extern const struct deband_opts deband_opts_def;
+extern const struct m_sub_options deband_conf;
+
+void sampler_prelude(struct gl_shader_cache *sc, int tex_num);
+void pass_sample_separated_gen(struct gl_shader_cache *sc, struct scaler *scaler,
+ int d_x, int d_y);
+void pass_sample_polar(struct gl_shader_cache *sc, struct scaler *scaler,
+ int components, bool sup_gather);
+void pass_compute_polar(struct gl_shader_cache *sc, struct scaler *scaler,
+ int components, int bw, int bh, int iw, int ih);
+void pass_sample_bicubic_fast(struct gl_shader_cache *sc);
+void pass_sample_oversample(struct gl_shader_cache *sc, struct scaler *scaler,
+ int w, int h);
+
+void pass_linearize(struct gl_shader_cache *sc, enum mp_csp_trc trc);
+void pass_delinearize(struct gl_shader_cache *sc, enum mp_csp_trc trc);
+
+void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
+ struct mp_colorspace src, struct mp_colorspace dst,
+ const struct gl_tone_map_opts *opts);
+
+void pass_sample_deband(struct gl_shader_cache *sc, struct deband_opts *opts,
+ AVLFG *lfg, enum mp_csp_trc trc);
+
+void pass_sample_unsharp(struct gl_shader_cache *sc, float param);
+
+#endif
diff --git a/video/out/gpu_next/context.c b/video/out/gpu_next/context.c
new file mode 100644
index 0000000..2887cff
--- /dev/null
+++ b/video/out/gpu_next/context.c
@@ -0,0 +1,240 @@
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+#include <libplacebo/config.h>
+
+#ifdef PL_HAVE_D3D11
+#include <libplacebo/d3d11.h>
+#endif
+
+#ifdef PL_HAVE_OPENGL
+#include <libplacebo/opengl.h>
+#endif
+
+#include "context.h"
+#include "config.h"
+#include "common/common.h"
+#include "options/m_config.h"
+#include "video/out/placebo/utils.h"
+#include "video/out/gpu/video.h"
+
+#if HAVE_D3D11
+#include "osdep/windows_utils.h"
+#include "video/out/d3d11/ra_d3d11.h"
+#include "video/out/d3d11/context.h"
+#endif
+
+#if HAVE_GL
+#include "video/out/opengl/context.h"
+#include "video/out/opengl/ra_gl.h"
+# if HAVE_EGL
+#include <EGL/egl.h>
+# endif
+#endif
+
+#if HAVE_VULKAN
+#include "video/out/vulkan/context.h"
+#endif
+
+#if HAVE_D3D11
+static bool d3d11_pl_init(struct vo *vo, struct gpu_ctx *ctx,
+ struct ra_ctx_opts *ctx_opts)
+{
+#if !defined(PL_HAVE_D3D11)
+ MP_MSG(ctx, vo->probing ? MSGL_V : MSGL_ERR,
+ "libplacebo was built without D3D11 support.\n");
+ return false;
+#else // defined(PL_HAVE_D3D11)
+ bool success = false;
+
+ ID3D11Device *device = ra_d3d11_get_device(ctx->ra_ctx->ra);
+ IDXGISwapChain *swapchain = ra_d3d11_ctx_get_swapchain(ctx->ra_ctx);
+ if (!device || !swapchain) {
+ mp_err(ctx->log,
+ "Failed to receive required components from the mpv d3d11 "
+ "context! (device: %s, swap chain: %s)\n",
+ device ? "OK" : "failed",
+ swapchain ? "OK" : "failed");
+ goto err_out;
+ }
+
+ pl_d3d11 d3d11 = pl_d3d11_create(ctx->pllog,
+ pl_d3d11_params(
+ .device = device,
+ )
+ );
+ if (!d3d11) {
+ mp_err(ctx->log, "Failed to acquire a d3d11 libplacebo context!\n");
+ goto err_out;
+ }
+ ctx->gpu = d3d11->gpu;
+
+ mppl_log_set_probing(ctx->pllog, false);
+
+ ctx->swapchain = pl_d3d11_create_swapchain(d3d11,
+ pl_d3d11_swapchain_params(
+ .swapchain = swapchain,
+ )
+ );
+ if (!ctx->swapchain) {
+ mp_err(ctx->log, "Failed to acquire a d3d11 libplacebo swap chain!\n");
+ goto err_out;
+ }
+
+ success = true;
+
+err_out:
+ SAFE_RELEASE(swapchain);
+ SAFE_RELEASE(device);
+
+ return success;
+#endif // defined(PL_HAVE_D3D11)
+}
+#endif // HAVE_D3D11
+
+struct gpu_ctx *gpu_ctx_create(struct vo *vo, struct gl_video_opts *gl_opts)
+{
+ struct gpu_ctx *ctx = talloc_zero(NULL, struct gpu_ctx);
+ ctx->log = vo->log;
+
+ struct ra_ctx_opts *ctx_opts = mp_get_config_group(ctx, vo->global, &ra_ctx_conf);
+ ctx_opts->want_alpha = gl_opts->alpha_mode == ALPHA_YES;
+ ctx->ra_ctx = ra_ctx_create(vo, *ctx_opts);
+ if (!ctx->ra_ctx)
+ goto err_out;
+
+#if HAVE_VULKAN
+ struct mpvk_ctx *vkctx = ra_vk_ctx_get(ctx->ra_ctx);
+ if (vkctx) {
+ ctx->pllog = vkctx->pllog;
+ ctx->gpu = vkctx->gpu;
+ ctx->swapchain = vkctx->swapchain;
+ return ctx;
+ }
+#endif
+
+ ctx->pllog = mppl_log_create(ctx, ctx->log);
+ if (!ctx->pllog)
+ goto err_out;
+
+ mppl_log_set_probing(ctx->pllog, vo->probing);
+
+#if HAVE_D3D11
+ if (ra_is_d3d11(ctx->ra_ctx->ra)) {
+ if (!d3d11_pl_init(vo, ctx, ctx_opts))
+ goto err_out;
+
+ return ctx;
+ }
+#endif
+
+#if HAVE_GL && defined(PL_HAVE_OPENGL)
+ if (ra_is_gl(ctx->ra_ctx->ra)) {
+ struct GL *gl = ra_gl_get(ctx->ra_ctx->ra);
+ pl_opengl opengl = pl_opengl_create(ctx->pllog,
+ pl_opengl_params(
+ .debug = ctx_opts->debug,
+ .allow_software = ctx_opts->allow_sw,
+ .get_proc_addr_ex = (void *) gl->get_fn,
+ .proc_ctx = gl->fn_ctx,
+# if HAVE_EGL
+ .egl_display = eglGetCurrentDisplay(),
+ .egl_context = eglGetCurrentContext(),
+# endif
+ )
+ );
+ if (!opengl)
+ goto err_out;
+ ctx->gpu = opengl->gpu;
+
+ mppl_log_set_probing(ctx->pllog, false);
+
+ ctx->swapchain = pl_opengl_create_swapchain(opengl, pl_opengl_swapchain_params(
+ .max_swapchain_depth = vo->opts->swapchain_depth,
+ .framebuffer.flipped = gl->flipped,
+ ));
+ if (!ctx->swapchain)
+ goto err_out;
+
+ return ctx;
+ }
+#elif HAVE_GL
+ if (ra_is_gl(ctx->ra_ctx->ra)) {
+ MP_MSG(ctx, vo->probing ? MSGL_V : MSGL_ERR,
+ "libplacebo was built without OpenGL support.\n");
+ }
+#endif
+
+err_out:
+ gpu_ctx_destroy(&ctx);
+ return NULL;
+}
+
+bool gpu_ctx_resize(struct gpu_ctx *ctx, int w, int h)
+{
+#if HAVE_VULKAN
+ if (ra_vk_ctx_get(ctx->ra_ctx))
+ // vulkan RA handles this by itself
+ return true;
+#endif
+
+ return pl_swapchain_resize(ctx->swapchain, &w, &h);
+}
+
+void gpu_ctx_destroy(struct gpu_ctx **ctxp)
+{
+ struct gpu_ctx *ctx = *ctxp;
+ if (!ctx)
+ return;
+ if (!ctx->ra_ctx)
+ goto skip_common_pl_cleanup;
+
+#if HAVE_VULKAN
+ if (ra_vk_ctx_get(ctx->ra_ctx))
+ // vulkan RA context handles pl cleanup by itself,
+ // skip common local clean-up.
+ goto skip_common_pl_cleanup;
+#endif
+
+ if (ctx->swapchain)
+ pl_swapchain_destroy(&ctx->swapchain);
+
+ if (ctx->gpu) {
+#if HAVE_GL && defined(PL_HAVE_OPENGL)
+ if (ra_is_gl(ctx->ra_ctx->ra)) {
+ pl_opengl opengl = pl_opengl_get(ctx->gpu);
+ pl_opengl_destroy(&opengl);
+ }
+#endif
+
+#if HAVE_D3D11 && defined(PL_HAVE_D3D11)
+ if (ra_is_d3d11(ctx->ra_ctx->ra)) {
+ pl_d3d11 d3d11 = pl_d3d11_get(ctx->gpu);
+ pl_d3d11_destroy(&d3d11);
+ }
+#endif
+ }
+
+ if (ctx->pllog)
+ pl_log_destroy(&ctx->pllog);
+
+skip_common_pl_cleanup:
+ ra_ctx_destroy(&ctx->ra_ctx);
+
+ talloc_free(ctx);
+ *ctxp = NULL;
+}
diff --git a/video/out/gpu_next/context.h b/video/out/gpu_next/context.h
new file mode 100644
index 0000000..b98b9e7
--- /dev/null
+++ b/video/out/gpu_next/context.h
@@ -0,0 +1,40 @@
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <libplacebo/renderer.h>
+
+struct mp_log;
+struct ra_ctx;
+struct vo;
+struct gl_video_opts;
+
+struct gpu_ctx {
+ struct mp_log *log;
+ struct ra_ctx *ra_ctx;
+
+ pl_log pllog;
+ pl_gpu gpu;
+ pl_swapchain swapchain;
+
+ void *priv;
+};
+
+struct gpu_ctx *gpu_ctx_create(struct vo *vo, struct gl_video_opts *gl_opts);
+bool gpu_ctx_resize(struct gpu_ctx *ctx, int w, int h);
+void gpu_ctx_destroy(struct gpu_ctx **ctxp);
diff --git a/video/out/hwdec/dmabuf_interop.h b/video/out/hwdec/dmabuf_interop.h
new file mode 100644
index 0000000..e9b3e8e
--- /dev/null
+++ b/video/out/hwdec/dmabuf_interop.h
@@ -0,0 +1,57 @@
+/*
+ * 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/>.
+ */
+
+#pragma once
+
+#include <libavutil/hwcontext_drm.h>
+
+#include "video/out/gpu/hwdec.h"
+
+struct dmabuf_interop {
+ bool use_modifiers;
+ bool composed_layers;
+
+ bool (*interop_init)(struct ra_hwdec_mapper *mapper,
+ const struct ra_imgfmt_desc *desc);
+ void (*interop_uninit)(const struct ra_hwdec_mapper *mapper);
+
+ bool (*interop_map)(struct ra_hwdec_mapper *mapper,
+ struct dmabuf_interop *dmabuf_interop,
+ bool probing);
+ void (*interop_unmap)(struct ra_hwdec_mapper *mapper);
+};
+
+struct dmabuf_interop_priv {
+ int num_planes;
+ struct mp_image layout;
+ struct ra_tex *tex[4];
+
+ AVDRMFrameDescriptor desc;
+ bool surface_acquired;
+
+ void *interop_mapper_priv;
+};
+
+typedef bool (*dmabuf_interop_init)(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop);
+
+bool dmabuf_interop_gl_init(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop);
+bool dmabuf_interop_pl_init(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop);
+bool dmabuf_interop_wl_init(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop);
diff --git a/video/out/hwdec/dmabuf_interop_gl.c b/video/out/hwdec/dmabuf_interop_gl.c
new file mode 100644
index 0000000..e7fb103
--- /dev/null
+++ b/video/out/hwdec/dmabuf_interop_gl.c
@@ -0,0 +1,311 @@
+/*
+ * 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 "dmabuf_interop.h"
+
+#include <drm_fourcc.h>
+#include <EGL/egl.h>
+#include "video/out/opengl/ra_gl.h"
+
+typedef void* GLeglImageOES;
+typedef void *EGLImageKHR;
+
+// Any EGL_EXT_image_dma_buf_import definitions used in this source file.
+#define EGL_LINUX_DMA_BUF_EXT 0x3270
+#define EGL_LINUX_DRM_FOURCC_EXT 0x3271
+#define EGL_DMA_BUF_PLANE0_FD_EXT 0x3272
+#define EGL_DMA_BUF_PLANE0_OFFSET_EXT 0x3273
+#define EGL_DMA_BUF_PLANE0_PITCH_EXT 0x3274
+#define EGL_DMA_BUF_PLANE1_FD_EXT 0x3275
+#define EGL_DMA_BUF_PLANE1_OFFSET_EXT 0x3276
+#define EGL_DMA_BUF_PLANE1_PITCH_EXT 0x3277
+#define EGL_DMA_BUF_PLANE2_FD_EXT 0x3278
+#define EGL_DMA_BUF_PLANE2_OFFSET_EXT 0x3279
+#define EGL_DMA_BUF_PLANE2_PITCH_EXT 0x327A
+
+
+// Any EGL_EXT_image_dma_buf_import definitions used in this source file.
+#define EGL_DMA_BUF_PLANE3_FD_EXT 0x3440
+#define EGL_DMA_BUF_PLANE3_OFFSET_EXT 0x3441
+#define EGL_DMA_BUF_PLANE3_PITCH_EXT 0x3442
+#define EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT 0x3443
+#define EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT 0x3444
+#define EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT 0x3445
+#define EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT 0x3446
+#define EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT 0x3447
+#define EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT 0x3448
+#define EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT 0x3449
+#define EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT 0x344A
+
+struct vaapi_gl_mapper_priv {
+ GLuint gl_textures[4];
+ EGLImageKHR images[4];
+
+ EGLImageKHR (EGLAPIENTRY *CreateImageKHR)(EGLDisplay, EGLContext,
+ EGLenum, EGLClientBuffer,
+ const EGLint *);
+ EGLBoolean (EGLAPIENTRY *DestroyImageKHR)(EGLDisplay, EGLImageKHR);
+ void (EGLAPIENTRY *EGLImageTargetTexture2DOES)(GLenum, GLeglImageOES);
+};
+
+static bool vaapi_gl_mapper_init(struct ra_hwdec_mapper *mapper,
+ const struct ra_imgfmt_desc *desc)
+{
+ struct dmabuf_interop_priv *p_mapper = mapper->priv;
+ struct vaapi_gl_mapper_priv *p = talloc_ptrtype(NULL, p);
+ p_mapper->interop_mapper_priv = p;
+
+ *p = (struct vaapi_gl_mapper_priv) {
+ // EGL_KHR_image_base
+ .CreateImageKHR = (void *)eglGetProcAddress("eglCreateImageKHR"),
+ .DestroyImageKHR = (void *)eglGetProcAddress("eglDestroyImageKHR"),
+ // GL_OES_EGL_image
+ .EGLImageTargetTexture2DOES =
+ (void *)eglGetProcAddress("glEGLImageTargetTexture2DOES"),
+ };
+
+ if (!p->CreateImageKHR || !p->DestroyImageKHR ||
+ !p->EGLImageTargetTexture2DOES)
+ return false;
+
+ GL *gl = ra_gl_get(mapper->ra);
+ gl->GenTextures(4, p->gl_textures);
+ for (int n = 0; n < desc->num_planes; n++) {
+ gl->BindTexture(GL_TEXTURE_2D, p->gl_textures[n]);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = mp_image_plane_w(&p_mapper->layout, n),
+ .h = mp_image_plane_h(&p_mapper->layout, n),
+ .d = 1,
+ .format = desc->planes[n],
+ .render_src = true,
+ .src_linear = true,
+ };
+
+ if (params.format->ctype != RA_CTYPE_UNORM)
+ return false;
+
+ p_mapper->tex[n] = ra_create_wrapped_tex(mapper->ra, &params,
+ p->gl_textures[n]);
+ if (!p_mapper->tex[n])
+ return false;
+ }
+
+ return true;
+}
+
+static void vaapi_gl_mapper_uninit(const struct ra_hwdec_mapper *mapper)
+{
+ struct dmabuf_interop_priv *p_mapper = mapper->priv;
+ struct vaapi_gl_mapper_priv *p = p_mapper->interop_mapper_priv;
+
+ if (p) {
+ GL *gl = ra_gl_get(mapper->ra);
+ gl->DeleteTextures(4, p->gl_textures);
+ for (int n = 0; n < 4; n++) {
+ p->gl_textures[n] = 0;
+ ra_tex_free(mapper->ra, &p_mapper->tex[n]);
+ }
+ talloc_free(p);
+ p_mapper->interop_mapper_priv = NULL;
+ }
+}
+
+#define ADD_ATTRIB(name, value) \
+ do { \
+ assert(num_attribs + 3 < MP_ARRAY_SIZE(attribs)); \
+ attribs[num_attribs++] = (name); \
+ attribs[num_attribs++] = (value); \
+ attribs[num_attribs] = EGL_NONE; \
+ } while(0)
+
+#define ADD_PLANE_ATTRIBS(plane) do { \
+ uint64_t drm_format_modifier = p_mapper->desc.objects[p_mapper->desc.layers[i].planes[j].object_index].format_modifier; \
+ ADD_ATTRIB(EGL_DMA_BUF_PLANE ## plane ## _FD_EXT, \
+ p_mapper->desc.objects[p_mapper->desc.layers[i].planes[j].object_index].fd); \
+ ADD_ATTRIB(EGL_DMA_BUF_PLANE ## plane ## _OFFSET_EXT, \
+ p_mapper->desc.layers[i].planes[j].offset); \
+ ADD_ATTRIB(EGL_DMA_BUF_PLANE ## plane ## _PITCH_EXT, \
+ p_mapper->desc.layers[i].planes[j].pitch); \
+ if (dmabuf_interop->use_modifiers && drm_format_modifier != DRM_FORMAT_MOD_INVALID) { \
+ ADD_ATTRIB(EGL_DMA_BUF_PLANE ## plane ## _MODIFIER_LO_EXT, drm_format_modifier & 0xfffffffful); \
+ ADD_ATTRIB(EGL_DMA_BUF_PLANE ## plane ## _MODIFIER_HI_EXT, drm_format_modifier >> 32); \
+ } \
+ } while (0)
+
+static bool vaapi_gl_map(struct ra_hwdec_mapper *mapper,
+ struct dmabuf_interop *dmabuf_interop,
+ bool probing)
+{
+ struct dmabuf_interop_priv *p_mapper = mapper->priv;
+ struct vaapi_gl_mapper_priv *p = p_mapper->interop_mapper_priv;
+
+ GL *gl = ra_gl_get(mapper->ra);
+
+ for (int i = 0, n = 0; i < p_mapper->desc.nb_layers; i++) {
+ /*
+ * As we must map surfaces as one texture per plane, we can only support
+ * a subset of possible multi-plane layer formats. This is due to having
+ * to manually establish what DRM format each synthetic layer should
+ * have.
+ */
+ uint32_t format[AV_DRM_MAX_PLANES] = {
+ p_mapper->desc.layers[i].format,
+ };
+
+ if (p_mapper->desc.layers[i].nb_planes > 1) {
+ switch (p_mapper->desc.layers[i].format) {
+ case DRM_FORMAT_NV12:
+ case DRM_FORMAT_NV16:
+ format[0] = DRM_FORMAT_R8;
+ format[1] = DRM_FORMAT_GR88;
+ break;
+ case DRM_FORMAT_YUV420:
+ format[0] = DRM_FORMAT_R8;
+ format[1] = DRM_FORMAT_R8;
+ format[2] = DRM_FORMAT_R8;
+ break;
+ case DRM_FORMAT_P010:
+#ifdef DRM_FORMAT_P030 /* Format added in a newer libdrm version than minimum */
+ case DRM_FORMAT_P030:
+#endif
+ format[0] = DRM_FORMAT_R16;
+ format[1] = DRM_FORMAT_GR1616;
+ break;
+ default:
+ mp_msg(mapper->log, probing ? MSGL_DEBUG : MSGL_ERR,
+ "Cannot map unknown multi-plane format: 0x%08X\n",
+ p_mapper->desc.layers[i].format);
+ return false;
+ }
+ } else {
+ /*
+ * As OpenGL only has one guaranteed rgba format (rgba8), drivers
+ * that support importing dmabuf formats with different channel
+ * orders do implicit swizzling to get to rgba. However, we look at
+ * the original imgfmt to decide channel order, and we then swizzle
+ * based on that. So, we can get into a situation where we swizzle
+ * twice and end up with a mess.
+ *
+ * The simplest way to avoid that is to lie to OpenGL and say that
+ * the surface we are importing is in the natural channel order, so
+ * that our swizzling does the right thing.
+ *
+ * DRM ABGR corresponds to OpenGL RGBA due to different naming
+ * conventions.
+ */
+ switch (format[0]) {
+ case DRM_FORMAT_ARGB8888:
+ case DRM_FORMAT_RGBA8888:
+ case DRM_FORMAT_BGRA8888:
+ format[0] = DRM_FORMAT_ABGR8888;
+ break;
+ case DRM_FORMAT_XRGB8888:
+ format[0] = DRM_FORMAT_XBGR8888;
+ break;
+ case DRM_FORMAT_RGBX8888:
+ case DRM_FORMAT_BGRX8888:
+ // Logically, these two formats should be handled as above,
+ // but there appear to be additional problems that make the
+ // format change here insufficient or incorrect, so we're
+ // doing nothing for now.
+ break;
+ }
+ }
+
+ for (int j = 0; j < p_mapper->desc.layers[i].nb_planes; j++, n++) {
+ int attribs[48] = {EGL_NONE};
+ int num_attribs = 0;
+
+ ADD_ATTRIB(EGL_LINUX_DRM_FOURCC_EXT, format[j]);
+ ADD_ATTRIB(EGL_WIDTH, p_mapper->tex[n]->params.w);
+ ADD_ATTRIB(EGL_HEIGHT, p_mapper->tex[n]->params.h);
+ ADD_PLANE_ATTRIBS(0);
+
+ p->images[n] = p->CreateImageKHR(eglGetCurrentDisplay(),
+ EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attribs);
+ if (!p->images[n]) {
+ mp_msg(mapper->log, probing ? MSGL_DEBUG : MSGL_ERR,
+ "Failed to import surface in EGL: %u\n", eglGetError());
+ return false;
+ }
+
+ gl->BindTexture(GL_TEXTURE_2D, p->gl_textures[n]);
+ p->EGLImageTargetTexture2DOES(GL_TEXTURE_2D, p->images[n]);
+
+ mapper->tex[n] = p_mapper->tex[n];
+ }
+ }
+
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+ return true;
+}
+
+static void vaapi_gl_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct dmabuf_interop_priv *p_mapper = mapper->priv;
+ struct vaapi_gl_mapper_priv *p = p_mapper->interop_mapper_priv;
+
+ if (p) {
+ for (int n = 0; n < 4; n++) {
+ if (p->images[n])
+ p->DestroyImageKHR(eglGetCurrentDisplay(), p->images[n]);
+ p->images[n] = 0;
+ }
+ }
+}
+
+bool dmabuf_interop_gl_init(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop)
+{
+ if (!ra_is_gl(hw->ra_ctx->ra)) {
+ // This is not an OpenGL RA.
+ return false;
+ }
+
+ if (!eglGetCurrentContext())
+ return false;
+
+ const char *exts = eglQueryString(eglGetCurrentDisplay(), EGL_EXTENSIONS);
+ if (!exts)
+ return false;
+
+ GL *gl = ra_gl_get(hw->ra_ctx->ra);
+ if (!gl_check_extension(exts, "EGL_EXT_image_dma_buf_import") ||
+ !gl_check_extension(exts, "EGL_KHR_image_base") ||
+ !gl_check_extension(gl->extensions, "GL_OES_EGL_image") ||
+ !(gl->mpgl_caps & MPGL_CAP_TEX_RG))
+ return false;
+
+ dmabuf_interop->use_modifiers =
+ gl_check_extension(exts, "EGL_EXT_image_dma_buf_import_modifiers");
+
+ MP_VERBOSE(hw, "using EGL dmabuf interop\n");
+
+ dmabuf_interop->interop_init = vaapi_gl_mapper_init;
+ dmabuf_interop->interop_uninit = vaapi_gl_mapper_uninit;
+ dmabuf_interop->interop_map = vaapi_gl_map;
+ dmabuf_interop->interop_unmap = vaapi_gl_unmap;
+
+ return true;
+}
diff --git a/video/out/hwdec/dmabuf_interop_pl.c b/video/out/hwdec/dmabuf_interop_pl.c
new file mode 100644
index 0000000..0a8ec5b
--- /dev/null
+++ b/video/out/hwdec/dmabuf_interop_pl.c
@@ -0,0 +1,138 @@
+/*
+ * 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 <errno.h>
+#include <unistd.h>
+
+#include "dmabuf_interop.h"
+#include "video/out/placebo/ra_pl.h"
+#include "video/out/placebo/utils.h"
+
+static bool vaapi_pl_map(struct ra_hwdec_mapper *mapper,
+ struct dmabuf_interop *dmabuf_interop,
+ bool probing)
+{
+ struct dmabuf_interop_priv *p = mapper->priv;
+ pl_gpu gpu = ra_pl_get(mapper->ra);
+
+ struct ra_imgfmt_desc desc = {0};
+ if (!ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &desc))
+ return false;
+
+ // The calling code validates that the total number of exported planes
+ // equals the number we expected in p->num_planes.
+ int layer = 0;
+ int layer_plane = 0;
+ for (int n = 0; n < p->num_planes; n++) {
+
+ const struct ra_format *format = desc.planes[n];
+ int id = p->desc.layers[layer].planes[layer_plane].object_index;
+ int fd = p->desc.objects[id].fd;
+ uint32_t size = p->desc.objects[id].size;
+ uint32_t offset = p->desc.layers[layer].planes[layer_plane].offset;
+ uint32_t pitch = p->desc.layers[layer].planes[layer_plane].pitch;
+
+ // AMD drivers do not return the size in the surface description, so we
+ // need to query it manually.
+ if (size == 0) {
+ size = lseek(fd, 0, SEEK_END);
+ if (size == -1) {
+ MP_ERR(mapper, "Cannot obtain size of object with fd %d: %s\n",
+ fd, mp_strerror(errno));
+ return false;
+ }
+ off_t err = lseek(fd, 0, SEEK_SET);
+ if (err == -1) {
+ MP_ERR(mapper, "Failed to reset offset for fd %d: %s\n",
+ fd, mp_strerror(errno));
+ return false;
+ }
+ }
+
+ struct pl_tex_params tex_params = {
+ .w = mp_image_plane_w(&p->layout, n),
+ .h = mp_image_plane_h(&p->layout, n),
+ .d = 0,
+ .format = format->priv,
+ .sampleable = true,
+ .import_handle = PL_HANDLE_DMA_BUF,
+ .shared_mem = (struct pl_shared_mem) {
+ .handle = {
+ .fd = fd,
+ },
+ .size = size,
+ .offset = offset,
+ .drm_format_mod = p->desc.objects[id].format_modifier,
+ .stride_w = pitch,
+ },
+ };
+
+ mppl_log_set_probing(gpu->log, probing);
+ pl_tex pltex = pl_tex_create(gpu, &tex_params);
+ mppl_log_set_probing(gpu->log, false);
+ if (!pltex)
+ return false;
+
+ struct ra_tex *ratex = talloc_ptrtype(NULL, ratex);
+ int ret = mppl_wrap_tex(mapper->ra, pltex, ratex);
+ if (!ret) {
+ pl_tex_destroy(gpu, &pltex);
+ talloc_free(ratex);
+ return false;
+ }
+ mapper->tex[n] = ratex;
+
+ MP_TRACE(mapper, "Object %d with fd %d imported as %p\n",
+ id, fd, ratex);
+
+ layer_plane++;
+ if (layer_plane == p->desc.layers[layer].nb_planes) {
+ layer_plane = 0;
+ layer++;
+ }
+ }
+ return true;
+}
+
+static void vaapi_pl_unmap(struct ra_hwdec_mapper *mapper)
+{
+ for (int n = 0; n < 4; n++)
+ ra_tex_free(mapper->ra, &mapper->tex[n]);
+}
+
+bool dmabuf_interop_pl_init(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop)
+{
+ pl_gpu gpu = ra_pl_get(hw->ra_ctx->ra);
+ if (!gpu) {
+ // This is not a libplacebo RA;
+ return false;
+ }
+
+ if (!(gpu->import_caps.tex & PL_HANDLE_DMA_BUF)) {
+ MP_VERBOSE(hw, "libplacebo dmabuf interop requires support for "
+ "PL_HANDLE_DMA_BUF import.\n");
+ return false;
+ }
+
+ MP_VERBOSE(hw, "using libplacebo dmabuf interop\n");
+
+ dmabuf_interop->interop_map = vaapi_pl_map;
+ dmabuf_interop->interop_unmap = vaapi_pl_unmap;
+
+ return true;
+}
diff --git a/video/out/hwdec/dmabuf_interop_wl.c b/video/out/hwdec/dmabuf_interop_wl.c
new file mode 100644
index 0000000..606a0aa
--- /dev/null
+++ b/video/out/hwdec/dmabuf_interop_wl.c
@@ -0,0 +1,83 @@
+/*
+ * 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 "video/out/wldmabuf/ra_wldmabuf.h"
+#include "dmabuf_interop.h"
+
+static bool mapper_init(struct ra_hwdec_mapper *mapper,
+ const struct ra_imgfmt_desc *desc)
+{
+ return true;
+}
+
+static void mapper_uninit(const struct ra_hwdec_mapper *mapper)
+{
+}
+
+static bool map(struct ra_hwdec_mapper *mapper,
+ struct dmabuf_interop *dmabuf_interop,
+ bool probing)
+{
+ // 1. only validate format when composed layers is enabled (i.e. vaapi)
+ // 2. for drmprime, just return true for now, as this use case
+ // has not been tested.
+ if (!dmabuf_interop->composed_layers)
+ return true;
+
+ int layer_no = 0;
+ struct dmabuf_interop_priv *mapper_p = mapper->priv;
+ uint32_t drm_format = mapper_p->desc.layers[layer_no].format;
+
+ if (mapper_p->desc.nb_layers != 1) {
+ MP_VERBOSE(mapper, "Mapped surface has separate layers - expected composed layers.\n");
+ return false;
+ } else if (!ra_compatible_format(mapper->ra, drm_format,
+ mapper_p->desc.objects[0].format_modifier)) {
+ MP_VERBOSE(mapper, "Mapped surface with format %s; drm format '%s(%016lx)' "
+ "is not supported by compositor.\n",
+ mp_imgfmt_to_name(mapper->src->params.hw_subfmt),
+ mp_tag_str(drm_format),
+ mapper_p->desc.objects[0].format_modifier);
+ return false;
+ }
+
+ MP_VERBOSE(mapper, "Supported Wayland display format %s: '%s(%016lx)'\n",
+ mp_imgfmt_to_name(mapper->src->params.hw_subfmt),
+ mp_tag_str(drm_format), mapper_p->desc.objects[0].format_modifier);
+
+ return true;
+}
+
+static void unmap(struct ra_hwdec_mapper *mapper)
+{
+}
+
+bool dmabuf_interop_wl_init(const struct ra_hwdec *hw,
+ struct dmabuf_interop *dmabuf_interop)
+{
+ if (!ra_is_wldmabuf(hw->ra_ctx->ra))
+ return false;
+
+ if (strstr(hw->driver->name, "vaapi") != NULL)
+ dmabuf_interop->composed_layers = true;
+
+ dmabuf_interop->interop_init = mapper_init;
+ dmabuf_interop->interop_uninit = mapper_uninit;
+ dmabuf_interop->interop_map = map;
+ dmabuf_interop->interop_unmap = unmap;
+
+ return true;
+}
diff --git a/video/out/hwdec/hwdec_aimagereader.c b/video/out/hwdec/hwdec_aimagereader.c
new file mode 100644
index 0000000..0dd5497
--- /dev/null
+++ b/video/out/hwdec/hwdec_aimagereader.c
@@ -0,0 +1,402 @@
+/*
+ * Copyright (c) 2021 sfan5 <sfan5@live.de>
+ *
+ * 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 <assert.h>
+#include <dlfcn.h>
+#include <EGL/egl.h>
+#include <media/NdkImageReader.h>
+#include <android/native_window_jni.h>
+#include <libavcodec/mediacodec.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_mediacodec.h>
+
+#include "misc/jni.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+#include "video/out/gpu/hwdec.h"
+#include "video/out/opengl/ra_gl.h"
+
+typedef void *GLeglImageOES;
+typedef void *EGLImageKHR;
+#define EGL_NATIVE_BUFFER_ANDROID 0x3140
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+ AImageReader *reader;
+ jobject surface;
+ void *lib_handle;
+
+ media_status_t (*AImageReader_newWithUsage)(
+ int32_t, int32_t, int32_t, uint64_t, int32_t, AImageReader **);
+ media_status_t (*AImageReader_getWindow)(
+ AImageReader *, ANativeWindow **);
+ media_status_t (*AImageReader_setImageListener)(
+ AImageReader *, AImageReader_ImageListener *);
+ media_status_t (*AImageReader_acquireLatestImage)(AImageReader *, AImage **);
+ void (*AImageReader_delete)(AImageReader *);
+ media_status_t (*AImage_getHardwareBuffer)(const AImage *, AHardwareBuffer **);
+ void (*AImage_delete)(AImage *);
+ void (*AHardwareBuffer_describe)(const AHardwareBuffer *, AHardwareBuffer_Desc *);
+ jobject (*ANativeWindow_toSurface)(JNIEnv *, ANativeWindow *);
+};
+
+struct priv {
+ struct mp_log *log;
+
+ GLuint gl_texture;
+ AImage *image;
+ EGLImageKHR egl_image;
+
+ mp_mutex lock;
+ mp_cond cond;
+ bool image_available;
+
+ EGLImageKHR (EGLAPIENTRY *CreateImageKHR)(
+ EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint *);
+ EGLBoolean (EGLAPIENTRY *DestroyImageKHR)(EGLDisplay, EGLImageKHR);
+ EGLClientBuffer (EGLAPIENTRY *GetNativeClientBufferANDROID)(
+ const struct AHardwareBuffer *);
+ void (EGLAPIENTRY *EGLImageTargetTexture2DOES)(GLenum, GLeglImageOES);
+};
+
+const static struct { const char *symbol; int offset; } lib_functions[] = {
+ { "AImageReader_newWithUsage", offsetof(struct priv_owner, AImageReader_newWithUsage) },
+ { "AImageReader_getWindow", offsetof(struct priv_owner, AImageReader_getWindow) },
+ { "AImageReader_setImageListener", offsetof(struct priv_owner, AImageReader_setImageListener) },
+ { "AImageReader_acquireLatestImage", offsetof(struct priv_owner, AImageReader_acquireLatestImage) },
+ { "AImageReader_delete", offsetof(struct priv_owner, AImageReader_delete) },
+ { "AImage_getHardwareBuffer", offsetof(struct priv_owner, AImage_getHardwareBuffer) },
+ { "AImage_delete", offsetof(struct priv_owner, AImage_delete) },
+ { "AHardwareBuffer_describe", offsetof(struct priv_owner, AHardwareBuffer_describe) },
+ { "ANativeWindow_toSurface", offsetof(struct priv_owner, ANativeWindow_toSurface) },
+ { NULL, 0 },
+};
+
+
+static AVBufferRef *create_mediacodec_device_ref(jobject surface)
+{
+ AVBufferRef *device_ref = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_MEDIACODEC);
+ if (!device_ref)
+ return NULL;
+
+ AVHWDeviceContext *ctx = (void *)device_ref->data;
+ AVMediaCodecDeviceContext *hwctx = ctx->hwctx;
+ hwctx->surface = surface;
+
+ if (av_hwdevice_ctx_init(device_ref) < 0)
+ av_buffer_unref(&device_ref);
+
+ return device_ref;
+}
+
+static bool load_lib_functions(struct priv_owner *p, struct mp_log *log)
+{
+ p->lib_handle = dlopen("libmediandk.so", RTLD_NOW | RTLD_GLOBAL);
+ if (!p->lib_handle)
+ return false;
+ for (int i = 0; lib_functions[i].symbol; i++) {
+ const char *sym = lib_functions[i].symbol;
+ void *fun = dlsym(p->lib_handle, sym);
+ if (!fun)
+ fun = dlsym(RTLD_DEFAULT, sym);
+ if (!fun) {
+ mp_warn(log, "Could not resolve symbol %s\n", sym);
+ return false;
+ }
+
+ *(void **) ((uint8_t*)p + lib_functions[i].offset) = fun;
+ }
+ return true;
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ if (!ra_is_gl(hw->ra_ctx->ra))
+ return -1;
+ if (!eglGetCurrentContext())
+ return -1;
+
+ const char *exts = eglQueryString(eglGetCurrentDisplay(), EGL_EXTENSIONS);
+ if (!gl_check_extension(exts, "EGL_ANDROID_image_native_buffer"))
+ return -1;
+
+ if (!load_lib_functions(p, hw->log))
+ return -1;
+
+ static const char *es2_exts[] = {"GL_OES_EGL_image_external", 0};
+ static const char *es3_exts[] = {"GL_OES_EGL_image_external_essl3", 0};
+ GL *gl = ra_gl_get(hw->ra_ctx->ra);
+ if (gl_check_extension(gl->extensions, es3_exts[0]))
+ hw->glsl_extensions = es3_exts;
+ else
+ hw->glsl_extensions = es2_exts;
+
+ // dummy dimensions, AImageReader only transports hardware buffers
+ media_status_t ret = p->AImageReader_newWithUsage(16, 16,
+ AIMAGE_FORMAT_PRIVATE, AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE,
+ 5, &p->reader);
+ if (ret != AMEDIA_OK) {
+ MP_ERR(hw, "newWithUsage failed: %d\n", ret);
+ return -1;
+ }
+ assert(p->reader);
+
+ ANativeWindow *window;
+ ret = p->AImageReader_getWindow(p->reader, &window);
+ if (ret != AMEDIA_OK) {
+ MP_ERR(hw, "getWindow failed: %d\n", ret);
+ return -1;
+ }
+ assert(window);
+
+ JNIEnv *env = MP_JNI_GET_ENV(hw);
+ assert(env);
+ jobject surface = p->ANativeWindow_toSurface(env, window);
+ p->surface = (*env)->NewGlobalRef(env, surface);
+ (*env)->DeleteLocalRef(env, surface);
+
+ p->hwctx = (struct mp_hwdec_ctx) {
+ .driver_name = hw->driver->name,
+ .av_device_ref = create_mediacodec_device_ref(p->surface),
+ .hw_imgfmt = IMGFMT_MEDIACODEC,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx\n");
+ return -1;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ return 0;
+}
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ JNIEnv *env = MP_JNI_GET_ENV(hw);
+ assert(env);
+
+ if (p->surface) {
+ (*env)->DeleteGlobalRef(env, p->surface);
+ p->surface = NULL;
+ }
+
+ if (p->reader) {
+ p->AImageReader_delete(p->reader);
+ p->reader = NULL;
+ }
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+
+ if (p->lib_handle) {
+ dlclose(p->lib_handle);
+ p->lib_handle = NULL;
+ }
+}
+
+static void image_callback(void *context, AImageReader *reader)
+{
+ struct priv *p = context;
+
+ mp_mutex_lock(&p->lock);
+ p->image_available = true;
+ mp_cond_signal(&p->cond);
+ mp_mutex_unlock(&p->lock);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ struct priv_owner *o = mapper->owner->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ p->log = mapper->log;
+ mp_mutex_init(&p->lock);
+ mp_cond_init(&p->cond);
+
+ p->CreateImageKHR = (void *)eglGetProcAddress("eglCreateImageKHR");
+ p->DestroyImageKHR = (void *)eglGetProcAddress("eglDestroyImageKHR");
+ p->GetNativeClientBufferANDROID =
+ (void *)eglGetProcAddress("eglGetNativeClientBufferANDROID");
+ p->EGLImageTargetTexture2DOES =
+ (void *)eglGetProcAddress("glEGLImageTargetTexture2DOES");
+
+ if (!p->CreateImageKHR || !p->DestroyImageKHR ||
+ !p->GetNativeClientBufferANDROID || !p->EGLImageTargetTexture2DOES)
+ return -1;
+
+ AImageReader_ImageListener listener = {
+ .context = p,
+ .onImageAvailable = image_callback,
+ };
+ o->AImageReader_setImageListener(o->reader, &listener);
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = IMGFMT_RGB0;
+ mapper->dst_params.hw_subfmt = 0;
+
+ // texture creation
+ gl->GenTextures(1, &p->gl_texture);
+ gl->BindTexture(GL_TEXTURE_EXTERNAL_OES, p->gl_texture);
+ gl->TexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ gl->BindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = mapper->src_params.w,
+ .h = mapper->src_params.h,
+ .d = 1,
+ .format = ra_find_unorm_format(mapper->ra, 1, 4),
+ .render_src = true,
+ .src_linear = true,
+ .external_oes = true,
+ };
+
+ if (params.format->ctype != RA_CTYPE_UNORM)
+ return -1;
+
+ mapper->tex[0] = ra_create_wrapped_tex(mapper->ra, &params, p->gl_texture);
+ if (!mapper->tex[0])
+ return -1;
+
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ struct priv_owner *o = mapper->owner->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ o->AImageReader_setImageListener(o->reader, NULL);
+
+ gl->DeleteTextures(1, &p->gl_texture);
+ p->gl_texture = 0;
+
+ ra_tex_free(mapper->ra, &mapper->tex[0]);
+
+ mp_mutex_destroy(&p->lock);
+ mp_cond_destroy(&p->cond);
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ struct priv_owner *o = mapper->owner->priv;
+
+ if (p->egl_image) {
+ p->DestroyImageKHR(eglGetCurrentDisplay(), p->egl_image);
+ p->egl_image = 0;
+ }
+
+ if (p->image) {
+ o->AImage_delete(p->image);
+ p->image = NULL;
+ }
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ struct priv_owner *o = mapper->owner->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ {
+ if (mapper->src->imgfmt != IMGFMT_MEDIACODEC)
+ return -1;
+ AVMediaCodecBuffer *buffer = (AVMediaCodecBuffer *)mapper->src->planes[3];
+ av_mediacodec_release_buffer(buffer, 1);
+ }
+
+ bool image_available = false;
+ mp_mutex_lock(&p->lock);
+ if (!p->image_available) {
+ mp_cond_timedwait(&p->cond, &p->lock, MP_TIME_MS_TO_NS(100));
+ if (!p->image_available)
+ MP_WARN(mapper, "Waiting for frame timed out!\n");
+ }
+ image_available = p->image_available;
+ p->image_available = false;
+ mp_mutex_unlock(&p->lock);
+
+ media_status_t ret = o->AImageReader_acquireLatestImage(o->reader, &p->image);
+ if (ret != AMEDIA_OK) {
+ MP_ERR(mapper, "acquireLatestImage failed: %d\n", ret);
+ // If we merely timed out waiting return success anyway to avoid
+ // flashing frames of render errors.
+ return image_available ? -1 : 0;
+ }
+ assert(p->image);
+
+ AHardwareBuffer *hwbuf = NULL;
+ ret = o->AImage_getHardwareBuffer(p->image, &hwbuf);
+ if (ret != AMEDIA_OK) {
+ MP_ERR(mapper, "getHardwareBuffer failed: %d\n", ret);
+ return -1;
+ }
+ assert(hwbuf);
+
+ // Update texture size since it may differ
+ AHardwareBuffer_Desc d;
+ o->AHardwareBuffer_describe(hwbuf, &d);
+ if (mapper->tex[0]->params.w != d.width || mapper->tex[0]->params.h != d.height) {
+ MP_VERBOSE(p, "Texture dimensions changed to %dx%d\n", d.width, d.height);
+ mapper->tex[0]->params.w = d.width;
+ mapper->tex[0]->params.h = d.height;
+ }
+
+ EGLClientBuffer buf = p->GetNativeClientBufferANDROID(hwbuf);
+ if (!buf)
+ return -1;
+
+ const int attribs[] = {EGL_NONE};
+ p->egl_image = p->CreateImageKHR(eglGetCurrentDisplay(),
+ EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, buf, attribs);
+ if (!p->egl_image)
+ return -1;
+
+ gl->BindTexture(GL_TEXTURE_EXTERNAL_OES, p->gl_texture);
+ p->EGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, p->egl_image);
+ gl->BindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
+
+ return 0;
+}
+
+
+const struct ra_hwdec_driver ra_hwdec_aimagereader = {
+ .name = "aimagereader",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_MEDIACODEC, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/hwdec/hwdec_cuda.c b/video/out/hwdec/hwdec_cuda.c
new file mode 100644
index 0000000..68ad60d
--- /dev/null
+++ b/video/out/hwdec/hwdec_cuda.c
@@ -0,0 +1,286 @@
+/*
+ * Copyright (c) 2016 Philip Langdale <philipl@overt.org>
+ *
+ * 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/>.
+ */
+
+/*
+ * This hwdec implements an optimized output path using CUDA->OpenGL
+ * or CUDA->Vulkan interop for frame data that is stored in CUDA
+ * device memory. Although it is not explicit in the code here, the
+ * only practical way to get data in this form is from the
+ * nvdec/cuvid decoder.
+ */
+
+#include "config.h"
+#include "hwdec_cuda.h"
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_cuda.h>
+
+int check_cu(const struct ra_hwdec *hw, CUresult err, const char *func)
+{
+ const char *err_name;
+ const char *err_string;
+
+ struct cuda_hw_priv *p = hw->priv;
+ int level = hw->probing ? MSGL_V : MSGL_ERR;
+
+ MP_TRACE(hw, "Calling %s\n", func);
+
+ if (err == CUDA_SUCCESS)
+ return 0;
+
+ p->cu->cuGetErrorName(err, &err_name);
+ p->cu->cuGetErrorString(err, &err_string);
+
+ MP_MSG(hw, level, "%s failed", func);
+ if (err_name && err_string)
+ MP_MSG(hw, level, " -> %s: %s", err_name, err_string);
+ MP_MSG(hw, level, "\n");
+
+ return -1;
+}
+
+#define CHECK_CU(x) check_cu(hw, (x), #x)
+
+const static cuda_interop_init interop_inits[] = {
+#if HAVE_GL
+ cuda_gl_init,
+#endif
+#if HAVE_VULKAN
+ cuda_vk_init,
+#endif
+ NULL
+};
+
+static int cuda_init(struct ra_hwdec *hw)
+{
+ AVBufferRef *hw_device_ctx = NULL;
+ CUcontext dummy;
+ int ret = 0;
+ struct cuda_hw_priv *p = hw->priv;
+ CudaFunctions *cu;
+ int level = hw->probing ? MSGL_V : MSGL_ERR;
+
+ ret = cuda_load_functions(&p->cu, NULL);
+ if (ret != 0) {
+ MP_MSG(hw, level, "Failed to load CUDA symbols\n");
+ return -1;
+ }
+ cu = p->cu;
+
+ ret = CHECK_CU(cu->cuInit(0));
+ if (ret < 0)
+ return -1;
+
+ // Initialise CUDA context from backend.
+ for (int i = 0; interop_inits[i]; i++) {
+ if (interop_inits[i](hw)) {
+ break;
+ }
+ }
+
+ if (!p->ext_init || !p->ext_uninit) {
+ MP_MSG(hw, level,
+ "CUDA hwdec only works with OpenGL or Vulkan backends.\n");
+ return -1;
+ }
+
+ hw_device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA);
+ if (!hw_device_ctx)
+ goto error;
+
+ AVHWDeviceContext *device_ctx = (void *)hw_device_ctx->data;
+
+ AVCUDADeviceContext *device_hwctx = device_ctx->hwctx;
+ device_hwctx->cuda_ctx = p->decode_ctx;
+
+ ret = av_hwdevice_ctx_init(hw_device_ctx);
+ if (ret < 0) {
+ MP_MSG(hw, level, "av_hwdevice_ctx_init failed\n");
+ goto error;
+ }
+
+ ret = CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+ if (ret < 0)
+ goto error;
+
+ p->hwctx = (struct mp_hwdec_ctx) {
+ .driver_name = hw->driver->name,
+ .av_device_ref = hw_device_ctx,
+ .hw_imgfmt = IMGFMT_CUDA,
+ };
+ hwdec_devices_add(hw->devs, &p->hwctx);
+ return 0;
+
+ error:
+ av_buffer_unref(&hw_device_ctx);
+ CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+
+ return -1;
+}
+
+static void cuda_uninit(struct ra_hwdec *hw)
+{
+ struct cuda_hw_priv *p = hw->priv;
+ CudaFunctions *cu = p->cu;
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+
+ if (p->decode_ctx && p->decode_ctx != p->display_ctx)
+ CHECK_CU(cu->cuCtxDestroy(p->decode_ctx));
+
+ if (p->display_ctx)
+ CHECK_CU(cu->cuCtxDestroy(p->display_ctx));
+
+ cuda_free_functions(&p->cu);
+}
+
+#undef CHECK_CU
+#define CHECK_CU(x) check_cu((mapper)->owner, (x), #x)
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CUcontext dummy;
+ CudaFunctions *cu = p_owner->cu;
+ int ret = 0, eret = 0;
+
+ p->display_ctx = p_owner->display_ctx;
+
+ int imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = imgfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ mp_image_set_params(&p->layout, &mapper->dst_params);
+
+ struct ra_imgfmt_desc desc;
+ if (!ra_get_imgfmt_desc(mapper->ra, imgfmt, &desc)) {
+ MP_ERR(mapper, "Unsupported format: %s\n", mp_imgfmt_to_name(imgfmt));
+ return -1;
+ }
+
+ ret = CHECK_CU(cu->cuCtxPushCurrent(p->display_ctx));
+ if (ret < 0)
+ return ret;
+
+ for (int n = 0; n < desc.num_planes; n++) {
+ if (!p_owner->ext_init(mapper, desc.planes[n], n))
+ goto error;
+ }
+
+ error:
+ eret = CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+ if (eret < 0)
+ return eret;
+
+ return ret;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct cuda_mapper_priv *p = mapper->priv;
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ CudaFunctions *cu = p_owner->cu;
+ CUcontext dummy;
+
+ // Don't bail if any CUDA calls fail. This is all best effort.
+ CHECK_CU(cu->cuCtxPushCurrent(p->display_ctx));
+ for (int n = 0; n < 4; n++) {
+ p_owner->ext_uninit(mapper, n);
+ ra_tex_free(mapper->ra, &mapper->tex[n]);
+ }
+ CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct cuda_mapper_priv *p = mapper->priv;
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ CudaFunctions *cu = p_owner->cu;
+ CUcontext dummy;
+ int ret = 0, eret = 0;
+
+ ret = CHECK_CU(cu->cuCtxPushCurrent(p->display_ctx));
+ if (ret < 0)
+ return ret;
+
+ for (int n = 0; n < p->layout.num_planes; n++) {
+ if (p_owner->ext_wait) {
+ if (!p_owner->ext_wait(mapper, n))
+ goto error;
+ }
+
+ CUDA_MEMCPY2D cpy = {
+ .srcMemoryType = CU_MEMORYTYPE_DEVICE,
+ .srcDevice = (CUdeviceptr)mapper->src->planes[n],
+ .srcPitch = mapper->src->stride[n],
+ .srcY = 0,
+ .dstMemoryType = CU_MEMORYTYPE_ARRAY,
+ .dstArray = p->cu_array[n],
+ .WidthInBytes = mp_image_plane_w(&p->layout, n) *
+ mapper->tex[n]->params.format->pixel_size,
+ .Height = mp_image_plane_h(&p->layout, n),
+ };
+
+ ret = CHECK_CU(cu->cuMemcpy2DAsync(&cpy, 0));
+ if (ret < 0)
+ goto error;
+
+ if (p_owner->ext_signal) {
+ if (!p_owner->ext_signal(mapper, n))
+ goto error;
+ }
+ }
+ if (p_owner->do_full_sync)
+ CHECK_CU(cu->cuStreamSynchronize(0));
+
+ // fall through
+ error:
+
+ // Regardless of success or failure, we no longer need the source image,
+ // because this hwdec makes an explicit memcpy into the mapper textures
+ mp_image_unrefp(&mapper->src);
+
+ eret = CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+ if (eret < 0)
+ return eret;
+
+ return ret;
+}
+
+const struct ra_hwdec_driver ra_hwdec_cuda = {
+ .name = "cuda",
+ .imgfmts = {IMGFMT_CUDA, 0},
+ .priv_size = sizeof(struct cuda_hw_priv),
+ .init = cuda_init,
+ .uninit = cuda_uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct cuda_mapper_priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/hwdec/hwdec_cuda.h b/video/out/hwdec/hwdec_cuda.h
new file mode 100644
index 0000000..9c55053
--- /dev/null
+++ b/video/out/hwdec/hwdec_cuda.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2019 Philip Langdale <philipl@overt.org>
+ *
+ * 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/>.
+ */
+
+#pragma once
+
+#include <ffnvcodec/dynlink_loader.h>
+
+#include "video/out/gpu/hwdec.h"
+
+struct cuda_hw_priv {
+ struct mp_hwdec_ctx hwctx;
+ CudaFunctions *cu;
+ CUcontext display_ctx;
+ CUcontext decode_ctx;
+
+ // Do we need to do a full CPU sync after copying
+ bool do_full_sync;
+
+ bool (*ext_init)(struct ra_hwdec_mapper *mapper,
+ const struct ra_format *format, int n);
+ void (*ext_uninit)(const struct ra_hwdec_mapper *mapper, int n);
+
+ // These are only necessary if the gpu api requires synchronisation
+ bool (*ext_wait)(const struct ra_hwdec_mapper *mapper, int n);
+ bool (*ext_signal)(const struct ra_hwdec_mapper *mapper, int n);
+};
+
+struct cuda_mapper_priv {
+ struct mp_image layout;
+ CUarray cu_array[4];
+
+ CUcontext display_ctx;
+
+ void *ext[4];
+};
+
+typedef bool (*cuda_interop_init)(const struct ra_hwdec *hw);
+
+bool cuda_gl_init(const struct ra_hwdec *hw);
+
+bool cuda_vk_init(const struct ra_hwdec *hw);
+
+int check_cu(const struct ra_hwdec *hw, CUresult err, const char *func);
diff --git a/video/out/hwdec/hwdec_cuda_gl.c b/video/out/hwdec/hwdec_cuda_gl.c
new file mode 100644
index 0000000..f20540e
--- /dev/null
+++ b/video/out/hwdec/hwdec_cuda_gl.c
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2019 Philip Langdale <philipl@overt.org>
+ *
+ * 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 "hwdec_cuda.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "video/out/opengl/formats.h"
+#include "video/out/opengl/ra_gl.h"
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_cuda.h>
+#include <unistd.h>
+
+#define CHECK_CU(x) check_cu((mapper)->owner, (x), #x)
+
+struct ext_gl {
+ CUgraphicsResource cu_res;
+};
+
+static bool cuda_ext_gl_init(struct ra_hwdec_mapper *mapper,
+ const struct ra_format *format, int n)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CudaFunctions *cu = p_owner->cu;
+ int ret = 0;
+ CUcontext dummy;
+
+ struct ext_gl *egl = talloc_ptrtype(NULL, egl);
+ p->ext[n] = egl;
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = mp_image_plane_w(&p->layout, n),
+ .h = mp_image_plane_h(&p->layout, n),
+ .d = 1,
+ .format = format,
+ .render_src = true,
+ .src_linear = format->linear_filter,
+ };
+
+ mapper->tex[n] = ra_tex_create(mapper->ra, &params);
+ if (!mapper->tex[n]) {
+ goto error;
+ }
+
+ GLuint texture;
+ GLenum target;
+ ra_gl_get_raw_tex(mapper->ra, mapper->tex[n], &texture, &target);
+
+ ret = CHECK_CU(cu->cuGraphicsGLRegisterImage(&egl->cu_res, texture, target,
+ CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD));
+ if (ret < 0)
+ goto error;
+
+ ret = CHECK_CU(cu->cuGraphicsMapResources(1, &egl->cu_res, 0));
+ if (ret < 0)
+ goto error;
+
+ ret = CHECK_CU(cu->cuGraphicsSubResourceGetMappedArray(&p->cu_array[n], egl->cu_res,
+ 0, 0));
+ if (ret < 0)
+ goto error;
+
+ ret = CHECK_CU(cu->cuGraphicsUnmapResources(1, &egl->cu_res, 0));
+ if (ret < 0)
+ goto error;
+
+ return true;
+
+error:
+ CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+ return false;
+}
+
+static void cuda_ext_gl_uninit(const struct ra_hwdec_mapper *mapper, int n)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CudaFunctions *cu = p_owner->cu;
+
+ struct ext_gl *egl = p->ext[n];
+ if (egl && egl->cu_res) {
+ CHECK_CU(cu->cuGraphicsUnregisterResource(egl->cu_res));
+ egl->cu_res = 0;
+ }
+ talloc_free(egl);
+}
+
+#undef CHECK_CU
+#define CHECK_CU(x) check_cu(hw, (x), #x)
+
+bool cuda_gl_init(const struct ra_hwdec *hw) {
+ int ret = 0;
+ struct cuda_hw_priv *p = hw->priv;
+ CudaFunctions *cu = p->cu;
+
+ if (ra_is_gl(hw->ra_ctx->ra)) {
+ GL *gl = ra_gl_get(hw->ra_ctx->ra);
+ if (gl->version < 210 && gl->es < 300) {
+ MP_VERBOSE(hw, "need OpenGL >= 2.1 or OpenGL-ES >= 3.0\n");
+ return false;
+ }
+ } else {
+ // This is not an OpenGL RA.
+ return false;
+ }
+
+ CUdevice display_dev;
+ unsigned int device_count;
+ ret = CHECK_CU(cu->cuGLGetDevices(&device_count, &display_dev, 1,
+ CU_GL_DEVICE_LIST_ALL));
+ if (ret < 0)
+ return false;
+
+ ret = CHECK_CU(cu->cuCtxCreate(&p->display_ctx, CU_CTX_SCHED_BLOCKING_SYNC,
+ display_dev));
+ if (ret < 0)
+ return false;
+
+ p->decode_ctx = p->display_ctx;
+
+ struct cuda_opts *opts = mp_get_config_group(NULL, hw->global, &cuda_conf);
+ int decode_dev_idx = opts->cuda_device;
+ talloc_free(opts);
+
+ if (decode_dev_idx > -1) {
+ CUcontext dummy;
+ CUdevice decode_dev;
+ ret = CHECK_CU(cu->cuDeviceGet(&decode_dev, decode_dev_idx));
+ if (ret < 0) {
+ CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+ return false;
+ }
+
+ if (decode_dev != display_dev) {
+ MP_INFO(hw, "Using separate decoder and display devices\n");
+
+ // Pop the display context. We won't use it again during init()
+ ret = CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+ if (ret < 0)
+ return false;
+
+ ret = CHECK_CU(cu->cuCtxCreate(&p->decode_ctx, CU_CTX_SCHED_BLOCKING_SYNC,
+ decode_dev));
+ if (ret < 0)
+ return false;
+ }
+ }
+
+ // We don't have a way to do a GPU sync after copying
+ p->do_full_sync = true;
+
+ p->ext_init = cuda_ext_gl_init;
+ p->ext_uninit = cuda_ext_gl_uninit;
+
+ return true;
+}
diff --git a/video/out/hwdec/hwdec_cuda_vk.c b/video/out/hwdec/hwdec_cuda_vk.c
new file mode 100644
index 0000000..b9f8caa
--- /dev/null
+++ b/video/out/hwdec/hwdec_cuda_vk.c
@@ -0,0 +1,344 @@
+/*
+ * Copyright (c) 2019 Philip Langdale <philipl@overt.org>
+ *
+ * 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 "config.h"
+#include "hwdec_cuda.h"
+#include "video/out/placebo/ra_pl.h"
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_cuda.h>
+#include <libplacebo/vulkan.h>
+#include <unistd.h>
+
+#if HAVE_WIN32_DESKTOP
+#include <versionhelpers.h>
+#define HANDLE_TYPE PL_HANDLE_WIN32
+#else
+#define HANDLE_TYPE PL_HANDLE_FD
+#endif
+
+#define CHECK_CU(x) check_cu((mapper)->owner, (x), #x)
+
+struct ext_vk {
+ CUexternalMemory mem;
+ CUmipmappedArray mma;
+
+ pl_tex pltex;
+ pl_vulkan_sem vk_sem;
+ union pl_handle sem_handle;
+ CUexternalSemaphore cuda_sem;
+};
+
+static bool cuda_ext_vk_init(struct ra_hwdec_mapper *mapper,
+ const struct ra_format *format, int n)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CudaFunctions *cu = p_owner->cu;
+ int mem_fd = -1;
+ int ret = 0;
+
+ struct ext_vk *evk = talloc_ptrtype(NULL, evk);
+ p->ext[n] = evk;
+
+ pl_gpu gpu = ra_pl_get(mapper->ra);
+
+ struct pl_tex_params tex_params = {
+ .w = mp_image_plane_w(&p->layout, n),
+ .h = mp_image_plane_h(&p->layout, n),
+ .d = 0,
+ .format = ra_pl_fmt_get(format),
+ .sampleable = true,
+ .export_handle = HANDLE_TYPE,
+ };
+
+ evk->pltex = pl_tex_create(gpu, &tex_params);
+ if (!evk->pltex) {
+ goto error;
+ }
+
+ struct ra_tex *ratex = talloc_ptrtype(NULL, ratex);
+ ret = mppl_wrap_tex(mapper->ra, evk->pltex, ratex);
+ if (!ret) {
+ pl_tex_destroy(gpu, &evk->pltex);
+ talloc_free(ratex);
+ goto error;
+ }
+ mapper->tex[n] = ratex;
+
+#if !HAVE_WIN32_DESKTOP
+ mem_fd = dup(evk->pltex->shared_mem.handle.fd);
+ if (mem_fd < 0)
+ goto error;
+#endif
+
+ CUDA_EXTERNAL_MEMORY_HANDLE_DESC ext_desc = {
+#if HAVE_WIN32_DESKTOP
+ .type = CU_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32,
+ .handle.win32.handle = evk->pltex->shared_mem.handle.handle,
+#else
+ .type = CU_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD,
+ .handle.fd = mem_fd,
+#endif
+ .size = evk->pltex->shared_mem.size,
+ .flags = 0,
+ };
+ ret = CHECK_CU(cu->cuImportExternalMemory(&evk->mem, &ext_desc));
+ if (ret < 0)
+ goto error;
+ // CUDA takes ownership of imported memory
+ mem_fd = -1;
+
+ CUarray_format cufmt;
+ switch (format->pixel_size / format->num_components) {
+ case 1:
+ cufmt = CU_AD_FORMAT_UNSIGNED_INT8;
+ break;
+ case 2:
+ cufmt = CU_AD_FORMAT_UNSIGNED_INT16;
+ break;
+ default:
+ ret = -1;
+ goto error;
+ }
+
+ CUDA_EXTERNAL_MEMORY_MIPMAPPED_ARRAY_DESC tex_desc = {
+ .offset = evk->pltex->shared_mem.offset,
+ .arrayDesc = {
+ .Width = mp_image_plane_w(&p->layout, n),
+ .Height = mp_image_plane_h(&p->layout, n),
+ .Depth = 0,
+ .Format = cufmt,
+ .NumChannels = format->num_components,
+ .Flags = 0,
+ },
+ .numLevels = 1,
+ };
+
+ ret = CHECK_CU(cu->cuExternalMemoryGetMappedMipmappedArray(&evk->mma, evk->mem, &tex_desc));
+ if (ret < 0)
+ goto error;
+
+ ret = CHECK_CU(cu->cuMipmappedArrayGetLevel(&p->cu_array[n], evk->mma, 0));
+ if (ret < 0)
+ goto error;
+
+ evk->vk_sem.sem = pl_vulkan_sem_create(gpu, pl_vulkan_sem_params(
+ .type = VK_SEMAPHORE_TYPE_TIMELINE,
+ .export_handle = HANDLE_TYPE,
+ .out_handle = &(evk->sem_handle),
+ ));
+ if (evk->vk_sem.sem == VK_NULL_HANDLE) {
+ ret = -1;
+ goto error;
+ }
+ // The returned FD or Handle is owned by the caller (us).
+
+ CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC w_desc = {
+#if HAVE_WIN32_DESKTOP
+ .type = CU_EXTERNAL_SEMAPHORE_HANDLE_TYPE_TIMELINE_SEMAPHORE_WIN32,
+ .handle.win32.handle = evk->sem_handle.handle,
+#else
+ .type = CU_EXTERNAL_SEMAPHORE_HANDLE_TYPE_TIMELINE_SEMAPHORE_FD,
+ .handle.fd = evk->sem_handle.fd,
+#endif
+ };
+ ret = CHECK_CU(cu->cuImportExternalSemaphore(&evk->cuda_sem, &w_desc));
+ if (ret < 0)
+ goto error;
+ // CUDA takes ownership of an imported FD *but not* an imported Handle.
+ evk->sem_handle.fd = -1;
+
+ return true;
+
+error:
+ MP_ERR(mapper, "cuda_ext_vk_init failed\n");
+ if (mem_fd > -1)
+ close(mem_fd);
+#if HAVE_WIN32_DESKTOP
+ if (evk->sem_handle.handle != NULL)
+ CloseHandle(evk->sem_handle.handle);
+#else
+ if (evk->sem_handle.fd > -1)
+ close(evk->sem_handle.fd);
+#endif
+ return false;
+}
+
+static void cuda_ext_vk_uninit(const struct ra_hwdec_mapper *mapper, int n)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CudaFunctions *cu = p_owner->cu;
+
+ struct ext_vk *evk = p->ext[n];
+ if (evk) {
+ if (evk->mma) {
+ CHECK_CU(cu->cuMipmappedArrayDestroy(evk->mma));
+ evk->mma = 0;
+ }
+ if (evk->mem) {
+ CHECK_CU(cu->cuDestroyExternalMemory(evk->mem));
+ evk->mem = 0;
+ }
+ if (evk->cuda_sem) {
+ CHECK_CU(cu->cuDestroyExternalSemaphore(evk->cuda_sem));
+ evk->cuda_sem = 0;
+ }
+ pl_vulkan_sem_destroy(ra_pl_get(mapper->ra), &evk->vk_sem.sem);
+#if HAVE_WIN32_DESKTOP
+ CloseHandle(evk->sem_handle.handle);
+#endif
+ }
+ talloc_free(evk);
+}
+
+static bool cuda_ext_vk_wait(const struct ra_hwdec_mapper *mapper, int n)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CudaFunctions *cu = p_owner->cu;
+ int ret;
+ struct ext_vk *evk = p->ext[n];
+
+ evk->vk_sem.value += 1;
+ ret = pl_vulkan_hold_ex(ra_pl_get(mapper->ra), pl_vulkan_hold_params(
+ .tex = evk->pltex,
+ .layout = VK_IMAGE_LAYOUT_GENERAL,
+ .qf = VK_QUEUE_FAMILY_EXTERNAL,
+ .semaphore = evk->vk_sem,
+ ));
+ if (!ret)
+ return false;
+
+ CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS wp = {
+ .params = {
+ .fence = {
+ .value = evk->vk_sem.value
+ }
+ }
+ };
+ ret = CHECK_CU(cu->cuWaitExternalSemaphoresAsync(&evk->cuda_sem,
+ &wp, 1, 0));
+ return ret == 0;
+}
+
+static bool cuda_ext_vk_signal(const struct ra_hwdec_mapper *mapper, int n)
+{
+ struct cuda_hw_priv *p_owner = mapper->owner->priv;
+ struct cuda_mapper_priv *p = mapper->priv;
+ CudaFunctions *cu = p_owner->cu;
+ int ret;
+ struct ext_vk *evk = p->ext[n];
+
+ evk->vk_sem.value += 1;
+ CUDA_EXTERNAL_SEMAPHORE_SIGNAL_PARAMS sp = {
+ .params = {
+ .fence = {
+ .value = evk->vk_sem.value
+ }
+ }
+ };
+ ret = CHECK_CU(cu->cuSignalExternalSemaphoresAsync(&evk->cuda_sem,
+ &sp, 1, 0));
+ if (ret != 0)
+ return false;
+
+ pl_vulkan_release_ex(ra_pl_get(mapper->ra), pl_vulkan_release_params(
+ .tex = evk->pltex,
+ .layout = VK_IMAGE_LAYOUT_GENERAL,
+ .qf = VK_QUEUE_FAMILY_EXTERNAL,
+ .semaphore = evk->vk_sem,
+ ));
+ return ret == 0;
+}
+
+#undef CHECK_CU
+#define CHECK_CU(x) check_cu(hw, (x), #x)
+
+bool cuda_vk_init(const struct ra_hwdec *hw) {
+ int ret = 0;
+ int level = hw->probing ? MSGL_V : MSGL_ERR;
+ struct cuda_hw_priv *p = hw->priv;
+ CudaFunctions *cu = p->cu;
+
+ pl_gpu gpu = ra_pl_get(hw->ra_ctx->ra);
+ if (gpu != NULL) {
+ if (!(gpu->export_caps.tex & HANDLE_TYPE)) {
+ MP_VERBOSE(hw, "CUDA hwdec with Vulkan requires exportable texture memory of type 0x%X.\n",
+ HANDLE_TYPE);
+ return false;
+ } else if (!(gpu->export_caps.sync & HANDLE_TYPE)) {
+ MP_VERBOSE(hw, "CUDA hwdec with Vulkan requires exportable semaphores of type 0x%X.\n",
+ HANDLE_TYPE);
+ return false;
+ }
+ } else {
+ // This is not a Vulkan RA.
+ return false;
+ }
+
+ if (!cu->cuImportExternalMemory) {
+ MP_MSG(hw, level, "CUDA hwdec with Vulkan requires driver version 410.48 or newer.\n");
+ return false;
+ }
+
+ int device_count;
+ ret = CHECK_CU(cu->cuDeviceGetCount(&device_count));
+ if (ret < 0)
+ return false;
+
+ CUdevice display_dev = -1;
+ for (int i = 0; i < device_count; i++) {
+ CUdevice dev;
+ ret = CHECK_CU(cu->cuDeviceGet(&dev, i));
+ if (ret < 0)
+ continue;
+
+ CUuuid uuid;
+ ret = CHECK_CU(cu->cuDeviceGetUuid(&uuid, dev));
+ if (ret < 0)
+ continue;
+
+ if (memcmp(gpu->uuid, uuid.bytes, sizeof (gpu->uuid)) == 0) {
+ display_dev = dev;
+ break;
+ }
+ }
+
+ if (display_dev == -1) {
+ MP_MSG(hw, level, "Could not match Vulkan display device in CUDA.\n");
+ return false;
+ }
+
+ ret = CHECK_CU(cu->cuCtxCreate(&p->display_ctx, CU_CTX_SCHED_BLOCKING_SYNC,
+ display_dev));
+ if (ret < 0)
+ return false;
+
+ p->decode_ctx = p->display_ctx;
+
+ p->ext_init = cuda_ext_vk_init;
+ p->ext_uninit = cuda_ext_vk_uninit;
+ p->ext_wait = cuda_ext_vk_wait;
+ p->ext_signal = cuda_ext_vk_signal;
+
+ return true;
+}
+
diff --git a/video/out/hwdec/hwdec_drmprime.c b/video/out/hwdec/hwdec_drmprime.c
new file mode 100644
index 0000000..f7c6250
--- /dev/null
+++ b/video/out/hwdec/hwdec_drmprime.c
@@ -0,0 +1,294 @@
+/*
+ * 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 <fcntl.h>
+#include <stddef.h>
+#include <string.h>
+#include <assert.h>
+#include <unistd.h>
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_drm.h>
+#include <xf86drm.h>
+
+#include "config.h"
+
+#include "libmpv/render_gl.h"
+#include "options/m_config.h"
+#include "video/fmt-conversion.h"
+#include "video/out/drm_common.h"
+#include "video/out/gpu/hwdec.h"
+#include "video/out/hwdec/dmabuf_interop.h"
+
+extern const struct m_sub_options drm_conf;
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+ int *formats;
+
+ struct dmabuf_interop dmabuf_interop;
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ if (p->hwctx.driver_name)
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+}
+
+const static dmabuf_interop_init interop_inits[] = {
+#if HAVE_DMABUF_INTEROP_GL
+ dmabuf_interop_gl_init,
+#endif
+#if HAVE_VAAPI
+ dmabuf_interop_pl_init,
+#endif
+#if HAVE_DMABUF_WAYLAND
+ dmabuf_interop_wl_init,
+#endif
+ NULL
+};
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ for (int i = 0; interop_inits[i]; i++) {
+ if (interop_inits[i](hw, &p->dmabuf_interop)) {
+ break;
+ }
+ }
+
+ if (!p->dmabuf_interop.interop_map || !p->dmabuf_interop.interop_unmap) {
+ MP_VERBOSE(hw, "drmprime hwdec requires at least one dmabuf interop backend.\n");
+ return -1;
+ }
+
+ /*
+ * The drm_params resource is not provided when using X11 or Wayland, but
+ * there are extensions that supposedly provide this information from the
+ * drivers. Not properly documented. Of course.
+ */
+ mpv_opengl_drm_params_v2 *params = ra_get_native_resource(hw->ra_ctx->ra,
+ "drm_params_v2");
+
+ /*
+ * Respect drm_device option, so there is a way to control this when not
+ * using a DRM gpu context. If drm_params_v2 are present, they will already
+ * respect this option.
+ */
+ void *tmp = talloc_new(NULL);
+ struct drm_opts *drm_opts = mp_get_config_group(tmp, hw->global, &drm_conf);
+ const char *opt_path = drm_opts->device_path;
+
+ const char *device_path = params && params->render_fd > -1 ?
+ drmGetRenderDeviceNameFromFd(params->render_fd) :
+ opt_path ? opt_path : "/dev/dri/renderD128";
+ MP_VERBOSE(hw, "Using DRM device: %s\n", device_path);
+
+ int ret = av_hwdevice_ctx_create(&p->hwctx.av_device_ref,
+ AV_HWDEVICE_TYPE_DRM,
+ device_path, NULL, 0);
+ talloc_free(tmp);
+ if (ret != 0) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx: %s\n", av_err2str(ret));
+ return -1;
+ }
+
+ /*
+ * At the moment, there is no way to discover compatible formats
+ * from the hwdevice_ctx, and in fact the ffmpeg hwaccels hard-code
+ * formats too, so we're not missing out on anything.
+ */
+ int num_formats = 0;
+ MP_TARRAY_APPEND(p, p->formats, num_formats, IMGFMT_NV12);
+ MP_TARRAY_APPEND(p, p->formats, num_formats, IMGFMT_420P);
+ MP_TARRAY_APPEND(p, p->formats, num_formats, pixfmt2imgfmt(AV_PIX_FMT_NV16));
+ MP_TARRAY_APPEND(p, p->formats, num_formats, 0); // terminate it
+
+ p->hwctx.hw_imgfmt = IMGFMT_DRMPRIME;
+ p->hwctx.supported_formats = p->formats;
+ p->hwctx.driver_name = hw->driver->name;
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct dmabuf_interop_priv *p = mapper->priv;
+
+ p_owner->dmabuf_interop.interop_unmap(mapper);
+
+ if (p->surface_acquired) {
+ for (int n = 0; n < p->desc.nb_objects; n++) {
+ if (p->desc.objects[n].fd > -1)
+ close(p->desc.objects[n].fd);
+ }
+ p->surface_acquired = false;
+ }
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ if (p_owner->dmabuf_interop.interop_uninit) {
+ p_owner->dmabuf_interop.interop_uninit(mapper);
+ }
+}
+
+static bool check_fmt(struct ra_hwdec_mapper *mapper, int fmt)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ for (int n = 0; p_owner->formats && p_owner->formats[n]; n++) {
+ if (p_owner->formats[n] == fmt)
+ return true;
+ }
+ return false;
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct dmabuf_interop_priv *p = mapper->priv;
+
+ mapper->dst_params = mapper->src_params;
+
+ /*
+ * rpi4_8 and rpi4_10 function identically to NV12. These two pixel
+ * formats however are not defined in upstream ffmpeg so a string
+ * comparison is used to identify them instead of a mpv IMGFMT.
+ */
+ const char* fmt_name = mp_imgfmt_to_name(mapper->src_params.hw_subfmt);
+ if (strcmp(fmt_name, "rpi4_8") == 0 || strcmp(fmt_name, "rpi4_10") == 0)
+ mapper->dst_params.imgfmt = IMGFMT_NV12;
+ else
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ struct ra_imgfmt_desc desc = {0};
+
+ if (mapper->ra->num_formats &&
+ !ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &desc))
+ return -1;
+
+ p->num_planes = desc.num_planes;
+ mp_image_set_params(&p->layout, &mapper->dst_params);
+
+ if (p_owner->dmabuf_interop.interop_init)
+ if (!p_owner->dmabuf_interop.interop_init(mapper, &desc))
+ return -1;
+
+ if (!check_fmt(mapper, mapper->dst_params.imgfmt))
+ {
+ MP_FATAL(mapper, "unsupported DRM image format %s\n",
+ mp_imgfmt_to_name(mapper->dst_params.imgfmt));
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct dmabuf_interop_priv *p = mapper->priv;
+
+ /*
+ * Although we use the same AVDRMFrameDescriptor to hold the dmabuf
+ * properties, we additionally need to dup the fds to ensure the
+ * frame doesn't disappear out from under us. And then for clarity,
+ * we copy all the individual fields.
+ */
+ const AVDRMFrameDescriptor *desc = (AVDRMFrameDescriptor *)mapper->src->planes[0];
+ p->desc.nb_layers = desc->nb_layers;
+ p->desc.nb_objects = desc->nb_objects;
+ for (int i = 0; i < desc->nb_layers; i++) {
+ p->desc.layers[i].format = desc->layers[i].format;
+ p->desc.layers[i].nb_planes = desc->layers[i].nb_planes;
+ for (int j = 0; j < desc->layers[i].nb_planes; j++) {
+ p->desc.layers[i].planes[j].object_index = desc->layers[i].planes[j].object_index;
+ p->desc.layers[i].planes[j].offset = desc->layers[i].planes[j].offset;
+ p->desc.layers[i].planes[j].pitch = desc->layers[i].planes[j].pitch;
+ }
+ }
+ for (int i = 0; i < desc->nb_objects; i++) {
+ p->desc.objects[i].format_modifier = desc->objects[i].format_modifier;
+ p->desc.objects[i].size = desc->objects[i].size;
+ // Initialise fds to -1 to make partial failure cleanup easier.
+ p->desc.objects[i].fd = -1;
+ }
+ // Surface is now safe to treat as acquired to allow for unmapping to run.
+ p->surface_acquired = true;
+
+ // Now actually dup the fds
+ for (int i = 0; i < desc->nb_objects; i++) {
+ p->desc.objects[i].fd = fcntl(desc->objects[i].fd, F_DUPFD_CLOEXEC, 0);
+ if (p->desc.objects[i].fd == -1) {
+ MP_ERR(mapper, "Failed to duplicate dmabuf fd: %s\n",
+ mp_strerror(errno));
+ goto err;
+ }
+ }
+
+ // We can handle composed formats if the total number of planes is still
+ // equal the number of planes we expect. Complex formats with auxiliary
+ // planes cannot be supported.
+
+ int num_returned_planes = 0;
+ for (int i = 0; i < p->desc.nb_layers; i++) {
+ num_returned_planes += p->desc.layers[i].nb_planes;
+ }
+
+ if (p->num_planes != 0 && p->num_planes != num_returned_planes) {
+ MP_ERR(mapper,
+ "Mapped surface with format '%s' has unexpected number of planes. "
+ "(%d layers and %d planes, but expected %d planes)\n",
+ mp_imgfmt_to_name(mapper->src->params.hw_subfmt),
+ p->desc.nb_layers, num_returned_planes, p->num_planes);
+ goto err;
+ }
+
+ if (!p_owner->dmabuf_interop.interop_map(mapper, &p_owner->dmabuf_interop,
+ false))
+ goto err;
+
+ return 0;
+
+err:
+ mapper_unmap(mapper);
+
+ MP_FATAL(mapper, "mapping DRM dmabuf failed\n");
+ return -1;
+}
+
+const struct ra_hwdec_driver ra_hwdec_drmprime = {
+ .name = "drmprime",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_DRMPRIME, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct dmabuf_interop_priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/hwdec/hwdec_drmprime_overlay.c b/video/out/hwdec/hwdec_drmprime_overlay.c
new file mode 100644
index 0000000..6b6aae6
--- /dev/null
+++ b/video/out/hwdec/hwdec_drmprime_overlay.c
@@ -0,0 +1,334 @@
+/*
+ * 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 <assert.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <stdbool.h>
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_drm.h>
+
+#include "video/hwdec.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "libmpv/render_gl.h"
+#include "video/out/drm_atomic.h"
+#include "video/out/drm_common.h"
+#include "video/out/drm_prime.h"
+#include "video/out/gpu/hwdec.h"
+#include "video/mp_image.h"
+
+extern const struct m_sub_options drm_conf;
+
+struct drm_frame {
+ struct drm_prime_framebuffer fb;
+ struct mp_image *image; // associated mpv image
+};
+
+struct priv {
+ struct mp_log *log;
+ struct mp_hwdec_ctx hwctx;
+
+ struct mp_image_params params;
+
+ struct drm_atomic_context *ctx;
+ struct drm_frame current_frame, last_frame, old_frame;
+
+ struct mp_rect src, dst;
+
+ int display_w, display_h;
+
+ struct drm_prime_handle_refs handle_refs;
+};
+
+static void set_current_frame(struct ra_hwdec *hw, struct drm_frame *frame)
+{
+ struct priv *p = hw->priv;
+
+ // frame will be on screen after next vsync
+ // current_frame is currently the displayed frame and will be replaced
+ // by frame after next vsync.
+ // We used old frame as triple buffering to make sure that the drm framebuffer
+ // is not being displayed when we release it.
+
+ if (p->ctx) {
+ drm_prime_destroy_framebuffer(p->log, p->ctx->fd, &p->old_frame.fb, &p->handle_refs);
+ }
+
+ mp_image_setrefp(&p->old_frame.image, p->last_frame.image);
+ p->old_frame.fb = p->last_frame.fb;
+
+ mp_image_setrefp(&p->last_frame.image, p->current_frame.image);
+ p->last_frame.fb = p->current_frame.fb;
+
+ if (frame) {
+ p->current_frame.fb = frame->fb;
+ mp_image_setrefp(&p->current_frame.image, frame->image);
+ } else {
+ memset(&p->current_frame.fb, 0, sizeof(p->current_frame.fb));
+ mp_image_setrefp(&p->current_frame.image, NULL);
+ }
+}
+
+static void scale_dst_rect(struct ra_hwdec *hw, int source_w, int source_h ,struct mp_rect *src, struct mp_rect *dst)
+{
+ struct priv *p = hw->priv;
+
+ // drm can allow to have a layer that has a different size from framebuffer
+ // we scale here the destination size to video mode
+ double hratio = p->display_w / (double)source_w;
+ double vratio = p->display_h / (double)source_h;
+ double ratio = hratio <= vratio ? hratio : vratio;
+
+ dst->x0 = src->x0 * ratio;
+ dst->x1 = src->x1 * ratio;
+ dst->y0 = src->y0 * ratio;
+ dst->y1 = src->y1 * ratio;
+
+ int offset_x = (p->display_w - ratio * source_w) / 2;
+ int offset_y = (p->display_h - ratio * source_h) / 2;
+
+ dst->x0 += offset_x;
+ dst->x1 += offset_x;
+ dst->y0 += offset_y;
+ dst->y1 += offset_y;
+}
+
+static void disable_video_plane(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+ if (!p->ctx)
+ return;
+
+ if (!p->ctx->drmprime_video_plane)
+ return;
+
+ // Disabling the drmprime video plane is needed on some devices when using
+ // the primary plane for video. Primary buffer can't be active with no
+ // framebuffer associated. So we need this function to commit it right away
+ // as mpv will free all framebuffers on playback end.
+ drmModeAtomicReqPtr request = drmModeAtomicAlloc();
+ if (request) {
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "FB_ID", 0);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "CRTC_ID", 0);
+
+ int ret = drmModeAtomicCommit(p->ctx->fd, request,
+ 0, NULL);
+
+ if (ret)
+ MP_ERR(hw, "Failed to commit disable plane request (code %d)", ret);
+ drmModeAtomicFree(request);
+ }
+}
+
+static int overlay_frame(struct ra_hwdec *hw, struct mp_image *hw_image,
+ struct mp_rect *src, struct mp_rect *dst, bool newframe)
+{
+ struct priv *p = hw->priv;
+ AVDRMFrameDescriptor *desc = NULL;
+ drmModeAtomicReq *request = NULL;
+ struct drm_frame next_frame = {0};
+ int ret;
+
+ struct ra *ra = hw->ra_ctx->ra;
+
+ // grab atomic request from native resources
+ if (p->ctx) {
+ struct mpv_opengl_drm_params_v2 *drm_params;
+ drm_params = (mpv_opengl_drm_params_v2 *)ra_get_native_resource(ra, "drm_params_v2");
+ if (!drm_params) {
+ MP_ERR(hw, "Failed to retrieve drm params from native resources\n");
+ return -1;
+ }
+ if (drm_params->atomic_request_ptr) {
+ request = *drm_params->atomic_request_ptr;
+ } else {
+ MP_ERR(hw, "drm params pointer to atomic request is invalid\n");
+ return -1;
+ }
+ }
+
+ if (hw_image) {
+
+ // grab draw plane windowing info to eventually upscale the overlay
+ // as egl windows could be upscaled to draw plane.
+ struct mpv_opengl_drm_draw_surface_size *draw_surface_size = ra_get_native_resource(ra, "drm_draw_surface_size");
+ if (draw_surface_size) {
+ scale_dst_rect(hw, draw_surface_size->width, draw_surface_size->height, dst, &p->dst);
+ } else {
+ p->dst = *dst;
+ }
+ p->src = *src;
+
+ next_frame.image = hw_image;
+ desc = (AVDRMFrameDescriptor *)hw_image->planes[0];
+
+ if (desc) {
+ int srcw = p->src.x1 - p->src.x0;
+ int srch = p->src.y1 - p->src.y0;
+ int dstw = MP_ALIGN_UP(p->dst.x1 - p->dst.x0, 2);
+ int dsth = MP_ALIGN_UP(p->dst.y1 - p->dst.y0, 2);
+
+ if (drm_prime_create_framebuffer(p->log, p->ctx->fd, desc, srcw, srch, &next_frame.fb, &p->handle_refs)) {
+ ret = -1;
+ goto fail;
+ }
+
+ if (request) {
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "FB_ID", next_frame.fb.fb_id);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "CRTC_ID", p->ctx->crtc->id);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "SRC_X", p->src.x0 << 16);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "SRC_Y", p->src.y0 << 16);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "SRC_W", srcw << 16);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "SRC_H", srch << 16);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "CRTC_X", MP_ALIGN_DOWN(p->dst.x0, 2));
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "CRTC_Y", MP_ALIGN_DOWN(p->dst.y0, 2));
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "CRTC_W", dstw);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "CRTC_H", dsth);
+ drm_object_set_property(request, p->ctx->drmprime_video_plane, "ZPOS", 0);
+ } else {
+ ret = drmModeSetPlane(p->ctx->fd, p->ctx->drmprime_video_plane->id, p->ctx->crtc->id, next_frame.fb.fb_id, 0,
+ MP_ALIGN_DOWN(p->dst.x0, 2), MP_ALIGN_DOWN(p->dst.y0, 2), dstw, dsth,
+ p->src.x0 << 16, p->src.y0 << 16 , srcw << 16, srch << 16);
+ if (ret < 0) {
+ MP_ERR(hw, "Failed to set the drmprime video plane %d (buffer %d).\n",
+ p->ctx->drmprime_video_plane->id, next_frame.fb.fb_id);
+ goto fail;
+ }
+ }
+ }
+ } else {
+ disable_video_plane(hw);
+
+ while (p->old_frame.fb.fb_id)
+ set_current_frame(hw, NULL);
+ }
+
+ set_current_frame(hw, &next_frame);
+ return 0;
+
+ fail:
+ drm_prime_destroy_framebuffer(p->log, p->ctx->fd, &next_frame.fb, &p->handle_refs);
+ return ret;
+}
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+
+ disable_video_plane(hw);
+ set_current_frame(hw, NULL);
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+
+ if (p->ctx) {
+ drm_atomic_destroy_context(p->ctx);
+ p->ctx = NULL;
+ }
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+ int draw_plane, drmprime_video_plane;
+
+ p->log = hw->log;
+
+ void *tmp = talloc_new(NULL);
+ struct drm_opts *opts = mp_get_config_group(tmp, hw->global, &drm_conf);
+ draw_plane = opts->draw_plane;
+ drmprime_video_plane = opts->drmprime_video_plane;
+ talloc_free(tmp);
+
+ struct mpv_opengl_drm_params_v2 *drm_params;
+
+ drm_params = ra_get_native_resource(hw->ra_ctx->ra, "drm_params_v2");
+ if (drm_params) {
+ p->ctx = drm_atomic_create_context(p->log, drm_params->fd, drm_params->crtc_id,
+ drm_params->connector_id, draw_plane, drmprime_video_plane);
+ if (!p->ctx) {
+ mp_err(p->log, "Failed to retrieve DRM atomic context.\n");
+ goto err;
+ }
+ if (!p->ctx->drmprime_video_plane) {
+ mp_warn(p->log, "No drmprime video plane. You might need to specify it manually using --drm-drmprime-video-plane\n");
+ goto err;
+ }
+ } else {
+ mp_verbose(p->log, "Failed to retrieve DRM fd from native display.\n");
+ goto err;
+ }
+
+ drmModeCrtcPtr crtc;
+ crtc = drmModeGetCrtc(p->ctx->fd, p->ctx->crtc->id);
+ if (crtc) {
+ p->display_w = crtc->mode.hdisplay;
+ p->display_h = crtc->mode.vdisplay;
+ drmModeFreeCrtc(crtc);
+ }
+
+ uint64_t has_prime;
+ if (drmGetCap(p->ctx->fd, DRM_CAP_PRIME, &has_prime) < 0) {
+ MP_ERR(hw, "Card does not support prime handles.\n");
+ goto err;
+ }
+
+ if (has_prime) {
+ drm_prime_init_handle_ref_count(p, &p->handle_refs);
+ }
+
+ disable_video_plane(hw);
+
+ p->hwctx = (struct mp_hwdec_ctx) {
+ .driver_name = hw->driver->name,
+ .hw_imgfmt = IMGFMT_DRMPRIME,
+ };
+
+ char *device = drmGetDeviceNameFromFd2(p->ctx->fd);
+ int ret = av_hwdevice_ctx_create(&p->hwctx.av_device_ref,
+ AV_HWDEVICE_TYPE_DRM, device, NULL, 0);
+
+ if (device)
+ free(device);
+
+ if (ret != 0) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx: %s\n", av_err2str(ret));
+ goto err;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ return 0;
+
+err:
+ uninit(hw);
+ return -1;
+}
+
+const struct ra_hwdec_driver ra_hwdec_drmprime_overlay = {
+ .name = "drmprime-overlay",
+ .priv_size = sizeof(struct priv),
+ .imgfmts = {IMGFMT_DRMPRIME, 0},
+ .init = init,
+ .overlay_frame = overlay_frame,
+ .uninit = uninit,
+};
diff --git a/video/out/hwdec/hwdec_ios_gl.m b/video/out/hwdec/hwdec_ios_gl.m
new file mode 100644
index 0000000..633cc3d
--- /dev/null
+++ b/video/out/hwdec/hwdec_ios_gl.m
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2013 Stefano Pigozzi <stefano.pigozzi@gmail.com>
+ * 2017 Aman Gupta <ffmpeg@tmm1.net>
+ *
+ * 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 <assert.h>
+
+#include <CoreVideo/CoreVideo.h>
+#include <OpenGLES/EAGL.h>
+
+#include <libavutil/hwcontext.h>
+
+#include "video/out/gpu/hwdec.h"
+#include "video/mp_image_pool.h"
+#include "video/out/opengl/ra_gl.h"
+#include "hwdec_vt.h"
+
+static bool check_hwdec(const struct ra_hwdec *hw)
+{
+ if (!ra_is_gl(hw->ra_ctx->ra))
+ return false;
+
+ GL *gl = ra_gl_get(hw->ra_ctx->ra);
+ if (gl->es < 200) {
+ MP_ERR(hw, "need OpenGLES 2.0 for CVOpenGLESTextureCacheCreateTextureFromImage()\n");
+ return false;
+ }
+
+ if ([EAGLContext currentContext] == nil) {
+ MP_ERR(hw, "need a current EAGLContext set\n");
+ return false;
+ }
+
+ return true;
+}
+
+// In GLES3 mode, CVOpenGLESTextureCacheCreateTextureFromImage()
+// will return error -6683 unless invoked with GL_LUMINANCE and
+// GL_LUMINANCE_ALPHA (http://stackoverflow.com/q/36213994/332798)
+// If a format trues to use GL_RED/GL_RG instead, try to find a format
+// that uses GL_LUMINANCE[_ALPHA] instead.
+static const struct ra_format *find_la_variant(struct ra *ra,
+ const struct ra_format *fmt)
+{
+ GLint internal_format;
+ GLenum format;
+ GLenum type;
+ ra_gl_get_format(fmt, &internal_format, &format, &type);
+
+ if (format == GL_RED) {
+ format = internal_format = GL_LUMINANCE;
+ } else if (format == GL_RG) {
+ format = internal_format = GL_LUMINANCE_ALPHA;
+ } else {
+ return fmt;
+ }
+
+ for (int n = 0; n < ra->num_formats; n++) {
+ const struct ra_format *fmt2 = ra->formats[n];
+ GLint internal_format2;
+ GLenum format2;
+ GLenum type2;
+ ra_gl_get_format(fmt2, &internal_format2, &format2, &type2);
+ if (internal_format2 == internal_format &&
+ format2 == format && type2 == type)
+ return fmt2;
+ }
+
+ return NULL;
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ for (int n = 0; n < p->desc.num_planes; n++) {
+ p->desc.planes[n] = find_la_variant(mapper->ra, p->desc.planes[n]);
+ if (!p->desc.planes[n] || p->desc.planes[n]->ctype != RA_CTYPE_UNORM) {
+ MP_ERR(mapper, "Format unsupported.\n");
+ return -1;
+ }
+ }
+
+ CVReturn err = CVOpenGLESTextureCacheCreate(
+ kCFAllocatorDefault,
+ NULL,
+ [EAGLContext currentContext],
+ NULL,
+ &p->gl_texture_cache);
+
+ if (err != noErr) {
+ MP_ERR(mapper, "Failure in CVOpenGLESTextureCacheCreate: %d\n", err);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ for (int i = 0; i < p->desc.num_planes; i++) {
+ ra_tex_free(mapper->ra, &mapper->tex[i]);
+ if (p->gl_planes[i]) {
+ CFRelease(p->gl_planes[i]);
+ p->gl_planes[i] = NULL;
+ }
+ }
+
+ CVOpenGLESTextureCacheFlush(p->gl_texture_cache, 0);
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ CVPixelBufferRelease(p->pbuf);
+ p->pbuf = (CVPixelBufferRef)mapper->src->planes[3];
+ CVPixelBufferRetain(p->pbuf);
+
+ const bool planar = CVPixelBufferIsPlanar(p->pbuf);
+ const int planes = CVPixelBufferGetPlaneCount(p->pbuf);
+ assert((planar && planes == p->desc.num_planes) || p->desc.num_planes == 1);
+
+ for (int i = 0; i < p->desc.num_planes; i++) {
+ const struct ra_format *fmt = p->desc.planes[i];
+
+ GLint internal_format;
+ GLenum format;
+ GLenum type;
+ ra_gl_get_format(fmt, &internal_format, &format, &type);
+
+ CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage(
+ kCFAllocatorDefault,
+ p->gl_texture_cache,
+ p->pbuf,
+ NULL,
+ GL_TEXTURE_2D,
+ internal_format,
+ CVPixelBufferGetWidthOfPlane(p->pbuf, i),
+ CVPixelBufferGetHeightOfPlane(p->pbuf, i),
+ format,
+ type,
+ i,
+ &p->gl_planes[i]);
+
+ if (err != noErr) {
+ MP_ERR(mapper, "error creating texture for plane %d: %d\n", i, err);
+ return -1;
+ }
+
+ gl->BindTexture(GL_TEXTURE_2D, CVOpenGLESTextureGetName(p->gl_planes[i]));
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = CVPixelBufferGetWidthOfPlane(p->pbuf, i),
+ .h = CVPixelBufferGetHeightOfPlane(p->pbuf, i),
+ .d = 1,
+ .format = fmt,
+ .render_src = true,
+ .src_linear = true,
+ };
+
+ mapper->tex[i] = ra_create_wrapped_tex(
+ mapper->ra,
+ &params,
+ CVOpenGLESTextureGetName(p->gl_planes[i])
+ );
+ if (!mapper->tex[i])
+ return -1;
+ }
+
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ CVPixelBufferRelease(p->pbuf);
+ if (p->gl_texture_cache) {
+ CFRelease(p->gl_texture_cache);
+ p->gl_texture_cache = NULL;
+ }
+}
+
+bool vt_gl_init(const struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ if (!check_hwdec(hw))
+ return false;
+
+ p->interop_init = mapper_init;
+ p->interop_uninit = mapper_uninit;
+ p->interop_map = mapper_map;
+ p->interop_unmap = mapper_unmap;
+
+ return true;
+}
diff --git a/video/out/hwdec/hwdec_mac_gl.c b/video/out/hwdec/hwdec_mac_gl.c
new file mode 100644
index 0000000..b73f5b9
--- /dev/null
+++ b/video/out/hwdec/hwdec_mac_gl.c
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2013 Stefano Pigozzi <stefano.pigozzi@gmail.com>
+ *
+ * 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 <assert.h>
+
+#include <IOSurface/IOSurface.h>
+#include <CoreVideo/CoreVideo.h>
+#include <OpenGL/OpenGL.h>
+#include <OpenGL/CGLIOSurface.h>
+
+#include <libavutil/hwcontext.h>
+
+#include "video/mp_image_pool.h"
+#include "video/out/gpu/hwdec.h"
+#include "video/out/opengl/ra_gl.h"
+#include "hwdec_vt.h"
+
+static bool check_hwdec(const struct ra_hwdec *hw)
+{
+ if (!ra_is_gl(hw->ra_ctx->ra))
+ return false;
+
+ GL *gl = ra_gl_get(hw->ra_ctx->ra);
+ if (gl->version < 300) {
+ MP_ERR(hw, "need >= OpenGL 3.0 for core rectangle texture support\n");
+ return false;
+ }
+
+ if (!CGLGetCurrentContext()) {
+ MP_ERR(hw, "need cocoa opengl backend to be active");
+ return false;
+ }
+
+ return true;
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ gl->GenTextures(MP_MAX_PLANES, p->gl_planes);
+
+ for (int n = 0; n < p->desc.num_planes; n++) {
+ if (p->desc.planes[n]->ctype != RA_CTYPE_UNORM) {
+ MP_ERR(mapper, "Format unsupported.\n");
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ // Is this sane? No idea how to release the texture without deleting it.
+ CVPixelBufferRelease(p->pbuf);
+ p->pbuf = NULL;
+
+ for (int i = 0; i < p->desc.num_planes; i++)
+ ra_tex_free(mapper->ra, &mapper->tex[i]);
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ CVPixelBufferRelease(p->pbuf);
+ p->pbuf = (CVPixelBufferRef)mapper->src->planes[3];
+ CVPixelBufferRetain(p->pbuf);
+ IOSurfaceRef surface = CVPixelBufferGetIOSurface(p->pbuf);
+ if (!surface) {
+ MP_ERR(mapper, "CVPixelBuffer has no IOSurface\n");
+ return -1;
+ }
+
+ const bool planar = CVPixelBufferIsPlanar(p->pbuf);
+ const int planes = CVPixelBufferGetPlaneCount(p->pbuf);
+ assert((planar && planes == p->desc.num_planes) || p->desc.num_planes == 1);
+
+ GLenum gl_target = GL_TEXTURE_RECTANGLE;
+
+ for (int i = 0; i < p->desc.num_planes; i++) {
+ const struct ra_format *fmt = p->desc.planes[i];
+
+ GLint internal_format;
+ GLenum format;
+ GLenum type;
+ ra_gl_get_format(fmt, &internal_format, &format, &type);
+
+ gl->BindTexture(gl_target, p->gl_planes[i]);
+
+ CGLError err = CGLTexImageIOSurface2D(
+ CGLGetCurrentContext(), gl_target,
+ internal_format,
+ IOSurfaceGetWidthOfPlane(surface, i),
+ IOSurfaceGetHeightOfPlane(surface, i),
+ format, type, surface, i);
+
+ gl->BindTexture(gl_target, 0);
+
+ if (err != kCGLNoError) {
+ MP_ERR(mapper,
+ "error creating IOSurface texture for plane %d: %s (%x)\n",
+ i, CGLErrorString(err), gl->GetError());
+ return -1;
+ }
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = IOSurfaceGetWidthOfPlane(surface, i),
+ .h = IOSurfaceGetHeightOfPlane(surface, i),
+ .d = 1,
+ .format = fmt,
+ .render_src = true,
+ .src_linear = true,
+ .non_normalized = gl_target == GL_TEXTURE_RECTANGLE,
+ };
+
+ mapper->tex[i] = ra_create_wrapped_tex(mapper->ra, &params,
+ p->gl_planes[i]);
+ if (!mapper->tex[i])
+ return -1;
+ }
+
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ gl->DeleteTextures(MP_MAX_PLANES, p->gl_planes);
+}
+
+bool vt_gl_init(const struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ if (!check_hwdec(hw))
+ return false;
+
+ p->interop_init = mapper_init;
+ p->interop_uninit = mapper_uninit;
+ p->interop_map = mapper_map;
+ p->interop_unmap = mapper_unmap;
+
+ return true;
+}
diff --git a/video/out/hwdec/hwdec_vaapi.c b/video/out/hwdec/hwdec_vaapi.c
new file mode 100644
index 0000000..d8a4517
--- /dev/null
+++ b/video/out/hwdec/hwdec_vaapi.c
@@ -0,0 +1,557 @@
+/*
+ * 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 <stddef.h>
+#include <string.h>
+#include <assert.h>
+#include <unistd.h>
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_vaapi.h>
+#include <va/va_drmcommon.h>
+
+#include "config.h"
+
+#include "video/out/gpu/hwdec.h"
+#include "video/out/hwdec/dmabuf_interop.h"
+#include "video/fmt-conversion.h"
+#include "video/mp_image_pool.h"
+#include "video/vaapi.h"
+
+#if HAVE_VAAPI_DRM
+#include "libmpv/render_gl.h"
+#endif
+
+#if HAVE_VAAPI_X11
+#include <va/va_x11.h>
+
+static VADisplay *create_x11_va_display(struct ra *ra)
+{
+ Display *x11 = ra_get_native_resource(ra, "x11");
+ return x11 ? vaGetDisplay(x11) : NULL;
+}
+#endif
+
+#if HAVE_VAAPI_WAYLAND
+#include <va/va_wayland.h>
+
+static VADisplay *create_wayland_va_display(struct ra *ra)
+{
+ struct wl_display *wl = ra_get_native_resource(ra, "wl");
+
+ return wl ? vaGetDisplayWl(wl) : NULL;
+}
+#endif
+
+#if HAVE_VAAPI_DRM
+#include <va/va_drm.h>
+
+static VADisplay *create_drm_va_display(struct ra *ra)
+{
+ mpv_opengl_drm_params_v2 *params = ra_get_native_resource(ra, "drm_params_v2");
+ if (!params || params->render_fd == -1)
+ return NULL;
+
+ return vaGetDisplayDRM(params->render_fd);
+}
+#endif
+
+struct va_create_native {
+ const char *name;
+ VADisplay *(*create)(struct ra *ra);
+};
+
+static const struct va_create_native create_native_cbs[] = {
+#if HAVE_VAAPI_X11
+ {"x11", create_x11_va_display},
+#endif
+#if HAVE_VAAPI_WAYLAND
+ {"wayland", create_wayland_va_display},
+#endif
+#if HAVE_VAAPI_DRM
+ {"drm", create_drm_va_display},
+#endif
+};
+
+static VADisplay *create_native_va_display(struct ra *ra, struct mp_log *log)
+{
+ for (int n = 0; n < MP_ARRAY_SIZE(create_native_cbs); n++) {
+ const struct va_create_native *disp = &create_native_cbs[n];
+ mp_verbose(log, "Trying to open a %s VA display...\n", disp->name);
+ VADisplay *display = disp->create(ra);
+ if (display)
+ return display;
+ }
+ return NULL;
+}
+
+static void determine_working_formats(struct ra_hwdec *hw);
+
+struct priv_owner {
+ struct mp_vaapi_ctx *ctx;
+ VADisplay *display;
+ int *formats;
+ bool probing_formats; // temporary during init
+
+ struct dmabuf_interop dmabuf_interop;
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ if (p->ctx) {
+ hwdec_devices_remove(hw->devs, &p->ctx->hwctx);
+ if (p->ctx->hwctx.conversion_config) {
+ AVVAAPIHWConfig *hwconfig = p->ctx->hwctx.conversion_config;
+ vaDestroyConfig(p->ctx->display, hwconfig->config_id);
+ av_freep(&p->ctx->hwctx.conversion_config);
+ }
+ }
+ va_destroy(p->ctx);
+}
+
+const static dmabuf_interop_init interop_inits[] = {
+#if HAVE_DMABUF_INTEROP_GL
+ dmabuf_interop_gl_init,
+#endif
+ dmabuf_interop_pl_init,
+#if HAVE_DMABUF_WAYLAND
+ dmabuf_interop_wl_init,
+#endif
+ NULL
+};
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ VAStatus vas;
+
+ for (int i = 0; interop_inits[i]; i++) {
+ if (interop_inits[i](hw, &p->dmabuf_interop)) {
+ break;
+ }
+ }
+
+ if (!p->dmabuf_interop.interop_map || !p->dmabuf_interop.interop_unmap) {
+ MP_VERBOSE(hw, "VAAPI hwdec only works with OpenGL or Vulkan backends.\n");
+ return -1;
+ }
+
+ p->display = create_native_va_display(hw->ra_ctx->ra, hw->log);
+ if (!p->display) {
+ MP_VERBOSE(hw, "Could not create a VA display.\n");
+ return -1;
+ }
+
+ p->ctx = va_initialize(p->display, hw->log, true);
+ if (!p->ctx) {
+ vaTerminate(p->display);
+ return -1;
+ }
+ if (!p->ctx->av_device_ref) {
+ MP_VERBOSE(hw, "libavutil vaapi code rejected the driver?\n");
+ return -1;
+ }
+
+ if (hw->probing && va_guess_if_emulated(p->ctx)) {
+ return -1;
+ }
+
+ determine_working_formats(hw);
+ if (!p->formats || !p->formats[0]) {
+ return -1;
+ }
+
+ VAConfigID config_id;
+ AVVAAPIHWConfig *hwconfig = NULL;
+ vas = vaCreateConfig(p->display, VAProfileNone, VAEntrypointVideoProc, NULL,
+ 0, &config_id);
+ if (vas == VA_STATUS_SUCCESS) {
+ hwconfig = av_hwdevice_hwconfig_alloc(p->ctx->av_device_ref);
+ hwconfig->config_id = config_id;
+ }
+
+ // it's now safe to set the display resource
+ ra_add_native_resource(hw->ra_ctx->ra, "VADisplay", p->display);
+
+ p->ctx->hwctx.hw_imgfmt = IMGFMT_VAAPI;
+ p->ctx->hwctx.supported_formats = p->formats;
+ p->ctx->hwctx.driver_name = hw->driver->name;
+ p->ctx->hwctx.conversion_filter_name = "scale_vaapi";
+ p->ctx->hwctx.conversion_config = hwconfig;
+ hwdec_devices_add(hw->devs, &p->ctx->hwctx);
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct dmabuf_interop_priv *p = mapper->priv;
+
+ p_owner->dmabuf_interop.interop_unmap(mapper);
+
+ if (p->surface_acquired) {
+ for (int n = 0; n < p->desc.nb_objects; n++)
+ close(p->desc.objects[n].fd);
+ p->surface_acquired = false;
+ }
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ if (p_owner->dmabuf_interop.interop_uninit) {
+ p_owner->dmabuf_interop.interop_uninit(mapper);
+ }
+}
+
+static bool check_fmt(struct ra_hwdec_mapper *mapper, int fmt)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ for (int n = 0; p_owner->formats && p_owner->formats[n]; n++) {
+ if (p_owner->formats[n] == fmt)
+ return true;
+ }
+ return false;
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct dmabuf_interop_priv *p = mapper->priv;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ struct ra_imgfmt_desc desc = {0};
+
+ if (mapper->ra->num_formats &&
+ !ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &desc))
+ return -1;
+
+ p->num_planes = desc.num_planes;
+ mp_image_set_params(&p->layout, &mapper->dst_params);
+
+ if (p_owner->dmabuf_interop.interop_init)
+ if (!p_owner->dmabuf_interop.interop_init(mapper, &desc))
+ return -1;
+
+ if (!p_owner->probing_formats && !check_fmt(mapper, mapper->dst_params.imgfmt))
+ {
+ MP_FATAL(mapper, "unsupported VA image format %s\n",
+ mp_imgfmt_to_name(mapper->dst_params.imgfmt));
+ return -1;
+ }
+
+ return 0;
+}
+
+static void close_file_descriptors(VADRMPRIMESurfaceDescriptor desc)
+{
+ for (int i = 0; i < desc.num_objects; i++)
+ close(desc.objects[i].fd);
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct dmabuf_interop_priv *p = mapper->priv;
+ VAStatus status;
+ VADisplay *display = p_owner->display;
+ VADRMPRIMESurfaceDescriptor desc = {0};
+
+ uint32_t flags = p_owner->dmabuf_interop.composed_layers ?
+ VA_EXPORT_SURFACE_COMPOSED_LAYERS : VA_EXPORT_SURFACE_SEPARATE_LAYERS;
+ status = vaExportSurfaceHandle(display, va_surface_id(mapper->src),
+ VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
+ VA_EXPORT_SURFACE_READ_ONLY |
+ flags,
+ &desc);
+ if (!CHECK_VA_STATUS_LEVEL(mapper, "vaExportSurfaceHandle()",
+ p_owner->probing_formats ? MSGL_DEBUG : MSGL_ERR))
+ {
+ close_file_descriptors(desc);
+ goto err;
+ }
+ vaSyncSurface(display, va_surface_id(mapper->src));
+ // No need to error out if sync fails, but good to know if it did.
+ CHECK_VA_STATUS(mapper, "vaSyncSurface()");
+ p->surface_acquired = true;
+
+ // We use AVDRMFrameDescriptor to store the dmabuf so we need to copy the
+ // values over.
+ int num_returned_planes = 0;
+ p->desc.nb_layers = desc.num_layers;
+ p->desc.nb_objects = desc.num_objects;
+ for (int i = 0; i < desc.num_layers; i++) {
+ p->desc.layers[i].format = desc.layers[i].drm_format;
+ p->desc.layers[i].nb_planes = desc.layers[i].num_planes;
+ for (int j = 0; j < desc.layers[i].num_planes; j++)
+ {
+ p->desc.layers[i].planes[j].object_index = desc.layers[i].object_index[j];
+ p->desc.layers[i].planes[j].offset = desc.layers[i].offset[j];
+ p->desc.layers[i].planes[j].pitch = desc.layers[i].pitch[j];
+ }
+
+ num_returned_planes += desc.layers[i].num_planes;
+ }
+ for (int i = 0; i < desc.num_objects; i++) {
+ p->desc.objects[i].format_modifier = desc.objects[i].drm_format_modifier;
+ p->desc.objects[i].fd = desc.objects[i].fd;
+ p->desc.objects[i].size = desc.objects[i].size;
+ }
+
+ // We can handle composed formats if the total number of planes is still
+ // equal the number of planes we expect. Complex formats with auxiliary
+ // planes cannot be supported.
+ if (p->num_planes != 0 && p->num_planes != num_returned_planes) {
+ mp_msg(mapper->log, p_owner->probing_formats ? MSGL_DEBUG : MSGL_ERR,
+ "Mapped surface with format '%s' has unexpected number of planes. "
+ "(%d layers and %d planes, but expected %d planes)\n",
+ mp_imgfmt_to_name(mapper->src->params.hw_subfmt),
+ desc.num_layers, num_returned_planes, p->num_planes);
+ goto err;
+ }
+
+ if (!p_owner->dmabuf_interop.interop_map(mapper, &p_owner->dmabuf_interop,
+ p_owner->probing_formats))
+ goto err;
+
+ if (desc.fourcc == VA_FOURCC_YV12)
+ MPSWAP(struct ra_tex*, mapper->tex[1], mapper->tex[2]);
+
+ return 0;
+
+err:
+ mapper_unmap(mapper);
+
+ if (!p_owner->probing_formats)
+ MP_FATAL(mapper, "mapping VAAPI EGL image failed\n");
+ return -1;
+}
+
+static bool try_format_map(struct ra_hwdec *hw, struct mp_image *surface)
+{
+ struct ra_hwdec_mapper *mapper = ra_hwdec_mapper_create(hw, &surface->params);
+ if (!mapper) {
+ MP_DBG(hw, "Failed to create mapper\n");
+ return false;
+ }
+
+ bool ok = ra_hwdec_mapper_map(mapper, surface) >= 0;
+ ra_hwdec_mapper_free(&mapper);
+ return ok;
+}
+
+static void try_format_pixfmt(struct ra_hwdec *hw, enum AVPixelFormat pixfmt)
+{
+ bool supported = false;
+ struct priv_owner *p = hw->priv;
+
+ int mp_fmt = pixfmt2imgfmt(pixfmt);
+ if (!mp_fmt)
+ return;
+
+ int num_formats = 0;
+ for (int n = 0; p->formats && p->formats[n]; n++) {
+ if (p->formats[n] == mp_fmt)
+ return; // already added
+ num_formats += 1;
+ }
+
+ AVBufferRef *fref = NULL;
+ struct mp_image *s = NULL;
+ AVFrame *frame = NULL;
+ fref = av_hwframe_ctx_alloc(p->ctx->av_device_ref);
+ if (!fref)
+ goto err;
+ AVHWFramesContext *fctx = (void *)fref->data;
+ fctx->format = AV_PIX_FMT_VAAPI;
+ fctx->sw_format = pixfmt;
+ fctx->width = 128;
+ fctx->height = 128;
+ if (av_hwframe_ctx_init(fref) < 0)
+ goto err;
+ frame = av_frame_alloc();
+ if (!frame)
+ goto err;
+ if (av_hwframe_get_buffer(fref, frame, 0) < 0)
+ goto err;
+ s = mp_image_from_av_frame(frame);
+ if (!s || !mp_image_params_valid(&s->params))
+ goto err;
+ if (try_format_map(hw, s)) {
+ supported = true;
+ MP_TARRAY_APPEND(p, p->formats, num_formats, mp_fmt);
+ MP_TARRAY_APPEND(p, p->formats, num_formats, 0); // terminate it
+ }
+err:
+ if (!supported)
+ MP_DBG(hw, "Unsupported format: %s\n",
+ mp_imgfmt_to_name(mp_fmt));
+
+ talloc_free(s);
+ av_frame_free(&frame);
+ av_buffer_unref(&fref);
+}
+
+static void try_format_config(struct ra_hwdec *hw, AVVAAPIHWConfig *hwconfig)
+{
+ struct priv_owner *p = hw->priv;
+ enum AVPixelFormat *fmts = NULL;
+
+ AVHWFramesConstraints *fc =
+ av_hwdevice_get_hwframe_constraints(p->ctx->av_device_ref, hwconfig);
+ if (!fc) {
+ MP_WARN(hw, "failed to retrieve libavutil frame constraints\n");
+ return;
+ }
+
+ /*
+ * We need a hwframe_ctx to be able to get the valid formats, but to
+ * initialise it, we need a format, so we get the first format from the
+ * hwconfig. We don't care about the other formats in the config because the
+ * transfer formats list will already include them.
+ */
+ AVBufferRef *fref = NULL;
+ fref = av_hwframe_ctx_alloc(p->ctx->av_device_ref);
+ if (!fref) {
+ MP_WARN(hw, "failed to alloc libavutil frame context\n");
+ goto err;
+ }
+ AVHWFramesContext *fctx = (void *)fref->data;
+ fctx->format = AV_PIX_FMT_VAAPI;
+ fctx->sw_format = fc->valid_sw_formats[0];
+ fctx->width = 128;
+ fctx->height = 128;
+ if (av_hwframe_ctx_init(fref) < 0) {
+ MP_WARN(hw, "failed to init libavutil frame context\n");
+ goto err;
+ }
+
+ int ret = av_hwframe_transfer_get_formats(fref, AV_HWFRAME_TRANSFER_DIRECTION_TO, &fmts, 0);
+ if (ret) {
+ MP_WARN(hw, "failed to get libavutil frame context supported formats\n");
+ goto err;
+ }
+
+ for (int n = 0; fmts &&
+ fmts[n] != AV_PIX_FMT_NONE; n++)
+ try_format_pixfmt(hw, fmts[n]);
+
+err:
+ av_hwframe_constraints_free(&fc);
+ av_buffer_unref(&fref);
+ av_free(fmts);
+}
+
+static void determine_working_formats(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ VAStatus status;
+ VAProfile *profiles = NULL;
+ VAEntrypoint *entrypoints = NULL;
+
+ MP_VERBOSE(hw, "Going to probe surface formats (may log bogus errors)...\n");
+ p->probing_formats = true;
+
+ AVVAAPIHWConfig *hwconfig = av_hwdevice_hwconfig_alloc(p->ctx->av_device_ref);
+ if (!hwconfig) {
+ MP_WARN(hw, "Could not allocate FFmpeg AVVAAPIHWConfig\n");
+ goto done;
+ }
+
+ profiles = talloc_zero_array(NULL, VAProfile, vaMaxNumProfiles(p->display));
+ entrypoints = talloc_zero_array(NULL, VAEntrypoint,
+ vaMaxNumEntrypoints(p->display));
+ int num_profiles = 0;
+ status = vaQueryConfigProfiles(p->display, profiles, &num_profiles);
+ if (!CHECK_VA_STATUS(hw, "vaQueryConfigProfiles()"))
+ num_profiles = 0;
+
+ /*
+ * We need to find one declared format to bootstrap probing. So find a valid
+ * decoding profile and use its config. If try_format_config() finds any
+ * formats, they will be all the supported formats, and we don't need to
+ * look at any other profiles.
+ */
+ for (int n = 0; n < num_profiles; n++) {
+ VAProfile profile = profiles[n];
+ if (profile == VAProfileNone) {
+ // We don't use the None profile.
+ continue;
+ }
+ int num_ep = 0;
+ status = vaQueryConfigEntrypoints(p->display, profile, entrypoints,
+ &num_ep);
+ if (status != VA_STATUS_SUCCESS) {
+ MP_DBG(hw, "vaQueryConfigEntrypoints(): '%s' for profile %d",
+ vaErrorStr(status), (int)profile);
+ continue;
+ }
+ for (int ep = 0; ep < num_ep; ep++) {
+ if (entrypoints[ep] != VAEntrypointVLD) {
+ // We are only interested in decoding entrypoints.
+ continue;
+ }
+ VAConfigID config = VA_INVALID_ID;
+ status = vaCreateConfig(p->display, profile, entrypoints[ep],
+ NULL, 0, &config);
+ if (status != VA_STATUS_SUCCESS) {
+ MP_DBG(hw, "vaCreateConfig(): '%s' for profile %d",
+ vaErrorStr(status), (int)profile);
+ continue;
+ }
+
+ hwconfig->config_id = config;
+ try_format_config(hw, hwconfig);
+
+ vaDestroyConfig(p->display, config);
+ if (p->formats && p->formats[0]) {
+ goto done;
+ }
+ }
+ }
+
+done:
+ av_free(hwconfig);
+ talloc_free(profiles);
+ talloc_free(entrypoints);
+
+ p->probing_formats = false;
+
+ MP_DBG(hw, "Supported formats:\n");
+ for (int n = 0; p->formats && p->formats[n]; n++)
+ MP_DBG(hw, " %s\n", mp_imgfmt_to_name(p->formats[n]));
+ MP_VERBOSE(hw, "Done probing surface formats.\n");
+}
+
+const struct ra_hwdec_driver ra_hwdec_vaapi = {
+ .name = "vaapi",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_VAAPI, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct dmabuf_interop_priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/hwdec/hwdec_vt.c b/video/out/hwdec/hwdec_vt.c
new file mode 100644
index 0000000..ab41d02
--- /dev/null
+++ b/video/out/hwdec/hwdec_vt.c
@@ -0,0 +1,141 @@
+/*
+ * 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 <stddef.h>
+#include <string.h>
+#include <assert.h>
+#include <unistd.h>
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_videotoolbox.h>
+
+#include "config.h"
+
+#include "video/out/gpu/hwdec.h"
+#include "video/out/hwdec/hwdec_vt.h"
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+}
+
+const static vt_interop_init interop_inits[] = {
+#if HAVE_VIDEOTOOLBOX_GL || HAVE_IOS_GL
+ vt_gl_init,
+#endif
+#if HAVE_VIDEOTOOLBOX_PL
+ vt_pl_init,
+#endif
+ NULL
+};
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ for (int i = 0; interop_inits[i]; i++) {
+ if (interop_inits[i](hw)) {
+ break;
+ }
+ }
+
+ if (!p->interop_map || !p->interop_unmap) {
+ MP_VERBOSE(hw, "VT hwdec only works with OpenGL or Vulkan backends.\n");
+ return -1;
+ }
+
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = hw->driver->name,
+ .hw_imgfmt = IMGFMT_VIDEOTOOLBOX,
+ };
+
+ int ret = av_hwdevice_ctx_create(&p->hwctx.av_device_ref,
+ AV_HWDEVICE_TYPE_VIDEOTOOLBOX, NULL, NULL, 0);
+ if (ret != 0) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx: %s\n", av_err2str(ret));
+ return -1;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+
+ p_owner->interop_unmap(mapper);
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ if (p_owner->interop_uninit) {
+ p_owner->interop_uninit(mapper);
+ }
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ if (!mapper->dst_params.imgfmt) {
+ MP_ERR(mapper, "Unsupported CVPixelBuffer format.\n");
+ return -1;
+ }
+
+ if (!ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &p->desc)) {
+ MP_ERR(mapper, "Unsupported texture format.\n");
+ return -1;
+ }
+
+ if (p_owner->interop_init)
+ return p_owner->interop_init(mapper);
+
+ return 0;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+
+ return p_owner->interop_map(mapper);
+}
+
+const struct ra_hwdec_driver ra_hwdec_videotoolbox = {
+ .name = "videotoolbox",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_VIDEOTOOLBOX, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/hwdec/hwdec_vt.h b/video/out/hwdec/hwdec_vt.h
new file mode 100644
index 0000000..b79c641
--- /dev/null
+++ b/video/out/hwdec/hwdec_vt.h
@@ -0,0 +1,63 @@
+/*
+ * 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/>.
+ */
+
+#pragma once
+
+#include <CoreVideo/CoreVideo.h>
+
+#include "config.h"
+#include "video/out/gpu/hwdec.h"
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+
+ int (*interop_init)(struct ra_hwdec_mapper *mapper);
+ void (*interop_uninit)(struct ra_hwdec_mapper *mapper);
+
+ int (*interop_map)(struct ra_hwdec_mapper *mapper);
+ void (*interop_unmap)(struct ra_hwdec_mapper *mapper);
+};
+
+#ifndef __OBJC__
+typedef struct __CVMetalTextureCache *CVMetalTextureCacheRef;
+typedef CVImageBufferRef CVMetalTextureRef;
+#endif
+
+struct priv {
+ void *interop_mapper_priv;
+
+ CVPixelBufferRef pbuf;
+
+#if HAVE_VIDEOTOOLBOX_GL
+ GLuint gl_planes[MP_MAX_PLANES];
+#elif HAVE_IOS_GL
+ CVOpenGLESTextureCacheRef gl_texture_cache;
+ CVOpenGLESTextureRef gl_planes[MP_MAX_PLANES];
+#endif
+
+#if HAVE_VIDEOTOOLBOX_PL
+ CVMetalTextureCacheRef mtl_texture_cache;
+ CVMetalTextureRef mtl_planes[MP_MAX_PLANES];
+#endif
+
+ struct ra_imgfmt_desc desc;
+};
+
+typedef bool (*vt_interop_init)(const struct ra_hwdec *hw);
+
+bool vt_gl_init(const struct ra_hwdec *hw);
+bool vt_pl_init(const struct ra_hwdec *hw);
diff --git a/video/out/hwdec/hwdec_vt_pl.m b/video/out/hwdec/hwdec_vt_pl.m
new file mode 100644
index 0000000..cd133a0
--- /dev/null
+++ b/video/out/hwdec/hwdec_vt_pl.m
@@ -0,0 +1,312 @@
+/*
+ * Copyright (c) 2013 Stefano Pigozzi <stefano.pigozzi@gmail.com>
+ * 2017 Aman Gupta <ffmpeg@tmm1.net>
+ * 2023 rcombs <rcombs@rcombs.me>
+ *
+ * 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 <assert.h>
+
+#include <CoreVideo/CoreVideo.h>
+#include <Metal/Metal.h>
+
+#include <libavutil/hwcontext.h>
+
+#include <libplacebo/renderer.h>
+
+#include "config.h"
+
+#include "video/out/gpu/hwdec.h"
+#include "video/out/placebo/ra_pl.h"
+#include "video/mp_image_pool.h"
+
+#if HAVE_VULKAN
+#include "video/out/vulkan/common.h"
+#endif
+
+#include "hwdec_vt.h"
+
+static bool check_hwdec(const struct ra_hwdec *hw)
+{
+ pl_gpu gpu = ra_pl_get(hw->ra_ctx->ra);
+ if (!gpu) {
+ // This is not a libplacebo RA;
+ return false;
+ }
+
+ if (!(gpu->import_caps.tex & PL_HANDLE_MTL_TEX)) {
+ MP_VERBOSE(hw, "VideoToolbox libplacebo interop requires support for "
+ "PL_HANDLE_MTL_TEX import.\n");
+ return false;
+ }
+
+ return true;
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ if (!mapper->dst_params.imgfmt) {
+ MP_ERR(mapper, "Unsupported CVPixelBuffer format.\n");
+ return -1;
+ }
+
+ if (!ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &p->desc)) {
+ MP_ERR(mapper, "Unsupported texture format.\n");
+ return -1;
+ }
+
+ for (int n = 0; n < p->desc.num_planes; n++) {
+ if (!p->desc.planes[n] || p->desc.planes[n]->ctype != RA_CTYPE_UNORM) {
+ MP_ERR(mapper, "Format unsupported.\n");
+ return -1;
+ }
+ }
+
+ id<MTLDevice> mtl_device = nil;
+
+#ifdef VK_EXT_METAL_OBJECTS_SPEC_VERSION
+ pl_gpu gpu = ra_pl_get(mapper->ra);
+ if (gpu) {
+ pl_vulkan vulkan = pl_vulkan_get(gpu);
+ if (vulkan && vulkan->device && vulkan->instance && vulkan->get_proc_addr) {
+ PFN_vkExportMetalObjectsEXT pExportMetalObjects = (PFN_vkExportMetalObjectsEXT)vulkan->get_proc_addr(vulkan->instance, "vkExportMetalObjectsEXT");
+ if (pExportMetalObjects) {
+ VkExportMetalDeviceInfoEXT device_info = {
+ .sType = VK_STRUCTURE_TYPE_EXPORT_METAL_DEVICE_INFO_EXT,
+ .pNext = NULL,
+ .mtlDevice = nil,
+ };
+
+ VkExportMetalObjectsInfoEXT objects_info = {
+ .sType = VK_STRUCTURE_TYPE_EXPORT_METAL_OBJECTS_INFO_EXT,
+ .pNext = &device_info,
+ };
+
+ pExportMetalObjects(vulkan->device, &objects_info);
+
+ mtl_device = device_info.mtlDevice;
+ [mtl_device retain];
+ }
+ }
+ }
+#endif
+
+ if (!mtl_device) {
+ mtl_device = MTLCreateSystemDefaultDevice();
+ }
+
+ CVReturn err = CVMetalTextureCacheCreate(
+ kCFAllocatorDefault,
+ NULL,
+ mtl_device,
+ NULL,
+ &p->mtl_texture_cache);
+
+ [mtl_device release];
+
+ if (err != noErr) {
+ MP_ERR(mapper, "Failure in CVOpenGLESTextureCacheCreate: %d\n", err);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ for (int i = 0; i < p->desc.num_planes; i++) {
+ ra_tex_free(mapper->ra, &mapper->tex[i]);
+ if (p->mtl_planes[i]) {
+ CFRelease(p->mtl_planes[i]);
+ p->mtl_planes[i] = NULL;
+ }
+ }
+
+ CVMetalTextureCacheFlush(p->mtl_texture_cache, 0);
+}
+
+static const struct {
+ const char *glsl;
+ MTLPixelFormat mtl;
+} mtl_fmts[] = {
+ {"r16f", MTLPixelFormatR16Float },
+ {"r32f", MTLPixelFormatR32Float },
+ {"rg16f", MTLPixelFormatRG16Float },
+ {"rg32f", MTLPixelFormatRG32Float },
+ {"rgba16f", MTLPixelFormatRGBA16Float },
+ {"rgba32f", MTLPixelFormatRGBA32Float },
+ {"r11f_g11f_b10f", MTLPixelFormatRG11B10Float },
+
+ {"r8", MTLPixelFormatR8Unorm },
+ {"r16", MTLPixelFormatR16Unorm },
+ {"rg8", MTLPixelFormatRG8Unorm },
+ {"rg16", MTLPixelFormatRG16Unorm },
+ {"rgba8", MTLPixelFormatRGBA8Unorm },
+ {"rgba16", MTLPixelFormatRGBA16Unorm },
+ {"rgb10_a2", MTLPixelFormatRGB10A2Unorm },
+
+ {"r8_snorm", MTLPixelFormatR8Snorm },
+ {"r16_snorm", MTLPixelFormatR16Snorm },
+ {"rg8_snorm", MTLPixelFormatRG8Snorm },
+ {"rg16_snorm", MTLPixelFormatRG16Snorm },
+ {"rgba8_snorm", MTLPixelFormatRGBA8Snorm },
+ {"rgba16_snorm", MTLPixelFormatRGBA16Snorm },
+
+ {"r8ui", MTLPixelFormatR8Uint },
+ {"r16ui", MTLPixelFormatR16Uint },
+ {"r32ui", MTLPixelFormatR32Uint },
+ {"rg8ui", MTLPixelFormatRG8Uint },
+ {"rg16ui", MTLPixelFormatRG16Uint },
+ {"rg32ui", MTLPixelFormatRG32Uint },
+ {"rgba8ui", MTLPixelFormatRGBA8Uint },
+ {"rgba16ui", MTLPixelFormatRGBA16Uint },
+ {"rgba32ui", MTLPixelFormatRGBA32Uint },
+ {"rgb10_a2ui", MTLPixelFormatRGB10A2Uint },
+
+ {"r8i", MTLPixelFormatR8Sint },
+ {"r16i", MTLPixelFormatR16Sint },
+ {"r32i", MTLPixelFormatR32Sint },
+ {"rg8i", MTLPixelFormatRG8Sint },
+ {"rg16i", MTLPixelFormatRG16Sint },
+ {"rg32i", MTLPixelFormatRG32Sint },
+ {"rgba8i", MTLPixelFormatRGBA8Sint },
+ {"rgba16i", MTLPixelFormatRGBA16Sint },
+ {"rgba32i", MTLPixelFormatRGBA32Sint },
+
+ { NULL, MTLPixelFormatInvalid },
+};
+
+static MTLPixelFormat get_mtl_fmt(const char* glsl)
+{
+ if (!glsl)
+ return MTLPixelFormatInvalid;
+
+ for (int i = 0; mtl_fmts[i].glsl; i++) {
+ if (!strcmp(glsl, mtl_fmts[i].glsl))
+ return mtl_fmts[i].mtl;
+ }
+
+ return MTLPixelFormatInvalid;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ pl_gpu gpu = ra_pl_get(mapper->owner->ra_ctx->ra);
+
+ CVPixelBufferRelease(p->pbuf);
+ p->pbuf = (CVPixelBufferRef)mapper->src->planes[3];
+ CVPixelBufferRetain(p->pbuf);
+
+ const bool planar = CVPixelBufferIsPlanar(p->pbuf);
+ const int planes = CVPixelBufferGetPlaneCount(p->pbuf);
+ assert((planar && planes == p->desc.num_planes) || p->desc.num_planes == 1);
+
+ for (int i = 0; i < p->desc.num_planes; i++) {
+ const struct ra_format *fmt = p->desc.planes[i];
+
+ pl_fmt plfmt = ra_pl_fmt_get(fmt);
+ MTLPixelFormat format = get_mtl_fmt(plfmt->glsl_format);
+
+ if (!format) {
+ MP_ERR(mapper, "Format unsupported.\n");
+ return -1;
+ }
+
+ size_t width = CVPixelBufferGetWidthOfPlane(p->pbuf, i),
+ height = CVPixelBufferGetHeightOfPlane(p->pbuf, i);
+
+ CVReturn err = CVMetalTextureCacheCreateTextureFromImage(
+ kCFAllocatorDefault,
+ p->mtl_texture_cache,
+ p->pbuf,
+ NULL,
+ format,
+ width,
+ height,
+ i,
+ &p->mtl_planes[i]);
+
+ if (err != noErr) {
+ MP_ERR(mapper, "error creating texture for plane %d: %d\n", i, err);
+ return -1;
+ }
+
+ struct pl_tex_params tex_params = {
+ .w = width,
+ .h = height,
+ .d = 0,
+ .format = plfmt,
+ .sampleable = true,
+ .import_handle = PL_HANDLE_MTL_TEX,
+ .shared_mem = (struct pl_shared_mem) {
+ .handle = {
+ .handle = CVMetalTextureGetTexture(p->mtl_planes[i]),
+ },
+ },
+ };
+
+ pl_tex pltex = pl_tex_create(gpu, &tex_params);
+ if (!pltex)
+ return -1;
+
+ struct ra_tex *ratex = talloc_ptrtype(NULL, ratex);
+ int ret = mppl_wrap_tex(mapper->ra, pltex, ratex);
+ if (!ret) {
+ pl_tex_destroy(gpu, &pltex);
+ talloc_free(ratex);
+ return -1;
+ }
+ mapper->tex[i] = ratex;
+ }
+
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ CVPixelBufferRelease(p->pbuf);
+ if (p->mtl_texture_cache) {
+ CFRelease(p->mtl_texture_cache);
+ p->mtl_texture_cache = NULL;
+ }
+}
+
+bool vt_pl_init(const struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ if (!check_hwdec(hw))
+ return false;
+
+ p->interop_init = mapper_init;
+ p->interop_uninit = mapper_uninit;
+ p->interop_map = mapper_map;
+ p->interop_unmap = mapper_unmap;
+
+ return true;
+}
diff --git a/video/out/hwdec/hwdec_vulkan.c b/video/out/hwdec/hwdec_vulkan.c
new file mode 100644
index 0000000..5f7354d
--- /dev/null
+++ b/video/out/hwdec/hwdec_vulkan.c
@@ -0,0 +1,333 @@
+/*
+ * Copyright (c) 2022 Philip Langdale <philipl@overt.org>
+ *
+ * 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 "config.h"
+#include "video/out/gpu/hwdec.h"
+#include "video/out/vulkan/context.h"
+#include "video/out/placebo/ra_pl.h"
+
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_vulkan.h>
+
+struct vulkan_hw_priv {
+ struct mp_hwdec_ctx hwctx;
+ pl_gpu gpu;
+};
+
+struct vulkan_mapper_priv {
+ struct mp_image layout;
+ AVVkFrame *vkf;
+ pl_tex tex[4];
+};
+
+static void lock_queue(struct AVHWDeviceContext *ctx,
+ uint32_t queue_family, uint32_t index)
+{
+ pl_vulkan vulkan = ctx->user_opaque;
+ vulkan->lock_queue(vulkan, queue_family, index);
+}
+
+static void unlock_queue(struct AVHWDeviceContext *ctx,
+ uint32_t queue_family, uint32_t index)
+{
+ pl_vulkan vulkan = ctx->user_opaque;
+ vulkan->unlock_queue(vulkan, queue_family, index);
+}
+
+static int vulkan_init(struct ra_hwdec *hw)
+{
+ AVBufferRef *hw_device_ctx = NULL;
+ int ret = 0;
+ struct vulkan_hw_priv *p = hw->priv;
+ int level = hw->probing ? MSGL_V : MSGL_ERR;
+
+ struct mpvk_ctx *vk = ra_vk_ctx_get(hw->ra_ctx);
+ if (!vk) {
+ MP_MSG(hw, level, "This is not a libplacebo vulkan gpu api context.\n");
+ return 0;
+ }
+
+ p->gpu = ra_pl_get(hw->ra_ctx->ra);
+ if (!p->gpu) {
+ MP_MSG(hw, level, "Failed to obtain pl_gpu.\n");
+ return 0;
+ }
+
+ /*
+ * libplacebo initialises all queues, but we still need to discover which
+ * one is the decode queue.
+ */
+ uint32_t num_qf = 0;
+ VkQueueFamilyProperties *qf = NULL;
+ vkGetPhysicalDeviceQueueFamilyProperties(vk->vulkan->phys_device, &num_qf, NULL);
+ if (!num_qf)
+ goto error;
+
+ qf = talloc_array(NULL, VkQueueFamilyProperties, num_qf);
+ vkGetPhysicalDeviceQueueFamilyProperties(vk->vulkan->phys_device, &num_qf, qf);
+
+ int decode_index = -1, decode_count = 0;
+ for (int i = 0; i < num_qf; i++) {
+ /*
+ * Pick the first discovered decode queue that we find. Maybe a day will
+ * come when this needs to be smarter, but I'm sure a bunch of other
+ * things will have to change too.
+ */
+ if ((qf[i].queueFlags) & VK_QUEUE_VIDEO_DECODE_BIT_KHR) {
+ decode_index = i;
+ decode_count = qf[i].queueCount;
+ }
+ }
+
+ hw_device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VULKAN);
+ if (!hw_device_ctx)
+ goto error;
+
+ AVHWDeviceContext *device_ctx = (void *)hw_device_ctx->data;
+ AVVulkanDeviceContext *device_hwctx = device_ctx->hwctx;
+
+ device_ctx->user_opaque = (void *)vk->vulkan;
+ device_hwctx->lock_queue = lock_queue;
+ device_hwctx->unlock_queue = unlock_queue;
+ device_hwctx->get_proc_addr = vk->vkinst->get_proc_addr;
+ device_hwctx->inst = vk->vkinst->instance;
+ device_hwctx->phys_dev = vk->vulkan->phys_device;
+ device_hwctx->act_dev = vk->vulkan->device;
+ device_hwctx->device_features = *vk->vulkan->features;
+ device_hwctx->enabled_inst_extensions = vk->vkinst->extensions;
+ device_hwctx->nb_enabled_inst_extensions = vk->vkinst->num_extensions;
+ device_hwctx->enabled_dev_extensions = vk->vulkan->extensions;
+ device_hwctx->nb_enabled_dev_extensions = vk->vulkan->num_extensions;
+ device_hwctx->queue_family_index = vk->vulkan->queue_graphics.index;
+ device_hwctx->nb_graphics_queues = vk->vulkan->queue_graphics.count;
+ device_hwctx->queue_family_tx_index = vk->vulkan->queue_transfer.index;
+ device_hwctx->nb_tx_queues = vk->vulkan->queue_transfer.count;
+ device_hwctx->queue_family_comp_index = vk->vulkan->queue_compute.index;
+ device_hwctx->nb_comp_queues = vk->vulkan->queue_compute.count;
+ device_hwctx->queue_family_decode_index = decode_index;
+ device_hwctx->nb_decode_queues = decode_count;
+
+ ret = av_hwdevice_ctx_init(hw_device_ctx);
+ if (ret < 0) {
+ MP_MSG(hw, level, "av_hwdevice_ctx_init failed\n");
+ goto error;
+ }
+
+ p->hwctx = (struct mp_hwdec_ctx) {
+ .driver_name = hw->driver->name,
+ .av_device_ref = hw_device_ctx,
+ .hw_imgfmt = IMGFMT_VULKAN,
+ };
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ talloc_free(qf);
+ return 0;
+
+ error:
+ talloc_free(qf);
+ av_buffer_unref(&hw_device_ctx);
+ return -1;
+}
+
+static void vulkan_uninit(struct ra_hwdec *hw)
+{
+ struct vulkan_hw_priv *p = hw->priv;
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct vulkan_mapper_priv *p = mapper->priv;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ mp_image_set_params(&p->layout, &mapper->dst_params);
+
+ struct ra_imgfmt_desc desc = {0};
+ if (!ra_get_imgfmt_desc(mapper->ra, mapper->dst_params.imgfmt, &desc))
+ return -1;
+
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct vulkan_hw_priv *p_owner = mapper->owner->priv;
+ struct vulkan_mapper_priv *p = mapper->priv;
+ if (!mapper->src)
+ goto end;
+
+ AVHWFramesContext *hwfc = (AVHWFramesContext *) mapper->src->hwctx->data;;
+ const AVVulkanFramesContext *vkfc = hwfc->hwctx;;
+ AVVkFrame *vkf = p->vkf;
+
+ int num_images;
+ for (num_images = 0; (vkf->img[num_images] != VK_NULL_HANDLE); num_images++);
+
+ for (int i = 0; (p->tex[i] != NULL); i++) {
+ pl_tex *tex = &p->tex[i];
+ if (!*tex)
+ continue;
+
+ // If we have multiple planes and one image, then that is a multiplane
+ // frame. Anything else is treated as one-image-per-plane.
+ int index = p->layout.num_planes > 1 && num_images == 1 ? 0 : i;
+
+ // Update AVVkFrame state to reflect current layout
+ bool ok = pl_vulkan_hold_ex(p_owner->gpu, pl_vulkan_hold_params(
+ .tex = *tex,
+ .out_layout = &vkf->layout[index],
+ .qf = VK_QUEUE_FAMILY_IGNORED,
+ .semaphore = (pl_vulkan_sem) {
+ .sem = vkf->sem[index],
+ .value = vkf->sem_value[index] + 1,
+ },
+ ));
+
+ vkf->access[index] = 0;
+ vkf->sem_value[index] += !!ok;
+ *tex = NULL;
+ }
+
+ vkfc->unlock_frame(hwfc, vkf);
+
+ end:
+ for (int i = 0; i < p->layout.num_planes; i++)
+ ra_tex_free(mapper->ra, &mapper->tex[i]);
+
+ p->vkf = NULL;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ bool result = false;
+ struct vulkan_hw_priv *p_owner = mapper->owner->priv;
+ struct vulkan_mapper_priv *p = mapper->priv;
+ pl_vulkan vk = pl_vulkan_get(p_owner->gpu);
+ if (!vk)
+ return -1;
+
+ AVHWFramesContext *hwfc = (AVHWFramesContext *) mapper->src->hwctx->data;
+ const AVVulkanFramesContext *vkfc = hwfc->hwctx;
+ AVVkFrame *vkf = (AVVkFrame *) mapper->src->planes[0];
+
+ /*
+ * We need to use the dimensions from the HW Frames Context for the
+ * textures, as the underlying images may be larger than the logical frame
+ * size. This most often happens with 1080p content where the actual frame
+ * height is 1088.
+ */
+ struct mp_image raw_layout;
+ mp_image_setfmt(&raw_layout, p->layout.params.imgfmt);
+ mp_image_set_size(&raw_layout, hwfc->width, hwfc->height);
+
+ int num_images;
+ for (num_images = 0; (vkf->img[num_images] != VK_NULL_HANDLE); num_images++);
+ const VkFormat *vk_fmt = av_vkfmt_from_pixfmt(hwfc->sw_format);
+
+ vkfc->lock_frame(hwfc, vkf);
+
+ for (int i = 0; i < p->layout.num_planes; i++) {
+ pl_tex *tex = &p->tex[i];
+ VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
+ int index = i;
+
+ // If we have multiple planes and one image, then that is a multiplane
+ // frame. Anything else is treated as one-image-per-plane.
+ if (p->layout.num_planes > 1 && num_images == 1) {
+ index = 0;
+
+ switch (i) {
+ case 0:
+ aspect = VK_IMAGE_ASPECT_PLANE_0_BIT_KHR;
+ break;
+ case 1:
+ aspect = VK_IMAGE_ASPECT_PLANE_1_BIT_KHR;
+ break;
+ case 2:
+ aspect = VK_IMAGE_ASPECT_PLANE_2_BIT_KHR;
+ break;
+ default:
+ goto error;
+ }
+ }
+
+ *tex = pl_vulkan_wrap(p_owner->gpu, pl_vulkan_wrap_params(
+ .image = vkf->img[index],
+ .width = mp_image_plane_w(&raw_layout, i),
+ .height = mp_image_plane_h(&raw_layout, i),
+ .format = vk_fmt[i],
+ .usage = vkfc->usage,
+ .aspect = aspect,
+ ));
+ if (!*tex)
+ goto error;
+
+ pl_vulkan_release_ex(p_owner->gpu, pl_vulkan_release_params(
+ .tex = p->tex[i],
+ .layout = vkf->layout[index],
+ .qf = VK_QUEUE_FAMILY_IGNORED,
+ .semaphore = (pl_vulkan_sem) {
+ .sem = vkf->sem[index],
+ .value = vkf->sem_value[index],
+ },
+ ));
+
+ struct ra_tex *ratex = talloc_ptrtype(NULL, ratex);
+ result = mppl_wrap_tex(mapper->ra, *tex, ratex);
+ if (!result) {
+ pl_tex_destroy(p_owner->gpu, tex);
+ talloc_free(ratex);
+ goto error;
+ }
+ mapper->tex[i] = ratex;
+ }
+
+ p->vkf = vkf;
+ return 0;
+
+ error:
+ vkfc->unlock_frame(hwfc, vkf);
+ mapper_unmap(mapper);
+ return -1;
+}
+
+const struct ra_hwdec_driver ra_hwdec_vulkan = {
+ .name = "vulkan",
+ .imgfmts = {IMGFMT_VULKAN, 0},
+ .priv_size = sizeof(struct vulkan_hw_priv),
+ .init = vulkan_init,
+ .uninit = vulkan_uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct vulkan_mapper_priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/libmpv.h b/video/out/libmpv.h
new file mode 100644
index 0000000..a697eaf
--- /dev/null
+++ b/video/out/libmpv.h
@@ -0,0 +1,83 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include "libmpv/render.h"
+#include "vo.h"
+
+// Helper for finding a parameter value. It returns the direct pointer to the
+// value, and if not present, just returns the def argument. In particular, if
+// def is not NULL, this never returns NULL (unless a param value is defined
+// as accepting NULL, or the libmpv API user is triggering UB).
+void *get_mpv_render_param(mpv_render_param *params, mpv_render_param_type type,
+ void *def);
+
+#define GET_MPV_RENDER_PARAM(params, type, ctype, def) \
+ (*(ctype *)get_mpv_render_param(params, type, &(ctype){(def)}))
+
+typedef int (*mp_render_cb_control_fn)(struct vo *vo, void *cb_ctx, int *events,
+ uint32_t request, void *data);
+void mp_render_context_set_control_callback(mpv_render_context *ctx,
+ mp_render_cb_control_fn callback,
+ void *callback_ctx);
+bool mp_render_context_acquire(mpv_render_context *ctx);
+
+struct render_backend {
+ struct mpv_global *global;
+ struct mp_log *log;
+ const struct render_backend_fns *fns;
+
+ // Set on init, immutable afterwards.
+ int driver_caps;
+ struct mp_hwdec_devices *hwdec_devs;
+
+ void *priv;
+};
+
+// Generic backend for rendering via libmpv. This corresponds to vo/vo_driver,
+// except for rendering via the mpv_render_*() API. (As a consequence it's as
+// generic as the VO API.) Like with VOs, one backend can support multiple
+// underlying GPU APIs.
+struct render_backend_fns {
+ // Returns libmpv error code. In particular, this function has to check for
+ // MPV_RENDER_PARAM_API_TYPE, and silently return MPV_ERROR_NOT_IMPLEMENTED
+ // if the API is not included in this backend.
+ // If this fails, ->destroy() will be called.
+ int (*init)(struct render_backend *ctx, mpv_render_param *params);
+ // Check if the passed IMGFMT_ is supported.
+ bool (*check_format)(struct render_backend *ctx, int imgfmt);
+ // Implementation of mpv_render_context_set_parameter(). Optional.
+ int (*set_parameter)(struct render_backend *ctx, mpv_render_param param);
+ // Like vo_driver.reconfig().
+ void (*reconfig)(struct render_backend *ctx, struct mp_image_params *params);
+ // Like VOCTRL_RESET.
+ void (*reset)(struct render_backend *ctx);
+ void (*screenshot)(struct render_backend *ctx, struct vo_frame *frame,
+ struct voctrl_screenshot *args);
+ void (*perfdata)(struct render_backend *ctx,
+ struct voctrl_performance_data *out);
+ // Like vo_driver.get_image().
+ struct mp_image *(*get_image)(struct render_backend *ctx, int imgfmt,
+ int w, int h, int stride_align, int flags);
+ // This has two purposes: 1. set queue attributes on VO, 2. update the
+ // renderer's OSD pointer. Keep in mind that as soon as the caller releases
+ // the renderer lock, the VO pointer can become invalid. The OSD pointer
+ // will technically remain valid (even though it's a vo field), until it's
+ // unset with this function.
+ // Will be called if vo changes, or if renderer options change.
+ void (*update_external)(struct render_backend *ctx, struct vo *vo);
+ // Update screen area.
+ void (*resize)(struct render_backend *ctx, struct mp_rect *src,
+ struct mp_rect *dst, struct mp_osd_res *osd);
+ // Get target surface size from mpv_render_context_render() arguments.
+ int (*get_target_size)(struct render_backend *ctx, mpv_render_param *params,
+ int *out_w, int *out_h);
+ // Implementation of mpv_render_context_render().
+ int (*render)(struct render_backend *ctx, mpv_render_param *params,
+ struct vo_frame *frame);
+ // Free all data in ctx->priv.
+ void (*destroy)(struct render_backend *ctx);
+};
+
+extern const struct render_backend_fns render_backend_gpu;
+extern const struct render_backend_fns render_backend_sw;
diff --git a/video/out/libmpv_sw.c b/video/out/libmpv_sw.c
new file mode 100644
index 0000000..f1b08f0
--- /dev/null
+++ b/video/out/libmpv_sw.c
@@ -0,0 +1,208 @@
+#include "libmpv/render_gl.h"
+#include "libmpv.h"
+#include "sub/osd.h"
+#include "video/sws_utils.h"
+
+struct priv {
+ struct libmpv_gpu_context *context;
+
+ struct mp_sws_context *sws;
+ struct osd_state *osd;
+
+ struct mp_image_params src_params, dst_params;
+ struct mp_rect src_rc, dst_rc;
+ struct mp_osd_res osd_rc;
+ bool anything_changed;
+};
+
+static int init(struct render_backend *ctx, mpv_render_param *params)
+{
+ ctx->priv = talloc_zero(NULL, struct priv);
+ struct priv *p = ctx->priv;
+
+ char *api = get_mpv_render_param(params, MPV_RENDER_PARAM_API_TYPE, NULL);
+ if (!api)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ if (strcmp(api, MPV_RENDER_API_TYPE_SW) != 0)
+ return MPV_ERROR_NOT_IMPLEMENTED;
+
+ p->sws = mp_sws_alloc(p);
+ mp_sws_enable_cmdline_opts(p->sws, ctx->global);
+
+ p->anything_changed = true;
+
+ return 0;
+}
+
+static bool check_format(struct render_backend *ctx, int imgfmt)
+{
+ struct priv *p = ctx->priv;
+
+ // Note: we don't know the output format yet. Using an arbitrary supported
+ // format is fine, because we know that any supported input format can
+ // be converted to any supported output format.
+ return mp_sws_supports_formats(p->sws, IMGFMT_RGB0, imgfmt);
+}
+
+static int set_parameter(struct render_backend *ctx, mpv_render_param param)
+{
+ return MPV_ERROR_NOT_IMPLEMENTED;
+}
+
+static void reconfig(struct render_backend *ctx, struct mp_image_params *params)
+{
+ struct priv *p = ctx->priv;
+
+ p->src_params = *params;
+ p->anything_changed = true;
+}
+
+static void reset(struct render_backend *ctx)
+{
+ // stateless
+}
+
+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;
+
+ p->src_rc = *src;
+ p->dst_rc = *dst;
+ p->osd_rc = *osd;
+ p->anything_changed = true;
+}
+
+static int get_target_size(struct render_backend *ctx, mpv_render_param *params,
+ int *out_w, int *out_h)
+{
+ int *sz = get_mpv_render_param(params, MPV_RENDER_PARAM_SW_SIZE, NULL);
+ if (!sz)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ *out_w = sz[0];
+ *out_h = sz[1];
+ return 0;
+}
+
+static int render(struct render_backend *ctx, mpv_render_param *params,
+ struct vo_frame *frame)
+{
+ struct priv *p = ctx->priv;
+
+ int *sz = get_mpv_render_param(params, MPV_RENDER_PARAM_SW_SIZE, NULL);
+ char *fmt = get_mpv_render_param(params, MPV_RENDER_PARAM_SW_FORMAT, NULL);
+ size_t *stride = get_mpv_render_param(params, MPV_RENDER_PARAM_SW_STRIDE, NULL);
+ void *ptr = get_mpv_render_param(params, MPV_RENDER_PARAM_SW_POINTER, NULL);
+
+ if (!sz || !fmt || !stride || !ptr)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ char *prev_fmt = mp_imgfmt_to_name(p->dst_params.imgfmt);
+ if (strcmp(prev_fmt, fmt) != 0)
+ p->anything_changed = true;
+
+ if (sz[0] != p->dst_params.w || sz[1] != p->dst_params.h)
+ p->anything_changed = true;
+
+ if (p->anything_changed) {
+ p->dst_params = (struct mp_image_params){
+ .imgfmt = mp_imgfmt_from_name(bstr0(fmt)),
+ .w = sz[0],
+ .h = sz[1],
+ };
+
+ // Exclude "problematic" formats. In particular, reject multi-plane and
+ // hw formats. Exclude non-byte-aligned formats for easier stride
+ // checking.
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(p->dst_params.imgfmt);
+ if (!(desc.flags & MP_IMGFLAG_COLOR_RGB) ||
+ !(desc.flags & (MP_IMGFLAG_TYPE_UINT | MP_IMGFLAG_TYPE_FLOAT)) ||
+ (desc.flags & MP_IMGFLAG_TYPE_PAL8) ||
+ !(desc.flags & MP_IMGFLAG_BYTE_ALIGNED) ||
+ desc.num_planes != 1)
+ return MPV_ERROR_UNSUPPORTED;
+
+ mp_image_params_guess_csp(&p->dst_params);
+
+ // Can be unset if rendering before any video was loaded.
+ if (p->src_params.imgfmt) {
+ p->sws->src = p->src_params;
+ p->sws->src.w = mp_rect_w(p->src_rc);
+ p->sws->src.h = mp_rect_h(p->src_rc);
+
+ p->sws->dst = p->dst_params;
+ p->sws->dst.w = mp_rect_w(p->dst_rc);
+ p->sws->dst.h = mp_rect_h(p->dst_rc);
+
+ if (mp_sws_reinit(p->sws) < 0)
+ return MPV_ERROR_UNSUPPORTED; // probably
+ }
+
+ p->anything_changed = false;
+ }
+
+ struct mp_image wrap_img = {0};
+ mp_image_set_params(&wrap_img, &p->dst_params);
+
+ size_t bpp = wrap_img.fmt.bpp[0] / 8;
+ if (!bpp || bpp * wrap_img.w > *stride || *stride % bpp)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ wrap_img.planes[0] = ptr;
+ wrap_img.stride[0] = *stride;
+
+ struct mp_image *img = frame->current;
+ if (img) {
+ assert(p->src_params.imgfmt);
+
+ mp_image_clear_rc_inv(&wrap_img, p->dst_rc);
+
+ struct mp_image src = *img;
+ struct mp_rect src_rc = p->src_rc;
+ src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, src.fmt.align_x);
+ src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, src.fmt.align_y);
+ mp_image_crop_rc(&src, src_rc);
+
+ struct mp_image dst = wrap_img;
+ mp_image_crop_rc(&dst, p->dst_rc);
+
+ if (mp_sws_scale(p->sws, &dst, &src) < 0) {
+ mp_image_clear(&wrap_img, 0, 0, wrap_img.w, wrap_img.h);
+ return MPV_ERROR_GENERIC;
+ }
+ } else {
+ mp_image_clear(&wrap_img, 0, 0, wrap_img.w, wrap_img.h);
+ }
+
+ if (p->osd)
+ osd_draw_on_image(p->osd, p->osd_rc, img ? img->pts : 0, 0, &wrap_img);
+
+ return 0;
+}
+
+static void destroy(struct render_backend *ctx)
+{
+ // nop
+}
+
+const struct render_backend_fns render_backend_sw = {
+ .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,
+ .destroy = destroy,
+};
diff --git a/video/out/mac/common.swift b/video/out/mac/common.swift
new file mode 100644
index 0000000..aac7050
--- /dev/null
+++ b/video/out/mac/common.swift
@@ -0,0 +1,691 @@
+/*
+ * 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/>.
+ */
+
+import Cocoa
+import IOKit.pwr_mgt
+
+class Common: NSObject {
+ var mpv: MPVHelper?
+ var log: LogHelper
+ let queue: DispatchQueue = DispatchQueue(label: "io.mpv.queue")
+
+ var window: Window?
+ var view: View?
+ var titleBar: TitleBar?
+
+ var link: CVDisplayLink?
+
+ let eventsLock = NSLock()
+ var events: Int = 0
+
+ var lightSensor: io_connect_t = 0
+ var lastLmu: UInt64 = 0
+ var lightSensorIOPort: IONotificationPortRef?
+
+ var displaySleepAssertion: IOPMAssertionID = IOPMAssertionID(0)
+
+ var appNotificationObservers: [NSObjectProtocol] = []
+
+ var cursorVisibilityWanted: Bool = true
+
+ var title: String = "mpv" {
+ didSet { if let window = window { window.title = title } }
+ }
+
+ init(_ mpLog: OpaquePointer?) {
+ log = LogHelper(mpLog)
+ }
+
+ func initMisc(_ vo: UnsafeMutablePointer<vo>) {
+ guard let mpv = mpv else {
+ log.sendError("Something went wrong, no MPVHelper was initialized")
+ exit(1)
+ }
+
+ startDisplayLink(vo)
+ initLightSensor()
+ addDisplayReconfigureObserver()
+ addAppNotifications()
+ mpv.setMacOptionCallback(macOptsWakeupCallback, context: self)
+ }
+
+ func initApp() {
+ guard let mpv = mpv else {
+ log.sendError("Something went wrong, no MPVHelper was initialized")
+ exit(1)
+ }
+
+ var policy: NSApplication.ActivationPolicy = .regular
+ switch mpv.macOpts.macos_app_activation_policy {
+ case 0:
+ policy = .regular
+ case 1:
+ policy = .accessory
+ case 2:
+ policy = .prohibited
+ default:
+ break
+ }
+
+ NSApp.setActivationPolicy(policy)
+ setAppIcon()
+ }
+
+ func initWindow(_ vo: UnsafeMutablePointer<vo>, _ previousActiveApp: NSRunningApplication?) {
+ let (mpv, targetScreen, wr) = getInitProperties(vo)
+
+ guard let view = self.view else {
+ log.sendError("Something went wrong, no View was initialized")
+ exit(1)
+ }
+
+ window = Window(contentRect: wr, screen: targetScreen, view: view, common: self)
+ guard let window = self.window else {
+ log.sendError("Something went wrong, no Window was initialized")
+ exit(1)
+ }
+
+ window.setOnTop(Bool(mpv.opts.ontop), Int(mpv.opts.ontop_level))
+ window.setOnAllWorkspaces(Bool(mpv.opts.all_workspaces))
+ window.keepAspect = Bool(mpv.opts.keepaspect_window)
+ window.title = title
+ window.border = Bool(mpv.opts.border)
+
+ titleBar = TitleBar(frame: wr, window: window, common: self)
+
+ let minimized = Bool(mpv.opts.window_minimized)
+ window.isRestorable = false
+ window.isReleasedWhenClosed = false
+ window.setMaximized(minimized ? false : Bool(mpv.opts.window_maximized))
+ window.setMinimized(minimized)
+ window.makeMain()
+ window.makeKey()
+
+ if !minimized {
+ window.orderFront(nil)
+ }
+
+ NSApp.activate(ignoringOtherApps: mpv.opts.focus_on_open)
+
+ // workaround for macOS 10.15 to refocus the previous App
+ if (!mpv.opts.focus_on_open) {
+ previousActiveApp?.activate(options: .activateAllWindows)
+ }
+ }
+
+ func initView(_ vo: UnsafeMutablePointer<vo>, _ layer: CALayer) {
+ let (_, _, wr) = getInitProperties(vo)
+
+ view = View(frame: wr, common: self)
+ guard let view = self.view else {
+ log.sendError("Something went wrong, no View was initialized")
+ exit(1)
+ }
+
+ view.layer = layer
+ view.wantsLayer = true
+ view.layerContentsPlacement = .scaleProportionallyToFit
+ }
+
+ func initWindowState() {
+ if mpv?.opts.fullscreen ?? false {
+ DispatchQueue.main.async {
+ self.window?.toggleFullScreen(nil)
+ }
+ } else {
+ window?.isMovableByWindowBackground = true
+ }
+ }
+
+ func uninitCommon() {
+ setCursorVisibility(true)
+ stopDisplaylink()
+ uninitLightSensor()
+ removeDisplayReconfigureObserver()
+ removeAppNotifications()
+ enableDisplaySleep()
+ window?.orderOut(nil)
+
+ titleBar?.removeFromSuperview()
+ view?.removeFromSuperview()
+ }
+
+ func displayLinkCallback(_ displayLink: CVDisplayLink,
+ _ inNow: UnsafePointer<CVTimeStamp>,
+ _ inOutputTime: UnsafePointer<CVTimeStamp>,
+ _ flagsIn: CVOptionFlags,
+ _ flagsOut: UnsafeMutablePointer<CVOptionFlags>) -> CVReturn
+ {
+ return kCVReturnSuccess
+ }
+
+ func startDisplayLink(_ vo: UnsafeMutablePointer<vo>) {
+ CVDisplayLinkCreateWithActiveCGDisplays(&link)
+
+ guard let screen = getTargetScreen(forFullscreen: false) ?? NSScreen.main,
+ let link = self.link else
+ {
+ log.sendWarning("Couldn't start DisplayLink, no MPVHelper, Screen or DisplayLink available")
+ return
+ }
+
+ CVDisplayLinkSetCurrentCGDisplay(link, screen.displayID)
+ CVDisplayLinkSetOutputHandler(link) { link, now, out, inFlags, outFlags -> CVReturn in
+ return self.displayLinkCallback(link, now, out, inFlags, outFlags)
+ }
+ CVDisplayLinkStart(link)
+ }
+
+ func stopDisplaylink() {
+ if let link = self.link, CVDisplayLinkIsRunning(link) {
+ CVDisplayLinkStop(link)
+ }
+ }
+
+ func updateDisplaylink() {
+ guard let screen = window?.screen, let link = self.link else {
+ log.sendWarning("Couldn't update DisplayLink, no Screen or DisplayLink available")
+ return
+ }
+
+ CVDisplayLinkSetCurrentCGDisplay(link, screen.displayID)
+ queue.asyncAfter(deadline: DispatchTime.now() + 0.1) {
+ self.flagEvents(VO_EVENT_WIN_STATE)
+ }
+ }
+
+ func currentFps() -> Double {
+ if let link = self.link {
+ var actualFps = CVDisplayLinkGetActualOutputVideoRefreshPeriod(link)
+ let nominalData = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link)
+
+ if (nominalData.flags & Int32(CVTimeFlags.isIndefinite.rawValue)) < 1 {
+ let nominalFps = Double(nominalData.timeScale) / Double(nominalData.timeValue)
+
+ if actualFps > 0 {
+ actualFps = 1/actualFps
+ }
+
+ if fabs(actualFps - nominalFps) > 0.1 {
+ log.sendVerbose("Falling back to nominal display refresh rate: \(nominalFps)")
+ return nominalFps
+ } else {
+ return actualFps
+ }
+ }
+ } else {
+ log.sendWarning("No DisplayLink available")
+ }
+
+ log.sendWarning("Falling back to standard display refresh rate: 60Hz")
+ return 60.0
+ }
+
+ func enableDisplaySleep() {
+ IOPMAssertionRelease(displaySleepAssertion)
+ displaySleepAssertion = IOPMAssertionID(0)
+ }
+
+ func disableDisplaySleep() {
+ if displaySleepAssertion != IOPMAssertionID(0) { return }
+ IOPMAssertionCreateWithName(
+ kIOPMAssertionTypePreventUserIdleDisplaySleep as CFString,
+ IOPMAssertionLevel(kIOPMAssertionLevelOn),
+ "io.mpv.video_playing_back" as CFString,
+ &displaySleepAssertion)
+ }
+
+ func lmuToLux(_ v: UInt64) -> Int {
+ // the polinomial approximation for apple lmu value -> lux was empirically
+ // derived by firefox developers (Apple provides no documentation).
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=793728
+ let power_c4: Double = 1 / pow(10, 27)
+ let power_c3: Double = 1 / pow(10, 19)
+ let power_c2: Double = 1 / pow(10, 12)
+ let power_c1: Double = 1 / pow(10, 5)
+
+ let lum = Double(v)
+ let term4: Double = -3.0 * power_c4 * pow(lum, 4.0)
+ let term3: Double = 2.6 * power_c3 * pow(lum, 3.0)
+ let term2: Double = -3.4 * power_c2 * pow(lum, 2.0)
+ let term1: Double = 3.9 * power_c1 * lum
+
+ let lux = Int(ceil(term4 + term3 + term2 + term1 - 0.19))
+ return lux > 0 ? lux : 0
+ }
+
+ var lightSensorCallback: IOServiceInterestCallback = { (ctx, service, messageType, messageArgument) -> Void in
+ let com = unsafeBitCast(ctx, to: Common.self)
+
+ var outputs: UInt32 = 2
+ var values: [UInt64] = [0, 0]
+
+ var kr = IOConnectCallMethod(com.lightSensor, 0, nil, 0, nil, 0, &values, &outputs, nil, nil)
+ if kr == KERN_SUCCESS {
+ var mean = (values[0] + values[1]) / 2
+ if com.lastLmu != mean {
+ com.lastLmu = mean
+ com.lightSensorUpdate()
+ }
+ }
+ }
+
+ func lightSensorUpdate() {
+ log.sendWarning("lightSensorUpdate not implemented")
+ }
+
+ func initLightSensor() {
+ let srv = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleLMUController"))
+ if srv == IO_OBJECT_NULL {
+ log.sendVerbose("Can't find an ambient light sensor")
+ return
+ }
+
+ lightSensorIOPort = IONotificationPortCreate(kIOMasterPortDefault)
+ IONotificationPortSetDispatchQueue(lightSensorIOPort, queue)
+ var n = io_object_t()
+ IOServiceAddInterestNotification(lightSensorIOPort, srv, kIOGeneralInterest, lightSensorCallback, MPVHelper.bridge(obj: self), &n)
+ let kr = IOServiceOpen(srv, mach_task_self_, 0, &lightSensor)
+ IOObjectRelease(srv)
+
+ if kr != KERN_SUCCESS {
+ log.sendVerbose("Can't start ambient light sensor connection")
+ return
+ }
+ lightSensorCallback(MPVHelper.bridge(obj: self), 0, 0, nil)
+ }
+
+ func uninitLightSensor() {
+ if lightSensorIOPort != nil {
+ IONotificationPortDestroy(lightSensorIOPort)
+ IOObjectRelease(lightSensor)
+ }
+ }
+
+ var reconfigureCallback: CGDisplayReconfigurationCallBack = { (display, flags, userInfo) in
+ if flags.contains(.setModeFlag) {
+ let com = unsafeBitCast(userInfo, to: Common.self)
+ let displayID = com.window?.screen?.displayID ?? display
+
+ if displayID == display {
+ com.log.sendVerbose("Detected display mode change, updating screen refresh rate")
+ com.flagEvents(VO_EVENT_WIN_STATE)
+ }
+ }
+ }
+
+ func addDisplayReconfigureObserver() {
+ CGDisplayRegisterReconfigurationCallback(reconfigureCallback, MPVHelper.bridge(obj: self))
+ }
+
+ func removeDisplayReconfigureObserver() {
+ CGDisplayRemoveReconfigurationCallback(reconfigureCallback, MPVHelper.bridge(obj: self))
+ }
+
+ func addAppNotifications() {
+ appNotificationObservers.append(NotificationCenter.default.addObserver(
+ forName: NSApplication.didBecomeActiveNotification,
+ object: nil,
+ queue: .main,
+ using: { [weak self] (_) in self?.appDidBecomeActive() }
+ ))
+ appNotificationObservers.append(NotificationCenter.default.addObserver(
+ forName: NSApplication.didResignActiveNotification,
+ object: nil,
+ queue: .main,
+ using: { [weak self] (_) in self?.appDidResignActive() }
+ ))
+ }
+
+ func removeAppNotifications() {
+ appNotificationObservers.forEach { NotificationCenter.default.removeObserver($0) }
+ appNotificationObservers.removeAll()
+ }
+
+ func appDidBecomeActive() {
+ flagEvents(VO_EVENT_FOCUS)
+ }
+
+ func appDidResignActive() {
+ flagEvents(VO_EVENT_FOCUS)
+ }
+
+ func setAppIcon() {
+ if let app = NSApp as? Application,
+ ProcessInfo.processInfo.environment["MPVBUNDLE"] != "true"
+ {
+ NSApp.applicationIconImage = app.getMPVIcon()
+ }
+ }
+
+ func updateCursorVisibility() {
+ setCursorVisibility(cursorVisibilityWanted)
+ }
+
+ func setCursorVisibility(_ visible: Bool) {
+ NSCursor.setHiddenUntilMouseMoves(!visible && (view?.canHideCursor() ?? false))
+ }
+
+ func updateICCProfile() {
+ log.sendWarning("updateICCProfile not implemented")
+ }
+
+ func getScreenBy(id screenID: Int) -> NSScreen? {
+ if screenID >= NSScreen.screens.count {
+ log.sendInfo("Screen ID \(screenID) does not exist, falling back to current device")
+ return nil
+ } else if screenID < 0 {
+ return nil
+ }
+ return NSScreen.screens[screenID]
+ }
+
+ func getScreenBy(name screenName: String?) -> NSScreen? {
+ for screen in NSScreen.screens {
+ if screen.localizedName == screenName {
+ return screen
+ }
+ }
+ return nil
+ }
+
+ func getTargetScreen(forFullscreen fs: Bool) -> NSScreen? {
+ guard let mpv = mpv else {
+ log.sendWarning("Unexpected nil value in getTargetScreen")
+ return nil
+ }
+
+ let screenID = fs ? mpv.opts.fsscreen_id : mpv.opts.screen_id
+ var name: String?
+ if let screenName = fs ? mpv.opts.fsscreen_name : mpv.opts.screen_name {
+ name = String(cString: screenName)
+ }
+ return getScreenBy(id: Int(screenID)) ?? getScreenBy(name: name)
+ }
+
+ func getCurrentScreen() -> NSScreen? {
+ return window != nil ? window?.screen :
+ getTargetScreen(forFullscreen: false) ??
+ NSScreen.main
+ }
+
+ func getWindowGeometry(forScreen screen: NSScreen,
+ videoOut vo: UnsafeMutablePointer<vo>) -> NSRect {
+ let r = screen.convertRectToBacking(screen.frame)
+ let targetFrame = (mpv?.macOpts.macos_geometry_calculation ?? Int32(FRAME_VISIBLE)) == FRAME_VISIBLE
+ ? screen.visibleFrame : screen.frame
+ let rv = screen.convertRectToBacking(targetFrame)
+
+ // convert origin to be relative to target screen
+ var originY = rv.origin.y - r.origin.y
+ let originX = rv.origin.x - r.origin.x
+ // flip the y origin, mp_rect expects the origin at the top-left
+ // macOS' windowing system operates from the bottom-left
+ originY = -(originY + rv.size.height)
+ var screenRC: mp_rect = mp_rect(x0: Int32(originX),
+ y0: Int32(originY),
+ x1: Int32(originX + rv.size.width),
+ y1: Int32(originY + rv.size.height))
+
+ var geo: vo_win_geometry = vo_win_geometry()
+ vo_calc_window_geometry2(vo, &screenRC, Double(screen.backingScaleFactor), &geo)
+ vo_apply_window_geometry(vo, &geo)
+
+ let height = CGFloat(geo.win.y1 - geo.win.y0)
+ let width = CGFloat(geo.win.x1 - geo.win.x0)
+ // flip the y origin again
+ let y = CGFloat(-geo.win.y1)
+ let x = CGFloat(geo.win.x0)
+ return screen.convertRectFromBacking(NSMakeRect(x, y, width, height))
+ }
+
+ func getInitProperties(_ vo: UnsafeMutablePointer<vo>) -> (MPVHelper, NSScreen, NSRect) {
+ guard let mpv = mpv else {
+ log.sendError("Something went wrong, no MPVHelper was initialized")
+ exit(1)
+ }
+ guard let targetScreen = getTargetScreen(forFullscreen: false) ?? NSScreen.main else {
+ log.sendError("Something went wrong, no Screen was found")
+ exit(1)
+ }
+
+ let wr = getWindowGeometry(forScreen: targetScreen, videoOut: vo)
+
+ return (mpv, targetScreen, wr)
+ }
+
+ // call before initApp, because on macOS +10.15 it changes the active App
+ func getActiveApp() -> NSRunningApplication? {
+ return NSWorkspace.shared.runningApplications.first(where: {$0.isActive})
+ }
+
+ func flagEvents(_ ev: Int) {
+ eventsLock.lock()
+ events |= ev
+ eventsLock.unlock()
+
+ guard let vout = mpv?.vo else {
+ log.sendWarning("vo nil in flagEvents")
+ return
+ }
+ vo_wakeup(vout)
+ }
+
+ func checkEvents() -> Int {
+ eventsLock.lock()
+ let ev = events
+ events = 0
+ eventsLock.unlock()
+ return ev
+ }
+
+ func windowDidEndAnimation() {}
+ func windowSetToFullScreen() {}
+ func windowSetToWindow() {}
+ func windowDidUpdateFrame() {}
+ func windowDidChangeScreen() {}
+ func windowDidChangeScreenProfile() {}
+ func windowDidChangeBackingProperties() {}
+ func windowWillStartLiveResize() {}
+ func windowDidEndLiveResize() {}
+ func windowDidResize() {}
+ func windowDidChangeOcclusionState() {}
+
+ @objc func control(_ vo: UnsafeMutablePointer<vo>,
+ events: UnsafeMutablePointer<Int32>,
+ request: UInt32,
+ data: UnsafeMutableRawPointer?) -> Int32
+ {
+ guard let mpv = mpv else {
+ log.sendWarning("Unexpected nil value in Control Callback")
+ return VO_FALSE
+ }
+
+ switch mp_voctrl(request) {
+ case VOCTRL_CHECK_EVENTS:
+ events.pointee |= Int32(checkEvents())
+ return VO_TRUE
+ case VOCTRL_VO_OPTS_CHANGED:
+ var opt: UnsafeMutableRawPointer?
+ while mpv.nextChangedOption(property: &opt) {
+ switch opt {
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.border):
+ DispatchQueue.main.async {
+ self.window?.border = Bool(mpv.opts.border)
+ }
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.fullscreen):
+ DispatchQueue.main.async {
+ self.window?.toggleFullScreen(nil)
+ }
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.ontop): fallthrough
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.ontop_level):
+ DispatchQueue.main.async {
+ self.window?.setOnTop(Bool(mpv.opts.ontop), Int(mpv.opts.ontop_level))
+ }
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.all_workspaces):
+ DispatchQueue.main.async {
+ self.window?.setOnAllWorkspaces(Bool(mpv.opts.all_workspaces))
+ }
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.keepaspect_window):
+ DispatchQueue.main.async {
+ self.window?.keepAspect = Bool(mpv.opts.keepaspect_window)
+ }
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.window_minimized):
+ DispatchQueue.main.async {
+ self.window?.setMinimized(Bool(mpv.opts.window_minimized))
+ }
+ case MPVHelper.getPointer(&mpv.optsPtr.pointee.window_maximized):
+ DispatchQueue.main.async {
+ self.window?.setMaximized(Bool(mpv.opts.window_maximized))
+ }
+ default:
+ break
+ }
+ }
+ return VO_TRUE
+ case VOCTRL_GET_DISPLAY_FPS:
+ let fps = data!.assumingMemoryBound(to: CDouble.self)
+ fps.pointee = currentFps()
+ return VO_TRUE
+ case VOCTRL_GET_HIDPI_SCALE:
+ let scaleFactor = data!.assumingMemoryBound(to: CDouble.self)
+ let screen = getCurrentScreen()
+ let factor = window?.backingScaleFactor ??
+ screen?.backingScaleFactor ?? 1.0
+ scaleFactor.pointee = Double(factor)
+ return VO_TRUE
+ case VOCTRL_RESTORE_SCREENSAVER:
+ enableDisplaySleep()
+ return VO_TRUE
+ case VOCTRL_KILL_SCREENSAVER:
+ disableDisplaySleep()
+ return VO_TRUE
+ case VOCTRL_SET_CURSOR_VISIBILITY:
+ let cursorVisibility = data!.assumingMemoryBound(to: CBool.self)
+ cursorVisibilityWanted = cursorVisibility.pointee
+ DispatchQueue.main.async {
+ self.setCursorVisibility(self.cursorVisibilityWanted)
+ }
+ return VO_TRUE
+ case VOCTRL_GET_ICC_PROFILE:
+ let screen = getCurrentScreen()
+ guard var iccData = screen?.colorSpace?.iccProfileData else {
+ log.sendWarning("No Screen available to retrieve ICC profile")
+ return VO_TRUE
+ }
+
+ let icc = data!.assumingMemoryBound(to: bstr.self)
+ iccData.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) in
+ guard let baseAddress = ptr.baseAddress, ptr.count > 0 else { return }
+ let u8Ptr = baseAddress.assumingMemoryBound(to: UInt8.self)
+ icc.pointee = bstrdup(nil, bstr(start: u8Ptr, len: ptr.count))
+ }
+ return VO_TRUE
+ case VOCTRL_GET_AMBIENT_LUX:
+ if lightSensor != 0 {
+ let lux = data!.assumingMemoryBound(to: Int32.self)
+ lux.pointee = Int32(lmuToLux(lastLmu))
+ return VO_TRUE;
+ }
+ return VO_NOTIMPL
+ case VOCTRL_GET_UNFS_WINDOW_SIZE:
+ let sizeData = data!.assumingMemoryBound(to: Int32.self)
+ let size = UnsafeMutableBufferPointer(start: sizeData, count: 2)
+ var rect = window?.unfsContentFrame ?? NSRect(x: 0, y: 0, width: 1280, height: 720)
+ if let screen = window?.currentScreen, !Bool(mpv.opts.hidpi_window_scale) {
+ rect = screen.convertRectToBacking(rect)
+ }
+
+ size[0] = Int32(rect.size.width)
+ size[1] = Int32(rect.size.height)
+ return VO_TRUE
+ case VOCTRL_SET_UNFS_WINDOW_SIZE:
+ let sizeData = data!.assumingMemoryBound(to: Int32.self)
+ let size = UnsafeBufferPointer(start: sizeData, count: 2)
+ var rect = NSMakeRect(0, 0, CGFloat(size[0]), CGFloat(size[1]))
+ DispatchQueue.main.async {
+ if let screen = self.window?.currentScreen, !Bool(self.mpv?.opts.hidpi_window_scale ?? true) {
+ rect = screen.convertRectFromBacking(rect)
+ }
+ self.window?.updateSize(rect.size)
+ }
+ return VO_TRUE
+ case VOCTRL_GET_DISPLAY_NAMES:
+ let dnames = data!.assumingMemoryBound(to: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>?.self)
+ var array: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>? = nil
+ var count: Int32 = 0
+ let displayName = getCurrentScreen()?.localizedName ?? "Unknown"
+
+ SWIFT_TARRAY_STRING_APPEND(nil, &array, &count, ta_xstrdup(nil, displayName))
+ SWIFT_TARRAY_STRING_APPEND(nil, &array, &count, nil)
+ dnames.pointee = array
+ return VO_TRUE
+ case VOCTRL_GET_DISPLAY_RES:
+ guard let screen = getCurrentScreen() else {
+ log.sendWarning("No Screen available to retrieve frame")
+ return VO_NOTAVAIL
+ }
+ let sizeData = data!.assumingMemoryBound(to: Int32.self)
+ let size = UnsafeMutableBufferPointer(start: sizeData, count: 2)
+ let frame = screen.convertRectToBacking(screen.frame)
+ size[0] = Int32(frame.size.width)
+ size[1] = Int32(frame.size.height)
+ return VO_TRUE
+ case VOCTRL_GET_FOCUSED:
+ let focus = data!.assumingMemoryBound(to: CBool.self)
+ focus.pointee = NSApp.isActive
+ return VO_TRUE
+ case VOCTRL_UPDATE_WINDOW_TITLE:
+ let titleData = data!.assumingMemoryBound(to: Int8.self)
+ DispatchQueue.main.async {
+ let title = NSString(utf8String: titleData) as String?
+ self.title = title ?? "Unknown Title"
+ }
+ return VO_TRUE
+ default:
+ return VO_NOTIMPL
+ }
+ }
+
+ let macOptsWakeupCallback: swift_wakeup_cb_fn = { ( ctx ) in
+ let com = unsafeBitCast(ctx, to: Common.self)
+ DispatchQueue.main.async {
+ com.macOptsUpdate()
+ }
+ }
+
+ func macOptsUpdate() {
+ guard let mpv = mpv else {
+ log.sendWarning("Unexpected nil value in mac opts update")
+ return
+ }
+
+ var opt: UnsafeMutableRawPointer?
+ while mpv.nextChangedMacOption(property: &opt) {
+ switch opt {
+ case MPVHelper.getPointer(&mpv.macOptsPtr.pointee.macos_title_bar_appearance):
+ titleBar?.set(appearance: Int(mpv.macOpts.macos_title_bar_appearance))
+ case MPVHelper.getPointer(&mpv.macOptsPtr.pointee.macos_title_bar_material):
+ titleBar?.set(material: Int(mpv.macOpts.macos_title_bar_material))
+ case MPVHelper.getPointer(&mpv.macOptsPtr.pointee.macos_title_bar_color):
+ titleBar?.set(color: mpv.macOpts.macos_title_bar_color)
+ default:
+ break
+ }
+ }
+ }
+}
diff --git a/video/out/mac/gl_layer.swift b/video/out/mac/gl_layer.swift
new file mode 100644
index 0000000..dd96af7
--- /dev/null
+++ b/video/out/mac/gl_layer.swift
@@ -0,0 +1,322 @@
+/*
+ * 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/>.
+ */
+
+import Cocoa
+import OpenGL.GL
+import OpenGL.GL3
+
+let glVersions: [CGLOpenGLProfile] = [
+ kCGLOGLPVersion_3_2_Core,
+ kCGLOGLPVersion_Legacy
+]
+
+let glFormatBase: [CGLPixelFormatAttribute] = [
+ kCGLPFAOpenGLProfile,
+ kCGLPFAAccelerated,
+ kCGLPFADoubleBuffer
+]
+
+let glFormatSoftwareBase: [CGLPixelFormatAttribute] = [
+ kCGLPFAOpenGLProfile,
+ kCGLPFARendererID,
+ CGLPixelFormatAttribute(UInt32(kCGLRendererGenericFloatID)),
+ kCGLPFADoubleBuffer
+]
+
+let glFormatOptional: [[CGLPixelFormatAttribute]] = [
+ [kCGLPFABackingStore],
+ [kCGLPFAAllowOfflineRenderers]
+]
+
+let glFormat10Bit: [CGLPixelFormatAttribute] = [
+ kCGLPFAColorSize,
+ _CGLPixelFormatAttribute(rawValue: 64),
+ kCGLPFAColorFloat
+]
+
+let glFormatAutoGPU: [CGLPixelFormatAttribute] = [
+ kCGLPFASupportsAutomaticGraphicsSwitching
+]
+
+let attributeLookUp: [UInt32:String] = [
+ kCGLOGLPVersion_3_2_Core.rawValue: "kCGLOGLPVersion_3_2_Core",
+ kCGLOGLPVersion_Legacy.rawValue: "kCGLOGLPVersion_Legacy",
+ kCGLPFAOpenGLProfile.rawValue: "kCGLPFAOpenGLProfile",
+ UInt32(kCGLRendererGenericFloatID): "kCGLRendererGenericFloatID",
+ kCGLPFARendererID.rawValue: "kCGLPFARendererID",
+ kCGLPFAAccelerated.rawValue: "kCGLPFAAccelerated",
+ kCGLPFADoubleBuffer.rawValue: "kCGLPFADoubleBuffer",
+ kCGLPFABackingStore.rawValue: "kCGLPFABackingStore",
+ kCGLPFAColorSize.rawValue: "kCGLPFAColorSize",
+ kCGLPFAColorFloat.rawValue: "kCGLPFAColorFloat",
+ kCGLPFAAllowOfflineRenderers.rawValue: "kCGLPFAAllowOfflineRenderers",
+ kCGLPFASupportsAutomaticGraphicsSwitching.rawValue: "kCGLPFASupportsAutomaticGraphicsSwitching",
+]
+
+class GLLayer: CAOpenGLLayer {
+ unowned var cocoaCB: CocoaCB
+ var libmpv: LibmpvHelper { get { return cocoaCB.libmpv } }
+
+ let displayLock = NSLock()
+ let cglContext: CGLContextObj
+ let cglPixelFormat: CGLPixelFormatObj
+ var needsFlip: Bool = false
+ var forceDraw: Bool = false
+ var surfaceSize: NSSize = NSSize(width: 0, height: 0)
+ var bufferDepth: GLint = 8
+
+ enum Draw: Int { case normal = 1, atomic, atomicEnd }
+ var draw: Draw = .normal
+
+ let queue: DispatchQueue = DispatchQueue(label: "io.mpv.queue.draw")
+
+ var needsICCUpdate: Bool = false {
+ didSet {
+ if needsICCUpdate == true {
+ update()
+ }
+ }
+ }
+
+ var inLiveResize: Bool = false {
+ didSet {
+ if inLiveResize {
+ isAsynchronous = true
+ }
+ update(force: true)
+ }
+ }
+
+ init(cocoaCB ccb: CocoaCB) {
+ cocoaCB = ccb
+ (cglPixelFormat, bufferDepth) = GLLayer.createPixelFormat(ccb)
+ cglContext = GLLayer.createContext(ccb, cglPixelFormat)
+ super.init()
+ autoresizingMask = [.layerWidthSizable, .layerHeightSizable]
+ backgroundColor = NSColor.black.cgColor
+
+ if bufferDepth > 8 {
+ contentsFormat = .RGBA16Float
+ }
+
+ var i: GLint = 1
+ CGLSetParameter(cglContext, kCGLCPSwapInterval, &i)
+ CGLSetCurrentContext(cglContext)
+
+ libmpv.initRender()
+ libmpv.setRenderUpdateCallback(updateCallback, context: self)
+ libmpv.setRenderControlCallback(cocoaCB.controlCallback, context: cocoaCB)
+ }
+
+ // necessary for when the layer containing window changes the screen
+ override init(layer: Any) {
+ guard let oldLayer = layer as? GLLayer else {
+ fatalError("init(layer: Any) passed an invalid layer")
+ }
+ cocoaCB = oldLayer.cocoaCB
+ surfaceSize = oldLayer.surfaceSize
+ cglPixelFormat = oldLayer.cglPixelFormat
+ cglContext = oldLayer.cglContext
+ super.init()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func canDraw(inCGLContext ctx: CGLContextObj,
+ pixelFormat pf: CGLPixelFormatObj,
+ forLayerTime t: CFTimeInterval,
+ displayTime ts: UnsafePointer<CVTimeStamp>?) -> Bool {
+ if inLiveResize == false {
+ isAsynchronous = false
+ }
+ return cocoaCB.backendState == .initialized &&
+ (forceDraw || libmpv.isRenderUpdateFrame())
+ }
+
+ override func draw(inCGLContext ctx: CGLContextObj,
+ pixelFormat pf: CGLPixelFormatObj,
+ forLayerTime t: CFTimeInterval,
+ displayTime ts: UnsafePointer<CVTimeStamp>?) {
+ needsFlip = false
+ forceDraw = false
+
+ if draw.rawValue >= Draw.atomic.rawValue {
+ if draw == .atomic {
+ draw = .atomicEnd
+ } else {
+ atomicDrawingEnd()
+ }
+ }
+
+ updateSurfaceSize()
+ libmpv.drawRender(surfaceSize, bufferDepth, ctx)
+
+ if needsICCUpdate {
+ needsICCUpdate = false
+ cocoaCB.updateICCProfile()
+ }
+ }
+
+ func updateSurfaceSize() {
+ var dims: [GLint] = [0, 0, 0, 0]
+ glGetIntegerv(GLenum(GL_VIEWPORT), &dims)
+ surfaceSize = NSSize(width: CGFloat(dims[2]), height: CGFloat(dims[3]))
+
+ if NSEqualSizes(surfaceSize, NSZeroSize) {
+ surfaceSize = bounds.size
+ surfaceSize.width *= contentsScale
+ surfaceSize.height *= contentsScale
+ }
+ }
+
+ func atomicDrawingStart() {
+ if draw == .normal {
+ NSDisableScreenUpdates()
+ draw = .atomic
+ }
+ }
+
+ func atomicDrawingEnd() {
+ if draw.rawValue >= Draw.atomic.rawValue {
+ NSEnableScreenUpdates()
+ draw = .normal
+ }
+ }
+
+ override func copyCGLPixelFormat(forDisplayMask mask: UInt32) -> CGLPixelFormatObj {
+ return cglPixelFormat
+ }
+
+ override func copyCGLContext(forPixelFormat pf: CGLPixelFormatObj) -> CGLContextObj {
+ contentsScale = cocoaCB.window?.backingScaleFactor ?? 1.0
+ return cglContext
+ }
+
+ let updateCallback: mpv_render_update_fn = { (ctx) in
+ let layer: GLLayer = unsafeBitCast(ctx, to: GLLayer.self)
+ layer.update()
+ }
+
+ override func display() {
+ displayLock.lock()
+ let isUpdate = needsFlip
+ super.display()
+ CATransaction.flush()
+ if isUpdate && needsFlip {
+ CGLSetCurrentContext(cglContext)
+ if libmpv.isRenderUpdateFrame() {
+ libmpv.drawRender(NSZeroSize, bufferDepth, cglContext, skip: true)
+ }
+ }
+ displayLock.unlock()
+ }
+
+ func update(force: Bool = false) {
+ if force { forceDraw = true }
+ queue.async {
+ if self.forceDraw || !self.inLiveResize {
+ self.needsFlip = true
+ self.display()
+ }
+ }
+ }
+
+ class func createPixelFormat(_ ccb: CocoaCB) -> (CGLPixelFormatObj, GLint) {
+ var pix: CGLPixelFormatObj?
+ var depth: GLint = 8
+ var err: CGLError = CGLError(rawValue: 0)
+ let swRender = ccb.libmpv.macOpts.cocoa_cb_sw_renderer
+
+ if swRender != 1 {
+ (pix, depth, err) = GLLayer.findPixelFormat(ccb)
+ }
+
+ if (err != kCGLNoError || pix == nil) && swRender != 0 {
+ (pix, depth, err) = GLLayer.findPixelFormat(ccb, software: true)
+ }
+
+ guard let pixelFormat = pix, err == kCGLNoError else {
+ ccb.log.sendError("Couldn't create any CGL pixel format")
+ exit(1)
+ }
+
+ return (pixelFormat, depth)
+ }
+
+ class func findPixelFormat(_ ccb: CocoaCB, software: Bool = false) -> (CGLPixelFormatObj?, GLint, CGLError) {
+ var pix: CGLPixelFormatObj?
+ var err: CGLError = CGLError(rawValue: 0)
+ var npix: GLint = 0
+
+ for ver in glVersions {
+ var glBase = software ? glFormatSoftwareBase : glFormatBase
+ glBase.insert(CGLPixelFormatAttribute(ver.rawValue), at: 1)
+
+ var glFormat = [glBase]
+ if ccb.libmpv.macOpts.cocoa_cb_10bit_context {
+ glFormat += [glFormat10Bit]
+ }
+ glFormat += glFormatOptional
+
+ if !ccb.libmpv.macOpts.macos_force_dedicated_gpu {
+ glFormat += [glFormatAutoGPU]
+ }
+
+ for index in stride(from: glFormat.count-1, through: 0, by: -1) {
+ let format = glFormat.flatMap { $0 } + [_CGLPixelFormatAttribute(rawValue: 0)]
+ err = CGLChoosePixelFormat(format, &pix, &npix)
+
+ if err == kCGLBadAttribute || err == kCGLBadPixelFormat || pix == nil {
+ glFormat.remove(at: index)
+ } else {
+ let attArray = format.map({ (value: _CGLPixelFormatAttribute) -> String in
+ return attributeLookUp[value.rawValue] ?? String(value.rawValue)
+ })
+
+ ccb.log.sendVerbose("Created CGL pixel format with attributes: " +
+ "\(attArray.joined(separator: ", "))")
+ return (pix, glFormat.contains(glFormat10Bit) ? 16 : 8, err)
+ }
+ }
+ }
+
+ let errS = String(cString: CGLErrorString(err))
+ ccb.log.sendWarning("Couldn't create a " +
+ "\(software ? "software" : "hardware accelerated") " +
+ "CGL pixel format: \(errS) (\(err.rawValue))")
+ if software == false && ccb.libmpv.macOpts.cocoa_cb_sw_renderer == -1 {
+ ccb.log.sendWarning("Falling back to software renderer")
+ }
+
+ return (pix, 8, err)
+ }
+
+ class func createContext(_ ccb: CocoaCB, _ pixelFormat: CGLPixelFormatObj) -> CGLContextObj {
+ var context: CGLContextObj?
+ let error = CGLCreateContext(pixelFormat, nil, &context)
+
+ guard let cglContext = context, error == kCGLNoError else {
+ let errS = String(cString: CGLErrorString(error))
+ ccb.log.sendError("Couldn't create a CGLContext: " + errS)
+ exit(1)
+ }
+
+ return cglContext
+ }
+}
diff --git a/video/out/mac/metal_layer.swift b/video/out/mac/metal_layer.swift
new file mode 100644
index 0000000..7cea87c
--- /dev/null
+++ b/video/out/mac/metal_layer.swift
@@ -0,0 +1,43 @@
+/*
+ * 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/>.
+ */
+
+import Cocoa
+
+class MetalLayer: CAMetalLayer {
+ unowned var common: MacCommon
+
+ init(common com: MacCommon) {
+ common = com
+ super.init()
+
+ pixelFormat = .rgba16Float
+ backgroundColor = NSColor.black.cgColor
+ }
+
+ // necessary for when the layer containing window changes the screen
+ override init(layer: Any) {
+ guard let oldLayer = layer as? MetalLayer else {
+ fatalError("init(layer: Any) passed an invalid layer")
+ }
+ common = oldLayer.common
+ super.init()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/video/out/mac/title_bar.swift b/video/out/mac/title_bar.swift
new file mode 100644
index 0000000..764c1ff
--- /dev/null
+++ b/video/out/mac/title_bar.swift
@@ -0,0 +1,229 @@
+/*
+ * 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/>.
+ */
+
+import Cocoa
+
+class TitleBar: NSVisualEffectView {
+ unowned var common: Common
+ var mpv: MPVHelper? { get { return common.mpv } }
+
+ var systemBar: NSView? {
+ get { return common.window?.standardWindowButton(.closeButton)?.superview }
+ }
+ static var height: CGFloat {
+ get { return NSWindow.frameRect(forContentRect: CGRect.zero, styleMask: .titled).size.height }
+ }
+ var buttons: [NSButton] {
+ get { return ([.closeButton, .miniaturizeButton, .zoomButton] as [NSWindow.ButtonType]).compactMap { common.window?.standardWindowButton($0) } }
+ }
+
+ override var material: NSVisualEffectView.Material {
+ get { return super.material }
+ set {
+ super.material = newValue
+ // fix for broken deprecated materials
+ if material == .light || material == .dark || material == .mediumLight ||
+ material == .ultraDark
+ {
+ state = .active
+ } else {
+ state = .followsWindowActiveState
+ }
+
+ }
+ }
+
+ init(frame: NSRect, window: NSWindow, common com: Common) {
+ let f = NSMakeRect(0, frame.size.height - TitleBar.height,
+ frame.size.width, TitleBar.height + 1)
+ common = com
+ super.init(frame: f)
+ buttons.forEach { $0.isHidden = true }
+ isHidden = true
+ alphaValue = 0
+ blendingMode = .withinWindow
+ autoresizingMask = [.width, .minYMargin]
+ systemBar?.alphaValue = 0
+ state = .followsWindowActiveState
+ wantsLayer = true
+
+ window.contentView?.addSubview(self, positioned: .above, relativeTo: nil)
+ window.titlebarAppearsTransparent = true
+ window.styleMask.insert(.fullSizeContentView)
+ set(appearance: Int(mpv?.macOpts.macos_title_bar_appearance ?? 0))
+ set(material: Int(mpv?.macOpts.macos_title_bar_material ?? 0))
+ set(color: mpv?.macOpts.macos_title_bar_color ?? "#00000000")
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ // catch these events so they are not propagated to the underlying view
+ override func mouseDown(with event: NSEvent) { }
+
+ override func mouseUp(with event: NSEvent) {
+ if event.clickCount > 1 {
+ let def = UserDefaults.standard
+ var action = def.string(forKey: "AppleActionOnDoubleClick")
+
+ // macOS 10.10 and earlier
+ if action == nil {
+ action = def.bool(forKey: "AppleMiniaturizeOnDoubleClick") == true ?
+ "Minimize" : "Maximize"
+ }
+
+ if action == "Minimize" {
+ window?.miniaturize(self)
+ } else if action == "Maximize" {
+ window?.zoom(self)
+ }
+ }
+
+ common.window?.isMoving = false
+ }
+
+ func set(appearance: Any) {
+ if appearance is Int {
+ window?.appearance = appearanceFrom(string: String(appearance as? Int ?? 0))
+ } else {
+ window?.appearance = appearanceFrom(string: appearance as? String ?? "auto")
+ }
+ }
+
+ func set(material: Any) {
+ if material is Int {
+ self.material = materialFrom(string: String(material as? Int ?? 0))
+ } else {
+ self.material = materialFrom(string: material as? String ?? "titlebar")
+ }
+ }
+
+ func set(color: Any) {
+ if color is String {
+ layer?.backgroundColor = NSColor(hex: color as? String ?? "#00000000").cgColor
+ } else {
+ let col = color as? m_color ?? m_color(r: 0, g: 0, b: 0, a: 0)
+ let red = CGFloat(col.r)/255
+ let green = CGFloat(col.g)/255
+ let blue = CGFloat(col.b)/255
+ let alpha = CGFloat(col.a)/255
+ layer?.backgroundColor = NSColor(calibratedRed: red, green: green,
+ blue: blue, alpha: alpha).cgColor
+ }
+ }
+
+ func show() {
+ guard let window = common.window else { return }
+ if !window.border && !window.isInFullscreen { return }
+ let loc = common.view?.convert(window.mouseLocationOutsideOfEventStream, from: nil)
+
+ buttons.forEach { $0.isHidden = false }
+ NSAnimationContext.runAnimationGroup({ (context) -> Void in
+ context.duration = 0.20
+ systemBar?.animator().alphaValue = 1
+ if !window.isInFullscreen && !window.isAnimating {
+ animator().alphaValue = 1
+ isHidden = false
+ }
+ }, completionHandler: nil )
+
+ if loc?.y ?? 0 > TitleBar.height {
+ hideDelayed()
+ } else {
+ NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hide), object: nil)
+ }
+ }
+
+ @objc func hide(_ duration: TimeInterval = 0.20) {
+ guard let window = common.window else { return }
+ if window.isInFullscreen && !window.isAnimating {
+ alphaValue = 0
+ isHidden = true
+ return
+ }
+ NSAnimationContext.runAnimationGroup({ (context) -> Void in
+ context.duration = duration
+ systemBar?.animator().alphaValue = 0
+ animator().alphaValue = 0
+ }, completionHandler: {
+ self.buttons.forEach { $0.isHidden = true }
+ self.isHidden = true
+ })
+ }
+
+ func hideDelayed() {
+ NSObject.cancelPreviousPerformRequests(withTarget: self,
+ selector: #selector(hide),
+ object: nil)
+ perform(#selector(hide), with: nil, afterDelay: 0.5)
+ }
+
+ func appearanceFrom(string: String) -> NSAppearance? {
+ switch string {
+ case "1", "aqua":
+ return NSAppearance(named: .aqua)
+ case "2", "darkAqua":
+ return NSAppearance(named: .darkAqua)
+ case "3", "vibrantLight":
+ return NSAppearance(named: .vibrantLight)
+ case "4", "vibrantDark":
+ return NSAppearance(named: .vibrantDark)
+ case "5", "aquaHighContrast":
+ return NSAppearance(named: .accessibilityHighContrastAqua)
+ case "6", "darkAquaHighContrast":
+ return NSAppearance(named: .accessibilityHighContrastDarkAqua)
+ case "7", "vibrantLightHighContrast":
+ return NSAppearance(named: .accessibilityHighContrastVibrantLight)
+ case "8", "vibrantDarkHighContrast":
+ return NSAppearance(named: .accessibilityHighContrastVibrantDark)
+ case "0", "auto": fallthrough
+ default:
+ return nil
+ }
+
+
+ let style = UserDefaults.standard.string(forKey: "AppleInterfaceStyle")
+ return appearanceFrom(string: style == nil ? "aqua" : "vibrantDark")
+ }
+
+ func materialFrom(string: String) -> NSVisualEffectView.Material {
+ switch string {
+ case "0", "titlebar": return .titlebar
+ case "1", "selection": return .selection
+ case "2,", "menu": return .menu
+ case "3", "popover": return .popover
+ case "4", "sidebar": return .sidebar
+ case "5,", "headerView": return .headerView
+ case "6", "sheet": return .sheet
+ case "7", "windowBackground": return .windowBackground
+ case "8", "hudWindow": return .hudWindow
+ case "9", "fullScreen": return .fullScreenUI
+ case "10", "toolTip": return .toolTip
+ case "11", "contentBackground": return .contentBackground
+ case "12", "underWindowBackground": return .underWindowBackground
+ case "13", "underPageBackground": return .underPageBackground
+ case "14", "dark": return .dark
+ case "15", "light": return .light
+ case "16", "mediumLight": return .mediumLight
+ case "17", "ultraDark": return .ultraDark
+ default: break
+ }
+
+ return .titlebar
+ }
+}
diff --git a/video/out/mac/view.swift b/video/out/mac/view.swift
new file mode 100644
index 0000000..c4776c3
--- /dev/null
+++ b/video/out/mac/view.swift
@@ -0,0 +1,297 @@
+/*
+ * 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/>.
+ */
+
+import Cocoa
+
+class View: NSView {
+ unowned var common: Common
+ var mpv: MPVHelper? { get { return common.mpv } }
+
+ var tracker: NSTrackingArea?
+ var hasMouseDown: Bool = false
+
+ override var isFlipped: Bool { return true }
+ override var acceptsFirstResponder: Bool { return true }
+
+
+ init(frame: NSRect, common com: Common) {
+ common = com
+ super.init(frame: frame)
+ autoresizingMask = [.width, .height]
+ wantsBestResolutionOpenGLSurface = true
+ registerForDraggedTypes([ .fileURL, .URL, .string ])
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func updateTrackingAreas() {
+ if let tracker = self.tracker {
+ removeTrackingArea(tracker)
+ }
+
+ tracker = NSTrackingArea(rect: bounds,
+ options: [.activeAlways, .mouseEnteredAndExited, .mouseMoved, .enabledDuringMouseDrag],
+ owner: self, userInfo: nil)
+ // here tracker is guaranteed to be none-nil
+ addTrackingArea(tracker!)
+
+ if containsMouseLocation() {
+ cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_LEAVE, 0)
+ }
+ }
+
+ override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
+ guard let types = sender.draggingPasteboard.types else { return [] }
+ if types.contains(.fileURL) || types.contains(.URL) || types.contains(.string) {
+ return .copy
+ }
+ return []
+ }
+
+ func isURL(_ str: String) -> Bool {
+ // force unwrapping is fine here, regex is guaranteed to be valid
+ let regex = try! NSRegularExpression(pattern: "^(https?|ftp)://[^\\s/$.?#].[^\\s]*$",
+ options: .caseInsensitive)
+ let isURL = regex.numberOfMatches(in: str,
+ options: [],
+ range: NSRange(location: 0, length: str.count))
+ return isURL > 0
+ }
+
+ override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
+ let pb = sender.draggingPasteboard
+ guard let types = pb.types else { return false }
+
+ if types.contains(.fileURL) || types.contains(.URL) {
+ if let urls = pb.readObjects(forClasses: [NSURL.self]) as? [URL] {
+ let files = urls.map { $0.absoluteString }
+ EventsResponder.sharedInstance().handleFilesArray(files)
+ return true
+ }
+ } else if types.contains(.string) {
+ guard let str = pb.string(forType: .string) else { return false }
+ var filesArray: [String] = []
+
+ for val in str.components(separatedBy: "\n") {
+ let url = val.trimmingCharacters(in: .whitespacesAndNewlines)
+ let path = (url as NSString).expandingTildeInPath
+ if isURL(url) {
+ filesArray.append(url)
+ } else if path.starts(with: "/") {
+ filesArray.append(path)
+ }
+ }
+ EventsResponder.sharedInstance().handleFilesArray(filesArray)
+ return true
+ }
+ return false
+ }
+
+ override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
+ return true
+ }
+
+ override func becomeFirstResponder() -> Bool {
+ return true
+ }
+
+ override func resignFirstResponder() -> Bool {
+ return true
+ }
+
+ override func mouseEntered(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_ENTER, 0)
+ }
+ common.updateCursorVisibility()
+ }
+
+ override func mouseExited(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_LEAVE, 0)
+ }
+ common.titleBar?.hide()
+ common.setCursorVisibility(true)
+ }
+
+ override func mouseMoved(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseMovement(event)
+ }
+ common.titleBar?.show()
+ }
+
+ override func mouseDragged(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseMovement(event)
+ }
+ }
+
+ override func mouseDown(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseDown(event)
+ }
+ }
+
+ override func mouseUp(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseUp(event)
+ }
+ common.window?.isMoving = false
+ }
+
+ override func rightMouseDown(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseDown(event)
+ }
+ }
+
+ override func rightMouseUp(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseUp(event)
+ }
+ }
+
+ override func otherMouseDown(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseDown(event)
+ }
+ }
+
+ override func otherMouseUp(with event: NSEvent) {
+ if mpv?.mouseEnabled() ?? true {
+ signalMouseUp(event)
+ }
+ }
+
+ override func magnify(with event: NSEvent) {
+ event.phase == .ended ?
+ common.windowDidEndLiveResize() : common.windowWillStartLiveResize()
+
+ common.window?.addWindowScale(Double(event.magnification))
+ }
+
+ func signalMouseDown(_ event: NSEvent) {
+ signalMouseEvent(event, MP_KEY_STATE_DOWN)
+ if event.clickCount > 1 {
+ signalMouseEvent(event, MP_KEY_STATE_UP)
+ }
+ }
+
+ func signalMouseUp(_ event: NSEvent) {
+ signalMouseEvent(event, MP_KEY_STATE_UP)
+ }
+
+ func signalMouseEvent(_ event: NSEvent, _ state: UInt32) {
+ hasMouseDown = state == MP_KEY_STATE_DOWN
+ let mpkey = getMpvButton(event)
+ cocoa_put_key_with_modifiers((mpkey | Int32(state)), Int32(event.modifierFlags.rawValue))
+ }
+
+ func signalMouseMovement(_ event: NSEvent) {
+ var point = convert(event.locationInWindow, from: nil)
+ point = convertToBacking(point)
+ point.y = -point.y
+
+ common.window?.updateMovableBackground(point)
+ if !(common.window?.isMoving ?? false) {
+ mpv?.setMousePosition(point)
+ }
+ }
+
+ func preciseScroll(_ event: NSEvent) {
+ var delta: Double
+ var cmd: Int32
+
+ if abs(event.deltaY) >= abs(event.deltaX) {
+ delta = Double(event.deltaY) * 0.1
+ cmd = delta > 0 ? SWIFT_WHEEL_UP : SWIFT_WHEEL_DOWN
+ } else {
+ delta = Double(event.deltaX) * 0.1
+ cmd = delta > 0 ? SWIFT_WHEEL_LEFT : SWIFT_WHEEL_RIGHT
+ }
+
+ mpv?.putAxis(cmd, delta: abs(delta))
+ }
+
+ override func scrollWheel(with event: NSEvent) {
+ if !(mpv?.mouseEnabled() ?? true) {
+ return
+ }
+
+ if event.hasPreciseScrollingDeltas {
+ preciseScroll(event)
+ } else {
+ let modifiers = event.modifierFlags
+ let deltaX = modifiers.contains(.shift) ? event.scrollingDeltaY : event.scrollingDeltaX
+ let deltaY = modifiers.contains(.shift) ? event.scrollingDeltaX : event.scrollingDeltaY
+ var mpkey: Int32
+
+ if abs(deltaY) >= abs(deltaX) {
+ mpkey = deltaY > 0 ? SWIFT_WHEEL_UP : SWIFT_WHEEL_DOWN
+ } else {
+ mpkey = deltaX > 0 ? SWIFT_WHEEL_LEFT : SWIFT_WHEEL_RIGHT
+ }
+
+ cocoa_put_key_with_modifiers(mpkey, Int32(modifiers.rawValue))
+ }
+ }
+
+ func containsMouseLocation() -> Bool {
+ var topMargin: CGFloat = 0.0
+ let menuBarHeight = NSApp.mainMenu?.menuBarHeight ?? 23.0
+
+ guard let window = common.window else { return false }
+ guard var vF = window.screen?.frame else { return false }
+
+ if window.isInFullscreen && (menuBarHeight > 0) {
+ topMargin = TitleBar.height + 1 + menuBarHeight
+ }
+
+ vF.size.height -= topMargin
+
+ let vFW = window.convertFromScreen(vF)
+ let vFV = convert(vFW, from: nil)
+ let pt = convert(window.mouseLocationOutsideOfEventStream, from: nil)
+
+ var clippedBounds = bounds.intersection(vFV)
+ if !window.isInFullscreen {
+ clippedBounds.origin.y += TitleBar.height
+ clippedBounds.size.height -= TitleBar.height
+ }
+ return clippedBounds.contains(pt)
+ }
+
+ func canHideCursor() -> Bool {
+ guard let window = common.window else { return false }
+ return !hasMouseDown && containsMouseLocation() && window.isKeyWindow
+ }
+
+ func getMpvButton(_ event: NSEvent) -> Int32 {
+ let buttonNumber = event.buttonNumber
+ switch (buttonNumber) {
+ case 0: return SWIFT_MBTN_LEFT
+ case 1: return SWIFT_MBTN_RIGHT
+ case 2: return SWIFT_MBTN_MID
+ case 3: return SWIFT_MBTN_BACK
+ case 4: return SWIFT_MBTN_FORWARD
+ default: return SWIFT_MBTN9 + Int32(buttonNumber - 5)
+ }
+ }
+}
diff --git a/video/out/mac/window.swift b/video/out/mac/window.swift
new file mode 100644
index 0000000..7b1a858
--- /dev/null
+++ b/video/out/mac/window.swift
@@ -0,0 +1,593 @@
+/*
+ * 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/>.
+ */
+
+import Cocoa
+
+class Window: NSWindow, NSWindowDelegate {
+ weak var common: Common! = nil
+ var mpv: MPVHelper? { get { return common.mpv } }
+
+ var targetScreen: NSScreen?
+ var previousScreen: NSScreen?
+ var currentScreen: NSScreen?
+ var unfScreen: NSScreen?
+
+ var unfsContentFrame: NSRect?
+ var isInFullscreen: Bool = false
+ var isMoving: Bool = false
+ var previousStyleMask: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable]
+
+ var isAnimating: Bool = false
+ let animationLock: NSCondition = NSCondition()
+
+ var unfsContentFramePixel: NSRect { get { return convertToBacking(unfsContentFrame ?? NSRect(x: 0, y: 0, width: 160, height: 90)) } }
+ var framePixel: NSRect { get { return convertToBacking(frame) } }
+
+ var keepAspect: Bool = true {
+ didSet {
+ if let contentViewFrame = contentView?.frame, !isInFullscreen {
+ unfsContentFrame = convertToScreen(contentViewFrame)
+ }
+
+ if keepAspect {
+ contentAspectRatio = unfsContentFrame?.size ?? contentAspectRatio
+ } else {
+ resizeIncrements = NSSize(width: 1.0, height: 1.0)
+ }
+ }
+ }
+
+ var border: Bool = true {
+ didSet { if !border { common.titleBar?.hide() } }
+ }
+
+ override var canBecomeKey: Bool { return true }
+ override var canBecomeMain: Bool { return true }
+
+ override var styleMask: NSWindow.StyleMask {
+ get { return super.styleMask }
+ set {
+ let responder = firstResponder
+ let windowTitle = title
+ previousStyleMask = super.styleMask
+ super.styleMask = newValue
+ makeFirstResponder(responder)
+ title = windowTitle
+ }
+ }
+
+ convenience init(contentRect: NSRect, screen: NSScreen?, view: NSView, common com: Common) {
+ self.init(contentRect: contentRect,
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
+ backing: .buffered, defer: false, screen: screen)
+
+ // workaround for an AppKit bug where the NSWindow can't be placed on a
+ // none Main screen NSScreen outside the Main screen's frame bounds
+ if let wantedScreen = screen, screen != NSScreen.main {
+ var absoluteWantedOrigin = contentRect.origin
+ absoluteWantedOrigin.x += wantedScreen.frame.origin.x
+ absoluteWantedOrigin.y += wantedScreen.frame.origin.y
+
+ if !NSEqualPoints(absoluteWantedOrigin, self.frame.origin) {
+ self.setFrameOrigin(absoluteWantedOrigin)
+ }
+ }
+
+ common = com
+ title = com.title
+ minSize = NSMakeSize(160, 90)
+ collectionBehavior = .fullScreenPrimary
+ delegate = self
+
+ if let cView = contentView {
+ cView.addSubview(view)
+ view.frame = cView.frame
+ unfsContentFrame = convertToScreen(cView.frame)
+ }
+
+ targetScreen = screen
+ currentScreen = screen
+ unfScreen = screen
+
+ if let app = NSApp as? Application {
+ app.menuBar.register(#selector(setHalfWindowSize), for: MPM_H_SIZE)
+ app.menuBar.register(#selector(setNormalWindowSize), for: MPM_N_SIZE)
+ app.menuBar.register(#selector(setDoubleWindowSize), for: MPM_D_SIZE)
+ app.menuBar.register(#selector(performMiniaturize(_:)), for: MPM_MINIMIZE)
+ app.menuBar.register(#selector(performZoom(_:)), for: MPM_ZOOM)
+ }
+ }
+
+ override func toggleFullScreen(_ sender: Any?) {
+ if isAnimating {
+ return
+ }
+
+ animationLock.lock()
+ isAnimating = true
+ animationLock.unlock()
+
+ targetScreen = common.getTargetScreen(forFullscreen: !isInFullscreen)
+ if targetScreen == nil && previousScreen == nil {
+ targetScreen = screen
+ } else if targetScreen == nil {
+ targetScreen = previousScreen
+ previousScreen = nil
+ } else {
+ previousScreen = screen
+ }
+
+ if let contentViewFrame = contentView?.frame, !isInFullscreen {
+ unfsContentFrame = convertToScreen(contentViewFrame)
+ unfScreen = screen
+ }
+ // move window to target screen when going to fullscreen
+ if let tScreen = targetScreen, !isInFullscreen && (tScreen != screen) {
+ let frame = calculateWindowPosition(for: tScreen, withoutBounds: false)
+ setFrame(frame, display: true)
+ }
+
+ if Bool(mpv?.opts.native_fs ?? true) {
+ super.toggleFullScreen(sender)
+ } else {
+ if !isInFullscreen {
+ setToFullScreen()
+ }
+ else {
+ setToWindow()
+ }
+ }
+ }
+
+ func customWindowsToEnterFullScreen(for window: NSWindow) -> [NSWindow]? {
+ return [window]
+ }
+
+ func customWindowsToExitFullScreen(for window: NSWindow) -> [NSWindow]? {
+ return [window]
+ }
+
+ func window(_ window: NSWindow, startCustomAnimationToEnterFullScreenWithDuration duration: TimeInterval) {
+ guard let tScreen = targetScreen else { return }
+ common.view?.layerContentsPlacement = .scaleProportionallyToFit
+ common.titleBar?.hide()
+ NSAnimationContext.runAnimationGroup({ (context) -> Void in
+ context.duration = getFsAnimationDuration(duration - 0.05)
+ window.animator().setFrame(tScreen.frame, display: true)
+ }, completionHandler: nil)
+ }
+
+ func window(_ window: NSWindow, startCustomAnimationToExitFullScreenWithDuration duration: TimeInterval) {
+ guard let tScreen = targetScreen, let currentScreen = screen else { return }
+ let newFrame = calculateWindowPosition(for: tScreen, withoutBounds: tScreen == screen)
+ let intermediateFrame = aspectFit(rect: newFrame, in: currentScreen.frame)
+ common.titleBar?.hide(0.0)
+
+ NSAnimationContext.runAnimationGroup({ (context) -> Void in
+ context.duration = 0.0
+ common.view?.layerContentsPlacement = .scaleProportionallyToFill
+ window.animator().setFrame(intermediateFrame, display: true)
+ }, completionHandler: {
+ NSAnimationContext.runAnimationGroup({ (context) -> Void in
+ context.duration = self.getFsAnimationDuration(duration - 0.05)
+ self.styleMask.remove(.fullScreen)
+ window.animator().setFrame(newFrame, display: true)
+ }, completionHandler: nil)
+ })
+ }
+
+ func windowDidEnterFullScreen(_ notification: Notification) {
+ isInFullscreen = true
+ mpv?.setOption(fullscreen: isInFullscreen)
+ common.updateCursorVisibility()
+ endAnimation(frame)
+ common.titleBar?.show()
+ }
+
+ func windowDidExitFullScreen(_ notification: Notification) {
+ guard let tScreen = targetScreen else { return }
+ isInFullscreen = false
+ mpv?.setOption(fullscreen: isInFullscreen)
+ endAnimation(calculateWindowPosition(for: tScreen, withoutBounds: targetScreen == screen))
+ common.view?.layerContentsPlacement = .scaleProportionallyToFit
+ }
+
+ func windowDidFailToEnterFullScreen(_ window: NSWindow) {
+ guard let tScreen = targetScreen else { return }
+ let newFrame = calculateWindowPosition(for: tScreen, withoutBounds: targetScreen == screen)
+ setFrame(newFrame, display: true)
+ endAnimation()
+ }
+
+ func windowDidFailToExitFullScreen(_ window: NSWindow) {
+ guard let targetFrame = targetScreen?.frame else { return }
+ setFrame(targetFrame, display: true)
+ endAnimation()
+ common.view?.layerContentsPlacement = .scaleProportionallyToFit
+ }
+
+ func endAnimation(_ newFrame: NSRect = NSZeroRect) {
+ if !NSEqualRects(newFrame, NSZeroRect) && isAnimating {
+ NSAnimationContext.runAnimationGroup({ (context) -> Void in
+ context.duration = 0.01
+ self.animator().setFrame(newFrame, display: true)
+ }, completionHandler: nil )
+ }
+
+ animationLock.lock()
+ isAnimating = false
+ animationLock.signal()
+ animationLock.unlock()
+ common.windowDidEndAnimation()
+ }
+
+ func setToFullScreen() {
+ guard let targetFrame = targetScreen?.frame else { return }
+
+ if #available(macOS 11.0, *) {
+ styleMask = .borderless
+ common.titleBar?.hide(0.0)
+ } else {
+ styleMask.insert(.fullScreen)
+ }
+
+ NSApp.presentationOptions = [.autoHideMenuBar, .autoHideDock]
+ setFrame(targetFrame, display: true)
+ endAnimation()
+ isInFullscreen = true
+ mpv?.setOption(fullscreen: isInFullscreen)
+ common.windowSetToFullScreen()
+ }
+
+ func setToWindow() {
+ guard let tScreen = targetScreen else { return }
+
+ if #available(macOS 11.0, *) {
+ styleMask = previousStyleMask
+ common.titleBar?.hide(0.0)
+ } else {
+ styleMask.remove(.fullScreen)
+ }
+
+ let newFrame = calculateWindowPosition(for: tScreen, withoutBounds: targetScreen == screen)
+ NSApp.presentationOptions = []
+ setFrame(newFrame, display: true)
+ endAnimation()
+ isInFullscreen = false
+ mpv?.setOption(fullscreen: isInFullscreen)
+ common.windowSetToWindow()
+ }
+
+ func waitForAnimation() {
+ animationLock.lock()
+ while(isAnimating){
+ animationLock.wait()
+ }
+ animationLock.unlock()
+ }
+
+ func getFsAnimationDuration(_ def: Double) -> Double {
+ let duration = mpv?.macOpts.macos_fs_animation_duration ?? -1
+ if duration < 0 {
+ return def
+ } else {
+ return Double(duration)/1000
+ }
+ }
+
+ func setOnTop(_ state: Bool, _ ontopLevel: Int) {
+ if state {
+ switch ontopLevel {
+ case -1:
+ level = .floating
+ case -2:
+ level = .statusBar + 1
+ case -3:
+ level = NSWindow.Level(Int(CGWindowLevelForKey(.desktopWindow)))
+ default:
+ level = NSWindow.Level(ontopLevel)
+ }
+ collectionBehavior.remove(.transient)
+ collectionBehavior.insert(.managed)
+ } else {
+ level = .normal
+ }
+ }
+
+ func setOnAllWorkspaces(_ state: Bool) {
+ if state {
+ collectionBehavior.insert(.canJoinAllSpaces)
+ } else {
+ collectionBehavior.remove(.canJoinAllSpaces)
+ }
+ }
+
+ func setMinimized(_ stateWanted: Bool) {
+ if isMiniaturized == stateWanted { return }
+
+ if stateWanted {
+ performMiniaturize(self)
+ } else {
+ deminiaturize(self)
+ }
+ }
+
+ func setMaximized(_ stateWanted: Bool) {
+ if isZoomed == stateWanted { return }
+
+ zoom(self)
+ }
+
+ func updateMovableBackground(_ pos: NSPoint) {
+ if !isInFullscreen {
+ isMovableByWindowBackground = mpv?.canBeDraggedAt(pos) ?? true
+ } else {
+ isMovableByWindowBackground = false
+ }
+ }
+
+ func updateFrame(_ rect: NSRect) {
+ if rect != frame {
+ let cRect = frameRect(forContentRect: rect)
+ unfsContentFrame = rect
+ setFrame(cRect, display: true)
+ common.windowDidUpdateFrame()
+ }
+ }
+
+ func updateSize(_ size: NSSize) {
+ if let currentSize = contentView?.frame.size, size != currentSize {
+ let newContentFrame = centeredContentSize(for: frame, size: size)
+ if !isInFullscreen {
+ updateFrame(newContentFrame)
+ } else {
+ unfsContentFrame = newContentFrame
+ }
+ }
+ }
+
+ override func setFrame(_ frameRect: NSRect, display flag: Bool) {
+ if frameRect.width < minSize.width || frameRect.height < minSize.height {
+ common.log.sendVerbose("tried to set too small window size: \(frameRect.size)")
+ return
+ }
+
+ super.setFrame(frameRect, display: flag)
+
+ if let size = unfsContentFrame?.size, keepAspect {
+ contentAspectRatio = size
+ }
+ }
+
+ func centeredContentSize(for rect: NSRect, size sz: NSSize) -> NSRect {
+ let cRect = contentRect(forFrameRect: rect)
+ let dx = (cRect.size.width - sz.width) / 2
+ let dy = (cRect.size.height - sz.height) / 2
+ return NSInsetRect(cRect, dx, dy)
+ }
+
+ func aspectFit(rect r: NSRect, in rTarget: NSRect) -> NSRect {
+ var s = rTarget.width / r.width
+ if r.height*s > rTarget.height {
+ s = rTarget.height / r.height
+ }
+ let w = r.width * s
+ let h = r.height * s
+ return NSRect(x: rTarget.midX - w/2, y: rTarget.midY - h/2, width: w, height: h)
+ }
+
+ func calculateWindowPosition(for tScreen: NSScreen, withoutBounds: Bool) -> NSRect {
+ guard let contentFrame = unfsContentFrame, let screen = unfScreen else {
+ return frame
+ }
+ var newFrame = frameRect(forContentRect: contentFrame)
+ let targetFrame = tScreen.frame
+ let targetVisibleFrame = tScreen.visibleFrame
+ let unfsScreenFrame = screen.frame
+ let visibleWindow = NSIntersectionRect(unfsScreenFrame, newFrame)
+
+ // calculate visible area of every side
+ let left = newFrame.origin.x - unfsScreenFrame.origin.x
+ let right = unfsScreenFrame.size.width -
+ (newFrame.origin.x - unfsScreenFrame.origin.x + newFrame.size.width)
+ let bottom = newFrame.origin.y - unfsScreenFrame.origin.y
+ let top = unfsScreenFrame.size.height -
+ (newFrame.origin.y - unfsScreenFrame.origin.y + newFrame.size.height)
+
+ // normalize visible areas, decide which one to take horizontal/vertical
+ var xPer = (unfsScreenFrame.size.width - visibleWindow.size.width)
+ var yPer = (unfsScreenFrame.size.height - visibleWindow.size.height)
+ if xPer != 0 { xPer = (left >= 0 || right < 0 ? left : right) / xPer }
+ if yPer != 0 { yPer = (bottom >= 0 || top < 0 ? bottom : top) / yPer }
+
+ // calculate visible area for every side for target screen
+ let xNewLeft = targetFrame.origin.x +
+ (targetFrame.size.width - visibleWindow.size.width) * xPer
+ let xNewRight = targetFrame.origin.x + targetFrame.size.width -
+ (targetFrame.size.width - visibleWindow.size.width) * xPer - newFrame.size.width
+ let yNewBottom = targetFrame.origin.y +
+ (targetFrame.size.height - visibleWindow.size.height) * yPer
+ let yNewTop = targetFrame.origin.y + targetFrame.size.height -
+ (targetFrame.size.height - visibleWindow.size.height) * yPer - newFrame.size.height
+
+ // calculate new coordinates, decide which one to take horizontal/vertical
+ newFrame.origin.x = left >= 0 || right < 0 ? xNewLeft : xNewRight
+ newFrame.origin.y = bottom >= 0 || top < 0 ? yNewBottom : yNewTop
+
+ // don't place new window on top of a visible menubar
+ let topMar = targetFrame.size.height -
+ (newFrame.origin.y - targetFrame.origin.y + newFrame.size.height)
+ let menuBarHeight = targetFrame.size.height -
+ (targetVisibleFrame.size.height + targetVisibleFrame.origin.y)
+ if topMar < menuBarHeight {
+ newFrame.origin.y -= top - menuBarHeight
+ }
+
+ if withoutBounds {
+ return newFrame
+ }
+
+ // screen bounds right and left
+ if newFrame.origin.x + newFrame.size.width > targetFrame.origin.x + targetFrame.size.width {
+ newFrame.origin.x = targetFrame.origin.x + targetFrame.size.width - newFrame.size.width
+ }
+ if newFrame.origin.x < targetFrame.origin.x {
+ newFrame.origin.x = targetFrame.origin.x
+ }
+
+ // screen bounds top and bottom
+ if newFrame.origin.y + newFrame.size.height > targetFrame.origin.y + targetFrame.size.height {
+ newFrame.origin.y = targetFrame.origin.y + targetFrame.size.height - newFrame.size.height
+ }
+ if newFrame.origin.y < targetFrame.origin.y {
+ newFrame.origin.y = targetFrame.origin.y
+ }
+ return newFrame
+ }
+
+ override func constrainFrameRect(_ frameRect: NSRect, to tScreen: NSScreen?) -> NSRect {
+ if (isAnimating && !isInFullscreen) || (!isAnimating && isInFullscreen ||
+ level == NSWindow.Level(Int(CGWindowLevelForKey(.desktopWindow))))
+ {
+ return frameRect
+ }
+
+ guard let ts: NSScreen = tScreen ?? screen ?? NSScreen.main else {
+ return frameRect
+ }
+ var nf: NSRect = frameRect
+ let of: NSRect = frame
+ let vf: NSRect = (isAnimating ? (targetScreen ?? ts) : ts).visibleFrame
+ let ncf: NSRect = contentRect(forFrameRect: nf)
+
+ // screen bounds top and bottom
+ if NSMaxY(nf) > NSMaxY(vf) {
+ nf.origin.y = NSMaxY(vf) - NSHeight(nf)
+ }
+ if NSMaxY(ncf) < NSMinY(vf) {
+ nf.origin.y = NSMinY(vf) + NSMinY(ncf) - NSMaxY(ncf)
+ }
+
+ // screen bounds right and left
+ if NSMinX(nf) > NSMaxX(vf) {
+ nf.origin.x = NSMaxX(vf) - NSWidth(nf)
+ }
+ if NSMaxX(nf) < NSMinX(vf) {
+ nf.origin.x = NSMinX(vf)
+ }
+
+ if NSHeight(nf) < NSHeight(vf) && NSHeight(of) > NSHeight(vf) && !isInFullscreen {
+ // If the window height is smaller than the visible frame, but it was
+ // bigger previously recenter the smaller window vertically. This is
+ // needed to counter the 'snap to top' behaviour.
+ nf.origin.y = (NSHeight(vf) - NSHeight(nf)) / 2
+ }
+ return nf
+ }
+
+ @objc func setNormalWindowSize() { setWindowScale(1.0) }
+ @objc func setHalfWindowSize() { setWindowScale(0.5) }
+ @objc func setDoubleWindowSize() { setWindowScale(2.0) }
+
+ func setWindowScale(_ scale: Double) {
+ mpv?.command("set window-scale \(scale)")
+ }
+
+ func addWindowScale(_ scale: Double) {
+ if !isInFullscreen {
+ mpv?.command("add window-scale \(scale)")
+ }
+ }
+
+ func windowDidChangeScreen(_ notification: Notification) {
+ if screen == nil {
+ return
+ }
+ if !isAnimating && (currentScreen != screen) {
+ previousScreen = screen
+ }
+ if currentScreen != screen {
+ common.updateDisplaylink()
+ common.windowDidChangeScreen()
+ }
+ currentScreen = screen
+ }
+
+ func windowDidChangeScreenProfile(_ notification: Notification) {
+ common.windowDidChangeScreenProfile()
+ }
+
+ func windowDidChangeBackingProperties(_ notification: Notification) {
+ common.windowDidChangeBackingProperties()
+ common.flagEvents(VO_EVENT_DPI)
+ }
+
+ func windowWillStartLiveResize(_ notification: Notification) {
+ common.windowWillStartLiveResize()
+ }
+
+ func windowDidEndLiveResize(_ notification: Notification) {
+ common.windowDidEndLiveResize()
+ mpv?.setOption(maximized: isZoomed)
+
+ if let contentViewFrame = contentView?.frame,
+ !isAnimating && !isInFullscreen
+ {
+ unfsContentFrame = convertToScreen(contentViewFrame)
+ }
+ }
+
+ func windowDidResize(_ notification: Notification) {
+ common.windowDidResize()
+ }
+
+ func windowShouldClose(_ sender: NSWindow) -> Bool {
+ cocoa_put_key(MP_KEY_CLOSE_WIN)
+ return false
+ }
+
+ func windowDidMiniaturize(_ notification: Notification) {
+ mpv?.setOption(minimized: true)
+ }
+
+ func windowDidDeminiaturize(_ notification: Notification) {
+ mpv?.setOption(minimized: false)
+ }
+
+ func windowDidResignKey(_ notification: Notification) {
+ common.setCursorVisibility(true)
+ }
+
+ func windowDidBecomeKey(_ notification: Notification) {
+ common.updateCursorVisibility()
+ }
+
+ func windowDidChangeOcclusionState(_ notification: Notification) {
+ if occlusionState.contains(.visible) {
+ common.windowDidChangeOcclusionState()
+ common.updateCursorVisibility()
+ }
+ }
+
+ func windowWillMove(_ notification: Notification) {
+ isMoving = true
+ }
+
+ func windowDidMove(_ notification: Notification) {
+ mpv?.setOption(maximized: isZoomed)
+ }
+}
diff --git a/video/out/mac_common.swift b/video/out/mac_common.swift
new file mode 100644
index 0000000..349712b
--- /dev/null
+++ b/video/out/mac_common.swift
@@ -0,0 +1,174 @@
+/*
+ * 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/>.
+ */
+
+import Cocoa
+
+class MacCommon: Common {
+ @objc var layer: MetalLayer?
+
+ var timer: PreciseTimer?
+ var swapTime: UInt64 = 0
+ let swapLock: NSCondition = NSCondition()
+
+ var needsICCUpdate: Bool = false
+
+ @objc init(_ vo: UnsafeMutablePointer<vo>) {
+ let newlog = mp_log_new(vo, vo.pointee.log, "mac")
+ super.init(newlog)
+ mpv = MPVHelper(vo, log)
+ timer = PreciseTimer(common: self)
+
+ DispatchQueue.main.sync {
+ layer = MetalLayer(common: self)
+ initMisc(vo)
+ }
+ }
+
+ @objc func config(_ vo: UnsafeMutablePointer<vo>) -> Bool {
+ mpv?.vo = vo
+
+ DispatchQueue.main.sync {
+ let previousActiveApp = getActiveApp()
+ initApp()
+
+ let (_, _, wr) = getInitProperties(vo)
+
+ guard let layer = self.layer else {
+ log.sendError("Something went wrong, no MetalLayer was initialized")
+ exit(1)
+ }
+
+ if window == nil {
+ initView(vo, layer)
+ initWindow(vo, previousActiveApp)
+ initWindowState()
+ }
+
+ if !NSEqualSizes(window?.unfsContentFramePixel.size ?? NSZeroSize, wr.size) {
+ window?.updateSize(wr.size)
+ }
+
+ windowDidResize()
+ needsICCUpdate = true
+ }
+
+ return true
+ }
+
+ @objc func uninit(_ vo: UnsafeMutablePointer<vo>) {
+ window?.waitForAnimation()
+
+ timer?.terminate()
+
+ DispatchQueue.main.sync {
+ window?.delegate = nil
+ window?.close()
+
+ uninitCommon()
+ }
+ }
+
+ @objc func swapBuffer() {
+ if mpv?.macOpts.macos_render_timer ?? Int32(RENDER_TIMER_CALLBACK) != RENDER_TIMER_SYSTEM {
+ swapLock.lock()
+ while(swapTime < 1) {
+ swapLock.wait()
+ }
+ swapTime = 0
+ swapLock.unlock()
+ }
+
+ if needsICCUpdate {
+ needsICCUpdate = false
+ updateICCProfile()
+ }
+ }
+
+ func updateRenderSize(_ size: NSSize) {
+ mpv?.vo.pointee.dwidth = Int32(size.width)
+ mpv?.vo.pointee.dheight = Int32(size.height)
+ flagEvents(VO_EVENT_RESIZE | VO_EVENT_EXPOSE)
+ }
+
+ override func displayLinkCallback(_ displayLink: CVDisplayLink,
+ _ inNow: UnsafePointer<CVTimeStamp>,
+ _ inOutputTime: UnsafePointer<CVTimeStamp>,
+ _ flagsIn: CVOptionFlags,
+ _ flagsOut: UnsafeMutablePointer<CVOptionFlags>) -> CVReturn
+ {
+ let frameTimer = mpv?.macOpts.macos_render_timer ?? Int32(RENDER_TIMER_CALLBACK)
+ let signalSwap = {
+ self.swapLock.lock()
+ self.swapTime += 1
+ self.swapLock.signal()
+ self.swapLock.unlock()
+ }
+
+ if frameTimer != RENDER_TIMER_SYSTEM {
+ if let timer = self.timer, frameTimer == RENDER_TIMER_PRECISE {
+ timer.scheduleAt(time: inOutputTime.pointee.hostTime, closure: signalSwap)
+ return kCVReturnSuccess
+ }
+
+ signalSwap()
+ }
+
+ return kCVReturnSuccess
+ }
+
+ override func startDisplayLink(_ vo: UnsafeMutablePointer<vo>) {
+ super.startDisplayLink(vo)
+ timer?.updatePolicy(periodSeconds: 1 / currentFps())
+ }
+
+ override func updateDisplaylink() {
+ super.updateDisplaylink()
+ timer?.updatePolicy(periodSeconds: 1 / currentFps())
+ }
+
+ override func lightSensorUpdate() {
+ flagEvents(VO_EVENT_AMBIENT_LIGHTING_CHANGED)
+ }
+
+ @objc override func updateICCProfile() {
+ guard let colorSpace = window?.screen?.colorSpace else {
+ log.sendWarning("Couldn't update ICC Profile, no color space available")
+ return
+ }
+
+ layer?.colorspace = colorSpace.cgColorSpace
+ flagEvents(VO_EVENT_ICC_PROFILE_CHANGED)
+ }
+
+ override func windowDidResize() {
+ guard let window = window else {
+ log.sendWarning("No window available on window resize event")
+ return
+ }
+
+ updateRenderSize(window.framePixel.size)
+ }
+
+ override func windowDidChangeScreenProfile() {
+ needsICCUpdate = true
+ }
+
+ override func windowDidChangeBackingProperties() {
+ layer?.contentsScale = window?.backingScaleFactor ?? 1
+ windowDidResize()
+ }
+}
diff --git a/video/out/meson.build b/video/out/meson.build
new file mode 100644
index 0000000..e2808d6
--- /dev/null
+++ b/video/out/meson.build
@@ -0,0 +1,51 @@
+wl_protocol_dir = wayland['deps'][2].get_variable(pkgconfig: 'pkgdatadir', internal: 'pkgdatadir')
+protocols = [[wl_protocol_dir, 'stable/presentation-time/presentation-time.xml'],
+ [wl_protocol_dir, 'stable/viewporter/viewporter.xml'],
+ [wl_protocol_dir, 'stable/xdg-shell/xdg-shell.xml'],
+ [wl_protocol_dir, 'unstable/idle-inhibit/idle-inhibit-unstable-v1.xml'],
+ [wl_protocol_dir, 'unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml'],
+ [wl_protocol_dir, 'unstable/xdg-decoration/xdg-decoration-unstable-v1.xml']]
+wl_protocols_source = []
+wl_protocols_headers = []
+
+foreach v: ['1.27', '1.31', '1.32']
+ features += {'wayland-protocols-' + v.replace('.', '-'):
+ wayland['deps'][2].version().version_compare('>=' + v)}
+endforeach
+
+if features['wayland-protocols-1-27']
+ protocols += [[wl_protocol_dir, 'staging/content-type/content-type-v1.xml'],
+ [wl_protocol_dir, 'staging/single-pixel-buffer/single-pixel-buffer-v1.xml']]
+endif
+if features['wayland-protocols-1-31']
+ protocols += [[wl_protocol_dir, 'staging/fractional-scale/fractional-scale-v1.xml']]
+endif
+if features['wayland-protocols-1-32']
+ protocols += [[wl_protocol_dir, 'staging/cursor-shape/cursor-shape-v1.xml'],
+ [wl_protocol_dir, 'unstable/tablet/tablet-unstable-v2.xml']] # required by cursor-shape
+endif
+
+foreach p: protocols
+ xml = join_paths(p)
+ wl_protocols_source += custom_target(xml.underscorify() + '_c',
+ input: xml,
+ output: '@BASENAME@.c',
+ command: [wayland['scanner'], 'private-code', '@INPUT@', '@OUTPUT@'],
+ )
+ wl_protocols_headers += custom_target(xml.underscorify() + '_h',
+ input: xml,
+ output: '@BASENAME@.h',
+ command: [wayland['scanner'], 'client-header', '@INPUT@', '@OUTPUT@'],
+ )
+endforeach
+
+lib_client_protocols = static_library('protocols',
+ wl_protocols_source + wl_protocols_headers,
+ dependencies: wayland['deps'][0])
+
+client_protocols = declare_dependency(link_with: lib_client_protocols,
+ sources: wl_protocols_headers)
+
+dependencies += [client_protocols, wayland['deps']]
+
+sources += files('wayland_common.c')
diff --git a/video/out/opengl/angle_dynamic.c b/video/out/opengl/angle_dynamic.c
new file mode 100644
index 0000000..2483828
--- /dev/null
+++ b/video/out/opengl/angle_dynamic.c
@@ -0,0 +1,39 @@
+#include <windows.h>
+
+#include "angle_dynamic.h"
+
+#include "common/common.h"
+#include "osdep/threads.h"
+
+#if HAVE_EGL_ANGLE_LIB
+bool angle_load(void)
+{
+ return true;
+}
+#else
+#define ANGLE_DECL(NAME, VAR) \
+ VAR;
+ANGLE_FNS(ANGLE_DECL)
+
+static bool angle_loaded;
+static mp_once angle_load_once = MP_STATIC_ONCE_INITIALIZER;
+
+static void angle_do_load(void)
+{
+ // Note: we let this handle "leak", as the functions remain valid forever.
+ HANDLE angle_dll = LoadLibraryW(L"LIBEGL.DLL");
+ if (!angle_dll)
+ return;
+#define ANGLE_LOAD_ENTRY(NAME, VAR) \
+ NAME = (void *)GetProcAddress(angle_dll, #NAME); \
+ if (!NAME) return;
+ ANGLE_FNS(ANGLE_LOAD_ENTRY)
+ angle_loaded = true;
+}
+
+bool angle_load(void)
+{
+ mp_exec_once(&angle_load_once, angle_do_load);
+ return angle_loaded;
+}
+#endif
diff --git a/video/out/opengl/angle_dynamic.h b/video/out/opengl/angle_dynamic.h
new file mode 100644
index 0000000..d419c3f
--- /dev/null
+++ b/video/out/opengl/angle_dynamic.h
@@ -0,0 +1,89 @@
+// Based on Khronos headers, thus MIT licensed.
+
+#ifndef MP_ANGLE_DYNAMIC_H
+#define MP_ANGLE_DYNAMIC_H
+
+#include <stdbool.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "config.h"
+
+#define ANGLE_FNS(FN) \
+ FN(eglBindAPI, EGLBoolean (*EGLAPIENTRY PFN_eglBindAPI)(EGLenum)) \
+ FN(eglBindTexImage, EGLBoolean (*EGLAPIENTRY PFN_eglBindTexImage) \
+ (EGLDisplay, EGLSurface, EGLint)) \
+ FN(eglChooseConfig, EGLBoolean (*EGLAPIENTRY PFN_eglChooseConfig) \
+ (EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *)) \
+ FN(eglCreateContext, EGLContext (*EGLAPIENTRY PFN_eglCreateContext) \
+ (EGLDisplay, EGLConfig, EGLContext, const EGLint *)) \
+ FN(eglCreatePbufferFromClientBuffer, EGLSurface (*EGLAPIENTRY \
+ PFN_eglCreatePbufferFromClientBuffer)(EGLDisplay, EGLenum, \
+ EGLClientBuffer, EGLConfig, const EGLint *)) \
+ FN(eglCreateWindowSurface, EGLSurface (*EGLAPIENTRY \
+ PFN_eglCreateWindowSurface)(EGLDisplay, EGLConfig, \
+ EGLNativeWindowType, const EGLint *)) \
+ FN(eglDestroyContext, EGLBoolean (*EGLAPIENTRY PFN_eglDestroyContext) \
+ (EGLDisplay, EGLContext)) \
+ FN(eglDestroySurface, EGLBoolean (*EGLAPIENTRY PFN_eglDestroySurface) \
+ (EGLDisplay, EGLSurface)) \
+ FN(eglGetConfigAttrib, EGLBoolean (*EGLAPIENTRY PFN_eglGetConfigAttrib) \
+ (EGLDisplay, EGLConfig, EGLint, EGLint *)) \
+ FN(eglGetCurrentContext, EGLContext (*EGLAPIENTRY \
+ PFN_eglGetCurrentContext)(void)) \
+ FN(eglGetCurrentDisplay, EGLDisplay (*EGLAPIENTRY \
+ PFN_eglGetCurrentDisplay)(void)) \
+ FN(eglGetDisplay, EGLDisplay (*EGLAPIENTRY PFN_eglGetDisplay) \
+ (EGLNativeDisplayType)) \
+ FN(eglGetError, EGLint (*EGLAPIENTRY PFN_eglGetError)(void)) \
+ FN(eglGetProcAddress, void *(*EGLAPIENTRY \
+ PFN_eglGetProcAddress)(const char *)) \
+ FN(eglInitialize, EGLBoolean (*EGLAPIENTRY PFN_eglInitialize) \
+ (EGLDisplay, EGLint *, EGLint *)) \
+ FN(eglMakeCurrent, EGLBoolean (*EGLAPIENTRY PFN_eglMakeCurrent) \
+ (EGLDisplay, EGLSurface, EGLSurface, EGLContext)) \
+ FN(eglQueryString, const char *(*EGLAPIENTRY PFN_eglQueryString) \
+ (EGLDisplay, EGLint)) \
+ FN(eglSwapBuffers, EGLBoolean (*EGLAPIENTRY PFN_eglSwapBuffers) \
+ (EGLDisplay, EGLSurface)) \
+ FN(eglSwapInterval, EGLBoolean (*EGLAPIENTRY PFN_eglSwapInterval) \
+ (EGLDisplay, EGLint)) \
+ FN(eglReleaseTexImage, EGLBoolean (*EGLAPIENTRY PFN_eglReleaseTexImage) \
+ (EGLDisplay, EGLSurface, EGLint)) \
+ FN(eglTerminate, EGLBoolean (*EGLAPIENTRY PFN_eglTerminate)(EGLDisplay)) \
+ FN(eglWaitClient, EGLBoolean (*EGLAPIENTRY PFN_eglWaitClient)(void))
+
+#define ANGLE_EXT_DECL(NAME, VAR) \
+ extern VAR;
+ANGLE_FNS(ANGLE_EXT_DECL)
+
+bool angle_load(void);
+
+// Source compatibility to statically linked ANGLE.
+#if !HAVE_EGL_ANGLE_LIB
+#define eglBindAPI PFN_eglBindAPI
+#define eglBindTexImage PFN_eglBindTexImage
+#define eglChooseConfig PFN_eglChooseConfig
+#define eglCreateContext PFN_eglCreateContext
+#define eglCreatePbufferFromClientBuffer PFN_eglCreatePbufferFromClientBuffer
+#define eglCreateWindowSurface PFN_eglCreateWindowSurface
+#define eglDestroyContext PFN_eglDestroyContext
+#define eglDestroySurface PFN_eglDestroySurface
+#define eglGetConfigAttrib PFN_eglGetConfigAttrib
+#define eglGetCurrentContext PFN_eglGetCurrentContext
+#define eglGetCurrentDisplay PFN_eglGetCurrentDisplay
+#define eglGetDisplay PFN_eglGetDisplay
+#define eglGetError PFN_eglGetError
+#define eglGetProcAddress PFN_eglGetProcAddress
+#define eglInitialize PFN_eglInitialize
+#define eglMakeCurrent PFN_eglMakeCurrent
+#define eglQueryString PFN_eglQueryString
+#define eglReleaseTexImage PFN_eglReleaseTexImage
+#define eglSwapBuffers PFN_eglSwapBuffers
+#define eglSwapInterval PFN_eglSwapInterval
+#define eglTerminate PFN_eglTerminate
+#define eglWaitClient PFN_eglWaitClient
+#endif
+
+#endif
diff --git a/video/out/opengl/common.c b/video/out/opengl/common.c
new file mode 100644
index 0000000..ee26508
--- /dev/null
+++ b/video/out/opengl/common.c
@@ -0,0 +1,694 @@
+/*
+ * common OpenGL routines
+ *
+ * copyleft (C) 2005-2010 Reimar Döffinger <Reimar.Doeffinger@gmx.de>
+ * Special thanks go to the xine team and Matthias Hopf, whose video_out_opengl.c
+ * gave me lots of good ideas.
+ *
+ * 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 <stddef.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <stdbool.h>
+#include <math.h>
+#include <assert.h>
+
+#include "common.h"
+#include "common/common.h"
+#include "utils.h"
+
+// This guesses if the current GL context is a suspected software renderer.
+static bool is_software_gl(GL *gl)
+{
+ const char *renderer = gl->GetString(GL_RENDERER);
+ // Note we don't attempt to blacklist Microsoft's fallback implementation.
+ // It only provides OpenGL 1.1 and will be skipped anyway.
+ return !renderer ||
+ strcmp(renderer, "Software Rasterizer") == 0 ||
+ strstr(renderer, "llvmpipe") ||
+ strstr(renderer, "softpipe") ||
+ strcmp(renderer, "Mesa X11") == 0 ||
+ strcmp(renderer, "Apple Software Renderer") == 0;
+}
+
+// This guesses whether our DR path is fast or slow
+static bool is_fast_dr(GL *gl)
+{
+ const char *vendor = gl->GetString(GL_VENDOR);
+ if (!vendor)
+ return false;
+
+ return strcasecmp(vendor, "AMD") == 0 ||
+ strcasecmp(vendor, "NVIDIA Corporation") == 0 ||
+ strcasecmp(vendor, "ATI Technologies Inc.") == 0; // AMD on Windows
+}
+
+static void GLAPIENTRY dummy_glBindFramebuffer(GLenum target, GLuint framebuffer)
+{
+ assert(framebuffer == 0);
+}
+
+#define FN_OFFS(name) offsetof(GL, name)
+
+#define DEF_FN(name) {FN_OFFS(name), "gl" # name}
+#define DEF_FN_NAME(name, str) {FN_OFFS(name), str}
+
+struct gl_function {
+ ptrdiff_t offset;
+ char *name;
+};
+
+struct gl_functions {
+ const char *extension; // introduced with this extension in any version
+ int provides; // bitfield of MPGL_CAP_* constants
+ int ver_core; // introduced as required function
+ int ver_es_core; // introduced as required GL ES function
+ int ver_exclude; // not applicable to versions >= ver_exclude
+ int ver_es_exclude; // same for GLES
+ const struct gl_function *functions;
+};
+
+#define MAX_FN_COUNT 100 // max functions per gl_functions section
+
+// Note: to keep the number of sections low, some functions are in multiple
+// sections (if there are tricky combinations of GL/ES versions)
+static const struct gl_functions gl_functions[] = {
+ // GL 2.1+ desktop and GLES 2.0+ (anything we support)
+ // Probably all of these are in GL 2.0 too, but we require GLSL 120.
+ {
+ .ver_core = 210,
+ .ver_es_core = 200,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(ActiveTexture),
+ DEF_FN(AttachShader),
+ DEF_FN(BindAttribLocation),
+ DEF_FN(BindBuffer),
+ DEF_FN(BindTexture),
+ DEF_FN(BlendFuncSeparate),
+ DEF_FN(BufferData),
+ DEF_FN(BufferSubData),
+ DEF_FN(Clear),
+ DEF_FN(ClearColor),
+ DEF_FN(CompileShader),
+ DEF_FN(CreateProgram),
+ DEF_FN(CreateShader),
+ DEF_FN(DeleteBuffers),
+ DEF_FN(DeleteProgram),
+ DEF_FN(DeleteShader),
+ DEF_FN(DeleteTextures),
+ DEF_FN(Disable),
+ DEF_FN(DisableVertexAttribArray),
+ DEF_FN(DrawArrays),
+ DEF_FN(Enable),
+ DEF_FN(EnableVertexAttribArray),
+ DEF_FN(Finish),
+ DEF_FN(Flush),
+ DEF_FN(GenBuffers),
+ DEF_FN(GenTextures),
+ DEF_FN(GetAttribLocation),
+ DEF_FN(GetError),
+ DEF_FN(GetIntegerv),
+ DEF_FN(GetProgramInfoLog),
+ DEF_FN(GetProgramiv),
+ DEF_FN(GetShaderInfoLog),
+ DEF_FN(GetShaderiv),
+ DEF_FN(GetString),
+ DEF_FN(GetUniformLocation),
+ DEF_FN(LinkProgram),
+ DEF_FN(PixelStorei),
+ DEF_FN(ReadPixels),
+ DEF_FN(Scissor),
+ DEF_FN(ShaderSource),
+ DEF_FN(TexImage2D),
+ DEF_FN(TexParameteri),
+ DEF_FN(TexSubImage2D),
+ DEF_FN(Uniform1f),
+ DEF_FN(Uniform2f),
+ DEF_FN(Uniform3f),
+ DEF_FN(Uniform1i),
+ DEF_FN(UniformMatrix2fv),
+ DEF_FN(UniformMatrix3fv),
+ DEF_FN(UseProgram),
+ DEF_FN(VertexAttribPointer),
+ DEF_FN(Viewport),
+ {0}
+ },
+ },
+ // GL 2.1+ desktop only (and GLSL 120 shaders)
+ {
+ .ver_core = 210,
+ .provides = MPGL_CAP_ROW_LENGTH | MPGL_CAP_1D_TEX,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(DrawBuffer),
+ DEF_FN(GetTexLevelParameteriv),
+ DEF_FN(ReadBuffer),
+ DEF_FN(TexImage1D),
+ DEF_FN(UnmapBuffer),
+ {0}
+ },
+ },
+ // GL 2.1 has this as extension only.
+ {
+ .ver_exclude = 300,
+ .ver_es_exclude = 300,
+ .extension = "GL_ARB_map_buffer_range",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(MapBufferRange),
+ {0}
+ },
+ },
+ // GL 3.0+ and ES 3.x core only functions.
+ {
+ .ver_core = 300,
+ .ver_es_core = 300,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(BindBufferBase),
+ DEF_FN(BlitFramebuffer),
+ DEF_FN(GetStringi),
+ DEF_FN(MapBufferRange),
+ // for ES 3.0
+ DEF_FN(ReadBuffer),
+ DEF_FN(UnmapBuffer),
+ {0}
+ },
+ },
+ // For ES 3.1 core
+ {
+ .ver_es_core = 310,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(GetTexLevelParameteriv),
+ {0}
+ },
+ },
+ {
+ .ver_core = 210,
+ .ver_es_core = 300,
+ .provides = MPGL_CAP_3D_TEX,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(TexImage3D),
+ {0}
+ },
+ },
+ // Useful for ES 2.0
+ {
+ .ver_core = 110,
+ .ver_es_core = 300,
+ .extension = "GL_EXT_unpack_subimage",
+ .provides = MPGL_CAP_ROW_LENGTH,
+ },
+ // Framebuffers, extension in GL 2.x, core in GL 3.x core.
+ {
+ .ver_core = 300,
+ .ver_es_core = 200,
+ .extension = "GL_ARB_framebuffer_object",
+ .provides = MPGL_CAP_FB,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(BindFramebuffer),
+ DEF_FN(GenFramebuffers),
+ DEF_FN(DeleteFramebuffers),
+ DEF_FN(CheckFramebufferStatus),
+ DEF_FN(FramebufferTexture2D),
+ DEF_FN(GetFramebufferAttachmentParameteriv),
+ {0}
+ },
+ },
+ // VAOs, extension in GL 2.x, core in GL 3.x core.
+ {
+ .ver_core = 300,
+ .ver_es_core = 300,
+ .extension = "GL_ARB_vertex_array_object",
+ .provides = MPGL_CAP_VAO,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(GenVertexArrays),
+ DEF_FN(BindVertexArray),
+ DEF_FN(DeleteVertexArrays),
+ {0}
+ }
+ },
+ // GL_RED / GL_RG textures, extension in GL 2.x, core in GL 3.x core.
+ {
+ .ver_core = 300,
+ .ver_es_core = 300,
+ .extension = "GL_ARB_texture_rg",
+ .provides = MPGL_CAP_TEX_RG,
+ },
+ {
+ .ver_core = 300,
+ .ver_es_core = 300,
+ .extension = "GL_EXT_texture_rg",
+ .provides = MPGL_CAP_TEX_RG,
+ },
+ // GL_R16 etc.
+ {
+ .extension = "GL_EXT_texture_norm16",
+ .provides = MPGL_CAP_EXT16,
+ .ver_exclude = 1, // never in desktop GL
+ },
+ // Float texture support for GL 2.x
+ {
+ .extension = "GL_ARB_texture_float",
+ .provides = MPGL_CAP_ARB_FLOAT,
+ .ver_exclude = 300,
+ .ver_es_exclude = 1,
+ },
+ // 16 bit float textures that can be rendered to in GLES
+ {
+ .extension = "GL_EXT_color_buffer_half_float",
+ .provides = MPGL_CAP_EXT_CR_HFLOAT,
+ .ver_exclude = 1,
+ .ver_es_exclude = 320,
+ },
+ {
+ .ver_core = 320,
+ .ver_es_core = 300,
+ .extension = "GL_ARB_sync",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(FenceSync),
+ DEF_FN(ClientWaitSync),
+ DEF_FN(DeleteSync),
+ {0}
+ },
+ },
+ {
+ .ver_core = 330,
+ .extension = "GL_ARB_timer_query",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(GenQueries),
+ DEF_FN(DeleteQueries),
+ DEF_FN(BeginQuery),
+ DEF_FN(EndQuery),
+ DEF_FN(QueryCounter),
+ DEF_FN(IsQuery),
+ DEF_FN(GetQueryObjectiv),
+ DEF_FN(GetQueryObjecti64v),
+ DEF_FN(GetQueryObjectuiv),
+ DEF_FN(GetQueryObjectui64v),
+ {0}
+ },
+ },
+ {
+ .extension = "GL_EXT_disjoint_timer_query",
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(GenQueries, "glGenQueriesEXT"),
+ DEF_FN_NAME(DeleteQueries, "glDeleteQueriesEXT"),
+ DEF_FN_NAME(BeginQuery, "glBeginQueryEXT"),
+ DEF_FN_NAME(EndQuery, "glEndQueryEXT"),
+ DEF_FN_NAME(QueryCounter, "glQueryCounterEXT"),
+ DEF_FN_NAME(IsQuery, "glIsQueryEXT"),
+ DEF_FN_NAME(GetQueryObjectiv, "glGetQueryObjectivEXT"),
+ DEF_FN_NAME(GetQueryObjecti64v, "glGetQueryObjecti64vEXT"),
+ DEF_FN_NAME(GetQueryObjectuiv, "glGetQueryObjectuivEXT"),
+ DEF_FN_NAME(GetQueryObjectui64v, "glGetQueryObjectui64vEXT"),
+ {0}
+ },
+ },
+ {
+ .ver_core = 430,
+ .extension = "GL_ARB_invalidate_subdata",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(InvalidateTexImage),
+ {0}
+ },
+ },
+ {
+ .ver_core = 430,
+ .ver_es_core = 300,
+ .functions = (const struct gl_function[]) {
+ DEF_FN(InvalidateFramebuffer),
+ {0}
+ },
+ },
+ {
+ .ver_core = 410,
+ .ver_es_core = 300,
+ .extension = "GL_ARB_get_program_binary",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(GetProgramBinary),
+ DEF_FN(ProgramBinary),
+ {0}
+ },
+ },
+ {
+ .ver_core = 440,
+ .extension = "GL_ARB_buffer_storage",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(BufferStorage),
+ {0}
+ },
+ },
+ // Equivalent extension for ES
+ {
+ .extension = "GL_EXT_buffer_storage",
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(BufferStorage, "glBufferStorageEXT"),
+ {0}
+ },
+ },
+ {
+ .ver_core = 420,
+ .ver_es_core = 310,
+ .extension = "GL_ARB_shader_image_load_store",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(BindImageTexture),
+ DEF_FN(MemoryBarrier),
+ {0}
+ },
+ },
+ {
+ .ver_core = 310,
+ .ver_es_core = 300,
+ .extension = "GL_ARB_uniform_buffer_object",
+ .provides = MPGL_CAP_UBO,
+ },
+ {
+ .ver_core = 430,
+ .ver_es_core = 310,
+ .extension = "GL_ARB_shader_storage_buffer_object",
+ .provides = MPGL_CAP_SSBO,
+ },
+ {
+ .ver_core = 430,
+ .ver_es_core = 310,
+ .extension = "GL_ARB_compute_shader",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(DispatchCompute),
+ {0},
+ },
+ },
+ {
+ .ver_core = 430,
+ .extension = "GL_ARB_arrays_of_arrays",
+ .provides = MPGL_CAP_NESTED_ARRAY,
+ },
+ // Swap control, always an OS specific extension
+ // The OSX code loads this manually.
+ {
+ .extension = "GLX_SGI_swap_control",
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(SwapInterval, "glXSwapIntervalSGI"),
+ {0},
+ },
+ },
+ // This one overrides GLX_SGI_swap_control on platforms using mesa. The
+ // only difference is that it supports glXSwapInterval(0).
+ {
+ .extension = "GLX_MESA_swap_control",
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(SwapInterval, "glXSwapIntervalMESA"),
+ {0},
+ },
+ },
+ {
+ .extension = "WGL_EXT_swap_control",
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(SwapInterval, "wglSwapIntervalEXT"),
+ {0},
+ },
+ },
+ {
+ .extension = "GLX_SGI_video_sync",
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(GetVideoSync, "glXGetVideoSyncSGI"),
+ DEF_FN_NAME(WaitVideoSync, "glXWaitVideoSyncSGI"),
+ {0},
+ },
+ },
+ // For gl_hwdec_vdpau.c
+ // http://www.opengl.org/registry/specs/NV/vdpau_interop.txt
+ {
+ .extension = "GL_NV_vdpau_interop",
+ .provides = MPGL_CAP_VDPAU,
+ .functions = (const struct gl_function[]) {
+ // (only functions needed by us)
+ DEF_FN(VDPAUInitNV),
+ DEF_FN(VDPAUFiniNV),
+ DEF_FN(VDPAURegisterOutputSurfaceNV),
+ DEF_FN(VDPAURegisterVideoSurfaceNV),
+ DEF_FN(VDPAUUnregisterSurfaceNV),
+ DEF_FN(VDPAUSurfaceAccessNV),
+ DEF_FN(VDPAUMapSurfacesNV),
+ DEF_FN(VDPAUUnmapSurfacesNV),
+ {0}
+ },
+ },
+#if HAVE_GL_DXINTEROP
+ {
+ .extension = "WGL_NV_DX_interop",
+ .provides = MPGL_CAP_DXINTEROP,
+ .functions = (const struct gl_function[]) {
+ DEF_FN_NAME(DXSetResourceShareHandleNV, "wglDXSetResourceShareHandleNV"),
+ DEF_FN_NAME(DXOpenDeviceNV, "wglDXOpenDeviceNV"),
+ DEF_FN_NAME(DXCloseDeviceNV, "wglDXCloseDeviceNV"),
+ DEF_FN_NAME(DXRegisterObjectNV, "wglDXRegisterObjectNV"),
+ DEF_FN_NAME(DXUnregisterObjectNV, "wglDXUnregisterObjectNV"),
+ DEF_FN_NAME(DXLockObjectsNV, "wglDXLockObjectsNV"),
+ DEF_FN_NAME(DXUnlockObjectsNV, "wglDXUnlockObjectsNV"),
+ {0}
+ },
+ },
+#endif
+ // Apple Packed YUV Formats
+ // For gl_hwdec_vda.c
+ // http://www.opengl.org/registry/specs/APPLE/rgb_422.txt
+ {
+ .extension = "GL_APPLE_rgb_422",
+ .provides = MPGL_CAP_APPLE_RGB_422,
+ },
+ {
+ .ver_core = 430,
+ .extension = "GL_ARB_debug_output",
+ .provides = MPGL_CAP_DEBUG,
+ .functions = (const struct gl_function[]) {
+ // (only functions needed by us)
+ DEF_FN(DebugMessageCallback),
+ {0}
+ },
+ },
+ // ES version uses a different extension.
+ {
+ .ver_es_core = 320,
+ .extension = "GL_KHR_debug",
+ .provides = MPGL_CAP_DEBUG,
+ .functions = (const struct gl_function[]) {
+ // (only functions needed by us)
+ DEF_FN(DebugMessageCallback),
+ {0}
+ },
+ },
+ {
+ .extension = "GL_ANGLE_translated_shader_source",
+ .functions = (const struct gl_function[]) {
+ DEF_FN(GetTranslatedShaderSourceANGLE),
+ {0}
+ },
+ },
+};
+
+#undef FN_OFFS
+#undef DEF_FN_HARD
+#undef DEF_FN
+#undef DEF_FN_NAME
+
+// Fill the GL struct with function pointers and extensions from the current
+// GL context. Called by the backend.
+// get_fn: function to resolve function names
+// ext2: an extra extension string
+// log: used to output messages
+void mpgl_load_functions2(GL *gl, void *(*get_fn)(void *ctx, const char *n),
+ void *fn_ctx, const char *ext2, struct mp_log *log)
+{
+ talloc_free(gl->extensions);
+ *gl = (GL) {
+ .extensions = talloc_strdup(gl, ext2 ? ext2 : ""),
+ .get_fn = get_fn,
+ .fn_ctx = fn_ctx,
+ };
+
+ gl->GetString = get_fn(fn_ctx, "glGetString");
+ if (!gl->GetString) {
+ mp_err(log, "Can't load OpenGL functions.\n");
+ goto error;
+ }
+
+ int major = 0, minor = 0;
+ const char *version_string = gl->GetString(GL_VERSION);
+ if (!version_string) {
+ mp_fatal(log, "glGetString(GL_VERSION) returned NULL.\n");
+ goto error;
+ }
+ mp_verbose(log, "GL_VERSION='%s'\n", version_string);
+ if (strncmp(version_string, "OpenGL ES ", 10) == 0) {
+ version_string += 10;
+ gl->es = 100;
+ }
+ if (sscanf(version_string, "%d.%d", &major, &minor) < 2)
+ goto error;
+ gl->version = MPGL_VER(major, minor);
+ mp_verbose(log, "Detected %s %d.%d.\n", gl->es ? "GLES" : "desktop OpenGL",
+ major, minor);
+
+ if (gl->es) {
+ gl->es = gl->version;
+ gl->version = 0;
+ if (gl->es < 200) {
+ mp_fatal(log, "At least GLESv2 required.\n");
+ goto error;
+ }
+ }
+
+ mp_verbose(log, "GL_VENDOR='%s'\n", gl->GetString(GL_VENDOR));
+ mp_verbose(log, "GL_RENDERER='%s'\n", gl->GetString(GL_RENDERER));
+ const char *shader = gl->GetString(GL_SHADING_LANGUAGE_VERSION);
+ if (shader)
+ mp_verbose(log, "GL_SHADING_LANGUAGE_VERSION='%s'\n", shader);
+
+ if (gl->version >= 300) {
+ gl->GetStringi = get_fn(fn_ctx, "glGetStringi");
+ gl->GetIntegerv = get_fn(fn_ctx, "glGetIntegerv");
+
+ if (!(gl->GetStringi && gl->GetIntegerv))
+ goto error;
+
+ GLint exts;
+ gl->GetIntegerv(GL_NUM_EXTENSIONS, &exts);
+ for (int n = 0; n < exts; n++) {
+ const char *ext = gl->GetStringi(GL_EXTENSIONS, n);
+ gl->extensions = talloc_asprintf_append(gl->extensions, " %s", ext);
+ }
+
+ } else {
+ const char *ext = (char*)gl->GetString(GL_EXTENSIONS);
+ gl->extensions = talloc_asprintf_append(gl->extensions, " %s", ext);
+ }
+
+ mp_dbg(log, "Combined OpenGL extensions string:\n%s\n", gl->extensions);
+
+ for (int n = 0; n < MP_ARRAY_SIZE(gl_functions); n++) {
+ const struct gl_functions *section = &gl_functions[n];
+ int version = gl->es ? gl->es : gl->version;
+ int ver_core = gl->es ? section->ver_es_core : section->ver_core;
+
+ // NOTE: Function entrypoints can exist, even if they do not work.
+ // We must always check extension strings and versions.
+
+ if (gl->version && section->ver_exclude &&
+ gl->version >= section->ver_exclude)
+ continue;
+ if (gl->es && section->ver_es_exclude &&
+ gl->es >= section->ver_es_exclude)
+ continue;
+
+ bool exists = false, must_exist = false;
+ if (ver_core)
+ must_exist = version >= ver_core;
+
+ if (section->extension)
+ exists = gl_check_extension(gl->extensions, section->extension);
+
+ exists |= must_exist;
+ if (!exists)
+ continue;
+
+ void *loaded[MAX_FN_COUNT] = {0};
+ bool all_loaded = true;
+ const struct gl_function *fnlist = section->functions;
+
+ for (int i = 0; fnlist && fnlist[i].name; i++) {
+ const struct gl_function *fn = &fnlist[i];
+ void *ptr = get_fn(fn_ctx, fn->name);
+ if (!ptr) {
+ all_loaded = false;
+ if (must_exist) {
+ mp_err(log, "GL %d.%d function %s not found.\n",
+ MPGL_VER_GET_MAJOR(ver_core),
+ MPGL_VER_GET_MINOR(ver_core), fn->name);
+ goto error;
+ } else {
+ mp_warn(log, "Function %s from extension %s not found.\n",
+ fn->name, section->extension);
+ }
+ break;
+ }
+ assert(i < MAX_FN_COUNT);
+ loaded[i] = ptr;
+ }
+
+ if (all_loaded) {
+ gl->mpgl_caps |= section->provides;
+ for (int i = 0; fnlist && fnlist[i].name; i++) {
+ const struct gl_function *fn = &fnlist[i];
+ void **funcptr = (void**)(((char*)gl) + fn->offset);
+ if (loaded[i])
+ *funcptr = loaded[i];
+ }
+ if (!must_exist && section->extension)
+ mp_verbose(log, "Loaded extension %s.\n", section->extension);
+ }
+ }
+
+ gl->glsl_version = 0;
+ if (gl->es) {
+ if (gl->es >= 200)
+ gl->glsl_version = 100;
+ if (gl->es >= 300)
+ gl->glsl_version = gl->es;
+ } else {
+ gl->glsl_version = 120;
+ int glsl_major = 0, glsl_minor = 0;
+ if (shader && sscanf(shader, "%d.%d", &glsl_major, &glsl_minor) == 2)
+ gl->glsl_version = glsl_major * 100 + glsl_minor;
+ // restrict GLSL version to be forwards compatible
+ gl->glsl_version = MPMIN(gl->glsl_version, 440);
+ }
+
+ if (is_software_gl(gl)) {
+ gl->mpgl_caps |= MPGL_CAP_SW;
+ mp_verbose(log, "Detected suspected software renderer.\n");
+ }
+
+ if (!is_fast_dr(gl))
+ gl->mpgl_caps |= MPGL_CAP_SLOW_DR;
+
+ // GL_ARB_compute_shader & GL_ARB_shader_image_load_store
+ if (gl->DispatchCompute && gl->BindImageTexture)
+ gl->mpgl_caps |= MPGL_CAP_COMPUTE_SHADER;
+
+ // Provided for simpler handling if no framebuffer support is available.
+ if (!gl->BindFramebuffer)
+ gl->BindFramebuffer = &dummy_glBindFramebuffer;
+ return;
+
+error:
+ gl->version = 0;
+ gl->es = 0;
+ gl->mpgl_caps = 0;
+}
+
+static void *get_procaddr_wrapper(void *ctx, const char *name)
+{
+ void *(*getProcAddress)(const GLubyte *) = ctx;
+ return getProcAddress ? getProcAddress((const GLubyte*)name) : NULL;
+}
+
+void mpgl_load_functions(GL *gl, void *(*getProcAddress)(const GLubyte *),
+ const char *ext2, struct mp_log *log)
+{
+ mpgl_load_functions2(gl, get_procaddr_wrapper, getProcAddress, ext2, log);
+}
diff --git a/video/out/opengl/common.h b/video/out/opengl/common.h
new file mode 100644
index 0000000..a6b02c9
--- /dev/null
+++ b/video/out/opengl/common.h
@@ -0,0 +1,258 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MPLAYER_GL_COMMON_H
+#define MPLAYER_GL_COMMON_H
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#include "config.h"
+#include "common/msg.h"
+#include "misc/bstr.h"
+
+#include "video/csputils.h"
+#include "video/mp_image.h"
+#include "video/out/vo.h"
+#include "video/out/gpu/ra.h"
+
+#include "gl_headers.h"
+
+#if HAVE_GL_WIN32
+#include <windows.h>
+#endif
+
+struct GL;
+typedef struct GL GL;
+
+enum {
+ MPGL_CAP_ROW_LENGTH = (1 << 4), // GL_[UN]PACK_ROW_LENGTH
+ MPGL_CAP_FB = (1 << 5),
+ MPGL_CAP_VAO = (1 << 6),
+ MPGL_CAP_TEX_RG = (1 << 10), // GL_ARB_texture_rg / GL 3.x
+ MPGL_CAP_VDPAU = (1 << 11), // GL_NV_vdpau_interop
+ MPGL_CAP_APPLE_RGB_422 = (1 << 12), // GL_APPLE_rgb_422
+ MPGL_CAP_1D_TEX = (1 << 14),
+ MPGL_CAP_3D_TEX = (1 << 15),
+ MPGL_CAP_DEBUG = (1 << 16),
+ MPGL_CAP_DXINTEROP = (1 << 17), // WGL_NV_DX_interop
+ MPGL_CAP_EXT16 = (1 << 18), // GL_EXT_texture_norm16
+ MPGL_CAP_ARB_FLOAT = (1 << 19), // GL_ARB_texture_float
+ MPGL_CAP_EXT_CR_HFLOAT = (1 << 20), // GL_EXT_color_buffer_half_float
+ MPGL_CAP_UBO = (1 << 21), // GL_ARB_uniform_buffer_object
+ MPGL_CAP_SSBO = (1 << 22), // GL_ARB_shader_storage_buffer_object
+ MPGL_CAP_COMPUTE_SHADER = (1 << 23), // GL_ARB_compute_shader & GL_ARB_shader_image_load_store
+ MPGL_CAP_NESTED_ARRAY = (1 << 24), // GL_ARB_arrays_of_arrays
+
+ MPGL_CAP_SLOW_DR = (1 << 29), // direct rendering is assumed to be slow
+ MPGL_CAP_SW = (1 << 30), // indirect or sw renderer
+};
+
+// E.g. 310 means 3.1
+// Code doesn't have to use the macros; they are for convenience only.
+#define MPGL_VER(major, minor) (((major) * 100) + (minor) * 10)
+#define MPGL_VER_GET_MAJOR(ver) ((unsigned)(ver) / 100)
+#define MPGL_VER_GET_MINOR(ver) ((unsigned)(ver) % 100 / 10)
+
+#define MPGL_VER_P(ver) MPGL_VER_GET_MAJOR(ver), MPGL_VER_GET_MINOR(ver)
+
+void mpgl_load_functions(GL *gl, void *(*getProcAddress)(const GLubyte *),
+ const char *ext2, struct mp_log *log);
+void mpgl_load_functions2(GL *gl, void *(*get_fn)(void *ctx, const char *n),
+ void *fn_ctx, const char *ext2, struct mp_log *log);
+
+typedef void (GLAPIENTRY *MP_GLDEBUGPROC)(GLenum, GLenum, GLuint, GLenum,
+ GLsizei, const GLchar *,const void *);
+
+//function pointers loaded from the OpenGL library
+struct GL {
+ int version; // MPGL_VER() mangled (e.g. 210 for 2.1)
+ int es; // es version (e.g. 300), 0 for desktop GL
+ int glsl_version; // e.g. 130 for GLSL 1.30
+ char *extensions; // Equivalent to GL_EXTENSIONS
+ int mpgl_caps; // Bitfield of MPGL_CAP_* constants
+ bool debug_context; // use of e.g. GLX_CONTEXT_DEBUG_BIT_ARB
+
+ // Set to false if the implementation follows normal GL semantics, which is
+ // upside down. Set to true if it does *not*, i.e. if rendering is right
+ // side up
+ bool flipped;
+
+ // Copy of function pointer used to load GL.
+ // Caution: Not necessarily valid to use after VO init has completed!
+ void *(*get_fn)(void *ctx, const char *n);
+ void *fn_ctx;
+
+ void (GLAPIENTRY *Viewport)(GLint, GLint, GLsizei, GLsizei);
+ void (GLAPIENTRY *Clear)(GLbitfield);
+ void (GLAPIENTRY *GenTextures)(GLsizei, GLuint *);
+ void (GLAPIENTRY *DeleteTextures)(GLsizei, const GLuint *);
+ void (GLAPIENTRY *ClearColor)(GLclampf, GLclampf, GLclampf, GLclampf);
+ void (GLAPIENTRY *Enable)(GLenum);
+ void (GLAPIENTRY *Disable)(GLenum);
+ const GLubyte *(GLAPIENTRY * GetString)(GLenum);
+ void (GLAPIENTRY *BlendFuncSeparate)(GLenum, GLenum, GLenum, GLenum);
+ void (GLAPIENTRY *Flush)(void);
+ void (GLAPIENTRY *Finish)(void);
+ void (GLAPIENTRY *PixelStorei)(GLenum, GLint);
+ void (GLAPIENTRY *TexImage1D)(GLenum, GLint, GLint, GLsizei, GLint,
+ GLenum, GLenum, const GLvoid *);
+ void (GLAPIENTRY *TexImage2D)(GLenum, GLint, GLint, GLsizei, GLsizei,
+ GLint, GLenum, GLenum, const GLvoid *);
+ void (GLAPIENTRY *TexSubImage2D)(GLenum, GLint, GLint, GLint,
+ GLsizei, GLsizei, GLenum, GLenum,
+ const GLvoid *);
+ void (GLAPIENTRY *TexParameteri)(GLenum, GLenum, GLint);
+ void (GLAPIENTRY *GetIntegerv)(GLenum, GLint *);
+ void (GLAPIENTRY *ReadPixels)(GLint, GLint, GLsizei, GLsizei, GLenum,
+ GLenum, GLvoid *);
+ void (GLAPIENTRY *ReadBuffer)(GLenum);
+ void (GLAPIENTRY *DrawBuffer)(GLenum);
+ void (GLAPIENTRY *DrawArrays)(GLenum, GLint, GLsizei);
+ GLenum (GLAPIENTRY *GetError)(void);
+ void (GLAPIENTRY *GetTexLevelParameteriv)(GLenum, GLint, GLenum, GLint *);
+ void (GLAPIENTRY *Scissor)(GLint, GLint, GLsizei, GLsizei);
+
+ void (GLAPIENTRY *GenBuffers)(GLsizei, GLuint *);
+ void (GLAPIENTRY *DeleteBuffers)(GLsizei, const GLuint *);
+ void (GLAPIENTRY *BindBuffer)(GLenum, GLuint);
+ void (GLAPIENTRY *BindBufferBase)(GLenum, GLuint, GLuint);
+ GLvoid * (GLAPIENTRY *MapBufferRange)(GLenum, GLintptr, GLsizeiptr,
+ GLbitfield);
+ GLboolean (GLAPIENTRY *UnmapBuffer)(GLenum);
+ void (GLAPIENTRY *BufferData)(GLenum, intptr_t, const GLvoid *, GLenum);
+ void (GLAPIENTRY *BufferSubData)(GLenum, GLintptr, GLsizeiptr, const GLvoid *);
+ void (GLAPIENTRY *ActiveTexture)(GLenum);
+ void (GLAPIENTRY *BindTexture)(GLenum, GLuint);
+ int (GLAPIENTRY *SwapInterval)(int);
+ void (GLAPIENTRY *TexImage3D)(GLenum, GLint, GLenum, GLsizei, GLsizei,
+ GLsizei, GLint, GLenum, GLenum,
+ const GLvoid *);
+
+ void (GLAPIENTRY *GenVertexArrays)(GLsizei, GLuint *);
+ void (GLAPIENTRY *BindVertexArray)(GLuint);
+ GLint (GLAPIENTRY *GetAttribLocation)(GLuint, const GLchar *);
+ void (GLAPIENTRY *EnableVertexAttribArray)(GLuint);
+ void (GLAPIENTRY *DisableVertexAttribArray)(GLuint);
+ void (GLAPIENTRY *VertexAttribPointer)(GLuint, GLint, GLenum, GLboolean,
+ GLsizei, const GLvoid *);
+ void (GLAPIENTRY *DeleteVertexArrays)(GLsizei, const GLuint *);
+ void (GLAPIENTRY *UseProgram)(GLuint);
+ GLint (GLAPIENTRY *GetUniformLocation)(GLuint, const GLchar *);
+ void (GLAPIENTRY *CompileShader)(GLuint);
+ GLuint (GLAPIENTRY *CreateProgram)(void);
+ GLuint (GLAPIENTRY *CreateShader)(GLenum);
+ void (GLAPIENTRY *ShaderSource)(GLuint, GLsizei, const GLchar **,
+ const GLint *);
+ void (GLAPIENTRY *LinkProgram)(GLuint);
+ void (GLAPIENTRY *AttachShader)(GLuint, GLuint);
+ void (GLAPIENTRY *DeleteShader)(GLuint);
+ void (GLAPIENTRY *DeleteProgram)(GLuint);
+ void (GLAPIENTRY *GetShaderInfoLog)(GLuint, GLsizei, GLsizei *, GLchar *);
+ void (GLAPIENTRY *GetShaderiv)(GLuint, GLenum, GLint *);
+ void (GLAPIENTRY *GetProgramInfoLog)(GLuint, GLsizei, GLsizei *, GLchar *);
+ void (GLAPIENTRY *GetProgramiv)(GLenum, GLenum, GLint *);
+ void (GLAPIENTRY *GetProgramBinary)(GLuint, GLsizei, GLsizei *, GLenum *,
+ void *);
+ void (GLAPIENTRY *ProgramBinary)(GLuint, GLenum, const void *, GLsizei);
+
+ void (GLAPIENTRY *DispatchCompute)(GLuint, GLuint, GLuint);
+ void (GLAPIENTRY *BindImageTexture)(GLuint, GLuint, GLint, GLboolean,
+ GLint, GLenum, GLenum);
+ void (GLAPIENTRY *MemoryBarrier)(GLbitfield);
+
+ const GLubyte* (GLAPIENTRY *GetStringi)(GLenum, GLuint);
+ void (GLAPIENTRY *BindAttribLocation)(GLuint, GLuint, const GLchar *);
+ void (GLAPIENTRY *BindFramebuffer)(GLenum, GLuint);
+ void (GLAPIENTRY *GenFramebuffers)(GLsizei, GLuint *);
+ void (GLAPIENTRY *DeleteFramebuffers)(GLsizei, const GLuint *);
+ GLenum (GLAPIENTRY *CheckFramebufferStatus)(GLenum);
+ void (GLAPIENTRY *FramebufferTexture2D)(GLenum, GLenum, GLenum, GLuint,
+ GLint);
+ void (GLAPIENTRY *BlitFramebuffer)(GLint, GLint, GLint, GLint, GLint, GLint,
+ GLint, GLint, GLbitfield, GLenum);
+ void (GLAPIENTRY *GetFramebufferAttachmentParameteriv)(GLenum, GLenum,
+ GLenum, GLint *);
+
+ void (GLAPIENTRY *Uniform1f)(GLint, GLfloat);
+ void (GLAPIENTRY *Uniform2f)(GLint, GLfloat, GLfloat);
+ void (GLAPIENTRY *Uniform3f)(GLint, GLfloat, GLfloat, GLfloat);
+ void (GLAPIENTRY *Uniform4f)(GLint, GLfloat, GLfloat, GLfloat, GLfloat);
+ void (GLAPIENTRY *Uniform1i)(GLint, GLint);
+ void (GLAPIENTRY *UniformMatrix2fv)(GLint, GLsizei, GLboolean,
+ const GLfloat *);
+ void (GLAPIENTRY *UniformMatrix3fv)(GLint, GLsizei, GLboolean,
+ const GLfloat *);
+
+ void (GLAPIENTRY *InvalidateTexImage)(GLuint, GLint);
+ void (GLAPIENTRY *InvalidateFramebuffer)(GLenum, GLsizei, const GLenum *);
+
+ GLsync (GLAPIENTRY *FenceSync)(GLenum, GLbitfield);
+ GLenum (GLAPIENTRY *ClientWaitSync)(GLsync, GLbitfield, GLuint64);
+ void (GLAPIENTRY *DeleteSync)(GLsync sync);
+
+ void (GLAPIENTRY *BufferStorage)(GLenum, intptr_t, const GLvoid *, GLenum);
+
+ void (GLAPIENTRY *GenQueries)(GLsizei, GLuint *);
+ void (GLAPIENTRY *DeleteQueries)(GLsizei, const GLuint *);
+ void (GLAPIENTRY *BeginQuery)(GLenum, GLuint);
+ void (GLAPIENTRY *EndQuery)(GLenum);
+ void (GLAPIENTRY *QueryCounter)(GLuint, GLenum);
+ GLboolean (GLAPIENTRY *IsQuery)(GLuint);
+ void (GLAPIENTRY *GetQueryObjectiv)(GLuint, GLenum, GLint *);
+ void (GLAPIENTRY *GetQueryObjecti64v)(GLuint, GLenum, GLint64 *);
+ void (GLAPIENTRY *GetQueryObjectuiv)(GLuint, GLenum, GLuint *);
+ void (GLAPIENTRY *GetQueryObjectui64v)(GLuint, GLenum, GLuint64 *);
+
+ void (GLAPIENTRY *VDPAUInitNV)(const GLvoid *, const GLvoid *);
+ void (GLAPIENTRY *VDPAUFiniNV)(void);
+ GLvdpauSurfaceNV (GLAPIENTRY *VDPAURegisterOutputSurfaceNV)
+ (GLvoid *, GLenum, GLsizei, const GLuint *);
+ GLvdpauSurfaceNV (GLAPIENTRY *VDPAURegisterVideoSurfaceNV)
+ (GLvoid *, GLenum, GLsizei, const GLuint *);
+ void (GLAPIENTRY *VDPAUUnregisterSurfaceNV)(GLvdpauSurfaceNV);
+ void (GLAPIENTRY *VDPAUSurfaceAccessNV)(GLvdpauSurfaceNV, GLenum);
+ void (GLAPIENTRY *VDPAUMapSurfacesNV)(GLsizei, const GLvdpauSurfaceNV *);
+ void (GLAPIENTRY *VDPAUUnmapSurfacesNV)(GLsizei, const GLvdpauSurfaceNV *);
+
+#if HAVE_GL_WIN32
+ // The HANDLE type might not be present on non-Win32
+ BOOL (GLAPIENTRY *DXSetResourceShareHandleNV)(void *dxObject,
+ HANDLE shareHandle);
+ HANDLE (GLAPIENTRY *DXOpenDeviceNV)(void *dxDevice);
+ BOOL (GLAPIENTRY *DXCloseDeviceNV)(HANDLE hDevice);
+ HANDLE (GLAPIENTRY *DXRegisterObjectNV)(HANDLE hDevice, void *dxObject,
+ GLuint name, GLenum type, GLenum access);
+ BOOL (GLAPIENTRY *DXUnregisterObjectNV)(HANDLE hDevice, HANDLE hObject);
+ BOOL (GLAPIENTRY *DXLockObjectsNV)(HANDLE hDevice, GLint count,
+ HANDLE *hObjects);
+ BOOL (GLAPIENTRY *DXUnlockObjectsNV)(HANDLE hDevice, GLint count,
+ HANDLE *hObjects);
+#endif
+
+ GLint (GLAPIENTRY *GetVideoSync)(GLuint *);
+ GLint (GLAPIENTRY *WaitVideoSync)(GLint, GLint, unsigned int *);
+
+ void (GLAPIENTRY *GetTranslatedShaderSourceANGLE)(GLuint, GLsizei,
+ GLsizei*, GLchar* source);
+
+ void (GLAPIENTRY *DebugMessageCallback)(MP_GLDEBUGPROC callback,
+ const void *userParam);
+};
+
+#endif /* MPLAYER_GL_COMMON_H */
diff --git a/video/out/opengl/context.c b/video/out/opengl/context.c
new file mode 100644
index 0000000..05e279b
--- /dev/null
+++ b/video/out/opengl/context.c
@@ -0,0 +1,324 @@
+/*
+ * 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 "options/m_config.h"
+#include "context.h"
+#include "ra_gl.h"
+#include "utils.h"
+
+// 0-terminated list of desktop GL versions a backend should try to
+// initialize. Each entry is the minimum required version.
+const int mpgl_min_required_gl_versions[] = {
+ /*
+ * Nvidia drivers will not provide the highest supported version
+ * when 320 core is requested. Instead, it just returns 3.2. This
+ * would be bad, as we actually want compute shaders that require
+ * 4.2, so we have to request a sufficiently high version. We use
+ * 440 to maximise driver compatibility as we don't need anything
+ * from newer versions.
+ */
+ 440,
+ 320,
+ 210,
+ 0
+};
+
+enum {
+ FLUSH_NO = 0,
+ FLUSH_YES,
+ FLUSH_AUTO,
+};
+
+struct opengl_opts {
+ bool use_glfinish;
+ bool waitvsync;
+ int vsync_pattern[2];
+ int swapinterval;
+ int early_flush;
+ int gles_mode;
+};
+
+#define OPT_BASE_STRUCT struct opengl_opts
+const struct m_sub_options opengl_conf = {
+ .opts = (const struct m_option[]) {
+ {"opengl-glfinish", OPT_BOOL(use_glfinish)},
+ {"opengl-waitvsync", OPT_BOOL(waitvsync)},
+ {"opengl-swapinterval", OPT_INT(swapinterval)},
+ {"opengl-check-pattern-a", OPT_INT(vsync_pattern[0])},
+ {"opengl-check-pattern-b", OPT_INT(vsync_pattern[1])},
+ {"opengl-es", OPT_CHOICE(gles_mode,
+ {"auto", GLES_AUTO}, {"yes", GLES_YES}, {"no", GLES_NO})},
+ {"opengl-early-flush", OPT_CHOICE(early_flush,
+ {"no", FLUSH_NO}, {"yes", FLUSH_YES}, {"auto", FLUSH_AUTO})},
+ {0},
+ },
+ .defaults = &(const struct opengl_opts) {
+ .swapinterval = 1,
+ },
+ .size = sizeof(struct opengl_opts),
+};
+
+struct priv {
+ GL *gl;
+ struct mp_log *log;
+ struct ra_gl_ctx_params params;
+ struct opengl_opts *opts;
+ struct ra_swapchain_fns fns;
+ GLuint main_fb;
+ struct ra_tex *wrapped_fb; // corresponds to main_fb
+ // for debugging:
+ int frames_rendered;
+ unsigned int prev_sgi_sync_count;
+ // for gl_vsync_pattern
+ int last_pattern;
+ int matches, mismatches;
+ // for swapchain_depth simulation
+ GLsync *vsync_fences;
+ int num_vsync_fences;
+};
+
+enum gles_mode ra_gl_ctx_get_glesmode(struct ra_ctx *ctx)
+{
+ void *tmp = talloc_new(NULL);
+ struct opengl_opts *opts;
+ enum gles_mode mode;
+
+ opts = mp_get_config_group(tmp, ctx->global, &opengl_conf);
+ mode = opts->gles_mode;
+
+ talloc_free(tmp);
+ return mode;
+}
+
+void ra_gl_ctx_uninit(struct ra_ctx *ctx)
+{
+ if (ctx->swapchain) {
+ struct priv *p = ctx->swapchain->priv;
+ if (ctx->ra && p->wrapped_fb)
+ ra_tex_free(ctx->ra, &p->wrapped_fb);
+ talloc_free(ctx->swapchain);
+ ctx->swapchain = NULL;
+ }
+
+ // Clean up any potentially left-over debug callback
+ if (ctx->ra)
+ ra_gl_set_debug(ctx->ra, false);
+
+ ra_free(&ctx->ra);
+}
+
+static const struct ra_swapchain_fns ra_gl_swapchain_fns;
+
+bool ra_gl_ctx_init(struct ra_ctx *ctx, GL *gl, struct ra_gl_ctx_params params)
+{
+ struct ra_swapchain *sw = ctx->swapchain = talloc_ptrtype(NULL, sw);
+ *sw = (struct ra_swapchain) {
+ .ctx = ctx,
+ };
+
+ struct priv *p = sw->priv = talloc_ptrtype(sw, p);
+ *p = (struct priv) {
+ .gl = gl,
+ .log = ctx->log,
+ .params = params,
+ .opts = mp_get_config_group(p, ctx->global, &opengl_conf),
+ .fns = ra_gl_swapchain_fns,
+ };
+
+ sw->fns = &p->fns;
+
+ const struct ra_swapchain_fns *ext = p->params.external_swapchain;
+ if (ext) {
+ if (ext->color_depth)
+ p->fns.color_depth = ext->color_depth;
+ if (ext->start_frame)
+ p->fns.start_frame = ext->start_frame;
+ if (ext->submit_frame)
+ p->fns.submit_frame = ext->submit_frame;
+ if (ext->swap_buffers)
+ p->fns.swap_buffers = ext->swap_buffers;
+ }
+
+ if (!gl->version && !gl->es)
+ return false;
+
+ if (gl->mpgl_caps & MPGL_CAP_SW) {
+ MP_WARN(p, "Suspected software renderer or indirect context.\n");
+ if (ctx->opts.probing && !ctx->opts.allow_sw)
+ return false;
+ }
+
+ gl->debug_context = ctx->opts.debug;
+
+ if (gl->SwapInterval) {
+ gl->SwapInterval(p->opts->swapinterval);
+ } else {
+ MP_VERBOSE(p, "GL_*_swap_control extension missing.\n");
+ }
+
+ ctx->ra = ra_create_gl(p->gl, ctx->log);
+ return !!ctx->ra;
+}
+
+void ra_gl_ctx_resize(struct ra_swapchain *sw, int w, int h, int fbo)
+{
+ struct priv *p = sw->priv;
+ if (p->main_fb == fbo && p->wrapped_fb && p->wrapped_fb->params.w == w
+ && p->wrapped_fb->params.h == h)
+ return;
+
+ if (p->wrapped_fb)
+ ra_tex_free(sw->ctx->ra, &p->wrapped_fb);
+
+ p->main_fb = fbo;
+ p->wrapped_fb = ra_create_wrapped_fb(sw->ctx->ra, fbo, w, h);
+}
+
+int ra_gl_ctx_color_depth(struct ra_swapchain *sw)
+{
+ struct priv *p = sw->priv;
+ GL *gl = p->gl;
+
+ if (!p->wrapped_fb)
+ return 0;
+
+ if ((gl->es < 300 && !gl->version) || !(gl->mpgl_caps & MPGL_CAP_FB))
+ return 0;
+
+ gl->BindFramebuffer(GL_FRAMEBUFFER, p->main_fb);
+
+ GLenum obj = gl->version ? GL_BACK_LEFT : GL_BACK;
+ if (p->main_fb)
+ obj = GL_COLOR_ATTACHMENT0;
+
+ GLint depth_g = 0;
+
+ gl->GetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, obj,
+ GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE, &depth_g);
+
+ gl->BindFramebuffer(GL_FRAMEBUFFER, 0);
+
+ return depth_g;
+}
+
+bool ra_gl_ctx_start_frame(struct ra_swapchain *sw, struct ra_fbo *out_fbo)
+{
+ struct priv *p = sw->priv;
+
+ bool visible = true;
+ if (p->params.check_visible)
+ visible = p->params.check_visible(sw->ctx);
+
+ // If out_fbo is NULL, this was called from vo_gpu_next. Bail out.
+ if (!out_fbo || !visible)
+ return visible;
+
+ *out_fbo = (struct ra_fbo) {
+ .tex = p->wrapped_fb,
+ .flip = !p->gl->flipped, // OpenGL FBs are normally flipped
+ };
+ return true;
+}
+
+bool ra_gl_ctx_submit_frame(struct ra_swapchain *sw, const struct vo_frame *frame)
+{
+ struct priv *p = sw->priv;
+ GL *gl = p->gl;
+
+ if (p->opts->use_glfinish)
+ gl->Finish();
+
+ if (gl->FenceSync && !p->params.external_swapchain) {
+ GLsync fence = gl->FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ if (fence)
+ MP_TARRAY_APPEND(p, p->vsync_fences, p->num_vsync_fences, fence);
+ }
+
+ switch (p->opts->early_flush) {
+ case FLUSH_AUTO:
+ if (frame->display_synced)
+ break;
+ MP_FALLTHROUGH;
+ case FLUSH_YES:
+ gl->Flush();
+ }
+
+ return true;
+}
+
+static void check_pattern(struct priv *p, int item)
+{
+ int expected = p->opts->vsync_pattern[p->last_pattern];
+ if (item == expected) {
+ p->last_pattern++;
+ if (p->last_pattern >= 2)
+ p->last_pattern = 0;
+ p->matches++;
+ } else {
+ p->mismatches++;
+ MP_WARN(p, "wrong pattern, expected %d got %d (hit: %d, mis: %d)\n",
+ expected, item, p->matches, p->mismatches);
+ }
+}
+
+void ra_gl_ctx_swap_buffers(struct ra_swapchain *sw)
+{
+ struct priv *p = sw->priv;
+ GL *gl = p->gl;
+
+ p->params.swap_buffers(sw->ctx);
+ p->frames_rendered++;
+
+ if (p->frames_rendered > 5 && !sw->ctx->opts.debug)
+ ra_gl_set_debug(sw->ctx->ra, false);
+
+ if ((p->opts->waitvsync || p->opts->vsync_pattern[0])
+ && gl->GetVideoSync)
+ {
+ unsigned int n1 = 0, n2 = 0;
+ gl->GetVideoSync(&n1);
+ if (p->opts->waitvsync)
+ gl->WaitVideoSync(2, (n1 + 1) % 2, &n2);
+ int step = n1 - p->prev_sgi_sync_count;
+ p->prev_sgi_sync_count = n1;
+ MP_DBG(p, "Flip counts: %u->%u, step=%d\n", n1, n2, step);
+ if (p->opts->vsync_pattern[0])
+ check_pattern(p, step);
+ }
+
+ while (p->num_vsync_fences >= sw->ctx->vo->opts->swapchain_depth) {
+ gl->ClientWaitSync(p->vsync_fences[0], GL_SYNC_FLUSH_COMMANDS_BIT, 1e9);
+ gl->DeleteSync(p->vsync_fences[0]);
+ MP_TARRAY_REMOVE_AT(p->vsync_fences, p->num_vsync_fences, 0);
+ }
+}
+
+static void ra_gl_ctx_get_vsync(struct ra_swapchain *sw,
+ struct vo_vsync_info *info)
+{
+ struct priv *p = sw->priv;
+ if (p->params.get_vsync)
+ p->params.get_vsync(sw->ctx, info);
+}
+
+static const struct ra_swapchain_fns ra_gl_swapchain_fns = {
+ .color_depth = ra_gl_ctx_color_depth,
+ .start_frame = ra_gl_ctx_start_frame,
+ .submit_frame = ra_gl_ctx_submit_frame,
+ .swap_buffers = ra_gl_ctx_swap_buffers,
+ .get_vsync = ra_gl_ctx_get_vsync,
+};
diff --git a/video/out/opengl/context.h b/video/out/opengl/context.h
new file mode 100644
index 0000000..c96450e
--- /dev/null
+++ b/video/out/opengl/context.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "common/global.h"
+#include "video/out/gpu/context.h"
+#include "common.h"
+
+extern const int mpgl_min_required_gl_versions[];
+
+enum gles_mode {
+ GLES_AUTO = 0,
+ GLES_YES,
+ GLES_NO,
+};
+
+// Returns the gles mode based on the --opengl opts.
+enum gles_mode ra_gl_ctx_get_glesmode(struct ra_ctx *ctx);
+
+// These are a set of helpers for ra_ctx providers based on ra_gl.
+// The init function also initializes ctx->ra and ctx->swapchain, so the user
+// doesn't have to do this manually. (Similarly, the uninit function will
+// clean them up)
+
+struct ra_gl_ctx_params {
+ // For special contexts (i.e. wayland) that want to check visibility
+ // before drawing a frame.
+ bool (*check_visible)(struct ra_ctx *ctx);
+
+ // Set to the platform-specific function to swap buffers, like
+ // glXSwapBuffers, eglSwapBuffers etc. This will be called by
+ // ra_gl_ctx_swap_buffers. Required unless you either never call that
+ // function or if you override it yourself.
+ void (*swap_buffers)(struct ra_ctx *ctx);
+
+ // See ra_swapchain_fns.get_vsync.
+ void (*get_vsync)(struct ra_ctx *ctx, struct vo_vsync_info *info);
+
+ // If this is set to non-NULL, then the ra_gl_ctx will consider the GL
+ // implementation to be using an external swapchain, which disables the
+ // software simulation of --swapchain-depth. Any functions defined by this
+ // ra_swapchain_fns structs will entirely replace the equivalent ra_gl_ctx
+ // functions in the resulting ra_swapchain.
+ const struct ra_swapchain_fns *external_swapchain;
+};
+
+void ra_gl_ctx_uninit(struct ra_ctx *ctx);
+bool ra_gl_ctx_init(struct ra_ctx *ctx, GL *gl, struct ra_gl_ctx_params params);
+
+// Call this any time the window size or main framebuffer changes
+void ra_gl_ctx_resize(struct ra_swapchain *sw, int w, int h, int fbo);
+
+// These functions are normally set in the ra_swapchain->fns, but if an
+// implementation has a need to override this fns struct with custom functions
+// for whatever reason, these can be used to inherit the original behavior.
+int ra_gl_ctx_color_depth(struct ra_swapchain *sw);
+struct mp_image *ra_gl_ctx_screenshot(struct ra_swapchain *sw);
+bool ra_gl_ctx_start_frame(struct ra_swapchain *sw, struct ra_fbo *out_fbo);
+bool ra_gl_ctx_submit_frame(struct ra_swapchain *sw, const struct vo_frame *frame);
+void ra_gl_ctx_swap_buffers(struct ra_swapchain *sw);
diff --git a/video/out/opengl/context_android.c b/video/out/opengl/context_android.c
new file mode 100644
index 0000000..bc1717c
--- /dev/null
+++ b/video/out/opengl/context_android.c
@@ -0,0 +1,130 @@
+/*
+ * 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 <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "video/out/android_common.h"
+#include "egl_helpers.h"
+#include "common/common.h"
+#include "context.h"
+
+struct priv {
+ struct GL gl;
+ EGLDisplay egl_display;
+ EGLContext egl_context;
+ EGLSurface egl_surface;
+};
+
+static void android_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ eglSwapBuffers(p->egl_display, p->egl_surface);
+}
+
+static void android_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ra_gl_ctx_uninit(ctx);
+
+ if (p->egl_surface) {
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ eglDestroySurface(p->egl_display, p->egl_surface);
+ }
+ if (p->egl_context)
+ eglDestroyContext(p->egl_display, p->egl_context);
+
+ vo_android_uninit(ctx->vo);
+}
+
+static bool android_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+
+ if (!vo_android_init(ctx->vo))
+ goto fail;
+
+ p->egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ if (!eglInitialize(p->egl_display, NULL, NULL)) {
+ MP_FATAL(ctx, "EGL failed to initialize.\n");
+ goto fail;
+ }
+
+ EGLConfig config;
+ if (!mpegl_create_context(ctx, p->egl_display, &p->egl_context, &config))
+ goto fail;
+
+ ANativeWindow *native_window = vo_android_native_window(ctx->vo);
+ EGLint format;
+ eglGetConfigAttrib(p->egl_display, config, EGL_NATIVE_VISUAL_ID, &format);
+ ANativeWindow_setBuffersGeometry(native_window, 0, 0, format);
+
+ p->egl_surface = eglCreateWindowSurface(p->egl_display, config,
+ (EGLNativeWindowType)native_window, NULL);
+
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ MP_FATAL(ctx, "Could not create EGL surface!\n");
+ goto fail;
+ }
+
+ if (!eglMakeCurrent(p->egl_display, p->egl_surface, p->egl_surface,
+ p->egl_context)) {
+ MP_FATAL(ctx, "Failed to set context!\n");
+ goto fail;
+ }
+
+ mpegl_load_functions(&p->gl, ctx->log);
+
+ struct ra_gl_ctx_params params = {
+ .swap_buffers = android_swap_buffers,
+ };
+
+ if (!ra_gl_ctx_init(ctx, &p->gl, params))
+ goto fail;
+
+ return true;
+fail:
+ android_uninit(ctx);
+ return false;
+}
+
+static bool android_reconfig(struct ra_ctx *ctx)
+{
+ int w, h;
+ if (!vo_android_surface_size(ctx->vo, &w, &h))
+ return false;
+
+ ctx->vo->dwidth = w;
+ ctx->vo->dheight = h;
+ ra_gl_ctx_resize(ctx->swapchain, w, h, 0);
+ return true;
+}
+
+static int android_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ return VO_NOTIMPL;
+}
+
+const struct ra_ctx_fns ra_ctx_android = {
+ .type = "opengl",
+ .name = "android",
+ .reconfig = android_reconfig,
+ .control = android_control,
+ .init = android_init,
+ .uninit = android_uninit,
+};
diff --git a/video/out/opengl/context_angle.c b/video/out/opengl/context_angle.c
new file mode 100644
index 0000000..553718a
--- /dev/null
+++ b/video/out/opengl/context_angle.c
@@ -0,0 +1,653 @@
+/*
+ * 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 <windows.h>
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+#include <EGL/eglext_angle.h>
+#include <d3d11.h>
+#include <dxgi1_2.h>
+#include <dwmapi.h>
+
+#include "angle_dynamic.h"
+#include "egl_helpers.h"
+#include "video/out/gpu/d3d11_helpers.h"
+
+#include "common/common.h"
+#include "options/m_config.h"
+#include "video/out/w32_common.h"
+#include "osdep/windows_utils.h"
+#include "context.h"
+#include "utils.h"
+
+#ifndef EGL_D3D_TEXTURE_ANGLE
+#define EGL_D3D_TEXTURE_ANGLE 0x33A3
+#endif
+#ifndef EGL_OPTIMAL_SURFACE_ORIENTATION_ANGLE
+#define EGL_OPTIMAL_SURFACE_ORIENTATION_ANGLE 0x33A7
+#define EGL_SURFACE_ORIENTATION_ANGLE 0x33A8
+#define EGL_SURFACE_ORIENTATION_INVERT_Y_ANGLE 0x0002
+#endif
+
+enum {
+ RENDERER_AUTO,
+ RENDERER_D3D9,
+ RENDERER_D3D11,
+};
+
+struct angle_opts {
+ int renderer;
+ int d3d11_warp;
+ int d3d11_feature_level;
+ int egl_windowing;
+ bool flip;
+};
+
+#define OPT_BASE_STRUCT struct angle_opts
+const struct m_sub_options angle_conf = {
+ .opts = (const struct m_option[]) {
+ {"angle-renderer", OPT_CHOICE(renderer,
+ {"auto", RENDERER_AUTO},
+ {"d3d9", RENDERER_D3D9},
+ {"d3d11", RENDERER_D3D11})},
+ {"angle-d3d11-warp", OPT_CHOICE(d3d11_warp,
+ {"auto", -1},
+ {"no", 0},
+ {"yes", 1})},
+ {"angle-d3d11-feature-level", OPT_CHOICE(d3d11_feature_level,
+ {"11_0", D3D_FEATURE_LEVEL_11_0},
+ {"10_1", D3D_FEATURE_LEVEL_10_1},
+ {"10_0", D3D_FEATURE_LEVEL_10_0},
+ {"9_3", D3D_FEATURE_LEVEL_9_3})},
+ {"angle-egl-windowing", OPT_CHOICE(egl_windowing,
+ {"auto", -1},
+ {"no", 0},
+ {"yes", 1})},
+ {"angle-flip", OPT_BOOL(flip)},
+ {0}
+ },
+ .defaults = &(const struct angle_opts) {
+ .renderer = RENDERER_AUTO,
+ .d3d11_warp = -1,
+ .d3d11_feature_level = D3D_FEATURE_LEVEL_11_0,
+ .egl_windowing = -1,
+ .flip = true,
+ },
+ .size = sizeof(struct angle_opts),
+};
+
+struct priv {
+ GL gl;
+
+ IDXGISwapChain *dxgi_swapchain;
+
+ ID3D11Device *d3d11_device;
+ ID3D11DeviceContext *d3d11_context;
+ ID3D11Texture2D *d3d11_backbuffer;
+
+ EGLConfig egl_config;
+ EGLDisplay egl_display;
+ EGLDeviceEXT egl_device;
+ EGLContext egl_context;
+ EGLSurface egl_window; // For the EGL windowing surface only
+ EGLSurface egl_backbuffer; // For the DXGI swap chain based surface
+
+ int sc_width, sc_height; // Swap chain width and height
+ int swapinterval;
+ bool flipped;
+
+ struct angle_opts *opts;
+};
+
+static __thread struct ra_ctx *current_ctx;
+
+static void update_sizes(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ p->sc_width = ctx->vo->dwidth ? ctx->vo->dwidth : 1;
+ p->sc_height = ctx->vo->dheight ? ctx->vo->dheight : 1;
+}
+
+static void d3d11_backbuffer_release(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->egl_backbuffer) {
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ eglDestroySurface(p->egl_display, p->egl_backbuffer);
+ }
+ p->egl_backbuffer = EGL_NO_SURFACE;
+
+ SAFE_RELEASE(p->d3d11_backbuffer);
+}
+
+static bool d3d11_backbuffer_get(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+ HRESULT hr;
+
+ hr = IDXGISwapChain_GetBuffer(p->dxgi_swapchain, 0, &IID_ID3D11Texture2D,
+ (void**)&p->d3d11_backbuffer);
+ if (FAILED(hr)) {
+ MP_FATAL(vo, "Couldn't get swap chain back buffer\n");
+ return false;
+ }
+
+ EGLint pbuffer_attributes[] = {
+ EGL_TEXTURE_FORMAT, EGL_TEXTURE_RGBA,
+ EGL_TEXTURE_TARGET, EGL_TEXTURE_2D,
+ EGL_NONE,
+ };
+ p->egl_backbuffer = eglCreatePbufferFromClientBuffer(p->egl_display,
+ EGL_D3D_TEXTURE_ANGLE, p->d3d11_backbuffer, p->egl_config,
+ pbuffer_attributes);
+ if (!p->egl_backbuffer) {
+ MP_FATAL(vo, "Couldn't create EGL pbuffer\n");
+ return false;
+ }
+
+ eglMakeCurrent(p->egl_display, p->egl_backbuffer, p->egl_backbuffer,
+ p->egl_context);
+ return true;
+}
+
+static void d3d11_backbuffer_resize(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+ HRESULT hr;
+
+ int old_sc_width = p->sc_width;
+ int old_sc_height = p->sc_height;
+
+ update_sizes(ctx);
+ // Avoid unnecessary resizing
+ if (old_sc_width == p->sc_width && old_sc_height == p->sc_height)
+ return;
+
+ // All references to backbuffers must be released before ResizeBuffers
+ // (including references held by ANGLE)
+ d3d11_backbuffer_release(ctx);
+
+ // The DirectX runtime may report errors related to the device like
+ // DXGI_ERROR_DEVICE_REMOVED at this point
+ hr = IDXGISwapChain_ResizeBuffers(p->dxgi_swapchain, 0, p->sc_width,
+ p->sc_height, DXGI_FORMAT_UNKNOWN, 0);
+ if (FAILED(hr))
+ MP_FATAL(vo, "Couldn't resize swapchain: %s\n", mp_HRESULT_to_str(hr));
+
+ if (!d3d11_backbuffer_get(ctx))
+ MP_FATAL(vo, "Couldn't get back buffer after resize\n");
+}
+
+static void d3d11_device_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ PFNEGLRELEASEDEVICEANGLEPROC eglReleaseDeviceANGLE =
+ (PFNEGLRELEASEDEVICEANGLEPROC)eglGetProcAddress("eglReleaseDeviceANGLE");
+
+ if (p->egl_display)
+ eglTerminate(p->egl_display);
+ p->egl_display = EGL_NO_DISPLAY;
+
+ if (p->egl_device && eglReleaseDeviceANGLE)
+ eglReleaseDeviceANGLE(p->egl_device);
+ p->egl_device = 0;
+
+ SAFE_RELEASE(p->d3d11_device);
+}
+
+static bool d3d11_device_create(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+ struct angle_opts *o = p->opts;
+
+ struct d3d11_device_opts device_opts = {
+ .allow_warp = o->d3d11_warp != 0,
+ .force_warp = o->d3d11_warp == 1,
+ .max_feature_level = o->d3d11_feature_level,
+ .min_feature_level = D3D_FEATURE_LEVEL_9_3,
+ .max_frame_latency = ctx->vo->opts->swapchain_depth,
+ };
+ if (!mp_d3d11_create_present_device(vo->log, &device_opts, &p->d3d11_device))
+ return false;
+ ID3D11Device_GetImmediateContext(p->d3d11_device, &p->d3d11_context);
+
+ PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT =
+ (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT");
+ if (!eglGetPlatformDisplayEXT) {
+ MP_FATAL(vo, "Missing EGL_EXT_platform_base\n");
+ return false;
+ }
+ PFNEGLCREATEDEVICEANGLEPROC eglCreateDeviceANGLE =
+ (PFNEGLCREATEDEVICEANGLEPROC)eglGetProcAddress("eglCreateDeviceANGLE");
+ if (!eglCreateDeviceANGLE) {
+ MP_FATAL(vo, "Missing EGL_EXT_platform_device\n");
+ return false;
+ }
+
+ p->egl_device = eglCreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE,
+ p->d3d11_device, NULL);
+ if (!p->egl_device) {
+ MP_FATAL(vo, "Couldn't create EGL device\n");
+ return false;
+ }
+
+ p->egl_display = eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT,
+ p->egl_device, NULL);
+ if (!p->egl_display) {
+ MP_FATAL(vo, "Couldn't get EGL display\n");
+ return false;
+ }
+
+ return true;
+}
+
+static void d3d11_swapchain_surface_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ bool had_swapchain = p->dxgi_swapchain;
+ SAFE_RELEASE(p->dxgi_swapchain);
+ d3d11_backbuffer_release(ctx);
+
+ // Ensure the swapchain is destroyed by flushing the D3D11 immediate
+ // context. This is needed because the HWND may be reused. See:
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/ff476425.aspx
+ if (had_swapchain && p->d3d11_context)
+ ID3D11DeviceContext_Flush(p->d3d11_context);
+}
+
+static bool d3d11_swapchain_surface_create(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+ struct angle_opts *o = p->opts;
+
+ if (!p->d3d11_device)
+ goto fail;
+
+ update_sizes(ctx);
+ struct d3d11_swapchain_opts swapchain_opts = {
+ .window = vo_w32_hwnd(vo),
+ .width = p->sc_width,
+ .height = p->sc_height,
+ .flip = o->flip,
+ // Add one frame for the backbuffer and one frame of "slack" to reduce
+ // contention with the window manager when acquiring the backbuffer
+ .length = ctx->vo->opts->swapchain_depth + 2,
+ .usage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT,
+ };
+ if (!mp_d3d11_create_swapchain(p->d3d11_device, vo->log, &swapchain_opts,
+ &p->dxgi_swapchain))
+ goto fail;
+ if (!d3d11_backbuffer_get(ctx))
+ goto fail;
+
+ p->flipped = true;
+ return true;
+
+fail:
+ d3d11_swapchain_surface_destroy(ctx);
+ return false;
+}
+
+static void d3d9_device_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->egl_display)
+ eglTerminate(p->egl_display);
+ p->egl_display = EGL_NO_DISPLAY;
+}
+
+static bool d3d9_device_create(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+
+ PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT =
+ (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT");
+ if (!eglGetPlatformDisplayEXT) {
+ MP_FATAL(vo, "Missing EGL_EXT_platform_base\n");
+ return false;
+ }
+
+ EGLint display_attributes[] = {
+ EGL_PLATFORM_ANGLE_TYPE_ANGLE,
+ EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE,
+ EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE,
+ EGL_PLATFORM_ANGLE_DEVICE_TYPE_HARDWARE_ANGLE,
+ EGL_NONE,
+ };
+ p->egl_display = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE,
+ EGL_DEFAULT_DISPLAY, display_attributes);
+ if (p->egl_display == EGL_NO_DISPLAY) {
+ MP_FATAL(vo, "Couldn't get display\n");
+ return false;
+ }
+
+ return true;
+}
+
+static void egl_window_surface_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ if (p->egl_window) {
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ }
+}
+
+static bool egl_window_surface_create(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+
+ int window_attribs_len = 0;
+ EGLint *window_attribs = NULL;
+
+ EGLint flip_val;
+ if (eglGetConfigAttrib(p->egl_display, p->egl_config,
+ EGL_OPTIMAL_SURFACE_ORIENTATION_ANGLE, &flip_val))
+ {
+ if (flip_val == EGL_SURFACE_ORIENTATION_INVERT_Y_ANGLE) {
+ MP_TARRAY_APPEND(NULL, window_attribs, window_attribs_len,
+ EGL_SURFACE_ORIENTATION_ANGLE);
+ MP_TARRAY_APPEND(NULL, window_attribs, window_attribs_len,
+ EGL_SURFACE_ORIENTATION_INVERT_Y_ANGLE);
+ p->flipped = true;
+ MP_VERBOSE(vo, "Rendering flipped.\n");
+ }
+ }
+
+ MP_TARRAY_APPEND(NULL, window_attribs, window_attribs_len, EGL_NONE);
+ p->egl_window = eglCreateWindowSurface(p->egl_display, p->egl_config,
+ vo_w32_hwnd(vo), window_attribs);
+ talloc_free(window_attribs);
+ if (!p->egl_window) {
+ MP_FATAL(vo, "Could not create EGL surface!\n");
+ goto fail;
+ }
+
+ eglMakeCurrent(p->egl_display, p->egl_window, p->egl_window,
+ p->egl_context);
+ return true;
+fail:
+ egl_window_surface_destroy(ctx);
+ return false;
+}
+
+static void context_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ if (p->egl_context) {
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ eglDestroyContext(p->egl_display, p->egl_context);
+ }
+ p->egl_context = EGL_NO_CONTEXT;
+}
+
+static bool context_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+
+ if (!eglInitialize(p->egl_display, NULL, NULL)) {
+ MP_FATAL(vo, "Couldn't initialize EGL\n");
+ goto fail;
+ }
+
+ const char *exts = eglQueryString(p->egl_display, EGL_EXTENSIONS);
+ if (exts)
+ MP_DBG(vo, "EGL extensions: %s\n", exts);
+
+ if (!mpegl_create_context(ctx, p->egl_display, &p->egl_context,
+ &p->egl_config))
+ {
+ MP_FATAL(vo, "Could not create EGL context!\n");
+ goto fail;
+ }
+
+ return true;
+fail:
+ context_destroy(ctx);
+ return false;
+}
+
+static void angle_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_gl_ctx_uninit(ctx);
+
+ DwmEnableMMCSS(FALSE);
+
+ // Uninit the EGL surface implementation that is being used. Note: This may
+ // result in the *_destroy function being called twice since it is also
+ // called when the surface create function fails. This is fine because the
+ // *_destroy functions are idempotent.
+ if (p->dxgi_swapchain)
+ d3d11_swapchain_surface_destroy(ctx);
+ else
+ egl_window_surface_destroy(ctx);
+
+ context_destroy(ctx);
+
+ // Uninit the EGL device implementation that is being used
+ if (p->d3d11_device)
+ d3d11_device_destroy(ctx);
+ else
+ d3d9_device_destroy(ctx);
+
+ vo_w32_uninit(ctx->vo);
+}
+
+static int GLAPIENTRY angle_swap_interval(int interval)
+{
+ if (!current_ctx)
+ return 0;
+ struct priv *p = current_ctx->priv;
+
+ if (p->dxgi_swapchain) {
+ p->swapinterval = MPCLAMP(interval, 0, 4);
+ return 1;
+ } else {
+ return eglSwapInterval(p->egl_display, interval);
+ }
+}
+
+static void d3d11_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ // Calling Present() on a flip-sequential swap chain will silently change
+ // the underlying storage of the back buffer to point to the next buffer in
+ // the chain. This results in the RTVs for the back buffer becoming
+ // unbound. Since ANGLE doesn't know we called Present(), it will continue
+ // using the unbound RTVs, so we must save and restore them ourselves.
+ ID3D11RenderTargetView *rtvs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {0};
+ ID3D11DepthStencilView *dsv = NULL;
+ ID3D11DeviceContext_OMGetRenderTargets(p->d3d11_context,
+ MP_ARRAY_SIZE(rtvs), rtvs, &dsv);
+
+ HRESULT hr = IDXGISwapChain_Present(p->dxgi_swapchain, p->swapinterval, 0);
+ if (FAILED(hr))
+ MP_FATAL(ctx->vo, "Couldn't present: %s\n", mp_HRESULT_to_str(hr));
+
+ // Restore the RTVs and release the objects
+ ID3D11DeviceContext_OMSetRenderTargets(p->d3d11_context,
+ MP_ARRAY_SIZE(rtvs), rtvs, dsv);
+ for (int i = 0; i < MP_ARRAY_SIZE(rtvs); i++)
+ SAFE_RELEASE(rtvs[i]);
+ SAFE_RELEASE(dsv);
+}
+
+static void egl_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ eglSwapBuffers(p->egl_display, p->egl_window);
+}
+
+static void angle_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ if (p->dxgi_swapchain)
+ d3d11_swap_buffers(ctx);
+ else
+ egl_swap_buffers(ctx);
+}
+
+
+static int angle_color_depth(struct ra_swapchain *sw)
+{
+ // Only 8-bit output is supported at the moment
+ return 8;
+}
+
+static bool angle_submit_frame(struct ra_swapchain *sw,
+ const struct vo_frame *frame)
+{
+ struct priv *p = sw->ctx->priv;
+ bool ret = ra_gl_ctx_submit_frame(sw, frame);
+ if (p->d3d11_context) {
+ // DXGI Present doesn't flush the immediate context, which can make
+ // timers inaccurate, since the end queries might not be sent until the
+ // next frame. Fix this by flushing the context now.
+ ID3D11DeviceContext_Flush(p->d3d11_context);
+ }
+ return ret;
+}
+
+static bool angle_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct vo *vo = ctx->vo;
+ GL *gl = &p->gl;
+
+ p->opts = mp_get_config_group(ctx, ctx->global, &angle_conf);
+ struct angle_opts *o = p->opts;
+
+ if (!angle_load()) {
+ MP_VERBOSE(vo, "Failed to load LIBEGL.DLL\n");
+ goto fail;
+ }
+
+ // Create the underlying EGL device implementation
+ bool context_ok = false;
+ if ((!context_ok && !o->renderer) || o->renderer == RENDERER_D3D11) {
+ context_ok = d3d11_device_create(ctx);
+ if (context_ok) {
+ context_ok = context_init(ctx);
+ if (!context_ok)
+ d3d11_device_destroy(ctx);
+ }
+ }
+ if ((!context_ok && !o->renderer) || o->renderer == RENDERER_D3D9) {
+ context_ok = d3d9_device_create(ctx);
+ if (context_ok) {
+ MP_VERBOSE(vo, "Using Direct3D 9\n");
+
+ context_ok = context_init(ctx);
+ if (!context_ok)
+ d3d9_device_destroy(ctx);
+ }
+ }
+ if (!context_ok)
+ goto fail;
+
+ if (!vo_w32_init(vo))
+ goto fail;
+
+ // Create the underlying EGL surface implementation
+ bool surface_ok = false;
+ if ((!surface_ok && o->egl_windowing == -1) || o->egl_windowing == 0) {
+ surface_ok = d3d11_swapchain_surface_create(ctx);
+ }
+ if ((!surface_ok && o->egl_windowing == -1) || o->egl_windowing == 1) {
+ surface_ok = egl_window_surface_create(ctx);
+ if (surface_ok)
+ MP_VERBOSE(vo, "Using EGL windowing\n");
+ }
+ if (!surface_ok)
+ goto fail;
+
+ mpegl_load_functions(gl, vo->log);
+
+ current_ctx = ctx;
+ gl->SwapInterval = angle_swap_interval;
+
+ // Custom swapchain impl for the D3D11 swapchain-based surface
+ static const struct ra_swapchain_fns dxgi_swapchain_fns = {
+ .color_depth = angle_color_depth,
+ .submit_frame = angle_submit_frame,
+ };
+ struct ra_gl_ctx_params params = {
+ .swap_buffers = angle_swap_buffers,
+ .external_swapchain = p->dxgi_swapchain ? &dxgi_swapchain_fns : NULL,
+ };
+
+ gl->flipped = p->flipped;
+ if (!ra_gl_ctx_init(ctx, gl, params))
+ goto fail;
+
+ DwmEnableMMCSS(TRUE); // DWM MMCSS cargo-cult. The dxgl backend also does this.
+
+ return true;
+fail:
+ angle_uninit(ctx);
+ return false;
+}
+
+static void resize(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ if (p->dxgi_swapchain)
+ d3d11_backbuffer_resize(ctx);
+ else
+ eglWaitClient(); // Should get ANGLE to resize its swapchain
+ ra_gl_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight, 0);
+}
+
+static bool angle_reconfig(struct ra_ctx *ctx)
+{
+ vo_w32_config(ctx->vo);
+ resize(ctx);
+ return true;
+}
+
+static int angle_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ int ret = vo_w32_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE)
+ resize(ctx);
+ return ret;
+}
+
+const struct ra_ctx_fns ra_ctx_angle = {
+ .type = "opengl",
+ .name = "angle",
+ .init = angle_init,
+ .reconfig = angle_reconfig,
+ .control = angle_control,
+ .uninit = angle_uninit,
+};
diff --git a/video/out/opengl/context_drm_egl.c b/video/out/opengl/context_drm_egl.c
new file mode 100644
index 0000000..2db428f
--- /dev/null
+++ b/video/out/opengl/context_drm_egl.c
@@ -0,0 +1,744 @@
+/*
+ * 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 <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <string.h>
+#include <poll.h>
+#include <unistd.h>
+
+#include <gbm.h>
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+#include <drm_fourcc.h>
+
+#include "libmpv/render_gl.h"
+#include "common/common.h"
+#include "osdep/timer.h"
+#include "video/out/drm_atomic.h"
+#include "video/out/drm_common.h"
+#include "video/out/present_sync.h"
+
+#include "egl_helpers.h"
+#include "common.h"
+#include "context.h"
+
+#ifndef EGL_PLATFORM_GBM_MESA
+#define EGL_PLATFORM_GBM_MESA 0x31D7
+#endif
+
+#ifndef EGL_PLATFORM_GBM_KHR
+#define EGL_PLATFORM_GBM_KHR 0x31D7
+#endif
+
+struct gbm_frame {
+ struct gbm_bo *bo;
+};
+
+struct gbm {
+ struct gbm_surface *surface;
+ struct gbm_device *device;
+ struct gbm_frame **bo_queue;
+ unsigned int num_bos;
+};
+
+struct egl {
+ EGLDisplay display;
+ EGLContext context;
+ EGLSurface surface;
+};
+
+struct priv {
+ GL gl;
+
+ struct egl egl;
+ struct gbm gbm;
+
+ GLsync *vsync_fences;
+ unsigned int num_vsync_fences;
+
+ uint32_t gbm_format;
+ uint64_t *gbm_modifiers;
+ unsigned int num_gbm_modifiers;
+
+ struct mpv_opengl_drm_params_v2 drm_params;
+ struct mpv_opengl_drm_draw_surface_size draw_surface_size;
+};
+
+// Not general. Limited to only the formats being used in this module
+static const char *gbm_format_to_string(uint32_t format)
+{
+ switch (format) {
+ case GBM_FORMAT_XRGB8888:
+ return "GBM_FORMAT_XRGB8888";
+ case GBM_FORMAT_ARGB8888:
+ return "GBM_FORMAT_ARGB8888";
+ case GBM_FORMAT_XBGR8888:
+ return "GBM_FORMAT_XBGR8888";
+ case GBM_FORMAT_ABGR8888:
+ return "GBM_FORMAT_ABGR8888";
+ case GBM_FORMAT_XRGB2101010:
+ return "GBM_FORMAT_XRGB2101010";
+ case GBM_FORMAT_ARGB2101010:
+ return "GBM_FORMAT_ARGB2101010";
+ case GBM_FORMAT_XBGR2101010:
+ return "GBM_FORMAT_XBGR2101010";
+ case GBM_FORMAT_ABGR2101010:
+ return "GBM_FORMAT_ABGR2101010";
+ default:
+ return "UNKNOWN";
+ }
+}
+
+// Allow falling back to an ARGB EGLConfig when we have an XRGB framebuffer.
+// Also allow falling back to an XRGB EGLConfig for ARGB framebuffers, since
+// this seems necessary to work with broken Mali drivers that don't report
+// their EGLConfigs as supporting alpha properly.
+static uint32_t fallback_format_for(uint32_t format)
+{
+ switch (format) {
+ case GBM_FORMAT_XRGB8888:
+ return GBM_FORMAT_ARGB8888;
+ case GBM_FORMAT_ARGB8888:
+ return GBM_FORMAT_XRGB8888;
+ case GBM_FORMAT_XBGR8888:
+ return GBM_FORMAT_ABGR8888;
+ case GBM_FORMAT_ABGR8888:
+ return GBM_FORMAT_XBGR8888;
+ case GBM_FORMAT_XRGB2101010:
+ return GBM_FORMAT_ARGB2101010;
+ case GBM_FORMAT_ARGB2101010:
+ return GBM_FORMAT_XRGB2101010;
+ case GBM_FORMAT_XBGR2101010:
+ return GBM_FORMAT_ABGR2101010;
+ case GBM_FORMAT_ABGR2101010:
+ return GBM_FORMAT_XBGR2101010;
+ default:
+ return 0;
+ }
+}
+
+static int match_config_to_visual(void *user_data, EGLConfig *configs, int num_configs)
+{
+ struct ra_ctx *ctx = (struct ra_ctx*)user_data;
+ struct priv *p = ctx->priv;
+ const EGLint visual_id[] = {
+ (EGLint)p->gbm_format,
+ (EGLint)fallback_format_for(p->gbm_format),
+ 0
+ };
+
+ for (unsigned int i = 0; visual_id[i] != 0; ++i) {
+ MP_VERBOSE(ctx, "Attempting to find EGLConfig matching %s\n",
+ gbm_format_to_string(visual_id[i]));
+ for (unsigned int j = 0; j < num_configs; ++j) {
+ EGLint id;
+
+ if (!eglGetConfigAttrib(p->egl.display, configs[j], EGL_NATIVE_VISUAL_ID, &id))
+ continue;
+
+ if (visual_id[i] == id) {
+ MP_VERBOSE(ctx, "Found matching EGLConfig for %s\n",
+ gbm_format_to_string(visual_id[i]));
+ return j;
+ }
+ }
+ MP_VERBOSE(ctx, "No matching EGLConfig for %s\n", gbm_format_to_string(visual_id[i]));
+ }
+
+ MP_ERR(ctx, "Could not find EGLConfig matching the GBM visual (%s).\n",
+ gbm_format_to_string(p->gbm_format));
+ return -1;
+}
+
+static EGLDisplay egl_get_display(struct gbm_device *gbm_device)
+{
+ EGLDisplay ret;
+
+ ret = mpegl_get_display(EGL_PLATFORM_GBM_MESA, "EGL_MESA_platform_gbm", gbm_device);
+ if (ret != EGL_NO_DISPLAY)
+ return ret;
+
+ ret = mpegl_get_display(EGL_PLATFORM_GBM_KHR, "EGL_KHR_platform_gbm", gbm_device);
+ if (ret != EGL_NO_DISPLAY)
+ return ret;
+
+ return eglGetDisplay(gbm_device);
+}
+
+static bool init_egl(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ MP_VERBOSE(ctx, "Initializing EGL\n");
+ p->egl.display = egl_get_display(p->gbm.device);
+
+ if (p->egl.display == EGL_NO_DISPLAY) {
+ MP_ERR(ctx, "Failed to get EGL display.\n");
+ return false;
+ }
+ if (!eglInitialize(p->egl.display, NULL, NULL)) {
+ MP_ERR(ctx, "Failed to initialize EGL.\n");
+ return false;
+ }
+ EGLConfig config;
+ if (!mpegl_create_context_cb(ctx,
+ p->egl.display,
+ (struct mpegl_cb){match_config_to_visual, ctx},
+ &p->egl.context,
+ &config))
+ return false;
+
+ MP_VERBOSE(ctx, "Initializing EGL surface\n");
+ p->egl.surface = mpegl_create_window_surface(
+ p->egl.display, config, p->gbm.surface);
+ if (p->egl.surface == EGL_NO_SURFACE) {
+ p->egl.surface = eglCreateWindowSurface(
+ p->egl.display, config, p->gbm.surface, NULL);
+ }
+ if (p->egl.surface == EGL_NO_SURFACE) {
+ MP_ERR(ctx, "Failed to create EGL surface.\n");
+ return false;
+ }
+ return true;
+}
+
+static bool init_gbm(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+ MP_VERBOSE(ctx->vo, "Creating GBM device\n");
+ p->gbm.device = gbm_create_device(drm->fd);
+ if (!p->gbm.device) {
+ MP_ERR(ctx->vo, "Failed to create GBM device.\n");
+ return false;
+ }
+
+ MP_VERBOSE(ctx->vo, "Initializing GBM surface (%d x %d)\n",
+ p->draw_surface_size.width, p->draw_surface_size.height);
+ if (p->num_gbm_modifiers == 0) {
+ p->gbm.surface = gbm_surface_create(
+ p->gbm.device,
+ p->draw_surface_size.width,
+ p->draw_surface_size.height,
+ p->gbm_format,
+ GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
+ } else {
+ p->gbm.surface = gbm_surface_create_with_modifiers(
+ p->gbm.device,
+ p->draw_surface_size.width,
+ p->draw_surface_size.height,
+ p->gbm_format,
+ p->gbm_modifiers,
+ p->num_gbm_modifiers);
+ }
+ if (!p->gbm.surface) {
+ MP_ERR(ctx->vo, "Failed to create GBM surface.\n");
+ return false;
+ }
+ return true;
+}
+
+static void framebuffer_destroy_callback(struct gbm_bo *bo, void *data)
+{
+ struct framebuffer *fb = data;
+ if (fb) {
+ drmModeRmFB(fb->fd, fb->id);
+ }
+}
+
+static void update_framebuffer_from_bo(struct ra_ctx *ctx, struct gbm_bo *bo)
+{
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+ struct framebuffer *fb = gbm_bo_get_user_data(bo);
+ if (fb) {
+ drm->fb = fb;
+ return;
+ }
+
+ fb = talloc_zero(ctx, struct framebuffer);
+ fb->fd = drm->fd;
+ fb->width = gbm_bo_get_width(bo);
+ fb->height = gbm_bo_get_height(bo);
+ uint64_t modifier = gbm_bo_get_modifier(bo);
+
+ int ret;
+ if (p->num_gbm_modifiers == 0 || modifier == DRM_FORMAT_MOD_INVALID) {
+ uint32_t stride = gbm_bo_get_stride(bo);
+ uint32_t handle = gbm_bo_get_handle(bo).u32;
+ ret = drmModeAddFB2(fb->fd, fb->width, fb->height,
+ p->gbm_format,
+ (uint32_t[4]){handle, 0, 0, 0},
+ (uint32_t[4]){stride, 0, 0, 0},
+ (uint32_t[4]){0, 0, 0, 0},
+ &fb->id, 0);
+ } else {
+ MP_VERBOSE(ctx, "GBM surface using modifier 0x%"PRIX64"\n", modifier);
+
+ uint32_t handles[4] = {0};
+ uint32_t strides[4] = {0};
+ uint32_t offsets[4] = {0};
+ uint64_t modifiers[4] = {0};
+
+ const int num_planes = gbm_bo_get_plane_count(bo);
+ for (int i = 0; i < num_planes; ++i) {
+ handles[i] = gbm_bo_get_handle_for_plane(bo, i).u32;
+ strides[i] = gbm_bo_get_stride_for_plane(bo, i);
+ offsets[i] = gbm_bo_get_offset(bo, i);
+ modifiers[i] = modifier;
+ }
+
+ ret = drmModeAddFB2WithModifiers(fb->fd, fb->width, fb->height,
+ p->gbm_format,
+ handles, strides, offsets, modifiers,
+ &fb->id, DRM_MODE_FB_MODIFIERS);
+ }
+ if (ret) {
+ MP_ERR(ctx->vo, "Failed to create framebuffer: %s\n", mp_strerror(errno));
+ }
+ gbm_bo_set_user_data(bo, fb, framebuffer_destroy_callback);
+ drm->fb = fb;
+}
+
+static void queue_flip(struct ra_ctx *ctx, struct gbm_frame *frame)
+{
+ struct vo_drm_state *drm = ctx->vo->drm;
+
+ update_framebuffer_from_bo(ctx, frame->bo);
+
+ struct drm_atomic_context *atomic_ctx = drm->atomic_context;
+ drm_object_set_property(atomic_ctx->request, atomic_ctx->draw_plane, "FB_ID", drm->fb->id);
+ drm_object_set_property(atomic_ctx->request, atomic_ctx->draw_plane, "CRTC_ID", atomic_ctx->crtc->id);
+ drm_object_set_property(atomic_ctx->request, atomic_ctx->draw_plane, "ZPOS", 1);
+
+ int ret = drmModeAtomicCommit(drm->fd, atomic_ctx->request,
+ DRM_MODE_ATOMIC_NONBLOCK | DRM_MODE_PAGE_FLIP_EVENT, drm);
+
+ if (ret)
+ MP_WARN(ctx->vo, "Failed to commit atomic request: %s\n", mp_strerror(ret));
+ drm->waiting_for_flip = !ret;
+
+ drmModeAtomicFree(atomic_ctx->request);
+ atomic_ctx->request = drmModeAtomicAlloc();
+}
+
+static void enqueue_bo(struct ra_ctx *ctx, struct gbm_bo *bo)
+{
+ struct priv *p = ctx->priv;
+
+ struct gbm_frame *new_frame = talloc(p, struct gbm_frame);
+ new_frame->bo = bo;
+ MP_TARRAY_APPEND(p, p->gbm.bo_queue, p->gbm.num_bos, new_frame);
+}
+
+static void dequeue_bo(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ talloc_free(p->gbm.bo_queue[0]);
+ MP_TARRAY_REMOVE_AT(p->gbm.bo_queue, p->gbm.num_bos, 0);
+}
+
+static void swapchain_step(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (!(p->gbm.num_bos > 0))
+ return;
+
+ if (p->gbm.bo_queue[0]->bo)
+ gbm_surface_release_buffer(p->gbm.surface, p->gbm.bo_queue[0]->bo);
+ dequeue_bo(ctx);
+}
+
+static void new_fence(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->gl.FenceSync) {
+ GLsync fence = p->gl.FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ if (fence)
+ MP_TARRAY_APPEND(p, p->vsync_fences, p->num_vsync_fences, fence);
+ }
+}
+
+static void wait_fence(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ while (p->num_vsync_fences && (p->num_vsync_fences >= p->gbm.num_bos)) {
+ p->gl.ClientWaitSync(p->vsync_fences[0], GL_SYNC_FLUSH_COMMANDS_BIT, 1e9);
+ p->gl.DeleteSync(p->vsync_fences[0]);
+ MP_TARRAY_REMOVE_AT(p->vsync_fences, p->num_vsync_fences, 0);
+ }
+}
+
+static bool drm_egl_start_frame(struct ra_swapchain *sw, struct ra_fbo *out_fbo)
+{
+ struct ra_ctx *ctx = sw->ctx;
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+
+ if (!drm->atomic_context->request) {
+ drm->atomic_context->request = drmModeAtomicAlloc();
+ p->drm_params.atomic_request_ptr = &drm->atomic_context->request;
+ }
+
+ return ra_gl_ctx_start_frame(sw, out_fbo);
+}
+
+static bool drm_egl_submit_frame(struct ra_swapchain *sw, const struct vo_frame *frame)
+{
+ struct ra_ctx *ctx = sw->ctx;
+ struct vo_drm_state *drm = ctx->vo->drm;
+
+ drm->still = frame->still;
+
+ return ra_gl_ctx_submit_frame(sw, frame);
+}
+
+static void drm_egl_swap_buffers(struct ra_swapchain *sw)
+{
+ struct ra_ctx *ctx = sw->ctx;
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+ const bool drain = drm->paused || drm->still; // True when we need to drain the swapchain
+
+ if (!drm->active)
+ return;
+
+ wait_fence(ctx);
+
+ eglSwapBuffers(p->egl.display, p->egl.surface);
+
+ struct gbm_bo *new_bo = gbm_surface_lock_front_buffer(p->gbm.surface);
+ if (!new_bo) {
+ MP_ERR(ctx->vo, "Couldn't lock front buffer\n");
+ return;
+ }
+ enqueue_bo(ctx, new_bo);
+ new_fence(ctx);
+
+ while (drain || p->gbm.num_bos > ctx->vo->opts->swapchain_depth ||
+ !gbm_surface_has_free_buffers(p->gbm.surface)) {
+ if (drm->waiting_for_flip) {
+ vo_drm_wait_on_flip(drm);
+ swapchain_step(ctx);
+ }
+ if (p->gbm.num_bos <= 1)
+ break;
+ if (!p->gbm.bo_queue[1] || !p->gbm.bo_queue[1]->bo) {
+ MP_ERR(ctx->vo, "Hole in swapchain?\n");
+ swapchain_step(ctx);
+ continue;
+ }
+ queue_flip(ctx, p->gbm.bo_queue[1]);
+ }
+}
+
+static const struct ra_swapchain_fns drm_egl_swapchain = {
+ .start_frame = drm_egl_start_frame,
+ .submit_frame = drm_egl_submit_frame,
+ .swap_buffers = drm_egl_swap_buffers,
+};
+
+static void drm_egl_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+ if (drm) {
+ struct drm_atomic_context *atomic_ctx = drm->atomic_context;
+
+ if (drmModeAtomicCommit(drm->fd, atomic_ctx->request, 0, NULL))
+ MP_ERR(ctx->vo, "Failed to commit atomic request: %s\n",
+ mp_strerror(errno));
+
+ drmModeAtomicFree(atomic_ctx->request);
+ }
+
+ ra_gl_ctx_uninit(ctx);
+ vo_drm_uninit(ctx->vo);
+
+ if (p) {
+ // According to GBM documentation all BO:s must be released
+ // before gbm_surface_destroy can be called on the surface.
+ while (p->gbm.num_bos) {
+ swapchain_step(ctx);
+ }
+
+ eglMakeCurrent(p->egl.display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ if (p->egl.display != EGL_NO_DISPLAY) {
+ eglDestroySurface(p->egl.display, p->egl.surface);
+ eglDestroyContext(p->egl.display, p->egl.context);
+ }
+ if (p->gbm.surface)
+ gbm_surface_destroy(p->gbm.surface);
+ eglTerminate(p->egl.display);
+ gbm_device_destroy(p->gbm.device);
+
+ if (p->drm_params.render_fd != -1)
+ close(p->drm_params.render_fd);
+ }
+}
+
+// If the draw plane supports ARGB we want to use that, but if it doesn't we
+// fall back on XRGB. If we do not have atomic there is no particular reason to
+// be using ARGB (drmprime hwdec will not work without atomic, anyway), so we
+// fall back to XRGB (another reason is that we do not have the convenient
+// atomic_ctx and its convenient plane fields).
+static bool probe_gbm_format(struct ra_ctx *ctx, uint32_t argb_format, uint32_t xrgb_format)
+{
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+
+ drmModePlane *drmplane = drmModeGetPlane(drm->fd, drm->atomic_context->draw_plane->id);
+ bool have_argb = false;
+ bool have_xrgb = false;
+ bool result = false;
+ for (unsigned int i = 0; i < drmplane->count_formats; ++i) {
+ if (drmplane->formats[i] == argb_format) {
+ have_argb = true;
+ } else if (drmplane->formats[i] == xrgb_format) {
+ have_xrgb = true;
+ }
+ }
+
+ if (have_argb) {
+ p->gbm_format = argb_format;
+ MP_VERBOSE(ctx->vo, "%s supported by draw plane.\n", gbm_format_to_string(argb_format));
+ result = true;
+ } else if (have_xrgb) {
+ p->gbm_format = xrgb_format;
+ MP_VERBOSE(ctx->vo, "%s not supported by draw plane: Falling back to %s.\n",
+ gbm_format_to_string(argb_format), gbm_format_to_string(xrgb_format));
+ result = true;
+ }
+
+ drmModeFreePlane(drmplane);
+ return result;
+}
+
+static bool probe_gbm_modifiers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo_drm_state *drm = ctx->vo->drm;
+
+ drmModePropertyBlobPtr blob = drm_object_get_property_blob(drm->atomic_context->draw_plane,
+ "IN_FORMATS");
+ if (!blob) {
+ MP_VERBOSE(ctx->vo, "Failed to find IN_FORMATS property\n");
+ return false;
+ }
+
+ struct drm_format_modifier_blob *data = blob->data;
+ uint32_t *fmts = (uint32_t *)((char *)data + data->formats_offset);
+ struct drm_format_modifier *mods =
+ (struct drm_format_modifier *)((char *)data + data->modifiers_offset);
+
+ for (unsigned int j = 0; j < data->count_modifiers; ++j) {
+ struct drm_format_modifier *mod = &mods[j];
+ for (uint64_t k = 0; k < 64; ++k) {
+ if (mod->formats & (1ull << k)) {
+ uint32_t fmt = fmts[k + mod->offset];
+ if (fmt == p->gbm_format) {
+ MP_TARRAY_APPEND(p, p->gbm_modifiers,
+ p->num_gbm_modifiers, mod->modifier);
+ MP_VERBOSE(ctx->vo, "Supported modifier: 0x%"PRIX64"\n",
+ (uint64_t)mod->modifier);
+ break;
+ }
+ }
+ }
+ }
+ drmModeFreePropertyBlob(blob);
+
+ if (p->num_gbm_modifiers == 0) {
+ MP_VERBOSE(ctx->vo, "No supported DRM modifiers found.\n");
+ }
+ return true;
+}
+
+static void drm_egl_get_vsync(struct ra_ctx *ctx, struct vo_vsync_info *info)
+{
+ struct vo_drm_state *drm = ctx->vo->drm;
+ present_sync_get_info(drm->present, info);
+}
+
+static bool drm_egl_init(struct ra_ctx *ctx)
+{
+ if (!vo_drm_init(ctx->vo))
+ goto err;
+
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct vo_drm_state *drm = ctx->vo->drm;
+
+ if (ctx->vo->drm->opts->draw_surface_size.wh_valid) {
+ p->draw_surface_size.width = ctx->vo->drm->opts->draw_surface_size.w;
+ p->draw_surface_size.height = ctx->vo->drm->opts->draw_surface_size.h;
+ } else {
+ p->draw_surface_size.width = drm->mode.mode.hdisplay;
+ p->draw_surface_size.height = drm->mode.mode.vdisplay;
+ }
+
+ drm->width = p->draw_surface_size.width;
+ drm->height = p->draw_surface_size.height;
+
+ uint32_t argb_format;
+ uint32_t xrgb_format;
+ switch (ctx->vo->drm->opts->drm_format) {
+ case DRM_OPTS_FORMAT_XRGB2101010:
+ argb_format = GBM_FORMAT_ARGB2101010;
+ xrgb_format = GBM_FORMAT_XRGB2101010;
+ break;
+ case DRM_OPTS_FORMAT_XBGR2101010:
+ argb_format = GBM_FORMAT_ABGR2101010;
+ xrgb_format = GBM_FORMAT_XBGR2101010;
+ break;
+ case DRM_OPTS_FORMAT_XBGR8888:
+ argb_format = GBM_FORMAT_ABGR8888;
+ xrgb_format = GBM_FORMAT_XBGR8888;
+ break;
+ default:
+ argb_format = GBM_FORMAT_ARGB8888;
+ xrgb_format = GBM_FORMAT_XRGB8888;
+ break;
+ }
+
+ if (!probe_gbm_format(ctx, argb_format, xrgb_format)) {
+ MP_ERR(ctx->vo, "No suitable format found on draw plane (tried: %s and %s).\n",
+ gbm_format_to_string(argb_format), gbm_format_to_string(xrgb_format));
+ goto err;
+ }
+
+ // It is not fatal if this fails. We'll just try without modifiers.
+ probe_gbm_modifiers(ctx);
+
+ if (!init_gbm(ctx)) {
+ MP_ERR(ctx->vo, "Failed to setup GBM.\n");
+ goto err;
+ }
+
+ if (!init_egl(ctx)) {
+ MP_ERR(ctx->vo, "Failed to setup EGL.\n");
+ goto err;
+ }
+
+ if (!eglMakeCurrent(p->egl.display, p->egl.surface, p->egl.surface,
+ p->egl.context)) {
+ MP_ERR(ctx->vo, "Failed to make context current.\n");
+ goto err;
+ }
+
+ mpegl_load_functions(&p->gl, ctx->vo->log);
+ // required by gbm_surface_lock_front_buffer
+ eglSwapBuffers(p->egl.display, p->egl.surface);
+
+ MP_VERBOSE(ctx, "Preparing framebuffer\n");
+ struct gbm_bo *new_bo = gbm_surface_lock_front_buffer(p->gbm.surface);
+ if (!new_bo) {
+ MP_ERR(ctx, "Failed to lock GBM surface.\n");
+ goto err;
+ }
+
+ enqueue_bo(ctx, new_bo);
+ update_framebuffer_from_bo(ctx, new_bo);
+ if (!drm->fb || !drm->fb->id) {
+ MP_ERR(ctx, "Failed to create framebuffer.\n");
+ goto err;
+ }
+
+ if (!vo_drm_acquire_crtc(ctx->vo->drm)) {
+ MP_ERR(ctx, "Failed to set CRTC for connector %u: %s\n",
+ drm->connector->connector_id, mp_strerror(errno));
+ goto err;
+ }
+
+ vo_drm_set_monitor_par(ctx->vo);
+
+ p->drm_params.fd = drm->fd;
+ p->drm_params.crtc_id = drm->crtc_id;
+ p->drm_params.connector_id = drm->connector->connector_id;
+ p->drm_params.atomic_request_ptr = &drm->atomic_context->request;
+ char *rendernode_path = drmGetRenderDeviceNameFromFd(drm->fd);
+ if (rendernode_path) {
+ MP_VERBOSE(ctx, "Opening render node \"%s\"\n", rendernode_path);
+ p->drm_params.render_fd = open(rendernode_path, O_RDWR | O_CLOEXEC);
+ if (p->drm_params.render_fd == -1) {
+ MP_WARN(ctx, "Cannot open render node: %s\n", mp_strerror(errno));
+ }
+ free(rendernode_path);
+ } else {
+ p->drm_params.render_fd = -1;
+ MP_VERBOSE(ctx, "Could not find path to render node.\n");
+ }
+
+ struct ra_gl_ctx_params params = {
+ .external_swapchain = &drm_egl_swapchain,
+ .get_vsync = &drm_egl_get_vsync,
+ };
+ if (!ra_gl_ctx_init(ctx, &p->gl, params))
+ goto err;
+
+ ra_add_native_resource(ctx->ra, "drm_params_v2", &p->drm_params);
+ ra_add_native_resource(ctx->ra, "drm_draw_surface_size", &p->draw_surface_size);
+
+ return true;
+
+err:
+ drm_egl_uninit(ctx);
+ return false;
+}
+
+static bool drm_egl_reconfig(struct ra_ctx *ctx)
+{
+ struct vo_drm_state *drm = ctx->vo->drm;
+ ctx->vo->dwidth = drm->fb->width;
+ ctx->vo->dheight = drm->fb->height;
+ ra_gl_ctx_resize(ctx->swapchain, drm->fb->width, drm->fb->height, 0);
+ return true;
+}
+
+static int drm_egl_control(struct ra_ctx *ctx, int *events, int request,
+ void *arg)
+{
+ int ret = vo_drm_control(ctx->vo, events, request, arg);
+ return ret;
+}
+
+static void drm_egl_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ vo_drm_wait_events(ctx->vo, until_time_ns);
+}
+
+static void drm_egl_wakeup(struct ra_ctx *ctx)
+{
+ vo_drm_wakeup(ctx->vo);
+}
+
+const struct ra_ctx_fns ra_ctx_drm_egl = {
+ .type = "opengl",
+ .name = "drm",
+ .reconfig = drm_egl_reconfig,
+ .control = drm_egl_control,
+ .init = drm_egl_init,
+ .uninit = drm_egl_uninit,
+ .wait_events = drm_egl_wait_events,
+ .wakeup = drm_egl_wakeup,
+};
diff --git a/video/out/opengl/context_dxinterop.c b/video/out/opengl/context_dxinterop.c
new file mode 100644
index 0000000..cda696f
--- /dev/null
+++ b/video/out/opengl/context_dxinterop.c
@@ -0,0 +1,605 @@
+/*
+ * 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 <windows.h>
+#include <versionhelpers.h>
+#include <d3d9.h>
+#include <dwmapi.h>
+#include "osdep/windows_utils.h"
+#include "video/out/w32_common.h"
+#include "context.h"
+#include "utils.h"
+
+// For WGL_ACCESS_WRITE_DISCARD_NV, etc.
+#include <GL/wglext.h>
+
+EXTERN_C IMAGE_DOS_HEADER __ImageBase;
+#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase)
+
+// mingw-w64 header typo?
+#ifndef IDirect3DSwapChain9Ex_GetBackBuffer
+#define IDirect3DSwapChain9Ex_GetBackBuffer IDirect3DSwapChain9EX_GetBackBuffer
+#endif
+
+struct priv {
+ GL gl;
+
+ HMODULE d3d9_dll;
+ HRESULT (WINAPI *Direct3DCreate9Ex)(UINT SDKVersion, IDirect3D9Ex **ppD3D);
+
+ // Direct3D9 device and resources
+ IDirect3D9Ex *d3d9ex;
+ IDirect3DDevice9Ex *device;
+ HANDLE device_h;
+ IDirect3DSwapChain9Ex *swapchain;
+ IDirect3DSurface9 *backbuffer;
+ IDirect3DSurface9 *rtarget;
+ HANDLE rtarget_h;
+
+ // OpenGL offscreen context
+ HWND os_wnd;
+ HDC os_dc;
+ HGLRC os_ctx;
+
+ // OpenGL resources
+ GLuint texture;
+ GLuint main_fb;
+
+ // Did we lose the device?
+ bool lost_device;
+
+ // Requested and current parameters
+ int requested_swapinterval;
+ int width, height, swapinterval;
+};
+
+static __thread struct ra_ctx *current_ctx;
+
+static void pump_message_loop(void)
+{
+ // We have a hidden window on this thread (for the OpenGL context,) so pump
+ // its message loop at regular intervals to be safe
+ MSG message;
+ while (PeekMessageW(&message, NULL, 0, 0, PM_REMOVE))
+ DispatchMessageW(&message);
+}
+
+static void *w32gpa(const GLubyte *procName)
+{
+ HMODULE oglmod;
+ void *res = wglGetProcAddress(procName);
+ if (res)
+ return res;
+ oglmod = GetModuleHandleW(L"opengl32.dll");
+ return GetProcAddress(oglmod, procName);
+}
+
+static int os_ctx_create(struct ra_ctx *ctx)
+{
+ static const wchar_t os_wnd_class[] = L"mpv offscreen gl";
+ struct priv *p = ctx->priv;
+ GL *gl = &p->gl;
+ HGLRC legacy_context = NULL;
+
+ RegisterClassExW(&(WNDCLASSEXW) {
+ .cbSize = sizeof(WNDCLASSEXW),
+ .style = CS_OWNDC,
+ .lpfnWndProc = DefWindowProc,
+ .hInstance = HINST_THISCOMPONENT,
+ .lpszClassName = os_wnd_class,
+ });
+
+ // Create a hidden window for an offscreen OpenGL context. It might also be
+ // possible to use the VO window, but MSDN recommends against drawing to
+ // the same window with flip mode present and other APIs, so play it safe.
+ p->os_wnd = CreateWindowExW(0, os_wnd_class, os_wnd_class, 0, 0, 0, 200,
+ 200, NULL, NULL, HINST_THISCOMPONENT, NULL);
+ p->os_dc = GetDC(p->os_wnd);
+ if (!p->os_dc) {
+ MP_FATAL(ctx->vo, "Couldn't create window for offscreen rendering\n");
+ goto fail;
+ }
+
+ // Choose a pixel format. It probably doesn't matter what this is because
+ // the primary framebuffer will not be used.
+ PIXELFORMATDESCRIPTOR pfd = {
+ .nSize = sizeof pfd,
+ .nVersion = 1,
+ .dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
+ .iPixelType = PFD_TYPE_RGBA,
+ .cColorBits = 24,
+ .iLayerType = PFD_MAIN_PLANE,
+ };
+ int pf = ChoosePixelFormat(p->os_dc, &pfd);
+ if (!pf) {
+ MP_FATAL(ctx->vo,
+ "Couldn't choose pixelformat for offscreen rendering: %s\n",
+ mp_LastError_to_str());
+ goto fail;
+ }
+ SetPixelFormat(p->os_dc, pf, &pfd);
+
+ legacy_context = wglCreateContext(p->os_dc);
+ if (!legacy_context || !wglMakeCurrent(p->os_dc, legacy_context)) {
+ MP_FATAL(ctx->vo, "Couldn't create OpenGL context for offscreen rendering: %s\n",
+ mp_LastError_to_str());
+ goto fail;
+ }
+
+ const char *(GLAPIENTRY *wglGetExtensionsStringARB)(HDC hdc)
+ = w32gpa((const GLubyte*)"wglGetExtensionsStringARB");
+ if (!wglGetExtensionsStringARB) {
+ MP_FATAL(ctx->vo, "The OpenGL driver does not support OpenGL 3.x\n");
+ goto fail;
+ }
+
+ const char *wgl_exts = wglGetExtensionsStringARB(p->os_dc);
+ if (!gl_check_extension(wgl_exts, "WGL_ARB_create_context")) {
+ MP_FATAL(ctx->vo, "The OpenGL driver does not support OpenGL 3.x\n");
+ goto fail;
+ }
+
+ HGLRC (GLAPIENTRY *wglCreateContextAttribsARB)(HDC hDC, HGLRC hShareContext,
+ const int *attribList)
+ = w32gpa((const GLubyte*)"wglCreateContextAttribsARB");
+ if (!wglCreateContextAttribsARB) {
+ MP_FATAL(ctx->vo, "The OpenGL driver does not support OpenGL 3.x\n");
+ goto fail;
+ }
+
+ int attribs[] = {
+ WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
+ WGL_CONTEXT_MINOR_VERSION_ARB, 0,
+ WGL_CONTEXT_FLAGS_ARB, 0,
+ WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
+ 0
+ };
+
+ p->os_ctx = wglCreateContextAttribsARB(p->os_dc, 0, attribs);
+ if (!p->os_ctx) {
+ // NVidia, instead of ignoring WGL_CONTEXT_FLAGS_ARB, will error out if
+ // it's present on pre-3.2 contexts.
+ // Remove it from attribs and retry the context creation.
+ attribs[6] = attribs[7] = 0;
+ p->os_ctx = wglCreateContextAttribsARB(p->os_dc, 0, attribs);
+ }
+ if (!p->os_ctx) {
+ MP_FATAL(ctx->vo,
+ "Couldn't create OpenGL 3.x context for offscreen rendering: %s\n",
+ mp_LastError_to_str());
+ goto fail;
+ }
+
+ wglMakeCurrent(p->os_dc, NULL);
+ wglDeleteContext(legacy_context);
+ legacy_context = NULL;
+
+ if (!wglMakeCurrent(p->os_dc, p->os_ctx)) {
+ MP_FATAL(ctx->vo,
+ "Couldn't activate OpenGL 3.x context for offscreen rendering: %s\n",
+ mp_LastError_to_str());
+ goto fail;
+ }
+
+ mpgl_load_functions(gl, w32gpa, wgl_exts, ctx->vo->log);
+ if (!(gl->mpgl_caps & MPGL_CAP_DXINTEROP)) {
+ MP_FATAL(ctx->vo, "WGL_NV_DX_interop is not supported\n");
+ goto fail;
+ }
+
+ return 0;
+fail:
+ if (legacy_context) {
+ wglMakeCurrent(p->os_dc, NULL);
+ wglDeleteContext(legacy_context);
+ }
+ return -1;
+}
+
+static void os_ctx_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->os_ctx) {
+ wglMakeCurrent(p->os_dc, NULL);
+ wglDeleteContext(p->os_ctx);
+ }
+ if (p->os_dc)
+ ReleaseDC(p->os_wnd, p->os_dc);
+ if (p->os_wnd)
+ DestroyWindow(p->os_wnd);
+}
+
+static int d3d_size_dependent_create(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ GL *gl = &p->gl;
+ HRESULT hr;
+
+ IDirect3DSwapChain9 *sw9;
+ hr = IDirect3DDevice9Ex_GetSwapChain(p->device, 0, &sw9);
+ if (FAILED(hr)) {
+ MP_ERR(ctx->vo, "Couldn't get swap chain: %s\n", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ hr = IDirect3DSwapChain9_QueryInterface(sw9, &IID_IDirect3DSwapChain9Ex,
+ (void**)&p->swapchain);
+ if (FAILED(hr)) {
+ SAFE_RELEASE(sw9);
+ MP_ERR(ctx->vo, "Obtained swap chain is not IDirect3DSwapChain9Ex: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+ SAFE_RELEASE(sw9);
+
+ hr = IDirect3DSwapChain9Ex_GetBackBuffer(p->swapchain, 0,
+ D3DBACKBUFFER_TYPE_MONO, &p->backbuffer);
+ if (FAILED(hr)) {
+ MP_ERR(ctx->vo, "Couldn't get backbuffer: %s\n", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ // Get the format of the backbuffer
+ D3DSURFACE_DESC bb_desc = { 0 };
+ IDirect3DSurface9_GetDesc(p->backbuffer, &bb_desc);
+
+ MP_VERBOSE(ctx->vo, "DX_interop backbuffer size: %ux%u\n",
+ (unsigned)bb_desc.Width, (unsigned)bb_desc.Height);
+ MP_VERBOSE(ctx->vo, "DX_interop backbuffer format: %u\n",
+ (unsigned)bb_desc.Format);
+
+ // Create a rendertarget with the same format as the backbuffer for
+ // rendering from OpenGL
+ HANDLE share_handle = NULL;
+ hr = IDirect3DDevice9Ex_CreateRenderTarget(p->device, bb_desc.Width,
+ bb_desc.Height, bb_desc.Format, D3DMULTISAMPLE_NONE, 0, FALSE,
+ &p->rtarget, &share_handle);
+ if (FAILED(hr)) {
+ MP_ERR(ctx->vo, "Couldn't create rendertarget: %s\n", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ // Register the share handle with WGL_NV_DX_interop. Nvidia does not
+ // require the use of share handles, but Intel does.
+ if (share_handle)
+ gl->DXSetResourceShareHandleNV(p->rtarget, share_handle);
+
+ // Create the OpenGL-side texture
+ gl->GenTextures(1, &p->texture);
+
+ // Now share the rendertarget with OpenGL as a texture
+ p->rtarget_h = gl->DXRegisterObjectNV(p->device_h, p->rtarget, p->texture,
+ GL_TEXTURE_2D, WGL_ACCESS_WRITE_DISCARD_NV);
+ if (!p->rtarget_h) {
+ MP_ERR(ctx->vo, "Couldn't share rendertarget with OpenGL: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ // Lock the rendertarget for use from OpenGL. This will only be unlocked in
+ // swap_buffers() when it is blitted to the real Direct3D backbuffer.
+ if (!gl->DXLockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(ctx->vo, "Couldn't lock rendertarget: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ gl->BindFramebuffer(GL_FRAMEBUFFER, p->main_fb);
+ gl->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+ GL_TEXTURE_2D, p->texture, 0);
+ gl->BindFramebuffer(GL_FRAMEBUFFER, 0);
+
+ return 0;
+}
+
+static void d3d_size_dependent_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ GL *gl = &p->gl;
+
+ if (p->rtarget_h) {
+ gl->DXUnlockObjectsNV(p->device_h, 1, &p->rtarget_h);
+ gl->DXUnregisterObjectNV(p->device_h, p->rtarget_h);
+ }
+ p->rtarget_h = 0;
+ if (p->texture)
+ gl->DeleteTextures(1, &p->texture);
+ p->texture = 0;
+
+ SAFE_RELEASE(p->rtarget);
+ SAFE_RELEASE(p->backbuffer);
+ SAFE_RELEASE(p->swapchain);
+}
+
+static void fill_presentparams(struct ra_ctx *ctx,
+ D3DPRESENT_PARAMETERS *pparams)
+{
+ struct priv *p = ctx->priv;
+
+ // Present intervals other than IMMEDIATE and ONE don't seem to work. It's
+ // possible that they're not compatible with FLIPEX.
+ UINT presentation_interval;
+ switch (p->requested_swapinterval) {
+ case 0: presentation_interval = D3DPRESENT_INTERVAL_IMMEDIATE; break;
+ case 1: presentation_interval = D3DPRESENT_INTERVAL_ONE; break;
+ default: presentation_interval = D3DPRESENT_INTERVAL_ONE; break;
+ }
+
+ *pparams = (D3DPRESENT_PARAMETERS) {
+ .Windowed = TRUE,
+ .BackBufferWidth = ctx->vo->dwidth ? ctx->vo->dwidth : 1,
+ .BackBufferHeight = ctx->vo->dheight ? ctx->vo->dheight : 1,
+ // Add one frame for the backbuffer and one frame of "slack" to reduce
+ // contention with the window manager when acquiring the backbuffer
+ .BackBufferCount = ctx->vo->opts->swapchain_depth + 2,
+ .SwapEffect = IsWindows7OrGreater() ? D3DSWAPEFFECT_FLIPEX : D3DSWAPEFFECT_FLIP,
+ // Automatically get the backbuffer format from the display format
+ .BackBufferFormat = D3DFMT_UNKNOWN,
+ .PresentationInterval = presentation_interval,
+ .hDeviceWindow = vo_w32_hwnd(ctx->vo),
+ };
+}
+
+static int d3d_create(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ GL *gl = &p->gl;
+ HRESULT hr;
+
+ p->d3d9_dll = LoadLibraryW(L"d3d9.dll");
+ if (!p->d3d9_dll) {
+ MP_FATAL(ctx->vo, "Failed to load \"d3d9.dll\": %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ // WGL_NV_dx_interop requires Direct3D 9Ex on WDDM systems. Direct3D 9Ex
+ // also enables flip mode present for efficient rendering with the DWM.
+ p->Direct3DCreate9Ex = (void*)GetProcAddress(p->d3d9_dll,
+ "Direct3DCreate9Ex");
+ if (!p->Direct3DCreate9Ex) {
+ MP_FATAL(ctx->vo, "Direct3D 9Ex not supported\n");
+ return -1;
+ }
+
+ hr = p->Direct3DCreate9Ex(D3D_SDK_VERSION, &p->d3d9ex);
+ if (FAILED(hr)) {
+ MP_FATAL(ctx->vo, "Couldn't create Direct3D9Ex: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ D3DPRESENT_PARAMETERS pparams;
+ fill_presentparams(ctx, &pparams);
+
+ hr = IDirect3D9Ex_CreateDeviceEx(p->d3d9ex, D3DADAPTER_DEFAULT,
+ D3DDEVTYPE_HAL, vo_w32_hwnd(ctx->vo),
+ D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_PUREDEVICE |
+ D3DCREATE_FPU_PRESERVE | D3DCREATE_MULTITHREADED |
+ D3DCREATE_NOWINDOWCHANGES,
+ &pparams, NULL, &p->device);
+ if (FAILED(hr)) {
+ MP_FATAL(ctx->vo, "Couldn't create device: %s\n", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ IDirect3DDevice9Ex_SetMaximumFrameLatency(p->device, ctx->vo->opts->swapchain_depth);
+
+ // Register the Direct3D device with WGL_NV_dx_interop
+ p->device_h = gl->DXOpenDeviceNV(p->device);
+ if (!p->device_h) {
+ MP_FATAL(ctx->vo, "Couldn't open Direct3D device from OpenGL: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ return 0;
+}
+
+static void d3d_destroy(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ GL *gl = &p->gl;
+
+ if (p->device_h)
+ gl->DXCloseDeviceNV(p->device_h);
+ SAFE_RELEASE(p->device);
+ SAFE_RELEASE(p->d3d9ex);
+ if (p->d3d9_dll)
+ FreeLibrary(p->d3d9_dll);
+}
+
+static void dxgl_uninit(struct ra_ctx *ctx)
+{
+ ra_gl_ctx_uninit(ctx);
+ d3d_size_dependent_destroy(ctx);
+ d3d_destroy(ctx);
+ os_ctx_destroy(ctx);
+ vo_w32_uninit(ctx->vo);
+ DwmEnableMMCSS(FALSE);
+ pump_message_loop();
+}
+
+static void dxgl_reset(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ HRESULT hr;
+
+ // Check if the device actually needs to be reset
+ if (ctx->vo->dwidth == p->width && ctx->vo->dheight == p->height &&
+ p->requested_swapinterval == p->swapinterval && !p->lost_device)
+ return;
+
+ d3d_size_dependent_destroy(ctx);
+
+ D3DPRESENT_PARAMETERS pparams;
+ fill_presentparams(ctx, &pparams);
+
+ hr = IDirect3DDevice9Ex_ResetEx(p->device, &pparams, NULL);
+ if (FAILED(hr)) {
+ p->lost_device = true;
+ MP_ERR(ctx->vo, "Couldn't reset device: %s\n", mp_HRESULT_to_str(hr));
+ return;
+ }
+
+ if (d3d_size_dependent_create(ctx) < 0) {
+ p->lost_device = true;
+ MP_ERR(ctx->vo, "Couldn't recreate Direct3D objects after reset\n");
+ return;
+ }
+
+ MP_VERBOSE(ctx->vo, "Direct3D device reset\n");
+ p->width = ctx->vo->dwidth;
+ p->height = ctx->vo->dheight;
+ p->swapinterval = p->requested_swapinterval;
+ p->lost_device = false;
+}
+
+static int GLAPIENTRY dxgl_swap_interval(int interval)
+{
+ if (!current_ctx)
+ return 0;
+ struct priv *p = current_ctx->priv;
+
+ p->requested_swapinterval = interval;
+ dxgl_reset(current_ctx);
+ return 1;
+}
+
+static void dxgl_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ GL *gl = &p->gl;
+ HRESULT hr;
+
+ pump_message_loop();
+
+ // If the device is still lost, try to reset it again
+ if (p->lost_device)
+ dxgl_reset(ctx);
+ if (p->lost_device)
+ return;
+
+ if (!gl->DXUnlockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(ctx->vo, "Couldn't unlock rendertarget for present: %s\n",
+ mp_LastError_to_str());
+ return;
+ }
+
+ // Blit the OpenGL rendertarget to the backbuffer
+ hr = IDirect3DDevice9Ex_StretchRect(p->device, p->rtarget, NULL,
+ p->backbuffer, NULL, D3DTEXF_NONE);
+ if (FAILED(hr)) {
+ MP_ERR(ctx->vo, "Couldn't stretchrect for present: %s\n",
+ mp_HRESULT_to_str(hr));
+ return;
+ }
+
+ hr = IDirect3DDevice9Ex_PresentEx(p->device, NULL, NULL, NULL, NULL, 0);
+ switch (hr) {
+ case D3DERR_DEVICELOST:
+ case D3DERR_DEVICEHUNG:
+ MP_VERBOSE(ctx->vo, "Direct3D device lost! Resetting.\n");
+ p->lost_device = true;
+ dxgl_reset(ctx);
+ return;
+ default:
+ if (FAILED(hr))
+ MP_ERR(ctx->vo, "Failed to present: %s\n", mp_HRESULT_to_str(hr));
+ }
+
+ if (!gl->DXLockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(ctx->vo, "Couldn't lock rendertarget after present: %s\n",
+ mp_LastError_to_str());
+ }
+}
+
+static bool dxgl_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ GL *gl = &p->gl;
+
+ p->requested_swapinterval = 1;
+
+ if (!vo_w32_init(ctx->vo))
+ goto fail;
+ if (os_ctx_create(ctx) < 0)
+ goto fail;
+
+ // Create the shared framebuffer
+ gl->GenFramebuffers(1, &p->main_fb);
+
+ current_ctx = ctx;
+ gl->SwapInterval = dxgl_swap_interval;
+
+ if (d3d_create(ctx) < 0)
+ goto fail;
+ if (d3d_size_dependent_create(ctx) < 0)
+ goto fail;
+
+ static const struct ra_swapchain_fns empty_swapchain_fns = {0};
+ struct ra_gl_ctx_params params = {
+ .swap_buffers = dxgl_swap_buffers,
+ .external_swapchain = &empty_swapchain_fns,
+ };
+
+ gl->flipped = true;
+ if (!ra_gl_ctx_init(ctx, gl, params))
+ goto fail;
+
+ ra_add_native_resource(ctx->ra, "IDirect3DDevice9Ex", p->device);
+ ra_add_native_resource(ctx->ra, "dxinterop_device_HANDLE", p->device_h);
+
+ DwmEnableMMCSS(TRUE);
+ return true;
+fail:
+ dxgl_uninit(ctx);
+ return false;
+}
+
+static void resize(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ dxgl_reset(ctx);
+ ra_gl_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight, p->main_fb);
+}
+
+static bool dxgl_reconfig(struct ra_ctx *ctx)
+{
+ vo_w32_config(ctx->vo);
+ resize(ctx);
+ return true;
+}
+
+static int dxgl_control(struct ra_ctx *ctx, int *events, int request,
+ void *arg)
+{
+ int ret = vo_w32_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE)
+ resize(ctx);
+ return ret;
+}
+
+const struct ra_ctx_fns ra_ctx_dxgl = {
+ .type = "opengl",
+ .name = "dxinterop",
+ .init = dxgl_init,
+ .reconfig = dxgl_reconfig,
+ .control = dxgl_control,
+ .uninit = dxgl_uninit,
+};
diff --git a/video/out/opengl/context_glx.c b/video/out/opengl/context_glx.c
new file mode 100644
index 0000000..4062224
--- /dev/null
+++ b/video/out/opengl/context_glx.c
@@ -0,0 +1,351 @@
+/*
+ * 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 <X11/Xlib.h>
+#include <GL/glx.h>
+
+// FreeBSD 10.0-CURRENT lacks the GLX_ARB_create_context extension completely
+#ifndef GLX_CONTEXT_MAJOR_VERSION_ARB
+#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091
+#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092
+#define GLX_CONTEXT_FLAGS_ARB 0x2094
+#define GLX_CONTEXT_PROFILE_MASK_ARB 0x9126
+#ifndef __APPLE__
+// These are respectively 0x00000001 and 0x00000002 on OSX
+#define GLX_CONTEXT_DEBUG_BIT_ARB 0x0001
+#define GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB 0x0002
+#endif
+#define GLX_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001
+#define GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB 0x00000002
+#endif
+// GLX_EXT_create_context_es2_profile
+#ifndef GLX_CONTEXT_ES2_PROFILE_BIT_EXT
+#define GLX_CONTEXT_ES2_PROFILE_BIT_EXT 0x00000004
+#endif
+
+#include "osdep/timer.h"
+#include "video/out/present_sync.h"
+#include "video/out/x11_common.h"
+#include "context.h"
+#include "utils.h"
+
+struct priv {
+ GL gl;
+ XVisualInfo *vinfo;
+ GLXContext context;
+ GLXFBConfig fbc;
+};
+
+static void glx_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ra_gl_ctx_uninit(ctx);
+
+ if (p->vinfo)
+ XFree(p->vinfo);
+ if (p->context) {
+ Display *display = ctx->vo->x11->display;
+ glXMakeCurrent(display, None, NULL);
+ glXDestroyContext(display, p->context);
+ }
+
+ vo_x11_uninit(ctx->vo);
+}
+
+typedef GLXContext (*glXCreateContextAttribsARBProc)
+ (Display*, GLXFBConfig, GLXContext, Bool, const int*);
+
+static bool create_context_x11(struct ra_ctx *ctx, GL *gl, bool es)
+{
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+
+ glXCreateContextAttribsARBProc glXCreateContextAttribsARB =
+ (glXCreateContextAttribsARBProc)
+ glXGetProcAddressARB((const GLubyte *)"glXCreateContextAttribsARB");
+
+ const char *glxstr =
+ glXQueryExtensionsString(vo->x11->display, vo->x11->screen);
+ if (!glxstr) {
+ MP_ERR(ctx, "GLX did not advertise any extensions\n");
+ return false;
+ }
+
+ if (!gl_check_extension(glxstr, "GLX_ARB_create_context_profile") ||
+ !glXCreateContextAttribsARB) {
+ MP_ERR(ctx, "GLX does not support GLX_ARB_create_context_profile\n");
+ return false;
+ }
+
+ int ctx_flags = ctx->opts.debug ? GLX_CONTEXT_DEBUG_BIT_ARB : 0;
+ int profile_mask = GLX_CONTEXT_CORE_PROFILE_BIT_ARB;
+
+ if (es) {
+ profile_mask = GLX_CONTEXT_ES2_PROFILE_BIT_EXT;
+ if (!gl_check_extension(glxstr, "GLX_EXT_create_context_es2_profile"))
+ return false;
+ }
+
+ int context_attribs[] = {
+ GLX_CONTEXT_MAJOR_VERSION_ARB, 0,
+ GLX_CONTEXT_MINOR_VERSION_ARB, 0,
+ GLX_CONTEXT_PROFILE_MASK_ARB, profile_mask,
+ GLX_CONTEXT_FLAGS_ARB, ctx_flags,
+ None
+ };
+
+ GLXContext context;
+
+ if (!es) {
+ for (int n = 0; mpgl_min_required_gl_versions[n]; n++) {
+ int version = mpgl_min_required_gl_versions[n];
+ MP_VERBOSE(ctx, "Creating OpenGL %d.%d context...\n",
+ MPGL_VER_P(version));
+
+ context_attribs[1] = MPGL_VER_GET_MAJOR(version);
+ context_attribs[3] = MPGL_VER_GET_MINOR(version);
+
+ vo_x11_silence_xlib(1);
+ context = glXCreateContextAttribsARB(vo->x11->display,
+ p->fbc, 0, True,
+ context_attribs);
+ vo_x11_silence_xlib(-1);
+
+ if (context)
+ break;
+ }
+ } else {
+ context_attribs[1] = 2;
+
+ vo_x11_silence_xlib(1);
+ context = glXCreateContextAttribsARB(vo->x11->display,
+ p->fbc, 0, True,
+ context_attribs);
+ vo_x11_silence_xlib(-1);
+ }
+
+ if (!context)
+ return false;
+
+ // set context
+ if (!glXMakeCurrent(vo->x11->display, vo->x11->window, context)) {
+ MP_FATAL(vo, "Could not set GLX context!\n");
+ glXDestroyContext(vo->x11->display, context);
+ return false;
+ }
+
+ p->context = context;
+
+ mpgl_load_functions(gl, (void *)glXGetProcAddressARB, glxstr, vo->log);
+ return true;
+}
+
+// The GL3/FBC initialization code roughly follows/copies from:
+// http://www.opengl.org/wiki/Tutorial:_OpenGL_3.0_Context_Creation_(GLX)
+// but also uses some of the old code.
+
+static GLXFBConfig select_fb_config(struct vo *vo, const int *attribs, bool alpha)
+{
+ int fbcount;
+ GLXFBConfig *fbc = glXChooseFBConfig(vo->x11->display, vo->x11->screen,
+ attribs, &fbcount);
+ if (!fbc)
+ return NULL;
+
+ // The list in fbc is sorted (so that the first element is the best).
+ GLXFBConfig fbconfig = fbcount > 0 ? fbc[0] : NULL;
+
+ if (alpha) {
+ for (int n = 0; n < fbcount; n++) {
+ XVisualInfo *v = glXGetVisualFromFBConfig(vo->x11->display, fbc[n]);
+ if (v) {
+ bool is_rgba = vo_x11_is_rgba_visual(v);
+ XFree(v);
+ if (is_rgba) {
+ fbconfig = fbc[n];
+ break;
+ }
+ }
+ }
+ }
+
+ XFree(fbc);
+
+ return fbconfig;
+}
+
+static void set_glx_attrib(int *attribs, int name, int value)
+{
+ for (int n = 0; attribs[n * 2 + 0] != None; n++) {
+ if (attribs[n * 2 + 0] == name) {
+ attribs[n * 2 + 1] = value;
+ break;
+ }
+ }
+}
+
+static bool glx_check_visible(struct ra_ctx *ctx)
+{
+ return vo_x11_check_visible(ctx->vo);
+}
+
+static void glx_swap_buffers(struct ra_ctx *ctx)
+{
+ glXSwapBuffers(ctx->vo->x11->display, ctx->vo->x11->window);
+ if (ctx->vo->x11->use_present)
+ present_sync_swap(ctx->vo->x11->present);
+}
+
+static void glx_get_vsync(struct ra_ctx *ctx, struct vo_vsync_info *info)
+{
+ struct vo_x11_state *x11 = ctx->vo->x11;
+ if (ctx->vo->x11->use_present)
+ present_sync_get_info(x11->present, info);
+}
+
+static bool glx_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct vo *vo = ctx->vo;
+ GL *gl = &p->gl;
+
+ if (!vo_x11_init(ctx->vo))
+ goto uninit;
+
+ int glx_major, glx_minor;
+
+ if (!glXQueryVersion(vo->x11->display, &glx_major, &glx_minor)) {
+ MP_ERR(ctx, "GLX not found.\n");
+ goto uninit;
+ }
+ // FBConfigs were added in GLX version 1.3.
+ if (MPGL_VER(glx_major, glx_minor) < MPGL_VER(1, 3)) {
+ MP_ERR(ctx, "GLX version older than 1.3.\n");
+ goto uninit;
+ }
+
+ int glx_attribs[] = {
+ GLX_X_RENDERABLE, True,
+ GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR,
+ GLX_RED_SIZE, 1,
+ GLX_GREEN_SIZE, 1,
+ GLX_BLUE_SIZE, 1,
+ GLX_ALPHA_SIZE, 0,
+ GLX_DOUBLEBUFFER, True,
+ None
+ };
+ GLXFBConfig fbc = NULL;
+ if (ctx->opts.want_alpha) {
+ set_glx_attrib(glx_attribs, GLX_ALPHA_SIZE, 1);
+ fbc = select_fb_config(vo, glx_attribs, true);
+ if (!fbc)
+ set_glx_attrib(glx_attribs, GLX_ALPHA_SIZE, 0);
+ }
+ if (!fbc)
+ fbc = select_fb_config(vo, glx_attribs, false);
+ if (!fbc) {
+ MP_ERR(ctx, "no GLX support present\n");
+ goto uninit;
+ }
+
+ int fbid = -1;
+ if (!glXGetFBConfigAttrib(vo->x11->display, fbc, GLX_FBCONFIG_ID, &fbid))
+ MP_VERBOSE(ctx, "GLX chose FB config with ID 0x%x\n", fbid);
+
+ p->fbc = fbc;
+ p->vinfo = glXGetVisualFromFBConfig(vo->x11->display, fbc);
+ if (p->vinfo) {
+ MP_VERBOSE(ctx, "GLX chose visual with ID 0x%x\n",
+ (int)p->vinfo->visualid);
+ } else {
+ MP_WARN(ctx, "Selected GLX FB config has no associated X visual\n");
+ }
+
+ if (!vo_x11_create_vo_window(vo, p->vinfo, "gl"))
+ goto uninit;
+
+ bool success = false;
+ enum gles_mode mode = ra_gl_ctx_get_glesmode(ctx);
+
+ if (mode == GLES_NO || mode == GLES_AUTO)
+ success = create_context_x11(ctx, gl, false);
+ if (!success && (mode == GLES_YES || mode == GLES_AUTO))
+ success = create_context_x11(ctx, gl, true);
+ if (success && !glXIsDirect(vo->x11->display, p->context))
+ gl->mpgl_caps |= MPGL_CAP_SW;
+ if (!success)
+ goto uninit;
+
+ struct ra_gl_ctx_params params = {
+ .check_visible = glx_check_visible,
+ .swap_buffers = glx_swap_buffers,
+ .get_vsync = glx_get_vsync,
+ };
+
+ if (!ra_gl_ctx_init(ctx, gl, params))
+ goto uninit;
+
+ ra_add_native_resource(ctx->ra, "x11", vo->x11->display);
+
+ return true;
+
+uninit:
+ glx_uninit(ctx);
+ return false;
+}
+
+
+static void resize(struct ra_ctx *ctx)
+{
+ ra_gl_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight, 0);
+}
+
+static bool glx_reconfig(struct ra_ctx *ctx)
+{
+ vo_x11_config_vo_window(ctx->vo);
+ resize(ctx);
+ return true;
+}
+
+static int glx_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ int ret = vo_x11_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE)
+ resize(ctx);
+ return ret;
+}
+
+static void glx_wakeup(struct ra_ctx *ctx)
+{
+ vo_x11_wakeup(ctx->vo);
+}
+
+static void glx_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ vo_x11_wait_events(ctx->vo, until_time_ns);
+}
+
+const struct ra_ctx_fns ra_ctx_glx = {
+ .type = "opengl",
+ .name = "x11",
+ .reconfig = glx_reconfig,
+ .control = glx_control,
+ .wakeup = glx_wakeup,
+ .wait_events = glx_wait_events,
+ .init = glx_init,
+ .uninit = glx_uninit,
+};
diff --git a/video/out/opengl/context_rpi.c b/video/out/opengl/context_rpi.c
new file mode 100644
index 0000000..0b6babb
--- /dev/null
+++ b/video/out/opengl/context_rpi.c
@@ -0,0 +1,327 @@
+/*
+ * 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 <assert.h>
+#include <stdatomic.h>
+#include <stddef.h>
+
+#include <bcm_host.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "common/common.h"
+#include "video/out/win_state.h"
+#include "context.h"
+#include "egl_helpers.h"
+
+struct priv {
+ struct GL gl;
+ DISPMANX_DISPLAY_HANDLE_T display;
+ DISPMANX_ELEMENT_HANDLE_T window;
+ DISPMANX_UPDATE_HANDLE_T update;
+ EGLDisplay egl_display;
+ EGLConfig egl_config;
+ EGLContext egl_context;
+ EGLSurface egl_surface;
+ // yep, the API keeps a pointer to it
+ EGL_DISPMANX_WINDOW_T egl_window;
+ int x, y, w, h;
+ double display_fps;
+ atomic_int reload_display;
+ int win_params[4];
+};
+
+static void tv_callback(void *callback_data, uint32_t reason, uint32_t param1,
+ uint32_t param2)
+{
+ struct ra_ctx *ctx = callback_data;
+ struct priv *p = ctx->priv;
+ atomic_store(&p->reload_display, true);
+ vo_wakeup(ctx->vo);
+}
+
+static void destroy_dispmanx(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->egl_surface) {
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ eglDestroySurface(p->egl_display, p->egl_surface);
+ p->egl_surface = EGL_NO_SURFACE;
+ }
+
+ if (p->window)
+ vc_dispmanx_element_remove(p->update, p->window);
+ p->window = 0;
+ if (p->display)
+ vc_dispmanx_display_close(p->display);
+ p->display = 0;
+ if (p->update)
+ vc_dispmanx_update_submit_sync(p->update);
+ p->update = 0;
+}
+
+static void rpi_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ra_gl_ctx_uninit(ctx);
+
+ vc_tv_unregister_callback_full(tv_callback, ctx);
+
+ destroy_dispmanx(ctx);
+
+ if (p->egl_context)
+ eglDestroyContext(p->egl_display, p->egl_context);
+ p->egl_context = EGL_NO_CONTEXT;
+ eglReleaseThread();
+ p->egl_display = EGL_NO_DISPLAY;
+}
+
+static bool recreate_dispmanx(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ int display_nr = 0;
+ int layer = 0;
+
+ MP_VERBOSE(ctx, "Recreating DISPMANX state...\n");
+
+ destroy_dispmanx(ctx);
+
+ p->display = vc_dispmanx_display_open(display_nr);
+ p->update = vc_dispmanx_update_start(0);
+ if (!p->display || !p->update) {
+ MP_FATAL(ctx, "Could not get DISPMANX objects.\n");
+ goto fail;
+ }
+
+ uint32_t dispw, disph;
+ if (graphics_get_display_size(0, &dispw, &disph) < 0) {
+ MP_FATAL(ctx, "Could not get display size.\n");
+ goto fail;
+ }
+ p->w = dispw;
+ p->h = disph;
+
+ if (ctx->vo->opts->fullscreen) {
+ p->x = p->y = 0;
+ } else {
+ struct vo_win_geometry geo;
+ struct mp_rect screenrc = {0, 0, p->w, p->h};
+
+ vo_calc_window_geometry(ctx->vo, &screenrc, &geo);
+
+ mp_rect_intersection(&geo.win, &screenrc);
+
+ p->x = geo.win.x0;
+ p->y = geo.win.y0;
+ p->w = geo.win.x1 - geo.win.x0;
+ p->h = geo.win.y1 - geo.win.y0;
+ }
+
+ // dispmanx is like a neanderthal version of Wayland - you can add an
+ // overlay any place on the screen.
+ VC_RECT_T dst = {.x = p->x, .y = p->y, .width = p->w, .height = p->h};
+ VC_RECT_T src = {.width = p->w << 16, .height = p->h << 16};
+ VC_DISPMANX_ALPHA_T alpha = {
+ .flags = DISPMANX_FLAGS_ALPHA_FIXED_ALL_PIXELS,
+ .opacity = 0xFF,
+ };
+ p->window = vc_dispmanx_element_add(p->update, p->display, layer, &dst, 0,
+ &src, DISPMANX_PROTECTION_NONE, &alpha,
+ 0, 0);
+ if (!p->window) {
+ MP_FATAL(ctx, "Could not add DISPMANX element.\n");
+ goto fail;
+ }
+
+ vc_dispmanx_update_submit_sync(p->update);
+ p->update = vc_dispmanx_update_start(0);
+
+ p->egl_window = (EGL_DISPMANX_WINDOW_T){
+ .element = p->window,
+ .width = p->w,
+ .height = p->h,
+ };
+ p->egl_surface = eglCreateWindowSurface(p->egl_display, p->egl_config,
+ &p->egl_window, NULL);
+
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ MP_FATAL(ctx, "Could not create EGL surface!\n");
+ goto fail;
+ }
+
+ if (!eglMakeCurrent(p->egl_display, p->egl_surface, p->egl_surface,
+ p->egl_context))
+ {
+ MP_FATAL(ctx, "Failed to set context!\n");
+ goto fail;
+ }
+
+ p->display_fps = 0;
+ TV_GET_STATE_RESP_T tvstate;
+ TV_DISPLAY_STATE_T tvstate_disp;
+ if (!vc_tv_get_state(&tvstate) && !vc_tv_get_display_state(&tvstate_disp)) {
+ if (tvstate_disp.state & (VC_HDMI_HDMI | VC_HDMI_DVI)) {
+ p->display_fps = tvstate_disp.display.hdmi.frame_rate;
+
+ HDMI_PROPERTY_PARAM_T param = {
+ .property = HDMI_PROPERTY_PIXEL_CLOCK_TYPE,
+ };
+ if (!vc_tv_hdmi_get_property(&param) &&
+ param.param1 == HDMI_PIXEL_CLOCK_TYPE_NTSC)
+ p->display_fps = p->display_fps / 1.001;
+ } else {
+ p->display_fps = tvstate_disp.display.sdtv.frame_rate;
+ }
+ }
+
+ p->win_params[0] = display_nr;
+ p->win_params[1] = layer;
+ p->win_params[2] = p->x;
+ p->win_params[3] = p->y;
+
+ ctx->vo->dwidth = p->w;
+ ctx->vo->dheight = p->h;
+ if (ctx->swapchain)
+ ra_gl_ctx_resize(ctx->swapchain, p->w, p->h, 0);
+
+ ctx->vo->want_redraw = true;
+
+ vo_event(ctx->vo, VO_EVENT_WIN_STATE);
+ return true;
+
+fail:
+ destroy_dispmanx(ctx);
+ return false;
+}
+
+static void rpi_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ eglSwapBuffers(p->egl_display, p->egl_surface);
+}
+
+static bool rpi_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+
+ bcm_host_init();
+
+ vc_tv_register_callback(tv_callback, ctx);
+
+ p->egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ if (!eglInitialize(p->egl_display, NULL, NULL)) {
+ MP_FATAL(ctx, "EGL failed to initialize.\n");
+ goto fail;
+ }
+
+ if (!mpegl_create_context(ctx, p->egl_display, &p->egl_context, &p->egl_config))
+ goto fail;
+
+ if (!recreate_dispmanx(ctx))
+ goto fail;
+
+ mpegl_load_functions(&p->gl, ctx->log);
+
+ struct ra_gl_ctx_params params = {
+ .swap_buffers = rpi_swap_buffers,
+ };
+
+ if (!ra_gl_ctx_init(ctx, &p->gl, params))
+ goto fail;
+
+ ra_add_native_resource(ctx->ra, "MPV_RPI_WINDOW", p->win_params);
+
+ ra_gl_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight, 0);
+ return true;
+
+fail:
+ rpi_uninit(ctx);
+ return false;
+}
+
+static bool rpi_reconfig(struct ra_ctx *ctx)
+{
+ return recreate_dispmanx(ctx);
+}
+
+static struct mp_image *take_screenshot(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (!p->display)
+ return NULL;
+
+ struct mp_image *img = mp_image_alloc(IMGFMT_BGR0, p->w, p->h);
+ if (!img)
+ return NULL;
+
+ DISPMANX_RESOURCE_HANDLE_T resource =
+ vc_dispmanx_resource_create(VC_IMAGE_ARGB8888,
+ img->w | ((img->w * 4) << 16), img->h,
+ &(int32_t){0});
+ if (!resource)
+ goto fail;
+
+ if (vc_dispmanx_snapshot(p->display, resource, 0))
+ goto fail;
+
+ VC_RECT_T rc = {.width = img->w, .height = img->h};
+ if (vc_dispmanx_resource_read_data(resource, &rc, img->planes[0], img->stride[0]))
+ goto fail;
+
+ vc_dispmanx_resource_delete(resource);
+ return img;
+
+fail:
+ vc_dispmanx_resource_delete(resource);
+ talloc_free(img);
+ return NULL;
+}
+
+static int rpi_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ struct priv *p = ctx->priv;
+
+ switch (request) {
+ case VOCTRL_SCREENSHOT_WIN:
+ *(struct mp_image **)arg = take_screenshot(ctx);
+ return VO_TRUE;
+ case VOCTRL_CHECK_EVENTS:
+ if (atomic_fetch_and(&p->reload_display, 0)) {
+ MP_WARN(ctx, "Recovering from display mode switch...\n");
+ recreate_dispmanx(ctx);
+ }
+ return VO_TRUE;
+ case VOCTRL_GET_DISPLAY_FPS:
+ *(double *)arg = p->display_fps;
+ return VO_TRUE;
+ }
+
+ return VO_NOTIMPL;
+}
+
+const struct ra_ctx_fns ra_ctx_rpi = {
+ .type = "opengl",
+ .name = "rpi",
+ .reconfig = rpi_reconfig,
+ .control = rpi_control,
+ .init = rpi_init,
+ .uninit = rpi_uninit,
+};
diff --git a/video/out/opengl/context_wayland.c b/video/out/opengl/context_wayland.c
new file mode 100644
index 0000000..26c5268
--- /dev/null
+++ b/video/out/opengl/context_wayland.c
@@ -0,0 +1,230 @@
+/*
+ * This file is part of mpv video player.
+ * Copyright © 2013 Alexander Preisinger <alexander.preisinger@gmail.com>
+ *
+ * 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 <wayland-egl.h>
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "video/out/present_sync.h"
+#include "video/out/wayland_common.h"
+#include "context.h"
+#include "egl_helpers.h"
+#include "utils.h"
+
+#define EGL_PLATFORM_WAYLAND_EXT 0x31D8
+
+struct priv {
+ GL gl;
+ EGLDisplay egl_display;
+ EGLContext egl_context;
+ EGLSurface egl_surface;
+ EGLConfig egl_config;
+ struct wl_egl_window *egl_window;
+};
+
+static void resize(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo_wayland_state *wl = ctx->vo->wl;
+
+ MP_VERBOSE(wl, "Handling resize on the egl side\n");
+
+ const int32_t width = mp_rect_w(wl->geometry);
+ const int32_t height = mp_rect_h(wl->geometry);
+
+ vo_wayland_set_opaque_region(wl, ctx->opts.want_alpha);
+ if (p->egl_window)
+ wl_egl_window_resize(p->egl_window, width, height, 0, 0);
+
+ wl->vo->dwidth = width;
+ wl->vo->dheight = height;
+
+ vo_wayland_handle_fractional_scale(wl);
+}
+
+static bool wayland_egl_check_visible(struct ra_ctx *ctx)
+{
+ return vo_wayland_check_visible(ctx->vo);
+}
+
+static void wayland_egl_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo_wayland_state *wl = ctx->vo->wl;
+
+ eglSwapBuffers(p->egl_display, p->egl_surface);
+
+ if (!wl->opts->disable_vsync)
+ vo_wayland_wait_frame(wl);
+
+ if (wl->use_present)
+ present_sync_swap(wl->present);
+}
+
+static void wayland_egl_get_vsync(struct ra_ctx *ctx, struct vo_vsync_info *info)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+ if (wl->use_present)
+ present_sync_get_info(wl->present, info);
+}
+
+static bool egl_create_context(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct vo_wayland_state *wl = ctx->vo->wl;
+
+ if (!(p->egl_display = mpegl_get_display(EGL_PLATFORM_WAYLAND_EXT,
+ "EGL_EXT_platform_wayland",
+ wl->display)))
+ return false;
+
+ if (eglInitialize(p->egl_display, NULL, NULL) != EGL_TRUE)
+ return false;
+
+ if (!mpegl_create_context(ctx, p->egl_display, &p->egl_context,
+ &p->egl_config))
+ return false;
+
+ eglMakeCurrent(p->egl_display, NULL, NULL, p->egl_context);
+
+ mpegl_load_functions(&p->gl, wl->log);
+
+ struct ra_gl_ctx_params params = {
+ .check_visible = wayland_egl_check_visible,
+ .swap_buffers = wayland_egl_swap_buffers,
+ .get_vsync = wayland_egl_get_vsync,
+ };
+
+ if (!ra_gl_ctx_init(ctx, &p->gl, params))
+ return false;
+
+ ra_add_native_resource(ctx->ra, "wl", wl->display);
+
+ return true;
+}
+
+static void egl_create_window(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ struct vo_wayland_state *wl = ctx->vo->wl;
+
+ p->egl_window = wl_egl_window_create(wl->surface,
+ mp_rect_w(wl->geometry),
+ mp_rect_h(wl->geometry));
+
+ p->egl_surface = mpegl_create_window_surface(
+ p->egl_display, p->egl_config, p->egl_window);
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ p->egl_surface = eglCreateWindowSurface(
+ p->egl_display, p->egl_config, p->egl_window, NULL);
+ }
+
+ eglMakeCurrent(p->egl_display, p->egl_surface, p->egl_surface, p->egl_context);
+ // eglMakeCurrent may not configure the draw or read buffers if the context
+ // has been made current previously. On nvidia GL_NONE is bound because EGL_NO_SURFACE
+ // is used initially and we must bind the read and draw buffers here.
+ if(!p->gl.es) {
+ p->gl.ReadBuffer(GL_BACK);
+ p->gl.DrawBuffer(GL_BACK);
+ }
+
+ eglSwapInterval(p->egl_display, 0);
+}
+
+static bool wayland_egl_reconfig(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (!vo_wayland_reconfig(ctx->vo))
+ return false;
+
+ if (!p->egl_window)
+ egl_create_window(ctx);
+
+ return true;
+}
+
+static void wayland_egl_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_gl_ctx_uninit(ctx);
+
+ if (p->egl_context) {
+ eglReleaseThread();
+ if (p->egl_window)
+ wl_egl_window_destroy(p->egl_window);
+ eglDestroySurface(p->egl_display, p->egl_surface);
+ eglMakeCurrent(p->egl_display, NULL, NULL, EGL_NO_CONTEXT);
+ eglDestroyContext(p->egl_display, p->egl_context);
+ p->egl_context = NULL;
+ }
+ eglTerminate(p->egl_display);
+
+ vo_wayland_uninit(ctx->vo);
+}
+
+static int wayland_egl_control(struct ra_ctx *ctx, int *events, int request,
+ void *data)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+ int r = vo_wayland_control(ctx->vo, events, request, data);
+
+ if (*events & VO_EVENT_RESIZE) {
+ resize(ctx);
+ ra_gl_ctx_resize(ctx->swapchain, wl->vo->dwidth, wl->vo->dheight, 0);
+ }
+
+ return r;
+}
+
+static void wayland_egl_wakeup(struct ra_ctx *ctx)
+{
+ vo_wayland_wakeup(ctx->vo);
+}
+
+static void wayland_egl_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ vo_wayland_wait_events(ctx->vo, until_time_ns);
+}
+
+static void wayland_egl_update_render_opts(struct ra_ctx *ctx)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+ vo_wayland_set_opaque_region(wl, ctx->opts.want_alpha);
+ wl_surface_commit(wl->surface);
+}
+
+static bool wayland_egl_init(struct ra_ctx *ctx)
+{
+ if (!vo_wayland_init(ctx->vo))
+ return false;
+ return egl_create_context(ctx);
+}
+
+const struct ra_ctx_fns ra_ctx_wayland_egl = {
+ .type = "opengl",
+ .name = "wayland",
+ .reconfig = wayland_egl_reconfig,
+ .control = wayland_egl_control,
+ .wakeup = wayland_egl_wakeup,
+ .wait_events = wayland_egl_wait_events,
+ .update_render_opts = wayland_egl_update_render_opts,
+ .init = wayland_egl_init,
+ .uninit = wayland_egl_uninit,
+};
diff --git a/video/out/opengl/context_win.c b/video/out/opengl/context_win.c
new file mode 100644
index 0000000..968b176
--- /dev/null
+++ b/video/out/opengl/context_win.c
@@ -0,0 +1,378 @@
+/*
+ * 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 <assert.h>
+#include <windows.h>
+#include <dwmapi.h>
+
+#include "options/m_config.h"
+#include "video/out/w32_common.h"
+#include "context.h"
+#include "utils.h"
+
+#if !defined(WGL_CONTEXT_MAJOR_VERSION_ARB)
+/* these are supposed to be defined in wingdi.h but mingw's is too old */
+/* only the bits actually used by mplayer are defined */
+/* reference: http://www.opengl.org/registry/specs/ARB/wgl_create_context.txt */
+
+#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091
+#define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092
+#define WGL_CONTEXT_FLAGS_ARB 0x2094
+#define WGL_CONTEXT_PROFILE_MASK_ARB 0x9126
+#define WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB 0x0002
+#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001
+#endif
+
+struct wingl_opts {
+ int wingl_dwm_flush;
+};
+
+#define OPT_BASE_STRUCT struct wingl_opts
+const struct m_sub_options wingl_conf = {
+ .opts = (const struct m_option[]) {
+ {"opengl-dwmflush", OPT_CHOICE(wingl_dwm_flush,
+ {"no", -1}, {"auto", 0}, {"windowed", 1}, {"yes", 2})},
+ {0}
+ },
+ .size = sizeof(struct wingl_opts),
+};
+
+struct priv {
+ GL gl;
+
+ int opt_swapinterval;
+ int current_swapinterval;
+
+ int (GLAPIENTRY *real_wglSwapInterval)(int);
+ struct m_config_cache *opts_cache;
+ struct wingl_opts *opts;
+
+ HGLRC context;
+ HDC hdc;
+};
+
+static void wgl_uninit(struct ra_ctx *ctx);
+
+static __thread struct priv *current_wgl_context;
+
+static int GLAPIENTRY wgl_swap_interval(int interval)
+{
+ if (current_wgl_context)
+ current_wgl_context->opt_swapinterval = interval;
+ return 0;
+}
+
+static bool create_dc(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ HWND win = vo_w32_hwnd(ctx->vo);
+
+ if (p->hdc)
+ return true;
+
+ HDC hdc = GetDC(win);
+ if (!hdc)
+ return false;
+
+ PIXELFORMATDESCRIPTOR pfd;
+ memset(&pfd, 0, sizeof pfd);
+ pfd.nSize = sizeof pfd;
+ pfd.nVersion = 1;
+ pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
+
+ pfd.iPixelType = PFD_TYPE_RGBA;
+ pfd.cColorBits = 24;
+ pfd.iLayerType = PFD_MAIN_PLANE;
+ int pf = ChoosePixelFormat(hdc, &pfd);
+
+ if (!pf) {
+ MP_ERR(ctx->vo, "unable to select a valid pixel format!\n");
+ ReleaseDC(win, hdc);
+ return false;
+ }
+
+ SetPixelFormat(hdc, pf, &pfd);
+
+ p->hdc = hdc;
+ return true;
+}
+
+static void *wglgpa(const GLubyte *procName)
+{
+ HMODULE oglmod;
+ void *res = wglGetProcAddress(procName);
+ if (res)
+ return res;
+ oglmod = GetModuleHandle(L"opengl32.dll");
+ return GetProcAddress(oglmod, procName);
+}
+
+static bool create_context_wgl_old(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ HDC windc = p->hdc;
+ bool res = false;
+
+ HGLRC context = wglCreateContext(windc);
+ if (!context) {
+ MP_FATAL(ctx->vo, "Could not create GL context!\n");
+ return res;
+ }
+
+ if (!wglMakeCurrent(windc, context)) {
+ MP_FATAL(ctx->vo, "Could not set GL context!\n");
+ wglDeleteContext(context);
+ return res;
+ }
+
+ p->context = context;
+ return true;
+}
+
+static bool create_context_wgl_gl3(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ HDC windc = p->hdc;
+ HGLRC context = 0;
+
+ // A legacy context is needed to get access to the new functions.
+ HGLRC legacy_context = wglCreateContext(windc);
+ if (!legacy_context) {
+ MP_FATAL(ctx->vo, "Could not create GL context!\n");
+ return false;
+ }
+
+ // set context
+ if (!wglMakeCurrent(windc, legacy_context)) {
+ MP_FATAL(ctx->vo, "Could not set GL context!\n");
+ goto out;
+ }
+
+ const char *(GLAPIENTRY *wglGetExtensionsStringARB)(HDC hdc)
+ = wglgpa((const GLubyte*)"wglGetExtensionsStringARB");
+
+ if (!wglGetExtensionsStringARB)
+ goto unsupported;
+
+ const char *wgl_exts = wglGetExtensionsStringARB(windc);
+ if (!gl_check_extension(wgl_exts, "WGL_ARB_create_context"))
+ goto unsupported;
+
+ HGLRC (GLAPIENTRY *wglCreateContextAttribsARB)(HDC hDC, HGLRC hShareContext,
+ const int *attribList)
+ = wglgpa((const GLubyte*)"wglCreateContextAttribsARB");
+
+ if (!wglCreateContextAttribsARB)
+ goto unsupported;
+
+ int attribs[] = {
+ WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
+ WGL_CONTEXT_MINOR_VERSION_ARB, 0,
+ WGL_CONTEXT_FLAGS_ARB, 0,
+ WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
+ 0
+ };
+
+ context = wglCreateContextAttribsARB(windc, 0, attribs);
+ if (!context) {
+ // NVidia, instead of ignoring WGL_CONTEXT_FLAGS_ARB, will error out if
+ // it's present on pre-3.2 contexts.
+ // Remove it from attribs and retry the context creation.
+ attribs[6] = attribs[7] = 0;
+ context = wglCreateContextAttribsARB(windc, 0, attribs);
+ }
+ if (!context) {
+ int err = GetLastError();
+ MP_FATAL(ctx->vo, "Could not create an OpenGL 3.x context: error 0x%x\n", err);
+ goto out;
+ }
+
+ wglMakeCurrent(windc, NULL);
+ wglDeleteContext(legacy_context);
+
+ if (!wglMakeCurrent(windc, context)) {
+ MP_FATAL(ctx->vo, "Could not set GL3 context!\n");
+ wglDeleteContext(context);
+ return false;
+ }
+
+ p->context = context;
+ return true;
+
+unsupported:
+ MP_ERR(ctx->vo, "The OpenGL driver does not support OpenGL 3.x \n");
+out:
+ wglMakeCurrent(windc, NULL);
+ wglDeleteContext(legacy_context);
+ return false;
+}
+
+static void create_ctx(void *ptr)
+{
+ struct ra_ctx *ctx = ptr;
+ struct priv *p = ctx->priv;
+
+ if (!create_dc(ctx))
+ return;
+
+ create_context_wgl_gl3(ctx);
+ if (!p->context)
+ create_context_wgl_old(ctx);
+
+ wglMakeCurrent(p->hdc, NULL);
+}
+
+static bool compositor_active(struct ra_ctx *ctx)
+{
+ // For Windows 7.
+ BOOL enabled = 0;
+ if (FAILED(DwmIsCompositionEnabled(&enabled)) || !enabled)
+ return false;
+
+ // This works at least on Windows 8.1: it returns an error in fullscreen,
+ // which is also when we get consistent timings without DwmFlush. Might
+ // be cargo-cult.
+ DWM_TIMING_INFO info = { .cbSize = sizeof(DWM_TIMING_INFO) };
+ if (FAILED(DwmGetCompositionTimingInfo(0, &info)))
+ return false;
+
+ return true;
+}
+
+static void wgl_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ SwapBuffers(p->hdc);
+
+ // default if we don't DwmFLush
+ int new_swapinterval = p->opt_swapinterval;
+
+ if (p->opts->wingl_dwm_flush >= 0) {
+ if ((p->opts->wingl_dwm_flush == 1 && !ctx->vo->opts->fullscreen) ||
+ (p->opts->wingl_dwm_flush == 2) ||
+ (p->opts->wingl_dwm_flush == 0 && compositor_active(ctx)))
+ {
+ if (DwmFlush() == S_OK)
+ new_swapinterval = 0;
+ }
+ }
+
+ if (new_swapinterval != p->current_swapinterval &&
+ p->real_wglSwapInterval)
+ {
+ p->real_wglSwapInterval(new_swapinterval);
+ MP_VERBOSE(ctx->vo, "set SwapInterval(%d)\n", new_swapinterval);
+ }
+ p->current_swapinterval = new_swapinterval;
+}
+
+static bool wgl_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ GL *gl = &p->gl;
+
+ p->opts_cache = m_config_cache_alloc(ctx, ctx->global, &wingl_conf);
+ p->opts = p->opts_cache->opts;
+
+ if (!vo_w32_init(ctx->vo))
+ goto fail;
+
+ vo_w32_run_on_thread(ctx->vo, create_ctx, ctx);
+ if (!p->context)
+ goto fail;
+
+ current_wgl_context = p;
+ wglMakeCurrent(p->hdc, p->context);
+
+ mpgl_load_functions(gl, wglgpa, NULL, ctx->vo->log);
+
+ if (!gl->SwapInterval)
+ MP_VERBOSE(ctx->vo, "WGL_EXT_swap_control missing.\n");
+ p->real_wglSwapInterval = gl->SwapInterval;
+ gl->SwapInterval = wgl_swap_interval;
+ p->current_swapinterval = -1;
+
+ struct ra_gl_ctx_params params = {
+ .swap_buffers = wgl_swap_buffers,
+ };
+
+ if (!ra_gl_ctx_init(ctx, gl, params))
+ goto fail;
+
+ DwmEnableMMCSS(TRUE);
+ return true;
+
+fail:
+ wgl_uninit(ctx);
+ return false;
+}
+
+static void resize(struct ra_ctx *ctx)
+{
+ ra_gl_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight, 0);
+}
+
+static bool wgl_reconfig(struct ra_ctx *ctx)
+{
+ vo_w32_config(ctx->vo);
+ resize(ctx);
+ return true;
+}
+
+static void destroy_gl(void *ptr)
+{
+ struct ra_ctx *ctx = ptr;
+ struct priv *p = ctx->priv;
+ if (p->context)
+ wglDeleteContext(p->context);
+ p->context = 0;
+ if (p->hdc)
+ ReleaseDC(vo_w32_hwnd(ctx->vo), p->hdc);
+ p->hdc = NULL;
+ current_wgl_context = NULL;
+}
+
+static void wgl_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ra_gl_ctx_uninit(ctx);
+ if (p->context)
+ wglMakeCurrent(p->hdc, 0);
+ vo_w32_run_on_thread(ctx->vo, destroy_gl, ctx);
+
+ DwmEnableMMCSS(FALSE);
+ vo_w32_uninit(ctx->vo);
+}
+
+static int wgl_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ int ret = vo_w32_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE)
+ resize(ctx);
+ return ret;
+}
+
+const struct ra_ctx_fns ra_ctx_wgl = {
+ .type = "opengl",
+ .name = "win",
+ .init = wgl_init,
+ .reconfig = wgl_reconfig,
+ .control = wgl_control,
+ .uninit = wgl_uninit,
+};
diff --git a/video/out/opengl/context_x11egl.c b/video/out/opengl/context_x11egl.c
new file mode 100644
index 0000000..3201f29
--- /dev/null
+++ b/video/out/opengl/context_x11egl.c
@@ -0,0 +1,225 @@
+/*
+ * 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 <assert.h>
+
+#include <X11/Xlib.h>
+#include <X11/extensions/Xpresent.h>
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "common/common.h"
+#include "video/out/present_sync.h"
+#include "video/out/x11_common.h"
+#include "context.h"
+#include "egl_helpers.h"
+#include "utils.h"
+
+#define EGL_PLATFORM_X11_EXT 0x31D5
+
+struct priv {
+ GL gl;
+ EGLDisplay egl_display;
+ EGLContext egl_context;
+ EGLSurface egl_surface;
+};
+
+static void mpegl_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ra_gl_ctx_uninit(ctx);
+
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ eglTerminate(p->egl_display);
+ vo_x11_uninit(ctx->vo);
+}
+
+static int pick_xrgba_config(void *user_data, EGLConfig *configs, int num_configs)
+{
+ struct ra_ctx *ctx = user_data;
+ struct priv *p = ctx->priv;
+ struct vo *vo = ctx->vo;
+
+ for (int n = 0; n < num_configs; n++) {
+ int vID = 0, num;
+ eglGetConfigAttrib(p->egl_display, configs[n], EGL_NATIVE_VISUAL_ID, &vID);
+ XVisualInfo template = {.visualid = vID};
+ XVisualInfo *vi = XGetVisualInfo(vo->x11->display, VisualIDMask,
+ &template, &num);
+ if (vi) {
+ bool is_rgba = vo_x11_is_rgba_visual(vi);
+ XFree(vi);
+ if (is_rgba)
+ return n;
+ }
+ }
+
+ return 0;
+}
+
+static bool mpegl_check_visible(struct ra_ctx *ctx)
+{
+ return vo_x11_check_visible(ctx->vo);
+}
+
+static void mpegl_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ eglSwapBuffers(p->egl_display, p->egl_surface);
+ if (ctx->vo->x11->use_present)
+ present_sync_swap(ctx->vo->x11->present);
+}
+
+static void mpegl_get_vsync(struct ra_ctx *ctx, struct vo_vsync_info *info)
+{
+ struct vo_x11_state *x11 = ctx->vo->x11;
+ if (ctx->vo->x11->use_present)
+ present_sync_get_info(x11->present, info);
+}
+
+static bool mpegl_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct vo *vo = ctx->vo;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_FATAL;
+
+ if (!vo_x11_init(vo))
+ goto uninit;
+
+ p->egl_display = mpegl_get_display(EGL_PLATFORM_X11_EXT,
+ "EGL_EXT_platform_x11",
+ vo->x11->display);
+ if (!eglInitialize(p->egl_display, NULL, NULL)) {
+ MP_MSG(ctx, msgl, "Could not initialize EGL.\n");
+ goto uninit;
+ }
+
+ struct mpegl_cb cb = {
+ .user_data = ctx,
+ .refine_config = ctx->opts.want_alpha ? pick_xrgba_config : NULL,
+ };
+
+ EGLConfig config;
+ if (!mpegl_create_context_cb(ctx, p->egl_display, cb, &p->egl_context, &config))
+ goto uninit;
+
+ int cid, vID, n;
+ if (!eglGetConfigAttrib(p->egl_display, config, EGL_CONFIG_ID, &cid)) {
+ MP_FATAL(ctx, "Getting EGL_CONFIG_ID failed!\n");
+ goto uninit;
+ }
+ if (!eglGetConfigAttrib(p->egl_display, config, EGL_NATIVE_VISUAL_ID, &vID)) {
+ MP_FATAL(ctx, "Getting X visual ID failed!\n");
+ goto uninit;
+ }
+ MP_VERBOSE(ctx, "Choosing visual EGL config 0x%x, visual ID 0x%x\n", cid, vID);
+ XVisualInfo template = {.visualid = vID};
+ XVisualInfo *vi = XGetVisualInfo(vo->x11->display, VisualIDMask, &template, &n);
+
+ if (!vi) {
+ MP_FATAL(ctx, "Getting X visual failed!\n");
+ goto uninit;
+ }
+
+ if (!vo_x11_create_vo_window(vo, vi, "gl")) {
+ XFree(vi);
+ goto uninit;
+ }
+
+ XFree(vi);
+
+ p->egl_surface = mpegl_create_window_surface(
+ p->egl_display, config, &vo->x11->window);
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ p->egl_surface = eglCreateWindowSurface(
+ p->egl_display, config, (EGLNativeWindowType)vo->x11->window, NULL);
+ }
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ MP_FATAL(ctx, "Could not create EGL surface!\n");
+ goto uninit;
+ }
+
+ if (!eglMakeCurrent(p->egl_display, p->egl_surface, p->egl_surface,
+ p->egl_context))
+ {
+ MP_FATAL(ctx, "Could not make context current!\n");
+ goto uninit;
+ }
+
+ mpegl_load_functions(&p->gl, ctx->log);
+
+ struct ra_gl_ctx_params params = {
+ .check_visible = mpegl_check_visible,
+ .swap_buffers = mpegl_swap_buffers,
+ .get_vsync = mpegl_get_vsync,
+ };
+
+ if (!ra_gl_ctx_init(ctx, &p->gl, params))
+ goto uninit;
+
+ ra_add_native_resource(ctx->ra, "x11", vo->x11->display);
+
+ return true;
+
+uninit:
+ mpegl_uninit(ctx);
+ return false;
+}
+
+static void resize(struct ra_ctx *ctx)
+{
+ ra_gl_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight, 0);
+}
+
+static bool mpegl_reconfig(struct ra_ctx *ctx)
+{
+ vo_x11_config_vo_window(ctx->vo);
+ resize(ctx);
+ return true;
+}
+
+static int mpegl_control(struct ra_ctx *ctx, int *events, int request,
+ void *arg)
+{
+ int ret = vo_x11_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE)
+ resize(ctx);
+ return ret;
+}
+
+static void mpegl_wakeup(struct ra_ctx *ctx)
+{
+ vo_x11_wakeup(ctx->vo);
+}
+
+static void mpegl_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ vo_x11_wait_events(ctx->vo, until_time_ns);
+}
+
+const struct ra_ctx_fns ra_ctx_x11_egl = {
+ .type = "opengl",
+ .name = "x11egl",
+ .reconfig = mpegl_reconfig,
+ .control = mpegl_control,
+ .wakeup = mpegl_wakeup,
+ .wait_events = mpegl_wait_events,
+ .init = mpegl_init,
+ .uninit = mpegl_uninit,
+};
diff --git a/video/out/opengl/egl_helpers.c b/video/out/opengl/egl_helpers.c
new file mode 100644
index 0000000..3bf6239
--- /dev/null
+++ b/video/out/opengl/egl_helpers.c
@@ -0,0 +1,381 @@
+/*
+ * 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 "config.h"
+
+#if HAVE_LIBDL
+#include <dlfcn.h>
+#endif
+
+#include "common/common.h"
+
+#include "egl_helpers.h"
+#include "common.h"
+#include "utils.h"
+#include "context.h"
+
+#if HAVE_EGL_ANGLE
+// On Windows, egl_helpers.c is only used by ANGLE, where the EGL functions may
+// be loaded dynamically from ANGLE DLLs
+#include "angle_dynamic.h"
+#endif
+
+// EGL 1.5
+#ifndef EGL_CONTEXT_OPENGL_PROFILE_MASK
+#define EGL_CONTEXT_MAJOR_VERSION 0x3098
+#define EGL_CONTEXT_MINOR_VERSION 0x30FB
+#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
+#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
+#define EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE 0x31B1
+typedef intptr_t EGLAttrib;
+#endif
+
+// Not every EGL provider (like RPI) has these.
+#ifndef EGL_CONTEXT_FLAGS_KHR
+#define EGL_CONTEXT_FLAGS_KHR EGL_NONE
+#endif
+
+#ifndef EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR
+#define EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR 0
+#endif
+
+struct mp_egl_config_attr {
+ int attrib;
+ const char *name;
+};
+
+#define MP_EGL_ATTRIB(id) {id, # id}
+
+static const struct mp_egl_config_attr mp_egl_attribs[] = {
+ MP_EGL_ATTRIB(EGL_CONFIG_ID),
+ MP_EGL_ATTRIB(EGL_RED_SIZE),
+ MP_EGL_ATTRIB(EGL_GREEN_SIZE),
+ MP_EGL_ATTRIB(EGL_BLUE_SIZE),
+ MP_EGL_ATTRIB(EGL_ALPHA_SIZE),
+ MP_EGL_ATTRIB(EGL_COLOR_BUFFER_TYPE),
+ MP_EGL_ATTRIB(EGL_CONFIG_CAVEAT),
+ MP_EGL_ATTRIB(EGL_CONFORMANT),
+ MP_EGL_ATTRIB(EGL_NATIVE_VISUAL_ID),
+};
+
+static void dump_egl_config(struct mp_log *log, int msgl, EGLDisplay display,
+ EGLConfig config)
+{
+ for (int n = 0; n < MP_ARRAY_SIZE(mp_egl_attribs); n++) {
+ const char *name = mp_egl_attribs[n].name;
+ EGLint v = -1;
+ if (eglGetConfigAttrib(display, config, mp_egl_attribs[n].attrib, &v)) {
+ mp_msg(log, msgl, " %s=0x%x\n", name, v);
+ } else {
+ mp_msg(log, msgl, " %s=<error>\n", name);
+ }
+ }
+}
+
+static void *mpegl_get_proc_address(void *ctx, const char *name)
+{
+ void *p = eglGetProcAddress(name);
+#if defined(__GLIBC__) && HAVE_LIBDL
+ // Some crappy ARM/Linux things do not provide EGL 1.5, so above call does
+ // not necessarily return function pointers for core functions. Try to get
+ // them from a loaded GLES lib. As POSIX leaves RTLD_DEFAULT "reserved",
+ // use it only with glibc.
+ if (!p)
+ p = dlsym(RTLD_DEFAULT, name);
+#endif
+ return p;
+}
+
+static bool create_context(struct ra_ctx *ctx, EGLDisplay display,
+ bool es, struct mpegl_cb cb,
+ EGLContext *out_context, EGLConfig *out_config)
+{
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_FATAL;
+
+ EGLenum api;
+ EGLint rend;
+ const char *name;
+
+ if (!es) {
+ api = EGL_OPENGL_API;
+ rend = EGL_OPENGL_BIT;
+ name = "Desktop OpenGL";
+ } else {
+ api = EGL_OPENGL_ES_API;
+ rend = EGL_OPENGL_ES2_BIT;
+ name = "GLES 2.x +";
+ }
+
+ MP_VERBOSE(ctx, "Trying to create %s context.\n", name);
+
+ if (!eglBindAPI(api)) {
+ MP_VERBOSE(ctx, "Could not bind API!\n");
+ return false;
+ }
+
+ EGLint attributes[] = {
+ EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
+ EGL_RED_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_BLUE_SIZE, 8,
+ EGL_ALPHA_SIZE, ctx->opts.want_alpha ? 8 : 0,
+ EGL_RENDERABLE_TYPE, rend,
+ EGL_NONE
+ };
+
+ EGLint num_configs;
+ if (!eglChooseConfig(display, attributes, NULL, 0, &num_configs))
+ num_configs = 0;
+
+ EGLConfig *configs = talloc_array(NULL, EGLConfig, num_configs);
+ if (!eglChooseConfig(display, attributes, configs, num_configs, &num_configs))
+ num_configs = 0;
+
+ if (!num_configs) {
+ talloc_free(configs);
+ MP_MSG(ctx, msgl, "Could not choose EGLConfig for %s!\n", name);
+ return false;
+ }
+
+ for (int n = 0; n < num_configs; n++)
+ dump_egl_config(ctx->log, MSGL_TRACE, display, configs[n]);
+
+ int chosen = 0;
+ if (cb.refine_config)
+ chosen = cb.refine_config(cb.user_data, configs, num_configs);
+ if (chosen < 0) {
+ talloc_free(configs);
+ MP_MSG(ctx, msgl, "Could not refine EGLConfig for %s!\n", name);
+ return false;
+ }
+ EGLConfig config = configs[chosen];
+
+ talloc_free(configs);
+
+ MP_DBG(ctx, "Chosen EGLConfig:\n");
+ dump_egl_config(ctx->log, MSGL_DEBUG, display, config);
+
+ int ctx_flags = ctx->opts.debug ? EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR : 0;
+ EGLContext *egl_ctx = NULL;
+
+ if (!es) {
+ for (int n = 0; mpgl_min_required_gl_versions[n]; n++) {
+ int ver = mpgl_min_required_gl_versions[n];
+
+ EGLint attrs[] = {
+ EGL_CONTEXT_MAJOR_VERSION, MPGL_VER_GET_MAJOR(ver),
+ EGL_CONTEXT_MINOR_VERSION, MPGL_VER_GET_MINOR(ver),
+ EGL_CONTEXT_OPENGL_PROFILE_MASK,
+ ver >= 320 ? EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT : 0,
+ EGL_CONTEXT_FLAGS_KHR, ctx_flags,
+ EGL_NONE
+ };
+
+ egl_ctx = eglCreateContext(display, config, EGL_NO_CONTEXT, attrs);
+ if (egl_ctx)
+ break;
+ }
+ }
+ if (!egl_ctx) {
+ // Fallback for EGL 1.4 without EGL_KHR_create_context or GLES
+ // Add the context flags only for GLES - GL has been attempted above
+ EGLint attrs[] = {
+ EGL_CONTEXT_CLIENT_VERSION, 2,
+ es ? EGL_CONTEXT_FLAGS_KHR : EGL_NONE, ctx_flags,
+ EGL_NONE
+ };
+
+ egl_ctx = eglCreateContext(display, config, EGL_NO_CONTEXT, attrs);
+ }
+
+ if (!egl_ctx) {
+ MP_MSG(ctx, msgl, "Could not create EGL context for %s!\n", name);
+ return false;
+ }
+
+ *out_context = egl_ctx;
+ *out_config = config;
+ return true;
+}
+
+#define STR_OR_ERR(s) ((s) ? (s) : "(error)")
+
+// Create a context and return it and the config it was created with. If it
+// returns false, the out_* pointers are set to NULL.
+// vo_flags is a combination of VOFLAG_* values.
+bool mpegl_create_context(struct ra_ctx *ctx, EGLDisplay display,
+ EGLContext *out_context, EGLConfig *out_config)
+{
+ return mpegl_create_context_cb(ctx, display, (struct mpegl_cb){0},
+ out_context, out_config);
+}
+
+// Create a context and return it and the config it was created with. If it
+// returns false, the out_* pointers are set to NULL.
+bool mpegl_create_context_cb(struct ra_ctx *ctx, EGLDisplay display,
+ struct mpegl_cb cb, EGLContext *out_context,
+ EGLConfig *out_config)
+{
+ *out_context = NULL;
+ *out_config = NULL;
+
+ const char *version = eglQueryString(display, EGL_VERSION);
+ const char *vendor = eglQueryString(display, EGL_VENDOR);
+ const char *apis = eglQueryString(display, EGL_CLIENT_APIS);
+ MP_VERBOSE(ctx, "EGL_VERSION=%s\nEGL_VENDOR=%s\nEGL_CLIENT_APIS=%s\n",
+ STR_OR_ERR(version), STR_OR_ERR(vendor), STR_OR_ERR(apis));
+
+ enum gles_mode mode = ra_gl_ctx_get_glesmode(ctx);
+
+ if ((mode == GLES_NO || mode == GLES_AUTO) &&
+ create_context(ctx, display, false, cb, out_context, out_config))
+ return true;
+
+ if ((mode == GLES_YES || mode == GLES_AUTO) &&
+ create_context(ctx, display, true, cb, out_context, out_config))
+ return true;
+
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+ MP_MSG(ctx, msgl, "Could not create a GL context.\n");
+ return false;
+}
+
+static int GLAPIENTRY swap_interval(int interval)
+{
+ EGLDisplay display = eglGetCurrentDisplay();
+ if (!display)
+ return 1;
+ return !eglSwapInterval(display, interval);
+}
+
+// Load gl version and function pointers into *gl.
+// Expects a current EGL context set.
+void mpegl_load_functions(struct GL *gl, struct mp_log *log)
+{
+ const char *egl_exts = "";
+ EGLDisplay display = eglGetCurrentDisplay();
+ if (display != EGL_NO_DISPLAY)
+ egl_exts = eglQueryString(display, EGL_EXTENSIONS);
+
+ mpgl_load_functions2(gl, mpegl_get_proc_address, NULL, egl_exts, log);
+ if (!gl->SwapInterval)
+ gl->SwapInterval = swap_interval;
+}
+
+static bool is_egl15(void)
+{
+ // It appears that EGL 1.4 is specified to _require_ an initialized display
+ // for EGL_VERSION, while EGL 1.5 is _required_ to return the EGL version.
+ const char *ver = eglQueryString(EGL_NO_DISPLAY, EGL_VERSION);
+ // Of course we have to go through the excruciating pain of parsing a
+ // version string, since EGL provides no other way without a display. In
+ // theory version!=NULL is already proof enough that it's 1.5, but be
+ // extra defensive, since this should have been true for EGL_EXTENSIONS as
+ // well, but then they added an extension that modified standard behavior.
+ int ma = 0, mi = 0;
+ return ver && sscanf(ver, "%d.%d", &ma, &mi) == 2 && (ma > 1 || mi >= 5);
+}
+
+// This is similar to eglGetPlatformDisplay(platform, native_display, NULL),
+// except that it 1. may use eglGetPlatformDisplayEXT, 2. checks for the
+// platform client extension platform_ext_name, and 3. does not support passing
+// an attrib list, because the type for that parameter is different in the EXT
+// and standard functions (EGL can't not fuck up, no matter what).
+// platform: e.g. EGL_PLATFORM_X11_KHR
+// platform_ext_name: e.g. "EGL_KHR_platform_x11"
+// native_display: e.g. X11 Display*
+// Returns EGL_NO_DISPLAY on failure.
+// Warning: the EGL version can be different at runtime depending on the chosen
+// platform, so this might return a display corresponding to some older EGL
+// version (often 1.4).
+// Often, there are two extension variants of a platform (KHR and EXT). If you
+// need to check both, call this function twice. (Why do they define them twice?
+// They're crazy.)
+EGLDisplay mpegl_get_display(EGLenum platform, const char *platform_ext_name,
+ void *native_display)
+{
+ // EGL is awful. Designed as ultra-portable library, it fails at dealing
+ // with slightly more complex environment than its short-sighted design
+ // could deal with. So they invented an awful, awful kludge that modifies
+ // EGL standard behavior, the EGL_EXT_client_extensions extension. EGL 1.4
+ // normally is to return NULL when querying EGL_EXTENSIONS on EGL_NO_DISPLAY,
+ // however, with that extension, it'll return the set of "client extensions",
+ // which may include EGL_EXT_platform_base.
+
+ // Prerequisite: check the platform extension.
+ // If this is either EGL 1.5, or 1.4 with EGL_EXT_client_extensions, then
+ // this must return a valid extension string.
+ const char *exts = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
+ if (!gl_check_extension(exts, platform_ext_name))
+ return EGL_NO_DISPLAY;
+
+ // Before we go through the EGL 1.4 BS, try if we can use native EGL 1.5
+ if (is_egl15()) {
+ // This is EGL 1.5. It must support querying standard functions through
+ // eglGetProcAddress(). Note that on EGL 1.4, even if the function is
+ // unknown, it could return non-NULL anyway (because EGL is crazy).
+ EGLDisplay (EGLAPIENTRYP GetPlatformDisplay)
+ (EGLenum, void *, const EGLAttrib *) =
+ (void *)eglGetProcAddress("eglGetPlatformDisplay");
+ // (It should be impossible to be NULL, but uh.)
+ if (GetPlatformDisplay)
+ return GetPlatformDisplay(platform, native_display, NULL);
+ }
+
+ if (!gl_check_extension(exts, "EGL_EXT_platform_base"))
+ return EGL_NO_DISPLAY;
+
+ EGLDisplay (EGLAPIENTRYP GetPlatformDisplayEXT)(EGLenum, void*, const EGLint*)
+ = (void *)eglGetProcAddress("eglGetPlatformDisplayEXT");
+
+ // (It should be impossible to be NULL, but uh.)
+ if (GetPlatformDisplayEXT)
+ return GetPlatformDisplayEXT(platform, native_display, NULL);
+
+ return EGL_NO_DISPLAY;
+}
+
+// The same mess but with eglCreatePlatformWindowSurface(EXT)
+// again no support for an attribute list because the type differs
+// Returns EGL_NO_SURFACE on failure.
+EGLSurface mpegl_create_window_surface(EGLDisplay dpy, EGLConfig config,
+ void *native_window)
+{
+ // Use the EGL 1.5 function if possible
+ if (is_egl15()) {
+ EGLSurface (EGLAPIENTRYP CreatePlatformWindowSurface)
+ (EGLDisplay, EGLConfig, void *, const EGLAttrib *) =
+ (void *)eglGetProcAddress("eglCreatePlatformWindowSurface");
+ // (It should be impossible to be NULL, but uh.)
+ if (CreatePlatformWindowSurface)
+ return CreatePlatformWindowSurface(dpy, config, native_window, NULL);
+ }
+
+ // Check the extension that provides the *EXT function
+ const char *exts = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
+ if (!gl_check_extension(exts, "EGL_EXT_platform_base"))
+ return EGL_NO_SURFACE;
+
+ EGLSurface (EGLAPIENTRYP CreatePlatformWindowSurfaceEXT)
+ (EGLDisplay, EGLConfig, void *, const EGLint *) =
+ (void *)eglGetProcAddress("eglCreatePlatformWindowSurfaceEXT");
+ // (It should be impossible to be NULL, but uh.)
+ if (CreatePlatformWindowSurfaceEXT)
+ return CreatePlatformWindowSurfaceEXT(dpy, config, native_window, NULL);
+
+ return EGL_NO_SURFACE;
+}
diff --git a/video/out/opengl/egl_helpers.h b/video/out/opengl/egl_helpers.h
new file mode 100644
index 0000000..32ec5d1
--- /dev/null
+++ b/video/out/opengl/egl_helpers.h
@@ -0,0 +1,38 @@
+#ifndef MP_GL_EGL_HELPERS_H
+#define MP_GL_EGL_HELPERS_H
+
+#include <stdbool.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "video/out/gpu/context.h"
+
+struct mp_log;
+
+bool mpegl_create_context(struct ra_ctx *ctx, EGLDisplay display,
+ EGLContext *out_context, EGLConfig *out_config);
+
+struct mpegl_cb {
+ // if set, pick the desired config from the given list and return its index
+ // defaults to 0 (they are sorted by eglChooseConfig). return a negative
+ // number to indicate an error condition or that no suitable configs could
+ // be found.
+ int (*refine_config)(void *user_data, EGLConfig *configs, int num_configs);
+ void *user_data;
+};
+
+bool mpegl_create_context_cb(struct ra_ctx *ctx, EGLDisplay display,
+ struct mpegl_cb cb, EGLContext *out_context,
+ EGLConfig *out_config);
+
+struct GL;
+void mpegl_load_functions(struct GL *gl, struct mp_log *log);
+
+EGLDisplay mpegl_get_display(EGLenum platform, const char *platform_ext_name,
+ void *native_display);
+
+EGLSurface mpegl_create_window_surface(EGLDisplay dpy, EGLConfig config,
+ void *native_window);
+
+#endif
diff --git a/video/out/opengl/formats.c b/video/out/opengl/formats.c
new file mode 100644
index 0000000..a0b79e2
--- /dev/null
+++ b/video/out/opengl/formats.c
@@ -0,0 +1,196 @@
+#include "common/common.h"
+#include "formats.h"
+
+enum {
+ // --- GL type aliases (for readability)
+ T_U8 = GL_UNSIGNED_BYTE,
+ T_U16 = GL_UNSIGNED_SHORT,
+ T_FL = GL_FLOAT,
+};
+
+// List of allowed formats, and their usability for bilinear filtering and FBOs.
+// This is limited to combinations that are useful for our renderer.
+const struct gl_format gl_formats[] = {
+ // These are used for desktop GL 3+, and GLES 3+ with GL_EXT_texture_norm16.
+ {"r8", GL_R8, GL_RED, T_U8, F_CF | F_GL3 | F_GL2F | F_ES3},
+ {"rg8", GL_RG8, GL_RG, T_U8, F_CF | F_GL3 | F_GL2F | F_ES3},
+ {"rgb8", GL_RGB8, GL_RGB, T_U8, F_CF | F_GL3 | F_GL2F | F_ES3},
+ {"rgba8", GL_RGBA8, GL_RGBA, T_U8, F_CF | F_GL3 | F_GL2F | F_ES3},
+ {"r16", GL_R16, GL_RED, T_U16, F_CF | F_GL3 | F_GL2F | F_EXT16},
+ {"rg16", GL_RG16, GL_RG, T_U16, F_CF | F_GL3 | F_GL2F | F_EXT16},
+ {"rgb16", GL_RGB16, GL_RGB, T_U16, F_CF | F_GL3 | F_GL2F},
+ {"rgba16", GL_RGBA16, GL_RGBA, T_U16, F_CF | F_GL3 | F_GL2F | F_EXT16},
+
+ // Specifically not color-renderable.
+ {"rgb16", GL_RGB16, GL_RGB, T_U16, F_TF | F_EXT16},
+
+ // GL2 legacy. Ignores possibly present FBO extensions (no CF flag set).
+ {"l8", GL_LUMINANCE8, GL_LUMINANCE, T_U8, F_TF | F_GL2},
+ {"la8", GL_LUMINANCE8_ALPHA8, GL_LUMINANCE_ALPHA, T_U8, F_TF | F_GL2},
+ {"rgb8", GL_RGB8, GL_RGB, T_U8, F_TF | F_GL2},
+ {"rgba8", GL_RGBA8, GL_RGBA, T_U8, F_TF | F_GL2},
+ {"l16", GL_LUMINANCE16, GL_LUMINANCE, T_U16, F_TF | F_GL2},
+ {"la16", GL_LUMINANCE16_ALPHA16, GL_LUMINANCE_ALPHA, T_U16, F_TF | F_GL2},
+ {"rgb16", GL_RGB16, GL_RGB, T_U16, F_TF | F_GL2},
+ {"rgba16", GL_RGBA16, GL_RGBA, T_U16, F_TF | F_GL2},
+
+ // ES3 legacy. This is literally to compensate for Apple bugs in their iOS
+ // interop (can they do anything right?). ES3 still allows these formats,
+ // but they are deprecated.
+ {"l" , GL_LUMINANCE,GL_LUMINANCE, T_U8, F_CF | F_ES3},
+ {"la",GL_LUMINANCE_ALPHA,GL_LUMINANCE_ALPHA, T_U8, F_CF | F_ES3},
+
+ // ES2 legacy
+ {"l" , GL_LUMINANCE,GL_LUMINANCE, T_U8, F_TF | F_ES2},
+ {"la",GL_LUMINANCE_ALPHA,GL_LUMINANCE_ALPHA, T_U8, F_TF | F_ES2},
+ {"rgb", GL_RGB, GL_RGB, T_U8, F_TF | F_ES2},
+ {"rgba", GL_RGBA, GL_RGBA, T_U8, F_TF | F_ES2},
+
+ // Non-normalized integer formats.
+ // Follows ES 3.0 as to which are color-renderable.
+ {"r8ui", GL_R8UI, GL_RED_INTEGER, T_U8, F_CR | F_GL3 | F_ES3},
+ {"rg8ui", GL_RG8UI, GL_RG_INTEGER, T_U8, F_CR | F_GL3 | F_ES3},
+ {"rgb8ui", GL_RGB8UI, GL_RGB_INTEGER, T_U8, F_GL3 | F_ES3},
+ {"rgba8ui", GL_RGBA8UI, GL_RGBA_INTEGER, T_U8, F_CR | F_GL3 | F_ES3},
+ {"r16ui", GL_R16UI, GL_RED_INTEGER, T_U16, F_CR | F_GL3 | F_ES3},
+ {"rg16ui", GL_RG16UI, GL_RG_INTEGER, T_U16, F_CR | F_GL3 | F_ES3},
+ {"rgb16ui", GL_RGB16UI, GL_RGB_INTEGER, T_U16, F_GL3 | F_ES3},
+ {"rgba16ui",GL_RGBA16UI, GL_RGBA_INTEGER, T_U16, F_CR | F_GL3 | F_ES3},
+
+ // On GL3+ or GL2.1 with GL_ARB_texture_float, floats work fully.
+ {"r16f", GL_R16F, GL_RED, T_FL, F_F16 | F_CF | F_GL3 | F_GL2F},
+ {"rg16f", GL_RG16F, GL_RG, T_FL, F_F16 | F_CF | F_GL3 | F_GL2F},
+ {"rgb16f", GL_RGB16F, GL_RGB, T_FL, F_F16 | F_CF | F_GL3 | F_GL2F},
+ {"rgba16f", GL_RGBA16F, GL_RGBA, T_FL, F_F16 | F_CF | F_GL3 | F_GL2F},
+ {"r32f", GL_R32F, GL_RED, T_FL, F_CF | F_GL3 | F_GL2F},
+ {"rg32f", GL_RG32F, GL_RG, T_FL, F_CF | F_GL3 | F_GL2F},
+ {"rgb32f", GL_RGB32F, GL_RGB, T_FL, F_CF | F_GL3 | F_GL2F},
+ {"rgba32f", GL_RGBA32F, GL_RGBA, T_FL, F_CF | F_GL3 | F_GL2F},
+
+ // Note: we simply don't support float anything on ES2, despite extensions.
+ // We also don't bother with non-filterable float formats, and we ignore
+ // 32 bit float formats that are not blendable when rendering to them.
+
+ // On ES3.2+, both 16 bit floats work fully (except 3-component formats).
+ // F_EXTF16 implies extensions that also enable 16 bit floats fully.
+ {"r16f", GL_R16F, GL_RED, T_FL, F_F16 | F_CF | F_ES32 | F_EXTF16},
+ {"rg16f", GL_RG16F, GL_RG, T_FL, F_F16 | F_CF | F_ES32 | F_EXTF16},
+ {"rgb16f", GL_RGB16F, GL_RGB, T_FL, F_F16 | F_TF | F_ES32 | F_EXTF16},
+ {"rgba16f", GL_RGBA16F, GL_RGBA, T_FL, F_F16 | F_CF | F_ES32 | F_EXTF16},
+
+ // On ES3.0+, 16 bit floats are texture-filterable.
+ // Don't bother with 32 bit floats; they exist but are neither CR nor TF.
+ {"r16f", GL_R16F, GL_RED, T_FL, F_F16 | F_TF | F_ES3},
+ {"rg16f", GL_RG16F, GL_RG, T_FL, F_F16 | F_TF | F_ES3},
+ {"rgb16f", GL_RGB16F, GL_RGB, T_FL, F_F16 | F_TF | F_ES3},
+ {"rgba16f", GL_RGBA16F, GL_RGBA, T_FL, F_F16 | F_TF | F_ES3},
+
+ // These might be useful as FBO formats.
+ {"rgb10_a2",GL_RGB10_A2, GL_RGBA,
+ GL_UNSIGNED_INT_2_10_10_10_REV, F_CF | F_GL3 | F_ES3},
+ {"rgba12", GL_RGBA12, GL_RGBA, T_U16, F_CF | F_GL2 | F_GL3},
+ {"rgb10", GL_RGB10, GL_RGB, T_U16, F_CF | F_GL2 | F_GL3},
+
+ // Special formats.
+ {"rgb565", GL_RGB8, GL_RGB,
+ GL_UNSIGNED_SHORT_5_6_5, F_TF | F_GL2 | F_GL3},
+ // Worthless, but needed by OSX videotoolbox interop on old Apple hardware.
+ {"appleyp", GL_RGB, GL_RGB_422_APPLE,
+ GL_UNSIGNED_SHORT_8_8_APPLE, F_TF | F_APPL},
+
+ {0}
+};
+
+// Return an or-ed combination of all F_ flags that apply.
+int gl_format_feature_flags(GL *gl)
+{
+ return (gl->version == 210 ? F_GL2 : 0)
+ | (gl->version >= 300 ? F_GL3 : 0)
+ | (gl->es == 200 ? F_ES2 : 0)
+ | (gl->es >= 300 ? F_ES3 : 0)
+ | (gl->es >= 320 ? F_ES32 : 0)
+ | (gl->mpgl_caps & MPGL_CAP_EXT16 ? F_EXT16 : 0)
+ | ((gl->es >= 300 &&
+ (gl->mpgl_caps & MPGL_CAP_EXT_CR_HFLOAT)) ? F_EXTF16 : 0)
+ | ((gl->version == 210 &&
+ (gl->mpgl_caps & MPGL_CAP_ARB_FLOAT) &&
+ (gl->mpgl_caps & MPGL_CAP_TEX_RG) &&
+ (gl->mpgl_caps & MPGL_CAP_FB)) ? F_GL2F : 0)
+ | (gl->mpgl_caps & MPGL_CAP_APPLE_RGB_422 ? F_APPL : 0);
+}
+
+int gl_format_type(const struct gl_format *format)
+{
+ if (!format)
+ return 0;
+ if (format->type == GL_FLOAT)
+ return MPGL_TYPE_FLOAT;
+ if (gl_integer_format_to_base(format->format))
+ return MPGL_TYPE_UINT;
+ return MPGL_TYPE_UNORM;
+}
+
+// Return base internal format of an integer format, or 0 if it's not integer.
+// "format" is like in struct gl_format.
+GLenum gl_integer_format_to_base(GLenum format)
+{
+ switch (format) {
+ case GL_RED_INTEGER: return GL_RED;
+ case GL_RG_INTEGER: return GL_RG;
+ case GL_RGB_INTEGER: return GL_RGB;
+ case GL_RGBA_INTEGER: return GL_RGBA;
+ }
+ return 0;
+}
+
+// Return the number of bytes per component this format implies.
+// Returns 0 for formats with non-byte alignments and formats which
+// merge multiple components (like GL_UNSIGNED_SHORT_5_6_5).
+// "type" is like in struct gl_format.
+int gl_component_size(GLenum type)
+{
+ switch (type) {
+ case GL_UNSIGNED_BYTE: return 1;
+ case GL_UNSIGNED_SHORT: return 2;
+ case GL_FLOAT: return 4;
+ }
+ return 0;
+}
+
+// Return the number of separate color components.
+// "format" is like in struct gl_format.
+int gl_format_components(GLenum format)
+{
+ switch (format) {
+ case GL_RED:
+ case GL_RED_INTEGER:
+ case GL_LUMINANCE:
+ return 1;
+ case GL_RG:
+ case GL_RG_INTEGER:
+ case GL_LUMINANCE_ALPHA:
+ return 2;
+ case GL_RGB:
+ case GL_RGB_INTEGER:
+ return 3;
+ case GL_RGBA:
+ case GL_RGBA_INTEGER:
+ return 4;
+ }
+ return 0;
+}
+
+// Return the number of bytes per pixel for the given format.
+// Parameter names like in struct gl_format.
+int gl_bytes_per_pixel(GLenum format, GLenum type)
+{
+ // Formats with merged components are special.
+ switch (type) {
+ case GL_UNSIGNED_INT_2_10_10_10_REV: return 4;
+ case GL_UNSIGNED_SHORT_5_6_5: return 2;
+ case GL_UNSIGNED_SHORT_8_8_APPLE: return 2;
+ case GL_UNSIGNED_SHORT_8_8_REV_APPLE: return 2;
+ }
+
+ return gl_component_size(type) * gl_format_components(format);
+}
diff --git a/video/out/opengl/formats.h b/video/out/opengl/formats.h
new file mode 100644
index 0000000..f727a3b
--- /dev/null
+++ b/video/out/opengl/formats.h
@@ -0,0 +1,51 @@
+#ifndef MPGL_FORMATS_H_
+#define MPGL_FORMATS_H_
+
+#include "common.h"
+
+struct gl_format {
+ const char *name; // symbolic name for user interaction/debugging
+ GLint internal_format; // glTexImage argument
+ GLenum format; // glTexImage argument
+ GLenum type; // e.g. GL_UNSIGNED_SHORT
+ int flags; // F_* flags
+};
+
+enum {
+ // --- gl_format.flags
+
+ // Version flags. If at least 1 flag matches, the format entry is considered
+ // supported on the current GL context.
+ F_GL2 = 1 << 0, // GL2.1-only
+ F_GL3 = 1 << 1, // GL3.0 or later
+ F_ES2 = 1 << 2, // ES2-only
+ F_ES3 = 1 << 3, // ES3.0 or later
+ F_ES32 = 1 << 4, // ES3.2 or later
+ F_EXT16 = 1 << 5, // ES with GL_EXT_texture_norm16
+ F_EXTF16 = 1 << 6, // GL_EXT_color_buffer_half_float
+ F_GL2F = 1 << 7, // GL2.1-only with texture_rg + texture_float + FBOs
+ F_APPL = 1 << 8, // GL_APPLE_rgb_422
+
+ // Feature flags. They are additional and signal presence of features.
+ F_CR = 1 << 16, // color-renderable
+ F_TF = 1 << 17, // texture-filterable with GL_LINEAR
+ F_CF = F_CR | F_TF,
+ F_F16 = 1 << 18, // uses half-floats (16 bit) internally, even though
+ // the format is still GL_FLOAT (32 bit)
+
+ // --- Other constants.
+ MPGL_TYPE_UNORM = RA_CTYPE_UNORM, // normalized integer (fixed point) formats
+ MPGL_TYPE_UINT = RA_CTYPE_UINT, // full integer formats
+ MPGL_TYPE_FLOAT = RA_CTYPE_FLOAT, // float formats (both full and half)
+};
+
+extern const struct gl_format gl_formats[];
+
+int gl_format_feature_flags(GL *gl);
+int gl_format_type(const struct gl_format *format);
+GLenum gl_integer_format_to_base(GLenum format);
+int gl_component_size(GLenum type);
+int gl_format_components(GLenum format);
+int gl_bytes_per_pixel(GLenum format, GLenum type);
+
+#endif
diff --git a/video/out/opengl/gl_headers.h b/video/out/opengl/gl_headers.h
new file mode 100644
index 0000000..5c36718
--- /dev/null
+++ b/video/out/opengl/gl_headers.h
@@ -0,0 +1,799 @@
+/*
+ * Parts of OpenGL(ES) needed by the OpenGL renderer.
+ *
+ * This excludes function declarations.
+ *
+ * This header is based on:
+ * - Khronos GLES headers (MIT)
+ * - mpv or MPlayer code (LGPL 2.1 or later)
+ * - probably Mesa GL headers (MIT)
+ */
+
+#ifndef MPV_GL_HEADERS_H
+#define MPV_GL_HEADERS_H
+
+#include <stdint.h>
+
+// Enable this to use system headers instead.
+#if 0
+#include <GL/gl.h>
+#include <GLES3/gl3.h>
+#endif
+
+#ifndef GLAPIENTRY
+#ifdef _WIN32
+#define GLAPIENTRY __stdcall
+#else
+#define GLAPIENTRY
+#endif
+#endif
+
+// Typedefs. This needs to work with system headers too (consider GLX), and
+// before C11, duplicated typedefs were an error. So try to tolerate at least
+// Mesa.
+#ifdef GL_TRUE
+ // Tolerate old Mesa which has only definitions up to GL 2.0.
+ #define MP_GET_GL_TYPES_2_0 0
+ #ifdef GL_VERSION_3_2
+ #define MP_GET_GL_TYPES_3_2 0
+ #else
+ #define MP_GET_GL_TYPES_3_2 1
+ #endif
+#else
+ // Get them all.
+ #define MP_GET_GL_TYPES_2_0 1
+ #define MP_GET_GL_TYPES_3_2 1
+#endif
+
+#if MP_GET_GL_TYPES_2_0
+// GL_VERSION_1_0, GL_ES_VERSION_2_0
+typedef unsigned int GLbitfield;
+typedef unsigned char GLboolean;
+typedef unsigned int GLenum;
+typedef float GLfloat;
+typedef int GLint;
+typedef int GLsizei;
+typedef uint8_t GLubyte;
+typedef unsigned int GLuint;
+typedef void GLvoid;
+// GL 1.1 GL_VERSION_1_1, GL_ES_VERSION_2_0
+typedef float GLclampf;
+// GL 1.5 GL_VERSION_1_5, GL_ES_VERSION_2_0
+typedef intptr_t GLintptr;
+typedef ptrdiff_t GLsizeiptr;
+// GL 2.0 GL_VERSION_2_0, GL_ES_VERSION_2_0
+typedef int8_t GLbyte;
+typedef char GLchar;
+typedef short GLshort;
+typedef unsigned short GLushort;
+#endif
+
+#if MP_GET_GL_TYPES_3_2
+// GL 3.2 GL_VERSION_3_2, GL_ES_VERSION_2_0
+typedef int64_t GLint64;
+typedef struct __GLsync *GLsync;
+typedef uint64_t GLuint64;
+#endif
+
+// --- GL 1.1
+
+#define GL_BACK_LEFT 0x0402
+#define GL_TEXTURE_1D 0x0DE0
+#define GL_RGB16 0x8054
+#define GL_RGB10 0x8052
+#define GL_RGBA12 0x805A
+#define GL_RGBA16 0x805B
+#define GL_TEXTURE_RED_SIZE 0x805C
+#define GL_TEXTURE_GREEN_SIZE 0x805D
+#define GL_TEXTURE_BLUE_SIZE 0x805E
+#define GL_TEXTURE_ALPHA_SIZE 0x805F
+
+// --- GL 1.1 (removed from 3.0 core and not in GLES 2/3)
+
+#define GL_TEXTURE_LUMINANCE_SIZE 0x8060
+#define GL_LUMINANCE8 0x8040
+#define GL_LUMINANCE8_ALPHA8 0x8045
+#define GL_LUMINANCE16 0x8042
+#define GL_LUMINANCE16_ALPHA16 0x8048
+
+// --- GL 1.5
+
+#define GL_READ_ONLY 0x88B8
+#define GL_WRITE_ONLY 0x88B9
+#define GL_READ_WRITE 0x88BA
+
+// --- GL 3.0
+
+#define GL_R16 0x822A
+#define GL_RG16 0x822C
+
+// --- GL 3.1
+
+#define GL_TEXTURE_RECTANGLE 0x84F5
+
+// --- GL 3.3 or GL_ARB_timer_query
+
+#define GL_TIME_ELAPSED 0x88BF
+#define GL_TIMESTAMP 0x8E28
+
+// --- GL 4.3 or GL_ARB_debug_output
+
+#define GL_DEBUG_SEVERITY_HIGH 0x9146
+#define GL_DEBUG_SEVERITY_MEDIUM 0x9147
+#define GL_DEBUG_SEVERITY_LOW 0x9148
+#define GL_DEBUG_SEVERITY_NOTIFICATION 0x826B
+
+// --- GL 4.4 or GL_ARB_buffer_storage
+
+#define GL_MAP_PERSISTENT_BIT 0x0040
+#define GL_MAP_COHERENT_BIT 0x0080
+#define GL_DYNAMIC_STORAGE_BIT 0x0100
+#define GL_CLIENT_STORAGE_BIT 0x0200
+
+// --- GL 4.2 or GL_ARB_image_load_store
+
+#define GL_TEXTURE_FETCH_BARRIER_BIT 0x00000008
+
+// --- GL 4.3 or GL_ARB_compute_shader
+
+#define GL_COMPUTE_SHADER 0x91B9
+#define GL_MAX_COMPUTE_SHARED_MEMORY_SIZE 0x8262
+#define GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS 0x90EB
+
+// --- GL 4.3 or GL_ARB_shader_storage_buffer_object
+
+#define GL_SHADER_STORAGE_BUFFER 0x90D2
+#define GL_SHADER_STORAGE_BARRIER_BIT 0x00002000
+
+// --- GL_NV_vdpau_interop
+
+#define GLvdpauSurfaceNV GLintptr
+#define GL_WRITE_DISCARD_NV 0x88BE
+
+// --- GL_OES_EGL_image_external, GL_NV_EGL_stream_consumer_external
+
+#define GL_TEXTURE_EXTERNAL_OES 0x8D65
+
+// --- GL_APPLE_rgb_422
+
+#define GL_RGB_422_APPLE 0x8A1F
+#define GL_UNSIGNED_SHORT_8_8_APPLE 0x85BA
+#define GL_UNSIGNED_SHORT_8_8_REV_APPLE 0x85BB
+
+// --- GL_ANGLE_translated_shader_source
+
+#define GL_TRANSLATED_SHADER_SOURCE_LENGTH_ANGLE 0x93A0
+
+// ---- GLES 2
+
+#define GL_DEPTH_BUFFER_BIT 0x00000100
+#define GL_STENCIL_BUFFER_BIT 0x00000400
+#define GL_COLOR_BUFFER_BIT 0x00004000
+#define GL_FALSE 0
+#define GL_TRUE 1
+#define GL_POINTS 0x0000
+#define GL_LINES 0x0001
+#define GL_LINE_LOOP 0x0002
+#define GL_LINE_STRIP 0x0003
+#define GL_TRIANGLES 0x0004
+#define GL_TRIANGLE_STRIP 0x0005
+#define GL_TRIANGLE_FAN 0x0006
+#define GL_ZERO 0
+#define GL_ONE 1
+#define GL_SRC_COLOR 0x0300
+#define GL_ONE_MINUS_SRC_COLOR 0x0301
+#define GL_SRC_ALPHA 0x0302
+#define GL_ONE_MINUS_SRC_ALPHA 0x0303
+#define GL_DST_ALPHA 0x0304
+#define GL_ONE_MINUS_DST_ALPHA 0x0305
+#define GL_DST_COLOR 0x0306
+#define GL_ONE_MINUS_DST_COLOR 0x0307
+#define GL_SRC_ALPHA_SATURATE 0x0308
+#define GL_FUNC_ADD 0x8006
+#define GL_BLEND_EQUATION 0x8009
+#define GL_BLEND_EQUATION_RGB 0x8009
+#define GL_BLEND_EQUATION_ALPHA 0x883D
+#define GL_FUNC_SUBTRACT 0x800A
+#define GL_FUNC_REVERSE_SUBTRACT 0x800B
+#define GL_BLEND_DST_RGB 0x80C8
+#define GL_BLEND_SRC_RGB 0x80C9
+#define GL_BLEND_DST_ALPHA 0x80CA
+#define GL_BLEND_SRC_ALPHA 0x80CB
+#define GL_CONSTANT_COLOR 0x8001
+#define GL_ONE_MINUS_CONSTANT_COLOR 0x8002
+#define GL_CONSTANT_ALPHA 0x8003
+#define GL_ONE_MINUS_CONSTANT_ALPHA 0x8004
+#define GL_BLEND_COLOR 0x8005
+#define GL_ARRAY_BUFFER 0x8892
+#define GL_ELEMENT_ARRAY_BUFFER 0x8893
+#define GL_ARRAY_BUFFER_BINDING 0x8894
+#define GL_ELEMENT_ARRAY_BUFFER_BINDING 0x8895
+#define GL_STREAM_DRAW 0x88E0
+#define GL_STATIC_DRAW 0x88E4
+#define GL_DYNAMIC_DRAW 0x88E8
+#define GL_BUFFER_SIZE 0x8764
+#define GL_BUFFER_USAGE 0x8765
+#define GL_CURRENT_VERTEX_ATTRIB 0x8626
+#define GL_FRONT 0x0404
+#define GL_BACK 0x0405
+#define GL_FRONT_AND_BACK 0x0408
+#define GL_TEXTURE_2D 0x0DE1
+#define GL_CULL_FACE 0x0B44
+#define GL_BLEND 0x0BE2
+#define GL_DITHER 0x0BD0
+#define GL_STENCIL_TEST 0x0B90
+#define GL_DEPTH_TEST 0x0B71
+#define GL_SCISSOR_TEST 0x0C11
+#define GL_POLYGON_OFFSET_FILL 0x8037
+#define GL_SAMPLE_ALPHA_TO_COVERAGE 0x809E
+#define GL_SAMPLE_COVERAGE 0x80A0
+#define GL_NO_ERROR 0
+#define GL_INVALID_ENUM 0x0500
+#define GL_INVALID_VALUE 0x0501
+#define GL_INVALID_OPERATION 0x0502
+#define GL_OUT_OF_MEMORY 0x0505
+#define GL_CW 0x0900
+#define GL_CCW 0x0901
+#define GL_LINE_WIDTH 0x0B21
+#define GL_ALIASED_POINT_SIZE_RANGE 0x846D
+#define GL_ALIASED_LINE_WIDTH_RANGE 0x846E
+#define GL_CULL_FACE_MODE 0x0B45
+#define GL_FRONT_FACE 0x0B46
+#define GL_DEPTH_RANGE 0x0B70
+#define GL_DEPTH_WRITEMASK 0x0B72
+#define GL_DEPTH_CLEAR_VALUE 0x0B73
+#define GL_DEPTH_FUNC 0x0B74
+#define GL_STENCIL_CLEAR_VALUE 0x0B91
+#define GL_STENCIL_FUNC 0x0B92
+#define GL_STENCIL_FAIL 0x0B94
+#define GL_STENCIL_PASS_DEPTH_FAIL 0x0B95
+#define GL_STENCIL_PASS_DEPTH_PASS 0x0B96
+#define GL_STENCIL_REF 0x0B97
+#define GL_STENCIL_VALUE_MASK 0x0B93
+#define GL_STENCIL_WRITEMASK 0x0B98
+#define GL_STENCIL_BACK_FUNC 0x8800
+#define GL_STENCIL_BACK_FAIL 0x8801
+#define GL_STENCIL_BACK_PASS_DEPTH_FAIL 0x8802
+#define GL_STENCIL_BACK_PASS_DEPTH_PASS 0x8803
+#define GL_STENCIL_BACK_REF 0x8CA3
+#define GL_STENCIL_BACK_VALUE_MASK 0x8CA4
+#define GL_STENCIL_BACK_WRITEMASK 0x8CA5
+#define GL_VIEWPORT 0x0BA2
+#define GL_SCISSOR_BOX 0x0C10
+#define GL_COLOR_CLEAR_VALUE 0x0C22
+#define GL_COLOR_WRITEMASK 0x0C23
+#define GL_UNPACK_ALIGNMENT 0x0CF5
+#define GL_PACK_ALIGNMENT 0x0D05
+#define GL_MAX_TEXTURE_SIZE 0x0D33
+#define GL_MAX_VIEWPORT_DIMS 0x0D3A
+#define GL_SUBPIXEL_BITS 0x0D50
+#define GL_RED_BITS 0x0D52
+#define GL_GREEN_BITS 0x0D53
+#define GL_BLUE_BITS 0x0D54
+#define GL_ALPHA_BITS 0x0D55
+#define GL_DEPTH_BITS 0x0D56
+#define GL_STENCIL_BITS 0x0D57
+#define GL_POLYGON_OFFSET_UNITS 0x2A00
+#define GL_POLYGON_OFFSET_FACTOR 0x8038
+#define GL_TEXTURE_BINDING_2D 0x8069
+#define GL_SAMPLE_BUFFERS 0x80A8
+#define GL_SAMPLES 0x80A9
+#define GL_SAMPLE_COVERAGE_VALUE 0x80AA
+#define GL_SAMPLE_COVERAGE_INVERT 0x80AB
+#define GL_NUM_COMPRESSED_TEXTURE_FORMATS 0x86A2
+#define GL_COMPRESSED_TEXTURE_FORMATS 0x86A3
+#define GL_DONT_CARE 0x1100
+#define GL_FASTEST 0x1101
+#define GL_NICEST 0x1102
+#define GL_GENERATE_MIPMAP_HINT 0x8192
+#define GL_BYTE 0x1400
+#define GL_UNSIGNED_BYTE 0x1401
+#define GL_SHORT 0x1402
+#define GL_UNSIGNED_SHORT 0x1403
+#define GL_INT 0x1404
+#define GL_UNSIGNED_INT 0x1405
+#define GL_FLOAT 0x1406
+#define GL_FIXED 0x140C
+#define GL_DEPTH_COMPONENT 0x1902
+#define GL_ALPHA 0x1906
+#define GL_RGB 0x1907
+#define GL_RGBA 0x1908
+#define GL_LUMINANCE 0x1909
+#define GL_LUMINANCE_ALPHA 0x190A
+#define GL_UNSIGNED_SHORT_4_4_4_4 0x8033
+#define GL_UNSIGNED_SHORT_5_5_5_1 0x8034
+#define GL_UNSIGNED_SHORT_5_6_5 0x8363
+#define GL_FRAGMENT_SHADER 0x8B30
+#define GL_VERTEX_SHADER 0x8B31
+#define GL_MAX_VERTEX_ATTRIBS 0x8869
+#define GL_MAX_VERTEX_UNIFORM_VECTORS 0x8DFB
+#define GL_MAX_VARYING_VECTORS 0x8DFC
+#define GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS 0x8B4D
+#define GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS 0x8B4C
+#define GL_MAX_TEXTURE_IMAGE_UNITS 0x8872
+#define GL_MAX_FRAGMENT_UNIFORM_VECTORS 0x8DFD
+#define GL_SHADER_TYPE 0x8B4F
+#define GL_DELETE_STATUS 0x8B80
+#define GL_LINK_STATUS 0x8B82
+#define GL_VALIDATE_STATUS 0x8B83
+#define GL_ATTACHED_SHADERS 0x8B85
+#define GL_ACTIVE_UNIFORMS 0x8B86
+#define GL_ACTIVE_UNIFORM_MAX_LENGTH 0x8B87
+#define GL_ACTIVE_ATTRIBUTES 0x8B89
+#define GL_ACTIVE_ATTRIBUTE_MAX_LENGTH 0x8B8A
+#define GL_SHADING_LANGUAGE_VERSION 0x8B8C
+#define GL_CURRENT_PROGRAM 0x8B8D
+#define GL_NEVER 0x0200
+#define GL_LESS 0x0201
+#define GL_EQUAL 0x0202
+#define GL_LEQUAL 0x0203
+#define GL_GREATER 0x0204
+#define GL_NOTEQUAL 0x0205
+#define GL_GEQUAL 0x0206
+#define GL_ALWAYS 0x0207
+#define GL_KEEP 0x1E00
+#define GL_REPLACE 0x1E01
+#define GL_INCR 0x1E02
+#define GL_DECR 0x1E03
+#define GL_INVERT 0x150A
+#define GL_INCR_WRAP 0x8507
+#define GL_DECR_WRAP 0x8508
+#define GL_VENDOR 0x1F00
+#define GL_RENDERER 0x1F01
+#define GL_VERSION 0x1F02
+#define GL_EXTENSIONS 0x1F03
+#define GL_NEAREST 0x2600
+#define GL_LINEAR 0x2601
+#define GL_NEAREST_MIPMAP_NEAREST 0x2700
+#define GL_LINEAR_MIPMAP_NEAREST 0x2701
+#define GL_NEAREST_MIPMAP_LINEAR 0x2702
+#define GL_LINEAR_MIPMAP_LINEAR 0x2703
+#define GL_TEXTURE_MAG_FILTER 0x2800
+#define GL_TEXTURE_MIN_FILTER 0x2801
+#define GL_TEXTURE_WRAP_S 0x2802
+#define GL_TEXTURE_WRAP_T 0x2803
+#define GL_TEXTURE 0x1702
+#define GL_TEXTURE_CUBE_MAP 0x8513
+#define GL_TEXTURE_BINDING_CUBE_MAP 0x8514
+#define GL_TEXTURE_CUBE_MAP_POSITIVE_X 0x8515
+#define GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x8516
+#define GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0x8517
+#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x8518
+#define GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0x8519
+#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x851A
+#define GL_MAX_CUBE_MAP_TEXTURE_SIZE 0x851C
+#define GL_TEXTURE0 0x84C0
+#define GL_TEXTURE1 0x84C1
+#define GL_TEXTURE2 0x84C2
+#define GL_TEXTURE3 0x84C3
+#define GL_TEXTURE4 0x84C4
+#define GL_TEXTURE5 0x84C5
+#define GL_TEXTURE6 0x84C6
+#define GL_TEXTURE7 0x84C7
+#define GL_TEXTURE8 0x84C8
+#define GL_TEXTURE9 0x84C9
+#define GL_TEXTURE10 0x84CA
+#define GL_TEXTURE11 0x84CB
+#define GL_TEXTURE12 0x84CC
+#define GL_TEXTURE13 0x84CD
+#define GL_TEXTURE14 0x84CE
+#define GL_TEXTURE15 0x84CF
+#define GL_TEXTURE16 0x84D0
+#define GL_TEXTURE17 0x84D1
+#define GL_TEXTURE18 0x84D2
+#define GL_TEXTURE19 0x84D3
+#define GL_TEXTURE20 0x84D4
+#define GL_TEXTURE21 0x84D5
+#define GL_TEXTURE22 0x84D6
+#define GL_TEXTURE23 0x84D7
+#define GL_TEXTURE24 0x84D8
+#define GL_TEXTURE25 0x84D9
+#define GL_TEXTURE26 0x84DA
+#define GL_TEXTURE27 0x84DB
+#define GL_TEXTURE28 0x84DC
+#define GL_TEXTURE29 0x84DD
+#define GL_TEXTURE30 0x84DE
+#define GL_TEXTURE31 0x84DF
+#define GL_ACTIVE_TEXTURE 0x84E0
+#define GL_REPEAT 0x2901
+#define GL_CLAMP_TO_EDGE 0x812F
+#define GL_MIRRORED_REPEAT 0x8370
+#define GL_FLOAT_VEC2 0x8B50
+#define GL_FLOAT_VEC3 0x8B51
+#define GL_FLOAT_VEC4 0x8B52
+#define GL_INT_VEC2 0x8B53
+#define GL_INT_VEC3 0x8B54
+#define GL_INT_VEC4 0x8B55
+#define GL_BOOL 0x8B56
+#define GL_BOOL_VEC2 0x8B57
+#define GL_BOOL_VEC3 0x8B58
+#define GL_BOOL_VEC4 0x8B59
+#define GL_FLOAT_MAT2 0x8B5A
+#define GL_FLOAT_MAT3 0x8B5B
+#define GL_FLOAT_MAT4 0x8B5C
+#define GL_SAMPLER_2D 0x8B5E
+#define GL_SAMPLER_CUBE 0x8B60
+#define GL_VERTEX_ATTRIB_ARRAY_ENABLED 0x8622
+#define GL_VERTEX_ATTRIB_ARRAY_SIZE 0x8623
+#define GL_VERTEX_ATTRIB_ARRAY_STRIDE 0x8624
+#define GL_VERTEX_ATTRIB_ARRAY_TYPE 0x8625
+#define GL_VERTEX_ATTRIB_ARRAY_NORMALIZED 0x886A
+#define GL_VERTEX_ATTRIB_ARRAY_POINTER 0x8645
+#define GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING 0x889F
+#define GL_IMPLEMENTATION_COLOR_READ_TYPE 0x8B9A
+#define GL_IMPLEMENTATION_COLOR_READ_FORMAT 0x8B9B
+#define GL_COMPILE_STATUS 0x8B81
+#define GL_INFO_LOG_LENGTH 0x8B84
+#define GL_SHADER_SOURCE_LENGTH 0x8B88
+#define GL_SHADER_COMPILER 0x8DFA
+#define GL_SHADER_BINARY_FORMATS 0x8DF8
+#define GL_NUM_SHADER_BINARY_FORMATS 0x8DF9
+#define GL_LOW_FLOAT 0x8DF0
+#define GL_MEDIUM_FLOAT 0x8DF1
+#define GL_HIGH_FLOAT 0x8DF2
+#define GL_LOW_INT 0x8DF3
+#define GL_MEDIUM_INT 0x8DF4
+#define GL_HIGH_INT 0x8DF5
+#define GL_FRAMEBUFFER 0x8D40
+#define GL_RENDERBUFFER 0x8D41
+#define GL_RGBA4 0x8056
+#define GL_RGB5_A1 0x8057
+#define GL_RGB565 0x8D62
+#define GL_DEPTH_COMPONENT16 0x81A5
+#define GL_STENCIL_INDEX8 0x8D48
+#define GL_RENDERBUFFER_WIDTH 0x8D42
+#define GL_RENDERBUFFER_HEIGHT 0x8D43
+#define GL_RENDERBUFFER_INTERNAL_FORMAT 0x8D44
+#define GL_RENDERBUFFER_RED_SIZE 0x8D50
+#define GL_RENDERBUFFER_GREEN_SIZE 0x8D51
+#define GL_RENDERBUFFER_BLUE_SIZE 0x8D52
+#define GL_RENDERBUFFER_ALPHA_SIZE 0x8D53
+#define GL_RENDERBUFFER_DEPTH_SIZE 0x8D54
+#define GL_RENDERBUFFER_STENCIL_SIZE 0x8D55
+#define GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE 0x8CD0
+#define GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME 0x8CD1
+#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL 0x8CD2
+#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE 0x8CD3
+#define GL_COLOR_ATTACHMENT0 0x8CE0
+#define GL_DEPTH_ATTACHMENT 0x8D00
+#define GL_STENCIL_ATTACHMENT 0x8D20
+#define GL_NONE 0
+#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
+#define GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT 0x8CD6
+#define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT 0x8CD7
+#define GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS 0x8CD9
+#define GL_FRAMEBUFFER_UNSUPPORTED 0x8CDD
+#define GL_FRAMEBUFFER_BINDING 0x8CA6
+#define GL_RENDERBUFFER_BINDING 0x8CA7
+#define GL_MAX_RENDERBUFFER_SIZE 0x84E8
+#define GL_INVALID_FRAMEBUFFER_OPERATION 0x0506
+
+// ---- GLES 3
+
+#ifndef GL_READ_BUFFER
+typedef unsigned short GLhalf;
+#endif
+
+#define GL_READ_BUFFER 0x0C02
+#define GL_UNPACK_ROW_LENGTH 0x0CF2
+#define GL_UNPACK_SKIP_ROWS 0x0CF3
+#define GL_UNPACK_SKIP_PIXELS 0x0CF4
+#define GL_PACK_ROW_LENGTH 0x0D02
+#define GL_PACK_SKIP_ROWS 0x0D03
+#define GL_PACK_SKIP_PIXELS 0x0D04
+#define GL_COLOR 0x1800
+#define GL_DEPTH 0x1801
+#define GL_STENCIL 0x1802
+#define GL_RED 0x1903
+#define GL_RGB8 0x8051
+#define GL_RGBA8 0x8058
+#define GL_RGB10_A2 0x8059
+#define GL_TEXTURE_BINDING_3D 0x806A
+#define GL_UNPACK_SKIP_IMAGES 0x806D
+#define GL_UNPACK_IMAGE_HEIGHT 0x806E
+#define GL_TEXTURE_3D 0x806F
+#define GL_TEXTURE_WRAP_R 0x8072
+#define GL_MAX_3D_TEXTURE_SIZE 0x8073
+#define GL_UNSIGNED_INT_2_10_10_10_REV 0x8368
+#define GL_MAX_ELEMENTS_VERTICES 0x80E8
+#define GL_MAX_ELEMENTS_INDICES 0x80E9
+#define GL_TEXTURE_MIN_LOD 0x813A
+#define GL_TEXTURE_MAX_LOD 0x813B
+#define GL_TEXTURE_BASE_LEVEL 0x813C
+#define GL_TEXTURE_MAX_LEVEL 0x813D
+#define GL_MIN 0x8007
+#define GL_MAX 0x8008
+#define GL_DEPTH_COMPONENT24 0x81A6
+#define GL_MAX_TEXTURE_LOD_BIAS 0x84FD
+#define GL_TEXTURE_COMPARE_MODE 0x884C
+#define GL_TEXTURE_COMPARE_FUNC 0x884D
+#define GL_CURRENT_QUERY 0x8865
+#define GL_QUERY_RESULT 0x8866
+#define GL_QUERY_RESULT_AVAILABLE 0x8867
+#define GL_BUFFER_MAPPED 0x88BC
+#define GL_BUFFER_MAP_POINTER 0x88BD
+#define GL_STREAM_READ 0x88E1
+#define GL_STREAM_COPY 0x88E2
+#define GL_STATIC_READ 0x88E5
+#define GL_STATIC_COPY 0x88E6
+#define GL_DYNAMIC_READ 0x88E9
+#define GL_DYNAMIC_COPY 0x88EA
+#define GL_MAX_DRAW_BUFFERS 0x8824
+#define GL_DRAW_BUFFER0 0x8825
+#define GL_DRAW_BUFFER1 0x8826
+#define GL_DRAW_BUFFER2 0x8827
+#define GL_DRAW_BUFFER3 0x8828
+#define GL_DRAW_BUFFER4 0x8829
+#define GL_DRAW_BUFFER5 0x882A
+#define GL_DRAW_BUFFER6 0x882B
+#define GL_DRAW_BUFFER7 0x882C
+#define GL_DRAW_BUFFER8 0x882D
+#define GL_DRAW_BUFFER9 0x882E
+#define GL_DRAW_BUFFER10 0x882F
+#define GL_DRAW_BUFFER11 0x8830
+#define GL_DRAW_BUFFER12 0x8831
+#define GL_DRAW_BUFFER13 0x8832
+#define GL_DRAW_BUFFER14 0x8833
+#define GL_DRAW_BUFFER15 0x8834
+#define GL_MAX_FRAGMENT_UNIFORM_COMPONENTS 0x8B49
+#define GL_MAX_VERTEX_UNIFORM_COMPONENTS 0x8B4A
+#define GL_SAMPLER_3D 0x8B5F
+#define GL_SAMPLER_2D_SHADOW 0x8B62
+#define GL_FRAGMENT_SHADER_DERIVATIVE_HINT 0x8B8B
+#define GL_PIXEL_PACK_BUFFER 0x88EB
+#define GL_PIXEL_UNPACK_BUFFER 0x88EC
+#define GL_PIXEL_PACK_BUFFER_BINDING 0x88ED
+#define GL_PIXEL_UNPACK_BUFFER_BINDING 0x88EF
+#define GL_FLOAT_MAT2x3 0x8B65
+#define GL_FLOAT_MAT2x4 0x8B66
+#define GL_FLOAT_MAT3x2 0x8B67
+#define GL_FLOAT_MAT3x4 0x8B68
+#define GL_FLOAT_MAT4x2 0x8B69
+#define GL_FLOAT_MAT4x3 0x8B6A
+#define GL_SRGB 0x8C40
+#define GL_SRGB8 0x8C41
+#define GL_SRGB8_ALPHA8 0x8C43
+#define GL_COMPARE_REF_TO_TEXTURE 0x884E
+#define GL_MAJOR_VERSION 0x821B
+#define GL_MINOR_VERSION 0x821C
+#define GL_NUM_EXTENSIONS 0x821D
+#define GL_RGBA32F 0x8814
+#define GL_RGB32F 0x8815
+#define GL_RGBA16F 0x881A
+#define GL_RGB16F 0x881B
+#define GL_VERTEX_ATTRIB_ARRAY_INTEGER 0x88FD
+#define GL_MAX_ARRAY_TEXTURE_LAYERS 0x88FF
+#define GL_MIN_PROGRAM_TEXEL_OFFSET 0x8904
+#define GL_MAX_PROGRAM_TEXEL_OFFSET 0x8905
+#define GL_MAX_VARYING_COMPONENTS 0x8B4B
+#define GL_TEXTURE_2D_ARRAY 0x8C1A
+#define GL_TEXTURE_BINDING_2D_ARRAY 0x8C1D
+#define GL_R11F_G11F_B10F 0x8C3A
+#define GL_UNSIGNED_INT_10F_11F_11F_REV 0x8C3B
+#define GL_RGB9_E5 0x8C3D
+#define GL_UNSIGNED_INT_5_9_9_9_REV 0x8C3E
+#define GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH 0x8C76
+#define GL_TRANSFORM_FEEDBACK_BUFFER_MODE 0x8C7F
+#define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS 0x8C80
+#define GL_TRANSFORM_FEEDBACK_VARYINGS 0x8C83
+#define GL_TRANSFORM_FEEDBACK_BUFFER_START 0x8C84
+#define GL_TRANSFORM_FEEDBACK_BUFFER_SIZE 0x8C85
+#define GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN 0x8C88
+#define GL_RASTERIZER_DISCARD 0x8C89
+#define GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS 0x8C8A
+#define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS 0x8C8B
+#define GL_INTERLEAVED_ATTRIBS 0x8C8C
+#define GL_SEPARATE_ATTRIBS 0x8C8D
+#define GL_TRANSFORM_FEEDBACK_BUFFER 0x8C8E
+#define GL_TRANSFORM_FEEDBACK_BUFFER_BINDING 0x8C8F
+#define GL_RGBA32UI 0x8D70
+#define GL_RGB32UI 0x8D71
+#define GL_RGBA16UI 0x8D76
+#define GL_RGB16UI 0x8D77
+#define GL_RGBA8UI 0x8D7C
+#define GL_RGB8UI 0x8D7D
+#define GL_RGBA32I 0x8D82
+#define GL_RGB32I 0x8D83
+#define GL_RGBA16I 0x8D88
+#define GL_RGB16I 0x8D89
+#define GL_RGBA8I 0x8D8E
+#define GL_RGB8I 0x8D8F
+#define GL_RED_INTEGER 0x8D94
+#define GL_RGB_INTEGER 0x8D98
+#define GL_RGBA_INTEGER 0x8D99
+#define GL_SAMPLER_2D_ARRAY 0x8DC1
+#define GL_SAMPLER_2D_ARRAY_SHADOW 0x8DC4
+#define GL_SAMPLER_CUBE_SHADOW 0x8DC5
+#define GL_UNSIGNED_INT_VEC2 0x8DC6
+#define GL_UNSIGNED_INT_VEC3 0x8DC7
+#define GL_UNSIGNED_INT_VEC4 0x8DC8
+#define GL_INT_SAMPLER_2D 0x8DCA
+#define GL_INT_SAMPLER_3D 0x8DCB
+#define GL_INT_SAMPLER_CUBE 0x8DCC
+#define GL_INT_SAMPLER_2D_ARRAY 0x8DCF
+#define GL_UNSIGNED_INT_SAMPLER_2D 0x8DD2
+#define GL_UNSIGNED_INT_SAMPLER_3D 0x8DD3
+#define GL_UNSIGNED_INT_SAMPLER_CUBE 0x8DD4
+#define GL_UNSIGNED_INT_SAMPLER_2D_ARRAY 0x8DD7
+#define GL_BUFFER_ACCESS_FLAGS 0x911F
+#define GL_BUFFER_MAP_LENGTH 0x9120
+#define GL_BUFFER_MAP_OFFSET 0x9121
+#define GL_DEPTH_COMPONENT32F 0x8CAC
+#define GL_DEPTH32F_STENCIL8 0x8CAD
+#define GL_FLOAT_32_UNSIGNED_INT_24_8_REV 0x8DAD
+#define GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING 0x8210
+#define GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE 0x8211
+#define GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE 0x8212
+#define GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE 0x8213
+#define GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE 0x8214
+#define GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE 0x8215
+#define GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE 0x8216
+#define GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE 0x8217
+#define GL_FRAMEBUFFER_DEFAULT 0x8218
+#define GL_FRAMEBUFFER_UNDEFINED 0x8219
+#define GL_DEPTH_STENCIL_ATTACHMENT 0x821A
+#define GL_DEPTH_STENCIL 0x84F9
+#define GL_UNSIGNED_INT_24_8 0x84FA
+#define GL_DEPTH24_STENCIL8 0x88F0
+#define GL_UNSIGNED_NORMALIZED 0x8C17
+#define GL_DRAW_FRAMEBUFFER_BINDING 0x8CA6
+#define GL_READ_FRAMEBUFFER 0x8CA8
+#define GL_DRAW_FRAMEBUFFER 0x8CA9
+#define GL_READ_FRAMEBUFFER_BINDING 0x8CAA
+#define GL_RENDERBUFFER_SAMPLES 0x8CAB
+#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER 0x8CD4
+#define GL_MAX_COLOR_ATTACHMENTS 0x8CDF
+#define GL_COLOR_ATTACHMENT1 0x8CE1
+#define GL_COLOR_ATTACHMENT2 0x8CE2
+#define GL_COLOR_ATTACHMENT3 0x8CE3
+#define GL_COLOR_ATTACHMENT4 0x8CE4
+#define GL_COLOR_ATTACHMENT5 0x8CE5
+#define GL_COLOR_ATTACHMENT6 0x8CE6
+#define GL_COLOR_ATTACHMENT7 0x8CE7
+#define GL_COLOR_ATTACHMENT8 0x8CE8
+#define GL_COLOR_ATTACHMENT9 0x8CE9
+#define GL_COLOR_ATTACHMENT10 0x8CEA
+#define GL_COLOR_ATTACHMENT11 0x8CEB
+#define GL_COLOR_ATTACHMENT12 0x8CEC
+#define GL_COLOR_ATTACHMENT13 0x8CED
+#define GL_COLOR_ATTACHMENT14 0x8CEE
+#define GL_COLOR_ATTACHMENT15 0x8CEF
+#define GL_COLOR_ATTACHMENT16 0x8CF0
+#define GL_COLOR_ATTACHMENT17 0x8CF1
+#define GL_COLOR_ATTACHMENT18 0x8CF2
+#define GL_COLOR_ATTACHMENT19 0x8CF3
+#define GL_COLOR_ATTACHMENT20 0x8CF4
+#define GL_COLOR_ATTACHMENT21 0x8CF5
+#define GL_COLOR_ATTACHMENT22 0x8CF6
+#define GL_COLOR_ATTACHMENT23 0x8CF7
+#define GL_COLOR_ATTACHMENT24 0x8CF8
+#define GL_COLOR_ATTACHMENT25 0x8CF9
+#define GL_COLOR_ATTACHMENT26 0x8CFA
+#define GL_COLOR_ATTACHMENT27 0x8CFB
+#define GL_COLOR_ATTACHMENT28 0x8CFC
+#define GL_COLOR_ATTACHMENT29 0x8CFD
+#define GL_COLOR_ATTACHMENT30 0x8CFE
+#define GL_COLOR_ATTACHMENT31 0x8CFF
+#define GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE 0x8D56
+#define GL_MAX_SAMPLES 0x8D57
+#define GL_HALF_FLOAT 0x140B
+#define GL_MAP_READ_BIT 0x0001
+#define GL_MAP_WRITE_BIT 0x0002
+#define GL_MAP_INVALIDATE_RANGE_BIT 0x0004
+#define GL_MAP_INVALIDATE_BUFFER_BIT 0x0008
+#define GL_MAP_FLUSH_EXPLICIT_BIT 0x0010
+#define GL_MAP_UNSYNCHRONIZED_BIT 0x0020
+#define GL_RG 0x8227
+#define GL_RG_INTEGER 0x8228
+#define GL_R8 0x8229
+#define GL_RG8 0x822B
+#define GL_R16F 0x822D
+#define GL_R32F 0x822E
+#define GL_RG16F 0x822F
+#define GL_RG32F 0x8230
+#define GL_R8I 0x8231
+#define GL_R8UI 0x8232
+#define GL_R16I 0x8233
+#define GL_R16UI 0x8234
+#define GL_R32I 0x8235
+#define GL_R32UI 0x8236
+#define GL_RG8I 0x8237
+#define GL_RG8UI 0x8238
+#define GL_RG16I 0x8239
+#define GL_RG16UI 0x823A
+#define GL_RG32I 0x823B
+#define GL_RG32UI 0x823C
+#define GL_VERTEX_ARRAY_BINDING 0x85B5
+#define GL_R8_SNORM 0x8F94
+#define GL_RG8_SNORM 0x8F95
+#define GL_RGB8_SNORM 0x8F96
+#define GL_RGBA8_SNORM 0x8F97
+#define GL_SIGNED_NORMALIZED 0x8F9C
+#define GL_PRIMITIVE_RESTART_FIXED_INDEX 0x8D69
+#define GL_COPY_READ_BUFFER 0x8F36
+#define GL_COPY_WRITE_BUFFER 0x8F37
+#define GL_COPY_READ_BUFFER_BINDING 0x8F36
+#define GL_COPY_WRITE_BUFFER_BINDING 0x8F37
+#define GL_UNIFORM_BUFFER 0x8A11
+#define GL_UNIFORM_BUFFER_BINDING 0x8A28
+#define GL_UNIFORM_BUFFER_START 0x8A29
+#define GL_UNIFORM_BUFFER_SIZE 0x8A2A
+#define GL_MAX_VERTEX_UNIFORM_BLOCKS 0x8A2B
+#define GL_MAX_FRAGMENT_UNIFORM_BLOCKS 0x8A2D
+#define GL_MAX_COMBINED_UNIFORM_BLOCKS 0x8A2E
+#define GL_MAX_UNIFORM_BUFFER_BINDINGS 0x8A2F
+#define GL_MAX_UNIFORM_BLOCK_SIZE 0x8A30
+#define GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS 0x8A31
+#define GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS 0x8A33
+#define GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT 0x8A34
+#define GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH 0x8A35
+#define GL_ACTIVE_UNIFORM_BLOCKS 0x8A36
+#define GL_UNIFORM_TYPE 0x8A37
+#define GL_UNIFORM_SIZE 0x8A38
+#define GL_UNIFORM_NAME_LENGTH 0x8A39
+#define GL_UNIFORM_BLOCK_INDEX 0x8A3A
+#define GL_UNIFORM_OFFSET 0x8A3B
+#define GL_UNIFORM_ARRAY_STRIDE 0x8A3C
+#define GL_UNIFORM_MATRIX_STRIDE 0x8A3D
+#define GL_UNIFORM_IS_ROW_MAJOR 0x8A3E
+#define GL_UNIFORM_BLOCK_BINDING 0x8A3F
+#define GL_UNIFORM_BLOCK_DATA_SIZE 0x8A40
+#define GL_UNIFORM_BLOCK_NAME_LENGTH 0x8A41
+#define GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS 0x8A42
+#define GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES 0x8A43
+#define GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER 0x8A44
+#define GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER 0x8A46
+#define GL_INVALID_INDEX 0xFFFFFFFFu
+#define GL_MAX_VERTEX_OUTPUT_COMPONENTS 0x9122
+#define GL_MAX_FRAGMENT_INPUT_COMPONENTS 0x9125
+#define GL_MAX_SERVER_WAIT_TIMEOUT 0x9111
+#define GL_OBJECT_TYPE 0x9112
+#define GL_SYNC_CONDITION 0x9113
+#define GL_SYNC_STATUS 0x9114
+#define GL_SYNC_FLAGS 0x9115
+#define GL_SYNC_FENCE 0x9116
+#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
+#define GL_UNSIGNALED 0x9118
+#define GL_SIGNALED 0x9119
+#define GL_ALREADY_SIGNALED 0x911A
+#define GL_TIMEOUT_EXPIRED 0x911B
+#define GL_CONDITION_SATISFIED 0x911C
+#define GL_WAIT_FAILED 0x911D
+#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
+#define GL_TIMEOUT_IGNORED 0xFFFFFFFFFFFFFFFFull
+#define GL_VERTEX_ATTRIB_ARRAY_DIVISOR 0x88FE
+#define GL_ANY_SAMPLES_PASSED 0x8C2F
+#define GL_ANY_SAMPLES_PASSED_CONSERVATIVE 0x8D6A
+#define GL_SAMPLER_BINDING 0x8919
+#define GL_RGB10_A2UI 0x906F
+#define GL_TEXTURE_SWIZZLE_R 0x8E42
+#define GL_TEXTURE_SWIZZLE_G 0x8E43
+#define GL_TEXTURE_SWIZZLE_B 0x8E44
+#define GL_TEXTURE_SWIZZLE_A 0x8E45
+#define GL_GREEN 0x1904
+#define GL_BLUE 0x1905
+#define GL_INT_2_10_10_10_REV 0x8D9F
+#define GL_TRANSFORM_FEEDBACK 0x8E22
+#define GL_TRANSFORM_FEEDBACK_PAUSED 0x8E23
+#define GL_TRANSFORM_FEEDBACK_ACTIVE 0x8E24
+#define GL_TRANSFORM_FEEDBACK_BINDING 0x8E25
+#define GL_PROGRAM_BINARY_RETRIEVABLE_HINT 0x8257
+#define GL_PROGRAM_BINARY_LENGTH 0x8741
+#define GL_NUM_PROGRAM_BINARY_FORMATS 0x87FE
+#define GL_PROGRAM_BINARY_FORMATS 0x87FF
+#define GL_COMPRESSED_R11_EAC 0x9270
+#define GL_COMPRESSED_SIGNED_R11_EAC 0x9271
+#define GL_COMPRESSED_RG11_EAC 0x9272
+#define GL_COMPRESSED_SIGNED_RG11_EAC 0x9273
+#define GL_COMPRESSED_RGB8_ETC2 0x9274
+#define GL_COMPRESSED_SRGB8_ETC2 0x9275
+#define GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9276
+#define GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9277
+#define GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278
+#define GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC 0x9279
+#define GL_TEXTURE_IMMUTABLE_FORMAT 0x912F
+#define GL_MAX_ELEMENT_INDEX 0x8D6B
+#define GL_NUM_SAMPLE_COUNTS 0x9380
+#define GL_TEXTURE_IMMUTABLE_LEVELS 0x82DF
+
+#endif
diff --git a/video/out/opengl/hwdec_d3d11egl.c b/video/out/opengl/hwdec_d3d11egl.c
new file mode 100644
index 0000000..c312091
--- /dev/null
+++ b/video/out/opengl/hwdec_d3d11egl.c
@@ -0,0 +1,363 @@
+/*
+ * 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 <assert.h>
+#include <windows.h>
+#include <d3d11.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "angle_dynamic.h"
+
+#include "common/common.h"
+#include "osdep/timer.h"
+#include "osdep/windows_utils.h"
+#include "video/out/gpu/hwdec.h"
+#include "ra_gl.h"
+#include "video/hwdec.h"
+#include "video/d3d.h"
+
+#ifndef EGL_D3D_TEXTURE_SUBRESOURCE_ID_ANGLE
+#define EGL_D3D_TEXTURE_SUBRESOURCE_ID_ANGLE 0x33AB
+#endif
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+
+ ID3D11Device *d3d11_device;
+ EGLDisplay egl_display;
+
+ // EGL_KHR_stream
+ EGLStreamKHR (EGLAPIENTRY *CreateStreamKHR)(EGLDisplay dpy,
+ const EGLint *attrib_list);
+ EGLBoolean (EGLAPIENTRY *DestroyStreamKHR)(EGLDisplay dpy,
+ EGLStreamKHR stream);
+
+ // EGL_KHR_stream_consumer_gltexture
+ EGLBoolean (EGLAPIENTRY *StreamConsumerAcquireKHR)
+ (EGLDisplay dpy, EGLStreamKHR stream);
+ EGLBoolean (EGLAPIENTRY *StreamConsumerReleaseKHR)
+ (EGLDisplay dpy, EGLStreamKHR stream);
+
+ // EGL_NV_stream_consumer_gltexture_yuv
+ EGLBoolean (EGLAPIENTRY *StreamConsumerGLTextureExternalAttribsNV)
+ (EGLDisplay dpy, EGLStreamKHR stream, EGLAttrib *attrib_list);
+
+ // EGL_ANGLE_stream_producer_d3d_texture
+ EGLBoolean (EGLAPIENTRY *CreateStreamProducerD3DTextureANGLE)
+ (EGLDisplay dpy, EGLStreamKHR stream, const EGLAttrib *attrib_list);
+ EGLBoolean (EGLAPIENTRY *StreamPostD3DTextureANGLE)
+ (EGLDisplay dpy, EGLStreamKHR stream, void *texture,
+ const EGLAttrib *attrib_list);
+};
+
+struct priv {
+ EGLStreamKHR egl_stream;
+ GLuint gl_textures[2];
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+
+ if (p->d3d11_device)
+ ID3D11Device_Release(p->d3d11_device);
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ HRESULT hr;
+
+ if (!ra_is_gl(hw->ra_ctx->ra))
+ return -1;
+ if (!angle_load())
+ return -1;
+
+ EGLDisplay egl_display = eglGetCurrentDisplay();
+ if (!egl_display)
+ return -1;
+
+ if (!eglGetCurrentContext())
+ return -1;
+
+ GL *gl = ra_gl_get(hw->ra_ctx->ra);
+
+ const char *exts = eglQueryString(egl_display, EGL_EXTENSIONS);
+ if (!gl_check_extension(exts, "EGL_ANGLE_d3d_share_handle_client_buffer") ||
+ !gl_check_extension(exts, "EGL_ANGLE_stream_producer_d3d_texture") ||
+ !(gl_check_extension(gl->extensions, "GL_OES_EGL_image_external_essl3") ||
+ gl->es == 200) ||
+ !gl_check_extension(exts, "EGL_EXT_device_query") ||
+ !(gl->mpgl_caps & MPGL_CAP_TEX_RG))
+ return -1;
+
+ p->egl_display = egl_display;
+
+ p->CreateStreamKHR = (void *)eglGetProcAddress("eglCreateStreamKHR");
+ p->DestroyStreamKHR = (void *)eglGetProcAddress("eglDestroyStreamKHR");
+ p->StreamConsumerAcquireKHR =
+ (void *)eglGetProcAddress("eglStreamConsumerAcquireKHR");
+ p->StreamConsumerReleaseKHR =
+ (void *)eglGetProcAddress("eglStreamConsumerReleaseKHR");
+ p->StreamConsumerGLTextureExternalAttribsNV =
+ (void *)eglGetProcAddress("eglStreamConsumerGLTextureExternalAttribsNV");
+ p->CreateStreamProducerD3DTextureANGLE =
+ (void *)eglGetProcAddress("eglCreateStreamProducerD3DTextureANGLE");
+ p->StreamPostD3DTextureANGLE =
+ (void *)eglGetProcAddress("eglStreamPostD3DTextureANGLE");
+
+ if (!p->CreateStreamKHR || !p->DestroyStreamKHR ||
+ !p->StreamConsumerAcquireKHR || !p->StreamConsumerReleaseKHR ||
+ !p->StreamConsumerGLTextureExternalAttribsNV ||
+ !p->CreateStreamProducerD3DTextureANGLE ||
+ !p->StreamPostD3DTextureANGLE)
+ {
+ MP_ERR(hw, "Failed to load some EGLStream functions.\n");
+ goto fail;
+ }
+
+ static const char *es2_exts[] = {"GL_NV_EGL_stream_consumer_external", 0};
+ static const char *es3_exts[] = {"GL_NV_EGL_stream_consumer_external",
+ "GL_OES_EGL_image_external_essl3", 0};
+ hw->glsl_extensions = gl->es == 200 ? es2_exts : es3_exts;
+
+ PFNEGLQUERYDISPLAYATTRIBEXTPROC p_eglQueryDisplayAttribEXT =
+ (void *)eglGetProcAddress("eglQueryDisplayAttribEXT");
+ PFNEGLQUERYDEVICEATTRIBEXTPROC p_eglQueryDeviceAttribEXT =
+ (void *)eglGetProcAddress("eglQueryDeviceAttribEXT");
+ if (!p_eglQueryDisplayAttribEXT || !p_eglQueryDeviceAttribEXT)
+ goto fail;
+
+ EGLAttrib device = 0;
+ if (!p_eglQueryDisplayAttribEXT(egl_display, EGL_DEVICE_EXT, &device))
+ goto fail;
+ EGLAttrib d3d_device = 0;
+ if (!p_eglQueryDeviceAttribEXT((EGLDeviceEXT)device,
+ EGL_D3D11_DEVICE_ANGLE, &d3d_device))
+ {
+ MP_ERR(hw, "Could not get EGL_D3D11_DEVICE_ANGLE from ANGLE.\n");
+ goto fail;
+ }
+
+ p->d3d11_device = (ID3D11Device *)d3d_device;
+ if (!p->d3d11_device)
+ goto fail;
+ ID3D11Device_AddRef(p->d3d11_device);
+
+ if (!d3d11_check_decoding(p->d3d11_device)) {
+ MP_VERBOSE(hw, "D3D11 video decoding not supported on this system.\n");
+ goto fail;
+ }
+
+ ID3D10Multithread *multithread;
+ hr = ID3D11Device_QueryInterface(p->d3d11_device, &IID_ID3D10Multithread,
+ (void **)&multithread);
+ if (FAILED(hr)) {
+ MP_ERR(hw, "Failed to get Multithread interface: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+ ID3D10Multithread_SetMultithreadProtected(multithread, TRUE);
+ ID3D10Multithread_Release(multithread);
+
+ static const int subfmts[] = {IMGFMT_NV12, IMGFMT_P010, 0};
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = hw->driver->name,
+ .av_device_ref = d3d11_wrap_device_ref(p->d3d11_device),
+ .supported_formats = subfmts,
+ .hw_imgfmt = IMGFMT_D3D11,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx\n");
+ return -1;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ return 0;
+fail:
+ return -1;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *o = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ if (p->egl_stream)
+ o->DestroyStreamKHR(o->egl_display, p->egl_stream);
+ p->egl_stream = 0;
+
+ gl->DeleteTextures(2, p->gl_textures);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *o = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ struct ra_imgfmt_desc desc = {0};
+
+ ra_get_imgfmt_desc(mapper->ra, mapper->src_params.hw_subfmt, &desc);
+
+ // ANGLE hardcodes the list of accepted formats. This is a subset.
+ if ((mapper->src_params.hw_subfmt != IMGFMT_NV12 &&
+ mapper->src_params.hw_subfmt != IMGFMT_P010) ||
+ desc.num_planes < 1 || desc.num_planes > 2)
+ {
+ MP_FATAL(mapper, "Format not supported.\n");
+ return -1;
+ }
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = mapper->src_params.hw_subfmt;
+ mapper->dst_params.hw_subfmt = 0;
+
+ // The texture units need to be bound during init only, and are free for
+ // use again after the initialization here is done.
+ int texunits = 0; // [texunits, texunits + num_planes)
+ int num_planes = desc.num_planes;
+ int gl_target = GL_TEXTURE_EXTERNAL_OES;
+
+ p->egl_stream = o->CreateStreamKHR(o->egl_display, (EGLint[]){EGL_NONE});
+ if (!p->egl_stream)
+ goto fail;
+
+ EGLAttrib attrs[(2 + 2 + 1) * 2] = {
+ EGL_COLOR_BUFFER_TYPE, EGL_YUV_BUFFER_EXT,
+ EGL_YUV_NUMBER_OF_PLANES_EXT, num_planes,
+ };
+
+ for (int n = 0; n < num_planes; n++) {
+ gl->ActiveTexture(GL_TEXTURE0 + texunits + n);
+ gl->GenTextures(1, &p->gl_textures[n]);
+ gl->BindTexture(gl_target, p->gl_textures[n]);
+ gl->TexParameteri(gl_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(gl_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(gl_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(gl_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ attrs[(2 + n) * 2 + 0] = EGL_YUV_PLANE0_TEXTURE_UNIT_NV + n;
+ attrs[(2 + n) * 2 + 1] = texunits + n;
+ }
+
+ attrs[(2 + num_planes) * 2 + 0] = EGL_NONE;
+
+ if (!o->StreamConsumerGLTextureExternalAttribsNV(o->egl_display, p->egl_stream,
+ attrs))
+ goto fail;
+
+ if (!o->CreateStreamProducerD3DTextureANGLE(o->egl_display, p->egl_stream,
+ (EGLAttrib[]){EGL_NONE}))
+ goto fail;
+
+ for (int n = 0; n < num_planes; n++) {
+ gl->ActiveTexture(GL_TEXTURE0 + texunits + n);
+ gl->BindTexture(gl_target, 0);
+ }
+ gl->ActiveTexture(GL_TEXTURE0);
+ return 0;
+fail:
+ gl->ActiveTexture(GL_TEXTURE0);
+ MP_ERR(mapper, "Failed to create EGLStream\n");
+ return -1;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *o = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+
+ ID3D11Texture2D *d3d_tex = (void *)mapper->src->planes[0];
+ int d3d_subindex = (intptr_t)mapper->src->planes[1];
+ if (!d3d_tex)
+ return -1;
+
+ EGLAttrib attrs[] = {
+ EGL_D3D_TEXTURE_SUBRESOURCE_ID_ANGLE, d3d_subindex,
+ EGL_NONE,
+ };
+ if (!o->StreamPostD3DTextureANGLE(o->egl_display, p->egl_stream,
+ (void *)d3d_tex, attrs))
+ {
+ // ANGLE changed the enum ID of this without warning at one point.
+ attrs[0] = attrs[0] == 0x33AB ? 0x3AAB : 0x33AB;
+ if (!o->StreamPostD3DTextureANGLE(o->egl_display, p->egl_stream,
+ (void *)d3d_tex, attrs))
+ return -1;
+ }
+
+ if (!o->StreamConsumerAcquireKHR(o->egl_display, p->egl_stream))
+ return -1;
+
+ D3D11_TEXTURE2D_DESC texdesc;
+ ID3D11Texture2D_GetDesc(d3d_tex, &texdesc);
+
+ for (int n = 0; n < 2; n++) {
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = texdesc.Width / (n ? 2 : 1),
+ .h = texdesc.Height / (n ? 2 : 1),
+ .d = 1,
+ .format = ra_find_unorm_format(mapper->ra, 1, n ? 2 : 1),
+ .render_src = true,
+ .src_linear = true,
+ .external_oes = true,
+ };
+ if (!params.format)
+ return -1;
+
+ mapper->tex[n] = ra_create_wrapped_tex(mapper->ra, &params,
+ p->gl_textures[n]);
+ if (!mapper->tex[n])
+ return -1;
+ }
+
+ return 0;
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *o = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+
+ for (int n = 0; n < 2; n++)
+ ra_tex_free(mapper->ra, &mapper->tex[n]);
+ if (p->egl_stream)
+ o->StreamConsumerReleaseKHR(o->egl_display, p->egl_stream);
+}
+
+const struct ra_hwdec_driver ra_hwdec_d3d11egl = {
+ .name = "d3d11-egl",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_D3D11, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/opengl/hwdec_dxva2egl.c b/video/out/opengl/hwdec_dxva2egl.c
new file mode 100644
index 0000000..979ef59
--- /dev/null
+++ b/video/out/opengl/hwdec_dxva2egl.c
@@ -0,0 +1,384 @@
+/*
+ * 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 <assert.h>
+#include <windows.h>
+#include <d3d9.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include "angle_dynamic.h"
+
+#include "common/common.h"
+#include "osdep/timer.h"
+#include "osdep/windows_utils.h"
+#include "video/out/gpu/hwdec.h"
+#include "ra_gl.h"
+#include "video/hwdec.h"
+#include "video/d3d.h"
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+ IDirect3D9Ex *d3d9ex;
+ IDirect3DDevice9Ex *device9ex;
+
+ EGLDisplay egl_display;
+ EGLConfig egl_config;
+ EGLint alpha;
+};
+
+struct priv {
+ IDirect3DDevice9Ex *device9ex; // (no own reference)
+ IDirect3DQuery9 *query9;
+ IDirect3DTexture9 *texture9;
+ IDirect3DSurface9 *surface9;
+
+ EGLDisplay egl_display;
+ EGLSurface egl_surface;
+
+ GLuint gl_texture;
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+
+ if (p->device9ex)
+ IDirect3DDevice9Ex_Release(p->device9ex);
+
+ if (p->d3d9ex)
+ IDirect3D9Ex_Release(p->d3d9ex);
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ HRESULT hr;
+
+ if (!ra_is_gl(hw->ra_ctx->ra))
+ return -1;
+ if (!angle_load())
+ return -1;
+
+ d3d_load_dlls();
+
+ EGLDisplay egl_display = eglGetCurrentDisplay();
+ if (!egl_display)
+ return -1;
+
+ if (!eglGetCurrentContext())
+ return -1;
+
+ const char *exts = eglQueryString(egl_display, EGL_EXTENSIONS);
+ if (!gl_check_extension(exts, "EGL_ANGLE_d3d_share_handle_client_buffer")) {
+ return -1;
+ }
+
+ p->egl_display = egl_display;
+
+ if (!d3d9_dll) {
+ MP_FATAL(hw, "Failed to load \"d3d9.dll\": %s\n",
+ mp_LastError_to_str());
+ goto fail;
+ }
+
+ HRESULT (WINAPI *Direct3DCreate9Ex)(UINT SDKVersion, IDirect3D9Ex **ppD3D);
+ Direct3DCreate9Ex = (void *)GetProcAddress(d3d9_dll, "Direct3DCreate9Ex");
+ if (!Direct3DCreate9Ex) {
+ MP_FATAL(hw, "Direct3D 9Ex not supported\n");
+ goto fail;
+ }
+
+ hr = Direct3DCreate9Ex(D3D_SDK_VERSION, &p->d3d9ex);
+ if (FAILED(hr)) {
+ MP_FATAL(hw, "Couldn't create Direct3D9Ex: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+
+ // We must create our own Direct3D9Ex device. ANGLE can give us the device
+ // it's using, but that's probably a ID3D11Device.
+ // (copied from chromium dxva_video_decode_accelerator_win.cc)
+ D3DPRESENT_PARAMETERS present_params = {
+ .BackBufferWidth = 1,
+ .BackBufferHeight = 1,
+ .BackBufferFormat = D3DFMT_UNKNOWN,
+ .BackBufferCount = 1,
+ .SwapEffect = D3DSWAPEFFECT_DISCARD,
+ .hDeviceWindow = NULL,
+ .Windowed = TRUE,
+ .Flags = D3DPRESENTFLAG_VIDEO,
+ .FullScreen_RefreshRateInHz = 0,
+ .PresentationInterval = 0,
+ };
+ hr = IDirect3D9Ex_CreateDeviceEx(p->d3d9ex,
+ D3DADAPTER_DEFAULT,
+ D3DDEVTYPE_HAL,
+ NULL,
+ D3DCREATE_FPU_PRESERVE |
+ D3DCREATE_HARDWARE_VERTEXPROCESSING |
+ D3DCREATE_DISABLE_PSGP_THREADING |
+ D3DCREATE_MULTITHREADED,
+ &present_params,
+ NULL,
+ &p->device9ex);
+ if (FAILED(hr)) {
+ MP_FATAL(hw, "Failed to create Direct3D9Ex device: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+
+ EGLint attrs[] = {
+ EGL_BUFFER_SIZE, 32,
+ EGL_RED_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_BLUE_SIZE, 8,
+ EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
+ EGL_ALPHA_SIZE, 0,
+ EGL_NONE
+ };
+ EGLint count;
+ if (!eglChooseConfig(p->egl_display, attrs, &p->egl_config, 1, &count) ||
+ !count) {
+ MP_ERR(hw, "Failed to get EGL surface configuration\n");
+ goto fail;
+ }
+
+ if (!eglGetConfigAttrib(p->egl_display, p->egl_config,
+ EGL_BIND_TO_TEXTURE_RGBA, &p->alpha)) {
+ MP_FATAL(hw, "Failed to query EGL surface alpha\n");
+ goto fail;
+ }
+
+ struct mp_image_params dummy_params = {
+ .imgfmt = IMGFMT_DXVA2,
+ .w = 256,
+ .h = 256,
+ };
+ struct ra_hwdec_mapper *mapper = ra_hwdec_mapper_create(hw, &dummy_params);
+ if (!mapper)
+ goto fail;
+ ra_hwdec_mapper_free(&mapper);
+
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = hw->driver->name,
+ .av_device_ref = d3d9_wrap_device_ref((IDirect3DDevice9 *)p->device9ex),
+ .hw_imgfmt = IMGFMT_DXVA2,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx\n");
+ goto fail;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+
+ return 0;
+fail:
+ return -1;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ ra_tex_free(mapper->ra, &mapper->tex[0]);
+ gl->DeleteTextures(1, &p->gl_texture);
+
+ if (p->egl_display && p->egl_surface) {
+ eglReleaseTexImage(p->egl_display, p->egl_surface, EGL_BACK_BUFFER);
+ eglDestroySurface(p->egl_display, p->egl_surface);
+ }
+
+ if (p->surface9)
+ IDirect3DSurface9_Release(p->surface9);
+
+ if (p->texture9)
+ IDirect3DTexture9_Release(p->texture9);
+
+ if (p->query9)
+ IDirect3DQuery9_Release(p->query9);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+ HRESULT hr;
+
+ p->device9ex = p_owner->device9ex;
+ p->egl_display = p_owner->egl_display;
+
+ hr = IDirect3DDevice9_CreateQuery(p->device9ex, D3DQUERYTYPE_EVENT,
+ &p->query9);
+ if (FAILED(hr)) {
+ MP_FATAL(mapper, "Failed to create Direct3D query interface: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+
+ // Test the query API
+ hr = IDirect3DQuery9_Issue(p->query9, D3DISSUE_END);
+ if (FAILED(hr)) {
+ MP_FATAL(mapper, "Failed to issue Direct3D END test query: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+
+ HANDLE share_handle = NULL;
+ hr = IDirect3DDevice9Ex_CreateTexture(p->device9ex,
+ mapper->src_params.w,
+ mapper->src_params.h,
+ 1, D3DUSAGE_RENDERTARGET,
+ p_owner->alpha ?
+ D3DFMT_A8R8G8B8 : D3DFMT_X8R8G8B8,
+ D3DPOOL_DEFAULT,
+ &p->texture9,
+ &share_handle);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to create Direct3D9 texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+
+ hr = IDirect3DTexture9_GetSurfaceLevel(p->texture9, 0, &p->surface9);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to get Direct3D9 surface from texture: %s\n",
+ mp_HRESULT_to_str(hr));
+ goto fail;
+ }
+
+ EGLint attrib_list[] = {
+ EGL_WIDTH, mapper->src_params.w,
+ EGL_HEIGHT, mapper->src_params.h,
+ EGL_TEXTURE_FORMAT, p_owner->alpha ? EGL_TEXTURE_RGBA : EGL_TEXTURE_RGB,
+ EGL_TEXTURE_TARGET, EGL_TEXTURE_2D,
+ EGL_NONE
+ };
+ p->egl_surface = eglCreatePbufferFromClientBuffer(
+ p->egl_display, EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE,
+ share_handle, p_owner->egl_config, attrib_list);
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ MP_ERR(mapper, "Failed to create EGL surface\n");
+ goto fail;
+ }
+
+ gl->GenTextures(1, &p->gl_texture);
+ gl->BindTexture(GL_TEXTURE_2D, p->gl_texture);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = mapper->src_params.w,
+ .h = mapper->src_params.h,
+ .d = 1,
+ .format = ra_find_unorm_format(mapper->ra, 1, p_owner->alpha ? 4 : 3),
+ .render_src = true,
+ .src_linear = true,
+ };
+ if (!params.format)
+ goto fail;
+
+ mapper->tex[0] = ra_create_wrapped_tex(mapper->ra, &params, p->gl_texture);
+ if (!mapper->tex[0])
+ goto fail;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = IMGFMT_RGB0;
+ mapper->dst_params.hw_subfmt = 0;
+ return 0;
+fail:
+ return -1;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ HRESULT hr;
+ RECT rc = {0, 0, mapper->src->w, mapper->src->h};
+ IDirect3DSurface9* hw_surface = (IDirect3DSurface9 *)mapper->src->planes[3];
+ hr = IDirect3DDevice9Ex_StretchRect(p->device9ex,
+ hw_surface, &rc,
+ p->surface9, &rc,
+ D3DTEXF_NONE);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Direct3D RGB conversion failed: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ hr = IDirect3DQuery9_Issue(p->query9, D3DISSUE_END);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to issue Direct3D END query\n");
+ return -1;
+ }
+
+ // There doesn't appear to be an efficient way to do a blocking flush
+ // of the above StretchRect. Timeout of 8ms is required to reliably
+ // render 4k on Intel Haswell, Ivybridge and Cherry Trail Atom.
+ const int max_retries = 8;
+ const int64_t wait_ns = MP_TIME_MS_TO_NS(1);
+ int retries = 0;
+ while (true) {
+ hr = IDirect3DQuery9_GetData(p->query9, NULL, 0, D3DGETDATA_FLUSH);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed to query Direct3D flush state\n");
+ return -1;
+ } else if (hr == S_FALSE) {
+ if (++retries > max_retries) {
+ MP_VERBOSE(mapper, "Failed to flush frame after %lld ms\n",
+ (long long)MP_TIME_MS_TO_NS(wait_ns * max_retries));
+ break;
+ }
+ mp_sleep_ns(wait_ns);
+ } else {
+ break;
+ }
+ }
+
+ gl->BindTexture(GL_TEXTURE_2D, p->gl_texture);
+ eglBindTexImage(p->egl_display, p->egl_surface, EGL_BACK_BUFFER);
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+
+ return 0;
+}
+
+const struct ra_hwdec_driver ra_hwdec_dxva2egl = {
+ .name = "dxva2-egl",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_DXVA2, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ },
+};
diff --git a/video/out/opengl/hwdec_dxva2gldx.c b/video/out/opengl/hwdec_dxva2gldx.c
new file mode 100644
index 0000000..0172813
--- /dev/null
+++ b/video/out/opengl/hwdec_dxva2gldx.c
@@ -0,0 +1,247 @@
+/*
+ * 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 <d3d9.h>
+#include <assert.h>
+
+#include "common/common.h"
+#include "osdep/windows_utils.h"
+#include "video/out/gpu/hwdec.h"
+#include "ra_gl.h"
+#include "video/hwdec.h"
+#include "video/d3d.h"
+
+// for WGL_ACCESS_READ_ONLY_NV
+#include <GL/wglext.h>
+
+#define SHARED_SURFACE_D3DFMT D3DFMT_X8R8G8B8
+
+struct priv_owner {
+ struct mp_hwdec_ctx hwctx;
+ IDirect3DDevice9Ex *device;
+ HANDLE device_h;
+};
+
+struct priv {
+ IDirect3DDevice9Ex *device;
+ HANDLE device_h;
+ IDirect3DSurface9 *rtarget;
+ HANDLE rtarget_h;
+ GLuint texture;
+};
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ hwdec_devices_remove(hw->devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+
+ if (p->device)
+ IDirect3DDevice9Ex_Release(p->device);
+}
+
+static int init(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+ struct ra *ra = hw->ra_ctx->ra;
+
+ if (!ra_is_gl(ra))
+ return -1;
+ GL *gl = ra_gl_get(ra);
+ if (!(gl->mpgl_caps & MPGL_CAP_DXINTEROP))
+ return -1;
+
+ // AMD drivers won't open multiple dxinterop HANDLES on the same D3D device,
+ // so we request the one already in use by context_dxinterop
+ p->device_h = ra_get_native_resource(ra, "dxinterop_device_HANDLE");
+ if (!p->device_h)
+ return -1;
+
+ // But we also still need the actual D3D device
+ p->device = ra_get_native_resource(ra, "IDirect3DDevice9Ex");
+ if (!p->device)
+ return -1;
+ IDirect3DDevice9Ex_AddRef(p->device);
+
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = hw->driver->name,
+ .av_device_ref = d3d9_wrap_device_ref((IDirect3DDevice9 *)p->device),
+ .hw_imgfmt = IMGFMT_DXVA2,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(hw, "Failed to create hwdevice_ctx\n");
+ return -1;
+ }
+
+ hwdec_devices_add(hw->devs, &p->hwctx);
+ return 0;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+
+ if (p->rtarget_h && p->device_h) {
+ if (!gl->DXUnlockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(mapper, "Failed unlocking texture for access by OpenGL: %s\n",
+ mp_LastError_to_str());
+ }
+ }
+
+ if (p->rtarget_h) {
+ if (!gl->DXUnregisterObjectNV(p->device_h, p->rtarget_h)) {
+ MP_ERR(mapper, "Failed to unregister Direct3D surface with OpenGL: %s\n",
+ mp_LastError_to_str());
+ } else {
+ p->rtarget_h = 0;
+ }
+ }
+
+ gl->DeleteTextures(1, &p->texture);
+ p->texture = 0;
+
+ if (p->rtarget) {
+ IDirect3DSurface9_Release(p->rtarget);
+ p->rtarget = NULL;
+ }
+
+ ra_tex_free(mapper->ra, &mapper->tex[0]);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+ HRESULT hr;
+
+ p->device = p_owner->device;
+ p->device_h = p_owner->device_h;
+
+ HANDLE share_handle = NULL;
+ hr = IDirect3DDevice9Ex_CreateRenderTarget(
+ p->device,
+ mapper->src_params.w, mapper->src_params.h,
+ SHARED_SURFACE_D3DFMT, D3DMULTISAMPLE_NONE, 0, FALSE,
+ &p->rtarget, &share_handle);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Failed creating offscreen Direct3D surface: %s\n",
+ mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ if (share_handle &&
+ !gl->DXSetResourceShareHandleNV(p->rtarget, share_handle)) {
+ MP_ERR(mapper, "Failed setting Direct3D/OpenGL share handle for surface: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ gl->GenTextures(1, &p->texture);
+ gl->BindTexture(GL_TEXTURE_2D, p->texture);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+
+ p->rtarget_h = gl->DXRegisterObjectNV(p->device_h, p->rtarget, p->texture,
+ GL_TEXTURE_2D,
+ WGL_ACCESS_READ_ONLY_NV);
+ if (!p->rtarget_h) {
+ MP_ERR(mapper, "Failed to register Direct3D surface with OpenGL: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ if (!gl->DXLockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(mapper, "Failed locking texture for access by OpenGL %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = mapper->src_params.w,
+ .h = mapper->src_params.h,
+ .d = 1,
+ .format = ra_find_unorm_format(mapper->ra, 1, 4),
+ .render_src = true,
+ .src_linear = true,
+ };
+ if (!params.format)
+ return -1;
+
+ mapper->tex[0] = ra_create_wrapped_tex(mapper->ra, &params, p->texture);
+ if (!mapper->tex[0])
+ return -1;
+
+ mapper->dst_params = mapper->src_params;
+ mapper->dst_params.imgfmt = IMGFMT_RGB0;
+ mapper->dst_params.hw_subfmt = 0;
+
+ return 0;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = ra_gl_get(mapper->ra);
+ HRESULT hr;
+
+ if (!gl->DXUnlockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(mapper, "Failed unlocking texture for access by OpenGL: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ IDirect3DSurface9* hw_surface = (IDirect3DSurface9 *)mapper->src->planes[3];
+ RECT rc = {0, 0, mapper->src->w, mapper->src->h};
+ hr = IDirect3DDevice9Ex_StretchRect(p->device,
+ hw_surface, &rc,
+ p->rtarget, &rc,
+ D3DTEXF_NONE);
+ if (FAILED(hr)) {
+ MP_ERR(mapper, "Direct3D RGB conversion failed: %s", mp_HRESULT_to_str(hr));
+ return -1;
+ }
+
+ if (!gl->DXLockObjectsNV(p->device_h, 1, &p->rtarget_h)) {
+ MP_ERR(mapper, "Failed locking texture for access by OpenGL: %s\n",
+ mp_LastError_to_str());
+ return -1;
+ }
+
+ return 0;
+}
+
+const struct ra_hwdec_driver ra_hwdec_dxva2gldx = {
+ .name = "dxva2-dxinterop",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_DXVA2, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ },
+};
diff --git a/video/out/opengl/hwdec_rpi.c b/video/out/opengl/hwdec_rpi.c
new file mode 100644
index 0000000..5362832
--- /dev/null
+++ b/video/out/opengl/hwdec_rpi.c
@@ -0,0 +1,384 @@
+/*
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <stdbool.h>
+#include <assert.h>
+
+#include <bcm_host.h>
+#include <interface/mmal/mmal.h>
+#include <interface/mmal/util/mmal_util.h>
+#include <interface/mmal/util/mmal_default_components.h>
+#include <interface/mmal/vc/mmal_vc_api.h>
+
+#include <libavutil/rational.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "video/mp_image.h"
+#include "video/out/gpu/hwdec.h"
+
+#include "common.h"
+
+struct priv {
+ struct mp_log *log;
+
+ struct mp_image_params params;
+
+ MMAL_COMPONENT_T *renderer;
+ bool renderer_enabled;
+
+ // for RAM input
+ MMAL_POOL_T *swpool;
+
+ struct mp_image *current_frame;
+
+ struct mp_rect src, dst;
+ int cur_window[4]; // raw user params
+};
+
+// Magic alignments (in pixels) expected by the MMAL internals.
+#define ALIGN_W 32
+#define ALIGN_H 16
+
+// Make mpi point to buffer, assuming MMAL_ENCODING_I420.
+// buffer can be NULL.
+// Return the required buffer space.
+static size_t layout_buffer(struct mp_image *mpi, MMAL_BUFFER_HEADER_T *buffer,
+ struct mp_image_params *params)
+{
+ assert(params->imgfmt == IMGFMT_420P);
+ mp_image_set_params(mpi, params);
+ int w = MP_ALIGN_UP(params->w, ALIGN_W);
+ int h = MP_ALIGN_UP(params->h, ALIGN_H);
+ uint8_t *cur = buffer ? buffer->data : NULL;
+ size_t size = 0;
+ for (int i = 0; i < 3; i++) {
+ int div = i ? 2 : 1;
+ mpi->planes[i] = cur;
+ mpi->stride[i] = w / div;
+ size_t plane_size = h / div * mpi->stride[i];
+ if (cur)
+ cur += plane_size;
+ size += plane_size;
+ }
+ return size;
+}
+
+static MMAL_FOURCC_T map_csp(enum mp_csp csp)
+{
+ switch (csp) {
+ case MP_CSP_BT_601: return MMAL_COLOR_SPACE_ITUR_BT601;
+ case MP_CSP_BT_709: return MMAL_COLOR_SPACE_ITUR_BT709;
+ case MP_CSP_SMPTE_240M: return MMAL_COLOR_SPACE_SMPTE240M;
+ default: return MMAL_COLOR_SPACE_UNKNOWN;
+ }
+}
+
+static void control_port_cb(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer)
+{
+ mmal_buffer_header_release(buffer);
+}
+
+static void input_port_cb(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer)
+{
+ struct mp_image *mpi = buffer->user_data;
+ talloc_free(mpi);
+}
+
+static void disable_renderer(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+
+ if (p->renderer_enabled) {
+ mmal_port_disable(p->renderer->control);
+ mmal_port_disable(p->renderer->input[0]);
+
+ mmal_port_flush(p->renderer->control);
+ mmal_port_flush(p->renderer->input[0]);
+
+ mmal_component_disable(p->renderer);
+ }
+ mmal_pool_destroy(p->swpool);
+ p->swpool = NULL;
+ p->renderer_enabled = false;
+}
+
+// check_window_only: assume params and dst/src rc are unchanged
+static void update_overlay(struct ra_hwdec *hw, bool check_window_only)
+{
+ struct priv *p = hw->priv;
+ MMAL_PORT_T *input = p->renderer->input[0];
+ struct mp_rect src = p->src;
+ struct mp_rect dst = p->dst;
+
+ int defs[4] = {0, 0, 0, 0};
+ int *z = ra_get_native_resource(hw->ra_ctx->ra, "MPV_RPI_WINDOW");
+ if (!z)
+ z = defs;
+
+ // As documented in the libmpv openglcb headers.
+ int display = z[0];
+ int layer = z[1];
+ int x = z[2];
+ int y = z[3];
+
+ if (check_window_only && memcmp(z, p->cur_window, sizeof(p->cur_window)) == 0)
+ return;
+
+ memcpy(p->cur_window, z, sizeof(p->cur_window));
+
+ int rotate[] = {MMAL_DISPLAY_ROT0,
+ MMAL_DISPLAY_ROT90,
+ MMAL_DISPLAY_ROT180,
+ MMAL_DISPLAY_ROT270};
+
+ int src_w = src.x1 - src.x0, src_h = src.y1 - src.y0,
+ dst_w = dst.x1 - dst.x0, dst_h = dst.y1 - dst.y0;
+ int p_x, p_y;
+ av_reduce(&p_x, &p_y, dst_w * src_h, src_w * dst_h, 16000);
+ MMAL_DISPLAYREGION_T dr = {
+ .hdr = { .id = MMAL_PARAMETER_DISPLAYREGION,
+ .size = sizeof(MMAL_DISPLAYREGION_T), },
+ .src_rect = { .x = src.x0, .y = src.y0,
+ .width = src_w, .height = src_h },
+ .dest_rect = { .x = dst.x0 + x, .y = dst.y0 + y,
+ .width = dst_w, .height = dst_h },
+ .layer = layer - 1, // under the GL layer
+ .display_num = display,
+ .pixel_x = p_x,
+ .pixel_y = p_y,
+ .transform = rotate[p->params.rotate / 90],
+ .fullscreen = 0,
+ .set = MMAL_DISPLAY_SET_SRC_RECT | MMAL_DISPLAY_SET_DEST_RECT |
+ MMAL_DISPLAY_SET_LAYER | MMAL_DISPLAY_SET_NUM |
+ MMAL_DISPLAY_SET_PIXEL | MMAL_DISPLAY_SET_TRANSFORM |
+ MMAL_DISPLAY_SET_FULLSCREEN,
+ };
+
+ if (p->params.rotate % 180 == 90) {
+ MPSWAP(int, dr.src_rect.x, dr.src_rect.y);
+ MPSWAP(int, dr.src_rect.width, dr.src_rect.height);
+ }
+
+ if (mmal_port_parameter_set(input, &dr.hdr))
+ MP_WARN(p, "could not set video rectangle\n");
+}
+
+static int enable_renderer(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+ MMAL_PORT_T *input = p->renderer->input[0];
+ struct mp_image_params *params = &p->params;
+
+ if (p->renderer_enabled)
+ return 0;
+
+ if (!params->imgfmt)
+ return -1;
+
+ bool opaque = params->imgfmt == IMGFMT_MMAL;
+
+ input->format->encoding = opaque ? MMAL_ENCODING_OPAQUE : MMAL_ENCODING_I420;
+ input->format->es->video.width = MP_ALIGN_UP(params->w, ALIGN_W);
+ input->format->es->video.height = MP_ALIGN_UP(params->h, ALIGN_H);
+ input->format->es->video.crop = (MMAL_RECT_T){0, 0, params->w, params->h};
+ input->format->es->video.par = (MMAL_RATIONAL_T){params->p_w, params->p_h};
+ input->format->es->video.color_space = map_csp(params->color.space);
+
+ if (mmal_port_format_commit(input))
+ return -1;
+
+ input->buffer_num = MPMAX(input->buffer_num_min,
+ input->buffer_num_recommended) + 3;
+ input->buffer_size = MPMAX(input->buffer_size_min,
+ input->buffer_size_recommended);
+
+ if (!opaque) {
+ size_t size = layout_buffer(&(struct mp_image){0}, NULL, params);
+ if (input->buffer_size != size) {
+ MP_FATAL(hw, "We disagree with MMAL about buffer sizes.\n");
+ return -1;
+ }
+
+ p->swpool = mmal_pool_create(input->buffer_num, input->buffer_size);
+ if (!p->swpool) {
+ MP_FATAL(hw, "Could not allocate buffer pool.\n");
+ return -1;
+ }
+ }
+
+ update_overlay(hw, false);
+
+ p->renderer_enabled = true;
+
+ if (mmal_port_enable(p->renderer->control, control_port_cb))
+ return -1;
+
+ if (mmal_port_enable(input, input_port_cb))
+ return -1;
+
+ if (mmal_component_enable(p->renderer)) {
+ MP_FATAL(hw, "Failed to enable video renderer.\n");
+ return -1;
+ }
+
+ return 0;
+}
+
+static void free_mmal_buffer(void *arg)
+{
+ MMAL_BUFFER_HEADER_T *buffer = arg;
+ mmal_buffer_header_release(buffer);
+}
+
+static struct mp_image *upload(struct ra_hwdec *hw, struct mp_image *hw_image)
+{
+ struct priv *p = hw->priv;
+
+ MMAL_BUFFER_HEADER_T *buffer = mmal_queue_wait(p->swpool->queue);
+ if (!buffer) {
+ MP_ERR(hw, "Can't allocate buffer.\n");
+ return NULL;
+ }
+ mmal_buffer_header_reset(buffer);
+
+ struct mp_image *new_ref = mp_image_new_custom_ref(NULL, buffer,
+ free_mmal_buffer);
+ if (!new_ref) {
+ mmal_buffer_header_release(buffer);
+ MP_ERR(hw, "Out of memory.\n");
+ return NULL;
+ }
+
+ mp_image_setfmt(new_ref, IMGFMT_MMAL);
+ new_ref->planes[3] = (void *)buffer;
+
+ struct mp_image dmpi = {0};
+ buffer->length = layout_buffer(&dmpi, buffer, &p->params);
+ mp_image_copy(&dmpi, hw_image);
+
+ return new_ref;
+}
+
+static int overlay_frame(struct ra_hwdec *hw, struct mp_image *hw_image,
+ struct mp_rect *src, struct mp_rect *dst, bool newframe)
+{
+ struct priv *p = hw->priv;
+
+ if (hw_image && !mp_image_params_equal(&p->params, &hw_image->params)) {
+ p->params = hw_image->params;
+
+ disable_renderer(hw);
+ mp_image_unrefp(&p->current_frame);
+
+ if (enable_renderer(hw) < 0)
+ return -1;
+ }
+
+ if (hw_image && p->current_frame && !newframe) {
+ if (!mp_rect_equals(&p->src, src) ||mp_rect_equals(&p->dst, dst)) {
+ p->src = *src;
+ p->dst = *dst;
+ update_overlay(hw, false);
+ }
+ return 0; // don't reupload
+ }
+
+ mp_image_unrefp(&p->current_frame);
+
+ if (!hw_image) {
+ disable_renderer(hw);
+ return 0;
+ }
+
+ if (enable_renderer(hw) < 0)
+ return -1;
+
+ update_overlay(hw, true);
+
+ struct mp_image *mpi = NULL;
+ if (hw_image->imgfmt == IMGFMT_MMAL) {
+ mpi = mp_image_new_ref(hw_image);
+ } else {
+ mpi = upload(hw, hw_image);
+ }
+
+ if (!mpi) {
+ disable_renderer(hw);
+ return -1;
+ }
+
+ MMAL_BUFFER_HEADER_T *ref = (void *)mpi->planes[3];
+
+ // Assume this field is free for use by us.
+ ref->user_data = mpi;
+
+ if (mmal_port_send_buffer(p->renderer->input[0], ref)) {
+ MP_ERR(hw, "could not queue picture!\n");
+ talloc_free(mpi);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void destroy(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+
+ disable_renderer(hw);
+
+ if (p->renderer)
+ mmal_component_release(p->renderer);
+
+ mmal_vc_deinit();
+}
+
+static int create(struct ra_hwdec *hw)
+{
+ struct priv *p = hw->priv;
+ p->log = hw->log;
+
+ bcm_host_init();
+
+ if (mmal_vc_init()) {
+ MP_FATAL(hw, "Could not initialize MMAL.\n");
+ return -1;
+ }
+
+ if (mmal_component_create(MMAL_COMPONENT_DEFAULT_VIDEO_RENDERER, &p->renderer))
+ {
+ MP_FATAL(hw, "Could not create MMAL renderer.\n");
+ mmal_vc_deinit();
+ return -1;
+ }
+
+ return 0;
+}
+
+const struct ra_hwdec_driver ra_hwdec_rpi_overlay = {
+ .name = "rpi-overlay",
+ .priv_size = sizeof(struct priv),
+ .imgfmts = {IMGFMT_MMAL, IMGFMT_420P, 0},
+ .init = create,
+ .overlay_frame = overlay_frame,
+ .uninit = destroy,
+};
diff --git a/video/out/opengl/hwdec_vdpau.c b/video/out/opengl/hwdec_vdpau.c
new file mode 100644
index 0000000..acdc703
--- /dev/null
+++ b/video/out/opengl/hwdec_vdpau.c
@@ -0,0 +1,251 @@
+/*
+ * 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 <stddef.h>
+#include <assert.h>
+
+#include "video/out/gpu/hwdec.h"
+#include "ra_gl.h"
+#include "video/vdpau.h"
+#include "video/vdpau_mixer.h"
+
+// This is a GL_NV_vdpau_interop specification bug, and headers (unfortunately)
+// follow it. I'm not sure about the original nvidia headers.
+#define BRAINDEATH(x) ((void *)(uintptr_t)(x))
+
+struct priv_owner {
+ struct mp_vdpau_ctx *ctx;
+};
+
+struct priv {
+ struct mp_vdpau_ctx *ctx;
+ GL *gl;
+ uint64_t preemption_counter;
+ GLuint gl_texture;
+ bool vdpgl_initialized;
+ GLvdpauSurfaceNV vdpgl_surface;
+ VdpOutputSurface vdp_surface;
+ struct mp_vdpau_mixer *mixer;
+ struct ra_imgfmt_desc direct_desc;
+ bool mapped;
+};
+
+static int init(struct ra_hwdec *hw)
+{
+ struct ra *ra = hw->ra_ctx->ra;
+ Display *x11disp = ra_get_native_resource(ra, "x11");
+ if (!x11disp || !ra_is_gl(ra))
+ return -1;
+ GL *gl = ra_gl_get(ra);
+ if (!(gl->mpgl_caps & MPGL_CAP_VDPAU))
+ return -1;
+ struct priv_owner *p = hw->priv;
+ p->ctx = mp_vdpau_create_device_x11(hw->log, x11disp, true);
+ if (!p->ctx)
+ return -1;
+ if (mp_vdpau_handle_preemption(p->ctx, NULL) < 1)
+ return -1;
+ if (hw->probing && mp_vdpau_guess_if_emulated(p->ctx))
+ return -1;
+ p->ctx->hwctx.driver_name = hw->driver->name;
+ p->ctx->hwctx.hw_imgfmt = IMGFMT_VDPAU;
+ hwdec_devices_add(hw->devs, &p->ctx->hwctx);
+ return 0;
+}
+
+static void uninit(struct ra_hwdec *hw)
+{
+ struct priv_owner *p = hw->priv;
+
+ if (p->ctx)
+ hwdec_devices_remove(hw->devs, &p->ctx->hwctx);
+ mp_vdpau_destroy(p->ctx);
+}
+
+static void mapper_unmap(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = p->gl;
+
+ for (int n = 0; n < 4; n++)
+ ra_tex_free(mapper->ra, &mapper->tex[n]);
+
+ if (p->mapped) {
+ gl->VDPAUUnmapSurfacesNV(1, &p->vdpgl_surface);
+ }
+ p->mapped = false;
+}
+
+static void mark_vdpau_objects_uninitialized(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+
+ p->vdp_surface = VDP_INVALID_HANDLE;
+ p->mapped = false;
+}
+
+static void mapper_uninit(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = p->gl;
+ struct vdp_functions *vdp = &p->ctx->vdp;
+ VdpStatus vdp_st;
+
+ assert(!p->mapped);
+
+ if (p->vdpgl_surface)
+ gl->VDPAUUnregisterSurfaceNV(p->vdpgl_surface);
+ p->vdpgl_surface = 0;
+
+ gl->DeleteTextures(1, &p->gl_texture);
+
+ if (p->vdp_surface != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_destroy(p->vdp_surface);
+ CHECK_VDP_WARNING(mapper, "Error when calling vdp_output_surface_destroy");
+ }
+ p->vdp_surface = VDP_INVALID_HANDLE;
+
+ gl_check_error(gl, mapper->log, "Before uninitializing OpenGL interop");
+
+ if (p->vdpgl_initialized)
+ gl->VDPAUFiniNV();
+
+ p->vdpgl_initialized = false;
+
+ gl_check_error(gl, mapper->log, "After uninitializing OpenGL interop");
+
+ mp_vdpau_mixer_destroy(p->mixer);
+}
+
+static int mapper_init(struct ra_hwdec_mapper *mapper)
+{
+ struct priv_owner *p_owner = mapper->owner->priv;
+ struct priv *p = mapper->priv;
+
+ p->gl = ra_gl_get(mapper->ra);
+ p->ctx = p_owner->ctx;
+
+ GL *gl = p->gl;
+ struct vdp_functions *vdp = &p->ctx->vdp;
+ VdpStatus vdp_st;
+
+ p->vdp_surface = VDP_INVALID_HANDLE;
+ p->mixer = mp_vdpau_mixer_create(p->ctx, mapper->log);
+ if (!p->mixer)
+ return -1;
+
+ mapper->dst_params = mapper->src_params;
+
+ if (mp_vdpau_handle_preemption(p->ctx, &p->preemption_counter) < 0)
+ return -1;
+
+ gl->VDPAUInitNV(BRAINDEATH(p->ctx->vdp_device), p->ctx->get_proc_address);
+
+ p->vdpgl_initialized = true;
+
+ gl->GenTextures(1, &p->gl_texture);
+
+ gl->BindTexture(GL_TEXTURE_2D, p->gl_texture);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ gl->BindTexture(GL_TEXTURE_2D, 0);
+
+ vdp_st = vdp->output_surface_create(p->ctx->vdp_device,
+ VDP_RGBA_FORMAT_B8G8R8A8,
+ mapper->src_params.w,
+ mapper->src_params.h,
+ &p->vdp_surface);
+ CHECK_VDP_ERROR(mapper, "Error when calling vdp_output_surface_create");
+
+ p->vdpgl_surface = gl->VDPAURegisterOutputSurfaceNV(BRAINDEATH(p->vdp_surface),
+ GL_TEXTURE_2D,
+ 1, &p->gl_texture);
+ if (!p->vdpgl_surface)
+ return -1;
+
+ gl->VDPAUSurfaceAccessNV(p->vdpgl_surface, GL_READ_ONLY);
+
+ mapper->dst_params.imgfmt = IMGFMT_RGB0;
+ mapper->dst_params.hw_subfmt = 0;
+
+ gl_check_error(gl, mapper->log, "After initializing vdpau OpenGL interop");
+
+ return 0;
+}
+
+static int mapper_map(struct ra_hwdec_mapper *mapper)
+{
+ struct priv *p = mapper->priv;
+ GL *gl = p->gl;
+
+ int pe = mp_vdpau_handle_preemption(p->ctx, &p->preemption_counter);
+ if (pe < 1) {
+ mark_vdpau_objects_uninitialized(mapper);
+ if (pe < 0)
+ return -1;
+ mapper_uninit(mapper);
+ if (mapper_init(mapper) < 0)
+ return -1;
+ }
+
+ if (!p->vdpgl_surface)
+ return -1;
+
+ mp_vdpau_mixer_render(p->mixer, NULL, p->vdp_surface, NULL, mapper->src,
+ NULL);
+
+ gl->VDPAUMapSurfacesNV(1, &p->vdpgl_surface);
+
+ p->mapped = true;
+
+ struct ra_tex_params params = {
+ .dimensions = 2,
+ .w = mapper->src_params.w,
+ .h = mapper->src_params.h,
+ .d = 1,
+ .format = ra_find_unorm_format(mapper->ra, 1, 4),
+ .render_src = true,
+ .src_linear = true,
+ };
+
+ if (!params.format)
+ return -1;
+
+ mapper->tex[0] =
+ ra_create_wrapped_tex(mapper->ra, &params, p->gl_texture);
+ if (!mapper->tex[0])
+ return -1;
+
+ return 0;
+}
+
+const struct ra_hwdec_driver ra_hwdec_vdpau = {
+ .name = "vdpau-gl",
+ .priv_size = sizeof(struct priv_owner),
+ .imgfmts = {IMGFMT_VDPAU, 0},
+ .init = init,
+ .uninit = uninit,
+ .mapper = &(const struct ra_hwdec_mapper_driver){
+ .priv_size = sizeof(struct priv),
+ .init = mapper_init,
+ .uninit = mapper_uninit,
+ .map = mapper_map,
+ .unmap = mapper_unmap,
+ },
+};
diff --git a/video/out/opengl/libmpv_gl.c b/video/out/opengl/libmpv_gl.c
new file mode 100644
index 0000000..c297c13
--- /dev/null
+++ b/video/out/opengl/libmpv_gl.c
@@ -0,0 +1,114 @@
+#include "common.h"
+#include "context.h"
+#include "ra_gl.h"
+#include "options/m_config.h"
+#include "libmpv/render_gl.h"
+#include "video/out/gpu/libmpv_gpu.h"
+#include "video/out/gpu/ra.h"
+
+struct priv {
+ GL *gl;
+ struct ra_ctx *ra_ctx;
+};
+
+static int init(struct libmpv_gpu_context *ctx, mpv_render_param *params)
+{
+ ctx->priv = talloc_zero(NULL, struct priv);
+ struct priv *p = ctx->priv;
+
+ mpv_opengl_init_params *init_params =
+ get_mpv_render_param(params, MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, NULL);
+ if (!init_params)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ p->gl = talloc_zero(p, GL);
+
+ mpgl_load_functions2(p->gl, init_params->get_proc_address,
+ init_params->get_proc_address_ctx,
+ NULL, ctx->log);
+ if (!p->gl->version && !p->gl->es) {
+ MP_FATAL(ctx, "OpenGL not initialized.\n");
+ return MPV_ERROR_UNSUPPORTED;
+ }
+
+ // initialize a blank ra_ctx to reuse ra_gl_ctx
+ p->ra_ctx = talloc_zero(p, struct ra_ctx);
+ p->ra_ctx->log = ctx->log;
+ p->ra_ctx->global = ctx->global;
+ p->ra_ctx->opts = (struct ra_ctx_opts) {
+ .allow_sw = true,
+ };
+
+ static const struct ra_swapchain_fns empty_swapchain_fns = {0};
+ struct ra_gl_ctx_params gl_params = {
+ // vo_libmpv is essentially like a gigantic external swapchain where
+ // the user is in charge of presentation / swapping etc. But we don't
+ // actually need to provide any of these functions, since we can just
+ // not call them to begin with - so just set it to an empty object to
+ // signal to ra_gl_p that we don't care about its latency emulation
+ // functionality
+ .external_swapchain = &empty_swapchain_fns
+ };
+
+ p->gl->SwapInterval = NULL; // we shouldn't randomly change this, so lock it
+ if (!ra_gl_ctx_init(p->ra_ctx, p->gl, gl_params))
+ return MPV_ERROR_UNSUPPORTED;
+
+ struct ra_ctx_opts *ctx_opts = mp_get_config_group(ctx, ctx->global, &ra_ctx_conf);
+ p->ra_ctx->opts.debug = ctx_opts->debug;
+ p->gl->debug_context = ctx_opts->debug;
+ ra_gl_set_debug(p->ra_ctx->ra, ctx_opts->debug);
+ talloc_free(ctx_opts);
+
+ ctx->ra_ctx = p->ra_ctx;
+
+ return 0;
+}
+
+static int wrap_fbo(struct libmpv_gpu_context *ctx, mpv_render_param *params,
+ struct ra_tex **out)
+{
+ struct priv *p = ctx->priv;
+
+ mpv_opengl_fbo *fbo =
+ get_mpv_render_param(params, MPV_RENDER_PARAM_OPENGL_FBO, NULL);
+ if (!fbo)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ if (fbo->fbo && !(p->gl->mpgl_caps & MPGL_CAP_FB)) {
+ MP_FATAL(ctx, "Rendering to FBO requested, but no FBO extension found!\n");
+ return MPV_ERROR_UNSUPPORTED;
+ }
+
+ struct ra_swapchain *sw = p->ra_ctx->swapchain;
+ struct ra_fbo target;
+ ra_gl_ctx_resize(sw, fbo->w, fbo->h, fbo->fbo);
+ ra_gl_ctx_start_frame(sw, &target);
+ *out = target.tex;
+ return 0;
+}
+
+static void done_frame(struct libmpv_gpu_context *ctx, bool ds)
+{
+ struct priv *p = ctx->priv;
+
+ struct ra_swapchain *sw = p->ra_ctx->swapchain;
+ struct vo_frame dummy = {.display_synced = ds};
+ ra_gl_ctx_submit_frame(sw, &dummy);
+}
+
+static void destroy(struct libmpv_gpu_context *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (p->ra_ctx)
+ ra_gl_ctx_uninit(p->ra_ctx);
+}
+
+const struct libmpv_gpu_context_fns libmpv_gpu_context_gl = {
+ .api_name = MPV_RENDER_API_TYPE_OPENGL,
+ .init = init,
+ .wrap_fbo = wrap_fbo,
+ .done_frame = done_frame,
+ .destroy = destroy,
+};
diff --git a/video/out/opengl/ra_gl.c b/video/out/opengl/ra_gl.c
new file mode 100644
index 0000000..f535f1f
--- /dev/null
+++ b/video/out/opengl/ra_gl.c
@@ -0,0 +1,1208 @@
+#include <libavutil/intreadwrite.h>
+
+#include "formats.h"
+#include "utils.h"
+#include "ra_gl.h"
+
+static struct ra_fns ra_fns_gl;
+
+// For ra.priv
+struct ra_gl {
+ GL *gl;
+ bool debug_enable;
+ bool timer_active; // hack for GL_TIME_ELAPSED limitations
+};
+
+// For ra_tex.priv
+struct ra_tex_gl {
+ struct ra_buf_pool pbo; // for ra.use_pbo
+ bool own_objects;
+ GLenum target;
+ GLuint texture; // 0 if no texture data associated
+ GLuint fbo; // 0 if no rendering requested, or it default framebuffer
+ // These 3 fields can be 0 if unknown.
+ GLint internal_format;
+ GLenum format;
+ GLenum type;
+};
+
+// For ra_buf.priv
+struct ra_buf_gl {
+ GLenum target;
+ GLuint buffer;
+ GLsync fence;
+};
+
+// For ra_renderpass.priv
+struct ra_renderpass_gl {
+ GLuint program;
+ // 1 entry for each ra_renderpass_params.inputs[] entry
+ GLint *uniform_loc;
+ int num_uniform_loc; // == ra_renderpass_params.num_inputs
+ struct gl_vao vao;
+};
+
+// (Init time only.)
+static void probe_real_size(GL *gl, struct ra_format *fmt)
+{
+ const struct gl_format *gl_fmt = fmt->priv;
+
+ if (!gl->GetTexLevelParameteriv)
+ return; // GLES
+
+ bool is_la = gl_fmt->format == GL_LUMINANCE ||
+ gl_fmt->format == GL_LUMINANCE_ALPHA;
+ if (is_la && gl->es)
+ return; // GLES doesn't provide GL_TEXTURE_LUMINANCE_SIZE.
+
+ GLuint tex;
+ gl->GenTextures(1, &tex);
+ gl->BindTexture(GL_TEXTURE_2D, tex);
+ gl->TexImage2D(GL_TEXTURE_2D, 0, gl_fmt->internal_format, 64, 64, 0,
+ gl_fmt->format, gl_fmt->type, NULL);
+ for (int i = 0; i < fmt->num_components; i++) {
+ const GLenum pnames[] = {
+ GL_TEXTURE_RED_SIZE,
+ GL_TEXTURE_GREEN_SIZE,
+ GL_TEXTURE_BLUE_SIZE,
+ GL_TEXTURE_ALPHA_SIZE,
+ GL_TEXTURE_LUMINANCE_SIZE,
+ GL_TEXTURE_ALPHA_SIZE,
+ };
+ int comp = is_la ? i + 4 : i;
+ assert(comp < MP_ARRAY_SIZE(pnames));
+ GLint param = -1;
+ gl->GetTexLevelParameteriv(GL_TEXTURE_2D, 0, pnames[comp], &param);
+ fmt->component_depth[i] = param > 0 ? param : 0;
+ }
+ gl->DeleteTextures(1, &tex);
+}
+
+static int ra_init_gl(struct ra *ra, GL *gl)
+{
+ if (gl->version < 210 && gl->es < 200) {
+ MP_ERR(ra, "At least OpenGL 2.1 or OpenGL ES 2.0 required.\n");
+ return -1;
+ }
+
+ struct ra_gl *p = ra->priv = talloc_zero(NULL, struct ra_gl);
+ p->gl = gl;
+
+ ra_gl_set_debug(ra, true);
+
+ ra->fns = &ra_fns_gl;
+ ra->glsl_version = gl->glsl_version;
+ ra->glsl_es = gl->es > 0;
+
+ static const int caps_map[][2] = {
+ {RA_CAP_DIRECT_UPLOAD, 0},
+ {RA_CAP_GLOBAL_UNIFORM, 0},
+ {RA_CAP_FRAGCOORD, 0},
+ {RA_CAP_TEX_1D, MPGL_CAP_1D_TEX},
+ {RA_CAP_TEX_3D, MPGL_CAP_3D_TEX},
+ {RA_CAP_COMPUTE, MPGL_CAP_COMPUTE_SHADER},
+ {RA_CAP_NUM_GROUPS, MPGL_CAP_COMPUTE_SHADER},
+ {RA_CAP_NESTED_ARRAY, MPGL_CAP_NESTED_ARRAY},
+ {RA_CAP_SLOW_DR, MPGL_CAP_SLOW_DR},
+ };
+
+ for (int i = 0; i < MP_ARRAY_SIZE(caps_map); i++) {
+ if ((gl->mpgl_caps & caps_map[i][1]) == caps_map[i][1])
+ ra->caps |= caps_map[i][0];
+ }
+
+ if (gl->BindBufferBase) {
+ if (gl->mpgl_caps & MPGL_CAP_UBO)
+ ra->caps |= RA_CAP_BUF_RO;
+ if (gl->mpgl_caps & MPGL_CAP_SSBO)
+ ra->caps |= RA_CAP_BUF_RW;
+ }
+
+ // textureGather is only supported in GLSL 400+ / ES 310+
+ if (ra->glsl_version >= (ra->glsl_es ? 310 : 400))
+ ra->caps |= RA_CAP_GATHER;
+
+ if (gl->BlitFramebuffer)
+ ra->caps |= RA_CAP_BLIT;
+
+ // Disable compute shaders for GLSL < 420. This work-around is needed since
+ // some buggy OpenGL drivers expose compute shaders for lower GLSL versions,
+ // despite the spec requiring 420+.
+ if (ra->glsl_version < (ra->glsl_es ? 310 : 420)) {
+ ra->caps &= ~RA_CAP_COMPUTE;
+ }
+
+ // While we can handle compute shaders on GLES the spec (intentionally)
+ // does not support binding textures for writing, which all uses inside mpv
+ // would require. So disable it unconditionally anyway.
+ if (ra->glsl_es)
+ ra->caps &= ~RA_CAP_COMPUTE;
+
+ int gl_fmt_features = gl_format_feature_flags(gl);
+
+ for (int n = 0; gl_formats[n].internal_format; n++) {
+ const struct gl_format *gl_fmt = &gl_formats[n];
+
+ if (!(gl_fmt->flags & gl_fmt_features))
+ continue;
+
+ struct ra_format *fmt = talloc_zero(ra, struct ra_format);
+ *fmt = (struct ra_format){
+ .name = gl_fmt->name,
+ .priv = (void *)gl_fmt,
+ .ctype = gl_format_type(gl_fmt),
+ .num_components = gl_format_components(gl_fmt->format),
+ .ordered = gl_fmt->format != GL_RGB_422_APPLE,
+ .pixel_size = gl_bytes_per_pixel(gl_fmt->format, gl_fmt->type),
+ .luminance_alpha = gl_fmt->format == GL_LUMINANCE_ALPHA,
+ .linear_filter = gl_fmt->flags & F_TF,
+ .renderable = (gl_fmt->flags & F_CR) &&
+ (gl->mpgl_caps & MPGL_CAP_FB),
+ // TODO: Check whether it's a storable format
+ // https://www.khronos.org/opengl/wiki/Image_Load_Store
+ .storable = true,
+ };
+
+ int csize = gl_component_size(gl_fmt->type) * 8;
+ int depth = csize;
+
+ if (gl_fmt->flags & F_F16) {
+ depth = 16;
+ csize = 32; // always upload as GL_FLOAT (simpler for us)
+ }
+
+ for (int i = 0; i < fmt->num_components; i++) {
+ fmt->component_size[i] = csize;
+ fmt->component_depth[i] = depth;
+ }
+
+ if (fmt->ctype == RA_CTYPE_UNORM && depth != 8)
+ probe_real_size(gl, fmt);
+
+ // Special formats for which OpenGL happens to have direct support.
+ if (strcmp(fmt->name, "rgb565") == 0) {
+ fmt->special_imgfmt = IMGFMT_RGB565;
+ struct ra_imgfmt_desc *desc = talloc_zero(fmt, struct ra_imgfmt_desc);
+ fmt->special_imgfmt_desc = desc;
+ desc->num_planes = 1;
+ desc->planes[0] = fmt;
+ for (int i = 0; i < 3; i++)
+ desc->components[0][i] = i + 1;
+ desc->chroma_w = desc->chroma_h = 1;
+ }
+ if (strcmp(fmt->name, "rgb10_a2") == 0) {
+ fmt->special_imgfmt = IMGFMT_RGB30;
+ struct ra_imgfmt_desc *desc = talloc_zero(fmt, struct ra_imgfmt_desc);
+ fmt->special_imgfmt_desc = desc;
+ desc->component_bits = 10;
+ desc->num_planes = 1;
+ desc->planes[0] = fmt;
+ for (int i = 0; i < 3; i++)
+ desc->components[0][i] = 3 - i;
+ desc->chroma_w = desc->chroma_h = 1;
+ }
+ if (strcmp(fmt->name, "appleyp") == 0) {
+ fmt->special_imgfmt = IMGFMT_UYVY;
+ struct ra_imgfmt_desc *desc = talloc_zero(fmt, struct ra_imgfmt_desc);
+ fmt->special_imgfmt_desc = desc;
+ desc->num_planes = 1;
+ desc->planes[0] = fmt;
+ desc->components[0][0] = 3;
+ desc->components[0][1] = 1;
+ desc->components[0][2] = 2;
+ desc->chroma_w = desc->chroma_h = 1;
+ }
+
+ fmt->glsl_format = ra_fmt_glsl_format(fmt);
+
+ MP_TARRAY_APPEND(ra, ra->formats, ra->num_formats, fmt);
+ }
+
+ GLint ival;
+ gl->GetIntegerv(GL_MAX_TEXTURE_SIZE, &ival);
+ ra->max_texture_wh = ival;
+
+ if (ra->caps & RA_CAP_COMPUTE) {
+ gl->GetIntegerv(GL_MAX_COMPUTE_SHARED_MEMORY_SIZE, &ival);
+ ra->max_shmem = ival;
+ gl->GetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &ival);
+ ra->max_compute_group_threads = ival;
+ }
+
+ gl->Disable(GL_DITHER);
+
+ if (!ra_find_unorm_format(ra, 2, 1))
+ MP_VERBOSE(ra, "16 bit UNORM textures not available.\n");
+
+ return 0;
+}
+
+struct ra *ra_create_gl(GL *gl, struct mp_log *log)
+{
+ struct ra *ra = talloc_zero(NULL, struct ra);
+ ra->log = log;
+ if (ra_init_gl(ra, gl) < 0) {
+ talloc_free(ra);
+ return NULL;
+ }
+ return ra;
+}
+
+static void gl_destroy(struct ra *ra)
+{
+ talloc_free(ra->priv);
+}
+
+void ra_gl_set_debug(struct ra *ra, bool enable)
+{
+ struct ra_gl *p = ra->priv;
+ GL *gl = ra_gl_get(ra);
+
+ p->debug_enable = enable;
+ if (gl->debug_context)
+ gl_set_debug_logger(gl, enable ? ra->log : NULL);
+}
+
+static void gl_tex_destroy(struct ra *ra, struct ra_tex *tex)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_tex_gl *tex_gl = tex->priv;
+
+ ra_buf_pool_uninit(ra, &tex_gl->pbo);
+
+ if (tex_gl->own_objects) {
+ if (tex_gl->fbo)
+ gl->DeleteFramebuffers(1, &tex_gl->fbo);
+
+ gl->DeleteTextures(1, &tex_gl->texture);
+ }
+ talloc_free(tex_gl);
+ talloc_free(tex);
+}
+
+static struct ra_tex *gl_tex_create_blank(struct ra *ra,
+ const struct ra_tex_params *params)
+{
+ struct ra_tex *tex = talloc_zero(NULL, struct ra_tex);
+ tex->params = *params;
+ tex->params.initial_data = NULL;
+ struct ra_tex_gl *tex_gl = tex->priv = talloc_zero(NULL, struct ra_tex_gl);
+
+ const struct gl_format *fmt = params->format->priv;
+ tex_gl->internal_format = fmt->internal_format;
+ tex_gl->format = fmt->format;
+ tex_gl->type = fmt->type;
+ switch (params->dimensions) {
+ case 1: tex_gl->target = GL_TEXTURE_1D; break;
+ case 2: tex_gl->target = GL_TEXTURE_2D; break;
+ case 3: tex_gl->target = GL_TEXTURE_3D; break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+ if (params->non_normalized) {
+ assert(params->dimensions == 2);
+ tex_gl->target = GL_TEXTURE_RECTANGLE;
+ }
+ if (params->external_oes) {
+ assert(params->dimensions == 2 && !params->non_normalized);
+ tex_gl->target = GL_TEXTURE_EXTERNAL_OES;
+ }
+
+ if (params->downloadable && !(params->dimensions == 2 &&
+ params->format->renderable))
+ {
+ gl_tex_destroy(ra, tex);
+ return NULL;
+ }
+
+ return tex;
+}
+
+static struct ra_tex *gl_tex_create(struct ra *ra,
+ const struct ra_tex_params *params)
+{
+ GL *gl = ra_gl_get(ra);
+ assert(!params->format->dummy_format);
+
+ struct ra_tex *tex = gl_tex_create_blank(ra, params);
+ if (!tex)
+ return NULL;
+ struct ra_tex_gl *tex_gl = tex->priv;
+
+ tex_gl->own_objects = true;
+
+ gl->GenTextures(1, &tex_gl->texture);
+ gl->BindTexture(tex_gl->target, tex_gl->texture);
+
+ GLint filter = params->src_linear ? GL_LINEAR : GL_NEAREST;
+ GLint wrap = params->src_repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE;
+ gl->TexParameteri(tex_gl->target, GL_TEXTURE_MIN_FILTER, filter);
+ gl->TexParameteri(tex_gl->target, GL_TEXTURE_MAG_FILTER, filter);
+ gl->TexParameteri(tex_gl->target, GL_TEXTURE_WRAP_S, wrap);
+ if (params->dimensions > 1)
+ gl->TexParameteri(tex_gl->target, GL_TEXTURE_WRAP_T, wrap);
+ if (params->dimensions > 2)
+ gl->TexParameteri(tex_gl->target, GL_TEXTURE_WRAP_R, wrap);
+
+ gl->PixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ switch (params->dimensions) {
+ case 1:
+ gl->TexImage1D(tex_gl->target, 0, tex_gl->internal_format, params->w,
+ 0, tex_gl->format, tex_gl->type, params->initial_data);
+ break;
+ case 2:
+ gl->TexImage2D(tex_gl->target, 0, tex_gl->internal_format, params->w,
+ params->h, 0, tex_gl->format, tex_gl->type,
+ params->initial_data);
+ break;
+ case 3:
+ gl->TexImage3D(tex_gl->target, 0, tex_gl->internal_format, params->w,
+ params->h, params->d, 0, tex_gl->format, tex_gl->type,
+ params->initial_data);
+ break;
+ }
+ gl->PixelStorei(GL_UNPACK_ALIGNMENT, 4);
+
+ gl->BindTexture(tex_gl->target, 0);
+
+ gl_check_error(gl, ra->log, "after creating texture");
+
+ // Even blitting needs an FBO in OpenGL for strange reasons.
+ // Download is handled by reading from an FBO.
+ if (tex->params.render_dst || tex->params.blit_src ||
+ tex->params.blit_dst || tex->params.downloadable)
+ {
+ if (!tex->params.format->renderable) {
+ MP_ERR(ra, "Trying to create renderable texture with unsupported "
+ "format.\n");
+ ra_tex_free(ra, &tex);
+ return NULL;
+ }
+
+ assert(gl->mpgl_caps & MPGL_CAP_FB);
+
+ gl->GenFramebuffers(1, &tex_gl->fbo);
+ gl->BindFramebuffer(GL_FRAMEBUFFER, tex_gl->fbo);
+ gl->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+ GL_TEXTURE_2D, tex_gl->texture, 0);
+ GLenum err = gl->CheckFramebufferStatus(GL_FRAMEBUFFER);
+ gl->BindFramebuffer(GL_FRAMEBUFFER, 0);
+
+ if (err != GL_FRAMEBUFFER_COMPLETE) {
+ MP_ERR(ra, "Error: framebuffer completeness check failed (error=%d).\n",
+ (int)err);
+ ra_tex_free(ra, &tex);
+ return NULL;
+ }
+
+
+ gl_check_error(gl, ra->log, "after creating framebuffer");
+ }
+
+ return tex;
+}
+
+// Create a ra_tex that merely wraps an existing texture. The returned object
+// is freed with ra_tex_free(), but this will not delete the texture passed to
+// this function.
+// Some features are unsupported, e.g. setting params->initial_data or render_dst.
+struct ra_tex *ra_create_wrapped_tex(struct ra *ra,
+ const struct ra_tex_params *params,
+ GLuint gl_texture)
+{
+ struct ra_tex *tex = gl_tex_create_blank(ra, params);
+ if (!tex)
+ return NULL;
+ struct ra_tex_gl *tex_gl = tex->priv;
+ tex_gl->texture = gl_texture;
+ return tex;
+}
+
+static const struct ra_format fbo_dummy_format = {
+ .name = "unknown_fbo",
+ .priv = (void *)&(const struct gl_format){
+ .name = "unknown",
+ .format = GL_RGBA,
+ .flags = F_CR,
+ },
+ .renderable = true,
+ .dummy_format = true,
+};
+
+// Create a ra_tex that merely wraps an existing framebuffer. gl_fbo can be 0
+// to wrap the default framebuffer.
+// The returned object is freed with ra_tex_free(), but this will not delete
+// the framebuffer object passed to this function.
+struct ra_tex *ra_create_wrapped_fb(struct ra *ra, GLuint gl_fbo, int w, int h)
+{
+ struct ra_tex *tex = talloc_zero(ra, struct ra_tex);
+ *tex = (struct ra_tex){
+ .params = {
+ .dimensions = 2,
+ .w = w, .h = h, .d = 1,
+ .format = &fbo_dummy_format,
+ .render_dst = true,
+ .blit_src = true,
+ .blit_dst = true,
+ },
+ };
+
+ struct ra_tex_gl *tex_gl = tex->priv = talloc_zero(NULL, struct ra_tex_gl);
+ *tex_gl = (struct ra_tex_gl){
+ .fbo = gl_fbo,
+ .internal_format = 0,
+ .format = GL_RGBA,
+ .type = 0,
+ };
+
+ return tex;
+}
+
+GL *ra_gl_get(struct ra *ra)
+{
+ struct ra_gl *p = ra->priv;
+ return p->gl;
+}
+
+// Return the associate glTexImage arguments for the given format. Sets all
+// fields to 0 on failure.
+void ra_gl_get_format(const struct ra_format *fmt, GLint *out_internal_format,
+ GLenum *out_format, GLenum *out_type)
+{
+ const struct gl_format *gl_format = fmt->priv;
+ *out_internal_format = gl_format->internal_format;
+ *out_format = gl_format->format;
+ *out_type = gl_format->type;
+}
+
+void ra_gl_get_raw_tex(struct ra *ra, struct ra_tex *tex,
+ GLuint *out_texture, GLenum *out_target)
+{
+ struct ra_tex_gl *tex_gl = tex->priv;
+ *out_texture = tex_gl->texture;
+ *out_target = tex_gl->target;
+}
+
+// Return whether the ra instance was created with ra_create_gl(). This is the
+// _only_ function that can be called on a ra instance of any type.
+bool ra_is_gl(struct ra *ra)
+{
+ return ra->fns == &ra_fns_gl;
+}
+
+static bool gl_tex_upload(struct ra *ra,
+ const struct ra_tex_upload_params *params)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_tex *tex = params->tex;
+ struct ra_buf *buf = params->buf;
+ struct ra_tex_gl *tex_gl = tex->priv;
+ struct ra_buf_gl *buf_gl = buf ? buf->priv : NULL;
+ assert(tex->params.host_mutable);
+ assert(!params->buf || !params->src);
+
+ if (ra->use_pbo && !params->buf)
+ return ra_tex_upload_pbo(ra, &tex_gl->pbo, params);
+
+ const void *src = params->src;
+ if (buf) {
+ gl->BindBuffer(GL_PIXEL_UNPACK_BUFFER, buf_gl->buffer);
+ src = (void *)params->buf_offset;
+ }
+
+ gl->BindTexture(tex_gl->target, tex_gl->texture);
+ if (params->invalidate && gl->InvalidateTexImage)
+ gl->InvalidateTexImage(tex_gl->texture, 0);
+
+ switch (tex->params.dimensions) {
+ case 1:
+ gl->TexImage1D(tex_gl->target, 0, tex_gl->internal_format,
+ tex->params.w, 0, tex_gl->format, tex_gl->type, src);
+ break;
+ case 2: {
+ struct mp_rect rc = {0, 0, tex->params.w, tex->params.h};
+ if (params->rc)
+ rc = *params->rc;
+ gl_upload_tex(gl, tex_gl->target, tex_gl->format, tex_gl->type,
+ src, params->stride, rc.x0, rc.y0, rc.x1 - rc.x0,
+ rc.y1 - rc.y0);
+ break;
+ }
+ case 3:
+ gl->PixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ gl->TexImage3D(GL_TEXTURE_3D, 0, tex_gl->internal_format, tex->params.w,
+ tex->params.h, tex->params.d, 0, tex_gl->format,
+ tex_gl->type, src);
+ gl->PixelStorei(GL_UNPACK_ALIGNMENT, 4);
+ break;
+ }
+
+ gl->BindTexture(tex_gl->target, 0);
+
+ if (buf) {
+ gl->BindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+ if (buf->params.host_mapped) {
+ // Make sure the PBO is not reused until GL is done with it. If a
+ // previous operation is pending, "update" it by creating a new
+ // fence that will cover the previous operation as well.
+ gl->DeleteSync(buf_gl->fence);
+ buf_gl->fence = gl->FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ }
+ }
+
+ return true;
+}
+
+static bool gl_tex_download(struct ra *ra, struct ra_tex_download_params *params)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_tex *tex = params->tex;
+ struct ra_tex_gl *tex_gl = tex->priv;
+ if (!tex_gl->fbo)
+ return false;
+ return gl_read_fbo_contents(gl, tex_gl->fbo, 1, tex_gl->format, tex_gl->type,
+ tex->params.w, tex->params.h, params->dst,
+ params->stride);
+}
+
+static void gl_buf_destroy(struct ra *ra, struct ra_buf *buf)
+{
+ if (!buf)
+ return;
+
+ GL *gl = ra_gl_get(ra);
+ struct ra_buf_gl *buf_gl = buf->priv;
+
+ if (buf_gl->fence)
+ gl->DeleteSync(buf_gl->fence);
+
+ if (buf->data) {
+ gl->BindBuffer(buf_gl->target, buf_gl->buffer);
+ gl->UnmapBuffer(buf_gl->target);
+ gl->BindBuffer(buf_gl->target, 0);
+ }
+ gl->DeleteBuffers(1, &buf_gl->buffer);
+
+ talloc_free(buf_gl);
+ talloc_free(buf);
+}
+
+static struct ra_buf *gl_buf_create(struct ra *ra,
+ const struct ra_buf_params *params)
+{
+ GL *gl = ra_gl_get(ra);
+
+ if (params->host_mapped && !gl->BufferStorage)
+ return NULL;
+
+ struct ra_buf *buf = talloc_zero(NULL, struct ra_buf);
+ buf->params = *params;
+ buf->params.initial_data = NULL;
+
+ struct ra_buf_gl *buf_gl = buf->priv = talloc_zero(NULL, struct ra_buf_gl);
+ gl->GenBuffers(1, &buf_gl->buffer);
+
+ switch (params->type) {
+ case RA_BUF_TYPE_TEX_UPLOAD: buf_gl->target = GL_PIXEL_UNPACK_BUFFER; break;
+ case RA_BUF_TYPE_SHADER_STORAGE: buf_gl->target = GL_SHADER_STORAGE_BUFFER; break;
+ case RA_BUF_TYPE_UNIFORM: buf_gl->target = GL_UNIFORM_BUFFER; break;
+ default: abort();
+ };
+
+ gl->BindBuffer(buf_gl->target, buf_gl->buffer);
+
+ if (params->host_mapped) {
+ unsigned flags = GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT |
+ GL_MAP_READ_BIT | GL_MAP_WRITE_BIT;
+
+ unsigned storflags = flags;
+ if (params->type == RA_BUF_TYPE_TEX_UPLOAD)
+ storflags |= GL_CLIENT_STORAGE_BIT;
+
+ gl->BufferStorage(buf_gl->target, params->size, params->initial_data,
+ storflags);
+ buf->data = gl->MapBufferRange(buf_gl->target, 0, params->size, flags);
+ if (!buf->data) {
+ gl_check_error(gl, ra->log, "mapping buffer");
+ gl_buf_destroy(ra, buf);
+ buf = NULL;
+ }
+ } else {
+ GLenum hint;
+ switch (params->type) {
+ case RA_BUF_TYPE_TEX_UPLOAD: hint = GL_STREAM_DRAW; break;
+ case RA_BUF_TYPE_SHADER_STORAGE: hint = GL_STREAM_COPY; break;
+ case RA_BUF_TYPE_UNIFORM: hint = GL_STATIC_DRAW; break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+
+ gl->BufferData(buf_gl->target, params->size, params->initial_data, hint);
+ }
+
+ gl->BindBuffer(buf_gl->target, 0);
+ return buf;
+}
+
+static void gl_buf_update(struct ra *ra, struct ra_buf *buf, ptrdiff_t offset,
+ const void *data, size_t size)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_buf_gl *buf_gl = buf->priv;
+ assert(buf->params.host_mutable);
+
+ gl->BindBuffer(buf_gl->target, buf_gl->buffer);
+ gl->BufferSubData(buf_gl->target, offset, size, data);
+ gl->BindBuffer(buf_gl->target, 0);
+}
+
+static bool gl_buf_poll(struct ra *ra, struct ra_buf *buf)
+{
+ // Non-persistently mapped buffers are always implicitly reusable in OpenGL,
+ // the implementation will create more buffers under the hood if needed.
+ if (!buf->data)
+ return true;
+
+ GL *gl = ra_gl_get(ra);
+ struct ra_buf_gl *buf_gl = buf->priv;
+
+ if (buf_gl->fence) {
+ GLenum res = gl->ClientWaitSync(buf_gl->fence, 0, 0); // non-blocking
+ if (res == GL_ALREADY_SIGNALED) {
+ gl->DeleteSync(buf_gl->fence);
+ buf_gl->fence = NULL;
+ }
+ }
+
+ return !buf_gl->fence;
+}
+
+static void gl_clear(struct ra *ra, struct ra_tex *dst, float color[4],
+ struct mp_rect *scissor)
+{
+ GL *gl = ra_gl_get(ra);
+
+ assert(dst->params.render_dst);
+ struct ra_tex_gl *dst_gl = dst->priv;
+
+ gl->BindFramebuffer(GL_FRAMEBUFFER, dst_gl->fbo);
+
+ gl->Scissor(scissor->x0, scissor->y0,
+ scissor->x1 - scissor->x0,
+ scissor->y1 - scissor->y0);
+
+ gl->Enable(GL_SCISSOR_TEST);
+ gl->ClearColor(color[0], color[1], color[2], color[3]);
+ gl->Clear(GL_COLOR_BUFFER_BIT);
+ gl->Disable(GL_SCISSOR_TEST);
+
+ gl->BindFramebuffer(GL_FRAMEBUFFER, 0);
+}
+
+static void gl_blit(struct ra *ra, struct ra_tex *dst, struct ra_tex *src,
+ struct mp_rect *dst_rc, struct mp_rect *src_rc)
+{
+ GL *gl = ra_gl_get(ra);
+
+ assert(src->params.blit_src);
+ assert(dst->params.blit_dst);
+
+ struct ra_tex_gl *src_gl = src->priv;
+ struct ra_tex_gl *dst_gl = dst->priv;
+
+ gl->BindFramebuffer(GL_READ_FRAMEBUFFER, src_gl->fbo);
+ gl->BindFramebuffer(GL_DRAW_FRAMEBUFFER, dst_gl->fbo);
+ gl->BlitFramebuffer(src_rc->x0, src_rc->y0, src_rc->x1, src_rc->y1,
+ dst_rc->x0, dst_rc->y0, dst_rc->x1, dst_rc->y1,
+ GL_COLOR_BUFFER_BIT, GL_NEAREST);
+ gl->BindFramebuffer(GL_READ_FRAMEBUFFER, 0);
+ gl->BindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
+}
+
+static int gl_desc_namespace(struct ra *ra, enum ra_vartype type)
+{
+ return type;
+}
+
+static void gl_renderpass_destroy(struct ra *ra, struct ra_renderpass *pass)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_renderpass_gl *pass_gl = pass->priv;
+ gl->DeleteProgram(pass_gl->program);
+ gl_vao_uninit(&pass_gl->vao);
+
+ talloc_free(pass_gl);
+ talloc_free(pass);
+}
+
+static const char *shader_typestr(GLenum type)
+{
+ switch (type) {
+ case GL_VERTEX_SHADER: return "vertex";
+ case GL_FRAGMENT_SHADER: return "fragment";
+ case GL_COMPUTE_SHADER: return "compute";
+ default: MP_ASSERT_UNREACHABLE();
+ }
+}
+
+static void compile_attach_shader(struct ra *ra, GLuint program,
+ GLenum type, const char *source, bool *ok)
+{
+ GL *gl = ra_gl_get(ra);
+
+ GLuint shader = gl->CreateShader(type);
+ gl->ShaderSource(shader, 1, &source, NULL);
+ gl->CompileShader(shader);
+ GLint status = 0;
+ gl->GetShaderiv(shader, GL_COMPILE_STATUS, &status);
+ GLint log_length = 0;
+ gl->GetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length);
+
+ int pri = status ? (log_length > 1 ? MSGL_V : MSGL_DEBUG) : MSGL_ERR;
+ const char *typestr = shader_typestr(type);
+ if (mp_msg_test(ra->log, pri)) {
+ MP_MSG(ra, pri, "%s shader source:\n", typestr);
+ mp_log_source(ra->log, pri, source);
+ }
+ if (log_length > 1) {
+ GLchar *logstr = talloc_zero_size(NULL, log_length + 1);
+ gl->GetShaderInfoLog(shader, log_length, NULL, logstr);
+ MP_MSG(ra, pri, "%s shader compile log (status=%d):\n%s\n",
+ typestr, status, logstr);
+ talloc_free(logstr);
+ }
+ if (gl->GetTranslatedShaderSourceANGLE && mp_msg_test(ra->log, MSGL_DEBUG)) {
+ GLint len = 0;
+ gl->GetShaderiv(shader, GL_TRANSLATED_SHADER_SOURCE_LENGTH_ANGLE, &len);
+ if (len > 0) {
+ GLchar *sstr = talloc_zero_size(NULL, len + 1);
+ gl->GetTranslatedShaderSourceANGLE(shader, len, NULL, sstr);
+ MP_DBG(ra, "Translated shader:\n");
+ mp_log_source(ra->log, MSGL_DEBUG, sstr);
+ }
+ }
+
+ gl->AttachShader(program, shader);
+ gl->DeleteShader(shader);
+
+ *ok &= status;
+}
+
+static void link_shader(struct ra *ra, GLuint program, bool *ok)
+{
+ GL *gl = ra_gl_get(ra);
+
+ gl->LinkProgram(program);
+ GLint status = 0;
+ gl->GetProgramiv(program, GL_LINK_STATUS, &status);
+ GLint log_length = 0;
+ gl->GetProgramiv(program, GL_INFO_LOG_LENGTH, &log_length);
+
+ int pri = status ? (log_length > 1 ? MSGL_V : MSGL_DEBUG) : MSGL_ERR;
+ if (mp_msg_test(ra->log, pri)) {
+ GLchar *logstr = talloc_zero_size(NULL, log_length + 1);
+ gl->GetProgramInfoLog(program, log_length, NULL, logstr);
+ MP_MSG(ra, pri, "shader link log (status=%d): %s\n", status, logstr);
+ talloc_free(logstr);
+ }
+
+ *ok &= status;
+}
+
+// either 'compute' or both 'vertex' and 'frag' are needed
+static GLuint compile_program(struct ra *ra, const struct ra_renderpass_params *p)
+{
+ GL *gl = ra_gl_get(ra);
+
+ GLuint prog = gl->CreateProgram();
+ bool ok = true;
+ if (p->type == RA_RENDERPASS_TYPE_COMPUTE)
+ compile_attach_shader(ra, prog, GL_COMPUTE_SHADER, p->compute_shader, &ok);
+ if (p->type == RA_RENDERPASS_TYPE_RASTER) {
+ compile_attach_shader(ra, prog, GL_VERTEX_SHADER, p->vertex_shader, &ok);
+ compile_attach_shader(ra, prog, GL_FRAGMENT_SHADER, p->frag_shader, &ok);
+ for (int n = 0; n < p->num_vertex_attribs; n++)
+ gl->BindAttribLocation(prog, n, p->vertex_attribs[n].name);
+ }
+ link_shader(ra, prog, &ok);
+ if (!ok) {
+ gl->DeleteProgram(prog);
+ prog = 0;
+ }
+ return prog;
+}
+
+static GLuint load_program(struct ra *ra, const struct ra_renderpass_params *p,
+ bstr *out_cached_data)
+{
+ GL *gl = ra_gl_get(ra);
+
+ GLuint prog = 0;
+
+ if (gl->ProgramBinary && p->cached_program.len > 4) {
+ GLenum format = AV_RL32(p->cached_program.start);
+ prog = gl->CreateProgram();
+ gl_check_error(gl, ra->log, "before loading program");
+ gl->ProgramBinary(prog, format, p->cached_program.start + 4,
+ p->cached_program.len - 4);
+ gl->GetError(); // discard potential useless error
+ GLint status = 0;
+ gl->GetProgramiv(prog, GL_LINK_STATUS, &status);
+ if (status) {
+ MP_DBG(ra, "Loading binary program succeeded.\n");
+ } else {
+ gl->DeleteProgram(prog);
+ prog = 0;
+ }
+ }
+
+ if (!prog) {
+ prog = compile_program(ra, p);
+
+ if (gl->GetProgramBinary && prog) {
+ GLint size = 0;
+ gl->GetProgramiv(prog, GL_PROGRAM_BINARY_LENGTH, &size);
+ uint8_t *buffer = talloc_size(NULL, size + 4);
+ GLsizei actual_size = 0;
+ GLenum binary_format = 0;
+ if (size > 0) {
+ gl->GetProgramBinary(prog, size, &actual_size, &binary_format,
+ buffer + 4);
+ }
+ AV_WL32(buffer, binary_format);
+ if (actual_size) {
+ *out_cached_data = (bstr){buffer, actual_size + 4};
+ } else {
+ talloc_free(buffer);
+ }
+ }
+ }
+
+ return prog;
+}
+
+static struct ra_renderpass *gl_renderpass_create(struct ra *ra,
+ const struct ra_renderpass_params *params)
+{
+ GL *gl = ra_gl_get(ra);
+
+ struct ra_renderpass *pass = talloc_zero(NULL, struct ra_renderpass);
+ pass->params = *ra_renderpass_params_copy(pass, params);
+ pass->params.cached_program = (bstr){0};
+ struct ra_renderpass_gl *pass_gl = pass->priv =
+ talloc_zero(NULL, struct ra_renderpass_gl);
+
+ bstr cached = {0};
+ pass_gl->program = load_program(ra, params, &cached);
+ if (!pass_gl->program) {
+ gl_renderpass_destroy(ra, pass);
+ return NULL;
+ }
+
+ talloc_steal(pass, cached.start);
+ pass->params.cached_program = cached;
+
+ gl->UseProgram(pass_gl->program);
+ for (int n = 0; n < params->num_inputs; n++) {
+ GLint loc =
+ gl->GetUniformLocation(pass_gl->program, params->inputs[n].name);
+ MP_TARRAY_APPEND(pass_gl, pass_gl->uniform_loc, pass_gl->num_uniform_loc,
+ loc);
+
+ // For compatibility with older OpenGL, we need to explicitly update
+ // the texture/image unit bindings after creating the shader program,
+ // since specifying it directly requires GLSL 4.20+
+ switch (params->inputs[n].type) {
+ case RA_VARTYPE_TEX:
+ case RA_VARTYPE_IMG_W:
+ gl->Uniform1i(loc, params->inputs[n].binding);
+ break;
+ }
+ }
+ gl->UseProgram(0);
+
+ gl_vao_init(&pass_gl->vao, gl, pass->params.vertex_stride,
+ pass->params.vertex_attribs, pass->params.num_vertex_attribs);
+
+ return pass;
+}
+
+static GLenum map_blend(enum ra_blend blend)
+{
+ switch (blend) {
+ case RA_BLEND_ZERO: return GL_ZERO;
+ case RA_BLEND_ONE: return GL_ONE;
+ case RA_BLEND_SRC_ALPHA: return GL_SRC_ALPHA;
+ case RA_BLEND_ONE_MINUS_SRC_ALPHA: return GL_ONE_MINUS_SRC_ALPHA;
+ default: return 0;
+ }
+}
+
+// Assumes program is current (gl->UseProgram(program)).
+static void update_uniform(struct ra *ra, struct ra_renderpass *pass,
+ struct ra_renderpass_input_val *val)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_renderpass_gl *pass_gl = pass->priv;
+
+ struct ra_renderpass_input *input = &pass->params.inputs[val->index];
+ assert(val->index >= 0 && val->index < pass_gl->num_uniform_loc);
+ GLint loc = pass_gl->uniform_loc[val->index];
+
+ switch (input->type) {
+ case RA_VARTYPE_INT: {
+ assert(input->dim_v * input->dim_m == 1);
+ if (loc < 0)
+ break;
+ gl->Uniform1i(loc, *(int *)val->data);
+ break;
+ }
+ case RA_VARTYPE_FLOAT: {
+ float *f = val->data;
+ if (loc < 0)
+ break;
+ if (input->dim_m == 1) {
+ switch (input->dim_v) {
+ case 1: gl->Uniform1f(loc, f[0]); break;
+ case 2: gl->Uniform2f(loc, f[0], f[1]); break;
+ case 3: gl->Uniform3f(loc, f[0], f[1], f[2]); break;
+ case 4: gl->Uniform4f(loc, f[0], f[1], f[2], f[3]); break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+ } else if (input->dim_v == 2 && input->dim_m == 2) {
+ gl->UniformMatrix2fv(loc, 1, GL_FALSE, f);
+ } else if (input->dim_v == 3 && input->dim_m == 3) {
+ gl->UniformMatrix3fv(loc, 1, GL_FALSE, f);
+ } else {
+ MP_ASSERT_UNREACHABLE();
+ }
+ break;
+ }
+ case RA_VARTYPE_IMG_W: {
+ struct ra_tex *tex = *(struct ra_tex **)val->data;
+ struct ra_tex_gl *tex_gl = tex->priv;
+ assert(tex->params.storage_dst);
+ gl->BindImageTexture(input->binding, tex_gl->texture, 0, GL_FALSE, 0,
+ GL_WRITE_ONLY, tex_gl->internal_format);
+ break;
+ }
+ case RA_VARTYPE_TEX: {
+ struct ra_tex *tex = *(struct ra_tex **)val->data;
+ struct ra_tex_gl *tex_gl = tex->priv;
+ assert(tex->params.render_src);
+ gl->ActiveTexture(GL_TEXTURE0 + input->binding);
+ gl->BindTexture(tex_gl->target, tex_gl->texture);
+ break;
+ }
+ case RA_VARTYPE_BUF_RO: // fall through
+ case RA_VARTYPE_BUF_RW: {
+ struct ra_buf *buf = *(struct ra_buf **)val->data;
+ struct ra_buf_gl *buf_gl = buf->priv;
+ gl->BindBufferBase(buf_gl->target, input->binding, buf_gl->buffer);
+ // SSBOs are not implicitly coherent in OpengL
+ if (input->type == RA_VARTYPE_BUF_RW)
+ gl->MemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
+ break;
+ }
+ default:
+ MP_ASSERT_UNREACHABLE();
+ }
+}
+
+static void disable_binding(struct ra *ra, struct ra_renderpass *pass,
+ struct ra_renderpass_input_val *val)
+{
+ GL *gl = ra_gl_get(ra);
+
+ struct ra_renderpass_input *input = &pass->params.inputs[val->index];
+
+ switch (input->type) {
+ case RA_VARTYPE_IMG_W: /* fall through */
+ case RA_VARTYPE_TEX: {
+ struct ra_tex *tex = *(struct ra_tex **)val->data;
+ struct ra_tex_gl *tex_gl = tex->priv;
+ assert(tex->params.render_src);
+ if (input->type == RA_VARTYPE_TEX) {
+ gl->ActiveTexture(GL_TEXTURE0 + input->binding);
+ gl->BindTexture(tex_gl->target, 0);
+ } else {
+ gl->BindImageTexture(input->binding, 0, 0, GL_FALSE, 0,
+ GL_WRITE_ONLY, tex_gl->internal_format);
+ }
+ break;
+ }
+ case RA_VARTYPE_BUF_RW:
+ gl->BindBufferBase(GL_SHADER_STORAGE_BUFFER, input->binding, 0);
+ break;
+ }
+}
+
+static void gl_renderpass_run(struct ra *ra,
+ const struct ra_renderpass_run_params *params)
+{
+ GL *gl = ra_gl_get(ra);
+ struct ra_renderpass *pass = params->pass;
+ struct ra_renderpass_gl *pass_gl = pass->priv;
+
+ gl->UseProgram(pass_gl->program);
+
+ for (int n = 0; n < params->num_values; n++)
+ update_uniform(ra, pass, &params->values[n]);
+ gl->ActiveTexture(GL_TEXTURE0);
+
+ switch (pass->params.type) {
+ case RA_RENDERPASS_TYPE_RASTER: {
+ struct ra_tex_gl *target_gl = params->target->priv;
+ assert(params->target->params.render_dst);
+ assert(params->target->params.format == pass->params.target_format);
+ gl->BindFramebuffer(GL_FRAMEBUFFER, target_gl->fbo);
+ if (pass->params.invalidate_target && gl->InvalidateFramebuffer) {
+ GLenum fb = target_gl->fbo ? GL_COLOR_ATTACHMENT0 : GL_COLOR;
+ gl->InvalidateFramebuffer(GL_FRAMEBUFFER, 1, &fb);
+ }
+ gl->Viewport(params->viewport.x0, params->viewport.y0,
+ mp_rect_w(params->viewport),
+ mp_rect_h(params->viewport));
+ gl->Scissor(params->scissors.x0, params->scissors.y0,
+ mp_rect_w(params->scissors),
+ mp_rect_h(params->scissors));
+ gl->Enable(GL_SCISSOR_TEST);
+ if (pass->params.enable_blend) {
+ gl->BlendFuncSeparate(map_blend(pass->params.blend_src_rgb),
+ map_blend(pass->params.blend_dst_rgb),
+ map_blend(pass->params.blend_src_alpha),
+ map_blend(pass->params.blend_dst_alpha));
+ gl->Enable(GL_BLEND);
+ }
+ gl_vao_draw_data(&pass_gl->vao, GL_TRIANGLES, params->vertex_data,
+ params->vertex_count);
+ gl->Disable(GL_SCISSOR_TEST);
+ gl->Disable(GL_BLEND);
+ gl->BindFramebuffer(GL_FRAMEBUFFER, 0);
+ break;
+ }
+ case RA_RENDERPASS_TYPE_COMPUTE: {
+ gl->DispatchCompute(params->compute_groups[0],
+ params->compute_groups[1],
+ params->compute_groups[2]);
+
+ gl->MemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT);
+ break;
+ }
+ default: MP_ASSERT_UNREACHABLE();
+ }
+
+ for (int n = 0; n < params->num_values; n++)
+ disable_binding(ra, pass, &params->values[n]);
+ gl->ActiveTexture(GL_TEXTURE0);
+
+ gl->UseProgram(0);
+}
+
+// Timers in GL use query objects, and are asynchronous. So pool a few of
+// these together. GL_QUERY_OBJECT_NUM should be large enough to avoid this
+// ever blocking. We can afford to throw query objects around, there's no
+// practical limit on them and their overhead is small.
+
+#define GL_QUERY_OBJECT_NUM 8
+
+struct gl_timer {
+ GLuint query[GL_QUERY_OBJECT_NUM];
+ int idx;
+ uint64_t result;
+ bool active;
+};
+
+static ra_timer *gl_timer_create(struct ra *ra)
+{
+ GL *gl = ra_gl_get(ra);
+
+ if (!gl->GenQueries)
+ return NULL;
+
+ struct gl_timer *timer = talloc_zero(NULL, struct gl_timer);
+ gl->GenQueries(GL_QUERY_OBJECT_NUM, timer->query);
+
+ return (ra_timer *)timer;
+}
+
+static void gl_timer_destroy(struct ra *ra, ra_timer *ratimer)
+{
+ if (!ratimer)
+ return;
+
+ GL *gl = ra_gl_get(ra);
+ struct gl_timer *timer = ratimer;
+
+ gl->DeleteQueries(GL_QUERY_OBJECT_NUM, timer->query);
+ talloc_free(timer);
+}
+
+static void gl_timer_start(struct ra *ra, ra_timer *ratimer)
+{
+ struct ra_gl *p = ra->priv;
+ GL *gl = p->gl;
+ struct gl_timer *timer = ratimer;
+
+ // GL_TIME_ELAPSED queries are not re-entrant, so just do nothing instead
+ // of crashing. Work-around for shitty GL limitations
+ if (p->timer_active)
+ return;
+
+ // If this query object already contains a result, we need to retrieve it
+ timer->result = 0;
+ if (gl->IsQuery(timer->query[timer->idx])) {
+ gl->GetQueryObjectui64v(timer->query[timer->idx], GL_QUERY_RESULT,
+ &timer->result);
+ }
+
+ gl->BeginQuery(GL_TIME_ELAPSED, timer->query[timer->idx++]);
+ timer->idx %= GL_QUERY_OBJECT_NUM;
+
+ p->timer_active = timer->active = true;
+}
+
+static uint64_t gl_timer_stop(struct ra *ra, ra_timer *ratimer)
+{
+ struct ra_gl *p = ra->priv;
+ GL *gl = p->gl;
+ struct gl_timer *timer = ratimer;
+
+ if (!timer->active)
+ return 0;
+
+ gl->EndQuery(GL_TIME_ELAPSED);
+ p->timer_active = timer->active = false;
+
+ return timer->result;
+}
+
+static void gl_debug_marker(struct ra *ra, const char *msg)
+{
+ struct ra_gl *p = ra->priv;
+
+ if (p->debug_enable)
+ gl_check_error(p->gl, ra->log, msg);
+}
+
+static struct ra_fns ra_fns_gl = {
+ .destroy = gl_destroy,
+ .tex_create = gl_tex_create,
+ .tex_destroy = gl_tex_destroy,
+ .tex_upload = gl_tex_upload,
+ .tex_download = gl_tex_download,
+ .buf_create = gl_buf_create,
+ .buf_destroy = gl_buf_destroy,
+ .buf_update = gl_buf_update,
+ .buf_poll = gl_buf_poll,
+ .clear = gl_clear,
+ .blit = gl_blit,
+ .uniform_layout = std140_layout,
+ .desc_namespace = gl_desc_namespace,
+ .renderpass_create = gl_renderpass_create,
+ .renderpass_destroy = gl_renderpass_destroy,
+ .renderpass_run = gl_renderpass_run,
+ .timer_create = gl_timer_create,
+ .timer_destroy = gl_timer_destroy,
+ .timer_start = gl_timer_start,
+ .timer_stop = gl_timer_stop,
+ .debug_marker = gl_debug_marker,
+};
diff --git a/video/out/opengl/ra_gl.h b/video/out/opengl/ra_gl.h
new file mode 100644
index 0000000..9844977
--- /dev/null
+++ b/video/out/opengl/ra_gl.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "common.h"
+#include "utils.h"
+
+struct ra *ra_create_gl(GL *gl, struct mp_log *log);
+struct ra_tex *ra_create_wrapped_tex(struct ra *ra,
+ const struct ra_tex_params *params,
+ GLuint gl_texture);
+struct ra_tex *ra_create_wrapped_fb(struct ra *ra, GLuint gl_fbo, int w, int h);
+GL *ra_gl_get(struct ra *ra);
+void ra_gl_set_debug(struct ra *ra, bool enable);
+void ra_gl_get_format(const struct ra_format *fmt, GLint *out_internal_format,
+ GLenum *out_format, GLenum *out_type);
+void ra_gl_get_raw_tex(struct ra *ra, struct ra_tex *tex,
+ GLuint *out_texture, GLenum *out_target);
+bool ra_is_gl(struct ra *ra);
diff --git a/video/out/opengl/utils.c b/video/out/opengl/utils.c
new file mode 100644
index 0000000..a551ce4
--- /dev/null
+++ b/video/out/opengl/utils.c
@@ -0,0 +1,282 @@
+/*
+ * This file is part of mpv.
+ * Parts based on MPlayer code by Reimar Döffinger.
+ *
+ * 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 <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <assert.h>
+
+#include <libavutil/sha.h>
+#include <libavutil/intreadwrite.h>
+#include <libavutil/mem.h>
+
+#include "osdep/io.h"
+
+#include "common/common.h"
+#include "options/path.h"
+#include "stream/stream.h"
+#include "formats.h"
+#include "utils.h"
+
+// GLU has this as gluErrorString (we don't use GLU, as it is legacy-OpenGL)
+static const char *gl_error_to_string(GLenum error)
+{
+ switch (error) {
+ case GL_INVALID_ENUM: return "INVALID_ENUM";
+ case GL_INVALID_VALUE: return "INVALID_VALUE";
+ case GL_INVALID_OPERATION: return "INVALID_OPERATION";
+ case GL_INVALID_FRAMEBUFFER_OPERATION: return "INVALID_FRAMEBUFFER_OPERATION";
+ case GL_OUT_OF_MEMORY: return "OUT_OF_MEMORY";
+ default: return "unknown";
+ }
+}
+
+void gl_check_error(GL *gl, struct mp_log *log, const char *info)
+{
+ for (;;) {
+ GLenum error = gl->GetError();
+ if (error == GL_NO_ERROR)
+ break;
+ mp_msg(log, MSGL_ERR, "%s: OpenGL error %s.\n", info,
+ gl_error_to_string(error));
+ }
+}
+
+static int get_alignment(int stride)
+{
+ if (stride % 8 == 0)
+ return 8;
+ if (stride % 4 == 0)
+ return 4;
+ if (stride % 2 == 0)
+ return 2;
+ return 1;
+}
+
+// upload a texture, handling things like stride and slices
+// target: texture target, usually GL_TEXTURE_2D
+// format, type: texture parameters
+// dataptr, stride: image data
+// x, y, width, height: part of the image to upload
+void gl_upload_tex(GL *gl, GLenum target, GLenum format, GLenum type,
+ const void *dataptr, int stride,
+ int x, int y, int w, int h)
+{
+ int bpp = gl_bytes_per_pixel(format, type);
+ const uint8_t *data = dataptr;
+ int y_max = y + h;
+ if (w <= 0 || h <= 0 || !bpp)
+ return;
+ assert(stride > 0);
+ gl->PixelStorei(GL_UNPACK_ALIGNMENT, get_alignment(stride));
+ int slice = h;
+ if (gl->mpgl_caps & MPGL_CAP_ROW_LENGTH) {
+ // this is not always correct, but should work for MPlayer
+ gl->PixelStorei(GL_UNPACK_ROW_LENGTH, stride / bpp);
+ } else {
+ if (stride != bpp * w)
+ slice = 1; // very inefficient, but at least it works
+ }
+ for (; y + slice <= y_max; y += slice) {
+ gl->TexSubImage2D(target, 0, x, y, w, slice, format, type, data);
+ data += stride * slice;
+ }
+ if (y < y_max)
+ gl->TexSubImage2D(target, 0, x, y, w, y_max - y, format, type, data);
+ if (gl->mpgl_caps & MPGL_CAP_ROW_LENGTH)
+ gl->PixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+ gl->PixelStorei(GL_UNPACK_ALIGNMENT, 4);
+}
+
+bool gl_read_fbo_contents(GL *gl, int fbo, int dir, GLenum format, GLenum type,
+ int w, int h, uint8_t *dst, int dst_stride)
+{
+ assert(dir == 1 || dir == -1);
+ if (fbo == 0 && gl->es)
+ return false; // ES can't read from front buffer
+ gl->BindFramebuffer(GL_FRAMEBUFFER, fbo);
+ GLenum obj = fbo ? GL_COLOR_ATTACHMENT0 : GL_FRONT;
+ gl->PixelStorei(GL_PACK_ALIGNMENT, 1);
+ gl->ReadBuffer(obj);
+ // reading by line allows flipping, and avoids stride-related trouble
+ int y1 = dir > 0 ? 0 : h;
+ for (int y = 0; y < h; y++)
+ gl->ReadPixels(0, y, w, 1, format, type, dst + (y1 + dir * y) * dst_stride);
+ gl->PixelStorei(GL_PACK_ALIGNMENT, 4);
+ gl->BindFramebuffer(GL_FRAMEBUFFER, 0);
+ return true;
+}
+
+static void gl_vao_enable_attribs(struct gl_vao *vao)
+{
+ GL *gl = vao->gl;
+
+ for (int n = 0; n < vao->num_entries; n++) {
+ const struct ra_renderpass_input *e = &vao->entries[n];
+ GLenum type = 0;
+ bool normalized = false;
+ switch (e->type) {
+ case RA_VARTYPE_INT:
+ type = GL_INT;
+ break;
+ case RA_VARTYPE_FLOAT:
+ type = GL_FLOAT;
+ break;
+ case RA_VARTYPE_BYTE_UNORM:
+ type = GL_UNSIGNED_BYTE;
+ normalized = true;
+ break;
+ default:
+ abort();
+ }
+ assert(e->dim_m == 1);
+
+ gl->EnableVertexAttribArray(n);
+ gl->VertexAttribPointer(n, e->dim_v, type, normalized,
+ vao->stride, (void *)(intptr_t)e->offset);
+ }
+}
+
+void gl_vao_init(struct gl_vao *vao, GL *gl, int stride,
+ const struct ra_renderpass_input *entries,
+ int num_entries)
+{
+ assert(!vao->vao);
+ assert(!vao->buffer);
+
+ *vao = (struct gl_vao){
+ .gl = gl,
+ .stride = stride,
+ .entries = entries,
+ .num_entries = num_entries,
+ };
+
+ gl->GenBuffers(1, &vao->buffer);
+
+ if (gl->BindVertexArray) {
+ gl->BindBuffer(GL_ARRAY_BUFFER, vao->buffer);
+
+ gl->GenVertexArrays(1, &vao->vao);
+ gl->BindVertexArray(vao->vao);
+ gl_vao_enable_attribs(vao);
+ gl->BindVertexArray(0);
+
+ gl->BindBuffer(GL_ARRAY_BUFFER, 0);
+ }
+}
+
+void gl_vao_uninit(struct gl_vao *vao)
+{
+ GL *gl = vao->gl;
+ if (!gl)
+ return;
+
+ if (gl->DeleteVertexArrays)
+ gl->DeleteVertexArrays(1, &vao->vao);
+ gl->DeleteBuffers(1, &vao->buffer);
+
+ *vao = (struct gl_vao){0};
+}
+
+static void gl_vao_bind(struct gl_vao *vao)
+{
+ GL *gl = vao->gl;
+
+ if (gl->BindVertexArray) {
+ gl->BindVertexArray(vao->vao);
+ } else {
+ gl->BindBuffer(GL_ARRAY_BUFFER, vao->buffer);
+ gl_vao_enable_attribs(vao);
+ gl->BindBuffer(GL_ARRAY_BUFFER, 0);
+ }
+}
+
+static void gl_vao_unbind(struct gl_vao *vao)
+{
+ GL *gl = vao->gl;
+
+ if (gl->BindVertexArray) {
+ gl->BindVertexArray(0);
+ } else {
+ for (int n = 0; n < vao->num_entries; n++)
+ gl->DisableVertexAttribArray(n);
+ }
+}
+
+// Draw the vertex data (as described by the gl_vao_entry entries) in ptr
+// to the screen. num is the number of vertexes. prim is usually GL_TRIANGLES.
+// If ptr is NULL, then skip the upload, and use the data uploaded with the
+// previous call.
+void gl_vao_draw_data(struct gl_vao *vao, GLenum prim, void *ptr, size_t num)
+{
+ GL *gl = vao->gl;
+
+ if (ptr) {
+ gl->BindBuffer(GL_ARRAY_BUFFER, vao->buffer);
+ gl->BufferData(GL_ARRAY_BUFFER, num * vao->stride, ptr, GL_STREAM_DRAW);
+ gl->BindBuffer(GL_ARRAY_BUFFER, 0);
+ }
+
+ gl_vao_bind(vao);
+
+ gl->DrawArrays(prim, 0, num);
+
+ gl_vao_unbind(vao);
+}
+
+static void GLAPIENTRY gl_debug_cb(GLenum source, GLenum type, GLuint id,
+ GLenum severity, GLsizei length,
+ const GLchar *message, const void *userParam)
+{
+ // keep in mind that the debug callback can be asynchronous
+ struct mp_log *log = (void *)userParam;
+ int level = MSGL_ERR;
+ switch (severity) {
+ case GL_DEBUG_SEVERITY_NOTIFICATION:level = MSGL_V; break;
+ case GL_DEBUG_SEVERITY_LOW: level = MSGL_INFO; break;
+ case GL_DEBUG_SEVERITY_MEDIUM: level = MSGL_WARN; break;
+ case GL_DEBUG_SEVERITY_HIGH: level = MSGL_ERR; break;
+ }
+ mp_msg(log, level, "GL: %s\n", message);
+}
+
+void gl_set_debug_logger(GL *gl, struct mp_log *log)
+{
+ if (gl->DebugMessageCallback)
+ gl->DebugMessageCallback(log ? gl_debug_cb : NULL, log);
+}
+
+// Given a GL combined extension string in extensions, find out whether ext
+// is included in it. Basically, a word search.
+bool gl_check_extension(const char *extensions, const char *ext)
+{
+ int len = strlen(ext);
+ const char *cur = extensions;
+ while (cur) {
+ cur = strstr(cur, ext);
+ if (!cur)
+ break;
+ if ((cur == extensions || cur[-1] == ' ') &&
+ (cur[len] == '\0' || cur[len] == ' '))
+ return true;
+ cur += len;
+ }
+ return false;
+}
diff --git a/video/out/opengl/utils.h b/video/out/opengl/utils.h
new file mode 100644
index 0000000..9bcadae
--- /dev/null
+++ b/video/out/opengl/utils.h
@@ -0,0 +1,57 @@
+/*
+ * This file is part of mpv.
+ * Parts based on MPlayer code by Reimar Döffinger.
+ *
+ * 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/>.
+ */
+
+#ifndef MP_GL_UTILS_
+#define MP_GL_UTILS_
+
+#include <math.h>
+
+#include "video/out/gpu/utils.h"
+#include "common.h"
+
+struct mp_log;
+
+void gl_check_error(GL *gl, struct mp_log *log, const char *info);
+
+void gl_upload_tex(GL *gl, GLenum target, GLenum format, GLenum type,
+ const void *dataptr, int stride,
+ int x, int y, int w, int h);
+
+bool gl_read_fbo_contents(GL *gl, int fbo, int dir, GLenum format, GLenum type,
+ int w, int h, uint8_t *dst, int dst_stride);
+
+struct gl_vao {
+ GL *gl;
+ GLuint vao; // the VAO object, or 0 if unsupported by driver
+ GLuint buffer; // GL_ARRAY_BUFFER used for the data
+ int stride; // size of each element (interleaved elements are assumed)
+ const struct ra_renderpass_input *entries;
+ int num_entries;
+};
+
+void gl_vao_init(struct gl_vao *vao, GL *gl, int stride,
+ const struct ra_renderpass_input *entries,
+ int num_entries);
+void gl_vao_uninit(struct gl_vao *vao);
+void gl_vao_draw_data(struct gl_vao *vao, GLenum prim, void *ptr, size_t num);
+
+void gl_set_debug_logger(GL *gl, struct mp_log *log);
+
+bool gl_check_extension(const char *extensions, const char *ext);
+
+#endif
diff --git a/video/out/placebo/ra_pl.c b/video/out/placebo/ra_pl.c
new file mode 100644
index 0000000..6259651
--- /dev/null
+++ b/video/out/placebo/ra_pl.c
@@ -0,0 +1,677 @@
+#include "common/common.h"
+#include "common/msg.h"
+
+#include "ra_pl.h"
+#include "utils.h"
+
+struct ra_pl {
+ pl_gpu gpu;
+ struct ra_timer_pl *active_timer;
+};
+
+static inline pl_gpu get_gpu(const struct ra *ra)
+{
+ struct ra_pl *p = ra->priv;
+ return p->gpu;
+}
+
+static struct ra_fns ra_fns_pl;
+
+pl_gpu ra_pl_get(const struct ra *ra)
+{
+ return ra->fns == &ra_fns_pl ? get_gpu(ra) : NULL;
+}
+
+static pl_timer get_active_timer(const struct ra *ra);
+
+struct ra *ra_create_pl(pl_gpu gpu, struct mp_log *log)
+{
+ assert(gpu);
+
+ struct ra *ra = talloc_zero(NULL, struct ra);
+ ra->log = log;
+ ra->fns = &ra_fns_pl;
+
+ struct ra_pl *p = ra->priv = talloc_zero(ra, struct ra_pl);
+ p->gpu = gpu;
+
+ ra->glsl_version = gpu->glsl.version;
+ ra->glsl_vulkan = gpu->glsl.vulkan;
+ ra->glsl_es = gpu->glsl.gles;
+
+ ra->caps = RA_CAP_DIRECT_UPLOAD | RA_CAP_NESTED_ARRAY | RA_CAP_FRAGCOORD;
+
+ if (gpu->glsl.compute)
+ ra->caps |= RA_CAP_COMPUTE | RA_CAP_NUM_GROUPS;
+ if (gpu->limits.compute_queues > gpu->limits.fragment_queues)
+ ra->caps |= RA_CAP_PARALLEL_COMPUTE;
+ if (gpu->limits.max_variable_comps)
+ ra->caps |= RA_CAP_GLOBAL_UNIFORM;
+ if (!gpu->limits.host_cached)
+ ra->caps |= RA_CAP_SLOW_DR;
+
+ if (gpu->limits.max_tex_1d_dim)
+ ra->caps |= RA_CAP_TEX_1D;
+ if (gpu->limits.max_tex_3d_dim)
+ ra->caps |= RA_CAP_TEX_3D;
+ if (gpu->limits.max_ubo_size)
+ ra->caps |= RA_CAP_BUF_RO;
+ if (gpu->limits.max_ssbo_size)
+ ra->caps |= RA_CAP_BUF_RW;
+ if (gpu->glsl.min_gather_offset && gpu->glsl.max_gather_offset)
+ ra->caps |= RA_CAP_GATHER;
+
+ // Semi-hack: assume all textures are blittable if r8 is
+ pl_fmt r8 = pl_find_named_fmt(gpu, "r8");
+ if (r8->caps & PL_FMT_CAP_BLITTABLE)
+ ra->caps |= RA_CAP_BLIT;
+
+ ra->max_texture_wh = gpu->limits.max_tex_2d_dim;
+ ra->max_pushc_size = gpu->limits.max_pushc_size;
+ ra->max_compute_group_threads = gpu->glsl.max_group_threads;
+ ra->max_shmem = gpu->glsl.max_shmem_size;
+
+ // Set up format wrappers
+ for (int i = 0; i < gpu->num_formats; i++) {
+ pl_fmt plfmt = gpu->formats[i];
+ static const enum ra_ctype fmt_type_map[PL_FMT_TYPE_COUNT] = {
+ [PL_FMT_UNORM] = RA_CTYPE_UNORM,
+ [PL_FMT_UINT] = RA_CTYPE_UINT,
+ [PL_FMT_FLOAT] = RA_CTYPE_FLOAT,
+ };
+
+ enum ra_ctype type = fmt_type_map[plfmt->type];
+ if (!type || !(plfmt->caps & PL_FMT_CAP_SAMPLEABLE))
+ continue;
+
+ struct ra_format *rafmt = talloc_zero(ra, struct ra_format);
+ *rafmt = (struct ra_format) {
+ .name = plfmt->name,
+ .priv = (void *) plfmt,
+ .ctype = type,
+ .ordered = pl_fmt_is_ordered(plfmt),
+ .num_components = plfmt->num_components,
+ .pixel_size = plfmt->texel_size,
+ .linear_filter = plfmt->caps & PL_FMT_CAP_LINEAR,
+ .renderable = plfmt->caps & PL_FMT_CAP_RENDERABLE,
+ .storable = plfmt->caps & PL_FMT_CAP_STORABLE,
+ .glsl_format = plfmt->glsl_format,
+ };
+
+ for (int c = 0; c < plfmt->num_components; c++) {
+ rafmt->component_size[c] = plfmt->host_bits[c];
+ rafmt->component_depth[c] = plfmt->component_depth[c];
+ }
+
+ MP_TARRAY_APPEND(ra, ra->formats, ra->num_formats, rafmt);
+ }
+
+ return ra;
+}
+
+static void destroy_ra_pl(struct ra *ra)
+{
+ talloc_free(ra);
+}
+
+static struct ra_format *map_fmt(struct ra *ra, pl_fmt plfmt)
+{
+ for (int i = 0; i < ra->num_formats; i++) {
+ if (ra->formats[i]->priv == plfmt)
+ return ra->formats[i];
+ }
+
+ MP_ERR(ra, "Failed mapping pl_fmt '%s' to ra_fmt?\n", plfmt->name);
+ return NULL;
+}
+
+bool mppl_wrap_tex(struct ra *ra, pl_tex pltex, struct ra_tex *out_tex)
+{
+ if (!pltex)
+ return false;
+
+ *out_tex = (struct ra_tex) {
+ .params = {
+ .dimensions = pl_tex_params_dimension(pltex->params),
+ .w = pltex->params.w,
+ .h = pltex->params.h,
+ .d = pltex->params.d,
+ .format = map_fmt(ra, pltex->params.format),
+ .render_src = pltex->params.sampleable,
+ .render_dst = pltex->params.renderable,
+ .storage_dst = pltex->params.storable,
+ .blit_src = pltex->params.blit_src,
+ .blit_dst = pltex->params.blit_dst,
+ .host_mutable = pltex->params.host_writable,
+ .downloadable = pltex->params.host_readable,
+ // These don't exist upstream, so just pick something reasonable
+ .src_linear = pltex->params.format->caps & PL_FMT_CAP_LINEAR,
+ .src_repeat = false,
+ },
+ .priv = (void *) pltex,
+ };
+
+ return !!out_tex->params.format;
+}
+
+static struct ra_tex *tex_create_pl(struct ra *ra,
+ const struct ra_tex_params *params)
+{
+ pl_gpu gpu = get_gpu(ra);
+ pl_tex pltex = pl_tex_create(gpu, &(struct pl_tex_params) {
+ .w = params->w,
+ .h = params->dimensions >= 2 ? params->h : 0,
+ .d = params->dimensions >= 3 ? params->d : 0,
+ .format = params->format->priv,
+ .sampleable = params->render_src,
+ .renderable = params->render_dst,
+ .storable = params->storage_dst,
+ .blit_src = params->blit_src,
+ .blit_dst = params->blit_dst || params->render_dst,
+ .host_writable = params->host_mutable,
+ .host_readable = params->downloadable,
+ .initial_data = params->initial_data,
+ });
+
+ struct ra_tex *ratex = talloc_ptrtype(NULL, ratex);
+ if (!mppl_wrap_tex(ra, pltex, ratex)) {
+ pl_tex_destroy(gpu, &pltex);
+ talloc_free(ratex);
+ return NULL;
+ }
+
+ // Keep track of these, so we can correctly bind them later
+ ratex->params.src_repeat = params->src_repeat;
+ ratex->params.src_linear = params->src_linear;
+
+ return ratex;
+}
+
+static void tex_destroy_pl(struct ra *ra, struct ra_tex *tex)
+{
+ if (!tex)
+ return;
+
+ pl_tex_destroy(get_gpu(ra), (pl_tex *) &tex->priv);
+ talloc_free(tex);
+}
+
+static bool tex_upload_pl(struct ra *ra, const struct ra_tex_upload_params *params)
+{
+ pl_gpu gpu = get_gpu(ra);
+ pl_tex tex = params->tex->priv;
+ struct pl_tex_transfer_params pl_params = {
+ .tex = tex,
+ .buf = params->buf ? params->buf->priv : NULL,
+ .buf_offset = params->buf_offset,
+ .ptr = (void *) params->src,
+ .timer = get_active_timer(ra),
+ };
+
+ pl_buf staging = NULL;
+ if (params->tex->params.dimensions == 2) {
+ if (params->rc) {
+ pl_params.rc = (struct pl_rect3d) {
+ .x0 = params->rc->x0, .x1 = params->rc->x1,
+ .y0 = params->rc->y0, .y1 = params->rc->y1,
+ };
+ }
+
+ pl_params.row_pitch = params->stride;
+ }
+
+ bool ok = pl_tex_upload(gpu, &pl_params);
+ pl_buf_destroy(gpu, &staging);
+ return ok;
+}
+
+static bool tex_download_pl(struct ra *ra, struct ra_tex_download_params *params)
+{
+ pl_tex tex = params->tex->priv;
+ struct pl_tex_transfer_params pl_params = {
+ .tex = tex,
+ .ptr = params->dst,
+ .timer = get_active_timer(ra),
+ .row_pitch = params->stride,
+ };
+
+ return pl_tex_download(get_gpu(ra), &pl_params);
+}
+
+static struct ra_buf *buf_create_pl(struct ra *ra,
+ const struct ra_buf_params *params)
+{
+ pl_buf plbuf = pl_buf_create(get_gpu(ra), &(struct pl_buf_params) {
+ .size = params->size,
+ .uniform = params->type == RA_BUF_TYPE_UNIFORM,
+ .storable = params->type == RA_BUF_TYPE_SHADER_STORAGE,
+ .host_mapped = params->host_mapped,
+ .host_writable = params->host_mutable,
+ .initial_data = params->initial_data,
+ });
+
+ if (!plbuf)
+ return NULL;
+
+ struct ra_buf *rabuf = talloc_ptrtype(NULL, rabuf);
+ *rabuf = (struct ra_buf) {
+ .params = *params,
+ .data = plbuf->data,
+ .priv = (void *) plbuf,
+ };
+
+ rabuf->params.initial_data = NULL;
+ return rabuf;
+}
+
+static void buf_destroy_pl(struct ra *ra, struct ra_buf *buf)
+{
+ if (!buf)
+ return;
+
+ pl_buf_destroy(get_gpu(ra), (pl_buf *) &buf->priv);
+ talloc_free(buf);
+}
+
+static void buf_update_pl(struct ra *ra, struct ra_buf *buf, ptrdiff_t offset,
+ const void *data, size_t size)
+{
+ pl_buf_write(get_gpu(ra), buf->priv, offset, data, size);
+}
+
+static bool buf_poll_pl(struct ra *ra, struct ra_buf *buf)
+{
+ return !pl_buf_poll(get_gpu(ra), buf->priv, 0);
+}
+
+static void clear_pl(struct ra *ra, struct ra_tex *dst, float color[4],
+ struct mp_rect *scissor)
+{
+ // TODO: implement scissor clearing by bltting a 1x1 tex instead
+ pl_tex_clear(get_gpu(ra), dst->priv, color);
+}
+
+static void blit_pl(struct ra *ra, struct ra_tex *dst, struct ra_tex *src,
+ struct mp_rect *dst_rc, struct mp_rect *src_rc)
+{
+ struct pl_rect3d plsrc = {0}, pldst = {0};
+ if (src_rc) {
+ plsrc.x0 = MPMIN(MPMAX(src_rc->x0, 0), src->params.w);
+ plsrc.y0 = MPMIN(MPMAX(src_rc->y0, 0), src->params.h);
+ plsrc.x1 = MPMIN(MPMAX(src_rc->x1, 0), src->params.w);
+ plsrc.y1 = MPMIN(MPMAX(src_rc->y1, 0), src->params.h);
+ }
+
+ if (dst_rc) {
+ pldst.x0 = MPMIN(MPMAX(dst_rc->x0, 0), dst->params.w);
+ pldst.y0 = MPMIN(MPMAX(dst_rc->y0, 0), dst->params.h);
+ pldst.x1 = MPMIN(MPMAX(dst_rc->x1, 0), dst->params.w);
+ pldst.y1 = MPMIN(MPMAX(dst_rc->y1, 0), dst->params.h);
+ }
+
+ pl_tex_blit(get_gpu(ra), &(struct pl_tex_blit_params) {
+ .src = src->priv,
+ .dst = dst->priv,
+ .src_rc = plsrc,
+ .dst_rc = pldst,
+ .sample_mode = src->params.src_linear ? PL_TEX_SAMPLE_LINEAR
+ : PL_TEX_SAMPLE_NEAREST,
+ });
+}
+
+static const enum pl_var_type var_type[RA_VARTYPE_COUNT] = {
+ [RA_VARTYPE_INT] = PL_VAR_SINT,
+ [RA_VARTYPE_FLOAT] = PL_VAR_FLOAT,
+};
+
+static const enum pl_desc_type desc_type[RA_VARTYPE_COUNT] = {
+ [RA_VARTYPE_TEX] = PL_DESC_SAMPLED_TEX,
+ [RA_VARTYPE_IMG_W] = PL_DESC_STORAGE_IMG,
+ [RA_VARTYPE_BUF_RO] = PL_DESC_BUF_UNIFORM,
+ [RA_VARTYPE_BUF_RW] = PL_DESC_BUF_STORAGE,
+};
+
+static const enum pl_fmt_type fmt_type[RA_VARTYPE_COUNT] = {
+ [RA_VARTYPE_INT] = PL_FMT_SINT,
+ [RA_VARTYPE_FLOAT] = PL_FMT_FLOAT,
+ [RA_VARTYPE_BYTE_UNORM] = PL_FMT_UNORM,
+};
+
+static const size_t var_size[RA_VARTYPE_COUNT] = {
+ [RA_VARTYPE_INT] = sizeof(int),
+ [RA_VARTYPE_FLOAT] = sizeof(float),
+ [RA_VARTYPE_BYTE_UNORM] = sizeof(uint8_t),
+};
+
+static struct ra_layout uniform_layout_pl(struct ra_renderpass_input *inp)
+{
+ // To get the alignment requirements, we try laying this out with
+ // an offset of 1 and then see where it ends up. This will always be
+ // the minimum alignment requirement.
+ struct pl_var_layout layout = pl_buf_uniform_layout(1, &(struct pl_var) {
+ .name = inp->name,
+ .type = var_type[inp->type],
+ .dim_v = inp->dim_v,
+ .dim_m = inp->dim_m,
+ .dim_a = 1,
+ });
+
+ return (struct ra_layout) {
+ .align = layout.offset,
+ .stride = layout.stride,
+ .size = layout.size,
+ };
+}
+
+static struct ra_layout push_constant_layout_pl(struct ra_renderpass_input *inp)
+{
+ struct pl_var_layout layout = pl_push_constant_layout(1, &(struct pl_var) {
+ .name = inp->name,
+ .type = var_type[inp->type],
+ .dim_v = inp->dim_v,
+ .dim_m = inp->dim_m,
+ .dim_a = 1,
+ });
+
+ return (struct ra_layout) {
+ .align = layout.offset,
+ .stride = layout.stride,
+ .size = layout.size,
+ };
+}
+
+static int desc_namespace_pl(struct ra *ra, enum ra_vartype type)
+{
+ return pl_desc_namespace(get_gpu(ra), desc_type[type]);
+}
+
+struct pass_priv {
+ pl_pass pass;
+ uint16_t *inp_index; // index translation map
+ // Space to hold the descriptor bindings and variable updates
+ struct pl_desc_binding *binds;
+ struct pl_var_update *varups;
+ int num_varups;
+};
+
+static struct ra_renderpass *renderpass_create_pl(struct ra *ra,
+ const struct ra_renderpass_params *params)
+{
+ void *tmp = talloc_new(NULL);
+ pl_gpu gpu = get_gpu(ra);
+ struct ra_renderpass *pass = NULL;
+
+ static const enum pl_pass_type pass_type[] = {
+ [RA_RENDERPASS_TYPE_RASTER] = PL_PASS_RASTER,
+ [RA_RENDERPASS_TYPE_COMPUTE] = PL_PASS_COMPUTE,
+ };
+
+ struct pl_var *vars = NULL;
+ struct pl_desc *descs = NULL;
+ int num_vars = 0, num_descs = 0;
+
+ struct pass_priv *priv = talloc_ptrtype(tmp, priv);
+ priv->inp_index = talloc_zero_array(priv, uint16_t, params->num_inputs);
+
+ for (int i = 0; i < params->num_inputs; i++) {
+ const struct ra_renderpass_input *inp = &params->inputs[i];
+ if (var_type[inp->type]) {
+ priv->inp_index[i] = num_vars;
+ MP_TARRAY_APPEND(tmp, vars, num_vars, (struct pl_var) {
+ .name = inp->name,
+ .type = var_type[inp->type],
+ .dim_v = inp->dim_v,
+ .dim_m = inp->dim_m,
+ .dim_a = 1,
+ });
+ } else if (desc_type[inp->type]) {
+ priv->inp_index[i] = num_descs;
+ MP_TARRAY_APPEND(tmp, descs, num_descs, (struct pl_desc) {
+ .name = inp->name,
+ .type = desc_type[inp->type],
+ .binding = inp->binding,
+ .access = inp->type == RA_VARTYPE_IMG_W ? PL_DESC_ACCESS_WRITEONLY
+ : inp->type == RA_VARTYPE_BUF_RW ? PL_DESC_ACCESS_READWRITE
+ : PL_DESC_ACCESS_READONLY,
+ });
+ }
+ }
+
+ // Allocate space to store the bindings map persistently
+ priv->binds = talloc_zero_array(priv, struct pl_desc_binding, num_descs);
+
+ struct pl_pass_params pl_params = {
+ .type = pass_type[params->type],
+ .variables = vars,
+ .num_variables = num_vars,
+ .descriptors = descs,
+ .num_descriptors = num_descs,
+ .push_constants_size = params->push_constants_size,
+ .glsl_shader = params->type == RA_RENDERPASS_TYPE_COMPUTE
+ ? params->compute_shader
+ : params->frag_shader,
+ };
+
+ struct pl_blend_params blend_params;
+
+ if (params->type == RA_RENDERPASS_TYPE_RASTER) {
+ pl_params.vertex_shader = params->vertex_shader;
+ pl_params.vertex_type = PL_PRIM_TRIANGLE_LIST;
+ pl_params.vertex_stride = params->vertex_stride;
+ pl_params.load_target = !params->invalidate_target;
+ pl_params.target_format = params->target_format->priv;
+
+ if (params->enable_blend) {
+ pl_params.blend_params = &blend_params;
+ blend_params = (struct pl_blend_params) {
+ // Same enum order as ra_blend
+ .src_rgb = (enum pl_blend_mode) params->blend_src_rgb,
+ .dst_rgb = (enum pl_blend_mode) params->blend_dst_rgb,
+ .src_alpha = (enum pl_blend_mode) params->blend_src_alpha,
+ .dst_alpha = (enum pl_blend_mode) params->blend_dst_alpha,
+ };
+ }
+
+ for (int i = 0; i < params->num_vertex_attribs; i++) {
+ const struct ra_renderpass_input *inp = &params->vertex_attribs[i];
+ struct pl_vertex_attrib attrib = {
+ .name = inp->name,
+ .offset = inp->offset,
+ .location = i,
+ .fmt = pl_find_fmt(gpu, fmt_type[inp->type], inp->dim_v, 0,
+ var_size[inp->type] * 8, PL_FMT_CAP_VERTEX),
+ };
+
+ if (!attrib.fmt) {
+ MP_ERR(ra, "Failed mapping vertex attrib '%s' to pl_fmt?\n",
+ inp->name);
+ goto error;
+ }
+
+ MP_TARRAY_APPEND(tmp, pl_params.vertex_attribs,
+ pl_params.num_vertex_attribs, attrib);
+ }
+ }
+
+ priv->pass = pl_pass_create(gpu, &pl_params);
+ if (!priv->pass)
+ goto error;
+
+ pass = talloc_ptrtype(NULL, pass);
+ *pass = (struct ra_renderpass) {
+ .params = *ra_renderpass_params_copy(pass, params),
+ .priv = talloc_steal(pass, priv),
+ };
+
+ // fall through
+error:
+ talloc_free(tmp);
+ return pass;
+}
+
+static void renderpass_destroy_pl(struct ra *ra, struct ra_renderpass *pass)
+{
+ if (!pass)
+ return;
+
+ struct pass_priv *priv = pass->priv;
+ pl_pass_destroy(get_gpu(ra), (pl_pass *) &priv->pass);
+ talloc_free(pass);
+}
+
+static void renderpass_run_pl(struct ra *ra,
+ const struct ra_renderpass_run_params *params)
+{
+ struct pass_priv *p = params->pass->priv;
+ p->num_varups = 0;
+
+ for (int i = 0; i < params->num_values; i++) {
+ const struct ra_renderpass_input_val *val = &params->values[i];
+ const struct ra_renderpass_input *inp = &params->pass->params.inputs[i];
+ if (var_type[inp->type]) {
+ MP_TARRAY_APPEND(p, p->varups, p->num_varups, (struct pl_var_update) {
+ .index = p->inp_index[val->index],
+ .data = val->data,
+ });
+ } else {
+ struct pl_desc_binding bind;
+ switch (inp->type) {
+ case RA_VARTYPE_TEX:
+ case RA_VARTYPE_IMG_W: {
+ struct ra_tex *tex = *((struct ra_tex **) val->data);
+ bind.object = tex->priv;
+ bind.sample_mode = tex->params.src_linear ? PL_TEX_SAMPLE_LINEAR
+ : PL_TEX_SAMPLE_NEAREST;
+ bind.address_mode = tex->params.src_repeat ? PL_TEX_ADDRESS_REPEAT
+ : PL_TEX_ADDRESS_CLAMP;
+ break;
+ }
+ case RA_VARTYPE_BUF_RO:
+ case RA_VARTYPE_BUF_RW:
+ bind.object = (* (struct ra_buf **) val->data)->priv;
+ break;
+ default: MP_ASSERT_UNREACHABLE();
+ };
+
+ p->binds[p->inp_index[val->index]] = bind;
+ };
+ }
+
+ struct pl_pass_run_params pl_params = {
+ .pass = p->pass,
+ .var_updates = p->varups,
+ .num_var_updates = p->num_varups,
+ .desc_bindings = p->binds,
+ .push_constants = params->push_constants,
+ .timer = get_active_timer(ra),
+ };
+
+ if (p->pass->params.type == PL_PASS_RASTER) {
+ pl_params.target = params->target->priv;
+ pl_params.viewport = mp_rect2d_to_pl(params->viewport);
+ pl_params.scissors = mp_rect2d_to_pl(params->scissors);
+ pl_params.vertex_data = params->vertex_data;
+ pl_params.vertex_count = params->vertex_count;
+ } else {
+ for (int i = 0; i < MP_ARRAY_SIZE(pl_params.compute_groups); i++)
+ pl_params.compute_groups[i] = params->compute_groups[i];
+ }
+
+ pl_pass_run(get_gpu(ra), &pl_params);
+}
+
+struct ra_timer_pl {
+ // Because libpplacebo only supports one operation per timer, we need
+ // to use multiple pl_timers to sum up multiple passes/transfers
+ pl_timer *timers;
+ int num_timers;
+ int idx_timers;
+};
+
+static ra_timer *timer_create_pl(struct ra *ra)
+{
+ struct ra_timer_pl *t = talloc_zero(ra, struct ra_timer_pl);
+ return t;
+}
+
+static void timer_destroy_pl(struct ra *ra, ra_timer *timer)
+{
+ pl_gpu gpu = get_gpu(ra);
+ struct ra_timer_pl *t = timer;
+
+ for (int i = 0; i < t->num_timers; i++)
+ pl_timer_destroy(gpu, &t->timers[i]);
+
+ talloc_free(t);
+}
+
+static void timer_start_pl(struct ra *ra, ra_timer *timer)
+{
+ struct ra_pl *p = ra->priv;
+ struct ra_timer_pl *t = timer;
+
+ // There's nothing easy we can do in this case, since libplacebo only
+ // supports one timer object per operation; so just ignore "inner" timers
+ // when the user is nesting different timer queries
+ if (p->active_timer)
+ return;
+
+ p->active_timer = t;
+ t->idx_timers = 0;
+}
+
+static uint64_t timer_stop_pl(struct ra *ra, ra_timer *timer)
+{
+ struct ra_pl *p = ra->priv;
+ struct ra_timer_pl *t = timer;
+
+ if (p->active_timer != t)
+ return 0;
+
+ p->active_timer = NULL;
+
+ // Sum up all of the active results
+ uint64_t res = 0;
+ for (int i = 0; i < t->idx_timers; i++)
+ res += pl_timer_query(p->gpu, t->timers[i]);
+
+ return res;
+}
+
+static pl_timer get_active_timer(const struct ra *ra)
+{
+ struct ra_pl *p = ra->priv;
+ if (!p->active_timer)
+ return NULL;
+
+ struct ra_timer_pl *t = p->active_timer;
+ if (t->idx_timers == t->num_timers)
+ MP_TARRAY_APPEND(t, t->timers, t->num_timers, pl_timer_create(p->gpu));
+
+ return t->timers[t->idx_timers++];
+}
+
+static struct ra_fns ra_fns_pl = {
+ .destroy = destroy_ra_pl,
+ .tex_create = tex_create_pl,
+ .tex_destroy = tex_destroy_pl,
+ .tex_upload = tex_upload_pl,
+ .tex_download = tex_download_pl,
+ .buf_create = buf_create_pl,
+ .buf_destroy = buf_destroy_pl,
+ .buf_update = buf_update_pl,
+ .buf_poll = buf_poll_pl,
+ .clear = clear_pl,
+ .blit = blit_pl,
+ .uniform_layout = uniform_layout_pl,
+ .push_constant_layout = push_constant_layout_pl,
+ .desc_namespace = desc_namespace_pl,
+ .renderpass_create = renderpass_create_pl,
+ .renderpass_destroy = renderpass_destroy_pl,
+ .renderpass_run = renderpass_run_pl,
+ .timer_create = timer_create_pl,
+ .timer_destroy = timer_destroy_pl,
+ .timer_start = timer_start_pl,
+ .timer_stop = timer_stop_pl,
+};
+
diff --git a/video/out/placebo/ra_pl.h b/video/out/placebo/ra_pl.h
new file mode 100644
index 0000000..1290c9c
--- /dev/null
+++ b/video/out/placebo/ra_pl.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "video/out/gpu/ra.h"
+#include <libplacebo/gpu.h>
+
+struct ra *ra_create_pl(pl_gpu gpu, struct mp_log *log);
+
+pl_gpu ra_pl_get(const struct ra *ra);
+
+static inline pl_fmt ra_pl_fmt_get(const struct ra_format *format)
+{
+ return format->priv;
+}
+
+// Wrap a pl_tex into a ra_tex struct, returns if successful
+bool mppl_wrap_tex(struct ra *ra, pl_tex pltex, struct ra_tex *out_tex);
diff --git a/video/out/placebo/utils.c b/video/out/placebo/utils.c
new file mode 100644
index 0000000..1209b72
--- /dev/null
+++ b/video/out/placebo/utils.c
@@ -0,0 +1,263 @@
+#include "common/common.h"
+#include "utils.h"
+
+#include <libplacebo/utils/dolbyvision.h>
+
+static const int pl_log_to_msg_lev[PL_LOG_ALL+1] = {
+ [PL_LOG_FATAL] = MSGL_FATAL,
+ [PL_LOG_ERR] = MSGL_ERR,
+ [PL_LOG_WARN] = MSGL_WARN,
+ [PL_LOG_INFO] = MSGL_V,
+ [PL_LOG_DEBUG] = MSGL_DEBUG,
+ [PL_LOG_TRACE] = MSGL_TRACE,
+};
+
+static const enum pl_log_level msg_lev_to_pl_log[MSGL_MAX+1] = {
+ [MSGL_FATAL] = PL_LOG_FATAL,
+ [MSGL_ERR] = PL_LOG_ERR,
+ [MSGL_WARN] = PL_LOG_WARN,
+ [MSGL_INFO] = PL_LOG_WARN,
+ [MSGL_STATUS] = PL_LOG_WARN,
+ [MSGL_V] = PL_LOG_INFO,
+ [MSGL_DEBUG] = PL_LOG_DEBUG,
+ [MSGL_TRACE] = PL_LOG_TRACE,
+ [MSGL_MAX] = PL_LOG_ALL,
+};
+
+// translates log levels while probing
+static const enum pl_log_level probing_map(enum pl_log_level level)
+{
+ switch (level) {
+ case PL_LOG_FATAL:
+ case PL_LOG_ERR:
+ case PL_LOG_WARN:
+ return PL_LOG_INFO;
+
+ default:
+ return level;
+ }
+}
+
+static void log_cb(void *priv, enum pl_log_level level, const char *msg)
+{
+ struct mp_log *log = priv;
+ mp_msg(log, pl_log_to_msg_lev[level], "%s\n", msg);
+}
+
+static void log_cb_probing(void *priv, enum pl_log_level level, const char *msg)
+{
+ struct mp_log *log = priv;
+ mp_msg(log, pl_log_to_msg_lev[probing_map(level)], "%s\n", msg);
+}
+
+pl_log mppl_log_create(void *tactx, struct mp_log *log)
+{
+ return pl_log_create(PL_API_VER, &(struct pl_log_params) {
+ .log_cb = log_cb,
+ .log_level = msg_lev_to_pl_log[mp_msg_level(log)],
+ .log_priv = mp_log_new(tactx, log, "libplacebo"),
+ });
+}
+
+void mppl_log_set_probing(pl_log log, bool probing)
+{
+ struct pl_log_params params = log->params;
+ params.log_cb = probing ? log_cb_probing : log_cb;
+ pl_log_update(log, &params);
+}
+
+enum pl_color_primaries mp_prim_to_pl(enum mp_csp_prim prim)
+{
+ switch (prim) {
+ case MP_CSP_PRIM_AUTO: return PL_COLOR_PRIM_UNKNOWN;
+ case MP_CSP_PRIM_BT_601_525: return PL_COLOR_PRIM_BT_601_525;
+ case MP_CSP_PRIM_BT_601_625: return PL_COLOR_PRIM_BT_601_625;
+ case MP_CSP_PRIM_BT_709: return PL_COLOR_PRIM_BT_709;
+ case MP_CSP_PRIM_BT_2020: return PL_COLOR_PRIM_BT_2020;
+ case MP_CSP_PRIM_BT_470M: return PL_COLOR_PRIM_BT_470M;
+ case MP_CSP_PRIM_APPLE: return PL_COLOR_PRIM_APPLE;
+ case MP_CSP_PRIM_ADOBE: return PL_COLOR_PRIM_ADOBE;
+ case MP_CSP_PRIM_PRO_PHOTO: return PL_COLOR_PRIM_PRO_PHOTO;
+ case MP_CSP_PRIM_CIE_1931: return PL_COLOR_PRIM_CIE_1931;
+ case MP_CSP_PRIM_DCI_P3: return PL_COLOR_PRIM_DCI_P3;
+ case MP_CSP_PRIM_DISPLAY_P3: return PL_COLOR_PRIM_DISPLAY_P3;
+ case MP_CSP_PRIM_V_GAMUT: return PL_COLOR_PRIM_V_GAMUT;
+ case MP_CSP_PRIM_S_GAMUT: return PL_COLOR_PRIM_S_GAMUT;
+ case MP_CSP_PRIM_EBU_3213: return PL_COLOR_PRIM_EBU_3213;
+ case MP_CSP_PRIM_FILM_C: return PL_COLOR_PRIM_FILM_C;
+ case MP_CSP_PRIM_ACES_AP0: return PL_COLOR_PRIM_ACES_AP0;
+ case MP_CSP_PRIM_ACES_AP1: return PL_COLOR_PRIM_ACES_AP1;
+ case MP_CSP_PRIM_COUNT: return PL_COLOR_PRIM_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum mp_csp_prim mp_prim_from_pl(enum pl_color_primaries prim)
+{
+ switch (prim){
+ case PL_COLOR_PRIM_UNKNOWN: return MP_CSP_PRIM_AUTO;
+ case PL_COLOR_PRIM_BT_601_525: return MP_CSP_PRIM_BT_601_525;
+ case PL_COLOR_PRIM_BT_601_625: return MP_CSP_PRIM_BT_601_625;
+ case PL_COLOR_PRIM_BT_709: return MP_CSP_PRIM_BT_709;
+ case PL_COLOR_PRIM_BT_2020: return MP_CSP_PRIM_BT_2020;
+ case PL_COLOR_PRIM_BT_470M: return MP_CSP_PRIM_BT_470M;
+ case PL_COLOR_PRIM_APPLE: return MP_CSP_PRIM_APPLE;
+ case PL_COLOR_PRIM_ADOBE: return MP_CSP_PRIM_ADOBE;
+ case PL_COLOR_PRIM_PRO_PHOTO: return MP_CSP_PRIM_PRO_PHOTO;
+ case PL_COLOR_PRIM_CIE_1931: return MP_CSP_PRIM_CIE_1931;
+ case PL_COLOR_PRIM_DCI_P3: return MP_CSP_PRIM_DCI_P3;
+ case PL_COLOR_PRIM_DISPLAY_P3: return MP_CSP_PRIM_DISPLAY_P3;
+ case PL_COLOR_PRIM_V_GAMUT: return MP_CSP_PRIM_V_GAMUT;
+ case PL_COLOR_PRIM_S_GAMUT: return MP_CSP_PRIM_S_GAMUT;
+ case PL_COLOR_PRIM_EBU_3213: return MP_CSP_PRIM_EBU_3213;
+ case PL_COLOR_PRIM_FILM_C: return MP_CSP_PRIM_FILM_C;
+ case PL_COLOR_PRIM_ACES_AP0: return MP_CSP_PRIM_ACES_AP0;
+ case PL_COLOR_PRIM_ACES_AP1: return MP_CSP_PRIM_ACES_AP1;
+ case PL_COLOR_PRIM_COUNT: return MP_CSP_PRIM_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum pl_color_transfer mp_trc_to_pl(enum mp_csp_trc trc)
+{
+ switch (trc) {
+ case MP_CSP_TRC_AUTO: return PL_COLOR_TRC_UNKNOWN;
+ case MP_CSP_TRC_BT_1886: return PL_COLOR_TRC_BT_1886;
+ case MP_CSP_TRC_SRGB: return PL_COLOR_TRC_SRGB;
+ case MP_CSP_TRC_LINEAR: return PL_COLOR_TRC_LINEAR;
+ case MP_CSP_TRC_GAMMA18: return PL_COLOR_TRC_GAMMA18;
+ case MP_CSP_TRC_GAMMA20: return PL_COLOR_TRC_GAMMA20;
+ case MP_CSP_TRC_GAMMA22: return PL_COLOR_TRC_GAMMA22;
+ case MP_CSP_TRC_GAMMA24: return PL_COLOR_TRC_GAMMA24;
+ case MP_CSP_TRC_GAMMA26: return PL_COLOR_TRC_GAMMA26;
+ case MP_CSP_TRC_GAMMA28: return PL_COLOR_TRC_GAMMA28;
+ case MP_CSP_TRC_PRO_PHOTO: return PL_COLOR_TRC_PRO_PHOTO;
+ case MP_CSP_TRC_PQ: return PL_COLOR_TRC_PQ;
+ case MP_CSP_TRC_HLG: return PL_COLOR_TRC_HLG;
+ case MP_CSP_TRC_V_LOG: return PL_COLOR_TRC_V_LOG;
+ case MP_CSP_TRC_S_LOG1: return PL_COLOR_TRC_S_LOG1;
+ case MP_CSP_TRC_S_LOG2: return PL_COLOR_TRC_S_LOG2;
+ case MP_CSP_TRC_ST428: return PL_COLOR_TRC_ST428;
+ case MP_CSP_TRC_COUNT: return PL_COLOR_TRC_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum mp_csp_trc mp_trc_from_pl(enum pl_color_transfer trc)
+{
+ switch (trc){
+ case PL_COLOR_TRC_UNKNOWN: return MP_CSP_TRC_AUTO;
+ case PL_COLOR_TRC_BT_1886: return MP_CSP_TRC_BT_1886;
+ case PL_COLOR_TRC_SRGB: return MP_CSP_TRC_SRGB;
+ case PL_COLOR_TRC_LINEAR: return MP_CSP_TRC_LINEAR;
+ case PL_COLOR_TRC_GAMMA18: return MP_CSP_TRC_GAMMA18;
+ case PL_COLOR_TRC_GAMMA20: return MP_CSP_TRC_GAMMA20;
+ case PL_COLOR_TRC_GAMMA22: return MP_CSP_TRC_GAMMA22;
+ case PL_COLOR_TRC_GAMMA24: return MP_CSP_TRC_GAMMA24;
+ case PL_COLOR_TRC_GAMMA26: return MP_CSP_TRC_GAMMA26;
+ case PL_COLOR_TRC_GAMMA28: return MP_CSP_TRC_GAMMA28;
+ case PL_COLOR_TRC_PRO_PHOTO: return MP_CSP_TRC_PRO_PHOTO;
+ case PL_COLOR_TRC_PQ: return MP_CSP_TRC_PQ;
+ case PL_COLOR_TRC_HLG: return MP_CSP_TRC_HLG;
+ case PL_COLOR_TRC_V_LOG: return MP_CSP_TRC_V_LOG;
+ case PL_COLOR_TRC_S_LOG1: return MP_CSP_TRC_S_LOG1;
+ case PL_COLOR_TRC_S_LOG2: return MP_CSP_TRC_S_LOG2;
+ case PL_COLOR_TRC_ST428: return MP_CSP_TRC_ST428;
+ case PL_COLOR_TRC_COUNT: return MP_CSP_TRC_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum pl_color_system mp_csp_to_pl(enum mp_csp csp)
+{
+ switch (csp) {
+ case MP_CSP_AUTO: return PL_COLOR_SYSTEM_UNKNOWN;
+ case MP_CSP_BT_601: return PL_COLOR_SYSTEM_BT_601;
+ case MP_CSP_BT_709: return PL_COLOR_SYSTEM_BT_709;
+ case MP_CSP_SMPTE_240M: return PL_COLOR_SYSTEM_SMPTE_240M;
+ case MP_CSP_BT_2020_NC: return PL_COLOR_SYSTEM_BT_2020_NC;
+ case MP_CSP_BT_2020_C: return PL_COLOR_SYSTEM_BT_2020_C;
+ case MP_CSP_RGB: return PL_COLOR_SYSTEM_RGB;
+ case MP_CSP_XYZ: return PL_COLOR_SYSTEM_XYZ;
+ case MP_CSP_YCGCO: return PL_COLOR_SYSTEM_YCGCO;
+ case MP_CSP_COUNT: return PL_COLOR_SYSTEM_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum pl_color_levels mp_levels_to_pl(enum mp_csp_levels levels)
+{
+ switch (levels) {
+ case MP_CSP_LEVELS_AUTO: return PL_COLOR_LEVELS_UNKNOWN;
+ case MP_CSP_LEVELS_TV: return PL_COLOR_LEVELS_TV;
+ case MP_CSP_LEVELS_PC: return PL_COLOR_LEVELS_PC;
+ case MP_CSP_LEVELS_COUNT: return PL_COLOR_LEVELS_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum mp_csp_levels mp_levels_from_pl(enum pl_color_levels levels)
+{
+ switch (levels){
+ case PL_COLOR_LEVELS_UNKNOWN: return MP_CSP_LEVELS_AUTO;
+ case PL_COLOR_LEVELS_TV: return MP_CSP_LEVELS_TV;
+ case PL_COLOR_LEVELS_PC: return MP_CSP_LEVELS_PC;
+ case PL_COLOR_LEVELS_COUNT: return MP_CSP_LEVELS_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum pl_alpha_mode mp_alpha_to_pl(enum mp_alpha_type alpha)
+{
+ switch (alpha) {
+ case MP_ALPHA_AUTO: return PL_ALPHA_UNKNOWN;
+ case MP_ALPHA_STRAIGHT: return PL_ALPHA_INDEPENDENT;
+ case MP_ALPHA_PREMUL: return PL_ALPHA_PREMULTIPLIED;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+enum pl_chroma_location mp_chroma_to_pl(enum mp_chroma_location chroma)
+{
+ switch (chroma) {
+ case MP_CHROMA_AUTO: return PL_CHROMA_UNKNOWN;
+ case MP_CHROMA_TOPLEFT: return PL_CHROMA_TOP_LEFT;
+ case MP_CHROMA_LEFT: return PL_CHROMA_LEFT;
+ case MP_CHROMA_CENTER: return PL_CHROMA_CENTER;
+ case MP_CHROMA_COUNT: return PL_CHROMA_COUNT;
+ }
+
+ MP_ASSERT_UNREACHABLE();
+}
+
+void mp_map_dovi_metadata_to_pl(struct mp_image *mpi,
+ struct pl_frame *frame)
+{
+#ifdef PL_HAVE_LAV_DOLBY_VISION
+ if (mpi->dovi) {
+ const AVDOVIMetadata *metadata = (AVDOVIMetadata *) mpi->dovi->data;
+ const AVDOVIRpuDataHeader *header = av_dovi_get_header(metadata);
+
+ if (header->disable_residual_flag) {
+ // Only automatically map DoVi RPUs that don't require an EL
+ struct pl_dovi_metadata *dovi = talloc_ptrtype(mpi, dovi);
+ pl_frame_map_avdovi_metadata(frame, dovi, metadata);
+ }
+ }
+
+#if defined(PL_HAVE_LIBDOVI)
+ if (mpi->dovi_buf)
+ pl_hdr_metadata_from_dovi_rpu(&frame->color.hdr, mpi->dovi_buf->data,
+ mpi->dovi_buf->size);
+#endif
+
+#endif // PL_HAVE_LAV_DOLBY_VISION
+}
diff --git a/video/out/placebo/utils.h b/video/out/placebo/utils.h
new file mode 100644
index 0000000..bf780a8
--- /dev/null
+++ b/video/out/placebo/utils.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "config.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "video/csputils.h"
+#include "video/mp_image.h"
+
+#include <libavutil/buffer.h>
+
+#include <libplacebo/common.h>
+#include <libplacebo/log.h>
+#include <libplacebo/colorspace.h>
+#include <libplacebo/renderer.h>
+#include <libplacebo/utils/libav.h>
+
+pl_log mppl_log_create(void *tactx, struct mp_log *log);
+void mppl_log_set_probing(pl_log log, bool probing);
+
+static inline struct pl_rect2d mp_rect2d_to_pl(struct mp_rect rc)
+{
+ return (struct pl_rect2d) {
+ .x0 = rc.x0,
+ .y0 = rc.y0,
+ .x1 = rc.x1,
+ .y1 = rc.y1,
+ };
+}
+
+enum pl_color_primaries mp_prim_to_pl(enum mp_csp_prim prim);
+enum mp_csp_prim mp_prim_from_pl(enum pl_color_primaries prim);
+enum pl_color_transfer mp_trc_to_pl(enum mp_csp_trc trc);
+enum mp_csp_trc mp_trc_from_pl(enum pl_color_transfer trc);
+enum pl_color_system mp_csp_to_pl(enum mp_csp csp);
+enum pl_color_levels mp_levels_to_pl(enum mp_csp_levels levels);
+enum mp_csp_levels mp_levels_from_pl(enum pl_color_levels levels);
+enum pl_alpha_mode mp_alpha_to_pl(enum mp_alpha_type alpha);
+enum pl_chroma_location mp_chroma_to_pl(enum mp_chroma_location chroma);
+
+void mp_map_dovi_metadata_to_pl(struct mp_image *mpi,
+ struct pl_frame *frame);
diff --git a/video/out/present_sync.c b/video/out/present_sync.c
new file mode 100644
index 0000000..a3b1089
--- /dev/null
+++ b/video/out/present_sync.c
@@ -0,0 +1,126 @@
+/*
+ * 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 <time.h>
+
+#include "misc/linked_list.h"
+#include "mpv_talloc.h"
+#include "osdep/timer.h"
+#include "present_sync.h"
+
+/* General nonsense about this mechanism.
+ *
+ * This requires that that caller has access to two, related values:
+ * (ust, msc): clock time and incrementing counter of last vsync (this is
+ * increased continuously, even if we don't swap)
+ *
+ * Note that this concept originates from the GLX_OML_sync_control extension
+ * which includes another parameter: sbc (swap counter of frame that was
+ * last displayed). Both the xorg present extension and wayland's
+ * presentation-time protocol do not include sbc values so they are omitted
+ * from this mechanism. mpv does not need to keep track of sbc calls and can
+ * have reliable presentation without it.
+ */
+
+void present_sync_get_info(struct mp_present *present, struct vo_vsync_info *info)
+{
+ struct mp_present_entry *cur = present->head;
+ while (cur) {
+ if (cur->queue_display_time)
+ break;
+ cur = cur->list_node.next;
+ }
+ if (!cur)
+ return;
+
+ info->vsync_duration = cur->vsync_duration;
+ info->skipped_vsyncs = cur->skipped_vsyncs;
+ info->last_queue_display_time = cur->queue_display_time;
+
+ // Remove from the list, zero out everything, and append at the end
+ LL_REMOVE(list_node, present, cur);
+ *cur = (struct mp_present_entry){0};
+ LL_APPEND(list_node, present, cur);
+}
+
+struct mp_present *mp_present_initialize(void *talloc_ctx, struct mp_vo_opts *opts, int entries)
+{
+ struct mp_present *present = talloc_zero(talloc_ctx, struct mp_present);
+ for (int i = 0; i < entries; i++) {
+ struct mp_present_entry *entry = talloc_zero(present, struct mp_present_entry);
+ LL_APPEND(list_node, present, entry);
+ }
+ present->opts = opts;
+ return present;
+}
+
+void present_sync_swap(struct mp_present *present)
+{
+ struct mp_present_entry *cur = present->head;
+ while (cur) {
+ if (!cur->queue_display_time)
+ break;
+ cur = cur->list_node.next;
+ }
+ if (!cur)
+ return;
+
+ int64_t ust = cur->ust;
+ int64_t msc = cur->msc;
+ int64_t last_ust = cur->list_node.prev ? cur->list_node.prev->ust : 0;
+ int64_t last_msc = cur->list_node.prev ? cur->list_node.prev->msc : 0;
+
+ // Avoid attempting to use any presentation statistics if the ust is 0 or has
+ // not actually updated (i.e. the last_ust is equal to ust).
+ if (!ust || ust == last_ust) {
+ cur->skipped_vsyncs = -1;
+ cur->vsync_duration = -1;
+ cur->queue_display_time = -1;
+ return;
+ }
+
+ cur->skipped_vsyncs = 0;
+ int64_t ust_passed = ust ? ust - last_ust: 0;
+ int64_t msc_passed = msc ? msc - last_msc: 0;
+ if (msc_passed && ust_passed)
+ cur->vsync_duration = ust_passed / msc_passed;
+
+ struct timespec ts;
+ if (clock_gettime(CLOCK_MONOTONIC, &ts))
+ return;
+
+ int64_t now_monotonic = MP_TIME_S_TO_NS(ts.tv_sec) + ts.tv_nsec;
+ int64_t ust_mp_time = mp_time_ns() - (now_monotonic - ust);
+ cur->queue_display_time = ust_mp_time;
+}
+
+void present_sync_update_values(struct mp_present *present, int64_t ust,
+ int64_t msc)
+{
+ struct mp_present_entry *cur = present->head;
+ int index = 0;
+ while (cur && ++index) {
+ if (!cur->ust || index == present->opts->swapchain_depth)
+ break;
+ cur = cur->list_node.next;
+ }
+ if (!cur)
+ return;
+
+ cur->ust = ust;
+ cur->msc = msc;
+}
diff --git a/video/out/present_sync.h b/video/out/present_sync.h
new file mode 100644
index 0000000..ba6d0b3
--- /dev/null
+++ b/video/out/present_sync.h
@@ -0,0 +1,57 @@
+/*
+ * This file is part of mpv video player.
+ *
+ * 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/>.
+ */
+
+#ifndef MP_PRESENT_SYNC_H
+#define MP_PRESENT_SYNC_H
+
+#include <stdbool.h>
+#include <stdint.h>
+#include "vo.h"
+
+/* Generic helpers for obtaining presentation feedback from
+ * backend APIs. This requires ust/msc values. */
+
+struct mp_present_entry {
+ int64_t ust;
+ int64_t msc;
+ int64_t vsync_duration;
+ int64_t skipped_vsyncs;
+ int64_t queue_display_time;
+
+ struct {
+ struct mp_present_entry *next, *prev;
+ } list_node;
+};
+
+struct mp_present {
+ struct mp_present_entry *head, *tail;
+ struct mp_vo_opts *opts;
+};
+
+struct mp_present *mp_present_initialize(void *talloc_ctx, struct mp_vo_opts *opts, int entries);
+
+// Used during the get_vsync call to deliver the presentation statistics to the VO.
+void present_sync_get_info(struct mp_present *present, struct vo_vsync_info *info);
+
+// Called after every buffer swap to update presentation statistics.
+void present_sync_swap(struct mp_present *present);
+
+// Called anytime the backend delivers new ust/msc values.
+void present_sync_update_values(struct mp_present *present, int64_t ust,
+ int64_t msc);
+
+#endif /* MP_PRESENT_SYNC_H */
diff --git a/video/out/vo.c b/video/out/vo.c
new file mode 100644
index 0000000..50129fb
--- /dev/null
+++ b/video/out/vo.c
@@ -0,0 +1,1441 @@
+/*
+ * 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 <assert.h>
+#include <math.h>
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "mpv_talloc.h"
+
+#include "config.h"
+#include "osdep/timer.h"
+#include "osdep/threads.h"
+#include "misc/dispatch.h"
+#include "misc/rendezvous.h"
+#include "options/options.h"
+#include "misc/bstr.h"
+#include "vo.h"
+#include "aspect.h"
+#include "dr_helper.h"
+#include "input/input.h"
+#include "options/m_config.h"
+#include "common/msg.h"
+#include "common/global.h"
+#include "common/stats.h"
+#include "video/hwdec.h"
+#include "video/mp_image.h"
+#include "sub/osd.h"
+#include "osdep/io.h"
+#include "osdep/threads.h"
+
+extern const struct vo_driver video_out_mediacodec_embed;
+extern const struct vo_driver video_out_x11;
+extern const struct vo_driver video_out_vdpau;
+extern const struct vo_driver video_out_xv;
+extern const struct vo_driver video_out_gpu;
+extern const struct vo_driver video_out_gpu_next;
+extern const struct vo_driver video_out_libmpv;
+extern const struct vo_driver video_out_null;
+extern const struct vo_driver video_out_image;
+extern const struct vo_driver video_out_lavc;
+extern const struct vo_driver video_out_caca;
+extern const struct vo_driver video_out_drm;
+extern const struct vo_driver video_out_direct3d;
+extern const struct vo_driver video_out_sdl;
+extern const struct vo_driver video_out_vaapi;
+extern const struct vo_driver video_out_dmabuf_wayland;
+extern const struct vo_driver video_out_wlshm;
+extern const struct vo_driver video_out_rpi;
+extern const struct vo_driver video_out_tct;
+extern const struct vo_driver video_out_sixel;
+extern const struct vo_driver video_out_kitty;
+
+static const struct vo_driver *const video_out_drivers[] =
+{
+ &video_out_libmpv,
+#if HAVE_ANDROID
+ &video_out_mediacodec_embed,
+#endif
+ &video_out_gpu,
+ &video_out_gpu_next,
+#if HAVE_VDPAU
+ &video_out_vdpau,
+#endif
+#if HAVE_DIRECT3D
+ &video_out_direct3d,
+#endif
+#if HAVE_WAYLAND && HAVE_MEMFD_CREATE
+ &video_out_wlshm,
+#endif
+#if HAVE_XV
+ &video_out_xv,
+#endif
+#if HAVE_SDL2_VIDEO
+ &video_out_sdl,
+#endif
+#if HAVE_DMABUF_WAYLAND
+ &video_out_dmabuf_wayland,
+#endif
+#if HAVE_VAAPI_X11 && HAVE_GPL
+ &video_out_vaapi,
+#endif
+#if HAVE_X11
+ &video_out_x11,
+#endif
+ &video_out_null,
+ // should not be auto-selected
+ &video_out_image,
+ &video_out_tct,
+#if HAVE_CACA
+ &video_out_caca,
+#endif
+#if HAVE_DRM
+ &video_out_drm,
+#endif
+#if HAVE_RPI_MMAL
+ &video_out_rpi,
+#endif
+#if HAVE_SIXEL
+ &video_out_sixel,
+#endif
+ &video_out_kitty,
+ &video_out_lavc,
+};
+
+struct vo_internal {
+ mp_thread thread;
+ struct mp_dispatch_queue *dispatch;
+ struct dr_helper *dr_helper;
+
+ // --- The following fields are protected by lock
+ mp_mutex lock;
+ mp_cond wakeup;
+
+ bool need_wakeup;
+ bool terminate;
+
+ bool hasframe;
+ bool hasframe_rendered;
+ bool request_redraw; // redraw request from player to VO
+ bool want_redraw; // redraw request from VO to player
+ bool send_reset; // send VOCTRL_RESET
+ bool paused;
+ int queued_events; // event mask for the user
+ int internal_events; // event mask for us
+
+ double nominal_vsync_interval;
+
+ double vsync_interval;
+ int64_t *vsync_samples;
+ int num_vsync_samples;
+ int64_t num_total_vsync_samples;
+ int64_t prev_vsync;
+ double base_vsync;
+ int drop_point;
+ double estimated_vsync_interval;
+ double estimated_vsync_jitter;
+ bool expecting_vsync;
+ int64_t num_successive_vsyncs;
+
+ int64_t flip_queue_offset; // queue flip events at most this much in advance
+ int64_t timing_offset; // same (but from options; not VO configured)
+
+ int64_t delayed_count;
+ int64_t drop_count;
+ bool dropped_frame; // the previous frame was dropped
+
+ struct vo_frame *current_frame; // last frame queued to the VO
+
+ int64_t wakeup_pts; // time at which to pull frame from decoder
+
+ bool rendering; // true if an image is being rendered
+ struct vo_frame *frame_queued; // should be drawn next
+ int req_frames; // VO's requested value of num_frames
+ uint64_t current_frame_id;
+
+ double display_fps;
+ double reported_display_fps;
+
+ struct stats_ctx *stats;
+};
+
+extern const struct m_sub_options gl_video_conf;
+
+static void forget_frames(struct vo *vo);
+static MP_THREAD_VOID vo_thread(void *ptr);
+
+static bool get_desc(struct m_obj_desc *dst, int index)
+{
+ if (index >= MP_ARRAY_SIZE(video_out_drivers))
+ return false;
+ const struct vo_driver *vo = video_out_drivers[index];
+ *dst = (struct m_obj_desc) {
+ .name = vo->name,
+ .description = vo->description,
+ .priv_size = vo->priv_size,
+ .priv_defaults = vo->priv_defaults,
+ .options = vo->options,
+ .options_prefix = vo->options_prefix,
+ .global_opts = vo->global_opts,
+ .hidden = vo->encode,
+ .p = vo,
+ };
+ return true;
+}
+
+// For the vo option
+const struct m_obj_list vo_obj_list = {
+ .get_desc = get_desc,
+ .description = "video outputs",
+ .aliases = {
+ {"gl", "gpu"},
+ {"direct3d_shaders", "direct3d"},
+ {"opengl", "gpu"},
+ {"opengl-cb", "libmpv"},
+ {0}
+ },
+ .allow_trailer = true,
+ .disallow_positional_parameters = true,
+ .use_global_options = true,
+};
+
+static void dispatch_wakeup_cb(void *ptr)
+{
+ struct vo *vo = ptr;
+ vo_wakeup(vo);
+}
+
+// Initialize or update options from vo->opts
+static void read_opts(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+
+ mp_mutex_lock(&in->lock);
+ in->timing_offset = (uint64_t)(MP_TIME_S_TO_NS(vo->opts->timing_offset));
+ mp_mutex_unlock(&in->lock);
+}
+
+static void update_opts(void *p)
+{
+ struct vo *vo = p;
+
+ if (m_config_cache_update(vo->opts_cache)) {
+ read_opts(vo);
+
+ if (vo->driver->control) {
+ vo->driver->control(vo, VOCTRL_VO_OPTS_CHANGED, NULL);
+ // "Legacy" update of video position related options.
+ // Unlike VOCTRL_VO_OPTS_CHANGED, often not propagated to backends.
+ vo->driver->control(vo, VOCTRL_SET_PANSCAN, NULL);
+ }
+ }
+
+ if (vo->gl_opts_cache && m_config_cache_update(vo->gl_opts_cache)) {
+ // "Legacy" update of video GL renderer related options.
+ if (vo->driver->control)
+ vo->driver->control(vo, VOCTRL_UPDATE_RENDER_OPTS, NULL);
+ }
+
+ if (m_config_cache_update(vo->eq_opts_cache)) {
+ // "Legacy" update of video equalizer related options.
+ if (vo->driver->control)
+ vo->driver->control(vo, VOCTRL_SET_EQUALIZER, NULL);
+ }
+}
+
+// Does not include thread- and VO uninit.
+static void dealloc_vo(struct vo *vo)
+{
+ forget_frames(vo); // implicitly synchronized
+
+ // These must be free'd before vo->in->dispatch.
+ talloc_free(vo->opts_cache);
+ talloc_free(vo->gl_opts_cache);
+ talloc_free(vo->eq_opts_cache);
+
+ mp_mutex_destroy(&vo->in->lock);
+ mp_cond_destroy(&vo->in->wakeup);
+ talloc_free(vo);
+}
+
+static struct vo *vo_create(bool probing, struct mpv_global *global,
+ struct vo_extra *ex, char *name)
+{
+ assert(ex->wakeup_cb);
+
+ struct mp_log *log = mp_log_new(NULL, global->log, "vo");
+ struct m_obj_desc desc;
+ if (!m_obj_list_find(&desc, &vo_obj_list, bstr0(name))) {
+ mp_msg(log, MSGL_ERR, "Video output %s not found!\n", name);
+ talloc_free(log);
+ return NULL;
+ };
+ struct vo *vo = talloc_ptrtype(NULL, vo);
+ *vo = (struct vo) {
+ .log = mp_log_new(vo, log, name),
+ .driver = desc.p,
+ .global = global,
+ .encode_lavc_ctx = ex->encode_lavc_ctx,
+ .input_ctx = ex->input_ctx,
+ .osd = ex->osd,
+ .monitor_par = 1,
+ .extra = *ex,
+ .probing = probing,
+ .in = talloc(vo, struct vo_internal),
+ };
+ talloc_steal(vo, log);
+ *vo->in = (struct vo_internal) {
+ .dispatch = mp_dispatch_create(vo),
+ .req_frames = 1,
+ .estimated_vsync_jitter = -1,
+ .stats = stats_ctx_create(vo, global, "vo"),
+ };
+ mp_dispatch_set_wakeup_fn(vo->in->dispatch, dispatch_wakeup_cb, vo);
+ mp_mutex_init(&vo->in->lock);
+ mp_cond_init(&vo->in->wakeup);
+
+ vo->opts_cache = m_config_cache_alloc(NULL, global, &vo_sub_opts);
+ vo->opts = vo->opts_cache->opts;
+
+ m_config_cache_set_dispatch_change_cb(vo->opts_cache, vo->in->dispatch,
+ update_opts, vo);
+
+ vo->gl_opts_cache = m_config_cache_alloc(NULL, global, &gl_video_conf);
+ m_config_cache_set_dispatch_change_cb(vo->gl_opts_cache, vo->in->dispatch,
+ update_opts, vo);
+
+ vo->eq_opts_cache = m_config_cache_alloc(NULL, global, &mp_csp_equalizer_conf);
+ m_config_cache_set_dispatch_change_cb(vo->eq_opts_cache, vo->in->dispatch,
+ update_opts, vo);
+
+ mp_input_set_mouse_transform(vo->input_ctx, NULL, NULL);
+ if (vo->driver->encode != !!vo->encode_lavc_ctx)
+ goto error;
+ vo->priv = m_config_group_from_desc(vo, vo->log, global, &desc, name);
+ if (!vo->priv)
+ goto error;
+
+ if (mp_thread_create(&vo->in->thread, vo_thread, vo))
+ goto error;
+ if (mp_rendezvous(vo, 0) < 0) { // init barrier
+ mp_thread_join(vo->in->thread);
+ goto error;
+ }
+ return vo;
+
+error:
+ dealloc_vo(vo);
+ return NULL;
+}
+
+struct vo *init_best_video_out(struct mpv_global *global, struct vo_extra *ex)
+{
+ struct mp_vo_opts *opts = mp_get_config_group(NULL, global, &vo_sub_opts);
+ struct m_obj_settings *vo_list = opts->video_driver_list;
+ struct vo *vo = NULL;
+ // first try the preferred drivers, with their optional subdevice param:
+ if (vo_list && vo_list[0].name) {
+ for (int n = 0; vo_list[n].name; n++) {
+ // Something like "-vo name," allows fallback to autoprobing.
+ if (strlen(vo_list[n].name) == 0)
+ goto autoprobe;
+ bool p = !!vo_list[n + 1].name;
+ vo = vo_create(p, global, ex, vo_list[n].name);
+ if (vo)
+ goto done;
+ }
+ goto done;
+ }
+autoprobe:
+ // now try the rest...
+ for (int i = 0; i < MP_ARRAY_SIZE(video_out_drivers); i++) {
+ const struct vo_driver *driver = video_out_drivers[i];
+ if (driver == &video_out_null)
+ break;
+ vo = vo_create(true, global, ex, (char *)driver->name);
+ if (vo)
+ goto done;
+ }
+done:
+ talloc_free(opts);
+ return vo;
+}
+
+static void terminate_vo(void *p)
+{
+ struct vo *vo = p;
+ struct vo_internal *in = vo->in;
+ in->terminate = true;
+}
+
+void vo_destroy(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_dispatch_run(in->dispatch, terminate_vo, vo);
+ mp_thread_join(vo->in->thread);
+ dealloc_vo(vo);
+}
+
+// Wakeup the playloop to queue new video frames etc.
+static void wakeup_core(struct vo *vo)
+{
+ vo->extra.wakeup_cb(vo->extra.wakeup_ctx);
+}
+
+// Drop timing information on discontinuities like seeking.
+// Always called locked.
+static void reset_vsync_timings(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ in->drop_point = 0;
+ in->base_vsync = 0;
+ in->expecting_vsync = false;
+ in->num_successive_vsyncs = 0;
+}
+
+static double vsync_stddef(struct vo *vo, double ref_vsync)
+{
+ struct vo_internal *in = vo->in;
+ double jitter = 0;
+ for (int n = 0; n < in->num_vsync_samples; n++) {
+ double diff = in->vsync_samples[n] - ref_vsync;
+ jitter += diff * diff;
+ }
+ return sqrt(jitter / in->num_vsync_samples);
+}
+
+#define MAX_VSYNC_SAMPLES 1000
+#define DELAY_VSYNC_SAMPLES 10
+
+// Check if we should switch to measured average display FPS if it seems
+// "better" then the system-reported one. (Note that small differences are
+// handled as drift instead.)
+static void check_estimated_display_fps(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+
+ bool use_estimated = false;
+ if (in->num_total_vsync_samples >= MAX_VSYNC_SAMPLES / 2 &&
+ in->estimated_vsync_interval <= 1e9 / 20.0 &&
+ in->estimated_vsync_interval >= 1e9 / 400.0)
+ {
+ for (int n = 0; n < in->num_vsync_samples; n++) {
+ if (fabs(in->vsync_samples[n] - in->estimated_vsync_interval)
+ >= in->estimated_vsync_interval / 4)
+ goto done;
+ }
+ double mjitter = vsync_stddef(vo, in->estimated_vsync_interval);
+ double njitter = vsync_stddef(vo, in->nominal_vsync_interval);
+ if (mjitter * 1.01 < njitter)
+ use_estimated = true;
+ done: ;
+ }
+ if (use_estimated == (fabs(in->vsync_interval - in->nominal_vsync_interval) < 1e9)) {
+ if (use_estimated) {
+ MP_TRACE(vo, "adjusting display FPS to a value closer to %.3f Hz\n",
+ 1e9 / in->estimated_vsync_interval);
+ } else {
+ MP_TRACE(vo, "switching back to assuming display fps = %.3f Hz\n",
+ 1e9 / in->nominal_vsync_interval);
+ }
+ }
+ in->vsync_interval = use_estimated ? in->estimated_vsync_interval
+ : in->nominal_vsync_interval;
+}
+
+// Attempt to detect vsyncs delayed/skipped by the driver. This tries to deal
+// with strong jitter too, because some drivers have crap vsync timing.
+static void vsync_skip_detection(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+
+ int window = 4;
+ double t_r = in->prev_vsync, t_e = in->base_vsync, diff = 0.0, desync_early = 0.0;
+ for (int n = 0; n < in->drop_point; n++) {
+ diff += t_r - t_e;
+ t_r -= in->vsync_samples[n];
+ t_e -= in->vsync_interval;
+ if (n == window + 1)
+ desync_early = diff / window;
+ }
+ double desync = diff / in->num_vsync_samples;
+ if (in->drop_point > window * 2 &&
+ fabs(desync - desync_early) >= in->vsync_interval * 3 / 4)
+ {
+ // Assume a drop. An underflow can technically speaking not be a drop
+ // (it's up to the driver what this is supposed to mean), but no reason
+ // to treat it differently.
+ in->base_vsync = in->prev_vsync;
+ in->delayed_count += 1;
+ in->drop_point = 0;
+ MP_STATS(vo, "vo-delayed");
+ }
+ if (in->drop_point > 10)
+ in->base_vsync += desync / 10; // smooth out drift
+}
+
+// Always called locked.
+static void update_vsync_timing_after_swap(struct vo *vo,
+ struct vo_vsync_info *vsync)
+{
+ struct vo_internal *in = vo->in;
+
+ int64_t vsync_time = vsync->last_queue_display_time;
+ int64_t prev_vsync = in->prev_vsync;
+ in->prev_vsync = vsync_time;
+
+ if (!in->expecting_vsync) {
+ reset_vsync_timings(vo);
+ return;
+ }
+
+ in->num_successive_vsyncs++;
+ if (in->num_successive_vsyncs <= DELAY_VSYNC_SAMPLES)
+ return;
+
+ if (vsync_time <= 0 || vsync_time <= prev_vsync) {
+ in->prev_vsync = 0;
+ return;
+ }
+
+ if (prev_vsync <= 0)
+ return;
+
+ if (in->num_vsync_samples >= MAX_VSYNC_SAMPLES)
+ in->num_vsync_samples -= 1;
+ MP_TARRAY_INSERT_AT(in, in->vsync_samples, in->num_vsync_samples, 0,
+ vsync_time - prev_vsync);
+ in->drop_point = MPMIN(in->drop_point + 1, in->num_vsync_samples);
+ in->num_total_vsync_samples += 1;
+ if (in->base_vsync) {
+ in->base_vsync += in->vsync_interval;
+ } else {
+ in->base_vsync = vsync_time;
+ }
+
+ double avg = 0;
+ for (int n = 0; n < in->num_vsync_samples; n++) {
+ assert(in->vsync_samples[n] > 0);
+ avg += in->vsync_samples[n];
+ }
+ in->estimated_vsync_interval = avg / in->num_vsync_samples;
+ in->estimated_vsync_jitter =
+ vsync_stddef(vo, in->vsync_interval) / in->vsync_interval;
+
+ check_estimated_display_fps(vo);
+ vsync_skip_detection(vo);
+
+ MP_STATS(vo, "value %f jitter", in->estimated_vsync_jitter);
+ MP_STATS(vo, "value %f vsync-diff", MP_TIME_NS_TO_S(in->vsync_samples[0]));
+}
+
+// to be called from VO thread only
+static void update_display_fps(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ if (in->internal_events & VO_EVENT_WIN_STATE) {
+ in->internal_events &= ~(unsigned)VO_EVENT_WIN_STATE;
+
+ mp_mutex_unlock(&in->lock);
+
+ double fps = 0;
+ vo->driver->control(vo, VOCTRL_GET_DISPLAY_FPS, &fps);
+
+ mp_mutex_lock(&in->lock);
+
+ in->reported_display_fps = fps;
+ }
+
+ double display_fps = vo->opts->display_fps_override;
+ if (display_fps <= 0)
+ display_fps = in->reported_display_fps;
+
+ if (in->display_fps != display_fps) {
+ in->nominal_vsync_interval = display_fps > 0 ? 1e9 / display_fps : 0;
+ in->vsync_interval = MPMAX(in->nominal_vsync_interval, 1);
+ in->display_fps = display_fps;
+
+ MP_VERBOSE(vo, "Assuming %f FPS for display sync.\n", display_fps);
+
+ // make sure to update the player
+ in->queued_events |= VO_EVENT_WIN_STATE;
+ wakeup_core(vo);
+ }
+
+ mp_mutex_unlock(&in->lock);
+}
+
+static void check_vo_caps(struct vo *vo)
+{
+ int rot = vo->params->rotate;
+ if (rot) {
+ bool ok = rot % 90 ? false : (vo->driver->caps & VO_CAP_ROTATE90);
+ if (!ok) {
+ MP_WARN(vo, "Video is flagged as rotated by %d degrees, but the "
+ "video output does not support this.\n", rot);
+ }
+ }
+}
+
+static void run_reconfig(void *p)
+{
+ void **pp = p;
+ struct vo *vo = pp[0];
+ struct mp_image *img = pp[1];
+ int *ret = pp[2];
+
+ struct mp_image_params *params = &img->params;
+
+ struct vo_internal *in = vo->in;
+
+ MP_VERBOSE(vo, "reconfig to %s\n", mp_image_params_to_str(params));
+
+ update_opts(vo);
+
+ mp_image_params_get_dsize(params, &vo->dwidth, &vo->dheight);
+
+ talloc_free(vo->params);
+ vo->params = talloc_dup(vo, params);
+
+ if (vo->driver->reconfig2) {
+ *ret = vo->driver->reconfig2(vo, img);
+ } else {
+ *ret = vo->driver->reconfig(vo, vo->params);
+ }
+ vo->config_ok = *ret >= 0;
+ if (vo->config_ok) {
+ check_vo_caps(vo);
+ } else {
+ talloc_free(vo->params);
+ vo->params = NULL;
+ }
+
+ mp_mutex_lock(&in->lock);
+ talloc_free(in->current_frame);
+ in->current_frame = NULL;
+ forget_frames(vo);
+ reset_vsync_timings(vo);
+ mp_mutex_unlock(&in->lock);
+
+ update_display_fps(vo);
+}
+
+int vo_reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ int ret;
+ struct mp_image dummy = {0};
+ mp_image_set_params(&dummy, params);
+ void *p[] = {vo, &dummy, &ret};
+ mp_dispatch_run(vo->in->dispatch, run_reconfig, p);
+ return ret;
+}
+
+int vo_reconfig2(struct vo *vo, struct mp_image *img)
+{
+ int ret;
+ void *p[] = {vo, img, &ret};
+ mp_dispatch_run(vo->in->dispatch, run_reconfig, p);
+ return ret;
+}
+
+static void run_control(void *p)
+{
+ void **pp = p;
+ struct vo *vo = pp[0];
+ int request = (intptr_t)pp[1];
+ void *data = pp[2];
+ update_opts(vo);
+ int ret = vo->driver->control(vo, request, data);
+ if (pp[3])
+ *(int *)pp[3] = ret;
+}
+
+int vo_control(struct vo *vo, int request, void *data)
+{
+ int ret;
+ void *p[] = {vo, (void *)(intptr_t)request, data, &ret};
+ mp_dispatch_run(vo->in->dispatch, run_control, p);
+ return ret;
+}
+
+// Run vo_control() without waiting for a reply.
+// (Only works for some VOCTRLs.)
+void vo_control_async(struct vo *vo, int request, void *data)
+{
+ void *p[4] = {vo, (void *)(intptr_t)request, NULL, NULL};
+ void **d = talloc_memdup(NULL, p, sizeof(p));
+
+ switch (request) {
+ case VOCTRL_UPDATE_PLAYBACK_STATE:
+ d[2] = talloc_dup(d, (struct voctrl_playback_state *)data);
+ break;
+ case VOCTRL_KILL_SCREENSAVER:
+ case VOCTRL_RESTORE_SCREENSAVER:
+ break;
+ default:
+ abort(); // requires explicit support
+ }
+
+ mp_dispatch_enqueue_autofree(vo->in->dispatch, run_control, d);
+}
+
+// must be called locked
+static void forget_frames(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ in->hasframe = false;
+ in->hasframe_rendered = false;
+ in->drop_count = 0;
+ in->delayed_count = 0;
+ talloc_free(in->frame_queued);
+ in->frame_queued = NULL;
+ in->current_frame_id += VO_MAX_REQ_FRAMES + 1;
+ // don't unref current_frame; we always want to be able to redraw it
+ if (in->current_frame) {
+ in->current_frame->num_vsyncs = 0; // but reset future repeats
+ in->current_frame->display_synced = false; // mark discontinuity
+ }
+}
+
+// VOs which have no special requirements on UI event loops etc. can set the
+// vo_driver.wait_events callback to this (and leave vo_driver.wakeup unset).
+// This function must not be used or called for other purposes.
+void vo_wait_default(struct vo *vo, int64_t until_time)
+{
+ struct vo_internal *in = vo->in;
+
+ mp_mutex_lock(&in->lock);
+ if (!in->need_wakeup)
+ mp_cond_timedwait_until(&in->wakeup, &in->lock, until_time);
+ mp_mutex_unlock(&in->lock);
+}
+
+// Called unlocked.
+static void wait_vo(struct vo *vo, int64_t until_time)
+{
+ struct vo_internal *in = vo->in;
+
+ if (vo->driver->wait_events) {
+ vo->driver->wait_events(vo, until_time);
+ } else {
+ vo_wait_default(vo, until_time);
+ }
+ mp_mutex_lock(&in->lock);
+ in->need_wakeup = false;
+ mp_mutex_unlock(&in->lock);
+}
+
+static void wakeup_locked(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+
+ mp_cond_broadcast(&in->wakeup);
+ if (vo->driver->wakeup)
+ vo->driver->wakeup(vo);
+ in->need_wakeup = true;
+}
+
+// Wakeup VO thread, and make it check for new events with VOCTRL_CHECK_EVENTS.
+// To be used by threaded VO backends.
+void vo_wakeup(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+
+ mp_mutex_lock(&in->lock);
+ wakeup_locked(vo);
+ mp_mutex_unlock(&in->lock);
+}
+
+// Whether vo_queue_frame() can be called. If the VO is not ready yet, the
+// function will return false, and the VO will call the wakeup callback once
+// it's ready.
+// next_pts is the exact time when the next frame should be displayed. If the
+// VO is ready, but the time is too "early", return false, and call the wakeup
+// callback once the time is right.
+// If next_pts is negative, disable any timing and draw the frame as fast as
+// possible.
+bool vo_is_ready_for_frame(struct vo *vo, int64_t next_pts)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ bool blocked = vo->driver->initially_blocked &&
+ !(in->internal_events & VO_EVENT_INITIAL_UNBLOCK);
+ bool r = vo->config_ok && !in->frame_queued && !blocked &&
+ (!in->current_frame || in->current_frame->num_vsyncs < 1);
+ if (r && next_pts >= 0) {
+ // Don't show the frame too early - it would basically freeze the
+ // display by disallowing OSD redrawing or VO interaction.
+ // Actually render the frame at earliest the given offset before target
+ // time.
+ next_pts -= in->timing_offset;
+ next_pts -= in->flip_queue_offset;
+ int64_t now = mp_time_ns();
+ if (next_pts > now)
+ r = false;
+ if (!in->wakeup_pts || next_pts < in->wakeup_pts) {
+ in->wakeup_pts = next_pts;
+ // If we have to wait, update the vo thread's timer.
+ if (!r)
+ wakeup_locked(vo);
+ }
+ }
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+// Direct the VO thread to put the currently queued image on the screen.
+// vo_is_ready_for_frame() must have returned true before this call.
+// Ownership of frame is handed to the vo.
+void vo_queue_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ assert(vo->config_ok && !in->frame_queued &&
+ (!in->current_frame || in->current_frame->num_vsyncs < 1));
+ in->hasframe = true;
+ frame->frame_id = ++(in->current_frame_id);
+ in->frame_queued = frame;
+ in->wakeup_pts = frame->display_synced
+ ? 0 : frame->pts + MPMAX(frame->duration, 0);
+ wakeup_locked(vo);
+ mp_mutex_unlock(&in->lock);
+}
+
+// If a frame is currently being rendered (or queued), wait until it's done.
+// Otherwise, return immediately.
+void vo_wait_frame(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ while (in->frame_queued || in->rendering)
+ mp_cond_wait(&in->wakeup, &in->lock);
+ mp_mutex_unlock(&in->lock);
+}
+
+// Wait until realtime is >= ts
+// called without lock
+static void wait_until(struct vo *vo, int64_t target)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ while (target > mp_time_ns()) {
+ if (in->queued_events & VO_EVENT_LIVE_RESIZING)
+ break;
+ if (mp_cond_timedwait_until(&in->wakeup, &in->lock, target))
+ break;
+ }
+ mp_mutex_unlock(&in->lock);
+}
+
+static bool render_frame(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ struct vo_frame *frame = NULL;
+ bool more_frames = false;
+
+ update_display_fps(vo);
+
+ mp_mutex_lock(&in->lock);
+
+ if (in->frame_queued) {
+ talloc_free(in->current_frame);
+ in->current_frame = in->frame_queued;
+ in->frame_queued = NULL;
+ } else if (in->paused || !in->current_frame || !in->hasframe ||
+ (in->current_frame->display_synced && in->current_frame->num_vsyncs < 1) ||
+ !in->current_frame->display_synced)
+ {
+ goto done;
+ }
+
+ frame = vo_frame_ref(in->current_frame);
+ assert(frame);
+
+ if (frame->display_synced) {
+ frame->pts = 0;
+ frame->duration = -1;
+ }
+
+ int64_t now = mp_time_ns();
+ int64_t pts = frame->pts;
+ int64_t duration = frame->duration;
+ int64_t end_time = pts + duration;
+
+ // Time at which we should flip_page on the VO.
+ int64_t target = frame->display_synced ? 0 : pts - in->flip_queue_offset;
+
+ // "normal" strict drop threshold.
+ in->dropped_frame = duration >= 0 && end_time < now;
+
+ in->dropped_frame &= !frame->display_synced;
+ in->dropped_frame &= !(vo->driver->caps & VO_CAP_FRAMEDROP);
+ in->dropped_frame &= frame->can_drop;
+ // Even if we're hopelessly behind, rather degrade to 10 FPS playback,
+ // instead of just freezing the display forever.
+ in->dropped_frame &= now - in->prev_vsync < MP_TIME_MS_TO_NS(100);
+ in->dropped_frame &= in->hasframe_rendered;
+
+ // Setup parameters for the next time this frame is drawn. ("frame" is the
+ // frame currently drawn, while in->current_frame is the potentially next.)
+ in->current_frame->repeat = true;
+ if (frame->display_synced) {
+ // Increment the offset only if it's not the last vsync. The current_frame
+ // can still be reused. This is mostly important for redraws that might
+ // overshoot the target vsync point.
+ if (in->current_frame->num_vsyncs > 1) {
+ in->current_frame->vsync_offset += in->current_frame->vsync_interval;
+ in->current_frame->ideal_frame_vsync += in->current_frame->ideal_frame_vsync_duration;
+ }
+ in->dropped_frame |= in->current_frame->num_vsyncs < 1;
+ }
+ if (in->current_frame->num_vsyncs > 0)
+ in->current_frame->num_vsyncs -= 1;
+
+ // Always render when paused (it's typically the last frame for a while).
+ in->dropped_frame &= !in->paused;
+
+ bool use_vsync = in->current_frame->display_synced && !in->paused;
+ if (use_vsync && !in->expecting_vsync) // first DS frame in a row
+ in->prev_vsync = now;
+ in->expecting_vsync = use_vsync;
+
+ // Store the initial value before we unlock.
+ bool request_redraw = in->request_redraw;
+
+ if (in->dropped_frame) {
+ in->drop_count += 1;
+ } else {
+ in->rendering = true;
+ in->hasframe_rendered = true;
+ int64_t prev_drop_count = vo->in->drop_count;
+ // Can the core queue new video now? Non-display-sync uses a separate
+ // timer instead, but possibly benefits from preparing a frame early.
+ bool can_queue = !in->frame_queued &&
+ (in->current_frame->num_vsyncs < 1 || !use_vsync);
+ mp_mutex_unlock(&in->lock);
+
+ if (can_queue)
+ wakeup_core(vo);
+
+ stats_time_start(in->stats, "video-draw");
+
+ vo->driver->draw_frame(vo, frame);
+
+ stats_time_end(in->stats, "video-draw");
+
+ wait_until(vo, target);
+
+ stats_time_start(in->stats, "video-flip");
+
+ vo->driver->flip_page(vo);
+
+ struct vo_vsync_info vsync = {
+ .last_queue_display_time = -1,
+ .skipped_vsyncs = -1,
+ };
+ if (vo->driver->get_vsync)
+ vo->driver->get_vsync(vo, &vsync);
+
+ // Make up some crap if presentation feedback is missing.
+ if (vsync.last_queue_display_time <= 0)
+ vsync.last_queue_display_time = mp_time_ns();
+
+ stats_time_end(in->stats, "video-flip");
+
+ mp_mutex_lock(&in->lock);
+ in->dropped_frame = prev_drop_count < vo->in->drop_count;
+ in->rendering = false;
+
+ update_vsync_timing_after_swap(vo, &vsync);
+ }
+
+ if (vo->driver->caps & VO_CAP_NORETAIN) {
+ talloc_free(in->current_frame);
+ in->current_frame = NULL;
+ }
+
+ if (in->dropped_frame) {
+ MP_STATS(vo, "drop-vo");
+ } else {
+ // If the initial redraw request was true or mpv is still playing,
+ // then we can clear it here since we just performed a redraw, or the
+ // next loop will draw what we need. However if there initially is
+ // no redraw request, then something can change this (i.e. the OSD)
+ // while the vo was unlocked. If we are paused, don't touch
+ // in->request_redraw in that case.
+ if (request_redraw || !in->paused)
+ in->request_redraw = false;
+ }
+
+ if (in->current_frame && in->current_frame->num_vsyncs &&
+ in->current_frame->display_synced)
+ more_frames = true;
+
+ if (in->frame_queued && in->frame_queued->display_synced)
+ more_frames = true;
+
+ mp_cond_broadcast(&in->wakeup); // for vo_wait_frame()
+ wakeup_core(vo);
+
+done:
+ if (!vo->driver->frame_owner)
+ talloc_free(frame);
+ mp_mutex_unlock(&in->lock);
+
+ return more_frames;
+}
+
+static void do_redraw(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+
+ if (!vo->config_ok || (vo->driver->caps & VO_CAP_NORETAIN))
+ return;
+
+ mp_mutex_lock(&in->lock);
+ in->request_redraw = false;
+ bool full_redraw = in->dropped_frame;
+ struct vo_frame *frame = NULL;
+ if (!vo->driver->untimed)
+ frame = vo_frame_ref(in->current_frame);
+ if (frame)
+ in->dropped_frame = false;
+ struct vo_frame dummy = {0};
+ if (!frame)
+ frame = &dummy;
+ frame->redraw = !full_redraw; // unconditionally redraw if it was dropped
+ frame->repeat = false;
+ frame->still = true;
+ frame->pts = 0;
+ frame->duration = -1;
+ mp_mutex_unlock(&in->lock);
+
+ vo->driver->draw_frame(vo, frame);
+ vo->driver->flip_page(vo);
+
+ if (frame != &dummy && !vo->driver->frame_owner)
+ talloc_free(frame);
+}
+
+static struct mp_image *get_image_vo(void *ctx, int imgfmt, int w, int h,
+ int stride_align, int flags)
+{
+ struct vo *vo = ctx;
+ return vo->driver->get_image(vo, imgfmt, w, h, stride_align, flags);
+}
+
+static MP_THREAD_VOID vo_thread(void *ptr)
+{
+ struct vo *vo = ptr;
+ struct vo_internal *in = vo->in;
+ bool vo_paused = false;
+
+ mp_thread_set_name("vo");
+
+ if (vo->driver->get_image) {
+ in->dr_helper = dr_helper_create(in->dispatch, get_image_vo, vo);
+ dr_helper_acquire_thread(in->dr_helper);
+ }
+
+ int r = vo->driver->preinit(vo) ? -1 : 0;
+ mp_rendezvous(vo, r); // init barrier
+ if (r < 0)
+ goto done;
+
+ read_opts(vo);
+ update_display_fps(vo);
+ vo_event(vo, VO_EVENT_WIN_STATE);
+
+ while (1) {
+ mp_dispatch_queue_process(vo->in->dispatch, 0);
+ if (in->terminate)
+ break;
+ stats_event(in->stats, "iterations");
+ vo->driver->control(vo, VOCTRL_CHECK_EVENTS, NULL);
+ bool working = render_frame(vo);
+ int64_t now = mp_time_ns();
+ int64_t wait_until = now + MP_TIME_S_TO_NS(working ? 0 : 1000);
+
+ mp_mutex_lock(&in->lock);
+ if (in->wakeup_pts) {
+ if (in->wakeup_pts > now) {
+ wait_until = MPMIN(wait_until, in->wakeup_pts);
+ } else {
+ in->wakeup_pts = 0;
+ wakeup_core(vo);
+ }
+ }
+ if (vo->want_redraw && !in->want_redraw) {
+ in->want_redraw = true;
+ wakeup_core(vo);
+ }
+ vo->want_redraw = false;
+ bool redraw = in->request_redraw;
+ bool send_reset = in->send_reset;
+ in->send_reset = false;
+ bool send_pause = in->paused != vo_paused;
+ vo_paused = in->paused;
+ mp_mutex_unlock(&in->lock);
+
+ if (send_reset)
+ vo->driver->control(vo, VOCTRL_RESET, NULL);
+ if (send_pause)
+ vo->driver->control(vo, vo_paused ? VOCTRL_PAUSE : VOCTRL_RESUME, NULL);
+ if (wait_until > now && redraw) {
+ do_redraw(vo); // now is a good time
+ continue;
+ }
+ if (vo->want_redraw) // might have been set by VOCTRLs
+ wait_until = 0;
+
+ if (wait_until <= now)
+ continue;
+
+ wait_vo(vo, wait_until);
+ }
+ forget_frames(vo); // implicitly synchronized
+ talloc_free(in->current_frame);
+ in->current_frame = NULL;
+ vo->driver->uninit(vo);
+done:
+ TA_FREEP(&in->dr_helper);
+ MP_THREAD_RETURN();
+}
+
+void vo_set_paused(struct vo *vo, bool paused)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ if (in->paused != paused) {
+ in->paused = paused;
+ if (in->paused && in->dropped_frame) {
+ in->request_redraw = true;
+ wakeup_core(vo);
+ }
+ reset_vsync_timings(vo);
+ wakeup_locked(vo);
+ }
+ mp_mutex_unlock(&in->lock);
+}
+
+int64_t vo_get_drop_count(struct vo *vo)
+{
+ mp_mutex_lock(&vo->in->lock);
+ int64_t r = vo->in->drop_count;
+ mp_mutex_unlock(&vo->in->lock);
+ return r;
+}
+
+void vo_increment_drop_count(struct vo *vo, int64_t n)
+{
+ mp_mutex_lock(&vo->in->lock);
+ vo->in->drop_count += n;
+ mp_mutex_unlock(&vo->in->lock);
+}
+
+// Make the VO redraw the OSD at some point in the future.
+void vo_redraw(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ if (!in->request_redraw) {
+ in->request_redraw = true;
+ in->want_redraw = false;
+ wakeup_locked(vo);
+ }
+ mp_mutex_unlock(&in->lock);
+}
+
+bool vo_want_redraw(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ bool r = in->want_redraw;
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+void vo_seek_reset(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ forget_frames(vo);
+ reset_vsync_timings(vo);
+ in->send_reset = true;
+ wakeup_locked(vo);
+ mp_mutex_unlock(&in->lock);
+}
+
+// Return true if there is still a frame being displayed (or queued).
+// If this returns true, a wakeup some time in the future is guaranteed.
+bool vo_still_displaying(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ bool working = in->rendering || in->frame_queued;
+ mp_mutex_unlock(&in->lock);
+ return working && in->hasframe;
+}
+
+// Whether at least 1 frame was queued or rendered since last seek or reconfig.
+bool vo_has_frame(struct vo *vo)
+{
+ return vo->in->hasframe;
+}
+
+static void run_query_format(void *p)
+{
+ void **pp = p;
+ struct vo *vo = pp[0];
+ uint8_t *list = pp[1];
+ for (int format = IMGFMT_START; format < IMGFMT_END; format++)
+ list[format - IMGFMT_START] = vo->driver->query_format(vo, format);
+}
+
+// For each item in the list (allocated as uint8_t[IMGFMT_END - IMGFMT_START]),
+// set the supported format flags.
+void vo_query_formats(struct vo *vo, uint8_t *list)
+{
+ void *p[] = {vo, list};
+ mp_dispatch_run(vo->in->dispatch, run_query_format, p);
+}
+
+// Calculate the appropriate source and destination rectangle to
+// get a correctly scaled picture, including pan-scan.
+// out_src: visible part of the video
+// out_dst: area of screen covered by the video source rectangle
+// out_osd: OSD size, OSD margins, etc.
+// Must be called from the VO thread only.
+void vo_get_src_dst_rects(struct vo *vo, struct mp_rect *out_src,
+ struct mp_rect *out_dst, struct mp_osd_res *out_osd)
+{
+ if (!vo->params) {
+ *out_src = *out_dst = (struct mp_rect){0};
+ *out_osd = (struct mp_osd_res){0};
+ return;
+ }
+ mp_get_src_dst_rects(vo->log, vo->opts, vo->driver->caps, vo->params,
+ vo->dwidth, vo->dheight, vo->monitor_par,
+ out_src, out_dst, out_osd);
+}
+
+// flip_page[_timed] will be called offset_us nanoseconds too early.
+// (For vo_vdpau, which does its own timing.)
+// num_req_frames set the requested number of requested vo_frame.frames.
+// (For vo_gpu interpolation.)
+void vo_set_queue_params(struct vo *vo, int64_t offset_ns, int num_req_frames)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ in->flip_queue_offset = offset_ns;
+ in->req_frames = MPCLAMP(num_req_frames, 1, VO_MAX_REQ_FRAMES);
+ mp_mutex_unlock(&in->lock);
+}
+
+int vo_get_num_req_frames(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ int res = in->req_frames;
+ mp_mutex_unlock(&in->lock);
+ return res;
+}
+
+double vo_get_vsync_interval(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ double res = vo->in->vsync_interval > 1 ? vo->in->vsync_interval : -1;
+ mp_mutex_unlock(&in->lock);
+ return res;
+}
+
+double vo_get_estimated_vsync_interval(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ double res = in->estimated_vsync_interval;
+ mp_mutex_unlock(&in->lock);
+ return res;
+}
+
+double vo_get_estimated_vsync_jitter(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ double res = in->estimated_vsync_jitter;
+ mp_mutex_unlock(&in->lock);
+ return res;
+}
+
+// Get the time in seconds at after which the currently rendering frame will
+// end. Returns positive values if the frame is yet to be finished, negative
+// values if it already finished.
+// This can only be called while no new frame is queued (after
+// vo_is_ready_for_frame). Returns 0 for non-display synced frames, or if the
+// deadline for continuous display was missed.
+double vo_get_delay(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ assert (!in->frame_queued);
+ int64_t res = 0;
+ if (in->base_vsync && in->vsync_interval > 1 && in->current_frame) {
+ res = in->base_vsync;
+ int extra = !!in->rendering;
+ res += (in->current_frame->num_vsyncs + extra) * in->vsync_interval;
+ if (!in->current_frame->display_synced)
+ res = 0;
+ }
+ mp_mutex_unlock(&in->lock);
+ return res ? MP_TIME_NS_TO_S(res - mp_time_ns()) : 0;
+}
+
+void vo_discard_timing_info(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ reset_vsync_timings(vo);
+ mp_mutex_unlock(&in->lock);
+}
+
+int64_t vo_get_delayed_count(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ int64_t res = vo->in->delayed_count;
+ mp_mutex_unlock(&in->lock);
+ return res;
+}
+
+double vo_get_display_fps(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ double res = vo->in->display_fps;
+ mp_mutex_unlock(&in->lock);
+ return res;
+}
+
+// Set specific event flags, and wakeup the playback core if needed.
+// vo_query_and_reset_events() can retrieve the events again.
+void vo_event(struct vo *vo, int event)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ if ((in->queued_events & event & VO_EVENTS_USER) != (event & VO_EVENTS_USER))
+ wakeup_core(vo);
+ if (event)
+ wakeup_locked(vo);
+ in->queued_events |= event;
+ in->internal_events |= event;
+ mp_mutex_unlock(&in->lock);
+}
+
+// Check event flags set with vo_event(). Return the mask of events that was
+// set and included in the events parameter. Clear the returned events.
+int vo_query_and_reset_events(struct vo *vo, int events)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ int r = in->queued_events & events;
+ in->queued_events &= ~(unsigned)r;
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+struct mp_image *vo_get_current_frame(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ struct mp_image *r = NULL;
+ if (vo->in->current_frame)
+ r = mp_image_new_ref(vo->in->current_frame->current);
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+struct vo_frame *vo_get_current_vo_frame(struct vo *vo)
+{
+ struct vo_internal *in = vo->in;
+ mp_mutex_lock(&in->lock);
+ struct vo_frame *r = vo_frame_ref(vo->in->current_frame);
+ mp_mutex_unlock(&in->lock);
+ return r;
+}
+
+struct mp_image *vo_get_image(struct vo *vo, int imgfmt, int w, int h,
+ int stride_align, int flags)
+{
+ if (vo->driver->get_image_ts)
+ return vo->driver->get_image_ts(vo, imgfmt, w, h, stride_align, flags);
+ if (vo->in->dr_helper)
+ return dr_helper_get_image(vo->in->dr_helper, imgfmt, w, h, stride_align, flags);
+ return NULL;
+}
+
+static void destroy_frame(void *p)
+{
+ struct vo_frame *frame = p;
+ for (int n = 0; n < frame->num_frames; n++)
+ talloc_free(frame->frames[n]);
+}
+
+// Return a new reference to the given frame. The image pointers are also new
+// references. Calling talloc_free() on the frame unrefs all currently set
+// image references. (Assuming current==frames[0].)
+struct vo_frame *vo_frame_ref(struct vo_frame *frame)
+{
+ if (!frame)
+ return NULL;
+
+ struct vo_frame *new = talloc_ptrtype(NULL, new);
+ talloc_set_destructor(new, destroy_frame);
+ *new = *frame;
+ for (int n = 0; n < frame->num_frames; n++)
+ new->frames[n] = mp_image_new_ref(frame->frames[n]);
+ new->current = new->num_frames ? new->frames[0] : NULL;
+ return new;
+}
+
+/*
+ * lookup an integer in a table, table must have 0 as the last key
+ * param: key key to search for
+ * returns translation corresponding to key or "to" value of last mapping
+ * if not found.
+ */
+int lookup_keymap_table(const struct mp_keymap *map, int key)
+{
+ while (map->from && map->from != key)
+ map++;
+ return map->to;
+}
+
+struct mp_image_params vo_get_current_params(struct vo *vo)
+{
+ struct mp_image_params p = {0};
+ mp_mutex_lock(&vo->in->lock);
+ if (vo->params)
+ p = *vo->params;
+ mp_mutex_unlock(&vo->in->lock);
+ return p;
+}
diff --git a/video/out/vo.h b/video/out/vo.h
new file mode 100644
index 0000000..e38dcf8
--- /dev/null
+++ b/video/out/vo.h
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) Aaron Holtzman - Aug 1999
+ *
+ * Strongly modified, most parts rewritten: A'rpi/ESP-team - 2000-2001
+ * (C) MPlayer developers
+ *
+ * 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/>.
+ */
+
+#ifndef MPLAYER_VIDEO_OUT_H
+#define MPLAYER_VIDEO_OUT_H
+
+#include <inttypes.h>
+#include <stdbool.h>
+
+#include "video/img_format.h"
+#include "common/common.h"
+#include "options/options.h"
+
+enum {
+ // VO needs to redraw
+ VO_EVENT_EXPOSE = 1 << 0,
+ // VO needs to update state to a new window size
+ VO_EVENT_RESIZE = 1 << 1,
+ // The ICC profile needs to be reloaded
+ VO_EVENT_ICC_PROFILE_CHANGED = 1 << 2,
+ // Some other window state changed (position, window state, fps)
+ VO_EVENT_WIN_STATE = 1 << 3,
+ // The ambient light conditions changed and need to be reloaded
+ VO_EVENT_AMBIENT_LIGHTING_CHANGED = 1 << 4,
+ // Special mechanism for making resizing with Cocoa react faster
+ VO_EVENT_LIVE_RESIZING = 1 << 5,
+ // For VOCTRL_GET_HIDPI_SCALE changes.
+ VO_EVENT_DPI = 1 << 6,
+ // Special thing for encode mode (vo_driver.initially_blocked).
+ // Part of VO_EVENTS_USER to make vo_is_ready_for_frame() work properly.
+ VO_EVENT_INITIAL_UNBLOCK = 1 << 7,
+ VO_EVENT_FOCUS = 1 << 8,
+
+ // Set of events the player core may be interested in.
+ VO_EVENTS_USER = VO_EVENT_RESIZE | VO_EVENT_WIN_STATE | VO_EVENT_DPI |
+ VO_EVENT_INITIAL_UNBLOCK | VO_EVENT_FOCUS,
+};
+
+enum mp_voctrl {
+ /* signal a device reset seek */
+ VOCTRL_RESET = 1,
+ /* Handle input and redraw events, called by vo_check_events() */
+ VOCTRL_CHECK_EVENTS,
+ /* signal a device pause */
+ VOCTRL_PAUSE,
+ /* start/resume playback */
+ VOCTRL_RESUME,
+
+ VOCTRL_SET_PANSCAN,
+ VOCTRL_SET_EQUALIZER,
+
+ // Triggered by any change to mp_vo_opts. This is for convenience. In theory,
+ // you could install your own listener.
+ VOCTRL_VO_OPTS_CHANGED,
+
+ /* private to vo_gpu */
+ VOCTRL_LOAD_HWDEC_API,
+
+ // Only used internally in vo_libmpv
+ VOCTRL_PREINIT,
+ VOCTRL_UNINIT,
+ VOCTRL_RECONFIG,
+
+ VOCTRL_UPDATE_WINDOW_TITLE, // char*
+ VOCTRL_UPDATE_PLAYBACK_STATE, // struct voctrl_playback_state*
+
+ VOCTRL_PERFORMANCE_DATA, // struct voctrl_performance_data*
+
+ VOCTRL_SET_CURSOR_VISIBILITY, // bool*
+
+ VOCTRL_CONTENT_TYPE, // enum mp_content_type*
+
+ VOCTRL_KILL_SCREENSAVER,
+ VOCTRL_RESTORE_SCREENSAVER,
+
+ // Return or set window size (not-fullscreen mode only - if fullscreened,
+ // these must access the not-fullscreened window size only).
+ VOCTRL_GET_UNFS_WINDOW_SIZE, // int[2] (w/h)
+ VOCTRL_SET_UNFS_WINDOW_SIZE, // int[2] (w/h)
+
+ VOCTRL_GET_FOCUSED, // bool*
+
+ // char *** (NULL terminated array compatible with CONF_TYPE_STRING_LIST)
+ // names for displays the window is on
+ VOCTRL_GET_DISPLAY_NAMES,
+
+ // Retrieve window contents. (Normal screenshots use vo_get_current_frame().)
+ // Deprecated for VOCTRL_SCREENSHOT with corresponding flags.
+ VOCTRL_SCREENSHOT_WIN, // struct mp_image**
+
+ // A normal screenshot - VOs can react to this if vo_get_current_frame() is
+ // not sufficient.
+ VOCTRL_SCREENSHOT, // struct voctrl_screenshot*
+
+ VOCTRL_UPDATE_RENDER_OPTS,
+
+ VOCTRL_GET_ICC_PROFILE, // bstr*
+ VOCTRL_GET_AMBIENT_LUX, // int*
+ VOCTRL_GET_DISPLAY_FPS, // double*
+ VOCTRL_GET_HIDPI_SCALE, // double*
+ VOCTRL_GET_DISPLAY_RES, // int[2]
+ VOCTRL_GET_WINDOW_ID, // int64_t*
+
+ /* private to vo_gpu and vo_gpu_next */
+ VOCTRL_EXTERNAL_RESIZE,
+};
+
+// Helper to expose what kind of content is currently playing to the VO.
+enum mp_content_type {
+ MP_CONTENT_NONE, // used for force-window
+ MP_CONTENT_IMAGE,
+ MP_CONTENT_VIDEO,
+};
+
+#define VO_TRUE true
+#define VO_FALSE false
+#define VO_ERROR -1
+#define VO_NOTAVAIL -2
+#define VO_NOTIMPL -3
+
+// VOCTRL_UPDATE_PLAYBACK_STATE
+struct voctrl_playback_state {
+ bool taskbar_progress;
+ bool playing;
+ bool paused;
+ int percent_pos;
+};
+
+// VOCTRL_PERFORMANCE_DATA
+#define VO_PERF_SAMPLE_COUNT 256
+
+struct mp_pass_perf {
+ // times are all in nanoseconds
+ uint64_t last, avg, peak;
+ uint64_t samples[VO_PERF_SAMPLE_COUNT];
+ uint64_t count;
+};
+
+#define VO_PASS_PERF_MAX 64
+#define VO_PASS_DESC_MAX_LEN 128
+
+struct mp_frame_perf {
+ int count;
+ struct mp_pass_perf perf[VO_PASS_PERF_MAX];
+ char desc[VO_PASS_PERF_MAX][VO_PASS_DESC_MAX_LEN];
+};
+
+struct voctrl_performance_data {
+ struct mp_frame_perf fresh, redraw;
+};
+
+struct voctrl_screenshot {
+ bool scaled, subs, osd, high_bit_depth, native_csp;
+ struct mp_image *res;
+};
+
+enum {
+ // VO does handle mp_image_params.rotate in 90 degree steps
+ VO_CAP_ROTATE90 = 1 << 0,
+ // VO does framedrop itself (vo_vdpau). Untimed/encoding VOs never drop.
+ VO_CAP_FRAMEDROP = 1 << 1,
+ // VO does not allow frames to be retained (vo_mediacodec_embed).
+ VO_CAP_NORETAIN = 1 << 2,
+ // VO supports applying film grain
+ VO_CAP_FILM_GRAIN = 1 << 3,
+};
+
+enum {
+ // Require DR buffers to be host-cached (i.e. fast readback)
+ VO_DR_FLAG_HOST_CACHED = 1 << 0,
+};
+
+#define VO_MAX_REQ_FRAMES 10
+#define VO_MAX_SWAPCHAIN_DEPTH 8
+
+struct vo;
+struct osd_state;
+struct mp_image;
+struct mp_image_params;
+
+struct vo_extra {
+ struct input_ctx *input_ctx;
+ struct osd_state *osd;
+ struct encode_lavc_context *encode_lavc_ctx;
+ void (*wakeup_cb)(void *ctx);
+ void *wakeup_ctx;
+};
+
+struct vo_frame {
+ // If > 0, realtime when frame should be shown, in mp_time_ns() units.
+ // If 0, present immediately.
+ int64_t pts;
+ // Approximate frame duration, in ns.
+ int duration;
+ // Realtime of estimated distance between 2 vsync events.
+ double vsync_interval;
+ // "ideal" display time within the vsync
+ double vsync_offset;
+ // "ideal" frame duration (can be different from num_vsyncs*vsync_interval
+ // up to a vsync) - valid for the entire frame, i.e. not changed for repeats
+ double ideal_frame_duration;
+ // "ideal" frame vsync point relative to the pts
+ double ideal_frame_vsync;
+ // "ideal" frame duration relative to the pts
+ double ideal_frame_vsync_duration;
+ // how often the frame will be repeated (does not include OSD redraws)
+ int num_vsyncs;
+ // Set if the current frame is repeated from the previous. It's guaranteed
+ // that the current is the same as the previous one, even if the image
+ // pointer is different.
+ // The repeat flag is set if exactly the same frame should be rendered
+ // again (and the OSD does not need to be redrawn).
+ // A repeat frame can be redrawn, in which case repeat==redraw==true, and
+ // OSD should be updated.
+ bool redraw, repeat;
+ // The frame is not in movement - e.g. redrawing while paused.
+ bool still;
+ // Frames are output as fast as possible, with implied vsync blocking.
+ bool display_synced;
+ // Dropping the frame is allowed if the VO is behind.
+ bool can_drop;
+ // The current frame to be drawn.
+ // Warning: When OSD should be redrawn in --force-window --idle mode, this
+ // can be NULL. The VO should draw a black background, OSD on top.
+ struct mp_image *current;
+ // List of future images, starting with the current one. This does not
+ // care about repeated frames - it simply contains the next real frames.
+ // vo_set_queue_params() sets how many future frames this should include.
+ // The actual number of frames delivered to the VO can be lower.
+ // frames[0] is current, frames[1] is the next frame.
+ // Note that some future frames may never be sent as current frame to the
+ // VO if frames are dropped.
+ int num_frames;
+ struct mp_image *frames[VO_MAX_REQ_FRAMES];
+ // Speed unadjusted, approximate frame duration inferred from past frames
+ double approx_duration;
+ // ID for frames[0] (== current). If current==NULL, the number is
+ // meaningless. Otherwise, it's an unique ID for the frame. The ID for
+ // a frame is guaranteed not to change (instant redraws will use the same
+ // ID). frames[n] has the ID frame_id+n, with the guarantee that frame
+ // drops or reconfigs will keep the guarantee.
+ // The ID is never 0 (unless num_frames==0). IDs are strictly monotonous.
+ uint64_t frame_id;
+};
+
+// Presentation feedback. See get_vsync() for how backends should fill this
+// struct.
+struct vo_vsync_info {
+ // mp_time_ns() timestamp at which the last queued frame will likely be
+ // displayed (this is in the future, unless the frame is instantly output).
+ // 0 or lower if unset or unsupported.
+ // This implies the latency of the output.
+ int64_t last_queue_display_time;
+
+ // Time between 2 vsync events in nanoseconds. The difference should be the
+ // from 2 times sampled from the same reference point (it should not be the
+ // difference between e.g. the end of scanout and the start of the next one;
+ // it must be continuous).
+ // -1 if unsupported.
+ // 0 if supported, but no value available yet. It is assumed that the value
+ // becomes available after enough swap_buffers() calls were done.
+ // >0 values are taken for granted. Very bad things will happen if it's
+ // inaccurate.
+ int64_t vsync_duration;
+
+ // Number of skipped physical vsyncs at some point in time. Typically, this
+ // value is some time in the past by an offset that equals to the latency.
+ // This value is reset and newly sampled at every swap_buffers() call.
+ // This can be used to detect delayed frames iff you try to call
+ // swap_buffers() for every physical vsync.
+ // -1 if unset or unsupported.
+ int64_t skipped_vsyncs;
+};
+
+struct vo_driver {
+ // Encoding functionality, which can be invoked via --o only.
+ bool encode;
+
+ // This requires waiting for a VO_EVENT_INITIAL_UNBLOCK event before the
+ // first frame can be sent. Doing vo_reconfig*() calls is allowed though.
+ // Encode mode uses this, the core uses vo_is_ready_for_frame() to
+ // implicitly check for this.
+ bool initially_blocked;
+
+ // VO_CAP_* bits
+ int caps;
+
+ // Disable video timing, push frames as quickly as possible, never redraw.
+ bool untimed;
+
+ // The VO is responsible for freeing frames.
+ bool frame_owner;
+
+ const char *name;
+ const char *description;
+
+ /*
+ * returns: zero on successful initialization, non-zero on error.
+ */
+ int (*preinit)(struct vo *vo);
+
+ /*
+ * Whether the given image format is supported and config() will succeed.
+ * format: one of IMGFMT_*
+ * returns: 0 on not supported, otherwise 1
+ */
+ int (*query_format)(struct vo *vo, int format);
+
+ /*
+ * Initialize or reconfigure the display driver.
+ * params: video parameters, like pixel format and frame size
+ * returns: < 0 on error, >= 0 on success
+ */
+ int (*reconfig)(struct vo *vo, struct mp_image_params *params);
+
+ /*
+ * Like reconfig(), but provides the whole mp_image for which the change is
+ * required. (The image doesn't have to have real data.)
+ */
+ int (*reconfig2)(struct vo *vo, struct mp_image *img);
+
+ /*
+ * Control interface
+ */
+ int (*control)(struct vo *vo, uint32_t request, void *data);
+
+ /*
+ * lavc callback for direct rendering
+ *
+ * Optional. To make implementation easier, the callback is always run on
+ * the VO thread. The returned mp_image's destructor callback is also called
+ * on the VO thread, even if it's actually unref'ed from another thread.
+ *
+ * It is guaranteed that the last reference to an image is destroyed before
+ * ->uninit is called (except it's not - libmpv screenshots can hold the
+ * reference longer, fuck).
+ *
+ * The allocated image - or a part of it, can be passed to draw_frame(). The
+ * point of this mechanism is that the decoder directly renders to GPU
+ * staging memory, to avoid a memcpy on frame upload. But this is not a
+ * guarantee. A filter could change the data pointers or return a newly
+ * allocated image. It's even possible that only 1 plane uses the buffer
+ * allocated by the get_image function. The VO has to check for this.
+ *
+ * stride_align is always a value >=1. The stride values of the returned
+ * image must be divisible by this value. This may be a non power of two.
+ *
+ * flags is a combination of VO_DR_FLAG_* flags.
+ *
+ * Currently, the returned image must have exactly 1 AVBufferRef set, for
+ * internal implementation simplicity.
+ *
+ * returns: an allocated, refcounted image; if NULL is returned, the caller
+ * will silently fallback to a default allocator
+ */
+ struct mp_image *(*get_image)(struct vo *vo, int imgfmt, int w, int h,
+ int stride_align, int flags);
+
+ /*
+ * Thread-safe variant of get_image. Set at most one of these callbacks.
+ * This excludes _all_ synchronization magic. The only guarantee is that
+ * vo_driver.uninit is not called before this function returns.
+ */
+ struct mp_image *(*get_image_ts)(struct vo *vo, int imgfmt, int w, int h,
+ int stride_align, int flags);
+
+ /* Render the given frame. Note that this is also called when repeating
+ * or redrawing frames.
+ *
+ * frame is freed by the caller if the callee did not assume ownership
+ * of the frames, but in any case the callee can still modify the
+ * contained data and references.
+ */
+ void (*draw_frame)(struct vo *vo, struct vo_frame *frame);
+
+ /*
+ * Blit/Flip buffer to the screen. Must be called after each frame!
+ */
+ void (*flip_page)(struct vo *vo);
+
+ /*
+ * Return presentation feedback. The implementation should not touch fields
+ * it doesn't support; the info fields are preinitialized to neutral values.
+ * Usually called once after flip_page(), but can be called any time.
+ * The values returned by this are always relative to the last flip_page()
+ * call.
+ */
+ void (*get_vsync)(struct vo *vo, struct vo_vsync_info *info);
+
+ /* These optional callbacks can be provided if the GUI framework used by
+ * the VO requires entering a message loop for receiving events and does
+ * not call vo_wakeup() from a separate thread when there are new events.
+ *
+ * wait_events() will wait for new events, until the timeout expires, or the
+ * function is interrupted. wakeup() is used to possibly interrupt the
+ * event loop (wakeup() itself must be thread-safe, and not call any other
+ * VO functions; it's the only vo_driver function with this requirement).
+ * wakeup() should behave like a binary semaphore; if wait_events() is not
+ * being called while wakeup() is, the next wait_events() call should exit
+ * immediately.
+ */
+ void (*wakeup)(struct vo *vo);
+ void (*wait_events)(struct vo *vo, int64_t until_time_ns);
+
+ /*
+ * Closes driver. Should restore the original state of the system.
+ */
+ void (*uninit)(struct vo *vo);
+
+ // Size of private struct for automatic allocation (0 doesn't allocate)
+ int priv_size;
+
+ // If not NULL, it's copied into the newly allocated private struct.
+ const void *priv_defaults;
+
+ // List of options to parse into priv struct (requires priv_size to be set)
+ // This will register them as global options (with options_prefix), and
+ // copy the current value at VO creation time to the priv struct.
+ const struct m_option *options;
+
+ // All options in the above array are prefixed with this string. (It's just
+ // for convenience and makes no difference in semantics.)
+ const char *options_prefix;
+
+ // Registers global options that go to a separate options struct.
+ const struct m_sub_options *global_opts;
+};
+
+struct vo {
+ const struct vo_driver *driver;
+ struct mp_log *log; // Using e.g. "[vo/vdpau]" as prefix
+ void *priv;
+ struct mpv_global *global;
+ struct vo_x11_state *x11;
+ struct vo_w32_state *w32;
+ struct vo_wayland_state *wl;
+ struct vo_android_state *android;
+ struct vo_drm_state *drm;
+ struct mp_hwdec_devices *hwdec_devs;
+ struct input_ctx *input_ctx;
+ struct osd_state *osd;
+ struct encode_lavc_context *encode_lavc_ctx;
+ struct vo_internal *in;
+ struct vo_extra extra;
+
+ // --- The following fields are generally only changed during initialization.
+
+ bool probing;
+
+ // --- The following fields are only changed with vo_reconfig(), and can
+ // be accessed unsynchronized (read-only).
+
+ int config_ok; // Last config call was successful?
+ struct mp_image_params *params; // Configured parameters (as in vo_reconfig)
+
+ // --- The following fields can be accessed only by the VO thread, or from
+ // anywhere _if_ the VO thread is suspended (use vo->dispatch).
+
+ struct m_config_cache *opts_cache; // cache for ->opts
+ struct mp_vo_opts *opts;
+ struct m_config_cache *gl_opts_cache;
+ struct m_config_cache *eq_opts_cache;
+
+ bool want_redraw; // redraw as soon as possible
+
+ // current window state
+ int dwidth;
+ int dheight;
+ float monitor_par;
+};
+
+struct mpv_global;
+struct vo *init_best_video_out(struct mpv_global *global, struct vo_extra *ex);
+int vo_reconfig(struct vo *vo, struct mp_image_params *p);
+int vo_reconfig2(struct vo *vo, struct mp_image *img);
+
+int vo_control(struct vo *vo, int request, void *data);
+void vo_control_async(struct vo *vo, int request, void *data);
+bool vo_is_ready_for_frame(struct vo *vo, int64_t next_pts);
+void vo_queue_frame(struct vo *vo, struct vo_frame *frame);
+void vo_wait_frame(struct vo *vo);
+bool vo_still_displaying(struct vo *vo);
+bool vo_has_frame(struct vo *vo);
+void vo_redraw(struct vo *vo);
+bool vo_want_redraw(struct vo *vo);
+void vo_seek_reset(struct vo *vo);
+void vo_destroy(struct vo *vo);
+void vo_set_paused(struct vo *vo, bool paused);
+int64_t vo_get_drop_count(struct vo *vo);
+void vo_increment_drop_count(struct vo *vo, int64_t n);
+int64_t vo_get_delayed_count(struct vo *vo);
+void vo_query_formats(struct vo *vo, uint8_t *list);
+void vo_event(struct vo *vo, int event);
+int vo_query_and_reset_events(struct vo *vo, int events);
+struct mp_image *vo_get_current_frame(struct vo *vo);
+void vo_set_queue_params(struct vo *vo, int64_t offset_ns, int num_req_frames);
+int vo_get_num_req_frames(struct vo *vo);
+double vo_get_vsync_interval(struct vo *vo);
+double vo_get_estimated_vsync_interval(struct vo *vo);
+double vo_get_estimated_vsync_jitter(struct vo *vo);
+double vo_get_display_fps(struct vo *vo);
+double vo_get_delay(struct vo *vo);
+void vo_discard_timing_info(struct vo *vo);
+struct vo_frame *vo_get_current_vo_frame(struct vo *vo);
+struct mp_image *vo_get_image(struct vo *vo, int imgfmt, int w, int h,
+ int stride_align, int flags);
+
+void vo_wakeup(struct vo *vo);
+void vo_wait_default(struct vo *vo, int64_t until_time);
+
+struct mp_keymap {
+ int from;
+ int to;
+};
+int lookup_keymap_table(const struct mp_keymap *map, int key);
+
+struct mp_osd_res;
+void vo_get_src_dst_rects(struct vo *vo, struct mp_rect *out_src,
+ struct mp_rect *out_dst, struct mp_osd_res *out_osd);
+
+struct vo_frame *vo_frame_ref(struct vo_frame *frame);
+
+struct mp_image_params vo_get_current_params(struct vo *vo);
+
+#endif /* MPLAYER_VIDEO_OUT_H */
diff --git a/video/out/vo_caca.c b/video/out/vo_caca.c
new file mode 100644
index 0000000..0625de0
--- /dev/null
+++ b/video/out/vo_caca.c
@@ -0,0 +1,314 @@
+/*
+ * video output driver for libcaca
+ *
+ * by Pigeon <pigeon@pigeond.net>
+ *
+ * Some functions/codes/ideas are from x11 and aalib vo
+ *
+ * TODO: support draw_alpha?
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <string.h>
+#include <time.h>
+#include <errno.h>
+#include <assert.h>
+#include <caca.h>
+
+#include "config.h"
+#include "vo.h"
+#include "video/mp_image.h"
+
+#include "input/keycodes.h"
+#include "input/input.h"
+#include "common/msg.h"
+#include "input/input.h"
+
+#include "config.h"
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+struct priv {
+ caca_canvas_t *canvas;
+ caca_display_t *display;
+ caca_dither_t *dither;
+ uint8_t *dither_buffer;
+ const char *dither_antialias;
+ const char *dither_charset;
+ const char *dither_color;
+ const char *dither_algo;
+
+ /* image infos */
+ int image_format;
+ int image_width;
+ int image_height;
+
+ int screen_w, screen_h;
+};
+
+/* We want 24bpp always for now */
+static const unsigned int bpp = 24;
+static const unsigned int depth = 3;
+static const unsigned int rmask = 0xff0000;
+static const unsigned int gmask = 0x00ff00;
+static const unsigned int bmask = 0x0000ff;
+static const unsigned int amask = 0;
+
+static int resize(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+ priv->screen_w = caca_get_canvas_width(priv->canvas);
+ priv->screen_h = caca_get_canvas_height(priv->canvas);
+
+ caca_free_dither(priv->dither);
+ talloc_free(priv->dither_buffer);
+
+ priv->dither = caca_create_dither(bpp, priv->image_width, priv->image_height,
+ depth * priv->image_width,
+ rmask, gmask, bmask, amask);
+ if (priv->dither == NULL) {
+ MP_FATAL(vo, "caca_create_dither failed!\n");
+ return -1;
+ }
+ priv->dither_buffer =
+ talloc_array(NULL, uint8_t, depth * priv->image_width * priv->image_height);
+
+ /* Default libcaca features */
+ caca_set_dither_antialias(priv->dither, priv->dither_antialias);
+ caca_set_dither_charset(priv->dither, priv->dither_charset);
+ caca_set_dither_color(priv->dither, priv->dither_color);
+ caca_set_dither_algorithm(priv->dither, priv->dither_algo);
+
+ return 0;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *priv = vo->priv;
+ priv->image_height = params->h;
+ priv->image_width = params->w;
+ priv->image_format = params->imgfmt;
+
+ return resize(vo);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *priv = vo->priv;
+ struct mp_image *mpi = frame->current;
+ if (!mpi)
+ return;
+ memcpy_pic(priv->dither_buffer, mpi->planes[0], priv->image_width * depth, priv->image_height,
+ priv->image_width * depth, mpi->stride[0]);
+ caca_dither_bitmap(priv->canvas, 0, 0, priv->screen_w, priv->screen_h, priv->dither, priv->dither_buffer);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+ caca_refresh_display(priv->display);
+}
+
+static void set_next_str(const char * const *list, const char **str,
+ const char **msg)
+{
+ int ind;
+ for (ind = 0; list[ind]; ind += 2) {
+ if (strcmp(list[ind], *str) == 0) {
+ if (list[ind + 2] == NULL)
+ ind = -2;
+ *str = list[ind + 2];
+ *msg = list[ind + 3];
+ return;
+ }
+ }
+
+ *str = list[0];
+ *msg = list[1];
+}
+
+static const struct mp_keymap keysym_map[] = {
+ {CACA_KEY_RETURN, MP_KEY_ENTER}, {CACA_KEY_ESCAPE, MP_KEY_ESC},
+ {CACA_KEY_UP, MP_KEY_UP}, {CACA_KEY_DOWN, MP_KEY_DOWN},
+ {CACA_KEY_LEFT, MP_KEY_LEFT}, {CACA_KEY_RIGHT, MP_KEY_RIGHT},
+ {CACA_KEY_PAGEUP, MP_KEY_PAGE_UP}, {CACA_KEY_PAGEDOWN, MP_KEY_PAGE_DOWN},
+ {CACA_KEY_HOME, MP_KEY_HOME}, {CACA_KEY_END, MP_KEY_END},
+ {CACA_KEY_INSERT, MP_KEY_INSERT}, {CACA_KEY_DELETE, MP_KEY_DELETE},
+ {CACA_KEY_BACKSPACE, MP_KEY_BACKSPACE}, {CACA_KEY_TAB, MP_KEY_TAB},
+ {CACA_KEY_PAUSE, MP_KEY_PAUSE},
+ {CACA_KEY_F1, MP_KEY_F+1}, {CACA_KEY_F2, MP_KEY_F+2},
+ {CACA_KEY_F3, MP_KEY_F+3}, {CACA_KEY_F4, MP_KEY_F+4},
+ {CACA_KEY_F5, MP_KEY_F+5}, {CACA_KEY_F6, MP_KEY_F+6},
+ {CACA_KEY_F7, MP_KEY_F+7}, {CACA_KEY_F8, MP_KEY_F+8},
+ {CACA_KEY_F9, MP_KEY_F+9}, {CACA_KEY_F10, MP_KEY_F+10},
+ {CACA_KEY_F11, MP_KEY_F+11}, {CACA_KEY_F12, MP_KEY_F+12},
+ {CACA_KEY_F13, MP_KEY_F+13}, {CACA_KEY_F14, MP_KEY_F+14},
+ {CACA_KEY_F15, MP_KEY_F+15},
+ {0, 0}
+};
+
+static void check_events(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+
+ caca_event_t cev;
+ while (caca_get_event(priv->display, CACA_EVENT_ANY, &cev, 0)) {
+
+ switch (cev.type) {
+ case CACA_EVENT_RESIZE:
+ caca_refresh_display(priv->display);
+ resize(vo);
+ break;
+ case CACA_EVENT_QUIT:
+ mp_input_put_key(vo->input_ctx, MP_KEY_CLOSE_WIN);
+ break;
+ case CACA_EVENT_MOUSE_MOTION:
+ mp_input_set_mouse_pos(vo->input_ctx, cev.data.mouse.x, cev.data.mouse.y);
+ break;
+ case CACA_EVENT_MOUSE_PRESS:
+ mp_input_put_key(vo->input_ctx,
+ (MP_MBTN_BASE + cev.data.mouse.button - 1) | MP_KEY_STATE_DOWN);
+ break;
+ case CACA_EVENT_MOUSE_RELEASE:
+ mp_input_put_key(vo->input_ctx,
+ (MP_MBTN_BASE + cev.data.mouse.button - 1) | MP_KEY_STATE_UP);
+ break;
+ case CACA_EVENT_KEY_PRESS:
+ {
+ int key = cev.data.key.ch;
+ int mpkey = lookup_keymap_table(keysym_map, key);
+ const char *msg_name;
+
+ if (mpkey)
+ mp_input_put_key(vo->input_ctx, mpkey);
+ else
+ switch (key) {
+ case 'd':
+ case 'D':
+ /* Toggle dithering algorithm */
+ set_next_str(caca_get_dither_algorithm_list(priv->dither),
+ &priv->dither_algo, &msg_name);
+ caca_set_dither_algorithm(priv->dither, priv->dither_algo);
+ break;
+
+ case 'a':
+ case 'A':
+ /* Toggle antialiasing method */
+ set_next_str(caca_get_dither_antialias_list(priv->dither),
+ &priv->dither_antialias, &msg_name);
+ caca_set_dither_antialias(priv->dither, priv->dither_antialias);
+ break;
+
+ case 'h':
+ case 'H':
+ /* Toggle charset method */
+ set_next_str(caca_get_dither_charset_list(priv->dither),
+ &priv->dither_charset, &msg_name);
+ caca_set_dither_charset(priv->dither, priv->dither_charset);
+ break;
+
+ case 'c':
+ case 'C':
+ /* Toggle color method */
+ set_next_str(caca_get_dither_color_list(priv->dither),
+ &priv->dither_color, &msg_name);
+ caca_set_dither_color(priv->dither, priv->dither_color);
+ break;
+
+ default:
+ if (key <= 255)
+ mp_input_put_key(vo->input_ctx, key);
+ break;
+ }
+ }
+ }
+ }
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+ caca_free_dither(priv->dither);
+ priv->dither = NULL;
+ talloc_free(priv->dither_buffer);
+ priv->dither_buffer = NULL;
+ caca_free_display(priv->display);
+ caca_free_canvas(priv->canvas);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+
+ priv->dither_antialias = "default";
+ priv->dither_charset = "default";
+ priv->dither_color = "default";
+ priv->dither_algo = "none";
+
+ priv->canvas = caca_create_canvas(0, 0);
+ if (priv->canvas == NULL) {
+ MP_ERR(vo, "failed to create canvas\n");
+ return ENOSYS;
+ }
+
+ priv->display = caca_create_display(priv->canvas);
+
+ if (priv->display == NULL) {
+ MP_ERR(vo, "failed to create display\n");
+ caca_free_canvas(priv->canvas);
+ return ENOSYS;
+ }
+
+ return 0;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return format == IMGFMT_BGR24;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct priv *priv = vo->priv;
+ switch (request) {
+ case VOCTRL_CHECK_EVENTS:
+ check_events(vo);
+ return VO_TRUE;
+ case VOCTRL_UPDATE_WINDOW_TITLE:
+ caca_set_display_title(priv->display, (char *)data);
+ return VO_TRUE;
+ }
+ return VO_NOTIMPL;
+}
+
+const struct vo_driver video_out_caca = {
+ .name = "caca",
+ .description = "libcaca",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+};
diff --git a/video/out/vo_direct3d.c b/video/out/vo_direct3d.c
new file mode 100644
index 0000000..16936bb
--- /dev/null
+++ b/video/out/vo_direct3d.c
@@ -0,0 +1,1247 @@
+/*
+ * Copyright (c) 2008 Georgi Petrov (gogothebee) <gogothebee@gmail.com>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+#include <stdbool.h>
+#include <assert.h>
+#include <d3d9.h>
+#include <inttypes.h>
+#include <limits.h>
+#include "config.h"
+#include "options/options.h"
+#include "options/m_option.h"
+#include "sub/draw_bmp.h"
+#include "mpv_talloc.h"
+#include "vo.h"
+#include "video/csputils.h"
+#include "video/mp_image.h"
+#include "video/img_format.h"
+#include "common/msg.h"
+#include "common/common.h"
+#include "w32_common.h"
+#include "sub/osd.h"
+
+#include "config.h"
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+#define DEVTYPE D3DDEVTYPE_HAL
+//#define DEVTYPE D3DDEVTYPE_REF
+
+#define D3DFVF_OSD_VERTEX (D3DFVF_XYZ | D3DFVF_TEX1)
+
+typedef struct {
+ float x, y, z;
+ float tu, tv;
+} vertex_osd;
+
+struct d3dtex {
+ // user-requested size
+ int w, h;
+ // allocated texture size
+ int tex_w, tex_h;
+ // D3DPOOL_SYSTEMMEM (or others) texture:
+ // - can be locked in order to write (and even read) data
+ // - can _not_ (probably) be used as texture for rendering
+ // This is always non-NULL if d3dtex_allocate succeeds.
+ IDirect3DTexture9 *system;
+ // D3DPOOL_DEFAULT texture:
+ // - can't be locked (Probably.)
+ // - must be used for rendering
+ // This can be NULL if the system one can be both locked and mapped.
+ IDirect3DTexture9 *device;
+};
+
+#define MAX_OSD_RECTS 64
+
+/* Global variables "priv" structure. I try to keep their count low.
+ */
+typedef struct d3d_priv {
+ struct mp_log *log;
+
+ bool opt_disable_texture_align;
+ // debugging
+ bool opt_force_power_of_2;
+ int opt_texture_memory;
+ bool opt_swap_discard;
+ bool opt_exact_backbuffer;
+
+ struct vo *vo;
+
+ bool have_image;
+ double osd_pts;
+
+ D3DLOCKED_RECT locked_rect; /**< The locked offscreen surface */
+ RECT fs_movie_rect; /**< Rect (upscaled) of the movie when displayed
+ in fullscreen */
+ RECT fs_panscan_rect; /**< PanScan source surface cropping in
+ fullscreen */
+ int src_width; /**< Source (movie) width */
+ int src_height; /**< Source (movie) height */
+ struct mp_osd_res osd_res;
+ int image_format; /**< mplayer image format */
+ struct mp_image_params params;
+
+ D3DFORMAT movie_src_fmt; /**< Movie colorspace format (depends on
+ the movie's codec) */
+ D3DFORMAT desktop_fmt; /**< Desktop (screen) colorspace format.
+ Usually XRGB */
+
+ HANDLE d3d9_dll; /**< d3d9 Library HANDLE */
+ IDirect3D9 * (WINAPI *pDirect3DCreate9)(UINT); /**< pointer to Direct3DCreate9 function */
+
+ LPDIRECT3D9 d3d_handle; /**< Direct3D Handle */
+ LPDIRECT3DDEVICE9 d3d_device; /**< The Direct3D Adapter */
+ bool d3d_in_scene; /**< BeginScene was called, EndScene not */
+ IDirect3DSurface9 *d3d_surface; /**< Offscreen Direct3D Surface. MPlayer
+ renders inside it. Uses colorspace
+ priv->movie_src_fmt */
+ IDirect3DSurface9 *d3d_backbuf; /**< Video card's back buffer (used to
+ display next frame) */
+ int cur_backbuf_width; /**< Current backbuffer width */
+ int cur_backbuf_height; /**< Current backbuffer height */
+ int device_caps_power2_only; /**< 1 = texture sizes have to be power 2
+ 0 = texture sizes can be anything */
+ int device_caps_square_only; /**< 1 = textures have to be square
+ 0 = textures do not have to be square */
+ int device_texture_sys; /**< 1 = device can texture from system memory
+ 0 = device requires shadow */
+ int max_texture_width; /**< from the device capabilities */
+ int max_texture_height; /**< from the device capabilities */
+
+ D3DMATRIX d3d_colormatrix;
+
+ struct mp_draw_sub_cache *osd_cache;
+ struct d3dtex osd_texture;
+ int osd_num_vertices;
+ vertex_osd osd_vertices[MAX_OSD_RECTS * 6];
+} d3d_priv;
+
+struct fmt_entry {
+ const unsigned int mplayer_fmt; /**< Given by MPlayer */
+ const D3DFORMAT fourcc; /**< Required by D3D's test function */
+};
+
+/* Map table from reported MPlayer format to the required
+ fourcc. This is needed to perform the format query. */
+
+static const struct fmt_entry fmt_table[] = {
+ // planar YUV
+ {IMGFMT_420P, MAKEFOURCC('Y','V','1','2')},
+ {IMGFMT_420P, MAKEFOURCC('I','4','2','0')},
+ {IMGFMT_420P, MAKEFOURCC('I','Y','U','V')},
+ {IMGFMT_NV12, MAKEFOURCC('N','V','1','2')},
+ // packed YUV
+ {IMGFMT_UYVY, D3DFMT_UYVY},
+ // packed RGB
+ {IMGFMT_BGR0, D3DFMT_X8R8G8B8},
+ {IMGFMT_RGB0, D3DFMT_X8B8G8R8},
+ {IMGFMT_BGR24, D3DFMT_R8G8B8}, //untested
+ {IMGFMT_RGB565, D3DFMT_R5G6B5},
+ {0},
+};
+
+
+static bool resize_d3d(d3d_priv *priv);
+static void uninit(struct vo *vo);
+static void flip_page(struct vo *vo);
+static mp_image_t *get_window_screenshot(d3d_priv *priv);
+static void draw_osd(struct vo *vo);
+static bool change_d3d_backbuffer(d3d_priv *priv);
+
+static void d3d_matrix_identity(D3DMATRIX *m)
+{
+ memset(m, 0, sizeof(D3DMATRIX));
+ m->_11 = m->_22 = m->_33 = m->_44 = 1.0f;
+}
+
+static void d3d_matrix_ortho(D3DMATRIX *m, float left, float right,
+ float bottom, float top)
+{
+ d3d_matrix_identity(m);
+ m->_11 = 2.0f / (right - left);
+ m->_22 = 2.0f / (top - bottom);
+ m->_33 = 1.0f;
+ m->_41 = -(right + left) / (right - left);
+ m->_42 = -(top + bottom) / (top - bottom);
+ m->_43 = 0;
+ m->_44 = 1.0f;
+}
+
+/****************************************************************************
+ * *
+ * *
+ * *
+ * Direct3D specific implementation functions *
+ * *
+ * *
+ * *
+ ****************************************************************************/
+
+static bool d3d_begin_scene(d3d_priv *priv)
+{
+ if (!priv->d3d_in_scene) {
+ if (FAILED(IDirect3DDevice9_BeginScene(priv->d3d_device))) {
+ MP_ERR(priv, "BeginScene failed.\n");
+ return false;
+ }
+ priv->d3d_in_scene = true;
+ }
+ return true;
+}
+
+/** @brief Calculate scaled fullscreen movie rectangle with
+ * preserved aspect ratio.
+ */
+static void calc_fs_rect(d3d_priv *priv)
+{
+ struct mp_rect src_rect;
+ struct mp_rect dst_rect;
+ vo_get_src_dst_rects(priv->vo, &src_rect, &dst_rect, &priv->osd_res);
+
+ priv->fs_movie_rect.left = dst_rect.x0;
+ priv->fs_movie_rect.right = dst_rect.x1;
+ priv->fs_movie_rect.top = dst_rect.y0;
+ priv->fs_movie_rect.bottom = dst_rect.y1;
+ priv->fs_panscan_rect.left = src_rect.x0;
+ priv->fs_panscan_rect.right = src_rect.x1;
+ priv->fs_panscan_rect.top = src_rect.y0;
+ priv->fs_panscan_rect.bottom = src_rect.y1;
+}
+
+// Adjust the texture size *width/*height to fit the requirements of the D3D
+// device. The texture size is only increased.
+static void d3d_fix_texture_size(d3d_priv *priv, int *width, int *height)
+{
+ int tex_width = *width;
+ int tex_height = *height;
+
+ // avoid nasty special cases with 0-sized textures and texture sizes
+ tex_width = MPMAX(tex_width, 1);
+ tex_height = MPMAX(tex_height, 1);
+
+ if (priv->device_caps_power2_only) {
+ tex_width = 1;
+ tex_height = 1;
+ while (tex_width < *width) tex_width <<= 1;
+ while (tex_height < *height) tex_height <<= 1;
+ }
+ if (priv->device_caps_square_only)
+ /* device only supports square textures */
+ tex_width = tex_height = MPMAX(tex_width, tex_height);
+ // better round up to a multiple of 16
+ if (!priv->opt_disable_texture_align) {
+ tex_width = (tex_width + 15) & ~15;
+ tex_height = (tex_height + 15) & ~15;
+ }
+
+ *width = tex_width;
+ *height = tex_height;
+}
+
+static void d3dtex_release(d3d_priv *priv, struct d3dtex *tex)
+{
+ if (tex->system)
+ IDirect3DTexture9_Release(tex->system);
+ tex->system = NULL;
+
+ if (tex->device)
+ IDirect3DTexture9_Release(tex->device);
+ tex->device = NULL;
+
+ tex->tex_w = tex->tex_h = 0;
+}
+
+static bool d3dtex_allocate(d3d_priv *priv, struct d3dtex *tex, D3DFORMAT fmt,
+ int w, int h)
+{
+ d3dtex_release(priv, tex);
+
+ tex->w = w;
+ tex->h = h;
+
+ int tw = w, th = h;
+ d3d_fix_texture_size(priv, &tw, &th);
+
+ bool use_sh = !priv->device_texture_sys;
+ int memtype = D3DPOOL_SYSTEMMEM;
+ switch (priv->opt_texture_memory) {
+ case 1: memtype = D3DPOOL_MANAGED; use_sh = false; break;
+ case 2: memtype = D3DPOOL_DEFAULT; use_sh = false; break;
+ case 3: memtype = D3DPOOL_DEFAULT; use_sh = true; break;
+ case 4: memtype = D3DPOOL_SCRATCH; use_sh = true; break;
+ }
+
+ if (FAILED(IDirect3DDevice9_CreateTexture(priv->d3d_device, tw, th, 1,
+ D3DUSAGE_DYNAMIC, fmt, memtype, &tex->system, NULL)))
+ {
+ MP_ERR(priv, "Allocating %dx%d texture in system RAM failed.\n", w, h);
+ goto error_exit;
+ }
+
+ if (use_sh) {
+ if (FAILED(IDirect3DDevice9_CreateTexture(priv->d3d_device, tw, th, 1,
+ D3DUSAGE_DYNAMIC, fmt, D3DPOOL_DEFAULT, &tex->device, NULL)))
+ {
+ MP_ERR(priv, "Allocating %dx%d texture in video RAM failed.\n", w, h);
+ goto error_exit;
+ }
+ }
+
+ tex->tex_w = tw;
+ tex->tex_h = th;
+
+ return true;
+
+error_exit:
+ d3dtex_release(priv, tex);
+ return false;
+}
+
+static IDirect3DBaseTexture9 *d3dtex_get_render_texture(d3d_priv *priv,
+ struct d3dtex *tex)
+{
+ return (IDirect3DBaseTexture9 *)(tex->device ? tex->device : tex->system);
+}
+
+// Copy system texture contents to device texture.
+static bool d3dtex_update(d3d_priv *priv, struct d3dtex *tex)
+{
+ if (!tex->device)
+ return true;
+ return !FAILED(IDirect3DDevice9_UpdateTexture(priv->d3d_device,
+ (IDirect3DBaseTexture9 *)tex->system,
+ (IDirect3DBaseTexture9 *)tex->device));
+}
+
+static void d3d_unlock_video_objects(d3d_priv *priv)
+{
+ if (priv->locked_rect.pBits) {
+ if (FAILED(IDirect3DSurface9_UnlockRect(priv->d3d_surface)))
+ MP_VERBOSE(priv, "Unlocking video objects failed.\n");
+ }
+ priv->locked_rect.pBits = NULL;
+}
+
+// Free video surface/textures, etc.
+static void d3d_destroy_video_objects(d3d_priv *priv)
+{
+ d3d_unlock_video_objects(priv);
+
+ if (priv->d3d_surface)
+ IDirect3DSurface9_Release(priv->d3d_surface);
+ priv->d3d_surface = NULL;
+}
+
+/** @brief Destroy D3D Offscreen and Backbuffer surfaces.
+ */
+static void destroy_d3d_surfaces(d3d_priv *priv)
+{
+ MP_VERBOSE(priv, "destroy_d3d_surfaces called.\n");
+
+ d3d_destroy_video_objects(priv);
+ d3dtex_release(priv, &priv->osd_texture);
+
+ if (priv->d3d_backbuf)
+ IDirect3DSurface9_Release(priv->d3d_backbuf);
+ priv->d3d_backbuf = NULL;
+
+ priv->d3d_in_scene = false;
+}
+
+// Allocate video surface.
+static bool d3d_configure_video_objects(d3d_priv *priv)
+{
+ assert(priv->image_format != 0);
+
+ if (!priv->d3d_surface &&
+ FAILED(IDirect3DDevice9_CreateOffscreenPlainSurface(
+ priv->d3d_device, priv->src_width, priv->src_height,
+ priv->movie_src_fmt, D3DPOOL_DEFAULT, &priv->d3d_surface, NULL)))
+ {
+ MP_ERR(priv, "Allocating offscreen surface failed.\n");
+ return false;
+ }
+
+ return true;
+}
+
+// Recreate and initialize D3D objects if necessary. The amount of work that
+// needs to be done can be quite different: it could be that full initialization
+// is required, or that some objects need to be created, or that nothing is
+// done.
+static bool create_d3d_surfaces(d3d_priv *priv)
+{
+ MP_VERBOSE(priv, "create_d3d_surfaces called.\n");
+
+ if (!priv->d3d_backbuf &&
+ FAILED(IDirect3DDevice9_GetBackBuffer(priv->d3d_device, 0, 0,
+ D3DBACKBUFFER_TYPE_MONO,
+ &priv->d3d_backbuf))) {
+ MP_ERR(priv, "Allocating backbuffer failed.\n");
+ return 0;
+ }
+
+ if (!d3d_configure_video_objects(priv))
+ return 0;
+
+ /* setup default renderstate */
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_ALPHAFUNC, D3DCMP_GREATER);
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_ALPHAREF, (DWORD)0x0);
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_LIGHTING, FALSE);
+
+ // we use up to 3 samplers for up to 3 YUV planes
+ // TODO
+ /*
+ for (int n = 0; n < 3; n++) {
+ IDirect3DDevice9_SetSamplerState(priv->d3d_device, n, D3DSAMP_MINFILTER,
+ D3DTEXF_LINEAR);
+ IDirect3DDevice9_SetSamplerState(priv->d3d_device, n, D3DSAMP_MAGFILTER,
+ D3DTEXF_LINEAR);
+ IDirect3DDevice9_SetSamplerState(priv->d3d_device, n, D3DSAMP_ADDRESSU,
+ D3DTADDRESS_CLAMP);
+ IDirect3DDevice9_SetSamplerState(priv->d3d_device, n, D3DSAMP_ADDRESSV,
+ D3DTADDRESS_CLAMP);
+ }
+ */
+
+ return 1;
+}
+
+static bool init_d3d(d3d_priv *priv)
+{
+ D3DDISPLAYMODE disp_mode;
+ D3DCAPS9 disp_caps;
+ DWORD texture_caps;
+ DWORD dev_caps;
+
+ priv->d3d_handle = priv->pDirect3DCreate9(D3D_SDK_VERSION);
+ if (!priv->d3d_handle) {
+ MP_ERR(priv, "Initializing Direct3D failed.\n");
+ return false;
+ }
+
+ if (FAILED(IDirect3D9_GetAdapterDisplayMode(priv->d3d_handle,
+ D3DADAPTER_DEFAULT,
+ &disp_mode))) {
+ MP_ERR(priv, "Reading display mode failed.\n");
+ return false;
+ }
+
+ priv->desktop_fmt = disp_mode.Format;
+ priv->cur_backbuf_width = disp_mode.Width;
+ priv->cur_backbuf_height = disp_mode.Height;
+
+ MP_VERBOSE(priv, "Setting backbuffer dimensions to (%dx%d).\n",
+ disp_mode.Width, disp_mode.Height);
+
+ if (FAILED(IDirect3D9_GetDeviceCaps(priv->d3d_handle,
+ D3DADAPTER_DEFAULT,
+ DEVTYPE,
+ &disp_caps)))
+ {
+ MP_ERR(priv, "Reading display capabilities failed.\n");
+ return false;
+ }
+
+ /* Store relevant information reguarding caps of device */
+ texture_caps = disp_caps.TextureCaps;
+ dev_caps = disp_caps.DevCaps;
+ priv->device_caps_power2_only = (texture_caps & D3DPTEXTURECAPS_POW2) &&
+ !(texture_caps & D3DPTEXTURECAPS_NONPOW2CONDITIONAL);
+ priv->device_caps_square_only = texture_caps & D3DPTEXTURECAPS_SQUAREONLY;
+ priv->device_texture_sys = dev_caps & D3DDEVCAPS_TEXTURESYSTEMMEMORY;
+ priv->max_texture_width = disp_caps.MaxTextureWidth;
+ priv->max_texture_height = disp_caps.MaxTextureHeight;
+
+ if (priv->opt_force_power_of_2)
+ priv->device_caps_power2_only = 1;
+
+ if (FAILED(IDirect3D9_CheckDeviceFormat(priv->d3d_handle,
+ D3DADAPTER_DEFAULT,
+ DEVTYPE,
+ priv->desktop_fmt,
+ D3DUSAGE_DYNAMIC | D3DUSAGE_QUERY_FILTER,
+ D3DRTYPE_TEXTURE,
+ D3DFMT_A8R8G8B8)))
+ {
+ MP_ERR(priv, "OSD texture format not supported.\n");
+ return false;
+ }
+
+ if (!change_d3d_backbuffer(priv))
+ return false;
+
+ MP_VERBOSE(priv, "device_caps_power2_only %d, device_caps_square_only %d\n"
+ "device_texture_sys %d\n"
+ "max_texture_width %d, max_texture_height %d\n",
+ priv->device_caps_power2_only, priv->device_caps_square_only,
+ priv->device_texture_sys, priv->max_texture_width,
+ priv->max_texture_height);
+
+ return true;
+}
+
+/** @brief Fill D3D Presentation parameters
+ */
+static void fill_d3d_presentparams(d3d_priv *priv,
+ D3DPRESENT_PARAMETERS *present_params)
+{
+ /* Prepare Direct3D initialization parameters. */
+ memset(present_params, 0, sizeof(D3DPRESENT_PARAMETERS));
+ present_params->Windowed = TRUE;
+ present_params->SwapEffect =
+ priv->opt_swap_discard ? D3DSWAPEFFECT_DISCARD : D3DSWAPEFFECT_COPY;
+ present_params->Flags = D3DPRESENTFLAG_VIDEO;
+ present_params->hDeviceWindow = vo_w32_hwnd(priv->vo);
+ present_params->BackBufferWidth = priv->cur_backbuf_width;
+ present_params->BackBufferHeight = priv->cur_backbuf_height;
+ present_params->MultiSampleType = D3DMULTISAMPLE_NONE;
+ present_params->PresentationInterval = D3DPRESENT_INTERVAL_ONE;
+ present_params->BackBufferFormat = priv->desktop_fmt;
+ present_params->BackBufferCount = 1;
+ present_params->EnableAutoDepthStencil = FALSE;
+}
+
+
+// Create a new backbuffer. Create or Reset the D3D device.
+static bool change_d3d_backbuffer(d3d_priv *priv)
+{
+ int window_w = priv->vo->dwidth;
+ int window_h = priv->vo->dheight;
+
+ /* Grow the backbuffer in the required dimension. */
+ if (window_w > priv->cur_backbuf_width)
+ priv->cur_backbuf_width = window_w;
+
+ if (window_h > priv->cur_backbuf_height)
+ priv->cur_backbuf_height = window_h;
+
+ if (priv->opt_exact_backbuffer) {
+ priv->cur_backbuf_width = window_w;
+ priv->cur_backbuf_height = window_h;
+ }
+
+ /* The grown backbuffer dimensions are ready and fill_d3d_presentparams
+ * will use them, so we can reset the device.
+ */
+ D3DPRESENT_PARAMETERS present_params;
+ fill_d3d_presentparams(priv, &present_params);
+
+ if (!priv->d3d_device) {
+ if (FAILED(IDirect3D9_CreateDevice(priv->d3d_handle,
+ D3DADAPTER_DEFAULT,
+ DEVTYPE, vo_w32_hwnd(priv->vo),
+ D3DCREATE_SOFTWARE_VERTEXPROCESSING
+ | D3DCREATE_FPU_PRESERVE
+ | D3DCREATE_MULTITHREADED,
+ &present_params, &priv->d3d_device)))
+ {
+ MP_VERBOSE(priv, "Creating Direct3D device failed.\n");
+ return 0;
+ }
+ } else {
+ if (FAILED(IDirect3DDevice9_Reset(priv->d3d_device, &present_params))) {
+ MP_ERR(priv, "Resetting Direct3D device failed.\n");
+ return 0;
+ }
+ }
+
+ MP_VERBOSE(priv, "New backbuffer (%dx%d), VO (%dx%d)\n",
+ present_params.BackBufferWidth, present_params.BackBufferHeight,
+ window_w, window_h);
+
+ return 1;
+}
+
+static void destroy_d3d(d3d_priv *priv)
+{
+ destroy_d3d_surfaces(priv);
+
+ if (priv->d3d_device)
+ IDirect3DDevice9_Release(priv->d3d_device);
+ priv->d3d_device = NULL;
+
+ if (priv->d3d_handle) {
+ MP_VERBOSE(priv, "Stopping Direct3D.\n");
+ IDirect3D9_Release(priv->d3d_handle);
+ }
+ priv->d3d_handle = NULL;
+}
+
+/** @brief Reconfigure the whole Direct3D. Called only
+ * when the video adapter becomes uncooperative. ("Lost" devices)
+ * @return 1 on success, 0 on failure
+ */
+static int reconfigure_d3d(d3d_priv *priv)
+{
+ MP_VERBOSE(priv, "reconfigure_d3d called.\n");
+
+ // Force complete destruction of the D3D state.
+ // Note: this step could be omitted. The resize_d3d call below would detect
+ // that d3d_device is NULL, and would properly recreate it. I'm not sure why
+ // the following code to release and recreate the d3d_handle exists.
+ destroy_d3d(priv);
+ if (!init_d3d(priv))
+ return 0;
+
+ // Proper re-initialization.
+ if (!resize_d3d(priv))
+ return 0;
+
+ return 1;
+}
+
+// Resize Direct3D context on window resize.
+// This function also is called when major initializations need to be done.
+static bool resize_d3d(d3d_priv *priv)
+{
+ D3DVIEWPORT9 vp = {0, 0, priv->vo->dwidth, priv->vo->dheight, 0, 1};
+
+ MP_VERBOSE(priv, "resize_d3d %dx%d called.\n",
+ priv->vo->dwidth, priv->vo->dheight);
+
+ /* Make sure that backbuffer is large enough to accommodate the new
+ viewport dimensions. Grow it if necessary. */
+
+ bool backbuf_resize = priv->vo->dwidth > priv->cur_backbuf_width ||
+ priv->vo->dheight > priv->cur_backbuf_height;
+
+ if (priv->opt_exact_backbuffer) {
+ backbuf_resize = priv->vo->dwidth != priv->cur_backbuf_width ||
+ priv->vo->dheight != priv->cur_backbuf_height;
+ }
+
+ if (backbuf_resize || !priv->d3d_device)
+ {
+ destroy_d3d_surfaces(priv);
+ if (!change_d3d_backbuffer(priv))
+ return 0;
+ }
+
+ if (!priv->d3d_device || !priv->image_format)
+ return 1;
+
+ if (!create_d3d_surfaces(priv))
+ return 0;
+
+ if (FAILED(IDirect3DDevice9_SetViewport(priv->d3d_device, &vp))) {
+ MP_ERR(priv, "Setting viewport failed.\n");
+ return 0;
+ }
+
+ // so that screen coordinates map to D3D ones
+ D3DMATRIX view;
+ d3d_matrix_ortho(&view, 0.5f, vp.Width + 0.5f, vp.Height + 0.5f, 0.5f);
+ IDirect3DDevice9_SetTransform(priv->d3d_device, D3DTS_VIEW, &view);
+
+ calc_fs_rect(priv);
+ priv->vo->want_redraw = true;
+
+ return 1;
+}
+
+/** @brief Uninitialize Direct3D and close the window.
+ */
+static void uninit_d3d(d3d_priv *priv)
+{
+ MP_VERBOSE(priv, "uninit_d3d called.\n");
+
+ destroy_d3d(priv);
+}
+
+static uint32_t d3d_draw_frame(d3d_priv *priv)
+{
+ if (!priv->d3d_device)
+ return VO_TRUE;
+
+ if (!d3d_begin_scene(priv))
+ return VO_ERROR;
+
+ IDirect3DDevice9_Clear(priv->d3d_device, 0, NULL, D3DCLEAR_TARGET, 0, 0, 0);
+
+ if (!priv->have_image)
+ goto render_osd;
+
+ RECT rm = priv->fs_movie_rect;
+ RECT rs = priv->fs_panscan_rect;
+
+ rs.left &= ~(ULONG)1;
+ rs.top &= ~(ULONG)1;
+ rs.right &= ~(ULONG)1;
+ rs.bottom &= ~(ULONG)1;
+ if (FAILED(IDirect3DDevice9_StretchRect(priv->d3d_device,
+ priv->d3d_surface,
+ &rs,
+ priv->d3d_backbuf,
+ &rm,
+ D3DTEXF_LINEAR))) {
+ MP_ERR(priv, "Copying frame to the backbuffer failed.\n");
+ return VO_ERROR;
+ }
+
+render_osd:
+
+ draw_osd(priv->vo);
+
+ return VO_TRUE;
+}
+
+static D3DFORMAT check_format(d3d_priv *priv, uint32_t movie_fmt)
+{
+ const struct fmt_entry *cur = &fmt_table[0];
+
+ while (cur->mplayer_fmt) {
+ if (cur->mplayer_fmt == movie_fmt) {
+ HRESULT res;
+ /* Test conversion from Movie colorspace to
+ * display's target colorspace. */
+ res = IDirect3D9_CheckDeviceFormatConversion(priv->d3d_handle,
+ D3DADAPTER_DEFAULT,
+ DEVTYPE,
+ cur->fourcc,
+ priv->desktop_fmt);
+ if (FAILED(res)) {
+ MP_VERBOSE(priv, "Rejected image format: %s\n",
+ vo_format_name(cur->mplayer_fmt));
+ return 0;
+ }
+
+ MP_DBG(priv, "Accepted image format: %s\n",
+ vo_format_name(cur->mplayer_fmt));
+
+ return cur->fourcc;
+ }
+ cur++;
+ }
+
+ return 0;
+}
+
+// Return if the image format can be used. If it can, decide which rendering
+// and conversion mode to use.
+// If initialize is true, actually setup all variables to use the picked
+// rendering mode.
+static bool init_rendering_mode(d3d_priv *priv, uint32_t fmt, bool initialize)
+{
+ int blit_d3dfmt = check_format(priv, fmt);
+
+ if (!blit_d3dfmt)
+ return false;
+
+ MP_VERBOSE(priv, "Accepted rendering methods for "
+ "format='%s': StretchRect=%#x.\n",
+ vo_format_name(fmt), blit_d3dfmt);
+
+ if (!initialize)
+ return true;
+
+ // initialization doesn't fail beyond this point
+
+ priv->movie_src_fmt = 0;
+ priv->image_format = fmt;
+
+ priv->movie_src_fmt = blit_d3dfmt;
+
+ return true;
+}
+
+/** @brief Query if movie colorspace is supported by the HW.
+ * @return 0 on failure, device capabilities (not probed
+ * currently) on success.
+ */
+static int query_format(struct vo *vo, int movie_fmt)
+{
+ d3d_priv *priv = vo->priv;
+ if (!init_rendering_mode(priv, movie_fmt, false))
+ return 0;
+
+ return 1;
+}
+
+/****************************************************************************
+ * *
+ * *
+ * *
+ * libvo Control / Callback functions *
+ * *
+ * *
+ * *
+ ****************************************************************************/
+
+
+/** @brief libvo Callback: Preinitialize the video card.
+ * Preinit the hardware just enough to be queried about
+ * supported formats.
+ *
+ * @return 0 on success, -1 on failure
+ */
+
+static int preinit(struct vo *vo)
+{
+ d3d_priv *priv = vo->priv;
+ priv->vo = vo;
+ priv->log = vo->log;
+
+ priv->d3d9_dll = LoadLibraryA("d3d9.dll");
+ if (!priv->d3d9_dll) {
+ MP_ERR(priv, "Unable to dynamically load d3d9.dll\n");
+ goto err_out;
+ }
+
+ priv->pDirect3DCreate9 = (void *)GetProcAddress(priv->d3d9_dll,
+ "Direct3DCreate9");
+ if (!priv->pDirect3DCreate9) {
+ MP_ERR(priv, "Unable to find entry point of Direct3DCreate9\n");
+ goto err_out;
+ }
+
+ /* w32_common framework call. Configures window on the screen, gets
+ * fullscreen dimensions and does other useful stuff.
+ */
+ if (!vo_w32_init(vo)) {
+ MP_VERBOSE(priv, "Configuring onscreen window failed.\n");
+ goto err_out;
+ }
+
+ if (!init_d3d(priv))
+ goto err_out;
+
+ return 0;
+
+err_out:
+ uninit(vo);
+ return -1;
+}
+
+/** @brief libvo Callback: Handle control requests.
+ * @return VO_TRUE on success, VO_NOTIMPL when not implemented
+ */
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ d3d_priv *priv = vo->priv;
+
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ calc_fs_rect(priv);
+ priv->vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_SCREENSHOT_WIN:
+ *(struct mp_image **)data = get_window_screenshot(priv);
+ return VO_TRUE;
+ }
+
+ int events = 0;
+ int r = vo_w32_control(vo, &events, request, data);
+
+ if (events & VO_EVENT_RESIZE)
+ resize_d3d(priv);
+
+ if (events & VO_EVENT_EXPOSE)
+ vo->want_redraw = true;
+
+ vo_event(vo, events);
+
+ return r;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ d3d_priv *priv = vo->priv;
+
+ priv->have_image = false;
+
+ vo_w32_config(vo);
+
+ if ((priv->image_format != params->imgfmt)
+ || (priv->src_width != params->w)
+ || (priv->src_height != params->h))
+ {
+ d3d_destroy_video_objects(priv);
+
+ priv->src_width = params->w;
+ priv->src_height = params->h;
+ priv->params = *params;
+ init_rendering_mode(priv, params->imgfmt, true);
+ }
+
+ if (!resize_d3d(priv))
+ return VO_ERROR;
+
+ return 0; /* Success */
+}
+
+/** @brief libvo Callback: Flip next already drawn frame on the
+ * screen.
+ */
+static void flip_page(struct vo *vo)
+{
+ d3d_priv *priv = vo->priv;
+
+ if (priv->d3d_device && priv->d3d_in_scene) {
+ if (FAILED(IDirect3DDevice9_EndScene(priv->d3d_device))) {
+ MP_ERR(priv, "EndScene failed.\n");
+ }
+ }
+ priv->d3d_in_scene = false;
+
+ RECT rect = {0, 0, vo->dwidth, vo->dheight};
+ if (!priv->d3d_device ||
+ FAILED(IDirect3DDevice9_Present(priv->d3d_device, &rect, 0, 0, 0))) {
+ MP_VERBOSE(priv, "Trying to reinitialize uncooperative video adapter.\n");
+ if (!reconfigure_d3d(priv)) {
+ MP_VERBOSE(priv, "Reinitialization failed.\n");
+ return;
+ } else {
+ MP_VERBOSE(priv, "Video adapter reinitialized.\n");
+ }
+ }
+}
+
+/** @brief libvo Callback: Uninitializes all pointers and closes
+ * all D3D related stuff,
+ */
+static void uninit(struct vo *vo)
+{
+ d3d_priv *priv = vo->priv;
+
+ MP_VERBOSE(priv, "uninit called.\n");
+
+ uninit_d3d(priv);
+ vo_w32_uninit(vo);
+ if (priv->d3d9_dll)
+ FreeLibrary(priv->d3d9_dll);
+ priv->d3d9_dll = NULL;
+}
+
+// Lock buffers and fill out to point to them.
+// Must call d3d_unlock_video_objects() to unlock the buffers again.
+static bool get_video_buffer(d3d_priv *priv, struct mp_image *out)
+{
+ *out = (struct mp_image) {0};
+ mp_image_set_size(out, priv->src_width, priv->src_height);
+ mp_image_setfmt(out, priv->image_format);
+
+ if (!priv->d3d_device)
+ return false;
+
+ if (!priv->locked_rect.pBits) {
+ if (FAILED(IDirect3DSurface9_LockRect(priv->d3d_surface,
+ &priv->locked_rect, NULL, 0)))
+ {
+ MP_ERR(priv, "Surface lock failed.\n");
+ return false;
+ }
+ }
+
+ uint8_t *base = priv->locked_rect.pBits;
+ size_t stride = priv->locked_rect.Pitch;
+
+ out->planes[0] = base;
+ out->stride[0] = stride;
+
+ if (out->num_planes == 2) {
+ // NV12, NV21
+ out->planes[1] = base + stride * out->h;
+ out->stride[1] = stride;
+ }
+
+ if (out->num_planes == 3) {
+ bool swap = priv->movie_src_fmt == MAKEFOURCC('Y','V','1','2');
+
+ size_t uv_stride = stride / 2;
+ uint8_t *u = base + out->h * stride;
+ uint8_t *v = u + (out->h / 2) * uv_stride;
+
+ out->planes[1] = swap ? v : u;
+ out->planes[2] = swap ? u : v;
+
+ out->stride[1] = out->stride[2] = uv_stride;
+ }
+
+ return true;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ d3d_priv *priv = vo->priv;
+ if (!priv->d3d_device)
+ return;
+
+ struct mp_image buffer;
+ if (!get_video_buffer(priv, &buffer))
+ return;
+
+ if (!frame->current)
+ return;
+
+ mp_image_copy(&buffer, frame->current);
+
+ d3d_unlock_video_objects(priv);
+
+ priv->have_image = true;
+ priv->osd_pts = frame->current->pts;
+
+ d3d_draw_frame(priv);
+}
+
+static mp_image_t *get_window_screenshot(d3d_priv *priv)
+{
+ D3DDISPLAYMODE mode;
+ mp_image_t *image = NULL;
+ RECT window_rc;
+ RECT screen_rc;
+ RECT visible;
+ POINT pt;
+ D3DLOCKED_RECT locked_rect;
+ int width, height;
+ IDirect3DSurface9 *surface = NULL;
+
+ if (FAILED(IDirect3DDevice9_GetDisplayMode(priv->d3d_device, 0, &mode))) {
+ MP_ERR(priv, "GetDisplayMode failed.\n");
+ goto error_exit;
+ }
+
+ if (FAILED(IDirect3DDevice9_CreateOffscreenPlainSurface(priv->d3d_device,
+ mode.Width, mode.Height, D3DFMT_A8R8G8B8, D3DPOOL_SYSTEMMEM, &surface,
+ NULL)))
+ {
+ MP_ERR(priv, "Couldn't create surface.\n");
+ goto error_exit;
+ }
+
+ if (FAILED(IDirect3DDevice9_GetFrontBufferData(priv->d3d_device, 0,
+ surface)))
+ {
+ MP_ERR(priv, "Couldn't copy frontbuffer.\n");
+ goto error_exit;
+ }
+
+ GetClientRect(vo_w32_hwnd(priv->vo), &window_rc);
+ pt = (POINT) { 0, 0 };
+ ClientToScreen(vo_w32_hwnd(priv->vo), &pt);
+ window_rc.left = pt.x;
+ window_rc.top = pt.y;
+ window_rc.right += window_rc.left;
+ window_rc.bottom += window_rc.top;
+
+ screen_rc = (RECT) { 0, 0, mode.Width, mode.Height };
+
+ if (!IntersectRect(&visible, &screen_rc, &window_rc))
+ goto error_exit;
+ width = visible.right - visible.left;
+ height = visible.bottom - visible.top;
+ if (width < 1 || height < 1)
+ goto error_exit;
+
+ image = mp_image_alloc(IMGFMT_BGR0, width, height);
+ if (!image)
+ goto error_exit;
+
+ IDirect3DSurface9_LockRect(surface, &locked_rect, NULL, 0);
+
+ memcpy_pic(image->planes[0], (char*)locked_rect.pBits + visible.top *
+ locked_rect.Pitch + visible.left * 4, width * 4, height,
+ image->stride[0], locked_rect.Pitch);
+
+ IDirect3DSurface9_UnlockRect(surface);
+ IDirect3DSurface9_Release(surface);
+
+ return image;
+
+error_exit:
+ talloc_free(image);
+ if (surface)
+ IDirect3DSurface9_Release(surface);
+ return NULL;
+}
+
+static void update_osd(d3d_priv *priv)
+{
+ if (!priv->osd_cache)
+ priv->osd_cache = mp_draw_sub_alloc(priv, priv->vo->global);
+
+ struct sub_bitmap_list *sbs = osd_render(priv->vo->osd, priv->osd_res,
+ priv->osd_pts, 0, mp_draw_sub_formats);
+
+ struct mp_rect act_rc[MAX_OSD_RECTS], mod_rc[64];
+ int num_act_rc = 0, num_mod_rc = 0;
+
+ struct mp_image *osd = mp_draw_sub_overlay(priv->osd_cache, sbs,
+ act_rc, MP_ARRAY_SIZE(act_rc), &num_act_rc,
+ mod_rc, MP_ARRAY_SIZE(mod_rc), &num_mod_rc);
+
+ talloc_free(sbs);
+
+ if (!osd) {
+ MP_ERR(priv, "Failed to render OSD.\n");
+ return;
+ }
+
+ if (!num_mod_rc && priv->osd_texture.system)
+ return; // nothing changed
+
+ priv->osd_num_vertices = 0;
+
+ if (osd->w > priv->osd_texture.tex_w || osd->h > priv->osd_texture.tex_h) {
+ int new_w = osd->w;
+ int new_h = osd->h;
+ d3d_fix_texture_size(priv, &new_w, &new_h);
+
+ MP_DBG(priv, "reallocate OSD surface to %dx%d.\n", new_w, new_h);
+
+ d3dtex_release(priv, &priv->osd_texture);
+ if (!d3dtex_allocate(priv, &priv->osd_texture, D3DFMT_A8R8G8B8,
+ new_w, new_h))
+ return;
+ }
+
+ // Lazy; could/should use the bounding rect, or perform multiple lock calls.
+ // The previous approach (fully packed texture) was more efficient.
+ RECT dirty_rc = { 0, 0, priv->osd_texture.w, priv->osd_texture.h };
+
+ D3DLOCKED_RECT locked_rect;
+
+ if (FAILED(IDirect3DTexture9_LockRect(priv->osd_texture.system, 0, &locked_rect,
+ &dirty_rc, 0)))
+ {
+ MP_ERR(priv, "OSD texture lock failed.\n");
+ return;
+ }
+
+ for (int n = 0; n < num_mod_rc; n++) {
+ struct mp_rect rc = mod_rc[n];
+ int w = mp_rect_w(rc);
+ int h = mp_rect_h(rc);
+ void *src = mp_image_pixel_ptr(osd, 0, rc.x0, rc.y0);
+ void *dst = (char *)locked_rect.pBits + locked_rect.Pitch * rc.y0 +
+ rc.x0 * 4;
+ memcpy_pic(dst, src, w * 4, h, locked_rect.Pitch, osd->stride[0]);
+ }
+
+ if (FAILED(IDirect3DTexture9_UnlockRect(priv->osd_texture.system, 0))) {
+ MP_ERR(priv, "OSD texture unlock failed.\n");
+ return;
+ }
+
+ if (!d3dtex_update(priv, &priv->osd_texture))
+ return;
+
+ // We need 2 primitives per quad which makes 6 vertices.
+ priv->osd_num_vertices = num_act_rc * 6;
+
+ float tex_w = priv->osd_texture.tex_w;
+ float tex_h = priv->osd_texture.tex_h;
+
+ for (int n = 0; n < num_act_rc; n++) {
+ struct mp_rect rc = act_rc[n];
+
+ float tx0 = rc.x0 / tex_w;
+ float ty0 = rc.y0 / tex_h;
+ float tx1 = rc.x1 / tex_w;
+ float ty1 = rc.y1 / tex_h;
+
+ vertex_osd *v = &priv->osd_vertices[n * 6];
+ v[0] = (vertex_osd) { rc.x0, rc.y0, 0, tx0, ty0 };
+ v[1] = (vertex_osd) { rc.x1, rc.y0, 0, tx1, ty0 };
+ v[2] = (vertex_osd) { rc.x0, rc.y1, 0, tx0, ty1 };
+ v[3] = (vertex_osd) { rc.x1, rc.y1, 0, tx1, ty1 };
+ v[4] = v[2];
+ v[5] = v[1];
+ }
+}
+
+static void draw_osd(struct vo *vo)
+{
+ d3d_priv *priv = vo->priv;
+ if (!priv->d3d_device)
+ return;
+
+ update_osd(priv);
+
+ if (!priv->osd_num_vertices)
+ return;
+
+ d3d_begin_scene(priv);
+
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_ALPHABLENDENABLE, TRUE);
+
+ IDirect3DDevice9_SetTexture(priv->d3d_device, 0,
+ d3dtex_get_render_texture(priv, &priv->osd_texture));
+
+ IDirect3DDevice9_SetRenderState(priv->d3d_device, D3DRS_SRCBLEND,
+ D3DBLEND_ONE);
+
+ IDirect3DDevice9_SetFVF(priv->d3d_device, D3DFVF_OSD_VERTEX);
+ IDirect3DDevice9_DrawPrimitiveUP(priv->d3d_device, D3DPT_TRIANGLELIST,
+ priv->osd_num_vertices / 3,
+ priv->osd_vertices, sizeof(vertex_osd));
+
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
+
+ IDirect3DDevice9_SetTexture(priv->d3d_device, 0, NULL);
+
+ IDirect3DDevice9_SetRenderState(priv->d3d_device,
+ D3DRS_ALPHABLENDENABLE, FALSE);
+}
+
+#define OPT_BASE_STRUCT d3d_priv
+
+static const struct m_option opts[] = {
+ {"force-power-of-2", OPT_BOOL(opt_force_power_of_2)},
+ {"disable-texture-align", OPT_BOOL(opt_disable_texture_align)},
+ {"texture-memory", OPT_CHOICE(opt_texture_memory,
+ {"default", 0},
+ {"managed", 1},
+ {"default-pool", 2},
+ {"default-pool-shadow", 3},
+ {"scratch", 4})},
+ {"swap-discard", OPT_BOOL(opt_swap_discard)},
+ {"exact-backbuffer", OPT_BOOL(opt_exact_backbuffer)},
+ {0}
+};
+
+const struct vo_driver video_out_direct3d = {
+ .description = "Direct3D 9 Renderer",
+ .name = "direct3d",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(d3d_priv),
+ .options = opts,
+ .options_prefix = "vo-direct3d",
+};
diff --git a/video/out/vo_dmabuf_wayland.c b/video/out/vo_dmabuf_wayland.c
new file mode 100644
index 0000000..e04ff5d
--- /dev/null
+++ b/video/out/vo_dmabuf_wayland.c
@@ -0,0 +1,872 @@
+/*
+ * 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 <libavutil/hwcontext_drm.h>
+#include <sys/mman.h>
+#include <unistd.h>
+#include "config.h"
+
+#if HAVE_VAAPI
+#include <va/va_drmcommon.h>
+#endif
+
+#include "common/global.h"
+#include "gpu/hwdec.h"
+#include "gpu/video.h"
+#include "mpv_talloc.h"
+#include "present_sync.h"
+#include "sub/draw_bmp.h"
+#include "video/fmt-conversion.h"
+#include "video/mp_image.h"
+#include "vo.h"
+#include "wayland_common.h"
+#include "wldmabuf/ra_wldmabuf.h"
+
+#if HAVE_VAAPI
+#include "video/vaapi.h"
+#endif
+
+// Generated from wayland-protocols
+#include "linux-dmabuf-unstable-v1.h"
+#include "viewporter.h"
+
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+#include "single-pixel-buffer-v1.h"
+#endif
+
+// We need at least enough buffers to avoid a
+// flickering artifact in certain formats.
+#define WL_BUFFERS_WANTED 15
+
+enum hwdec_type {
+ HWDEC_NONE,
+ HWDEC_VAAPI,
+ HWDEC_DRMPRIME,
+};
+
+struct buffer {
+ struct vo *vo;
+ struct wl_buffer *buffer;
+ struct wl_list link;
+ struct vo_frame *frame;
+
+ uint32_t drm_format;
+ uintptr_t id;
+};
+
+struct osd_buffer {
+ struct vo *vo;
+ struct wl_buffer *buffer;
+ struct wl_list link;
+ struct mp_image image;
+ size_t size;
+};
+
+struct priv {
+ struct mp_log *log;
+ struct mp_rect src;
+ struct mpv_global *global;
+
+ struct ra_ctx *ctx;
+ struct ra_hwdec_ctx hwdec_ctx;
+
+ struct wl_shm_pool *solid_buffer_pool;
+ struct wl_buffer *solid_buffer;
+ struct wl_list buffer_list;
+ struct wl_list osd_buffer_list;
+
+ struct wl_shm_pool *osd_shm_pool;
+ uint8_t *osd_shm_data;
+ int osd_shm_width;
+ int osd_shm_stride;
+ int osd_shm_height;
+ bool osd_surface_is_mapped;
+ bool osd_surface_has_contents;
+
+ struct osd_buffer *osd_buffer;
+ struct mp_draw_sub_cache *osd_cache;
+ struct mp_osd_res screen_osd_res;
+
+ bool destroy_buffers;
+ bool force_window;
+ enum hwdec_type hwdec_type;
+ uint32_t drm_format;
+ uint64_t drm_modifier;
+};
+
+static void buffer_handle_release(void *data, struct wl_buffer *wl_buffer)
+{
+ struct buffer *buf = data;
+ if (buf->frame) {
+ talloc_free(buf->frame);
+ buf->frame = NULL;
+ }
+}
+
+static const struct wl_buffer_listener buffer_listener = {
+ buffer_handle_release,
+};
+
+static void osd_buffer_handle_release(void *data, struct wl_buffer *wl_buffer)
+{
+ struct osd_buffer *osd_buf = data;
+ wl_list_remove(&osd_buf->link);
+ if (osd_buf->buffer) {
+ wl_buffer_destroy(osd_buf->buffer);
+ osd_buf->buffer = NULL;
+ }
+ talloc_free(osd_buf);
+}
+
+static const struct wl_buffer_listener osd_buffer_listener = {
+ osd_buffer_handle_release,
+};
+
+#if HAVE_VAAPI
+static void close_file_descriptors(VADRMPRIMESurfaceDescriptor desc)
+{
+ for (int i = 0; i < desc.num_objects; i++)
+ close(desc.objects[i].fd);
+}
+#endif
+
+static uintptr_t vaapi_surface_id(struct mp_image *src)
+{
+ uintptr_t id = 0;
+#if HAVE_VAAPI
+ id = (uintptr_t)va_surface_id(src);
+#endif
+ return id;
+}
+
+static bool vaapi_drm_format(struct vo *vo, struct mp_image *src)
+{
+ bool format = false;
+#if HAVE_VAAPI
+ struct priv *p = vo->priv;
+ VADRMPRIMESurfaceDescriptor desc = {0};
+
+ uintptr_t id = vaapi_surface_id(src);
+ VADisplay display = ra_get_native_resource(p->ctx->ra, "VADisplay");
+ VAStatus status = vaExportSurfaceHandle(display, id, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
+ VA_EXPORT_SURFACE_COMPOSED_LAYERS | VA_EXPORT_SURFACE_READ_ONLY, &desc);
+
+ if (!CHECK_VA_STATUS(vo, "vaExportSurfaceHandle()")) {
+ /* invalid surface warning => composed layers not supported */
+ if (status == VA_STATUS_ERROR_INVALID_SURFACE)
+ MP_VERBOSE(vo, "vaExportSurfaceHandle: composed layers not supported.\n");
+ goto done;
+ }
+ p->drm_format = desc.layers[0].drm_format;
+ p->drm_modifier = desc.objects[0].drm_format_modifier;
+ format = true;
+done:
+ close_file_descriptors(desc);
+#endif
+ return format;
+}
+
+static void vaapi_dmabuf_importer(struct buffer *buf, struct mp_image *src,
+ struct zwp_linux_buffer_params_v1 *params)
+{
+#if HAVE_VAAPI
+ struct vo *vo = buf->vo;
+ struct priv *p = vo->priv;
+ VADRMPRIMESurfaceDescriptor desc = {0};
+ VADisplay display = ra_get_native_resource(p->ctx->ra, "VADisplay");
+
+ /* composed has single layer */
+ int layer_no = 0;
+ buf->id = vaapi_surface_id(src);
+ VAStatus status = vaExportSurfaceHandle(display, buf->id, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
+ VA_EXPORT_SURFACE_COMPOSED_LAYERS | VA_EXPORT_SURFACE_READ_ONLY, &desc);
+
+ if (!CHECK_VA_STATUS(vo, "vaExportSurfaceHandle()")) {
+ /* invalid surface warning => composed layers not supported */
+ if (status == VA_STATUS_ERROR_INVALID_SURFACE)
+ MP_VERBOSE(vo, "vaExportSurfaceHandle: composed layers not supported.\n");
+ goto done;
+ }
+ buf->drm_format = desc.layers[layer_no].drm_format;
+ if (!ra_compatible_format(p->ctx->ra, buf->drm_format, desc.objects[0].drm_format_modifier)) {
+ MP_VERBOSE(vo, "%s(%016lx) is not supported.\n",
+ mp_tag_str(buf->drm_format), desc.objects[0].drm_format_modifier);
+ buf->drm_format = 0;
+ goto done;
+ }
+ for (int plane_no = 0; plane_no < desc.layers[layer_no].num_planes; ++plane_no) {
+ int object = desc.layers[layer_no].object_index[plane_no];
+ uint64_t modifier = desc.objects[object].drm_format_modifier;
+ zwp_linux_buffer_params_v1_add(params, desc.objects[object].fd, plane_no, desc.layers[layer_no].offset[plane_no],
+ desc.layers[layer_no].pitch[plane_no], modifier >> 32, modifier & 0xffffffff);
+ }
+
+done:
+ close_file_descriptors(desc);
+#endif
+}
+
+static uintptr_t drmprime_surface_id(struct mp_image *src)
+{
+ uintptr_t id = 0;
+ struct AVDRMFrameDescriptor *desc = (AVDRMFrameDescriptor *)src->planes[0];
+
+ AVDRMObjectDescriptor object = desc->objects[0];
+ id = (uintptr_t)object.fd;
+ return id;
+}
+
+static bool drmprime_drm_format(struct vo *vo, struct mp_image *src)
+{
+ struct priv *p = vo->priv;
+ struct AVDRMFrameDescriptor *desc = (AVDRMFrameDescriptor *)src->planes[0];
+ if (!desc)
+ return false;
+
+ // Just check the very first layer/plane.
+ p->drm_format = desc->layers[0].format;
+ int object_index = desc->layers[0].planes[0].object_index;
+ p->drm_modifier = desc->objects[object_index].format_modifier;
+ return true;
+}
+
+static void drmprime_dmabuf_importer(struct buffer *buf, struct mp_image *src,
+ struct zwp_linux_buffer_params_v1 *params)
+{
+ int layer_no, plane_no;
+ int max_planes = 0;
+ const AVDRMFrameDescriptor *desc = (AVDRMFrameDescriptor *)src->planes[0];
+ if (!desc)
+ return;
+
+ buf->id = drmprime_surface_id(src);
+ for (layer_no = 0; layer_no < desc->nb_layers; layer_no++) {
+ AVDRMLayerDescriptor layer = desc->layers[layer_no];
+
+ buf->drm_format = layer.format;
+ max_planes = MPMAX(max_planes, layer.nb_planes);
+ for (plane_no = 0; plane_no < layer.nb_planes; ++plane_no) {
+ AVDRMPlaneDescriptor plane = layer.planes[plane_no];
+ int object_index = plane.object_index;
+ AVDRMObjectDescriptor object = desc->objects[object_index];
+ uint64_t modifier = object.format_modifier;
+
+ zwp_linux_buffer_params_v1_add(params, object.fd, plane_no, plane.offset,
+ plane.pitch, modifier >> 32, modifier & 0xffffffff);
+ }
+ }
+}
+
+static intptr_t surface_id(struct vo *vo, struct mp_image *src)
+{
+ struct priv *p = vo->priv;
+ switch(p->hwdec_type) {
+ case HWDEC_VAAPI:
+ return vaapi_surface_id(src);
+ case HWDEC_DRMPRIME:
+ return drmprime_surface_id(src);
+ default:
+ return 0;
+ }
+}
+
+static bool drm_format_check(struct vo *vo, struct mp_image *src)
+{
+ struct priv *p = vo->priv;
+ switch(p->hwdec_type) {
+ case HWDEC_VAAPI:
+ return vaapi_drm_format(vo, src);
+ case HWDEC_DRMPRIME:
+ return drmprime_drm_format(vo, src);
+ }
+ return false;
+}
+
+static struct buffer *buffer_check(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+
+ /* Make more buffers if we're not at the desired amount yet. */
+ if (wl_list_length(&p->buffer_list) < WL_BUFFERS_WANTED)
+ goto done;
+
+ uintptr_t id = surface_id(vo, frame->current);
+ struct buffer *buf;
+ wl_list_for_each(buf, &p->buffer_list, link) {
+ if (buf->id == id) {
+ if (buf->frame)
+ talloc_free(buf->frame);
+ buf->frame = frame;
+ return buf;
+ }
+ }
+
+done:
+ return NULL;
+}
+
+static struct buffer *buffer_create(struct vo *vo, struct vo_frame *frame)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ struct priv *p = vo->priv;
+
+ struct buffer *buf = talloc_zero(vo, struct buffer);
+ buf->vo = vo;
+ buf->frame = frame;
+
+ struct mp_image *image = buf->frame->current;
+ struct zwp_linux_buffer_params_v1 *params = zwp_linux_dmabuf_v1_create_params(wl->dmabuf);
+
+ switch(p->hwdec_type) {
+ case HWDEC_VAAPI:
+ vaapi_dmabuf_importer(buf, image, params);
+ break;
+ case HWDEC_DRMPRIME:
+ drmprime_dmabuf_importer(buf, image, params);
+ break;
+ }
+
+ if (!buf->drm_format) {
+ talloc_free(buf->frame);
+ talloc_free(buf);
+ zwp_linux_buffer_params_v1_destroy(params);
+ return NULL;
+ }
+
+ buf->buffer = zwp_linux_buffer_params_v1_create_immed(params, image->params.w, image->params.h,
+ buf->drm_format, 0);
+ zwp_linux_buffer_params_v1_destroy(params);
+ wl_buffer_add_listener(buf->buffer, &buffer_listener, buf);
+ wl_list_insert(&p->buffer_list, &buf->link);
+ return buf;
+}
+
+static struct buffer *buffer_get(struct vo *vo, struct vo_frame *frame)
+{
+ /* Reuse existing buffer if possible. */
+ struct buffer *buf = buffer_check(vo, frame);
+ if (buf) {
+ return buf;
+ } else {
+ return buffer_create(vo, frame);
+ }
+}
+
+static void destroy_buffers(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct buffer *buf, *tmp;
+ p->destroy_buffers = false;
+ wl_list_for_each_safe(buf, tmp, &p->buffer_list, link) {
+ wl_list_remove(&buf->link);
+ if (buf->frame) {
+ talloc_free(buf->frame);
+ buf->frame = NULL;
+ }
+ if (buf->buffer) {
+ wl_buffer_destroy(buf->buffer);
+ buf->buffer = NULL;
+ }
+ talloc_free(buf);
+ }
+}
+
+static void destroy_osd_buffers(struct vo *vo)
+{
+ if (!vo->wl)
+ return;
+
+ // Remove any existing buffer before we destroy them.
+ wl_surface_attach(vo->wl->osd_surface, NULL, 0, 0);
+ wl_surface_commit(vo->wl->osd_surface);
+
+ struct priv *p = vo->priv;
+ struct osd_buffer *osd_buf, *tmp;
+ wl_list_for_each_safe(osd_buf, tmp, &p->osd_buffer_list, link) {
+ wl_list_remove(&osd_buf->link);
+ munmap(osd_buf->image.planes[0], osd_buf->size);
+ if (osd_buf->buffer) {
+ wl_buffer_destroy(osd_buf->buffer);
+ osd_buf->buffer = NULL;
+ }
+ }
+}
+
+static struct osd_buffer *osd_buffer_check(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct osd_buffer *osd_buf;
+ wl_list_for_each(osd_buf, &p->osd_buffer_list, link) {
+ return osd_buf;
+ }
+ return NULL;
+}
+
+static struct osd_buffer *osd_buffer_create(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct osd_buffer *osd_buf = talloc_zero(vo, struct osd_buffer);
+
+ osd_buf->vo = vo;
+ osd_buf->size = p->osd_shm_height * p->osd_shm_stride;
+ mp_image_set_size(&osd_buf->image, p->osd_shm_width, p->osd_shm_height);
+ osd_buf->image.planes[0] = p->osd_shm_data;
+ osd_buf->image.stride[0] = p->osd_shm_stride;
+ osd_buf->buffer = wl_shm_pool_create_buffer(p->osd_shm_pool, 0,
+ p->osd_shm_width, p->osd_shm_height,
+ p->osd_shm_stride, WL_SHM_FORMAT_ARGB8888);
+
+ if (!osd_buf->buffer) {
+ talloc_free(osd_buf);
+ return NULL;
+ }
+
+ wl_list_insert(&p->osd_buffer_list, &osd_buf->link);
+ wl_buffer_add_listener(osd_buf->buffer, &osd_buffer_listener, osd_buf);
+ return osd_buf;
+}
+
+static struct osd_buffer *osd_buffer_get(struct vo *vo)
+{
+ struct osd_buffer *osd_buf = osd_buffer_check(vo);
+ if (osd_buf) {
+ return osd_buf;
+ } else {
+ return osd_buffer_create(vo);
+ }
+}
+
+static void create_shm_pool(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ struct priv *p = vo->priv;
+
+ int stride = MP_ALIGN_UP(vo->dwidth * 4, 16);
+ size_t size = vo->dheight * stride;
+ int fd = vo_wayland_allocate_memfd(vo, size);
+ if (fd < 0)
+ return;
+ uint8_t *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (data == MAP_FAILED)
+ goto error1;
+ struct wl_shm_pool *pool = wl_shm_create_pool(wl->shm, fd, size);
+ if (!pool)
+ goto error2;
+ close(fd);
+
+ destroy_osd_buffers(vo);
+
+ if (p->osd_shm_pool)
+ wl_shm_pool_destroy(p->osd_shm_pool);
+ p->osd_shm_pool = pool;
+ p->osd_shm_width = vo->dwidth;
+ p->osd_shm_height = vo->dheight;
+ p->osd_shm_stride = stride;
+ p->osd_shm_data = data;
+ return;
+
+error2:
+ munmap(data, size);
+error1:
+ close(fd);
+}
+
+static void set_viewport_source(struct vo *vo, struct mp_rect src)
+{
+ struct priv *p = vo->priv;
+ struct vo_wayland_state *wl = vo->wl;
+
+ if (p->force_window)
+ return;
+
+ if (wl->video_viewport && !mp_rect_equals(&p->src, &src)) {
+ wp_viewport_set_source(wl->video_viewport, src.x0 << 8,
+ src.y0 << 8, mp_rect_w(src) << 8,
+ mp_rect_h(src) << 8);
+ p->src = src;
+ }
+}
+
+static void resize(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ struct priv *p = vo->priv;
+
+ struct mp_rect src;
+ struct mp_rect dst;
+ struct mp_vo_opts *vo_opts = wl->vo_opts;
+
+ const int width = mp_rect_w(wl->geometry);
+ const int height = mp_rect_h(wl->geometry);
+
+ if (width == 0 || height == 0)
+ return;
+
+ vo_wayland_set_opaque_region(wl, false);
+ vo->dwidth = width;
+ vo->dheight = height;
+
+ create_shm_pool(vo);
+
+ // top level viewport is calculated with pan set to zero
+ vo->opts->pan_x = 0;
+ vo->opts->pan_y = 0;
+ vo_get_src_dst_rects(vo, &src, &dst, &p->screen_osd_res);
+ int window_w = p->screen_osd_res.ml + p->screen_osd_res.mr + mp_rect_w(dst);
+ int window_h = p->screen_osd_res.mt + p->screen_osd_res.mb + mp_rect_h(dst);
+ wp_viewport_set_destination(wl->viewport, window_w, window_h);
+
+ //now we restore pan for video viewport calculation
+ vo->opts->pan_x = vo_opts->pan_x;
+ vo->opts->pan_y = vo_opts->pan_y;
+ vo_get_src_dst_rects(vo, &src, &dst, &p->screen_osd_res);
+ wp_viewport_set_destination(wl->video_viewport, mp_rect_w(dst), mp_rect_h(dst));
+ wl_subsurface_set_position(wl->video_subsurface, dst.x0, dst.y0);
+ wp_viewport_set_destination(wl->osd_viewport, vo->dwidth, vo->dheight);
+ wl_subsurface_set_position(wl->osd_subsurface, 0 - dst.x0, 0 - dst.y0);
+ set_viewport_source(vo, src);
+}
+
+static bool draw_osd(struct vo *vo, struct mp_image *cur, double pts)
+{
+ struct priv *p = vo->priv;
+ struct mp_osd_res *res = &p->screen_osd_res;
+ bool draw = false;
+
+ struct sub_bitmap_list *sbs = osd_render(vo->osd, *res, pts, 0, mp_draw_sub_formats);
+
+ if (!sbs)
+ return draw;
+
+ struct mp_rect act_rc[1], mod_rc[64];
+ int num_act_rc = 0, num_mod_rc = 0;
+
+ if (!p->osd_cache)
+ p->osd_cache = mp_draw_sub_alloc(p, vo->global);
+
+ struct mp_image *osd = mp_draw_sub_overlay(p->osd_cache, sbs, act_rc,
+ MP_ARRAY_SIZE(act_rc), &num_act_rc,
+ mod_rc, MP_ARRAY_SIZE(mod_rc), &num_mod_rc);
+
+ p->osd_surface_has_contents = num_act_rc > 0;
+
+ if (!osd || !num_mod_rc)
+ goto done;
+
+ for (int n = 0; n < num_mod_rc; n++) {
+ struct mp_rect rc = mod_rc[n];
+
+ int rw = mp_rect_w(rc);
+ int rh = mp_rect_h(rc);
+
+ void *src = mp_image_pixel_ptr(osd, 0, rc.x0, rc.y0);
+ void *dst = cur->planes[0] + rc.x0 * 4 + rc.y0 * cur->stride[0];
+
+ memcpy_pic(dst, src, rw * 4, rh, cur->stride[0], osd->stride[0]);
+ }
+
+ draw = true;
+done:
+ talloc_free(sbs);
+ return draw;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+ struct vo_wayland_state *wl = vo->wl;
+ struct buffer *buf;
+ struct osd_buffer *osd_buf;
+ double pts;
+
+ if (!vo_wayland_check_visible(vo)) {
+ if (frame->current)
+ talloc_free(frame);
+ return;
+ }
+
+ if (p->destroy_buffers)
+ destroy_buffers(vo);
+
+ // Reuse the solid buffer so the osd can be visible
+ if (p->force_window) {
+ wl_surface_attach(wl->video_surface, p->solid_buffer, 0, 0);
+ wl_surface_damage_buffer(wl->video_surface, 0, 0, 1, 1);
+ }
+
+ pts = frame->current ? frame->current->pts : 0;
+ if (frame->current) {
+ buf = buffer_get(vo, frame);
+
+ if (buf && buf->frame) {
+ struct mp_image *image = buf->frame->current;
+ wl_surface_attach(wl->video_surface, buf->buffer, 0, 0);
+ wl_surface_damage_buffer(wl->video_surface, 0, 0, image->w,
+ image->h);
+
+ }
+ }
+
+ osd_buf = osd_buffer_get(vo);
+ if (osd_buf && osd_buf->buffer) {
+ if (draw_osd(vo, &osd_buf->image, pts) && p->osd_surface_has_contents) {
+ wl_surface_attach(wl->osd_surface, osd_buf->buffer, 0, 0);
+ wl_surface_damage_buffer(wl->osd_surface, 0, 0, osd_buf->image.w,
+ osd_buf->image.h);
+ p->osd_surface_is_mapped = true;
+ } else if (!p->osd_surface_has_contents && p->osd_surface_is_mapped) {
+ wl_surface_attach(wl->osd_surface, NULL, 0, 0);
+ p->osd_surface_is_mapped = false;
+ }
+ }
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+
+ wl_surface_commit(wl->video_surface);
+ wl_surface_commit(wl->osd_surface);
+ wl_surface_commit(wl->surface);
+
+ if (!wl->opts->disable_vsync)
+ vo_wayland_wait_frame(wl);
+
+ if (wl->use_present)
+ present_sync_swap(wl->present);
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ if (wl->use_present)
+ present_sync_get_info(wl->present, info);
+}
+
+static bool is_supported_fmt(int fmt)
+{
+ return (fmt == IMGFMT_DRMPRIME || fmt == IMGFMT_VAAPI);
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return is_supported_fmt(format);
+}
+
+static int reconfig(struct vo *vo, struct mp_image *img)
+{
+ struct priv *p = vo->priv;
+
+ if (img->params.force_window) {
+ p->force_window = true;
+ goto done;
+ }
+
+ if (!drm_format_check(vo, img)) {
+ MP_ERR(vo, "Unable to get drm format from hardware decoding!\n");
+ return VO_ERROR;
+ }
+
+ if (!ra_compatible_format(p->ctx->ra, p->drm_format, p->drm_modifier)) {
+ MP_ERR(vo, "Format '%s' with modifier '(%016lx)' is not supported by"
+ " the compositor.\n", mp_tag_str(p->drm_format), p->drm_modifier);
+ return VO_ERROR;
+ }
+
+ p->force_window = false;
+done:
+ if (!vo_wayland_reconfig(vo))
+ return VO_ERROR;
+
+ // mpv rotates clockwise but the wayland spec has counter-clockwise rotations
+ // swap 1 and 3 to match mpv's direction
+ int transform = (360 - img->params.rotate) % 360 / 90;
+ wl_surface_set_buffer_transform(vo->wl->video_surface, transform);
+
+ // Immediately destroy all buffers if params change.
+ destroy_buffers(vo);
+ return 0;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct priv *p = vo->priv;
+ int events = 0;
+ int ret;
+
+ switch (request) {
+ case VOCTRL_RESET:
+ p->destroy_buffers = true;
+ return VO_TRUE;
+ case VOCTRL_SET_PANSCAN:
+ resize(vo);
+ return VO_TRUE;
+ }
+
+ ret = vo_wayland_control(vo, &events, request, data);
+ if (events & VO_EVENT_RESIZE)
+ resize(vo);
+ if (events & VO_EVENT_EXPOSE)
+ vo->want_redraw = true;
+ vo_event(vo, events);
+
+ return ret;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ destroy_buffers(vo);
+ destroy_osd_buffers(vo);
+ if (p->osd_shm_pool)
+ wl_shm_pool_destroy(p->osd_shm_pool);
+ if (p->solid_buffer_pool)
+ wl_shm_pool_destroy(p->solid_buffer_pool);
+ if (p->solid_buffer)
+ wl_buffer_destroy(p->solid_buffer);
+ ra_hwdec_ctx_uninit(&p->hwdec_ctx);
+ if (vo->hwdec_devs) {
+ hwdec_devices_set_loader(vo->hwdec_devs, NULL, NULL);
+ hwdec_devices_destroy(vo->hwdec_devs);
+ }
+
+ vo_wayland_uninit(vo);
+ ra_ctx_destroy(&p->ctx);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ p->log = vo->log;
+ p->global = vo->global;
+ p->ctx = ra_ctx_create_by_name(vo, "wldmabuf");
+ wl_list_init(&p->buffer_list);
+ wl_list_init(&p->osd_buffer_list);
+ if (!p->ctx)
+ goto err;
+
+ assert(p->ctx->ra);
+
+ if (!vo->wl->dmabuf || !vo->wl->dmabuf_feedback) {
+ MP_FATAL(vo->wl, "Compositor doesn't support the %s (ver. 4) protocol!\n",
+ zwp_linux_dmabuf_v1_interface.name);
+ goto err;
+ }
+
+ if (!vo->wl->shm) {
+ MP_FATAL(vo->wl, "Compositor doesn't support the %s protocol!\n",
+ wl_shm_interface.name);
+ goto err;
+ }
+
+ if (!vo->wl->video_subsurface) {
+ MP_FATAL(vo->wl, "Compositor doesn't support the %s protocol!\n",
+ wl_subcompositor_interface.name);
+ goto err;
+ }
+
+ if (!vo->wl->viewport) {
+ MP_FATAL(vo->wl, "Compositor doesn't support the %s protocol!\n",
+ wp_viewporter_interface.name);
+ goto err;
+ }
+
+ if (vo->wl->single_pixel_manager) {
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ p->solid_buffer = wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer(
+ vo->wl->single_pixel_manager, 0, 0, 0, UINT32_MAX); /* R, G, B, A */
+#endif
+ } else {
+ int width = 1;
+ int height = 1;
+ int stride = MP_ALIGN_UP(width * 4, 16);
+ int fd = vo_wayland_allocate_memfd(vo, stride);
+ if (fd < 0)
+ goto err;
+ p->solid_buffer_pool = wl_shm_create_pool(vo->wl->shm, fd, height * stride);
+ close(fd);
+ if (!p->solid_buffer_pool)
+ goto err;
+ p->solid_buffer = wl_shm_pool_create_buffer(
+ p->solid_buffer_pool, 0, width, height, stride, WL_SHM_FORMAT_XRGB8888);
+ }
+ if (!p->solid_buffer)
+ goto err;
+
+ wl_surface_attach(vo->wl->surface, p->solid_buffer, 0, 0);
+
+ vo->hwdec_devs = hwdec_devices_create();
+ p->hwdec_ctx = (struct ra_hwdec_ctx) {
+ .log = p->log,
+ .global = p->global,
+ .ra_ctx = p->ctx,
+ };
+ ra_hwdec_ctx_init(&p->hwdec_ctx, vo->hwdec_devs, NULL, true);
+
+ // Loop through hardware accelerated formats and only request known
+ // supported formats.
+ for (int i = IMGFMT_VDPAU_OUTPUT; i < IMGFMT_AVPIXFMT_START; ++i) {
+ if (is_supported_fmt(i)) {
+ struct hwdec_imgfmt_request params = {
+ .imgfmt = i,
+ .probing = false,
+ };
+ ra_hwdec_ctx_load_fmt(&p->hwdec_ctx, vo->hwdec_devs, &params);
+ }
+ }
+
+ for (int i = 0; i < p->hwdec_ctx.num_hwdecs; i++) {
+ struct ra_hwdec *hw = p->hwdec_ctx.hwdecs[i];
+ if (ra_get_native_resource(p->ctx->ra, "VADisplay")) {
+ p->hwdec_type = HWDEC_VAAPI;
+ } else if (strcmp(hw->driver->name, "drmprime") == 0) {
+ p->hwdec_type = HWDEC_DRMPRIME;
+ }
+ }
+
+ if (p->hwdec_type == HWDEC_NONE) {
+ MP_ERR(vo, "No valid hardware decoding driver could be loaded!\n");
+ goto err;
+ }
+
+ p->src = (struct mp_rect){0, 0, 0, 0};
+ return 0;
+
+err:
+ uninit(vo);
+ return -1;
+}
+
+const struct vo_driver video_out_dmabuf_wayland = {
+ .description = "Wayland dmabuf video output",
+ .name = "dmabuf-wayland",
+ .caps = VO_CAP_ROTATE90,
+ .frame_owner = true,
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig2 = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .wakeup = vo_wayland_wakeup,
+ .wait_events = vo_wayland_wait_events,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+};
diff --git a/video/out/vo_drm.c b/video/out/vo_drm.c
new file mode 100644
index 0000000..aae73f7
--- /dev/null
+++ b/video/out/vo_drm.c
@@ -0,0 +1,458 @@
+/*
+ * 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 <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <sys/mman.h>
+#include <poll.h>
+#include <unistd.h>
+
+#include <drm_fourcc.h>
+#include <libswscale/swscale.h>
+
+#include "common/msg.h"
+#include "drm_atomic.h"
+#include "drm_common.h"
+#include "osdep/timer.h"
+#include "sub/osd.h"
+#include "video/fmt-conversion.h"
+#include "video/mp_image.h"
+#include "video/out/present_sync.h"
+#include "video/sws_utils.h"
+#include "vo.h"
+
+#define IMGFMT_XRGB8888 IMGFMT_BGR0
+#if BYTE_ORDER == BIG_ENDIAN
+#define IMGFMT_XRGB2101010 pixfmt2imgfmt(AV_PIX_FMT_GBRP10BE)
+#else
+#define IMGFMT_XRGB2101010 pixfmt2imgfmt(AV_PIX_FMT_GBRP10LE)
+#endif
+
+#define BYTES_PER_PIXEL 4
+#define BITS_PER_PIXEL 32
+
+struct drm_frame {
+ struct framebuffer *fb;
+};
+
+struct priv {
+ struct drm_frame **fb_queue;
+ unsigned int fb_queue_len;
+
+ uint32_t drm_format;
+ enum mp_imgfmt imgfmt;
+
+ struct mp_image *last_input;
+ struct mp_image *cur_frame;
+ struct mp_image *cur_frame_cropped;
+ struct mp_rect src;
+ struct mp_rect dst;
+ struct mp_osd_res osd;
+ struct mp_sws_context *sws;
+
+ struct framebuffer **bufs;
+ int front_buf;
+ int buf_count;
+};
+
+static void destroy_framebuffer(int fd, struct framebuffer *fb)
+{
+ if (!fb)
+ return;
+
+ if (fb->map) {
+ munmap(fb->map, fb->size);
+ }
+ if (fb->id) {
+ drmModeRmFB(fd, fb->id);
+ }
+ if (fb->handle) {
+ struct drm_mode_destroy_dumb dreq = {
+ .handle = fb->handle,
+ };
+ drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dreq);
+ }
+}
+
+static struct framebuffer *setup_framebuffer(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct vo_drm_state *drm = vo->drm;
+
+ struct framebuffer *fb = talloc_zero(drm, struct framebuffer);
+ fb->width = drm->mode.mode.hdisplay;
+ fb->height = drm->mode.mode.vdisplay;
+ fb->fd = drm->fd;
+ fb->handle = 0;
+
+ // create dumb buffer
+ struct drm_mode_create_dumb creq = {
+ .width = fb->width,
+ .height = fb->height,
+ .bpp = BITS_PER_PIXEL,
+ };
+
+ if (drmIoctl(drm->fd, DRM_IOCTL_MODE_CREATE_DUMB, &creq) < 0) {
+ MP_ERR(vo, "Cannot create dumb buffer: %s\n", mp_strerror(errno));
+ goto err;
+ }
+
+ fb->stride = creq.pitch;
+ fb->size = creq.size;
+ fb->handle = creq.handle;
+
+ // select format
+ if (drm->opts->drm_format == DRM_OPTS_FORMAT_XRGB2101010) {
+ p->drm_format = DRM_FORMAT_XRGB2101010;
+ p->imgfmt = IMGFMT_XRGB2101010;
+ } else {
+ p->drm_format = DRM_FORMAT_XRGB8888;;
+ p->imgfmt = IMGFMT_XRGB8888;
+ }
+
+ // create framebuffer object for the dumb-buffer
+ int ret = drmModeAddFB2(fb->fd, fb->width, fb->height,
+ p->drm_format,
+ (uint32_t[4]){fb->handle, 0, 0, 0},
+ (uint32_t[4]){fb->stride, 0, 0, 0},
+ (uint32_t[4]){0, 0, 0, 0},
+ &fb->id, 0);
+ if (ret) {
+ MP_ERR(vo, "Cannot create framebuffer: %s\n", mp_strerror(errno));
+ goto err;
+ }
+
+ // prepare buffer for memory mapping
+ struct drm_mode_map_dumb mreq = {
+ .handle = fb->handle,
+ };
+ if (drmIoctl(drm->fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq)) {
+ MP_ERR(vo, "Cannot map dumb buffer: %s\n", mp_strerror(errno));
+ goto err;
+ }
+
+ // perform actual memory mapping
+ fb->map = mmap(0, fb->size, PROT_READ | PROT_WRITE, MAP_SHARED,
+ drm->fd, mreq.offset);
+ if (fb->map == MAP_FAILED) {
+ MP_ERR(vo, "Cannot map dumb buffer: %s\n", mp_strerror(errno));
+ goto err;
+ }
+
+ memset(fb->map, 0, fb->size);
+ return fb;
+
+err:
+ destroy_framebuffer(drm->fd, fb);
+ return NULL;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *p = vo->priv;
+ struct vo_drm_state *drm = vo->drm;
+
+ vo->dwidth =drm->fb->width;
+ vo->dheight = drm->fb->height;
+ vo_get_src_dst_rects(vo, &p->src, &p->dst, &p->osd);
+
+ int w = p->dst.x1 - p->dst.x0;
+ int h = p->dst.y1 - p->dst.y0;
+
+ p->sws->src = *params;
+ p->sws->dst = (struct mp_image_params) {
+ .imgfmt = p->imgfmt,
+ .w = w,
+ .h = h,
+ .p_w = 1,
+ .p_h = 1,
+ };
+
+ talloc_free(p->cur_frame);
+ p->cur_frame = mp_image_alloc(p->imgfmt, drm->fb->width, drm->fb->height);
+ mp_image_params_guess_csp(&p->sws->dst);
+ mp_image_set_params(p->cur_frame, &p->sws->dst);
+ mp_image_set_size(p->cur_frame, drm->fb->width, drm->fb->height);
+
+ talloc_free(p->cur_frame_cropped);
+ p->cur_frame_cropped = mp_image_new_dummy_ref(p->cur_frame);
+ mp_image_crop_rc(p->cur_frame_cropped, p->dst);
+
+ talloc_free(p->last_input);
+ p->last_input = NULL;
+
+ if (mp_sws_reinit(p->sws) < 0)
+ return -1;
+
+ vo->want_redraw = true;
+ return 0;
+}
+
+static struct framebuffer *get_new_fb(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ p->front_buf++;
+ p->front_buf %= p->buf_count;
+
+ return p->bufs[p->front_buf];
+}
+
+static void draw_image(struct vo *vo, mp_image_t *mpi, struct framebuffer *buf)
+{
+ struct priv *p = vo->priv;
+ struct vo_drm_state *drm = vo->drm;
+
+ if (drm->active && buf != NULL) {
+ if (mpi) {
+ struct mp_image src = *mpi;
+ struct mp_rect src_rc = p->src;
+ src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, mpi->fmt.align_x);
+ src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, mpi->fmt.align_y);
+ mp_image_crop_rc(&src, src_rc);
+
+ mp_image_clear(p->cur_frame, 0, 0, p->cur_frame->w, p->dst.y0);
+ mp_image_clear(p->cur_frame, 0, p->dst.y1, p->cur_frame->w, p->cur_frame->h);
+ mp_image_clear(p->cur_frame, 0, p->dst.y0, p->dst.x0, p->dst.y1);
+ mp_image_clear(p->cur_frame, p->dst.x1, p->dst.y0, p->cur_frame->w, p->dst.y1);
+
+ mp_sws_scale(p->sws, p->cur_frame_cropped, &src);
+ osd_draw_on_image(vo->osd, p->osd, src.pts, 0, p->cur_frame);
+ } else {
+ mp_image_clear(p->cur_frame, 0, 0, p->cur_frame->w, p->cur_frame->h);
+ osd_draw_on_image(vo->osd, p->osd, 0, 0, p->cur_frame);
+ }
+
+ if (p->drm_format == DRM_FORMAT_XRGB2101010) {
+ // Pack GBRP10 image into XRGB2101010 for DRM
+ const int w = p->cur_frame->w;
+ const int h = p->cur_frame->h;
+
+ const int g_padding = p->cur_frame->stride[0]/sizeof(uint16_t) - w;
+ const int b_padding = p->cur_frame->stride[1]/sizeof(uint16_t) - w;
+ const int r_padding = p->cur_frame->stride[2]/sizeof(uint16_t) - w;
+ const int fbuf_padding = buf->stride/sizeof(uint32_t) - w;
+
+ uint16_t *g_ptr = (uint16_t*)p->cur_frame->planes[0];
+ uint16_t *b_ptr = (uint16_t*)p->cur_frame->planes[1];
+ uint16_t *r_ptr = (uint16_t*)p->cur_frame->planes[2];
+ uint32_t *fbuf_ptr = (uint32_t*)buf->map;
+ for (unsigned y = 0; y < h; ++y) {
+ for (unsigned x = 0; x < w; ++x) {
+ *fbuf_ptr++ = (*r_ptr++ << 20) | (*g_ptr++ << 10) | (*b_ptr++);
+ }
+ g_ptr += g_padding;
+ b_ptr += b_padding;
+ r_ptr += r_padding;
+ fbuf_ptr += fbuf_padding;
+ }
+ } else { // p->drm_format == DRM_FORMAT_XRGB8888
+ memcpy_pic(buf->map, p->cur_frame->planes[0],
+ p->cur_frame->w * BYTES_PER_PIXEL, p->cur_frame->h,
+ buf->stride,
+ p->cur_frame->stride[0]);
+ }
+ }
+
+ if (mpi != p->last_input) {
+ talloc_free(p->last_input);
+ p->last_input = mpi;
+ }
+}
+
+static void enqueue_frame(struct vo *vo, struct framebuffer *fb)
+{
+ struct priv *p = vo->priv;
+
+ struct drm_frame *new_frame = talloc(p, struct drm_frame);
+ new_frame->fb = fb;
+ MP_TARRAY_APPEND(p, p->fb_queue, p->fb_queue_len, new_frame);
+}
+
+static void dequeue_frame(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ talloc_free(p->fb_queue[0]);
+ MP_TARRAY_REMOVE_AT(p->fb_queue, p->fb_queue_len, 0);
+}
+
+static void swapchain_step(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (p->fb_queue_len > 0) {
+ dequeue_frame(vo);
+ }
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct vo_drm_state *drm = vo->drm;
+ struct priv *p = vo->priv;
+
+ if (!drm->active)
+ return;
+
+ drm->still = frame->still;
+
+ // we redraw the entire image when OSD needs to be redrawn
+ struct framebuffer *fb = p->bufs[p->front_buf];
+ const bool repeat = frame->repeat && !frame->redraw;
+ if (!repeat) {
+ fb = get_new_fb(vo);
+ draw_image(vo, mp_image_new_ref(frame->current), fb);
+ }
+
+ enqueue_frame(vo, fb);
+}
+
+static void queue_flip(struct vo *vo, struct drm_frame *frame)
+{
+ struct vo_drm_state *drm = vo->drm;
+
+ drm->fb = frame->fb;
+
+ int ret = drmModePageFlip(drm->fd, drm->crtc_id,
+ drm->fb->id, DRM_MODE_PAGE_FLIP_EVENT, drm);
+ if (ret)
+ MP_WARN(vo, "Failed to queue page flip: %s\n", mp_strerror(errno));
+ drm->waiting_for_flip = !ret;
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct vo_drm_state *drm = vo->drm;
+ const bool drain = drm->paused || drm->still;
+
+ if (!drm->active)
+ return;
+
+ while (drain || p->fb_queue_len > vo->opts->swapchain_depth) {
+ if (drm->waiting_for_flip) {
+ vo_drm_wait_on_flip(vo->drm);
+ swapchain_step(vo);
+ }
+ if (p->fb_queue_len <= 1)
+ break;
+ if (!p->fb_queue[1] || !p->fb_queue[1]->fb) {
+ MP_ERR(vo, "Hole in swapchain?\n");
+ swapchain_step(vo);
+ continue;
+ }
+ queue_flip(vo, p->fb_queue[1]);
+ }
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct vo_drm_state *drm = vo->drm;
+ present_sync_get_info(drm->present, info);
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ vo_drm_uninit(vo);
+
+ while (p->fb_queue_len > 0) {
+ swapchain_step(vo);
+ }
+
+ talloc_free(p->last_input);
+ talloc_free(p->cur_frame);
+ talloc_free(p->cur_frame_cropped);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (!vo_drm_init(vo))
+ goto err;
+
+ struct vo_drm_state *drm = vo->drm;
+ p->buf_count = vo->opts->swapchain_depth + 1;
+ p->bufs = talloc_zero_array(p, struct framebuffer *, p->buf_count);
+
+ p->front_buf = 0;
+ for (int i = 0; i < p->buf_count; i++) {
+ p->bufs[i] = setup_framebuffer(vo);
+ if (!p->bufs[i])
+ goto err;
+ }
+ drm->fb = p->bufs[0];
+
+ vo->drm->width = vo->drm->fb->width;
+ vo->drm->height = vo->drm->fb->height;
+
+ if (!vo_drm_acquire_crtc(vo->drm)) {
+ MP_ERR(vo, "Failed to set CRTC for connector %u: %s\n",
+ vo->drm->connector->connector_id, mp_strerror(errno));
+ goto err;
+ }
+
+ vo_drm_set_monitor_par(vo);
+ p->sws = mp_sws_alloc(vo);
+ p->sws->log = vo->log;
+ mp_sws_enable_cmdline_opts(p->sws, vo->global);
+ return 0;
+
+err:
+ uninit(vo);
+ return -1;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return sws_isSupportedInput(imgfmt2pixfmt(format));
+}
+
+static int control(struct vo *vo, uint32_t request, void *arg)
+{
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ if (vo->config_ok)
+ reconfig(vo, vo->params);
+ return VO_TRUE;
+ }
+
+ int events = 0;
+ int ret = vo_drm_control(vo, &events, request, arg);
+ vo_event(vo, events);
+ return ret;
+}
+
+const struct vo_driver video_out_drm = {
+ .name = "drm",
+ .description = "Direct Rendering Manager (software scaling)",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .uninit = uninit,
+ .wait_events = vo_drm_wait_events,
+ .wakeup = vo_drm_wakeup,
+ .priv_size = sizeof(struct priv),
+};
diff --git a/video/out/vo_gpu.c b/video/out/vo_gpu.c
new file mode 100644
index 0000000..c02e6e7
--- /dev/null
+++ b/video/out/vo_gpu.c
@@ -0,0 +1,336 @@
+/*
+ * Based on vo_gl.c by Reimar Doeffinger.
+ *
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <stdbool.h>
+#include <assert.h>
+
+#include <libavutil/common.h>
+
+#include "mpv_talloc.h"
+#include "common/common.h"
+#include "misc/bstr.h"
+#include "common/msg.h"
+#include "common/global.h"
+#include "options/m_config.h"
+#include "vo.h"
+#include "video/mp_image.h"
+#include "sub/osd.h"
+
+#include "gpu/context.h"
+#include "gpu/hwdec.h"
+#include "gpu/video.h"
+
+struct gpu_priv {
+ struct mp_log *log;
+ struct ra_ctx *ctx;
+
+ char *context_name;
+ char *context_type;
+ struct gl_video *renderer;
+
+ int events;
+};
+static void resize(struct vo *vo)
+{
+ struct gpu_priv *p = vo->priv;
+ struct ra_swapchain *sw = p->ctx->swapchain;
+
+ MP_VERBOSE(vo, "Resize: %dx%d\n", vo->dwidth, vo->dheight);
+
+ struct mp_rect src, dst;
+ struct mp_osd_res osd;
+ vo_get_src_dst_rects(vo, &src, &dst, &osd);
+
+ gl_video_resize(p->renderer, &src, &dst, &osd);
+
+ int fb_depth = sw->fns->color_depth ? sw->fns->color_depth(sw) : 0;
+ if (fb_depth)
+ MP_VERBOSE(p, "Reported display depth: %d\n", fb_depth);
+ gl_video_set_fb_depth(p->renderer, fb_depth);
+
+ vo->want_redraw = true;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct gpu_priv *p = vo->priv;
+ struct ra_swapchain *sw = p->ctx->swapchain;
+
+ struct ra_fbo fbo;
+ if (!sw->fns->start_frame(sw, &fbo))
+ return;
+
+ gl_video_render_frame(p->renderer, frame, fbo, RENDER_FRAME_DEF);
+ if (!sw->fns->submit_frame(sw, frame)) {
+ MP_ERR(vo, "Failed presenting frame!\n");
+ return;
+ }
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct gpu_priv *p = vo->priv;
+ struct ra_swapchain *sw = p->ctx->swapchain;
+ sw->fns->swap_buffers(sw);
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct gpu_priv *p = vo->priv;
+ struct ra_swapchain *sw = p->ctx->swapchain;
+ if (sw->fns->get_vsync)
+ sw->fns->get_vsync(sw, info);
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct gpu_priv *p = vo->priv;
+ if (!gl_video_check_format(p->renderer, format))
+ return 0;
+ return 1;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct gpu_priv *p = vo->priv;
+
+ if (!p->ctx->fns->reconfig(p->ctx))
+ return -1;
+
+ resize(vo);
+ gl_video_config(p->renderer, params);
+
+ return 0;
+}
+
+static void request_hwdec_api(struct vo *vo, void *data)
+{
+ struct gpu_priv *p = vo->priv;
+ gl_video_load_hwdecs_for_img_fmt(p->renderer, vo->hwdec_devs, data);
+}
+
+static void call_request_hwdec_api(void *ctx,
+ struct hwdec_imgfmt_request *params)
+{
+ // Roundabout way to run hwdec loading on the VO thread.
+ // Redirects to request_hwdec_api().
+ vo_control(ctx, VOCTRL_LOAD_HWDEC_API, params);
+}
+
+static void get_and_update_icc_profile(struct gpu_priv *p)
+{
+ if (gl_video_icc_auto_enabled(p->renderer)) {
+ MP_VERBOSE(p, "Querying ICC profile...\n");
+ bstr icc = bstr0(NULL);
+ int r = p->ctx->fns->control(p->ctx, &p->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");
+ }
+
+ gl_video_set_icc_profile(p->renderer, icc);
+ }
+ }
+}
+
+static void get_and_update_ambient_lighting(struct gpu_priv *p)
+{
+ int lux;
+ int r = p->ctx->fns->control(p->ctx, &p->events, VOCTRL_GET_AMBIENT_LUX, &lux);
+ if (r == VO_TRUE) {
+ gl_video_set_ambient_lux(p->renderer, lux);
+ }
+ if (r != VO_TRUE && gl_video_gamma_auto_enabled(p->renderer)) {
+ MP_ERR(p, "gamma_auto option provided, but querying for ambient"
+ " lighting is not supported on this platform\n");
+ }
+}
+
+static void update_ra_ctx_options(struct vo *vo)
+{
+ struct gpu_priv *p = vo->priv;
+
+ /* Only the alpha option has any runtime toggle ability. */
+ struct gl_video_opts *gl_opts = mp_get_config_group(p->ctx, vo->global, &gl_video_conf);
+ p->ctx->opts.want_alpha = gl_opts->alpha_mode == 1;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct gpu_priv *p = vo->priv;
+
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ resize(vo);
+ return VO_TRUE;
+ case VOCTRL_SET_EQUALIZER:
+ vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_SCREENSHOT: {
+ struct vo_frame *frame = vo_get_current_vo_frame(vo);
+ if (frame)
+ gl_video_screenshot(p->renderer, frame, data);
+ talloc_free(frame);
+ return true;
+ }
+ case VOCTRL_LOAD_HWDEC_API:
+ request_hwdec_api(vo, data);
+ return true;
+ case VOCTRL_UPDATE_RENDER_OPTS: {
+ update_ra_ctx_options(vo);
+ gl_video_configure_queue(p->renderer, vo);
+ get_and_update_icc_profile(p);
+ if (p->ctx->fns->update_render_opts)
+ p->ctx->fns->update_render_opts(p->ctx);
+ vo->want_redraw = true;
+ return true;
+ }
+ case VOCTRL_RESET:
+ gl_video_reset(p->renderer);
+ return true;
+ case VOCTRL_PAUSE:
+ if (gl_video_showing_interpolated_frame(p->renderer))
+ vo->want_redraw = true;
+ return true;
+ case VOCTRL_PERFORMANCE_DATA:
+ gl_video_perfdata(p->renderer, (struct voctrl_performance_data *)data);
+ return true;
+ case VOCTRL_EXTERNAL_RESIZE:
+ p->ctx->fns->reconfig(p->ctx);
+ resize(vo);
+ return true;
+ }
+
+ int events = 0;
+ int r = p->ctx->fns->control(p->ctx, &events, request, data);
+ if (events & VO_EVENT_ICC_PROFILE_CHANGED) {
+ get_and_update_icc_profile(p);
+ vo->want_redraw = true;
+ }
+ if (events & VO_EVENT_AMBIENT_LIGHTING_CHANGED) {
+ get_and_update_ambient_lighting(p);
+ vo->want_redraw = true;
+ }
+ events |= p->events;
+ p->events = 0;
+ 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 gpu_priv *p = vo->priv;
+ if (p->ctx && p->ctx->fns->wakeup)
+ p->ctx->fns->wakeup(p->ctx);
+}
+
+static void wait_events(struct vo *vo, int64_t until_time_ns)
+{
+ struct gpu_priv *p = vo->priv;
+ if (p->ctx && p->ctx->fns->wait_events) {
+ p->ctx->fns->wait_events(p->ctx, until_time_ns);
+ } else {
+ vo_wait_default(vo, until_time_ns);
+ }
+}
+
+static struct mp_image *get_image(struct vo *vo, int imgfmt, int w, int h,
+ int stride_align, int flags)
+{
+ struct gpu_priv *p = vo->priv;
+
+ return gl_video_get_image(p->renderer, imgfmt, w, h, stride_align, flags);
+}
+
+static void uninit(struct vo *vo)
+{
+ struct gpu_priv *p = vo->priv;
+
+ gl_video_uninit(p->renderer);
+ if (vo->hwdec_devs) {
+ hwdec_devices_set_loader(vo->hwdec_devs, NULL, NULL);
+ hwdec_devices_destroy(vo->hwdec_devs);
+ }
+ ra_ctx_destroy(&p->ctx);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct gpu_priv *p = vo->priv;
+ p->log = vo->log;
+
+ struct ra_ctx_opts *ctx_opts = mp_get_config_group(vo, vo->global, &ra_ctx_conf);
+ struct gl_video_opts *gl_opts = mp_get_config_group(vo, vo->global, &gl_video_conf);
+ struct ra_ctx_opts opts = *ctx_opts;
+ opts.want_alpha = gl_opts->alpha_mode == 1;
+ p->ctx = ra_ctx_create(vo, opts);
+ talloc_free(ctx_opts);
+ talloc_free(gl_opts);
+ if (!p->ctx)
+ goto err_out;
+ assert(p->ctx->ra);
+ assert(p->ctx->swapchain);
+
+ p->renderer = gl_video_init(p->ctx->ra, vo->log, vo->global);
+ gl_video_set_osd_source(p->renderer, vo->osd);
+ gl_video_configure_queue(p->renderer, vo);
+
+ get_and_update_icc_profile(p);
+
+ vo->hwdec_devs = hwdec_devices_create();
+ hwdec_devices_set_loader(vo->hwdec_devs, call_request_hwdec_api, vo);
+
+ gl_video_init_hwdecs(p->renderer, p->ctx, vo->hwdec_devs, false);
+
+ return 0;
+
+err_out:
+ uninit(vo);
+ return -1;
+}
+
+const struct vo_driver video_out_gpu = {
+ .description = "Shader-based GPU Renderer",
+ .name = "gpu",
+ .caps = VO_CAP_ROTATE90,
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .get_image = 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 gpu_priv),
+};
diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c
new file mode 100644
index 0000000..1dc1b18
--- /dev/null
+++ b/video/out/vo_gpu_next.c
@@ -0,0 +1,2104 @@
+/*
+ * 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 cache {
+ char *path;
+ pl_cache cache;
+ uint64_t sig;
+};
+
+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 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 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);
+ }
+}
+
+#if PL_API_VER < 342
+static inline void xor_hash(void *hash, pl_cache_obj obj)
+{
+ *((uint64_t *) hash) ^= obj.key;
+}
+
+static inline uint64_t pl_cache_signature(pl_cache cache)
+{
+ uint64_t hash = 0;
+ pl_cache_iterate(cache, xor_hash, &hash);
+ return hash;
+}
+#endif
+
+static void cache_init(struct vo *vo, struct cache *cache, size_t max_size,
+ const char *dir_opt)
+{
+ struct priv *p = vo->priv;
+ const char *name = cache == &p->shader_cache ? "shader.cache" : "icc.cache";
+
+ char *dir;
+ if (dir_opt && dir_opt[0]) {
+ dir = mp_get_user_path(NULL, p->global, dir_opt);
+ } else {
+ dir = mp_find_user_file(NULL, p->global, "cache", "");
+ }
+ if (!dir || !dir[0])
+ goto done;
+
+ mp_mkdirp(dir);
+ cache->path = mp_path_join(vo, dir, name);
+ cache->cache = pl_cache_create(pl_cache_params(
+ .log = p->pllog,
+ .max_total_size = max_size,
+ ));
+
+ FILE *file = fopen(cache->path, "rb");
+ if (file) {
+ int ret = pl_cache_load_file(cache->cache, file);
+ fclose(file);
+ if (ret < 0)
+ MP_WARN(p, "Failed loading cache from %s\n", cache->path);
+ }
+
+ cache->sig = pl_cache_signature(cache->cache);
+done:
+ talloc_free(dir);
+}
+
+static void cache_uninit(struct priv *p, struct cache *cache)
+{
+ if (!cache->cache)
+ goto done;
+ if (pl_cache_signature(cache->cache) == cache->sig)
+ goto done; // skip re-saving identical cache
+
+ assert(cache->path);
+ char *tmp = talloc_asprintf(cache->path, "%sXXXXXX", cache->path);
+ int fd = mkstemp(tmp);
+ if (fd < 0)
+ goto done;
+ FILE *file = fdopen(fd, "wb");
+ if (!file) {
+ close(fd);
+ unlink(tmp);
+ goto done;
+ }
+ int ret = pl_cache_save_file(cache->cache, file);
+ fclose(file);
+ if (ret >= 0)
+ ret = rename(tmp, cache->path);
+ if (ret < 0) {
+ MP_WARN(p, "Failed saving cache to %s\n", cache->path);
+ unlink(tmp);
+ }
+
+ // fall through
+done:
+ pl_cache_destroy(&cache->cache);
+}
+
+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);
+
+ cache_uninit(p, &p->shader_cache);
+ cache_uninit(p, &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);
+
+ if (gl_opts->shader_cache)
+ cache_init(vo, &p->shader_cache, 10 << 20, gl_opts->shader_cache_dir);
+ if (gl_opts->icc_opts->cache)
+ cache_init(vo, &p->icc_cache, 20 << 20, gl_opts->icc_opts->cache_dir);
+
+ pl_gpu_set_cache(p->gpu, p->shader_cache.cache);
+ 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.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/2, 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 : 2);
+ }
+ 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}
+ },
+};
diff --git a/video/out/vo_image.c b/video/out/vo_image.c
new file mode 100644
index 0000000..cc48ab3
--- /dev/null
+++ b/video/out/vo_image.c
@@ -0,0 +1,165 @@
+/*
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <stdbool.h>
+#include <sys/stat.h>
+
+#include <libswscale/swscale.h>
+
+#include "misc/bstr.h"
+#include "osdep/io.h"
+#include "options/m_config.h"
+#include "options/path.h"
+#include "mpv_talloc.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "video/out/vo.h"
+#include "video/csputils.h"
+#include "video/mp_image.h"
+#include "video/fmt-conversion.h"
+#include "video/image_writer.h"
+#include "video/sws_utils.h"
+#include "sub/osd.h"
+#include "options/m_option.h"
+
+static const struct m_sub_options image_writer_conf = {
+ .opts = image_writer_opts,
+ .size = sizeof(struct image_writer_opts),
+ .defaults = &image_writer_opts_defaults,
+};
+
+struct vo_image_opts {
+ struct image_writer_opts *opts;
+ char *outdir;
+};
+
+#define OPT_BASE_STRUCT struct vo_image_opts
+
+static const struct m_sub_options vo_image_conf = {
+ .opts = (const struct m_option[]) {
+ {"vo-image", OPT_SUBSTRUCT(opts, image_writer_conf)},
+ {"vo-image-outdir", OPT_STRING(outdir), .flags = M_OPT_FILE},
+ {0},
+ },
+ .size = sizeof(struct vo_image_opts),
+};
+
+struct priv {
+ struct vo_image_opts *opts;
+
+ struct mp_image *current;
+ int frame;
+};
+
+static bool checked_mkdir(struct vo *vo, const char *buf)
+{
+ MP_INFO(vo, "Creating output directory '%s'...\n", buf);
+ if (mkdir(buf, 0755) < 0) {
+ char *errstr = mp_strerror(errno);
+ if (errno == EEXIST) {
+ struct stat stat_p;
+ if (stat(buf, &stat_p ) == 0 && S_ISDIR(stat_p.st_mode))
+ return true;
+ }
+ MP_ERR(vo, "Error creating output directory: %s\n", errstr);
+ return false;
+ }
+ return true;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ return 0;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+ if (!frame->current)
+ return;
+
+ p->current = frame->current;
+
+ struct mp_osd_res dim = osd_res_from_image_params(vo->params);
+ osd_draw_on_image(vo->osd, dim, frame->current->pts, OSD_DRAW_SUB_ONLY, p->current);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ if (!p->current)
+ return;
+
+ (p->frame)++;
+
+ void *t = talloc_new(NULL);
+ char *filename = talloc_asprintf(t, "%08d.%s", p->frame,
+ image_writer_file_ext(p->opts->opts));
+
+ if (p->opts->outdir && strlen(p->opts->outdir))
+ filename = mp_path_join(t, p->opts->outdir, filename);
+
+ MP_INFO(vo, "Saving %s\n", filename);
+ write_image(p->current, p->opts->opts, filename, vo->global, vo->log);
+
+ talloc_free(t);
+}
+
+static int query_format(struct vo *vo, int fmt)
+{
+ if (mp_sws_supported_format(fmt))
+ return 1;
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ p->opts = mp_get_config_group(vo, vo->global, &vo_image_conf);
+ if (p->opts->outdir && !checked_mkdir(vo, p->opts->outdir))
+ return -1;
+ return 0;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ return VO_NOTIMPL;
+}
+
+const struct vo_driver video_out_image =
+{
+ .description = "Write video frames to image files",
+ .name = "image",
+ .untimed = true,
+ .priv_size = sizeof(struct priv),
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .global_opts = &vo_image_conf,
+};
diff --git a/video/out/vo_kitty.c b/video/out/vo_kitty.c
new file mode 100644
index 0000000..7d548c7
--- /dev/null
+++ b/video/out/vo_kitty.c
@@ -0,0 +1,433 @@
+/*
+ * Video output device using the kitty terminal graphics protocol
+ * See https://sw.kovidgoyal.net/kitty/graphics-protocol/
+ *
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+
+#include "config.h"
+
+#if HAVE_POSIX
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#endif
+
+#include <libswscale/swscale.h>
+#include <libavutil/base64.h>
+
+#include "options/m_config.h"
+#include "osdep/terminal.h"
+#include "sub/osd.h"
+#include "vo.h"
+#include "video/sws_utils.h"
+#include "video/mp_image.h"
+
+#define IMGFMT IMGFMT_RGB24
+#define BYTES_PER_PX 3
+#define DEFAULT_WIDTH_PX 320
+#define DEFAULT_HEIGHT_PX 240
+#define DEFAULT_WIDTH 80
+#define DEFAULT_HEIGHT 25
+
+static inline void write_str(const char *s)
+{
+ // On POSIX platforms, write() is the fastest method. It also is the only
+ // one that allows atomic writes so mpv’s output will not be interrupted
+ // by other processes or threads that write to stdout, which would cause
+ // screen corruption. POSIX does not guarantee atomicity for writes
+ // exceeding PIPE_BUF, but at least Linux does seem to implement it that
+ // way.
+#if HAVE_POSIX
+ int remain = strlen(s);
+ while (remain > 0) {
+ ssize_t written = write(STDOUT_FILENO, s, remain);
+ if (written < 0)
+ return;
+ remain -= written;
+ s += written;
+ }
+#else
+ printf("%s", s);
+ fflush(stdout);
+#endif
+}
+
+#define KITTY_ESC_IMG "\033_Ga=T,f=24,s=%d,v=%d,C=1,q=2,m=1;"
+#define KITTY_ESC_IMG_SHM "\033_Ga=T,t=s,f=24,s=%d,v=%d,C=1,q=2,m=1;%s\033\\"
+#define KITTY_ESC_CONTINUE "\033_Gm=%d;"
+#define KITTY_ESC_END "\033\\"
+#define KITTY_ESC_DELETE_ALL "\033_Ga=d;\033\\"
+
+struct vo_kitty_opts {
+ int width, height, top, left, rows, cols;
+ bool config_clear, alt_screen;
+ bool use_shm;
+};
+
+struct priv {
+ struct vo_kitty_opts opts;
+
+ uint8_t *buffer;
+ char *output;
+ char *shm_path, *shm_path_b64;
+ int buffer_size, output_size;
+ int shm_fd;
+
+ int left, top, width, height, cols, rows;
+
+ struct mp_rect src;
+ struct mp_rect dst;
+ struct mp_osd_res osd;
+ struct mp_image *frame;
+ struct mp_sws_context *sws;
+};
+
+#if HAVE_POSIX
+static struct sigaction saved_sigaction = {0};
+static bool resized;
+#endif
+
+static void close_shm(struct priv *p)
+{
+#if HAVE_POSIX_SHM
+ if (p->buffer != NULL) {
+ munmap(p->buffer, p->buffer_size);
+ p->buffer = NULL;
+ }
+ if (p->shm_fd != -1) {
+ close(p->shm_fd);
+ p->shm_fd = -1;
+ }
+#endif
+}
+
+static void free_bufs(struct vo* vo)
+{
+ struct priv* p = vo->priv;
+
+ talloc_free(p->frame);
+ talloc_free(p->output);
+
+ if (p->opts.use_shm) {
+ close_shm(p);
+ } else {
+ talloc_free(p->buffer);
+ }
+}
+
+static void get_win_size(struct vo *vo, int *out_rows, int *out_cols,
+ int *out_width, int *out_height)
+{
+ struct priv *p = vo->priv;
+ *out_rows = DEFAULT_HEIGHT;
+ *out_cols = DEFAULT_WIDTH;
+ *out_width = DEFAULT_WIDTH_PX;
+ *out_height = DEFAULT_HEIGHT_PX;
+
+ terminal_get_size2(out_rows, out_cols, out_width, out_height);
+
+ *out_rows = p->opts.rows > 0 ? p->opts.rows : *out_rows;
+ *out_cols = p->opts.cols > 0 ? p->opts.cols : *out_cols;
+ *out_width = p->opts.width > 0 ? p->opts.width : *out_width;
+ *out_height = p->opts.height > 0 ? p->opts.height : *out_height;
+}
+
+static void set_out_params(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ vo_get_src_dst_rects(vo, &p->src, &p->dst, &p->osd);
+
+ p->width = p->dst.x1 - p->dst.x0;
+ p->height = p->dst.y1 - p->dst.y0;
+ p->top = p->opts.top > 0 ?
+ p->opts.top : p->rows * p->dst.y0 / vo->dheight;
+ p->left = p->opts.left > 0 ?
+ p->opts.left : p->cols * p->dst.x0 / vo->dwidth;
+
+ p->buffer_size = 3 * p->width * p->height;
+ p->output_size = AV_BASE64_SIZE(p->buffer_size);
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *p = vo->priv;
+
+ vo->want_redraw = true;
+ write_str(KITTY_ESC_DELETE_ALL);
+ if (p->opts.config_clear)
+ write_str(TERM_ESC_CLEAR_SCREEN);
+
+ get_win_size(vo, &p->rows, &p->cols, &vo->dwidth, &vo->dheight);
+ set_out_params(vo);
+ free_bufs(vo);
+
+ p->sws->src = *params;
+ p->sws->src.w = mp_rect_w(p->src);
+ p->sws->src.h = mp_rect_h(p->src);
+ p->sws->dst = (struct mp_image_params) {
+ .imgfmt = IMGFMT,
+ .w = p->width,
+ .h = p->height,
+ .p_w = 1,
+ .p_h = 1,
+ };
+
+ p->frame = mp_image_alloc(IMGFMT, p->width, p->height);
+ if (!p->frame)
+ return -1;
+
+ if (mp_sws_reinit(p->sws) < 0)
+ return -1;
+
+ if (!p->opts.use_shm) {
+ p->buffer = talloc_array(NULL, uint8_t, p->buffer_size);
+ p->output = talloc_array(NULL, char, p->output_size);
+ }
+
+ return 0;
+}
+
+static int create_shm(struct vo *vo)
+{
+#if HAVE_POSIX_SHM
+ struct priv *p = vo->priv;
+ p->shm_fd = shm_open(p->shm_path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
+ if (p->shm_fd == -1) {
+ MP_ERR(vo, "Failed to create shared memory object");
+ return 0;
+ }
+
+ if (ftruncate(p->shm_fd, p->buffer_size) == -1) {
+ MP_ERR(vo, "Failed to truncate shared memory object");
+ shm_unlink(p->shm_path);
+ close(p->shm_fd);
+ return 0;
+ }
+
+ p->buffer = mmap(NULL, p->buffer_size,
+ PROT_READ | PROT_WRITE, MAP_SHARED, p->shm_fd, 0);
+
+ if (p->buffer == MAP_FAILED) {
+ MP_ERR(vo, "Failed to mmap shared memory object");
+ shm_unlink(p->shm_path);
+ close(p->shm_fd);
+ return 0;
+ }
+ return 1;
+#else
+ return 0;
+#endif
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+ mp_image_t *mpi = NULL;
+
+#if !HAVE_POSIX
+ int prev_height = vo->dheight;
+ int prev_width = vo->dwidth;
+ get_win_size(vo, &p->rows, &p->cols, &vo->dwidth, &vo->dheight);
+ bool resized = (prev_width != vo->dwidth || prev_height != vo->dheight);
+#endif
+
+ if (resized)
+ reconfig(vo, vo->params);
+
+ resized = false;
+
+ if (frame->current) {
+ mpi = mp_image_new_ref(frame->current);
+ struct mp_rect src_rc = p->src;
+ src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, mpi->fmt.align_x);
+ src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, mpi->fmt.align_y);
+ mp_image_crop_rc(mpi, src_rc);
+
+ mp_sws_scale(p->sws, p->frame, mpi);
+ } else {
+ mp_image_clear(p->frame, 0, 0, p->width, p->height);
+ }
+
+ struct mp_osd_res res = { .w = p->width, .h = p->height };
+ osd_draw_on_image(vo->osd, res, mpi ? mpi->pts : 0, 0, p->frame);
+
+
+ if (p->opts.use_shm && !create_shm(vo))
+ return;
+
+ memcpy_pic(p->buffer, p->frame->planes[0], p->width * BYTES_PER_PX,
+ p->height, p->width * BYTES_PER_PX, p->frame->stride[0]);
+
+ if (!p->opts.use_shm)
+ av_base64_encode(p->output, p->output_size, p->buffer, p->buffer_size);
+
+ talloc_free(mpi);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv* p = vo->priv;
+
+ if (p->buffer == NULL)
+ return;
+
+ char *cmd = talloc_asprintf(NULL, TERM_ESC_GOTO_YX, p->top, p->left);
+
+ if (p->opts.use_shm) {
+ cmd = talloc_asprintf_append(cmd, KITTY_ESC_IMG_SHM, p->width, p->height, p->shm_path_b64);
+ } else {
+ if (p->output == NULL) {
+ talloc_free(cmd);
+ return;
+ }
+
+ cmd = talloc_asprintf_append(cmd, KITTY_ESC_IMG, p->width, p->height);
+ for (int offset = 0, noffset;; offset += noffset) {
+ if (offset)
+ cmd = talloc_asprintf_append(cmd, KITTY_ESC_CONTINUE, offset < p->output_size);
+ noffset = MPMIN(4096, p->output_size - offset);
+ cmd = talloc_strndup_append(cmd, p->output + offset, noffset);
+ cmd = talloc_strdup_append(cmd, KITTY_ESC_END);
+
+ if (offset >= p->output_size)
+ break;
+ }
+ }
+
+ write_str(cmd);
+ talloc_free(cmd);
+
+#if HAVE_POSIX
+ if (p->opts.use_shm)
+ close_shm(p);
+#endif
+}
+
+#if HAVE_POSIX
+static void handle_winch(int sig) {
+ resized = true;
+ if (saved_sigaction.sa_handler)
+ saved_sigaction.sa_handler(sig);
+}
+#endif
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ p->sws = mp_sws_alloc(vo);
+ p->sws->log = vo->log;
+ mp_sws_enable_cmdline_opts(p->sws, vo->global);
+
+#if HAVE_POSIX
+ struct sigaction sa;
+ sa.sa_handler = handle_winch;
+ sigaction(SIGWINCH, &sa, &saved_sigaction);
+#endif
+
+#if HAVE_POSIX_SHM
+ if (p->opts.use_shm) {
+ p->shm_path = talloc_asprintf(vo, "/mpv-kitty-%p", vo);
+ int p_size = strlen(p->shm_path) - 1;
+ int b64_size = AV_BASE64_SIZE(p_size);
+ p->shm_path_b64 = talloc_array(vo, char, b64_size);
+ av_base64_encode(p->shm_path_b64, b64_size, p->shm_path + 1, p_size);
+ }
+#else
+ if (p->opts.use_shm) {
+ MP_ERR(vo, "Shared memory support is not available on this platform.");
+ return -1;
+ }
+#endif
+
+ write_str(TERM_ESC_HIDE_CURSOR);
+ if (p->opts.alt_screen)
+ write_str(TERM_ESC_ALT_SCREEN);
+
+ return 0;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return format == IMGFMT;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ if (request == VOCTRL_SET_PANSCAN)
+ return (vo->config_ok && !reconfig(vo, vo->params)) ? VO_TRUE : VO_FALSE;
+ return VO_NOTIMPL;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+#if HAVE_POSIX
+ sigaction(SIGWINCH, &saved_sigaction, NULL);
+#endif
+
+ write_str(TERM_ESC_RESTORE_CURSOR);
+
+ if (p->opts.alt_screen) {
+ write_str(TERM_ESC_NORMAL_SCREEN);
+ } else {
+ char *cmd = talloc_asprintf(vo, TERM_ESC_GOTO_YX, p->cols, 0);
+ write_str(cmd);
+ }
+
+ free_bufs(vo);
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct vo_driver video_out_kitty = {
+ .name = "kitty",
+ .description = "Kitty terminal graphics protocol",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .shm_fd = -1,
+ .opts.config_clear = true,
+ .opts.alt_screen = true,
+ },
+ .options = (const m_option_t[]) {
+ {"width", OPT_INT(opts.width)},
+ {"height", OPT_INT(opts.height)},
+ {"top", OPT_INT(opts.top)},
+ {"left", OPT_INT(opts.left)},
+ {"rows", OPT_INT(opts.rows)},
+ {"cols", OPT_INT(opts.cols)},
+ {"config-clear", OPT_BOOL(opts.config_clear), },
+ {"alt-screen", OPT_BOOL(opts.alt_screen), },
+ {"use-shm", OPT_BOOL(opts.use_shm), },
+ {0}
+ },
+ .options_prefix = "vo-kitty",
+};
diff --git a/video/out/vo_lavc.c b/video/out/vo_lavc.c
new file mode 100644
index 0000000..7170c1d
--- /dev/null
+++ b/video/out/vo_lavc.c
@@ -0,0 +1,262 @@
+/*
+ * video encoding using libavformat
+ *
+ * Copyright (C) 2010 Nicolas George <george@nsup.org>
+ * Copyright (C) 2011-2012 Rudolf Polzer <divVerent@xonotic.org>
+ *
+ * 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 <stdio.h>
+#include <stdlib.h>
+
+#include "common/common.h"
+#include "options/options.h"
+#include "video/fmt-conversion.h"
+#include "video/mp_image.h"
+#include "mpv_talloc.h"
+#include "vo.h"
+
+#include "common/encode_lavc.h"
+
+#include "sub/osd.h"
+
+struct priv {
+ struct encoder_context *enc;
+
+ bool shutdown;
+};
+
+static int preinit(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ vc->enc = encoder_context_alloc(vo->encode_lavc_ctx, STREAM_VIDEO, vo->log);
+ if (!vc->enc)
+ return -1;
+ talloc_steal(vc, vc->enc);
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ struct encoder_context *enc = vc->enc;
+
+ if (!vc->shutdown)
+ encoder_encode(enc, NULL); // finish encoding
+}
+
+static void on_ready(void *ptr)
+{
+ struct vo *vo = ptr;
+
+ vo_event(vo, VO_EVENT_INITIAL_UNBLOCK);
+}
+
+static int reconfig2(struct vo *vo, struct mp_image *img)
+{
+ struct priv *vc = vo->priv;
+ AVCodecContext *encoder = vc->enc->encoder;
+
+ struct mp_image_params *params = &img->params;
+ enum AVPixelFormat pix_fmt = imgfmt2pixfmt(params->imgfmt);
+ AVRational aspect = {params->p_w, params->p_h};
+ int width = params->w;
+ int height = params->h;
+
+ if (vc->shutdown)
+ return -1;
+
+ if (avcodec_is_open(encoder)) {
+ if (width == encoder->width && height == encoder->height &&
+ pix_fmt == encoder->pix_fmt)
+ {
+ // consider these changes not critical
+ MP_ERR(vo, "Ignoring mid-stream parameter changes!\n");
+ return 0;
+ }
+
+ /* FIXME Is it possible with raw video? */
+ MP_ERR(vo, "resolution changes not supported.\n");
+ goto error;
+ }
+
+ // When we get here, this must be the first call to reconfigure(). Thus, we
+ // can rely on no existing data in vc having been allocated yet.
+ // Reason:
+ // - Second calls after reconfigure() already failed once fail (due to the
+ // vc->shutdown check above).
+ // - Second calls after reconfigure() already succeeded once return early
+ // (due to the avcodec_is_open() check above).
+
+ if (pix_fmt == AV_PIX_FMT_NONE) {
+ MP_FATAL(vo, "Format %s not supported by lavc.\n",
+ mp_imgfmt_to_name(params->imgfmt));
+ goto error;
+ }
+
+ encoder->sample_aspect_ratio = aspect;
+ encoder->width = width;
+ encoder->height = height;
+ encoder->pix_fmt = pix_fmt;
+ encoder->colorspace = mp_csp_to_avcol_spc(params->color.space);
+ encoder->color_range = mp_csp_levels_to_avcol_range(params->color.levels);
+
+ AVRational tb;
+
+ // we want to handle:
+ // 1/25
+ // 1001/24000
+ // 1001/30000
+ // for this we would need 120000fps...
+ // however, mpeg-4 only allows 16bit values
+ // so let's take 1001/30000 out
+ tb.num = 24000;
+ tb.den = 1;
+
+ const AVRational *rates = encoder->codec->supported_framerates;
+ if (rates && rates[0].den)
+ tb = rates[av_find_nearest_q_idx(tb, rates)];
+
+ encoder->time_base = av_inv_q(tb);
+
+ // Used for rate control, level selection, etc.
+ // Usually it's not too catastrophic if this isn't exactly correct,
+ // as long as it's not off by orders of magnitude.
+ // If we don't set anything, encoders will use the time base,
+ // and 24000 is so high that the output can end up extremely screwy (see #11215),
+ // so we default to 240 if we don't have a real value.
+ if (img->nominal_fps > 0)
+ encoder->framerate = av_d2q(img->nominal_fps, img->nominal_fps * 1001 + 2); // Hopefully give exact results for NTSC rates
+ else
+ encoder->framerate = (AVRational){ 240, 1 };
+
+ if (!encoder_init_codec_and_muxer(vc->enc, on_ready, vo))
+ goto error;
+
+ return 0;
+
+error:
+ vc->shutdown = true;
+ return -1;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct priv *vc = vo->priv;
+
+ enum AVPixelFormat pix_fmt = imgfmt2pixfmt(format);
+ const enum AVPixelFormat *p = vc->enc->encoder->codec->pix_fmts;
+
+ if (!p)
+ return 1;
+
+ while (*p != AV_PIX_FMT_NONE) {
+ if (*p == pix_fmt)
+ return 1;
+ p++;
+ }
+
+ return 0;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *voframe)
+{
+ struct priv *vc = vo->priv;
+ struct encoder_context *enc = vc->enc;
+ struct encode_lavc_context *ectx = enc->encode_lavc_ctx;
+ AVCodecContext *avc = enc->encoder;
+
+ if (voframe->redraw || voframe->repeat || voframe->num_frames < 1)
+ return;
+
+ struct mp_image *mpi = voframe->frames[0];
+
+ struct mp_osd_res dim = osd_res_from_image_params(vo->params);
+ osd_draw_on_image(vo->osd, dim, mpi->pts, OSD_DRAW_SUB_ONLY, mpi);
+
+ if (vc->shutdown)
+ return;
+
+ // Lock for shared timestamp fields.
+ mp_mutex_lock(&ectx->lock);
+
+ double pts = mpi->pts;
+ double outpts = pts;
+ if (!enc->options->rawts) {
+ // fix the discontinuity pts offset
+ if (ectx->discontinuity_pts_offset == MP_NOPTS_VALUE) {
+ ectx->discontinuity_pts_offset = ectx->next_in_pts - pts;
+ } else if (fabs(pts + ectx->discontinuity_pts_offset -
+ ectx->next_in_pts) > 30)
+ {
+ MP_WARN(vo, "detected an unexpected discontinuity (pts jumped by "
+ "%f seconds)\n",
+ pts + ectx->discontinuity_pts_offset - ectx->next_in_pts);
+ ectx->discontinuity_pts_offset = ectx->next_in_pts - pts;
+ }
+
+ outpts = pts + ectx->discontinuity_pts_offset;
+ }
+
+ if (!enc->options->rawts) {
+ // calculate expected pts of next video frame
+ double timeunit = av_q2d(avc->time_base);
+ double expected_next_pts = pts + timeunit;
+ // set next allowed output pts value
+ double nextpts = expected_next_pts + ectx->discontinuity_pts_offset;
+ if (nextpts > ectx->next_in_pts)
+ ectx->next_in_pts = nextpts;
+ }
+
+ mp_mutex_unlock(&ectx->lock);
+
+ AVFrame *frame = mp_image_to_av_frame(mpi);
+ MP_HANDLE_OOM(frame);
+
+ frame->pts = rint(outpts * av_q2d(av_inv_q(avc->time_base)));
+ frame->pict_type = 0; // keep this at unknown/undefined
+ frame->quality = avc->global_quality;
+ encoder_encode(enc, frame);
+ av_frame_free(&frame);
+}
+
+static void flip_page(struct vo *vo)
+{
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ return VO_NOTIMPL;
+}
+
+const struct vo_driver video_out_lavc = {
+ .encode = true,
+ .description = "video encoding using libavcodec",
+ .name = "lavc",
+ .initially_blocked = true,
+ .untimed = true,
+ .priv_size = sizeof(struct priv),
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig2 = reconfig2,
+ .control = control,
+ .uninit = uninit,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+};
+
+// vim: sw=4 ts=4 et tw=80
diff --git a/video/out/vo_libmpv.c b/video/out/vo_libmpv.c
new file mode 100644
index 0000000..972588e
--- /dev/null
+++ b/video/out/vo_libmpv.c
@@ -0,0 +1,748 @@
+#include <assert.h>
+#include <limits.h>
+#include <math.h>
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "mpv_talloc.h"
+#include "common/common.h"
+#include "misc/bstr.h"
+#include "misc/dispatch.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "aspect.h"
+#include "dr_helper.h"
+#include "vo.h"
+#include "video/mp_image.h"
+#include "sub/osd.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+
+#include "common/global.h"
+#include "player/client.h"
+
+#include "libmpv.h"
+
+/*
+ * mpv_render_context is managed by the host application - the host application
+ * can access it any time, even if the VO is destroyed (or not created yet).
+ *
+ * - the libmpv user can mix render API and normal API; thus render API
+ * functions can wait on the core, but not the reverse
+ * - the core does blocking calls into the VO thread, thus the VO functions
+ * can't wait on the user calling the API functions
+ * - to make video timing work like it should, the VO thread waits on the
+ * render API user anyway, and the (unlikely) deadlock is avoided with
+ * a timeout
+ *
+ * Locking: mpv core > VO > mpv_render_context.lock > mp_client_api.lock
+ * > mpv_render_context.update_lock
+ * And: render thread > VO (wait for present)
+ * VO > render thread (wait for present done, via timeout)
+ *
+ * Locking gets more complex with advanced_control enabled. Use
+ * mpv_render_context.dispatch with care; synchronous calls can add lock
+ * dependencies.
+ */
+
+struct vo_priv {
+ struct mpv_render_context *ctx; // immutable after init
+};
+
+struct mpv_render_context {
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct mp_client_api *client_api;
+
+ atomic_bool in_use;
+
+ // --- Immutable after init
+ struct mp_dispatch_queue *dispatch;
+ bool advanced_control;
+ struct dr_helper *dr; // NULL if advanced_control disabled
+
+ mp_mutex control_lock;
+ // --- Protected by control_lock
+ mp_render_cb_control_fn control_cb;
+ void *control_cb_ctx;
+
+ mp_mutex update_lock;
+ mp_cond update_cond; // paired with update_lock
+
+ // --- Protected by update_lock
+ mpv_render_update_fn update_cb;
+ void *update_cb_ctx;
+
+ mp_mutex lock;
+ mp_cond video_wait; // paired with lock
+
+ // --- Protected by lock
+ struct vo_frame *next_frame; // next frame to draw
+ int64_t present_count; // incremented when next frame can be shown
+ int64_t expected_flip_count; // next vsync event for next_frame
+ bool redrawing; // next_frame was a redraw request
+ int64_t flip_count;
+ struct vo_frame *cur_frame;
+ struct mp_image_params img_params;
+ int vp_w, vp_h;
+ bool flip;
+ bool imgfmt_supported[IMGFMT_END - IMGFMT_START];
+ bool need_reconfig;
+ bool need_resize;
+ bool need_reset;
+ bool need_update_external;
+ struct vo *vo;
+
+ // --- Mostly immutable after init.
+ struct mp_hwdec_devices *hwdec_devs;
+
+ // --- All of these can only be accessed from mpv_render_*() API, for
+ // which the user makes sure they're called synchronized.
+ struct render_backend *renderer;
+ struct m_config_cache *vo_opts_cache;
+ struct mp_vo_opts *vo_opts;
+};
+
+const struct render_backend_fns *render_backends[] = {
+ &render_backend_gpu,
+ &render_backend_sw,
+ NULL
+};
+
+static void update(struct mpv_render_context *ctx)
+{
+ mp_mutex_lock(&ctx->update_lock);
+ if (ctx->update_cb)
+ ctx->update_cb(ctx->update_cb_ctx);
+
+ mp_cond_broadcast(&ctx->update_cond);
+ mp_mutex_unlock(&ctx->update_lock);
+}
+
+void *get_mpv_render_param(mpv_render_param *params, mpv_render_param_type type,
+ void *def)
+{
+ for (int n = 0; params && params[n].type; n++) {
+ if (params[n].type == type)
+ return params[n].data;
+ }
+ return def;
+}
+
+static void forget_frames(struct mpv_render_context *ctx, bool all)
+{
+ mp_cond_broadcast(&ctx->video_wait);
+ if (all) {
+ talloc_free(ctx->cur_frame);
+ ctx->cur_frame = NULL;
+ }
+}
+
+static void dispatch_wakeup(void *ptr)
+{
+ struct mpv_render_context *ctx = ptr;
+
+ update(ctx);
+}
+
+static struct mp_image *render_get_image(void *ptr, int imgfmt, int w, int h,
+ int stride_align, int flags)
+{
+ struct mpv_render_context *ctx = ptr;
+
+ return ctx->renderer->fns->get_image(ctx->renderer, imgfmt, w, h, stride_align, flags);
+}
+
+int mpv_render_context_create(mpv_render_context **res, mpv_handle *mpv,
+ mpv_render_param *params)
+{
+ mpv_render_context *ctx = talloc_zero(NULL, mpv_render_context);
+ mp_mutex_init(&ctx->control_lock);
+ mp_mutex_init(&ctx->lock);
+ mp_mutex_init(&ctx->update_lock);
+ mp_cond_init(&ctx->update_cond);
+ mp_cond_init(&ctx->video_wait);
+
+ ctx->global = mp_client_get_global(mpv);
+ ctx->client_api = ctx->global->client_api;
+ ctx->log = mp_log_new(ctx, ctx->global->log, "libmpv_render");
+
+ ctx->vo_opts_cache = m_config_cache_alloc(ctx, ctx->global, &vo_sub_opts);
+ ctx->vo_opts = ctx->vo_opts_cache->opts;
+
+ ctx->dispatch = mp_dispatch_create(ctx);
+ mp_dispatch_set_wakeup_fn(ctx->dispatch, dispatch_wakeup, ctx);
+
+ if (GET_MPV_RENDER_PARAM(params, MPV_RENDER_PARAM_ADVANCED_CONTROL, int, 0))
+ ctx->advanced_control = true;
+
+ int err = MPV_ERROR_NOT_IMPLEMENTED;
+ for (int n = 0; render_backends[n]; n++) {
+ ctx->renderer = talloc_zero(NULL, struct render_backend);
+ *ctx->renderer = (struct render_backend){
+ .global = ctx->global,
+ .log = ctx->log,
+ .fns = render_backends[n],
+ };
+ err = ctx->renderer->fns->init(ctx->renderer, params);
+ if (err >= 0)
+ break;
+ ctx->renderer->fns->destroy(ctx->renderer);
+ talloc_free(ctx->renderer->priv);
+ TA_FREEP(&ctx->renderer);
+ if (err != MPV_ERROR_NOT_IMPLEMENTED)
+ break;
+ }
+
+ if (err < 0) {
+ mpv_render_context_free(ctx);
+ return err;
+ }
+
+ ctx->hwdec_devs = ctx->renderer->hwdec_devs;
+
+ for (int n = IMGFMT_START; n < IMGFMT_END; n++) {
+ ctx->imgfmt_supported[n - IMGFMT_START] =
+ ctx->renderer->fns->check_format(ctx->renderer, n);
+ }
+
+ if (ctx->renderer->fns->get_image && ctx->advanced_control)
+ ctx->dr = dr_helper_create(ctx->dispatch, render_get_image, ctx);
+
+ if (!mp_set_main_render_context(ctx->client_api, ctx, true)) {
+ MP_ERR(ctx, "There is already a mpv_render_context set.\n");
+ mpv_render_context_free(ctx);
+ return MPV_ERROR_GENERIC;
+ }
+
+ *res = ctx;
+ return 0;
+}
+
+void mpv_render_context_set_update_callback(mpv_render_context *ctx,
+ mpv_render_update_fn callback,
+ void *callback_ctx)
+{
+ mp_mutex_lock(&ctx->update_lock);
+ ctx->update_cb = callback;
+ ctx->update_cb_ctx = callback_ctx;
+ if (ctx->update_cb)
+ ctx->update_cb(ctx->update_cb_ctx);
+ mp_mutex_unlock(&ctx->update_lock);
+}
+
+void mp_render_context_set_control_callback(mpv_render_context *ctx,
+ mp_render_cb_control_fn callback,
+ void *callback_ctx)
+{
+ mp_mutex_lock(&ctx->control_lock);
+ ctx->control_cb = callback;
+ ctx->control_cb_ctx = callback_ctx;
+ mp_mutex_unlock(&ctx->control_lock);
+}
+
+void mpv_render_context_free(mpv_render_context *ctx)
+{
+ if (!ctx)
+ return;
+
+ // From here on, ctx becomes invisible and cannot be newly acquired. Only
+ // a VO could still hold a reference.
+ mp_set_main_render_context(ctx->client_api, ctx, false);
+
+ if (atomic_load(&ctx->in_use)) {
+ // Start destroy the VO, and also bring down the decoder etc., which
+ // still might be using the hwdec context or use DR images. The above
+ // mp_set_main_render_context() call guarantees it can't come back (so
+ // ctx->vo can't change to non-NULL).
+ // In theory, this races with vo_libmpv exiting and another VO being
+ // used, which is a harmless grotesque corner case.
+ kill_video_async(ctx->client_api);
+
+ while (atomic_load(&ctx->in_use)) {
+ // As a nasty detail, we need to wait until the VO is released, but
+ // also need to react to update() calls during it (the update calls
+ // are supposed to trigger processing ctx->dispatch). We solve this
+ // by making the VO uninit function call mp_dispatch_interrupt().
+ //
+ // Other than that, processing ctx->dispatch is needed to serve the
+ // video decoder, which might still not be fully destroyed, and e.g.
+ // performs calls to release DR images (or, as a grotesque corner
+ // case may even try to allocate new ones).
+ //
+ // Once the VO is released, ctx->dispatch becomes truly inactive.
+ // (The libmpv API user could call mpv_render_context_update() while
+ // mpv_render_context_free() is being called, but of course this is
+ // invalid.)
+ mp_dispatch_queue_process(ctx->dispatch, INFINITY);
+ }
+ }
+
+ mp_mutex_lock(&ctx->lock);
+ // Barrier - guarantee uninit() has left the lock region. It will access ctx
+ // until the lock has been released, so we must not proceed with destruction
+ // before we can acquire the lock. (The opposite, uninit() acquiring the
+ // lock, can not happen anymore at this point - we've waited for VO uninit,
+ // and prevented that new VOs can be created.)
+ mp_mutex_unlock(&ctx->lock);
+
+ assert(!atomic_load(&ctx->in_use));
+ assert(!ctx->vo);
+
+ // With the dispatch queue not being served anymore, allow frame free
+ // requests from this thread to be served directly.
+ if (ctx->dr)
+ dr_helper_acquire_thread(ctx->dr);
+
+ // Possibly remaining outstanding work.
+ mp_dispatch_queue_process(ctx->dispatch, 0);
+
+ forget_frames(ctx, true);
+
+ if (ctx->renderer) {
+ ctx->renderer->fns->destroy(ctx->renderer);
+ talloc_free(ctx->renderer->priv);
+ talloc_free(ctx->renderer);
+ }
+ talloc_free(ctx->dr);
+ talloc_free(ctx->dispatch);
+
+ mp_cond_destroy(&ctx->update_cond);
+ mp_cond_destroy(&ctx->video_wait);
+ mp_mutex_destroy(&ctx->update_lock);
+ mp_mutex_destroy(&ctx->lock);
+ mp_mutex_destroy(&ctx->control_lock);
+
+ talloc_free(ctx);
+}
+
+// Try to mark the context as "in exclusive use" (e.g. by a VO).
+// Note: the function must not acquire any locks, because it's called with an
+// external leaf lock held.
+bool mp_render_context_acquire(mpv_render_context *ctx)
+{
+ bool prev = false;
+ return atomic_compare_exchange_strong(&ctx->in_use, &prev, true);
+}
+
+int mpv_render_context_render(mpv_render_context *ctx, mpv_render_param *params)
+{
+ mp_mutex_lock(&ctx->lock);
+
+ int do_render =
+ !GET_MPV_RENDER_PARAM(params, MPV_RENDER_PARAM_SKIP_RENDERING, int, 0);
+
+ if (do_render) {
+ int vp_w, vp_h;
+ int err = ctx->renderer->fns->get_target_size(ctx->renderer, params,
+ &vp_w, &vp_h);
+ if (err < 0) {
+ mp_mutex_unlock(&ctx->lock);
+ return err;
+ }
+
+ if (ctx->vo && (ctx->vp_w != vp_w || ctx->vp_h != vp_h ||
+ ctx->need_resize))
+ {
+ ctx->vp_w = vp_w;
+ ctx->vp_h = vp_h;
+
+ m_config_cache_update(ctx->vo_opts_cache);
+
+ struct mp_rect src, dst;
+ struct mp_osd_res osd;
+ mp_get_src_dst_rects(ctx->log, ctx->vo_opts, ctx->vo->driver->caps,
+ &ctx->img_params, vp_w, abs(vp_h),
+ 1.0, &src, &dst, &osd);
+
+ ctx->renderer->fns->resize(ctx->renderer, &src, &dst, &osd);
+ }
+ ctx->need_resize = false;
+ }
+
+ if (ctx->need_reconfig)
+ ctx->renderer->fns->reconfig(ctx->renderer, &ctx->img_params);
+ ctx->need_reconfig = false;
+
+ if (ctx->need_update_external)
+ ctx->renderer->fns->update_external(ctx->renderer, ctx->vo);
+ ctx->need_update_external = false;
+
+ if (ctx->need_reset) {
+ ctx->renderer->fns->reset(ctx->renderer);
+ if (ctx->cur_frame)
+ ctx->cur_frame->still = true;
+ }
+ ctx->need_reset = false;
+
+ struct vo_frame *frame = ctx->next_frame;
+ int64_t wait_present_count = ctx->present_count;
+ if (frame) {
+ ctx->next_frame = NULL;
+ if (!(frame->redraw || !frame->current))
+ wait_present_count += 1;
+ mp_cond_broadcast(&ctx->video_wait);
+ talloc_free(ctx->cur_frame);
+ ctx->cur_frame = vo_frame_ref(frame);
+ } else {
+ frame = vo_frame_ref(ctx->cur_frame);
+ if (frame)
+ frame->redraw = true;
+ MP_STATS(ctx, "glcb-noframe");
+ }
+ struct vo_frame dummy = {0};
+ if (!frame)
+ frame = &dummy;
+
+ mp_mutex_unlock(&ctx->lock);
+
+ MP_STATS(ctx, "glcb-render");
+
+ int err = 0;
+
+ if (do_render)
+ err = ctx->renderer->fns->render(ctx->renderer, params, frame);
+
+ if (frame != &dummy)
+ talloc_free(frame);
+
+ if (GET_MPV_RENDER_PARAM(params, MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME,
+ int, 1))
+ {
+ mp_mutex_lock(&ctx->lock);
+ while (wait_present_count > ctx->present_count)
+ mp_cond_wait(&ctx->video_wait, &ctx->lock);
+ mp_mutex_unlock(&ctx->lock);
+ }
+
+ return err;
+}
+
+void mpv_render_context_report_swap(mpv_render_context *ctx)
+{
+ MP_STATS(ctx, "glcb-reportflip");
+
+ mp_mutex_lock(&ctx->lock);
+ ctx->flip_count += 1;
+ mp_cond_broadcast(&ctx->video_wait);
+ mp_mutex_unlock(&ctx->lock);
+}
+
+uint64_t mpv_render_context_update(mpv_render_context *ctx)
+{
+ uint64_t res = 0;
+
+ mp_dispatch_queue_process(ctx->dispatch, 0);
+
+ mp_mutex_lock(&ctx->lock);
+ if (ctx->next_frame)
+ res |= MPV_RENDER_UPDATE_FRAME;
+ mp_mutex_unlock(&ctx->lock);
+ return res;
+}
+
+int mpv_render_context_set_parameter(mpv_render_context *ctx,
+ mpv_render_param param)
+{
+ return ctx->renderer->fns->set_parameter(ctx->renderer, param);
+}
+
+int mpv_render_context_get_info(mpv_render_context *ctx,
+ mpv_render_param param)
+{
+ int res = MPV_ERROR_NOT_IMPLEMENTED;
+ mp_mutex_lock(&ctx->lock);
+
+ switch (param.type) {
+ case MPV_RENDER_PARAM_NEXT_FRAME_INFO: {
+ mpv_render_frame_info *info = param.data;
+ *info = (mpv_render_frame_info){0};
+ struct vo_frame *frame = ctx->next_frame;
+ if (frame) {
+ info->flags =
+ MPV_RENDER_FRAME_INFO_PRESENT |
+ (frame->redraw ? MPV_RENDER_FRAME_INFO_REDRAW : 0) |
+ (frame->repeat ? MPV_RENDER_FRAME_INFO_REPEAT : 0) |
+ (frame->display_synced && !frame->redraw ?
+ MPV_RENDER_FRAME_INFO_BLOCK_VSYNC : 0);
+ info->target_time = frame->pts;
+ }
+ res = 0;
+ break;
+ }
+ default:;
+ }
+
+ mp_mutex_unlock(&ctx->lock);
+ return res;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+
+ mp_mutex_lock(&ctx->lock);
+ assert(!ctx->next_frame);
+ ctx->next_frame = vo_frame_ref(frame);
+ ctx->expected_flip_count = ctx->flip_count + 1;
+ ctx->redrawing = frame->redraw || !frame->current;
+ mp_mutex_unlock(&ctx->lock);
+
+ update(ctx);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+ int64_t until = mp_time_ns() + MP_TIME_MS_TO_NS(200);
+
+ mp_mutex_lock(&ctx->lock);
+
+ // Wait until frame was rendered
+ while (ctx->next_frame) {
+ if (mp_cond_timedwait_until(&ctx->video_wait, &ctx->lock, until)) {
+ if (ctx->next_frame) {
+ MP_VERBOSE(vo, "mpv_render_context_render() not being called "
+ "or stuck.\n");
+ goto done;
+ }
+ }
+ }
+
+ // Unblock mpv_render_context_render().
+ ctx->present_count += 1;
+ mp_cond_broadcast(&ctx->video_wait);
+
+ if (ctx->redrawing)
+ goto done; // do not block for redrawing
+
+ // Wait until frame was presented
+ while (ctx->expected_flip_count > ctx->flip_count) {
+ // mpv_render_report_swap() is declared as optional API.
+ // Assume the user calls it consistently _if_ it's called at all.
+ if (!ctx->flip_count)
+ break;
+ if (mp_cond_timedwait_until(&ctx->video_wait, &ctx->lock, until)) {
+ MP_VERBOSE(vo, "mpv_render_report_swap() not being called.\n");
+ goto done;
+ }
+ }
+
+done:
+
+ // Cleanup after the API user is not reacting, or is being unusually slow.
+ if (ctx->next_frame) {
+ talloc_free(ctx->cur_frame);
+ ctx->cur_frame = ctx->next_frame;
+ ctx->next_frame = NULL;
+ ctx->present_count += 2;
+ mp_cond_signal(&ctx->video_wait);
+ vo_increment_drop_count(vo, 1);
+ }
+
+ mp_mutex_unlock(&ctx->lock);
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+
+ bool ok = false;
+ mp_mutex_lock(&ctx->lock);
+ if (format >= IMGFMT_START && format < IMGFMT_END)
+ ok = ctx->imgfmt_supported[format - IMGFMT_START];
+ mp_mutex_unlock(&ctx->lock);
+ return ok;
+}
+
+static void run_control_on_render_thread(void *p)
+{
+ void **args = p;
+ struct mpv_render_context *ctx = args[0];
+ int request = (intptr_t)args[1];
+ void *data = args[2];
+ int ret = VO_NOTIMPL;
+
+ switch (request) {
+ case VOCTRL_SCREENSHOT: {
+ mp_mutex_lock(&ctx->lock);
+ struct vo_frame *frame = vo_frame_ref(ctx->cur_frame);
+ mp_mutex_unlock(&ctx->lock);
+ if (frame && ctx->renderer->fns->screenshot)
+ ctx->renderer->fns->screenshot(ctx->renderer, frame, data);
+ talloc_free(frame);
+ break;
+ }
+ case VOCTRL_PERFORMANCE_DATA: {
+ if (ctx->renderer->fns->perfdata) {
+ ctx->renderer->fns->perfdata(ctx->renderer, data);
+ ret = VO_TRUE;
+ }
+ break;
+ }
+ }
+
+ *(int *)args[3] = ret;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+
+ switch (request) {
+ case VOCTRL_RESET:
+ mp_mutex_lock(&ctx->lock);
+ forget_frames(ctx, false);
+ ctx->need_reset = true;
+ mp_mutex_unlock(&ctx->lock);
+ vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_PAUSE:
+ vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_SET_EQUALIZER:
+ vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_SET_PANSCAN:
+ mp_mutex_lock(&ctx->lock);
+ ctx->need_resize = true;
+ mp_mutex_unlock(&ctx->lock);
+ vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_UPDATE_RENDER_OPTS:
+ mp_mutex_lock(&ctx->lock);
+ ctx->need_update_external = true;
+ mp_mutex_unlock(&ctx->lock);
+ vo->want_redraw = true;
+ return VO_TRUE;
+ }
+
+ // VOCTRLs to be run on the renderer thread (if possible at all).
+ if (ctx->advanced_control) {
+ switch (request) {
+ case VOCTRL_SCREENSHOT:
+ case VOCTRL_PERFORMANCE_DATA: {
+ int ret;
+ void *args[] = {ctx, (void *)(intptr_t)request, data, &ret};
+ mp_dispatch_run(ctx->dispatch, run_control_on_render_thread, args);
+ return ret;
+ }
+ }
+ }
+
+ int r = VO_NOTIMPL;
+ mp_mutex_lock(&ctx->control_lock);
+ if (ctx->control_cb) {
+ int events = 0;
+ r = p->ctx->control_cb(vo, p->ctx->control_cb_ctx,
+ &events, request, data);
+ vo_event(vo, events);
+ }
+ mp_mutex_unlock(&ctx->control_lock);
+
+ return r;
+}
+
+static struct mp_image *get_image(struct vo *vo, int imgfmt, int w, int h,
+ int stride_align, int flags)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+
+ if (ctx->dr)
+ return dr_helper_get_image(ctx->dr, imgfmt, w, h, stride_align, flags);
+
+ return NULL;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+
+ mp_mutex_lock(&ctx->lock);
+ forget_frames(ctx, true);
+ ctx->img_params = *params;
+ ctx->need_reconfig = true;
+ ctx->need_resize = true;
+ mp_mutex_unlock(&ctx->lock);
+
+ control(vo, VOCTRL_RECONFIG, NULL);
+
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct vo_priv *p = vo->priv;
+ struct mpv_render_context *ctx = p->ctx;
+
+ control(vo, VOCTRL_UNINIT, NULL);
+
+ mp_mutex_lock(&ctx->lock);
+
+ forget_frames(ctx, true);
+ ctx->img_params = (struct mp_image_params){0};
+ ctx->need_reconfig = true;
+ ctx->need_resize = true;
+ ctx->need_update_external = true;
+ ctx->need_reset = true;
+ ctx->vo = NULL;
+
+ // The following do not normally need ctx->lock, however, ctx itself may
+ // become invalid once we release ctx->lock.
+ bool prev_in_use = atomic_exchange(&ctx->in_use, false);
+ assert(prev_in_use); // obviously must have been set
+ mp_dispatch_interrupt(ctx->dispatch);
+
+ mp_mutex_unlock(&ctx->lock);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct vo_priv *p = vo->priv;
+
+ struct mpv_render_context *ctx =
+ mp_client_api_acquire_render_context(vo->global->client_api);
+ p->ctx = ctx;
+
+ if (!ctx) {
+ if (!vo->probing)
+ MP_FATAL(vo, "No render context set.\n");
+ return -1;
+ }
+
+ mp_mutex_lock(&ctx->lock);
+ ctx->vo = vo;
+ ctx->need_resize = true;
+ ctx->need_update_external = true;
+ mp_mutex_unlock(&ctx->lock);
+
+ vo->hwdec_devs = ctx->hwdec_devs;
+ control(vo, VOCTRL_PREINIT, NULL);
+
+ return 0;
+}
+
+const struct vo_driver video_out_libmpv = {
+ .description = "render API for libmpv",
+ .name = "libmpv",
+ .caps = VO_CAP_ROTATE90,
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .get_image_ts = get_image,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct vo_priv),
+};
diff --git a/video/out/vo_mediacodec_embed.c b/video/out/vo_mediacodec_embed.c
new file mode 100644
index 0000000..08d3866
--- /dev/null
+++ b/video/out/vo_mediacodec_embed.c
@@ -0,0 +1,127 @@
+/*
+ * 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 <libavcodec/mediacodec.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_mediacodec.h>
+
+#include "common/common.h"
+#include "vo.h"
+#include "video/mp_image.h"
+#include "video/hwdec.h"
+
+struct priv {
+ struct mp_image *next_image;
+ struct mp_hwdec_ctx hwctx;
+};
+
+static AVBufferRef *create_mediacodec_device_ref(struct vo *vo)
+{
+ AVBufferRef *device_ref = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_MEDIACODEC);
+ if (!device_ref)
+ return NULL;
+
+ AVHWDeviceContext *ctx = (void *)device_ref->data;
+ AVMediaCodecDeviceContext *hwctx = ctx->hwctx;
+ assert(vo->opts->WinID != 0 && vo->opts->WinID != -1);
+ hwctx->surface = (void *)(intptr_t)(vo->opts->WinID);
+
+ if (av_hwdevice_ctx_init(device_ref) < 0)
+ av_buffer_unref(&device_ref);
+
+ return device_ref;
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ vo->hwdec_devs = hwdec_devices_create();
+ p->hwctx = (struct mp_hwdec_ctx){
+ .driver_name = "mediacodec_embed",
+ .av_device_ref = create_mediacodec_device_ref(vo),
+ .hw_imgfmt = IMGFMT_MEDIACODEC,
+ };
+
+ if (!p->hwctx.av_device_ref) {
+ MP_VERBOSE(vo, "Failed to create hwdevice_ctx\n");
+ return -1;
+ }
+
+ hwdec_devices_add(vo->hwdec_devs, &p->hwctx);
+ return 0;
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ if (!p->next_image)
+ return;
+
+ AVMediaCodecBuffer *buffer = (AVMediaCodecBuffer *)p->next_image->planes[3];
+ av_mediacodec_release_buffer(buffer, 1);
+ mp_image_unrefp(&p->next_image);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+
+ mp_image_t *mpi = NULL;
+ if (!frame->redraw && !frame->repeat)
+ mpi = mp_image_new_ref(frame->current);
+
+ talloc_free(p->next_image);
+ p->next_image = mpi;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return format == IMGFMT_MEDIACODEC;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ return VO_NOTIMPL;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ mp_image_unrefp(&p->next_image);
+
+ hwdec_devices_remove(vo->hwdec_devs, &p->hwctx);
+ av_buffer_unref(&p->hwctx.av_device_ref);
+}
+
+const struct vo_driver video_out_mediacodec_embed = {
+ .description = "Android (Embedded MediaCodec Surface)",
+ .name = "mediacodec_embed",
+ .caps = VO_CAP_NORETAIN,
+ .preinit = preinit,
+ .query_format = query_format,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .reconfig = reconfig,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+};
diff --git a/video/out/vo_null.c b/video/out/vo_null.c
new file mode 100644
index 0000000..0c49062
--- /dev/null
+++ b/video/out/vo_null.c
@@ -0,0 +1,104 @@
+/*
+ * based on video_out_null.c from mpeg2dec
+ *
+ * Copyright (C) Aaron Holtzman - June 2000
+ *
+ * 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 <stdlib.h>
+#include "common/msg.h"
+#include "vo.h"
+#include "video/mp_image.h"
+#include "osdep/timer.h"
+#include "options/m_option.h"
+
+struct priv {
+ int64_t last_vsync;
+
+ double cfg_fps;
+};
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ if (p->cfg_fps) {
+ int64_t ft = 1e9 / p->cfg_fps;
+ int64_t prev_vsync = mp_time_ns() / ft;
+ int64_t target_time = (prev_vsync + 1) * ft;
+ for (;;) {
+ int64_t now = mp_time_ns();
+ if (now >= target_time)
+ break;
+ mp_sleep_ns(target_time - now);
+ }
+ }
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return 1;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+}
+
+static int preinit(struct vo *vo)
+{
+ return 0;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct priv *p = vo->priv;
+ switch (request) {
+ case VOCTRL_GET_DISPLAY_FPS:
+ if (!p->cfg_fps)
+ break;
+ *(double *)data = p->cfg_fps;
+ return VO_TRUE;
+ }
+ return VO_NOTIMPL;
+}
+
+#define OPT_BASE_STRUCT struct priv
+const struct vo_driver video_out_null = {
+ .description = "Null video output",
+ .name = "null",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+ .options = (const struct m_option[]) {
+ {"fps", OPT_DOUBLE(cfg_fps), M_RANGE(0, 10000)},
+ {0},
+ },
+ .options_prefix = "vo-null",
+};
diff --git a/video/out/vo_rpi.c b/video/out/vo_rpi.c
new file mode 100644
index 0000000..55f1a68
--- /dev/null
+++ b/video/out/vo_rpi.c
@@ -0,0 +1,938 @@
+/*
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <stdbool.h>
+#include <assert.h>
+
+#include <bcm_host.h>
+#include <interface/mmal/mmal.h>
+#include <interface/mmal/util/mmal_util.h>
+#include <interface/mmal/util/mmal_default_components.h>
+#include <interface/mmal/vc/mmal_vc_api.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include <libavutil/rational.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "opengl/common.h"
+#include "options/m_config.h"
+#include "osdep/timer.h"
+#include "vo.h"
+#include "win_state.h"
+#include "video/mp_image.h"
+#include "sub/osd.h"
+
+#include "opengl/ra_gl.h"
+#include "gpu/video.h"
+
+struct mp_egl_rpi {
+ struct mp_log *log;
+ struct GL *gl;
+ struct ra *ra;
+ EGLDisplay egl_display;
+ EGLConfig egl_config;
+ EGLContext egl_context;
+ EGLSurface egl_surface;
+ // yep, the API keeps a pointer to it
+ EGL_DISPMANX_WINDOW_T egl_window;
+};
+
+struct priv {
+ DISPMANX_DISPLAY_HANDLE_T display;
+ DISPMANX_ELEMENT_HANDLE_T window;
+ DISPMANX_ELEMENT_HANDLE_T osd_overlay;
+ DISPMANX_UPDATE_HANDLE_T update;
+ uint32_t w, h;
+ uint32_t x, y;
+ double display_fps;
+
+ double osd_pts;
+ struct mp_osd_res osd_res;
+ struct m_config_cache *opts_cache;
+
+ struct mp_egl_rpi egl;
+ struct gl_video *gl_video;
+ struct mpgl_osd *osd;
+
+ MMAL_COMPONENT_T *renderer;
+ bool renderer_enabled;
+
+ bool display_synced, skip_osd;
+ struct mp_image *next_image;
+
+ // for RAM input
+ MMAL_POOL_T *swpool;
+
+ mp_mutex display_mutex;
+ mp_cond display_cond;
+ int64_t vsync_counter;
+ bool reload_display;
+
+ int background_layer;
+ int video_layer;
+ int osd_layer;
+
+ int display_nr;
+ int layer;
+ bool background;
+ bool enable_osd;
+};
+
+// Magic alignments (in pixels) expected by the MMAL internals.
+#define ALIGN_W 32
+#define ALIGN_H 16
+
+static void recreate_renderer(struct vo *vo);
+
+static void *get_proc_address(const GLubyte *name)
+{
+ void *p = eglGetProcAddress(name);
+ // EGL 1.4 (supported by the RPI firmware) does not necessarily return
+ // function pointers for core functions.
+ if (!p) {
+ void *h = dlopen("/opt/vc/lib/libbrcmGLESv2.so", RTLD_LAZY);
+ if (h) {
+ p = dlsym(h, name);
+ dlclose(h);
+ }
+ }
+ return p;
+}
+
+static EGLConfig select_fb_config_egl(struct mp_egl_rpi *p)
+{
+ EGLint attributes[] = {
+ EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
+ EGL_RED_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_BLUE_SIZE, 8,
+ EGL_DEPTH_SIZE, 0,
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+ EGL_NONE
+ };
+
+ EGLint config_count;
+ EGLConfig config;
+
+ eglChooseConfig(p->egl_display, attributes, &config, 1, &config_count);
+
+ if (!config_count) {
+ MP_FATAL(p, "Could find EGL configuration!\n");
+ return NULL;
+ }
+
+ return config;
+}
+
+static void mp_egl_rpi_destroy(struct mp_egl_rpi *p)
+{
+ if (p->egl_display) {
+ eglMakeCurrent(p->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT);
+ }
+ if (p->egl_surface)
+ eglDestroySurface(p->egl_display, p->egl_surface);
+ if (p->egl_context)
+ eglDestroyContext(p->egl_display, p->egl_context);
+ p->egl_context = EGL_NO_CONTEXT;
+ eglReleaseThread();
+ p->egl_display = EGL_NO_DISPLAY;
+ talloc_free(p->gl);
+ p->gl = NULL;
+}
+
+static int mp_egl_rpi_init(struct mp_egl_rpi *p, DISPMANX_ELEMENT_HANDLE_T window,
+ int w, int h)
+{
+ p->egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ if (!eglInitialize(p->egl_display, NULL, NULL)) {
+ MP_FATAL(p, "EGL failed to initialize.\n");
+ goto fail;
+ }
+
+ eglBindAPI(EGL_OPENGL_ES_API);
+
+ EGLConfig config = select_fb_config_egl(p);
+ if (!config)
+ goto fail;
+
+ p->egl_window = (EGL_DISPMANX_WINDOW_T){
+ .element = window,
+ .width = w,
+ .height = h,
+ };
+ p->egl_surface = eglCreateWindowSurface(p->egl_display, config,
+ &p->egl_window, NULL);
+
+ if (p->egl_surface == EGL_NO_SURFACE) {
+ MP_FATAL(p, "Could not create EGL surface!\n");
+ goto fail;
+ }
+
+ EGLint context_attributes[] = {
+ EGL_CONTEXT_CLIENT_VERSION, 2,
+ EGL_NONE
+ };
+ p->egl_context = eglCreateContext(p->egl_display, config,
+ EGL_NO_CONTEXT, context_attributes);
+
+ if (p->egl_context == EGL_NO_CONTEXT) {
+ MP_FATAL(p, "Could not create EGL context!\n");
+ goto fail;
+ }
+
+ eglMakeCurrent(p->egl_display, p->egl_surface, p->egl_surface,
+ p->egl_context);
+
+ p->gl = talloc_zero(NULL, struct GL);
+
+ const char *exts = eglQueryString(p->egl_display, EGL_EXTENSIONS);
+ mpgl_load_functions(p->gl, get_proc_address, exts, p->log);
+
+ if (!p->gl->version && !p->gl->es)
+ goto fail;
+
+ p->ra = ra_create_gl(p->gl, p->log);
+ if (!p->ra)
+ goto fail;
+
+ return 0;
+
+fail:
+ mp_egl_rpi_destroy(p);
+ return -1;
+}
+
+// Make mpi point to buffer, assuming MMAL_ENCODING_I420.
+// buffer can be NULL.
+// Return the required buffer space.
+static size_t layout_buffer(struct mp_image *mpi, MMAL_BUFFER_HEADER_T *buffer,
+ struct mp_image_params *params)
+{
+ assert(params->imgfmt == IMGFMT_420P);
+ mp_image_set_params(mpi, params);
+ int w = MP_ALIGN_UP(params->w, ALIGN_W);
+ int h = MP_ALIGN_UP(params->h, ALIGN_H);
+ uint8_t *cur = buffer ? buffer->data : NULL;
+ size_t size = 0;
+ for (int i = 0; i < 3; i++) {
+ int div = i ? 2 : 1;
+ mpi->planes[i] = cur;
+ mpi->stride[i] = w / div;
+ size_t plane_size = h / div * mpi->stride[i];
+ if (cur)
+ cur += plane_size;
+ size += plane_size;
+ }
+ return size;
+}
+
+static void update_osd(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ if (!p->enable_osd)
+ return;
+
+ if (!gl_video_check_osd_change(p->gl_video, &p->osd_res, p->osd_pts)) {
+ p->skip_osd = true;
+ return;
+ }
+
+ MP_STATS(vo, "start rpi_osd");
+
+ struct vo_frame frame = {0};
+ struct ra_fbo target = {
+ .tex = ra_create_wrapped_fb(p->egl.ra, 0, p->osd_res.w, p->osd_res.h),
+ .flip = true,
+ };
+ gl_video_set_osd_pts(p->gl_video, p->osd_pts);
+ gl_video_render_frame(p->gl_video, &frame, target, RENDER_FRAME_DEF);
+ ra_tex_free(p->egl.ra, &target.tex);
+
+ MP_STATS(vo, "stop rpi_osd");
+}
+
+static void resize(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ MMAL_PORT_T *input = p->renderer->input[0];
+
+ struct mp_rect src, dst;
+
+ vo_get_src_dst_rects(vo, &src, &dst, &p->osd_res);
+
+ int rotate[] = {MMAL_DISPLAY_ROT0,
+ MMAL_DISPLAY_ROT90,
+ MMAL_DISPLAY_ROT180,
+ MMAL_DISPLAY_ROT270};
+
+
+ int src_w = src.x1 - src.x0, src_h = src.y1 - src.y0,
+ dst_w = dst.x1 - dst.x0, dst_h = dst.y1 - dst.y0;
+ int p_x, p_y;
+ av_reduce(&p_x, &p_y, dst_w * src_h, src_w * dst_h, 16000);
+ MMAL_DISPLAYREGION_T dr = {
+ .hdr = { .id = MMAL_PARAMETER_DISPLAYREGION,
+ .size = sizeof(MMAL_DISPLAYREGION_T), },
+ .src_rect = { .x = src.x0, .y = src.y0, .width = src_w, .height = src_h },
+ .dest_rect = { .x = dst.x0 + p->x, .y = dst.y0 + p->y,
+ .width = dst_w, .height = dst_h },
+ .layer = p->video_layer,
+ .display_num = p->display_nr,
+ .pixel_x = p_x,
+ .pixel_y = p_y,
+ .transform = rotate[vo->params ? vo->params->rotate / 90 : 0],
+ .fullscreen = vo->opts->fullscreen,
+ .set = MMAL_DISPLAY_SET_SRC_RECT | MMAL_DISPLAY_SET_DEST_RECT |
+ MMAL_DISPLAY_SET_LAYER | MMAL_DISPLAY_SET_NUM |
+ MMAL_DISPLAY_SET_PIXEL | MMAL_DISPLAY_SET_TRANSFORM |
+ MMAL_DISPLAY_SET_FULLSCREEN,
+ };
+
+ if (vo->params && (vo->params->rotate % 180) == 90) {
+ MPSWAP(int, dr.src_rect.x, dr.src_rect.y);
+ MPSWAP(int, dr.src_rect.width, dr.src_rect.height);
+ }
+
+ if (mmal_port_parameter_set(input, &dr.hdr))
+ MP_WARN(vo, "could not set video rectangle\n");
+
+ if (p->gl_video)
+ gl_video_resize(p->gl_video, &src, &dst, &p->osd_res);
+}
+
+static void destroy_overlays(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (p->window)
+ vc_dispmanx_element_remove(p->update, p->window);
+ p->window = 0;
+
+ gl_video_uninit(p->gl_video);
+ p->gl_video = NULL;
+ ra_free(&p->egl.ra);
+ mp_egl_rpi_destroy(&p->egl);
+
+ if (p->osd_overlay)
+ vc_dispmanx_element_remove(p->update, p->osd_overlay);
+ p->osd_overlay = 0;
+}
+
+static int update_display_size(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ uint32_t n_w = 0, n_h = 0;
+ if (graphics_get_display_size(0, &n_w, &n_h) < 0) {
+ MP_FATAL(vo, "Could not get display size.\n");
+ return -1;
+ }
+
+ if (p->w == n_w && p->h == n_h)
+ return 0;
+
+ p->w = n_w;
+ p->h = n_h;
+
+ MP_VERBOSE(vo, "Display size: %dx%d\n", p->w, p->h);
+
+ return 0;
+}
+
+static int create_overlays(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ destroy_overlays(vo);
+
+ if (!p->display)
+ return -1;
+
+ if (vo->opts->fullscreen && p->background) {
+ // Use the whole screen.
+ VC_RECT_T dst = {.width = p->w, .height = p->h};
+ VC_RECT_T src = {.width = 1 << 16, .height = 1 << 16};
+ VC_DISPMANX_ALPHA_T alpha = {
+ .flags = DISPMANX_FLAGS_ALPHA_FIXED_ALL_PIXELS,
+ .opacity = 0xFF,
+ };
+
+ p->window = vc_dispmanx_element_add(p->update, p->display,
+ p->background_layer,
+ &dst, 0, &src,
+ DISPMANX_PROTECTION_NONE,
+ &alpha, 0, 0);
+ if (!p->window) {
+ MP_FATAL(vo, "Could not add DISPMANX element.\n");
+ return -1;
+ }
+ }
+
+ if (p->enable_osd) {
+ VC_RECT_T dst = {.x = p->x, .y = p->y,
+ .width = p->osd_res.w, .height = p->osd_res.h};
+ VC_RECT_T src = {.width = p->osd_res.w << 16, .height = p->osd_res.h << 16};
+ VC_DISPMANX_ALPHA_T alpha = {
+ .flags = DISPMANX_FLAGS_ALPHA_FROM_SOURCE,
+ .opacity = 0xFF,
+ };
+ p->osd_overlay = vc_dispmanx_element_add(p->update, p->display,
+ p->osd_layer,
+ &dst, 0, &src,
+ DISPMANX_PROTECTION_NONE,
+ &alpha, 0, 0);
+ if (!p->osd_overlay) {
+ MP_FATAL(vo, "Could not add DISPMANX element.\n");
+ return -1;
+ }
+
+ if (mp_egl_rpi_init(&p->egl, p->osd_overlay,
+ p->osd_res.w, p->osd_res.h) < 0)
+ {
+ MP_FATAL(vo, "EGL/GLES initialization for OSD renderer failed.\n");
+ return -1;
+ }
+ p->gl_video = gl_video_init(p->egl.ra, vo->log, vo->global);
+ gl_video_set_clear_color(p->gl_video, (struct m_color){.a = 0});
+ gl_video_set_osd_source(p->gl_video, vo->osd);
+ }
+
+ p->display_fps = 0;
+ TV_GET_STATE_RESP_T tvstate;
+ TV_DISPLAY_STATE_T tvstate_disp;
+ if (!vc_tv_get_state(&tvstate) && !vc_tv_get_display_state(&tvstate_disp)) {
+ if (tvstate_disp.state & (VC_HDMI_HDMI | VC_HDMI_DVI)) {
+ p->display_fps = tvstate_disp.display.hdmi.frame_rate;
+
+ HDMI_PROPERTY_PARAM_T param = {
+ .property = HDMI_PROPERTY_PIXEL_CLOCK_TYPE,
+ };
+ if (!vc_tv_hdmi_get_property(&param) &&
+ param.param1 == HDMI_PIXEL_CLOCK_TYPE_NTSC)
+ p->display_fps = p->display_fps / 1.001;
+ } else {
+ p->display_fps = tvstate_disp.display.sdtv.frame_rate;
+ }
+ }
+
+ resize(vo);
+
+ vo_event(vo, VO_EVENT_WIN_STATE);
+
+ vc_dispmanx_update_submit_sync(p->update);
+ p->update = vc_dispmanx_update_start(10);
+
+ return 0;
+}
+
+static int set_geometry(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (vo->opts->fullscreen) {
+ vo->dwidth = p->w;
+ vo->dheight = p->h;
+ p->x = p->y = 0;
+ } else {
+ struct vo_win_geometry geo;
+ struct mp_rect screenrc = {0, 0, p->w, p->h};
+
+ vo_calc_window_geometry(vo, &screenrc, &geo);
+ vo_apply_window_geometry(vo, &geo);
+
+ p->x = geo.win.x0;
+ p->y = geo.win.y0;
+ }
+
+ resize(vo);
+
+ if (create_overlays(vo) < 0)
+ return -1;
+
+ return 0;
+}
+
+static void wait_next_vsync(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ mp_mutex_lock(&p->display_mutex);
+ int64_t end = mp_time_ns() + MP_TIME_MS_TO_NS(50);
+ int64_t old = p->vsync_counter;
+ while (old == p->vsync_counter && !p->reload_display) {
+ if (mp_cond_timedwait_until(&p->display_cond, &p->display_mutex, end))
+ break;
+ }
+ mp_mutex_unlock(&p->display_mutex);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (!p->renderer_enabled)
+ return;
+
+ struct mp_image *mpi = p->next_image;
+ p->next_image = NULL;
+
+ // For OSD
+ if (!p->skip_osd && p->egl.gl)
+ eglSwapBuffers(p->egl.egl_display, p->egl.egl_surface);
+ p->skip_osd = false;
+
+ if (mpi) {
+ MMAL_PORT_T *input = p->renderer->input[0];
+ MMAL_BUFFER_HEADER_T *ref = (void *)mpi->planes[3];
+
+ // Assume this field is free for use by us.
+ ref->user_data = mpi;
+
+ if (mmal_port_send_buffer(input, ref)) {
+ MP_ERR(vo, "could not queue picture!\n");
+ talloc_free(mpi);
+ }
+ }
+
+ if (p->display_synced)
+ wait_next_vsync(vo);
+}
+
+static void free_mmal_buffer(void *arg)
+{
+ MMAL_BUFFER_HEADER_T *buffer = arg;
+ mmal_buffer_header_release(buffer);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+
+ if (!p->renderer_enabled)
+ return;
+
+ mp_image_t *mpi = NULL;
+ if (!frame->redraw && !frame->repeat)
+ mpi = mp_image_new_ref(frame->current);
+
+ talloc_free(p->next_image);
+ p->next_image = NULL;
+
+ if (mpi)
+ p->osd_pts = mpi->pts;
+
+ // Redraw only if the OSD has meaningfully changed, which we assume it
+ // hasn't when a frame is merely repeated for display sync.
+ p->skip_osd = !frame->redraw && frame->repeat;
+
+ if (!p->skip_osd && p->egl.gl)
+ update_osd(vo);
+
+ p->display_synced = frame->display_synced;
+
+ if (mpi && mpi->imgfmt != IMGFMT_MMAL) {
+ MMAL_BUFFER_HEADER_T *buffer = mmal_queue_wait(p->swpool->queue);
+ if (!buffer) {
+ talloc_free(mpi);
+ MP_ERR(vo, "Can't allocate buffer.\n");
+ return;
+ }
+ mmal_buffer_header_reset(buffer);
+
+ struct mp_image *new_ref = mp_image_new_custom_ref(NULL, buffer,
+ free_mmal_buffer);
+ if (!new_ref) {
+ mmal_buffer_header_release(buffer);
+ talloc_free(mpi);
+ MP_ERR(vo, "Out of memory.\n");
+ return;
+ }
+
+ mp_image_setfmt(new_ref, IMGFMT_MMAL);
+ new_ref->planes[3] = (void *)buffer;
+
+ struct mp_image dmpi = {0};
+ buffer->length = layout_buffer(&dmpi, buffer, vo->params);
+ mp_image_copy(&dmpi, mpi);
+
+ talloc_free(mpi);
+ mpi = new_ref;
+ }
+
+ p->next_image = mpi;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return format == IMGFMT_MMAL || format == IMGFMT_420P;
+}
+
+static MMAL_FOURCC_T map_csp(enum mp_csp csp)
+{
+ switch (csp) {
+ case MP_CSP_BT_601: return MMAL_COLOR_SPACE_ITUR_BT601;
+ case MP_CSP_BT_709: return MMAL_COLOR_SPACE_ITUR_BT709;
+ case MP_CSP_SMPTE_240M: return MMAL_COLOR_SPACE_SMPTE240M;
+ default: return MMAL_COLOR_SPACE_UNKNOWN;
+ }
+}
+
+static void control_port_cb(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer)
+{
+ mmal_buffer_header_release(buffer);
+}
+
+static void input_port_cb(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer)
+{
+ struct mp_image *mpi = buffer->user_data;
+ talloc_free(mpi);
+}
+
+static void disable_renderer(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (p->renderer_enabled) {
+ mmal_port_disable(p->renderer->control);
+ mmal_port_disable(p->renderer->input[0]);
+
+ mmal_port_flush(p->renderer->control);
+ mmal_port_flush(p->renderer->input[0]);
+
+ mmal_component_disable(p->renderer);
+ }
+ mmal_pool_destroy(p->swpool);
+ p->swpool = NULL;
+ p->renderer_enabled = false;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *p = vo->priv;
+ MMAL_PORT_T *input = p->renderer->input[0];
+ bool opaque = params->imgfmt == IMGFMT_MMAL;
+
+ if (!p->display)
+ return -1;
+
+ disable_renderer(vo);
+
+ input->format->encoding = opaque ? MMAL_ENCODING_OPAQUE : MMAL_ENCODING_I420;
+ input->format->es->video.width = MP_ALIGN_UP(params->w, ALIGN_W);
+ input->format->es->video.height = MP_ALIGN_UP(params->h, ALIGN_H);
+ input->format->es->video.crop = (MMAL_RECT_T){0, 0, params->w, params->h};
+ input->format->es->video.par = (MMAL_RATIONAL_T){params->p_w, params->p_h};
+ input->format->es->video.color_space = map_csp(params->color.space);
+
+ if (mmal_port_format_commit(input))
+ return -1;
+
+ input->buffer_num = MPMAX(input->buffer_num_min,
+ input->buffer_num_recommended) + 3;
+ input->buffer_size = MPMAX(input->buffer_size_min,
+ input->buffer_size_recommended);
+
+ if (!opaque) {
+ size_t size = layout_buffer(&(struct mp_image){0}, NULL, params);
+ if (input->buffer_size != size) {
+ MP_FATAL(vo, "We disagree with MMAL about buffer sizes.\n");
+ return -1;
+ }
+
+ p->swpool = mmal_pool_create(input->buffer_num, input->buffer_size);
+ if (!p->swpool) {
+ MP_FATAL(vo, "Could not allocate buffer pool.\n");
+ return -1;
+ }
+ }
+
+ if (set_geometry(vo) < 0)
+ return -1;
+
+ p->renderer_enabled = true;
+
+ if (mmal_port_enable(p->renderer->control, control_port_cb))
+ return -1;
+
+ if (mmal_port_enable(input, input_port_cb))
+ return -1;
+
+ if (mmal_component_enable(p->renderer)) {
+ MP_FATAL(vo, "Failed to enable video renderer.\n");
+ return -1;
+ }
+
+ resize(vo);
+
+ return 0;
+}
+
+static struct mp_image *take_screenshot(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (!p->display)
+ return NULL;
+
+ struct mp_image *img = mp_image_alloc(IMGFMT_BGR0, p->w, p->h);
+ if (!img)
+ return NULL;
+
+ DISPMANX_RESOURCE_HANDLE_T resource =
+ vc_dispmanx_resource_create(VC_IMAGE_ARGB8888,
+ img->w | ((img->w * 4) << 16), img->h,
+ &(int32_t){0});
+ if (!resource)
+ goto fail;
+
+ if (vc_dispmanx_snapshot(p->display, resource, 0))
+ goto fail;
+
+ VC_RECT_T rc = {.width = img->w, .height = img->h};
+ if (vc_dispmanx_resource_read_data(resource, &rc, img->planes[0], img->stride[0]))
+ goto fail;
+
+ vc_dispmanx_resource_delete(resource);
+ return img;
+
+fail:
+ vc_dispmanx_resource_delete(resource);
+ talloc_free(img);
+ return NULL;
+}
+
+static void set_fullscreen(struct vo *vo) {
+ struct priv *p = vo->priv;
+
+ if (p->renderer_enabled)
+ set_geometry(vo);
+ vo->want_redraw = true;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct priv *p = vo->priv;
+
+ switch (request) {
+ case VOCTRL_VO_OPTS_CHANGED: {
+ void *opt;
+ while (m_config_cache_get_next_changed(p->opts_cache, &opt)) {
+ struct mp_vo_opts *opts = p->opts_cache->opts;
+ if (&opts->fullscreen == opt)
+ set_fullscreen(vo);
+ }
+ return VO_TRUE;
+ }
+ case VOCTRL_SET_PANSCAN:
+ if (p->renderer_enabled)
+ resize(vo);
+ vo->want_redraw = true;
+ return VO_TRUE;
+ case VOCTRL_REDRAW_FRAME:
+ update_osd(vo);
+ return VO_TRUE;
+ case VOCTRL_SCREENSHOT_WIN:
+ *(struct mp_image **)data = take_screenshot(vo);
+ return VO_TRUE;
+ case VOCTRL_CHECK_EVENTS: {
+ mp_mutex_lock(&p->display_mutex);
+ bool reload_required = p->reload_display;
+ p->reload_display = false;
+ mp_mutex_unlock(&p->display_mutex);
+ if (reload_required)
+ recreate_renderer(vo);
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_FPS:
+ *(double *)data = p->display_fps;
+ return VO_TRUE;
+ case VOCTRL_GET_DISPLAY_RES:
+ ((int *)data)[0] = p->w;
+ ((int *)data)[1] = p->h;
+ return VO_TRUE;
+ }
+
+ return VO_NOTIMPL;
+}
+
+static void tv_callback(void *callback_data, uint32_t reason, uint32_t param1,
+ uint32_t param2)
+{
+ struct vo *vo = callback_data;
+ struct priv *p = vo->priv;
+ mp_mutex_lock(&p->display_mutex);
+ p->reload_display = true;
+ mp_cond_signal(&p->display_cond);
+ mp_mutex_unlock(&p->display_mutex);
+ vo_wakeup(vo);
+}
+
+static void vsync_callback(DISPMANX_UPDATE_HANDLE_T u, void *arg)
+{
+ struct vo *vo = arg;
+ struct priv *p = vo->priv;
+ mp_mutex_lock(&p->display_mutex);
+ p->vsync_counter += 1;
+ mp_cond_signal(&p->display_cond);
+ mp_mutex_unlock(&p->display_mutex);
+}
+
+static void destroy_dispmanx(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ disable_renderer(vo);
+ destroy_overlays(vo);
+
+ if (p->update)
+ vc_dispmanx_update_submit_sync(p->update);
+ p->update = 0;
+
+ if (p->display) {
+ vc_dispmanx_vsync_callback(p->display, NULL, NULL);
+ vc_dispmanx_display_close(p->display);
+ }
+ p->display = 0;
+}
+
+static int recreate_dispmanx(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ p->display = vc_dispmanx_display_open(p->display_nr);
+ p->update = vc_dispmanx_update_start(0);
+ if (!p->display || !p->update) {
+ MP_FATAL(vo, "Could not get DISPMANX objects.\n");
+ if (p->display)
+ vc_dispmanx_display_close(p->display);
+ p->display = 0;
+ p->update = 0;
+ return -1;
+ }
+
+ update_display_size(vo);
+
+ vc_dispmanx_vsync_callback(p->display, vsync_callback, vo);
+
+ return 0;
+}
+
+static void recreate_renderer(struct vo *vo)
+{
+ MP_WARN(vo, "Recreating renderer after display change.\n");
+
+ destroy_dispmanx(vo);
+ recreate_dispmanx(vo);
+
+ if (vo->params) {
+ if (reconfig(vo, vo->params) < 0)
+ MP_FATAL(vo, "Recreation failed.\n");
+ }
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ vc_tv_unregister_callback_full(tv_callback, vo);
+
+ talloc_free(p->next_image);
+
+ destroy_dispmanx(vo);
+
+ if (p->renderer)
+ mmal_component_release(p->renderer);
+
+ mmal_vc_deinit();
+
+ mp_cond_destroy(&p->display_cond);
+ mp_mutex_destroy(&p->display_mutex);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ p->background_layer = p->layer;
+ p->video_layer = p->layer + 1;
+ p->osd_layer = p->layer + 2;
+
+ p->egl.log = vo->log;
+
+ bcm_host_init();
+
+ if (mmal_vc_init()) {
+ MP_FATAL(vo, "Could not initialize MMAL.\n");
+ return -1;
+ }
+
+ mp_mutex_init(&p->display_mutex);
+ mp_cond_init(&p->display_cond);
+
+ p->opts_cache = m_config_cache_alloc(p, vo->global, &vo_sub_opts);
+
+ if (recreate_dispmanx(vo) < 0)
+ goto fail;
+
+ if (update_display_size(vo) < 0)
+ goto fail;
+
+ if (mmal_component_create(MMAL_COMPONENT_DEFAULT_VIDEO_RENDERER, &p->renderer))
+ {
+ MP_FATAL(vo, "Could not create MMAL renderer.\n");
+ goto fail;
+ }
+
+ vc_tv_register_callback(tv_callback, vo);
+
+ return 0;
+
+fail:
+ uninit(vo);
+ return -1;
+}
+
+#define OPT_BASE_STRUCT struct priv
+static const struct m_option options[] = {
+ {"display", OPT_INT(display_nr)},
+ {"layer", OPT_INT(layer), OPTDEF_INT(-10)},
+ {"background", OPT_BOOL(background)},
+ {"osd", OPT_BOOL(enable_osd), OPTDEF_INT(1)},
+ {0},
+};
+
+const struct vo_driver video_out_rpi = {
+ .description = "Raspberry Pi (MMAL)",
+ .name = "rpi",
+ .caps = VO_CAP_ROTATE90,
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+ .options = options,
+ .options_prefix = "rpi",
+};
diff --git a/video/out/vo_sdl.c b/video/out/vo_sdl.c
new file mode 100644
index 0000000..5f4c027
--- /dev/null
+++ b/video/out/vo_sdl.c
@@ -0,0 +1,992 @@
+/*
+ * video output driver for SDL 2.0+
+ *
+ * Copyright (C) 2012 Rudolf Polzer <divVerent@xonotic.org>
+ *
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <string.h>
+#include <time.h>
+#include <errno.h>
+#include <assert.h>
+
+#include <SDL.h>
+
+#include "input/input.h"
+#include "input/keycodes.h"
+#include "input/input.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "options/options.h"
+
+#include "osdep/timer.h"
+
+#include "sub/osd.h"
+
+#include "video/mp_image.h"
+
+#include "win_state.h"
+#include "vo.h"
+
+struct formatmap_entry {
+ Uint32 sdl;
+ unsigned int mpv;
+ int is_rgba;
+};
+const struct formatmap_entry formats[] = {
+ {SDL_PIXELFORMAT_YV12, IMGFMT_420P, 0},
+ {SDL_PIXELFORMAT_IYUV, IMGFMT_420P, 0},
+ {SDL_PIXELFORMAT_UYVY, IMGFMT_UYVY, 0},
+ //{SDL_PIXELFORMAT_YVYU, IMGFMT_YVYU, 0},
+#if BYTE_ORDER == BIG_ENDIAN
+ {SDL_PIXELFORMAT_RGB888, IMGFMT_0RGB, 0}, // RGB888 means XRGB8888
+ {SDL_PIXELFORMAT_RGBX8888, IMGFMT_RGB0, 0}, // has no alpha -> bad for OSD
+ {SDL_PIXELFORMAT_BGR888, IMGFMT_0BGR, 0}, // BGR888 means XBGR8888
+ {SDL_PIXELFORMAT_BGRX8888, IMGFMT_BGR0, 0}, // has no alpha -> bad for OSD
+ {SDL_PIXELFORMAT_ARGB8888, IMGFMT_ARGB, 1}, // matches SUBBITMAP_BGRA
+ {SDL_PIXELFORMAT_RGBA8888, IMGFMT_RGBA, 1},
+ {SDL_PIXELFORMAT_ABGR8888, IMGFMT_ABGR, 1},
+ {SDL_PIXELFORMAT_BGRA8888, IMGFMT_BGRA, 1},
+#else
+ {SDL_PIXELFORMAT_RGB888, IMGFMT_BGR0, 0}, // RGB888 means XRGB8888
+ {SDL_PIXELFORMAT_RGBX8888, IMGFMT_0BGR, 0}, // has no alpha -> bad for OSD
+ {SDL_PIXELFORMAT_BGR888, IMGFMT_RGB0, 0}, // BGR888 means XBGR8888
+ {SDL_PIXELFORMAT_BGRX8888, IMGFMT_0RGB, 0}, // has no alpha -> bad for OSD
+ {SDL_PIXELFORMAT_ARGB8888, IMGFMT_BGRA, 1}, // matches SUBBITMAP_BGRA
+ {SDL_PIXELFORMAT_RGBA8888, IMGFMT_ABGR, 1},
+ {SDL_PIXELFORMAT_ABGR8888, IMGFMT_RGBA, 1},
+ {SDL_PIXELFORMAT_BGRA8888, IMGFMT_ARGB, 1},
+#endif
+ {SDL_PIXELFORMAT_RGB24, IMGFMT_RGB24, 0},
+ {SDL_PIXELFORMAT_BGR24, IMGFMT_BGR24, 0},
+ {SDL_PIXELFORMAT_RGB565, IMGFMT_RGB565, 0},
+};
+
+struct keymap_entry {
+ SDL_Keycode sdl;
+ int mpv;
+};
+const struct keymap_entry keys[] = {
+ {SDLK_RETURN, MP_KEY_ENTER},
+ {SDLK_ESCAPE, MP_KEY_ESC},
+ {SDLK_BACKSPACE, MP_KEY_BACKSPACE},
+ {SDLK_TAB, MP_KEY_TAB},
+ {SDLK_PRINTSCREEN, MP_KEY_PRINT},
+ {SDLK_PAUSE, MP_KEY_PAUSE},
+ {SDLK_INSERT, MP_KEY_INSERT},
+ {SDLK_HOME, MP_KEY_HOME},
+ {SDLK_PAGEUP, MP_KEY_PAGE_UP},
+ {SDLK_DELETE, MP_KEY_DELETE},
+ {SDLK_END, MP_KEY_END},
+ {SDLK_PAGEDOWN, MP_KEY_PAGE_DOWN},
+ {SDLK_RIGHT, MP_KEY_RIGHT},
+ {SDLK_LEFT, MP_KEY_LEFT},
+ {SDLK_DOWN, MP_KEY_DOWN},
+ {SDLK_UP, MP_KEY_UP},
+ {SDLK_KP_ENTER, MP_KEY_KPENTER},
+ {SDLK_KP_1, MP_KEY_KP1},
+ {SDLK_KP_2, MP_KEY_KP2},
+ {SDLK_KP_3, MP_KEY_KP3},
+ {SDLK_KP_4, MP_KEY_KP4},
+ {SDLK_KP_5, MP_KEY_KP5},
+ {SDLK_KP_6, MP_KEY_KP6},
+ {SDLK_KP_7, MP_KEY_KP7},
+ {SDLK_KP_8, MP_KEY_KP8},
+ {SDLK_KP_9, MP_KEY_KP9},
+ {SDLK_KP_0, MP_KEY_KP0},
+ {SDLK_KP_PERIOD, MP_KEY_KPDEC},
+ {SDLK_POWER, MP_KEY_POWER},
+ {SDLK_MENU, MP_KEY_MENU},
+ {SDLK_STOP, MP_KEY_STOP},
+ {SDLK_MUTE, MP_KEY_MUTE},
+ {SDLK_VOLUMEUP, MP_KEY_VOLUME_UP},
+ {SDLK_VOLUMEDOWN, MP_KEY_VOLUME_DOWN},
+ {SDLK_KP_COMMA, MP_KEY_KPDEC},
+ {SDLK_AUDIONEXT, MP_KEY_NEXT},
+ {SDLK_AUDIOPREV, MP_KEY_PREV},
+ {SDLK_AUDIOSTOP, MP_KEY_STOP},
+ {SDLK_AUDIOPLAY, MP_KEY_PLAY},
+ {SDLK_AUDIOMUTE, MP_KEY_MUTE},
+ {SDLK_F1, MP_KEY_F + 1},
+ {SDLK_F2, MP_KEY_F + 2},
+ {SDLK_F3, MP_KEY_F + 3},
+ {SDLK_F4, MP_KEY_F + 4},
+ {SDLK_F5, MP_KEY_F + 5},
+ {SDLK_F6, MP_KEY_F + 6},
+ {SDLK_F7, MP_KEY_F + 7},
+ {SDLK_F8, MP_KEY_F + 8},
+ {SDLK_F9, MP_KEY_F + 9},
+ {SDLK_F10, MP_KEY_F + 10},
+ {SDLK_F11, MP_KEY_F + 11},
+ {SDLK_F12, MP_KEY_F + 12},
+ {SDLK_F13, MP_KEY_F + 13},
+ {SDLK_F14, MP_KEY_F + 14},
+ {SDLK_F15, MP_KEY_F + 15},
+ {SDLK_F16, MP_KEY_F + 16},
+ {SDLK_F17, MP_KEY_F + 17},
+ {SDLK_F18, MP_KEY_F + 18},
+ {SDLK_F19, MP_KEY_F + 19},
+ {SDLK_F20, MP_KEY_F + 20},
+ {SDLK_F21, MP_KEY_F + 21},
+ {SDLK_F22, MP_KEY_F + 22},
+ {SDLK_F23, MP_KEY_F + 23},
+ {SDLK_F24, MP_KEY_F + 24}
+};
+
+struct mousemap_entry {
+ Uint8 sdl;
+ int mpv;
+};
+const struct mousemap_entry mousebtns[] = {
+ {SDL_BUTTON_LEFT, MP_MBTN_LEFT},
+ {SDL_BUTTON_MIDDLE, MP_MBTN_MID},
+ {SDL_BUTTON_RIGHT, MP_MBTN_RIGHT},
+ {SDL_BUTTON_X1, MP_MBTN_BACK},
+ {SDL_BUTTON_X2, MP_MBTN_FORWARD},
+};
+
+struct priv {
+ SDL_Window *window;
+ SDL_Renderer *renderer;
+ int renderer_index;
+ SDL_RendererInfo renderer_info;
+ SDL_Texture *tex;
+ int tex_swapped;
+ struct mp_image_params params;
+ struct mp_rect src_rect;
+ struct mp_rect dst_rect;
+ struct mp_osd_res osd_res;
+ struct formatmap_entry osd_format;
+ struct osd_bitmap_surface {
+ int change_id;
+ struct osd_target {
+ SDL_Rect source;
+ SDL_Rect dest;
+ SDL_Texture *tex;
+ SDL_Texture *tex2;
+ } *targets;
+ int num_targets;
+ int targets_size;
+ } osd_surfaces[MAX_OSD_PARTS];
+ double osd_pts;
+ Uint32 wakeup_event;
+ bool screensaver_enabled;
+ struct m_config_cache *opts_cache;
+
+ // options
+ bool allow_sw;
+ bool switch_mode;
+ bool vsync;
+};
+
+static bool lock_texture(struct vo *vo, struct mp_image *texmpi)
+{
+ struct priv *vc = vo->priv;
+ *texmpi = (struct mp_image){0};
+ mp_image_set_size(texmpi, vc->params.w, vc->params.h);
+ mp_image_setfmt(texmpi, vc->params.imgfmt);
+ switch (texmpi->num_planes) {
+ case 1:
+ case 3:
+ break;
+ default:
+ MP_ERR(vo, "Invalid plane count\n");
+ return false;
+ }
+ void *pixels;
+ int pitch;
+ if (SDL_LockTexture(vc->tex, NULL, &pixels, &pitch)) {
+ MP_ERR(vo, "SDL_LockTexture failed\n");
+ return false;
+ }
+ texmpi->planes[0] = pixels;
+ texmpi->stride[0] = pitch;
+ if (texmpi->num_planes == 3) {
+ if (vc->tex_swapped) {
+ texmpi->planes[2] =
+ ((Uint8 *) texmpi->planes[0] + texmpi->h * pitch);
+ texmpi->stride[2] = pitch / 2;
+ texmpi->planes[1] =
+ ((Uint8 *) texmpi->planes[2] + (texmpi->h * pitch) / 4);
+ texmpi->stride[1] = pitch / 2;
+ } else {
+ texmpi->planes[1] =
+ ((Uint8 *) texmpi->planes[0] + texmpi->h * pitch);
+ texmpi->stride[1] = pitch / 2;
+ texmpi->planes[2] =
+ ((Uint8 *) texmpi->planes[1] + (texmpi->h * pitch) / 4);
+ texmpi->stride[2] = pitch / 2;
+ }
+ }
+ return true;
+}
+
+static bool is_good_renderer(SDL_RendererInfo *ri,
+ const char *driver_name_wanted, bool allow_sw,
+ struct formatmap_entry *osd_format)
+{
+ if (driver_name_wanted && driver_name_wanted[0])
+ if (strcmp(driver_name_wanted, ri->name))
+ return false;
+
+ if (!allow_sw &&
+ !(ri->flags & SDL_RENDERER_ACCELERATED))
+ return false;
+
+ int i, j;
+ for (i = 0; i < ri->num_texture_formats; ++i)
+ for (j = 0; j < sizeof(formats) / sizeof(formats[0]); ++j)
+ if (ri->texture_formats[i] == formats[j].sdl)
+ if (formats[j].is_rgba) {
+ if (osd_format)
+ *osd_format = formats[j];
+ return true;
+ }
+
+ return false;
+}
+
+static void destroy_renderer(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+
+ // free ALL the textures
+ if (vc->tex) {
+ SDL_DestroyTexture(vc->tex);
+ vc->tex = NULL;
+ }
+
+ int i, j;
+ for (i = 0; i < MAX_OSD_PARTS; ++i) {
+ for (j = 0; j < vc->osd_surfaces[i].targets_size; ++j) {
+ if (vc->osd_surfaces[i].targets[j].tex) {
+ SDL_DestroyTexture(vc->osd_surfaces[i].targets[j].tex);
+ vc->osd_surfaces[i].targets[j].tex = NULL;
+ }
+ if (vc->osd_surfaces[i].targets[j].tex2) {
+ SDL_DestroyTexture(vc->osd_surfaces[i].targets[j].tex2);
+ vc->osd_surfaces[i].targets[j].tex2 = NULL;
+ }
+ }
+ }
+
+ if (vc->renderer) {
+ SDL_DestroyRenderer(vc->renderer);
+ vc->renderer = NULL;
+ }
+}
+
+static bool try_create_renderer(struct vo *vo, int i, const char *driver)
+{
+ struct priv *vc = vo->priv;
+
+ // first probe
+ SDL_RendererInfo ri;
+ if (SDL_GetRenderDriverInfo(i, &ri))
+ return false;
+ if (!is_good_renderer(&ri, driver, vc->allow_sw, NULL))
+ return false;
+
+ vc->renderer = SDL_CreateRenderer(vc->window, i, 0);
+ if (!vc->renderer) {
+ MP_ERR(vo, "SDL_CreateRenderer failed\n");
+ return false;
+ }
+
+ if (SDL_GetRendererInfo(vc->renderer, &vc->renderer_info)) {
+ MP_ERR(vo, "SDL_GetRendererInfo failed\n");
+ destroy_renderer(vo);
+ return false;
+ }
+
+ if (!is_good_renderer(&vc->renderer_info, NULL, vc->allow_sw,
+ &vc->osd_format)) {
+ MP_ERR(vo, "Renderer '%s' does not fulfill "
+ "requirements on this system\n",
+ vc->renderer_info.name);
+ destroy_renderer(vo);
+ return false;
+ }
+
+ if (vc->renderer_index != i) {
+ MP_INFO(vo, "Using %s\n", vc->renderer_info.name);
+ vc->renderer_index = i;
+ }
+
+ return true;
+}
+
+static int init_renderer(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+
+ int n = SDL_GetNumRenderDrivers();
+ int i;
+
+ if (vc->renderer_index >= 0)
+ if (try_create_renderer(vo, vc->renderer_index, NULL))
+ return 0;
+
+ for (i = 0; i < n; ++i)
+ if (try_create_renderer(vo, i, SDL_GetHint(SDL_HINT_RENDER_DRIVER)))
+ return 0;
+
+ for (i = 0; i < n; ++i)
+ if (try_create_renderer(vo, i, NULL))
+ return 0;
+
+ MP_ERR(vo, "No supported renderer\n");
+ return -1;
+}
+
+static void resize(struct vo *vo, int w, int h)
+{
+ struct priv *vc = vo->priv;
+ vo->dwidth = w;
+ vo->dheight = h;
+ vo_get_src_dst_rects(vo, &vc->src_rect, &vc->dst_rect,
+ &vc->osd_res);
+ SDL_RenderSetLogicalSize(vc->renderer, w, h);
+ vo->want_redraw = true;
+ vo_wakeup(vo);
+}
+
+static void force_resize(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ int w, h;
+ SDL_GetWindowSize(vc->window, &w, &h);
+ resize(vo, w, h);
+}
+
+static void check_resize(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ int w, h;
+ SDL_GetWindowSize(vc->window, &w, &h);
+ if (vo->dwidth != w || vo->dheight != h)
+ resize(vo, w, h);
+}
+
+static inline void set_screensaver(bool enabled)
+{
+ if (!!enabled == !!SDL_IsScreenSaverEnabled())
+ return;
+
+ if (enabled)
+ SDL_EnableScreenSaver();
+ else
+ SDL_DisableScreenSaver();
+}
+
+static void set_fullscreen(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ struct mp_vo_opts *opts = vc->opts_cache->opts;
+ int fs = opts->fullscreen;
+ SDL_bool prev_screensaver_state = SDL_IsScreenSaverEnabled();
+
+ Uint32 fs_flag;
+ if (vc->switch_mode)
+ fs_flag = SDL_WINDOW_FULLSCREEN;
+ else
+ fs_flag = SDL_WINDOW_FULLSCREEN_DESKTOP;
+
+ Uint32 old_flags = SDL_GetWindowFlags(vc->window);
+ int prev_fs = !!(old_flags & fs_flag);
+ if (fs == prev_fs)
+ return;
+
+ Uint32 flags = 0;
+ if (fs)
+ flags |= fs_flag;
+
+ if (SDL_SetWindowFullscreen(vc->window, flags)) {
+ MP_ERR(vo, "SDL_SetWindowFullscreen failed\n");
+ return;
+ }
+
+ // toggling fullscreen might recreate the window, so better guard for this
+ set_screensaver(prev_screensaver_state);
+
+ force_resize(vo);
+}
+
+static void update_screeninfo(struct vo *vo, struct mp_rect *screenrc)
+{
+ struct priv *vc = vo->priv;
+ SDL_DisplayMode mode;
+ if (SDL_GetCurrentDisplayMode(SDL_GetWindowDisplayIndex(vc->window),
+ &mode)) {
+ MP_ERR(vo, "SDL_GetCurrentDisplayMode failed\n");
+ return;
+ }
+ *screenrc = (struct mp_rect){0, 0, mode.w, mode.h};
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *vc = vo->priv;
+
+ struct vo_win_geometry geo;
+ struct mp_rect screenrc;
+
+ update_screeninfo(vo, &screenrc);
+ vo_calc_window_geometry(vo, &screenrc, &geo);
+ vo_apply_window_geometry(vo, &geo);
+
+ int win_w = vo->dwidth;
+ int win_h = vo->dheight;
+
+ SDL_SetWindowSize(vc->window, win_w, win_h);
+ if (geo.flags & VO_WIN_FORCE_POS)
+ SDL_SetWindowPosition(vc->window, geo.win.x0, geo.win.y0);
+
+ if (vc->tex)
+ SDL_DestroyTexture(vc->tex);
+ Uint32 texfmt = SDL_PIXELFORMAT_UNKNOWN;
+ int i, j;
+ for (i = 0; i < vc->renderer_info.num_texture_formats; ++i)
+ for (j = 0; j < sizeof(formats) / sizeof(formats[0]); ++j)
+ if (vc->renderer_info.texture_formats[i] == formats[j].sdl)
+ if (params->imgfmt == formats[j].mpv)
+ texfmt = formats[j].sdl;
+ if (texfmt == SDL_PIXELFORMAT_UNKNOWN) {
+ MP_ERR(vo, "Invalid pixel format\n");
+ return -1;
+ }
+
+ vc->tex_swapped = texfmt == SDL_PIXELFORMAT_YV12;
+ vc->tex = SDL_CreateTexture(vc->renderer, texfmt,
+ SDL_TEXTUREACCESS_STREAMING,
+ params->w, params->h);
+ if (!vc->tex) {
+ MP_ERR(vo, "Could not create a texture\n");
+ return -1;
+ }
+
+ vc->params = *params;
+
+ struct mp_image tmp;
+ if (!lock_texture(vo, &tmp)) {
+ SDL_DestroyTexture(vc->tex);
+ vc->tex = NULL;
+ return -1;
+ }
+ mp_image_clear(&tmp, 0, 0, tmp.w, tmp.h);
+ SDL_UnlockTexture(vc->tex);
+
+ resize(vo, win_w, win_h);
+
+ set_screensaver(vc->screensaver_enabled);
+ set_fullscreen(vo);
+
+ SDL_ShowWindow(vc->window);
+
+ check_resize(vo);
+
+ return 0;
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ SDL_RenderPresent(vc->renderer);
+}
+
+static void wakeup(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ SDL_Event event = {.type = vc->wakeup_event};
+ // Note that there is no context - SDL is a singleton.
+ SDL_PushEvent(&event);
+}
+
+static void wait_events(struct vo *vo, int64_t until_time_ns)
+{
+ int64_t wait_ns = until_time_ns - mp_time_ns();
+ // Round-up to 1ms for short timeouts (100us, 1000us]
+ if (wait_ns > MP_TIME_US_TO_NS(100))
+ wait_ns = MPMAX(wait_ns, MP_TIME_MS_TO_NS(1));
+ int timeout_ms = MPCLAMP(wait_ns / MP_TIME_MS_TO_NS(1), 0, 10000);
+ SDL_Event ev;
+
+ while (SDL_WaitEventTimeout(&ev, timeout_ms)) {
+ timeout_ms = 0;
+ switch (ev.type) {
+ case SDL_WINDOWEVENT:
+ switch (ev.window.event) {
+ case SDL_WINDOWEVENT_EXPOSED:
+ vo->want_redraw = true;
+ break;
+ case SDL_WINDOWEVENT_SIZE_CHANGED:
+ check_resize(vo);
+ vo_event(vo, VO_EVENT_RESIZE);
+ break;
+ case SDL_WINDOWEVENT_ENTER:
+ mp_input_put_key(vo->input_ctx, MP_KEY_MOUSE_ENTER);
+ break;
+ case SDL_WINDOWEVENT_LEAVE:
+ mp_input_put_key(vo->input_ctx, MP_KEY_MOUSE_LEAVE);
+ break;
+ }
+ break;
+ case SDL_QUIT:
+ mp_input_put_key(vo->input_ctx, MP_KEY_CLOSE_WIN);
+ break;
+ case SDL_TEXTINPUT: {
+ int sdl_mod = SDL_GetModState();
+ int mpv_mod = 0;
+ // we ignore KMOD_LSHIFT, KMOD_RSHIFT and KMOD_RALT (if
+ // mp_input_use_alt_gr() is true) because these are already
+ // factored into ev.text.text
+ if (sdl_mod & (KMOD_LCTRL | KMOD_RCTRL))
+ mpv_mod |= MP_KEY_MODIFIER_CTRL;
+ if ((sdl_mod & KMOD_LALT) ||
+ ((sdl_mod & KMOD_RALT) && !mp_input_use_alt_gr(vo->input_ctx)))
+ mpv_mod |= MP_KEY_MODIFIER_ALT;
+ if (sdl_mod & (KMOD_LGUI | KMOD_RGUI))
+ mpv_mod |= MP_KEY_MODIFIER_META;
+ struct bstr t = {
+ ev.text.text, strlen(ev.text.text)
+ };
+ mp_input_put_key_utf8(vo->input_ctx, mpv_mod, t);
+ break;
+ }
+ case SDL_KEYDOWN: {
+ // Issue: we don't know in advance whether this keydown event
+ // will ALSO cause a SDL_TEXTINPUT event
+ // So we're conservative, and only map non printable keycodes
+ // (e.g. function keys, arrow keys, etc.)
+ // However, this does lose some keypresses at least on X11
+ // (e.g. Ctrl-A generates SDL_KEYDOWN only, but the key is
+ // 'a'... and 'a' is normally also handled by SDL_TEXTINPUT).
+ // The default config does not use Ctrl, so this is fine...
+ int keycode = 0;
+ int i;
+ for (i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i)
+ if (keys[i].sdl == ev.key.keysym.sym) {
+ keycode = keys[i].mpv;
+ break;
+ }
+ if (keycode) {
+ if (ev.key.keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT))
+ keycode |= MP_KEY_MODIFIER_SHIFT;
+ if (ev.key.keysym.mod & (KMOD_LCTRL | KMOD_RCTRL))
+ keycode |= MP_KEY_MODIFIER_CTRL;
+ if (ev.key.keysym.mod & (KMOD_LALT | KMOD_RALT))
+ keycode |= MP_KEY_MODIFIER_ALT;
+ if (ev.key.keysym.mod & (KMOD_LGUI | KMOD_RGUI))
+ keycode |= MP_KEY_MODIFIER_META;
+ mp_input_put_key(vo->input_ctx, keycode);
+ }
+ break;
+ }
+ case SDL_MOUSEMOTION:
+ mp_input_set_mouse_pos(vo->input_ctx, ev.motion.x, ev.motion.y);
+ break;
+ case SDL_MOUSEBUTTONDOWN: {
+ int i;
+ for (i = 0; i < sizeof(mousebtns) / sizeof(mousebtns[0]); ++i)
+ if (mousebtns[i].sdl == ev.button.button) {
+ mp_input_put_key(vo->input_ctx, mousebtns[i].mpv | MP_KEY_STATE_DOWN);
+ break;
+ }
+ break;
+ }
+ case SDL_MOUSEBUTTONUP: {
+ int i;
+ for (i = 0; i < sizeof(mousebtns) / sizeof(mousebtns[0]); ++i)
+ if (mousebtns[i].sdl == ev.button.button) {
+ mp_input_put_key(vo->input_ctx, mousebtns[i].mpv | MP_KEY_STATE_UP);
+ break;
+ }
+ break;
+ }
+ case SDL_MOUSEWHEEL: {
+#if SDL_VERSION_ATLEAST(2, 0, 4)
+ double multiplier = ev.wheel.direction == SDL_MOUSEWHEEL_FLIPPED ? -1 : 1;
+#else
+ double multiplier = 1;
+#endif
+ int y_code = ev.wheel.y > 0 ? MP_WHEEL_UP : MP_WHEEL_DOWN;
+ mp_input_put_wheel(vo->input_ctx, y_code, abs(ev.wheel.y) * multiplier);
+ int x_code = ev.wheel.x > 0 ? MP_WHEEL_RIGHT : MP_WHEEL_LEFT;
+ mp_input_put_wheel(vo->input_ctx, x_code, abs(ev.wheel.x) * multiplier);
+ break;
+ }
+ }
+ }
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ destroy_renderer(vo);
+ SDL_DestroyWindow(vc->window);
+ vc->window = NULL;
+ SDL_QuitSubSystem(SDL_INIT_VIDEO);
+ talloc_free(vc);
+}
+
+static inline void upload_to_texture(struct vo *vo, SDL_Texture *tex,
+ int w, int h, void *bitmap, int stride)
+{
+ struct priv *vc = vo->priv;
+
+ if (vc->osd_format.sdl == SDL_PIXELFORMAT_ARGB8888) {
+ // NOTE: this optimization is questionable, because SDL docs say
+ // that this way is slow.
+ // It did measure up faster, though...
+ SDL_UpdateTexture(tex, NULL, bitmap, stride);
+ return;
+ }
+
+ void *pixels;
+ int pitch;
+ if (SDL_LockTexture(tex, NULL, &pixels, &pitch)) {
+ MP_ERR(vo, "Could not lock texture\n");
+ } else {
+ SDL_ConvertPixels(w, h, SDL_PIXELFORMAT_ARGB8888,
+ bitmap, stride,
+ vc->osd_format.sdl,
+ pixels, pitch);
+ SDL_UnlockTexture(tex);
+ }
+}
+
+static inline void subbitmap_to_texture(struct vo *vo, SDL_Texture *tex,
+ struct sub_bitmap *bmp,
+ uint32_t ormask)
+{
+ if (ormask == 0) {
+ upload_to_texture(vo, tex, bmp->w, bmp->h,
+ bmp->bitmap, bmp->stride);
+ } else {
+ uint32_t *temppixels;
+ temppixels = talloc_array(vo, uint32_t, bmp->w * bmp->h);
+
+ int x, y;
+ for (y = 0; y < bmp->h; ++y) {
+ const uint32_t *src =
+ (const uint32_t *) ((const char *) bmp->bitmap + y * bmp->stride);
+ uint32_t *dst = temppixels + y * bmp->w;
+ for (x = 0; x < bmp->w; ++x)
+ dst[x] = src[x] | ormask;
+ }
+
+ upload_to_texture(vo, tex, bmp->w, bmp->h,
+ temppixels, sizeof(uint32_t) * bmp->w);
+
+ talloc_free(temppixels);
+ }
+}
+
+static void generate_osd_part(struct vo *vo, struct sub_bitmaps *imgs)
+{
+ struct priv *vc = vo->priv;
+ struct osd_bitmap_surface *sfc = &vc->osd_surfaces[imgs->render_index];
+
+ if (imgs->format == SUBBITMAP_EMPTY || imgs->num_parts == 0)
+ return;
+
+ if (imgs->change_id == sfc->change_id)
+ return;
+
+ if (imgs->num_parts > sfc->targets_size) {
+ sfc->targets = talloc_realloc(vc, sfc->targets,
+ struct osd_target, imgs->num_parts);
+ memset(&sfc->targets[sfc->targets_size], 0, sizeof(struct osd_target) *
+ (imgs->num_parts - sfc->targets_size));
+ sfc->targets_size = imgs->num_parts;
+ }
+ sfc->num_targets = imgs->num_parts;
+
+ for (int i = 0; i < imgs->num_parts; i++) {
+ struct osd_target *target = sfc->targets + i;
+ struct sub_bitmap *bmp = imgs->parts + i;
+
+ target->source = (SDL_Rect){
+ 0, 0, bmp->w, bmp->h
+ };
+ target->dest = (SDL_Rect){
+ bmp->x, bmp->y, bmp->dw, bmp->dh
+ };
+
+ // tex: alpha blended texture
+ if (target->tex) {
+ SDL_DestroyTexture(target->tex);
+ target->tex = NULL;
+ }
+ if (!target->tex)
+ target->tex = SDL_CreateTexture(vc->renderer,
+ vc->osd_format.sdl, SDL_TEXTUREACCESS_STREAMING,
+ bmp->w, bmp->h);
+ if (!target->tex) {
+ MP_ERR(vo, "Could not create texture\n");
+ }
+ if (target->tex) {
+ SDL_SetTextureBlendMode(target->tex,
+ SDL_BLENDMODE_BLEND);
+ SDL_SetTextureColorMod(target->tex, 0, 0, 0);
+ subbitmap_to_texture(vo, target->tex, bmp, 0); // RGBA -> 000A
+ }
+
+ // tex2: added texture
+ if (target->tex2) {
+ SDL_DestroyTexture(target->tex2);
+ target->tex2 = NULL;
+ }
+ if (!target->tex2)
+ target->tex2 = SDL_CreateTexture(vc->renderer,
+ vc->osd_format.sdl, SDL_TEXTUREACCESS_STREAMING,
+ bmp->w, bmp->h);
+ if (!target->tex2) {
+ MP_ERR(vo, "Could not create texture\n");
+ }
+ if (target->tex2) {
+ SDL_SetTextureBlendMode(target->tex2,
+ SDL_BLENDMODE_ADD);
+ subbitmap_to_texture(vo, target->tex2, bmp,
+ 0xFF000000); // RGBA -> RGB1
+ }
+ }
+
+ sfc->change_id = imgs->change_id;
+}
+
+static void draw_osd_part(struct vo *vo, int index)
+{
+ struct priv *vc = vo->priv;
+ struct osd_bitmap_surface *sfc = &vc->osd_surfaces[index];
+ int i;
+
+ for (i = 0; i < sfc->num_targets; i++) {
+ struct osd_target *target = sfc->targets + i;
+ if (target->tex)
+ SDL_RenderCopy(vc->renderer, target->tex,
+ &target->source, &target->dest);
+ if (target->tex2)
+ SDL_RenderCopy(vc->renderer, target->tex2,
+ &target->source, &target->dest);
+ }
+}
+
+static void draw_osd_cb(void *ctx, struct sub_bitmaps *imgs)
+{
+ struct vo *vo = ctx;
+ generate_osd_part(vo, imgs);
+ draw_osd_part(vo, imgs->render_index);
+}
+
+static void draw_osd(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+
+ static const bool osdformats[SUBBITMAP_COUNT] = {
+ [SUBBITMAP_BGRA] = true,
+ };
+
+ osd_draw(vo->osd, vc->osd_res, vc->osd_pts, 0, osdformats, draw_osd_cb, vo);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+
+ if (SDL_WasInit(SDL_INIT_EVENTS)) {
+ MP_ERR(vo, "Another component is using SDL already.\n");
+ return -1;
+ }
+
+ vc->opts_cache = m_config_cache_alloc(vc, vo->global, &vo_sub_opts);
+
+ // predefine SDL defaults (SDL env vars shall override)
+ SDL_SetHintWithPriority(SDL_HINT_RENDER_SCALE_QUALITY, "1",
+ SDL_HINT_DEFAULT);
+ SDL_SetHintWithPriority(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0",
+ SDL_HINT_DEFAULT);
+
+ // predefine MPV options (SDL env vars shall be overridden)
+ SDL_SetHintWithPriority(SDL_HINT_RENDER_VSYNC, vc->vsync ? "1" : "0",
+ SDL_HINT_OVERRIDE);
+
+ if (SDL_InitSubSystem(SDL_INIT_VIDEO)) {
+ MP_ERR(vo, "SDL_Init failed\n");
+ return -1;
+ }
+
+ // then actually try
+ vc->window = SDL_CreateWindow("MPV", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
+ 640, 480, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN);
+ if (!vc->window) {
+ MP_ERR(vo, "SDL_CreateWindow failed\n");
+ return -1;
+ }
+
+ // try creating a renderer (this also gets the renderer_info data
+ // for query_format to use!)
+ if (init_renderer(vo) != 0) {
+ SDL_DestroyWindow(vc->window);
+ vc->window = NULL;
+ return -1;
+ }
+
+ vc->wakeup_event = SDL_RegisterEvents(1);
+ if (vc->wakeup_event == (Uint32)-1)
+ MP_ERR(vo, "SDL_RegisterEvents() failed.\n");
+
+ MP_WARN(vo, "Warning: this legacy VO has bad performance. Consider fixing "
+ "your graphics drivers, or not forcing the sdl VO.\n");
+
+ return 0;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct priv *vc = vo->priv;
+ int i, j;
+ for (i = 0; i < vc->renderer_info.num_texture_formats; ++i)
+ for (j = 0; j < sizeof(formats) / sizeof(formats[0]); ++j)
+ if (vc->renderer_info.texture_formats[i] == formats[j].sdl)
+ if (format == formats[j].mpv)
+ return 1;
+ return 0;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *vc = vo->priv;
+
+ // typically this runs in parallel with the following mp_image_copy call
+ SDL_SetRenderDrawColor(vc->renderer, 0, 0, 0, 255);
+ SDL_RenderClear(vc->renderer);
+
+ SDL_SetTextureBlendMode(vc->tex, SDL_BLENDMODE_NONE);
+
+ if (frame->current) {
+ vc->osd_pts = frame->current->pts;
+
+ mp_image_t texmpi;
+ if (!lock_texture(vo, &texmpi))
+ return;
+
+ mp_image_copy(&texmpi, frame->current);
+
+ SDL_UnlockTexture(vc->tex);
+ }
+
+ SDL_Rect src, dst;
+ src.x = vc->src_rect.x0;
+ src.y = vc->src_rect.y0;
+ src.w = vc->src_rect.x1 - vc->src_rect.x0;
+ src.h = vc->src_rect.y1 - vc->src_rect.y0;
+ dst.x = vc->dst_rect.x0;
+ dst.y = vc->dst_rect.y0;
+ dst.w = vc->dst_rect.x1 - vc->dst_rect.x0;
+ dst.h = vc->dst_rect.y1 - vc->dst_rect.y0;
+
+ SDL_RenderCopy(vc->renderer, vc->tex, &src, &dst);
+
+ draw_osd(vo);
+}
+
+static struct mp_image *get_window_screenshot(struct vo *vo)
+{
+ struct priv *vc = vo->priv;
+ struct mp_image *image = mp_image_alloc(vc->osd_format.mpv, vo->dwidth,
+ vo->dheight);
+ if (!image)
+ return NULL;
+ if (SDL_RenderReadPixels(vc->renderer, NULL, vc->osd_format.sdl,
+ image->planes[0], image->stride[0])) {
+ MP_ERR(vo, "SDL_RenderReadPixels failed\n");
+ talloc_free(image);
+ return NULL;
+ }
+ return image;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct priv *vc = vo->priv;
+
+ switch (request) {
+ case VOCTRL_VO_OPTS_CHANGED: {
+ void *opt;
+ while (m_config_cache_get_next_changed(vc->opts_cache, &opt)) {
+ struct mp_vo_opts *opts = vc->opts_cache->opts;
+ if (&opts->fullscreen == opt)
+ set_fullscreen(vo);
+ }
+ return 1;
+ }
+ case VOCTRL_SET_PANSCAN:
+ force_resize(vo);
+ return VO_TRUE;
+ case VOCTRL_SCREENSHOT_WIN:
+ *(struct mp_image **)data = get_window_screenshot(vo);
+ return true;
+ case VOCTRL_SET_CURSOR_VISIBILITY:
+ SDL_ShowCursor(*(bool *)data);
+ return true;
+ case VOCTRL_KILL_SCREENSAVER:
+ vc->screensaver_enabled = false;
+ set_screensaver(vc->screensaver_enabled);
+ return VO_TRUE;
+ case VOCTRL_RESTORE_SCREENSAVER:
+ vc->screensaver_enabled = true;
+ set_screensaver(vc->screensaver_enabled);
+ return VO_TRUE;
+ case VOCTRL_UPDATE_WINDOW_TITLE:
+ SDL_SetWindowTitle(vc->window, (char *)data);
+ return true;
+ }
+ return VO_NOTIMPL;
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct vo_driver video_out_sdl = {
+ .description = "SDL 2.0 Renderer",
+ .name = "sdl",
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .renderer_index = -1,
+ .vsync = true,
+ },
+ .options = (const struct m_option []){
+ {"sw", OPT_BOOL(allow_sw)},
+ {"switch-mode", OPT_BOOL(switch_mode)},
+ {"vsync", OPT_BOOL(vsync)},
+ {NULL}
+ },
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .uninit = uninit,
+ .flip_page = flip_page,
+ .wait_events = wait_events,
+ .wakeup = wakeup,
+ .options_prefix = "sdl",
+};
diff --git a/video/out/vo_sixel.c b/video/out/vo_sixel.c
new file mode 100644
index 0000000..e05c455
--- /dev/null
+++ b/video/out/vo_sixel.c
@@ -0,0 +1,627 @@
+/*
+ * Sixel mpv output device implementation based on ffmpeg libavdevice implementation
+ * by Hayaki Saito
+ * https://github.com/saitoha/FFmpeg-SIXEL/blob/sixel/libavdevice/sixel.c
+ *
+ * Copyright (c) 2014 Hayaki Saito
+ *
+ * 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 <stdio.h>
+#include <stdlib.h>
+
+#include <libswscale/swscale.h>
+#include <sixel.h>
+
+#include "config.h"
+#include "options/m_config.h"
+#include "osdep/terminal.h"
+#include "sub/osd.h"
+#include "vo.h"
+#include "video/sws_utils.h"
+#include "video/mp_image.h"
+
+#if HAVE_POSIX
+#include <unistd.h>
+#endif
+
+#define IMGFMT IMGFMT_RGB24
+
+#define TERM_ESC_USE_GLOBAL_COLOR_REG "\033[?1070l"
+
+#define TERMINAL_FALLBACK_COLS 80
+#define TERMINAL_FALLBACK_ROWS 25
+#define TERMINAL_FALLBACK_PX_WIDTH 320
+#define TERMINAL_FALLBACK_PX_HEIGHT 240
+
+struct vo_sixel_opts {
+ int diffuse;
+ int reqcolors;
+ bool fixedpal;
+ int threshold;
+ int width, height, top, left;
+ int pad_y, pad_x;
+ int rows, cols;
+ bool config_clear, alt_screen;
+ bool buffered;
+};
+
+struct priv {
+ // User specified options
+ struct vo_sixel_opts opts;
+
+ // Internal data
+ sixel_output_t *output;
+ sixel_dither_t *dither;
+ sixel_dither_t *testdither;
+ uint8_t *buffer;
+ char *sixel_output_buf;
+ bool skip_frame_draw;
+
+ int left, top; // image origin cell (1 based)
+ int width, height; // actual image px size - always reflects dst_rect.
+ int num_cols, num_rows; // terminal size in cells
+ int canvas_ok; // whether canvas vo->dwidth and vo->dheight are positive
+
+ int previous_histogram_colors;
+
+ struct mp_rect src_rect;
+ struct mp_rect dst_rect;
+ struct mp_osd_res osd;
+ struct mp_image *frame;
+ struct mp_sws_context *sws;
+};
+
+static const unsigned int depth = 3;
+
+static int detect_scene_change(struct vo* vo)
+{
+ struct priv* priv = vo->priv;
+ int previous_histogram_colors = priv->previous_histogram_colors;
+ int histogram_colors = 0;
+
+ // If threshold is set negative, then every frame must be a scene change
+ if (priv->dither == NULL || priv->opts.threshold < 0)
+ return 1;
+
+ histogram_colors = sixel_dither_get_num_of_histogram_colors(priv->testdither);
+
+ int color_difference_count = previous_histogram_colors - histogram_colors;
+ color_difference_count = (color_difference_count > 0) ? // abs value
+ color_difference_count : -color_difference_count;
+
+ if (100 * color_difference_count >
+ priv->opts.threshold * previous_histogram_colors)
+ {
+ priv->previous_histogram_colors = histogram_colors; // update history
+ return 1;
+ } else {
+ return 0;
+ }
+
+}
+
+static void dealloc_dithers_and_buffers(struct vo* vo)
+{
+ struct priv* priv = vo->priv;
+
+ if (priv->buffer) {
+ talloc_free(priv->buffer);
+ priv->buffer = NULL;
+ }
+
+ if (priv->frame) {
+ talloc_free(priv->frame);
+ priv->frame = NULL;
+ }
+
+ if (priv->dither) {
+ sixel_dither_unref(priv->dither);
+ priv->dither = NULL;
+ }
+
+ if (priv->testdither) {
+ sixel_dither_unref(priv->testdither);
+ priv->testdither = NULL;
+ }
+}
+
+static SIXELSTATUS prepare_static_palette(struct vo* vo)
+{
+ struct priv* priv = vo->priv;
+
+ if (!priv->dither) {
+ priv->dither = sixel_dither_get(BUILTIN_XTERM256);
+ if (priv->dither == NULL)
+ return SIXEL_FALSE;
+
+ sixel_dither_set_diffusion_type(priv->dither, priv->opts.diffuse);
+ }
+
+ sixel_dither_set_body_only(priv->dither, 0);
+ return SIXEL_OK;
+}
+
+static SIXELSTATUS prepare_dynamic_palette(struct vo *vo)
+{
+ SIXELSTATUS status = SIXEL_FALSE;
+ struct priv *priv = vo->priv;
+
+ /* create histogram and construct color palette
+ * with median cut algorithm. */
+ status = sixel_dither_initialize(priv->testdither, priv->buffer,
+ priv->width, priv->height,
+ SIXEL_PIXELFORMAT_RGB888,
+ LARGE_NORM, REP_CENTER_BOX,
+ QUALITY_LOW);
+ if (SIXEL_FAILED(status))
+ return status;
+
+ if (detect_scene_change(vo)) {
+ if (priv->dither) {
+ sixel_dither_unref(priv->dither);
+ priv->dither = NULL;
+ }
+
+ priv->dither = priv->testdither;
+ status = sixel_dither_new(&priv->testdither, priv->opts.reqcolors, NULL);
+
+ if (SIXEL_FAILED(status))
+ return status;
+
+ sixel_dither_set_diffusion_type(priv->dither, priv->opts.diffuse);
+ } else {
+ if (priv->dither == NULL)
+ return SIXEL_FALSE;
+ }
+
+ sixel_dither_set_body_only(priv->dither, 0);
+ return status;
+}
+
+static void update_canvas_dimensions(struct vo *vo)
+{
+ // this function sets the vo canvas size in pixels vo->dwidth, vo->dheight,
+ // and the number of rows and columns available in priv->num_rows/cols
+ struct priv *priv = vo->priv;
+ int num_rows = TERMINAL_FALLBACK_ROWS;
+ int num_cols = TERMINAL_FALLBACK_COLS;
+ int total_px_width = 0;
+ int total_px_height = 0;
+
+ terminal_get_size2(&num_rows, &num_cols, &total_px_width, &total_px_height);
+
+ // If the user has specified rows/cols use them for further calculations
+ num_rows = (priv->opts.rows > 0) ? priv->opts.rows : num_rows;
+ num_cols = (priv->opts.cols > 0) ? priv->opts.cols : num_cols;
+
+ // If the pad value is set in between 0 and width/2 - 1, then we
+ // subtract from the detected width. Otherwise, we assume that the width
+ // output must be a integer multiple of num_cols and accordingly set
+ // total_width to be an integer multiple of num_cols. So in case the padding
+ // added by terminal is less than the number of cells in that axis, then rounding
+ // down will take care of correcting the detected width and remove padding.
+ if (priv->opts.width > 0) {
+ // option - set by the user, hard truth
+ total_px_width = priv->opts.width;
+ } else {
+ if (total_px_width <= 0) {
+ // ioctl failed to read terminal width
+ total_px_width = TERMINAL_FALLBACK_PX_WIDTH;
+ } else {
+ if (priv->opts.pad_x >= 0 && priv->opts.pad_x < total_px_width / 2) {
+ // explicit padding set by the user
+ total_px_width -= (2 * priv->opts.pad_x);
+ } else {
+ // rounded "auto padding"
+ total_px_width = total_px_width / num_cols * num_cols;
+ }
+ }
+ }
+
+ if (priv->opts.height > 0) {
+ total_px_height = priv->opts.height;
+ } else {
+ if (total_px_height <= 0) {
+ total_px_height = TERMINAL_FALLBACK_PX_HEIGHT;
+ } else {
+ if (priv->opts.pad_y >= 0 && priv->opts.pad_y < total_px_height / 2) {
+ total_px_height -= (2 * priv->opts.pad_y);
+ } else {
+ total_px_height = total_px_height / num_rows * num_rows;
+ }
+ }
+ }
+
+ // use n-1 rows for height
+ // The last row can't be used for encoding image, because after sixel encode
+ // the terminal moves the cursor to next line below the image, causing the
+ // last line to be empty instead of displaying image data.
+ // TODO: Confirm if the output height must be a multiple of 6, if not, remove
+ // the / 6 * 6 part which is setting the height to be a multiple of 6.
+ vo->dheight = total_px_height * (num_rows - 1) / num_rows / 6 * 6;
+ vo->dwidth = total_px_width;
+
+ priv->num_rows = num_rows;
+ priv->num_cols = num_cols;
+
+ priv->canvas_ok = vo->dwidth > 0 && vo->dheight > 0;
+}
+
+static void set_sixel_output_parameters(struct vo *vo)
+{
+ // This function sets output scaled size in priv->width, priv->height
+ // and the scaling rectangles in pixels priv->src_rect, priv->dst_rect
+ // as well as image positioning in cells priv->top, priv->left.
+ struct priv *priv = vo->priv;
+
+ vo_get_src_dst_rects(vo, &priv->src_rect, &priv->dst_rect, &priv->osd);
+
+ // priv->width and priv->height are the width and height of dst_rect
+ // and they are not changed anywhere else outside this function.
+ // It is the sixel image output dimension which is output by libsixel.
+ priv->width = priv->dst_rect.x1 - priv->dst_rect.x0;
+ priv->height = priv->dst_rect.y1 - priv->dst_rect.y0;
+
+ // top/left values must be greater than 1. If it is set, then
+ // the image will be rendered from there and no further centering is done.
+ priv->top = (priv->opts.top > 0) ? priv->opts.top :
+ priv->num_rows * priv->dst_rect.y0 / vo->dheight + 1;
+ priv->left = (priv->opts.left > 0) ? priv->opts.left :
+ priv->num_cols * priv->dst_rect.x0 / vo->dwidth + 1;
+}
+
+static int update_sixel_swscaler(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *priv = vo->priv;
+
+ priv->sws->src = *params;
+ priv->sws->src.w = mp_rect_w(priv->src_rect);
+ priv->sws->src.h = mp_rect_h(priv->src_rect);
+ priv->sws->dst = (struct mp_image_params) {
+ .imgfmt = IMGFMT,
+ .w = priv->width,
+ .h = priv->height,
+ .p_w = 1,
+ .p_h = 1,
+ };
+
+ dealloc_dithers_and_buffers(vo);
+
+ priv->frame = mp_image_alloc(IMGFMT, priv->width, priv->height);
+ if (!priv->frame)
+ return -1;
+
+ if (mp_sws_reinit(priv->sws) < 0)
+ return -1;
+
+ // create testdither only if dynamic palette mode is set
+ if (!priv->opts.fixedpal) {
+ SIXELSTATUS status = sixel_dither_new(&priv->testdither,
+ priv->opts.reqcolors, NULL);
+ if (SIXEL_FAILED(status)) {
+ MP_ERR(vo, "update_sixel_swscaler: Failed to create new dither: %s\n",
+ sixel_helper_format_error(status));
+ return -1;
+ }
+ }
+
+ priv->buffer =
+ talloc_array(NULL, uint8_t, depth * priv->width * priv->height);
+
+ return 0;
+}
+
+static inline int sixel_buffer(char *data, int size, void *priv) {
+ char **out = (char **)priv;
+ *out = talloc_strndup_append_buffer(*out, data, size);
+ return size;
+}
+
+static inline int sixel_write(char *data, int size, void *priv)
+{
+ FILE *p = (FILE *)priv;
+ // On POSIX platforms, write() is the fastest method. It also is the only
+ // one that allows atomic writes so mpv’s output will not be interrupted
+ // by other processes or threads that write to stdout, which would cause
+ // screen corruption. POSIX does not guarantee atomicity for writes
+ // exceeding PIPE_BUF, but at least Linux does seem to implement it that
+ // way.
+#if HAVE_POSIX
+ int remain = size;
+
+ while (remain > 0) {
+ ssize_t written = write(fileno(p), data, remain);
+ if (written < 0)
+ return written;
+ remain -= written;
+ data += written;
+ }
+
+ return size;
+#else
+ int ret = fwrite(data, 1, size, p);
+ fflush(p);
+ return ret;
+#endif
+}
+
+static inline void sixel_strwrite(char *s)
+{
+ sixel_write(s, strlen(s), stdout);
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *priv = vo->priv;
+ int ret = 0;
+ update_canvas_dimensions(vo);
+ if (priv->canvas_ok) { // if too small - succeed but skip the rendering
+ set_sixel_output_parameters(vo);
+ ret = update_sixel_swscaler(vo, params);
+ }
+
+ if (priv->opts.config_clear)
+ sixel_strwrite(TERM_ESC_CLEAR_SCREEN);
+ vo->want_redraw = true;
+
+ return ret;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *priv = vo->priv;
+ SIXELSTATUS status;
+ struct mp_image *mpi = NULL;
+
+ int prev_rows = priv->num_rows;
+ int prev_cols = priv->num_cols;
+ int prev_height = vo->dheight;
+ int prev_width = vo->dwidth;
+ bool resized = false;
+ update_canvas_dimensions(vo);
+ if (!priv->canvas_ok)
+ return;
+
+ if (prev_rows != priv->num_rows || prev_cols != priv->num_cols ||
+ prev_width != vo->dwidth || prev_height != vo->dheight)
+ {
+ set_sixel_output_parameters(vo);
+ // Not checking for vo->config_ok because draw_frame is never called
+ // with a failed reconfig.
+ update_sixel_swscaler(vo, vo->params);
+
+ if (priv->opts.config_clear)
+ sixel_strwrite(TERM_ESC_CLEAR_SCREEN);
+ resized = true;
+ }
+
+ if (frame->repeat && !frame->redraw && !resized) {
+ // Frame is repeated, and no need to update OSD either
+ priv->skip_frame_draw = true;
+ return;
+ } else {
+ // Either frame is new, or OSD has to be redrawn
+ priv->skip_frame_draw = false;
+ }
+
+ // Normal case where we have to draw the frame and the image is not NULL
+ if (frame->current) {
+ mpi = mp_image_new_ref(frame->current);
+ struct mp_rect src_rc = priv->src_rect;
+ src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, mpi->fmt.align_x);
+ src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, mpi->fmt.align_y);
+ mp_image_crop_rc(mpi, src_rc);
+
+ // scale/pan to our dest rect
+ mp_sws_scale(priv->sws, priv->frame, mpi);
+ } else {
+ // Image is NULL, so need to clear image and draw OSD
+ mp_image_clear(priv->frame, 0, 0, priv->width, priv->height);
+ }
+
+ struct mp_osd_res dim = {
+ .w = priv->width,
+ .h = priv->height
+ };
+ osd_draw_on_image(vo->osd, dim, mpi ? mpi->pts : 0, 0, priv->frame);
+
+ // Copy from mpv to RGB format as required by libsixel
+ memcpy_pic(priv->buffer, priv->frame->planes[0], priv->width * depth,
+ priv->height, priv->width * depth, priv->frame->stride[0]);
+
+ // Even if either of these prepare palette functions fail, on re-running them
+ // they should try to re-initialize the dithers, so it shouldn't dereference
+ // any NULL pointers. flip_page also has a check to make sure dither is not
+ // NULL before drawing, so failure in these functions should still be okay.
+ if (priv->opts.fixedpal) {
+ status = prepare_static_palette(vo);
+ } else {
+ status = prepare_dynamic_palette(vo);
+ }
+
+ if (SIXEL_FAILED(status)) {
+ MP_WARN(vo, "draw_frame: prepare_palette returned error: %s\n",
+ sixel_helper_format_error(status));
+ }
+
+ if (mpi)
+ talloc_free(mpi);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv* priv = vo->priv;
+ if (!priv->canvas_ok)
+ return;
+
+ // If frame is repeated and no update required, then we skip encoding
+ if (priv->skip_frame_draw)
+ return;
+
+ // Make sure that image and dither are valid before drawing
+ if (priv->buffer == NULL || priv->dither == NULL)
+ return;
+
+ // Go to the offset row and column, then display the image
+ priv->sixel_output_buf = talloc_asprintf(NULL, TERM_ESC_GOTO_YX,
+ priv->top, priv->left);
+ if (!priv->opts.buffered)
+ sixel_strwrite(priv->sixel_output_buf);
+
+ sixel_encode(priv->buffer, priv->width, priv->height,
+ depth, priv->dither, priv->output);
+
+ if (priv->opts.buffered)
+ sixel_write(priv->sixel_output_buf,
+ ta_get_size(priv->sixel_output_buf), stdout);
+
+ talloc_free(priv->sixel_output_buf);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+ SIXELSTATUS status = SIXEL_FALSE;
+
+ // Parse opts set by CLI or conf
+ priv->sws = mp_sws_alloc(vo);
+ priv->sws->log = vo->log;
+ mp_sws_enable_cmdline_opts(priv->sws, vo->global);
+
+ if (priv->opts.buffered)
+ status = sixel_output_new(&priv->output, sixel_buffer,
+ &priv->sixel_output_buf, NULL);
+ else
+ status = sixel_output_new(&priv->output, sixel_write, stdout, NULL);
+ if (SIXEL_FAILED(status)) {
+ MP_ERR(vo, "preinit: Failed to create output file: %s\n",
+ sixel_helper_format_error(status));
+ return -1;
+ }
+
+ sixel_output_set_encode_policy(priv->output, SIXEL_ENCODEPOLICY_FAST);
+
+ if (priv->opts.alt_screen)
+ sixel_strwrite(TERM_ESC_ALT_SCREEN);
+
+ sixel_strwrite(TERM_ESC_HIDE_CURSOR);
+
+ /* don't use private color registers for each frame. */
+ sixel_strwrite(TERM_ESC_USE_GLOBAL_COLOR_REG);
+
+ priv->dither = NULL;
+
+ // create testdither only if dynamic palette mode is set
+ if (!priv->opts.fixedpal) {
+ status = sixel_dither_new(&priv->testdither, priv->opts.reqcolors, NULL);
+ if (SIXEL_FAILED(status)) {
+ MP_ERR(vo, "preinit: Failed to create new dither: %s\n",
+ sixel_helper_format_error(status));
+ return -1;
+ }
+ }
+
+ priv->previous_histogram_colors = 0;
+
+ return 0;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return format == IMGFMT;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ if (request == VOCTRL_SET_PANSCAN)
+ return (vo->config_ok && !reconfig(vo, vo->params)) ? VO_TRUE : VO_FALSE;
+ return VO_NOTIMPL;
+}
+
+
+static void uninit(struct vo *vo)
+{
+ struct priv *priv = vo->priv;
+
+ sixel_strwrite(TERM_ESC_RESTORE_CURSOR);
+
+ if (priv->opts.alt_screen)
+ sixel_strwrite(TERM_ESC_NORMAL_SCREEN);
+ fflush(stdout);
+
+ if (priv->output) {
+ sixel_output_unref(priv->output);
+ priv->output = NULL;
+ }
+
+ dealloc_dithers_and_buffers(vo);
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct vo_driver video_out_sixel = {
+ .name = "sixel",
+ .description = "terminal graphics using sixels",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .opts.diffuse = DIFFUSE_AUTO,
+ .opts.reqcolors = 256,
+ .opts.threshold = -1,
+ .opts.fixedpal = true,
+ .opts.pad_y = -1,
+ .opts.pad_x = -1,
+ .opts.config_clear = true,
+ .opts.alt_screen = true,
+ },
+ .options = (const m_option_t[]) {
+ {"dither", OPT_CHOICE(opts.diffuse,
+ {"auto", DIFFUSE_AUTO},
+ {"none", DIFFUSE_NONE},
+ {"atkinson", DIFFUSE_ATKINSON},
+ {"fs", DIFFUSE_FS},
+ {"jajuni", DIFFUSE_JAJUNI},
+ {"stucki", DIFFUSE_STUCKI},
+ {"burkes", DIFFUSE_BURKES},
+ {"arithmetic", DIFFUSE_A_DITHER},
+ {"xor", DIFFUSE_X_DITHER})},
+ {"width", OPT_INT(opts.width)},
+ {"height", OPT_INT(opts.height)},
+ {"reqcolors", OPT_INT(opts.reqcolors)},
+ {"fixedpalette", OPT_BOOL(opts.fixedpal)},
+ {"threshold", OPT_INT(opts.threshold)},
+ {"top", OPT_INT(opts.top)},
+ {"left", OPT_INT(opts.left)},
+ {"pad-y", OPT_INT(opts.pad_y)},
+ {"pad-x", OPT_INT(opts.pad_x)},
+ {"rows", OPT_INT(opts.rows)},
+ {"cols", OPT_INT(opts.cols)},
+ {"config-clear", OPT_BOOL(opts.config_clear), },
+ {"alt-screen", OPT_BOOL(opts.alt_screen), },
+ {"buffered", OPT_BOOL(opts.buffered), },
+ {"exit-clear", OPT_REPLACED("vo-sixel-alt-screen")},
+ {0}
+ },
+ .options_prefix = "vo-sixel",
+};
diff --git a/video/out/vo_tct.c b/video/out/vo_tct.c
new file mode 100644
index 0000000..8859095
--- /dev/null
+++ b/video/out/vo_tct.c
@@ -0,0 +1,347 @@
+/*
+ * 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 <stdio.h>
+#include <unistd.h>
+#include <config.h>
+
+#if HAVE_POSIX
+#include <sys/ioctl.h>
+#endif
+
+#include <libswscale/swscale.h>
+
+#include "options/m_config.h"
+#include "config.h"
+#include "osdep/terminal.h"
+#include "osdep/io.h"
+#include "vo.h"
+#include "sub/osd.h"
+#include "video/sws_utils.h"
+#include "video/mp_image.h"
+
+#define IMGFMT IMGFMT_BGR24
+
+#define ALGO_PLAIN 1
+#define ALGO_HALF_BLOCKS 2
+
+#define TERM_ESC_CLEAR_COLORS "\033[0m"
+#define TERM_ESC_COLOR256_BG "\033[48;5"
+#define TERM_ESC_COLOR256_FG "\033[38;5"
+#define TERM_ESC_COLOR24BIT_BG "\033[48;2"
+#define TERM_ESC_COLOR24BIT_FG "\033[38;2"
+
+#define DEFAULT_WIDTH 80
+#define DEFAULT_HEIGHT 25
+
+struct vo_tct_opts {
+ int algo;
+ int width; // 0 -> default
+ int height; // 0 -> default
+ bool term256; // 0 -> true color
+};
+
+struct lut_item {
+ char str[4];
+ int width;
+};
+
+struct priv {
+ struct vo_tct_opts opts;
+ size_t buffer_size;
+ int swidth;
+ int sheight;
+ struct mp_image *frame;
+ struct mp_rect src;
+ struct mp_rect dst;
+ struct mp_sws_context *sws;
+ struct lut_item lut[256];
+};
+
+// Convert RGB24 to xterm-256 8-bit value
+// For simplicity, assume RGB space is perceptually uniform.
+// There are 5 places where one of two outputs needs to be chosen when the
+// input is the exact middle:
+// - The r/g/b channels and the gray value: the higher value output is chosen.
+// - If the gray and color have same distance from the input - color is chosen.
+static int rgb_to_x256(uint8_t r, uint8_t g, uint8_t b)
+{
+ // Calculate the nearest 0-based color index at 16 .. 231
+# define v2ci(v) (v < 48 ? 0 : v < 115 ? 1 : (v - 35) / 40)
+ int ir = v2ci(r), ig = v2ci(g), ib = v2ci(b); // 0..5 each
+# define color_index() (36 * ir + 6 * ig + ib) /* 0..215, lazy evaluation */
+
+ // Calculate the nearest 0-based gray index at 232 .. 255
+ int average = (r + g + b) / 3;
+ int gray_index = average > 238 ? 23 : (average - 3) / 10; // 0..23
+
+ // Calculate the represented colors back from the index
+ static const int i2cv[6] = {0, 0x5f, 0x87, 0xaf, 0xd7, 0xff};
+ int cr = i2cv[ir], cg = i2cv[ig], cb = i2cv[ib]; // r/g/b, 0..255 each
+ int gv = 8 + 10 * gray_index; // same value for r/g/b, 0..255
+
+ // Return the one which is nearer to the original input rgb value
+# define dist_square(A,B,C, a,b,c) ((A-a)*(A-a) + (B-b)*(B-b) + (C-c)*(C-c))
+ int color_err = dist_square(cr, cg, cb, r, g, b);
+ int gray_err = dist_square(gv, gv, gv, r, g, b);
+ return color_err <= gray_err ? 16 + color_index() : 232 + gray_index;
+}
+
+static void print_seq3(struct lut_item *lut, const char* prefix,
+ uint8_t r, uint8_t g, uint8_t b)
+{
+// The fwrite implementation is about 25% faster than the printf code
+// (even if we use *.s with the lut values), however,
+// on windows we need to use printf in order to translate escape sequences and
+// UTF8 output for the console.
+#ifndef _WIN32
+ fputs(prefix, stdout);
+ fwrite(lut[r].str, lut[r].width, 1, stdout);
+ fwrite(lut[g].str, lut[g].width, 1, stdout);
+ fwrite(lut[b].str, lut[b].width, 1, stdout);
+ fputc('m', stdout);
+#else
+ printf("%s;%d;%d;%dm", prefix, (int)r, (int)g, (int)b);
+#endif
+}
+
+static void print_seq1(struct lut_item *lut, const char* prefix, uint8_t c)
+{
+#ifndef _WIN32
+ fputs(prefix, stdout);
+ fwrite(lut[c].str, lut[c].width, 1, stdout);
+ fputc('m', stdout);
+#else
+ printf("%s;%dm", prefix, (int)c);
+#endif
+}
+
+
+static void write_plain(
+ const int dwidth, const int dheight,
+ const int swidth, const int sheight,
+ const unsigned char *source, const int source_stride,
+ bool term256, struct lut_item *lut)
+{
+ assert(source);
+ const int tx = (dwidth - swidth) / 2;
+ const int ty = (dheight - sheight) / 2;
+ for (int y = 0; y < sheight; y++) {
+ const unsigned char *row = source + y * source_stride;
+ printf(TERM_ESC_GOTO_YX, ty + y, tx);
+ for (int x = 0; x < swidth; x++) {
+ unsigned char b = *row++;
+ unsigned char g = *row++;
+ unsigned char r = *row++;
+ if (term256) {
+ print_seq1(lut, TERM_ESC_COLOR256_BG, rgb_to_x256(r, g, b));
+ } else {
+ print_seq3(lut, TERM_ESC_COLOR24BIT_BG, r, g, b);
+ }
+ printf(" ");
+ }
+ printf(TERM_ESC_CLEAR_COLORS);
+ }
+ printf("\n");
+}
+
+static void write_half_blocks(
+ const int dwidth, const int dheight,
+ const int swidth, const int sheight,
+ unsigned char *source, int source_stride,
+ bool term256, struct lut_item *lut)
+{
+ assert(source);
+ const int tx = (dwidth - swidth) / 2;
+ const int ty = (dheight - sheight) / 2;
+ for (int y = 0; y < sheight * 2; y += 2) {
+ const unsigned char *row_up = source + y * source_stride;
+ const unsigned char *row_down = source + (y + 1) * source_stride;
+ printf(TERM_ESC_GOTO_YX, ty + y / 2, tx);
+ for (int x = 0; x < swidth; x++) {
+ unsigned char b_up = *row_up++;
+ unsigned char g_up = *row_up++;
+ unsigned char r_up = *row_up++;
+ unsigned char b_down = *row_down++;
+ unsigned char g_down = *row_down++;
+ unsigned char r_down = *row_down++;
+ if (term256) {
+ print_seq1(lut, TERM_ESC_COLOR256_BG, rgb_to_x256(r_up, g_up, b_up));
+ print_seq1(lut, TERM_ESC_COLOR256_FG, rgb_to_x256(r_down, g_down, b_down));
+ } else {
+ print_seq3(lut, TERM_ESC_COLOR24BIT_BG, r_up, g_up, b_up);
+ print_seq3(lut, TERM_ESC_COLOR24BIT_FG, r_down, g_down, b_down);
+ }
+ printf("\xe2\x96\x84"); // UTF8 bytes of U+2584 (lower half block)
+ }
+ printf(TERM_ESC_CLEAR_COLORS);
+ }
+ printf("\n");
+}
+
+static void get_win_size(struct vo *vo, int *out_width, int *out_height) {
+ struct priv *p = vo->priv;
+ *out_width = DEFAULT_WIDTH;
+ *out_height = DEFAULT_HEIGHT;
+
+ terminal_get_size(out_width, out_height);
+
+ if (p->opts.width > 0)
+ *out_width = p->opts.width;
+ if (p->opts.height > 0)
+ *out_height = p->opts.height;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *p = vo->priv;
+
+ get_win_size(vo, &vo->dwidth, &vo->dheight);
+
+ struct mp_osd_res osd;
+ vo_get_src_dst_rects(vo, &p->src, &p->dst, &osd);
+ p->swidth = p->dst.x1 - p->dst.x0;
+ p->sheight = p->dst.y1 - p->dst.y0;
+
+ p->sws->src = *params;
+ p->sws->dst = (struct mp_image_params) {
+ .imgfmt = IMGFMT,
+ .w = p->swidth,
+ .h = p->sheight,
+ .p_w = 1,
+ .p_h = 1,
+ };
+
+ const int mul = (p->opts.algo == ALGO_PLAIN ? 1 : 2);
+ if (p->frame)
+ talloc_free(p->frame);
+ p->frame = mp_image_alloc(IMGFMT, p->swidth, p->sheight * mul);
+ if (!p->frame)
+ return -1;
+
+ if (mp_sws_reinit(p->sws) < 0)
+ return -1;
+
+ printf(TERM_ESC_CLEAR_SCREEN);
+
+ vo->want_redraw = true;
+ return 0;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+ struct mp_image *src = frame->current;
+ if (!src)
+ return;
+ // XXX: pan, crop etc.
+ mp_sws_scale(p->sws, p->frame, src);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ int width, height;
+ get_win_size(vo, &width, &height);
+
+ if (vo->dwidth != width || vo->dheight != height)
+ reconfig(vo, vo->params);
+
+ if (p->opts.algo == ALGO_PLAIN) {
+ write_plain(
+ vo->dwidth, vo->dheight, p->swidth, p->sheight,
+ p->frame->planes[0], p->frame->stride[0],
+ p->opts.term256, p->lut);
+ } else {
+ write_half_blocks(
+ vo->dwidth, vo->dheight, p->swidth, p->sheight,
+ p->frame->planes[0], p->frame->stride[0],
+ p->opts.term256, p->lut);
+ }
+ fflush(stdout);
+}
+
+static void uninit(struct vo *vo)
+{
+ printf(TERM_ESC_RESTORE_CURSOR);
+ printf(TERM_ESC_NORMAL_SCREEN);
+ struct priv *p = vo->priv;
+ if (p->frame)
+ talloc_free(p->frame);
+}
+
+static int preinit(struct vo *vo)
+{
+ // most terminal characters aren't 1:1, so we default to 2:1.
+ // if user passes their own value of choice, it'll be scaled accordingly.
+ vo->monitor_par = vo->opts->monitor_pixel_aspect * 2;
+
+ struct priv *p = vo->priv;
+ p->sws = mp_sws_alloc(vo);
+ p->sws->log = vo->log;
+ mp_sws_enable_cmdline_opts(p->sws, vo->global);
+
+ for (int i = 0; i < 256; ++i) {
+ char buff[8];
+ p->lut[i].width = snprintf(buff, sizeof(buff), ";%d", i);
+ memcpy(p->lut[i].str, buff, 4); // some strings may not end on a null byte, but that's ok.
+ }
+
+ printf(TERM_ESC_HIDE_CURSOR);
+ printf(TERM_ESC_ALT_SCREEN);
+
+ return 0;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return format == IMGFMT;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ return VO_NOTIMPL;
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct vo_driver video_out_tct = {
+ .name = "tct",
+ .description = "true-color terminals",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .opts.algo = ALGO_HALF_BLOCKS,
+ },
+ .options = (const m_option_t[]) {
+ {"algo", OPT_CHOICE(opts.algo,
+ {"plain", ALGO_PLAIN},
+ {"half-blocks", ALGO_HALF_BLOCKS})},
+ {"width", OPT_INT(opts.width)},
+ {"height", OPT_INT(opts.height)},
+ {"256", OPT_BOOL(opts.term256)},
+ {0}
+ },
+ .options_prefix = "vo-tct",
+};
diff --git a/video/out/vo_vaapi.c b/video/out/vo_vaapi.c
new file mode 100644
index 0000000..12888fe
--- /dev/null
+++ b/video/out/vo_vaapi.c
@@ -0,0 +1,877 @@
+/*
+ * VA API output module
+ *
+ * Copyright (C) 2008-2009 Splitted-Desktop Systems
+ * Gwenole Beauchesne <gbeauchesne@splitted-desktop.com>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <stdarg.h>
+#include <limits.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <va/va_x11.h>
+
+#include "common/msg.h"
+#include "video/out/vo.h"
+#include "video/mp_image_pool.h"
+#include "video/sws_utils.h"
+#include "sub/draw_bmp.h"
+#include "sub/img_convert.h"
+#include "sub/osd.h"
+#include "present_sync.h"
+#include "x11_common.h"
+
+#include "video/mp_image.h"
+#include "video/vaapi.h"
+#include "video/hwdec.h"
+
+struct vaapi_osd_image {
+ int w, h;
+ VAImage image;
+ VASubpictureID subpic_id;
+ bool is_used;
+};
+
+struct vaapi_subpic {
+ VASubpictureID id;
+ int src_x, src_y, src_w, src_h;
+ int dst_x, dst_y, dst_w, dst_h;
+};
+
+struct vaapi_osd_part {
+ bool active;
+ int change_id;
+ struct vaapi_osd_image image;
+ struct vaapi_subpic subpic;
+};
+
+#define MAX_OUTPUT_SURFACES 2
+
+struct priv {
+ struct mp_log *log;
+ struct vo *vo;
+ VADisplay display;
+ struct mp_vaapi_ctx *mpvaapi;
+
+ struct mp_image_params image_params;
+ struct mp_rect src_rect;
+ struct mp_rect dst_rect;
+ struct mp_osd_res screen_osd_res;
+
+ struct mp_image *output_surfaces[MAX_OUTPUT_SURFACES];
+ struct mp_image *swdec_surfaces[MAX_OUTPUT_SURFACES];
+
+ int output_surface;
+ int visible_surface;
+ int scaling;
+ bool force_scaled_osd;
+
+ VAImageFormat osd_format; // corresponds to OSD_VA_FORMAT
+ struct vaapi_osd_part osd_part;
+ bool osd_screen;
+ struct mp_draw_sub_cache *osd_cache;
+
+ struct mp_image_pool *pool;
+
+ struct mp_image *black_surface;
+
+ VAImageFormat *va_subpic_formats;
+ unsigned int *va_subpic_flags;
+ int va_num_subpic_formats;
+ VADisplayAttribute *va_display_attrs;
+ int *mp_display_attr;
+ int va_num_display_attrs;
+
+ struct va_image_formats *image_formats;
+};
+
+#define OSD_VA_FORMAT VA_FOURCC_BGRA
+
+static void draw_osd(struct vo *vo);
+
+
+struct fmtentry {
+ uint32_t va;
+ enum mp_imgfmt mp;
+};
+
+static const struct fmtentry va_to_imgfmt[] = {
+ {VA_FOURCC_NV12, IMGFMT_NV12},
+ {VA_FOURCC_YV12, IMGFMT_420P},
+ {VA_FOURCC_IYUV, IMGFMT_420P},
+ {VA_FOURCC_UYVY, IMGFMT_UYVY},
+ // Note: not sure about endian issues (the mp formats are byte-addressed)
+ {VA_FOURCC_RGBA, IMGFMT_RGBA},
+ {VA_FOURCC_RGBX, IMGFMT_RGBA},
+ {VA_FOURCC_BGRA, IMGFMT_BGRA},
+ {VA_FOURCC_BGRX, IMGFMT_BGRA},
+ {0 , IMGFMT_NONE}
+};
+
+static enum mp_imgfmt va_fourcc_to_imgfmt(uint32_t fourcc)
+{
+ for (const struct fmtentry *entry = va_to_imgfmt; entry->va; ++entry) {
+ if (entry->va == fourcc)
+ return entry->mp;
+ }
+ return IMGFMT_NONE;
+}
+
+static uint32_t va_fourcc_from_imgfmt(int imgfmt)
+{
+ for (const struct fmtentry *entry = va_to_imgfmt; entry->va; ++entry) {
+ if (entry->mp == imgfmt)
+ return entry->va;
+ }
+ return 0;
+}
+
+struct va_image_formats {
+ VAImageFormat *entries;
+ int num;
+};
+
+static void va_get_formats(struct priv *ctx)
+{
+ struct va_image_formats *formats = talloc_ptrtype(ctx, formats);
+ formats->num = vaMaxNumImageFormats(ctx->display);
+ formats->entries = talloc_array(formats, VAImageFormat, formats->num);
+ VAStatus status = vaQueryImageFormats(ctx->display, formats->entries,
+ &formats->num);
+ if (!CHECK_VA_STATUS(ctx, "vaQueryImageFormats()"))
+ return;
+ MP_VERBOSE(ctx, "%d image formats available:\n", formats->num);
+ for (int i = 0; i < formats->num; i++)
+ MP_VERBOSE(ctx, " %s\n", mp_tag_str(formats->entries[i].fourcc));
+ ctx->image_formats = formats;
+}
+
+static VAImageFormat *va_image_format_from_imgfmt(struct priv *ctx,
+ int imgfmt)
+{
+ struct va_image_formats *formats = ctx->image_formats;
+ const int fourcc = va_fourcc_from_imgfmt(imgfmt);
+ if (!formats || !formats->num || !fourcc)
+ return NULL;
+ for (int i = 0; i < formats->num; i++) {
+ if (formats->entries[i].fourcc == fourcc)
+ return &formats->entries[i];
+ }
+ return NULL;
+}
+
+struct va_surface {
+ struct mp_vaapi_ctx *ctx;
+ VADisplay display;
+
+ VASurfaceID id;
+ int rt_format;
+
+ // The actually allocated surface size (needed for cropping).
+ // mp_images can have a smaller size than this, which means they are
+ // cropped down to a smaller size by removing right/bottom pixels.
+ int w, h;
+
+ VAImage image; // used for software decoding case
+ bool is_derived; // is image derived by vaDeriveImage()?
+};
+
+static struct va_surface *va_surface_in_mp_image(struct mp_image *mpi)
+{
+ return mpi && mpi->imgfmt == IMGFMT_VAAPI ?
+ (struct va_surface*)mpi->planes[0] : NULL;
+}
+
+static void release_va_surface(void *arg)
+{
+ struct va_surface *surface = arg;
+
+ if (surface->id != VA_INVALID_ID) {
+ if (surface->image.image_id != VA_INVALID_ID)
+ vaDestroyImage(surface->display, surface->image.image_id);
+ vaDestroySurfaces(surface->display, &surface->id, 1);
+ }
+
+ talloc_free(surface);
+}
+
+static struct mp_image *alloc_surface(struct mp_vaapi_ctx *ctx, int rt_format,
+ int w, int h)
+{
+ VASurfaceID id = VA_INVALID_ID;
+ VAStatus status;
+ status = vaCreateSurfaces(ctx->display, rt_format, w, h, &id, 1, NULL, 0);
+ if (!CHECK_VA_STATUS(ctx, "vaCreateSurfaces()"))
+ return NULL;
+
+ struct va_surface *surface = talloc_ptrtype(NULL, surface);
+ if (!surface)
+ return NULL;
+
+ *surface = (struct va_surface){
+ .ctx = ctx,
+ .id = id,
+ .rt_format = rt_format,
+ .w = w,
+ .h = h,
+ .display = ctx->display,
+ .image = { .image_id = VA_INVALID_ID, .buf = VA_INVALID_ID },
+ };
+
+ struct mp_image img = {0};
+ mp_image_setfmt(&img, IMGFMT_VAAPI);
+ mp_image_set_size(&img, w, h);
+ img.planes[0] = (uint8_t*)surface;
+ img.planes[3] = (uint8_t*)(uintptr_t)surface->id;
+ return mp_image_new_custom_ref(&img, surface, release_va_surface);
+}
+
+static void va_surface_image_destroy(struct va_surface *surface)
+{
+ if (!surface || surface->image.image_id == VA_INVALID_ID)
+ return;
+ vaDestroyImage(surface->display, surface->image.image_id);
+ surface->image.image_id = VA_INVALID_ID;
+ surface->is_derived = false;
+}
+
+static int va_surface_image_alloc(struct va_surface *p, VAImageFormat *format)
+{
+ VADisplay *display = p->display;
+
+ if (p->image.image_id != VA_INVALID_ID &&
+ p->image.format.fourcc == format->fourcc)
+ return 0;
+
+ int r = 0;
+
+ va_surface_image_destroy(p);
+
+ VAStatus status = vaDeriveImage(display, p->id, &p->image);
+ if (status == VA_STATUS_SUCCESS) {
+ /* vaDeriveImage() is supported, check format */
+ if (p->image.format.fourcc == format->fourcc &&
+ p->image.width == p->w && p->image.height == p->h)
+ {
+ p->is_derived = true;
+ MP_TRACE(p->ctx, "Using vaDeriveImage()\n");
+ } else {
+ vaDestroyImage(p->display, p->image.image_id);
+ status = VA_STATUS_ERROR_OPERATION_FAILED;
+ }
+ }
+ if (status != VA_STATUS_SUCCESS) {
+ p->image.image_id = VA_INVALID_ID;
+ status = vaCreateImage(p->display, format, p->w, p->h, &p->image);
+ if (!CHECK_VA_STATUS(p->ctx, "vaCreateImage()")) {
+ p->image.image_id = VA_INVALID_ID;
+ r = -1;
+ }
+ }
+
+ return r;
+}
+
+// img must be a VAAPI surface; make sure its internal VAImage is allocated
+// to a format corresponding to imgfmt (or return an error).
+static int va_surface_alloc_imgfmt(struct priv *priv, struct mp_image *img,
+ int imgfmt)
+{
+ struct va_surface *p = va_surface_in_mp_image(img);
+ if (!p)
+ return -1;
+ // Multiple FourCCs can refer to the same imgfmt, so check by doing the
+ // surjective conversion first.
+ if (p->image.image_id != VA_INVALID_ID &&
+ va_fourcc_to_imgfmt(p->image.format.fourcc) == imgfmt)
+ return 0;
+ VAImageFormat *format = va_image_format_from_imgfmt(priv, imgfmt);
+ if (!format)
+ return -1;
+ if (va_surface_image_alloc(p, format) < 0)
+ return -1;
+ return 0;
+}
+
+static bool va_image_map(struct mp_vaapi_ctx *ctx, VAImage *image,
+ struct mp_image *mpi)
+{
+ int imgfmt = va_fourcc_to_imgfmt(image->format.fourcc);
+ if (imgfmt == IMGFMT_NONE)
+ return false;
+ void *data = NULL;
+ const VAStatus status = vaMapBuffer(ctx->display, image->buf, &data);
+ if (!CHECK_VA_STATUS(ctx, "vaMapBuffer()"))
+ return false;
+
+ *mpi = (struct mp_image) {0};
+ mp_image_setfmt(mpi, imgfmt);
+ mp_image_set_size(mpi, image->width, image->height);
+
+ for (int p = 0; p < image->num_planes; p++) {
+ mpi->stride[p] = image->pitches[p];
+ mpi->planes[p] = (uint8_t *)data + image->offsets[p];
+ }
+
+ if (image->format.fourcc == VA_FOURCC_YV12) {
+ MPSWAP(int, mpi->stride[1], mpi->stride[2]);
+ MPSWAP(uint8_t *, mpi->planes[1], mpi->planes[2]);
+ }
+
+ return true;
+}
+
+static bool va_image_unmap(struct mp_vaapi_ctx *ctx, VAImage *image)
+{
+ const VAStatus status = vaUnmapBuffer(ctx->display, image->buf);
+ return CHECK_VA_STATUS(ctx, "vaUnmapBuffer()");
+}
+
+// va_dst: copy destination, must be IMGFMT_VAAPI
+// sw_src: copy source, must be a software pixel format
+static int va_surface_upload(struct priv *priv, struct mp_image *va_dst,
+ struct mp_image *sw_src)
+{
+ struct va_surface *p = va_surface_in_mp_image(va_dst);
+ if (!p)
+ return -1;
+
+ if (va_surface_alloc_imgfmt(priv, va_dst, sw_src->imgfmt) < 0)
+ return -1;
+
+ struct mp_image img;
+ if (!va_image_map(p->ctx, &p->image, &img))
+ return -1;
+ assert(sw_src->w <= img.w && sw_src->h <= img.h);
+ mp_image_set_size(&img, sw_src->w, sw_src->h); // copy only visible part
+ mp_image_copy(&img, sw_src);
+ va_image_unmap(p->ctx, &p->image);
+
+ if (!p->is_derived) {
+ VAStatus status = vaPutImage(p->display, p->id,
+ p->image.image_id,
+ 0, 0, sw_src->w, sw_src->h,
+ 0, 0, sw_src->w, sw_src->h);
+ if (!CHECK_VA_STATUS(p->ctx, "vaPutImage()"))
+ return -1;
+ }
+
+ if (p->is_derived)
+ va_surface_image_destroy(p);
+ return 0;
+}
+
+struct pool_alloc_ctx {
+ struct mp_vaapi_ctx *vaapi;
+ int rt_format;
+};
+
+static struct mp_image *alloc_pool(void *pctx, int fmt, int w, int h)
+{
+ struct pool_alloc_ctx *alloc_ctx = pctx;
+ if (fmt != IMGFMT_VAAPI)
+ return NULL;
+
+ return alloc_surface(alloc_ctx->vaapi, alloc_ctx->rt_format, w, h);
+}
+
+// The allocator of the given image pool to allocate VAAPI surfaces, using
+// the given rt_format.
+static void va_pool_set_allocator(struct mp_image_pool *pool,
+ struct mp_vaapi_ctx *ctx, int rt_format)
+{
+ struct pool_alloc_ctx *alloc_ctx = talloc_ptrtype(pool, alloc_ctx);
+ *alloc_ctx = (struct pool_alloc_ctx){
+ .vaapi = ctx,
+ .rt_format = rt_format,
+ };
+ mp_image_pool_set_allocator(pool, alloc_pool, alloc_ctx);
+ mp_image_pool_set_lru(pool);
+}
+
+static void flush_output_surfaces(struct priv *p)
+{
+ for (int n = 0; n < MAX_OUTPUT_SURFACES; n++)
+ mp_image_unrefp(&p->output_surfaces[n]);
+ p->output_surface = 0;
+ p->visible_surface = 0;
+}
+
+// See flush_surfaces() remarks - the same applies.
+static void free_video_specific(struct priv *p)
+{
+ flush_output_surfaces(p);
+
+ mp_image_unrefp(&p->black_surface);
+
+ for (int n = 0; n < MAX_OUTPUT_SURFACES; n++)
+ mp_image_unrefp(&p->swdec_surfaces[n]);
+
+ if (p->pool)
+ mp_image_pool_clear(p->pool);
+}
+
+static bool alloc_swdec_surfaces(struct priv *p, int w, int h, int imgfmt)
+{
+ free_video_specific(p);
+ for (int i = 0; i < MAX_OUTPUT_SURFACES; i++) {
+ p->swdec_surfaces[i] = mp_image_pool_get(p->pool, IMGFMT_VAAPI, w, h);
+ if (va_surface_alloc_imgfmt(p, p->swdec_surfaces[i], imgfmt) < 0)
+ return false;
+ }
+ return true;
+}
+
+static void resize(struct priv *p)
+{
+ vo_get_src_dst_rects(p->vo, &p->src_rect, &p->dst_rect, &p->screen_osd_res);
+
+ // It's not clear whether this is needed; maybe not.
+ //vo_x11_clearwindow(p->vo, p->vo->x11->window);
+
+ p->vo->want_redraw = true;
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *p = vo->priv;
+
+ free_video_specific(p);
+
+ vo_x11_config_vo_window(vo);
+
+ if (params->imgfmt != IMGFMT_VAAPI) {
+ if (!alloc_swdec_surfaces(p, params->w, params->h, params->imgfmt))
+ return -1;
+ }
+
+ p->image_params = *params;
+ resize(p);
+ return 0;
+}
+
+static int query_format(struct vo *vo, int imgfmt)
+{
+ struct priv *p = vo->priv;
+ if (imgfmt == IMGFMT_VAAPI || va_image_format_from_imgfmt(p, imgfmt))
+ return 1;
+
+ return 0;
+}
+
+static bool render_to_screen(struct priv *p, struct mp_image *mpi)
+{
+ VAStatus status;
+
+ VASurfaceID surface = va_surface_id(mpi);
+ if (surface == VA_INVALID_ID) {
+ if (!p->black_surface) {
+ int w = p->image_params.w, h = p->image_params.h;
+ // 4:2:0 should work everywhere
+ int fmt = IMGFMT_420P;
+ p->black_surface = mp_image_pool_get(p->pool, IMGFMT_VAAPI, w, h);
+ if (p->black_surface) {
+ struct mp_image *img = mp_image_alloc(fmt, w, h);
+ if (img) {
+ mp_image_clear(img, 0, 0, w, h);
+ if (va_surface_upload(p, p->black_surface, img) < 0)
+ mp_image_unrefp(&p->black_surface);
+ talloc_free(img);
+ }
+ }
+ }
+ surface = va_surface_id(p->black_surface);
+ }
+
+ if (surface == VA_INVALID_ID)
+ return false;
+
+ struct vaapi_osd_part *part = &p->osd_part;
+ if (part->active) {
+ struct vaapi_subpic *sp = &part->subpic;
+ int flags = 0;
+ if (p->osd_screen)
+ flags |= VA_SUBPICTURE_DESTINATION_IS_SCREEN_COORD;
+ status = vaAssociateSubpicture(p->display,
+ sp->id, &surface, 1,
+ sp->src_x, sp->src_y,
+ sp->src_w, sp->src_h,
+ sp->dst_x, sp->dst_y,
+ sp->dst_w, sp->dst_h,
+ flags);
+ CHECK_VA_STATUS(p, "vaAssociateSubpicture()");
+ }
+
+ int flags = va_get_colorspace_flag(p->image_params.color.space) |
+ p->scaling | VA_FRAME_PICTURE;
+ status = vaPutSurface(p->display,
+ surface,
+ p->vo->x11->window,
+ p->src_rect.x0,
+ p->src_rect.y0,
+ p->src_rect.x1 - p->src_rect.x0,
+ p->src_rect.y1 - p->src_rect.y0,
+ p->dst_rect.x0,
+ p->dst_rect.y0,
+ p->dst_rect.x1 - p->dst_rect.x0,
+ p->dst_rect.y1 - p->dst_rect.y0,
+ NULL, 0,
+ flags);
+ CHECK_VA_STATUS(p, "vaPutSurface()");
+
+ if (part->active) {
+ struct vaapi_subpic *sp = &part->subpic;
+ status = vaDeassociateSubpicture(p->display, sp->id,
+ &surface, 1);
+ CHECK_VA_STATUS(p, "vaDeassociateSubpicture()");
+ }
+
+ return true;
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ p->visible_surface = p->output_surface;
+ render_to_screen(p, p->output_surfaces[p->output_surface]);
+ p->output_surface = (p->output_surface + 1) % MAX_OUTPUT_SURFACES;
+ vo_x11_present(vo);
+ present_sync_swap(vo->x11->present);
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ present_sync_get_info(x11->present, info);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+ struct mp_image *mpi = frame->current;
+
+ if (mpi && mpi->imgfmt != IMGFMT_VAAPI) {
+ struct mp_image *dst = p->swdec_surfaces[p->output_surface];
+ if (!dst || va_surface_upload(p, dst, mpi) < 0) {
+ MP_WARN(vo, "Could not upload surface.\n");
+ talloc_free(mpi);
+ return;
+ }
+ mp_image_copy_attributes(dst, mpi);
+ mpi = mp_image_new_ref(dst);
+ }
+
+ talloc_free(p->output_surfaces[p->output_surface]);
+ p->output_surfaces[p->output_surface] = mpi;
+
+ draw_osd(vo);
+}
+
+static void free_subpicture(struct priv *p, struct vaapi_osd_image *img)
+{
+ if (img->image.image_id != VA_INVALID_ID)
+ vaDestroyImage(p->display, img->image.image_id);
+ if (img->subpic_id != VA_INVALID_ID)
+ vaDestroySubpicture(p->display, img->subpic_id);
+ img->image.image_id = VA_INVALID_ID;
+ img->subpic_id = VA_INVALID_ID;
+}
+
+static int new_subpicture(struct priv *p, int w, int h,
+ struct vaapi_osd_image *out)
+{
+ VAStatus status;
+
+ free_subpicture(p, out);
+
+ struct vaapi_osd_image m = {
+ .image = {.image_id = VA_INVALID_ID, .buf = VA_INVALID_ID},
+ .subpic_id = VA_INVALID_ID,
+ .w = w,
+ .h = h,
+ };
+
+ status = vaCreateImage(p->display, &p->osd_format, w, h, &m.image);
+ if (!CHECK_VA_STATUS(p, "vaCreateImage()"))
+ goto error;
+ status = vaCreateSubpicture(p->display, m.image.image_id, &m.subpic_id);
+ if (!CHECK_VA_STATUS(p, "vaCreateSubpicture()"))
+ goto error;
+
+ *out = m;
+ return 0;
+
+error:
+ free_subpicture(p, &m);
+ MP_ERR(p, "failed to allocate OSD sub-picture of size %dx%d.\n", w, h);
+ return -1;
+}
+
+static void draw_osd(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ struct mp_image *cur = p->output_surfaces[p->output_surface];
+ double pts = cur ? cur->pts : 0;
+
+ if (!p->osd_format.fourcc)
+ return;
+
+ struct mp_osd_res vid_res = osd_res_from_image_params(vo->params);
+
+ struct mp_osd_res *res;
+ if (p->osd_screen) {
+ res = &p->screen_osd_res;
+ } else {
+ res = &vid_res;
+ }
+
+ p->osd_part.active = false;
+
+ if (!p->osd_cache)
+ p->osd_cache = mp_draw_sub_alloc(p, vo->global);
+
+ struct sub_bitmap_list *sbs = osd_render(vo->osd, *res, pts, 0,
+ mp_draw_sub_formats);
+
+ struct mp_rect act_rc[1], mod_rc[64];
+ int num_act_rc = 0, num_mod_rc = 0;
+
+ struct mp_image *osd = mp_draw_sub_overlay(p->osd_cache, sbs,
+ act_rc, MP_ARRAY_SIZE(act_rc), &num_act_rc,
+ mod_rc, MP_ARRAY_SIZE(mod_rc), &num_mod_rc);
+
+ if (!osd)
+ goto error;
+
+ struct vaapi_osd_part *part = &p->osd_part;
+
+ part->active = false;
+
+ int w = res->w;
+ int h = res->h;
+ if (part->image.w != w || part->image.h != h) {
+ if (new_subpicture(p, w, h, &part->image) < 0)
+ goto error;
+ }
+
+ struct vaapi_osd_image *img = &part->image;
+ struct mp_image vaimg;
+ if (!va_image_map(p->mpvaapi, &img->image, &vaimg))
+ goto error;
+
+ for (int n = 0; n < num_mod_rc; n++) {
+ struct mp_rect *rc = &mod_rc[n];
+
+ int rw = mp_rect_w(*rc);
+ int rh = mp_rect_h(*rc);
+
+ void *src = mp_image_pixel_ptr(osd, 0, rc->x0, rc->y0);
+ void *dst = vaimg.planes[0] + rc->y0 * vaimg.stride[0] + rc->x0 * 4;
+
+ memcpy_pic(dst, src, rw * 4, rh, vaimg.stride[0], osd->stride[0]);
+ }
+
+ if (!va_image_unmap(p->mpvaapi, &img->image))
+ goto error;
+
+ if (num_act_rc) {
+ struct mp_rect rc = act_rc[0];
+ rc.x0 = rc.y0 = 0; // must be a Mesa bug
+ part->subpic = (struct vaapi_subpic) {
+ .id = img->subpic_id,
+ .src_x = rc.x0, .src_y = rc.y0,
+ .src_w = mp_rect_w(rc), .src_h = mp_rect_h(rc),
+ .dst_x = rc.x0, .dst_y = rc.y0,
+ .dst_w = mp_rect_w(rc), .dst_h = mp_rect_h(rc),
+ };
+ part->active = true;
+ }
+
+error:
+ talloc_free(sbs);
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ struct priv *p = vo->priv;
+
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ resize(p);
+ return VO_TRUE;
+ }
+
+ int events = 0;
+ int r = vo_x11_control(vo, &events, request, data);
+ if (events & VO_EVENT_RESIZE)
+ resize(p);
+ if (events & VO_EVENT_EXPOSE)
+ vo->want_redraw = true;
+ vo_event(vo, events);
+ return r;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ free_video_specific(p);
+ talloc_free(p->pool);
+
+ struct vaapi_osd_part *part = &p->osd_part;
+ free_subpicture(p, &part->image);
+
+ if (vo->hwdec_devs) {
+ hwdec_devices_remove(vo->hwdec_devs, &p->mpvaapi->hwctx);
+ hwdec_devices_destroy(vo->hwdec_devs);
+ }
+
+ va_destroy(p->mpvaapi);
+
+ vo_x11_uninit(vo);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ p->vo = vo;
+ p->log = vo->log;
+
+ VAStatus status;
+
+ if (!vo_x11_init(vo))
+ goto fail;
+
+ if (!vo_x11_create_vo_window(vo, NULL, "vaapi"))
+ goto fail;
+
+ p->display = vaGetDisplay(vo->x11->display);
+ if (!p->display)
+ goto fail;
+
+ p->mpvaapi = va_initialize(p->display, p->log, false);
+ if (!p->mpvaapi) {
+ vaTerminate(p->display);
+ p->display = NULL;
+ goto fail;
+ }
+
+ if (va_guess_if_emulated(p->mpvaapi)) {
+ MP_WARN(vo, "VA-API is most likely emulated via VDPAU.\n"
+ "It's better to use VDPAU directly with: --vo=vdpau\n");
+ }
+
+ va_get_formats(p);
+ if (!p->image_formats)
+ goto fail;
+
+ p->mpvaapi->hwctx.hw_imgfmt = IMGFMT_VAAPI;
+ p->pool = mp_image_pool_new(p);
+ va_pool_set_allocator(p->pool, p->mpvaapi, VA_RT_FORMAT_YUV420);
+
+ int max_subpic_formats = vaMaxNumSubpictureFormats(p->display);
+ p->va_subpic_formats = talloc_array(vo, VAImageFormat, max_subpic_formats);
+ p->va_subpic_flags = talloc_array(vo, unsigned int, max_subpic_formats);
+ status = vaQuerySubpictureFormats(p->display,
+ p->va_subpic_formats,
+ p->va_subpic_flags,
+ &p->va_num_subpic_formats);
+ if (!CHECK_VA_STATUS(p, "vaQuerySubpictureFormats()"))
+ p->va_num_subpic_formats = 0;
+ MP_VERBOSE(vo, "%d subpicture formats available:\n",
+ p->va_num_subpic_formats);
+
+ for (int i = 0; i < p->va_num_subpic_formats; i++) {
+ MP_VERBOSE(vo, " %s, flags 0x%x\n",
+ mp_tag_str(p->va_subpic_formats[i].fourcc),
+ p->va_subpic_flags[i]);
+ if (p->va_subpic_formats[i].fourcc == OSD_VA_FORMAT) {
+ p->osd_format = p->va_subpic_formats[i];
+ if (!p->force_scaled_osd) {
+ p->osd_screen =
+ p->va_subpic_flags[i] & VA_SUBPICTURE_DESTINATION_IS_SCREEN_COORD;
+ }
+ }
+ }
+
+ if (!p->osd_format.fourcc)
+ MP_ERR(vo, "OSD format not supported. Disabling OSD.\n");
+
+ struct vaapi_osd_part *part = &p->osd_part;
+ part->image.image.image_id = VA_INVALID_ID;
+ part->image.subpic_id = VA_INVALID_ID;
+
+ int max_display_attrs = vaMaxNumDisplayAttributes(p->display);
+ p->va_display_attrs = talloc_array(vo, VADisplayAttribute, max_display_attrs);
+ if (p->va_display_attrs) {
+ status = vaQueryDisplayAttributes(p->display, p->va_display_attrs,
+ &p->va_num_display_attrs);
+ if (!CHECK_VA_STATUS(p, "vaQueryDisplayAttributes()"))
+ p->va_num_display_attrs = 0;
+ p->mp_display_attr = talloc_zero_array(vo, int, p->va_num_display_attrs);
+ }
+
+ vo->hwdec_devs = hwdec_devices_create();
+ hwdec_devices_add(vo->hwdec_devs, &p->mpvaapi->hwctx);
+
+ MP_WARN(vo, "Warning: this compatibility VO is low quality and may "
+ "have issues with OSD, scaling, screenshots and more.\n"
+ "vo=gpu is the preferred choice in any case and "
+ "includes VA-API support via hwdec=vaapi or vaapi-copy.\n");
+
+ return 0;
+
+fail:
+ uninit(vo);
+ return -1;
+}
+
+#define OPT_BASE_STRUCT struct priv
+
+const struct vo_driver video_out_vaapi = {
+ .description = "VA API with X11",
+ .name = "vaapi",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .wakeup = vo_x11_wakeup,
+ .wait_events = vo_x11_wait_events,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+ .priv_defaults = &(const struct priv) {
+ .scaling = VA_FILTER_SCALING_DEFAULT,
+ },
+ .options = (const struct m_option[]) {
+ {"scaling", OPT_CHOICE(scaling,
+ {"default", VA_FILTER_SCALING_DEFAULT},
+ {"fast", VA_FILTER_SCALING_FAST},
+ {"hq", VA_FILTER_SCALING_HQ},
+ {"nla", VA_FILTER_SCALING_NL_ANAMORPHIC})},
+ {"scaled-osd", OPT_BOOL(force_scaled_osd)},
+ {0}
+ },
+ .options_prefix = "vo-vaapi",
+};
diff --git a/video/out/vo_vdpau.c b/video/out/vo_vdpau.c
new file mode 100644
index 0000000..d6b261f
--- /dev/null
+++ b/video/out/vo_vdpau.c
@@ -0,0 +1,1139 @@
+/*
+ * VDPAU video output driver
+ *
+ * Copyright (C) 2008 NVIDIA (Rajib Mahapatra <rmahapatra@nvidia.com>)
+ * Copyright (C) 2009 Uoti Urpala
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Actual decoding is done in video/decode/vdpau.c
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <limits.h>
+#include <assert.h>
+
+#include "video/vdpau.h"
+#include "video/vdpau_mixer.h"
+#include "video/hwdec.h"
+#include "common/msg.h"
+#include "options/options.h"
+#include "mpv_talloc.h"
+#include "vo.h"
+#include "x11_common.h"
+#include "video/csputils.h"
+#include "sub/osd.h"
+#include "options/m_option.h"
+#include "video/mp_image.h"
+#include "osdep/timer.h"
+
+// Returns x + a, but wrapped around to the range [0, m)
+// a must be within [-m, m], x within [0, m)
+#define WRAP_ADD(x, a, m) ((a) < 0 \
+ ? ((x)+(a)+(m) < (m) ? (x)+(a)+(m) : (x)+(a)) \
+ : ((x)+(a) < (m) ? (x)+(a) : (x)+(a)-(m)))
+
+
+/* number of video and output surfaces */
+#define MAX_OUTPUT_SURFACES 15
+
+/* Pixelformat used for output surfaces */
+#define OUTPUT_RGBA_FORMAT VDP_RGBA_FORMAT_B8G8R8A8
+
+/*
+ * Global variable declaration - VDPAU specific
+ */
+
+struct vdpctx {
+ struct mp_vdpau_ctx *mpvdp;
+ struct vdp_functions *vdp;
+ VdpDevice vdp_device;
+ uint64_t preemption_counter;
+
+ struct m_color colorkey;
+
+ VdpPresentationQueueTarget flip_target;
+ VdpPresentationQueue flip_queue;
+
+ VdpOutputSurface output_surfaces[MAX_OUTPUT_SURFACES];
+ int num_output_surfaces;
+ VdpOutputSurface black_pixel;
+ VdpOutputSurface rotation_surface;
+
+ struct mp_image *current_image;
+ int64_t current_pts;
+ int current_duration;
+
+ int output_surface_w, output_surface_h;
+ int rotation;
+
+ bool force_yuv;
+ struct mp_vdpau_mixer *video_mixer;
+ bool pullup;
+ float denoise;
+ float sharpen;
+ int hqscaling;
+ bool chroma_deint;
+ int flip_offset_window;
+ int flip_offset_fs;
+ int64_t flip_offset_us;
+
+ VdpRect src_rect_vid;
+ VdpRect out_rect_vid;
+ struct mp_osd_res osd_rect;
+ VdpBool supports_a8;
+
+ int surface_num; // indexes output_surfaces
+ int query_surface_num;
+ VdpTime recent_vsync_time;
+ float user_fps;
+ bool composite_detect;
+ int vsync_interval;
+ uint64_t last_queue_time;
+ uint64_t queue_time[MAX_OUTPUT_SURFACES];
+ uint64_t last_ideal_time;
+ bool dropped_frame;
+ uint64_t dropped_time;
+ uint32_t vid_width, vid_height;
+ uint32_t image_format;
+ VdpYCbCrFormat vdp_pixel_format;
+ bool rgb_mode;
+
+ // OSD
+ struct osd_bitmap_surface {
+ VdpRGBAFormat format;
+ VdpBitmapSurface surface;
+ uint32_t surface_w, surface_h;
+ // List of surfaces to be rendered
+ struct osd_target {
+ VdpRect source;
+ VdpRect dest;
+ VdpColor color;
+ } *targets;
+ int targets_size;
+ int render_count;
+ int change_id;
+ } osd_surfaces[MAX_OSD_PARTS];
+};
+
+static bool status_ok(struct vo *vo);
+
+static int video_to_output_surface(struct vo *vo, struct mp_image *mpi)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpTime dummy;
+ VdpStatus vdp_st;
+
+ VdpOutputSurface output_surface = vc->output_surfaces[vc->surface_num];
+ VdpRect *output_rect = &vc->out_rect_vid;
+ VdpRect *video_rect = &vc->src_rect_vid;
+
+ vdp_st = vdp->presentation_queue_block_until_surface_idle(vc->flip_queue,
+ output_surface,
+ &dummy);
+ CHECK_VDP_WARNING(vo, "Error when calling "
+ "vdp_presentation_queue_block_until_surface_idle");
+
+ // Clear the borders between video and window (if there are any).
+ // For some reason, video_mixer_render doesn't need it for YUV.
+ // Also, if there is nothing to render, at least clear the screen.
+ if (vc->rgb_mode || !mpi || mpi->params.rotate != 0) {
+ int flags = VDP_OUTPUT_SURFACE_RENDER_ROTATE_0;
+ vdp_st = vdp->output_surface_render_output_surface(output_surface,
+ NULL, vc->black_pixel,
+ NULL, NULL, NULL,
+ flags);
+ CHECK_VDP_WARNING(vo, "Error clearing screen");
+ }
+
+ if (!mpi)
+ return -1;
+
+ struct mp_vdpau_mixer_frame *frame = mp_vdpau_mixed_frame_get(mpi);
+ struct mp_vdpau_mixer_opts opts = {0};
+ if (frame)
+ opts = frame->opts;
+
+ // Apply custom vo_vdpau suboptions.
+ opts.chroma_deint |= vc->chroma_deint;
+ opts.pullup |= vc->pullup;
+ opts.denoise = MPCLAMP(opts.denoise + vc->denoise, 0, 1);
+ opts.sharpen = MPCLAMP(opts.sharpen + vc->sharpen, -1, 1);
+ if (vc->hqscaling)
+ opts.hqscaling = vc->hqscaling;
+
+ if (mpi->params.rotate != 0) {
+ int flags;
+ VdpRect r_rect;
+ switch (mpi->params.rotate) {
+ case 90:
+ r_rect.y0 = output_rect->x0;
+ r_rect.y1 = output_rect->x1;
+ r_rect.x0 = output_rect->y0;
+ r_rect.x1 = output_rect->y1;
+ flags = VDP_OUTPUT_SURFACE_RENDER_ROTATE_90;
+ break;
+ case 180:
+ r_rect.x0 = output_rect->x0;
+ r_rect.x1 = output_rect->x1;
+ r_rect.y0 = output_rect->y0;
+ r_rect.y1 = output_rect->y1;
+ flags = VDP_OUTPUT_SURFACE_RENDER_ROTATE_180;
+ break;
+ case 270:
+ r_rect.y0 = output_rect->x0;
+ r_rect.y1 = output_rect->x1;
+ r_rect.x0 = output_rect->y0;
+ r_rect.x1 = output_rect->y1;
+ flags = VDP_OUTPUT_SURFACE_RENDER_ROTATE_270;
+ break;
+ default:
+ MP_ERR(vo, "Unsupported rotation angle: %u\n", mpi->params.rotate);
+ return -1;
+ }
+
+ mp_vdpau_mixer_render(vc->video_mixer, &opts, vc->rotation_surface,
+ &r_rect, mpi, video_rect);
+ vdp_st = vdp->output_surface_render_output_surface(output_surface,
+ output_rect,
+ vc->rotation_surface,
+ &r_rect,
+ NULL,
+ NULL,
+ flags);
+ CHECK_VDP_WARNING(vo, "Error rendering rotated frame");
+ } else {
+ mp_vdpau_mixer_render(vc->video_mixer, &opts, output_surface,
+ output_rect, mpi, video_rect);
+ }
+ return 0;
+}
+
+static void forget_frames(struct vo *vo, bool seek_reset)
+{
+ struct vdpctx *vc = vo->priv;
+
+ if (!seek_reset)
+ mp_image_unrefp(&vc->current_image);
+
+ vc->dropped_frame = false;
+}
+
+static int s_size(int max, int s, int disp)
+{
+ disp = MPMAX(1, disp);
+ return MPMIN(max, MPMAX(s, disp));
+}
+
+static void resize(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+ struct mp_rect src_rect;
+ struct mp_rect dst_rect;
+ vo_get_src_dst_rects(vo, &src_rect, &dst_rect, &vc->osd_rect);
+ vc->out_rect_vid.x0 = dst_rect.x0;
+ vc->out_rect_vid.x1 = dst_rect.x1;
+ vc->out_rect_vid.y0 = dst_rect.y0;
+ vc->out_rect_vid.y1 = dst_rect.y1;
+ if (vo->params->rotate == 90 || vo->params->rotate == 270) {
+ vc->src_rect_vid.y0 = src_rect.x0;
+ vc->src_rect_vid.y1 = src_rect.x1;
+ vc->src_rect_vid.x0 = src_rect.y0;
+ vc->src_rect_vid.x1 = src_rect.y1;
+ } else {
+ vc->src_rect_vid.x0 = src_rect.x0;
+ vc->src_rect_vid.x1 = src_rect.x1;
+ vc->src_rect_vid.y0 = src_rect.y0;
+ vc->src_rect_vid.y1 = src_rect.y1;
+ }
+
+ VdpBool ok;
+ uint32_t max_w, max_h;
+ vdp_st = vdp->output_surface_query_capabilities(vc->vdp_device,
+ OUTPUT_RGBA_FORMAT,
+ &ok, &max_w, &max_h);
+ if (vdp_st != VDP_STATUS_OK || !ok)
+ return;
+
+ vc->flip_offset_us = vo->opts->fullscreen ?
+ 1000LL * vc->flip_offset_fs :
+ 1000LL * vc->flip_offset_window;
+ vo_set_queue_params(vo, vc->flip_offset_us * 1000, 1);
+
+ if (vc->output_surface_w < vo->dwidth || vc->output_surface_h < vo->dheight ||
+ vc->rotation != vo->params->rotate)
+ {
+ vc->output_surface_w = s_size(max_w, vc->output_surface_w, vo->dwidth);
+ vc->output_surface_h = s_size(max_h, vc->output_surface_h, vo->dheight);
+ // Creation of output_surfaces
+ for (int i = 0; i < vc->num_output_surfaces; i++)
+ if (vc->output_surfaces[i] != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_destroy(vc->output_surfaces[i]);
+ CHECK_VDP_WARNING(vo, "Error when calling "
+ "vdp_output_surface_destroy");
+ }
+ for (int i = 0; i < vc->num_output_surfaces; i++) {
+ vdp_st = vdp->output_surface_create(vc->vdp_device,
+ OUTPUT_RGBA_FORMAT,
+ vc->output_surface_w,
+ vc->output_surface_h,
+ &vc->output_surfaces[i]);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_output_surface_create");
+ MP_DBG(vo, "vdpau out create: %u\n",
+ vc->output_surfaces[i]);
+ }
+ if (vc->rotation_surface != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_destroy(vc->rotation_surface);
+ CHECK_VDP_WARNING(vo, "Error when calling "
+ "vdp_output_surface_destroy");
+ vc->rotation_surface = VDP_INVALID_HANDLE;
+ }
+ if (vo->params->rotate == 90 || vo->params->rotate == 270) {
+ vdp_st = vdp->output_surface_create(vc->vdp_device,
+ OUTPUT_RGBA_FORMAT,
+ vc->output_surface_h,
+ vc->output_surface_w,
+ &vc->rotation_surface);
+ } else if (vo->params->rotate == 180) {
+ vdp_st = vdp->output_surface_create(vc->vdp_device,
+ OUTPUT_RGBA_FORMAT,
+ vc->output_surface_w,
+ vc->output_surface_h,
+ &vc->rotation_surface);
+ }
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_output_surface_create");
+ MP_DBG(vo, "vdpau rotation surface create: %u\n",
+ vc->rotation_surface);
+ }
+ vc->rotation = vo->params->rotate;
+ vo->want_redraw = true;
+}
+
+static int win_x11_init_vdpau_flip_queue(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ struct vo_x11_state *x11 = vo->x11;
+ VdpStatus vdp_st;
+
+ if (vc->flip_target == VDP_INVALID_HANDLE) {
+ vdp_st = vdp->presentation_queue_target_create_x11(vc->vdp_device,
+ x11->window,
+ &vc->flip_target);
+ CHECK_VDP_ERROR(vo, "Error when calling "
+ "vdp_presentation_queue_target_create_x11");
+ }
+
+ /* Empirically this seems to be the first call which fails when we
+ * try to reinit after preemption while the user is still switched
+ * from X to a virtual terminal (creating the vdp_device initially
+ * succeeds, as does creating the flip_target above). This is
+ * probably not guaranteed behavior.
+ */
+ if (vc->flip_queue == VDP_INVALID_HANDLE) {
+ vdp_st = vdp->presentation_queue_create(vc->vdp_device, vc->flip_target,
+ &vc->flip_queue);
+ CHECK_VDP_ERROR(vo, "Error when calling vdp_presentation_queue_create");
+ }
+
+ if (vc->colorkey.a > 0) {
+ VdpColor color = {
+ .red = vc->colorkey.r / 255.0,
+ .green = vc->colorkey.g / 255.0,
+ .blue = vc->colorkey.b / 255.0,
+ .alpha = 0,
+ };
+ vdp_st = vdp->presentation_queue_set_background_color(vc->flip_queue,
+ &color);
+ CHECK_VDP_WARNING(vo, "Error setting colorkey");
+ }
+
+ if (vc->composite_detect && vo_x11_screen_is_composited(vo)) {
+ MP_INFO(vo, "Compositing window manager detected. Assuming timing info "
+ "is inaccurate.\n");
+ vc->user_fps = -1;
+ }
+
+ return 0;
+}
+
+// Free everything specific to a certain video file
+static void free_video_specific(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+
+ forget_frames(vo, false);
+
+ if (vc->black_pixel != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_destroy(vc->black_pixel);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_output_surface_destroy");
+ }
+ vc->black_pixel = VDP_INVALID_HANDLE;
+}
+
+static int initialize_vdpau_objects(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+
+ mp_vdpau_get_format(vc->image_format, NULL, &vc->vdp_pixel_format);
+
+ vc->video_mixer->initialized = false;
+
+ if (win_x11_init_vdpau_flip_queue(vo) < 0)
+ return -1;
+
+ if (vc->black_pixel == VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_create(vc->vdp_device, OUTPUT_RGBA_FORMAT,
+ 1, 1, &vc->black_pixel);
+ CHECK_VDP_ERROR(vo, "Allocating clearing surface");
+ const char data[4] = {0};
+ vdp_st = vdp->output_surface_put_bits_native(vc->black_pixel,
+ (const void*[]){data},
+ (uint32_t[]){4}, NULL);
+ CHECK_VDP_ERROR(vo, "Initializing clearing surface");
+ }
+
+ forget_frames(vo, false);
+ resize(vo);
+ return 0;
+}
+
+static void mark_vdpau_objects_uninitialized(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+
+ forget_frames(vo, false);
+ vc->black_pixel = VDP_INVALID_HANDLE;
+ vc->flip_queue = VDP_INVALID_HANDLE;
+ vc->flip_target = VDP_INVALID_HANDLE;
+ for (int i = 0; i < MAX_OUTPUT_SURFACES; i++)
+ vc->output_surfaces[i] = VDP_INVALID_HANDLE;
+ vc->rotation_surface = VDP_INVALID_HANDLE;
+ vc->vdp_device = VDP_INVALID_HANDLE;
+ for (int i = 0; i < MAX_OSD_PARTS; i++) {
+ struct osd_bitmap_surface *sfc = &vc->osd_surfaces[i];
+ sfc->change_id = 0;
+ *sfc = (struct osd_bitmap_surface){
+ .surface = VDP_INVALID_HANDLE,
+ };
+ }
+ vc->output_surface_w = vc->output_surface_h = -1;
+}
+
+static bool check_preemption(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+
+ int r = mp_vdpau_handle_preemption(vc->mpvdp, &vc->preemption_counter);
+ if (r < 1) {
+ mark_vdpau_objects_uninitialized(vo);
+ if (r < 0)
+ return false;
+ vc->vdp_device = vc->mpvdp->vdp_device;
+ if (initialize_vdpau_objects(vo) < 0)
+ return false;
+ }
+ return true;
+}
+
+static bool status_ok(struct vo *vo)
+{
+ return vo->config_ok && check_preemption(vo);
+}
+
+/*
+ * connect to X server, create and map window, initialize all
+ * VDPAU objects, create different surfaces etc.
+ */
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+
+ if (!check_preemption(vo))
+ {
+ /*
+ * When prempted, leave the reconfig() immediately
+ * without reconfiguring the vo_window and without
+ * initializing the vdpau objects. When recovered
+ * from preemption, if there is a difference between
+ * the VD thread parameters and the VO thread parameters
+ * the reconfig() is triggered again.
+ */
+ return 0;
+ }
+
+ VdpChromaType chroma_type = VDP_CHROMA_TYPE_420;
+ mp_vdpau_get_format(params->imgfmt, &chroma_type, NULL);
+
+ VdpBool ok;
+ uint32_t max_w, max_h;
+ vdp_st = vdp->video_surface_query_capabilities(vc->vdp_device, chroma_type,
+ &ok, &max_w, &max_h);
+ CHECK_VDP_ERROR(vo, "Error when calling vdp_video_surface_query_capabilities");
+
+ if (!ok)
+ return -1;
+ if (params->w > max_w || params->h > max_h) {
+ if (ok)
+ MP_ERR(vo, "Video too large for vdpau.\n");
+ return -1;
+ }
+
+ vc->image_format = params->imgfmt;
+ vc->vid_width = params->w;
+ vc->vid_height = params->h;
+
+ vc->rgb_mode = mp_vdpau_get_rgb_format(params->imgfmt, NULL);
+
+ free_video_specific(vo);
+
+ vo_x11_config_vo_window(vo);
+
+ if (initialize_vdpau_objects(vo) < 0)
+ return -1;
+
+ return 0;
+}
+
+static void draw_osd_part(struct vo *vo, int index)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+ struct osd_bitmap_surface *sfc = &vc->osd_surfaces[index];
+ VdpOutputSurface output_surface = vc->output_surfaces[vc->surface_num];
+ int i;
+
+ VdpOutputSurfaceRenderBlendState blend_state = {
+ .struct_version = VDP_OUTPUT_SURFACE_RENDER_BLEND_STATE_VERSION,
+ .blend_factor_source_color =
+ VDP_OUTPUT_SURFACE_RENDER_BLEND_FACTOR_SRC_ALPHA,
+ .blend_factor_source_alpha =
+ VDP_OUTPUT_SURFACE_RENDER_BLEND_FACTOR_ZERO,
+ .blend_factor_destination_color =
+ VDP_OUTPUT_SURFACE_RENDER_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
+ .blend_factor_destination_alpha =
+ VDP_OUTPUT_SURFACE_RENDER_BLEND_FACTOR_ZERO,
+ .blend_equation_color = VDP_OUTPUT_SURFACE_RENDER_BLEND_EQUATION_ADD,
+ .blend_equation_alpha = VDP_OUTPUT_SURFACE_RENDER_BLEND_EQUATION_ADD,
+ };
+
+ VdpOutputSurfaceRenderBlendState blend_state_premultiplied = blend_state;
+ blend_state_premultiplied.blend_factor_source_color =
+ VDP_OUTPUT_SURFACE_RENDER_BLEND_FACTOR_ONE;
+
+ for (i = 0; i < sfc->render_count; i++) {
+ VdpOutputSurfaceRenderBlendState *blend = &blend_state;
+ if (sfc->format == VDP_RGBA_FORMAT_B8G8R8A8)
+ blend = &blend_state_premultiplied;
+ vdp_st = vdp->
+ output_surface_render_bitmap_surface(output_surface,
+ &sfc->targets[i].dest,
+ sfc->surface,
+ &sfc->targets[i].source,
+ &sfc->targets[i].color,
+ blend,
+ VDP_OUTPUT_SURFACE_RENDER_ROTATE_0);
+ CHECK_VDP_WARNING(vo, "OSD: Error when rendering");
+ }
+}
+
+static int next_pow2(int v)
+{
+ for (int x = 0; x < 30; x++) {
+ if ((1 << x) >= v)
+ return 1 << x;
+ }
+ return INT_MAX;
+}
+
+static void generate_osd_part(struct vo *vo, struct sub_bitmaps *imgs)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+ struct osd_bitmap_surface *sfc = &vc->osd_surfaces[imgs->render_index];
+
+ if (imgs->change_id == sfc->change_id)
+ return; // Nothing changed and we still have the old data
+
+ sfc->change_id = imgs->change_id;
+ sfc->render_count = 0;
+
+ if (imgs->format == SUBBITMAP_EMPTY || imgs->num_parts == 0)
+ return;
+
+ VdpRGBAFormat format;
+ switch (imgs->format) {
+ case SUBBITMAP_LIBASS:
+ format = VDP_RGBA_FORMAT_A8;
+ break;
+ case SUBBITMAP_BGRA:
+ format = VDP_RGBA_FORMAT_B8G8R8A8;
+ break;
+ default:
+ MP_ASSERT_UNREACHABLE();
+ };
+
+ assert(imgs->packed);
+
+ int r_w = next_pow2(imgs->packed_w);
+ int r_h = next_pow2(imgs->packed_h);
+
+ if (sfc->format != format || sfc->surface == VDP_INVALID_HANDLE ||
+ sfc->surface_w < r_w || sfc->surface_h < r_h)
+ {
+ MP_VERBOSE(vo, "Allocating a %dx%d surface for OSD bitmaps.\n", r_w, r_h);
+
+ uint32_t m_w = 0, m_h = 0;
+ vdp_st = vdp->bitmap_surface_query_capabilities(vc->vdp_device, format,
+ &(VdpBool){0}, &m_w, &m_h);
+ CHECK_VDP_WARNING(vo, "Query to get max OSD surface size failed");
+
+ if (r_w > m_w || r_h > m_h) {
+ MP_ERR(vo, "OSD bitmaps do not fit on a surface with the maximum "
+ "supported size\n");
+ return;
+ }
+
+ if (sfc->surface != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->bitmap_surface_destroy(sfc->surface);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_bitmap_surface_destroy");
+ }
+
+ VdpBitmapSurface surface;
+ vdp_st = vdp->bitmap_surface_create(vc->vdp_device, format,
+ r_w, r_h, true, &surface);
+ CHECK_VDP_WARNING(vo, "OSD: error when creating surface");
+ if (vdp_st != VDP_STATUS_OK)
+ return;
+
+ sfc->surface = surface;
+ sfc->surface_w = r_w;
+ sfc->surface_h = r_h;
+ sfc->format = format;
+ }
+
+ void *data = imgs->packed->planes[0];
+ int stride = imgs->packed->stride[0];
+ VdpRect rc = {0, 0, imgs->packed_w, imgs->packed_h};
+ vdp_st = vdp->bitmap_surface_put_bits_native(sfc->surface,
+ &(const void *){data},
+ &(uint32_t){stride},
+ &rc);
+ CHECK_VDP_WARNING(vo, "OSD: putbits failed");
+
+ MP_TARRAY_GROW(vc, sfc->targets, imgs->num_parts);
+ sfc->render_count = imgs->num_parts;
+
+ for (int i = 0; i < imgs->num_parts; i++) {
+ struct sub_bitmap *b = &imgs->parts[i];
+ struct osd_target *target = &sfc->targets[i];
+ target->source = (VdpRect){b->src_x, b->src_y,
+ b->src_x + b->w, b->src_y + b->h};
+ target->dest = (VdpRect){b->x, b->y, b->x + b->dw, b->y + b->dh};
+ target->color = (VdpColor){1, 1, 1, 1};
+ if (imgs->format == SUBBITMAP_LIBASS) {
+ uint32_t color = b->libass.color;
+ target->color.alpha = 1.0 - ((color >> 0) & 0xff) / 255.0;
+ target->color.blue = ((color >> 8) & 0xff) / 255.0;
+ target->color.green = ((color >> 16) & 0xff) / 255.0;
+ target->color.red = ((color >> 24) & 0xff) / 255.0;
+ }
+ }
+}
+
+static void draw_osd_cb(void *ctx, struct sub_bitmaps *imgs)
+{
+ struct vo *vo = ctx;
+ generate_osd_part(vo, imgs);
+ draw_osd_part(vo, imgs->render_index);
+}
+
+static void draw_osd(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+
+ if (!status_ok(vo))
+ return;
+
+ bool formats[SUBBITMAP_COUNT] = {
+ [SUBBITMAP_LIBASS] = vc->supports_a8,
+ [SUBBITMAP_BGRA] = true,
+ };
+
+ double pts = vc->current_image ? vc->current_image->pts : 0;
+ osd_draw(vo->osd, vc->osd_rect, pts, 0, formats, draw_osd_cb, vo);
+}
+
+static int update_presentation_queue_status(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+
+ while (vc->query_surface_num != vc->surface_num) {
+ VdpTime vtime;
+ VdpPresentationQueueStatus status;
+ VdpOutputSurface surface = vc->output_surfaces[vc->query_surface_num];
+ vdp_st = vdp->presentation_queue_query_surface_status(vc->flip_queue,
+ surface,
+ &status, &vtime);
+ CHECK_VDP_WARNING(vo, "Error calling "
+ "presentation_queue_query_surface_status");
+ if (mp_msg_test(vo->log, MSGL_TRACE)) {
+ VdpTime current;
+ vdp_st = vdp->presentation_queue_get_time(vc->flip_queue, &current);
+ CHECK_VDP_WARNING(vo, "Error when calling "
+ "vdp_presentation_queue_get_time");
+ MP_TRACE(vo, "Vdpau time: %"PRIu64"\n", (uint64_t)current);
+ MP_TRACE(vo, "Surface %d status: %d time: %"PRIu64"\n",
+ (int)surface, (int)status, (uint64_t)vtime);
+ }
+ if (status == VDP_PRESENTATION_QUEUE_STATUS_QUEUED)
+ break;
+ if (vc->vsync_interval > 1) {
+ uint64_t qtime = vc->queue_time[vc->query_surface_num];
+ int diff = ((int64_t)vtime - (int64_t)qtime) / 1e6;
+ MP_TRACE(vo, "Queue time difference: %d ms\n", diff);
+ if (vtime < qtime + vc->vsync_interval / 2)
+ MP_VERBOSE(vo, "Frame shown too early (%d ms)\n", diff);
+ if (vtime > qtime + vc->vsync_interval)
+ MP_VERBOSE(vo, "Frame shown late (%d ms)\n", diff);
+ }
+ vc->query_surface_num = WRAP_ADD(vc->query_surface_num, 1,
+ vc->num_output_surfaces);
+ vc->recent_vsync_time = vtime;
+ }
+ int num_queued = WRAP_ADD(vc->surface_num, -vc->query_surface_num,
+ vc->num_output_surfaces);
+ MP_DBG(vo, "Queued surface count (before add): %d\n", num_queued);
+ return num_queued;
+}
+
+// Return the timestamp of the vsync that must have happened before ts.
+static inline uint64_t prev_vsync(struct vdpctx *vc, uint64_t ts)
+{
+ int64_t diff = (int64_t)(ts - vc->recent_vsync_time);
+ int64_t offset = diff % vc->vsync_interval;
+ if (offset < 0)
+ offset += vc->vsync_interval;
+ return ts - offset;
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+ VdpStatus vdp_st;
+
+ int64_t pts_us = vc->current_pts;
+ int duration = vc->current_duration;
+
+ vc->dropped_frame = true; // changed at end if false
+
+ if (!check_preemption(vo))
+ goto drop;
+
+ vc->vsync_interval = 1;
+ if (vc->user_fps > 0) {
+ vc->vsync_interval = 1e9 / vc->user_fps;
+ } else if (vc->user_fps == 0) {
+ vc->vsync_interval = vo_get_vsync_interval(vo);
+ }
+ vc->vsync_interval = MPMAX(vc->vsync_interval, 1);
+
+ if (duration > INT_MAX / 1000)
+ duration = -1;
+ else
+ duration *= 1000;
+
+ if (vc->vsync_interval == 1)
+ duration = -1; // Make sure drop logic is disabled
+
+ VdpTime vdp_time = 0;
+ vdp_st = vdp->presentation_queue_get_time(vc->flip_queue, &vdp_time);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_presentation_queue_get_time");
+
+ int64_t rel_pts_ns = (pts_us * 1000) - mp_time_ns();
+ if (!pts_us || rel_pts_ns < 0)
+ rel_pts_ns = 0;
+
+ uint64_t now = vdp_time;
+ uint64_t pts = now + rel_pts_ns;
+ uint64_t ideal_pts = pts;
+ uint64_t npts = duration >= 0 ? pts + duration : UINT64_MAX;
+
+ /* This should normally never happen.
+ * - The last queued frame can't have a PTS that goes more than 50ms in the
+ * future. This is guaranteed by vo.c, which currently actually queues
+ * ahead by roughly the flip queue offset. Just to be sure
+ * give some additional room by doubling the time.
+ * - The last vsync can never be in the future.
+ */
+ int64_t max_pts_ahead = vc->flip_offset_us * 1000 * 2;
+ if (vc->last_queue_time > now + max_pts_ahead ||
+ vc->recent_vsync_time > now)
+ {
+ vc->last_queue_time = 0;
+ vc->recent_vsync_time = 0;
+ MP_WARN(vo, "Inconsistent timing detected.\n");
+ }
+
+#define PREV_VSYNC(ts) prev_vsync(vc, ts)
+
+ /* We hope to be here at least one vsync before the frame should be shown.
+ * If we are running late then don't drop the frame unless there is
+ * already one queued for the next vsync; even if we _hope_ to show the
+ * next frame soon enough to mean this one should be dropped we might
+ * not make the target time in reality. Without this check we could drop
+ * every frame, freezing the display completely if video lags behind.
+ */
+ if (now > PREV_VSYNC(MPMAX(pts, vc->last_queue_time + vc->vsync_interval)))
+ npts = UINT64_MAX;
+
+ /* Allow flipping a frame at a vsync if its presentation time is a
+ * bit after that vsync and the change makes the flip time delta
+ * from previous frame better match the target timestamp delta.
+ * This avoids instability with frame timestamps falling near vsyncs.
+ * For example if the frame timestamps were (with vsyncs at
+ * integer values) 0.01, 1.99, 4.01, 5.99, 8.01, ... then
+ * straightforward timing at next vsync would flip the frames at
+ * 1, 2, 5, 6, 9; this changes it to 1, 2, 4, 6, 8 and so on with
+ * regular 2-vsync intervals.
+ *
+ * Also allow moving the frame forward if it looks like we dropped
+ * the previous frame incorrectly (now that we know better after
+ * having final exact timestamp information for this frame) and
+ * there would unnecessarily be a vsync without a frame change.
+ */
+ uint64_t vsync = PREV_VSYNC(pts);
+ if (pts < vsync + vc->vsync_interval / 4
+ && (vsync - PREV_VSYNC(vc->last_queue_time)
+ > pts - vc->last_ideal_time + vc->vsync_interval / 2
+ || (vc->dropped_frame && vsync > vc->dropped_time)))
+ pts -= vc->vsync_interval / 2;
+
+ vc->dropped_time = ideal_pts;
+
+ pts = MPMAX(pts, vc->last_queue_time + vc->vsync_interval);
+ pts = MPMAX(pts, now);
+ if (npts < PREV_VSYNC(pts) + vc->vsync_interval)
+ goto drop;
+
+ int num_flips = update_presentation_queue_status(vo);
+ vsync = vc->recent_vsync_time + num_flips * vc->vsync_interval;
+ pts = MPMAX(pts, now);
+ pts = MPMAX(pts, vsync + (vc->vsync_interval >> 2));
+ vsync = PREV_VSYNC(pts);
+ if (npts < vsync + vc->vsync_interval)
+ goto drop;
+ pts = vsync + (vc->vsync_interval >> 2);
+ VdpOutputSurface frame = vc->output_surfaces[vc->surface_num];
+ vdp_st = vdp->presentation_queue_display(vc->flip_queue, frame,
+ vo->dwidth, vo->dheight, pts);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_presentation_queue_display");
+
+ MP_TRACE(vo, "Queue new surface %d: Vdpau time: %"PRIu64" "
+ "pts: %"PRIu64"\n", (int)frame, now, pts);
+
+ vc->last_queue_time = pts;
+ vc->queue_time[vc->surface_num] = pts;
+ vc->last_ideal_time = ideal_pts;
+ vc->dropped_frame = false;
+ vc->surface_num = WRAP_ADD(vc->surface_num, 1, vc->num_output_surfaces);
+ return;
+
+drop:
+ vo_increment_drop_count(vo, 1);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct vdpctx *vc = vo->priv;
+
+ check_preemption(vo);
+
+ if (frame->current && !frame->redraw) {
+ struct mp_image *vdp_mpi =
+ mp_vdpau_upload_video_surface(vc->mpvdp, frame->current);
+ if (!vdp_mpi)
+ MP_ERR(vo, "Could not upload image.\n");
+
+ talloc_free(vc->current_image);
+ vc->current_image = vdp_mpi;
+ }
+
+ vc->current_pts = frame->pts;
+ vc->current_duration = frame->duration;
+
+ if (status_ok(vo)) {
+ video_to_output_surface(vo, vc->current_image);
+ draw_osd(vo);
+ }
+}
+
+// warning: the size and pixel format of surface must match that of the
+// surfaces in vc->output_surfaces
+static struct mp_image *read_output_surface(struct vo *vo,
+ VdpOutputSurface surface)
+{
+ struct vdpctx *vc = vo->priv;
+ VdpStatus vdp_st;
+ struct vdp_functions *vdp = vc->vdp;
+ if (!vo->params)
+ return NULL;
+
+ VdpRGBAFormat fmt;
+ uint32_t w, h;
+ vdp_st = vdp->output_surface_get_parameters(surface, &fmt, &w, &h);
+ if (vdp_st != VDP_STATUS_OK)
+ return NULL;
+
+ assert(fmt == OUTPUT_RGBA_FORMAT);
+
+ struct mp_image *image = mp_image_alloc(IMGFMT_BGR0, w, h);
+ if (!image)
+ return NULL;
+
+ void *dst_planes[] = { image->planes[0] };
+ uint32_t dst_pitches[] = { image->stride[0] };
+ vdp_st = vdp->output_surface_get_bits_native(surface, NULL, dst_planes,
+ dst_pitches);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_output_surface_get_bits_native");
+
+ return image;
+}
+
+static struct mp_image *get_window_screenshot(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ int last_surface = WRAP_ADD(vc->surface_num, -1, vc->num_output_surfaces);
+ VdpOutputSurface screen = vc->output_surfaces[last_surface];
+ struct mp_image *image = read_output_surface(vo, screen);
+ if (image && image->w >= vo->dwidth && image->h >= vo->dheight)
+ mp_image_set_size(image, vo->dwidth, vo->dheight);
+ return image;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct vdpctx *vc = vo->priv;
+
+ if (mp_vdpau_get_format(format, NULL, NULL))
+ return 1;
+ if (!vc->force_yuv && mp_vdpau_get_rgb_format(format, NULL))
+ return 1;
+ return 0;
+}
+
+static void destroy_vdpau_objects(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+ struct vdp_functions *vdp = vc->vdp;
+
+ VdpStatus vdp_st;
+
+ free_video_specific(vo);
+
+ if (vc->flip_queue != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->presentation_queue_destroy(vc->flip_queue);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_presentation_queue_destroy");
+ }
+
+ if (vc->flip_target != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->presentation_queue_target_destroy(vc->flip_target);
+ CHECK_VDP_WARNING(vo, "Error when calling "
+ "vdp_presentation_queue_target_destroy");
+ }
+
+ for (int i = 0; i < vc->num_output_surfaces; i++) {
+ if (vc->output_surfaces[i] == VDP_INVALID_HANDLE)
+ continue;
+ vdp_st = vdp->output_surface_destroy(vc->output_surfaces[i]);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_output_surface_destroy");
+ }
+ if (vc->rotation_surface != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->output_surface_destroy(vc->rotation_surface);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_output_surface_destroy");
+ }
+
+ for (int i = 0; i < MAX_OSD_PARTS; i++) {
+ struct osd_bitmap_surface *sfc = &vc->osd_surfaces[i];
+ if (sfc->surface != VDP_INVALID_HANDLE) {
+ vdp_st = vdp->bitmap_surface_destroy(sfc->surface);
+ CHECK_VDP_WARNING(vo, "Error when calling vdp_bitmap_surface_destroy");
+ }
+ }
+
+ mp_vdpau_destroy(vc->mpvdp);
+ vc->mpvdp = NULL;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+
+ hwdec_devices_remove(vo->hwdec_devs, &vc->mpvdp->hwctx);
+ hwdec_devices_destroy(vo->hwdec_devs);
+
+ /* Destroy all vdpau objects */
+ mp_vdpau_mixer_destroy(vc->video_mixer);
+ destroy_vdpau_objects(vo);
+
+ vo_x11_uninit(vo);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct vdpctx *vc = vo->priv;
+
+ if (!vo_x11_init(vo))
+ return -1;
+
+ if (!vo_x11_create_vo_window(vo, NULL, "vdpau")) {
+ vo_x11_uninit(vo);
+ return -1;
+ }
+
+ vc->mpvdp = mp_vdpau_create_device_x11(vo->log, vo->x11->display, false);
+ if (!vc->mpvdp) {
+ vo_x11_uninit(vo);
+ return -1;
+ }
+ vc->mpvdp->hwctx.hw_imgfmt = IMGFMT_VDPAU;
+
+ vo->hwdec_devs = hwdec_devices_create();
+ hwdec_devices_add(vo->hwdec_devs, &vc->mpvdp->hwctx);
+
+ vc->video_mixer = mp_vdpau_mixer_create(vc->mpvdp, vo->log);
+ vc->video_mixer->video_eq = mp_csp_equalizer_create(vo, vo->global);
+
+ if (mp_vdpau_guess_if_emulated(vc->mpvdp)) {
+ MP_WARN(vo, "VDPAU is most likely emulated via VA-API.\n"
+ "This is inefficient. Use --vo=gpu instead.\n");
+ }
+
+ // Mark everything as invalid first so uninit() can tell what has been
+ // allocated
+ mark_vdpau_objects_uninitialized(vo);
+
+ mp_vdpau_handle_preemption(vc->mpvdp, &vc->preemption_counter);
+
+ vc->vdp_device = vc->mpvdp->vdp_device;
+ vc->vdp = &vc->mpvdp->vdp;
+
+ vc->vdp->bitmap_surface_query_capabilities(vc->vdp_device, VDP_RGBA_FORMAT_A8,
+ &vc->supports_a8, &(uint32_t){0}, &(uint32_t){0});
+
+ MP_WARN(vo, "Warning: this compatibility VO is low quality and may "
+ "have issues with OSD, scaling, screenshots and more.\n"
+ "vo=gpu is the preferred choice in any case and "
+ "includes VDPAU support via hwdec=vdpau or vdpau-copy.\n");
+
+ return 0;
+}
+
+static void checked_resize(struct vo *vo)
+{
+ if (!status_ok(vo))
+ return;
+ resize(vo);
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ check_preemption(vo);
+
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ checked_resize(vo);
+ return VO_TRUE;
+ case VOCTRL_SET_EQUALIZER:
+ vo->want_redraw = true;
+ return true;
+ case VOCTRL_RESET:
+ forget_frames(vo, true);
+ return true;
+ case VOCTRL_SCREENSHOT_WIN:
+ if (!status_ok(vo))
+ return false;
+ *(struct mp_image **)data = get_window_screenshot(vo);
+ return true;
+ }
+
+ int events = 0;
+ int r = vo_x11_control(vo, &events, request, data);
+
+ if (events & VO_EVENT_RESIZE) {
+ checked_resize(vo);
+ } else if (events & VO_EVENT_EXPOSE) {
+ vo->want_redraw = true;
+ }
+ vo_event(vo, events);
+
+ return r;
+}
+
+#define OPT_BASE_STRUCT struct vdpctx
+
+const struct vo_driver video_out_vdpau = {
+ .description = "VDPAU with X11",
+ .name = "vdpau",
+ .caps = VO_CAP_FRAMEDROP | VO_CAP_ROTATE90,
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .wakeup = vo_x11_wakeup,
+ .wait_events = vo_x11_wait_events,
+ .uninit = uninit,
+ .priv_size = sizeof(struct vdpctx),
+ .options = (const struct m_option []){
+ {"chroma-deint", OPT_BOOL(chroma_deint), OPTDEF_INT(1)},
+ {"pullup", OPT_BOOL(pullup)},
+ {"denoise", OPT_FLOAT(denoise), M_RANGE(0, 1)},
+ {"sharpen", OPT_FLOAT(sharpen), M_RANGE(-1, 1)},
+ {"hqscaling", OPT_INT(hqscaling), M_RANGE(0, 9)},
+ {"fps", OPT_FLOAT(user_fps)},
+ {"composite-detect", OPT_BOOL(composite_detect), OPTDEF_INT(1)},
+ {"queuetime-windowed", OPT_INT(flip_offset_window), OPTDEF_INT(50)},
+ {"queuetime-fs", OPT_INT(flip_offset_fs), OPTDEF_INT(50)},
+ {"output-surfaces", OPT_INT(num_output_surfaces),
+ M_RANGE(2, MAX_OUTPUT_SURFACES), OPTDEF_INT(3)},
+ {"colorkey", OPT_COLOR(colorkey),
+ .defval = &(const struct m_color){.r = 2, .g = 5, .b = 7, .a = 255}},
+ {"force-yuv", OPT_BOOL(force_yuv)},
+ {NULL},
+ },
+ .options_prefix = "vo-vdpau",
+};
diff --git a/video/out/vo_wlshm.c b/video/out/vo_wlshm.c
new file mode 100644
index 0000000..1e5e009
--- /dev/null
+++ b/video/out/vo_wlshm.c
@@ -0,0 +1,324 @@
+/*
+ * This file is part of mpv video player.
+ *
+ * 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 <errno.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <libswscale/swscale.h>
+
+#include "osdep/endian.h"
+#include "present_sync.h"
+#include "sub/osd.h"
+#include "video/fmt-conversion.h"
+#include "video/mp_image.h"
+#include "video/sws_utils.h"
+#include "vo.h"
+#include "wayland_common.h"
+
+struct buffer {
+ struct vo *vo;
+ size_t size;
+ struct wl_shm_pool *pool;
+ struct wl_buffer *buffer;
+ struct mp_image mpi;
+ struct buffer *next;
+};
+
+struct priv {
+ struct mp_sws_context *sws;
+ struct buffer *free_buffers;
+ struct mp_rect src;
+ struct mp_rect dst;
+ struct mp_osd_res osd;
+};
+
+static void buffer_handle_release(void *data, struct wl_buffer *wl_buffer)
+{
+ struct buffer *buf = data;
+ struct vo *vo = buf->vo;
+ struct priv *p = vo->priv;
+
+ if (buf->mpi.w == vo->dwidth && buf->mpi.h == vo->dheight) {
+ buf->next = p->free_buffers;
+ p->free_buffers = buf;
+ } else {
+ talloc_free(buf);
+ }
+}
+
+static const struct wl_buffer_listener buffer_listener = {
+ buffer_handle_release,
+};
+
+static void buffer_destroy(void *p)
+{
+ struct buffer *buf = p;
+ wl_buffer_destroy(buf->buffer);
+ wl_shm_pool_destroy(buf->pool);
+ munmap(buf->mpi.planes[0], buf->size);
+}
+
+static struct buffer *buffer_create(struct vo *vo, int width, int height)
+{
+ struct priv *p = vo->priv;
+ struct vo_wayland_state *wl = vo->wl;
+ int fd;
+ int stride;
+ size_t size;
+ uint8_t *data;
+ struct buffer *buf;
+
+ stride = MP_ALIGN_UP(width * 4, 16);
+ size = height * stride;
+ fd = vo_wayland_allocate_memfd(vo, size);
+ if (fd < 0)
+ goto error0;
+ data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (data == MAP_FAILED)
+ goto error1;
+ buf = talloc_zero(NULL, struct buffer);
+ if (!buf)
+ goto error2;
+ buf->vo = vo;
+ buf->size = size;
+ mp_image_set_params(&buf->mpi, &p->sws->dst);
+ mp_image_set_size(&buf->mpi, width, height);
+ buf->mpi.planes[0] = data;
+ buf->mpi.stride[0] = stride;
+ buf->pool = wl_shm_create_pool(wl->shm, fd, size);
+ if (!buf->pool)
+ goto error3;
+ buf->buffer = wl_shm_pool_create_buffer(buf->pool, 0, width, height,
+ stride, WL_SHM_FORMAT_XRGB8888);
+ if (!buf->buffer)
+ goto error4;
+ wl_buffer_add_listener(buf->buffer, &buffer_listener, buf);
+
+ close(fd);
+ talloc_set_destructor(buf, buffer_destroy);
+
+ return buf;
+
+error4:
+ wl_shm_pool_destroy(buf->pool);
+error3:
+ talloc_free(buf);
+error2:
+ munmap(data, size);
+error1:
+ close(fd);
+error0:
+ return NULL;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct buffer *buf;
+
+ while (p->free_buffers) {
+ buf = p->free_buffers;
+ p->free_buffers = buf->next;
+ talloc_free(buf);
+ }
+ vo_wayland_uninit(vo);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ if (!vo_wayland_init(vo))
+ goto err;
+ if (!vo->wl->shm) {
+ MP_FATAL(vo->wl, "Compositor doesn't support the %s protocol!\n",
+ wl_shm_interface.name);
+ goto err;
+ }
+ p->sws = mp_sws_alloc(vo);
+ p->sws->log = vo->log;
+ mp_sws_enable_cmdline_opts(p->sws, vo->global);
+
+ return 0;
+err:
+ uninit(vo);
+ return -1;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ return sws_isSupportedInput(imgfmt2pixfmt(format));
+}
+
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct priv *p = vo->priv;
+
+ if (!vo_wayland_reconfig(vo))
+ return -1;
+ p->sws->src = *params;
+
+ return 0;
+}
+
+static int resize(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ struct vo_wayland_state *wl = vo->wl;
+ const int32_t width = mp_rect_w(wl->geometry);
+ const int32_t height = mp_rect_h(wl->geometry);
+
+ if (width == 0 || height == 0)
+ return 1;
+
+ struct buffer *buf;
+
+ vo_wayland_set_opaque_region(wl, false);
+ vo->want_redraw = true;
+ vo->dwidth = width;
+ vo->dheight = height;
+ vo_get_src_dst_rects(vo, &p->src, &p->dst, &p->osd);
+ p->sws->dst = (struct mp_image_params) {
+ .imgfmt = MP_SELECT_LE_BE(IMGFMT_BGR0, IMGFMT_0RGB),
+ .w = width,
+ .h = height,
+ .p_w = 1,
+ .p_h = 1,
+ };
+ mp_image_params_guess_csp(&p->sws->dst);
+ while (p->free_buffers) {
+ buf = p->free_buffers;
+ p->free_buffers = buf->next;
+ talloc_free(buf);
+ }
+
+ vo_wayland_handle_fractional_scale(wl);
+
+ return mp_sws_reinit(p->sws);
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ resize(vo);
+ return VO_TRUE;
+ }
+
+ int events = 0;
+ int ret = vo_wayland_control(vo, &events, request, data);
+
+ if (events & VO_EVENT_RESIZE)
+ ret = resize(vo);
+ if (events & VO_EVENT_EXPOSE)
+ vo->want_redraw = true;
+ vo_event(vo, events);
+ return ret;
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+ struct vo_wayland_state *wl = vo->wl;
+ struct mp_image *src = frame->current;
+ struct buffer *buf;
+
+ bool render = vo_wayland_check_visible(vo);
+ if (!render)
+ return;
+
+ buf = p->free_buffers;
+ if (buf) {
+ p->free_buffers = buf->next;
+ } else {
+ buf = buffer_create(vo, vo->dwidth, vo->dheight);
+ if (!buf) {
+ wl_surface_attach(wl->surface, NULL, 0, 0);
+ return;
+ }
+ }
+ if (src) {
+ struct mp_image dst = buf->mpi;
+ struct mp_rect src_rc;
+ struct mp_rect dst_rc;
+ src_rc.x0 = MP_ALIGN_DOWN(p->src.x0, MPMAX(src->fmt.align_x, 4));
+ src_rc.y0 = MP_ALIGN_DOWN(p->src.y0, MPMAX(src->fmt.align_y, 4));
+ src_rc.x1 = p->src.x1 - (p->src.x0 - src_rc.x0);
+ src_rc.y1 = p->src.y1 - (p->src.y0 - src_rc.y0);
+ dst_rc.x0 = MP_ALIGN_DOWN(p->dst.x0, MPMAX(dst.fmt.align_x, 4));
+ dst_rc.y0 = MP_ALIGN_DOWN(p->dst.y0, MPMAX(dst.fmt.align_y, 4));
+ dst_rc.x1 = p->dst.x1 - (p->dst.x0 - dst_rc.x0);
+ dst_rc.y1 = p->dst.y1 - (p->dst.y0 - dst_rc.y0);
+ mp_image_crop_rc(src, src_rc);
+ mp_image_crop_rc(&dst, dst_rc);
+ mp_sws_scale(p->sws, &dst, src);
+ if (dst_rc.y0 > 0)
+ mp_image_clear(&buf->mpi, 0, 0, buf->mpi.w, dst_rc.y0);
+ if (buf->mpi.h > dst_rc.y1)
+ mp_image_clear(&buf->mpi, 0, dst_rc.y1, buf->mpi.w, buf->mpi.h);
+ if (dst_rc.x0 > 0)
+ mp_image_clear(&buf->mpi, 0, dst_rc.y0, dst_rc.x0, dst_rc.y1);
+ if (buf->mpi.w > dst_rc.x1)
+ mp_image_clear(&buf->mpi, dst_rc.x1, dst_rc.y0, buf->mpi.w, dst_rc.y1);
+ osd_draw_on_image(vo->osd, p->osd, src->pts, 0, &buf->mpi);
+ } else {
+ mp_image_clear(&buf->mpi, 0, 0, buf->mpi.w, buf->mpi.h);
+ osd_draw_on_image(vo->osd, p->osd, 0, 0, &buf->mpi);
+ }
+ wl_surface_attach(wl->surface, buf->buffer, 0, 0);
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+
+ wl_surface_damage_buffer(wl->surface, 0, 0, vo->dwidth,
+ vo->dheight);
+ wl_surface_commit(wl->surface);
+
+ if (!wl->opts->disable_vsync)
+ vo_wayland_wait_frame(wl);
+
+ if (wl->use_present)
+ present_sync_swap(wl->present);
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ if (wl->use_present)
+ present_sync_get_info(wl->present, info);
+}
+
+const struct vo_driver video_out_wlshm = {
+ .description = "Wayland SHM video output (software scaling)",
+ .name = "wlshm",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .wakeup = vo_wayland_wakeup,
+ .wait_events = vo_wayland_wait_events,
+ .uninit = uninit,
+ .priv_size = sizeof(struct priv),
+};
diff --git a/video/out/vo_x11.c b/video/out/vo_x11.c
new file mode 100644
index 0000000..fa93157
--- /dev/null
+++ b/video/out/vo_x11.c
@@ -0,0 +1,447 @@
+/*
+ * Original author: Aaron Holtzman <aholtzma@ess.engr.uvic.ca>
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+
+#include <libswscale/swscale.h>
+
+#include "vo.h"
+#include "video/csputils.h"
+#include "video/mp_image.h"
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+
+#include <errno.h>
+
+#include "present_sync.h"
+#include "x11_common.h"
+
+#include <sys/ipc.h>
+#include <sys/shm.h>
+#include <X11/extensions/XShm.h>
+
+#include "sub/osd.h"
+#include "sub/draw_bmp.h"
+
+#include "video/sws_utils.h"
+#include "video/fmt-conversion.h"
+
+#include "common/msg.h"
+#include "input/input.h"
+#include "options/options.h"
+#include "osdep/timer.h"
+
+struct priv {
+ struct vo *vo;
+
+ struct mp_image *original_image;
+
+ XImage *myximage[2];
+ struct mp_image mp_ximages[2];
+ int depth;
+ GC gc;
+
+ uint32_t image_width;
+ uint32_t image_height;
+
+ struct mp_rect src;
+ struct mp_rect dst;
+ struct mp_osd_res osd;
+
+ struct mp_sws_context *sws;
+
+ XVisualInfo vinfo;
+
+ int current_buf;
+
+ int Shmem_Flag;
+ XShmSegmentInfo Shminfo[2];
+ int Shm_Warned_Slow;
+};
+
+static bool resize(struct vo *vo);
+
+static bool getMyXImage(struct priv *p, int foo)
+{
+ struct vo *vo = p->vo;
+ if (vo->x11->display_is_local && XShmQueryExtension(vo->x11->display)) {
+ p->Shmem_Flag = 1;
+ vo->x11->ShmCompletionEvent = XShmGetEventBase(vo->x11->display)
+ + ShmCompletion;
+ } else {
+ p->Shmem_Flag = 0;
+ MP_WARN(vo, "Shared memory not supported\nReverting to normal Xlib\n");
+ }
+
+ if (p->Shmem_Flag) {
+ p->myximage[foo] =
+ XShmCreateImage(vo->x11->display, p->vinfo.visual, p->depth,
+ ZPixmap, NULL, &p->Shminfo[foo], p->image_width,
+ p->image_height);
+ if (p->myximage[foo] == NULL) {
+ MP_WARN(vo, "Shared memory error,disabling ( Ximage error )\n");
+ goto shmemerror;
+ }
+ p->Shminfo[foo].shmid = shmget(IPC_PRIVATE,
+ p->myximage[foo]->bytes_per_line *
+ p->myximage[foo]->height,
+ IPC_CREAT | 0777);
+ if (p->Shminfo[foo].shmid < 0) {
+ XDestroyImage(p->myximage[foo]);
+ MP_WARN(vo, "Shared memory error,disabling ( seg id error )\n");
+ goto shmemerror;
+ }
+ p->Shminfo[foo].shmaddr = (char *) shmat(p->Shminfo[foo].shmid, 0, 0);
+
+ if (p->Shminfo[foo].shmaddr == ((char *) -1)) {
+ XDestroyImage(p->myximage[foo]);
+ MP_WARN(vo, "Shared memory error,disabling ( address error )\n");
+ goto shmemerror;
+ }
+ p->myximage[foo]->data = p->Shminfo[foo].shmaddr;
+ p->Shminfo[foo].readOnly = False;
+ XShmAttach(vo->x11->display, &p->Shminfo[foo]);
+
+ XSync(vo->x11->display, False);
+
+ shmctl(p->Shminfo[foo].shmid, IPC_RMID, 0);
+ } else {
+shmemerror:
+ p->Shmem_Flag = 0;
+
+ MP_VERBOSE(vo, "Not using SHM.\n");
+ p->myximage[foo] =
+ XCreateImage(vo->x11->display, p->vinfo.visual, p->depth, ZPixmap,
+ 0, NULL, p->image_width, p->image_height, 8, 0);
+ if (p->myximage[foo]) {
+ p->myximage[foo]->data =
+ calloc(1, p->myximage[foo]->bytes_per_line * p->image_height + 32);
+ }
+ if (!p->myximage[foo] || !p->myximage[foo]->data) {
+ MP_WARN(vo, "could not allocate image");
+ return false;
+ }
+ }
+ return true;
+}
+
+static void freeMyXImage(struct priv *p, int foo)
+{
+ struct vo *vo = p->vo;
+ if (p->Shmem_Flag) {
+ XShmDetach(vo->x11->display, &p->Shminfo[foo]);
+ XDestroyImage(p->myximage[foo]);
+ shmdt(p->Shminfo[foo].shmaddr);
+ } else {
+ if (p->myximage[foo]) {
+ // XDestroyImage() would free the data too since XFree() just calls
+ // free(), but do it ourselves for portability reasons
+ free(p->myximage[foo]->data);
+ p->myximage[foo]->data = NULL;
+ XDestroyImage(p->myximage[foo]);
+ }
+ }
+ p->myximage[foo] = NULL;
+}
+
+#define MAKE_MASK(comp) (((1ul << (comp).size) - 1) << (comp).offset)
+
+static int reconfig(struct vo *vo, struct mp_image_params *fmt)
+{
+ vo_x11_config_vo_window(vo);
+
+ if (!resize(vo))
+ return -1;
+
+ return 0;
+}
+
+static bool resize(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+
+ // Attempt to align. We don't know the size in bytes yet (????), so just
+ // assume worst case (1 byte per pixel).
+ int nw = MPMAX(1, MP_ALIGN_UP(vo->dwidth, MP_IMAGE_BYTE_ALIGN));
+ int nh = MPMAX(1, vo->dheight);
+
+ if (nw > p->image_width || nh > p->image_height) {
+ for (int i = 0; i < 2; i++)
+ freeMyXImage(p, i);
+
+ p->image_width = nw;
+ p->image_height = nh;
+
+ for (int i = 0; i < 2; i++) {
+ if (!getMyXImage(p, i)) {
+ p->image_width = 0;
+ p->image_height = 0;
+ return false;
+ }
+ }
+ }
+
+ int mpfmt = 0;
+ for (int fmt = IMGFMT_START; fmt < IMGFMT_END; fmt++) {
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(fmt);
+ if ((desc.flags & MP_IMGFLAG_HAS_COMPS) && desc.num_planes == 1 &&
+ (desc.flags & MP_IMGFLAG_COLOR_MASK) == MP_IMGFLAG_COLOR_RGB &&
+ (desc.flags & MP_IMGFLAG_TYPE_MASK) == MP_IMGFLAG_TYPE_UINT &&
+ (desc.flags & MP_IMGFLAG_NE) && !(desc.flags & MP_IMGFLAG_ALPHA) &&
+ desc.bpp[0] <= 8 * sizeof(unsigned long) &&
+ p->myximage[0]->bits_per_pixel == desc.bpp[0] &&
+ p->myximage[0]->byte_order == MP_SELECT_LE_BE(LSBFirst, MSBFirst))
+ {
+ // desc.comps[] uses little endian bit offsets, so "swap" the
+ // offsets here.
+ if (MP_SELECT_LE_BE(0, 1)) {
+ // Except for formats that use byte swapping; for these, the
+ // offsets are in native endian. There is no way to distinguish
+ // which one a given format is (could even be both), and using
+ // mp_find_other_endian() is just a guess.
+ if (!mp_find_other_endian(fmt)) {
+ for (int c = 0; c < 3; c++) {
+ desc.comps[c].offset =
+ desc.bpp[0] - desc.comps[c].size -desc.comps[c].offset;
+ }
+ }
+ }
+ if (p->myximage[0]->red_mask == MAKE_MASK(desc.comps[0]) &&
+ p->myximage[0]->green_mask == MAKE_MASK(desc.comps[1]) &&
+ p->myximage[0]->blue_mask == MAKE_MASK(desc.comps[2]))
+ {
+ mpfmt = fmt;
+ break;
+ }
+ }
+ }
+
+ if (!mpfmt) {
+ MP_ERR(vo, "X server image format not supported, use another VO.\n");
+ return false;
+ }
+ MP_VERBOSE(vo, "Using mp format: %s\n", mp_imgfmt_to_name(mpfmt));
+
+ for (int i = 0; i < 2; i++) {
+ struct mp_image *img = &p->mp_ximages[i];
+ *img = (struct mp_image){0};
+ mp_image_setfmt(img, mpfmt);
+ mp_image_set_size(img, p->image_width, p->image_height);
+ img->planes[0] = p->myximage[i]->data;
+ img->stride[0] = p->myximage[i]->bytes_per_line;
+
+ mp_image_params_guess_csp(&img->params);
+ }
+
+ vo_get_src_dst_rects(vo, &p->src, &p->dst, &p->osd);
+
+ if (vo->params) {
+ p->sws->src = *vo->params;
+ p->sws->src.w = mp_rect_w(p->src);
+ p->sws->src.h = mp_rect_h(p->src);
+
+ p->sws->dst = p->mp_ximages[0].params;
+ p->sws->dst.w = mp_rect_w(p->dst);
+ p->sws->dst.h = mp_rect_h(p->dst);
+
+ if (mp_sws_reinit(p->sws) < 0)
+ return false;
+ }
+
+ vo->want_redraw = true;
+ return true;
+}
+
+static void Display_Image(struct priv *p, XImage *myximage)
+{
+ struct vo *vo = p->vo;
+
+ XImage *x_image = p->myximage[p->current_buf];
+
+ if (p->Shmem_Flag) {
+ XShmPutImage(vo->x11->display, vo->x11->window, p->gc, x_image,
+ 0, 0, 0, 0, vo->dwidth, vo->dheight, True);
+ vo->x11->ShmCompletionWaitCount++;
+ } else {
+ XPutImage(vo->x11->display, vo->x11->window, p->gc, x_image,
+ 0, 0, 0, 0, vo->dwidth, vo->dheight);
+ }
+}
+
+static void wait_for_completion(struct vo *vo, int max_outstanding)
+{
+ struct priv *ctx = vo->priv;
+ struct vo_x11_state *x11 = vo->x11;
+ if (ctx->Shmem_Flag) {
+ while (x11->ShmCompletionWaitCount > max_outstanding) {
+ if (!ctx->Shm_Warned_Slow) {
+ MP_WARN(vo, "can't keep up! Waiting"
+ " for XShm completion events...\n");
+ ctx->Shm_Warned_Slow = 1;
+ }
+ mp_sleep_ns(MP_TIME_MS_TO_NS(1));
+ vo_x11_check_events(vo);
+ }
+ }
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ Display_Image(p, p->myximage[p->current_buf]);
+ p->current_buf = (p->current_buf + 1) % 2;
+ if (vo->x11->use_present) {
+ vo_x11_present(vo);
+ present_sync_swap(vo->x11->present);
+ }
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (x11->use_present)
+ present_sync_get_info(x11->present, info);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct priv *p = vo->priv;
+
+ wait_for_completion(vo, 1);
+ bool render = vo_x11_check_visible(vo);
+ if (!render)
+ return;
+
+ struct mp_image *img = &p->mp_ximages[p->current_buf];
+
+ if (frame->current) {
+ mp_image_clear_rc_inv(img, p->dst);
+
+ struct mp_image *src = frame->current;
+ struct mp_rect src_rc = p->src;
+ src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, src->fmt.align_x);
+ src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, src->fmt.align_y);
+ mp_image_crop_rc(src, src_rc);
+
+ struct mp_image dst = *img;
+ mp_image_crop_rc(&dst, p->dst);
+
+ mp_sws_scale(p->sws, &dst, src);
+ } else {
+ mp_image_clear(img, 0, 0, img->w, img->h);
+ }
+
+ osd_draw_on_image(vo->osd, p->osd, frame->current ? frame->current->pts : 0, 0, img);
+
+ if (frame->current != p->original_image)
+ p->original_image = frame->current;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct priv *p = vo->priv;
+ if (mp_sws_supports_formats(p->sws, IMGFMT_RGB0, format))
+ return 1;
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ if (p->myximage[0])
+ freeMyXImage(p, 0);
+ if (p->myximage[1])
+ freeMyXImage(p, 1);
+ if (p->gc)
+ XFreeGC(vo->x11->display, p->gc);
+
+ vo_x11_uninit(vo);
+}
+
+static int preinit(struct vo *vo)
+{
+ struct priv *p = vo->priv;
+ p->vo = vo;
+ p->sws = mp_sws_alloc(vo);
+ p->sws->log = vo->log;
+ mp_sws_enable_cmdline_opts(p->sws, vo->global);
+
+ if (!vo_x11_init(vo))
+ goto error;
+ struct vo_x11_state *x11 = vo->x11;
+
+ XWindowAttributes attribs;
+ XGetWindowAttributes(x11->display, x11->rootwin, &attribs);
+ p->depth = attribs.depth;
+
+ if (!XMatchVisualInfo(x11->display, x11->screen, p->depth,
+ TrueColor, &p->vinfo))
+ goto error;
+
+ MP_VERBOSE(vo, "selected visual: %d\n", (int)p->vinfo.visualid);
+
+ if (!vo_x11_create_vo_window(vo, &p->vinfo, "x11"))
+ goto error;
+
+ p->gc = XCreateGC(x11->display, x11->window, 0, NULL);
+ MP_WARN(vo, "Warning: this legacy VO has bad performance. Consider fixing "
+ "your graphics drivers, or not forcing the x11 VO.\n");
+ return 0;
+
+error:
+ uninit(vo);
+ return -1;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ if (vo->config_ok)
+ resize(vo);
+ return VO_TRUE;
+ }
+
+ int events = 0;
+ int r = vo_x11_control(vo, &events, request, data);
+ if (vo->config_ok && (events & (VO_EVENT_EXPOSE | VO_EVENT_RESIZE)))
+ resize(vo);
+ vo_event(vo, events);
+ return r;
+}
+
+const struct vo_driver video_out_x11 = {
+ .description = "X11 (software scaling)",
+ .name = "x11",
+ .priv_size = sizeof(struct priv),
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .wakeup = vo_x11_wakeup,
+ .wait_events = vo_x11_wait_events,
+ .uninit = uninit,
+};
diff --git a/video/out/vo_xv.c b/video/out/vo_xv.c
new file mode 100644
index 0000000..6c776c5
--- /dev/null
+++ b/video/out/vo_xv.c
@@ -0,0 +1,921 @@
+/*
+ * X11 Xv interface
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <float.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+
+#include <libavutil/common.h>
+
+#include <sys/types.h>
+#include <sys/ipc.h>
+#include <sys/shm.h>
+#include <X11/extensions/XShm.h>
+
+// Note: depends on the inclusion of X11/extensions/XShm.h
+#include <X11/extensions/Xv.h>
+#include <X11/extensions/Xvlib.h>
+
+#include "options/options.h"
+#include "mpv_talloc.h"
+#include "common/msg.h"
+#include "vo.h"
+#include "video/mp_image.h"
+#include "present_sync.h"
+#include "x11_common.h"
+#include "sub/osd.h"
+#include "sub/draw_bmp.h"
+#include "video/csputils.h"
+#include "options/m_option.h"
+#include "input/input.h"
+#include "osdep/timer.h"
+
+#define CK_METHOD_NONE 0 // no colorkey drawing
+#define CK_METHOD_BACKGROUND 1 // set colorkey as window background
+#define CK_METHOD_AUTOPAINT 2 // let xv draw the colorkey
+#define CK_METHOD_MANUALFILL 3 // manually draw the colorkey
+#define CK_SRC_USE 0 // use specified / default colorkey
+#define CK_SRC_SET 1 // use and set specified / default colorkey
+#define CK_SRC_CUR 2 // use current colorkey (get it from xv)
+
+#define MAX_BUFFERS 10
+
+struct xvctx {
+ struct xv_ck_info_s {
+ int method; // CK_METHOD_* constants
+ int source; // CK_SRC_* constants
+ } xv_ck_info;
+ int colorkey;
+ unsigned long xv_colorkey;
+ int xv_port;
+ int cfg_xv_adaptor;
+ int cfg_buffers;
+ XvAdaptorInfo *ai;
+ XvImageFormatValues *fo;
+ unsigned int formats, adaptors, xv_format;
+ int current_buf;
+ int current_ip_buf;
+ int num_buffers;
+ XvImage *xvimage[MAX_BUFFERS];
+ struct mp_image *original_image;
+ uint32_t image_width;
+ uint32_t image_height;
+ uint32_t image_format;
+ int cached_csp;
+ struct mp_rect src_rect;
+ struct mp_rect dst_rect;
+ uint32_t max_width, max_height; // zero means: not set
+ GC f_gc; // used to paint background
+ GC vo_gc; // used to paint video
+ int Shmem_Flag;
+ XShmSegmentInfo Shminfo[MAX_BUFFERS];
+ int Shm_Warned_Slow;
+};
+
+#define MP_FOURCC(a,b,c,d) ((a) | ((b)<<8) | ((c)<<16) | ((unsigned)(d)<<24))
+
+#define MP_FOURCC_YV12 MP_FOURCC('Y', 'V', '1', '2')
+#define MP_FOURCC_I420 MP_FOURCC('I', '4', '2', '0')
+#define MP_FOURCC_IYUV MP_FOURCC('I', 'Y', 'U', 'V')
+#define MP_FOURCC_UYVY MP_FOURCC('U', 'Y', 'V', 'Y')
+
+struct fmt_entry {
+ int imgfmt;
+ int fourcc;
+};
+static const struct fmt_entry fmt_table[] = {
+ {IMGFMT_420P, MP_FOURCC_YV12},
+ {IMGFMT_420P, MP_FOURCC_I420},
+ {IMGFMT_UYVY, MP_FOURCC_UYVY},
+ {0}
+};
+
+static bool allocate_xvimage(struct vo *, int);
+static void deallocate_xvimage(struct vo *vo, int foo);
+static struct mp_image get_xv_buffer(struct vo *vo, int buf_index);
+
+static int find_xv_format(int imgfmt)
+{
+ for (int n = 0; fmt_table[n].imgfmt; n++) {
+ if (fmt_table[n].imgfmt == imgfmt)
+ return fmt_table[n].fourcc;
+ }
+ return 0;
+}
+
+static int xv_find_atom(struct vo *vo, uint32_t xv_port, const char *name,
+ bool get, int *min, int *max)
+{
+ Atom atom = None;
+ int howmany = 0;
+ XvAttribute *attributes = XvQueryPortAttributes(vo->x11->display, xv_port,
+ &howmany);
+ for (int i = 0; i < howmany && attributes; i++) {
+ int flag = get ? XvGettable : XvSettable;
+ if (attributes[i].flags & flag) {
+ atom = XInternAtom(vo->x11->display, attributes[i].name, True);
+ *min = attributes[i].min_value;
+ *max = attributes[i].max_value;
+/* since we have SET_DEFAULTS first in our list, we can check if it's available
+ then trigger it if it's ok so that the other values are at default upon query */
+ if (atom != None) {
+ if (!strcmp(attributes[i].name, "XV_BRIGHTNESS") &&
+ (!strcmp(name, "brightness")))
+ break;
+ else if (!strcmp(attributes[i].name, "XV_CONTRAST") &&
+ (!strcmp(name, "contrast")))
+ break;
+ else if (!strcmp(attributes[i].name, "XV_SATURATION") &&
+ (!strcmp(name, "saturation")))
+ break;
+ else if (!strcmp(attributes[i].name, "XV_HUE") &&
+ (!strcmp(name, "hue")))
+ break;
+ if (!strcmp(attributes[i].name, "XV_RED_INTENSITY") &&
+ (!strcmp(name, "red_intensity")))
+ break;
+ else if (!strcmp(attributes[i].name, "XV_GREEN_INTENSITY")
+ && (!strcmp(name, "green_intensity")))
+ break;
+ else if (!strcmp(attributes[i].name, "XV_BLUE_INTENSITY")
+ && (!strcmp(name, "blue_intensity")))
+ break;
+ else if ((!strcmp(attributes[i].name, "XV_ITURBT_709") //NVIDIA
+ || !strcmp(attributes[i].name, "XV_COLORSPACE")) //ATI
+ && (!strcmp(name, "bt_709")))
+ break;
+ atom = None;
+ continue;
+ }
+ }
+ }
+ XFree(attributes);
+ return atom;
+}
+
+static int xv_set_eq(struct vo *vo, uint32_t xv_port, const char *name,
+ int value)
+{
+ MP_VERBOSE(vo, "xv_set_eq called! (%s, %d)\n", name, value);
+
+ int min, max;
+ int atom = xv_find_atom(vo, xv_port, name, false, &min, &max);
+ if (atom != None) {
+ // -100 -> min
+ // 0 -> (max+min)/2
+ // +100 -> max
+ int port_value = (value + 100) * (max - min) / 200 + min;
+ XvSetPortAttribute(vo->x11->display, xv_port, atom, port_value);
+ return VO_TRUE;
+ }
+ return VO_FALSE;
+}
+
+static int xv_get_eq(struct vo *vo, uint32_t xv_port, const char *name,
+ int *value)
+{
+ int min, max;
+ int atom = xv_find_atom(vo, xv_port, name, true, &min, &max);
+ if (atom != None) {
+ int port_value = 0;
+ XvGetPortAttribute(vo->x11->display, xv_port, atom, &port_value);
+
+ *value = (port_value - min) * 200 / (max - min) - 100;
+ MP_VERBOSE(vo, "xv_get_eq called! (%s, %d)\n", name, *value);
+ return VO_TRUE;
+ }
+ return VO_FALSE;
+}
+
+static Atom xv_intern_atom_if_exists(struct vo *vo, char const *atom_name)
+{
+ struct xvctx *ctx = vo->priv;
+ XvAttribute *attributes;
+ int attrib_count, i;
+ Atom xv_atom = None;
+
+ attributes = XvQueryPortAttributes(vo->x11->display, ctx->xv_port,
+ &attrib_count);
+ if (attributes != NULL) {
+ for (i = 0; i < attrib_count; ++i) {
+ if (strcmp(attributes[i].name, atom_name) == 0) {
+ xv_atom = XInternAtom(vo->x11->display, atom_name, False);
+ break;
+ }
+ }
+ XFree(attributes);
+ }
+
+ return xv_atom;
+}
+
+// Try to enable vsync for xv.
+// Returns -1 if not available, 0 on failure and 1 on success.
+static int xv_enable_vsync(struct vo *vo)
+{
+ struct xvctx *ctx = vo->priv;
+ Atom xv_atom = xv_intern_atom_if_exists(vo, "XV_SYNC_TO_VBLANK");
+ if (xv_atom == None)
+ return -1;
+ return XvSetPortAttribute(vo->x11->display, ctx->xv_port, xv_atom, 1)
+ == Success;
+}
+
+// Get maximum supported source image dimensions.
+// If querying the dimensions fails, don't change *width and *height.
+static void xv_get_max_img_dim(struct vo *vo, uint32_t *width, uint32_t *height)
+{
+ struct xvctx *ctx = vo->priv;
+ XvEncodingInfo *encodings;
+ unsigned int num_encodings, idx;
+
+ XvQueryEncodings(vo->x11->display, ctx->xv_port, &num_encodings, &encodings);
+
+ if (encodings) {
+ for (idx = 0; idx < num_encodings; ++idx) {
+ if (strcmp(encodings[idx].name, "XV_IMAGE") == 0) {
+ *width = encodings[idx].width;
+ *height = encodings[idx].height;
+ break;
+ }
+ }
+ }
+
+ MP_VERBOSE(vo, "Maximum source image dimensions: %ux%u\n", *width, *height);
+
+ XvFreeEncodingInfo(encodings);
+}
+
+static void xv_print_ck_info(struct vo *vo)
+{
+ struct xvctx *xv = vo->priv;
+
+ switch (xv->xv_ck_info.method) {
+ case CK_METHOD_NONE:
+ MP_VERBOSE(vo, "Drawing no colorkey.\n");
+ return;
+ case CK_METHOD_AUTOPAINT:
+ MP_VERBOSE(vo, "Colorkey is drawn by Xv.\n");
+ break;
+ case CK_METHOD_MANUALFILL:
+ MP_VERBOSE(vo, "Drawing colorkey manually.\n");
+ break;
+ case CK_METHOD_BACKGROUND:
+ MP_VERBOSE(vo, "Colorkey is drawn as window background.\n");
+ break;
+ }
+
+ switch (xv->xv_ck_info.source) {
+ case CK_SRC_CUR:
+ MP_VERBOSE(vo, "Using colorkey from Xv (0x%06lx).\n", xv->xv_colorkey);
+ break;
+ case CK_SRC_USE:
+ if (xv->xv_ck_info.method == CK_METHOD_AUTOPAINT) {
+ MP_VERBOSE(vo, "Ignoring colorkey from mpv (0x%06lx).\n",
+ xv->xv_colorkey);
+ } else {
+ MP_VERBOSE(vo, "Using colorkey from mpv (0x%06lx). Use -colorkey to change.\n",
+ xv->xv_colorkey);
+ }
+ break;
+ case CK_SRC_SET:
+ MP_VERBOSE(vo, "Setting and using colorkey from mpv (0x%06lx)."
+ " Use -colorkey to change.\n", xv->xv_colorkey);
+ break;
+ }
+}
+
+/* NOTE: If vo.colorkey has bits set after the first 3 low order bytes
+ * we don't draw anything as this means it was forced to off. */
+static int xv_init_colorkey(struct vo *vo)
+{
+ struct xvctx *ctx = vo->priv;
+ Display *display = vo->x11->display;
+ Atom xv_atom;
+ int rez;
+
+ /* check if colorkeying is needed */
+ xv_atom = xv_intern_atom_if_exists(vo, "XV_COLORKEY");
+ if (xv_atom != None && ctx->xv_ck_info.method != CK_METHOD_NONE) {
+ if (ctx->xv_ck_info.source == CK_SRC_CUR) {
+ int colorkey_ret;
+
+ rez = XvGetPortAttribute(display, ctx->xv_port, xv_atom,
+ &colorkey_ret);
+ if (rez == Success)
+ ctx->xv_colorkey = colorkey_ret;
+ else {
+ MP_FATAL(vo, "Couldn't get colorkey! "
+ "Maybe the selected Xv port has no overlay.\n");
+ return 0; // error getting colorkey
+ }
+ } else {
+ ctx->xv_colorkey = ctx->colorkey;
+
+ /* check if we have to set the colorkey too */
+ if (ctx->xv_ck_info.source == CK_SRC_SET) {
+ xv_atom = XInternAtom(display, "XV_COLORKEY", False);
+
+ rez = XvSetPortAttribute(display, ctx->xv_port, xv_atom,
+ ctx->colorkey);
+ if (rez != Success) {
+ MP_FATAL(vo, "Couldn't set colorkey!\n");
+ return 0; // error setting colorkey
+ }
+ }
+ }
+
+ xv_atom = xv_intern_atom_if_exists(vo, "XV_AUTOPAINT_COLORKEY");
+
+ /* should we draw the colorkey ourselves or activate autopainting? */
+ if (ctx->xv_ck_info.method == CK_METHOD_AUTOPAINT) {
+ rez = !Success;
+
+ if (xv_atom != None) // autopaint is supported
+ rez = XvSetPortAttribute(display, ctx->xv_port, xv_atom, 1);
+
+ if (rez != Success)
+ ctx->xv_ck_info.method = CK_METHOD_MANUALFILL;
+ } else {
+ // disable colorkey autopainting if supported
+ if (xv_atom != None)
+ XvSetPortAttribute(display, ctx->xv_port, xv_atom, 0);
+ }
+ } else { // do no colorkey drawing at all
+ ctx->xv_ck_info.method = CK_METHOD_NONE;
+ ctx->colorkey = 0xFF000000;
+ }
+
+ xv_print_ck_info(vo);
+
+ return 1;
+}
+
+/* Draw the colorkey on the video window.
+ *
+ * Draws the colorkey depending on the set method ( colorkey_handling ).
+ *
+ * Also draws the black bars ( when the video doesn't fit the display in
+ * fullscreen ) separately, so they don't overlap with the video area. */
+static void xv_draw_colorkey(struct vo *vo, const struct mp_rect *rc)
+{
+ struct xvctx *ctx = vo->priv;
+ struct vo_x11_state *x11 = vo->x11;
+ if (ctx->xv_ck_info.method == CK_METHOD_MANUALFILL ||
+ ctx->xv_ck_info.method == CK_METHOD_BACKGROUND)
+ {
+ if (!ctx->vo_gc)
+ return;
+ //less tearing than XClearWindow()
+ XSetForeground(x11->display, ctx->vo_gc, ctx->xv_colorkey);
+ XFillRectangle(x11->display, x11->window, ctx->vo_gc, rc->x0, rc->y0,
+ rc->x1 - rc->x0, rc->y1 - rc->y0);
+ }
+}
+
+static void read_xv_csp(struct vo *vo)
+{
+ struct xvctx *ctx = vo->priv;
+ ctx->cached_csp = 0;
+ int bt709_enabled;
+ if (xv_get_eq(vo, ctx->xv_port, "bt_709", &bt709_enabled))
+ ctx->cached_csp = bt709_enabled == 100 ? MP_CSP_BT_709 : MP_CSP_BT_601;
+}
+
+
+static void fill_rect(struct vo *vo, GC gc, int x0, int y0, int x1, int y1)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ x0 = MPMAX(x0, 0);
+ y0 = MPMAX(y0, 0);
+ x1 = MPMIN(x1, vo->dwidth);
+ y1 = MPMIN(y1, vo->dheight);
+
+ if (x11->window && gc && x1 > x0 && y1 > y0)
+ XFillRectangle(x11->display, x11->window, gc, x0, y0, x1 - x0, y1 - y0);
+}
+
+// Clear everything outside of rc with the background color
+static void vo_x11_clear_background(struct vo *vo, const struct mp_rect *rc)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct xvctx *ctx = vo->priv;
+ GC gc = ctx->f_gc;
+
+ int w = vo->dwidth;
+ int h = vo->dheight;
+
+ fill_rect(vo, gc, 0, 0, w, rc->y0); // top
+ fill_rect(vo, gc, 0, rc->y1, w, h); // bottom
+ fill_rect(vo, gc, 0, rc->y0, rc->x0, rc->y1); // left
+ fill_rect(vo, gc, rc->x1, rc->y0, w, rc->y1); // right
+
+ XFlush(x11->display);
+}
+
+static void resize(struct vo *vo)
+{
+ struct xvctx *ctx = vo->priv;
+
+ // Can't be used, because the function calculates screen-space coordinates,
+ // while we need video-space.
+ struct mp_osd_res unused;
+
+ vo_get_src_dst_rects(vo, &ctx->src_rect, &ctx->dst_rect, &unused);
+
+ vo_x11_clear_background(vo, &ctx->dst_rect);
+ xv_draw_colorkey(vo, &ctx->dst_rect);
+ read_xv_csp(vo);
+
+ mp_input_set_mouse_transform(vo->input_ctx, &ctx->dst_rect, &ctx->src_rect);
+
+ vo->want_redraw = true;
+}
+
+/*
+ * create and map window,
+ * allocate colors and (shared) memory
+ */
+static int reconfig(struct vo *vo, struct mp_image_params *params)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct xvctx *ctx = vo->priv;
+ int i;
+
+ ctx->image_height = params->h;
+ ctx->image_width = params->w;
+ ctx->image_format = params->imgfmt;
+
+ if ((ctx->max_width != 0 && ctx->max_height != 0)
+ && (ctx->image_width > ctx->max_width
+ || ctx->image_height > ctx->max_height)) {
+ MP_ERR(vo, "Source image dimensions are too high: %ux%u (maximum is %ux%u)\n",
+ ctx->image_width, ctx->image_height, ctx->max_width,
+ ctx->max_height);
+ return -1;
+ }
+
+ /* check image formats */
+ ctx->xv_format = 0;
+ for (i = 0; i < ctx->formats; i++) {
+ MP_VERBOSE(vo, "Xvideo image format: 0x%x (%4.4s) %s\n",
+ ctx->fo[i].id, (char *) &ctx->fo[i].id,
+ (ctx->fo[i].format == XvPacked) ? "packed" : "planar");
+ if (ctx->fo[i].id == find_xv_format(ctx->image_format))
+ ctx->xv_format = ctx->fo[i].id;
+ }
+ if (!ctx->xv_format)
+ return -1;
+
+ vo_x11_config_vo_window(vo);
+
+ if (!ctx->f_gc && !ctx->vo_gc) {
+ ctx->f_gc = XCreateGC(x11->display, x11->window, 0, 0);
+ ctx->vo_gc = XCreateGC(x11->display, x11->window, 0, NULL);
+ XSetForeground(x11->display, ctx->f_gc, 0);
+ }
+
+ if (ctx->xv_ck_info.method == CK_METHOD_BACKGROUND)
+ XSetWindowBackground(x11->display, x11->window, ctx->xv_colorkey);
+
+ MP_VERBOSE(vo, "using Xvideo port %d for hw scaling\n", ctx->xv_port);
+
+ // In case config has been called before
+ for (i = 0; i < ctx->num_buffers; i++)
+ deallocate_xvimage(vo, i);
+
+ ctx->num_buffers = ctx->cfg_buffers;
+
+ for (i = 0; i < ctx->num_buffers; i++) {
+ if (!allocate_xvimage(vo, i)) {
+ MP_FATAL(vo, "could not allocate Xv image data\n");
+ return -1;
+ }
+ }
+
+ ctx->current_buf = 0;
+ ctx->current_ip_buf = 0;
+
+ int is_709 = params->color.space == MP_CSP_BT_709;
+ xv_set_eq(vo, ctx->xv_port, "bt_709", is_709 * 200 - 100);
+ read_xv_csp(vo);
+
+ resize(vo);
+
+ return 0;
+}
+
+static bool allocate_xvimage(struct vo *vo, int foo)
+{
+ struct xvctx *ctx = vo->priv;
+ struct vo_x11_state *x11 = vo->x11;
+ // align it for faster OSD rendering (draw_bmp.c swscale usage)
+ int aligned_w = FFALIGN(ctx->image_width, 32);
+ // round up the height to next chroma boundary too
+ int aligned_h = FFALIGN(ctx->image_height, 2);
+ if (x11->display_is_local && XShmQueryExtension(x11->display)) {
+ ctx->Shmem_Flag = 1;
+ x11->ShmCompletionEvent = XShmGetEventBase(x11->display)
+ + ShmCompletion;
+ } else {
+ ctx->Shmem_Flag = 0;
+ MP_INFO(vo, "Shared memory not supported\nReverting to normal Xv.\n");
+ }
+ if (ctx->Shmem_Flag) {
+ ctx->xvimage[foo] =
+ (XvImage *) XvShmCreateImage(x11->display, ctx->xv_port,
+ ctx->xv_format, NULL,
+ aligned_w, aligned_h,
+ &ctx->Shminfo[foo]);
+ if (!ctx->xvimage[foo])
+ return false;
+
+ ctx->Shminfo[foo].shmid = shmget(IPC_PRIVATE,
+ ctx->xvimage[foo]->data_size,
+ IPC_CREAT | 0777);
+ ctx->Shminfo[foo].shmaddr = shmat(ctx->Shminfo[foo].shmid, 0, 0);
+ if (ctx->Shminfo[foo].shmaddr == (void *)-1)
+ return false;
+ ctx->Shminfo[foo].readOnly = False;
+
+ ctx->xvimage[foo]->data = ctx->Shminfo[foo].shmaddr;
+ XShmAttach(x11->display, &ctx->Shminfo[foo]);
+ XSync(x11->display, False);
+ shmctl(ctx->Shminfo[foo].shmid, IPC_RMID, 0);
+ } else {
+ ctx->xvimage[foo] =
+ (XvImage *) XvCreateImage(x11->display, ctx->xv_port,
+ ctx->xv_format, NULL, aligned_w,
+ aligned_h);
+ if (!ctx->xvimage[foo])
+ return false;
+ ctx->xvimage[foo]->data = av_malloc(ctx->xvimage[foo]->data_size);
+ if (!ctx->xvimage[foo]->data)
+ return false;
+ XSync(x11->display, False);
+ }
+
+ if ((ctx->xvimage[foo]->width < aligned_w) ||
+ (ctx->xvimage[foo]->height < aligned_h)) {
+ MP_ERR(vo, "Got XvImage with too small size: %ux%u (expected %ux%u)\n",
+ ctx->xvimage[foo]->width, ctx->xvimage[foo]->height,
+ aligned_w, ctx->image_height);
+ return false;
+ }
+
+ struct mp_image img = get_xv_buffer(vo, foo);
+ mp_image_set_size(&img, aligned_w, aligned_h);
+ mp_image_clear(&img, 0, 0, img.w, img.h);
+ return true;
+}
+
+static void deallocate_xvimage(struct vo *vo, int foo)
+{
+ struct xvctx *ctx = vo->priv;
+ if (ctx->Shmem_Flag) {
+ XShmDetach(vo->x11->display, &ctx->Shminfo[foo]);
+ shmdt(ctx->Shminfo[foo].shmaddr);
+ } else {
+ av_free(ctx->xvimage[foo]->data);
+ }
+ if (ctx->xvimage[foo])
+ XFree(ctx->xvimage[foo]);
+
+ ctx->xvimage[foo] = NULL;
+ ctx->Shminfo[foo] = (XShmSegmentInfo){0};
+
+ XSync(vo->x11->display, False);
+ return;
+}
+
+static inline void put_xvimage(struct vo *vo, XvImage *xvi)
+{
+ struct xvctx *ctx = vo->priv;
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_rect *src = &ctx->src_rect;
+ struct mp_rect *dst = &ctx->dst_rect;
+ int dw = dst->x1 - dst->x0, dh = dst->y1 - dst->y0;
+ int sw = src->x1 - src->x0, sh = src->y1 - src->y0;
+
+ if (ctx->Shmem_Flag) {
+ XvShmPutImage(x11->display, ctx->xv_port, x11->window, ctx->vo_gc, xvi,
+ src->x0, src->y0, sw, sh,
+ dst->x0, dst->y0, dw, dh,
+ True);
+ x11->ShmCompletionWaitCount++;
+ } else {
+ XvPutImage(x11->display, ctx->xv_port, x11->window, ctx->vo_gc, xvi,
+ src->x0, src->y0, sw, sh,
+ dst->x0, dst->y0, dw, dh);
+ }
+}
+
+static struct mp_image get_xv_buffer(struct vo *vo, int buf_index)
+{
+ struct xvctx *ctx = vo->priv;
+ XvImage *xv_image = ctx->xvimage[buf_index];
+
+ struct mp_image img = {0};
+ mp_image_set_size(&img, ctx->image_width, ctx->image_height);
+ mp_image_setfmt(&img, ctx->image_format);
+
+ bool swapuv = ctx->xv_format == MP_FOURCC_YV12;
+ for (int n = 0; n < img.num_planes; n++) {
+ int sn = n > 0 && swapuv ? (n == 1 ? 2 : 1) : n;
+ img.planes[n] = xv_image->data + xv_image->offsets[sn];
+ img.stride[n] = xv_image->pitches[sn];
+ }
+
+ if (vo->params) {
+ struct mp_image_params params = *vo->params;
+ if (ctx->cached_csp)
+ params.color.space = ctx->cached_csp;
+ mp_image_set_attributes(&img, &params);
+ }
+
+ return img;
+}
+
+static void wait_for_completion(struct vo *vo, int max_outstanding)
+{
+ struct xvctx *ctx = vo->priv;
+ struct vo_x11_state *x11 = vo->x11;
+ if (ctx->Shmem_Flag) {
+ while (x11->ShmCompletionWaitCount > max_outstanding) {
+ if (!ctx->Shm_Warned_Slow) {
+ MP_WARN(vo, "X11 can't keep up! Waiting"
+ " for XShm completion events...\n");
+ ctx->Shm_Warned_Slow = 1;
+ }
+ mp_sleep_ns(MP_TIME_MS_TO_NS(1));
+ vo_x11_check_events(vo);
+ }
+ }
+}
+
+static void flip_page(struct vo *vo)
+{
+ struct xvctx *ctx = vo->priv;
+ put_xvimage(vo, ctx->xvimage[ctx->current_buf]);
+
+ /* remember the currently visible buffer */
+ ctx->current_buf = (ctx->current_buf + 1) % ctx->num_buffers;
+
+ if (!ctx->Shmem_Flag)
+ XSync(vo->x11->display, False);
+
+ if (vo->x11->use_present) {
+ vo_x11_present(vo);
+ present_sync_swap(vo->x11->present);
+ }
+}
+
+static void get_vsync(struct vo *vo, struct vo_vsync_info *info)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (x11->use_present)
+ present_sync_get_info(x11->present, info);
+}
+
+static void draw_frame(struct vo *vo, struct vo_frame *frame)
+{
+ struct xvctx *ctx = vo->priv;
+
+ wait_for_completion(vo, ctx->num_buffers - 1);
+ bool render = vo_x11_check_visible(vo);
+ if (!render)
+ return;
+
+ struct mp_image xv_buffer = get_xv_buffer(vo, ctx->current_buf);
+ if (frame->current) {
+ mp_image_copy(&xv_buffer, frame->current);
+ } else {
+ mp_image_clear(&xv_buffer, 0, 0, xv_buffer.w, xv_buffer.h);
+ }
+
+ struct mp_osd_res res = osd_res_from_image_params(vo->params);
+ osd_draw_on_image(vo->osd, res, frame->current ? frame->current->pts : 0, 0, &xv_buffer);
+
+ if (frame->current != ctx->original_image)
+ ctx->original_image = frame->current;
+}
+
+static int query_format(struct vo *vo, int format)
+{
+ struct xvctx *ctx = vo->priv;
+ uint32_t i;
+
+ int fourcc = find_xv_format(format);
+ if (fourcc) {
+ for (i = 0; i < ctx->formats; i++) {
+ if (ctx->fo[i].id == fourcc)
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static void uninit(struct vo *vo)
+{
+ struct xvctx *ctx = vo->priv;
+ int i;
+
+ if (ctx->ai)
+ XvFreeAdaptorInfo(ctx->ai);
+ ctx->ai = NULL;
+ if (ctx->fo) {
+ XFree(ctx->fo);
+ ctx->fo = NULL;
+ }
+ for (i = 0; i < ctx->num_buffers; i++)
+ deallocate_xvimage(vo, i);
+ if (ctx->f_gc != None)
+ XFreeGC(vo->x11->display, ctx->f_gc);
+ if (ctx->vo_gc != None)
+ XFreeGC(vo->x11->display, ctx->vo_gc);
+ // uninit() shouldn't get called unless initialization went past vo_init()
+ vo_x11_uninit(vo);
+}
+
+static int preinit(struct vo *vo)
+{
+ XvPortID xv_p;
+ int busy_ports = 0;
+ unsigned int i;
+ struct xvctx *ctx = vo->priv;
+ int xv_adaptor = ctx->cfg_xv_adaptor;
+
+ if (!vo_x11_init(vo))
+ return -1;
+
+ if (!vo_x11_create_vo_window(vo, NULL, "xv"))
+ goto error;
+
+ struct vo_x11_state *x11 = vo->x11;
+
+ /* check for Xvideo extension */
+ unsigned int ver, rel, req, ev, err;
+ if (Success != XvQueryExtension(x11->display, &ver, &rel, &req, &ev, &err)) {
+ MP_ERR(vo, "Xv not supported by this X11 version/driver\n");
+ goto error;
+ }
+
+ /* check for Xvideo support */
+ if (Success !=
+ XvQueryAdaptors(x11->display, DefaultRootWindow(x11->display),
+ &ctx->adaptors, &ctx->ai)) {
+ MP_ERR(vo, "XvQueryAdaptors failed.\n");
+ goto error;
+ }
+
+ /* check adaptors */
+ if (ctx->xv_port) {
+ int port_found;
+
+ for (port_found = 0, i = 0; !port_found && i < ctx->adaptors; i++) {
+ if ((ctx->ai[i].type & XvInputMask)
+ && (ctx->ai[i].type & XvImageMask)) {
+ for (xv_p = ctx->ai[i].base_id;
+ xv_p < ctx->ai[i].base_id + ctx->ai[i].num_ports;
+ ++xv_p) {
+ if (xv_p == ctx->xv_port) {
+ port_found = 1;
+ break;
+ }
+ }
+ }
+ }
+ if (port_found) {
+ if (XvGrabPort(x11->display, ctx->xv_port, CurrentTime))
+ ctx->xv_port = 0;
+ } else {
+ MP_WARN(vo, "Invalid port parameter, overriding with port 0.\n");
+ ctx->xv_port = 0;
+ }
+ }
+
+ for (i = 0; i < ctx->adaptors && ctx->xv_port == 0; i++) {
+ /* check if adaptor number has been specified */
+ if (xv_adaptor != -1 && xv_adaptor != i)
+ continue;
+
+ if ((ctx->ai[i].type & XvInputMask) && (ctx->ai[i].type & XvImageMask)) {
+ for (xv_p = ctx->ai[i].base_id;
+ xv_p < ctx->ai[i].base_id + ctx->ai[i].num_ports; ++xv_p)
+ if (!XvGrabPort(x11->display, xv_p, CurrentTime)) {
+ ctx->xv_port = xv_p;
+ MP_VERBOSE(vo, "Using Xv Adapter #%d (%s)\n",
+ i, ctx->ai[i].name);
+ break;
+ } else {
+ MP_WARN(vo, "Could not grab port %i.\n", (int) xv_p);
+ ++busy_ports;
+ }
+ }
+ }
+ if (!ctx->xv_port) {
+ if (busy_ports)
+ MP_ERR(vo, "Xvideo ports busy.\n");
+ else
+ MP_ERR(vo, "No Xvideo support found.\n");
+ goto error;
+ }
+
+ if (!xv_init_colorkey(vo)) {
+ goto error; // bail out, colorkey setup failed
+ }
+ xv_enable_vsync(vo);
+ xv_get_max_img_dim(vo, &ctx->max_width, &ctx->max_height);
+
+ ctx->fo = XvListImageFormats(x11->display, ctx->xv_port,
+ (int *) &ctx->formats);
+
+ MP_WARN(vo, "Warning: this legacy VO has bad quality and performance, "
+ "and will in particular result in blurry OSD and subtitles. "
+ "You should fix your graphics drivers, or not force the xv VO.\n");
+ return 0;
+
+ error:
+ uninit(vo); // free resources
+ return -1;
+}
+
+static int control(struct vo *vo, uint32_t request, void *data)
+{
+ switch (request) {
+ case VOCTRL_SET_PANSCAN:
+ resize(vo);
+ return VO_TRUE;
+ }
+ int events = 0;
+ int r = vo_x11_control(vo, &events, request, data);
+ if (events & (VO_EVENT_EXPOSE | VO_EVENT_RESIZE))
+ resize(vo);
+ vo_event(vo, events);
+ return r;
+}
+
+#define OPT_BASE_STRUCT struct xvctx
+
+const struct vo_driver video_out_xv = {
+ .description = "X11/Xv",
+ .name = "xv",
+ .preinit = preinit,
+ .query_format = query_format,
+ .reconfig = reconfig,
+ .control = control,
+ .draw_frame = draw_frame,
+ .flip_page = flip_page,
+ .get_vsync = get_vsync,
+ .wakeup = vo_x11_wakeup,
+ .wait_events = vo_x11_wait_events,
+ .uninit = uninit,
+ .priv_size = sizeof(struct xvctx),
+ .priv_defaults = &(const struct xvctx) {
+ .cfg_xv_adaptor = -1,
+ .xv_ck_info = {CK_METHOD_MANUALFILL, CK_SRC_CUR},
+ .colorkey = 0x0000ff00, // default colorkey is green
+ // (0xff000000 means that colorkey has been disabled)
+ .cfg_buffers = 2,
+ },
+ .options = (const struct m_option[]) {
+ {"port", OPT_INT(xv_port), M_RANGE(0, DBL_MAX)},
+ {"adaptor", OPT_INT(cfg_xv_adaptor), M_RANGE(-1, DBL_MAX)},
+ {"ck", OPT_CHOICE(xv_ck_info.source,
+ {"use", CK_SRC_USE},
+ {"set", CK_SRC_SET},
+ {"cur", CK_SRC_CUR})},
+ {"ck-method", OPT_CHOICE(xv_ck_info.method,
+ {"none", CK_METHOD_NONE},
+ {"bg", CK_METHOD_BACKGROUND},
+ {"man", CK_METHOD_MANUALFILL},
+ {"auto", CK_METHOD_AUTOPAINT})},
+ {"colorkey", OPT_INT(colorkey)},
+ {"buffers", OPT_INT(cfg_buffers), M_RANGE(1, MAX_BUFFERS)},
+ {0}
+ },
+ .options_prefix = "xv",
+};
diff --git a/video/out/vulkan/common.h b/video/out/vulkan/common.h
new file mode 100644
index 0000000..d006942
--- /dev/null
+++ b/video/out/vulkan/common.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <assert.h>
+
+#include "config.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+
+// We need to define all platforms we want to support. Since we have
+// our own mechanism for checking this, we re-define the right symbols
+#if HAVE_WAYLAND
+#define VK_USE_PLATFORM_WAYLAND_KHR
+#endif
+#if HAVE_X11
+#define VK_USE_PLATFORM_XLIB_KHR
+#endif
+#if HAVE_WIN32_DESKTOP
+#define VK_USE_PLATFORM_WIN32_KHR
+#endif
+#if HAVE_COCOA
+#define VK_USE_PLATFORM_MACOS_MVK
+#define VK_USE_PLATFORM_METAL_EXT
+#endif
+
+#include <libplacebo/vulkan.h>
+
+// Shared struct used to hold vulkan context information
+struct mpvk_ctx {
+ pl_log pllog;
+ pl_vk_inst vkinst;
+ pl_vulkan vulkan;
+ pl_gpu gpu; // points to vulkan->gpu for convenience
+ pl_swapchain swapchain;
+ VkSurfaceKHR surface;
+};
diff --git a/video/out/vulkan/context.c b/video/out/vulkan/context.c
new file mode 100644
index 0000000..5087403
--- /dev/null
+++ b/video/out/vulkan/context.c
@@ -0,0 +1,372 @@
+/*
+ * 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 "config.h"
+
+#if HAVE_LAVU_UUID
+#include <libavutil/uuid.h>
+#else
+#include "misc/uuid.h"
+#endif
+
+#include "options/m_config.h"
+#include "video/out/placebo/ra_pl.h"
+
+#include "context.h"
+#include "utils.h"
+
+struct vulkan_opts {
+ char *device; // force a specific GPU
+ int swap_mode;
+ int queue_count;
+ bool async_transfer;
+ bool async_compute;
+};
+
+static int vk_validate_dev(struct mp_log *log, const struct m_option *opt,
+ struct bstr name, const char **value)
+{
+ struct bstr param = bstr0(*value);
+ int ret = M_OPT_INVALID;
+ VkResult res;
+
+ // Create a dummy instance to validate/list the devices
+ VkInstanceCreateInfo info = {
+ .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
+ .pApplicationInfo = &(VkApplicationInfo) {
+ .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
+ .apiVersion = VK_API_VERSION_1_1,
+ }
+ };
+
+ VkInstance inst;
+ VkPhysicalDevice *devices = NULL;
+ uint32_t num = 0;
+
+ res = vkCreateInstance(&info, NULL, &inst);
+ if (res != VK_SUCCESS)
+ goto done;
+
+ res = vkEnumeratePhysicalDevices(inst, &num, NULL);
+ if (res != VK_SUCCESS)
+ goto done;
+
+ devices = talloc_array(NULL, VkPhysicalDevice, num);
+ res = vkEnumeratePhysicalDevices(inst, &num, devices);
+ if (res != VK_SUCCESS)
+ goto done;
+
+ bool help = bstr_equals0(param, "help");
+ if (help) {
+ mp_info(log, "Available vulkan devices:\n");
+ ret = M_OPT_EXIT;
+ }
+
+ AVUUID param_uuid;
+ bool is_uuid = av_uuid_parse(*value, param_uuid) == 0;
+
+ for (int i = 0; i < num; i++) {
+ VkPhysicalDeviceIDPropertiesKHR id_prop = { 0 };
+ id_prop.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES_KHR;
+
+ VkPhysicalDeviceProperties2KHR prop2 = { 0 };
+ prop2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2_KHR;
+ prop2.pNext = &id_prop;
+
+ vkGetPhysicalDeviceProperties2(devices[i], &prop2);
+
+ const VkPhysicalDeviceProperties *prop = &prop2.properties;
+
+ if (help) {
+ char device_uuid[37];
+ av_uuid_unparse(id_prop.deviceUUID, device_uuid);
+ mp_info(log, " '%s' (GPU %d, PCI ID %x:%x, UUID %s)\n",
+ prop->deviceName, i, (unsigned)prop->vendorID,
+ (unsigned)prop->deviceID, device_uuid);
+ } else if (bstr_equals0(param, prop->deviceName)) {
+ ret = 0;
+ goto done;
+ } else if (is_uuid && av_uuid_equal(param_uuid, id_prop.deviceUUID)) {
+ ret = 0;
+ goto done;
+ }
+ }
+
+ if (!help)
+ mp_err(log, "No device with %s '%.*s'!\n", is_uuid ? "UUID" : "name",
+ BSTR_P(param));
+
+done:
+ talloc_free(devices);
+ return ret;
+}
+
+#define OPT_BASE_STRUCT struct vulkan_opts
+const struct m_sub_options vulkan_conf = {
+ .opts = (const struct m_option[]) {
+ {"vulkan-device", OPT_STRING_VALIDATE(device, vk_validate_dev)},
+ {"vulkan-swap-mode", OPT_CHOICE(swap_mode,
+ {"auto", -1},
+ {"fifo", VK_PRESENT_MODE_FIFO_KHR},
+ {"fifo-relaxed", VK_PRESENT_MODE_FIFO_RELAXED_KHR},
+ {"mailbox", VK_PRESENT_MODE_MAILBOX_KHR},
+ {"immediate", VK_PRESENT_MODE_IMMEDIATE_KHR})},
+ {"vulkan-queue-count", OPT_INT(queue_count), M_RANGE(1, 8)},
+ {"vulkan-async-transfer", OPT_BOOL(async_transfer)},
+ {"vulkan-async-compute", OPT_BOOL(async_compute)},
+ {"vulkan-disable-events", OPT_REMOVED("Unused")},
+ {0}
+ },
+ .size = sizeof(struct vulkan_opts),
+ .defaults = &(struct vulkan_opts) {
+ .swap_mode = -1,
+ .queue_count = 1,
+ .async_transfer = true,
+ .async_compute = true,
+ },
+};
+
+struct priv {
+ struct mpvk_ctx *vk;
+ struct vulkan_opts *opts;
+ struct ra_vk_ctx_params params;
+ struct ra_tex proxy_tex;
+};
+
+static const struct ra_swapchain_fns vulkan_swapchain;
+
+struct mpvk_ctx *ra_vk_ctx_get(struct ra_ctx *ctx)
+{
+ if (!ctx->swapchain || ctx->swapchain->fns != &vulkan_swapchain)
+ return NULL;
+
+ struct priv *p = ctx->swapchain->priv;
+ return p->vk;
+}
+
+void ra_vk_ctx_uninit(struct ra_ctx *ctx)
+{
+ if (!ctx->swapchain)
+ return;
+
+ struct priv *p = ctx->swapchain->priv;
+ struct mpvk_ctx *vk = p->vk;
+
+ if (ctx->ra) {
+ pl_gpu_finish(vk->gpu);
+ pl_swapchain_destroy(&vk->swapchain);
+ ctx->ra->fns->destroy(ctx->ra);
+ ctx->ra = NULL;
+ }
+
+ vk->gpu = NULL;
+ pl_vulkan_destroy(&vk->vulkan);
+ TA_FREEP(&ctx->swapchain);
+}
+
+bool ra_vk_ctx_init(struct ra_ctx *ctx, struct mpvk_ctx *vk,
+ struct ra_vk_ctx_params params,
+ VkPresentModeKHR preferred_mode)
+{
+ struct ra_swapchain *sw = ctx->swapchain = talloc_zero(NULL, struct ra_swapchain);
+ sw->ctx = ctx;
+ sw->fns = &vulkan_swapchain;
+
+ struct priv *p = sw->priv = talloc_zero(sw, struct priv);
+ p->vk = vk;
+ p->params = params;
+ p->opts = mp_get_config_group(p, ctx->global, &vulkan_conf);
+
+ VkPhysicalDeviceFeatures2 features = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
+ };
+
+#if HAVE_VULKAN_INTEROP
+ /*
+ * Request the additional extensions and features required to make full use
+ * of the ffmpeg Vulkan hwcontext and video decoding capability.
+ */
+ const char *opt_extensions[] = {
+ VK_EXT_DESCRIPTOR_BUFFER_EXTENSION_NAME,
+ VK_EXT_SHADER_ATOMIC_FLOAT_EXTENSION_NAME,
+ VK_KHR_VIDEO_DECODE_QUEUE_EXTENSION_NAME,
+ VK_KHR_VIDEO_DECODE_H264_EXTENSION_NAME,
+ VK_KHR_VIDEO_DECODE_H265_EXTENSION_NAME,
+ VK_KHR_VIDEO_QUEUE_EXTENSION_NAME,
+ // This is a literal string as it's not in the official headers yet.
+ "VK_MESA_video_decode_av1",
+ };
+
+ VkPhysicalDeviceDescriptorBufferFeaturesEXT descriptor_buffer_feature = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_BUFFER_FEATURES_EXT,
+ .pNext = NULL,
+ .descriptorBuffer = true,
+ .descriptorBufferPushDescriptors = true,
+ };
+
+ VkPhysicalDeviceShaderAtomicFloatFeaturesEXT atomic_float_feature = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_FLOAT_FEATURES_EXT,
+ .pNext = &descriptor_buffer_feature,
+ .shaderBufferFloat32Atomics = true,
+ .shaderBufferFloat32AtomicAdd = true,
+ };
+
+ features.pNext = &atomic_float_feature;
+#endif
+
+ AVUUID param_uuid = { 0 };
+ bool is_uuid = p->opts->device &&
+ av_uuid_parse(p->opts->device, param_uuid) == 0;
+
+ assert(vk->pllog);
+ assert(vk->vkinst);
+ struct pl_vulkan_params device_params = {
+ .instance = vk->vkinst->instance,
+ .get_proc_addr = vk->vkinst->get_proc_addr,
+ .surface = vk->surface,
+ .async_transfer = p->opts->async_transfer,
+ .async_compute = p->opts->async_compute,
+ .queue_count = p->opts->queue_count,
+#if HAVE_VULKAN_INTEROP
+ .extra_queues = VK_QUEUE_VIDEO_DECODE_BIT_KHR,
+ .opt_extensions = opt_extensions,
+ .num_opt_extensions = MP_ARRAY_SIZE(opt_extensions),
+#endif
+ .features = &features,
+ .device_name = is_uuid ? NULL : p->opts->device,
+ };
+ if (is_uuid)
+ av_uuid_copy(device_params.device_uuid, param_uuid);
+
+ vk->vulkan = pl_vulkan_create(vk->pllog, &device_params);
+ if (!vk->vulkan)
+ goto error;
+
+ vk->gpu = vk->vulkan->gpu;
+ ctx->ra = ra_create_pl(vk->gpu, ctx->log);
+ if (!ctx->ra)
+ goto error;
+
+ // Create the swapchain
+ struct pl_vulkan_swapchain_params pl_params = {
+ .surface = vk->surface,
+ .present_mode = preferred_mode,
+ .swapchain_depth = ctx->vo->opts->swapchain_depth,
+ // mpv already handles resize events, so gracefully allow suboptimal
+ // swapchains to exist in order to make resizing even smoother
+ .allow_suboptimal = true,
+ };
+
+ if (p->opts->swap_mode >= 0) // user override
+ pl_params.present_mode = p->opts->swap_mode;
+
+ vk->swapchain = pl_vulkan_create_swapchain(vk->vulkan, &pl_params);
+ if (!vk->swapchain)
+ goto error;
+
+ return true;
+
+error:
+ ra_vk_ctx_uninit(ctx);
+ return false;
+}
+
+bool ra_vk_ctx_resize(struct ra_ctx *ctx, int width, int height)
+{
+ struct priv *p = ctx->swapchain->priv;
+
+ bool ok = pl_swapchain_resize(p->vk->swapchain, &width, &height);
+ ctx->vo->dwidth = width;
+ ctx->vo->dheight = height;
+
+ return ok;
+}
+
+char *ra_vk_ctx_get_device_name(struct ra_ctx *ctx)
+{
+ /*
+ * This implementation is a bit odd because it has to work even if the
+ * ctx hasn't been initialised yet. A context implementation may need access
+ * to the device name before it can fully initialise the ctx.
+ */
+ struct vulkan_opts *opts = mp_get_config_group(NULL, ctx->global,
+ &vulkan_conf);
+ char *device_name = talloc_strdup(NULL, opts->device);
+ talloc_free(opts);
+ return device_name;
+}
+
+static int color_depth(struct ra_swapchain *sw)
+{
+ return 0; // TODO: implement this somehow?
+}
+
+static bool start_frame(struct ra_swapchain *sw, struct ra_fbo *out_fbo)
+{
+ struct priv *p = sw->priv;
+ struct pl_swapchain_frame frame;
+
+ bool visible = true;
+ if (p->params.check_visible)
+ visible = p->params.check_visible(sw->ctx);
+
+ // If out_fbo is NULL, this was called from vo_gpu_next. Bail out.
+ if (out_fbo == NULL || !visible)
+ return visible;
+
+ if (!pl_swapchain_start_frame(p->vk->swapchain, &frame))
+ return false;
+ if (!mppl_wrap_tex(sw->ctx->ra, frame.fbo, &p->proxy_tex))
+ return false;
+
+ *out_fbo = (struct ra_fbo) {
+ .tex = &p->proxy_tex,
+ .flip = frame.flipped,
+ };
+
+ return true;
+}
+
+static bool submit_frame(struct ra_swapchain *sw, const struct vo_frame *frame)
+{
+ struct priv *p = sw->priv;
+ return pl_swapchain_submit_frame(p->vk->swapchain);
+}
+
+static void swap_buffers(struct ra_swapchain *sw)
+{
+ struct priv *p = sw->priv;
+ pl_swapchain_swap_buffers(p->vk->swapchain);
+ if (p->params.swap_buffers)
+ p->params.swap_buffers(sw->ctx);
+}
+
+static void get_vsync(struct ra_swapchain *sw,
+ struct vo_vsync_info *info)
+{
+ struct priv *p = sw->priv;
+ if (p->params.get_vsync)
+ p->params.get_vsync(sw->ctx, info);
+}
+
+static const struct ra_swapchain_fns vulkan_swapchain = {
+ .color_depth = color_depth,
+ .start_frame = start_frame,
+ .submit_frame = submit_frame,
+ .swap_buffers = swap_buffers,
+ .get_vsync = get_vsync,
+};
diff --git a/video/out/vulkan/context.h b/video/out/vulkan/context.h
new file mode 100644
index 0000000..c846942
--- /dev/null
+++ b/video/out/vulkan/context.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "video/out/gpu/context.h"
+#include "common.h"
+
+struct ra_vk_ctx_params {
+ // See ra_swapchain_fns.get_vsync.
+ void (*get_vsync)(struct ra_ctx *ctx, struct vo_vsync_info *info);
+
+ // For special contexts (i.e. wayland) that want to check visibility
+ // before drawing a frame.
+ bool (*check_visible)(struct ra_ctx *ctx);
+
+ // In case something special needs to be done on the buffer swap.
+ void (*swap_buffers)(struct ra_ctx *ctx);
+};
+
+// Helpers for ra_ctx based on ra_vk. These initialize ctx->ra and ctx->swchain.
+void ra_vk_ctx_uninit(struct ra_ctx *ctx);
+bool ra_vk_ctx_init(struct ra_ctx *ctx, struct mpvk_ctx *vk,
+ struct ra_vk_ctx_params params,
+ VkPresentModeKHR preferred_mode);
+
+// Handles a resize request, and updates ctx->vo->dwidth/dheight
+bool ra_vk_ctx_resize(struct ra_ctx *ctx, int width, int height);
+
+// May be called on a ra_ctx of any type.
+struct mpvk_ctx *ra_vk_ctx_get(struct ra_ctx *ctx);
+
+// Get the user requested Vulkan device name.
+char *ra_vk_ctx_get_device_name(struct ra_ctx *ctx);
diff --git a/video/out/vulkan/context_android.c b/video/out/vulkan/context_android.c
new file mode 100644
index 0000000..ddab391
--- /dev/null
+++ b/video/out/vulkan/context_android.c
@@ -0,0 +1,96 @@
+/*
+ * 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 <vulkan/vulkan.h>
+#include <vulkan/vulkan_android.h>
+
+#include "video/out/android_common.h"
+#include "common.h"
+#include "context.h"
+#include "utils.h"
+
+struct priv {
+ struct mpvk_ctx vk;
+};
+
+static void android_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ ra_vk_ctx_uninit(ctx);
+ mpvk_uninit(&p->vk);
+
+ vo_android_uninit(ctx->vo);
+}
+
+static bool android_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct mpvk_ctx *vk = &p->vk;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+
+ if (!vo_android_init(ctx->vo))
+ goto fail;
+
+ if (!mpvk_init(vk, ctx, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME))
+ goto fail;
+
+ VkAndroidSurfaceCreateInfoKHR info = {
+ .sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR,
+ .window = vo_android_native_window(ctx->vo)
+ };
+
+ struct ra_vk_ctx_params params = {0};
+
+ VkInstance inst = vk->vkinst->instance;
+ VkResult res = vkCreateAndroidSurfaceKHR(inst, &info, NULL, &vk->surface);
+ if (res != VK_SUCCESS) {
+ MP_MSG(ctx, msgl, "Failed creating Android surface\n");
+ goto fail;
+ }
+
+ if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_FIFO_KHR))
+ goto fail;
+
+ return true;
+fail:
+ android_uninit(ctx);
+ return false;
+}
+
+static bool android_reconfig(struct ra_ctx *ctx)
+{
+ int w, h;
+ if (!vo_android_surface_size(ctx->vo, &w, &h))
+ return false;
+
+ ra_vk_ctx_resize(ctx, w, h);
+ return true;
+}
+
+static int android_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ return VO_NOTIMPL;
+}
+
+const struct ra_ctx_fns ra_ctx_vulkan_android = {
+ .type = "vulkan",
+ .name = "androidvk",
+ .reconfig = android_reconfig,
+ .control = android_control,
+ .init = android_init,
+ .uninit = android_uninit,
+};
diff --git a/video/out/vulkan/context_display.c b/video/out/vulkan/context_display.c
new file mode 100644
index 0000000..84cef1e
--- /dev/null
+++ b/video/out/vulkan/context_display.c
@@ -0,0 +1,491 @@
+/*
+ * 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 "context.h"
+#include "options/m_config.h"
+#include "utils.h"
+
+#if HAVE_DRM
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "libmpv/render_gl.h"
+#include "video/out/drm_common.h"
+#endif
+
+struct vulkan_display_opts {
+ int display;
+ int mode;
+ int plane;
+};
+
+struct mode_selector {
+ // Indexes of selected display/mode/plane.
+ int display_idx;
+ int mode_idx;
+ int plane_idx;
+
+ // Must be freed with talloc_free
+ VkDisplayModePropertiesKHR *out_mode_props;
+};
+
+/**
+ * If a selector is passed, verify that it is valid and return the matching
+ * mode properties. If null is passed, walk all modes and print them out.
+ */
+static bool walk_display_properties(struct mp_log *log,
+ int msgl_err,
+ VkPhysicalDevice device,
+ struct mode_selector *selector) {
+ bool ret = false;
+ VkResult res;
+
+ int msgl_info = selector ? MSGL_TRACE : MSGL_INFO;
+
+ // Use a dummy as parent for all other allocations.
+ void *tmp = talloc_new(NULL);
+
+ VkPhysicalDeviceProperties prop;
+ vkGetPhysicalDeviceProperties(device, &prop);
+ mp_msg(log, msgl_info, " '%s' (GPU ID %x:%x)\n", prop.deviceName,
+ (unsigned)prop.vendorID, (unsigned)prop.deviceID);
+
+ // Count displays. This must be done before enumerating planes with the
+ // Intel driver, or it will not enumerate any planes. WTF.
+ int num_displays = 0;
+ vkGetPhysicalDeviceDisplayPropertiesKHR(device, &num_displays, NULL);
+ if (!num_displays) {
+ mp_msg(log, msgl_info, " No available displays for device.\n");
+ goto done;
+ }
+ if (selector && selector->display_idx + 1 > num_displays) {
+ mp_msg(log, msgl_err, "Selected display (%d) not present.\n",
+ selector->display_idx);
+ goto done;
+ }
+
+ // Enumerate Planes
+ int num_planes = 0;
+ vkGetPhysicalDeviceDisplayPlanePropertiesKHR(device, &num_planes, NULL);
+ if (!num_planes) {
+ mp_msg(log, msgl_info, " No available planes for device.\n");
+ goto done;
+ }
+ if (selector && selector->plane_idx + 1 > num_planes) {
+ mp_msg(log, msgl_err, "Selected plane (%d) not present.\n",
+ selector->plane_idx);
+ goto done;
+ }
+
+ VkDisplayPlanePropertiesKHR *planes =
+ talloc_array(tmp, VkDisplayPlanePropertiesKHR, num_planes);
+ res = vkGetPhysicalDeviceDisplayPlanePropertiesKHR(device, &num_planes,
+ planes);
+ if (res != VK_SUCCESS) {
+ mp_msg(log, msgl_err, " Failed enumerating planes\n");
+ goto done;
+ }
+
+ // Allocate zeroed arrays so that planes with no displays have a null entry.
+ VkDisplayKHR **planes_to_displays =
+ talloc_zero_array(tmp, VkDisplayKHR *, num_planes);
+ for (int j = 0; j < num_planes; j++) {
+ int num_displays_for_plane = 0;
+ vkGetDisplayPlaneSupportedDisplaysKHR(device, j,
+ &num_displays_for_plane, NULL);
+ if (!num_displays_for_plane)
+ continue;
+
+ // Null terminated array
+ VkDisplayKHR *displays =
+ talloc_zero_array(planes_to_displays, VkDisplayKHR,
+ num_displays_for_plane + 1);
+ res = vkGetDisplayPlaneSupportedDisplaysKHR(device, j,
+ &num_displays_for_plane,
+ displays);
+ if (res != VK_SUCCESS) {
+ mp_msg(log, msgl_err, " Failed enumerating plane displays\n");
+ continue;
+ }
+ planes_to_displays[j] = displays;
+ }
+
+ // Enumerate Displays and Modes
+ VkDisplayPropertiesKHR *props =
+ talloc_array(tmp, VkDisplayPropertiesKHR, num_displays);
+ res = vkGetPhysicalDeviceDisplayPropertiesKHR(device, &num_displays, props);
+ if (res != VK_SUCCESS) {
+ mp_msg(log, msgl_err, " Failed enumerating display properties\n");
+ goto done;
+ }
+
+ for (int j = 0; j < num_displays; j++) {
+ if (selector && selector->display_idx != j)
+ continue;
+
+ mp_msg(log, msgl_info, " Display %d: '%s' (%dx%d)\n",
+ j,
+ props[j].displayName,
+ props[j].physicalResolution.width,
+ props[j].physicalResolution.height);
+
+ VkDisplayKHR display = props[j].display;
+
+ mp_msg(log, msgl_info, " Modes:\n");
+
+ int num_modes = 0;
+ vkGetDisplayModePropertiesKHR(device, display, &num_modes, NULL);
+ if (!num_modes) {
+ mp_msg(log, msgl_info, " No available modes for display.\n");
+ continue;
+ }
+ if (selector && selector->mode_idx + 1 > num_modes) {
+ mp_msg(log, msgl_err, "Selected mode (%d) not present.\n",
+ selector->mode_idx);
+ goto done;
+ }
+
+ VkDisplayModePropertiesKHR *modes =
+ talloc_array(tmp, VkDisplayModePropertiesKHR, num_modes);
+ res = vkGetDisplayModePropertiesKHR(device, display, &num_modes, modes);
+ if (res != VK_SUCCESS) {
+ mp_msg(log, msgl_err, " Failed enumerating display modes\n");
+ continue;
+ }
+
+ for (int k = 0; k < num_modes; k++) {
+ if (selector && selector->mode_idx != k)
+ continue;
+
+ mp_msg(log, msgl_info, " Mode %02d: %dx%d (%02d.%03d Hz)\n", k,
+ modes[k].parameters.visibleRegion.width,
+ modes[k].parameters.visibleRegion.height,
+ modes[k].parameters.refreshRate / 1000,
+ modes[k].parameters.refreshRate % 1000);
+
+ if (selector)
+ selector->out_mode_props = talloc_dup(NULL, &modes[k]);
+ }
+
+ int found_plane = -1;
+ mp_msg(log, msgl_info, " Planes:\n");
+ for (int k = 0; k < num_planes; k++) {
+ VkDisplayKHR *displays = planes_to_displays[k];
+ if (!displays) {
+ // This plane is not connected to any displays.
+ continue;
+ }
+ for (int d = 0; displays[d]; d++) {
+ if (displays[d] == display) {
+ if (selector && selector->plane_idx != k)
+ continue;
+
+ mp_msg(log, msgl_info, " Plane: %d\n", k);
+ found_plane = k;
+ }
+ }
+ }
+ if (selector && selector->plane_idx != found_plane) {
+ mp_msg(log, msgl_err,
+ "Selected plane (%d) not available on selected display.\n",
+ selector->plane_idx);
+ goto done;
+ }
+ }
+ ret = true;
+done:
+ talloc_free(tmp);
+ return ret;
+}
+
+static int print_display_info(struct mp_log *log, const struct m_option *opt,
+ struct bstr name) {
+ VkResult res;
+ VkPhysicalDevice *devices = NULL;
+
+ // Create a dummy instance to list the resources
+ VkInstanceCreateInfo info = {
+ .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
+ .enabledExtensionCount = 1,
+ .ppEnabledExtensionNames = (const char*[]) {
+ VK_KHR_DISPLAY_EXTENSION_NAME
+ },
+ };
+
+ VkInstance inst = NULL;
+ res = vkCreateInstance(&info, NULL, &inst);
+ if (res != VK_SUCCESS) {
+ mp_warn(log, "Unable to create Vulkan instance.\n");
+ goto done;
+ }
+
+ uint32_t num_devices = 0;
+ vkEnumeratePhysicalDevices(inst, &num_devices, NULL);
+ if (!num_devices) {
+ mp_info(log, "No Vulkan devices detected.\n");
+ goto done;
+ }
+
+ devices = talloc_array(NULL, VkPhysicalDevice, num_devices);
+ vkEnumeratePhysicalDevices(inst, &num_devices, devices);
+ if (res != VK_SUCCESS) {
+ mp_warn(log, "Failed enumerating physical devices.\n");
+ goto done;
+ }
+
+ mp_info(log, "Vulkan Devices:\n");
+ for (int i = 0; i < num_devices; i++) {
+ walk_display_properties(log, MSGL_WARN, devices[i], NULL);
+ }
+
+done:
+ talloc_free(devices);
+ vkDestroyInstance(inst, NULL);
+ return M_OPT_EXIT;
+}
+
+#define OPT_BASE_STRUCT struct vulkan_display_opts
+const struct m_sub_options vulkan_display_conf = {
+ .opts = (const struct m_option[]) {
+ {"vulkan-display-display", OPT_INT(display),
+ .help = print_display_info,
+ },
+ {"vulkan-display-mode", OPT_INT(mode),
+ .help = print_display_info,
+ },
+ {"vulkan-display-plane", OPT_INT(plane),
+ .help = print_display_info,
+ },
+ {0}
+ },
+ .size = sizeof(struct vulkan_display_opts),
+ .defaults = &(struct vulkan_display_opts) {0},
+};
+
+struct priv {
+ struct mpvk_ctx vk;
+ struct vulkan_display_opts *opts;
+ uint32_t width;
+ uint32_t height;
+
+#if HAVE_DRM
+ struct mpv_opengl_drm_params_v2 drm_params;
+#endif
+};
+
+#if HAVE_DRM
+static void open_render_fd(struct ra_ctx *ctx, const char *render_path)
+{
+ struct priv *p = ctx->priv;
+ p->drm_params.fd = -1;
+ p->drm_params.render_fd = open(render_path, O_RDWR | O_CLOEXEC);
+ if (p->drm_params.render_fd == -1) {
+ MP_WARN(ctx, "Failed to open render node: %s\n",
+ strerror(errno));
+ }
+}
+
+static bool drm_setup(struct ra_ctx *ctx, int display_idx,
+ VkPhysicalDevicePCIBusInfoPropertiesEXT *pci_props)
+{
+ drmDevice *devs[32] = {};
+ int count = drmGetDevices2(0, devs, MP_ARRAY_SIZE(devs));
+ for (int i = 0; i < count; i++) {
+ drmDevice *dev = devs[i];
+
+ if (dev->bustype != DRM_BUS_PCI ||
+ dev->businfo.pci->domain != pci_props->pciDomain ||
+ dev->businfo.pci->bus != pci_props->pciBus ||
+ dev->businfo.pci->dev != pci_props->pciDevice ||
+ dev->businfo.pci->func != pci_props->pciFunction)
+ {
+ continue;
+ }
+
+ // Found our matching device.
+ MP_DBG(ctx, "DRM device found for Vulkan device at %04X:%02X:%02X:%02X\n",
+ pci_props->pciDomain, pci_props->pciBus,
+ pci_props->pciDevice, pci_props->pciFunction);
+
+ if (!(dev->available_nodes & 1 << DRM_NODE_RENDER)) {
+ MP_DBG(ctx, "Card does not have a render node.\n");
+ continue;
+ }
+
+ open_render_fd(ctx, dev->nodes[DRM_NODE_RENDER]);
+
+ break;
+ }
+ drmFreeDevices(devs, MP_ARRAY_SIZE(devs));
+
+ struct priv *p = ctx->priv;
+ if (p->drm_params.render_fd == -1) {
+ MP_WARN(ctx, "Couldn't open DRM render node for Vulkan device "
+ "at: %04X:%02X:%02X:%02X\n",
+ pci_props->pciDomain, pci_props->pciBus,
+ pci_props->pciDevice, pci_props->pciFunction);
+ return false;
+ }
+
+ return true;
+}
+#endif
+
+static void display_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_vk_ctx_uninit(ctx);
+ mpvk_uninit(&p->vk);
+
+#if HAVE_DRM
+ if (p->drm_params.render_fd != -1) {
+ close(p->drm_params.render_fd);
+ p->drm_params.render_fd = -1;
+ }
+#endif
+}
+
+static bool display_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct mpvk_ctx *vk = &p->vk;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+ VkResult res;
+ bool ret = false;
+
+ VkDisplayModePropertiesKHR *mode = NULL;
+
+ p->opts = mp_get_config_group(p, ctx->global, &vulkan_display_conf);
+ int display_idx = p->opts->display;
+ int mode_idx = p->opts->mode;
+ int plane_idx = p->opts->plane;
+
+ if (!mpvk_init(vk, ctx, VK_KHR_DISPLAY_EXTENSION_NAME))
+ goto error;
+
+ char *device_name = ra_vk_ctx_get_device_name(ctx);
+ struct pl_vulkan_device_params vulkan_params = {
+ .instance = vk->vkinst->instance,
+ .device_name = device_name,
+ };
+ VkPhysicalDevice device = pl_vulkan_choose_device(vk->pllog, &vulkan_params);
+ talloc_free(device_name);
+ if (!device) {
+ MP_MSG(ctx, msgl, "Failed to open physical device.\n");
+ goto error;
+ }
+
+#if HAVE_DRM
+ VkPhysicalDevicePCIBusInfoPropertiesEXT pci_props = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PCI_BUS_INFO_PROPERTIES_EXT,
+ };
+ VkPhysicalDeviceProperties2KHR props = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2_KHR,
+ .pNext = &pci_props,
+ };
+ vkGetPhysicalDeviceProperties2(device, &props);
+
+ if (!drm_setup(ctx, display_idx, &pci_props))
+ MP_WARN(ctx, "Failed to set up DRM.\n");
+#endif
+
+ struct mode_selector selector = {
+ .display_idx = display_idx,
+ .mode_idx = mode_idx,
+ .plane_idx = plane_idx,
+
+ };
+ if (!walk_display_properties(ctx->log, msgl, device, &selector))
+ goto error;
+ mode = selector.out_mode_props;
+
+ VkDisplaySurfaceCreateInfoKHR xinfo = {
+ .sType = VK_STRUCTURE_TYPE_DISPLAY_SURFACE_CREATE_INFO_KHR,
+ .displayMode = mode->displayMode,
+ .imageExtent = mode->parameters.visibleRegion,
+ .planeIndex = plane_idx,
+ .transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR,
+ .alphaMode = VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR,
+ };
+
+ res = vkCreateDisplayPlaneSurfaceKHR(vk->vkinst->instance, &xinfo, NULL,
+ &vk->surface);
+ if (res != VK_SUCCESS) {
+ MP_MSG(ctx, msgl, "Failed creating Display surface\n");
+ goto error;
+ }
+
+ p->width = mode->parameters.visibleRegion.width;
+ p->height = mode->parameters.visibleRegion.height;
+
+ struct ra_vk_ctx_params params = {0};
+ if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_FIFO_KHR))
+ goto error;
+
+#if HAVE_DRM
+ if (p->drm_params.render_fd > -1) {
+ ra_add_native_resource(ctx->ra, "drm_params_v2", &p->drm_params);
+ } else {
+ MP_WARN(ctx,
+ "No DRM render fd available. VAAPI hwaccel will not be usable.\n");
+ }
+#endif
+
+ ret = true;
+
+done:
+ talloc_free(mode);
+ return ret;
+
+error:
+ display_uninit(ctx);
+ goto done;
+}
+
+static bool display_reconfig(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ return ra_vk_ctx_resize(ctx, p->width, p->height);
+}
+
+static int display_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ return VO_NOTIMPL;
+}
+
+static void display_wakeup(struct ra_ctx *ctx)
+{
+ // TODO
+}
+
+static void display_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ // TODO
+}
+
+const struct ra_ctx_fns ra_ctx_vulkan_display = {
+ .type = "vulkan",
+ .name = "displayvk",
+ .reconfig = display_reconfig,
+ .control = display_control,
+ .wakeup = display_wakeup,
+ .wait_events = display_wait_events,
+ .init = display_init,
+ .uninit = display_uninit,
+};
diff --git a/video/out/vulkan/context_mac.m b/video/out/vulkan/context_mac.m
new file mode 100644
index 0000000..8ac6e16
--- /dev/null
+++ b/video/out/vulkan/context_mac.m
@@ -0,0 +1,119 @@
+/*
+ * 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 "video/out/gpu/context.h"
+#include "osdep/macOS_swift.h"
+
+#include "common.h"
+#include "context.h"
+#include "utils.h"
+
+struct priv {
+ struct mpvk_ctx vk;
+ MacCommon *vo_mac;
+};
+
+static void mac_vk_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_vk_ctx_uninit(ctx);
+ mpvk_uninit(&p->vk);
+ [p->vo_mac uninit:ctx->vo];
+}
+
+static void mac_vk_swap_buffers(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ [p->vo_mac swapBuffer];
+}
+
+static bool mac_vk_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct mpvk_ctx *vk = &p->vk;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+
+ if (!mpvk_init(vk, ctx, VK_EXT_METAL_SURFACE_EXTENSION_NAME))
+ goto error;
+
+ p->vo_mac = [[MacCommon alloc] init:ctx->vo];
+ if (!p->vo_mac)
+ goto error;
+
+ VkMetalSurfaceCreateInfoEXT mac_info = {
+ .sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK,
+ .pNext = NULL,
+ .flags = 0,
+ .pLayer = p->vo_mac.layer,
+ };
+
+ struct ra_vk_ctx_params params = {
+ .swap_buffers = mac_vk_swap_buffers,
+ };
+
+ VkInstance inst = vk->vkinst->instance;
+ VkResult res = vkCreateMetalSurfaceEXT(inst, &mac_info, NULL, &vk->surface);
+ if (res != VK_SUCCESS) {
+ MP_MSG(ctx, msgl, "Failed creating metal surface\n");
+ goto error;
+ }
+
+ if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_FIFO_KHR))
+ goto error;
+
+ return true;
+error:
+ if (p->vo_mac)
+ [p->vo_mac uninit:ctx->vo];
+ return false;
+}
+
+static bool resize(struct ra_ctx *ctx)
+{
+ return ra_vk_ctx_resize(ctx, ctx->vo->dwidth, ctx->vo->dheight);
+}
+
+static bool mac_vk_reconfig(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+ if (![p->vo_mac config:ctx->vo])
+ return false;
+ return true;
+}
+
+static int mac_vk_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ struct priv *p = ctx->priv;
+ int ret = [p->vo_mac control:ctx->vo events:events request:request data:arg];
+
+ if (*events & VO_EVENT_RESIZE) {
+ if (!resize(ctx))
+ return VO_ERROR;
+ }
+
+ return ret;
+}
+
+const struct ra_ctx_fns ra_ctx_vulkan_mac = {
+ .type = "vulkan",
+ .name = "macvk",
+ .reconfig = mac_vk_reconfig,
+ .control = mac_vk_control,
+ .init = mac_vk_init,
+ .uninit = mac_vk_uninit,
+};
diff --git a/video/out/vulkan/context_wayland.c b/video/out/vulkan/context_wayland.c
new file mode 100644
index 0000000..761ff5b
--- /dev/null
+++ b/video/out/vulkan/context_wayland.c
@@ -0,0 +1,167 @@
+/*
+ * 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 "video/out/gpu/context.h"
+#include "video/out/present_sync.h"
+#include "video/out/wayland_common.h"
+
+#include "common.h"
+#include "context.h"
+#include "utils.h"
+
+struct priv {
+ struct mpvk_ctx vk;
+};
+
+static bool wayland_vk_check_visible(struct ra_ctx *ctx)
+{
+ return vo_wayland_check_visible(ctx->vo);
+}
+
+static void wayland_vk_swap_buffers(struct ra_ctx *ctx)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+
+ if (!wl->opts->disable_vsync)
+ vo_wayland_wait_frame(wl);
+
+ if (wl->use_present)
+ present_sync_swap(wl->present);
+}
+
+static void wayland_vk_get_vsync(struct ra_ctx *ctx, struct vo_vsync_info *info)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+ if (wl->use_present)
+ present_sync_get_info(wl->present, info);
+}
+
+static void wayland_vk_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_vk_ctx_uninit(ctx);
+ mpvk_uninit(&p->vk);
+ vo_wayland_uninit(ctx->vo);
+}
+
+static bool wayland_vk_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct mpvk_ctx *vk = &p->vk;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+
+ if (!mpvk_init(vk, ctx, VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME))
+ goto error;
+
+ if (!vo_wayland_init(ctx->vo))
+ goto error;
+
+ VkWaylandSurfaceCreateInfoKHR wlinfo = {
+ .sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR,
+ .display = ctx->vo->wl->display,
+ .surface = ctx->vo->wl->surface,
+ };
+
+ struct ra_vk_ctx_params params = {
+ .check_visible = wayland_vk_check_visible,
+ .swap_buffers = wayland_vk_swap_buffers,
+ .get_vsync = wayland_vk_get_vsync,
+ };
+
+ VkInstance inst = vk->vkinst->instance;
+ VkResult res = vkCreateWaylandSurfaceKHR(inst, &wlinfo, NULL, &vk->surface);
+ if (res != VK_SUCCESS) {
+ MP_MSG(ctx, msgl, "Failed creating Wayland surface\n");
+ goto error;
+ }
+
+ /* Because in Wayland clients render whenever they receive a callback from
+ * the compositor, and the fact that the compositor usually stops sending
+ * callbacks once the surface is no longer visible, using FIFO here would
+ * mean the entire player would block on acquiring swapchain images. Hence,
+ * use MAILBOX to guarantee that there'll always be a swapchain image and
+ * the player won't block waiting on those */
+ if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_MAILBOX_KHR))
+ goto error;
+
+ ra_add_native_resource(ctx->ra, "wl", ctx->vo->wl->display);
+
+ return true;
+
+error:
+ wayland_vk_uninit(ctx);
+ return false;
+}
+
+static bool resize(struct ra_ctx *ctx)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+
+ MP_VERBOSE(wl, "Handling resize on the vk side\n");
+
+ const int32_t width = mp_rect_w(wl->geometry);
+ const int32_t height = mp_rect_h(wl->geometry);
+
+ vo_wayland_set_opaque_region(wl, ctx->opts.want_alpha);
+ vo_wayland_handle_fractional_scale(wl);
+ return ra_vk_ctx_resize(ctx, width, height);
+}
+
+static bool wayland_vk_reconfig(struct ra_ctx *ctx)
+{
+ return vo_wayland_reconfig(ctx->vo);
+}
+
+static int wayland_vk_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ int ret = vo_wayland_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE) {
+ if (!resize(ctx))
+ return VO_ERROR;
+ }
+ return ret;
+}
+
+static void wayland_vk_wakeup(struct ra_ctx *ctx)
+{
+ vo_wayland_wakeup(ctx->vo);
+}
+
+static void wayland_vk_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ vo_wayland_wait_events(ctx->vo, until_time_ns);
+}
+
+static void wayland_vk_update_render_opts(struct ra_ctx *ctx)
+{
+ struct vo_wayland_state *wl = ctx->vo->wl;
+ vo_wayland_set_opaque_region(wl, ctx->opts.want_alpha);
+ wl_surface_commit(wl->surface);
+}
+
+const struct ra_ctx_fns ra_ctx_vulkan_wayland = {
+ .type = "vulkan",
+ .name = "waylandvk",
+ .reconfig = wayland_vk_reconfig,
+ .control = wayland_vk_control,
+ .wakeup = wayland_vk_wakeup,
+ .wait_events = wayland_vk_wait_events,
+ .update_render_opts = wayland_vk_update_render_opts,
+ .init = wayland_vk_init,
+ .uninit = wayland_vk_uninit,
+};
diff --git a/video/out/vulkan/context_win.c b/video/out/vulkan/context_win.c
new file mode 100644
index 0000000..a89c644
--- /dev/null
+++ b/video/out/vulkan/context_win.c
@@ -0,0 +1,106 @@
+/*
+ * 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 "video/out/gpu/context.h"
+#include "video/out/w32_common.h"
+
+#include "common.h"
+#include "context.h"
+#include "utils.h"
+
+EXTERN_C IMAGE_DOS_HEADER __ImageBase;
+#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase)
+
+struct priv {
+ struct mpvk_ctx vk;
+};
+
+static void win_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_vk_ctx_uninit(ctx);
+ mpvk_uninit(&p->vk);
+ vo_w32_uninit(ctx->vo);
+}
+
+static bool win_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct mpvk_ctx *vk = &p->vk;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+
+ if (!mpvk_init(vk, ctx, VK_KHR_WIN32_SURFACE_EXTENSION_NAME))
+ goto error;
+
+ if (!vo_w32_init(ctx->vo))
+ goto error;
+
+ VkWin32SurfaceCreateInfoKHR wininfo = {
+ .sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR,
+ .hinstance = HINST_THISCOMPONENT,
+ .hwnd = vo_w32_hwnd(ctx->vo),
+ };
+
+ struct ra_vk_ctx_params params = {0};
+
+ VkInstance inst = vk->vkinst->instance;
+ VkResult res = vkCreateWin32SurfaceKHR(inst, &wininfo, NULL, &vk->surface);
+ if (res != VK_SUCCESS) {
+ MP_MSG(ctx, msgl, "Failed creating Windows surface\n");
+ goto error;
+ }
+
+ if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_FIFO_KHR))
+ goto error;
+
+ return true;
+
+error:
+ win_uninit(ctx);
+ return false;
+}
+
+static bool resize(struct ra_ctx *ctx)
+{
+ return ra_vk_ctx_resize(ctx, ctx->vo->dwidth, ctx->vo->dheight);
+}
+
+static bool win_reconfig(struct ra_ctx *ctx)
+{
+ vo_w32_config(ctx->vo);
+ return resize(ctx);
+}
+
+static int win_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ int ret = vo_w32_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE) {
+ if (!resize(ctx))
+ return VO_ERROR;
+ }
+ return ret;
+}
+
+const struct ra_ctx_fns ra_ctx_vulkan_win = {
+ .type = "vulkan",
+ .name = "winvk",
+ .reconfig = win_reconfig,
+ .control = win_control,
+ .init = win_init,
+ .uninit = win_uninit,
+};
diff --git a/video/out/vulkan/context_xlib.c b/video/out/vulkan/context_xlib.c
new file mode 100644
index 0000000..673dc31
--- /dev/null
+++ b/video/out/vulkan/context_xlib.c
@@ -0,0 +1,143 @@
+/*
+ * 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 "video/out/gpu/context.h"
+#include "video/out/present_sync.h"
+#include "video/out/x11_common.h"
+
+#include "common.h"
+#include "context.h"
+#include "utils.h"
+
+struct priv {
+ struct mpvk_ctx vk;
+};
+
+static bool xlib_check_visible(struct ra_ctx *ctx)
+{
+ return vo_x11_check_visible(ctx->vo);
+}
+
+static void xlib_vk_swap_buffers(struct ra_ctx *ctx)
+{
+ if (ctx->vo->x11->use_present)
+ present_sync_swap(ctx->vo->x11->present);
+}
+
+static void xlib_vk_get_vsync(struct ra_ctx *ctx, struct vo_vsync_info *info)
+{
+ struct vo_x11_state *x11 = ctx->vo->x11;
+ if (ctx->vo->x11->use_present)
+ present_sync_get_info(x11->present, info);
+}
+
+static void xlib_uninit(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ ra_vk_ctx_uninit(ctx);
+ mpvk_uninit(&p->vk);
+ vo_x11_uninit(ctx->vo);
+}
+
+static bool xlib_init(struct ra_ctx *ctx)
+{
+ struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
+ struct mpvk_ctx *vk = &p->vk;
+ int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
+
+ if (!mpvk_init(vk, ctx, VK_KHR_XLIB_SURFACE_EXTENSION_NAME))
+ goto error;
+
+ if (!vo_x11_init(ctx->vo))
+ goto error;
+
+ if (!vo_x11_create_vo_window(ctx->vo, NULL, "mpvk"))
+ goto error;
+
+ VkXlibSurfaceCreateInfoKHR xinfo = {
+ .sType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR,
+ .dpy = ctx->vo->x11->display,
+ .window = ctx->vo->x11->window,
+ };
+
+ struct ra_vk_ctx_params params = {
+ .check_visible = xlib_check_visible,
+ .swap_buffers = xlib_vk_swap_buffers,
+ .get_vsync = xlib_vk_get_vsync,
+ };
+
+ VkInstance inst = vk->vkinst->instance;
+ VkResult res = vkCreateXlibSurfaceKHR(inst, &xinfo, NULL, &vk->surface);
+ if (res != VK_SUCCESS) {
+ MP_MSG(ctx, msgl, "Failed creating Xlib surface\n");
+ goto error;
+ }
+
+ if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_FIFO_KHR))
+ goto error;
+
+ ra_add_native_resource(ctx->ra, "x11", ctx->vo->x11->display);
+
+ return true;
+
+error:
+ xlib_uninit(ctx);
+ return false;
+}
+
+static bool resize(struct ra_ctx *ctx)
+{
+ return ra_vk_ctx_resize(ctx, ctx->vo->dwidth, ctx->vo->dheight);
+}
+
+static bool xlib_reconfig(struct ra_ctx *ctx)
+{
+ vo_x11_config_vo_window(ctx->vo);
+ return resize(ctx);
+}
+
+static int xlib_control(struct ra_ctx *ctx, int *events, int request, void *arg)
+{
+ int ret = vo_x11_control(ctx->vo, events, request, arg);
+ if (*events & VO_EVENT_RESIZE) {
+ if (!resize(ctx))
+ return VO_ERROR;
+ }
+ return ret;
+}
+
+static void xlib_wakeup(struct ra_ctx *ctx)
+{
+ vo_x11_wakeup(ctx->vo);
+}
+
+static void xlib_wait_events(struct ra_ctx *ctx, int64_t until_time_ns)
+{
+ vo_x11_wait_events(ctx->vo, until_time_ns);
+}
+
+const struct ra_ctx_fns ra_ctx_vulkan_xlib = {
+ .type = "vulkan",
+ .name = "x11vk",
+ .reconfig = xlib_reconfig,
+ .control = xlib_control,
+ .wakeup = xlib_wakeup,
+ .wait_events = xlib_wait_events,
+ .init = xlib_init,
+ .uninit = xlib_uninit,
+};
diff --git a/video/out/vulkan/utils.c b/video/out/vulkan/utils.c
new file mode 100644
index 0000000..57a3664
--- /dev/null
+++ b/video/out/vulkan/utils.c
@@ -0,0 +1,42 @@
+#include "video/out/placebo/utils.h"
+#include "utils.h"
+
+bool mpvk_init(struct mpvk_ctx *vk, struct ra_ctx *ctx, const char *surface_ext)
+{
+ vk->pllog = mppl_log_create(ctx, ctx->vo->log);
+ if (!vk->pllog)
+ goto error;
+
+ const char *exts[] = {
+ VK_KHR_SURFACE_EXTENSION_NAME,
+ surface_ext,
+ };
+
+ mppl_log_set_probing(vk->pllog, true);
+ vk->vkinst = pl_vk_inst_create(vk->pllog, &(struct pl_vk_inst_params) {
+ .debug = ctx->opts.debug,
+ .extensions = exts,
+ .num_extensions = MP_ARRAY_SIZE(exts),
+ });
+ mppl_log_set_probing(vk->pllog, false);
+ if (!vk->vkinst)
+ goto error;
+
+ return true;
+
+error:
+ mpvk_uninit(vk);
+ return false;
+}
+
+void mpvk_uninit(struct mpvk_ctx *vk)
+{
+ if (vk->surface) {
+ assert(vk->vkinst);
+ vkDestroySurfaceKHR(vk->vkinst->instance, vk->surface, NULL);
+ vk->surface = VK_NULL_HANDLE;
+ }
+
+ pl_vk_inst_destroy(&vk->vkinst);
+ pl_log_destroy(&vk->pllog);
+}
diff --git a/video/out/vulkan/utils.h b/video/out/vulkan/utils.h
new file mode 100644
index 0000000..a98e147
--- /dev/null
+++ b/video/out/vulkan/utils.h
@@ -0,0 +1,6 @@
+#pragma once
+#include "common.h"
+#include "video/out/gpu/context.h"
+
+bool mpvk_init(struct mpvk_ctx *vk, struct ra_ctx *ctx, const char *surface_ext);
+void mpvk_uninit(struct mpvk_ctx *vk);
diff --git a/video/out/w32_common.c b/video/out/w32_common.c
new file mode 100644
index 0000000..e6a4670
--- /dev/null
+++ b/video/out/w32_common.c
@@ -0,0 +1,2144 @@
+/*
+ * 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 <assert.h>
+#include <limits.h>
+#include <stdatomic.h>
+#include <stdio.h>
+
+#include <windows.h>
+#include <windowsx.h>
+#include <dwmapi.h>
+#include <ole2.h>
+#include <shobjidl.h>
+#include <avrt.h>
+
+#include "options/m_config.h"
+#include "options/options.h"
+#include "input/keycodes.h"
+#include "input/input.h"
+#include "input/event.h"
+#include "stream/stream.h"
+#include "common/msg.h"
+#include "common/common.h"
+#include "vo.h"
+#include "win_state.h"
+#include "w32_common.h"
+#include "win32/displayconfig.h"
+#include "win32/droptarget.h"
+#include "osdep/io.h"
+#include "osdep/threads.h"
+#include "osdep/w32_keyboard.h"
+#include "misc/dispatch.h"
+#include "misc/rendezvous.h"
+#include "mpv_talloc.h"
+
+EXTERN_C IMAGE_DOS_HEADER __ImageBase;
+#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase)
+
+#ifndef WM_DPICHANGED
+#define WM_DPICHANGED (0x02E0)
+#endif
+
+#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
+#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
+#endif
+
+
+//Older MinGW compatibility
+#define DWMWA_WINDOW_CORNER_PREFERENCE 33
+#define DWMWA_SYSTEMBACKDROP_TYPE 38
+
+#ifndef DPI_ENUMS_DECLARED
+typedef enum MONITOR_DPI_TYPE {
+ MDT_EFFECTIVE_DPI = 0,
+ MDT_ANGULAR_DPI = 1,
+ MDT_RAW_DPI = 2,
+ MDT_DEFAULT = MDT_EFFECTIVE_DPI
+} MONITOR_DPI_TYPE;
+#endif
+
+#define rect_w(r) ((r).right - (r).left)
+#define rect_h(r) ((r).bottom - (r).top)
+
+struct w32_api {
+ HRESULT (WINAPI *pGetDpiForMonitor)(HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*);
+ BOOL (WINAPI *pImmDisableIME)(DWORD);
+ BOOL (WINAPI *pAdjustWindowRectExForDpi)(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi);
+ BOOLEAN (WINAPI *pShouldAppsUseDarkMode)(void);
+ DWORD (WINAPI *pSetPreferredAppMode)(DWORD mode);
+};
+
+struct vo_w32_state {
+ struct mp_log *log;
+ struct vo *vo;
+ struct mp_vo_opts *opts;
+ struct m_config_cache *opts_cache;
+ struct input_ctx *input_ctx;
+
+ mp_thread thread;
+ bool terminate;
+ struct mp_dispatch_queue *dispatch; // used to run stuff on the GUI thread
+ bool in_dispatch;
+
+ struct w32_api api; // stores functions from dynamically loaded DLLs
+
+ HWND window;
+ HWND parent; // 0 normally, set in embedding mode
+ HHOOK parent_win_hook;
+ HWINEVENTHOOK parent_evt_hook;
+
+ HMONITOR monitor; // Handle of the current screen
+ char *color_profile; // Path of the current screen's color profile
+
+ // Has the window seen a WM_DESTROY? If so, don't call DestroyWindow again.
+ bool destroyed;
+
+ bool focused;
+
+ // whether the window position and size were initialized
+ bool window_bounds_initialized;
+
+ bool current_fs;
+ bool toggle_fs; // whether the current fullscreen state needs to be switched
+
+ // Note: maximized state doesn't involve nor modify windowrc
+ RECT windowrc; // currently known normal/fullscreen window client rect
+ RECT prev_windowrc; // saved normal window client rect while in fullscreen
+
+ // video size
+ uint32_t o_dwidth;
+ uint32_t o_dheight;
+
+ int dpi;
+ double dpi_scale;
+
+ bool disable_screensaver;
+ bool cursor_visible;
+ atomic_uint event_flags;
+
+ BOOL tracking;
+ TRACKMOUSEEVENT trackEvent;
+
+ int mouse_x;
+ int mouse_y;
+
+ // Should SetCursor be called when handling VOCTRL_SET_CURSOR_VISIBILITY?
+ bool can_set_cursor;
+
+ // UTF-16 decoding state for WM_CHAR and VK_PACKET
+ int high_surrogate;
+
+ // Fit the window to one monitor working area next time it's not fullscreen
+ // and not maximized. Used once after every new "untrusted" size comes from
+ // mpv, else we assume that the last known size is valid and don't fit.
+ // FIXME: on a multi-monitor setup one bit is not enough, because the first
+ // fit (autofit etc) should be to one monitor, but later size changes from
+ // mpv like window-scale (VOCTRL_SET_UNFS_WINDOW_SIZE) should allow the
+ // entire virtual desktop area - but we still limit to one monitor size.
+ bool fit_on_screen;
+
+ bool win_force_pos;
+
+ ITaskbarList2 *taskbar_list;
+ ITaskbarList3 *taskbar_list3;
+ UINT tbtnCreatedMsg;
+ bool tbtnCreated;
+
+ struct voctrl_playback_state current_pstate;
+
+ // updates on move/resize/displaychange
+ double display_fps;
+
+ bool moving;
+
+ union {
+ uint8_t snapped;
+ struct {
+ uint8_t snapped_left : 1;
+ uint8_t snapped_right : 1;
+ uint8_t snapped_top : 1;
+ uint8_t snapped_bottom : 1;
+ };
+ };
+ int snap_dx;
+ int snap_dy;
+
+ HANDLE avrt_handle;
+
+ bool cleared;
+};
+
+static void adjust_window_rect(struct vo_w32_state *w32, HWND hwnd, RECT *rc)
+{
+ if (!w32->opts->border)
+ return;
+
+ if (w32->api.pAdjustWindowRectExForDpi) {
+ w32->api.pAdjustWindowRectExForDpi(rc,
+ GetWindowLongPtrW(hwnd, GWL_STYLE), 0,
+ GetWindowLongPtrW(hwnd, GWL_EXSTYLE), w32->dpi);
+ } else {
+ AdjustWindowRect(rc, GetWindowLongPtrW(hwnd, GWL_STYLE), 0);
+ }
+}
+
+static void add_window_borders(struct vo_w32_state *w32, HWND hwnd, RECT *rc)
+{
+ RECT win = *rc;
+ adjust_window_rect(w32, hwnd, rc);
+ // Adjust for title bar height that will be hidden in WM_NCCALCSIZE
+ if (w32->opts->border && !w32->opts->title_bar && !w32->current_fs)
+ rc->top -= rc->top - win.top;
+}
+
+// basically a reverse AdjustWindowRect (win32 doesn't appear to have this)
+static void subtract_window_borders(struct vo_w32_state *w32, HWND hwnd, RECT *rc)
+{
+ RECT b = { 0, 0, 0, 0 };
+ add_window_borders(w32, hwnd, &b);
+ rc->left -= b.left;
+ rc->top -= b.top;
+ rc->right -= b.right;
+ rc->bottom -= b.bottom;
+}
+
+static LRESULT borderless_nchittest(struct vo_w32_state *w32, int x, int y)
+{
+ if (IsMaximized(w32->window))
+ return HTCLIENT;
+
+ RECT rc;
+ if (!GetWindowRect(w32->window, &rc))
+ return HTNOWHERE;
+
+ POINT frame = {GetSystemMetrics(SM_CXSIZEFRAME),
+ GetSystemMetrics(SM_CYSIZEFRAME)};
+ if (w32->opts->border) {
+ frame.x += GetSystemMetrics(SM_CXPADDEDBORDER);
+ frame.y += GetSystemMetrics(SM_CXPADDEDBORDER);
+ if (!w32->opts->title_bar)
+ rc.top -= GetSystemMetrics(SM_CXPADDEDBORDER);
+ }
+ InflateRect(&rc, -frame.x, -frame.y);
+
+ // Hit-test top border
+ if (y < rc.top) {
+ if (x < rc.left)
+ return HTTOPLEFT;
+ if (x > rc.right)
+ return HTTOPRIGHT;
+ return HTTOP;
+ }
+
+ // Hit-test bottom border
+ if (y > rc.bottom) {
+ if (x < rc.left)
+ return HTBOTTOMLEFT;
+ if (x > rc.right)
+ return HTBOTTOMRIGHT;
+ return HTBOTTOM;
+ }
+
+ // Hit-test side borders
+ if (x < rc.left)
+ return HTLEFT;
+ if (x > rc.right)
+ return HTRIGHT;
+ return HTCLIENT;
+}
+
+// turn a WMSZ_* input value in v into the border that should be resized
+// take into consideration which borders are snapped to avoid detaching
+// returns: 0=left, 1=top, 2=right, 3=bottom, -1=undefined
+static int get_resize_border(struct vo_w32_state *w32, int v)
+{
+ switch (v) {
+ case WMSZ_LEFT:
+ case WMSZ_RIGHT:
+ return w32->snapped_bottom ? 1 : 3;
+ case WMSZ_TOP:
+ case WMSZ_BOTTOM:
+ return w32->snapped_right ? 0 : 2;
+ case WMSZ_TOPLEFT: return 1;
+ case WMSZ_TOPRIGHT: return 1;
+ case WMSZ_BOTTOMLEFT: return 3;
+ case WMSZ_BOTTOMRIGHT: return 3;
+ default: return -1;
+ }
+}
+
+static bool key_state(int vk)
+{
+ return GetKeyState(vk) & 0x8000;
+}
+
+static int mod_state(struct vo_w32_state *w32)
+{
+ int res = 0;
+
+ // AltGr is represented as LCONTROL+RMENU on Windows
+ bool alt_gr = mp_input_use_alt_gr(w32->input_ctx) &&
+ key_state(VK_RMENU) && key_state(VK_LCONTROL);
+
+ if (key_state(VK_RCONTROL) || (key_state(VK_LCONTROL) && !alt_gr))
+ res |= MP_KEY_MODIFIER_CTRL;
+ if (key_state(VK_SHIFT))
+ res |= MP_KEY_MODIFIER_SHIFT;
+ if (key_state(VK_LMENU) || (key_state(VK_RMENU) && !alt_gr))
+ res |= MP_KEY_MODIFIER_ALT;
+ return res;
+}
+
+static int decode_surrogate_pair(wchar_t lead, wchar_t trail)
+{
+ return 0x10000 + (((lead & 0x3ff) << 10) | (trail & 0x3ff));
+}
+
+static int decode_utf16(struct vo_w32_state *w32, wchar_t c)
+{
+ // Decode UTF-16, keeping state in w32->high_surrogate
+ if (IS_HIGH_SURROGATE(c)) {
+ w32->high_surrogate = c;
+ return 0;
+ }
+ if (IS_LOW_SURROGATE(c)) {
+ if (!w32->high_surrogate) {
+ MP_ERR(w32, "Invalid UTF-16 input\n");
+ return 0;
+ }
+ int codepoint = decode_surrogate_pair(w32->high_surrogate, c);
+ w32->high_surrogate = 0;
+ return codepoint;
+ }
+ if (w32->high_surrogate != 0) {
+ w32->high_surrogate = 0;
+ MP_ERR(w32, "Invalid UTF-16 input\n");
+ return 0;
+ }
+
+ return c;
+}
+
+static void clear_keyboard_buffer(void)
+{
+ static const UINT vkey = VK_DECIMAL;
+ static const BYTE keys[256] = { 0 };
+ UINT scancode = MapVirtualKey(vkey, MAPVK_VK_TO_VSC);
+ wchar_t buf[10];
+ int ret = 0;
+
+ // Use the method suggested by Michael Kaplan to clear any pending dead
+ // keys from the current keyboard layout. See:
+ // https://web.archive.org/web/20101004154432/http://blogs.msdn.com/b/michkap/archive/2006/04/06/569632.aspx
+ // https://web.archive.org/web/20100820152419/http://blogs.msdn.com/b/michkap/archive/2007/10/27/5717859.aspx
+ do {
+ ret = ToUnicode(vkey, scancode, keys, buf, MP_ARRAY_SIZE(buf), 0);
+ } while (ret < 0);
+}
+
+static int to_unicode(UINT vkey, UINT scancode, const BYTE keys[256])
+{
+ // This wraps ToUnicode to be stateless and to return only one character
+
+ // Make the buffer 10 code units long to be safe, same as here:
+ // https://web.archive.org/web/20101013215215/http://blogs.msdn.com/b/michkap/archive/2006/03/24/559169.aspx
+ wchar_t buf[10] = { 0 };
+
+ // Dead keys aren't useful for key shortcuts, so clear the keyboard state
+ clear_keyboard_buffer();
+
+ int len = ToUnicode(vkey, scancode, keys, buf, MP_ARRAY_SIZE(buf), 0);
+
+ // Return the last complete UTF-16 code point. A negative return value
+ // indicates a dead key, however there should still be a non-combining
+ // version of the key in the buffer.
+ if (len < 0)
+ len = -len;
+ if (len >= 2 && IS_SURROGATE_PAIR(buf[len - 2], buf[len - 1]))
+ return decode_surrogate_pair(buf[len - 2], buf[len - 1]);
+ if (len >= 1)
+ return buf[len - 1];
+
+ return 0;
+}
+
+static int decode_key(struct vo_w32_state *w32, UINT vkey, UINT scancode)
+{
+ BYTE keys[256];
+ GetKeyboardState(keys);
+
+ // If mp_input_use_alt_gr is false, detect and remove AltGr so normal
+ // characters are generated. Note that AltGr is represented as
+ // LCONTROL+RMENU on Windows.
+ if ((keys[VK_RMENU] & 0x80) && (keys[VK_LCONTROL] & 0x80) &&
+ !mp_input_use_alt_gr(w32->input_ctx))
+ {
+ keys[VK_RMENU] = keys[VK_LCONTROL] = 0;
+ keys[VK_MENU] = keys[VK_LMENU];
+ keys[VK_CONTROL] = keys[VK_RCONTROL];
+ }
+
+ int c = to_unicode(vkey, scancode, keys);
+
+ // Some shift states prevent ToUnicode from working or cause it to produce
+ // control characters. If this is detected, remove modifiers until it
+ // starts producing normal characters.
+ if (c < 0x20 && (keys[VK_MENU] & 0x80)) {
+ keys[VK_LMENU] = keys[VK_RMENU] = keys[VK_MENU] = 0;
+ c = to_unicode(vkey, scancode, keys);
+ }
+ if (c < 0x20 && (keys[VK_CONTROL] & 0x80)) {
+ keys[VK_LCONTROL] = keys[VK_RCONTROL] = keys[VK_CONTROL] = 0;
+ c = to_unicode(vkey, scancode, keys);
+ }
+ if (c < 0x20)
+ return 0;
+
+ // Decode lone UTF-16 surrogates (VK_PACKET can generate these)
+ if (c < 0x10000)
+ return decode_utf16(w32, c);
+ return c;
+}
+
+static bool handle_appcommand(struct vo_w32_state *w32, UINT cmd)
+{
+ if (!mp_input_use_media_keys(w32->input_ctx))
+ return false;
+ int mpkey = mp_w32_appcmd_to_mpkey(cmd);
+ if (!mpkey)
+ return false;
+ mp_input_put_key(w32->input_ctx, mpkey | mod_state(w32));
+ return true;
+}
+
+static void handle_key_down(struct vo_w32_state *w32, UINT vkey, UINT scancode)
+{
+ // Ignore key repeat
+ if (scancode & KF_REPEAT)
+ return;
+
+ int mpkey = mp_w32_vkey_to_mpkey(vkey, scancode & KF_EXTENDED);
+ if (!mpkey) {
+ mpkey = decode_key(w32, vkey, scancode & (0xff | KF_EXTENDED));
+ if (!mpkey)
+ return;
+ }
+
+ mp_input_put_key(w32->input_ctx, mpkey | mod_state(w32) | MP_KEY_STATE_DOWN);
+}
+
+static void handle_key_up(struct vo_w32_state *w32, UINT vkey, UINT scancode)
+{
+ switch (vkey) {
+ case VK_MENU:
+ case VK_CONTROL:
+ case VK_SHIFT:
+ break;
+ default:
+ // Releasing all keys on key-up is simpler and ensures no keys can be
+ // get "stuck." This matches the behaviour of other VOs.
+ mp_input_put_key(w32->input_ctx, MP_INPUT_RELEASE_ALL);
+ }
+}
+
+static bool handle_char(struct vo_w32_state *w32, wchar_t wc)
+{
+ int c = decode_utf16(w32, wc);
+
+ if (c == 0)
+ return true;
+ if (c < 0x20)
+ return false;
+
+ mp_input_put_key(w32->input_ctx, c | mod_state(w32));
+ return true;
+}
+
+static bool handle_mouse_down(struct vo_w32_state *w32, int btn, int x, int y)
+{
+ btn |= mod_state(w32);
+ mp_input_put_key(w32->input_ctx, btn | MP_KEY_STATE_DOWN);
+
+ if (btn == MP_MBTN_LEFT && !w32->current_fs &&
+ !mp_input_test_dragging(w32->input_ctx, x, y))
+ {
+ // Window dragging hack
+ ReleaseCapture();
+ SendMessage(w32->window, WM_NCLBUTTONDOWN, HTCAPTION, 0);
+ mp_input_put_key(w32->input_ctx, MP_MBTN_LEFT | MP_KEY_STATE_UP);
+
+ // Indicate the message was handled, so DefWindowProc won't be called
+ return true;
+ }
+
+ SetCapture(w32->window);
+ return false;
+}
+
+static void handle_mouse_up(struct vo_w32_state *w32, int btn)
+{
+ btn |= mod_state(w32);
+ mp_input_put_key(w32->input_ctx, btn | MP_KEY_STATE_UP);
+
+ ReleaseCapture();
+}
+
+static void handle_mouse_wheel(struct vo_w32_state *w32, bool horiz, int val)
+{
+ int code;
+ if (horiz)
+ code = val > 0 ? MP_WHEEL_RIGHT : MP_WHEEL_LEFT;
+ else
+ code = val > 0 ? MP_WHEEL_UP : MP_WHEEL_DOWN;
+ mp_input_put_wheel(w32->input_ctx, code | mod_state(w32), abs(val) / 120.);
+}
+
+static void signal_events(struct vo_w32_state *w32, int events)
+{
+ atomic_fetch_or(&w32->event_flags, events);
+ vo_wakeup(w32->vo);
+}
+
+static void wakeup_gui_thread(void *ctx)
+{
+ struct vo_w32_state *w32 = ctx;
+ // Wake up the window procedure (which processes the dispatch queue)
+ if (GetWindowThreadProcessId(w32->window, NULL) == GetCurrentThreadId()) {
+ PostMessageW(w32->window, WM_NULL, 0, 0);
+ } else {
+ // Use a sent message when cross-thread, since the queue of sent
+ // messages is processed in some cases when posted messages are blocked
+ SendNotifyMessageW(w32->window, WM_NULL, 0, 0);
+ }
+}
+
+static double get_refresh_rate_from_gdi(const wchar_t *device)
+{
+ DEVMODEW dm = { .dmSize = sizeof dm };
+ if (!EnumDisplaySettingsW(device, ENUM_CURRENT_SETTINGS, &dm))
+ return 0.0;
+
+ // May return 0 or 1 which "represent the display hardware's default refresh rate"
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/dd183565%28v=vs.85%29.aspx
+ // mpv validates this value with a threshold of 1, so don't return exactly 1
+ if (dm.dmDisplayFrequency == 1)
+ return 0.0;
+
+ // dm.dmDisplayFrequency is an integer which is rounded down, so it's
+ // highly likely that 23 represents 24/1.001, 59 represents 60/1.001, etc.
+ // A caller can always reproduce the original value by using floor.
+ double rv = dm.dmDisplayFrequency;
+ switch (dm.dmDisplayFrequency) {
+ case 23:
+ case 29:
+ case 47:
+ case 59:
+ case 71:
+ case 89:
+ case 95:
+ case 119:
+ case 143:
+ case 164:
+ case 239:
+ case 359:
+ case 479:
+ rv = (rv + 1) / 1.001;
+ }
+
+ return rv;
+}
+
+static char *get_color_profile(void *ctx, const wchar_t *device)
+{
+ char *name = NULL;
+
+ HDC ic = CreateICW(device, NULL, NULL, NULL);
+ if (!ic)
+ goto done;
+ wchar_t wname[MAX_PATH + 1];
+ if (!GetICMProfileW(ic, &(DWORD){ MAX_PATH }, wname))
+ goto done;
+
+ name = mp_to_utf8(ctx, wname);
+done:
+ if (ic)
+ DeleteDC(ic);
+ return name;
+}
+
+static void update_dpi(struct vo_w32_state *w32)
+{
+ UINT dpiX, dpiY;
+ HDC hdc = NULL;
+ int dpi = 0;
+
+ if (w32->api.pGetDpiForMonitor && w32->api.pGetDpiForMonitor(w32->monitor,
+ MDT_EFFECTIVE_DPI, &dpiX, &dpiY) == S_OK) {
+ dpi = (int)dpiX;
+ MP_VERBOSE(w32, "DPI detected from the new API: %d\n", dpi);
+ } else if ((hdc = GetDC(NULL))) {
+ dpi = GetDeviceCaps(hdc, LOGPIXELSX);
+ ReleaseDC(NULL, hdc);
+ MP_VERBOSE(w32, "DPI detected from the old API: %d\n", dpi);
+ }
+
+ if (dpi <= 0) {
+ dpi = 96;
+ MP_VERBOSE(w32, "Couldn't determine DPI, falling back to %d\n", dpi);
+ }
+
+ w32->dpi = dpi;
+ w32->dpi_scale = w32->opts->hidpi_window_scale ? w32->dpi / 96.0 : 1.0;
+ signal_events(w32, VO_EVENT_DPI);
+}
+
+static void update_display_info(struct vo_w32_state *w32)
+{
+ HMONITOR monitor = MonitorFromWindow(w32->window, MONITOR_DEFAULTTOPRIMARY);
+ if (w32->monitor == monitor)
+ return;
+ w32->monitor = monitor;
+
+ update_dpi(w32);
+
+ MONITORINFOEXW mi = { .cbSize = sizeof mi };
+ GetMonitorInfoW(monitor, (MONITORINFO*)&mi);
+
+ // Try to get the monitor refresh rate.
+ double freq = 0.0;
+
+ if (freq == 0.0)
+ freq = mp_w32_displayconfig_get_refresh_rate(mi.szDevice);
+ if (freq == 0.0)
+ freq = get_refresh_rate_from_gdi(mi.szDevice);
+
+ if (freq != w32->display_fps) {
+ MP_VERBOSE(w32, "display-fps: %f\n", freq);
+ if (freq == 0.0)
+ MP_WARN(w32, "Couldn't determine monitor refresh rate\n");
+ w32->display_fps = freq;
+ signal_events(w32, VO_EVENT_WIN_STATE);
+ }
+
+ char *color_profile = get_color_profile(w32, mi.szDevice);
+ if ((color_profile == NULL) != (w32->color_profile == NULL) ||
+ (color_profile && strcmp(color_profile, w32->color_profile)))
+ {
+ if (color_profile)
+ MP_VERBOSE(w32, "color-profile: %s\n", color_profile);
+ talloc_free(w32->color_profile);
+ w32->color_profile = color_profile;
+ color_profile = NULL;
+ signal_events(w32, VO_EVENT_ICC_PROFILE_CHANGED);
+ }
+
+ talloc_free(color_profile);
+}
+
+static void force_update_display_info(struct vo_w32_state *w32)
+{
+ w32->monitor = 0;
+ update_display_info(w32);
+}
+
+static void update_playback_state(struct vo_w32_state *w32)
+{
+ struct voctrl_playback_state *pstate = &w32->current_pstate;
+
+ if (!w32->taskbar_list3 || !w32->tbtnCreated)
+ return;
+
+ if (!pstate->playing || !pstate->taskbar_progress) {
+ ITaskbarList3_SetProgressState(w32->taskbar_list3, w32->window,
+ TBPF_NOPROGRESS);
+ return;
+ }
+
+ ITaskbarList3_SetProgressValue(w32->taskbar_list3, w32->window,
+ pstate->percent_pos, 100);
+ ITaskbarList3_SetProgressState(w32->taskbar_list3, w32->window,
+ pstate->paused ? TBPF_PAUSED :
+ TBPF_NORMAL);
+}
+
+struct get_monitor_data {
+ int i;
+ int target;
+ HMONITOR mon;
+};
+
+static BOOL CALLBACK get_monitor_proc(HMONITOR mon, HDC dc, LPRECT r, LPARAM p)
+{
+ struct get_monitor_data *data = (struct get_monitor_data*)p;
+
+ if (data->i == data->target) {
+ data->mon = mon;
+ return FALSE;
+ }
+ data->i++;
+ return TRUE;
+}
+
+static HMONITOR get_monitor(int id)
+{
+ struct get_monitor_data data = { .target = id };
+ EnumDisplayMonitors(NULL, NULL, get_monitor_proc, (LPARAM)&data);
+ return data.mon;
+}
+
+static HMONITOR get_default_monitor(struct vo_w32_state *w32)
+{
+ const int id = w32->current_fs ? w32->opts->fsscreen_id :
+ w32->opts->screen_id;
+
+ // Handle --fs-screen=<all|default> and --screen=default
+ if (id < 0) {
+ if (w32->win_force_pos && !w32->current_fs) {
+ // Get window from forced position
+ return MonitorFromRect(&w32->windowrc, MONITOR_DEFAULTTOPRIMARY);
+ } else {
+ // Let compositor decide
+ return MonitorFromWindow(w32->window, MONITOR_DEFAULTTOPRIMARY);
+ }
+ }
+
+ HMONITOR mon = get_monitor(id);
+ if (mon)
+ return mon;
+ MP_VERBOSE(w32, "Screen %d does not exist, falling back to primary\n", id);
+ return MonitorFromPoint((POINT){0, 0}, MONITOR_DEFAULTTOPRIMARY);
+}
+
+static MONITORINFO get_monitor_info(struct vo_w32_state *w32)
+{
+ HMONITOR mon;
+ if (IsWindowVisible(w32->window) && !w32->current_fs) {
+ mon = MonitorFromWindow(w32->window, MONITOR_DEFAULTTOPRIMARY);
+ } else {
+ // The window is not visible during initialization, so get the
+ // monitor by --screen or --fs-screen id, or fallback to primary.
+ mon = get_default_monitor(w32);
+ }
+ MONITORINFO mi = { .cbSize = sizeof(mi) };
+ GetMonitorInfoW(mon, &mi);
+ return mi;
+}
+
+static RECT get_screen_area(struct vo_w32_state *w32)
+{
+ // Handle --fs-screen=all
+ if (w32->current_fs && w32->opts->fsscreen_id == -2) {
+ const int x = GetSystemMetrics(SM_XVIRTUALSCREEN);
+ const int y = GetSystemMetrics(SM_YVIRTUALSCREEN);
+ return (RECT) { x, y, x + GetSystemMetrics(SM_CXVIRTUALSCREEN),
+ y + GetSystemMetrics(SM_CYVIRTUALSCREEN) };
+ }
+ return get_monitor_info(w32).rcMonitor;
+}
+
+static RECT get_working_area(struct vo_w32_state *w32)
+{
+ return w32->current_fs ? get_screen_area(w32) :
+ get_monitor_info(w32).rcWork;
+}
+
+// Adjust working area boundaries to compensate for invisible borders.
+static void adjust_working_area_for_extended_frame(RECT *wa_rect, RECT *wnd_rect, HWND wnd)
+{
+ RECT frame = {0};
+
+ if (DwmGetWindowAttribute(wnd, DWMWA_EXTENDED_FRAME_BOUNDS,
+ &frame, sizeof(RECT)) == S_OK) {
+ wa_rect->left -= frame.left - wnd_rect->left;
+ wa_rect->top -= frame.top - wnd_rect->top;
+ wa_rect->right += wnd_rect->right - frame.right;
+ wa_rect->bottom += wnd_rect->bottom - frame.bottom;
+ }
+}
+
+static bool snap_to_screen_edges(struct vo_w32_state *w32, RECT *rc)
+{
+ if (w32->parent || w32->current_fs || IsMaximized(w32->window))
+ return false;
+
+ if (!w32->opts->snap_window) {
+ w32->snapped = 0;
+ return false;
+ }
+
+ RECT rect;
+ POINT cursor;
+ if (!GetWindowRect(w32->window, &rect) || !GetCursorPos(&cursor))
+ return false;
+ // Check if window is going to be aero-snapped
+ if (rect_w(*rc) != rect_w(rect) || rect_h(*rc) != rect_h(rect))
+ return false;
+
+ // Check if window has already been aero-snapped
+ WINDOWPLACEMENT wp = {0};
+ wp.length = sizeof(wp);
+ if (!GetWindowPlacement(w32->window, &wp))
+ return false;
+ RECT wr = wp.rcNormalPosition;
+ if (rect_w(*rc) != rect_w(wr) || rect_h(*rc) != rect_h(wr))
+ return false;
+
+ // Get the work area to let the window snap to taskbar
+ wr = get_working_area(w32);
+
+ adjust_working_area_for_extended_frame(&wr, &rect, w32->window);
+
+ // Let the window to unsnap by changing its position,
+ // otherwise it will stick to the screen edges forever
+ rect = *rc;
+ if (w32->snapped) {
+ OffsetRect(&rect, cursor.x - rect.left - w32->snap_dx,
+ cursor.y - rect.top - w32->snap_dy);
+ }
+
+ int threshold = (w32->dpi * 16) / 96;
+ bool was_snapped = !!w32->snapped;
+ w32->snapped = 0;
+ // Adjust X position
+ // snapped_left & snapped_right are mutually exclusive
+ if (abs(rect.left - wr.left) < threshold) {
+ w32->snapped_left = 1;
+ OffsetRect(&rect, wr.left - rect.left, 0);
+ } else if (abs(rect.right - wr.right) < threshold) {
+ w32->snapped_right = 1;
+ OffsetRect(&rect, wr.right - rect.right, 0);
+ }
+ // Adjust Y position
+ // snapped_top & snapped_bottom are mutually exclusive
+ if (abs(rect.top - wr.top) < threshold) {
+ w32->snapped_top = 1;
+ OffsetRect(&rect, 0, wr.top - rect.top);
+ } else if (abs(rect.bottom - wr.bottom) < threshold) {
+ w32->snapped_bottom = 1;
+ OffsetRect(&rect, 0, wr.bottom - rect.bottom);
+ }
+
+ if (!was_snapped && w32->snapped != 0) {
+ w32->snap_dx = cursor.x - rc->left;
+ w32->snap_dy = cursor.y - rc->top;
+ }
+
+ *rc = rect;
+ return true;
+}
+
+static DWORD update_style(struct vo_w32_state *w32, DWORD style)
+{
+ const DWORD NO_FRAME = WS_OVERLAPPED | WS_MINIMIZEBOX | WS_THICKFRAME;
+ const DWORD FRAME = WS_OVERLAPPEDWINDOW;
+ const DWORD FULLSCREEN = NO_FRAME & ~WS_THICKFRAME;
+ style &= ~(NO_FRAME | FRAME | FULLSCREEN);
+ style |= WS_SYSMENU;
+ if (w32->current_fs) {
+ style |= FULLSCREEN;
+ } else {
+ style |= w32->opts->border ? FRAME : NO_FRAME;
+ }
+ return style;
+}
+
+static LONG get_title_bar_height(struct vo_w32_state *w32)
+{
+ RECT rc = {0};
+ adjust_window_rect(w32, w32->window, &rc);
+ return -rc.top;
+}
+
+static void update_window_style(struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ // SetWindowLongPtr can trigger a WM_SIZE event, so window rect
+ // has to be saved now and restored after setting the new style.
+ const RECT wr = w32->windowrc;
+ const DWORD style = GetWindowLongPtrW(w32->window, GWL_STYLE);
+ SetWindowLongPtrW(w32->window, GWL_STYLE, update_style(w32, style));
+ w32->windowrc = wr;
+}
+
+// Resize window rect to width = w and height = h. If window is snapped,
+// don't let it detach from snapped borders. Otherwise resize around the center.
+static void resize_and_move_rect(struct vo_w32_state *w32, RECT *rc, int w, int h)
+{
+ int x, y;
+
+ if (w32->snapped_left)
+ x = rc->left;
+ else if (w32->snapped_right)
+ x = rc->right - w;
+ else
+ x = rc->left + rect_w(*rc) / 2 - w / 2;
+
+ if (w32->snapped_top)
+ y = rc->top;
+ else if (w32->snapped_bottom)
+ y = rc->bottom - h;
+ else
+ y = rc->top + rect_h(*rc) / 2 - h / 2;
+
+ SetRect(rc, x, y, x + w, y + h);
+}
+
+// If rc is wider/taller than n_w/n_h, shrink rc size while keeping the center.
+// returns true if the rectangle was modified.
+static bool fit_rect_size(struct vo_w32_state *w32, RECT *rc, long n_w, long n_h)
+{
+ // nothing to do if we already fit.
+ int o_w = rect_w(*rc), o_h = rect_h(*rc);
+ if (o_w <= n_w && o_h <= n_h)
+ return false;
+
+ // Apply letterboxing
+ const float o_asp = o_w / (float)MPMAX(o_h, 1);
+ const float n_asp = n_w / (float)MPMAX(n_h, 1);
+ if (o_asp > n_asp) {
+ n_h = n_w / o_asp;
+ } else {
+ n_w = n_h * o_asp;
+ }
+
+ resize_and_move_rect(w32, rc, n_w, n_h);
+
+ return true;
+}
+
+// If the window is bigger than the desktop, shrink to fit with same center.
+// Also, if the top edge is above the working area, move down to align.
+static void fit_window_on_screen(struct vo_w32_state *w32)
+{
+ RECT screen = get_working_area(w32);
+ if (w32->opts->border)
+ subtract_window_borders(w32, w32->window, &screen);
+
+ RECT window_rect;
+ if (GetWindowRect(w32->window, &window_rect))
+ adjust_working_area_for_extended_frame(&screen, &window_rect, w32->window);
+
+ bool adjusted = fit_rect_size(w32, &w32->windowrc, rect_w(screen), rect_h(screen));
+
+ if (w32->windowrc.top < screen.top) {
+ // if the top-edge of client area is above the target area (mainly
+ // because the client-area is centered but the title bar is taller
+ // than the bottom border), then move it down to align the edges.
+ // Windows itself applies the same constraint during manual move.
+ w32->windowrc.bottom += screen.top - w32->windowrc.top;
+ w32->windowrc.top = screen.top;
+ adjusted = true;
+ }
+
+ if (adjusted) {
+ MP_VERBOSE(w32, "adjusted window bounds: %d:%d:%d:%d\n",
+ (int)w32->windowrc.left, (int)w32->windowrc.top,
+ (int)rect_w(w32->windowrc), (int)rect_h(w32->windowrc));
+ }
+}
+
+// Calculate new fullscreen state and change window size and position.
+static void update_fullscreen_state(struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ bool new_fs = w32->opts->fullscreen;
+ if (w32->toggle_fs) {
+ new_fs = !w32->current_fs;
+ w32->toggle_fs = false;
+ }
+
+ bool toggle_fs = w32->current_fs != new_fs;
+ w32->opts->fullscreen = w32->current_fs = new_fs;
+ m_config_cache_write_opt(w32->opts_cache,
+ &w32->opts->fullscreen);
+
+ if (toggle_fs) {
+ if (w32->current_fs) {
+ // Save window rect when switching to fullscreen.
+ w32->prev_windowrc = w32->windowrc;
+ MP_VERBOSE(w32, "save window bounds: %d:%d:%d:%d\n",
+ (int)w32->windowrc.left, (int)w32->windowrc.top,
+ (int)rect_w(w32->windowrc), (int)rect_h(w32->windowrc));
+ } else {
+ // Restore window rect when switching from fullscreen.
+ w32->windowrc = w32->prev_windowrc;
+ }
+ }
+
+ if (w32->current_fs)
+ w32->windowrc = get_screen_area(w32);
+
+ MP_VERBOSE(w32, "reset window bounds: %d:%d:%d:%d\n",
+ (int)w32->windowrc.left, (int)w32->windowrc.top,
+ (int)rect_w(w32->windowrc), (int)rect_h(w32->windowrc));
+}
+
+static void update_minimized_state(struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ if (!!IsMinimized(w32->window) != w32->opts->window_minimized) {
+ if (w32->opts->window_minimized) {
+ ShowWindow(w32->window, SW_SHOWMINNOACTIVE);
+ } else {
+ ShowWindow(w32->window, SW_RESTORE);
+ }
+ }
+}
+
+static void update_maximized_state(struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ // Don't change the maximized state in fullscreen for now. In future, this
+ // should be made to apply the maximized state on leaving fullscreen.
+ if (w32->current_fs)
+ return;
+
+ WINDOWPLACEMENT wp = { .length = sizeof wp };
+ GetWindowPlacement(w32->window, &wp);
+
+ if (wp.showCmd == SW_SHOWMINIMIZED) {
+ // When the window is minimized, setting this property just changes
+ // whether it will be maximized when it's restored
+ if (w32->opts->window_maximized) {
+ wp.flags |= WPF_RESTORETOMAXIMIZED;
+ } else {
+ wp.flags &= ~WPF_RESTORETOMAXIMIZED;
+ }
+ SetWindowPlacement(w32->window, &wp);
+ } else if ((wp.showCmd == SW_SHOWMAXIMIZED) != w32->opts->window_maximized) {
+ if (w32->opts->window_maximized) {
+ ShowWindow(w32->window, SW_SHOWMAXIMIZED);
+ } else {
+ ShowWindow(w32->window, SW_SHOWNOACTIVATE);
+ }
+ }
+}
+
+static bool is_visible(HWND window)
+{
+ // Unlike IsWindowVisible, this doesn't check the window's parents
+ return GetWindowLongPtrW(window, GWL_STYLE) & WS_VISIBLE;
+}
+
+//Set the mpv window's affinity.
+//This will affect how it's displayed on the desktop and in system-level operations like taking screenshots.
+static void update_affinity(struct vo_w32_state *w32)
+{
+ if (!w32 || w32->parent) {
+ return;
+ }
+ SetWindowDisplayAffinity(w32->window, w32->opts->window_affinity);
+}
+
+static void update_window_state(struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ RECT wr = w32->windowrc;
+ add_window_borders(w32, w32->window, &wr);
+
+ SetWindowPos(w32->window, w32->opts->ontop ? HWND_TOPMOST : HWND_NOTOPMOST,
+ wr.left, wr.top, rect_w(wr), rect_h(wr),
+ SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOOWNERZORDER);
+
+ // Show the window if it's not yet visible
+ if (!is_visible(w32->window)) {
+ if (w32->opts->window_minimized) {
+ ShowWindow(w32->window, SW_SHOWMINNOACTIVE);
+ update_maximized_state(w32); // Set the WPF_RESTORETOMAXIMIZED flag
+ } else if (w32->opts->window_maximized) {
+ ShowWindow(w32->window, SW_SHOWMAXIMIZED);
+ } else {
+ ShowWindow(w32->window, SW_SHOW);
+ }
+ }
+
+ // Notify the taskbar about the fullscreen state only after the window
+ // is visible, to make sure the taskbar item has already been created
+ if (w32->taskbar_list) {
+ ITaskbarList2_MarkFullscreenWindow(w32->taskbar_list,
+ w32->window, w32->current_fs);
+ }
+
+ // Update snapping status if needed
+ if (w32->opts->snap_window && !w32->parent &&
+ !w32->current_fs && !IsMaximized(w32->window)) {
+ RECT wa = get_working_area(w32);
+
+ adjust_working_area_for_extended_frame(&wa, &wr, w32->window);
+
+ // snapped_left & snapped_right are mutually exclusive
+ if (wa.left == wr.left && wa.right == wr.right) {
+ // Leave as is.
+ } else if (wa.left == wr.left) {
+ w32->snapped_left = 1;
+ w32->snapped_right = 0;
+ } else if (wa.right == wr.right) {
+ w32->snapped_right = 1;
+ w32->snapped_left = 0;
+ } else {
+ w32->snapped_left = w32->snapped_right = 0;
+ }
+
+ // snapped_top & snapped_bottom are mutually exclusive
+ if (wa.top == wr.top && wa.bottom == wr.bottom) {
+ // Leave as is.
+ } else if (wa.top == wr.top) {
+ w32->snapped_top = 1;
+ w32->snapped_bottom = 0;
+ } else if (wa.bottom == wr.bottom) {
+ w32->snapped_bottom = 1;
+ w32->snapped_top = 0;
+ } else {
+ w32->snapped_top = w32->snapped_bottom = 0;
+ }
+ }
+
+ signal_events(w32, VO_EVENT_RESIZE);
+}
+
+static void update_corners_pref(const struct vo_w32_state *w32) {
+ if (w32->parent)
+ return;
+
+ int pref = w32->current_fs ? 0 : w32->opts->window_corners;
+ DwmSetWindowAttribute(w32->window, DWMWA_WINDOW_CORNER_PREFERENCE,
+ &pref, sizeof(pref));
+}
+
+static void reinit_window_state(struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ // The order matters: fs state should be updated prior to changing styles
+ update_fullscreen_state(w32);
+ update_corners_pref(w32);
+ update_window_style(w32);
+
+ // fit_on_screen is applied at most once when/if applicable (normal win).
+ if (w32->fit_on_screen && !w32->current_fs && !IsMaximized(w32->window)) {
+ fit_window_on_screen(w32);
+ w32->fit_on_screen = false;
+ }
+
+ // Show and activate the window after all window state parameters were set
+ update_window_state(w32);
+}
+
+// Follow Windows settings and update dark mode state
+// Microsoft documented how to enable dark mode for title bar:
+// https://learn.microsoft.com/windows/apps/desktop/modernize/apply-windows-themes
+// https://learn.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
+// Documentation says to set the DWMWA_USE_IMMERSIVE_DARK_MODE attribute to
+// TRUE to honor dark mode for the window, FALSE to always use light mode. While
+// in fact setting it to TRUE causes dark mode to be always enabled, regardless
+// of the settings. Since it is quite unlikely that it will be fixed, just use
+// UxTheme API to check if dark mode should be applied and while at it enable it
+// fully. Ideally this function should only call the DwmSetWindowAttribute(),
+// but it just doesn't work as documented.
+static void update_dark_mode(const struct vo_w32_state *w32)
+{
+ if (w32->api.pSetPreferredAppMode)
+ w32->api.pSetPreferredAppMode(1); // allow dark mode
+
+ HIGHCONTRAST hc = {sizeof(hc)};
+ SystemParametersInfo(SPI_GETHIGHCONTRAST, sizeof(hc), &hc, 0);
+ bool high_contrast = hc.dwFlags & HCF_HIGHCONTRASTON;
+
+ // if pShouldAppsUseDarkMode is not available, just assume it to be true
+ const BOOL use_dark_mode = !high_contrast && (!w32->api.pShouldAppsUseDarkMode ||
+ w32->api.pShouldAppsUseDarkMode());
+
+ SetWindowTheme(w32->window, use_dark_mode ? L"DarkMode_Explorer" : L"", NULL);
+
+ DwmSetWindowAttribute(w32->window, DWMWA_USE_IMMERSIVE_DARK_MODE,
+ &use_dark_mode, sizeof(use_dark_mode));
+}
+
+static void update_backdrop(const struct vo_w32_state *w32)
+{
+ if (w32->parent)
+ return;
+
+ int backdropType = w32->opts->backdrop_type;
+ DwmSetWindowAttribute(w32->window, DWMWA_SYSTEMBACKDROP_TYPE,
+ &backdropType, sizeof(backdropType));
+}
+
+static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,
+ LPARAM lParam)
+{
+ struct vo_w32_state *w32 = (void*)GetWindowLongPtrW(hWnd, GWLP_USERDATA);
+ if (!w32) {
+ // WM_NCCREATE is supposed to be the first message that a window
+ // receives. It allows struct vo_w32_state to be passed from
+ // CreateWindow's lpParam to the window procedure. However, as a
+ // longstanding Windows bug, overlapped top-level windows will get a
+ // WM_GETMINMAXINFO before WM_NCCREATE. This can be ignored.
+ if (message != WM_NCCREATE)
+ return DefWindowProcW(hWnd, message, wParam, lParam);
+
+ CREATESTRUCTW *cs = (CREATESTRUCTW *)lParam;
+ w32 = cs->lpCreateParams;
+ w32->window = hWnd;
+ SetWindowLongPtrW(hWnd, GWLP_USERDATA, (LONG_PTR)w32);
+ }
+
+ // The dispatch queue should be processed as soon as possible to prevent
+ // playback glitches, since it is likely blocking the VO thread
+ if (!w32->in_dispatch) {
+ w32->in_dispatch = true;
+ mp_dispatch_queue_process(w32->dispatch, 0);
+ w32->in_dispatch = false;
+ }
+
+ switch (message) {
+ case WM_ERASEBKGND:
+ if (w32->cleared || !w32->opts->border || w32->current_fs)
+ return TRUE;
+ break;
+ case WM_PAINT:
+ w32->cleared = true;
+ signal_events(w32, VO_EVENT_EXPOSE);
+ break;
+ case WM_MOVE: {
+ w32->moving = false;
+ const int x = GET_X_LPARAM(lParam), y = GET_Y_LPARAM(lParam);
+ OffsetRect(&w32->windowrc, x - w32->windowrc.left,
+ y - w32->windowrc.top);
+
+ // Window may intersect with new monitors (see VOCTRL_GET_DISPLAY_NAMES)
+ signal_events(w32, VO_EVENT_WIN_STATE);
+
+ update_display_info(w32); // if we moved between monitors
+ break;
+ }
+ case WM_MOVING: {
+ w32->moving = true;
+ RECT *rc = (RECT*)lParam;
+ if (snap_to_screen_edges(w32, rc))
+ return TRUE;
+ break;
+ }
+ case WM_ENTERSIZEMOVE:
+ w32->moving = true;
+ if (w32->snapped != 0) {
+ // Save the cursor offset from the window borders,
+ // so the player window can be unsnapped later
+ RECT rc;
+ POINT cursor;
+ if (GetWindowRect(w32->window, &rc) && GetCursorPos(&cursor)) {
+ w32->snap_dx = cursor.x - rc.left;
+ w32->snap_dy = cursor.y - rc.top;
+ }
+ }
+ break;
+ case WM_EXITSIZEMOVE:
+ w32->moving = false;
+ break;
+ case WM_SIZE: {
+ const int w = LOWORD(lParam), h = HIWORD(lParam);
+ if (w > 0 && h > 0) {
+ w32->windowrc.right = w32->windowrc.left + w;
+ w32->windowrc.bottom = w32->windowrc.top + h;
+ signal_events(w32, VO_EVENT_RESIZE);
+ MP_VERBOSE(w32, "resize window: %d:%d\n", w, h);
+ }
+
+ // Window may have been minimized, maximized or restored
+ if (is_visible(w32->window)) {
+ WINDOWPLACEMENT wp = { .length = sizeof wp };
+ GetWindowPlacement(w32->window, &wp);
+
+ bool is_minimized = wp.showCmd == SW_SHOWMINIMIZED;
+ if (w32->opts->window_minimized != is_minimized) {
+ w32->opts->window_minimized = is_minimized;
+ m_config_cache_write_opt(w32->opts_cache,
+ &w32->opts->window_minimized);
+ }
+
+ bool is_maximized = wp.showCmd == SW_SHOWMAXIMIZED ||
+ (wp.showCmd == SW_SHOWMINIMIZED &&
+ (wp.flags & WPF_RESTORETOMAXIMIZED));
+ if (w32->opts->window_maximized != is_maximized) {
+ w32->opts->window_maximized = is_maximized;
+ m_config_cache_write_opt(w32->opts_cache,
+ &w32->opts->window_maximized);
+ }
+ }
+
+ signal_events(w32, VO_EVENT_WIN_STATE);
+
+ update_display_info(w32);
+ break;
+ }
+ case WM_SIZING:
+ if (w32->opts->keepaspect && w32->opts->keepaspect_window &&
+ !w32->current_fs && !w32->parent)
+ {
+ RECT *rc = (RECT*)lParam;
+ // get client area of the windows if it had the rect rc
+ // (subtracting the window borders)
+ RECT r = *rc;
+ subtract_window_borders(w32, w32->window, &r);
+ int c_w = rect_w(r), c_h = rect_h(r);
+ float aspect = w32->o_dwidth / (float) MPMAX(w32->o_dheight, 1);
+ int d_w = c_h * aspect - c_w;
+ int d_h = c_w / aspect - c_h;
+ int d_corners[4] = { d_w, d_h, -d_w, -d_h };
+ int corners[4] = { rc->left, rc->top, rc->right, rc->bottom };
+ int corner = get_resize_border(w32, wParam);
+ if (corner >= 0)
+ corners[corner] -= d_corners[corner];
+ *rc = (RECT) { corners[0], corners[1], corners[2], corners[3] };
+ return TRUE;
+ }
+ break;
+ case WM_DPICHANGED:
+ update_display_info(w32);
+
+ RECT *rc = (RECT*)lParam;
+ w32->windowrc = *rc;
+ subtract_window_borders(w32, w32->window, &w32->windowrc);
+ update_window_state(w32);
+ break;
+ case WM_CLOSE:
+ // Don't destroy the window yet to not lose wakeup events.
+ mp_input_put_key(w32->input_ctx, MP_KEY_CLOSE_WIN);
+ return 0;
+ case WM_NCDESTROY: // Sometimes only WM_NCDESTROY is received in --wid mode
+ case WM_DESTROY:
+ if (w32->destroyed)
+ break;
+ // If terminate is not set, something else destroyed the window. This
+ // can also happen in --wid mode when the parent window is destroyed.
+ if (!w32->terminate)
+ mp_input_put_key(w32->input_ctx, MP_KEY_CLOSE_WIN);
+ RevokeDragDrop(w32->window);
+ w32->destroyed = true;
+ w32->window = NULL;
+ PostQuitMessage(0);
+ break;
+ case WM_SYSCOMMAND:
+ switch (wParam & 0xFFF0) {
+ case SC_SCREENSAVE:
+ case SC_MONITORPOWER:
+ if (w32->disable_screensaver) {
+ MP_VERBOSE(w32, "killing screensaver\n");
+ return 0;
+ }
+ break;
+ case SC_RESTORE:
+ if (IsMaximized(w32->window) && w32->current_fs) {
+ w32->toggle_fs = true;
+ reinit_window_state(w32);
+
+ return 0;
+ }
+ break;
+ }
+ break;
+ case WM_NCACTIVATE:
+ // Cosmetic to remove blinking window border when initializing window
+ if (!w32->opts->border)
+ lParam = -1;
+ break;
+ case WM_NCHITTEST:
+ // Provide sizing handles for borderless windows
+ if ((!w32->opts->border || !w32->opts->title_bar) && !w32->current_fs) {
+ return borderless_nchittest(w32, GET_X_LPARAM(lParam),
+ GET_Y_LPARAM(lParam));
+ }
+ break;
+ case WM_APPCOMMAND:
+ if (handle_appcommand(w32, GET_APPCOMMAND_LPARAM(lParam)))
+ return TRUE;
+ break;
+ case WM_SYSKEYDOWN:
+ // Open the window menu on Alt+Space. Normally DefWindowProc opens the
+ // window menu in response to WM_SYSCHAR, but since mpv translates its
+ // own keyboard input, WM_SYSCHAR isn't generated, so the window menu
+ // must be opened manually.
+ if (wParam == VK_SPACE) {
+ SendMessage(w32->window, WM_SYSCOMMAND, SC_KEYMENU, ' ');
+ return 0;
+ }
+
+ handle_key_down(w32, wParam, HIWORD(lParam));
+ if (wParam == VK_F10)
+ return 0;
+ break;
+ case WM_KEYDOWN:
+ handle_key_down(w32, wParam, HIWORD(lParam));
+ break;
+ case WM_SYSKEYUP:
+ case WM_KEYUP:
+ handle_key_up(w32, wParam, HIWORD(lParam));
+ if (wParam == VK_F10)
+ return 0;
+ break;
+ case WM_CHAR:
+ case WM_SYSCHAR:
+ if (handle_char(w32, wParam))
+ return 0;
+ break;
+ case WM_KILLFOCUS:
+ mp_input_put_key(w32->input_ctx, MP_INPUT_RELEASE_ALL);
+ w32->focused = false;
+ signal_events(w32, VO_EVENT_FOCUS);
+ return 0;
+ case WM_SETFOCUS:
+ w32->focused = true;
+ signal_events(w32, VO_EVENT_FOCUS);
+ return 0;
+ case WM_SETCURSOR:
+ // The cursor should only be hidden if the mouse is in the client area
+ // and if the window isn't in menu mode (HIWORD(lParam) is non-zero)
+ w32->can_set_cursor = LOWORD(lParam) == HTCLIENT && HIWORD(lParam);
+ if (w32->can_set_cursor && !w32->cursor_visible) {
+ SetCursor(NULL);
+ return TRUE;
+ }
+ break;
+ case WM_MOUSELEAVE:
+ w32->tracking = FALSE;
+ mp_input_put_key(w32->input_ctx, MP_KEY_MOUSE_LEAVE);
+ break;
+ case WM_MOUSEMOVE: {
+ if (!w32->tracking) {
+ w32->tracking = TrackMouseEvent(&w32->trackEvent);
+ mp_input_put_key(w32->input_ctx, MP_KEY_MOUSE_ENTER);
+ }
+ // Windows can send spurious mouse events, which would make the mpv
+ // core unhide the mouse cursor on completely unrelated events. See:
+ // https://blogs.msdn.com/b/oldnewthing/archive/2003/10/01/55108.aspx
+ int x = GET_X_LPARAM(lParam);
+ int y = GET_Y_LPARAM(lParam);
+ if (x != w32->mouse_x || y != w32->mouse_y) {
+ w32->mouse_x = x;
+ w32->mouse_y = y;
+ mp_input_set_mouse_pos(w32->input_ctx, x, y);
+ }
+ break;
+ }
+ case WM_LBUTTONDOWN:
+ if (handle_mouse_down(w32, MP_MBTN_LEFT, GET_X_LPARAM(lParam),
+ GET_Y_LPARAM(lParam)))
+ return 0;
+ break;
+ case WM_LBUTTONUP:
+ handle_mouse_up(w32, MP_MBTN_LEFT);
+ break;
+ case WM_MBUTTONDOWN:
+ handle_mouse_down(w32, MP_MBTN_MID, GET_X_LPARAM(lParam),
+ GET_Y_LPARAM(lParam));
+ break;
+ case WM_MBUTTONUP:
+ handle_mouse_up(w32, MP_MBTN_MID);
+ break;
+ case WM_RBUTTONDOWN:
+ handle_mouse_down(w32, MP_MBTN_RIGHT, GET_X_LPARAM(lParam),
+ GET_Y_LPARAM(lParam));
+ break;
+ case WM_RBUTTONUP:
+ handle_mouse_up(w32, MP_MBTN_RIGHT);
+ break;
+ case WM_MOUSEWHEEL:
+ handle_mouse_wheel(w32, false, GET_WHEEL_DELTA_WPARAM(wParam));
+ return 0;
+ case WM_MOUSEHWHEEL:
+ handle_mouse_wheel(w32, true, GET_WHEEL_DELTA_WPARAM(wParam));
+ // Some buggy mouse drivers (SetPoint) stop delivering WM_MOUSEHWHEEL
+ // events when the message loop doesn't return TRUE (even on Windows 7)
+ return TRUE;
+ case WM_XBUTTONDOWN:
+ handle_mouse_down(w32,
+ HIWORD(wParam) == 1 ? MP_MBTN_BACK : MP_MBTN_FORWARD,
+ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
+ break;
+ case WM_XBUTTONUP:
+ handle_mouse_up(w32,
+ HIWORD(wParam) == 1 ? MP_MBTN_BACK : MP_MBTN_FORWARD);
+ break;
+ case WM_DISPLAYCHANGE:
+ force_update_display_info(w32);
+ break;
+ case WM_SETTINGCHANGE:
+ update_dark_mode(w32);
+ break;
+ case WM_NCCALCSIZE:
+ if (!w32->opts->border)
+ return 0;
+ // Apparently removing WS_CAPTION disables some window animation, instead
+ // just reduce non-client size to remove title bar.
+ if (wParam && lParam && w32->opts->border && !w32->opts->title_bar &&
+ !w32->current_fs && !w32->parent)
+ {
+ ((LPNCCALCSIZE_PARAMS) lParam)->rgrc[0].top -= get_title_bar_height(w32);
+ }
+ break;
+ }
+
+ if (message == w32->tbtnCreatedMsg) {
+ w32->tbtnCreated = true;
+ update_playback_state(w32);
+ return 0;
+ }
+
+ return DefWindowProcW(hWnd, message, wParam, lParam);
+}
+
+static mp_once window_class_init_once = MP_STATIC_ONCE_INITIALIZER;
+static ATOM window_class;
+static void register_window_class(void)
+{
+ window_class = RegisterClassExW(&(WNDCLASSEXW) {
+ .cbSize = sizeof(WNDCLASSEXW),
+ .style = CS_HREDRAW | CS_VREDRAW,
+ .lpfnWndProc = WndProc,
+ .hInstance = HINST_THISCOMPONENT,
+ .hIcon = LoadIconW(HINST_THISCOMPONENT, L"IDI_ICON1"),
+ .hCursor = LoadCursor(NULL, IDC_ARROW),
+ .hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH),
+ .lpszClassName = L"mpv",
+ });
+}
+
+static ATOM get_window_class(void)
+{
+ mp_exec_once(&window_class_init_once, register_window_class);
+ return window_class;
+}
+
+static void resize_child_win(HWND parent)
+{
+ // Check if an mpv window is a child of this window. This will not
+ // necessarily be the case because the hook functions will run for all
+ // windows on the parent window's thread.
+ ATOM cls = get_window_class();
+ HWND child = FindWindowExW(parent, NULL, (LPWSTR)MAKEINTATOM(cls), NULL);
+ if (!child)
+ return;
+ // Make sure the window was created by this instance
+ if (GetWindowLongPtrW(child, GWLP_HINSTANCE) != (LONG_PTR)HINST_THISCOMPONENT)
+ return;
+
+ // Resize the mpv window to match its parent window's size
+ RECT rm, rp;
+ if (!GetClientRect(child, &rm))
+ return;
+ if (!GetClientRect(parent, &rp))
+ return;
+ if (EqualRect(&rm, &rp))
+ return;
+ SetWindowPos(child, NULL, 0, 0, rp.right, rp.bottom, SWP_ASYNCWINDOWPOS |
+ SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOSENDCHANGING);
+}
+
+static LRESULT CALLBACK parent_win_hook(int nCode, WPARAM wParam, LPARAM lParam)
+{
+ if (nCode != HC_ACTION)
+ goto done;
+ CWPSTRUCT *cwp = (CWPSTRUCT*)lParam;
+ if (cwp->message != WM_WINDOWPOSCHANGED)
+ goto done;
+ resize_child_win(cwp->hwnd);
+done:
+ return CallNextHookEx(NULL, nCode, wParam, lParam);
+}
+
+static void CALLBACK parent_evt_hook(HWINEVENTHOOK hWinEventHook, DWORD event,
+ HWND hwnd, LONG idObject, LONG idChild, DWORD dwEventThread,
+ DWORD dwmsEventTime)
+{
+ if (event != EVENT_OBJECT_LOCATIONCHANGE)
+ return;
+ if (!hwnd || idObject != OBJID_WINDOW || idChild != CHILDID_SELF)
+ return;
+ resize_child_win(hwnd);
+}
+
+static void install_parent_hook(struct vo_w32_state *w32)
+{
+ DWORD pid;
+ DWORD tid = GetWindowThreadProcessId(w32->parent, &pid);
+
+ // If the parent lives inside the current process, install a Windows hook
+ if (pid == GetCurrentProcessId()) {
+ w32->parent_win_hook = SetWindowsHookExW(WH_CALLWNDPROC,
+ parent_win_hook, NULL, tid);
+ } else {
+ // Otherwise, use a WinEvent hook. These don't seem to be as smooth as
+ // Windows hooks, but they can be delivered across process boundaries.
+ w32->parent_evt_hook = SetWinEventHook(
+ EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE,
+ NULL, parent_evt_hook, pid, tid, WINEVENT_OUTOFCONTEXT);
+ }
+}
+
+static void remove_parent_hook(struct vo_w32_state *w32)
+{
+ if (w32->parent_win_hook)
+ UnhookWindowsHookEx(w32->parent_win_hook);
+ if (w32->parent_evt_hook)
+ UnhookWinEvent(w32->parent_evt_hook);
+}
+
+// Dispatch incoming window events and handle them.
+// This returns only when the thread is asked to terminate.
+static void run_message_loop(struct vo_w32_state *w32)
+{
+ MSG msg;
+ while (GetMessageW(&msg, 0, 0, 0) > 0)
+ DispatchMessageW(&msg);
+
+ // Even if the message loop somehow exits, we still have to respond to
+ // external requests until termination is requested.
+ while (!w32->terminate)
+ mp_dispatch_queue_process(w32->dispatch, 1000);
+}
+
+static void gui_thread_reconfig(void *ptr)
+{
+ struct vo_w32_state *w32 = ptr;
+ struct vo *vo = w32->vo;
+
+ RECT r = get_working_area(w32);
+ // for normal window which is auto-positioned (centered), center the window
+ // rather than the content (by subtracting the borders from the work area)
+ if (!w32->current_fs && !IsMaximized(w32->window) && w32->opts->border &&
+ !w32->opts->geometry.xy_valid /* specific position not requested */)
+ {
+ subtract_window_borders(w32, w32->window, &r);
+ }
+ struct mp_rect screen = { r.left, r.top, r.right, r.bottom };
+ struct vo_win_geometry geo;
+
+ RECT monrc = get_monitor_info(w32).rcMonitor;
+ struct mp_rect mon = { monrc.left, monrc.top, monrc.right, monrc.bottom };
+
+ if (w32->dpi_scale == 0)
+ force_update_display_info(w32);
+
+ vo_calc_window_geometry3(vo, &screen, &mon, w32->dpi_scale, &geo);
+ vo_apply_window_geometry(vo, &geo);
+
+ bool reset_size = (w32->o_dwidth != vo->dwidth ||
+ w32->o_dheight != vo->dheight) &&
+ w32->opts->auto_window_resize;
+
+ w32->o_dwidth = vo->dwidth;
+ w32->o_dheight = vo->dheight;
+
+ if (!w32->parent && !w32->window_bounds_initialized) {
+ SetRect(&w32->windowrc, geo.win.x0, geo.win.y0,
+ geo.win.x0 + vo->dwidth, geo.win.y0 + vo->dheight);
+ w32->prev_windowrc = w32->windowrc;
+ w32->window_bounds_initialized = true;
+ w32->win_force_pos = geo.flags & VO_WIN_FORCE_POS;
+ w32->fit_on_screen = !w32->win_force_pos;
+ goto finish;
+ }
+
+ // The rect which size is going to be modified.
+ RECT *rc = &w32->windowrc;
+
+ // The desired size always matches the window size in wid mode.
+ if (!reset_size || w32->parent) {
+ GetClientRect(w32->window, &r);
+ // Restore vo_dwidth and vo_dheight, which were reset in vo_config()
+ vo->dwidth = r.right;
+ vo->dheight = r.bottom;
+ } else {
+ if (w32->current_fs)
+ rc = &w32->prev_windowrc;
+ w32->fit_on_screen = true;
+ }
+
+ resize_and_move_rect(w32, rc, vo->dwidth, vo->dheight);
+
+finish:
+ reinit_window_state(w32);
+}
+
+// Resize the window. On the first call, it's also made visible.
+void vo_w32_config(struct vo *vo)
+{
+ struct vo_w32_state *w32 = vo->w32;
+ mp_dispatch_run(w32->dispatch, gui_thread_reconfig, w32);
+}
+
+static void w32_api_load(struct vo_w32_state *w32)
+{
+ HMODULE shcore_dll = LoadLibraryW(L"shcore.dll");
+ // Available since Win8.1
+ w32->api.pGetDpiForMonitor = !shcore_dll ? NULL :
+ (void *)GetProcAddress(shcore_dll, "GetDpiForMonitor");
+
+ HMODULE user32_dll = LoadLibraryW(L"user32.dll");
+ // Available since Win10
+ w32->api.pAdjustWindowRectExForDpi = !user32_dll ? NULL :
+ (void *)GetProcAddress(user32_dll, "AdjustWindowRectExForDpi");
+
+ // imm32.dll must be loaded dynamically
+ // to account for machines without East Asian language support
+ HMODULE imm32_dll = LoadLibraryW(L"imm32.dll");
+ w32->api.pImmDisableIME = !imm32_dll ? NULL :
+ (void *)GetProcAddress(imm32_dll, "ImmDisableIME");
+
+ // Dark mode related functions, available since the 1809 Windows 10 update
+ // Check the Windows build version as on previous versions used ordinals
+ // may point to unexpected code/data. Alternatively could check uxtheme.dll
+ // version directly, but it is little bit more boilerplate code, and build
+ // number is good enough check.
+ void (WINAPI *pRtlGetNtVersionNumbers)(LPDWORD, LPDWORD, LPDWORD) =
+ (void *)GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlGetNtVersionNumbers");
+
+ DWORD major, build;
+ pRtlGetNtVersionNumbers(&major, NULL, &build);
+ build &= ~0xF0000000;
+
+ HMODULE uxtheme_dll = (major < 10 || build < 17763) ? NULL :
+ GetModuleHandle(L"uxtheme.dll");
+ w32->api.pShouldAppsUseDarkMode = !uxtheme_dll ? NULL :
+ (void *)GetProcAddress(uxtheme_dll, MAKEINTRESOURCEA(132));
+ w32->api.pSetPreferredAppMode = !uxtheme_dll ? NULL :
+ (void *)GetProcAddress(uxtheme_dll, MAKEINTRESOURCEA(135));
+}
+
+static MP_THREAD_VOID gui_thread(void *ptr)
+{
+ struct vo_w32_state *w32 = ptr;
+ bool ole_ok = false;
+ int res = 0;
+
+ mp_thread_set_name("window");
+
+ w32_api_load(w32);
+
+ // Disables the IME for windows on this thread
+ if (w32->api.pImmDisableIME)
+ w32->api.pImmDisableIME(0);
+
+ if (w32->opts->WinID >= 0)
+ w32->parent = (HWND)(intptr_t)(w32->opts->WinID);
+
+ ATOM cls = get_window_class();
+ if (w32->parent) {
+ RECT r;
+ GetClientRect(w32->parent, &r);
+ CreateWindowExW(WS_EX_NOPARENTNOTIFY, (LPWSTR)MAKEINTATOM(cls), L"mpv",
+ WS_CHILD | WS_VISIBLE, 0, 0, r.right, r.bottom,
+ w32->parent, 0, HINST_THISCOMPONENT, w32);
+
+ // Install a hook to get notifications when the parent changes size
+ if (w32->window)
+ install_parent_hook(w32);
+ } else {
+ CreateWindowExW(0, (LPWSTR)MAKEINTATOM(cls), L"mpv",
+ update_style(w32, 0), CW_USEDEFAULT, SW_HIDE, 100, 100,
+ 0, 0, HINST_THISCOMPONENT, w32);
+ }
+
+ if (!w32->window) {
+ MP_ERR(w32, "unable to create window!\n");
+ goto done;
+ }
+
+ update_dark_mode(w32);
+ update_corners_pref(w32);
+ if (w32->opts->window_affinity)
+ update_affinity(w32);
+ if (w32->opts->backdrop_type)
+ update_backdrop(w32);
+
+ if (SUCCEEDED(OleInitialize(NULL))) {
+ ole_ok = true;
+
+ IDropTarget *dt = mp_w32_droptarget_create(w32->log, w32->opts, w32->input_ctx);
+ RegisterDragDrop(w32->window, dt);
+
+ // ITaskbarList2 has the MarkFullscreenWindow method, which is used to
+ // make sure the taskbar is hidden when mpv goes fullscreen
+ if (SUCCEEDED(CoCreateInstance(&CLSID_TaskbarList, NULL,
+ CLSCTX_INPROC_SERVER, &IID_ITaskbarList2,
+ (void**)&w32->taskbar_list)))
+ {
+ if (FAILED(ITaskbarList2_HrInit(w32->taskbar_list))) {
+ ITaskbarList2_Release(w32->taskbar_list);
+ w32->taskbar_list = NULL;
+ }
+ }
+
+ // ITaskbarList3 has methods for status indication on taskbar buttons,
+ // however that interface is only available on Win7/2008 R2 or newer
+ if (SUCCEEDED(CoCreateInstance(&CLSID_TaskbarList, NULL,
+ CLSCTX_INPROC_SERVER, &IID_ITaskbarList3,
+ (void**)&w32->taskbar_list3)))
+ {
+ if (FAILED(ITaskbarList3_HrInit(w32->taskbar_list3))) {
+ ITaskbarList3_Release(w32->taskbar_list3);
+ w32->taskbar_list3 = NULL;
+ } else {
+ w32->tbtnCreatedMsg = RegisterWindowMessage(L"TaskbarButtonCreated");
+ }
+ }
+ } else {
+ MP_ERR(w32, "Failed to initialize OLE/COM\n");
+ }
+
+ w32->tracking = FALSE;
+ w32->trackEvent = (TRACKMOUSEEVENT){
+ .cbSize = sizeof(TRACKMOUSEEVENT),
+ .dwFlags = TME_LEAVE,
+ .hwndTrack = w32->window,
+ };
+
+ if (w32->parent)
+ EnableWindow(w32->window, 0);
+
+ w32->cursor_visible = true;
+ w32->moving = false;
+ w32->snapped = 0;
+ w32->snap_dx = w32->snap_dy = 0;
+
+ mp_dispatch_set_wakeup_fn(w32->dispatch, wakeup_gui_thread, w32);
+
+ res = 1;
+done:
+
+ mp_rendezvous(w32, res); // init barrier
+
+ // This blocks until the GUI thread is to be exited.
+ if (res)
+ run_message_loop(w32);
+
+ MP_VERBOSE(w32, "uninit\n");
+
+ remove_parent_hook(w32);
+ if (w32->window && !w32->destroyed)
+ DestroyWindow(w32->window);
+ if (w32->taskbar_list)
+ ITaskbarList2_Release(w32->taskbar_list);
+ if (w32->taskbar_list3)
+ ITaskbarList3_Release(w32->taskbar_list3);
+ if (ole_ok)
+ OleUninitialize();
+ SetThreadExecutionState(ES_CONTINUOUS);
+ MP_THREAD_RETURN();
+}
+
+bool vo_w32_init(struct vo *vo)
+{
+ assert(!vo->w32);
+
+ struct vo_w32_state *w32 = talloc_ptrtype(vo, w32);
+ *w32 = (struct vo_w32_state){
+ .log = mp_log_new(w32, vo->log, "win32"),
+ .vo = vo,
+ .opts_cache = m_config_cache_alloc(w32, vo->global, &vo_sub_opts),
+ .input_ctx = vo->input_ctx,
+ .dispatch = mp_dispatch_create(w32),
+ };
+ w32->opts = w32->opts_cache->opts;
+ vo->w32 = w32;
+
+ if (mp_thread_create(&w32->thread, gui_thread, w32))
+ goto fail;
+
+ if (!mp_rendezvous(w32, 0)) { // init barrier
+ mp_thread_join(w32->thread);
+ goto fail;
+ }
+
+ // While the UI runs in its own thread, the thread in which this function
+ // runs in will be the renderer thread. Apply magic MMCSS cargo-cult,
+ // which might stop Windows from throttling clock rate and so on.
+ if (vo->opts->mmcss_profile[0]) {
+ wchar_t *profile = mp_from_utf8(NULL, vo->opts->mmcss_profile);
+ w32->avrt_handle = AvSetMmThreadCharacteristicsW(profile, &(DWORD){0});
+ talloc_free(profile);
+ }
+
+ return true;
+fail:
+ talloc_free(w32);
+ vo->w32 = NULL;
+ return false;
+}
+
+struct disp_names_data {
+ HMONITOR assoc;
+ int count;
+ char **names;
+};
+
+static BOOL CALLBACK disp_names_proc(HMONITOR mon, HDC dc, LPRECT r, LPARAM p)
+{
+ struct disp_names_data *data = (struct disp_names_data*)p;
+
+ // get_disp_names() adds data->assoc to the list, so skip it here
+ if (mon == data->assoc)
+ return TRUE;
+
+ MONITORINFOEXW mi = { .cbSize = sizeof mi };
+ if (GetMonitorInfoW(mon, (MONITORINFO*)&mi)) {
+ MP_TARRAY_APPEND(NULL, data->names, data->count,
+ mp_to_utf8(NULL, mi.szDevice));
+ }
+ return TRUE;
+}
+
+static char **get_disp_names(struct vo_w32_state *w32)
+{
+ // Get the client area of the window in screen space
+ RECT rect = { 0 };
+ GetClientRect(w32->window, &rect);
+ MapWindowPoints(w32->window, NULL, (POINT*)&rect, 2);
+
+ struct disp_names_data data = { .assoc = w32->monitor };
+
+ // Make sure the monitor that Windows considers to be associated with the
+ // window is first in the list
+ MONITORINFOEXW mi = { .cbSize = sizeof mi };
+ if (GetMonitorInfoW(data.assoc, (MONITORINFO*)&mi)) {
+ MP_TARRAY_APPEND(NULL, data.names, data.count,
+ mp_to_utf8(NULL, mi.szDevice));
+ }
+
+ // Get the names of the other monitors that intersect the client rect
+ EnumDisplayMonitors(NULL, &rect, disp_names_proc, (LPARAM)&data);
+ MP_TARRAY_APPEND(NULL, data.names, data.count, NULL);
+ return data.names;
+}
+
+static int gui_thread_control(struct vo_w32_state *w32, int request, void *arg)
+{
+ switch (request) {
+ case VOCTRL_VO_OPTS_CHANGED: {
+ void *changed_option;
+
+ while (m_config_cache_get_next_changed(w32->opts_cache,
+ &changed_option))
+ {
+ struct mp_vo_opts *vo_opts = w32->opts_cache->opts;
+
+ if (changed_option == &vo_opts->fullscreen) {
+ reinit_window_state(w32);
+ } else if (changed_option == &vo_opts->window_affinity) {
+ update_affinity(w32);
+ } else if (changed_option == &vo_opts->ontop) {
+ update_window_state(w32);
+ } else if (changed_option == &vo_opts->backdrop_type) {
+ update_backdrop(w32);
+ } else if (changed_option == &vo_opts->border ||
+ changed_option == &vo_opts->title_bar)
+ {
+ update_window_style(w32);
+ update_window_state(w32);
+ } else if (changed_option == &vo_opts->window_minimized) {
+ update_minimized_state(w32);
+ } else if (changed_option == &vo_opts->window_maximized) {
+ update_maximized_state(w32);
+ } else if (changed_option == &vo_opts->window_corners) {
+ update_corners_pref(w32);
+ }
+ }
+
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_WINDOW_ID: {
+ if (!w32->window)
+ return VO_NOTAVAIL;
+ *(int64_t *)arg = (intptr_t)w32->window;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_HIDPI_SCALE: {
+ *(double *)arg = w32->dpi_scale;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_UNFS_WINDOW_SIZE: {
+ int *s = arg;
+
+ if (!w32->window_bounds_initialized)
+ return VO_FALSE;
+
+ RECT *rc = w32->current_fs ? &w32->prev_windowrc : &w32->windowrc;
+ s[0] = rect_w(*rc) / w32->dpi_scale;
+ s[1] = rect_h(*rc) / w32->dpi_scale;
+ return VO_TRUE;
+ }
+ case VOCTRL_SET_UNFS_WINDOW_SIZE: {
+ int *s = arg;
+
+ if (!w32->window_bounds_initialized)
+ return VO_FALSE;
+
+ s[0] *= w32->dpi_scale;
+ s[1] *= w32->dpi_scale;
+
+ RECT *rc = w32->current_fs ? &w32->prev_windowrc : &w32->windowrc;
+ resize_and_move_rect(w32, rc, s[0], s[1]);
+
+ w32->fit_on_screen = true;
+ reinit_window_state(w32);
+ return VO_TRUE;
+ }
+ case VOCTRL_SET_CURSOR_VISIBILITY:
+ w32->cursor_visible = *(bool *)arg;
+
+ if (w32->can_set_cursor && w32->tracking) {
+ if (w32->cursor_visible)
+ SetCursor(LoadCursor(NULL, IDC_ARROW));
+ else
+ SetCursor(NULL);
+ }
+ return VO_TRUE;
+ case VOCTRL_KILL_SCREENSAVER:
+ w32->disable_screensaver = true;
+ SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED |
+ ES_DISPLAY_REQUIRED);
+ return VO_TRUE;
+ case VOCTRL_RESTORE_SCREENSAVER:
+ w32->disable_screensaver = false;
+ SetThreadExecutionState(ES_CONTINUOUS);
+ return VO_TRUE;
+ case VOCTRL_UPDATE_WINDOW_TITLE: {
+ wchar_t *title = mp_from_utf8(NULL, (char *)arg);
+ SetWindowTextW(w32->window, title);
+ talloc_free(title);
+ return VO_TRUE;
+ }
+ case VOCTRL_UPDATE_PLAYBACK_STATE: {
+ w32->current_pstate = *(struct voctrl_playback_state *)arg;
+
+ update_playback_state(w32);
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_FPS:
+ update_display_info(w32);
+ *(double*) arg = w32->display_fps;
+ return VO_TRUE;
+ case VOCTRL_GET_DISPLAY_RES: ;
+ RECT monrc = get_monitor_info(w32).rcMonitor;
+ ((int *)arg)[0] = monrc.right - monrc.left;
+ ((int *)arg)[1] = monrc.bottom - monrc.top;
+ return VO_TRUE;
+ case VOCTRL_GET_DISPLAY_NAMES:
+ *(char ***)arg = get_disp_names(w32);
+ return VO_TRUE;
+ case VOCTRL_GET_ICC_PROFILE:
+ update_display_info(w32);
+ if (w32->color_profile) {
+ bstr *p = arg;
+ *p = stream_read_file(w32->color_profile, NULL,
+ w32->vo->global, 100000000); // 100 MB
+ return p->len ? VO_TRUE : VO_FALSE;
+ }
+ return VO_FALSE;
+ case VOCTRL_GET_FOCUSED:
+ *(bool *)arg = w32->focused;
+ return VO_TRUE;
+ }
+ return VO_NOTIMPL;
+}
+
+static void do_control(void *ptr)
+{
+ void **p = ptr;
+ struct vo_w32_state *w32 = p[0];
+ int *events = p[1];
+ int request = *(int *)p[2];
+ void *arg = p[3];
+ int *ret = p[4];
+ *ret = gui_thread_control(w32, request, arg);
+ *events |= atomic_fetch_and(&w32->event_flags, 0);
+ // Safe access, since caller (owner of vo) is blocked.
+ if (*events & VO_EVENT_RESIZE) {
+ w32->vo->dwidth = rect_w(w32->windowrc);
+ w32->vo->dheight = rect_h(w32->windowrc);
+ }
+}
+
+int vo_w32_control(struct vo *vo, int *events, int request, void *arg)
+{
+ struct vo_w32_state *w32 = vo->w32;
+ if (request == VOCTRL_CHECK_EVENTS) {
+ *events |= atomic_fetch_and(&w32->event_flags, 0);
+ if (*events & VO_EVENT_RESIZE) {
+ mp_dispatch_lock(w32->dispatch);
+ vo->dwidth = rect_w(w32->windowrc);
+ vo->dheight = rect_h(w32->windowrc);
+ mp_dispatch_unlock(w32->dispatch);
+ }
+ return VO_TRUE;
+ } else {
+ int r;
+ void *p[] = {w32, events, &request, arg, &r};
+ mp_dispatch_run(w32->dispatch, do_control, p);
+ return r;
+ }
+}
+
+static void do_terminate(void *ptr)
+{
+ struct vo_w32_state *w32 = ptr;
+ w32->terminate = true;
+
+ if (!w32->destroyed)
+ DestroyWindow(w32->window);
+
+ mp_dispatch_interrupt(w32->dispatch);
+}
+
+void vo_w32_uninit(struct vo *vo)
+{
+ struct vo_w32_state *w32 = vo->w32;
+ if (!w32)
+ return;
+
+ mp_dispatch_run(w32->dispatch, do_terminate, w32);
+ mp_thread_join(w32->thread);
+
+ AvRevertMmThreadCharacteristics(w32->avrt_handle);
+
+ talloc_free(w32);
+ vo->w32 = NULL;
+}
+
+HWND vo_w32_hwnd(struct vo *vo)
+{
+ struct vo_w32_state *w32 = vo->w32;
+ return w32->window; // immutable, so no synchronization needed
+}
+
+void vo_w32_run_on_thread(struct vo *vo, void (*cb)(void *ctx), void *ctx)
+{
+ struct vo_w32_state *w32 = vo->w32;
+ mp_dispatch_run(w32->dispatch, cb, ctx);
+}
diff --git a/video/out/w32_common.h b/video/out/w32_common.h
new file mode 100644
index 0000000..528b216
--- /dev/null
+++ b/video/out/w32_common.h
@@ -0,0 +1,36 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MPLAYER_W32_COMMON_H
+#define MPLAYER_W32_COMMON_H
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <windows.h>
+
+#include "common/common.h"
+
+struct vo;
+
+bool vo_w32_init(struct vo *vo);
+void vo_w32_uninit(struct vo *vo);
+int vo_w32_control(struct vo *vo, int *events, int request, void *arg);
+void vo_w32_config(struct vo *vo);
+HWND vo_w32_hwnd(struct vo *vo);
+void vo_w32_run_on_thread(struct vo *vo, void (*cb)(void *ctx), void *ctx);
+
+#endif /* MPLAYER_W32_COMMON_H */
diff --git a/video/out/wayland_common.c b/video/out/wayland_common.c
new file mode 100644
index 0000000..589135f
--- /dev/null
+++ b/video/out/wayland_common.c
@@ -0,0 +1,2629 @@
+/*
+ * This file is part of mpv video player.
+ *
+ * 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 <errno.h>
+#include <limits.h>
+#include <linux/input-event-codes.h>
+#include <poll.h>
+#include <time.h>
+#include <unistd.h>
+#include <wayland-cursor.h>
+#include <xkbcommon/xkbcommon.h>
+
+#include "common/msg.h"
+#include "input/input.h"
+#include "input/keycodes.h"
+#include "options/m_config.h"
+#include "osdep/io.h"
+#include "osdep/poll_wrapper.h"
+#include "osdep/timer.h"
+#include "present_sync.h"
+#include "wayland_common.h"
+#include "win_state.h"
+
+// Generated from wayland-protocols
+#include "idle-inhibit-unstable-v1.h"
+#include "linux-dmabuf-unstable-v1.h"
+#include "presentation-time.h"
+#include "xdg-decoration-unstable-v1.h"
+#include "xdg-shell.h"
+#include "viewporter.h"
+
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+#include "content-type-v1.h"
+#include "single-pixel-buffer-v1.h"
+#endif
+
+#if HAVE_WAYLAND_PROTOCOLS_1_31
+#include "fractional-scale-v1.h"
+#endif
+
+#if HAVE_WAYLAND_PROTOCOLS_1_32
+#include "cursor-shape-v1.h"
+#endif
+
+#if WAYLAND_VERSION_MAJOR > 1 || WAYLAND_VERSION_MINOR >= 22
+#define HAVE_WAYLAND_1_22
+#endif
+
+#ifndef CLOCK_MONOTONIC_RAW
+#define CLOCK_MONOTONIC_RAW 4
+#endif
+
+#ifndef XDG_TOPLEVEL_STATE_SUSPENDED
+#define XDG_TOPLEVEL_STATE_SUSPENDED 9
+#endif
+
+
+static const struct mp_keymap keymap[] = {
+ /* Special keys */
+ {XKB_KEY_Pause, MP_KEY_PAUSE}, {XKB_KEY_Escape, MP_KEY_ESC},
+ {XKB_KEY_BackSpace, MP_KEY_BS}, {XKB_KEY_Tab, MP_KEY_TAB},
+ {XKB_KEY_Return, MP_KEY_ENTER}, {XKB_KEY_Menu, MP_KEY_MENU},
+ {XKB_KEY_Print, MP_KEY_PRINT}, {XKB_KEY_ISO_Left_Tab, MP_KEY_TAB},
+
+ /* Cursor keys */
+ {XKB_KEY_Left, MP_KEY_LEFT}, {XKB_KEY_Right, MP_KEY_RIGHT},
+ {XKB_KEY_Up, MP_KEY_UP}, {XKB_KEY_Down, MP_KEY_DOWN},
+
+ /* Navigation keys */
+ {XKB_KEY_Insert, MP_KEY_INSERT}, {XKB_KEY_Delete, MP_KEY_DELETE},
+ {XKB_KEY_Home, MP_KEY_HOME}, {XKB_KEY_End, MP_KEY_END},
+ {XKB_KEY_Page_Up, MP_KEY_PAGE_UP}, {XKB_KEY_Page_Down, MP_KEY_PAGE_DOWN},
+
+ /* F-keys */
+ {XKB_KEY_F1, MP_KEY_F + 1}, {XKB_KEY_F2, MP_KEY_F + 2},
+ {XKB_KEY_F3, MP_KEY_F + 3}, {XKB_KEY_F4, MP_KEY_F + 4},
+ {XKB_KEY_F5, MP_KEY_F + 5}, {XKB_KEY_F6, MP_KEY_F + 6},
+ {XKB_KEY_F7, MP_KEY_F + 7}, {XKB_KEY_F8, MP_KEY_F + 8},
+ {XKB_KEY_F9, MP_KEY_F + 9}, {XKB_KEY_F10, MP_KEY_F +10},
+ {XKB_KEY_F11, MP_KEY_F +11}, {XKB_KEY_F12, MP_KEY_F +12},
+ {XKB_KEY_F13, MP_KEY_F +13}, {XKB_KEY_F14, MP_KEY_F +14},
+ {XKB_KEY_F15, MP_KEY_F +15}, {XKB_KEY_F16, MP_KEY_F +16},
+ {XKB_KEY_F17, MP_KEY_F +17}, {XKB_KEY_F18, MP_KEY_F +18},
+ {XKB_KEY_F19, MP_KEY_F +19}, {XKB_KEY_F20, MP_KEY_F +20},
+ {XKB_KEY_F21, MP_KEY_F +21}, {XKB_KEY_F22, MP_KEY_F +22},
+ {XKB_KEY_F23, MP_KEY_F +23}, {XKB_KEY_F24, MP_KEY_F +24},
+
+ /* Numpad independent of numlock */
+ {XKB_KEY_KP_Subtract, '-'}, {XKB_KEY_KP_Add, '+'},
+ {XKB_KEY_KP_Multiply, '*'}, {XKB_KEY_KP_Divide, '/'},
+ {XKB_KEY_KP_Enter, MP_KEY_KPENTER},
+
+ /* Numpad with numlock */
+ {XKB_KEY_KP_0, MP_KEY_KP0}, {XKB_KEY_KP_1, MP_KEY_KP1},
+ {XKB_KEY_KP_2, MP_KEY_KP2}, {XKB_KEY_KP_3, MP_KEY_KP3},
+ {XKB_KEY_KP_4, MP_KEY_KP4}, {XKB_KEY_KP_5, MP_KEY_KP5},
+ {XKB_KEY_KP_6, MP_KEY_KP6}, {XKB_KEY_KP_7, MP_KEY_KP7},
+ {XKB_KEY_KP_8, MP_KEY_KP8}, {XKB_KEY_KP_9, MP_KEY_KP9},
+ {XKB_KEY_KP_Decimal, MP_KEY_KPDEC}, {XKB_KEY_KP_Separator, MP_KEY_KPDEC},
+
+ /* Numpad without numlock */
+ {XKB_KEY_KP_Insert, MP_KEY_KPINS}, {XKB_KEY_KP_End, MP_KEY_KPEND},
+ {XKB_KEY_KP_Down, MP_KEY_KPDOWN}, {XKB_KEY_KP_Page_Down, MP_KEY_KPPGDOWN},
+ {XKB_KEY_KP_Left, MP_KEY_KPLEFT}, {XKB_KEY_KP_Begin, MP_KEY_KP5},
+ {XKB_KEY_KP_Right, MP_KEY_KPRIGHT}, {XKB_KEY_KP_Home, MP_KEY_KPHOME},
+ {XKB_KEY_KP_Up, MP_KEY_KPUP}, {XKB_KEY_KP_Page_Up, MP_KEY_KPPGUP},
+ {XKB_KEY_KP_Delete, MP_KEY_KPDEL},
+
+ /* Multimedia keys */
+ {XKB_KEY_XF86MenuKB, MP_KEY_MENU},
+ {XKB_KEY_XF86AudioPlay, MP_KEY_PLAY}, {XKB_KEY_XF86AudioPause, MP_KEY_PAUSE},
+ {XKB_KEY_XF86AudioStop, MP_KEY_STOP},
+ {XKB_KEY_XF86AudioPrev, MP_KEY_PREV}, {XKB_KEY_XF86AudioNext, MP_KEY_NEXT},
+ {XKB_KEY_XF86AudioRewind, MP_KEY_REWIND},
+ {XKB_KEY_XF86AudioForward, MP_KEY_FORWARD},
+ {XKB_KEY_XF86AudioMute, MP_KEY_MUTE},
+ {XKB_KEY_XF86AudioLowerVolume, MP_KEY_VOLUME_DOWN},
+ {XKB_KEY_XF86AudioRaiseVolume, MP_KEY_VOLUME_UP},
+ {XKB_KEY_XF86HomePage, MP_KEY_HOMEPAGE}, {XKB_KEY_XF86WWW, MP_KEY_WWW},
+ {XKB_KEY_XF86Mail, MP_KEY_MAIL}, {XKB_KEY_XF86Favorites, MP_KEY_FAVORITES},
+ {XKB_KEY_XF86Search, MP_KEY_SEARCH}, {XKB_KEY_XF86Sleep, MP_KEY_SLEEP},
+ {XKB_KEY_XF86Back, MP_KEY_BACK}, {XKB_KEY_XF86Tools, MP_KEY_TOOLS},
+ {XKB_KEY_XF86ZoomIn, MP_KEY_ZOOMIN}, {XKB_KEY_XF86ZoomOut, MP_KEY_ZOOMOUT},
+
+ {0, 0}
+};
+
+#define OPT_BASE_STRUCT struct wayland_opts
+const struct m_sub_options wayland_conf = {
+ .opts = (const struct m_option[]) {
+ {"wayland-configure-bounds", OPT_CHOICE(configure_bounds,
+ {"auto", -1}, {"no", 0}, {"yes", 1})},
+ {"wayland-disable-vsync", OPT_BOOL(disable_vsync)},
+ {"wayland-edge-pixels-pointer", OPT_INT(edge_pixels_pointer),
+ M_RANGE(0, INT_MAX)},
+ {"wayland-edge-pixels-touch", OPT_INT(edge_pixels_touch),
+ M_RANGE(0, INT_MAX)},
+ {0},
+ },
+ .size = sizeof(struct wayland_opts),
+ .defaults = &(struct wayland_opts) {
+ .configure_bounds = -1,
+ .edge_pixels_pointer = 16,
+ .edge_pixels_touch = 32,
+ },
+};
+
+struct vo_wayland_feedback_pool {
+ struct wp_presentation_feedback **fback;
+ struct vo_wayland_state *wl;
+ int len;
+};
+
+struct vo_wayland_output {
+ struct vo_wayland_state *wl;
+ struct wl_output *output;
+ struct mp_rect geometry;
+ bool has_surface;
+ uint32_t id;
+ uint32_t flags;
+ int phys_width;
+ int phys_height;
+ int scale;
+ double refresh_rate;
+ char *make;
+ char *model;
+ char *name;
+ struct wl_list link;
+};
+
+static int check_for_resize(struct vo_wayland_state *wl, int edge_pixels,
+ enum xdg_toplevel_resize_edge *edge);
+static int get_mods(struct vo_wayland_state *wl);
+static int lookupkey(int key);
+static int set_cursor_visibility(struct vo_wayland_state *wl, bool on);
+static int spawn_cursor(struct vo_wayland_state *wl);
+
+static void add_feedback(struct vo_wayland_feedback_pool *fback_pool,
+ struct wp_presentation_feedback *fback);
+static void get_shape_device(struct vo_wayland_state *wl);
+static int greatest_common_divisor(int a, int b);
+static void guess_focus(struct vo_wayland_state *wl);
+static void prepare_resize(struct vo_wayland_state *wl, int width, int height);
+static void remove_feedback(struct vo_wayland_feedback_pool *fback_pool,
+ struct wp_presentation_feedback *fback);
+static void remove_output(struct vo_wayland_output *out);
+static void request_decoration_mode(struct vo_wayland_state *wl, uint32_t mode);
+static void rescale_geometry(struct vo_wayland_state *wl, double old_scale);
+static void set_geometry(struct vo_wayland_state *wl, bool resize);
+static void set_surface_scaling(struct vo_wayland_state *wl);
+static void window_move(struct vo_wayland_state *wl, uint32_t serial);
+
+/* Wayland listener boilerplate */
+static void pointer_handle_enter(void *data, struct wl_pointer *pointer,
+ uint32_t serial, struct wl_surface *surface,
+ wl_fixed_t sx, wl_fixed_t sy)
+{
+ struct vo_wayland_state *wl = data;
+
+ wl->pointer = pointer;
+ wl->pointer_id = serial;
+
+ set_cursor_visibility(wl, wl->cursor_visible);
+ mp_input_put_key(wl->vo->input_ctx, MP_KEY_MOUSE_ENTER);
+}
+
+static void pointer_handle_leave(void *data, struct wl_pointer *pointer,
+ uint32_t serial, struct wl_surface *surface)
+{
+ struct vo_wayland_state *wl = data;
+ mp_input_put_key(wl->vo->input_ctx, MP_KEY_MOUSE_LEAVE);
+}
+
+static void pointer_handle_motion(void *data, struct wl_pointer *pointer,
+ uint32_t time, wl_fixed_t sx, wl_fixed_t sy)
+{
+ struct vo_wayland_state *wl = data;
+
+ wl->mouse_x = wl_fixed_to_int(sx) * wl->scaling;
+ wl->mouse_y = wl_fixed_to_int(sy) * wl->scaling;
+
+ if (!wl->toplevel_configured)
+ mp_input_set_mouse_pos(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y);
+ wl->toplevel_configured = false;
+}
+
+static void pointer_handle_button(void *data, struct wl_pointer *wl_pointer,
+ uint32_t serial, uint32_t time, uint32_t button,
+ uint32_t state)
+{
+ struct vo_wayland_state *wl = data;
+ state = state == WL_POINTER_BUTTON_STATE_PRESSED ? MP_KEY_STATE_DOWN
+ : MP_KEY_STATE_UP;
+
+ if (button >= BTN_MOUSE && button < BTN_JOYSTICK) {
+ switch (button) {
+ case BTN_LEFT:
+ button = MP_MBTN_LEFT;
+ break;
+ case BTN_MIDDLE:
+ button = MP_MBTN_MID;
+ break;
+ case BTN_RIGHT:
+ button = MP_MBTN_RIGHT;
+ break;
+ case BTN_SIDE:
+ button = MP_MBTN_BACK;
+ break;
+ case BTN_EXTRA:
+ button = MP_MBTN_FORWARD;
+ break;
+ default:
+ button += MP_MBTN9 - BTN_FORWARD;
+ break;
+ }
+ } else {
+ button = 0;
+ }
+
+ if (button)
+ mp_input_put_key(wl->vo->input_ctx, button | state | wl->mpmod);
+
+ if (!mp_input_test_dragging(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y) &&
+ !wl->locked_size && (button == MP_MBTN_LEFT) && (state == MP_KEY_STATE_DOWN))
+ {
+ uint32_t edges;
+ // Implement an edge resize zone if there are no decorations
+ if (!wl->vo_opts->border && check_for_resize(wl, wl->opts->edge_pixels_pointer, &edges)) {
+ xdg_toplevel_resize(wl->xdg_toplevel, wl->seat, serial, edges);
+ } else {
+ window_move(wl, serial);
+ }
+ // Explicitly send an UP event after the client finishes a move/resize
+ mp_input_put_key(wl->vo->input_ctx, button | MP_KEY_STATE_UP);
+ }
+}
+
+static void pointer_handle_axis(void *data, struct wl_pointer *wl_pointer,
+ uint32_t time, uint32_t axis, wl_fixed_t value)
+{
+ struct vo_wayland_state *wl = data;
+
+ double val = wl_fixed_to_double(value) < 0 ? -1 : 1;
+ switch (axis) {
+ case WL_POINTER_AXIS_VERTICAL_SCROLL:
+ if (value > 0)
+ mp_input_put_wheel(wl->vo->input_ctx, MP_WHEEL_DOWN | wl->mpmod, +val);
+ if (value < 0)
+ mp_input_put_wheel(wl->vo->input_ctx, MP_WHEEL_UP | wl->mpmod, -val);
+ break;
+ case WL_POINTER_AXIS_HORIZONTAL_SCROLL:
+ if (value > 0)
+ mp_input_put_wheel(wl->vo->input_ctx, MP_WHEEL_RIGHT | wl->mpmod, +val);
+ if (value < 0)
+ mp_input_put_wheel(wl->vo->input_ctx, MP_WHEEL_LEFT | wl->mpmod, -val);
+ break;
+ }
+}
+
+static const struct wl_pointer_listener pointer_listener = {
+ pointer_handle_enter,
+ pointer_handle_leave,
+ pointer_handle_motion,
+ pointer_handle_button,
+ pointer_handle_axis,
+};
+
+static void touch_handle_down(void *data, struct wl_touch *wl_touch,
+ uint32_t serial, uint32_t time, struct wl_surface *surface,
+ int32_t id, wl_fixed_t x_w, wl_fixed_t y_w)
+{
+ struct vo_wayland_state *wl = data;
+ wl->mouse_x = wl_fixed_to_int(x_w) * wl->scaling;
+ wl->mouse_y = wl_fixed_to_int(y_w) * wl->scaling;
+
+ enum xdg_toplevel_resize_edge edge;
+ if (!mp_input_test_dragging(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y)) {
+ if (check_for_resize(wl, wl->opts->edge_pixels_touch, &edge)) {
+ xdg_toplevel_resize(wl->xdg_toplevel, wl->seat, serial, edge);
+ } else {
+ xdg_toplevel_move(wl->xdg_toplevel, wl->seat, serial);
+ }
+ }
+
+ mp_input_set_mouse_pos(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y);
+ mp_input_put_key(wl->vo->input_ctx, MP_MBTN_LEFT | MP_KEY_STATE_DOWN);
+}
+
+static void touch_handle_up(void *data, struct wl_touch *wl_touch,
+ uint32_t serial, uint32_t time, int32_t id)
+{
+ struct vo_wayland_state *wl = data;
+ mp_input_put_key(wl->vo->input_ctx, MP_MBTN_LEFT | MP_KEY_STATE_UP);
+}
+
+static void touch_handle_motion(void *data, struct wl_touch *wl_touch,
+ uint32_t time, int32_t id, wl_fixed_t x_w, wl_fixed_t y_w)
+{
+ struct vo_wayland_state *wl = data;
+
+ wl->mouse_x = wl_fixed_to_int(x_w) * wl->scaling;
+ wl->mouse_y = wl_fixed_to_int(y_w) * wl->scaling;
+
+ mp_input_set_mouse_pos(wl->vo->input_ctx, wl->mouse_x, wl->mouse_y);
+}
+
+static void touch_handle_frame(void *data, struct wl_touch *wl_touch)
+{
+}
+
+static void touch_handle_cancel(void *data, struct wl_touch *wl_touch)
+{
+}
+
+static const struct wl_touch_listener touch_listener = {
+ touch_handle_down,
+ touch_handle_up,
+ touch_handle_motion,
+ touch_handle_frame,
+ touch_handle_cancel,
+};
+
+static void keyboard_handle_keymap(void *data, struct wl_keyboard *wl_keyboard,
+ uint32_t format, int32_t fd, uint32_t size)
+{
+ struct vo_wayland_state *wl = data;
+ char *map_str;
+
+ if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
+ close(fd);
+ return;
+ }
+
+ map_str = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
+ if (map_str == MAP_FAILED) {
+ close(fd);
+ return;
+ }
+
+ wl->xkb_keymap = xkb_keymap_new_from_buffer(wl->xkb_context, map_str,
+ strnlen(map_str, size),
+ XKB_KEYMAP_FORMAT_TEXT_V1, 0);
+
+ munmap(map_str, size);
+ close(fd);
+
+ if (!wl->xkb_keymap) {
+ MP_ERR(wl, "failed to compile keymap\n");
+ return;
+ }
+
+ wl->xkb_state = xkb_state_new(wl->xkb_keymap);
+ if (!wl->xkb_state) {
+ MP_ERR(wl, "failed to create XKB state\n");
+ xkb_keymap_unref(wl->xkb_keymap);
+ wl->xkb_keymap = NULL;
+ return;
+ }
+}
+
+static void keyboard_handle_enter(void *data, struct wl_keyboard *wl_keyboard,
+ uint32_t serial, struct wl_surface *surface,
+ struct wl_array *keys)
+{
+ struct vo_wayland_state *wl = data;
+ wl->has_keyboard_input = true;
+ guess_focus(wl);
+}
+
+static void keyboard_handle_leave(void *data, struct wl_keyboard *wl_keyboard,
+ uint32_t serial, struct wl_surface *surface)
+{
+ struct vo_wayland_state *wl = data;
+ wl->has_keyboard_input = false;
+ wl->keyboard_code = 0;
+ wl->mpkey = 0;
+ wl->mpmod = 0;
+ mp_input_put_key(wl->vo->input_ctx, MP_INPUT_RELEASE_ALL);
+ guess_focus(wl);
+}
+
+static void keyboard_handle_key(void *data, struct wl_keyboard *wl_keyboard,
+ uint32_t serial, uint32_t time, uint32_t key,
+ uint32_t state)
+{
+ struct vo_wayland_state *wl = data;
+
+ wl->keyboard_code = key + 8;
+ xkb_keysym_t sym = xkb_state_key_get_one_sym(wl->xkb_state, wl->keyboard_code);
+ int mpkey = lookupkey(sym);
+
+ state = state == WL_KEYBOARD_KEY_STATE_PRESSED ? MP_KEY_STATE_DOWN
+ : MP_KEY_STATE_UP;
+
+ if (mpkey) {
+ mp_input_put_key(wl->vo->input_ctx, mpkey | state | wl->mpmod);
+ } else {
+ char s[128];
+ if (xkb_keysym_to_utf8(sym, s, sizeof(s)) > 0) {
+ mp_input_put_key_utf8(wl->vo->input_ctx, state | wl->mpmod, bstr0(s));
+ } else {
+ // Assume a modifier was pressed and handle it in the mod event instead.
+ return;
+ }
+ }
+ if (state == MP_KEY_STATE_DOWN)
+ wl->mpkey = mpkey;
+ if (mpkey && state == MP_KEY_STATE_UP)
+ wl->mpkey = 0;
+}
+
+static void keyboard_handle_modifiers(void *data, struct wl_keyboard *wl_keyboard,
+ uint32_t serial, uint32_t mods_depressed,
+ uint32_t mods_latched, uint32_t mods_locked,
+ uint32_t group)
+{
+ struct vo_wayland_state *wl = data;
+
+ if (wl->xkb_state) {
+ xkb_state_update_mask(wl->xkb_state, mods_depressed, mods_latched,
+ mods_locked, 0, 0, group);
+ wl->mpmod = get_mods(wl);
+ if (wl->mpkey)
+ mp_input_put_key(wl->vo->input_ctx, wl->mpkey | MP_KEY_STATE_DOWN | wl->mpmod);
+ }
+}
+
+static void keyboard_handle_repeat_info(void *data, struct wl_keyboard *wl_keyboard,
+ int32_t rate, int32_t delay)
+{
+ struct vo_wayland_state *wl = data;
+ if (wl->vo_opts->native_keyrepeat)
+ mp_input_set_repeat_info(wl->vo->input_ctx, rate, delay);
+}
+
+static const struct wl_keyboard_listener keyboard_listener = {
+ keyboard_handle_keymap,
+ keyboard_handle_enter,
+ keyboard_handle_leave,
+ keyboard_handle_key,
+ keyboard_handle_modifiers,
+ keyboard_handle_repeat_info,
+};
+
+static void seat_handle_caps(void *data, struct wl_seat *seat,
+ enum wl_seat_capability caps)
+{
+ struct vo_wayland_state *wl = data;
+
+ if ((caps & WL_SEAT_CAPABILITY_POINTER) && !wl->pointer) {
+ wl->pointer = wl_seat_get_pointer(seat);
+ get_shape_device(wl);
+ wl_pointer_add_listener(wl->pointer, &pointer_listener, wl);
+ } else if (!(caps & WL_SEAT_CAPABILITY_POINTER) && wl->pointer) {
+ wl_pointer_destroy(wl->pointer);
+ wl->pointer = NULL;
+ }
+
+ if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !wl->keyboard) {
+ wl->keyboard = wl_seat_get_keyboard(seat);
+ wl_keyboard_add_listener(wl->keyboard, &keyboard_listener, wl);
+ } else if (!(caps & WL_SEAT_CAPABILITY_KEYBOARD) && wl->keyboard) {
+ wl_keyboard_destroy(wl->keyboard);
+ wl->keyboard = NULL;
+ }
+
+ if ((caps & WL_SEAT_CAPABILITY_TOUCH) && !wl->touch) {
+ wl->touch = wl_seat_get_touch(seat);
+ wl_touch_set_user_data(wl->touch, wl);
+ wl_touch_add_listener(wl->touch, &touch_listener, wl);
+ } else if (!(caps & WL_SEAT_CAPABILITY_TOUCH) && wl->touch) {
+ wl_touch_destroy(wl->touch);
+ wl->touch = NULL;
+ }
+}
+
+static const struct wl_seat_listener seat_listener = {
+ seat_handle_caps,
+};
+
+static void data_offer_handle_offer(void *data, struct wl_data_offer *offer,
+ const char *mime_type)
+{
+ struct vo_wayland_state *wl = data;
+ int score = mp_event_get_mime_type_score(wl->vo->input_ctx, mime_type);
+ if (score > wl->dnd_mime_score && wl->vo_opts->drag_and_drop != -2) {
+ wl->dnd_mime_score = score;
+ if (wl->dnd_mime_type)
+ talloc_free(wl->dnd_mime_type);
+ wl->dnd_mime_type = talloc_strdup(wl, mime_type);
+ MP_VERBOSE(wl, "Given DND offer with mime type %s\n", wl->dnd_mime_type);
+ }
+}
+
+static void data_offer_source_actions(void *data, struct wl_data_offer *offer, uint32_t source_actions)
+{
+}
+
+static void data_offer_action(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action)
+{
+ struct vo_wayland_state *wl = data;
+ if (dnd_action && wl->vo_opts->drag_and_drop != -2) {
+ if (wl->vo_opts->drag_and_drop >= 0) {
+ wl->dnd_action = wl->vo_opts->drag_and_drop;
+ } else {
+ wl->dnd_action = dnd_action & WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY ?
+ DND_REPLACE : DND_APPEND;
+ }
+ MP_VERBOSE(wl, "DND action is %s\n",
+ wl->dnd_action == DND_REPLACE ? "DND_REPLACE" : "DND_APPEND");
+ }
+}
+
+static const struct wl_data_offer_listener data_offer_listener = {
+ data_offer_handle_offer,
+ data_offer_source_actions,
+ data_offer_action,
+};
+
+static void data_device_handle_data_offer(void *data, struct wl_data_device *wl_ddev,
+ struct wl_data_offer *id)
+{
+ struct vo_wayland_state *wl = data;
+ if (wl->dnd_offer)
+ wl_data_offer_destroy(wl->dnd_offer);
+
+ wl->dnd_offer = id;
+ wl_data_offer_add_listener(id, &data_offer_listener, wl);
+}
+
+static void data_device_handle_enter(void *data, struct wl_data_device *wl_ddev,
+ uint32_t serial, struct wl_surface *surface,
+ wl_fixed_t x, wl_fixed_t y,
+ struct wl_data_offer *id)
+{
+ struct vo_wayland_state *wl = data;
+ if (wl->dnd_offer != id) {
+ MP_FATAL(wl, "DND offer ID mismatch!\n");
+ return;
+ }
+
+ if (wl->vo_opts->drag_and_drop != -2) {
+ wl_data_offer_set_actions(id, WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY |
+ WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE,
+ WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY);
+ wl_data_offer_accept(id, serial, wl->dnd_mime_type);
+ MP_VERBOSE(wl, "Accepting DND offer with mime type %s\n", wl->dnd_mime_type);
+ }
+
+}
+
+static void data_device_handle_leave(void *data, struct wl_data_device *wl_ddev)
+{
+ struct vo_wayland_state *wl = data;
+
+ if (wl->dnd_offer) {
+ if (wl->dnd_fd != -1)
+ return;
+ wl_data_offer_destroy(wl->dnd_offer);
+ wl->dnd_offer = NULL;
+ }
+
+ if (wl->vo_opts->drag_and_drop != -2) {
+ MP_VERBOSE(wl, "Releasing DND offer with mime type %s\n", wl->dnd_mime_type);
+ if (wl->dnd_mime_type)
+ TA_FREEP(&wl->dnd_mime_type);
+ wl->dnd_mime_score = 0;
+ }
+}
+
+static void data_device_handle_motion(void *data, struct wl_data_device *wl_ddev,
+ uint32_t time, wl_fixed_t x, wl_fixed_t y)
+{
+ struct vo_wayland_state *wl = data;
+ wl_data_offer_accept(wl->dnd_offer, time, wl->dnd_mime_type);
+}
+
+static void data_device_handle_drop(void *data, struct wl_data_device *wl_ddev)
+{
+ struct vo_wayland_state *wl = data;
+
+ int pipefd[2];
+
+ if (pipe2(pipefd, O_CLOEXEC) == -1) {
+ MP_ERR(wl, "Failed to create dnd pipe!\n");
+ return;
+ }
+
+ if (wl->vo_opts->drag_and_drop != -2) {
+ MP_VERBOSE(wl, "Receiving DND offer with mime %s\n", wl->dnd_mime_type);
+ wl_data_offer_receive(wl->dnd_offer, wl->dnd_mime_type, pipefd[1]);
+ }
+
+ close(pipefd[1]);
+ wl->dnd_fd = pipefd[0];
+}
+
+static void data_device_handle_selection(void *data, struct wl_data_device *wl_ddev,
+ struct wl_data_offer *id)
+{
+ struct vo_wayland_state *wl = data;
+
+ if (wl->dnd_offer) {
+ wl_data_offer_destroy(wl->dnd_offer);
+ wl->dnd_offer = NULL;
+ MP_VERBOSE(wl, "Received a new DND offer. Releasing the previous offer.\n");
+ }
+
+}
+
+static const struct wl_data_device_listener data_device_listener = {
+ data_device_handle_data_offer,
+ data_device_handle_enter,
+ data_device_handle_leave,
+ data_device_handle_motion,
+ data_device_handle_drop,
+ data_device_handle_selection,
+};
+
+static void output_handle_geometry(void *data, struct wl_output *wl_output,
+ int32_t x, int32_t y, int32_t phys_width,
+ int32_t phys_height, int32_t subpixel,
+ const char *make, const char *model,
+ int32_t transform)
+{
+ struct vo_wayland_output *output = data;
+ output->make = talloc_strdup(output->wl, make);
+ output->model = talloc_strdup(output->wl, model);
+ output->geometry.x0 = x;
+ output->geometry.y0 = y;
+ output->phys_width = phys_width;
+ output->phys_height = phys_height;
+}
+
+static void output_handle_mode(void *data, struct wl_output *wl_output,
+ uint32_t flags, int32_t width,
+ int32_t height, int32_t refresh)
+{
+ struct vo_wayland_output *output = data;
+
+ /* Only save current mode */
+ if (!(flags & WL_OUTPUT_MODE_CURRENT))
+ return;
+
+ output->geometry.x1 = width;
+ output->geometry.y1 = height;
+ output->flags = flags;
+ output->refresh_rate = (double)refresh * 0.001;
+}
+
+static void output_handle_done(void *data, struct wl_output *wl_output)
+{
+ struct vo_wayland_output *o = data;
+ struct vo_wayland_state *wl = o->wl;
+
+ o->geometry.x1 += o->geometry.x0;
+ o->geometry.y1 += o->geometry.y0;
+
+ MP_VERBOSE(o->wl, "Registered output %s %s (0x%x):\n"
+ "\tx: %dpx, y: %dpx\n"
+ "\tw: %dpx (%dmm), h: %dpx (%dmm)\n"
+ "\tscale: %d\n"
+ "\tHz: %f\n", o->make, o->model, o->id, o->geometry.x0,
+ o->geometry.y0, mp_rect_w(o->geometry), o->phys_width,
+ mp_rect_h(o->geometry), o->phys_height, o->scale, o->refresh_rate);
+
+ /* If we satisfy this conditional, something about the current
+ * output must have changed (resolution, scale, etc). All window
+ * geometry and scaling should be recalculated. */
+ if (wl->current_output && wl->current_output->output == wl_output) {
+ set_surface_scaling(wl);
+ spawn_cursor(wl);
+ set_geometry(wl, false);
+ prepare_resize(wl, 0, 0);
+ wl->pending_vo_events |= VO_EVENT_DPI;
+ }
+
+ wl->pending_vo_events |= VO_EVENT_WIN_STATE;
+}
+
+static void output_handle_scale(void *data, struct wl_output *wl_output,
+ int32_t factor)
+{
+ struct vo_wayland_output *output = data;
+ if (!factor) {
+ MP_ERR(output->wl, "Invalid output scale given by the compositor!\n");
+ return;
+ }
+ output->scale = factor;
+}
+
+static void output_handle_name(void *data, struct wl_output *wl_output,
+ const char *name)
+{
+ struct vo_wayland_output *output = data;
+ output->name = talloc_strdup(output->wl, name);
+}
+
+static void output_handle_description(void *data, struct wl_output *wl_output,
+ const char *description)
+{
+}
+
+static const struct wl_output_listener output_listener = {
+ output_handle_geometry,
+ output_handle_mode,
+ output_handle_done,
+ output_handle_scale,
+ output_handle_name,
+ output_handle_description,
+};
+
+static void surface_handle_enter(void *data, struct wl_surface *wl_surface,
+ struct wl_output *output)
+{
+ struct vo_wayland_state *wl = data;
+ if (!wl->current_output)
+ return;
+
+ struct mp_rect old_output_geometry = wl->current_output->geometry;
+ struct mp_rect old_geometry = wl->geometry;
+ wl->current_output = NULL;
+
+ struct vo_wayland_output *o;
+ wl_list_for_each(o, &wl->output_list, link) {
+ if (o->output == output) {
+ wl->current_output = o;
+ break;
+ }
+ }
+
+ wl->current_output->has_surface = true;
+ bool force_resize = false;
+
+ if (!wl->fractional_scale_manager && wl_surface_get_version(wl_surface) < 6 &&
+ wl->scaling != wl->current_output->scale)
+ {
+ set_surface_scaling(wl);
+ spawn_cursor(wl);
+ force_resize = true;
+ wl->pending_vo_events |= VO_EVENT_DPI;
+ }
+
+ if (!mp_rect_equals(&old_output_geometry, &wl->current_output->geometry)) {
+ set_geometry(wl, false);
+ force_resize = true;
+ }
+
+ if (!mp_rect_equals(&old_geometry, &wl->geometry) || force_resize)
+ prepare_resize(wl, 0, 0);
+
+ MP_VERBOSE(wl, "Surface entered output %s %s (0x%x), scale = %f, refresh rate = %f Hz\n",
+ o->make, o->model, o->id, wl->scaling, o->refresh_rate);
+
+ wl->pending_vo_events |= VO_EVENT_WIN_STATE;
+}
+
+static void surface_handle_leave(void *data, struct wl_surface *wl_surface,
+ struct wl_output *output)
+{
+ struct vo_wayland_state *wl = data;
+
+ struct vo_wayland_output *o;
+ wl_list_for_each(o, &wl->output_list, link) {
+ if (o->output == output) {
+ o->has_surface = false;
+ wl->pending_vo_events |= VO_EVENT_WIN_STATE;
+ return;
+ }
+ }
+}
+
+#ifdef HAVE_WAYLAND_1_22
+static void surface_handle_preferred_buffer_scale(void *data,
+ struct wl_surface *wl_surface,
+ int32_t scale)
+{
+ struct vo_wayland_state *wl = data;
+ double old_scale = wl->scaling;
+
+ if (wl->fractional_scale_manager)
+ return;
+
+ // dmabuf_wayland is always wl->scaling = 1
+ wl->scaling = !wl->using_dmabuf_wayland ? scale : 1;
+ MP_VERBOSE(wl, "Obtained preferred scale, %f, from the compositor.\n",
+ wl->scaling);
+ wl->pending_vo_events |= VO_EVENT_DPI;
+ if (wl->current_output) {
+ rescale_geometry(wl, old_scale);
+ set_geometry(wl, false);
+ prepare_resize(wl, 0, 0);
+ }
+}
+
+static void surface_handle_preferred_buffer_transform(void *data,
+ struct wl_surface *wl_surface,
+ uint32_t transform)
+{
+}
+#endif
+
+static const struct wl_surface_listener surface_listener = {
+ surface_handle_enter,
+ surface_handle_leave,
+#ifdef HAVE_WAYLAND_1_22
+ surface_handle_preferred_buffer_scale,
+ surface_handle_preferred_buffer_transform,
+#endif
+};
+
+static void xdg_wm_base_ping(void *data, struct xdg_wm_base *wm_base, uint32_t serial)
+{
+ xdg_wm_base_pong(wm_base, serial);
+}
+
+static const struct xdg_wm_base_listener xdg_wm_base_listener = {
+ xdg_wm_base_ping,
+};
+
+static void handle_surface_config(void *data, struct xdg_surface *surface,
+ uint32_t serial)
+{
+ xdg_surface_ack_configure(surface, serial);
+}
+
+static const struct xdg_surface_listener xdg_surface_listener = {
+ handle_surface_config,
+};
+
+static void handle_toplevel_config(void *data, struct xdg_toplevel *toplevel,
+ int32_t width, int32_t height, struct wl_array *states)
+{
+ struct vo_wayland_state *wl = data;
+ struct mp_vo_opts *vo_opts = wl->vo_opts;
+ struct mp_rect old_geometry = wl->geometry;
+
+ int old_toplevel_width = wl->toplevel_width;
+ int old_toplevel_height = wl->toplevel_height;
+ wl->toplevel_width = width;
+ wl->toplevel_height = height;
+
+ if (!wl->configured) {
+ /* Save initial window size if the compositor gives us a hint here. */
+ bool autofit_or_geometry = vo_opts->geometry.wh_valid || vo_opts->autofit.wh_valid ||
+ vo_opts->autofit_larger.wh_valid || vo_opts->autofit_smaller.wh_valid;
+ if (width && height && !autofit_or_geometry) {
+ wl->initial_size_hint = true;
+ wl->window_size = (struct mp_rect){0, 0, width, height};
+ wl->geometry = wl->window_size;
+ }
+ return;
+ }
+
+ bool is_maximized = false;
+ bool is_fullscreen = false;
+ bool is_activated = false;
+ bool is_suspended = false;
+ bool is_tiled = false;
+ enum xdg_toplevel_state *state;
+ wl_array_for_each(state, states) {
+ switch (*state) {
+ case XDG_TOPLEVEL_STATE_FULLSCREEN:
+ is_fullscreen = true;
+ break;
+ case XDG_TOPLEVEL_STATE_RESIZING:
+ break;
+ case XDG_TOPLEVEL_STATE_ACTIVATED:
+ is_activated = true;
+ /*
+ * If we get an ACTIVATED state, we know it cannot be
+ * minimized, but it may not have been minimized
+ * previously, so we can't detect the exact state.
+ */
+ vo_opts->window_minimized = false;
+ m_config_cache_write_opt(wl->vo_opts_cache,
+ &vo_opts->window_minimized);
+ break;
+ case XDG_TOPLEVEL_STATE_TILED_TOP:
+ case XDG_TOPLEVEL_STATE_TILED_LEFT:
+ case XDG_TOPLEVEL_STATE_TILED_RIGHT:
+ case XDG_TOPLEVEL_STATE_TILED_BOTTOM:
+ is_tiled = true;
+ break;
+ case XDG_TOPLEVEL_STATE_MAXIMIZED:
+ is_maximized = true;
+ break;
+ case XDG_TOPLEVEL_STATE_SUSPENDED:
+ is_suspended = true;
+ break;
+ }
+ }
+
+ if (wl->hidden != is_suspended)
+ wl->hidden = is_suspended;
+
+ if (vo_opts->fullscreen != is_fullscreen) {
+ wl->state_change = true;
+ vo_opts->fullscreen = is_fullscreen;
+ m_config_cache_write_opt(wl->vo_opts_cache, &vo_opts->fullscreen);
+ }
+
+ if (vo_opts->window_maximized != is_maximized) {
+ wl->state_change = true;
+ vo_opts->window_maximized = is_maximized;
+ m_config_cache_write_opt(wl->vo_opts_cache, &vo_opts->window_maximized);
+ }
+
+ wl->tiled = is_tiled;
+
+ wl->locked_size = is_fullscreen || is_maximized || is_tiled;
+
+ if (wl->requested_decoration)
+ request_decoration_mode(wl, wl->requested_decoration);
+
+ if (wl->activated != is_activated) {
+ wl->activated = is_activated;
+ guess_focus(wl);
+ /* Just force a redraw to be on the safe side. */
+ if (wl->activated) {
+ wl->hidden = false;
+ wl->pending_vo_events |= VO_EVENT_EXPOSE;
+ }
+ }
+
+ if (wl->state_change) {
+ if (!wl->locked_size) {
+ wl->geometry = wl->window_size;
+ wl->state_change = false;
+ goto resize;
+ }
+ }
+
+ /* Reuse old size if either of these are 0. */
+ if (width == 0 || height == 0) {
+ if (!wl->locked_size) {
+ wl->geometry = wl->window_size;
+ }
+ goto resize;
+ }
+
+ if (old_toplevel_width == wl->toplevel_width &&
+ old_toplevel_height == wl->toplevel_height)
+ return;
+
+ if (!wl->locked_size) {
+ if (vo_opts->keepaspect) {
+ double scale_factor = (double)width / wl->reduced_width;
+ width = ceil(wl->reduced_width * scale_factor);
+ if (vo_opts->keepaspect_window)
+ height = ceil(wl->reduced_height * scale_factor);
+ }
+ wl->window_size.x0 = 0;
+ wl->window_size.y0 = 0;
+ wl->window_size.x1 = round(width * wl->scaling);
+ wl->window_size.y1 = round(height * wl->scaling);
+ }
+ wl->geometry.x0 = 0;
+ wl->geometry.y0 = 0;
+ wl->geometry.x1 = round(width * wl->scaling);
+ wl->geometry.y1 = round(height * wl->scaling);
+
+ if (mp_rect_equals(&old_geometry, &wl->geometry))
+ return;
+
+resize:
+ MP_VERBOSE(wl, "Resizing due to xdg from %ix%i to %ix%i\n",
+ mp_rect_w(old_geometry), mp_rect_h(old_geometry),
+ mp_rect_w(wl->geometry), mp_rect_h(wl->geometry));
+
+ prepare_resize(wl, width, height);
+ wl->toplevel_configured = true;
+}
+
+static void handle_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel)
+{
+ struct vo_wayland_state *wl = data;
+ mp_input_put_key(wl->vo->input_ctx, MP_KEY_CLOSE_WIN);
+}
+
+static void handle_configure_bounds(void *data, struct xdg_toplevel *xdg_toplevel,
+ int32_t width, int32_t height)
+{
+ struct vo_wayland_state *wl = data;
+ wl->bounded_width = width * wl->scaling;
+ wl->bounded_height = height * wl->scaling;
+}
+
+#ifdef XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION
+static void handle_wm_capabilities(void *data, struct xdg_toplevel *xdg_toplevel,
+ struct wl_array *capabilities)
+{
+}
+#endif
+
+static const struct xdg_toplevel_listener xdg_toplevel_listener = {
+ handle_toplevel_config,
+ handle_toplevel_close,
+ handle_configure_bounds,
+#ifdef XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION
+ handle_wm_capabilities,
+#endif
+};
+
+#if HAVE_WAYLAND_PROTOCOLS_1_31
+static void preferred_scale(void *data,
+ struct wp_fractional_scale_v1 *fractional_scale,
+ uint32_t scale)
+{
+ struct vo_wayland_state *wl = data;
+ double old_scale = wl->scaling;
+
+ // dmabuf_wayland is always wl->scaling = 1
+ wl->scaling = !wl->using_dmabuf_wayland ? (double)scale / 120 : 1;
+ MP_VERBOSE(wl, "Obtained preferred scale, %f, from the compositor.\n",
+ wl->scaling);
+ wl->pending_vo_events |= VO_EVENT_DPI;
+ if (wl->current_output) {
+ rescale_geometry(wl, old_scale);
+ set_geometry(wl, false);
+ prepare_resize(wl, 0, 0);
+ }
+}
+
+static const struct wp_fractional_scale_v1_listener fractional_scale_listener = {
+ preferred_scale,
+};
+#endif
+
+static const char *zxdg_decoration_mode_to_str(const uint32_t mode)
+{
+ switch (mode) {
+ case ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE:
+ return "server-side";
+ case ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE:
+ return "client-side";
+ default:
+ return "<unknown>";
+ }
+}
+
+static void configure_decorations(void *data,
+ struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration,
+ uint32_t mode)
+{
+ struct vo_wayland_state *wl = data;
+ struct mp_vo_opts *opts = wl->vo_opts;
+
+ if (wl->requested_decoration && mode != wl->requested_decoration) {
+ MP_DBG(wl,
+ "Requested %s decorations but compositor responded with %s. "
+ "It is likely that compositor wants us to stay in a given mode.\n",
+ zxdg_decoration_mode_to_str(wl->requested_decoration),
+ zxdg_decoration_mode_to_str(mode));
+ }
+
+ wl->requested_decoration = 0;
+
+ if (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE) {
+ MP_VERBOSE(wl, "Enabling server decorations\n");
+ } else {
+ MP_VERBOSE(wl, "Disabling server decorations\n");
+ }
+ opts->border = mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE;
+ m_config_cache_write_opt(wl->vo_opts_cache, &opts->border);
+}
+
+static const struct zxdg_toplevel_decoration_v1_listener decoration_listener = {
+ configure_decorations,
+};
+
+static void pres_set_clockid(void *data, struct wp_presentation *pres,
+ uint32_t clockid)
+{
+ struct vo_wayland_state *wl = data;
+
+ if (clockid == CLOCK_MONOTONIC || clockid == CLOCK_MONOTONIC_RAW)
+ wl->use_present = true;
+}
+
+static const struct wp_presentation_listener pres_listener = {
+ pres_set_clockid,
+};
+
+static void feedback_sync_output(void *data, struct wp_presentation_feedback *fback,
+ struct wl_output *output)
+{
+}
+
+static void feedback_presented(void *data, struct wp_presentation_feedback *fback,
+ uint32_t tv_sec_hi, uint32_t tv_sec_lo,
+ uint32_t tv_nsec, uint32_t refresh_nsec,
+ uint32_t seq_hi, uint32_t seq_lo,
+ uint32_t flags)
+{
+ struct vo_wayland_feedback_pool *fback_pool = data;
+ struct vo_wayland_state *wl = fback_pool->wl;
+
+ if (fback)
+ remove_feedback(fback_pool, fback);
+
+ wl->refresh_interval = (int64_t)refresh_nsec;
+
+ // Very similar to oml_sync_control, in this case we assume that every
+ // time the compositor receives feedback, a buffer swap has been already
+ // been performed.
+ //
+ // Notes:
+ // - tv_sec_lo + tv_sec_hi is the equivalent of oml's ust
+ // - seq_lo + seq_hi is the equivalent of oml's msc
+ // - these values are updated every time the compositor receives feedback.
+
+ int64_t sec = (uint64_t) tv_sec_lo + ((uint64_t) tv_sec_hi << 32);
+ int64_t ust = MP_TIME_S_TO_NS(sec) + (uint64_t) tv_nsec;
+ int64_t msc = (uint64_t) seq_lo + ((uint64_t) seq_hi << 32);
+ present_sync_update_values(wl->present, ust, msc);
+}
+
+static void feedback_discarded(void *data, struct wp_presentation_feedback *fback)
+{
+ struct vo_wayland_feedback_pool *fback_pool = data;
+ if (fback)
+ remove_feedback(fback_pool, fback);
+}
+
+static const struct wp_presentation_feedback_listener feedback_listener = {
+ feedback_sync_output,
+ feedback_presented,
+ feedback_discarded,
+};
+
+static const struct wl_callback_listener frame_listener;
+
+static void frame_callback(void *data, struct wl_callback *callback, uint32_t time)
+{
+ struct vo_wayland_state *wl = data;
+
+ if (callback)
+ wl_callback_destroy(callback);
+
+ wl->frame_callback = wl_surface_frame(wl->callback_surface);
+ wl_callback_add_listener(wl->frame_callback, &frame_listener, wl);
+
+ if (wl->use_present) {
+ struct wp_presentation_feedback *fback = wp_presentation_feedback(wl->presentation, wl->callback_surface);
+ add_feedback(wl->fback_pool, fback);
+ wp_presentation_feedback_add_listener(fback, &feedback_listener, wl->fback_pool);
+ }
+
+ wl->frame_wait = false;
+ wl->hidden = false;
+}
+
+static const struct wl_callback_listener frame_listener = {
+ frame_callback,
+};
+
+static void done(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1)
+{
+}
+
+static void format_table(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1,
+ int32_t fd,
+ uint32_t size)
+{
+ struct vo_wayland_state *wl = data;
+
+ void *map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
+ close(fd);
+
+ if (map != MAP_FAILED) {
+ wl->format_map = map;
+ wl->format_size = size;
+ }
+}
+
+static void main_device(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1,
+ struct wl_array *device)
+{
+}
+
+static void tranche_done(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1)
+{
+}
+
+static void tranche_target_device(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1,
+ struct wl_array *device)
+{
+}
+
+static void tranche_formats(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1,
+ struct wl_array *indices)
+{
+}
+
+static void tranche_flags(void *data,
+ struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1,
+ uint32_t flags)
+{
+}
+
+static const struct zwp_linux_dmabuf_feedback_v1_listener dmabuf_feedback_listener = {
+ done,
+ format_table,
+ main_device,
+ tranche_done,
+ tranche_target_device,
+ tranche_formats,
+ tranche_flags,
+};
+
+static void registry_handle_add(void *data, struct wl_registry *reg, uint32_t id,
+ const char *interface, uint32_t ver)
+{
+ int found = 1;
+ struct vo_wayland_state *wl = data;
+
+ if (!strcmp(interface, wl_compositor_interface.name) && (ver >= 4) && found++) {
+#ifdef HAVE_WAYLAND_1_22
+ ver = MPMIN(ver, 6); /* Cap at 6 in case new events are added later. */
+#else
+ ver = 4;
+#endif
+ wl->compositor = wl_registry_bind(reg, id, &wl_compositor_interface, ver);
+ wl->surface = wl_compositor_create_surface(wl->compositor);
+ wl->video_surface = wl_compositor_create_surface(wl->compositor);
+ wl->osd_surface = wl_compositor_create_surface(wl->compositor);
+
+ /* never accept input events on anything besides the main surface */
+ struct wl_region *region = wl_compositor_create_region(wl->compositor);
+ wl_surface_set_input_region(wl->osd_surface, region);
+ wl_surface_set_input_region(wl->video_surface, region);
+ wl_region_destroy(region);
+
+ wl->cursor_surface = wl_compositor_create_surface(wl->compositor);
+ wl_surface_add_listener(wl->surface, &surface_listener, wl);
+ }
+
+ if (!strcmp(interface, wl_subcompositor_interface.name) && (ver >= 1) && found++) {
+ wl->subcompositor = wl_registry_bind(reg, id, &wl_subcompositor_interface, 1);
+ }
+
+ if (!strcmp (interface, zwp_linux_dmabuf_v1_interface.name) && (ver >= 4) && found++) {
+ wl->dmabuf = wl_registry_bind(reg, id, &zwp_linux_dmabuf_v1_interface, 4);
+ wl->dmabuf_feedback = zwp_linux_dmabuf_v1_get_default_feedback(wl->dmabuf);
+ zwp_linux_dmabuf_feedback_v1_add_listener(wl->dmabuf_feedback, &dmabuf_feedback_listener, wl);
+ }
+
+ if (!strcmp (interface, wp_viewporter_interface.name) && (ver >= 1) && found++) {
+ wl->viewporter = wl_registry_bind (reg, id, &wp_viewporter_interface, 1);
+ }
+
+ if (!strcmp(interface, wl_data_device_manager_interface.name) && (ver >= 3) && found++) {
+ wl->dnd_devman = wl_registry_bind(reg, id, &wl_data_device_manager_interface, 3);
+ }
+
+ if (!strcmp(interface, wl_output_interface.name) && (ver >= 2) && found++) {
+ struct vo_wayland_output *output = talloc_zero(wl, struct vo_wayland_output);
+
+ output->wl = wl;
+ output->id = id;
+ output->scale = 1;
+ output->name = "";
+
+ ver = MPMIN(ver, 4); /* Cap at 4 in case new events are added later. */
+ output->output = wl_registry_bind(reg, id, &wl_output_interface, ver);
+ wl_output_add_listener(output->output, &output_listener, output);
+ wl_list_insert(&wl->output_list, &output->link);
+ }
+
+ if (!strcmp(interface, wl_seat_interface.name) && found++) {
+ wl->seat = wl_registry_bind(reg, id, &wl_seat_interface, 1);
+ wl_seat_add_listener(wl->seat, &seat_listener, wl);
+ }
+
+ if (!strcmp(interface, wl_shm_interface.name) && found++) {
+ wl->shm = wl_registry_bind(reg, id, &wl_shm_interface, 1);
+ }
+
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ if (!strcmp(interface, wp_content_type_manager_v1_interface.name) && found++) {
+ wl->content_type_manager = wl_registry_bind(reg, id, &wp_content_type_manager_v1_interface, 1);
+ }
+
+ if (!strcmp(interface, wp_single_pixel_buffer_manager_v1_interface.name) && found++) {
+ wl->single_pixel_manager = wl_registry_bind(reg, id, &wp_single_pixel_buffer_manager_v1_interface, 1);
+ }
+#endif
+
+#if HAVE_WAYLAND_PROTOCOLS_1_31
+ if (!strcmp(interface, wp_fractional_scale_manager_v1_interface.name) && found++) {
+ wl->fractional_scale_manager = wl_registry_bind(reg, id, &wp_fractional_scale_manager_v1_interface, 1);
+ }
+#endif
+
+#if HAVE_WAYLAND_PROTOCOLS_1_32
+ if (!strcmp(interface, wp_cursor_shape_manager_v1_interface.name) && found++) {
+ wl->cursor_shape_manager = wl_registry_bind(reg, id, &wp_cursor_shape_manager_v1_interface, 1);
+ }
+#endif
+
+ if (!strcmp(interface, wp_presentation_interface.name) && found++) {
+ wl->presentation = wl_registry_bind(reg, id, &wp_presentation_interface, 1);
+ wp_presentation_add_listener(wl->presentation, &pres_listener, wl);
+ }
+
+ if (!strcmp(interface, xdg_wm_base_interface.name) && found++) {
+ ver = MPMIN(ver, 6); /* Cap at 6 in case new events are added later. */
+ wl->wm_base = wl_registry_bind(reg, id, &xdg_wm_base_interface, ver);
+ xdg_wm_base_add_listener(wl->wm_base, &xdg_wm_base_listener, wl);
+ }
+
+ if (!strcmp(interface, zxdg_decoration_manager_v1_interface.name) && found++) {
+ wl->xdg_decoration_manager = wl_registry_bind(reg, id, &zxdg_decoration_manager_v1_interface, 1);
+ }
+
+ if (!strcmp(interface, zwp_idle_inhibit_manager_v1_interface.name) && found++) {
+ wl->idle_inhibit_manager = wl_registry_bind(reg, id, &zwp_idle_inhibit_manager_v1_interface, 1);
+ }
+
+ if (found > 1)
+ MP_VERBOSE(wl, "Registered for protocol %s\n", interface);
+}
+
+static void registry_handle_remove(void *data, struct wl_registry *reg, uint32_t id)
+{
+ struct vo_wayland_state *wl = data;
+ struct vo_wayland_output *output, *tmp;
+ wl_list_for_each_safe(output, tmp, &wl->output_list, link) {
+ if (output->id == id) {
+ remove_output(output);
+ return;
+ }
+ }
+}
+
+static const struct wl_registry_listener registry_listener = {
+ registry_handle_add,
+ registry_handle_remove,
+};
+
+/* Static functions */
+static void check_dnd_fd(struct vo_wayland_state *wl)
+{
+ if (wl->dnd_fd == -1)
+ return;
+
+ struct pollfd fdp = { wl->dnd_fd, POLLIN | POLLHUP, 0 };
+ if (poll(&fdp, 1, 0) <= 0)
+ return;
+
+ if (fdp.revents & POLLIN) {
+ ptrdiff_t offset = 0;
+ size_t data_read = 0;
+ const size_t chunk_size = 1;
+ uint8_t *buffer = ta_zalloc_size(wl, chunk_size);
+ if (!buffer)
+ goto end;
+
+ while ((data_read = read(wl->dnd_fd, buffer + offset, chunk_size)) > 0) {
+ offset += data_read;
+ buffer = ta_realloc_size(wl, buffer, offset + chunk_size);
+ memset(buffer + offset, 0, chunk_size);
+ if (!buffer)
+ goto end;
+ }
+
+ MP_VERBOSE(wl, "Read %td bytes from the DND fd\n", offset);
+
+ struct bstr file_list = bstr0(buffer);
+ mp_event_drop_mime_data(wl->vo->input_ctx, wl->dnd_mime_type,
+ file_list, wl->dnd_action);
+ talloc_free(buffer);
+end:
+ if (wl->dnd_mime_type)
+ talloc_free(wl->dnd_mime_type);
+
+ if (wl->dnd_action >= 0 && wl->dnd_offer)
+ wl_data_offer_finish(wl->dnd_offer);
+
+ wl->dnd_action = -1;
+ wl->dnd_mime_type = NULL;
+ wl->dnd_mime_score = 0;
+ }
+
+ if (fdp.revents & (POLLIN | POLLERR | POLLHUP)) {
+ close(wl->dnd_fd);
+ wl->dnd_fd = -1;
+ }
+}
+
+static int check_for_resize(struct vo_wayland_state *wl, int edge_pixels,
+ enum xdg_toplevel_resize_edge *edge)
+{
+ if (wl->vo_opts->fullscreen || wl->vo_opts->window_maximized)
+ return 0;
+
+ int pos[2] = { wl->mouse_x, wl->mouse_y };
+ int left_edge = pos[0] < edge_pixels;
+ int top_edge = pos[1] < edge_pixels;
+ int right_edge = pos[0] > (mp_rect_w(wl->geometry) - edge_pixels);
+ int bottom_edge = pos[1] > (mp_rect_h(wl->geometry) - edge_pixels);
+
+ if (left_edge) {
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_LEFT;
+ if (top_edge)
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT;
+ else if (bottom_edge)
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT;
+ } else if (right_edge) {
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT;
+ if (top_edge)
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT;
+ else if (bottom_edge)
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT;
+ } else if (top_edge) {
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_TOP;
+ } else if (bottom_edge) {
+ *edge = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM;
+ } else {
+ *edge = 0;
+ return 0;
+ }
+
+ return 1;
+}
+
+static bool create_input(struct vo_wayland_state *wl)
+{
+ wl->xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+
+ if (!wl->xkb_context) {
+ MP_ERR(wl, "failed to initialize input: check xkbcommon\n");
+ return 1;
+ }
+
+ return 0;
+}
+
+static int create_viewports(struct vo_wayland_state *wl)
+{
+ if (wl->viewporter) {
+ wl->viewport = wp_viewporter_get_viewport(wl->viewporter, wl->surface);
+ wl->osd_viewport = wp_viewporter_get_viewport(wl->viewporter, wl->osd_surface);
+ wl->video_viewport = wp_viewporter_get_viewport(wl->viewporter, wl->video_surface);
+ }
+
+ if (wl->viewporter && (!wl->viewport || !wl->osd_viewport || !wl->video_viewport)) {
+ MP_ERR(wl, "failed to create viewport interfaces!\n");
+ return 1;
+ }
+ return 0;
+}
+
+static int create_xdg_surface(struct vo_wayland_state *wl)
+{
+ wl->xdg_surface = xdg_wm_base_get_xdg_surface(wl->wm_base, wl->surface);
+ xdg_surface_add_listener(wl->xdg_surface, &xdg_surface_listener, wl);
+
+ wl->xdg_toplevel = xdg_surface_get_toplevel(wl->xdg_surface);
+ xdg_toplevel_add_listener(wl->xdg_toplevel, &xdg_toplevel_listener, wl);
+
+ if (!wl->xdg_surface || !wl->xdg_toplevel) {
+ MP_ERR(wl, "failed to create xdg_surface and xdg_toplevel!\n");
+ return 1;
+ }
+ return 0;
+}
+
+static void add_feedback(struct vo_wayland_feedback_pool *fback_pool,
+ struct wp_presentation_feedback *fback)
+{
+ for (int i = 0; i < fback_pool->len; ++i) {
+ if (!fback_pool->fback[i]) {
+ fback_pool->fback[i] = fback;
+ break;
+ } else if (i == fback_pool->len - 1) {
+ // Shouldn't happen in practice.
+ wp_presentation_feedback_destroy(fback_pool->fback[i]);
+ fback_pool->fback[i] = fback;
+ }
+ }
+}
+
+static void do_minimize(struct vo_wayland_state *wl)
+{
+ if (!wl->xdg_toplevel)
+ return;
+ if (wl->vo_opts->window_minimized)
+ xdg_toplevel_set_minimized(wl->xdg_toplevel);
+}
+
+static char **get_displays_spanned(struct vo_wayland_state *wl)
+{
+ char **names = NULL;
+ int displays_spanned = 0;
+ struct vo_wayland_output *output;
+ wl_list_for_each(output, &wl->output_list, link) {
+ if (output->has_surface) {
+ char *name = output->name ? output->name : output->model;
+ MP_TARRAY_APPEND(NULL, names, displays_spanned,
+ talloc_strdup(NULL, name));
+ }
+ }
+ MP_TARRAY_APPEND(NULL, names, displays_spanned, NULL);
+ return names;
+}
+
+static int get_mods(struct vo_wayland_state *wl)
+{
+ static char* const mod_names[] = {
+ XKB_MOD_NAME_SHIFT,
+ XKB_MOD_NAME_CTRL,
+ XKB_MOD_NAME_ALT,
+ XKB_MOD_NAME_LOGO,
+ };
+
+ static const int mods[] = {
+ MP_KEY_MODIFIER_SHIFT,
+ MP_KEY_MODIFIER_CTRL,
+ MP_KEY_MODIFIER_ALT,
+ MP_KEY_MODIFIER_META,
+ };
+
+ int modifiers = 0;
+
+ for (int n = 0; n < MP_ARRAY_SIZE(mods); n++) {
+ xkb_mod_index_t index = xkb_keymap_mod_get_index(wl->xkb_keymap, mod_names[n]);
+ if (!xkb_state_mod_index_is_consumed(wl->xkb_state, wl->keyboard_code, index)
+ && xkb_state_mod_index_is_active(wl->xkb_state, index,
+ XKB_STATE_MODS_DEPRESSED))
+ modifiers |= mods[n];
+ }
+ return modifiers;
+}
+
+static void get_shape_device(struct vo_wayland_state *wl)
+{
+#if HAVE_WAYLAND_PROTOCOLS_1_32
+ if (!wl->cursor_shape_device && wl->cursor_shape_manager) {
+ wl->cursor_shape_device = wp_cursor_shape_manager_v1_get_pointer(wl->cursor_shape_manager,
+ wl->pointer);
+ }
+#endif
+}
+
+static int greatest_common_divisor(int a, int b)
+{
+ int rem = a % b;
+ if (rem == 0)
+ return b;
+ return greatest_common_divisor(b, rem);
+}
+
+static void guess_focus(struct vo_wayland_state *wl)
+{
+ // We can't actually know if the window is focused or not in wayland,
+ // so just guess it with some common sense. Obviously won't work if
+ // the user has no keyboard.
+ if ((!wl->focused && wl->activated && wl->has_keyboard_input) ||
+ (wl->focused && !wl->activated))
+ {
+ wl->focused = !wl->focused;
+ wl->pending_vo_events |= VO_EVENT_FOCUS;
+ }
+}
+
+static struct vo_wayland_output *find_output(struct vo_wayland_state *wl)
+{
+ int index = 0;
+ struct mp_vo_opts *opts = wl->vo_opts;
+ int screen_id = opts->fullscreen ? opts->fsscreen_id : opts->screen_id;
+ char *screen_name = opts->fullscreen ? opts->fsscreen_name : opts->screen_name;
+ struct vo_wayland_output *output = NULL;
+ struct vo_wayland_output *fallback_output = NULL;
+ wl_list_for_each(output, &wl->output_list, link) {
+ if (index == 0)
+ fallback_output = output;
+ if (screen_id == -1 && !screen_name)
+ return output;
+ if (screen_id == -1 && screen_name && !strcmp(screen_name, output->name))
+ return output;
+ if (screen_id == -1 && screen_name && !strcmp(screen_name, output->model))
+ return output;
+ if (screen_id == index++)
+ return output;
+ }
+ if (!fallback_output) {
+ MP_ERR(wl, "No screens could be found!\n");
+ return NULL;
+ } else if (screen_id >= 0) {
+ MP_WARN(wl, "Screen index %i not found/unavailable! Falling back to screen 0!\n", screen_id);
+ } else if (screen_name && screen_name[0]) {
+ MP_WARN(wl, "Screen name %s not found/unavailable! Falling back to screen 0!\n", screen_name);
+ }
+ return fallback_output;
+}
+
+static int lookupkey(int key)
+{
+ const char *passthrough_keys = " -+*/<>`~!@#$%^&()_{}:;\"\',.?\\|=[]";
+
+ int mpkey = 0;
+ if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') ||
+ (key >= '0' && key <= '9') ||
+ (key > 0 && key < 256 && strchr(passthrough_keys, key)))
+ mpkey = key;
+
+ if (!mpkey)
+ mpkey = lookup_keymap_table(keymap, key);
+
+ return mpkey;
+}
+
+static void prepare_resize(struct vo_wayland_state *wl, int width, int height)
+{
+ if (!width)
+ width = mp_rect_w(wl->geometry) / wl->scaling;
+ if (!height)
+ height = mp_rect_h(wl->geometry) / wl->scaling;
+ xdg_surface_set_window_geometry(wl->xdg_surface, 0, 0, width, height);
+ wl->pending_vo_events |= VO_EVENT_RESIZE;
+}
+
+static void request_decoration_mode(struct vo_wayland_state *wl, uint32_t mode)
+{
+ wl->requested_decoration = mode;
+ zxdg_toplevel_decoration_v1_set_mode(wl->xdg_toplevel_decoration, mode);
+}
+
+static void rescale_geometry(struct vo_wayland_state *wl, double old_scale)
+{
+ double factor = old_scale / wl->scaling;
+ wl->window_size.x1 /= factor;
+ wl->window_size.y1 /= factor;
+ wl->geometry.x1 /= factor;
+ wl->geometry.y1 /= factor;
+}
+
+static void clean_feedback_pool(struct vo_wayland_feedback_pool *fback_pool)
+{
+ for (int i = 0; i < fback_pool->len; ++i) {
+ if (fback_pool->fback[i]) {
+ wp_presentation_feedback_destroy(fback_pool->fback[i]);
+ fback_pool->fback[i] = NULL;
+ }
+ }
+}
+
+static void remove_feedback(struct vo_wayland_feedback_pool *fback_pool,
+ struct wp_presentation_feedback *fback)
+{
+ for (int i = 0; i < fback_pool->len; ++i) {
+ if (fback_pool->fback[i] == fback) {
+ wp_presentation_feedback_destroy(fback);
+ fback_pool->fback[i] = NULL;
+ break;
+ }
+ }
+}
+
+static void remove_output(struct vo_wayland_output *out)
+{
+ if (!out)
+ return;
+
+ MP_VERBOSE(out->wl, "Deregistering output %s %s (0x%x)\n", out->make,
+ out->model, out->id);
+ wl_list_remove(&out->link);
+ wl_output_destroy(out->output);
+ talloc_free(out->make);
+ talloc_free(out->model);
+ talloc_free(out);
+ return;
+}
+
+static void set_content_type(struct vo_wayland_state *wl)
+{
+ if (!wl->content_type_manager)
+ return;
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ // handle auto;
+ if (wl->vo_opts->content_type == -1) {
+ wp_content_type_v1_set_content_type(wl->content_type, wl->current_content_type);
+ } else {
+ wp_content_type_v1_set_content_type(wl->content_type, wl->vo_opts->content_type);
+ }
+#endif
+}
+
+static void set_cursor_shape(struct vo_wayland_state *wl)
+{
+#if HAVE_WAYLAND_PROTOCOLS_1_32
+ wp_cursor_shape_device_v1_set_shape(wl->cursor_shape_device, wl->pointer_id,
+ WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
+#endif
+}
+
+static int set_cursor_visibility(struct vo_wayland_state *wl, bool on)
+{
+ wl->cursor_visible = on;
+ if (on) {
+ if (wl->cursor_shape_device) {
+ set_cursor_shape(wl);
+ } else {
+ if (spawn_cursor(wl))
+ return VO_FALSE;
+ struct wl_cursor_image *img = wl->default_cursor->images[0];
+ struct wl_buffer *buffer = wl_cursor_image_get_buffer(img);
+ if (!buffer)
+ return VO_FALSE;
+ int scale = MPMAX(wl->scaling, 1);
+ wl_pointer_set_cursor(wl->pointer, wl->pointer_id, wl->cursor_surface,
+ img->hotspot_x / scale, img->hotspot_y / scale);
+ wl_surface_set_buffer_scale(wl->cursor_surface, scale);
+ wl_surface_attach(wl->cursor_surface, buffer, 0, 0);
+ wl_surface_damage_buffer(wl->cursor_surface, 0, 0, img->width, img->height);
+ }
+ wl_surface_commit(wl->cursor_surface);
+ } else {
+ wl_pointer_set_cursor(wl->pointer, wl->pointer_id, NULL, 0, 0);
+ }
+ return VO_TRUE;
+}
+
+static void set_geometry(struct vo_wayland_state *wl, bool resize)
+{
+ struct vo *vo = wl->vo;
+ if (!wl->current_output)
+ return;
+
+ struct vo_win_geometry geo;
+ struct mp_rect screenrc = wl->current_output->geometry;
+ vo_calc_window_geometry2(vo, &screenrc, wl->scaling, &geo);
+ vo_apply_window_geometry(vo, &geo);
+
+ int gcd = greatest_common_divisor(vo->dwidth, vo->dheight);
+ wl->reduced_width = vo->dwidth / gcd;
+ wl->reduced_height = vo->dheight / gcd;
+
+ if (!wl->initial_size_hint)
+ wl->window_size = (struct mp_rect){0, 0, vo->dwidth, vo->dheight};
+ wl->initial_size_hint = false;
+
+ if (resize) {
+ if (!wl->locked_size)
+ wl->geometry = wl->window_size;
+ prepare_resize(wl, 0, 0);
+ }
+}
+
+static void set_input_region(struct vo_wayland_state *wl, bool passthrough)
+{
+ if (passthrough) {
+ struct wl_region *region = wl_compositor_create_region(wl->compositor);
+ wl_surface_set_input_region(wl->surface, region);
+ wl_region_destroy(region);
+ } else {
+ wl_surface_set_input_region(wl->surface, NULL);
+ }
+}
+
+static int set_screensaver_inhibitor(struct vo_wayland_state *wl, int state)
+{
+ if (!wl->idle_inhibit_manager)
+ return VO_NOTIMPL;
+ if (state == (!!wl->idle_inhibitor))
+ return VO_TRUE;
+ if (state) {
+ MP_VERBOSE(wl, "Enabling idle inhibitor\n");
+ struct zwp_idle_inhibit_manager_v1 *mgr = wl->idle_inhibit_manager;
+ wl->idle_inhibitor = zwp_idle_inhibit_manager_v1_create_inhibitor(mgr, wl->surface);
+ } else {
+ MP_VERBOSE(wl, "Disabling the idle inhibitor\n");
+ zwp_idle_inhibitor_v1_destroy(wl->idle_inhibitor);
+ wl->idle_inhibitor = NULL;
+ }
+ return VO_TRUE;
+}
+
+static void set_surface_scaling(struct vo_wayland_state *wl)
+{
+ if (wl->fractional_scale_manager)
+ return;
+
+ // dmabuf_wayland is always wl->scaling = 1
+ double old_scale = wl->scaling;
+ wl->scaling = !wl->using_dmabuf_wayland ? wl->current_output->scale : 1;
+
+ rescale_geometry(wl, old_scale);
+ wl_surface_set_buffer_scale(wl->surface, wl->scaling);
+}
+
+static void set_window_bounds(struct vo_wayland_state *wl)
+{
+ // If the user has set geometry/autofit and the option is auto,
+ // don't use these.
+ if (wl->opts->configure_bounds == -1 && (wl->vo_opts->geometry.wh_valid ||
+ wl->vo_opts->autofit.wh_valid || wl->vo_opts->autofit_larger.wh_valid ||
+ wl->vo_opts->autofit_smaller.wh_valid))
+ {
+ return;
+ }
+
+ if (wl->bounded_width && wl->bounded_width < wl->window_size.x1)
+ wl->window_size.x1 = wl->bounded_width;
+ if (wl->bounded_height && wl->bounded_height < wl->window_size.y1)
+ wl->window_size.y1 = wl->bounded_height;
+}
+
+static int spawn_cursor(struct vo_wayland_state *wl)
+{
+ /* Don't use this if we have cursor-shape. */
+ if (wl->cursor_shape_device)
+ return 0;
+ /* Reuse if size is identical */
+ if (!wl->pointer || wl->allocated_cursor_scale == wl->scaling)
+ return 0;
+ else if (wl->cursor_theme)
+ wl_cursor_theme_destroy(wl->cursor_theme);
+
+ const char *xcursor_theme = getenv("XCURSOR_THEME");
+ const char *size_str = getenv("XCURSOR_SIZE");
+ int size = 24;
+ if (size_str != NULL) {
+ errno = 0;
+ char *end;
+ long size_long = strtol(size_str, &end, 10);
+ if (!*end && !errno && size_long > 0 && size_long <= INT_MAX)
+ size = (int)size_long;
+ }
+
+ wl->cursor_theme = wl_cursor_theme_load(xcursor_theme, size*wl->scaling, wl->shm);
+ if (!wl->cursor_theme) {
+ MP_ERR(wl, "Unable to load cursor theme!\n");
+ return 1;
+ }
+
+ wl->default_cursor = wl_cursor_theme_get_cursor(wl->cursor_theme, "left_ptr");
+ if (!wl->default_cursor) {
+ MP_ERR(wl, "Unable to load cursor theme!\n");
+ return 1;
+ }
+
+ wl->allocated_cursor_scale = wl->scaling;
+
+ return 0;
+}
+
+static void toggle_fullscreen(struct vo_wayland_state *wl)
+{
+ if (!wl->xdg_toplevel)
+ return;
+ wl->state_change = true;
+ bool specific_screen = wl->vo_opts->fsscreen_id >= 0 || wl->vo_opts->fsscreen_name;
+ if (wl->vo_opts->fullscreen && !specific_screen) {
+ xdg_toplevel_set_fullscreen(wl->xdg_toplevel, NULL);
+ } else if (wl->vo_opts->fullscreen && specific_screen) {
+ struct vo_wayland_output *output = find_output(wl);
+ xdg_toplevel_set_fullscreen(wl->xdg_toplevel, output->output);
+ } else {
+ xdg_toplevel_unset_fullscreen(wl->xdg_toplevel);
+ }
+}
+
+static void toggle_maximized(struct vo_wayland_state *wl)
+{
+ if (!wl->xdg_toplevel)
+ return;
+ wl->state_change = true;
+ if (wl->vo_opts->window_maximized) {
+ xdg_toplevel_set_maximized(wl->xdg_toplevel);
+ } else {
+ xdg_toplevel_unset_maximized(wl->xdg_toplevel);
+ }
+}
+
+static void update_app_id(struct vo_wayland_state *wl)
+{
+ if (!wl->xdg_toplevel)
+ return;
+ xdg_toplevel_set_app_id(wl->xdg_toplevel, wl->vo_opts->appid);
+}
+
+static int update_window_title(struct vo_wayland_state *wl, const char *title)
+{
+ if (!wl->xdg_toplevel)
+ return VO_NOTAVAIL;
+ /* The xdg-shell protocol requires that the title is UTF-8. */
+ void *tmp = talloc_new(NULL);
+ struct bstr b_title = bstr_sanitize_utf8_latin1(tmp, bstr0(title));
+ xdg_toplevel_set_title(wl->xdg_toplevel, bstrto0(tmp, b_title));
+ talloc_free(tmp);
+ return VO_TRUE;
+}
+
+static void window_move(struct vo_wayland_state *wl, uint32_t serial)
+{
+ if (wl->xdg_toplevel)
+ xdg_toplevel_move(wl->xdg_toplevel, wl->seat, serial);
+}
+
+static void wayland_dispatch_events(struct vo_wayland_state *wl, int nfds, int64_t timeout_ns)
+{
+ if (wl->display_fd == -1)
+ return;
+
+ struct pollfd fds[2] = {
+ {.fd = wl->display_fd, .events = POLLIN },
+ {.fd = wl->wakeup_pipe[0], .events = POLLIN },
+ };
+
+ while (wl_display_prepare_read(wl->display) != 0)
+ wl_display_dispatch_pending(wl->display);
+ wl_display_flush(wl->display);
+
+ mp_poll(fds, nfds, timeout_ns);
+
+ if (fds[0].revents & POLLIN) {
+ wl_display_read_events(wl->display);
+ } else {
+ wl_display_cancel_read(wl->display);
+ }
+
+ if (fds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) {
+ MP_FATAL(wl, "Error occurred on the display fd\n");
+ wl->display_fd = -1;
+ mp_input_put_key(wl->vo->input_ctx, MP_KEY_CLOSE_WIN);
+ }
+
+ if (fds[1].revents & POLLIN)
+ mp_flush_wakeup_pipe(wl->wakeup_pipe[0]);
+
+ wl_display_dispatch_pending(wl->display);
+}
+
+/* Non-static */
+int vo_wayland_allocate_memfd(struct vo *vo, size_t size)
+{
+#if !HAVE_MEMFD_CREATE
+ return VO_ERROR;
+#else
+ int fd = memfd_create("mpv", MFD_CLOEXEC | MFD_ALLOW_SEALING);
+ if (fd < 0) {
+ MP_ERR(vo, "Failed to allocate memfd: %s\n", mp_strerror(errno));
+ return VO_ERROR;
+ }
+
+ fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_SEAL);
+
+ if (posix_fallocate(fd, 0, size) == 0)
+ return fd;
+
+ close(fd);
+ MP_ERR(vo, "Failed to allocate memfd: %s\n", mp_strerror(errno));
+
+ return VO_ERROR;
+#endif
+}
+
+bool vo_wayland_check_visible(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ bool render = !wl->hidden || wl->vo_opts->force_render;
+ wl->frame_wait = true;
+ return render;
+}
+
+int vo_wayland_control(struct vo *vo, int *events, int request, void *arg)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ struct mp_vo_opts *opts = wl->vo_opts;
+ wl_display_dispatch_pending(wl->display);
+
+ switch (request) {
+ case VOCTRL_CHECK_EVENTS: {
+ check_dnd_fd(wl);
+ *events |= wl->pending_vo_events;
+ if (*events & VO_EVENT_RESIZE) {
+ *events |= VO_EVENT_EXPOSE;
+ wl->frame_wait = false;
+ wl->timeout_count = 0;
+ wl->hidden = false;
+ }
+ wl->pending_vo_events = 0;
+ return VO_TRUE;
+ }
+ case VOCTRL_VO_OPTS_CHANGED: {
+ void *opt;
+ while (m_config_cache_get_next_changed(wl->vo_opts_cache, &opt)) {
+ if (opt == &opts->appid)
+ update_app_id(wl);
+ if (opt == &opts->border)
+ {
+ // This is stupid but the value of border shouldn't be written
+ // unless we get a configure event. Change it back to its old
+ // value and let configure_decorations handle it after the request.
+ if (wl->xdg_toplevel_decoration) {
+ int requested_border_mode = opts->border;
+ opts->border = !opts->border;
+ m_config_cache_write_opt(wl->vo_opts_cache,
+ &opts->border);
+ request_decoration_mode(
+ wl, requested_border_mode ?
+ ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE :
+ ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE);
+ } else {
+ opts->border = false;
+ m_config_cache_write_opt(wl->vo_opts_cache,
+ &wl->vo_opts->border);
+ }
+ }
+ if (opt == &opts->content_type)
+ set_content_type(wl);
+ if (opt == &opts->cursor_passthrough)
+ set_input_region(wl, opts->cursor_passthrough);
+ if (opt == &opts->fullscreen)
+ toggle_fullscreen(wl);
+ if (opt == &opts->hidpi_window_scale)
+ set_geometry(wl, true);
+ if (opt == &opts->window_maximized)
+ toggle_maximized(wl);
+ if (opt == &opts->window_minimized)
+ do_minimize(wl);
+ if (opt == &opts->geometry || opt == &opts->autofit ||
+ opt == &opts->autofit_smaller || opt == &opts->autofit_larger)
+ {
+ set_geometry(wl, true);
+ }
+ }
+ return VO_TRUE;
+ }
+ case VOCTRL_CONTENT_TYPE: {
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ wl->current_content_type = *(enum mp_content_type *)arg;
+ set_content_type(wl);
+#endif
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_FOCUSED: {
+ *(bool *)arg = wl->focused;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_NAMES: {
+ *(char ***)arg = get_displays_spanned(wl);
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_UNFS_WINDOW_SIZE: {
+ int *s = arg;
+ if (wl->vo_opts->window_maximized || wl->tiled) {
+ s[0] = mp_rect_w(wl->geometry);
+ s[1] = mp_rect_h(wl->geometry);
+ } else {
+ s[0] = mp_rect_w(wl->window_size);
+ s[1] = mp_rect_h(wl->window_size);
+ }
+ return VO_TRUE;
+ }
+ case VOCTRL_SET_UNFS_WINDOW_SIZE: {
+ int *s = arg;
+ wl->window_size.x0 = 0;
+ wl->window_size.y0 = 0;
+ wl->window_size.x1 = s[0];
+ wl->window_size.y1 = s[1];
+ if (!wl->vo_opts->fullscreen && !wl->tiled) {
+ if (wl->vo_opts->window_maximized) {
+ xdg_toplevel_unset_maximized(wl->xdg_toplevel);
+ wl_display_dispatch_pending(wl->display);
+ /* Make sure the compositor let us unmaximize */
+ if (wl->vo_opts->window_maximized)
+ return VO_TRUE;
+ }
+ wl->geometry = wl->window_size;
+ prepare_resize(wl, 0, 0);
+ }
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_FPS: {
+ struct vo_wayland_output *out;
+ if (wl->current_output) {
+ out = wl->current_output;
+ } else {
+ out = find_output(wl);
+ }
+ if (!out)
+ return VO_NOTAVAIL;
+ *(double *)arg = out->refresh_rate;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_RES: {
+ struct vo_wayland_output *out;
+ if (wl->current_output) {
+ out = wl->current_output;
+ } else {
+ out = find_output(wl);
+ }
+ if (!out)
+ return VO_NOTAVAIL;
+ ((int *)arg)[0] = out->geometry.x1;
+ ((int *)arg)[1] = out->geometry.y1;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_HIDPI_SCALE: {
+ if (!wl->scaling)
+ return VO_NOTAVAIL;
+ *(double *)arg = wl->scaling;
+ return VO_TRUE;
+ }
+ case VOCTRL_UPDATE_WINDOW_TITLE:
+ return update_window_title(wl, (const char *)arg);
+ case VOCTRL_SET_CURSOR_VISIBILITY:
+ if (!wl->pointer)
+ return VO_NOTAVAIL;
+ return set_cursor_visibility(wl, *(bool *)arg);
+ case VOCTRL_KILL_SCREENSAVER:
+ return set_screensaver_inhibitor(wl, true);
+ case VOCTRL_RESTORE_SCREENSAVER:
+ return set_screensaver_inhibitor(wl, false);
+ }
+
+ return VO_NOTIMPL;
+}
+
+void vo_wayland_handle_fractional_scale(struct vo_wayland_state *wl)
+{
+ if (wl->fractional_scale_manager && wl->viewport)
+ wp_viewport_set_destination(wl->viewport,
+ round(mp_rect_w(wl->geometry) / wl->scaling),
+ round(mp_rect_h(wl->geometry) / wl->scaling));
+}
+
+bool vo_wayland_init(struct vo *vo)
+{
+ vo->wl = talloc_zero(NULL, struct vo_wayland_state);
+ struct vo_wayland_state *wl = vo->wl;
+
+ *wl = (struct vo_wayland_state) {
+ .display = wl_display_connect(NULL),
+ .vo = vo,
+ .log = mp_log_new(wl, vo->log, "wayland"),
+ .bounded_width = 0,
+ .bounded_height = 0,
+ .refresh_interval = 0,
+ .scaling = 1,
+ .wakeup_pipe = {-1, -1},
+ .display_fd = -1,
+ .dnd_fd = -1,
+ .cursor_visible = true,
+ .vo_opts_cache = m_config_cache_alloc(wl, vo->global, &vo_sub_opts),
+ };
+ wl->vo_opts = wl->vo_opts_cache->opts;
+ wl->using_dmabuf_wayland = !strcmp(wl->vo->driver->name, "dmabuf-wayland");
+
+ wl_list_init(&wl->output_list);
+
+ if (!wl->display)
+ goto err;
+
+ if (create_input(wl))
+ goto err;
+
+ wl->registry = wl_display_get_registry(wl->display);
+ wl_registry_add_listener(wl->registry, &registry_listener, wl);
+
+ /* Do a roundtrip to run the registry */
+ wl_display_roundtrip(wl->display);
+
+ if (!wl->surface) {
+ MP_FATAL(wl, "Compositor doesn't support %s (ver. 4)\n",
+ wl_compositor_interface.name);
+ goto err;
+ }
+
+ if (!wl->wm_base) {
+ MP_FATAL(wl, "Compositor doesn't support the required %s protocol!\n",
+ xdg_wm_base_interface.name);
+ goto err;
+ }
+
+ if (!wl_list_length(&wl->output_list)) {
+ MP_FATAL(wl, "No outputs found or compositor doesn't support %s (ver. 2)\n",
+ wl_output_interface.name);
+ goto err;
+ }
+
+ /* Can't be initialized during registry due to multi-protocol dependence */
+ if (create_viewports(wl))
+ goto err;
+
+ if (create_xdg_surface(wl))
+ goto err;
+
+ if (wl->subcompositor) {
+ wl->osd_subsurface = wl_subcompositor_get_subsurface(wl->subcompositor, wl->osd_surface, wl->video_surface);
+ wl->video_subsurface = wl_subcompositor_get_subsurface(wl->subcompositor, wl->video_surface, wl->surface);
+ }
+
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ if (wl->content_type_manager) {
+ wl->content_type = wp_content_type_manager_v1_get_surface_content_type(wl->content_type_manager, wl->surface);
+ } else {
+ MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n",
+ wp_content_type_manager_v1_interface.name);
+ }
+
+ if (!wl->single_pixel_manager) {
+ MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n",
+ wp_single_pixel_buffer_manager_v1_interface.name);
+ }
+#endif
+
+#if HAVE_WAYLAND_PROTOCOLS_1_31
+ if (wl->fractional_scale_manager) {
+ wl->fractional_scale = wp_fractional_scale_manager_v1_get_fractional_scale(wl->fractional_scale_manager, wl->surface);
+ wp_fractional_scale_v1_add_listener(wl->fractional_scale, &fractional_scale_listener, wl);
+ } else {
+ MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n",
+ wp_fractional_scale_manager_v1_interface.name);
+ }
+#endif
+
+ if (wl->dnd_devman && wl->seat) {
+ wl->dnd_ddev = wl_data_device_manager_get_data_device(wl->dnd_devman, wl->seat);
+ wl_data_device_add_listener(wl->dnd_ddev, &data_device_listener, wl);
+ } else if (!wl->dnd_devman) {
+ MP_VERBOSE(wl, "Compositor doesn't support the %s (ver. 3) protocol!\n",
+ wl_data_device_manager_interface.name);
+ }
+
+ if (wl->presentation) {
+ wl->fback_pool = talloc_zero(wl, struct vo_wayland_feedback_pool);
+ wl->fback_pool->wl = wl;
+ wl->fback_pool->len = VO_MAX_SWAPCHAIN_DEPTH;
+ wl->fback_pool->fback = talloc_zero_array(wl->fback_pool, struct wp_presentation_feedback *,
+ wl->fback_pool->len);
+ wl->present = mp_present_initialize(wl, wl->vo_opts, VO_MAX_SWAPCHAIN_DEPTH);
+ } else {
+ MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n",
+ wp_presentation_interface.name);
+ }
+
+ if (wl->xdg_decoration_manager) {
+ wl->xdg_toplevel_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(wl->xdg_decoration_manager, wl->xdg_toplevel);
+ zxdg_toplevel_decoration_v1_add_listener(wl->xdg_toplevel_decoration, &decoration_listener, wl);
+ request_decoration_mode(
+ wl, wl->vo_opts->border ?
+ ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE :
+ ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE);
+ } else {
+ wl->vo_opts->border = false;
+ m_config_cache_write_opt(wl->vo_opts_cache,
+ &wl->vo_opts->border);
+ MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n",
+ zxdg_decoration_manager_v1_interface.name);
+ }
+
+ if (!wl->idle_inhibit_manager) {
+ MP_VERBOSE(wl, "Compositor doesn't support the %s protocol!\n",
+ zwp_idle_inhibit_manager_v1_interface.name);
+ }
+
+ wl->opts = mp_get_config_group(wl, wl->vo->global, &wayland_conf);
+ wl->display_fd = wl_display_get_fd(wl->display);
+
+ update_app_id(wl);
+ mp_make_wakeup_pipe(wl->wakeup_pipe);
+
+ wl->callback_surface = wl->using_dmabuf_wayland ? wl->video_surface : wl->surface;
+ wl->frame_callback = wl_surface_frame(wl->callback_surface);
+ wl_callback_add_listener(wl->frame_callback, &frame_listener, wl);
+ wl_surface_commit(wl->surface);
+
+ /* Do another roundtrip to ensure all of the above is initialized
+ * before mpv does anything else. */
+ wl_display_roundtrip(wl->display);
+
+ return true;
+
+err:
+ vo_wayland_uninit(vo);
+ return false;
+}
+
+bool vo_wayland_reconfig(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+
+ MP_VERBOSE(wl, "Reconfiguring!\n");
+
+ if (!wl->current_output) {
+ wl->current_output = find_output(wl);
+ if (!wl->current_output)
+ return false;
+ set_surface_scaling(wl);
+ wl->pending_vo_events |= VO_EVENT_DPI;
+ }
+
+ if (wl->vo_opts->auto_window_resize || !wl->configured)
+ set_geometry(wl, false);
+
+ if (wl->opts->configure_bounds)
+ set_window_bounds(wl);
+
+ if (!wl->configured || !wl->locked_size) {
+ wl->geometry = wl->window_size;
+ wl->configured = true;
+ }
+
+ if (wl->vo_opts->cursor_passthrough)
+ set_input_region(wl, true);
+
+ if (wl->vo_opts->fullscreen)
+ toggle_fullscreen(wl);
+
+ if (wl->vo_opts->window_maximized)
+ toggle_maximized(wl);
+
+ if (wl->vo_opts->window_minimized)
+ do_minimize(wl);
+
+ prepare_resize(wl, 0, 0);
+
+ return true;
+}
+
+void vo_wayland_set_opaque_region(struct vo_wayland_state *wl, bool alpha)
+{
+ const int32_t width = mp_rect_w(wl->geometry);
+ const int32_t height = mp_rect_h(wl->geometry);
+ if (!alpha) {
+ struct wl_region *region = wl_compositor_create_region(wl->compositor);
+ wl_region_add(region, 0, 0, width, height);
+ wl_surface_set_opaque_region(wl->surface, region);
+ wl_region_destroy(region);
+ } else {
+ wl_surface_set_opaque_region(wl->surface, NULL);
+ }
+}
+
+void vo_wayland_uninit(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ if (!wl)
+ return;
+
+ mp_input_put_key(wl->vo->input_ctx, MP_INPUT_RELEASE_ALL);
+
+ if (wl->compositor)
+ wl_compositor_destroy(wl->compositor);
+
+ if (wl->subcompositor)
+ wl_subcompositor_destroy(wl->subcompositor);
+
+#if HAVE_WAYLAND_PROTOCOLS_1_32
+ if (wl->cursor_shape_device)
+ wp_cursor_shape_device_v1_destroy(wl->cursor_shape_device);
+
+ if (wl->cursor_shape_manager)
+ wp_cursor_shape_manager_v1_destroy(wl->cursor_shape_manager);
+#endif
+
+ if (wl->cursor_surface)
+ wl_surface_destroy(wl->cursor_surface);
+
+ if (wl->cursor_theme)
+ wl_cursor_theme_destroy(wl->cursor_theme);
+
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ if (wl->content_type)
+ wp_content_type_v1_destroy(wl->content_type);
+
+ if (wl->content_type_manager)
+ wp_content_type_manager_v1_destroy(wl->content_type_manager);
+#endif
+
+ if (wl->dnd_ddev)
+ wl_data_device_destroy(wl->dnd_ddev);
+
+ if (wl->dnd_devman)
+ wl_data_device_manager_destroy(wl->dnd_devman);
+
+ if (wl->dnd_offer)
+ wl_data_offer_destroy(wl->dnd_offer);
+
+ if (wl->fback_pool)
+ clean_feedback_pool(wl->fback_pool);
+
+#if HAVE_WAYLAND_PROTOCOLS_1_31
+ if (wl->fractional_scale)
+ wp_fractional_scale_v1_destroy(wl->fractional_scale);
+
+ if (wl->fractional_scale_manager)
+ wp_fractional_scale_manager_v1_destroy(wl->fractional_scale_manager);
+#endif
+
+ if (wl->frame_callback)
+ wl_callback_destroy(wl->frame_callback);
+
+ if (wl->idle_inhibitor)
+ zwp_idle_inhibitor_v1_destroy(wl->idle_inhibitor);
+
+ if (wl->idle_inhibit_manager)
+ zwp_idle_inhibit_manager_v1_destroy(wl->idle_inhibit_manager);
+
+ if (wl->keyboard)
+ wl_keyboard_destroy(wl->keyboard);
+
+ if (wl->pointer)
+ wl_pointer_destroy(wl->pointer);
+
+ if (wl->presentation)
+ wp_presentation_destroy(wl->presentation);
+
+ if (wl->registry)
+ wl_registry_destroy(wl->registry);
+
+ if (wl->viewporter)
+ wp_viewporter_destroy(wl->viewporter);
+
+ if (wl->viewport)
+ wp_viewport_destroy(wl->viewport);
+
+ if (wl->osd_viewport)
+ wp_viewport_destroy(wl->osd_viewport);
+
+ if (wl->video_viewport)
+ wp_viewport_destroy(wl->video_viewport);
+
+ if (wl->dmabuf)
+ zwp_linux_dmabuf_v1_destroy(wl->dmabuf);
+
+ if (wl->dmabuf_feedback)
+ zwp_linux_dmabuf_feedback_v1_destroy(wl->dmabuf_feedback);
+
+ if (wl->seat)
+ wl_seat_destroy(wl->seat);
+
+ if (wl->shm)
+ wl_shm_destroy(wl->shm);
+
+#if HAVE_WAYLAND_PROTOCOLS_1_27
+ if (wl->single_pixel_manager)
+ wp_single_pixel_buffer_manager_v1_destroy(wl->single_pixel_manager);
+#endif
+
+ if (wl->surface)
+ wl_surface_destroy(wl->surface);
+
+ if (wl->osd_surface)
+ wl_surface_destroy(wl->osd_surface);
+
+ if (wl->osd_subsurface)
+ wl_subsurface_destroy(wl->osd_subsurface);
+
+ if (wl->video_surface)
+ wl_surface_destroy(wl->video_surface);
+
+ if (wl->video_subsurface)
+ wl_subsurface_destroy(wl->video_subsurface);
+
+ if (wl->wm_base)
+ xdg_wm_base_destroy(wl->wm_base);
+
+ if (wl->xdg_decoration_manager)
+ zxdg_decoration_manager_v1_destroy(wl->xdg_decoration_manager);
+
+ if (wl->xdg_toplevel)
+ xdg_toplevel_destroy(wl->xdg_toplevel);
+
+ if (wl->xdg_toplevel_decoration)
+ zxdg_toplevel_decoration_v1_destroy(wl->xdg_toplevel_decoration);
+
+ if (wl->xdg_surface)
+ xdg_surface_destroy(wl->xdg_surface);
+
+ if (wl->xkb_context)
+ xkb_context_unref(wl->xkb_context);
+
+ if (wl->xkb_keymap)
+ xkb_keymap_unref(wl->xkb_keymap);
+
+ if (wl->xkb_state)
+ xkb_state_unref(wl->xkb_state);
+
+ struct vo_wayland_output *output, *tmp;
+ wl_list_for_each_safe(output, tmp, &wl->output_list, link)
+ remove_output(output);
+
+ if (wl->display)
+ wl_display_disconnect(wl->display);
+
+ munmap(wl->format_map, wl->format_size);
+
+ for (int n = 0; n < 2; n++)
+ close(wl->wakeup_pipe[n]);
+ talloc_free(wl);
+ vo->wl = NULL;
+}
+
+void vo_wayland_wait_frame(struct vo_wayland_state *wl)
+{
+ int64_t vblank_time = 0;
+ /* We need some vblank interval to use for the timeout in
+ * this function. The order of preference of values to use is:
+ * 1. vsync duration from presentation time
+ * 2. refresh interval reported by presentation time
+ * 3. refresh rate of the output reported by the compositor
+ * 4. make up crap if vblank_time is still <= 0 (better than nothing) */
+
+ if (wl->use_present && wl->present->head)
+ vblank_time = wl->present->head->vsync_duration;
+
+ if (vblank_time <= 0 && wl->refresh_interval > 0)
+ vblank_time = wl->refresh_interval;
+
+ if (vblank_time <= 0 && wl->current_output->refresh_rate > 0)
+ vblank_time = 1e9 / wl->current_output->refresh_rate;
+
+ // Ideally you should never reach this point.
+ if (vblank_time <= 0)
+ vblank_time = 1e9 / 60;
+
+ // Completely arbitrary amount of additional time to wait.
+ vblank_time += 0.05 * vblank_time;
+ int64_t finish_time = mp_time_ns() + vblank_time;
+
+ while (wl->frame_wait && finish_time > mp_time_ns()) {
+ int64_t poll_time = finish_time - mp_time_ns();
+ if (poll_time < 0) {
+ poll_time = 0;
+ }
+ wayland_dispatch_events(wl, 1, poll_time);
+ }
+
+ /* If the compositor does not have presentation time, we cannot be sure
+ * that this wait is accurate. Do a hacky block with wl_display_roundtrip. */
+ if (!wl->use_present && !wl_display_get_error(wl->display))
+ wl_display_roundtrip(wl->display);
+
+ /* Only use this heuristic if the compositor doesn't support the suspended state. */
+ if (wl->frame_wait && xdg_toplevel_get_version(wl->xdg_toplevel) < 6) {
+ // Only consider consecutive missed callbacks.
+ if (wl->timeout_count > 1) {
+ wl->hidden = true;
+ return;
+ } else {
+ wl->timeout_count += 1;
+ return;
+ }
+ }
+
+ wl->timeout_count = 0;
+}
+
+void vo_wayland_wait_events(struct vo *vo, int64_t until_time_ns)
+{
+ struct vo_wayland_state *wl = vo->wl;
+
+ int64_t wait_ns = until_time_ns - mp_time_ns();
+ int64_t timeout_ns = MPCLAMP(wait_ns, 0, MP_TIME_S_TO_NS(10));
+
+ wayland_dispatch_events(wl, 2, timeout_ns);
+}
+
+void vo_wayland_wakeup(struct vo *vo)
+{
+ struct vo_wayland_state *wl = vo->wl;
+ (void)write(wl->wakeup_pipe[1], &(char){0}, 1);
+}
diff --git a/video/out/wayland_common.h b/video/out/wayland_common.h
new file mode 100644
index 0000000..adbcca6
--- /dev/null
+++ b/video/out/wayland_common.h
@@ -0,0 +1,189 @@
+/*
+ * This file is part of mpv video player.
+ *
+ * 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/>.
+ */
+
+#ifndef MPLAYER_WAYLAND_COMMON_H
+#define MPLAYER_WAYLAND_COMMON_H
+
+#include <wayland-client.h>
+#include "input/event.h"
+#include "vo.h"
+
+typedef struct {
+ uint32_t format;
+ uint32_t padding;
+ uint64_t modifier;
+} wayland_format;
+
+struct wayland_opts {
+ int configure_bounds;
+ int content_type;
+ bool disable_vsync;
+ int edge_pixels_pointer;
+ int edge_pixels_touch;
+};
+
+struct vo_wayland_state {
+ struct m_config_cache *vo_opts_cache;
+ struct mp_log *log;
+ struct mp_vo_opts *vo_opts;
+ struct vo *vo;
+ struct wayland_opts *opts;
+ struct wl_callback *frame_callback;
+ struct wl_compositor *compositor;
+ struct wl_subcompositor *subcompositor;
+ struct wl_display *display;
+ struct wl_registry *registry;
+ struct wl_shm *shm;
+ struct wl_surface *surface;
+ struct wl_surface *osd_surface;
+ struct wl_subsurface *osd_subsurface;
+ struct wl_surface *video_surface;
+ struct wl_surface *callback_surface;
+ struct wl_subsurface *video_subsurface;
+
+ /* Geometry */
+ struct mp_rect geometry;
+ struct mp_rect window_size;
+ struct wl_list output_list;
+ struct vo_wayland_output *current_output;
+ int bounded_height;
+ int bounded_width;
+ int reduced_height;
+ int reduced_width;
+ int toplevel_width;
+ int toplevel_height;
+
+ /* State */
+ bool activated;
+ bool configured;
+ bool focused;
+ bool frame_wait;
+ bool has_keyboard_input;
+ bool hidden;
+ bool initial_size_hint;
+ bool locked_size;
+ bool state_change;
+ bool tiled;
+ bool toplevel_configured;
+ int display_fd;
+ int mouse_x;
+ int mouse_y;
+ int pending_vo_events;
+ double scaling;
+ int timeout_count;
+ int wakeup_pipe[2];
+
+ /* content-type */
+ /* TODO: unvoid these if required wayland protocols is bumped to 1.27+ */
+ void *content_type_manager;
+ void *content_type;
+ int current_content_type;
+
+ /* cursor-shape */
+ /* TODO: unvoid these if required wayland protocols is bumped to 1.32+ */
+ void *cursor_shape_manager;
+ void *cursor_shape_device;
+
+ /* fractional-scale */
+ /* TODO: unvoid these if required wayland protocols is bumped to 1.31+ */
+ void *fractional_scale_manager;
+ void *fractional_scale;
+
+ /* idle-inhibit */
+ struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager;
+ struct zwp_idle_inhibitor_v1 *idle_inhibitor;
+
+ /* linux-dmabuf */
+ struct zwp_linux_dmabuf_v1 *dmabuf;
+ struct zwp_linux_dmabuf_feedback_v1 *dmabuf_feedback;
+ wayland_format *format_map;
+ uint32_t format_size;
+ bool using_dmabuf_wayland;
+
+ /* presentation-time */
+ struct wp_presentation *presentation;
+ struct vo_wayland_feedback_pool *fback_pool;
+ struct mp_present *present;
+ int64_t refresh_interval;
+ bool use_present;
+
+ /* single-pixel-buffer */
+ /* TODO: unvoid this if required wayland-protocols is bumped to 1.27+ */
+ void *single_pixel_manager;
+
+ /* xdg-decoration */
+ struct zxdg_decoration_manager_v1 *xdg_decoration_manager;
+ struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration;
+ int requested_decoration;
+
+ /* xdg-shell */
+ struct xdg_wm_base *wm_base;
+ struct xdg_surface *xdg_surface;
+ struct xdg_toplevel *xdg_toplevel;
+
+ /* viewporter */
+ struct wp_viewporter *viewporter;
+ struct wp_viewport *viewport;
+ struct wp_viewport *osd_viewport;
+ struct wp_viewport *video_viewport;
+
+ /* Input */
+ struct wl_keyboard *keyboard;
+ struct wl_pointer *pointer;
+ struct wl_seat *seat;
+ struct wl_touch *touch;
+ struct xkb_context *xkb_context;
+ struct xkb_keymap *xkb_keymap;
+ struct xkb_state *xkb_state;
+ uint32_t keyboard_code;
+ int mpkey;
+ int mpmod;
+
+ /* DND */
+ struct wl_data_device *dnd_ddev;
+ struct wl_data_device_manager *dnd_devman;
+ struct wl_data_offer *dnd_offer;
+ enum mp_dnd_action dnd_action;
+ char *dnd_mime_type;
+ int dnd_fd;
+ int dnd_mime_score;
+
+ /* Cursor */
+ struct wl_cursor_theme *cursor_theme;
+ struct wl_cursor *default_cursor;
+ struct wl_surface *cursor_surface;
+ bool cursor_visible;
+ int allocated_cursor_scale;
+ uint32_t pointer_id;
+};
+
+bool vo_wayland_check_visible(struct vo *vo);
+bool vo_wayland_init(struct vo *vo);
+bool vo_wayland_reconfig(struct vo *vo);
+
+int vo_wayland_allocate_memfd(struct vo *vo, size_t size);
+int vo_wayland_control(struct vo *vo, int *events, int request, void *arg);
+
+void vo_wayland_handle_fractional_scale(struct vo_wayland_state *wl);
+void vo_wayland_set_opaque_region(struct vo_wayland_state *wl, bool alpha);
+void vo_wayland_sync_swap(struct vo_wayland_state *wl);
+void vo_wayland_uninit(struct vo *vo);
+void vo_wayland_wait_events(struct vo *vo, int64_t until_time_ns);
+void vo_wayland_wait_frame(struct vo_wayland_state *wl);
+void vo_wayland_wakeup(struct vo *vo);
+
+#endif /* MPLAYER_WAYLAND_COMMON_H */
diff --git a/video/out/win32/displayconfig.c b/video/out/win32/displayconfig.c
new file mode 100644
index 0000000..9844afd
--- /dev/null
+++ b/video/out/win32/displayconfig.c
@@ -0,0 +1,140 @@
+/*
+ * 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 <windows.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include "displayconfig.h"
+
+#include "mpv_talloc.h"
+
+static bool is_valid_refresh_rate(DISPLAYCONFIG_RATIONAL rr)
+{
+ // DisplayConfig sometimes reports a rate of 1 when the rate is not known
+ return rr.Denominator != 0 && rr.Numerator / rr.Denominator > 1;
+}
+
+static int get_config(void *ctx,
+ UINT32 *num_paths, DISPLAYCONFIG_PATH_INFO** paths,
+ UINT32 *num_modes, DISPLAYCONFIG_MODE_INFO** modes)
+{
+ LONG res;
+ *paths = NULL;
+ *modes = NULL;
+
+ // The display configuration could change between the call to
+ // GetDisplayConfigBufferSizes and the call to QueryDisplayConfig, so call
+ // them in a loop until the correct buffer size is chosen
+ do {
+ res = GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, num_paths,
+ num_modes);
+ if (res != ERROR_SUCCESS)
+ goto fail;
+
+ // Free old buffers if they exist and allocate new ones
+ talloc_free(*paths);
+ talloc_free(*modes);
+ *paths = talloc_array(ctx, DISPLAYCONFIG_PATH_INFO, *num_paths);
+ *modes = talloc_array(ctx, DISPLAYCONFIG_MODE_INFO, *num_modes);
+
+ res = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, num_paths, *paths,
+ num_modes, *modes, NULL);
+ } while (res == ERROR_INSUFFICIENT_BUFFER);
+ if (res != ERROR_SUCCESS)
+ goto fail;
+
+ return 0;
+fail:
+ talloc_free(*paths);
+ talloc_free(*modes);
+ return -1;
+}
+
+static DISPLAYCONFIG_PATH_INFO *get_path(UINT32 num_paths,
+ DISPLAYCONFIG_PATH_INFO* paths,
+ const wchar_t *device)
+{
+ // Search for a path with a matching device name
+ for (UINT32 i = 0; i < num_paths; i++) {
+ // Send a GET_SOURCE_NAME request
+ DISPLAYCONFIG_SOURCE_DEVICE_NAME source = {
+ .header = {
+ .size = sizeof source,
+ .type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
+ .adapterId = paths[i].sourceInfo.adapterId,
+ .id = paths[i].sourceInfo.id,
+ }
+ };
+ if (DisplayConfigGetDeviceInfo(&source.header) != ERROR_SUCCESS)
+ return NULL;
+
+ // Check if the device name matches
+ if (!wcscmp(device, source.viewGdiDeviceName))
+ return &paths[i];
+ }
+
+ return NULL;
+}
+
+static double get_refresh_rate_from_mode(DISPLAYCONFIG_MODE_INFO *mode)
+{
+ if (mode->infoType != DISPLAYCONFIG_MODE_INFO_TYPE_TARGET)
+ return 0.0;
+
+ DISPLAYCONFIG_VIDEO_SIGNAL_INFO *info =
+ &mode->targetMode.targetVideoSignalInfo;
+ if (!is_valid_refresh_rate(info->vSyncFreq))
+ return 0.0;
+
+ return ((double)info->vSyncFreq.Numerator) /
+ ((double)info->vSyncFreq.Denominator);
+}
+
+double mp_w32_displayconfig_get_refresh_rate(const wchar_t *device)
+{
+ void *ctx = talloc_new(NULL);
+ double freq = 0.0;
+
+ // Get the current display configuration
+ UINT32 num_paths;
+ DISPLAYCONFIG_PATH_INFO* paths;
+ UINT32 num_modes;
+ DISPLAYCONFIG_MODE_INFO* modes;
+ if (get_config(ctx, &num_paths, &paths, &num_modes, &modes))
+ goto end;
+
+ // Get the path for the specified monitor
+ DISPLAYCONFIG_PATH_INFO* path;
+ if (!(path = get_path(num_paths, paths, device)))
+ goto end;
+
+ // Try getting the refresh rate from the mode first. The value in the mode
+ // overrides the value in the path.
+ if (path->targetInfo.modeInfoIdx != DISPLAYCONFIG_PATH_MODE_IDX_INVALID)
+ freq = get_refresh_rate_from_mode(&modes[path->targetInfo.modeInfoIdx]);
+
+ // If the mode didn't contain a valid refresh rate, try the path
+ if (freq == 0.0 && is_valid_refresh_rate(path->targetInfo.refreshRate)) {
+ freq = ((double)path->targetInfo.refreshRate.Numerator) /
+ ((double)path->targetInfo.refreshRate.Denominator);
+ }
+
+end:
+ talloc_free(ctx);
+ return freq;
+}
diff --git a/video/out/win32/displayconfig.h b/video/out/win32/displayconfig.h
new file mode 100644
index 0000000..ee6cd03
--- /dev/null
+++ b/video/out/win32/displayconfig.h
@@ -0,0 +1,27 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MP_WIN32_DISPLAYCONFIG_H_
+#define MP_WIN32_DISPLAYCONFIG_H_
+
+#include <wchar.h>
+
+// Given a GDI monitor device name, get the precise refresh rate using the
+// Windows 7 DisplayConfig API. Returns 0.0 on failure.
+double mp_w32_displayconfig_get_refresh_rate(const wchar_t *device);
+
+#endif
diff --git a/video/out/win32/droptarget.c b/video/out/win32/droptarget.c
new file mode 100644
index 0000000..8a33522
--- /dev/null
+++ b/video/out/win32/droptarget.c
@@ -0,0 +1,227 @@
+/*
+ * 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 <stdatomic.h>
+
+#include <windows.h>
+#include <ole2.h>
+#include <shobjidl.h>
+
+#include "common/msg.h"
+#include "common/common.h"
+#include "input/input.h"
+#include "input/event.h"
+#include "osdep/io.h"
+#include "osdep/windows_utils.h"
+#include "mpv_talloc.h"
+
+#include "droptarget.h"
+
+struct droptarget {
+ IDropTarget iface;
+ atomic_int ref_cnt;
+ struct mp_log *log;
+ struct input_ctx *input_ctx;
+ struct mp_vo_opts *opts;
+ DWORD last_effect;
+ IDataObject *data_obj;
+};
+
+static FORMATETC fmtetc_file = {
+ .cfFormat = CF_HDROP,
+ .dwAspect = DVASPECT_CONTENT,
+ .lindex = -1,
+ .tymed = TYMED_HGLOBAL,
+};
+
+static FORMATETC fmtetc_url = {
+ .dwAspect = DVASPECT_CONTENT,
+ .lindex = -1,
+ .tymed = TYMED_HGLOBAL,
+};
+
+static void DropTarget_Destroy(struct droptarget *t)
+{
+ SAFE_RELEASE(t->data_obj);
+ talloc_free(t);
+}
+
+static STDMETHODIMP DropTarget_QueryInterface(IDropTarget *self, REFIID riid,
+ void **ppvObject)
+{
+ if (IsEqualIID(riid, &IID_IUnknown) || IsEqualIID(riid, &IID_IDropTarget)) {
+ *ppvObject = self;
+ IDropTarget_AddRef(self);
+ return S_OK;
+ }
+
+ *ppvObject = NULL;
+ return E_NOINTERFACE;
+}
+
+static STDMETHODIMP_(ULONG) DropTarget_AddRef(IDropTarget *self)
+{
+ struct droptarget *t = (struct droptarget *)self;
+ return atomic_fetch_add(&t->ref_cnt, 1) + 1;
+}
+
+static STDMETHODIMP_(ULONG) DropTarget_Release(IDropTarget *self)
+{
+ struct droptarget *t = (struct droptarget *)self;
+
+ ULONG ref_cnt = atomic_fetch_add(&t->ref_cnt, -1) - 1;
+ if (ref_cnt == 0)
+ DropTarget_Destroy(t);
+ return ref_cnt;
+}
+
+static STDMETHODIMP DropTarget_DragEnter(IDropTarget *self,
+ IDataObject *pDataObj,
+ DWORD grfKeyState, POINTL pt,
+ DWORD *pdwEffect)
+{
+ struct droptarget *t = (struct droptarget *)self;
+
+ IDataObject_AddRef(pDataObj);
+ if (FAILED(IDataObject_QueryGetData(pDataObj, &fmtetc_file)) &&
+ FAILED(IDataObject_QueryGetData(pDataObj, &fmtetc_url)))
+ {
+ *pdwEffect = DROPEFFECT_NONE;
+ }
+
+ SAFE_RELEASE(t->data_obj);
+ t->data_obj = pDataObj;
+ t->last_effect = *pdwEffect;
+ return S_OK;
+}
+
+static STDMETHODIMP DropTarget_DragOver(IDropTarget *self, DWORD grfKeyState,
+ POINTL pt, DWORD *pdwEffect)
+{
+ struct droptarget *t = (struct droptarget *)self;
+
+ *pdwEffect = t->last_effect;
+ return S_OK;
+}
+
+static STDMETHODIMP DropTarget_DragLeave(IDropTarget *self)
+{
+ struct droptarget *t = (struct droptarget *)self;
+
+ SAFE_RELEASE(t->data_obj);
+ return S_OK;
+}
+
+static STDMETHODIMP DropTarget_Drop(IDropTarget *self, IDataObject *pDataObj,
+ DWORD grfKeyState, POINTL pt,
+ DWORD *pdwEffect)
+{
+ struct droptarget *t = (struct droptarget *)self;
+
+ enum mp_dnd_action action;
+ if (t->opts->drag_and_drop >= 0) {
+ action = t->opts->drag_and_drop;
+ } else {
+ action = (grfKeyState & MK_SHIFT) ? DND_APPEND : DND_REPLACE;
+ }
+
+ SAFE_RELEASE(t->data_obj);
+
+ STGMEDIUM medium;
+ if (t->opts->drag_and_drop == -2) {
+ t->last_effect = DROPEFFECT_NONE;
+ } else if (SUCCEEDED(IDataObject_GetData(pDataObj, &fmtetc_file, &medium))) {
+ if (GlobalLock(medium.hGlobal)) {
+ HDROP drop = medium.hGlobal;
+
+ UINT files_num = DragQueryFileW(drop, 0xFFFFFFFF, NULL, 0);
+ char **files = talloc_zero_array(NULL, char*, files_num);
+
+ UINT recvd_files = 0;
+ for (UINT i = 0; i < files_num; i++) {
+ UINT len = DragQueryFileW(drop, i, NULL, 0);
+ wchar_t *buf = talloc_array(NULL, wchar_t, len + 1);
+
+ if (DragQueryFileW(drop, i, buf, len + 1) == len) {
+ char *fname = mp_to_utf8(files, buf);
+ files[recvd_files++] = fname;
+
+ MP_VERBOSE(t, "received dropped file: %s\n", fname);
+ } else {
+ MP_ERR(t, "error getting dropped file name\n");
+ }
+
+ talloc_free(buf);
+ }
+
+ GlobalUnlock(medium.hGlobal);
+ mp_event_drop_files(t->input_ctx, recvd_files, files, action);
+ talloc_free(files);
+ }
+
+ ReleaseStgMedium(&medium);
+ } else if (SUCCEEDED(IDataObject_GetData(pDataObj, &fmtetc_url, &medium))) {
+ wchar_t *wurl = GlobalLock(medium.hGlobal);
+ if (wurl) {
+ char *url = mp_to_utf8(NULL, wurl);
+ if (mp_event_drop_mime_data(t->input_ctx, "text/uri-list",
+ bstr0(url), action) > 0)
+ {
+ MP_VERBOSE(t, "received dropped URL: %s\n", url);
+ } else {
+ MP_ERR(t, "error getting dropped URL\n");
+ }
+
+ talloc_free(url);
+ GlobalUnlock(medium.hGlobal);
+ }
+
+ ReleaseStgMedium(&medium);
+ } else {
+ t->last_effect = DROPEFFECT_NONE;
+ }
+
+ *pdwEffect = t->last_effect;
+ return S_OK;
+}
+
+static IDropTargetVtbl idroptarget_vtbl = {
+ .QueryInterface = DropTarget_QueryInterface,
+ .AddRef = DropTarget_AddRef,
+ .Release = DropTarget_Release,
+ .DragEnter = DropTarget_DragEnter,
+ .DragOver = DropTarget_DragOver,
+ .DragLeave = DropTarget_DragLeave,
+ .Drop = DropTarget_Drop,
+};
+
+IDropTarget *mp_w32_droptarget_create(struct mp_log *log,
+ struct mp_vo_opts *opts,
+ struct input_ctx *input_ctx)
+{
+ fmtetc_url.cfFormat = RegisterClipboardFormatW(L"UniformResourceLocatorW");
+
+ struct droptarget *dt = talloc(NULL, struct droptarget);
+ dt->iface.lpVtbl = &idroptarget_vtbl;
+ atomic_store(&dt->ref_cnt, 0);
+ dt->last_effect = 0;
+ dt->data_obj = NULL;
+ dt->log = mp_log_new(dt, log, "droptarget");
+ dt->opts = opts;
+ dt->input_ctx = input_ctx;
+
+ return &dt->iface;
+}
diff --git a/video/out/win32/droptarget.h b/video/out/win32/droptarget.h
new file mode 100644
index 0000000..1c18c06
--- /dev/null
+++ b/video/out/win32/droptarget.h
@@ -0,0 +1,35 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MP_WIN32_DROPTARGET_H_
+#define MP_WIN32_DROPTARGET_H_
+
+#include <windows.h>
+#include <ole2.h>
+#include <shobjidl.h>
+
+#include "input/input.h"
+#include "common/msg.h"
+#include "common/common.h"
+#include "options/options.h"
+
+// Create a IDropTarget implementation that sends dropped files to input_ctx
+IDropTarget *mp_w32_droptarget_create(struct mp_log *log,
+ struct mp_vo_opts *opts,
+ struct input_ctx *input_ctx);
+
+#endif
diff --git a/video/out/win_state.c b/video/out/win_state.c
new file mode 100644
index 0000000..b4bc9fd
--- /dev/null
+++ b/video/out/win_state.c
@@ -0,0 +1,155 @@
+/*
+ * 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 "win_state.h"
+#include "vo.h"
+
+#include "video/mp_image.h"
+
+static void calc_monitor_aspect(struct mp_vo_opts *opts, int scr_w, int scr_h,
+ double *pixelaspect, int *w, int *h)
+{
+ *pixelaspect = 1.0 / opts->monitor_pixel_aspect;
+
+ if (scr_w > 0 && scr_h > 0 && opts->force_monitor_aspect)
+ *pixelaspect = 1.0 / (opts->force_monitor_aspect * scr_h / scr_w);
+
+ if (*pixelaspect < 1) {
+ *h /= *pixelaspect;
+ } else {
+ *w *= *pixelaspect;
+ }
+}
+
+// Fit *w/*h into the size specified by geo.
+static void apply_autofit(int *w, int *h, int scr_w, int scr_h,
+ struct m_geometry *geo, bool allow_up, bool allow_down)
+{
+ if (!geo->wh_valid)
+ return;
+
+ int dummy = 0;
+ int n_w = *w, n_h = *h;
+ m_geometry_apply(&dummy, &dummy, &n_w, &n_h, scr_w, scr_h, geo);
+
+ if (!allow_up && *w <= n_w && *h <= n_h)
+ return;
+ if (!allow_down && *w >= n_w && *h >= n_h)
+ return;
+
+ // If aspect mismatches, always make the window smaller than the fit box
+ // (Or larger, if allow_down==false.)
+ double asp = (double)*w / *h;
+ double n_asp = (double)n_w / n_h;
+ if ((n_asp <= asp) == allow_down) {
+ *w = n_w;
+ *h = n_w / asp;
+ } else {
+ *w = n_h * asp;
+ *h = n_h;
+ }
+}
+
+// Compute the "suggested" window size and position and return it in *out_geo.
+// screen is the bounding box of the current screen within the virtual desktop.
+// Does not change *vo.
+// screen: position of the area on virtual desktop on which the video-content
+// should be placed (maybe after excluding decorations, taskbars, etc)
+// monitor: position of the monitor on virtual desktop (used for pixelaspect).
+// dpi_scale: the DPI multiplier to get from virtual to real coordinates
+// (>1 for "hidpi")
+// Use vo_apply_window_geometry() to copy the result into the vo.
+// NOTE: currently, all windowing backends do their own handling of window
+// geometry additional to this code. This is to deal with initial window
+// placement, fullscreen handling, avoiding resize on reconfig() with no
+// size change, multi-monitor stuff, and possibly more.
+void vo_calc_window_geometry3(struct vo *vo, const struct mp_rect *screen,
+ const struct mp_rect *monitor,
+ double dpi_scale, struct vo_win_geometry *out_geo)
+{
+ struct mp_vo_opts *opts = vo->opts;
+
+ *out_geo = (struct vo_win_geometry){0};
+
+ // The case of calling this function even though no video was configured
+ // yet (i.e. vo->params==NULL) happens when vo_gpu creates a hidden window
+ // in order to create a rendering context.
+ struct mp_image_params params = { .w = 320, .h = 200 };
+ if (vo->params)
+ params = *vo->params;
+
+ if (!opts->hidpi_window_scale)
+ dpi_scale = 1;
+
+ int d_w, d_h;
+ mp_image_params_get_dsize(&params, &d_w, &d_h);
+ if ((vo->driver->caps & VO_CAP_ROTATE90) && params.rotate % 180 == 90)
+ MPSWAP(int, d_w, d_h);
+ d_w = MPCLAMP(d_w * opts->window_scale * dpi_scale, 1, 16000);
+ d_h = MPCLAMP(d_h * opts->window_scale * dpi_scale, 1, 16000);
+
+ int scr_w = screen->x1 - screen->x0;
+ int scr_h = screen->y1 - screen->y0;
+
+ int mon_w = monitor->x1 - monitor->x0;
+ int mon_h = monitor->y1 - monitor->y0;
+
+ MP_DBG(vo, "max content size: %dx%d\n", scr_w, scr_h);
+ MP_DBG(vo, "monitor size: %dx%d\n", mon_w, mon_h);
+
+ calc_monitor_aspect(opts, mon_w, mon_h, &out_geo->monitor_par, &d_w, &d_h);
+
+ apply_autofit(&d_w, &d_h, scr_w, scr_h, &opts->autofit, true, true);
+ apply_autofit(&d_w, &d_h, scr_w, scr_h, &opts->autofit_smaller, true, false);
+ apply_autofit(&d_w, &d_h, scr_w, scr_h, &opts->autofit_larger, false, true);
+
+ out_geo->win.x0 = (int)(scr_w - d_w) / 2;
+ out_geo->win.y0 = (int)(scr_h - d_h) / 2;
+ m_geometry_apply(&out_geo->win.x0, &out_geo->win.y0, &d_w, &d_h,
+ scr_w, scr_h, &opts->geometry);
+
+ out_geo->win.x0 += screen->x0;
+ out_geo->win.y0 += screen->y0;
+ out_geo->win.x1 = out_geo->win.x0 + d_w;
+ out_geo->win.y1 = out_geo->win.y0 + d_h;
+
+ if (opts->geometry.xy_valid || opts->force_window_position)
+ out_geo->flags |= VO_WIN_FORCE_POS;
+}
+
+// same as vo_calc_window_geometry3 with monitor assumed same as screen
+void vo_calc_window_geometry2(struct vo *vo, const struct mp_rect *screen,
+ double dpi_scale, struct vo_win_geometry *out_geo)
+{
+ vo_calc_window_geometry3(vo, screen, screen, dpi_scale, out_geo);
+}
+
+void vo_calc_window_geometry(struct vo *vo, const struct mp_rect *screen,
+ struct vo_win_geometry *out_geo)
+{
+ vo_calc_window_geometry2(vo, screen, 1.0, out_geo);
+}
+
+// Copy the parameters in *geo to the vo fields.
+// (Doesn't do anything else - windowing backends should trigger VO_EVENT_RESIZE
+// to ensure that the VO reinitializes rendering properly.)
+void vo_apply_window_geometry(struct vo *vo, const struct vo_win_geometry *geo)
+{
+ vo->dwidth = geo->win.x1 - geo->win.x0;
+ vo->dheight = geo->win.y1 - geo->win.y0;
+ vo->monitor_par = geo->monitor_par;
+}
diff --git a/video/out/win_state.h b/video/out/win_state.h
new file mode 100644
index 0000000..a253efa
--- /dev/null
+++ b/video/out/win_state.h
@@ -0,0 +1,35 @@
+#ifndef MP_WIN_STATE_H_
+#define MP_WIN_STATE_H_
+
+#include "common/common.h"
+
+struct vo;
+
+enum {
+ // By user settings, the window manager's chosen window position should
+ // be overridden.
+ VO_WIN_FORCE_POS = (1 << 0),
+};
+
+struct vo_win_geometry {
+ // Bitfield of VO_WIN_* flags
+ int flags;
+ // Position & size of the window. In xinerama coordinates, i.e. they're
+ // relative to the virtual desktop encompassing all screens, not the
+ // current screen.
+ struct mp_rect win;
+ // Aspect ratio of the current monitor.
+ // (calculated from screen size and options.)
+ double monitor_par;
+};
+
+void vo_calc_window_geometry(struct vo *vo, const struct mp_rect *screen,
+ struct vo_win_geometry *out_geo);
+void vo_calc_window_geometry2(struct vo *vo, const struct mp_rect *screen,
+ double dpi_scale, struct vo_win_geometry *out_geo);
+void vo_calc_window_geometry3(struct vo *vo, const struct mp_rect *screen,
+ const struct mp_rect *monitor,
+ double dpi_scale, struct vo_win_geometry *out_geo);
+void vo_apply_window_geometry(struct vo *vo, const struct vo_win_geometry *geo);
+
+#endif
diff --git a/video/out/wldmabuf/context_wldmabuf.c b/video/out/wldmabuf/context_wldmabuf.c
new file mode 100644
index 0000000..c494575
--- /dev/null
+++ b/video/out/wldmabuf/context_wldmabuf.c
@@ -0,0 +1,43 @@
+/*
+ * This file is part of mpv video player.
+ *
+ * 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 "video/out/wayland_common.h"
+#include "video/out/opengl/context.h"
+#include "ra_wldmabuf.h"
+
+static void uninit(struct ra_ctx *ctx)
+{
+ ra_free(&ctx->ra);
+ vo_wayland_uninit(ctx->vo);
+}
+
+static bool init(struct ra_ctx *ctx)
+{
+ if (!vo_wayland_init(ctx->vo))
+ return false;
+ ctx->ra = ra_create_wayland(ctx->log, ctx->vo);
+
+ return true;
+}
+
+const struct ra_ctx_fns ra_ctx_wldmabuf = {
+ .type = "none",
+ .name = "wldmabuf",
+ .hidden = true,
+ .init = init,
+ .uninit = uninit,
+};
diff --git a/video/out/wldmabuf/ra_wldmabuf.c b/video/out/wldmabuf/ra_wldmabuf.c
new file mode 100644
index 0000000..3f27314
--- /dev/null
+++ b/video/out/wldmabuf/ra_wldmabuf.c
@@ -0,0 +1,66 @@
+/*
+ * 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 "video/out/wayland_common.h"
+#include "video/out/gpu/ra.h"
+#include "ra_wldmabuf.h"
+
+struct priv {
+ struct vo *vo;
+};
+
+static void destroy(struct ra *ra)
+{
+ talloc_free(ra->priv);
+}
+
+bool ra_compatible_format(struct ra* ra, uint32_t drm_format, uint64_t modifier)
+{
+ struct priv* p = ra->priv;
+ struct vo_wayland_state *wl = p->vo->wl;
+ const wayland_format *formats = wl->format_map;
+
+ for (int i = 0; i < wl->format_size / sizeof(wayland_format); i++) {
+ if (drm_format == formats[i].format && modifier == formats[i].modifier)
+ return true;
+ }
+
+ return false;
+}
+
+static struct ra_fns ra_fns_wldmabuf = {
+ .destroy = destroy,
+};
+
+struct ra *ra_create_wayland(struct mp_log *log, struct vo* vo)
+{
+ struct ra *ra = talloc_zero(NULL, struct ra);
+
+ ra->fns = &ra_fns_wldmabuf;
+ ra->log = log;
+ ra_add_native_resource(ra, "wl", vo->wl->display);
+ ra->priv = talloc_zero(NULL, struct priv);
+ struct priv *p = ra->priv;
+ p->vo = vo;
+
+ return ra;
+}
+
+bool ra_is_wldmabuf(struct ra *ra)
+{
+ return (ra->fns == &ra_fns_wldmabuf);
+}
diff --git a/video/out/wldmabuf/ra_wldmabuf.h b/video/out/wldmabuf/ra_wldmabuf.h
new file mode 100644
index 0000000..8e20173
--- /dev/null
+++ b/video/out/wldmabuf/ra_wldmabuf.h
@@ -0,0 +1,23 @@
+/*
+ * 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/>.
+ */
+
+#pragma once
+#include "video/out/wayland_common.h"
+
+struct ra *ra_create_wayland(struct mp_log *log, struct vo *vo);
+bool ra_compatible_format(struct ra* ra, uint32_t drm_format, uint64_t modifier);
+bool ra_is_wldmabuf(struct ra *ra);
diff --git a/video/out/x11_common.c b/video/out/x11_common.c
new file mode 100644
index 0000000..b4605bf
--- /dev/null
+++ b/video/out/x11_common.c
@@ -0,0 +1,2291 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <unistd.h>
+#include <poll.h>
+#include <string.h>
+#include <assert.h>
+
+#include <X11/Xmd.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/Xatom.h>
+#include <X11/keysym.h>
+#include <X11/XKBlib.h>
+#include <X11/XF86keysym.h>
+
+#include <X11/extensions/scrnsaver.h>
+#include <X11/extensions/dpms.h>
+#include <X11/extensions/shape.h>
+#include <X11/extensions/Xpresent.h>
+#include <X11/extensions/Xrandr.h>
+
+#include "misc/bstr.h"
+#include "options/options.h"
+#include "options/m_config.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "input/input.h"
+#include "input/event.h"
+#include "video/image_loader.h"
+#include "video/mp_image.h"
+#include "present_sync.h"
+#include "x11_common.h"
+#include "mpv_talloc.h"
+
+#include "vo.h"
+#include "win_state.h"
+#include "osdep/io.h"
+#include "osdep/poll_wrapper.h"
+#include "osdep/timer.h"
+#include "osdep/subprocess.h"
+
+#include "input/input.h"
+#include "input/keycodes.h"
+
+#define vo_wm_LAYER 1
+#define vo_wm_FULLSCREEN 2
+#define vo_wm_STAYS_ON_TOP 4
+#define vo_wm_ABOVE 8
+#define vo_wm_BELOW 16
+#define vo_wm_STICKY 32
+
+/* EWMH state actions, see
+ http://freedesktop.org/Standards/wm-spec/index.html#id2768769 */
+#define NET_WM_STATE_REMOVE 0 /* remove/unset property */
+#define NET_WM_STATE_ADD 1 /* add/set property */
+#define NET_WM_STATE_TOGGLE 2 /* toggle property */
+
+#define WIN_LAYER_ONBOTTOM 2
+#define WIN_LAYER_NORMAL 4
+#define WIN_LAYER_ONTOP 6
+#define WIN_LAYER_ABOVE_DOCK 10
+
+#define DND_VERSION 5
+
+#define XEMBED_VERSION 0
+#define XEMBED_MAPPED (1 << 0)
+#define XEMBED_EMBEDDED_NOTIFY 0
+#define XEMBED_REQUEST_FOCUS 3
+
+// ----- Motif header: -------
+
+#define MWM_HINTS_FUNCTIONS (1L << 0)
+#define MWM_HINTS_DECORATIONS (1L << 1)
+
+#define MWM_FUNC_RESIZE (1L << 1)
+#define MWM_FUNC_MOVE (1L << 2)
+#define MWM_FUNC_MINIMIZE (1L << 3)
+#define MWM_FUNC_MAXIMIZE (1L << 4)
+#define MWM_FUNC_CLOSE (1L << 5)
+
+#define MWM_DECOR_ALL (1L << 0)
+
+typedef struct
+{
+ long flags;
+ long functions;
+ long decorations;
+ long input_mode;
+ long state;
+} MotifWmHints;
+
+static const char x11_icon_16[] =
+#include "etc/mpv-icon-8bit-16x16.png.inc"
+;
+
+static const char x11_icon_32[] =
+#include "etc/mpv-icon-8bit-32x32.png.inc"
+;
+
+static const char x11_icon_64[] =
+#include "etc/mpv-icon-8bit-64x64.png.inc"
+;
+
+static const char x11_icon_128[] =
+#include "etc/mpv-icon-8bit-128x128.png.inc"
+;
+
+#define ICON_ENTRY(var) { (char *)var, sizeof(var) }
+static const struct bstr x11_icons[] = {
+ ICON_ENTRY(x11_icon_16),
+ ICON_ENTRY(x11_icon_32),
+ ICON_ENTRY(x11_icon_64),
+ ICON_ENTRY(x11_icon_128),
+ {0}
+};
+
+static struct mp_log *x11_error_output;
+static atomic_int x11_error_silence;
+
+static bool rc_overlaps(struct mp_rect rc1, struct mp_rect rc2);
+static void vo_x11_update_geometry(struct vo *vo);
+static void vo_x11_fullscreen(struct vo *vo);
+static void xscreensaver_heartbeat(struct vo_x11_state *x11);
+static void set_screensaver(struct vo_x11_state *x11, bool enabled);
+static void vo_x11_selectinput_witherr(struct vo *vo, Display *display,
+ Window w, long event_mask);
+static void vo_x11_setlayer(struct vo *vo, bool ontop);
+static void vo_x11_xembed_handle_message(struct vo *vo, XClientMessageEvent *ce);
+static void vo_x11_xembed_send_message(struct vo_x11_state *x11, long m[4]);
+static void vo_x11_move_resize(struct vo *vo, bool move, bool resize,
+ struct mp_rect rc);
+static void vo_x11_maximize(struct vo *vo);
+static void vo_x11_minimize(struct vo *vo);
+static void vo_x11_set_input_region(struct vo *vo, bool passthrough);
+static void vo_x11_sticky(struct vo *vo, bool sticky);
+
+#define XA(x11, s) (XInternAtom((x11)->display, # s, False))
+#define XAs(x11, s) XInternAtom((x11)->display, s, False)
+
+#define RC_W(rc) ((rc).x1 - (rc).x0)
+#define RC_H(rc) ((rc).y1 - (rc).y0)
+
+static char *x11_atom_name_buf(struct vo_x11_state *x11, Atom atom,
+ char *buf, size_t buf_size)
+{
+ buf[0] = '\0';
+
+ char *new_name = XGetAtomName(x11->display, atom);
+ if (new_name) {
+ snprintf(buf, buf_size, "%s", new_name);
+ XFree(new_name);
+ }
+
+ return buf;
+}
+
+#define x11_atom_name(x11, atom) x11_atom_name_buf(x11, atom, (char[80]){0}, 80)
+
+// format = 8 (unsigned char), 16 (short), 32 (long, even on LP64 systems)
+// *out_nitems = returned number of items of requested format
+static void *x11_get_property(struct vo_x11_state *x11, Window w, Atom property,
+ Atom type, int format, int *out_nitems)
+{
+ assert(format == 8 || format == 16 || format == 32);
+ *out_nitems = 0;
+ if (!w)
+ return NULL;
+ long max_len = 128 * 1024 * 1024; // static maximum limit
+ Atom ret_type = 0;
+ int ret_format = 0;
+ unsigned long ret_nitems = 0;
+ unsigned long ret_bytesleft = 0;
+ unsigned char *ret_prop = NULL;
+ if (XGetWindowProperty(x11->display, w, property, 0, max_len, False, type,
+ &ret_type, &ret_format, &ret_nitems, &ret_bytesleft,
+ &ret_prop) != Success)
+ return NULL;
+ if (ret_format != format || ret_nitems < 1 || ret_bytesleft) {
+ XFree(ret_prop);
+ ret_prop = NULL;
+ ret_nitems = 0;
+ }
+ *out_nitems = ret_nitems;
+ return ret_prop;
+}
+
+static bool x11_get_property_copy(struct vo_x11_state *x11, Window w,
+ Atom property, Atom type, int format,
+ void *dst, size_t dst_size)
+{
+ bool ret = false;
+ int len;
+ void *ptr = x11_get_property(x11, w, property, type, format, &len);
+ if (ptr) {
+ size_t ib = format == 32 ? sizeof(long) : format / 8;
+ if (dst_size <= len * ib) {
+ memcpy(dst, ptr, dst_size);
+ ret = true;
+ }
+ XFree(ptr);
+ }
+ return ret;
+}
+
+static void x11_send_ewmh_msg(struct vo_x11_state *x11, char *message_type,
+ long params[5])
+{
+ if (!x11->window)
+ return;
+
+ XEvent xev = {
+ .xclient = {
+ .type = ClientMessage,
+ .send_event = True,
+ .message_type = XInternAtom(x11->display, message_type, False),
+ .window = x11->window,
+ .format = 32,
+ },
+ };
+ for (int n = 0; n < 5; n++)
+ xev.xclient.data.l[n] = params[n];
+
+ if (!XSendEvent(x11->display, x11->rootwin, False,
+ SubstructureRedirectMask | SubstructureNotifyMask,
+ &xev))
+ MP_ERR(x11, "Couldn't send EWMH %s message!\n", message_type);
+}
+
+// change the _NET_WM_STATE hint. Remove or add the state according to "set".
+static void x11_set_ewmh_state(struct vo_x11_state *x11, char *state, bool set)
+{
+ long params[5] = {
+ set ? NET_WM_STATE_ADD : NET_WM_STATE_REMOVE,
+ XInternAtom(x11->display, state, False),
+ 0, // No second state
+ 1, // source indication: normal
+ };
+ x11_send_ewmh_msg(x11, "_NET_WM_STATE", params);
+}
+
+static void vo_update_cursor(struct vo *vo)
+{
+ Cursor no_ptr;
+ Pixmap bm_no;
+ XColor black, dummy;
+ Colormap colormap;
+ const char bm_no_data[] = {0, 0, 0, 0, 0, 0, 0, 0};
+ struct vo_x11_state *x11 = vo->x11;
+ Display *disp = x11->display;
+ Window win = x11->window;
+ bool should_hide = x11->has_focus && !x11->mouse_cursor_visible;
+
+ if (should_hide == x11->mouse_cursor_set)
+ return;
+
+ x11->mouse_cursor_set = should_hide;
+
+ if (x11->parent == x11->rootwin || !win)
+ return; // do not hide if playing on the root window
+
+ if (x11->mouse_cursor_set) {
+ colormap = DefaultColormap(disp, DefaultScreen(disp));
+ if (!XAllocNamedColor(disp, colormap, "black", &black, &dummy))
+ return; // color alloc failed, give up
+ bm_no = XCreateBitmapFromData(disp, win, bm_no_data, 8, 8);
+ no_ptr = XCreatePixmapCursor(disp, bm_no, bm_no, &black, &black, 0, 0);
+ XDefineCursor(disp, win, no_ptr);
+ XFreeCursor(disp, no_ptr);
+ if (bm_no != None)
+ XFreePixmap(disp, bm_no);
+ XFreeColors(disp, colormap, &black.pixel, 1, 0);
+ } else {
+ XDefineCursor(x11->display, x11->window, 0);
+ }
+}
+
+static int x11_errorhandler(Display *display, XErrorEvent *event)
+{
+ struct mp_log *log = x11_error_output;
+ if (!log)
+ return 0;
+
+ char msg[60];
+ XGetErrorText(display, event->error_code, (char *) &msg, sizeof(msg));
+
+ int lev = atomic_load(&x11_error_silence) ? MSGL_V : MSGL_ERR;
+ mp_msg(log, lev, "X11 error: %s\n", msg);
+ mp_msg(log, lev, "Type: %x, display: %p, resourceid: %lx, serial: %lx\n",
+ event->type, event->display, event->resourceid, event->serial);
+ mp_msg(log, lev, "Error code: %x, request code: %x, minor code: %x\n",
+ event->error_code, event->request_code, event->minor_code);
+
+ return 0;
+}
+
+void vo_x11_silence_xlib(int dir)
+{
+ atomic_fetch_add(&x11_error_silence, dir);
+}
+
+static int net_wm_support_state_test(struct vo_x11_state *x11, Atom atom)
+{
+#define NET_WM_STATE_TEST(x) { \
+ if (atom == XA(x11, _NET_WM_STATE_##x)) { \
+ MP_DBG(x11, "Detected wm supports " #x " state.\n" ); \
+ return vo_wm_##x; \
+ } \
+}
+
+ NET_WM_STATE_TEST(FULLSCREEN);
+ NET_WM_STATE_TEST(ABOVE);
+ NET_WM_STATE_TEST(STAYS_ON_TOP);
+ NET_WM_STATE_TEST(BELOW);
+ NET_WM_STATE_TEST(STICKY);
+ return 0;
+}
+
+static int vo_wm_detect(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ int i;
+ int wm = 0;
+ int nitems;
+ Atom *args = NULL;
+ Window win = x11->rootwin;
+
+ if (x11->parent)
+ return 0;
+
+// -- supports layers
+ args = x11_get_property(x11, win, XA(x11, _WIN_PROTOCOLS), XA_ATOM, 32,
+ &nitems);
+ if (args) {
+ for (i = 0; i < nitems; i++) {
+ if (args[i] == XA(x11, _WIN_LAYER)) {
+ MP_DBG(x11, "Detected wm supports layers.\n");
+ wm |= vo_wm_LAYER;
+ }
+ }
+ XFree(args);
+ }
+// --- netwm
+ args = x11_get_property(x11, win, XA(x11, _NET_SUPPORTED), XA_ATOM, 32,
+ &nitems);
+ if (args) {
+ MP_DBG(x11, "Detected wm supports NetWM.\n");
+ if (x11->opts->x11_netwm >= 0) {
+ for (i = 0; i < nitems; i++)
+ wm |= net_wm_support_state_test(vo->x11, args[i]);
+ } else {
+ MP_DBG(x11, "NetWM usage disabled by user.\n");
+ }
+ XFree(args);
+ }
+
+ if (wm == 0)
+ MP_DBG(x11, "Unknown wm type...\n");
+ if (x11->opts->x11_netwm > 0 && !(wm & vo_wm_FULLSCREEN)) {
+ MP_WARN(x11, "Forcing NetWM FULLSCREEN support.\n");
+ wm |= vo_wm_FULLSCREEN;
+ }
+ return wm;
+}
+
+static void xpresent_set(struct vo_x11_state *x11)
+{
+ int present = x11->opts->x11_present;
+ x11->use_present = x11->present_code &&
+ ((x11->has_mesa && !x11->has_nvidia && present) ||
+ present == 2);
+ if (x11->use_present) {
+ MP_VERBOSE(x11, "XPresent enabled.\n");
+ } else {
+ MP_VERBOSE(x11, "XPresent disabled.\n");
+ }
+}
+
+static void xrandr_read(struct vo_x11_state *x11)
+{
+ for(int i = 0; i < x11->num_displays; i++)
+ talloc_free(x11->displays[i].name);
+
+ x11->num_displays = 0;
+
+ if (x11->xrandr_event < 0) {
+ int event_base, error_base;
+ if (!XRRQueryExtension(x11->display, &event_base, &error_base)) {
+ MP_VERBOSE(x11, "Couldn't init Xrandr.\n");
+ return;
+ }
+ x11->xrandr_event = event_base + RRNotify;
+ XRRSelectInput(x11->display, x11->rootwin, RRScreenChangeNotifyMask |
+ RRCrtcChangeNotifyMask | RROutputChangeNotifyMask);
+ }
+
+ XRRScreenResources *r = XRRGetScreenResourcesCurrent(x11->display, x11->rootwin);
+ if (!r) {
+ MP_VERBOSE(x11, "Xrandr doesn't work.\n");
+ return;
+ }
+
+ /* Look at the available providers on the current screen and try to determine
+ * the driver. If amd/intel/radeon, assume this is mesa. If nvidia is found,
+ * assume nvidia. Because the same screen can have multiple providers (e.g.
+ * a laptop with switchable graphics), we need to know both of these things.
+ * In practice, this is used for determining whether or not to use XPresent
+ * (i.e. needs to be Mesa and not Nvidia). Requires Randr 1.4. */
+ XRRProviderResources *pr = XRRGetProviderResources(x11->display, x11->rootwin);
+ for (int i = 0; i < pr->nproviders; i++) {
+ XRRProviderInfo *info = XRRGetProviderInfo(x11->display, r, pr->providers[i]);
+ struct bstr provider_name = bstrdup(x11, bstr0(info->name));
+ bstr_lower(provider_name);
+ int amd = bstr_find0(provider_name, "amd");
+ int intel = bstr_find0(provider_name, "intel");
+ int modesetting = bstr_find0(provider_name, "modesetting");
+ int nouveau = bstr_find0(provider_name, "nouveau");
+ int nvidia = bstr_find0(provider_name, "nvidia");
+ int radeon = bstr_find0(provider_name, "radeon");
+ x11->has_mesa = x11->has_mesa || amd >= 0 || intel >= 0 ||
+ modesetting >= 0 || nouveau >= 0 || radeon >= 0;
+ x11->has_nvidia = x11->has_nvidia || nvidia >= 0;
+ XRRFreeProviderInfo(info);
+ }
+ if (x11->present_code)
+ xpresent_set(x11);
+ XRRFreeProviderResources(pr);
+
+ int primary_id = -1;
+ RROutput primary = XRRGetOutputPrimary(x11->display, x11->rootwin);
+ for (int o = 0; o < r->noutput; o++) {
+ RROutput output = r->outputs[o];
+ XRRCrtcInfo *crtc = NULL;
+ XRROutputInfo *out = XRRGetOutputInfo(x11->display, r, output);
+ if (!out || !out->crtc)
+ goto next;
+ crtc = XRRGetCrtcInfo(x11->display, r, out->crtc);
+ if (!crtc)
+ goto next;
+ for (int om = 0; om < out->nmode; om++) {
+ RRMode xm = out->modes[om];
+ for (int n = 0; n < r->nmode; n++) {
+ XRRModeInfo m = r->modes[n];
+ if (m.id != xm || crtc->mode != xm)
+ continue;
+ if (x11->num_displays >= MAX_DISPLAYS)
+ continue;
+ double vTotal = m.vTotal;
+ if (m.modeFlags & RR_DoubleScan)
+ vTotal *= 2;
+ if (m.modeFlags & RR_Interlace)
+ vTotal /= 2;
+ struct xrandr_display d = {
+ .rc = { crtc->x, crtc->y,
+ crtc->x + crtc->width, crtc->y + crtc->height },
+ .fps = m.dotClock / (m.hTotal * vTotal),
+ .name = talloc_strdup(x11, out->name),
+ };
+ int num = x11->num_displays++;
+ MP_VERBOSE(x11, "Display %d (%s): [%d, %d, %d, %d] @ %f FPS\n",
+ num, d.name, d.rc.x0, d.rc.y0, d.rc.x1, d.rc.y1, d.fps);
+ x11->displays[num] = d;
+ if (output == primary)
+ primary_id = num;
+ }
+ }
+ next:
+ if (crtc)
+ XRRFreeCrtcInfo(crtc);
+ if (out)
+ XRRFreeOutputInfo(out);
+ }
+
+ for (int i = 0; i < x11->num_displays; i++) {
+ struct xrandr_display *d = &(x11->displays[i]);
+ d->screen = i;
+
+ if (i == primary_id) {
+ d->atom_id = 0;
+ continue;
+ }
+ if (primary_id > 0 && i < primary_id) {
+ d->atom_id = i+1;
+ continue;
+ }
+ d->atom_id = i;
+ }
+
+ XRRFreeScreenResources(r);
+}
+
+static int vo_x11_select_screen(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+ int screen = -2; // all displays
+ if (!opts->fullscreen || opts->fsscreen_id != -2) {
+ screen = opts->fullscreen ? opts->fsscreen_id : opts->screen_id;
+ if (opts->fullscreen && opts->fsscreen_id == -1)
+ screen = opts->screen_id;
+
+ if (screen == -1 && (opts->fsscreen_name || opts->screen_name)) {
+ char *screen_name = opts->fullscreen ? opts->fsscreen_name : opts->screen_name;
+ if (screen_name) {
+ bool screen_found = false;
+ for (int n = 0; n < x11->num_displays; n++) {
+ char *display_name = x11->displays[n].name;
+ if (!strcmp(display_name, screen_name)) {
+ screen = n;
+ screen_found = true;
+ break;
+ }
+ }
+ if (!screen_found)
+ MP_WARN(x11, "Screen name %s not found!\n", screen_name);
+ }
+ }
+
+ if (screen >= x11->num_displays)
+ screen = x11->num_displays - 1;
+ }
+ return screen;
+}
+
+static void vo_x11_update_screeninfo(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ x11->screenrc = (struct mp_rect){.x1 = x11->ws_width, .y1 = x11->ws_height};
+ int screen = vo_x11_select_screen(vo);
+ if (screen >= -1) {
+ if (screen == -1) {
+ int x = x11->winrc.x0 + RC_W(x11->winrc) / 2;
+ int y = x11->winrc.y0 + RC_H(x11->winrc) / 2;
+ for (screen = x11->num_displays - 1; screen > 0; screen--) {
+ struct xrandr_display *disp = &x11->displays[screen];
+ int left = disp->rc.x0;
+ int right = disp->rc.x1;
+ int top = disp->rc.y0;
+ int bottom = disp->rc.y1;
+ if (left <= x && x <= right && top <= y && y <= bottom)
+ break;
+ }
+ }
+
+ if (screen < 0)
+ screen = 0;
+ x11->screenrc = (struct mp_rect){
+ .x0 = x11->displays[screen].rc.x0,
+ .y0 = x11->displays[screen].rc.y0,
+ .x1 = x11->displays[screen].rc.x1,
+ .y1 = x11->displays[screen].rc.y1,
+ };
+ }
+}
+
+static struct xrandr_display *get_current_display(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct xrandr_display *selected_disp = NULL;
+ for (int n = 0; n < x11->num_displays; n++) {
+ struct xrandr_display *disp = &x11->displays[n];
+ disp->overlaps = rc_overlaps(disp->rc, x11->winrc);
+ if (disp->overlaps && (!selected_disp || disp->fps < selected_disp->fps))
+ selected_disp = disp;
+ }
+ return selected_disp;
+}
+
+// Get the monitors for the 4 edges of the rectangle spanning all screens.
+static void vo_x11_get_bounding_monitors(struct vo_x11_state *x11, long b[4])
+{
+ //top bottom left right
+ b[0] = b[1] = b[2] = b[3] = 0;
+ for (int n = 0; n < x11->num_displays; n++) {
+ struct xrandr_display *d = &x11->displays[n];
+ if (d->rc.y0 < x11->displays[b[0]].rc.y0)
+ b[0] = n;
+ if (d->rc.y1 < x11->displays[b[1]].rc.y1)
+ b[1] = n;
+ if (d->rc.x0 < x11->displays[b[2]].rc.x0)
+ b[2] = n;
+ if (d->rc.x1 < x11->displays[b[3]].rc.x1)
+ b[3] = n;
+ }
+}
+
+bool vo_x11_init(struct vo *vo)
+{
+ char *dispName;
+
+ assert(!vo->x11);
+
+ XInitThreads();
+
+ struct vo_x11_state *x11 = talloc_ptrtype(NULL, x11);
+ *x11 = (struct vo_x11_state){
+ .log = mp_log_new(x11, vo->log, "x11"),
+ .input_ctx = vo->input_ctx,
+ .screensaver_enabled = true,
+ .xrandr_event = -1,
+ .wakeup_pipe = {-1, -1},
+ .dpi_scale = 1,
+ .opts_cache = m_config_cache_alloc(x11, vo->global, &vo_sub_opts),
+ };
+ x11->opts = x11->opts_cache->opts;
+ vo->x11 = x11;
+
+ x11_error_output = x11->log;
+ XSetErrorHandler(x11_errorhandler);
+ x11->present = mp_present_initialize(x11, x11->opts, VO_MAX_SWAPCHAIN_DEPTH);
+
+ dispName = XDisplayName(NULL);
+
+ MP_VERBOSE(x11, "X11 opening display: %s\n", dispName);
+
+ x11->display = XOpenDisplay(dispName);
+ if (!x11->display) {
+ MP_MSG(x11, vo->probing ? MSGL_V : MSGL_ERR,
+ "couldn't open the X11 display (%s)!\n", dispName);
+ goto error;
+ }
+ x11->screen = DefaultScreen(x11->display); // screen ID
+ x11->rootwin = RootWindow(x11->display, x11->screen); // root window ID
+
+ if (x11->opts->WinID >= 0)
+ x11->parent = x11->opts->WinID ? x11->opts->WinID : x11->rootwin;
+
+ if (!x11->opts->native_keyrepeat) {
+ Bool ok = False;
+ XkbSetDetectableAutoRepeat(x11->display, True, &ok);
+ x11->no_autorepeat = ok;
+ }
+
+ x11->xim = XOpenIM(x11->display, NULL, NULL, NULL);
+ if (!x11->xim)
+ MP_WARN(x11, "XOpenIM() failed. Unicode input will not work.\n");
+
+ x11->ws_width = DisplayWidth(x11->display, x11->screen);
+ x11->ws_height = DisplayHeight(x11->display, x11->screen);
+
+ if (strncmp(dispName, "unix:", 5) == 0)
+ dispName += 4;
+ else if (strncmp(dispName, "localhost:", 10) == 0)
+ dispName += 9;
+ x11->display_is_local = dispName[0] == ':' &&
+ strtoul(dispName + 1, NULL, 10) < 10;
+ MP_DBG(x11, "X11 running at %dx%d (\"%s\" => %s display)\n",
+ x11->ws_width, x11->ws_height, dispName,
+ x11->display_is_local ? "local" : "remote");
+
+ int w_mm = DisplayWidthMM(x11->display, x11->screen);
+ int h_mm = DisplayHeightMM(x11->display, x11->screen);
+ double dpi_x = x11->ws_width * 25.4 / w_mm;
+ double dpi_y = x11->ws_height * 25.4 / h_mm;
+ double base_dpi = 96;
+ if (isfinite(dpi_x) && isfinite(dpi_y) && x11->opts->hidpi_window_scale) {
+ int s_x = lrint(MPCLAMP(dpi_x / base_dpi, 0, 10));
+ int s_y = lrint(MPCLAMP(dpi_y / base_dpi, 0, 10));
+ if (s_x == s_y && s_x > 1 && s_x < 10) {
+ x11->dpi_scale = s_x;
+ MP_VERBOSE(x11, "Assuming DPI scale %d for prescaling. This can "
+ "be disabled with --hidpi-window-scale=no.\n",
+ x11->dpi_scale);
+ }
+ }
+
+ x11->wm_type = vo_wm_detect(vo);
+
+ x11->event_fd = ConnectionNumber(x11->display);
+ mp_make_wakeup_pipe(x11->wakeup_pipe);
+
+ xrandr_read(x11);
+
+ vo_x11_update_geometry(vo);
+
+ return true;
+
+error:
+ vo_x11_uninit(vo);
+ return false;
+}
+
+static const struct mp_keymap keymap[] = {
+ // special keys
+ {XK_Pause, MP_KEY_PAUSE}, {XK_Escape, MP_KEY_ESC},
+ {XK_BackSpace, MP_KEY_BS}, {XK_Tab, MP_KEY_TAB}, {XK_Return, MP_KEY_ENTER},
+ {XK_Menu, MP_KEY_MENU}, {XK_Print, MP_KEY_PRINT},
+ {XK_Cancel, MP_KEY_CANCEL}, {XK_ISO_Left_Tab, MP_KEY_TAB},
+
+ // cursor keys
+ {XK_Left, MP_KEY_LEFT}, {XK_Right, MP_KEY_RIGHT}, {XK_Up, MP_KEY_UP},
+ {XK_Down, MP_KEY_DOWN},
+
+ // navigation block
+ {XK_Insert, MP_KEY_INSERT}, {XK_Delete, MP_KEY_DELETE},
+ {XK_Home, MP_KEY_HOME}, {XK_End, MP_KEY_END}, {XK_Page_Up, MP_KEY_PAGE_UP},
+ {XK_Page_Down, MP_KEY_PAGE_DOWN},
+
+ // F-keys
+ {XK_F1, MP_KEY_F+1}, {XK_F2, MP_KEY_F+2}, {XK_F3, MP_KEY_F+3},
+ {XK_F4, MP_KEY_F+4}, {XK_F5, MP_KEY_F+5}, {XK_F6, MP_KEY_F+6},
+ {XK_F7, MP_KEY_F+7}, {XK_F8, MP_KEY_F+8}, {XK_F9, MP_KEY_F+9},
+ {XK_F10, MP_KEY_F+10}, {XK_F11, MP_KEY_F+11}, {XK_F12, MP_KEY_F+12},
+ {XK_F13, MP_KEY_F+13}, {XK_F14, MP_KEY_F+14}, {XK_F15, MP_KEY_F+15},
+ {XK_F16, MP_KEY_F+16}, {XK_F17, MP_KEY_F+17}, {XK_F18, MP_KEY_F+18},
+ {XK_F19, MP_KEY_F+19}, {XK_F20, MP_KEY_F+20}, {XK_F21, MP_KEY_F+21},
+ {XK_F22, MP_KEY_F+22}, {XK_F23, MP_KEY_F+23}, {XK_F24, MP_KEY_F+24},
+
+ // numpad independent of numlock
+ {XK_KP_Subtract, '-'}, {XK_KP_Add, '+'}, {XK_KP_Multiply, '*'},
+ {XK_KP_Divide, '/'}, {XK_KP_Enter, MP_KEY_KPENTER},
+
+ // numpad with numlock
+ {XK_KP_0, MP_KEY_KP0}, {XK_KP_1, MP_KEY_KP1}, {XK_KP_2, MP_KEY_KP2},
+ {XK_KP_3, MP_KEY_KP3}, {XK_KP_4, MP_KEY_KP4}, {XK_KP_5, MP_KEY_KP5},
+ {XK_KP_6, MP_KEY_KP6}, {XK_KP_7, MP_KEY_KP7}, {XK_KP_8, MP_KEY_KP8},
+ {XK_KP_9, MP_KEY_KP9}, {XK_KP_Decimal, MP_KEY_KPDEC},
+ {XK_KP_Separator, MP_KEY_KPDEC},
+
+ // numpad without numlock
+ {XK_KP_Insert, MP_KEY_KPINS}, {XK_KP_End, MP_KEY_KPEND},
+ {XK_KP_Down, MP_KEY_KPDOWN}, {XK_KP_Page_Down, MP_KEY_KPPGDOWN},
+ {XK_KP_Left, MP_KEY_KPLEFT}, {XK_KP_Begin, MP_KEY_KP5},
+ {XK_KP_Right, MP_KEY_KPRIGHT}, {XK_KP_Home, MP_KEY_KPHOME}, {XK_KP_Up, MP_KEY_KPUP},
+ {XK_KP_Page_Up, MP_KEY_KPPGUP}, {XK_KP_Delete, MP_KEY_KPDEL},
+
+ {XF86XK_MenuKB, MP_KEY_MENU},
+ {XF86XK_AudioPlay, MP_KEY_PLAY}, {XF86XK_AudioPause, MP_KEY_PAUSE},
+ {XF86XK_AudioStop, MP_KEY_STOP},
+ {XF86XK_AudioPrev, MP_KEY_PREV}, {XF86XK_AudioNext, MP_KEY_NEXT},
+ {XF86XK_AudioRewind, MP_KEY_REWIND}, {XF86XK_AudioForward, MP_KEY_FORWARD},
+ {XF86XK_AudioMute, MP_KEY_MUTE},
+ {XF86XK_AudioLowerVolume, MP_KEY_VOLUME_DOWN},
+ {XF86XK_AudioRaiseVolume, MP_KEY_VOLUME_UP},
+ {XF86XK_HomePage, MP_KEY_HOMEPAGE}, {XF86XK_WWW, MP_KEY_WWW},
+ {XF86XK_Mail, MP_KEY_MAIL}, {XF86XK_Favorites, MP_KEY_FAVORITES},
+ {XF86XK_Search, MP_KEY_SEARCH}, {XF86XK_Sleep, MP_KEY_SLEEP},
+ {XF86XK_Back, MP_KEY_BACK}, {XF86XK_Tools, MP_KEY_TOOLS},
+ {XF86XK_ZoomIn, MP_KEY_ZOOMIN}, {XF86XK_ZoomOut, MP_KEY_ZOOMOUT},
+
+ {0, 0}
+};
+
+static int vo_x11_lookupkey(int key)
+{
+ const char *passthrough_keys = " -+*/<>`~!@#$%^&()_{}:;\"\',.?\\|=[]";
+ int mpkey = 0;
+ if ((key >= 'a' && key <= 'z') ||
+ (key >= 'A' && key <= 'Z') ||
+ (key >= '0' && key <= '9') ||
+ (key > 0 && key < 256 && strchr(passthrough_keys, key)))
+ mpkey = key;
+
+ if (!mpkey)
+ mpkey = lookup_keymap_table(keymap, key);
+
+ // XFree86 keysym range; typically contains obscure "extra" keys
+ if (!mpkey && key >= 0x10080001 && key <= 0x1008FFFF) {
+ mpkey = MP_KEY_UNKNOWN_RESERVED_START + (key - 0x10080000);
+ if (mpkey > MP_KEY_UNKNOWN_RESERVED_LAST)
+ mpkey = 0;
+ }
+
+ return mpkey;
+}
+
+static void vo_x11_decoration(struct vo *vo, bool d)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (x11->parent || !x11->window)
+ return;
+
+ Atom motif_hints = XA(x11, _MOTIF_WM_HINTS);
+ MotifWmHints mhints = {0};
+ bool got = x11_get_property_copy(x11, x11->window, motif_hints,
+ motif_hints, 32, &mhints, sizeof(mhints));
+ // hints weren't set, and decorations requested -> assume WM displays them
+ if (!got && d)
+ return;
+ if (!got) {
+ mhints.flags = MWM_HINTS_FUNCTIONS;
+ mhints.functions = MWM_FUNC_MOVE | MWM_FUNC_CLOSE | MWM_FUNC_MINIMIZE |
+ MWM_FUNC_MAXIMIZE | MWM_FUNC_RESIZE;
+ }
+ mhints.flags |= MWM_HINTS_DECORATIONS;
+ mhints.decorations = d ? MWM_DECOR_ALL : 0;
+ XChangeProperty(x11->display, x11->window, motif_hints, motif_hints, 32,
+ PropModeReplace, (unsigned char *) &mhints, 5);
+}
+
+static void vo_x11_wm_hints(struct vo *vo, Window window)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ XWMHints hints = {0};
+ hints.flags = InputHint | StateHint;
+ hints.input = 1;
+ hints.initial_state = NormalState;
+ XSetWMHints(x11->display, window, &hints);
+}
+
+static void vo_x11_classhint(struct vo *vo, Window window, const char *name)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+ XClassHint wmClass;
+ long pid = getpid();
+
+ wmClass.res_name = opts->winname ? opts->winname : (char *)name;
+ wmClass.res_class = "mpv";
+ XSetClassHint(x11->display, window, &wmClass);
+ XChangeProperty(x11->display, window, XA(x11, _NET_WM_PID), XA_CARDINAL,
+ 32, PropModeReplace, (unsigned char *) &pid, 1);
+}
+
+void vo_x11_uninit(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (!x11)
+ return;
+
+ mp_input_put_key(x11->input_ctx, MP_INPUT_RELEASE_ALL);
+
+ set_screensaver(x11, true);
+
+ if (x11->window != None && x11->window != x11->rootwin)
+ XDestroyWindow(x11->display, x11->window);
+ if (x11->xic)
+ XDestroyIC(x11->xic);
+ if (x11->colormap != None)
+ XFreeColormap(vo->x11->display, x11->colormap);
+
+ MP_DBG(x11, "uninit ...\n");
+ if (x11->xim)
+ XCloseIM(x11->xim);
+ if (x11->display) {
+ XSetErrorHandler(NULL);
+ x11_error_output = NULL;
+ XCloseDisplay(x11->display);
+ }
+
+ if (x11->wakeup_pipe[0] >= 0) {
+ close(x11->wakeup_pipe[0]);
+ close(x11->wakeup_pipe[1]);
+ }
+
+ talloc_free(x11);
+ vo->x11 = NULL;
+}
+
+#define DND_PROPERTY "mpv_dnd_selection"
+
+static void vo_x11_dnd_init_window(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ Atom version = DND_VERSION;
+ XChangeProperty(x11->display, x11->window, XA(x11, XdndAware), XA_ATOM,
+ 32, PropModeReplace, (unsigned char *)&version, 1);
+}
+
+// The Atom does not always map to a mime type, but often.
+static char *x11_dnd_mime_type_buf(struct vo_x11_state *x11, Atom atom,
+ char *buf, size_t buf_size)
+{
+ if (atom == XInternAtom(x11->display, "UTF8_STRING", False))
+ return "text";
+ return x11_atom_name_buf(x11, atom, buf, buf_size);
+}
+
+#define x11_dnd_mime_type(x11, atom) \
+ x11_dnd_mime_type_buf(x11, atom, (char[80]){0}, 80)
+
+static bool dnd_format_is_better(struct vo_x11_state *x11, Atom cur, Atom new)
+{
+ int new_score = mp_event_get_mime_type_score(x11->input_ctx,
+ x11_dnd_mime_type(x11, new));
+ int cur_score = -1;
+ if (cur) {
+ cur_score = mp_event_get_mime_type_score(x11->input_ctx,
+ x11_dnd_mime_type(x11, cur));
+ }
+ return new_score >= 0 && new_score > cur_score;
+}
+
+static void dnd_select_format(struct vo_x11_state *x11, Atom *args, int items)
+{
+ x11->dnd_requested_format = 0;
+
+ for (int n = 0; n < items; n++) {
+ MP_VERBOSE(x11, "DnD type: '%s'\n", x11_atom_name(x11, args[n]));
+ // There are other types; possibly not worth supporting.
+ if (dnd_format_is_better(x11, x11->dnd_requested_format, args[n]))
+ x11->dnd_requested_format = args[n];
+ }
+
+ MP_VERBOSE(x11, "Selected DnD type: %s\n", x11->dnd_requested_format ?
+ x11_atom_name(x11, x11->dnd_requested_format) : "(none)");
+}
+
+static void dnd_reset(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ x11->dnd_src_window = 0;
+ x11->dnd_requested_format = 0;
+}
+
+static void vo_x11_dnd_handle_message(struct vo *vo, XClientMessageEvent *ce)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (!x11->window)
+ return;
+
+ if (ce->message_type == XA(x11, XdndEnter)) {
+ x11->dnd_requested_format = 0;
+
+ Window src = ce->data.l[0];
+ if (ce->data.l[1] & 1) {
+ int nitems;
+ Atom *args = x11_get_property(x11, src, XA(x11, XdndTypeList),
+ XA_ATOM, 32, &nitems);
+ if (args) {
+ dnd_select_format(x11, args, nitems);
+ XFree(args);
+ }
+ } else {
+ Atom args[3];
+ for (int n = 2; n <= 4; n++)
+ args[n - 2] = ce->data.l[n];
+ dnd_select_format(x11, args, 3);
+ }
+ } else if (ce->message_type == XA(x11, XdndPosition)) {
+ x11->dnd_requested_action = ce->data.l[4];
+
+ Window src = ce->data.l[0];
+ XEvent xev;
+
+ xev.xclient.type = ClientMessage;
+ xev.xclient.serial = 0;
+ xev.xclient.send_event = True;
+ xev.xclient.message_type = XA(x11, XdndStatus);
+ xev.xclient.window = src;
+ xev.xclient.format = 32;
+ xev.xclient.data.l[0] = x11->window;
+ xev.xclient.data.l[1] = x11->dnd_requested_format ? 1 : 0;
+ xev.xclient.data.l[2] = 0;
+ xev.xclient.data.l[3] = 0;
+ xev.xclient.data.l[4] = XA(x11, XdndActionCopy);
+
+ XSendEvent(x11->display, src, False, 0, &xev);
+ } else if (ce->message_type == XA(x11, XdndDrop)) {
+ x11->dnd_src_window = ce->data.l[0];
+ XConvertSelection(x11->display, XA(x11, XdndSelection),
+ x11->dnd_requested_format, XAs(x11, DND_PROPERTY),
+ x11->window, ce->data.l[2]);
+ } else if (ce->message_type == XA(x11, XdndLeave)) {
+ dnd_reset(vo);
+ }
+}
+
+static void vo_x11_dnd_handle_selection(struct vo *vo, XSelectionEvent *se)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (!x11->window || !x11->dnd_src_window)
+ return;
+
+ bool success = false;
+
+ if (se->selection == XA(x11, XdndSelection) &&
+ se->property == XAs(x11, DND_PROPERTY) &&
+ se->target == x11->dnd_requested_format &&
+ x11->opts->drag_and_drop != -2)
+ {
+ int nitems;
+ void *prop = x11_get_property(x11, x11->window, XAs(x11, DND_PROPERTY),
+ x11->dnd_requested_format, 8, &nitems);
+ if (prop) {
+ enum mp_dnd_action action;
+ if (x11->opts->drag_and_drop >= 0) {
+ action = x11->opts->drag_and_drop;
+ } else {
+ action = x11->dnd_requested_action == XA(x11, XdndActionCopy) ?
+ DND_REPLACE : DND_APPEND;
+ }
+
+ char *mime_type = x11_dnd_mime_type(x11, x11->dnd_requested_format);
+ MP_VERBOSE(x11, "Dropping type: %s (%s)\n",
+ x11_atom_name(x11, x11->dnd_requested_format), mime_type);
+
+ // No idea if this is guaranteed to be \0-padded, so use bstr.
+ success = mp_event_drop_mime_data(x11->input_ctx, mime_type,
+ (bstr){prop, nitems}, action) > 0;
+ XFree(prop);
+ }
+ }
+
+ XEvent xev;
+
+ xev.xclient.type = ClientMessage;
+ xev.xclient.serial = 0;
+ xev.xclient.send_event = True;
+ xev.xclient.message_type = XA(x11, XdndFinished);
+ xev.xclient.window = x11->dnd_src_window;
+ xev.xclient.format = 32;
+ xev.xclient.data.l[0] = x11->window;
+ xev.xclient.data.l[1] = success ? 1 : 0;
+ xev.xclient.data.l[2] = success ? XA(x11, XdndActionCopy) : 0;
+ xev.xclient.data.l[3] = 0;
+ xev.xclient.data.l[4] = 0;
+
+ XSendEvent(x11->display, x11->dnd_src_window, False, 0, &xev);
+
+ dnd_reset(vo);
+}
+
+static void update_vo_size(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (RC_W(x11->winrc) != vo->dwidth || RC_H(x11->winrc) != vo->dheight) {
+ vo->dwidth = RC_W(x11->winrc);
+ vo->dheight = RC_H(x11->winrc);
+ x11->pending_vo_events |= VO_EVENT_RESIZE;
+ }
+}
+
+static int get_mods(unsigned int state)
+{
+ int modifiers = 0;
+ if (state & ShiftMask)
+ modifiers |= MP_KEY_MODIFIER_SHIFT;
+ if (state & ControlMask)
+ modifiers |= MP_KEY_MODIFIER_CTRL;
+ if (state & Mod1Mask)
+ modifiers |= MP_KEY_MODIFIER_ALT;
+ if (state & Mod4Mask)
+ modifiers |= MP_KEY_MODIFIER_META;
+ return modifiers;
+}
+
+static void vo_x11_update_composition_hint(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ long hint = 0;
+ switch (x11->opts->x11_bypass_compositor) {
+ case 0: hint = 0; break; // leave default
+ case 1: hint = 1; break; // always bypass
+ case 2: hint = x11->fs ? 1 : 0; break; // bypass in FS
+ case 3: hint = 2; break; // always enable
+ }
+
+ XChangeProperty(x11->display, x11->window, XA(x11,_NET_WM_BYPASS_COMPOSITOR),
+ XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&hint, 1);
+}
+
+static void vo_x11_check_net_wm_state_change(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+
+ if (x11->parent)
+ return;
+
+ if (x11->wm_type & vo_wm_FULLSCREEN) {
+ int num_elems;
+ long *elems = x11_get_property(x11, x11->window, XA(x11, _NET_WM_STATE),
+ XA_ATOM, 32, &num_elems);
+ int is_fullscreen = 0, is_minimized = 0, is_maximized = 0;
+ if (elems) {
+ Atom fullscreen_prop = XA(x11, _NET_WM_STATE_FULLSCREEN);
+ Atom hidden = XA(x11, _NET_WM_STATE_HIDDEN);
+ Atom max_vert = XA(x11, _NET_WM_STATE_MAXIMIZED_VERT);
+ Atom max_horiz = XA(x11, _NET_WM_STATE_MAXIMIZED_HORZ);
+ for (int n = 0; n < num_elems; n++) {
+ if (elems[n] == fullscreen_prop)
+ is_fullscreen = 1;
+ if (elems[n] == hidden)
+ is_minimized = 1;
+ if (elems[n] == max_vert || elems[n] == max_horiz)
+ is_maximized = 1;
+ }
+ XFree(elems);
+ }
+
+ if (opts->window_maximized && !is_maximized && x11->geometry_change) {
+ x11->geometry_change = false;
+ vo_x11_config_vo_window(vo);
+ }
+
+ opts->window_minimized = is_minimized;
+ x11->hidden = is_minimized;
+ m_config_cache_write_opt(x11->opts_cache, &opts->window_minimized);
+ opts->window_maximized = is_maximized;
+ m_config_cache_write_opt(x11->opts_cache, &opts->window_maximized);
+
+ if ((x11->opts->fullscreen && !is_fullscreen) ||
+ (!x11->opts->fullscreen && is_fullscreen))
+ {
+ x11->opts->fullscreen = is_fullscreen;
+ x11->fs = is_fullscreen;
+ m_config_cache_write_opt(x11->opts_cache, &x11->opts->fullscreen);
+
+ if (!is_fullscreen && (x11->pos_changed_during_fs ||
+ x11->size_changed_during_fs))
+ {
+ vo_x11_move_resize(vo, x11->pos_changed_during_fs,
+ x11->size_changed_during_fs,
+ x11->nofsrc);
+ }
+
+ x11->size_changed_during_fs = false;
+ x11->pos_changed_during_fs = false;
+
+ vo_x11_update_composition_hint(vo);
+ }
+ }
+}
+
+static void vo_x11_check_net_wm_desktop_change(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (x11->parent)
+ return;
+
+ long params[1] = {0};
+ if (x11_get_property_copy(x11, x11->window, XA(x11, _NET_WM_DESKTOP),
+ XA_CARDINAL, 32, params, sizeof(params)))
+ {
+ x11->opts->all_workspaces = params[0] == -1; // (gets sign-extended?)
+ m_config_cache_write_opt(x11->opts_cache, &x11->opts->all_workspaces);
+ }
+}
+
+// Releasing all keys on key-up or defocus is simpler and ensures no keys can
+// get "stuck".
+static void release_all_keys(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (x11->no_autorepeat)
+ mp_input_put_key(x11->input_ctx, MP_INPUT_RELEASE_ALL);
+ x11->win_drag_button1_down = false;
+}
+
+void vo_x11_check_events(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ Display *display = vo->x11->display;
+ XEvent Event;
+
+ xscreensaver_heartbeat(vo->x11);
+
+ while (XPending(display)) {
+ XNextEvent(display, &Event);
+ MP_TRACE(x11, "XEvent: %d\n", Event.type);
+ switch (Event.type) {
+ case Expose:
+ x11->pending_vo_events |= VO_EVENT_EXPOSE;
+ break;
+ case ConfigureNotify:
+ if (x11->window == None)
+ break;
+ vo_x11_update_geometry(vo);
+ if (x11->parent && Event.xconfigure.window == x11->parent) {
+ MP_TRACE(x11, "adjusting embedded window position\n");
+ XMoveResizeWindow(x11->display, x11->window,
+ 0, 0, RC_W(x11->winrc), RC_H(x11->winrc));
+ }
+ break;
+ case KeyPress: {
+ char buf[100];
+ KeySym keySym = 0;
+ int modifiers = get_mods(Event.xkey.state);
+ if (x11->no_autorepeat)
+ modifiers |= MP_KEY_STATE_DOWN;
+ if (x11->xic) {
+ Status status;
+ int len = Xutf8LookupString(x11->xic, &Event.xkey, buf,
+ sizeof(buf), &keySym, &status);
+ int mpkey = vo_x11_lookupkey(keySym);
+ if (mpkey) {
+ mp_input_put_key(x11->input_ctx, mpkey | modifiers);
+ } else if (status == XLookupChars || status == XLookupBoth) {
+ struct bstr t = { buf, len };
+ mp_input_put_key_utf8(x11->input_ctx, modifiers, t);
+ }
+ } else {
+ XLookupString(&Event.xkey, buf, sizeof(buf), &keySym,
+ &x11->compose_status);
+ int mpkey = vo_x11_lookupkey(keySym);
+ if (mpkey)
+ mp_input_put_key(x11->input_ctx, mpkey | modifiers);
+ }
+ break;
+ }
+ case FocusIn:
+ x11->has_focus = true;
+ vo_update_cursor(vo);
+ x11->pending_vo_events |= VO_EVENT_FOCUS;
+ break;
+ case FocusOut:
+ release_all_keys(vo);
+ x11->has_focus = false;
+ vo_update_cursor(vo);
+ x11->pending_vo_events |= VO_EVENT_FOCUS;
+ break;
+ case KeyRelease:
+ release_all_keys(vo);
+ break;
+ case MotionNotify:
+ if (x11->win_drag_button1_down && !x11->fs &&
+ !mp_input_test_dragging(x11->input_ctx, Event.xmotion.x,
+ Event.xmotion.y))
+ {
+ mp_input_put_key(x11->input_ctx, MP_INPUT_RELEASE_ALL);
+ XUngrabPointer(x11->display, CurrentTime);
+
+ long params[5] = {
+ Event.xmotion.x_root, Event.xmotion.y_root,
+ 8, // _NET_WM_MOVERESIZE_MOVE
+ 1, // button 1
+ 1, // source indication: normal
+ };
+ x11_send_ewmh_msg(x11, "_NET_WM_MOVERESIZE", params);
+ } else {
+ mp_input_set_mouse_pos(x11->input_ctx, Event.xmotion.x,
+ Event.xmotion.y);
+ }
+ x11->win_drag_button1_down = false;
+ break;
+ case LeaveNotify:
+ if (Event.xcrossing.mode != NotifyNormal)
+ break;
+ x11->win_drag_button1_down = false;
+ mp_input_put_key(x11->input_ctx, MP_KEY_MOUSE_LEAVE);
+ break;
+ case EnterNotify:
+ if (Event.xcrossing.mode != NotifyNormal)
+ break;
+ mp_input_put_key(x11->input_ctx, MP_KEY_MOUSE_ENTER);
+ break;
+ case ButtonPress:
+ if (Event.xbutton.button - 1 >= MP_KEY_MOUSE_BTN_COUNT)
+ break;
+ if (Event.xbutton.button == 1)
+ x11->win_drag_button1_down = true;
+ mp_input_put_key(x11->input_ctx,
+ (MP_MBTN_BASE + Event.xbutton.button - 1) |
+ get_mods(Event.xbutton.state) | MP_KEY_STATE_DOWN);
+ long msg[4] = {XEMBED_REQUEST_FOCUS};
+ vo_x11_xembed_send_message(x11, msg);
+ break;
+ case ButtonRelease:
+ if (Event.xbutton.button - 1 >= MP_KEY_MOUSE_BTN_COUNT)
+ break;
+ if (Event.xbutton.button == 1)
+ x11->win_drag_button1_down = false;
+ mp_input_put_key(x11->input_ctx,
+ (MP_MBTN_BASE + Event.xbutton.button - 1) |
+ get_mods(Event.xbutton.state) | MP_KEY_STATE_UP);
+ break;
+ case MapNotify:
+ x11->window_hidden = false;
+ x11->pseudo_mapped = true;
+ x11->current_screen = -1;
+ vo_x11_update_geometry(vo);
+ break;
+ case DestroyNotify:
+ MP_WARN(x11, "Our window was destroyed, exiting\n");
+ mp_input_put_key(x11->input_ctx, MP_KEY_CLOSE_WIN);
+ x11->window = 0;
+ break;
+ case ClientMessage:
+ if (Event.xclient.message_type == XA(x11, WM_PROTOCOLS) &&
+ Event.xclient.data.l[0] == XA(x11, WM_DELETE_WINDOW))
+ mp_input_put_key(x11->input_ctx, MP_KEY_CLOSE_WIN);
+ vo_x11_dnd_handle_message(vo, &Event.xclient);
+ vo_x11_xembed_handle_message(vo, &Event.xclient);
+ break;
+ case SelectionNotify:
+ vo_x11_dnd_handle_selection(vo, &Event.xselection);
+ break;
+ case PropertyNotify:
+ if (Event.xproperty.atom == XA(x11, _NET_FRAME_EXTENTS) ||
+ Event.xproperty.atom == XA(x11, WM_STATE))
+ {
+ if (!x11->pseudo_mapped && !x11->parent) {
+ MP_VERBOSE(x11, "not waiting for MapNotify\n");
+ x11->pseudo_mapped = true;
+ }
+ } else if (Event.xproperty.atom == XA(x11, _NET_WM_STATE)) {
+ vo_x11_check_net_wm_state_change(vo);
+ } else if (Event.xproperty.atom == XA(x11, _NET_WM_DESKTOP)) {
+ vo_x11_check_net_wm_desktop_change(vo);
+ } else if (Event.xproperty.atom == x11->icc_profile_property) {
+ x11->pending_vo_events |= VO_EVENT_ICC_PROFILE_CHANGED;
+ }
+ break;
+ case GenericEvent: {
+ XGenericEventCookie *cookie = (XGenericEventCookie *)&Event.xcookie;
+ if (cookie->extension == x11->present_code && x11->use_present)
+ {
+ XGetEventData(x11->display, cookie);
+ if (cookie->evtype == PresentCompleteNotify) {
+ XPresentCompleteNotifyEvent *present_event;
+ present_event = (XPresentCompleteNotifyEvent *)cookie->data;
+ present_sync_update_values(x11->present,
+ present_event->ust * 1000,
+ present_event->msc);
+ }
+ }
+ XFreeEventData(x11->display, cookie);
+ break;
+ }
+ default:
+ if (Event.type == x11->ShmCompletionEvent) {
+ if (x11->ShmCompletionWaitCount > 0)
+ x11->ShmCompletionWaitCount--;
+ }
+ if (Event.type == x11->xrandr_event) {
+ xrandr_read(x11);
+ vo_x11_update_geometry(vo);
+ }
+ break;
+ }
+ }
+
+ update_vo_size(vo);
+}
+
+static void vo_x11_sizehint(struct vo *vo, struct mp_rect rc, bool override_pos)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+
+ if (!x11->window || x11->parent)
+ return;
+
+ bool screen = opts->screen_id >= 0 || (opts->screen_name &&
+ opts->screen_name[0]);
+ bool fsscreen = opts->fsscreen_id >= 0 || (opts->fsscreen_name &&
+ opts->fsscreen_name[0]);
+ bool force_pos = opts->geometry.xy_valid || // explicitly forced by user
+ opts->force_window_position || // resize -> reset position
+ screen || fsscreen || // force onto screen area
+ opts->screen_name || // also force onto screen area
+ x11->parent || // force to fill parent
+ override_pos; // for fullscreen and such
+
+ XSizeHints *hint = XAllocSizeHints();
+ if (!hint)
+ return; // OOM
+
+ hint->flags |= PSize | (force_pos ? PPosition : 0);
+ hint->x = rc.x0;
+ hint->y = rc.y0;
+ hint->width = RC_W(rc);
+ hint->height = RC_H(rc);
+ hint->max_width = 0;
+ hint->max_height = 0;
+
+ if (opts->keepaspect && opts->keepaspect_window) {
+ hint->flags |= PAspect;
+ hint->min_aspect.x = hint->width;
+ hint->min_aspect.y = hint->height;
+ hint->max_aspect.x = hint->width;
+ hint->max_aspect.y = hint->height;
+ }
+
+ // Set minimum height/width to 4 to avoid off-by-one errors.
+ hint->flags |= PMinSize;
+ hint->min_width = hint->min_height = 4;
+
+ hint->flags |= PWinGravity;
+ hint->win_gravity = StaticGravity;
+
+ XSetWMNormalHints(x11->display, x11->window, hint);
+ XFree(hint);
+}
+
+static void vo_x11_move_resize(struct vo *vo, bool move, bool resize,
+ struct mp_rect rc)
+{
+ if (!vo->x11->window)
+ return;
+ int w = RC_W(rc), h = RC_H(rc);
+ XWindowChanges req = {.x = rc.x0, .y = rc.y0, .width = w, .height = h};
+ unsigned mask = (move ? CWX | CWY : 0) | (resize ? CWWidth | CWHeight : 0);
+ if (mask)
+ XConfigureWindow(vo->x11->display, vo->x11->window, mask, &req);
+ vo_x11_sizehint(vo, rc, false);
+}
+
+// set a X text property that expects a UTF8_STRING type
+static void vo_x11_set_property_utf8(struct vo *vo, Atom name, const char *t)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ XChangeProperty(x11->display, x11->window, name, XA(x11, UTF8_STRING), 8,
+ PropModeReplace, t, strlen(t));
+}
+
+// set a X text property that expects a STRING or COMPOUND_TEXT type
+static void vo_x11_set_property_string(struct vo *vo, Atom name, const char *t)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ XTextProperty prop = {0};
+
+ if (Xutf8TextListToTextProperty(x11->display, (char **)&t, 1,
+ XStdICCTextStyle, &prop) == Success)
+ {
+ XSetTextProperty(x11->display, x11->window, &prop, name);
+ } else {
+ // Strictly speaking this violates the ICCCM, but there's no way we
+ // can do this correctly.
+ vo_x11_set_property_utf8(vo, name, t);
+ }
+ XFree(prop.value);
+}
+
+static void vo_x11_update_window_title(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (!x11->window || !x11->window_title)
+ return;
+
+ vo_x11_set_property_string(vo, XA_WM_NAME, x11->window_title);
+ vo_x11_set_property_string(vo, XA_WM_ICON_NAME, x11->window_title);
+
+ /* _NET_WM_NAME and _NET_WM_ICON_NAME must be sanitized to UTF-8. */
+ void *tmp = talloc_new(NULL);
+ struct bstr b_title = bstr_sanitize_utf8_latin1(tmp, bstr0(x11->window_title));
+ vo_x11_set_property_utf8(vo, XA(x11, _NET_WM_NAME), bstrto0(tmp, b_title));
+ vo_x11_set_property_utf8(vo, XA(x11, _NET_WM_ICON_NAME), bstrto0(tmp, b_title));
+ talloc_free(tmp);
+}
+
+static void vo_x11_xembed_update(struct vo_x11_state *x11, int flags)
+{
+ if (!x11->window || !x11->parent)
+ return;
+
+ long xembed_info[] = {XEMBED_VERSION, flags};
+ Atom name = XA(x11, _XEMBED_INFO);
+ XChangeProperty(x11->display, x11->window, name, name, 32,
+ PropModeReplace, (char *)xembed_info, 2);
+}
+
+static void vo_x11_xembed_handle_message(struct vo *vo, XClientMessageEvent *ce)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (!x11->window || !x11->parent || ce->message_type != XA(x11, _XEMBED))
+ return;
+
+ long msg = ce->data.l[1];
+ if (msg == XEMBED_EMBEDDED_NOTIFY)
+ MP_VERBOSE(x11, "Parent windows supports XEmbed.\n");
+}
+
+static void vo_x11_xembed_send_message(struct vo_x11_state *x11, long m[4])
+{
+ if (!x11->window || !x11->parent)
+ return;
+ XEvent ev = {.xclient = {
+ .type = ClientMessage,
+ .window = x11->parent,
+ .message_type = XA(x11, _XEMBED),
+ .format = 32,
+ .data = {.l = { CurrentTime, m[0], m[1], m[2], m[3] }},
+ } };
+ XSendEvent(x11->display, x11->parent, False, NoEventMask, &ev);
+}
+
+static void vo_x11_set_wm_icon(struct vo_x11_state *x11)
+{
+ int icon_size = 0;
+ long *icon = talloc_array(NULL, long, 0);
+
+ for (int n = 0; x11_icons[n].start; n++) {
+ struct mp_image *img =
+ load_image_png_buf(x11_icons[n].start, x11_icons[n].len, IMGFMT_RGBA);
+ if (!img)
+ continue;
+ int new_size = 2 + img->w * img->h;
+ MP_RESIZE_ARRAY(NULL, icon, icon_size + new_size);
+ long *cur = icon + icon_size;
+ icon_size += new_size;
+ *cur++ = img->w;
+ *cur++ = img->h;
+ for (int y = 0; y < img->h; y++) {
+ uint8_t *s = (uint8_t *)img->planes[0] + img->stride[0] * y;
+ for (int x = 0; x < img->w; x++) {
+ *cur++ = s[x * 4 + 0] | (s[x * 4 + 1] << 8) |
+ (s[x * 4 + 2] << 16) | ((unsigned)s[x * 4 + 3] << 24);
+ }
+ }
+ talloc_free(img);
+ }
+
+ XChangeProperty(x11->display, x11->window, XA(x11, _NET_WM_ICON),
+ XA_CARDINAL, 32, PropModeReplace,
+ (unsigned char *)icon, icon_size);
+ talloc_free(icon);
+}
+
+static void vo_x11_create_window(struct vo *vo, XVisualInfo *vis,
+ struct mp_rect rc)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ assert(x11->window == None);
+ assert(!x11->xic);
+
+ XVisualInfo vinfo_storage;
+ if (!vis) {
+ vis = &vinfo_storage;
+ XWindowAttributes att;
+ XGetWindowAttributes(x11->display, x11->rootwin, &att);
+ XMatchVisualInfo(x11->display, x11->screen, att.depth, TrueColor, vis);
+ }
+
+ if (x11->colormap == None) {
+ x11->colormap = XCreateColormap(x11->display, x11->rootwin,
+ vis->visual, AllocNone);
+ }
+
+ unsigned long xswamask = CWBorderPixel | CWColormap;
+ XSetWindowAttributes xswa = {
+ .border_pixel = 0,
+ .colormap = x11->colormap,
+ };
+
+ Window parent = x11->parent;
+ if (!parent)
+ parent = x11->rootwin;
+
+ x11->window =
+ XCreateWindow(x11->display, parent, rc.x0, rc.y0, RC_W(rc), RC_H(rc), 0,
+ vis->depth, CopyFromParent, vis->visual, xswamask, &xswa);
+ Atom protos[1] = {XA(x11, WM_DELETE_WINDOW)};
+ XSetWMProtocols(x11->display, x11->window, protos, 1);
+
+ if (!XPresentQueryExtension(x11->display, &x11->present_code, NULL, NULL)) {
+ MP_VERBOSE(x11, "The XPresent extension is not supported.\n");
+ } else {
+ MP_VERBOSE(x11, "The XPresent extension was found.\n");
+ XPresentSelectInput(x11->display, x11->window, PresentCompleteNotifyMask);
+ }
+ xpresent_set(x11);
+
+ x11->mouse_cursor_set = false;
+ x11->mouse_cursor_visible = true;
+ vo_update_cursor(vo);
+
+ if (x11->xim) {
+ x11->xic = XCreateIC(x11->xim,
+ XNInputStyle, XIMPreeditNone | XIMStatusNone,
+ XNClientWindow, x11->window,
+ XNFocusWindow, x11->window,
+ NULL);
+ }
+
+ if (!x11->parent) {
+ vo_x11_update_composition_hint(vo);
+ vo_x11_set_wm_icon(x11);
+ vo_x11_dnd_init_window(vo);
+ vo_x11_set_property_utf8(vo, XA(x11, _GTK_THEME_VARIANT), "dark");
+ }
+ if (!x11->parent || x11->opts->x11_wid_title)
+ vo_x11_update_window_title(vo);
+ vo_x11_xembed_update(x11, 0);
+}
+
+static void vo_x11_map_window(struct vo *vo, struct mp_rect rc)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ vo_x11_move_resize(vo, true, true, rc);
+ vo_x11_decoration(vo, x11->opts->border);
+
+ if (x11->opts->fullscreen && (x11->wm_type & vo_wm_FULLSCREEN)) {
+ Atom state = XA(x11, _NET_WM_STATE_FULLSCREEN);
+ XChangeProperty(x11->display, x11->window, XA(x11, _NET_WM_STATE), XA_ATOM,
+ 32, PropModeAppend, (unsigned char *)&state, 1);
+ x11->fs = 1;
+ // The "saved" positions are bogus, so reset them when leaving FS again.
+ x11->size_changed_during_fs = true;
+ x11->pos_changed_during_fs = true;
+ }
+
+ if (x11->opts->fsscreen_id != -1) {
+ long params[5] = {0};
+ if (x11->opts->fsscreen_id >= 0) {
+ for (int n = 0; n < 4; n++)
+ params[n] = x11->opts->fsscreen_id;
+ } else {
+ vo_x11_get_bounding_monitors(x11, &params[0]);
+ }
+ params[4] = 1; // source indication: normal
+ x11_send_ewmh_msg(x11, "_NET_WM_FULLSCREEN_MONITORS", params);
+ }
+
+ if (x11->opts->all_workspaces) {
+ if (x11->wm_type & vo_wm_STICKY) {
+ Atom state = XA(x11, _NET_WM_STATE_STICKY);
+ XChangeProperty(x11->display, x11->window, XA(x11, _NET_WM_STATE), XA_ATOM,
+ 32, PropModeReplace, (unsigned char *)&state, 1);
+ } else {
+ long v = 0xFFFFFFFF;
+ XChangeProperty(x11->display, x11->window, XA(x11, _NET_WM_DESKTOP),
+ XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&v, 1);
+ }
+ } else if (x11->opts->geometry.ws > 0) {
+ long v = x11->opts->geometry.ws - 1;
+ XChangeProperty(x11->display, x11->window, XA(x11, _NET_WM_DESKTOP),
+ XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&v, 1);
+ }
+
+ vo_x11_update_composition_hint(vo);
+
+ // map window
+ int events = StructureNotifyMask | ExposureMask | PropertyChangeMask |
+ LeaveWindowMask | EnterWindowMask | FocusChangeMask;
+ if (mp_input_mouse_enabled(x11->input_ctx))
+ events |= PointerMotionMask | ButtonPressMask | ButtonReleaseMask;
+ if (mp_input_vo_keyboard_enabled(x11->input_ctx))
+ events |= KeyPressMask | KeyReleaseMask;
+ vo_x11_selectinput_witherr(vo, x11->display, x11->window, events);
+ XMapWindow(x11->display, x11->window);
+
+ if (x11->opts->cursor_passthrough)
+ vo_x11_set_input_region(vo, true);
+
+ if (x11->opts->window_maximized) // don't override WM default on "no"
+ vo_x11_maximize(vo);
+ if (x11->opts->window_minimized) // don't override WM default on "no"
+ vo_x11_minimize(vo);
+
+ if (x11->opts->fullscreen && (x11->wm_type & vo_wm_FULLSCREEN))
+ x11_set_ewmh_state(x11, "_NET_WM_STATE_FULLSCREEN", 1);
+
+ vo_x11_xembed_update(x11, XEMBED_MAPPED);
+}
+
+static void vo_x11_highlevel_resize(struct vo *vo, struct mp_rect rc)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+
+ bool reset_pos = opts->force_window_position;
+ if (reset_pos) {
+ x11->nofsrc = rc;
+ } else {
+ x11->nofsrc.x1 = x11->nofsrc.x0 + RC_W(rc);
+ x11->nofsrc.y1 = x11->nofsrc.y0 + RC_H(rc);
+ }
+
+ if (opts->fullscreen) {
+ x11->size_changed_during_fs = true;
+ x11->pos_changed_during_fs = reset_pos;
+ vo_x11_sizehint(vo, rc, false);
+ } else {
+ vo_x11_move_resize(vo, reset_pos, true, rc);
+ }
+}
+
+static void wait_until_mapped(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (!x11->pseudo_mapped)
+ x11_send_ewmh_msg(x11, "_NET_REQUEST_FRAME_EXTENTS", (long[5]){0});
+ while (!x11->pseudo_mapped && x11->window) {
+ XWindowAttributes att;
+ XGetWindowAttributes(x11->display, x11->window, &att);
+ if (att.map_state != IsUnmapped) {
+ x11->pseudo_mapped = true;
+ break;
+ }
+ XEvent unused;
+ XPeekEvent(x11->display, &unused);
+ vo_x11_check_events(vo);
+ }
+}
+
+// Create the X11 window. There is only 1, and it must be created before
+// vo_x11_config_vo_window() is called. vis can be NULL for default.
+bool vo_x11_create_vo_window(struct vo *vo, XVisualInfo *vis,
+ const char *classname)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ assert(!x11->window);
+
+ if (x11->parent) {
+ if (x11->parent == x11->rootwin) {
+ x11->window = x11->rootwin;
+ x11->pseudo_mapped = true;
+ XSelectInput(x11->display, x11->window, StructureNotifyMask);
+ } else {
+ XSelectInput(x11->display, x11->parent, StructureNotifyMask);
+ }
+ }
+ if (x11->window == None) {
+ vo_x11_create_window(vo, vis, (struct mp_rect){.x1 = 320, .y1 = 200 });
+ vo_x11_classhint(vo, x11->window, classname);
+ vo_x11_wm_hints(vo, x11->window);
+ x11->window_hidden = true;
+ }
+
+ return !!x11->window;
+}
+
+// Resize the window (e.g. new file, or video resolution change)
+void vo_x11_config_vo_window(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+
+ assert(x11->window);
+
+ // Don't attempt to change autofit/geometry on maximized windows.
+ if (x11->geometry_change && opts->window_maximized)
+ return;
+
+ vo_x11_update_screeninfo(vo);
+
+ struct vo_win_geometry geo;
+ vo_calc_window_geometry2(vo, &x11->screenrc, x11->dpi_scale, &geo);
+ vo_apply_window_geometry(vo, &geo);
+
+ struct mp_rect rc = geo.win;
+
+ if (x11->parent) {
+ vo_x11_update_geometry(vo);
+ rc = (struct mp_rect){0, 0, RC_W(x11->winrc), RC_H(x11->winrc)};
+ }
+
+ bool reset_size = (x11->old_dw != RC_W(rc) || x11->old_dh != RC_H(rc)) &&
+ (opts->auto_window_resize || x11->geometry_change);
+
+ x11->old_dw = RC_W(rc);
+ x11->old_dh = RC_H(rc);
+
+ if (x11->window_hidden) {
+ x11->nofsrc = rc;
+ vo_x11_map_window(vo, rc);
+ } else if (reset_size) {
+ vo_x11_highlevel_resize(vo, rc);
+ }
+
+ x11->geometry_change = false;
+
+ if (opts->ontop)
+ vo_x11_setlayer(vo, opts->ontop);
+
+ vo_x11_fullscreen(vo);
+
+ wait_until_mapped(vo);
+ vo_x11_update_geometry(vo);
+ update_vo_size(vo);
+ x11->pending_vo_events &= ~VO_EVENT_RESIZE; // implicitly done by the VO
+}
+
+static void vo_x11_sticky(struct vo *vo, bool sticky)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (x11->wm_type & vo_wm_STICKY) {
+ x11_set_ewmh_state(x11, "_NET_WM_STATE_STICKY", sticky);
+ } else {
+ long params[5] = {0xFFFFFFFF, 1};
+ if (!sticky) {
+ x11_get_property_copy(x11, x11->rootwin,
+ XA(x11, _NET_CURRENT_DESKTOP),
+ XA_CARDINAL, 32, &params[0],
+ sizeof(params[0]));
+ }
+ x11_send_ewmh_msg(x11, "_NET_WM_DESKTOP", params);
+ }
+}
+
+static void vo_x11_setlayer(struct vo *vo, bool ontop)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ if (x11->parent || !x11->window)
+ return;
+
+ if (x11->wm_type & (vo_wm_STAYS_ON_TOP | vo_wm_ABOVE)) {
+ char *state = "_NET_WM_STATE_ABOVE";
+
+ // Not in EWMH - but the old code preferred this (maybe it is "better")
+ if (x11->wm_type & vo_wm_STAYS_ON_TOP)
+ state = "_NET_WM_STATE_STAYS_ON_TOP";
+
+ x11_set_ewmh_state(x11, state, ontop);
+
+ MP_VERBOSE(x11, "NET style stay on top (%d). Using state %s.\n",
+ ontop, state);
+ } else if (x11->wm_type & vo_wm_LAYER) {
+ if (!x11->orig_layer) {
+ x11->orig_layer = WIN_LAYER_NORMAL;
+ x11_get_property_copy(x11, x11->window, XA(x11, _WIN_LAYER),
+ XA_CARDINAL, 32, &x11->orig_layer, sizeof(long));
+ MP_VERBOSE(x11, "original window layer is %ld.\n", x11->orig_layer);
+ }
+
+ long params[5] = {0};
+ // if not fullscreen, stay on default layer
+ params[0] = ontop ? WIN_LAYER_ABOVE_DOCK : x11->orig_layer;
+ params[1] = CurrentTime;
+ MP_VERBOSE(x11, "Layered style stay on top (layer %ld).\n", params[0]);
+ x11_send_ewmh_msg(x11, "_WIN_LAYER", params);
+ }
+}
+
+static bool rc_overlaps(struct mp_rect rc1, struct mp_rect rc2)
+{
+ return mp_rect_intersection(&rc1, &rc2); // changes the first argument
+}
+
+// update x11->winrc with current boundaries of vo->x11->window
+static void vo_x11_update_geometry(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ int x = 0, y = 0;
+ unsigned w, h, dummy_uint;
+ int dummy_int;
+ Window dummy_win;
+ Window win = x11->parent ? x11->parent : x11->window;
+ x11->winrc = (struct mp_rect){0, 0, 0, 0};
+ if (win) {
+ XGetGeometry(x11->display, win, &dummy_win, &dummy_int, &dummy_int,
+ &w, &h, &dummy_int, &dummy_uint);
+ if (w > INT_MAX || h > INT_MAX)
+ w = h = 0;
+ XTranslateCoordinates(x11->display, win, x11->rootwin, 0, 0,
+ &x, &y, &dummy_win);
+ x11->winrc = (struct mp_rect){x, y, x + w, y + h};
+ }
+ struct xrandr_display *disp = get_current_display(vo);
+ // Try to fallback to something reasonable if we have no disp yet
+ if (!disp) {
+ int screen = vo_x11_select_screen(vo);
+ if (screen > -1) {
+ disp = &x11->displays[screen];
+ } else if (x11->current_screen > - 1) {
+ disp = &x11->displays[x11->current_screen];
+ }
+ }
+ double fps = disp ? disp->fps : 0;
+ if (fps != x11->current_display_fps)
+ MP_VERBOSE(x11, "Current display FPS: %f\n", fps);
+ x11->current_display_fps = fps;
+ if (disp && x11->current_screen != disp->screen) {
+ x11->current_screen = disp->screen;
+ x11->pending_vo_events |= VO_EVENT_ICC_PROFILE_CHANGED;
+ }
+ x11->pending_vo_events |= VO_EVENT_WIN_STATE;
+}
+
+static void vo_x11_fullscreen(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+
+ if (opts->fullscreen == x11->fs)
+ return;
+ x11->fs = opts->fullscreen; // x11->fs now contains the new state
+ if (x11->parent || !x11->window)
+ return;
+
+ // Save old state before entering fullscreen
+ if (x11->fs) {
+ vo_x11_update_geometry(vo);
+ x11->nofsrc = x11->winrc;
+ }
+
+ struct mp_rect rc = x11->nofsrc;
+
+ if (x11->wm_type & vo_wm_FULLSCREEN) {
+ x11_set_ewmh_state(x11, "_NET_WM_STATE_FULLSCREEN", x11->fs);
+ if (!x11->fs && (x11->pos_changed_during_fs ||
+ x11->size_changed_during_fs))
+ {
+ if (x11->screenrc.x0 == rc.x0 && x11->screenrc.x1 == rc.x1 &&
+ x11->screenrc.y0 == rc.y0 && x11->screenrc.y1 == rc.y1)
+ {
+ // Workaround for some WMs switching back to FS in this case.
+ MP_VERBOSE(x11, "avoiding triggering old-style fullscreen\n");
+ rc.x1 -= 1;
+ rc.y1 -= 1;
+ }
+ vo_x11_move_resize(vo, x11->pos_changed_during_fs,
+ x11->size_changed_during_fs, rc);
+ }
+ } else {
+ if (x11->fs) {
+ vo_x11_update_screeninfo(vo);
+ rc = x11->screenrc;
+ }
+
+ vo_x11_decoration(vo, opts->border && !x11->fs);
+ vo_x11_sizehint(vo, rc, true);
+
+ XMoveResizeWindow(x11->display, x11->window, rc.x0, rc.y0,
+ RC_W(rc), RC_H(rc));
+
+ vo_x11_setlayer(vo, x11->fs || opts->ontop);
+
+ XRaiseWindow(x11->display, x11->window);
+ XFlush(x11->display);
+ }
+
+ x11->size_changed_during_fs = false;
+ x11->pos_changed_during_fs = false;
+
+ vo_x11_update_composition_hint(vo);
+}
+
+static void vo_x11_maximize(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ long params[5] = {
+ x11->opts->window_maximized ? NET_WM_STATE_ADD : NET_WM_STATE_REMOVE,
+ XA(x11, _NET_WM_STATE_MAXIMIZED_VERT),
+ XA(x11, _NET_WM_STATE_MAXIMIZED_HORZ),
+ 1, // source indication: normal
+ };
+ x11_send_ewmh_msg(x11, "_NET_WM_STATE", params);
+}
+
+static void vo_x11_minimize(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (x11->opts->window_minimized) {
+ XIconifyWindow(x11->display, x11->window, x11->screen);
+ } else {
+ long params[5] = {0};
+ x11_send_ewmh_msg(x11, "_NET_ACTIVE_WINDOW", params);
+ }
+}
+
+static void vo_x11_set_geometry(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (!x11->window)
+ return;
+
+ x11->geometry_change = true;
+ vo_x11_config_vo_window(vo);
+}
+
+bool vo_x11_check_visible(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+
+ bool render = !x11->hidden || opts->force_render ||
+ VS_IS_DISP(opts->video_sync);
+ return render;
+}
+
+static void vo_x11_set_input_region(struct vo *vo, bool passthrough)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ if (passthrough) {
+ XRectangle rect = {0, 0, 0, 0};
+ Region region = XCreateRegion();
+ XUnionRectWithRegion(&rect, region, region);
+ XShapeCombineRegion(x11->display, x11->window, ShapeInput, 0, 0,
+ region, ShapeSet);
+ XDestroyRegion(region);
+ } else {
+ XShapeCombineMask(x11->display, x11->window, ShapeInput, 0, 0,
+ 0, ShapeSet);
+ }
+}
+
+int vo_x11_control(struct vo *vo, int *events, int request, void *arg)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ struct mp_vo_opts *opts = x11->opts;
+ switch (request) {
+ case VOCTRL_CHECK_EVENTS:
+ vo_x11_check_events(vo);
+ *events |= x11->pending_vo_events;
+ x11->pending_vo_events = 0;
+ return VO_TRUE;
+ case VOCTRL_VO_OPTS_CHANGED: {
+ void *opt;
+ while (m_config_cache_get_next_changed(x11->opts_cache, &opt)) {
+ if (opt == &opts->fullscreen)
+ vo_x11_fullscreen(vo);
+ if (opt == &opts->ontop)
+ vo_x11_setlayer(vo, opts->ontop);
+ if (opt == &opts->border)
+ vo_x11_decoration(vo, opts->border);
+ if (opt == &opts->all_workspaces)
+ vo_x11_sticky(vo, opts->all_workspaces);
+ if (opt == &opts->window_minimized)
+ vo_x11_minimize(vo);
+ if (opt == &opts->window_maximized)
+ vo_x11_maximize(vo);
+ if (opt == &opts->cursor_passthrough)
+ vo_x11_set_input_region(vo, opts->cursor_passthrough);
+ if (opt == &opts->x11_present)
+ xpresent_set(x11);
+ if (opt == &opts->geometry || opt == &opts->autofit ||
+ opt == &opts->autofit_smaller || opt == &opts->autofit_larger)
+ {
+ vo_x11_set_geometry(vo);
+ }
+ }
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_UNFS_WINDOW_SIZE: {
+ int *s = arg;
+ if (!x11->window || x11->parent)
+ return VO_FALSE;
+ s[0] = (x11->fs ? RC_W(x11->nofsrc) : RC_W(x11->winrc)) / x11->dpi_scale;
+ s[1] = (x11->fs ? RC_H(x11->nofsrc) : RC_H(x11->winrc)) / x11->dpi_scale;
+ return VO_TRUE;
+ }
+ case VOCTRL_SET_UNFS_WINDOW_SIZE: {
+ int *s = arg;
+ if (!x11->window || x11->parent)
+ return VO_FALSE;
+ int w = s[0] * x11->dpi_scale;
+ int h = s[1] * x11->dpi_scale;
+ struct mp_rect rc = x11->winrc;
+ rc.x1 = rc.x0 + w;
+ rc.y1 = rc.y0 + h;
+ if (x11->opts->window_maximized) {
+ x11->opts->window_maximized = false;
+ m_config_cache_write_opt(x11->opts_cache,
+ &x11->opts->window_maximized);
+ vo_x11_maximize(vo);
+ }
+ vo_x11_highlevel_resize(vo, rc);
+ if (!x11->fs) { // guess new window size, instead of waiting for X
+ x11->winrc.x1 = x11->winrc.x0 + w;
+ x11->winrc.y1 = x11->winrc.y0 + h;
+ }
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_FOCUSED: {
+ *(bool *)arg = x11->has_focus;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_NAMES: {
+ if (!x11->pseudo_mapped)
+ return VO_FALSE;
+ char **names = NULL;
+ int displays_spanned = 0;
+ for (int n = 0; n < x11->num_displays; n++) {
+ if (rc_overlaps(x11->displays[n].rc, x11->winrc))
+ MP_TARRAY_APPEND(NULL, names, displays_spanned,
+ talloc_strdup(NULL, x11->displays[n].name));
+ }
+ MP_TARRAY_APPEND(NULL, names, displays_spanned, NULL);
+ *(char ***)arg = names;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_ICC_PROFILE: {
+ if (!x11->pseudo_mapped)
+ return VO_NOTAVAIL;
+ int atom_id = x11->displays[x11->current_screen].atom_id;
+ char prop[80];
+ snprintf(prop, sizeof(prop), "_ICC_PROFILE");
+ if (atom_id > 0)
+ mp_snprintf_cat(prop, sizeof(prop), "_%d", atom_id);
+ x11->icc_profile_property = XAs(x11, prop);
+ int len;
+ MP_VERBOSE(x11, "Retrieving ICC profile for display: %d\n", x11->current_screen);
+ void *icc = x11_get_property(x11, x11->rootwin, x11->icc_profile_property,
+ XA_CARDINAL, 8, &len);
+ if (!icc)
+ return VO_FALSE;
+ *(bstr *)arg = bstrdup(NULL, (bstr){icc, len});
+ XFree(icc);
+ // Watch x11->icc_profile_property
+ XSelectInput(x11->display, x11->rootwin, PropertyChangeMask);
+ return VO_TRUE;
+ }
+ case VOCTRL_SET_CURSOR_VISIBILITY:
+ x11->mouse_cursor_visible = *(bool *)arg;
+ vo_update_cursor(vo);
+ return VO_TRUE;
+ case VOCTRL_KILL_SCREENSAVER:
+ set_screensaver(x11, false);
+ return VO_TRUE;
+ case VOCTRL_RESTORE_SCREENSAVER:
+ set_screensaver(x11, true);
+ return VO_TRUE;
+ case VOCTRL_UPDATE_WINDOW_TITLE:
+ talloc_free(x11->window_title);
+ x11->window_title = talloc_strdup(x11, (char *)arg);
+ if (!x11->parent || x11->opts->x11_wid_title)
+ vo_x11_update_window_title(vo);
+ return VO_TRUE;
+ case VOCTRL_GET_DISPLAY_FPS: {
+ double fps = x11->current_display_fps;
+ if (fps <= 0)
+ break;
+ *(double *)arg = fps;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_DISPLAY_RES: {
+ struct xrandr_display *disp = NULL;
+ if (x11->current_screen > -1)
+ disp = &x11->displays[x11->current_screen];
+ if (!x11->window || x11->parent || !disp)
+ return VO_NOTAVAIL;
+ ((int *)arg)[0] = mp_rect_w(disp->rc);
+ ((int *)arg)[1] = mp_rect_h(disp->rc);
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_WINDOW_ID: {
+ if (!x11->window)
+ return VO_NOTAVAIL;
+ *(int64_t *)arg = x11->window;
+ return VO_TRUE;
+ }
+ case VOCTRL_GET_HIDPI_SCALE:
+ *(double *)arg = x11->dpi_scale;
+ return VO_TRUE;
+ }
+ return VO_NOTIMPL;
+}
+
+void vo_x11_present(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ XPresentNotifyMSC(x11->display, x11->window,
+ 0, 0, 1, 0);
+}
+
+void vo_x11_wakeup(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ (void)write(x11->wakeup_pipe[1], &(char){0}, 1);
+}
+
+void vo_x11_wait_events(struct vo *vo, int64_t until_time_ns)
+{
+ struct vo_x11_state *x11 = vo->x11;
+
+ struct pollfd fds[2] = {
+ { .fd = x11->event_fd, .events = POLLIN },
+ { .fd = x11->wakeup_pipe[0], .events = POLLIN },
+ };
+ int64_t wait_ns = until_time_ns - mp_time_ns();
+ int64_t timeout_ns = MPCLAMP(wait_ns, 0, MP_TIME_S_TO_NS(10));
+
+ mp_poll(fds, 2, timeout_ns);
+
+ if (fds[1].revents & POLLIN)
+ mp_flush_wakeup_pipe(x11->wakeup_pipe[0]);
+}
+
+static void xscreensaver_heartbeat(struct vo_x11_state *x11)
+{
+ double time = mp_time_sec();
+
+ if (x11->display && !x11->screensaver_enabled &&
+ (time - x11->screensaver_time_last) >= 10)
+ {
+ x11->screensaver_time_last = time;
+ XResetScreenSaver(x11->display);
+ }
+}
+
+static int xss_suspend(Display *mDisplay, Bool suspend)
+{
+ int event, error, major, minor;
+ if (XScreenSaverQueryExtension(mDisplay, &event, &error) != True ||
+ XScreenSaverQueryVersion(mDisplay, &major, &minor) != True)
+ return 0;
+ if (major < 1 || (major == 1 && minor < 1))
+ return 0;
+ XScreenSaverSuspend(mDisplay, suspend);
+ return 1;
+}
+
+static void set_screensaver(struct vo_x11_state *x11, bool enabled)
+{
+ Display *mDisplay = x11->display;
+ if (!mDisplay || x11->screensaver_enabled == enabled)
+ return;
+ MP_VERBOSE(x11, "%s screensaver.\n", enabled ? "Enabling" : "Disabling");
+ x11->screensaver_enabled = enabled;
+ if (xss_suspend(mDisplay, !enabled))
+ return;
+ int nothing;
+ if (DPMSQueryExtension(mDisplay, &nothing, &nothing)) {
+ BOOL onoff = 0;
+ CARD16 state;
+ DPMSInfo(mDisplay, &state, &onoff);
+ if (!x11->dpms_touched && enabled)
+ return; // enable DPMS only we we disabled it before
+ if (enabled != !!onoff) {
+ MP_VERBOSE(x11, "Setting DMPS: %s.\n", enabled ? "on" : "off");
+ if (enabled) {
+ DPMSEnable(mDisplay);
+ } else {
+ DPMSDisable(mDisplay);
+ x11->dpms_touched = true;
+ }
+ DPMSInfo(mDisplay, &state, &onoff);
+ if (enabled != !!onoff)
+ MP_WARN(x11, "DPMS state could not be set.\n");
+ }
+ }
+}
+
+static void vo_x11_selectinput_witherr(struct vo *vo,
+ Display *display,
+ Window w,
+ long event_mask)
+{
+ XSelectInput(display, w, NoEventMask);
+
+ // NOTE: this can raise BadAccess, which should be ignored by the X error
+ // handler; also see below
+ XSelectInput(display, w, event_mask);
+
+ // Test whether setting the event mask failed (with a BadAccess X error,
+ // although we don't know whether this really happened).
+ // This is needed for obscure situations like using --rootwin with a window
+ // manager active.
+ XWindowAttributes a;
+ if (XGetWindowAttributes(display, w, &a)) {
+ long bad = ButtonPressMask | ButtonReleaseMask | PointerMotionMask;
+ if ((event_mask & bad) && (a.all_event_masks & bad) &&
+ ((a.your_event_mask & bad) != (event_mask & bad)))
+ {
+ MP_ERR(vo->x11, "X11 error: error during XSelectInput "
+ "call, trying without mouse events\n");
+ XSelectInput(display, w, event_mask & ~bad);
+ }
+ }
+}
+
+bool vo_x11_screen_is_composited(struct vo *vo)
+{
+ struct vo_x11_state *x11 = vo->x11;
+ char buf[50];
+ snprintf(buf, sizeof(buf), "_NET_WM_CM_S%d", x11->screen);
+ Atom NET_WM_CM = XInternAtom(x11->display, buf, False);
+ return XGetSelectionOwner(x11->display, NET_WM_CM) != None;
+}
+
+// Return whether the given visual has alpha (when compositing is used).
+bool vo_x11_is_rgba_visual(XVisualInfo *v)
+{
+ // This is a heuristic at best. Note that normal 8 bit Visuals use
+ // a depth of 24, even if the pixels are padded to 32 bit. If the
+ // depth is higher than 24, the remaining bits must be alpha.
+ // Note: vinfo->bits_per_rgb appears to be useless (is always 8).
+ unsigned long mask = v->depth == sizeof(unsigned long) * 8 ?
+ (unsigned long)-1 : (1UL << v->depth) - 1;
+ return mask & ~(v->red_mask | v->green_mask | v->blue_mask);
+}
diff --git a/video/out/x11_common.h b/video/out/x11_common.h
new file mode 100644
index 0000000..62a96d7
--- /dev/null
+++ b/video/out/x11_common.h
@@ -0,0 +1,164 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_X11_COMMON_H
+#define MPLAYER_X11_COMMON_H
+
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+
+#include "common/common.h"
+
+#include "config.h"
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+struct vo;
+struct mp_log;
+
+#define MAX_DISPLAYS 32 // ought to be enough for everyone
+
+struct xrandr_display {
+ struct mp_rect rc;
+ double fps;
+ char *name;
+ bool overlaps;
+ int atom_id; // offset by location of primary
+ int screen;
+};
+
+struct vo_x11_state {
+ struct mp_log *log;
+ struct input_ctx *input_ctx;
+ struct m_config_cache *opts_cache;
+ struct mp_vo_opts *opts;
+ Display *display;
+ int event_fd;
+ int wakeup_pipe[2];
+ Window window;
+ Window rootwin;
+ Window parent; // embedded in this foreign window
+ int screen;
+ int display_is_local;
+ int ws_width;
+ int ws_height;
+ int dpi_scale;
+ struct mp_rect screenrc;
+ char *window_title;
+
+ struct xrandr_display displays[MAX_DISPLAYS];
+ int num_displays;
+ int current_screen;
+
+ int xrandr_event;
+ bool has_mesa;
+ bool has_nvidia;
+
+ bool screensaver_enabled;
+ bool dpms_touched;
+ double screensaver_time_last;
+
+ struct mp_present *present;
+ bool use_present;
+ int present_code;
+
+ XIM xim;
+ XIC xic;
+ bool no_autorepeat;
+
+ Colormap colormap;
+
+ int wm_type;
+ bool hidden; // _NET_WM_STATE_HIDDEN
+ bool window_hidden; // the window was mapped at least once
+ bool pseudo_mapped; // not necessarily mapped, but known window size
+ int fs; // whether we assume the window is in fullscreen mode
+
+ bool mouse_cursor_visible; // whether we want the cursor to be visible (only
+ // takes effect when the window is focused)
+ bool mouse_cursor_set; // whether the cursor is *currently* *hidden*
+ bool has_focus;
+ long orig_layer;
+
+ // Current actual window position (updated on window move/resize events).
+ struct mp_rect winrc;
+ double current_display_fps;
+
+ int pending_vo_events;
+
+ // last non-fullscreen extends (updated on fullscreen or reinitialization)
+ struct mp_rect nofsrc;
+
+ /* Keep track of original video width/height to determine when to
+ * resize window when reconfiguring. Resize window when video size
+ * changes, but don't force window size changes as long as video size
+ * stays the same (even if that size is different from the current
+ * window size after the user modified the latter). */
+ int old_dw, old_dh;
+ /* Video size changed during fullscreen when we couldn't tell the new
+ * size to the window manager. Must set window size when turning
+ * fullscreen off. */
+ bool size_changed_during_fs;
+ bool pos_changed_during_fs;
+
+ /* One of the autofit/geometry options changed at runtime. */
+ bool geometry_change;
+
+ XComposeStatus compose_status;
+
+ /* XShm stuff */
+ int ShmCompletionEvent;
+ /* Number of outstanding XShmPutImage requests */
+ /* Decremented when ShmCompletionEvent is received */
+ /* Increment it before XShmPutImage */
+ int ShmCompletionWaitCount;
+
+ /* drag and drop */
+ Atom dnd_requested_format;
+ Atom dnd_requested_action;
+ Window dnd_src_window;
+
+ /* dragging the window */
+ bool win_drag_button1_down;
+
+ Atom icc_profile_property;
+};
+
+bool vo_x11_init(struct vo *vo);
+void vo_x11_uninit(struct vo *vo);
+void vo_x11_check_events(struct vo *vo);
+bool vo_x11_screen_is_composited(struct vo *vo);
+bool vo_x11_create_vo_window(struct vo *vo, XVisualInfo *vis,
+ const char *classname);
+void vo_x11_config_vo_window(struct vo *vo);
+bool vo_x11_check_visible(struct vo *vo);
+int vo_x11_control(struct vo *vo, int *events, int request, void *arg);
+void vo_x11_present(struct vo *vo);
+void vo_x11_sync_swap(struct vo *vo);
+void vo_x11_wakeup(struct vo *vo);
+void vo_x11_wait_events(struct vo *vo, int64_t until_time_ns);
+
+void vo_x11_silence_xlib(int dir);
+
+bool vo_x11_is_rgba_visual(XVisualInfo *v);
+
+#endif /* MPLAYER_X11_COMMON_H */