From 1f6dbb0daeea09270fb7248ba4250f80638f8ba3 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Sun, 19 Jul 2026 02:13:57 +1000 Subject: [PATCH] [idd] hdr: propagate per-frame HDR metadata Handle DEFAULT, NEW, and UNCHANGED IDDCX HDR10 metadata states and publish valid mastering metadata through KVMFR. Preserve the metadata across post-processing without reallocating resources for metadata-only changes. --- common/include/common/KVMFR.h | 5 +- idd/LGIdd/CIndirectDeviceContext.cpp | 41 ++++++++------- idd/LGIdd/CIndirectDeviceContext.h | 1 + idd/LGIdd/CPostProcessor.cpp | 22 +++++++- idd/LGIdd/CPostProcessor.h | 5 +- idd/LGIdd/CSwapChainProcessor.cpp | 75 ++++++++++++++++++++++++++- idd/LGIdd/CSwapChainProcessor.h | 12 +++++ idd/LGIdd/effect/CHDR16to10Effect.cpp | 16 ++---- 8 files changed, 139 insertions(+), 38 deletions(-) diff --git a/common/include/common/KVMFR.h b/common/include/common/KVMFR.h index 42eb6f9f..a3bfabb5 100644 --- a/common/include/common/KVMFR.h +++ b/common/include/common/KVMFR.h @@ -143,7 +143,8 @@ enum FRAME_FLAG_REQUEST_ACTIVATION = 0x2 , FRAME_FLAG_TRUNCATED = 0x4 , // ivshmem was too small for the frame FRAME_FLAG_HDR = 0x8 , // RGBA10 may not be HDR - FRAME_FLAG_HDR_PQ = 0x10 // HDR PQ has been applied to the frame + FRAME_FLAG_HDR_PQ = 0x10, // HDR PQ has been applied to the frame + FRAME_FLAG_HDR_METADATA = 0x20 // HDR static metadata fields are valid }; typedef uint32_t KVMFRFrameFlags; @@ -167,7 +168,7 @@ typedef struct KVMFRFrame FrameDamageRect damageRects[KVMFR_MAX_DAMAGE_RECTS]; KVMFRFrameFlags flags; // bit field combination of FRAME_FLAG_* - // HDR static metadata (valid when FRAME_FLAG_HDR is set) + // HDR static metadata (valid when FRAME_FLAG_HDR_METADATA is set) // Display color primaries in 0.00002 units (SMPTE ST 2086 format) uint16_t hdrDisplayPrimary[3][2]; // Rx,Ry, Gx,Gy, Bx,By uint16_t hdrWhitePoint[2]; // Wx, Wy diff --git a/idd/LGIdd/CIndirectDeviceContext.cpp b/idd/LGIdd/CIndirectDeviceContext.cpp index 36847dff..82577158 100644 --- a/idd/LGIdd/CIndirectDeviceContext.cpp +++ b/idd/LGIdd/CIndirectDeviceContext.cpp @@ -1052,24 +1052,21 @@ CIndirectDeviceContext::PreparedFrameBuffer CIndirectDeviceContext::PrepareFrame // Detect HDR metadata changes that require a format version bump // so the client knows to re-apply the HDR image description. // - // Use dstFormat: post-processing (CHDR16to10Effect) can rotate the gamut - // (BT.709 scRGB -> BT.2020 PQ), so the metadata describing the *emitted* - // frame lives in dstFormat, not srcFormat. Forwarding srcFormat here would - // tag PQ/BT.2020 pixels with the source's BT.709 primaries and mislead - // consumers that derive their colour conversion from these values. + // Use dstFormat so post-processing can propagate any metadata adjustments. if (dstFormat.hdr) { - if (!m_lastHDRActive || - memcmp(m_lastHDRDisplayPrimary, dstFormat.displayPrimary, sizeof(m_lastHDRDisplayPrimary)) != 0 || + const bool metadataChanged = + m_lastHDRMetadata != dstFormat.hdrMetadata || + (dstFormat.hdrMetadata && + (memcmp(m_lastHDRDisplayPrimary, dstFormat.displayPrimary, sizeof(m_lastHDRDisplayPrimary)) != 0 || memcmp(m_lastHDRWhitePoint , dstFormat.whitePoint , sizeof(m_lastHDRWhitePoint )) != 0 || m_lastHDRMaxDisplayLuminance != dstFormat.maxDisplayLuminance || m_lastHDRMinDisplayLuminance != dstFormat.minDisplayLuminance || m_lastHDRMaxContentLightLevel != dstFormat.maxContentLightLevel || - m_lastHDRMaxFrameAverageLightLevel != dstFormat.maxFrameAverageLightLevel || - m_lastSDRWhiteLevel != dstFormat.sdrWhiteLevel) - { + m_lastHDRMaxFrameAverageLightLevel != dstFormat.maxFrameAverageLightLevel)); + + if (!m_lastHDRActive || metadataChanged || m_lastSDRWhiteLevel != dstFormat.sdrWhiteLevel) ++m_formatVer; - } } else if (m_lastHDRActive) { @@ -1077,7 +1074,8 @@ CIndirectDeviceContext::PreparedFrameBuffer CIndirectDeviceContext::PrepareFrame ++m_formatVer; } - m_lastHDRActive = dstFormat.hdr; + m_lastHDRActive = dstFormat.hdr; + m_lastHDRMetadata = dstFormat.hdrMetadata; memcpy(m_lastHDRDisplayPrimary, dstFormat.displayPrimary, sizeof(m_lastHDRDisplayPrimary)); memcpy(m_lastHDRWhitePoint , dstFormat.whitePoint , sizeof(m_lastHDRWhitePoint )); m_lastHDRMaxDisplayLuminance = dstFormat.maxDisplayLuminance; @@ -1100,8 +1098,9 @@ CIndirectDeviceContext::PreparedFrameBuffer CIndirectDeviceContext::PrepareFrame const unsigned maxRows = (unsigned)(m_maxFrameSize / pitch); const int bpp = dstFormat.format == FRAME_TYPE_RGBA16F ? 8 : 4; KVMFRFrameFlags flags = - (dstFormat.hdr ? FRAME_FLAG_HDR : 0) | - (dstFormat.hdrPQ ? FRAME_FLAG_HDR_PQ : 0); + (dstFormat.hdr ? FRAME_FLAG_HDR : 0) | + (dstFormat.hdrPQ ? FRAME_FLAG_HDR_PQ : 0) | + (dstFormat.hdrMetadata ? FRAME_FLAG_HDR_METADATA : 0); if (maxRows < dstFormat.desc.Height) flags |= FRAME_FLAG_TRUNCATED; @@ -1122,10 +1121,7 @@ CIndirectDeviceContext::PreparedFrameBuffer CIndirectDeviceContext::PrepareFrame fi->rotation = FRAME_ROT_0; fi->type = dstFormat.format; - // Populate HDR metadata if the frame is HDR. Use dstFormat so the primaries - // describe the emitted frame after any gamut conversion in post-processing - // (see the metadata-change detection above). - if (flags & FRAME_FLAG_HDR) + if (flags & FRAME_FLAG_HDR_METADATA) { memcpy(fi->hdrDisplayPrimary, dstFormat.displayPrimary, sizeof(fi->hdrDisplayPrimary)); memcpy(fi->hdrWhitePoint , dstFormat.whitePoint , sizeof(fi->hdrWhitePoint)); @@ -1134,6 +1130,15 @@ CIndirectDeviceContext::PreparedFrameBuffer CIndirectDeviceContext::PrepareFrame fi->hdrMaxContentLightLevel = dstFormat.maxContentLightLevel; fi->hdrMaxFrameAverageLightLevel = dstFormat.maxFrameAverageLightLevel; } + else + { + memset(fi->hdrDisplayPrimary, 0, sizeof(fi->hdrDisplayPrimary)); + memset(fi->hdrWhitePoint , 0, sizeof(fi->hdrWhitePoint )); + fi->hdrMaxDisplayLuminance = 0; + fi->hdrMinDisplayLuminance = 0; + fi->hdrMaxContentLightLevel = 0; + fi->hdrMaxFrameAverageLightLevel = 0; + } fi->damageRectsCount = 0; if (nbDirtyRects <= ARRAYSIZE(fi->damageRects)) diff --git a/idd/LGIdd/CIndirectDeviceContext.h b/idd/LGIdd/CIndirectDeviceContext.h index e52ad0b1..0dab53b6 100644 --- a/idd/LGIdd/CIndirectDeviceContext.h +++ b/idd/LGIdd/CIndirectDeviceContext.h @@ -135,6 +135,7 @@ private: uint32_t m_lastHDRMaxFrameAverageLightLevel = 0; uint32_t m_lastSDRWhiteLevel = 0; bool m_lastHDRActive = false; + bool m_lastHDRMetadata = false; void QueryIddCxCapabilities(); bool CanUseIddCx110DDIs() const { return m_canProcessFP16; } diff --git a/idd/LGIdd/CPostProcessor.cpp b/idd/LGIdd/CPostProcessor.cpp index 1c2c68ba..2343541a 100644 --- a/idd/LGIdd/CPostProcessor.cpp +++ b/idd/LGIdd/CPostProcessor.cpp @@ -17,8 +17,21 @@ #include "effect/CHDR16to10Effect.h" #include "effect/CRGB24Effect.h" +#include #include +static void CopyHDRMetadata(D12FrameFormat& dst, const D12FrameFormat& src) +{ + dst.hdrMetadata = src.hdrMetadata; + dst.sdrWhiteLevel = src.sdrWhiteLevel; + std::memcpy(dst.displayPrimary, src.displayPrimary, sizeof(dst.displayPrimary)); + std::memcpy(dst.whitePoint, src.whitePoint, sizeof(dst.whitePoint)); + dst.maxDisplayLuminance = src.maxDisplayLuminance; + dst.minDisplayLuminance = src.minDisplayLuminance; + dst.maxContentLightLevel = src.maxContentLightLevel; + dst.maxFrameAverageLightLevel = src.maxFrameAverageLightLevel; +} + bool CPostProcessor::Init(std::shared_ptr dx12Device) { m_dx12Device = dx12Device; @@ -73,9 +86,14 @@ bool CPostProcessor::Configure(const D12FrameFormat& srcFormat, bool * formatCha srcFormat.width == m_srcFormat.width && srcFormat.height == m_srcFormat.height && srcFormat.hdr == m_srcFormat.hdr && - srcFormat.hdrPQ == m_srcFormat.hdrPQ && - srcFormat.sdrWhiteLevel == m_srcFormat.sdrWhiteLevel) + srcFormat.hdrPQ == m_srcFormat.hdrPQ) + { + // Static HDR metadata may change independently of the resource format. + // Propagate it without recreating textures or post-processing state. + CopyHDRMetadata(m_srcFormat, srcFormat); + CopyHDRMetadata(m_dstFormat, srcFormat); return true; + } D12FrameFormat oldDst = m_dstFormat; D12FrameFormat cur = srcFormat; diff --git a/idd/LGIdd/CPostProcessor.h b/idd/LGIdd/CPostProcessor.h index e1999abf..60d4cdc7 100644 --- a/idd/LGIdd/CPostProcessor.h +++ b/idd/LGIdd/CPostProcessor.h @@ -42,6 +42,7 @@ struct D12FrameFormat FrameType format = FRAME_TYPE_INVALID; bool hdr = false; bool hdrPQ = false; + bool hdrMetadata = false; uint32_t sdrWhiteLevel = KVMFR_SDR_WHITE_LEVEL_DEFAULT; // HDR static metadata (SMPTE ST 2086) @@ -49,9 +50,9 @@ struct D12FrameFormat uint16_t displayPrimary[3][2]; // White point in 0.00002 units uint16_t whitePoint[2]; - // Max display luminance in 0.0001 cd/m² units + // Max mastering display luminance in whole cd/m² uint32_t maxDisplayLuminance; - // Min display luminance in 0.0001 cd/m² units + // Min mastering display luminance in 0.0001 cd/m² units uint32_t minDisplayLuminance; // MaxCLL and MaxFALL in cd/m² uint32_t maxContentLightLevel; diff --git a/idd/LGIdd/CSwapChainProcessor.cpp b/idd/LGIdd/CSwapChainProcessor.cpp index 7bb9aa63..1729f5e6 100644 --- a/idd/LGIdd/CSwapChainProcessor.cpp +++ b/idd/LGIdd/CSwapChainProcessor.cpp @@ -206,6 +206,7 @@ bool CSwapChainProcessor::SwapChainThreadCore() surface = buffer.MetaData.pSurface; colorSpace = buffer.MetaData.SurfaceColorSpace; sdrWhiteLevel = buffer.MetaData.SdrWhiteLevel; + UpdateHDRMetadata(buffer.MetaData); } } else @@ -398,6 +399,72 @@ void CSwapChainProcessor::AccumulateFrameDamage( m_nbPendingDirtyRects += nbDirtyRects; } +#ifdef HAS_IDDCX_110 +void CSwapChainProcessor::UpdateHDRMetadata(const IDDCX_METADATA2& metadata) +{ + if (!(metadata.ValidFlags & IDDCX_METADATA2_VALID_FLAGS_HDR10METADATA)) + return; + + const IDDCX_HDR10_FRAME_METADATA& frame = metadata.Hdr10FrameMetaData; + switch (frame.Type) + { + case IDDCX_HDR10_FRAME_METADATA_TYPE_DEFAULT: + if (!m_useDefaultHDRMetadata) + DEBUG_TRACE("HDR10 frame metadata switched to the monitor default"); + m_useDefaultHDRMetadata = true; + m_hasNewHDRMetadata = false; + break; + + case IDDCX_HDR10_FRAME_METADATA_TYPE_UNCHANGED: + break; + + case IDDCX_HDR10_FRAME_METADATA_TYPE_NEW: + if (!m_hasNewHDRMetadata || + memcmp(&m_newHDRMetadata, &frame.NewMetaData, + sizeof(m_newHDRMetadata)) != 0) + DEBUG_TRACE("Received new HDR10 frame metadata"); + m_newHDRMetadata = frame.NewMetaData; + m_useDefaultHDRMetadata = false; + m_hasNewHDRMetadata = true; + break; + + default: + DEBUG_WARN("Invalid HDR10 frame metadata type %u", + static_cast(frame.Type)); + break; + } +} +#endif + +bool CSwapChainProcessor::GetHDRMetadata(D12FrameFormat& format) const +{ +#ifdef HAS_IDDCX_110 + if (m_useDefaultHDRMetadata) + return m_devContext->GetHDRMetadata(format); + + if (!m_hasNewHDRMetadata) + return false; + + const IDDCX_HDR10_METADATA& metadata = m_newHDRMetadata; + format.displayPrimary[0][0] = metadata.RedPrimary [0]; + format.displayPrimary[0][1] = metadata.RedPrimary [1]; + format.displayPrimary[1][0] = metadata.GreenPrimary[0]; + format.displayPrimary[1][1] = metadata.GreenPrimary[1]; + format.displayPrimary[2][0] = metadata.BluePrimary [0]; + format.displayPrimary[2][1] = metadata.BluePrimary [1]; + format.whitePoint [0] = metadata.WhitePoint [0]; + format.whitePoint [1] = metadata.WhitePoint [1]; + format.maxDisplayLuminance = metadata.MaxMasteringLuminance; + format.minDisplayLuminance = metadata.MinMasteringLuminance; + format.maxContentLightLevel = metadata.MaxContentLightLevel; + format.maxFrameAverageLightLevel = metadata.MaxFrameAverageLightLevel; + return true; +#else + UNREFERENCED_PARAMETER(format); + return false; +#endif +} + bool CSwapChainProcessor::SwapChainNewFrame(ComPtr acquiredBuffer, unsigned dirtyRectCount, DXGI_COLOR_SPACE_TYPE colorSpace, UINT sdrWhiteLevel) { @@ -480,7 +547,7 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr acquiredBuffer // already applied to the pixel data. srcFormat.hdr = true; srcFormat.hdrPQ = true; - if (!m_devContext->GetHDRMetadata(srcFormat)) + if (!GetHDRMetadata(srcFormat)) { // HDR is active but the OS has not delivered static metadata yet // (e.g. a brief window during a mode switch). The pixels are still @@ -504,6 +571,8 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr acquiredBuffer srcFormat.maxContentLightLevel = 0; srcFormat.maxFrameAverageLightLevel = 0; } + else + srcFormat.hdrMetadata = true; break; case DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709: @@ -511,7 +580,7 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr acquiredBuffer // curve has not been applied. srcFormat.hdr = true; srcFormat.hdrPQ = false; - if (!m_devContext->GetHDRMetadata(srcFormat)) + if (!GetHDRMetadata(srcFormat)) { // No HDR metadata from the OS; provide reasonable defaults // so downstream consumers have valid primaries and luminances. @@ -533,6 +602,8 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr acquiredBuffer srcFormat.maxContentLightLevel = 0; srcFormat.maxFrameAverageLightLevel = 0; } + else + srcFormat.hdrMetadata = true; break; default: diff --git a/idd/LGIdd/CSwapChainProcessor.h b/idd/LGIdd/CSwapChainProcessor.h index 92d06fac..c82c18f5 100644 --- a/idd/LGIdd/CSwapChainProcessor.h +++ b/idd/LGIdd/CSwapChainProcessor.h @@ -75,6 +75,14 @@ private: unsigned m_nbPendingDirtyRects = 0; bool m_hasPendingDamage = true; +#ifdef HAS_IDDCX_110 + // The per-frame metadata stream can select the monitor default, provide a + // replacement block, or retain the selection from the previous frame. + bool m_useDefaultHDRMetadata = true; + bool m_hasNewHDRMetadata = false; + IDDCX_HDR10_METADATA m_newHDRMetadata = {}; +#endif + static DWORD CALLBACK _SwapChainThread(LPVOID arg); void SwapChainThread(); bool SwapChainThreadCore(); @@ -87,6 +95,10 @@ private: CD3D12CommandQueue * queue, bool result, void * param1, void * param2); void AccumulateFrameDamage(const RECT * dirtyRects, unsigned nbDirtyRects); void SetFullPendingDamage(); +#ifdef HAS_IDDCX_110 + void UpdateHDRMetadata(const IDDCX_METADATA2& metadata); +#endif + bool GetHDRMetadata(D12FrameFormat& format) const; bool SwapChainNewFrame(ComPtr acquiredBuffer, unsigned dirtyRectCount, DXGI_COLOR_SPACE_TYPE colorSpace, UINT sdrWhiteLevel); diff --git a/idd/LGIdd/effect/CHDR16to10Effect.cpp b/idd/LGIdd/effect/CHDR16to10Effect.cpp index 5a202dd1..8b11dcca 100644 --- a/idd/LGIdd/effect/CHDR16to10Effect.cpp +++ b/idd/LGIdd/effect/CHDR16to10Effect.cpp @@ -123,18 +123,10 @@ PostProcessStatus CHDR16to10Effect::SetFormat( dst.hdr = true; dst.hdrPQ = true; - // Explicitly propagate HDR static metadata so it survives post-processing. - // Do not rely on the caller's copy semantics in CPostProcessor::Configure(). - // The shader rotates the gamut from BT.709 to BT.2020, so advertise BT.2020 - // mastering primaries here rather than forwarding the source's BT.709 set - // (in 0.00002 units) - otherwise consumers see PQ/BT.2020 content tagged - // with BT.709 primaries. - dst.displayPrimary[0][0] = 35400; // Rx - dst.displayPrimary[0][1] = 14600; // Ry - dst.displayPrimary[1][0] = 8500; // Gx - dst.displayPrimary[1][1] = 39850; // Gy - dst.displayPrimary[2][0] = 6550; // Bx - dst.displayPrimary[2][1] = 2300; // By + // Gamut conversion changes the signal's container primaries to BT.2020, but + // does not change the mastering display chromaticities described by ST 2086. + dst.hdrMetadata = src.hdrMetadata; + memcpy(dst.displayPrimary, src.displayPrimary, sizeof(dst.displayPrimary)); memcpy(dst.whitePoint , src.whitePoint , sizeof(dst.whitePoint )); dst.maxDisplayLuminance = src.maxDisplayLuminance; dst.minDisplayLuminance = src.minDisplayLuminance;