[idd/client] apply the Windows display color transform

Honor the IddCx 3x4 XYZ matrix and post-transfer LUT in the IDD
conversion pipeline. Carry transform changes to the client so EGL applies
the same calibration to hardware cursors, and invalidate the full frame
whenever calibration changes.
This commit is contained in:
Geoffrey McRae
2026-07-19 03:11:42 +10:00
parent ad5d7753e8
commit 26ca90893a
17 changed files with 766 additions and 48 deletions

View File

@@ -158,6 +158,11 @@ typedef struct LG_RendererOps
bool (*onMouseShape)(LG_Renderer * renderer, const LG_RendererCursor cursor,
const int width, const int height, const int pitch, const uint8_t * data);
/* optional display calibration update for hardware cursor composition
* Context: cursorThread */
void (*onMouseColorTransform)(LG_Renderer * renderer,
const KVMFRColorTransform * transform);
/* called when the mouse has moved or changed visibillity
* Context: cursorThread */
bool (*onMouseEvent)(LG_Renderer * renderer, const bool visible, int x, int y,

View File

@@ -46,8 +46,11 @@ struct CursorTex
EGL_Uniform * uScale;
EGL_Uniform * uRotate;
EGL_Uniform * uCBMode;
EGL_Uniform * uMapSDRtoPQ;
EGL_Uniform * uWireTransfer;
EGL_Uniform * uSDRWhiteLevel;
EGL_Uniform * uColorTransformFlags;
EGL_Uniform * uColorMatrix;
EGL_Uniform * uColorScalar;
};
struct CursorPos
@@ -80,9 +83,14 @@ struct EGL_Cursor
_Atomic(struct CursorPos) hs;
_Atomic(struct CursorSize) size;
_Atomic(float) scale;
_Atomic(bool) mapSDRtoPQ;
_Atomic(int) wireTransfer;
_Atomic(float) sdrWhiteLevel;
bool colorTransformUpdate;
KVMFRColorTransform pendingColorTransform;
KVMFRColorTransform activeColorTransform;
GLuint colorLUT;
struct CursorTex norm;
struct CursorTex mono;
struct EGL_Model * model;
@@ -115,8 +123,13 @@ static bool cursorTexInit(struct CursorTex * t,
t->uScale = egl_shaderGetUniform(t->shader, "scale" );
t->uRotate = egl_shaderGetUniform(t->shader, "rotate" );
t->uCBMode = egl_shaderGetUniform(t->shader, "cbMode" );
t->uMapSDRtoPQ = egl_shaderGetUniform(t->shader, "mapSDRtoPQ" );
t->uWireTransfer = egl_shaderGetUniform(t->shader, "wireTransfer" );
t->uSDRWhiteLevel = egl_shaderGetUniform(t->shader, "sdrWhiteLevel");
t->uColorTransformFlags =
egl_shaderGetUniform(t->shader, "colorTransformFlags");
t->uColorMatrix = egl_shaderGetUniform(t->shader, "colorMatrix[0]");
t->uColorScalar = egl_shaderGetUniform(t->shader, "colorTransformScalar");
egl_shaderAssocTextures(t->shader, 2);
return true;
}
@@ -129,8 +142,16 @@ static inline void setCursorTexUniforms(EGL_Cursor * cursor,
egl_uniform1f(t->uScale , scale);
egl_uniform1i(t->uRotate , cursor->rotate);
egl_uniform1i(t->uCBMode , cursor->cbMode);
egl_uniform1i(t->uMapSDRtoPQ , !mono && atomic_load(&cursor->mapSDRtoPQ));
egl_uniform1i(t->uWireTransfer,
mono ? 0 : atomic_load(&cursor->wireTransfer));
egl_uniform1f(t->uSDRWhiteLevel, atomic_load(&cursor->sdrWhiteLevel));
egl_uniform1ui(t->uColorTransformFlags,
mono ? 0 : cursor->activeColorTransform.flags);
egl_uniformSet(t->uColorMatrix, EGL_UNIFORM_TYPE_4FV, 3, GL_FALSE,
cursor->activeColorTransform.matrix);
egl_uniform1f(t->uColorScalar, cursor->activeColorTransform.scalar);
if (!mono && cursor->colorLUT)
egl_stateBindTexture(1, GL_TEXTURE_2D, cursor->colorLUT);
}
static void cursorTexFree(struct CursorTex * t)
@@ -178,7 +199,7 @@ bool egl_cursorInit(EGL_Cursor ** cursor)
atomic_init(&(*cursor)->hs , hs );
atomic_init(&(*cursor)->size , size );
atomic_init(&(*cursor)->scale , 1.0f );
atomic_init(&(*cursor)->mapSDRtoPQ, false);
atomic_init(&(*cursor)->wireTransfer, 0);
atomic_init(&(*cursor)->sdrWhiteLevel,
KVMFR_SDR_WHITE_LEVEL_DEFAULT);
@@ -196,6 +217,8 @@ void egl_cursorFree(EGL_Cursor ** cursor)
cursorTexFree(&(*cursor)->norm);
cursorTexFree(&(*cursor)->mono);
if ((*cursor)->colorLUT)
glDeleteTextures(1, &(*cursor)->colorLUT);
egl_modelFree(&(*cursor)->model);
free(*cursor);
@@ -222,6 +245,7 @@ bool egl_cursorSetShape(EGL_Cursor * cursor, const LG_RendererCursor type,
if (!cursor->data)
{
DEBUG_ERROR("Failed to malloc buffer for cursor shape");
LG_UNLOCK(cursor->lock);
return false;
}
@@ -235,6 +259,15 @@ bool egl_cursorSetShape(EGL_Cursor * cursor, const LG_RendererCursor type,
return true;
}
void egl_cursorSetColorTransform(EGL_Cursor * cursor,
const KVMFRColorTransform * transform)
{
LG_LOCK(cursor->lock);
cursor->pendingColorTransform = *transform;
cursor->colorTransformUpdate = true;
LG_UNLOCK(cursor->lock);
}
void egl_cursorSetSize(EGL_Cursor * cursor, const float w, const float h)
{
struct CursorSize size = { .w = w, .h = h };
@@ -262,14 +295,36 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor,
if (!cursor->visible)
return (struct CursorState) { .visible = false };
if (cursor->update)
if (cursor->update || cursor->colorTransformUpdate)
{
LG_LOCK(cursor->lock);
const bool updateShape = cursor->update;
cursor->update = false;
uint8_t * data = cursor->data;
switch(cursor->type)
if (cursor->colorTransformUpdate)
{
cursor->activeColorTransform = cursor->pendingColorTransform;
cursor->colorTransformUpdate = false;
if (cursor->activeColorTransform.flags & KVMFR_COLOR_TRANSFORM_LUT)
{
if (!cursor->colorLUT)
glGenTextures(1, &cursor->colorLUT);
egl_stateBindTexture(1, GL_TEXTURE_2D, cursor->colorLUT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 4096, 1, 0,
GL_RGBA, GL_FLOAT, cursor->activeColorTransform.lut);
}
}
if (updateShape)
{
uint8_t * data = cursor->data;
switch(cursor->type)
{
case LG_CURSOR_MASKED_COLOR:
{
uint32_t xor[cursor->height][cursor->width];
@@ -330,6 +385,7 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor,
egl_textureUpdate(cursor->mono.texture, (uint8_t *)xor, true);
break;
}
}
}
LG_UNLOCK(cursor->lock);
}
@@ -447,5 +503,5 @@ void egl_cursorSetHDRState(EGL_Cursor * cursor, bool hdrActive, bool hdrPQ,
{
atomic_store(&cursor->sdrWhiteLevel, sdrWhiteLevel > 0.0f ?
sdrWhiteLevel : KVMFR_SDR_WHITE_LEVEL_DEFAULT);
atomic_store(&cursor->mapSDRtoPQ, hdrActive && hdrPQ);
atomic_store(&cursor->wireTransfer, !hdrActive ? 0 : (hdrPQ ? 2 : 1));
}

View File

@@ -43,6 +43,9 @@ bool egl_cursorSetShape(
const int stride,
const uint8_t * data);
void egl_cursorSetColorTransform(EGL_Cursor * cursor,
const KVMFRColorTransform * transform);
void egl_cursorSetSize(EGL_Cursor * cursor, const float x, const float y);
void egl_cursorSetScale(EGL_Cursor * cursor, const float scale);

View File

@@ -577,6 +577,13 @@ static bool egl_onMouseShape(LG_Renderer * renderer, const LG_RendererCursor cur
return true;
}
static void egl_onMouseColorTransform(LG_Renderer * renderer,
const KVMFRColorTransform * transform)
{
struct Inst * this = UPCAST(struct Inst, renderer);
egl_cursorSetColorTransform(this->cursor, transform);
}
static bool egl_onMouseEvent(LG_Renderer * renderer, const bool visible,
int x, int y, const int hx, const int hy)
{
@@ -1438,22 +1445,23 @@ static void egl_spiceShow(LG_Renderer * renderer, bool show)
struct LG_RendererOps LGR_EGL =
{
.getName = egl_getName,
.setup = egl_setup,
.create = egl_create,
.initialize = egl_initialize,
.deinitialize = egl_deinitialize,
.supports = egl_supports,
.onRestart = egl_onRestart,
.onResize = egl_onResize,
.onMouseShape = egl_onMouseShape,
.onMouseEvent = egl_onMouseEvent,
.onFrameFormat = egl_onFrameFormat,
.onFrame = egl_onFrame,
.renderStartup = egl_renderStartup,
.render = egl_render,
.createTexture = egl_createTexture,
.freeTexture = egl_freeTexture,
.getName = egl_getName,
.setup = egl_setup,
.create = egl_create,
.initialize = egl_initialize,
.deinitialize = egl_deinitialize,
.supports = egl_supports,
.onRestart = egl_onRestart,
.onResize = egl_onResize,
.onMouseShape = egl_onMouseShape,
.onMouseColorTransform = egl_onMouseColorTransform,
.onMouseEvent = egl_onMouseEvent,
.onFrameFormat = egl_onFrameFormat,
.onFrame = egl_onFrame,
.renderStartup = egl_renderStartup,
.render = egl_render,
.createTexture = egl_createTexture,
.freeTexture = egl_freeTexture,
.spiceConfigure = egl_spiceConfigure,
.spiceDrawFill = egl_spiceDrawFill,

View File

@@ -9,10 +9,56 @@ in vec2 uv;
out vec4 color;
uniform sampler2D sampler1;
uniform sampler2D sampler2;
uniform float scale;
uniform int cbMode;
uniform bool mapSDRtoPQ;
uniform int wireTransfer; // 0: sRGB, 1: scRGB, 2: PQ
uniform float sdrWhiteLevel;
uniform uint colorTransformFlags;
uniform vec4 colorMatrix[3];
uniform float colorTransformScalar;
const uint COLOR_TRANSFORM_MATRIX = 1u;
const uint COLOR_TRANSFORM_LUT = 2u;
vec3 bt709ToXYZ(vec3 rgb)
{
return vec3(
dot(rgb, vec3(0.4123908, 0.3575843, 0.1804808)),
dot(rgb, vec3(0.2126390, 0.7151687, 0.0721923)),
dot(rgb, vec3(0.0193308, 0.1191948, 0.9505322)));
}
vec3 xyzToBT709(vec3 xyz)
{
return vec3(
dot(xyz, vec3( 3.2409699, -1.5373832, -0.4986108)),
dot(xyz, vec3(-0.9692436, 1.8759675, 0.0415551)),
dot(xyz, vec3( 0.0556301, -0.2039770, 1.0569715)));
}
vec3 xyzToBT2020(vec3 xyz)
{
return vec3(
dot(xyz, vec3( 1.7166512, -0.3556708, -0.2533663)),
dot(xyz, vec3(-0.6666844, 1.6164812, 0.0157685)),
dot(xyz, vec3( 0.0176399, -0.0427706, 0.9421031)));
}
vec3 applyColorLUT(vec3 value)
{
vec3 pos = clamp(value, 0.0, 1.0) * 4095.0;
ivec3 lo = ivec3(floor(pos));
ivec3 hi = min(lo + ivec3(1), ivec3(4095));
vec3 f = fract(pos);
return vec3(
mix(texelFetch(sampler2, ivec2(lo.r, 0), 0).r,
texelFetch(sampler2, ivec2(hi.r, 0), 0).r, f.r),
mix(texelFetch(sampler2, ivec2(lo.g, 0), 0).g,
texelFetch(sampler2, ivec2(hi.g, 0), 0).g, f.g),
mix(texelFetch(sampler2, ivec2(lo.b, 0), 0).b,
texelFetch(sampler2, ivec2(hi.b, 0), 0).b, f.b));
}
void main()
{
@@ -31,17 +77,35 @@ void main()
if (cbMode > 0)
color = cbTransform(color, cbMode);
if (mapSDRtoPQ)
if (color.a > 0.0 && (wireTransfer != 0 || colorTransformFlags != 0u))
{
if (color.a > 0.0)
// Cursor pixels are premultiplied sRGB. Work on straight, linear BT.709
// values through the XYZ calibration stage, encode for the active wire
// format, apply its 1D LUT, then restore premultiplication.
vec3 value = srgb2lin(clamp(color.rgb / color.a, 0.0, 1.0));
if ((colorTransformFlags & COLOR_TRANSFORM_MATRIX) != 0u)
{
// Cursor pixels are premultiplied. Convert the straight color to PQ,
// then premultiply again so the blend operation remains valid.
vec3 srgb = clamp(color.rgb / color.a, 0.0, 1.0);
vec3 linear = bt709to2020(srgb2lin(srgb));
color.rgb = lin2pq(linear * (sdrWhiteLevel / 10000.0)) * color.a;
vec3 xyz = bt709ToXYZ(value);
xyz = vec3(
dot(vec4(xyz, 1.0), colorMatrix[0]),
dot(vec4(xyz, 1.0), colorMatrix[1]),
dot(vec4(xyz, 1.0), colorMatrix[2])) * colorTransformScalar;
value = wireTransfer == 2 ? xyzToBT2020(xyz) : xyzToBT709(xyz);
}
else if (wireTransfer == 2)
value = bt709to2020(value);
if (wireTransfer == 2)
value = lin2pq(max(value, 0.0) * (sdrWhiteLevel / 10000.0));
else if (wireTransfer == 1)
value *= sdrWhiteLevel / 80.0;
else
color.rgb = vec3(0.0);
value = lin2srgb(max(value, 0.0));
if ((colorTransformFlags & COLOR_TRANSFORM_LUT) != 0u)
value = applyColorLUT(value);
color.rgb = value * color.a;
}
else if (color.a == 0.0)
color.rgb = vec3(0.0);
}

View File

@@ -431,8 +431,11 @@ int main_cursorThread(void * unused)
}
KVMFRCursor * tmp = (KVMFRCursor *)msg.mem;
const int neededSize = sizeof(*tmp) +
(msg.udata & CURSOR_FLAG_SHAPE ? tmp->height * tmp->pitch : 0);
const int shapeSize = msg.udata & CURSOR_FLAG_SHAPE ?
tmp->height * tmp->pitch : 0;
const int neededSize = sizeof(*tmp) + shapeSize +
(msg.udata & CURSOR_FLAG_COLOR_TRANSFORM ?
sizeof(KVMFRColorTransform) : 0);
if (cursor && neededSize > cursorSize)
{
@@ -488,6 +491,15 @@ int main_cursorThread(void * unused)
}
}
if ((msg.udata & CURSOR_FLAG_COLOR_TRANSFORM) &&
g_state.lgr->ops.onMouseColorTransform)
{
const KVMFRColorTransform * transform =
(const KVMFRColorTransform *)((const uint8_t *)(cursor + 1) +
shapeSize);
g_state.lgr->ops.onMouseColorTransform(g_state.lgr, transform);
}
if (msg.udata & CURSOR_FLAG_POSITION)
{
bool valid = g_cursor.guest.valid;
@@ -517,7 +529,9 @@ int main_cursorThread(void * unused)
g_cursor.guest.hy
);
if (g_params.mouseRedraw && g_cursor.guest.visible && !g_state.stopVideo)
if ((g_params.mouseRedraw ||
(msg.udata & CURSOR_FLAG_COLOR_TRANSFORM)) &&
g_cursor.guest.visible && !g_state.stopVideo)
lgSignalEvent(g_state.frameEvent);
}

View File

@@ -28,7 +28,7 @@
#include "types.h"
#define KVMFR_MAGIC "KVMFR---"
#define KVMFR_VERSION 21
#define KVMFR_VERSION 22
// Fallback used by producers that cannot report the source display's SDR
// white level. IDD frames override this with IDDCX_METADATA2::SdrWhiteLevel.
@@ -51,9 +51,10 @@
enum
{
CURSOR_FLAG_POSITION = 0x1,
CURSOR_FLAG_VISIBLE = 0x2,
CURSOR_FLAG_SHAPE = 0x4
CURSOR_FLAG_POSITION = 0x1,
CURSOR_FLAG_VISIBLE = 0x2,
CURSOR_FLAG_SHAPE = 0x4,
CURSOR_FLAG_COLOR_TRANSFORM = 0x8
};
typedef uint32_t KVMFRCursorFlags;
@@ -137,6 +138,28 @@ typedef struct KVMFRCursor
}
KVMFRCursor;
enum
{
KVMFR_COLOR_TRANSFORM_MATRIX = 0x1,
KVMFR_COLOR_TRANSFORM_LUT = 0x2,
};
typedef uint32_t KVMFRColorTransformFlags;
// Optional payload appended to KVMFRCursor when
// CURSOR_FLAG_COLOR_TRANSFORM is present. The matrix is an XYZ-to-XYZ
// adjustment; the LUT is applied after encoding to the active wire transfer
// function. Four LUT components keep the payload directly uploadable as an
// RGBA32F texture, with alpha reserved and set to 1.0.
typedef struct KVMFRColorTransform
{
KVMFRColorTransformFlags flags;
float matrix[3][4];
float scalar;
float lut[4096][4];
}
KVMFRColorTransform;
enum
{
FRAME_FLAG_BLOCK_SCREENSAVER = 0x1 ,

View File

@@ -868,6 +868,20 @@ bool CIndirectDeviceContext::SetupLGMP(size_t alignSize)
memset(lgmpHostMemPtr(m_pointerShapeMemory[i]), 0, MAX_POINTER_SIZE);
}
for (int i = 0; i < COLOR_TRANSFORM_BUFFERS; ++i)
{
if ((status = lgmpHostMemAlloc(m_lgmp,
sizeof(KVMFRCursor) + sizeof(KVMFRColorTransform),
&m_pointerTransformMemory[i])) != LGMP_OK)
{
DEBUG_ERROR("lgmpHostMemAlloc Failed (Pointer Transform): %s",
lgmpStatusString(status));
return false;
}
memset(lgmpHostMemPtr(m_pointerTransformMemory[i]), 0,
sizeof(KVMFRCursor) + sizeof(KVMFRColorTransform));
}
m_maxFrameSize = lgmpHostMemAvail(m_lgmp);
m_maxFrameSize = (m_maxFrameSize -(m_alignSize - 1)) & ~(m_alignSize - 1);
m_maxFrameSize /= LGMP_Q_FRAME_LEN;
@@ -951,6 +965,9 @@ void CIndirectDeviceContext::DeInitLGMP()
lgmpHostMemFree(&m_pointerMemory[i]);
for (int i = 0; i < POINTER_SHAPE_BUFFERS; ++i)
lgmpHostMemFree(&m_pointerShapeMemory[i]);
for (int i = 0; i < COLOR_TRANSFORM_BUFFERS; ++i)
lgmpHostMemFree(&m_pointerTransformMemory[i]);
lgmpHostFree(&m_lgmp);
}
@@ -1017,7 +1034,10 @@ void CIndirectDeviceContext::LGMPTimer()
}
if (lgmpHostQueueNewSubs(m_pointerQueue))
{
ResendCursor();
SendColorTransform();
}
}
bool CIndirectDeviceContext::FrameBufferAvailable() const
@@ -1272,6 +1292,24 @@ void CIndirectDeviceContext::SendCursor(const IDARG_OUT_QUERY_HWCURSOR& info, co
}
}
void CIndirectDeviceContext::SetColorTransform(
std::shared_ptr<const D12ColorTransform> transform)
{
AcquireSRWLockExclusive(&m_colorTransformLock);
m_colorTransform = std::move(transform);
ReleaseSRWLockExclusive(&m_colorTransformLock);
SendColorTransform();
}
std::shared_ptr<const D12ColorTransform>
CIndirectDeviceContext::GetColorTransform() const
{
AcquireSRWLockShared(&m_colorTransformLock);
auto transform = m_colorTransform;
ReleaseSRWLockShared(&m_colorTransformLock);
return transform;
}
#ifdef HAS_IDDCX_110
void CIndirectDeviceContext::SetHDRActive(const struct IDDCX_HDR10_METADATA * hdrMeta)
{
@@ -1325,7 +1363,49 @@ bool CIndirectDeviceContext::GetHDRMetadata(D12FrameFormat & format) const
return true;
}
void CIndirectDeviceContext::ResendCursor() const
void CIndirectDeviceContext::SendColorTransform()
{
if (!m_pointerQueue || !m_pointerTransformMemory[0])
return;
PLGMPMemory mem = m_pointerTransformMemory[m_pointerTransformIndex];
if (++m_pointerTransformIndex == COLOR_TRANSFORM_BUFFERS)
m_pointerTransformIndex = 0;
KVMFRCursor * cursor = (KVMFRCursor *)lgmpHostMemPtr(mem);
KVMFRColorTransform * output =
(KVMFRColorTransform *)(cursor + 1);
const auto transform = GetColorTransform();
output->flags = 0;
if (transform)
{
if (transform->matrixEnabled)
output->flags |= KVMFR_COLOR_TRANSFORM_MATRIX;
if (transform->lutEnabled)
output->flags |= KVMFR_COLOR_TRANSFORM_LUT;
memcpy(output->matrix, transform->matrix, sizeof(output->matrix));
output->scalar = transform->scalar;
memcpy(output->lut, transform->lut, sizeof(output->lut));
}
LGMP_STATUS status;
while ((status = lgmpHostQueuePost(m_pointerQueue,
CURSOR_FLAG_COLOR_TRANSFORM, mem)) != LGMP_OK)
{
if (status == LGMP_ERR_QUEUE_FULL)
{
Sleep(1);
continue;
}
DEBUG_ERROR("lgmpHostQueuePost Failed (Pointer Transform): %s",
lgmpStatusString(status));
break;
}
}
void CIndirectDeviceContext::ResendCursor()
{
PLGMPMemory mem = m_pointerShape;
if (!mem)

View File

@@ -45,6 +45,7 @@ extern "C" {
#endif
#define MAX_POINTER_SIZE (sizeof(KVMFRCursor) + (512 * 512 * 4))
#define COLOR_TRANSFORM_BUFFERS 3
#define POINTER_SHAPE_BUFFERS 3
//FIXME: this should not really be done here, this is a hack
@@ -90,9 +91,11 @@ private:
PLGMPHostQueue m_pointerQueue = nullptr;
PLGMPMemory m_pointerMemory [LGMP_Q_POINTER_LEN ] = {};
PLGMPMemory m_pointerShapeMemory[POINTER_SHAPE_BUFFERS] = {};
PLGMPMemory m_pointerTransformMemory[COLOR_TRANSFORM_BUFFERS] = {};
PLGMPMemory m_pointerShape = nullptr;
int m_pointerMemoryIndex = 0;
int m_pointerShapeIndex = 0;
int m_pointerTransformIndex = 0;
bool m_cursorVisible = false;
int m_cursorX = 0, m_cursorY = 0;
@@ -137,6 +140,12 @@ private:
bool m_lastHDRActive = false;
bool m_lastHDRMetadata = false;
// Windows display calibration transform. The callback publishes immutable
// snapshots so the swap-chain thread never observes a partially updated
// matrix or LUT.
mutable SRWLOCK m_colorTransformLock = SRWLOCK_INIT;
std::shared_ptr<const D12ColorTransform> m_colorTransform;
void QueryIddCxCapabilities();
bool CanUseIddCx110DDIs() const { return m_canProcessFP16; }
@@ -145,7 +154,8 @@ private:
void DeInitLGMP();
void LGMPTimer();
void ResendCursor() const;
void ResendCursor();
void SendColorTransform();
void InitializeEdid();
// Guards m_displayModes and m_edid. The mode list is rebuilt on the LGMP
@@ -220,6 +230,9 @@ public:
// Tracks HDR state from EvtIddCxMonitorSetDefaultHdrMetadata
void SetHDRActive(const struct IDDCX_HDR10_METADATA * hdrMeta);
void SetColorTransform(std::shared_ptr<const D12ColorTransform> transform);
std::shared_ptr<const D12ColorTransform> GetColorTransform() const;
// Returns true if the display is currently in HDR mode
bool IsHDRActive() const
{

View File

@@ -13,6 +13,7 @@
#include "CD3D12Device.h"
#include "CDebug.h"
#include "effect/CColorTransformEffect.h"
#include "effect/CDownsampleEffect.h"
#include "effect/CHDR16to10Effect.h"
#include "effect/CRGB24Effect.h"
@@ -38,6 +39,15 @@ bool CPostProcessor::Init(std::shared_ptr<CD3D12Device> dx12Device)
m_device = dx12Device->GetDevice();
m_effects.clear();
std::unique_ptr<CColorTransformEffect> colorTransform(new CColorTransformEffect());
if (colorTransform->Init(m_device))
{
DEBUG_INFO("Created post-processing effect: %s", colorTransform->GetName());
m_effects.push_back(std::move(colorTransform));
}
else
return false;
std::unique_ptr<CDownsampleEffect> downsample(new CDownsampleEffect());
if (downsample->Init(m_device))
{
@@ -86,7 +96,8 @@ 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.hdrPQ == m_srcFormat.hdrPQ &&
srcFormat.colorTransform == m_srcFormat.colorTransform)
{
// Static HDR metadata may change independently of the resource format.
// Propagate it without recreating textures or post-processing state.
@@ -132,7 +143,8 @@ bool CPostProcessor::Configure(const D12FrameFormat& srcFormat, bool * formatCha
oldDst.height != m_dstFormat.height ||
oldDst.hdr != m_dstFormat.hdr ||
oldDst.hdrPQ != m_dstFormat.hdrPQ ||
oldDst.sdrWhiteLevel != m_dstFormat.sdrWhiteLevel;
oldDst.sdrWhiteLevel != m_dstFormat.sdrWhiteLevel ||
oldDst.colorTransform != m_dstFormat.colorTransform;
return true;
}

View File

@@ -34,6 +34,15 @@ enum class PostProcessStatus
FAILED
};
struct D12ColorTransform
{
bool matrixEnabled = false;
float matrix[3][4] = {};
float scalar = 1.0f;
bool lutEnabled = false;
float lut[4096][4] = {};
};
struct D12FrameFormat
{
D3D12_RESOURCE_DESC desc = {};
@@ -44,6 +53,7 @@ struct D12FrameFormat
bool hdrPQ = false;
bool hdrMetadata = false;
uint32_t sdrWhiteLevel = KVMFR_SDR_WHITE_LEVEL_DEFAULT;
std::shared_ptr<const D12ColorTransform> colorTransform;
// HDR static metadata (SMPTE ST 2086)
// Display color primaries in 0.00002 units (xy coordinates)

View File

@@ -538,6 +538,7 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr<IDXGIResource> acquiredBuffer
srcFormat.height = srcDesc.Height;
srcFormat.format = GetFrameType(srcDesc.Format);
srcFormat.sdrWhiteLevel = sdrWhiteLevel;
srcFormat.colorTransform = m_devContext->GetColorTransform();
switch (colorSpace)
{

View File

@@ -28,6 +28,8 @@
#include <IddCx.h>
#include <avrt.h>
#include <wrl.h>
#include <memory>
#include <utility>
#include "CDebug.h"
#include "CIndirectDeviceContext.h"
@@ -192,8 +194,51 @@ NTSTATUS LGIddMonitorSetDefaultHdrMetadata(IDDCX_MONITOR monitor,
NTSTATUS LGIddMonitorSetGammaRamp(IDDCX_MONITOR monitor, const IDARG_IN_SET_GAMMARAMP* inArgs)
{
UNREFERENCED_PARAMETER(monitor);
UNREFERENCED_PARAMETER(inArgs);
auto* wrapper = WdfObjectGet_CIndirectMonitorContextWrapper(monitor);
auto* ctx = wrapper->context->GetDeviceContext();
if (inArgs->Type == IDDCX_GAMMARAMP_TYPE_DEFAULT)
{
ctx->SetColorTransform(nullptr);
DEBUG_INFO("Display color transform reset to default");
return STATUS_SUCCESS;
}
if (inArgs->Type != IDDCX_GAMMARAMP_TYPE_3x4_COLORSPACE_TRANSFORM)
{
DEBUG_WARN("Unsupported gamma ramp type %u", inArgs->Type);
return STATUS_NOT_SUPPORTED;
}
if (!inArgs->pGammaRampData ||
inArgs->GammaRampSizeInBytes <
sizeof(IDDCX_GAMMARAMP_3X4_COLORSPACE_TRANSFORM))
{
DEBUG_ERROR("Invalid 3x4 color transform buffer (%u bytes)",
inArgs->GammaRampSizeInBytes);
return STATUS_INVALID_PARAMETER;
}
const auto* input = static_cast<
const IDDCX_GAMMARAMP_3X4_COLORSPACE_TRANSFORM*>(
inArgs->pGammaRampData);
auto transform = std::make_shared<D12ColorTransform>();
transform->matrixEnabled = !!input->MatrixEnabled;
transform->scalar = input->ScalarMultiplier;
transform->lutEnabled = !!input->LutEnabled;
memcpy(transform->matrix, input->ColorMatrix3x4,
sizeof(transform->matrix));
for (unsigned i = 0; i < 4096; ++i)
{
transform->lut[i][0] = input->LookupTable1D[i].Red;
transform->lut[i][1] = input->LookupTable1D[i].Green;
transform->lut[i][2] = input->LookupTable1D[i].Blue;
transform->lut[i][3] = 1.0f;
}
ctx->SetColorTransform(std::move(transform));
DEBUG_INFO("Display color transform updated (matrix:%d lut:%d)",
input->MatrixEnabled, input->LutEnabled);
return STATUS_SUCCESS;
}

View File

@@ -37,6 +37,7 @@
<ClCompile Include="CPipeServer.cpp" />
<ClCompile Include="CPlatformInfo.cpp" />
<ClCompile Include="CPostProcessor.cpp" />
<ClCompile Include="effect\CColorTransformEffect.cpp" />
<ClCompile Include="effect\CComputeEffect.cpp" />
<ClCompile Include="effect\CDownsampleEffect.cpp" />
<ClCompile Include="effect\CHDR16to10Effect.cpp" />
@@ -61,6 +62,7 @@
<ClInclude Include="CPipeServer.h" />
<ClInclude Include="CPlatformInfo.h" />
<ClInclude Include="CPostProcessor.h" />
<ClInclude Include="effect\CColorTransformEffect.h" />
<ClInclude Include="effect\CComputeEffect.h" />
<ClInclude Include="effect\CDownsampleEffect.h" />
<ClInclude Include="effect\CHDR16to10Effect.h" />
@@ -274,4 +276,4 @@
$(BuildDependsOn);
</BuildDependsOn>
</PropertyGroup>
</Project>
</Project>

View File

@@ -94,6 +94,9 @@
<ClInclude Include="CPostProcessor.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="effect\CColorTransformEffect.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="effect\CComputeEffect.h">
<Filter>Header Files\effect</Filter>
</ClInclude>
@@ -163,6 +166,9 @@
<ClCompile Include="CPostProcessor.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="effect\CColorTransformEffect.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="effect\CComputeEffect.cpp">
<Filter>Source Files\effect</Filter>
</ClCompile>

View File

@@ -0,0 +1,328 @@
/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*/
#include "CColorTransformEffect.h"
#include "CDebug.h"
#include <cstring>
using namespace PostProcessUtil;
namespace
{
enum TransferFunction : UINT
{
TRANSFER_LINEAR,
TRANSFER_SRGB,
TRANSFER_PQ,
};
bool CreateUploadBuffer(const ComPtr<ID3D12Device3>& device, size_t size,
ComPtr<ID3D12Resource>& resource)
{
D3D12_HEAP_PROPERTIES heapProps = {};
heapProps.Type = D3D12_HEAP_TYPE_UPLOAD;
D3D12_RESOURCE_DESC desc = {};
desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
desc.Width = size;
desc.Height = 1;
desc.DepthOrArraySize = 1;
desc.MipLevels = 1;
desc.SampleDesc.Count = 1;
desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
const HRESULT hr = device->CreateCommittedResource(&heapProps,
D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr, IID_PPV_ARGS(&resource));
return SUCCEEDED(hr);
}
bool Upload(const ComPtr<ID3D12Resource>& resource,
const void * data, size_t size)
{
void * dst = nullptr;
const D3D12_RANGE readRange = { 0, 0 };
if (FAILED(resource->Map(0, &readRange, &dst)))
return false;
std::memcpy(dst, data, size);
resource->Unmap(0, nullptr);
return true;
}
}
bool CColorTransformEffect::Init(const ComPtr<ID3D12Device3>& device)
{
D3D12_DESCRIPTOR_RANGE ranges[4] = {};
ranges[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
ranges[0].NumDescriptors = 1;
ranges[0].BaseShaderRegister = 0;
ranges[0].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
ranges[1].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
ranges[1].NumDescriptors = 1;
ranges[1].BaseShaderRegister = 0;
ranges[1].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
ranges[2].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
ranges[2].NumDescriptors = 1;
ranges[2].BaseShaderRegister = 1;
ranges[2].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
ranges[3].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_UAV;
ranges[3].NumDescriptors = 1;
ranges[3].BaseShaderRegister = 0;
ranges[3].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
const char * shader =
"cbuffer Constants : register(b0)\n"
"{\n"
" float4 ColorMatrix[3];\n"
" float Scalar;\n"
" uint MatrixEnabled;\n"
" uint LutEnabled;\n"
" uint InputTransfer;\n"
" uint OutputTransfer;\n"
"};\n"
"Texture2D<float4> src : register(t0);\n"
"Buffer<float4> lut : register(t1);\n"
"RWTexture2D<float4> dst : register(u0);\n"
"static const float PQ_m1 = 0.1593017578125;\n"
"static const float PQ_m2 = 78.84375;\n"
"static const float PQ_c1 = 0.8359375;\n"
"static const float PQ_c2 = 18.8515625;\n"
"static const float PQ_c3 = 18.6875;\n"
"float3 decode(float3 value)\n"
"{\n"
" if (InputTransfer == 1)\n"
" return lerp(value / 12.92, pow((value + 0.055) / 1.055, 2.4),\n"
" step(0.04045, value));\n"
" if (InputTransfer == 2)\n"
" {\n"
" float3 p = pow(max(value, 0.0), 1.0 / PQ_m2);\n"
" return pow(max(p - PQ_c1, 0.0) / max(PQ_c2 - PQ_c3 * p, 1e-6),\n"
" 1.0 / PQ_m1);\n"
" }\n"
" return value;\n"
"}\n"
"float3 encode(float3 value)\n"
"{\n"
" if (OutputTransfer == 1)\n"
" return lerp(value * 12.92, 1.055 * pow(max(value, 0.0), 1.0 / 2.4) - 0.055,\n"
" step(0.0031308, value));\n"
" if (OutputTransfer == 2)\n"
" {\n"
" float3 p = pow(max(value, 0.0), PQ_m1);\n"
" return pow((PQ_c1 + PQ_c2 * p) / (1.0 + PQ_c3 * p), PQ_m2);\n"
" }\n"
" return value;\n"
"}\n"
"float3 rgbToXYZ(float3 rgb)\n"
"{\n"
" if (InputTransfer == 2)\n"
" return float3(\n"
" dot(rgb, float3(0.6369580, 0.1446169, 0.1688810)),\n"
" dot(rgb, float3(0.2627002, 0.6779981, 0.0593017)),\n"
" dot(rgb, float3(0.0000000, 0.0280727, 1.0609851)));\n"
" return float3(\n"
" dot(rgb, float3(0.4123908, 0.3575843, 0.1804808)),\n"
" dot(rgb, float3(0.2126390, 0.7151687, 0.0721923)),\n"
" dot(rgb, float3(0.0193308, 0.1191948, 0.9505322)));\n"
"}\n"
"float3 xyzToRGB(float3 xyz)\n"
"{\n"
" if (OutputTransfer == 2)\n"
" return float3(\n"
" dot(xyz, float3( 1.7166512, -0.3556708, -0.2533663)),\n"
" dot(xyz, float3(-0.6666844, 1.6164812, 0.0157685)),\n"
" dot(xyz, float3( 0.0176399, -0.0427706, 0.9421031)));\n"
" return float3(\n"
" dot(xyz, float3( 3.2409699, -1.5373832, -0.4986108)),\n"
" dot(xyz, float3(-0.9692436, 1.8759675, 0.0415551)),\n"
" dot(xyz, float3( 0.0556301, -0.2039770, 1.0569715)));\n"
"}\n"
"float3 applyLut(float3 value)\n"
"{\n"
" float3 pos = saturate(value) * 4095.0;\n"
" uint3 lo = (uint3)floor(pos);\n"
" uint3 hi = min(lo + 1, 4095);\n"
" float3 f = frac(pos);\n"
" return float3(\n"
" lerp(lut[lo.r].r, lut[hi.r].r, f.r),\n"
" lerp(lut[lo.g].g, lut[hi.g].g, f.g),\n"
" lerp(lut[lo.b].b, lut[hi.b].b, f.b));\n"
"}\n"
"[numthreads(" POST_PROCESS_THREADS_STR ", " POST_PROCESS_THREADS_STR ", 1)]\n"
"void main(uint3 dt : SV_DispatchThreadID)\n"
"{\n"
" float4 pixel = src[dt.xy];\n"
" float3 value = decode(pixel.rgb);\n"
" if (MatrixEnabled != 0 || InputTransfer != OutputTransfer)\n"
" {\n"
" float3 xyz = rgbToXYZ(value);\n"
" if (MatrixEnabled != 0)\n"
" xyz = float3(dot(float4(xyz, 1.0), ColorMatrix[0]),\n"
" dot(float4(xyz, 1.0), ColorMatrix[1]),\n"
" dot(float4(xyz, 1.0), ColorMatrix[2])) * Scalar;\n"
" value = xyzToRGB(xyz);\n"
" }\n"
" if (InputTransfer == 0 && OutputTransfer == 2)\n"
" value *= 80.0 / 10000.0;\n"
" value = encode(value);\n"
" if (LutEnabled != 0)\n"
" value = applyLut(value);\n"
" dst[dt.xy] = float4(value, pixel.a);\n"
"}\n";
if (!InitCompute(device, ranges, ARRAYSIZE(ranges), nullptr, 0, shader))
return false;
const size_t constSize = AlignTo(sizeof(m_consts),
(size_t)D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT);
if (!CreateUploadBuffer(device, constSize, m_constBuffer) ||
!CreateUploadBuffer(device, sizeof(float) * 4096 * 4, m_lutBuffer))
{
DEBUG_ERROR("Failed to create color transform buffers");
return false;
}
return true;
}
PostProcessStatus CColorTransformEffect::SetFormat(
const ComPtr<ID3D12Device3>& device,
const D12FrameFormat& src, D12FrameFormat& dst)
{
if (!src.colorTransform ||
(!src.colorTransform->matrixEnabled && !src.colorTransform->lutEnabled))
return PostProcessStatus::BYPASS_EFFECT;
DXGI_FORMAT dstFormat;
FrameType frameType;
switch (src.desc.Format)
{
case DXGI_FORMAT_B8G8R8A8_UNORM:
dstFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
frameType = FRAME_TYPE_RGBA;
break;
case DXGI_FORMAT_R8G8B8A8_UNORM:
case DXGI_FORMAT_R10G10B10A2_UNORM:
dstFormat = src.desc.Format;
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;
break;
default:
DEBUG_ERROR("Unsupported color transform source format %u", src.desc.Format);
return PostProcessStatus::FAILED;
}
D3D12_RESOURCE_DESC desc = src.desc;
desc.Format = dstFormat;
desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
if (!m_dst || m_dst->GetDesc().Width != desc.Width ||
m_dst->GetDesc().Height != desc.Height ||
m_dst->GetDesc().Format != desc.Format)
{
if (!CreateDefaultTexture(device, desc, m_dst))
return PostProcessStatus::FAILED;
}
std::memcpy(m_consts.matrix, src.colorTransform->matrix,
sizeof(m_consts.matrix));
m_consts.scalar = src.colorTransform->scalar;
m_consts.matrixEnabled = src.colorTransform->matrixEnabled;
m_consts.lutEnabled = src.colorTransform->lutEnabled;
m_consts.inputTransfer = src.hdrPQ ? TRANSFER_PQ :
(src.hdr ? TRANSFER_LINEAR : TRANSFER_SRGB);
m_consts.outputTransfer = src.hdr ? TRANSFER_PQ : TRANSFER_SRGB;
std::memcpy(m_lut, src.colorTransform->lut, sizeof(m_lut));
m_uploadPending = true;
m_srcFormat = src.desc.Format;
m_dstFormat = dstFormat;
m_threadsX = ((unsigned)desc.Width + (Threads - 1)) / Threads;
m_threadsY = ((unsigned)desc.Height + (Threads - 1)) / Threads;
dst.desc = desc;
dst.format = frameType;
if (src.hdr)
dst.hdrPQ = true;
return PostProcessStatus::SUCCESS;
}
ComPtr<ID3D12Resource> CColorTransformEffect::Run(
const ComPtr<ID3D12Device3>& device,
const ComPtr<ID3D12GraphicsCommandList>& commandList,
const ComPtr<ID3D12Resource>& src, RECT dirtyRects[],
unsigned * nbDirtyRects)
{
UNREFERENCED_PARAMETER(dirtyRects);
UNREFERENCED_PARAMETER(nbDirtyRects);
// GetComputeQueue waits for the previous submission before Run is called,
// so this is the first point where the shared upload buffers are guaranteed
// not to be in use by the GPU.
if (m_uploadPending)
{
if (!Upload(m_constBuffer, &m_consts, sizeof(m_consts)) ||
!Upload(m_lutBuffer, m_lut, sizeof(m_lut)))
DEBUG_ERROR("Failed to upload display color transform");
else
m_uploadPending = false;
}
TransitionDst(commandList, D3D12_RESOURCE_STATE_COPY_SOURCE,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
D3D12_CPU_DESCRIPTOR_HANDLE handle =
m_descHeap->GetCPUDescriptorHandleForHeapStart();
const UINT inc = device->GetDescriptorHandleIncrementSize(
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
cbvDesc.BufferLocation = m_constBuffer->GetGPUVirtualAddress();
cbvDesc.SizeInBytes = (UINT)AlignTo(sizeof(m_consts),
(size_t)D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT);
device->CreateConstantBufferView(&cbvDesc, handle);
handle.ptr += inc;
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Format = m_srcFormat;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Texture2D.MipLevels = 1;
device->CreateShaderResourceView(src.Get(), &srvDesc, handle);
handle.ptr += inc;
D3D12_SHADER_RESOURCE_VIEW_DESC lutDesc = {};
lutDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
lutDesc.ViewDimension = D3D12_SRV_DIMENSION_BUFFER;
lutDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
lutDesc.Buffer.NumElements = 4096;
device->CreateShaderResourceView(m_lutBuffer.Get(), &lutDesc, handle);
handle.ptr += inc;
D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {};
uavDesc.Format = m_dstFormat;
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D;
device->CreateUnorderedAccessView(m_dst.Get(), nullptr, &uavDesc, handle);
Bind(commandList);
commandList->Dispatch(m_threadsX, m_threadsY, 1);
TransitionDst(commandList, D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
D3D12_RESOURCE_STATE_COPY_SOURCE);
return m_dst;
}

View File

@@ -0,0 +1,48 @@
/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*/
#pragma once
#include "CComputeEffect.h"
class CColorTransformEffect : public CComputeEffect
{
private:
struct Consts
{
float matrix[3][4];
float scalar;
UINT matrixEnabled;
UINT lutEnabled;
UINT inputTransfer;
UINT outputTransfer;
} m_consts = {};
float m_lut[4096][4] = {};
bool m_uploadPending = false;
ComPtr<ID3D12Resource> m_constBuffer;
ComPtr<ID3D12Resource> m_lutBuffer;
DXGI_FORMAT m_srcFormat = DXGI_FORMAT_UNKNOWN;
DXGI_FORMAT m_dstFormat = DXGI_FORMAT_UNKNOWN;
public:
const char * GetName() const override { return "ColorTransform"; }
bool Init(const ComPtr<ID3D12Device3>& device);
PostProcessStatus SetFormat(const ComPtr<ID3D12Device3>& device,
const D12FrameFormat& src, D12FrameFormat& dst) override;
ComPtr<ID3D12Resource> Run(const ComPtr<ID3D12Device3>& device,
const ComPtr<ID3D12GraphicsCommandList>& commandList,
const ComPtr<ID3D12Resource>& src, RECT dirtyRects[],
unsigned * nbDirtyRects) override;
};