fix(zerocopy/vulkan): VkBridge bring-up and the CSC build unwind instead of leaking
VkBridge::new leaked its instance (and past device creation, the device and command pool too) on every error path — reached repeatedly, because a box whose Vulkan device refuses the external-memory extensions retries the bridge on every LINEAR frame. Pre-device failures now destroy the instance explicitly (the VkSlotBlend::new shape); after device creation the remaining objects build into an incrementally-filled struct whose existing Drop tolerates the nulls a partial init leaves (Vulkan destroy calls are defined no-ops on VK_NULL_HANDLE). ensure_csc had the same hole across its six-object pipeline build; the fallible half now fills the Csc front to back and a mid-build failure destroys exactly what was created, leaving self.csc None for a clean retry. (R3 from design/pf-zerocopy-sweep-handoff.md.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -104,25 +104,44 @@ impl VkBridge {
|
|||||||
)
|
)
|
||||||
.context("vkCreateInstance")?;
|
.context("vkCreateInstance")?;
|
||||||
|
|
||||||
// Pick the NVIDIA GPU (matches CUDA device 0 on this single-dGPU host).
|
// Pick the NVIDIA GPU (matches CUDA device 0 on this single-dGPU host). Failure paths
|
||||||
let phys = instance
|
// between instance and device creation destroy the instance explicitly (the shape
|
||||||
|
// `VkSlotBlend::new` uses) — a box that refuses the bridge retries per frame, so a
|
||||||
|
// leak here is unbounded, not one-shot.
|
||||||
|
let phys = match instance
|
||||||
.enumerate_physical_devices()
|
.enumerate_physical_devices()
|
||||||
.context("enumerate GPUs")?
|
.context("enumerate GPUs")
|
||||||
.into_iter()
|
.map(|devs| {
|
||||||
.find(|&p| instance.get_physical_device_properties(p).vendor_id == 0x10DE)
|
devs.into_iter()
|
||||||
.ok_or_else(|| anyhow!("no NVIDIA Vulkan device"))?;
|
.find(|&p| instance.get_physical_device_properties(p).vendor_id == 0x10DE)
|
||||||
|
}) {
|
||||||
|
Ok(Some(p)) => p,
|
||||||
|
Ok(None) => {
|
||||||
|
instance.destroy_instance(None);
|
||||||
|
return Err(anyhow!("no NVIDIA Vulkan device"));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
instance.destroy_instance(None);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
let mem_props = instance.get_physical_device_memory_properties(phys);
|
let mem_props = instance.get_physical_device_memory_properties(phys);
|
||||||
|
|
||||||
// A COMPUTE-capable family (compute implies transfer): the copy path only needs
|
// A COMPUTE-capable family (compute implies transfer): the copy path only needs
|
||||||
// transfer, but the NV12 CSC dispatch (T2.5b) needs compute — on every NVIDIA
|
// transfer, but the NV12 CSC dispatch (T2.5b) needs compute — on every NVIDIA
|
||||||
// device family 0 is graphics+compute+transfer, so this picks the same family the
|
// device family 0 is graphics+compute+transfer, so this picks the same family the
|
||||||
// old transfer-only predicate did.
|
// old transfer-only predicate did.
|
||||||
let qf = instance
|
let qf = match instance
|
||||||
.get_physical_device_queue_family_properties(phys)
|
.get_physical_device_queue_family_properties(phys)
|
||||||
.iter()
|
.iter()
|
||||||
.position(|q| q.queue_flags.contains(vk::QueueFlags::COMPUTE))
|
.position(|q| q.queue_flags.contains(vk::QueueFlags::COMPUTE))
|
||||||
.ok_or_else(|| anyhow!("no compute-capable queue family"))?
|
{
|
||||||
as u32;
|
Some(i) => i as u32,
|
||||||
|
None => {
|
||||||
|
instance.destroy_instance(None);
|
||||||
|
return Err(anyhow!("no compute-capable queue family"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Global-priority queue (latency plan §7 LN4, PyroWave's `ac0e7332` lever for the
|
// Global-priority queue (latency plan §7 LN4, PyroWave's `ac0e7332` lever for the
|
||||||
// VkBridge): the LINEAR/gamescope CSC dispatch shares the SM/compute cores with the
|
// VkBridge): the LINEAR/gamescope CSC dispatch shares the SM/compute cores with the
|
||||||
@@ -206,15 +225,34 @@ impl VkBridge {
|
|||||||
try_priority = None;
|
try_priority = None;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
instance.destroy_instance(None);
|
||||||
return Err(e)
|
return Err(e)
|
||||||
.context("vkCreateDevice (external-memory extensions supported?)")
|
.context("vkCreateDevice (external-memory extensions supported?)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// From here teardown-on-error goes through `Drop`, which tolerates null handles
|
||||||
|
// (Vulkan destroy calls are defined no-ops on VK_NULL_HANDLE) — build the remaining
|
||||||
|
// objects into an incrementally-filled struct so any `?` below unwinds cleanly
|
||||||
|
// instead of leaking the instance + device.
|
||||||
let ext_fd = ash::khr::external_memory_fd::Device::new(&instance, &device);
|
let ext_fd = ash::khr::external_memory_fd::Device::new(&instance, &device);
|
||||||
let queue = device.get_device_queue(qf, 0);
|
let queue = device.get_device_queue(qf, 0);
|
||||||
|
let mut me = VkBridge {
|
||||||
let cmd_pool = device
|
_entry: entry,
|
||||||
|
instance,
|
||||||
|
device,
|
||||||
|
ext_fd,
|
||||||
|
queue,
|
||||||
|
cmd_pool: vk::CommandPool::null(),
|
||||||
|
cmd: vk::CommandBuffer::null(),
|
||||||
|
fence: vk::Fence::null(),
|
||||||
|
mem_props,
|
||||||
|
src_cache: HashMap::new(),
|
||||||
|
dst: None,
|
||||||
|
csc: None,
|
||||||
|
};
|
||||||
|
me.cmd_pool = me
|
||||||
|
.device
|
||||||
.create_command_pool(
|
.create_command_pool(
|
||||||
&vk::CommandPoolCreateInfo::default()
|
&vk::CommandPoolCreateInfo::default()
|
||||||
.queue_family_index(qf)
|
.queue_family_index(qf)
|
||||||
@@ -222,33 +260,22 @@ impl VkBridge {
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.context("create command pool")?;
|
.context("create command pool")?;
|
||||||
let cmd = device
|
me.cmd = me
|
||||||
|
.device
|
||||||
.allocate_command_buffers(
|
.allocate_command_buffers(
|
||||||
&vk::CommandBufferAllocateInfo::default()
|
&vk::CommandBufferAllocateInfo::default()
|
||||||
.command_pool(cmd_pool)
|
.command_pool(me.cmd_pool)
|
||||||
.level(vk::CommandBufferLevel::PRIMARY)
|
.level(vk::CommandBufferLevel::PRIMARY)
|
||||||
.command_buffer_count(1),
|
.command_buffer_count(1),
|
||||||
)
|
)
|
||||||
.context("allocate command buffer")?[0];
|
.context("allocate command buffer")?[0];
|
||||||
let fence = device
|
me.fence = me
|
||||||
|
.device
|
||||||
.create_fence(&vk::FenceCreateInfo::default(), None)
|
.create_fence(&vk::FenceCreateInfo::default(), None)
|
||||||
.context("create fence")?;
|
.context("create fence")?;
|
||||||
|
|
||||||
tracing::info!("Vulkan bridge ready (dmabuf import → OPAQUE_FD export → CUDA)");
|
tracing::info!("Vulkan bridge ready (dmabuf import → OPAQUE_FD export → CUDA)");
|
||||||
Ok(VkBridge {
|
Ok(me)
|
||||||
_entry: entry,
|
|
||||||
instance,
|
|
||||||
device,
|
|
||||||
ext_fd,
|
|
||||||
queue,
|
|
||||||
cmd_pool,
|
|
||||||
cmd,
|
|
||||||
fence,
|
|
||||||
mem_props,
|
|
||||||
src_cache: HashMap::new(),
|
|
||||||
dst: None,
|
|
||||||
csc: None,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,16 +473,45 @@ impl VkBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build the RGB→NV12 compute pipeline once (T2.5b): two-SSBO descriptor set + a 28-byte
|
/// Build the RGB→NV12 compute pipeline once (T2.5b): two-SSBO descriptor set + a 28-byte
|
||||||
/// push-constant block matching `rgb2nv12_buf.comp`'s `Push`.
|
/// push-constant block matching `rgb2nv12_buf.comp`'s `Push`. A mid-build failure destroys
|
||||||
|
/// what was already created (the caller retries per frame, so a leak would be unbounded) and
|
||||||
|
/// leaves `self.csc` `None`.
|
||||||
unsafe fn ensure_csc(&mut self) -> Result<()> {
|
unsafe fn ensure_csc(&mut self) -> Result<()> {
|
||||||
if self.csc.is_some() {
|
if self.csc.is_some() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
let mut csc = Csc {
|
||||||
|
module: vk::ShaderModule::null(),
|
||||||
|
dset_layout: vk::DescriptorSetLayout::null(),
|
||||||
|
playout: vk::PipelineLayout::null(),
|
||||||
|
pipeline: vk::Pipeline::null(),
|
||||||
|
dpool: vk::DescriptorPool::null(),
|
||||||
|
dset: vk::DescriptorSet::null(),
|
||||||
|
};
|
||||||
|
if let Err(e) = self.build_csc(&mut csc) {
|
||||||
|
// Reverse creation order; Vulkan destroy calls are defined no-ops on the null
|
||||||
|
// handles a partial build left behind.
|
||||||
|
let d = &self.device;
|
||||||
|
d.destroy_descriptor_pool(csc.dpool, None); // frees `csc.dset` with it
|
||||||
|
d.destroy_pipeline(csc.pipeline, None);
|
||||||
|
d.destroy_pipeline_layout(csc.playout, None);
|
||||||
|
d.destroy_descriptor_set_layout(csc.dset_layout, None);
|
||||||
|
d.destroy_shader_module(csc.module, None);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
self.csc = Some(csc);
|
||||||
|
tracing::info!("Vulkan-bridge NV12 compute CSC ready (LINEAR path feeds NVENC native YUV)");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The fallible half of [`ensure_csc`](Self::ensure_csc): fills `csc` front to back so the
|
||||||
|
/// caller knows exactly what to destroy when a step fails.
|
||||||
|
unsafe fn build_csc(&mut self, csc: &mut Csc) -> Result<()> {
|
||||||
let words: Vec<u32> = CSC_SPV
|
let words: Vec<u32> = CSC_SPV
|
||||||
.chunks_exact(4)
|
.chunks_exact(4)
|
||||||
.map(|c| u32::from_le_bytes(c.try_into().unwrap()))
|
.map(|c| u32::from_le_bytes(c.try_into().unwrap()))
|
||||||
.collect();
|
.collect();
|
||||||
let module = self
|
csc.module = self
|
||||||
.device
|
.device
|
||||||
.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&words), None)
|
.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&words), None)
|
||||||
.context("create CSC shader module")?;
|
.context("create CSC shader module")?;
|
||||||
@@ -471,7 +527,7 @@ impl VkBridge {
|
|||||||
.descriptor_count(1)
|
.descriptor_count(1)
|
||||||
.stage_flags(vk::ShaderStageFlags::COMPUTE),
|
.stage_flags(vk::ShaderStageFlags::COMPUTE),
|
||||||
];
|
];
|
||||||
let dset_layout = self
|
csc.dset_layout = self
|
||||||
.device
|
.device
|
||||||
.create_descriptor_set_layout(
|
.create_descriptor_set_layout(
|
||||||
&vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
|
&vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
|
||||||
@@ -481,8 +537,8 @@ impl VkBridge {
|
|||||||
let pc = [vk::PushConstantRange::default()
|
let pc = [vk::PushConstantRange::default()
|
||||||
.stage_flags(vk::ShaderStageFlags::COMPUTE)
|
.stage_flags(vk::ShaderStageFlags::COMPUTE)
|
||||||
.size(28)];
|
.size(28)];
|
||||||
let layouts = [dset_layout];
|
let layouts = [csc.dset_layout];
|
||||||
let playout = self
|
csc.playout = self
|
||||||
.device
|
.device
|
||||||
.create_pipeline_layout(
|
.create_pipeline_layout(
|
||||||
&vk::PipelineLayoutCreateInfo::default()
|
&vk::PipelineLayoutCreateInfo::default()
|
||||||
@@ -494,22 +550,22 @@ impl VkBridge {
|
|||||||
let entry = c"main";
|
let entry = c"main";
|
||||||
let stage = vk::PipelineShaderStageCreateInfo::default()
|
let stage = vk::PipelineShaderStageCreateInfo::default()
|
||||||
.stage(vk::ShaderStageFlags::COMPUTE)
|
.stage(vk::ShaderStageFlags::COMPUTE)
|
||||||
.module(module)
|
.module(csc.module)
|
||||||
.name(entry);
|
.name(entry);
|
||||||
let pipeline = self
|
csc.pipeline = self
|
||||||
.device
|
.device
|
||||||
.create_compute_pipelines(
|
.create_compute_pipelines(
|
||||||
vk::PipelineCache::null(),
|
vk::PipelineCache::null(),
|
||||||
&[vk::ComputePipelineCreateInfo::default()
|
&[vk::ComputePipelineCreateInfo::default()
|
||||||
.stage(stage)
|
.stage(stage)
|
||||||
.layout(playout)],
|
.layout(csc.playout)],
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.map_err(|(_, e)| anyhow!("create CSC pipeline: {e}"))?[0];
|
.map_err(|(_, e)| anyhow!("create CSC pipeline: {e}"))?[0];
|
||||||
let sizes = [vk::DescriptorPoolSize::default()
|
let sizes = [vk::DescriptorPoolSize::default()
|
||||||
.ty(vk::DescriptorType::STORAGE_BUFFER)
|
.ty(vk::DescriptorType::STORAGE_BUFFER)
|
||||||
.descriptor_count(2)];
|
.descriptor_count(2)];
|
||||||
let dpool = self
|
csc.dpool = self
|
||||||
.device
|
.device
|
||||||
.create_descriptor_pool(
|
.create_descriptor_pool(
|
||||||
&vk::DescriptorPoolCreateInfo::default()
|
&vk::DescriptorPoolCreateInfo::default()
|
||||||
@@ -518,23 +574,14 @@ impl VkBridge {
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.context("create CSC descriptor pool")?;
|
.context("create CSC descriptor pool")?;
|
||||||
let dset = self
|
csc.dset = self
|
||||||
.device
|
.device
|
||||||
.allocate_descriptor_sets(
|
.allocate_descriptor_sets(
|
||||||
&vk::DescriptorSetAllocateInfo::default()
|
&vk::DescriptorSetAllocateInfo::default()
|
||||||
.descriptor_pool(dpool)
|
.descriptor_pool(csc.dpool)
|
||||||
.set_layouts(&layouts),
|
.set_layouts(&layouts),
|
||||||
)
|
)
|
||||||
.context("allocate CSC descriptor set")?[0];
|
.context("allocate CSC descriptor set")?[0];
|
||||||
self.csc = Some(Csc {
|
|
||||||
module,
|
|
||||||
dset_layout,
|
|
||||||
playout,
|
|
||||||
pipeline,
|
|
||||||
dpool,
|
|
||||||
dset,
|
|
||||||
});
|
|
||||||
tracing::info!("Vulkan-bridge NV12 compute CSC ready (LINEAR path feeds NVENC native YUV)");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user