Files
punktfunk/crates/pf-zerocopy/src/imp/vkslot.rs
T
enricobuehlerandClaude Fable 5 d2c46eaf3c feat(encode/nvenc): SPIR-V cursor blend over Vulkan-allocated input slots — retire the PTX kernels
A vendored PTX blob is JIT'd against the driver's ISA ceiling, so the
cursor-blend module silently dies on drivers older than the generating
toolkit (CUDA_ERROR_UNSUPPORTED_PTX_VERSION/INVALID_PTX, 222/218 — the
KWin leg's invisible composite cursor on driver 595/CUDA 13.2 vs a
CUDA 13.3 blob). SPIR-V has no such coupling, and the in-tree precedent
already exists twice (vulkan_video's CSC blend, VkBridge's exportable
OPAQUE_FD → cuImportExternalMemory bridge).

New pf_zerocopy::vkslot::VkSlotBlend: the direct-SDK NVENC encoder now
allocates its input ring as exportable Vulkan buffers CUDA-imports (same
contiguous InputSurface layouts, pitch = row bytes rounded to 256), and
the cursor composite is a compute dispatch over the cursor's rectangle
(cursor_blend.comp, vendored .spv; spec-constant selects ARGB/NV12/YUV444;
BT.709 limited, matching the retired .cu). The surface SSBO is uint[] with
every invocation owning whole words — no 8-bit-storage device dependency.
Cursor-bearing frames force the existing CPU-synced submit path so the
CUDA copy → Vulkan dispatch (fence-waited) → NVENC encode ordering is
CPU-established; cursorless frames keep the stream-ordered fast path
untouched. Any bring-up/alloc/registration failure falls back wholesale
to plain pitched CUDA surfaces (never a mixed or short ring): sessions
always encode, composite mode just loses the cursor, warned once.

cursor_blend.cu / cursor_blend.ptx and the CursorBlend PTX loader are
deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:47:53 +02:00

711 lines
31 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Vulkan-allocated NVENC input slots + the Vulkan compute cursor blend — the driver-portable
//! replacement for the retired `cursor_blend.cu` PTX kernels (design: remote-desktop-sweep §8,
//! Phase A). A vendored PTX blob is JIT'd against the driver's ISA ceiling, so it silently dies
//! on drivers older than the generating toolkit (CUDA errors 222/218 on-glass — the KWin leg's
//! invisible composite cursor); SPIR-V has no such coupling.
//!
//! ```text
//! exportable VkBuffer ──vkGetMemoryFdKHR(OPAQUE_FD)──▶ cuImportExternalMemory ──▶ CUdeviceptr
//! ▲ │ NVENC registers + encodes
//! └── cursor_blend.comp dispatch (cursor rect only) ◀───┘ CUDA copies frames in
//! ```
//!
//! The direct-SDK NVENC encoder allocates its input ring through [`VkSlotBlend::alloc_slot`]
//! instead of `cuMemAllocPitch`: same contiguous layouts (`InputSurface` docs), but the memory is
//! Vulkan external memory both APIs address. Per cursor-bearing frame the encoder CPU-syncs its
//! CUDA copy, then [`VkSlotBlend::blend`] dispatches the compute blend over the cursor's
//! rectangle and fence-waits — the same coherence ceremony [`super::vulkan::VkBridge`] ships for
//! its CSC (fence-ordered cross-API access on NVIDIA, no queue-family transfer needed). Frames
//! without a cursor never touch Vulkan, keeping the stream-ordered fast path intact.
//!
//! Falls back cleanly: if bring-up fails the encoder allocates plain CUDA surfaces and composite
//! mode degrades to no cursor (warned once) — never a failed session.
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
#![deny(clippy::undocumented_unsafe_blocks)]
use super::cuda::{self, CUdeviceptr};
use anyhow::{anyhow, Context as _, Result};
use ash::vk;
/// Max cursor-overlay bitmap edge (px) — matches [`cuda::CURSOR_MAX`] and the capture-side clamp.
pub const CURSOR_MAX: u32 = cuda::CURSOR_MAX;
/// The vendored SPIR-V for `cursor_blend.comp` (beside this file; rebuild with
/// `glslc cursor_blend.comp -o cursor_blend.spv`).
const CURSOR_SPV: &[u8] = include_bytes!("cursor_blend.spv");
/// NVENC input-surface layout — selects the spec-constant `MODE` pipeline and the allocation
/// arithmetic (mirroring `InputSurface`'s contiguous layouts).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SlotFormat {
/// Packed 4-byte ARGB (NVENC byte order B,G,R,A): `pitch × height`.
Argb,
/// NV12: Y rows `[0, H)` + interleaved UV rows `[H, 3H/2)` under one pitch.
Nv12,
/// Planar YUV444: three full-res planes stacked at `pitch × height` intervals.
Yuv444,
}
impl SlotFormat {
fn mode(self) -> u32 {
match self {
SlotFormat::Argb => 0,
SlotFormat::Nv12 => 1,
SlotFormat::Yuv444 => 2,
}
}
fn row_bytes(self, width: u32) -> u64 {
match self {
SlotFormat::Argb => width as u64 * 4,
SlotFormat::Nv12 | SlotFormat::Yuv444 => width as u64,
}
}
fn rows(self, height: u32) -> u64 {
match self {
SlotFormat::Argb => height as u64,
SlotFormat::Nv12 => height as u64 + (height as u64 / 2).max(1),
SlotFormat::Yuv444 => height as u64 * 3,
}
}
}
/// What the encoder holds per ring slot: the CUDA view it registers with NVENC plus the id it
/// hands back to [`VkSlotBlend::blend`]. The backing Vulkan objects + CUDA mapping live in the
/// [`VkSlotBlend`] (freed by [`free_slots`](VkSlotBlend::free_slots) / drop), so this is Copy —
/// the encoder's ring keeps its existing shape.
#[derive(Clone, Copy)]
pub struct VkSlotRef {
/// Device pointer NVENC registers (CUDA's mapping of the Vulkan memory).
pub ptr: CUdeviceptr,
/// Row stride in bytes (ours: row bytes rounded up to 256).
pub pitch: usize,
/// Luma rows (the plane-stride multiplier, as in `InputSurface`).
pub height: u32,
/// Index into the blend's slot table.
pub id: usize,
}
/// One allocated slot's backing objects, freed together in reverse order (CUDA mapping first).
struct SlotAlloc {
buffer: vk::Buffer,
memory: vk::DeviceMemory,
/// CUDA's import of the exported OPAQUE_FD — must drop BEFORE the Vulkan memory is freed.
cuda: cuda::ExternalDmabuf,
size: u64,
}
/// 28-byte push-constant block matching `cursor_blend.comp`'s `Push`.
#[repr(C)]
struct Push {
pitch: u32,
surf_w: u32,
surf_h: u32,
cur_w: u32,
cur_h: u32,
ox: i32,
oy: i32,
}
pub struct VkSlotBlend {
_entry: ash::Entry,
instance: ash::Instance,
device: ash::Device,
ext_fd: ash::khr::external_memory_fd::Device,
queue: vk::Queue,
cmd_pool: vk::CommandPool,
cmd: vk::CommandBuffer,
fence: vk::Fence,
mem_props: vk::PhysicalDeviceMemoryProperties,
shader: vk::ShaderModule,
desc_layout: vk::DescriptorSetLayout,
pipe_layout: vk::PipelineLayout,
desc_pool: vk::DescriptorPool,
desc_set: vk::DescriptorSet,
/// One pipeline per [`SlotFormat`], indexed by `mode()` (spec constant).
pipelines: [vk::Pipeline; 3],
/// Host-visible cursor bitmap staging (CURSOR_MAX²·4, tight rows), persistently mapped.
cur_buf: vk::Buffer,
cur_mem: vk::DeviceMemory,
cur_map: *mut u8,
slots: Vec<SlotAlloc>,
}
// SAFETY: raw Vulkan handles + a persistently-mapped pointer, all uniquely owned by this struct
// and destroyed exactly once in `Drop`; used from the encoder thread but moved with it. `Send`
// only (not `Sync`), matching the single-thread use — transferring opaque handles cannot dangle.
unsafe impl Send for VkSlotBlend {}
impl VkSlotBlend {
/// Bring up the device + blend pipelines. Requires the CUDA shared context (the encoder's) to
/// be established; picks the NVIDIA physical device (the NVENC path is NVIDIA by definition).
pub fn new() -> Result<VkSlotBlend> {
// SAFETY: standard ash bring-up, same shape as `VkBridge::new` — every call is `unsafe`
// only because ash cannot statically verify handle/CreateInfo validity. Every
// `*CreateInfo`/`AllocateInfo` is built from locals that live for the duration of the
// synchronous call reading them; every handle passed was created and `?`-checked in this
// same function. Shares nothing across threads.
unsafe {
let entry = ash::Entry::load().context("load libvulkan")?;
let app = vk::ApplicationInfo::default().api_version(vk::API_VERSION_1_1);
let instance = entry
.create_instance(
&vk::InstanceCreateInfo::default().application_info(&app),
None,
)
.context("vkCreateInstance")?;
let phys = match instance
.enumerate_physical_devices()
.context("enumerate GPUs")?
.into_iter()
.find(|&p| instance.get_physical_device_properties(p).vendor_id == 0x10DE)
{
Some(p) => p,
None => {
instance.destroy_instance(None);
return Err(anyhow!("no NVIDIA Vulkan device"));
}
};
let mem_props = instance.get_physical_device_memory_properties(phys);
let qf = match instance
.get_physical_device_queue_family_properties(phys)
.iter()
.position(|q| q.queue_flags.contains(vk::QueueFlags::COMPUTE))
{
Some(i) => i as u32,
None => {
instance.destroy_instance(None);
return Err(anyhow!("no compute-capable queue family"));
}
};
let prio = [1.0f32];
let qci = [vk::DeviceQueueCreateInfo::default()
.queue_family_index(qf)
.queue_priorities(&prio)];
let exts = [ash::khr::external_memory_fd::NAME.as_ptr()];
let device = match instance.create_device(
phys,
&vk::DeviceCreateInfo::default()
.queue_create_infos(&qci)
.enabled_extension_names(&exts),
None,
) {
Ok(d) => d,
Err(e) => {
instance.destroy_instance(None);
return Err(e).context("vkCreateDevice (external_memory_fd supported?)");
}
};
// From here teardown-on-error goes through `destroy_partial`, which tolerates null
// handles — build everything into an incrementally-filled struct.
let ext_fd = ash::khr::external_memory_fd::Device::new(&instance, &device);
let queue = device.get_device_queue(qf, 0);
let mut me = VkSlotBlend {
_entry: entry,
instance,
device,
ext_fd,
queue,
cmd_pool: vk::CommandPool::null(),
cmd: vk::CommandBuffer::null(),
fence: vk::Fence::null(),
mem_props,
shader: vk::ShaderModule::null(),
desc_layout: vk::DescriptorSetLayout::null(),
pipe_layout: vk::PipelineLayout::null(),
desc_pool: vk::DescriptorPool::null(),
desc_set: vk::DescriptorSet::null(),
pipelines: [vk::Pipeline::null(); 3],
cur_buf: vk::Buffer::null(),
cur_mem: vk::DeviceMemory::null(),
cur_map: std::ptr::null_mut(),
slots: Vec::new(),
};
me.init_objects(qf).inspect_err(|_| {
// `Drop` runs the same teardown and tolerates the nulls left by a partial init.
})?;
tracing::info!(
"Vulkan slot blend ready (exportable NVENC inputs + SPIR-V cursor blend)"
);
Ok(me)
}
}
/// The non-device objects: command machinery, cursor staging, descriptor + pipelines.
fn init_objects(&mut self, qf: u32) -> Result<()> {
// SAFETY: same contract as `new` — ash calls on the live `self.device` with builder infos
// from locals outliving each synchronous call; created handles are stored into `self`
// immediately so the caller's `Drop` frees them on any later failure.
unsafe {
let d = &self.device;
self.cmd_pool = d
.create_command_pool(
&vk::CommandPoolCreateInfo::default()
.queue_family_index(qf)
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER),
None,
)
.context("create command pool")?;
self.cmd = d
.allocate_command_buffers(
&vk::CommandBufferAllocateInfo::default()
.command_pool(self.cmd_pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1),
)
.context("allocate command buffer")?[0];
self.fence = d
.create_fence(&vk::FenceCreateInfo::default(), None)
.context("create fence")?;
// Cursor staging: host-visible+coherent SSBO, persistently mapped.
let cur_size = (CURSOR_MAX * CURSOR_MAX * 4) as u64;
self.cur_buf = d
.create_buffer(
&vk::BufferCreateInfo::default()
.size(cur_size)
.usage(vk::BufferUsageFlags::STORAGE_BUFFER),
None,
)
.context("create cursor buffer")?;
let reqs = d.get_buffer_memory_requirements(self.cur_buf);
let mem_type = self
.memory_type(
reqs.memory_type_bits,
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
)
.context("cursor buffer memory type")?;
self.cur_mem = d
.allocate_memory(
&vk::MemoryAllocateInfo::default()
.allocation_size(reqs.size)
.memory_type_index(mem_type),
None,
)
.context("allocate cursor memory")?;
d.bind_buffer_memory(self.cur_buf, self.cur_mem, 0)
.context("bind cursor memory")?;
self.cur_map = d
.map_memory(self.cur_mem, 0, cur_size, vk::MemoryMapFlags::empty())
.context("map cursor memory")? as *mut u8;
// Descriptor set: binding 0 = surface SSBO (rebound per blend), 1 = cursor SSBO.
let bindings = [
vk::DescriptorSetLayoutBinding::default()
.binding(0)
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::COMPUTE),
vk::DescriptorSetLayoutBinding::default()
.binding(1)
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::COMPUTE),
];
self.desc_layout = d
.create_descriptor_set_layout(
&vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
None,
)
.context("create descriptor layout")?;
let pc = [vk::PushConstantRange::default()
.stage_flags(vk::ShaderStageFlags::COMPUTE)
.size(std::mem::size_of::<Push>() as u32)];
let dl = [self.desc_layout];
self.pipe_layout = d
.create_pipeline_layout(
&vk::PipelineLayoutCreateInfo::default()
.set_layouts(&dl)
.push_constant_ranges(&pc),
None,
)
.context("create pipeline layout")?;
let pool_sizes = [vk::DescriptorPoolSize::default()
.ty(vk::DescriptorType::STORAGE_BUFFER)
.descriptor_count(2)];
self.desc_pool = d
.create_descriptor_pool(
&vk::DescriptorPoolCreateInfo::default()
.max_sets(1)
.pool_sizes(&pool_sizes),
None,
)
.context("create descriptor pool")?;
let dls = [self.desc_layout];
self.desc_set = d
.allocate_descriptor_sets(
&vk::DescriptorSetAllocateInfo::default()
.descriptor_pool(self.desc_pool)
.set_layouts(&dls),
)
.context("allocate descriptor set")?[0];
// The shader + one pipeline per MODE (spec constant 0).
if CURSOR_SPV.len() % 4 != 0 {
anyhow::bail!("cursor_blend.spv is not word-aligned");
}
let words: Vec<u32> = CURSOR_SPV
.chunks_exact(4)
.map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect();
self.shader = d
.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&words), None)
.context("create blend shader module")?;
for mode in 0u32..3 {
let entries = [vk::SpecializationMapEntry::default()
.constant_id(0)
.offset(0)
.size(4)];
let data = mode.to_le_bytes();
let spec = vk::SpecializationInfo::default()
.map_entries(&entries)
.data(&data);
let stage = vk::PipelineShaderStageCreateInfo::default()
.stage(vk::ShaderStageFlags::COMPUTE)
.module(self.shader)
.name(c"main")
.specialization_info(&spec);
let info = [vk::ComputePipelineCreateInfo::default()
.stage(stage)
.layout(self.pipe_layout)];
let p = d
.create_compute_pipelines(vk::PipelineCache::null(), &info, None)
.map_err(|(_, e)| e)
.context("create blend pipeline")?[0];
self.pipelines[mode as usize] = p;
}
}
Ok(())
}
fn memory_type(&self, type_bits: u32, flags: vk::MemoryPropertyFlags) -> Result<u32> {
(0..self.mem_props.memory_type_count)
.find(|&i| {
type_bits & (1 << i) != 0
&& self.mem_props.memory_types[i as usize]
.property_flags
.contains(flags)
})
.ok_or_else(|| anyhow!("no memory type for flags {flags:?}"))
}
/// Allocate one NVENC input slot as exportable Vulkan memory mapped into CUDA. Layout matches
/// `InputSurface` (contiguous planes under one pitch); pitch = row bytes rounded to 256.
pub fn alloc_slot(&mut self, fmt: SlotFormat, width: u32, height: u32) -> Result<VkSlotRef> {
let pitch = (fmt.row_bytes(width) + 255) & !255;
let size = pitch * fmt.rows(height);
// SAFETY: exportable-buffer allocation, the exact `VkBridge::ensure_dst` incantation:
// `ExternalMemoryBufferCreateInfo`/`ExportMemoryAllocateInfo` declare OPAQUE_FD,
// `MemoryDedicatedAllocateInfo` ties the memory to the buffer; every info is a local
// outliving its synchronous call and every failure path destroys the objects created so
// far exactly once. `get_memory_fd` hands us an fd that `import_owned_fd` either adopts
// (driver owns it) or closes on failure.
unsafe {
let d = &self.device;
let mut ext_info = vk::ExternalMemoryBufferCreateInfo::default()
.handle_types(vk::ExternalMemoryHandleTypeFlags::OPAQUE_FD);
let buffer = d
.create_buffer(
&vk::BufferCreateInfo::default()
.size(size)
.usage(vk::BufferUsageFlags::STORAGE_BUFFER)
.push_next(&mut ext_info),
None,
)
.context("create slot buffer")?;
let reqs = d.get_buffer_memory_requirements(buffer);
let mem_type = match self
.memory_type(reqs.memory_type_bits, vk::MemoryPropertyFlags::DEVICE_LOCAL)
{
Ok(t) => t,
Err(e) => {
d.destroy_buffer(buffer, None);
return Err(e);
}
};
let mut export = vk::ExportMemoryAllocateInfo::default()
.handle_types(vk::ExternalMemoryHandleTypeFlags::OPAQUE_FD);
let mut dedicated = vk::MemoryDedicatedAllocateInfo::default().buffer(buffer);
let memory = match d.allocate_memory(
&vk::MemoryAllocateInfo::default()
.allocation_size(reqs.size)
.memory_type_index(mem_type)
.push_next(&mut export)
.push_next(&mut dedicated),
None,
) {
Ok(m) => m,
Err(e) => {
d.destroy_buffer(buffer, None);
return Err(e).context("allocate exportable slot memory");
}
};
if let Err(e) = d.bind_buffer_memory(buffer, memory, 0) {
d.free_memory(memory, None);
d.destroy_buffer(buffer, None);
return Err(e).context("bind slot memory");
}
let fd = match self.ext_fd.get_memory_fd(
&vk::MemoryGetFdInfoKHR::default()
.memory(memory)
.handle_type(vk::ExternalMemoryHandleTypeFlags::OPAQUE_FD),
) {
Ok(f) => f,
Err(e) => {
d.free_memory(memory, None);
d.destroy_buffer(buffer, None);
return Err(e).context("vkGetMemoryFdKHR(slot)");
}
};
let ext = match cuda::ExternalDmabuf::import_owned_fd(fd, reqs.size) {
Ok(c) => c,
Err(e) => {
d.free_memory(memory, None);
d.destroy_buffer(buffer, None);
return Err(e).context("cuImportExternalMemory(slot OPAQUE_FD)");
}
};
let r = VkSlotRef {
ptr: ext.ptr,
pitch: pitch as usize,
height,
id: self.slots.len(),
};
self.slots.push(SlotAlloc {
buffer,
memory,
cuda: ext,
size,
});
Ok(r)
}
}
/// Free every allocated slot (encoder teardown, alongside its ring clear). CUDA mappings drop
/// first (field order in [`SlotAlloc`] frees `cuda` via its own `Drop` before we free the VK
/// objects explicitly here).
pub fn free_slots(&mut self) {
for s in self.slots.drain(..) {
drop(s.cuda); // CUDA's view of the memory goes first
// SAFETY: `buffer`/`memory` were created in `alloc_slot`, are uniquely owned by the
// drained `SlotAlloc`, and are destroyed exactly once here. No queue work can be
// in flight: every `blend` fence-waits before returning.
unsafe {
self.device.destroy_buffer(s.buffer, None);
self.device.free_memory(s.memory, None);
}
}
}
/// Upload the cursor RGBA (`cw*ch*4`, tight rows) into the mapped staging buffer. Call only
/// when the bitmap changes; position moves are push constants.
pub fn upload_cursor(&mut self, rgba: &[u8], cw: u32, ch: u32) {
let cw = cw.min(CURSOR_MAX);
let ch = ch.min(CURSOR_MAX);
let len = (cw * ch * 4) as usize;
let len = len.min(rgba.len());
// SAFETY: `cur_map` is the live persistent mapping of the CURSOR_MAX²·4 host-coherent
// allocation (created in `init_objects`, unmapped only in `Drop`); `len` is clamped to
// both the source slice and the buffer capacity. No blend is in flight (every `blend`
// fence-waits before returning), so no GPU read races this host write.
unsafe {
std::ptr::copy_nonoverlapping(rgba.as_ptr(), self.cur_map, len);
}
}
/// Blend the uploaded cursor into `slot` at `(ox, oy)`: record, submit, fence-wait. The
/// caller has CPU-synced its CUDA frame copy first; the fence wait makes the shader's writes
/// visible to the subsequent NVENC encode (the `VkBridge` precedent: fence-ordered cross-API
/// access, no queue-family transfer — NVIDIA-only path).
#[allow(clippy::too_many_arguments)] // surface geometry + cursor rect — unpacked kernel args
pub fn blend_ref(
&mut self,
slot: &VkSlotRef,
fmt: SlotFormat,
surf_w: u32,
cw: u32,
ch: u32,
ox: i32,
oy: i32,
) -> Result<()> {
let alloc = self
.slots
.get(slot.id)
.ok_or_else(|| anyhow!("bad slot id {}", slot.id))?;
let cw = cw.min(CURSOR_MAX);
let ch = ch.min(CURSOR_MAX);
if cw == 0 || ch == 0 {
return Ok(());
}
let push = Push {
pitch: slot.pitch as u32,
surf_w,
surf_h: slot.height,
cur_w: cw,
cur_h: ch,
ox,
oy,
};
// Dispatch geometry (must match cursor_blend.comp): ARGB = per cursor px; NV12/YUV444 =
// per word-aligned 4-px span × (2-row blocks | rows).
let (gx, gy) = match fmt {
SlotFormat::Argb => (cw.div_ceil(8), ch.div_ceil(8)),
_ => {
let x0 = (ox >> 2) << 2;
let spans = ((ox + cw as i32) - x0 + 3).div_euclid(4).max(1) as u32;
let rows = match fmt {
SlotFormat::Nv12 => ch.div_ceil(2),
_ => ch,
};
(spans.div_ceil(8), rows.div_ceil(8))
}
};
// SAFETY: single-threaded record/submit/wait on handles this struct owns. The descriptor
// update is safe because no prior submission is in flight (every blend fence-waits and
// the fence is reset before reuse). Buffer infos and barrier structs are locals outliving
// their synchronous calls. The dispatch's shader accesses stay in-bounds by the shader's
// own guards (surfW/surfH/curW/curH from `push`) against the slot allocation sized in
// `alloc_slot` for exactly that geometry.
unsafe {
let d = &self.device;
let surf_info = [vk::DescriptorBufferInfo::default()
.buffer(alloc.buffer)
.offset(0)
.range(alloc.size)];
let cur_info = [vk::DescriptorBufferInfo::default()
.buffer(self.cur_buf)
.offset(0)
.range((CURSOR_MAX * CURSOR_MAX * 4) as u64)];
let writes = [
vk::WriteDescriptorSet::default()
.dst_set(self.desc_set)
.dst_binding(0)
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
.buffer_info(&surf_info),
vk::WriteDescriptorSet::default()
.dst_set(self.desc_set)
.dst_binding(1)
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
.buffer_info(&cur_info),
];
d.update_descriptor_sets(&writes, &[]);
d.begin_command_buffer(
self.cmd,
&vk::CommandBufferBeginInfo::default()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
)
.context("begin blend cmd")?;
// CUDA wrote the frame into this memory outside Vulkan's view — make it visible to
// the shader (external-memory coherence ceremony; NVIDIA honors this with the fence
// ordering alone, the barrier is the spec-shaped belt-and-braces).
let acquire = [vk::MemoryBarrier::default()
.src_access_mask(vk::AccessFlags::MEMORY_WRITE)
.dst_access_mask(vk::AccessFlags::SHADER_READ | vk::AccessFlags::SHADER_WRITE)];
d.cmd_pipeline_barrier(
self.cmd,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::PipelineStageFlags::COMPUTE_SHADER,
vk::DependencyFlags::empty(),
&acquire,
&[],
&[],
);
d.cmd_bind_pipeline(
self.cmd,
vk::PipelineBindPoint::COMPUTE,
self.pipelines[fmt.mode() as usize],
);
d.cmd_bind_descriptor_sets(
self.cmd,
vk::PipelineBindPoint::COMPUTE,
self.pipe_layout,
0,
&[self.desc_set],
&[],
);
let bytes = std::slice::from_raw_parts(
(&push as *const Push) as *const u8,
std::mem::size_of::<Push>(),
);
d.cmd_push_constants(
self.cmd,
self.pipe_layout,
vk::ShaderStageFlags::COMPUTE,
0,
bytes,
);
d.cmd_dispatch(self.cmd, gx.max(1), gy.max(1), 1);
// Release the shader's writes so the post-fence CUDA/NVENC reads see them.
let release = [vk::MemoryBarrier::default()
.src_access_mask(vk::AccessFlags::SHADER_WRITE)
.dst_access_mask(vk::AccessFlags::MEMORY_READ)];
d.cmd_pipeline_barrier(
self.cmd,
vk::PipelineStageFlags::COMPUTE_SHADER,
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
vk::DependencyFlags::empty(),
&release,
&[],
&[],
);
d.end_command_buffer(self.cmd).context("end blend cmd")?;
let cmds = [self.cmd];
let submit = [vk::SubmitInfo::default().command_buffers(&cmds)];
d.queue_submit(self.queue, &submit, self.fence)
.context("submit blend")?;
let r = d.wait_for_fences(&[self.fence], true, 1_000_000_000);
d.reset_fences(&[self.fence]).ok();
r.context("blend fence wait")?;
}
Ok(())
}
}
impl Drop for VkSlotBlend {
fn drop(&mut self) {
self.free_slots();
// SAFETY: every handle below was created in `new`/`init_objects` (or is null from a
// partial init — Vulkan destroy/free calls are defined no-ops on null handles) and is
// uniquely owned; each is destroyed exactly once here, pipelines/layouts/pools before the
// device, the device before the instance. No work is in flight (`blend` fence-waits).
unsafe {
let d = &self.device;
for p in self.pipelines {
if p != vk::Pipeline::null() {
d.destroy_pipeline(p, None);
}
}
if self.shader != vk::ShaderModule::null() {
d.destroy_shader_module(self.shader, None);
}
if self.desc_pool != vk::DescriptorPool::null() {
d.destroy_descriptor_pool(self.desc_pool, None);
}
if self.pipe_layout != vk::PipelineLayout::null() {
d.destroy_pipeline_layout(self.pipe_layout, None);
}
if self.desc_layout != vk::DescriptorSetLayout::null() {
d.destroy_descriptor_set_layout(self.desc_layout, None);
}
if !self.cur_map.is_null() {
d.unmap_memory(self.cur_mem);
}
if self.cur_buf != vk::Buffer::null() {
d.destroy_buffer(self.cur_buf, None);
}
if self.cur_mem != vk::DeviceMemory::null() {
d.free_memory(self.cur_mem, None);
}
if self.fence != vk::Fence::null() {
d.destroy_fence(self.fence, None);
}
if self.cmd_pool != vk::CommandPool::null() {
d.destroy_command_pool(self.cmd_pool, None);
}
d.destroy_device(None);
self.instance.destroy_instance(None);
}
}
}