Skip to content

feat(valve-stack): Valve stack replication — pure-PE Proton 11.0 (Phases 1–3) - #82

Merged
weter11 merged 18 commits into
mainfrom
phase3/pure-pe-proton11
Aug 15, 2026
Merged

feat(valve-stack): Valve stack replication — pure-PE Proton 11.0 (Phases 1–3)#82
weter11 merged 18 commits into
mainfrom
phase3/pure-pe-proton11

Conversation

@weter11

@weter11 weter11 commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the Valve Stack Replication directive (Phases 1–3): SteamFlow now replicates Valve's compatibility stack — pinned 11.x pure-WoW64 runners, native Rust launch-semantics parity, and a from-source pure-PE Proton 11.0 build — instead of maintaining a parallel wine-master-tracking stack.

Phase 1 — pinned 11.x pure-WoW64 stack (verified 2026-08-11)

  • RE2 (883710) launches and renders under wine-tkg-staging-git-11.13 (pure new-WoW64) via the headless pipeline: Steam API ownership gate passes, D3D12/vkd3d-proton initializes, continuous rendering.
  • Empirical runner reality matrix documented (GE / cachyos / official Proton 11.0 classic-wow64 builds are unbootable on this pure-64-bit host).
  • Decision encoded: no Proton ≤ 10; 11.x / steamrt4 / pure-WoW64 is the standard; i386-multilib permanently rejected.

Phase 2 — launch-semantics parity (complete 2026-08-11)

  • steamflow test-diff <appid> env-parity harness (src/parity.rs): parses native PROTON_LOG=1 output and diffs against SteamFlow's effective_env.json (MISSING / EXTRA / MISMATCHED / MATCHED). RE2: 0 missing, 2 matched after fixes.
  • Native Rust Proton ABI (src/runner/proton_abi.rs): port of Valve's proton script + default_pfx launch semantics — compat config assembly, PROTON_*/WINE_* env rules, base DLL overrides, native prefix seeding. No Python at game launch.
  • Runner versions surfaced into VERSIONS.txt at extraction (kills found(bundled)).

Phase 3 — pure-PE WoW64 Proton 11.0 from source (CLOSED 2026-08-12)

  • Containerized build of Valve's proton_11.0 source with the branch-pinned steamrt4 SDK (not soldier) via rootless podman → staged as compatibilitytools.d/steamflow-proton-11.0-purepe (1.4 GB).
  • Pure-PE gate: 0 ELF-32 files (611×PE32 i386-windows + 613×PE32+ x86_64-windows) — the official depot's 35 ELF-32 .so multilib dependency removed.
  • E2E-verified: RE2 boots live under pure-PE wine (isolated wineserver, vkd3d-proton swapchain, no i386-multilib anywhere).
  • Patch record: docs/architecture/patches/proton-11.0-makefile-pure-pe.patch.

Runner fixes included

  • Auto-fallback Shared → PerGame prefix mode on Steam-runtime/game runner mismatch (with visible warning).
  • PerGame background Steam spawn now uses the game's runner (pure-PE prefix lacks wow64.dll; wine-tkg Steam → exit 53).
  • symlink_metadata to prevent EEXIST on dangling DLL symlinks.

Files

  • 25 files changed, +3,326 / −34
  • Key additions: src/runner/proton_abi.rs, src/parity.rs, src/headless.rs, docs/architecture/valve-stack-replication.md, docs/architecture/phase3-pure-pe-proton11.md, vendor/steam-cdn/src/utils/zstd.rs (ZSTD chunk support for modern Proton depots)

Test Plan

  • cargo test — 96 unit + 37 integration tests, 0 failures
  • cargo build --release — clean (local, 2m09s); CI rust.yaml also builds + packages .deb
  • ✅ Live E2E: steamflow test-launch 883710 → RE2 renders (windowed, 1926×1112)
  • ✅ Parity: steamflow test-diff 883710 → 0 missing / 0 mismatched / 2 matched

Docs

  • docs/architecture/valve-stack-replication.md — directive, phases, findings
  • docs/architecture/phase3-pure-pe-proton11.md — full Phase 3 record (build, gates, E2E evidence)

weter11 added 11 commits August 12, 2026 16:52
…on_11.0 source cloned, pure-PE gate corrected

Phase 3 bootstrap status report (branch phase3/pure-pe-proton11):
- container prereqs: no podman/docker installed; apt candidate 4.9.3;
  user in sudo group; install blocked on sudo password (user action)
- SDK image correction: proton_11.0 pins steamrt4/sdk/x86_64:
  4.0.20260331.220802-0 (NOT soldier/sdk as the task spec said);
  verified anonymously pullable via Docker Bearer token flow
- source workspace: /home/wer/devis/steamflow-phase3/proton, proton_11.0
  @ 0745bfb, 23 submodules, 3.9G (shallow clone, disk 19G free)
- pure-PE gate corrected from source: Valve's Makefile always creates
  i386-unix/ with ONLY the 64-bit wine64 loader (--enable-win64, PE-only
  32-bit side); classic-wow64 discriminator = 32-bit ELF .so files in
  i386-unix/ (verified against official 11.0 depot on disk)
…E path identified

Phase 3 execution (branch phase3/pure-pe-proton11):
- podman rootless solved: uidmap installed (user), slirp4netns missing
  -> ~/.local/bin/podman wrapper injects --network=host (host networking
  sufficient: container only fetches gecko/mono/xalia over HTTPS)
- image_copy_tmp_dir -> /home/.cache/podman-image-tmp (root / is 31G,
  was filling during image unpack)
- steamrt4 SDK 4.0.20260331.220802-0 pulled (~7G)
- configure.sh OK with --container-engine=~/.local/bin/podman
- make module=winex11.drv module: MODULE_EXIT 0 (after regenerating
  stale src-wine/include/wine/server_protocol.h via perl tools/make_requests)
- CRITICAL: Valve default build = classic-wow64 (obj-wine-i386 is a full
  --host=i686-linux-gnu ELF-32 tree) — NOT pure PE; the i386-multilib-free
  goal needs ARCHS without i386-unix (single 64-bit tree with
  --enable-archs=i386-windows,x86_64-windows). Build-config patch to
  Proton's Makefile.in, no wine source changes.
- -j16 hung the 14G-RAM box (kaldi parallel gcc); -j8 completed
…e scan

Pure-PE WoW64 proof completed on phase3/pure-pe-proton11:

Build-config patch (proton/Makefile.in, 3 edits, no wine source changes):
- ARCHS drops i386-unix -> single 64-bit tree configures
  --enable-archs=x86_64,i386 --enable-win64 (both PE halves cross-compiled)
- kaldi i386<->x86_64 serialization edges gated on i386-unix in ARCHS
- module64 aliased to module32 (single tree builds both PE halves; the
  concurrent recipes raced on shared archives: libntdll.a file truncated)

Result: make -j8 module=winex11.drv module -> MODULE_EXIT 0
Gate scan (obj-wine-x86_64/dlls/winex11.drv):
- x86_64-windows/winex11.drv = PE32+ ; i386-windows/winex11.drv = PE32
- only .so is winex11.so = ELF 64-bit (unix side)
- ELF 32-bit files: 0 ; i386-unix dirs: 0 (whole build tree)
Disk: freed 10G->14G by removing dead i386-unix obj/dst trees (kept
i386-windows PE ones: dxvk/vkd3d/vkd3d-proton/dxvk-nvapi). p2-* research
dirs untouched.

Patch artifact: docs/architecture/patches/proton-11.0-makefile-pure-pe.patch
(apply with: patch -p1 < ... on a proton_11.0 checkout)
…ity harness, VERSIONS.txt visibility, native seed_prefix

Phase 2 of the valve-stack directive (docs/architecture/valve-stack-replication.md):
- items 1-2 (prior session): steam-cdn zstd/PICS fixes, test-diff harness
  (src/parity.rs), native Rust Proton ABI (src/runner/proton_abi.rs), headless
  test commands (src/headless.rs)
- item 3 (tail): utils::write_runner_versions_txt writes VERSIONS.txt at runner
  extraction (GitHub tarball install, steam-cdn install_game, headless
  test_download_proton) — harvests tarball component version files + stamps
  RUNNER_VERSION; kills the found(bundled) display bug for flat-layout Proton
  trees; never overwrites an authoritative existing VERSIONS.txt
- seed_prefix wired into prepare_prefix: fresh game prefixes seeded natively
  from runner files/share/default_pfx (no external Python init scripts)
- i386-multilib permanently REJECTED (host is pure 64-bit) — recorded in dev book
- test-diff 883710 re-run: 0 missing / 2 matched (PROTON_FORCE_LARGE_ADDRESS_AWARE,
  PROTON_USE_WINED3D resolved); EXTRA bucket = 2022-log format asymmetry
- tests: 93 lib + 6 integration, all green
…ymlinks

deploy_dll_symlinks checked dest_path.exists(), which follows symlinks and
returns false for dangling links (e.g. a link to a runner dir that was
renamed/removed). The stale link was never removed, and the subsequent
symlink() call failed with EEXIST, aborting PreparePrefix. Switch the
existence check to symlink_metadata() (succeeds on dangling links) so
stale links are removed and re-pointed like any other symlink.
PerGame mode seeds the per-game prefix (compatdata/<appid>/pfx) from the
game runner's default_pfx via seed_prefix. The background Steam client was
then hardcoded to the configured Steam Runtime Runner (default wine-tkg,
classic-wow64), which cannot initialize inside a pure-PE-seeded prefix:
wine-tkg's loader expects wow64.dll (unix-side WoW64 shim) which pure-PE
wine deliberately does not ship → 'init_wow64: could not load wow64.dll'
→ Steam exits 53 and the launch aborts at PreparePrefix.

Use the game's active runner for the background Steam in PerGame mode so
the client matches the wine family that owns the prefix. Shared mode keeps
the configured runtime runner (it owns the master prefix). Verified:
test-launch 883710 resolves WINEPREFIX to compatdata/883710/pfx, Steam
spawns without exit 53, and the game launches under the pure-PE runner.
…staged, E2E-verified on RE2

- phase3-pure-pe-proton11.md: bootstrap status → final record (build,
  blocker fixes, staging gates, live E2E, known limitation, open items
  all closed)
- valve-stack-replication.md: Phase 3 INITIATED → CLOSED; header + open
  questions updated; SDK corrected to steamrt4 (branch pin), not soldier
…atch

Shared prefix mode cannot host two different Wine/Proton runners in one WINEPREFIX (wineserver protocol mismatch -> 'wine client error: version mismatch'). effective_game_proton no longer force-equals the game runner to the Steam Runtime runner, so a Shared config with runtime runner = wine-tkg and game runner = steamflow-proton-11.0-purepe would crash at launch.

Add effective_prefix_mode(ctx) / effective_prefix_mode_impl: apply the configured mode (per-game user config -> launcher default) and, when Shared AND the Steam Runtime runner path differs from the game runner path, fall back to PerGame with a visible warning. Thread the effective mode through every prefix decision: steam_wineprefix_for_game (new Option param), background-Steam spawn, CEF-enforcement prefix, symlink-deployment stage, registry-fixup stage and DLL-provider detection. UI management buttons and launch_custom_exec keep the configured mode (None).

Tests (src/infra/runners/tests.rs): Shared + mismatched runners -> PerGame; matching runners -> Shared; no runtime runner -> Shared. Full lib suite (96) + launch_verification_tests (3) pass.
A prefix seeded by an older runner keeps absolute symlinks into that
runner's lib/wine tree (system32/*.dll, syswow64/*.dll). When the runner
dir is renamed/removed (e.g. swapping the vendored cachyos copy for the
pure-PE build), every builtin link dangles and ANY wine — game runner or
background Steam — dies before launch with:

  wine: could not load kernel32.dll, status c0000135  (exit 53)

seed_prefix only seeds when system.reg is absent, and deploy_dll_symlinks
only covers game-DLL providers, so the broken prefix was never repaired.

Add utils::repair_dangling_prefix_symlinks(): walks system32/syswow64,
re-points dangling lib/wine links at the ACTIVE runner's equivalent file,
and drops links the active runner doesn't ship. prepare_prefix runs it on
every launch (no-op scan on healthy prefixes). Regression test covers
repoint / drop / untouched-healthy / idempotency.
@weter11

weter11 commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

Follow-up commit (2026-08-12): prefix self-heal for dangling builtin DLL symlinks

User-facing bug this fixes: background Steam died 1s after spawn with exit status: 53 — the FATAL hint said 'corrupted Windows Steam install', but the real cause was a stale prefix, not Steam:

  • The RE2 prefix (compatdata/883710/pfx) was seeded while the game used the old vendored steamflow-proton-11.0 runner; every builtin DLL symlink (kernel32, ntdll, user32…) is an absolute link into that runner's lib/wine tree.
  • That runner dir no longer exists (swapped for steamflow-proton-11.0-purepe) → 599/609 system32 links dangledwine: could not load kernel32.dll, status c0000135 → exit 53, regardless of which wine runs (wine-tkg failed identically).
  • seed_prefix only seeds fresh prefixes (system.reg gate) and deploy_dll_symlinks only covers game-DLL providers — nothing repaired the broken builtin links.

Fix: utils::repair_dangling_prefix_symlinks() — walks system32/syswow64, re-points dangling lib/wine links at the active runner's equivalent file, drops links the active runner doesn't ship. prepare_prefix runs it on every launch (no-op on healthy prefixes).

Verified: new unit test (repoint/drop/untouched-healthy/idempotent) + live: after repairing the RE2 prefix (1,306 re-pointed, 16 dropped), the exact background-Steam invocation boots steam.exe under pure-PE wine — was exit 53 in ~1s, now alive past 20s (CEF/explorer/uiautomation initializing).

Also: the app-level exit-53 message is now only reachable when the Windows Steam install is genuinely broken — the stale-prefix case self-heals first.

…ime runner

Exit-1 regression after the pure-PE split (RE2 launch, 2026-08-13):

PerGame mode spawned background Steam with the GAME's runner (purepe) in
the per-game prefix. The Windows Steam client CANNOT boot under purepe
(documented 2026-08-12: CEF GPU-process crash + 32-bit network-init
stall — two independent purepe defects), so the client died ~2s after
spawn (exit 1, no Steam logs) and the launch aborted at PreparePrefix.
The stale-wineserver guard also targeted the per-game prefix, and the
'is Steam running' check looked at the per-game prefix — so even with
the user's wine-tkg client running in the master prefix (via Manage),
SteamFlow spawned a doomed duplicate.

Fix — the client ALWAYS belongs to the master prefix under the Steam
Runtime runner (wine-tkg), in Shared AND PerGame mode:
- PerGame mode: prefix_steam_dir/steam_wineprefix now resolve to the
  master Steam dir + master prefix (the per-game prefix still receives
  the client-file deployment for STEAM_COMPAT_CLIENT_INSTALL_PATH).
- steam_runner: always the configured Steam Runtime runner (wine-tkg),
  never the game's runner.
- Stale-wineserver guard: targets the GAME prefix (effective_game_prefix),
  so it can't kill the running master client.
- Deployed client files (steam.exe, steamclient*.dll) are now REFRESHED
  from master when a stale real-file copy exists (the per-game prefix had
  a Feb-14 steam.exe that self-exits with code 1; only symlink-or-copy-if-
  missing meant stale copies persisted forever).

Verified: test-launch 883710 → background Steam under wine-tkg, ready
signal in 6s, effective_steam_wineprefix = master_steam_prefix/pfx,
game launches under purepe, swapchain created, session result: Success.
@weter11

weter11 commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Follow-up (2026-08-13): background Steam exit-1 fix — client always in master prefix under runtime runner

Your reported bug: Play button → FATAL: Background Steam exited after 2s with: exit status: 1 (and Manage-then-Play also failed).

Root cause (two compounding):

  1. Wrong runner/prefix for the client in PerGame mode. The client was spawned with the game's runner (purepe) in the per-game prefix. The Windows Steam client cannot boot under purepe (documented 2026-08-12: CEF GPU crash + network-init stall — two independent purepe defects) → dies in ~2s before writing any Steam logs. Even when the user's wine-tkg client was already running in the master prefix (via Manage), the running-check looked at the per-game prefix, missed it, and spawned the doomed duplicate.
  2. Stale client file. The per-game client-file deployment only symlinked if !dst.exists() — the prefix held a Feb-14 steam.exe that self-exits with code 1. Verified: replacing it with the master's Aug-3 steam.exe → client boots and stays alive under purepe.

Fix (commit b5f5c0a): the client ALWAYS runs in the master prefix under the Steam Runtime runner (wine-tkg) — Shared AND PerGame. The per-game prefix keeps only the client-file deployment (for STEAM_COMPAT_CLIENT_INSTALL_PATH). Stale-wineserver guard now targets the game prefix (can't kill the running client); deployed client files are refreshed from master when stale.

Verified live: test-launch 883710 → background Steam under wine-tkg, ready signal in 6s, effective_steam_wineprefix = master prefix, game launches under purepe, DXVK cache + swapchain created, session result: Success. Full test suite: 134 passed, 0 failed.

⚠️ User action needed: rebuild/reinstall from this branch — the app binary you're running (c376560) still has the old PerGame spawn logic.

…window self-exit

b5f5c0a parked the background Steam client in the MASTER prefix while
games run in per-game prefixes (pure-PE). Wine named pipes are
per-wineserver, so the game's steamclient.dll cannot reach the client's
pipe → SteamAPI_Init fails → RE2 self-exits ~3s after the window appears
(clean MFShutdown, no crash) and Portal 2 shows 'Steam must be running'
even while Steam runs. b5f5c0a's 'verified' check (Steam ready in 6s +
swapchain) used the same shallow 2s alive-check that masks the exit.

Revert the placement to 8a56ed2 (client spawns with the game's runner in
the per-game prefix — same wineserver as the game), keep b5f5c0a's
client-file refresh (the real exit-1 fix was the stale Feb-14 steam.exe),
and keep the stale-wineserver guard on the game prefix.

launch_verification: two-phase check — 2s fast-fail for instant crashes,
then a sustained-liveness window to 8s (500ms polls) so a graceful
3-5s-after-window self-exit is reported failed_after_spawn with the real
lifetime instead of 'Success'. New test
test_launch_verification_graceful_self_exit_caught covers the 4s case the
old 2s check missed.
SteamClient::sync_master_session_to_prefix() copies the master client's
auth state into a per-game prefix's Steam dir before the headless client
spawns there:
- config/loginusers.vdf (account registry, AutoLogin/RememberPassword,
  fresh Timestamp) — copied wholesale
- config/config.vdf Authentication block (RememberedMachineID JWT) —
  merged, preserving the target's other per-prefix keys
- ssfn* sentry files (legacy) — copied when present
- HKCU\Software\Valve\Steam section of user.reg (AutoLoginUser, ...) —
  per-key overlay, preserving target keys/subkeys and EOL style

Guards: no-op when the master client has no session; never downgrades a
per-game login as fresh as (or fresher than) master's; non-fatal.

Wired into the PerGame spawn path (wine_tkg.rs) right before the client
spawn, only when no client is already running in the target prefix.

Verified: 8 new unit tests (VDF/registry merges, freshness guard, e2e
sync) + full suite 105 lib tests green; live run confirmed the 620 prefix
receives master's loginusers.vdf timestamp (1786715012), the fresh
RememberedMachineID JWT, and the registry login keys.

DOES NOT achieve a logged-in per-game client end-to-end. The purepe-hosted
client asserts ClientAPI_InitGlobalInstance (client_api.cpp:601, "missing
a 32-bit dependency of steamclient.dll") on every boot and stays anonymous;
under wine-tkg it connects to Steam CM but the copied machine token is
rejected ("Invalid Password"). Open architecture question recorded in
docs/architecture/phase3-pure-pe-proton11.md §Session-auth sync (2026-08-14).
…(2026-08-14)

Sync machinery committed separately (feat 1543484). This section states the
CONCLUSION (per-game client process required; pointer-only model disproven by
b5f5c0a), the two Portal 2 blockers with evidence (purepe client_api.cpp:601
assert dumps; wine-tkg CM connect + Invalid Password token rejection), the
MachineGuid seed_prefix doc/code mismatch as an unfixed bug, and the single
next step (MachineGuid cross-prefix injection test under wine-tkg).
…uct decision (2026-08-14)

Replaces the "single next step" with the executed result and corrected framing:

- MachineGuid cross-prefix injection test EXECUTED, verdict: NOT sufficient.
  Three-run table (620's own GUID -> server-side Invalid Password; master's
  GUID injected -> local 'cached creds not available', never attempts; own
  GUID + interactive login -> 'OK' + fresh JWT persisted). MachineGuid
  affects which failure mode the client hits but is not the auth binding;
  seed_prefix() doc/code mismatch stays open as a lower-priority real bug.
- Working auth mechanism: one-time interactive login per prefix (client's
  own key store cannot be file-synced). Fresh machine-bound JWT iss
  r:0001_28A6B125_2FF1F vs master's r:0012_28A6B9E0_68319; AutoLogin=1
  persists. Portal 2 now boots past SteamAPI_Init, loads p2-rtx chain on
  RTX 3070.
- PITFALL recorded: disk-full (100%) causes post-login client crash
  'Failed to create thread (error 0x3e6)' threadtools.cpp:3870; JWT never
  persists. Backlog item: defensive free-space check, NOT fixed this session.
- Next blocker #1: Shared/PerGame wineserver routing gap in the custom-exec
  path (launch_custom_exec passes None to steam_wineprefix_for_game,
  bypassing effective_prefix_mode's runner-mismatch guard -> Shared config
  resolves to master prefix; per-game logged-in client never detected).
  Decision logic verified in code; fix NOT yet implemented.
- Next blocker #2: winproc 1400 wine-11 get_window_thread() regression
  (pre-existing, patch unbuilt) - game exit code 5 at Remix bridge handshake.
- OPEN PRODUCT DECISION recorded (not resolved): one-time manual login per
  prefix as shipped default vs opt-in advanced/isolated session mode.
@weter11
weter11 merged commit 39699b3 into main Aug 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant