Direct3D 9 for Wine on macOS, backed by Metal.
mtld3d replaces Wine's d3d9.dll. The PE side implements the
application-facing D3D9 API and records commands for the native Unix runtime,
which translates them into Metal command buffers and submits them. The goal is
the fastest Direct3D 9 implementation for Wine on macOS. Direct3D 8 on the
same core is planned (#788).
Every other Direct3D version is a non-goal: D3D10 and later are already served
on macOS by Apple's D3DMetal and by DXMT.
Conformance serves speed: where matching D3D9 exactly would cost frame time
and no game breaks, speed wins. Those trades are listed in
docs/STATUS.md.
- Shader translation. Shader Model 1 to 3 and the fixed-function pipeline are translated to Metal Shading Language by mtld3d's own translator.
- Multithreading. The game's thread records each draw and state change into fixed records. Translation into Metal, submission and presentation run in a three-thread pipeline in the native Unix library that stays a fixed one frame ahead of the game, so latency is bounded. Those threads and the shader workers take no address space from a 32-bit game, and under an arm64 Wine they run as native arm64 code.
- Background shader builds. Shaders and pipelines a game uses for the first time build on worker threads. A frame waits once for all the builds it needs, not once per draw, and a pipeline whose shaders are ready builds ahead of shaders still queued. Asynchronous compilation goes further on a cold cache: a draw into a target the game rebuilds every frame is skipped for a frame or two instead of waited for, so the game does not stutter. Asynchronous compilation is on by default. Turned off, those draws wait with the rest.
- Shader cache. On by default. Translated shaders and pipelines are kept in a file next to the game and built again before the first frame of the next launch, so combinations seen before do not compile during play. Turned off, every launch compiles on first use.
- Frame pacing. Vsync and the frame cap both use Metal's minimum-duration present. Unlike pacing the game's thread or enabling display sync on the Metal layer, this works with ProMotion: the panel follows whatever rate the game sustains below its maximum. Vsync follows the game's present interval; the frame cap is off by default.
- Upscaling. Off by default. The game can render below the presented size and have MetalFX's spatial scaler upscale the result. A mip LOD bias, on by default, keeps texture detail at the presented size; turned off, textures are sampled for the smaller render size.
- HDR output. On by default. On a display with EDR headroom the frame is expanded into that headroom by inverse tone mapping that follows the live headroom. Turned off, or on a display without EDR headroom, the frame is presented as SDR.
- Software cursor. The cursor is drawn in its own overlay window independently of the game's frames, so it is not tied to the frame rate and has hardware-cursor latency. Under HDR it is tone-mapped like the frame, where the macOS cursor stays at SDR brightness. Showing or hiding it does not delay the next present by a refresh as the hardware cursor does. It is on by default whenever HDR output is active, and can be forced on or off. Turned off, the game gets the hardware cursor.
- Game profiles. The few games that need settings of their own get a built-in profile.
Every switch, with its default, is in mtld3d.conf.
docs/STATUS.md lists what is implemented, what is not
yet, what never will be, and the divergences from D3D9 kept on purpose.
- macOS 15 or 26, on Apple Silicon or Intel. CI covers macOS 15 and 26 on Apple Silicon and macOS 15 on Intel, because no macOS 26 Intel runners exist. Intel on macOS 26 is expected to work.
- A Wine from wine-build, based on CrossOver 26; CI pins a release of it. Older Wine or CrossOver releases are not expected to work.
- CrossOver 27's arm64 Wine is wired into the Makefile and tested locally, but not run in CI.
- A 64-bit prefix or bottle. 32-bit games run in it through WoW64.
- Rosetta 2 for an x86_64 Wine. An arm64 Wine translates x86 itself.
- x87sidecar under an x86_64 Wine. D3D9-era games do their floating-point math in x87 instructions, which Rosetta 2 translates slowly; the wine-build releases carry its patch.
Download mtld3d.tar.xz from
GitHub Releases. It installs
either as a Wine builtin, replacing the stock d3d9 of a Wine tree you own, or
as a native DLL override per prefix, which is the route for CrossOver.
INSTALL.md, also inside the bundle, has the steps for both.
Its Fullscreen section shows how to keep a
fullscreen game's mode change virtual.
mtld3d reads mtld3d.conf from the directory of the game's .exe at every
Direct3DCreate9 and keeps it for that interface and its devices. A missing
file means defaults. The MTLD3D_CONFIG environment variable takes the same
entries, separated by semicolons.
Each option is resolved from four layers. A later layer wins, key by key:
- The built-in default.
- The built-in profile for the running game, if there is one.
mtld3d.conf.MTLD3D_CONFIG.
A profile matches on the executable name plus the version resource its
vendor linked in, and the log names the profile that matched. The
sample mtld3d.conf documents every option, its default and
why it exists. app_profile.rs lists the
profiles and the reason for each setting they make.
Every process writes mtld3d-logs/<exe>-<pid>.log next to the executable,
never to the standard streams. RUST_LOG filters it: unset, everything logs
at info, and RUST_LOG=mtld3d=warn quiets the whole project. The log
targets, the levels and the F12 frame capture are described in
docs/ARCHITECTURE.md.
docs/GAMES.md lists the games tested so far and
how to report one that fails. Each release
also carries mtld3d-debug.tar.xz, the symbols that make a crash in a log
readable. If mtld3d does not seem to load at all, start with the
Troubleshooting section of INSTALL.md.
make setup # once: toolchains and SDKs, but not Wine
make # build every shipped binary
make install # install into the Wine tree WINE_SDK namesdocs/BUILDING.md covers the prerequisites, the tests,
the arm64 Wine and the release bundle.
d3d9.dll implements the COM API, keeps the state the game can observe, and
records each frame into fixed records. mtld3d.dll is the PE shim that owns
Wine's unix-call globals. mtld3d.so is the native runtime on the host: an
encoder thread translates the recorded frame into Metal commands, a submit
thread commits them, a presenter thread presents, and worker threads compile
shaders and pipelines and prewarm the shader cache.
docs/ARCHITECTURE.md has the boundary contract, the
threading model and the debugging toolkits.
CONTRIBUTING.md is the operating manual, and
docs/CONVENTIONS.md holds the code rules.
zlib.