feat: Controller support, performance enchancements, multi-stage images, fixes (#304)

## Description
Oops.. another massive PR 🥲 

This PR contains multiple improvements and changes.

Firstly, thanks gst-wayland-display's PR
[here](https://github.com/games-on-whales/gst-wayland-display/pull/20).
NVIDIA path is now way more efficient than before.

Secondly, adding controller support was a massive hurdle, requiring me
to start another project
[vimputti](https://github.com/DatCaptainHorse/vimputti) - which allows
simple virtual controller inputs in isolated containers. Well, it's not
simple, it includes LD_PRELOAD shims and other craziness, but the
library API is simple to use..

Thirdly, split runner image into 3 separate stages, base + build +
runtime, should help keep things in check in future, also added GitHub
Actions CI builds for v2 to v4 builds (hopefully they pass..).

Fourth, replaced the runner's runtime Steam patching with better and
simpler bubblewrap patch, massive thanks to `games-on-whales` to
figuring it out better!

Fifth, relay for once needed some changes, the new changes are still
mostly WIP, but I'll deal with them next time I have energy.. I'm spent
now. Needed to include these changes as relay needed a minor change to
allow rumble events to flow back to client peer.

Sixth.. tons of package updates, minor code improvements and the usual. 

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* End-to-end gamepad/controller support (attach/detach, buttons, sticks,
triggers, rumble) with client/server integration and virtual controller
plumbing.
  * Optional Prometheus metrics endpoint and WebTransport support.
  * Background vimputti manager process added for controller handling.

* **Improvements**
  * Multi-variant container image builds and streamlined runtime images.
  * Zero-copy video pipeline and encoder improvements for lower latency.
  * Updated Steam compat mapping and dependency/toolchain refreshes.

* **Bug Fixes**
* More robust GPU detection, input/fullscreen lifecycle,
startup/entrypoint, and container runtime fixes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
This commit is contained in:
Kristian Ollikainen
2025-10-20 11:20:05 +03:00
committed by GitHub
parent a3ee9aadd9
commit c62a22b552
62 changed files with 4203 additions and 2278 deletions

View File

@@ -49,25 +49,12 @@ func (r *Room) removeParticipantByID(pID ulid.ULID) {
}
}
// Removes all participants from a Room
/*func (r *Room) removeAllParticipants() {
for id, participant := range r.Participants.Copy() {
if err := r.signalParticipantOffline(participant); err != nil {
slog.Error("Failed to signal participant offline", "participant", participant.ID, "room", r.Name, "err", err)
}
r.Participants.Delete(id)
slog.Debug("Removed participant from room", "participant", id, "room", r.Name)
}
}*/
// IsOnline checks if the room is online (has both audio and video tracks)
func (r *Room) IsOnline() bool {
return r.AudioTrack != nil && r.VideoTrack != nil
}
func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) {
//oldOnline := r.IsOnline()
switch trackType {
case webrtc.RTPCodecTypeAudio:
r.AudioTrack = track
@@ -76,69 +63,4 @@ func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalS
default:
slog.Warn("Unknown track type", "room", r.Name, "trackType", trackType)
}
/*newOnline := r.IsOnline()
if oldOnline != newOnline {
if newOnline {
slog.Debug("Room online, participants will be signaled", "room", r.Name)
r.signalParticipantsWithTracks()
} else {
slog.Debug("Room offline, signaling participants", "room", r.Name)
r.signalParticipantsOffline()
}
// TODO: Publish updated state to mesh
go func() {
if err := r.Relay.publishRoomStates(context.Background()); err != nil {
slog.Error("Failed to publish room states on change", "room", r.Name, "err", err)
}
}()
}*/
}
/* TODO: libp2p'ify
func (r *Room) signalParticipantsWithTracks() {
for _, participant := range r.Participants.Copy() {
if err := r.signalParticipantWithTracks(participant); err != nil {
slog.Error("Failed to signal participant with tracks", "participant", participant.ID, "room", r.Name, "err", err)
}
}
}
func (r *Room) signalParticipantWithTracks(participant *Participant) error {
if r.AudioTrack != nil {
if err := participant.addTrack(r.AudioTrack); err != nil {
return fmt.Errorf("failed to add audio track: %w", err)
}
}
if r.VideoTrack != nil {
if err := participant.addTrack(r.VideoTrack); err != nil {
return fmt.Errorf("failed to add video track: %w", err)
}
}
if err := participant.signalOffer(); err != nil {
return fmt.Errorf("failed to signal offer: %w", err)
}
return nil
}
func (r *Room) signalParticipantsOffline() {
for _, participant := range r.Participants.Copy() {
if err := r.signalParticipantOffline(participant); err != nil {
slog.Error("Failed to signal participant offline", "participant", participant.ID, "room", r.Name, "err", err)
}
}
}
// signalParticipantOffline signals a single participant offline
func (r *Room) signalParticipantOffline(participant *Participant) error {
// Skip if websocket is nil or closed
if participant.WebSocket == nil || participant.WebSocket.IsClosed() {
return nil
}
if err := participant.WebSocket.SendAnswerMessageWS(connections.AnswerOffline); err != nil {
return err
}
return nil
}
*/