steamac puts Valve's ARM64 SteamOS in a VM on Apple Silicon
Valve's official ARM64 SteamOS in a lightweight VM on Apple Silicon: libkrun + Venus + MoltenVK, controllers, CoreAudio sound
At a glance
- What is it?
- steamac runs Valve's own Steam Frame image of SteamOS on macOS 15 or later inside a libkrun virtual machine, with GPU acceleration through Venus and MoltenVK on top of Metal. It is a build-from-source project with a demanding toolchain, a large disk image, and a pipeline that trades first-run shader compilation for smoother later sessions.
- Who is it for?
- Adopt steamac if you have an Apple Silicon Mac on macOS 15 or later, roughly 150 GB of free space, and you need Valve's ARM64 SteamOS build specifically rather than any Linux virtual machine. Skip it if you want a plug and play game launcher or if your machine is Intel based, since the requirements demand an Apple Silicon Mac, Xcode 26 or later, Homebrew, rustup and a Linux container runtime.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Valve's own Steam Frame image, not a Linux distribution you assemble
The distinction that matters most is what runs inside the machine. steamac does not build a Linux distribution from parts. It downloads Valve's actual SteamOS image for Steam Frame from Valve's servers, described as a signed RAUC bundle, and verifies its signature and sha256 before use. That bundle then runs as a guest inside a lightweight virtual machine built on Hypervisor.framework through libkrun.
The target audience is narrow and worth stating plainly: an Apple Silicon Mac running macOS 15 (Sequoia) or later, with about 150 GB of free space because the disk image is sparse. The space figure sounds alarming until you notice the sparse part, since the image only occupies what the guest actually writes. The toolchain requirements are heavier still: Xcode 26 or later in a full installation, because its actool step builds the app icon from an Icon Composer document, plus Homebrew and rustup. A Linux container runtime is also needed, either OrbStack or Docker with arm64 and the privileged flag, since the kernel, Mesa and the disk image are all built inside Linux containers.
The published project page is fxgl.github.io/steamac, and the repository also carries Russian and Simplified Chinese versions of the readme, so the audience is not confined to English readers.
The graphics chain runs DX9 to DX11 games through DXVK and Vulkan
The rendering path is what makes this more than a generic virtual machine, and the project documents it as a chain. A game using DX9, DX10 or DX11 is handled by DXVK, which translates those calls to Vulkan. That Vulkan runs inside the guest on Mesa's Venus driver, is passed to the host over virtio-gpu as a blob with 16K alignment, and then reaches virglrenderer in the host, which speaks the Venus protocol to MoltenVK or KosmicKrisp. Metal is the final destination, since MoltenVK is a Vulkan to Metal translation layer and KosmicKrisp is described as Mesa's Vulkan driver on Metal 4.
Two separate translations stack up here, and they are worth keeping distinct. The x86 to ARM translation for the game itself is handled by FEX, which the description places in the DXVK layer as part of the Proton 11 stack. The graphics translation from Vulkan to Metal is separate and happens on the host.
The consequence for a user is that a DirectX game ends up running on Metal through two abstraction layers, and any pipeline or timing problem can originate in any of them. That is why the project ships frame pacing instrumentation rather than asking users to reason about the chain themselves.
MoltenVK is the default, KosmicKrisp is the experimental path
The default Vulkan driver is MoltenVK taken from the UTM fork, with geometry shader support and robustness2 enabled, plus VK_EXT_depth_clip_enable from pull request 2712 and additional fixes from the project. The alternative is KosmicKrisp, which the description calls Mesa's Vulkan driver on Metal 4 and an experimental option available on macOS 26 and later.
Building KosmicKrisp is not part of the default flow. It has its own script at host/kosmickrisp/build.sh, that script installs its Homebrew dependencies, which are llvm, spirv-llvm-translator, spirv-tools, vulkan-loader and glslang, and it can only be built on macOS 26 or later. The default path instead relies on a prebuilt MoltenVK from the UTM fork.
There is one interesting constraint on the whole stack. libepoxy 1.5.10 is built from pinned sources by host/libepoxy/build.sh before virglrenderer and libkrun, and all three use MACOSX_DEPLOYMENT_TARGET=15.0, including when the build host runs macOS 26 or 27. Bundling rejects Mach-O deployment targets above macOS 15.0, with KosmicKrisp as the single exception that may require 26.0. In other words the project deliberately keeps its own binaries compatible with the oldest supported macOS so that the guest image stays portable across hosts.
Building and launching with build.sh and run.sh
The whole pipeline is driven by two scripts in the repository root. The build script produces MoltenVK, virglrenderer, libkrun, the launcher, the kernel, Mesa and the disk image, and the run script opens a window with SteamOS:
./build.sh # everything: MoltenVK, virglrenderer, libkrun, launcher, kernel, Mesa, disk
./run.sh # a window with SteamOSIndividual parts can be built separately with the host and guest arguments to build.sh, which matters when only the guest side changed. The Homebrew packages the build expects are meson, ninja, pkg-config, dtc, xz, lld, sshpass and go, the last one there for a licence inventory of the bundled gvproxy and desync.
The virtual machine can be tuned from the command line when the defaults do not suit the machine, with options for the display size and for the CPU and memory limits. The full list of options is printed by the virtual machine binary that the build produces in the work/out directory, which is the place to look for anything the readme does not cover.
Build receipts stop stale guest artifacts from shipping
The build system is stricter than a plain make invocation, and this is one of the more interesting engineering decisions in the project. Guest release artifacts carry adjacent .inputs.json build receipts that bind the selected Git working tree inputs to the output bytes. Relevant dirty edits invalidate a receipt, while unrelated commits do not, which is what makes the check meaningful rather than merely strict.
Two scripts enforce the rule. host/launcher/bundle.sh refuses to work with missing or stale receipts before modifying the app, and dist.sh additionally validates the receipts and the resources inside an existing app before packaging it. When something is rejected, the error names the step to rerun, so the failure is meant to be actionable.
The refresh sequence for guest artifacts is documented without touching an existing SteamOS disk. Run guest/kernel/build.sh, but only when its inputs changed or its receipt is missing, then scripts/build-image.sh builder rootfs, then guest/mesa/build.sh, and then scripts/build-image.sh initramfs layer, and finally host/launcher/build.sh. An artifact that has never been stamped needs its normal build once, and kernel builds reuse the build volume without a clean step. One flag is explicitly for testing only: layers built with ALLOW_NO_VENUS=1 cannot be released.
Shader pre-caching trades a slow first run for smooth later sessions
The largest performance story in this project is about shaders rather than CPU or memory, and it has a clear cost. Steam shader pre-caching and the allow background processing of Vulkan shaders option are on by default in the VM. Pre-caching is what makes Proton's transcoded cutscene videos available: Steam downloads them per game into steamapps/shadercache/<appid>/transcoded_video.foz, with the description citing 836 MB for Heroes of Might and Magic: Olden Era and 3.8 GB for Diplomacy is Not an Option, and passes STEAM_COMPAT_TRANSCODED_MEDIA_PATH to Proton so it can play videos it cannot decode itself. With pre-caching off, those videos show a placeholder. DXVK's own cache, for DXVK 2.7, lives in the game's Wine prefix and works either way.
The price is that every pipeline Steam's fossilize_replay processes is compiled by Metal on the Mac, at roughly 70 to 100 ms each. Steam also reprocesses after its own shader updates and, for every game, after a launcher update that changes MoltenVK, because the Venus driver identity is a hash of MoltenVK's pipelineCacheUUID. Background processing absorbs most of that while Steam is idle, and what remains shows as Processing Vulkan shaders when a game starts, which can be skipped.
The default is handled by a helper. Stock Steam keeps background processing off, and EnableShaderBackgroundProcessing missing from ~/.local/share/Steam/config/config.vdf reads as 0. Before Steam starts, /usr/lib/steamac/steam-shader-defaults, which is the ExecStartPre step of steam.service, writes the value 1 into the ShaderCacheManager block if there is no value yet, once per Steam installation, marked by a config/steamac-shader-defaults marker file, so a choice made later in the Steam settings survives.
The guest clock and time zone follow the Mac until you override them
A smaller but telling feature is how steamac handles the fact that the guest is a virtual machine with its own clock. SteamOS takes the Mac's time zone and its 12 or 24 hour clock format, controlled by the Settings option Use the Mac's time zone and clock format, which is on by default and applies on the next start.
On every boot the launcher adds steamac.tz with the IANA zone of the Mac, taken from TimeZone.current, and steamac.clock24 with either 0 or 1 derived from the localized hour template, which honours the macOS 24-hour time switch. The initramfs points /etc/localtime at the zone, using timedatectl, the Steam time zone setting through steamos-set-timezone, and Steam's own clock. The last applied zone is remembered in /etc/steamac/mac-timezone, and a zone chosen inside SteamOS sticks until it matches the Mac's zone again.
The clock format is applied differently, through /usr/lib/steamac/mac-clock-format, which updates b24HourClock in the account's userdata/<account>/config/localconfig.vdf at UserLocalConfigStore/Software/Valve/Steam/FriendsUI/FriendsUIJSON. That is the same setting as Steam's Settings to Time and date to 24-hour clock toggle, not a client wide option, and on a new disk the file does not exist before sign-in, so the change lands on the first Steam start after sign-in. Desktop Mode receives LC_TIME in ~/.config/plasma-localerc, with en_GB.UTF-8 for the 24-hour case and en_US.UTF-8 for the 12-hour case, which also picks the time and date locale. Independent overrides are kept in ~/.config/steamac/mac-clock24.json, so changing the format inside Steam or Plasma stops following the Mac for that one setting without disturbing the other.
Frame pacing is measured, not assumed
The project does not ask you to judge smoothness by eye. The run script has a perf-stats option, and the environment variable STEAMAC_PERF_STATS=1 has the same effect. It prints guest-frame and on-screen frame intervals to the terminal every five seconds, including p50, p95, p99 and max values and a count of intervals above 25 ms and above 50 ms.
Those two thresholds are the interesting part, because they separate two different problems. An interval over 25 ms is a dropped frame at 40 frames per second or worse, and anything over 50 ms is a hitch long enough to be noticed as a stutter.
The documented cause of a first-appearance stutter is Metal shader compilation at around 50 to 100 ms per pipeline. Metal caches the compiled result on disk, so the effect does not stutter again even after restarting the game or the VM, and the description cites a measurement through Venus of 102 ms falling to 0.95 ms per pipeline in a new process. That number is quoted from the project, not measured here, but the mechanism it describes is checkable: the cache lives on disk, so a second run of the same scene should be visibly cleaner than the first, and a fresh disk image should bring the stutter back.
Editorial conclusion
Adopt steamac if you have an Apple Silicon Mac on macOS 15 or later, roughly 150 GB of free space, and you need Valve's ARM64 SteamOS build specifically rather than any Linux virtual machine. Skip it if you want a plug and play game launcher or if your machine is Intel based, since the requirements demand an Apple Silicon Mac, Xcode 26 or later, Homebrew, rustup and a Linux container runtime. Before you start, read the requirements list, run ./build.sh once and let the disk image download, and confirm you can accept the shader pre-caching cost, since every Steam pipeline is compiled by Metal on the Mac and the first run of a new effect stutters.
Frequently asked questions
What are the requirements to run steamac on a Mac?
The project lists an Apple Silicon Mac on macOS 15 or later with about 150 GB of free space, Xcode 26 or later in a full installation, Homebrew, rustup, and either OrbStack or Docker with arm64 and the privileged flag. The kernel, Mesa and disk image are built in Linux containers.
How do I build and start steamac?
Run ./build.sh from the repository root to build MoltenVK, virglrenderer, libkrun, the launcher, the kernel, Mesa and the disk image, then run ./run.sh to open a window with SteamOS. Parts can be built separately with the host and guest arguments to build.sh, and VM options are listed by work/out/steamac-vm --help.
Does steamac use Valve's official SteamOS image?
Yes. The SteamOS image is downloaded from Valve's servers as a signed RAUC bundle, and its signature and sha256 are verified. It runs as a guest inside a libkrun virtual machine on Hypervisor.framework.
Why does the first run of a game stutter?
The stutter comes from Metal shader compilation, which the project puts at about 50 to 100 ms per pipeline the first time an effect appears. Metal caches the result on disk, so the effect does not stutter again after a restart of the game or the VM.
How does steamac handle the time zone and clock format?
SteamOS takes the Mac's time zone and 12 or 24 hour format by default. The launcher adds steamac.tz and steamac.clock24 on every boot, the initramfs points /etc/localtime at the zone, and the last applied values are remembered in /etc/steamac/mac-timezone and ~/.config/steamac/mac-clock24.json so a choice made inside SteamOS can override the Mac.
What licence is steamac released under?
The repository is licensed under Apache-2.0 and carries LICENSE, LICENSES/ and NOTICE files at the top level. Bundled components are tracked separately, and the build includes a licence inventory of the bundled gvproxy and desync.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/fxgl-steamac)Was this useful?