Files

6.4 KiB
Raw Permalink Blame History

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 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):

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