[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" #include "types.h"
#define KVMFR_MAGIC "KVMFR---" #define KVMFR_MAGIC "KVMFR---"
#define KVMFR_VERSION 22 #define KVMFR_VERSION 23
// Fallback used by producers that cannot report the source display's SDR // Fallback used by producers that cannot report the source display's SDR
// white level. IDD frames override this with IDDCX_METADATA2::SdrWhiteLevel. // white level. IDD frames override this with IDDCX_METADATA2::SdrWhiteLevel.
@@ -54,7 +54,10 @@ enum
CURSOR_FLAG_POSITION = 0x1, CURSOR_FLAG_POSITION = 0x1,
CURSOR_FLAG_VISIBLE = 0x2, CURSOR_FLAG_VISIBLE = 0x2,
CURSOR_FLAG_SHAPE = 0x4, 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; typedef uint32_t KVMFRCursorFlags;
@@ -135,6 +138,7 @@ typedef struct KVMFRCursor
uint32_t width; // width of the shape uint32_t width; // width of the shape
uint32_t height; // height of the shape uint32_t height; // height of the shape
uint32_t pitch; // row length in bytes 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; KVMFRCursor;

View File

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

View File

@@ -309,6 +309,41 @@ static EdidStandardTiming MakeUnusedStandardTiming()
return timing; 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) static void InitEdidBaseBlock(EdidBaseBlock& base)
{ {
memcpy(base.header, EDID_HEADER, sizeof(base.header)); memcpy(base.header, EDID_HEADER, sizeof(base.header));
@@ -328,6 +363,7 @@ static void InitEdidBaseBlock(EdidBaseBlock& base)
base.verticalSizeCm = EDID_VERTICAL_SIZE_CM; base.verticalSizeCm = EDID_VERTICAL_SIZE_CM;
base.displayGamma = EDID_DISPLAY_GAMMA_2_2; base.displayGamma = EDID_DISPLAY_GAMMA_2_2;
base.supportedFeatures = EDID_FEATURES_PREFERRED_TIMING_RGB; base.supportedFeatures = EDID_FEATURES_PREFERRED_TIMING_RGB;
SetChromaticityCoordinates(base.chromaticityCoordinates);
for (UINT i = 0; i < EDID_STANDARD_TIMING_COUNT; ++i) for (UINT i = 0; i < EDID_STANDARD_TIMING_COUNT; ++i)
base.standardTimings[i] = MakeUnusedStandardTiming(); base.standardTimings[i] = MakeUnusedStandardTiming();

View File

@@ -1222,7 +1222,8 @@ void CIndirectDeviceContext::FinalizeFrameBuffer(unsigned frameIndex) const
fb->wp = m_height * m_pitch; 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; PLGMPMemory mem;
if (info.CursorShapeInfo.CursorType == IDDCX_CURSOR_SHAPE_TYPE_UNINITIALIZED) 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); KVMFRCursor * cursor = (KVMFRCursor *)lgmpHostMemPtr(mem);
cursor->sdrWhiteLevel = sdrWhiteLevel ?
sdrWhiteLevel : KVMFR_SDR_WHITE_LEVEL_DEFAULT;
m_cursorVisible = info.IsCursorVisible; m_cursorVisible = info.IsCursorVisible;
uint32_t flags = 0; uint32_t flags = CURSOR_FLAG_VISIBLE_VALID;
if (info.IsCursorVisible) if (info.IsCursorVisible)
{ {
@@ -1416,7 +1419,7 @@ void CIndirectDeviceContext::ResendCursor()
cursor->y = (int16_t)m_cursorY; cursor->y = (int16_t)m_cursorY;
const uint32_t flags = 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); (m_cursorVisible ? CURSOR_FLAG_VISIBLE : 0);
LGMP_STATUS status; 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 WriteFrameBuffer(unsigned frameIndex, void* src, size_t offset, size_t len, bool setWritePos) const;
void FinalizeFrameBuffer(unsigned frameIndex) 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 // Tracks HDR state from EvtIddCxMonitorSetDefaultHdrMetadata
void SetHDRActive(const struct IDDCX_HDR10_METADATA * hdrMeta); void SetHDRActive(const struct IDDCX_HDR10_METADATA * hdrMeta);

View File

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

View File

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