# Phase A completion — A4, A5, A6

Branch `feat/gpu-a4-a6`. Finish the GPU-interface spine. Build + `test-full` gate each step.

## A4 — font atlas → `gpu.Texture`; drop `font/manager.zig` glad cImport
`font/manager.zig` uploads 4 glyph atlases (grayscale RED + color RGBA) via raw GL:
`syncAtlasTexture` does GenTextures + TexImage2D + TexParameteri(CLAMP/LINEAR) on create,
`GetTexLevelParameteriv` to read current size, then realloc (TexImage2D) or `TexSubImage2D`;
plus `PixelStorei(UNPACK_ALIGNMENT,1)` and `DeleteTextures` in teardown. Own `@cInclude("glad/gl.h")`.

Add to `gpu.Texture`: `subImage2D(self, x, y, w, h, data, o)` (glTexSubImage2D) and
`levelWidth(self) c_int` (glGetTexLevelParameteriv GL_TEXTURE_WIDTH). Then convert `manager.zig`:
- `const c = @cImport({ @cInclude("glad/gl.h"); })` → `const c = gpu.c;` (drops the only own glad include).
- atlas handles stay `c.GLuint`; create/upload via `Texture.create()` + `upload2D(.{ .internal_format=..., .format=..., .filter=.linear, .wrap=.clamp_to_edge, .unpack_alignment=1 })` (RED vs RGBA per atlas); size check via `Texture.fromHandle(h).levelWidth()`; same-size update via `subImage2D`; teardown via `Texture.fromHandle(h).destroy()`.
- the standalone `PixelStorei` (~1201) folds into upload2D/subImage2D's `unpack_alignment`.
Target: `font/manager.zig` has NO own glad cInclude (uses `gpu.c`); atlas goes through `gpu.Texture`.

## A6-prep — remove remaining own glad cIncludes (so the guard can be strong)
Swap `@cInclude("glad/gl.h")` → `const c = gpu.c;` in the 5 other files that carry their own
(they already call `gpu.glTable()`; only the GL constants/types come from `c`):
`cell_renderer.zig`, `Renderer.zig`, `weixin_qr_renderer.zig`, `overlays/resize.zig`,
`overlays/startup_shortcuts.zig`. Behavior-identical (gpu.c is the same glad constants).
After this + A4: `@cInclude("glad/gl.h")` exists ONLY under `src/renderer/gpu/opengl/`.

## A6 — the guard (`src/renderer/gpu/gl_backend_guard.zig`, unit-tested; comptime in build.zig)
Mirror `build_guards.zig`'s `firstLeak(source)` style. Rules:
1. **glad include containment (strong):** any file whose path is NOT under
   `src/renderer/gpu/opengl/` must not contain `@cInclude("glad/gl.h")`. This is the headline
   "raw GPU API only in the backend" invariant. Passes after A4 + A6-prep.
2. **AppWindow no glad:** `AppWindow.zig` must not `@cImport`/`@cInclude` glad (holds since A2).
3. **glTable regression-lock (scoped):** the set of files outside `gpu/opengl/` that call
   `gpu.glTable()` is frozen to a documented allowlist (the GL abstraction layer
   `ui_pipeline`/`cell_pipeline`, render coordination `Renderer`/`cell_renderer`/`post_process`,
   the overlay sub-modules still using it, `weixin_qr_renderer`, `font/manager` if any remains,
   `AppWindow`, and `gpu.zig` itself which defines glTable). A NEW file calling `glTable()` fails
   the guard — locks the cleaned A3 feature files against regression and blocks new leaks. The
   allowlist is the shrinking residue to be extracted as the primitive set grows (future).
Wire: `build.zig` embeds the relevant sources + runs the guard in a `comptime` block;
`test_main.zig` imports the guard module so the logic is unit-tested.

## A5 — reserve the Metal MSL shader slot
Create `src/renderer/gpu/metal/shaders.zig` as the MSL counterpart slot to
`gpu/opengl/shaders.zig` — a documented placeholder (the shader sources the future Metal backend
will fill), so the backend dir structure mirrors `gpu/opengl/`. No behavior change (metal backend
not built). Reference it from a comment in `backend.zig` if useful.

## Order
1. A4 (Texture.subImage2D/levelWidth + font/manager conversion).
2. A6-prep (5 cInclude swaps).
3. A6 (guard + wire-in) — now rule 1 passes.
4. A5 (MSL slot).
5. Build + `test-full` + PR.
