"""2D drawing pass — renders Draw2D geometry using ui.vert + ui_solid.frag, plus MSDF text."""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Any
import numpy as np
import vulkan as vk
from ..gpu.memory import create_buffer, upload_numpy
from ..gpu.pipeline import create_ui_pipeline
from .pass_helpers import load_shader_modules
__all__ = ["Draw2DPass"]
log = logging.getLogger(__name__)
SHADER_DIR = Path(__file__).resolve().parent.parent.parent.parent.parent / "shaders"
# Pre-allocate buffers — sized for full terminal rendering (100x30 terminal
# with bitmap font can generate ~100K verts for character pixels + backgrounds)
MAX_FILL_VERTS = 131072 # 128K verts (4 MB)
MAX_LINE_VERTS = 32768 # 32K verts (1 MB)
VERTEX_STRIDE = 32 # pos(vec2) + uv(vec2) + colour(vec4)
MAX_FILL_INDICES = 196608 # 192K indices (768 KB)
MAX_TEXT_VERTS = 32768
MAX_TEXT_INDICES = 49152
MAX_TEX_VERTS = 16384
MAX_TEX_INDICES = 24576
[docs]
class Draw2DPass:
"""GPU pass that renders 2D fills (triangles), lines, and MSDF text from Draw2D buffers.
Text rendering shares the TextPass's pipeline, descriptor set, and atlas — only the
text vertex/index buffers are owned here (needed for per-batch scissor clipping).
"""
def __init__(self, engine: Any, text_pass: Any = None):
self._engine = engine
self._text_pass = text_pass # Shared TextPass for text pipeline/atlas
# Fill pipeline (triangle list)
self._fill_pipeline: Any = None
self._fill_pipeline_layout: Any = None
# Line pipeline (line list)
self._line_pipeline: Any = None
self._line_pipeline_layout: Any = None
# Shared shader modules
self._vert_module: Any = None
self._frag_module: Any = None
# Fill buffers
self._fill_vb: Any = None
self._fill_vb_mem: Any = None
self._fill_ib: Any = None
self._fill_ib_mem: Any = None
# Line buffers
self._line_vb: Any = None
self._line_vb_mem: Any = None
# Text vertex/index buffers (own buffers for scissor-clipped batches)
self._text_vb: Any = None
self._text_vb_mem: Any = None
self._text_ib: Any = None
self._text_ib_mem: Any = None
# Textured quad pipeline (ui.vert + ui.frag with bindless textures)
self._tex_pipeline: Any = None
self._tex_pipeline_layout: Any = None
self._tex_frag_module: Any = None
# Textured quad buffers
self._tex_vb: Any = None
self._tex_vb_mem: Any = None
self._tex_ib: Any = None
self._tex_ib_mem: Any = None
self._ready = False
[docs]
def setup(self) -> None:
e = self._engine
device = e.ctx.device
phys = e.ctx.physical_device
# Compile shaders
self._vert_module, self._frag_module = load_shader_modules(
device, SHADER_DIR, "ui.vert", "ui_solid.frag",
)
# Fill pipeline (triangle topology) — reuses create_ui_pipeline
self._fill_pipeline, self._fill_pipeline_layout = create_ui_pipeline(
device, self._vert_module, self._frag_module,
e.render_pass, e.extent,
)
# Line pipeline (line topology) — create via CFFI
self._line_pipeline, self._line_pipeline_layout = _create_line2d_pipeline(
device, self._vert_module, self._frag_module,
e.render_pass, e.extent,
)
# Allocate host-visible buffers
host_flags = (
vk.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
| vk.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
)
self._fill_vb, self._fill_vb_mem = create_buffer(
device, phys, MAX_FILL_VERTS * VERTEX_STRIDE,
vk.VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_flags,
)
self._fill_ib, self._fill_ib_mem = create_buffer(
device, phys, MAX_FILL_INDICES * 4,
vk.VK_BUFFER_USAGE_INDEX_BUFFER_BIT, host_flags,
)
self._line_vb, self._line_vb_mem = create_buffer(
device, phys, MAX_LINE_VERTS * VERTEX_STRIDE,
vk.VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_flags,
)
# Text vertex/index buffers (geometry assembled per-batch for scissor clipping)
self._text_vb, self._text_vb_mem = create_buffer(
device, phys, MAX_TEXT_VERTS * VERTEX_STRIDE,
vk.VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_flags,
)
self._text_ib, self._text_ib_mem = create_buffer(
device, phys, MAX_TEXT_INDICES * 4,
vk.VK_BUFFER_USAGE_INDEX_BUFFER_BIT, host_flags,
)
# Textured quad pipeline — uses bindless texture descriptor set
from ..gpu.pipeline import create_shader_module
from ..materials.shader_compiler import compile_shader
tex_frag_spv = compile_shader(SHADER_DIR / "ui.frag")
self._tex_frag_module = create_shader_module(device, tex_frag_spv)
tex_layout = e.texture_descriptor_layout
self._tex_pipeline, self._tex_pipeline_layout = _create_textured_ui_pipeline(
device, self._vert_module, self._tex_frag_module,
e.render_pass, e.extent, tex_layout,
)
self._tex_vb, self._tex_vb_mem = create_buffer(
device, phys, MAX_TEX_VERTS * VERTEX_STRIDE,
vk.VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_flags,
)
self._tex_ib, self._tex_ib_mem = create_buffer(
device, phys, MAX_TEX_INDICES * 4,
vk.VK_BUFFER_USAGE_INDEX_BUFFER_BIT, host_flags,
)
self._ready = True
[docs]
def render(self, cmd: Any, width: int, height: int,
ui_width: int = 0, ui_height: int = 0) -> None:
if not self._ready:
return
from ..draw2d import Draw2D
device = self._engine.ctx.device
# UI coordinates may differ from framebuffer pixels (HiDPI / window vs framebuffer)
uw = ui_width or width
uh = ui_height or height
screen = np.array([uw, uh], dtype=np.float32)
vk_viewport = vk.VkViewport(
x=0.0, y=0.0,
width=float(width), height=float(height),
minDepth=0.0, maxDepth=1.0,
)
full_scissor = vk.VkRect2D(
offset=vk.VkOffset2D(x=0, y=0),
extent=vk.VkExtent2D(width=width, height=height),
)
# Get batches (each with optional clip rect)
batches = Draw2D._get_batches()
if not batches:
return
# Batched rendering: concatenate all data, upload once, draw with offsets
all_fill_verts = []
all_fill_indices = []
all_line_verts = []
all_text_verts = []
all_text_indices = []
draw_cmds = [] # (clip, fi_start, fi_count, fv_offset, lv_start, lv_count, ti_start, ti_count, tv_offset)
fv_total = 0 # fill vertex count so far
fi_total = 0 # fill index count so far
lv_total = 0 # line vertex count so far
tv_total = 0 # text vertex count so far
ti_total = 0 # text index count so far
# Textured quad data: list of (tex_id, verts, indices) per batch
all_tex_draws: list[list[tuple[int, Any, Any]]] = []
for clip, fill_data, line_data, text_data, tex_draws in batches:
fi_start = fi_total
fi_count = 0
fv_offset = fv_total
lv_start = lv_total
lv_count = 0
ti_start = ti_total
ti_count = 0
tv_offset = tv_total
if fill_data is not None:
verts, indices = fill_data
all_fill_verts.append(verts)
all_fill_indices.append(indices)
fi_count = len(indices)
fv_total += len(verts)
fi_total += len(indices)
if line_data is not None:
all_line_verts.append(line_data)
lv_count = len(line_data)
lv_total += len(line_data)
if text_data is not None:
verts, indices = text_data
all_text_verts.append(verts)
all_text_indices.append(indices)
ti_count = len(indices)
tv_total += len(verts)
ti_total += len(indices)
all_tex_draws.append(tex_draws if tex_draws else [])
draw_cmds.append((clip, fi_start, fi_count, fv_offset, lv_start, lv_count, ti_start, ti_count, tv_offset))
# Single upload for all batches (clamped to buffer capacity)
if all_fill_verts:
combined_fv = np.concatenate(all_fill_verts)
combined_fi = np.concatenate(all_fill_indices)
if len(combined_fv) > MAX_FILL_VERTS:
log.warning("Draw2D fill overflow: %d verts (max %d)", len(combined_fv), MAX_FILL_VERTS)
combined_fv = combined_fv[:MAX_FILL_VERTS]
combined_fi = combined_fi[:MAX_FILL_INDICES]
if len(combined_fi) > MAX_FILL_INDICES:
combined_fi = combined_fi[:MAX_FILL_INDICES]
upload_numpy(device, self._fill_vb_mem, combined_fv)
upload_numpy(device, self._fill_ib_mem, combined_fi)
if all_line_verts:
combined_lv = np.concatenate(all_line_verts)
if len(combined_lv) > MAX_LINE_VERTS:
log.warning("Draw2D line overflow: %d verts (max %d)", len(combined_lv), MAX_LINE_VERTS)
combined_lv = combined_lv[:MAX_LINE_VERTS]
upload_numpy(device, self._line_vb_mem, combined_lv)
if all_text_verts:
combined_tv = np.concatenate(all_text_verts)
combined_ti = np.concatenate(all_text_indices)
if len(combined_tv) > MAX_TEXT_VERTS:
log.warning("Draw2D text overflow: %d verts (max %d)", len(combined_tv), MAX_TEXT_VERTS)
combined_tv = combined_tv[:MAX_TEXT_VERTS]
combined_ti = combined_ti[:MAX_TEXT_INDICES]
if len(combined_ti) > MAX_TEXT_INDICES:
combined_ti = combined_ti[:MAX_TEXT_INDICES]
upload_numpy(device, self._text_vb_mem, combined_tv)
upload_numpy(device, self._text_ib_mem, combined_ti)
# Scale factor from UI coords to framebuffer pixels (for scissor rects)
clip_sx = width / uw if uw > 0 else 1.0
clip_sy = height / uh if uh > 0 else 1.0
# Issue per-batch draw calls with offsets
for batch_idx, (
clip, fi_start, fi_count, fv_offset, lv_start, lv_count, ti_start, ti_count, tv_offset,
) in enumerate(draw_cmds):
if clip is not None:
scissor = vk.VkRect2D(
offset=vk.VkOffset2D(x=int(clip[0] * clip_sx), y=int(clip[1] * clip_sy)),
extent=vk.VkExtent2D(width=int(clip[2] * clip_sx), height=int(clip[3] * clip_sy)),
)
else:
scissor = full_scissor
if fi_count > 0:
vk.vkCmdBindPipeline(cmd, vk.VK_PIPELINE_BIND_POINT_GRAPHICS, self._fill_pipeline)
vk.vkCmdSetViewport(cmd, 0, 1, [vk_viewport])
vk.vkCmdSetScissor(cmd, 0, 1, [scissor])
self._engine.push_constants(cmd, self._fill_pipeline_layout, screen.tobytes())
vk.vkCmdBindVertexBuffers(cmd, 0, 1, [self._fill_vb], [0])
vk.vkCmdBindIndexBuffer(cmd, self._fill_ib, 0, vk.VK_INDEX_TYPE_UINT32)
vk.vkCmdDrawIndexed(cmd, fi_count, 1, fi_start, fv_offset, 0)
if lv_count > 0:
vk.vkCmdBindPipeline(cmd, vk.VK_PIPELINE_BIND_POINT_GRAPHICS, self._line_pipeline)
vk.vkCmdSetViewport(cmd, 0, 1, [vk_viewport])
vk.vkCmdSetScissor(cmd, 0, 1, [scissor])
self._engine.push_constants(cmd, self._line_pipeline_layout, screen.tobytes())
vk.vkCmdBindVertexBuffers(cmd, 0, 1, [self._line_vb], [0])
vk.vkCmdDraw(cmd, lv_count, 1, lv_start, 0)
if ti_count > 0 and self._text_pass and self._text_pass.atlas_version > 0:
tp = self._text_pass
vk.vkCmdBindPipeline(cmd, vk.VK_PIPELINE_BIND_POINT_GRAPHICS, tp.pipeline)
vk.vkCmdSetViewport(cmd, 0, 1, [vk_viewport])
vk.vkCmdSetScissor(cmd, 0, 1, [scissor])
vk.vkCmdBindDescriptorSets(
cmd, vk.VK_PIPELINE_BIND_POINT_GRAPHICS, tp.pipeline_layout,
0, 1, [tp.descriptor_set], 0, None,
)
text_pc = np.array([uw, uh, tp.px_range], dtype=np.float32)
self._engine.push_constants(cmd, tp.pipeline_layout, text_pc.tobytes())
vk.vkCmdBindVertexBuffers(cmd, 0, 1, [self._text_vb], [0])
vk.vkCmdBindIndexBuffer(cmd, self._text_ib, 0, vk.VK_INDEX_TYPE_UINT32)
vk.vkCmdDrawIndexed(cmd, ti_count, 1, ti_start, tv_offset, 0)
# Textured quads (bindless texture sampling via ui.frag)
batch_tex = all_tex_draws[batch_idx] if batch_idx < len(all_tex_draws) else []
if batch_tex and self._tex_pipeline:
tex_desc = self._engine.texture_descriptor_set
if tex_desc:
for tex_id, tverts, tindices in batch_tex:
nv, ni = len(tverts), len(tindices)
if nv == 0 or ni == 0 or nv > MAX_TEX_VERTS or ni > MAX_TEX_INDICES:
continue
upload_numpy(device, self._tex_vb_mem, tverts)
upload_numpy(device, self._tex_ib_mem, tindices)
vk.vkCmdBindPipeline(cmd, vk.VK_PIPELINE_BIND_POINT_GRAPHICS, self._tex_pipeline)
vk.vkCmdSetViewport(cmd, 0, 1, [vk_viewport])
vk.vkCmdSetScissor(cmd, 0, 1, [scissor])
vk.vkCmdBindDescriptorSets(
cmd, vk.VK_PIPELINE_BIND_POINT_GRAPHICS, self._tex_pipeline_layout,
0, 1, [tex_desc], 0, None,
)
tex_pc = np.array([uw, uh], dtype=np.float32)
tex_pc_bytes = tex_pc.tobytes() + np.array([tex_id], dtype=np.int32).tobytes()
self._engine.push_constants(cmd, self._tex_pipeline_layout, tex_pc_bytes)
vk.vkCmdBindVertexBuffers(cmd, 0, 1, [self._tex_vb], [0])
vk.vkCmdBindIndexBuffer(cmd, self._tex_ib, 0, vk.VK_INDEX_TYPE_UINT32)
vk.vkCmdDrawIndexed(cmd, ni, 1, 0, 0, 0)
# Draw2D._reset() is called at start of frame in app.py, before tree.draw()
[docs]
def cleanup(self) -> None:
if not self._ready:
return
device = self._engine.ctx.device
for obj, fn in [
(self._fill_pipeline, vk.vkDestroyPipeline),
(self._fill_pipeline_layout, vk.vkDestroyPipelineLayout),
(self._line_pipeline, vk.vkDestroyPipeline),
(self._line_pipeline_layout, vk.vkDestroyPipelineLayout),
(self._tex_pipeline, vk.vkDestroyPipeline),
(self._tex_pipeline_layout, vk.vkDestroyPipelineLayout),
(self._vert_module, vk.vkDestroyShaderModule),
(self._frag_module, vk.vkDestroyShaderModule),
(self._tex_frag_module, vk.vkDestroyShaderModule),
(self._fill_vb, vk.vkDestroyBuffer),
(self._fill_ib, vk.vkDestroyBuffer),
(self._line_vb, vk.vkDestroyBuffer),
(self._text_vb, vk.vkDestroyBuffer),
(self._text_ib, vk.vkDestroyBuffer),
(self._tex_vb, vk.vkDestroyBuffer),
(self._tex_ib, vk.vkDestroyBuffer),
]:
if obj:
fn(device, obj, None)
for mem in [
self._fill_vb_mem, self._fill_ib_mem, self._line_vb_mem,
self._text_vb_mem, self._text_ib_mem,
self._tex_vb_mem, self._tex_ib_mem,
]:
if mem:
vk.vkFreeMemory(device, mem, None)
self._ready = False
def _create_line2d_pipeline(
device: Any,
vert_module: Any,
frag_module: Any,
render_pass: Any,
extent: tuple[int, int],
) -> tuple[Any, Any]:
"""Create a 2D line pipeline — same as ui_pipeline but LINE_LIST topology."""
ffi = vk.ffi
# Push constant: vec2 screen_size = 8 bytes
push_range = ffi.new("VkPushConstantRange*")
push_range.stageFlags = vk.VK_SHADER_STAGE_VERTEX_BIT | vk.VK_SHADER_STAGE_FRAGMENT_BIT
push_range.offset = 0
push_range.size = 8
layout_ci = ffi.new("VkPipelineLayoutCreateInfo*")
layout_ci.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO
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}")
pipeline_layout = layout_out[0]
pi = ffi.new("VkGraphicsPipelineCreateInfo*")
pi.sType = vk.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO
# Shader stages
stages = ffi.new("VkPipelineShaderStageCreateInfo[2]")
main_name = ffi.new("char[]", b"main")
stages[0].sType = vk.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO
stages[0].stage = vk.VK_SHADER_STAGE_VERTEX_BIT
stages[0].module = vert_module
stages[0].pName = main_name
stages[1].sType = vk.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO
stages[1].stage = vk.VK_SHADER_STAGE_FRAGMENT_BIT
stages[1].module = frag_module
stages[1].pName = main_name
pi.stageCount = 2
pi.pStages = stages
# Vertex input: pos(vec2) + uv(vec2) + colour(vec4) = 32 bytes
binding_desc = ffi.new("VkVertexInputBindingDescription*")
binding_desc.binding = 0
binding_desc.stride = VERTEX_STRIDE
binding_desc.inputRate = vk.VK_VERTEX_INPUT_RATE_VERTEX
attr_descs = ffi.new("VkVertexInputAttributeDescription[3]")
attr_descs[0].location = 0
attr_descs[0].binding = 0
attr_descs[0].format = vk.VK_FORMAT_R32G32_SFLOAT
attr_descs[0].offset = 0
attr_descs[1].location = 1
attr_descs[1].binding = 0
attr_descs[1].format = vk.VK_FORMAT_R32G32_SFLOAT
attr_descs[1].offset = 8
attr_descs[2].location = 2
attr_descs[2].binding = 0
attr_descs[2].format = vk.VK_FORMAT_R32G32B32A32_SFLOAT
attr_descs[2].offset = 16
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 = 3
vi.pVertexAttributeDescriptions = attr_descs
pi.pVertexInputState = vi
# Input assembly — LINE_LIST
ia = ffi.new("VkPipelineInputAssemblyStateCreateInfo*")
ia.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO
ia.topology = vk.VK_PRIMITIVE_TOPOLOGY_LINE_LIST
pi.pInputAssemblyState = ia
# Viewport state
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
pi.pViewportState = vps
# Rasterization
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 = vk.VK_CULL_MODE_NONE
pi.pRasterizationState = rs
# Multisample
ms = ffi.new("VkPipelineMultisampleStateCreateInfo*")
ms.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO
ms.rasterizationSamples = vk.VK_SAMPLE_COUNT_1_BIT
pi.pMultisampleState = ms
# No depth test
dss = ffi.new("VkPipelineDepthStencilStateCreateInfo*")
dss.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO
dss.depthTestEnable = 0
dss.depthWriteEnable = 0
pi.pDepthStencilState = dss
# Alpha blending
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 = vk.VK_BLEND_FACTOR_ZERO
cba.alphaBlendOp = vk.VK_BLEND_OP_ADD
cba.colorWriteMask = (
vk.VK_COLOR_COMPONENT_R_BIT | vk.VK_COLOR_COMPONENT_G_BIT
| vk.VK_COLOR_COMPONENT_B_BIT | vk.VK_COLOR_COMPONENT_A_BIT
)
cb = ffi.new("VkPipelineColorBlendStateCreateInfo*")
cb.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO
cb.attachmentCount = 1
cb.pAttachments = cba
pi.pColorBlendState = cb
# Dynamic state
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
pi.pDynamicState = ds
pi.layout = pipeline_layout
pi.renderPass = render_pass
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("2D line pipeline created")
return pipeline_out[0], pipeline_layout
def _create_textured_ui_pipeline(
device: Any,
vert_module: Any,
frag_module: Any,
render_pass: Any,
extent: tuple[int, int],
tex_descriptor_layout: Any,
) -> tuple[Any, Any]:
"""Create a 2D textured pipeline — same as ui_pipeline but with bindless textures.
Push constants: vec2 screen_size (8 bytes) + int texture_id (4 bytes) = 12 bytes.
Descriptor set 0: bindless sampler2D array.
"""
ffi = vk.ffi
push_range = ffi.new("VkPushConstantRange*")
push_range.stageFlags = vk.VK_SHADER_STAGE_VERTEX_BIT | vk.VK_SHADER_STAGE_FRAGMENT_BIT
push_range.offset = 0
push_range.size = 12 # vec2 screen_size + int texture_id
layouts = ffi.new("VkDescriptorSetLayout[1]", [tex_descriptor_layout])
layout_ci = ffi.new("VkPipelineLayoutCreateInfo*")
layout_ci.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO
layout_ci.setLayoutCount = 1
layout_ci.pSetLayouts = layouts
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}")
pipeline_layout = layout_out[0]
pi = ffi.new("VkGraphicsPipelineCreateInfo*")
pi.sType = vk.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO
stages = ffi.new("VkPipelineShaderStageCreateInfo[2]")
main_name = ffi.new("char[]", b"main")
stages[0].sType = vk.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO
stages[0].stage = vk.VK_SHADER_STAGE_VERTEX_BIT
stages[0].module = vert_module
stages[0].pName = main_name
stages[1].sType = vk.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO
stages[1].stage = vk.VK_SHADER_STAGE_FRAGMENT_BIT
stages[1].module = frag_module
stages[1].pName = main_name
pi.stageCount = 2
pi.pStages = stages
binding_desc = ffi.new("VkVertexInputBindingDescription*")
binding_desc.binding = 0
binding_desc.stride = VERTEX_STRIDE
binding_desc.inputRate = vk.VK_VERTEX_INPUT_RATE_VERTEX
attr_descs = ffi.new("VkVertexInputAttributeDescription[3]")
attr_descs[0].location = 0
attr_descs[0].binding = 0
attr_descs[0].format = vk.VK_FORMAT_R32G32_SFLOAT
attr_descs[0].offset = 0
attr_descs[1].location = 1
attr_descs[1].binding = 0
attr_descs[1].format = vk.VK_FORMAT_R32G32_SFLOAT
attr_descs[1].offset = 8
attr_descs[2].location = 2
attr_descs[2].binding = 0
attr_descs[2].format = vk.VK_FORMAT_R32G32B32A32_SFLOAT
attr_descs[2].offset = 16
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 = 3
vi.pVertexAttributeDescriptions = attr_descs
pi.pVertexInputState = vi
ia = ffi.new("VkPipelineInputAssemblyStateCreateInfo*")
ia.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO
ia.topology = vk.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
pi.pInputAssemblyState = ia
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
pi.pViewportState = vps
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 = vk.VK_CULL_MODE_NONE
pi.pRasterizationState = rs
ms = ffi.new("VkPipelineMultisampleStateCreateInfo*")
ms.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO
ms.rasterizationSamples = vk.VK_SAMPLE_COUNT_1_BIT
pi.pMultisampleState = ms
dss = ffi.new("VkPipelineDepthStencilStateCreateInfo*")
dss.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO
dss.depthTestEnable = 0
dss.depthWriteEnable = 0
pi.pDepthStencilState = dss
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 = vk.VK_BLEND_FACTOR_ZERO
cba.alphaBlendOp = vk.VK_BLEND_OP_ADD
cba.colorWriteMask = (
vk.VK_COLOR_COMPONENT_R_BIT | vk.VK_COLOR_COMPONENT_G_BIT
| vk.VK_COLOR_COMPONENT_B_BIT | vk.VK_COLOR_COMPONENT_A_BIT
)
cb = ffi.new("VkPipelineColorBlendStateCreateInfo*")
cb.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO
cb.attachmentCount = 1
cb.pAttachments = cba
pi.pColorBlendState = cb
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
pi.pDynamicState = ds
pi.layout = pipeline_layout
pi.renderPass = render_pass
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("2D textured pipeline created")
return pipeline_out[0], pipeline_layout