mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-27 04:52:25 +03:00
feat: media bitrate control, HDR (#346)
Fixes: #335 Still a work-in-progress. --------- Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Wanjohi <elviswanjohi47@gmail.com>
This commit is contained in:
co-authored by
DatCaptainHorse
Claude Opus 5
Wanjohi
parent
1c721962f4
commit
0811f57f1a
+38
-28
@@ -13,9 +13,9 @@
|
||||
# this image is plain Arch rather than a distribution chosen for its init.
|
||||
# ref(d-0064)
|
||||
#
|
||||
# Proton is here, and it is not a closed component: it is proton-cachyos built
|
||||
# from source with --enable-wow64, which is what removes the need for a whole
|
||||
# 32-bit host stack. Valve's steamclient.so is a different thing and is NOT
|
||||
# Proton is here, and it is not a closed component: it is proton-ge built
|
||||
# from source wow64-only, which is what removes the need for a whole 32-bit
|
||||
# host stack. Valve's steamclient.so is a different thing and is NOT
|
||||
# here — that one is closed, and nestri/CLAUDE.md is explicit that nothing
|
||||
# closed enters this repo. Whatever layers it on top of runtime_prod is a
|
||||
# closed build outside this repo — see build/README.md.
|
||||
@@ -31,8 +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-cachyos-native-wow64:11.0-20260703
|
||||
|
||||
ARG PROTON_IMAGE=ghcr.io/nestrilabs/proton-ge:11-7
|
||||
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# initial / builder
|
||||
@@ -72,18 +71,14 @@ ENV ARTIFACTS=/artifacts
|
||||
# ───────────────────────────────────────────────────────────
|
||||
FROM builder AS mesa-build
|
||||
|
||||
ARG MESA_GIT=https://gitlab.freedesktop.org/mesa/mesa.git
|
||||
ARG MESA_COMMIT=8ace865d958b0f17254afc427db21b0ad1747b4b
|
||||
ARG MESA_GIT=https://gitlab.freedesktop.org/nowrep/mesa.git
|
||||
ARG MESA_COMMIT=1af89bea491deabc4741700f8df4d41edfd22f7a
|
||||
ARG JOBS=
|
||||
|
||||
# Our patches to the amdgpu native-context path, applied on top of the pinned
|
||||
# commit. They are not cosmetic: upstream's winsys re-queries device-static
|
||||
# facts on a path that costs a synchronous round trip to the host under
|
||||
# virtio, and it was most of a frame's time. See each patch's own message.
|
||||
#
|
||||
# `git apply` and not `git am`: no committer identity is needed, and a patch
|
||||
# that no longer applies stops the build here rather than producing an image
|
||||
# that is quietly unpatched and slow.
|
||||
COPY build/patches/mesa /build/patches/mesa
|
||||
|
||||
RUN test -n "$JOBS" || JOBS=$(nproc) && \
|
||||
@@ -173,14 +168,13 @@ RUN --mount=type=cache,target=/root/.cargo/registry \
|
||||
# `FROM scratch` over the tree, so its root *is* the tree and there is nothing
|
||||
# in it to run — only something to copy from.
|
||||
#
|
||||
# Built with `--enable-wow64`, which is the whole reason it is a build of ours
|
||||
# rather than the distribution's package. wow64 runs 32-bit Windows code
|
||||
# inside a 64-bit unix process, so a box needs no lib32 anything: no 32-bit
|
||||
# glibc, no second Mesa for i686, and — the one that matters most here — no
|
||||
# Built wow64-only, which is the whole reason it is a build of ours rather
|
||||
# than a released one. wow64 runs 32-bit Windows code inside a 64-bit unix
|
||||
# process, so a box needs no lib32 anything: no 32-bit glibc, no second Mesa
|
||||
# for i686, and — the one that matters most here — no
|
||||
# second capture layer, because the game is a 64-bit process and loads the
|
||||
# 64-bit Vulkan loader the existing layer already sits in. The distribution's
|
||||
# package is built without the flag, which is exactly why it depends on
|
||||
# lib32-*.
|
||||
# 64-bit Vulkan loader the existing layer already sits in. The released builds
|
||||
# carry a 32-bit unix side, which is exactly why they need lib32-*.
|
||||
#
|
||||
# Override to build it yourself; the tag is a version and moves deliberately.
|
||||
# ───────────────────────────────────────────────────────────
|
||||
@@ -269,7 +263,7 @@ RUN pacman -Syu --noconfirm --needed \
|
||||
libglvnd libdrm libepoxy libxxf86vm libinput wayland \
|
||||
expat zlib llvm-libs lm_sensors elfutils libva shaderc vulkan-icd-loader \
|
||||
pixman libxkbcommon xcb-util-keysyms xorg-xwayland \
|
||||
pipewire pipewire-audio wireplumber opus \
|
||||
pipewire pipewire-audio pipewire-pulse libpulse wireplumber opus \
|
||||
python libunwind \
|
||||
&& rm -f /usr/share/libalpm/hooks/dbus-reload.hook \
|
||||
&& pacman -Rdd --noconfirm systemd systemd-sysvcompat \
|
||||
@@ -281,6 +275,15 @@ 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
|
||||
@@ -368,7 +371,7 @@ COPY --from=mesa-build /artifacts/mesa /
|
||||
COPY --from=nestri-build /artifacts/nestri /
|
||||
|
||||
# The Proton tree, whose image root is the tree, so this lands it at
|
||||
# /usr/share/steam/compatibilitytools.d/proton-cachyos.
|
||||
# /usr/share/steam/compatibilitytools.d/proton-ge.
|
||||
#
|
||||
# Deliberately not in the strip manifest above: that list is our own build
|
||||
# output, and the two stripping decisions are not the same one. Proton ships
|
||||
@@ -470,11 +473,17 @@ RUN for intruder in /usr/lib/systemd/systemd /sbin/openrc-init /usr/bin/openrc-i
|
||||
# directory. Forty files, every one a false positive, hiding the one real
|
||||
# entry.
|
||||
#
|
||||
# So: the programs in `bin/`, which are ordinary ELF and resolve normally, and
|
||||
# So: the programs in `bin/`, which are ordinary ELF and resolve normally,
|
||||
# `ntdll.so`, which is the first thing Wine loads and the one that linked the
|
||||
# missing library. That is exactly the failure that got through, with none of
|
||||
# the noise. The Windows-side DLLs beside them are not ELF and `ldd` skips them
|
||||
# anyway.
|
||||
# missing library, and `winepulse.so`, which is audio. That is exactly the
|
||||
# failure that got through, with none of the noise. The Windows-side DLLs beside
|
||||
# them are not ELF and `ldd` skips them anyway.
|
||||
#
|
||||
# Each file is resolved with its own directory on the library path. That is
|
||||
# where Wine's loader finds the siblings a unix object links by soname, so
|
||||
# `winepulse.so` naming `ntdll.so` resolves the way it does at run time, and
|
||||
# what is left is a library the image really lacks. For every other file in
|
||||
# the list it changes nothing.
|
||||
# The output is one file per line with its own missing libraries under it, and
|
||||
# then every missing library once at the end. That last list is what somebody
|
||||
# acts on -- it is the set of packages to add -- and forty files each naming the
|
||||
@@ -486,13 +495,14 @@ RUN failed=0; \
|
||||
: > /tmp/missing-libs; \
|
||||
for f in /usr/bin/nesinit /usr/bin/nescope /usr/bin/neshub /usr/bin/neswire \
|
||||
/usr/lib/libnescapture_layer.so \
|
||||
/usr/bin/dbus-daemon /usr/bin/pipewire /usr/bin/wireplumber /usr/bin/ip \
|
||||
/usr/bin/dbus-daemon /usr/bin/pipewire /usr/bin/pipewire-pulse \
|
||||
/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 \
|
||||
/usr/share/steam/compatibilitytools.d/proton-cachyos/files/bin/* \
|
||||
/usr/share/steam/compatibilitytools.d/proton-cachyos/files/lib*/wine/*-unix/ntdll.so; do \
|
||||
[ -e "$f" ] || continue; \
|
||||
libs="$(ldd "$f" 2>/dev/null | awk '/not found/ { print $1 }')"; \
|
||||
libs="$(LD_LIBRARY_PATH="${f%/*}" ldd "$f" 2>/dev/null | awk '/not found/ { print $1 }')"; \
|
||||
[ -n "$libs" ] || continue; \
|
||||
failed=1; \
|
||||
printf ' %s\n' "$f" >&2; \
|
||||
@@ -507,8 +517,8 @@ RUN failed=0; \
|
||||
fi
|
||||
|
||||
RUN for required in /usr/bin/nesinit /usr/bin/nescope /usr/bin/neshub /usr/bin/neswire \
|
||||
/usr/bin/dbus-daemon /usr/bin/pipewire /usr/bin/wireplumber /usr/bin/ip \
|
||||
/usr/bin/python3 \
|
||||
/usr/bin/dbus-daemon /usr/bin/pipewire /usr/bin/pipewire-pulse \
|
||||
/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; }; \
|
||||
done
|
||||
|
||||
+16
-93
@@ -1,102 +1,25 @@
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# proton-cachyos, built wow64-only
|
||||
# proton-ge, built wow64-only, as a copyable image
|
||||
#
|
||||
# This file does not build Proton. It packages a Proton tree that was already
|
||||
# built, and it cannot build it: proton-ge's build runs its own steps in the
|
||||
# Steam Runtime SDK image through a container engine, so it runs on the host,
|
||||
# and running it inside this build would mean nested containers.
|
||||
# `scripts/proton-build.sh` does the build, and this file wraps the result.
|
||||
#
|
||||
# Separate from the guest Containerfile because it is a separate cadence:
|
||||
# hours to build, and only when PROTON_TAG moves. The guest image pulls the
|
||||
# result from a registry instead of rebuilding it, which is why this file is
|
||||
# not part of that build and is not reached by `make build`.
|
||||
# result from a registry instead of rebuilding it, which is why none of this
|
||||
# is reached by `make build`.
|
||||
#
|
||||
# The final stage is FROM scratch, so the image *is* the Proton tree — nothing
|
||||
# to run, only something to COPY --from.
|
||||
# It is FROM scratch, so the image *is* the Proton tree. There is nothing in
|
||||
# it to run, only something to COPY --from.
|
||||
#
|
||||
# Build and publish with `make proton-image` / `make proton-push`. **Its
|
||||
# context is this directory**, not the repository root the guest build uses:
|
||||
# all it needs is the two scripts beside it, and a context of the whole tree
|
||||
# would hand it a multi-gigabyte `output/` for no reason.
|
||||
#
|
||||
# `PROTON_TAG` is the one thing to change, and the Makefile derives the
|
||||
# published image's version from it. They are the same number in two
|
||||
# spellings, and an image whose name does not say which Proton is inside it
|
||||
# is worse than no image.
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
FROM docker.io/archlinux:base-devel AS builder
|
||||
|
||||
# proton-cachyos-native's makedepends, minus every lib32-* (that is the whole
|
||||
# point of --enable-wow64), with two substitutions Arch requires: ocl-icd
|
||||
# provides opencl-icd-loader, and mesa-libgl is folded into libglvnd.
|
||||
#
|
||||
# glib2-devel is the one that is easy to miss — it carries glib-mkenums, which
|
||||
# libsoup's meson looks up through glib-2.0's pkg-config variables and fails on
|
||||
# obscurely. unzip and zip are for the xalia dist step. afdko is not packaged at
|
||||
# all, and the fonts submodule needs it, so it comes from PyPI.
|
||||
RUN pacman -Syu --noconfirm --needed \
|
||||
alsa-lib clang cmake curl ffmpeg fontforge giflib git glib2-devel \
|
||||
glslang gnutls gtk3 libgphoto2 libglvnd libpulse libva libxcomposite \
|
||||
libxinerama libxxf86vm lld mesa meson ninja nasm \
|
||||
opencl-headers ocl-icd pcsclite perl perl-json python python-pip \
|
||||
python-pefile python-setuptools-scm rsync rust samba unixodbc \
|
||||
unzip zip v4l-utils vulkan-headers vulkan-icd-loader wayland \
|
||||
wayland-protocols wget xorg-util-macros \
|
||||
mingw-w64-gcc mingw-w64-binutils mingw-w64-crt mingw-w64-headers \
|
||||
mingw-w64-winpthreads \
|
||||
&& pip install --break-system-packages --no-cache-dir afdko \
|
||||
&& pacman -Scc --noconfirm
|
||||
|
||||
# widl generates vkd3d's public headers. Without it autoconf sets HAVE_WIDL to
|
||||
# false, the headers are silently not generated, and the build dies an hour
|
||||
# later on a missing vkd3d_d3dx9shader.h. Arch ships widl only inside `wine`,
|
||||
# which requires multilib; the AUR's mingw-w64-tools builds it standalone, so do
|
||||
# the same.
|
||||
#
|
||||
# The digest is not a formality. What is extracted here has its `configure` and
|
||||
# its makefiles run as root in this builder, and what they produce is copied
|
||||
# into the image a box runs -- so whoever can change these bytes can change what
|
||||
# runs on every host. SourceForge hands the request to whichever mirror it
|
||||
# likes, over a connection this builder does not pin, and the project publishes
|
||||
# no signature. The digest is the only thing that makes the mirror not matter.
|
||||
#
|
||||
# Taken 2026-09-14 from two different mirrors of v14.0.0, which agreed. A
|
||||
# mismatch here is not a thing to paper over by taking the new digest: it means
|
||||
# the bytes behind this exact version string changed, and that wants looking at
|
||||
# before it wants fixing.
|
||||
ARG MINGW_W64_VER=14.0.0
|
||||
ARG MINGW_W64_SHA256=6eaf921d9eb987d3820b364ea9775bc19b965ec81490b6fdd716526c28e1995c
|
||||
RUN curl -fL --retry 3 -o /tmp/mingw-w64.tar.bz2 \
|
||||
"https://sourceforge.net/projects/mingw-w64/files/mingw-w64/mingw-w64-release/mingw-w64-v${MINGW_W64_VER}.tar.bz2/download" \
|
||||
&& echo "${MINGW_W64_SHA256} /tmp/mingw-w64.tar.bz2" | sha256sum -c - \
|
||||
&& tar xf /tmp/mingw-w64.tar.bz2 -C /tmp \
|
||||
&& for arch in i686-w64-mingw32 x86_64-w64-mingw32; do \
|
||||
mkdir -p "/tmp/widl-${arch}" && cd "/tmp/widl-${arch}" \
|
||||
&& "/tmp/mingw-w64-v${MINGW_W64_VER}/mingw-w64-tools/widl/configure" \
|
||||
--prefix=/usr --target="${arch}" --program-prefix="${arch}-" \
|
||||
&& make -j"$(nproc)" && make install; \
|
||||
done \
|
||||
&& rm -rf /tmp/mingw-w64* /tmp/widl-*
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
ARG PROTON_GIT=https://github.com/CachyOS/proton-cachyos.git
|
||||
ARG PROTON_TAG=cachyos-11.0-20260703-native
|
||||
ARG GECKO_VER=2.47.4
|
||||
ARG MONO_VER=11.2.0
|
||||
ARG XALIA_VER=0.4.9
|
||||
ARG JOBS=
|
||||
|
||||
# Fetch and build are separate layers on purpose: the submodule checkout runs
|
||||
# well past ten minutes, and a build that fails on a flag or a missing tool must
|
||||
# not pay for it again.
|
||||
COPY scripts/proton-fetch.sh /build/proton-fetch.sh
|
||||
RUN PROTON_GIT="${PROTON_GIT}" PROTON_TAG="${PROTON_TAG}" \
|
||||
GECKO_VER="${GECKO_VER}" MONO_VER="${MONO_VER}" XALIA_VER="${XALIA_VER}" \
|
||||
bash /build/proton-fetch.sh
|
||||
|
||||
COPY scripts/proton-build.sh /build/proton-build.sh
|
||||
RUN GECKO_VER="${GECKO_VER}" MONO_VER="${MONO_VER}" JOBS="${JOBS}" \
|
||||
bash /build/proton-build.sh
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# The publishable artifact: the Proton tree and nothing else
|
||||
# context is the built tree** (`output/proton/obj/dist`), and not this
|
||||
# directory or the repository root: the tree is the whole input, and it is
|
||||
# the only thing the build needs to send.
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
FROM scratch
|
||||
COPY --from=builder /artifacts/proton/ /
|
||||
ARG BUILD_NAME=proton-ge
|
||||
COPY . /usr/share/steam/compatibilitytools.d/${BUILD_NAME}/
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# This build's context is `build/`, not the repository root, because all it
|
||||
# needs is the two scripts beside the Containerfile.
|
||||
#
|
||||
# `output/` is the reason this file exists. It holds packed rootfs images —
|
||||
# multiple gigabytes each — and a build context is copied before the first
|
||||
# instruction runs, so without this line every Proton build starts by moving
|
||||
# the last one it produced.
|
||||
output
|
||||
|
||||
# Nothing else here is an input to this build.
|
||||
etc
|
||||
README.md
|
||||
Makefile
|
||||
Containerfile
|
||||
Containerfile.containerignore
|
||||
+83
-12
@@ -1,5 +1,5 @@
|
||||
SHELL := /bin/bash
|
||||
.PHONY: build build-debug image image-debug proton-image proton-push clean help
|
||||
.PHONY: build build-debug image image-debug kernel kernel-clean proton-image proton-push proton-clean clean help
|
||||
|
||||
CONTAINER_RT := $(shell command -v podman 2>/dev/null || command -v docker 2>/dev/null)
|
||||
ifeq ($(CONTAINER_RT),)
|
||||
@@ -37,14 +37,53 @@ IMAGE_NAME := ghcr.io/nestrilabs/nestri/base
|
||||
# number in two spellings and an image whose name does not say which Proton is
|
||||
# inside it is worse than no image at all. Changing the tag by hand and
|
||||
# forgetting the version is exactly the mistake this removes.
|
||||
PROTON_TAG ?= cachyos-11.0-20260703-native
|
||||
PROTON_VERSION := $(PROTON_TAG:cachyos-%-native=%)
|
||||
PROTON_GIT ?= https://github.com/gloriouseggroll/proton-ge-custom.git
|
||||
PROTON_TAG ?= GE-Proton11-7
|
||||
PROTON_VERSION := 11.0-20260703
|
||||
PROTON_IMAGE ?= ghcr.io/nestrilabs/proton-cachyos-native-wow64
|
||||
PROTON_REF := $(PROTON_IMAGE):$(PROTON_VERSION)
|
||||
# The directory name under compatibilitytools.d, and the name Steam shows.
|
||||
PROTON_NAME := proton-ge
|
||||
OUTPUT_DIR := output
|
||||
# Source, build tree, ccache and cargo downloads. Tens of gigabytes, and it
|
||||
# lives under output/ because that directory is already ignored by git and
|
||||
# kept out of the guest build's context.
|
||||
PROTON_WORK := $(OUTPUT_DIR)/proton
|
||||
ROOTFS_SIZE ?= 3G
|
||||
FORCE_REBUILD ?=
|
||||
|
||||
# The guest kernel: CachyOS's fork, for its scheduler patches, with our own
|
||||
# minimal config on top (kernel/nestri.fragment). The tree is a clone rather
|
||||
# than vendored, and lives under output/ for the same reason Proton's does.
|
||||
# KERNEL_SRC may point at an existing tree elsewhere.
|
||||
KERNEL_GIT ?= https://github.com/CachyOS/linux.git
|
||||
KERNEL_REF ?= cachyos-7.2.6-1
|
||||
# The target hardware is at least AVX2-capable. Never `native`: the build host
|
||||
# is not the target. Empty leaves the tree's own generic choice alone.
|
||||
KERNEL_MARCH ?= x86-64-v3
|
||||
|
||||
# Experimental: KERNEL_INFINITY=1 applies the Infinity scheduler series (fair,
|
||||
# rt and DRM scheduler rework) on top. It builds in its own tree and installs
|
||||
# under its own name, so the stock kernel is never patched and switching the
|
||||
# flag back and forth never needs a revert.
|
||||
#
|
||||
# The series is published per CachyOS release, so its directory is derived
|
||||
# from KERNEL_REF rather than written twice: bumping the kernel to a release
|
||||
# the series does not cover fails at the patch step, not in a booted box.
|
||||
KERNEL_INFINITY ?=
|
||||
INFINITY_GIT ?= https://github.com/galpt/infinity-sched-new.git
|
||||
INFINITY_REV ?= e6c85d841f25e21393a7ea47fdef286318915d8c
|
||||
INFINITY_SERIES := patches/cachyos/tuned-eevdf/$(KERNEL_REF:cachyos-%=%)
|
||||
INFINITY_WORK := $(OUTPUT_DIR)/infinity-sched
|
||||
|
||||
ifneq ($(KERNEL_INFINITY),)
|
||||
KERNEL_SRC ?= $(OUTPUT_DIR)/kernel-infinity
|
||||
KERNEL_OUTPUT := $(OUTPUT_DIR)/vmlinux-infinity
|
||||
else
|
||||
KERNEL_SRC ?= $(OUTPUT_DIR)/kernel
|
||||
KERNEL_OUTPUT := $(OUTPUT_DIR)/vmlinux
|
||||
endif
|
||||
|
||||
build:
|
||||
DOCKER_BUILDKIT=1 $(CONTAINER_RT) build $(if $(FORCE_REBUILD),--no-cache,) \
|
||||
--build-arg PROTON_IMAGE=$(PROTON_REF) \
|
||||
@@ -55,13 +94,20 @@ build-debug:
|
||||
--build-arg PROTON_IMAGE=$(PROTON_REF) \
|
||||
-f Containerfile -t $(IMAGE_NAME):debug --target runtime_debug $(CONTEXT)
|
||||
|
||||
# Hours, and only when PROTON_TAG moves. Its context is this directory rather
|
||||
# than the repository root: the two scripts beside the Containerfile are the
|
||||
# whole input, and the root would hand it everything else for nothing.
|
||||
# Hours, and only when PROTON_TAG moves. Two steps: the build runs on the host,
|
||||
# because proton-ge's build drives the container engine itself, and the
|
||||
# finished tree is then the whole context of a FROM scratch image. See the head
|
||||
# of Containerfile.proton for why the build cannot run inside it.
|
||||
#
|
||||
# A second run of the same tag resumes where the last one stopped. A new tag,
|
||||
# or FORCE_REBUILD, starts the tree over but keeps ccache.
|
||||
proton-image:
|
||||
DOCKER_BUILDKIT=1 $(CONTAINER_RT) build $(if $(FORCE_REBUILD),--no-cache,) \
|
||||
--build-arg PROTON_TAG=$(PROTON_TAG) \
|
||||
-f Containerfile.proton -t $(PROTON_REF) .
|
||||
PROTON_GIT=$(PROTON_GIT) PROTON_TAG=$(PROTON_TAG) PROTON_WORK=$(PROTON_WORK) \
|
||||
BUILD_NAME=$(PROTON_NAME) CONTAINER_ENGINE=$(notdir $(CONTAINER_RT)) \
|
||||
FORCE_REBUILD=$(FORCE_REBUILD) \
|
||||
bash scripts/proton-build.sh
|
||||
$(CONTAINER_RT) build --build-arg BUILD_NAME=$(PROTON_NAME) \
|
||||
-f Containerfile.proton -t $(PROTON_REF) $(PROTON_WORK)/obj/dist
|
||||
@echo "Built $(PROTON_REF)"
|
||||
|
||||
# Publishing is what makes `make build` cheap for everyone else, since that
|
||||
@@ -82,8 +128,29 @@ image-debug: build-debug
|
||||
@mkdir -p $(OUTPUT_DIR)
|
||||
bash scripts/mkimage.sh $(IMAGE_NAME):debug $(OUTPUT_DIR)/rootfs-debug.ext4 $(ROOTFS_SIZE)
|
||||
|
||||
# Not part of `build`: it changes far less often than the rootfs, and nothing
|
||||
# in the image depends on it. A rerun rebuilds only what changed in the tree.
|
||||
kernel:
|
||||
KERNEL_GIT=$(KERNEL_GIT) KERNEL_REF=$(KERNEL_REF) KERNEL_SRC=$(KERNEL_SRC) \
|
||||
KERNEL_MARCH=$(KERNEL_MARCH) KERNEL_OUTPUT=$(KERNEL_OUTPUT) \
|
||||
KERNEL_INFINITY=$(KERNEL_INFINITY) INFINITY_GIT=$(INFINITY_GIT) \
|
||||
INFINITY_REV=$(INFINITY_REV) INFINITY_SERIES=$(INFINITY_SERIES) \
|
||||
INFINITY_WORK=$(INFINITY_WORK) \
|
||||
bash scripts/kernel-build.sh
|
||||
|
||||
# Leaves $(PROTON_WORK) and the kernel alone: one is hours of build and ccache,
|
||||
# the other a clone and a warm object tree, and a rootfs is neither.
|
||||
# proton-clean and kernel-clean are the ones that drop them.
|
||||
clean:
|
||||
rm -rf $(OUTPUT_DIR)
|
||||
find $(OUTPUT_DIR) -mindepth 1 -maxdepth 1 ! -name proton ! -name 'kernel*' \
|
||||
! -name 'vmlinux*' ! -name infinity-sched -exec rm -rf {} + 2>/dev/null || true
|
||||
|
||||
kernel-clean:
|
||||
rm -rf $(OUTPUT_DIR)/kernel $(OUTPUT_DIR)/kernel-infinity $(INFINITY_WORK) \
|
||||
$(OUTPUT_DIR)/vmlinux $(OUTPUT_DIR)/vmlinux-infinity
|
||||
|
||||
proton-clean:
|
||||
rm -rf $(PROTON_WORK)
|
||||
|
||||
help:
|
||||
@echo "Usage:"
|
||||
@@ -91,9 +158,13 @@ help:
|
||||
@echo " make build-debug Build the runtime_debug container image"
|
||||
@echo " make image Build + pack runtime_prod into output/rootfs.ext4"
|
||||
@echo " make image-debug Build + pack runtime_debug into output/rootfs-debug.ext4"
|
||||
@echo " make clean Remove output/"
|
||||
@echo " make kernel Build the guest kernel into output/vmlinux"
|
||||
@echo " make KERNEL_INFINITY=1 kernel Experimental Infinity scheduler build → output/vmlinux-infinity"
|
||||
@echo " make kernel-clean Remove both kernel trees and images"
|
||||
@echo " make clean Remove the rootfs images from output/"
|
||||
@echo " make proton-image Build Proton from source (hours)"
|
||||
@echo " make proton-clean Remove the Proton source, build tree and ccache"
|
||||
@echo " make proton-push Build it and publish it"
|
||||
@echo " make FORCE_REBUILD=1 ... Rebuild from scratch, no layer cache"
|
||||
@echo " make PROTON_TAG=... ... Use a different proton-cachyos tag"
|
||||
@echo " make PROTON_TAG=... ... Use a different proton-ge tag"
|
||||
@echo " make ROOTFS_SIZE=8G image Override the packed image size (default 5G)"
|
||||
|
||||
+94
-26
@@ -66,17 +66,16 @@ wrong about Proton: it is compiled from source, which is not a thing you can
|
||||
do with closed software. Keeping it out cost a box the only way it has to run
|
||||
a Windows title, for a rule that did not apply to it.
|
||||
|
||||
What it is: **proton-cachyos built with `--enable-wow64`**, pulled by tag as a
|
||||
What it is: **proton-ge built wow64-only**, pulled by tag as a
|
||||
published image rather than rebuilt here, because it takes hours and moves
|
||||
only when its own tag does. `PROTON_IMAGE` overrides the tag, and it has to be
|
||||
declared before the first `FROM` — an `ARG` a `FROM` expands is global or it
|
||||
declared before the first `FROM`: an `ARG` a `FROM` expands is global or it
|
||||
is nothing, and getting that wrong fails with `no FROM statement found`, which
|
||||
says nothing about the actual mistake. wow64 is the whole reason it is a build of ours
|
||||
and not the distribution's package — it runs 32-bit Windows code inside a
|
||||
64-bit unix process, so a box needs no lib32 glibc, no second Mesa for i686,
|
||||
and no second capture layer for 32-bit titles to be captured. The
|
||||
distribution's package is built without the flag, which is exactly why it
|
||||
depends on `lib32-*`.
|
||||
says nothing about the actual mistake. wow64 is the whole reason it is a build
|
||||
of ours and not a released one. It runs 32-bit Windows code inside a 64-bit
|
||||
unix process, so a box needs no lib32 glibc, no second Mesa for i686, and no
|
||||
second capture layer for 32-bit titles to be captured. The released builds
|
||||
carry a 32-bit unix side, which is exactly why they need `lib32-*`.
|
||||
|
||||
It costs about 1.4 GB of image, and it is the one thing in here that is
|
||||
payload-shaped: a compatibility layer for Windows games in an image that is
|
||||
@@ -92,6 +91,59 @@ the virtio-gpu native-context protocol never drift apart. Only Mesa —
|
||||
`virglrenderer` is the host half of that protocol and nesbox builds its own,
|
||||
patched, from `nesbox/patches/`; nothing in this image carries it.
|
||||
|
||||
## The guest kernel
|
||||
|
||||
```sh
|
||||
make kernel # clone if needed, configure, verify, build → output/vmlinux
|
||||
make KERNEL_SRC=~/src/linux kernel # build an existing tree instead
|
||||
make kernel-clean # drop the tree and the image
|
||||
```
|
||||
|
||||
CachyOS's fork (`KERNEL_REF` in the `Makefile`), taken for its scheduler
|
||||
patches, not its config: theirs is a desktop build with thousands of modules,
|
||||
and this guest has `CONFIG_MODULES` off and no `/lib/modules` at all.
|
||||
|
||||
- **`kernel/nestri.fragment` is the source of truth**, and says why each entry
|
||||
is there. It is merged onto the tree's `.config`, resolved with
|
||||
`olddefconfig`, and then **checked**: any entry that did not survive fails the
|
||||
build. `merge_config.sh` and `olddefconfig` both drop options quietly, and
|
||||
the worst of these fails as perfectly healthy, perfectly silent audio.
|
||||
- **`kernel/base.config` is only a seed** for a tree with no `.config`, so a
|
||||
fresh clone does not start from `defconfig`'s enormous driver set. Change the
|
||||
fragment, not the seed and not a tree's `.config`.
|
||||
- **`vmlinux`, not `bzImage`.** The guest is loaded as a raw ELF with no
|
||||
bootloader in the path. It is ~16 MB unstripped, which costs nothing at run
|
||||
time: only the loadable segments are mapped.
|
||||
- **`-march=x86-64-v3`** goes in through `KCFLAGS`. It is safe in a kernel:
|
||||
the kernel's own `-mno-sse -mno-avx …` masks every vector extension off
|
||||
whatever the flag order, leaving v3's integer ISA.
|
||||
- The tree is off the pinned ref (a bisect, a local patch)? The build warns
|
||||
and builds what is there rather than checking the ref out over your work.
|
||||
|
||||
### Experimental: the Infinity scheduler
|
||||
|
||||
```sh
|
||||
make KERNEL_INFINITY=1 kernel # → output/vmlinux-infinity
|
||||
```
|
||||
|
||||
Applies [infinity-sched](https://github.com/galpt/infinity-sched-new)'s
|
||||
series (GPL-2), which reworks the fair, RT and DRM schedulers for latency
|
||||
under load. It is pinned by commit (`INFINITY_REV`), and the series directory
|
||||
comes from `KERNEL_REF`, since upstream publishes one per CachyOS release.
|
||||
|
||||
- **Its own tree and its own image.** It builds in `output/kernel-infinity`,
|
||||
so the stock kernel is never patched and switching between the two needs no
|
||||
revert.
|
||||
- **All or nothing, zero fuzz.** The whole series is checked against the
|
||||
stacked result before any of it is applied. The applied commit is recorded in
|
||||
the tree; to move `INFINITY_REV`, start over with `make kernel-clean`.
|
||||
- **Only the CPU half does anything here.** virtio-gpu does not use the DRM
|
||||
scheduler and `CONFIG_DRM_SCHED` is not built, so the GPU patch is compiled
|
||||
out. It is applied anyway because upstream says a partial series
|
||||
misbehaves.
|
||||
- Upstream's `/sys/kernel/debug/infinity_*` counters need `CONFIG_DEBUG_FS`,
|
||||
which this kernel does not have.
|
||||
|
||||
## Two packages that look droppable and are not
|
||||
|
||||
`llvm-libs` is 164 MB, the largest single thing in the image after Proton, and
|
||||
@@ -109,13 +161,14 @@ a package list says that; the check below is what said it.
|
||||
|
||||
`make build` **pulls** Proton by tag; it does not build it. Building it takes
|
||||
hours and it changes only when its tag moves, so it is one image published
|
||||
once and copied into every guest image after that. `Containerfile.proton` is
|
||||
once and copied into every guest image after that. `make proton-image` is
|
||||
that build, and it lives here so the published tag stays reproducible from
|
||||
this tree rather than from somebody's laptop.
|
||||
|
||||
```sh
|
||||
make proton-image # the current tag
|
||||
make PROTON_TAG=cachyos-11.1-20261115-native proton-image
|
||||
make proton-image # the current tag
|
||||
make PROTON_TAG=GE-Proton11-8 proton-image
|
||||
make proton-clean # drop the source, build tree and ccache
|
||||
```
|
||||
|
||||
**`PROTON_TAG` is the only thing to change.** The published version is derived
|
||||
@@ -125,23 +178,38 @@ which Proton is inside it is worse than no image. The `Containerfile`'s own
|
||||
`PROTON_IMAGE` default is a fallback for a bare container build; going through
|
||||
`make` is what keeps them in step.
|
||||
|
||||
Its **context is `build/`**, not the repository root the guest build uses. All
|
||||
it needs is the two scripts beside it, and `Containerfile.proton.containerignore`
|
||||
keeps `output/` out of that context — a build context is copied before the
|
||||
first instruction runs, so without it every Proton build would begin by moving
|
||||
the last rootfs image it produced.
|
||||
**The build runs on the host, not in a `podman build`.** proton-ge's build is
|
||||
container-driven itself: `make` runs outside, and every step runs in the Steam
|
||||
Runtime SDK image through the container engine, so the host needs only git,
|
||||
make and podman. Wrapping that in a container build would mean nested
|
||||
containers. So there are two steps:
|
||||
|
||||
Two things in the recipe are worth knowing before changing it:
|
||||
1. `scripts/proton-build.sh` clones the tag with its submodules, applies
|
||||
proton-ge's patch set, and runs its build with one change: the arch list
|
||||
drops the 32-bit unix side, which is what makes it wow64-only. Everything
|
||||
happens under `output/proton/`, which is tens of gigabytes.
|
||||
2. `Containerfile.proton` is `FROM scratch` with the built tree as its whole
|
||||
context, so the image is the tree and nothing else.
|
||||
|
||||
- **Fetch and build are separate layers on purpose.** The submodule checkout
|
||||
runs well past ten minutes, and a build that fails on a flag or a missing
|
||||
tool must not pay for that again. Keep anything that can fail *fast* in
|
||||
`proton-build.sh`.
|
||||
- **`widl` is built by hand from the mingw-w64 release.** Without it autoconf
|
||||
quietly sets `HAVE_WIDL` to false, vkd3d's public headers are never
|
||||
generated, and the build dies an hour later on a missing header. Arch ships
|
||||
`widl` only inside `wine`, which wants multilib — which is the thing
|
||||
`--enable-wow64` exists to avoid.
|
||||
Things worth knowing before changing it:
|
||||
|
||||
- **A rerun of the same tag resumes.** The clone, the patching and the
|
||||
configure step each run once per tag, and proton-ge's own make picks up
|
||||
where it stopped. A new tag or `FORCE_REBUILD=1` starts the tree over;
|
||||
ccache and the cargo downloads survive both. `make clean` leaves all of it
|
||||
alone, and `make proton-clean` removes it.
|
||||
- **The patch script does not fail on a patch that does not apply.** It
|
||||
carries on and exits 0, so `proton-build.sh` greps its output
|
||||
(`output/proton/patch.log`) and stops. Otherwise the result is an image
|
||||
that looks fine and is missing a fix.
|
||||
- **`patches/proton-ge/` is ours, applied after proton-ge's own set.** It holds
|
||||
what wow64-only needs that proton-ge's makefile does not handle, and fixes
|
||||
for things a tag pinned that have since moved. Each patch says why it exists
|
||||
at its top, and each one fails the build outright once it stops applying,
|
||||
which on a tag bump is usually upstream having fixed it.
|
||||
- **The patch script is not idempotent**, so a tree is patched exactly once.
|
||||
An interrupted run resets every submodule to the commits the tag pins before
|
||||
patching again.
|
||||
|
||||
## There is no init system in here, and that is the design
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# 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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# The graph's clock, floored where the audio sender needs it.
|
||||
#
|
||||
# Inside a virtual machine, pipewire.conf raises the graph's minimum quantum to
|
||||
# 1024 frames (21.3 ms), because a VM's timers jitter and small quanta then
|
||||
# underrun on real audio hardware. A box is a VM, so that rule applies, and it
|
||||
# silently overrides neswire's request for its 5 ms packet size. Every sample
|
||||
# then waits for a 21 ms cycle before it can be encoded.
|
||||
#
|
||||
# The floor comes down to 256 frames (5.3 ms), the smallest power of two that
|
||||
# holds one of neswire's packets. It is not lower than that on purpose: nothing
|
||||
# here needs less, and the reason for the rule, a VM's timer driving the graph,
|
||||
# still holds, because the Dummy-Driver is exactly that.
|
||||
#
|
||||
# A drop-in appends to a rules array, and every matching rule applies in order,
|
||||
# so this one runs after the shipped one and wins.
|
||||
context.properties.rules = [
|
||||
{ matches = [ { cpu.vm.name = !null } ]
|
||||
actions = {
|
||||
update-props = {
|
||||
default.clock.min-quantum = 256
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -3,14 +3,13 @@
|
||||
# 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, and no longer
|
||||
# needs to be: it came from pipewire-pulse's `module-always-sink`, and
|
||||
# pipewire-pulse is no longer installed. 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
|
||||
# ever comes back, it came back with pipewire-pulse -- the switch is
|
||||
# `pulse.cmd.always-sink = false` in a pipewire-pulse.conf.d drop-in, not
|
||||
# anything on this side.
|
||||
# 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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,207 @@
|
||||
# Kconfig fragment for the guest kernel.
|
||||
#
|
||||
# Merged onto the tree's config by scripts/kernel-build.sh with the kernel's own
|
||||
# merge_config.sh, then resolved with `make olddefconfig`, then checked: every
|
||||
# entry below must hold in the final .config or the build stops. A fragment
|
||||
# rather than a saved full config: a full .config pins thousands of symbols and
|
||||
# rots the moment the tree is bumped, while this says only what the guest
|
||||
# actually requires and lets the version's own defaults handle the rest.
|
||||
#
|
||||
# Every entry here is load-bearing or a deliberate performance choice. Nothing
|
||||
# is speculative.
|
||||
|
||||
# ── Gaming needs ─────────────────────────────────────────
|
||||
# NTSYNC is a great improvement over fsync and esync approaches.
|
||||
CONFIG_NTSYNC=y
|
||||
|
||||
# ── Timers ───────────────────────────────────────────────
|
||||
# The one that cost a day of silent audio. The guest has no sound hardware, so
|
||||
# PipeWire drives its whole graph off a timerfd at a 2.67ms cycle, which a
|
||||
# HZ_PERIODIC/250 kernel cannot express. Symptom: everything reports healthy,
|
||||
# neswire sends a steady ~3 kbps of Opus, and every sample is zero.
|
||||
CONFIG_HIGH_RES_TIMERS=y
|
||||
CONFIG_NO_HZ_IDLE=y
|
||||
CONFIG_HZ_1000=y
|
||||
|
||||
# ── Shared memory ────────────────────────────────────────
|
||||
# PipeWire moves audio through memfd, not through its socket. Wine uses POSIX
|
||||
# shm and semaphores directly. /dev/shm itself is mounted by nesinit --
|
||||
# devtmpfs does not provide it.
|
||||
CONFIG_SHMEM=y
|
||||
CONFIG_MEMFD_CREATE=y
|
||||
CONFIG_TMPFS=y
|
||||
CONFIG_DEVTMPFS=y
|
||||
CONFIG_DEVTMPFS_MOUNT=y
|
||||
|
||||
# ── What PipeWire schedules on ───────────────────────────
|
||||
# FUTEX_PI is also what Proton's fsync uses.
|
||||
CONFIG_TIMERFD=y
|
||||
CONFIG_EVENTFD=y
|
||||
CONFIG_EPOLL=y
|
||||
CONFIG_SIGNALFD=y
|
||||
CONFIG_FUTEX=y
|
||||
CONFIG_FUTEX_PI=y
|
||||
|
||||
# ── Clock sources ────────────────────────────────────────
|
||||
# Listed because they were correct throughout the silent-audio episode. A good
|
||||
# clock *source* says nothing about whether the kernel can schedule a timer
|
||||
# *event* at the resolution asked for; only the block above governs that.
|
||||
CONFIG_KVM_GUEST=y
|
||||
CONFIG_PARAVIRT_CLOCK=y
|
||||
CONFIG_X86_TSC=y
|
||||
|
||||
# ── Steal time ───────────────────────────────────────────
|
||||
# Time the host spends running something else on a vCPU is subtracted from
|
||||
# whatever task was on it, rather than charged to it. vCPUs share a set of host
|
||||
# cores instead of owning one each, so steal happens, and without this the
|
||||
# scheduler reads it as the task having run -- a frame thread preempted by the
|
||||
# host looks, to the fair class, like one that used its slice.
|
||||
CONFIG_PARAVIRT_TIME_ACCOUNTING=y
|
||||
|
||||
# ── Idle: poll before halting ────────────────────────────
|
||||
# An idle vCPU that halts exits to the host, and waking it costs an IPI and a
|
||||
# VM entry. The frame pipeline sleeps and wakes every few milliseconds, so that
|
||||
# is paid constantly. haltpoll spins for a bounded, adaptive window first
|
||||
# (guest_halt_poll_ns, 200us), so a wakeup that comes quickly never leaves the
|
||||
# guest. When the host offers it, the guest also switches host-side polling
|
||||
# off, so the two never poll at once.
|
||||
#
|
||||
# Built in but inert: it loads only if the host sets KVM_HINTS_REALTIME or the
|
||||
# command line forces it. The hint is the wrong tool -- it also promises the
|
||||
# vCPUs are never preempted, and the guest drops paravirt spinlocks on the
|
||||
# strength of that, which a shared set of host cores cannot promise. Forcing
|
||||
# the idle driver gets the polling and nothing else. Built into the command
|
||||
# line, which is prepended to what the host passes, so no caller has to know.
|
||||
# The cost is host CPU spent spinning on an idle vCPU.
|
||||
#
|
||||
# Prepended, not replacing: with OVERRIDE off the host's arguments come after
|
||||
# this and the last value wins, so `cpuidle_haltpoll.force=0` from the host
|
||||
# still turns it off.
|
||||
CONFIG_HALTPOLL_CPUIDLE=y
|
||||
CONFIG_CPU_IDLE_GOV_HALTPOLL=y
|
||||
CONFIG_CMDLINE_BOOL=y
|
||||
CONFIG_CMDLINE="cpuidle_haltpoll.force=1 swiotlb=noforce"
|
||||
# CONFIG_CMDLINE_OVERRIDE is not set
|
||||
|
||||
# ── No bounce buffer ─────────────────────────────────────
|
||||
# With more than about 3 GiB of RAM the guest has memory above 4 GiB, and x86
|
||||
# then sets aside a 64 MiB SWIOTLB bounce buffer and zeroes it at boot, which
|
||||
# makes the host back all of it. Nothing here ever bounces: the virtio devices
|
||||
# do not offer ACCESS_PLATFORM, so virtio skips the DMA API entirely, and there
|
||||
# is no other DMA-capable device. `swiotlb=noforce` in the command line above
|
||||
# skips the allocation. A device that genuinely needs a 32-bit DMA mask would
|
||||
# fail to map rather than bounce; there is none.
|
||||
|
||||
# ── microVM transport ────────────────────────────────────
|
||||
CONFIG_VIRTIO=y
|
||||
CONFIG_VIRTIO_PCI=y
|
||||
CONFIG_VIRTIO_MMIO=y
|
||||
CONFIG_VIRTIO_BLK=y # root on /dev/vda
|
||||
CONFIG_VIRTIO_CONSOLE=y # hvc0, the only way into a guest that will not boot
|
||||
CONFIG_VIRTIO_NET=y
|
||||
CONFIG_VIRTIO_FS=y # the shares the boot descriptor names
|
||||
CONFIG_FUSE_FS=y # virtiofs needs it
|
||||
CONFIG_VSOCKETS=y # neshub's link to the host
|
||||
CONFIG_VIRTIO_VSOCKETS=y
|
||||
CONFIG_VIRTIO_BALLOON=y
|
||||
CONFIG_DRM=y
|
||||
CONFIG_DRM_VIRTIO_GPU=y
|
||||
|
||||
# ── Filesystems ──────────────────────────────────────────
|
||||
CONFIG_EXT4_FS=y # output/rootfs.ext4, and a box's writable install layer
|
||||
CONFIG_MISC_FILESYSTEMS=y # the menu EROFS lives under; off in base.config, which drops it silently
|
||||
CONFIG_EROFS_FS=y # a game's build image, the install overlay's lower layer
|
||||
CONFIG_OVERLAY_FS=y # the session overlay over a read-only install
|
||||
|
||||
# ── Memory typing ────────────────────────────────────────
|
||||
# Page Attribute Table, and the MTRR support it depends on.
|
||||
#
|
||||
# Both are `def_bool y` upstream, promptable only under EXPERT -- so they were
|
||||
# switched off deliberately at some point, and the guest booted logging
|
||||
# "PAT support disabled because CONFIG_X86_PAT is disabled in the kernel".
|
||||
#
|
||||
# Without PAT there are no write-combining mappings. This guest exists to push
|
||||
# frames through virtio-gpu, and WC on the GPU aperture is precisely what PAT
|
||||
# provides; every frame would go through uncached or writeback mappings
|
||||
# instead. Not a correctness problem, which is why it went unnoticed.
|
||||
CONFIG_MTRR=y
|
||||
CONFIG_X86_PAT=y
|
||||
|
||||
# ── POSIX ACLs ───────────────────────────────────────────
|
||||
# Nothing depends on the ACLs themselves; without them, anything that sets one
|
||||
# on a device node or a file logs "Operation not supported" at every boot, and
|
||||
# a boot log full of failures that do not matter hides the ones that do.
|
||||
CONFIG_EXT4_FS_POSIX_ACL=y
|
||||
CONFIG_TMPFS_POSIX_ACL=y
|
||||
|
||||
# ── Performance ──────────────────────────────────────────
|
||||
# Chosen, not required. The mainline subset of what CachyOS enables, minus
|
||||
# anything needing patches. PREEMPT_LAZY is the tree's own default and is left
|
||||
# alone; PREEMPT_DYNAMIC on top allows `preempt=full` from the command line
|
||||
# without a rebuild.
|
||||
CONFIG_PREEMPT_DYNAMIC=y
|
||||
CONFIG_LRU_GEN=y
|
||||
CONFIG_LRU_GEN_ENABLED=y
|
||||
CONFIG_TRANSPARENT_HUGEPAGE=y
|
||||
CONFIG_TRANSPARENT_HUGEPAGE_MADVISE=y
|
||||
CONFIG_PSI=y
|
||||
|
||||
# ── Undoing the minimal seed ─────────────────────────────
|
||||
# base.config descends from a size-minimised config with EXPERT on, and EXPERT
|
||||
# is what makes each of these promptable. Every one is `default y` (or the
|
||||
# default choice) upstream, and every one was off in the built kernel. They
|
||||
# are pinned here because nothing else would stop a reseed from bringing them
|
||||
# back.
|
||||
#
|
||||
# SLUB_TINY drops the per-CPU slab caches, so every kmalloc and kfree takes the
|
||||
# locked slow path. Its own help text says it is not for performance. This
|
||||
# guest allocates constantly: network buffers, GPU ioctls, io_uring, futexes.
|
||||
# CONFIG_SLUB_TINY is not set
|
||||
#
|
||||
# Without ADVISE_SYSCALLS, madvise() and fadvise() return ENOSYS. THP above is
|
||||
# madvise-only, so no user mapping could ever get a huge page, and allocators
|
||||
# could never hand memory back with MADV_DONTNEED or MADV_FREE.
|
||||
CONFIG_ADVISE_SYSCALLS=y
|
||||
#
|
||||
# Without MEMBARRIER, Wine and the .NET runtime fall back to an mprotect() trick
|
||||
# to flush other threads' write buffers, which forces a TLB shootdown across
|
||||
# every vCPU -- an IPI storm, and each IPI is a VM exit. RSEQ gives glibc and
|
||||
# per-CPU allocators their fast paths.
|
||||
CONFIG_MEMBARRIER=y
|
||||
CONFIG_RSEQ=y
|
||||
#
|
||||
# -O2, not -Os: the whole kernel was being compiled for size, and a guest that
|
||||
# runs Wine spends much of its time in syscalls.
|
||||
CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE=y
|
||||
# CONFIG_CC_OPTIMIZE_FOR_SIZE is not set
|
||||
#
|
||||
# Without jump labels, every static key is a memory load and a branch instead
|
||||
# of a patched no-op, on paths such as steal-time accounting, the scheduler and
|
||||
# cgroup checks.
|
||||
CONFIG_JUMP_LABEL=y
|
||||
|
||||
# Autogroup is deliberately OFF, which is a departure from CachyOS.
|
||||
#
|
||||
# It groups tasks by session id, which is a desktop tuning: it keeps a
|
||||
# terminal's `make -j` from starving the browser. This guest is a
|
||||
# single-application appliance with no interactive sessions to balance
|
||||
# against each other, so there is nothing for it to do but add overhead.
|
||||
# CONFIG_SCHED_AUTOGROUP is not set
|
||||
|
||||
# ── Deliberately absent ──────────────────────────────────
|
||||
# No CONFIG_SND of any kind. The guest has no sound hardware and wants none:
|
||||
# neswire is a virtual PipeWire sink, and an ALSA stack would only add a second
|
||||
# thing that could claim to be the default output.
|
||||
|
||||
# Speculation mitigations are off, as a decision rather than an oversight.
|
||||
#
|
||||
# `lscpu` in the guest reports Spectre v1/v2, SSB, SRSO and TSA as Vulnerable,
|
||||
# and that is intended. This is a single-tenant sandbox: one player's session,
|
||||
# torn down at the end of it, with the VM boundary as the isolation. The
|
||||
# attacks these defend against are cross-tenant side channels, and the cost is
|
||||
# paid on every syscall and context switch in a workload that is latency
|
||||
# bound.
|
||||
#
|
||||
# What would change this: running two players' sessions on one box at the same
|
||||
# time, or anything of value living inside the guest. Neither is true today.
|
||||
# CONFIG_CPU_MITIGATIONS is not set
|
||||
@@ -1,140 +0,0 @@
|
||||
From 4dcb0baf29136c90776133afb714bac3c4cb7686 Mon Sep 17 00:00:00 2001
|
||||
From: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
|
||||
Date: Thu, 17 Sep 2026 21:50:25 +0300
|
||||
Subject: [PATCH] radv/amdgpu: cache AMDGPU_INFO_MEMORY briefly on the virtio
|
||||
path
|
||||
|
||||
Heap usage is asked for far more often than it changes. Measured on an
|
||||
amdgpu native context with one game running, RADV asked for it ~47 times
|
||||
per 8.8 ms frame -- the same answer, 47 times, each one a synchronous round
|
||||
trip to the host rather than an ioctl.
|
||||
|
||||
Serve a recent answer instead, for one millisecond. That is inside the
|
||||
contract of what the value is for: heap usage feeds VK_EXT_memory_budget
|
||||
and the winsys's own eviction decisions, and the spec calls those estimates
|
||||
that may be out of date. It is also short enough that an application
|
||||
allocating hard still sees its own pressure within a frame at any plausible
|
||||
frame rate.
|
||||
|
||||
Only on the virtio path, where a query costs a round trip; a local ioctl is
|
||||
cheap enough that caching it would be complexity for nothing. Only this
|
||||
query, and only at its natural size: every other AMDGPU_INFO_* either
|
||||
carries a caller-supplied selector in the union, so one cached answer would
|
||||
be the wrong answer to the next question, or is asked once at startup.
|
||||
|
||||
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||||
---
|
||||
src/amd/common/virtio/amdgpu_virtio.c | 53 +++++++++++++++++++
|
||||
src/amd/common/virtio/amdgpu_virtio_device.c | 1 +
|
||||
src/amd/common/virtio/amdgpu_virtio_private.h | 6 +++
|
||||
3 files changed, 60 insertions(+)
|
||||
|
||||
diff --git a/src/amd/common/virtio/amdgpu_virtio.c b/src/amd/common/virtio/amdgpu_virtio.c
|
||||
index 00494d6a750..dd38ef141f5 100644
|
||||
--- a/src/amd/common/virtio/amdgpu_virtio.c
|
||||
+++ b/src/amd/common/virtio/amdgpu_virtio.c
|
||||
@@ -19,8 +19,25 @@
|
||||
#include "drm-uapi/amdgpu_drm.h"
|
||||
|
||||
#include "util/log.h"
|
||||
+#include "util/os_time.h"
|
||||
#include "util/u_math.h"
|
||||
|
||||
+/* How long a cached AMDGPU_INFO_MEMORY answer is reused, in nanoseconds.
|
||||
+ *
|
||||
+ * Bounded by what the value is for. Heap usage feeds VK_EXT_memory_budget and
|
||||
+ * the winsys's own eviction decisions, and the spec calls those values
|
||||
+ * estimates that may be out of date -- so a millisecond of staleness is inside
|
||||
+ * the contract, while a round trip per ask is not free here the way it is on a
|
||||
+ * local ioctl.
|
||||
+ *
|
||||
+ * A millisecond rather than a frame: it is short enough that an application
|
||||
+ * allocating hard still sees its own pressure within a frame at any plausible
|
||||
+ * rate, and long enough to collapse the repeats. Measured on an amdgpu native
|
||||
+ * context with one game running, RADV asked for this ~47 times per 8.8 ms
|
||||
+ * frame -- the same answer, 47 synchronous round trips.
|
||||
+ */
|
||||
+#define AMDVGPU_MEMORY_INFO_TTL_NS (1000 * 1000)
|
||||
+
|
||||
int
|
||||
amdvgpu_query_info(amdvgpu_device_handle dev, struct drm_amdgpu_info *info)
|
||||
{
|
||||
@@ -32,6 +49,30 @@ amdvgpu_query_info(amdvgpu_device_handle dev, struct drm_amdgpu_info *info)
|
||||
struct amdgpu_ccmd_query_info_rsp *rsp;
|
||||
assert(0 == (offsetof(struct amdgpu_ccmd_query_info_rsp, payload) % 8));
|
||||
|
||||
+ /* AMDGPU_INFO_MEMORY is asked far more often than it changes, and over
|
||||
+ * virtio every ask is a synchronous round trip to the host rather than an
|
||||
+ * ioctl. Serve a recent answer instead.
|
||||
+ *
|
||||
+ * Only this query, and only at its natural size: every other query either
|
||||
+ * has a caller-supplied selector in the union -- so one cached answer would
|
||||
+ * be the wrong answer to the next question -- or is asked once at startup
|
||||
+ * and costs nothing.
|
||||
+ */
|
||||
+ bool cacheable = info->query == AMDGPU_INFO_MEMORY &&
|
||||
+ info->return_size == sizeof(struct drm_amdgpu_memory_info);
|
||||
+ if (cacheable) {
|
||||
+ int64_t now = os_time_get_nano();
|
||||
+ simple_mtx_lock(&dev->memory_info_mutex);
|
||||
+ if (dev->memory_info_stamp &&
|
||||
+ now - dev->memory_info_stamp < AMDVGPU_MEMORY_INFO_TTL_NS) {
|
||||
+ memcpy((void *)(uintptr_t)info->return_pointer, &dev->memory_info,
|
||||
+ sizeof(dev->memory_info));
|
||||
+ simple_mtx_unlock(&dev->memory_info_mutex);
|
||||
+ return 0;
|
||||
+ }
|
||||
+ simple_mtx_unlock(&dev->memory_info_mutex);
|
||||
+ }
|
||||
+
|
||||
req->hdr = AMDGPU_CCMD(QUERY_INFO, req_len);
|
||||
memcpy(&req->info, info, sizeof(struct drm_amdgpu_info));
|
||||
|
||||
@@ -43,6 +84,18 @@ amdvgpu_query_info(amdvgpu_device_handle dev, struct drm_amdgpu_info *info)
|
||||
|
||||
memcpy((void*)(uintptr_t)info->return_pointer, rsp->payload, info->return_size);
|
||||
|
||||
+ if (cacheable) {
|
||||
+ simple_mtx_lock(&dev->memory_info_mutex);
|
||||
+ memcpy(&dev->memory_info, rsp->payload, sizeof(dev->memory_info));
|
||||
+ /* Stamped after the answer is in hand, so the window covers the time the
|
||||
+ * value is actually served rather than the round trip that fetched it.
|
||||
+ * A zero stamp means "never fetched", so a clock that returns zero here
|
||||
+ * costs a re-fetch rather than pinning a stale answer forever.
|
||||
+ */
|
||||
+ dev->memory_info_stamp = os_time_get_nano();
|
||||
+ simple_mtx_unlock(&dev->memory_info_mutex);
|
||||
+ }
|
||||
+
|
||||
return 0;
|
||||
}
|
||||
|
||||
diff --git a/src/amd/common/virtio/amdgpu_virtio_device.c b/src/amd/common/virtio/amdgpu_virtio_device.c
|
||||
index eecfd6aa11a..7e58913d520 100644
|
||||
--- a/src/amd/common/virtio/amdgpu_virtio_device.c
|
||||
+++ b/src/amd/common/virtio/amdgpu_virtio_device.c
|
||||
@@ -136,6 +136,7 @@ int amdvgpu_device_initialize(int fd, uint32_t *drm_major, uint32_t *drm_minor,
|
||||
dev->vdev = vdev;
|
||||
|
||||
simple_mtx_init(&dev->handle_to_vbo_mutex, mtx_plain);
|
||||
+ simple_mtx_init(&dev->memory_info_mutex, mtx_plain);
|
||||
simple_mtx_init(&dev->contexts_mutex, mtx_plain);
|
||||
|
||||
dev->handle_to_vbo = _mesa_hash_table_u64_create(NULL);
|
||||
diff --git a/src/amd/common/virtio/amdgpu_virtio_private.h b/src/amd/common/virtio/amdgpu_virtio_private.h
|
||||
index 743f8f74b5c..fb877dc8d28 100644
|
||||
--- a/src/amd/common/virtio/amdgpu_virtio_private.h
|
||||
+++ b/src/amd/common/virtio/amdgpu_virtio_private.h
|
||||
@@ -60,6 +60,12 @@ struct amdvgpu_device {
|
||||
|
||||
struct drm_amdgpu_info_device dev_info;
|
||||
|
||||
+ /* AMDGPU_INFO_MEMORY, cached for a short while. See amdvgpu_query_info(). */
|
||||
+ simple_mtx_t memory_info_mutex;
|
||||
+ struct drm_amdgpu_memory_info memory_info;
|
||||
+ /* os_time_get_nano() when memory_info was fetched; 0 means never. */
|
||||
+ int64_t memory_info_stamp;
|
||||
+
|
||||
/* Blob id are per drm_file identifiers of host blobs.
|
||||
* Use a monotically increased integer to assign the blob id.
|
||||
*/
|
||||
@@ -0,0 +1,602 @@
|
||||
From 35db2e58006deca5c6383517f9228eb32a608548 Mon Sep 17 00:00:00 2001
|
||||
From: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
|
||||
Date: Wed, 23 Sep 2026 20:13:47 +0300
|
||||
Subject: [PATCH] virtio/vdrm, ac: log where threads wait, per wait and per
|
||||
second
|
||||
|
||||
A guest thread under a native context can be blocked on the host in more
|
||||
places than it looks: a synchronous request, a submission that holds
|
||||
eb_lock across an ioctl which itself waits on in-fences, a BO wait, or an
|
||||
ordinary syncobj wait. When a frame runs long there is no way to tell from
|
||||
the outside which of those it was, or whether it was any of them.
|
||||
|
||||
Two knobs, in milliseconds, both off when unset or 0:
|
||||
|
||||
- MESA_SLOW_WAIT_MS logs each single wait longer than the value, with its
|
||||
command, ring and how the time split between waiting for eb_lock, the
|
||||
submit or flush ioctl, the fence, and the host catching up.
|
||||
- MESA_WAIT_STATS logs, once a second per thread, the time and count spent
|
||||
in each kind of wait, when the total reached the value. Many short waits
|
||||
that add up to a long frame never trip a per-wait threshold; totals show
|
||||
them, and a second with a long frame can be compared with one without.
|
||||
|
||||
Syncobj waits are timed by wrapping the device's sync provider, since the
|
||||
Vulkan runtime waits through it directly and never through the ac_drm_cs_*
|
||||
helpers. The wrapper is installed only when a knob is set, and times only
|
||||
waits that could block; a poll is not.
|
||||
|
||||
Every line carries a UTC wall-clock stamp and the thread id, so it can be
|
||||
lined up with other components' logs. The shared pieces live in a header-only
|
||||
util/u_wait_log.h, so no build file changes.
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
src/amd/common/ac_linux_drm.c | 197 +++++++++++++++++++++++++++++++++
|
||||
src/util/u_wait_log.h | 133 ++++++++++++++++++++++
|
||||
src/virtio/vdrm/vdrm.c | 80 +++++++++++++
|
||||
src/virtio/vdrm/vdrm.h | 14 +++
|
||||
src/virtio/vdrm/vdrm_virtgpu.c | 20 +++-
|
||||
5 files changed, 443 insertions(+), 1 deletion(-)
|
||||
create mode 100644 src/util/u_wait_log.h
|
||||
|
||||
diff --git a/src/amd/common/ac_linux_drm.c b/src/amd/common/ac_linux_drm.c
|
||||
index 63b27058ec1..d2f16aefc83 100644
|
||||
--- a/src/amd/common/ac_linux_drm.c
|
||||
+++ b/src/amd/common/ac_linux_drm.c
|
||||
@@ -14,10 +14,205 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
+#include "util/u_wait_log.h"
|
||||
+
|
||||
#ifdef HAVE_AMDGPU_VIRTIO
|
||||
#include "virtio/amdgpu_virtio.h"
|
||||
#endif
|
||||
|
||||
+/* Wait logging for blocking syncobj waits -- what vkWaitForFences and timeline
|
||||
+ * semaphore waits come down to. See util/u_wait_log.h for the knobs; they are
|
||||
+ * the virtio transport's too, so one setting shows both a thread waiting on
|
||||
+ * the GPU and a thread waiting on the host. Polls (a zero timeout) are never
|
||||
+ * timed.
|
||||
+ *
|
||||
+ * Done by wrapping the device's sync provider rather than the ac_drm_cs_*
|
||||
+ * helpers, because the Vulkan runtime waits through the provider directly and
|
||||
+ * never calls those helpers: timing them saw almost nothing. The wrapper is
|
||||
+ * installed only when a knob is set, so it costs nothing otherwise.
|
||||
+ */
|
||||
+enum { SYNCOBJ_WAIT, TIMELINE_WAIT, WAIT_KINDS };
|
||||
+static const char *const wait_kinds[WAIT_KINDS] = { "syncobj", "timeline" };
|
||||
+static __thread struct u_wait_log_window wait_window;
|
||||
+
|
||||
+static void
|
||||
+note_wait(unsigned kind, int64_t t0, unsigned num_handles, int ret)
|
||||
+{
|
||||
+ int64_t took = os_time_get_nano() - t0;
|
||||
+ u_wait_log_account(&wait_window, "sync", wait_kinds, WAIT_KINDS, kind, took);
|
||||
+
|
||||
+ int64_t slow = u_wait_log_slow_ns();
|
||||
+ if (slow && took > slow) {
|
||||
+ char stamp[16];
|
||||
+ u_wait_log_stamp(stamp);
|
||||
+ mesa_logw("%s sync: %s wait on %u syncobj(s) took %.1f ms (ret %d) tid %d", stamp,
|
||||
+ wait_kinds[kind], num_handles, took / 1e6, ret, gettid());
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+struct timed_sync_provider {
|
||||
+ struct util_sync_provider base;
|
||||
+ struct util_sync_provider *inner;
|
||||
+};
|
||||
+
|
||||
+static struct util_sync_provider *
|
||||
+inner_of(struct util_sync_provider *p)
|
||||
+{
|
||||
+ return ((struct timed_sync_provider *)p)->inner;
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
+timed_create(struct util_sync_provider *p, uint32_t flags, uint32_t *handle)
|
||||
+{
|
||||
+ return inner_of(p)->create(inner_of(p), flags, handle);
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
+timed_destroy(struct util_sync_provider *p, uint32_t handle)
|
||||
+{
|
||||
+ return inner_of(p)->destroy(inner_of(p), handle);
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
+timed_handle_to_fd(struct util_sync_provider *p, uint32_t handle, int *out_obj_fd)
|
||||
+{
|
||||
+ return inner_of(p)->handle_to_fd(inner_of(p), handle, out_obj_fd);
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
+timed_fd_to_handle(struct util_sync_provider *p, int obj_fd, uint32_t *handle)
|
||||
+{
|
||||
+ return inner_of(p)->fd_to_handle(inner_of(p), obj_fd, handle);
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
+timed_import_sync_file(struct util_sync_provider *p, uint32_t handle, int sync_file_fd)
|
||||
+{
|
||||
+ return inner_of(p)->import_sync_file(inner_of(p), handle, sync_file_fd);
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
+timed_export_sync_file(struct util_sync_provider *p, uint32_t handle, int *out_sync_file_fd)
|
||||
+{
|
||||
+ return inner_of(p)->export_sync_file(inner_of(p), handle, out_sync_file_fd);
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
+timed_wait(struct util_sync_provider *p, uint32_t *handles, unsigned num_handles,
|
||||
+ int64_t timeout_nsec, unsigned flags, uint32_t *first_signaled)
|
||||
+{
|
||||
+ struct util_sync_provider *in = inner_of(p);
|
||||
+ if (!timeout_nsec)
|
||||
+ return in->wait(in, handles, num_handles, timeout_nsec, flags, first_signaled);
|
||||
+
|
||||
+ int64_t t0 = os_time_get_nano();
|
||||
+ int ret = in->wait(in, handles, num_handles, timeout_nsec, flags, first_signaled);
|
||||
+ note_wait(SYNCOBJ_WAIT, t0, num_handles, ret);
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
+timed_reset(struct util_sync_provider *p, const uint32_t *handles, uint32_t handle_count)
|
||||
+{
|
||||
+ return inner_of(p)->reset(inner_of(p), handles, handle_count);
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
+timed_signal(struct util_sync_provider *p, const uint32_t *handles, uint32_t handle_count)
|
||||
+{
|
||||
+ return inner_of(p)->signal(inner_of(p), handles, handle_count);
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
+timed_timeline_signal(struct util_sync_provider *p, const uint32_t *handles, uint64_t *points,
|
||||
+ uint32_t handle_count)
|
||||
+{
|
||||
+ return inner_of(p)->timeline_signal(inner_of(p), handles, points, handle_count);
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
+timed_timeline_wait(struct util_sync_provider *p, uint32_t *handles, uint64_t *points,
|
||||
+ unsigned num_handles, int64_t timeout_nsec, unsigned flags,
|
||||
+ uint32_t *first_signaled)
|
||||
+{
|
||||
+ struct util_sync_provider *in = inner_of(p);
|
||||
+ if (!timeout_nsec)
|
||||
+ return in->timeline_wait(in, handles, points, num_handles, timeout_nsec, flags,
|
||||
+ first_signaled);
|
||||
+
|
||||
+ int64_t t0 = os_time_get_nano();
|
||||
+ int ret = in->timeline_wait(in, handles, points, num_handles, timeout_nsec, flags,
|
||||
+ first_signaled);
|
||||
+ note_wait(TIMELINE_WAIT, t0, num_handles, ret);
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
+timed_query(struct util_sync_provider *p, uint32_t *handles, uint64_t *points,
|
||||
+ uint32_t handle_count, uint32_t flags)
|
||||
+{
|
||||
+ return inner_of(p)->query(inner_of(p), handles, points, handle_count, flags);
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
+timed_transfer(struct util_sync_provider *p, uint32_t dst_handle, uint64_t dst_point,
|
||||
+ uint32_t src_handle, uint64_t src_point, uint32_t flags)
|
||||
+{
|
||||
+ return inner_of(p)->transfer(inner_of(p), dst_handle, dst_point, src_handle, src_point,
|
||||
+ flags);
|
||||
+}
|
||||
+
|
||||
+static struct util_sync_provider *wrap_timed(struct util_sync_provider *inner);
|
||||
+
|
||||
+static void
|
||||
+timed_finalize(struct util_sync_provider *p)
|
||||
+{
|
||||
+ inner_of(p)->finalize(inner_of(p));
|
||||
+ free(p);
|
||||
+}
|
||||
+
|
||||
+static struct util_sync_provider *
|
||||
+timed_clone(struct util_sync_provider *p)
|
||||
+{
|
||||
+ struct util_sync_provider *cloned = inner_of(p)->clone(inner_of(p));
|
||||
+ return cloned ? wrap_timed(cloned) : NULL;
|
||||
+}
|
||||
+
|
||||
+/* Wrap `inner` so its blocking waits are timed. On allocation failure the
|
||||
+ * inner provider is returned as it is: logging is not worth failing a device.
|
||||
+ */
|
||||
+static struct util_sync_provider *
|
||||
+wrap_timed(struct util_sync_provider *inner)
|
||||
+{
|
||||
+ struct timed_sync_provider *t = calloc(1, sizeof(*t));
|
||||
+ if (!t)
|
||||
+ return inner;
|
||||
+ t->inner = inner;
|
||||
+ /* An operation the inner provider leaves NULL stays NULL here, since
|
||||
+ * callers test for it rather than call through it.
|
||||
+ */
|
||||
+#define FWD(op) .op = inner->op ? timed_##op : NULL
|
||||
+ t->base = (struct util_sync_provider) {
|
||||
+ FWD(create),
|
||||
+ FWD(destroy),
|
||||
+ FWD(handle_to_fd),
|
||||
+ FWD(fd_to_handle),
|
||||
+ FWD(import_sync_file),
|
||||
+ FWD(export_sync_file),
|
||||
+ FWD(wait),
|
||||
+ FWD(reset),
|
||||
+ FWD(signal),
|
||||
+ FWD(timeline_signal),
|
||||
+ FWD(timeline_wait),
|
||||
+ FWD(query),
|
||||
+ FWD(transfer),
|
||||
+ FWD(finalize),
|
||||
+ FWD(clone),
|
||||
+ };
|
||||
+#undef FWD
|
||||
+ return &t->base;
|
||||
+}
|
||||
+
|
||||
struct ac_drm_device {
|
||||
union {
|
||||
amdgpu_device_handle adev;
|
||||
@@ -82,6 +277,8 @@ int ac_drm_device_initialize(int fd, bool is_virtio,
|
||||
}
|
||||
|
||||
if (r == 0) {
|
||||
+ if (u_wait_log_enabled() && (*dev)->p)
|
||||
+ (*dev)->p = wrap_timed((*dev)->p);
|
||||
(*dev)->is_virtio = is_virtio;
|
||||
/* Device-static, so it is asked once here rather than on every heap
|
||||
* query. A failure is not fatal: the caller falls back to querying it.
|
||||
diff --git a/src/util/u_wait_log.h b/src/util/u_wait_log.h
|
||||
new file mode 100644
|
||||
index 00000000000..ce96256f371
|
||||
--- /dev/null
|
||||
+++ b/src/util/u_wait_log.h
|
||||
@@ -0,0 +1,133 @@
|
||||
+/*
|
||||
+ * SPDX-License-Identifier: MIT
|
||||
+ */
|
||||
+
|
||||
+/* Logging for time a thread spends waiting, for finding what a frame that
|
||||
+ * runs long was blocked on.
|
||||
+ *
|
||||
+ * Two knobs, both in milliseconds and both off when unset or 0:
|
||||
+ *
|
||||
+ * - MESA_SLOW_WAIT_MS: log each single wait longer than this.
|
||||
+ * - MESA_WAIT_STATS: once a second, per thread, log how much time went to
|
||||
+ * each kind of wait, if the total reached this. Many
|
||||
+ * short waits that add up to a long frame never trip a
|
||||
+ * per-wait threshold; totals show them.
|
||||
+ *
|
||||
+ * Every line carries a UTC wall-clock stamp, so it can be lined up with other
|
||||
+ * components' logs. Header-only, so each file that uses it keeps its own
|
||||
+ * per-thread window and reports under its own name.
|
||||
+ */
|
||||
+
|
||||
+#ifndef U_WAIT_LOG_H
|
||||
+#define U_WAIT_LOG_H
|
||||
+
|
||||
+#include <stdbool.h>
|
||||
+#include <stdint.h>
|
||||
+#include <stdio.h>
|
||||
+#include <string.h>
|
||||
+#include <time.h>
|
||||
+#include <unistd.h>
|
||||
+
|
||||
+#include "util/log.h"
|
||||
+#include "util/os_time.h"
|
||||
+#include "util/u_debug.h"
|
||||
+
|
||||
+#define U_WAIT_LOG_MAX_KINDS 8
|
||||
+
|
||||
+struct u_wait_log_window {
|
||||
+ int64_t start;
|
||||
+ int64_t ns[U_WAIT_LOG_MAX_KINDS];
|
||||
+ uint32_t n[U_WAIT_LOG_MAX_KINDS];
|
||||
+};
|
||||
+
|
||||
+/* A millisecond option as nanoseconds, read once into *cache. */
|
||||
+static inline int64_t
|
||||
+u_wait_log_option_ns(const char *name, int64_t *cache)
|
||||
+{
|
||||
+ if (*cache < 0)
|
||||
+ *cache = debug_get_num_option(name, 0) * 1000000ll;
|
||||
+ return *cache;
|
||||
+}
|
||||
+
|
||||
+static inline int64_t
|
||||
+u_wait_log_slow_ns(void)
|
||||
+{
|
||||
+ static int64_t cache = -1;
|
||||
+ return u_wait_log_option_ns("MESA_SLOW_WAIT_MS", &cache);
|
||||
+}
|
||||
+
|
||||
+static inline int64_t
|
||||
+u_wait_log_stats_ns(void)
|
||||
+{
|
||||
+ static int64_t cache = -1;
|
||||
+ return u_wait_log_option_ns("MESA_WAIT_STATS", &cache);
|
||||
+}
|
||||
+
|
||||
+/* Whether any wait timing is wanted at all. */
|
||||
+static inline bool
|
||||
+u_wait_log_enabled(void)
|
||||
+{
|
||||
+ return u_wait_log_slow_ns() || u_wait_log_stats_ns();
|
||||
+}
|
||||
+
|
||||
+/* "HH:MM:SS.mmm" in UTC. */
|
||||
+static inline void
|
||||
+u_wait_log_stamp(char buf[16])
|
||||
+{
|
||||
+ struct timespec ts;
|
||||
+ struct tm tm;
|
||||
+ clock_gettime(CLOCK_REALTIME, &ts);
|
||||
+ gmtime_r(&ts.tv_sec, &tm);
|
||||
+ snprintf(buf, 16, "%02d:%02d:%02d.%03ld", tm.tm_hour, tm.tm_min, tm.tm_sec,
|
||||
+ ts.tv_nsec / 1000000);
|
||||
+}
|
||||
+
|
||||
+/* Add one wait of `ns` nanoseconds of kind `kind` to this thread's window,
|
||||
+ * and report and reset the window once it is a second old.
|
||||
+ */
|
||||
+static inline void
|
||||
+u_wait_log_account(struct u_wait_log_window *w, const char *who,
|
||||
+ const char *const *kinds, unsigned num_kinds,
|
||||
+ unsigned kind, int64_t ns)
|
||||
+{
|
||||
+ int64_t floor = u_wait_log_stats_ns();
|
||||
+ if (!floor || kind >= num_kinds || num_kinds > U_WAIT_LOG_MAX_KINDS)
|
||||
+ return;
|
||||
+
|
||||
+ int64_t now = os_time_get_nano();
|
||||
+ if (!w->start)
|
||||
+ w->start = now;
|
||||
+ w->ns[kind] += ns;
|
||||
+ w->n[kind]++;
|
||||
+
|
||||
+ if (now - w->start < 1000000000ll)
|
||||
+ return;
|
||||
+
|
||||
+ int64_t total = 0;
|
||||
+ for (unsigned i = 0; i < num_kinds; i++)
|
||||
+ total += w->ns[i];
|
||||
+
|
||||
+ if (total >= floor) {
|
||||
+ char line[256];
|
||||
+ size_t len = 0;
|
||||
+ line[0] = '\0';
|
||||
+ for (unsigned i = 0; i < num_kinds && len < sizeof(line); i++) {
|
||||
+ if (!w->n[i])
|
||||
+ continue;
|
||||
+ int wrote = snprintf(line + len, sizeof(line) - len, " %s %.1f/%u",
|
||||
+ kinds[i], w->ns[i] / 1e6, w->n[i]);
|
||||
+ if (wrote < 0)
|
||||
+ break;
|
||||
+ len += wrote;
|
||||
+ }
|
||||
+ char stamp[16];
|
||||
+ u_wait_log_stamp(stamp);
|
||||
+ mesa_logw("%s %s: tid %d waited %.1f ms of %.0f:%s", stamp, who, gettid(),
|
||||
+ total / 1e6, (now - w->start) / 1e6, line);
|
||||
+ }
|
||||
+
|
||||
+ memset(w, 0, sizeof(*w));
|
||||
+ w->start = now;
|
||||
+}
|
||||
+
|
||||
+#endif /* U_WAIT_LOG_H */
|
||||
diff --git a/src/virtio/vdrm/vdrm.c b/src/virtio/vdrm/vdrm.c
|
||||
index d09df9bb99d..4819219cbc5 100644
|
||||
--- a/src/virtio/vdrm/vdrm.c
|
||||
+++ b/src/virtio/vdrm/vdrm.c
|
||||
@@ -4,10 +4,40 @@
|
||||
*/
|
||||
|
||||
#include "util/u_math.h"
|
||||
+#include "util/u_wait_log.h"
|
||||
#include "util/perf/cpu_trace.h"
|
||||
|
||||
#include "vdrm.h"
|
||||
|
||||
+/* Wait logging for the transport: see util/u_wait_log.h for the knobs.
|
||||
+ *
|
||||
+ * Every path here can stall the caller on the host, and execbuf holds eb_lock
|
||||
+ * while it does, which stalls every other thread of this device too. The
|
||||
+ * split into lock, flush, fence, host and submit time says which.
|
||||
+ */
|
||||
+static const char *const wait_kinds[VDRM_WAIT_COUNT] = {
|
||||
+ [VDRM_WAIT_LOCK] = "lock",
|
||||
+ [VDRM_WAIT_FLUSH] = "flush",
|
||||
+ [VDRM_WAIT_FENCE] = "fence",
|
||||
+ [VDRM_WAIT_HOST] = "host",
|
||||
+ [VDRM_WAIT_SUBMIT] = "submit",
|
||||
+ [VDRM_WAIT_BO] = "bo_wait",
|
||||
+};
|
||||
+
|
||||
+static __thread struct u_wait_log_window wait_window;
|
||||
+
|
||||
+void
|
||||
+vdrm_account_wait(enum vdrm_wait kind, int64_t ns)
|
||||
+{
|
||||
+ u_wait_log_account(&wait_window, "vdrm", wait_kinds, VDRM_WAIT_COUNT, kind, ns);
|
||||
+}
|
||||
+
|
||||
+static double
|
||||
+ms(int64_t ns)
|
||||
+{
|
||||
+ return ns / 1e6;
|
||||
+}
|
||||
+
|
||||
struct vdrm_device * vdrm_virtgpu_connect(int fd, uint32_t context_type);
|
||||
struct vdrm_device * vdrm_vpipe_connect(uint32_t context_type);
|
||||
|
||||
@@ -114,7 +144,11 @@ vdrm_execbuf(struct vdrm_device *vdev, struct vdrm_execbuf_params *p)
|
||||
|
||||
MESA_TRACE_FUNC();
|
||||
|
||||
+ bool timed = u_wait_log_enabled();
|
||||
+ int64_t t0 = timed ? os_time_get_nano() : 0;
|
||||
+
|
||||
simple_mtx_lock(&vdev->eb_lock);
|
||||
+ int64_t t_locked = timed ? os_time_get_nano() : 0;
|
||||
|
||||
p->req->seqno = ++vdev->next_seqno;
|
||||
|
||||
@@ -127,6 +161,23 @@ vdrm_execbuf(struct vdrm_device *vdev, struct vdrm_execbuf_params *p)
|
||||
out_unlock:
|
||||
simple_mtx_unlock(&vdev->eb_lock);
|
||||
|
||||
+ if (timed) {
|
||||
+ int64_t t_done = os_time_get_nano();
|
||||
+ vdrm_account_wait(VDRM_WAIT_LOCK, t_locked - t0);
|
||||
+ vdrm_account_wait(VDRM_WAIT_SUBMIT, t_done - t_locked);
|
||||
+
|
||||
+ int64_t slow = u_wait_log_slow_ns();
|
||||
+ if (slow && t_done - t0 > slow) {
|
||||
+ char stamp[16];
|
||||
+ u_wait_log_stamp(stamp);
|
||||
+ mesa_logw("%s vdrm: execbuf cmd %u ring %d took %.1f ms (lock %.1f, submit %.1f; "
|
||||
+ "%u in-syncobjs, in-fence %s) tid %d",
|
||||
+ stamp, p->req->cmd, p->ring_idx, ms(t_done - t0), ms(t_locked - t0),
|
||||
+ ms(t_done - t_locked), p->num_in_syncobjs,
|
||||
+ p->has_in_fence_fd ? "yes" : "no", gettid());
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -141,7 +192,11 @@ vdrm_send_req(struct vdrm_device *vdev, struct vdrm_ccmd_req *req, bool sync)
|
||||
uintptr_t fence = 0;
|
||||
int ret = 0;
|
||||
|
||||
+ bool timed = u_wait_log_enabled();
|
||||
+ int64_t t0 = timed ? os_time_get_nano() : 0;
|
||||
+
|
||||
simple_mtx_lock(&vdev->eb_lock);
|
||||
+ int64_t t_locked = timed ? os_time_get_nano() : 0;
|
||||
ret = enqueue_req(vdev, req);
|
||||
|
||||
if (ret || !sync)
|
||||
@@ -151,16 +206,41 @@ vdrm_send_req(struct vdrm_device *vdev, struct vdrm_ccmd_req *req, bool sync)
|
||||
|
||||
out_unlock:
|
||||
simple_mtx_unlock(&vdev->eb_lock);
|
||||
+ int64_t t_flushed = timed ? os_time_get_nano() : 0;
|
||||
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
+ int64_t t_fenced = t_flushed;
|
||||
if (sync) {
|
||||
MESA_TRACE_SCOPE("vdrm_execbuf sync");
|
||||
vdev->funcs->wait_fence(vdev, fence);
|
||||
+ if (timed)
|
||||
+ t_fenced = os_time_get_nano();
|
||||
vdrm_host_sync(vdev, req);
|
||||
}
|
||||
|
||||
+ if (timed) {
|
||||
+ int64_t t_done = os_time_get_nano();
|
||||
+ vdrm_account_wait(VDRM_WAIT_LOCK, t_locked - t0);
|
||||
+ vdrm_account_wait(VDRM_WAIT_FLUSH, t_flushed - t_locked);
|
||||
+ if (sync) {
|
||||
+ vdrm_account_wait(VDRM_WAIT_FENCE, t_fenced - t_flushed);
|
||||
+ vdrm_account_wait(VDRM_WAIT_HOST, t_done - t_fenced);
|
||||
+ }
|
||||
+
|
||||
+ int64_t slow = u_wait_log_slow_ns();
|
||||
+ if (slow && t_done - t0 > slow) {
|
||||
+ char stamp[16];
|
||||
+ u_wait_log_stamp(stamp);
|
||||
+ mesa_logw("%s vdrm: %s ccmd %u took %.1f ms (lock %.1f, flush %.1f, "
|
||||
+ "fence %.1f, host %.1f) tid %d",
|
||||
+ stamp, sync ? "sync" : "async", req->cmd, ms(t_done - t0),
|
||||
+ ms(t_locked - t0), ms(t_flushed - t_locked),
|
||||
+ ms(t_fenced - t_flushed), ms(t_done - t_fenced), gettid());
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
return 0;
|
||||
}
|
||||
|
||||
diff --git a/src/virtio/vdrm/vdrm.h b/src/virtio/vdrm/vdrm.h
|
||||
index fa676896f23..dbb6bfd2441 100644
|
||||
--- a/src/virtio/vdrm/vdrm.h
|
||||
+++ b/src/virtio/vdrm/vdrm.h
|
||||
@@ -114,6 +114,20 @@ int vdrm_execbuf(struct vdrm_device *vdev, struct vdrm_execbuf_params *p);
|
||||
|
||||
void vdrm_host_sync(struct vdrm_device *vdev, const struct vdrm_ccmd_req *req);
|
||||
|
||||
+/* Kinds of wait the transport accounts for; see util/u_wait_log.h. */
|
||||
+enum vdrm_wait {
|
||||
+ VDRM_WAIT_LOCK, /* waiting for eb_lock */
|
||||
+ VDRM_WAIT_FLUSH, /* the flush ioctl, lock held */
|
||||
+ VDRM_WAIT_FENCE, /* a sync request's fence */
|
||||
+ VDRM_WAIT_HOST, /* the host catching up after the fence */
|
||||
+ VDRM_WAIT_SUBMIT, /* the execbuf ioctl, lock held */
|
||||
+ VDRM_WAIT_BO, /* a BO wait */
|
||||
+ VDRM_WAIT_COUNT,
|
||||
+};
|
||||
+
|
||||
+/* Add one wait to this thread's per-second totals (MESA_WAIT_STATS). */
|
||||
+void vdrm_account_wait(enum vdrm_wait kind, int64_t ns);
|
||||
+
|
||||
/**
|
||||
* Import dmabuf fd returning a GEM handle
|
||||
*/
|
||||
diff --git a/src/virtio/vdrm/vdrm_virtgpu.c b/src/virtio/vdrm/vdrm_virtgpu.c
|
||||
index bccd4a5d279..9b9d5dc63c4 100644
|
||||
--- a/src/virtio/vdrm/vdrm_virtgpu.c
|
||||
+++ b/src/virtio/vdrm/vdrm_virtgpu.c
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "drm-uapi/virtgpu_drm.h"
|
||||
#include "util/libsync.h"
|
||||
#include "util/log.h"
|
||||
+#include "util/u_wait_log.h"
|
||||
#include "util/perf/cpu_trace.h"
|
||||
|
||||
|
||||
@@ -213,9 +214,26 @@ virtgpu_bo_wait(struct vdrm_device *vdev, uint32_t handle)
|
||||
};
|
||||
int ret;
|
||||
|
||||
+ bool timed = u_wait_log_enabled();
|
||||
+ int64_t t0 = timed ? os_time_get_nano() : 0;
|
||||
+
|
||||
/* Side note, this ioctl is defined as IO_WR but should be IO_W: */
|
||||
ret = virtgpu_ioctl(vgdev->fd, VIRTGPU_WAIT, &args);
|
||||
- if (ret && errno == EBUSY)
|
||||
+ int err = errno;
|
||||
+
|
||||
+ if (timed) {
|
||||
+ int64_t took = os_time_get_nano() - t0;
|
||||
+ vdrm_account_wait(VDRM_WAIT_BO, took);
|
||||
+ int64_t slow = u_wait_log_slow_ns();
|
||||
+ if (slow && took > slow) {
|
||||
+ char stamp[16];
|
||||
+ u_wait_log_stamp(stamp);
|
||||
+ mesa_logw("%s vdrm: bo_wait handle %u took %.1f ms tid %d", stamp, handle,
|
||||
+ took / 1e6, gettid());
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if (ret && err == EBUSY)
|
||||
return -EBUSY;
|
||||
|
||||
return 0;
|
||||
--
|
||||
2.55.0
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
kaldi's i386 and x86_64 builds share one source directory, and its cmake
|
||||
writes generated files into it, so proton-ge serializes the two with plain
|
||||
ordering rules. Those rules name the i386 steps unconditionally.
|
||||
|
||||
A wow64-only build has no i386-unix in ARCHS, so the i386 steps are never
|
||||
defined, and make stops with "no rule to make target .kaldi-i386-configure".
|
||||
Only order against them when they exist.
|
||||
|
||||
diff --git a/Makefile.in b/Makefile.in
|
||||
index f8d959c..9b92fec 100644
|
||||
--- a/Makefile.in
|
||||
+++ b/Makefile.in
|
||||
@@ -573,9 +573,13 @@ $(eval $(call rules-cmake,kaldi,aarch64,unix))
|
||||
# the same source directory.
|
||||
#
|
||||
# The below assures that only a single configure/build step can run at a time.
|
||||
+# The i386 steps exist only when i386-unix is in ARCHS; a wow64-only build has
|
||||
+# none, and ordering against them would ask for a target with no rule.
|
||||
+ifneq ($(findstring i386-unix,$(ARCHS)),)
|
||||
$(OBJ)/.kaldi-x86_64-configure: $(OBJ)/.kaldi-i386-configure
|
||||
$(OBJ)/.kaldi-i386-build: $(OBJ)/.kaldi-x86_64-configure
|
||||
$(OBJ)/.kaldi-i386-build: $(OBJ)/.kaldi-x86_64-build
|
||||
+endif
|
||||
|
||||
##
|
||||
## vosk
|
||||
@@ -0,0 +1,22 @@
|
||||
protonfixes pins unzip to Debian revision 6.0-29 and fetches it from the
|
||||
live pool. The pool keeps only current revisions, so once 6.0-31 landed the
|
||||
download started failing with a 404 and the build with it.
|
||||
|
||||
snapshot.debian.org keeps every revision Debian has published, at a stable
|
||||
path per timestamp. This is the first snapshot that carries 6.0-29, and the
|
||||
orig tarball has been in the pool since 2009, so both files resolve there
|
||||
and are the same bytes the pin always meant.
|
||||
|
||||
diff --git a/protonfixes/Makefile b/protonfixes/Makefile
|
||||
index 4dcd851..8fab279 100644
|
||||
--- a/protonfixes/Makefile
|
||||
+++ b/protonfixes/Makefile
|
||||
@@ -146,7 +146,7 @@ DEFINES = -DACORN_FTYPE_NFS -DWILD_STOP_AT_DIR -DLARGE_FILE_SUPPORT \
|
||||
-DNO_WORKING_ISPRINT
|
||||
UNZIP_VERSION := 6.0
|
||||
UNZIP_DEBIAN_REVISION := 29
|
||||
-UNZIP_BASE_URL := https://deb.debian.org/debian/pool/main/u/unzip
|
||||
+UNZIP_BASE_URL := https://snapshot.debian.org/archive/debian/20250311T215724Z/pool/main/u/unzip
|
||||
UNZIP_ORIG_TARBALL := unzip_$(UNZIP_VERSION).orig.tar.gz
|
||||
UNZIP_DEBIAN_TARBALL := unzip_$(UNZIP_VERSION)-$(UNZIP_DEBIAN_REVISION).debian.tar.xz
|
||||
UNZIP_DOWNLOAD_DIR := $(OBJDIR)/downloads/unzip
|
||||
@@ -0,0 +1,43 @@
|
||||
The x86_64 wine build gets VKD3D_PE_LIBS with a -L to the x86_64 vkd3d
|
||||
DLLs. That is right while a separate i386 wine build exists and gets its
|
||||
own value.
|
||||
|
||||
A wow64-only build has no i386 wine build: the x86_64 one builds the i386
|
||||
PE side as well, and wine takes one VKD3D_PE_LIBS for every PE arch. The
|
||||
i386 wined3d.dll then links against the x86_64 libvkd3d-1.dll and fails
|
||||
with "file format not recognized". ld does not skip a mismatched DLL
|
||||
named with -l:, so listing both directories does not work either.
|
||||
|
||||
Under wow64, keep the library names in VKD3D_PE_LIBS and pass each arch
|
||||
its own directory through i386_LDFLAGS and x86_64_LDFLAGS, which wine's
|
||||
configure already takes per arch. The i386 vkd3d was only ever built
|
||||
as a dependency of the i386 wine build, so the x86_64 one now waits for it.
|
||||
|
||||
diff --git a/Makefile.in b/Makefile.in
|
||||
index f8d959c..2ac0b34 100644
|
||||
--- a/Makefile.in
|
||||
+++ b/Makefile.in
|
||||
@@ -635,9 +639,23 @@ WINE_aarch64_AUTOCONF_ARGS = \
|
||||
WINE_i386_AUTOCONF_ARGS += \
|
||||
VKD3D_PE_CFLAGS="-I$(VULKAN_HEADERS_i386_DST)/include -I$(VKD3D_i386_DST)/include/vkd3d" \
|
||||
VKD3D_PE_LIBS="-L$(VKD3D_i386_LIBDIR)/vkd3d/i386-windows -l:libvkd3d-1.dll -l:libvkd3d-shader-1.dll -l:libvkd3d-utils-1.dll"
|
||||
+ifneq ($(findstring i386-unix,$(ARCHS)),)
|
||||
WINE_x86_64_AUTOCONF_ARGS += \
|
||||
VKD3D_PE_CFLAGS="-I$(VULKAN_HEADERS_x86_64_DST)/include -I$(VKD3D_x86_64_DST)/include/vkd3d" \
|
||||
VKD3D_PE_LIBS="-L$(VKD3D_x86_64_LIBDIR)/vkd3d/x86_64-windows -l:libvkd3d-1.dll -l:libvkd3d-shader-1.dll -l:libvkd3d-utils-1.dll"
|
||||
+else
|
||||
+# Without i386-unix there is no i386 wine build, and the x86_64 one builds the
|
||||
+# i386 PE side too (wow64). It links both, but VKD3D_PE_LIBS is one value for
|
||||
+# every PE arch, so a -L in it would hand the i386 link the x86_64 DLLs. Name
|
||||
+# the libraries there and give each arch its own directory through its own
|
||||
+# LDFLAGS instead.
|
||||
+WINE_x86_64_AUTOCONF_ARGS += \
|
||||
+ VKD3D_PE_CFLAGS="-I$(VULKAN_HEADERS_x86_64_DST)/include -I$(VKD3D_x86_64_DST)/include/vkd3d" \
|
||||
+ VKD3D_PE_LIBS="-l:libvkd3d-1.dll -l:libvkd3d-shader-1.dll -l:libvkd3d-utils-1.dll"
|
||||
+WINE_i386-windows_LIBFLAGS = -L$(VKD3D_i386_LIBDIR)/vkd3d/i386-windows
|
||||
+WINE_x86_64-windows_LIBFLAGS = -L$(VKD3D_x86_64_LIBDIR)/vkd3d/x86_64-windows
|
||||
+$(OBJ)/.wine-x86_64-build: $(OBJ)/.vkd3d-i386-post-build
|
||||
+endif
|
||||
|
||||
WINE_DEPENDS = ffmpeg openfst kaldi vosk libxml2 libxkbcommon libpcap
|
||||
WINE_i386_DEPENDS = vkd3d
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the guest kernel and installs it at "${KERNEL_OUTPUT}". Runs on the
|
||||
# host, as yourself: nothing here needs root.
|
||||
#
|
||||
# The tree is CachyOS's fork, taken for its patches rather than its config.
|
||||
# Theirs is a desktop distro config with thousands of modules; this guest has
|
||||
# no /lib/modules at all. What must hold is kernel/nestri.fragment, merged onto
|
||||
# whatever .config the tree has and then verified, so a version bump that
|
||||
# quietly drops an option fails here instead of in a booted box.
|
||||
set -euo pipefail
|
||||
|
||||
: "${KERNEL_GIT:?}"
|
||||
: "${KERNEL_REF:?}"
|
||||
: "${KERNEL_SRC:?}"
|
||||
: "${KERNEL_OUTPUT:?}"
|
||||
|
||||
JOBS="${JOBS:-$(nproc)}"
|
||||
KERNEL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../kernel" && pwd)"
|
||||
FRAGMENT="${KERNEL_DIR}/nestri.fragment"
|
||||
SEED="${KERNEL_DIR}/base.config"
|
||||
|
||||
# Resolved now, because everything below runs from inside the tree.
|
||||
mkdir -p "$(dirname "${KERNEL_OUTPUT}")"
|
||||
KERNEL_OUTPUT="$(cd "$(dirname "${KERNEL_OUTPUT}")" && pwd)/$(basename "${KERNEL_OUTPUT}")"
|
||||
|
||||
# ── Source ──────────────────────────────────────────────
|
||||
if [[ ! -f "${KERNEL_SRC}/Makefile" ]]; then
|
||||
echo "kernel: cloning ${KERNEL_REF} into ${KERNEL_SRC}"
|
||||
mkdir -p "$(dirname "${KERNEL_SRC}")"
|
||||
git clone --depth=1 --branch "${KERNEL_REF}" "${KERNEL_GIT}" "${KERNEL_SRC}"
|
||||
else
|
||||
have="$(git -C "${KERNEL_SRC}" describe --tags --exact-match 2>/dev/null || echo unknown)"
|
||||
if [[ "${have}" != "${KERNEL_REF}" ]]; then
|
||||
# Not fatal: a bisect or a local patch is a legitimate reason to be off
|
||||
# the pinned ref, and silently checking it out would throw that away.
|
||||
echo "kernel: tree is at '${have}', KERNEL_REF pins '${KERNEL_REF}'; building what is there" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
cd "${KERNEL_SRC}"
|
||||
|
||||
# ── Infinity scheduler (experimental) ───────────────────
|
||||
# Applied once per tree, and recorded, because `patch -N` on an already patched
|
||||
# tree does not skip cleanly: it rejects every hunk. The whole series goes in or
|
||||
# none of it does -- upstream is explicit that a partial series misbehaves -- so
|
||||
# every patch is dry-run against the stacked result before any is applied.
|
||||
#
|
||||
# In this guest only the fair and rt halves do anything. virtio-gpu does not
|
||||
# use the DRM scheduler, so the gpu patch is compiled out with the rest of
|
||||
# drivers/gpu/drm/scheduler; it is applied anyway to keep the series whole.
|
||||
if [[ -n "${KERNEL_INFINITY:-}" ]]; then
|
||||
: "${INFINITY_GIT:?}" "${INFINITY_REV:?}" "${INFINITY_SERIES:?}" "${INFINITY_WORK:?}"
|
||||
stamp=".nestri-infinity-rev"
|
||||
|
||||
if [[ "$(git -C "${INFINITY_WORK}" rev-parse HEAD 2>/dev/null)" != "${INFINITY_REV}" ]]; then
|
||||
echo "kernel: fetching infinity-sched ${INFINITY_REV}"
|
||||
rm -rf "${INFINITY_WORK}"
|
||||
git init -q "${INFINITY_WORK}"
|
||||
git -C "${INFINITY_WORK}" fetch -q --depth=1 "${INFINITY_GIT}" "${INFINITY_REV}"
|
||||
git -C "${INFINITY_WORK}" checkout -q FETCH_HEAD
|
||||
fi
|
||||
series_dir="${INFINITY_WORK}/${INFINITY_SERIES}"
|
||||
[[ -f "${series_dir}/series" ]] || {
|
||||
echo "kernel: infinity-sched has no series at ${INFINITY_SERIES} for ${KERNEL_REF}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
have="$(cat "${stamp}" 2>/dev/null || true)"
|
||||
if [[ "${have}" == "${INFINITY_REV}" ]]; then
|
||||
echo "kernel: infinity series already applied"
|
||||
elif [[ -n "${have}" ]]; then
|
||||
echo "kernel: tree carries infinity ${have}, INFINITY_REV pins ${INFINITY_REV}" >&2
|
||||
echo "kernel: start the tree over with \`make kernel-clean\`" >&2
|
||||
exit 1
|
||||
else
|
||||
if [[ -n "$(git status --porcelain --untracked-files=no)" ]]; then
|
||||
echo "kernel: ${KERNEL_SRC} has local changes; not applying the series over them" >&2
|
||||
exit 1
|
||||
fi
|
||||
mapfile -t patches < <(grep -v '^[[:space:]]*\(#\|$\)' "${series_dir}/series")
|
||||
# git apply --check takes the whole list and checks each patch against
|
||||
# the result of the ones before it, which a per-file `patch --dry-run`
|
||||
# cannot do.
|
||||
git apply --check "${patches[@]/#/${series_dir}/}"
|
||||
for p in "${patches[@]}"; do
|
||||
echo "kernel: applying ${p}"
|
||||
# -F 0: zero fuzz. Offsets are fine; a hunk that only fits
|
||||
# approximately is a scheduler change landing somewhere it was not
|
||||
# written for.
|
||||
patch -p1 -N -F 0 --quiet < "${series_dir}/${p}"
|
||||
done
|
||||
echo "${INFINITY_REV}" > "${stamp}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Config ──────────────────────────────────────────────
|
||||
# A fresh tree has no .config. The seed is a known-good minimal config that
|
||||
# olddefconfig migrates to whatever version the tree is at; it only saves a
|
||||
# fresh tree from `make defconfig`, whose driver set is enormous next to what a
|
||||
# microVM needs. An existing .config is always preferred.
|
||||
if [[ ! -f .config ]]; then
|
||||
echo "kernel: seeding .config from kernel/base.config"
|
||||
cp "${SEED}" .config
|
||||
fi
|
||||
|
||||
echo "kernel: merging kernel/nestri.fragment"
|
||||
# -m merges without running a config target, so olddefconfig resolves
|
||||
# dependencies once, in one place.
|
||||
./scripts/kconfig/merge_config.sh -m .config "${FRAGMENT}" >/dev/null
|
||||
make olddefconfig >/dev/null
|
||||
|
||||
# ── Verify the fragment actually took ───────────────────
|
||||
# merge_config.sh warns about overridden symbols but exits 0, and olddefconfig
|
||||
# will happily drop an option whose dependencies are unmet. Neither is loud
|
||||
# enough for a setting whose failure mode is silent audio, so check the result
|
||||
# rather than the intent. Both halves count: an option that must be on, and one
|
||||
# that must be off.
|
||||
missing=()
|
||||
total=0
|
||||
while read -r want; do
|
||||
total=$((total + 1))
|
||||
case "${want}" in
|
||||
CONFIG_*) grep -qx "${want}" .config || missing+=("${want%%=*}") ;;
|
||||
"# "*) grep -qx "${want}" .config || missing+=("${want:2} (must be off)") ;;
|
||||
esac
|
||||
done < <(grep -E '^(CONFIG_[A-Z0-9_]+=|# CONFIG_[A-Z0-9_]+ is not set)' "${FRAGMENT}" \
|
||||
| sed -E 's/^(CONFIG_[A-Z0-9_]+=[^[:space:]#]+)[[:space:]]*#.*/\1/')
|
||||
|
||||
if (( ${#missing[@]} )); then
|
||||
echo "kernel: these fragment entries did not survive olddefconfig:" >&2
|
||||
printf '%s\n' "${missing[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "kernel: all ${total} fragment entries hold"
|
||||
|
||||
# ── Build ───────────────────────────────────────────────
|
||||
# -march goes in through KCFLAGS because mainline has no Kconfig for
|
||||
# microarchitecture levels. It is safe for the kernel even at x86-64-v3:
|
||||
# arch/x86/Makefile passes -mno-sse -mno-mmx -mno-sse2 -mno-avx and friends,
|
||||
# and gcc applies those as a mask over -march regardless of flag order, so the
|
||||
# kernel gets v3's integer ISA (BMI2, LZCNT, MOVBE) and its scheduling model
|
||||
# and never touches a vector register.
|
||||
make_args=()
|
||||
if [[ -n "${KERNEL_MARCH:-}" ]]; then
|
||||
make_args+=("KCFLAGS=-march=${KERNEL_MARCH}")
|
||||
echo "kernel: building with -march=${KERNEL_MARCH}"
|
||||
fi
|
||||
|
||||
# vmlinux, not bzImage: the guest is booted by an ELF loader with no
|
||||
# bootloader in the path, and a bzImage is a self-decompressing image behind a
|
||||
# real-mode setup header, not an ELF.
|
||||
make -j"${JOBS}" "${make_args[@]}" vmlinux
|
||||
|
||||
cp vmlinux "${KERNEL_OUTPUT}"
|
||||
echo "kernel: installed ${KERNEL_OUTPUT} ($(numfmt --to=iec "$(stat -c %s "${KERNEL_OUTPUT}")"))"
|
||||
+137
-83
@@ -1,96 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds proton-cachyos from the tree proton-fetch.sh laid down. Container-only.
|
||||
# Builds proton-ge wow64-only and leaves the finished tree in "${PROTON_WORK}/obj/dist".
|
||||
# Runs on the host, not in a container.
|
||||
#
|
||||
# The one thing that matters here is --enable-wow64: it builds wine so that
|
||||
# 32-bit Windows code runs inside a 64-bit unix process, thunking down to the
|
||||
# 64-bit host libraries. Without it, Proton needs a complete 32-bit host stack —
|
||||
# lib32 glibc, a second Mesa built for i686, and a second nescapture layer,
|
||||
# because a 32-bit game would load the 32-bit Vulkan loader and our 64-bit
|
||||
# capture layer would be invisible to it. With it, none of that exists.
|
||||
# It has to run on the host because proton-ge's build is itself container-driven:
|
||||
# `make` runs outside, and every step runs in the Steam Runtime SDK image, where
|
||||
# the toolchains live, through the engine it is configured with. There is no
|
||||
# mode without a container, and a container engine inside `podman build` is
|
||||
# nested containers, which is a lot of fragile setup for no gain. So the only
|
||||
# thing this script needs from the host is git, make and the engine. The
|
||||
# Makefile packages the result afterwards.
|
||||
#
|
||||
# The cost is that the distro package cannot be used: proton-cachyos-native is
|
||||
# packaged without the flag, which is exactly why it depends on lib32-*.
|
||||
# The one thing we change is the arch list: it becomes wow64-only, and that
|
||||
# change is the reason this is our own build and not a download. wow64 runs
|
||||
# 32-bit Windows code inside a 64-bit unix process. Without it Proton needs a
|
||||
# complete 32-bit host stack: lib32 glibc, a second Mesa built for i686, and a
|
||||
# second nescapture layer, because a 32-bit game would load the 32-bit Vulkan
|
||||
# loader and our 64-bit capture layer would be invisible to it. The released
|
||||
# builds carry an i386 unix side, which is exactly why they need lib32-*.
|
||||
#
|
||||
# Everything else is proton-ge's own recipe: the same SDK image, the same flags
|
||||
# and the same patch set. The one addition is patches/proton-ge/: fixes for the
|
||||
# places its makefile assumes a 32-bit unix side that wow64 does not have, and
|
||||
# for things a tag pinned that have since moved out from under it.
|
||||
set -euo pipefail
|
||||
|
||||
: "${GECKO_VER:?}"
|
||||
: "${MONO_VER:?}"
|
||||
: "${PROTON_GIT:?}"
|
||||
: "${PROTON_TAG:?}"
|
||||
: "${PROTON_WORK:?}"
|
||||
: "${BUILD_NAME:?}"
|
||||
|
||||
ENGINE="${CONTAINER_ENGINE:-podman}"
|
||||
JOBS="${JOBS:-$(nproc)}"
|
||||
BUILD_NAME="proton-cachyos"
|
||||
SRC_DIR="/build/proton-cachyos"
|
||||
BUILD_DIR="/build/build"
|
||||
OUT_DIR="/artifacts/proton/usr/share/steam/compatibilitytools.d/${BUILD_NAME}"
|
||||
|
||||
[[ -d "${SRC_DIR}" ]] || { echo "no source tree — proton-fetch.sh did not run"; exit 1; }
|
||||
mkdir -p "${PROTON_WORK}"
|
||||
PROTON_WORK="$(cd "${PROTON_WORK}" && pwd)"
|
||||
SRC="${PROTON_WORK}/src"
|
||||
OBJ="${PROTON_WORK}/obj"
|
||||
STAMP_TAG="${PROTON_WORK}/.tag"
|
||||
STAMP_PATCHED="${PROTON_WORK}/.patched"
|
||||
PATCH_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../patches/proton-ge" && pwd)"
|
||||
|
||||
# ── Toolchain wrappers ──────────────────────────────────
|
||||
# Proton's build calls the compiler by GNU triplet. Arch's gcc does not install
|
||||
# under those names, so stand in for them. The i686 set is generated too: with
|
||||
# wow64 nothing should reach for it, and if something does, failing on a missing
|
||||
# 32-bit header beats silently building a 32-bit unix library we then have to
|
||||
# ship libraries for.
|
||||
WRAP=/build/wrappers
|
||||
rm -rf "$WRAP" && mkdir -p "$WRAP"
|
||||
_wrappers() {
|
||||
local arch="$1" gccflag="$2" ldflag="$3" asflag="$4" stripfmt="$5"
|
||||
local l t
|
||||
for l in ar ranlib nm; do
|
||||
ln -sf "/usr/bin/gcc-${l}" "${WRAP}/${arch}-pc-linux-gnu-${l}"
|
||||
# Two builds in one tree do not fail cleanly. They race on the same objects and
|
||||
# leave half-written files that a later build trusts. A failed make also keeps
|
||||
# running its in-flight jobs for a while after it reports the error, so the
|
||||
# first build is often still running when the second one starts.
|
||||
exec 9>"${PROTON_WORK}/.lock"
|
||||
flock -n 9 || { echo "proton: another build is using ${PROTON_WORK}" >&2; exit 1; }
|
||||
|
||||
# ccache and cargo's downloads are kept outside src/ and obj/, so a new tag or
|
||||
# FORCE_REBUILD throws away the build and keeps the parts that are correct to
|
||||
# reuse. proton-ge's makefile mounts both into the container from these
|
||||
# variables.
|
||||
export CCACHE_DIR="${PROTON_WORK}/ccache"
|
||||
export CARGO_HOME="${PROTON_WORK}/cargo"
|
||||
mkdir -p "${CCACHE_DIR}" "${CARGO_HOME}"
|
||||
|
||||
if [[ -n "${FORCE_REBUILD:-}" || "$(cat "${STAMP_TAG}" 2>/dev/null)" != "${PROTON_TAG}" ]]; then
|
||||
echo "proton: fresh tree for ${PROTON_TAG}"
|
||||
rm -rf "${SRC}" "${OBJ}" "${STAMP_TAG}" "${STAMP_PATCHED}"
|
||||
fi
|
||||
|
||||
# ── Fetch ───────────────────────────────────────────────
|
||||
if [[ ! -e "${STAMP_TAG}" ]]; then
|
||||
rm -rf "${SRC}"
|
||||
git clone --branch "${PROTON_TAG}" --depth=1 "${PROTON_GIT}" "${SRC}"
|
||||
# No --depth here: submodules are pinned to commits that are often not a
|
||||
# branch tip. --filter=tree:0 keeps the download down instead.
|
||||
git -C "${SRC}" submodule update --init --filter=tree:0 --recursive
|
||||
echo "${PROTON_TAG}" > "${STAMP_TAG}"
|
||||
fi
|
||||
|
||||
# The SDK image is pinned by proton-ge's own makefile, per tag. Asking it keeps
|
||||
# the patch step below and the build on the same image.
|
||||
SDK_IMAGE="$(make --silent --no-print-directory -f "${SRC}/Makefile.in" \
|
||||
SRCDIR="${SRC}" get-steamrt-image)"
|
||||
|
||||
# ── Patch ───────────────────────────────────────────────
|
||||
# The patch script edits the tree in place and is not idempotent: it resets
|
||||
# some submodules first and not others. So a tree is patched once, and one that
|
||||
# was interrupted halfway is reset to the commits the tag pins before trying
|
||||
# again.
|
||||
#
|
||||
# It is run in the SDK image rather than on the host, so it does not depend on
|
||||
# the host's python, patch or wget.
|
||||
#
|
||||
# The script carries on past a patch that does not apply and exits 0 anyway.
|
||||
# The upstream instructions are to grep its output for failures, so that is
|
||||
# what happens here. The alternative is an image that looks fine and is missing
|
||||
# a fix.
|
||||
#
|
||||
# A build tree does not survive its source being re-patched. Changing
|
||||
# Makefile.in re-syncs every component's source copy, but a component's
|
||||
# configure step depends on that sync order-only, so it does not rerun, and its
|
||||
# old build directory is left pointing at generated autotools files the sync
|
||||
# just removed. So patching starts obj/ over too. ccache keeps that cheap.
|
||||
if [[ ! -e "${STAMP_PATCHED}" ]]; then
|
||||
rm -rf "${OBJ}"
|
||||
git -C "${SRC}" reset -q --hard
|
||||
git -C "${SRC}" submodule foreach -q --recursive 'git reset -q --hard && git clean -qfdx'
|
||||
"${ENGINE}" run --rm -v "${SRC}:${SRC}" -w "${SRC}" "${SDK_IMAGE}" \
|
||||
./patches/protonprep-valve-staging.sh 2>&1 | tee "${PROTON_WORK}/patch.log"
|
||||
if grep -Ei 'hunk .* failed|saving rejects|can.t find file|malformed patch|skipping patch|^error' \
|
||||
"${PROTON_WORK}/patch.log"; then
|
||||
echo "proton: patches did not apply cleanly, see ${PROTON_WORK}/patch.log" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Ours go on top. They are paths from the root of the tree, submodules
|
||||
# included. `git apply` fails outright on a patch that no longer applies,
|
||||
# which is what a tag bump should do: each one says why it exists, so the
|
||||
# question is only whether upstream fixed it.
|
||||
for p in "${PATCH_DIR}"/*.patch; do
|
||||
[[ -e "$p" ]] || continue
|
||||
echo "proton: applying $(basename "$p")"
|
||||
git -C "${SRC}" apply "$p"
|
||||
done
|
||||
for t in gcc g++; do
|
||||
printf '#!/usr/bin/bash\n/usr/bin/%s %s "$@"\n' "$t" "$gccflag" \
|
||||
> "${WRAP}/${arch}-pc-linux-gnu-${t}"
|
||||
chmod 755 "${WRAP}/${arch}-pc-linux-gnu-${t}"
|
||||
done
|
||||
printf '#!/usr/bin/bash\n/usr/bin/ld %s "$@"\n' "$ldflag" > "${WRAP}/${arch}-pc-linux-gnu-ld"
|
||||
printf '#!/usr/bin/bash\n/usr/bin/as %s "$@"\n' "$asflag" > "${WRAP}/${arch}-pc-linux-gnu-as"
|
||||
printf '#!/usr/bin/bash\n/usr/bin/strip -F %s "$@"\n' "$stripfmt" > "${WRAP}/${arch}-pc-linux-gnu-strip"
|
||||
chmod 755 "${WRAP}/${arch}-pc-linux-gnu-"{ld,as,strip}
|
||||
}
|
||||
_wrappers x86_64 "-m64" "-melf_x86_64" "--64" "elf64-x86-64"
|
||||
_wrappers i686 "-m32" "-melf_i386" "--32" "elf32-i386"
|
||||
export PATH="${WRAP}:${PATH}"
|
||||
touch "${STAMP_PATCHED}"
|
||||
fi
|
||||
|
||||
# ── Configure ───────────────────────────────────────────
|
||||
# configure.sh refuses an in-tree build, and it test-runs the SDK image to work
|
||||
# out how the engine maps file ownership, so it is also where a broken engine
|
||||
# setup shows up first.
|
||||
mkdir -p "${OBJ}"
|
||||
if [[ ! -e "${OBJ}/Makefile" ]]; then
|
||||
(cd "${OBJ}" && "${SRC}/configure.sh" \
|
||||
--build-name="${BUILD_NAME}" \
|
||||
--container-engine="${ENGINE}")
|
||||
fi
|
||||
|
||||
# ── Build ───────────────────────────────────────────────
|
||||
# -march=nocona matches the distro packaging: Proton has to run on whatever CPU
|
||||
# the guest is given, and the VMM does not promise a feature level.
|
||||
export CFLAGS="-O3 -march=nocona -mtune=core-avx2"
|
||||
export CXXFLAGS="${CFLAGS}"
|
||||
export RUSTFLAGS="-C opt-level=3 -C target-cpu=nocona"
|
||||
export LDFLAGS="-Wl,-O1,--sort-common,--as-needed"
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
# ARCHS drops i386-unix, which leaves wine configured for x86_64 unix with an
|
||||
# i386 PE side. That is wow64. Every component rule is gated on ARCHS, so the
|
||||
# 32-bit unix builds of everything else go with it. ENABLE_WOW64 makes the
|
||||
# proton script ask wine for a wow64 prefix. proton-ge ships it as a switch
|
||||
# but never turns it on.
|
||||
#
|
||||
# A command-line variable reaches the container build too: the outer make
|
||||
# hands its overrides to the inner one.
|
||||
#
|
||||
# SOURCE_DATE_EPOCH is the tag's commit time rather than now, so two builds of
|
||||
# one tag stamp the same dates into their output.
|
||||
make -C "${OBJ}" \
|
||||
J="${JOBS}" \
|
||||
ARCHS="i386-windows x86_64-windows x86_64-unix" \
|
||||
ENABLE_WOW64=1 \
|
||||
SOURCE_DATE_EPOCH="$(git -C "${SRC}" log -1 --format=%ct)" \
|
||||
dist
|
||||
|
||||
mkdir -p "${BUILD_DIR}"
|
||||
cd "${BUILD_DIR}"
|
||||
|
||||
ROOTLESS_CONTAINER="" \
|
||||
"${SRC_DIR}/configure.sh" \
|
||||
--container-engine="none" \
|
||||
--proton-sdk-image="" \
|
||||
--build-name="${BUILD_NAME}" \
|
||||
--without-extras=all \
|
||||
--without-vklayers=all \
|
||||
--without-steamrt-depends \
|
||||
--without-tts \
|
||||
--without-nvidia-libs \
|
||||
--enable-wow64
|
||||
|
||||
# The top-level make is serial by design; SUBJOBS is what it hands to each
|
||||
# component's build.
|
||||
SUBJOBS="${JOBS}" make -j1 dist
|
||||
|
||||
# ── Install ─────────────────────────────────────────────
|
||||
mkdir -p "${OUT_DIR}"
|
||||
cp -a "${BUILD_DIR}/dist/." "${OUT_DIR}/"
|
||||
|
||||
# Debug symbols in the bundled PE runtimes are dead weight in a guest image.
|
||||
cd "${OUT_DIR}/files"
|
||||
find "share/wine/gecko/wine-gecko-${GECKO_VER}-x86" -name '*.dll' -o -name '*.exe' 2>/dev/null \
|
||||
| xargs -r i686-w64-mingw32-strip --strip-debug 2>/dev/null || true
|
||||
find "share/wine/gecko/wine-gecko-${GECKO_VER}-x86_64" -name '*.dll' -o -name '*.exe' 2>/dev/null \
|
||||
| xargs -r x86_64-w64-mingw32-strip --strip-debug 2>/dev/null || true
|
||||
find "share/wine/mono/wine-mono-${MONO_VER}" -name '*.dll' -o -name '*.exe' 2>/dev/null \
|
||||
| xargs -r i686-w64-mingw32-strip --strip-debug 2>/dev/null || true
|
||||
|
||||
rm -rf "${BUILD_DIR}"
|
||||
echo "proton: installed to ${OUT_DIR}"
|
||||
echo "proton: built ${OBJ}/dist"
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fetches proton-cachyos' source and its bundled runtimes. Container-only.
|
||||
#
|
||||
# Deliberately its own script, and its own layer: the submodule checkout runs
|
||||
# well past ten minutes, and it must not be redone every time a build flag or a
|
||||
# missing dependency changes. Keep everything that can fail *fast* in
|
||||
# proton-build.sh instead.
|
||||
set -euo pipefail
|
||||
|
||||
: "${PROTON_GIT:?}"
|
||||
: "${PROTON_TAG:?}"
|
||||
: "${GECKO_VER:?}"
|
||||
: "${MONO_VER:?}"
|
||||
: "${XALIA_VER:?}"
|
||||
|
||||
SRC_DIR="/build/proton-cachyos"
|
||||
|
||||
git clone --branch "${PROTON_TAG}" --depth=1 "${PROTON_GIT}" "${SRC_DIR}"
|
||||
cd "${SRC_DIR}"
|
||||
# Relative submodule paths resolve against origin, so it has to be the real URL
|
||||
# even though we cloned by tag.
|
||||
git remote set-url origin "${PROTON_GIT}"
|
||||
# No --depth here: submodules are pinned to commits that are often not a branch
|
||||
# tip. --filter=tree:0 keeps the download down instead.
|
||||
git submodule update --init --filter=tree:0 --recursive
|
||||
|
||||
# Still needed with wow64: these are PE-side, and a 32-bit Windows program wants
|
||||
# the 32-bit gecko and mono regardless of how wine is built.
|
||||
mkdir -p contrib
|
||||
for url in \
|
||||
"https://dl.winehq.org/wine/wine-gecko/${GECKO_VER}/wine-gecko-${GECKO_VER}-x86.tar.xz" \
|
||||
"https://dl.winehq.org/wine/wine-gecko/${GECKO_VER}/wine-gecko-${GECKO_VER}-x86_64.tar.xz" \
|
||||
"https://github.com/madewokherd/wine-mono/releases/download/wine-mono-${MONO_VER}/wine-mono-${MONO_VER}-x86.tar.xz" \
|
||||
"https://github.com/madewokherd/xalia/releases/download/xalia-${XALIA_VER}/xalia-${XALIA_VER}-net48-mono.zip" \
|
||||
; do
|
||||
curl -fL --retry 3 -o "contrib/$(basename "$url")" "$url"
|
||||
done
|
||||
|
||||
# Proton's cargo rule runs `cargo build --locked --offline`, so every crate has
|
||||
# to be in CARGO_HOME before the build starts — including the git dependencies,
|
||||
# which is what the "you are in the offline mode" failure is really saying. The
|
||||
# error names a URL that is perfectly reachable; the build simply refuses to go
|
||||
# out and get it.
|
||||
#
|
||||
# gst-plugins-rs is the only cargo component in the tree. Both targets are
|
||||
# fetched: wow64 should mean nothing builds the i386 unix side, but a fetch is
|
||||
# metadata only and costs almost nothing next to being wrong about that.
|
||||
#
|
||||
# CARGO_HOME is left at its default so it lands in this layer and the build
|
||||
# layer inherits it.
|
||||
export CARGO_NET_GIT_FETCH_WITH_CLI=true
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
cd "${SRC_DIR}/gst-plugins-rs"
|
||||
cargo fetch --locked --target x86_64-unknown-linux-gnu
|
||||
cargo fetch --locked --target i686-unknown-linux-gnu
|
||||
|
||||
echo "proton: source at ${SRC_DIR}"
|
||||
Reference in New Issue
Block a user