# 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.