feat: pyrowave support (#354)

At this point, why not..

Calling it `nespyro` since it's slightly modified, uses slang shader
language instead, along other things..

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kristian Ollikainen
2026-10-01 23:53:19 +03:00
committed by GitHub
co-authored by DatCaptainHorse Claude Opus 5.5
parent ca7115dba8
commit cf46bf4d12
62 changed files with 15649 additions and 589 deletions
+11 -19
View File
@@ -31,7 +31,7 @@
# scoped to one stage instead, `FROM ${PROTON_IMAGE}` expands to nothing, and
# the build fails with "no FROM statement found" — which says nothing about
# the actual mistake. See the Proton stage below for what this is.
ARG PROTON_IMAGE=ghcr.io/nestrilabs/proton-ge:11-7
ARG PROTON_IMAGE=ghcr.io/nestrilabs/proton-cachyos-native-wow64:11.0-20260703
# ───────────────────────────────────────────────────────────
# initial / builder
@@ -40,7 +40,7 @@ ARG PROTON_IMAGE=ghcr.io/nestrilabs/proton-ge:11-7
# guest was Artix, chosen for an init system this image no longer contains.
# Only build artifacts leave these stages.
# ───────────────────────────────────────────────────────────
FROM docker.io/archlinux:base-devel AS initial
FROM docker.io/cachyos/cachyos-v3:latest AS initial
RUN pacman -Syu --noconfirm
FROM initial AS builder
@@ -52,8 +52,8 @@ RUN pacman -S --noconfirm --needed \
libx11 libxext libxrandr libxshmfence libxfixes libxxf86vm libxcb \
xcb-util-keysyms xorgproto \
wayland wayland-protocols \
expat zlib zstd libxml2 lm_sensors \
llvm clang libclc spirv-tools spirv-llvm-translator glslang \
expat zlib-ng-compat zstd libxml2 lm_sensors \
llvm clang libclc spirv-tools spirv-llvm-translator glslang shader-slang \
elfutils libva libdrm directx-headers \
rust rust-bindgen cbindgen \
curl openssl \
@@ -131,6 +131,7 @@ FROM builder AS nestri-src
WORKDIR /build/nestri
COPY Cargo.toml Cargo.lock ./
COPY crates/nesprotocol crates/nesprotocol
COPY crates/nespyro crates/nespyro
# Not a guest component and not installed below — it runs on a stranger's own
# machine. It is here because `cargo` loads every workspace member's manifest
# before it builds anything, so a member missing from the context fails the
@@ -209,7 +210,7 @@ FROM ${PROTON_IMAGE} AS proton
# obscurely, and a previous attempt at this image lost two services to exactly
# that.
# ───────────────────────────────────────────────────────────
FROM docker.io/archlinux:base AS os-base
FROM docker.io/cachyos/cachyos-v3:latest AS os-base
# The base image ships an unsigned local keyring, so upgrading
# `archlinux-keyring` runs a hook that reports `There is no secret key
@@ -264,12 +265,12 @@ RUN pacman -Syu --noconfirm --needed \
dbus \
iptables iproute2 \
libglvnd libdrm libepoxy libxxf86vm libinput wayland \
expat zlib llvm-libs lm_sensors elfutils libva shaderc vulkan-icd-loader \
expat zlib-ng-compat llvm-libs lm_sensors elfutils libva shaderc vulkan-icd-loader \
pixman libxkbcommon xcb-util-keysyms xorg-xwayland \
pipewire pipewire-audio pipewire-pulse libpulse wireplumber opus \
pipewire pipewire-audio wireplumber opus \
python libunwind sdl2-compat \
&& rm -f /usr/share/libalpm/hooks/dbus-reload.hook \
&& pacman -Rdd --noconfirm systemd systemd-sysvcompat \
&& pacman -Rdd --noconfirm systemd systemd-sysvcompat openssh \
&& pacman -Scc --noconfirm
# `libunwind` is Wine's, not ours. `ntdll.so` links it, so without it every
@@ -278,15 +279,6 @@ RUN pacman -Syu --noconfirm --needed \
# after the prefix had already been created -- so the session got past every
# check that Proton was present and usable.
#
# `pipewire-pulse` and `libpulse` are Wine's audio. Its driver speaks only the
# PulseAudio protocol, so without the translation layer a game starts, renders
# and plays silently, with nothing failing to say why. `libpulse` is what that
# driver, `winepulse.so`, links, and it would arrive as a dependency of
# pipewire-pulse anyway. It is named because the dependency is Wine's and not
# pipewire-pulse's, and a library that only arrives as somebody else's
# dependency goes away when they do. The check below covers `winepulse.so`, so a
# missing library fails the build rather than the session.
# `python` is not a build dependency here -- the builder stage has its own for
# Mesa -- it is a *runtime* one. The compatibility tool's entry point is a
# Python script, so a box without an interpreter starts a game and the launch
@@ -508,7 +500,7 @@ RUN failed=0; \
: > /tmp/missing-libs; \
for f in /usr/bin/nesinit /usr/bin/nescope /usr/bin/neshub /usr/bin/neswire \
/usr/bin/nesgamepad /usr/lib/libnescapture_layer.so /usr/lib/libSDL2-2.0.so.0 \
/usr/bin/dbus-daemon /usr/bin/pipewire /usr/bin/pipewire-pulse \
/usr/bin/dbus-daemon /usr/bin/pipewire \
/usr/bin/wireplumber /usr/bin/ip \
/usr/lib/libgallium-*.so /usr/lib/libEGL_mesa.so.0 \
/usr/lib/libvulkan_*.so /usr/lib/dri/*.so /usr/lib/gbm/*.so \
@@ -531,7 +523,7 @@ RUN failed=0; \
RUN for required in /usr/bin/nesinit /usr/bin/nescope /usr/bin/neshub /usr/bin/neswire \
/usr/bin/nesgamepad /usr/lib/libSDL2-2.0.so.0 \
/usr/bin/dbus-daemon /usr/bin/pipewire /usr/bin/pipewire-pulse \
/usr/bin/dbus-daemon /usr/bin/pipewire \
/usr/bin/wireplumber /usr/bin/ip /usr/bin/python3 \
/usr/share/steam/compatibilitytools.d/proton-cachyos/proton; do \
test -x "$required" || { echo "the image is missing $required" >&2; exit 1; }; \
@@ -1,66 +0,0 @@
# pipewire-pulse, the PulseAudio protocol on top of PipeWire, for clients that
# speak nothing else. Wine's audio driver is one of them.
#
# The socket moves out of the service user's runtime directory. That directory
# is 0700 and the workload runs as a different user, so a client there finds
# nothing and plays silently, which is the failure PipeWire's own socket was
# moved for. The workload is pointed here with PULSE_SERVER, and nesinit starts
# this under a umask that lets a user who does not own the socket open it. This
# path is written in nesinit too, and a test there compares the two.
#
# A drop-in replaces a key of pulse.properties rather than appending to it, so
# the default "unix:native" socket is not created as well.
pulse.properties = {
server.address = [ "unix:/run/pipewire/pulse-native" ]
# module-always-sink creates a "Dummy Output" sink whenever no other sink
# exists, and while neswire's sink is not up yet that is always. A second
# sink is a second default-node candidate, and a game that picks it plays
# into nothing. neswire has to be the only sink; see the WirePlumber
# drop-in.
pulse.cmd.always-sink = false
# The root is read-only and every box starts from the same image, so there
# is nothing to restore and nowhere to save, which is the reasoning behind
# turning off WirePlumber's state hooks. Left on, these are one more place
# that could pin a default.
pulse.cmd.device-manager = false
pulse.cmd.device-restore = false
pulse.cmd.stream-restore = false
# Wine sizes its whole audio path from this number. At startup its driver
# opens a probe stream asking for a 1-frame request size, reads back what
# the server grants, which is this floor, and makes the device period ten
# times that. Streams then queue three periods. The default floor of 256
# frames therefore becomes a 53 ms period and about 160 ms of audio in
# flight, which is desktop playback and not a game being streamed.
#
# 48 frames (1 ms) makes that period 10 ms, the default period Windows
# itself uses, so a game gets the buffering it was written for. A stream
# then asks for 480-frame requests, which is what is granted, so the floor
# affects the probe and nothing else Wine does.
pulse.min.req = 48/48000
# What a client gets when it asks for no particular buffering. The shipped
# default is two seconds, which is only harmless where nobody is listening
# live. 10 ms requests and 40 ms of target buffer are enough for a client
# that does not say.
pulse.default.req = 480/48000
pulse.default.tlength = 1920/48000
}
# pipewire-pulse.conf raises the minimum node latency to 1024 frames (21.3 ms)
# inside a virtual machine, which a box is. That puts every Pulse client, and
# therefore Wine, at least 21 ms behind, whatever it asked for. Floor it where
# the graph itself is floored instead; see the pipewire.conf drop-in for why
# 256 frames. A drop-in appends to this array, and every matching rule applies
# in order, so this runs after the shipped rule and wins.
pulse.properties.rules = [
{ matches = [ { cpu.vm.name = !null } ]
actions = {
update-props = {
pulse.min.quantum = 256/48000
}
}
}
]
@@ -3,14 +3,6 @@
# The goal is that `neswire` is the *only* Audio/Sink in the graph, so
# default-node selection has exactly one candidate and cannot pick wrong.
#
# The "Dummy Output" (`auto_null`) sink is not disabled here, because it does
# not come from this side. pipewire-pulse creates it with `module-always-sink`,
# and the pipewire-pulse drop-in turns that off with
# `pulse.cmd.always-sink = false`. WirePlumber ships scripts/fallback-sink.lua,
# which creates a node by the same name, but no component in wireplumber.conf
# references it, so it never loads. If auto_null appears, look at the
# pipewire-pulse drop-in first.
#
# pipewire.conf's Dummy-Driver / Freewheel-Driver are a third thing again:
# support.node.driver objects with no ports -- drivers, not sinks, so nothing
# can play into them. Dummy-Driver is load-bearing here. With no ALSA hardware