a73x

docs/superpowers/plans/2026-04-09-text-coverage-comparison-implementation.md

Ref:   Size: 13.0 KiB

# Text Coverage Comparison Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a `--text-compare` mode that renders one specimen across four side-by-side coverage variants so text sharpness can be evaluated visually without changing the default terminal path.

**Architecture:** Reuse the existing Vulkan/font/atlas smoke-test path in `src/main.zig`, extend the renderer with a small fragment-side coverage control, and render four panels using the same atlas and instance format. Keep the default terminal rendering path unchanged; the comparison mode is an isolated inspection tool.

**Tech Stack:** Zig 0.15, Vulkan renderer in `src/renderer.zig`, GLSL fragment shader in `shaders/cell.frag`, Wayland window path in `src/main.zig`, configured font lookup in `src/font.zig`

---

## File Structure

- Modify: `src/main.zig`
  - Add the `--text-compare` CLI path, specimen layout helpers, and comparison-mode render loop.
- Modify: `src/renderer.zig`
  - Add a small configurable coverage parameter path that the fragment shader can read.
- Modify: `shaders/cell.frag`
  - Apply variant coverage shaping while preserving the current baseline behavior.
- Test: `src/main.zig`
  - Add tests for specimen layout and panel placement helpers.
- Test: `src/renderer.zig`
  - Add tests for the coverage-parameter packing/planning helpers that do not require a live Vulkan device.

### Task 1: Add coverage-variant planning helpers in the renderer

**Files:**
- Modify: `src/renderer.zig`
- Test: `src/renderer.zig`

- [ ] **Step 1: Write the failing tests**

Add tests for a small coverage parameter helper:

```zig
test "coverageVariantParams returns identity values for baseline" {
    const params = coverageVariantParams(.baseline);
    try std.testing.expectEqualDeep([2]f32{ 1.0, 0.0 }, params);
}

test "coverageVariantParams steepens non-baseline variants" {
    const mild = coverageVariantParams(.mild);
    const medium = coverageVariantParams(.medium);
    const crisp = coverageVariantParams(.crisp);

    try std.testing.expect(mild[0] > 1.0);
    try std.testing.expect(medium[0] > mild[0]);
    try std.testing.expect(crisp[0] > medium[0]);
}
```

- [ ] **Step 2: Run test to verify it fails**

Run: `rm -rf /tmp/zig-global-cache-coverage-plan && cp -a /home/xanderle/.cache/zig /tmp/zig-global-cache-coverage-plan && ZIG_GLOBAL_CACHE_DIR=/tmp/zig-global-cache-coverage-plan zig build test --summary all`
Expected:
- FAIL because `coverageVariantParams` and the enum are undefined.

- [ ] **Step 3: Add the minimal helper types and implementation**

Add a small enum and helper in `src/renderer.zig`:

```zig
pub const CoverageVariant = enum(u32) {
    baseline,
    mild,
    medium,
    crisp,
};

fn coverageVariantParams(variant: CoverageVariant) [2]f32 {
    return switch (variant) {
        .baseline => .{ 1.0, 0.0 },
        .mild => .{ 1.15, 0.0 },
        .medium => .{ 1.3, 0.0 },
        .crisp => .{ 1.55, -0.08 },
    };
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `rm -rf /tmp/zig-global-cache-coverage-plan && cp -a /home/xanderle/.cache/zig /tmp/zig-global-cache-coverage-plan && ZIG_GLOBAL_CACHE_DIR=/tmp/zig-global-cache-coverage-plan zig build test --summary all`
Expected:
- PASS for the new renderer helper tests.

- [ ] **Step 5: Commit**

```bash
git add src/renderer.zig
git commit -m "Add text coverage variant helpers"
```

### Task 2: Plumb coverage controls through push constants and fragment shader

**Files:**
- Modify: `src/renderer.zig`
- Modify: `shaders/cell.frag`
- Test: `src/renderer.zig`

- [ ] **Step 1: Write the failing test**

Add a test that checks the push-constant default remains baseline-safe:

```zig
test "PushConstants defaults preserve baseline coverage shaping" {
    const pc = PushConstants{
        .surface_size = .{ 800.0, 600.0 },
        .cell_size = .{ 8.0, 16.0 },
        .clear_color = .{ 0.0, 0.0, 0.0, 1.0 },
        .coverage_params = .{ 1.0, 0.0 },
    };

    try std.testing.expectEqualDeep([2]f32{ 1.0, 0.0 }, pc.coverage_params);
}
```

- [ ] **Step 2: Run test to verify it fails**

Run: `rm -rf /tmp/zig-global-cache-push-plan && cp -a /home/xanderle/.cache/zig /tmp/zig-global-cache-push-plan && ZIG_GLOBAL_CACHE_DIR=/tmp/zig-global-cache-push-plan zig build test --summary all`
Expected:
- FAIL because `PushConstants` does not yet contain `coverage_params`.

- [ ] **Step 3: Add push-constant and shader plumbing**

Extend `PushConstants` in `src/renderer.zig`:

```zig
pub const PushConstants = extern struct {
    surface_size: [2]f32,
    cell_size: [2]f32,
    clear_color: [4]f32,
    coverage_params: [2]f32,
};
```

Update `drawCells` to accept coverage params:

```zig
pub fn drawCells(
    self: *Context,
    instance_count: u32,
    cell_size: [2]f32,
    clear_color: [4]f32,
    coverage_params: [2]f32,
) !void
```

Set the push constants with that field populated.

Update `shaders/cell.frag`:

```glsl
layout(push_constant) uniform PushConstants {
    vec2 surface_size;
    vec2 cell_size;
    vec4 clear_color;
    vec2 coverage_params;
} pc;

float shape_coverage(float alpha) {
    float curved = pow(alpha, pc.coverage_params.x);
    return clamp(curved + pc.coverage_params.y, 0.0, 1.0);
}

void main() {
    float alpha = texture(glyph_atlas, in_uv).r;
    alpha = shape_coverage(alpha);
    out_color = mix(in_bg, in_fg, alpha);
}
```

Keep the baseline identity behavior by passing `{1.0, 0.0}` for normal paths.

- [ ] **Step 4: Run test to verify it passes**

Run: `rm -rf /tmp/zig-global-cache-push-plan && cp -a /home/xanderle/.cache/zig /tmp/zig-global-cache-push-plan && ZIG_GLOBAL_CACHE_DIR=/tmp/zig-global-cache-push-plan zig build test --summary all`
Expected:
- PASS with shader compilation and the new push-constant test green.

- [ ] **Step 5: Commit**

```bash
git add src/renderer.zig shaders/cell.frag
git commit -m "Plumb text coverage controls through renderer"
```

### Task 3: Keep the default draw path on baseline coverage

**Files:**
- Modify: `src/main.zig`

- [ ] **Step 1: Add the failing compile-path update**

Update existing `drawCells` call sites to pass baseline coverage:

```zig
ctx.drawCells(instance_count, cell_size, clear_color, .{ 1.0, 0.0 })
```

- [ ] **Step 2: Run test to verify it fails**

Run: `rm -rf /tmp/zig-global-cache-baseline-plan && cp -a /home/xanderle/.cache/zig /tmp/zig-global-cache-baseline-plan && ZIG_GLOBAL_CACHE_DIR=/tmp/zig-global-cache-baseline-plan zig build test --summary all`
Expected:
- FAIL until all `drawCells` call sites are updated.

- [ ] **Step 3: Update existing draw paths**

Update all current `ctx.drawCells(...)` calls in `src/main.zig` to use baseline params:

```zig
const baseline_coverage = .{ 1.0, 0.0 };
```

Pass `baseline_coverage` in:
- the normal terminal loop,
- `runDrawSmokeTest`,
- any other smoke/helper render loop.

- [ ] **Step 4: Run test to verify it passes**

Run: `rm -rf /tmp/zig-global-cache-baseline-plan && cp -a /home/xanderle/.cache/zig /tmp/zig-global-cache-baseline-plan && ZIG_GLOBAL_CACHE_DIR=/tmp/zig-global-cache-baseline-plan zig build test --summary all`
Expected:
- PASS with the default rendering path behavior unchanged.

- [ ] **Step 5: Commit**

```bash
git add src/main.zig
git commit -m "Keep default rendering on baseline coverage"
```

### Task 4: Add specimen layout helpers for comparison mode

**Files:**
- Modify: `src/main.zig`
- Test: `src/main.zig`

- [ ] **Step 1: Write the failing tests**

Add tests for panel offsets and specimen instance generation planning:

```zig
test "comparisonPanelOrigins splits four panels left to right" {
    const origins = comparisonPanelOrigins(4, 80, 24);
    try std.testing.expectEqual(@as(f32, 0), origins[0][0]);
    try std.testing.expect(origins[1][0] > origins[0][0]);
    try std.testing.expect(origins[2][0] > origins[1][0]);
    try std.testing.expect(origins[3][0] > origins[2][0]);
}

test "specimenLines remains fixed and non-empty" {
    const lines = comparisonSpecimenLines();
    try std.testing.expect(lines.len >= 5);
    try std.testing.expect(lines[0].len > 0);
}
```

- [ ] **Step 2: Run test to verify it fails**

Run: `rm -rf /tmp/zig-global-cache-layout-plan && cp -a /home/xanderle/.cache/zig /tmp/zig-global-cache-layout-plan && ZIG_GLOBAL_CACHE_DIR=/tmp/zig-global-cache-layout-plan zig build test --summary all`
Expected:
- FAIL because the comparison helpers are undefined.

- [ ] **Step 3: Add the layout helpers**

Add focused helpers in `src/main.zig`:

```zig
const ComparisonVariant = struct {
    label: []const u8,
    coverage: [2]f32,
};

fn comparisonVariants() [4]ComparisonVariant {
    return .{
        .{ .label = "baseline", .coverage = renderer.coverageVariantParams(.baseline) },
        .{ .label = "mild", .coverage = renderer.coverageVariantParams(.mild) },
        .{ .label = "medium", .coverage = renderer.coverageVariantParams(.medium) },
        .{ .label = "crisp", .coverage = renderer.coverageVariantParams(.crisp) },
    };
}

fn comparisonSpecimenLines() []const []const u8 { ... }
fn comparisonPanelOrigins(panel_count: usize, panel_cols: u32, top_margin_rows: u32) [4][2]f32 { ... }
```

Keep the specimen text fixed to the approved five lines.

- [ ] **Step 4: Run test to verify it passes**

Run: `rm -rf /tmp/zig-global-cache-layout-plan && cp -a /home/xanderle/.cache/zig /tmp/zig-global-cache-layout-plan && ZIG_GLOBAL_CACHE_DIR=/tmp/zig-global-cache-layout-plan zig build test --summary all`
Expected:
- PASS for the new comparison-layout tests.

- [ ] **Step 5: Commit**

```bash
git add src/main.zig
git commit -m "Add text comparison layout helpers"
```

### Task 5: Implement the `--text-compare` render mode

**Files:**
- Modify: `src/main.zig`

- [ ] **Step 1: Add the failing compile-path update**

Add a CLI branch:

```zig
if (args.len >= 2 and std.mem.eql(u8, args[1], "--text-compare")) {
    return runTextCoverageCompare(alloc);
}
```

- [ ] **Step 2: Run test to verify it fails**

Run: `rm -rf /tmp/zig-global-cache-compare-plan && cp -a /home/xanderle/.cache/zig /tmp/zig-global-cache-compare-plan && ZIG_GLOBAL_CACHE_DIR=/tmp/zig-global-cache-compare-plan zig build test --summary all`
Expected:
- FAIL because `runTextCoverageCompare` is undefined.

- [ ] **Step 3: Implement the comparison mode**

Add `runTextCoverageCompare(alloc)` in `src/main.zig` by reusing the draw-smoke structure:

```zig
fn runTextCoverageCompare(alloc: std.mem.Allocator) !void {
    // 1. Create Wayland connection, window, and Vulkan context.
    // 2. Load configured font and atlas.
    // 3. Build one instance list for the fixed specimen in each panel.
    // 4. Upload atlas and packed instances once.
    // 5. Render in a loop, selecting one panel coverage variant per draw.
}
```

Implementation notes:
- Use four separate draws per frame, one per panel.
- Re-upload only once; reuse the same instance buffer data.
- Keep the specimen identical in each panel and only vary `coverage_params`.
- Reserve enough horizontal spacing so panel text does not overlap.

- [ ] **Step 4: Run test to verify it passes**

Run: `rm -rf /tmp/zig-global-cache-compare-plan && cp -a /home/xanderle/.cache/zig /tmp/zig-global-cache-compare-plan && ZIG_GLOBAL_CACHE_DIR=/tmp/zig-global-cache-compare-plan zig build test --summary all`
Expected:
- PASS

- [ ] **Step 5: Commit**

```bash
git add src/main.zig
git commit -m "Add text coverage comparison mode"
```

### Task 6: Full verification

**Files:**
- Modify: none
- Test: `src/main.zig`, `src/renderer.zig`, `shaders/cell.frag`

- [ ] **Step 1: Run the full test suite**

Run: `rm -rf /tmp/zig-global-cache-compare-final && cp -a /home/xanderle/.cache/zig /tmp/zig-global-cache-compare-final && ZIG_GLOBAL_CACHE_DIR=/tmp/zig-global-cache-compare-final zig build test --summary all`
Expected:
- PASS

- [ ] **Step 2: Run a build verification**

Run: `rm -rf /tmp/zig-global-cache-compare-build && cp -a /home/xanderle/.cache/zig /tmp/zig-global-cache-compare-build && ZIG_GLOBAL_CACHE_DIR=/tmp/zig-global-cache-compare-build zig build`
Expected:
- PASS

- [ ] **Step 3: Run the comparison mode manually**

Run: `zig build run -- --text-compare`
Expected:
- One window opens.
- Four panels are visible.
- The specimen text matches across all panels.
- Baseline, mild, medium, and crisp variants are visually distinguishable.

- [ ] **Step 4: Commit**

```bash
git add src/main.zig src/renderer.zig shaders/cell.frag
git commit -m "Verify text coverage comparison mode"
```

## Self-Review

- Spec coverage:
  - New CLI comparison mode: Task 5
  - Reuse existing Vulkan/font/atlas path: Task 5
  - Four side-by-side panels with baseline + three variants: Tasks 4 and 5
  - Configured font family and size reuse: Tasks 3 and 5
  - Shader-only first pass: Tasks 1 and 2
  - Validation with build and manual comparison: Task 6
- Placeholder scan:
  - No `TODO`, `TBD`, or deferred “figure this out later” markers remain.
- Type consistency:
  - `CoverageVariant`, `coverageVariantParams`, `coverage_params`, `comparisonVariants`, and `runTextCoverageCompare` are named consistently across tasks.