Linux and macOS support#47
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends SharpEmu’s host/runtime layer to support linux-x64 and osx-x64 (macOS under Rosetta 2) by replacing Windows-only primitives with cross-platform equivalents, adding POSIX fault/signal bridging to the existing exception recovery pipeline, and rounding out macOS-specific presentation + audio paths.
Changes:
- Introduces a cross-platform
HostMemoryfacade and updates memory/TLS/stub allocation paths to use it instead of Win32Virtual*APIs. - Adds a POSIX
sigaction-based exception bridge that rebuilds Win64-style exception/context records and reuses the existing recovery chain. - Improves macOS integration (main-thread pump for GLFW/AppKit, MoltenVK/GLFW Vulkan loader wiring, CoreAudio backend, and non-Windows keyboard input via Silk.NET).
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs | macOS Vulkan loader wiring, portability extensions, main-thread hosting, diagnostics tweaks, and input attachment for pad emulation. |
| src/SharpEmu.Libs/SharpEmu.Libs.csproj | Adds Silk.NET.Input dependency for non-Windows keyboard input. |
| src/SharpEmu.Libs/Pad/PadExports.cs | Cross-platform key sampling (Silk.NET on POSIX) and env-gated “auto cross” debug input. |
| src/SharpEmu.Libs/Pad/HostWindowInput.cs | New shared keyboard state container sourced from the presenter window on POSIX. |
| src/SharpEmu.Libs/packages.lock.json | Locks new Silk.NET Input dependencies for libs project. |
| src/SharpEmu.Libs/Kernel/KernelVirtualRangeAllocator.cs | Adjusts trace output for expected fixed-address allocation failures. |
| src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs | Enables x64 RDTSC reader beyond Windows and routes VirtualAlloc via HostMemory. |
| src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs | Routes VirtualQuery/Protect via HostMemory and updates default mapping search base for POSIX. |
| src/SharpEmu.Libs/Audio/WinMmAudioPort.cs | Refactors WinMM backend to implement IHostAudioPort and reuse shared sample conversion. |
| src/SharpEmu.Libs/Audio/IHostAudioPort.cs | New interface + shared guest->stereo PCM16 converter used by host backends. |
| src/SharpEmu.Libs/Audio/CoreAudioPort.cs | New macOS AudioQueue-based audio backend for sceAudioOut. |
| src/SharpEmu.Libs/Audio/AudioOutExports.cs | Selects CoreAudio on macOS; keeps WinMM elsewhere (silent fallback behavior preserved). |
| src/SharpEmu.Libs/Agc/AgcExports.cs | Adds env-gated texture/source dumping and extra vertex/primitive tracing diagnostics. |
| src/SharpEmu.HLE/HostMemory.cs | New cross-platform virtual memory API with Win32-like semantics over mmap/mprotect + region tracking. |
| src/SharpEmu.HLE/HostMainThread.cs | New main-thread work pump used to keep AppKit/GLFW on the real macOS main thread. |
| src/SharpEmu.Core/packages.lock.json | Adds central transitive locks for Silk.NET Input. |
| src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs | Replaces Win32 Virtual* calls with HostMemory and improves POSIX allocation strategy. |
| src/SharpEmu.Core/Cpu/Native/StubManager.cs | Routes executable stub allocation/freeing through HostMemory. |
| src/SharpEmu.Core/Cpu/Native/PosixHostStubs.cs | Provides Win64-ABI-compatible helper stubs and Win64→SysV thunking for managed callbacks on POSIX. |
| src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.PosixSignals.cs | Implements SIGSEGV/SIGBUS/SIGILL recovery bridge into existing exception pipeline. |
| src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs | Makes diagnostics safer on POSIX by probing memory before reading stack/code bytes. |
| src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs | Platform-dispatches TLS/helpers, adjusts guest layout addresses for POSIX, and fixes TLS patch register/flags preservation. |
| src/SharpEmu.Core/Cpu/CpuDispatcher.cs | Moves guest stack/TLS/stub layout down on POSIX and expands TLS prefix for POSIX TLS block usage. |
| src/SharpEmu.CLI/SharpEmu.CLI.csproj | Adds linux-x64 and osx-x64 runtime identifiers (Rosetta target). |
| src/SharpEmu.CLI/Program.cs | macOS main-thread pump + Vulkan loader preload so GLFW can resolve the app-local MoltenVK. |
| src/SharpEmu.CLI/packages.lock.json | Adds osx-x64 lock section and Silk.NET Input central transitive dependency. |
| scripts/test-linux-docker.sh | Adds docker-based linux-x64 smoke test script. |
| scripts/fetch-macos-moltenvk.sh | Adds helper to fetch/stage universal MoltenVK dylib as libvulkan.1.dylib. |
| Directory.Packages.props | Adds centralized package version for Silk.NET.Input. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Where is CI for Mac/Linux builds? |
7e4af39 to
ed99e8b
Compare
|
Added in 316dda3, but there's some issues with glfw, fixing right now |
Yes, glfw needs to stay alone with binary file. This problem will be fixed when I switch to SDL Window. Also, as far as I can see right now, Mac and Linux builds do not give release builds, this should produce separate release zips just like win64. This is what I see for now, I will look at this PR in detail again later. Nice work. |
|
Since the macOS target currently depends on Rosetta 2 and an x64 process, would it be worth failing early with a clear diagnostic when Rosetta is unavailable or the process starts under an unsupported architecture? That could make startup failures easier to distinguish from MoltenVK, signal-handler, or guest-memory problems. A small host-capability check could also document the supported macOS execution model in one place. |
Absolutely |
|
@par274 The glfw problem is fixed in bbc5243 — the KeepGlfwOutsideSingleFile target only matched the Windows filename (glfw3.dll), so libglfw.3.dylib/libglfw.so.3 were getting embedded where Silk.NET's loader can't find them. It now matches the lib-prefixed names too, and the POSIX builds keep glfw loose next to the binary exactly like win64. Verified by downloading the CI-built osx-x64 archive and running it to gameplay on a Mac. Releases added in 089d0ec: linux-x64 and osx-x64 now publish per-RID releases with the same tag scheme as win64 ( @hiSandog Added in b94f410. CLI mode now checks the process architecture before any emulation starts and exits with a message naming the supported model (x86-64 process; osx-x64 under Rosetta 2 on Apple Silicon, including the |
|
We need a Linux tester |
|
Please rebase onto main and resolve the merge conflicts |
|
My game doesn't display on Linux sharpemu_dreaming_sarah_summary.log |
Can confirm this, game does not output video but audio seems to be working. |
|
The PR owner would know better, but there's probably a glfw issue; the window isn't drawing. |
|
@j92580498-max Your log shows the window and swapchain working — frames present at ~6 fps (presented_fps only ticks after vkQueuePresentKHR succeeds), so it's not a glfw/window issue. What stands out is that your GPU reports two features missing that the shaders need: GPU does not support shaderStorageImage(Read|Write)WithoutFormat When the guest shaders hit those, they fail and the frame renders blank while audio keeps working. To see which GPU/driver the loader is using, can you paste the output of: vulkaninfo --summary | grep -iE 'deviceName|driverName|driverInfo|deviceType|apiVersion' (no vulkaninfo? sudo apt install vulkan-tools, or pacman -S / dnf install equivalent) That'll tell us your exact GPU + driver version — with those two features it's likely a driver/version thing, and knowing the driver points at whether an update or a different Mesa/driver stack fixes it. @TheLeCrafter can you upload your logs? |
b94f410 to
2c8a9e6
Compare
done |
|
Once the Linux side is complete, I'll merge it. After merging that, I'll look at the SDL Windowing part. |
This is the log I copied from the console with log level info. Should I also post log level debug or trace? |
|
The RTX 5080 log is a different issue from the earlier reports — that GPU has every feature, and frames are presenting fine. The tells are Pushed a fix (eed66af): on a Wayland session that also has an X server available, the presenter now steers GLFW to X11/XWayland before it initializes. |
|
Tried your newest fix, still no display and still the same libdecor-gtk.so error. |
|
Thanks for testing. The libdecor error still showing is the useful part — libdecor is Wayland-only, so GLFW was still on Wayland, meaning my previous env-var approach did not actually switch it. Pushed a better fix (f65320e): it now uses GLFW’\s real backend selector — Could you grab the next CI build and paste two things from the new run:
If it shows |
|
[LOADER][INFO] GLFW windowing platform in use: X11 and my display and session type are |
|
Found the actual root cause — and it was on our side, not the driver. GLFW requires window creation and event processing on the process main thread on every platform (GLFW intro guide, thread-safety); X11 in particular has a single event queue that must be serviced from the main thread, and a window created/polled on another thread may never map. That is exactly "the game runs — audio, imports, even Vulkan present — but no window ever appears." macOS already ran the window on the main thread (AppKit needs it) and Windows is fine (per-thread event queues); Linux was the gap — it spawned a background thread for the presenter. Pushed e13a97d: it extends the existing main-thread pattern to Linux (emulation on a worker thread, the window loop on the main thread). Verified macOS still boots to its window and the Linux headless path runs clean with no deadlock, but I don't have a Linux desktop to see the actual window — @j92580498-max / @TheLeCrafter / whoever has the RTX 5080, could you grab the next build and check if the window shows now? With this fix native Wayland may also work again; if you were forcing XWayland you can drop it. |
|
rebase please |
e13a97d to
d8f1a88
Compare
|
Tried the newest version, the old error is now gone but the game is still black for me. |
…GBUS/SIGILL) Guest faults on macOS/Linux previously terminated the process because the recovery logic in DirectExecutionBackend.Exceptions.cs was Windows-only. This adds a POSIX front-end that reuses the existing handler bodies: - DirectExecutionBackend.PosixSignals.cs installs SA_SIGINFO handlers via an [UnmanagedCallersOnly] entry, rebuilds the Win64 EXCEPTION_POINTERS / CONTEXT view from the platform mcontext (Darwin __ss thread state via the mcontext pointer at ucontext+48, Linux glibc gregs at ucontext+40 -- offsets verified against the headers on both platforms), runs the same chain as the VEH path (TryRecoverUnresolvedSentinel trap-sentinel recovery, TryHandleLazyCommittedPage demand paging, VectoredHandler diagnostics incl. FS/GS TLS-fault detection), and writes register changes back into the mcontext so sigreturn resumes the repaired guest. Unrecovered faults chain to the previously installed handler so the .NET runtime keeps mapping its own faults to managed exceptions. - The whole recovery path is warmed up with fabricated inputs before the handlers are installed. This is required under Rosetta 2: the signal trampoline cannot enter x86 code that has never been executed (and so never translated) -- a cold handler is silently never invoked and the faulting instruction retries forever (reproduced and verified in an isolated .NET test under Rosetta for Linux). It also keeps first-fault JIT work out of the signal frame. - Handlers run without SA_ONSTACK: the runtime's alternate stacks are too small for the diagnostic path, while guest (2MB) and host thread stacks match where Windows dispatches exceptions anyway. - The raw reads in the shared fault diagnostics (stack qwords, RBP walk, code bytes at RIP) now probe the region table on POSIX before touching memory, since a nested SIGSEGV inside the handler would kill the process before diagnostics finish. Windows keeps its try/catch reads. - Escape hatches: SHARPEMU_DISABLE_POSIX_SIGNALS=1 skips installation, SHARPEMU_DISABLE_RAW_HANDLER=1 disables sentinel recovery (parity with Windows), SHARPEMU_LOG_POSIX_SIGNALS=1 traces every delivery (first 16 and every 1024th are always traced). Verified with the test game: Linux (amd64 container) previously died with SIGSEGV right after import #252; it now recovers/diagnoses signals and the run proceeds to the real next blocker, an unpatched negative-offset guest TLS read (fault at TLS base - 0x1708), which gets the full NATIVE EXCEPTION dump before terminating. macOS is unchanged: the bridge installs and the game still stops at the known Rosetta translation-cache error at import 12, which is the next work item.
…earch base Three fixes that take the test game from dying during libc init to running its full main loop on macOS and Linux: - Static TLS blocks live below the TCB (FreeBSD amd64 variant II) and libc.prx reaches past -0x1700, but only a 4KB prefix was mapped below the TLS base. The prefix is now 64KB on POSIX (Windows keeps 4KB); the fault was a read at TLS base - 0x1708 during libc init. - HostMemory exact allocation on macOS used MAP_FIXED, which silently maps over untracked host memory. The direct-memory allocator's address scan walked into the .NET runtime's JIT heap and replaced live code, which under Rosetta 2 surfaced as "no code fragment associated with the given arm pc". Exact placement now passes the address as a hint and fails on relocation, like MAP_FIXED_NOREPLACE does on Linux. - sceKernelMapDirectMemory/MapFlexibleMemory searched for free space starting at 4GB, which is the Mach-O image base on macOS. The default search base is 0x20_0000_0000 on POSIX, and TryAllocateAtOrAbove now asks the kernel for a placement instead of page-stepping through host- owned address space (Rosetta ignores mmap hints for whole VA windows), over-allocating when the caller needs more than page alignment. Windows behavior is unchanged; all divergences are platform-guarded.
…keyboard input
Gets the test game from a headless loop to a playable window on macOS:
- AppKit traps with SIGILL ("NSUpdateCycleInitialize() is called off the
main thread") when GLFW runs on a worker thread. The CLI now moves
emulation onto a worker thread on macOS and parks the real main thread
in HostMainThread.Pump(); the presenter posts its whole window loop
there instead of spawning a thread, and a shutdown handler asks the
render loop to close the window so the pump unwinds on guest exit.
- MoltenVK: enable VK_KHR_portability_enumeration (+ the portability
instance flag) and VK_KHR_portability_subset when advertised, and gate
robustBufferAccess2 on the device actually supporting it (Metal does
not; the old code keyed it off robustImageAccess2 and vkCreateDevice
failed with ErrorFeatureNotPresent).
- Input: pad exports polled user32 GetAsyncKeyState, so POSIX hosts threw
DllNotFoundException per scePadReadState call. The presenter now
attaches the window's keyboard via Silk.NET.Input into HostWindowInput,
and the pad exports map the existing VK-code layout onto it off
Windows. Headless hosts (Linux containers) report a disconnected
keyboard and fall back to neutral pad data silently.
GLFW needs an x86-64 Vulkan loader under Rosetta: place a universal
libMoltenVK.dylib next to SharpEmu named libvulkan.1.dylib (Homebrew's
arm64-only copy cannot load into the x86-64 process) and export
DYLD_LIBRARY_PATH to that directory.
Verified: Dreaming Sarah boots to a MoltenVK-backed 2560x1440 window on
macOS (Apple M4, Rosetta 2), renders the intro, title, and menus, and
keyboard input drives it into gameplay. Linux (amd64 container) runs the
same build headless through millions of imports with no faults. Windows
paths unchanged; arm64 and x64 builds clean.
…g polish
- Audio: sceAudioOut ports now play through an AudioQueue backend on macOS
(stereo PCM16 with the same 32KB backpressure pacing as the WinMM path).
The WinMM port and the new CoreAudio port share an IHostAudioPort
interface and sample converter; hosts without a backend (Linux
containers) keep the silent fallback.
- MoltenVK: GLFW resolves Vulkan with dlopen("libvulkan.1.dylib"), which
cannot see the app-local universal MoltenVK build, so the presenter now
feeds vkGetInstanceProcAddr straight into glfwInitVulkanLoader (GLFW
3.4) before creating the window. No DYLD_LIBRARY_PATH needed; the CLI
also preloads the dylib for Silk.NET and prints setup hints when it is
missing. scripts/fetch-macos-moltenvk.sh stages the official universal
dylib next to a build.
- The virtual-range allocator's failure trace now names the address and
length instead of "AllocateAt invocation threw".
Investigated and documented (not port defects): the savedata transaction
failure is identical on Linux and macOS (HLE argument-register mapping for
sceSaveDataCreateTransactionResource), and the in-game tile speckling has
no platform-specific code in its path - the one macOS-only delta is that
MoltenVK lacks robustBufferAccess2, so out-of-bounds shader reads return
garbage instead of zeros.
Verified on macOS: window, audio backend, and keyboard input all come up
with zero environment configuration; the game runs to gameplay. Linux
headless run unchanged (silent audio, no faults). Windows paths untouched;
arm64 and x64 builds clean.
The TLS patch handler replaces guest `mov reg, fs:[...]` instructions, which preserve every other register and the flags - but the handler loaded the TLS index into ecx and called TlsGetValue (Win64: clobbers rcx/rdx/r8-r11) with `sub/add rsp` trashing the arithmetic flags. Guest code that keeps live values or comparison results across a TLS access computed garbage deterministically. The handler now saves rcx, rdx, r8-r11, and the flags around the call, keeping the same inner stack alignment. This applies to the load patches and both store-helper stubs, on every platform. Also in this change, from the rendering-artifact investigation: - The present blit picks linear filtering for any fractional scale (nearest only for integer upscales): a 3840x2160 guest frame blitted into a 2560x1440 swapchain with nearest silently dropped every third row/column. - ClampViewport no longer trims the guest viewport rectangle to the render target; trimming changed the guest's scale/offset and skewed texel addressing. Vulkan permits viewports beyond the framebuffer (the scissor confines rendering), so only spec bounds are enforced. - Env-gated diagnostics grown during the investigation: guest texture dumps (SHARPEMU_TEXTURE_DUMP_DIR), aliased guest-image readback dumps (SHARPEMU_TRACE_GUEST_IMAGES=alias), small-render-target write movies (SHARPEMU_TRACE_GUEST_WRITES=small), unattended input injection (SHARPEMU_AUTO_CROSS=secs,...), viewport nudging (SHARPEMU_VIEWPORT_EPSILON), chunked-draw toggle (SHARPEMU_DISABLE_CHUNKED_DRAWS), and rect-list/draw vertex traces. Known remaining issue (root cause narrowed, not yet fixed): the game's terrain texture pages are corrupted in guest memory before any GPU work - the mound's solid-fill 32x32 tiles decode to fully transparent texels and the grass page has deterministic gaps, byte-identical across runs. Ruled out: memcpy/memmove/memset/realloc HLE semantics, sampler wrap modes, texel-boundary rounding, chunked draws, viewport handling. Next step is auditing the Chowdren asset decode path (custom compressed images) against the emulator's import surface.
sceAudioOut ports on Linux now play through libasound instead of the silent fallback. The PCM device opens in blocking mode with ~170ms of device buffer (the time-equivalent of the 32KB queue the WinMM and CoreAudio ports keep), so snd_pcm_writei provides the same backpressure pacing without a managed queue. Underruns and suspend/resume go through snd_pcm_recover with one retry per submit; anything else drops the buffer rather than stalling the guest. The "default" device routes through PulseAudio/PipeWire on desktops and straight to hardware on bare ALSA; SHARPEMU_ALSA_DEVICE overrides it (the null device makes the path testable in containers). A missing libasound or device fails port creation and lands in the existing silent fallback. Verified in an amd64 container: the test game opens the port (backend=alsa, 48kHz stereo float32) and streams sceAudioOutOutput through the null device for a full run; without a usable device the port logs a warning and falls back to silent. Playback on real Linux audio hardware has not been tested.
…ump errors - HostMemory: a MEM_COMMIT that runs past its reservation now fails like Win32 instead of committing a prefix and reporting success. All current callers already clamp their ranges to the region, so this only guards future callers. - CoreAudioPort: Dispose wakes a submitter waiting on backpressure and the wait treats ObjectDisposedException as a timed-out wait, so closing a port during playback can no longer throw. A failed AudioQueueStart tears the queue down and fails fast instead of leaving an undrainable queue that stalls every later submit on its timeout. - AgcExports: texture dumping catches all write failures (bad path, permissions), logging a warning instead of crashing when SHARPEMU_TEXTURE_DUMP_DIR points somewhere unusable. Verified with the Linux container run: game boots and streams audio with the stricter commit check, and a dump dir under /proc produces warnings instead of taking the process down.
Adds a build-posix matrix job (ubuntu-latest / macos-latest) mirroring the Windows build: locked restore, Release build, self-contained CLI publish, and a tar.gz artifact per RID (tar keeps the executable bit). The macOS archive also stages the universal MoltenVK dylib via scripts/fetch-macos-moltenvk.sh so the artifact runs without any manual Vulkan setup. The release job still only ships the Windows archive.
The KeepGlfwOutsideSingleFile target only matched filenames starting with 'glfw', which covers Windows (glfw3.dll) but not libglfw.3.dylib / libglfw.so.3. Those got embedded into the single-file bundle, and Silk.NET's library loader does not probe the bundle extraction directory, so a published build died with "Couldn't find a suitable window platform" (and the glfwInitVulkanLoader wiring, which loads the library from AppContext.BaseDirectory, could not run either). Keeping the POSIX names loose next to the executable fixes both, the same way the Windows build already handled it. Found by running the CI-built osx-x64 archive: video failed while local loose-file builds worked. With the fix the published single-file build opens the MoltenVK window, wires the loader, and reaches gameplay.
The build-posix artifacts now ship as per-RID GitHub releases on main pushes and manual dispatches, tagged the same way as the win64 ones (<rid>-<ref>-<sha>). Archives stay tar.gz so the executable bit survives extraction.
The CPU backend executes guest x86-64 code natively, so the process must be x86-64 (win-x64/linux-x64 on x64 hardware, osx-x64 under Rosetta 2 on Apple Silicon). An arm64 process previously failed deep inside emulation startup, indistinguishable from MoltenVK, signal handler, or guest memory problems. CLI mode now checks the process architecture up front and exits with a message naming the supported execution model (and the Rosetta install command on macOS). The GUI-only path stays usable on arm64.
The presenter never named the GPU it picked, so a 'no video' report could not be told apart from a real windowing failure without guessing. It now logs the device name, type, and API version right after selection. A software rasterizer (llvmpipe/lavapipe/SwiftShader) shows up here and typically lacks the device features the translated shaders need, which is the likely cause when a window opens and presents frames but nothing draws.
GLFW's native Wayland backend does not reliably map the Vulkan window with some drivers (NVIDIA in particular): frames present but the window never becomes visible, so the game runs with audio and no picture. A report on an RTX 5080 showed exactly this — all device features present, frames presenting, but the log had 'libdecor-gtk.so failed to init' and a 1.4x-scaled window, both Wayland tells. On a Wayland session that also exposes an X server (DISPLAY set), the presenter now clears WAYLAND_DISPLAY for its own process before GLFW initializes, so GLFW selects its dependable X11/XWayland backend. SHARPEMU_ENABLE_WAYLAND=1 opts back into native Wayland. Headless (no DISPLAY) and non-Linux hosts are unaffected.
…atform The previous attempt cleared WAYLAND_DISPLAY to steer GLFW off Wayland, but a reporter still hit the native-Wayland path (the Wayland-only libdecor error persisted), so that env trick doesn't switch GLFW. Use GLFW's supported mechanism instead: glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11) before GLFW initializes, called into the same libglfw GLFW itself loads (the pattern InitializeMacVulkanLoader already uses). Still gated on a Wayland session with an X server present (DISPLAY set) so we never force X11 where XWayland can't catch it, and still overridable with SHARPEMU_ENABLE_WAYLAND=1. Also logs 'GLFW windowing platform in use: <backend>' after init via glfwGetPlatform, so a 'no window' report shows X11 vs Wayland outright. Verified on macOS: the readback correctly reports Cocoa and the presenter is unaffected (the fix is a no-op off Linux).
GLFW requires window creation and event processing on the process main thread on every platform: initialization, window creation, and glfwPollEvents are main-thread-only, and X11 in particular has a single event queue that must be serviced there. A window created and polled on another thread may never map — which is why the game ran (audio, imports, even Vulkan present) with no visible window on Linux. macOS already routed the window loop to the main-thread pump (AppKit needs it); Windows is fine because it has a per-thread event queue. Linux was the gap: it spawned a background thread for the presenter. Extend the existing HostMainThread pattern to Linux — emulation runs on a worker, the main thread pumps the window work the presenter posts. Refs GLFW intro guide (thread-safety): init, window creation, and event processing are restricted to the main thread. Verified: macOS still boots to its window unchanged; the Linux headless container runs to millions of imports with no deadlock or regression. On-screen confirmation on a real Linux desktop is still pending, but this is the documented root cause for a windowless-but-running Linux session.
d8f1a88 to
e71dfb4
Compare


This adds
linux-x64andosx-x64(under Rosetta 2) as hosts. Since the CPU backend executes guest x86-64 code natively, both targets run the whole process as x86-64. The Windows-only host primitives get platform-dispatched equivalents; Windows paths are untouched.Rough breakdown by commit:
Host memory / TLS / ABI layer. New
HostMemoryfacade with Win32 semantics over mmap/mprotect/munmap, plus a shadow region table to answerVirtualQuery. Everything that used kernel32 P/Invokes goes through it now. Exact-address mappings useMAP_FIXED_NOREPLACEon Linux and a hint-and-verify mmap on macOS — plainMAP_FIXEDthere happily maps over the .NET JIT heap, which under Rosetta surfaces as "no code fragment associated with the given arm pc".PosixHostStubsprovides pthread-backed TLS and Win64-ABI stubs for the kernel32 helpers the backend embeds into emitted code (TlsGetValue, QueryPerformanceCounter, ...), with a Win64→SysV thunk for managed callbacks. Guest stack/TLS/stub regions move from 0x7FFx down to 0x6FFx, since dyld and the Rosetta runtime live up there.Signal bridge. sigaction handlers for SIGSEGV/SIGBUS/SIGILL rebuild the Win64 EXCEPTION_POINTERS/CONTEXT view from the platform mcontext (offsets checked against Darwin and glibc headers) and run the same recovery chain as the VEH path, then write the repaired registers back so sigreturn resumes the guest. One Rosetta quirk cost a while: the whole recovery path has to be executed once with fabricated inputs before the handlers are installed — a handler that has never been translated is silently never invoked and the faulting instruction retries forever. Unrecovered faults chain to the runtime's handler so .NET keeps its managed exceptions.
Guest memory layout fixes. libc.prx reads below the TLS base (fault was at TLS − 0x1708), so the prefix mapping below it grows to 64KB on POSIX. Plus the mmap fix above and moving the direct-memory search base off 4GB, which is the Mach-O image base on macOS.
macOS presenter. AppKit only runs on the real main thread (SIGILL from NSUpdateCycleInitialize otherwise), so on macOS emulation moves to a worker thread and the presenter posts its window loop to a pump on the main thread. MoltenVK needs VK_KHR_portability_enumeration/subset, and robustBufferAccess2 has to be gated on actual device support — Metal doesn't have it and vkCreateDevice failed. Pad exports also stop calling user32 GetAsyncKeyState off Windows and read the window's keyboard through Silk.NET instead.
CoreAudio + MoltenVK loading. sceAudioOut ports play through an AudioQueue backend on macOS, behind the same interface as the WinMM path. The presenter feeds vkGetInstanceProcAddr straight into glfwInitVulkanLoader, so no DYLD_LIBRARY_PATH is needed;
scripts/fetch-macos-moltenvk.shstages the official universal dylib next to the build.ALSA audio for Linux. sceAudioOut ports play through libasound on Linux, behind the same interface. The PCM device opens in blocking mode with ~170ms of device buffer (the time-equivalent of the 32KB queue the other backends keep), so snd_pcm_writei itself provides the backpressure pacing. The "default" device routes through PulseAudio/PipeWire on desktops; SHARPEMU_ALSA_DEVICE overrides it, and a missing libasound or device falls back to the silent path.
TLS patch register preservation (all platforms, including Windows). The handler that replaces guest
mov reg, fs:[...]instructions clobbered rcx/rdx/r8–r11 and the arithmetic flags around its TlsGetValue call, so guest code holding live values across a TLS access deterministically computed garbage. Now saved/restored. Same commit also switches the present blit to linear filtering for fractional scales, stops ClampViewport from trimming the guest viewport (the scissor already confines rendering), and adds a set of env-gated rendering diagnostics.Screenshot
Dreaming Sarah [PPSA02929] in gameplay on macOS (MacBook Pro M4, Rosetta 2):
Testing
macOS: runs to gameplay out of the box — window, keyboard and audio all work with no environment setup.
Linux: not fully tested, fair warning. I only have headless amd64 Docker containers (on an ARM Mac via Rosetta for Linux), so what's verified is: the guest boots, signal recovery works, the game runs its main loop through millions of imports without faults, and the ALSA port opens and streams sceAudioOutOutput against the null device for a full run (with graceful silent fallback when no device exists). Real speakers and the display side have not been exercised — keyboard input goes through the presenter window, which should work on a Linux desktop but I couldn't try it. If someone can run this on real Linux hardware I'd appreciate it.
Windows: paths unchanged, x64 and arm64 builds clean.