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:
Kristian Ollikainen
2026-09-25 12:13:34 +03:00
committed by GitHub
co-authored by DatCaptainHorse Claude Opus 5 Wanjohi
parent 1c721962f4
commit 0811f57f1a
64 changed files with 15151 additions and 2702 deletions
-2
View File
@@ -28,8 +28,6 @@ smithay = { version = "0.7", default-features = false, features = [
# Wayland client – connects to the host compositor to forward buffers.
wayland-client = "0.31"
wayland-protocols = { version = "0.32", features = ["client", "staging", "server"] }
# Needed to generate the gamescope_swapchain protocol bindings.
wayland-scanner = "0.31"
wayland-backend = "0.3"
# Event loop
-3
View File
@@ -1,3 +0,0 @@
fn main() {
println!("cargo:rerun-if-changed=src/protocols/gamescope-swapchain.xml");
}
+25 -2
View File
@@ -11,6 +11,7 @@ use smithay::desktop::Window;
use smithay::input::pointer::{CursorImageStatus, PointerHandle};
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_buffer;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
@@ -119,7 +120,6 @@ impl CompositorHandler for NescopeState {
fn destroyed(&mut self, surface: &WlSurface) {
self.hdr.surface_destroyed(surface);
self.vulkan_surfaces.remove(surface);
}
}
@@ -138,7 +138,7 @@ impl DmabufHandler for NescopeState {
_dmabuf: Dmabuf,
notifier: ImportNotifier,
) {
// Accept unconditionally — libhudless reads buffers
// Accept unconditionally — the nescapture layer reads buffers
// directly from the game's Vulkan queue; nescope doesn't need to.
let _ = notifier.successful::<NescopeState>();
}
@@ -187,6 +187,29 @@ impl XdgShellHandler for NescopeState {
self.determine_and_apply_focus();
}
// Granted, not merely acknowledged. The default answers with a configure
// that lacks the fullscreen state, which a client reads as a refusal: Wine
// then asks again on every window update and never treats its window as
// fullscreen, so a game switching to exclusive fullscreen -- Control does
// this on leaving its title screen -- stalls in the transition and stays
// where it was. Wine also scales an emulated display mode up to the output
// only for a fullscreen window.
fn fullscreen_request(&mut self, surface: ToplevelSurface, _output: Option<WlOutput>) {
surface.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
state.size = Some((self.width as i32, self.height as i32).into());
});
surface.send_configure();
}
// Still the size of the output: there is nowhere else for a window to be.
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
surface.with_pending_state(|state| {
state.states.unset(xdg_toplevel::State::Fullscreen);
});
surface.send_configure();
}
fn new_popup(&mut self, _: PopupSurface, _: PositionerState) {}
fn grab(&mut self, _: PopupSurface, _: WlSeat, _: Serial) {}
fn reposition_request(&mut self, _: PopupSurface, _: PositionerState, _: u32) {}
+564 -286
View File
File diff suppressed because it is too large Load Diff
-20
View File
@@ -106,11 +106,6 @@ pub fn process_input(event: InputEvent, state: &mut NescopeState) {
_ => state.last_pointer_activity = std::time::Instant::now(),
}
// One-time X11 focus reset when the gamescope WSI surface is active.
if state.override_surface.is_some() && state.x11_focus_needs_reset {
state.sync_x11_focus();
}
match event {
InputEvent::KeyDown { keycode } => {
if let Some(kb) = state.seat.get_keyboard() {
@@ -363,21 +358,6 @@ fn clamp_cursor(state: &mut NescopeState) {
/// Find the focused target under the current cursor position.
pub fn surface_under(state: &NescopeState) -> Option<(KeyboardFocusTarget, Point<f64, Logical>)> {
if state.override_surface.is_some() {
if let Some(wid) = state.focused_x11_window {
for window in state.space.elements() {
if let Some(x11) = window.x11_surface() {
if x11.window_id() == wid {
let loc = state.space.element_geometry(window)?.loc;
return Some((KeyboardFocusTarget::Window(window.clone()), loc.to_f64()));
}
}
}
}
let (window, loc) = state.space.element_under(state.cursor_position)?;
return Some((KeyboardFocusTarget::Window(window.clone()), loc.to_f64()));
}
// Try element_under first
if let Some((window, loc)) = state.space.element_under(state.cursor_position) {
return Some((KeyboardFocusTarget::Window(window.clone()), loc.to_f64()));
+44 -28
View File
@@ -4,7 +4,7 @@
//!
//! nescope creates a virtual Wayland output, starts XWayland, and gives games
//! a complete compositor environment. Frames are captured externally by a
//! Vulkan interception library (`hudless`); nescope itself
//! Vulkan interception library (`nescapture`); nescope itself
//! never allocates a GBM pool or forwards DMA-BUFs.
//!
//! # Usage
@@ -17,7 +17,7 @@
//! --height <N> Output height [default: 1080]
//! --fps <N> Virtual refresh rate, advertised only [default: 60]
//! --frame-callback-hz <N> wl_surface.frame cadence [default: 1000]
//! --hdr Enable HDR protocols (wp_color_management_v1 + gamescope_swapchain)
//! --hdr Enable HDR colour management (wp_color_manager_v1)
//! --socket <NAME> Wayland socket name [default: nescope-0]
//! ```
//!
@@ -67,7 +67,6 @@ mod hdr;
mod input;
mod input_ipc;
mod libinput_backend;
mod protocols;
//mod screenshot_ipc;
//mod screenshot_wire;
mod state;
@@ -80,6 +79,22 @@ use state::{CalloopData, ClientState, NescopeState};
// CLI
// ---------------------------------------------------------------------------
/// A flag that can also arrive as an environment variable.
///
/// `--hdr` on its own still means true. The difference is what a value may be:
/// clap's own bool parser takes `true` and `false` and nothing else, so
/// `NESCOPE_HDR=1` -- which is how every other environment variable in this
/// stack is written, and the first thing anyone tries -- was rejected outright.
fn flag_value(value: &str) -> Result<bool, String> {
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Ok(true),
"0" | "false" | "no" | "off" | "" => Ok(false),
other => Err(std::format!(
"expected 1 or 0 (true/false, yes/no and on/off are also taken), got {other:?}"
)),
}
}
#[derive(Parser, Debug)]
#[command(
name = "nescope",
@@ -125,8 +140,15 @@ struct Args {
#[arg(long, default_value = "1000", env = "NESCOPE_FRAME_CALLBACK_HZ")]
frame_callback_hz: u32,
/// Enable HDR protocols (wp_color_management_v1 + gamescope_swapchain_factory_v2).
#[arg(long, env = "NESCOPE_HDR")]
/// Enable HDR colour management (`wp_color_manager_v1`).
#[arg(
long,
env = "NESCOPE_HDR",
num_args = 0..=1,
default_value_t = false,
default_missing_value = "true",
value_parser = flag_value,
)]
hdr: bool,
/// Run XWayland, for Linux-native software with no Wayland support.
@@ -137,7 +159,14 @@ struct Args {
/// which is what the launch environment does -- and HDR is only offered on
/// the Wayland surface, so a game routed through XWayland loses it too.
/// Turn this on for the shrinking set of X11-only native software.
#[arg(long, env = "NESCOPE_XWAYLAND")]
#[arg(
long,
env = "NESCOPE_XWAYLAND",
num_args = 0..=1,
default_value_t = false,
default_missing_value = "true",
value_parser = flag_value,
)]
xwayland: bool,
/// Wayland socket name (created in $XDG_RUNTIME_DIR).
@@ -422,8 +451,7 @@ fn main() {
// wrong way round, and it capped them at 60 while sessions asked for 120.
// The capture layer holds the game instead, and this runs fast enough to
// stay out of the way.
let frame_interval =
Duration::from_micros(1_000_000 / args.frame_callback_hz.max(1) as u64);
let frame_interval = Duration::from_micros(1_000_000 / args.frame_callback_hz.max(1) as u64);
loop_handle
.insert_source(Timer::from_duration(frame_interval), move |_, _, data| {
if let Some(ref mut li) = data.libinput {
@@ -437,7 +465,7 @@ fn main() {
// ── CalloopData ───────────────────────────────────────────────────────
let socket_name_for_cleanup = args.socket.clone();
let command = args.command.clone();
let gamescope_wayland_socket = args.socket.clone();
let wayland_socket = args.socket.clone();
// ── libinput backend ─────────────────────────────────────────────────
let libinput_ctx =
@@ -491,7 +519,7 @@ fn main() {
// Put the game in its own process group so we can
// kill the whole tree at once with kill(-pgid, …).
.process_group(0)
.env("WAYLAND_DISPLAY", &gamescope_wayland_socket);
.env("WAYLAND_DISPLAY", &wayland_socket);
// DISPLAY only if XWayland is actually running. Setting it
// otherwise points clients at a server that is not there,
@@ -516,24 +544,6 @@ fn main() {
// this. Without it neither DX11 nor DX12 (vkd3d-proton
// through DXVK's dxgi) sees HDR as available.
cmd.env("DXVK_HDR", "1");
// Left set, but deliberately without ENABLE_GAMESCOPE_WSI
// alongside it, so it is inert unless somebody opts in.
//
// That pair activates gamescope's WSI layer, which
// predates Wayland colour management and works by
// hiding HDR from the driver and reporting it to the
// compositor out of band. We do not want it: it needs a
// layer this image does not ship, it only helps the
// XWayland path, and capture reads the colour space it
// hides -- measured, a game asking for HDR10 through it
// has its PQ samples encoded and tagged BT.709 SDR.
// Enabling it would trade no HDR for wrong HDR.
tracing::debug!(
gamescope_wayland_socket,
"HDR: Wayland colour management; gamescope WSI not enabled"
);
cmd.env("GAMESCOPE_WAYLAND_DISPLAY", &gamescope_wayland_socket);
}
// Detect GPU vendor from render device and set VK_DRIVER_FILES
@@ -625,6 +635,12 @@ fn main() {
}
}
// Answer any colour-management information requests that came in
// this iteration. Deferred to here because the event that ends
// them destroys the object, and doing that inside the request that
// created it panics the backend -- see .
data.state.hdr.flush_information();
// ── Flush Wayland clients ─────────────────────────────────
if let Err(e) = data.display.flush_clients() {
tracing::warn!("Error flushing Wayland clients: {e}");
@@ -1,194 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="gamescope_swapchain">
<copyright>
Copyright © 2023 Joshua Ashton for Valve Software
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</copyright>
<description summary="gamescope-specific swapchain protocol">
This is a private Gamescope protocol. Regular Wayland clients must not use
it.
</description>
<interface name="gamescope_swapchain_factory_v2" version="1">
<request name="destroy" type="destructor"></request>
<request name="create_swapchain">
<description summary="create Gamescope swapchain interface">
</description>
<arg name="surface" type="object" interface="wl_surface"
summary="target surface"/>
<arg name="callback" type="new_id" interface="gamescope_swapchain"
summary="new swapchain object"/>
</request>
</interface>
<interface name="gamescope_swapchain" version="1">
<request name="destroy" type="destructor"></request>
<request name="override_window_content">
<description summary="override an X11's window wl_surface">
Xwayland creates a wl_surface for each X11 window. It sends a
WL_SURFACE_ID client message to indicate the mapping between the X11
windows and the wl_surface objects.
This request overrides this mapping for a given X11 window, allowing an
X11 client to submit buffers via the Wayland protocol. The override
only affects buffer submission. Everything else (e.g. input events)
still uses Xwayland's WL_SURFACE_ID.
x11_server is gotten by the GAMESCOPE_XWAYLAND_SERVER_ID property on the
root window of the associated server.
</description>
<arg name="gamescope_xwayland_server_id" type="uint" summary="gamescope xwayland server ID"/>
<arg name="x11_window" type="uint" summary="X11 window ID"/>
</request>
<request name="swapchain_feedback">
<description summary="provide swapchain feedback">
Provide swapchain feedback to the compositor.
This is what the useless tearing protocol should have been.
Absolutely not enough information in the final protocol to do what we want for SteamOS --
which is have the Allow Tearing toggle apply to *both* Mailbox + Immediate and NOT fifo,
essentially acting as an override for tearing on/off for games.
The upstream protocol is very useless for our usecase here.
Provides image count ahead of time instead of needing to try and calculate it from
an initial stall if we are doing low latency.
Provides colorspace info for us to do HDR for both HDR10 PQ and scRGB.
The upstream HDR efforts seem to have no interest in supporting scRGB but we *need* that so /shrug
We can do it here now! Yipee!
Swapchain feedback solves so many problems! :D
</description>
<arg name="image_count" type="uint" summary="image count of swapchain"/>
<arg name="vk_format" type="uint" summary="VkFormat of swapchain"/>
<arg name="vk_colorspace" type="uint" summary="VkColorSpaceKHR of swapchain"/>
<arg name="vk_composite_alpha" type="uint" summary="VkCompositeAlphaFlagBitsKHR of swapchain"/>
<arg name="vk_pre_transform" type="uint" summary="VkSurfaceTransformFlagBitsKHR of swapchain"/>
<arg name="vk_clipped" type="uint" summary="clipped (VkBool32) of swapchain"/>
<arg name="vk_engine_name" type="string" summary="Engine name"/>
</request>
<request name="set_present_mode">
<description summary="Add a fifo queue constraint"/>
<arg name="vk_present_mode" type="uint" summary="VkPresentModeKHR"/>
</request>
<request name="set_hdr_metadata">
<description summary="set HDR metadata for a surface">
Forward HDR metadata from Vulkan to the compositor.
HDR Metadata Infoframe as per CTA 861.G spec.
This is expected to match exactly with the spec.
display_primary_*:
Color Primaries of the Data.
Specifies X and Y coordinates.
These are coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
white_point_*:
White Point of Colorspace Data.
Specifies X and Y coordinates.
These are coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
max_display_mastering_luminance:
Max Mastering Display Luminance.
This value is coded as an unsigned 16-bit value in units of 1 cd/m2,
where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2.
max_display_mastering_luminance:
Min Mastering Display Luminance.
This value is coded as an unsigned 16-bit value in units of
0.0001 cd/m2, where 0x0001 represents 0.0001 cd/m2 and 0xFFFF
represents 6.5535 cd/m2.
max_cll:
Max Content Light Level.
This value is coded as an unsigned 16-bit value in units of 1 cd/m2,
where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2.
max_fall:
Max Frame Average Light Level.
This value is coded as an unsigned 16-bit value in units of 1 cd/m2,
where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2.
</description>
<arg name="display_primary_red_x" type="uint" summary="red primary x coordinate"/>
<arg name="display_primary_red_y" type="uint" summary="red primary y coordinate"/>
<arg name="display_primary_green_x" type="uint" summary="green primary x coordinate"/>
<arg name="display_primary_green_y" type="uint" summary="green primary y coordinate"/>
<arg name="display_primary_blue_x" type="uint" summary="blue primary x coordinate"/>
<arg name="display_primary_blue_y" type="uint" summary="blue primary y coordinate"/>
<arg name="white_point_x" type="uint" summary="white point x coordinate"/>
<arg name="white_point_y" type="uint" summary="white point y coordinate"/>
<arg name="max_display_mastering_luminance" type="uint" summary="max display mastering luminance"/>
<arg name="min_display_mastering_luminance" type="uint" summary="min display mastering luminance"/>
<arg name="max_cll" type="uint" summary="max content light level"/>
<arg name="max_fall" type="uint" summary="max frame average light level"/>
</request>
<request name="set_present_time">
<description summary="display timing of next commit">
Sets the display timing of the next commit.
This gets reset to 0s in the compositor's state after a commit.
</description>
<arg name="present_id" type="uint" summary="application provided presentation id"/>
<arg name="desired_present_time_hi" type="uint" summary="high part of the desired presentation time for this commit. Uses CLOCK_MONOTONIC. 0 = present as soon as possible."/>
<arg name="desired_present_time_lo" type="uint" summary="low part of the desired presentation time for this commit. Uses CLOCK_MONOTONIC. 0 = present as soon as possible."/>
</request>
<event name="past_present_timing">
<description summary="information about past presentation">
Gives information on the past presentation timing
</description>
<arg name="present_id" type="uint" summary="application provided presentation id"/>
<arg name="desired_present_time_hi" type="uint" summary="high part of the desired presentation time for the commit. (from the app)"/>
<arg name="desired_present_time_lo" type="uint" summary="low part of the desired presentation time for the commit. (from the app)"/>
<arg name="actual_present_time_hi" type="uint" summary="high part of the actual present time for this commit."/>
<arg name="actual_present_time_lo" type="uint" summary="low part of the actual present time for this commit."/>
<arg name="earliest_present_time_hi" type="uint" summary="high part of the refresh time that Gamescope thought this commit was done by."/>
<arg name="earliest_present_time_lo" type="uint" summary="low part of the refresh time that Gamescope thought this commit was done by."/>
<arg name="present_margin_hi" type="uint" summary="high part of the difference between earliest present time and the earliest latch time"/>
<arg name="present_margin_lo" type="uint" summary="low part of the difference between earliest present time and the earliest latch time"/>
</event>
<event name="refresh_cycle">
<description summary="information about refresh cycle for this swapchain">
Gives information on the refresh cycle for this swapchain
</description>
<arg name="refresh_cycle_hi" type="uint" summary="high part of the refresh cycle in nanos"/>
<arg name="refresh_cycle_lo" type="uint" summary="low part of the refresh cycle in nanos"/>
</event>
<event name="retired">
<description summary="Swapchain was remotely retired"></description>
</event>
</interface>
</protocol>
-16
View File
@@ -1,16 +0,0 @@
//! Generated Wayland protocol bindings for the gamescope swapchain protocol.
#![allow(non_upper_case_globals, non_camel_case_types, unused)]
use smithay::reexports::wayland_server;
use wayland_server::protocol::*;
pub mod __interfaces {
use super::wayland_server;
use wayland_server::backend as wayland_backend;
use wayland_server::protocol::__interfaces::*;
wayland_scanner::generate_interfaces!("src/protocols/gamescope-swapchain.xml");
}
use self::__interfaces::*;
wayland_scanner::generate_server_code!("src/protocols/gamescope-swapchain.xml");
+27 -41
View File
@@ -24,6 +24,7 @@ use calloop::channel::Sender;
use smithay::desktop::utils::{
OutputPresentationFeedback, send_frames_surface_tree,
surface_presentation_feedback_flags_from_states, surface_primary_scanout_output,
take_presentation_feedback_surface_tree,
};
use smithay::desktop::{Space, Window};
use smithay::input::pointer::CursorImageStatus;
@@ -96,13 +97,18 @@ impl ClientData for ClientState {
// X11 atoms + connection
// ---------------------------------------------------------------------------
/// Gamescope-compatible X11 atoms used for focus and HDR signalling.
/// Gamescope-compatible X11 atoms, for focus.
///
/// Focus only. HDR used to be signalled through one of these as well, back
/// when a WSI layer inside the game reported the colour space out of band;
/// that route is gone and `wp_color_manager_v1` carries it. These remain
/// because Steam reads them to work out which window it is looking at, and
/// that is unrelated to colour.
pub struct CachedAtoms {
pub net_active_window: u32,
pub gamescope_focused_app: u32,
pub gamescope_focusable_apps: u32,
pub gamescope_focusable_windows: u32,
pub gamescope_hdr_output_feedback: u32,
pub gamescope_xwayland_server_id: u32,
pub xa_window: u32,
pub xa_cardinal: u32,
@@ -157,10 +163,6 @@ pub struct NescopeState {
pub focused_app_id: u32,
/// True when X11 focus needs to be re-synced on the next input event.
pub x11_focus_needs_reset: bool,
/// Gamescope WSI override surface (direct Vulkan → Wayland bypass).
pub override_surface: Option<WlSurface>,
/// Surfaces that have announced themselves as Vulkan via gamescope protocol.
pub vulkan_surfaces: HashSet<WlSurface>,
// ── Input ─────────────────────────────────────────────────────────────
/// Sender half of the input channel — clone and hand to callers.
@@ -267,7 +269,7 @@ impl NescopeState {
let (dmabuf_state, dmabuf_global) =
build_dmabuf_global::<Self>(&display_handle, render_device.as_deref());
// HDR + gamescope swapchain globals (optional).
// Colour management, when asked for.
let hdr_state = HdrState::new(&display_handle, hdr);
// Input channel — the Sender is returned to the caller.
@@ -309,8 +311,6 @@ impl NescopeState {
focused_x11_window: None,
focused_app_id: 0,
x11_focus_needs_reset: false,
override_surface: None,
vulkan_surfaces: HashSet::new(),
input_tx: input_tx.clone(),
cursor_position: Point::from((0.0f64, 0.0f64)),
cursor_status: CursorImageStatus::default_named(),
@@ -398,10 +398,6 @@ impl NescopeState {
gamescope_focused_app: intern_atom(&conn, b"GAMESCOPE_FOCUSED_APP"),
gamescope_focusable_apps: intern_atom(&conn, b"GAMESCOPE_FOCUSABLE_APPS"),
gamescope_focusable_windows: intern_atom(&conn, b"GAMESCOPE_FOCUSABLE_WINDOWS"),
gamescope_hdr_output_feedback: intern_atom(
&conn,
b"GAMESCOPE_HDR_OUTPUT_FEEDBACK",
),
gamescope_xwayland_server_id: intern_atom(
&conn,
b"GAMESCOPE_XWAYLAND_SERVER_ID",
@@ -422,8 +418,10 @@ impl NescopeState {
}
}
/// Write gamescope-specific X11 root window properties so the WSI layer
/// can discover this compositor as a gamescope-compatible server.
/// Write the gamescope-compatible X11 root window properties.
///
/// These say which application has focus and what could take it, which is
/// what Steam looks for. Nothing here concerns colour.
pub fn set_gamescope_atoms(
&self,
conn: &smithay::reexports::x11rb::rust_connection::RustConnection,
@@ -438,14 +436,6 @@ impl NescopeState {
let replace = PropMode::REPLACE;
let cardinal = AtomEnum::CARDINAL;
// HDR output feedback — set to 1 when HDR is active.
let _ = conn.change_property32(
replace,
root,
atoms.gamescope_hdr_output_feedback,
cardinal,
&[1u32],
);
// XWayland server ID — always 0 for a standalone compositor.
let _ = conn.change_property32(
replace,
@@ -465,16 +455,6 @@ impl NescopeState {
tracing::debug!("Set gamescope atoms on display :{display_number}");
}
// -----------------------------------------------------------------------
// Override surface (gamescope WSI bypass)
// -----------------------------------------------------------------------
/// Register the gamescope WSI override surface for an X11 window.
pub fn override_window_surface(&mut self, x11_window: u32, surface: WlSurface) {
tracing::debug!(x11_window, "Registered gamescope WSI override surface");
self.override_surface = Some(surface);
}
// -----------------------------------------------------------------------
// Resize
// -----------------------------------------------------------------------
@@ -769,12 +749,24 @@ impl NescopeState {
// 1. Release the held buffer → frees a swapchain image for the game.
self.held_buffer.take();
// 2. Presentation feedback — tell clients about vsync timing.
// 2. Presentation feedback — every frame committed since the last tick
// is reported presented, on the one output there is.
//
// Not filtered by primary scan-out output: that is recorded by a
// renderer, and nothing here renders, so the filter matched no
// surface ever. Feedback then resolved only as `discarded`, when the
// next commit superseded it -- which never happens for a client that
// waits for its last present before drawing the next. A Vulkan
// client with present-wait under FIFO does exactly that: Control on
// VKD3D-Proton froze on leaving its title screen, GPU idle, the game
// still running behind a stream that no longer moved.
let mut output_presentation_feedback = OutputPresentationFeedback::new(&output);
let on_output =
|_: &WlSurface, _: &smithay::wayland::compositor::SurfaceData| Some(output.clone());
for window in self.space.elements().cloned().collect::<Vec<_>>() {
window.take_presentation_feedback(
&mut output_presentation_feedback,
surface_primary_scanout_output,
on_output,
|_, _| wp_presentation_feedback::Kind::Vsync,
);
}
@@ -797,12 +789,6 @@ impl NescopeState {
}
}
if let Some(ref s) = self.override_surface {
send_frames_surface_tree(s, &output, now, Some(Duration::ZERO), |_, _| {
Some(output.clone())
});
}
// 4. Send periodic stats over IPC
let now = std::time::Instant::now();
if now.duration_since(self.last_stats_time) >= std::time::Duration::from_secs(1) {