mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 09:15:19 +03:00
Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
293 lines
13 KiB
Rust
293 lines
13 KiB
Rust
// ─────────────────────────────────────────────────────────────────────────────
|
|
// state.rs — global state shared across all hooks
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
use crate::config::ShaderHashSet;
|
|
use crate::dispatch::NextDeviceFn;
|
|
use crate::encode::PipelineHandle;
|
|
use ash::vk;
|
|
use dashmap::DashMap;
|
|
use once_cell::sync::Lazy;
|
|
use std::sync::Arc;
|
|
|
|
/// 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,
|
|
/// 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
|
|
/// cannot be synchronised with a semaphore.
|
|
pub fence: vk::Fence,
|
|
}
|
|
|
|
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
|
|
/// 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>,
|
|
}
|
|
|
|
/// 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)]
|
|
pub struct PipelineHashes {
|
|
pub vert_hash: Option<u64>,
|
|
pub frag_hash: Option<u64>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct PipelineState {
|
|
pub blend_enabled: bool,
|
|
pub depth_test_enabled: bool,
|
|
pub depth_write_enabled: bool,
|
|
}
|
|
|
|
// ── Per-device state ──────────────────────────────────────────────────────────
|
|
|
|
pub struct DeviceState {
|
|
pub raw: vk::Device,
|
|
pub physical_device: vk::PhysicalDevice,
|
|
pub fp: NextDeviceFn,
|
|
|
|
// Phase 1: shader / pipeline
|
|
pub shader_registry: DashMap<u64, u64>,
|
|
pub pipeline_registry: DashMap<u64, PipelineHashes>,
|
|
pub pipeline_state: DashMap<u64, PipelineState>,
|
|
|
|
// Phase 2: view / framebuffer tracking
|
|
pub view_to_image: DashMap<u64, u64>,
|
|
pub view_format: DashMap<u64, vk::Format>,
|
|
pub framebuffer_to_views: DashMap<u64, Vec<u64>>,
|
|
pub framebuffer_extent: DashMap<u64, vk::Extent2D>,
|
|
|
|
// Phase 3: shader hash config
|
|
pub shader_hashes: Option<ShaderHashSet>,
|
|
|
|
// Phase 4: HUDless capture (optional — HUDLESS_CAPTURE_HUDLESS=1)
|
|
pub hudless_image: std::sync::Mutex<Option<vk::Image>>,
|
|
pub hudless_memory: std::sync::Mutex<Option<vk::DeviceMemory>>,
|
|
pub hudless_size: std::sync::Mutex<(u32, u32, vk::Format)>,
|
|
|
|
// Phase 4: final-frame capture (DMA-BUF exportable)
|
|
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>>,
|
|
pub swapchain_images: std::sync::Mutex<Vec<vk::Image>>,
|
|
pub swapchain_format: std::sync::Mutex<vk::Format>,
|
|
/// Raw `VkColorSpaceKHR` value, captured from vkCreateSwapchainKHR.
|
|
/// 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,
|
|
|
|
// 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
|
|
/// arriving before it finishes would start another one.
|
|
pub encoder_starting: std::sync::atomic::AtomicBool,
|
|
}
|
|
|
|
// ── Per-command-buffer state ──────────────────────────────────────────────────
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct CbState {
|
|
pub device_key: usize,
|
|
|
|
pub current_color_image: Option<vk::Image>,
|
|
pub current_image_format: Option<vk::Format>,
|
|
pub current_image_extent: Option<vk::Extent2D>,
|
|
|
|
pub active_vert_hash: Option<u64>,
|
|
pub active_frag_hash: Option<u64>,
|
|
pub hud_captured: bool,
|
|
|
|
pub pending_capture: bool,
|
|
pub capture_injected: bool,
|
|
pub hud_detected: bool,
|
|
|
|
pub draw_counter: u32,
|
|
}
|
|
|
|
// ── Global state ──────────────────────────────────────────────────────────────
|
|
|
|
pub static INSTANCE_STATE: Lazy<DashMap<usize, Arc<crate::dispatch::NextInstanceFn>>> =
|
|
Lazy::new(DashMap::new);
|
|
|
|
pub static DEVICE_STATE: Lazy<DashMap<usize, Arc<DeviceState>>> = Lazy::new(DashMap::new);
|
|
|
|
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);
|