Source code for simvx.graphics.renderer.draw2d_pass

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