A Direct3D 9 → Metal 4 driver for Wine/CrossOver on Apple Silicon. No Vulkan, no MoltenVK: D3D9 calls are translated to Metal directly, for 32-bit and 64-bit games.
It pairs DXVK's D3D9 front-end (state tracking, resources, the whole D3D9 API
surface) with a Metal 4 backend and its own shader compiler, which turns D3D9
shader bytecode straight into Apple's AIR, so no Metal toolchain or MSL is
involved at runtime. Metal is reached through a fork of DXMT's winemetal
bridge.
Fork. This is a fork of neo773/d9mt-v2, the successor of neo773/d9mt. neo773 wrote d9mt and d9mt-v2, archived the original and permitted this fork. The fork adds 64-bit (x64) builds, macOS 27 fixes, rendering fixes, persistent shader and pipeline caches, asynchronous shader compilation and memory profiles. Its full history, neo773's included, is in this repository.
Tested with a handful of games. It has been run with:
- GTA IV (32-bit): neo773's target while developing d9mt and d9mt-v2
- Star Wars: The Old Republic (64-bit): this fork's main test target
- Star Wars Galaxies: Legends (32-bit, community server)
Nothing else has been tried yet, so expect unimplemented paths on other games. d9mt is not affiliated with or endorsed by the publishers or operators of these games; all trademarks belong to their owners.
YourGame.exe (32- or 64-bit Windows PE, x86 code under Rosetta 2)
│ loads
▼
d3d9.dll ← THIS PROJECT (mingw PE): DXVK D3D9 front-end + d9mt Metal 4 backend
│ DXSO bytecode → dxbc-spirv IR (sm3 parser) → serialized to the bridge
▼
winemetal.dll ← THIS PROJECT (fork of DXMT's bridge, PE side, Wine builtin)
│ Wine unix call
▼
winemetal.so ← THIS PROJECT (x86_64 Mach-O, Wine's unix side):
IR → AIR (LLVM 15 bitcode) → metallib, AIR + pipeline disk caches,
Metal 4 command path ──► Metal ──► CAMetalLayer
- Experimental, but playable in the games above. In our testing, the 64-bit MMO renders correctly and holds 60 fps with vsync at 4K on an M5 Max.
- Metal 4 only: MTL4 command queues, allocators, argument tables, residency sets and the MTL4 compiler. There is no Metal 3 path.
- Shader compiler: SM1–3 bytecode is parsed with dxbc-spirv and compiled
to AIR by d9mt's own LLVM-based writer (
air/) on Wine's unix side. The fixed-function pipeline uses DXVK's ubershaders, precompiled into an embedded metallib. - Caches: compiled shaders go to a persistent AIR cache and pipelines to Metal 4 pipeline archives, so a second visit to an area is much smoother than the first. Shaders and pipelines compile on worker threads. A pipeline state cache records which pipelines the game used and rebuilds them in the background in later sessions, as soon as the game has created their shaders, including after a d9mt or macOS update that invalidates the other caches (docs/state-cache.md).
- Memory profiles pick pool sizes and reported video memory from the Mac's RAM (8/16/24 GB Macs get smaller pools).
- This is research-grade software for one platform (Apple Silicon + a CrossOver-based Wine). Expect rough edges.
- Apple Silicon Mac on macOS 26 or later (Metal 4). Only macOS 27 has been tested; see Known issues.
- Rosetta 2: Wine's unix side, and with it
winemetal.so, runs as x86_64. - A Wine engine with an x86_64 unix side and the winemac hooks DXMT's
bridge uses (
macdrv_get_metal_layer). Tested with the CrossOver 24.0.7 engine; engines of CrossOver-based wrappers should work. Vanilla upstream Wine is untested. 32-bit games need 32-on-64 (wow64) support, as in CrossOver.
To build (see below): Homebrew bash (≥ 4.3), mingw-w64 (i686 and
x86_64), cmake, ninja, python3, Xcode Command Line Tools; optionally
Xcode with the Metal Toolchain plus glslang and spirv-cross to regenerate
the embedded metallibs.
From the repo root. The scripts need bash ≥ 4.3 (/bin/bash 3.2 fails), so
put Homebrew's first in PATH.
LLVM and winemetal.so target macOS 26.0, whatever the SDK; set
MACOSX_DEPLOYMENT_TARGET to change it. An LLVM built before this default
(with the SDK's version) gives linker warnings: rebuild it. The embedded
metallibs come in two builds: the SDK's newest Metal language (MSL 4.1 with
Xcode 27) and a Compat build for that minimum. The driver loads the newest
and falls back to Compat where Metal rejects it (MSL 4.1 on macOS 26).
export PATH="/opt/homebrew/bin:$PATH"
# 1. Fetch dxbc-spirv, pinned to c5c1a5b9, and apply patches/dxbc-spirv
# (-> third_party/dxbc-spirv). build-driver.sh refuses an unpatched copy.
scripts/fetch-deps.sh
# 2. Build LLVM 15.0.7 as static x86_64 libs (-> toolchains/llvm-x86_64).
# Once; a large clone and a long build.
scripts/build-llvm-unixside.sh
# 3. The bridge: winemetal.so (x86_64 Mach-O, includes the AIR compiler) and
# winemetal.dll for i386 and x86_64, marked as Wine builtins.
scripts/build-bridge.sh
# 4. The driver: build/driver-x64/d3d9.dll (64-bit games) and/or
# build/driver/d3d9.dll (32-bit games).
ARCH=x64 scripts/build-driver.sh
ARCH=x86 scripts/build-driver.sh- No DXVK or DXMT checkout and no Wine engine are needed to build;
driver/andbridge/contain everything. build-driver.shregeneratesdriver/d3d9/d3d9_ff_metallib.handdriver/backend/mtl_blit_metallib.hwhen their sources (or the script) are newer andxcrun metal,glslangValidatorandspirv-crossare available; otherwise it keeps the committed headers. On Xcode 27 runxcodebuild -downloadComponent MetalToolchainonce. Ifxcode-selectpoints at the Command Line Tools, the script falls back to/Applications/Xcode.app.build-bridge.shends with a smoke test that needs reference metallibs fromscripts/build-air-compiler.sh(Homebrewllvm@15and a SPIRV-Cross checkout inthird_party/SPIRV-Cross). Without them the test is skipped;SMOKE=0skips it explicitly.- The driver and the bridge share an ABI: always rebuild and install them together.
Absolute build paths are mapped out of the binaries (-ffile-prefix-map),
but the driver is built with -g and is about 80 MB. Strip before
installing or shipping:
x86_64-w64-mingw32-strip build/driver-x64/d3d9.dll
i686-w64-mingw32-strip build/driver/d3d9.dll
strip -x build/bridge/winemetal.soA binary release must carry the notices listed in THIRD_PARTY_NOTICES.md.
Native unit tests (no Wine or GPU needed), from the repo root:
clang++ -std=c++17 -I driver/d3d9 -I driver/mtl test/native/buffer_tracking_test.cpp -o /tmp/btt && /tmp/btt
clang++ -std=c++17 -I driver/util test/native/fenced_pool_test.cpp -o /tmp/fpt && /tmp/fpt
clang++ -std=c++17 -I driver/d3d9 test/native/hitch_test.cpp -o /tmp/ht && /tmp/ht
clang++ -std=c++17 -I driver/mtl test/native/memory_profile_test.cpp -o /tmp/mpt && /tmp/mpt
clang++ -std=c++17 -I test/native/stubs -I driver/backend test/native/upload_ring_test.cpp driver/backend/mtl_ring.cpp -o /tmp/urt && /tmp/urt
clang++ -std=c++17 -I driver/backend test/native/attachment_opt_test.cpp -o /tmp/aot && /tmp/aot
clang++ -std=c++17 -I driver/d3d9 test/native/stats_test.cpp -o /tmp/st && /tmp/st
clang++ -std=c++17 -I driver/d3d9 test/native/pace_test.cpp -o /tmp/pt && /tmp/ptscripts/test-state-cache.sh runs the pipeline state cache tests (file
format, corruption, versions, cap, record → load → prewarm with a fake
backend), plain and under ThreadSanitizer, plus the render-thread cost of
recording; it needs DXVK's native headers like test-compile-mt.sh.
scripts/run-statecache-bench.sh runs test/code_statecache.c, a synthetic
SWTOR-like many-pipeline workload, under Wine with every cache in a scratch
directory.
scripts/sm3_hazard_test.py checks the D3D9 SM3 hazard warning (shader
patterns dxbc-spirv's SM3 converter mishandles, GitHub issue #8) under Wine:
ENGINE=<engine with this build's bridge> [FRAMEWORKS=<dir>] [WORK=<scratch dir>] scripts/sm3_hazard_test.py build/driver-x64/d3d9.dll [<dir of .asm/.bin>...]
(corpora from scripts/sm3_corpus.py gen). scripts/sm3_ref_test.py (same
environment) renders crs/setp with every write mask, modifier and
predicate and packed I/O declarations on the GPU and compares the results
with a scalar model of the D3D9 semantics; --strict uses
d3d9.floatEmulation = Strict, --corpus <dir> adds a differential test of
every corpus crs against a mul/mul/add expansion.
scripts/test-compile-mt.sh runs the compile-path concurrency tests (with
ThreadSanitizer) against the built bridge. The scripts under scripts/run-*
and scripts/dump-verify.sh drive the D3D9 test programs in test/ under a
CrossOver bottle (CX and BOTTLE must be set).
scripts/run-hang-test.sh is the profiling/debug regression test: it runs
test/code_mtlock.c (a render thread presenting frames while a second thread
locks managed textures, on a device without D3DCREATE_MULTITHREADED, as
SWTOR does) with D9MT_PROFILE, D9MT_MEMSTATS, DXVK_LOG_LEVEL=info and a
D9MT_DEBUG_FILE flags file, in a throwaway prefix, and fails if a run doesn't
finish within the timeout:
ENGINE=<Wine engine root> scripts/run-hang-test.sh --arch x64 # or --arch x86Nothing measurably slower merges. PERFORMANCE.md is the binding guide: how to
measure (quiet window, uncapped, paired rounds, layout controls), the benchmarks, the D9MT_PROFILE
diagnostics and the design rules for the hot path. How to contribute:
CONTRIBUTING.md.
Use a dedicated Wine engine copy. d9mt's
winemetalreplaces the engine's builtinwinemetal, which DXMT (D3D10/11) also uses. DXMT games stop working on that engine. Don't modify/Applications/CrossOver.appitself: updates overwrite it and it is code-signed.
-
Copy a Wine engine, e.g. CrossOver's
Contents/SharedSupport/CrossOveror your wrapper's engine folder. Call it$ENGINE. -
Install the bridge into the engine (originals are kept as
*.cx-orig;--restoreputs them back):CX="$ENGINE" scripts/install-bridge-builtins.shwhich copies
build/bridge/winemetal.sotolib/wine/x86_64-unix/,build/bridge/winemetal64.dlltolib/wine/x86_64-windows/winemetal.dllandbuild/bridge/winemetal32.dlltolib/wine/i386-windows/winemetal.dll. -
Deploy the driver as
d3d9.dll: next to the game's.exe, or into the prefix (drive_c/windows/system32/for a 64-bit game,drive_c/windows/syswow64/for a 32-bit one).cp build/driver-x64/d3d9.dll "$GAME_DIR/d3d9.dll" # 64-bit game cp build/driver/d3d9.dll "$GAME_DIR/d3d9.dll" # 32-bit game
-
Launch with
d3d9overridden to the native (our) DLL, using the engine from step 1:WINEDLLOVERRIDES="d3d9=n,b" "$ENGINE/bin/wine" --bottle "My Game" \ start /unix "$GAME_DIR/YourGame.exe"
--bottleis CrossOver's; with plain Wine setWINEPREFIXinstead. The override can also go inwinecfg→ Libraries or your wrapper's DLL override setting.
To check that it's active, set DXVK_LOG_PATH=<dir> and look for
<game>_d3d9.log, or enable the Metal HUD (MTL_HUD_ENABLED=1).
d9mt reads DXVK-style configuration: dxvk.conf next to the game's .exe
(or the file in DXVK_CONFIG_FILE), with environment variables taking
priority where both exist. Under CrossOver, environment variables reach the
game only through the bottle's cxbottle.conf ([EnvironmentVariables]),
not through a shell export.
The D3D9 options of DXVK's front-end apply (d3d9.*, dxvk.maxFrameRate,
...). Options added or worth knowing:
| Option | Default | Effect |
|---|---|---|
d3d9.presentInterval |
-1 (the game decides) |
1 forces vsync, e.g. for games that request immediate present in borderless mode and tear |
d3d9.maxFrameRate / dxvk.maxFrameRate |
0 (off) |
frame-rate cap |
d3d9.maxAvailableMemory |
-1 (= the memory profile's value) |
video memory (MB) reported to the game |
d3d9.allowOutOfRangeVsDefs |
False |
accept hardware vertex shaders whose only float constants ≥ 256 are def immediates (advanced) |
d9mt.memoryProfile |
auto |
auto, low, medium, high, ultra or unlimited (see below); env D9MT_MEMORY_PROFILE |
d9mt.reportedVramMB |
per profile | video memory reported when d3d9.maxAvailableMemory is unset |
d9mt.memoryBudgetMB |
per profile | soft Metal budget; currently only logged |
d9mt.writeOnlyDirect |
True (False in unlimited) |
map DEFAULT-pool WRITEONLY buffers directly, like upstream DXVK; env D9MT_WRITEONLY_DIRECT |
d9mt.writeOnlyDemoteLocks |
2 (0 in unlimited) |
a directly mapped buffer locked in place more often than this goes back to a staging copy; 0 = never; env D9MT_WRITEONLY_DEMOTE |
d9mt.memoryStats |
False |
log a DIAG MEM line every 256 frames; env D9MT_MEMSTATS (1, or a frame interval ≥ 16) |
d9mt.resourceLabels |
False |
label Metal resources for Xcode's GPU tools; env D9MT_LABELS |
d9mt.pipelineWaitBudgetMs |
-1 (off: wait as long as it takes) |
most the render thread waits per frame for shaders/pipelines still compiling before it skips the draws that need them (see below); 0 = never wait (= D9MT_ASYNC_SKIP=1); max 10000; env D9MT_PSO_WAIT_MS |
d9mt.attachmentOpt |
True |
attachment load/store optimization (see below); D9MT_NO_ATTACHMENT_OPT=1 turns it off |
d9mt.stateCache |
True |
pipeline state cache: record the pipelines the game uses, rebuild them in the background in later sessions (docs/state-cache.md); D9MT_STATE_CACHE=0 turns it off too |
Memory profiles. auto picks by installed RAM: up to 8 GB low, up
to 16 GB medium, up to 24 GB high, below 64 GB ultra, otherwise
unlimited. The tiers set the reported video memory (low 2 GB, medium
3 GB, otherwise 4 GB) and how much the driver keeps pooled
(staging blocks, staging bytes in flight, spare buffer backings, idle
upload-ring chunks, command allocators). unlimited behaves like the driver
before profiles existed and is meant for A/B comparisons.
Pipeline wait budget. When a draw needs a shader or pipeline that is
still compiling on the worker threads, the render thread waits for it by
default: no draw is ever missing, but a slow compile is a hitch.
D9MT_ASYNC_SKIP=1 never waits and skips such draws instead: no hitch, but
objects pop in a few frames late. d9mt.pipelineWaitBudgetMs = N sits in
between: the render thread waits for pending compiles, but at most N ms per
frame in total (each wait gets what is left of the frame's N ms, so many
pending pipelines can't stack up their waits). Draws whose compile isn't done
when the budget runs out are skipped for that frame, as with
D9MT_ASYNC_SKIP; the compile carries on and the draws come back once it is
done. Most compiles finish within a few ms and are still waited for; only
the rare slow one is skipped, so a budget of e.g. 30 caps compile hitches
at about 30 ms in exchange for occasional brief pop-in. While a draw is
skipped, occlusion queries that cover it count fewer samples. Clears and
blits are never skipped (their pipelines are built on the spot). D9MT_ASYNC_SKIP=1 takes
precedence and acts as a budget of 0; without compile workers
(D9MT_COMPILE_THREADS=0) the budget is ignored. With D9MT_PROFILE, the
COMPILE and HITCH lines count skipped draws (skipped=) and waits cut
short by the budget (budgetHit=).
Attachment load/store optimization. A Metal render pass loads each attachment from memory when it starts and stores it when it ends. d9mt creates a pass's encoder only when the pass ends, so it knows what the pass did, and it tracks for each single-subresource attachment image whether its contents are a known clear value:
- An attachment that holds a known clear (from creation or a clear) is Clear-loaded instead of read from memory.
- If no draw of the pass writes it and nothing samples it during the pass,
its store is skipped. Its contents are still the clear value: the next pass
Clear-loads it, and any other reader (sampling, copies, blits, resolves,
readbacks, Present) gets it by a re-clear first (
restoreinSTATS). Sampling reads only the view's aspects: a depth-stencil texture sampled as a shadow map or INTZ (depth) doesn't need its unstored stencil clear. - Once a reader has needed such a clear (a target cleared every frame and sampled or copied later in the frame), the passes of the next 8 frames store it as before instead of leaving it to a re-clear; the hint lapses when the reads stop.
- A pass without draws whose clears needn't be stored gets no encoder at all; the clear moves into the next pass's load action.
- An MSAA color resolve (StretchRect) right after the pass that rendered the
source becomes that pass's
StoreAndMultisampleResolve, instead of a pass of its own that loads every sample again. - With
D3DPRESENTFLAG_DISCARD_DEPTHSTENCIL, the pass still rendering into the auto depth-stencil at Present doesn't store it, and the next pass doesn't load it (wined3d discards it too).
Everything else is loaded and stored as before. Written contents are never
dropped: when a pass ends, nothing tells whether a later pass needs them
(only D9MT_DEFERRED's whole-frame rules can). In SWTOR this removes the
load and store of the 2048² R32F color target that its shadow passes bind with
color writes off, about 32 MB per shadow pass. Memoryless textures aren't
used: D3D9 surfaces outlive the pass. no_attach_opt in the
D9MT_DEBUG_FILE flags switches back to the previous actions live, for A/B
tests; att_trace logs the decisions and the restores. With D9MT_PROFILE, a
RESTORE line follows each second's STATS line when there were restores:
the most frequent images (Metal format, size, aspects re-cleared), why (sample,
copy, blit, resolve, readback, partial-clear, ...) and the D3D9 call
they came from, e.g. 120x 2048x2048 Depth32Float_Stencil8 s sample@Draw;
RESTORE-SUMMARY has the session's totals when the device is destroyed. D9MT_DEFERRED turns it off (deferred
passes have their own load/store rules).
| Variable | Default | Effect |
|---|---|---|
D9MT_AIR_CACHE |
on | persistent compiled-shader cache (<user cache dir>/d9mt/air-cache.sqlite3); 0 turns it off, a path relocates it |
D9MT_PSO_ARCHIVE / D9MT_PSO_ARCHIVE_DIR |
on | Metal 4 pipeline archives in <user cache dir>/d9mt/pso/ (last 8 sessions); 0 turns them off, _DIR relocates them |
D9MT_STATE_CACHE |
~/Library/Caches/d9mt/state/<exe>.d9mtstate |
pipeline state cache file; 0 turns the cache off, a Unix directory or .d9mtstate file path relocates it. Launchers should set it to "$(getconf DARWIN_USER_CACHE_DIR)d9mt/state", next to the AIR cache and PSO archives. Without it, d9mt uses that dir if Wine passes TMPDIR (CrossOver's doesn't), else ~/Library/Caches/d9mt/state (home from WINEHOMEDIR), else the cache stays off (logged) |
D9MT_STATE_CACHE_MAX |
1024 |
most pipelines kept per game (least recently used dropped first), 64–16384; every session's PSO archive holds every prewarmed pipeline, ≈ 45 KB each |
D9MT_STATE_CACHE_THREADS |
2 |
background prewarm threads (lowest priority, the second one only for long queues), 0–4; 0 records and loads but never prewarms |
D9MT_COMPILE_THREADS |
min(4, cores/2) |
shader/pipeline compile worker threads; 0 compiles on first use |
D9MT_PS_PREFETCH |
1 |
compile pixel shaders at creation; 0 waits until a draw needs them |
D9MT_ASYNC_SKIP |
off | 1 skips draws whose pipeline isn't ready yet: less stutter, brief pop-in; overrides D9MT_PSO_WAIT_MS |
D9MT_PSO_WAIT_MS |
off | wait at most this many ms per frame for pending pipelines, then skip (see d9mt.pipelineWaitBudgetMs); off or -1 overrides a dxvk.conf value |
D9MT_GPU_CANARY |
on | 0 turns off the GPU canary: the present copy's encoder (and an encoder of its own in every 4th other command buffer) marks the command buffer's slot in a 16 KiB buffer; a command buffer that completed without its mark was discarded by the GPU (after a page fault: always the command buffers queued after it, the faulting one if the fault came before the mark); D9MT-GPU-CANARY: on once the first mark arrived (or off and why), so a log shows the canary worked and is a D9MT-GPU-LOST line in the log (the first 20, then a summary at most every 10 s, and the session's total). macOS itself only logs IOGPUMetalError in its unified log |
D9MT_NO_ATTACHMENT_OPT |
off | 1 turns the attachment load/store optimization off (see d9mt.attachmentOpt) |
D9MT_BG_FPS / D9MT_BG_FPS_MINIMIZED |
30 / 10 |
frame cap while the window is hidden / minimized; 0 turns it off |
D9MT_DRAWABLE_TIMEOUT_MS |
max(3 frames, 50 ms) | longest wait for a drawable before a present is skipped |
D9MT_AIR_FASTMATH |
off | 1 restores full fast-math in compiled shaders (faster, but can produce NaN specks) |
D9MT_METALFX |
off | MetalFX spatial upscaling at present. Currently broken (selector bug; fix pending) |
D9MT_MEMORY_PROFILE, D9MT_WRITEONLY_DIRECT, D9MT_WRITEONLY_DEMOTE, D9MT_MEMSTATS, D9MT_LABELS |
see the dxvk.conf table |
|
DXVK_LOG_PATH, DXVK_LOG_LEVEL |
none, info |
log directory and verbosity, as in DXVK |
Developer and diagnostic switches
| Variable | Effect |
|---|---|
D9MT_PROFILE |
per-frame timing lines (PSOBUILD, AIRCOMPILE, compile counters), HITCH lines that attribute frames longer than D9MT_HITCH_MS (default 50) to d9mt or to the game, and a STATS line every second: fps, Present-to-Present frame time (avg/p99/max, frames over 33 and 50 ms), per-frame render passes, skipped passes, command buffers, draws, attachment loads/stores and the estimated attachment traffic saved, the render thread's CPU time in the attachment optimization (attMs, ms per second) and clears folded into a pass or given one of their own, compiles and render-thread compile waits, Metal's allocated MiB (fields: driver/d3d9/d3d9_stats.h), plus a RESTORE line in seconds with attachment restores and RESTORE-SUMMARY at device destruction (driver/mtl/mtl_restore_stats.h), and a PACE line every second: the interval between the game's Present calls (min/avg/max, standard deviation) and the Present thread's time per frame inside d9mt (and how much of it is waiting) and outside it, with its CPU time per second split into d9mt's and the game's own (driver/d3d9/d3d9_pace.h). No GPU time: MTL4 command buffers don't report it |
D9MT_TRACE |
verbose trace lines |
D9MT_LIFETIME_CHECK |
1: a texture d9mt releases before the GPU is past the last command buffer that uses it (sampled, attachment, copy) is a D9MT-LIFETIME line and an abort, on any Mac, without needing the GPU to fault; log: the lines only (the first 100, then every 1000th). A drawable's texture (Core Animation holds it too) is logged as D9MT-LIFETIME drawable, never aborts (driver/backend/mtl_lifetime.h) |
D9MT_GPU_CANARY_TEST_FAULT |
<n>: the n-th armed mark is written far outside the canary buffer, which Metal drops, so the mark stays unwritten: tests the D9MT-GPU-LOST detection (not a GPU fault). Tests only |
D9MT_WSILOG |
window-system / presenter log (bridge, stderr) |
D9MT_DEBUG_FILE=<Windows path> |
live debug flags file, polled once a second: occ_visible, occ_trace, occ_sync, no_scissor, no_depthbias, cull_none, depth_clamp, no_ztest_blended, no_sameview_skip, no_clipplanes, bools_off, no_attach_opt (previous attachment load/store actions), att_trace (log up to 400 render-pass load/store decisions and restores each time it's set), and dump to dump the next frame's draws. Off unless set |
D9MT_SHADER_DUMP_PATH=<Windows dir> |
disassembly of vertex shaders rejected (or accepted via d3d9.allowOutOfRangeVsDefs) for using more than 256 float constants. Off unless set |
D9MT_AIR_ERRLOG=<host path> |
append the AIR compiler's reason for rejecting a shader. Off unless set |
D9MT_DUMP_TRIGGER=1 |
backbuffer dump for the test scripts (C:/d9mt-test/DUMP trigger) |
D9MT_GPUCAPTURE=<n>|trigger, D9MT_GPUCAPTURE_COMMITS |
Xcode GPU trace to /tmp/d9mt.gputrace |
D9MT_PSO_FLUSH_EVERY |
pipelines between archive flushes (default 256) |
D9MT_STATE_CACHE_TEST_NO_VALIDATE=1, D9MT_STATE_CACHE_TEST_FAIL_PREWARM=1 |
test builds only (STATE_CACHE_TESTING=1 scripts/build-driver.sh): skip the state cache's content validation to test its crash-loop guard with a real Metal crash (scripts/forge-statecache.py); make prewarm builds fail after 150 ms |
STATECACHE log lines (info) |
load (file, entries, how many can be prewarmed, invalid= entries dropped by content validation, a warning if the last session ended during prewarm builds (crash, kill, or exit without releasing the device): only the entries in flight then are dropped), prewarm (each time the background queue drains: built, already built by a draw, failed, time paused for the render thread), use (every 60 s and at the end: PSO misses served by a finished prewarm hits, by a still-building one late, not prewarmed misses, new records, the build time the hits saved, boosts: prewarm threads raised to normal priority for a late hit), write |
D9MT_DEFERRED, D9MT_XFER_HOIST |
experimental: deferred render passes; hoisting vertex/index uploads into an earlier command buffer |
D9MT_NO_SAMEVIEW_SKIP, D9MT_NO_PSOMRU, D9MT_NOPSOCACHE, D9MT_NO_PASSMERGE, D9MT_NO_RULE_{B,C,E,F}, D9MT_NOCOLORCLEAR, D9MT_NO_AB, D9MT_RTSKIP, D9MT_STALL_PACE=0 |
A/B escape hatches that turn individual optimizations off |
PROFILE_INSTRUMENT=1 (build) + D9MT_PROFILE_INSTRUMENT=1 |
per-function CPU profiler build |
- Conflicts with DXMT on the same engine. The bridge replaces the
engine's
winemetal; DXMT's D3D10/11 entry points are stubbed out and the render-pass struct has grown, so D3D10/11 games using DXMT break or crash on that engine. Use a dedicated engine copy until the bridge gets its own module name. - Driver and bridge must come from the same build. There is no version handshake; mixing them can crash.
- MetalFX upscaling (
D9MT_METALFX) is broken (selector bug); a fix is pending. - First-time stutter in new areas while shaders and pipelines compile.
Compiles run on worker threads and are cached on disk, so later visits are
smoother.
D9MT_ASYNC_SKIP=1trades stutter for pop-in;d9mt.pipelineWaitBudgetMscaps the stutter and only skips what takes longer. - The disk caches have no size cap, except the state cache (1024 pipelines per game, ≈ 390 KB). The AIR cache is one sqlite file shared by all games (entries from other driver builds are dropped); the pipeline archives keep the last 8 sessions.
- macOS 26 is barely tested. Before the Compat metallibs (see Build), builds with Xcode 27 embedded only MSL 4.1 metallibs, which Metal on macOS 26 rejects ("language version 4.1 … not supported"): a black screen. Checked on 26.6.2 in a VM, whose paravirtual GPU has no Metal 4, so d9mt itself can't run there; no in-game test on 26 yet.
- Gamma ramps (
SetGammaRamp, used by some brightness sliders) are ignored. - Not implemented or unverified, from code review: instance divisors
1, indexed triangle fans, stride-0 vertex streams, and hardware vertex shaders that address more than 256 float constants (rejected, as in DXVK; see
d3d9.allowOutOfRangeVsDefs).
- The D3D9 front-end in
driver/d3d9is DXVK's, extracted and with Vulkan removed (driver/EXTRACTION-LOG.md,driver/UPSTREAM). It tracks all D3D9 state and records work through DXVK's command-stream thread into a Metal seam (driver/mtl). - The backend (
driver/backend) implements that seam on Metal 4: argument tables, residency sets, MTL4 command buffers and allocators, render and compute encoders, MTL4 pipeline compilation, the upload ring and presentation to the window'sCAMetalLayer. - Shaders: the game's DXSO bytecode is parsed by dxbc-spirv's
sm3front-end into dxbc-spirv's IR ind3d9.dll, serialized and passed through the bridge. On Wine's unix side,airlib/airlower the IR to AIR (LLVM 15 bitcode) and wrap it in a metallib, which Metal loads directly. Results are cached in the AIR cache. - The bridge (
bridge/) is DXMT'swinemetalwith additions (bridge/UPSTREAM): vertex-descriptor pipelines, per-slot samplers and vertex textures, the AIR compile calls and the full Metal 4 command surface, plus persistent pipeline archives. - The pipeline state cache (
driver/mtl/mtl_state_cache*) lives entirely ind3d9.dll: it records D3D9-side shader identities plus the derived pipeline key, and rebuilds through the same compile path as a draw (docs/state-cache.md). docs/dev-notes/has neo773's design notes and research from v2's development (historical, not maintained).
- neo773: author of d9mt and d9mt-v2: the Metal 4 backend, the AIR shader compiler, the bridge extensions and the DXVK extraction this fork is built on.
- DXVK (Philip Rebohle, Joshua Ashton and contributors): the D3D9 front-end and its fixed-function shaders.
- DXMT (Feifan He / CodeWeavers): the
winemetalbridge that carries Metal calls across Wine's PE/unix boundary. - dxbc-spirv (Philip Rebohle): the shader bytecode parser and IR.
- LLVM: bitcode writing for AIR.
- SPIRV-Cross and glslang: build-time conversion of the fixed-function shaders.
- Wine and CodeWeavers: the platform all of this runs on.
- Fork maintenance, x64, memory and rendering work: Jan Drlý.
See the licenses of the third-party components in
THIRD_PARTY_NOTICES.md and
LICENSES/. d9mt's own code follows suit:
- d9mt's own code and the DXVK-derived driver are under the zlib/libpng
license (
LICENSE), DXVK's license, with DXVK's copyright notice kept.driver/is an altered version of DXVK, not the original. - The winemetal bridge in
bridge/follows DXMT and is under the GNU LGPL 2.1 or later (bridge/LICENSE,bridge/COPYING.LIB).