Fix hooked video darkening for *_SRGB-backbuffer games

The host sampled the shared backbuffer copy through a view typed exactly like the
game's backbuffer. For games whose backbuffer is an *_SRGB format (e.g. Life is
Strange: Before the Storm -- confirmed R8G8B8A8_UNORM_SRGB / fmt 29 via the hook
log), the GPU decoded sRGB->linear on the sample, and the host then wrote those
linear values straight to its plain-UNORM swapchain with no re-encode, so the
mirror came out noticeably darker than the game.

Sample the copy as the plain-UNORM sibling of the format (srgb_to_unorm) so the
bytes pass through unchanged -- matching what WGC already does. The UNORM and
*_SRGB formats share a typeless group, so CopyResource from the producer's sRGB
texture into the host's UNORM copy is allowed. Non-sRGB formats are unaffected.

Adds srgb_format_test locking the mapping. All 6 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 14:22:31 +02:00
parent edf2da69d8
commit 24d66020f6
5 changed files with 99 additions and 2 deletions

View File

@@ -0,0 +1,43 @@
// Small DXGI format helpers for the video mirror.
#pragma once
#include <dxgiformat.h>
namespace coop
{
// Map an sRGB DXGI format to its plain UNORM sibling (same byte layout / type
// group), leaving non-sRGB formats unchanged.
//
// Why the hooked video path needs this: a game backbuffer is often an *_SRGB
// format. If the host samples the shared copy through an sRGB-typed view, the GPU
// decodes sRGB->linear on the read; the host then writes those linear values to
// its plain-UNORM swapchain with no re-encode, so the mirror comes out too dark.
// Sampling the copy as plain UNORM passes the bytes through unchanged -- exactly
// what Windows Graphics Capture does -- so the colors match the original. The
// UNORM and *_SRGB formats share a typeless group, so CopyResource between them
// (the producer's sRGB texture -> the host's UNORM copy) is allowed.
inline DXGI_FORMAT srgb_to_unorm(DXGI_FORMAT format)
{
switch (format)
{
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
return DXGI_FORMAT_R8G8B8A8_UNORM;
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
return DXGI_FORMAT_B8G8R8A8_UNORM;
case DXGI_FORMAT_B8G8R8X8_UNORM_SRGB:
return DXGI_FORMAT_B8G8R8X8_UNORM;
case DXGI_FORMAT_BC1_UNORM_SRGB:
return DXGI_FORMAT_BC1_UNORM;
case DXGI_FORMAT_BC2_UNORM_SRGB:
return DXGI_FORMAT_BC2_UNORM;
case DXGI_FORMAT_BC3_UNORM_SRGB:
return DXGI_FORMAT_BC3_UNORM;
case DXGI_FORMAT_BC7_UNORM_SRGB:
return DXGI_FORMAT_BC7_UNORM;
default:
return format;
}
}
} // namespace coop

View File

@@ -1,5 +1,6 @@
#include "capture/shared_texture.hpp"
#include "capture/dxgi_format.hpp"
#include "coop/protocol.hpp"
#include "coop/shared_memory.hpp"
@@ -57,12 +58,15 @@ bool SharedTextureSource::reopen(unsigned long pid, const VideoShareView& share)
}
// Private copy we sample from, so we only hold the keyed mutex during the copy.
// Use the plain-UNORM sibling of the (possibly sRGB) backbuffer format so the
// SRV passes the bytes through without an sRGB->linear decode that would darken
// the mirror (CopyResource is allowed within the shared typeless group).
D3D11_TEXTURE2D_DESC desc{};
desc.Width = share.width;
desc.Height = share.height;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = static_cast<DXGI_FORMAT>(share.format);
desc.Format = srgb_to_unorm(static_cast<DXGI_FORMAT>(share.format));
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;