From a8eb49fb17e1e41a87f39ba1991a90796c929a69 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Thu, 13 Aug 2026 14:20:01 +1000 Subject: [PATCH] [idd] postprocess: separate calibrated frame branches --- idd/LGIdd/capture/CFrameGraph.cpp | 130 +++++++++++++----- idd/LGIdd/capture/CFrameGraph.h | 24 ++-- idd/LGIdd/capture/CSwapChainProcessor.cpp | 1 + .../effect/CColorTransformEffect.cpp | 29 ++-- .../effect/CColorTransformEffect.h | 13 ++ .../postprocess/effect/CDownsampleEffect.cpp | 28 ++-- .../postprocess/effect/CDownsampleEffect.h | 20 ++- 7 files changed, 177 insertions(+), 68 deletions(-) diff --git a/idd/LGIdd/capture/CFrameGraph.cpp b/idd/LGIdd/capture/CFrameGraph.cpp index 37265b5c..2ec5f70a 100644 --- a/idd/LGIdd/capture/CFrameGraph.cpp +++ b/idd/LGIdd/capture/CFrameGraph.cpp @@ -28,6 +28,7 @@ bool Frame::Same(const GraphCfg& left, const GraphCfg& right) left.srcWidth == right.srcWidth && left.srcHeight == right.srcHeight && Frame::Same(left.src, right.src) && + left.transform == right.transform && left.width == right.width && left.height == right.height && Frame::Same(left.checkpoint, right.checkpoint); @@ -141,60 +142,99 @@ unsigned CFrameGraph::Checkpoint(const FrameProfile& requested) { const FrameProfile profile = Frame::Store(requested, FrameStorage::D3D12_TEXTURE); + FrameProfile current = m_cfg.src; unsigned parent = 0; - FrameOp op; + + const D12ColorTransform * transform = m_cfg.transform.get(); + if (transform && transform->matrixEnabled) + { + // The common matrix stage preserves the source signal. A transfer-domain + // LUT is applied after each branch has selected scRGB or HDR10. + if (current.signal == FrameSignal::SRGB && + current.pixel == FramePixel::BGRA8) + current.pixel = FramePixel::RGBA8; + parent = AddNode(FrameOp::CAL, 0, + m_cfg.srcWidth, m_cfg.srcHeight, current); + if (parent == FRAME_GRAPH_ROOT) + return FRAME_GRAPH_ROOT; + } + + // Without calibration the established path filters linear scRGB before + // HDR10 encoding. This scale node is also shared by scRGB and HDR10 leaves. + const bool earlyScale = !transform && + (m_cfg.srcWidth != m_cfg.width || m_cfg.srcHeight != m_cfg.height); + if (earlyScale) + { + parent = AddNode(FrameOp::SCALE, parent, + m_cfg.width, m_cfg.height, current); + if (parent == FRAME_GRAPH_ROOT) + return FRAME_GRAPH_ROOT; + } + switch (profile.signal) { case FrameSignal::SRGB: - op = FrameOp::SDR; - parent = AddNode(FrameOp::SDR, 0, m_cfg.width, m_cfg.height, - m_cfg.checkpoint); - if (parent == FRAME_GRAPH_ROOT) - return FRAME_GRAPH_ROOT; - if (Frame::Same(profile, m_cfg.checkpoint)) - return parent; break; case FrameSignal::SCRGB_LINEAR: - { - op = FrameOp::SCRGB; - FrameProfile scRGB; - scRGB.storage = FrameStorage::D3D12_TEXTURE; - scRGB.pixel = FramePixel::RGBA16F; - scRGB.signal = FrameSignal::SCRGB_LINEAR; - return AddNode( - op, 0, m_cfg.width, m_cfg.height, scRGB); - } + break; case FrameSignal::PQ_BT2020: - op = FrameOp::HDR10; if (m_cfg.src.signal == FrameSignal::SCRGB_LINEAR) { - FrameProfile scRGB; - scRGB.storage = FrameStorage::D3D12_TEXTURE; - scRGB.pixel = FramePixel::RGBA16F; - scRGB.signal = FrameSignal::SCRGB_LINEAR; - parent = AddNode( - FrameOp::SCRGB, 0, m_cfg.width, m_cfg.height, scRGB); + const unsigned width = earlyScale ? m_cfg.width : m_cfg.srcWidth; + const unsigned height = earlyScale ? m_cfg.height : m_cfg.srcHeight; + parent = AddNode(FrameOp::HDR10, parent, + width, height, profile); if (parent == FRAME_GRAPH_ROOT) return FRAME_GRAPH_ROOT; - } - else - { - parent = AddNode(FrameOp::HDR10, 0, - m_cfg.width, m_cfg.height, m_cfg.checkpoint); - if (parent == FRAME_GRAPH_ROOT) - return FRAME_GRAPH_ROOT; - if (Frame::Same(profile, m_cfg.checkpoint)) - return parent; + current = profile; } break; default: return FRAME_GRAPH_ROOT; } - return AddNode( - op, parent, m_cfg.width, m_cfg.height, profile); + + if (transform && transform->lutEnabled) + { + if (current.signal == FrameSignal::SRGB && + current.pixel == FramePixel::BGRA8) + current.pixel = FramePixel::RGBA8; + parent = AddNode(FrameOp::LUT, parent, + m_cfg.srcWidth, m_cfg.srcHeight, current); + if (parent == FRAME_GRAPH_ROOT) + return FRAME_GRAPH_ROOT; + } + + if (!earlyScale && + (m_cfg.srcWidth != m_cfg.width || m_cfg.srcHeight != m_cfg.height)) + { + parent = AddNode(FrameOp::SCALE, parent, + m_cfg.width, m_cfg.height, current); + if (parent == FRAME_GRAPH_ROOT) + return FRAME_GRAPH_ROOT; + } + + if (parent != 0 && Frame::Same(current, profile)) + return parent; + + FrameOp op; + switch (profile.signal) + { + case FrameSignal::SRGB: + op = FrameOp::SDR; + break; + case FrameSignal::SCRGB_LINEAR: + op = FrameOp::SCRGB; + break; + case FrameSignal::PQ_BT2020: + op = FrameOp::HDR10; + break; + default: + return FRAME_GRAPH_ROOT; + } + return AddNode(op, parent, m_cfg.width, m_cfg.height, profile); } bool CFrameGraph::Add(BackendId id, uint32_t epoch, bool required, @@ -251,6 +291,26 @@ bool CFrameGraph::Same(const GraphCfg& cfg) const return m_sealed && Frame::Same(m_cfg, cfg); } +bool CFrameGraph::Need(FrameOp op) const +{ + if (!m_sealed || op == FrameOp::SRC) + return false; + for (unsigned i = 1; i < m_nodeCount; ++i) + if (m_nodes[i].op == op && m_nodes[i].refs) + return true; + return false; +} + +bool CFrameGraph::Want(FrameSignal signal) const +{ + if (!m_sealed) + return false; + for (unsigned i = 0; i < m_leafCount; ++i) + if (m_leaves[i].cfg.profile.signal == signal) + return true; + return false; +} + bool CFrameGraph::Desc(unsigned leaf, const FrameDesc& frame, LeafDesc& desc) const { diff --git a/idd/LGIdd/capture/CFrameGraph.h b/idd/LGIdd/capture/CFrameGraph.h index 58ce403f..9e2ba60a 100644 --- a/idd/LGIdd/capture/CFrameGraph.h +++ b/idd/LGIdd/capture/CFrameGraph.h @@ -31,11 +31,14 @@ static const unsigned FRAME_DAMAGE_MAX = 256; static const unsigned FRAME_GRAPH_ROOT = UINT_MAX; static const unsigned FRAME_GRAPH_MAX_NODES = - 1 + 2 * TRANSPORT_MAX_INSTANCES; + 1 + 5 * TRANSPORT_MAX_INSTANCES; enum class FrameOp : uint8_t { SRC, + CAL, + LUT, + SCALE, SDR, SCRGB, HDR10, @@ -50,14 +53,15 @@ enum class FrameDamage : uint8_t struct GraphCfg { - GpuMode mode = GpuMode::HARDWARE; - LUID adapter = {}; - unsigned srcWidth = 0; - unsigned srcHeight = 0; - FrameProfile src; - unsigned width = 0; - unsigned height = 0; - FrameProfile checkpoint; + GpuMode mode = GpuMode::HARDWARE; + LUID adapter = {}; + unsigned srcWidth = 0; + unsigned srcHeight = 0; + FrameProfile src; + std::shared_ptr transform; + unsigned width = 0; + unsigned height = 0; + FrameProfile checkpoint; }; namespace Frame @@ -134,6 +138,8 @@ public: const FrameCfg& cfg); bool Seal(); bool Same(const GraphCfg& cfg) const; + bool Want(FrameSignal signal) const; + bool Need(FrameOp op) const; bool Desc(unsigned leaf, const FrameDesc& frame, LeafDesc& desc) const; const GraphCfg& Cfg() const { return m_cfg; } diff --git a/idd/LGIdd/capture/CSwapChainProcessor.cpp b/idd/LGIdd/capture/CSwapChainProcessor.cpp index 2aefdea2..673d0f5a 100644 --- a/idd/LGIdd/capture/CSwapChainProcessor.cpp +++ b/idd/LGIdd/capture/CSwapChainProcessor.cpp @@ -63,6 +63,7 @@ static bool MakeGraphCfg(const D12FrameFormat& source, cfg.srcWidth = source.width; cfg.srcHeight = source.height; cfg.src = src; + cfg.transform = D12::Transform(source.colorTransform); cfg.width = checkpoint.width; cfg.height = checkpoint.height; cfg.checkpoint = output; diff --git a/idd/LGIdd/postprocess/effect/CColorTransformEffect.cpp b/idd/LGIdd/postprocess/effect/CColorTransformEffect.cpp index 520fc618..5d86916c 100644 --- a/idd/LGIdd/postprocess/effect/CColorTransformEffect.cpp +++ b/idd/LGIdd/postprocess/effect/CColorTransformEffect.cpp @@ -170,6 +170,13 @@ PostProcessStatus CColorTransformEffect::SetFormat( if (!transform) return PostProcessStatus::BYPASS_EFFECT; + const bool matrixEnabled = + m_part != CalPart::LUT && transform->matrixEnabled; + const bool lutEnabled = + m_part != CalPart::MATRIX && transform->lutEnabled; + if (!matrixEnabled && !lutEnabled) + return PostProcessStatus::BYPASS_EFFECT; + DXGI_FORMAT dstFormat; FrameType frameType; switch (src.desc.Format) @@ -184,10 +191,12 @@ PostProcessStatus CColorTransformEffect::SetFormat( frameType = src.format; break; case DXGI_FORMAT_R16G16B16A16_FLOAT: - // The client wire format is HDR10. Perform the XYZ adjustment before - // the BT.2020 rotation, and its LUT after PQ encoding, in one pass. - dstFormat = DXGI_FORMAT_R10G10B10A2_UNORM; - frameType = FRAME_TYPE_RGBA10; + // The legacy chain targets HDR10 in this pass. A graph CAL node keeps + // the linear scRGB signal so its HDR10 child remains independently + // selectable by transport demand. + dstFormat = m_keepSignal ? DXGI_FORMAT_R16G16B16A16_FLOAT : + DXGI_FORMAT_R10G10B10A2_UNORM; + frameType = m_keepSignal ? FRAME_TYPE_RGBA16F : FRAME_TYPE_RGBA10; break; default: DEBUG_ERROR("Unsupported color transform source format %u", src.desc.Format); @@ -208,11 +217,13 @@ PostProcessStatus CColorTransformEffect::SetFormat( std::memcpy(m_consts.matrix, transform->matrix, sizeof(m_consts.matrix)); m_consts.scalar = transform->scalar; - m_consts.matrixEnabled = transform->matrixEnabled; - m_consts.lutEnabled = transform->lutEnabled; - m_consts.inputTransfer = src.hdrPQ ? TRANSFER_PQ : + m_consts.matrixEnabled = matrixEnabled; + m_consts.lutEnabled = lutEnabled; + const UINT inputTransfer = src.hdrPQ ? TRANSFER_PQ : (src.hdr ? TRANSFER_LINEAR : TRANSFER_SRGB); - m_consts.outputTransfer = src.hdr ? TRANSFER_PQ : TRANSFER_SRGB; + m_consts.inputTransfer = inputTransfer; + m_consts.outputTransfer = m_keepSignal ? inputTransfer : + (src.hdr ? TRANSFER_PQ : TRANSFER_SRGB); std::memcpy(m_lut, transform->lut, sizeof(m_lut)); m_uploadPending = true; @@ -225,7 +236,7 @@ PostProcessStatus CColorTransformEffect::SetFormat( dst.desc = desc; dst.format = frameType; if (src.hdr) - dst.hdrPQ = true; + dst.hdrPQ = m_keepSignal ? src.hdrPQ : true; return PostProcessStatus::SUCCESS; } diff --git a/idd/LGIdd/postprocess/effect/CColorTransformEffect.h b/idd/LGIdd/postprocess/effect/CColorTransformEffect.h index 054f9f2e..5ada0e55 100644 --- a/idd/LGIdd/postprocess/effect/CColorTransformEffect.h +++ b/idd/LGIdd/postprocess/effect/CColorTransformEffect.h @@ -22,6 +22,13 @@ #include "CComputeEffect.h" +enum class CalPart : uint8_t +{ + ALL, + MATRIX, + LUT, +}; + class CColorTransformEffect : public CComputeEffect { private: @@ -41,8 +48,14 @@ private: ComPtr m_lutBuffer; DXGI_FORMAT m_srcFormat = DXGI_FORMAT_UNKNOWN; DXGI_FORMAT m_dstFormat = DXGI_FORMAT_UNKNOWN; + CalPart m_part; + bool m_keepSignal; public: + explicit CColorTransformEffect(CalPart part = CalPart::ALL, + bool keepSignal = false) : + m_part(part), m_keepSignal(keepSignal) {} + const char * GetName() const override { return "ColorTransform"; } bool Init(const ComPtr& device); diff --git a/idd/LGIdd/postprocess/effect/CDownsampleEffect.cpp b/idd/LGIdd/postprocess/effect/CDownsampleEffect.cpp index 9ba58cf9..c68267d6 100644 --- a/idd/LGIdd/postprocess/effect/CDownsampleEffect.cpp +++ b/idd/LGIdd/postprocess/effect/CDownsampleEffect.cpp @@ -96,7 +96,10 @@ const CDownsampleEffect::Rule * CDownsampleEffect::MatchRule( bool CDownsampleEffect::Init(const ComPtr& device, bool report) { - if (!ParseRules(g_settings.ReadStringValue(L"Downsample"), report)) + if ((!m_targetX && m_targetY) || (m_targetX && !m_targetY)) + return false; + if (!m_targetX && + !ParseRules(g_settings.ReadStringValue(L"Downsample"), report)) return false; D3D12_STATIC_SAMPLER_DESC sampler = {}; @@ -154,21 +157,30 @@ PostProcessStatus CDownsampleEffect::SetFormat( const ComPtr& device, const D12FrameFormat& src, D12FrameFormat& dst) { - const Rule * rule = MatchRule((unsigned)src.desc.Width, src.desc.Height); - if (!rule || - (rule->targetX == src.desc.Width && rule->targetY == src.desc.Height)) + unsigned targetX = m_targetX; + unsigned targetY = m_targetY; + if (!targetX || !targetY) + { + const Rule * rule = + MatchRule((unsigned)src.desc.Width, src.desc.Height); + if (!rule) + return PostProcessStatus::BYPASS_EFFECT; + targetX = rule->targetX; + targetY = rule->targetY; + } + if (targetX == src.desc.Width && targetY == src.desc.Height) return PostProcessStatus::BYPASS_EFFECT; D3D12_RESOURCE_DESC desc = src.desc; - desc.Width = rule->targetX; - desc.Height = rule->targetY; + desc.Width = targetX; + desc.Height = targetY; desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; if (!CreateDefaultTexture(device, desc, m_dst)) return PostProcessStatus::FAILED; - m_consts.width = (float)rule->targetX; - m_consts.height = (float)rule->targetY; + m_consts.width = (float)targetX; + m_consts.height = (float)targetY; const HRESULT hr = Upload(m_constBuffer, &m_consts, sizeof(m_consts)); if (FAILED(hr)) diff --git a/idd/LGIdd/postprocess/effect/CDownsampleEffect.h b/idd/LGIdd/postprocess/effect/CDownsampleEffect.h index 1c5efb33..898122ea 100644 --- a/idd/LGIdd/postprocess/effect/CDownsampleEffect.h +++ b/idd/LGIdd/postprocess/effect/CDownsampleEffect.h @@ -43,18 +43,24 @@ private: float height; } m_consts = {}; - std::vector m_rules; - ComPtr m_constBuffer; - DXGI_FORMAT m_format = DXGI_FORMAT_UNKNOWN; - double m_scaleX = 1.0; - double m_scaleY = 1.0; - unsigned m_width = 0; - unsigned m_height = 0; + std::vector m_rules; + ComPtr m_constBuffer; + DXGI_FORMAT m_format = DXGI_FORMAT_UNKNOWN; + double m_scaleX = 1.0; + double m_scaleY = 1.0; + unsigned m_width = 0; + unsigned m_height = 0; + unsigned m_targetX = 0; + unsigned m_targetY = 0; bool ParseRules(const std::wstring& value, bool report); const Rule * MatchRule(unsigned width, unsigned height) const; public: + CDownsampleEffect() = default; + CDownsampleEffect(unsigned width, unsigned height) : + m_targetX(width), m_targetY(height) {} + const char * GetName() const override { return "Downsample"; } bool Init(const ComPtr& device, bool report = true);