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