NT-handle import: consume on SUCCESS only — PUNKTFUNK LOCAL PATCH. Not upstream. Granite's DeviceAllocator::internal_allocate closed the imported by-reference NT handle (OPAQUE_WIN32 / D3D11_TEXTURE / D3D12_RESOURCE — the predicate excludes fd and KMT types) UNCONDITIONALLY after the FIRST vkAllocateMemory — on success AND on failure — while every failure before the allocator (c-shim validation, vkCreateImage, find_memory_type) left the handle open. Both classes surface to pyrowave_image_create's caller as the same error code, so the caller could not know whether to close: punktfunk's import_plane closed on every failure and double-closed whenever the failure was at or after the allocate — and a recycled handle value makes that close an unrelated live handle's. The unconditional consume also broke the allocator's own block-recycling retry loop, which re-ran vkAllocateMemory with import_info still pointing at the just-closed handle. Fixed by moving the consume to the API commit point: pyrowave_image_create closes the by-reference handle exactly on its success return. This pins pyrowave.h's documented contract ("take ownership and close the HANDLE on import") to mean on SUCCESS, matching pyrowave_sync_object_create (Granite's semaphore import already closes only after a successful vkImportSemaphore*). NT-handle memory imports never transfer ownership at the Vulkan level (the implementation holds its own reference), so the post-create close is legal. Behavior note: Granite code that imports by-reference memory WITHOUT going through pyrowave_image_create — in this vendored subset only wsi_dxgi.cpp's interop swapchain, which the pyrowave C API never reaches — now keeps the caller's handle open on allocate-stage failure instead of consuming it. That matches the semaphore contract; those callers own their close-on-failure. Correctness re-validated by pyrowave_win_smoke on real hardware after the change. diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/vulkan/memory_allocator.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/vulkan/memory_allocator.cpp index f33bdac3..fd035b66 100644 --- a/crates/pyrowave-sys/vendor/pyrowave/Granite/vulkan/memory_allocator.cpp +++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/vulkan/memory_allocator.cpp @@ -732,16 +732,15 @@ bool DeviceAllocator::internal_allocate( res = table->vkAllocateMemory(device->get_device(), &info, nullptr, &device_memory); } - // If we're importing, make sure we consume the native handle. - if (external && bool(*external) && - ExternalHandle::memory_handle_type_imports_by_reference(external->memory_handle_type)) - { -#ifdef _WIN32 - ::CloseHandle(external->handle); -#else - ::close(external->handle); -#endif - } + // PUNKTFUNK (patch 0006): the consume of by-reference native handles used to happen HERE, + // unconditionally — success AND failure of the first vkAllocateMemory. That made the caller's + // failure contract ambiguous (an allocate-stage failure had already closed the handle while a + // create/find_memory_type failure had not), and the block-recycling retry loop below re-ran + // vkAllocateMemory with import_info still pointing at the just-closed handle. The consume now + // lives at the API commit point (pyrowave_c.cpp: pyrowave_image_create's success return), so + // the allocator never closes a caller's handle and retries import a still-open handle. + // (fd-type imports were never affected: memory_handle_type_imports_by_reference excludes + // OPAQUE_FD/DMA_BUF, whose ownership vkAllocateMemory itself transfers on success.) if (res == VK_SUCCESS) { diff --git a/crates/pyrowave-sys/vendor/pyrowave/pyrowave_c.cpp b/crates/pyrowave-sys/vendor/pyrowave/pyrowave_c.cpp index eb917e64..985cd0a9 100644 --- a/crates/pyrowave-sys/vendor/pyrowave/pyrowave_c.cpp +++ b/crates/pyrowave-sys/vendor/pyrowave/pyrowave_c.cpp @@ -592,6 +592,21 @@ pyrowave_result pyrowave_image_create(const pyrowave_image_create_info *info, py if (!img) return PYROWAVE_ERROR_FAILED_EXTERNAL_HANDLE; + // PUNKTFUNK (patch 0006): consume the imported by-reference NT handle exactly here, at the + // API's success boundary — the allocator no longer closes it (see memory_allocator.cpp). + // This pins pyrowave.h's documented contract ("take ownership and close the HANDLE on + // import") to mean ON SUCCESS, matching pyrowave_sync_object_create's semantics: on ANY + // failure return the caller still owns the handle and can close it unconditionally. + // By-reference NT-handle imports never transfer ownership at the Vulkan level (the + // implementation keeps its own reference), so a post-create close here is always legal. +#ifdef _WIN32 + if (info->external_handle && + ExternalHandle::memory_handle_type_imports_by_reference(info->handle_type)) + { + ::CloseHandle(reinterpret_cast(info->external_handle)); + } +#endif + auto *image = new pyrowave_image_opaque(); image->device = &device; image->img = std::move(img);