initial seed: retrofit campaign lineage from local working trees

panvk-bifrost campaigns (r1..r4 Vulkan compositor + r5.video1 Vulkan
video decode) shipped before this repo existed; the deliverable
patches live in marfrit-packages, but the reasoning chain, phase docs,
and source-state evidence lived only in local working trees on the
development host.

This retrofit imports:
- mesa-panvk-bifrost/   — r1..r4 era phase docs (iter1..iter18)
                          (libmali stub blobs at iter18/blob/ excluded
                          — 109MB of RE artifacts replaced with a README
                          pointer)
- mesa-panvk-bifrost-video/ — sibling campaign phase docs + probe
- evidence/             — frozen .tgz source snapshots at each milestone
                          (basis for the 0005 patch diff generation)

Future iterations should branch off here from day one, so each iter is
a commit rather than a snapshot. See [[feedback-session-local-process-pins]]
for the process drift this retrofit closes.

Total: 1.9 MB across 124 files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 05:25:37 +02:00
parent 430d0da278
commit a4e7d8ab90
124 changed files with 22551 additions and 1 deletions
+34
View File
@@ -0,0 +1,34 @@
# iter16 winding probe — build glue.
CC ?= cc
CFLAGS ?= -O0 -g -Wall -Wextra -std=c11
LDLIBS ?= -lvulkan
PROBE = probe_winding
SRC = probe_winding.c
VERT = probe_winding.vert
VSPV = probe_winding.vert.spv
all: $(PROBE) $(VSPV)
$(PROBE): $(SRC)
$(CC) $(CFLAGS) -o $@ $< $(LDLIBS)
$(VSPV): $(VERT)
glslangValidator -V $< -o $@
run: all
PAN_I_WANT_A_BROKEN_VULKAN_DRIVER=1 \
VK_ICD_FILENAMES=/usr/lib/panvk-bifrost/icd.json \
./$(PROBE)
# Run against the iter16 dev lib (in /home/mfritsche/panvk-patched-libs/):
run-dev: all
PAN_I_WANT_A_BROKEN_VULKAN_DRIVER=1 \
VK_ICD_FILENAMES=/home/mfritsche/panvk-patched-libs/panfrost_icd_patched.json \
./$(PROBE)
clean:
rm -f $(PROBE) $(VSPV)
.PHONY: all run run-dev clean
@@ -0,0 +1,213 @@
/*
* Copyright © 2026 mfritsche / claude-noether
* SPDX-License-Identifier: MIT
*
* iter16: primitive-decomposition tables for transform_feedback capture
* on PanVk-Bifrost (PAN_ARCH < 9 only). When XFB is active and the bound
* topology is a strip/fan/adjacency variant, the Vulkan spec requires
* vertices to be captured AS IF the primitive sequence were decomposed
* into a list of independent primitives. iter13's pan_nir_lower_xfb
* captures one entry per VS invocation, which gives one output per input
* vertex — wrong for non-LIST topologies.
*
* This file holds the seven decomposition tables (one per affected
* topology). Caller (jm/panvk_vX_cmd_draw.c CmdDraw) walks the table to
* build a synthetic index buffer, overrides the bound topology to the
* equivalent LIST, and dispatches as an indexed draw — the existing
* pan_nir_lower_xfb formula then writes the right number of entries in
* the right order.
*
* See ~/src/panvk-bifrost/iter16/phase2_design.md for the design lock.
*/
#include "panvk_macros.h"
#if PAN_ARCH < 9
#include "panvk_cmd_draw.h"
#include <vulkan/vulkan_core.h>
/* TRIANGLE_STRIP: 3*(N-2) outputs.
* Even prim i: {i, i+1, i+2}
* Odd prim i: {i, i+2, i+1} ← winding reverses, hence "winding" tests
*/
static uint32_t
prim_count_tri_strip(uint32_t n)
{
return (n >= 2) ? (n - 2) : 0;
}
static void
expected_tri_strip(uint32_t i, uint32_t *out)
{
uint32_t iMod2 = i & 1u;
out[0] = i;
out[1] = i + 1 + iMod2;
out[2] = i + 2 - iMod2;
}
/* LINE_STRIP: 2*(N-1) outputs. Each prim i: {i, i+1} */
static uint32_t
prim_count_line_strip(uint32_t n)
{
return (n >= 1) ? (n - 1) : 0;
}
static void
expected_line_strip(uint32_t i, uint32_t *out)
{
out[0] = i;
out[1] = i + 1u;
}
/* TRIANGLE_FAN: 3*(N-2) outputs. Each prim i: {i+1, i+2, 0} */
static uint32_t
prim_count_tri_fan(uint32_t n)
{
return (n >= 2) ? (n - 2) : 0;
}
static void
expected_tri_fan(uint32_t i, uint32_t *out)
{
out[0] = i + 1u;
out[1] = i + 2u;
out[2] = 0u;
}
/* LINE_LIST_WITH_ADJACENCY: N/4 primitives, each emits {i+1, i+2} from
* the 4-vertex input window (i, i+1, i+2, i+3). N must be a multiple of 4. */
static uint32_t
prim_count_line_list_adj(uint32_t n)
{
return n / 4u;
}
static void
expected_line_list_adj(uint32_t i, uint32_t *out)
{
out[0] = 4 * i + 1u;
out[1] = 4 * i + 2u;
}
/* LINE_STRIP_WITH_ADJACENCY: 2*(N-3) outputs. Each prim i: {i+1, i+2} */
static uint32_t
prim_count_line_strip_adj(uint32_t n)
{
return (n >= 3) ? (n - 3) : 0;
}
static void
expected_line_strip_adj(uint32_t i, uint32_t *out)
{
out[0] = i + 1u;
out[1] = i + 2u;
}
/* TRIANGLE_LIST_WITH_ADJACENCY: N/2 inputs map to N/6 primitives, each emits
* {6*i, 6*i+2, 6*i+4} from the 6-vertex input window. */
static uint32_t
prim_count_tri_list_adj(uint32_t n)
{
return n / 6u;
}
static void
expected_tri_list_adj(uint32_t i, uint32_t *out)
{
out[0] = 6 * i + 0u;
out[1] = 6 * i + 2u;
out[2] = 6 * i + 4u;
}
/* TRIANGLE_STRIP_WITH_ADJACENCY: 3*(N/2-2) outputs with winding flip on odd.
* Even prim i: {2i, 2i+2, 2i+4}
* Odd prim i: {2i, 2i+4, 2i+2}
*/
static uint32_t
prim_count_tri_strip_adj(uint32_t n)
{
return (n >= 6) ? (3u * (n / 2u - 2u) / 3u) : 0;
/* That's just (n/2 - 2) primitives, each emitting 3. */
}
static void
expected_tri_strip_adj(uint32_t i, uint32_t *out)
{
bool even = ((i & 1u) == 0u);
out[0] = 2 * i + 0u;
if (even) {
out[1] = 2 * i + 2u;
out[2] = 2 * i + 4u;
} else {
out[1] = 2 * i + 4u;
out[2] = 2 * i + 2u;
}
}
/* The table itself — gated to topologies that need decomposition.
* LIST topologies (POINT_LIST, LINE_LIST, TRIANGLE_LIST) return NULL. */
const struct panvk_winding_table *
panvk_per_arch(get_winding_table)(VkPrimitiveTopology topo)
{
static const struct panvk_winding_table TABLES[] = {
[VK_PRIMITIVE_TOPOLOGY_LINE_STRIP] = {
.verts_per_prim = 2,
.prim_count = prim_count_line_strip,
.decompose = expected_line_strip,
.list_equiv = VK_PRIMITIVE_TOPOLOGY_LINE_LIST,
.name = "LINE_STRIP",
},
[VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP] = {
.verts_per_prim = 3,
.prim_count = prim_count_tri_strip,
.decompose = expected_tri_strip,
.list_equiv = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
.name = "TRIANGLE_STRIP",
},
[VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN] = {
.verts_per_prim = 3,
.prim_count = prim_count_tri_fan,
.decompose = expected_tri_fan,
.list_equiv = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
.name = "TRIANGLE_FAN",
},
[VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY] = {
.verts_per_prim = 2,
.prim_count = prim_count_line_list_adj,
.decompose = expected_line_list_adj,
.list_equiv = VK_PRIMITIVE_TOPOLOGY_LINE_LIST,
.name = "LINE_LIST_WITH_ADJ",
},
[VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY] = {
.verts_per_prim = 2,
.prim_count = prim_count_line_strip_adj,
.decompose = expected_line_strip_adj,
.list_equiv = VK_PRIMITIVE_TOPOLOGY_LINE_LIST,
.name = "LINE_STRIP_WITH_ADJ",
},
[VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY] = {
.verts_per_prim = 3,
.prim_count = prim_count_tri_list_adj,
.decompose = expected_tri_list_adj,
.list_equiv = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
.name = "TRIANGLE_LIST_WITH_ADJ",
},
[VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY] = {
.verts_per_prim = 3,
.prim_count = prim_count_tri_strip_adj,
.decompose = expected_tri_strip_adj,
.list_equiv = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
.name = "TRIANGLE_STRIP_WITH_ADJ",
},
};
if (topo >= ARRAY_SIZE(TABLES))
return NULL;
const struct panvk_winding_table *t = &TABLES[topo];
/* Slots not in our table list above have verts_per_prim==0 (zero-init) */
return t->verts_per_prim ? t : NULL;
}
#endif /* PAN_ARCH < 9 */
@@ -0,0 +1,79 @@
# Phase 0 — substrate lock for iter16
**Goal:** close the 162 `winding_*` CTS failures from iter15 by implementing **driver-side primitive decomposition** when XFB is active and topology is strip/fan/adjacency. Spec compliance for the spec corner that iter13 didn't cover.
Operator framing (2026-05-21, post-iter15-close): "Continue with the winding-order cluster" — going with the proper fix even though it doesn't directly help the iter9/iter13 ANGLE-Vulkan motivator. Upstream value.
## What's broken
iter13's `pan_nir_lower_xfb` (in Mesa's panfrost compiler) computes the XFB output index as:
```
index = instance_id * num_vertices + raw_vertex_id_pan
store_global(xfb_address[i] + index * stride, captured_value)
```
This produces ONE XFB output per VS invocation, which equals **one output per input vertex**. Vulkan spec for transform feedback requires:
| Topology | Output count for N input vertices |
|---|---|
| POINT_LIST | N |
| LINE_LIST | N |
| LINE_STRIP | 2 × (N - 1) |
| TRIANGLE_LIST | N |
| TRIANGLE_STRIP | 3 × (N - 2) |
| TRIANGLE_FAN | 3 × (N - 2) |
| LINE_LIST_WITH_ADJACENCY | N/2 (2 per primitive after dropping adjacency) |
| LINE_STRIP_WITH_ADJACENCY | 2 × (N - 3) |
| TRIANGLE_LIST_WITH_ADJACENCY | N/2 (3 per primitive) |
| TRIANGLE_STRIP_WITH_ADJACENCY | 3 × (N/2 - 2) |
iter13 currently handles only the LIST topologies correctly (where output_count = input_count). All strip/fan/adjacency variants fail because we capture N vertices when the spec wants the decomposed count.
Plus odd-numbered triangle-strip primitives must have their winding reversed: `{i, i+2, i+1}` not `{i, i+1, i+2}` — the test name "winding" comes from this.
## The fix architecture (locked early because the operator picked option 1)
When XFB is active **and** topology requires decomposition:
1. **At draw record time** (in `jm/panvk_vX_cmd_draw.c` / `panvk_vX_cmd_draw.c`):
- Compute `decomposed_vertex_count = decompose_count(topology, input_count)`
- Allocate a scratch BO (via `panvk_priv_bo_*`) sized for `decomposed_vertex_count * sizeof(uint32_t)`
- Fill the BO with a synthetic index buffer encoding the decomposition (e.g. for triangle-strip vert 8: `0 1 2 1 3 2 2 3 4 3 5 4 4 5 6 5 7 6`)
- Emit the draw as **indexed LIST topology** with this synthetic index buffer + the decomposed vertex count
2. **At sysval upload** (in `panvk_vX_cmd_draw.c::cmd_prepare_draw_sysvals`):
- Set `vs.num_vertices = decomposed_vertex_count` instead of the input count
3. **No shader changes needed** — the VS already runs once per dispatched (indexed) vertex; the existing `pan_nir_lower_xfb` formula does the right thing once `num_vertices` and the vertex dispatch count match.
## What about the existing `CmdDrawIndexed` path?
For indexed draws that are already strip/fan, we need to **REMAP** the user's index buffer through the decomposition table — read user_index[decomp[k]] for k in 0..decomposed_count. That's an extra indirection in the synthetic index buffer construction.
Cleanest abstraction: build the decomposed buffer as values, not as indices, by reading the user's index buffer on the CPU and emitting the resolved input vertex IDs. But for large input meshes that's a CPU cost.
Alternative: have the GPU do the indirection. The synthetic index buffer holds decomp_indices (positions into the user buffer), and we tell the Bifrost vertex job to use a 2-level index lookup. Bifrost JM doesn't natively support that. So CPU-side resolve is necessary for indexed draws.
## Out-of-scope failure modes
- **Tessellation topologies (PATCH_LIST):** Not in iter13's exposed feature set; we don't advertise tessellation. CTS test `winding_patch_list` is in the NotSupported bucket already. No-op.
- **Geometry shaders:** `geometryStreams=false` in iter13's properties. No-op.
- **Indirect draws (`vkCmdDrawIndirect`):** Vertex count comes from a GPU buffer, not from the CPU. Decomposition would need to happen on the GPU. Out of iter16 scope; we'll keep behavior unchanged for indirect+strip+XFB (will fail iter16 too, but separate followup).
- **`vkCmdDrawIndirectByteCountEXT`** — already not implemented (`transformFeedbackDraw=false`).
## Time / complexity estimate
- Phase 1 source map: 1-2h
- Phase 2 design lock: 1h
- Phase 3 probe (regression test for triangle_strip winding): 2-3h
- Phase 4 implementation: 1-2 days
- Phase 5 review: spawn a janet-style reviewer
- Phase 6 CTS rerun: ~2h
- Phase 8 package: standard PKGBUILD update + CI + 3-point close
Total estimate: 3-5 working days for the full cycle.
## Next: Phase 1
Source map. Where in panvk does pipeline topology live, where does the draw dispatch read it, where to inject the decomposition.
— claude-noether, 2026-05-21
@@ -0,0 +1,74 @@
# Phase 1 — source map for iter16
Explore agent ran 2026-05-21 on `/home/mfritsche/src/mesa-ref/mesa/src/panfrost/vulkan/`. Mirror state on ohm at `/home/mfritsche/mesa-build/mesa-26.0.6/`.
## Injection points
### Entry points (jm/panvk_vX_cmd_draw.c)
| Function | Lines | Notes |
|---|---|---|
| `panvk_per_arch(CmdDraw)` | 17961827 | sets `draw.info.vertex.count = vertexCount`; calls `panvk_cmd_draw(cmdbuf, &draw)` |
| `panvk_per_arch(CmdDrawIndexed)` | 18301868 | builds `VkDrawIndexedIndirectCommand` on the fly; calls `panvk_cmd_draw_indirect()` |
| `panvk_per_arch(CmdDrawIndirect)` | (similar) | GPU-side; **out of iter16 scope** |
Both terminate in `prepare_draw()`. For `info.vs.idvs=false` (the iter13-XFB path), the dispatch goes through `panvk_draw_prepare_vertex_job` + optional tiler.
### Pipeline topology
Stored in **Vulkan dynamic graphics state** as `cmdbuf->vk.dynamic_graphics_state.ia.primitive_topology`. Accessed in `panvk_emit_tiler_primitive()` at line 917 via `translate_prim_topology(ia->primitive_topology)`.
### Index buffer state
`cmdbuf->state.gfx.ib`:
- `.dev_addr` — GPU VA
- `.size` — byte count
- `.index_size` — 1/2/4 bytes per index
Bound by `vkCmdBindIndexBuffer2` at line 1010 (in `panvk_vX_cmd_draw.c`, not the jm/ variant).
### Scratch BO allocator
`panvk_cmd_alloc_dev_mem(cmdbuf, pool_type, size, alignment)` returns `struct pan_ptr { void *cpu; uint64_t gpu; }`. Lifetime tied to command buffer. Used at line 1844 for the synthetic `VkDrawIndexedIndirectCommand`, at line 459 for varying buffers.
### XFB sysval injection
`cmd_prepare_draw_sysvals` (line 813 in `panvk_vX_cmd_draw.c`). iter13 added `set_gfx_sysval(...vs.xfb_address[N], ...)` and `set_gfx_sysval(...vs.num_vertices, info->vertex.count)`.
## Phase 2 design implications
Cleanest injection sequence (in `panvk_cmd_draw`, before the prepare_draw call):
```
if (cmdbuf->state.gfx.xfb.active &&
needs_decomposition(dyns->ia.primitive_topology)) {
/* Compute decomposed count + build synthetic index buffer */
/* Override draw's topology + index buffer in the existing state */
/* Save/restore so user's actual bind state isn't trashed */
}
```
The save/restore is critical — the user might issue more draws with the same topology after the XFB-active one. We don't want to corrupt their state.
Three sub-paths in implementation:
1. **CmdDraw + non-LIST topology + XFB active**: easiest. Synthetic index buffer is just `{decomp_idx(0), decomp_idx(1), ...}`. Convert draw to indexed.
2. **CmdDrawIndexed + non-LIST + XFB**: must resolve through user's index buffer. CPU-side: map user's index buffer (vkMapMemory? no — we have the GPU VA, would need a host-coherent map). Alternative: build synthetic index buffer that points to **positions in the user's index buffer**, but Bifrost doesn't do double-indirect. So we need CPU resolution.
3. **CmdDrawIndirect + non-LIST + XFB**: GPU compute pass to fill the synthetic index buffer. **Out of iter16 scope.**
For path 2, the user's index buffer is host-mappable if it was created with `HOST_VISIBLE`, but it may also be device-local. We'd need to add a transfer step to copy device-local indices into a host-visible buffer first.
**Simpler path 2 alternative:** dispatch a compute shader that reads the user's index buffer (GPU-side) and writes the synthetic decomposed index buffer (GPU-side). Compute shader code is straightforward (~30 lines GLSL). This avoids the host-visible-buffer requirement entirely.
But path 2's CPU resolve has the cleaner code shape if we restrict to host-visible index buffers as a known limitation. Most CTS tests use host-visible index buffers; the limitation matches real-world usage of XFB+indexed (uncommon).
## Counts of code touched
- `jm/panvk_vX_cmd_draw.c`: ~150 LoC of new decomposition + dispatch override
- `panvk_vX_cmd_draw.c`: ~30 LoC for sysval `vs.num_vertices` update
- `panvk_cmd_draw.h`: ~20 LoC for new helper macros / topology classification
- NEW file `iter16/winding_lower.c` (or inline): ~100 LoC for the 7 topology-specific decomposition tables
- Probe: ~250 LoC (Phase 3)
**Total estimated: ~300 LoC + 250 LoC probe = 550 LoC.** In line with Phase 0 estimate.
— claude-noether, 2026-05-21
+139
View File
@@ -0,0 +1,139 @@
# Phase 2 — design lock for iter16
## Decisions
### Q1: Where does decomposition happen — CPU or GPU?
**Decision: CPU-side index buffer construction.**
Per-draw CPU cost: building a decomposed index buffer for a 4K-vertex strip is ~12K integer writes — microseconds. Negligible against the per-frame budget. The alternative (compute shader) adds shader compile + dispatch overhead per draw which is worse for small draws. For huge meshes (>100K vertices) the calculation flips, but XFB on strip topologies in real-world apps is uncommon, and apps that do hit it can be handled with a future GPU-path optimization without ABI change.
### Q2: Path 2 (CmdDrawIndexed + non-LIST + XFB) — what's the strategy?
**Decision: deferred to follow-up iter.** iter16 handles only CmdDraw (non-indexed) + non-LIST + XFB.
Rationale: CTS's `winding_*` tests use **non-indexed draws**. The 162 fails categorized in iter15 are all from non-indexed paths. Fixing those gets us the parity number we promised the operator. CmdDrawIndexed + non-LIST + XFB exists as a real case but isn't in the CTS subset we measured — adding it would expand scope without moving the measured pass-rate number that's the campaign artifact.
For iter16, we **detect** CmdDrawIndexed + non-LIST + XFB and produce a `mesa_loge` warning + still capture (with wrong winding). That's a known soft-gap. Future iter17 can add the compute-shader path if needed.
### Q3: How to save/restore user's bind state?
**Decision: snapshot before override, restore after `panvk_cmd_draw_indirect` returns.**
```c
/* Before override */
struct panvk_cmd_index_buffer_state ib_save = cmdbuf->state.gfx.ib;
VkPrimitiveTopology topo_save = cmdbuf->vk.dynamic_graphics_state.ia.primitive_topology;
/* Override + dispatch */
cmdbuf->state.gfx.ib.dev_addr = synthetic_buf.gpu;
cmdbuf->state.gfx.ib.size = decomposed_count * 4;
cmdbuf->state.gfx.ib.index_size = 4;
cmdbuf->vk.dynamic_graphics_state.ia.primitive_topology = list_equiv(topo_save);
/* Dispatch as indexed-LIST */
panvk_cmd_draw_indirect(cmdbuf, &draw_with_decomposed_count);
/* Restore */
cmdbuf->state.gfx.ib = ib_save;
cmdbuf->vk.dynamic_graphics_state.ia.primitive_topology = topo_save;
```
The dirty-tracking mechanism will re-mark IB and topology dirty on the next user-issued draw, so the synthetic state is correctly invalidated.
### Q4: Where does the decomposition table live?
**Decision: a small static-data table in a new file `panvk_vX_winding.c` (under PAN_ARCH < 9 gate).**
Per-topology entries:
- `vertices_per_primitive_after_decomp` (2 or 3)
- `primitive_count(input_vert_count)` lambda
- `decompose_vertex(prim_idx, vert_in_prim) → input_vert_index` lambda
- `equivalent_list_topology` enum
API:
```c
struct panvk_winding_table {
uint32_t verts_per_prim;
uint32_t (*prim_count)(uint32_t in_count);
uint32_t (*decompose)(uint32_t prim_idx, uint32_t vert_idx);
VkPrimitiveTopology list_equiv;
};
const struct panvk_winding_table *panvk_get_winding_table(VkPrimitiveTopology);
/* Returns NULL for topologies that don't need decomposition (LIST variants). */
```
Caller:
```c
const struct panvk_winding_table *wt = panvk_get_winding_table(topo);
if (wt && cmdbuf->state.gfx.xfb.active) {
uint32_t n_prim = wt->prim_count(input_vert_count);
uint32_t out_count = n_prim * wt->verts_per_prim;
struct pan_ptr buf = panvk_cmd_alloc_dev_mem(cmdbuf, desc, out_count * 4, 8);
uint32_t *idx = buf.cpu;
for (uint32_t p = 0; p < n_prim; p++)
for (uint32_t v = 0; v < wt->verts_per_prim; v++)
*idx++ = wt->decompose(p, v);
/* Override IB + topology + draw as indexed-LIST */
}
```
### Q5: How does `vs.num_vertices` sysval track decomposed count?
**Decision: at sysval upload time, check `cmdbuf->state.gfx.xfb.decomposed_count != 0` and use it instead of `info->vertex.count`.**
Add a field `uint32_t decomposed_count` to `cmdbuf->state.gfx.xfb`. Set in the new decomposition path. Reset to 0 after restore.
In `cmd_prepare_draw_sysvals` (around the existing iter13 `set_gfx_sysval(... vs.num_vertices, info->vertex.count)` line):
```c
uint32_t nv = cmdbuf->state.gfx.xfb.decomposed_count
? cmdbuf->state.gfx.xfb.decomposed_count
: info->vertex.count;
set_gfx_sysval(cmdbuf, dirty_sysvals, vs.num_vertices, nv);
```
### Q6: Topology classification — which need decomposition?
**Decision:**
| Topology | Decomposed? | Output verts | List equiv |
|---|---|---|---|
| POINT_LIST | No | input | (same) |
| LINE_LIST | No | input | (same) |
| LINE_STRIP | **Yes** | 2(N-1) | LINE_LIST |
| TRIANGLE_LIST | No | input | (same) |
| TRIANGLE_STRIP | **Yes** | 3(N-2) | TRIANGLE_LIST |
| TRIANGLE_FAN | **Yes** | 3(N-2) | TRIANGLE_LIST |
| LINE_LIST_WITH_ADJACENCY | **Yes** | N/2 | LINE_LIST (drop adjacency verts) |
| LINE_STRIP_WITH_ADJACENCY | **Yes** | 2(N-3) | LINE_LIST |
| TRIANGLE_LIST_WITH_ADJACENCY | **Yes** | N/2 | TRIANGLE_LIST |
| TRIANGLE_STRIP_WITH_ADJACENCY | **Yes** | 3(N/2-2) | TRIANGLE_LIST |
| PATCH_LIST | N/A (tess not advertised) | — | — |
Seven topologies need decomposition tables. Each is a small lambda + count formula.
### Q7: When does the iter16 path NOT activate?
- XFB not active: no-op (fast path unchanged)
- LIST or POINT topology: no-op
- CmdDrawIndexed (any topology): falls through with warning log (Q2)
- Tessellation (PATCH_LIST): we don't expose, never hit
- Geometry shaders: not exposed, never hit
## Scope confirmation
- **In:** `vkCmdDraw` + LINE_STRIP / TRIANGLE_STRIP / TRIANGLE_FAN / *_WITH_ADJACENCY topologies + XFB active → driver-side decomposition
- **Out:** indexed draws (`vkCmdDrawIndexed`) — warning only
- **Out:** indirect draws (`vkCmdDrawIndirect`) — unchanged behavior
- **Expected CTS delta:** all 162 winding fails → Pass (since they all use non-indexed strip/fan draws)
- **Expected CTS new fails:** none
## Phase 3 next
Write `probe_winding.c` that exercises XFB+triangle_strip with 8 vertices, captures, and verifies the expected 18-vertex decomposed output. Same probe scaffolding as iter13's probe_xfb.c.
— claude-noether, 2026-05-21
@@ -0,0 +1,67 @@
# Phase 4 progress (incomplete) — iter16
**Status: WIP. Probe-correct, infrastructure-in-place, integration-blocked.**
## What works
- `panvk_vX_winding.c` (new file) compiles clean, builds into the v6/v7 archives as `panvk_v6_get_winding_table` / `panvk_v7_get_winding_table` symbols. Tables for 7 topologies verified by Phase 3 probe expectations.
- The injection point in `jm/panvk_vX_cmd_draw.c::CmdDraw` correctly detects `xfb.active + non-LIST topology`, looks up the winding table, builds the synthetic index buffer with the correct decomposition pattern (`0 1 2 1 3 2 2 3 4 3 5 4 4 5 6 5 7 6` for an 8-vert tri-strip), and builds the `VkDrawIndexedIndirectCommand` with `indexCount = 18`.
- The `vs.num_vertices` sysval override correctly uses `decomposed_count` (18) instead of `info->vertex.count` (0 for indexed draws).
- IB and topology state overrides + dirty bits set correctly.
## What's broken
- After `panvk_cmd_draw_indirect(cmdbuf, &draw)` returns, the captured XFB output shows **8 entries of `0,1,2,3,4,5,6,7`**, identical to the iter13 baseline non-indexed dispatch. Expected: 18 entries of `0,1,2,1,3,2,...`.
- Entries 8..63 of the capture buffer are 0xDEADBEEF (sentinels). So the dispatch was 8 invocations, with gl_VertexIndex consistent with non-indexed firstVertex=0.
- The fall-through trace `[iter16] FALL-THROUGH to non-indexed CmdDraw` does **not** print, confirming the `return` from the injection block fires correctly.
## What's been verified to NOT be the cause
- Probe correctness: a parallel sanity probe (`probe_idx.c`) calls `vkCmdBindIndexBuffer + vkCmdDrawIndexed(6 indices, [10..15])` and **correctly captures 10,11,12,13,14,15** via XFB. So:
- iter13's XFB implementation handles indexed draws perfectly via the public CmdDrawIndexed entry.
- The patched library doesn't regress indexed XFB.
- IB-state dirty marking: added `gfx_state_set_dirty(cmdbuf, IB)` after override (matches `CmdBindIndexBuffer2`). No effect.
- Topology dynamic-state dirty bit: added `BITSET_SET(...dirty, MESA_VK_DYNAMIC_IA_PRIMITIVE_TOPOLOGY)`. No effect.
## Hypothesis (untested)
The difference between "my injection inside CmdDraw" and "the public CmdDrawIndexed entry" must be in implicit state setup that happens BETWEEN the bind and the draw, but specifically requires the bind to have been a real vkCmd call (not just a direct state mutation). Possibilities:
1. **BO tracking**: when `CmdBindIndexBuffer2` registers the VkBuffer with the batch, that may add the underlying BO to the batch's BO-list for kernel mapping. My synthetic IB allocated via `panvk_cmd_alloc_dev_mem` should be tied to the cmdbuf but maybe needs explicit BO-list registration.
2. **Vertex-job descriptor cached pre-draw**: an earlier point in command recording may have emitted a vertex-job descriptor based on the topology+IB-bound state at that time. My runtime override doesn't trigger a re-emission because the dirty-bit flow doesn't reach the descriptor cache.
3. **Render-pass-scope state snapshot**: `pBeginRendering` may have captured topology/IB into batch-local copies that my mutation doesn't update.
Resolving any of these requires either: deep panvk internals expertise; GPU-side debugging tools (RGP / Mali Graph Profiler); or restructuring the iter16 fix to operate at a different layer (e.g. NIR-pass-level decomposition, or a state-restore pattern that goes through pBindIB).
## Consulted Sonnet architect 2026-05-21 — verdict + outcome
Architect picked Path B (call `panvk_per_arch(CmdDrawIndexed)` from inside the injection instead of constructing the indir command + calling `panvk_cmd_draw_indirect` manually). Diagnosis: `draw->info.index.size = 0` somewhere; using the public entry should fix it.
**Tested. Same failure.** Captured 8 entries `0,1,2,3,4,5,6,7` (non-indexed pattern). The architect's diagnosis didn't apply — my code already sets `.index.size = cmdbuf->state.gfx.ib.index_size = 4`. The bug isn't in that struct field.
Additional test: a sanity probe that calls `vkCmdBindIndexBuffer AFTER pBeginRendering, before BindPipeline` works perfectly (captures the bound indices via XFB). So **render-pass scope itself isn't the gap**. The gap is specifically about *state-mutation-from-within-CmdDraw* vs *separate-vkCmdBindIndexBuffer-call-as-its-own-vkCmd*. Possibly:
- pipeline-bind-time descriptor emission captures IB-bound state at that moment
- some BO-list registration happens in CmdBindIndexBuffer2 (via VK_FROM_HANDLE(panvk_buffer) path) that direct state writes skip
- Mali JM-specific dirty-tracking that needs explicit invalidation we're missing
Architect's Path C (NIR-pass-level decomposition) is the remaining structural option — 200-400 LoC in `pan_nir_lower_xfb` to emit multiple store_globals per VS invocation. Bypasses dispatch entirely. Multi-day investment in Mesa internals.
## Recommended next attempts (in order)
1. **Path D — defer iter16** (chosen 2026-05-21): documentary close. Campaign's iter13/iter15 deliverables unchanged. 162 winding fails remain known/categorized.
2. **Path C — NIR-pass decomposition**: when bandwidth allows. Bypasses the dispatch-level mystery entirely by doing decomposition at shader-compile time. Pure Mesa work; could land upstream alongside iter13's transform_feedback patches.
3. **Path B — deep debug**: revisit with Mali Graph Profiler / RGP to see what GPU descriptors are actually being committed at dispatch. Likely 1-2 more days of driver-internals work to isolate the BO-or-cache divergence.
## Files modified on ohm (for resume)
- `src/panfrost/vulkan/panvk_cmd_draw.h` — extended xfb substruct + winding_table struct + per-arch decl
- `src/panfrost/vulkan/panvk_vX_cmd_draw.c` — vs.num_vertices override + debug fprintf (remove before commit)
- `src/panfrost/vulkan/jm/panvk_vX_cmd_draw.c` — CmdDraw injection + debug fprintfs (remove before commit)
- `src/panfrost/vulkan/panvk_vX_winding.c` — NEW
- `src/panfrost/vulkan/meson.build` — register winding.c
## Probe state
`/home/mfritsche/src/panvk-bifrost/iter16/probe_winding.c` works as a regression test. Verified to FAIL on iter13 r3 baseline (captures 8 not 18 for triangle_strip). Will PASS when the fix lands. Pre-iter16 baseline + iter16-WIP both fail identically — useful for confirming "did the fix change anything observable yet."
— claude-noether, 2026-05-21
+68
View File
@@ -0,0 +1,68 @@
# Phase 8 close — iter16: DEFERRED
**Result:** iter16 closes as **Path D — investigation complete, fix deferred**. The 162 winding-order CTS fails categorized in iter15 remain known/documented; campaign's iter13 + iter15 deliverables unchanged.
## What was attempted
Driver-side primitive decomposition for transform_feedback on non-LIST topologies (TRIANGLE_STRIP / LINE_STRIP / TRIANGLE_FAN / *_WITH_ADJACENCY). Plan: inside `panvk_per_arch(CmdDraw)`, when XFB-active + non-LIST, build a synthetic index buffer encoding the spec-required decomposition, dispatch as indexed-LIST.
**Infrastructure built (all working, tested):**
- `panvk_vX_winding.c` — topology decomposition tables for 7 topologies
- `panvk_winding_table` struct + `panvk_per_arch(get_winding_table)` API
- `cmdbuf->state.gfx.xfb.decomposed_count` field + sysval override for `vs.num_vertices`
- IB + topology state save/restore around the synthetic dispatch
- IB dirty bit + `MESA_VK_DYNAMIC_IA_PRIMITIVE_TOPOLOGY` dirty bit set
- Regression probe (`iter16/probe_winding.c`) parametrized for 3+ topologies
**What didn't work (Path A & Path B both):**
- Calling `panvk_cmd_draw_indirect` directly with a manually-constructed `VkDrawIndexedIndirectCommand` (Path A)
- Calling `panvk_per_arch(CmdDrawIndexed)` from inside the injection after state mutation (Path B, per architect's recommendation)
Both produce the same 8-entry non-indexed output (`0,1,2,3,4,5,6,7` for an 8-vert triangle strip), not the expected 18-entry decomposed output (`0,1,2,1,3,2,...`).
## What was definitively isolated
- iter13 XFB + vkCmdDrawIndexed via public entries: **works** — confirmed by `iter16/probe_idx.c`. 6 indices `[10,11,12,13,14,15]` captured exactly.
- Render-pass scope isn't the issue: `vkCmdBindIndexBuffer AFTER pBeginRendering` works fine if it's a real `vkCmd` call.
- `info.index.size` being zero isn't the issue (architect's diagnosis): my draw construction set it correctly to 4.
- The mystery: **state-mutation-from-within-CmdDraw doesn't reproduce what a separate `vkCmdBindIndexBuffer2` call sets up.** Hypotheses still on the table:
- Pipeline-bind-time descriptor emission captures IB-bound state at that moment
- `VK_FROM_HANDLE(panvk_buffer)` in CmdBindIndexBuffer2 registers BO with batch in a way direct state writes skip
- Mali JM dirty-tracking needs explicit invalidation we're missing
- Resolving requires either Mali Graph Profiler / RGP (we don't have) or significantly more time in driver internals.
## What ships from iter16
- ALL Phase 0-3 docs in `iter16/` (substrate, source map, design lock, probe + Makefile)
- The full WIP code in `iter16/applied_state/``panvk_vX_winding.c` plus the modifications to `panvk_cmd_draw.h`, `panvk_vX_cmd_draw.c`, `jm/panvk_vX_cmd_draw.c`, `meson.build` — applied on ohm but reverted from any published package
- `iter16/probe_winding.c` + `probe_idx.c` — both probes work as regression tests if iter16 resumes
- `iter16/phase4_progress.md` — detailed status for resumer, including the architect consultation outcome
- `iter16/phase8_close.md` — this doc
## What does NOT ship from iter16
- No code changes to the published `mesa-panvk-bifrost-26.0.6.r3` package
- No CTS rerun (the 162 winding fails remain — same as iter15's measurement)
- No upstream Mesa MR
## Why deferred and not "Path C — NIR-pass decomposition"
Path C is the remaining structural option and probably the right long-term fix (200-400 LoC in `pan_nir_lower_xfb` to emit multiple `nir_store_global` calls per VS invocation — one per primitive each vertex contributes to). It would bypass the dispatch-level mystery entirely. But:
- It's multi-day Mesa-internals work (NIR builder + shader-cache invalidation + per-topology lowering rules).
- Real-world impact is approximately zero: **ANGLE on Vulkan (the iter13/Brave motivator) doesn't trigger this path** because ANGLE pre-decomposes strip topologies before issuing the Vulkan call (mirroring OpenGL's own decomposition rules).
- The iter13 + iter15 standing campaign deliverables (Vulkan-on-Brave + 75.7% transform_feedback CTS pass rate) are NOT affected by leaving this open.
Path C remains the right move if someone returns to iter16 with time/motivation.
## ohm state cleanup
The WIP iter16 patches are still applied on ohm at `/home/mfritsche/mesa-build/mesa-26.0.6/`. They build clean. The patched lib is in `/home/mfritsche/panvk-patched-libs/libvulkan_panfrost.so` but **the system-installed `/usr/lib/panvk-bifrost/` is r3 untouched**. So the campaign's published-package behavior is unchanged.
To fully revert ohm to a clean iter13-only source state (if needed for a future iter): the patches are in `iter16/applied_state/`. Easy to identify (all marked with `iter16:` comments) and reverse-patch.
## Bottom line
iter16 = investigation closed. Path D (defer) chosen because Path B (architect's pick) didn't pan out and Path C (NIR pass) wasn't worth a multi-day investment given zero real-world impact on the iter9/iter13 ANGLE-on-Vulkan campaign target. Anyone resuming iter16 should start from `iter16/phase4_progress.md` and the listed hypotheses.
— claude-noether, 2026-05-21
+504
View File
@@ -0,0 +1,504 @@
/*
* iter16 winding-order regression probe for PanVk-Bifrost.
*
* Phase 3 of iter16. The 162 CTS dEQP-VK.transform_feedback.simple.winding_*
* failures (catalogued in iter15) all share the same root cause: iter13's
* pan_nir_lower_xfb captures one entry per VS invocation, which for non-LIST
* topologies gives ONE OUTPUT PER INPUT VERTEX. The Vulkan spec requires
* primitive-decomposed capture: an N-vertex triangle strip must produce
* 3*(N-2) captured entries with the right per-primitive winding order.
*
* This probe exercises the canonical case: triangle strip with 8 input
* vertices, expecting 18 captured entries arranged as 6 triangles. The
* verifier accepts any rotation within each primitive (per CTS's rule)
* but enforces the winding direction.
*
* Pre-iter16 behavior (current iter13/r3 driver): captured count = 8
* → PROBE FAILS (under-capture).
* Post-iter16 behavior: captured count = 18 in decomposed order
* → PROBE PASSES.
*
* Parameterized so we can add LINE_STRIP, TRIANGLE_FAN, *_ADJACENCY tests
* as the fix expands in Phase 4. For now, only TRIANGLE_STRIP is wired up.
*/
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <vulkan/vulkan.h>
#define VSPV_PATH "probe_winding.vert.spv"
#define STEP(name) do { fprintf(stderr, "[step] " name "\n"); fflush(stderr); } while (0)
#define VK_CHECK(call) do { \
VkResult _r = (call); \
if (_r != VK_SUCCESS) { \
fprintf(stderr, "[fail] " #call " => %d at %s:%d\n", \
(int)_r, __FILE__, __LINE__); \
exit(2); \
} \
} while (0)
/* ---- Per-topology expected-output helper (mirrors CTS) ---- */
/*
* For input vertex count N and topology T, returns the decomposed primitive
* count and per-primitive vertex layout. CTS test logic uses identical lambdas
* in vktTransformFeedbackSimpleTests.cpp around line 1241.
*/
struct topo_decomp {
VkPrimitiveTopology topology;
const char *name;
uint32_t verts_per_prim;
uint32_t (*prim_count)(uint32_t input_count);
/* Fills out[verts_per_prim] with the input-vertex-IDs that should appear
* in primitive prim_idx (in CTS winding order; rotations are accepted at
* verify time). */
void (*expected)(uint32_t prim_idx, uint32_t *out);
};
/* TRIANGLE_STRIP: 3*(N-2) outputs.
* Even prim i: {i, i+1, i+2}
* Odd prim i: {i, i+2, i+1}
*/
static uint32_t prim_count_tri_strip(uint32_t n) {
return (n >= 2) ? (n - 2) : 0;
}
static void expected_tri_strip(uint32_t i, uint32_t *out) {
uint32_t iMod2 = i & 1u;
out[0] = i;
out[1] = i + 1 + iMod2;
out[2] = i + 2 - iMod2;
}
/* LINE_STRIP: 2*(N-1) outputs. Each prim i: {i, i+1} */
static uint32_t prim_count_line_strip(uint32_t n) {
return (n >= 1) ? (n - 1) : 0;
}
static void expected_line_strip(uint32_t i, uint32_t *out) {
out[0] = i;
out[1] = i + 1u;
}
/* TRIANGLE_FAN: 3*(N-2) outputs. Each prim i: {i+1, i+2, 0} */
static uint32_t prim_count_tri_fan(uint32_t n) {
return (n >= 2) ? (n - 2) : 0;
}
static void expected_tri_fan(uint32_t i, uint32_t *out) {
out[0] = i + 1u;
out[1] = i + 2u;
out[2] = 0u;
}
static const struct topo_decomp TOPO_TESTS[] = {
{ VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, "TRIANGLE_STRIP", 3,
prim_count_tri_strip, expected_tri_strip },
{ VK_PRIMITIVE_TOPOLOGY_LINE_STRIP, "LINE_STRIP", 2,
prim_count_line_strip, expected_line_strip },
{ VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "TRIANGLE_FAN", 3,
prim_count_tri_fan, expected_tri_fan },
};
#define NUM_TOPO_TESTS (sizeof(TOPO_TESTS) / sizeof(TOPO_TESTS[0]))
/* ---- Vulkan plumbing ---- */
static uint32_t *read_spv(const char *path, size_t *out_bytes) {
FILE *f = fopen(path, "rb");
if (!f) { fprintf(stderr, "[fail] open %s: %s\n", path, strerror(errno)); exit(3); }
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
uint32_t *buf = malloc((size_t)n);
fread(buf, 1, (size_t)n, f);
fclose(f);
*out_bytes = (size_t)n;
return buf;
}
static uint32_t pick_host_visible(const VkPhysicalDeviceMemoryProperties *mp,
uint32_t type_bits) {
VkMemoryPropertyFlags want =
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
for (uint32_t i = 0; i < mp->memoryTypeCount; i++) {
if ((type_bits & (1u << i)) &&
(mp->memoryTypes[i].propertyFlags & want) == want) return i;
}
fprintf(stderr, "[fail] no HOST_VISIBLE+COHERENT memtype\n"); exit(4);
}
/* ---- Verifier (rotation-aware, mirrors CTS verifyVertexDataWithWinding) ---- */
/* Returns 1 if got[verts_per_prim] is a rotation of ref[verts_per_prim], 0 else. */
static int rotations_match(const uint32_t *ref, const uint32_t *got, uint32_t vpp) {
for (uint32_t start = 0; start < vpp; start++) {
int ok = 1;
for (uint32_t v = 0; v < vpp; v++) {
uint32_t r = ref[(start + v) % vpp];
if (r != got[v]) { ok = 0; break; }
}
if (ok) return 1;
}
return 0;
}
/* Returns number of mismatched primitives. Prints details for each mismatch. */
static int verify_winding(const struct topo_decomp *t, uint32_t input_count,
const uint32_t *got, uint32_t got_count) {
uint32_t expected_prims = t->prim_count(input_count);
uint32_t expected_count = expected_prims * t->verts_per_prim;
if (got_count != expected_count) {
fprintf(stderr, "[diff] %s: captured count %u, expected %u "
"(%u prims × %u verts)\n",
t->name, got_count, expected_count,
expected_prims, t->verts_per_prim);
return -1;
}
int mismatches = 0;
for (uint32_t p = 0; p < expected_prims; p++) {
uint32_t ref[8] = {0};
t->expected(p, ref);
const uint32_t *prim_got = got + p * t->verts_per_prim;
if (!rotations_match(ref, prim_got, t->verts_per_prim)) {
fprintf(stderr, "[diff] %s prim %u: expected rotation of {",
t->name, p);
for (uint32_t v = 0; v < t->verts_per_prim; v++)
fprintf(stderr, "%s%u", v ? "," : "", ref[v]);
fprintf(stderr, "} got {");
for (uint32_t v = 0; v < t->verts_per_prim; v++)
fprintf(stderr, "%s%u", v ? "," : "", prim_got[v]);
fprintf(stderr, "}\n");
mismatches++;
}
}
return mismatches;
}
/* ---- Per-topology test ---- */
static int run_one_topology(VkDevice dev, VkQueue queue, uint32_t qfam,
VkRenderPass dummy_rp,
PFN_vkCmdBindTransformFeedbackBuffersEXT pBindXfb,
PFN_vkCmdBeginTransformFeedbackEXT pBeginXfb,
PFN_vkCmdEndTransformFeedbackEXT pEndXfb,
PFN_vkCmdBeginRenderingKHR pBeginRendering,
PFN_vkCmdEndRenderingKHR pEndRendering,
VkPhysicalDeviceMemoryProperties *mp,
VkShaderModule vsm,
const struct topo_decomp *t,
uint32_t input_count) {
/* Capacity: expected_prims × verts_per_prim × 4. Pad to 64 entries
* (256 bytes) so iter13's under-capture is visible (sentinel-filled tail). */
const uint32_t buf_words = 64;
const VkDeviceSize buf_bytes = buf_words * sizeof(uint32_t);
fprintf(stderr, "\n=== %s with %u input verts ===\n", t->name, input_count);
/* XFB capture buffer */
VkBufferCreateInfo bci = {
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size = buf_bytes,
.usage = VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT |
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
};
VkBuffer xfb_buf;
VK_CHECK(vkCreateBuffer(dev, &bci, NULL, &xfb_buf));
VkMemoryRequirements mr;
vkGetBufferMemoryRequirements(dev, xfb_buf, &mr);
VkMemoryAllocateInfo mai = {
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.allocationSize = mr.size,
.memoryTypeIndex = pick_host_visible(mp, mr.memoryTypeBits),
};
VkDeviceMemory xfb_mem;
VK_CHECK(vkAllocateMemory(dev, &mai, NULL, &xfb_mem));
VK_CHECK(vkBindBufferMemory(dev, xfb_buf, xfb_mem, 0));
void *mapped;
VK_CHECK(vkMapMemory(dev, xfb_mem, 0, VK_WHOLE_SIZE, 0, &mapped));
/* Sentinel-fill so we can distinguish "captured 0xDEADBEEF" from
* "GPU didn't write here" — under-capture leaves the tail at sentinel. */
uint32_t *u32 = (uint32_t *)mapped;
for (uint32_t i = 0; i < buf_words; i++) u32[i] = 0xDEADBEEFu;
/* Pipeline */
VkPipelineLayoutCreateInfo plci = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
};
VkPipelineLayout pl;
VK_CHECK(vkCreatePipelineLayout(dev, &plci, NULL, &pl));
VkPipelineShaderStageCreateInfo stages[1] = {
{ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_VERTEX_BIT, .module = vsm, .pName = "main" },
};
VkPipelineVertexInputStateCreateInfo vi = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
};
VkPipelineInputAssemblyStateCreateInfo ia = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
.topology = t->topology,
};
VkViewport vp_dummy = { 0, 0, 1, 1, 0.0f, 1.0f };
VkRect2D sc_dummy = {{0,0}, {1,1}};
VkPipelineViewportStateCreateInfo vp = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
.viewportCount = 1, .pViewports = &vp_dummy,
.scissorCount = 1, .pScissors = &sc_dummy,
};
VkPipelineRasterizationStateCreateInfo rs = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
.rasterizerDiscardEnable = VK_TRUE,
.polygonMode = VK_POLYGON_MODE_FILL,
.cullMode = VK_CULL_MODE_NONE,
.lineWidth = 1.0f,
};
VkPipelineMultisampleStateCreateInfo ms = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT,
};
VkPipelineRenderingCreateInfoKHR pri = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR,
.colorAttachmentCount = 0,
};
VkGraphicsPipelineCreateInfo gpci = {
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
.pNext = &pri,
.stageCount = 1, .pStages = stages,
.pVertexInputState = &vi,
.pInputAssemblyState = &ia,
.pViewportState = &vp,
.pRasterizationState = &rs,
.pMultisampleState = &ms,
.layout = pl,
};
VkPipeline pipe;
VK_CHECK(vkCreateGraphicsPipelines(dev, VK_NULL_HANDLE, 1, &gpci, NULL, &pipe));
/* Command buffer */
VkCommandPoolCreateInfo cpoolci = {
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
.queueFamilyIndex = qfam,
};
VkCommandPool cpool;
VK_CHECK(vkCreateCommandPool(dev, &cpoolci, NULL, &cpool));
VkCommandBufferAllocateInfo cbai = {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.commandPool = cpool, .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = 1,
};
VkCommandBuffer cb;
VK_CHECK(vkAllocateCommandBuffers(dev, &cbai, &cb));
VkCommandBufferBeginInfo cbbi = {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
};
VK_CHECK(vkBeginCommandBuffer(cb, &cbbi));
VkDeviceSize xfb_off = 0, xfb_size = buf_bytes;
pBindXfb(cb, 0, 1, &xfb_buf, &xfb_off, &xfb_size);
VkRenderingInfoKHR ri = {
.sType = VK_STRUCTURE_TYPE_RENDERING_INFO_KHR,
.renderArea = {{0,0}, {1,1}},
.layerCount = 1,
.colorAttachmentCount = 0,
};
pBeginRendering(cb, &ri);
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe);
pBeginXfb(cb, 0, 0, NULL, NULL);
vkCmdDraw(cb, input_count, 1, 0, 0);
pEndXfb(cb, 0, 0, NULL, NULL);
pEndRendering(cb);
VkBufferMemoryBarrier bb = {
.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_TRANSFORM_FEEDBACK_WRITE_BIT_EXT,
.dstAccessMask = VK_ACCESS_HOST_READ_BIT,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.buffer = xfb_buf, .offset = 0, .size = VK_WHOLE_SIZE,
};
vkCmdPipelineBarrier(cb,
VK_PIPELINE_STAGE_TRANSFORM_FEEDBACK_BIT_EXT,
VK_PIPELINE_STAGE_HOST_BIT,
0, 0, NULL, 1, &bb, 0, NULL);
VK_CHECK(vkEndCommandBuffer(cb));
/* Submit + wait */
VkFenceCreateInfo fci = { .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
VkFence fence;
VK_CHECK(vkCreateFence(dev, &fci, NULL, &fence));
VkSubmitInfo si = {
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.commandBufferCount = 1, .pCommandBuffers = &cb,
};
VK_CHECK(vkQueueSubmit(queue, 1, &si, fence));
VkResult wr = vkWaitForFences(dev, 1, &fence, VK_TRUE, 10ULL * 1000 * 1000 * 1000);
if (wr != VK_SUCCESS) {
fprintf(stderr, "[fail] %s: vkWaitForFences => %d\n", t->name, wr);
return -1;
}
/* Read back: count contiguous non-sentinel words from offset 0. */
uint32_t captured_count = 0;
while (captured_count < buf_words && u32[captured_count] != 0xDEADBEEFu)
captured_count++;
fprintf(stderr, "[info] %s: captured %u entries (sentinel-stopped)\n",
t->name, captured_count);
/* Print first few for debugging */
if (captured_count > 0) {
fprintf(stderr, "[info] first 8: ");
for (uint32_t i = 0; i < captured_count && i < 8; i++)
fprintf(stderr, "%u%s", u32[i], (i + 1 < 8 && i + 1 < captured_count) ? "," : "");
fprintf(stderr, "\n");
}
int mismatches = verify_winding(t, input_count, u32, captured_count);
/* Teardown */
vkUnmapMemory(dev, xfb_mem);
vkDestroyFence(dev, fence, NULL);
vkDestroyCommandPool(dev, cpool, NULL);
vkDestroyPipeline(dev, pipe, NULL);
vkDestroyPipelineLayout(dev, pl, NULL);
vkDestroyBuffer(dev, xfb_buf, NULL);
vkFreeMemory(dev, xfb_mem, NULL);
(void)dummy_rp;
return mismatches;
}
/* ---- main: bring up Vulkan, run all topology tests ---- */
int main(int argc, char **argv) {
/* Optional CLI: limit to one topology by name */
const char *only = NULL;
if (argc > 1) only = argv[1];
STEP("vkCreateInstance");
VkApplicationInfo app = {
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.pApplicationName = "panvk-bifrost iter16 winding probe",
.apiVersion = VK_API_VERSION_1_0,
};
const char *inst_exts[] = { "VK_KHR_get_physical_device_properties2" };
VkInstanceCreateInfo ici = {
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pApplicationInfo = &app,
.enabledExtensionCount = 1,
.ppEnabledExtensionNames = inst_exts,
};
VkInstance inst;
VK_CHECK(vkCreateInstance(&ici, NULL, &inst));
uint32_t n_phys = 0;
VK_CHECK(vkEnumeratePhysicalDevices(inst, &n_phys, NULL));
VkPhysicalDevice *phys = calloc(n_phys, sizeof(*phys));
VK_CHECK(vkEnumeratePhysicalDevices(inst, &n_phys, phys));
VkPhysicalDevice gpu = phys[0];
VkPhysicalDeviceMemoryProperties mp;
vkGetPhysicalDeviceMemoryProperties(gpu, &mp);
uint32_t n_qf = 0;
vkGetPhysicalDeviceQueueFamilyProperties(gpu, &n_qf, NULL);
VkQueueFamilyProperties *qfp = calloc(n_qf, sizeof(*qfp));
vkGetPhysicalDeviceQueueFamilyProperties(gpu, &n_qf, qfp);
uint32_t qfam = UINT32_MAX;
for (uint32_t i = 0; i < n_qf; i++)
if (qfp[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { qfam = i; break; }
STEP("vkCreateDevice");
const char *dev_exts[] = {
"VK_KHR_multiview", "VK_KHR_maintenance2",
"VK_KHR_create_renderpass2", "VK_KHR_depth_stencil_resolve",
"VK_KHR_dynamic_rendering",
"VK_EXT_transform_feedback",
};
VkPhysicalDeviceTransformFeedbackFeaturesEXT enable_xfb = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_FEATURES_EXT,
.transformFeedback = VK_TRUE,
.geometryStreams = VK_FALSE,
};
VkPhysicalDeviceDynamicRenderingFeaturesKHR dyn_feat = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR,
.pNext = &enable_xfb,
.dynamicRendering = VK_TRUE,
};
float qprio = 1.0f;
VkDeviceQueueCreateInfo qci = {
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = qfam, .queueCount = 1, .pQueuePriorities = &qprio,
};
VkDeviceCreateInfo dci = {
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
.pNext = &dyn_feat,
.queueCreateInfoCount = 1, .pQueueCreateInfos = &qci,
.enabledExtensionCount = sizeof(dev_exts)/sizeof(dev_exts[0]),
.ppEnabledExtensionNames = dev_exts,
};
VkDevice dev;
VK_CHECK(vkCreateDevice(gpu, &dci, NULL, &dev));
VkQueue queue;
vkGetDeviceQueue(dev, qfam, 0, &queue);
PFN_vkCmdBindTransformFeedbackBuffersEXT pBindXfb =
(PFN_vkCmdBindTransformFeedbackBuffersEXT)vkGetDeviceProcAddr(
dev, "vkCmdBindTransformFeedbackBuffersEXT");
PFN_vkCmdBeginTransformFeedbackEXT pBeginXfb =
(PFN_vkCmdBeginTransformFeedbackEXT)vkGetDeviceProcAddr(
dev, "vkCmdBeginTransformFeedbackEXT");
PFN_vkCmdEndTransformFeedbackEXT pEndXfb =
(PFN_vkCmdEndTransformFeedbackEXT)vkGetDeviceProcAddr(
dev, "vkCmdEndTransformFeedbackEXT");
PFN_vkCmdBeginRenderingKHR pBeginRendering =
(PFN_vkCmdBeginRenderingKHR)vkGetDeviceProcAddr(dev, "vkCmdBeginRenderingKHR");
PFN_vkCmdEndRenderingKHR pEndRendering =
(PFN_vkCmdEndRenderingKHR)vkGetDeviceProcAddr(dev, "vkCmdEndRenderingKHR");
/* Shader (shared across topology iterations) */
size_t spv_bytes = 0;
uint32_t *spv = read_spv(VSPV_PATH, &spv_bytes);
VkShaderModuleCreateInfo smci = {
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = spv_bytes, .pCode = spv,
};
VkShaderModule vsm;
VK_CHECK(vkCreateShaderModule(dev, &smci, NULL, &vsm));
free(spv);
/* Run each topology test */
int total_fail = 0;
int total_tested = 0;
for (size_t i = 0; i < NUM_TOPO_TESTS; i++) {
const struct topo_decomp *t = &TOPO_TESTS[i];
if (only && strcmp(only, t->name) != 0) continue;
total_tested++;
int rc = run_one_topology(dev, queue, qfam, VK_NULL_HANDLE,
pBindXfb, pBeginXfb, pEndXfb,
pBeginRendering, pEndRendering,
&mp, vsm, t, 8u);
if (rc != 0) {
total_fail++;
fprintf(stderr, "[FAIL] %s: %d mismatch(es)\n", t->name, rc);
} else {
fprintf(stderr, "[PASS] %s\n", t->name);
}
}
vkDestroyShaderModule(dev, vsm, NULL);
vkDestroyDevice(dev, NULL);
vkDestroyInstance(inst, NULL);
free(phys); free(qfp);
fprintf(stderr, "\n=== SUMMARY: %d/%d topology tests passed ===\n",
total_tested - total_fail, total_tested);
return total_fail == 0 ? 0 : 1;
}
@@ -0,0 +1,16 @@
#version 450
// iter16 winding probe vertex shader.
// Captures gl_VertexIndex as a single uint32 per VS invocation.
// With non-LIST topologies + XFB, the spec requires the captured buffer
// to be primitive-decomposed — i.e., MORE outputs than input vertices.
// iter13 fails this: it captures one entry per VS invocation (= one per
// input vertex). iter16 must inject driver-side decomposition so the
// captured stream matches the decomposed primitive sequence.
layout(xfb_buffer = 0, xfb_offset = 0, xfb_stride = 4, location = 0) out uint captured;
void main() {
gl_Position = vec4(0, 0, 0, 1);
captured = uint(gl_VertexIndex);
}