Source code for simvx.graphics.gpu.device

"""Physical/logical device selection and queue management."""

import logging
from dataclasses import dataclass
from typing import Any

import vulkan as vk

from .capabilities import probe_device_features

__all__ = ["select_physical_device", "create_logical_device", "QueueFamilies"]

log = logging.getLogger(__name__)

[docs] @dataclass class QueueFamilies: graphics: int present: int
def _find_queue_families(physical_device: Any, surface: Any, vk_surface_support: Any) -> QueueFamilies | None: props = vk.vkGetPhysicalDeviceQueueFamilyProperties(physical_device) graphics = present = -1 for i, p in enumerate(props): if p.queueFlags & vk.VK_QUEUE_GRAPHICS_BIT: graphics = i if vk_surface_support(physical_device, i, surface): present = i if graphics >= 0 and present >= 0: return QueueFamilies(graphics, present) return None
[docs] def select_physical_device(instance: Any, surface: Any) -> tuple[Any, QueueFamilies]: """Pick a suitable VkPhysicalDevice. Returns (physical_device, queue_families).""" vk_surface_support = vk.vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceSurfaceSupportKHR") devices = vk.vkEnumeratePhysicalDevices(instance) if not devices: raise RuntimeError("No Vulkan-capable GPU found") for dev in devices: props = vk.vkGetPhysicalDeviceProperties(dev) qf = _find_queue_families(dev, surface, vk_surface_support) if qf is None: continue name = props.deviceName if isinstance(props.deviceName, str) else props.deviceName.decode("utf-8") log.debug("Selected GPU: %s", name) return dev, qf raise RuntimeError("No suitable GPU with graphics+present queues found")
[docs] def create_logical_device( physical_device: Any, queue_families: QueueFamilies, *, compute_qf: int | None = None, transfer_qf: int | None = None, external_memory_fd: bool = False, ) -> tuple[Any, Any, Any, Any, Any]: """Create a VkDevice. Returns ``(device, graphics_queue, present_queue, compute_queue, transfer_queue)``. ``compute_qf`` / ``transfer_qf`` are the *dedicated* queue families probed by :func:`probe_dedicated_queue_families` (``RenderCapabilities.dedicated_*_qf``). When supplied (the async-compute / dedicated-DMA path on multi-queue GPUs), a queue is created on each and its handle returned; otherwise the corresponding return is ``None``. A family that coincides with graphics or present is ignored (it is already covered by the universal queue), so on a single-universal-family box (``compute_qf=transfer_qf=None``) the queue set is exactly graphics+present as before: byte-identical device creation. ``external_memory_fd`` adds ``VK_KHR_external_memory_fd`` (and the ``VK_KHR_external_memory`` dependency) to the enabled device extensions. It is requested only by the D8 multi-GPU path so the dma-buf zero-copy cross-device transfer can later export/import image memory as an fd; the staging-copy floor needs nothing extra. Default ``False`` keeps single-GPU device creation byte-identical. The extension must be *enabled* here (not merely probed) for :class:`~simvx.graphics.gpu.multi_device.TransferMethod.DMABUF` selection to be valid; until both devices enable it the staging-copy floor is chosen. """ unique_families = {queue_families.graphics, queue_families.present} # Only add a *new* family. A dedicated family equal to graphics/present is # already in the set, so async-compute would alias the graphics queue: that # is the single-queue fallback, so leave its handle None and do not create a # redundant queue. add_compute = compute_qf is not None and compute_qf not in unique_families add_transfer = transfer_qf is not None and transfer_qf not in unique_families if add_compute: unique_families.add(compute_qf) if add_transfer: unique_families.add(transfer_qf) queue_create_infos = [ vk.VkDeviceQueueCreateInfo( queueFamilyIndex=family, queueCount=1, pQueuePriorities=[1.0], ) for family in unique_families ] device_extensions = [vk.VK_KHR_SWAPCHAIN_EXTENSION_NAME] if external_memory_fd: # Enable the dma-buf zero-copy prerequisites (D8 multi-GPU). Both names # are core-promoted in 1.1 but must still be listed as enabled device # extensions on a 1.0/1.1 instance. Resolve via getattr so a binding that # lacks the constant degrades to the staging-copy floor rather than # crashing init. for sym, default in ( ("VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME", "VK_KHR_external_memory"), ("VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME", "VK_KHR_external_memory_fd"), ): device_extensions.append(getattr(vk, sym, default)) # Only request multiDrawIndirect if the device supports it caps = probe_device_features(physical_device) features = vk.VkPhysicalDeviceFeatures( multiDrawIndirect=caps["multi_draw_indirect"], imageCubeArray=caps["image_cube_array"], # independentBlend: per-attachment blend/write masks for the thin # G-buffer MRT pass. Enabled when supported; zero-cost when the # G-buffer is never activated (it only affects pipeline creation). independentBlend=caps["independent_blend"], # Enable each block-compression family the device reports as supported. # caps[*] are already supported-or-False, so a present feature is enabled # and an absent one stays False (not requested). Enabling these is # mandatory: sampling an ASTC/ETC2 image without its feature on is # undefined behaviour even if the format reports SAMPLED. The per-format # SAMPLED gate (Engine.format_supported) is the second guard in the # TextureManager target probe. Purely additive: BC is enabled identically. textureCompressionBC=caps["texture_compression_bc"], textureCompressionETC2=caps["texture_compression_etc2"], textureCompressionASTC_LDR=caps["texture_compression_astc_ldr"], ) # Enable Vulkan 1.2 features required by shaders using nonuniformEXT. # descriptorBindingSampledImageUpdateAfterBind lets the bindless texture set # be written (e.g. a SubViewport registering its live texture) after it has # been bound into a still-recording command buffer, which is exactly the # bindless usage pattern -- without it such a write invalidates the cmd. features12 = vk.VkPhysicalDeviceVulkan12Features( sType=vk.VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, runtimeDescriptorArray=True, shaderSampledImageArrayNonUniformIndexing=True, descriptorBindingPartiallyBound=True, descriptorBindingSampledImageUpdateAfterBind=True, ) create_info = vk.VkDeviceCreateInfo( pNext=features12, queueCreateInfoCount=len(queue_create_infos), pQueueCreateInfos=queue_create_infos, enabledExtensionCount=len(device_extensions), ppEnabledExtensionNames=device_extensions, pEnabledFeatures=features, ) device = vk.vkCreateDevice(physical_device, create_info, None) graphics_queue = vk.vkGetDeviceQueue(device, queue_families.graphics, 0) present_queue = vk.vkGetDeviceQueue(device, queue_families.present, 0) compute_queue = vk.vkGetDeviceQueue(device, compute_qf, 0) if add_compute else None transfer_queue = vk.vkGetDeviceQueue(device, transfer_qf, 0) if add_transfer else None log.debug( "Logical device created (async_compute=%s, dedicated_transfer=%s)", add_compute, add_transfer, ) return device, graphics_queue, present_queue, compute_queue, transfer_queue