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 <therooferdev@gmail.com>
This commit is contained in:
2026-06-17 23:11:19 -04:00
parent 3c9eb2edbf
commit fffe4a4788
21 changed files with 418 additions and 16 deletions
+1
View File
@@ -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);
}
}
@@ -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) { }
}
}
+5
View File
@@ -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.
}
}
}
@@ -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);
@@ -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<float> 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<float> 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);
@@ -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);
}
}
@@ -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));
}
+35 -2
View File
@@ -33,6 +33,7 @@ namespace Ryujinx.Graphics.Vulkan
private readonly ISampler _samplerLinear;
private readonly ISampler _samplerNearest;
private readonly ShaderCollection _programColorBlit;
private readonly ShaderCollection _programColorBlitHdr;
private readonly ShaderCollection _programColorBlitMs;
private readonly ShaderCollection _programColorBlitClearAlpha;
private readonly ShaderCollection _programColorClearF;
@@ -73,6 +74,16 @@ namespace Ryujinx.Graphics.Vulkan
new ShaderSource(ReadSpirv("ColorBlitFragment.spv"), ShaderStage.Fragment, TargetLanguage.Spirv)
], blitResourceLayout);
ResourceLayout blitHdrResourceLayout = new ResourceLayoutBuilder()
.Add(ResourceStages.Vertex, ResourceType.UniformBuffer, 1)
.Add(ResourceStages.Fragment, ResourceType.TextureAndSampler, 0)
.Add(ResourceStages.Fragment, ResourceType.UniformBuffer, 2).Build();
_programColorBlitHdr = gd.CreateProgramWithMinimalLayout([
new ShaderSource(ReadSpirv("ColorBlitVertex.spv"), ShaderStage.Vertex, TargetLanguage.Spirv),
new ShaderSource(ReadSpirv("ColorBlitHdrFragment.spv"), ShaderStage.Fragment, TargetLanguage.Spirv)
], blitHdrResourceLayout);
_programColorBlitMs = gd.CreateProgramWithMinimalLayout([
new ShaderSource(ReadSpirv("ColorBlitVertex.spv"), ShaderStage.Vertex, TargetLanguage.Spirv),
new ShaderSource(ReadSpirv("ColorBlitMsFragment.spv"), ShaderStage.Fragment, TargetLanguage.Spirv)
@@ -352,7 +363,11 @@ namespace Ryujinx.Graphics.Vulkan
Extents2D srcRegion,
Extents2D dstRegion,
bool linearFilter,
bool clearAlpha = false)
bool clearAlpha = false,
bool hdr = false,
float paperWhite = 2.5f,
float peak = 12.5f,
float curve = 4.0f)
{
_pipeline.SetCommandBuffer(cbs);
@@ -383,7 +398,24 @@ namespace Ryujinx.Graphics.Vulkan
buffer.Holder.SetDataUnchecked<float>(buffer.Offset, region);
// 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<float> hdrParams = stackalloc float[4];
hdrParams[0] = paperWhite;
hdrParams[1] = peak;
hdrParams[2] = curve;
hdrBuffer.Holder.SetDataUnchecked<float>(hdrBuffer.Offset, hdrParams);
_pipeline.SetUniformBuffers([new BufferAssignment(1, buffer.Range), new BufferAssignment(2, hdrBuffer.Range)]);
}
else
{
_pipeline.SetUniformBuffers([new BufferAssignment(1, buffer.Range)]);
}
Span<Viewport> 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();
@@ -18,6 +18,7 @@
<EmbeddedResource Include="Effects\Shaders\AreaScaling.spv" />
<EmbeddedResource Include="Effects\Shaders\FsrScaling.spv" />
<EmbeddedResource Include="Effects\Shaders\FsrSharpening.spv" />
<EmbeddedResource Include="Effects\Shaders\FsrSharpeningHdr.spv" />
<EmbeddedResource Include="Effects\Shaders\Fxaa.spv" />
<EmbeddedResource Include="Effects\Shaders\SmaaBlend.spv" />
<EmbeddedResource Include="Effects\Shaders\SmaaEdge.spv" />
@@ -25,6 +26,7 @@
<EmbeddedResource Include="Shaders\SpirvBinaries\ChangeBufferStride.spv" />
<EmbeddedResource Include="Shaders\SpirvBinaries\ColorBlitClearAlphaFragment.spv" />
<EmbeddedResource Include="Shaders\SpirvBinaries\ColorBlitFragment.spv" />
<EmbeddedResource Include="Shaders\SpirvBinaries\ColorBlitHdrFragment.spv" />
<EmbeddedResource Include="Shaders\SpirvBinaries\ColorBlitMsFragment.spv" />
<EmbeddedResource Include="Shaders\SpirvBinaries\ColorBlitVertex.spv" />
<EmbeddedResource Include="Shaders\SpirvBinaries\ColorClearFFragment.spv" />
@@ -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);
}
+46 -4
View File
@@ -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)
@@ -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);
}
}
+33
View File
@@ -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<bool> e) => ApplyHdrMode();
private void UpdateHdrLevel(object sender, ReactiveEventArgs<int> 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<VSyncMode> 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;
@@ -17,7 +17,7 @@ namespace Ryujinx.Ava.Systems.Configuration
/// <summary>
/// The current version of the file format
/// </summary>
public const int CurrentVersion = 73;
public const int CurrentVersion = 75;
/// <summary>
/// Version of the configuration file format
@@ -69,6 +69,26 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary>
public int ScalingFilterLevel { get; set; }
/// <summary>
/// Enables or disables native HDR output (scRGB) with SDR->HDR tone mapping.
/// </summary>
public bool EnableHdr { get; set; }
/// <summary>
/// HDR paper white (reference white) brightness, in nits.
/// </summary>
public int HdrPaperWhite { get; set; }
/// <summary>
/// HDR peak (highlight) brightness target, in nits.
/// </summary>
public int HdrPeakBrightness { get; set; }
/// <summary>
/// HDR highlight intensity (0-100): how far down the brightness range the highlight expansion reaches.
/// </summary>
public int HdrIntensity { get; set; }
/// <summary>
/// Dumps shaders in this local directory
/// </summary>
@@ -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)
);
}
}
@@ -620,6 +620,27 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary>
public ReactiveObject<int> ScalingFilterLevel { get; private set; }
/// <summary>
/// Enables or disables native HDR output (scRGB) with SDR->HDR tone mapping.
/// </summary>
public ReactiveObject<bool> EnableHdr { get; private set; }
/// <summary>
/// HDR paper white (reference white) brightness, in nits.
/// </summary>
public ReactiveObject<int> HdrPaperWhite { get; private set; }
/// <summary>
/// HDR peak (highlight) brightness target, in nits.
/// </summary>
public ReactiveObject<int> HdrPeakBrightness { get; private set; }
/// <summary>
/// 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.
/// </summary>
public ReactiveObject<int> HdrIntensity { get; private set; }
/// <summary>
/// Preferred GPU
/// </summary>
@@ -662,6 +683,14 @@ namespace Ryujinx.Ava.Systems.Configuration
ScalingFilter.LogChangesToValue(nameof(ScalingFilter));
ScalingFilterLevel = new ReactiveObject<int>();
ScalingFilterLevel.LogChangesToValue(nameof(ScalingFilterLevel));
EnableHdr = new ReactiveObject<bool>();
EnableHdr.LogChangesToValue(nameof(EnableHdr));
HdrPaperWhite = new ReactiveObject<int>();
HdrPaperWhite.LogChangesToValue(nameof(HdrPaperWhite));
HdrPeakBrightness = new ReactiveObject<int>();
HdrPeakBrightness.LogChangesToValue(nameof(HdrPeakBrightness));
HdrIntensity = new ReactiveObject<int>();
HdrIntensity.LogChangesToValue(nameof(HdrIntensity));
}
}
@@ -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;
@@ -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;
@@ -95,6 +95,76 @@
ToolTip.Tip="{ext:Locale SettingsEnableColorSpacePassthroughTooltip}">
<TextBlock Text="{ext:Locale SettingsEnableColorSpacePassthrough}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableHdr}"
ToolTip.Tip="Outputs native HDR (scRGB) with SDR-&gt;HDR tone mapping. Only engages on an HDR-capable display with HDR enabled in the OS; SDR displays are unaffected. Vulkan only.">
<TextBlock Text="Enable HDR (experimental)" />
</CheckBox>
<StackPanel Orientation="Horizontal" Margin="24,4,0,0" IsVisible="{Binding EnableHdr}">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="Brightness of reference white (UI, paper, diffuse surfaces), in nits. Higher = brighter overall image."
Text="HDR paper white"
Width="226" />
<controls:SliderScroll Value="{Binding HdrPaperWhite}"
MinWidth="150"
Margin="10,-3,0,0"
Height="32"
Padding="0,-5"
TickFrequency="5"
IsSnapToTickEnabled="True"
LargeChange="20"
SmallChange="5"
VerticalAlignment="Center"
Minimum="80"
Maximum="400" />
<TextBlock Margin="5,0"
Width="70"
VerticalAlignment="Center"
Text="{Binding HdrPaperWhiteText}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="24,4,0,0" IsVisible="{Binding EnableHdr}">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="Peak highlight brightness target, in nits. Only the brightest parts of the image are pushed toward this; blacks stay black. Set near your display's peak."
Text="HDR peak brightness"
Width="226" />
<controls:SliderScroll Value="{Binding HdrPeakBrightness}"
MinWidth="150"
Margin="10,-3,0,0"
Height="32"
Padding="0,-5"
TickFrequency="10"
IsSnapToTickEnabled="True"
LargeChange="100"
SmallChange="10"
VerticalAlignment="Center"
Minimum="200"
Maximum="2000" />
<TextBlock Margin="5,0"
Width="70"
VerticalAlignment="Center"
Text="{Binding HdrPeakBrightnessText}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="24,4,0,0" IsVisible="{Binding EnableHdr}">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="How far down the brightness range the highlight 'pop' reaches. Low = only near-white (UI, sun) pops; high = the whole bright scene (sky, sunlit surfaces) pops in gameplay. Blacks always stay black."
Text="HDR intensity"
Width="226" />
<controls:SliderScroll Value="{Binding HdrIntensity}"
MinWidth="150"
Margin="10,-3,0,0"
Height="32"
Padding="0,-5"
TickFrequency="1"
IsSnapToTickEnabled="True"
LargeChange="10"
SmallChange="1"
VerticalAlignment="Center"
Minimum="0"
Maximum="100" />
<TextBlock Margin="5,0"
Width="70"
VerticalAlignment="Center"
Text="{Binding HdrIntensityText}"/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"