VIDEO/D3D11: GPU tonemap path for HDR screenshots

Move the HDR -> SDR conversion in d3d11_gfx_read_viewport from the
CPU decoder in dxgi_hdr_readback_to_bgr24() to a single-pass GPU
render using a new PSMainToSDR entry point in hdr_sm5.hlsl.h.

The GPU path:
 - Lazily compiles PSMainToSDR into d3d11->hdr.ps_readback on the
   first HDR screenshot, amortising the D3DCompile cost.
 - Copies the swapchain backbuffer to a shader-readable intermediate
   (the swapchain itself is created with RENDER_TARGET_OUTPUT only
   and cannot be bound as an SRV directly).
 - Draws a fullscreen quad through the new pixel shader, sampling
   the intermediate and writing sRGB-encoded bytes into a
   B8G8R8A8_UNORM render target.
 - Copies the RT to a staging texture, maps it, and unswizzles into
   the caller's BGR24 bottom-up buffer.

Mirrors the same math as the CPU decoder and the Vulkan hdr_to_sdr
pipeline, so output is visually consistent across all three paths.

On any failure (D3DCompile failure, OOM, unexpected driver state,
Map failure) the caller logs a RARCH_WARN and falls through to the
existing CPU decoder — HDR screenshots remain available even if the
GPU path breaks on a particular driver/hardware combination.

D3D12 port will follow in a separate commit; the descriptor-heap
and resource-state work there is substantially more invasive and
deserves its own focused test pass.
This commit is contained in:
U-DESKTOP-SPFP6AQ\twistedtechre committed 2026-04-22 19:25:00 +02:00
1 parent 7d3b452ef3
commit 5fbe019e32
2 files changed
+313 -3

No files matched your search

+240 -3
View File
@@ -352,6 +352,7 @@ typedef struct
{
dxgi_hdr_uniform_t ubo_values;
D3D11Buffer ubo;
D3D11PixelShader ps_readback; /* lazy: HDR -> SDR tonemap PS used by read_viewport */
float menu_nits;
float max_output_nits;
float min_output_nits;
@@ -2783,6 +2784,7 @@ static void d3d11_gfx_free(void* data)
#ifdef HAVE_DXGI_HDR
Release(d3d11->hdr.ubo);
Release(d3d11->hdr.ps_readback);
#endif
d3d11_release_shader(&d3d11->sprites.shader);
@@ -5016,6 +5018,231 @@ static void d3d11_gfx_viewport_info(void* data, struct video_viewport* vp)
*vp = d3d11->vp;
}
#ifdef HAVE_DXGI_HDR
/* GPU path for HDR screenshot readback.
*
* Runs a single full-screen pass that samples the captured HDR backbuffer
* and writes sRGB-encoded SDR into a B8G8R8A8_UNORM render target, then
* copies that to a CPU-mappable staging texture and unswizzles into the
* caller's BGR24 output. Mirrors the Vulkan driver's hdr_to_sdr path
* and the CPU implementation in dxgi_hdr_readback_to_bgr24() — either
* one should produce visually identical screenshots.
*
* Returns false on any failure; the caller then falls back to the CPU
* decoder so HDR screenshots still work even if the GPU path breaks
* (driver PSO compile bug, OOM, unexpected state, etc.). */
static bool d3d11_gpu_hdr_readback_to_bgr24(
d3d11_video_t* d3d11,
ID3D11Resource* src_backbuffer_res,
DXGI_FORMAT src_format,
unsigned full_width,
unsigned full_height,
unsigned vp_x, unsigned vp_y,
unsigned vp_w, unsigned vp_h,
uint8_t* buffer)
{
ID3D11Device* device = d3d11->device;
ID3D11DeviceContext* context = d3d11->context;
d3d11_shader_t* hdr_shader = &d3d11->shaders[VIDEO_SHADER_STOCK_HDR];
d3d11_texture_t src_tex = { 0 };
d3d11_texture_t sdr_tex = { 0 };
ID3D11Texture2D* staging_tex = NULL;
ID3D11Resource* staging_res = NULL;
ID3D11Resource* sdr_res = NULL;
D3D11_TEXTURE2D_DESC staging_desc;
D3D11_MAPPED_SUBRESOURCE map;
D3D11_VIEWPORT vp;
D3D11_RECT sc;
unsigned hdr_mode;
unsigned y, x;
UINT stride = sizeof(d3d11_vertex_t);
UINT offset = 0;
bool mapped = false;
bool ret = false;
if (src_format == DXGI_FORMAT_R10G10B10A2_UNORM)
hdr_mode = 1;
else if (src_format == DXGI_FORMAT_R16G16B16A16_FLOAT)
hdr_mode = 2;
else
return false;
/* Lazy compile of the readback pixel shader. Only pays the cost
* the first time a screenshot is taken with HDR enabled. */
if (!d3d11->hdr.ps_readback)
{
static const char shader_src[] =
#include "d3d_shaders/hdr_sm5.hlsl.h"
;
D3DBlob ps_code = NULL;
if (!d3d_compile(shader_src, sizeof(shader_src), NULL,
"PSMainToSDR", "ps_5_0", &ps_code) || !ps_code)
{
RARCH_ERR("[D3D11] Failed to compile PSMainToSDR for HDR readback.\n");
return false;
}
if (FAILED(device->lpVtbl->CreatePixelShader(device,
ps_code->lpVtbl->GetBufferPointer(ps_code),
ps_code->lpVtbl->GetBufferSize(ps_code),
NULL, &d3d11->hdr.ps_readback)))
{
ps_code->lpVtbl->Release(ps_code);
RARCH_ERR("[D3D11] Failed to create readback PS.\n");
return false;
}
ps_code->lpVtbl->Release(ps_code);
}
/* HDR-format intermediate source: a shader-readable copy of the
* swapchain backbuffer. (The swapchain itself is created with
* RENDER_TARGET_OUTPUT only and cannot be bound as an SRV.) */
src_tex.desc.Width = full_width;
src_tex.desc.Height = full_height;
src_tex.desc.Format = src_format;
src_tex.desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
if (!d3d11_init_texture(device, &src_tex))
goto cleanup;
context->lpVtbl->CopyResource(context,
(ID3D11Resource*)src_tex.handle, src_backbuffer_res);
/* SDR render target: receives the tonemap output. */
sdr_tex.desc.Width = full_width;
sdr_tex.desc.Height = full_height;
sdr_tex.desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
sdr_tex.desc.BindFlags = D3D11_BIND_RENDER_TARGET;
if (!d3d11_init_texture(device, &sdr_tex))
goto cleanup;
/* Populate the UBO with readback-specific values and push. */
{
const float prev_it = d3d11->hdr.ubo_values.inverse_tonemap;
const float prev_h = d3d11->hdr.ubo_values.hdr10;
const unsigned prev_m = d3d11->hdr.ubo_values.hdr_mode;
const float prev_sc = d3d11->hdr.ubo_values.scanlines;
D3D11_MAPPED_SUBRESOURCE mapped_ubo;
d3d11->hdr.ubo_values.inverse_tonemap = 0.0f;
d3d11->hdr.ubo_values.hdr10 = 0.0f;
d3d11->hdr.ubo_values.hdr_mode = hdr_mode;
d3d11->hdr.ubo_values.scanlines = 0.0f;
if (SUCCEEDED(context->lpVtbl->Map(context,
(ID3D11Resource*)d3d11->hdr.ubo, 0,
D3D11_MAP_WRITE_DISCARD, 0, &mapped_ubo)))
{
*(dxgi_hdr_uniform_t*)mapped_ubo.pData = d3d11->hdr.ubo_values;
context->lpVtbl->Unmap(context,
(ID3D11Resource*)d3d11->hdr.ubo, 0);
}
d3d11->hdr.ubo_values.inverse_tonemap = prev_it;
d3d11->hdr.ubo_values.hdr10 = prev_h;
d3d11->hdr.ubo_values.hdr_mode = prev_m;
d3d11->hdr.ubo_values.scanlines = prev_sc;
}
/* Bind state: VS / IL / GS from the HDR stock shader, PS from our
* readback shader. */
context->lpVtbl->IASetInputLayout(context, hdr_shader->layout);
context->lpVtbl->VSSetShader(context, hdr_shader->vs, NULL, 0);
context->lpVtbl->PSSetShader(context, d3d11->hdr.ps_readback, NULL, 0);
context->lpVtbl->GSSetShader(context, hdr_shader->gs, NULL, 0);
context->lpVtbl->IASetPrimitiveTopology(context,
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
context->lpVtbl->VSSetConstantBuffers(context, 0, 1, &d3d11->hdr.ubo);
context->lpVtbl->PSSetConstantBuffers(context, 0, 1, &d3d11->hdr.ubo);
context->lpVtbl->PSSetShaderResources(context, 0, 1, &src_tex.view);
context->lpVtbl->PSSetSamplers(context, 0, 1,
&d3d11->samplers[RARCH_FILTER_UNSPEC][RARCH_WRAP_DEFAULT]);
context->lpVtbl->IASetVertexBuffers(context, 0, 1,
&d3d11->frame.vbo, &stride, &offset);
context->lpVtbl->OMSetRenderTargets(context, 1, &sdr_tex.rt_view, NULL);
context->lpVtbl->OMSetBlendState(context, d3d11->blend_disable, NULL, 0xFFFFFFFF);
vp.TopLeftX = 0.0f;
vp.TopLeftY = 0.0f;
vp.Width = (float)full_width;
vp.Height = (float)full_height;
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;
sc.left = 0;
sc.top = 0;
sc.right = (LONG)full_width;
sc.bottom = (LONG)full_height;
context->lpVtbl->RSSetViewports(context, 1, &vp);
context->lpVtbl->RSSetScissorRects(context, 1, &sc);
context->lpVtbl->RSSetState(context, d3d11->scissor_disabled);
context->lpVtbl->Draw(context, 4, 0);
/* Unbind SRV before we may read from the same texture as a source
* for anything else (and to stop D3D11 complaining about RTV/SRV
* aliasing if anything upstream uses the same slot). */
{
ID3D11ShaderResourceView* null_srv = NULL;
context->lpVtbl->PSSetShaderResources(context, 0, 1, &null_srv);
}
/* Staging copy of the SDR RT. */
staging_desc = sdr_tex.desc;
staging_desc.MipLevels = 1;
staging_desc.BindFlags = 0;
staging_desc.MiscFlags = 0;
staging_desc.Usage = D3D11_USAGE_STAGING;
staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
if (FAILED(device->lpVtbl->CreateTexture2D(device, &staging_desc,
NULL, &staging_tex)))
goto cleanup;
#ifdef __cplusplus
staging_tex->lpVtbl->QueryInterface(staging_tex, IID_ID3D11Resource, (void**)&staging_res);
sdr_tex.handle->lpVtbl->QueryInterface(sdr_tex.handle, IID_ID3D11Resource, (void**)&sdr_res);
#else
staging_tex->lpVtbl->QueryInterface(staging_tex, &IID_ID3D11Resource, (void**)&staging_res);
sdr_tex.handle->lpVtbl->QueryInterface(sdr_tex.handle, &IID_ID3D11Resource, (void**)&sdr_res);
#endif
context->lpVtbl->CopyResource(context, staging_res, sdr_res);
if (FAILED(context->lpVtbl->Map(context, staging_res, 0,
D3D11_MAP_READ, 0, &map)))
goto cleanup;
mapped = true;
/* BGRA8 -> BGR24, bottom-up, clamped to viewport. */
{
const uint8_t* src_row = (const uint8_t*)map.pData + (size_t)map.RowPitch * vp_y;
for (y = 0; y < vp_h; y++, src_row += map.RowPitch)
{
uint8_t* dst = buffer + 3 * (size_t)(vp_h - y - 1) * vp_w;
for (x = 0; x < vp_w; x++)
{
dst[3 * x + 0] = src_row[4 * (x + vp_x) + 0];
dst[3 * x + 1] = src_row[4 * (x + vp_x) + 1];
dst[3 * x + 2] = src_row[4 * (x + vp_x) + 2];
}
}
}
ret = true;
cleanup:
if (mapped)
context->lpVtbl->Unmap(context, staging_res, 0);
if (staging_res)
staging_res->lpVtbl->Release(staging_res);
if (sdr_res)
sdr_res->lpVtbl->Release(sdr_res);
if (staging_tex)
staging_tex->lpVtbl->Release(staging_tex);
d3d11_release_texture(&sdr_tex);
d3d11_release_texture(&src_tex);
return ret;
}
#endif /* HAVE_DXGI_HDR */
static bool d3d11_gfx_read_viewport(void* data, uint8_t* buffer, bool is_idle)
{
d3d11_video_t* d3d11 = (d3d11_video_t*)data;
@@ -5118,9 +5345,19 @@ static bool d3d11_gfx_read_viewport(void* data, uint8_t* buffer, bool is_idle)
#ifdef HAVE_DXGI_HDR
case DXGI_FORMAT_R10G10B10A2_UNORM:
case DXGI_FORMAT_R16G16B16A16_FLOAT:
/* HDR10 PQ or scRGB: hand off to the CPU HDR decoder.
* It undoes the forward HDR encoding using paper_white_nits
* and writes sRGB-encoded BGR24 bottom-up. */
/* HDR10 PQ or scRGB. Try the GPU tonemap pass first — it's
* faster and avoids the per-pixel CPU cost at 4K — and fall
* back to the CPU decoder on any failure so HDR screenshots
* still work even if the GPU path breaks (driver PSO compile
* bug, OOM, etc.). */
if (d3d11_gpu_hdr_readback_to_bgr24(
d3d11, BackBufferResource, StagingDesc.Format,
StagingDesc.Width, StagingDesc.Height,
vp_x, vp_y, vp_width, vp_height,
buffer))
break;
RARCH_WARN("[D3D11] GPU HDR readback failed, falling back to CPU.\n");
if (!dxgi_hdr_readback_to_bgr24(
StagingDesc.Format,
Map.pData, Map.RowPitch,
+73
View File
@@ -558,4 +558,77 @@ float4 PSMain(PSInput input) : SV_TARGET
return t0.Sample(s0, input.texcoord);
}
};
/* --- Readback path: HDR -> SDR (inverse of the forward composition) ---
* Used by the read_viewport screenshot path in the D3D11/D3D12 drivers
* to sample the HDR backbuffer (HDR10 PQ or scRGB FP16) and produce
* sRGB-encoded bytes that a plain PNG viewer can display correctly.
* Mirrors dxgi_hdr_readback_to_bgr24() in gfx/common/dxgi_common.c but
* runs on the GPU. */
/* sRGB OETF: linear [0,1] -> sRGB encoded [0,1]. The readback render
* target is B8G8R8A8_UNORM (linear), so we apply the encoding ourselves. */
float3 LinearToSRGB(float3 c)
{
float3 clamped = saturate(c);
float3 lo = clamped * 12.92f;
float3 hi = 1.055f * pow(clamped, 1.0f / 2.4f) - 0.055f;
return (clamped <= 0.0031308f) ? lo : hi;
}
/* Reverse of InverseTonemap(): compresses values above SDR white back
* into [0,1] using a hue-preserving operator on the peak channel. */
float3 Tonemap(const float3 hdr_linear, const float max_nits, const float brightness_nits)
{
const float input_val = max(hdr_linear.r, max(hdr_linear.g, hdr_linear.b));
if (input_val < 0.0001f) return hdr_linear;
const float peak_ratio = max_nits / brightness_nits;
const float k = 1.0f - (1.0f / peak_ratio);
return hdr_linear / max(1.0f + input_val * k, 0.0001f);
}
/* PQ-encoded BT.2020 HDR10 -> linear BT.709 at paper-white units.
* Undoes the forward pass's PQ encode and BT.709->BT.2020 rotation,
* and rescales so that SDR paper-white maps back to 1.0. */
float3 HDR10ToLinear(float3 hdr10, float brightness_nits)
{
float3 linear_2020_10k = ST2084ToLinear(hdr10);
float3 linear_709 = mul(k2020to709, linear_2020_10k);
/* Forward path multiplied by (brightness_nits / 10000) before PQ
* encoding; undo that to bring SDR white back to 1.0. */
return linear_709 * (kMaxNitsFor2084 / brightness_nits);
}
/* Entry point selected by hdr_mode:
* 1 = HDR10 PQ backbuffer -> HDR10ToLinear + Tonemap + sRGB encode
* 2 = scRGB FP16 backbuffer -> rescale by (80 / paper_white) + sRGB
* The readback RT is B8G8R8A8_UNORM, so we always apply the sRGB OETF. */
float4 PSMainToSDR(PSInput input) : SV_TARGET
{
float4 src = t0.Sample(s0, input.texcoord);
float3 sdr_linear;
if (global.hdr_mode == 1)
{
float3 hdr_linear = HDR10ToLinear(src.rgb, global.brightness_nits);
sdr_linear = Tonemap(hdr_linear,
global.brightness_nits,
global.brightness_nits);
}
else if (global.hdr_mode == 2)
{
/* scRGB: undo forward scale of (brightness_nits / 80).
* Negative and >1 values are legal scRGB (wide-gamut / super-white)
* and will clamp in LinearToSRGB. */
sdr_linear = src.rgb * (kscRGBWhiteNits / global.brightness_nits);
}
else
{
/* Passthrough — shouldn't happen on the HDR readback path but
* keeps the shader well-defined. */
sdr_linear = src.rgb;
}
return float4(LinearToSRGB(sdr_linear), 1.0f);
}
)