Skip to content

DXMT: opt-in build of d3d12.dll (DXMT_D3D12=1, default off) - #43

Draft
Karl-saintmartin wants to merge 1 commit into
fxgl:mainfrom
Karl-saintmartin:dxmt-d3d12-opt-in
Draft

Karl-saintmartin wants to merge 1 commit into
fxgl:mainfrom
Karl-saintmartin:dxmt-d3d12-opt-in

Conversation

@Karl-saintmartin

Copy link
Copy Markdown

Opt-in switch to build DXMT's d3d12.dll, so D3D12 straight to Metal can be measured against vkd3d-proton. It does not make D3D12 games run on this path yet. I'm opening it as a draft to ask whether you want this direction, the same way #14 did for D3D11.

The change

DXMT_D3D12=1 host/dxmt/build.sh adds -Denable_d3d12=true and d3d12 to the pe step's DLL list. The default is off. wmtr/Makefile and steam-gfx-path already handle a d3d12.dll in the payload, so only the build step was missing. The README (en/ru/zh) documents the switch and its limits.

Why DXMT and not D3DMetal

CrossOver's DX12 route is Apple's D3DMetal, which doesn't fit here:

  • It only runs inside a macOS Wine process, handing out in-process COM objects, so it has no narrow interface to forward over virtio-gpu.
  • Its licence restricts it to developing, testing or evaluating games, so it couldn't be bundled.
  • The only route I found is remoting the whole D3D API to a Mac-side Wine server. StonkGuy's d3dmetal branch does this for a D3D11 subset, and D3D12 would be much larger.

DXMT's D3D12 is written by a CodeWeavers developer, is under active development, and compiles DXIL to AIR with airconv. It also reuses the wmtr path, which already marshals root signatures.

What still blocks games

Blocker Evidence Likely fix
The pinned DXMT rejects DXIL (SM 6) E_NOTIMPL in d3d12_pipeline_graphics.cpp at e94c312; upstream removed it in 4219e159d Move the pin. host/dxmt/patches conflict against upstream 3b25259 (16 hunks in 4 files)
Map on a placed buffer returns NULL with S_OK wmtr_client.c, case 141, clears the host pointer Design question, see below
Feature level 11_1 at most, SM 6.0 at most CheckFeatureSupport upstream, while vkd3d-proton reaches 12_0 on KosmicKrisp Upstream
No tiled or reserved resources, wave ops or mesh shaders reported Same Upstream
Upstream says it isn't ready dxmt#194, dxmt#239 Wait, or test early

Same trust boundary as the D3D11 path: this inherits the security warning in the README.

Questions for you

  1. Do you want this path at all, given your bar in D3D11 through DXMT straight to Metal (opt-in): design and first measurements #14 (clearly ahead of the default path in real games)? Until upstream lifts the 11_1 cap, it can run fewer titles than vkd3d-proton.
  2. How should placed-buffer CPU access work? Metal heaps can't be backed by the guest's shared memory, so I see two options:
    • Map the heap's host memory into the guest as a blob. This keeps aliasing and needs a libkrun change.
    • Back placed buffers with arena memory. This needs no libkrun change, but it breaks aliasing with placed textures, and wmtr can't tell upload heaps from default heaps because DXMT creates them all with the same options.
  3. Should the pin move wait until the fork's steamac branch is rebased? It's generated from there, so I left it out, and host/dxmt: pin StonkGuy/dxmt steamac at 17896ac (same tree as f130d3f) #39 also touches it.

Testing

Tested on an M2 Max, macOS 27.0.1:

  • DXMT_D3D12=1 host/dxmt/build.sh fetch pe builds a d3d12.dll marked as a Wine builtin, exporting D3D12CreateDevice.
  • Default build produces the same four DLLs and no d3d12.dll. Output hashes differ between any two runs, even with no changes, so I couldn't compare it byte for byte.
  • DXMT_D3D12=2 is refused.

Not tested: the llvm, native, host, client and payload steps, make test, and anything in the VM. d3d12.dll has never been loaded and no D3D12 game or sample has been run with it. The change doesn't touch wmtr code.

Prepared with Claude Code.

…3D12 on Metal can be measured against vkd3d-proton in dev builds. Release builds are unaffected. README en/ru/zh state what still blocks games: the pinned DXMT rejects DXIL, reports feature level 11_1 at most, and placed-buffer Map returns NULL over wmtr

@fxgl fxgl left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the write-up and for asking first. Short answers first, then what's missing in the PR as is.

1. Do we want this path now? Not yet.

  • At the pin, SM 6 (DXIL) pipelines return E_NOTIMPL (d3d12_pipeline_graphics.cpp:244-245).
  • Even upstream HEAD caps at FL 11_1, while vkd3d-proton on KosmicKrisp reports FL 12_0, TiledResourcesTier 2 and ResourceBindingTier 3 here.
  • DXMT's DLLs are x86-64 only, so D3D12 through DXMT always means Proton 10.0 under FEX, while vkd3d-proton runs on ARM64EC Proton 11.
  • The trust-boundary work from #14 isn't done yet, and D3D12 widens the attack surface.

Let's come back to it when upstream lifts the 11_1 cap and the pin is past 4219e159d. The switch itself is ~10 lines.

2. CPU access to placed buffers. Neither option as is. I'd do:

  • A DXMT patch at d3d12_heap.cpp:85 that picks the Metal storage mode from the D3D12 heap type: DEFAULT → Private, UPLOAD/READBACK → Shared. It closes the FIXME there, can go upstream, and gives wmtr the signal it lacks today.
  • No MTLHeap for CPU-visible heaps. Allocate the whole heap as one arena range and make placed buffers NoCopy buffers at offsets in it, as case 18 already does for committed buffers. Buffer aliasing and persistent mapping stay, no libkrun change is needed, and it is the (blob, offset, length) model from #14.
  • Textures can't live in UPLOAD/READBACK heaps (except CUSTOM/ROW_MAJOR), so texture aliasing is only needed in DEFAULT heaps. Those stay MTLHeaps without CPU access, and Map already returns E_INVALIDARG there.
  • Mapping host heap memory into the guest needs a new libkrun ABI primitive. That can come later.

3. Pin move. Yes, wait for the rebase.

  • The patches are generated from the fork's steamac branch, and scripts/test/dxmt-source-pin.sh checks that base + patches == the fork's tree.
  • Moving DXMT_COMMIT is a 103-commit rebase that changes the D3D11 path for everyone, so it needs D3D11 measurements, not an experimental D3D12 flag.
  • #39 is merged now; it only re-pins the same tree.

If you want the dev switch in anyway, it needs:

  • The DLL actually reaching the guest. scripts/steps/30-layer.sh:39-41 copies a fixed list (d3d11 dxgi d3d10core winemetal), so d3d12.dll never reaches /usr/lib/steamac/dxmt in the guest and steam-gfx-path keeps vkd3d-proton's. Today the switch changes nothing in the VM, so the README (en:799-801, ru:793-795, zh:590-591) and "only the build step was missing" are wrong. Install the DLL there when it's present.
  • No stale DLL. wmtr/Makefile:91 copies d3d12.dll only if it exists and never removes it, and build.sh clean doesn't touch work/out/dxmt. After one DXMT_D3D12=1 build, a normal build keeps shipping the stale DLL; it already feeds the dxmt= hash in layer-release. Remove it in the else branch.
  • README accuracy. steam-gfx-path replaces d3d12 in every installed x86-64 Proton, not per game, so "D3D12 games with a DXMT profile use it" is inaccurate (README:740 itself says "every installed x86-64 Proton"). README:770 ("D3D12 (vkd3d-proton) … remain on Venus; this path is for D3D11/D3D10 only") now contradicts the new paragraph.
  • Licences. If such a layer ever ships, d3d12 has to be added to the LGPL lists (host/launcher/licenses.sh:289-301, scripts/gpl-sources.sh:105).
  • A complete limits list:
    • GS, tessellation and stream-output PSOs also return E_NOTIMPL (d3d12_pipeline_graphics.cpp:469-481);
    • HighestShaderModel is 5_1 (d3d12_device.cpp:271);
    • D3D12CreateDevice ignores MinimumFeatureLevel above 11_0 (d3d12.cpp:37), so a title that asks for 12_0 gets a device and fails later.

Checked and fine:

  • Flag parsing: 0/1 accepted, anything else refused.
  • enable_d3d12 exists in the pinned meson.options and affects only d3d12.
  • DXMT_D3D12=1 build.sh fetch pe builds a builtin d3d12.dll whose imports all exist in a prefix.
  • The default build is unchanged.
  • Your blocker table matches what I see at the pin.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants