Skip to content

fix(dxvk): deploy markerless native dxgi into prefix when gptk is active - #251

Open
dappermint wants to merge 7 commits into
frankea:mainfrom
dappermint:fix/dxvk-clean-dxgi-fallback
Open

fix(dxvk): deploy markerless native dxgi into prefix when gptk is active#251
dappermint wants to merge 7 commits into
frankea:mainfrom
dappermint:fix/dxvk-clean-dxgi-fallback

Conversation

@dappermint

@dappermint dappermint commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

builds on #253. #248 was already closed by that merge, so this does not carry a fixes line; it finishes the gptk half of the same dxgi problem (#163).

problem

dxvk-macos ships no dxgi.dll and relies on wine's dxgi implementation. when d3dmetal is deployed into the wine runtime, the builtin dxgi.dll becomes apple's forwarder. steam's cef gpu process under dxvk then pairs dxvk's native d3d11.dll with that forwarder, swapchain creation fails, and the client crash-loops into a black window.

copying wine's clean dxgi.dll into system32 does not help on its own: it still carries the 16-byte "Wine builtin DLL" marker at 0x40, so the loader treats it as a builtin and redirects back to the forwarder.

#253 handles the other direction (payload absent, dxmt residue in the prefix). before this pr the two were stacked: a blind removeItem on system32 followed by a deploy, which deleted wine's builtin-marked placeholder on every dxvk launch and never touched syswow64.

fix

enableDXVK now runs one step, reconcileDXGIForDXVK(prefixRoot:gptkOriginalsDXGI:gptkPayloadIsDeployed:), keyed on whether the gptk payload is deployed:

  • deployed: install the backed-up original from originals/ into system32 and strip the marker at 0x40 so wine loads it as a true native pe under n,b. only system32 is written; gptk deploys forwarders into x86_64-windows only, so the 32-bit builtin is still wine's own.
  • not deployed: call fix(dxvk): remove stale native dxgi.dll left by a DXMT launch #253's removeStaleNativeDXGI(prefixRoot:), which sweeps both arches and skips anything builtin-marked, then put the runtime's own marked dxgi.dll back into the slot it emptied. wine's loader only reaches a builtin through its system32 placeholder (find_builtin_without_file in ntdll/loader.c refuses every non-16-bit dll that has no file outside prefix bootstrap), so an empty slot makes LoadLibrary("dxgi.dll") fail in every process. that is the black store, community and profile on v4.6.4 in wine build cannot execute gptk/d3dmetal payloads: unwinder lacks personality routine support in builtin modules #163: chromium's gpu process cannot bring up d3d11 and crash-loops into software. the copy is what wineboot installs, so n,b still defers to the builtin, and a marked file already in place is left alone.

the predicate is the payload, not the store. originals/ outlives an engine install and the payload does not, so a stale store from a previous engine would otherwise read as "gptk active" and deploy a dxgi the current runtime never shipped. the store path and payload predicate are injectable parameters with production defaults; removeStaleNativeDXGI loses its store guard and parameter since the caller owns that decision now.

removeCleanDXGIForDXVK is dropped. it had no caller, and the next dxvk launch reconciles the prefix anyway.

also in this pr

disablingNVAPI now clears nvapi and nvngx alongside nvapi64, so launcher helper processes get every nvidia bridge dll disabled rather than just the 64-bit nvapi one. NVAPIBridgeTests updated to match.

tests

eight cases in WineDXVKResidueTests drive reconcileDXGIForDXVK and removeStaleNativeDXGI directly: deployed installs the stripped original, deployed replaces existing residue, deployed leaves syswow64 alone, stale store replaces residue with the builtin in both arches, not-deployed keeps the builtin placeholder, deployed without a stored original is a no-op, removed residue is replaced by the builtin placeholder, and an existing placeholder is not rewritten. the removal tests inject a runtime folder rather than reading the installed one. the old testNativeDXGIIsKeptWhenGPTKOriginalsExist is replaced by the stale-store case, since that state now takes the removal path by design.

@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.90566% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
WhiskyKit/Sources/WhiskyKit/Wine/Wine.swift 84.31% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

@frankea frankea left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified hands-on: simulated the merge on main, built the app target and WhiskyKit, and ran the full suite (267 tests pass, lint clean). Checked the marker mechanics against the real runtime: e_lfanew is 0x80 on the runtime's dxgi.dll, so the 16-byte write at 0x40 lands entirely in DOS-stub padding, and the loader really does key on that string. A failed strip is caught and degrades to today's builtin redirect. The switch-away paths are covered too: a backend change prunes the stale dxgi=n,b on the next launch, and without an override the loader prefers the builtin (the runtime dxgi carries no PREFER_NATIVE flag), so the lingering native copy is inert outside DXVK. 32-bit is untouched since GPTK never writes i386-windows.

One change before merging: guard deployCleanDXGIForDXVK on GPTKImporter.isDeployed() in addition to the file check. The store survives engine installs but the deployed payload does not, so after an update to a non-gptkCapable runtime the previous engine's originals/ persists (neither prepareOriginals nor remove runs), and every DXVK launch would copy the old engine's dxgi into the prefix and load it as native against the new runtime. isDeployed() closes that: when the payload is not deployed the builtin is already clean and the copy is pointless; when it is, the deploy just refreshed the backups.

Two smaller notes, non-blocking:

  • The nvapi/nvngx widening of disablingNVAPI is a second behavior change the PR body does not mention; worth a line there so it is discoverable from the squash.
  • If GPTK is later un-deployed, the stripped copy stays in the prefix and keeps loading as native under DXVK; it matches the restored builtin at that moment but skews after the next runtime update. Cheap hardening, fine as a follow-up: when the guard fails and a markerless dxgi sits in system32, remove it and let the ,b half load the builtin.

@frankea

frankea commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Follow-up: I now have a live #248 reproduction on this machine (GPTK 4.0b2 imported and deployed on v4.6.4-beta.1, Steam under DXVK, app 3.7.0: bootstrap paints, client black, gpu process crash-looping with 0xC0000005 then 0xC000001D), and I verified this PR against it. An app built from this branch on the same bottle: crash loop gone, system32/dxgi.dll carries the stripped marker, sign-in and the Library render. One residual that is not this PR's problem: remote pages (Store, Community, Profile) stay black, which reproduces payload-less too and bisects to the wine 11.15 to 11.16 window; details on #163.

Related finding from the same session, verified bidirectionally by experiment: on payload-less installs, a DXMT launch leaves its native dxgi.dll in system32, and a later DXVK launch pairs DXVK's d3d11 with it and hard-fails CEF (0x887A0004, no window). I am opening a separate PR that removes a markerless dxgi on DXVK enable when no GPTK originals exist; it composes with this PR, which owns the originals-present case. The isDeployed() guard from my review still stands as the one change requested here.

frankea added a commit that referenced this pull request Sep 1, 2026
DXVK-macOS ships no dxgi.dll, so enableDXVK never overwrote the native
copy a DXMT deploy leaves in system32 and syswow64. A DXVK launch then
paired DXVK's d3d11 with DXMT's dxgi under the n,b override, and that
mix cannot create window swapchains (DXGI_ERROR_UNSUPPORTED,
EGL_BAD_ALLOC in Chromium clients): Steam ran with no window at all
after a bottle switched from DXMT to DXVK.

Verified bidirectionally on a real bottle against v4.6.4-beta.1:
planting DXMT's dxgi reproduces the failure on the next launch and
removing it brings the window back. Details on #163.

The removal stands down when the GPTK importer's originals backup
exists, since the builtin behind the override is then Apple's forwarder
and the prefix needs a clean native copy instead (#251
owns that case). Builtin-marked files are never touched.
frankea added a commit that referenced this pull request Sep 1, 2026
DXVK-macOS ships no dxgi.dll, so enableDXVK never overwrote the native
copy a DXMT deploy leaves in system32 and syswow64. A DXVK launch then
paired DXVK's d3d11 with DXMT's dxgi under the n,b override, and that
mix cannot create window swapchains (DXGI_ERROR_UNSUPPORTED,
EGL_BAD_ALLOC in Chromium clients): Steam ran with no window at all
after a bottle switched from DXMT to DXVK.

Verified bidirectionally on a real bottle against v4.6.4-beta.1:
planting DXMT's dxgi reproduces the failure on the next launch and
removing it brings the window back. Details on #163.

The removal stands down when the GPTK importer's originals backup
exists, since the builtin behind the override is then Apple's forwarder
and the prefix needs a clean native copy instead (#251
owns that case). Builtin-marked files are never touched.
@dappermint

Copy link
Copy Markdown
Contributor Author

pushed the isDeployed() guard as 2f2594b, before your follow-up landed, so it is on the branch now.

heads up before you write the separate PR: that commit also handles the payload-less case, and i think it collides with what you are about to build. the guard's else branch removes a markerless system32/dxgi.dll and returns:

guard GPTKImporter.isDeployed() else {
    let sys32DXGI = bottle.url.appending(path: "drive_c").appending(path: "windows")
        .appending(path: "system32").appending(path: "dxgi.dll")
    try? FileManager.default.removeItem(at: sys32DXGI)
    return
}

that came from your second non-blocking note, the stripped copy going stale after a runtime update. but it fires on the same condition as your DXMT finding: payload-less install, DXVK enable, a native dxgi sitting in system32 from an earlier DXMT launch. so 0x887A0004 should already be covered here.

two differences worth deciding on, both in your favour if you want the separate PR anyway:

  • mine removes unconditionally, it does not check whether the file is markerless or where it came from. a DXMT bottle that gets DXVK enabled loses its dxgi either way, which is correct, but it is blunt
  • mine only runs on the DXVK enable path, so a bottle switched DXMT to DXVK without passing through enableDXVK would not be cleaned

if your PR wants to own the payload-less case properly then say so and i will strip the else branch back to a bare return before you merge, so there is one owner rather than two overlapping deletes. otherwise this covers it and you can drop the separate PR. your call, i do not want to pre-empt work you have already reproduced.

separately, noted on the remote-pages residual bisecting to 11.15 to 11.16 and that it is not this PR's. i will pick that up on #163.

@frankea

frankea commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Ran the real three-way merge onto current main (post #252/#253) and verified the mechanics hands-on. The deploy-and-strip core checks out: the marker string sits at 0x40 in both the runtime's builtin i386 dxgi.dll and the stored original, and stripping a copy of the real stored original changes exactly 16 bytes, leaving a file isNativePE accepts. I can also confirm 2f2594b's premise directly: this machine is in the stale-store state right now (originals/ populated, unix-side payload gone, isDeployed() false), which is exactly where an originals-exist predicate misfires and yours reads it right.

So: right mechanism, but the branch needs a rebase onto current main, and the remaining issues all live at the seam with #253.

  1. enableDXVK conflicts textually with fix(dxvk): remove stale native dxgi.dll left by a DXMT launch #253; both append at the same tail of the function. When you resolve, mind the shared closing brace at the end of the hunk, it is easy to drop.

  2. Rather than stacking both steps, unify them into one isDeployed()-keyed step after enableDXVK: deployed, deploy the stripped copy (your path); not deployed, reuse fix(dxvk): remove stale native dxgi.dll left by a DXMT launch #253's isNativePE-guarded both-arch removal as the body of your else branch. As written, the blind try? removeItem on system32/dxgi.dll deletes wine's builtin-marked placeholder on every DXVK launch (a per-launch prefix mutation fix(dxvk): remove stale native dxgi.dll left by a DXMT launch #253 deliberately avoids, and wineboot keeps recreating the file), and it misses syswow64, so in the stale-store state a 32-bit title keeps the bad DXVK-d3d11 + DXMT-dxgi pairing a DXMT launch leaves there.

  3. removeCleanDXGIForDXVK has no caller on the branch; the pruning your commit message describes actually happens in deployCleanDXGIForDXVK's else branch at the next DXVK launch. Delete it or wire it into the GPTK undeploy path.

  4. Coverage: deployCleanDXGIForDXVK and stripBuiltinMarker hardcode GPTKImporter.isDeployed() and the store folder, so they are untestable as written and the codecov patch number is fair. Main already has the seam to copy: removeStaleNativeDXGI takes an injectable gptkOriginalsDXGI and WineDXVKResidueTests drives it. The same shape here would cover both.

On my merged resolution the app builds, the full kit suite passes (267 tests in 36 suites), and lint is clean at strict, so nothing structural is in the way. Two notes on the body: "fixes #248" points at an issue that #253's merge already auto-closed, so reword or leave it as you prefer; and the end-to-end crash fix under a real deployed payload is still unverified on my side (no payload deployable on this machine), though the loader evidence and the bottle-clone experiments from #253 make it credible.

DXVK-macOS ships no dxgi.dll and relies on Wine's DXGI implementation. When D3DMetal is deployed globally into the runtime tree, the builtin dxgi.dll becomes Apple's forwarder, which causes DXVK-based launchers (like Steam CEF) to fail during device creation.

Copy Wine's backed-up clean dxgi.dll from the GPTK originals store into the prefix system32 and strip the 'Wine builtin DLL' marker at 0x40 so Wine loads it as a native PE when overridden.
The store outlives an engine install but the deployed payload does not, so
a stale originals/ can belong to a previous engine. Without the payload
the builtin is already Wine's own and there is nothing to work around.

When the payload is undeployed, drop any stripped copy left in the prefix
so the next runtime update does not load a stale native dxgi against the
restored builtin.
enableDXVK now runs one reconciliation step instead of stacking a removal
and a deploy. Which dxgi.dll the prefix needs depends on what the builtin
is: with the D3DMetal payload deployed it is Apple's forwarder, which
DXVK's d3d11 cannot pair with, so wine's backed-up original is installed
and marker-stripped; without the payload the builtin is wine's own, so a
native copy in the prefix is DXMT residue and is removed.

The removal side is frankea#253's isNativePE-guarded both-arch helper, called
rather than reimplemented. The old else branch did a blind removeItem on
system32 only, which deleted wine's builtin-marked placeholder on every
DXVK launch (a per-launch prefix mutation wineboot just undoes) and left
syswow64 untouched, so a 32-bit title kept the bad DXVK-d3d11 plus
DXMT-dxgi pairing.

The payload predicate moves out of the removal helper and into the
caller. Keying on originals/ alone misreads the stale-store state, where
a previous engine's backups survive but the payload does not: the store
says GPTK is active while the builtin is already wine's own.

removeCleanDXGIForDXVK is dropped rather than wired into undeploy. It had
no caller, and the reconciliation step already restores the prefix on the
next DXVK launch without a second owner of that path.

deployCleanDXGI and the reconciliation step take injectable originals and
payload-state seams, mirroring removeStaleNativeDXGI, with tests covering
the deployed, not-deployed and syswow64 cases.
@dappermint
dappermint force-pushed the fix/dxvk-clean-dxgi-fallback branch from 2f2594b to cdcbc4a Compare September 1, 2026 21:12
@dappermint

Copy link
Copy Markdown
Contributor Author

rebased and rewritten, going through your four points in order.

1. rebase. on current main now, no textual conflict with #253. the enableDXVK diff is a single line: removeStaleNativeDXGI(prefixRoot:) becomes reconcileDXGIForDXVK(prefixRoot:).

2. one step, not a stack. reconcileDXGIForDXVK is keyed on gptkPayloadIsDeployed. the guard-else calls your removeStaleNativeDXGI(prefixRoot:) as-is rather than reimplementing removal, so the not-deployed path gets the isNativePE guard and both-arch sweep for free. the blind removeItem is gone. one change to your helper: the gptkOriginalsDXGI guard and parameter moved out of it and into the reconcile step, because keying on the store misreads the stale-store case (originals from a previous engine, payload gone). the helper now only answers "is this residue", and the caller decides whether removal is the right answer at all.

3. removeCleanDXGIForDXVK. deleted. i went back and forth on wiring it into undeploy but the reconcile step already restores the prefix on the next dxvk launch, and a second owner of that path is just another thing to keep in sync.

4. seams and coverage. deployCleanDXGI(prefixRoot:from:) takes the originals url, and reconcileDXGIForDXVK takes both the store path and the payload predicate as injectable defaults, mirroring what you had on removeStaleNativeDXGI. six new tests in WineDXVKResidueTests drive it: deployed installs the stripped original and asserts the marker window is gone and isNativePE is true, deployed replaces existing dxmt residue, deployed leaves syswow64 alone, stale store removes residue from both arches, not-deployed keeps wine's builtin placeholder byte-for-byte, deployed with no stored original is a no-op. the existing removal tests dropped the store argument and testNativeDXGIIsKeptWhenGPTKOriginalsExist is replaced by the stale-store test since that state is now supposed to remove.

local checks, being upfront about what i could and could not run on this machine:

  • swiftformat clean, 0/401
  • WhiskyKit builds
  • swiftlint could not run: sourcekit fails against the CommandLineTools-only toolchain here
  • kit tests need ci, no full xcode so XCTest will not resolve locally

so please let ci be the source of truth on lint and the test suite.

removing a markerless native dxgi.dll left the slot empty, and wine's loader
only reaches a builtin through its system32 placeholder: find_builtin_without_file
refuses every non-16-bit dll that has no file outside prefix bootstrap. with
nothing there LoadLibrary("dxgi.dll") fails in every process, chromium's gpu
process cannot bring up d3d11 and crash-loops into software rendering, and
steam's store, community and profile pages go black while the client chrome
and library keep rendering.

it only showed on a runtime whose wine.inf mtime matched the bottle's update
stamp, because any other runtime triggers a prefix update that silently
reinstalls the placeholder. that is why it read as a wine 11.16 regression:
runtime 4.6.4 is the first build that exactly matches the bottles it
populated. the wine side is unchanged.

restore the runtime's own marked copy into system32 and syswow64, which is
what wineboot installs, so the n,b override still defers to the builtin.
reconcileDXGIForDXVK threads the runtime folder through so the payload-absent
path restores it too, and the tests inject a runtime folder rather than
reading the installed one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants