# Iteration 2 close — GREEN Closed **2026-05-19** by mfritsche + claude-noether, same session as iter1 close. ## Locked question (From [phase0_findings_iter2.md](phase0_findings_iter2.md)) > Get a minimal Vulkan image-side workload to execute end-to-end on PanVk-Bifrost: create a 4×4 `VK_FORMAT_R8G8B8A8_UNORM` image, transition UNDEFINED → TRANSFER_DST, `vkCmdClearColorImage` to 0x11223344, transition TRANSFER_DST → TRANSFER_SRC, `vkCmdCopyImageToBuffer` to host-visible staging, fence-wait, verify all 16 pixels read back as 0x44332211. ## Result: GREEN 7/7 runs PASS (1 baseline + 1 with `VK_LAYER_KHRONOS_validation` + 5 stability). All 16 pixels match exactly. No GPU faults, no MMU faults, no kernel-side panfrost messages, no validation-layer warnings or errors. Evidence: [`phase0_evidence/iter2_image_clear_run.txt`](phase0_evidence/iter2_image_clear_run.txt). ## What the close tells us Four image-side hypotheses from [phase0_findings_iter2.md](phase0_findings_iter2.md) were tested. All four work: | Hypothesis | Status at iter2 | |---|---| | H1: image creation + memory binding | ✗ no — `vkCreateImage` + `vkGetImageMemoryRequirements` + bind work for 4×4 RGBA8 optimal-tiled (4096-byte aligned allocation) | | H2: layout transitions | ✗ no — UNDEFINED→TRANSFER_DST and TRANSFER_DST→TRANSFER_SRC both clean | | H3: `vkCmdClearColorImage` lowering | ✗ no — clear lands in image correctly | | H4: `vkCmdCopyImageToBuffer` + Bifrost tile decode | ✗ no — all 16 pixels round-trip with no shuffling, no rounding error | The image-side transfer path on PanVk-Bifrost is functional for this minimal case. Combined with iter1, we now know the following work end-to-end: - Vulkan instance + physical device + logical device + queue - Buffer create + alloc + bind + map (host-visible) - Image create + alloc + bind (device-local) - Image layout transitions via `vkCmdPipelineBarrier` - `vkCmdClearColorImage` (transfer-op level, not via shader) - `vkCmdCopyImageToBuffer` with Bifrost tile-layout decode - Compute pipeline: shader module + pipeline layout + compute pipeline + dispatch - Command buffer recording + submit + fence wait + memory barriers (memory + image + buffer) What we still **don't know works**: graphics pipeline (vertex + fragment + rasterizer + render pass / dynamic rendering). ## iter2 in-tree artifacts - [`iter2/probe_image_clear.c`](iter2/probe_image_clear.c) — ~340 LoC, pure Vulkan 1.0 core - [`iter2/Makefile`](iter2/Makefile) — `make` builds, `make run` / `make run-validation` ## Deferred to iter3+ (not in iter2 scope) - Vertex + fragment shaders - Render pass and/or dynamic rendering - Graphics pipeline state (rasterizer, viewport, blend, depth) - Larger images, mipmaps, layered images, MSAA - Other formats (R32G32B32A32_SFLOAT, BC/ETC2/ASTC compressed, depth/stencil) - WSI / swapchain (iter4+) - TuxRacer / Zink-on-PanVk ## Next iter — iter3 lock proposal Smallest viable graphics workload that exercises the **rasterizer + shaders**: > **Render a single full-screen triangle into a 64×64 R8G8B8A8_UNORM color attachment via dynamic rendering (`VK_KHR_dynamic_rendering`), using a trivial vertex shader (no vertex buffer — emit positions from `gl_VertexIndex`) and a trivial fragment shader (output constant color `gl_FragCoord`-encoded so we can detect rasterizer correctness). Copy attachment to host-visible buffer. Verify: (a) some pixels are written (not all sentinel), (b) at least one pixel has the encoded `gl_FragCoord` value matching its position.** Justifications: - 64×64 (not 4×4) so multiple tiles get exercised — Bifrost is a tile-based rasterizer, so single-tile workloads might side-step real tile binning. - Dynamic rendering instead of render pass — simpler API surface, no framebuffer object, no subpass dependencies. Render pass / framebuffer can be iter3.5 if needed. - Fullscreen triangle from `gl_VertexIndex` so no vertex buffer needed — exercises pipeline-state but not vertex-input-state. - Trivial fragment shader (no textures, no UBO, no SSBO) — exercises rasterization + frag shader output but not descriptor lookups (proven in iter1 anyway). - `gl_FragCoord`-encoded color so a wrong-rasterization bug (e.g. swapped-Y framebuffer convention, off-by-pixel) is detectable from pixel data. If iter3 turns up the first real failure, that's the campaign's first interesting bug. If iter3 also passes, iter4 adds vertex buffer + UBO + a texture sample, and we're well into "actually exercising PanVk-Bifrost" territory. Pacing: same 8-phase cadence. iter3 phase 0 substrate lock when the operator opens.