wip(nescapture): per-window capture

This commit is contained in:
Wanjohi
2026-10-25 13:59:47 +03:00
parent 714759b5de
commit 10bb1405d6
7 changed files with 335 additions and 127 deletions
+8 -48
View File
@@ -249,31 +249,17 @@ pub unsafe extern "system" fn vkCreateDevice(
hudless_image: std::sync::Mutex::new(None),
hudless_memory: std::sync::Mutex::new(None),
hudless_size: std::sync::Mutex::new((0, 0, vk::Format::UNDEFINED)),
capture_ring: std::sync::Mutex::new(None),
capture_slots: crate::slots::SlotPool::new(crate::state::CAPTURE_SLOTS),
swapchain: std::sync::Mutex::new(None),
swapchain_images: std::sync::Mutex::new(Vec::new()),
swapchain_format: std::sync::Mutex::new(vk::Format::UNDEFINED),
swapchain_transfer_src: std::sync::atomic::AtomicBool::new(false),
swapchain_extent: std::sync::Mutex::new(vk::Extent2D {
width: 0,
height: 0,
}),
swapchain_colorspace: std::sync::atomic::AtomicU32::new(0),
frame_counter: std::sync::atomic::AtomicU64::new(0),
ring_generation: std::sync::atomic::AtomicU64::new(0),
hud_detected_frame: std::sync::atomic::AtomicBool::new(false),
pending_capture_frame: std::sync::atomic::AtomicBool::new(false),
capture_injected_frame: std::sync::atomic::AtomicBool::new(false),
skipped_draws_frame: std::sync::atomic::AtomicU32::new(0),
encoder: std::sync::Mutex::new(None),
frame_gate: std::sync::Mutex::new(crate::pacing::FrameGate::from_env()),
frame_pacer: std::sync::Mutex::new(crate::pacing::FramePacer::from_env()),
last_present_return: std::sync::Mutex::new(None),
encoder_starting: std::sync::atomic::AtomicBool::new(false),
windows: DashMap::new(),
newest_window: std::sync::Mutex::new(None),
});
DEVICE_STATE.insert(key, dev_state);
@@ -299,38 +285,12 @@ pub unsafe extern "system" fn vkDestroyDevice(
ds.pipeline_registry.len(),
);
// ── 1. Shut down the encode pipeline ──────────────────────────────────
//
// Waited for, not just signalled. On a shared device the encoder, its
// converter and their images are objects on this very device, and
// destroying the device under them is a use-after-free. The receive
// timeout bounds how long the thread takes to notice.
let handle = ds.encoder.lock().ok().and_then(|mut g| g.take());
if let Some(handle) = handle {
if handle.finish(std::time::Duration::from_secs(2)) {
log::info!("encoder pipeline stopped");
} else {
log::error!("encoder thread did not stop in time; destroying the device anyway");
}
}
// ── 2. Tear down the capture ring ─────────────────────────────────────
//
// Images, memory, fences, exported fds and the per-swapchain-image
// semaphores all belong to the ring now, so one teardown covers what used
// to be three separate steps.
{
let ring = ds.capture_ring.lock().unwrap().take();
if let Some(ring) = ring {
if !ds.capture_slots.all_free() {
log::warn!(
"device destroyed with {} capture slot(s) still in flight",
crate::state::CAPTURE_SLOTS - ds.capture_slots.available()
);
}
unsafe { crate::capture::destroy_capture_ring(&ds, ring) };
log::debug!("capture ring destroyed");
}
// ── 1–2. Every window: its encoder, then its capture ring ─────────────
*ds.newest_window.lock().unwrap() = None;
let windows: Vec<_> = ds.windows.iter().map(|e| e.value().clone()).collect();
ds.windows.clear();
for window in windows {
crate::swapchain::retire_window(&window);
}
// ── 3. Free hudless_image / hudless_memory ────────────────────────────
+20 -16
View File
@@ -185,11 +185,11 @@ fn relay_legacy_commands(own: std::path::PathBuf) {
}
}
/// `STREAM_ENCODER_HELLO` for this process: its pid and `NESTRI_LAUNCH_ID`.
fn encoder_hello() -> Vec<u8> {
/// `STREAM_ENCODER_HELLO` for one source: its id and `NESTRI_LAUNCH_ID`.
fn encoder_hello(source: u32) -> Vec<u8> {
let launch = std::env::var("NESTRI_LAUNCH_ID").unwrap_or_default();
encode_ipc_frame_from(
std::process::id(),
source,
nesprotocol::STREAM_ENCODER_HELLO,
0,
0,
@@ -324,6 +324,8 @@ pub struct CapturedFrame {
/// now does the waiting that a separate capture thread used to do, and that
/// work is per-device.
pub ds_key: usize,
/// The window this frame was presented to: its ring holds the slot.
pub window: std::sync::Arc<crate::state::Window>,
pub width: u32,
pub height: u32,
pub vk_format: u32,
@@ -925,6 +927,9 @@ pub struct PipelineConfig {
pub codec_request: Option<String>,
pub ipc_path: std::path::PathBuf,
pub physical_device: Option<ash::vk::PhysicalDevice>,
/// What frames and commands are tagged with: this process, or one of its
/// windows. See `Window::source`.
pub source: u32,
}
impl PipelineConfig {
pub fn from_env(width: u32, height: u32) -> Option<Self> {
@@ -961,6 +966,7 @@ impl PipelineConfig {
pixel_format,
codec_request: std::env::var("NESCAPTURE_CODEC").ok(),
ipc_path,
source: std::process::id(),
physical_device: None,
})
}
@@ -1204,6 +1210,7 @@ impl PipelineHandle {
// is, so the listener answers a capability message without needing one.
let listener_host_caps = device_caps;
let listener_forced = config.codec_request.clone();
let cmd_source = config.source;
thread::Builder::new()
.name("nescapture-idr".into())
.spawn(move || {
@@ -1211,9 +1218,10 @@ impl PipelineHandle {
// in one box, one shared path meant the second encoder took it
// from the first, and every keyframe request and bitrate change
// after that reached only the newer game.
let cmd_path = std::path::PathBuf::from(nesprotocol::capture_cmd_socket(
std::process::id(),
));
// And one per window, so a pause or keyframe request names a
// window rather than everything its process draws.
let cmd_path =
std::path::PathBuf::from(nesprotocol::capture_cmd_socket(cmd_source));
let _ = std::fs::remove_file(&cmd_path);
let sock = match std::os::unix::net::UnixDatagram::bind(&cmd_path) {
Ok(s) => {
@@ -1232,7 +1240,9 @@ impl PipelineHandle {
// The shared path too, relayed onto this one, so a hub or a
// compositor that predates per-process sockets still reaches the
// newest encoder exactly as it did.
relay_legacy_commands(cmd_path.clone());
if cmd_source == std::process::id() {
relay_legacy_commands(cmd_path.clone());
}
let mut buf = [0u8; 128];
loop {
match sock.recv(&mut buf) {
@@ -1777,13 +1787,7 @@ fn encoder_thread(
// its own between the present hook and here; it is cheaper on this one,
// because the blit it waits for was submitted a frame earlier and has
// already completed, and every frame saves a channel and a wakeup.
let Some(ds) = crate::state::DEVICE_STATE
.get(&raw.ds_key)
.map(|s| s.clone())
else {
log::error!("encoder: device state gone");
break;
};
let ds = raw.window.clone();
let Some(mut source) = crate::present::resolve_source(&ds, &raw) else {
continue;
};
@@ -3087,7 +3091,7 @@ fn ipc_send_thread(
// Tagged with this process, so the hub can tell which
// window the frame is, ref(d-0085).
let ipc_frame = encode_ipc_frame_from(
std::process::id(),
cfg.source,
STREAM_VIDEO,
protocol_codec,
flags,
@@ -3101,7 +3105,7 @@ fn ipc_send_thread(
// process's environment, and a keyframe is where a late
// hub starts anyway.
let messages = if pkt.is_key_frame {
vec![encoder_hello(), ipc_frame]
vec![encoder_hello(cfg.source), ipc_frame]
} else {
vec![ipc_frame]
};
+5
View File
@@ -39,6 +39,7 @@ mod shader;
mod shared;
mod slots;
mod state;
mod surface;
mod swapchain;
mod timing;
@@ -56,6 +57,7 @@ use framebuffer::{
use instance::{vkCreateInstance, vkDestroyInstance};
use pipeline::{vkCreateGraphicsPipelines, vkDestroyPipeline};
use present::vkQueuePresentKHR;
use surface::{vkCreateXcbSurfaceKHR, vkCreateXlibSurfaceKHR, vkDestroySurfaceKHR};
use shader::{vkCreateShaderModule, vkDestroyShaderModule};
use swapchain::{
vkAcquireNextImage2KHR, vkAcquireNextImageKHR, vkCreateSwapchainKHR, vkDestroySwapchainKHR,
@@ -220,6 +222,9 @@ pub unsafe extern "system" fn vkGetInstanceProcAddr(
b"vkCreateInstance" => layer_fn!(vkCreateInstance),
b"vkDestroyInstance" => layer_fn!(vkDestroyInstance),
b"vkCreateDevice" => layer_fn!(vkCreateDevice),
b"vkCreateXcbSurfaceKHR" => layer_fn!(vkCreateXcbSurfaceKHR),
b"vkCreateXlibSurfaceKHR" => layer_fn!(vkCreateXlibSurfaceKHR),
b"vkDestroySurfaceKHR" => layer_fn!(vkDestroySurfaceKHR),
_ => {}
}
+53 -24
View File
@@ -32,6 +32,12 @@ pub unsafe extern "system" fn vkQueuePresentKHR(
// it is the game's own frame time with this layer's cost excluded — the
// three spans then partition the wall clock between presents exactly.
let entered = std::time::Instant::now();
let pi = unsafe { &*p_present_info };
// The window this present draws, when it is one: its own encoder, ring and
// gate. A present naming several swapchains is passed through untouched.
let window = (pi.swapchain_count == 1 && !pi.p_swapchains.is_null())
.then(|| crate::swapchain::window_of(&ds, unsafe { *pi.p_swapchains }))
.flatten();
note_present(&ds, PresentStep::InHook);
ds.frame_counter.fetch_add(1, Ordering::Relaxed);
@@ -40,7 +46,8 @@ pub unsafe extern "system" fn vkQueuePresentKHR(
ds.capture_injected_frame.store(false, Ordering::Relaxed);
ds.skipped_draws_frame.store(0, Ordering::Relaxed);
if let Ok(enc) = ds.encoder.lock()
if let Some(window) = &window
&& let Ok(enc) = window.encoder.lock()
&& let Some(ref h) = *enc
{
h.present_attempts.fetch_add(1, Ordering::Relaxed);
@@ -51,18 +58,15 @@ pub unsafe extern "system" fn vkQueuePresentKHR(
}
}
let pi = unsafe { &*p_present_info };
// Rewriting the wait semaphores is only well defined for a single
// swapchain. A multi-swapchain present is rare enough that passing it
// through untouched beats getting the interposition subtly wrong.
let single_swapchain =
pi.swapchain_count == 1 && !pi.p_swapchains.is_null() && !pi.p_image_indices.is_null();
let submission = if single_swapchain {
unsafe { try_capture(&ds, queue, pi) }
} else {
None
let submission = match &window {
Some(window) if single_swapchain => unsafe { try_capture(window, queue, pi) },
_ => None,
};
let down_us = std::cell::Cell::new(std::time::Duration::ZERO);
@@ -80,7 +84,7 @@ pub unsafe extern "system" fn vkQueuePresentKHR(
let Some(submission) = submission else {
let r = call_down(p_present_info);
finish(&ds, entered, down_us.get());
finish(&ds, window.as_deref(), entered, down_us.get());
return r;
};
@@ -105,15 +109,17 @@ pub unsafe extern "system" fn vkQueuePresentKHR(
// Only hand the frame on once the present has been accepted. A failed
// present drops the job, which returns the slot.
// A submission exists only for a window.
let window = window.expect("a capture without a window");
if matches!(result, vk::Result::SUCCESS | vk::Result::SUBOPTIMAL_KHR) {
queue_for_encode(&ds, submission);
queue_for_encode(&window, submission);
} else {
// The blit signalled this semaphore; whether the failed present waited
// on it is undefined. Set it aside rather than signal it twice.
capture::retire_present_semaphore(&ds, image_index);
capture::retire_present_semaphore(&window, image_index);
}
finish(&ds, entered, down_us.get());
finish(&ds, Some(&window), entered, down_us.get());
result
}
@@ -122,7 +128,12 @@ pub unsafe extern "system" fn vkQueuePresentKHR(
/// `layer` is everything in this hook that is not the down-call, both sides of
/// it added together, so `gap + layer + down` accounts for the wall clock
/// between one present and the next with nothing unattributed.
fn finish(ds: &crate::state::DeviceState, entered: std::time::Instant, down: std::time::Duration) {
fn finish(
ds: &crate::state::DeviceState,
window: Option<&crate::state::Window>,
entered: std::time::Instant,
down: std::time::Duration,
) {
// Everything this hook cost, before any deliberate waiting.
let worked = std::time::Instant::now();
@@ -148,7 +159,8 @@ fn finish(ds: &crate::state::DeviceState, entered: std::time::Instant, down: std
// and the overshoot lands nowhere else — the present-return is stamped
// after it, so `gap` cannot show it either.
let held = now.saturating_duration_since(worked);
if let Ok(enc) = ds.encoder.lock()
if let Some(window) = window
&& let Ok(enc) = window.encoder.lock()
&& let Some(ref h) = *enc
{
// `layer` is this layer's cost, so the hold comes out of it: it is
@@ -171,8 +183,14 @@ fn finish(ds: &crate::state::DeviceState, entered: std::time::Instant, down: std
/// Tell the stall watchdog where the game's present thread is. A no-op until
/// the pipeline exists.
///
/// The device's newest window stands for the device: the watchdog asks where
/// the present thread is, which is one answer however many windows it draws.
pub fn note_present(ds: &crate::state::DeviceState, step: PresentStep) {
if let Ok(enc) = ds.encoder.lock()
let Some(window) = ds.newest_window.lock().ok().and_then(|w| w.clone()) else {
return;
};
if let Ok(enc) = window.encoder.lock()
&& let Some(ref h) = *enc
{
h.note_present(step);
@@ -191,7 +209,7 @@ struct Submission {
}
unsafe fn try_capture(
ds: &crate::state::DeviceState,
ds: &std::sync::Arc<crate::state::Window>,
queue: vk::Queue,
pi: &vk::PresentInfoKHR,
) -> Option<Submission> {
@@ -240,7 +258,7 @@ unsafe fn try_capture(
// Before the blit, not after it. A frame captured with nowhere to send it
// costs a slot, a copy and an export for nothing.
let ds_key = unsafe { crate::dispatch_key(ds.raw.as_raw() as *const c_void) };
if !encoder_ready(ds, ds_key, sc_ext.width, sc_ext.height) {
if !encoder_ready(ds, sc_ext.width, sc_ext.height) {
return None;
}
@@ -283,7 +301,7 @@ unsafe fn try_capture(
})
}
fn queue_for_encode(ds: &crate::state::DeviceState, submission: Submission) {
fn queue_for_encode(ds: &std::sync::Arc<crate::state::Window>, submission: Submission) {
let ds_key = unsafe { crate::dispatch_key(ds.raw.as_raw() as *const c_void) };
let Ok(enc) = ds.encoder.lock() else {
@@ -297,6 +315,7 @@ fn queue_for_encode(ds: &crate::state::DeviceState, submission: Submission) {
// does, which is the backpressure the ring was always providing.
handle.push_frame(CapturedFrame {
ds_key,
window: ds.clone(),
width: submission.width,
height: submission.height,
vk_format: submission.sc_fmt.as_raw() as u32,
@@ -315,7 +334,10 @@ fn queue_for_encode(ds: &crate::state::DeviceState, submission: Submission) {
/// and doing it after a blit would waste one. It runs once, on a thread of its
/// own, and frames presented in the meantime are skipped before any GPU work is
/// queued for them.
fn encoder_ready(ds: &crate::state::DeviceState, ds_key: usize, width: u32, height: u32) -> bool {
fn encoder_ready(window: &crate::state::Window, width: u32, height: u32) -> bool {
let ds = window;
let ds_key = unsafe { crate::dispatch_key(ds.raw.as_raw() as *const c_void) };
let native = window.native;
if let Ok(enc) = ds.encoder.lock() {
if enc.is_some() {
return true;
@@ -327,13 +349,17 @@ fn encoder_ready(ds: &crate::state::DeviceState, ds_key: usize, width: u32, heig
std::thread::Builder::new()
.name("nescapture-encoder-init".into())
.spawn(move || {
let Some(ds) = DEVICE_STATE.get(&ds_key).map(|s| s.clone()) else {
let Some(ds) = DEVICE_STATE
.get(&ds_key)
.and_then(|d| d.windows.get(&native).map(|w| w.clone()))
else {
return;
};
let Some(cfg) = PipelineConfig::from_env(width, height) else {
let Some(mut cfg) = PipelineConfig::from_env(width, height) else {
log::error!("no encode pipeline configuration; capture disabled");
return;
};
cfg.source = ds.source;
// On the game's own device where it was created for that, and
// on a device of the encoder's own where it was not, or where
// adopting it fails.
@@ -359,7 +385,7 @@ fn encoder_ready(ds: &crate::state::DeviceState, ds_key: usize, width: u32, heig
let on_shared = shared.is_some();
match PipelineHandle::new(cfg, shared) {
Ok(h) => {
h.set_stall_probe(Box::new(move || describe_gpu(ds_key)));
h.set_stall_probe(Box::new(move || describe_gpu(ds_key, native)));
// Before the handle is published: the first capture after
// it builds the ring, and must build it for this device.
ds.shared_active.store(on_shared, Ordering::Release);
@@ -391,7 +417,7 @@ fn encoder_ready(ds: &crate::state::DeviceState, ds_key: usize, width: u32, heig
/// submitted a whole frame earlier and has long since completed — and takes a
/// channel and a wakeup out of every frame's path.
pub fn resolve_source(
ds: &crate::state::DeviceState,
ds: &crate::state::Window,
frame: &CapturedFrame,
) -> Option<FrameSource> {
let slot_index = frame.slot.as_ref()?.index();
@@ -440,8 +466,11 @@ pub fn resolve_source(
/// it, and whatever the game is waiting for is not GPU work on that queue.
///
/// Never blocks. It runs exactly when something may be holding a lock forever.
fn describe_gpu(ds_key: usize) -> String {
let Some(ds) = DEVICE_STATE.get(&ds_key).map(|s| s.clone()) else {
fn describe_gpu(ds_key: usize, native: u32) -> String {
let Some(ds) = DEVICE_STATE
.get(&ds_key)
.and_then(|d| d.windows.get(&native).map(|w| w.clone()))
else {
return "device gone".into();
};
let slots = format!(
+90 -22
View File
@@ -208,6 +208,51 @@ pub struct DeviceState {
pub hudless_memory: std::sync::Mutex<Option<vk::DeviceMemory>>,
pub hudless_size: std::sync::Mutex<(u32, u32, vk::Format)>,
pub frame_counter: std::sync::atomic::AtomicU64,
// Phase 3/4: per-frame HUD detection flags
pub hud_detected_frame: std::sync::atomic::AtomicBool,
pub pending_capture_frame: std::sync::atomic::AtomicBool,
pub capture_injected_frame: std::sync::atomic::AtomicBool,
pub skipped_draws_frame: std::sync::atomic::AtomicU32,
// ── Frame-rate throttle ───────────────────────────────────────────
/// Holds the game to the target rate. The gate decides what is captured;
/// this decides how fast the game is allowed to produce frames, which no
/// compositor can do once a player turns V-Sync off.
pub frame_pacer: std::sync::Mutex<crate::pacing::FramePacer>,
/// When the previous `vkQueuePresentKHR` returned to the game.
///
/// The base for the `gap` span: time from here to the next present's
/// arrival is the game's own, with nothing this layer does inside it.
pub last_present_return: std::sync::Mutex<Option<std::time::Instant>>,
/// One per native window: each window this device draws is captured and
/// encoded on its own, so a dialog never takes over the main window's
/// stream. Keyed by [`Window::native`]; a swapchain recreated for the same
/// window keeps its entry, and with it the stream. Every non-X11 surface
/// shares key 0, which is how a single window was always handled.
pub windows: DashMap<u32, Arc<Window>>,
/// The most recently created swapchain's window. What frame-wide work with
/// no swapchain in hand (HUD-less copies, acquire timing) reads.
pub newest_window: std::sync::Mutex<Option<Arc<Window>>>,
}
/// Everything that belongs to one presented window rather than to the device.
///
/// Dereferences to its device, so code that captures a window reads device
/// state (`fp`, `raw`, `shared`) through it unchanged and its own state
/// directly.
pub struct Window {
pub device: Arc<DeviceState>,
/// The native window this swapchain presents to: an X11 window id, or 0
/// when the surface was not an X11 one.
pub native: u32,
/// The id frames and commands are tagged with: the native window marked by
/// [`WINDOW_SOURCE_BIT`], or this process's id when there is no native
/// window, which is how a lone fullscreen game has always been named.
pub source: u32,
// Phase 4: final-frame capture
pub capture_ring: std::sync::Mutex<Option<CaptureRing>>,
/// Which ring slots are free. Held separately from the ring itself so a
@@ -229,35 +274,14 @@ pub struct DeviceState {
/// stays off for the life of that swapchain.
pub swapchain_transfer_src: std::sync::atomic::AtomicBool,
pub frame_counter: std::sync::atomic::AtomicU64,
/// Rings built for this device so far. Names the current one; see
/// Rings built for this window so far. Names the current one; see
/// [`CaptureRing::generation`].
pub ring_generation: std::sync::atomic::AtomicU64,
// Phase 3/4: per-frame HUD detection flags
pub hud_detected_frame: std::sync::atomic::AtomicBool,
pub pending_capture_frame: std::sync::atomic::AtomicBool,
pub capture_injected_frame: std::sync::atomic::AtomicBool,
pub skipped_draws_frame: std::sync::atomic::AtomicU32,
// Phase 7: encode + IPC pipeline (lazy-init on first frame)
pub encoder: std::sync::Mutex<Option<PipelineHandle>>,
// ── Frame-rate throttle ───────────────────────────────────────────
/// Decides which presented frames are worth capturing. Consulted in the
/// present hook, before any GPU work is queued, so a dropped frame costs
/// nothing beyond the comparison.
pub frame_gate: std::sync::Mutex<crate::pacing::FrameGate>,
/// Holds the game to the target rate. The gate decides what is captured;
/// this decides how fast the game is allowed to produce frames, which no
/// compositor can do once a player turns V-Sync off.
pub frame_pacer: std::sync::Mutex<crate::pacing::FramePacer>,
/// When the previous `vkQueuePresentKHR` returned to the game.
///
/// The base for the `gap` span: time from here to the next present's
/// arrival is the game's own, with nothing this layer does inside it.
pub last_present_return: std::sync::Mutex<Option<std::time::Instant>>,
/// Whether a thread has already been started to build the encode pipeline.
///
/// The build is slow and happens once; without this latch every present
@@ -265,6 +289,50 @@ pub struct DeviceState {
pub encoder_starting: std::sync::atomic::AtomicBool,
}
/// Marks a capture source as a window rather than a process. Process ids stay
/// far below it, so the two never collide.
pub const WINDOW_SOURCE_BIT: u32 = 0x8000_0000;
impl Window {
pub fn new(device: Arc<DeviceState>, native: u32, swapchain: vk::SwapchainKHR) -> Self {
Self {
device,
native,
source: if native == 0 {
std::process::id()
} else {
WINDOW_SOURCE_BIT | native
},
capture_ring: std::sync::Mutex::new(None),
capture_slots: crate::slots::SlotPool::new(CAPTURE_SLOTS),
swapchain: std::sync::Mutex::new(Some(swapchain)),
swapchain_images: std::sync::Mutex::new(Vec::new()),
swapchain_format: std::sync::Mutex::new(vk::Format::UNDEFINED),
swapchain_colorspace: std::sync::atomic::AtomicU32::new(0),
swapchain_extent: std::sync::Mutex::new(vk::Extent2D {
width: 0,
height: 0,
}),
swapchain_transfer_src: std::sync::atomic::AtomicBool::new(false),
ring_generation: std::sync::atomic::AtomicU64::new(0),
encoder: std::sync::Mutex::new(None),
frame_gate: std::sync::Mutex::new(crate::pacing::FrameGate::from_env()),
encoder_starting: std::sync::atomic::AtomicBool::new(false),
}
}
}
impl std::ops::Deref for Window {
type Target = DeviceState;
fn deref(&self) -> &DeviceState {
&self.device
}
}
/// `VkSurfaceKHR` → the X11 window it was created for. Surfaces belong to the
/// instance, swapchains to a device, so this is process-wide.
pub static SURFACE_WINDOW: Lazy<DashMap<u64, u32>> = Lazy::new(DashMap::new);
impl DeviceState {
/// The shared device, when the encoder is actually running on it.
pub fn shared_encoder(&self) -> Option<&crate::shared::SharedDevice> {
+81
View File
@@ -0,0 +1,81 @@
//! Which native window each surface draws to.
//!
//! A layer sees swapchains, and a swapchain names only a `VkSurfaceKHR`. The
//! compositor names windows. Recording the X11 window behind each surface is
//! what lets a capture say which window its frames belong to, so a process
//! drawing several windows -- a browser, a launcher with dialogs -- streams each
//! one on its own instead of all of them through one picture.
//!
//! Wayland surfaces are not recorded: a `wl_surface` is a client-side pointer
//! with no id the compositor would recognise. They keep the process as their
//! name, as every surface did before.
use crate::dispatch_key;
use crate::state::{INSTANCE_STATE, SURFACE_WINDOW};
use ash::vk::{self, Handle};
use std::ffi::CStr;
use std::os::raw::c_void;
/// The next layer's entry point for `name` on `instance`.
unsafe fn next<F>(instance: vk::Instance, name: &CStr) -> Option<F> {
let key = unsafe { dispatch_key(instance.as_raw() as *const c_void) };
let istate = INSTANCE_STATE.get(&key)?;
let f = unsafe { (istate.get_instance_proc_addr)(instance, name.as_ptr()) }?;
Some(unsafe { std::mem::transmute_copy::<_, F>(&f) })
}
#[unsafe(no_mangle)]
pub unsafe extern "system" fn vkCreateXcbSurfaceKHR(
instance: vk::Instance,
p_create_info: *const vk::XcbSurfaceCreateInfoKHR<'_>,
p_allocator: *const vk::AllocationCallbacks<'_>,
p_surface: *mut vk::SurfaceKHR,
) -> vk::Result {
let Some(create) =
(unsafe { next::<vk::PFN_vkCreateXcbSurfaceKHR>(instance, c"vkCreateXcbSurfaceKHR") })
else {
return vk::Result::ERROR_EXTENSION_NOT_PRESENT;
};
let result = unsafe { create(instance, p_create_info, p_allocator, p_surface) };
if result == vk::Result::SUCCESS {
let window = unsafe { (*p_create_info).window };
SURFACE_WINDOW.insert(unsafe { (*p_surface).as_raw() }, window);
}
result
}
#[unsafe(no_mangle)]
pub unsafe extern "system" fn vkCreateXlibSurfaceKHR(
instance: vk::Instance,
p_create_info: *const vk::XlibSurfaceCreateInfoKHR<'_>,
p_allocator: *const vk::AllocationCallbacks<'_>,
p_surface: *mut vk::SurfaceKHR,
) -> vk::Result {
let Some(create) =
(unsafe { next::<vk::PFN_vkCreateXlibSurfaceKHR>(instance, c"vkCreateXlibSurfaceKHR") })
else {
return vk::Result::ERROR_EXTENSION_NOT_PRESENT;
};
let result = unsafe { create(instance, p_create_info, p_allocator, p_surface) };
if result == vk::Result::SUCCESS {
// An X11 window id is 29 bits wide whatever `Window`'s C type is.
let window = unsafe { (*p_create_info).window } as u32;
SURFACE_WINDOW.insert(unsafe { (*p_surface).as_raw() }, window);
}
result
}
#[unsafe(no_mangle)]
pub unsafe extern "system" fn vkDestroySurfaceKHR(
instance: vk::Instance,
surface: vk::SurfaceKHR,
p_allocator: *const vk::AllocationCallbacks<'_>,
) {
// Before the call down: a handle can be reused the moment it is freed.
SURFACE_WINDOW.remove(&surface.as_raw());
if let Some(destroy) =
unsafe { next::<vk::PFN_vkDestroySurfaceKHR>(instance, c"vkDestroySurfaceKHR") }
{
unsafe { destroy(instance, surface, p_allocator) };
}
}
+78 -17
View File
@@ -53,13 +53,30 @@ pub unsafe extern "system" fn vkCreateSwapchainKHR(
Blitting from images the driver did not grant transfer usage is undefined."
);
}
ds.swapchain_transfer_src
// One window per native window: a swapchain recreated for the same X11
// window keeps the window it had, and with it the stream.
let swapchain = unsafe { *p_swapchain };
let native = crate::state::SURFACE_WINDOW
.get(&ci.surface.as_raw())
.map(|w| *w)
.unwrap_or(0);
let window = match ds.windows.get(&native).map(|w| w.clone()) {
Some(window) => window,
None => {
let window =
std::sync::Arc::new(crate::state::Window::new(ds.clone(), native, swapchain));
ds.windows.insert(native, window.clone());
window
}
};
window
.swapchain_transfer_src
.store(transfer_src, Ordering::Relaxed);
// A fresh swapchain means fresh images behind the same indices. The
// per-image capture semaphores may still be pending on presents from the
// outgoing swapchain, so they are set aside rather than reused.
crate::capture::retire_all_present_semaphores(&ds);
crate::capture::retire_all_present_semaphores(&window);
// Those fresh images also make every recorded blit invalid: a recording
// names its source image by handle, and the handles behind these indices
@@ -67,11 +84,11 @@ pub unsafe extern "system" fn vkCreateSwapchainKHR(
// `capture_present_frame` catches a resize on its own, but a swapchain
// recreated at the same size — which is the common case, on a format or
// present-mode change — looks identical to it.
crate::capture::invalidate_recorded_blits(&ds);
crate::capture::invalidate_recorded_blits(&window);
*ds.swapchain.lock().unwrap() = Some(unsafe { *p_swapchain });
*ds.swapchain_format.lock().unwrap() = ci.image_format;
*ds.swapchain_extent.lock().unwrap() = ci.image_extent;
*window.swapchain.lock().unwrap() = Some(swapchain);
*window.swapchain_format.lock().unwrap() = ci.image_format;
*window.swapchain_extent.lock().unwrap() = ci.image_extent;
// The colour space the layer below us was asked for, which is the one the
// game asked for in every configuration we ship.
//
@@ -97,8 +114,10 @@ pub unsafe extern "system" fn vkCreateSwapchainKHR(
// HDR for wrong HDR. Anyone re-enabling it has to give this process a
// channel to the compositor first, since the true colour space exists only
// there.
ds.swapchain_colorspace
window
.swapchain_colorspace
.store(ci.image_color_space.as_raw() as u32, Ordering::Relaxed);
*ds.newest_window.lock().unwrap() = Some(window.clone());
// `info`, not `debug`, and the present mode is why. It decides whether the
// compositor paces this game at all: a FIFO swapchain waits on
@@ -112,8 +131,9 @@ pub unsafe extern "system" fn vkCreateSwapchainKHR(
// and VKD3D choose the Vulkan present mode themselves, and what they pick
// from a given setting is theirs to decide.
log::info!(
"swapchain created — format={:?} colorspace={:?} extent={}x{} present_mode={:?} \
min_images={}",
"swapchain created — window={:#x} format={:?} colorspace={:?} extent={}x{} \
present_mode={:?} min_images={}",
native,
ci.image_format,
ci.image_color_space,
ci.image_extent.width,
@@ -145,18 +165,54 @@ pub unsafe extern "system" fn vkDestroySwapchainKHR(
// created and its images fetched, and clearing then would leave the
// live swapchain with no images: every present after would skip
// capture, silently, until the next recreation.
let mut current = ds.swapchain.lock().unwrap();
if is_current(*current, swapchain) {
*current = None;
*ds.swapchain_images.lock().unwrap() = Vec::new();
if let Some(window) = window_of(&ds, swapchain) {
*window.swapchain.lock().unwrap() = None;
*window.swapchain_images.lock().unwrap() = Vec::new();
}
drop(current);
if let Some(destroy_fn) = ds.fp.destroy_swapchain_khr {
unsafe { destroy_fn(device, swapchain, p_allocator) };
}
}
}
/// The window whose current swapchain is `swapchain`, if any.
pub fn window_of(
ds: &crate::state::DeviceState,
swapchain: vk::SwapchainKHR,
) -> Option<std::sync::Arc<crate::state::Window>> {
ds.windows
.iter()
.find(|w| is_current(*w.swapchain.lock().unwrap(), swapchain))
.map(|w| w.value().clone())
}
/// Stop a window's encoder and free its capture ring.
///
/// The encoder is waited for, not just signalled: on a shared device the
/// encoder, its converter and their images are objects on the device, and
/// destroying the device under them is a use-after-free. Its receive timeout
/// bounds how long the thread takes to notice.
pub fn retire_window(window: &crate::state::Window) {
let handle = window.encoder.lock().ok().and_then(|mut g| g.take());
if let Some(handle) = handle {
if handle.finish(std::time::Duration::from_secs(2)) {
log::info!("encoder pipeline stopped (window {:#x})", window.native);
} else {
log::error!("encoder thread did not stop in time; destroying the device anyway");
}
}
let ring = window.capture_ring.lock().unwrap().take();
if let Some(ring) = ring {
if !window.capture_slots.all_free() {
log::warn!(
"window destroyed with {} capture slot(s) still in flight",
crate::state::CAPTURE_SLOTS - window.capture_slots.available()
);
}
unsafe { crate::capture::destroy_capture_ring(window, ring) };
}
}
#[unsafe(no_mangle)]
pub unsafe extern "system" fn vkGetSwapchainImagesKHR(
device: vk::Device,
@@ -188,10 +244,12 @@ pub unsafe extern "system" fn vkGetSwapchainImagesKHR(
}
// Images of a retired swapchain are not the ones capture reads.
if !p_swapchain_images.is_null() && is_current(*ds.swapchain.lock().unwrap(), swapchain) {
if !p_swapchain_images.is_null()
&& let Some(window) = window_of(&ds, swapchain)
{
let count = unsafe { *p_swapchain_image_count as usize };
let images = unsafe { std::slice::from_raw_parts(p_swapchain_images, count) };
*ds.swapchain_images.lock().unwrap() = images.to_vec();
*window.swapchain_images.lock().unwrap() = images.to_vec();
log::debug!("swapchain images — {} images", count);
}
@@ -225,7 +283,10 @@ fn device_for(device: vk::Device) -> Option<std::sync::Arc<crate::state::DeviceS
}
fn record_acquire(ds: &crate::state::DeviceState, waited: std::time::Duration) {
if let Ok(enc) = ds.encoder.lock()
let Some(window) = ds.newest_window.lock().ok().and_then(|w| w.clone()) else {
return;
};
if let Ok(enc) = window.encoder.lock()
&& let Some(ref h) = *enc
{
h.timing.acquire.record(waited);