Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions Shaders/Shaders.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
<None Include="convert\hdr_tone_mapping.hlsl">
<FileType>Document</FileType>
</None>
<None Include="convert\hdr_tone_mapping_spline.hlsl">
<FileType>Document</FileType>
</None>
<None Include="convert\conv_matrix.hlsl">
<FileType>Document</FileType>
</None>
Expand Down Expand Up @@ -74,6 +77,34 @@
<ShaderModel>4.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
</FxCompile>
<FxCompile Include="d3d11\ps_convert_pq_to_sdr_tonemap.hlsl">
<ShaderModel>4.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
</FxCompile>
<FxCompile Include="d3d11\ps_fixconvert_pq_to_sdr_tonemap.hlsl">
<ShaderModel>4.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
</FxCompile>
<FxCompile Include="d3d11\ps_fixconvert_hlg_to_sdr_tonemap.hlsl">
<ShaderModel>4.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
</FxCompile>
<FxCompile Include="d3d11\ps_convert_pq_to_sdr_tonemap_measured.hlsl">
<ShaderModel>5.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
</FxCompile>
<FxCompile Include="d3d11\ps_fixconvert_pq_to_sdr_tonemap_measured.hlsl">
<ShaderModel>5.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
</FxCompile>
<FxCompile Include="d3d11\cs_hdr_hist.hlsl">
<ShaderModel>5.0</ShaderModel>
<ShaderType>Compute</ShaderType>
</FxCompile>
<FxCompile Include="d3d11\cs_hdr_resolve.hlsl">
<ShaderModel>5.0</ShaderModel>
<ShaderType>Compute</ShaderType>
</FxCompile>
<FxCompile Include="d3d11\ps_fix_bt2020.hlsl">
<ShaderModel>4.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
Expand Down Expand Up @@ -154,6 +185,30 @@
<ShaderModel>3.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
</FxCompile>
<FxCompile Include="d3d9\fixconvert_pq_to_sdr_tonemap.hlsl">
<ShaderModel>3.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
</FxCompile>
<FxCompile Include="d3d9\fixconvert_hlg_to_sdr_tonemap.hlsl">
<ShaderModel>3.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
</FxCompile>
<FxCompile Include="d3d9\fixconvert_pq_to_sdr_tonemap_measured.hlsl">
<ShaderModel>3.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
</FxCompile>
<FxCompile Include="d3d9\hdr_peak_first.hlsl">
<ShaderModel>3.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
</FxCompile>
<FxCompile Include="d3d9\hdr_peak_down.hlsl">
<ShaderModel>3.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
</FxCompile>
<FxCompile Include="d3d9\hdr_peak_state.hlsl">
<ShaderModel>3.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
</FxCompile>
<FxCompile Include="d3d9\fix_bt2020.hlsl">
<ShaderModel>3.0</ShaderModel>
<ShaderType>Pixel</ShaderType>
Expand Down
101 changes: 101 additions & 0 deletions Shaders/convert/hdr_tone_mapping_spline.hlsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// A curve and color model for the HDR to SDR conversion, used only by the Metadata
// method. It is kept out of hdr_tone_mapping.hlsl so that the shaders for the Fixed method
// contain none of it and are unchanged.
//
// BT.2020 linear light, nits in and nits out, content peak P onto display peak D. ToneMappingHable
// knows nothing about how bright the content is, so it compresses a 400 nit scene as hard as a 4000
// nit one; this does not.
//
// the gray curve the Hermite spline of BT.2390 in PQ, knee at 1.5 * PQ(D) - 0.5 so it follows
// the display and not the content, flat at max(P, 4000 nits)
// color the curve is given a saturation weighted mix of luminance and largest channel,
// mixed towards a per channel curve in LMS so very bright colors drift in hue
// and turn paler instead of clipping
//
// Saturation is taken on PQ encoded values rather than linear light. In linear light nearly every
// pixel of real content carrying any tint reads as almost fully saturated, so weighting by it
// compresses colored mid tones about twice as hard as it should; on a real 4K frame that was a 13%
// shortfall against an 8% one. A test chart cannot tell the two apart, because chart colors read
// as saturated on either scale, which is why this was not visible in the patch fit.
//
// Requires st2084.hlsl and hdr_tone_mapping.hlsl before it.
static const float kSplineFlatAt = 4000.0f;
static const float kSplineLmsShare = 0.2003f;
static const float kSplineLmsPower = 2.73f;
static const float3x3 kSplineRgbToLms = float3x3(
0.412109375f, 0.523925781f, 0.063964844f,
0.166748047f, 0.720458984f, 0.112792969f,
0.024169922f, 0.075439453f, 0.900390625f);
static const float3x3 kSplineLmsToRgb = float3x3(
3.436606694f, -2.506452119f, 0.069845424f,
-0.791329556f, 1.983600452f, -0.192270896f,
-0.025949900f, -0.098913715f, 1.124863614f);

inline float SplineCurve(float nits, float knee, float width, float endVal, float top)
{
const float e = LinearToST2084(nits, 10000.0f).x;
if (e <= knee)
return nits;
const float t = saturate((e - knee) / width);
const float t2 = t * t;
const float t3 = t2 * t;
const float h = (2.0f * t3 - 3.0f * t2 + 1.0f) * knee + (t3 - 2.0f * t2 + t) * width + (-2.0f * t3 + 3.0f * t2) * endVal;
return ST2084ToLinear(min(h, top), 10000.0f).x;
}

// toDisplay converts BT.2020 to the primaries being shown. It is needed because how far a color
// overshoots has to be judged on the channels the display will actually get, not on BT.2020: a
// saturated color that fits in BT.2020 can still be well outside BT.709, and judging it before the
// conversion leaves it to be clipped a channel at a time afterwards.
float3 ToneMappingSpline(float3 nits, float D, float P, float3x3 toDisplay)
{
const float M = max(nits.r, max(nits.g, nits.b));
if (M <= 0.000001f || P <= D)
return nits;

const float top = LinearToST2084(D, 10000.0f).x;
const float knee = max(1.5f * top - 0.5f, 0.0f);
const float width = LinearToST2084(max(P, kSplineFlatAt), 10000.0f).x - knee;
float endVal = top;
if (P < kSplineFlatAt) {
// the value at the content peak is linear in the end value, so it can be solved for
const float t = (LinearToST2084(P, 10000.0f).x - knee) / width;
const float t2 = t * t;
const float t3 = t2 * t;
endVal = (top - (2.0f * t3 - 3.0f * t2 + 1.0f) * knee - (t3 - 2.0f * t2 + t) * width) / (-2.0f * t3 + 3.0f * t2);
}

const float Y = 0.2627f * nits.r + 0.6780f * nits.g + 0.0593f * nits.b;
if (Y <= 0.000001f)
return nits;
const float3 e = LinearToST2084(float4(nits, 0.0f), 10000.0f).rgb;
const float eMax = max(e.r, max(e.g, e.b));
const float s = (eMax > 0.000001f) ? saturate(1.0f - min(e.r, min(e.g, e.b)) / eMax) : 0.0f;
const float N = Y * pow(max(M / Y, 1.0f), s);
const float3 S = nits * (SplineCurve(N, knee, width, endVal, top) / N);

const float3 lms = max(mul(kSplineRgbToLms, nits), 0.0f);
const float3 PC = max(mul(kSplineLmsToRgb, float3(
SplineCurve(lms.x, knee, width, endVal, top),
SplineCurve(lms.y, knee, width, endVal, top),
SplineCurve(lms.z, knee, width, endVal, top))), 0.0f);

const float3 Sd = mul(toDisplay, S);
const float rho = max(max(Sd.r, max(Sd.g, Sd.b)) / D, 1.0f);
const float mu = kSplineLmsShare + (1.0f - kSplineLmsShare) * (1.0f - pow(rho, -kSplineLmsPower));
const float3 c = max(lerp(S, PC, mu), 0.0f);

const float3 d = mul(toDisplay, c);
const float m = max(d.r, max(d.g, d.b));
return (m > D) ? c * (D / m) : c;
}

// rgb is scaled so that 1.0 is the display's white; contentNits carries the content peak, and is 0
// when there is nothing usable, which falls back to the fixed curve.
float3 ToneMappingSdr(float3 rgb, float luminanceScale, float contentNits, float3x3 toDisplay)
{
if (contentNits <= 0.0f)
return ToneMappingHable(rgb);
const float D = 10000.0f / max(luminanceScale, 0.0001f);
return ToneMappingSpline(rgb * D, D, contentNits, toDisplay) / D;
}
55 changes: 55 additions & 0 deletions Shaders/d3d11/cs_hdr_hist.hlsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Per-frame HDR peak measurement, pass 1 of 2.
//
// Builds a histogram of max(R, G, B) of the PQ-encoded frame. PQ is monotonic, so the
// histogram is taken directly on the encoded values and no EOTF is needed here: bin i
// stands for the PQ value i / (HIST_BINS - 1). That also makes the bins perceptually
// uniform, so the resolution is about the same everywhere in the range.
// max(R, G, B) is the quantity MaxCLL and MaxFALL are defined on (CTA-861.3).
//
// Pass 2 (cs_hdr_resolve.hlsl) turns the histogram into a peak and an average.

#define HIST_BINS 1024
#define GROUP_SIZE 16 // 16x16 threads, each covering 2x2 pixels = 32x32 pixels per group

Texture2D<float4> tex : register(t0);
RWStructuredBuffer<uint> hist : register(u0);

cbuffer MeasureConstants : register(b0)
{
uint2 rectOrigin; // top-left corner of the video rectangle inside the texture
uint2 rectSize;
};

groupshared uint gsHist[HIST_BINS];

[numthreads(GROUP_SIZE, GROUP_SIZE, 1)]
void main(uint3 groupId : SV_GroupID, uint3 threadId : SV_GroupThreadID, uint groupIndex : SV_GroupIndex)
{
for (uint i = groupIndex; i < HIST_BINS; i += GROUP_SIZE * GROUP_SIZE) {
gsHist[i] = 0;
}
GroupMemoryBarrierWithGroupSync();

const uint2 origin = groupId.xy * (GROUP_SIZE * 2) + threadId.xy * 2;

[unroll]
for (uint dy = 0; dy < 2; dy++) {
[unroll]
for (uint dx = 0; dx < 2; dx++) {
const uint2 p = origin + uint2(dx, dy);
if (p.x < rectSize.x && p.y < rectSize.y) {
const float3 c = saturate(tex.Load(int3(rectOrigin + p, 0)).rgb);
const float m = max(c.r, max(c.g, c.b));
const uint bin = min((uint)(m * (HIST_BINS - 1) + 0.5f), HIST_BINS - 1);
InterlockedAdd(gsHist[bin], 1);
}
}
}
GroupMemoryBarrierWithGroupSync();

for (uint j = groupIndex; j < HIST_BINS; j += GROUP_SIZE * GROUP_SIZE) {
if (gsHist[j] != 0) {
InterlockedAdd(hist[j], gsHist[j]);
}
}
}
Loading