Files
netris-nestri/apps/nescapture/src/device.rs
Wanjohi 6164e0c636 feat(nescapture): open the capture layer
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>
2026-08-26 18:04:02 +03:00

529 lines
20 KiB
Rust

// ─────────────────────────────────────────────────────────────────────────────
// device.rs — vkCreateDevice, vkDestroyDevice, vkGetDeviceQueue
// ─────────────────────────────────────────────────────────────────────────────
use crate::config;
use crate::dispatch::{NextDeviceFn, PFN_vkCreateDevice, PFN_vkGetInstanceProcAddr};
use crate::state::{DEVICE_STATE, DeviceState, INSTANCE_STATE, QUEUE_TO_DEVICE_KEY};
use crate::{
VkLayerDeviceCreateInfo, dispatch_key, find_layer_link, load_device_fn, try_load_device_fn,
};
use ash::vk::{self, Handle};
use dashmap::DashMap;
use std::os::raw::c_void;
use std::sync::Arc;
use std::sync::atomic::Ordering;
const VK_LAYER_LINK_INFO: u32 = 0;
#[unsafe(no_mangle)]
pub unsafe extern "system" fn vkCreateDevice(
physical_device: vk::PhysicalDevice,
p_create_info: *const vk::DeviceCreateInfo,
p_allocator: *const vk::AllocationCallbacks,
p_device: *mut vk::Device,
) -> vk::Result {
let layer_info: *mut VkLayerDeviceCreateInfo = match unsafe {
find_layer_link((*p_create_info).p_next as *const c_void, VK_LAYER_LINK_INFO)
} {
Some(p) => p,
None => return vk::Result::ERROR_INITIALIZATION_FAILED,
};
let dev_link = unsafe { (*layer_info).u.pDeviceLayerInfo };
let next_gipa: PFN_vkGetInstanceProcAddr =
match unsafe { (*dev_link).pfnNextGetInstanceProcAddr } {
Some(f) => f,
None => return vk::Result::ERROR_INITIALIZATION_FAILED,
};
let next_gdpa = unsafe {
match (*dev_link).pfnNextGetDeviceProcAddr {
Some(f) => f,
None => return vk::Result::ERROR_INITIALIZATION_FAILED,
}
};
unsafe { (*layer_info).u.pDeviceLayerInfo = (*dev_link).pNext };
let inst_key = unsafe { dispatch_key(physical_device.as_raw() as *const c_void) };
let istate = match INSTANCE_STATE.get(&inst_key) {
Some(s) => s.clone(),
None => {
let next_create: PFN_vkCreateDevice = unsafe {
crate::load_instance_fn(next_gipa, vk::Instance::null(), b"vkCreateDevice\0")
};
return unsafe { next_create(physical_device, p_create_info, p_allocator, p_device) };
}
};
// ── Inject DMA-BUF extensions for zero-copy capture ──────────────────
let ci = unsafe { &*p_create_info };
// Collect the game's original extensions.
let original_extensions: Vec<*const libc::c_char> =
if ci.enabled_extension_count > 0 && !ci.pp_enabled_extension_names.is_null() {
unsafe {
std::slice::from_raw_parts(
ci.pp_enabled_extension_names,
ci.enabled_extension_count as usize,
)
}
.to_vec()
} else {
Vec::new()
};
// Extensions we need — static byte strings so pointers stay valid.
const EXT_EXTERNAL_MEMORY: &[u8] = b"VK_KHR_external_memory\0";
const EXT_EXTERNAL_MEMORY_FD: &[u8] = b"VK_KHR_external_memory_fd\0";
const EXT_EXTERNAL_MEMORY_DMABUF: &[u8] = b"VK_EXT_external_memory_dma_buf\0";
let needed: &[&[u8]] = &[
EXT_EXTERNAL_MEMORY,
EXT_EXTERNAL_MEMORY_FD,
EXT_EXTERNAL_MEMORY_DMABUF,
];
// Build extended list: original + any of ours not already present.
let mut extended = original_extensions.clone();
for &ext in needed {
let name_cstr = unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(ext) };
let already = extended
.iter()
.any(|&ptr| unsafe { std::ffi::CStr::from_ptr(ptr) == name_cstr });
if !already {
extended.push(ext.as_ptr() as *const libc::c_char);
}
}
// ── Bump queue count for dedicated capture queue ──────────────────
// Add one extra queue to the first queue family so capture
// submissions don't compete with game rendering.
let mut capture_queue_index = 0u32;
let queue_infos: Vec<vk::DeviceQueueCreateInfo> = if ci.queue_create_info_count > 0
&& !ci.p_queue_create_infos.is_null()
{
let slice = unsafe {
std::slice::from_raw_parts(ci.p_queue_create_infos, ci.queue_create_info_count as usize)
};
let mut qis = slice.to_vec();
if let Some(first) = qis.first_mut() {
capture_queue_index = first.queue_count; // use the NEXT index
first.queue_count += 1;
}
qis
} else {
Vec::new()
};
// Try with injected extensions first.
let mut modified_ci = *ci;
modified_ci.enabled_extension_count = extended.len() as u32;
modified_ci.pp_enabled_extension_names = extended.as_ptr();
modified_ci.queue_create_info_count = queue_infos.len() as u32;
modified_ci.p_queue_create_infos = queue_infos.as_ptr();
let mut dmabuf_available = true;
let result =
unsafe { (istate.create_device)(physical_device, &modified_ci, p_allocator, p_device) };
let result = if result != vk::Result::SUCCESS {
// Driver rejected our extensions — retry with original create info.
log::warn!(
"vkCreateDevice with DMA-BUF extensions failed ({:?}), \
retrying without — CPU readback fallback will be used",
result
);
dmabuf_available = false;
unsafe { (istate.create_device)(physical_device, p_create_info, p_allocator, p_device) }
} else {
log::info!("DMA-BUF extensions injected successfully");
result
};
if result != vk::Result::SUCCESS {
return result;
}
if !dmabuf_available {
log::warn!("DMA-BUF extensions missing — will use CPU readback fallback (expensive!)");
}
let device = unsafe { *p_device };
// Cache physical device memory properties
let mut mem_props = vk::PhysicalDeviceMemoryProperties::default();
unsafe { (istate.get_physical_device_memory_properties)(physical_device, &mut mem_props) };
macro_rules! load {
($name:literal) => {
unsafe { load_device_fn(next_gdpa, device, $name) }
};
}
macro_rules! try_load {
($name:literal) => {
unsafe { try_load_device_fn(next_gdpa, device, $name) }
};
}
let fp = NextDeviceFn {
// Infrastructure
get_device_proc_addr: next_gdpa,
destroy_device: load!(b"vkDestroyDevice\0"),
get_device_queue: load!(b"vkGetDeviceQueue\0"),
queue_present_khr: try_load!(b"vkQueuePresentKHR\0"),
// Phase 1
create_shader_module: load!(b"vkCreateShaderModule\0"),
destroy_shader_module: load!(b"vkDestroyShaderModule\0"),
create_graphics_pipelines: load!(b"vkCreateGraphicsPipelines\0"),
destroy_pipeline: load!(b"vkDestroyPipeline\0"),
// Phase 2
create_image_view: load!(b"vkCreateImageView\0"),
destroy_image_view: load!(b"vkDestroyImageView\0"),
create_framebuffer: load!(b"vkCreateFramebuffer\0"),
destroy_framebuffer: load!(b"vkDestroyFramebuffer\0"),
allocate_command_buffers: load!(b"vkAllocateCommandBuffers\0"),
free_command_buffers: load!(b"vkFreeCommandBuffers\0"),
cmd_bind_pipeline: load!(b"vkCmdBindPipeline\0"),
cmd_begin_render_pass: load!(b"vkCmdBeginRenderPass\0"),
cmd_end_render_pass: load!(b"vkCmdEndRenderPass\0"),
cmd_begin_rendering_khr: try_load!(b"vkCmdBeginRenderingKHR\0"),
cmd_end_rendering_khr: try_load!(b"vkCmdEndRenderingKHR\0"),
// Phase 4 — capture images
create_image: load!(b"vkCreateImage\0"),
destroy_image: load!(b"vkDestroyImage\0"),
allocate_memory: load!(b"vkAllocateMemory\0"),
free_memory: load!(b"vkFreeMemory\0"),
bind_image_memory: load!(b"vkBindImageMemory\0"),
get_image_memory_requirements: load!(b"vkGetImageMemoryRequirements\0"),
map_memory: load!(b"vkMapMemory\0"),
unmap_memory: load!(b"vkUnmapMemory\0"),
cmd_pipeline_barrier: load!(b"vkCmdPipelineBarrier\0"),
cmd_copy_image: load!(b"vkCmdCopyImage\0"),
get_image_subresource_layout: load!(b"vkGetImageSubresourceLayout\0"),
get_memory_fd_khr: try_load!(b"vkGetMemoryFdKHR\0"),
// Phase 4 — synchronisation
create_fence: load!(b"vkCreateFence\0"),
destroy_fence: load!(b"vkDestroyFence\0"),
create_command_pool: load!(b"vkCreateCommandPool\0"),
destroy_command_pool: load!(b"vkDestroyCommandPool\0"),
reset_command_pool: load!(b"vkResetCommandPool\0"),
begin_command_buffer: load!(b"vkBeginCommandBuffer\0"),
end_command_buffer: load!(b"vkEndCommandBuffer\0"),
reset_command_buffer: load!(b"vkResetCommandBuffer\0"), // needed for double-buffered capture
queue_submit: load!(b"vkQueueSubmit\0"),
wait_for_fences: load!(b"vkWaitForFences\0"),
reset_fences: load!(b"vkResetFences\0"),
// Phase 4 — swapchain
create_swapchain_khr: try_load!(b"vkCreateSwapchainKHR\0"),
destroy_swapchain_khr: try_load!(b"vkDestroySwapchainKHR\0"),
get_swapchain_images_khr: try_load!(b"vkGetSwapchainImagesKHR\0"),
// Phase 6 — draw commands
cmd_draw: load!(b"vkCmdDraw\0"),
cmd_draw_indexed: load!(b"vkCmdDrawIndexed\0"),
cmd_draw_indirect: load!(b"vkCmdDrawIndirect\0"),
cmd_draw_indexed_indirect: load!(b"vkCmdDrawIndexedIndirect\0"),
cmd_draw_indirect_count: try_load!(b"vkCmdDrawIndirectCount\0"),
cmd_draw_indexed_indirect_count: try_load!(b"vkCmdDrawIndexedIndirectCount\0"),
};
// ── Retrieve capture queue from the bumped slot ───────────────────
let mut capture_queue = vk::Queue::null();
if queue_infos
.first()
.map(|q| q.queue_count > 1)
.unwrap_or(false)
{
let qi = &queue_infos[0];
unsafe {
(fp.get_device_queue)(
device,
qi.queue_family_index,
capture_queue_index,
&mut capture_queue,
);
}
log::info!(
"capture queue: family={} index={capture_queue_index}",
qi.queue_family_index
);
}
let key = unsafe { dispatch_key(device.as_raw() as *const c_void) };
// Phase 3: load shader hash config
let shader_hashes = config::resolve_config_path()
.as_ref()
.and_then(|p| config::load_config(p));
if let Some(ref set) = shader_hashes {
log::info!(
"config loaded — {} hud_frag, {} hud_vert, {} skip_frag",
set.hud_fragment_shaders.len(),
set.hud_vertex_shaders.len(),
set.skip_fragment_shaders.len(),
);
} else {
log::warn!("no config — draw suppression disabled");
}
let dev_state = Arc::new(DeviceState {
raw: device,
physical_device,
fp,
shader_registry: DashMap::new(),
pipeline_registry: DashMap::new(),
pipeline_state: DashMap::new(),
view_to_image: DashMap::new(),
view_format: DashMap::new(),
framebuffer_to_views: DashMap::new(),
framebuffer_extent: DashMap::new(),
shader_hashes,
hudless_image: std::sync::Mutex::new(None),
hudless_memory: std::sync::Mutex::new(None),
hudless_size: std::sync::Mutex::new((0, 0, vk::Format::UNDEFINED)),
final_image: std::sync::Mutex::new(None),
final_memory: std::sync::Mutex::new(None),
final_size: std::sync::Mutex::new((0, 0, vk::Format::UNDEFINED)),
final_stride: std::sync::atomic::AtomicU32::new(0),
swapchain: std::sync::Mutex::new(None),
swapchain_images: std::sync::Mutex::new(Vec::new()),
swapchain_format: std::sync::Mutex::new(vk::Format::UNDEFINED),
swapchain_extent: std::sync::Mutex::new(vk::Extent2D {
width: 0,
height: 0,
}),
swapchain_colorspace: std::sync::atomic::AtomicU32::new(0),
frame_counter: std::sync::atomic::AtomicU64::new(0),
largest_extent: std::sync::Mutex::new(vk::Extent2D {
width: 0,
height: 0,
}),
hud_detected_frame: std::sync::atomic::AtomicBool::new(false),
pending_capture_frame: std::sync::atomic::AtomicBool::new(false),
capture_injected_frame: std::sync::atomic::AtomicBool::new(false),
skipped_draws_frame: std::sync::atomic::AtomicU32::new(0),
encoder: std::sync::Mutex::new(None),
capture_resources: std::sync::Mutex::new(None),
capture_queue: std::sync::Mutex::new(capture_queue),
fake_images: std::sync::Mutex::new(Vec::new()),
fake_memories: std::sync::Mutex::new(Vec::new()),
fake_fds: std::sync::Mutex::new(Vec::new()),
fake_strides: std::sync::Mutex::new(Vec::new()),
fake_available: std::sync::Mutex::new(Vec::new()),
fake_image_count: std::sync::atomic::AtomicU32::new(0),
fake_swapchain: std::sync::Mutex::new(None),
signal_queue: std::sync::Mutex::new(vk::Queue::null()),
next_acquire: std::sync::atomic::AtomicU32::new(0),
memory_properties: std::sync::Mutex::new(mem_props),
acquire_dummy_pool: std::sync::Mutex::new(vk::CommandPool::null()),
acquire_dummy_cb: std::sync::Mutex::new(vk::CommandBuffer::null()),
cached_dmabuf_fd: std::sync::atomic::AtomicI32::new(-1),
target_fps: std::sync::atomic::AtomicU32::new(0),
last_capture_time: std::sync::Mutex::new(None),
capture_tx: std::sync::Mutex::new(None),
});
DEVICE_STATE.insert(key, dev_state);
log::info!("vkCreateDevice OK — key {:#x}", key);
vk::Result::SUCCESS
}
#[unsafe(no_mangle)]
pub unsafe extern "system" fn vkDestroyDevice(
device: vk::Device,
p_allocator: *const vk::AllocationCallbacks,
) {
let key = unsafe { dispatch_key(device.as_raw() as *const c_void) };
let ds = match DEVICE_STATE.remove(&key) {
Some((_, ds)) => ds,
None => return,
};
log::info!(
"vkDestroyDevice — {} shaders, {} pipelines evicted",
ds.shader_registry.len(),
ds.pipeline_registry.len(),
);
// ── 1. Shut down encoder pipeline (unblocks encoder + RTP threads) ───
{
let mut enc_guard = ds.encoder.lock().unwrap();
if let Some(handle) = enc_guard.take() {
handle.shutdown();
// `handle` is dropped here → drops `frame_tx` → encoder thread's
// recv_timeout returns Disconnected → encoder thread drops
// `encoded_tx` → RTP thread exits too.
//
// Give threads a moment to drain. In production you'd join the
// JoinHandles, but since we don't store them, a short sleep +
// the AtomicBool shutdown flag is sufficient.
log::info!("encoder pipeline shutdown signaled");
}
}
// Brief yield to let threads notice the disconnect.
std::thread::sleep(std::time::Duration::from_millis(50));
// ── 2. Clean up capture resources (double-buffered cmd pool + fences) ─
{
let mut res_guard = ds.capture_resources.lock().unwrap();
if let Some(res) = res_guard.take() {
unsafe {
// Wait for any in-flight capture commands to finish before
// destroying the fences / command pool.
let _ = (ds.fp.wait_for_fences)(
ds.raw,
res.fences.len() as u32,
res.fences.as_ptr(),
vk::TRUE,
5_000_000_000, // 5 seconds — should be instant
);
for &f in &res.fences {
(ds.fp.destroy_fence)(ds.raw, f, std::ptr::null());
}
(ds.fp.destroy_command_pool)(ds.raw, res.command_pool, std::ptr::null());
}
log::debug!("capture resources destroyed");
}
}
// ── 3. Free final_image / final_memory ────────────────────────────────
{
let img = ds.final_image.lock().unwrap().take();
let mem = ds.final_memory.lock().unwrap().take();
if let Some(i) = img {
unsafe { (ds.fp.destroy_image)(ds.raw, i, std::ptr::null()) };
}
if let Some(m) = mem {
unsafe { (ds.fp.free_memory)(ds.raw, m, std::ptr::null()) };
}
}
// ── 4. Free hudless_image / hudless_memory ────────────────────────────
{
let img = ds.hudless_image.lock().unwrap().take();
let mem = ds.hudless_memory.lock().unwrap().take();
if let Some(i) = img {
unsafe { (ds.fp.destroy_image)(ds.raw, i, std::ptr::null()) };
}
if let Some(m) = mem {
unsafe { (ds.fp.free_memory)(ds.raw, m, std::ptr::null()) };
}
}
// ── 5. Close cached DMA-BUF fd ────────────────────────────────────────
{
let fd = ds.cached_dmabuf_fd.load(Ordering::Relaxed);
if fd >= 0 {
unsafe { libc::close(fd) };
log::debug!("cached DMA-BUF fd {} closed", fd);
}
}
// ── 6. Clean up queue → device key mappings for this device ───────────
QUEUE_TO_DEVICE_KEY.retain(|_, dk| *dk != key);
// ── 7. Call the real vkDestroyDevice ──────────────────────────────────
unsafe { (ds.fp.destroy_device)(device, p_allocator) };
log::info!("vkDestroyDevice complete");
}
#[unsafe(no_mangle)]
pub unsafe extern "system" fn vkGetDeviceQueue(
device: vk::Device,
queue_family_index: u32,
queue_index: u32,
p_queue: *mut vk::Queue,
) {
let key = unsafe { dispatch_key(device.as_raw() as *const c_void) };
if let Some(ds) = DEVICE_STATE.get(&key) {
unsafe { (ds.fp.get_device_queue)(device, queue_family_index, queue_index, p_queue) };
let queue = unsafe { *p_queue };
QUEUE_TO_DEVICE_KEY.insert(queue.as_raw(), key);
// Store first queue for acquire semaphore signaling
let mut sq = ds.signal_queue.lock().unwrap();
if *sq == vk::Queue::null() {
*sq = queue;
}
}
}
/// Enumerate device extensions supported by the physical device.
unsafe fn enumerate_device_extensions(
istate: &crate::dispatch::NextInstanceFn,
physical_device: vk::PhysicalDevice,
) -> Vec<std::ffi::CString> {
// We need vkEnumerateDeviceExtensionProperties. Load it from the
// instance dispatch since it's a physical-device-level function.
// For simplicity, use ash's raw function signature.
type PFN_vkEnumerateDeviceExtensionProperties = unsafe extern "system" fn(
vk::PhysicalDevice,
*const libc::c_char,
*mut u32,
*mut vk::ExtensionProperties,
) -> vk::Result;
let func: Option<PFN_vkEnumerateDeviceExtensionProperties> = {
let raw = unsafe {
(istate.get_instance_proc_addr)(
vk::Instance::null(),
b"vkEnumerateDeviceExtensionProperties\0".as_ptr() as *const libc::c_char,
)
};
raw.map(|f| unsafe { std::mem::transmute(f) })
};
let Some(enumerate) = func else {
log::warn!("could not load vkEnumerateDeviceExtensionProperties");
return Vec::new();
};
let mut count = 0u32;
if unsafe {
enumerate(
physical_device,
std::ptr::null(),
&mut count,
std::ptr::null_mut(),
)
} != vk::Result::SUCCESS
{
return Vec::new();
}
let mut props = vec![vk::ExtensionProperties::default(); count as usize];
if unsafe {
enumerate(
physical_device,
std::ptr::null(),
&mut count,
props.as_mut_ptr(),
)
} != vk::Result::SUCCESS
{
return Vec::new();
}
props
.iter()
.filter_map(|p| {
let cstr = unsafe { std::ffi::CStr::from_ptr(p.extension_name.as_ptr()) };
Some(cstr.to_owned())
})
.collect()
}