php-io-extensions / vulkan
1:1 PHP bindings of Vulkan: devices, memory, pipelines, commands, swapchains and the Linux dmabuf export
Package info
github.com/php-io-extensions/vulkan
Language:C
Type:php-ext
Ext name:ext-vulkan
pkg:composer/php-io-extensions/vulkan
Requires
- php: >=8.2
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-11 16:25:43 UTC
README
1:1 PHP bindings of Vulkan: instance and device, memory, images, buffers, render
passes, pipelines, descriptors, commands, sync, surfaces and swapchains, and the
Linux dmabuf export GTK needs. Written in C against the Zend API and
<vulkan/vulkan.h>, linked to the system Vulkan loader. PHP 8.4+, NTS and ZTS.
Version 0.10.0.
Functions are global PHP functions under their C names. Enums and flags are PHP constants under their C names, values taken from the headers. Each handle is a final class. Each struct is a final class whose properties are the C members.
Requirements
- The Vulkan loader and headers. Debian and Raspberry Pi OS:
libvulkan-dev. macOS:brew install vulkan-loader vulkan-headers molten-vk. - A GPU device. Tests skip the CPU device (llvmpipe). On macOS the device tests run through MoltenVK once the instance and device enable portability, as in On macOS. The swapchain test and the dmabuf test stay Linux-only.
- The measured Pi is Mesa 26.2 V3DV, Vulkan headers 1.4.309, API 1.3. The Mac headers measured here are 1.4.357.
venusian buildreads the system packages fromextra.venusian.systemin composer.json: apt packages to build with and the run-time ones a.debdepends on or recommends, and the Homebrew ones.
Install
Through PIE:
pie install php-io-extensions/vulkan
From a checkout:
./install-macos.sh # Homebrew php@8.4 and php@8.4-zts, or pass PHP binaries ./install-debian-trixie.sh # Debian trixie, Raspberry Pi OS
./install-macos.sh refuses to start unless pkg-config finds vulkan, and
tells you to brew install vulkan-loader vulkan-headers molten-vk.
Translations
Bindings are 1:1. There are no defaults and no composites. The only translations:
- A pointer + count pair is one PHP list.
- The enumerate-twice calls (
vkEnumerate*,vkGet*Propertieswith a count,vkGetSwapchainImagesKHR) fill a by-reference list. The binding calls twice and retries while the result isVK_INCOMPLETE. - An out-handle or out-struct is a by-reference parameter.
sTypeis not a property. Each struct class fills its own.pNextis?object, a chain of struct objects. A chain that loops is aValueErrorbefore the binding walks it. A struct of the wrong kind for its parent is linked as given.vkGetPhysicalDeviceFeatures2takes theVkPhysicalDeviceFeatures2object and fills it in place, including the structs already linked onpNext. The call does not replace those objects.pAllocatorisnull. There is no custom allocator.VkShaderModuleCreateInfo::$codeis one string.codeSizeis its length. A length that is not a multiple of 4 is aValueError.vkCmdPushConstantstakes a string. A string shorter thansizeis aValueError.- Results are the
VkResultint, as in C. vkMapMemorywrites the mapped address to$ppData(int). PHP reads and writes that address withvk_read_mapped(int $address, int $size): stringandvk_write_mapped(int $address, string $bytes): void. Address 0 is refused. Any other address is trusted.vkFlushMappedMemoryRangesandvkInvalidateMappedMemoryRangesare what make non-coherent memory correct.vkGetMemoryFdKHRhands out a file descriptor the caller owns.vk_close_fd(int $fd): voidcloses it; a descriptor that is not open (closed already) is aValueError.vk_loader_path(): ?stringnames the file the Vulkan loader this extension calls was loaded from (dladdrofvkGetInstanceProcAddr), so another library that loads a loader itself (Qt'sQVulkanInstance) can be pointed at the same one.VkMemoryDedicatedAllocateInfochains onVkMemoryAllocateInfo: an exported image allocated dedicated works on drivers that require it.
VkCopyDescriptorSet and VkBufferMemoryBarrier are not bound.
vkUpdateDescriptorSets refuses a non-empty copy list. vkCmdPipelineBarrier
takes VkMemoryBarriers and VkImageMemoryBarriers and refuses a non-empty
buffer-barrier list; a list holding another class is a TypeError.
Handles and parents
Handles are final, not constructable, not cloneable, not serializable, with
pointer(): int and static fromPointer(int $pointer): static. Address 0 is
refused. One PHP object per class and native value.
Destroying a parent releases every PHP handle whose parent chain reaches it.
After vkDestroyDevice, a VkBuffer or VkQueue from that device throws
ValueError ("VkBuffer has been destroyed") instead of reaching the driver.
vkDestroyCommandPool releases its command buffers. vkDestroyDescriptorPool
releases its descriptor sets. Swapchain images are owned by the swapchain:
vkDestroySwapchainKHR releases them, and PHP does not destroy them.
A surface created outside this extension (SDL's SDL_Vulkan_CreateSurface)
enters through VkSurfaceKHR::fromPointer(). Its parent is unknown, so only
vkDestroySurfaceKHR releases it.
Device selection
Skip VK_PHYSICAL_DEVICE_TYPE_CPU. The integrated or discrete GPU is the
device. Pick a stencil format with vkGetPhysicalDeviceFormatProperties:
VK_FORMAT_D24_UNORM_S8_UINT, otherwise VK_FORMAT_D32_SFLOAT_S8_UINT.
On macOS
Homebrew packages: vulkan-loader, vulkan-headers, molten-vk. The loader
finds MoltenVK through its ICD manifest and lists that device only to an
instance created with VK_KHR_portability_enumeration and
VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR. A device on it enables
VK_KHR_portability_subset when vkEnumerateDeviceExtensionProperties lists
that extension. vkGetPhysicalDeviceFeatures2 with a
VkPhysicalDevicePortabilitySubsetFeaturesKHR chained on
VkPhysicalDeviceFeatures2::$pNext reports what the subset forbids.
triangleFans is one of those flags; read it from the driver.
A surface over ext-metal uses CAMetalLayer::pointer() as
VkMetalSurfaceCreateInfoEXT::$pLayer, with VK_KHR_surface and
VK_EXT_metal_surface enabled on the instance, then vkCreateMetalSurfaceEXT.
Enable portability only where the loader and the device list it. On the Pi neither name is present, so the same code leaves the create infos alone:
$app = new VkApplicationInfo(); $app->pApplicationName = 'ext-vulkan tests'; $app->apiVersion = VK_API_VERSION_1_3; vkEnumerateInstanceExtensionProperties(null, $available); $names = array_map(fn (VkExtensionProperties $e): string => $e->extensionName, $available); $extensions = []; $instanceInfo = new VkInstanceCreateInfo(); $instanceInfo->pApplicationInfo = $app; if (in_array(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, $names, true)) { $extensions[] = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME; $instanceInfo->flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; } $instanceInfo->enabledExtensionNames = $extensions; vkEnumerateDeviceExtensionProperties($physical, null, $deviceExtensions); $deviceNames = array_map(fn (VkExtensionProperties $e): string => $e->extensionName, $deviceExtensions); if (in_array(VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME, $deviceNames, true)) { $deviceInfo->enabledExtensionNames[] = VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME; }
Example
Clear an 8×8 image and copy it out. This skips the CPU device and uses the first graphics queue.
$app = new VkApplicationInfo(); $app->apiVersion = VK_API_VERSION_1_3; $instanceInfo = new VkInstanceCreateInfo(); $instanceInfo->pApplicationInfo = $app; vkCreateInstance($instanceInfo, null, $instance); vkEnumeratePhysicalDevices($instance, $devices); foreach ($devices as $candidate) { vkGetPhysicalDeviceProperties($candidate, $properties); if ($properties->deviceType !== VK_PHYSICAL_DEVICE_TYPE_CPU) { $physical = $candidate; break; } } vkGetPhysicalDeviceQueueFamilyProperties($physical, $families); $family = array_key_first(array_filter( $families, fn (VkQueueFamilyProperties $family): bool => ($family->queueFlags & VK_QUEUE_GRAPHICS_BIT) !== 0, )); $queueInfo = new VkDeviceQueueCreateInfo(); $queueInfo->queueFamilyIndex = $family; $queueInfo->pQueuePriorities = [1.0]; $deviceInfo = new VkDeviceCreateInfo(); $deviceInfo->pQueueCreateInfos = [$queueInfo]; vkCreateDevice($physical, $deviceInfo, null, $device); vkGetDeviceQueue($device, $family, 0, $queue); $imageInfo = new VkImageCreateInfo(); $imageInfo->imageType = VK_IMAGE_TYPE_2D; $imageInfo->format = VK_FORMAT_R8G8B8A8_UNORM; [$imageInfo->extent->width, $imageInfo->extent->height, $imageInfo->extent->depth] = [8, 8, 1]; $imageInfo->mipLevels = 1; $imageInfo->arrayLayers = 1; $imageInfo->samples = VK_SAMPLE_COUNT_1_BIT; $imageInfo->tiling = VK_IMAGE_TILING_OPTIMAL; $imageInfo->usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; vkCreateImage($device, $imageInfo, null, $image);
Map the destination buffer with vkMapMemory, then vk_read_mapped on the
address it wrote. Call vkFlushMappedMemoryRanges after a write and
vkInvalidateMappedMemoryRanges before a read when the memory is not coherent.
Testing
php84 -d memory_limit=128M vendor/bin/pest # Homebrew NTS zhp -d memory_limit=128M vendor/bin/pest # Homebrew ZTS php -d memory_limit=128M vendor/bin/pest # the Pi
On the Pi the swapchain test needs WAYLAND_DISPLAY=wayland-0 and
XDG_RUNTIME_DIR=/run/user/$(id -u). A window appears. The gate draws a
stencil-then-cover triangle into a 4× image, resolves it in the render pass,
blits, copies out, and reads the pixels back. On macOS that gate runs through
MoltenVK, which has VK_FORMAT_D32_SFLOAT_S8_UINT and not
VK_FORMAT_D24_UNORM_S8_UINT.