Updated every iteration.
CLAUDE.mdis the contract; this is the state.
-
🔧 Three fixes ported from Dino0005' fork of Silo (2026-08-05; 520 tests green, zero warnings). The fork (branched 2026-07-15, so it predates the whole DXVK backend) was reviewed commit by commit; most of it was rejected — an updater repointed at the fork, a script that lifts Wine out of an installed CrossOver.app (constraint #8), a source-patching Python script, and an English-literals-as-keys IT localization that wraps dynamic error strings in
LocalizedStringKey(where a%in a path renders as a format specifier). Three changes were real defects in Silo and were re-implemented to fit this codebase:- GPTK's MetalFX/NGX shim was never activated, so the "MetalFX upscaling" toggle did nothing. GPTK
ships the module INERT as
nvngx-on-metalfx.dll— nothing resolves that name, soD3DM_ENABLE_METALFX=1had no NGX provider behind it. CrossOver's own GPTK tree carries the same module already renamed to plainnvngx.dll.GraphicsLinker.activateNVNGXnow aliases it inside the wine tree (recreating the.so's relative symlink, never dereferencing it), placed BEFORE the witness early-return so a runtime overlaid by an older Silo self-repairs — the same shapelinkD3DMetalFrameworkalready uses.- The fork's version would have been a no-op here, and that only showed up on-device. CrossOver's
Steam bottle carries 1 KB
nvapi64/nvngxfakedll stubs insystem32; ours cannot, because Silo boots its prefixes against the BASE runtime and GPTK's modules only arrive later at first launch. Without a name that resolves on the Windows search path the=boverride has nothing to bind — the exactwinemetaltrapinstallDXMTPrefixLoadersdocuments. AddedinstallGPTKPrefixLoaders(system32 only; GPTK is 64-bit) and extended the shared-prefix coherence rule: GPTK claimsnvapi64,nvngx=b, DXMT and DXVK disable them (=) so a seeded D3DMetal-backed native can never load under a non-GPTK launch. - Verified against the REAL
GPTK-4.0_beta_2artifact, not a fixture, via a new opt-in report case (SILO_BOTTLE_REPORT=1 Scripts/test.sh --filter nvngx): shim name, alias, symlink target, prefix seed and idempotency all confirmed on this machine. ⚠️ Still unproven end-to-end: that a game actually reports an NVIDIA adapter and that MetalFX engages. Needs a real launch. Watch fullscreen too — activating the NGX shim registers a second (fake NVIDIA) adapter, andwinemac.drvonly honours a display-mode change on the PRIMARY one, which is the fork's stated reason for its always-on virtual desktop. If fullscreen regresses, the answer is the existing reactiveneedsVirtualDesktoppath, NOT the fork's blanket rule.
- The fork's version would have been a no-op here, and that only showed up on-device. CrossOver's
Steam bottle carries 1 KB
- The Sync picker was silently inert for manual games.
ManualGameSettingsSheetembedsPerformanceFlagsSection, which shows the Sync picker — butmakePlanforced msync unconditionally. That rule exists only to protect the shared Steam bottle's single wineserver; a manual game has its own isolated prefix with no Steam client in it.makePlan(coResidentWithSteamClient:)now scopes it, and defaults totruedeliberately: over-applying msync costs a setting, missing it in the shared bottle silently breaks Steamworks IPC, so a forgotten parameter fails safe. - The virtual-desktop fallback sized games to 1440x900. It reused
SteamBottle.desktopGeometry— a verified CEF workaround for the Steam client window — so the path that exists to fix a display problem was capping games at a fraction of a Retina panel.DesktopGeometry.mainScreen()(points ×backingScaleFactor, resolved on the main actor somakePlanstays pure and AppKit-free) is now threaded down. The desktop is also renamedSiloGame:explorerscopes a desktop by NAME, so a game sharing the client'sSilodesktop would join that window and inherit its size — making the geometry a no-op in the shared bottle, the one place it matters most.- Review follow-up (2026-08-17):
NSScreen.mainis the screen holding the KEY WINDOW and is nil when Silo owns none — precisely thesilo://shortcut path, where the game would then have taken the 1920x1080 fallback on a 3024x1964 panel. It now falls through to the primary display. The points→pixels arithmetic split into a pureDesktopGeometry.geometry(points:scale:)so it is testable at all: anNSScreencan't be constructed, so the injectable-screen parameter could only ever pin the nil case. Five tests added (525 green).
- Review follow-up (2026-08-17):
- GPTK's MetalFX/NGX shim was never activated, so the "MetalFX upscaling" toggle did nothing. GPTK
ships the module INERT as
-
📮 PR #5 (BananaStems) — CLOSED as fixed by the above. An outside report that the 1440x900 cap was real on hardware (M2, external 2560x1440, 7 Days to Die logging
current screen 1440 x 900). Its fix resizedSteamBottle.desktopGeometryitself, which is the client's CEF workaround, and read the size viaCGDisplayPixelsWide/High— those return the display mode's point size (measured here: 1512x982 on a 3024x1964 panel), so it would have capped Retina games at a quarter of the display. It only looked right on the reporter's 1x external panel. Kept as the record of why the points-vs-pixels distinction inDesktopGeometryis load-bearing. -
🧱 DirectX 9 on Source-engine titles is blocked UPSTREAM, not by Silo (2026-08-04, measured). After the DXVK runtime was reinstalled (the shipped one carried a STOCK MoltenVK — see below), DXVK finally creates its device, swapchain and presenter on a real game. Rendering still fails: SPIRV-Cross emits Metal that CALLS a depth-comparison sampler it never DECLARES —
error: use of undeclared identifier 's0_2d_shadowSmplr'— so Source's shadow-mapping pipelines fail and the game runs with audio over a black screen. Five things were tested; four are dead ends:attempt result d3d9.forceSamplerTypeSpecConstants = Trueno change (config confirmed loaded) MoltenVK argument buffers OFF undeclared-sampler gone, replaced by cannot reserve 'buffer' resource location at index 0Source shadow cvars ( r_flashlightdepthtexture 0, …)no change — the shaders compile regardless CrossOver's OWN d3d9.dllidentical failure — it is also DXVK v1.10.3 -windowed(Alien Swarm)fixes its display-mode failure (2 errors → 0), then hits the same shader wall - CrossOver has the same limitation today, so this is NOT a Silo defect and NOT a reason to adopt their patched DXVK — earlier notes in this file guessed their DXVK was patched around this; it is the same version and fails the same way.
- DXVK 2.x cannot rescue it: DXVK 2.6.2 requests Vulkan 1.3, and the working MoltenVK (CodeWeavers'
1.2.10) offers Vulkan 1.2.290. Upstream MoltenVK 1.4.1 does offer 1.3 — but DXVK 2.6.2 still
requires
geometryShader+shaderCullDistance, which only CodeWeavers' patch provides, and that patch lives on 1.2.10. Every combination is blocked; closing it would mean maintaining a private MoltenVK patch set on a newer base, which CLAUDE.md explicitly rejects. This is also why CodeWeavers still ship 1.10.3. - What still works: the DX9 routing is correct and DXVK earns its place — the defect is specific to depth-compare (shadow) samplers, so a DirectX 9 title that does not use shadow mapping renders fine.
- Silo-side gap this exposed: Source games need launch arguments Steam would normally supply —
-game te120(Transmissions),-game dab(Double Action; without ithl2.exesilently runs the basehl2content, which is why it produced sound), and-windowedfor Alien Swarm.GameConfig.customArgsalready exists and is applied at launch, so these are settable today; whether Silo should READ Steam's own launch options instead of relying on the user typing them is an open product question.
-
🔎 Round 2 of the verification sweep: the remaining assumed signals, checked against reality (2026-08-04,
main; 489 tests green, zero warnings). Two areas were still "verified by assumption". Both are now checked against this machine, and the report re-checks them so they cannot rot.WineServerProbe— CORRECT, and now proven. It predicts<tmp>/.wine-<uid>/server-<dev>-<ino>/socket; against a live wineserver serving the real Steam bottle it predictedserver-100000e-38b92faand that is exactly the directory holding the socket. The load-bearing detail: this CrossOver-sourced build uses/tmp, NOT$TMPDIR($TMPDIR/.wine-501exists but holds only an unrelatedbottle-<dev>-<ino>.lock), so the candidate-root list must keep/tmp— do not "simplify" it away./tmp/.wine-501also holds several STALEserver-*dirs with no socket; requiring the socket file is what stops those reading as live.isLive(SteamBottle) = truewith Steam running.GraphicsFallbackloader-failure signatures — one confirmed, two deleted. GPTK's"Failed to dlopen D3DMetal"is verbatim in the installed D3DMetal binary (GFXTHandle && "Failed to dlopen D3DMetal") and in the runtime's unix-side d3d modules ✅. DXVK's"DxvkAdapter: Failed to create device"and"Requested feature level not supported"are both in the pinned v1.10.3 source ✅. The other two —"Minimum required feature level","Maximum supported feature level: 0"— were speculative "DXVK 2.x wordings" that appear nowhere in the version Silo ships, and the test asserting them supplied the strings itself. Markers that cannot fire are not a safety net but a false inventory, so both were removed; the test now uses the two real wordings. Re-derive from source ifDXVK_VERSIONis bumped.- The on-device report gained a wineserver-probe section and is
.serializedso its three sections print in order rather than interleaved.
-
🔬 Systematic backend verification — an on-device report, and the three real bugs it caught (2026-08-04,
main; 481 tests green, zero warnings). "Do all three backends work, and does Automatic behave?" had no answer that wasn't an assumption. There is now an opt-in on-device report —SILO_BOTTLE_REPORT=1 Scripts/test.sh --filter BackendReport— that reads the REAL bottle and prints (a) what Automatic chooses for every installed game and why, and (b) what each backend actually did, from the launch logs Silo already writes. It calls the productionD3DProfile/BackendChooser/GraphicsFallback.classify, so it cannot drift from what a launch does; it is.enabled(if:)-gated, so a machine with no bottle skips it (constraint #4 intact).- Its first run found three defects, all in shipped code:
D3DProfileabsorbed redistributables shipped beside the exe. Microsoft's C++ AMP runtime (vcamp140.dll) importsd3d11for GPU compute, so Bloons TD Battles 2 — a pure OpenGL title — profiled as D3D10/11. The real damage is on DirectX 9: a DX9 game bundling the MSVC redist would failisD3D9Onlyand be routed away from DXVK, the only backend that can run it. Fixed with anexcludedModulePrefixeslist (vcamp/msvcp/vcruntime/ucrtbase/api-ms-win-/…), matched by prefix; engine DLLs (UnityPlayer,shaderapidx9, …) are explicitly still scanned.- DXMT's engagement signature was fiction.
"DXMT: created Metal device"occurs zero times in a real, working DXMT log (device created, feature level 11_1). DXMT is a DXVK fork and inherits its logger, so it printsinfo: Using feature level D3D_FEATURE_LEVEL_11_1. DXMT engagement had therefore never once been detected. - GPTK had no engagement signature at all — success was only ever inferred from the absence of
failure. But D3DMetal presents a spoofed adapter,
AMD Compatibility Mode, which any engine that logs its adapter prints (Renderer: AMD Compatibility Mode (ID=0x66af), Vendor ATI). wined3d on this stack reports the real GPU, so it is unambiguous. Positive-only → a game that never logs its adapter stays.unknown, never a false failure.
- Root cause of 2 and 3 — and the reason they survived: the tests built the very strings they
asserted. Three tests constructed a log containing
"DXMT: created Metal device"and then checked that it was recognised. Circular, and it proved nothing — the same mistake that shipped the 0.4.5 launch regression. Every replacement is pinned to verbatim excerpts of logs Silo itself wrote on this machine (Fixtures/log_gptk_engaged_real.txt,log_dxmt_engaged_real.txt,log_wined3d_fallback_real.txt). DXVK's signatures were checked against dxvk's own source and were already correct. - On-device result — 3 of 4 launch logs now decisive: GPTK → engaged (Bloons TD 6), DXMT →
engaged (manual game, FL 11_1), and a genuine GPTK → wined3d non-engagement correctly flagged
on the second manual game. DXVK reads
.unknownon Bloons TD Battles 2 — correct: that game is OpenGL and never asked DXVK for a device. - Remaining gap (honest): DXVK's D3D path has no game-log proof yet. It is proven by the
D3D11CreateDeviceprobe (feature level 11_0, M4 Pro) but no DirectX 9 game is installed, so the DXVK engagement signatures are source-verified, not observed in a real title.
- Its first run found three defects, all in shipped code:
-
🍎 GPTK 4.0 beta 2 support + a D3DMetal Metal 3 / Metal 4 selector (2026-08-04,
main; 460 tests green, zero warnings). Apple shippedGame_Porting_Toolkit_4.0_beta_2.dmg. Of everything that changed between the two betas, exactly one thing reaches Silo: on macOS 27+ D3DMetal now translates DirectX 12 through Metal 4 by default (beta 1 shipped it opt-in viaD3DM_MTL4=1), withD3DM_MTL4=0the way back to the Metal 3 backend.- The import/overlay pipeline needed ZERO code — and that was verified on-device, not assumed. Ran
silo --import-gptkagainst the real beta-2 dmg: nested "Evaluation environment" dmg mounted, all 12 modules +lib/externalextracted toRuntimes/GPTK-4.0_beta_2(name derived from the filename), quarantine stripped, Apple'scom.apple.D3DMetalsignature preserved,version.plist=4.0b2,LSMinimumSystemVersionstill14.0(so it runs on this 26.6 box). Then drove the realGraphicsLinker.overlayGPTKagainst an APFS clone of the real wine runtime: pristine (4/6 wine builtins, no framework link) → beta 1 (6/6 dlls, 6/6 unix symlinks →libd3dshared.dylib,D3DMetal 4.0b1) → re-overlay beta 1 (true no-op — the witness works) → beta 2 (all 6 modules byte-match beta 2, framework replaced,D3DMetal 4.0b2). Thed3d11.dllwitness byte-compare is what lands the upgrade. EnvFlags.metalBackend(MetalBackendChoice = .auto|.metal4|.metal3→D3DM_MTL4=1/0/nothing), surfaced as aPickerin the sharedPerformanceFlagsSection(both settings sheets get it; no view-model change —savecopiesenvFlagswholesale)..autodeliberately emits nothing — Apple's default depends on the GPTK version AND the OS, somakePlanstays pure with no OS-version probe (there is still none in the codebase) and no config written today can silently pin Metal 3 once macOS 27 ships. GPTK-only;extrastill overrides. Tolerant decode degrades an unknown value to.autorather than throwing.- Why a selector and not a Bool — the CrossOver oracle. Read out of the local CrossOver 26.2: it does
bundle Apple's D3DMetal (
lib64/apple_gptk/external/D3DMetal.framework) but atCFBundleShortVersionString = 3.0— a full major version behind, with noD3DM_MTL4and no Metal 4 concept anywhere in the app. Silo is ahead of it here, so there was nothing to copy for the flag itself. Its shape still settled the modelling: CrossOver gives backends an Automatic-plus-members selector (setGraphicsBackendAuto/…D3DMetal/…DXMT/…DXVK/…Wine) and features a plain switch with an availability predicate (isDLSSAvailable/isDLSSEnabled, "Requires Apple Silicon and macOS Tahoe for D3DMetal, macOS Sonoma for DXMT"). Metal 3 vs Metal 4 is a backend, so it gets the selector — which also matches theSyncModepicker already in that same form section. - Hardened
GPTKImporter: the runtime name (derived from a user-picked filename, then used to buildinstallDir, which is handed toremoveItem) now passes the sameRuntimeManager.safeRuntimeComponentboundary the rest of the runtime code uses — in bothimportGPTKandremove(name:). A filename can't actually express traversal (URL normalizes../evil.dmg→evil), but the explicitname:could. - Explicitly out of scope — the bulk of the beta-1→beta-2 diff is the GPTK SDK, which Silo never
compiles against: metal-cpp 370.63.1→381.0.0 breaking changes (
MTL::SamplerReductionModebase type, thesetFrontFacingWindingtypo fix,reactiveTextureFormatremoval), the new tensor classes, MetalFX subrect/distortion API,CA::MetalLayerEDR,NSSharedPtrstd::hash, Metal Shader ConverterIR_VERSION_PATCH0→1. Silo is a launcher that copiesredist/liband sets env vars; zero Silo code touches those headers. The converter dylibs insideD3DMetal.framework/Resourcesride along verbatim in the existing overlay. - Deferred (needs macOS 27 + an observed failure): a reactive "Metal 4 failed → retry Metal 3" hint
modelled on
learnedBackend. There is no log signature to match on — GPTK success is already silent inGraphicsFallback— and macOS 27 isn't on this box. Building it now would be guessing. - Follow-up surfaced by the CrossOver read (NOT done; independent of beta 2):
overlayGPTKcopiesnvapi64+nvngx-on-metalfx(they match itsnvprefix filter), butGraphicsBackend.gptk.dllOverridesnever lists them — so the DLSS shims are overlaid and never enabled. CrossOver exposes precisely this as a per-bottle "DLSS for D3DMetal and DXMT" switch, and beta 2's move to public MetalFX API makes it more plausible, not less. Deliberately not bundled into a runtime upgrade: it needs a DLSS-capable title to verify, and shipping it blind would confound the very regression triage the Metal-backend selector exists to enable. - Left to the user:
config.jsonstill hasgptkRuntimeName = GPTK-4.0_beta_1. Both versions are installed, so Settings → GPTK → default radio is a one-click switch and a one-click rollback. Not flipped automatically — beta 2 has no on-device render proof here (the library has 0 games installed), and switching invalidates everylearnedUnderRuntimehint, so titles that had settled onto a DXMT/DXVK fallback each retry GPTK once.
- The import/overlay pipeline needed ZERO code — and that was verified on-device, not assumed. Ran
-
🎯 Backend detection unified + DX9 detection fixed (2026-08-04,
main; 451 tests green, zero warnings). Follow-up to an adversarial audit of the Automatic selector: the DX9 route was mostly unreachable in practice, and the three backends decided from separate, weaker signals. Now oneD3DProfiledrives all.- New
D3DProfile(Launch/D3DProfile.swift) —usesD3D9/usesD3D1x/usesD3D12, derived byscan(executable:)from the game exe plus the DLLs shipped beside it (breadth-first, depth ≤2, redist dirs excluded, 256-file cap, memory-mapped header reads, fail-open). This is the fix: scanning only the.exemissed most real DX9 titles, because the renderer lives in a DLL — Source (bin/shaderapidx9.dll), Unity (UnityPlayer.dll), UE (Binaries/Win64). Those games previously went GPTK → fail → DXMT → fail → DXVK (3 launches); they now route to DXVK on the first launch. Regression-verified: crippling the scan back to exe-only makes the two "THE FIX" tests fail. - All three decisions read that one profile.
choose(_:is32Bit:profile:learned:), anddxmtMightHelp/dxvkMightHelpare now pure functions of the profile (no file I/O) instead of each re-reading the exe — so the forward choice and the reactive ladder can't contradict each other. The fallback path now does zero I/O and reuses the launch-time profile. ExecutableResolverno longer picks a redistributable as "the game". It took the largest exe recursively, so a Steam install'sVC_redist.x64.exe(~25 MB) orUEPrereqSetup_x64.execould win — poisoning the bitness read (wrong backend) and rooting the profile scan in the wrong directory. Redist dirs + installer name prefixes are excluded, failing open if that leaves nothing.- DirectX 9 under DXVK is no longer invisible. Every DXVK signature was d3d11-only, so a DX9 game — the
whole point of the backend — was silent whether it worked or died (
.unknown, status "Launched", no learning). Read out of the DXVK 1.10.3 source and verified on-device in BOTH directions: engaged =Device properties:(logged byDxvkAdapter::createDeviceAFTERvkCreateDevicesucceeds, on the path SHARED by d3d9/d3d10core/d3d11); failed =DxvkAdapter: Failed to create device(theDxvkErrormessage d3d9'sCreateDevicecatch logs verbatim). - DirectX 8 no longer gets a false failure. Nothing translates d3d8 (DXVK ships none; wine's builtin
d3d8 sits directly on wined3d, bypassing d3d9), so wined3d IS correct — but its renderer line tripped the
generic signature, producing "couldn't drive your graphics" + a pointless reroute.
isD3D8Onlynow makes both ladder gates refuse andhandleGraphicsFallbackstay silent. (Ad3d8to9wrapper imports d3d9, so a wrapped DX8 game correctly becomes a DX9 title → DXVK.) - Vulkan-native games route to DXVK (
isVulkanNative) — not for D3D translation (they need none) but because only the DXVK runtime ships a working MoltenVK; the GPTK path would hand them the stock one. - Tests: new
D3DProfileTests(sibling-DLL, subdirectory, redist exclusion, mixed-API, DX8, d3d8to9, Vulkan-native, unknown, garbage) + resolver redist cases;BackendChooserTestsladder gates rewritten as pure profile tables; a DX8 no-false-failure VM test built as a contrast (a D3D11 control must complain on the same log line) so it cannot pass vacuously — verified to fail when the guard is removed.
- New
-
🌋 DXVK / Vulkan — third graphics backend, Phase 2 model threading (2026-07-26,
main; 430 tests green). Adding DXVK (D3D9/10/11 → Vulkan → MoltenVK → Metal) as a third backend so DirectX 9 games (which neither GPTK nor DXMT can translate — today they black-screen on wined3d) and the D3D10/11 titles the Metal backends can't run get a mature compatibility path. Mirrors CrossOver 26, which ships DXVK as its Vulkan tier below D3DMetal. Plan:~/.claude/plans/silo-has-one-big-rippling-quail.md; routing decided **DX9-first- deep fallback** (GPTK > DXMT > DXVK for DX10/11; DXVK first for DX9), provenance from-source DXVK +
patched MoltenVK (never a vendored prebuilt). DXVK packaging decided NATIVE/upstream (patch-free), NOT
CrossOver's builtin: upstream DXVK has no wine-builtin build without CX patches, so Silo runs stock DXVK
as native — d3d dlls seeded into the prefix
system32/syswow64with a=noverride — on the base runtime (no clone, nothing overlaid intolib/wine), reusing theinstallDXMTPrefixLoaderspattern.
- On-device discovery (the big de-risk): Silo's from-source wine ALREADY carries the full Vulkan→Metal
substrate —
winevulkan, the winemac Vulkan backend inwin32u.sothat loads a driver viaCX_LIBVULKAN, and a bundled stock MoltenVK 1.4.1 atlib/silo-bundled/libMoltenVK.dylib. So DXVK needs only its PE dlls seeded into the prefix +=noverrides +CX_LIBVULKANpinned at that MoltenVK. (CrossOver 26.2's own DXVK =d3d9/d3d10core/d3d11PEs for both ABIs, no dxgi/d3d12/.so, but built builtin; its MoltenVK ~1.2.10 is heavily patched for geometry-shader + transform-feedback — the stock-vs-patched question Phase 0's render test settles.) - Phase 2 (landed):
GraphicsBackend.dxvk+GraphicsChoice.dxvk(all 9 exhaustive switches);BackendConfig.dxvkLibDirPath/dxvkRuntimeName+libDir(.dxvk)+dxvkSupports32Bit(32-bit DX9 is a capability GPTK structurally lacks);RuntimeVariants.prepare(.dxvk)returns the base wine (no clone);GraphicsLinker.installDXVKPrefixLoaders(seeds d3d9/10core/11 into the prefix — native,=n);LaunchOrchestratorseeds + forces=n+ exportsCX_LIBVULKAN;EnvFlags/GraphicsFallbackmade exhaustive (real DXVK log signatures captured in Phase 0). DXVK ships + seeds its OWNdxgi(upstream d3d11 is coupled to it — only CrossOver's patched build reuses wine's builtin dxgi). Compiles + unit-tests with ZERO runtimes (constraint #4); +19 tests. DXVK appears in the graphics picker but has no installer yet → an explicit pin honestly throwsbackendNotConfigureduntil Phase 3 (install/Settings) lands. - Phase 1 (landed):
versions.envpinsDXVK_REPO=doitsujin/dxvk+DXVK_VERSION=v2.6.2(reuses the DXMTLLVM_MINGW_VERSION);Scripts/build-dxvk.sh+.github/workflows/build-dxvk.ymlcross-compile upstream DXVK (meson + llvm-mingw, both ABIs, cherry-pick d3d9/d3d10core/d3d11/dxgi) →dxvk.tar.xz(+.sha256), tagdxvk-<ver>-cx<ver>, chainable fromwine-autoupdate.yml. DXVK needs NO Metal toolchain / Wine install (pure Windows-PE cross-compile), sobuild-dxvk.shruns on this CLT-only box. ✅ VALIDATED FOR REAL: ranbuild-dxvk.shend-to-end here — cross-compiled DXVK v2.6.2 →dxvk.tar.xzwith{x86_64,i386}-windows/d3d9,d3d10core,d3d11,dxgi.dll(confirmed PE32+/PE32). Real artifact in hand for the Phase 0 spike. - Phase 3 (landed):
DXVKInstallmodel;RuntimeManager.installDXVK/locateDXVKLibDir(d3d9+d3d11 signature — excludes a DXMT tree)/installedDXVK/matchedDXVKRelease;RuntimeKind.dxvk;DXVKManagerView+ a "DXVK" Settings tab;AppEnvironment.dxvkRuntimewiring (+dxvkReady, andrunFullSetupinstalls DXVK best-effort so the Automatic DX9 path works out of the box);BackendSettingsViewModel.applyDXVKLibDir/clearDXVKDefault. +3 tests, 433 green. DXVK is now installable end-to-end (the graphics picker's DXVK pin resolves once a runtime is installed). - Phase 4 (landed): DX9-first Automatic routing —
BackendChooser.choosegains anisD3D9Onlysignal (BackendChooser.isD3D9Onlyreads the PE imports: d3d9 and NONE of d3d10/11/12 → route to DXVK, the only DX9 translator, bitness-independent); computed off-main inplay/playManualfor.autoonly. Multi-target reactive learning generalized GPTK → DXMT → DXVK (fallbackAlternative+learn+dxvkMightHelp;learnedBackendnow holds.dxmtOR.dxvk, DXVK terminal).graphicsFallbackMessagesteers to the best installed alternative (install-aware).bitnessRefusalguards 32-bit DXVK.GraphicsFallback's wined3d signal reconfirmed valid for DXVK. +8 tests incl. a full DX9-Steam-game → native-DXVK integration test (base runtime,=n,CX_LIBVULKAN, seeded dlls). 438 green, zero warnings. - ✅ Phase 0 DONE — DXVK RENDERS ON SILO'S OWN WINE (2026-08-04, on-device, Apple M4 Pro). Proved
headlessly with a cross-compiled
D3D11CreateDeviceprobe run in a throwaway prefix seeded exactly likeinstallDXVKPrefixLoaders+=n:PROBE_RESULT hr=0x00000000 featureLevel=0xb000— a real D3D11 device at feature level 11_0 through DXVK →winevulkan→ MoltenVK → Metal. No DX9 game needed. Two findings, both fixed in this commit:- ROOT CAUSE — the bundled MoltenVK, not DXVK/winevulkan. A STOCK MoltenVK (the Homebrew
molten-vkthe wine build bundles, driver0.2.2209) cannot create a D3D device at ANY feature level — not even 9_1, on either DXVK 1.10.3 or 2.6.2. A PATCHED MoltenVK (driver0.2.2018,Vulkan: 1.2.290) works instantly. Isolated by a 5-way matrix (our DXVK on CrossOver's wine ✅, on Silo's wine with the patched dylib ✅, with the stock dylib ❌ — so Silo's winevulkan was exonerated). →versions.envpinsMOLTENVK_REPO/MOLTENVK_VERSION;build-dxvk.sh+build-dxvk.ymlnow build MoltenVK from Khronos source and ship it in the artifact aslib/libMoltenVK.dylib. DXVK pin moved to the validated v1.10.3 (2.6.2 was only ever tested on the broken stock driver — re-test before bumping). - 🐛 BUG:
CX_LIBVULKANwas a NO-OP.DYLD_PRINT_LIBRARIESproved wine ignored the absolute path and loadedlib/silo-bundled/libMoltenVK.dylibregardless — the Vulkan driver is resolved by dyld NAME lookup. →makePlannow prepends the DXVK runtime's ownlibdir toDYLD_FALLBACK_LIBRARY_PATH(newURL.dxvkMoltenVKDir) instead of exportingCX_LIBVULKAN; tests updated to assert the ordering. - Real engagement signatures captured →
GraphicsFallback.engagementSignatures(.dxvk)="Using feature level D3D_FEATURE_LEVEL"(version-robust), andloaderFailureSignatures(.dxvk)covers both the 1.x (Requested feature level not supported) and 2.x (Minimum required feature level,Maximum supported feature level: 0) failure wordings. +3 tests off the verbatim captured logs.
- ROOT CAUSE — the bundled MoltenVK, not DXVK/winevulkan. A STOCK MoltenVK (the Homebrew
- Still pending (needs a real game, not the probe): a DX9 title rendering end-to-end through Silo's UI,
and confirming the prefix-seeded native dlls stay dormant for the co-resident Steam client (GPTK/DXMT
games already force
=b); if the client picks them up, addd3d9,d3d10core,d3d11,dxgi=builtintoBottleDefaults. Also: CI has never runbuild-dxvk.yml(the MoltenVK step is untested on a runner).
- deep fallback** (GPTK > DXMT > DXVK for DX10/11; DXVK first for DX9), provenance from-source DXVK +
patched MoltenVK (never a vendored prebuilt). DXVK packaging decided NATIVE/upstream (patch-free), NOT
CrossOver's builtin: upstream DXVK has no wine-builtin build without CX patches, so Silo runs stock DXVK
as native — d3d dlls seeded into the prefix
-
🎮 Game-controller support: re-enable SDL in the Wine build (2026-07-26,
main; 416 tests green — CODE DONE, needs a newwine-cx-*CI release + on-device controller verification). Controllers didn't work because Wine was built--without-sdlandlibSDL2was stripped on install (M75→M80) — a real fix for a recurring off-main-threadNSWindow/NSAlert abort whenwinebus.sodlopens libSDL2. Investigation (vs the local CrossOver install, our config oracle) found the abort was a property of the generic Homebrew libSDL2, not SDL: CrossOver bundles SDL 2.30.12 x86_64, sets no SDL hints, and its bottles set no winebus registry keys — it relies purely on winebus's compiled-in defaults (Enable SDL=1,Map Controllers=1,DisableHidraw=0);Map Controllersis what remaps any pad to XInput ("just works"). Silo'swinebus.soalready contained Wine's native IOHID backend (never SDL-linked), so it never crashed and gives Xbox pads XInput today — but lacked SDL's broad mapping. Since Silo + CrossOver share the same CrossOver-FOSS winebus source, mirroring CrossOver's exact SDL is the evidence-based fix. One x86_64 SDL covers 32- + 64-bit games (winebus.so is the x86_64 unix-side driver). Changes:versions.env: pinnedSDL_VERSION=2.30.12(build-input only; not mirrored toVersions.swift).build-wine.sh+build-wine.yml: build the pinned SDL from libsdl-org source (cmake, x86_64), flip--without-sdl→--with-sdlpointed at it; drop the Homebrewsdl2dep.bundle-wine-dylibs.sh: bundle our builtlibSDL2-2.0.0.dylibintolib/silo-bundled(viaSILO_SDL_DYLIB) — winebus dlopens it by leaf name offDYLD_FALLBACK_LIBRARY_PATH.RuntimeManager: removedstripBundledSDL(call + fn + test) so the bundled SDL survives install.- No winebus registry work + deliberately no
xinput/dinputDLL overrides — CrossOver relies on Wine's builtin defaults + SDL Map Controllers; adding overrides would deviate from that proven template. - On-device-unverified (needs a fresh CI
wine-cx-*release built with these scripts): the crash gate (no NSWindow abort with 2.30.12), controller enumeration, and the non-Xbox→XInput parity win. Contingency if it still aborts:SDL_JOYSTICK_MFI=0/SDL_VIDEODRIVER=dummyin the launch env.
-
🔎 Add-via-installer now infers games from the installer's own Start-Menu shortcuts (2026-07-14,
main; 417 tests green). The failure that motivated this: a user ran an installer, then hand-picked a bare.exe(Browser.exe) that needs args + a working dir it couldn't know — blank window. CrossOver "just works" because it never asks you to pick an exe: it launches the installer's.lnk(wine --start …), which carries target +WORKING_DIR+ARGUMENTS. Silo now reads those shortcuts itself (keeps its direct-exec launch model + per-game log/backend control, unlike delegating towine start). New pieces:ShellLink(PE/ShellLink.swift) — clean-room MS-SHLLINK parser (target via LinkInfoLocalBasePath, NAME/WORKING_DIR/ARGUMENTS/ICON from StringData), Foundation-only, bounds-safe →nilon junk (mirrorsPEIcon). Fixture = the realGravityMark 1.89.lnk.BottleShortcuts(Launch/BottleShortcuts.swift) — scans a bottle's Start-Menu/Desktop for.lnks, mapsC:\…→drive_chost paths, filters uninstallers (Uninstall*/msiexec/Innounins*), dedups.ManualGamedecouples identity from bottle:bottleID(tolerant-decoded → defaults toid) so N shortcuts from ONE install share ONE prefix (N library entries, not N installs);workingDirectory(URL?) honored bymakePlan(cwd override). Bottle deletion is now ref-counted (only when the last entry using it is removed).addManualGamegainedbottleID/workingDirectory/customArgs.- Add sheet: the installer now runs blocking (
LaunchOrchestrator.runInstaller→runner.run, notspawnDetached; returnsProcessResult— an installer is transient setup, like the license-bearing component installers). Its exit when the user closes the window is the deterministic "install done" signal: Silo auto-scans + pre-selects the discovered shortcuts right then, so the Add button lights up blue with zero extra clicks (no manual "Find" button, no window-focus/polling heuristics — a small "Rescan" fallback covers the rare installer that exits before writing its shortcuts). Adding creates oneManualGameper pick, all sharing the install bottle. Installer-path only — a directly-chosen ready-to-run.exeis unchanged (no scanning). - Fixed a latent bug: the settings-sheet "Run Installer in this bottle" used
game.idnotgame.bottleID. On-device-unverified: the SwiftUI picker end-to-end (parser + discovery are unit-tested against the real.lnk; the resolved launch = exactly whatrun_browser.batruns).
-
🧩 Installer picker accepts
.msi— and the launch path actually runs it (2026-07-14,main; 407 tests green). The "Run Installer" pickers (Add-manual-game + manual-game settings) restrictedNSOpenPanelto theexeUTType, so.msipackages were greyed out. And even a selected.msicouldn't run:makePlandidwine <path>, but a.msiis data, not a PE — wine can't exec it. Two-part fix: (1)chooseExecutablegained aninstaller:opt-in that adds themsitype to the two installer pickers only (game-target pickers stay.exe-only — a launch target must be a PE image); (2)makePlanroutes an.msitarget through the bottle's builtinmsiexec /i, addressing the package via itsZ:(unix-root) DOS path (argv, no shell → spaces need no quoting).msiexec.exe/msiare already=builtininBottleDefaults, so wine's own msiexec handles it. Pureinvocation(for:)/dosPath(for:)helpers, table-tested (msi→msiexec incl. DOS-path/spaces/appended-args; exe passthrough, case-insensitive). Surfaced while prepping a GravityMark D3D12 GPTK-vs-CrossOver benchmark — GravityMark ships as a.msi. -
🎛️ Manual games now use the Automatic graphics backend too (2026-07-14,
main; 402 tests green). Manual (non-Steam) games previously took an explicitGraphicsBackend(.gptk/.dxmt, no Automatic) — a deliberate asymmetry, now reversed per the user.ManualGame.backend: GraphicsBackend→graphics: GraphicsChoice(default.auto), resolved forward by the SAMEBackendChooser.chooseplayuses (32-bit → DXMT, else GPTK).BottleResolver.manualnow takes the resolved backend explicitly (likesteam(backend:));playManualmirrorsplay's resolution + 32-bit refusals. The settings sheet + Add sheet use the sharedGraphicsChoicepicker (Automatic/GPTK/DXMT);BackendTagshowsAuto/GPTK/DXMT. Kept SIMPLE per the user: forward Automatic only — manual games do NOT get the reactive learned-DXMT hint (that machinery stays Steam-only; a GPTK failure on an Automatic manual game surfaces the honest "switch to DXMT" message rather than auto-rerouting). Tolerant Codable migrates an old explicitbackend→ the matching explicit choice; a config with neither key →.auto. Bonus UX win: a 32-bit manual game added with the default now routes to DXMT automatically instead of the old default-GPTK → refusal. Tests: migration + both Automatic-split paths. -
🔗 Re-added "Create Desktop Shortcut" — as a deep-link, for BOTH game kinds (2026-07-14,
main; 399 tests green). The old (removed 2026-07-10) shortcut snapshotted aLaunchPlaninto a standalone.appthatexec'd wine directly — which went stale, needed DXMT prefix pre-seeding (prepareGraphics), read "wine" in the Dock, and couldn't serve Steam titles (no co-resident client). Rebuilt on asilo://URL scheme instead: a shortcut.appjustopenssilo://play/steam/<appID>orsilo://play/manual/<uuid>, so the running/relaunched Silo resolves the backend (Automatic/learned-DXMT), prefix, and Steam client fresh at click time — correct forever, and works for Steam and manual games. New:SiloDeepLink(pure parse/build),GameShortcut(builds anLSUIElementagent.app, per-game bundle id, sanitized filename),AppEnvironment.handleDeepLink/route(+ a pending-link queue for cold-launch before the library loads),SiloApp.onOpenURL,CFBundleURLTypesinInfo.plist.template,GameLibraryViewModel.makeShortcut(for:)×2, and a "Create Desktop Shortcut" menu item on both tiles (best-effort icon: PE icon for manual, Steam header art for Steam). NoprepareGraphics/prefix-seeding needed — the whole class of problems is gone. On-device-unverified: the LaunchServices scheme registration +onOpenURLdelivery (needs the assembled, registered.app; the plist isplutil-clean and the pure logic is unit-tested). -
🧹 Post-change review sweep (2026-07-13,
main; 384 tests green). Two adversarial review agents over the session's changes (switcher + Dock removal). Verdict: Dock removal left NO rot; the PE parser and switcher state machine are bounds-safe / fail-open / correct. Fixed the one real finding + closed a test gap:- Bug (medium):
GameSettingsViewModel.learnedBackendmisreported a stale hint. It gated only ongraphics == .auto, not the runtime matchplayapplies — so after a GPTK upgrade the sheet showed "Automatic is using DXMT" while the game actually re-probed GPTK. Now takes the currentgptkRuntimeName(viaAppEnvironment.makeGameSettings) and mirrors the launch gate. +test. - Test gap: legacy VA-format delay import.
PEFixture.withDelayImportsgained alegacyVAImageBaseoption; new test covers thenameField &- imageBaseLow(grAttrs bit0=0) branch. - Tightened the
save()hint-reset comment (it overclaimed re-probe on an already-Automatic Save). - Accepted by design (not changed): the inert stale hint left in
config.json(ignored everywhere), the import-dir-not-gated-on-numDirsfail-open path, DXMT engagement precedence, and lingering inert monitors.
- Bug (medium):
-
🗑️ Removed the Dock-tile-naming feature (
DockAppBundle) (2026-07-13,main; 382 tests green). Steam (and games) show a Dock tile named "wine". The Phase-3DockAppBundle.app-wrapper (below) never fixed that — it named only the windowless launcher process Silo spawns, while Steam's window-owning children (theexplorervirtual desktop + CEFsteamwebhelper, spawned by wine viaWINELOADER) stayed "wine". A CrossOver-style co-located named-loader retry also failed (wine resolves the loader symlink back to its real name "wine"; the winemac driver is byte-identical to CrossOver's, so it's launch-side, but neither the wrapper nor the named loader reaches the children). Per the user, not worth the complexity for a cosmetic tile — removed entirely: deletedDockAppBundle(+tests),Silo.pinWineLoader,AppPaths.dockAppsDir,LaunchOrchestrator.{DockIdentity,launchVia}, and thedock:params;launchSteam/launchInBottle/launchManualGamenow spawn the wine loader directly. Tests that usedWINELOADER(set only as a wrapper side-effect) as the runtime proxy now assert on the spawnexecutable. -
🔎 Switcher hardening: positive engagement + delay-load imports + re-probe UX (2026-07-13,
main; 388 tests green). Three follow-ups from the honest post-Part-A review:- #1 Positive engagement (
GraphicsFallback): detection was inference-from-absence. AddedStatus.engaged+ backend-keyedengagementSignatures— DXMT logsDXMT: created Metal device, so a DXMT launch is now POSITIVELY confirmed (engagement wins over a later stray wined3d line; the monitor tears its watch down on engagement → no false "DXMT couldn't drive"). GPTK/D3DMetal success is SILENT in winediag (no positive line exists), so a healthy GPTK launch stays.unknown— documented, not faked. (Exact DXMT string to reconfirm on-device.) - #3 Delay-load imports (
WindowsExecutable):importedDLLswalked only the regular import directory, so a title that DELAY-loadsd3d12/d3d9(common) looked import-less anddxmtMightHelpfell through to its permissive path. Now also walks the delay-load directory (index 13, 32-byteImgDelayDescr, RVA name format + legacy-VA fallback), gated onNumberOfRvaAndSizes.PEFixture.withDelayImports+ tests. - #4 Re-probe UX (
GameSettingsSheet): a learned-DXMT game shows as "Automatic," so the sheet now surfaces "Automatic is using DXMT — GPTK couldn't run this game." + a Re-probe GPTK button (GameSettingsViewModel.reprobeGPTK) that clears the hint immediately (no two-step dance). - #5 resolved by POLICY (user, 2026-07-13): "GPTK can be considered always faster than DXMT — use it
unless it doesn't work." So there's no perf-ranking to build; GPTK is the defined-preferred backend and
DXMT is strictly a fallback (32-bit, or a proven GPTK failure). Made explicit in
BackendChooser's doc; the switcher already implements it. #2 skipped (user): avoiding the wasted first GPTK launch would need a per-title compat DB with no 64-bit data to seed it. - Still pending: Part B on-device signature capture (real DXMT-failure + reconfirm the DXMT-engaged string) needs one real game launch through Silo.
- #1 Positive engagement (
-
🧠 Automatic backend switcher: learned-hint split + GPTK re-probe (2026-07-13,
main; 381 tests green). Part A of the switcher-improvement plan. The reactive GPTK→DXMT downgrade previously overwrote the user'sgraphics = .autowith.dxmt— permanent, indistinguishable from a manual pin in the settings UI, and a GPTK runtime upgrade never re-tried GPTK. It now records a SEPARATE, re-evaluable hint:GameConfig.learnedBackend+learnedUnderRuntime(new, tolerant-decode,encodeIfPresent):.autosurvives, the sheet still shows "Automatic."BackendChooser.choose(_:is32Bit:learned:)consults the hint only for a 64-bit Automatic launch (explicit pin wins; 32-bit → DXMT regardless).- GPTK re-probe:
playdrops a stale hint whenconfig.learnedUnderRuntime != backend.gptkRuntimeName(a GPTK upgrade may fix the title), so GPTK is re-tried;learnDXMT→learnBackendstamps the currentgptkRuntimeNamewhen it learns. Thrash is impossible (a learned game getschosen == .dxmt→ not eligible to re-learn). - Hint reset:
GameSettingsViewModel.saveretires the hint only when the user actually changes the graphics picker (an unrelated Save keeps it). - Full offline state-machine coverage:
BackendChooser/GameConfig/GameLibraryViewModel/ViewModeltests (rewroteautoReactiveSwitchToDXMTto assert.autopreserved +learnedBackend == .dxmt; +7 new cases). - Part B deferred (on-device capture): real DXMT-specific failure + positive "GPTK engaged" log
signatures for
GraphicsFallback. The box has a working bottle + all 3 runtimes but no per-game log yet (onlysteam-bottle.log, where the Steam client runs wined3d/Vulkan — confirming a game needs itsd3d11=builtinoverride to make the wined3d signal trustworthy). Capture needs one real game launch.
-
✂️ Setup: dropped the warm-up progress % (2026-07-13,
main; 373 tests green). The "Steam is updating itself — N%…" counter was parsed out of Steam's own updater log (Downloading update (X of Y KB)), but the fraction was unreliable and not worth the complexity — removed the whole mechanism.WarmUpPhase.downloadingno longer carries a fraction;SteamBottle.updateState()→isUpdateCommitted() -> Bool(kept the one reliable signal — theUpdate completemarker the warm-up waits on);SteamBottleViewModel.warmUpFractiongone; both progress bars (onboarding + General settings) are now plain indeterminate. Status reads simply "Steam is updating itself…". No behaviour change to the warm-up's completion logic. -
✍️ Message review — one consistent voice (2026-07-12,
main; 373 tests green). Went over every user-facing status/error string. Unified the error voice to"Couldn't <verb>: <detail>"across all VMs (was a mix of that and"<X> failed:"— RuntimeVM/GPTKVM/BackendVM/installer now match GameLibrary/relocation; the top-level"Setup failed:"outcome is the one intentional exception). Shortened the last verbose lines (the exFAT-drive warning, the mid-move / drive-not-connected relocation messages) and aligned "Bottles drive not connected." everywhere. Progress ="<Verb>ing …", success = terse past tense, prompts = short imperative. -
⏬ Setup: download EVERYTHING at "Set up" in the background; dropped the download cache (2026-07-12,
main; 373 tests green). Rebuilt the setup download flow per the user's spec. Previously only core fonts were prefetched, so Source Han Sans (~360 MB) only started downloading when its install step arrived — blocking there for minutes while the status misleadingly read "Installing Asian Fonts…".- New
SetupDownloads(Sources/SiloKit/Steam/SetupDownloads.swift): the moment "Set up" is pressed,SteamBottleViewModel.setUpcallsbottle.startSetupDownloads(), which kicks off EVERY component's artifacts (core fonts, SHS, d3dcompiler cabs, MSVC redist) concurrently into a fresh temp dir — so the slow ones overlap the Steam download + wineboot + the earlier install steps. It skips components already installed (a re-run doesn't re-download 360 MB), and SHA-verifies the pinned artifacts (core fonts, cabs). - Download separated from install:
installCoreFonts/installSourceHanSans/installD3DCompiler47/installVCRedistnowawaittheir artifact fromSetupDownloadsand install it (staging intodrive_cwhere Wine needs aC:\…path), instead of downloading inline.provisionComponentsawaits each step's download, narrating.downloading("Downloading …") when it's still in flight, then.installing— exactly the "show downloading instead of a slow installing" the user asked for. - Cache removed: deleted the persistent
AppPaths.downloadCacheDir+cachedCoreFontExe/prefetchCoreFonts(the user found it needlessly complex + prone to stale installers). The temp dir is wiped at the start of every run and removed oncleanup()— always fresh, never a stale installer. LockedBox.mutateadded (atomic Set insert). Tests: install methods take adownloads; a directSetupDownloadstest (fetch + SHA-verify + temp cleanup) replaces the old cache test; also fixed a pre-existing latent test that did a real 20 s network download (now fully stubbed → suite runs in ~1 s). On-device-unverified (shares the setup path caveat).
- New
-
✂️ Shortened + homogenised user-facing status messages (2026-07-12,
main; 373 tests green). Trimmed the verbose, self-explanatory status lines to a consistent house style — progress as"<Verb>ing …"("Creating bottle…", "Downloading Steam client…", "Installing Asian Fonts…"), success as a terse past-tense sentence, errors as"Couldn't … ."/"… failed: <detail>". Setup got the biggest cut (dropped the "one-time, this can take a few minutes", the multi-clause ready/paused copy, and the wordy license line — now "Accept the license — ⌘-Tab if it's behind Silo."). Also trimmed the library launch-guard / uninstall / 32-bit / play-date / retina / "no build published" lines. All substrings the tests pin were preserved (no test copy changes). The already-terse per-title graphics-fallback messages were left as-is. -
🪟 Setup: REMOVED the installer-window focuser; rely on a ⌘-Tab hint instead (2026-07-12,
main; 373 tests green). Wine setup windows (Core Fonts EULA, MSVC redist, Steam) open BEHIND Silo, and reliably raising them proved unachievable from Silo's side: aProcess-forked Wine app doesn't self-activate, and macOS's focus-stealing guard refuses cross-app activation (even the cooperativeNSApp.yieldActivation). Three attempts (launch-notification, poll, KVO onrunningApplications— all + cooperative activation) didn't work; the durable fix would be winemac.drv-side (self-activating the window), a Wine-build change deferred until it's worth it. SoInstallerWindowFocuser(+ itsGuidedInstallFocusingprotocol, the VM'sfocuser/wineRoot, and the arm/disarm wiring) is deleted — not worth the complexity for a ⌘-Tab-able window. The user-guided component status now tells the user a license window opened and to press ⌘-Tab / click it in the Dock if it's behind Silo (they're looking at Silo when it is, so they see the hint). Net −2 files, simpler setup VM. -
🔎 Setup logical sweep + fixes before the on-device test (2026-07-12,
main;swift buildclean, 375 tests green). Two fresh-eyes audits of the whole setup path (runFullSetup→setUp→provisionComponents→ warm-up) confirmed the happy path is sound; fixed the defects most likely to bite an on-device run:- VC-redist no longer hard-fails on a weird Wine exit code (top on-device risk). It marked success on
{0,3010,1638}and treated EVERYTHING else — including a non-standard code an actually-completed installer can return under Wine — as a fatalcomponentCancelled, halting setup before Steam and re-failing every run (the SAME exit-code unreliability that just broke Core Fonts). Now only a real cancel (1602/1223) is fatal; any other outcome is best-effort (unmarked, re-prompts, continues to Steam). - Core-fonts license shows on the first AVAILABLE font, not a hard-coded index 0 — so a failed download of andale32 no longer silently skips the license while the rest install. And a DECLINE now installs NO core fonts (they share one license) and stops best-effort, instead of installing the rest.
- Warm-up settles after setUp's force-quit before its fresh launch (reuses
warmUpForceQuitSettle; 0 in tests), so the update client doesn't race the wineserver reaping the killed installer procs. - The background font prefetch is cancelled on an early setUp failure (defer), so it can't outlive setUp and race a re-run's prefetch/install on the same cache.
- Tests +2 (VC-redist unknown-code best-effort; license-on-first-available-font). Deferred NITs (noted,
low-risk):
hasCoreFontskeys on Arial only (a failed arial32 re-runs the font install each setup — wasteful, not broken); d3dcompiler destroy-before-verify on a resume (self-heals); a config-save failure surfacing a misleading "Set up Wine first." Still shares the Core Fonts path's on-device-unverified caveat.
- VC-redist no longer hard-fails on a weird Wine exit code (top on-device risk). It marked success on
-
⚡ Setup: prefetch the core fonts in the background at "Set up" (2026-07-12,
main;swift buildclean, 373 tests green). Follow-up to the Core Fonts fix — the font installers now download the MOMENT "Set up" is pressed, into a persistentpaths.downloadCacheDir(undersupportDir, so it works before the prefix exists and survives across runs), overlapping the Steam download + wineboot instead of stalling the Core Fonts step. NewSteamBottle.prefetchCoreFonts()(best-effort; no-ops whenhasCoreFonts) + a sharedcachedCoreFontExefetch-or-cache primitive that both the prefetch andinstallCoreFontsconsume — so each font downloads at most once (HTTPS-guarded, SHA-verified).SteamBottleViewModel.setUpkicks it off up front andawaits it just before the component phase (warm cache ⇒ no wait there, no double-download) — surfacing a "Downloading core fonts…" status there ONLY if the prefetch is still running (aLockedBoxcompletion flag), so a warm cache skips the flash. The user-guided component narration is now the short "Accept the … license in the window that opens…" (the "downloading first" text is obsolete now the download is prefetched). Shares the Core Fonts fix's on-device-unverified caveat. -
🩹 Fix: Core Fonts setup step (regression in 0.3.5) (2026-07-12,
main;swift buildclean, 372 tests green). A user hit two problems at the Core Fonts step during a real on-device setup:- Accepting the license was misread as a cancel → setup halted. The IExpress core-font installers return
a NON-ZERO exit code even on Accept under Wine (winetricks never runs them for exactly this reason — it
extracts them), so the exit-code check (
result?.succeeded) falsely trippedcomponentCancelled. ReworkedinstallCoreFonts: every font is extracted to Silo's own dir via/C /T(reliable under Wine), the FIRST runs WITHOUT/Qso IExpress shows Microsoft's license once, and accept-vs-decline is read from whether the.ttfactually extracted — a decline now just skips that font (best-effort), it never fails setup. Core Fonts no longer usescomponentCancelled(VC-redist / Steam still hard-stop on cancel, via their reliable MSI/installer exit codes). - Confusing "Accept the license" message appeared minutes before the window (during the font download). The user-guided component narration now says the license window will open shortly and may take a moment to download first.
⚠️ On-device-unverified (Wine absent on the dev box): the logic is unit-tested, but the Wine-specific behavior —/C /T(no/Q) showing the license + extracting the.ttfon Accept — needs a real Set up run to confirm before shipping a 0.3.6. Worst case it extracts silently (license not shown) but setup still completes; it can no longer falsely block.
- Accepting the license was misread as a cancel → setup halted. The IExpress core-font installers return
a NON-ZERO exit code even on Accept under Wine (winetricks never runs them for exactly this reason — it
extracts them), so the exit-code check (
-
🔐 Supply-chain integrity — third-party downloads now content-pinned (2026-07-12,
main;swift buildclean + zero warnings, 372 tests green). Closes the remaining production gate from sweep #2: the artifacts Silo downloads and then EXECUTES under Wine are now verified against pinned SHA-256 before they run, so a compromised mirror/CDN can no longer feed the prefix malicious code.- Core fonts pinned.
Silo.coreFontSHA256pins all 11 self-extracting.exes (winetricks' publishedload_corefontsdigests, cross-checked here against the live SourceForge and pushcx bytes — both matched).SteamBottle.installCoreFontsverifies each download before executing it; a tampered/corrupt mirror falls through to the other mirror, and if neither verifies the font is dropped (never run). The pushcx GitHub fallback is now safe to keep (it's winetricks' own primary mirror, and the pin makes the source immaterial) — so it stays, for resilience, rather than being removed. - Bugfix found while pinning: the core-font list had
webdings32, but the real corefonts filename iswebdin32—webdings32.exe404s on both mirrors, so Webdings never installed. Fixed towebdin32(+ its verified pin). - d3dcompiler cabs pinned.
Silo.d3dCompiler47{X64,X86}CabSHA256pin the two MS SDK cabinets (verified beforewine expands the DLL games load). winetricks doesn't pin these, so the values are SHA-256 of Microsoft's owndownload.microsoft.comHTTPS artifacts (immutable GUID-named SDK cabs; stable across re-download) — trust-on-first-use from the vendor, strictly stronger than the prior zero verification. - Redirect-safe HTTPS was already covered — by ATS, more strongly than app code could. Confirmed
NSAllowsArbitraryLoads=false(Info.plist) makes the OS refuse cleartext at EVERY hop, so a redirect tohttp://fails the whole request (through SourceForge/aka.msredirectors too). Documented the layering inDownloadGuardrather than adding a redundant per-hop delegate. The residual (https→attacker-https) is what the content pins above close. - Design: digest maps are injected into
SteamBottle(default = the realSilopins). A missing key = "unpinned → don't verify"; a completeness test asserts the production maps cover every font + both cabs, so fail-open-on-missing can't bite in production while tests inject[:]to run the install flow with stub bytes. Tests (+2): a mismatched font digest is dropped + never executed (accept + reject paths); pins are complete. - Still deferred (correctly): the Steam + VC-redist bootstrappers auto-rotate versions, so they stay HTTPS + official-host (aka.ms / steamstatic) without a content pin; and the self-update codesign/ notarization check remains a no-op until there's an Apple Developer ID (see BLOCKED). Neither is a fixed-artifact pin, so neither is guessed inline.
- Core fonts pinned.
-
**🛡️ Production-hardening sweep #2 — security/integrity/robustness (2026-07-12,
main;swift buildclean- zero warnings, 370 tests green).** A second, differently-scoped pass (3 parallel audits: security/injection
surface, download & extraction integrity, untrusted-input & filesystem/concurrency robustness) — the first
sweep was logical consistency; this one targets the production-bar dimensions it didn't. Verdict: the
parsers/PE-readers/config-store are genuinely well-hardened (depth-capped recursion, bounds-checked every PE
offset, atomic+backed-up config, no shell/
Process-argv injection, ATS blocks cleartext); the real defects were a filesystem TOCTOU + a couple of resource/robustness gaps, patched:
- Bottle-move TOCTOU (data corruption) closed.
launchInFlightclears whenspawnDetachedreturns — before wine creates its wineserver socket — so a game launched during a slow cross-volume move was invisible to both liveness signals and its prefix got deleted out from under it.BottleRelocator.movenow takes asourcesInUseprobe re-checked right before the source rename/delete (the point of no return); the coordinator passesWineServerProbe.isAnyBottleLive, aborting + rolling back (sources intact) with a clear message if a bottle went live mid-move. - GraphicsFallbackMonitor kqueue-fd leak bounded. A healthy launch (the common case) never fired the
fallback signature, so its
FileWatchstayed armed for the whole session — one leaked fd per launch. The monitor now auto-releases the watch after a boundedobservationWindow(120s; the backend engages within seconds), self-cancelling on fire/stop. libraryfolders.vdfread is now size-capped like the appmanifest read already was (a hostile/corrupt multi-MB VDF is no longer slurped into memory);maxManifestBytesbecame an injectableDiscoveryEngineparam.- Tests (+3): relocator aborts + rolls back on a mid-move live bottle (both cross-volume and rename paths); oversized VDF skipped; fallback monitor releases its watch after the window.
⚠️ REMAINING production gate — supply-chain integrity (deferred, needs external inputs): Silo checksum-verifies the two artifacts it publishes (Wine/DXMT runtime, app self-update) but the four third-party artifacts it downloads and executes under Wine — Steam installer, VC++ redist, MS core-font.exes, d3dcompiler CABs — are HTTPS-only, not integrity-verified; a compromised mirror/CDN → code execution in the prefix (sharpest edge: the core-fonts fallback pulls.exes from a personal GitHub repo,pushcx/corefonts, inSilo.swift). Also:DownloadGuard.requireHTTPSchecks only the initial URL, not redirect hops (ATS blocks http→ downgrades but not https→attacker-host); a user-overridden runtime repo skips the digest when no sidecar.sha256; and the self-update does no codesign/notarization check before replacing the running app (a no-op until there's a Developer ID — see BLOCKED). Fix track: pin SHA-256 for the fixed-version artifacts (corefonts/d3dcompiler/SHS — the Steam/vcredist bootstrappers auto-rotate, so those stay HTTPS+official-host), drop the personal-repo fallback, re-applyrequireHTTPSper redirect hop, and add the codesign check when signing lands. This is the real "before production" item; needs trustworthy published hashes, so it's a dedicated follow-up, not guessed inline.
- zero warnings, 370 tests green).** A second, differently-scoped pass (3 parallel audits: security/injection
surface, download & extraction integrity, untrusted-input & filesystem/concurrency robustness) — the first
sweep was logical consistency; this one targets the production-bar dimensions it didn't. Verdict: the
parsers/PE-readers/config-store are genuinely well-hardened (depth-capped recursion, bounds-checked every PE
offset, atomic+backed-up config, no shell/
-
🧭 Production-readiness architecture sweep — inconsistencies patched (2026-07-12,
main;swift buildclean + zero warnings, 367 tests green). A logical sweep (4 parallel audits: concurrency/protocol boundaries, single-source-of-truth routing, dead-code/stale-refs, big-file logic) found the core architecture sound (no@unchecked, no strayFoundation.Process, entitlements sandbox-free, both launch paths route throughBottleResolver). Fixed the real inconsistencies it surfaced:- Steam readiness now cross-checks liveness.
SteamClientSession.isRunningANDed the regActiveProcesspid withWineServerProbe.isLive(prefix:)— a stale non-zero pid (crash / setup's warm-uptaskkill /F) no longer reads as "up" and letsensureRunningskip the relaunch → a game launched against a dead client (silentSteamAPI_Initfail). The reg pid is Wine's Windows-pid namespace (not host-killable), so the wineserver socket is the correct host-side liveness signal. playManualgained the 32-bit-DXMT guardplayalready had. A manual DXMT game with a 32-bit exe on a 64-bit-only DXMT build was refused honestly instead of launching to a silent black screen.- Wine maintenance tools route through
BottleResolver. winecfg (Steam + manual) / regedit / retina no longer hard-codepaths.steamBottle+backend.wineBinaryPath; newBottleResolver .steamTool/.manualTool→ToolTarget, andrunWineTooltakes a resolvedwine. Retina also gates on a BOOTED prefix (system.reg) not the downloaded client, so it no longer reports success while silently skipping the write. - Setup idempotency. Core-fonts EULA is recorded by a dedicated marker (
.silo-installed/corefonts-eula) so a partial-failure resume never re-prompts the already-accepted license; Source Han Sans marks a pack installed only when ≥1.otfactually landed (a truncated-but-tar-OK archive is retried, not recorded). - Tests (+6): stale-pid-without-wineserver liveness;
playManual32-bit refusal;BottleResolvertool-targets + wineNotConfigured; core-fonts EULA-resume runs silent; SHS empty-extract not marked.setSteamReadytest helper now stages/unstages the wineserver socket to model a genuinely-live bottle. - Doc drift reconciled: STATUS
## BLOCKED+ Handoff checklist purged of removed features (stop(), flat-10s grace,.sharedSteamClient/.emulatorStub,Kegworksplaceholder repo); CLAUDE.md concurrency tier fixed (BackendResolver→BackendChooser.choose/BottleResolver, noteddxmtMightHelp's PE read) and the models rule split into persisted vs FS-probe descriptors. - Deliberately NOT changed (documented decisions / test-load-bearing, not defects): the
.gptkdefault on the puremakePlan/EnvFlags.environment/runInstaller(makePlan is the pure builder, always fed an explicitgraphicsby the launch methods); the four FS-probe model descriptors staySendable/Equatable/Identifiable(contract clarified rather than bloating them with unusedCodable).
- Steam readiness now cross-checks liveness.
-
🪟 Setup installer windows: focus them + a cancel now stops setup (2026-07-12,
main;swift buildclean + zero warnings, 360 tests green). Two onboarding annoyances the user hit during a real setup:- Focus the license/installer windows. A window Silo's forked
wineopens (a Core Fonts EULA, an MSVC redist, the Steam installer) lands behind the still-active Silo, so the user can miss that it appeared at all. NewInstallerWindowFocuser(Sources/SiloKit/Support/, behind aGuidedInstallFocusingprotocol so the VM unit-tests with a spy) observesNSWorkspace.didLaunchApplicationNotificationandactivate()s the launched app whose executable lives under the Wine runtime root (isWineApp, trailing-slash guarded so…/wine-dxmtcan't match…/wine).SteamBottleViewModelarms it per user-guided component step and disarms between steps / before the windowless warm-up / on exit. Fail-safe: an unmatched window just stays where macOS put it (today's behaviour) — never a regression. On-device-unverified (Wine absent on the dev box); the arm/disarm bracketing + the match predicate are unit-tested. - Cancelling a font/redist installer now FAILS setup instead of silently continuing with a
half-provisioned bottle. Declining the first Core Font EULA, or a non-success MSVC redist exit (incl. a
1602 user cancel), throws
BottleError.componentCancelled(component);provisionComponentsrethrows it (was best-effort for everything but Steam). Nothing is marked, so the next Set up re-prompts that component. The VM surfaces it as a pause — "Setup paused — you cancelled the … installer. Run Set up again to finish." — not a hard failure. - Tests (+5): VC-redist cancel now asserts the throw; first-Core-Font decline throws + installs nothing +
stops after the first font;
provisionComponentsrethrows a mid-set cancel (Steam never runs);isWineApppredicate; the setUp flow arms the focuser with the runtime root on the user-guided step, then disarms;setupFailureMessagecancel copy.
- Focus the license/installer windows. A window Silo's forked
-
🎛️ Automatic graphics backend (GPTK ⇄ DXMT) for the shared Steam bottle (2026-07-11,
main;swift buildclean + zero warnings, 354 tests green). Steam games are no longer GPTK-only: each has a per-gameGraphicsChoice(.auto/.gptk/.dxmt, default.auto) and GPTK + DXMT games co-reside in the ONE Steam bottle. Investigation (local CrossOver 26 + web) confirmed CrossOver's "Automatic" is a proprietary online per-title DB (default → wined3d, unusable) and that backend selection is per-process env — so Silo's variant-runtime + per-launch-overrides design already matches, and Silo's Automatic is an educated guess from the game binary + reactive learning instead of a title DB.BackendChooser(pure,Sources/SiloKit/Launch/BackendChooser.swift):.auto→ 32-bit ⇒ DXMT (GPTK is 64-bit-only), else GPTK (the proven default; also the only D3D12 path).dxmtMightHelpreads the PE import table (WindowsExecutable.importedDLLs) to gate the reactive switch — fail-open (empty imports / dynamicLoadLibraryloaders → try DXMT), suppressed only when confident DXMT can't help (imports D3D12, or D3D9 with no D3D10/11).- Reactive learning: when the
GraphicsFallbackMonitordetects "GPTK didn't engage" on an.autogame and DXMT is installed + might help,playpersists.dxmtfor that game ("Silo will use DXMT next time"). GameConfig.graphics(tolerant decode,graphicskey — the legacy dual-bottlebackendkey stays ignored);BottleResolver.steam(backend:config:)routes a DXMT Steam game onto the DXMT variant clone in the SAME Steam prefix (unconfigured DXMT still throwsbackendNotConfigured);playpicks the backend off the chooser (32-bit-on-explicit-GPTK still refused, now steering to DXMT); a Graphics picker (Automatic/GPTK/DXMT) added to the Steam game settings sheet. Fallback/refusal messages unified to steer to DXMT (Steam + manual both have a per-game Graphics setting).- Tests (+10):
BackendChoosertable + PE-import reader (synthetic PE32/PE32+ fixtures, fail-open);BottleResolver.steam(backend:.dxmt)→ clone runtime + Steam prefix (+ refusal);playauto-routes a 32-bit Steam game onto the DXMT clone in the shared prefix with winemetal seeded; reactive switch persists.dxmt;GameConfiggraphics codec. - Review fixes (2026-07-11, high-effort multi-agent review): the Steam Graphics picker now actually
persists (
GameSettingsViewModel.savewas droppinggraphics); the reactive switch re-reads fresh config at fire time (can't clobber an explicit pin or write for an uninstalled DXMT) and only promises the switch when the write succeeds; the fallback message steers to DXMT only when it could help (no false steer for D3D12/D3D9-only); a 32-bit game routed to a 64-bit-only DXMT is refused up front (BackendConfig .dxmtSupports32Bit);playresolves the exe ONCE and hands it tolaunchInBottle(no double install-dir walk, decision + launch use the same binary);BackendChooser.chooseis now pure (is32Bit:in). 355 tests green. - Quality pass: the failure-only PE import read (
dxmtMightHelp) is now lazy — computed inhandleGraphicsFallbackonly when a fallback actually fires, never on a healthy launch; the reactive-learn logic is factored into namedhandleGraphicsFallback/learnDXMT/fallbackMessagemethods (no dense nested closure); the.gptkdefault was removed fromBottleResolver.steam/launchInBottle/launchManualGameso a future launch path can't silently land on GPTK (makePlankeeps its default — it's the pure builder, always fedgraphicsby those methods); the four copied test PE-byte builders collapsed into oneSupport/PEFixture. No polling loops (fallback detection stays kqueue-driven viaGraphicsFallbackMonitor). - On-device (Wine absent here): (1) co-residency — with the bottle Steam up, launch a DXMT-routed
game and confirm it joins the SAME wineserver (one
server-*socket under/tmp/.wine-$(id -u)/or$TMPDIR), Steamworks connects, and the log shows DXMT's feature level; (2) a 32-bit Steam title (e.g. Overcooked 2) end-to-end via Automatic → DXMT (needs the both-ABI DXMT release asset — confirm i386 is published, else runbuild-dxmt.yml); (3) a known-good GPTK title unchanged; (4) a GPTK-failing DX11 title flips itself to DXMT and works on the second launch.
-
🚪 Phase 4 — quit leaves Steam + games running; PID-free bottle liveness (2026-07-10,
main;swift buildclean + zero warnings, 350 tests green). Like CrossOver, Silo now LAUNCHES detached and never owns a launched process's lifecycle: quitting Silo no longer kills Steam or games, and there is no per-game Stop button, PID tracking, or exit observer.- Removed: the app-quit teardown (
AppEnvironment.terminateAllOnQuit+ theRootViewwillTerminate hook);GameProcessCoordinator(the PID/observer table); the per-game Stop button + running badge (tiles are just Play / Launching…);SteamClientSession.stop/force-quit +SteamBottle.forceQuitSync;LaunchOrchestrator.stopGame/observeExit/resolvedExecutableName; and theProcessLedgerPID shadow (+ the now-deadobserveExit/spawnDetachedForget/startTime/ProcessObservationprimitives). KEPTisRunning/terminate+SteamBottle.forceQuit/shutdownSteamfor the first-run WARM-UP only (setup plumbing that owns a transient client PID locally to drive its download/relaunch loop). - New
WineServerProbe(Sources/SiloKit/Process/WineServerProbe.swift): PID-free bottle liveness via the wineserver socket (<tmp>/.wine-<uid>/server-<dev>-<inode>/socket, keyed by the prefix's dev+inode — the identity wine itself uses). Replaces the ledger as the corruption guard:blockedForBottleWork/anythingRunningrefuse a bottle move / self-update while ANY bottle's wineserver is live — INCLUDING a crash orphan (its socket persists), so the crash-orphan protection survives PID-free.removeManualrefuses while the game's own bottle is live. SteamClientSessionoff PIDs:isRunning=SteamReadiness.isReady(Steam's own registeredActiveProcesspid, not a PID Silo tracks);ensureRunningcoalesces concurrent callers, skips a redundant relaunch when already ready (Steam single-instances anyway), and reports launch success.- What the "wine" processes taught us (winedevice.exe ×2, wineserver, wineloader when only Steam runs): the wineserver is the detached per-prefix daemon that outlives the launcher — so bottle liveness belongs to the SOCKET, not a PID Silo holds. That's the basis for both halves of this phase.
- Tests: deleted the coordinator/ledger suites + every stop/kill/track test; added
WineServerProbeTests+ a fake-socket fixture; the gate tests drive a fake wineserver socket and the Steam tests drive readiness viauser.reg. New: "quitting does NOT kill launched games or Steam." - On-device (Wine absent here): confirm the exact temp root wine uses for its socket (
/tmpvs$TMPDIRvs$XDG_RUNTIME_DIR— all three are probed; verify the one this runtime uses) so the guard actually fires; confirm quitting Silo leaves Steam + a running game alive.
- Removed: the app-quit teardown (
-
🪟 Phase 3 — correctly NAMED Dock tiles for Silo-launched Steam + games (2026-07-10,
main;swift buildclean + zero warnings, 371 tests green). A barewine steam.exelaunch shows a Dock tile named "wine". macOS names a GUI process's tile from[NSBundle mainBundle].CFBundleName, resolved from the executable path AS INVOKED — so Silo now spawns each launch through a generated.appwrapper whoseContents/MacOS/<name>is a symlink to the wine loader: spawning that in-bundle path makesmainBundleresolve to the wrapper → the tile is named "Steam" / the game's name. Confirmed from the loader binary that this is safe: the macOS loader maps ntdll in-process (no preloader re-exec — no exec symbols; itrealpaths_NSGetExecutablePathfor lib discovery, which FOLLOWS the symlink to the real runtime, while CFBundle uses the UNRESOLVED invoked path), so a bare symlink yields BOTH the name AND correct lib self-location at once.- New
DockAppBundle(Sources/SiloKit/Launch/DockAppBundle.swift): pure plist builder +writethat (re)creates<folder>.appwith theMacOS/<exe>symlink. No bundle icon —winemac.drvsupplies the live tile icon from the game window at runtime; the wrapper only fixes the NAME. Silo.pinWineLoadersetsWINELOADER/WINESERVERto the REAL runtime (safe: the INITIAL process never re-execs the loader; only child procs do — so it must NOT be the symlink, or every child would be named).makePlangainslaunchVia;launchInBottle/launchManualGamegain aDockIdentity(name + stable folder slug +paths.dockAppsDir).SteamBottle.launchSteamwraps the client asSteam.app(itsexplorer /desktop=root window owns the tile). Best-effort: a wrapper-write failure falls back to launching the loader directly (tile → "wine").- Wrappers live under
supportDir/DockApps(always reachable — not the relocatable bottles drive). - Tests (+7):
DockAppBundleTests(names via CFBundleName, no icon/LSUIElement, symlink target, idempotent repoint);makePlanlaunchVia (spawns the symlink + pins WINELOADER/WINESERVER); the DXMT-manual + Steam launch tests now assert the wrapper executable + pinned loader. - On-device (Wine absent here): confirm the Steam window's PID reports
<Name>vialsappinfo info -only name <pid>and that a single primary tile appears (a straysteamwebhelpertile would be a child-coalescing follow-up — CrossOver solves that with a proprietary helper Silo doesn't have).
- New
-
🧹 Post-Phase-4 — removed the "Create Desktop Shortcut" feature (2026-07-10,
main; 344 tests green). Per the user, the Desktop-shortcut feature is gone entirely (GameAppShortcut,GameLibraryViewModel.makeShortcut,LaunchOrchestrator.prepareGraphics, the tile menu item, and their tests) — which also moots the Phase 3 follow-up (that standalone.appexec'd wine, so its tile read "wine"). Manual games launch from Silo (correctly named tile) or their ownwinecfg; no standalone launcher.app. -
🔧 Phase 2 — default Wine config for the Steam bottle (2026-07-10,
main;swift buildclean + zero warnings, 364 tests green serial + parallel). A vanillawinebootprefix carries noHKCU\Software\Wine\DllOverrides, but games expect the standard Windows-compatibility set. Silo now applies its own 58-entry default override set (the classic Wine default template) to the Steam bottle.- New
Silo.defaultDllOverrides(Sources/SiloKit/Steam/BottleDefaults.swift) +SteamBottle.applyWineDefaults: builds a REGEDIT4.regand imports it with ONEwine regedit /S(cheaper than 58reg adds), idempotent (.silo-installed/wine-defaultsmarker). Called insetUpright afterprovision("Configuring the bottle…"). - Removed Silo's
d3dcompiler_47=nativeoverride (kept the DLL file) — the native DLLs (d3dcompiler_47 4.3 MB, msvcp140 643 KB, vcruntime140 179 KB) are present, so Wine's load order picks them up with NO override. Dropped the now-deadsetDllOverridehelper. - MSVC unchanged — Phase 1 already installs the redist without overrides. The redist places the real
msvcp140.dllon this Wine (bug-57518 doesn't bite cx-26.x), so the winetricks force-native workaround (+ risky CAB-extract) is not needed. - Tests (+2): a completeness pin (the 58-entry set, and NOT msvcp140/vcruntime140/d3dcompiler_47/concrt140),
the
regeditimport + idempotency,installD3DCompiler47now asserts NO override. - On-device: after a fresh setUp, confirm winecfg → Libraries shows the override set,
d3dcompiler_47no longer appears, andsystem32/msvcp140.dllis the real 643 KB file.
- New
-
📦 Phase 1 — bottle provisioning + 2-step onboarding (2026-07-10,
main;swift buildclean + zero warnings,swift testgreen serial + parallel, 360 tests). The Steam bottle now installs its game-dependency component set in a fixed order, with the license-bearing pieces run as user-guided GUI installers (ProcessRunning.runblocks until the user closes the window). Onboarding collapses from 3 steps to 2: (1) import GPTK.dmg, (2) "Set up" →AppEnvironment.runFullSetup()chains it all.- Ordered component model.
BottleComponentenum (allCases= the single source of truth for order) + per-componentisSatisfied/installonSteamBottle, driven byprovisionComponents(wine:onPhase:)— satisfied components are skipped (resumable/idempotent), best-effort per component except the terminal Steam install. Order: Core Fonts → Source Han Sans → d3dcompiler_47 → MSVC x86 → MSVC x64 → msync → Steam.SteamBottleViewModel.setUp()now: download Steam →wineboot→provisionComponents→forceQuit(black-window guard) → warm-up → webhelper wrap. - Core Fonts (
installCoreFontsreworked): installed in the FIXEDSilo.coreFontsorder; the FIRST font runs its installer bare (shows the Microsoft EULA, blocks), the rest extract silently (/T /C /Q) — "user-guided initially then auto." Added a GitHub-mirror fallback URL (SourceForge is flaky). - Source Han Sans (new
installSourceHanSans): all 4 language packs (J/K/SC/TC, ~360 MB, OFL, no prompt) — download → bsdtar extract → copy.otfintowindows/Fonts; per-pack markers make the big download resumable. - d3dcompiler_47 (new
installD3DCompiler47): both ABIs, extracted from Microsoft's Windows-SDK CABs via Wine's builtinwine expand(no cabextract), 64-bit→system32/ 32-bit→syswow64(Phase 1 added a native override here; Phase 2 removed it — the file's presence is enough).⚠️ R2 (highest on-device risk): whetherwine expand -F:<member>pulls the named member on a real Mac — fallback is re-hosting the two redistributable DLLs as Silo release assets. - MSVC redist (new
installVCRedist): x86 then x64, user-guided (no/quiet→ license shown). msync is a no-op (env-only, always satisfied → skipped). - 🐛 On-device fix (2026-07-10): MSVC never showed its user-guided installer. Root cause:
winebootpre-populates system32/syswow64 with tiny fakedll stubs for Wine's builtins (incl.msvcp140.dll), so the "ismsvcp140.dllpresent?" marker read true on a fresh prefix and skipped the redist entirely — same latent bug ford3dcompiler_47. Fixed: MSVC now tracks a Silo marker (.silo-installed/ vcredist-{x86,x64}) written only on a success exit code (0/3010/1638; a cancel 1602 re-prompts), and d3dcompiler is size-gated (real DLL is multi-MB vs the ~KB stub). +2 tests pin it (a fakedll stub no longer satisfies either; a cancel re-prompts). 362 tests green. - Steam install is now user-guided (
runSteamInstaller(userGuided:)drops/S);installSteam(silent) kept for the CLI/tests.downloadSteamInstalleris a separate early step (fails fast on network; now creates the prefix since it runs beforewineboot). - Orchestrator
AppEnvironment.runFullSetup(): download Wine (if!wineReady, then await the default-persist so the DXMT match / setUp don't read a nil wine binary — R7) → download DXMT (if!dxmtReady) →steamBottleVM.setUp().setupBusydrives the onboarding spinner.OnboardingView→ 2StepRows;--setup-steamCLI drives the whole chain. - New constants in
Silo.swift(noversions.envchange): corefonts mirror, Source Han Sans base + packs, MSVCaka.msURLs, d3dcompiler CAB URLs + member ids. - Tests (+9): per-component (EULA-first fonts, 4-pack SHS + resume,
wine expandd3dcompiler + override, user-guided MSVC no-/quiet, user-guided Steam no-/S), the ordered-driver sequence + skip-satisfied, the reworkedsetUp(user-guided Steam +forceQuitbefore warm-up), the purecomponentStatusmapping, andrunFullSetupskip-when-ready delegation.createComponentMarkerstest helper added. - Pending on-device validation (a real Mac + Wine/GPTK; not gating the commit): R1 SteamSetup
auto-launch black-window (forceQuit mitigation); R2
wine expandmember extraction; R3 MSVC bug-57518 (manualmsvcp140.dll); R4 first-corefont bare EULA under Wine; R5 exact aka.ms redirect / SHS asset + OTF names; R6 MSVC DLL-override set.
- Ordered component model.
-
🧹 Phase 0 — removed the DXMT Steam bottle; collapsed to a SINGLE "Steam" bottle (2026-07-10,
main;swift buildclean + zero warnings,swift testgreen serial + parallel, 351 tests). First of a multi-phase restructure. The dual-Steam-bottle topology (a GPTKSteamBottle+ aSteamBottle-DXMT, each its own Steam install/login) is gone — there is now ONE shared Steam bottle, GPTK-only for now, with no GPTK/DXMT tags on it. DXMT the graphics backend stays (manual/non-Steam games still pick it; the DXMT runtime is still installed via Settings → DXMT). Removed/collapsed:AppPaths.steamBottle(_:)family → single no-argsteamBottle/…ClientDir/…Exe/…CEFDir/…Log;"SteamBottle-DXMT"dropped frombottleDirNames;log(forAppID:backend:)→log(forAppID:).SteamApp.IDcomposite(appID,backend)+SteamApp.backend→ plainid = appID; discovery no longer tags a bottle backend.GameID.steam(appID:backend:)→.steam(appID:).GameConfigun-keyed from backend (appID only; a legacybackendJSON key decodes-and-ignores — no data loss).AppEnvironment: the per-backendBackendServicesdict +services(for:)+dxmtBottleVM/dxmtClientSession/dxmtSteamReady/gptkSteamReady→ one inlinedsteamBottleVM/steamClientSession;steamReadyis the single gate.GameLibraryViewModel: droppeddxmtSession, the cross-bottle co-residency guards (activeSteamBackend/stopOtherSteamClients/activeBackend/runningBackend),steamInstalledBackendsset →steamInstalled: Bool, dual-bottle discovery + two-cards-per-title.busyGames: Set<Int>.SteamBottle/SteamClientSession/SteamBottleViewModel: dropped thebackendfield, the sibling-seed fast path (seedFromCompleteBottle), and the cross-backendSteamSetupGate+ "other bottle" wiring.- UI: the "Steam bottle (DXMT)" General-settings section, the whole DXMT-bottle onboarding step, and the
GPTK/DXMT backend tag on Steam cards are gone; "Steam bottle (GPTK)" → just "Steam bottle"; the
"Open Steam" toolbar is a plain button again.
DXMTManagerView(runtime tab) kept. CLI--setup-steamis no-arg. A 32-bit Steam game (GPTK is 64-bit-only) now says "not supported yet" instead of steering to the removed DXMT Steam bottle. - Decisions (user): the whole optional DXMT onboarding section removed (runtime still in Settings → DXMT);
an existing on-disk
SteamBottle-DXMTis left in place (Silo just stops using it — no auto-delete). - Tests: dual-bottle/cross-bottle/seed/setup-gate cases (which asserted now-removed behavior) deleted; per-backend config + discovery cases rewritten to single-bottle; fallback-message assertions updated.
-
✨ Library status line is now transient — auto-dismisses (2026-07-08,
main; shipped in 0.3.2). The bottom status bar set"Launched X."once and never cleared it, so it lingered long after the game closed (the game card's running indicator cleared correctly via kqueue; only the status text was sticky).setStatusnow schedules a self-clear (default 5s) and each new status cancels the prior message's timer, so a stale timer can never wipe a newer line (e.g. a graphics-fallback warning that legitimately replaced it). Scoped to the library bar; settings/manager panes keep their ownstatusMessage. +2 tests, 365 green. -
🔁 Second adversarial sweep (post-0.3.0) — 8 more real bugs, all fixed (2026-07-08,
main; serial + parallel green, 362 tests). Prompted by "are there no more remaining fixes?" — a fresh three-lens review (ledger/gates, launch/co-residency, setup/readiness) that INCLUDED the just-shipped crash-orphan code found bugs file-local review missed. Commits656f608→3ebcf11:- (HIGH) Durable ledger dropped an entry before confirmed death.
terminateAllSync/stop/clearremoved optimistically right after a bare async SIGTERM — on a clean quit where the game outlived the signal, the next launch's gate saw no survivor → move/update over a live wineserver. Now removed ONLY on confirmed death (kqueue exit) or self-prune (once the PID is actually gone). The exact false-negative the ledger exists to prevent. - (HIGH) Relocation vacuously "succeeded" when the current root was on an ejected drive → persisted the
new location + relaunched into an empty dir while the real bottles sat orphaned on the drive. Now refuses
until
bottlesRootReachable. - (MED-HIGH) The gate was blind to in-flight work:
isAnythingRunningignored the busy sets (a launch that claimed a bottle but hadn't spawned), andanythingRunningignored a bottle mid-setup/warm-up. Both now count; the warm-up download client is also recorded in the ledger (crash-during-setup). - (MED) Launches weren't blocked during a self-update (ends in
exit(0), no teardown → orphan) and a move + update could overlap (both relaunch).launchBlockedByBottlesnow refuses during an update; the gate is mutually exclusive with a move/update in flight. - (MED) openSteam/uninstall could race a cross-bottle
playinto TWO live Steam clients (one account → Steam logs one out).stop()no-op'd against a client caught mid-spawn; it now cancels the in-flight launch andstartSteamself-terminates if cancelled after the spawn. - (LOW-MED)
makeShortcutskipped the 32-bit-on-GPTK refusal → a shortcut that launches to a wined3d-fallback failure with no steer. Now refused likeplayManual. - (LOW) taskkill sibling-collision guard was case-sensitive but wine's
/IMisn't —Game.exevsgame.exeslipped through. Now case-folded. - Verified sound (no change): the
(pid, startTime)reuse-proofing, the seed exclude-list + setup-gate (the two earlier user-found bugs can't regress),ensureRunningcoalescing/readiness, ConfigStore recovery. Residual: a game that re-execs under a PID Silo never recorded still needs the on-device wineserver-lock probe (a Wine-verified handoff item).
- (HIGH) Durable ledger dropped an entry before confirmed death.
-
🏛️ Architecture-level review before onboarding users — 4 themes, ~15 bugs, all fixed (2026-07-07, branch
gptk-path-review; SERIAL + PARALLEL both green, 340 tests). Three lifecycle-scoped adversarial reviewers (launch/co-residency, setup/discovery/onboarding, relocation/update/persistence) found cross-subsystem bugs that file-local review missed. Commits62dde8f→093fbfc:- Theme A — co-residency was per-appID, must be per-backend.
play/openSteam/uninstallrefused only the SAME title cross-bottle, butstopOtherSteamClientstears down the other bottle's client — so launching a DIFFERENT game in the other bottle killed a running game's Steamworks. NowactiveSteamBackend(excluding:)refuses any cross-bottle Steam launch, checked+claimed before any await. - Theme B — liveness was in-memory only; gates were start-only. Per the user's call, quit (and
self-update relaunch) now TEARS DOWN games + Steam clients (
terminateAllOnQuit; retired the opt-in toggle), so nothing orphans and cross-session gates stay accurate. Update refused while anything runs; launches refused during a bottles move. Crash-orphan residual now closed (2026-07-08) byProcessLedger: a crash-durable (pid, start-time) shadow of every process Silo spawns into a bottle (games + Steam clients). The relocation/update gate (blockedForBottleWork) also refuses while a PRIOR run's process is still alive; (pid, start-time) identity makes a reused PID never falsely block; fail-open- self-pruning; the durable probe runs only at the action gate, never a SwiftUI body. Remaining residual: a game that re-execs under a PID Silo never recorded (a wineserver-lock probe — needs Wine to verify).
- Theme C — "ready" was defined 3 ways; gates picked the weakest. "Installed" now means the WARMED
client (
hasWarmedClient: steamui.dll + webhelper), not the bootstrapper; onboarding's GPTK step +setupCompletekey ongptkSteamReady(DXMT-first can't mark GPTK done); removing a runtime reconcilesBackendConfig(no sticky readiness against a deleted runtime);refreshLibraryIfReadyre-syncs both ways. - Theme D — ejected relocated drive now shows a distinct
BottlesDisconnectedView(not first-run onboarding); launches +setUprefuse when the root is unreachable (no phantom bottle on the boot disk). - Bonus:
BottlesRelocationCoordinatoruses the injectable app-bundle resolver, so relocation'srelaunch/exit(0)no longer kills the SERIAL test run — the whole suite passes serially for the first time (the parallelteehad been masking failures). +co-residency/relocation/warmed-client tests. - Sweep leftovers cleared (2026-07-08):
taskkill /IMbasename collision FIXED (stopdrops to a SIGTERM-only stop when a co-resident sibling shares the exe basename, else fires /IM); the crash-orphan residual FIXED (ProcessLedger, see Theme B);terminateAllOnQuitcomposition now has a dedicated test. Reviewed + consciously left:strand-on-failed-deleteis already surfaced (removeManual / discardManualBottle show a Finder path);isSharedSystemAppis a documented LastOwner heuristic with no better single-manifest signal;bottlesDisconnectedshort-circuits to zero I/O in the default location and must stay live to detect drive ejection.
- Theme A — co-residency was per-appID, must be per-backend.
-
🐛 Adversarial correctness pass — 10 bugs fixed (2026-07-07,
swift buildclean +Scripts/test.shgreen, +2 tests). Two independent adversarial reviewers swept the GPTK bottle path for BUGS (not just rot). The GPTK core came back clean; the fixes (most-severe first):- Steam readiness race (GPTK, the one important core bug).
SteamClientSession.ensureRunningchecked thesteamPIDfast path before joining an in-flight launch — so a 2nd Play during Steam cold-start returned "ready" before Steam had registered itsActiveProcesspid, and the game'sSteamAPI_Initcould lose to Steam's init. Now joins the in-flight launch (which owns the readiness wait) FIRST. - DXMT manual-game shortcuts never seeded
winemetal.dllinto the prefix.makeShortcutcalledmakePlandirectly, bypassinglinkGraphics/installDXMTPrefixLoaders; a shortcut made before any in-Silo launch produced a.appthat fell back to wined3d and failed. NewLaunchOrchestrator.prepareGraphics(launch-free graphics prep);makeShortcutnow calls it. Test added. - Clone race + interrupted-clone reuse.
RuntimeVariants.ensureClonewas check-then-act; two quick first-time DXMT launches could make the loser's copy-fallback hit EEXIST, and a hard-killed mid-clone left a partial tree later reused. Now clones into a.cloning-<uuid>staging dir published by atomic rename (loser reuses the winner's; a partial never becomes the clone). - GPTK importer leaked a mount when
hdiutil attachsucceeded but the plist had no mount-point (the caller never received a URL to detach).attachnow best-effort detaches the parseddev-entry. Test added. overlayGPTKpartial-overlay masquerade. A mid-copy failure could leave a freshd3d11.dll(the witness) beside stale siblings, so the next launch's witness check wrongly skipped. Copy the witness LAST.- Webhelper wrap could strand a CEF dir (real webhelper preserved as
_orig, nosteamwebhelper.exe) on a mid-op I/O failure → black login, no self-heal. Now stage-then-rename (byte copy first, swap by rename). - Stale
errnoin the clone copy-fallback error message — now the underlying POSIX code, captured at the failure point. RuntimeManager.installreinstall was non-atomic — extracted in place and removeddeston a mid-extract failure (nuking an existing good install; also merged stale files on reinstall). Now extracts into a.extracting-<uuid>staging dir and publishes with an atomic rename.stripBundledSDLonly searchedlib/silo-bundled— a custom-repo runtime bundling libSDL2 elsewhere kept the winebus/SDL crash. Now walks the whole runtime tree.GraphicsFallbackMonitorarmed a kqueue watch even when the pre-check already fired — now returns before arming, so no fd lingers. Ruled out after tracing:stopGame's base-wine taskkill (correct — wineserver is prefix-keyed) and the double-overlay (harmless idempotent no-op). Left as-is (documented): the readiness kqueue watch onuser.regcan miss an atomic rename-replace and fall back to the bounded 20 s failsafe — but it's on-device-validated as event-driven, degrades gracefully, and the only fix touches the sharedFileWatchthe log tailer also uses (wide blast radius for a case the evidence says doesn't occur).
- Steam readiness race (GPTK, the one important core bug).
-
🧹 GPTK-path quality pass — 5 review findings fixed (2026-07-07,
swift buildclean +Scripts/test.shgreen, EXIT=0/"All tests passed"). A focused audit of the GPTK bottle path (deterministic core, launch pipeline, runtime pieces) found it largely clean; five items closed:- Dead pre-dual-bottle shims removed. All five no-arg
AppPaths.steamBottle*convenience vars (steamBottle/ClientDir/Exe/CEFDir/Log) dropped from the shipping type — four were unused in Sources, the one live caller (GeneralSettingsView) now passes.gptk; the four the test suite uses moved toTests/SiloKitTests/Support/AppPaths+TestSupport.swift. Also removed the deadRuntimeVariants.variantWine(superseded byprepare/ensureClone; test-only) + itscloneWinehelper and the now-orphaned test. ManualGame.gameConfig— theGameConfig(appID: 0, …)mapping was open-coded in two places (LaunchOrchestrator.launchManualGame,GameLibraryViewModel.makeShortcut); now one computed property.- Resolver output threaded explicitly.
makePlan/launchInBottle/launchManualGamegained an optionalwine:param (defaults tobackend.wineBinaryPath), so the VM feeds the resolved variant runtime directly instead of mutating aBackendConfigcopy at three call sites;linkGraphicstakes the wine explicitly too. Backward-compatible — every existing makePlan/pipeline test unchanged. - Test-only PID projections retired.
GameLibraryViewModel.runningPIDs/manualRunningPIDs(dictionary reshaping that existed only for the test suite) replaced by narrowpid(for:)accessors mirroringisRunning; the ~8 test sites migrated. - Doc fix:
WineRuntimeLayout's no-argwindowsModulesDir/unixModulesDirwere mislabeled "back-compat" — they're the live GPTK-x86_64-only overlay path (overlayGPTKuses them;overlayDXMTpasses an explicitWineArch) — relabeled.
- Dead pre-dual-bottle shims removed. All five no-arg
-
🖥️ High Resolution Mode: pair LogPixels with RetinaMode (2026-07-07, on-device validated). The Retina toggle wrote only
HKCU\Software\Wine\Mac Driver\RetinaMode, so turning it on made game/UI text render tiny (Wine renders at native backing pixels with no DPI compensation). That's the missing half of what CrossOver calls "High Resolution Mode" — it reports 192 DPI alongside RetinaMode so the UI scales up to match.WineTools.setRetinaModenow writes the coupled pair:RetinaMode(y/n) andHKCU\Control Panel\Desktop\LogPixels(192/96), so retina is never tiny and the two can't drift; LogPixels is only ever written here (192 DPI on a non-retina bottle would just bloat the UI). Validated on-device in the DXMT bottle: Overcooked 2 runs on DXMT (feature level 11_1) crisp + legible. Two findings recorded: (1) second monitor stays live in fullscreen because Silo never enablesCaptureDisplaysForFullscreen(Wine's default-off = non-capturing borderless fullscreen; capture=y is what blanks other displays). (2) games must be launched via Silo, not the co-resident Steam client's Play button — Steam runs on the BASE runtime with no DXMT override, so a game it spawns falls to wined3d → "None of the requested D3D feature levels" →InitializeEngineGraphics failed(see thesilo-steam-launch-gotchanote). -
🔎 Dependency + per-runtime audit vs AppleGamingWiki (2026-07-06) — two gaps closed, on-device validated.
- Core fonts (dependency gap). Wine ships no MS TrueType fonts (the bottle's
windows/Fontswas empty), which the Wine-Steam community flags as blank/garbled text in the client UI + games.setUp()now installs Microsoft's redistributable "core fonts for the web" (the winetrickscorefontsset,Silo.coreFonts) into BOTH bottles — downloaded from SourceForge's canonical mirror and extracted with Wine's own IExpress/T /C /Q(no cabextract/winetricks dependency; validated on-device → 25 fonts: Arial/Times/Courier/Verdana/Georgia/Comic/Impact/Andale/Trebuchet/Webdings). Idempotent (SteamBottle.hasCoreFontsmarker), best-effort per font, cleans up after itself. - MetalFX was GPTK-only (per-runtime gap). The per-game MetalFX toggle always emitted
D3DM_ENABLE_METALFX— a no-op for a DXMT game.EnvFlags.environment(graphics:)is now backend-aware: GPTK →D3DM_ENABLE_METALFX, DXMT →DXMT_METALFX_SPATIAL_SWAPCHAIN. DXR stays GPTK-only (DXMT has no DX12/raytracing).makePlanpasses the launch backend through. - Verified already-correct: GPTK env set complete (
ROSETTA_ADVERTISE_AVX,MTL_HUD_ENABLED, DXR);WINEMSYNC(deliberately msync, not the wiki's esync — required for shared-bottle co-residency); per-runtime DLL overrides + D3DMetal-framework-in-DYLD (GPTK only) + cloned runtimes; Windows 10 both bottles; DXVK/VKD3D/Vulkan correctly N/A (Metal-direct); vcrun covered by Wine builtins + Steam's per-game installers. Onboarding already lean — the 3 steps map to 3 genuinely-required user actions (GPTK needs a manual Apple.dmg, can't be automated); no meaningful simplification available.
- Core fonts (dependency gap). Wine ships no MS TrueType fonts (the bottle's
-
🩹 Steam-bottle warm-up: fold the first-run self-update into setup (2026-07-06, on-device validated). Problem: after setup, the user's first Steam launch hit "failed to load steamui.dll", the second was a black login window, and only the THIRD reached login. Root cause:
SteamSetup.exe /Sinstalls only the ~2 MB bootstrapper; the real client (steamui.dll + CEF/steamwebhelper) self-downloads on first run, and the webhelper wrap races that download. Fix:SteamBottleViewModel.setUp()now runs a one-time warm-up (SteamClientSession.warmUpUpdate) that completes the download BEFORE the user's first launch, then wraps the webhelper against the now-existing CEF dir. Works for BOTH GPTK + DXMT bottles (setUp is shared), non-invasive (rootlesssteam.exe, no window pops up), with a real progress bar (parsed from Steam'sDownloading update (X of Y KB)log). Validated on-device against the fresh DXMT bottle: 7.1 MB bootstrapper → 1.0 GB fully-installed client (steamui.dll + wrapped webhelper), no rollback, no leftover processes. The debugging took several real runs; each found a concrete bug (recorded so the hard-won knowledge isn't lost):-silentmakes Steam start minimized and SKIP the first-run client download → dropped it (launch baresteam.exe, rootless).- "steamui.dll exists" fires MID-download (Steam extracts it early); shutting down then makes Steam roll the half-applied update all the way back. The reliable "done" signal is Steam's own "Update complete" log marker (a single launch does the whole download→install→commit).
- Wine spams thousands of
msync_init Failedlines that pushed Steam's progress lines out of any log tail → parse the WHOLE log (SteamBottle.updateState). - The bottle log lives OUTSIDE the client dir and persists across setups, so a stale "Update complete"
fired completion instantly →
SteamBottle.resetLog()at warm-up start. --setup-steam <gptk|dxmt>CLI harness added (like--import-gptk) to run setup headlessly on-device.- Pending: on-device confirm the DXMT bottle's first real launch now lands on login (the whole point);
commit is on
dxmt-dualbottle-fixes. Corefonts install (recommended follow-up) not yet done.
-
🔧 Six dual-backend UX/correctness fixes (2026-07-06, branch
dxmt-dualbottle-fixes, 5 commits, eachswift buildclean +Scripts/test.shgreen; app assembles + smoke ok). Reported from on-device use of the dual-bottle build. Note on the test gate: the parallelScripts/test.shteecan drop both✔/✘lines cosmetically, so each commit was ALSO verified with a fullswift test --no-parallelrun — the ONLY serial failure is the pre-existing, environment-dependentinstallUpdateNoBundle(runningAppBundle()resolves differently under--no-parallel; fails identically onmain, unrelated to this work). Commits:- A — runtime listings exclude DXMT variant clones;
remove()cascades. The DXMT variant runtime is an APFS clone<base>-dxmtcreated as a SIBLING inRuntimes/byRuntimeVariants; it carries both a wine binary AND the overlaid DXMT modules, so it surfaced in BOTHinstalledWines()andinstalledDXMT()(cross-listing the Wine + DXMT panes).RuntimeVariantsnow owns the ONE clone-naming source of truth (cloneName(ofBase:backend:)+isVariantClone, onrawValuenotbadge); both listings skip clones; a realdxmt-*-cx*tag is never flagged.remove(name:)cascades to the base's derived clone (dead weight once its base is gone —ensureClonekeeps an existing clone forever). Decision:setDefaultdoes NOT touch clones (re-derived per launch byBottleResolver). - B — honest, backend-aware graphics-failure message. The old "running on fallback graphics
(wined3d)" implied Silo has a working fallback; it doesn't (the wined3d fallback is inside GPTK's own
d3d11.dll and, for Overcooked-class titles, then fails device creation) and Silo deliberately has NO
rerouting (deterministic backend⇔bottle rule). New pure, table-tested
graphicsFallbackMessage(name:backend:isSteamGame:dxmtAvailable:): a GPTK title is pointed at DXMT, adapting to whether the DXMT bottle/runtime is set up (read at detection time); a DXMT title admits the wined3d fallback likely failed → Settings → DXMT.GraphicsFallbackdoc comments corrected. Detection- the pure GPTK→D3DMetal launch path untouched.
- C — a title installed in BOTH bottles surfaces as two cards.
load()deduped by appID (first wins), hiding the second bottle's copy. Identity is now (appID, backend):SteamApp.idis a computed compositeID{appID,backend}(no persistence change) andGameID.steam(appID:backend:)carries the backend, so both cards render (each with its GPTK/DXMTBackendTag) and tracking/stop/monitors are per-copy.play()gains a cross-bottle guard BEFOREstopOtherSteamClients(never kill the running game's co-resident Steam; one account can't be in-game twice → explanatory status). Busy/spinner is per-COPY (busyGames: Set<SteamApp.ID>) so only the launching card's button spins — an earlier appID-keyed set made BOTH cards spin during a launch (fixed 2026-07-06); cross-bottle launch protection is separate viaactiveBackend(ofAppID:)(running OR mid-launch), so blocking the other copy no longer requires marking it busy.uninstall()routessteam://uninstallthrough the game's OWN backend session (the DXMT copy must reach the DXMT bottle's Steam). Per-game config/settings/log stay appID-keyed (shared; the copies can never run at once). Known, out of scope (noted for a follow-up): launching a different game in the other bottle still stops the first bottle's Steam client under a running game — pre-existing, orthogonal to this fix. - D — both Steam bottles in Settings → General ("Steam bottle (GPTK)" then "Steam bottle (DXMT)"), moved out of the DXMT tab. The DXMT tab is now runtime-only.
- E — ONE runtime-install flow for Wine + DXMT (kills the onboarding/settings duplication). A
RuntimeKindstrategy (.wine/.dxmt: noun, download hint, release picker, installed-list, install fn) parameterizes a singleRuntimeViewModel; a newRuntimeInstallvalue is the common shape ofWineInstall/DXMTInstallfor the VM + a sharedRuntimeInstalledSectionlist row.AppEnvironmentgainsdxmtRuntime(matched to the configured wine at click time), wires its default toapplyDXMTLibDir, seeds it inbootstrap;downloadLatestDXMT/dxmtDownloadingdeleted. The DXMT tab now mirrors the Wine tab (install latest / import folder / installed list with Set default + Remove); onboarding's DXMT step + status chain usedxmtRuntimewith Wine's string templates. Decision: DXMT adopts-as-default only when none is set (Wine semantics, was always-adopt); first install still flipsdxmtReadyviaonDefaultChanged. The convenienceRuntimeViewModel(manager:repo:)= Wine kind, keeping every existing call site + test valid. - Pending (needs a human / on-device): a
Scripts/dev.shvisual pass (both bottle sections in General; DXMT tab mirrors Wine; two cards for a dual-installed title; the honest Overcooked-2 message); mergedxmt-dualbottle-fixes→main.
- A — runtime listings exclude DXMT variant clones;
-
🧹 Full-project cleanup COMPLETE (2026-07-02, 3 tiers — robustness, dedupe, structure; 12 phases, each landed green). Branch
dxmt-dual-bottle-backendmerged tomain(ff); all phases onmain. Final verification:swift buildzero warnings, 305 tests green,Scripts/build-app.shassembles + ad-hoc-signsdist/Silo.app,SILO_SMOKE=1run passes,git statusclean. Remaining (needs a human/on-device): aScripts/dev.shvisual pass over the deduped views (library grid, both settings sheets, both bottle sections) + the nextbuild-wineCI dispatch exercises the shared wrapper-check script. Phase log:- Phase 0:
.dxmt-build/+.dxmt-build-fullrun.loggitignored (670 MB build artifacts). - Phase 1:
ConfigStorerecovery copy — every save refreshesconfig.json.bak; a present-but-corrupt primary restores the last good save (and self-heals the primary) instead of silently wiping all state. A missing primary still resets (deliberate). - Phase 2: swallowed errors surfaced.
DiscoveryEnginedistinguishes an unreadable primary library (libraryUnreadable, thrown) from the benign no-library-yet (steamDirNotFound, silently skipped); the library shows a per-bottle failure status (or.errorwhen nothing else can show — LibraryGridView's error case is now reachable).GameSettingsViewModel.save() -> Bool+errorMessage(sheet only dismisses on success). A failedlastPlayedwrite after launch says the config is unwritable.deleteBottlefailures say "remove it in Finder: ".resolveMessagemaps the whole launch stack (exe-not-found / wineboot / DXMT-clone / linker-source-missing) to actionable text and all catch-sites route through it. 282 tests green. - Phase 3: correctness fixes. (1) Manual-game shortcuts route through
BottleResolver(GameLibraryViewModel.makeShortcut, replacesAppEnvironment.makeManualGameShortcut) — a DXMT game's Desktop.appnow snapshots the DXMT variant runtime + overrides instead of silently using the base/GPTK env; failures surface in the status bar. (2) Bottle tools (setSteamBottleRetina/openWineTool) take aGraphicsBackend(default.gptk) so the DXMT bottle can get Retina/winecfg (UI row lands with Phase 7's shared component). (3)GameAppShortcutwrites atomically. (4)deQuarantinereturns aHardeningOutcome;RuntimeManager.lastHardeningIssue+ GPTK-importonWarningsurface a failed de-quarantine/re-sign at install time ("Gatekeeper may refuse…") instead of a cryptic launch failure (newLockedBoxMutex util). (5)LogTailer.startcreates/reads the log OFF the main actor (generation-guarded against stale arms). 287 tests green. - Phase 4: no more sync disk probes in SwiftUI body evaluation (bottles can live on a
slow/disconnected external volume).
GameLibraryViewModel.steamInstalledBackends= off-main cache (probed byrefreshSteamInstalled(), called by everyload());steamReady/steamInstalled(_:)AppEnvironment.dxmtSteamReadyread it.SteamBottleViewModel.steamInstalledcached the same way (refreshInstalled()at bootstrap;setUpsets it). Invalidation wiring:onSteamInstalledfires after a fresh install → AppEnvironment reloads the library, so the onboarding gate flips without a relaunch (pinned by a wiring test — a missed invalidation would stall onboarding). 290 tests green.
- Phase 5: the co-residency sync rule (
WINEMSYNC=1, stripWINEESYNC) now lives in ONE place —Silo.enforceMsync/msyncWineEnvironment— adopted by all five sites that each rebuilt it (makePlan,stopGame,runWineTool,WineTools.environment,SteamBottle.steamEnvironment). Zero behavior change (pinned by the exact env assertions across those suites). 292 tests green. - Phase 6:
GraphicsLinkermechanics dedupe —isGPTKModule/isDXMTModuleparameterize oneisOverlayModule(_:prefixes:); the witness idempotency check and the per-dll+.socopy loop are shared (witnessMatches,copyModules). SEMANTICS untouched: GPTK keeps its exact choreography (pre-witness framework-link self-repair → copy → re-link; nowineWinDircreation), DXMT keeps its dir creation. Pinned by the idempotency/self-repair/symlink suites + new direct helper tests. 294 tests green. - Phase 7: view dedupe.
GameTileCard= the one library-tile chrome (artwork band, three-state Play/Launching/Stop button, menu, hover treatment) — Steam + manual tiles now inject only their artwork/subtitle/menu/confirmations.PerformanceFlagsSection+LaunchOptionsSectionshared by both settings sheets (manual sheet gains the guidance footers).SteamBottleControls= the one Setup/Launch/Reset-login/log block for the GPTK + DXMT settings sections; the DXMT section gains a Repair row (winecfg/regedit/control on the DXMT bottle, the Phase-3 backend-aware tools) and the Retina toggle now writes the registry key into EVERY installed bottle (one preference, both bottles consistent). No view tests per repo convention; logic unchanged (VM suites). 294 green. - Phase 8: scripts dedupe.
Scripts/check-webhelper-wrapper.py= the ONE load-bearing CEF-flag guard (was duplicated betweenbuild-wine.shandbuild-wine.yml— a drift there ships a broken Steam login); verified on synthetic pass/fail PEs.Scripts/bootstrap-x86-brew.sh= the shared Rosetta + x86_64-Homebrew bootstrap forbuild-wine.sh+build-dxmt.sh.bash -n+ YAML-parse clean; CI proper validates on the next workflow dispatch. Toolchain pins untouched. - Phase 9:
GameProcessCoordinator— the live-process bookkeeping (PIDs, kqueue exit observers, graphics-fallback monitors) moved out ofGameLibraryViewModelinto SINGLE tables keyed byGameID{.steam(appID)|.manual(uuid)}, replacing the four parallel Steam/manual dictionaries + 8 observe/exit/clear/watch methods. The VM's public API (isRunning/isBusy/isAnythingRunning/terminateAllSync) is unchanged (zero view edits);runningPIDs/manualRunningPIDsremain as computed projections so the 507-line VM test suite passed UNMODIFIED. New coordinator tests pin the pid-match stale-exit guard, re-track cancellation, clear-stops-monitor, and exact-PID terminate. 300 tests green. - Phase 10 (a/b/c): AppEnvironment decomposition, three green commits. (a)
BackendServices= one keyed bundle (bottle + client session + settings VM) perGraphicsBackend, built in a loop (killing the gptk/dxmt construction copy-paste); pre-bundle names kept as computed forwards so views/tests were untouched. Bonus fix:anythingRunningnow checks EVERY backend's session — a live DXMT Steam client blocks bottle relocation like the GPTK one. (b)UpdateCoordinator(env.updates) owns the inline self-update flow + its state. (c)BottlesRelocationCoordinator(env.bottles, in Provisioning/ next toBottleRelocator) owns the move flow; relocation-via-relaunch design unchanged;isBlockedlate-bound toenv.anythingRunning. AppEnvironment is now ≈300 lines of composition + thin orchestration. 301 tests green. - Phase 11:
RuntimeVariantsdirect tests (the one real coverage gap): GPTK prepares in place (no clone), DXMT clones to<root>-dxmt+ overlays the CLONE only, an existing clone survives re-prepare (idempotency — a re-clone would wipe in-clone state),variantWineis pure path math. 305 tests green.
- Phase 0:
-
🩹 Two follow-up fixes (2026-07-05):
- Runtime install no longer ad-hoc re-signs. The install hardening ran
codesign --force --sign - --deep <runtime-dir>, which ALWAYS failed (bundle format unrecognized— a runtime root is a plainbin/lib/sharetree, not a bundle) and surfaced a scary "couldn't re-sign… Gatekeeper may refuse" warning after the cleanup made hardening report its result. Re-signing is also unnecessary: the runtimes are x86_64 (run unsigned under Rosetta) and GPTK's D3DMetal must keep Apple's signature. Removed the whole re-sign path (reSignparam, the codesign branch,HardeningOutcome.signed,RuntimeManager.harden);deQuarantinenow only stripscom.apple.quarantine(the load-bearing step). Warnings now fire only on a genuine de-quarantine failure. Scripts/test.shnow fails when tests fail.swift testunder the CLT framework-search-path invocation printed Swift Testing failures but exited 0 on a full-suite run (verified Swift 6.3.3) — andrelease.ymlgates publishing on this script, so CI could ship a broken build. test.sh now tees output and exits non-zero if any✘failure line appears OR swift test itself errors. Verified: clean → exit 0, deliberate failure → exit 1.
- Runtime install no longer ad-hoc re-signs. The install hardening ran
-
🪟 Settings UX pass (2026-07-05): DXMT is now its own runtime tab (
DXMTManagerView) alongside Wine + GPTK — Settings tabs are General · Wine · GPTK · DXMT. The whole DXMT concern (runtime download/import + its Steam bottle + repair tools) moved out of the General tab into the DXMT tab, so General is just the primary Steam bottle, bottle tools, bottle location, and updates. Trimmed explanatory captions across onboarding + settings (kept labels, status/error messages, and warnings) to cut visual clutter. 305 tests still green; app assembles + smoke ok. -
🧩 DXMT as a second graphics backend — dual-bottle feature built end-to-end (2026-06-30, 267 tests green). Reverses the GPTK-only stance (and M87's DXVK removal) per the user's design;
CLAUDE.md"Graphics backends" rewritten to match. Branchdxmt-dual-bottle-backend. Done + green:- Deterministic core (backend ⇔ runtime ⇔ bottle):
GraphicsBackend{gptk,dxmt}= single source of truth (per-backenddllOverrides/overlaysExternalFramework);makePlanemits exactly one backend's builtin set (determinism test: DXMT never leaks GPTK's).GraphicsLinker.overlayDXMT.RuntimeVariants(GPTK in place; DXMT = APFS clonefile clone + overlay) +BottleResolver(the one(game,backend) → {prefix,wineBinary,graphics}dispatch; refuses an unconfigured secondary backend). - Models:
ManualGame.backend(tolerant decode) +SteamApp.backend(discovery-derived). - Manual games:
playManual→ resolver → a DXMT manual game runs on its cloned DXMT runtime in its own bottle. Backend picker in Add-a-Game + settings. - Two Steam bottles:
AppPaths.steamBottle(_:)→SteamBottle(GPTK) /SteamBottle-DXMT.SteamBottle+SteamClientSessionare backend-aware;AppEnvironmentruns a GPTK + a DXMT bottle/ session.play/stop/openWinecfgroute bygame.backend(DXMT Steam game → DXMT bottle on/wine-dxmt, only that bottle's client online). Discovery scans BOTH bottles, tags each game. Steam clients run on base wine (CEF; the co-resident game picks the variant — shared wineserver). No login sync (machine tokens are per-prefix → sign into each bottle once, by design). - UI: per-card backend tag on EVERY library card (Steam + manual); onboarding "Older games (DXMT) —
optional" section + a General-settings DXMT section.
GraphicsFallbackbackend-aware. - DXMT runtime delivery: auto-download from Silo's Releases (
AppEnvironment.downloadLatestDXMT→RuntimeManager.installDXMT, reusing the Wine downloader engine — SHA-256 verify + extract + de-quarantine/ad-hoc-sign) OR manual folder import. One-click "Download…" in onboarding + Settings. - Decision: GPTK keeps the existing
SteamBottledir (no migration of the multi-GB prefix); DXMT is a sibling. Dropped the plan'sSteamBottle-GPTKrename +SteamLoginSync. - DXMT build — BUILDS on-device (macOS 26 Tahoe + Xcode 26.6, 2026-06-30):
Scripts/build-dxmt.sh(local) +.github/workflows/build-dxmt.yml(CI) build DXMT v0.72 from upstream3Shain/dxmt(the version CrossOver 26 bundles) against the publishedwine-cx-*CrossOver Wine, via DXMT's canonical Meson build, x86_64 to match the Wine. Fullmeson compilesucceeds;dxmt.tar.xz(6.5 MB) shipsx86_64-windows/{d3d11,dxgi,d3d10core,winemetal}.dll+x86_64-unix/winemetal.so(all builtin) — the exact treeimportDXMTRuntime/overlayDXMTexpect. Pins inversions.env. Real bugs fixed while validating:- Toolchain: llvm-mingw (clang) is REQUIRED — v0.72 doesn't compile with Homebrew GCC-mingw (tested:
std::setfill/libc++ deps). It's DXMT's own pinned, intended toolchain. - Native clang: pin
/usr/bin/clang -arch x86_64via a meson native file — llvm-mingw/llvm@15 both ship a bareclangthat shadowed the Apple clang →ld: library 'System' not found. - Metal: Xcode 26 ships
metalbut its toolchain is a separate ~688 MB component; fetch it + probe an actual compile (a-f metalcheck is insufficient). - Layout:
-Dwine_builtin_dll=true(v0.72 defaults false → d3d in system32); package thex86_64-windows+x86_64-unixsibling dirs.
- Toolchain: llvm-mingw (clang) is REQUIRED — v0.72 doesn't compile with Homebrew GCC-mingw (tested:
- Decision: GPTK keeps the existing
SteamBottledir (no migration of the multi-GB prefix); DXMT is a sibling. Dropped the plan'sSteamBottle-GPTKrename +SteamLoginSync. - PENDING (final on-device): publish
dxmt-v0.72-cx26.2.0(build-dxmt chained off wine-autoupdate, orgh release), Download it in Silo → Settings → DXMT, then confirm DXMT renders Overcooked 2.
- Deterministic core (backend ⇔ runtime ⇔ bottle):
-
✨ Tier-1 features from the Whisky study (2026-06-30, 239 tests green). Five features mined from Whisky (the closest analog launcher) + Apple's GPTK materials, each with tests:
- Retina/HiDPI toggle for the Steam bottle (
WineTools.setRetinaMode→HKCU\…\Mac Driver\RetinaMode; persisted inBackendConfig.retinaModewith a tolerant decoder so old config never wipes). Settings → General → "Bottle tools". The standard fix for wrong-sized game windows. - Wine repair tools (winecfg / regedit / Control Panel + "Reveal Bottle in Finder") — escape hatch to
fix a prefix by hand. Routes through the existing
LaunchOrchestrator.runWineTool, which gainedWINEMSYNC=1(shares the bottle's wineserver, no 2nd-server fork — also fixes the existing callers).WineToolsis now registry-only (no duplicate tool-launcher). - Structured launch-log header (
LaunchPlan.logHeader, pure): every launch log opens with the resolved exe/args/cwd/env (sorted), written before spawn → a black-window report is self-explanatory. - Opt-in kill-on-quit (Settings toggle, default off):
RootView'swillTerminatehook →GameLibraryViewModel.terminateAllSyncSIGTERMs only the games Silo launched, never the co-resident Steam client (test-verified). - PE icon extraction for manual games (
PEIcon, clean-room PE/.rsrc/.ico parser, bounds-checked): manual (non-Steam) games now show their.exe's real icon in the grid (parsed off-main, cached). Steam games keep cover-art. - Game-Mode
.appshortcut for manual games (GameAppShortcut): "Create Desktop Shortcut" writes a standalone.app(categorizedpublic.app-category.games→ macOS Game Mode) that execs wine directly with a snapshot of the real launch env. Steam-game shortcuts deferred (need co-resident orchestration). Verified earlier vs Whisky: itsWINEESYNC-under-msync quirk is GONE in GPTK 4 (NOT adopted); skipped DXVK/winetricks/custom-registry-UI/CLI per Silo's constraints.
- Retina/HiDPI toggle for the Steam bottle (
-
🟢 GPTK D3DMetal CONFIRMED working — it IS Silo's active graphics path (2026-06-30). Decisive on-device positive control: We Were Here (582500) launched co-resident under GPTK with verbose logging renders D3D11 through D3DMetal, proven by THREE independent signals (not a single-signal overclaim): (1)
d3d11.dll+dxgi.dllload asbuiltin(GPTK's overlaid DLLs, not the native wined3d redist copies); (2) its UnityPlayer.logreportsDirect3D 11.0 [level 11.1], adapter "AMD Compatibility Mode (ID=0x66af)" — D3DMetal's signature fake adapter (wined3d-on-MoltenVK would report "Apple M4 Pro"); (3) ZERO wined3d/Vulkan/dlopen/feature-level signatures across 405 verbose lines (wined3d ALWAYS printserr:winediag:…Using the Vulkan renderer— absent). So the M83 "Bloons renders" gate is vindicated, theGPTK-4.0_beta_1+wine-cx-26.2.0pairing works, and the dlopen-layer fix (linkD3DMetalFrameworksymlink, self-repairing) holds.- "How did wined3d slip in?" — it didn't. wined3d lives inside Apple's GPTK
d3d11.dll(built from wine d3d11 source + a D3DMetal backend + aunix_call_fallback). Silo is GPTK-only (DXVK removed M87) and never added a wined3d path. The fallback is GPTK's own, triggered only when a specific game's D3DMetal device-creation fails. - Overcooked! 2 is a GAME-SPECIFIC exception, not a global failure. Its
D3D11CreateDevicevia D3DMetal fails (opaque — inside closed GPTK;d3dm_print/os_log give nothing), so GPTK's d3d11 falls to its internal wined3d →None of the requested D3D feature levels is supported→ "failed to initialize graphics." We Were Here (same Unity/D3D11 family) succeeds, so this is Overcooked-specific. Prime lever: a different / non-beta GPTK version (user can supply other.dmgs) — the beta likely matters for this class. RULED OUT for the global path: arch, native-redist shadowing, Metal-unavailable, dlopen. Correction of my prior STATUS: "device creation STILL fails (UNRESOLVED, casts doubt on whether GPTK renders ANYTHING)" was overgeneralized from Overcooked alone and is now disproven. - Verbose wine logging for local builds (07c1c1e):
Silo.wineDebug=+loaddlllocally,-allunder CI (gated onSILO_QUIET_WINE, set bybuild-app.shonly when$CI).WINEDEBUG=-allhad been hiding the very fallbackfixme:winediagsignatures theGraphicsFallbackguardrail keys on — so the guardrail can now actually fire in dev. Shipped app stays silent automatically. - Guardrail (shipped, working):
GraphicsFallback+GraphicsFallbackMonitorsurface "GPTK didn't engage — fallback graphics" for the failing class instead of a silent "Launched". 224 tests green. - GPTK 4 best-practice investigation (2026-06-30, read Apple's docs + cloned
apple/game-porting-toolkit). Key correction to the premise: GPTK 4 is a NATIVE-Metal-porting toolkit (AI agent skills + Metal Shader Converter + metal-cpp + native samples; prereqs macOS 27 / Xcode 27). The Windows-game "evaluation environment" (the D3DMetal that Silo overlays) is positioned as a developer triage/eval tool, not a documented end-user runtime. The repo has ZERO Wine/D3DMetal launcher-integration guidance (grepped the whole tree) — the only launch-env vars Apple documents are Metal-level (MTL_HUD_ENABLED,MTL_HUD_LOG_ENABLED,MTL_CAPTURE_ENABLED). So there is no Apple reference implementation to "match" for Silo's overlay-into-CrossOver-wine approach; Silo's launch env already matches the de-facto launcher standard (WINEPREFIX iso, WINEMSYNC,ROSETTA_ADVERTISE_AVX=1default-on, DYLD→lib/external, builtin d3d overrides, the D3DMetal.framework symlink) and is proven working (We Were Here). Overcooked-class device-creation failures are D3DMetal's own feature/format limits (Apple'sdebugging-rendering-issuesskill flags Apple-GPU format gaps, e.g.DXGI_FORMAT_D24_UNORM_S8_UINTis not universally supported) — NOT a Silo implementation bug. Levers for that class: a different/non-beta GPTK 4 build, or per-game Unity graphics args (-force-feature-level-11-0/-force-d3d11-no-singlethreaded).
- "How did wined3d slip in?" — it didn't. wined3d lives inside Apple's GPTK
-
🏷️ Release v0.2.1 (2026-06-29). Patch over v0.2.0. (a) Adversarial multi-agent quality audit closed in four tiers — P0: readiness TOCTOU fixed (kqueue is edge-triggered; re-check after arming) + the M114 event-driven gate now tested live (
FileWatch+ readiness, previously never run withreadinessTimeout>0); P1:makePlanexhaustiveness gaps (WINEDLLOVERRIDES;-merge, perf-flag propagation) +BottleRelocatorfailure paths (rollback, non-writable dest) covered,play()now surfaces a Steam-couldn't-start failure instead of launching against a dead client; P2: stale docs fixed + dead public surface removed (installLocation,SteamBottle.isProvisioned,SteamStoreDetails.categories/.directXVersion) +KeyValuesParserdepth cap (no stack-overflow on hostile.acf) + manifest size guard; P3: PID maps encapsulated, denylist also stripsextra,..-escape guard on relative exe, store fetch viarequireHTTPS, newRuntimeHardening/Filesystemtests. 216 tests / 36 suites green; clean build. (b) GitHub Pages site (docs/, Velox-style) — landing page at mikaelhug.github.io/Silo. -
🏷️ Release v0.2.0 (2026-06-29). Minor bump from 0.1.1 via
versions.env. Highlights since 0.1.1: manual non-Steam .exe games (each in its own isolated bottle), redistributables hidden (LastOwner==0), relocatable bottles (move to another disk/external drive — % progress, exFAT guard), versions.env single-source-of-truth, fully event-driven (every sleep/poll removed; readiness via a kqueue watch on Steam'sActiveProcess), and a compact fixed-size Settings window. 202 tests / 32 suites green; clean build (no warnings). Code/runtime production-quality (0% idle CPU, ~50 MB, no leaks); remaining ship-to-others gaps are on-device validation + notarization (human-gated), not code. -
✅ M114 — removed every sleep/poll; readiness is now event-driven. No fixed waits anywhere:
- Cold-start 10s grace → gone.
SteamClientSessionnow resolves the instant the co-resident Steam is ready via a kqueue watch on the prefix'suser.regfor Steam'sActiveProcesspid (exactly what a game'sSteamAPI_Initreads) —SteamReadiness(pure parse, unit-tested) + the reusableFileWatch. A cold launch waits only as long as Steam actually takes, not a flat 10s. The one remainingTask.sleepis a bounded failsafe (readinessTimeout, default 20s) that only fires if the signal never arrives (so a wrong signal can't hang a launch) — it is NOT the mechanism. - Status auto-dismiss (6s) → gone: the status bar shows the last action until replaced (no timer).
- Update-check spinner floor (700ms) → gone: the spinner reflects the real check duration.
- Log-viewer throttle (150ms) → gone: replaced with timer-free per-main-actor-turn coalescing (still
event-driven, still coalesces bursts). Extracted
FileWatchtoSupport/(shared by the log tailer + the readiness watch). - 202 tests / 32 suites green; clean build (no warnings); app reassembled.
- Cold-start 10s grace → gone.
-
✅ M112/M113 — single source of truth for versions (
versions.env, Velox-style). The app version was hard-coded inSilo.swiftAND duplicated as a fallback inbuild-app.sh. Nowversions.env(repo root) is the ONLY place a version is edited —SILO_VERSION,SILO_GITHUB_REPO,CROSSOVER_VERSION(the CrossOver FOSS wine-build input).Scripts/gen-versions.shmirrors it into the committed (generated, DO-NOT-EDIT)Sources/SiloKit/Versions.swift(Versionsenum);Silo.version/updateRepo/wineReporead from it.build-app.shregenerates + sourcesversions.env(dropped the grep + hard-coded version fallback);build-wine.shdefaults its CrossOver version toCROSSOVER_VERSION. A unit test fails ifVersions.swiftdrifts fromversions.env(verified). M113: scrubbed coincidental version literals from update test fixtures (arbitrary "current"/decoy-release versions that happened to equal the live one) so the live version lives ONLY inversions.env+ its generated mirror. 197 tests / 31 suites green; clean build; app reassembled (the Info.plist version flows from the env). -
✅ M111 — bottle move now has a progress bar + refuses exFAT/FAT. Building on M109–M110:
- Progress bar.
BottleRelocatornow does a byte-counting recursive copy for cross-volume moves (same-volume stays an instant rename) — preserves symlinks (a Wine prefix is full of them), sums total bytes up front, and reports a throttled0...1fraction.AppEnvironment.bottlesProgressdrives a determinateProgressViewwith a % label in Settings → Bottles (indeterminate spinner until the first fraction). Rollback unchanged (sources removed only after every dir copies). - exFAT guard.
Filesystem.isFATFamily(viastatfsf_fstypename) —moveBottlesrefuses an exFAT/msdos/vfatdestination ("can't hold a Wine bottle, no symlink support — reformat as APFS / Mac OS Extended") rather than silently creating a broken prefix. Injectable check for tests. - 195 tests / 30 suites green; clean build (no warnings); app reassembled.
- Progress bar.
-
✅ M109–M110 — bottles are relocatable (move to another disk / external drive). App state (config/logs/runtimes) stays under Application Support, but the bottles (Steam + every manual game's) now live under a configurable
AppPaths.bottlesRoot(default = supportDir).BottlesLocationpersists the chosen root in a tiny file read SYNCHRONOUSLY at startup (AppPaths. standard), so every derived bottle path is correct from the first frame (M109).BottleRelocatordoes a validated old→new move (writable + not-occupied checks; cross-volume copy+delete; best-effort rollback so it never half-relocates) (M109).AppEnvironment.moveBottles(to:)/resetBottlesLocation(): refuse while anything's running, relocate off the main actor, persist, then relaunch to adopt the new root everywhere (AppPaths is injected by value).anythingRunninggate (M110).- UI: Settings → General → Bottles — shows the location (+ an "isn't reachable, is the drive
connected?" warning when a relocated drive is ejected), Move… (folder picker →
<chosen>/Silo Bottles), and Reset to Default; a spinner while moving (M110). - 193 tests / 30 suites green; clean build (no warnings); app reassembled.
-
✅ M107–M108 — each manual (non-Steam) game now runs in its OWN isolated bottle. Steam games still share the one Steam bottle (Steamworks needs co-residency), but manual games no longer do — each gets a private Wine prefix at
~/Library/Application Support/Silo/ManualBottles/<uuid>(own registry/drive_c/ winecfg), so they can't pollute each other or Steam.WinePrefixProvisioner(M107) = reusablewineboot --initfor any prefix;SteamBottledelegates to it (DRY).AppPaths.manualBottle(id).- VM (M108):
ensureManualBottle(idempotent boot), play/install/stop/winecfg usepaths.manualBottle(id),removeManualdeletes the bottle,discardManualBottlecleans up an unsaved draft. - UI: Add Game provisions the game's bottle (installer runs into it; a "Setting up…" spinner; Cancel discards a draft bottle). Manual settings sheet gains a Bottle section ("Run Installer in this bottle…", "Show bottle in Finder"); the tile's Wine Config opens the game's own bottle.
- 187 tests / 29 suites green; clean build (no warnings); app reassembled.
-
✅ M101–M105 — non-Steam (.exe) games + hide Steam's redistributables. Two core-app changes:
- Redistributables no longer surface as a game (M101). Root cause: discovery parsed every
appmanifest_*.acf; "Steamworks Common Redistributables" (228980) looks like a normal manifest. The principled signal (verified on-device): Steam auto-installs shared packages withLastOwner == 0, while user-owned games carry the owner's SteamID64.SteamApp.isSharedSystemApp+ a DiscoveryEngine filter — not a name match. (An exe-presence heuristic would wrongly drop real games like Split Fiction, whose exe is nested.) - Add non-Steam .exe games (M102–M105). New
ManualGamemodel persisted inconfig.json(backward-compatible tolerant decoder so a new key never wipes existing config);LaunchOrchestrator. launchManualGame+runInstaller(reusemakePlan, which lost its long-deadapp:param); a UUID-keyed manual run-state inGameLibraryViewModel(Steam path untouched); and the UI: an Add Game wizard (Run Installer → Choose .exe → Add),ManualGameTileView, and a manual settings sheet. Manual games launch in the shared bottle prefix under GPTK without needing Steam. - 184 tests / 28 suites green; clean build (no warnings); app reassembled. Commits M101–M105.
- Redistributables no longer surface as a game (M101). Root cause: discovery parsed every
-
✅ M100 — polished, stateful Updates UI in Settings → General. Replaced the bare version/button/text rows with one self-contained status row that morphs between states (a
Phaseenum → icon + tint + title + subtitle + action): Check Now shows an animated spinner (held for a ~700 ms minimum so the loading always reads as deliberate), then the result cross-fades in — a green ✓ "You're on the latest version" or an accent ↓ "Version X is available" with a prominent Update & Relaunch button; install progress (downloading/installing) and a ⚠ failed+Retry state share the same row. Smooth (.smooth(0.32)) animation +contentTransition(.opacity)on the text + a scale/opacity transition on the icon. Mirrors the Wine tab's "load → result surfaces" flow. Dropped the now-redundantAppEnvironment.updateMessage(the view derives all copy fromupdateCheck). 175 tests green; clean build. -
✅ M99 — code-rot sweep after the settings reshape (M94–M98). Audited every file the settings reshaping touched. Removed dead code:
PathPickerRow(the manual-paths picker, orphaned when the "Advanced (manual paths)" disclosure was dropped) andBackendSettingsViewModel.isConfigured(declared, never read). Fixed stale references: a user-visible onboarding string and a doc comment still said "Advanced → …" (now "Settings → General"); a leftover duplicate doc line called the Settings window "a sheet (Wine/GPTK paths)"; and "Wine Manager" / "GPTK Manager" doc mentions acrossRuntimeViewModel,BackendSettingsViewModel,BackendConfig,WineInstall(+ a test) now say "the Wine/GPTK settings tab". Dropped the stale "experimental" framing onSteamBottleViewModel. Verified all readiness flags, VM members, and picker helpers are still live. 175 tests green; clean build (no warnings). -
✅ M98 — dropped settings explanatory footers + "already latest" update message. Removed the descriptive footer
Textunder Steam bottle, Updates (General tab), Wine, and GPTK — the sections speak for themselves. Added an "already latest" confirmation to the app updater: a manual Check for Updates (or the bootstrap auto-check) now setsAppEnvironment.updateMessageto "You're on the latest version (X)" when current (nil when an update is available — the install button says it — or on offline), shown under the Check button. Mirrors the Wine tab's "already installed" message. 175 tests green; clean build (no warnings). -
✅ M97 — Wine "install latest" no-op when current + a manual update check. (1)
RuntimeViewModel. installLatestnow short-circuits when the newest published Wine is already installed — instead of re-downloading the ~250 MB build it reports "Latest Wine (X) is already installed" (and adopts it as default if none set). (2) Added a "Check for Updates" button to Settings → General → Updates (AppEnvironment.checkForUpdate+isCheckingForUpdate) so the user can re-check on demand, even though bootstrap still checks automatically. +2 tests → 175 / 28 suites green; clean build (no warnings). -
✅ M96 — Settings tabs restructured to General / GPTK / Wine. The Settings window now has three top-level tabs: General (the former Steam-bottle pane, with the app version + inline updater moved to a "Updates" section at the bottom —
GeneralSettingsView, renamed fromBackendSettingsView), GPTK (GPTKManagerView), and Wine (WineDownloadView). Removed the combined "Runtimes" tab + itsWineManagerViewwrapper (the GPTK/Wine segmented sub-tabs are now top-level tabs), and the standaloneUpdatesView(folded into General). 173 tests / 28 suites green; clean build (no warnings). -
✅ M95 — UI refinements (6 changes). (1) Renamed "Advanced Settings" → Settings and made it the standard macOS Settings window (app-menu "Settings…" / ⌘, via a
Settingsscene +openSettings; the Library toolbar gear now opens it). Tabs: Steam Bottle, Runtimes, Updates. (2) Removed the Status section (Ready-to-launch / Default Wine / Default GPTK) and (3) the "Advanced (manual paths)" disclosure + the now-vestigial Save button fromBackendSettingsView(it's just the Steam-bottle pane now). (4) Removed the experimental HW-accelerated Steam UI entirely (cefHardwareArgs/hardwareAcceleratedeverywhere) — on-device it only black-screened, confirming the ANGLE-D3D11-under-GPTK limit. (5) Fixed the GPTK Runtimes list showing wine runtimes — the M83 overlay copiesD3DMetal.frameworkinto the wine runtime'slib/external, soGPTKImporter.installed()matched it; now excludes any dir with a wine binary. (6) Fixed the updater offering a Wine version as an app update — it queried/releases/latest(oftenwine-cx-*); now fetches the release list and considers only the app's ownv*releases (isAppRelease). 173 tests / 28 suites green; clean build. -
✅ M94 — UI: single-pane Library + consolidated Advanced Settings. Removed the sidebar entirely (
RootViewis now justNavigationStack { LibraryGridView() }); deletedSidebarView/SidebarItemand the About pane. Advanced Settings (Library toolbar → gear) is now aTabView: Backend (the formerBackendSettingsView), Runtimes (the former Wine Manager — GPTK + Wine tabs), and Updates (newUpdatesView= version + the inline updater, replacing About). Update availability also surfaces as a small "· Update vX.Y.Z available" note to the right of the "X games" subtitle. 173 tests / 28 suites green; clean build. (Pending: a real Steam logo for the Steam button needs a bundled asset — SF Symbols has none and I won't fabricate Valve's mark; kept the SF Symbol for now.) -
✅ M93 — unify the live Steam client under one owner (fixes the double-spawn bug). The bottle's Steam client had TWO uncoordinated owners:
GameLibraryViewModeltracked it (PID + coalescing + cold-start grace), whileSteamBottleViewModel.launchSteamspawned its OWN untracked copy — so clicking Advanced → "Launch Steam" then Play on a game could start a second client (the Library'ssteamPIDwas still nil). ExtractedSteamClientSession(@MainActor @Observable) as the single owner of the live client: PID tracking, launch coalescing, cold-start grace, the experimental HW-accel flag, andensureRunning()/sendURL(). Both view models now route through it (LibraryopenSteam/play/install/ uninstall and settingslaunchSteam→session.ensureRunning()), keeping their distinct roles (operational library vs setup/admin) but with ONE tracked client. New test proves the cross-VM case (settings launch + Play → exactly one client). 173 tests / 28 suites green; clean build (no warnings). (This was the architecture finding I'd deferred at M88; safe to do now with M89's coalescing coverage.) -
✅ M92 — Phase 5: hardware-accelerated Steam bottle (experimental opt-in path; on-device test needed). Important framing first: games launched from the bottle are ALREADY hardware-accelerated — GPTK D3DMetal, proven on-device (Bloons TD 6, M83). Only the 2D Steam client UI is software-rendered (SwiftShader), a deliberate CEF black-window workaround. That workaround predates the M83 overlay that made GPTK's D3D11 actually work, so CEF's GPU path (ANGLE→D3D11→D3DMetal) might now render where it couldn't before. Added an opt-in experimental HW path to test that:
SteamBottle.cefHardwareArgs+steamEnvironment(hardwareAccelerated:)— enables CEF's GPU process (--use-gl=angle --use-angle=d3d11, drops--disable-gpu/--use-gl=swiftshader/STEAM_DISABLE_GPU_PROCESS) and points the DYLD fallbacks at the runtime's overlaid D3DMetal (same wiring a game launch uses) so ANGLE's D3D11 can reach Metal. Default launch stays software (verified).- Toggle: Advanced Settings → Steam bottle → "Hardware-accelerated UI (experimental)", then Launch Steam.
- Honest caveat: our own GeoGuessr/Electron test showed ANGLE's D3D11 backend FAILS under GPTK
(
eglInitialize D3D11 failed) even post-overlay, so this may still black-screen; and even if it renders, the surface may not present. It's opt-in precisely so it can't break the working software default. - 172 tests / 28 suites green; clean build;
dist/Silo.appreassembled.
-
✅ M91 — Phase 4 performance review (agentic). 4 lenses (UI re-layout, main-actor blocking, redundant work, I/O) → skeptical verify → only 3 real worth-doing fixes (the codebase was already perf-clean since the 100%-CPU fix + polling removal): (1) roomier
URLCache.shared(32 MB mem / 128 MB disk) so library cover-art is a cache hit on scroll-back, not a re-fetch; (2) hoistedGameLibraryVM. filteredto compute the filter+sort ONCE perLibraryGridViewbody (was twice — subtitle count + grid); (3)LogTailernow coalesces log-file write bursts to ~7×/sec (trailing throttle) so a noisy launch doesn't re-lay-out the 256 KB monospaced log Text on every kqueue event. No fix-now/high findings. 171 tests / 28 suites green; clean build (no warnings). -
✅ M90 — Phase 3 security hardening (agentic adversarial review, 14 findings). 4 threat lenses (download/execute integrity, archive extraction, process-exec injection, error/failure modes) → exploitability verification → applied 8 hardenings:
- CRITICAL — in-app updater had ZERO integrity check before replacing+executing the running app.
Added fail-closed SHA-256 verification of the release
.zipagainst a published<asset>.zip.sha256(release.yml now ships it) + https-only download guard. (App is ad-hoc signed → no Developer-ID/spctl to pin; this defeats MITM/CDN tampering. Defeating a compromised release needs notarization — see BLOCKED.) - HIGH — path traversal via an attacker-named release tag flowing into a
Runtimes/path: addedsafeRuntimeComponentsanitizer + aruntimesDir-containment assert; HIGH — runtime SHA-256 now mandatory (fail-closed) for the built-inSilo.wineRepo(was best-effort-skip). - https-only guard on all release-derived downloads (+
NSAppTransportSecurityATS dict, default-deny); appmanifestinstalldirpath-escape validation; scrubDYLD_INSERT_LIBRARIES/DYLD_FORCE_FLAT_NAMESPACEfrom inherited env for wine children; crash-safe staged GPTK import; honest checksum UI copy. - Shared
FileDigest.sha256+DownloadGuard.requireHTTPS. +10 tests → 171 / 28 suites green; clean build.
- CRITICAL — in-app updater had ZERO integrity check before replacing+executing the running app.
Added fail-closed SHA-256 verification of the release
-
✅ M89 — Phase 2 test-coverage gaps (agentic, +49 tests → 161). 4 domain mappers (orchestration, error/edge, graphics/runtime, parsing/models) → adversarial verify (real gap + catches a real bug, not coverage theater) → 25 verified gaps filled. New:
AppEnvironment.installUpdateorchestration (no-bundle.failed+ not-newer no-op),applyBackendfan-out, fullSteamBottleViewModelsuite,GameLibraryVMstop/uninstall-guard, and error branches acrossUpdater/RuntimeManager/GPTKImporter/ConfigStore/SteamBottle(download/unpack/replace/wineboot/checksum failures + corrupt-config fallback), plus parser/Codable edges (SteamPresenceStrategyunknown-case,EnvFlagslegacy migration + round-trip,AppManifestDecoder/LibraryFoldersDecoder). Test-infra only:FakeProcessRunnerreal terminate + incrementing PIDs;FakeURLProtocolper-session stub scoping (fixes a shared-registry race). No production code touched; no production bugs found (all assert existing behavior). 161 tests / 28 suites green; clean build. -
✅ M88 — Phase 1 architecture review (agentic, 8 verified fixes). 4 cross-file lenses (boundaries, abstraction value, dependency direction, concurrency-fit) → adversarial verify → 12 actionable, applied the 8 safe ones: deleted the vestigial
BackendResolver(it adopted an installed Whisky/CrossOver runtime — contradicts #8) + itsdetectedSource;ProcessRunning.observeExitnow required (dropped the dead Noop default that could silently swallow game-exit); propagated the injected runner intoUpdater(closed a test-seam leak); removedAppEnvironment.logTargetview-type leak; addedConfigStore.updateGamefield-scoped transaction (fixes alastPlayedlost-update vs a concurrent settings save); consolidated the backend-config fan-out (applyBackend+applyDefaultWine/GPTK); extractedWineRuntimeLayout(one home for runtime FS-layout math, mirrorsPrefixLayout). Stale CLAUDE.md actor list fixed. Deferred (flagged, regression-risky on validated CEF code): unify the Steam-client lifecycle (two VMs own it → possible double-spawn) and anUpdaterViewModelextraction. 112 tests / 25 suites green; clean build (no warnings). -
✅ M87 — removed the dead
.crossover/DXVK backend (GPTK-only). The CrossOver/DXVK fallback was advertised across config, UI, and policy but never wired (no DXVK download, no install path) — pure rot (decided with the user, 2026-06-28). Collapsed to a single graphics path: deletedGraphicsBackend+BackendPolicy(+ their tests), droppedGameConfig.backend,BackendConfig.{crossoverWinePath,dxvkDLLDirPath,wineBinary(for:)},EnvFlags.dxvkHUD,GraphicsLinker.linkDXVK, the backend Picker + the CrossOver/DXVK path rows in Advanced Settings, the per-game DXVK-HUD field, the GameDetail backend recommendation, and the now-deadapplicationsDirectoryautodetect param (it only existed to find CrossOver.app).makePlan/linkGraphicsare GPTK-only. Legit "CrossOver source" wine-build references (constraint #8) preserved; CLAUDE.md "Two runtime roles" updated to GPTK-only. 112 tests / 25 suites green; clean build (no warnings). -
✅ M86 — agentic codebase audit (17 verified cleanups). A 5-reviewer multi-agent audit (dedup → adversarial verify, 17 of 21 confirmed) → applied: dead code removed (
Asset.size,AppPaths.steamBottleWebHelper,BackendSettingsViewModel.paths,GameSettingsViewModel.appName); duplication collapsed (GPTKImporter.Result→GPTKInstall, shareddeQuarantine()+RuntimeInstallRow, genericAppManifestDecoder.opt<T>);bootstrap()re-entrancy fixed (isBootstrapping/didBootstrapsplit);BackendPolicy.effectivewired intoplay()(gptk→ crossover fallback — was dead code; covered by BackendPolicyTests); four stale M83 GPTK "system32" docs corrected to the overlay mechanism. 122 tests / 26 suites green; clean build (no warnings). -
✅ M85 — inline in-app updater (Sparkle-style). The updater now applies updates inline: download the release
.zip→ unpack besideSilo.app→ atomicreplaceItemAt→lsregister→ relaunch — no browser hop / manual install, replacing the old check+download-Link.Updater.downloadUpdate/installUpdate/relaunch(binary exec viaProcessRunning);AppEnvironment.installUpdate+UpdateState;AboutView"Download & Relaunch" button + progress. -
✅ CI(wine) — brew-link crash fixed (follow-up to M84 GnuTLS). The x86_64-Homebrew step hard-failed on a transitive
python@3.14link conflict (idle3 already exists). Now tolerates link failures (we reference every formula bybrew --prefix, never the linked name) and asserts the needed formulae are installed. Validated in CI: "Build dependencies" + "Fetch CrossOver source" steps pass; the build proceeds into compiling Wine. -
✅ M84 — Wine CI GnuTLS configure fix.
build-wine.ymlnow fails fast if x86_64 Homebrew dependencies do not install, installspkgconf, and exports the x86_64 Homebrewpkg-config/ include / library paths plus explicit x86_64 clang selection before Wine configure. This should unblock the GitHub runner failure:libgnutls 64-bit development files not foundwhile keeping--with-gnutlsrequired for schannel. Local verification: workflow YAML parses;swift build --disable-sandboxclean;Scripts/test.sh --disable-sandboxgreen (119 tests / 26 suites). The--disable-sandboxflag was only needed because this managed local session blocks SwiftPM's manifest sandbox. -
✅ M83 — GPTK D3DMetal OVERLAY baked into Silo (native DX11 games render under GPTK on-device). 119 tests / 26 suites green; clean build (no warnings);
dist/Silo.appreassembled. The load-bearing GPTK activation: Apple's d3d modules must be OVERLAID into the wine runtime's OWNlib/winetree, not merely put onWINEDLLPATH(which loads GPTK's PE dll but pairs it with wine's ownwined3d→OpenGL backend →D3D11CreateDevice0x80004005). This replaces the M29 WINEDLLPATH/system32-symlink wiring.GraphicsLinker.overlayGPTK(wineBinary:gptkLibDir:)copies GPTK's 6 graphics modules' PE.dllinto<wine>/lib/wine/x86_64-windows, recreates each unix.soas a relative symlink inx86_64-unix(preserved, not dereferenced — keeps D3DMetal.framework's@rpathlookup working), and copieslib/external(libd3dshared.dylib + D3DMetal.framework) into<wine>/lib/external. The runtime is then self-contained for D3DMetal (GPTK not consulted at launch). Idempotent (byte-compares a witness module): no-op when current, re-applies on a runtime re-download or GPTK update — so it survives both.makePlanGPTK env now points the DYLD fallbacks at the runtime's ownlib/externaland forces only the GPTK-translated modules builtin (WINEDLLOVERRIDES=d3d10,d3d11,d3d12,dxgi=b; no WINEDLLPATH; d3d9/wined3d untouched). Optimally-tuned set confirmed against Apple's GPTK README (documented env is justROSETTA_ADVERTISE_AVX+D3DM_SUPPORT_DXR; MetalFX/DXR are per-game, off by default) — all already inEnvFlags. The overlay was the only substantive gap.- Proven on-device: Bloons TD 6 (native Unity D3D11) creates a real D3DMetal device
(
Direct3D 11.0 level 10.1), renders with sound + fullscreen, co-resident Steamworks connected. - Cleanup: old GPTK-into-
system32path deleted;link()→linkDXVK()(crossover-only); deadgptkExternalDirPath/gptkWineDLLDirPath/LinkError.backendNotConfiguredremoved.
-
✅ M73–M81 — THE GATE IS CLEARED: bottle Steam RENDERS + LOGS IN on the from-source CrossOver wine (on-device, 2026-06-28). 117 tests / 26 suites green; clean build. Three fixes, each found from live logs, finally got the Windows Steam client visible + signed in:
- winebus/SDL crash (the recurring
NSWindow … main threadabort):winebus.sodlopens libSDL2 whose macOS init pops an off-main-thread NSAlert → Wine aborts.WINEDLLOVERRIDES=winebus=does NOT disable a PnP.sysdriver; the reliable fix is removing the dylib. → build--without-sdl(M80) +RuntimeManager.stripBundledSDLauto-strips bundledlibSDL2*(no rebuild needed).⚠️ SUPERSEDED 2026-07-26 (controller support, see "Now"): the crash was the generic Homebrew libSDL2, not SDL itself — CrossOver ships SDL 2.30.12 (same winebus source) and does NOT crash. SDL is now re-enabled: build--with-sdl+ bundle a pinned SDL 2.30.12 built from source;stripBundledSDLremoved. - wrapper stranded (M81): a Steam update switched the CEF dir
cef.win7x64→cef.win64, leaving the single-dir wrapper orphaned while Steam ran the unwrapped webhelper → black.installWebHelperWrappernow wraps ALLbin/cef/*/steamwebhelper.exe. (Path was also wronglycef.win64-hardcoded pre-M78.) - presentation (M79): Steam launches in a Wine virtual desktop (
explorer /desktop=) so winemac.drv presents the CEF surface (rootless = black on CrossOver). CEF forced onto SwiftShader software GL viaSTEAM_CEF_COMMAND_LINE+ the--in-process-gpuwrapper (M76). - Login via QR (Steam mobile) succeeded + cached (AllowAutoLogin=1). The Chromium
WSALookupServiceBegin/10045/Transport Errorlog spam is NON-fatal background noise, not a login blocker. - ✅ CO-RESIDENT LAUNCH + STEAMWORKS VALIDATED: GeoGuessr Steam Edition (3478870 — previously FAILED
Steamworks with no logged-in Steam) launched via
launchInBottleunder GPTK and gotgetAuthTicketForWebApi -> OKfrom the co-resident Steam. The whole shared-bottle architecture works. Per-game polish left: GeoGuessr is Electron and its ANGLE/WebGL doesn't init under GPTK (map renders broken) — fixable per-game via software/SwiftShader GL, separate from the (working) architecture.
- winebus/SDL crash (the recurring
-
M68–M72 — REVERT to the Steam-bottle model + a 3-round agentic audit. 115 tests / 26 suites green; clean build (no warnings). SteamCMD + macOS credential-seeding were removed and the app reverted to a single shared Steam bottle: one Wine prefix hosting a logged-in Windows Steam client; games install there and launch co-resident under GPTK/D3DMetal so Steamworks/DRM works (IPC is prefix-scoped). Then an agentic audit-fix loop (4 read-only audits → verify → apply → re-audit):
- M68: the revert itself (bottle foundation, discovery from the bottle's
appmanifest, launchInBottle). - M69: removed the dead isolated-prefix layer (PrefixProvisioner, GameLogStore, SteamBottle.launchGame,
AppPaths.prefix/prefixesDir, RuntimeManager.installedRuntimes/availableAssets, SteamApp.downloadProgress/
needsUpdate). Bottle now launches Steam in a Wine virtual desktop (
explorer /desktop=) with overlay-disable overrides + msync;play()brings Steam up ONCE (tracked PID) with a cold-start grace. Wine build: addCROSSCFLAGS=-fvisibility=default; drop/usr/local/libfrom the DYLD fallback (it leaked Homebrew's duplicate gtk → the "implemented in both" crash seen launching bottle Steam). - M70: removed the now-obsolete
.sharedSteamClientstrategy + unusedReceipt/revert(the bottle IS the in-prefix Steam);load()surfaces real discovery errors instead of swallowing them. - M71: force msync for every bottle game launch (a per-game esync/none would fork a 2nd wineserver and
break Steamworks);
steam://install/uninstall deliver via single-instance forwarding (no 2nd Steam in a duplicate desktop); GraphicsLinker scoped tod3d*/dxgi*so it can't clobber the shared bottle; removed the orphanedWineRuntimetype. - M72:
stop()alsowine taskkill /F /IM <game exe>in the bottle's msync wineserver so a child/relauncher game isn't orphaned (Steam untouched — different image names).
- M68: the revert itself (bottle foundation, discovery from the bottle's
-
M58–M60 COMPLETE — spring cleaning. 150 tests / 30 suites green; clean build (no warnings). Three parallel audits (dead code / duplication / post-pivot vestigial) → verified findings → acted.
- M58: Uninstall also removes the game's isolated Wine prefix (full reclaim).
- M59 (dedup): SteamAppInfo.headerArtURL/storePageURL (views stop hand-rolling URLs); one
GameArtworkPlaceholder; one URL.tailString; shared .uninstallConfirmation modifier; LogTarget.windowID
- AppEnvironment.logTarget(for:).
- M60 (removals): deleted zero-ref dead symbols + post-pivot vestigial code (masterBottlePath/ steamRoot/steamWineBinaryPath/isMasterBottleConfigured/steamWine, DiscoveryEngine.steamRoot(inBottle:), Silo.steamInstallerURL/steamLaunchArgs, AppPaths.masterBottleDefault, WineRuntime.wineserverBinary, PrefixLayout.syswow64/dosDevices, StateFlags.isDownloading, SteamApp URL helpers, requiresUserStub), removed CrashLoopGuard + orphaned ProcessRunning.processCount, hid the inert .sharedSteamClient from the picker, reworded stale Master-bottle docs. Net −156 lines (5045→4940 LOC) despite adding helpers.
-
M51–M57 COMPLETE — perf + reliability + UX pass. 153 tests / 31 suites green; clean build (no warnings); .app assembles; verified running at 0.0% idle CPU (was pinned at 100%).
- M51 (the energy bug): sampled the live app → main thread pinned in SwiftUI layout driven by a
CADisplayLink. Root cause: indeterminate
ProgressView()spinners INSIDE the ScrollView (loading / "Updating" / AsyncImage placeholder) re-laid out the whole grid every frame. Moved spinners out of the scroll content; download barsafeAreaInset→VStack sibling;filteredno longer re-sorts. Verified 100%→0%. - M52 (event-driven, no polling):
ProcessRunninggainsobserveExit(DispatchSource process) +observeWrites(file-system) +firstPID. Downloads read progress reactively from the SteamCMD log and detect completion/interruption from the process's real exit (no 2s poll, no flaky pgrep) — fixes the false "Resume"; manifest is authoritative on exit. Game-exit clears state via an exit observer. - M53 (UX): whole card opens details; detail view shows Disk size / Metacritic / Minimum
requirements; status messages auto-dismiss (6s); refresh toolbar keeps button chrome while spinning.
Fixed logged-in account "falling away" —
autodetectwas wipingsteamUsername; now preserved + the account shows in the navigation subtitle. - M54: Uninstall (menu + details, confirmed) deletes the game's bucket files.
- M55: fast refresh — incremental app-metadata cache (
ownedGames(known:)onlyapp_infos new apps; the cache persists the full owned catalog). - M56:
BackendPolicy— GPTK default for DirectX 9–12, auto CrossOver fallback when GPTK absent; detail view shows the recommended backend + DirectX-derived rationale. - M57: log viewer is now a kqueue file-watcher (was a 1s poll). No timer/poll loops remain anywhere.
- M51 (the energy bug): sampled the live app → main thread pinned in SwiftUI layout driven by a
CADisplayLink. Root cause: indeterminate
-
M0–M41 COMPLETE — pivot DONE. 137 tests / 29 suites green; clean build (no warnings); .app assembles.
-
PIVOT COMPLETE (M36–M41): Wine Steam-client GUI fully removed; replaced by native-macOS SteamCMD.
- M37–M38: SteamCMDClient (install + force-windows download + capture) + SteamAppInfo metadata + ownedWindowsGames enumeration (licenses→packages→app_info, filtered to windows-only games).
- M39: SyncMode enum, MSync default (Apple-Silicon best practice).
- M40: GameLibraryViewModel + SteamLoginViewModel wired into AppEnvironment (account in BackendConfig).
- M41 (UI swap + rip-out): new SteamLoginView + SteamGameTileView; LibraryGridView lists owned Windows-only games (Download→SteamCMD, Play→GPTK bucket); OnboardingView step 3 = "Sign in to Steam"; readiness = wineReady && gptkReady && steamLoggedIn. DELETED: SteamBottleInstaller, SteamCardView, GameCardView, LibraryViewModel, SteamLibraryInstaller, OwnedAppsReader (+ their tests). ViewModelTests pruned to the surviving VMs. CrashLoopGuard retained (available; no longer wired to Steam GUI).
- REMAINING (human-gated): real SteamCMD login + a real Windows-only game download → launch in a GPTK bucket (needs the user's Steam credentials). All headless-testable logic is done + green.
-
>>> ARCHITECTURE PIVOT (2026-06-27, user decision) <<< The Wine Steam-client GUI does not render under our self-built wine on macOS 26 (CEF black window; verified that -no-cef-sandbox fixes the crash-loop but neither GPU-on nor GPU-off nor RetinaMode nor virtual-desktop renders it — this is the industry-wide problem that got Whisky archived). New model "Native Steam library → SteamCMD → GPTK buckets": (1) DROP the Wine Steam bottle entirely (SteamBottleInstaller/openSteam/CEF flags/shared- client presence); (2) library = the user's owned games filtered to Windows-only (no native mac build); (3) download via native macOS SteamCMD
@sSteamCmdForcePlatformType windows(no Wine/CEF); (4) launch each in a per-game GPTK bucket configured from the game's Steam metadata (DirectX→backend) else sensible default. Owned-list + metadata via SteamCMD itself (licenses_print / app_info_print) — no Web API key needed.- P0 DONE: native macOS SteamCMD verified on macOS 26 (bootstraps, accepts force-windows, returns app_info platforms for appID 70).
- M36 / P1-foundation DONE:
SteamCMDpure command builders (download / app_info / licenses) + tests. - TODO: P1
SteamCMDClient(install steamcmd + run download/login via ProcessRunning); P2 owned Windows-only library + metadata; P3 metadata-driven GPTK bucket; P4 rip out old Steam-bottle code + UI rework.
-
M35 = bundler no longer bundles glib/gstreamer/ffmpeg media stack (killed the "implemented in both" + glib-type dup warnings); 44→21 libs; clean wineboot = 0 freetype + 0 dup. RetinaMode reverted (broke windowing).
-
M33 (user UX/bug fixes): (1) Steam card now has a right-click context menu + always-visible ellipsis (Open Steam, Reinstall, View Log…, Wine Config…, Reveal Bottle, Settings…). (2) Log viewer opens as a STANDALONE WINDOW (WindowGroup id "silo-log" + openWindow), not a modal sheet, so it live-tails while you drive the main window; generalized to any file (title+url), added an Autoscroll toggle. (3) (b) CrashLoopGuard + ProcessRunning.processCount: auto
wineserver -kif awinedbgstorm appears, wired behind openSteam. (4) (a) gstreamer dedup: reorder to bundled-LAST was tried but BREAKS FreeType (wine only finds its dlopen'd freetype from the bundle), so kept bundled-FIRST; proper dedup = don't bundle the glib/gstreamer/ffmpeg media stack (TODO in bundler; only manifests during video playback). -
OPEN (windowing): Steam launches but renders as two blank/black rootless windows (steam + CEF steamwebhelper). Testing a wine VIRTUAL DESKTOP (HKCU\Software\Wine\Explorer Desktop=Default) to composite into one window — enabled on the user's bottle; awaiting visual confirmation it renders.
-
M32 (bug: "Open Steam" opens nothing): Steam WAS launching but its CEF UI renderer went "unresponsive" and Steam killed+relaunched it every ~90s forever, so the window stayed 0x0/blank. Root cause: the CEF sandbox under wine. Fix:
Silo.steamLaunchArgsnow passes-no-cef-sandbox(+-cef-disable-gpu -allosarches; dropped obsolete-cef-force-32bit). EMPIRICALLY VERIFIED on the user's machine: 0 "unresponsive" events after relaunch and a real 705x440 Steam login window appeared. -
M31 (bug: can't right-click library cards): GameCardView had only the ellipsis
Menu, no.contextMenu. Added a right-click menu (Play/Stop, Isolate, Settings…, View Log…, Reveal Prefix, Wine Config…, View on Steam Store, Reset Prefix) via a sharedmanagementMenu()builder reused by the ellipsis menu, which is now always visible (even while running). Per-game settings pane gained Launch options (GameConfig.launchOptionsString↔customArgs, Steam-style) and a DXVK HUD field (CrossOver backend only).SteamApp.storePageURLadded. +3 tests. -
M30 (bug: Install Steam hung + crash storm): the silent
SteamSetup.exe /Sauto-launches Steam.exe, which crash-loops under wine (Steam CEF) and spawns hundreds ofwinedbg --autoprocesses, so the installer NEVER returns → app stuck "Installing…",masterBottlePathnever set. Fix:SteamBottleInstallernow SPAWNS the installer detached, polls forSteam.exeto appear (≤180s), thenwineserver -ks the bottle so the crash-loop can't accumulate; full client downloads on first real run via "Open Steam" (which passes CEF-safe flags). Verified on the user's machine: bottle + Steam.exe present; storm killed. -
M29 (D3DMetal path wiring): GPTK game launches now (a) put GPTK's
lib/externalonDYLD_FALLBACK_LIBRARY_PATH+DYLD_FALLBACK_FRAMEWORK_PATHsod3d11.soresolves@rpath/libd3dshared.dylibandD3DMetal.framework; (b) add GPTK'slib/winetoWINEDLLPATHand force d3d/dxgi=b(builtin) so wine loads GPTK's d3d instead of the base wine's. NewBackendConfig.gptkExternalDirPath/gptkWineDLLDirPathderive these fromgptkLibDirPath. STATICALLY VERIFIED on the real GPTK-4.0_beta_1: D3DMetal.framework loads under x86_64; converter libs (libmetalirconverter/libdxccontainer) resolve via the framework's own rpath; wine honors DYLD_FALLBACK (proven by the freetype fix). E2E activation is human-gated (see BLOCKED). -
M28 (self-contained wine):
Scripts/bundle-wine-dylibs.shcopies the transitive closure of wine's non-system dylib deps (arch-filtered to the wine's arch — x86_64) into<wine>/lib/silo-bundled; the app launches wine withDYLD_FALLBACK_LIBRARY_PATH=<…>/lib/silo-bundled(URL.siloDyldFallback) so freetype/gstreamer/etc. resolve without Homebrew. Wired into build-wine (CI + local) + install- local-wine. VERIFIED: wineboot with the app's exact env → 0 FreeType warnings, prefix boots. -
M27 (bug: "Install Steam does nothing"): first-run
winebootwas hanging on blocking wine-mono/ wine-gecko install dialogs. Nowwineboot(SteamBottleInstaller + PrefixProvisioner) setsWINEDLLOVERRIDES=mscoree,mshtml=(Silo.winePrefixInitOverrides) so it completes headlessly. Verified: the user's wine-cx-26.2.0 boots a home-dir prefix cleanly with the override. -
KNOWN (build follow-up): the locally-built wine logs "cannot find the FreeType font library" — the self-built wine depends on Homebrew dylibs (freetype/gstreamer/…) not bundled/relocated, so it's not fully self-contained. Prefix creation still works; fonts won't render until deps are bundled.
-
CI FIX:
Scripts/test.shcrashed on the runner (bash 3.2 +set -u+ emptyFLAGSarray → "unbound variable"); now guards the empty-array expansion. (This was failing every CI run.) -
M26 = game artwork:
SteamApp.headerArtURL(Steam CDN header.jpg); GameCardView shows the cover via AsyncImage with a gradient placeholder fallback. -
M25 (Wine Manager fixes from user report):
locateWineBinarynow excludes directories, so GPTK installs (lib/winedir) no longer masquerade as Wine in the Wine tab; Wine tab simplified to a single "Install latest Wine" (dropped the broken multi-version refresh — CI publishes the canonical latest); removed a strayRuntimes/GPTKleft by the M15 verification import. -
M24 = downloaded-Wine SHA-256 verification (build-wine publishes
.sha256; RuntimeManager verifies). -
M23 = audit robustness + UX: downloaded Wine is de-quarantined + ad-hoc re-signed (Gatekeeper), extraction cleans up on failure; GPTK de-quarantined (no re-sign — keeps Apple's signature); live log tail; library recently-played sort + installed/updates filter; prefix management (reveal / Wine config / reset); CI concurrency + ccache + timeouts + read perms. Perf levers (msync default, DXMT, rosettax87, DXVK install) still deferred — say "do perf" to start.
-
M22 = launch feedback + UX wins: Running/exited state + Stop button (
ProcessRunning.isRunning,LaunchOrchestrator.stopviawineserver -k,LibraryViewModelPID monitor);lastPlayedstamped;Updaterwired (bootstrap check → About "Update available"); exe picker in GameSettingsSheet (ExecutableResolver.allExecutables); library auto-refresh on app re-activation (scenePhase).
- PERF (deferred per user — say "do perf"): msync default-on (esync/msync mutually-exclusive enum);
DXMT backend; rosettax87 fast x86; DXVK install path (the
.crossoverbackend is unreachable on a clean install). - HUMAN-GATED: notarization in release.yml (needs your Apple Developer ID + secrets).
- D3DMETAL PATH: DONE (M29). Runtime env wired + statically verified. Real activation needs a game launch (BLOCKED).
- NICE-TO-HAVE: pin GitHub Actions by commit SHA (clears Node-20 deprecation notice).
- All other audit findings (correctness, robustness, UX) are DONE (M21–M24). Wine sourcing architecture settled (see
WINE-BUILD.md): self-hosted CrossOver-based Wine built in our own CI (
build-wine.yml, workflow_dispatch) → published to our Releases → app pulls fromSilo.wineRepo(= mikaelhug/Silo); no third-party prebuilt dependency. D3DMetal still imported from Apple's.dmg. Steam launches with CEF crash-workaround flags. Perf work (DXMT/rosettax87/msync) deferred per user.
- CrossOver's Wine is LGPL open source (what Apple's GPTK formula compiles). We build it ourselves in CI and host it, rather than depend on Gcenx/Sikarugir prebuilts (which can go stale). Don't build upstream Wine from scratch — perf comes from translation layers (D3DMetal/DXMT/DXVK) + x86 translator.
- CI-gated:
build-wine.ymlis a best-effort recipe NOT yet validated end-to-end; until the firstwine-*release exists, the Wine tab is empty — use CrossOver (auto-detected) or override the path. - Pivot (user, 2026-06-26): GPTK acquisition is "Browse to Apple
.dmg" → Silo mounts + extractsredist/lib. VERIFIED against the realGame_Porting_Toolkit_4.0_beta_1.dmg(gitignored) viasilo --import-gptk <dmg>: extracts D3DMetal.framework + 6 DLLs to Runtimes/GPTK (68M), clean detach.
apple/game-porting-toolkitis a resources repo, no binary releases; official GPTK = a DMG behind Apple-ID login (not automatable).Gcenx/game-porting-toolkit/releaseshas prebuilt GPTK binaries (no login) → use as the 1-click default; link Apple's repo for the manual route.- Steam Windows installer:
https://cdn.cloudflare.steamstatic.com/client/installer/SteamSetup.exe(akamai mirror:https://steamcdn-a.akamaihd.net/client/installer/SteamSetup.exe), silent flag/S. - No single "install whole library" command. Mechanism =
steam://install/<appid>per owned app via the running Steam client; owned appids parsed fromuserdata/*/config/localconfig.vdf. - "wine-mirror/wine" is source-only (no mac binaries) → it means "use a vanilla Wine runtime" as the Steam-bottle fallback when GPTK can't run the Steam client.
swift build: ✅ clean (no warnings)swift test: ✅ 88 tests / 19 suites passing (run viaScripts/test.sh)Scripts/build-app.sh: ✅ produces ad-hoc-signeddist/Silo.app(com.mikael.silo, min OS 26.0); bundled binary smoke-runs- CI/Release: ✅
.github/workflows/{ci,release}.ymlvalid YAML - Last green commit: M12 CI + release + README
- (none)
- (none — all milestones complete)
- M0 — Scaffold SPM project + harness docs (Package.swift, silo/SiloKit/SiloKitTests, CLAUDE.md, STATUS.md, README, .gitignore, Scripts/test.sh).
- M1 — KeyValues tokenizer + parser + KVNode (
Discovery/{ACFTokenizer,KeyValuesParser,KVNode}.swift; 14 parser/tokenizer tests). - M2 — Models (
SteamApp,StateFlags,LibraryFolder) + decoders (AppManifestDecoder,LibraryFoldersDecoder) + fixtures +FixtureLoader; 10 decoder tests. - M3 —
DiscoveryEngine(actor): scans primary + extra libraries, skips bad manifests;TempDirhelper; 5 tests. - M4 — Config models (
GraphicsBackend,SteamPresenceStrategy,EnvFlags,WineRuntime,BackendConfig,GameConfig) +AppPaths+AppState+ConfigStoreactor (JSON); 8 tests. - M5 —
ProcessRunningprotocol +ProcessResult+SystemProcessRunner(temp-file redirect, env merge, detached spawn) +FakeProcessRunner(lock-guarded); 8 tests incl real subprocesses. - M6 —
PrefixLayout,PrefixProvisioneractor (idempotent wineboot --init),GraphicsLinker(symlink/copy GPTK or DXVK into system32); 9 tests. Note: Sendable structs use computedFileManager.default(can't store non-Sendable); actors may store it. - M7 —
LaunchPlan, pureLaunchOrchestrator.makePlan(static; isolated WINEPREFIX, backend env, DXVK overrides),launchpipeline (provision→link→log→spawn),ExecutableResolver,GameLogStore; GameConfig gainedexecutableRelativePath; 12 tests. - M8 —
BackendResolver(Whisky/Kegworks/CrossOver detection, .none on clean machine) +SteamPresenceInstaller(none/appIDFile/sharedClient/emulatorStub with backup+revert), wired into launch pipeline; 10 tests. - M9 —
GitHubReleasemodel,Updater(GH Releases version check, numeric compare),RuntimeManageractor (list/fetch/download+tar-extract/remove);FakeURLProtocoltest support; 9 tests. Note: Swift Testing runs in parallel — network tests use unique stub URLs (no shared-state reset). - M10 —
AppEnvironmentcomposition root +SiloApp(SwiftUI App); view models (LibraryViewModel,BackendSettingsViewModel,GameSettingsViewModel,RuntimeViewModel); views (Root/Sidebar/LibraryGrid/GameCard/Badge/BackendSettings/RuntimeManager/GameSettingsSheet/LogViewer/About/PathPickerRow);silo --smokeheadless path; 7 VM tests. - M11 —
Resources/{Info.plist.template,silo.entitlements (no sandbox)}+Scripts/{build-app,sign,run,dev,clean}.sh; assembles + ad-hoc signsdist/Silo.app, strips quarantine. Verified bundle valid + bundled binary smoke-runs. - M12 —
.github/workflows/{ci,release}.yml(build+test+bundle on push/PR; tag → ad-hoc-signed Silo.zip release) + README (build, first-run setup, sandbox, legal). - M13 — App icon: CoreGraphics generator (
Scripts/make-icon.swift) +make-icon.sh(sips/iconutil) ->Resources/AppIcon.icns; wired viaCFBundleIconFile; bundled by build-app.sh. - M14 —
SteamBottleInstaller(boot bottle → download SteamSetup.exe → silent/Sinstall) +BackendConfig.steamWine(vanilla fallback) + AppPaths.masterBottleDefault; "Create Master Steam Bottle (1-click)" button + VM; 4 tests. - M15 —
GPTKImporter(browse Apple .dmg →hdiutil attachouter+nested via plist → copyredist/lib→ Runtimes/GPTK, setgptkLibDirPath); RuntimeVM.importGPTK + "Import GPTK from .dmg…" UI + Apple link;silo --import-gptkCLI; verified on real GPTK 4.0 DMG; 4 tests. Decision log: GPTK has no wine binary (overlay only); base wine still from CrossOver/download. - M16 —
OwnedAppsReader(parse userdata/*/config/localconfig.vdf owned appids) +SteamLibraryInstaller(queuesteam://install/<appid>per owned app via wine); LibraryVM.installEntireLibrary + "Install entire library" toolbar button; localconfig.vdf fixture; 6 tests. - M17 — GPTK Manager: versioned installs (
Runtimes/GPTK-<version>from DMG name) viaGPTKImporter.runtimeName/installed/remove;GPTKInstallmodel;BackendConfig.gptkRuntimeName;GPTKManagerViewModel(import/remove/set-default, auto-default on first import) +GPTKManagerView+ sidebar "GPTK Manager". Moved GPTK import out of Wine Runtimes view. 5 new tests. - M18 — Wine Manager (
WineManagerViewsegmented GPTK|Wine tabs): GPTK tab =GPTKManagerView; Wine tab =WineDownloadViewdriven by rewrittenRuntimeViewModel(latest 3 Gcenx releases, 1-click install, set-default, remove).WineInstallmodel;RuntimeManager.availableReleases/preferredAsset/installWine/installedWines/locateWineBinary;BackendConfig.wineRuntimeName;Silo.wineRepo(Gcenx, .tar.xz ~250MB). Backend view → "Setup" with Advanced disclosure for manual paths; deleted RuntimeManagerView; sidebar Library/Setup/Wine Manager/About. 3 new tests (109 total). - M23 — Audit robustness+UX: RuntimeManager
harden(xattr de-quarantine + ad-hoc codesign) + extraction cleanup; GPTKImporter de-quarantine; LogViewer live tail+autoscroll; LibraryViewModel SortOrder/Filter + lastPlayed map; PrefixProvisioner.remove + LaunchOrchestrator.runWineTool (winecfg) + GameCard prefix menu; CI concurrency/ccache/timeouts/read-perms; 3 tests (117 total). - M22 — Launch feedback + UX:
ProcessRunning.isRunning(pid:)(kill(pid,0));LaunchOrchestrator.stop(wineserver -k);LibraryViewModelrunningPIDs + monitor + Stop +lastPlayed; Updater wired into AppEnvironment/About; exe picker (ExecutableResolver.allExecutables); scenePhase auto-refresh; 3 new tests (116 total). - M21 — Post-review correctness hardening (see git log).
- M20 — Wine sourcing architecture:
Silo.wineRepo→ self-hostedmikaelhug/Silo(removed stale GcenxdefaultRuntimeRepo/gptkRepo);WINE-BUILD.mddecision doc;.github/workflows/build-wine.yml(CI builds CrossOver-base Wine from open source → our Releases; workflow_dispatch, needs CI iteration). Steam launches withSilo.steamLaunchArgsCEF flags (openSteam). 1 new test (112 total). Perf (DXMT/rosettax87/msync) deferred. - M19 — Library-as-home: removed Setup sidebar pane.
OnboardingView(3 StepRows: Install Wine/Import GPTK/Install Steam) shown whenAppEnvironment.setupCompleteis false;SteamCardView(Open Steam viaAppEnvironment.openSteam) pinned first in the grid when complete.RuntimeViewModel.installLatest; setup-readiness computed on AppEnvironment; Advanced settings via Library toolbar gear →AdvancedSettingsSheet(BackendSettingsView). Sidebar Library/Wine Manager/About. 2 new tests (111 total).
- 2026-06-26 — Use Swift Testing (
import Testing) not XCTest: bundled in toolchain, keeps zero deps. XCTest is NOT available under Command Line Tools (no Xcode), Testing is. - 2026-06-26 — Testing under CLT needs framework search paths:
Testing.frameworklives in$(xcode-select -p)/Library/Developer/Frameworksandlib_TestingInterop.dylibin.../Library/Developer/usr/lib.Scripts/test.shadds both via-F+-rpath. Plainswift testfails with "no such module 'Testing'". - 2026-06-26 — Package
platforms: .macOS(.v15); real min OS enforced via Info.plistLSMinimumSystemVersion=26.0. - 2026-06-26 — Custom
URLSessionGitHub-Releases updater instead of Sparkle to keepPackage.swiftdependency-free.
- DiscoveryEngine skips Windows-style (
C:\...) library paths inlibraryfolders.vdf; only host-absolute (/...) extra libraries are scanned. In the single-downloader model games land in the primary C: library (always scanned), so this is sufficient for v1. Add Winedosdevicesdrive-letter translation if cross-drive libraries are needed. - Co-resident logged-in Steam: the real "Steam client in the game's prefix" answer is now the shared Steam bottle (
SteamBottle+SteamClientSessionrun one logged-in Windows Steam client that all co-resident Steam games reach). The old per-game presence strategies.sharedSteamClientand.emulatorStubare removed —SteamPresenceStrategyhas only.none+.steamAppIDFile(unknown/legacy raw values decode to.none). Constraint #7 still bars bundling any emulator.
- HW-accelerated Steam UI (M92, on-device test): flip Advanced → Steam bottle → "Hardware-accelerated
UI (experimental)" → Launch Steam. If the CEF window renders (not black) and the GPU log shows ANGLE/D3D11
(not SwiftShader), GPTK now drives the Steam UI on Metal — report back and it can become the default. If it
black-screens /
eglInitialize D3D11 failed, the ANGLE-D3D11-under-GPTK limit still holds and software GL stays the path (the 2D UI is fine on software; games are HW regardless). Only the user can run this gate. - (none for the build — the items below need a real Wine runtime + on-device launch, not code changes)
- Bottle Steam CEF render + login (M76, verified recipe applied — needs on-device confirm): deep-research
(→ MelonForAll/vineport, confirmed working macOS 2026) found the root cause of BOTH the black window AND
the
Transport Error 2login failure: the steamwebhelper wrapper injected--single-process, which also breaks Chromium's network service under Wine. Fixed to--in-process-gpu+ SwiftShader software GL (STEAM_CEF_COMMAND_LINE/STEAM_DISABLE_GPU_PROCESS/GALLIUM_DRIVER=llvmpipe) + rootless launch + Vineport's steam.exe flags. To verify: rebuild wine (Scripts/build-wine.sh <ver>— corrected wrapper) + rebuild the app, Advanced → Reset Steam login → Launch Steam → confirm the UI paints + login completes. If still failing, the research's fallback is kaon's model (native macOS Steam primary). explorer /desktop=program-path form:launchSteampasses the macOS unix path ofsteam.exeas the program arg towine explorer /desktop=Silo,<geom>. If wine's explorer needs a Windows path (C:\Program Files (x86)\Steam\steam.exe) instead, Steam won't launch — verify on-device and switch if so.stop() under real Wine+cold-start 10s grace— REMOVED as stale (reconciled 2026-07-12). Neither exists in the code: Phase 4 dropped the per-gamestop()/taskkillteardown (Silo launches detached and never stops a game), and the flat-10splay()grace was replaced by the event-driven readiness gate (SteamClientSession.ensureRunning→ kqueue onuser.reg, 20s failsafe;isRunningnow also cross-checks the wineserver socket so a stale reg pid can't read as "up"). Delisted, not a gate.- GPTK E2E activation — RESOLVED (M83). Confirmed from a real D3D game (Bloons TD 6):
WINEDLLPATHalone does NOT activate GPTK (wine keeps its own wined3d backend → device-creation failure); the overlay copy into wine's ownlib/wine(Whisky's method) is required and now automated byGraphicsLinker.overlayGPTK. D3DMetal device creation + render verified on-device. Confirm the default Wine/GPTK runtime repo— RESOLVED (reconciled 2026-07-12). NoSilo.defaultRuntimeRepo/Kegworkssymbol exists; the runtime repo isSilo.wineRepo=Versions.githubRepo=mikaelhug/Silo(Silo ships its own from-CrossOver-source Wine). Delisted.- Remaining items below are genuine on-device HARDWARE gates (not code drift) — they need a real Wine runtime + a Mac to run, and are a human on-device pass, not a code change.
- Build the patched wine:
Scripts/build-wine.sh 26.2.0(adds-fvisibility=default+ the steamwebhelper wrapper). Download/point Silo at a GPTK runtime. - Onboarding: import GPTK
.dmg, then Set up (chains Wine → DXMT → Steam → wineboot → components → warm-up) → Launch Steam. Confirm the CEF login window actually PAINTS (the gate above); sign in once (Steam caches it). - Confirm the library lists games installed in the bottle; Install routes a
steam://URL to the running Steam. - Play → game launches co-resident in the bottle under its chosen backend (GPTK by default, DXMT per game); Steamworks/online works. (There is NO Stop — Silo launches detached and leaves games running, like CrossOver; quitting Silo doesn't kill them.)
- (Distribution) provide Apple Developer ID + notarization secrets for signed releases.