# 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: ```python 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`.