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

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:
enricobuehler
2026-07-20 20:35:51 +02:00
parent 3aacec53d8
commit 96e19986bc
+274 -25
View File
@@ -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
@@ -265,7 +271,12 @@ struct Frame {
ts_pool: vk::QueryPool, ts_pool: vk::QueryPool,
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,
&regions,
);
// 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 24 (step 1 and the bookkeeping tail are /// RGB-direct twin of [`record_submit`]'s steps 24 (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).