Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a38adad943 |
@@ -1176,7 +1176,24 @@ impl Encoder for PyroWaveEncoder {
|
||||
fn submit(&mut self, frame: &CapturedFrame) -> Result<()> {
|
||||
// SAFETY: single-threaded encoder; `encode_frame` records/submits on handles this
|
||||
// struct owns and waits its own fence before touching results.
|
||||
unsafe { self.encode_frame(frame) }
|
||||
let r = unsafe { self.encode_frame(frame) };
|
||||
if r.is_err() {
|
||||
// `encode_frame` opens the recording window early and has several fallible steps
|
||||
// inside it (cursor prep, dmabuf import, format mapping, the CPU-RGB staging path,
|
||||
// an unsupported-payload bail, and the encode call itself). Every one returns with
|
||||
// `self.cmd` still RECORDING, and nothing downstream repairs it — there is exactly
|
||||
// one `begin_command_buffer` in this file and `reset()`/`Drop` never touch `cmd` —
|
||||
// so the NEXT frame would call `begin` on a recording buffer, which is invalid usage.
|
||||
// Legal here on every path: the pool carries RESET_COMMAND_BUFFER and the buffer is
|
||||
// not pending (we never reached the submit, or the submit itself failed).
|
||||
// SAFETY: `self.cmd` is owned by this encoder and, on these paths, not in flight.
|
||||
unsafe {
|
||||
let _ = self
|
||||
.device
|
||||
.reset_command_buffer(self.cmd, vk::CommandBufferResetFlags::empty());
|
||||
}
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
fn caps(&self) -> EncoderCaps {
|
||||
|
||||
@@ -37,6 +37,12 @@ const DPB_SLOTS: u32 = 8;
|
||||
/// latency — on-glass validated as rock-solid at 1080p@240, so it is the real-time default;
|
||||
/// backpressure kicks in at the 2nd unread frame. Distinct from `DPB_SLOTS` (reference pool).
|
||||
const RING_DEFAULT: usize = 2;
|
||||
|
||||
/// Ceiling on any blocking GPU fence wait on the encode thread (5 s). Generous against a real
|
||||
/// encode (single-digit ms even on a loaded GPU) and against a driver hiccup, but finite: this is
|
||||
/// the thread the stall watchdog's `reset()` runs on, so an unbounded wait would deadlock the very
|
||||
/// path that recovers the session. Matches the Windows NVENC retrieve-thread budget.
|
||||
const ENCODE_FENCE_TIMEOUT_NS: u64 = 5_000_000_000;
|
||||
/// AV1 base quantizer index (0..=255) seeded into every frame. CBR rate control overrides it per
|
||||
/// frame; it only matters as the starting point and for the (rate-control-ignored) constant-Q path.
|
||||
const AV1_BASE_Q_IDX: u8 = 128;
|
||||
@@ -868,6 +874,15 @@ impl VulkanVideoEncoder {
|
||||
let (img, mem, view) = self.import_dmabuf(d, cw, ch)?;
|
||||
// Bound the cache; evict oldest (FIFO). A stable PipeWire pool never trips this in steady state
|
||||
// (all imports resident); it only cycles across a pool change (which also rebuilds the session).
|
||||
// Up to `ring_depth - 1` submitted frames may still be executing against a cached image
|
||||
// (`enqueue` only drains down to `frames.len()`, and `record_submit` imports before it
|
||||
// records), so destroying an evicted import here is a GPU-side use-after-free. `Drop` and
|
||||
// `reset()` both idle the device first; this was the one unguarded destroy. Guarded on the
|
||||
// length test so the steady-state path — where the cache is resident and never evicts —
|
||||
// pays nothing.
|
||||
if self.import_cache.len() >= IMPORT_CACHE_CAP {
|
||||
let _ = self.device.device_wait_idle();
|
||||
}
|
||||
while self.import_cache.len() >= IMPORT_CACHE_CAP {
|
||||
let (_, _, oi, om, ov) = self.import_cache.remove(0);
|
||||
self.device.destroy_image_view(ov, None);
|
||||
@@ -1849,8 +1864,25 @@ impl VulkanVideoEncoder {
|
||||
unsafe fn read_slot(&mut self, slot: usize) -> Result<EncodedFrame> {
|
||||
let dev = self.device.clone();
|
||||
let f = &self.frames[slot];
|
||||
let mut fb = [[0u32; 2]; 1];
|
||||
dev.get_query_pool_results(f.query_pool, 0, &mut fb, vk::QueryResultFlags::WAIT)?;
|
||||
// Ask for the operation status alongside the two feedback words: without it a FAILED encode
|
||||
// is indistinguishable from a successful one, and its offset/bytes-written are read as if
|
||||
// they described real bitstream. The status rides as a trailing element (signed:
|
||||
// `VkQueryResultStatusKHR` is >0 COMPLETE, 0 NOT_READY, <0 error).
|
||||
let mut fb = [[0i32; 3]; 1];
|
||||
dev.get_query_pool_results(
|
||||
f.query_pool,
|
||||
0,
|
||||
&mut fb,
|
||||
vk::QueryResultFlags::WAIT | vk::QueryResultFlags::WITH_STATUS_KHR,
|
||||
)?;
|
||||
let status = fb[0][2];
|
||||
if status <= 0 {
|
||||
anyhow::bail!(
|
||||
"vulkan-encode: encode feedback for slot {slot} reports status {status} \
|
||||
(not COMPLETE) — dropping the frame rather than shipping its bitstream"
|
||||
);
|
||||
}
|
||||
let fb = [[fb[0][0] as u32, fb[0][1] as u32]];
|
||||
// The (offset, bytes-written) pair is driver-reported: validate it against the bitstream
|
||||
// allocation BEFORE mapping, or the `from_raw_parts` below reads outside the buffer and
|
||||
// ships whatever it finds straight onto the wire. Checked in u64 so the add cannot wrap,
|
||||
@@ -1892,8 +1924,25 @@ impl VulkanVideoEncoder {
|
||||
// and free it — that oldest slot is exactly the round-robin `ring` cursor we reuse next.
|
||||
while self.in_flight.len() >= self.frames.len() {
|
||||
let slot = self.in_flight.pop_front().unwrap();
|
||||
self.device
|
||||
.wait_for_fences(&[self.frames[slot].fence], true, u64::MAX)?;
|
||||
// Bounded, not `u64::MAX`: this runs ON the host encode thread, which is also the
|
||||
// thread the stall watchdog's `reset()` would run on. An infinite wait against a
|
||||
// wedged GPU/driver therefore parks the one thread that could recover the session —
|
||||
// it never errors, never resets, and teardown blocks joining it. Surfacing expiry as
|
||||
// an error hands control back to the existing recovery path (same convention as the
|
||||
// pyrowave and Windows NVENC backends).
|
||||
match self.device.wait_for_fences(
|
||||
&[self.frames[slot].fence],
|
||||
true,
|
||||
ENCODE_FENCE_TIMEOUT_NS,
|
||||
) {
|
||||
Ok(()) => {}
|
||||
Err(vk::Result::TIMEOUT) => anyhow::bail!(
|
||||
"vulkan-encode: fence for slot {slot} did not signal within {} ms — GPU or \
|
||||
driver wedged; failing the submit so the session can reset",
|
||||
ENCODE_FENCE_TIMEOUT_NS / 1_000_000
|
||||
),
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
let done = self.read_slot(slot)?;
|
||||
self.pending.push_back(done);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ const BS_SLACK: usize = 256 * 1024;
|
||||
/// (a desktop-switch device recreate), in which case the stale imports are evicted + destroyed.
|
||||
const IMPORT_CACHE_CAP: usize = 8;
|
||||
|
||||
/// Plane-import cache key: the texture's COM address plus the extent it was imported at.
|
||||
type PlaneKey = (isize, u32, u32);
|
||||
|
||||
// --- Vulkan enum values not surfaced by pyrowave-sys' bindgen (only enums *reachable* from the
|
||||
// pyrowave C API are generated; these plain #define / flags-typedef values are stable spec
|
||||
// constants). bindgen renders every reachable Vulkan enum as a `u32` type alias, so these u32
|
||||
@@ -136,8 +139,8 @@ pub struct PyroWaveEncoder {
|
||||
// Imported plane textures, cached by the out-ring texture's raw pointer (stable per ring slot):
|
||||
// the full-res R8 Y plane and the half-res R8G8 CbCr plane, imported SEPARATELY (a single planar
|
||||
// NV12 import is unreliable on NVIDIA at arbitrary sizes).
|
||||
y_images: Vec<(isize, pw::pyrowave_image)>,
|
||||
cbcr_images: Vec<(isize, pw::pyrowave_image)>,
|
||||
y_images: Vec<(PlaneKey, pw::pyrowave_image)>,
|
||||
cbcr_images: Vec<(PlaneKey, pw::pyrowave_image)>,
|
||||
|
||||
width: u32,
|
||||
height: u32,
|
||||
@@ -351,10 +354,16 @@ impl PyroWaveEncoder {
|
||||
///
|
||||
/// # Safety
|
||||
/// Same contract as [`import_plane`].
|
||||
/// Keyed on `(texture address, width, height)` rather than the bare address: the COM pointer
|
||||
/// carries no reference here, so a released texture's address can be recycled by a later
|
||||
/// allocation and return an import describing the WRONG surface. Folding the extent in means a
|
||||
/// recycled address at a different size can never alias. (A recycle at the SAME size is still
|
||||
/// possible in principle — the complete fix is to key on the capturer's ring generation, which
|
||||
/// needs that generation plumbed onto `PyroFrameShare`.)
|
||||
unsafe fn cached_plane(
|
||||
cache: &mut Vec<(isize, pw::pyrowave_image)>,
|
||||
cache: &mut Vec<(PlaneKey, pw::pyrowave_image)>,
|
||||
make: impl FnOnce() -> Result<pw::pyrowave_image>,
|
||||
key: isize,
|
||||
key: PlaneKey,
|
||||
) -> Result<pw::pyrowave_image> {
|
||||
if let Some((_, img)) = cache.iter().find(|(k, _)| *k == key) {
|
||||
return Ok(*img);
|
||||
@@ -423,6 +432,21 @@ impl PyroWaveEncoder {
|
||||
!self.pw_enc.is_null(),
|
||||
"pyrowave: encode after a failed reset (encoder was destroyed and not rebuilt)"
|
||||
);
|
||||
// The plane textures are imported at the encoder's CONFIGURED extent, not the frame's, so a
|
||||
// capture that changed size would be read under a stale `VkImageCreateInfo`. This is
|
||||
// reachable without any client Reconfigure: the IDD capturer autonomously recreates its ring
|
||||
// on a confirmed display-descriptor change (e.g. a fullscreen game mode-setting the virtual
|
||||
// display). Refuse instead — the session must reopen the encoder at the new mode. Mirrors
|
||||
// the guard the QSV and AMF backends already carry.
|
||||
anyhow::ensure!(
|
||||
frame.width == self.width && frame.height == self.height,
|
||||
"pyrowave: captured frame {}x{} != encoder {}x{} (the capturer recreated its ring at a \
|
||||
new mode — the encoder must be reopened)",
|
||||
frame.width,
|
||||
frame.height,
|
||||
self.width,
|
||||
self.height
|
||||
);
|
||||
let FramePayload::D3d11(d3d) = &frame.payload else {
|
||||
bail!("pyrowave (Windows) needs a D3D11 frame (the capturer must be in pyrowave mode)")
|
||||
};
|
||||
@@ -465,7 +489,7 @@ impl PyroWaveEncoder {
|
||||
};
|
||||
let pw_dev = self.pw_dev;
|
||||
let y_img = {
|
||||
let key = d3d.texture.as_raw() as isize;
|
||||
let key = (d3d.texture.as_raw() as isize, w, h);
|
||||
let tex = &d3d.texture;
|
||||
Self::cached_plane(
|
||||
&mut self.y_images,
|
||||
@@ -474,7 +498,7 @@ impl PyroWaveEncoder {
|
||||
)?
|
||||
};
|
||||
let cbcr_img = {
|
||||
let key = share.cbcr.as_raw() as isize;
|
||||
let key = (share.cbcr.as_raw() as isize, cw, ch);
|
||||
let tex = &share.cbcr;
|
||||
Self::cached_plane(
|
||||
&mut self.cbcr_images,
|
||||
|
||||
Reference in New Issue
Block a user