feat: nescapture pacing, improvements and deps update (#334)

Make nescapture better with proper queue family checks, FPS limiting,
semaphore usage and other.. also updated deps like pollster and
pixelforge.

<!-- greptile_comment -->

<!-- greptile_summary -->

<h2><a
href="https://app.greptile.com/api/retrigger?id=64083904"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/RetriggerDark.svg?v=1"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=1"><img
alt="Retrigger"
src="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=1"
align="right"></picture></a>Confidence Score: 5/5</h2>

The final review contains no accepted findings, so the PR appears safe
to merge.

<h3>Summary</h3>

- This PR reworks `nescapture` frame capture around a four-slot DMA-BUF
ring, tracks presentation queue families, adds semaphore-based
capture/present ordering, introduces capture frame-rate pacing, carries
presentation timestamps through encoding, and updates Vulkan-related
dependencies.

<sub>Reviews (1) · Last reviewed commit: ["feat: Update deps and remove
deprecated
..."](79e21f4aac)</sub>

<!-- /greptile_comment -->

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Ollikainen
2026-09-15 13:58:57 +03:00
committed by GitHub
parent 8246aa5538
commit 15ad60bf49
13 changed files with 1045 additions and 465 deletions

View File

@@ -10,12 +10,58 @@ use dashmap::DashMap;
use once_cell::sync::Lazy;
use std::sync::Arc;
// Holds capture-specific resources instead of re-creating them *each frame*
pub struct CaptureResources {
/// How many frames may be in flight between the present hook and the encoder.
///
/// The path holds at most three at once — two queued for the encoder plus the
/// one it is working on — so four leaves a slot spare and the present hook
/// practically never finds the ring empty.
pub const CAPTURE_SLOTS: usize = 4;
/// One destination for the swapchain blit, with everything that belongs to it.
///
/// Every slot owns its image: the previous version rotated four command buffers
/// over a single shared destination, so a capture overwrote the frame the
/// encoder was still reading.
pub struct CaptureSlot {
pub image: vk::Image,
pub memory: vk::DeviceMemory,
/// Exported once at allocation and duplicated per frame. -1 if the export
/// failed, which sends that frame down the CPU readback path instead.
pub dmabuf_fd: std::os::raw::c_int,
pub stride: u32,
pub command_buffer: vk::CommandBuffer,
/// Signalled when this slot's blit has finished reading the swapchain and
/// writing the slot. The capture worker waits on it before handing the
/// DMA-BUF to the encoder, which reads it from a different VkDevice and so
/// cannot be synchronised with a semaphore.
pub fence: vk::Fence,
}
pub struct CaptureRing {
pub command_pool: vk::CommandPool,
pub command_buffers: [vk::CommandBuffer; 4],
pub fences: [vk::Fence; 4],
pub current: usize,
pub slots: Vec<CaptureSlot>,
pub size: (u32, u32, vk::Format),
/// Queue family the command pool was created for. Command buffers may only
/// be submitted to a queue of the family their pool belongs to, so a
/// present arriving on a different family rebuilds the ring rather than
/// submitting invalid work.
pub queue_family: u32,
/// Semaphores that were handed to a present which then failed.
///
/// Whether such a present waited on the semaphore is unknowable, so it can
/// neither be signalled again nor safely destroyed while it might still be
/// pending. They are set aside here and destroyed with the device. A
/// swapchain recreation retires at most one per image.
pub retired: Vec<vk::Semaphore>,
/// Signalled by the capture blit, waited on by the present that follows it.
///
/// Indexed by *swapchain image index*, not by ring slot. A binary semaphore
/// may not be re-signalled until its previous wait has completed, and the
/// only guarantee of that available here is the application's own acquire:
/// it cannot present image N again until it has reacquired it, and it
/// cannot reacquire it until the present that waited on this semaphore is
/// done.
pub present_wait: Vec<vk::Semaphore>,
}
// ── Per-pipeline records ──────────────────────────────────────────────────────
@@ -60,12 +106,11 @@ pub struct DeviceState {
pub hudless_size: std::sync::Mutex<(u32, u32, vk::Format)>,
// Phase 4: final-frame capture (DMA-BUF exportable)
pub final_image: std::sync::Mutex<Option<vk::Image>>,
pub final_memory: std::sync::Mutex<Option<vk::DeviceMemory>>,
pub final_size: std::sync::Mutex<(u32, u32, vk::Format)>,
/// Row stride in bytes of `final_image`, queried once after allocation.
/// Zero until the first frame is captured.
pub final_stride: std::sync::atomic::AtomicU32,
pub capture_ring: std::sync::Mutex<Option<CaptureRing>>,
/// Which ring slots are free. Held separately from the ring itself so a
/// slot can be returned from the encoder thread without taking the lock
/// the present hook needs.
pub capture_slots: Arc<crate::slots::SlotPool>,
// Phase 4: swapchain tracking
pub swapchain: std::sync::Mutex<Option<vk::SwapchainKHR>>,
@@ -75,9 +120,13 @@ pub struct DeviceState {
/// Used to derive color space for the encoder (SDR vs HDR10 etc.).
pub swapchain_colorspace: std::sync::atomic::AtomicU32,
pub swapchain_extent: std::sync::Mutex<vk::Extent2D>,
/// Whether the swapchain was created with TRANSFER_SRC usage. When the
/// driver refuses it the layer falls back to the application's own usage
/// flags, and blitting from those images would be undefined — so capture
/// stays off for the life of that swapchain.
pub swapchain_transfer_src: std::sync::atomic::AtomicBool,
pub frame_counter: std::sync::atomic::AtomicU64,
pub largest_extent: std::sync::Mutex<vk::Extent2D>,
// Phase 3/4: per-frame HUD detection flags
pub hud_detected_frame: std::sync::atomic::AtomicBool,
@@ -88,32 +137,12 @@ pub struct DeviceState {
// Phase 7: encode + IPC pipeline (lazy-init on first frame)
pub encoder: std::sync::Mutex<Option<PipelineHandle>>,
// Re-usable capture resources (double-buffered)
pub capture_resources: std::sync::Mutex<Option<CaptureResources>>,
/// Dedicated queue for capture submissions (separate from game rendering).
pub capture_queue: std::sync::Mutex<vk::Queue>,
// Fake swapchain pool (headless — no real present)
pub fake_images: std::sync::Mutex<Vec<vk::Image>>,
pub fake_memories: std::sync::Mutex<Vec<vk::DeviceMemory>>,
pub fake_fds: std::sync::Mutex<Vec<std::os::raw::c_int>>,
pub fake_strides: std::sync::Mutex<Vec<u32>>,
pub fake_available: std::sync::Mutex<Vec<bool>>,
pub fake_image_count: std::sync::atomic::AtomicU32,
pub fake_swapchain: std::sync::Mutex<Option<vk::SwapchainKHR>>,
pub signal_queue: std::sync::Mutex<vk::Queue>,
pub next_acquire: std::sync::atomic::AtomicU32,
pub memory_properties: std::sync::Mutex<vk::PhysicalDeviceMemoryProperties>,
pub acquire_dummy_pool: std::sync::Mutex<vk::CommandPool>,
pub acquire_dummy_cb: std::sync::Mutex<vk::CommandBuffer>,
/// Cached DMA-BUF fd for final_memory (-1 = not cached).
/// Avoids a kernel ioctl per frame.
pub cached_dmabuf_fd: std::sync::atomic::AtomicI32,
// ── Frame-rate throttle ───────────────────────────────────────────
/// Target FPS for capture throttling (set from HUDLESS_FPS on pipeline init).
pub target_fps: std::sync::atomic::AtomicU32,
/// Timestamp of last captured frame (for rate limiting).
pub last_capture_time: std::sync::Mutex<Option<std::time::Instant>>,
/// 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>,
/// Channel for threaded capture worker (present → worker).
pub capture_tx: std::sync::Mutex<Option<std::sync::mpsc::Sender<crate::present::CaptureJob>>>,
}
@@ -151,5 +180,8 @@ pub static CB_STATE: Lazy<DashMap<u64, CbState>> = Lazy::new(DashMap::new);
/// VkQueue → device dispatch key
pub static QUEUE_TO_DEVICE_KEY: Lazy<DashMap<u64, usize>> = Lazy::new(DashMap::new);
/// VkQueue → queue family index, recorded at vkGetDeviceQueue.
pub static QUEUE_TO_FAMILY: Lazy<DashMap<u64, u32>> = Lazy::new(DashMap::new);
/// VkCommandBuffer → device dispatch key
pub static CMD_BUF_TO_DEVICE_KEY: Lazy<DashMap<u64, usize>> = Lazy::new(DashMap::new);