diff --git a/CMakeLists.txt b/CMakeLists.txt index c2aa8dbf3..c22020c68 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -316,7 +316,7 @@ function(add_gpu_blit_shader shader_name shader_file) "-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}" + DEPENDS "${shader_source}" "${gpu_tiler_shader_dir}/gpu_video_out_hdr10_to_sdr.inc" VERBATIM ) set(gpu_blit_shader_headers ${gpu_blit_shader_headers} "${shader_header}" PARENT_SCOPE) @@ -738,6 +738,8 @@ if(BUILD_TESTING) COMMAND $ --gpu-command-lane-only) add_test(NAME pm4_flip COMMAND $ --flip-only) + add_test(NAME video_out_hdr10_to_sdr + COMMAND $ --video-out-only) add_test(NAME depth_bits COMMAND $ --depth-bits-only) add_test(NAME msaa_storage diff --git a/src/graphics/host_gpu/shaders/gpu_video_out_hdr10_to_sdr.inc b/src/graphics/host_gpu/shaders/gpu_video_out_hdr10_to_sdr.inc new file mode 100644 index 000000000..eb3cbbe27 --- /dev/null +++ b/src/graphics/host_gpu/shaders/gpu_video_out_hdr10_to_sdr.inc @@ -0,0 +1,28 @@ +// Converts SMPTE ST 2084 PQ (or linear nits) Rec.2020 to sRGB-encoded Rec.709. The 250-nit SDR +// white scales to 1.0, then an extended Reinhard curve rolls off luminance above 0.75 so that +// 10,000 nits reaches 1.0. +vec3 Hdr10ToSdr(vec3 rgb, bool source_linear) { + vec3 nits = clamp(rgb, 0.0, 10000.0); + if (!source_linear) { + vec3 powered = pow(rgb, vec3(32.0 / 2523.0)); + nits = 10000.0 * pow(max(powered - 3424.0 / 4096.0, 0.0) / + (2413.0 / 128.0 - (2392.0 / 128.0) * powered), + vec3(16384.0 / 2610.0)); + } + const mat3 to_rec709 = (1.0 / 250.0) * + mat3(1.660491, -0.124550, -0.018151, + -0.587641, 1.132900, -0.100579, + -0.072850, -0.008349, 1.118730); + vec3 linear = max(to_rec709 * nits, 0.0); + const float knee = 0.75; + const float span = 1.0 - knee; + const float white = (10000.0 / 250.0 - knee) / span; + float luma = dot(linear, vec3(0.2126, 0.7152, 0.0722)); + if (luma > knee) { + float x = (luma - knee) / span; + linear *= (knee + span * x * (1.0 + x / (white * white)) / (1.0 + x)) / luma; + } + linear = clamp(linear, 0.0, 1.0); + return mix(linear * 12.92, 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055, + greaterThan(linear, vec3(0.0031308))); +} 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 fcc7e7ba2..42afba380 100644 --- a/src/graphics/host_gpu/shaders/gpu_video_out_overlay.frag +++ b/src/graphics/host_gpu/shaders/gpu_video_out_overlay.frag @@ -1,4 +1,5 @@ #version 450 +#extension GL_GOOGLE_include_directive : require layout(location = 0) out vec4 color; @@ -27,25 +28,33 @@ void main() { color = vec4(SdrToHdr10(sampled.rgb, false), sampled.a); } #else +#include "gpu_video_out_hdr10_to_sdr.inc" + layout(location = 0) in vec2 uv; layout(binding = 0) uniform sampler2D source; layout(push_constant) uniform AlphaControl { - uint flags; // bit 0: premultiplied, bit 1: HDR target, bit 2: PQ source, bit 3: linear source + uint flags; // bit 0: premultiplied, 1: HDR target, 2: PQ source, 3: linear source, 4: opaque }; void main() { const uint HDR_TARGET = 2u; const uint PQ_SOURCE = 4u; vec4 sampled = texture(source, uv); + if ((flags & 16u) != 0u) { + sampled.a = 1.0; + } bool premultiplied = (flags & 1u) != 0u; - bool convert_to_hdr = (flags & (HDR_TARGET | PQ_SOURCE)) == HDR_TARGET; - if (convert_to_hdr && premultiplied) { + uint conversion = flags & (HDR_TARGET | PQ_SOURCE); + bool convert = conversion == HDR_TARGET || conversion == PQ_SOURCE; + if (convert && premultiplied) { sampled.rgb = sampled.a > 0.0 ? sampled.rgb / sampled.a : vec3(0.0); } - if (convert_to_hdr) { + if (conversion == HDR_TARGET) { sampled.rgb = SdrToHdr10(sampled.rgb, (flags & 8u) != 0u); + } else if (conversion == PQ_SOURCE) { + sampled.rgb = Hdr10ToSdr(sampled.rgb, (flags & 8u) != 0u); } - if (!premultiplied || convert_to_hdr) { + if (!premultiplied || convert) { sampled.rgb *= sampled.a; } color = sampled; diff --git a/src/graphics/presentation/videoOut.cpp b/src/graphics/presentation/videoOut.cpp index 247df7a48..29360b385 100644 --- a/src/graphics/presentation/videoOut.cpp +++ b/src/graphics/presentation/videoOut.cpp @@ -1125,12 +1125,6 @@ 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, diff --git a/src/graphics/presentation/window/swapchain.cpp b/src/graphics/presentation/window/swapchain.cpp index 46eb79c88..2cbe89d15 100644 --- a/src/graphics/presentation/window/swapchain.cpp +++ b/src/graphics/presentation/window/swapchain.cpp @@ -290,8 +290,8 @@ public: private: vk::SurfaceFormatKHR SelectFormat(); void Destroy(); - void DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer, - const vk::Rect2D& rect); + void DrawLayer(vk::CommandBuffer command, const Presenter::Layer& layer, + const vk::Rect2D& rect); WindowContext& m_window; vk::SwapchainKHR m_handle = nullptr; @@ -653,8 +653,8 @@ static vk::Rect2D FitAspect(const vk::Extent3D& source, const vk::Extent2D& targ {w, h}}; } -void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer, - const vk::Rect2D& rect) { +void Swapchain::DrawLayer(vk::CommandBuffer command, const Presenter::Layer& layer, + const vk::Rect2D& rect) { auto device = m_window.graphic_ctx.device; if (m_overlay_pipeline == nullptr) { vk::SamplerCreateInfo sampler {}; @@ -756,9 +756,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 flags = (layer.premultiplied_alpha ? 1u : 0u) | - (m_hdr_active ? 2u : 0u) | (frame.hdr ? 4u : 0u) | - (frame.image.format == vk::Format::eR16G16B16A16Sfloat ? 8u : 0u); + // The main bus ignores 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) | + (layer.bus == 0 ? 16u : 0u); command.pushConstants(m_overlay_layout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(flags), &flags); command.bindPipeline(vk::PipelineBindPoint::eGraphics, m_overlay_pipeline); @@ -789,12 +791,18 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame* const Presenter::Layer& overlay, bool draw_system_overlay) { EXIT_IF(m_image_index >= m_images.size()); auto vk_command = command.Handle(); + const bool convert_source = source != nullptr && source->hdr && !m_hdr_active; + const bool blit_source = source != nullptr && !convert_source; const bool draw_overlay = overlay.frame != nullptr; - const bool draw_attachment = draw_overlay || draw_system_overlay; - if (source != nullptr) { + const bool draw_attachment = convert_source || draw_overlay || draw_system_overlay; + if (blit_source) { source->Transit(vk_command, vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead); } + if (convert_source) { + source->Transit(vk_command, vk::ImageLayout::eShaderReadOnlyOptimal, + vk::AccessFlagBits2::eShaderRead); + } if (draw_overlay) { overlay.frame->Transit(vk_command, vk::ImageLayout::eShaderReadOnlyOptimal, vk::AccessFlagBits2::eShaderRead); @@ -826,7 +834,7 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame* const vk::ClearColorValue black {}; vk_command.clearColorImage(m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal, &black, 1, &to_transfer.subresourceRange); - if (source != nullptr) { + if (blit_source) { vk::ImageMemoryBarrier after_clear = to_transfer; after_clear.srcAccessMask = vk::AccessFlagBits::eTransferWrite; after_clear.oldLayout = vk::ImageLayout::eTransferDstOptimal; @@ -836,7 +844,7 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame* &after_clear); } } - if (source != nullptr) { + if (blit_source) { vk::ImageBlit region {}; region.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; region.srcSubresource.mipLevel = 0; @@ -882,23 +890,31 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame* vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1, &to_present); if (draw_attachment) { + // Each pass blends over the color writes of the previous one. + to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; + to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal; + const auto attachment_barrier = [&] { + vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1, + &to_present); + }; + if (convert_source) { + DrawLayer(vk_command, {source, 0, false}, game_rect); + if (draw_overlay || draw_system_overlay) { + attachment_barrier(); + } + } if (draw_overlay) { - DrawOverlay(vk_command, overlay, game_rect); + DrawLayer(vk_command, overlay, game_rect); if (draw_system_overlay) { - to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal; - vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput, - vk::PipelineStageFlagBits::eColorAttachmentOutput, - vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, - nullptr, 1, &to_present); + attachment_barrier(); } } if (draw_system_overlay) { m_system_overlay->Record(vk_command, m_image_views[m_image_index]); } - to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; to_present.dstAccessMask = vk::AccessFlagBits::eMemoryRead; - to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal; to_present.newLayout = vk::ImageLayout::ePresentSrcKHR; vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eAllCommands, diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index c95d562cf..897a8a224 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -8,6 +8,8 @@ #include "common/logging/log.h" #include "common/subsystems.h" #include "common/threads.h" +#include "gpu_blit_shaders/gpu_blit_fs_triangle_spv.h" +#include "gpu_blit_shaders/gpu_video_out_overlay_spv.h" #include "gpu_test_shaders/gpu_test_ms_depth_spv.h" #include "graphics/guest_gpu/command_processor/commandProcessor.h" #include "graphics/guest_gpu/command_processor/pm4Dispatch.h" @@ -2213,6 +2215,160 @@ public: std::printf("[host] %-32s ok\n", name); } + // The video-out layer shader converts PQ and linear-nit main-bus frames for an SDR swapchain. + void CheckVideoOutHdr10ToSdr() { + constexpr const char *name = "VideoOutHdr10ToSdr"; + const auto vertex = CompileSPV(GPU_BLIT_FS_TRIANGLE_SPV, m_device); + const auto fragment = CompileSPV(GPU_VIDEO_OUT_OVERLAY_SPV, m_device); + const auto sampler = CreateSampler(name); + const vk::DescriptorSetLayoutBinding binding{0, vk::DescriptorType::eCombinedImageSampler, + 1, vk::ShaderStageFlagBits::eFragment}; + vk::DescriptorSetLayoutCreateInfo descriptors{}; + descriptors.flags = vk::DescriptorSetLayoutCreateFlagBits::ePushDescriptorKHR; + descriptors.bindingCount = 1; + descriptors.pBindings = &binding; + vk::DescriptorSetLayout descriptor_layout = nullptr; + RequireVk(name, "descriptors", + m_device.createDescriptorSetLayout(&descriptors, nullptr, &descriptor_layout), + "vkCreateDescriptorSetLayout"); + const vk::PushConstantRange push{vk::ShaderStageFlagBits::eFragment, 0, sizeof(u32)}; + vk::PipelineLayoutCreateInfo layout_info{}; + layout_info.setLayoutCount = 1; + layout_info.pSetLayouts = &descriptor_layout; + layout_info.pushConstantRangeCount = 1; + layout_info.pPushConstantRanges = &push; + vk::PipelineLayout layout = nullptr; + RequireVk(name, "layout", m_device.createPipelineLayout(&layout_info, nullptr, &layout), + "vkCreatePipelineLayout"); + std::array stages{}; + stages[0].stage = vk::ShaderStageFlagBits::eVertex; + stages[0].module = vertex; + stages[0].pName = "main"; + stages[1].stage = vk::ShaderStageFlagBits::eFragment; + stages[1].module = fragment; + stages[1].pName = "main"; + vk::PipelineVertexInputStateCreateInfo vertex_input{}; + vk::PipelineInputAssemblyStateCreateInfo assembly{}; + assembly.topology = vk::PrimitiveTopology::eTriangleList; + const vk::Viewport viewport{0.0f, 0.0f, 4.0f, 1.0f, 0.0f, 1.0f}; + const vk::Rect2D scissor{{0, 0}, {4, 1}}; + vk::PipelineViewportStateCreateInfo viewport_state{}; + viewport_state.viewportCount = 1; + viewport_state.pViewports = &viewport; + viewport_state.scissorCount = 1; + viewport_state.pScissors = &scissor; + vk::PipelineRasterizationStateCreateInfo rasterization{}; + rasterization.lineWidth = 1.0f; + vk::PipelineMultisampleStateCreateInfo multisample{}; + vk::PipelineColorBlendAttachmentState attachment{}; + attachment.colorWriteMask = + vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; + vk::PipelineColorBlendStateCreateInfo blend{}; + blend.attachmentCount = 1; + blend.pAttachments = &attachment; + const auto target_format = vk::Format::eR8G8B8A8Unorm; + vk::PipelineRenderingCreateInfo rendering_format{}; + rendering_format.colorAttachmentCount = 1; + rendering_format.pColorAttachmentFormats = &target_format; + vk::GraphicsPipelineCreateInfo create{}; + create.pNext = &rendering_format; + create.stageCount = static_cast(stages.size()); + create.pStages = stages.data(); + create.pVertexInputState = &vertex_input; + create.pInputAssemblyState = &assembly; + create.pViewportState = &viewport_state; + create.pRasterizationState = &rasterization; + create.pMultisampleState = &multisample; + create.pColorBlendState = &blend; + create.layout = layout; + vk::Pipeline pipeline = nullptr; + RequireVk(name, "pipeline", + m_device.createGraphicsPipelines(nullptr, 1, &create, nullptr, &pipeline), + "vkCreateGraphicsPipelines"); + + auto target = CreateImage2D(name, 4, 1, target_format, + vk::ImageUsageFlagBits::eColorAttachment, {}, 1, + vk::ImageLayout::eTransferSrcOptimal); + const auto render = [&](Image &source, u32 flags) { + vk::CommandBuffer cmd = BeginCommands(name, "draw"); + AddImageBarrier(cmd, target.image, target.layout, + vk::ImageLayout::eColorAttachmentOptimal, + vk::PipelineStageFlagBits::eTransfer, + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::AccessFlagBits::eTransferRead, + vk::AccessFlagBits::eColorAttachmentWrite); + target.layout = vk::ImageLayout::eColorAttachmentOptimal; + vk::RenderingAttachmentInfo color{}; + color.imageView = target.view; + color.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; + color.loadOp = vk::AttachmentLoadOp::eClear; + color.storeOp = vk::AttachmentStoreOp::eStore; + vk::RenderingInfo rendering{}; + rendering.renderArea.extent = scissor.extent; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &color; + const vk::DescriptorImageInfo image{sampler, source.view, + vk::ImageLayout::eShaderReadOnlyOptimal}; + vk::WriteDescriptorSet write{}; + write.descriptorCount = 1; + write.descriptorType = vk::DescriptorType::eCombinedImageSampler; + write.pImageInfo = ℑ + cmd.beginRendering(rendering); + cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); + cmd.pushDescriptorSetKHR(vk::PipelineBindPoint::eGraphics, layout, 0, 1, &write); + cmd.pushConstants(layout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(flags), &flags); + cmd.draw(3, 1, 0, 0); + cmd.endRendering(); + EndSubmitAndFree(name, "draw", cmd); + return ReadImage(name, &target); + }; + const auto near = [](const std::vector &actual, const std::vector &expected) { + for (size_t i = 0; i < expected.size(); i++) { + for (u32 shift = 0; shift < 32; shift += 8) { + if (std::abs(static_cast((actual[i] >> shift) & 0xffu) - + static_cast((expected[i] >> shift) & 0xffu)) > 1) { + return false; + } + } + } + return true; + }; + + // Black, 100-nit and 250-nit gray, then a color; every source alpha is zero. Flags: PQ + // source (4), linear source (8), opaque main bus (16). + const auto pq = [](u32 r, u32 g, u32 b) { return r | (g << 10u) | (b << 20u); }; + auto pq_source = CreateImage2D( + name, 4, 1, vk::Format::eA2B10G10R10UnormPack32, vk::ImageUsageFlagBits::eSampled, + {pq(0, 0, 0), pq(520, 520, 520), pq(616, 616, 616), pq(560, 500, 440)}, 1, + vk::ImageLayout::eShaderReadOnlyOptimal); + Require(name, "PQ source", + near(render(pq_source, 4u | 16u), + {0xff000000u, 0xffaaaaaau, 0xfff0f0f0u, 0xff6e95e3u}), + "PQ pixels were not converted to opaque sRGB"); + // Half-float nits: 0, 100, 250 and (150, 90, 40). + auto nit_source = CreateImage2D( + name, 4, 1, vk::Format::eR16G16B16A16Sfloat, vk::ImageUsageFlagBits::eSampled, + {0, 0, 0x56405640u, 0x5640u, 0x5bd05bd0u, 0x5bd0u, 0x55a058b0u, 0x5100u}, 2, + vk::ImageLayout::eShaderReadOnlyOptimal); + Require(name, "linear source", + near(render(nit_source, 4u | 8u | 16u), + {0xff000000u, 0xffaaaaaau, 0xfff0f0f0u, 0xff669ce4u}), + "linear-nit pixels were not converted to opaque sRGB"); + + DestroyImage(&nit_source); + DestroyImage(&pq_source); + DestroyImage(&target); + m_device.destroyPipeline(pipeline, nullptr); + m_device.destroyPipelineLayout(layout, nullptr); + m_device.destroyDescriptorSetLayout(descriptor_layout, nullptr); + m_device.destroySampler(sampler, nullptr); + m_device.destroyShaderModule(fragment, nullptr); + m_device.destroyShaderModule(vertex, nullptr); + std::printf("[host] %-32s ok\n", name); + } + void CheckDescriptorHeapLargeSet() { EnsureRuntimeContext(); std::array bindings{}; @@ -54104,6 +54260,11 @@ int main(int argc, char **argv) { CheckPm4Flip(); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--video-out-only") == 0) { + VulkanHarness vulkan; + vulkan.CheckVideoOutHdr10ToSdr(); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--gpu-command-lane-only") == 0) { VulkanHarness vulkan; vulkan.CheckGpuCommandLane(); @@ -54641,6 +54802,7 @@ int main(int argc, char **argv) { CheckPs5GameExampleImageClearRuntimeShape(); vulkan.CheckSchedulerTimeline(); vulkan.CheckHostImageAllocation(); + vulkan.CheckVideoOutHdr10ToSdr(); vulkan.CheckDescriptorHeapLargeSet(); vulkan.CheckGpuMappedRangeLifecycle(); vulkan.CheckStreamBufferRing();