Skip to content

Linux and macOS support#47

Open
xnetcat wants to merge 29 commits into
par274:mainfrom
xnetcat:linux-macos-support
Open

Linux and macOS support#47
xnetcat wants to merge 29 commits into
par274:mainfrom
xnetcat:linux-macos-support

Conversation

@xnetcat

@xnetcat xnetcat commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

This adds linux-x64 and osx-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 HostMemory facade with Win32 semantics over mmap/mprotect/munmap, plus a shadow region table to answer VirtualQuery. Everything that used kernel32 P/Invokes goes through it now. Exact-address mappings use MAP_FIXED_NOREPLACE on Linux and a hint-and-verify mmap on macOS — plain MAP_FIXED there happily maps over the .NET JIT heap, which under Rosetta surfaces as "no code fragment associated with the given arm pc". PosixHostStubs provides 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.sh stages 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):

Dreaming Sarah in gameplay on macOS

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.

Copilot AI review requested due to automatic review settings July 11, 2026 03:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 HostMemory facade and updates memory/TLS/stub allocation paths to use it instead of Win32 Virtual* 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.

Comment thread src/SharpEmu.HLE/HostMemory.cs
Comment thread src/SharpEmu.Libs/Audio/CoreAudioPort.cs
Comment thread src/SharpEmu.Libs/Audio/CoreAudioPort.cs
Comment thread src/SharpEmu.Libs/Audio/CoreAudioPort.cs
Comment thread src/SharpEmu.Libs/Agc/AgcExports.cs
@par274

par274 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Where is CI for Mac/Linux builds?

@xnetcat xnetcat force-pushed the linux-macos-support branch from 7e4af39 to ed99e8b Compare July 11, 2026 03:39
@xnetcat

xnetcat commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

Added in 316dda3, but there's some issues with glfw, fixing right now

@par274

par274 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Added in 316dda3, but glfw doesn't get embedded in the binary, will push new commit with a fix soon

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.

@hiSandog

Copy link
Copy Markdown

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.

@par274

par274 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

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

@xnetcat

xnetcat commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

@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 (<rid>-<ref>-<sha>), on main pushes and manual dispatch. One note: they're tar.gz rather than zip so the executable bit survives extraction. The job is gated the same way as the win64 release, so it hasn't fired on this branch — worth a manual dispatch after merge to confirm.

@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 softwareupdate --install-rosetta hint). The GUI-only path stays usable in an arm64 process. The one case that can't be diagnosed in-process is Rosetta being absent entirely — the x64 binary never launches, so no code of ours runs; the error message and the check's doc comment document that model in one place.

@par274

par274 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

We need a Linux tester

@par274

par274 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Please rebase onto main and resolve the merge conflicts

@j92580498-max

Copy link
Copy Markdown
Contributor

@TheLeCrafter

Copy link
Copy Markdown

My game doesn't display on Linux sharpemu_dreaming_sarah_full_fps.log

sharpemu_dreaming_sarah_summary.log sharpemu_dreaming_sarah_dump_run.log

Can confirm this, game does not output video but audio seems to be working.

@par274

par274 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

The PR owner would know better, but there's probably a glfw issue; the window isn't drawing.

@xnetcat

xnetcat commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

@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
GPU does not support VK_KHR_maintenance8

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?

@xnetcat xnetcat force-pushed the linux-macos-support branch from b94f410 to 2c8a9e6 Compare July 11, 2026 11:08
@xnetcat

xnetcat commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

Please rebase onto main and resolve the merge conflicts

done

@par274

par274 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Once the Linux side is complete, I'll merge it. After merging that, I'll look at the SDL Windowing part.

@TheLeCrafter

Copy link
Copy Markdown

@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 GPU does not support VK_KHR_maintenance8

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?

This is the log I copied from the console with log level info. Should I also post log level debug or trace?

sharpemu_dreaming_sarah_logs.log

@xnetcat

xnetcat commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

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 libdecor-gtk.so failed to init and the 1.4x-scaled window, both meaning a Wayland session: GLFW's native Wayland backend doesn't reliably map the Vulkan window with some drivers (NVIDIA especially), so the surface presents but stays invisible while audio works.

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. SHARPEMU_ENABLE_WAYLAND=1 opts back into native Wayland. I don't have a Wayland box to test on, so if whoever hit this can grab the next build (or run today's with WAYLAND_DISPLAY= ./SharpEmu <game> to force XWayland manually) and confirm the window shows, that'd verify it.

@TheLeCrafter

Copy link
Copy Markdown

Tried your newest fix, still no display and still the same libdecor-gtk.so error.

@xnetcat

xnetcat commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

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 — glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11) before init — instead of the env trick. It also logs GLFW windowing platform in use: <X11|Wayland> right after the window comes up, so we can see exactly what GLFW picked.

Could you grab the next CI build and paste two things from the new run:

  • the GLFW windowing platform in use: ... line
  • the output of echo "$DISPLAY / $XDG_SESSION_TYPE"

If it shows X11 and the window appears — fixed. If it still shows Wayland, or $DISPLAY is empty (no XWayland to fall back to), that points at exactly what to do next.

@TheLeCrafter

Copy link
Copy Markdown

[LOADER][INFO] GLFW windowing platform in use: X11

and my display and session type are
:0 / wayland

@xnetcat

xnetcat commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

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.

@par274

par274 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

rebase please

@xnetcat xnetcat force-pushed the linux-macos-support branch from e13a97d to d8f1a88 Compare July 11, 2026 16:40
@TheLeCrafter

Copy link
Copy Markdown

Tried the newest version, the old error is now gone but the game is still black for me.

sharpemu_dreaming_sarah_logs.log

@j92580498-max

Copy link
Copy Markdown
Contributor

@j92580498-max

j92580498-max commented Jul 11, 2026

Copy link
Copy Markdown
Contributor
02_desktop_game_running 01_emulator_gui

driverInfo = Mesa 22.3.6
EXIT=0
.

VULKANINFO

Vulkan Instance Version: 1.3.239

Instance Extensions: count = 20

VK_EXT_acquire_drm_display : extension revision 1
VK_EXT_acquire_xlib_display : extension revision 1
VK_EXT_debug_report : extension revision 10
VK_EXT_debug_utils : extension revision 2
VK_EXT_direct_mode_display : extension revision 1
VK_EXT_display_surface_counter : extension revision 1
VK_KHR_device_group_creation : extension revision 1
VK_KHR_display : extension revision 23
VK_KHR_external_fence_capabilities : extension revision 1
VK_KHR_external_memory_capabilities : extension revision 1
VK_KHR_external_semaphore_capabilities : extension revision 1
VK_KHR_get_display_properties2 : extension revision 1
VK_KHR_get_physical_device_properties2 : extension revision 2

xnetcat added 15 commits July 13, 2026 00:20
…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.
@xnetcat xnetcat force-pushed the linux-macos-support branch from d8f1a88 to e71dfb4 Compare July 12, 2026 22:20
@Deathedit

Copy link
Copy Markdown

still-black-screen.log

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants