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:
@@ -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) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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);
|
||||
}
|
||||
Binary file not shown.
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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->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"
|
||||
|
||||
Reference in New Issue
Block a user