[idd] hdr: complete HDR display and cursor metadata

Publish BT.2020 chromaticities in the virtual monitor EDID so
Windows receives a valid HDR colour volume.

Extend cursor messages with their IddCx SDR white level and an
explicit visibility-valid flag. Consumers can then calibrate cursors
without treating colour-transform-only messages as cursor hides.
This commit is contained in:
Geoffrey McRae
2026-07-19 04:42:03 +10:00
parent e9d0c88764
commit 20225f05d1
7 changed files with 62 additions and 10 deletions

View File

@@ -28,7 +28,7 @@
#include "types.h"
#define KVMFR_MAGIC "KVMFR---"
#define KVMFR_VERSION 22
#define KVMFR_VERSION 23
// Fallback used by producers that cannot report the source display's SDR
// white level. IDD frames override this with IDDCX_METADATA2::SdrWhiteLevel.
@@ -54,7 +54,10 @@ enum
CURSOR_FLAG_POSITION = 0x1,
CURSOR_FLAG_VISIBLE = 0x2,
CURSOR_FLAG_SHAPE = 0x4,
CURSOR_FLAG_COLOR_TRANSFORM = 0x8
CURSOR_FLAG_COLOR_TRANSFORM = 0x8,
// CURSOR_FLAG_VISIBLE contains a new visibility state. This distinguishes
// an invisible cursor update from messages carrying unrelated cursor data.
CURSOR_FLAG_VISIBLE_VALID = 0x10
};
typedef uint32_t KVMFRCursorFlags;
@@ -135,6 +138,7 @@ typedef struct KVMFRCursor
uint32_t width; // width of the shape
uint32_t height; // height of the shape
uint32_t pitch; // row length in bytes of the shape
uint32_t sdrWhiteLevel; // cursor white level in nits for HDR composition
}
KVMFRCursor;

View File

@@ -515,8 +515,9 @@ static void sendPointer(bool newClient)
KVMFRCursor *cursor = lgmpHostMemPtr(mem);
cursor->x = app.pointerInfo.x;
cursor->y = app.pointerInfo.y;
cursor->sdrWhiteLevel = KVMFR_SDR_WHITE_LEVEL_DEFAULT;
const uint32_t flags = CURSOR_FLAG_POSITION |
const uint32_t flags = CURSOR_FLAG_POSITION | CURSOR_FLAG_VISIBLE_VALID |
(app.pointerShapeValid ? CURSOR_FLAG_SHAPE : 0) |
(app.pointerInfo.visible ? CURSOR_FLAG_VISIBLE : 0);
@@ -524,7 +525,7 @@ static void sendPointer(bool newClient)
return;
}
uint32_t flags = 0;
uint32_t flags = CURSOR_FLAG_VISIBLE_VALID;
PLGMPMemory mem;
if (app.pointerInfo.shapeUpdate)
{
@@ -539,6 +540,7 @@ static void sendPointer(bool newClient)
app.pointerIndex = 0;
}
KVMFRCursor *cursor = lgmpHostMemPtr(mem);
cursor->sdrWhiteLevel = KVMFR_SDR_WHITE_LEVEL_DEFAULT;
if (app.pointerInfo.positionUpdate || newClient)
{

View File

@@ -309,6 +309,41 @@ static EdidStandardTiming MakeUnusedStandardTiming()
return timing;
}
#include <algorithm> // Ensure this header is included for std::min and std::max
static WORD EdidChromaticity(double value)
{
return (WORD)min(1023.0, max(0.0, value * 1024.0 + 0.5));
}
static void SetChromaticityCoordinates(BYTE coordinates[10])
{
// The virtual display transports HDR in the BT.2020 container. Describe
// that full container gamut and its D65 white point rather than leaving an
// invalid all-zero base-block colour volume.
const WORD rx = EdidChromaticity(0.7080);
const WORD ry = EdidChromaticity(0.2920);
const WORD gx = EdidChromaticity(0.1700);
const WORD gy = EdidChromaticity(0.7970);
const WORD bx = EdidChromaticity(0.1310);
const WORD by = EdidChromaticity(0.0460);
const WORD wx = EdidChromaticity(0.3127);
const WORD wy = EdidChromaticity(0.3290);
coordinates[0] = (BYTE)(((rx & 3) << 6) | ((ry & 3) << 4) |
((gx & 3) << 2) | (gy & 3));
coordinates[1] = (BYTE)(((bx & 3) << 6) | ((by & 3) << 4) |
((wx & 3) << 2) | (wy & 3));
coordinates[2] = (BYTE)(rx >> 2);
coordinates[3] = (BYTE)(ry >> 2);
coordinates[4] = (BYTE)(gx >> 2);
coordinates[5] = (BYTE)(gy >> 2);
coordinates[6] = (BYTE)(bx >> 2);
coordinates[7] = (BYTE)(by >> 2);
coordinates[8] = (BYTE)(wx >> 2);
coordinates[9] = (BYTE)(wy >> 2);
}
static void InitEdidBaseBlock(EdidBaseBlock& base)
{
memcpy(base.header, EDID_HEADER, sizeof(base.header));
@@ -328,6 +363,7 @@ static void InitEdidBaseBlock(EdidBaseBlock& base)
base.verticalSizeCm = EDID_VERTICAL_SIZE_CM;
base.displayGamma = EDID_DISPLAY_GAMMA_2_2;
base.supportedFeatures = EDID_FEATURES_PREFERRED_TIMING_RGB;
SetChromaticityCoordinates(base.chromaticityCoordinates);
for (UINT i = 0; i < EDID_STANDARD_TIMING_COUNT; ++i)
base.standardTimings[i] = MakeUnusedStandardTiming();
@@ -612,4 +648,4 @@ void CEdid::Build(const CSettings::DisplayModes& modes)
SetChecksum(cta);
memcpy(m_data.data() + sizeof(baseBlock), &ctaBlock, sizeof(ctaBlock));
}
}

View File

@@ -1222,7 +1222,8 @@ void CIndirectDeviceContext::FinalizeFrameBuffer(unsigned frameIndex) const
fb->wp = m_height * m_pitch;
}
void CIndirectDeviceContext::SendCursor(const IDARG_OUT_QUERY_HWCURSOR& info, const BYTE * data)
void CIndirectDeviceContext::SendCursor(const IDARG_OUT_QUERY_HWCURSOR& info,
const BYTE * data, UINT sdrWhiteLevel)
{
PLGMPMemory mem;
if (info.CursorShapeInfo.CursorType == IDDCX_CURSOR_SHAPE_TYPE_UNINITIALIZED)
@@ -1239,9 +1240,11 @@ void CIndirectDeviceContext::SendCursor(const IDARG_OUT_QUERY_HWCURSOR& info, co
}
KVMFRCursor * cursor = (KVMFRCursor *)lgmpHostMemPtr(mem);
cursor->sdrWhiteLevel = sdrWhiteLevel ?
sdrWhiteLevel : KVMFR_SDR_WHITE_LEVEL_DEFAULT;
m_cursorVisible = info.IsCursorVisible;
uint32_t flags = 0;
uint32_t flags = CURSOR_FLAG_VISIBLE_VALID;
if (info.IsCursorVisible)
{
@@ -1416,7 +1419,7 @@ void CIndirectDeviceContext::ResendCursor()
cursor->y = (int16_t)m_cursorY;
const uint32_t flags =
CURSOR_FLAG_POSITION | CURSOR_FLAG_SHAPE |
CURSOR_FLAG_POSITION | CURSOR_FLAG_SHAPE | CURSOR_FLAG_VISIBLE_VALID |
(m_cursorVisible ? CURSOR_FLAG_VISIBLE : 0);
LGMP_STATUS status;

View File

@@ -225,7 +225,8 @@ public:
void WriteFrameBuffer(unsigned frameIndex, void* src, size_t offset, size_t len, bool setWritePos) const;
void FinalizeFrameBuffer(unsigned frameIndex) const;
void SendCursor(const IDARG_OUT_QUERY_HWCURSOR & info, const BYTE * data);
void SendCursor(const IDARG_OUT_QUERY_HWCURSOR & info, const BYTE * data,
UINT sdrWhiteLevel);
// Tracks HDR state from EvtIddCxMonitorSetDefaultHdrMetadata
void SetHDRActive(const struct IDDCX_HDR10_METADATA * hdrMeta);

View File

@@ -206,6 +206,7 @@ bool CSwapChainProcessor::SwapChainThreadCore()
surface = buffer.MetaData.pSurface;
colorSpace = buffer.MetaData.SurfaceColorSpace;
sdrWhiteLevel = buffer.MetaData.SdrWhiteLevel;
m_sdrWhiteLevel.store(sdrWhiteLevel, std::memory_order_relaxed);
UpdateHDRMetadata(buffer.MetaData);
}
}
@@ -776,6 +777,7 @@ bool CSwapChainProcessor::QueryHWCursor()
in.ShapeBufferSizeInBytes = 512 * 512 * 4;
IDARG_OUT_QUERY_HWCURSOR out = {};
UINT cursorWhiteLevel = m_sdrWhiteLevel.load(std::memory_order_relaxed);
NTSTATUS status;
#ifdef HAS_IDDCX_110
if (m_devContext->CanProcessFP16())
@@ -787,6 +789,8 @@ bool CSwapChainProcessor::QueryHWCursor()
out.Y = out3.Y;
out.IsCursorShapeUpdated = out3.IsCursorShapeUpdated;
out.CursorShapeInfo = out3.CursorShapeInfo;
if (out3.SdrWhiteLevel)
cursorWhiteLevel = out3.SdrWhiteLevel;
}
else
#endif
@@ -810,7 +814,7 @@ bool CSwapChainProcessor::QueryHWCursor()
if (out.IsCursorShapeUpdated)
m_lastShapeId = out.CursorShapeInfo.ShapeId;
m_devContext->SendCursor(out, m_shapeBuffer);
m_devContext->SendCursor(out, m_shapeBuffer, cursorWhiteLevel);
return true;
}

View File

@@ -30,6 +30,7 @@
#include <Windows.h>
#include <wrl.h>
#include <IddCx.h>
#include <atomic>
#include <memory>
using namespace Microsoft::WRL;
@@ -60,6 +61,7 @@ private:
Wrappers::Event m_cursorDataEvent;
BYTE* m_shapeBuffer;
DWORD m_lastShapeId = 0;
std::atomic<UINT> m_sdrWhiteLevel { KVMFR_SDR_WHITE_LEVEL_DEFAULT };
// Output-space damage from the previous published frame. The shared-memory
// frame buffers alternate, so this must be copied along with the current