Warning: This is an unofficial workaround that binary patches your compositor (
libkwin.so) to skip a Wayland protocol validation check. It makes KWin non-spec-compliant and must be reapplied after every KWin update. Use at your own risk. The proper fix belongs in Proton's Wine Wayland HDR WSI layer, as described above.
If you need HDR working in games right now, this script patches out the min_lum >= max_lum validation in KWin:
#!/bin/bash
# Patch KWin to skip min_lum >= max_lum validation in HDR metadata
# Re-run after every KWin update (pacman -S kwin)
set -euo pipefail
LIBKWIN="/usr/lib/libkwin.so"
BACKUP="${LIBKWIN}.bak"
PATCH_FROM="735f" # jae (jump if above or equal)
PATCH_TO="9090" # NOP NOP
find_patch_offset() {
python3 -c "
import sys
with open('${LIBKWIN}', 'rb') as f:
data = f.read()
errstr = b'min_lum can\x27t be higher or equal to max_lum'
str_offset = data.find(errstr)
if str_offset == -1:
print('ERROR: error string not found', file=sys.stderr)
sys.exit(1)
comisd = b'\x66\x0f\x2f\xc1'
candidates = []
pos = max(0, str_offset - 0x200000)
while True:
idx = data.find(comisd, pos, str_offset)
if idx == -1:
break
patch_offset = idx + 4
next_bytes = data[patch_offset:patch_offset + 2]
if next_bytes == b'\x90\x90':
candidates.append(patch_offset)
elif next_bytes[0] == 0x73:
jae_target = patch_offset + 2 + next_bytes[1]
if b'\xbe\x05' in data[jae_target:jae_target + 30]:
candidates.append(patch_offset)
pos = idx + 1
if not candidates:
print('ERROR: patch location not found', file=sys.stderr)
sys.exit(1)
offset = candidates[-1]
print(f'{offset:#x} {data[offset:offset+2].hex()}')
"
}
echo "=== KWin HDR min_lum Patch ==="
RESULT=\$(find_patch_offset)
PATCH_OFFSET=\$(echo "\$RESULT" | awk '{print \$1}')
CURRENT=\$(echo "\$RESULT" | awk '{print \$2}')
echo "Location: \${PATCH_OFFSET} Current: \${CURRENT}"
if [ "\$CURRENT" = "\$PATCH_TO" ]; then echo "Already patched."; exit 0; fi
if [ "\$CURRENT" != "\$PATCH_FROM" ]; then echo "ERROR: unexpected bytes"; exit 1; fi
[ ! -f "\$BACKUP" ] && sudo cp "\$LIBKWIN" "\$BACKUP"
echo "Patching: \${PATCH_FROM} -> \${PATCH_TO}"
sudo python3 -c "
with open('\${LIBKWIN}', 'r+b') as f:
f.seek(\${PATCH_OFFSET})
assert f.read(2) == bytes.fromhex('\${PATCH_FROM}')
f.seek(\${PATCH_OFFSET})
f.write(bytes.fromhex('\${PATCH_TO}'))
"
echo "Done. Log out and back in to apply."
What it does: Replaces a jae instruction with two NOPs at the point where KWin validates mastering display luminance values, so it accepts the metadata instead of killing the client.
To revert: sudo cp /usr/lib/libkwin.so.bak /usr/lib/libkwin.so and log out/in. Or just update KWin — pacman overwrites the patch.
Tested on: KDE Plasma 6.6.4, KWin 6.5+, NVIDIA 595.58.03, Crimson Desert with proton-cachyos-slr.
Also reported to KDE as a feature request to consider clamping instead of disconnecting: https://bugs.kde.org/show_bug.cgi?id=519000
Correction: This bug only affects Proton builds that include Wine's native Wayland driver (winewayland.drv), such as proton-cachyos-slr and GE-Proton.
Valve's official Proton Experimental and Hotfix do not ship winewayland.drv — they use XWayland, so they cannot hit this codepath. The original report incorrectly stated it also affects Proton Experimental.
The bug is still relevant for the Proton project since the Wine Wayland driver is upstream and will presumably ship in official Proton builds eventually.
No, it's not relevant at this time and should be reported to and evaluated in the upstream wine project.
For anyone still hitting this: I traced where the bad values come from and worked around it with a tiny implicit Vulkan layer, no compositor or driver patching needed.
Finding: Crimson Desert calls vkSetHdrMetadataEXT with all zeros — minLuminance, maxLuminance, maxContentLightLevel, maxFrameAverageLightLevel and all primaries are 0. With max_lum == 0, KWin's min_lum >= max_lum check fires. Mesa's Wayland WSI has is_hdr_metadata_legal() and silently skips the mastering luminance in this case; NVIDIA's WSI (610.57.04) forwards it as-is.
Workaround: an implicit layer that intercepts vkSetHdrMetadataEXT and applies the same rules Mesa uses before the ICD sees the struct (maxLuminance == 0 → 1000 nits, min >= max → min = 0, CLL/FALL clamped to ≤ max, zero primaries → BT.2020). Enabled per game with HDRFIX=1 in launch options; loads fine inside pressure-vessel next to MangoHud.
Verified 2026-09-03 on Fedora 44 / Plasma 6.7.4 / NVIDIA 610.57.04 / RTX 5080 / GE-Proton11-6: HDR10 swapchain (R16G16B16A16_SFLOAT + HDR10_ST2084), no protocol error, toggling HDR in-game works.
Since this only happens in Wayland-driver builds (GE-Proton, proton-cachyos), the proper place for the clamp is probably vkd3d-proton's swapchain or winewayland, matching what Mesa already does.
Build: gcc -std=gnu11 -O2 -shared -fPIC -o libhdrfix.so hdrfix.c -lpthread -lm -Wl,--version-script=<(printf 'V { global: hdrfix_GetInstanceProcAddr; hdrfix_GetDeviceProcAddr; vkNegotiateLoaderLayerInterfaceVersion; local: *; };') (needs vulkan-headers).
Manifest ~/.local/share/vulkan/implicit_layer.d/hdrfix.x86_64.json:
{
"file_format_version": "1.0.0",
"layer": {
"name": "VK_LAYER_TRYGVE_hdrfix",
"type": "GLOBAL",
"api_version": "1.3.0",
"library_path": "/home/USER/.local/lib/hdrfix/libhdrfix.so",
"implementation_version": "1",
"description": "Log and sanitize VkHdrMetadataEXT (NVIDIA/KWin min_lum>=max_lum workaround)",
"functions": {
"vkGetInstanceProcAddr": "hdrfix_GetInstanceProcAddr",
"vkGetDeviceProcAddr": "hdrfix_GetDeviceProcAddr"
},
"enable_environment": { "HDRFIX": "1" },
"disable_environment": { "DISABLE_HDRFIX": "1" }
}
}
hdrfix.c:
/*
* SPDX-License-Identifier: MIT — (c) 2026 Trygve Rasmussen
* VK_LAYER_TRYGVE_hdrfix — implicit Vulkan layer that logs and sanitizes
* VkHdrMetadataEXT before it reaches the ICD (NVIDIA 610.x forwards invalid
* luminance to the Wayland compositor unfiltered; KWin then kills the client).
*
* Env:
* HDRFIX=1 enable (via manifest enable_environment)
* HDRFIX_LOG=<path> log file (default $HOME/hdrfix.log)
* HDRFIX_MODE=clamp (default) make metadata legal, or
* HDRFIX_MODE=drop skip vkSetHdrMetadataEXT entirely when illegal
* HDRFIX_MODE=passthrough log only
* HDRFIX_MAX_NITS=N max luminance to use when the app sends 0 (default 1000)
*/
#define VK_NO_PROTOTYPES
#include <vulkan/vulkan.h>
#include <vulkan/vk_layer.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <unistd.h>
#include <stdarg.h>
#define LAYER_NAME "VK_LAYER_TRYGVE_hdrfix"
struct inst_data { void *key; PFN_vkGetInstanceProcAddr gipa; struct inst_data *next; };
struct dev_data { void *key; PFN_vkGetDeviceProcAddr gdpa; PFN_vkSetHdrMetadataEXT set_hdr;
PFN_vkCreateSwapchainKHR create_sc; PFN_vkDestroyDevice destroy; struct dev_data *next; };
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
static struct inst_data *g_inst;
static struct dev_data *g_dev;
static FILE *g_log;
static void *key_of(const void *dispatchable) { return *(void **)dispatchable; }
static void logf_(const char *fmt, ...)
{
va_list ap;
if (!g_log) {
const char *p = getenv("HDRFIX_LOG");
char buf[512];
if (!p || !*p) { const char *h = getenv("HOME"); snprintf(buf, sizeof buf, "%s/hdrfix.log", h ? h : "/tmp"); p = buf; }
g_log = fopen(p, "a");
if (!g_log) g_log = stderr;
}
time_t t = time(NULL); struct tm tm; localtime_r(&t, &tm);
char ts[32]; strftime(ts, sizeof ts, "%Y-%m-%d %H:%M:%S", &tm);
fprintf(g_log, "[hdrfix %s pid=%d] ", ts, (int)getpid());
va_start(ap, fmt); vfprintf(g_log, fmt, ap); va_end(ap);
fputc('\n', g_log); fflush(g_log);
if (g_log != stderr) {
fprintf(stderr, "[hdrfix] ");
va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap);
fputc('\n', stderr);
}
}
static struct inst_data *find_inst(void *key) { struct inst_data *i; pthread_mutex_lock(&g_lock); for (i = g_inst; i && i->key != key; i = i->next); pthread_mutex_unlock(&g_lock); return i; }
static struct dev_data *find_dev (void *key) { struct dev_data *d; pthread_mutex_lock(&g_lock); for (d = g_dev; d && d->key != key; d = d->next); pthread_mutex_unlock(&g_lock); return d; }
/* ---- sanitize ---------------------------------------------------------- */
static void dump(const char *tag, const VkHdrMetadataEXT *m)
{
logf_("%s: minLum=%.6f maxLum=%.3f maxCLL=%.3f maxFALL=%.3f R=(%.4f,%.4f) G=(%.4f,%.4f) B=(%.4f,%.4f) W=(%.4f,%.4f)",
tag, m->minLuminance, m->maxLuminance, m->maxContentLightLevel, m->maxFrameAverageLightLevel,
m->displayPrimaryRed.x, m->displayPrimaryRed.y, m->displayPrimaryGreen.x, m->displayPrimaryGreen.y,
m->displayPrimaryBlue.x, m->displayPrimaryBlue.y, m->whitePoint.x, m->whitePoint.y);
}
/* Mirrors Mesa's is_hdr_metadata_legal + KWin's set_mastering_luminance check,
* in Wayland protocol units (min in 1/10000 nit, others in nits). */
static int is_legal(const VkHdrMetadataEXT *m)
{
double min_u = round(m->minLuminance * 10000.0), max_l = round(m->maxLuminance);
double cll = round(m->maxContentLightLevel), fall = round(m->maxFrameAverageLightLevel);
if (cll != 0) { if (cll * 10000.0 < min_u) return 0; if (max_l != 0 && cll > max_l) return 0; }
if (fall != 0) { if (fall * 10000.0 < min_u) return 0; if (max_l != 0 && fall > max_l) return 0; if (cll != 0 && fall > cll) return 0; }
if (max_l <= 0) return 0; /* KWin: min_lum*1e-4 >= max_lum -> error (also when both 0) */
return min_u < max_l * 10000.0;
}
static void sanitize(VkHdrMetadataEXT *m)
{
const char *e = getenv("HDRFIX_MAX_NITS");
double fallback_max = e && atof(e) > 0 ? atof(e) : 1000.0;
if (!(m->maxLuminance > 0)) m->maxLuminance = fallback_max;
if (m->maxLuminance > 10000) m->maxLuminance = 10000;
if (!(m->minLuminance >= 0) || m->minLuminance * 10000.0 >= m->maxLuminance * 10000.0 - 1) m->minLuminance = 0.0f;
if (m->maxContentLightLevel < 0 || m->maxContentLightLevel > m->maxLuminance) m->maxContentLightLevel = m->maxLuminance;
if (m->maxFrameAverageLightLevel < 0 || m->maxFrameAverageLightLevel > m->maxLuminance) m->maxFrameAverageLightLevel = m->maxLuminance;
if (m->maxContentLightLevel != 0 && m->maxFrameAverageLightLevel > m->maxContentLightLevel) m->maxFrameAverageLightLevel = m->maxContentLightLevel;
if (m->displayPrimaryRed.x == 0 && m->displayPrimaryGreen.x == 0 && m->displayPrimaryBlue.x == 0) {
/* BT.2020 */
m->displayPrimaryRed = (VkXYColorEXT){0.708f, 0.292f};
m->displayPrimaryGreen = (VkXYColorEXT){0.170f, 0.797f};
m->displayPrimaryBlue = (VkXYColorEXT){0.131f, 0.046f};
m->whitePoint = (VkXYColorEXT){0.3127f, 0.3290f};
}
}
/* ---- intercepted entry points ----------------------------------------- */
static VKAPI_ATTR void VKAPI_CALL hdrfix_SetHdrMetadataEXT(VkDevice device, uint32_t count, const VkSwapchainKHR *swapchains, const VkHdrMetadataEXT *metadata)
{
struct dev_data *d = find_dev(key_of(device));
const char *mode = getenv("HDRFIX_MODE"); if (!mode) mode = "clamp";
VkHdrMetadataEXT *copy = calloc(count ? count : 1, sizeof *copy);
uint32_t i, any_illegal = 0;
for (i = 0; i < count; i++) {
copy[i] = metadata[i];
int legal = is_legal(&metadata[i]);
logf_("vkSetHdrMetadataEXT[%u/%u] sType=%d legal=%d mode=%s", i, count, metadata[i].sType, legal, mode);
dump(" app sent", &metadata[i]);
if (!legal) { any_illegal = 1; if (!strcmp(mode, "clamp")) { sanitize(©[i]); dump(" sanitized", ©[i]); } }
}
if (!d || !d->set_hdr) { logf_("no next vkSetHdrMetadataEXT (d=%p) — dropping", (void *)d); free(copy); return; }
if (any_illegal && !strcmp(mode, "drop")) { logf_("illegal metadata and mode=drop — not forwarding"); free(copy); return; }
d->set_hdr(device, count, swapchains, !strcmp(mode, "passthrough") ? metadata : copy);
logf_("forwarded to next layer/ICD");
free(copy);
}
static VKAPI_ATTR VkResult VKAPI_CALL hdrfix_CreateSwapchainKHR(VkDevice device, const VkSwapchainCreateInfoKHR *ci, const VkAllocationCallbacks *ac, VkSwapchainKHR *sc)
{
struct dev_data *d = find_dev(key_of(device));
logf_("vkCreateSwapchainKHR format=%d colorSpace=%d extent=%ux%u images=%u presentMode=%d",
ci->imageFormat, ci->imageColorSpace, ci->imageExtent.width, ci->imageExtent.height, ci->minImageCount, ci->presentMode);
return d->create_sc(device, ci, ac, sc);
}
static VkLayerInstanceCreateInfo *find_inst_chain(const VkInstanceCreateInfo *ci)
{
VkLayerInstanceCreateInfo *p = (VkLayerInstanceCreateInfo *)ci->pNext;
while (p && !(p->sType == VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO && p->function == VK_LAYER_LINK_INFO)) p = (VkLayerInstanceCreateInfo *)p->pNext;
return p;
}
static VkLayerDeviceCreateInfo *find_dev_chain(const VkDeviceCreateInfo *ci)
{
VkLayerDeviceCreateInfo *p = (VkLayerDeviceCreateInfo *)ci->pNext;
while (p && !(p->sType == VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO && p->function == VK_LAYER_LINK_INFO)) p = (VkLayerDeviceCreateInfo *)p->pNext;
return p;
}
static VKAPI_ATTR VkResult VKAPI_CALL hdrfix_CreateInstance(const VkInstanceCreateInfo *ci, const VkAllocationCallbacks *ac, VkInstance *inst)
{
VkLayerInstanceCreateInfo *chain = find_inst_chain(ci);
if (!chain) return VK_ERROR_INITIALIZATION_FAILED;
PFN_vkGetInstanceProcAddr gipa = chain->u.pLayerInfo->pfnNextGetInstanceProcAddr;
PFN_vkCreateInstance create = (PFN_vkCreateInstance)gipa(VK_NULL_HANDLE, "vkCreateInstance");
chain->u.pLayerInfo = chain->u.pLayerInfo->pNext;
VkResult r = create(ci, ac, inst);
if (r != VK_SUCCESS) return r;
struct inst_data *d = calloc(1, sizeof *d); d->key = key_of(*inst); d->gipa = gipa;
pthread_mutex_lock(&g_lock); d->next = g_inst; g_inst = d; pthread_mutex_unlock(&g_lock);
logf_("layer active: vkCreateInstance app=%s", ci->pApplicationInfo && ci->pApplicationInfo->pApplicationName ? ci->pApplicationInfo->pApplicationName : "?");
return r;
}
static VKAPI_ATTR void VKAPI_CALL hdrfix_DestroyInstance(VkInstance inst, const VkAllocationCallbacks *ac)
{
struct inst_data *d = find_inst(key_of(inst));
PFN_vkDestroyInstance destroy = d ? (PFN_vkDestroyInstance)d->gipa(inst, "vkDestroyInstance") : NULL;
if (d) { pthread_mutex_lock(&g_lock); struct inst_data **pp = &g_inst; while (*pp && *pp != d) pp = &(*pp)->next; if (*pp) *pp = d->next; pthread_mutex_unlock(&g_lock); free(d); }
if (destroy) destroy(inst, ac);
}
static VKAPI_ATTR VkResult VKAPI_CALL hdrfix_CreateDevice(VkPhysicalDevice pd, const VkDeviceCreateInfo *ci, const VkAllocationCallbacks *ac, VkDevice *dev)
{
VkLayerDeviceCreateInfo *chain = find_dev_chain(ci);
if (!chain) return VK_ERROR_INITIALIZATION_FAILED;
PFN_vkGetInstanceProcAddr gipa = chain->u.pLayerInfo->pfnNextGetInstanceProcAddr;
PFN_vkGetDeviceProcAddr gdpa = chain->u.pLayerInfo->pfnNextGetDeviceProcAddr;
PFN_vkCreateDevice create = (PFN_vkCreateDevice)gipa(VK_NULL_HANDLE, "vkCreateDevice");
chain->u.pLayerInfo = chain->u.pLayerInfo->pNext;
VkResult r = create(pd, ci, ac, dev);
if (r != VK_SUCCESS) return r;
struct dev_data *d = calloc(1, sizeof *d);
d->key = key_of(*dev); d->gdpa = gdpa;
d->set_hdr = (PFN_vkSetHdrMetadataEXT)gdpa(*dev, "vkSetHdrMetadataEXT");
d->create_sc = (PFN_vkCreateSwapchainKHR)gdpa(*dev, "vkCreateSwapchainKHR");
d->destroy = (PFN_vkDestroyDevice)gdpa(*dev, "vkDestroyDevice");
pthread_mutex_lock(&g_lock); d->next = g_dev; g_dev = d; pthread_mutex_unlock(&g_lock);
int has_hdr_ext = 0; for (uint32_t i = 0; i < ci->enabledExtensionCount; i++) if (!strcmp(ci->ppEnabledExtensionNames[i], "VK_EXT_hdr_metadata")) has_hdr_ext = 1;
logf_("vkCreateDevice ok: VK_EXT_hdr_metadata enabled=%d next vkSetHdrMetadataEXT=%p", has_hdr_ext, (void *)d->set_hdr);
return r;
}
static VKAPI_ATTR void VKAPI_CALL hdrfix_DestroyDevice(VkDevice dev, const VkAllocationCallbacks *ac)
{
struct dev_data *d = find_dev(key_of(dev));
PFN_vkDestroyDevice destroy = d ? d->destroy : NULL;
if (d) { pthread_mutex_lock(&g_lock); struct dev_data **pp = &g_dev; while (*pp && *pp != d) pp = &(*pp)->next; if (*pp) *pp = d->next; pthread_mutex_unlock(&g_lock); free(d); }
if (destroy) destroy(dev, ac);
}
/* ---- proc addr ---------------------------------------------------------- */
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hdrfix_GetDeviceProcAddr(VkDevice dev, const char *name);
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hdrfix_GetInstanceProcAddr(VkInstance inst, const char *name);
static PFN_vkVoidFunction lookup(const char *name)
{
if (!strcmp(name, "vkGetInstanceProcAddr")) return (PFN_vkVoidFunction)hdrfix_GetInstanceProcAddr;
if (!strcmp(name, "vkGetDeviceProcAddr")) return (PFN_vkVoidFunction)hdrfix_GetDeviceProcAddr;
if (!strcmp(name, "vkCreateInstance")) return (PFN_vkVoidFunction)hdrfix_CreateInstance;
if (!strcmp(name, "vkDestroyInstance")) return (PFN_vkVoidFunction)hdrfix_DestroyInstance;
if (!strcmp(name, "vkCreateDevice")) return (PFN_vkVoidFunction)hdrfix_CreateDevice;
if (!strcmp(name, "vkDestroyDevice")) return (PFN_vkVoidFunction)hdrfix_DestroyDevice;
if (!strcmp(name, "vkSetHdrMetadataEXT")) return (PFN_vkVoidFunction)hdrfix_SetHdrMetadataEXT;
if (!strcmp(name, "vkCreateSwapchainKHR")) return (PFN_vkVoidFunction)hdrfix_CreateSwapchainKHR;
return NULL;
}
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hdrfix_GetDeviceProcAddr(VkDevice dev, const char *name)
{
PFN_vkVoidFunction f = lookup(name);
if (f && strcmp(name, "vkCreateInstance") && strcmp(name, "vkDestroyInstance") && strcmp(name, "vkCreateDevice") && strcmp(name, "vkGetInstanceProcAddr")) {
/* only wrap device functions the next layer actually provides */
struct dev_data *d = dev ? find_dev(key_of(dev)) : NULL;
if (!strcmp(name, "vkSetHdrMetadataEXT") && d && !d->set_hdr) return NULL;
return f;
}
struct dev_data *d = dev ? find_dev(key_of(dev)) : NULL;
return d ? d->gdpa(dev, name) : NULL;
}
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hdrfix_GetInstanceProcAddr(VkInstance inst, const char *name)
{
PFN_vkVoidFunction f = lookup(name);
if (f) return f;
struct inst_data *d = inst ? find_inst(key_of(inst)) : NULL;
return d ? d->gipa(inst, name) : NULL;
}
VKAPI_ATTR VkResult VKAPI_CALL vkNegotiateLoaderLayerInterfaceVersion(VkNegotiateLayerInterface *p)
{
if (!p || p->sType != LAYER_NEGOTIATE_INTERFACE_STRUCT) return VK_ERROR_INITIALIZATION_FAILED;
if (p->loaderLayerInterfaceVersion >= 2) {
p->loaderLayerInterfaceVersion = 2;
p->pfnGetInstanceProcAddr = hdrfix_GetInstanceProcAddr;
p->pfnGetDeviceProcAddr = hdrfix_GetDeviceProcAddr;
p->pfnGetPhysicalDeviceProcAddr = NULL;
}
return VK_SUCCESS;
}
ge-proton11-6x1 2026-09proton experimentalx2 2026-04DXVK_ENABLE_NVAPI=1x1 2026-04PROTON_ENABLE_HDR=1x1 2026-04PROTON_ENABLE_NVAPI=1x1 2026-04PROTON_ENABLE_WAYLAND=1x1 2026-04
Summary
Games running through Proton with HDR enabled on KDE Plasma Wayland crash when the game sends HDR mastering display metadata with
min_luminance >= max_luminance. KWin rejects the metadata per thewp_color_management_v1Wayland protocol spec, raisinginvalid_luminance, which kills the Wayland connection and terminates the game.This affects NVIDIA specifically because Mesa's WSI implementation filters out invalid luminance values before they reach the compositor, but NVIDIA's driver does not.
Error
Root Cause
min_luminance >= max_luminance(often both zero, or nonsensical values)winewayland.drv) passes these values through towp_color_management_surface_v1.set_mastering_luminanceinvalid_luminanceis raised"Mesa's Vulkan WSI layer sanitizes these values before calling
set_mastering_luminance, so this only affects NVIDIA users.Suggested Fix
Sanitize HDR mastering display luminance values in Wine's Wayland driver (or in Proton's HDR WSI layer) before passing them to the compositor. For example:
min_luminance >= max_luminance, clampmin_luminanceto 0set_mastering_luminancecall entirely when values are invalidThis matches what Mesa's WSI already does and avoids triggering the protocol error regardless of compositor.
Affected Software
Reproduction
PROTON_ENABLE_NVAPI=1 DXVK_ENABLE_NVAPI=1 PROTON_ENABLE_WAYLAND=1 PROTON_ENABLE_HDR=1 ENABLE_HDR_WSI=1 %command%invalid_luminanceprotocol errorWorkaround
Binary patching KWin's
libkwin.soto skip themin_lum >= max_lumvalidation works but is not a proper fix — it requires the compositor to deviate from the Wayland protocol spec.Related
wp_color_management_v1protocol spec: https://wayland.app/protocols/color-management-v1