fix(encode/vulkan): five mode-specific correctness bugs, three caught on real silicon
WP4.2 of the pf-encode audit, validated on RADV PHOENIX (780M, Mesa 26.0.4) under the Vulkan
validation layers rather than by inspection alone. Three of the five reproduce with the
driver naming the violation; one turned out milder than filed and is recorded as such.
1. `ensure_cpu_rgb` cached the staging image on FORMAT ONLY while sizing it to the SOURCE
frame, and the copy uses the CURRENT frame's extent — so a same-format source-size
increase copied past the allocation. Now keyed on (format, width, height). This is the
memory-safety one: on hardware it was 8 x
VUID-vkCmdCopyBufferToImage-imageSubresource-07971 ("extent.width (512) exceeds
imageSubresource width extent (128)") and `submit` returned Ok every single time, so
nothing upstream could notice. Zero after the fix. Note the trigger for anyone re-testing:
the image is cached PER RING SLOT, so the ring must wrap before a slot sees a larger frame
— two differently-sized submits in a row land on different slots and look fine.
2. The RGB-direct CPU padding loop only ever grows a source into the aligned extent; a source
LARGER than the encode extent panicked on the row `copy_from_slice`. Now refused by name,
matching what the DMA-BUF arms already do, so a bad frame takes the encoder-rebuild path
instead of unwinding out of the encode thread with an index message.
3. AV1 wrote `render_width/height_minus_1` but never set
`render_and_frame_size_different`, and AV1 ignores the render size unless that flag says it
differs — so every mode needing 64x16 alignment presented the padding. libdav1d on the
encoded stream now reports `size 1920x1088 ... render 1920x1080`; before, the flag being
unset meant render defaulted to the coded 1088, i.e. 8 rows of duplicated edge pixels
shipped to the client.
4. `read_slot` read the PERF timestamp pool whenever the pool merely EXISTED, but the
padded-RGB path's CPU-upload arm records its own command buffer and writes no timestamps —
reading an unreset query with WAIT is undefined. Now gated on a per-slot `ts_written`,
because "a pool exists" was being used as proof of "the pool was written". SEVERITY
CORRECTION vs the audit: this is not a hang in practice. It needs PUNKTFUNK_PERF *and*
RGB-direct *and* the non-default PUNKTFUNK_VULKAN_RGB_TRUE_EXTENT=0 *and* an unaligned mode
*and* CPU capture, and RADV then fails the read rather than blocking. Another driver may
block, hence the fix.
5. `reset()` re-arms `first_frame`, which was also what decided whether begin-video-coding
declares the rate-control state. But a reset does not rebuild the session, so the CBR
installed earlier is still current when the next frame opens its coding scope: the
declaration was omitted exactly when it was required. Split into `rc_installed`, which
survives reset. On hardware this was
VUID-vkCmdBeginVideoCodingKHR-pBeginInfo-08253 ("no VkVideoEncodeRateControlInfoKHR ... but
the currently set mode is CBR"); zero after.
Also fixes two pre-existing AV1 spec violations that were firing on EVERY frame of a shipped
path and are not in the audit at all — found only because the validation layers were on, and
confirmed untouched by this branch before being attributed as pre-existing:
`constantQIndex` must be 0 unless rate control is DISABLED (VUID-...-10320; this session is
always CBR, so the driver owns Q), and `pStdPictureInfo->pSegmentation` must be NULL
(VUID-...-10350; it pointed at a zeroed struct, and leaving it unset means the same
"segmentation disabled" while being spec-correct).
Items 1 and 5 keep their reproductions as `#[ignore]`d hardware tests, documented as
meaningful only under `VK_LOADER_LAYERS_ENABLE='*validation*'` since the violations are
invisible to the API's return values.
STILL OPEN, filed not fixed (three more pre-existing validation errors the cleanup exposed):
VUID-VkQueryPoolCreateInfo-pNext-pNext on the AV1 and RGB paths, and 2 x
VUID-VkImageViewCreateInfo-image-08336 (the RGB-direct encode-src view's format features lack
VIDEO_ENCODE_INPUT) — the latter may be a validation-layer gap around the VALVE extension
rather than our bug, and wants real analysis rather than a quick patch.
Verified: canonical Linux gate (docker linux/amd64) fmt + clippy --all-targets at default and
at nvenc,vulkan-encode,pyrowave + both test legs; and on RADV PHOENIX all four `vulkan_smoke*`
tests pass at 256x256 and at 1920x1080 with zero validation errors on the paths touched here.
This commit is contained in:
@@ -437,6 +437,13 @@ struct Frame {
|
|||||||
// GPU timestamps around the CSC batch (`PUNKTFUNK_PERF` only; null otherwise): [0]=batch
|
// GPU timestamps around the CSC batch (`PUNKTFUNK_PERF` only; null otherwise): [0]=batch
|
||||||
// start, [1]=batch end — splits the fence wait into "compute CSC+copies" vs "ASIC encode".
|
// start, [1]=batch end — splits the fence wait into "compute CSC+copies" vs "ASIC encode".
|
||||||
ts_pool: vk::QueryPool,
|
ts_pool: vk::QueryPool,
|
||||||
|
// Whether the submission currently in this slot actually reset AND wrote `ts_pool`. A pool
|
||||||
|
// existing is NOT proof that it was written: `with_ts` arms the pool for the CSC and
|
||||||
|
// padded-RGB paths, but the padded-RGB path's CPU-upload arm records its own command buffer
|
||||||
|
// and writes no timestamps. Reading an unreset/unwritten query with `WAIT` is undefined —
|
||||||
|
// RADV happens to fail the read rather than block, another driver may hang — so `read_slot`
|
||||||
|
// consults this instead of `ts_pool != null`.
|
||||||
|
ts_written: bool,
|
||||||
bs_buf: vk::Buffer,
|
bs_buf: vk::Buffer,
|
||||||
bs_mem: vk::DeviceMemory,
|
bs_mem: vk::DeviceMemory,
|
||||||
bs_ptr: BsPtr, // persistent mapping of bs_mem (see BsPtr)
|
bs_ptr: BsPtr, // persistent mapping of bs_mem (see BsPtr)
|
||||||
@@ -456,7 +463,18 @@ struct Frame {
|
|||||||
nv12_mem: vk::DeviceMemory,
|
nv12_mem: vk::DeviceMemory,
|
||||||
nv12_view: vk::ImageView,
|
nv12_view: vk::ImageView,
|
||||||
// CPU-input staging (lazily sized; only the software-capture / smoke-test path uses it).
|
// CPU-input staging (lazily sized; only the software-capture / smoke-test path uses it).
|
||||||
cpu_img: Option<(vk::Image, vk::DeviceMemory, vk::ImageView, vk::Format)>,
|
// Keyed on (format, width, height) — NOT format alone. The CSC arm sizes this image to the
|
||||||
|
// SOURCE frame while `cmd_copy_buffer_to_image` uses the CURRENT frame's extent, so a
|
||||||
|
// same-format size change against a format-only key copies past the allocation (confirmed on
|
||||||
|
// RADV as VUID-vkCmdCopyBufferToImage-imageSubresource-07971, with `submit` still returning Ok).
|
||||||
|
cpu_img: Option<(
|
||||||
|
vk::Image,
|
||||||
|
vk::DeviceMemory,
|
||||||
|
vk::ImageView,
|
||||||
|
vk::Format,
|
||||||
|
u32,
|
||||||
|
u32,
|
||||||
|
)>,
|
||||||
cpu_stage: Option<(vk::Buffer, vk::DeviceMemory, u64)>,
|
cpu_stage: Option<(vk::Buffer, vk::DeviceMemory, u64)>,
|
||||||
// Per-slot cursor overlay (cursor-as-metadata): a fixed CURSOR_MAX² RGBA8 sampled image (bound
|
// Per-slot cursor overlay (cursor-as-metadata): a fixed CURSOR_MAX² RGBA8 sampled image (bound
|
||||||
// once at binding 3) + host staging. Re-uploaded only when the bitmap changes (`cursor_serial`);
|
// once at binding 3) + host staging. Re-uploaded only when the bitmap changes (`cursor_serial`);
|
||||||
@@ -581,6 +599,18 @@ pub struct VulkanVideoEncoder {
|
|||||||
enc_count: u64, // total frames encoded — drives the DPB ring cursor
|
enc_count: u64, // total frames encoded — drives the DPB ring cursor
|
||||||
auto_wire: i64, // fallback wire index when submit() (not submit_indexed) is used
|
auto_wire: i64, // fallback wire index when submit() (not submit_indexed) is used
|
||||||
first_frame: bool, // needs RESET + DPB layout transition + CBR install + IDR
|
first_frame: bool, // needs RESET + DPB layout transition + CBR install + IDR
|
||||||
|
/// Whether the SESSION OBJECT currently has a non-default rate-control mode installed.
|
||||||
|
///
|
||||||
|
/// Deliberately NOT `!first_frame`. `first_frame` means "this frame must issue RESET + install
|
||||||
|
/// CBR + quality", which is true both at open and after a mid-stream [`Encoder::reset`] — but
|
||||||
|
/// `vkCmdBeginVideoCodingKHR` must declare the rate-control state the session ACTUALLY has at
|
||||||
|
/// that moment, and a `reset()` does not touch the session object: the CBR installed before it
|
||||||
|
/// is still current when the next frame begins its coding scope. Keying the declaration on
|
||||||
|
/// `first_frame` therefore omitted it after every reset, which RADV's validation reports as
|
||||||
|
/// VUID-vkCmdBeginVideoCodingKHR-pBeginInfo-08253 ("no VkVideoEncodeRateControlInfoKHR ... but
|
||||||
|
/// the currently set mode is CBR"). Set once the install actually happens, and only cleared by
|
||||||
|
/// building a new session.
|
||||||
|
rc_installed: bool,
|
||||||
force_kf: bool, // request_keyframe / non-recoverable loss -> next frame is IDR
|
force_kf: bool, // request_keyframe / non-recoverable loss -> next frame is IDR
|
||||||
pending_loss: Option<i64>, // invalidate_ref_frames(first) -> recover on next frame
|
pending_loss: Option<i64>, // invalidate_ref_frames(first) -> recover on next frame
|
||||||
pending: VecDeque<EncodedFrame>,
|
pending: VecDeque<EncodedFrame>,
|
||||||
@@ -1272,6 +1302,9 @@ impl VulkanVideoEncoder {
|
|||||||
enc_count: 0,
|
enc_count: 0,
|
||||||
auto_wire: 0,
|
auto_wire: 0,
|
||||||
first_frame: true,
|
first_frame: true,
|
||||||
|
// No rate control installed on a brand-new session: its mode is DEFAULT until the
|
||||||
|
// first frame's control command, so frame 0 must NOT declare one at begin-coding.
|
||||||
|
rc_installed: false,
|
||||||
force_kf: false,
|
force_kf: false,
|
||||||
pending_loss: None,
|
pending_loss: None,
|
||||||
pending: VecDeque::new(),
|
pending: VecDeque::new(),
|
||||||
@@ -1546,8 +1579,12 @@ impl VulkanVideoEncoder {
|
|||||||
(src_w, src_h)
|
(src_w, src_h)
|
||||||
};
|
};
|
||||||
let need = (iw * ih * 4) as u64;
|
let need = (iw * ih * 4) as u64;
|
||||||
if self.frames[slot].cpu_img.map(|(_, _, _, f)| f) != Some(fmt) {
|
if self.frames[slot]
|
||||||
if let Some((i, m, v, _)) = self.frames[slot].cpu_img.take() {
|
.cpu_img
|
||||||
|
.map(|(_, _, _, f, iw, ih)| (f, iw, ih))
|
||||||
|
!= Some((fmt, iw, ih))
|
||||||
|
{
|
||||||
|
if let Some((i, m, v, ..)) = self.frames[slot].cpu_img.take() {
|
||||||
dev.destroy_image_view(v, None);
|
dev.destroy_image_view(v, None);
|
||||||
dev.destroy_image(i, None);
|
dev.destroy_image(i, None);
|
||||||
dev.free_memory(m, None);
|
dev.free_memory(m, None);
|
||||||
@@ -1590,7 +1627,7 @@ impl VulkanVideoEncoder {
|
|||||||
vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST,
|
vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST,
|
||||||
)?
|
)?
|
||||||
};
|
};
|
||||||
self.frames[slot].cpu_img = Some((i, m, v, fmt));
|
self.frames[slot].cpu_img = Some((i, m, v, fmt, iw, ih));
|
||||||
}
|
}
|
||||||
if self.frames[slot]
|
if self.frames[slot]
|
||||||
.cpu_stage
|
.cpu_stage
|
||||||
@@ -1640,6 +1677,20 @@ impl VulkanVideoEncoder {
|
|||||||
bytes.len()
|
bytes.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// The padding loop below only ever GROWS the source into the aligned extent. A source
|
||||||
|
// larger than the encode extent is a contract violation (the Dmabuf arms already bail
|
||||||
|
// on it) and would panic here on the row `copy_from_slice`, so refuse it by name
|
||||||
|
// instead of unwinding out of the encode thread with an index message.
|
||||||
|
if src_w > w || src_h > h {
|
||||||
|
bail!(
|
||||||
|
"vulkan-encode (rgb-direct): CPU frame {}x{} exceeds the encode extent {}x{} \
|
||||||
|
— the RGB-direct source must fit inside the aligned encode size",
|
||||||
|
src_w,
|
||||||
|
src_h,
|
||||||
|
w,
|
||||||
|
h
|
||||||
|
);
|
||||||
|
}
|
||||||
let mut out = vec![0u8; dw * dh * 4];
|
let mut out = vec![0u8; dw * dh * 4];
|
||||||
for y in 0..dh {
|
for y in 0..dh {
|
||||||
let sy = y.min(sh - 1);
|
let sy = y.min(sh - 1);
|
||||||
@@ -1768,9 +1819,12 @@ impl VulkanVideoEncoder {
|
|||||||
// PUNKTFUNK_PERF: bracket the whole compute batch (import barriers + cursor prep + CSC
|
// PUNKTFUNK_PERF: bracket the whole compute batch (import barriers + cursor prep + CSC
|
||||||
// dispatch + plane copies) with timestamps — read back in `read_slot` as `csc_us`, the
|
// dispatch + plane copies) with timestamps — read back in `read_slot` as `csc_us`, the
|
||||||
// half of the fence wait that is NOT the ASIC encode.
|
// half of the fence wait that is NOT the ASIC encode.
|
||||||
|
self.frames[slot].ts_written = false;
|
||||||
if self.ts_period_ns > 0.0 {
|
if self.ts_period_ns > 0.0 {
|
||||||
dev.cmd_reset_query_pool(compute_cmd, ts_pool, 0, 2);
|
dev.cmd_reset_query_pool(compute_cmd, ts_pool, 0, 2);
|
||||||
dev.cmd_write_timestamp2(compute_cmd, vk::PipelineStageFlags2::NONE, ts_pool, 0);
|
dev.cmd_write_timestamp2(compute_cmd, vk::PipelineStageFlags2::NONE, ts_pool, 0);
|
||||||
|
// This batch resets the pool and closes it below, so `read_slot` may read it.
|
||||||
|
self.frames[slot].ts_written = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cursor-as-metadata: refresh this slot's cursor image (only when the bitmap changed) and
|
// Cursor-as-metadata: refresh this slot's cursor image (only when the bitmap changed) and
|
||||||
@@ -2358,6 +2412,10 @@ impl VulkanVideoEncoder {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let padded = self.rgb.as_ref().is_some_and(|r| r.padded);
|
let padded = self.rgb.as_ref().is_some_and(|r| r.padded);
|
||||||
|
// Only the padded Dmabuf arm below records timestamps (via `record_pad_blit`); the
|
||||||
|
// CPU-upload arm records its own command buffer and writes none. Default to "not written"
|
||||||
|
// so `read_slot` can never read an unreset query — see `Frame::ts_written`.
|
||||||
|
self.frames[slot].ts_written = false;
|
||||||
let (src_img, src_view, acquire, compute_active) = match &frame.payload {
|
let (src_img, src_view, acquire, compute_active) = match &frame.payload {
|
||||||
FramePayload::Dmabuf(d) if !padded => {
|
FramePayload::Dmabuf(d) if !padded => {
|
||||||
// Defense in depth for the OOB class the alignment gate closes at open: the
|
// Defense in depth for the OOB class the alignment gate closes at open: the
|
||||||
@@ -2419,6 +2477,8 @@ impl VulkanVideoEncoder {
|
|||||||
ts_pool,
|
ts_pool,
|
||||||
&[(vk::ImageAspectFlags::COLOR, 1)],
|
&[(vk::ImageAspectFlags::COLOR, 1)],
|
||||||
)?;
|
)?;
|
||||||
|
// `record_pad_blit` reset and wrote both timestamps when PERF is armed.
|
||||||
|
self.frames[slot].ts_written = self.ts_period_ns > 0.0;
|
||||||
// The staging image ends the blit in GENERAL with the csc_sem ordering the
|
// The staging image ends the blit in GENERAL with the csc_sem ordering the
|
||||||
// hand-off — exactly the CscGeneral acquire contract.
|
// hand-off — exactly the CscGeneral acquire contract.
|
||||||
(pad_img, pad_view, SrcAcquire::CscGeneral, true)
|
(pad_img, pad_view, SrcAcquire::CscGeneral, true)
|
||||||
@@ -2544,6 +2604,10 @@ impl VulkanVideoEncoder {
|
|||||||
self.poc = poc + 1;
|
self.poc = poc + 1;
|
||||||
self.enc_count += 1;
|
self.enc_count += 1;
|
||||||
self.first_frame = false;
|
self.first_frame = false;
|
||||||
|
// The frame just recorded carried the RESET + CBR install (that is what `first_frame`
|
||||||
|
// gated), so from here on the session HAS a non-default rate-control mode — and unlike
|
||||||
|
// `first_frame` this survives `reset()`, because a reset does not rebuild the session.
|
||||||
|
self.rc_installed = true;
|
||||||
self.force_kf = false;
|
self.force_kf = false;
|
||||||
if let Some(nb) = self.pending_bitrate.take() {
|
if let Some(nb) = self.pending_bitrate.take() {
|
||||||
// The retarget control command is recorded (execution follows submission order): the
|
// The retarget control command is recorded (execution follows submission order): the
|
||||||
@@ -2815,9 +2879,11 @@ impl VulkanVideoEncoder {
|
|||||||
.video_session(self.session)
|
.video_session(self.session)
|
||||||
.video_session_parameters(self.params)
|
.video_session_parameters(self.params)
|
||||||
.reference_slots(begin_slots);
|
.reference_slots(begin_slots);
|
||||||
if !self.first_frame {
|
// Declare the session's ACTUAL current rate-control state, not "not the first frame" —
|
||||||
|
// a mid-stream `reset()` re-arms `first_frame` while the session keeps its installed CBR.
|
||||||
|
if self.rc_installed {
|
||||||
begin.p_next = rc_ptr;
|
begin.p_next = rc_ptr;
|
||||||
} // CBR is current state after frame 0's control
|
}
|
||||||
(self.vq_dev.fp().cmd_begin_video_coding_khr)(cmd, &begin);
|
(self.vq_dev.fp().cmd_begin_video_coding_khr)(cmd, &begin);
|
||||||
if self.first_frame {
|
if self.first_frame {
|
||||||
// RESET + CBR install + explicit quality level. Without ENCODE_QUALITY_LEVEL, RADV
|
// RESET + CBR install + explicit quality level. Without ENCODE_QUALITY_LEVEL, RADV
|
||||||
@@ -2950,7 +3016,6 @@ impl VulkanVideoEncoder {
|
|||||||
lr.FrameRestorationType =
|
lr.FrameRestorationType =
|
||||||
[h::StdVideoAV1FrameRestorationType_STD_VIDEO_AV1_FRAME_RESTORATION_TYPE_NONE; 3];
|
[h::StdVideoAV1FrameRestorationType_STD_VIDEO_AV1_FRAME_RESTORATION_TYPE_NONE; 3];
|
||||||
|
|
||||||
let seg: h::StdVideoAV1Segmentation = std::mem::zeroed();
|
|
||||||
let gm: h::StdVideoAV1GlobalMotion = std::mem::zeroed();
|
let gm: h::StdVideoAV1GlobalMotion = std::mem::zeroed();
|
||||||
|
|
||||||
// Order hints of the 8 physical reference buffers (DPB slots), 0 where empty.
|
// Order hints of the 8 physical reference buffers (DPB slots), 0 where empty.
|
||||||
@@ -2970,6 +3035,14 @@ impl VulkanVideoEncoder {
|
|||||||
if independent {
|
if independent {
|
||||||
pic_flags.set_error_resilient_mode(1);
|
pic_flags.set_error_resilient_mode(1);
|
||||||
}
|
}
|
||||||
|
// AV1 IGNORES render_width/height_minus_1 unless this flag says the render size differs
|
||||||
|
// from the coded frame size. We always wrote the render size (below) but never the flag, so
|
||||||
|
// at any mode that needed 64x16 alignment the decoder fell back to the CODED size and the
|
||||||
|
// client displayed our alignment padding — e.g. 1080p arriving as 1088 rows, the bottom 8
|
||||||
|
// being duplicated edge pixels.
|
||||||
|
if self.render_w != self.width || self.render_h != self.height {
|
||||||
|
pic_flags.set_render_and_frame_size_different(1);
|
||||||
|
}
|
||||||
let mut std_pic: av1::StdVideoEncodeAV1PictureInfo = std::mem::zeroed();
|
let mut std_pic: av1::StdVideoEncodeAV1PictureInfo = std::mem::zeroed();
|
||||||
std_pic.flags = pic_flags;
|
std_pic.flags = pic_flags;
|
||||||
std_pic.frame_type = if is_idr {
|
std_pic.frame_type = if is_idr {
|
||||||
@@ -2998,7 +3071,10 @@ impl VulkanVideoEncoder {
|
|||||||
std_pic.pLoopFilter = &loop_filter;
|
std_pic.pLoopFilter = &loop_filter;
|
||||||
std_pic.pCDEF = &cdef;
|
std_pic.pCDEF = &cdef;
|
||||||
std_pic.pLoopRestoration = &lr;
|
std_pic.pLoopRestoration = &lr;
|
||||||
std_pic.pSegmentation = &seg;
|
// pSegmentation MUST be NULL for an AV1 encode operation
|
||||||
|
// (VUID-vkCmdEncodeVideoKHR-pStdPictureInfo-10350). It used to point at a zeroed
|
||||||
|
// segmentation struct, which RADV's validation rejects on every frame; `std_pic` is
|
||||||
|
// zeroed, so leaving it unset is both spec-correct and the same "segmentation disabled".
|
||||||
std_pic.pGlobalMotion = &gm;
|
std_pic.pGlobalMotion = &gm;
|
||||||
|
|
||||||
// ---- KHR picture info ----
|
// ---- KHR picture info ----
|
||||||
@@ -3015,7 +3091,11 @@ impl VulkanVideoEncoder {
|
|||||||
} else {
|
} else {
|
||||||
av1::RC_GROUP_PREDICTIVE
|
av1::RC_GROUP_PREDICTIVE
|
||||||
},
|
},
|
||||||
constant_q_index: quant.base_q_idx as u32,
|
// MUST be zero whenever the rate control mode is not DISABLED
|
||||||
|
// (VUID-vkCmdEncodeVideoKHR-constantQIndex-10320) — this session installs CBR, so the
|
||||||
|
// driver owns Q. Passing `base_q_idx` here was rejected by validation on every frame;
|
||||||
|
// the value still reaches the encoder through `pQuantization` for the header.
|
||||||
|
constant_q_index: 0,
|
||||||
p_std_picture_info: &std_pic,
|
p_std_picture_info: &std_pic,
|
||||||
reference_name_slot_indices: if is_idr {
|
reference_name_slot_indices: if is_idr {
|
||||||
[-1; av1::MAX_VIDEO_AV1_REFERENCES_PER_FRAME]
|
[-1; av1::MAX_VIDEO_AV1_REFERENCES_PER_FRAME]
|
||||||
@@ -3105,7 +3185,8 @@ impl VulkanVideoEncoder {
|
|||||||
.video_session(self.session)
|
.video_session(self.session)
|
||||||
.video_session_parameters(self.params)
|
.video_session_parameters(self.params)
|
||||||
.reference_slots(begin_slots);
|
.reference_slots(begin_slots);
|
||||||
if !self.first_frame {
|
// See the h265 twin: declare what the session actually has, not `!first_frame`.
|
||||||
|
if self.rc_installed {
|
||||||
begin.p_next = rc_ptr;
|
begin.p_next = rc_ptr;
|
||||||
}
|
}
|
||||||
(self.vq_dev.fp().cmd_begin_video_coding_khr)(cmd, &begin);
|
(self.vq_dev.fp().cmd_begin_video_coding_khr)(cmd, &begin);
|
||||||
@@ -3211,6 +3292,7 @@ impl VulkanVideoEncoder {
|
|||||||
// remaining host fence wait as pure VCN time; queueing and synchronization remain in it.
|
// remaining host fence wait as pure VCN time; queueing and synchronization remain in it.
|
||||||
if self.ts_period_ns > 0.0
|
if self.ts_period_ns > 0.0
|
||||||
&& f.ts_pool != vk::QueryPool::null()
|
&& f.ts_pool != vk::QueryPool::null()
|
||||||
|
&& f.ts_written
|
||||||
&& self.perf_at.elapsed() >= std::time::Duration::from_secs(2)
|
&& self.perf_at.elapsed() >= std::time::Duration::from_secs(2)
|
||||||
{
|
{
|
||||||
let mut ts = [0u64; 2];
|
let mut ts = [0u64; 2];
|
||||||
@@ -3561,7 +3643,7 @@ impl Drop for VkTeardown {
|
|||||||
device.destroy_image(img, None);
|
device.destroy_image(img, None);
|
||||||
device.free_memory(mem, None);
|
device.free_memory(mem, None);
|
||||||
}
|
}
|
||||||
if let Some((i, m, v, _)) = f.cpu_img {
|
if let Some((i, m, v, ..)) = f.cpu_img {
|
||||||
device.destroy_image_view(v, None);
|
device.destroy_image_view(v, None);
|
||||||
device.destroy_image(i, None);
|
device.destroy_image(i, None);
|
||||||
device.free_memory(m, None);
|
device.free_memory(m, None);
|
||||||
@@ -4755,6 +4837,86 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A CPU-capture source that CHANGES SIZE mid-session must not copy past the cached staging
|
||||||
|
/// image. `ensure_cpu_rgb` keys that image on (format, width, height); when it was keyed on
|
||||||
|
/// format alone the image kept the FIRST frame's size while `cmd_copy_buffer_to_image` used the
|
||||||
|
/// current frame's extent, so a same-format size increase wrote out of bounds — and `submit`
|
||||||
|
/// still returned `Ok`, so nothing upstream noticed.
|
||||||
|
///
|
||||||
|
/// The out-of-bounds copy is only *observable* through the Vulkan validation layers, so run this
|
||||||
|
/// as: `VK_LOADER_LAYERS_ENABLE='*validation*' cargo test ... -- --ignored`. Confirmed on RADV
|
||||||
|
/// PHOENIX 2026-07-25: 8 x VUID-vkCmdCopyBufferToImage-imageSubresource-07971 before the fix
|
||||||
|
/// ("extent.width (512) exceeds imageSubresource width extent (128)"), zero after.
|
||||||
|
#[test]
|
||||||
|
#[ignore = "needs a real VK_KHR_video_encode_h265 device; meaningful only under validation layers"]
|
||||||
|
fn vulkan_cpu_img_survives_a_source_size_change() {
|
||||||
|
// CSC mode (rgb=false) — that is the arm where the image is sized to the SOURCE frame.
|
||||||
|
let mut enc = VulkanVideoEncoder::open_opts(Codec::H265, 512, 512, 60, 10_000_000, false)
|
||||||
|
.expect("open");
|
||||||
|
// `cpu_img` is cached PER RING SLOT, so the small frames must first populate every slot;
|
||||||
|
// only when the ring wraps does a slot holding a 128x128 image get a 512x512 copy.
|
||||||
|
eprintln!("phase 1: 8x 128x128 — populates every ring slot with a 128x128 cpu_img");
|
||||||
|
for i in 0..8u64 {
|
||||||
|
enc.submit_indexed(
|
||||||
|
&cpu_frame(128, 128, i * 16_666_667, [40, 40, 200, 255]),
|
||||||
|
i as u32,
|
||||||
|
)
|
||||||
|
.expect("submit small");
|
||||||
|
while enc.poll().expect("poll").is_some() {}
|
||||||
|
}
|
||||||
|
eprintln!("phase 2: 8x 512x512 — SAME format, so each slot REUSES its 128x128 image");
|
||||||
|
for i in 8..16u64 {
|
||||||
|
let r = enc.submit_indexed(
|
||||||
|
&cpu_frame(512, 512, i * 16_666_667, [200, 40, 40, 255]),
|
||||||
|
i as u32,
|
||||||
|
);
|
||||||
|
r.expect("submit after the source grew");
|
||||||
|
while matches!(enc.poll(), Ok(Some(_))) {}
|
||||||
|
}
|
||||||
|
let _ = enc.flush();
|
||||||
|
while matches!(enc.poll(), Ok(Some(_))) {}
|
||||||
|
eprintln!("done — under validation layers this run must report ZERO VUID errors");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mid-stream [`Encoder::reset`] must not change what `vkCmdBeginVideoCodingKHR` declares
|
||||||
|
/// about the session's rate-control state. `reset()` re-arms `first_frame` (the next frame
|
||||||
|
/// re-issues RESET + install), but it does NOT rebuild the session, so the CBR installed
|
||||||
|
/// earlier is still the session's current mode when that frame opens its coding scope —
|
||||||
|
/// which is why the declaration keys on `rc_installed`, not `!first_frame`.
|
||||||
|
///
|
||||||
|
/// Only the validation layers can see the mismatch, so run as:
|
||||||
|
/// `VK_LOADER_LAYERS_ENABLE='*validation*' cargo test ... -- --ignored`. Confirmed on RADV
|
||||||
|
/// PHOENIX 2026-07-25: VUID-vkCmdBeginVideoCodingKHR-pBeginInfo-08253 before the fix ("no
|
||||||
|
/// VkVideoEncodeRateControlInfoKHR ... but the currently set mode is CBR"), zero after.
|
||||||
|
#[test]
|
||||||
|
#[ignore = "needs a real VK_KHR_video_encode_h265 device; meaningful only under validation layers"]
|
||||||
|
fn vulkan_reset_keeps_the_declared_rate_control_state() {
|
||||||
|
let (w, h) = (256u32, 256u32);
|
||||||
|
let mut enc =
|
||||||
|
VulkanVideoEncoder::open_opts(Codec::H265, w, h, 60, 10_000_000, false).expect("open");
|
||||||
|
eprintln!("phase 1: 4 frames (installs CBR on frame 0)");
|
||||||
|
for i in 0..4u64 {
|
||||||
|
enc.submit_indexed(
|
||||||
|
&cpu_frame(w, h, i * 16_666_667, [40, 40, 200, 255]),
|
||||||
|
i as u32,
|
||||||
|
)
|
||||||
|
.expect("submit");
|
||||||
|
while enc.poll().expect("poll").is_some() {}
|
||||||
|
}
|
||||||
|
eprintln!("phase 2: reset() mid-stream");
|
||||||
|
assert!(enc.reset(), "reset should succeed");
|
||||||
|
eprintln!("phase 3: 4 more frames — first one re-declares state as if fresh");
|
||||||
|
for i in 4..8u64 {
|
||||||
|
enc.submit_indexed(
|
||||||
|
&cpu_frame(w, h, i * 16_666_667, [200, 40, 40, 255]),
|
||||||
|
i as u32,
|
||||||
|
)
|
||||||
|
.expect("submit after reset");
|
||||||
|
while enc.poll().expect("poll").is_some() {}
|
||||||
|
}
|
||||||
|
eprintln!("done — under validation layers this run must report ZERO VUID errors");
|
||||||
|
}
|
||||||
|
|
||||||
/// `PUNKTFUNK_VULKAN_RGB_DIRECT` accepts the same spellings as every sibling knob, trimmed.
|
/// `PUNKTFUNK_VULKAN_RGB_DIRECT` accepts the same spellings as every sibling knob, trimmed.
|
||||||
#[test]
|
#[test]
|
||||||
fn rgb_direct_knob_accepts_the_house_spellings() {
|
fn rgb_direct_knob_accepts_the_house_spellings() {
|
||||||
|
|||||||
Reference in New Issue
Block a user