"""Vulkan initialisation and swapchain lifecycle helpers for Engine."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
import vulkan as vk
from ._types import FRAMES_IN_FLIGHT, SHADER_DIR
from .gpu.commands import CommandContext
from .gpu.device import create_logical_device, select_physical_device
from .gpu.instance import create_debug_messenger, create_instance
from .gpu.memory import create_image
from .gpu.pipeline import create_graphics_pipeline, create_shader_module
from .gpu.swapchain import Swapchain
from .gpu.sync import FrameSync
from .materials.shader_compiler import compile_shader
from .renderer.passes import create_render_pass
if TYPE_CHECKING:
from .engine import Engine
__all__ = [
"init_vulkan",
"create_depth_resources",
"destroy_depth_resources",
"create_framebuffers",
"destroy_framebuffers",
"recreate_swapchain",
]
log = logging.getLogger(__name__)
[docs]
def init_vulkan(engine: Engine, use_triangle: bool = True) -> None:
"""Initialise Vulkan instance, device, swapchain, render pass, and command buffers."""
extensions = engine._window.get_required_instance_extensions()
engine._instance, has_validation = create_instance("SimVX", extensions, validation=True)
engine._debug_messenger = create_debug_messenger(engine._instance) if has_validation else None
engine._surface = engine._window.create_graphics_surface(engine._instance)
engine._physical_device, qf = select_physical_device(engine._instance, engine._surface)
engine._graphics_qf = qf.graphics
# Query device capabilities before creating logical device
from .gpu.device import _query_device_features
engine._device_features = _query_device_features(engine._physical_device)
engine._has_mdi = engine._device_features["multi_draw_indirect"]
if not engine._has_mdi:
log.info("multiDrawIndirect not supported — using individual draw calls")
engine._device, engine._graphics_queue, engine._present_queue = create_logical_device(
engine._physical_device, qf,
)
# Load swapchain-related KHR functions
engine._vk_acquire = vk.vkGetInstanceProcAddr(engine._instance, "vkAcquireNextImageKHR")
engine._vk_present = vk.vkGetInstanceProcAddr(engine._instance, "vkQueuePresentKHR")
# Use framebuffer (physical) size for swapchain — may differ from logical
# window size on HiDPI displays (e.g. 3200x1800 vs 1600x900 at 200%)
fb_size = engine._window.get_framebuffer_size() if engine._window else (engine.width, engine.height)
engine._swapchain = Swapchain(
engine._instance,
engine._device,
engine._physical_device,
engine._surface,
fb_size,
qf.graphics,
qf.present,
vsync=engine._vsync,
)
engine._swapchain.create()
depth_fmt = vk.VK_FORMAT_D32_SFLOAT if engine._use_depth else 0
engine._render_pass = create_render_pass(engine._device, engine._swapchain.image_format, depth_fmt)
if engine._use_depth:
create_depth_resources(engine)
create_framebuffers(engine)
if use_triangle:
# Compile and load triangle shaders (built-in demo)
vert_spv = compile_shader(SHADER_DIR / "triangle.vert")
frag_spv = compile_shader(SHADER_DIR / "triangle.frag")
engine._vert_module = create_shader_module(engine._device, vert_spv)
engine._frag_module = create_shader_module(engine._device, frag_spv)
engine._pipeline, engine._pipeline_layout = create_graphics_pipeline(
engine._device,
engine._vert_module,
engine._frag_module,
engine._render_pass,
engine._swapchain.extent,
)
engine._cmd_ctx = CommandContext(engine._device, qf.graphics)
engine._cmd_ctx.create_pool()
engine._cmd_buffers = engine._cmd_ctx.allocate(FRAMES_IN_FLIGHT)
# Build the shared GPU context now that all handles exist
from .gpu.context import GPUContext
engine._ctx = GPUContext(
device=engine._device,
physical_device=engine._physical_device,
graphics_queue=engine._graphics_queue,
present_queue=engine._present_queue,
graphics_qf=engine._graphics_qf,
cmd_ctx=engine._cmd_ctx,
)
engine._sync = FrameSync(engine._device, len(engine._swapchain.images))
engine._sync.create()
[docs]
def create_depth_resources(engine: Engine) -> None:
"""Create depth buffer image, memory, and view."""
w, h = engine._swapchain.extent
engine._depth_image, engine._depth_memory = create_image(
engine._device,
engine._physical_device,
w, h,
vk.VK_FORMAT_D32_SFLOAT,
vk.VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
)
view_info = vk.VkImageViewCreateInfo(
image=engine._depth_image,
viewType=vk.VK_IMAGE_VIEW_TYPE_2D,
format=vk.VK_FORMAT_D32_SFLOAT,
subresourceRange=vk.VkImageSubresourceRange(
aspectMask=vk.VK_IMAGE_ASPECT_DEPTH_BIT,
baseMipLevel=0,
levelCount=1,
baseArrayLayer=0,
layerCount=1,
),
)
engine._depth_view = vk.vkCreateImageView(engine._device, view_info, None)
[docs]
def destroy_depth_resources(engine: Engine) -> None:
"""Destroy depth buffer resources."""
if engine._depth_view:
vk.vkDestroyImageView(engine._device, engine._depth_view, None)
engine._depth_view = None
if engine._depth_image:
vk.vkDestroyImage(engine._device, engine._depth_image, None)
engine._depth_image = None
if engine._depth_memory:
vk.vkFreeMemory(engine._device, engine._depth_memory, None)
engine._depth_memory = None
[docs]
def create_framebuffers(engine: Engine) -> None:
"""Create framebuffers for each swapchain image."""
engine._framebuffers = []
for view in engine._swapchain.image_views:
attachments: list[Any] = [view]
if engine._use_depth and engine._depth_view:
attachments.append(engine._depth_view)
fb_info = vk.VkFramebufferCreateInfo(
renderPass=engine._render_pass,
attachmentCount=len(attachments),
pAttachments=attachments,
width=engine._swapchain.extent[0],
height=engine._swapchain.extent[1],
layers=1,
)
engine._framebuffers.append(vk.vkCreateFramebuffer(engine._device, fb_info, None))
[docs]
def destroy_framebuffers(engine: Engine) -> None:
"""Destroy all swapchain framebuffers."""
for fb in engine._framebuffers:
vk.vkDestroyFramebuffer(engine._device, fb, None)
engine._framebuffers.clear()
[docs]
def recreate_swapchain(engine: Engine) -> None:
"""Recreate swapchain, depth buffer, and framebuffers after resize."""
w, h = engine._window.get_framebuffer_size()
while w == 0 or h == 0:
engine._window.poll_events()
w, h = engine._window.get_framebuffer_size()
vk.vkDeviceWaitIdle(engine._device)
destroy_framebuffers(engine)
if engine._use_depth:
destroy_depth_resources(engine)
engine._sync.destroy()
engine._swapchain.recreate((w, h))
engine.width, engine.height = w, h
if engine._use_depth:
create_depth_resources(engine)
create_framebuffers(engine)
engine._sync = FrameSync(engine._device, len(engine._swapchain.images))
engine._sync.create()
if engine._pick_pass:
engine._pick_pass.resize(engine._swapchain.extent)