The Windows equivalent to Linux DRM color management.
From ntddvdeo.h, we have IOCTL_COLORSPACE_TRANSFORM_SET(COLORSPACE_TRANSFORM_SET_INPUT). Reverse engineering of Windows.Internal.Graphics.Display.DisplayColorManagement.dll supposes it is applied on {monitor instance path}\color.
I always get
ERROR_ACCESS_DENIEDwhen trying to open the device.
From d3dkmthk.h, we have D3DKMTSetMonitorColorSpaceTransform(D3DKMT_SET_COLORSPACE_TRANSFORM).
All my experiments end in
STATUS_NOT_SUPPORTED.
Check color management capabilities with CapsCheck tool, specifically, the MatrixDDI value.
The Windows ICC loader (mscms.dll) passes a matrix and LUT originated from ICC profile to Windows.Internal.Graphics.Display.DisplayColorManagement.dll.
The MHC2 tag is a Microsoft-specific private extension to ICC profiles. The tag contains minimum1 and peak2 luminance values for the display, a color transform matrix, and 3 regamma LUTs for each channel.
The transform matrix and regamma LUT are transformed to LUT-matrix-LUT form and passed to IOCTL_COLORSPACE_TRANSFORM_SET.
Similar to vcgt, the ICC profile should describe characteristics after matrix and regamma LUT transform.
Below is the data structure of the MHC2 tag, which is inferred from several public-available ICC profiles and reverse engineering of Windows ICC loader.
// NOTE: all values are stored in big endian
__attribute__((scalar_storage_order("big-endian")))
struct MHC2Tag {
uint32_t signature = 'MHC2';
uint32_t reserved = 0;
uint32_t regamma_lut_size;
ICCs15Fixed16Number min_luminance; // in cd/m2
ICCs15Fixed16Number max_luminance; // in cd/m2
uint32_t matrix_offset = offsetof(MHC2Tag, matrix);
uint32_t channel0_regamma_lut_offset = offsetof(MHC2Tag, channel0_regamma_lut);
uint32_t channel1_regamma_lut_offset = offsetof(MHC2Tag, channel1_regamma_lut);
uint32_t channel2_regamma_lut_offset = offsetof(MHC2Tag, channel2_regamma_lut);
ICCs15Fixed16Number matrix[12]; // 4x3 matrix, row-major
ICCs15Fixed16ArrayType<regamma_lut_size> channel0_regamma_lut;
ICCs15Fixed16ArrayType<regamma_lut_size> channel1_regamma_lut;
ICCs15Fixed16ArrayType<regamma_lut_size> channel2_regamma_lut;
}
template<size_t size>
__attribute__((scalar_storage_order("big-endian")))
struct ICCs15Fixed16ArrayType {
uint32_t signature = 'sf32';
uint32_t reserved = 0;
ICCs15Fixed16Number values[size];
}All collected ICC samples have the same semi-identity color transform matrix.
The last column seems to be offset.
The observed behavior is like vcgt in ICC profile. Intermediate values are linearly interpolated from known points.
The GDI SetDeviceGammaRamp will merge together with MHC2 LUT, but the apply order is to be determined.
Public headers, reverse engineering and experiments reveals the following pipeline:
RGB/XYZ (3×3) matrices3 and degamma/regamma function are fixed in this pipeline:
- For SDR output, RGB/XYZ transform is based on sRGB and degamma/regamma transform is based on sRGB transfer function (or gamma 2.24).
- For HDR output, RGB/XYZ transform is based on Rec. 2020 and degamma/regamma transform is based on ST 2084 (PQ).
We can do some linear algebra 101 exercise to eliminate fixed RGB/XYZ matrices in pipeline and use a RGB-to-RGB matrix:
and given
which will mathematically transform the pipeline to a more famaliar one
A matrix of
in SDR mode swaps red and green channels, check profiles/SwapRedGreen.icm.
Since degamma/regamma function are fixed, we need to use corresponding TRC (sRGB or gamma 2.24) in the ICC profile that have non-identity XYZ transform matrix.
- Older Intel GPUs (at least UHD 630) doesn’t support matrix transform DDI and they are not adding new features to driver any more, nor implementing DDI for an already implemented feature.
- AMD GPUs (at least RX 6800 XT) don’t apply the matrix to, or mess up, the mouse cursor. Their own API doesn’t suffer from this issue.
- Interactively adjust the matrix with InteractiveMatrix tool.
- Assign
nvIccAdvancedColorIdentity.icmto target display, and it will change the matrix values then reload calibration.
- Assign
Advanced color, often referred to as HDR, is introduced in Windows 10. It performs color-managed composition in scRGB color space, and output to an HDR display in Rec. 2020 color space.
Windows 11 22H2 update introduced advanced color for SDR displays, which can be activated by a trivial registry tweak:
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers]
"EnableAcmSupportDeveloperPreview"=dword:00000001With supported hardware configuration (see below), a new setting will appear:
To specify display characteristics, add an ICC profile with MHC2 tag for that display (don’t check “Add as Advanced Color Profile”). If Windows thinks it is a valid profile, proper transform from composition space to device space will be set.
As of version 22622.598, only lumi, MHC2 and primaries values in a “valid” ICC profile are used (tone curves and vcgt are ignored). Extra calibration to sRGB (or gamma 2.24]) tone response via MHC2 regamma LUT is needed for optimal results. However, with an “invalid” profile, vcgt will be applied.
💡 Generate the profile with MHC2Gen:
MHC2Gen sdr-acm [--calibrate-transfer] "C:\...\DisplayCAL\storage\...\MODEL #1 2022-01-01 00-00 0.3127x 0.329y sRGB F-S XYZLUT+MTX.icm" "MODEL SDR ACM.icm"
It is expected future releases will use more characteristics like tone curves and probably PCS LUT in ICC profile, without the requirement of MHC2 tag.
A supported hardware configuration consists of:
- GPU that support color space transform DDI (Intel UHD 630 is not the case)
- Non-HDR display (this may subject to change)
If display is HDR-capable, Windows will prefer HDR output (at present), i.e. color encoded in Rec. 2020 and brightness encoded in SMPTE ST 2084 (PQ) instead of native gamut and transfer, and expects the display controller to convert them to appropriate electrical signal for the panel. Except for some professional or reference displays, this should be considered unreliable, inaccurate, and impossible to calibrate.
Footnotes
-
The mediaBlackPointTag (
bkpt) is no longer in ICC specification. ↩ -
Peak luminance of probably a small part of the display. Maximum average full-frame luminance should reside in the standard luminanceTag (
lumi). ↩ -
Check http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html for XYZ/RGB conversion matrix. ↩
-
headers suppose a gamma 2.2 transfer (
OUTPUT_WIRE_COLOR_SPACE_G22_P709) but sRGB transfer function coefficients are found inWindows.Internal.Graphics.Display.DisplayColorManagement.dll, which merges XYZ matrix, the MHC2 regamma LUT and the GDI gamma ramp to an RGB LUT-matrix-LUT pipeline. ↩ ↩2 ↩3
