mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
A Vulkan implicit layer that captures frames from inside the workload's own process and encodes them on the GPU they were drawn on. Fourth and last of this batch, imported as a tree from `nestrilabs/nescapture` on the same terms. Filed under `apps/` rather than `crates/` despite building a cdylib. The rule here is what a thing *is*, not what it compiles to: this is a finished artefact that gets installed into an image beside its layer manifest, not a library another crate in this tree depends on. `crates/` is for the latter, and putting this there would make the distinction useless the first time someone looked. Wired to the workspace, `nesprotocol` by path. Its description named the transport component; that reads better as what it actually is — where the frames go — so it says that instead. Whole workspace builds and tests: 21 across four members. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
738 lines
24 KiB
Rust
738 lines
24 KiB
Rust
// ─────────────────────────────────────────────────────────────────────────────
|
|
// capture.rs — Frame capture helpers
|
|
//
|
|
// final_image is allocated with VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT
|
|
// so that after the GPU blit we can export an fd and import it into pixelforge's
|
|
// separate VkDevice for zero-copy hardware encoding via DmaBufImporter.
|
|
//
|
|
// After ensure_final_image allocates (or re-allocates) the image, we query
|
|
// its SubresourceLayout and cache the row stride in DeviceState::final_stride.
|
|
// The stride is needed by the encoder to correctly import the LINEAR image.
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
use crate::state::{CB_STATE, CaptureResources, DEVICE_STATE};
|
|
use ash::vk::{self, Handle};
|
|
use std::os::raw::c_int;
|
|
use std::sync::atomic::Ordering;
|
|
|
|
fn make_subresource_range() -> vk::ImageSubresourceRange {
|
|
vk::ImageSubresourceRange {
|
|
aspect_mask: vk::ImageAspectFlags::COLOR,
|
|
base_mip_level: 0,
|
|
level_count: 1,
|
|
base_array_layer: 0,
|
|
layer_count: 1,
|
|
}
|
|
}
|
|
fn make_subresource_layers() -> vk::ImageSubresourceLayers {
|
|
vk::ImageSubresourceLayers {
|
|
aspect_mask: vk::ImageAspectFlags::COLOR,
|
|
mip_level: 0,
|
|
base_array_layer: 0,
|
|
layer_count: 1,
|
|
}
|
|
}
|
|
|
|
macro_rules! image_barrier {
|
|
($src:expr, $dst:expr, $old:expr, $new:expr, $img:expr) => {
|
|
vk::ImageMemoryBarrier {
|
|
s_type: vk::StructureType::IMAGE_MEMORY_BARRIER,
|
|
p_next: std::ptr::null(),
|
|
src_access_mask: $src,
|
|
dst_access_mask: $dst,
|
|
old_layout: $old,
|
|
new_layout: $new,
|
|
src_queue_family_index: vk::QUEUE_FAMILY_IGNORED,
|
|
dst_queue_family_index: vk::QUEUE_FAMILY_IGNORED,
|
|
image: $img,
|
|
subresource_range: make_subresource_range(),
|
|
_marker: std::marker::PhantomData,
|
|
}
|
|
};
|
|
}
|
|
|
|
// ── Memory helper ─────────────────────────────────────────────────────────────
|
|
|
|
unsafe fn find_host_coherent_mt(ds: &crate::state::DeviceState, bits: u32) -> u32 {
|
|
let mut mp = vk::PhysicalDeviceMemoryProperties::default();
|
|
let k = unsafe { crate::dispatch_key(ds.physical_device.as_raw() as *const std::ffi::c_void) };
|
|
if let Some(i) = crate::state::INSTANCE_STATE.get(&k) {
|
|
unsafe { (i.get_physical_device_memory_properties)(ds.physical_device, &mut mp) };
|
|
}
|
|
(0..mp.memory_type_count)
|
|
.find(|&i| {
|
|
(bits & (1 << i)) != 0
|
|
&& mp.memory_types[i as usize].property_flags.contains(
|
|
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
|
|
)
|
|
})
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
// ── Image allocators ──────────────────────────────────────────────────────────
|
|
|
|
/// Plain HOST_VISIBLE image (nescapture capture — no cross-device sharing needed).
|
|
unsafe fn allocate_host_image(
|
|
ds: &crate::state::DeviceState,
|
|
w: u32,
|
|
h: u32,
|
|
fmt: vk::Format,
|
|
label: &str,
|
|
) -> Option<(vk::Image, vk::DeviceMemory)> {
|
|
let ci = vk::ImageCreateInfo {
|
|
s_type: vk::StructureType::IMAGE_CREATE_INFO,
|
|
p_next: std::ptr::null(),
|
|
flags: vk::ImageCreateFlags::empty(),
|
|
image_type: vk::ImageType::TYPE_2D,
|
|
format: fmt,
|
|
extent: vk::Extent3D {
|
|
width: w,
|
|
height: h,
|
|
depth: 1,
|
|
},
|
|
mip_levels: 1,
|
|
array_layers: 1,
|
|
samples: vk::SampleCountFlags::TYPE_1,
|
|
tiling: vk::ImageTiling::LINEAR,
|
|
usage: vk::ImageUsageFlags::TRANSFER_DST,
|
|
sharing_mode: vk::SharingMode::EXCLUSIVE,
|
|
queue_family_index_count: 0,
|
|
p_queue_family_indices: std::ptr::null(),
|
|
initial_layout: vk::ImageLayout::UNDEFINED,
|
|
_marker: std::marker::PhantomData,
|
|
};
|
|
unsafe { alloc_image(ds, &ci, None, label) }
|
|
}
|
|
|
|
/// DMA-BUF exportable image (final capture — imported into pixelforge for encoding).
|
|
///
|
|
/// Falls back to a plain host image if the driver rejects external memory.
|
|
/// In that case `get_dmabuf_fd` will return `None` and the encoder will use
|
|
/// the CPU pixel-readback fallback.
|
|
unsafe fn allocate_dmabuf_image(
|
|
ds: &crate::state::DeviceState,
|
|
w: u32,
|
|
h: u32,
|
|
fmt: vk::Format,
|
|
label: &str,
|
|
) -> Option<(vk::Image, vk::DeviceMemory)> {
|
|
let ext_img = vk::ExternalMemoryImageCreateInfo {
|
|
s_type: vk::StructureType::EXTERNAL_MEMORY_IMAGE_CREATE_INFO,
|
|
p_next: std::ptr::null_mut(),
|
|
handle_types: vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT,
|
|
_marker: std::marker::PhantomData,
|
|
};
|
|
let ci = vk::ImageCreateInfo {
|
|
s_type: vk::StructureType::IMAGE_CREATE_INFO,
|
|
p_next: &ext_img as *const _ as *const _,
|
|
flags: vk::ImageCreateFlags::empty(),
|
|
image_type: vk::ImageType::TYPE_2D,
|
|
format: fmt,
|
|
extent: vk::Extent3D {
|
|
width: w,
|
|
height: h,
|
|
depth: 1,
|
|
},
|
|
mip_levels: 1,
|
|
array_layers: 1,
|
|
samples: vk::SampleCountFlags::TYPE_1,
|
|
tiling: vk::ImageTiling::LINEAR,
|
|
usage: vk::ImageUsageFlags::TRANSFER_DST,
|
|
sharing_mode: vk::SharingMode::EXCLUSIVE,
|
|
queue_family_index_count: 0,
|
|
p_queue_family_indices: std::ptr::null(),
|
|
initial_layout: vk::ImageLayout::UNDEFINED,
|
|
_marker: std::marker::PhantomData,
|
|
};
|
|
let export_ai = vk::ExportMemoryAllocateInfo {
|
|
s_type: vk::StructureType::EXPORT_MEMORY_ALLOCATE_INFO,
|
|
p_next: std::ptr::null_mut(),
|
|
handle_types: vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT,
|
|
_marker: std::marker::PhantomData,
|
|
};
|
|
if let Some(r) = unsafe { alloc_image(ds, &ci, Some(&export_ai), label) } {
|
|
return Some(r);
|
|
}
|
|
log::warn!(
|
|
"DMA-BUF alloc failed for '{}' — using plain host image. \
|
|
Zero-copy GPU path will be unavailable; CPU readback fallback active.",
|
|
label
|
|
);
|
|
unsafe { allocate_host_image(ds, w, h, fmt, label) }
|
|
}
|
|
|
|
unsafe fn alloc_image(
|
|
ds: &crate::state::DeviceState,
|
|
ci: &vk::ImageCreateInfo,
|
|
export: Option<&vk::ExportMemoryAllocateInfo>,
|
|
label: &str,
|
|
) -> Option<(vk::Image, vk::DeviceMemory)> {
|
|
let mut image = vk::Image::null();
|
|
if unsafe { (ds.fp.create_image)(ds.raw, ci, std::ptr::null(), &mut image) }
|
|
!= vk::Result::SUCCESS
|
|
{
|
|
return None;
|
|
}
|
|
let mut mr = vk::MemoryRequirements {
|
|
size: 0,
|
|
alignment: 0,
|
|
memory_type_bits: 0,
|
|
};
|
|
unsafe { (ds.fp.get_image_memory_requirements)(ds.raw, image, &mut mr) };
|
|
let mt = unsafe { find_host_coherent_mt(ds, mr.memory_type_bits) };
|
|
let p_next: *const _ = match export {
|
|
Some(e) => e as *const _ as *const _,
|
|
None => std::ptr::null(),
|
|
};
|
|
let ai = vk::MemoryAllocateInfo {
|
|
s_type: vk::StructureType::MEMORY_ALLOCATE_INFO,
|
|
p_next,
|
|
allocation_size: mr.size,
|
|
memory_type_index: mt,
|
|
_marker: std::marker::PhantomData,
|
|
};
|
|
let mut mem = vk::DeviceMemory::null();
|
|
if unsafe { (ds.fp.allocate_memory)(ds.raw, &ai, std::ptr::null(), &mut mem) }
|
|
!= vk::Result::SUCCESS
|
|
{
|
|
unsafe { (ds.fp.destroy_image)(ds.raw, image, std::ptr::null()) };
|
|
return None;
|
|
}
|
|
if unsafe { (ds.fp.bind_image_memory)(ds.raw, image, mem, 0) } != vk::Result::SUCCESS {
|
|
unsafe { (ds.fp.free_memory)(ds.raw, mem, std::ptr::null()) };
|
|
unsafe { (ds.fp.destroy_image)(ds.raw, image, std::ptr::null()) };
|
|
return None;
|
|
}
|
|
log::info!(
|
|
"alloc {} {}x{} fmt={} ({} bytes)",
|
|
label,
|
|
ci.extent.width,
|
|
ci.extent.height,
|
|
ci.format.as_raw(),
|
|
mr.size
|
|
);
|
|
Some((image, mem))
|
|
}
|
|
|
|
// ── Stride query ──────────────────────────────────────────────────────────────
|
|
|
|
/// Query and cache the row stride of final_image.
|
|
/// Returns stride in bytes; 0 on failure.
|
|
pub unsafe fn query_and_cache_final_stride(
|
|
ds: &crate::state::DeviceState,
|
|
image: vk::Image,
|
|
) -> u32 {
|
|
let subresource = vk::ImageSubresource {
|
|
aspect_mask: vk::ImageAspectFlags::COLOR,
|
|
mip_level: 0,
|
|
array_layer: 0,
|
|
};
|
|
let mut layout = vk::SubresourceLayout {
|
|
offset: 0,
|
|
size: 0,
|
|
row_pitch: 0,
|
|
array_pitch: 0,
|
|
depth_pitch: 0,
|
|
};
|
|
unsafe { (ds.fp.get_image_subresource_layout)(ds.raw, image, &subresource, &mut layout) };
|
|
let stride = layout.row_pitch as u32;
|
|
ds.final_stride.store(stride, Ordering::Relaxed);
|
|
stride
|
|
}
|
|
|
|
// ── DMA-BUF fd export ─────────────────────────────────────────────────────────
|
|
|
|
/// Export `memory` as a DMA-BUF fd via vkGetMemoryFdKHR.
|
|
/// Callers own the fd and must close it when done.
|
|
/// Returns `None` if VK_KHR_external_memory_fd is unavailable.
|
|
pub unsafe fn get_dmabuf_fd(
|
|
ds: &crate::state::DeviceState,
|
|
memory: vk::DeviceMemory,
|
|
) -> Option<c_int> {
|
|
let f = match ds.fp.get_memory_fd_khr {
|
|
Some(f) => f,
|
|
None => {
|
|
log::warn!("get_dmabuf_fd: vkGetMemoryFdKHR not available");
|
|
return None;
|
|
}
|
|
};
|
|
let fi = vk::MemoryGetFdInfoKHR {
|
|
s_type: vk::StructureType::MEMORY_GET_FD_INFO_KHR,
|
|
p_next: std::ptr::null(),
|
|
memory,
|
|
handle_type: vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT,
|
|
_marker: std::marker::PhantomData,
|
|
};
|
|
let mut fd: c_int = -1;
|
|
let result = unsafe { f(ds.raw, &fi, &mut fd) };
|
|
if result == vk::Result::SUCCESS && fd >= 0 {
|
|
Some(fd)
|
|
} else {
|
|
log::warn!("get_dmabuf_fd failed: result={:?} fd={}", result, fd);
|
|
None
|
|
}
|
|
}
|
|
|
|
// ── ensure helpers ────────────────────────────────────────────────────────────
|
|
|
|
pub unsafe fn ensure_hudless_image(ds: &crate::state::DeviceState, w: u32, h: u32, f: vk::Format) {
|
|
let mut ig = ds.hudless_image.lock().unwrap();
|
|
let mut mg = ds.hudless_memory.lock().unwrap();
|
|
let mut sg = ds.hudless_size.lock().unwrap();
|
|
if let (Some(i), Some(m)) = (*ig, *mg) {
|
|
let (ew, eh, ef) = *sg;
|
|
if ew >= w && eh >= h && ef == f {
|
|
return;
|
|
}
|
|
unsafe { (ds.fp.destroy_image)(ds.raw, i, std::ptr::null()) };
|
|
unsafe { (ds.fp.free_memory)(ds.raw, m, std::ptr::null()) };
|
|
*ig = None;
|
|
*mg = None;
|
|
}
|
|
if let Some((i, m)) = unsafe { allocate_host_image(ds, w, h, f, "nescapture") } {
|
|
*ig = Some(i);
|
|
*mg = Some(m);
|
|
*sg = (w, h, f);
|
|
}
|
|
}
|
|
|
|
pub unsafe fn ensure_final_image(ds: &crate::state::DeviceState, w: u32, h: u32, f: vk::Format) {
|
|
let mut ig = ds.final_image.lock().unwrap();
|
|
let mut mg = ds.final_memory.lock().unwrap();
|
|
let mut sg = ds.final_size.lock().unwrap();
|
|
if let (Some(i), Some(m)) = (*ig, *mg) {
|
|
let (ew, eh, ef) = *sg;
|
|
if ew >= w && eh >= h && ef == f {
|
|
return;
|
|
}
|
|
unsafe { (ds.fp.destroy_image)(ds.raw, i, std::ptr::null()) };
|
|
unsafe { (ds.fp.free_memory)(ds.raw, m, std::ptr::null()) };
|
|
*ig = None;
|
|
*mg = None;
|
|
ds.final_stride.store(0, Ordering::Relaxed);
|
|
}
|
|
if let Some((i, m)) = unsafe { allocate_dmabuf_image(ds, w, h, f, "final") } {
|
|
// Query stride immediately after allocation so it's available on first frame.
|
|
unsafe { query_and_cache_final_stride(ds, i) };
|
|
*ig = Some(i);
|
|
*mg = Some(m);
|
|
*sg = (w, h, f);
|
|
}
|
|
}
|
|
|
|
// ── HUDless command injection ─────────────────────────────────────────────────
|
|
|
|
pub unsafe fn inject_hudless_copy(cb: vk::CommandBuffer, dk: usize) {
|
|
let ds = match DEVICE_STATE.get(&dk) {
|
|
Some(s) => s.clone(),
|
|
None => return,
|
|
};
|
|
let cbk = cb.as_raw();
|
|
let cs = match CB_STATE.get(&cbk) {
|
|
Some(e) => e.value().clone(),
|
|
None => return,
|
|
};
|
|
let ci = match cs.current_color_image {
|
|
Some(i) => i,
|
|
None => return,
|
|
};
|
|
let fmt = match cs.current_image_format {
|
|
Some(f) => f,
|
|
None => return,
|
|
};
|
|
let ext = match cs.current_image_extent {
|
|
Some(e) => e,
|
|
None => return,
|
|
};
|
|
let sc = *ds.swapchain_extent.lock().unwrap();
|
|
if sc.width > 0 && sc.height > 0 && (ext.width != sc.width || ext.height != sc.height) {
|
|
return;
|
|
}
|
|
unsafe { ensure_hudless_image(&ds, ext.width, ext.height, fmt) };
|
|
let (hi, _) = {
|
|
let a = ds.hudless_image.lock().unwrap();
|
|
let b = ds.hudless_memory.lock().unwrap();
|
|
match (*a, *b) {
|
|
(Some(i), Some(m)) => (i, m),
|
|
_ => return,
|
|
}
|
|
};
|
|
// src → TRANSFER_SRC
|
|
let b1 = image_barrier!(
|
|
vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
|
|
vk::AccessFlags::TRANSFER_READ,
|
|
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
|
|
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
|
|
ci
|
|
);
|
|
unsafe {
|
|
(ds.fp.cmd_pipeline_barrier)(
|
|
cb,
|
|
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
|
|
vk::PipelineStageFlags::TRANSFER,
|
|
vk::DependencyFlags::empty(),
|
|
0,
|
|
std::ptr::null(),
|
|
0,
|
|
std::ptr::null(),
|
|
1,
|
|
&b1,
|
|
);
|
|
}
|
|
// dst → TRANSFER_DST
|
|
let b2 = image_barrier!(
|
|
vk::AccessFlags::empty(),
|
|
vk::AccessFlags::TRANSFER_WRITE,
|
|
vk::ImageLayout::UNDEFINED,
|
|
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
|
|
hi
|
|
);
|
|
unsafe {
|
|
(ds.fp.cmd_pipeline_barrier)(
|
|
cb,
|
|
vk::PipelineStageFlags::TOP_OF_PIPE,
|
|
vk::PipelineStageFlags::TRANSFER,
|
|
vk::DependencyFlags::empty(),
|
|
0,
|
|
std::ptr::null(),
|
|
0,
|
|
std::ptr::null(),
|
|
1,
|
|
&b2,
|
|
);
|
|
}
|
|
let cr = vk::ImageCopy {
|
|
src_subresource: make_subresource_layers(),
|
|
src_offset: vk::Offset3D { x: 0, y: 0, z: 0 },
|
|
dst_subresource: make_subresource_layers(),
|
|
dst_offset: vk::Offset3D { x: 0, y: 0, z: 0 },
|
|
extent: vk::Extent3D {
|
|
width: ext.width,
|
|
height: ext.height,
|
|
depth: 1,
|
|
},
|
|
};
|
|
unsafe {
|
|
(ds.fp.cmd_copy_image)(
|
|
cb,
|
|
ci,
|
|
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
|
|
hi,
|
|
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
|
|
1,
|
|
&cr,
|
|
);
|
|
}
|
|
// restore src
|
|
let b3 = image_barrier!(
|
|
vk::AccessFlags::TRANSFER_READ,
|
|
vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
|
|
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
|
|
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
|
|
ci
|
|
);
|
|
unsafe {
|
|
(ds.fp.cmd_pipeline_barrier)(
|
|
cb,
|
|
vk::PipelineStageFlags::TRANSFER,
|
|
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
|
|
vk::DependencyFlags::empty(),
|
|
0,
|
|
std::ptr::null(),
|
|
0,
|
|
std::ptr::null(),
|
|
1,
|
|
&b3,
|
|
);
|
|
}
|
|
if let Some(mut s) = CB_STATE.get_mut(&cb.as_raw()) {
|
|
s.pending_capture = false;
|
|
s.capture_injected = true;
|
|
}
|
|
}
|
|
|
|
// ── Final frame GPU blit (swapchain → final_image) ────────────────────────────
|
|
|
|
pub unsafe fn capture_final_frame(
|
|
ds: &crate::state::DeviceState,
|
|
queue: vk::Queue,
|
|
si: vk::Image,
|
|
fmt: vk::Format,
|
|
ext: vk::Extent2D,
|
|
_frame: u64,
|
|
) {
|
|
if ext.width == 0 || ext.height == 0 {
|
|
return;
|
|
}
|
|
unsafe { ensure_final_image(ds, ext.width, ext.height, fmt) };
|
|
let fi = match *ds.final_image.lock().unwrap() {
|
|
Some(i) => i,
|
|
None => return,
|
|
};
|
|
|
|
// ── Lazy-init reusable capture resources ──────────────────────
|
|
let mut res_guard = ds.capture_resources.lock().unwrap();
|
|
let res = match res_guard.as_mut() {
|
|
Some(r) => r,
|
|
None => match unsafe { create_capture_resources(ds) } {
|
|
Some(r) => {
|
|
*res_guard = Some(r);
|
|
res_guard.as_mut().unwrap()
|
|
}
|
|
None => return,
|
|
},
|
|
};
|
|
|
|
let idx = res.current;
|
|
let cb = res.command_buffers[idx];
|
|
let fence = res.fences[idx];
|
|
|
|
// Wait for THIS slot's previous use to finish (not the other slot).
|
|
// Use a short timeout — if the GPU is busy with game rendering, skip
|
|
// this capture instead of stalling the game's render loop.
|
|
unsafe {
|
|
let result = (ds.fp.wait_for_fences)(ds.raw, 1, &fence, vk::TRUE, 1_000_000); // 1ms timeout
|
|
if result != vk::Result::SUCCESS {
|
|
// GPU not ready — skip this capture, try next slot
|
|
res.current = (idx + 1) % 4;
|
|
return;
|
|
}
|
|
let _ = (ds.fp.reset_fences)(ds.raw, 1, &fence);
|
|
}
|
|
|
|
// Reset and re-record
|
|
unsafe {
|
|
let _ = (ds.fp.reset_command_buffer)(cb, vk::CommandBufferResetFlags::empty());
|
|
}
|
|
|
|
let bi = vk::CommandBufferBeginInfo {
|
|
s_type: vk::StructureType::COMMAND_BUFFER_BEGIN_INFO,
|
|
p_next: std::ptr::null(),
|
|
flags: vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT,
|
|
p_inheritance_info: std::ptr::null(),
|
|
_marker: std::marker::PhantomData,
|
|
};
|
|
if unsafe { (ds.fp.begin_command_buffer)(cb, &bi) } != vk::Result::SUCCESS {
|
|
return;
|
|
}
|
|
|
|
let b1 = image_barrier!(
|
|
vk::AccessFlags::MEMORY_READ,
|
|
vk::AccessFlags::TRANSFER_READ,
|
|
vk::ImageLayout::PRESENT_SRC_KHR,
|
|
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
|
|
si
|
|
);
|
|
unsafe {
|
|
(ds.fp.cmd_pipeline_barrier)(
|
|
cb,
|
|
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
|
|
vk::PipelineStageFlags::TRANSFER,
|
|
vk::DependencyFlags::empty(),
|
|
0,
|
|
std::ptr::null(),
|
|
0,
|
|
std::ptr::null(),
|
|
1,
|
|
&b1,
|
|
);
|
|
}
|
|
let b2 = image_barrier!(
|
|
vk::AccessFlags::empty(),
|
|
vk::AccessFlags::TRANSFER_WRITE,
|
|
vk::ImageLayout::UNDEFINED,
|
|
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
|
|
fi
|
|
);
|
|
unsafe {
|
|
(ds.fp.cmd_pipeline_barrier)(
|
|
cb,
|
|
vk::PipelineStageFlags::TOP_OF_PIPE,
|
|
vk::PipelineStageFlags::TRANSFER,
|
|
vk::DependencyFlags::empty(),
|
|
0,
|
|
std::ptr::null(),
|
|
0,
|
|
std::ptr::null(),
|
|
1,
|
|
&b2,
|
|
);
|
|
}
|
|
let cr = vk::ImageCopy {
|
|
src_subresource: make_subresource_layers(),
|
|
src_offset: vk::Offset3D { x: 0, y: 0, z: 0 },
|
|
dst_subresource: make_subresource_layers(),
|
|
dst_offset: vk::Offset3D { x: 0, y: 0, z: 0 },
|
|
extent: vk::Extent3D {
|
|
width: ext.width,
|
|
height: ext.height,
|
|
depth: 1,
|
|
},
|
|
};
|
|
unsafe {
|
|
(ds.fp.cmd_copy_image)(
|
|
cb,
|
|
si,
|
|
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
|
|
fi,
|
|
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
|
|
1,
|
|
&cr,
|
|
);
|
|
}
|
|
let b3 = image_barrier!(
|
|
vk::AccessFlags::TRANSFER_READ,
|
|
vk::AccessFlags::MEMORY_READ,
|
|
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
|
|
vk::ImageLayout::PRESENT_SRC_KHR,
|
|
si
|
|
);
|
|
unsafe {
|
|
(ds.fp.cmd_pipeline_barrier)(
|
|
cb,
|
|
vk::PipelineStageFlags::TRANSFER,
|
|
vk::PipelineStageFlags::TOP_OF_PIPE,
|
|
vk::DependencyFlags::empty(),
|
|
0,
|
|
std::ptr::null(),
|
|
0,
|
|
std::ptr::null(),
|
|
1,
|
|
&b3,
|
|
);
|
|
}
|
|
|
|
if unsafe { (ds.fp.end_command_buffer)(cb) } != vk::Result::SUCCESS {
|
|
return;
|
|
}
|
|
|
|
let subi = vk::SubmitInfo {
|
|
s_type: vk::StructureType::SUBMIT_INFO,
|
|
p_next: std::ptr::null(),
|
|
wait_semaphore_count: 0,
|
|
p_wait_semaphores: std::ptr::null(),
|
|
p_wait_dst_stage_mask: std::ptr::null(),
|
|
command_buffer_count: 1,
|
|
p_command_buffers: &cb,
|
|
signal_semaphore_count: 0,
|
|
p_signal_semaphores: std::ptr::null(),
|
|
_marker: std::marker::PhantomData,
|
|
};
|
|
|
|
unsafe {
|
|
if (ds.fp.queue_submit)(queue, 1, &subi, fence) != vk::Result::SUCCESS {
|
|
log::warn!("capture queue_submit failed — frame skipped");
|
|
let _ = (ds.fp.reset_fences)(ds.raw, 1, &fence);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Toggle to the next slot
|
|
res.current = (idx + 1) % 4;
|
|
}
|
|
|
|
unsafe fn create_capture_resources(ds: &crate::state::DeviceState) -> Option<CaptureResources> {
|
|
let pci = vk::CommandPoolCreateInfo {
|
|
s_type: vk::StructureType::COMMAND_POOL_CREATE_INFO,
|
|
p_next: std::ptr::null(),
|
|
flags: vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER, // allow per-cb reset
|
|
queue_family_index: 0,
|
|
_marker: std::marker::PhantomData,
|
|
};
|
|
let mut cp = vk::CommandPool::null();
|
|
if unsafe { (ds.fp.create_command_pool)(ds.raw, &pci, std::ptr::null(), &mut cp) }
|
|
!= vk::Result::SUCCESS
|
|
{
|
|
return None;
|
|
}
|
|
|
|
let ai = vk::CommandBufferAllocateInfo {
|
|
s_type: vk::StructureType::COMMAND_BUFFER_ALLOCATE_INFO,
|
|
p_next: std::ptr::null(),
|
|
command_pool: cp,
|
|
level: vk::CommandBufferLevel::PRIMARY,
|
|
command_buffer_count: 4,
|
|
_marker: std::marker::PhantomData,
|
|
};
|
|
let mut cbs = [vk::CommandBuffer::null(); 4];
|
|
if unsafe { (ds.fp.allocate_command_buffers)(ds.raw, &ai, cbs.as_mut_ptr()) }
|
|
!= vk::Result::SUCCESS
|
|
{
|
|
unsafe { (ds.fp.destroy_command_pool)(ds.raw, cp, std::ptr::null()) };
|
|
return None;
|
|
}
|
|
|
|
// Create fences PRE-SIGNALED so the first wait_for_fences returns immediately
|
|
let fci = vk::FenceCreateInfo {
|
|
s_type: vk::StructureType::FENCE_CREATE_INFO,
|
|
p_next: std::ptr::null(),
|
|
flags: vk::FenceCreateFlags::SIGNALED,
|
|
_marker: std::marker::PhantomData,
|
|
};
|
|
let mut fences = [vk::Fence::null(); 4];
|
|
for f in &mut fences {
|
|
if unsafe { (ds.fp.create_fence)(ds.raw, &fci, std::ptr::null(), f) } != vk::Result::SUCCESS
|
|
{
|
|
unsafe { (ds.fp.destroy_command_pool)(ds.raw, cp, std::ptr::null()) };
|
|
return None;
|
|
}
|
|
}
|
|
|
|
Some(CaptureResources {
|
|
command_pool: cp,
|
|
command_buffers: cbs,
|
|
fences,
|
|
current: 0,
|
|
})
|
|
}
|
|
|
|
// ── CPU pixel readback (fallback when DMA-BUF unavailable) ───────────────────
|
|
|
|
pub unsafe fn read_frame_pixels(
|
|
ds: &crate::state::DeviceState,
|
|
image: vk::Image,
|
|
mem: vk::DeviceMemory,
|
|
w: u32,
|
|
h: u32,
|
|
) -> Option<Vec<u8>> {
|
|
if w == 0 || h == 0 {
|
|
return None;
|
|
}
|
|
let subresource = vk::ImageSubresource {
|
|
aspect_mask: vk::ImageAspectFlags::COLOR,
|
|
mip_level: 0,
|
|
array_layer: 0,
|
|
};
|
|
let mut layout = vk::SubresourceLayout {
|
|
offset: 0,
|
|
size: 0,
|
|
row_pitch: 0,
|
|
array_pitch: 0,
|
|
depth_pitch: 0,
|
|
};
|
|
unsafe { (ds.fp.get_image_subresource_layout)(ds.raw, image, &subresource, &mut layout) };
|
|
let row_pitch = layout.row_pitch as usize;
|
|
let bpr = w as usize * 4;
|
|
let mut mp: *mut std::os::raw::c_void = std::ptr::null_mut();
|
|
if unsafe {
|
|
(ds.fp.map_memory)(
|
|
ds.raw,
|
|
mem,
|
|
0,
|
|
vk::WHOLE_SIZE,
|
|
vk::MemoryMapFlags::empty(),
|
|
&mut mp,
|
|
) != vk::Result::SUCCESS
|
|
} {
|
|
return None;
|
|
}
|
|
let mut pixels = vec![0u8; bpr * h as usize];
|
|
let base = mp as *const u8;
|
|
for row in 0..h as usize {
|
|
let src = unsafe { std::slice::from_raw_parts(base.add(row * row_pitch), bpr) };
|
|
pixels[row * bpr..row * bpr + bpr].copy_from_slice(src);
|
|
}
|
|
unsafe { (ds.fp.unmap_memory)(ds.raw, mem) };
|
|
Some(pixels)
|
|
}
|