Declarative Pipeline Builder

Status: migration complete

The old per-pass create_*_pipeline factory zoo has been fully retired (decision C, approved). Every graphics pipeline in the engine is now created through a single declarative path:

from simvx.graphics.gpu.pipeline import PipelineSpec, build_pipeline

spec = PipelineSpec(name="...", ...)              # immutable description
pipeline, layout = build_pipeline(device, spec, render_pass, extent,
                                  vert_module=..., frag_module=...)

A render pass declares what pipeline it wants via an immutable PipelineSpec and never touches ffi.new itself. build_pipeline composes the private _make_* sub-struct builders so there is exactly one shared creation path, and owns all cffi lifetime internally (every sub-struct is rooted in a local keep list that stays reachable across vkCreatePipelineLayout and vkCreateGraphicsPipelines). Callers never root a sub-struct by hand.

render_pass, extent, and the optional pre-created shader modules are late-bound arguments to build_pipeline rather than spec fields, so one immutable spec can be rebuilt against a different render pass (e.g. the HDR R16G16B16A16_SFLOAT offscreen pass vs the swapchain B8G8R8A8_SRGB pass) without copying.

Factories retired (converged onto build_pipeline + inline PipelineSpec)

The following module-level factories formerly lived in gpu/pipeline.py and have been deleted (no compatibility shim or alias). Their config now lives as an inline PipelineSpec at the call site:

  • create_forward_pipeline, create_transparent_pipeline, create_skinned_pipeline -> forward / transparent / skinned mesh specs

  • create_pick_pipeline -> picking (picking/pick_pass.py)

  • create_line_pipeline -> debug-line overlay (renderer/overlay_renderer.py)

  • create_gizmo_pipeline -> editor gizmo overlays (renderer/gizmo_pass.py)

  • create_ui_pipeline, create_textured_quad_pipeline -> 2D fill / line / textured-quad pipelines (renderer/draw2d_pass.py)

  • create_graphics_pipeline -> the generic monolith; removed entirely

  • _Draw2DPipelineSpec / _build_draw2d_pipeline -> the bespoke Draw2D builder; replaced by PipelineSpec + build_pipeline

All render passes now build through this path: skybox, taa, bloom, particle, text, tilemap, grid, shadow, point_shadow, velocity, depth_prepass, draw2d, gizmo, overlay (debug lines), pick, and the forward/transparent/skinned mesh pipelines (renderer/pipeline_manager.py).

Inert-state normalization

Pipelines that previously declared front_face = CLOCKWISE together with cull_mode = NONE were normalized to the PipelineSpec default (VK_FRONT_FACE_COUNTER_CLOCKWISE). With culling disabled, winding order is never consulted, so this is GPU-equivalent (pixel-identical, max abs diff 0).

Allowed exception: outline_pass

renderer/outline_pass.py keeps its hand-rolled vkCreateGraphicsPipelines call (a two-pass stencil silhouette pipeline with stencil state that PipelineSpec deliberately does not model). This is the single sanctioned exception, and it lives in outline_pass.py, not in gpu/pipeline.py.

Shared spec fragments (named, not duplicated)

Vertex layouts reused across multiple passes are named module-level constants in gpu/pipeline.py rather than re-declared at each call site:

  • FORWARD_VERTEX_STRIDE / FORWARD_VERTEX_ATTRS – pos/normal/uv (32 B); shared by forward, transparent, and (as a prefix) skinned mesh pipelines.

  • SKINNED_VERTEX_STRIDE / SKINNED_VERTEX_ATTRS – forward attrs + joints + weights (56 B).

  • POS_COLOUR_VERTEX_STRIDE / POS_COLOUR_VERTEX_ATTRS – pos/colour (28 B); shared by the debug-line and gizmo overlay pipelines.

  • UI_VERTEX_STRIDE / UI_VERTEX_ATTRS – 2D pos/uv/colour (32 B); shared by all three Draw2D pipelines and the Engine UI pipeline.

  • MESH_PUSH_CONSTANT_SIZE – view+proj+hdr_output push block (132 B).

Final public API (simvx.graphics.gpu.pipeline)

__all__:

  • PipelineSpec – frozen dataclass describing the varying fixed-function / layout state (topology, vertex input, rasterization, depth/stencil, blend, set layouts, push constants). Everything constant across every surveyed pass (polygon mode FILL, line width 1.0, 1x MSAA, entry point "main", dynamic viewport + scissor) is fixed inside build_pipeline and is not a spec field.

  • build_pipeline(device, spec, render_pass, extent, *, vert_module=None, frag_module=None) -> (VkPipeline, VkPipelineLayout) – the single high-level creation entry point.

  • create_shader_module(device, spirv_path) -> VkShaderModule

  • Shared fragments: FORWARD_VERTEX_STRIDE, FORWARD_VERTEX_ATTRS, SKINNED_VERTEX_STRIDE, SKINNED_VERTEX_ATTRS, POS_COLOUR_VERTEX_STRIDE, POS_COLOUR_VERTEX_ATTRS, UI_VERTEX_STRIDE, UI_VERTEX_ATTRS, MESH_PUSH_CONSTANT_SIZE.

Private primitives (not exported): _make_shader_stages, _make_vertex_input, _make_empty_vertex_input, _make_input_assembly, _make_viewport_state, _make_rasterization, _make_multisample, _make_depth_stencil, _make_colour_blend_opaque / _alpha / _none, _make_dynamic_state, _create_pipeline_layout, _build_pipeline.

No pass-specific create_*_pipeline factory remains in gpu/pipeline.py.