mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-24 03:28:18 +03:00
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:
committed by
GitHub
parent
8246aa5538
commit
15ad60bf49
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user