diff --git a/apps/nescope/src/handlers.rs b/apps/nescope/src/handlers.rs index 2a0d341e..075ec57f 100644 --- a/apps/nescope/src/handlers.rs +++ b/apps/nescope/src/handlers.rs @@ -100,22 +100,27 @@ impl CompositorHandler for NescopeState { s }; - let Some(window) = self + // Matched on the window's surface, not its xdg toplevel: an Xwayland + // window has no toplevel. + let window = self .space .elements() - .find(|w| { - w.toplevel() - .map(|t| t.wl_surface() == &root) - .unwrap_or(false) - }) - .cloned() - else { - return; - }; + .find(|w| w.wl_surface().is_some_and(|s| *s == root)) + .cloned(); + if let Some(window) = &window { + window.on_commit(); + self.game_frame_count += 1; + } - window.on_commit(); - self.game_frame_count += 1; - self.present_now(&root, &window); + // Every commit is answered, mapped window or not. A Vulkan client + // presenting FIFO waits in the driver for this frame callback, and + // not every surface a game draws to is a window here: Chromium's GPU + // process draws for a window another process owns, which Wine's + // Wayland driver gives a surface of its own with no role. Unanswered, + // that game presents one frame and waits forever. There is no screen + // here to pace against -- capture paces itself -- so answering every + // commit costs nothing a mapped window does not already get. + self.present_now(&root); } fn destroyed(&mut self, surface: &WlSurface) { diff --git a/apps/nescope/src/state.rs b/apps/nescope/src/state.rs index b3d13fb6..b682ae66 100644 --- a/apps/nescope/src/state.rs +++ b/apps/nescope/src/state.rs @@ -735,13 +735,14 @@ impl NescopeState { } } - pub(crate) fn present_now(&mut self, root: &WlSurface, window: &Window) { + pub(crate) fn present_now(&mut self, root: &WlSurface) { let output = self.output.clone(); let now = self.clock.now(); self.presentation_seq += 1; let mut fb = OutputPresentationFeedback::new(&output); - window.take_presentation_feedback( + take_presentation_feedback_surface_tree( + root, &mut fb, |_, _| Some(output.clone()), |_, _| wp_presentation_feedback::Kind::Vsync, diff --git a/apps/nesinit/src/system.rs b/apps/nesinit/src/system.rs index 5a952c9a..8246b58a 100644 --- a/apps/nesinit/src/system.rs +++ b/apps/nesinit/src/system.rs @@ -219,11 +219,26 @@ fn devices() { /// or Wine, and the process dies — here as a `wineserver crashed` with the game /// already gone. SteamOS and every distribution that packages Proton raise it /// to this value for that reason. -const SYSCTLS: &[(&str, &str, &str)] = &[( - "vm/max_map_count", - "2147483642", - "a game that maps more than 65530 regions (Unreal Engine 5 does) dies on a failed allocation", -)]; +/// - `vm.overcommit_memory`: **the kernel's default refuses any one allocation +/// larger than RAM plus swap**, and a box has no swap, so the ceiling is the +/// box's size: 8 GiB at 1080p. Engines and drivers reserve address space far +/// past what they touch, and a reservation is refused before a byte of it is +/// used — Dome Keeper asked for 9 GiB on an 8 GiB box and never started +/// (2026-10-14). `1` grants every request and leaves the OOM killer to act on +/// what is actually used; in a box that is the game, since this process has +/// already refused it. +const SYSCTLS: &[(&str, &str, &str)] = &[ + ( + "vm/max_map_count", + "2147483642", + "a game that maps more than 65530 regions (Unreal Engine 5 does) dies on a failed allocation", + ), + ( + "vm/overcommit_memory", + "1", + "a game that reserves more address space than the box has RAM is refused it and does not start", + ), +]; /// How many files a process may hold open, which every process in the box /// inherits from here. @@ -678,6 +693,23 @@ mod tests { assert!("2147483642".parse::().unwrap() > 65530 * 1000); } + #[test] + fn a_reservation_bigger_than_the_box_is_granted() { + let root = scratch("overcommit"); + std::fs::create_dir_all(root.join("vm")).unwrap(); + std::fs::write(root.join("vm/overcommit_memory"), "0\n").unwrap(); + + apply_sysctls(&root); + + assert_eq!( + std::fs::read_to_string(root.join("vm/overcommit_memory")) + .unwrap() + .trim(), + "1" + ); + let _ = std::fs::remove_dir_all(&root); + } + /// A box on a kernel without the knob still boots; the cost is said, not /// raised. #[test] diff --git a/build/Containerfile b/build/Containerfile index 0a068d7e..4ff69045 100644 --- a/build/Containerfile +++ b/build/Containerfile @@ -80,6 +80,13 @@ ARG MESA_GIT=https://gitlab.freedesktop.org/mesa/mesa.git ARG MESA_COMMIT=a4a47003a194effad8cabe727f74985e9417476f ARG JOBS= +# OpenGL is zink: GL on top of whichever Vulkan driver the box has, radv over +# native context or the forwarded NVIDIA one, so one build serves both. Its +# frontends have to be on for any of it to be reachable. With EGL, GLX, GBM and +# GLES off, a GL game found no GL at all and ran with sound and a black screen, +# which is what three alpha testers saw. GLX needs the x11 platform for +# Xwayland's clients, and Xwayland needs GBM to give them DRI3. +# # 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 @@ -93,17 +100,18 @@ RUN test -n "$JOBS" || JOBS=$(nproc) && \ meson setup builddir \ -Dprefix=/usr \ -Dbuildtype=release \ - -Dplatforms=wayland \ + -Dplatforms=wayland,x11 \ -Dgallium-drivers=zink \ -Dvulkan-drivers=amd,intel \ -Damdgpu-virtio=true \ -Dintel-virtio-experimental=true \ -Dvideo-codecs=all \ - -Degl=disabled \ - -Dglx=disabled \ + -Degl=enabled \ + -Dglx=dri \ + -Dglvnd=enabled \ -Dgles1=disabled \ - -Dgles2=disabled \ - -Dgbm=disabled \ + -Dgles2=enabled \ + -Dgbm=enabled \ -Dgallium-va=disabled \ -Db_ndebug=true && \ mold -run ninja -C builddir -j${JOBS:-$(nproc)} && \ diff --git a/build/Containerfile.proton b/build/Containerfile.proton index a8721017..172eea6f 100644 --- a/build/Containerfile.proton +++ b/build/Containerfile.proton @@ -110,6 +110,18 @@ RUN pacman -Syu --noconfirm --needed \ libsm libtheora xz \ && pacman -Scc --noconfirm +# Our own patches, one directory per submodule they apply to: patches/proton/ +# wine/ applies in wine/. Its own layer, after the fetch and the dependencies, +# so a patch change costs the build and nothing before it. A patch that no +# longer applies to a new tag stops here, before the hours of building. +COPY patches/proton /build/patches/proton +RUN set -e; cd /build/patches/proton; \ + for dir in */; do \ + for p in "${dir}"*.patch; do \ + echo "applying ${p}"; \ + patch -d "/build/proton-cachyos/${dir%/}" -p1 --forward < "${p}"; \ + done; \ + done 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 diff --git a/build/Makefile b/build/Makefile index d167742b..be252cfb 100644 --- a/build/Makefile +++ b/build/Makefile @@ -38,8 +38,13 @@ 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_PATCHES` counts our own changes on top of that tag (patches/proton/). +# Bump it whenever they change, so a patched build never takes the name of the +# one before it; reset it to 0 when the tag moves. PROTON_TAG ?= cachyos-11.0-20261005-native -PROTON_VERSION := $(PROTON_TAG:cachyos-%-native=%) +PROTON_PATCHES ?= 1 +PROTON_VERSION := $(PROTON_TAG:cachyos-%-native=%)-n$(PROTON_PATCHES) PROTON_IMAGE ?= ghcr.io/nestrilabs/proton-cachyos-native-wow64 PROTON_REF := $(PROTON_IMAGE):$(PROTON_VERSION) OUTPUT_DIR := output diff --git a/build/patches/proton/wine/0001-ntdll-read-an-ASCII-locale-as-UTF-8.patch b/build/patches/proton/wine/0001-ntdll-read-an-ASCII-locale-as-UTF-8.patch new file mode 100644 index 00000000..50060c15 --- /dev/null +++ b/build/patches/proton/wine/0001-ntdll-read-an-ASCII-locale-as-UTF-8.patch @@ -0,0 +1,14 @@ +--- a/dlls/ntdll/unix/env.c ++++ b/dlls/ntdll/unix/env.c +@@ -286,6 +286,11 @@ + + if (!(ctype = setlocale( LC_CTYPE, "" ))) name = "UTF-8"; + else if (!(name = nl_langinfo( CODESET ))) return; ++ /* The C and POSIX locales name plain ASCII, in which a non-ASCII file ++ * name cannot be created at all. Steam starts games with LC_ALL=C, so ++ * under it every such name failed; ASCII is a subset of UTF-8, so ++ * reading it as UTF-8 changes nothing that worked before. */ ++ if (!strcmp( name, "ANSI_X3.4-1968" )) name = "UTF-8"; + TRACE_(nls)( "Unix LC_CTYPE %s, using %s codeset\n", debugstr_a(ctype), debugstr_a(name) ); + + /* remove punctuation characters from charset name */