feat(encode): RGB-direct padded-copy — unaligned modes (1080p) get the EFC path safely
apple / swift (push) Successful in 1m20s
apple / screenshots (push) Successful in 6m42s
windows-host / package (push) Successful in 9m24s
android / android (push) Failing after 18s
ci / web (push) Successful in 1m8s
ci / docs-site (push) Successful in 1m43s
arch / build-publish (push) Successful in 11m48s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
ci / bench (push) Successful in 7m33s
deb / build-publish (push) Successful in 9m1s
deb / build-publish-host (push) Successful in 13m6s
ci / rust (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
apple / swift (push) Successful in 1m20s
apple / screenshots (push) Successful in 6m42s
windows-host / package (push) Successful in 9m24s
android / android (push) Failing after 18s
ci / web (push) Successful in 1m8s
ci / docs-site (push) Successful in 1m43s
arch / build-publish (push) Successful in 11m48s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
ci / bench (push) Successful in 7m33s
deb / build-publish (push) Successful in 9m1s
deb / build-publish-host (push) Successful in 13m6s
ci / rust (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
Lifts 3aacec53's alignment gate into a mode select: an aligned mode keeps the true zero-copy direct import; an unaligned one (1080p!) now blits the visible frame into a per-slot ALIGNED BGRA staging image and duplicates the edge rows/columns into the 64x16 padding — transfer-only regions in one vkCmdCopyImage (1080p: visible + 8 row regions; width padding adds a second self-copy pass in GENERAL), no compute shader. The encode reads the staging image, never the capture buffer, so the EFC can never read past a producer allocation (the field GPU hang). Still one ~8 MB copy vs the CSC path's ~17 MB + dispatch + plane copies. Verdict line: active(padded-copy). The staging import drops the video profile entirely (TRANSFER_SRC only). CPU-payload paths made honest on the way (they were the smoke baseline AND the software-capture fallback): - rgb mode: the staging upload is padded CPU-side (edge duplication) so the aligned encode-src is fully defined; - CSC mode: the sampled image is now SOURCE-sized with a matching copy extent — the old aligned-size image + tightly-packed buffer sheared rows and left garbage rows at unaligned modes (black-bar artifacts; YMIN=16 in every smoke frame), which also masked as a 24 dB PSNR 'regression' against the (correct) padded output. On-glass (780M, host Mesa 26.0.4): all four smokes pass at 256x256 (direct) AND 250x250 (padded); padded-EFC frames decode perfectly uniform (YMIN==YMAX) at the exact 709-narrow values (79/148/60 for the first three fills); CSC-vs-padded PSNR 49.9 dB avg after the baseline fix. clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,12 @@ fn rgb_request() -> bool {
|
|||||||
struct RgbDirect {
|
struct RgbDirect {
|
||||||
x_offset: u32, // vk_valve_rgb::CHROMA_OFFSET_*
|
x_offset: u32, // vk_valve_rgb::CHROMA_OFFSET_*
|
||||||
y_offset: u32,
|
y_offset: u32,
|
||||||
|
/// The mode is not 64x16-aligned, so the captured buffer cannot be the encode source
|
||||||
|
/// directly (the EFC would read past it — the 2026-07-20 field GPU hang). Instead each
|
||||||
|
/// frame is copied into a per-slot ALIGNED BGRA staging image with the edge rows/columns
|
||||||
|
/// duplicated into the padding (transfer-only, no shader) and encoded from there. Aligned
|
||||||
|
/// modes keep the true zero-copy import.
|
||||||
|
padded: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stack storage for a complete rgb-chained video profile. Profiled image creation AFTER open
|
/// Stack storage for a complete rgb-chained video profile. Profiled image creation AFTER open
|
||||||
@@ -266,6 +272,11 @@ struct Frame {
|
|||||||
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)
|
||||||
|
// Padded-copy RGB staging (RGB-direct on an unaligned mode ONLY, see RgbDirect::padded):
|
||||||
|
// an ALIGNED BGRA encode-src the visible frame is blitted into with edges duplicated.
|
||||||
|
pad_img: vk::Image,
|
||||||
|
pad_mem: vk::DeviceMemory,
|
||||||
|
pad_view: vk::ImageView,
|
||||||
csc_set: vk::DescriptorSet, // Y/UV bindings fixed; binding 0 (RGB) rewritten each use
|
csc_set: vk::DescriptorSet, // Y/UV bindings fixed; binding 0 (RGB) rewritten each use
|
||||||
y_img: vk::Image,
|
y_img: vk::Image,
|
||||||
y_mem: vk::DeviceMemory,
|
y_mem: vk::DeviceMemory,
|
||||||
@@ -533,26 +544,29 @@ impl VulkanVideoEncoder {
|
|||||||
// deterministic VM protection faults, vcn_enc ring timeouts, and — through the stall
|
// deterministic VM protection faults, vcn_enc ring timeouts, and — through the stall
|
||||||
// watchdog's rebuild-and-refault loop — a full MODE1 GPU reset with VRAM loss. The CSC
|
// watchdog's rebuild-and-refault loop — a full MODE1 GPU reset with VRAM loss. The CSC
|
||||||
// shader absorbs the padding by clamping reads and duplicating the edge; RGB-direct has
|
// shader absorbs the padding by clamping reads and duplicating the edge; RGB-direct has
|
||||||
// no such stage, so until the padded-copy variant lands (design doc B2) it only engages
|
// no such stage. Mode select: an aligned mode (720p/1440p/4K) encodes the imported
|
||||||
// when the mode is already aligned (720p/1440p/4K are; 1080p is NOT).
|
// buffer directly (true zero-copy); an unaligned one (1080p!) goes through the
|
||||||
|
// padded-copy staging image (see [`RgbDirect::padded`]) — transfer-only, still no
|
||||||
|
// compute CSC.
|
||||||
let aligned = rw == w && rh == h;
|
let aligned = rw == w && rh == h;
|
||||||
let rgb_cfg: Option<RgbDirect> = match (&rgb_probe, want_rgb, aligned) {
|
let rgb_cfg: Option<RgbDirect> = match (&rgb_probe, want_rgb) {
|
||||||
(Ok((x, y)), true, true) => Some(RgbDirect {
|
(Ok((x, y)), true) => Some(RgbDirect {
|
||||||
x_offset: *x,
|
x_offset: *x,
|
||||||
y_offset: *y,
|
y_offset: *y,
|
||||||
|
padded: !aligned,
|
||||||
}),
|
}),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
rgb_direct = match (&rgb_probe, want_rgb, aligned, &rgb_cfg) {
|
rgb_direct = match (&rgb_probe, want_rgb, &rgb_cfg) {
|
||||||
(_, _, _, Some(_)) => "active",
|
(_, _, Some(RgbDirect { padded: false, .. })) => "active",
|
||||||
(Ok(_), true, false, None) =>
|
(_, _, Some(RgbDirect { padded: true, .. })) =>
|
||||||
"unaligned-mode(coded extent is 64x16-aligned; direct source would read \
|
"active(padded-copy: mode is not 64x16-aligned — staging blit + edge \
|
||||||
past the capture buffer — CSC path used; padded-copy variant is B2)",
|
duplication instead of the direct import)",
|
||||||
(Ok(_), false, _, None) => "available(off; set PUNKTFUNK_VULKAN_RGB_DIRECT=1)",
|
(Ok(_), false, None) => "available(off; set PUNKTFUNK_VULKAN_RGB_DIRECT=1)",
|
||||||
(Err(e), _, _, None) => e,
|
(Err(e), _, None) => e,
|
||||||
// (Ok, wanted, aligned) always builds Some above.
|
// (Ok, wanted) always builds Some above.
|
||||||
(Ok(_), true, true, None) => unreachable!("rgb gate and cfg disagree"),
|
(Ok(_), true, None) => unreachable!("rgb gate and cfg disagree"),
|
||||||
},
|
},
|
||||||
"vulkan-encode: EFC RGB-direct encode source (design/vulkan-rgb-direct-encode.md)"
|
"vulkan-encode: EFC RGB-direct encode source (design/vulkan-rgb-direct-encode.md)"
|
||||||
);
|
);
|
||||||
@@ -963,6 +977,7 @@ impl VulkanVideoEncoder {
|
|||||||
sampler,
|
sampler,
|
||||||
ts_period_ns > 0.0 && rgb_cfg.is_none(),
|
ts_period_ns > 0.0 && rgb_cfg.is_none(),
|
||||||
rgb_cfg.is_none(),
|
rgb_cfg.is_none(),
|
||||||
|
rgb_cfg.as_ref().is_some_and(|c| c.padded),
|
||||||
guard.frames.last_mut().expect("frame just pushed"),
|
guard.frames.last_mut().expect("frame just pushed"),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
@@ -1173,7 +1188,20 @@ impl VulkanVideoEncoder {
|
|||||||
cw: u32,
|
cw: u32,
|
||||||
ch: u32,
|
ch: u32,
|
||||||
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
|
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
|
||||||
if self.rgb.is_some() {
|
if self.rgb.as_ref().is_some_and(|r| r.padded) {
|
||||||
|
// Padded-copy mode: the import is only ever a transfer SOURCE (the blit into the
|
||||||
|
// aligned staging image) — plain TRANSFER_SRC, no video profile involved.
|
||||||
|
super::vk_util::import_rgb_dmabuf_as(
|
||||||
|
&self.device,
|
||||||
|
&self.ext_fd,
|
||||||
|
&self.mem_props,
|
||||||
|
d,
|
||||||
|
cw,
|
||||||
|
ch,
|
||||||
|
vk::ImageUsageFlags::TRANSFER_SRC,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
} else if self.rgb.is_some() {
|
||||||
let mut ps = RgbProfileStack::new(codec_op_for(self.codec == Codec::Av1));
|
let mut ps = RgbProfileStack::new(codec_op_for(self.codec == Codec::Av1));
|
||||||
let profile = *ps.wire(self.codec == Codec::Av1);
|
let profile = *ps.wire(self.codec == Codec::Av1);
|
||||||
let arr = [profile];
|
let arr = [profile];
|
||||||
@@ -1256,10 +1284,21 @@ impl VulkanVideoEncoder {
|
|||||||
slot: usize,
|
slot: usize,
|
||||||
fmt: vk::Format,
|
fmt: vk::Format,
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
|
src_w: u32,
|
||||||
|
src_h: u32,
|
||||||
) -> Result<vk::ImageView> {
|
) -> Result<vk::ImageView> {
|
||||||
let dev = self.device.clone();
|
let dev = self.device.clone();
|
||||||
let (w, h) = (self.width, self.height);
|
let (w, h) = (self.width, self.height);
|
||||||
let need = (w * h * 4) as u64;
|
// CSC mode: the image is the SHADER'S sampling source — size it to the real frame so
|
||||||
|
// the clamp-to-edge duplicates true content (the aligned-size image made the clamp land
|
||||||
|
// on unwritten rows: black garbage at unaligned modes, the pre-B2 smoke-baseline bug).
|
||||||
|
// RGB mode: the image IS the encode source — aligned dims, CPU-side padding below.
|
||||||
|
let (iw, ih) = if self.rgb.is_some() {
|
||||||
|
(w, h)
|
||||||
|
} else {
|
||||||
|
(src_w, src_h)
|
||||||
|
};
|
||||||
|
let need = (iw * ih * 4) as u64;
|
||||||
if self.frames[slot].cpu_img.map(|(_, _, _, f)| f) != Some(fmt) {
|
if self.frames[slot].cpu_img.map(|(_, _, _, f)| f) != Some(fmt) {
|
||||||
if let Some((i, m, v, _)) = self.frames[slot].cpu_img.take() {
|
if let Some((i, m, v, _)) = self.frames[slot].cpu_img.take() {
|
||||||
dev.destroy_image_view(v, None);
|
dev.destroy_image_view(v, None);
|
||||||
@@ -1299,8 +1338,8 @@ impl VulkanVideoEncoder {
|
|||||||
&dev,
|
&dev,
|
||||||
&self.mem_props,
|
&self.mem_props,
|
||||||
fmt,
|
fmt,
|
||||||
w,
|
iw,
|
||||||
h,
|
ih,
|
||||||
vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST,
|
vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST,
|
||||||
)?
|
)?
|
||||||
};
|
};
|
||||||
@@ -1337,9 +1376,43 @@ impl VulkanVideoEncoder {
|
|||||||
self.frames[slot].cpu_stage = Some((buf, mem, need));
|
self.frames[slot].cpu_stage = Some((buf, mem, need));
|
||||||
}
|
}
|
||||||
let (_, m, _) = self.frames[slot].cpu_stage.unwrap();
|
let (_, m, _) = self.frames[slot].cpu_stage.unwrap();
|
||||||
|
// RGB-direct sessions upload the image the ENCODER reads directly, so an undersized
|
||||||
|
// source (unaligned mode) must be padded here — rows/columns duplicated from the edge,
|
||||||
|
// matching the CSC shader's clamped reads (and record_pad_blit's GPU-side equivalent).
|
||||||
|
// The CSC path keeps the raw copy: its shader clamps at sample time.
|
||||||
|
let padded_owned: Vec<u8>;
|
||||||
|
let upload: &[u8] = if self.rgb.is_some() && (src_w != w || src_h != h) {
|
||||||
|
let (sw, sh) = (src_w as usize, src_h as usize);
|
||||||
|
let (dw, dh) = (w as usize, h as usize);
|
||||||
|
if bytes.len() < sw * sh * 4 {
|
||||||
|
bail!(
|
||||||
|
"vulkan-encode (rgb-direct): CPU frame {}x{} needs {} bytes, got {}",
|
||||||
|
src_w,
|
||||||
|
src_h,
|
||||||
|
sw * sh * 4,
|
||||||
|
bytes.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut out = vec![0u8; dw * dh * 4];
|
||||||
|
for y in 0..dh {
|
||||||
|
let sy = y.min(sh - 1);
|
||||||
|
let srow = &bytes[sy * sw * 4..][..sw * 4];
|
||||||
|
let drow = &mut out[y * dw * 4..][..dw * 4];
|
||||||
|
drow[..sw * 4].copy_from_slice(srow);
|
||||||
|
let mut last = [0u8; 4];
|
||||||
|
last.copy_from_slice(&srow[(sw - 1) * 4..]);
|
||||||
|
for x in sw..dw {
|
||||||
|
drow[x * 4..(x + 1) * 4].copy_from_slice(&last);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
padded_owned = out;
|
||||||
|
&padded_owned
|
||||||
|
} else {
|
||||||
|
bytes
|
||||||
|
};
|
||||||
let p = dev.map_memory(m, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty())? as *mut u8;
|
let p = dev.map_memory(m, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty())? as *mut u8;
|
||||||
let n = bytes.len().min(need as usize);
|
let n = upload.len().min(need as usize);
|
||||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), p, n);
|
std::ptr::copy_nonoverlapping(upload.as_ptr(), p, n);
|
||||||
dev.unmap_memory(m);
|
dev.unmap_memory(m);
|
||||||
Ok(self.frames[slot].cpu_img.unwrap().2)
|
Ok(self.frames[slot].cpu_img.unwrap().2)
|
||||||
}
|
}
|
||||||
@@ -1487,7 +1560,7 @@ impl VulkanVideoEncoder {
|
|||||||
}
|
}
|
||||||
FramePayload::Cpu(bytes) => {
|
FramePayload::Cpu(bytes) => {
|
||||||
let fmt = pixel_to_vk(frame.format).context("unsupported CPU pixel format")?;
|
let fmt = pixel_to_vk(frame.format).context("unsupported CPU pixel format")?;
|
||||||
let view = self.ensure_cpu_rgb(slot, fmt, bytes)?;
|
let view = self.ensure_cpu_rgb(slot, fmt, bytes, frame.width, frame.height)?;
|
||||||
let (img, ..) = self.frames[slot].cpu_img.unwrap();
|
let (img, ..) = self.frames[slot].cpu_img.unwrap();
|
||||||
let (stage, ..) = self.frames[slot].cpu_stage.unwrap();
|
let (stage, ..) = self.frames[slot].cpu_stage.unwrap();
|
||||||
let to_dst = vk::ImageMemoryBarrier2::default()
|
let to_dst = vk::ImageMemoryBarrier2::default()
|
||||||
@@ -1516,9 +1589,13 @@ impl VulkanVideoEncoder {
|
|||||||
.aspect_mask(vk::ImageAspectFlags::COLOR)
|
.aspect_mask(vk::ImageAspectFlags::COLOR)
|
||||||
.layer_count(1),
|
.layer_count(1),
|
||||||
)
|
)
|
||||||
|
// The staging buffer holds the REAL frame tightly packed and the image
|
||||||
|
// is source-sized (see ensure_cpu_rgb) — an aligned-size extent here
|
||||||
|
// sheared rows against the packed buffer and left garbage rows at
|
||||||
|
// unaligned modes.
|
||||||
.image_extent(vk::Extent3D {
|
.image_extent(vk::Extent3D {
|
||||||
width: self.width,
|
width: frame.width,
|
||||||
height: self.height,
|
height: frame.height,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
})],
|
})],
|
||||||
);
|
);
|
||||||
@@ -1733,6 +1810,133 @@ impl VulkanVideoEncoder {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Padded-copy blit (unaligned-mode RGB-direct): record — into `compute_cmd` — the visible
|
||||||
|
/// frame copy from the imported capture image into the aligned staging image, plus the edge
|
||||||
|
/// duplication into the 64x16 padding (the same edge semantics the CSC shader implements
|
||||||
|
/// with clamped reads). Transfer-only, no shader. The staging image lives in GENERAL — it
|
||||||
|
/// is both copy dst and, for the right-column pass, copy src — and the encode acquires it
|
||||||
|
/// via [`SrcAcquire::CscGeneral`] (content produced on the compute queue, csc_sem-ordered).
|
||||||
|
unsafe fn record_pad_blit(
|
||||||
|
&self,
|
||||||
|
dev: &ash::Device,
|
||||||
|
compute_cmd: vk::CommandBuffer,
|
||||||
|
src: vk::Image,
|
||||||
|
src_fresh: bool,
|
||||||
|
pad: vk::Image,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (rw, rh) = (self.render_w, self.render_h);
|
||||||
|
let (w, h) = (self.width, self.height);
|
||||||
|
dev.begin_command_buffer(
|
||||||
|
compute_cmd,
|
||||||
|
&vk::CommandBufferBeginInfo::default()
|
||||||
|
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
|
||||||
|
)?;
|
||||||
|
// Acquire the imported capture buffer for transfer reads (FOREIGN hand-off on first
|
||||||
|
// import — UNDEFINED preserves the modifier-tiled bytes — visibility-only afterwards),
|
||||||
|
// and the staging image for transfer writes (prior contents discarded).
|
||||||
|
let src_acq = if src_fresh {
|
||||||
|
vk::ImageMemoryBarrier2::default()
|
||||||
|
.src_stage_mask(vk::PipelineStageFlags2::NONE)
|
||||||
|
.src_access_mask(vk::AccessFlags2::NONE)
|
||||||
|
.dst_stage_mask(vk::PipelineStageFlags2::ALL_TRANSFER)
|
||||||
|
.dst_access_mask(vk::AccessFlags2::TRANSFER_READ)
|
||||||
|
.old_layout(vk::ImageLayout::UNDEFINED)
|
||||||
|
.new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
|
||||||
|
.src_queue_family_index(vk::QUEUE_FAMILY_FOREIGN_EXT)
|
||||||
|
.dst_queue_family_index(self.compute_family)
|
||||||
|
} else {
|
||||||
|
vk::ImageMemoryBarrier2::default()
|
||||||
|
.src_stage_mask(vk::PipelineStageFlags2::NONE)
|
||||||
|
.src_access_mask(vk::AccessFlags2::NONE)
|
||||||
|
.dst_stage_mask(vk::PipelineStageFlags2::ALL_TRANSFER)
|
||||||
|
.dst_access_mask(vk::AccessFlags2::TRANSFER_READ)
|
||||||
|
.old_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
|
||||||
|
.new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
|
||||||
|
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||||
|
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||||
|
}
|
||||||
|
.image(src)
|
||||||
|
.subresource_range(color_range(0));
|
||||||
|
let pad_dst = vk::ImageMemoryBarrier2::default()
|
||||||
|
.src_stage_mask(vk::PipelineStageFlags2::NONE)
|
||||||
|
.src_access_mask(vk::AccessFlags2::NONE)
|
||||||
|
.dst_stage_mask(vk::PipelineStageFlags2::ALL_TRANSFER)
|
||||||
|
.dst_access_mask(vk::AccessFlags2::TRANSFER_WRITE)
|
||||||
|
.old_layout(vk::ImageLayout::UNDEFINED)
|
||||||
|
.new_layout(vk::ImageLayout::GENERAL)
|
||||||
|
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||||
|
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||||
|
.image(pad)
|
||||||
|
.subresource_range(color_range(0));
|
||||||
|
dev.cmd_pipeline_barrier2(
|
||||||
|
compute_cmd,
|
||||||
|
&vk::DependencyInfo::default().image_memory_barriers(&[src_acq, pad_dst]),
|
||||||
|
);
|
||||||
|
let layers = vk::ImageSubresourceLayers::default()
|
||||||
|
.aspect_mask(vk::ImageAspectFlags::COLOR)
|
||||||
|
.layer_count(1);
|
||||||
|
let region = |sx: i32, sy: i32, dx: i32, dy: i32, cw: u32, ch: u32| {
|
||||||
|
vk::ImageCopy::default()
|
||||||
|
.src_subresource(layers)
|
||||||
|
.dst_subresource(layers)
|
||||||
|
.src_offset(vk::Offset3D { x: sx, y: sy, z: 0 })
|
||||||
|
.dst_offset(vk::Offset3D { x: dx, y: dy, z: 0 })
|
||||||
|
.extent(vk::Extent3D {
|
||||||
|
width: cw,
|
||||||
|
height: ch,
|
||||||
|
depth: 1,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
// Pass 1 — from the capture: the visible region, then each bottom padding row as a
|
||||||
|
// copy of the last visible row (1080p: 8 such rows). One call, disjoint regions.
|
||||||
|
let mut regions = vec![region(0, 0, 0, 0, rw, rh)];
|
||||||
|
for y in rh..h {
|
||||||
|
regions.push(region(0, rh as i32 - 1, 0, y as i32, rw, 1));
|
||||||
|
}
|
||||||
|
dev.cmd_copy_image(
|
||||||
|
compute_cmd,
|
||||||
|
src,
|
||||||
|
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
|
||||||
|
pad,
|
||||||
|
vk::ImageLayout::GENERAL,
|
||||||
|
®ions,
|
||||||
|
);
|
||||||
|
// Pass 2 — right padding columns (width alignment, e.g. 1366→1408): duplicate the
|
||||||
|
// staging image's own last visible column over the FULL aligned height (valid after
|
||||||
|
// pass 1 filled the bottom rows). Self-copy in GENERAL, W→R barrier between, regions
|
||||||
|
// disjoint from their source column.
|
||||||
|
if w > rw {
|
||||||
|
let self_dep = vk::ImageMemoryBarrier2::default()
|
||||||
|
.src_stage_mask(vk::PipelineStageFlags2::ALL_TRANSFER)
|
||||||
|
.src_access_mask(vk::AccessFlags2::TRANSFER_WRITE)
|
||||||
|
.dst_stage_mask(vk::PipelineStageFlags2::ALL_TRANSFER)
|
||||||
|
.dst_access_mask(vk::AccessFlags2::TRANSFER_READ)
|
||||||
|
.old_layout(vk::ImageLayout::GENERAL)
|
||||||
|
.new_layout(vk::ImageLayout::GENERAL)
|
||||||
|
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||||
|
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||||
|
.image(pad)
|
||||||
|
.subresource_range(color_range(0));
|
||||||
|
dev.cmd_pipeline_barrier2(
|
||||||
|
compute_cmd,
|
||||||
|
&vk::DependencyInfo::default().image_memory_barriers(&[self_dep]),
|
||||||
|
);
|
||||||
|
let cols: Vec<vk::ImageCopy> = (rw..w)
|
||||||
|
.map(|x| region(rw as i32 - 1, 0, x as i32, 0, 1, h))
|
||||||
|
.collect();
|
||||||
|
dev.cmd_copy_image(
|
||||||
|
compute_cmd,
|
||||||
|
pad,
|
||||||
|
vk::ImageLayout::GENERAL,
|
||||||
|
pad,
|
||||||
|
vk::ImageLayout::GENERAL,
|
||||||
|
&cols,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
dev.end_command_buffer(compute_cmd)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// RGB-direct twin of [`record_submit`]'s steps 2–4 (step 1 and the bookkeeping tail are
|
/// RGB-direct twin of [`record_submit`]'s steps 2–4 (step 1 and the bookkeeping tail are
|
||||||
/// shared): resolve the RGB encode source — the imported capture dmabuf itself, or the CPU
|
/// shared): resolve the RGB encode source — the imported capture dmabuf itself, or the CPU
|
||||||
/// staging upload — record the encode, and submit. There is no compute CSC: the VCN EFC
|
/// staging upload — record the encode, and submit. There is no compute CSC: the VCN EFC
|
||||||
@@ -1766,8 +1970,9 @@ impl VulkanVideoEncoder {
|
|||||||
metadata-cursor captures)"
|
metadata-cursor captures)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let padded = self.rgb.as_ref().is_some_and(|r| r.padded);
|
||||||
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) => {
|
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
|
||||||
// imported buffer must cover the FULL coded extent, or the EFC reads past it
|
// imported buffer must cover the FULL coded extent, or the EFC reads past it
|
||||||
// (VM faults → VCN ring hang → GPU reset, the 2026-07-20 field report). A
|
// (VM faults → VCN ring hang → GPU reset, the 2026-07-20 field report). A
|
||||||
@@ -1791,9 +1996,33 @@ impl VulkanVideoEncoder {
|
|||||||
};
|
};
|
||||||
(img, view, acq, false)
|
(img, view, acq, false)
|
||||||
}
|
}
|
||||||
|
FramePayload::Dmabuf(d) => {
|
||||||
|
// Padded-copy mode (unaligned mode, e.g. 1080p): blit the visible frame into
|
||||||
|
// the per-slot ALIGNED staging image and duplicate the edge into the 64x16
|
||||||
|
// padding, all on the transfer path of the compute queue (no shader). The
|
||||||
|
// encode reads the staging image — never the capture buffer — so the EFC can
|
||||||
|
// never read past a producer allocation again.
|
||||||
|
if frame.width != self.render_w || frame.height != self.render_h {
|
||||||
|
bail!(
|
||||||
|
"vulkan-encode (rgb-direct/padded): frame {}x{} != mode {}x{} — \
|
||||||
|
refusing a mismatched blit source",
|
||||||
|
frame.width,
|
||||||
|
frame.height,
|
||||||
|
self.render_w,
|
||||||
|
self.render_h
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let (img, _view, fresh) = self.import_cached(d, frame.width, frame.height)?;
|
||||||
|
let pad_img = self.frames[slot].pad_img;
|
||||||
|
let pad_view = self.frames[slot].pad_view;
|
||||||
|
self.record_pad_blit(&dev, compute_cmd, img, fresh, pad_img)?;
|
||||||
|
// The staging image ends the blit in GENERAL with the csc_sem ordering the
|
||||||
|
// hand-off — exactly the CscGeneral acquire contract.
|
||||||
|
(pad_img, pad_view, SrcAcquire::CscGeneral, true)
|
||||||
|
}
|
||||||
FramePayload::Cpu(bytes) => {
|
FramePayload::Cpu(bytes) => {
|
||||||
let fmt = pixel_to_vk(frame.format).context("unsupported CPU pixel format")?;
|
let fmt = pixel_to_vk(frame.format).context("unsupported CPU pixel format")?;
|
||||||
let view = self.ensure_cpu_rgb(slot, fmt, bytes)?;
|
let view = self.ensure_cpu_rgb(slot, fmt, bytes, frame.width, frame.height)?;
|
||||||
let (img, ..) = self.frames[slot].cpu_img.expect("ensure_cpu_rgb built it");
|
let (img, ..) = self.frames[slot].cpu_img.expect("ensure_cpu_rgb built it");
|
||||||
let (stage, ..) = self.frames[slot]
|
let (stage, ..) = self.frames[slot]
|
||||||
.cpu_stage
|
.cpu_stage
|
||||||
@@ -2856,6 +3085,9 @@ impl Drop for VkTeardown {
|
|||||||
device.destroy_fence(f.fence, None);
|
device.destroy_fence(f.fence, None);
|
||||||
device.destroy_query_pool(f.query_pool, None);
|
device.destroy_query_pool(f.query_pool, None);
|
||||||
device.destroy_query_pool(f.ts_pool, None);
|
device.destroy_query_pool(f.ts_pool, None);
|
||||||
|
device.destroy_image_view(f.pad_view, None);
|
||||||
|
device.destroy_image(f.pad_img, None);
|
||||||
|
device.free_memory(f.pad_mem, None);
|
||||||
device.destroy_buffer(f.bs_buf, None);
|
device.destroy_buffer(f.bs_buf, None);
|
||||||
// bs_mem is persistently mapped (f.bs_ptr); vkFreeMemory implicitly unmaps.
|
// bs_mem is persistently mapped (f.bs_ptr); vkFreeMemory implicitly unmaps.
|
||||||
device.free_memory(f.bs_mem, None);
|
device.free_memory(f.bs_mem, None);
|
||||||
@@ -3159,10 +3391,27 @@ unsafe fn make_frame(
|
|||||||
sampler: vk::Sampler,
|
sampler: vk::Sampler,
|
||||||
with_ts: bool,
|
with_ts: bool,
|
||||||
csc: bool,
|
csc: bool,
|
||||||
|
rgb_pad: bool,
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// "no cursor uploaded yet" sentinel — a real serial may be 0 (see `prep_cursor`).
|
// "no cursor uploaded yet" sentinel — a real serial may be 0 (see `prep_cursor`).
|
||||||
f.cursor_serial = u64::MAX;
|
f.cursor_serial = u64::MAX;
|
||||||
|
// Padded-copy RGB staging (unaligned-mode RGB-direct): aligned BGRA encode-src filled by a
|
||||||
|
// transfer blit each frame — concurrent compute (copy) + encode (source read).
|
||||||
|
if rgb_pad {
|
||||||
|
(f.pad_img, f.pad_mem) = make_video_image(
|
||||||
|
device,
|
||||||
|
mem_props,
|
||||||
|
vk::Format::B8G8R8A8_UNORM,
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
1,
|
||||||
|
vk::ImageUsageFlags::VIDEO_ENCODE_SRC_KHR | vk::ImageUsageFlags::TRANSFER_DST,
|
||||||
|
profile_list,
|
||||||
|
fams,
|
||||||
|
)?;
|
||||||
|
f.pad_view = make_view(device, f.pad_img, vk::Format::B8G8R8A8_UNORM, 0)?;
|
||||||
|
}
|
||||||
// RGB-direct sessions never touch the CSC pipeline: no NV12 encode-src, no Y/UV scratch, no
|
// RGB-direct sessions never touch the CSC pipeline: no NV12 encode-src, no Y/UV scratch, no
|
||||||
// cursor overlay, no descriptor set — the encode source is the imported RGB itself (or the
|
// cursor overlay, no descriptor set — the encode source is the imported RGB itself (or the
|
||||||
// CPU staging image, built lazily). Their Frame keeps the null handles (teardown-safe).
|
// CPU staging image, built lazily). Their Frame keeps the null handles (teardown-safe).
|
||||||
|
|||||||
Reference in New Issue
Block a user