mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-28 13:32:25 +03:00
feat: nescapture capture improvements and drive mounts (#337)
Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
DatCaptainHorse
Claude Opus 5
parent
ebc0242b49
commit
6811c93d51
@@ -29,7 +29,13 @@ pub struct CaptureSlot {
|
||||
/// 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,
|
||||
/// The DRM format modifier the driver gave this slot's image.
|
||||
///
|
||||
/// Carried per slot rather than assumed, and passed to the importer, which
|
||||
/// creates its side with this exact value. It used to be hard-coded to
|
||||
/// `DRM_FORMAT_MOD_LINEAR` on both sides — true at the time, because the
|
||||
/// producer only ever asked for linear.
|
||||
pub modifier: u64,
|
||||
/// 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
|
||||
@@ -40,6 +46,40 @@ pub struct CaptureSlot {
|
||||
pub struct CaptureRing {
|
||||
pub command_pool: vk::CommandPool,
|
||||
pub slots: Vec<CaptureSlot>,
|
||||
/// One command buffer per (swapchain image, slot) pair, recorded on first
|
||||
/// use and re-submitted from then on.
|
||||
///
|
||||
/// The blit's contents depend on the source image, the destination image
|
||||
/// and the extent, and on nothing else — so recording it again every frame
|
||||
/// was work done on the game's own present thread for a result that never
|
||||
/// changed. Indexed by [`blit_index`].
|
||||
pub blits: Vec<vk::CommandBuffer>,
|
||||
/// Which entries of `blits` hold a valid recording.
|
||||
///
|
||||
/// Cleared wholesale when the swapchain is recreated and when the extent
|
||||
/// changes: a recorded buffer names specific `VkImage` handles and bakes in
|
||||
/// the copy region, and a recreated swapchain's images are different
|
||||
/// objects at a possibly different size. Submitting a stale one reads freed
|
||||
/// memory.
|
||||
pub blits_recorded: Vec<bool>,
|
||||
/// How many swapchain images the ring allocated command buffers for.
|
||||
pub image_count: usize,
|
||||
/// Two timestamps per slot, bracketing that slot's blit, or null where the
|
||||
/// presenting queue family cannot timestamp.
|
||||
///
|
||||
/// Per slot rather than per (image, slot) pair because only one blit per
|
||||
/// slot is ever in flight — the `SlotGuard` guarantees it — and the query
|
||||
/// index has to be baked into a command buffer recorded once.
|
||||
pub timestamp_pool: vk::QueryPool,
|
||||
/// Nanoseconds per device tick, for turning the pair into a duration.
|
||||
pub timestamp_period: f32,
|
||||
/// The extent `blits` were recorded for.
|
||||
///
|
||||
/// The ring is kept when the swapchain shrinks — `ensure_capture_ring`
|
||||
/// accepts a ring at least as large as the request — so the extent can
|
||||
/// change under a ring that is not rebuilt, and the recordings have to
|
||||
/// follow it even though the images do not.
|
||||
pub blit_extent: vk::Extent2D,
|
||||
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
|
||||
@@ -64,6 +104,58 @@ pub struct CaptureRing {
|
||||
pub present_wait: Vec<vk::Semaphore>,
|
||||
}
|
||||
|
||||
/// Index into [`CaptureRing::blits`] for one (swapchain image, slot) pair.
|
||||
///
|
||||
/// Flat rather than nested so the ring holds one `Vec` and takes one allocation
|
||||
/// from the command pool. `None` when either index is out of range, which means
|
||||
/// a swapchain that gained images under a ring built for fewer — a frame
|
||||
/// skipped rather than a blit from an image the ring never saw.
|
||||
pub fn blit_index(image_index: usize, slot: usize, image_count: usize) -> Option<usize> {
|
||||
if image_index >= image_count || slot >= CAPTURE_SLOTS {
|
||||
return None;
|
||||
}
|
||||
Some(image_index * CAPTURE_SLOTS + slot)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_image_and_slot_pair_has_its_own_index() {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for image in 0..3 {
|
||||
for slot in 0..CAPTURE_SLOTS {
|
||||
let i = blit_index(image, slot, 3).expect("in range");
|
||||
assert!(seen.insert(i), "image {image} slot {slot} collided at {i}");
|
||||
}
|
||||
}
|
||||
assert_eq!(seen.len(), 3 * CAPTURE_SLOTS);
|
||||
}
|
||||
|
||||
/// Every index must land inside a `Vec` of `image_count * CAPTURE_SLOTS`,
|
||||
/// which is what the ring allocates.
|
||||
#[test]
|
||||
fn indices_stay_inside_the_allocation() {
|
||||
let count = 4;
|
||||
for image in 0..count {
|
||||
for slot in 0..CAPTURE_SLOTS {
|
||||
let i = blit_index(image, slot, count).expect("in range");
|
||||
assert!(i < count * CAPTURE_SLOTS, "{i} is outside the allocation");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A swapchain recreated with more images than the ring was built for.
|
||||
/// Blitting from an image the ring never allocated a buffer for would
|
||||
/// submit whatever that slot happened to hold.
|
||||
#[test]
|
||||
fn an_out_of_range_image_or_slot_has_no_index() {
|
||||
assert_eq!(blit_index(3, 0, 3), None);
|
||||
assert_eq!(blit_index(0, CAPTURE_SLOTS, 3), None);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-pipeline records ──────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -143,8 +235,21 @@ pub struct DeviceState {
|
||||
/// 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>>>,
|
||||
/// 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
|
||||
/// arriving before it finishes would start another one.
|
||||
pub encoder_starting: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
// ── Per-command-buffer state ──────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user