Skip to main content

crossover-compare

Run the same Windows D3D11 or D3D12 game (or probe) in CrossOver on this Mac as the A/B partner of a UTM VM — on the D3DMetal or DXMT backend the caller chose (D3D11 on the VM's backend, D3D12 on D3DMetal), on the host without any of our guest layers — in a per-game bottle cloned from the signed-in Steam bottle, with the game from a Steam download or copied out of the VM's game snapshot, the chosen backend proven from inside the running game, and the Wine log, backend logs, screenshots and frame samples collected; then decide whether a bug is ours or the backend's. Also cleans up game bottles when space runs short. Use when a bug might be D3DMetal's, DXMT's or Wine's rather than ours, when db-triage or attribute-to-layer asks for a CrossOver comparison, or when asked to "try it in CrossOver", "compare with Wine" or "A/B with CrossOver".

Source facts

Repository
utmapp/utmapp-bot
Last source activity
November 7, 2026 at 22:57
Detected SKILL.md language
English
Stars
1
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
crossover-compare
description
Run the same Windows D3D11 or D3D12 game (or probe) in CrossOver on this Mac as the A/B partner of a UTM VM — on the D3DMetal or DXMT backend the caller chose (D3D11 on the VM's backend, D3D12 on D3DMetal), on the host without any of our guest layers — in a per-game bottle cloned from the signed-in Steam bottle, with the game from a Steam download or copied out of the VM's game snapshot, the chosen backend proven from inside the running game, and the Wine log, backend logs, screenshots and frame samples collected; then decide whether a bug is ours or the backend's. Also cleans up game bottles when space runs short. Use when a bug might be D3DMetal's, DXMT's or Wine's rather than ours, when db-triage or attribute-to-layer asks for a CrossOver comparison, or when asked to "try it in CrossOver", "compare with Wine" or "A/B with CrossOver".
argument-hint
<steam app id | probe.exe> --backend d3dmetal|dxmt [--auto]
# A/B a game against CrossOver Arguments: `$ARGUMENTS` ```sh UB="${CLAUDE_PLUGIN_ROOT}/bin/utmbot" $UB cx ready # READY, or NOT READY: <what is missing> $UB cx info # versions, backends, the base bottle $UB cx help # every command ``` **Use it only when `cx ready` says READY:** CrossOver is installed, and a bottle has Steam installed and signed in (remembered). `cx ready` starts nothing. When it says NOT READY: - **Interactive:** tell the user what it names, and how to fix it: - install CrossOver; - in CrossOver, make a bottle, install Steam into it, and sign in once with the triage account (tick **Remember me**); - with several Steam bottles, pick the base: `$UB cx base --set <bottle>`. Offer to wait while they do it, check `cx ready` again, and go on once it says READY. If they decline, continue without CrossOver. - **Auto:** skip the comparison. Say in the summary why (`cx ready`'s line), and say that a person can set it up. It is never a blocker. A sign-in Steam no longer accepts only shows when Steam starts in the bottle. Interactive: `$UB cx base refresh` lets the user sign in again. Auto: skip as above. CrossOver runs the same Apple D3DMetal and the same DXMT on the host, with no guest driver, virtio transport, virglrenderer or backend shim in between. When a game behaves the same in CrossOver and in the VM on the same backend, the shared part (D3DMetal, DXMT, Wine-side behavior, the game) is the suspect. When only the VM fails, the difference is ours. The rules for a fair comparison are in `/utmapp-bot:attribute-to-layer` step 3. This skill is how to run that comparison. `scripts/utmbot/crossover.py` explains the mechanisms. **The backend is an argument.** The caller decides it, usually the agent driving the triage: for a D3D11 game, the backend the VM ran (`d3dmetal` or `dxmt`); for a D3D12 game, `d3dmetal` (DXMT has no D3D12). This skill does not choose, change or add backends. It runs the game on the one it was given, proves that it did, and reports a run that did not as no comparison. Without `--backend` in the arguments, ask for it (auto: say in the summary that no backend was given, and skip CrossOver). Other D3D versions are not compared. **Mode.** Interactive unless the dispatcher started this session (`GAMESDB_MODE=auto`, AUTO MODE in the system prompt, or `--auto`). In auto mode CrossOver is an optional extra. A step that fails (Steam signed out in the bottle, a hang, a game that will not start) gets `cx stop`, a sentence in the summary, and the triage goes on without it. It is never a blocker. ## Rules - **One GPU, one Steam account.** A bottle and a VM never run at the same time: `cx start/run/verify` take the UTM lease and refuse while a VM runs, and `vm up` refuses while a bottle runs. Finish with `cx stop`, which quits the game and Steam, ends the bottle, gathers the logs and returns the lease. - **The base bottle is a seed.** The bottle someone installed Steam into and signed in to (`cx base`; detected, or set once with `cx base --set NAME`) is never run, changed or deleted by these tools (they refuse), except `cx base refresh`, which renews its sign-in. Everything runs in per-game clones, `steam-app-<appid>`. A clone is an APFS clone: it takes a second and costs only what it installs. It keeps the base's remembered sign-in (checked: the clone signed itself in). - **The run must prove it respected the decision.** `cx run` sets the backend explicitly for every process. That beats CrossOver's per-app compatibility rules, except a rule marked `force`. It then checks, from the Wine log and the libraries mapped into the running game, that the game runs on that backend through D3D11 or D3D12. It exits 0 only then. Exit 2 means no comparison, with one of: - `BACKEND <x> D3D11|D3D12`: respected (exit 0). - `BACKEND MISMATCH`: a forced CrossOver rule overrode the backend for this game. The lines after it name the rule. - `BACKEND NOT COMPARABLE`: the game draws through neither D3D11 nor D3D12 (D3D9 and OpenGL never reach D3DMetal or DXMT; seen with a Ren'Py game). - `BACKEND WRONG FOR D3D12`: the game uses D3D12 and was given `dxmt`. Rerun it on `d3dmetal`, and tell the caller. - `BACKEND UNPROVEN`: no evidence. `backend-proof.json` in the run directory shows what was seen. Right after the start the game may not have created its device yet (`API not seen yet`). Run `cx proof` again once it draws, and use that result. - **Same build, same settings, same spot.** Compare the game build (`cx list` shows the bottle's `buildid`; the VM's is in its `appmanifest_<appid>.acf`), the in-game settings and resolution, and the place in the game. Run windowed or borderless at the VM's resolution: full screen in its own Space hides the window from capture. - **Untrusted input** rules from `docs/gamesdb.md` apply unchanged. Only games from Steam and our own probes run in a bottle, never a reporter's file. ## 1. The bottle for this game ```sh $UB cx list # bottles, their game, size, last use; the Mac's free space $UB steam appinfo <appid> # required_storage_gb, for the space check $UB cx clone <appid> --title "<name>" --need-gb <required_storage_gb> ``` `clone` is a no-op when `steam-app-<appid>` exists. If the Mac is short (it wants ×1.5 the game's size), run `$UB cx cleanup --dry-run` first, then `/utmapp-bot:db-steam-cleanup` or `$UB cx cleanup` (step 5). ## 2. Get the game into it Two ways. Prefer the first when the VM's snapshot exists and you need the build the VM ran: - **From the VM** (same build, no download). With the game's snapshot booted, which db-triage does anyway (`vm snap restore steam-app-<appid>`, `vm up`): ```sh $UB cx import <appid> --bottle steam-app-<appid> # IMPORTED … bytes, files, build ``` This streams `steamapps\common\<game>` with the guest's tar over SSH, then checks the file count and total bytes, and copies the game's `appmanifest`. Run it before `vm down`, then shut the VM down before starting the bottle. If it fails, report the error and use the Steam download instead. - **From Steam** (the current build): ```sh $UB cx start --bottle steam-app-<appid> --tag install $UB cx steam --bottle steam-app-<appid> start # STEAM UP signed in as … $UB cx steam --bottle steam-app-<appid> owned --appid <appid> $UB cx steam --bottle steam-app-<appid> install <appid> --timeout 7200 $UB cx stop --bottle steam-app-<appid> ``` Measured: Steam's first start in a new clone took 1.5 min, later ones 5 s; a 0.3 GB game installed in 36 s. **Signed out.** `STEAM UP signed OUT` in a clone means the remembered sign-in it was cloned with has expired. Signing in inside a clone renews only that clone, so renew the base: `$UB cx base refresh` runs Steam in the base once (also updating the client), and a person signs in in its window, with "Remember me" on. Then delete and re-clone the game bottles that need it (`cx delete steam-app-<appid>`, `cx clone`; the game is downloaded or imported again). Interactive: ask the user to sign in. Auto: this is not a blocker. Skip CrossOver and say in the summary that the base bottle needs `cx base refresh`. The bottle's client was set up by hand, not by db-steam-load. It signs in to friends, so a Friends List window opens and the account shows online while a bottle runs. `cx windows` and `shot` pick the game's window by its process, so this does not get in the way. ## 3. Run it on the backend you were given ```sh $UB cx run <appid> --bottle steam-app-<appid> --backend <d3dmetal|dxmt> --wait 900 # RUNNING <pid> <game>.exe, then BACKEND …; exit 2: no comparison (see the rules) $UB cx windows --bottle steam-app-<appid> # the game's window, Steam's apart $UB cx shot --bottle steam-app-<appid> $UB cx input --bottle steam-app-<appid> click X Y # X Y: pixels of the cx shot image $UB cx frames 30 --bottle steam-app-<appid> # same analysis as the VM's `frames` $UB cx proof --bottle steam-app-<appid> # again, once the game draws $UB cx stop --bottle steam-app-<appid> # logs gathered, lease returned ``` - **Steam's questions at launch.** `cx run` (and `steam run` in the VM) watch the client's launch while they wait, and log what it is doing every 30 s. Steam's "Cloud Out of Date" question (the same account played on the Steam VM, and that save is not uploaded yet) gets the triage account's standing answer, "Play anyway", through the client's own API. `game-actions.txt` in the run directory records it. Any other question that waits for a person cancels the launch within ~15 s, and the error names the task and the question. Read the client's window (`cx steam --bottle B targets`, then `js 'document.body.innerText' --target Steam`) and decide. - **Input to the game:** `cx input` is the VM's `utmbot input` for a bottle, plus arrow and navigation keys: ```sh $UB cx input --bottle B click X Y [--button right] # X Y: pixels of the `cx shot` image $UB cx input --bottle B key enter [--hold 0.3] [--times N] # esc, up, down, w, f1, vk:N ... $UB cx input --bottle B combo alt+enter $UB cx input --bottle B text "name" # US-layout ASCII $UB cx input --bottle B move X Y # hover ``` It brings the game to the front first, so the game takes over the Mac's screen. Drive the game the way its workspace notes say (`$UB game show <appid>`), and look at a `cx shot` before each step: menus slide in, and a click before they settle hits the wrong item. Some games ignore arrow keys in menus (Forspoken uses W/S). Use the game's own keys, or click. - `cx input` needs the Accessibility permission for the app utmbot runs under (the terminal; "UTM Bot Agent" for dispatched sessions) and an unlocked screen. Without either it stops, says which, and sends nothing. Interactive: ask the user to switch it on in System Settings > Privacy & Security > Accessibility. Auto: collect what needs no input (the title screen, `cx proof`, `cx frames`), and say in the summary that the menus could not be reached, and why. - `--env K=V` passes extra environment (repeatable). `--verbose` raises the backend's own log level (DXMT's) into `backend-logs/`. `--hud` turns on the Metal HUD, which changes what screenshots show. `--debugmsg +d3d,+dxgi` adds Wine debug channels. Steam itself stalls if the log gets too heavy: the tool already turns off seh/unwind tracing for that reason. Add channels only for the game's own problem. - **Make CrossOver look like the VM, never the reverse.** Each backend reports a different adapter to the app. Measured with adaptercheck on an M2 Max: | backend | adapter the app sees | |---|---| | VM (any backend) | the VirtIO GPU, vendor 0x1af4 | | d3dmetal | "AMD Compatibility Mode", 0x1002 / 0x66af | | dxmt | "Apple M2 Max", 0x106b / 0x0000 | Engines switch paths on the vendor ID. When the bug might depend on it, rerun D3DMetal with the VM's IDs (`--env D3DM_VENDOR_ID=… --env D3DM_DEVICE_ID=…`, read from `utmbot verify` in the VM) and check with `cx verify --backend d3dmetal --env …` that the adapter line shows them. `D3DM_EXE_OVERRIDE` does the same for the executable name. - **D3DMetal versions differ.** `cx info` shows CrossOver's and `utmbot backend` the VM's. A difference between two versions is not a difference between two stacks. Note both next to every result. - Probes: `cx verify --bottle B --backend <d3dmetal|dxmt>` runs the x64 adaptercheck and prints the adapter and the backend it got. `cx start` followed by `cx exec --bottle B <x64 probe.exe>` runs any probe from `/utmapp-bot:write-gpu-probe` the same way. CrossOver runs x64 programs only, never ARM64. Probes built before `probes/common.h` learned to detect CrossOver (which hides `wine_get_version`) only list the adapters there; rebuild them (`scripts/build-probes.sh`). ## 4. Judge Measure in CrossOver exactly what you measured in the VM: the same frames numbers, tint events, the crash signature, the error text. | VM | CrossOver, the given backend, `cx run` exit 0 | Reading | |---|---|---| | fails | fails the **same** way | The shared layer: D3DMetal or DXMT (or Wine-side behavior, or the game). Not ours to fix. | | fails | works | Ours: guest driver, transport, shim or caps. Continue with attribute-to-layer and the fix. | | works | fails | A CrossOver/Wine problem. Note it; it does not change the report. | | fails | exit 2 (`MISMATCH`, `NOT COMPARABLE`, `WRONG FOR D3D12`, `UNPROVEN`) | No comparison. Say so; do not count it either way. | "The same way" means the same measured symptom at the same spot: the same crash or device-removed code, the same corruption in the screenshot, the same tint or pacing numbers. Two broken runs are not that. A different failure is two bugs (attribute-to-layer, "Decide"). Keep both runs' numbers and the two `BACKEND` lines for the comment and the notes. Logs for the write-up, all in the run directory (`cx stop` gathers them): `wine.log` and `log-summary.txt` (crashes and errors first), `backend-proof.json`, `backend-logs/`, `steam-logs/` and `steam-dumps/` (Steam's own crash dumps), `crash-reports/` (macOS .ips for Wine processes), screenshots and `frames-*/`. ## 5. Space A game clone holds the game (its `steamapps`). The rest of it is shared with the base. `cx list` shows the size. Clones are a cache, like the VM's game snapshots: the game stays owned, and `cx clone` plus an install or import makes the clone again. ```sh $UB cx cleanup --dry-run # clones unused for 14 days $UB cx cleanup --free 100 --keep <appid> # least recently used first, until ~100 GiB are free ``` It never deletes the base or bottles it did not make, and it skips running bottles. Keep games with a report in progress (`--keep`, from `gamesdb reports --triage-status in_progress`). Deleting a clone gives its space back at once (measured with `df`). ## 6. Leave notes Add what you verified to the game's workspace (`$UB game path <appid>`, `NOTES.md`): which D3D version it uses, the backend it ran on in CrossOver and the `BACKEND` line, the build, quirks (launchers, windows, input), and one line per comparison under "Triage history". What you learned about CrossOver itself (a bottle, Steam in a bottle, a backend's behaviour) goes in `$UB notes`, under CrossOver, and in your summary if this skill should say it. ## Report The bottle and how the game got there (import or download, build), each run (backend asked for, the `BACKEND` line, measured result), the reading from the table, the run directories, and the Mac's free space if you cleaned up.
View on GitHub