Source code for simvx.graphics.gpu.pipeline

"""Graphics pipeline and shader module management."""


from __future__ import annotations

import logging
from pathlib import Path
from typing import Any

import vulkan as vk

log = logging.getLogger(__name__)

__all__ = [
    "create_shader_module",
    "create_graphics_pipeline",
    "create_forward_pipeline",
    "create_transparent_pipeline",
    "create_skinned_pipeline",
    "create_pick_pipeline",
    "create_line_pipeline",
    "create_gizmo_pipeline",
    "create_ui_pipeline",
    "create_textured_quad_pipeline",
]

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

_COLOUR_WRITE_ALL = (
    vk.VK_COLOR_COMPONENT_R_BIT
    | vk.VK_COLOR_COMPONENT_G_BIT
    | vk.VK_COLOR_COMPONENT_B_BIT
    | vk.VK_COLOR_COMPONENT_A_BIT
)

# ---------------------------------------------------------------------------
# Internal cffi struct builders
# ---------------------------------------------------------------------------


def _make_shader_stages(ffi: Any, vert_module: Any, frag_module: Any) -> tuple[Any, Any]:
    """Build a 2-stage (vert + frag) shader stage array.  Returns (stages, main_name)."""
    stages = ffi.new("VkPipelineShaderStageCreateInfo[2]")
    main_name = ffi.new("char[]", b"main")
    for i, (stage_bit, module) in enumerate([
        (vk.VK_SHADER_STAGE_VERTEX_BIT, vert_module),
        (vk.VK_SHADER_STAGE_FRAGMENT_BIT, frag_module),
    ]):
        stages[i].sType = vk.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO
        stages[i].stage = stage_bit
        stages[i].module = module
        stages[i].pName = main_name
    return stages, main_name


def _make_vertex_input(
    ffi: Any,
    stride: int,
    attributes: list[tuple[int, int, int]],
) -> tuple[Any, Any, Any]:
    """Build vertex input state.

    *attributes* is a list of (location, format, offset) tuples.
    Returns (vi, binding_desc, attr_descs) -- all three must stay alive until pipeline creation.
    """
    binding_desc = ffi.new("VkVertexInputBindingDescription*")
    binding_desc.binding = 0
    binding_desc.stride = stride
    binding_desc.inputRate = vk.VK_VERTEX_INPUT_RATE_VERTEX

    n = len(attributes)
    attr_descs = ffi.new(f"VkVertexInputAttributeDescription[{n}]")
    for i, (loc, fmt, off) in enumerate(attributes):
        attr_descs[i].location = loc
        attr_descs[i].binding = 0
        attr_descs[i].format = fmt
        attr_descs[i].offset = off

    vi = ffi.new("VkPipelineVertexInputStateCreateInfo*")
    vi.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
    vi.vertexBindingDescriptionCount = 1
    vi.pVertexBindingDescriptions = binding_desc
    vi.vertexAttributeDescriptionCount = n
    vi.pVertexAttributeDescriptions = attr_descs
    return vi, binding_desc, attr_descs


def _make_empty_vertex_input(ffi: Any) -> Any:
    """Build an empty vertex input state (no vertex buffers)."""
    vi = ffi.new("VkPipelineVertexInputStateCreateInfo*")
    vi.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
    return vi


def _make_input_assembly(ffi: Any, topology: int = vk.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) -> Any:
    """Build input assembly state."""
    ia = ffi.new("VkPipelineInputAssemblyStateCreateInfo*")
    ia.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO
    ia.topology = topology
    return ia


def _make_viewport_state(ffi: Any, extent: tuple[int, int]) -> tuple[Any, Any, Any]:
    """Build viewport state.  Returns (vps, viewport, scissor) -- all must stay alive."""
    vps = ffi.new("VkPipelineViewportStateCreateInfo*")
    vps.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO
    vps.viewportCount = 1
    viewport = ffi.new("VkViewport*")
    viewport.width = float(extent[0])
    viewport.height = float(extent[1])
    viewport.maxDepth = 1.0
    vps.pViewports = viewport
    scissor = ffi.new("VkRect2D*")
    scissor.extent.width = extent[0]
    scissor.extent.height = extent[1]
    vps.scissorCount = 1
    vps.pScissors = scissor
    return vps, viewport, scissor


def _make_rasterization(
    ffi: Any,
    cull_mode: int = vk.VK_CULL_MODE_BACK_BIT,
    front_face: int = vk.VK_FRONT_FACE_COUNTER_CLOCKWISE,
) -> Any:
    """Build rasterization state."""
    rs = ffi.new("VkPipelineRasterizationStateCreateInfo*")
    rs.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO
    rs.polygonMode = vk.VK_POLYGON_MODE_FILL
    rs.lineWidth = 1.0
    rs.cullMode = cull_mode
    rs.frontFace = front_face
    return rs


def _make_multisample(ffi: Any) -> Any:
    """Build multisample state (1x, no MSAA)."""
    ms = ffi.new("VkPipelineMultisampleStateCreateInfo*")
    ms.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO
    ms.rasterizationSamples = vk.VK_SAMPLE_COUNT_1_BIT
    return ms


def _make_depth_stencil(
    ffi: Any,
    *,
    test: bool = True,
    write: bool = True,
    compare_op: int = vk.VK_COMPARE_OP_LESS,
) -> Any:
    """Build depth/stencil state."""
    dss = ffi.new("VkPipelineDepthStencilStateCreateInfo*")
    dss.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO
    dss.depthTestEnable = int(test)
    dss.depthWriteEnable = int(write)
    dss.depthCompareOp = compare_op
    return dss


def _make_colour_blend_opaque(ffi: Any, write_mask: int = _COLOUR_WRITE_ALL) -> tuple[Any, Any]:
    """Build opaque colour blend state (no blending).  Returns (cb, cba)."""
    cba = ffi.new("VkPipelineColorBlendAttachmentState*")
    cba.colorWriteMask = write_mask
    cb = ffi.new("VkPipelineColorBlendStateCreateInfo*")
    cb.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO
    cb.attachmentCount = 1
    cb.pAttachments = cba
    return cb, cba


def _make_colour_blend_alpha(
    ffi: Any,
    dst_alpha_factor: int = vk.VK_BLEND_FACTOR_ZERO,
) -> tuple[Any, Any]:
    """Build alpha-blended colour blend state.  Returns (cb, cba).

    src colour = srcAlpha, dst colour = 1-srcAlpha.
    *dst_alpha_factor* controls destination alpha (ZERO for overlay, ONE_MINUS_SRC_ALPHA for compositing).
    """
    cba = ffi.new("VkPipelineColorBlendAttachmentState*")
    cba.blendEnable = 1
    cba.srcColorBlendFactor = vk.VK_BLEND_FACTOR_SRC_ALPHA
    cba.dstColorBlendFactor = vk.VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA
    cba.colorBlendOp = vk.VK_BLEND_OP_ADD
    cba.srcAlphaBlendFactor = vk.VK_BLEND_FACTOR_ONE
    cba.dstAlphaBlendFactor = dst_alpha_factor
    cba.alphaBlendOp = vk.VK_BLEND_OP_ADD
    cba.colorWriteMask = _COLOUR_WRITE_ALL
    cb = ffi.new("VkPipelineColorBlendStateCreateInfo*")
    cb.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO
    cb.attachmentCount = 1
    cb.pAttachments = cba
    return cb, cba


def _make_dynamic_state(ffi: Any) -> tuple[Any, Any]:
    """Build dynamic state for viewport + scissor.  Returns (ds, dyn_states)."""
    dyn_states = ffi.new("VkDynamicState[2]", [vk.VK_DYNAMIC_STATE_VIEWPORT, vk.VK_DYNAMIC_STATE_SCISSOR])
    ds = ffi.new("VkPipelineDynamicStateCreateInfo*")
    ds.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO
    ds.dynamicStateCount = 2
    ds.pDynamicStates = dyn_states
    return ds, dyn_states


def _create_pipeline_layout(
    ffi: Any,
    device: Any,
    descriptor_layouts: list[Any] | None = None,
    push_constant_size: int = 0,
    push_stage_flags: int = vk.VK_SHADER_STAGE_VERTEX_BIT | vk.VK_SHADER_STAGE_FRAGMENT_BIT,
) -> Any:
    """Create a VkPipelineLayout via raw cffi.

    Returns the pipeline layout handle.  Raises RuntimeError on failure.
    """
    layout_ci = ffi.new("VkPipelineLayoutCreateInfo*")
    layout_ci.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO

    # Keep cffi allocations alive until the call completes
    _set_layouts = None
    _push_range = None

    if descriptor_layouts:
        n = len(descriptor_layouts)
        _set_layouts = ffi.new(f"VkDescriptorSetLayout[{n}]", descriptor_layouts)
        layout_ci.setLayoutCount = n
        layout_ci.pSetLayouts = _set_layouts

    if push_constant_size > 0:
        _push_range = ffi.new("VkPushConstantRange*")
        _push_range.stageFlags = push_stage_flags
        _push_range.offset = 0
        _push_range.size = push_constant_size
        layout_ci.pushConstantRangeCount = 1
        layout_ci.pPushConstantRanges = _push_range

    layout_out = ffi.new("VkPipelineLayout*")
    result = vk._vulkan._callApi(
        vk._vulkan.lib.vkCreatePipelineLayout,
        device,
        layout_ci,
        ffi.NULL,
        layout_out,
    )
    if result != vk.VK_SUCCESS:
        raise RuntimeError(f"vkCreatePipelineLayout failed: {result}")
    return layout_out[0]


def _build_pipeline(ffi: Any, device: Any, pi: Any, name: str) -> Any:
    """Call vkCreateGraphicsPipelines and return the pipeline handle."""
    pipeline_out = ffi.new("VkPipeline*")
    result = vk._vulkan._callApi(
        vk._vulkan.lib.vkCreateGraphicsPipelines,
        device,
        ffi.NULL,
        1,
        pi,
        ffi.NULL,
        pipeline_out,
    )
    if result != vk.VK_SUCCESS:
        raise RuntimeError(f"vkCreateGraphicsPipelines failed: {result}")
    log.debug("%s pipeline created", name)
    return pipeline_out[0]


# ---------------------------------------------------------------------------
# Vertex format presets — (location, format, offset) tuples
# ---------------------------------------------------------------------------

# position(vec3) + normal(vec3) + uv(vec2) = 32 bytes
_VERTEX_POS_NORM_UV: list[tuple[int, int, int]] = [
    (0, vk.VK_FORMAT_R32G32B32_SFLOAT, 0),   # position
    (1, vk.VK_FORMAT_R32G32B32_SFLOAT, 12),  # normal
    (2, vk.VK_FORMAT_R32G32_SFLOAT, 24),     # uv
]

# position(vec3) + colour(vec4) = 28 bytes
_VERTEX_POS_COLOUR: list[tuple[int, int, int]] = [
    (0, vk.VK_FORMAT_R32G32B32_SFLOAT, 0),      # position
    (1, vk.VK_FORMAT_R32G32B32A32_SFLOAT, 12),  # colour
]


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


[docs] def create_shader_module(device: Any, spirv_path: Path) -> Any: """Load a SPIR-V file and create a VkShaderModule.""" code = spirv_path.read_bytes() create_info = vk.VkShaderModuleCreateInfo( codeSize=len(code), pCode=code, ) module = vk.vkCreateShaderModule(device, create_info, None) log.debug("Shader module loaded: %s", spirv_path.name) return module
[docs] def create_graphics_pipeline( device: Any, vert_module: Any, frag_module: Any, render_pass: Any, extent: tuple[int, int], ) -> tuple[Any, Any]: """Create a VkPipeline for triangle rendering. Returns (pipeline, layout). Uses raw cffi allocation to avoid the vulkan wrapper's GC lifetime issues with nested pointer structs. """ ffi = vk.ffi pipeline_layout = vk.vkCreatePipelineLayout(device, vk.VkPipelineLayoutCreateInfo(), None) pi = ffi.new("VkGraphicsPipelineCreateInfo*") pi.sType = vk.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO stages, _main = _make_shader_stages(ffi, vert_module, frag_module) pi.stageCount = 2 pi.pStages = stages vi = _make_empty_vertex_input(ffi) pi.pVertexInputState = vi ia = _make_input_assembly(ffi) pi.pInputAssemblyState = ia vps, _vp, _sc = _make_viewport_state(ffi, extent) pi.pViewportState = vps rs = _make_rasterization(ffi) pi.pRasterizationState = rs ms = _make_multisample(ffi) pi.pMultisampleState = ms cb, _cba = _make_colour_blend_opaque(ffi) pi.pColorBlendState = cb ds, _dyn = _make_dynamic_state(ffi) pi.pDynamicState = ds pi.layout = pipeline_layout pi.renderPass = render_pass pipeline = _build_pipeline(ffi, device, pi, "Graphics") return pipeline, pipeline_layout
[docs] def create_forward_pipeline( device: Any, vert_module: Any, frag_module: Any, render_pass: Any, extent: tuple[int, int], descriptor_layout: Any, texture_layout: Any | None = None, double_sided: bool = False, ) -> tuple[Any, Any]: """Create a pipeline for forward rendering with vertex inputs and SSBOs. Vertex format: position (vec3), normal (vec3), uv (vec2) = 32 bytes stride. Push constants: view (mat4) + proj (mat4) = 128 bytes. If texture_layout is provided, set 1 is bound to the texture descriptor layout. If double_sided is True, backface culling is disabled. Returns (pipeline, pipeline_layout). """ ffi = vk.ffi layouts = [descriptor_layout, texture_layout] if texture_layout else [descriptor_layout] pipeline_layout = _create_pipeline_layout(ffi, device, layouts, push_constant_size=128) pi = ffi.new("VkGraphicsPipelineCreateInfo*") pi.sType = vk.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO stages, _main = _make_shader_stages(ffi, vert_module, frag_module) pi.stageCount = 2 pi.pStages = stages vi, _bd, _ad = _make_vertex_input(ffi, stride=32, attributes=_VERTEX_POS_NORM_UV) pi.pVertexInputState = vi ia = _make_input_assembly(ffi) pi.pInputAssemblyState = ia vps, _vp, _sc = _make_viewport_state(ffi, extent) pi.pViewportState = vps cull = vk.VK_CULL_MODE_NONE if double_sided else vk.VK_CULL_MODE_BACK_BIT rs = _make_rasterization(ffi, cull_mode=cull) pi.pRasterizationState = rs ms = _make_multisample(ffi) pi.pMultisampleState = ms dss = _make_depth_stencil(ffi) pi.pDepthStencilState = dss cb, _cba = _make_colour_blend_opaque(ffi) pi.pColorBlendState = cb ds, _dyn = _make_dynamic_state(ffi) pi.pDynamicState = ds pi.layout = pipeline_layout pi.renderPass = render_pass pipeline = _build_pipeline(ffi, device, pi, "Forward") return pipeline, pipeline_layout
[docs] def create_transparent_pipeline( device: Any, vert_module: Any, frag_module: Any, render_pass: Any, extent: tuple[int, int], descriptor_layout: Any, texture_layout: Any | None = None, ) -> tuple[Any, Any]: """Create a pipeline for transparent object rendering. Identical to the forward pipeline except: - Alpha blending enabled (srcAlpha, oneMinusSrcAlpha) - Depth write disabled (depth test still enabled) - Backface culling disabled (transparent objects often need both sides) Returns (pipeline, pipeline_layout). """ ffi = vk.ffi layouts = [descriptor_layout, texture_layout] if texture_layout else [descriptor_layout] pipeline_layout = _create_pipeline_layout(ffi, device, layouts, push_constant_size=128) pi = ffi.new("VkGraphicsPipelineCreateInfo*") pi.sType = vk.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO stages, _main = _make_shader_stages(ffi, vert_module, frag_module) pi.stageCount = 2 pi.pStages = stages vi, _bd, _ad = _make_vertex_input(ffi, stride=32, attributes=_VERTEX_POS_NORM_UV) pi.pVertexInputState = vi ia = _make_input_assembly(ffi) pi.pInputAssemblyState = ia vps, _vp, _sc = _make_viewport_state(ffi, extent) pi.pViewportState = vps rs = _make_rasterization(ffi, cull_mode=vk.VK_CULL_MODE_NONE) pi.pRasterizationState = rs ms = _make_multisample(ffi) pi.pMultisampleState = ms dss = _make_depth_stencil(ffi, write=False) pi.pDepthStencilState = dss cb, _cba = _make_colour_blend_alpha(ffi, dst_alpha_factor=vk.VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA) pi.pColorBlendState = cb ds, _dyn = _make_dynamic_state(ffi) pi.pDynamicState = ds pi.layout = pipeline_layout pi.renderPass = render_pass pipeline = _build_pipeline(ffi, device, pi, "Transparent") return pipeline, pipeline_layout
[docs] def create_skinned_pipeline( device: Any, vert_module: Any, frag_module: Any, render_pass: Any, extent: tuple[int, int], descriptor_layout: Any, texture_layout: Any | None = None, joint_layout: Any | None = None, ) -> tuple[Any, Any]: """Create a pipeline for skinned mesh rendering. Vertex format: position(vec3) + normal(vec3) + uv(vec2) + joints(uvec4) + weights(vec4) = 56 bytes. Descriptor sets: 0=SSBOs, 1=textures, 2=joint matrices SSBO. Push constants: view (mat4) + proj (mat4) = 128 bytes. Returns (pipeline, pipeline_layout). """ ffi = vk.ffi # Build descriptor set layouts list (up to 3 sets) layouts = [descriptor_layout] if texture_layout: layouts.append(texture_layout) if joint_layout: while len(layouts) < 2: layouts.append(descriptor_layout) # placeholder for set 1 layouts.append(joint_layout) pipeline_layout = _create_pipeline_layout(ffi, device, layouts, push_constant_size=128) pi = ffi.new("VkGraphicsPipelineCreateInfo*") pi.sType = vk.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO stages, _main = _make_shader_stages(ffi, vert_module, frag_module) pi.stageCount = 2 pi.pStages = stages # position(vec3) + normal(vec3) + uv(vec2) + joints(u16x4) + weights(f32x4) = 56 bytes skinned_attrs: list[tuple[int, int, int]] = [ (0, vk.VK_FORMAT_R32G32B32_SFLOAT, 0), # position (1, vk.VK_FORMAT_R32G32B32_SFLOAT, 12), # normal (2, vk.VK_FORMAT_R32G32_SFLOAT, 24), # uv (3, vk.VK_FORMAT_R16G16B16A16_UINT, 32), # joints (4, vk.VK_FORMAT_R32G32B32A32_SFLOAT, 40), # weights ] vi, _bd, _ad = _make_vertex_input(ffi, stride=56, attributes=skinned_attrs) pi.pVertexInputState = vi ia = _make_input_assembly(ffi) pi.pInputAssemblyState = ia vps, _vp, _sc = _make_viewport_state(ffi, extent) pi.pViewportState = vps rs = _make_rasterization(ffi) pi.pRasterizationState = rs ms = _make_multisample(ffi) pi.pMultisampleState = ms dss = _make_depth_stencil(ffi) pi.pDepthStencilState = dss cb, _cba = _make_colour_blend_opaque(ffi) pi.pColorBlendState = cb ds, _dyn = _make_dynamic_state(ffi) pi.pDynamicState = ds pi.layout = pipeline_layout pi.renderPass = render_pass pipeline = _build_pipeline(ffi, device, pi, "Skinned") return pipeline, pipeline_layout
[docs] def create_pick_pipeline( device: Any, vert_module: Any, frag_module: Any, render_pass: Any, extent: tuple[int, int], descriptor_layout: Any, ) -> tuple[Any, Any]: """Create a pipeline for the pick pass (R32_UINT output, position-only vertex input). Push constants: view (mat4) + proj (mat4) = 128 bytes. Vertex input: position (vec3) only, stride 32 (same buffer as forward, skips normal/uv). Returns (pipeline, pipeline_layout). """ ffi = vk.ffi pipeline_layout = _create_pipeline_layout(ffi, device, [descriptor_layout], push_constant_size=128) pi = ffi.new("VkGraphicsPipelineCreateInfo*") pi.sType = vk.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO stages, _main = _make_shader_stages(ffi, vert_module, frag_module) pi.stageCount = 2 pi.pStages = stages # position(vec3) only at offset 0, stride 32 (same interleaved buffer as forward) pick_attrs: list[tuple[int, int, int]] = [ (0, vk.VK_FORMAT_R32G32B32_SFLOAT, 0), ] vi, _bd, _ad = _make_vertex_input(ffi, stride=32, attributes=pick_attrs) pi.pVertexInputState = vi ia = _make_input_assembly(ffi) pi.pInputAssemblyState = ia vps, _vp, _sc = _make_viewport_state(ffi, extent) pi.pViewportState = vps rs = _make_rasterization(ffi) pi.pRasterizationState = rs ms = _make_multisample(ffi) pi.pMultisampleState = ms dss = _make_depth_stencil(ffi) pi.pDepthStencilState = dss # R32_UINT — no blending, write R only cb, _cba = _make_colour_blend_opaque(ffi, write_mask=vk.VK_COLOR_COMPONENT_R_BIT) pi.pColorBlendState = cb ds, _dyn = _make_dynamic_state(ffi) pi.pDynamicState = ds pi.layout = pipeline_layout pi.renderPass = render_pass pipeline = _build_pipeline(ffi, device, pi, "Pick") return pipeline, pipeline_layout
[docs] def create_line_pipeline( device: Any, vert_module: Any, frag_module: Any, render_pass: Any, extent: tuple[int, int], ) -> tuple[Any, Any]: """Create a pipeline for line rendering with per-vertex colour. Vertex format: position (vec3) + colour (vec4) = 28 bytes stride. Push constants: view (mat4) + proj (mat4) = 128 bytes. No descriptor sets. Returns (pipeline, pipeline_layout). """ ffi = vk.ffi pipeline_layout = _create_pipeline_layout(ffi, device, push_constant_size=128) pi = ffi.new("VkGraphicsPipelineCreateInfo*") pi.sType = vk.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO stages, _main = _make_shader_stages(ffi, vert_module, frag_module) pi.stageCount = 2 pi.pStages = stages vi, _bd, _ad = _make_vertex_input(ffi, stride=28, attributes=_VERTEX_POS_COLOUR) pi.pVertexInputState = vi ia = _make_input_assembly(ffi, topology=vk.VK_PRIMITIVE_TOPOLOGY_LINE_LIST) pi.pInputAssemblyState = ia vps, _vp, _sc = _make_viewport_state(ffi, extent) pi.pViewportState = vps rs = _make_rasterization(ffi, cull_mode=vk.VK_CULL_MODE_NONE) pi.pRasterizationState = rs ms = _make_multisample(ffi) pi.pMultisampleState = ms # Depth test but no write (lines render on top of scene) dss = _make_depth_stencil(ffi, write=False, compare_op=vk.VK_COMPARE_OP_LESS_OR_EQUAL) pi.pDepthStencilState = dss cb, _cba = _make_colour_blend_alpha(ffi) pi.pColorBlendState = cb ds, _dyn = _make_dynamic_state(ffi) pi.pDynamicState = ds pi.layout = pipeline_layout pi.renderPass = render_pass pipeline = _build_pipeline(ffi, device, pi, "Line") return pipeline, pipeline_layout
[docs] def create_ui_pipeline( device: Any, vert_module: Any, frag_module: Any, render_pass: Any, extent: tuple[int, int], ) -> tuple[Any, Any]: """Create a pipeline for solid-colour 2D UI rendering. Vertex format: position (vec2) + uv (vec2) + colour (vec4) = 32 bytes stride. Push constants: screen_size (vec2) = 8 bytes. No descriptor sets, no depth test. Returns (pipeline, pipeline_layout). """ ffi = vk.ffi pipeline_layout = _create_pipeline_layout(ffi, device, push_constant_size=8) pi = ffi.new("VkGraphicsPipelineCreateInfo*") pi.sType = vk.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO stages, _main = _make_shader_stages(ffi, vert_module, frag_module) pi.stageCount = 2 pi.pStages = stages # position(vec2) + uv(vec2) + colour(vec4) = 32 bytes ui_attrs: list[tuple[int, int, int]] = [ (0, vk.VK_FORMAT_R32G32_SFLOAT, 0), # position (1, vk.VK_FORMAT_R32G32_SFLOAT, 8), # uv (2, vk.VK_FORMAT_R32G32B32A32_SFLOAT, 16), # colour ] vi, _bd, _ad = _make_vertex_input(ffi, stride=32, attributes=ui_attrs) pi.pVertexInputState = vi ia = _make_input_assembly(ffi) pi.pInputAssemblyState = ia vps, _vp, _sc = _make_viewport_state(ffi, extent) pi.pViewportState = vps rs = _make_rasterization(ffi, cull_mode=vk.VK_CULL_MODE_NONE) pi.pRasterizationState = rs ms = _make_multisample(ffi) pi.pMultisampleState = ms dss = _make_depth_stencil(ffi, test=False, write=False) pi.pDepthStencilState = dss cb, _cba = _make_colour_blend_alpha(ffi) pi.pColorBlendState = cb ds, _dyn = _make_dynamic_state(ffi) pi.pDynamicState = ds pi.layout = pipeline_layout pi.renderPass = render_pass pipeline = _build_pipeline(ffi, device, pi, "UI") return pipeline, pipeline_layout
[docs] def create_textured_quad_pipeline( device: Any, vert_module: Any, frag_module: Any, render_pass: Any, extent: tuple[int, int], texture_layout: Any, ) -> tuple[Any, Any]: """Create a pipeline for a textured fullscreen quad (no depth test). Vertex format: position (vec2) + uv (vec2) = 16 bytes stride. Descriptor set 0: combined image sampler (texture). Returns (pipeline, pipeline_layout). """ ffi = vk.ffi pipeline_layout = _create_pipeline_layout(ffi, device, [texture_layout]) pi = ffi.new("VkGraphicsPipelineCreateInfo*") pi.sType = vk.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO stages, _main = _make_shader_stages(ffi, vert_module, frag_module) pi.stageCount = 2 pi.pStages = stages # position(vec2) + uv(vec2) = 16 bytes quad_attrs: list[tuple[int, int, int]] = [ (0, vk.VK_FORMAT_R32G32_SFLOAT, 0), (1, vk.VK_FORMAT_R32G32_SFLOAT, 8), ] vi, _bd, _ad = _make_vertex_input(ffi, stride=16, attributes=quad_attrs) pi.pVertexInputState = vi ia = _make_input_assembly(ffi) pi.pInputAssemblyState = ia vps, _vp, _sc = _make_viewport_state(ffi, extent) pi.pViewportState = vps rs = _make_rasterization(ffi, cull_mode=vk.VK_CULL_MODE_NONE) pi.pRasterizationState = rs ms = _make_multisample(ffi) pi.pMultisampleState = ms dss = _make_depth_stencil(ffi, test=False, write=False) pi.pDepthStencilState = dss cb, _cba = _make_colour_blend_opaque(ffi) pi.pColorBlendState = cb ds, _dyn = _make_dynamic_state(ffi) pi.pDynamicState = ds pi.layout = pipeline_layout pi.renderPass = render_pass pipeline = _build_pipeline(ffi, device, pi, "Textured quad") return pipeline, pipeline_layout
[docs] def create_gizmo_pipeline( device: Any, vert_module: Any, frag_module: Any, render_pass: Any, extent: tuple[int, int], *, topology: int = vk.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, ) -> tuple[Any, Any]: """Create a pipeline for gizmo overlay rendering (depth test DISABLED). Same vertex format as line pipeline: position (vec3) + colour (vec4) = 28 bytes. Push constants: view (mat4) + proj (mat4) = 128 bytes. Supports both TRIANGLE_LIST and LINE_LIST topology via *topology* parameter. Returns (pipeline, pipeline_layout). """ ffi = vk.ffi pipeline_layout = _create_pipeline_layout(ffi, device, push_constant_size=128) pi = ffi.new("VkGraphicsPipelineCreateInfo*") pi.sType = vk.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO stages, _main = _make_shader_stages(ffi, vert_module, frag_module) pi.stageCount = 2 pi.pStages = stages vi, _bd, _ad = _make_vertex_input(ffi, stride=28, attributes=_VERTEX_POS_COLOUR) pi.pVertexInputState = vi ia = _make_input_assembly(ffi, topology=topology) pi.pInputAssemblyState = ia vps, _vp, _sc = _make_viewport_state(ffi, extent) pi.pViewportState = vps rs = _make_rasterization(ffi, cull_mode=vk.VK_CULL_MODE_NONE) pi.pRasterizationState = rs ms = _make_multisample(ffi) pi.pMultisampleState = ms dss = _make_depth_stencil(ffi, test=False, write=False) pi.pDepthStencilState = dss cb, _cba = _make_colour_blend_alpha(ffi) pi.pColorBlendState = cb ds, _dyn = _make_dynamic_state(ffi) pi.pDynamicState = ds pi.layout = pipeline_layout pi.renderPass = render_pass pipeline = _build_pipeline(ffi, device, pi, f"Gizmo (topology={topology})") return pipeline, pipeline_layout