diff --git a/video/out/d3d11/context.c b/video/out/d3d11/context.c index fc8802e607e4e..12c17c99e0562 100644 --- a/video/out/d3d11/context.c +++ b/video/out/d3d11/context.c @@ -230,6 +230,24 @@ static float d3d11_target_ref_luma(struct ra_swapchain *sw) return mp_dxgi_sdr_white_level_from_hwnd(&p->dxgi_ctx, vo_w32_hwnd(sw->ctx->vo)); } +static bool d3d11_target_global_color_management_status(struct ra_swapchain *sw) +{ + struct priv *p = sw->priv; + bool reliable = false; + struct mp_w32_acm_status status = {0}; + + // We don't have a window to match the monitor on d3d11 composition mode. + if (sw->ctx->opts.composition) + return false; + + reliable = mp_dxgi_get_acm_status_from_hwnd(&p->dxgi_ctx, vo_w32_hwnd(sw->ctx->vo), &status); + + if (reliable && status.acm_enabled) + return true; + else + return false; +} + static bool d3d11_start_frame(struct ra_swapchain *sw, struct ra_fbo *out_fbo) { struct priv *p = sw->priv; @@ -496,6 +514,7 @@ static const struct ra_swapchain_fns d3d11_swapchain = { .color_depth = d3d11_color_depth, .target_csp = d3d11_target_color_space, .target_ref_luma = d3d11_target_ref_luma, + .target_global_color_management_status = d3d11_target_global_color_management_status, .start_frame = d3d11_start_frame, .submit_frame = d3d11_submit_frame, .swap_buffers = d3d11_swap_buffers, diff --git a/video/out/gpu/context.h b/video/out/gpu/context.h index 68a3621c0fc74..502af85338d64 100644 --- a/video/out/gpu/context.h +++ b/video/out/gpu/context.h @@ -80,6 +80,9 @@ struct ra_ctx_params { // See ra_swapchain_fns.target_ref_luma. Optional. float (*preferred_ref_luma)(struct ra_ctx *ctx); + // See ra_swapchain_fns.target_global_color_management_status. Optional. + bool (*global_color_management_status)(struct ra_ctx *ctx); + // See ra_swapchain_fns.set_color. Optional. bool (*set_color)(struct ra_ctx *ctx, struct mp_image_params *params); @@ -122,6 +125,10 @@ struct ra_swapchain_fns { // the system displays diffuse (SDR) white. Returns 0 if unknown. Optional. float (*target_ref_luma)(struct ra_swapchain *sw); + // Get enable status for global color management and HDR. Returns false if + // not enabled or result is not reliable. Optional. + bool (*target_global_color_management_status)(struct ra_swapchain *sw); + // Call into backends so they can use the appropriate platform-specific // functions to configure color spaces. Returns true if request was handled. bool (*set_color)(struct ra_swapchain *sw, struct mp_image_params *params); diff --git a/video/out/gpu/d3d11_helpers.c b/video/out/gpu/d3d11_helpers.c index 93510d705b4d0..44ff5a51160dc 100644 --- a/video/out/gpu/d3d11_helpers.c +++ b/video/out/gpu/d3d11_helpers.c @@ -26,7 +26,6 @@ #include "osdep/io.h" #include "osdep/threads.h" #include "osdep/windows_utils.h" -#include "video/out/win32/displayconfig.h" #include "d3d11_helpers.h" @@ -993,8 +992,11 @@ void mp_dxgi_factory_uninit(struct mp_dxgi_factory_ctx *ctx) SAFE_RELEASE(ctx->factory); SAFE_RELEASE(ctx->last_matched_output); - ctx->white_level_monitor = NULL; + ctx->target_monitor = NULL; ctx->sdr_white_level = 0; + ctx->color_management_status_reliable = false; + ctx->color_management_status.acm_enabled = false; + ctx->color_management_status.hdr_enabled = false; } bool mp_dxgi_output_desc_from_hwnd(struct mp_dxgi_factory_ctx *ctx, @@ -1009,7 +1011,7 @@ bool mp_dxgi_output_desc_from_hwnd(struct mp_dxgi_factory_ctx *ctx, return false; if (!ctx->factory || !IDXGIFactory1_IsCurrent(ctx->factory)) { - // This also clears `white_level_monitor`, to invalidate cached value. + // This also clears `target_monitor`, to invalidate cached value. // While we are using displayconfig API to get reference luminance, // DXGI IsCurrent() is actually tracking reference luminance changes in // settings. There is no window message sent on this change. @@ -1082,7 +1084,7 @@ float mp_dxgi_sdr_white_level_from_hwnd(struct mp_dxgi_factory_ctx *ctx, if (!mp_dxgi_output_desc_from_hwnd(ctx, hwnd, &desc)) return 0; - if (ctx && ctx->white_level_monitor == desc.Monitor) + if (ctx && ctx->target_monitor == desc.Monitor) return ctx->sdr_white_level; MONITORINFOEXW mi = { .cbSize = sizeof(mi) }; @@ -1092,7 +1094,7 @@ float mp_dxgi_sdr_white_level_from_hwnd(struct mp_dxgi_factory_ctx *ctx, float white_level = mp_w32_displayconfig_get_sdr_white_level(mi.szDevice); if (ctx) { - ctx->white_level_monitor = desc.Monitor; + ctx->target_monitor = desc.Monitor; ctx->sdr_white_level = white_level; } @@ -1102,6 +1104,48 @@ float mp_dxgi_sdr_white_level_from_hwnd(struct mp_dxgi_factory_ctx *ctx, #endif } +bool mp_dxgi_get_acm_status_from_hwnd(struct mp_dxgi_factory_ctx *ctx, + HWND hwnd, + struct mp_w32_acm_status *status) +{ +#if HAVE_WIN32_DESKTOP + DXGI_OUTPUT_DESC1 desc; + bool reliable = false; + + if (!mp_dxgi_output_desc_from_hwnd(ctx, hwnd, &desc)) { + ctx->color_management_status_reliable = false; + return false; + } + + // Cache ACM status to avoid query OS API every time. Switching ACM during + // playing would still trigger re-query. + if (ctx && ctx->target_monitor == desc.Monitor) { + status->acm_enabled = ctx->color_management_status.acm_enabled; + status->hdr_enabled = ctx->color_management_status.hdr_enabled; + return ctx->color_management_status_reliable; + } + + MONITORINFOEXW mi = { .cbSize = sizeof(mi) }; + if (!GetMonitorInfoW(desc.Monitor, (MONITORINFO*)&mi)) { + ctx->color_management_status_reliable = false; + return false; + } + + reliable = mp_w32_displayconfig_get_acm_status(mi.szDevice, status); + + if (ctx) { + ctx->target_monitor = desc.Monitor; + ctx->color_management_status_reliable = reliable; + ctx->color_management_status.acm_enabled = status->acm_enabled; + ctx->color_management_status.hdr_enabled = status->hdr_enabled; + } + + return reliable; +#else + return false; +#endif +} + struct pl_color_space mp_dxgi_desc_to_color_space(const DXGI_OUTPUT_DESC1 *desc) { struct pl_color_space ret = {0}; diff --git a/video/out/gpu/d3d11_helpers.h b/video/out/gpu/d3d11_helpers.h index f3d4908a17ba1..ec201db3ea07c 100644 --- a/video/out/gpu/d3d11_helpers.h +++ b/video/out/gpu/d3d11_helpers.h @@ -26,6 +26,7 @@ #include #include "video/mp_image.h" +#include "video/out/win32/displayconfig.h" #if !HAVE_DXGI_DEBUG_D3D11 DEFINE_GUID(DXGI_DEBUG_D3D11, 0x4b99317b, 0xac39, 0x4aa6, 0xbb, 0xb, 0xba, 0xa0, 0x47, 0x84, 0x79, 0x8f); @@ -65,8 +66,10 @@ struct d3d11_device_opts { struct mp_dxgi_factory_ctx { IDXGIFactory1 *factory; IDXGIOutput6 *last_matched_output; - HMONITOR white_level_monitor; + HMONITOR target_monitor; float sdr_white_level; + bool color_management_status_reliable; + struct mp_w32_acm_status color_management_status; }; void mp_dxgi_factory_uninit(struct mp_dxgi_factory_ctx *ctx); @@ -86,6 +89,12 @@ bool mp_dxgi_output_desc_from_swapchain(struct mp_dxgi_factory_ctx *ctx, float mp_dxgi_sdr_white_level_from_hwnd(struct mp_dxgi_factory_ctx *ctx, HWND hwnd); +// Get the status of ACM and HDR for the monitor the window is on. Returns +// false if related API is not supported. +bool mp_dxgi_get_acm_status_from_hwnd(struct mp_dxgi_factory_ctx *ctx, + HWND hwnd, + struct mp_w32_acm_status *status); + struct pl_color_space mp_dxgi_desc_to_color_space(const DXGI_OUTPUT_DESC1 *desc); OPT_STRING_VALIDATE_FUNC(mp_dxgi_validate_adapter); diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c index f731a1fdcdd5b..d003e30187d17 100644 --- a/video/out/vo_gpu_next.c +++ b/video/out/vo_gpu_next.c @@ -1095,6 +1095,19 @@ static void update_tm_viz(struct pl_color_map_params *params, static void update_hook_opts_dynamic(struct priv *p, const struct pl_hook *hook, const struct mp_image *mpi); +// Currently only used by Windows implementation +#ifdef _WIN32 +static bool get_global_color_management_status(struct priv *p) +{ + struct ra_swapchain *sw = p->ra_ctx->swapchain; + + if (sw->fns->target_global_color_management_status) + return sw->fns->target_global_color_management_status(sw); + else + return false; +} +#endif + static bool draw_frame(struct vo *vo, struct vo_frame *frame) { struct priv *p = vo->priv; @@ -1377,18 +1390,18 @@ static bool draw_frame(struct vo *vo, struct vo_frame *frame) #ifdef _WIN32 // Windows uses the sRGB piecewise function. Send piecewise sRGB to // Windows in HDR mode so that it can be converted to PQ, the same way - // as mpv does internally. Note that in SDR mode, even with ACM enabled, - // Windows assumes the display is sRGB. It doesn't perform gamma - // conversion, or any conversions would roundtrip back to sRGB. - // In which case the EOTF depends on the display. - // Ideally, compositors would agree on how to handle sRGB, but I’ll + // as mpv does internally. This happens on both ACM and HDR. + // For non-HDR/ACM cases, use power 2.2 since most monitors default to + // power 2.2. + // Ideally, compositors would agree on how to handle sRGB, but I'll // leave that part of the story for the reader to explore. // Note: Older Windows versions, without ACM, were not able to convert // sRGB to PQ output. We are not concerned about this case, as it would // look wrong anyway. bool target_pq = !target_unknown && target_csp.transfer == PL_COLOR_TRC_PQ; - if (opts->treat_srgb_as_power22 & 4 && target_pq) - target.color.transfer = PL_COLOR_TRC_SRGB; + if (opts->treat_srgb_as_power22 & 4) + if (get_global_color_management_status(p) || target_pq) + target.color.transfer = PL_COLOR_TRC_SRGB; #endif } stats_time_start(p->stats, "osd-update"); diff --git a/video/out/vulkan/context.c b/video/out/vulkan/context.c index 4fcdf11567c55..694823d19b287 100644 --- a/video/out/vulkan/context.c +++ b/video/out/vulkan/context.c @@ -591,11 +591,22 @@ static float target_ref_luma(struct ra_swapchain *sw) return 0; } +static bool target_global_color_management_status(struct ra_swapchain *sw) +{ + struct priv *p = sw->priv; + + if (p->params.global_color_management_status) + return p->params.global_color_management_status(sw->ctx); + else + return false; +} + static const struct ra_swapchain_fns vulkan_swapchain = { .color_depth = color_depth, .set_color = set_color, .target_csp = target_csp, .target_ref_luma = target_ref_luma, + .target_global_color_management_status = target_global_color_management_status, .start_frame = start_frame, .submit_frame = submit_frame, .swap_buffers = swap_buffers, diff --git a/video/out/vulkan/context_win.c b/video/out/vulkan/context_win.c index a05124347876d..2d2846d6fd2d9 100644 --- a/video/out/vulkan/context_win.c +++ b/video/out/vulkan/context_win.c @@ -69,6 +69,20 @@ static float preferred_ref_luma(struct ra_ctx *ctx) return mp_dxgi_sdr_white_level_from_hwnd(&p->dxgi_ctx, vo_w32_hwnd(ctx->vo)); } +static bool global_color_management_status(struct ra_ctx *ctx) +{ + struct priv *p = ctx->priv; + bool reliable = false; + struct mp_w32_acm_status status = {0}; + + reliable = mp_dxgi_get_acm_status_from_hwnd(&p->dxgi_ctx, vo_w32_hwnd(ctx->vo), &status); + + if (reliable && status.acm_enabled) + return true; + else + return false; +} + static bool win_init(struct ra_ctx *ctx) { struct priv *p = ctx->priv = talloc_zero(ctx, struct priv); @@ -94,6 +108,7 @@ static bool win_init(struct ra_ctx *ctx) .color_depth = color_depth, .preferred_csp = preferred_csp, .preferred_ref_luma = preferred_ref_luma, + .global_color_management_status = global_color_management_status, }; VkInstance inst = vk->vkinst->instance; diff --git a/video/out/win32/displayconfig.c b/video/out/win32/displayconfig.c index 85f7f067d43c8..d9a7906ee7f51 100644 --- a/video/out/win32/displayconfig.c +++ b/video/out/win32/displayconfig.c @@ -174,3 +174,52 @@ double mp_w32_displayconfig_get_sdr_white_level(const wchar_t *device) talloc_free(ctx); return white_level; } + +bool mp_w32_displayconfig_get_acm_status(const wchar_t *device, + struct mp_w32_acm_status *status) +{ + void *ctx = talloc_new(NULL); + + bool acm_status_unknown = true; + status->acm_enabled = false; + status->hdr_enabled = false; + + 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; + + DISPLAYCONFIG_PATH_INFO* path; + if (!(path = get_path(num_paths, paths, device))) + goto end; + + DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO color_info = { + .header = { + .size = sizeof(color_info), + .type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO, + .adapterId = path->targetInfo.adapterId, + .id = path->targetInfo.id, + } + }; + + if (DisplayConfigGetDeviceInfo(&color_info.header) != ERROR_SUCCESS) + goto end; + + acm_status_unknown = false; + // This particular combination indicates HDR mode is enabled. This is + // undocumented but used by WinRT. When wideColorEnforced is true we are in + //SDR mode with advanced color. + // Reference: + // + status->acm_enabled = color_info.advancedColorSupported && + color_info.advancedColorEnabled; + status->hdr_enabled = status->acm_enabled && + !color_info.wideColorEnforced; + +end: + talloc_free(ctx); + return !acm_status_unknown; +} diff --git a/video/out/win32/displayconfig.h b/video/out/win32/displayconfig.h index 9e5c4946844f0..af71a3c714379 100644 --- a/video/out/win32/displayconfig.h +++ b/video/out/win32/displayconfig.h @@ -20,6 +20,12 @@ #include +// Struct describing current host OS's global color management and HDR status +struct mp_w32_acm_status { + bool acm_enabled; + bool hdr_enabled; +}; + // 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); @@ -28,4 +34,9 @@ double mp_w32_displayconfig_get_refresh_rate(const wchar_t *device); // in nits, using the Windows 10 DisplayConfig API. Returns 0.0 on failure. double mp_w32_displayconfig_get_sdr_white_level(const wchar_t *device); +// Given a GDI monitor device name, get the current ACM and HDR status. +// Returns false if related API is not supported. +bool mp_w32_displayconfig_get_acm_status(const wchar_t *device, + struct mp_w32_acm_status *status); + #endif