feat: start titles from a game bundle, because macOS only puts a process in Game Mode when its bundle says it is a game - #11
Open
KAMEDAkyosuke wants to merge 1 commit into
Conversation
KAMEDAkyosuke
force-pushed
the
feat/game-bundle
branch
from
September 24, 2026 02:37
ad86ae9 to
b9bcd1c
Compare
…ess in Game Mode when its bundle says it is a game macOS puts a process in Game Mode only when its bundle says it is a game, and the engine's wine has none. Without a bundle the Dock already showed each program under its own name and icon; once a process is in one, the Dock and the Game Overlay take both from the bundle instead, so every program needs a bundle of its own. A title now starts from engine/SakeGame.app, a copy of Wine's unix side with LSApplicationCategoryType games and LSUIElement, and patches/0007 has Wine start every program the title starts from a bundle of its own in engine/SakePrograms/<key>/<program>.app: hard links to the parent's bundle, named after the exe, keyed by a hash of its path, and carrying the exe's icon from before its first launch. Measured on 2026-09-23: Diablo IV started from Battle.net shows as "Diablo IV" with its icon in the Dock and the Game Overlay, and goes into Game Mode full-screen. docs/runtime.md has why each piece is there; the patch header has the measurements.
KAMEDAkyosuke
force-pushed
the
feat/game-bundle
branch
from
September 24, 2026 02:37
b9bcd1c to
8886478
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
macOS puts a process in Game Mode only when its bundle says it is a game, and the
engine's wine has none. Without a bundle the Dock already showed each program under its
own name and icon; once a process is in one, the Dock and the Game Overlay take both from
the bundle instead, so every program needs a bundle of its own. A title now starts from
engine/SakeGame.app, a copy of Wine's unix side with LSApplicationCategoryType games and
LSUIElement, and patches/0007 has Wine start every program the title starts from a bundle
of its own in engine/SakePrograms//.app: hard links to the parent's bundle,
named after the exe, keyed by a hash of its path, and carrying the exe's icon from before
its first launch. Measured on 2026-09-23: Diablo IV started from Battle.net shows as
"Diablo IV" with its icon in the Dock and the Game Overlay, and goes into Game Mode
full-screen. docs/runtime.md has why each piece is there; the patch header has the
measurements.
What changes in the architecture
Before
A title ran the engine's own
wine, and so did everything it started. No process had abundle, and nothing wrote to the engine once it was built. The Dock already showed each
program under its own name and icon; what was missing was Game Mode.
After
Game Mode requires a bundle. macOS turns it on only for a process whose executable sits
in an
.appwithLSApplicationCategoryTypeending ingames, and there is no other keyfor it (
docs/runtime.md). A bare executable likebin/winenever qualifies.A bundle takes away the name and icon, and
patches/0007is what gives them back. Oncea process runs from a bundle, the Dock labels it with the bundle's file name and nothing
else, and the Game Overlay shows the icon the bundle had when macOS first registered it.
With
SakeGame.appalone, every process of a title — Battle.net, Diablo IV — would haveshown as that one bundle, losing what Before displayed correctly. So each program a title
starts gets a bundle of its own, named after its exe and carrying its icon.
Game Mode also requires full screen. A bundle only makes the process eligible; macOS
turns Game Mode on when its window goes full screen in the macOS sense. For Diablo IV,
the game's own full-screen setting does not do that — Game Mode stays off. The window's
green button has to be pressed. (Observed by the PR author on this branch.)
GameBundlecopies the unix side rather than linking it, because Wine takes the nextloader from
realpath(ntdll.so)and a link would lead back into the engine. It isrebuilt only when it no longer matches the engine, since running titles execute from it.
TitleLauncher.command(wine:)runs that bundle'swineand setsSAKE_GAME_BUNDLES. Ifthe bundle cannot be made, the title starts from
bin/wineas before and only Game Modeis lost.
CrossOver's Hack 22144 is skipped for these programs.
Known problems, not addressed here
DiskUsagecounts the hard links again for every program bundle (confirmed from the code). It sumsfileAllocatedSizewithout deduplicating by inode, so the engine size the Uninstall sheet shows grows by about 5.6 MB per program bundle. That contradictslayout.md's "nothing on disk". Small fix: deduplicate by(st_dev, st_ino)..stale-*,.staging-*and the pre-key layout. The LaunchServices registrations stay behind as well.GameBundlereplaces the directory as a whole), but that is only a convention. An in-place write —codesign, re-patching — would change every program bundle at once, and the inode currency check would still report them as current.SakeProgramsis in the engine. An engine moved with a copy that does not preserve hard links (cp -R) turns them into full copies: it heals itself by remaking the bundles, but disk is wasted until then.Alternatives worth considering
clonefile(2)instead oflink(2).realpath()still stays inside the bundle and no disk is used, but each file gets its own inode, which removes problem 3 and makeslsofreadable again. sake already clones for imports. The currency check would need a stamp of its own, for example a generation ID written intoSakeGame.app'sInfo.plistand carried into each program bundle. Fall back tolinkor a copy where cloning fails.init_paths()to derive the loader from the running executable rather than fromrealpath(ntdll.so). A program bundle then needs onlyContents/MacOS/wine, itsInfo.plistand its icon, with symlinks for everything else. That is a simpler failure surface, but a deeper change to Wine to carry across upgrades.Either way problem 2 needs its own fix: record the exe path in each bundle's
Info.plistand sweep keys whose exe is gone, or drop a bottle's keys when the bottle is renamed or deleted.