6.4 KiB
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):
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
justfileshadersrecipe:slangc -target spirv -profile spirv_1_4 -o build/shaders/blit.spv assets/blit.slang(entry points auto-detected from[shader(...)]attributes).builddepends on it. main.cpploadsbuild/shaders/blit.spvvia wapp file I/O (wpFileOpen/wpFileGetLength/wpFileRead) into an arena buffer, thenprRhiCreateShader. Noslang.hincludes, no runtime compilation.- Drop
-lslangand the-I .../slanginclude 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):
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
VK_SUBOPTIMAL_KHRaborted the app.prRhiAcquireNextImageVk/prRhiPresentVkroutedSUBOPTIMALinto_checkVk→__builtin_trap(). Both now treat it likeOUT_OF_DATE(returnPR_RHI_SWAPCHAIN_OUT_OF_DATE). Triggered immediately under X11/XWayland.- Swapchain recreate ignored surface extent.
prRhiRecreateSwapchainVkhard-coded the passed width/height; on surfaces whosecurrentExtentis meaningful (X11) that mismatched the drawable and re-looped on SUBOPTIMAL. Now queriesvkGetPhysicalDeviceSurfaceCapabilitiesKHRand falls back to the passed size only whencurrentExtent == 0xFFFFFFFF. - 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 usesSDL_GetWindowSizeInPixelsfor swapchain width/height, fit math, viewport and scissor.