Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

Diablo II: Resurrected on macOS (Apple Silicon)

Complete setup from a clean Mac to a playable game, including the four separate bugs that must be fixed and the one that can't be.

Battle.net was discontinued for macOS and D2R is a DirectX 12 title, so nothing here works out of the box. Every fix below was root-caused and verified by measurement — the reasoning is included so you can check it rather than trust it.

Verified on: Apple M3 Max · macOS 26 (Darwin 25.5) · Wine 10.0 · Battle.net 2.52.6 (17627) · D2R 3.2.92777 · MoltenVK 1.4.2

Status: fully playable in Legacy (2D) graphics mode. HD mode runs at good framerate but has a terrain lighting defect — see Known limitation.

Diablo II: Resurrected running on macOS via Wine, Legacy graphics mode

D2R running on an M3 Max under Wine — Rogue Encampment in Legacy mode, terrain rendering correctly, smooth framerate. Online play through Battle.net.


Contents

  1. Read this first
  2. The bugs
  3. Requirements
  4. Step 1 — Wine
  5. Step 2 — Prefix
  6. Step 3 — Battle.net
  7. Step 4 — Patch libcef.dll
  8. Step 5 — Lock the patch
  9. Step 6 — MoltenVK 1.4.2
  10. Step 7 — Launcher
  11. Step 8 — Download D2R
  12. Step 9 — Play
  13. Known limitation — HD terrain
  14. Recovery / after updates
  15. Troubleshooting
  16. Appendix — how each bug was found

Read this first

Step 4 modifies a Blizzard binary (libcef.dll) — two bytes, so a debug assertion stops firing. It is not a cheat, gives no gameplay advantage, and affects only the launcher's embedded browser, not the game or anti-cheat.

Blizzard's ToS discourages modifying their client and modified files can in principle be detected. Your account, your risk. Originals are backed up; reverting is one command. Without it the launcher cannot complete a login at all.

Performance note: D2R runs x86-64 under Rosetta 2 with DX12 translated to Metal. It plays well, but this is not native.


The bugs

# Symptom Root cause Fix
1 Launcher traps with int3 during login libcef.dll asserts VirtualProtect returned PAGE_READWRITE (4); Wine reports image-backed copy-on-write pages as PAGE_WRITECOPY (8), so a successful call trips the assert Patch 2 branches to NOP (Step 4)
2 Launcher window solid black; native chrome fine Chromium's GPU process shares surfaces with the browser process via Windows handle duplication Wine can't reproduce --in-process-gpu (Step 7)
3 D2R shows title screen then freezes; cursor still moves MoltenVK falls back to "semaphores using Metal implicit guarantees within a single queue", which can't satisfy DX12 cross-queue fences MoltenVK ≥ 1.4 + MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE=2 (Steps 6–7)
4 Half-second hitches while running Pipeline state objects compiled on-demand, stalling the frame. Plus MoltenVK writing 194 MB of descriptor warnings mid-session SHOULD_MAXIMIZE_CONCURRENT_COMPILATION=1, USE_METAL_ARGUMENT_BUFFERS=1, LOG_LEVEL=1 (Step 7)
5 HD terrain renders black in ambient-lit areas Metal cannot blend integer-format render targets — see Known limitation Play in Legacy mode (G)

Plus a critical non-bug: launching D2R.exe directly hangs on "Connecting to Battle.net". The client performs a session handoff supplying account credentials; a direct launch skips it and dies with D2BackendServices error 7. Always use Play.


Requirements

  • Apple Silicon Mac, macOS Sonoma or later
  • Homebrew
  • ~30 GB free for D2R
  • A Battle.net account that owns Diablo II: Resurrected

Step 1 — Install Wine

brew install --cask wine-stable
wine --version      # expect wine-10.0

Wine 11.x devel/staging was tested and is worse — Battle.net deadlocks at startup and never draws a window. Stay on 10.0.


Step 2 — Create the prefix (must report Windows 10)

Blizzard refuses to serve D2R to a client reporting an old Windows version.

export WINEPREFIX="$HOME/.wine"
wineboot -i
winecfg -v win10
wine reg query "HKLM\Software\Microsoft\Windows NT\CurrentVersion" /v ProductName

Step 3 — Install Battle.net

The macOS installer won't work; fetch the Windows one:

cd ~/Downloads
curl -L -o Battle.net-Setup.exe \
  "https://battle.net/download/getInstallerForGame?os=win&locale=enUS&version=live&gameProgram=BATTLENET_APP"
file Battle.net-Setup.exe          # expect PE32 executable
WINEPREFIX="$HOME/.wine" wine Battle.net-Setup.exe

Click through, then quit. Don't try to log in yet — it crashes until Step 4.


Step 4 — Patch libcef.dll — fixes the launcher crash

patch_cef_scan.py (in this folder) scans for this sequence and neuters both traps:

FF 15 xx xx xx xx   call [VirtualProtect]
85 C0               test eax, eax
74 15               je   -> __debugbreak    <-- NOP NOP
83 7D F8 04         cmp  dword [ebp-8], 4   (PAGE_READWRITE)
75 12               jne  -> __debugbreak    <-- NOP NOP

Dry run (changes nothing without --apply):

cd ~/Desktop/D2R-macOS-Setup
python3 patch_cef_scan.py ~/.wine/drive_c/Program\ Files\ \(x86\)/Battle.net/Battle.net.*/libcef.dll

Expect unpatched sites: 1 and preceded by call: True. If it reports 0 sites and 0 already-patched, stop — the binary differs from what this targets.

Apply (creates .orig backups):

python3 patch_cef_scan.py ~/.wine/drive_c/Program\ Files\ \(x86\)/Battle.net/Battle.net.*/libcef.dll --apply

Step 5 — Lock the patch

The client verifies and restores libcef.dll at startup (observed happening mid-session). Make it immutable:

chflags uchg ~/.wine/drive_c/Program\ Files\ \(x86\)/Battle.net/Battle.net.*/libcef.dll

Optional — stop Wine's crash dialog wedging the app behind a modal:

WINEPREFIX="$HOME/.wine" wine reg add "HKCU\Software\Wine\WineDbg" \
  /v ShowCrashDialog /t REG_DWORD /d 0 /f

Step 6 — Upgrade MoltenVK — fixes the DX12 freeze

Wine 10.0 bundles MoltenVK 1.3.0, whose semaphore emulation deadlocks DX12 games.

cd ~/Downloads
curl -L -O https://github.com/KhronosGroup/MoltenVK/releases/download/v1.4.2/MoltenVK-macos.tar
tar -xf MoltenVK-macos.tar

TGT="/Applications/Wine Stable.app/Contents/Resources/wine/lib/libMoltenVK.dylib"
cp "$TGT" "$TGT.orig"                                   # back up 1.3.0

# Wine runs x86_64 under Rosetta -- use that slice
lipo MoltenVK/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib \
     -thin x86_64 -output /tmp/libMoltenVK_x64.dylib
cp /tmp/libMoltenVK_x64.dylib "$TGT"
codesign --force --sign - "$TGT"                        # replacing a dylib breaks the signature

Verify in the log after launching: MoltenVK version 1.4.2, and no mention of "within a single queue".


Step 7 — Create the launcher

Copy battlenet-launch.sh from this folder, or recreate it:

#!/bin/bash
LOG="$HOME/bnet-crash.log"
echo "===== launch $(date) =====" >> "$LOG"

export WINEPREFIX="$HOME/.wine"
export WINEDLLOVERRIDES="mfplat,mf,mfreadwrite,msmpeg2vdec,winedmo,mfmp4srcsnk,msauddecmft,rtworkq="

# Bug 3: real Metal events instead of the single-queue fallback (needs MoltenVK >=1.4)
export MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE=2

# Bug 4a: correct descriptor path. Without it MoltenVK creates pools with 0
# uniform-buffer descriptors and allocates dynamically PER DRAW, logging 1.25M
# warnings / 194 MB in one session.
export MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS=1

# Bug 4b: keep logging at errors only. Warning-level logging alone causes hitching.
export MVK_CONFIG_LOG_LEVEL=1

# Bug 4c: compile pipelines concurrently instead of stalling the frame that needs
# them. This is what made it "buttery smooth".
export MVK_CONFIG_SHOULD_MAXIMIZE_CONCURRENT_COMPILATION=1

export MVK_CONFIG_USE_METAL_PRIVATE_API=1
export MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE=1024

# DO NOT SET MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS=1 -- tested twice, causes stutter
# and does not fix terrain (texture cancellations went 35 -> 0 with no visual change).

cd "$WINEPREFIX/drive_c/Program Files (x86)/Battle.net" || exit 1

# Bug 2. Do NOT add --disable-gpu*: that leaves no compositor (blank grey window)
# because SwANGLE can't initialise under Wine (no VK_KHR_win32_surface).
exec /opt/homebrew/bin/wine "Battle.net.exe" --in-process-gpu >> "$LOG" 2>&1
Make it a double-clickable .app
APP="$HOME/Desktop/Battle.net.app"
mkdir -p "$APP/Contents/MacOS"
cp ~/Desktop/D2R-macOS-Setup/battlenet-launch.sh "$APP/Contents/MacOS/launch"
chmod +x "$APP/Contents/MacOS/launch"
cat > "$APP/Contents/Info.plist" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>CFBundleName</key><string>Battle.net</string>
  <key>CFBundleIdentifier</key><string>local.wine.battlenet</string>
  <key>CFBundleExecutable</key><string>launch</string>
  <key>CFBundlePackageType</key><string>APPL</string>
  <key>NSHighResolutionCapable</key><true/>
</dict></plist>
PLIST
touch "$APP"

Step 8 — Download D2R

Run the launcher, sign in, select Diablo II: Resurrected, click Install (~30 GB).

Downloads are handled by Agent.exe, a plain Win32 process that doesn't touch the browser component — it's reliable and resumes if interrupted.


Step 9 — Play

Launch the game with the client's Play button. Never run D2R.exe directly.

Direct launch renders fine but hangs on "Connecting to Battle.net" — the session handoff never happens (presence=0, D2BackendServices error 7). Play also passes the MVK_CONFIG_* variables to the game by inheritance, which is what keeps graphics working.

Then press G in-game to switch to Legacy (2D) graphics mode. See below.


Known limitation — HD terrain

In HD mode, ground renders black in areas that rely on ambient light. Directly-lit and emissive areas are fine (River of Flame looks perfect); dark wilderness and interiors lose most of the ground. Everything else — props, walls, characters, effects, UI — renders correctly.

Workaround: press G for Legacy graphics mode. Legacy uses the original 2D sprite renderer, bypasses the HD terrain pipeline entirely, and works perfectly.

Cause — Wine's vkd3d cannot create cube-map SRV descriptors. Running with WINEDEBUG=+d3d12, the D3D12 layer reports once at startup:

vkd3d:fixme:vkd3d_create_null_srv Unhandled view dimension 0x9.

0x9 is D3D12_SRV_DIMENSION_TEXTURECUBE. Cube maps are how the renderer supplies ambient / image-based lighting — the environment probe lighting everything not hit by a direct light source.

Dumping the translated Metal shaders (MVK_CONFIG_SHADER_DUMP_DIR) confirms the connection: 7 shaders sample texturecube<float>, and the SPIR-V declares SPV_EXT_descriptor_indexing, so those cube maps are fetched bindlessly from a descriptor heap. A heap whose cube entries were never created yields no ambient term:

  • surfaces lit directly (torches) or emissively (lava) render correctly
  • surfaces relying on ambient light render black

This is a Wine/vkd3d limitation, not a Metal one, and is fixable only upstream in Wine's D3D12 implementation. All 152 shaders translate cleanly with zero unsupported constructs, so the shader code is fine — the fault is in what gets bound to it.

Ruled out by measurement — don't repeat these:

Theory Disproof
Primitive-restart pipelines failing Disassembled libMoltenVK.dylib: calls MVKBaseObject::reportWarning (0xfa20), not reportError — non-fatal, pipelines are created
R32_UINT blending breaking terrain Same disassembly test — also reportWarning, non-fatal. (An earlier revision of this README wrongly blamed this.)
Descriptor pool exhaustion Argument buffers took spam 1.25M → 0; ground still black
Texture streaming race Sync submits took cancellations 35 → 0; ground still black
Channel swizzle MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE=1; no change
Texture resolution / AO / any quality setting All settings at minimum; ground still black
vkd3d-proton as a replacement D3D12 Fails to initialise on Wine's winevulkan even with MoltenVK 1.4.2
Broken shader translation 152 SPIR-V → 152 MSL, zero conversion failures

Recovery / after a Blizzard update

An update can restore the original libcef.dll and the launcher crash returns.

BN=~/.wine/drive_c/Program\ Files\ \(x86\)/Battle.net
chflags nouchg $BN/Battle.net.*/libcef.dll          # unlock
python3 ~/Desktop/D2R-macOS-Setup/patch_cef_scan.py $BN/Battle.net.*/libcef.dll --apply
chflags uchg $BN/Battle.net.*/libcef.dll            # re-lock

The patcher finds the pattern by signature, so it survives version changes.

Revert everything:

# libcef
BN=~/.wine/drive_c/Program\ Files\ \(x86\)/Battle.net
chflags nouchg $BN/Battle.net.*/libcef.dll
for d in $BN/Battle.net.*/; do cp "$d/libcef.dll.orig" "$d/libcef.dll"; done

# MoltenVK
TGT="/Applications/Wine Stable.app/Contents/Resources/wine/lib/libMoltenVK.dylib"
cp "$TGT.orig" "$TGT" && codesign --force --sign - "$TGT"

Troubleshooting

Symptom Cause Fix
int3 / "Exception raised" at login Patch reverted by update Recovery section above
Launcher window solid black --in-process-gpu missing Step 7
Launcher window blank grey --disable-gpu* present Remove those flags
Game freezes at title, cursor moves MoltenVK too old, or vars not inherited Step 6; launch via Play
Stuck on "Connecting to Battle.net" Ran D2R.exe directly, or stale session Quit D2R and Battle.net, relaunch client, use Play
Client spins at 100% CPU, frozen UI Transient; also seen once with argument buffers Quit fully and relaunch
Stuttering returned SYNCHRONOUS_QUEUE_SUBMITS got re-added Remove it
"Not available on macOS" Prefix reports wrong Windows version Step 2
Exits instantly, no window WINEESYNC mismatch with running wineserver wineserver -k, relaunch

Logs

tail -f ~/bnet-crash.log                                                    # launcher + MoltenVK
tail -f ~/.wine/drive_c/Program\ Files\ \(x86\)/Diablo\ II\ Resurrected/blz-log.txt   # game (truncated per launch)

Harmless noise: NVSDK_NGX_D3D12_Init failed (DLSS, NVIDIA-only), no Kerberos/NTLM support, Failed to find: data/hd/pak_manifest.json (assets live in CASC archives), Metal does not support disabling primitive restart (non-fatal), most fixme: lines.


Appendix — how each bug was found

Bug 1 — the libcef crash, disassembled

Always libcef.dll + 0x16D00E1, raising 0x80000003 STATUS_BREAKPOINT with no Check failed: message — which is why normal Chromium debugging advice doesn't apply.

0x116D00A0: push ebp
0x116D00BA: push edx                    ; arg4 = &old_protection
0x116D00BB: push 2                      ; arg3 = PAGE_READONLY
0x116D00BF: call dword ptr [...]        ; KERNEL32!VirtualProtect
0x116D00C5: test eax, eax
0x116D00C7: je  0x116D00DE              ; call failed     -> int3
0x116D00C9: cmp dword ptr [ebp-8], 4    ; old == PAGE_READWRITE?
0x116D00CD: jne 0x116D00E1              ; no              -> int3   <-- crash

The IAT slot resolves to KERNEL32.dll!VirtualProtect. This is Chromium's protected-memory guard: flip a page read-only and assert it was previously writable. Windows always satisfies it; Wine reports image-backed COW pages as PAGE_WRITECOPY, so the check fails on a call that succeeded. It fires whenever a renderer touches protected memory — guaranteed during login.

Bug 3 — proving the DX12 freeze with frame-diff

Path: D2R (DX12) → d3d12.dll → vkd3d → Vulkan → MoltenVK → Metal.

MoltenVK 1.3.0 logs "Vulkan semaphores using Metal implicit guarantees within a single queue". Metal has no native cross-queue semaphore, so it's emulated; DX12 uses multiple queues, so a fence never signals. Frames stop, the process stays alive at low CPU, the cursor still moves (that's the OS, not the game).

Measured by screenshotting the window twice 8s apart and counting changed pixels:

Config Frame delta Result
MoltenVK 1.3.0, default 0.000% dead
SEMAPHORE_SUPPORT_STYLE=2 1.9–6.1% animating
MoltenVK 1.4.2 + style 2 ~12% animating (menu)

Rejected alternatives: vkd3d-proton 3.0.1 can't initialise on Wine's winevulkan (Failed to load instance procs); Apple GPTK/D3DMetal (Gcenx build) is wine 7.7, too old and exits before init; Wine 11.x deadlocks at startup; Whisky is deprecated since April 2026 and won't launch.

Bug 4 — the 194 MB log

Without argument buffers MoltenVK creates descriptor pools with zero uniform buffer descriptors and falls back to a dynamic allocation per draw call — logging VkDescriptorPool exhausted pool of 0 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER 1,252,090 times, writing 194 MB to disk during play. Fixing the cause (argument buffers) and silencing the channel (LOG_LEVEL=1) removed most hitching; concurrent pipeline compilation removed the rest.


Credits & license

Produced from a full root-cause debugging session: PE disassembly of libcef.dll and libMoltenVK.dylib, Chromium/CEF process-model analysis, and MoltenVK synchronization testing with objective frame-diff measurement.

Public domain / CC0. No warranty — you are modifying software on your own machine at your own risk.

About

How to play D2: Resurrected on MacOS

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages