Releases: athei/x87sidecar
Release list
x87sidecar v1.7.0
Since v1.6.0: this release fixes the Call of Duty 2 audio mixer crash caused by signed-zero handling in FPATAN, restores Rosetta's native 80-bit x87 state at translation reply boundaries, adds opt-in native-state tracing, handles default-attach release on macOS 26.6.2, and makes profiler output paths safe when Wine starts multiple processes.
Correctness
The inline FPATAN implementation now uses the raw sign bit when deciding whether x is negative, so -0.0 selects the correct quadrant. Both-zero inputs produce the required signed zero or signed pi result while NaN inputs continue to propagate. This fixes the root cause of the Call of Duty 2 mixer crash in #23: the MSVC acos sequence returned -pi/2 for a -0.0 dot product, which produced a negative channel volume and eventually a division overflow in Miles Sound System. The signed-zero cases, the CRT acos sequence, NaN propagation and an independent FPATAN matrix are covered by regressions. (#36 by @sdponomarenko)
Rosetta's signal-context and stock save/restore paths expect packed 80-bit x87 values at offset 6 with a stride of 10. The sidecar previously left its private compact binary64 layout at offset 8 with a stride of 8 between translation replies. It now converts native state on entry and restores it after flushing TOP, tags and permutations. Signal handlers can inspect or replace saved x87 registers correctly in both 64-bit and 32-bit compatibility mode, and native FXSAVE/FXRSTOR payloads preserve all eight slots. (#32)
Profiling and diagnostics
X87_TRACE_BLOCK adds opt-in tracing for one IR hash. The trace records native x87 state, ARM integer registers, NZCV and FPCR at handled reply boundaries in a bounded shared ring. X87_TRACE_OUTPUT selects the output prefix, and X87_TRACE_STOP_NEGATIVE=1 freezes the diagnostic window after a negative ST(0) result without changing guest execution. tools/x87_trace_analyze.py validates captures and checks the CoD2 pitch block's input and output pairs. Tracing allocates nothing until the selected block is encountered and has no cost when disabled. (#35)
X87_PROFILE and X87_SAMPLE now always append .<target-pid> to their configured output path. Sampling windows use <path>.<target-pid>.windows. This prevents sidecars for concurrent Wine processes from truncating one another's captures and applies equally to environment and command-line configuration. A concurrent integration test covers block profiles, sampling profiles, windows, literal %p, extensionless names and preservation of existing files. (#31 by @rdbell)
macOS compatibility
The default attach path now uses the verified reply-before-detach sequence on Tahoe XNU 12377.161.14 and later builds in that kernel family. This fixes targets remaining stopped after PT_DETACH on the macOS 26.6.2 GitHub runner. Older Tahoe builds and early Golden Gate kernels retain their existing sequence. CI also captures process state and stack samples before terminating a stalled test run. (#33)
Documentation
The README now leads with the shipped capabilities, quick start, benchmarks and both profilers. It documents ROSETTA_X87_PATH, profiler formats, per-block controls, loader diagnostics and previously omitted help entries. Detailed signal-recovery, decoder and IPC-cache reasoning lives in docs/internals.md. (#30)
Upgrading
Scripts that previously opened the literal value of X87_PROFILE, X87_SAMPLE or --sample must now read <configured-path>.<target-pid>. Sampling windows are written to <configured-path>.<target-pid>.windows. There are no other configuration or source migrations.
Artifacts
x87sidecar is signed flat with no entitlements and works in cooperative mode, where the tracee hands over its task port voluntarily. x87sidecar_entitled carries cs.debugger and get-task-allow for the default task_for_pid plus ptrace attach.
x87sidecar v1.6.0
Since v1.5.0: the sidecar survives asynchronous signals inside the code it emits (the fix for #23), it handles the two x86 encodings Rosetta's decoder rejects so winerosetta.dll is no longer needed, it runs on macOS 27, FMA contraction is off by default, and blocks can be identified by a hash that is stable across launches, which gives the new per-block knobs something to key on.
Asynchronous signals inside emitted code
Call of Duty 2 under CrossOver lost the effect of fld1; faddp in one execution out of thousands and died with a division by zero in its audio mixer (#23). The mechanism: when a signal lands in translated code, Rosetta decodes and steps the translated ARM instructions forward to the next entry of the translation's instruction map, takes the guest state from the thread context there, and resumes from that state after the handler. Two things about the sidecar's code broke under that.
The runtime's decoder only knows the encodings stock's translator emits. The sidecar used FMOV (scalar, immediate) for fld1, FCSEL in several emitters, inline literal pools for other constants, and one backward branch in the fist indefinite guard, which the recovery treats as a loop and refuses. On macOS 27 any of these aborts the process with failed to decode instruction; on macOS 26 the instruction is silently skipped, which for fld1 means pushing 0 instead of 1.0. Constants are now materialised through a GPR, a conditional select is a branch over a register move, and no emitter branches backwards.
The cross-instruction run cache kept TOP in a register and deferred its tag-word and FXCH bookkeeping to the end of a run, while answering one instruction per reply, so the map had an entry after every instruction where the memory state was stale. A run of x87 instructions is now answered with a single reply, so the only entries are the run's start and its end, where everything has been flushed. tests/test_x87_signal_storm.c runs the reported chain and one case per x87 opcode under a SIGUSR1 storm and compares every iteration bit for bit; it also shows that stock Rosetta itself shifts the x87 stack when a signal lands in its fcomp, fcompp or ficomp translation, which the harness records as a stock divergence. (#29)
The two encodings Rosetta's decoder rejects
DC D8, the undocumented alias of fcomp st(0), and 63 /r, ARPL in legacy mode, run on real hardware but are absent from Rosetta's decode tables, so a program containing either takes an illegal-instruction trap. Both occur in WoW 1.12, and handling them is what winerosetta.dll was injected into the guest to do. A second, much smaller stub on decode_opcode now substitutes an encoding the decoder accepts and, for ARPL, relabels the result to a synthetic opcode that the translator implements with the architectural semantics. The guest's memory is never modified and the stub decides entirely in the tracee. The substitute lives on the stub's own stack frame, because anything the handler touches has to exist in every process that inherits the patch, and wine forks. (#18)
macOS 27
The loader now locates what it patches by anchors that survive a Rosetta rebuild: the exported translator_translate, the name each function hands its own assert calls, and the runtime's opcode mnemonic table. Before the target is launched it checks the assumptions the emitted code relies on against the installed runtime, and refuses a runtime that fails a check instead of patching guessed addresses. x87sidecar --probe runs that analysis and exits 0 only when the installed Rosetta is fully supported; run it first after any macOS update. The default attach path acquires the developer-tools authorization before forking, because the password dialog used to appear while the tracee sat frozen at its exec stop and every other Rosetta launch on the machine hung until it was answered. Two exit-path races the new kernel's timing exposed are fixed as well. (#24)
FMA contraction is off by default
Real x87 rounds the product of an fmul to the precision-control width before the following fadd or fsub consumes it. Windows processes run at 53-bit precision, where a separate double multiply and add is bit-exact against real x87 and against stock, while a fused multiply-add keeps the infinitely precise product and turns a cancelling sum into the product's rounding error. Contraction is now opt-in with X87_ENABLE_FMA_CONTRACT=1, at every site that emitted a fused instruction, and tests/test_fma_pc53.c pins the numerics. (#25, taking over #19 by @sdponomarenko)
Stable block hashes and per-block knobs
The IR-content hash that names a block for the bridge and rollback lists and for profile_analyze --dump-block-by-hash is now the same across launches, re-decodes and host versions: dead operand bytes and per-decode fields are canonicalised before hashing, and absolute addresses contribute only their page offset so an image slide does not change the hash. (#20 by @sdponomarenko, #27)
Keyed on that hash, X87_STOCK_HASH_LIST hands the listed blocks to stock's translator, which is the per-block exclusion that works under wow64 where the sidecar sees host PCs only. X87_STOCK_OPS does the same for every block containing a listed opcode, X87_LOG_HASH_LIST logs every request of a listed block with an uptime stamp, and X87_DIAG_DIR names a directory for that log. The per-TR translation cache is reset whenever stock starts a block over or a request bypasses the translator. (#26 and #27, taking over #21 and #22 by @sdponomarenko)
Upgrading
winerosetta.dll can be removed from a wine prefix that only carried it for the two encodings above. Code that relied on FMA contraction for speed needs X87_ENABLE_FMA_CONTRACT=1. Hash-list values recorded with an earlier release do not match the new canonical hashes and have to be taken again.
Artifacts
x87sidecar is signed flat with no entitlements and works in cooperative mode, where the tracee hands over its task port voluntarily. x87sidecar_entitled carries cs.debugger and get-task-allow for the default task_for_pid + ptrace attach.
x87sidecar v1.5.0
Three changes since v1.4.0: the sampler can now name the samples that have no guest pc at all, the translate path costs two fewer syscalls per request, and a capture no longer scatters a file per report interval across the directory it was written to.
The samples with no guest pc
Around 30% of a game thread's samples are native arm64 with no x86 behind them. v1.4.0 kept their host addresses instead of dropping them, but nothing could say what those addresses were, and the largest single one on a live client, 18% of the thread, printed as an offset into an anonymous region.
That region is /usr/libexec/rosetta/runtime mapped a second time, 16 KB at file offset 0x20000, next to each guest image. The module map now asks the kernel for the file behind a headerless executable region and records it as a slice row. A slice says only which file backs the mapping: the offset into it is not recoverable, because the kernel reports an offset within a VM object of Rosetta's own rather than a file offset, so nothing is symbolised against a slice and an offset in one is relative to the mapping alone.
The address itself is the instruction after svc #0x80 in Rosetta's guest-syscall dispatcher, and no read of the code can name it, because the syscall number comes from the guest's own eax rather than a mov x16, #imm. The sampler already reads the thread state, so it now records x16 for every such sample and tallies it per pc into a [host_syscalls] section. A client login screen reads 94% ulock_wait2, 3% read, 1% mach_msg2 through that single address.
Samples with no guest pc also carry the reason there is none: an ARM pc in no fragment, one in the runtime's own code, or one inside a translated fragment before its map's first boundary. These were indistinguishable to a reader before.
Registering a named image now drops any anonymous row it turns out to cover. Rows learned for the runtime's copy-on-write text pages sat at higher bases and shadowed the image for the rest of the run, so 4200 samples read as an anonymous offset when the export table could name them.
These additions bump the profile format to version 2.
Two fewer syscalls per translate request
Every translate_insn request the tracee routes to the sidecar used to cost 7 traps in steady state. The reply send and the next request receive are now a single mach_msg(MACH_SEND_MSG|MACH_RCV_MSG), the classic MIG-server shape, and the block's IR array is cached per TranslationResult address instead of being re-read in full for every instruction of the block, so a 100-instruction block no longer pays about 8 KB of cross-task reads per x87 run in it. The ThreadContextOffsets struct is cached by pointer as well.
Both caches revalidate rather than trust: the IR cache requires the block triple to match, the walk to stay monotonic, and an 80-byte probe read of the current instruction to compare equal, and the offsets cache cross-checks every miss against the first copy it saw and logs loudly if that ever differs. X87_NO_IR_CACHE=1 and X87_NO_TCO_CACHE=1 turn them off. On a 30-block probe this is 4.01 mach_vm traps per request with the caches on against 5.01 with them off, and 5 traps per request in total instead of 7. Cold-translation latency only; steady state was already off the sidecar path.
X87_LOG_THROUGHPUT now reports traps per request and the cache hit and miss counts, so a regression here has a number it will show up in.
One window file, and a 10 kHz default
The per-interval window profiles now go into one appended <profile>.windows instead of a <profile>.NNNN file each, so a run leaves two files whatever its length. At the ten-second default the old layout was six files a minute, around 360 for an hour of play, and nothing in --help said they were coming. Each record ends with an end_window line so a reader can drop a partial one left by a SIGKILL rather than sum it. A stale .windows and any .NNNN written by an older build are both cleared when the next capture starts, so an old window cannot be added into a new run's totals.
The default sample rate is now 10 kHz rather than 1. A sample costs about 10 us, so the rate buys resolution at close to 1% of one core per kHz, and it holds: 10 kHz measures 9989 Hz achieved with nothing dropped. Resolution is the scarce thing here and cpu is not, since a capture that comes back too thin costs a whole session to retake, and the cost is only paid when X87_SAMPLE is set. Above 10 kHz it is the cadence that degrades rather than the data, so there is no point paying for a rate the timer cannot hold. X87_SAMPLE_REPORT stays at 10 s.
X87_SAMPLE_REPORT was documented as defaulting to 60 s and has been 10 since it shipped, and X87_SAMPLE_WINDOWS was not documented at all. Both are fixed, and the help now says what a capture leaves on disk and what the rate costs.
Upgrading
A reader that parses sample profiles needs to handle version 2 and the <profile>.windows layout. Captures already on disk in the old layout still parse, but the sidecar deletes a .NNNN series when it next samples to that same path, so move any you want to keep.
Artifacts
x87sidecar is signed flat with no entitlements and works in cooperative mode, where the tracee hands over its task port voluntarily. x87sidecar_entitled carries cs.debugger and get-task-allow for the default task_for_pid + ptrace attach.
x87sidecar v1.4.0
Sampling profiler for guest code
This release adds a sampling profiler to the loader. It samples the tracee without stopping it and reports where the guest was, so a capture off a running x86 program under Rosetta comes back as guest addresses you can symbolise against the program's own binaries, rather than ARM addresses in the translation output.
Set X87_SAMPLE=<file> to enable it and name the profile. X87_SAMPLE_HZ is the rate, X87_SAMPLE_SWEEP_HZ the thread-discovery cadence, X87_SAMPLE_REPORT the rewrite interval, X87_SAMPLE_WINDOWS=0 turns off the per-interval window files, X87_GUEST_RANGE pins the address range to follow, and X87_NO_UNWIND=1 records leaf pcs only. The same options exist as flags, but the environment wins, because an app bundle can set variables and not argv.
Nothing in the sampler suspends the target. thread_get_state and mach_vm_read both read a running task, unlike the ~2.2 ms whole-task stop a debugger costs per sample, so the target pays only memory-read traffic and the rate is bounded by the sidecar's own CPU. A 10 kHz capture of a 32-bit game client costs about 13% of one core and achieves 99.6% of the configured rate.
How it resolves a guest pc
The Rosetta runtime keeps one CodeFragmentMetadata node per translated code fragment in a binary search tree keyed on the fragment's ARM interval, and every translated node carries a delta-coded map pairing offsets in the ARM output with offsets in the x86 input. The resolver walks that tree and decodes the map, so a host ARM pc maps back to the guest pc it was translated from. Nothing writes to the target and nothing takes the runtime's tree lock, so every step validates what it reads and reports the lookup unavailable rather than trusting a partially observed structure. An ARM pc in no translated fragment is reported as such, which doubles as the test for "this thread is not running translated code", where the guest register pinning does not hold either.
What lands in the profile
The sampler latches onto one thread: it sweeps every thread for a discovery window, scores each by how often its resolved guest pc lands in the guest range, then follows only the winner. The range is found rather than configured, by walking back from a resolved pc to the nearest image header and taking the first image that is not a library, which works for a PE under Wine and for a plain Mach-O. Every latch, unlatch and discard is recorded in the file, so a profile that threw samples away does not look like one that did not.
Stacks come from walking the guest frame-pointer chain. Return addresses on the guest stack are already guest addresses, so only the leaf pc needs the resolver. Samples that resolve to no guest pc keep their host pc instead of being dropped, which is around 40% of the samples on a game's own thread: native arm64, the Rosetta runtime, the unix half of Wine, Metal, libsystem.
The profile carries its own module map, giving base, size, kind, state and host path per image, so symbolising needs nothing external. It is built only from addresses that were actually sampled, never by walking the address space, because a walk takes the target's vm_map lock thousands of times and measurably slows a loading game. Sampling and aggregation run on separate threads joined by a lock-free ring, so no lookup, allocation, lock or file I/O happens on the sample path. Each report interval is also written as its own <path>.NNNN file holding only the samples since the last one, and the sum of the windows is the cumulative profile by construction.
The profile is written on every catchable exit, from the process-exit path and from a SIGTERM/SIGINT/SIGHUP handler, so the report interval bounds only what a SIGKILL can lose.
Test harness
scripts/run_tests.sh now reads Phase 1, native Rosetta with the sidecar nowhere in the picture, as the baseline for the other phases. A case that fails there is stock's and no phase of ours can make it pass, so a later phase failing only cases Phase 1 also failed is reported XFAIL and does not fail the run, while any case Phase 1 passed still does. Whole lines are compared, values included, so the same case failing differently under the sidecar still gates. Phase 1 itself only gets that latitude for tests named in KNOWN_STOCK_DIVERGENCE.
Artifacts
x87sidecar is signed flat with no entitlements and works in cooperative mode, where the tracee hands over its task port voluntarily. x87sidecar_entitled carries cs.debugger and get-task-allow for the default task_for_pid + ptrace attach.
x87sidecar v1.3.0
This release adds a notarizable cooperative attach mode, prepares the loader for the next macOS beta, widens run bridging, and brings the JIT's x87 edge cases in line with stock Rosetta.
Cooperative attach
The sidecar can now accelerate a target without task_for_pid, ptrace, or entitlements: the target opts in over a per-pid bootstrap service (--cooperative, handshake via X87_SIDECAR_BOOTSTRAP) and hands over its task and thread ports itself. Releases now ship two binaries: x87sidecar (hardened, notarizable) and x87sidecar_entitled (for the classic unprivileged debugger attach). Landing this also flushed out two bugs that crashed real games: the transcendental emitters clobbered live guest XMM registers, EFLAGS and stock allocator state, and the translation-result write-back overran into the emitted code buffer. TurtleWoW runs under the cooperative path.
Loader
Detaching works on the Golden Gate beta: on XNU 13432 and newer the loader uses a debugserver-style detach sequence, selected at runtime by kernel build. Older kernels keep the classic path, which remains correct there.
JIT
movzx, movsx, movsxd and fwait no longer break translation runs; they bridge like mov and lea. 64-bit immediates with a clear high half load in one 32-bit bitmask ORR instead of a MOVZ/MOVK chain.
x87 edge semantics now match stock Rosetta, verified differentially (the suite cross-checks every expectation by running natively under stock first):
- fist, fistp and fisttp store the x86 integer indefinite (INT_MIN of the width) for NaN and out-of-range values, with the range check applied after rounding. The m16 forms previously wrapped out-of-range values silently.
- fscale reaches the denormal range with correct rounding instead of flushing to zero, and denormal inputs scaled up by a large exponent give the correct finite result instead of infinity.
- fyl2xp1 is accurate for tiny inputs instead of returning 0 for |x| below 2^-52.
Tooling
profile_analyze gains upper-bound fragmentation tiers (--frag-wide-gap) for sizing wide-gap bridging against a real capture, and the test harness no longer misclassifies rare NO-PASS results.
Changes since v1.2.0
x87sidecar v1.2.0
x87sidecar v1.0.1
What's Changed
Full Changelog: v1.0.0...v1.0.1