[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.
This commit is contained in:
Geoffrey McRae
2026-07-19 02:13:57 +10:00
parent 624fa9f8bc
commit 1f6dbb0dae
8 changed files with 139 additions and 38 deletions

View File

@@ -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

View File

@@ -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))

View File

@@ -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; }

View File

@@ -17,8 +17,21 @@
#include "effect/CHDR16to10Effect.h"
#include "effect/CRGB24Effect.h"
#include <cstring>
#include <utility>
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<CD3D12Device> 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;

View File

@@ -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;

View File

@@ -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<unsigned>(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<IDXGIResource> acquiredBuffer, unsigned dirtyRectCount,
DXGI_COLOR_SPACE_TYPE colorSpace, UINT sdrWhiteLevel)
{
@@ -480,7 +547,7 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr<IDXGIResource> 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<IDXGIResource> 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<IDXGIResource> 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<IDXGIResource> acquiredBuffer
srcFormat.maxContentLightLevel = 0;
srcFormat.maxFrameAverageLightLevel = 0;
}
else
srcFormat.hdrMetadata = true;
break;
default:

View File

@@ -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<IDXGIResource> acquiredBuffer, unsigned dirtyRectCount,
DXGI_COLOR_SPACE_TYPE colorSpace, UINT sdrWhiteLevel);

View File

@@ -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;