# Plan — Fullscreen Texture Blit with Contain-Fit

## Goal

Render a texture fullscreen with aspect-ratio preservation. If the texture's
aspect ratio does not match the window's, draw it to fit within the window —
never stretched, squashed, or cropped. Tall/narrow textures pillarbox (bars on
left/right); wide textures letterbox (bars on top/bottom).

This replaces the Suzanne mesh demo in `main.cpp`, which was for testing the RHI.

## Fit semantics (contain — never crop)

```
scale       = min(win_w / tex_w, win_h / tex_h)
content_w   = tex_w * scale
content_h   = tex_h * scale
rect        = centered: left = (win_w - content_w)/2, top = (win_h - content_h)/2
```

| Texture vs window aspect | Limiting dim | Result            |
|--------------------------|--------------|-------------------|
| equal                    | —            | fills exactly     |
| wider (e.g. 2048x512)    | width        | letterbox (T/B)   |
| taller (e.g. 512x2048)   | height       | pillarbox (L/R)   |

The fit rect is recomputed per frame from the selected texture's dimensions and
the current window size, so window resize works without extra handling.

## Changes

### 1. RHI — texture size accessor

Add a value struct and a by-value getter (matches `prRhiGetSurfaceCapabilities`
pattern):

```c
typedef struct PrRhiTextureSize {
	u32 width;
	u32 height;
} PrRhiTextureSize;

PrRhiTextureSize prRhiGetTextureSize(PrRhiTexture *texture);
```

Files: `pr_rhi_types.h`, `pr_rhi.h`, `vulkan/pr_rhi_vk.h`,
`vulkan/pr_rhi_vk.c`, `vulkan/pr_rhi_vk_aliases.h`.

### 2. Offline shader compilation (drop Slang runtime)

- New `justfile` `shaders` recipe:
  `slangc -target spirv -profile spirv_1_4 -o build/shaders/blit.spv assets/blit.slang`
  (entry points auto-detected from `[shader(...)]` attributes).
  `build` depends on it.
- `main.cpp` loads `build/shaders/blit.spv` via wapp file I/O
  (`wpFileOpen` / `wpFileGetLength` / `wpFileRead`) into an arena buffer, then
  `prRhiCreateShader`. No `slang.h` includes, no runtime compilation.
- Drop `-lslang` and the `-I .../slang` include from the build.

### 3. `assets/blit.slang`

Push constant block (32 bytes — Slang pads structs to 16-byte alignment, so the
C++ struct carries explicit `pad[3]` to match):

```hlsl
struct BlitData {
	float4 rect;   // NDC fit rect: x0, y0, x1, y1
	uint   selected;
	uint   pad[3];
};
```

- **VS**: generates a 4-vertex triangle-strip quad from `SV_VertexID` (no vertex
  buffer), maps UV 0–1 into the NDC rect.
- **FS**: `textures[NonUniformResourceIndex(selected)].Sample(uv)` — reuses the
  existing bindless descriptor array.

### 4. `main.cpp` — clean texture viewer

Remove everything mesh-related: tinyobj loading, vertex/index buffers,
`ShaderData` storage buffers / device addresses, the mesh shader + pipeline,
mouse orbit, and the mesh draw. Drop `-ltinyobjloader -lglm` from the link.
Remove unused `assets/shader.slang`, `suzanne.obj`, `suzanne.mtl`.

New flow: RHI init → window/instance/pdev/surface/device/swapchain → load 7
textures → bindless descriptor set (variable count 7) → load `blit.spv` →
blit pipeline layout (`VERTEX|FRAGMENT` 32-byte push range) → blit pipeline
(no vertex input, `TRIANGLE_STRIP`, swapchain color format, no depth,
`cull NONE`, dynamic viewport/scissor).

Render loop per frame:
- `compute_fit_rect()` from the selected texture's dims + window size
- bind blit pipeline + descriptor set, push `BlitData{ rect, selected }`,
  `prRhiCmdDraw(cb, 4, 1, 0, 0)`
- `+/-` cycles the selected texture; resize recomputes fit automatically

### 5. Test textures (PIL + `build/bin/toktx`)

Four generated KTX files in `assets/`, loaded alongside the 3 Suzanne textures
(`texture_count = 7`, explicit path array):

| File            | Size      | Shows                    |
|-----------------|-----------|--------------------------|
| `test_square.ktx` | 1024x1024 | bars on both axes        |
| `test_fill.ktx`   | 1920x1080 | fills the 16:9 window    |
| `test_wide.ktx`   | 2048x512  | letterbox (T/B)          |
| `test_tall.ktx`   | 512x2048  | pillarbox (L/R)          |

Each with distinct gradients + a border grid so any stretch/squash is visible.
Generated with `--genmipmap` for mip-aware sampling.

## Verification

`just build && just run` — each texture fits without crop/stretch, `+/-` cycles,
window resize keeps fit, no Slang runtime in the binary.

## Verification results (2026-08-08)

Fit behavior confirmed by capturing the window (X11 driver) and sampling pixels:

| Texture             | Window        | Result verified                                |
|---------------------|---------------|------------------------------------------------|
| square 1024x1024    | 16:9 wide     | pillarbox — pure-black L/R bars, full height   |
| test_wide 2048x512  | 16:9 wide     | letterbox — pure-black T/B bars, full width    |
| test_tall 512x2048  | 16:9 wide     | pillarbox — pure-black L/R bars, full height   |
| test_fill 1920x1080 | 16:9 wide     | fills exactly — no bars anywhere               |
| square (resized)    | portrait      | fit recomputed per frame — flips to letterbox  |

Texture cycling (`+/-`, `SDLK_PLUS/KP_PLUS/EQUALS`, `SDLK_MINUS/KP_MINUS`) is a
straightforward `selected` bump in the key handler; it was reviewed but not
exercise-tested in the headless verification env (KWin/XWayland drops XTEST
synthesised keys). The other fit cases were verified by temporarily launching
each texture as the initial selection.

### Bugs found and fixed during verification

1. **`VK_SUBOPTIMAL_KHR` aborted the app.** `prRhiAcquireNextImageVk` /
   `prRhiPresentVk` routed `SUBOPTIMAL` into `_checkVk` → `__builtin_trap()`.
   Both now treat it like `OUT_OF_DATE` (return `PR_RHI_SWAPCHAIN_OUT_OF_DATE`).
   Triggered immediately under X11/XWayland.
2. **Swapchain recreate ignored surface extent.** `prRhiRecreateSwapchainVk`
   hard-coded the passed width/height; on surfaces whose `currentExtent` is
   meaningful (X11) that mismatched the drawable and re-looped on SUBOPTIMAL.
   Now queries `vkGetPhysicalDeviceSurfaceCapabilitiesKHR` and falls back to the
   passed size only when `currentExtent == 0xFFFFFFFF`.
3. **App used logical window size for the swapchain.** Under HiDPI the drawable
   differs from `SDL_GetWindowSize` (1920x1080 logical → 2400x1350 drawable at
   1.25x scale), which is the root cause of #1 under XWayland. main.cpp now uses
   `SDL_GetWindowSizeInPixels` for swapchain width/height, fit math, viewport
   and scissor.
