So long as you have at least 1 playlist in the base “playlists” folder, you can just make copies of that playlist until there are enough to test. RetroArch just treats the filename as the playlist name, and nothing seems to be bothered by multiple playlists with identical contents.
Sorry if it’s been asked already but are you supposed to set up HDR peak brightness and paper white luminance in the shader parameters or in the Retroarch config and leave the other at default values? Or both? Or one or the other it doesn’t matter?
crt-sony-megatron-v2-default.slangp uses the RetroArch HDR menu Luminance settings.
crt-sony-megatron-v2-default-config.slangp uses it’s own internal Luminance shader parameters.
If you use crt-sony-megatron-v2-default-config.slangp, you can instead use the RetroArch HDR menu Luminance settings to control menu/UI brightness independently of Megatron’s brightness.
Thanks for your reply all the v2 presets fail to apply for some reason.
Yes thats correct - you need a nightly build until a stable release becomes available. When you use v2 you can can just use the main menu settings to change peak and paper white.
@MajorPainTheCactus @Azurfel It looks like this is now the HDR support thread. Can you answer some questions about the original RetroArch HDR implementation? I am trying to understand how it works and what the expectations are.
How is RA supposed to distinguish between HDR and SDR shaders? Am I right in assuming that it applies it’s SDR inverse tone map to all shaders regardless of if they are ‘HDR’ shaders or not?
What is the mathematic relationship of the HDR settings? I understand that the old curve has a knee. I would like to have the function documented for posterity.
How do HDR shaders get around the inverse tone mapper? I know they must be because they have independent white level settings. I am working with nits directly and apply the PQ function at the end, but without knowing what RA is doing on the shader output, I can’t know if it’s going to scale well under different conditions. In order to get reasonable color rendition, I need to set paper white to 100, peak to 1000, and contrast to 0. However it is very bright. Also, the screen peak luminance is recorded as 2900 in rtings with a 10% window. I am not sure if that is accurate for my particular display. I don’t have a meter that can measure that high (I will acquire one later this year, hopefully).
Regarding the nightly version. My HDR shader code fails to compile. The SDR version works however. But I found that it looks very dim and washed out when setting paper white and peak to 100. How do I get a ‘passthrough’ SDR shader output?
It very much sounds like your SDR shader is being treated as an HDR shader.
Which nightly build are you using?
Try appending hdr-config.slangp and setting the shader Luminance parameters to 100 nits, see if that makes the HDR output match SDR output.
That’s possible. I don’t know how RetroArch decides if a shader is SDR or HDR.
Github Copilot gave me this information. Does it align with reality?
| Final Pass Format | emits_hdr10() flag | Inverse Tonemap Applied? | PQ Encoding Applied? |
|---|---|---|---|
| RGB10A2 (HDR10 format) | true | No | No |
| RGB10A2 (HDR10 format) | false | Yes | Yes |
| RGBA16_SFLOAT | N/A | No | Yes |
| RGBA8 (SDR) | N/A | Yes | Yes |
The SDR shader uses RGBA8 for the final two passes but uses RGBA16 in several passes.
The version of Nightly is from 2/12.
After playing around with this some more I was able to get things working reasonably well with the Nightly. I set paper white and peak luminance to 100 and can get a near perfect SDR passthrough with the SDR shader. The order of screenshots is baseline on SDR, followed by HDR mode enabled.
HDR has a slight brightness expansion. It looks like it’s significant enough to not just be due to imprecision.
WCG is where the problem lies. Since RA assumes any shader chain with RGB10A2 as final is HDR, WCG will not work correctly:
I will try HDR later. We need a way for WCG shaders to work, i.e. for a shader to tell RetroArch it is not HDR despite having a RGB10A2 format. Further, the peak luminance setting is not really about the display maximum luminance, but is rather a limit on the extension of the inverse tone mapping. However, it also seems to impact ‘HDR content’ (as intepreted by RetroArch) as well. Therefore it may be better to have 4 values instead of 2: SDR paper white level SDR peak extension (either absolute nits or nits beyond paper white, but if the former, why would peak ever be below paper white?)
HDR paper white level HDR peak luminance? (I don’t really understand why these are needed for shaders that are already PQ encoded; it seems like setting paper white to 100 and peak luminance to 1000 ‘passes through’ the PQ encoded data as is, but I’m not sure).
For a static SDR inverse tone mapper, it might be more intuitive to users to set a target value for middle gray (default 18) and a point for white (default 100) instead of two white points.
So this is a hacky bandaid solution, but if you append hdr-config.slangp or hdr.slangp, that will force the shader to be presented as SDR, and allow you to use either the shader parameters or the “Colour Boost” setting to select the appropriate gamut.
@Azurfel I tried the hack. When I try to append hdr.slangp (hdr_v2.slang) it fails to compile. It gives a cryptic error compiling hdr_v2.slang:
[ERROR] [Slang] Underlying type of semantic is invalid.
[ERROR] [Slang] Failed to reflect SPIR-V. Resource usage is inconsistent with expectations.
When I use hdr_v2_config.slang it does run but the result is washed out regardless of how I try to set settings either in the parameter menu or in RetroArch:
Trying the HDR version of the shader, with PQ encoding:
However, I tried skipping the PQ encoding and outputting linear values where 1.0 corresponds to paper white (i.e. scRGB). By setting peak luminance to a value 2x paper white I can get an acceptable result (in this screenshot paper white is 300 and peak is 600):
There are several 16-bit framebuffer stages in this shader pipeline. Could that point to a source of the problem?
Yeah. Clearly that isn’t working correctly, but it is beyond my understanding as to why. We will see what @MajorPainTheCactus thinks of the situation.
I tried to come up with an estimate of what the signal range of the shader is (at least the example set up I’m working with). There is an average gain of roughly 0.2 on the input which is used as an inversion factor. The brightest pixels will end up around 4 (maybe 5?). Midtones will be around 1. Shadows will be under that. So the linear range is supposed to be [0, 4+). or 0 to 400+ nits.
Actually, I think it looks best if I set paper white to about 400-500 and set peak luminance exactly to match it. That seems to be about as perceptually bright as the SDR version (display calibrated to 100 nits) and also seems to have matching gamma.
I’m trying to set up a debugger so I can get more exact values. For now I just estimated that gain factor by eye with an in-shader debug signal.
I am extremely pleased to report that “D3D12/Vulkan: Fix HDR blend for widgets/OSD text, add gfx_widgets_visible() (#18703)” has been merged and, as of the most recent nightly timestamped 2026-02-14 18:25, the entire UI respects the Luminance settings selected in RetroArch’s HDR settings menu when using vulkan or d3d12 (Also, d3d12 crashing if you have too many playlists seems to be fixed.)
No more forced 10000 nit text, RetroAchievement popup images are now in the correct color space and brightness, screenshot popups are now in the correct color space.
The one minor bug i noticed is that the images on screenshot popups are much dimmer than they should be, even when the menu isn’t active.
As a refresher:
How to use RetroArch’s Luminance settings as an HDR UI Brightness setting
You can use RetroArch’s HDR menu “Luminance” settings to define the maximum HDR UI brightness if you are using shaders with their own internal HDR content Luminance parameters, which currently includes crt-sony-megatron-v2-default-config.slangp, hdr-config.slangp, Megatron v1, and the AzMods version of Megatron v2 as far as i am aware.
So for example, if you set both of RetroArch’s HDR menu “Luminance” settings to 100, the UI will never exceed 100.2 nits, regardless of what you set an HDR shader’s Luminance parameters to. This will allow you to get the brightness you need for shaders without also making the UI eye-searingly bright.
I created an HDR test card shader to help with troubleshooting HDR behavior. Here is what the shader would look like in SDR:
No tone-mapping
Reinhard tone-mapping
Ignore the sides. The first row is a linear grayscale ramp. The second row is a gamma-corrected grayscale ramp. Both range from 0 to 100 nits. The third row are two gray swatches, one below and one above middle gray. The third row is an HDR grayscale ramp from 100 to 10 000 nits.
Below this are two sets of color bars. The first is in SDR range at 75 nits. The second is in HDR range at 400 nits.
Here is the shader code. You can save this as hdr-test.slang and put it in the hdr/shaders folder:
#version 450
// Filename: hdr-test.slang
//
// Copyright (C) 2026 W. M. Martinez
//
// Licensed under the Apache License, Version 2.0(the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma name HDRTestCard
#pragma format A2B10G10R10_UNORM_PACK32
#pragma parameter PQ_OUTPUT "PQ output (ST.2084)" 1.0 0.0 1.0 1.0
layout(push_constant) uniform Push
{
vec4 SourceSize;
vec4 OriginalSize;
vec4 OutputSize;
uint FrameCount;
float PQ_OUTPUT;
} params;
#define PQ_OUTPUT params.PQ_OUTPUT
layout(std140, set = 0, binding = 0) uniform UBO
{
mat4 MVP;
} global;
#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
void main()
{
gl_Position = global.MVP * Position;
vTexCoord = TexCoord;
}
#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 0) out vec4 FragColor;
float pq_oetf(float L)
{
// ST.2084 PQ OETF, input normalized to 0..1 for 0..10000 nits.
const float m1 = 2610.0 / 16384.0;
const float m2 = 2523.0 / 32.0;
const float c1 = 3424.0 / 4096.0;
const float c2 = 2413.0 / 128.0;
const float c3 = 2392.0 / 128.0;
float Lm1 = pow(clamp(L, 0.0, 1.0), m1);
return pow((c1 + c2 * Lm1) / (1.0 + c3 * Lm1), m2);
}
vec3 pq_encode_from_100nits(vec3 linear)
{
const float max_nits = 10000.0;
const float unit_nits = 100.0;
vec3 L = max(linear, vec3(0.0)) * (unit_nits / max_nits);
return vec3(pq_oetf(L.r), pq_oetf(L.g), pq_oetf(L.b));
}
vec3 render_test_card(vec2 uv)
{
float out_gray = 0.0;
vec3 out_color = vec3(0.0);
const float safe_gain = 0.164;
const float peak_gain = 0.312;
float row = floor(uv.y * 6.0);
if (row < 1.0) {
// 10-step gray ramp, linear
float step_index = floor(uv.x * 10.0);
out_gray = clamp(step_index / 9.0, 0.0, 1.0);
out_color = vec3(out_gray);
} else if (row < 2.0) {
// 10-step gray ramp, Rec. 709 OETF (decoded to linear)
float step_index = floor(uv.x * 10.0);
float code = clamp(step_index / 9.0, 0.0, 1.0);
out_gray = code < 0.018 ? code / 4.5 : pow((code + 0.099) / 1.099, 1.0 / 0.45);
out_color = vec3(out_gray);
} else if (row < 3.0) {
// Safe gain then peak gain swatches
out_gray = (uv.x < 0.5) ? safe_gain : peak_gain;
out_color = vec3(out_gray);
} else if (row < 4.0) {
// 10-step HDR ramp, linear, 100 nits to 10000 nits (1 to 100)
float step_index = floor(uv.x * 10.0);
out_gray = 1.0 + step_index * (99.0 / 9.0);
out_color = vec3(out_gray);
} else if (row < 5.0) {
// SDR color bars at 0.75
float bar = floor(uv.x * 7.0);
if (bar < 1.0) {
out_color = vec3(1.0);
} else if (bar < 2.0) {
out_color = vec3(1.0, 1.0, 0.0);
} else if (bar < 3.0) {
out_color = vec3(0.0, 1.0, 1.0);
} else if (bar < 4.0) {
out_color = vec3(0.0, 1.0, 0.0);
} else if (bar < 5.0) {
out_color = vec3(1.0, 0.0, 1.0);
} else if (bar < 6.0) {
out_color = vec3(1.0, 0.0, 0.0);
} else {
out_color = vec3(0.0, 0.0, 1.0);
}
out_color *= 0.75;
} else if (row < 6.0) {
// HDR color bars at 4.0 (linear)
float bar = floor(uv.x * 7.0);
if (bar < 1.0) {
out_color = vec3(1.0);
} else if (bar < 2.0) {
out_color = vec3(1.0, 1.0, 0.0);
} else if (bar < 3.0) {
out_color = vec3(0.0, 1.0, 1.0);
} else if (bar < 4.0) {
out_color = vec3(0.0, 1.0, 0.0);
} else if (bar < 5.0) {
out_color = vec3(1.0, 0.0, 1.0);
} else if (bar < 6.0) {
out_color = vec3(1.0, 0.0, 0.0);
} else {
out_color = vec3(0.0, 0.0, 1.0);
}
out_color *= 4.0;
}
return out_color;
}
void main()
{
vec3 color = render_test_card(vTexCoord);
if (PQ_OUTPUT > 0.5) {
color = pq_encode_from_100nits(color);
}
FragColor = vec4(color, 1.0);
}
Presets
shaders = 1
shader0 = shaders/hdr-test.slang
filter_linear0 = false
scale_type0 = viewport
scale0 = 1.0
PQ_OUTPUT = 1.0
shaders = 2
shader0 = shaders/hdr-test.slang
filter_linear0 = false
scale_type0 = viewport
scale0 = 1.0
shader1 = "shaders/hdr_v2.slang"
filter_linear1 = "false"
scale_type1 = viewport
scale1 = 1.0
PQ_OUTPUT = 0.0
shaders = 2
shader0 = shaders/hdr-test.slang
filter_linear0 = false
scale_type0 = viewport
scale0 = 1.0
shader1 = "shaders/hdr_v2_config.slang"
filter_linear1 = "false"
scale_type1 = viewport
scale1 = 1.0
PQ_OUTPUT = 0.0
@anikom15 sorry Ive been tracking down and fixing vulkan and d3d12 validation bugs so Ive havent had much time to go through the above and reply. So the key from your perspective is to make sure your last pass has:
#pragma format A2B10G10R10_UNORM_PACK32
Then retroarch wont do anything when HDR is enabled and its completely up to you and your shader how you handle HDR. I call these shaders native HDR shaders as they output 10bit render targets. If you dont have that and HDR is on then Retroarch will use its internal shaders to do the HDR conversion for you.
As for documentation of algorithms Id just advise taking the inverse tonemap function and give it to Gemini’s AI studio and asking it to build a visualisation app where you can see the graph and tweak input values to see what it does and how it behaves - its free and itll do it in a few minutes if that.
Are you saying that if there is a 10-bit shader as the final pass, the paper nits and peak luminance settings within RetroArch shouldn’t do anything at all? Is my understanding right?
If that’s the case, then something is not working correctly. I will try the latest nightly, maybe I just got unlucky. Do you mind trying the HDR test card shader I posted to verify the 10-bit detection is working on your end?
ETA: I suppose it’s also possible that the graphics system isn’t actually giving a 10-bit texture, perhaps as some sort of hardware incompatibility. I’ll see if I can confirm the texture format in the log.
Yes if you output 10bit as the final pass it shouldn’t do anything - this works as the Sony Megatron works. If it didnt work then the Sony Megatron wouldnt work. Its fairly obvious to see. If you’re having difficulty determining what is going on, use RenderDoc (or your graphics debugger of choice) - you’ll be able to see exactly what is being executed and exactly what is being passed to shaders or not.
Found the problem, which confirms something i previously suspected.
If a shader preset has a “scale_type” line on the final shader, RetroArch treats it as an SDR shader, and the HDR menu Luminance and Colour Boost settings are active. By the sound of things this wasn’t meant to be the case, but it is.
So if i remove “scale_type17 = viewport” from your sfc_sfcjr.slangp HDR preset, it is no longer effected by RetroArch’s HDR menu settings. (Tho i’m pretty sure it still isn’t working as you intend… take a look.)
I notice in the vulkan_run_hdr_pipeline function in vulkan.c that inverse_tonemap and hdr10 are hardcoded to 1.0. Could that be a source of the problem?








