"""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