Skip to content

Repository files navigation

mtld3d

Direct3D 9 translation layer for Wine on macOS, backed by Metal.

mtld3d replaces Wine's built-in d3d9.dll with an implementation that translates D3D9 calls through Wine's PE/Unix boundary into Metal command buffers on the host. The pure-Rust core (mtld3d-core) handles DXSO → MSL shader translation, render-pass scheduling, and fixed-function state.

Goal

mtld3d aims to be the fastest Direct3D 9 implementation for Wine on macOS. Direct3D 8, on the same core, is planned and part of that goal. Every other Direct3D version is a non-goal; D3D10/11/12 are already well served on macOS by Apple's D3DMetal and by DXMT.

Conformance serves that goal rather than defining it: the implementation is developed against Wine's d3d9 test suite and every divergence it reports is triaged and written down (see Testing). But where matching D3D9 exactly would cost frame time, speed wins, as long as the divergence does not keep a game from running. Those trades are listed under Faster than conformant, and where a knob makes sense they are revertible from mtld3d.conf.

Apple Silicon is what is developed and tested today. Intel Macs are a goal, not a claim: the non-uniform-memory paths are written but unverified on real hardware.

Installation

Download the release bundle (mtld3d.tar.xz) from GitHub Releases.

INSTALL.md, which also ships inside the bundle, has the requirements and the walkthroughs for both routes: builtin, which drops into a Wine installation you own and replaces the stock d3d9 for that whole tree, and native override, loaded per game through d3d9=native while the Wine installation stays untouched, which is what CrossOver bottles use. It also covers x87sidecar, worth running under an x86_64 Wine because Rosetta 2 is slow at the x87 math D3D9-era games do.

Status

Implemented

  • Programmable pipeline: vertex and pixel shader models 1.x through 3.0 (DXSO bytecode → MSL translation), including vPos/vFace, flat shading, and the D3D9 half-pixel rasterization convention.
  • Fixed-function pipeline, vertex and pixel: lighting (directional, point, spot), texture-coordinate generation, the full texture-stage cascade, material color sources, hardware vertex blending.
  • Fog: vertex fog and per-pixel table fog, across the fixed-function, pre-transformed (RHW), and programmable paths.
  • All four draw paths: DrawPrimitive / DrawIndexedPrimitive and both UP variants.
  • Vertex streams: all sixteen SetStreamSource streams feed a draw, with per-stream offsets and strides; a declared stream with nothing bound reads zeros, as on hardware.
  • Hardware instancing: SetStreamSourceFreq with INDEXEDDATA / INSTANCEDATA, including per-instance step rates, on the indexed draws (D3D9 never instances a non-indexed draw).
  • State blocks: recorded (Begin/End) and D3DSBT_* snapshots.
  • Queries: occlusion queries backed by real Metal visibility results; event queries.
  • Resources: DXT1–5 and ATI1 compressed textures, the common uncompressed integer and float formats, cube and volume textures, mipmap auto-generation, managed-pool dirty-region uploads, StretchRect (including cross-format blits via a conversion pass), GetDC read-back.
  • Depth: sampleable depth textures (INTZ, DF16, DF24) with hardware shadow-compare PCF, depth bias and slope-scale bias, depth clamp for pre-transformed geometry.
  • Stencil: the full test, both faces, and every stencil operation, with Clear(D3DCLEAR_STENCIL) bounded to the viewport independently of depth.
  • Sampling and output: anisotropic filtering, sRGB read (compressed formats) and sRGB write, alpha test, scissor, separate alpha blend, blend factor, color write masks.
  • Multiple render targets: four simultaneous targets with independent formats and write masks, post-pixel-shader blending on each, and Clear reaching every bound target.
  • Presentation: windowed and fullscreen swap chains, adapter mode enumeration, hardware color cursors, and MetalFX upscaling on the way to the screen with render.scale choosing the render resolution. Fullscreen never changes the display mode; what it does instead is under Display-mode switching.
  • HDR: on an EDR-capable display the layer is upgraded to extendedDynamicRange and present routes through a BT.2446-A inverse-tone-mapping pass in ICtCp, its peak following the display's live headroom. On by default; color.hdr.enable and color.space control it.

Not implemented yet

Missing features a D3D9 application can reasonably want. Each fails cleanly, with an absent cap bit or a documented error return, so applications take their own fallback paths instead of breaking:

  • MSAA: multisampled creates are rejected; CheckDeviceMultiSampleType only accepts D3DMULTISAMPLE_NONE.
  • Point sprites and non-solid fill modes (Metal has no native wireframe).
  • TIMESTAMP and the other niche query types: creation reports NOTAVAILABLE, as the spec allows.
  • YUV conversion: YUY2/UYVY surfaces can be created and locked, but no YUV→RGB blit is performed.
  • Depth→depth StretchRect.

Faster than conformant

Divergences from D3D9 kept on purpose, because closing them costs frame time and no game depends on them:

  • GetData(D3DGETDATA_FLUSH) returns S_OK immediately for a pending occlusion query instead of blocking until the GPU has the count. Games use that poll loop as a fence against 2004-era drivers without hazard tracking, which Metal does explicitly, so the wait buys no correctness and stalls the API thread, the frame-time bottleneck. query.flushImmediate = false restores the spec-correct wait.
  • Depth stores are elided where nothing reads the buffer back, so content that relies on depth surviving a pass it never cleared can read stale depth. Preserving it unconditionally would cost the optimization on every frame of every game that does clear, which is all of the tested ones.

Deliberately not implemented

  • D3D9Ex: no Direct3DCreate9Ex, no shared resource handles, no D3D9On12. The extended interface is a different contract (device removal, OS-managed memory) built for the Vista+ compositor; the games this project targets are plain D3D9.
  • Display-mode switching: a fullscreen device owns its window but never the desktop mode, which Wine's mac driver would set through CGDisplaySetDisplayMode, rearranging every other window on the screen. The window goes borderless over the monitor instead, while the back buffer keeps the display mode the game picked, exactly as a real mode-set would leave it, and present scales the frame to the display (MetalFX when enlarging). A request matching no enumerable mode, which a real mode-set would reject, follows the window instead: games that ask for such sizes derived them from their window and keep sizing their rendering and input from it.
  • The fullscreen focus lifecycle: no device loss on deactivation, no focus-window subclassing, no synthesized activation messages. Presentation is a composited Metal layer, so a lost display is never a lost device.
  • Software paths: no reference or software rasterizer, no software vertex processing, no ProcessVertices, no RegisterSoftwareDevice. HAL on the default Metal device is the only device type; multi-adapter setups are not enumerated.
  • Clip-plane application: SetClipPlane state round-trips but planes are not applied on the GPU.
  • Legacy remnants: N-patch/RT-patch tessellation, vertex tweening, palettized textures, gamma ramp. Dead features in real-world content, accepted or rejected per spec but non-functional.

Testing

mtld3d is developed and tested against World of Warcraft 1.12 and 3.3.5a under Wine and CrossOver. No other games have been exercised yet; reports are welcome.

Beyond the game workloads it is hardened against Wine's d3d9 test suite, the de-facto D3D9 conformance suite. make conformance runs it against the installed builtin, one runner process per PE architecture, and gates on a per-site baseline; every remaining divergence is classified with a written rationale, the ones kept for speed included, in unix/conformance/CONFORMANCE.md. The unit and end-to-end suites (make test) run the pure-Rust core natively on the host and the full stack under Wine.

Building from source

mtld3d builds and runs on Apple Silicon macOS. mtld3d.so ships as both an x86_64 and an arm64 Mach-O, since Wine loads it from the lib/wine/<cpu>-unix directory matching its own build; the PE side is x86 either way, translated by Rosetta 2 under an x86_64 Wine and by FEX under an arm64 one. The Metal backend targets macOS 15 or newer (unix/.cargo/config.toml pins MACOSX_DEPLOYMENT_TARGET = 15.0).

Two things have to exist before a build: a Wine build or install providing wine, winebuild and wineserver plus its development tree (lib/wine/{i386,x86_64}-windows/ and libwinecrt0.a), and rustup. Everything else is make setup's job.

WINE_SDK points at that Wine tree and must be exported before any target, including make setup: the Makefile takes the Wine binaries from it by absolute path, windows/shim/build.rs reads libwinecrt0.a and ntdll.a there for linking, and make conformance finds Wine's d3d9 test binaries in it. WINE_INSTALL_DIR is a second tree make install copies into, alongside WINE_SDK itself.

make setup              # one-time toolchain bootstrap
make                    # build all (windows i386+x64 + unix)
make install            # install into both Wine trees
make bundle             # pack the distributable tarball + its debug symbols (PROD=1 by default)
make test               # unit tests on the host, e2e under Wine, one leg per PE arch
make conformance        # Wine's d3d9 suite vs the checked-in baseline, one leg per PE arch
make check              # the pre-commit gate: fmt + clippy + audit + doc
make fmt                # format all workspaces (requires nightly)
make clippy             # run clippy on all workspaces
make audit              # the conventions clippy can't express (see docs/CONVENTIONS.md)
make doc                # build the docs with rustdoc warnings denied
make clean              # cargo clean both workspaces
make upgrade            # cargo update (semver-compatible) in both workspaces
make upgrade-incompat   # cargo upgrade --incompatible + cargo update

Every leg is also its own CI job, with the toolchains and tools the Makefile leaves floating for a developer pinned in .github/workflows/ci.yml.

make setup is the one-time bootstrap: both rustup toolchains (stable, 1.97 or newer per rust-version in the manifests, for everything; nightly only for make fmt, whose rustfmt.toml uses nightly-only options) with the four cross-compilation targets, the cargo tools, the PE linker and archiver as symlinks onto the toolchain's own rust-lld and llvm-ar, Rosetta 2 if it is missing, and the pinned Windows SDK splatted to /opt/xwin (~3 GB, and one sudo prompt to create that root-owned directory). It does not install Wine. The internal crates are path dependencies and are not published to crates.io.

Frame pointers are off by default; FP=1 make forces them on for the guest-pc sampling profiler, whose stack walks follow the guest frame-pointer chain.

make install copies the PE DLLs into lib/wine/{i386,x86_64}-windows/ and the .so into lib/wine/{x86_64,aarch64}-unix/, stamping the Wine-builtin signature onto the d3d9.dll copies, because the loader ignores unsigned PEs on the builtin search path. The build outputs themselves stay unsigned so they can also serve as a native DLL override, and make bundle packs both flavors into windows/target/mtld3d.tar.xz. Debug symbols travel with every binary, a .pdb beside each PE and a .dSYM beside the .so (make runs dsymutil, since Mach-O DWARF otherwise stays behind in the object files); make bundle writes them as windows/target/mtld3d-debug.tar.xz, and every DLL logs its release and linker-assigned image ID on load, so a crash report names the archive that symbolicates it.

Architecture

game.exe → d3d9.dll → mtld3d.dll → mtld3d.so
(PE, i386 or x64: one chain per arch)  (Mach-O, Wine's own arch)
  • d3d9.dll: D3D9 API implementation, COM vtables, caps, state management.
  • mtld3d.dll: PE shim that owns Wine's unix-call globals and exports mtld3d_unix_call.
  • mtld3d.so: native macOS side, a pure Metal abstraction layer.
  • mtld3d-core: host-testable pure-Rust rlib linked into d3d9.dll.

At runtime the frame flows through a three-thread pipeline:

API thread (the game's)     Encoder thread            Submit thread
───────────────────────     ──────────────            ─────────────
record frame N+1        →   encode frame N        →   submit + present frame N−1
  • The API thread is the game's own render thread and the frame-time bottleneck, so it never waits on translation, Metal, or the GPU: each call only snapshots the state it needs into a closure on the frame's op list.
  • The encoder thread (one per device) runs those closures: D3D9 → Metal translation, render-pass scheduling and load/store optimization, pipeline and sampler caches, lazy resource creation and texture uploads.
  • The submit thread crosses the PE/Unix boundary to replay the finished command stream, waits for the drawable, presents and commits.

Each hand-off has capacity one, so the pipeline never runs more than one frame ahead per stage: backpressure, not queueing, bounds latency.

For the boundary contract, the threading details, perf instrumentation, and the shader/heap debugging toolkits, see docs/ARCHITECTURE.md.

Workspaces

Two Cargo workspaces, one per target platform: windows/ builds the PE side for i686-pc-windows-msvc and x86_64-pc-windows-msvc, unix/ the Mach-O side for x86_64-apple-darwin and aarch64-apple-darwin (the latter is also the native test target). Open each in its own editor window for rust-analyzer to work. The shipped crates (both workspaces also hold test, types, and conformance support crates):

Crate Workspace Output
d3d9 windows/ d3d9.dll
mtld3d windows/ mtld3d.dll
mtld3d-core windows/ rlib (linked into d3d9.dll)
mtld3d-unix unix/ mtld3d.so
shared unix/ rlib (shared by d3d9.dll, mtld3d.dll, mtld3d.so)

mtld3d-core holds every platform-independent helper (DXSO → MSL emission, the render-pass state machine, the slab allocator, format / FVF / vertex-decl / dirty-rect math, fixed-function state) and compiles for the macOS host as well as PE, so cargo test -p mtld3d-core --target aarch64-apple-darwin runs its unit tests natively instead of through Wine.

unix/shared is the crate every linkage unit depends on, primarily for the PE↔Unix wire format (the Command enum, the Thunks enum, param structs, typed mtl:: wire values). Pure data and pure-Rust helpers only, no FFI and no #[link], so both workspaces can depend on it cleanly.

Configuration and logging

User-facing runtime options live in the optional mtld3d.conf, read once at Direct3DCreate9 from the directory of the running .exe, so a change needs a restart. The sample in the repo root documents every option, its default, and the MTLD3D_CONFIG environment override that beats the file.

Every crate logs via log + env_logger. All targets sit under mtld3d::* and env_logger matches by ::-separated prefix, so RUST_LOG=mtld3d=warn is the single switch for the whole project; narrow it per target, for example RUST_LOG=mtld3d=warn,mtld3d::unix=trace.

Target Scope
mtld3d::d3d9 windows/d3d9/ + windows/core/ (everything except dxso and perf)
mtld3d::d3d9::cursor hardware cursor (HCURSOR) lifecycle, bitmap cache, wndproc
mtld3d::dxso DXSO → MSL emitter
mtld3d::perf 5-second averaged performance summary (PERF=1 builds only)
mtld3d::shim Wine unix-call PE shim DLL
mtld3d::unix Metal-side .so

Levels: info! for one-shot milestones, warn! for unimplemented stubs and fallback paths, error! for unexpected internal failures, trace! for per-call breadcrumbs, debug! for routine per-call noise useful in deep debugging.

Each cdylib initializes the logger independently and idempotently; mtld3d.so has no owning entry point, so d3d9.dll dispatches a one-shot InitLogger thunk from its init path.

Contributing

CONTRIBUTING.md is the operating manual: what to read before changing anything, the gates and how to read their output, how conformance work is organised, and what a pull request is expected to contain. The development conventions themselves live in docs/CONVENTIONS.md.

The short version: make check and make test are green before every commit, and one pull request is one clearly defined change.

License

zlib.

About

Direct3D9 implementation on top of Metal.

Resources

Contributing

Stars

33 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages