[idd] select linear RGB24 packing automatically

Replace the texture-shaped RGB24 output with a linear raw buffer and
keep its packing, damage translation, and buffer copies inside the
RGB24 effect.

Benchmark full-frame native and packed processing and retain the faster
path for each source format. Preserve logical damage rectangles for
client updates and alternating framebuffer repair.

Return compute outputs to COMMON for COPY queue handoff and refresh
cached framebuffer sizes when switching packed and native layouts.
This commit is contained in:
Geoffrey McRae
2026-08-03 20:35:49 +10:00
parent fa2769b332
commit 6109501eed
16 changed files with 987 additions and 232 deletions

View File

@@ -20,69 +20,517 @@
#include "CRGB24Effect.h"
#include "CDebug.h"
#include "../CSettings.h"
#include "common/LGMPConfig.h"
#include <algorithm>
#include <climits>
#include <cstdint>
#include <cstring>
using namespace PostProcessUtil;
static_assert(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT == 256,
"RGB24 shader row alignment must match D3D12");
struct CRGB24Effect::State
{
enum class Phase
{
DISABLED,
NATIVE_WARMUP,
NATIVE_SAMPLE,
PACKED_WARMUP,
PACKED_SAMPLE,
LOCKED_NATIVE,
LOCKED_PACKED,
};
struct FormatKey
{
D3D12_RESOURCE_DIMENSION resourceDimension = D3D12_RESOURCE_DIMENSION_UNKNOWN;
UINT64 resourceWidth = 0;
UINT resourceHeight = 0;
DXGI_FORMAT resourceFormat = DXGI_FORMAT_UNKNOWN;
unsigned width = 0;
unsigned height = 0;
FrameType format = FRAME_TYPE_INVALID;
bool hdr = false;
bool hdrPQ = false;
std::shared_ptr<const D12ColorTransform> colorTransform;
};
static const unsigned WarmupCount = LGMP_Q_FRAME_LEN;
static const unsigned SampleCount = 64;
static const unsigned TrimCount = SampleCount / 8;
SRWLOCK lock = SRWLOCK_INIT;
Phase phase = Phase::DISABLED;
FormatKey format = {};
bool formatValid = false;
uint64_t generation = 0;
unsigned warmups = 0;
unsigned sampleCount = 0;
uint64_t nativeMean = 0;
uint64_t samples[SampleCount] = {};
static bool IsEligible(const D12FrameFormat& format)
{
if (format.hdr ||
format.desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D ||
format.desc.Format != DXGI_FORMAT_B8G8R8A8_UNORM)
return false;
if (!format.colorTransform ||
(!format.colorTransform->matrixEnabled &&
!format.colorTransform->lutEnabled))
return true;
return IsIdentityColorTransform(*format.colorTransform);
}
bool WantsPackedLocked() const
{
return phase == Phase::PACKED_WARMUP ||
phase == Phase::PACKED_SAMPLE ||
phase == Phase::LOCKED_PACKED;
}
bool IsBenchmarkingLocked() const
{
return phase == Phase::NATIVE_WARMUP ||
phase == Phase::NATIVE_SAMPLE ||
phase == Phase::PACKED_WARMUP ||
phase == Phase::PACKED_SAMPLE;
}
void ResetStageLocked()
{
warmups = 0;
sampleCount = 0;
std::memset(samples, 0, sizeof(samples));
}
uint64_t TrimmedMeanLocked()
{
std::sort(samples, samples + SampleCount);
uint64_t total = 0;
for (unsigned i = TrimCount; i < SampleCount - TrimCount; ++i)
total += samples[i];
return total / (SampleCount - TrimCount * 2);
}
void Update(const D12FrameFormat& next)
{
AcquireSRWLockExclusive(&lock);
const bool formatChanged = !formatValid ||
format.resourceDimension != next.desc.Dimension ||
format.resourceWidth != next.desc.Width ||
format.resourceHeight != next.desc.Height ||
format.resourceFormat != next.desc.Format ||
format.width != next.width ||
format.height != next.height ||
format.format != next.format ||
format.hdr != next.hdr ||
format.hdrPQ != next.hdrPQ ||
format.colorTransform != next.colorTransform;
if (formatChanged)
{
format.resourceDimension = next.desc.Dimension;
format.resourceWidth = next.desc.Width;
format.resourceHeight = next.desc.Height;
format.resourceFormat = next.desc.Format;
format.width = next.width;
format.height = next.height;
format.format = next.format;
format.hdr = next.hdr;
format.hdrPQ = next.hdrPQ;
format.colorTransform = next.colorTransform;
formatValid = true;
phase = IsEligible(next) ?
Phase::NATIVE_WARMUP : Phase::DISABLED;
nativeMean = 0;
++generation;
ResetStageLocked();
}
switch (phase)
{
case Phase::NATIVE_WARMUP:
if (warmups >= WarmupCount)
{
phase = Phase::NATIVE_SAMPLE;
++generation;
ResetStageLocked();
}
break;
case Phase::NATIVE_SAMPLE:
if (sampleCount >= SampleCount)
{
nativeMean = TrimmedMeanLocked();
phase = Phase::PACKED_WARMUP;
++generation;
ResetStageLocked();
}
break;
case Phase::PACKED_WARMUP:
if (warmups >= WarmupCount)
{
phase = Phase::PACKED_SAMPLE;
++generation;
ResetStageLocked();
}
break;
case Phase::PACKED_SAMPLE:
if (sampleCount >= SampleCount)
{
const uint64_t packedMean = TrimmedMeanLocked();
const uint64_t relativeThreshold = nativeMean / 20;
const uint64_t threshold = relativeThreshold > 50000ULL ?
relativeThreshold : 50000ULL;
// Prefer the bandwidth saving unless native is meaningfully faster.
const bool usePacked = packedMean <= nativeMean ||
packedMean - nativeMean <= threshold;
DEBUG_INFO(
"RGB24 benchmark: native=%llu us, packed=%llu us, selected=%s",
(unsigned long long)(nativeMean / 1000),
(unsigned long long)(packedMean / 1000),
usePacked ? "packed" : "native");
phase = usePacked ?
Phase::LOCKED_PACKED : Phase::LOCKED_NATIVE;
++generation;
ResetStageLocked();
}
break;
default:
break;
}
ReleaseSRWLockExclusive(&lock);
}
bool WantsPacked()
{
AcquireSRWLockShared(&lock);
const bool result = WantsPackedLocked();
ReleaseSRWLockShared(&lock);
return result;
}
bool IsBenchmarking()
{
AcquireSRWLockShared(&lock);
const bool result = IsBenchmarkingLocked();
ReleaseSRWLockShared(&lock);
return result;
}
uint64_t GetTimingToken(bool packed)
{
AcquireSRWLockShared(&lock);
const uint64_t result = IsBenchmarkingLocked() &&
packed == WantsPackedLocked() ? generation : 0;
ReleaseSRWLockShared(&lock);
return result;
}
void Reject()
{
AcquireSRWLockExclusive(&lock);
if (WantsPackedLocked())
{
phase = Phase::LOCKED_NATIVE;
++generation;
ResetStageLocked();
}
ReleaseSRWLockExclusive(&lock);
}
void RecordTiming(uint64_t token, bool fullCopy, uint64_t totalTime)
{
AcquireSRWLockExclusive(&lock);
if (token == generation && fullCopy)
switch (phase)
{
case Phase::NATIVE_WARMUP:
case Phase::PACKED_WARMUP:
++warmups;
break;
case Phase::NATIVE_SAMPLE:
case Phase::PACKED_SAMPLE:
if (sampleCount < SampleCount)
samples[sampleCount++] = totalTime;
break;
default:
break;
}
ReleaseSRWLockExclusive(&lock);
}
};
bool CRGB24Effect::Init(const ComPtr<ID3D12Device3>& device)
{
if (!g_settings.ReadBoolValue(L"AllowRGB24", false))
if (!g_settings.ReadBoolValue(L"AllowRGB24", true))
return false;
D3D12_DESCRIPTOR_RANGE ranges[2] = {};
ranges[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
ranges[0].NumDescriptors = 1;
ranges[0].BaseShaderRegister = 0;
ranges[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
ranges[0].NumDescriptors = 1;
ranges[0].BaseShaderRegister = 0;
ranges[0].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
ranges[1].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_UAV;
ranges[1].NumDescriptors = 1;
ranges[1].BaseShaderRegister = 0;
ranges[1].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_UAV;
ranges[1].NumDescriptors = 1;
ranges[1].BaseShaderRegister = 0;
ranges[1].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
const char * shader =
"Texture2D <float4> src : register(t0);\n"
"RWTexture2D<float4> dst : register(u0);\n"
"Texture2D<float4> src : register(t0);\n"
"RWByteAddressBuffer dst : register(u0);\n"
"[numthreads(" POST_PROCESS_THREADS_STR ", " POST_PROCESS_THREADS_STR ", 1)]\n"
"void main(uint3 dt : SV_DispatchThreadID)\n"
"{\n"
" uint fstInputX = (dt.x * 4) / 3;\n"
" float4 color0 = src[uint2(fstInputX, dt.y)];\n"
" uint sndInputX = fstInputX + 1;\n"
" float4 color3 = src[uint2(sndInputX, dt.y)];\n"
" uint width, height;\n"
" src.GetDimensions(width, height);\n"
" uint rowBytes = width * 3;\n"
" uint dataWidth = ((rowBytes + 255) & ~255u) / 4;\n"
" if (dt.x >= dataWidth || dt.y >= height)\n"
" return;\n"
" uint rowOffset = dt.x * 4;\n"
" if (rowOffset >= rowBytes)\n"
" {\n"
" dst.Store((dt.y * dataWidth + dt.x) * 4, 0);\n"
" return;\n"
" }\n"
" uint firstX = (dt.x * 4) / 3;\n"
" uint secondX = firstX + 1;\n"
" float4 color0 = src[uint2(firstX, dt.y)];\n"
" float4 color3 = secondX < width ?\n"
" src[uint2(secondX, dt.y)] : 0.0f;\n"
" uint xmod3 = dt.x % 3;\n"
" float4 color1 = xmod3 <= 1 ? color0 : color3;\n"
" float4 color2 = xmod3 == 0 ? color0 : color3;\n"
" float b = color0.bgr[xmod3];\n"
" float g = color1.grb[xmod3];\n"
" float r = color2.rbg[xmod3];\n"
" float a = color3.bgr[xmod3];\n"
" dst[dt.xy] = float4(r, g, b, a);\n"
" float4 packed = float4(\n"
" color0.bgr[xmod3], color1.grb[xmod3],\n"
" color2.rbg[xmod3], color3.bgr[xmod3]);\n"
" uint4 bytes = (uint4)(saturate(packed) * 255.0f + 0.5f);\n"
" uint value = bytes.x | (bytes.y << 8) |\n"
" (bytes.z << 16) | (bytes.w << 24);\n"
" dst.Store((dt.y * dataWidth + dt.x) * 4, value);\n"
"}\n";
return InitCompute(device, ranges, ARRAYSIZE(ranges), nullptr, 0, shader);
if (!InitCompute(device, ranges, ARRAYSIZE(ranges), nullptr, 0, shader))
return false;
m_state = std::make_shared<State>();
return true;
}
void CRGB24Effect::ShareState(const CPostProcessEffect& other)
{
m_state = static_cast<const CRGB24Effect&>(other).m_state;
}
void CRGB24Effect::Update(const D12FrameFormat& format)
{
m_state->Update(format);
}
bool CRGB24Effect::NeedsReconfigure() const
{
return m_state->WantsPacked() != Enabled;
}
bool CRGB24Effect::RequiresFullDamage() const
{
return m_state->IsBenchmarking();
}
uint64_t CRGB24Effect::GetTimingToken() const
{
return m_state->GetTimingToken(Enabled);
}
void CRGB24Effect::RecordTiming(
uint64_t token, bool fullCopy, uint64_t totalTime)
{
m_state->RecordTiming(token, fullCopy, totalTime);
}
bool CRGB24Effect::GetCopyLayout(
unsigned * pitch, unsigned * dataHeight) const
{
*pitch = m_pitch;
*dataHeight = m_height;
return true;
}
static void GetCopySpan(const RECT& rect, unsigned width,
unsigned pitch, UINT64 * left, UINT64 * right)
{
// Damage stays in logical pixels until the packed copy is recorded.
*left = ((UINT64)rect.left * 3) & ~3ULL;
if (rect.left == 0 && rect.right == (LONG)width)
*right = pitch;
else
*right = ((UINT64)rect.right * 3 + 3) & ~3ULL;
}
bool CRGB24Effect::ShouldCopyFully(
const RECT dirtyRects[], unsigned nbDirtyRects) const
{
static const unsigned commandLimit = 256;
const uint64_t frameSize = (uint64_t)m_pitch * m_height;
uint64_t copiedBytes = 0;
unsigned commands = 0;
for (const RECT * rect = dirtyRects;
rect < dirtyRects + nbDirtyRects; ++rect)
{
UINT64 left;
UINT64 right;
GetCopySpan(*rect, m_width, m_pitch, &left, &right);
const unsigned rows = (unsigned)(rect->bottom - rect->top);
if (left == 0 && right == m_pitch)
{
++commands;
copiedBytes += (uint64_t)rows * m_pitch;
}
else
{
commands += rows;
copiedBytes += (right - left) * rows;
}
if (commands > commandLimit || copiedBytes >= frameSize)
return true;
}
return false;
}
void CRGB24Effect::CopyFrame(
const ComPtr<ID3D12GraphicsCommandList>& commandList,
ID3D12Resource * dst, ID3D12Resource * src,
const RECT dirtyRects[], unsigned nbDirtyRects, bool fullCopy) const
{
if (fullCopy)
{
commandList->CopyBufferRegion(
dst, 0, src, 0, (UINT64)m_pitch * m_height);
return;
}
for (const RECT * rect = dirtyRects;
rect < dirtyRects + nbDirtyRects; ++rect)
{
UINT64 left;
UINT64 right;
GetCopySpan(*rect, m_width, m_pitch, &left, &right);
if (left == 0 && right == m_pitch)
{
const UINT64 offset = (UINT64)rect->top * m_pitch;
const UINT64 size =
(UINT64)(rect->bottom - rect->top) * m_pitch;
commandList->CopyBufferRegion(dst, offset, src, offset, size);
continue;
}
for (LONG y = rect->top; y < rect->bottom; ++y)
{
const UINT64 offset = (UINT64)y * m_pitch + left;
commandList->CopyBufferRegion(
dst, offset, src, offset, right - left);
}
}
}
PostProcessStatus CRGB24Effect::SetFormat(const ComPtr<ID3D12Device3>& device,
const D12FrameFormat& src, D12FrameFormat& dst)
{
if (src.desc.Format != DXGI_FORMAT_B8G8R8A8_UNORM)
if (!m_state->WantsPacked())
{
m_dst.Reset();
return PostProcessStatus::BYPASS_EFFECT;
}
const unsigned packedPitch = AlignTo((unsigned)src.desc.Width * 3, 4u);
D3D12_RESOURCE_DESC desc = src.desc;
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.Width = AlignTo(packedPitch / 4, 64u);
desc.Height = ((unsigned)src.desc.Width * src.desc.Height) / (packedPitch / 3);
desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
if (src.desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D ||
src.desc.Format != DXGI_FORMAT_B8G8R8A8_UNORM ||
src.hdr)
{
DEBUG_WARN("RGB24 packing is unavailable for the current format");
m_state->Reject();
m_dst.Reset();
return PostProcessStatus::BYPASS_EFFECT;
}
if (!CreateDefaultTexture(device, desc, m_dst))
return PostProcessStatus::FAILED;
if (src.desc.Width >
(UINT64_MAX - (D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1)) / 3)
{
m_state->Reject();
m_dst.Reset();
return PostProcessStatus::BYPASS_EFFECT;
}
m_threadsX = ((unsigned)desc.Width + (Threads - 1)) / Threads;
m_threadsY = ((unsigned)desc.Height + (Threads - 1)) / Threads;
const UINT64 packedPitch = AlignTo<UINT64>(
src.desc.Width * 3, D3D12_TEXTURE_DATA_PITCH_ALIGNMENT);
if (!src.desc.Height || packedPitch > LONG_MAX ||
packedPitch > UINT64_MAX / src.desc.Height)
{
m_state->Reject();
m_dst.Reset();
return PostProcessStatus::BYPASS_EFFECT;
}
dst.desc = desc;
dst.format = FRAME_TYPE_BGR_32;
const UINT64 bufferSize = packedPitch * src.desc.Height;
const UINT64 maxUAVSize =
(1ULL << D3D12_REQ_BUFFER_RESOURCE_TEXEL_COUNT_2_TO_EXP) *
sizeof(uint32_t);
if (bufferSize > UINT32_MAX || bufferSize > maxUAVSize)
{
m_state->Reject();
m_dst.Reset();
return PostProcessStatus::BYPASS_EFFECT;
}
if (!CreateDefaultBuffer(device, bufferSize, m_dst))
{
m_state->Reject();
m_dst.Reset();
return PostProcessStatus::BYPASS_EFFECT;
}
const unsigned dataWidth = (unsigned)(packedPitch / 4);
m_threadsX = (dataWidth + (Threads - 1)) / Threads;
m_threadsY = (src.desc.Height + (Threads - 1)) / Threads;
m_width = (unsigned)src.desc.Width;
m_height = src.desc.Height;
m_pitch = (unsigned)packedPitch;
dst.desc = m_dst->GetDesc();
dst.dataWidth = dataWidth;
dst.dataHeight = src.desc.Height;
dst.pitch = m_pitch;
dst.format = FRAME_TYPE_BGR_32;
return PostProcessStatus::SUCCESS;
}
@@ -94,7 +542,7 @@ ComPtr<ID3D12Resource> CRGB24Effect::Run(const ComPtr<ID3D12Device3>& device,
UNREFERENCED_PARAMETER(dirtyRects);
UNREFERENCED_PARAMETER(nbDirtyRects);
TransitionDst(commandList, D3D12_RESOURCE_STATE_COPY_SOURCE,
TransitionDst(commandList, D3D12_RESOURCE_STATE_COMMON,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
D3D12_CPU_DESCRIPTOR_HANDLE handle =
@@ -103,31 +551,27 @@ ComPtr<ID3D12Resource> CRGB24Effect::Run(const ComPtr<ID3D12Device3>& device,
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Texture2D.MipLevels = 1;
srvDesc.Texture2D.MipLevels = 1;
device->CreateShaderResourceView(src.Get(), &srvDesc, handle);
handle.ptr += inc;
D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {};
uavDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D;
uavDesc.Format = DXGI_FORMAT_R32_TYPELESS;
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_BUFFER;
uavDesc.Buffer.NumElements = (UINT)(m_dst->GetDesc().Width / 4);
uavDesc.Buffer.StructureByteStride = 0;
uavDesc.Buffer.CounterOffsetInBytes = 0;
uavDesc.Buffer.Flags = D3D12_BUFFER_UAV_FLAG_RAW;
device->CreateUnorderedAccessView(m_dst.Get(), nullptr, &uavDesc, handle);
Bind(commandList);
commandList->Dispatch(m_threadsX, m_threadsY, 1);
TransitionDst(commandList, D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
D3D12_RESOURCE_STATE_COPY_SOURCE);
for (RECT * rect = dirtyRects; rect < dirtyRects + *nbDirtyRects; ++rect)
{
const LONG left = rect->left;
const LONG right = rect->right;
rect->left = (left * 3) / 4;
rect->right = (right * 3 + 3) / 4;
}
D3D12_RESOURCE_STATE_COMMON);
return m_dst;
}