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
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=src/protocols/gamescope-swapchain.xml");
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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>
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user