diff --git a/crates/pf-zerocopy/src/dmabuf_fence.rs b/crates/pf-zerocopy/src/dmabuf_fence.rs index 406d99cc..40d68ad0 100644 --- a/crates/pf-zerocopy/src/dmabuf_fence.rs +++ b/crates/pf-zerocopy/src/dmabuf_fence.rs @@ -11,7 +11,7 @@ //! fd we can `poll()` — readable once the producer's writes complete. This makes zero-copy capture //! race-free WITHOUT the producer doing anything, *iff* the driver actually attaches the fence. If it //! attaches none, the export yields an already-signaled sync_file (poll returns immediately) — no -//! wait, no harm, and [`WaitOutcome::NoFence`] tells us the driver doesn't fence (so zero-copy +//! wait, no harm, and `WaitOutcome::NoFence` tells us the driver doesn't fence (so zero-copy //! would still race). // Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). diff --git a/crates/pf-zerocopy/src/imp/cuda.rs b/crates/pf-zerocopy/src/imp/cuda.rs index 0526e558..5059e4cf 100644 --- a/crates/pf-zerocopy/src/imp/cuda.rs +++ b/crates/pf-zerocopy/src/imp/cuda.rs @@ -249,9 +249,14 @@ fn copy_stream() -> CUstream { /// (the source dmabuf is safe to recycle once this returns), but the wait is scoped to our own /// stream and the copy carries the high priority hint. unsafe fn copy_blocking(copy: &CUDA_MEMCPY2D, what: &str) -> Result<()> { - let stream = copy_stream(); - ck(cuMemcpy2DAsync_v2(copy, stream), what)?; - ck(cuStreamSynchronize(stream), "cuStreamSynchronize") + // SAFETY: caller contract: the shared context is current and `copy` describes live, in-bounds + // device/host memory (each caller carries that proof). Wrapper -> live table; `©` outlives + // the synchronous call. + unsafe { + let stream = copy_stream(); + ck(cuMemcpy2DAsync_v2(copy, stream), what)?; + ck(cuStreamSynchronize(stream), "cuStreamSynchronize") + } } /// Issue `copy` on this thread's priority stream WITHOUT waiting — for stream-ordered consumers @@ -259,7 +264,10 @@ unsafe fn copy_blocking(copy: &CUDA_MEMCPY2D, what: &str) -> Result<()> { /// stream, not the CPU, orders completion, so the SOURCE must stay valid until the downstream /// stream work (the encode) has finished. unsafe fn copy_async(copy: &CUDA_MEMCPY2D, what: &str) -> Result<()> { - ck(cuMemcpy2DAsync_v2(copy, copy_stream()), what) + // SAFETY: caller contract: the shared context is current and `copy` describes live, in-bounds + // device/host memory that stays valid until the stream work completes (each caller carries that + // proof). Wrapper -> live table. + unsafe { ck(cuMemcpy2DAsync_v2(copy, copy_stream()), what) } } /// Block until everything enqueued on THIS THREAD's copy stream completed — the shared tail of @@ -268,16 +276,23 @@ unsafe fn copy_async(copy: &CUDA_MEMCPY2D, what: &str) -> Result<()> { /// game's GPU load each exposed wait eats scheduling latency). The shared context must be /// current. unsafe fn sync_copy_stream() -> Result<()> { - ck(cuStreamSynchronize(copy_stream()), "cuStreamSynchronize") + // SAFETY: caller contract: the shared context is current. Wrapper -> live table; synchronizing + // the dedicated copy stream touches no Rust memory. + unsafe { ck(cuStreamSynchronize(copy_stream()), "cuStreamSynchronize") } } /// `copy_blocking` when `sync`, else `copy_async` — the shared tail of the public `copy_*_to_device` /// helpers, whose `sync: false` mode carries `copy_async`'s source-lifetime contract. unsafe fn copy_issue(copy: &CUDA_MEMCPY2D, what: &str, sync: bool) -> Result<()> { - if sync { - copy_blocking(copy, what) - } else { - copy_async(copy, what) + // SAFETY: caller contract: the shared context is current and `copy` describes live, in-bounds + // memory (each caller carries that proof). The stream handle is the once-created per-process + // copy stream. Wrapper -> live table. + unsafe { + if sync { + copy_blocking(copy, what) + } else { + copy_async(copy, what) + } } } @@ -785,19 +800,24 @@ impl RegisteredTexture { /// The GL context and the shared CUDA context must both be current on this thread, and /// `texture` must be a valid `GL_TEXTURE_2D`. pub unsafe fn register_gl(texture: u32) -> Result { - const GL_TEXTURE_2D: c_uint = 0x0DE1; - const CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY: c_uint = 0x01; - let mut resource: CUgraphicsResource = std::ptr::null_mut(); - ck( - cuGraphicsGLRegisterImage( - &mut resource, - texture, - GL_TEXTURE_2D, - CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY, - ), - "cuGraphicsGLRegisterImage", - )?; - Ok(RegisteredTexture { resource }) + // SAFETY: caller contract: the GL context owning `texture` and the shared CUDA context are + // current on this thread, and `texture` is a live, complete GL texture. The out-param is a + // live stack local; wrapper -> live table. + unsafe { + const GL_TEXTURE_2D: c_uint = 0x0DE1; + const CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY: c_uint = 0x01; + let mut resource: CUgraphicsResource = std::ptr::null_mut(); + ck( + cuGraphicsGLRegisterImage( + &mut resource, + texture, + GL_TEXTURE_2D, + CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY, + ), + "cuGraphicsGLRegisterImage", + )?; + Ok(RegisteredTexture { resource }) + } } /// Map the texture for this frame, copy its (already-linear RGBA8) array into `dst`, then diff --git a/crates/pf-zerocopy/src/imp/cuda/ffi.rs b/crates/pf-zerocopy/src/imp/cuda/ffi.rs index 67c243a3..626aa627 100644 --- a/crates/pf-zerocopy/src/imp/cuda/ffi.rs +++ b/crates/pf-zerocopy/src/imp/cuda/ffi.rs @@ -307,13 +307,19 @@ pub(crate) fn cuda_api() -> Option<&'static CudaApi> { // present; every other entry runs after `context()` succeeded, so its wrapper always hits `Some`. pub(crate) unsafe fn cuInit(flags: c_uint) -> CUresult { match cuda_api() { - Some(a) => (a.cuInit)(flags), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuInit)(flags) }, None => CU_ERROR_NOT_LOADED, } } pub(crate) unsafe fn cuDeviceGet(device: *mut CUdevice, ordinal: c_int) -> CUresult { match cuda_api() { - Some(a) => (a.cuDeviceGet)(device, ordinal), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuDeviceGet)(device, ordinal) }, None => CU_ERROR_NOT_LOADED, } } @@ -323,19 +329,28 @@ pub(crate) unsafe fn cuCtxCreate_v2( dev: CUdevice, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuCtxCreate_v2)(pctx, flags, dev), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuCtxCreate_v2)(pctx, flags, dev) }, None => CU_ERROR_NOT_LOADED, } } pub(crate) unsafe fn cuCtxDestroy_v2(ctx: CUcontext) -> CUresult { match cuda_api() { - Some(a) => (a.cuCtxDestroy_v2)(ctx), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuCtxDestroy_v2)(ctx) }, None => CU_ERROR_NOT_LOADED, } } pub(crate) unsafe fn cuCtxSetCurrent(ctx: CUcontext) -> CUresult { match cuda_api() { - Some(a) => (a.cuCtxSetCurrent)(ctx), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuCtxSetCurrent)(ctx) }, None => CU_ERROR_NOT_LOADED, } } @@ -347,25 +362,39 @@ pub(crate) unsafe fn cuMemAllocPitch_v2( element_size: c_uint, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuMemAllocPitch_v2)(dptr, pitch, width_bytes, height, element_size), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { + (a.cuMemAllocPitch_v2)(dptr, pitch, width_bytes, height, element_size) + }, None => CU_ERROR_NOT_LOADED, } } pub(crate) unsafe fn cuMemFree_v2(dptr: CUdeviceptr) -> CUresult { match cuda_api() { - Some(a) => (a.cuMemFree_v2)(dptr), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuMemFree_v2)(dptr) }, None => CU_ERROR_NOT_LOADED, } } pub(crate) unsafe fn cuMemcpy2DAsync_v2(copy: *const CUDA_MEMCPY2D, stream: CUstream) -> CUresult { match cuda_api() { - Some(a) => (a.cuMemcpy2DAsync_v2)(copy, stream), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuMemcpy2DAsync_v2)(copy, stream) }, None => CU_ERROR_NOT_LOADED, } } pub(crate) unsafe fn cuStreamSynchronize(stream: CUstream) -> CUresult { match cuda_api() { - Some(a) => (a.cuStreamSynchronize)(stream), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuStreamSynchronize)(stream) }, None => CU_ERROR_NOT_LOADED, } } @@ -374,7 +403,10 @@ pub(crate) unsafe fn cuCtxGetStreamPriorityRange( greatest: *mut c_int, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuCtxGetStreamPriorityRange)(least, greatest), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuCtxGetStreamPriorityRange)(least, greatest) }, None => CU_ERROR_NOT_LOADED, } } @@ -384,7 +416,10 @@ pub(crate) unsafe fn cuStreamCreateWithPriority( priority: c_int, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuStreamCreateWithPriority)(stream, flags, priority), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuStreamCreateWithPriority)(stream, flags, priority) }, None => CU_ERROR_NOT_LOADED, } } @@ -395,7 +430,10 @@ pub(crate) unsafe fn cuGraphicsGLRegisterImage( flags: c_uint, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuGraphicsGLRegisterImage)(resource, texture, target, flags), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuGraphicsGLRegisterImage)(resource, texture, target, flags) }, None => CU_ERROR_NOT_LOADED, } } @@ -405,7 +443,10 @@ pub(crate) unsafe fn cuGraphicsMapResources( stream: *mut c_void, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuGraphicsMapResources)(count, resources, stream), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuGraphicsMapResources)(count, resources, stream) }, None => CU_ERROR_NOT_LOADED, } } @@ -415,7 +456,10 @@ pub(crate) unsafe fn cuGraphicsUnmapResources( stream: *mut c_void, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuGraphicsUnmapResources)(count, resources, stream), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuGraphicsUnmapResources)(count, resources, stream) }, None => CU_ERROR_NOT_LOADED, } } @@ -426,13 +470,21 @@ pub(crate) unsafe fn cuGraphicsSubResourceGetMappedArray( mip_level: c_uint, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuGraphicsSubResourceGetMappedArray)(array, resource, array_index, mip_level), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { + (a.cuGraphicsSubResourceGetMappedArray)(array, resource, array_index, mip_level) + }, None => CU_ERROR_NOT_LOADED, } } pub(crate) unsafe fn cuGraphicsUnregisterResource(resource: CUgraphicsResource) -> CUresult { match cuda_api() { - Some(a) => (a.cuGraphicsUnregisterResource)(resource), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuGraphicsUnregisterResource)(resource) }, None => CU_ERROR_NOT_LOADED, } } @@ -441,7 +493,10 @@ pub(crate) unsafe fn cuImportExternalMemory( mem_handle_desc: *const CUDA_EXTERNAL_MEMORY_HANDLE_DESC, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuImportExternalMemory)(ext_mem_out, mem_handle_desc), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuImportExternalMemory)(ext_mem_out, mem_handle_desc) }, None => CU_ERROR_NOT_LOADED, } } @@ -451,13 +506,19 @@ pub(crate) unsafe fn cuExternalMemoryGetMappedBuffer( buffer_desc: *const CUDA_EXTERNAL_MEMORY_BUFFER_DESC, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuExternalMemoryGetMappedBuffer)(dev_ptr, ext_mem, buffer_desc), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuExternalMemoryGetMappedBuffer)(dev_ptr, ext_mem, buffer_desc) }, None => CU_ERROR_NOT_LOADED, } } pub(crate) unsafe fn cuDestroyExternalMemory(ext_mem: CUexternalMemory) -> CUresult { match cuda_api() { - Some(a) => (a.cuDestroyExternalMemory)(ext_mem), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuDestroyExternalMemory)(ext_mem) }, None => CU_ERROR_NOT_LOADED, } } @@ -466,13 +527,19 @@ pub(crate) unsafe fn cuImportExternalSemaphore( sem_handle_desc: *const CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuImportExternalSemaphore)(ext_sem_out, sem_handle_desc), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuImportExternalSemaphore)(ext_sem_out, sem_handle_desc) }, None => CU_ERROR_NOT_LOADED, } } pub(crate) unsafe fn cuDestroyExternalSemaphore(ext_sem: CUexternalSemaphore) -> CUresult { match cuda_api() { - Some(a) => (a.cuDestroyExternalSemaphore)(ext_sem), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuDestroyExternalSemaphore)(ext_sem) }, None => CU_ERROR_NOT_LOADED, } } @@ -483,7 +550,10 @@ pub(crate) unsafe fn cuSignalExternalSemaphoresAsync( stream: CUstream, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuSignalExternalSemaphoresAsync)(ext_sems, params, count, stream), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuSignalExternalSemaphoresAsync)(ext_sems, params, count, stream) }, None => CU_ERROR_NOT_LOADED, } } @@ -494,13 +564,19 @@ pub(crate) unsafe fn cuWaitExternalSemaphoresAsync( stream: CUstream, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuWaitExternalSemaphoresAsync)(ext_sems, params, count, stream), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuWaitExternalSemaphoresAsync)(ext_sems, params, count, stream) }, None => CU_ERROR_NOT_LOADED, } } pub(crate) unsafe fn cuIpcGetMemHandle(handle: *mut CUipcMemHandle, dptr: CUdeviceptr) -> CUresult { match cuda_api() { - Some(a) => (a.cuIpcGetMemHandle)(handle, dptr), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuIpcGetMemHandle)(handle, dptr) }, None => CU_ERROR_NOT_LOADED, } } @@ -510,13 +586,19 @@ pub(crate) unsafe fn cuIpcOpenMemHandle( flags: c_uint, ) -> CUresult { match cuda_api() { - Some(a) => (a.cuIpcOpenMemHandle)(dptr, handle, flags), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuIpcOpenMemHandle)(dptr, handle, flags) }, None => CU_ERROR_NOT_LOADED, } } pub(crate) unsafe fn cuIpcCloseMemHandle(dptr: CUdeviceptr) -> CUresult { match cuda_api() { - Some(a) => (a.cuIpcCloseMemHandle)(dptr), + // SAFETY: forwards this unsafe wrapper's arguments to the live dlopen'd entry point (the + // table is never unloaded — `mem::forget(lib)`); the caller upholds the driver-API contract, + // with the site-specific proof at each call site. + Some(a) => unsafe { (a.cuIpcCloseMemHandle)(dptr) }, None => CU_ERROR_NOT_LOADED, } } diff --git a/crates/pf-zerocopy/src/imp/egl.rs b/crates/pf-zerocopy/src/imp/egl.rs index bbe77bcf..9d6b1a81 100644 --- a/crates/pf-zerocopy/src/imp/egl.rs +++ b/crates/pf-zerocopy/src/imp/egl.rs @@ -179,89 +179,98 @@ struct GlBlit { impl GlBlit { unsafe fn new(width: u32, height: u32) -> Result { - // Declared before every GL name so it drops LAST on unwind — after the CUDA registration - // (declared below) has unregistered itself. - let mut guard = GlNameGuard::default(); - let program = compile_program()?; - guard.programs.push(program); - let mut vao = 0u32; - glGenVertexArrays(1, &mut vao); // core profile needs a bound VAO for glDrawArrays - guard.vaos.push(vao); - let mut fbo = 0u32; - glGenFramebuffers(1, &mut fbo); - guard.fbos.push(fbo); + // SAFETY: caller contract (`import_inner`): the GL context and the shared CUDA context are + // current on this thread. Raw GL calls pass live locals whose pointers outlive each + // synchronous call; every created name is owned by `guard` until the struct exists. + unsafe { + // Declared before every GL name so it drops LAST on unwind — after the CUDA registration + // (declared below) has unregistered itself. + let mut guard = GlNameGuard::default(); + let program = compile_program()?; + guard.programs.push(program); + let mut vao = 0u32; + glGenVertexArrays(1, &mut vao); // core profile needs a bound VAO for glDrawArrays + guard.vaos.push(vao); + let mut fbo = 0u32; + glGenFramebuffers(1, &mut fbo); + guard.fbos.push(fbo); - let mut dst_tex = 0u32; - glGenTextures(1, &mut dst_tex); - guard.textures.push(dst_tex); - glBindTexture(GL_TEXTURE_2D, dst_tex); - glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width as c_int, height as c_int); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + let mut dst_tex = 0u32; + glGenTextures(1, &mut dst_tex); + guard.textures.push(dst_tex); + glBindTexture(GL_TEXTURE_2D, dst_tex); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width as c_int, height as c_int); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - let mut src_tex = 0u32; - glGenTextures(1, &mut src_tex); - guard.textures.push(src_tex); - glBindTexture(GL_TEXTURE_2D, src_tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glBindTexture(GL_TEXTURE_2D, 0); + let mut src_tex = 0u32; + glGenTextures(1, &mut src_tex); + guard.textures.push(src_tex); + glBindTexture(GL_TEXTURE_2D, src_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glBindTexture(GL_TEXTURE_2D, 0); - glBindFramebuffer(GL_FRAMEBUFFER, fbo); - glFramebufferTexture2D( - GL_FRAMEBUFFER, - GL_COLOR_ATTACHMENT0, - GL_TEXTURE_2D, - dst_tex, - 0, - ); - let status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - ensure!( - status == GL_FRAMEBUFFER_COMPLETE, - "blit FBO incomplete ({status:#x})" - ); - // Register the (immutable, reused) destination texture with CUDA once, and stand up the - // device-buffer pool — both per-resolution, not per-frame. Requires the CUDA context to be - // current (the caller makes it current before constructing the blit). - let registered = cuda::RegisteredTexture::register_gl(dst_tex)?; - let pool = cuda::BufferPool::new(width, height)?; - guard.defuse(); - Ok(GlBlit { - program, - vao, - fbo, - dst_tex, - src_tex, - width, - height, - registered, - pool, - }) + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, + dst_tex, + 0, + ); + let status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + ensure!( + status == GL_FRAMEBUFFER_COMPLETE, + "blit FBO incomplete ({status:#x})" + ); + // Register the (immutable, reused) destination texture with CUDA once, and stand up the + // device-buffer pool — both per-resolution, not per-frame. Requires the CUDA context to be + // current (the caller makes it current before constructing the blit). + let registered = cuda::RegisteredTexture::register_gl(dst_tex)?; + let pool = cuda::BufferPool::new(width, height)?; + guard.defuse(); + Ok(GlBlit { + program, + vao, + fbo, + dst_tex, + src_tex, + width, + height, + registered, + pool, + }) + } } /// Bind `image` to the source texture and render it into `dst_tex`. /// /// # Safety: the GL context is current on this thread; `image` is a valid `EGLImage`. unsafe fn run(&self, egl_image_target: EglImageTargetFn, image: *mut c_void) -> Result<()> { - glBindTexture(GL_TEXTURE_2D, self.src_tex); - let _ = glGetError(); - egl_image_target(GL_TEXTURE_2D, image); - let e = glGetError(); - glBindTexture(GL_TEXTURE_2D, 0); - ensure!(e == 0, "glEGLImageTargetTexture2DOES failed ({e:#x})"); + // SAFETY: caller contract (`# Safety` above): GL context current, `image` a valid EGLImage. + // Raw GL calls pass names owned by `self`, created on this same context. + unsafe { + glBindTexture(GL_TEXTURE_2D, self.src_tex); + let _ = glGetError(); + egl_image_target(GL_TEXTURE_2D, image); + let e = glGetError(); + glBindTexture(GL_TEXTURE_2D, 0); + ensure!(e == 0, "glEGLImageTargetTexture2DOES failed ({e:#x})"); - glBindFramebuffer(GL_FRAMEBUFFER, self.fbo); - glViewport(0, 0, self.width as c_int, self.height as c_int); - glUseProgram(self.program); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, self.src_tex); - glBindVertexArray(self.vao); - glDrawArrays(GL_TRIANGLES, 0, 3); - glBindVertexArray(0); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - glFlush(); // submit GL work before CUDA maps the texture - Ok(()) + glBindFramebuffer(GL_FRAMEBUFFER, self.fbo); + glViewport(0, 0, self.width as c_int, self.height as c_int); + glUseProgram(self.program); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, self.src_tex); + glBindVertexArray(self.vao); + glDrawArrays(GL_TRIANGLES, 0, 3); + glBindVertexArray(0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glFlush(); // submit GL work before CUDA maps the texture + Ok(()) + } } } @@ -319,102 +328,111 @@ struct Nv12Blit { impl Nv12Blit { unsafe fn new(width: u32, height: u32) -> Result { - ensure!( - width % 2 == 0 && height % 2 == 0, - "NV12 convert needs even dimensions (got {width}x{height})" - ); - // Declared before every GL name so it drops LAST on unwind — after the CUDA registrations - // (declared below) have unregistered themselves. - let mut guard = GlNameGuard::default(); - let y_program = compile_program_with(FRAG_Y_SRC)?; - guard.programs.push(y_program); - let uv_program = compile_program_with(FRAG_UV_SRC)?; - guard.programs.push(uv_program); - let mut vao = 0u32; - glGenVertexArrays(1, &mut vao); - guard.vaos.push(vao); - let mut fbos = [0u32; 2]; - glGenFramebuffers(2, fbos.as_mut_ptr()); - guard.fbos.extend_from_slice(&fbos); - let (y_fbo, uv_fbo) = (fbos[0], fbos[1]); - - // Luma target: GL_R8 at full resolution. - let mut y_tex = 0u32; - glGenTextures(1, &mut y_tex); - guard.textures.push(y_tex); - glBindTexture(GL_TEXTURE_2D, y_tex); - glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, width as c_int, height as c_int); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - - // Chroma target: GL_RG8 at half resolution (R=U, G=V). - let mut uv_tex = 0u32; - glGenTextures(1, &mut uv_tex); - guard.textures.push(uv_tex); - glBindTexture(GL_TEXTURE_2D, uv_tex); - glTexStorage2D( - GL_TEXTURE_2D, - 1, - GL_RG8, - (width / 2) as c_int, - (height / 2) as c_int, - ); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - - // Source: GL_LINEAR so the half-res UV pass averages the 2×2 chroma footprint. - let mut src_tex = 0u32; - glGenTextures(1, &mut src_tex); - guard.textures.push(src_tex); - glBindTexture(GL_TEXTURE_2D, src_tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glBindTexture(GL_TEXTURE_2D, 0); - - for (fbo, tex) in [(y_fbo, y_tex), (uv_fbo, uv_tex)] { - glBindFramebuffer(GL_FRAMEBUFFER, fbo); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); - let status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - glBindFramebuffer(GL_FRAMEBUFFER, 0); + // SAFETY: caller contract (`import_inner`): the GL context and the shared CUDA context are + // current on this thread. Raw GL calls pass live locals whose pointers outlive each + // synchronous call; every created name is owned by `guard` until the struct exists. + unsafe { ensure!( - status == GL_FRAMEBUFFER_COMPLETE, - "NV12 blit FBO incomplete ({status:#x}) — GL_R8/GL_RG8 not renderable?" + width % 2 == 0 && height % 2 == 0, + "NV12 convert needs even dimensions (got {width}x{height})" ); + // Declared before every GL name so it drops LAST on unwind — after the CUDA registrations + // (declared below) have unregistered themselves. + let mut guard = GlNameGuard::default(); + let y_program = compile_program_with(FRAG_Y_SRC)?; + guard.programs.push(y_program); + let uv_program = compile_program_with(FRAG_UV_SRC)?; + guard.programs.push(uv_program); + let mut vao = 0u32; + glGenVertexArrays(1, &mut vao); + guard.vaos.push(vao); + let mut fbos = [0u32; 2]; + glGenFramebuffers(2, fbos.as_mut_ptr()); + guard.fbos.extend_from_slice(&fbos); + let (y_fbo, uv_fbo) = (fbos[0], fbos[1]); + + // Luma target: GL_R8 at full resolution. + let mut y_tex = 0u32; + glGenTextures(1, &mut y_tex); + guard.textures.push(y_tex); + glBindTexture(GL_TEXTURE_2D, y_tex); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, width as c_int, height as c_int); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + // Chroma target: GL_RG8 at half resolution (R=U, G=V). + let mut uv_tex = 0u32; + glGenTextures(1, &mut uv_tex); + guard.textures.push(uv_tex); + glBindTexture(GL_TEXTURE_2D, uv_tex); + glTexStorage2D( + GL_TEXTURE_2D, + 1, + GL_RG8, + (width / 2) as c_int, + (height / 2) as c_int, + ); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + // Source: GL_LINEAR so the half-res UV pass averages the 2×2 chroma footprint. + let mut src_tex = 0u32; + glGenTextures(1, &mut src_tex); + guard.textures.push(src_tex); + glBindTexture(GL_TEXTURE_2D, src_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glBindTexture(GL_TEXTURE_2D, 0); + + for (fbo, tex) in [(y_fbo, y_tex), (uv_fbo, uv_tex)] { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); + let status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + ensure!( + status == GL_FRAMEBUFFER_COMPLETE, + "NV12 blit FBO incomplete ({status:#x}) — GL_R8/GL_RG8 not renderable?" + ); + } + // Register both convert targets with CUDA once (per-resolution), + the NV12 two-plane pool. + let y_registered = cuda::RegisteredTexture::register_gl(y_tex)?; + let uv_registered = cuda::RegisteredTexture::register_gl(uv_tex)?; + let pool = cuda::BufferPool::new_nv12(width, height)?; + guard.defuse(); + Ok(Nv12Blit { + y_program, + uv_program, + vao, + y_fbo, + uv_fbo, + y_tex, + uv_tex, + src_tex, + width, + height, + y_registered, + uv_registered, + pool, + test_src_storage: false, + }) } - // Register both convert targets with CUDA once (per-resolution), + the NV12 two-plane pool. - let y_registered = cuda::RegisteredTexture::register_gl(y_tex)?; - let uv_registered = cuda::RegisteredTexture::register_gl(uv_tex)?; - let pool = cuda::BufferPool::new_nv12(width, height)?; - guard.defuse(); - Ok(Nv12Blit { - y_program, - uv_program, - vao, - y_fbo, - uv_fbo, - y_tex, - uv_tex, - src_tex, - width, - height, - y_registered, - uv_registered, - pool, - test_src_storage: false, - }) } /// Bind `image` to the source texture and run both convert passes into `y_tex`/`uv_tex`. /// /// # Safety: the GL context is current on this thread; `image` is a valid `EGLImage`. unsafe fn run(&self, egl_image_target: EglImageTargetFn, image: *mut c_void) -> Result<()> { - glBindTexture(GL_TEXTURE_2D, self.src_tex); - let _ = glGetError(); - egl_image_target(GL_TEXTURE_2D, image); - let e = glGetError(); - glBindTexture(GL_TEXTURE_2D, 0); - ensure!(e == 0, "glEGLImageTargetTexture2DOES failed ({e:#x})"); - self.run_passes() + // SAFETY: caller contract (`# Safety` above): GL context current, `image` a valid EGLImage. + // Raw GL calls pass names owned by `self`, created on this same context. + unsafe { + glBindTexture(GL_TEXTURE_2D, self.src_tex); + let _ = glGetError(); + egl_image_target(GL_TEXTURE_2D, image); + let e = glGetError(); + glBindTexture(GL_TEXTURE_2D, 0); + ensure!(e == 0, "glEGLImageTargetTexture2DOES failed ({e:#x})"); + self.run_passes() + } } /// Run the two convert passes from whatever is currently in `src_tex` (caller populated it). @@ -422,25 +440,29 @@ impl Nv12Blit { /// /// # Safety: the GL context is current on this thread. unsafe fn run_passes(&self) -> Result<()> { - glActiveTexture(GL_TEXTURE0); - glBindVertexArray(self.vao); - // Y pass: full-res into the R8 target. - glBindFramebuffer(GL_FRAMEBUFFER, self.y_fbo); - glViewport(0, 0, self.width as c_int, self.height as c_int); - glUseProgram(self.y_program); - glBindTexture(GL_TEXTURE_2D, self.src_tex); - glDrawArrays(GL_TRIANGLES, 0, 3); - // UV pass: half-res into the RG8 target (GL_LINEAR averages the 2×2). - glBindFramebuffer(GL_FRAMEBUFFER, self.uv_fbo); - glViewport(0, 0, (self.width / 2) as c_int, (self.height / 2) as c_int); - glUseProgram(self.uv_program); - glBindTexture(GL_TEXTURE_2D, self.src_tex); - glDrawArrays(GL_TRIANGLES, 0, 3); + // SAFETY: caller contract (`# Safety` above): GL context current. Raw GL calls pass names + // owned by `self`, created on this same context. + unsafe { + glActiveTexture(GL_TEXTURE0); + glBindVertexArray(self.vao); + // Y pass: full-res into the R8 target. + glBindFramebuffer(GL_FRAMEBUFFER, self.y_fbo); + glViewport(0, 0, self.width as c_int, self.height as c_int); + glUseProgram(self.y_program); + glBindTexture(GL_TEXTURE_2D, self.src_tex); + glDrawArrays(GL_TRIANGLES, 0, 3); + // UV pass: half-res into the RG8 target (GL_LINEAR averages the 2×2). + glBindFramebuffer(GL_FRAMEBUFFER, self.uv_fbo); + glViewport(0, 0, (self.width / 2) as c_int, (self.height / 2) as c_int); + glUseProgram(self.uv_program); + glBindTexture(GL_TEXTURE_2D, self.src_tex); + glDrawArrays(GL_TRIANGLES, 0, 3); - glBindVertexArray(0); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - glFlush(); // submit GL work before CUDA maps the textures - Ok(()) + glBindVertexArray(0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glFlush(); // submit GL work before CUDA maps the textures + Ok(()) + } } } @@ -498,100 +520,110 @@ struct Yuv444Blit { impl Yuv444Blit { unsafe fn new(width: u32, height: u32) -> Result { - ensure!( - width % 2 == 0 && height % 2 == 0, - "YUV444 convert needs even dimensions (got {width}x{height})" - ); - let full_range = std::env::var("PUNKTFUNK_444_FULLRANGE").is_ok_and(|v| v.trim() == "1"); - let (y_src, u_src, v_src) = yuv444_frag_sources(full_range); - // Declared before every GL name so it drops LAST on unwind — after the CUDA registrations - // (declared below) have unregistered themselves. - let mut guard = GlNameGuard::default(); - let mut programs = [0u32; 3]; - for (p, src) in programs.iter_mut().zip([&y_src, &u_src, &v_src]) { - *p = compile_program_with(src)?; - guard.programs.push(*p); - } - let mut vao = 0u32; - glGenVertexArrays(1, &mut vao); - guard.vaos.push(vao); - let mut fbos = [0u32; 3]; - glGenFramebuffers(3, fbos.as_mut_ptr()); - guard.fbos.extend_from_slice(&fbos); - let mut texs = [0u32; 3]; - glGenTextures(3, texs.as_mut_ptr()); - guard.textures.extend_from_slice(&texs); - for &tex in &texs { - glBindTexture(GL_TEXTURE_2D, tex); - glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, width as c_int, height as c_int); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - } - // Source: LINEAR is exact at the 1:1 mapping every pass uses (texel centres), matching - // the Nv12Blit source setup. - let mut src_tex = 0u32; - glGenTextures(1, &mut src_tex); - guard.textures.push(src_tex); - glBindTexture(GL_TEXTURE_2D, src_tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glBindTexture(GL_TEXTURE_2D, 0); - for (&fbo, &tex) in fbos.iter().zip(&texs) { - glBindFramebuffer(GL_FRAMEBUFFER, fbo); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); - let status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - glBindFramebuffer(GL_FRAMEBUFFER, 0); + // SAFETY: caller contract (`import_inner`): the GL context and the shared CUDA context are + // current on this thread. Raw GL calls pass live locals whose pointers outlive each + // synchronous call; every created name is owned by `guard` until the struct exists. + unsafe { ensure!( - status == GL_FRAMEBUFFER_COMPLETE, - "YUV444 blit FBO incomplete ({status:#x}) — GL_R8 not renderable?" + width % 2 == 0 && height % 2 == 0, + "YUV444 convert needs even dimensions (got {width}x{height})" ); + let full_range = + std::env::var("PUNKTFUNK_444_FULLRANGE").is_ok_and(|v| v.trim() == "1"); + let (y_src, u_src, v_src) = yuv444_frag_sources(full_range); + // Declared before every GL name so it drops LAST on unwind — after the CUDA registrations + // (declared below) have unregistered themselves. + let mut guard = GlNameGuard::default(); + let mut programs = [0u32; 3]; + for (p, src) in programs.iter_mut().zip([&y_src, &u_src, &v_src]) { + *p = compile_program_with(src)?; + guard.programs.push(*p); + } + let mut vao = 0u32; + glGenVertexArrays(1, &mut vao); + guard.vaos.push(vao); + let mut fbos = [0u32; 3]; + glGenFramebuffers(3, fbos.as_mut_ptr()); + guard.fbos.extend_from_slice(&fbos); + let mut texs = [0u32; 3]; + glGenTextures(3, texs.as_mut_ptr()); + guard.textures.extend_from_slice(&texs); + for &tex in &texs { + glBindTexture(GL_TEXTURE_2D, tex); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, width as c_int, height as c_int); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + // Source: LINEAR is exact at the 1:1 mapping every pass uses (texel centres), matching + // the Nv12Blit source setup. + let mut src_tex = 0u32; + glGenTextures(1, &mut src_tex); + guard.textures.push(src_tex); + glBindTexture(GL_TEXTURE_2D, src_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glBindTexture(GL_TEXTURE_2D, 0); + for (&fbo, &tex) in fbos.iter().zip(&texs) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); + let status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + ensure!( + status == GL_FRAMEBUFFER_COMPLETE, + "YUV444 blit FBO incomplete ({status:#x}) — GL_R8 not renderable?" + ); + } + let registered = [ + cuda::RegisteredTexture::register_gl(texs[0])?, + cuda::RegisteredTexture::register_gl(texs[1])?, + cuda::RegisteredTexture::register_gl(texs[2])?, + ]; + let pool = cuda::BufferPool::new_yuv444(width, height)?; + guard.defuse(); + if full_range { + tracing::info!("YUV444 zero-copy convert: FULL range (PUNKTFUNK_444_FULLRANGE=1)"); + } + Ok(Yuv444Blit { + programs, + vao, + fbos, + texs, + src_tex, + width, + height, + registered, + pool, + }) } - let registered = [ - cuda::RegisteredTexture::register_gl(texs[0])?, - cuda::RegisteredTexture::register_gl(texs[1])?, - cuda::RegisteredTexture::register_gl(texs[2])?, - ]; - let pool = cuda::BufferPool::new_yuv444(width, height)?; - guard.defuse(); - if full_range { - tracing::info!("YUV444 zero-copy convert: FULL range (PUNKTFUNK_444_FULLRANGE=1)"); - } - Ok(Yuv444Blit { - programs, - vao, - fbos, - texs, - src_tex, - width, - height, - registered, - pool, - }) } /// Bind `image` to the source texture and run the three plane passes. /// /// # Safety: the GL context is current on this thread; `image` is a valid `EGLImage`. unsafe fn run(&self, egl_image_target: EglImageTargetFn, image: *mut c_void) -> Result<()> { - glBindTexture(GL_TEXTURE_2D, self.src_tex); - let _ = glGetError(); - egl_image_target(GL_TEXTURE_2D, image); - let e = glGetError(); - glBindTexture(GL_TEXTURE_2D, 0); - ensure!(e == 0, "glEGLImageTargetTexture2DOES failed ({e:#x})"); - glActiveTexture(GL_TEXTURE0); - glBindVertexArray(self.vao); - for (&fbo, &program) in self.fbos.iter().zip(&self.programs) { - glBindFramebuffer(GL_FRAMEBUFFER, fbo); - glViewport(0, 0, self.width as c_int, self.height as c_int); - glUseProgram(program); + // SAFETY: caller contract (`# Safety` above): GL context current, `image` a valid EGLImage. + // Raw GL calls pass names owned by `self`, created on this same context. + unsafe { glBindTexture(GL_TEXTURE_2D, self.src_tex); - glDrawArrays(GL_TRIANGLES, 0, 3); + let _ = glGetError(); + egl_image_target(GL_TEXTURE_2D, image); + let e = glGetError(); + glBindTexture(GL_TEXTURE_2D, 0); + ensure!(e == 0, "glEGLImageTargetTexture2DOES failed ({e:#x})"); + glActiveTexture(GL_TEXTURE0); + glBindVertexArray(self.vao); + for (&fbo, &program) in self.fbos.iter().zip(&self.programs) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glViewport(0, 0, self.width as c_int, self.height as c_int); + glUseProgram(program); + glBindTexture(GL_TEXTURE_2D, self.src_tex); + glDrawArrays(GL_TRIANGLES, 0, 3); + } + glBindVertexArray(0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glFlush(); // submit GL work before CUDA maps the textures + Ok(()) } - glBindVertexArray(0); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - glFlush(); // submit GL work before CUDA maps the textures - Ok(()) } } diff --git a/crates/pf-zerocopy/src/imp/egl/gl.rs b/crates/pf-zerocopy/src/imp/egl/gl.rs index 216ba081..32116f7c 100644 --- a/crates/pf-zerocopy/src/imp/egl/gl.rs +++ b/crates/pf-zerocopy/src/imp/egl/gl.rs @@ -148,60 +148,71 @@ pub(crate) fn yuv444_frag_sources(full_range: bool) -> (Vec, Vec, Vec Result { - let sh = glCreateShader(kind); - ensure!(sh != 0, "glCreateShader failed"); - let ptr = src.as_ptr() as *const i8; - let len = src.len() as c_int; - glShaderSource(sh, 1, &ptr, &len); - glCompileShader(sh); - let mut ok: c_int = 0; - glGetShaderiv(sh, GL_COMPILE_STATUS, &mut ok); - if ok == 0 { - glDeleteShader(sh); - bail!("GL shader compile failed"); + // SAFETY: caller contract: the GL context is current on this thread. `src` is a live slice; its + // pointer/length locals outlive the synchronous `glShaderSource`; the shader name is deleted on + // the compile-failure path. + unsafe { + let sh = glCreateShader(kind); + ensure!(sh != 0, "glCreateShader failed"); + let ptr = src.as_ptr() as *const i8; + let len = src.len() as c_int; + glShaderSource(sh, 1, &ptr, &len); + glCompileShader(sh); + let mut ok: c_int = 0; + glGetShaderiv(sh, GL_COMPILE_STATUS, &mut ok); + if ok == 0 { + glDeleteShader(sh); + bail!("GL shader compile failed"); + } + Ok(sh) } - Ok(sh) } /// Compile+link the fullscreen-triangle program with fragment source `frag` and bind its `image` /// sampler to texture unit 0. pub(crate) unsafe fn compile_program_with(frag: &[u8]) -> Result { - // Callers retry per frame, so every error path below must delete what it created — a leak - // here is unbounded, not one-shot. - let vs = compile_shader(GL_VERTEX_SHADER, VERT_SRC)?; - let fs = match compile_shader(GL_FRAGMENT_SHADER, frag) { - Ok(fs) => fs, - Err(e) => { + // SAFETY: caller contract: the GL context is current on this thread. All names are created here + // and deleted on every failure path; `c"image"` is a valid NUL-terminated literal. + unsafe { + // Callers retry per frame, so every error path below must delete what it created — a leak + // here is unbounded, not one-shot. + let vs = compile_shader(GL_VERTEX_SHADER, VERT_SRC)?; + let fs = match compile_shader(GL_FRAGMENT_SHADER, frag) { + Ok(fs) => fs, + Err(e) => { + glDeleteShader(vs); + return Err(e); + } + }; + let prog = glCreateProgram(); + if prog == 0 { glDeleteShader(vs); - return Err(e); + glDeleteShader(fs); + bail!("glCreateProgram failed"); } - }; - let prog = glCreateProgram(); - if prog == 0 { + glAttachShader(prog, vs); + glAttachShader(prog, fs); + glLinkProgram(prog); glDeleteShader(vs); glDeleteShader(fs); - bail!("glCreateProgram failed"); + let mut ok: c_int = 0; + glGetProgramiv(prog, GL_LINK_STATUS, &mut ok); + if ok == 0 { + glDeleteProgram(prog); + bail!("GL program link failed"); + } + glUseProgram(prog); + let loc = glGetUniformLocation(prog, c"image".as_ptr()); + if loc >= 0 { + glUniform1i(loc, 0); // sampler -> texture unit 0 + } + glUseProgram(0); + Ok(prog) } - glAttachShader(prog, vs); - glAttachShader(prog, fs); - glLinkProgram(prog); - glDeleteShader(vs); - glDeleteShader(fs); - let mut ok: c_int = 0; - glGetProgramiv(prog, GL_LINK_STATUS, &mut ok); - if ok == 0 { - glDeleteProgram(prog); - bail!("GL program link failed"); - } - glUseProgram(prog); - let loc = glGetUniformLocation(prog, c"image".as_ptr()); - if loc >= 0 { - glUniform1i(loc, 0); // sampler -> texture unit 0 - } - glUseProgram(0); - Ok(prog) } pub(crate) unsafe fn compile_program() -> Result { - compile_program_with(FRAG_SRC) + // SAFETY: caller contract: the GL context is current on this thread (forwarded to + // `compile_program_with`). + unsafe { compile_program_with(FRAG_SRC) } } diff --git a/crates/pf-zerocopy/src/imp/vkslot.rs b/crates/pf-zerocopy/src/imp/vkslot.rs index a61426a9..5f369a4e 100644 --- a/crates/pf-zerocopy/src/imp/vkslot.rs +++ b/crates/pf-zerocopy/src/imp/vkslot.rs @@ -797,74 +797,80 @@ impl VkSlotBlend { gx: u32, gy: u32, ) -> Result { - let alloc = self - .slots - .get(id) - .ok_or_else(|| anyhow!("bad slot id {id}"))?; - let d = &self.device; - let cmd = alloc.cmd; - d.begin_command_buffer( - 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/semaphore 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( - cmd, - vk::PipelineStageFlags::TOP_OF_PIPE, - vk::PipelineStageFlags::COMPUTE_SHADER, - vk::DependencyFlags::empty(), - &acquire, - &[], - &[], - ); - d.cmd_bind_pipeline( - cmd, - vk::PipelineBindPoint::COMPUTE, - self.pipelines[fmt.mode() as usize], - ); - d.cmd_bind_descriptor_sets( - cmd, - vk::PipelineBindPoint::COMPUTE, - self.pipe_layout, - 0, - &[alloc.desc], - &[], - ); - let bytes = std::slice::from_raw_parts( - (push as *const Push) as *const u8, - std::mem::size_of::(), - ); - d.cmd_push_constants( - cmd, - self.pipe_layout, - vk::ShaderStageFlags::COMPUTE, - 0, - bytes, - ); - d.cmd_dispatch(cmd, gx.max(1), gy.max(1), 1); - // Release the shader's writes so the downstream CUDA/NVENC reads (fence- or - // semaphore-ordered) 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( - cmd, - vk::PipelineStageFlags::COMPUTE_SHADER, - vk::PipelineStageFlags::BOTTOM_OF_PIPE, - vk::DependencyFlags::empty(), - &release, - &[], - &[], - ); - d.end_command_buffer(cmd).context("end blend cmd")?; - Ok(cmd) + // SAFETY: caller contract (`# Safety` above): the slot's previous submission completed, so + // its command buffer is re-recordable. Handles are owned by `self` on this single thread; + // `bytes` reborrows `push` (repr(C), size_of::()) for the synchronous push-constant + // copy; builder infos are locals outliving each call. + unsafe { + let alloc = self + .slots + .get(id) + .ok_or_else(|| anyhow!("bad slot id {id}"))?; + let d = &self.device; + let cmd = alloc.cmd; + d.begin_command_buffer( + 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/semaphore 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( + cmd, + vk::PipelineStageFlags::TOP_OF_PIPE, + vk::PipelineStageFlags::COMPUTE_SHADER, + vk::DependencyFlags::empty(), + &acquire, + &[], + &[], + ); + d.cmd_bind_pipeline( + cmd, + vk::PipelineBindPoint::COMPUTE, + self.pipelines[fmt.mode() as usize], + ); + d.cmd_bind_descriptor_sets( + cmd, + vk::PipelineBindPoint::COMPUTE, + self.pipe_layout, + 0, + &[alloc.desc], + &[], + ); + let bytes = std::slice::from_raw_parts( + (push as *const Push) as *const u8, + std::mem::size_of::(), + ); + d.cmd_push_constants( + cmd, + self.pipe_layout, + vk::ShaderStageFlags::COMPUTE, + 0, + bytes, + ); + d.cmd_dispatch(cmd, gx.max(1), gy.max(1), 1); + // Release the shader's writes so the downstream CUDA/NVENC reads (fence- or + // semaphore-ordered) 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( + cmd, + vk::PipelineStageFlags::COMPUTE_SHADER, + vk::PipelineStageFlags::BOTTOM_OF_PIPE, + vk::DependencyFlags::empty(), + &release, + &[], + &[], + ); + d.end_command_buffer(cmd).context("end blend cmd")?; + Ok(cmd) + } } /// Blend the uploaded cursor into `slot` at `(ox, oy)`: record, submit, fence-wait. The diff --git a/crates/pf-zerocopy/src/imp/vulkan.rs b/crates/pf-zerocopy/src/imp/vulkan.rs index 807183d2..08157bc4 100644 --- a/crates/pf-zerocopy/src/imp/vulkan.rs +++ b/crates/pf-zerocopy/src/imp/vulkan.rs @@ -292,184 +292,197 @@ impl VkBridge { /// Import `fd` (dup'd internally; Vulkan owns the dup) as a transfer-src buffer of `size`. unsafe fn import_src(&mut self, fd: i32, size: u64) -> Result<()> { - use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd}; - let dup = libc::dup(fd); - if dup < 0 { - bail!("dup(dmabuf fd)"); - } - // Own the dup so every early return BEFORE Vulkan consumes it (at `allocate_memory` success) - // closes it. `SrcBuf` holds raw handles with no Drop and is only populated on the success - // path, so each fallible step below must also destroy the buffer it created — otherwise a - // failed import (which the worker survives and the caller retries every frame) leaks a - // VkBuffer + VkDeviceMemory + fd per frame for the worker's whole lifetime. - let dup = OwnedFd::from_raw_fd(dup); - let mut ext_info = vk::ExternalMemoryBufferCreateInfo::default() - .handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT); - let buffer = self - .device - .create_buffer( - &vk::BufferCreateInfo::default() - .size(size) - // STORAGE so the NV12 compute CSC can read it as an SSBO (T2.5b); harmless - // for the plain copy path. - .usage( - vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::STORAGE_BUFFER, - ) - .push_next(&mut ext_info), - None, - ) - .context("create import buffer")?; // `dup` drops → closes on failure - let mut fd_props = vk::MemoryFdPropertiesKHR::default(); - if let Err(e) = self.ext_fd.get_memory_fd_properties( - vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT, - dup.as_raw_fd(), - &mut fd_props, - ) { - self.device.destroy_buffer(buffer, None); - return Err(e).context("vkGetMemoryFdPropertiesKHR"); - } - let reqs = self.device.get_buffer_memory_requirements(buffer); - let mem_type = match self.memory_type( - reqs.memory_type_bits & fd_props.memory_type_bits, - vk::MemoryPropertyFlags::empty(), - ) { - Ok(t) => t, - Err(e) => { - self.device.destroy_buffer(buffer, None); - return Err(e); + // SAFETY: caller contract: single-threaded use of handles this bridge owns; every builder + // info is built from locals that outlive the synchronous call reading them, and every + // fallible step destroys what it created before returning (comments below). + unsafe { + use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd}; + let dup = libc::dup(fd); + if dup < 0 { + bail!("dup(dmabuf fd)"); } - }; - // Vulkan takes ownership of the fd on a SUCCESSFUL import: hand over the raw fd now, and on - // failure close it ourselves (matching the original contract) plus destroy the buffer. - let raw = dup.into_raw_fd(); - let mut import = vk::ImportMemoryFdInfoKHR::default() - .handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT) - .fd(raw); - let mut dedicated = vk::MemoryDedicatedAllocateInfo::default().buffer(buffer); - let memory = match self.device.allocate_memory( - &vk::MemoryAllocateInfo::default() - .allocation_size(reqs.size.max(size)) - .memory_type_index(mem_type) - .push_next(&mut import) - .push_next(&mut dedicated), - None, - ) { - Ok(m) => m, - Err(e) => { - libc::close(raw); // failed import does not consume the fd + // Own the dup so every early return BEFORE Vulkan consumes it (at `allocate_memory` success) + // closes it. `SrcBuf` holds raw handles with no Drop and is only populated on the success + // path, so each fallible step below must also destroy the buffer it created — otherwise a + // failed import (which the worker survives and the caller retries every frame) leaks a + // VkBuffer + VkDeviceMemory + fd per frame for the worker's whole lifetime. + let dup = OwnedFd::from_raw_fd(dup); + let mut ext_info = vk::ExternalMemoryBufferCreateInfo::default() + .handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT); + let buffer = self + .device + .create_buffer( + &vk::BufferCreateInfo::default() + .size(size) + // STORAGE so the NV12 compute CSC can read it as an SSBO (T2.5b); harmless + // for the plain copy path. + .usage( + vk::BufferUsageFlags::TRANSFER_SRC + | vk::BufferUsageFlags::STORAGE_BUFFER, + ) + .push_next(&mut ext_info), + None, + ) + .context("create import buffer")?; // `dup` drops → closes on failure + let mut fd_props = vk::MemoryFdPropertiesKHR::default(); + if let Err(e) = self.ext_fd.get_memory_fd_properties( + vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT, + dup.as_raw_fd(), + &mut fd_props, + ) { self.device.destroy_buffer(buffer, None); - return Err(anyhow!("import dmabuf memory: {e}")); + return Err(e).context("vkGetMemoryFdPropertiesKHR"); } - }; - if let Err(e) = self.device.bind_buffer_memory(buffer, memory, 0) { - // `memory` owns the imported fd — freeing it releases the fd too. - self.device.free_memory(memory, None); - self.device.destroy_buffer(buffer, None); - return Err(e).context("bind import memory"); - } - self.src_cache.insert( - fd, - SrcBuf { - buffer, - memory, - size, - }, - ); - Ok(()) - } - - /// (Re)create the exportable destination of at least `size` bytes + its CUDA mapping. - unsafe fn ensure_dst(&mut self, size: u64) -> Result<()> { - if self.dst.as_ref().is_some_and(|d| d.size >= size) { - return Ok(()); - } - // Build the replacement FULLY before retiring the old one. Previously the old dst was - // destroyed and `self.dst` nulled up front, so a failed rebuild both dropped the working - // buffer AND leaked every object the partial rebuild created (`buffer`/`memory` are raw ash - // handles with no Drop, and `VkBridge::drop` only frees the live `self.dst`). Now every - // fallible step unwinds locally, and the swap happens only on full success. - let mut ext_info = vk::ExternalMemoryBufferCreateInfo::default() - .handle_types(vk::ExternalMemoryHandleTypeFlags::OPAQUE_FD); - let buffer = self - .device - .create_buffer( - &vk::BufferCreateInfo::default() - .size(size) - // STORAGE so the NV12 compute CSC can write it as an SSBO (T2.5b). - .usage( - vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::STORAGE_BUFFER, - ) - .push_next(&mut ext_info), - None, - ) - .context("create export buffer")?; - let reqs = self.device.get_buffer_memory_requirements(buffer); - let mem_type = - match self.memory_type(reqs.memory_type_bits, vk::MemoryPropertyFlags::DEVICE_LOCAL) { + let reqs = self.device.get_buffer_memory_requirements(buffer); + let mem_type = match self.memory_type( + reqs.memory_type_bits & fd_props.memory_type_bits, + vk::MemoryPropertyFlags::empty(), + ) { Ok(t) => t, Err(e) => { self.device.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 self.device.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) => { - self.device.destroy_buffer(buffer, None); - return Err(e).context("allocate exportable memory"); - } - }; - if let Err(e) = self.device.bind_buffer_memory(buffer, memory, 0) { - self.device.free_memory(memory, None); - self.device.destroy_buffer(buffer, None); - return Err(e).context("bind export memory"); - } - let opaque_fd = match self.ext_fd.get_memory_fd( - &vk::MemoryGetFdInfoKHR::default() - .memory(memory) - .handle_type(vk::ExternalMemoryHandleTypeFlags::OPAQUE_FD), - ) { - Ok(f) => f, - Err(e) => { + // Vulkan takes ownership of the fd on a SUCCESSFUL import: hand over the raw fd now, and on + // failure close it ourselves (matching the original contract) plus destroy the buffer. + let raw = dup.into_raw_fd(); + let mut import = vk::ImportMemoryFdInfoKHR::default() + .handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT) + .fd(raw); + let mut dedicated = vk::MemoryDedicatedAllocateInfo::default().buffer(buffer); + let memory = match self.device.allocate_memory( + &vk::MemoryAllocateInfo::default() + .allocation_size(reqs.size.max(size)) + .memory_type_index(mem_type) + .push_next(&mut import) + .push_next(&mut dedicated), + None, + ) { + Ok(m) => m, + Err(e) => { + libc::close(raw); // failed import does not consume the fd + self.device.destroy_buffer(buffer, None); + return Err(anyhow!("import dmabuf memory: {e}")); + } + }; + if let Err(e) = self.device.bind_buffer_memory(buffer, memory, 0) { + // `memory` owns the imported fd — freeing it releases the fd too. self.device.free_memory(memory, None); self.device.destroy_buffer(buffer, None); - return Err(e).context("vkGetMemoryFdKHR"); + return Err(e).context("bind import memory"); } - }; - // CUDA imports (and on success owns) the exported fd. Size must match the allocation. - // `import_owned_fd` closes `opaque_fd` on its own failure, so only the Vulkan objects unwind. - let cuda = match cuda::ExternalDmabuf::import_owned_fd(opaque_fd, reqs.size) { - Ok(c) => c, - Err(e) => { + self.src_cache.insert( + fd, + SrcBuf { + buffer, + memory, + size, + }, + ); + Ok(()) + } + } + + /// (Re)create the exportable destination of at least `size` bytes + its CUDA mapping. + unsafe fn ensure_dst(&mut self, size: u64) -> Result<()> { + // SAFETY: caller contract: single-threaded use of handles this bridge owns; every builder + // info is built from locals that outlive the synchronous call reading them; created handles + // are destroyed on the error paths below or owned by `DstBuf`. + unsafe { + if self.dst.as_ref().is_some_and(|d| d.size >= size) { + return Ok(()); + } + // Build the replacement FULLY before retiring the old one. Previously the old dst was + // destroyed and `self.dst` nulled up front, so a failed rebuild both dropped the working + // buffer AND leaked every object the partial rebuild created (`buffer`/`memory` are raw ash + // handles with no Drop, and `VkBridge::drop` only frees the live `self.dst`). Now every + // fallible step unwinds locally, and the swap happens only on full success. + let mut ext_info = vk::ExternalMemoryBufferCreateInfo::default() + .handle_types(vk::ExternalMemoryHandleTypeFlags::OPAQUE_FD); + let buffer = self + .device + .create_buffer( + &vk::BufferCreateInfo::default() + .size(size) + // STORAGE so the NV12 compute CSC can write it as an SSBO (T2.5b). + .usage( + vk::BufferUsageFlags::TRANSFER_DST + | vk::BufferUsageFlags::STORAGE_BUFFER, + ) + .push_next(&mut ext_info), + None, + ) + .context("create export buffer")?; + let reqs = self.device.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) => { + self.device.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 self.device.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) => { + self.device.destroy_buffer(buffer, None); + return Err(e).context("allocate exportable memory"); + } + }; + if let Err(e) = self.device.bind_buffer_memory(buffer, memory, 0) { self.device.free_memory(memory, None); self.device.destroy_buffer(buffer, None); - return Err(e).context("cuImportExternalMemory(OPAQUE_FD from Vulkan)"); + return Err(e).context("bind export memory"); } - }; - // Full success: retire the previous buffer now, then publish the new one. - if let Some(old) = self.dst.take() { - self.device.destroy_buffer(old.buffer, None); - self.device.free_memory(old.memory, None); - // old.cuda drops its mapping with it + let opaque_fd = match self.ext_fd.get_memory_fd( + &vk::MemoryGetFdInfoKHR::default() + .memory(memory) + .handle_type(vk::ExternalMemoryHandleTypeFlags::OPAQUE_FD), + ) { + Ok(f) => f, + Err(e) => { + self.device.free_memory(memory, None); + self.device.destroy_buffer(buffer, None); + return Err(e).context("vkGetMemoryFdKHR"); + } + }; + // CUDA imports (and on success owns) the exported fd. Size must match the allocation. + // `import_owned_fd` closes `opaque_fd` on its own failure, so only the Vulkan objects unwind. + let cuda = match cuda::ExternalDmabuf::import_owned_fd(opaque_fd, reqs.size) { + Ok(c) => c, + Err(e) => { + self.device.free_memory(memory, None); + self.device.destroy_buffer(buffer, None); + return Err(e).context("cuImportExternalMemory(OPAQUE_FD from Vulkan)"); + } + }; + // Full success: retire the previous buffer now, then publish the new one. + if let Some(old) = self.dst.take() { + self.device.destroy_buffer(old.buffer, None); + self.device.free_memory(old.memory, None); + // old.cuda drops its mapping with it + } + tracing::info!(size, "Vulkan→CUDA exportable staging buffer ready"); + self.dst = Some(DstBuf { + buffer, + memory, + size: reqs.size, + cuda, + }); + Ok(()) } - tracing::info!(size, "Vulkan→CUDA exportable staging buffer ready"); - self.dst = Some(DstBuf { - buffer, - memory, - size: reqs.size, - cuda, - }); - Ok(()) } /// Build the RGB→NV12 compute pipeline once (T2.5b): two-SSBO descriptor set + a 28-byte @@ -477,112 +490,124 @@ impl VkBridge { /// 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<()> { - if self.csc.is_some() { - return Ok(()); + // SAFETY: caller contract: single-threaded use of handles this bridge owns. `build_csc` + // fills `csc` front to back; on failure the destroy calls below run in reverse creation + // order, and Vulkan destroys are defined no-ops on the null handles a partial build left. + unsafe { + if self.csc.is_some() { + 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(()) } - 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 = CSC_SPV - .chunks_exact(4) - .map(|c| u32::from_le_bytes(c.try_into().unwrap())) - .collect(); - csc.module = self - .device - .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&words), None) - .context("create CSC shader module")?; - 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), - ]; - csc.dset_layout = self - .device - .create_descriptor_set_layout( - &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings), - None, - ) - .context("create CSC dset layout")?; - let pc = [vk::PushConstantRange::default() - .stage_flags(vk::ShaderStageFlags::COMPUTE) - .size(28)]; - let layouts = [csc.dset_layout]; - csc.playout = self - .device - .create_pipeline_layout( - &vk::PipelineLayoutCreateInfo::default() - .set_layouts(&layouts) - .push_constant_ranges(&pc), - None, - ) - .context("create CSC pipeline layout")?; - let entry = c"main"; - let stage = vk::PipelineShaderStageCreateInfo::default() - .stage(vk::ShaderStageFlags::COMPUTE) - .module(csc.module) - .name(entry); - csc.pipeline = self - .device - .create_compute_pipelines( - vk::PipelineCache::null(), - &[vk::ComputePipelineCreateInfo::default() - .stage(stage) - .layout(csc.playout)], - None, - ) - .map_err(|(_, e)| anyhow!("create CSC pipeline: {e}"))?[0]; - let sizes = [vk::DescriptorPoolSize::default() - .ty(vk::DescriptorType::STORAGE_BUFFER) - .descriptor_count(2)]; - csc.dpool = self - .device - .create_descriptor_pool( - &vk::DescriptorPoolCreateInfo::default() - .max_sets(1) - .pool_sizes(&sizes), - None, - ) - .context("create CSC descriptor pool")?; - csc.dset = self - .device - .allocate_descriptor_sets( - &vk::DescriptorSetAllocateInfo::default() - .descriptor_pool(csc.dpool) - .set_layouts(&layouts), - ) - .context("allocate CSC descriptor set")?[0]; - Ok(()) + // SAFETY: caller contract (via `ensure_csc`): single-threaded use of handles this bridge + // owns; every builder info is built from locals that outlive the synchronous call reading + // them; partial results land in `csc` for the caller to destroy on failure. + unsafe { + let words: Vec = CSC_SPV + .chunks_exact(4) + .map(|c| u32::from_le_bytes(c.try_into().unwrap())) + .collect(); + csc.module = self + .device + .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&words), None) + .context("create CSC shader module")?; + 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), + ]; + csc.dset_layout = self + .device + .create_descriptor_set_layout( + &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings), + None, + ) + .context("create CSC dset layout")?; + let pc = [vk::PushConstantRange::default() + .stage_flags(vk::ShaderStageFlags::COMPUTE) + .size(28)]; + let layouts = [csc.dset_layout]; + csc.playout = self + .device + .create_pipeline_layout( + &vk::PipelineLayoutCreateInfo::default() + .set_layouts(&layouts) + .push_constant_ranges(&pc), + None, + ) + .context("create CSC pipeline layout")?; + let entry = c"main"; + let stage = vk::PipelineShaderStageCreateInfo::default() + .stage(vk::ShaderStageFlags::COMPUTE) + .module(csc.module) + .name(entry); + csc.pipeline = self + .device + .create_compute_pipelines( + vk::PipelineCache::null(), + &[vk::ComputePipelineCreateInfo::default() + .stage(stage) + .layout(csc.playout)], + None, + ) + .map_err(|(_, e)| anyhow!("create CSC pipeline: {e}"))?[0]; + let sizes = [vk::DescriptorPoolSize::default() + .ty(vk::DescriptorType::STORAGE_BUFFER) + .descriptor_count(2)]; + csc.dpool = self + .device + .create_descriptor_pool( + &vk::DescriptorPoolCreateInfo::default() + .max_sets(1) + .pool_sizes(&sizes), + None, + ) + .context("create CSC descriptor pool")?; + csc.dset = self + .device + .allocate_descriptor_sets( + &vk::DescriptorSetAllocateInfo::default() + .descriptor_pool(csc.dpool) + .set_layouts(&layouts), + ) + .context("allocate CSC descriptor set")?[0]; + Ok(()) + } } /// Bridge one LINEAR dmabuf frame into a pooled NV12 CUDA buffer (latency plan T2.5b): diff --git a/crates/pf-zerocopy/src/lib.rs b/crates/pf-zerocopy/src/lib.rs index 150804e4..8f6d1eee 100644 --- a/crates/pf-zerocopy/src/lib.rs +++ b/crates/pf-zerocopy/src/lib.rs @@ -10,7 +10,11 @@ // Unsafe-proof program: every `unsafe {}` / `unsafe impl` must carry a `// SAFETY:` proof. Each // file keeps its own `#![deny(...)]` too; this crate-root deny is the catch-all gate. +// `unsafe_op_in_unsafe_fn` closes the gap the clippy lint leaves: operations inside an +// `unsafe fn` body are not "unsafe blocks", so without it ~45 functions' worth of raw driver +// calls sat OUTSIDE the invariant this crate advertises. #![deny(clippy::undocumented_unsafe_blocks)] +#![deny(unsafe_op_in_unsafe_fn)] /// Wait for a dmabuf's implicit read-ready fence (`DMA_BUF_IOCTL_EXPORT_SYNC_FILE` + poll). #[cfg(target_os = "linux")]