protonscr

Wine Wayland HDR: games crash on KWin with invalid_luminance protocol error (min_lum >= max_lum)

protonclosed
ValveSoftware/Proton#9672 · opened 2026-04-15 by omercagatay · updated 2026-09-04 · 5 comments · github
Oomercagatay 2026-04-15 github

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 the wp_color_management_v1 Wayland protocol spec, raising invalid_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

[destroyed object]: error 5: min_lum can't be higher or equal to max_lum
err:waylanddrv:wayland_read_events_thread Failed to read events from the compositor, terminating process

Root Cause

  1. Game sends HDR metadata with min_luminance >= max_luminance (often both zero, or nonsensical values)
  2. Proton's Wine Wayland driver (winewayland.drv) passes these values through to wp_color_management_surface_v1.set_mastering_luminance
  3. The Wayland protocol spec mandates: "If max L is less than or equal to min L, the protocol error invalid_luminance is raised"
  4. KWin follows the spec, raises the error, and disconnects the client
  5. Game crashes

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:

  • If min_luminance >= max_luminance, clamp min_luminance to 0
  • Or skip the set_mastering_luminance call entirely when values are invalid

This matches what Mesa's WSI already does and avoids triggering the protocol error regardless of compositor.

Affected Software

  • Proton: proton-cachyos-10.0-20260408-slr (also affects Proton Experimental, GE-Proton)
  • Compositor: KDE Plasma 6.6.4 / KWin on Wayland
  • GPU driver: NVIDIA 595.58.03 (open kernel module)
  • GPU: NVIDIA GeForce RTX 5080 Laptop GPU
  • Kernel: Linux 7.0.0-1-cachyos-gcc
  • Confirmed game: Crimson Desert (AppID 3321460) — likely affects any DX12 game sending invalid HDR metadata through Proton

Reproduction

  1. Enable HDR on KDE Wayland with NVIDIA GPU
  2. Set launch options: PROTON_ENABLE_NVAPI=1 DXVK_ENABLE_NVAPI=1 PROTON_ENABLE_WAYLAND=1 PROTON_ENABLE_HDR=1 ENABLE_HDR_WSI=1 %command%
  3. Launch Crimson Desert (or another game that sends HDR metadata)
  4. Enable HDR in the game's graphics settings
  5. Game crashes immediately with the invalid_luminance protocol error

Workaround

Binary patching KWin's libkwin.so to skip the min_lum >= max_lum validation works but is not a proper fix — it requires the compositor to deviate from the Wayland protocol spec.

Related

Oomercagatay 2026-04-15 github

Temporary Workaround: KWin Binary Patch

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.

Oomercagatay 2026-04-15 github

Also reported to KDE as a feature request to consider clamping instead of disconnecting: https://bugs.kde.org/show_bug.cgi?id=519000

Oomercagatay 2026-04-15 github

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.

Kkisak-valve maintainer 2026-04-15 github

No, it's not relevant at this time and should be reported to and evaluated in the upstream wine project.

SShipBear1000 2026-09-04 github

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.

Layer source (~200 lines of C, MIT) + manifest + build command

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(&copy[i]); dump("  sanitized", &copy[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;
}