Releases: athei/mtld3d
Release list
v0.7.0
- Cube texture support: GPU backed DEFAULT and MANAGED cubes for the mapped colour and DXT formats, per face and per mip locks and uploads, cube faces as render targets,
D3DUSAGE_AUTOGENMIPMAP, fixed function and programmable sampling, state block capture and restore. ATI1 and YUV cubes stay CPU only scratch resources. - Caps advertise
D3DPTEXTURECAPS_CUBEMAPandD3DPTEXTURECAPS_MIPCUBEMAP.CUBEMAP_POW2stays off. D3DDEVCAPS_HWRASTERIZATIONis advertised, so games that check it no longer read the device as a software rasterizer.- The Gunmetal benchmark starts.
- A fullscreen device gets the resolution it requested when that resolution is an enumerable display mode: the back buffer keeps the mode, the window covers the monitor, and present resamples to the drawable, through MetalFX when enlarging, with
render.scaleon top. - A fullscreen request that is not a display mode follows the monitor covering window as before, logged once. This is what WoW's windowed to fullscreen toggle asks for.
GetDisplayModereports the honoured mode when fullscreen and the desktop mode when windowed.- A fullscreen window the game resizes itself is put back over the monitor, once per size and within a per session budget.
debug.capsAllno longer claims texture restrictions (power of two, square only) that were never intended. Default caps are unchanged bit for bit.INSTALL.mdgained a troubleshooting entry for ad3d9.dllthat never loads, which WoW 1.12 reports as ERROR #132 at0x0063A915.- Conformance against Wine's d3d9 suite improves by 12 sites per architecture.
- Installing is unchanged, see
INSTALL.mdin the bundle.
Full changelog: v0.6.0...v0.7.0
v0.6.0
mtld3d now runs on an arm64 Wine. The bundle ships the unix library for both host architectures, wine/x86_64-unix/mtld3d.so and wine/aarch64-unix/mtld3d.so, and Wine loads the one matching its own build, so the same tree installs into either. Verified against an arm64 Wine rebundled from CrossOver with a 32-bit game: the end to end suite passes there, 154 tests across 18 binaries. The PE side is x86 either way, translated by Rosetta 2 under an x86_64 Wine and by FEX under an arm64 one.
The Direct3D DLLs are now built without BMI1, BMI2 and LZCNT. Those instructions abort module initialisation with an illegal instruction under an arm64 Wine, which surfaced in the game as ERROR #132 at startup. The ISA baseline is nehalem on 32-bit and x86-64-v2 on 64-bit.
The unix crash handler no longer terminates on memory faults raised outside mtld3d. It is installed process wide, and faults from elsewhere are now forwarded to the handler that owned the signal before it. Under an arm64 Wine the x86 translation faults as part of normal operation, so the previous behaviour killed the process on the first one. On x86_64 this also stops the handler swallowing guest faults that Wine translates into Windows exceptions for the game to handle.
Also in this release: crash reports from an arm64 build decode the faulting registers, and the debug archive carries symbols for both unix architectures.
Installing is unchanged, and covered in INSTALL.md inside the bundle.
Full changelog: v0.5.0...v0.6.0
v0.5.0
HDR is now on by default. On a display that reports extended dynamic range, present routes through the BT.2446-A inverse tone mapper (operated in ICtCp, so saturated content stays saturated as it is lifted) and the layer runs as a float surface tagged for extended linear output, with the shader's peak following the display's live headroom frame by frame. Previously this was opt-in behind color.hdr.enable in mtld3d.conf.
The display decides, not the setting. HDR only engages when the attached screen actually reports EDR headroom, so a monitor without it renders exactly the same frames it did before; nothing about the game's rendering changes, only what the final present does with the brightness the panel has available. To go back to the SDR path on a display that does have headroom, set color.hdr.enable = false in mtld3d.conf, or launch with MTLD3D_CONFIG="color.hdr.enable=false".
The reason it was opt-in was that the HDR path shipped young, and it showed: the ICtCp matrix pair went out in two different scalings, which quietly halved every chromatic difference; the drawable was pinned to the game's back-buffer size, so the compositor resampled the whole frame on the way to the screen; and a fresh float drawable could reach the display with nothing written into it, which is what the reported pink flash was. Those are fixed and have been in the last three releases, and what is left is a better picture than the SDR path can give on a panel with headroom, since that path throws away every nit above paper white.
Also in this release: the log line that reports HDR being disabled now names the actual config key. It printed hdr.enable=false, which is not a key anyone can type, and it is exactly the line someone turning HDR off would search for.
Installing is unchanged, and covered in INSTALL.md inside the bundle.
Full changelog: v0.4.2...v0.5.0
v0.4.2
This is a documentation fix release. The binaries are functionally identical to v0.4.1 and differ only in the version stamp.
The CrossOver section of INSTALL.md used to advise leaving the bottle's graphics backend on its default, which was vaguer than the mechanism deserves. The graphics switch, including the Auto setting and the per-application database it consults, only redirects Wine's builtin DLL search, and the d3d9=native override this install route uses bypasses builtin resolution entirely. The note now says plainly that any selection is fine and that d3d9 stays with mtld3d in every position, while other Direct3D versions keep following the bottle's selection.
The note also gained the one caveat that matters: CrossOver's compatibility database can ship per-game DLL overrides that outrank the bottle's registry override. No game is known to carry one for d3d9. If d3d9 ever loads wrong anyway, the tell is wined3d GLSL fixme lines in the log with no mtld3d lines, and running with WINEDEBUG=+cxcompatdb names the applied rule.
Also in this release: the repository README and ARCHITECTURE pages were caught up with the perf gate and the configuration knobs.
Installing is unchanged: unpack mtld3d.tar.xz and follow the INSTALL.md inside (also readable in the repository).
Full changelog: v0.4.1...v0.4.2
v0.4.1
A performance-only patch release. It trims the CPU cost of the per-draw state snapshot, the path the API thread runs on every draw call to hand render state to the encoder thread, which the frame-pointer profiles introduced in 0.4.0 identified as the single hottest piece of the translation layer in busy scenes.
Four things changed, none of them rendering behaviour. The per-frame scratch arena's bump allocation is now split so the common case inlines into its callers; the unsplit function was just large enough that the compiler kept it out of line, and at snapshot rates that meant a full call per 16-byte bump. The bind-time texture checks (pending mip uploads, cross-device migration) now gate inline on a dirty mask and only call out when there is real work, instead of paying an out-of-line call per bound texture per draw that almost always found nothing to do. The texture-stage walk keeps an incremental mask of bound slots and visits only those, rather than probing all 16 stages and re-initialising a 1 KB scratch array per state-dirty draw. And the small fixed-function constant payloads (texture factor, alpha ref, fog colour, bump-env matrices, integer constants) are built in fixed stack buffers instead of round-tripping a heap allocation each time they change.
Measured on a busy scene with the built-in perf summary, the snapshot path drops from about 229 to 202 ns per draw, with the stage-walk share falling from 46 to 28 ns. At the several thousand draws per frame a crowded scene produces, that returns roughly a tenth of a millisecond per frame to the API thread, which is the bottleneck thread in exactly those scenes.
Installing is unchanged, and covered in INSTALL.md inside the bundle.
Full changelog: v0.4.0...v0.4.1
v0.4.0
This release removes the biggest per-frame allocator cost in the translation layer: dynamic vertex and index buffer backings are now recycled through an in-process pool instead of round-tripping the general allocator. The pool is on by default with a 128 MiB cap, configurable as memory.pageboxPoolCapMB in mtld3d.conf (0 disables it and restores the old behaviour).
The problem it fixes: every contended lock of a DYNAMIC buffer allocates a fresh backing and frees the old one a frame later. Buffers above the allocator's local-cache limit have their pages returned to the OS on every free, so the game's first write into each fresh backing pays zero-fill page faults plus mapping syscalls, and both are roughly an order of magnitude more expensive under Wine and Rosetta than on native Windows. A busy scene moves close to 2 GB/s through this path, most of it one hot ~3 MB buffer renamed once or twice per frame.
With the pool, retired backings are parked per size class and handed back warm to the next same-size lock. Measured on a busy scene: the pool serves 99.8 to 100 percent of requests, allocator-bound backing traffic per 5-second window drops from gigabytes to a few megabytes, the process page-fault rate drops about 50x, and the buffer-lock path falls from several percent of the game thread in sampling profiles to noise. The trade-off is that parked pages stay committed instead of returning to the OS, bounded by the cap; the observed busy-scene peak is 59 MiB, so the default leaves about 2x headroom.
The shipped binaries now also keep frame pointers. The sampling profiler used to diagnose performance reports recovers call stacks by walking the frame-pointer chain, and a build without them stops every walk at the first frame, so a profile taken on a user's machine could name a hot function but not its callers. On 32-bit x86 this costs a general-purpose register out of eight; attributable profiles from the field are worth more than the difference.
Also in this release: the perf summary (available in PERF=1 builds) grew the instrumentation that made the above measurable, including rename byte totals, backing traffic that reaches the global allocator, and a per-window sample of process page faults.
Installing is unchanged, and covered in INSTALL.md inside the bundle.
Full changelog: v0.3.1...v0.4.0
v0.3.1
A bugfix release for a split-second full-screen magenta flash, reported during scene transitions in WoW 3.3.5, logging out to the login screen being the reliable spot. Magenta is what undefined memory looks like on the HDR layer's float format, so every fix here is a variation on one rule: never let a frame reach the screen with pixels nothing wrote.
The back buffer is now cleared to black when it is created. A freshly created texture holds whatever memory its allocation recycled, and the back buffer is presentable immediately: presenting before the first draw is legal Direct3D and routine right after a Reset, which is exactly what a scene transition does. The spec leaves post-Reset contents formally undefined, but real drivers hand out zeroed surfaces and games visibly rely on that. The clear rides the frame queue, so it costs nothing per frame and nothing waits on it.
A Clear with explicit rects as the frame's first touch of a render target kept the pass's load action at don't-care, so everything outside the rects came from undefined tile memory. It now loads the previous contents, which is what the rect form of Clear promises.
Two present-path repairs behind the same rule. If the HDR present shader ever fails to encode, the fallback used to copy the BGRA8 back buffer straight into the float drawable, which is invalid API use that kills the command buffer and so guarantees the undefined frame it was trying to avoid; it now presents a defined black frame and says so in the log. And a command buffer the GPU rejects used to discard all of its rendering while its queued present still reached the screen, one magenta frame with nothing in the log to show for it. The completion handler now checks for that and logs the actual GPU error once per error kind, so if a flash ever survives this release, the log names its cause.
Installing is unchanged, and covered in INSTALL.md inside the bundle.
Full changelog: v0.3.0...v0.3.1
v0.3.0
Present now happens at the layer's own size, and this release owns every resample that reaches the screen.
The drawable used to be pinned to the guest's back-buffer size. Whenever that differed from the layer's backing store, Core Animation rescaled the finished frame during compositing, downstream of everything we encode and with a phase we do not choose. At a ratio near 1.0 that put the whole image, interface included, a pixel away from where it was drawn, and it is what made text look soft in a window that was not an exact match for its back buffer. The drawable is now the layer's backing store, pushed at attach and again before every acquire, which also covers the frames between a window resize and the game reacting to it.
Present resolves the two sizes itself, by one of three routes: a straight copy at matching extents, MetalFX when the drawable is larger in both axes, and a filtered stretch for everything else. Because the routing reads the real texture extents, render.scale and a window that does not match its back buffer now collapse into a single resample instead of two. Only the stretch covers every ratio, so it is the backstop when MetalFX declines; the straight copy cannot stand in for it, because copying a smaller source into a larger drawable leaves the rest holding undefined memory.
Two fixes for problems that only showed up after living with the layer for a while. The MetalFX scaler cache is now bounded at eight entries, evicted least recently used: a scaler was cached per exact geometry and kept for the life of the process, so resizing a window steadily leaked them, around 16 MiB apiece at desktop sizes. And the EDR headroom is now read on the main thread rather than the presenting one. Reaching the screen's headroom means walking a view to its window to its screen, and only the screen at the end of that walk is safe to touch off the main thread; the two objects on the way to it are not. A zone transition is exactly when the main thread rebuilds them, which is where this surfaced as a crash a few seconds after porting into a raid.
Releases now ship their debug symbols, as a second archive, mtld3d-debug.tar.xz. Nothing in it needs installing. It exists so a crash report from a release build can actually be read: it holds the .pdb for each shipped DLL and a .dSYM for mtld3d.so. The Mach-O half was not previously producible at all, since the debug information for a dylib stays behind in the compiler's object files unless something gathers it.
To make a report identify its own build, every DLL and the .so now name themselves as they load, with the release they came from and the ID the linker assigned that image:
[mtld3d::d3d9] d3d9.dll v0.3.0 95ADD3A2-04B1-89D9-4C4C-44205044422E loaded
That ID is the PDB GUID on Windows and the UUID on macOS. Both are derived from the binary's contents, so it changes whenever the binary does and it is the same key the debugger uses to pair a binary with its symbols. If you report a crash, the first lines of the log now say exactly which archive resolves it.
Installing is unchanged, and covered in INSTALL.md inside the bundle.
Full changelog: v0.2.0...v0.3.0
v0.2.0
Render resolution scaling
render.scale in mtld3d.conf separates the resolution a game renders at from the resolution it displays at. Any value in (0, 1.0] works: 0.75 is a good first try, 0.5 renders a quarter of the pixels and is noticeably soft because the interface and text scale along with the world. MetalFX's spatial scaler enlarges the frame on present, which is edge-aware and sharper than the plain stretch the display would otherwise do. It is a way to trade pixel count for frame rate on a GPU that has headroom.
A render target or depth buffer a game creates at exactly the back-buffer size is treated as part of the same image and scales with it, so a game that draws its world into a target of its own and blits the result still rasterizes fewer pixels. Targets at other sizes, like shadow maps and post-process chains, keep the resolution the game chose. Readback through LockRect, GetDC and GetRenderTargetData resolves with the same scaler, so what a game reads back matches what is on screen.
Values above 1.0 are rejected, because rendering more pixels than are displayed needs a downscale on present and MetalFX only enlarges. On a GPU without MetalFX the scale is held at 1.0 rather than falling back to a blurrier scaler, and says so once in the log.
The knob composes with color.hdr.enable. The BT.2446-A inverse tone map now runs at render resolution and MetalFX finishes the frame in its HDR colour processing mode, so a scaled HDR frame is both sharper and cheaper than it was at full resolution: the ICtCp and PQ math covers fewer pixels, and MetalFX replaces a full-resolution shader pass.
Fullscreen is a borderless window, never a display mode change
A fullscreen device now takes over its window instead of the desktop. It saves the window's style and rect, strips the decoration, stretches the window over the monitor, and puts all of it back on the way out. Previously the device never touched the window at all, so a game that asked for 1024x768 fullscreen got a 1024x768 window sitting in the corner of the desktop.
Switching the desktop mode is the letter of the spec, but Wine's Mac driver hands the request to CGDisplaySetDisplayMode, so a fullscreen game would reconfigure the user's screen, rearrange every other window, and pay for a second mode change on the way out. A monitor-covering borderless window buys the property the mode change was there for: the guest's coordinate space matches the client rect, so GetClientRect, mouse input and the back buffer stay in agreement.
The trade-off is that the back buffer follows the window, so the resolution a game picks in its own video options has no effect in fullscreen, or in a maximized window. render.scale is the render-resolution control in both cases.
Also in this release
- The README leads with installation and recommends running under x87sidecar. D3D9-era games do their floating-point math in x87, which Rosetta 2 translates slowly.
- Every conformance classification was re-derived from Wine's d3d9 test suite, its raw failure messages and the implementation. The previous "zero real failures" headline was wrong: 46 sites covering 13 distinct defects were filed as expected or caps when they are intend-to-fix work. Counts and classifications each have one authoritative home now, and drift fails the test suite instead of going unnoticed.
- Compiler and clippy warnings are gated in
make checkthrough cargo'sbuild.warnings, so a plaincargo clippyreports findings without hard-failing at the first crate. - Cursor entry points record the calling thread, which is what decides whether Wine forwards a cursor change to its Mac driver at all.
Installing
Download mtld3d.tar.xz below. It serves both install routes: wine/ mirrors a Wine installation's lib/wine/ with every PE marked builtin, and native/ holds the unmarked d3d9.dll for the DLL-override route required on CrossOver. INSTALL.md is inside the bundle, and configuration lives in mtld3d.conf.
Full changelog: v0.1.0...v0.2.0