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:
@@ -145,7 +145,10 @@ Done:
|
||||
swapchains with an `ID3D11Texture2D` backbuffer are caught (the common D3D11
|
||||
case); D3D9 / pure-D3D12 games keep WGC. Validated by `present_hook_test`
|
||||
(drives a real D3D11 swapchain end-to-end and reads the pixels back through the
|
||||
shared texture).
|
||||
shared texture). The host samples the shared copy as plain UNORM (see
|
||||
`srgb_to_unorm`) so games with an `*_SRGB` backbuffer (e.g. Life is Strange:
|
||||
Before the Storm, whose backbuffer is `R8G8B8A8_UNORM_SRGB`) mirror with correct
|
||||
brightness instead of being darkened by an sRGB→linear decode.
|
||||
|
||||
- **x86 (32-bit) game support. ✅** A nested Win32 sub-build (CMake
|
||||
`ExternalProject`, driven from the normal x64 build) produces `coop_hook_x86.dll`
|
||||
|
||||
43
host/src/capture/dxgi_format.hpp
Normal file
43
host/src/capture/dxgi_format.hpp
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -59,6 +59,12 @@ target_link_libraries(audio_hook_test PRIVATE
|
||||
|
||||
add_test(NAME audio_hook_test COMMAND audio_hook_test)
|
||||
|
||||
# Unit test for the sRGB->UNORM format mapping the hooked video path uses to avoid
|
||||
# darkening *_SRGB-backbuffer games. Header-only, no device.
|
||||
add_executable(srgb_format_test srgb_format_test.cpp)
|
||||
target_include_directories(srgb_format_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
|
||||
add_test(NAME srgb_format_test COMMAND srgb_format_test)
|
||||
|
||||
# In-process self-test for the Present-hook video path. Reuses the shipping
|
||||
# present_hook.cpp and drives a real D3D11 swapchain in the same process, so it
|
||||
# exercises the IDXGISwapChain::Present inline hook, the shared keyed-mutex
|
||||
|
||||
41
tests/srgb_format_test.cpp
Normal file
41
tests/srgb_format_test.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
// Unit test for srgb_to_unorm (host/src/capture/dxgi_format.hpp): the hooked
|
||||
// video path samples the shared backbuffer copy as plain UNORM so an *_SRGB game
|
||||
// backbuffer isn't darkened by an sRGB->linear decode the host never re-encodes.
|
||||
// Locks the format mapping that fix depends on. No device needed.
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include "capture/dxgi_format.hpp"
|
||||
|
||||
using namespace coop;
|
||||
|
||||
namespace
|
||||
{
|
||||
int g_failures = 0;
|
||||
void expect(DXGI_FORMAT in, DXGI_FORMAT want, const char* what)
|
||||
{
|
||||
const DXGI_FORMAT got = srgb_to_unorm(in);
|
||||
if (got != want)
|
||||
{
|
||||
std::printf(" FAIL: %s (got %d, want %d)\n", what, static_cast<int>(got), static_cast<int>(want));
|
||||
++g_failures;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
// sRGB formats map to their plain-UNORM sibling.
|
||||
expect(DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, DXGI_FORMAT_R8G8B8A8_UNORM, "R8G8B8A8 sRGB -> UNORM");
|
||||
expect(DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, DXGI_FORMAT_B8G8R8A8_UNORM, "B8G8R8A8 sRGB -> UNORM");
|
||||
expect(DXGI_FORMAT_B8G8R8X8_UNORM_SRGB, DXGI_FORMAT_B8G8R8X8_UNORM, "B8G8R8X8 sRGB -> UNORM");
|
||||
|
||||
// Non-sRGB formats pass through unchanged.
|
||||
expect(DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM, "R8G8B8A8 UNORM passthrough");
|
||||
expect(DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM, "B8G8R8A8 UNORM passthrough");
|
||||
expect(DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM, "R10G10B10A2 passthrough");
|
||||
expect(DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_FORMAT_R16G16B16A16_FLOAT, "RGBA16F passthrough");
|
||||
|
||||
std::printf(g_failures == 0 ? "SRGB FORMAT TEST PASS\n" : "SRGB FORMAT TEST FAILED (%d)\n", g_failures);
|
||||
return g_failures == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user