Repository navigation
Releases: superuser404notfound/Polyseat
Release list
Polyseat 0.40.0
No game started out of Steam any more, in any seat. Steam showed it as
running, nothing came up, and the launch held a core at 100 % until Steam was
restarted. Native and Proton alike, found with Valheim and Dredge in vince and
reproduced in joser. It came with the Steam client of 2026-10-13 and not with
anything in Polyseat: the last game that started in vince did so that morning.
The framerate cap reaches Steam through polyseat-capped, so MangoHud's OpenGL
shim is preloaded into everything Steam starts, and Steam appends its overlay
behind it. With the new overlay that pair hangs any program at all before
main, /bin/true included. The overlay asks for the real dlopen with
dlsym(RTLD_NEXT), the shim answers that call from inside itself, and so hands
the overlay its own dlopen back, which it then jumps into forever. MangoHud's
master has a fix that trades this hang for another one, in every $(...) of a
launch script, and a segfault under OpenGL (flightlessmango/MangoHud#2177).
Seats now get the shim from a package of Polyseat's own, MangoHud 0.8.4's with
one patch that hands every lookup it does not hook on to the next dlsym, and
it installs beside the distribution's MangoHud rather than over it. OpenGL
games keep their cap: glxgears with the overlay preloaded runs at the 30 fps it
was capped to, Valheim native under OpenGL starts with MangoHud loaded, and
Dredge starts under Proton. packaging/mangohud-shim has the measurements and
how to build it.
Seats need provisioning again, recipe generation 67: polyseat-capped
points at the new shim, and the recipe installs it. Updating a seat's software
installs the package too, but only provisioning rewrites the script that uses
it.
Polyseat 0.39.0
A game can be uninstalled from every seat at once. The library's button
for a title used to take it out of the pool and leave the seats alone, and that
lasted about a minute: the next pass found a finished install in any seat still
holding it and took it straight back into the pool. Getting rid of a game meant
uninstalling it inside every seat by hand. The button is now Uninstall, and it
deletes the title from the pool, from every seat taking part in the library and
from the host's library, Steam titles and shared folders alike.
Nothing is deleted while anybody is playing it: if one copy is in use, by the
same test that holds back an update, every copy stays and the message names the
seat. A watched library the daemon only reads from refuses it as well, since its
copy would come back. Each seat's compatdata for the title stays, because that
is the Proton prefix and usually holds the saves; shadercache and
downloading go. A seat that had uninstalled the title itself forgets that, so
installing the game again later shares it like a new one.
No provisioning needed, the daemon alone changes.
MangoHud shim 0.8.4-1 for seats
The MangoHud shim package a Polyseat seat installs from 0.40.0 on. Not a Polyseat release, and nothing to install by hand: provisioning fetches it and checks it against the checksum in internal/seat/provision.go.
It is the OpenGL shim, libMangoHud_shim.so, from MangoHud's v0.8.4 with one patch, shim-rtld-next.patch, built for 64 and 32 bit and installed beside the distribution's MangoHud under /usr/$LIB/polyseat/mangohud. Since the Steam client of 2026-10-13, the unpatched shim and Steam's gameoverlayrenderer.so hang every game Steam starts; the patch hands every lookup the shim does not hook on to the next dlsym as a tail call. The recipe, the patch and what was measured are in the tree this tag points at and in packaging/mangohud-shim on main. Upstream: flightlessmango/MangoHud#2177.
sha256 753ef3315928a7fbfa63ccba4e59936331f9cd37693955d54231ba73d813053b
MangoHud is MIT licensed. Its source is the upstream tag v0.8.4 plus the patch here.
Polyseat 0.38.0
A game left running rendered flat out once nobody was watching. A seat caps
its games at the framerate the client asked for, and took that cap off again
when the client left. That was written on the belief that MangoHud reads its
file once, when a game starts, so that removing it would cost a running game
nothing. MangoHud rereads the file while the game runs. Somebody who
disconnected without quitting therefore left a game behind with no limit at
all, and since the seat also asks for a present mode that never blocks, nothing
else held it either: one game was found at 220 % of a core with no client, and
an OpenGL test program in a seat went from 60 to about 7000 frames a second
every time the cap was removed, and back every time it was written.
The cap now stays. A client arriving sets it to its own rate, as before, and a
game that was already running follows within seconds. Once the stream has
stayed gone, the daemon sets it to the rate of the seat's own mode, in the same
step that puts the mode back, where it used to remove it. Sunshine no longer
runs anything for the cap when a client leaves.
Measured in a seat with a Vulkan and an OpenGL program running throughout: the
cap removed and rewritten by the old script, then the same sequence with the
new one, where both stayed at 60 through every step that used to release them,
followed a 120 fps client up and came back to 60 for the seat's own mode.
Seats need provisioning again for this, recipe generation 66: the script and
Sunshine's configuration both change. Until a seat's Sunshine has been
restarted it still asks for the cap to be taken off, and the new script answers
that by leaving it alone.
Not measured: a real game through a real disconnect and reconnect on the
released build. The measurements above are from test programs, with the script
run by hand the way Sunshine and the daemon run it.
Polyseat 0.37.0
A stream stuttered with nothing wrong with it. On a wired connection, with
every statistic on the client clean, a game streamed out of a seat still
hitched a few times a second. It was not the network and not the encoder: 60.00
frames a second left the seat and not a packet was lost. Five to eleven of
every hundred frames were the picture before, sent again, and for each of those
another one was never sent.
The cause was Sunshine's capture on sway. It asked for a frame on a clock of its
own, which made sway produce one whether the game had drawn anything new or
not, and depending on how that clock happened to stand against the game's the
same picture went out twice. A seat now runs the same Sunshine release with one
patch that takes a frame when sway has a new one, and its output runs at twice
the rate the client asked for, so that a frame the game delivers a little early
has somewhere to go. Measured on the same seat, game and client, before and
after: 5 to 11 repeats in a hundred down to none, at 60.00 frames a second,
over seven connections.
Both halves are needed and each was measured without the other. The patch alone
delivered 54 to 57 frames a second, and the faster output alone was clean on one
connection and one repeat in ten on the next.
A seat's Sunshine is built here now. It is LizardByte's 2026.914.233613
with packaging/sunshine/wlgrab-event-driven.patch, published as a package on
a release of this repository and checked against a checksum in the source
before it is installed. The web interface of that Sunshine names Polyseat as
its publisher, so that a problem with it is brought here. The recipe and how to
build it are in packaging/sunshine. The change itself is open with LizardByte
as #5751 and #5748; when a release of theirs carries it, the seats go back to
their package.
The interface shows twice the client's rate while somebody is streaming. A
seat serving a 60 fps client reads 3840x2160@120Hz now. That is the output's
rate and it is meant to be; the stream is still the 60 the client asked for.
Not measured: a client asking for 120 frames a second, which puts the
output at 240 Hz; a game running directly on sway rather than through Steam;
and an AMD host, where the package is the same but the encoder is not.
Seats have to be built again for this: recipe generation 65. Provisioning
replaces the Sunshine package and the script that sizes the output. A seat
built before keeps stuttering exactly as it did.
Sunshine 2026.914.233613-1.1 for seats
The Sunshine package a Polyseat seat installs from 0.37.0 on. Not a Polyseat release, and nothing to install by hand: provisioning fetches it and checks it against the checksum in internal/seat/provision.go.
It is LizardByte's Sunshine v2026.914.233613 with one patch, wlgrab-event-driven.patch, which makes the wlr capture take a frame when the compositor has a new one instead of sampling on a clock of its own. The recipe, the patch and what was measured are in the tree this tag points at and in packaging/sunshine on main.
sha256 5cd0bebd19e7a8ca267dda29b5810285c46965b4e594b0e36d9d2d7f8df488df
Sunshine is GPL-3.0. Its source is the upstream commit 63d35f702ee9e362e43263742981836ec0710384 plus the patch here.
Polyseat 0.36.0
A client that came back with a different size got the old one. Leaving
without quitting keeps the application running in Sunshine, and connecting
again within the next minute resumed it. A resume runs none of the commands
that size the seat, so a client that changed its size in between - a split
screen going from side by side to top and bottom, a phone turned round - was
streamed the size it had before. The daemon now follows each seat's Sunshine
log and closes the application the moment a client leaves, unless it is back
first, so every connection is a launch sized for whoever makes it. Every
application is detached: Steam and a running game carry on, and picking the
same entry lands back in them.
A brief drop no longer changes the seat's size twice. Sunshine used to put
the seat's own size back the moment a client left. With the application now
closed on every drop, that would have put a game through two mode changes for a
reconnect at the same size, so the daemon puts the size back itself once the
stream has stayed gone for 45 seconds, as it already did after any stream that
ended some other way.
Seats have to be built again for this: recipe generation 64. The change to
the size is in the seat's Sunshine configuration. A seat built before still
gets the right size on every connection, but goes back to its own size and up
again on a brief drop until it is provisioned again.
Polyseat 0.35.1
Signing in to Steam ended on a black screen. 0.35.0 put the sign in window
of a new seat on the screen and stopped there. Steam starts without a window,
so once the sign in was done there was nothing left to show, and nothing said
that Steam had to be picked a second time. The seat now waits for the sign in
to finish and opens Big Picture after it.
Big Picture was asked for again while it was on the screen. After a sign
in, and whenever gamescope has had nothing to show for a moment, its window
comes back without the name the seat looked for it by. Picking Steam in
Moonlight then asked Steam for Big Picture every five seconds for a minute and
a half. The window is now recognised by the process it belongs to as well.
Everything 0.35.0 says applies, including that seats have to be built again:
recipe generation 63.
Polyseat 0.35.0
A new seat could not be signed in to Steam. Steam runs inside gamescope,
and gamescope only shows the windows Steam tells it to, which Steam does once
Big Picture is up. A Steam nobody has signed in to has no Big Picture, so its
sign in window existed and was never on the screen: picking Steam in Moonlight
gave a black screen, in every seat built since 0.30.0. Seats signed in before
that never met it. Picking Steam now puts that window on the screen. After
signing in, pick Steam again if Big Picture does not follow by itself.
Steam in a seat's launcher does something. Started from the grid on the
desktop, Steam opened on the other workspace, where it lives, and the desktop
stayed as it was. The entry now switches to Steam and opens Big Picture, the
way picking Steam in Moonlight does.
Seats have to be built again for this: recipe generation 62. Both are
files inside the seat, and a seat built before keeps the old ones until it is
provisioned again, which restarts it once.
Polyseat 0.34.0
The launcher in a seat is drawn for the client that is connected, not for
the one before it. A seat's grid of applications takes its size when it
opens, and the session opens one at the seat's own resolution before anybody
connects. A client connecting changes the resolution and asks for the grid,
and a grid that was already open was left as it was: a 4K client got the small
icons meant for 1080p until the grid was closed and opened again, and a phone
connecting after a television got the doubled ones. The grid is now opened
again whenever its size no longer fits the screen.
The doubled grid needs a screen that is wide as well as tall. The size was
decided by the height alone, doubled from 1800 pixels up. A client that shows
two seats side by side on one television asks each for 1920x2160, and seven
doubled columns do not fit into that: the grid came up with five. It is now
doubled only from 2880x1800 up, so 4K is as it was, and half a television or a
phone held upright gets the grid that fits it.
Seats have to be built again for this: recipe generation 61. The launcher
is a script inside the seat, and a seat built before keeps the old one until it
is provisioned again, which restarts it once.