From e51de54ee401df70f042103fb7c1c346ee4fdc23 Mon Sep 17 00:00:00 2001 From: nmzik Date: Sun, 11 Oct 2026 20:29:32 +0200 Subject: [PATCH] add hdr display support --- CMakeLists.txt | 19 ++--- src/common/emulatorConfig.cpp | 4 + src/common/emulatorConfig.h | 2 + .../host_gpu/renderer/image/imageInfo.h | 13 ++- .../shaders/gpu_video_out_overlay.frag | 48 +++++++++-- src/graphics/host_gpu/vulkanCommon.h | 8 ++ src/graphics/presentation/presenter.h | 4 +- src/graphics/presentation/systemOverlay.cpp | 6 ++ src/graphics/presentation/videoOut.cpp | 64 +++++++------- .../presentation/window/swapchain.cpp | 83 +++++++++++++++---- .../presentation/window/vulkanWindow.cpp | 5 ++ .../forms/configuration_edit_dialog.ui | 6 ++ src/launcher/include/configuration.h | 2 + src/launcher/src/configuration.cpp | 2 + src/launcher/src/configurationEditDialog.cpp | 2 + src/launcher/src/mainDialog.cpp | 3 + src/loader/systemContent.cpp | 2 + src/main.cpp | 6 ++ 18 files changed, 215 insertions(+), 64 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d50f3b36..83466115 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -303,28 +303,27 @@ list(APPEND kyty_emulator_src "${fault_buffer_shader_header}") list(APPEND kyty_emulator_src ${gpu_tiler_shader_headers}) set(gpu_blit_generated_dir "${PROJECT_BINARY_DIR}/gpu_blit_shaders") -set(gpu_blit_shader_sources - "${gpu_tiler_shader_dir}/gpu_blit_fs_triangle.vert" - "${gpu_tiler_shader_dir}/gpu_blit_color_to_ms_depth.frag" - "${gpu_tiler_shader_dir}/gpu_video_out_overlay.frag" -) -foreach(shader_source IN LISTS gpu_blit_shader_sources) - get_filename_component(shader_name "${shader_source}" NAME_WE) +function(add_gpu_blit_shader shader_name shader_file) + set(shader_source "${gpu_tiler_shader_dir}/${shader_file}") set(shader_spv "${gpu_blit_generated_dir}/${shader_name}.spv") set(shader_header "${gpu_blit_generated_dir}/${shader_name}_spv.h") string(TOUPPER "${shader_name}_SPV" shader_symbol) add_custom_command( OUTPUT "${shader_header}" COMMAND ${CMAKE_COMMAND} -E make_directory "${gpu_blit_generated_dir}" - COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os + COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os ${ARGN} "-I${gpu_tiler_shader_dir}" -o "${shader_spv}" "${shader_source}" COMMAND ${CMAKE_COMMAND} -DINPUT=${shader_spv} -DOUTPUT=${shader_header} -DSYMBOL=${shader_symbol} -P "${KYTY_SOURCE_DIR}/embed_spirv.cmake" DEPENDS "${shader_source}" VERBATIM ) - list(APPEND gpu_blit_shader_headers "${shader_header}") -endforeach() + set(gpu_blit_shader_headers ${gpu_blit_shader_headers} "${shader_header}" PARENT_SCOPE) +endfunction() +add_gpu_blit_shader(gpu_blit_fs_triangle gpu_blit_fs_triangle.vert) +add_gpu_blit_shader(gpu_blit_color_to_ms_depth gpu_blit_color_to_ms_depth.frag) +add_gpu_blit_shader(gpu_video_out_overlay gpu_video_out_overlay.frag) +add_gpu_blit_shader(gpu_imgui_hdr gpu_video_out_overlay.frag -DIMGUI_HDR=1) list(APPEND kyty_emulator_src ${gpu_blit_shader_headers}) list(APPEND kyty_emulator_src diff --git a/src/common/emulatorConfig.cpp b/src/common/emulatorConfig.cpp index 58ec3e91..9dc6aefc 100644 --- a/src/common/emulatorConfig.cpp +++ b/src/common/emulatorConfig.cpp @@ -72,6 +72,10 @@ bool FullscreenEnabled() { return g_config->fullscreen_enabled; } +bool HdrEnabled() { + return g_config->hdr_enabled; +} + bool HideCursorEnabled() { return g_config->hide_cursor_enabled; } diff --git a/src/common/emulatorConfig.h b/src/common/emulatorConfig.h index 8684a80b..6cd047fc 100644 --- a/src/common/emulatorConfig.h +++ b/src/common/emulatorConfig.h @@ -53,6 +53,7 @@ struct ConfigOptions { PresentMode present_mode = PresentMode::Mailbox; int32_t gpu_index = -1; bool fullscreen_enabled = false; + bool hdr_enabled = false; bool hide_cursor_enabled = false; bool splash_enabled = true; bool vr_enabled = false; @@ -100,6 +101,7 @@ uint32_t GetControllerVibrationIntensity(); PresentMode GetPresentMode(); int32_t GetGpuIndex(); bool FullscreenEnabled(); +bool HdrEnabled(); bool HideCursorEnabled(); bool VrEnabled(); bool AmdCpuEnabled(); diff --git a/src/graphics/host_gpu/renderer/image/imageInfo.h b/src/graphics/host_gpu/renderer/image/imageInfo.h index b8ae73e3..71bedc34 100644 --- a/src/graphics/host_gpu/renderer/image/imageInfo.h +++ b/src/graphics/host_gpu/renderer/image/imageInfo.h @@ -362,8 +362,17 @@ struct VideoOutFormatPolicy { }; inline constexpr uint64_t VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ = 0x8100070422000000ull; +inline constexpr uint64_t VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ = 0x8100070400000000ull; -inline constexpr std::array VIDEO_OUT_FORMAT_POLICIES {{ +[[nodiscard]] constexpr bool IsHdrVideoOutPixelFormat(uint64_t format) { + switch (format) { + case VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ: + case VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ: return true; + default: return false; + } +} + +inline constexpr std::array VIDEO_OUT_FORMAT_POLICIES {{ {0x8000000022000000ull, {vk::Format::eR8G8B8A8Srgb, Prospero::BufferFormat::k8_8_8_8Srgb, 4, false}}, {0x8000000000000000ull, @@ -374,6 +383,8 @@ inline constexpr std::array VIDEO_OUT_FORMAT_POLICIES { {vk::Format::eA2R10G10B10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}}, {VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ, {vk::Format::eA2B10G10R10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}}, + {VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ, + {vk::Format::eA2R10G10B10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}}, {0xc001000622000000ull, {vk::Format::eR16G16B16A16Sfloat, Prospero::BufferFormat::k16_16_16_16Float, 8, false}}, {0xc001000600000000ull, diff --git a/src/graphics/host_gpu/shaders/gpu_video_out_overlay.frag b/src/graphics/host_gpu/shaders/gpu_video_out_overlay.frag index 63c33072..fcc7e7ba 100644 --- a/src/graphics/host_gpu/shaders/gpu_video_out_overlay.frag +++ b/src/graphics/host_gpu/shaders/gpu_video_out_overlay.frag @@ -1,15 +1,53 @@ #version 450 -layout(location = 0) in vec2 uv; layout(location = 0) out vec4 color; + +vec3 SdrToHdr10(vec3 rgb, bool source_linear) { + rgb = clamp(rgb, 0.0, 1.0); + vec3 linear = source_linear ? rgb : + mix(rgb / 12.92, pow((rgb + 0.055) / 1.055, vec3(2.4)), + greaterThan(rgb, vec3(0.04045))); + // Convert primaries and scale 250-nit reference white relative to 10,000 nits. + const mat3 to_rec2020 = (250.0 / 10000.0) * + mat3(0.627404, 0.069097, 0.016391, + 0.329283, 0.919540, 0.088013, + 0.043313, 0.011362, 0.895595); + vec3 powered = pow(to_rec2020 * linear, vec3(2610.0 / 16384.0)); + return pow((3424.0 / 4096.0 + (2413.0 / 128.0) * powered) / + (1.0 + (2392.0 / 128.0) * powered), vec3(2523.0 / 32.0)); +} + +#ifdef IMGUI_HDR +layout(set = 0, binding = 0) uniform texture2D image; +layout(set = 1, binding = 0) uniform sampler image_sampler; +layout(location = 0) in struct { vec4 Color; vec2 UV; } In; + +void main() { + vec4 sampled = In.Color * texture(sampler2D(image, image_sampler), In.UV); + color = vec4(SdrToHdr10(sampled.rgb, false), sampled.a); +} +#else +layout(location = 0) in vec2 uv; layout(binding = 0) uniform sampler2D source; layout(push_constant) uniform AlphaControl { - uint premultiplied; + uint flags; // bit 0: premultiplied, bit 1: HDR target, bit 2: PQ source, bit 3: linear source }; void main() { - color = texture(source, uv); - if (premultiplied == 0u) { - color.rgb *= color.a; + const uint HDR_TARGET = 2u; + const uint PQ_SOURCE = 4u; + vec4 sampled = texture(source, uv); + bool premultiplied = (flags & 1u) != 0u; + bool convert_to_hdr = (flags & (HDR_TARGET | PQ_SOURCE)) == HDR_TARGET; + if (convert_to_hdr && premultiplied) { + sampled.rgb = sampled.a > 0.0 ? sampled.rgb / sampled.a : vec3(0.0); } + if (convert_to_hdr) { + sampled.rgb = SdrToHdr10(sampled.rgb, (flags & 8u) != 0u); + } + if (!premultiplied || convert_to_hdr) { + sampled.rgb *= sampled.a; + } + color = sampled; } +#endif diff --git a/src/graphics/host_gpu/vulkanCommon.h b/src/graphics/host_gpu/vulkanCommon.h index f735d8ab..d9450fa4 100644 --- a/src/graphics/host_gpu/vulkanCommon.h +++ b/src/graphics/host_gpu/vulkanCommon.h @@ -28,6 +28,14 @@ vk::Format VulkanFormat(Prospero::BufferFormat guest_format); void RequireVulkanSuccess(vk::Result result, const char* operation); vk::ShaderModule CompileSPV(std::span code, vk::Device device); +[[nodiscard]] constexpr bool IsHdrSwapchainFormat(vk::Format format) { + switch (format) { + case vk::Format::eA2B10G10R10UnormPack32: + case vk::Format::eA2R10G10B10UnormPack32: return true; + default: return false; + } +} + template void SetVulkanObjectNameF(vk::Device device, Handle handle, fmt::format_string format, Args&&... args) { diff --git a/src/graphics/presentation/presenter.h b/src/graphics/presentation/presenter.h index f5dc36b2..a0821a8a 100644 --- a/src/graphics/presentation/presenter.h +++ b/src/graphics/presentation/presenter.h @@ -26,11 +26,13 @@ public: ~Presenter(); KYTY_CLASS_NO_COPY(Presenter); - [[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info); + [[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info, + bool hdr = false); [[nodiscard]] Frame& PrepareBlankFrame(uint32_t width, uint32_t height, bool opaque, CommandBuffer* producer = nullptr); [[nodiscard]] bool PresentLastFrame(); [[nodiscard]] bool IsGuestPaused() const noexcept; + [[nodiscard]] bool IsHdrSupported() const noexcept; [[nodiscard]] bool NeedsSystemOverlayRefresh() const noexcept; [[nodiscard]] RenderContext& Renderer() const noexcept; void Present(Frame& frame); diff --git a/src/graphics/presentation/systemOverlay.cpp b/src/graphics/presentation/systemOverlay.cpp index 1d80af48..f39d2c73 100644 --- a/src/graphics/presentation/systemOverlay.cpp +++ b/src/graphics/presentation/systemOverlay.cpp @@ -7,6 +7,7 @@ #include "common/file.h" #include "common/logging/log.h" #include "common/stringUtils.h" +#include "gpu_blit_shaders/gpu_imgui_hdr_spv.h" #include "graphics/host_gpu/graphicContext.h" #include "graphics/presentation/ddsImage.h" #include "imgui.h" @@ -890,6 +891,11 @@ struct SystemOverlay::Impl { info.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount = 1; info.PipelineInfoMain.PipelineRenderingCreateInfo.pColorAttachmentFormats = &color_format; info.CheckVkResultFn = CheckVulkanResult; + if (IsHdrSwapchainFormat(format)) { + info.CustomShaderFragCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + info.CustomShaderFragCreateInfo.codeSize = sizeof(GPU_IMGUI_HDR_SPV); + info.CustomShaderFragCreateInfo.pCode = GPU_IMGUI_HDR_SPV; + } EXIT_IF(!ImGui_ImplVulkan_Init(&info)); vulkan_initialized = true; } diff --git a/src/graphics/presentation/videoOut.cpp b/src/graphics/presentation/videoOut.cpp index 57e25baa..11e989e4 100644 --- a/src/graphics/presentation/videoOut.cpp +++ b/src/graphics/presentation/videoOut.cpp @@ -272,6 +272,7 @@ public: void Init(uint32_t width, uint32_t height); FlipQueue& GetFlipQueue() { return m_flip_queue; } Graphics::RenderContext& Renderer() const noexcept { return m_renderer; } + bool IsHdrSupported() const noexcept { return m_presenter.IsHdrSupported(); } void VblankBegin(); void VblankEnd(); @@ -558,14 +559,6 @@ Graphics::ImageInfo BufferAttributeGroup::ImageInfo(const VideoOutBuffer& buffer if (!Graphics::DecodeVideoOutPixelFormat(attribute.pixel_format, pixel_format)) { EXIT("unsupported video-out pixel format: 0x%016" PRIx64 "\n", attribute.pixel_format); } - if (attribute.pixel_format == Graphics::VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ) { - static std::atomic_flag warned = ATOMIC_FLAG_INIT; - if (!warned.test_and_set(std::memory_order_relaxed)) { - Log::WriteToConsoleAndLog(fmt::format( - "Warning: HDR-to-SDR conversion is not implemented; displaying PQ video-out " - "pixels unchanged (format=0x{:016x}).\n", attribute.pixel_format)); - } - } const auto tile_mode = Graphics::Prospero::TileMode::kRenderTarget; Graphics::ImageInfo info {}; info.data = {buffer.data_address, 0}; @@ -1071,31 +1064,31 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) { uint32_t height = 0; bool current = false; bool premultiplied_alpha = false; + bool hdr = false; { Common::LockGuard lock(cfg->mutex); current = cfg->opened && !cfg->closing && cfg->generation == generation; - if (current) { - if (special) { - width = cfg->width; - height = cfg->height; - } else { - const auto& surface = cfg->buffers[index]; - if (!surface.Occupied()) { - EXIT("cannot prepare flip from an unregistered surface, id=%" PRIu64 - " index=%d\n", - request_id, index); - } - if (surface.group_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX || - !cfg->groups[surface.group_index].occupied) { - EXIT("video-out surface references an unavailable attribute group, id=%" PRIu64 - " index=%d group=%d\n", - request_id, index, surface.group_index); - } - source_info = cfg->groups[surface.group_index].ImageInfo(surface); - premultiplied_alpha = - (cfg->groups[surface.group_index].attribute.option & - VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED) != 0; + if (current && special) { + width = cfg->width; + height = cfg->height; + } else if (current) { + const auto& surface = cfg->buffers[index]; + if (!surface.Occupied()) { + EXIT("cannot prepare flip from an unregistered surface, id=%" PRIu64 + " index=%d\n", + request_id, index); } + if (surface.group_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX || + !cfg->groups[surface.group_index].occupied) { + EXIT("video-out surface references an unavailable attribute group, id=%" PRIu64 + " index=%d group=%d\n", + request_id, index, surface.group_index); + } + const auto& group = cfg->groups[surface.group_index]; + source_info = group.ImageInfo(surface); + hdr = Graphics::IsHdrVideoOutPixelFormat(group.attribute.pixel_format); + premultiplied_alpha = (group.attribute.option & + VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED) != 0; } } if (!current) { @@ -1108,12 +1101,18 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) { } return; } + static std::atomic_flag warned = ATOMIC_FLAG_INIT; + if (hdr && !m_presenter.IsHdrSupported() && !warned.test_and_set(std::memory_order_relaxed)) { + Log::WriteToConsoleAndLog( + "Warning: HDR output is unavailable; HDR-to-SDR conversion is not implemented, " + "displaying PQ video-out pixels unchanged.\n"); + } Graphics::Presenter::Frame* frame = nullptr; if (special) { frame = &m_presenter.PrepareBlankFrame(width, height, index == VIDEO_OUT_BUFFER_INDEX_BLACK, &buffer); } else { - frame = &m_presenter.PrepareFrame(buffer, source_info); + frame = &m_presenter.PrepareFrame(buffer, source_info, hdr); } Common::LockGuard lock(m_mutex); @@ -1790,11 +1789,14 @@ KYTY_SYSV_ABI int VideoOutGetOutputStatus(int handle, VideoOutOutputStatus* stat int32_t attribute3 = 0; Loader::SystemContentParamSfoGetInt("ATTRIBUTE3", &attribute3); + int32_t attribute = 0; + const bool hdr_allowed = !Loader::SystemContentParamSfoGetInt("ATTRIBUTE", &attribute) || + (attribute & 0x20000000) != 0; ctx->mutex.Lock(); // Primary output reports 4K unless param.json Video-out Info enables resolution detection. status->resolution = ((attribute3 & 4) != 0 && ctx->width < 3840 && ctx->height < 2160 ? 1u : 2u); - status->dynamicRange = 1; + status->dynamicRange = hdr_allowed && DriverState().IsHdrSupported() ? 2u : 1u; status->refreshRate = (ctx->output_mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ || Config::GetVblankFrequency() >= 119 ? VIDEO_OUT_REFRESH_RATE_119_88HZ diff --git a/src/graphics/presentation/window/swapchain.cpp b/src/graphics/presentation/window/swapchain.cpp index ff684659..99e8a289 100644 --- a/src/graphics/presentation/window/swapchain.cpp +++ b/src/graphics/presentation/window/swapchain.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -31,6 +32,8 @@ struct Presenter::Frame { vk::ImageView view = nullptr; uint64_t present_tick = 0; bool busy = false; + bool hdr = false; + bool blank = false; void Configure(GraphicContext& graphics, vk::Extent2D extent, vk::Format format); void Transit(vk::CommandBuffer command, vk::ImageLayout layout, vk::AccessFlags2 access); @@ -274,14 +277,24 @@ public: return static_cast(m_images.size()); } [[nodiscard]] vk::Format Format() const noexcept { return m_format; } + [[nodiscard]] bool IsHdrSupported() const noexcept { return m_hdr_supported.load(); } + bool SetHdr(bool hdr) { + const bool changed = m_hdr_active != (hdr && IsHdrSupported()); + m_hdr_requested = hdr; + return changed; + } private: + vk::SurfaceFormatKHR SelectFormat(); void Destroy(); void DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer); WindowContext& m_window; vk::SwapchainKHR m_handle = nullptr; vk::Format m_format = vk::Format::eUndefined; + std::atomic_bool m_hdr_supported {false}; + bool m_hdr_requested = false; + bool m_hdr_active = false; vk::Extent2D m_extent {}; // Drawable pixel size observed when this swapchain was created. vk::Extent2D m_window_extent {}; @@ -346,6 +359,40 @@ struct Presenter::Impl { std::atomic presented_overlay_revision {0}; }; +vk::SurfaceFormatKHR Swapchain::SelectFormat() { + const auto& formats = m_window.surface_capabilities.formats; + auto& graphics = m_window.graphic_ctx; + const auto hdr_format = std::ranges::find_if(formats, [&](const auto& candidate) { + return candidate.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT && + IsHdrSwapchainFormat(candidate.format) && + static_cast( + graphics.GetFormatProperties(candidate.format).optimalTilingFeatures & + vk::FormatFeatureFlagBits::eBlitDst); + }); + m_hdr_supported = Config::HdrEnabled() && hdr_format != formats.end(); + m_hdr_active = m_hdr_requested && IsHdrSupported(); + if (m_hdr_active) { + return *hdr_format; + } + if (Config::HdrEnabled() && !IsHdrSupported()) { + LOGF("HDR10 surface unavailable; using SDR output\n"); + } + if (formats.size() == 1 && formats.front().format == vk::Format::eUndefined) { + return {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear}; + } + for (const auto& candidate: formats) { + if (candidate.colorSpace != vk::ColorSpaceKHR::eSrgbNonlinear) { + continue; + } + switch (candidate.format) { + case vk::Format::eB8G8R8A8Unorm: + case vk::Format::eR8G8B8A8Unorm: return candidate; + default: break; + } + } + EXIT("no supported UNORM swapchain format\n"); +} + void Swapchain::Create() { auto& graphics = m_window.graphic_ctx; EXIT_IF(graphics.device == nullptr); @@ -385,18 +432,7 @@ void Swapchain::Create() { ? vk::CompositeAlphaFlagBitsKHR::eOpaque : vk::CompositeAlphaFlagBitsKHR::eInherit; - vk::SurfaceFormatKHR format {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear}; - if (surface.formats.size() != 1 || surface.formats.front().format != vk::Format::eUndefined) { - const auto it = std::find_if(surface.formats.begin(), surface.formats.end(), - [](const vk::SurfaceFormatKHR& candidate) { - return candidate.format == vk::Format::eB8G8R8A8Unorm || - candidate.format == vk::Format::eR8G8B8A8Unorm; - }); - if (it == surface.formats.end()) { - EXIT("no supported UNORM swapchain format\n"); - } - format = *it; - } + const auto format = SelectFormat(); m_format = format.format; const auto swapchain_features = graphics.GetFormatProperties(m_format).optimalTilingFeatures; if (!static_cast(swapchain_features & vk::FormatFeatureFlagBits::eBlitDst)) { @@ -436,6 +472,7 @@ void Swapchain::Create() { RequireVulkanSuccess(graphics.device.createSwapchainKHR(&create_info, nullptr, &m_handle), "vkCreateSwapchainKHR"); EXIT_IF(m_handle == nullptr); + LOGF("Vulkan presentation: %s\n", m_hdr_active ? "HDR10 (PQ)" : "SDR"); m_images = EnumerateVulkan( "vkGetSwapchainImagesKHR", [&](uint32_t* count, vk::Image* images) { @@ -692,9 +729,11 @@ void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& l write.descriptorType = vk::DescriptorType::eCombinedImageSampler; write.pImageInfo = ℑ command.pushDescriptorSetKHR(vk::PipelineBindPoint::eGraphics, m_overlay_layout, 0, 1, &write); - const uint32_t premultiplied = layer.premultiplied_alpha; + const uint32_t flags = (layer.premultiplied_alpha ? 1u : 0u) | + (m_hdr_active ? 2u : 0u) | (frame.hdr ? 4u : 0u) | + (frame.image.format == vk::Format::eR16G16B16A16Sfloat ? 8u : 0u); command.pushConstants(m_overlay_layout, vk::ShaderStageFlagBits::eFragment, 0, - sizeof(premultiplied), &premultiplied); + sizeof(flags), &flags); command.bindPipeline(vk::PipelineBindPoint::eGraphics, m_overlay_pipeline); vk::RenderingAttachmentInfo attachment {}; attachment.imageView = m_image_views[m_image_index]; @@ -868,7 +907,7 @@ Presenter::Presenter(WindowContext& window): m_impl(std::make_unique(windo Presenter::~Presenter() = default; -Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info) { +Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr) { KYTY_PROFILER_FUNCTION(); EXIT_IF(buffer.IsInvalid()); auto frame_format = info.pixel_format; @@ -878,6 +917,8 @@ Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo default: break; } auto* frame = m_impl->frames.Acquire({info.extent.width, info.extent.height}, frame_format); + frame->hdr = hdr; + frame->blank = false; Common::LockGuard render_lock(m_impl->renderer.GetMutex()); auto& image = m_impl->ResolveSurface(info); if (image.backing.format == vk::Format::eUndefined) { @@ -894,6 +935,8 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height, KYTY_PROFILER_FUNCTION(); auto format = m_impl->frames.GetFormat(); auto* frame = m_impl->frames.Acquire({width, height}, format); + frame->hdr = false; + frame->blank = true; Common::LockGuard render_lock(m_impl->renderer.GetMutex()); frame->Configure(m_impl->window.graphic_ctx, {width, height}, format); vk::ClearColorValue clear {}; @@ -922,6 +965,10 @@ bool Presenter::IsGuestPaused() const noexcept { return m_impl->window.loop.paused.load(std::memory_order_acquire); } +bool Presenter::IsHdrSupported() const noexcept { + return m_impl->swapchain.IsHdrSupported(); +} + bool Presenter::NeedsSystemOverlayRefresh() const noexcept { const auto visual = GetSystemOverlayVisualState(); return visual.active || @@ -967,8 +1014,12 @@ void Presenter::Impl::Present() { KYTY_PROFILER_FUNCTION(); const auto overlay_visual = GetSystemOverlayVisualState(); + // Keep black/blank flips in the current color space; only real main-bus frames switch it. + const auto* main_frame = layers[0].frame; + const bool hdr_changed = main_frame != nullptr && !main_frame->blank && + swapchain.SetHdr(main_frame->hdr); // Some window systems keep presenting an old swapchain after a resize. - if (swapchain.NeedsResize()) { + if (hdr_changed || swapchain.NeedsResize()) { RecoverSwapchain(Swapchain::Status::Recreate); } for (uint32_t attempt = 0; attempt < 2; attempt++) { diff --git a/src/graphics/presentation/window/vulkanWindow.cpp b/src/graphics/presentation/window/vulkanWindow.cpp index 43cc734d..5f23e283 100644 --- a/src/graphics/presentation/window/vulkanWindow.cpp +++ b/src/graphics/presentation/window/vulkanWindow.cpp @@ -685,6 +685,11 @@ static void VulkanGetExtensions(VulkanExtensions& r) { }); r.enable_validation_layers = Config::VulkanValidationEnabled(); + if (Config::HdrEnabled() && + HasExtension(r.available_extensions, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME) && + !HasExtension(r.required_extensions, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) { + r.required_extensions.push_back(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME); + } if (HasExtension(r.available_extensions, VK_EXT_DEBUG_UTILS_EXTENSION_NAME)) { r.required_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); diff --git a/src/launcher/forms/configuration_edit_dialog.ui b/src/launcher/forms/configuration_edit_dialog.ui index 17711178..5c0baf00 100644 --- a/src/launcher/forms/configuration_edit_dialog.ui +++ b/src/launcher/forms/configuration_edit_dialog.ui @@ -180,6 +180,12 @@ Accurate texture synchronization + + + Enable HDR10 output for supported games. Requires HDR enabled on the host display. + Enable HDR display support + + diff --git a/src/launcher/include/configuration.h b/src/launcher/include/configuration.h index 047c043f..649c7cee 100644 --- a/src/launcher/include/configuration.h +++ b/src/launcher/include/configuration.h @@ -117,6 +117,7 @@ public: PresentMode present_mode = PresentMode::Mailbox; int gpu_index = -1; bool fullscreen_enabled = false; + bool hdr_enabled = false; bool hide_cursor_enabled = false; bool readback_linear_images = false; bool sync_raw_image_buffers = false; @@ -152,6 +153,7 @@ public: present_mode = other.present_mode; gpu_index = other.gpu_index; fullscreen_enabled = other.fullscreen_enabled; + hdr_enabled = other.hdr_enabled; hide_cursor_enabled = other.hide_cursor_enabled; readback_linear_images = other.readback_linear_images; sync_raw_image_buffers = other.sync_raw_image_buffers; diff --git a/src/launcher/src/configuration.cpp b/src/launcher/src/configuration.cpp index 01f248da..76bc81d7 100644 --- a/src/launcher/src/configuration.cpp +++ b/src/launcher/src/configuration.cpp @@ -10,6 +10,7 @@ QVariantMap Configuration::GameSettings() const { KYTY_CFG_SET(present_mode); KYTY_CFG_SET(gpu_index); KYTY_CFG_SET(fullscreen_enabled); + KYTY_CFG_SET(hdr_enabled); KYTY_CFG_SET(hide_cursor_enabled); KYTY_CFG_SET(readback_linear_images); KYTY_CFG_SET(sync_raw_image_buffers); @@ -55,6 +56,7 @@ void Configuration::ReadGameSettingsValues(const Settings& s) { present_mode = PresentMode::Mailbox; } KYTY_CFG_GET(fullscreen_enabled); + KYTY_CFG_GET(hdr_enabled); KYTY_CFG_GET(hide_cursor_enabled); KYTY_CFG_GET(readback_linear_images); KYTY_CFG_GET(sync_raw_image_buffers); diff --git a/src/launcher/src/configurationEditDialog.cpp b/src/launcher/src/configurationEditDialog.cpp index 657d7a9d..3493b00e 100644 --- a/src/launcher/src/configurationEditDialog.cpp +++ b/src/launcher/src/configurationEditDialog.cpp @@ -273,6 +273,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) { info.gpu_index >= 0 && info.gpu_index < m_ui->comboBox_gpu->count() - 1 ? info.gpu_index + 1 : 0); m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled); + m_ui->checkBox_hdr->setChecked(info.hdr_enabled); m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled); m_ui->checkBox_readback->setChecked(info.readback_linear_images); m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers); @@ -434,6 +435,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo TextToEnum(ui.comboBox_present_mode->currentText()); info.gpu_index = ui.comboBox_gpu->currentIndex() - 1; info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked(); + info.hdr_enabled = ui.checkBox_hdr->isChecked(); info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked(); info.readback_linear_images = ui.checkBox_readback->isChecked(); info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked(); diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index ac0c4297..8d3ba3da 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -245,6 +245,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) { if (info.fullscreen_enabled) { args << "--fullscreen"; } + if (info.hdr_enabled) { + args << "--hdr"; + } if (info.hide_cursor_enabled) { args << "--hide-cursor"; } diff --git a/src/loader/systemContent.cpp b/src/loader/systemContent.cpp index 1537b4fe..6db2bae5 100644 --- a/src/loader/systemContent.cpp +++ b/src/loader/systemContent.cpp @@ -164,6 +164,8 @@ void Psf::OpenJson(const std::filesystem::path& file_name) { add_string_param("contentId", "CONTENT_ID"); add_string_param("contentVersion", "APP_VER"); add_string_param("appVersion", "APP_VER"); + AddParamInt("ATTRIBUTE", 0); + add_int_param("attribute", "ATTRIBUTE"); add_int_param("attribute3", "ATTRIBUTE3"); add_int_param("userDefinedParam1", "USER_DEFINED_PARAM_1"); add_int_param("userDefinedParam2", "USER_DEFINED_PARAM_2"); diff --git a/src/main.cpp b/src/main.cpp index 6cfdcba7..8057e56f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -61,6 +61,7 @@ static void PrintUsage() { ::printf( " --gpu Vulkan physical device index. Default: auto.\n"); ::printf(" --fullscreen Run in borderless desktop fullscreen.\n"); + ::printf(" --hdr Enable HDR10 output when supported.\n"); ::printf( " --hide-cursor Hide the cursor after 2 s idle. Default: off.\n"); ::printf(" --no-splash Disable the startup splash screen.\n"); @@ -215,6 +216,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he continue; } + if (arg == "--hdr") { + options.config.hdr_enabled = true; + continue; + } + if (arg == "--hide-cursor") { options.config.hide_cursor_enabled = true; continue;