151 lines
6.4 KiB
Markdown
151 lines
6.4 KiB
Markdown
# 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.
|