From fffe4a47887393fb96ae81704a643c167c675489 Mon Sep 17 00:00:00 2001 From: The Roofer Dev Date: Wed, 17 Jun 2026 23:11:19 -0400 Subject: [PATCH] Native HDR output: settings toggle + paper white / peak / intensity sliders Adds optional native HDR (scRGB) presentation with an SDR->HDR highlight tone map, exposed as real graphics settings: - 'Enable HDR' checkbox, off by default. Only engages when the display/OS advertises the scRGB (extended sRGB linear) HDR colorspace, so SDR output is left completely unchanged. - Paper white, peak brightness and highlight intensity sliders, read by the HDR color-blit and FSR sharpening shaders through uniforms. - Blacks are preserved by construction (highlight-only expansion). - Defaults calibrated on a 4K OLED: 200 nits paper white, 1100 nits peak, intensity 80. Vulkan only. Co-Authored-By: The Roofer Dev --- src/Ryujinx.Graphics.GAL/IWindow.cs | 1 + .../Multithreading/ThreadedWindow.cs | 2 + src/Ryujinx.Graphics.OpenGL/Window.cs | 5 ++ .../Effects/AreaScalingFilter.cs | 6 +- .../Effects/FsrScalingFilter.cs | 29 ++++++-- .../Effects/IScalingFilter.cs | 6 +- .../Effects/Shaders/FsrSharpening.glsl | 32 ++++++++ .../Effects/Shaders/FsrSharpeningHdr.spv | Bin 0 -> 22040 bytes src/Ryujinx.Graphics.Vulkan/HelperShader.cs | 39 +++++++++- .../Ryujinx.Graphics.Vulkan.csproj | 2 + .../ColorBlitHdrFragmentShaderSource.frag | 38 ++++++++++ .../SpirvBinaries/ColorBlitHdrFragment.spv | Bin 0 -> 2584 bytes src/Ryujinx.Graphics.Vulkan/Window.cs | 50 ++++++++++++- src/Ryujinx.Graphics.Vulkan/WindowBase.cs | 1 + src/Ryujinx/Systems/AppHost.cs | 33 +++++++++ .../Configuration/ConfigurationFileFormat.cs | 22 +++++- .../ConfigurationState.Migration.cs | 13 +++- .../Configuration/ConfigurationState.Model.cs | 29 ++++++++ .../Configuration/ConfigurationState.cs | 8 ++ .../UI/ViewModels/SettingsViewModel.cs | 48 ++++++++++++ .../Views/Settings/SettingsGraphicsView.axaml | 70 ++++++++++++++++++ 21 files changed, 418 insertions(+), 16 deletions(-) create mode 100644 src/Ryujinx.Graphics.Vulkan/Effects/Shaders/FsrSharpeningHdr.spv create mode 100644 src/Ryujinx.Graphics.Vulkan/Shaders/ColorBlitHdrFragmentShaderSource.frag create mode 100644 src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/ColorBlitHdrFragment.spv diff --git a/src/Ryujinx.Graphics.GAL/IWindow.cs b/src/Ryujinx.Graphics.GAL/IWindow.cs index 48144f0b0..68e847f82 100644 --- a/src/Ryujinx.Graphics.GAL/IWindow.cs +++ b/src/Ryujinx.Graphics.GAL/IWindow.cs @@ -15,5 +15,6 @@ namespace Ryujinx.Graphics.GAL void SetScalingFilter(ScalingFilter type); void SetScalingFilterLevel(float level); void SetColorSpacePassthrough(bool colorSpacePassThroughEnabled); + void SetHdrMode(bool enabled, float paperWhite, float peak, float curve); } } diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs index 78c39c493..05d9cc4a8 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs @@ -41,5 +41,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading public void SetScalingFilterLevel(float level) { } public void SetColorSpacePassthrough(bool colorSpacePassthroughEnabled) { } + + public void SetHdrMode(bool enabled, float paperWhite, float peak, float curve) { } } } diff --git a/src/Ryujinx.Graphics.OpenGL/Window.cs b/src/Ryujinx.Graphics.OpenGL/Window.cs index fb3208f00..b1b68d809 100644 --- a/src/Ryujinx.Graphics.OpenGL/Window.cs +++ b/src/Ryujinx.Graphics.OpenGL/Window.cs @@ -423,5 +423,10 @@ namespace Ryujinx.Graphics.OpenGL _scalingFilterLevel = level; _updateScalingFilter = true; } + + public void SetHdrMode(bool enabled, float paperWhite, float peak, float curve) + { + // Native HDR output is only implemented on the Vulkan backend. + } } } diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/AreaScalingFilter.cs b/src/Ryujinx.Graphics.Vulkan/Effects/AreaScalingFilter.cs index b8992c652..bfd21bb35 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/AreaScalingFilter.cs +++ b/src/Ryujinx.Graphics.Vulkan/Effects/AreaScalingFilter.cs @@ -63,7 +63,11 @@ namespace Ryujinx.Graphics.Vulkan.Effects int width, int height, Extent2D source, - Extent2D destination) + Extent2D destination, + bool hdr = false, + float paperWhite = 2.5f, + float peak = 12.5f, + float curve = 4.0f) { _pipeline.SetCommandBuffer(cbs); _pipeline.SetProgram(_scalingProgram); diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs b/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs index 64b6c3b37..104871fcb 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs +++ b/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs @@ -17,6 +17,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects private ISampler _sampler; private ShaderCollection _scalingProgram; private ShaderCollection _sharpeningProgram; + private ShaderCollection _sharpeningProgramHdr; private float _sharpeningLevel = 1; private Device _device; private TextureView _intermediaryTexture; @@ -43,6 +44,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects _pipeline.Dispose(); _scalingProgram.Dispose(); _sharpeningProgram.Dispose(); + _sharpeningProgramHdr.Dispose(); _sampler.Dispose(); _intermediaryTexture?.Dispose(); } @@ -77,6 +79,12 @@ namespace Ryujinx.Graphics.Vulkan.Effects _sharpeningProgram = _renderer.CreateProgramWithMinimalLayout([ new ShaderSource(sharpeningShader, ShaderStage.Compute, TargetLanguage.Spirv) ], sharpeningResourceLayout); + + byte[] sharpeningShaderHdr = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/FsrSharpeningHdr.spv"); + + _sharpeningProgramHdr = _renderer.CreateProgramWithMinimalLayout([ + new ShaderSource(sharpeningShaderHdr, ShaderStage.Compute, TargetLanguage.Spirv) + ], sharpeningResourceLayout); } public void Run( @@ -87,7 +95,11 @@ namespace Ryujinx.Graphics.Vulkan.Effects int width, int height, Extent2D source, - Extent2D destination) + Extent2D destination, + bool hdr = false, + float paperWhite = 2.5f, + float peak = 12.5f, + float curve = 4.0f) { if (_intermediaryTexture == null || _intermediaryTexture.Info.Width != width @@ -143,9 +155,16 @@ namespace Ryujinx.Graphics.Vulkan.Effects using ScopedTemporaryBuffer buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize); buffer.Holder.SetDataUnchecked(buffer.Offset, dimensionsBuffer); - ReadOnlySpan sharpeningBufferData = [1.5f - (Level * 0.01f * 1.5f)]; - using ScopedTemporaryBuffer sharpeningBuffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, sizeof(float)); - sharpeningBuffer.Holder.SetDataUnchecked(sharpeningBuffer.Offset, sharpeningBufferData); + // Non-HDR: [sharpening]. HDR: [sharpening, paperWhite, peak, curve] (the HDR sharpening + // shader reads the extra floats from the same uniform block to tone-map to scRGB). + int sharpeningCount = hdr ? 4 : 1; + Span sharpeningBufferData = stackalloc float[4]; + sharpeningBufferData[0] = 1.5f - (Level * 0.01f * 1.5f); + sharpeningBufferData[1] = paperWhite; + sharpeningBufferData[2] = peak; + sharpeningBufferData[3] = curve; + using ScopedTemporaryBuffer sharpeningBuffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, sharpeningCount * sizeof(float)); + sharpeningBuffer.Holder.SetDataUnchecked(sharpeningBuffer.Offset, sharpeningBufferData[..sharpeningCount]); int threadGroupWorkRegionDim = 16; int dispatchX = (width + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim; @@ -157,7 +176,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects _pipeline.ComputeBarrier(); // Sharpening pass - _pipeline.SetProgram(_sharpeningProgram); + _pipeline.SetProgram(hdr ? _sharpeningProgramHdr : _sharpeningProgram); _pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, _intermediaryTexture, _sampler); _pipeline.SetUniformBuffers([new BufferAssignment(4, sharpeningBuffer.Range)]); _pipeline.SetImage(0, destinationTexture); diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/IScalingFilter.cs b/src/Ryujinx.Graphics.Vulkan/Effects/IScalingFilter.cs index 50fc3710c..898908a7f 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/IScalingFilter.cs +++ b/src/Ryujinx.Graphics.Vulkan/Effects/IScalingFilter.cs @@ -15,6 +15,10 @@ namespace Ryujinx.Graphics.Vulkan.Effects int width, int height, Extent2D source, - Extent2D destination); + Extent2D destination, + bool hdr = false, + float paperWhite = 2.5f, + float peak = 12.5f, + float curve = 4.0f); } } diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/FsrSharpening.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/FsrSharpening.glsl index 785bc0c83..72239cef1 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/FsrSharpening.glsl +++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/FsrSharpening.glsl @@ -1,7 +1,11 @@ // Sharpening #version 430 core layout (local_size_x = 64) in; +#ifdef HDR_OUTPUT +layout( rgba16f, binding = 0, set = 3) uniform image2D imgOutput; +#else layout( rgba8, binding = 0, set = 3) uniform image2D imgOutput; +#endif layout( binding = 2 ) uniform invResolution { vec2 invResolution_data; @@ -14,6 +18,11 @@ layout( binding = 1, set = 2) uniform sampler2D source; layout( binding = 4 ) uniform sharpening { float sharpening_data; +#ifdef HDR_OUTPUT + float paperWhite_data; + float peak_data; + float curve_data; +#endif }; #define A_GPU 1 @@ -3883,10 +3892,33 @@ AF1 sharpness){ #endif +#ifdef HDR_OUTPUT +// sRGB encoded -> linear, for the scRGB (extended sRGB linear) HDR swapchain. +vec3 srgbToLinear(vec3 c) +{ + return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(vec3(0.04045), c)); +} +// First-pass SDR->HDR: scale to paper white + expand ONLY the highlights (blacks stay black). +// scRGB: 1.0 == 80 nits, paperWhite 2.0 ~= 160 nits, peak 8.0 ~= 640 nits. +vec3 sdrToHdr(vec3 srgb) +{ + vec3 lin = srgbToLinear(srgb); + float paperWhite = paperWhite_data; + float peak = peak_data; + float curve = curve_data; + float luma = dot(lin, vec3(0.2126, 0.7152, 0.0722)); + float boost = mix(1.0, peak / paperWhite, pow(clamp(luma, 0.0, 1.0), curve)); + return lin * paperWhite * boost; +} +#endif + void CurrFilter(AU2 pos) { AF3 c; FsrRcasF(c.r, c.g, c.b, pos, con0); +#ifdef HDR_OUTPUT + c = sdrToHdr(c); +#endif imageStore(imgOutput, ASU2(pos), AF4(c, 1)); } diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/FsrSharpeningHdr.spv b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/FsrSharpeningHdr.spv new file mode 100644 index 0000000000000000000000000000000000000000..23d9b8f995d96d5b712ef4a85ff7bec3a43601c1 GIT binary patch literal 22040 zcmZvk2bfjW)rJqu44`1aE{M*EA_$@&DyS$Rf+$VIUT}Z`MqmbqBAOUcgC&VQc8x7D zQKK=om|}~X*b+;O#@LN5(Zm)@tpEGod)BaLKmV2e?C)FOT5GSp_c`~Td)Zjixo_8^ z=u~tsx)m)oMdj7A=!~Vn)}_*qpSa(|!Hbv79z1M^?Q~ePs5p$znQqm!da4@y(Nx z1lF(-_K4pY+}yIb65k8FD!yycVEm-U<=c-N;tZ;Pu$g01`sF3wjQ-dX*V7+Y!Ci}f z^rI&=xBP$cebFL*3vk4539iQ16$6Z)HfvFJ`0BX*+vDnrLF)T8E=l!)?RwW@TlJ}J z%O^F>PO?<<3`UE67^dH}g^PwSA70rB=eK>k-nG~jJZ^E@v{{XdC$=`u9@l?a^U&B& z`$e=muDda-{3f(4TDoLhvTEks9lfpMTT{7?=9YLg{mcsPT1=oX$J(Q{rT?;}!(8`j z&EwH#wYD%;U9lH<@w~>iMJ-K>-S9fDtZA!alcAR_9lDEY^r^N<2Pme=8WHx@mn)~d&cj`_?;QQ7rwZ4Y1^zO*Qd64 z3_fo^H@vzR OA1#rysRq(1cswrNF&!(BQyc?tDt(Xbpw$+~ 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.Offset, region); - _pipeline.SetUniformBuffers([new BufferAssignment(1, buffer.Range)]); + // Reserved at method scope (must stay alive until the draw). Only filled/bound for HDR; + // the HDR blit program's layout has a fragment uniform buffer at binding 2. + using ScopedTemporaryBuffer hdrBuffer = gd.BufferManager.ReserveOrCreate(gd, cbs, 16); + + if (hdr) + { + Span hdrParams = stackalloc float[4]; + hdrParams[0] = paperWhite; + hdrParams[1] = peak; + hdrParams[2] = curve; + hdrBuffer.Holder.SetDataUnchecked(hdrBuffer.Offset, hdrParams); + + _pipeline.SetUniformBuffers([new BufferAssignment(1, buffer.Range), new BufferAssignment(2, hdrBuffer.Range)]); + } + else + { + _pipeline.SetUniformBuffers([new BufferAssignment(1, buffer.Range)]); + } Span viewports = stackalloc Viewport[1]; @@ -415,7 +447,7 @@ namespace Ryujinx.Graphics.Vulkan } else if (clearAlpha) { - _pipeline.SetProgram(_programColorBlitClearAlpha); + _pipeline.SetProgram(hdr ? _programColorBlitHdr : _programColorBlitClearAlpha); } else { @@ -1672,6 +1704,7 @@ namespace Ryujinx.Graphics.Vulkan { _programColorBlitClearAlpha.Dispose(); _programColorBlit.Dispose(); + _programColorBlitHdr.Dispose(); _programColorBlitMs.Dispose(); _programColorClearF.Dispose(); _programColorClearSI.Dispose(); diff --git a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj index 897bf8d09..e59823648 100644 --- a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj +++ b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj @@ -18,6 +18,7 @@ + @@ -25,6 +26,7 @@ + diff --git a/src/Ryujinx.Graphics.Vulkan/Shaders/ColorBlitHdrFragmentShaderSource.frag b/src/Ryujinx.Graphics.Vulkan/Shaders/ColorBlitHdrFragmentShaderSource.frag new file mode 100644 index 000000000..c5c5c5f32 --- /dev/null +++ b/src/Ryujinx.Graphics.Vulkan/Shaders/ColorBlitHdrFragmentShaderSource.frag @@ -0,0 +1,38 @@ +#version 450 core + +layout (binding = 0, set = 2) uniform sampler2D tex; + +layout (std140, binding = 2) uniform hdr_params +{ + // x = paper white (scRGB multiplier), y = peak (scRGB multiplier), z = highlight curve exponent + vec4 hdr_params_data; +}; + +layout (location = 0) in vec2 tex_coord; +layout (location = 0) out vec4 colour; + +// sRGB encoded -> linear, for the scRGB (extended sRGB linear) HDR swapchain. +vec3 srgbToLinear(vec3 c) +{ + return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(vec3(0.04045), c)); +} + +// First-pass SDR->HDR "inverse tone map": scale to paper white, then expand ONLY the +// highlights toward the peak. Blacks stay black (no constant lift) to avoid a washed look. +// scRGB: 1.0 == 80 nits, so paperWhite 2.0 ~= 160 nits, peak 8.0 ~= 640 nits. +vec3 sdrToHdr(vec3 srgb) +{ + vec3 lin = srgbToLinear(srgb); + float paperWhite = hdr_params_data.x; + float peak = hdr_params_data.y; + float curve = hdr_params_data.z; + float luma = dot(lin, vec3(0.2126, 0.7152, 0.0722)); + float boost = mix(1.0, peak / paperWhite, pow(clamp(luma, 0.0, 1.0), curve)); + return lin * paperWhite * boost; +} + +void main() +{ + vec3 c = texture(tex, tex_coord).rgb; + colour = vec4(sdrToHdr(c), 1.0f); +} diff --git a/src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/ColorBlitHdrFragment.spv b/src/Ryujinx.Graphics.Vulkan/Shaders/SpirvBinaries/ColorBlitHdrFragment.spv new file mode 100644 index 0000000000000000000000000000000000000000..4298f31109474fd3a9f8f1e0cf11f7790bf44e20 GIT binary patch literal 2584 zcmZ9NO>8B6Rk6|klx1V`A*SlF8UcEWjn67tl_NEuL6VH^# z3^A`$j%%Gl-^`lXM(!)8vpL>eY0j^%wN~Vp(>ZLb_2y#E+e+-bC%0klurJoLR+i@< z(ZMkPCFaeQ>~Y|$D9hW;r=-sPLR(G6iPpL+{bq^wP0-xycyS{#hezE@G zd$z|Bhkok`d>EFz!Thw_F-Ot6u~m?M_}&GvH>d*M$E-~m1iw(|+K0h%4pg#^#K|9&=94c?nzZ6Vfo1{^h8|1j8^ zyuZhberuEaHnaP;?_&kF-`b9V-(jvGU5GV^^9RMO|2^gk;?OUCKS%BS=h4mO`r#i% zk9_jS5IM2O3G_dIUlp3=8FaW`|E(f}v+ z8M-xDN9PE9j6N;kkY}GhFG$+#llXpNv&Ubeo6mj7|BBgM+U1MAhuZY}cDzrXgMP91 zQrjiofq6VHhqiyPl$qsv&wPKLabVw+xQgEzzd)={Yz+?(*PkbgYi5X?2gq$Q8oPvO zR~%i4b&OMi>*+V2+=qxZeXcFOLT-JJ5Wjia?_jeptB7_-*w)asxlY)=E^MwlOEznL zj2JIp{}kf7_DkQtWVnpjH`nh)9KOp@c=pThu|4c#jy)VK?6Xv8+{=jXHspsQ-(hTW z^+)V0=;n;rqv*czknf7ver$5}2X_G7_a74LyoOD_{yr+QrZ*7#qfha>5H(cLts%IB z=yH9Zeitf8Uvai4(DnJY-H&sb@tD*b9DC4f0g|^z#MmO2;CY3pX$J8(9iC| z{&Rw_|LQJ0z6;+daP0RjuyuI$t|g8;cN*Pu*RJ2%v`uoheuFL`?%m$_eKKb^^F>6T zG1e=Nn9Jx9Gm0KDSJ3qtcgq~EFW=fdL;dFMVV*_YYpinz-P(UF-pgHdIogAB58XV$nM0SS-TN!{{{9c7 C_Nfp6 literal 0 HcmV?d00001 diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs index 8b2a53fc0..5889fa85f 100644 --- a/src/Ryujinx.Graphics.Vulkan/Window.cs +++ b/src/Ryujinx.Graphics.Vulkan/Window.cs @@ -42,6 +42,10 @@ namespace Ryujinx.Graphics.Vulkan private bool _updateScalingFilter; private ScalingFilter _currentScalingFilter; private bool _colorSpacePassthroughEnabled; + private bool _hdrEnabled; + private float _hdrPaperWhite = 200f; + private float _hdrPeak = 1100f; + private float _hdrCurve = 2.8f; public unsafe Window(VulkanRenderer gd, SurfaceKHR surface, PhysicalDevice physicalDevice, Device device) { @@ -117,7 +121,7 @@ namespace Ryujinx.Graphics.Vulkan imageCount = capabilities.MaxImageCount; } - SurfaceFormatKHR surfaceFormat = ChooseSwapSurfaceFormat(surfaceFormats, _colorSpacePassthroughEnabled); + SurfaceFormatKHR surfaceFormat = ChooseSwapSurfaceFormat(surfaceFormats, _colorSpacePassthroughEnabled, _hdrEnabled); Extent2D extent = ChooseSwapExtent(capabilities); @@ -226,8 +230,24 @@ namespace Ryujinx.Graphics.Vulkan return new TextureView(_gd, _device, new DisposableImageView(_gd.Api, _device, imageView), info, format); } - private static SurfaceFormatKHR ChooseSwapSurfaceFormat(SurfaceFormatKHR[] availableFormats, bool colorSpacePassthroughEnabled) + private static SurfaceFormatKHR ChooseSwapSurfaceFormat(SurfaceFormatKHR[] availableFormats, bool colorSpacePassthroughEnabled, bool hdrEnabled) { + // Native HDR output: when HDR is enabled and the surface advertises the scRGB + // (extended sRGB linear) HDR colorspace, select the 16-bit float swapchain. That + // colorspace is only enumerated on a genuinely HDR-capable display/OS, so on an SDR + // display this branch never triggers and the normal SDR format is used unchanged. + if (hdrEnabled) + { + foreach (SurfaceFormatKHR hdrFormat in availableFormats) + { + if (hdrFormat.Format == VkFormat.R16G16B16A16Sfloat && + hdrFormat.ColorSpace == ColorSpaceKHR.ColorSpaceExtendedSrgbLinearExt) + { + return hdrFormat; + } + } + } + if (availableFormats.Length == 1 && availableFormats[0].Format == VkFormat.Undefined) { return new SurfaceFormatKHR(VkFormat.B8G8R8A8Unorm, ColorSpaceKHR.PaceSrgbNonlinearKhr); @@ -438,7 +458,11 @@ namespace Ryujinx.Graphics.Vulkan _width, _height, new Extents2D(srcX0, srcY0, srcX1, srcY1), - new Extents2D(dstX0, dstY0, dstX1, dstY1) + new Extents2D(dstX0, dstY0, dstX1, dstY1), + _format == VkFormat.R16G16B16A16Sfloat, + _hdrPaperWhite / 80f, + _hdrPeak / 80f, + _hdrCurve ); } else @@ -451,7 +475,11 @@ namespace Ryujinx.Graphics.Vulkan new Extents2D(srcX0, srcY0, srcX1, srcY1), new Extents2D(dstX0, dstY1, dstX1, dstY0), _isLinear, - true); + true, + _format == VkFormat.R16G16B16A16Sfloat, + _hdrPaperWhite / 80f, + _hdrPeak / 80f, + _hdrCurve); } Transition( @@ -653,6 +681,20 @@ namespace Ryujinx.Graphics.Vulkan _swapchainIsDirty = true; } + public override void SetHdrMode(bool enabled, float paperWhite, float peak, float curve) + { + _hdrPaperWhite = paperWhite; + _hdrPeak = peak; + _hdrCurve = curve; + + if (_hdrEnabled != enabled) + { + _hdrEnabled = enabled; + // The swapchain surface format (SDR vs scRGB float) changes, so recreate it. + _swapchainIsDirty = true; + } + } + protected virtual void Dispose(bool disposing) { if (disposing) diff --git a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs index 34ac63a83..0e330cc61 100644 --- a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs @@ -16,5 +16,6 @@ namespace Ryujinx.Graphics.Vulkan public abstract void SetScalingFilter(ScalingFilter scalerType); public abstract void SetScalingFilterLevel(float scale); public abstract void SetColorSpacePassthrough(bool colorSpacePassthroughEnabled); + public abstract void SetHdrMode(bool enabled, float paperWhite, float peak, float curve); } } diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 23f6d6b47..21ecd7f26 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -206,6 +206,10 @@ namespace Ryujinx.Ava.Systems ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter; ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough; + ConfigurationState.Instance.Graphics.EnableHdr.Event += UpdateHdrEnabled; + ConfigurationState.Instance.Graphics.HdrPaperWhite.Event += UpdateHdrLevel; + ConfigurationState.Instance.Graphics.HdrPeakBrightness.Event += UpdateHdrLevel; + ConfigurationState.Instance.Graphics.HdrIntensity.Event += UpdateHdrLevel; ConfigurationState.Instance.Graphics.VSyncMode.Event += UpdateVSyncMode; ConfigurationState.Instance.Graphics.CustomVSyncInterval.Event += UpdateCustomVSyncIntervalValue; ConfigurationState.Instance.Graphics.EnableCustomVSyncInterval.Event += UpdateCustomVSyncIntervalEnabled; @@ -305,6 +309,26 @@ namespace Ryujinx.Ava.Systems _renderer.Window?.SetColorSpacePassthrough(ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough); } + private void UpdateHdrEnabled(object sender, ReactiveEventArgs e) => ApplyHdrMode(); + + private void UpdateHdrLevel(object sender, ReactiveEventArgs e) => ApplyHdrMode(); + + private void ApplyHdrMode() + { + _renderer.Window?.SetHdrMode( + ConfigurationState.Instance.Graphics.EnableHdr, + ConfigurationState.Instance.Graphics.HdrPaperWhite, + ConfigurationState.Instance.Graphics.HdrPeakBrightness, + HdrIntensityToCurve(ConfigurationState.Instance.Graphics.HdrIntensity)); + } + + // Maps the user-facing HDR intensity (0-100, higher = more pop) to the highlight curve + // exponent: 0 -> 6.0 (only near-white pops), 50 -> 4.0 (default), 100 -> 2.0 (whole scene pops). + private static float HdrIntensityToCurve(int intensity) + { + return 6.0f - (Math.Clamp(intensity, 0, 100) * 0.04f); + } + public void UpdateVSyncMode(object sender, ReactiveEventArgs e) { if (Device != null) @@ -661,6 +685,10 @@ namespace Ryujinx.Ava.Systems ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel; ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAntiAliasing; ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event -= UpdateColorSpacePassthrough; + ConfigurationState.Instance.Graphics.EnableHdr.Event -= UpdateHdrEnabled; + ConfigurationState.Instance.Graphics.HdrPaperWhite.Event -= UpdateHdrLevel; + ConfigurationState.Instance.Graphics.HdrPeakBrightness.Event -= UpdateHdrLevel; + ConfigurationState.Instance.Graphics.HdrIntensity.Event -= UpdateHdrLevel; _topLevel.PointerMoved -= TopLevel_PointerEnteredOrMoved; _topLevel.PointerEntered -= TopLevel_PointerEnteredOrMoved; @@ -1116,6 +1144,11 @@ namespace Ryujinx.Ava.Systems _renderer?.Window?.SetScalingFilter(ConfigurationState.Instance.Graphics.ScalingFilter); _renderer?.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel); _renderer?.Window?.SetColorSpacePassthrough(ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough); + _renderer?.Window?.SetHdrMode( + ConfigurationState.Instance.Graphics.EnableHdr, + ConfigurationState.Instance.Graphics.HdrPaperWhite, + ConfigurationState.Instance.Graphics.HdrPeakBrightness, + HdrIntensityToCurve(ConfigurationState.Instance.Graphics.HdrIntensity)); Width = (int)RendererHost.Bounds.Width; Height = (int)RendererHost.Bounds.Height; diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs index 55b2d2cac..5e5c0721e 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs @@ -17,7 +17,7 @@ namespace Ryujinx.Ava.Systems.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 73; + public const int CurrentVersion = 75; /// /// Version of the configuration file format @@ -69,6 +69,26 @@ namespace Ryujinx.Ava.Systems.Configuration /// public int ScalingFilterLevel { get; set; } + /// + /// Enables or disables native HDR output (scRGB) with SDR->HDR tone mapping. + /// + public bool EnableHdr { get; set; } + + /// + /// HDR paper white (reference white) brightness, in nits. + /// + public int HdrPaperWhite { get; set; } + + /// + /// HDR peak (highlight) brightness target, in nits. + /// + public int HdrPeakBrightness { get; set; } + + /// + /// HDR highlight intensity (0-100): how far down the brightness range the highlight expansion reaches. + /// + public int HdrIntensity { get; set; } + /// /// Dumps shaders in this local directory /// diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs index 3fd132cff..e2cf1af19 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs @@ -85,6 +85,10 @@ namespace Ryujinx.Ava.Systems.Configuration Graphics.AntiAliasing.Value = cff.AntiAliasing; Graphics.ScalingFilter.Value = cff.ScalingFilter; Graphics.ScalingFilterLevel.Value = cff.ScalingFilterLevel; + Graphics.EnableHdr.Value = cff.EnableHdr; + Graphics.HdrPaperWhite.Value = cff.HdrPaperWhite; + Graphics.HdrPeakBrightness.Value = cff.HdrPeakBrightness; + Graphics.HdrIntensity.Value = cff.HdrIntensity; Graphics.VSyncMode.Value = cff.VSyncMode; Graphics.EnableCustomVSyncInterval.Value = cff.EnableCustomVSyncInterval; Graphics.CustomVSyncInterval.Value = cff.CustomVSyncInterval; @@ -539,7 +543,14 @@ namespace Ryujinx.Ava.Systems.Configuration if (cff.AudioBackend is AudioBackend.SDL2) cff.AudioBackend = AudioBackend.SDL3; }), - (72, static cff => cff.GCLowLatency = false) + (72, static cff => cff.GCLowLatency = false), + (74, static cff => + { + cff.EnableHdr = false; + cff.HdrPaperWhite = 200; + cff.HdrPeakBrightness = 1100; + }), + (75, static cff => cff.HdrIntensity = 80) ); } } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs index c66f74ed5..c4d6ce273 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs @@ -620,6 +620,27 @@ namespace Ryujinx.Ava.Systems.Configuration /// public ReactiveObject ScalingFilterLevel { get; private set; } + /// + /// Enables or disables native HDR output (scRGB) with SDR->HDR tone mapping. + /// + public ReactiveObject EnableHdr { get; private set; } + + /// + /// HDR paper white (reference white) brightness, in nits. + /// + public ReactiveObject HdrPaperWhite { get; private set; } + + /// + /// HDR peak (highlight) brightness target, in nits. + /// + public ReactiveObject HdrPeakBrightness { get; private set; } + + /// + /// HDR highlight intensity (0-100): how far down the brightness range the highlight + /// expansion reaches. Higher = more of the scene pops, lower = only near-white pops. + /// + public ReactiveObject HdrIntensity { get; private set; } + /// /// Preferred GPU /// @@ -662,6 +683,14 @@ namespace Ryujinx.Ava.Systems.Configuration ScalingFilter.LogChangesToValue(nameof(ScalingFilter)); ScalingFilterLevel = new ReactiveObject(); ScalingFilterLevel.LogChangesToValue(nameof(ScalingFilterLevel)); + EnableHdr = new ReactiveObject(); + EnableHdr.LogChangesToValue(nameof(EnableHdr)); + HdrPaperWhite = new ReactiveObject(); + HdrPaperWhite.LogChangesToValue(nameof(HdrPaperWhite)); + HdrPeakBrightness = new ReactiveObject(); + HdrPeakBrightness.LogChangesToValue(nameof(HdrPeakBrightness)); + HdrIntensity = new ReactiveObject(); + HdrIntensity.LogChangesToValue(nameof(HdrIntensity)); } } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs index acf8a6793..00e94ba34 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs @@ -40,6 +40,10 @@ namespace Ryujinx.Ava.Systems.Configuration AntiAliasing = Graphics.AntiAliasing, ScalingFilter = Graphics.ScalingFilter, ScalingFilterLevel = Graphics.ScalingFilterLevel, + EnableHdr = Graphics.EnableHdr, + HdrPaperWhite = Graphics.HdrPaperWhite, + HdrPeakBrightness = Graphics.HdrPeakBrightness, + HdrIntensity = Graphics.HdrIntensity, GraphicsShadersDumpPath = Graphics.ShadersDumpPath, LoggingEnableDebug = Logger.EnableDebug, LoggingEnableStub = Logger.EnableStub, @@ -208,6 +212,10 @@ namespace Ryujinx.Ava.Systems.Configuration Graphics.AntiAliasing.Value = AntiAliasing.None; Graphics.ScalingFilter.Value = ScalingFilter.Bilinear; Graphics.ScalingFilterLevel.Value = 80; + Graphics.EnableHdr.Value = false; + Graphics.HdrPaperWhite.Value = 200; + Graphics.HdrPeakBrightness.Value = 1100; + Graphics.HdrIntensity.Value = 80; System.EnablePtc.Value = true; System.EnableInternetAccess.Value = false; System.EnableFsIntegrityChecks.Value = true; diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 44caffb8d..0f6e473e3 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -287,6 +287,7 @@ namespace Ryujinx.Ava.UI.ViewModels public MemoryConfiguration DramSize { get; set; } public bool EnableShaderCache { get; set; } public bool EnableTextureRecompression { get; set; } + public bool EnableHdr { get; set; } public bool EnableMacroHLE { get; set; } public bool EnableColorSpacePassthrough { get; set; } public bool ColorSpacePassthroughAvailable => RunningPlatform.IsMacOS; @@ -352,6 +353,45 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public string HdrPaperWhiteText => $"{HdrPaperWhite} nits"; + + public int HdrPaperWhite + { + get; + set + { + field = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HdrPaperWhiteText)); + } + } + + public string HdrPeakBrightnessText => $"{HdrPeakBrightness} nits"; + + public int HdrPeakBrightness + { + get; + set + { + field = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HdrPeakBrightnessText)); + } + } + + public string HdrIntensityText => $"{HdrIntensity}"; + + public int HdrIntensity + { + get; + set + { + field = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HdrIntensityText)); + } + } + public int OpenglDebugLevel { get; set; } public int MemoryMode { get; set; } public int BaseStyleIndex { get; set; } @@ -725,6 +765,10 @@ namespace Ryujinx.Ava.UI.ViewModels // Physical devices are queried asynchronously hence the preferred index config value is loaded in LoadAvailableGpus(). EnableShaderCache = config.Graphics.EnableShaderCache; EnableTextureRecompression = config.Graphics.EnableTextureRecompression; + EnableHdr = config.Graphics.EnableHdr; + HdrPaperWhite = config.Graphics.HdrPaperWhite.Value; + HdrPeakBrightness = config.Graphics.HdrPeakBrightness.Value; + HdrIntensity = config.Graphics.HdrIntensity.Value; EnableMacroHLE = config.Graphics.EnableMacroHLE; EnableColorSpacePassthrough = config.Graphics.EnableColorSpacePassthrough; ResolutionScale = config.Graphics.ResScale == -1 ? 4 : config.Graphics.ResScale - 1; @@ -842,6 +886,10 @@ namespace Ryujinx.Ava.UI.ViewModels config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex); config.Graphics.EnableShaderCache.Value = EnableShaderCache; config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression; + config.Graphics.EnableHdr.Value = EnableHdr; + config.Graphics.HdrPaperWhite.Value = HdrPaperWhite; + config.Graphics.HdrPeakBrightness.Value = HdrPeakBrightness; + config.Graphics.HdrIntensity.Value = HdrIntensity; config.Graphics.EnableMacroHLE.Value = EnableMacroHLE; config.Graphics.EnableColorSpacePassthrough.Value = EnableColorSpacePassthrough; config.Graphics.ResScale.Value = ResolutionScale == 4 ? -1 : ResolutionScale + 1; diff --git a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml index 22c7acfa2..9e5b17fd7 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml @@ -95,6 +95,76 @@ ToolTip.Tip="{ext:Locale SettingsEnableColorSpacePassthroughTooltip}"> + + + + + + + + + + + + + + + + + +