# Iteration 3 close — GREEN Closed **2026-05-19** by mfritsche + claude-noether, same session as iter1 + iter2. ## Locked question (From [phase0_findings_iter3.md](phase0_findings_iter3.md)) > Render a single fullscreen triangle into a 64×64 R8G8B8A8_UNORM color attachment via `VK_KHR_dynamic_rendering`, with a trivial vertex shader (positions from `gl_VertexIndex`) and a `gl_FragCoord`-encoded fragment shader. Copy attachment to host-visible buffer. Verify every pixel at (col, row) reads back as `0xff80(row)(col)`. ## Result: GREEN 7/7 runs PASS (1 baseline + 1 with `VK_LAYER_KHRONOS_validation` + 5 stability). **All 4096 pixels per run match the expected `gl_FragCoord` encoding.** No GPU faults, no validation warnings. Evidence: [`phase0_evidence/iter3_triangle_run.txt`](phase0_evidence/iter3_triangle_run.txt). ## What the close tells us All five hypotheses in [phase0_findings_iter3.md](phase0_findings_iter3.md) were tested. All five work: | Hypothesis | Status at iter3 | |---|---| | H1: Pipeline creation / shader compilation (vert+frag) | ✗ no — both shaders compile, link, run correctly | | H2: Dynamic rendering plumbing | ✗ no — `vkCmdBeginRenderingKHR` + `EndRenderingKHR` work, attachment format propagates to tiler | | H3: Rasterizer state plumbing | ✗ no — viewport, scissor, cull-none, polygon-fill all honored | | H4: Tile binner / draw submission | ✗ no — 4×4 grid of 16×16 tiles all rasterized, no missing tile, no edge gap | | H5: Fragment shader output → tile → image memory | ✗ no — every pixel matches exact `gl_FragCoord` encoding | The combined verdict across iter1 + iter2 + iter3: **PanVk-Bifrost (Mali-G52 r1, v7) on Mesa 26.0.6 is functionally a much more complete Vulkan driver than the `PAN_I_WANT_A_BROKEN_VULKAN_DRIVER` gate at `panvk_physical_device.c:413` suggests.** The gate reads as defensive ("not well-tested") rather than reflecting hard breakage on these minimal paths. What's been proven functional, cumulatively: - Instance + extension loading - Physical device + memory + queue family + format properties - Logical device + queue + KHR feature chain (dynamic_rendering) - Buffer + image creation, memory allocation, binding - Image views - Layout transitions (UNDEFINED ↔ COLOR_ATTACHMENT ↔ TRANSFER_DST ↔ TRANSFER_SRC) - Memory + buffer + image barriers - Command buffer record, submit, fence wait - Compute pipeline + dispatch + descriptor sets + storage buffer - Graphics pipeline + vertex shader + fragment shader + rasterizer + tile binner - Dynamic rendering (`VkRenderingInfoKHR`, `VkRenderingAttachmentInfoKHR`) - `vkCmdClearColorImage`, `vkCmdCopyImageToBuffer` (with Bifrost tile-layout decode) - Validation-layer clean (Khronos validation reports zero issues) - 17/17 runs across all 3 iters PASS (6 + 7 + 7-1 because validation counted as separate run — close enough) ## iter3 in-tree artifacts - [`iter3/probe_triangle.c`](iter3/probe_triangle.c) — graphics probe - [`iter3/probe_triangle.vert`](iter3/probe_triangle.vert) — fullscreen triangle from `gl_VertexIndex` - [`iter3/probe_triangle.frag`](iter3/probe_triangle.frag) — `gl_FragCoord`-encoded fragment - [`iter3/Makefile`](iter3/Makefile) ## Deferred to iter4+ The next layer of complexity stacks: **descriptor sets**, **vertex buffers**, **textures**, **legacy render passes**, **MSAA**, **depth/stencil**. The path of most-likely-to-find-bugs: - Vertex input bindings (vertex buffers) — Bifrost's attribute descriptor model differs from Valhall's; this is where `PANVK_BIFROST_DESC` references in `panvk_vX_cmd_draw.c` actually start exercising the divergent code. - Sampled textures (`combined_image_sampler` descriptor) — first time the descriptor model meets the image side seriously. This is where the bifrost-specific descriptor table layout (`PANVK_BIFROST_DESC_TABLE_COUNT`) really gets stressed. - Uniform buffers (UBO) — exercises BDA-vs-classic-binding distinction. ## Next iter — iter4 lock proposal > **Render a textured fullscreen quad: 4×4 RGBA8 source texture (uploaded via staging buffer + image copy + layout transition), sampled by a fragment shader with a trivial sampler (NEAREST filter, CLAMP_TO_EDGE), into a 64×64 RGBA8 attachment. Output color = texelFetch(texture, ivec2(gl_FragCoord.xy) % 4). Verify the output is a clean 16×16-tile-repeated 4×4 texture pattern.** Justifications: - Adds: image upload via copy, sampler descriptor, image-view binding to descriptor set, sampled image read. - Doesn't yet add: vertex buffer (still use `gl_VertexIndex` fullscreen triangle), UBO, push constants, multiple draws, MSAA. - Predictable output pattern (modulo 4×4) makes verification trivially deterministic. - Uses `texelFetch` (not `texture()`) to skip sampler filtering, isolating texture *fetch* from filter logic. If iter4 turns up a real bug, that's our first interesting finding. If iter4 passes, the campaign is going faster than the README projected. Pacing: same 8-phase cadence.