Skip to content

Feature Wayland, GameScope, Steam - #769

Merged
maxjivi05 merged 117 commits into
WinNative-Emu:mainfrom
maxjivi05:feature/wayland-gamescope
Sep 23, 2026
Merged

maxjivi05 merged 117 commits into
WinNative-Emu:mainfrom
maxjivi05:feature/wayland-gamescope

Conversation

@maxjivi05

Copy link
Copy Markdown
Contributor

No description provided.

Port the embedded Wayland compositor (libbannerwayland) and the winewayland
launch path written by Banner (The412Banner) for Bannerlator into WinNative.

- Native: waylandcomp compositor with pre-generated protocol glue, vendored
  libwayland-server 1.26 and libffi, screen-effect shader headers, frame
  generation stub; JNI bound to com.winlator.cmod.runtime.display.wayland.
  New per-session begin/end on the long-lived compositor thread so a session
  can end and a new one start without killing the process, and a
  configurable session log directory.
- Settings: Display Server (X11/Wayland) dropdown in the Display section of
  Container Settings and Shortcut Settings. Shortcuts follow the container
  until changed and saved; Wayland is only selectable when the device has an
  Adreno GPU and the selected Wine/Proton ships winewayland and the Wayland
  Turnip. Strings added for every locale; PaneNav handled by SettingDropdown.
- Launch: display backend resolved per session with an X11 fallback and
  toast, compositor surface over the idle X view, X server input sink and
  pointer-lock relative mode, WinHandler relative mouse routing, clipboard
  and soft-keyboard text bridges, FPS limit and fullscreen stretch
  forwarding, FPS HUD feed, guest environment (WAYLAND_DISPLAY,
  XDG_RUNTIME_DIR, Proton lib path, Wayland Turnip variant by GPU,
  GALLIUM_THREAD=0), prefix registry driver selection with self-healing,
  winex11.drv disabled through WINEDLLOVERRIDES instead of renaming files.
- Session lifecycle covers exit, forced cleanup, activity destruction and
  background session reattach.
- Docs in docs/WAYLAND-DISPLAY.md; credit to The412Banner in CREDITS.md.
The Graphics tab now tells apart a non-Adreno device from a Proton that lacks
winewayland and the Wayland Turnip, names Banner's Wayland Proton layer as the
fix, and no longer cuts the explanation off after three lines. The doc points
at the stock Proton as the reason the dropdown stays on X11.
…les into any arm64ec Proton

A Wayland launch hung at Starting Wine because nothing created the compositor's
runtime directory, so wayland-0 was never bound and winewayland waited forever.
The session now makes the directory before the compositor starts, and the guest
launcher extracts the xkb keymap and waits up to eight seconds for the socket
before exec.

An arm64ec Proton that lacks winewayland can now borrow the driver, the Wayland
Turnips, the client libraries and the xkb data from an installed Wayland Proton
of the same Wine major version. The Display Server row offers Wayland and names
the donor, saving Wayland runs the copy on a worker thread with a toast on
completion, and a launch that still finds the files missing starts the copy and
runs on X11 that time. Files land through temp names so a half copy never reads
as capable.
…ng at startup

Seeding HKCU Software\Wine\Explorer Desktop=shell made every process of the
session start on that desktop, so a second explorer raced the launcher for the
desktop window. The loser's window creation was aborted by WM_NCCREATE, Wine's
explorer never entered its desktop message loop, it returned immediately, and
the guest wrapper exiting tore the whole session down at Starting Wine. Device
traces show the same launch on X11 creating the window and entering the loop.
Only the Desktops\shell size entry is written now; the desktop window is
created, the message loop runs and the session stays alive.

Also on Wayland: the X server is no longer started, a stale prefix is brought up
to date before the session instead of blocking explorer behind wineboot, and the
launch overlay is cleared on a timer since no first frame arrives for a desktop
that only presents shm.

Turnip manifests are written under WinNative's own wayland_turnip[_variant].json
names and winewayland is pointed at the chosen one, so no donor branding is
relied on.
…on file copy

A shortcut or container left on the System graphics driver started the
compositor on the stock Vulkan driver, which has no
VK_EXT_image_drm_format_modifier, so vkCreateDevice failed and the session
presented nothing. The compositor driver now falls back to the container's own
driver and then to any installed one, and says which it picked.

Copying winewayland.so into a Proton that does not ship it is removed. It is a
Wine unixlib bound to the win32u it was built against: the borrowed driver
loads, connects to the compositor and enumerates its outputs, but creates no
window, so the session ran with a black screen and nothing in the log said why.
A Proton now has to ship its own Wayland driver, which the Display Server row
already states.

Device-proven on an Adreno 840: NieR:Automata runs on the Wayland layer at
around 59 fps with its window on the compositor's virtual desktop.
winewayland.so in an x86_64 layer needs x86_64 Wayland, xkb and their
transitive libraries; the container image only has aarch64 copies, so the
layer vendors its own. Shipping them in the Proton's lib/ put x86_64
libc++_shared.so and libz.so.1 ahead of the image for the native box64
binary, which then refused to link and the session ended with no window.

They now live in lib/wayland-x86_64/, which only Box64 is pointed at.
The Bionic launcher's call is also moved after the two library paths it
was being overwritten by.
An x86_64 layer's winewayland.so runs fine under Box64 and drives the
compositor, but it has to dlopen the Wayland Turnip to give the game its
ICD and that Turnip is aarch64, so the pin fails and the session is a
desktop that never presents a frame. Binding the driver to Box64's
native libwayland-client.so.0 wrapper instead shares one connection but
faults at the entry to the driver's event thread.

The x86_64-unix fallback in the capability check is dropped, so those
layers keep the Display Server control on X11, where DXVK reaches the
GPU through Box64's Vulkan wrapper.
The Wayland backend compiled a stub for its frame-generation engines, so
the compositor reported both engines unavailable and never generated a
frame. Build the X11 renderer's own engines into libwnwayland instead and
drive them from the compositor's Turnip device:

- framegen_engine.c implements the fge_* contract over vkr_lsfg and
  vkr_dis. Their sources are compiled into the compositor rather than
  linked from libwinlator, because each library keeps its own VkDispatch;
  -fvisibility=hidden keeps the two copies apart.
- vkd_bind_proc() binds VkDispatch from a vkGetInstanceProcAddr the
  caller already holds, which is how the compositor reaches Turnip
  through adrenotools.
- The bridge's engine tuning took Win-FG's model/preset pair and clamped
  DIS's flow resolution and target rate into it, which turned 180/120
  into 4/2 and planned nothing. It now carries both values through.
- The generation ring was sized to multiplier-1, capping Wayland at 2x
  while X11 reached the configured target rate. With a target rate set,
  size it to the engine maximum and let the pacer choose, as X11 does.
- frameGenRefreshRate was never assigned on the DIS path, so the pacer
  saw a 0 Hz panel.
- The HUD's output-frame source and the system frame-generation monitor
  read the compositor's cumulative counters in Wayland mode; the X11
  renderer holds none there.

Also drop the upstream project's name from source filenames and
identifiers - it belongs in CREDITS.md, not in our tree. The
banner_desktop_v1 and banner_ahb_v1 interface names and the
BANNER_WAYLAND_* environment variables stay: the shipped winewayland.so
binds those strings, and renaming them would break every existing
Wayland Proton layer.

Verified on device with Monster Hunter Stories at 30 fps, target 120:
LSFG and DIS each reach 120 fps on both Wayland and X11.
The HUD showed "Vulkan" for every Wayland session, whatever the game ran
on. It reads the name from an X11 window property that the Vulkan wrapper
sets on the game's window, and a Wayland session has neither half of
that: the game never makes an X11 window, and winewayland.drv repoints
VK_ICD_FILENAMES at the bundled Turnip, so the wrapper is not even
loaded. The field kept its "Vulkan" initialiser for the whole session.

The container's own processes carry the same answer - whichever
translation layer the game runs on is mapped into it - so read it from
/proc instead, ranked so the highest-level layer wins: a VKD3D title maps
dxgi as well, and a store front end maps its own Direct3D before the game
maps its. Polling continues after the first answer so that second case
corrects itself.

The probe runs on its own thread; a tick reads about a megabyte of /proc.
It touches the HUD only through the UI thread, and only when the name
actually changes. The Mango HUD's engine label comes off the same field,
so it follows.

Verified on device: Monster Hunter Stories on Wayland now reads DXVK,
where it read Vulkan before.
Switching a shortcut from a container on an x86_64 Proton to one on an
arm64ec Proton left the Wayland row greyed out with the "needs a Wayland
Proton" text until the sheet was saved and reopened.

The row keys its capability check on state.wineVersionIdentifier, and
handleContainerChanged updated isArm64EC and the version display but not
the identifier, so the check never re-ran and kept the old container's
verdict. Move the identifier with the container, as loadInitialData does.

The identifier has no other reader, so nothing else changes.
A game presenting through the compositor drew far more frames than reached
the screen: 1780 in ten seconds for 908 shown, measured on a session log.
A swapchain that does not wait for frame callbacks is paced only by how
fast its buffers come back, and a replaced buffer went back the moment it
was replaced, so the game ran as fast as the GPU allowed and everything
above the refresh rate was thrown away. X11 never did that - there a buffer
returns only once the scene using it has been composed.

Release a replaced buffer on the screen's cadence when the session has no
frame rate limit of its own, through the limiter's existing schedule. The
same session now produces 1076 frames for 1010 on screen.

Two more things a Wayland session did that an X11 one did not:

The vsync ticks ran on after the output surface was gone, so a backgrounded
session woke the UI thread and the compositor thread at the panel's refresh
rate to draw on a screen it no longer had. X11 parks its render thread at
the same point; stop the ticks with the surface.

The renderer the HUD names was found by reading every process on the device
every couple of seconds. The compositor already reports the process behind
the window it is showing, so read that one instead, and keep the sweep only
for when there is no pid to go on.
Two orphaned helper processes were found on a test device with three
threads spinning at 100%, holding 3.2 GB, having survived every session
teardown for the better part of seven hours - 20.9 CPU-hours between them
and a load average of 19 on an idle device.

The session sweep matches a process against a list of Windows executable
names, over its stat line and its command line together. A Chromium helper
(Steam's web helper is one) clears its own argv, so the command line is
empty and the only mark it carries is its name, which is not a Windows
executable name. Nothing in the list could match, so the sweep never saw
these processes at all, let alone signalled them.

Match those names too, for a process of this app's own uid and not this
process. The sweep walks all of /proc, so a name on its own is not enough:
a foreign process cannot be signalled, and terminateSessionProcessesAndWait
waits while its list is non-empty, so one such match would spend the whole
timeout on every teardown. WebView's renderers carry Chromium's names as
well and are excluded by the same test, running as they do under an
isolated uid.

The web helper is listed as a core process so background pausing leaves it
running; its name joins that list too, now that the sweep can see it.
The Add dialog now takes any Windows or Linux executable (.AppImage, .sh,
.run, .bin, .elf and the .x86_64/.aarch64 build suffixes) alongside console
ROMs, and asks whether the entry is a game or an application. The answer is
stored as the library_type extra and the Games / Applications filters honour
it; custom entries previously ignored the filters altogether.

Linux entries are written with runtime=linux and Exec=linux:native. There is
no Linux runtime yet, so starting one reports that instead of handing the
path to Wine. docs/GAMESCOPE.md records what gamescope is, what the
compositor already provides for its Wayland backend, and the phases to a
Linux runtime and gamescope session.

The hard-coded English in the dialog and its toasts moved to resources,
translated for every locale.
Its display is an unprivileged app over SurfaceControl; root is only for
the chroot rootfs. gamescope runs nested in a small dma-buf-only Wayland
compositor with Android input bridged as wl_seat events, which is the
shape this plan already has. The launch command, env and the native arm64
Steam path are recorded so Phase 2 and 3 do not rediscover them.
Container Settings and Shortcut Settings gain a Runtime row: Wine, or
Gamescope, stored as the runtime extra with the same per-shortcut override
as Display Server. A Gamescope container starts the Wayland compositor and,
in place of Wine, execs proot over files/linuxfs running gamescope
--backend wayland --expose-wayland as a client of it, with the session
script deciding what gamescope hosts: the file manager from Edit
Containers, a Linux program from a library entry, or the native arm64
Steam client for a Steam entry. Windows entries are refused with a
translated message. A background session records that it is a Linux one so
a reattach resolves to gamescope.

proot (the Termux-derived tree already in cpp/) joins the CMake build as
libproot.so and libproot-loader.so. The loader is linked freestanding at a
high image base, statx is path-translated, and PROOT_NO_SECCOMP disables
the seccomp accelerator for debugging.

tools/linuxfs/build-linuxfs.sh assembles the runtime from Arch Linux ARM
packages (gamescope 3.16.29, Xwayland, Mesa with Turnip and Zink, pcmanfm)
plus the session scripts and the fake /proc files Android denies. It ships
no Valve software; winnative-steam-install fetches the native arm64 client
from Valve's client-update manifest at first use.

gamescope refuses a wl_seat older than 8, so the compositor advertises 9
and sends axis_value120 to version 8+ pointers. Verified on the build
machine: the full launch line runs from the rootfs under qemu-user as a
client of a nested sway up to gamescope's Vulkan allocation, which
lavapipe under emulation cannot satisfy.
…client

Verified on the NP06J (Adreno 840, Android 16): gamescope, Xwayland with
glamor on Zink and the Steam arm64 client all run inside the proot rootfs
as the app's own uid, and the Steam client updates itself and reaches
steamwebhelper.

- proot: emulate set*uid/gid and setfs*id under the app seccomp policy
  (Xwayland's xkbcomp and the X access control need them).
- compositor: xdg_toplevel fullscreen configure (gamescope drops its
  libdecor frame), wl_output name, and seat events to every pointer and
  keyboard object of a client (gamescope reads input on a second pair).
- Turnip: cross-built Mesa 26.2.2 with the KGSL backend, reporting the
  KGSL device as its DRM node so gamescope offers linux-dmabuf and the
  WSI agrees on a main device (build-turnip.sh, turnip/*.patch).
- LinuxRuntime: present /dev/kgsl-3d0 as /dev/dri/renderD<n> with the
  sysfs entries libdrm reads; passwd/group for the app uid; fake
  overflowuid/gid for glycin's sandbox.
- libwnsession.so, preloaded into the session: System V IPC, GEM handles
  for the KGSL node, get_robust_list for Steam's cross-process mutex,
  and loopback port ownership for Steam's lsof peer check.
- rootfs: GTK 2 from Debian for steamui.so, openal, libvdpau, lsof,
  DejaVu fonts, pacman hook equivalents; Steam gets the ~/.steam links
  steam.sh makes and restarts on exit code 42.
The client splits lsof's address on "->" and drops the record unless it
gets two halves, so the single-address answer left it with no peer pid and
it refused steamwebhelper's websocket (Checked: 0/<pid>). Record the far end
of every loopback socket and print source->destination the way lsof does;
posix_spawn reports a failed fork instead of success with no pid.
execve of a script used to reach the kernel as-is and fail with ENOEXEC unless
bash's fallback caught it; Steam's compat tools are python scripts and need
argv rewritten the way the kernel does it, up to four interpreter levels.
The Linux runtime now preloads the fake evdev layer built against glibc, with
the input rings bound in as /dev/input, so Steam and SDL see the on-screen and
physical controllers as evdev gamepads. Games downloaded through the app are
presented to the native client as a second library folder at /mnt/winnative:
each install directory is bound in under its Steam name, its manifest copied
alongside, and winnative-steam-library registers the folder in
steamapps/libraryfolders.vdf. Steam's arm64 launcher also wants the sdkarm64
and binarm64 links.
The runtime is no longer a per-container dropdown that a save could silently
rewrite; instead Settings > Containers has its own GameScope section that
creates one Wayland container, and the library carries a Steam entry that boots
into it. Edit Container and Shortcut Settings keep the display-server choice but
drop the runtime row, and the GameScope container is hidden from the setup
wizard's Wine checks.
execveat(2) was absent from the seccomp trap table, so the tracer never saw it
and guest calls fell through to the kernel; FEX launches every emulated child
that way, which crashed x86 processes that spawned others (wine starting
wineserver). Trap it, and at enter rewrite it into the execve the loader path
already handles: the target becomes SYSARG_1 (named through /proc/self/fd for an
fd-relative call, then resolved to the real path), argv and envp shift down from
SYSARG_3/4. A program run as /proc/self/fd/N or by execveat now maps correctly.
The kernel here has no user namespaces, so pressure-vessel cannot build the
container Proton normally runs in. Instead winnative-fex-rootfs completes the
Steam Linux Runtime platform tree in place (its mtree names the symlinks and
deduplicated objects the depot ships unpacked) and uses it as the x86_64 root
FEX emulates against, and winnative-proton is registered with the client as a
compatibility tool that runs Proton's own script there, with Vulkan and GL
thunked to the host drivers. Verified: Proton builds a full prefix and wine
starts its child processes.
The entry showed the generic placeholder: the icon was written only when the
shortcut was first created, and a square image is cropped by the wide card.
Render both sizes whenever the entry is ensured, and give the card a banner-
shaped cover with the glyph centred on the client's own dark ground.
Two things kept Windows games from starting in the GameScope session.

The app sandbox's seccomp filter answers openat2() and faccessat2() with ENOSYS, and a filter
returning an errno outranks proot's trace action, so the tracer never sees the call and cannot
answer it either. FEX resolves every guest path with openat2(rootfs_fd, path, RESOLVE_IN_ROOT);
denied that, it falls back to the host's own root and the guest loader finds none of its
libraries. libwnopenat2.so, preloaded into the FEX processes alone, answers both with the
syscalls the filter does allow, walking a rooted resolution component by component so that an
absolute symlink inside the runtime tree lands back at that tree's root.

The Steamworks stub a game loads is then the second half: Proton's lsteamclient runs emulated
and dlopens ~/.steam/sdk64/steamclient.so, which pointed at the arm64 client's tree. An x86_64
process cannot map an AArch64 library, and Steamworks aborted on it. Valve's client ships all
three builds, so each SDK path now names its own architecture; the stub is only an IPC shim to
the running client, which is free to stay arm64.

Run-as processes carry no seccomp filter, so neither fault reproduces in the test rig; both were
confirmed by installing the filter there by hand.
The client died a minute or two into every session with systemd's safe_fclose() asserting on
EBADF, taking the running game down with it. The backtrace puts it in libudev, reached from
SDL's controller discovery by way of libusb: the sandbox denies the NETLINK_KOBJECT_UEVENT
socket every monitor is built on, and libudev unwinds from that through a path that closes a
descriptor a stream still owns. SDL asks often enough - fourteen thousand times in two minutes
on one thread - that hitting it is a matter of when.

Answering with the NULL libudev would have returned keeps the caller on the failure path it
already handles. SDL loads udev through dlopen() and dlsym() rather than the linker, so the
library itself is refused for SDL as well, which is the same answer a system without udev
gives; SDL then reads /dev/input directly, which is where this session's controllers are. The
socket is tried first so both answers stay out of the way wherever a monitor could really be
created, and no other caller is touched - the client's web helper loads udev for its own device
enumeration and does not survive being told it is missing.
… seccomp off

The Poco F6 that cannot start a session fails identically on the build that
carried the fallback, and Bannerlator saw the same three errors from this proot
tree with PROOT_NO_SECCOMP=1 as without. The probe cost every session a proot
start for nothing. The dialog for a session that dies at once stays.
Android's allocator tags heap pointers in their top byte, and the first execve
is made by proot's own child with its path and environment on that heap. A
verbose trace from a Poco F6 (kernel 6.1.138) shows the call entering with
0xb400007b80852f10 and being answered EFAULT: that kernel does not read a
tagged address on a tracer's behalf, so no program ever started. Guest
addresses carry no tag, so masking it changes nothing for them.
Every path proot translates was read with a ptrace call per eight bytes and
written back the same way. process_vm_readv and process_vm_writev do it in one,
as upstream proot does; what the kernel refuses still goes through ptrace, which
can force a read-only page and reports the error.
-r was only passed with a limit, so without one gamescope said 60 Hz and games
offered and capped themselves at 60 on a faster panel. From Bannerlator ec93d55.
@maxjivi05
maxjivi05 force-pushed the feature/wayland-gamescope branch 2 times, most recently from 8ce3f27 to 3eb5968 Compare September 22, 2026 02:13
@maxjivi05
maxjivi05 force-pushed the feature/wayland-gamescope branch from 3eb5968 to dddaa6f Compare September 22, 2026 02:17
A Linux session died the moment it started, on every device: the command line
carried -i <uid>:<uid>, an option upstream proot takes and this tree does not,
and an option it does not know is fatal before any session runs. Three users'
logs are the same three lines.

Identity needs no option here. setuid, setgid, setresuid, setresgid, setreuid,
setregid and setgroups are answered in tracee/seccomp.c, which Android reaches
through its own SIGSYS and through the ENOSYS restart in syscall/exit.c, so it
is not in proot's trace filter and does not belong there.

The check that was meant to cover this ran the host's proot, which does have
-i, so it passed while validating a binary that is not the one shipped. It is
replaced by one that runs the packaged binary with the real option list.
Steam mode started a heartbeat in the background and never took it down. It
outlives the script by up to an hour, and on the way out gamescope's reaper
adopts it and waits for it; gamescope is proot's root tracee, so proot never
exits, waitFor never returns, and the screen stays black with no way back to
the library. The sweep afterwards is no second chance: it matches processes
already handed to the reaper, and a job this shell still owns is not one of
them. Every background job is now killed and waited for before it runs.

Ending a session goes straight to the kill again. Asking proot first does
nothing, since it answers SIG_IGN to every signal but the fatal few, and its
tracees are traced with PTRACE_O_EXITKILL, so the kernel takes gamescope and
everything under it down with proot. Waiting on a thread of its own only let
the exit path reach killProcess first and leave that tree running.
Only a session that died inside twenty seconds was reported; anything later
closed the screen without a word, which from the outside is a crash, and left
the one person who could send the log with no reason to look for it. Every end
nobody asked for is now said. A status of 137 names Android's limit on
background processes, the likeliest cause of a death mid-session, under the
same name the setup wizard uses for the switch that lifts it.

The log says what it was asked and how it ended: the build that wrote it,
proot's own options, and a signal named rather than left as a number, which
137 otherwise reads as an exit code the session chose. The guest command is
left out, since the container's environment rides in it.

An alert has nothing to cancel, so back or a tap outside now means what OK
means. Without it the callback was skipped and this dialog left the activity
alive over a session that was already gone.
The Steam client records the token that signs its account in to its own
connection log, and Logs Manager hands those files to whoever the user sends
them to. Every way out - the archive, Share and Download - now goes through
one copy that takes it out, and the app's own logs are copied unchanged.

Share writes to a directory of its own, emptied first, so two logs of the same
name cannot be confused for each other.
Android kills an app's background child processes past thirty-two, and a
session runs well past that: killing proot ends it. It is said once, when the
Linux client download starts, because that is when the limit begins to matter
and there are minutes of download to act in. Android 14 and later carry the
switch in Developer options and are offered it; earlier releases get the adb
line, since only adb can set it there.
The driver an entry runs on falls back to the one the Drivers page calls
active when nothing is saved, and to the bundled Mesa when a saved name is
gone, rather than to whichever driver happened to be first.

The game's layer is posted with the cadence the session is aiming for - the
frame limit when one is set, otherwise the panel's rate. It had been posted
with a vote of zero every session, which leaves the system to infer the
cadence from the rate already being achieved; on a phone whose vendor picks
clocks from that, a session that has been slowed reads as one that wants to be.

A Proton download checks for room before it starts and clears a half-finished
tree, and PROTON_USE_XALIA is one of the known variables so Proton's own gate
is reachable without typing the name.
The update check only ever knew the official releases, and only from a build
whose version parsed and whose signature was the release one. A build from CI
is neither, so anyone running one was never told a newer one existed.

A pull request build now follows its own pull request. CI stamps the build with
the pull request it came from and the commit the release notes name, so the
install knows which of the CI releases is its own: pr-769 is offered to 769 and
to nothing else, and 770 to 770. The tag never moves over a pull request's life
and the assets under it are replaced, so there is no version to compare - the
build under the tag is a different one when its commit is not the one installed.
The APK taken is the one matching this build's flavour, as it already was for
official releases. A pull request that has been merged or closed takes its
release with it, and that reads as nothing to install rather than as a failure.

Which releases a build follows is no longer a setting. The two are signed with
different keys, so neither could be installed over the other, and offering the
choice only invited a download that install would refuse. Development went with
it: it offered pre-releases of the official repository, and there are none.
The store services are START_STICKY, so Android recreated them with a null
intent after any process death. Each one registered the keep-alive component
before looking at the intent, which pinned a foreground service and a partial
wakelock with no session, no UI and nobody asking: measured at 5h 2m of
foreground service against 13m 34s of real use, and a wakelock held 8h 39m
across ten spans because the screen-off receiver took it without checking
whether a session was running.

Background chat is the one caller that needs the restart - stopManagedServices
leaves SteamService started while onTaskRemoved kills the process - so Steam
refuses only when that setting is off and there is no login to return to.
libpci chooses its procfs backend on whether it can read the /proc/bus/pci
directory, which the app can, and then opens the devices file inside it, which
Android denies. Its error path is die(), so the process calling it exits(1).
Chromium loads libpci in its GPU process to name the video card, so the GPU
process died six times at every startup - exit_code=256 against six
"pcilib: Cannot open /proc/bus/pci/devices" lines, interleaved one to one - and
what came back was slow enough to halve the Steam UI.

Binding an empty file over it answers the scan truthfully: nothing the app can
see is on a PCI bus. The file is created at session start rather than shipped in
the rootfs overlay so installs that already have a rootfs are fixed too, and the
table's existing guard binds it only when the real file cannot be read, so it can
never stand in front of real data.

Measured on a OnePlus 15, scrolling the Big Picture library: 44 fps before, 85
after, against a gamescope asked for 90, with the GPU process crash count going
from twelve to zero.
GE-Proton and CachyOS Proton import protonfixes from the top of their proton
script, and its esync check opens /proc/sys/fs/file-max with nothing catching a
failure. Android denies that file to an app uid, so the PermissionError came out
of the import and ended the launch before the game started - Steam put the Play
button back with no window and no clue, for every game on either build.

file-max joins the /proc stand-ins the session already binds. protonfixes only
warns below 8192, so what matters is that the read succeeds. Both it and
pci_devices are written at session start rather than shipped in the rootfs
overlay, because an install that already has a rootfs would never receive them.

Verified on a OnePlus 15 with Among Us: on GE-Proton11-7-aarch64 protonfixes
goes from PermissionError to "All checks successful" and the game reaches its
menu at 60 fps. CachyOS 11.0-20260703 clears the same check and brings Unity up
with its full thread pool, but still parks before it presents a window - a
separate fault, not this one.
The Linux Client dialog carried a "Linux Proton builds" link to a second
download screen for the same GE and CachyOS builds that Components already
lists, installs and removes through the same LinuxProtons calls. The dialog was
that link's only entry point, so it goes with it, along with the four strings
that only it used.

Components had no way to stop an in-flight download, which the dialog did, and
these builds are several hundred megabytes; a mistaken tap could otherwise only
be undone by killing the app. So the cancel moves to the component card rather
than being lost with the dialog.
CachyOS turns on its PipeWire driver for every game, and with no PipeWire
server in the session the game hung at audio init behind Steam's spinner.
The launcher now defaults PROTON_USE_PIPEWIRE to 0, leaving an explicit
setting alone; no other Proton reads it.
Every PR comes from a fork, and fork runs get no secrets, so PR CI signed
each build with a freshly generated key. The publish run holds secrets
without running PR code; it now re-signs the APKs with a key kept for PR
builds (PR_KEYSTORE_BASE64, PR_KEYSTORE_PASSWORD, PR_KEY_ALIAS,
PR_KEY_PASSWORD) and warns when that key is not configured.
The winnative-proton tool now runs the Proton Experimental build that
ships with WinNative before any ARM64 depot Steam installed, and Steam
lists it under that name.
Advanced in a GameScope container, and in a game's shortcut, now offers
WN Proton Experimental and the Proton builds downloaded under
Components. The container's choice is the default until a shortcut sets
its own, like the other settings.

A game's choice is kept in step with its Compatibility setting in Steam,
whichever was changed last: a change made in Steam shows in the
shortcut, and one made in WinNative is written into Steam's mapping
when the client is not running. Until then winnative-proton-launch reads
the choice as the game starts, so it applies to a running client too.
…river

The runtime's Turnip gets the device-table fixes WN's Android drivers ship:
early preamble and scalar predicates off on A725/A730, the TP UBWC flag hint
on A740/X1-85, GMEM off on A810/A829, the KGSL ids A810/A829 report, and an
A825 entry. A840, A750 and every other GPU build unchanged.

A GameScope container's driver setting names its Linux Turnip, so the
compositor took whichever Android Turnip was listed first. It now prefers the
WN Turnip the Linux Client install fetched, and falls back as before.
A session draws with the driver of the entry that started it, and every
game the Steam client started inherited that one, so a game's own Graphics
driver was ignored whenever it was started from Big Picture or the Steam
entry. The app now records each game's driver - its shortcut's, else the
container's - and winnative-proton-launch sets it as the game starts.
@maxjivi05
maxjivi05 merged commit 860cf97 into WinNative-Emu:main Sep 23, 2026
5 checks 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