Skip to content

Add Windows SDR to HDR Tonemapping Fix mod - #5016

Open
millerpb wants to merge 11 commits into
ramensoftware:mainfrom
millerpb:millerpb/dwm-eotf-gamma-correction
Open

Add Windows SDR to HDR Tonemapping Fix mod#5016
millerpb wants to merge 11 commits into
ramensoftware:mainfrom
millerpb:millerpb/dwm-eotf-gamma-correction

Conversation

@millerpb

@millerpb millerpb commented Aug 4, 2026

Copy link
Copy Markdown

Windows SDR to HDR Tonemapping Fix

Adds a new mod (dwm-eotf-gamma.wh.cpp) that patches the DXBC shader bytecode in dwmcore.dll at DWM startup to replace the sRGB EOTF with a configurable pure power-law gamma for SDR-to-HDR tone mapping.

What it does

When Windows HDR is enabled, DWM converts SDR content to HDR scRGB using the sRGB EOTF (an ~2.2 power curve with a linear toe segment near black). This mod patches the four floating-point constants that define that transfer function — replacing them with a simple power-law curve — giving users direct control over the SDR-to-HDR tone mapping without permanently modifying any files on disk.

Based on dwm_eotf by ledoge (GPL-3.0).

Key details

  • Targets dwm.exe (x86_64)
  • Patches DXBC bytecode in dwmcore.dll's read-only memory at startup; recalculates shader checksums so D3D accepts the patched bytecode
  • Fully reversible: all patched bytes are restored when the mod is unloaded
  • User-configurable gamma value (default 2.2); supported range ~1.8–2.6
  • Requires Windows HDR to be enabled and a DWM restart to take effect

Changelog

If this pull request updates an existing mod, describe the changes below:

  • Changelog Initial version 1.0

Mod authorship

If this pull request introduces a new mod, please complete the section below.

This mod was created by:

    • The submitter, without AI assistance
    • The submitter, with AI assistance
    • Claude
    • ChatGPT
    • Gemini
    • Another AI (please specify):

Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback.

@windhawk-reviewer

Copy link
Copy Markdown

Thanks for the pull request! This repository uses a two-stage review: an AI review that you run yourself, followed by a human review.

To get started, comment /ai-review. Once you're happy with the result, comment /ready-for-reviewer to hand it over to a human reviewer.

See the pull request review process for the full details.

@windhawk-reviewer windhawk-reviewer Bot added the waiting-for-author The author's turn: request an AI review, or respond to one that was posted. label Aug 4, 2026
@millerpb

millerpb commented Aug 4, 2026

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 4, 2026
@millerpb

millerpb commented Aug 4, 2026

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer

Copy link
Copy Markdown

@millerpb /ai-review can't be applied here: an AI review was already requested, please wait for it to be posted.

@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The approach (reversible in-memory patching of the DXBC constants, with checksum fixup) is sound and there's precedent for this kind of thing in the catalog, and there's no existing mod that overlaps. A few things need attention before merge.

1. The README is missing the dwm.exe inclusion-list note — without it the mod silently does nothing.

dwm.exe is on Windhawk's critical system processes list, so Windhawk does not inject into it by default. A user who follows the current "Usage" steps ("Install and enable this mod", "Log off and back in") will see nothing happen and have no idea why. Every other dwm.exe mod in the catalog carries the same note — see w11-dwm-fix.wh.cpp#L20-L26 and dwm-custom-projection-border.wh.cpp#L21-L27:

## ⚠ Important usage note ⚠

In order to use this mod, you must allow Windhawk to inject into the **dwm.exe**
system process. To do so, add it to the process inclusion list in the advanced
settings. If you do not do this, it will silently fail to inject.

![Advanced settings screenshot](https://i.imgur.com/LRhREtJ.png)

2. @architecture amd64 locks out ARM64 devices for no reason — use x86-64.

amd64 means x64 only. x86-64 is the value every other dwm.exe mod uses, and because dwm.exe is on Windhawk's predefined process list, x86-64 compiles the mod as native ARM64 on ARM64 devices and as x64 everywhere else — exactly what you want. Nothing in this mod is CPU-architecture-specific (DXBC bytecode is the same build artifact on both), so unless you've specifically verified that the ARM64 dwmcore.dll ships different shader blobs, there's no reason to exclude those users:

-// @architecture    amd64
+// @architecture    x86-64

3. The hardcoded checksum allowlist makes the mod break silently on every dwmcore.dll update.

kKnownChecksums pins the mod to the exact four shader blobs on the builds you tested. Any servicing update that recompiles those shaders changes the checksums, and the mod then patches nothing — with no user-visible signal at all (Wh_Log is off by default, so the "No shaders patched" line is invisible in practice). The README already acknowledges this ("the known-checksum list may need updating"), which means the mod is expected to rot on a Patch Tuesday cadence and each break needs a new release.

You already have a much more version-stable identifier: the sRGB constants themselves. Consider dropping the allowlist and instead, for every DXBC blob found, running the constant search on a copy and patching only if all four splats are present:

// PatchShaderBuf: count per-constant matches instead of returning a single bool.
static int PatchShaderBuf(BYTE* buf, DWORD size, float gamma) {
    int constantsFound = 0;
    for (int ci = 0; ci < 4; ci++) {
        ...
        if (foundThisConstant) constantsFound++;
    }
    return constantsFound;  // caller requires == 4
}

Requiring all four makes false positives essentially impossible (a blob containing 2.4f, 0.04045f, 0.055f and 0.94786733f as vec3 splats is the sRGB EOTF), and the checksum is recomputed afterwards either way, so nothing else in the pipeline changes.

Related, and important if you make this change: PatchShaderBuf currently returns true if any one of the four constants matched. With the allowlist that can't bite you, but without it a blob where only the exponent matched would be patched into a broken hybrid curve (c <= 0.04045 ? c/12.92 : ((c+0.055)*0.94786733)^gamma) and silently accepted. All-or-nothing is the safe rule.

4. The memory-region scan can silently skip the shaders — walk the PE sections instead.

Wh_ModInit finds candidate memory with VirtualQuery plus two heuristics that aren't guaranteed to hold:

if (mbi.State == MEM_COMMIT &&
    mbi.Protect == PAGE_READONLY &&
    regionSize > 4096)

An exact PAGE_READONLY match plus an arbitrary > 4096 size floor means that if anything has split .rdata into multiple regions with differing protection (another mod patching an IAT entry in .rdata, for example), the scan can miss part of the section — and ScanRegionForShaders rejects any blob that doesn't fit entirely inside the region it was found in (i + hdr->size > regionSize), so a blob straddling such a boundary is dropped too. Both failure modes are silent.

Since you already parse the PE headers to get SizeOfImage, iterating IMAGE_SECTION_HEADERs is simpler and deterministic — one contiguous scan per section, no protection guessing:

auto* sec = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) {
    if (!(sec->Characteristics & IMAGE_SCN_MEM_READ) ||
        (sec->Characteristics & IMAGE_SCN_MEM_EXECUTE)) {
        continue;
    }
    totalPatched += ScanRegionForShaders(modBase + sec->VirtualAddress,
                                         sec->Misc.VirtualSize, gamma);
}
Optional improvements

Minor polish — none of this affects users, so it's your call.

  • g_gamma is written in Wh_ModInit and never read anywhere (the local gamma is what gets passed down). It can be deleted.
  • kPatchConsts[0] is a 0.0f placeholder that's never used, since index 0 always takes gamma. A two-element {threshold, offset, scale} layout (or just a comment-free {0.0f, 0.0f, 1.0f} indexed from 1) would be less confusing.
  • Use WindhawkUtils::StringSetting (RAII) instead of the raw Wh_GetStringSetting + Wh_FreeStringSetting pair:
    float gamma = wcstof(WindhawkUtils::StringSetting::make(L"GammaCurve"), nullptr);
    (requires #include <windhawk_utils.h>).
  • wcstof is declared in <cwchar>, not <cstdlib> — you're relying on a transitive include. Swap <cstdlib> (nothing else uses it) for <cwchar>.
  • The gamma < 1.0f || gamma > 10.0f check is unreachable as written: $options restricts the setting to five fixed strings, so wcstof can only ever produce one of them. Either keep the validation and drop $options (see the functionality notes), or drop the check.
  • Wh_Log(L"DWM EOTF: ...") — the DWM EOTF: prefix is redundant. Windhawk already prefixes log lines with the mod name, and the Wh_Log macro adds [line:function] on top.
  • The "No shaders patched" message blames HDR first: L"No shaders patched. HDR may be off, or dwmcore.dll was updated". The shader blobs live in dwmcore.dll's .rdata regardless of whether HDR is enabled — HDR only affects whether the patched code path runs, not whether the blobs are found. That diagnosis will send users down the wrong path.
  • Consider return FALSE from Wh_ModInit when totalPatched == 0 — there's nothing left for the mod to do in that process, and Windhawk reloads it after the next settings change anyway.
  • README accuracy, two small things:
    • "When the mod is unloaded (Windhawk disabled, settings changed, or Windows shutdown), all patched bytes are restored" — Wh_ModBeforeUninit is not called when the host process terminates, so nothing is restored at shutdown. (That's fine, the process is going away, but the README shouldn't claim it.)
    • "On DWM startup, Windhawk injects this mod before Direct3D is initialized" is only true when DWM starts after the mod is installed; enabling the mod into a running DWM patches bytecode that's already been compiled. The Usage section covers this, but the "How it works" wording reads as an unconditional guarantee.
  • typedef unsigned long DX_UINT4; happens to be 32-bit on Windows, but uint32_t says what's actually required and won't surprise anyone who lifts this code elsewhere.
  • In ScanRegionForShaders, the second half of if (!bigShader || region + i >= (BYTE*)bigShader + bigShader->size) is dead — the identical check at the top of the loop already cleared bigShader in that case.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • Patching a live DWM is a (small) race. When the mod is enabled while DWM is already running, WriteMemorySafe rewrites up to several KB of .rdata in an actively compositing process. If DWM happened to be creating a shader from that blob at the same instant it would read a half-patched blob with a mismatched checksum. The window is microseconds and blobs are only read at device/shader-creation time, so this is very unlikely — and there's no clean alternative, since the patch does need to be in place for the display-reconfiguration re-compile path you mention in the README. Just flagging it as a known cost of the approach.
  • $options limits users to five values. The README says the supported range is ~1.8–2.6, but the dropdown only offers 1.8/2.0/2.2/2.4/2.6 — someone who wants 2.15 or 2.35 can't get there. Since the setting is already a string and Wh_ModInit already parses and range-checks it, dropping $options and letting users type a value would work with no other change (and would make the 1.010.0 validation meaningful again). Keeping the dropdown is a reasonable choice too if you'd rather not expose untested values — just worth a deliberate decision.
  • PatchShaderBuf replaces every occurrence of each constant in the blob. If a target shader ever contains the inverse transfer function as well (linear→sRGB uses the same 0.055 constant), zeroing it there would corrupt that path. Logging a per-constant substitution count would make an unexpected number of replacements visible in the log rather than silent:
    Wh_Log(L"  Shader #%d: constant %d replaced %d time(s)", matchIdx, ci, count);
  • The "big shader" container heuristic deserves a comment explaining what those blobs actually are. As written it handles exactly one level of nesting and assumes the first enclosing unknown DXBC blob is the one whose checksum needs fixing. That's probably right for dwmcore.dll, but a reader (or future-you after a Windows update) has no way to tell whether a nested container would also need its checksum recomputed. A sentence about what you observed in the binary would go a long way.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 4, 2026
@millerpb

millerpb commented Aug 5, 2026

Copy link
Copy Markdown
Author

I've addressed the suggestions in the ai review within reason. However, the constant-only detection matched 37 unrelated shaders on Windows 11 (instead of the known 4), so checksums were kept as the primary selector with HasDXBCSubBlob kept as an improvement to the container detection logic.

@millerpb

millerpb commented Aug 5, 2026

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 5, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The previous round's items are all addressed — the dwm.exe inclusion note is in the README, @architecture is x86-64, PatchShaderBuf is now all-or-nothing, and the region scan walks PE sections. The pushback on constant-only detection is reasonable too, given you measured it. The approach itself (scan + patch read-only bytes + restore on unload) has precedent in the catalog — w11-dwm-fix.wh.cpp#L301-L317 does the same thing in dwm.exe. Two hardening items remain around the container-checksum path, which is the one place the mod writes a checksum it has never verified it can compute correctly.

1. Verify CalcDXBCChecksum reproduces a blob's existing checksum before overwriting it.

The whole reason the mod recomputes the checksum is that D3D rejects a DXBC blob whose hash doesn't match. So writing a wrong checksum is worse than not patching at all: the shader stops being creatable, and in DWM that means a compositing failure rather than "the mod did nothing".

For the four leaf shaders the allowlist makes this fairly safe — a checksum match means the bytes are exactly what you tested against. But the container blob at lines 474-476 and 567-569 is not allowlisted: it's whatever enclosing blob the heuristic picked, its contents change with every dwmcore.dll build, and its checksum is overwritten with a value the mod has never cross-checked. If the container's hash is computed over a different range, or isn't a DXBC-MD5 at all, the mod silently corrupts it.

A pre-flight self-check costs nothing and turns that into a clean skip:

// Before touching anything: confirm our checksum routine reproduces this blob's
// own checksum. If it doesn't, we can't safely rewrite it.
static bool ChecksumRoundTrips(const DXBCHeader* hdr) {
    DWORD verify[4];
    CalcDXBCChecksum((BYTE*)hdr, hdr->size, verify);
    return memcmp(verify, hdr->checksum, 16) == 0;
}

Apply it to a container before accepting it as bigShader, and to a leaf before patching it. Related, in the same path: WriteMemorySafe's return value is ignored for both container writes. If the container checksum write fails after its inner shader was already patched, DWM is left with exactly the broken state above — that one deserves a rollback (restore the leaf patch) rather than a silent Wh_Log.

2. The container relationship is positional, and only one nesting level is handled.

bigShader is whatever blob-with-sub-blobs was seen most recently (line 499-504), and any leaf patched while it is set marks it dirty (line 553-554) — there's no check that the leaf actually lies inside it. Two consequences:

  • A false-positive "container" (DXBC + reserved == 1 + a nested blob-shaped match) that happens to precede a real target would get 16 bytes of unrelated read-only data overwritten, while the real container keeps a stale checksum. Cheap to rule out:
    if (bigShader &&
        (BYTE*)hdr >= (BYTE*)bigShader &&
        (BYTE*)hdr + hdr->size <= (BYTE*)bigShader + bigShader->size) {
        bigPatched = true;
    }
  • if (!bigShader) bigShader = hdr; deliberately ignores nested containers, so a leaf two levels deep would leave the intermediate container's checksum stale. That's fine for today's dwmcore.dll — but the failure mode if a future build nests deeper is "D3D rejects that shader", not "the mod does nothing". Tracking a small stack of enclosing containers and fixing each one on the way out would make the depth assumption unnecessary; at minimum, bail out (and log) instead of patching when a leaf is found inside a second, nested container.
Optional improvements

Minor polish — none of this affects users, so it's your call.

  • Record which Windows builds the four checksums in kKnownChecksums (line 351) came from, in the comment above them. When the mod stops matching, the first question is "which build did this last work on" and the answer is currently nowhere in the file.
  • WindhawkUtils::StringSetting (RAII) is the preferred form over the raw Wh_GetStringSetting + Wh_FreeStringSetting pair at lines 594-596:
    float gamma = wcstof(WindhawkUtils::StringSetting::make(L"GammaCurve"), nullptr);
    (requires #include <windhawk_utils.h>).
  • Wh_Log(L"DWM EOTF: restoring original shader bytes") (line 655) — the DWM EOTF: prefix is redundant; Windhawk already prefixes log lines with the mod name. The other log lines dropped it already.
  • PatchShaderBuf steps j one byte at a time. DXBC is DWORD-structured, so j += 4 from an aligned blob start is 4× faster and removes the (already tiny) chance of a misaligned false positive.
  • CalcDXBCChecksum and HasDXBCSubBlob could take const BYTE* / const DXBCHeader* for the read-only paths — CalcDXBCChecksum never writes through pData.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • The checksum allowlist means the mod stops working silently after a dwmcore.dll update. You measured that constant-only detection over-matches (37 vs 4), so this is a defensible trade-off and I'm not asking you to change it — but the user-visible symptom is "nothing happens", and the only diagnostic is a Wh_Log line that's off by default. The README's last bullet tells users to look for No matching shaders found without telling them how; a sentence pointing at the mod's advanced settings → logging would make that bullet actionable.
  • A partial allowlist match produces a mixed state. If a future update changes only some of the four shaders, totalPatched is 1-3, the mod returns TRUE, and DWM ends up with some content power-law-mapped and some still sRGB — a visible inconsistency with no signal. If all four checksums are from the same build, requiring all four (or at least logging loudly on a partial match) would be safer. If they were collected across different Windows versions, ignore this — but then that's worth a comment, since it also changes what "4" means.
  • PatchShaderBuf replaces every occurrence of each constant in the blob. 0.055 appears in both the sRGB decode and encode transfer functions; if a target shader ever contains both directions, zeroing it would corrupt the encode path. Logging a per-constant replacement count would surface an unexpected number rather than hiding it:
    Wh_Log(L"  Shader #%d: constant %d replaced %d time(s)", matchIdx, ci, count);
  • Only three of a possibly four-component splat are rewritten. A DXBC 4-component immediate is encoded as four consecutive floats (l(2.4, 2.4, 2.4, 2.4)); the 12-byte search matches components 0-2 and leaves component 3 at the original sRGB value. That's harmless (and probably intentional) if the shader only uses .xyz, but worth confirming it's deliberate rather than inherited.
  • Patching a live DWM is still a small race. When the mod is enabled into a running DWM, WriteMemorySafe rewrites several KB of .rdata in an actively compositing process; a blob being read at that instant would see a half-patched state. The window is microseconds and blobs are only read at shader-creation time, and there's no clean alternative given the mod has to be in place for the display-reconfiguration recompile path. Just a known cost of the approach.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 5, 2026
@millerpb millerpb changed the title Add DWM EOTF Gamma Curve mod Add Windows SDR to HDR Tonemapping Fix mod Aug 5, 2026
@millerpb

millerpb commented Aug 5, 2026

Copy link
Copy Markdown
Author

/ready-for-reviewer

@windhawk-reviewer

Copy link
Copy Markdown

@millerpb /ready-for-reviewer can't be applied here: the most recent AI review covers 5e84c6d, but the current head of this pull request is d41a222. Comment /ai-review to get a review of the current code.

@millerpb

millerpb commented Aug 5, 2026

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 5, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


Solid, self-contained submission — no hooks, fully reversible in memory, correct use of Wh_Log and WindhawkUtils::StringSetting, and the dwm.exe inclusion-list note matches what the other DWM mods do. The risk is concentrated in the patching machinery, and a couple of the safety nets in it don't actually fire.

1. The container round-trip guard doesn't skip the container

ScanRegionForShaders logs "checksum did not round-trip, skipping" and then continues — which only advances i by one byte, so the scan walks straight into that container's body. Any allowlisted leaf inside it is then patched (with insideContainer == false, because bigShader was never set), gets its own checksum rewritten, and the enclosing container is left with a stale checksum — exactly the corruption the guard was added to prevent. Skip the whole blob, the way the nested-container branch already does:

if (!ChecksumRoundTrips(hdr))
{
    Wh_Log(L"  Container at %p: checksum did not round-trip, skipping", (void *)hdr);
    i += hdr->size - 1; // loop will add 1 more, landing just after the blob
    continue;
}

2. The leaf's checksum is never round-trip-verified before it's overwritten

Containers get ChecksumRoundTrips, leaves don't — even though a leaf's expected checksum is known exactly (it's the allowlist entry that just matched). If CalcDXBCChecksum ever produces the wrong hash for a leaf's layout, the mod writes a bogus checksum into a shader that DWM must compile, and D3D rejects it. In dwm.exe that isn't a cosmetic failure — the compositor is what draws the desktop. The check is free here and turns a potential black screen into a no-op:

if (matchIdx < 0)
    continue;

// The expected hash is kKnownChecksums[matchIdx], so this validates our
// checksum implementation against this exact blob before we overwrite it.
if (!ChecksumRoundTrips(hdr))
{
    Wh_Log(L"  Shader #%d at %p: checksum did not round-trip, skipping", matchIdx, (void *)hdr);
    continue;
}

3. A partial patch is reported as success

Wh_ModInit only rejects totalPatched == 0. If two of the four shaders are found and the other two hit the constsFound < 4 path (or aren't present in the installed dwmcore.dll), the mod returns TRUE and DWM ends up with some SDR surfaces on power-law gamma and others still on the sRGB EOTF — visibly inconsistent and very hard for a user to diagnose. Note also that numPatched isn't a reliable proxy for "all four matched": it counts patched blobs, not distinct allowlist entries. Track which entries matched and make it all-or-nothing:

static unsigned g_matchedMask;  // bit i = kKnownChecksums[i] was patched
...
g_matchedMask |= 1u << matchIdx;   // where numPatched++ happens
...
// in Wh_ModInit, after the section loop:
if (g_matchedMask != 0xF)
{
    Wh_Log(L"Only mask 0x%X of 4 target shaders patched — reverting", g_matchedMask);
    RestoreAllPatches();
    return FALSE;
}

4. The checksum allowlist pins the mod to a single dwmcore.dll build

kKnownChecksums are the hashes of the shaders in one specific dwmcore.dll. Any cumulative update that recompiles those shaders changes them, and the mod silently degrades to a no-op — Wh_Log is off by default, so the "No matching shaders found" line isn't visible to a normal user, who just sees the fix stop working after an update. It presumably also does nothing today on any Windows build other than the one you tested. That's a per-update maintenance loop for you and an unexplained regression for users.

PatchShaderBuf already requires all four sRGB constants to be present as vec3 splats in the same blob, which is a far stronger selector than "contains sRGB constants" and makes a reasonable fallback: keep the checksum allowlist as the fast path, and when nothing on it matches, accept leaf blobs where all four constants are found and each is found exactly once, requiring exactly four such blobs before committing (restore + FALSE otherwise, per item 3). At minimum, log the computed checksum of every blob that contains all four constants, so a user on a new build can paste the values into an issue instead of you having to re-extract them by hand each time.

5. dwmcore.dll may not be loaded yet at Wh_ModInit

GetModuleHandleW(L"dwmcore.dll") returning null aborts the mod for the lifetime of that process (Windhawk only reloads it after a settings change), and the patch has to land before D3D compiles the shaders. If dwmcore.dll is guaranteed to be a static import of dwm.exe and therefore always present at the point Windhawk loads mods, a short comment saying so would settle it. Otherwise the convention is to hook LoadLibraryExW in kernelbase.dll — not the kernel32 import, which internal callers bypass — and run the scan when dwmcore.dll appears. See taskbar-clock-customization.wh.cpp#L5814-L5819 for the resolution pattern and #L5332-L5336 for the hook itself.

6. README: no recovery path if DWM won't come up

The mod rewrites bytecode the desktop compositor has to accept. If that ever goes wrong on a build you haven't tested, the user is looking at a broken desktop with no obvious way back — and unlike an Explorer mod, they can't just kill the process. A couple of lines under "Known limitations" describing how to recover (safe mode, disabling the mod / Windhawk from there) would be worth having.

Optional improvements

Minor polish — none of this affects users today, so it's your call.

  • PatchShaderBuf's comment says "a count > 1 indicates unexpected overlap that would corrupt unintended code", but nothing acts on it: the caller only requires constsFound == 4 and logs the counts. On the pinned builds the counts are deterministic so this is just defense in depth — but it becomes load-bearing if you adopt content-based matching (item 4 above). Either reject counts[ci] != 1 or drop the claim from the comment.
  • wcstof is locale-dependent. If anything in dwm.exe sets a locale whose decimal separator isn't ., wcstof(L"2.2") returns 2.0, which passes the 1.0f..10.0f range check silently. Since the values come from a fixed $options list, a small string→float lookup table sidesteps the question entirely.
  • Both scan loops step one byte at a time, while PatchShaderBuf already notes that DXBC constants are DWORD-aligned. DXBC blobs are DWORD-aligned in .rdata too, so the outer loop in ScanRegionForShaders and the loop in HasDXBCSubBlob could step by 4 — a small saving in a path that runs during DWM startup. Worth confirming the alignment assumption before relying on it.
  • RestoreAllPatches ignores WriteMemorySafe failures but clears g_patches unconditionally, so a failed restore is both unrecoverable and invisible. Logging the failure would at least make it diagnosable.
  • The README's "See examples and read more on GitHub by dylanraga" points at win11hdr-srgb-to-gamma2.2-icm, which is a different (ICC-profile) approach to the same problem. A word of context would avoid users thinking this mod installs that profile.
  • If you can capture one, a before/after image of the effect in the README would help. HDR screenshots are awkward to make representative, so this is genuinely optional.

Functionality notes

Non-critical observations about the feature behavior itself.

  • The gamma dropdown fixes five values, while dwm_eotf lets you pick any exponent. A free-form string setting — validated by the 1.0f..10.0f check you already have — would cover users who want e.g. 2.3, at the cost of a less guided UI.
  • Nesting deeper than one container level is skipped outright, so if a future dwmcore.dll moves the target shaders under two levels of nesting the mod stops working rather than degrading. That's consistent with the README's "file an issue" line; just noting the limitation is structural, not only checksum-related.
  • The mod patches unconditionally, without checking whether HDR is currently enabled. That looks like the right call (HDR can be toggled and DWM recreates its device without restarting, so a gate at init would be wrong), but it does mean users who never enable HDR still carry the patch. FYI only.
  • Restoring in Wh_ModBeforeUninit writes into dwmcore.dll's .rdata while DWM is live and multithreaded. Nothing reads those blobs after shader compilation, so this looks fine — but if D3D happens to re-read a blob at device-recreation time during an unload, it would see partially restored bytes. Very narrow, and there's no clean alternative, so just an FYI.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added the waiting-for-author The author's turn: request an AI review, or respond to one that was posted. label Aug 5, 2026
@m417z m417z added the waiting-for-ai-review An AI review was requested and is being prepared. label Aug 5, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


Nice idea, and a genuinely better home for it than the upstream PoC: no TerminateProcess on DWM, no NtSuspendProcess, no admin tool, and the patch is reverted on unload. There's no overlapping mod in the catalog, the @include scope is correctly narrow, and the fail-safe design (checksum round-trip pre-flight, all-or-nothing revert on a partial match) is the right instinct for a critical system process. A few things to fix:

1. The shader identification is pinned to one dwmcore.dll build, and it fails silently.

kKnownChecksums (lines 363–368) is copied verbatim from ledoge's 2022 proof of concept. Those are hashes of four specific compiled DXBC blobs — any Windows update that recompiles even one of those shaders makes matchedMask != 0xF, which triggers the full revert and leaves the mod doing nothing at all. Combined with the all-or-nothing gate, a single changed shader disables the whole mod, and the only signal is a Wh_Log line that's off by default. Users will just see an enabled mod that does nothing.

Two things worth doing:

  • State in the README which Windows build(s) you verified this on (e.g. "tested on 26100.xxxx"). That's the single most useful piece of information for the next person who hits a mismatch, and it turns "please file an issue" into something actionable.
  • Consider a fallback selector for when the checksums don't match. The comment at line 360 says constant-only detection over-patches — but that's constant detection per constant. A leaf blob (!HasDXBCSubBlob) that contains all four sRGB splats and whose own checksum round-trips is a much more specific signature than any single constant, and it's exactly what makes a blob the EOTF shader. If that still over-matches in practice, say so in the code comment so it's documented as tried-and-rejected rather than not-considered.

2. PatchShaderBuf documents a corruption guard that the code never enforces.

Lines 405–410 say:

some constants (e.g. 0.055) appear in both decode and encode paths, so a count > 1 indicates unexpected overlap that would corrupt unintended code

…but the code only logs the counts (line 627–628) and patches every occurrence anyway. If that comment is right, then on a build where a target shader also contains an sRGB encode path, the mod silently corrupts it — and in DWM that means a compositor failure, not a cosmetic glitch. Either enforce the check or drop the claim. The fail-safe machinery already exists, so enforcing is cheap:

for (int ci = 0; ci < 4; ci++) {
    if (counts[ci] != 1) {
        Wh_Log(L"  Shader #%d: constant %d matched %d time(s), expected 1 — skipping",
               matchIdx, ci, counts[ci]);
        continue;  // -> partial mask -> Wh_ModInit reverts everything
    }
}

If in fact you've verified that a count > 1 is normal and harmless on the shaders you target, then the comment is the thing to fix.

3. The "HDR may be off" diagnostic is wrong and will send users down the wrong path.

Line 756:

Wh_Log(L"No target shaders found — HDR may be off, or dwmcore.dll checksums changed after a Windows Update");

The shader blobs live in dwmcore.dll's .rdata and are present in the mapped image regardless of the display's HDR state — HDR only decides whether DWM executes that code path at runtime. So matchedMask == 0 can only mean the checksums no longer match; it can never mean "HDR is off". Since issue reports are the mod's only recovery mechanism for finding 1, this message actively discourages the report you need. Drop the HDR clause.

4. Rewriting the entire blob into live read-only image memory is a wider window than it needs to be.

WriteMemorySafe at line 640 flips the protection on the whole shader blob and memcpys all hdr->size bytes, while DWM's other threads are running (this is the mid-session enable path — Wh_ModInit on the Windhawk engine thread). Only ~48 bytes of constants plus the 16-byte checksum actually change. Two consequences:

  • If DWM recreates its D3D device concurrently (display reconfiguration, GPU reset, HDR toggle), it can read a partially-written blob whose checksum doesn't match its contents → CreateShader fails in the compositor. Upstream sidestepped this by suspending the process; you can't do that from inside, but you can shrink the window to a few 12-byte stores.
  • Every page the blob touches becomes a private copy-on-write page in a critical system process, and PatchRecord::original keeps a full copy of each blob alive for the mod's lifetime. Patching only the deltas keeps both down to almost nothing.

Have PatchShaderBuf record the offsets it wrote, then write those ranges plus the checksum instead of the whole blob:

// PatchShaderBuf fills `offsets` with the byte offset of each 12-byte splat it replaced.
for (size_t off : offsets) {
    PatchRecord rec;
    rec.addr = (BYTE*)hdr + off;
    rec.original.assign(rec.addr, rec.addr + 12);
    if (!WriteMemorySafe(rec.addr, patched.data() + off, 12)) { /* roll back, continue */ }
    g_patches.push_back(std::move(rec));
}
// checksum last

This also makes the multi-page VirtualProtect concern below go away on its own.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • Skipping a blob can break the 4-byte scan alignment. Lines 562 and 576 do i += hdr->size - 4. The whole scan relies on a 4-byte stride, so if any blob's size isn't a multiple of 4, every subsequent offset is misaligned and the scan can miss all remaining shaders — a silent total no-op. Round up: i += ((hdr->size + 3) & ~(DWORD)3) - 4;.

  • The container rollback can leave matchedMask bits set for shaders it reverted. FinalizeContainer (lines 508–515) pops everything back to containerPatchBase but only clears containerMatchedBits. A leaf patched while bigShader was set but with insideContainer == false (line 610 — the leaf's end falls outside the container's bounds) is rolled back without its mask bit being cleared, so Wh_ModInit's all-or-nothing check would report success for a shader that was reverted. It needs a VirtualProtect failure to trigger, but tracking a second containerAllBits (set unconditionally alongside matchedMask) is a two-line fix.

  • ScanRegionForShaders's return value is dead. It carefully maintains numPatched (including decrementing it during rollback), but the call site at line 747 discards it — matchedMask is what actually drives the decision. Either drop the counter or log the total.

  • WriteMemorySafe restores the wrong protection on a multi-page range. VirtualProtect reports only the first page's old protection, so a blob spanning pages with different protections gets restored uniformly to the first page's value. Benign for a uniform .rdata, and moot if you adopt the delta-write suggestion above.

  • README/log string mismatch. The README (line 99) tells users to look for "No matching shaders found", but the code logs "No target shaders found". Users grepping the log won't find it.

  • The advanced-settings path is slightly off. The actual UI path is Settings → Advanced settings → More advanced settings → Process inclusion list; the numbered steps skip the "More advanced settings" level. The existing dwm.exe mods word it loosely enough to dodge this (dwm-custom-projection-border.wh.cpp#L21-L27).

  • Formatting. The repo has a .clang-format (Chromium, IndentWidth: 4); the mod uses Allman braces throughout. Running clang-format over it would match the rest of mods/.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • The gamma is limited to five fixed steps. Upstream accepts any value in 1–10. The $options lookup table exists to dodge locale-dependent wcstof, which is a legitimate concern — but an integer setting in hundredths avoids the parse entirely and gives continuous control:

    - GammaCurveX100: 220
      $name: Gamma curve (×100)
      $description: 220 = gamma 2.2. Typical range 180–260.
    
    int g = Wh_GetIntSetting(L"GammaCurveX100");
    float gamma = std::clamp(g, 100, 400) / 100.0f;

    If you'd rather keep a dropdown for discoverability, std::from_chars on the string is also locale-independent and would let you accept arbitrary values.

  • Upstream's SDR brightness scale factor was dropped. ledoge's tool also patches constant index 3 to powf(sqrtf(scale), 1/gamma) to scale SDR peak brightness. You already patch that constant (to 1.0f), so exposing it as an optional second setting would be nearly free. Upstream does note it produces artifacts on some Windows elements, so gating it behind a clearly-labelled advanced setting (or leaving it out, as you have) are both defensible — just noting the option.

  • There's no positive confirmation that the patch applied. Given finding 1, it'd help to tell users in the README how to verify: enable logging in Windhawk and look for All 4 target shaders patched. Cheap, and it makes "it's not working" reports much more useful.

  • The Chromium/Electron mode-switching limitation is documented honestly and matches upstream's known-issues list — good call including it rather than letting users discover it.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 5, 2026
@millerpb

millerpb commented Aug 5, 2026

Copy link
Copy Markdown
Author

Made more adjustments per ai review. Here are some explanations for the ones I did not pursue.

#4b — (hdr->size + 3) & ~3u alignment roundup
The 4-byte scan stride assumes blob sizes are multiples of 4. The DXBC spec mandates it, and scanning the actual dwmcore.dll binary confirmed 0 of 404 DXBC occurrences are unaligned. Defending against something the spec prohibits and the data disproves adds noise without benefit.

#4c — "out-of-bounds leaf" rollback leaves matchedMask bit set
The scenario requires a leaf whose start is inside the container but whose end extends past it. In a valid DXBC container the size field of the outer blob accounts for every byte of its sub-blobs; a sub-blob that overflows its parent would be malformed. HasDXBCSubBlob already validates k + inner->size <= hdr->size before accepting a container, so only geometrically valid containers reach the patch path. The condition the ai reviewer describes is structurally impossible in well-formed DXBC.

#4e — VirtualProtect restoring wrong protection across multi-page blobs
VirtualProtect reports the first page's old protection, so a blob spanning pages with different protections would have all pages restored to the first page's value. .rdata is uniformly PAGE_READONLY, so in practice all pages have the same protection and the "wrong value" is the correct value. The ai reviewer acknowledged this themselves ("benign for a uniform .rdata"). It also becomes moot entirely if delta writes are ever adopted.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 5, 2026
@windhawk-reviewer

Copy link
Copy Markdown

This pull request has already had 3 AI reviews in the last 24 hours, which is the limit, so no review was posted this time.

Comment /ai-review again after 2026-08-06 00:46 UTC (in 3 hours) to get another one.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 5, 2026
@millerpb

millerpb commented Aug 5, 2026

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 5, 2026
@windhawk-reviewer

Copy link
Copy Markdown

This pull request has already had 3 AI reviews in the last 24 hours, which is the limit, so no review was posted this time.

Comment /ai-review again after 2026-08-06 00:46 UTC (in 2 hours) to get another one.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 5, 2026
@millerpb

millerpb commented Aug 6, 2026

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 6, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The patching machinery is in good shape now — the counts[ci] != 1 guard is enforced, the container round-trip pre-flight skips the whole blob, the HDR diagnostic is gone, and the all-or-nothing revert closes the mixed-state hole. I went through the scanner, the container/leaf bookkeeping and the AMD DXBC-MD5 port line by line this round and did not find a remaining correctness defect: the rollback keeps matchedMask and g_patches consistent (whenever g_patches is non-empty at least one mask bit is set, so the matchedMask == 0 early return can't leak a patch), the restore order is safe because the container-checksum record at container + 4 can never overlap a leaf record, and the in-place replacement in PatchShaderBuf means overlapping splat positions can't double-count. Your pushbacks on the DXBC 4-byte alignment, the out-of-bounds-leaf rollback and the multi-page VirtualProtect all hold up. g_patches needs no [[clang::no_destroy]] — its destructor only frees heap, which is safe at process shutdown.

What's left is about what happens when the allowlist doesn't match, which is the mod's normal state on every build except the one you tested.

1. When the checksums don't match, neither the user nor you get anything to act on.

The README says "please file an issue", and the log says the checksums changed — but there is nothing in that report to work with. matchedMask == 0 returns before anything is logged about what is in the binary, and the per-constant counts you added only fire for blobs whose checksum already matched, so they never run on a build where the allowlist is stale. That makes the mod's only recovery mechanism (a new checksum list from you) depend on you re-extracting the blobs by hand for every reporter.

A read-only diagnostic pass, run only when matchedMask != 0xF, closes that with no new state tracking — it doesn't patch, so over-matching is harmless, and it doesn't need the container bookkeeping you deferred:

// Read-only: log every leaf blob that carries all four sRGB splats exactly
// once, so a user on an unrecognized build can paste the checksums into an
// issue.
static void LogSrgbCandidates(BYTE* region, size_t regionSize) {
    for (size_t i = 0; i + kDXBCHeaderSize <= regionSize; i += 4) {
        auto* hdr = (DXBCHeader*)(region + i);
        if (memcmp(hdr->magic, "DXBC", 4) != 0 || hdr->reserved != 1 ||
            hdr->size < (DWORD)kDXBCHeaderSize || i + hdr->size > regionSize ||
            HasDXBCSubBlob(hdr)) {
            continue;
        }
        std::vector<BYTE> tmp(hdr->size);
        memcpy(tmp.data(), hdr, hdr->size);
        int counts[4] = {};
        if (PatchShaderBuf(tmp.data(), hdr->size, 2.2f, counts) != 4 ||
            counts[0] != 1 || counts[1] != 1 || counts[2] != 1 ||
            counts[3] != 1) {
            continue;
        }
        const BYTE* ck = (const BYTE*)hdr->checksum;
        Wh_Log(L"sRGB candidate at +0x%zx, size %lu, checksum "
               L"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
               i, hdr->size, ck[0], ck[1], ck[2], ck[3], ck[4], ck[5], ck[6],
               ck[7], ck[8], ck[9], ck[10], ck[11], ck[12], ck[13], ck[14],
               ck[15]);
    }
}

This also settles the question behind the fallback you deferred. Your measurement of 37 over-matches was for constant-only detection; the selector PatchShaderBuf actually implements today — all four splats, each exactly once, in a single leaf blob — is much narrower, and this pass tells you (and reporters) exactly how many blobs it yields on other builds. If it reliably yields four, the fallback becomes a small change rather than a research project; if it doesn't, you have the data to say so in the code comment.

2. The README has no recovery path if DWM doesn't come up.

This was raised in an earlier round and is still missing. The guards make it unlikely — an allowlisted checksum means byte-identical content, and ChecksumRoundTrips runs before any write — but this is the desktop compositor: unlike an Explorer mod, a user who ends up with a broken session can't just kill the process and can't reach Windhawk's UI to disable the mod. A few lines under "Known limitations" turn that from a dead end into a two-minute fix. Windhawk ships a documented flag for exactly this (see Command-line-flags):

## If something goes wrong

If DWM fails to start or the desktop is unusable after enabling this mod, boot
Windows into Safe Mode (Windhawk does not inject there) or start Windhawk with
`windhawk.exe -safe-mode`, then disable or uninstall the mod.
Optional improvements

Minor polish — none of this affects users, so it's your call.

  • Skip past a leaf once it's patched. After a successful WriteMemorySafe, the scan resumes at i + 4 and walks the whole patched shader body. Nothing inside a leaf can be another target, and a byte pattern in there that happens to look like a container header would send the scan on an arbitrary i += hdr->size - 4 jump — potentially past real targets, which silently degrades to a partial mask and a full revert. One line removes both the risk and the work:

    Wh_Log(L"  Patched shader #%d at %p (size %lu)", matchIdx, (void*)hdr, hdr->size);
    i += hdr->size - 4;  // loop adds 4, landing just after the blob
  • On the whole-blob write (last round's item 4) — I don't think it's worth changing for the race argument. patched is a copy of the blob with only the four splats and the checksum altered, so memcpy rewrites every other byte with the value already there; a concurrent reader can only observe a change in exactly the bytes that delta writes would touch anyway. What delta writes would actually buy is fewer copy-on-write pages in a critical system process and not retaining a full copy of each blob in PatchRecord::original — both small. Worth a sentence in the code so the next reader doesn't re-derive it.

  • The advanced-settings path in the README is one level short. The actual UI path is Settings → Advanced settings → More advanced settings → Process inclusion list. The mod does nothing at all if the user can't find it, and the numbered steps skip the "More advanced settings" level. The other DWM mods word it loosely enough to dodge the issue — see dwm-custom-projection-border.wh.cpp#L21-L27.

  • kPatchConsts[0] is still a 0.0f placeholder that is never read, since index 0 always takes gamma. The comment underneath explains it, but the dead slot is easy to just remove.

Functionality notes

Non-critical observations about the feature behavior itself.

  • The fourth component of the splat is still left at the sRGB value. A DXBC 4-component immediate is four consecutive floats; the 12-byte pattern matches components 0-2 and rewrites those, leaving component 3 as 2.4f / 0.04045f / etc. The counts[ci] == 1 check can't detect this, because the in-place replacement destroys the overlapping match at j + 4 before the loop reaches it. This is almost certainly fine (the shader presumably only uses .xyz) and it matches upstream, but since it's invisible to your guards it's worth confirming once against the disassembly rather than inheriting it.

  • Nothing tells the user the patch actually applied. Given that a stale allowlist is a silent no-op, a line in the README — enable logging in the mod's advanced settings and look for All 4 target shaders patched — would let people distinguish "the mod didn't apply" from "HDR is off" or "I didn't restart DWM" before filing anything. Pairs with item 1.

  • The read-only section walk covers every non-writable section, including .rsrc, .pdata and .reloc, and HasDXBCSubBlob runs over each candidate blob's full size (404 of them by your own count). It's a few milliseconds on DWM's startup path, so not worth optimizing — just noting where the time goes if you ever measure it.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 6, 2026
@millerpb

millerpb commented Aug 6, 2026

Copy link
Copy Markdown
Author

Okay, I've addressed the remaining points, optional improvements, and verified the fourth component of the splat concern:

I disassembled all four target blobs directly from the installed dwmcore.dll using D3DDisassemble. In the SM4 (lib_4_0) sections, the only paths that run on any HDR-capable hardware, every sRGB constant is a 4-component immediate of the form l(x, x, x, 0.000000), and every instruction that references them uses .xyz only. The .w slot is always 0, is never read, and is never touched by our patch. Confirmed non-issue.

@millerpb

millerpb commented Aug 6, 2026

Copy link
Copy Markdown
Author

Made some more changes to target more eotf values in dwm. This generally fixes some of the odd flicker in scRGB apps. Only seems occur on occasion now on window resize, then back to expected scRGB on interaction.

@millerpb

millerpb commented Aug 6, 2026

Copy link
Copy Markdown
Author

/ai-review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-for-author The author's turn: request an AI review, or respond to one that was posted.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants