Compare commits

..

6 Commits

Author SHA1 Message Date
abdelrahman 8e6f051955 Research shaders for compositor ops 2026-08-09 22:49:26 +01:00
abdelrahman e59865bff5 Fix slangc warnings 2026-08-09 20:46:14 +01:00
abdelrahman e206e4647b Clear screen to neutral gray 2026-08-09 20:45:36 +01:00
abdelrahman a0b7c0672a Update AGENTS.md 2026-08-09 16:47:12 +01:00
abdelrahman 3d4f34c531 Session log 2026-08-08 23:50:06 +01:00
abdelrahman ebd1801883 Test a blit shader implementation 2026-08-08 23:14:29 +01:00
20 changed files with 2362 additions and 8532 deletions
+4 -1
View File
@@ -234,7 +234,7 @@ WpAllocator scratch = wpMemArenaAllocatorInitZero(KiB(16));
## Documentation
Save research notes and implementation plans as markdown in `documents/`:
Save research notes, implementation plans and session logs as markdown in `documents/`:
```
documents/
@@ -248,6 +248,9 @@ documents/
└── YYYY-MM-DD.md
```
At the start of each new session, read the previous session logs to understand what
we've implemented so far
## Skills
Domain-specific conventions are stored as skills in `.opencode/skills/<name>/SKILL.md`.
+41
View File
@@ -0,0 +1,41 @@
// Prism fullscreen texture blit shader.
//
// Draws a selected texture from the bindless array as a fullscreen quad that
// is letterboxed/pillarboxed to preserve aspect ratio (contain-fit). The NDC
// content rect is supplied via push constants so the texture is never
// stretched, squashed, or cropped.
struct BlitData {
float4 rect; // NDC fit rect: x0, y0, x1, y1
uint selected;
uint mode; // 0 = sample texture, 1 = solid background
uint pad[2];
};
[[vk::push_constant]]
BlitData blit;
Sampler2D textures[];
struct VSOutput {
float4 Pos : SV_POSITION;
float2 UV;
};
[shader("vertex")]
VSOutput main(uint vertexIndex : SV_VertexID) {
VSOutput output;
float2 uv = float2(float(vertexIndex & 1), float((vertexIndex >> 1) & 1));
output.UV = uv;
float2 pos = lerp(blit.rect.xy, blit.rect.zw, uv);
output.Pos = float4(pos, 0.0, 1.0);
return output;
}
[shader("fragment")]
float4 main(VSOutput input) {
if (blit.mode == 1) {
return float4(0.18, 0.18, 0.18, 1.0);
}
return textures[NonUniformResourceIndex(blit.selected)].Sample(input.UV);
}
-61
View File
@@ -1,61 +0,0 @@
/* Copyright (c) 2025-2026, Sascha Willems
*
* SPDX-License-Identifier: MIT
*
*/
struct VSInput {
float3 Pos;
float3 Normal;
float2 UV;
};
Sampler2D textures[];
struct ShaderData {
float4x4 projection;
float4x4 view;
float4x4 model[3];
float4 lightPos;
uint32_t selected;
};
struct VSOutput {
float4 Pos : SV_POSITION;
float3 Normal;
float2 UV;
float3 Factor;
float3 LightVec;
float3 ViewVec;
uint32_t InstanceIndex;
};
[shader("vertex")]
VSOutput main(VSInput input, uniform ShaderData *shaderData, uint instanceIndex : SV_VulkanInstanceID) {
VSOutput output;
float4x4 modelMat = shaderData->model[instanceIndex];
output.Normal = mul((float3x3)mul(shaderData->view, modelMat), input.Normal);
output.UV = input.UV;
output.Pos = mul(shaderData->projection, mul(shaderData->view, mul(modelMat, float4(input.Pos.xyz, 1.0))));
output.Factor = (shaderData->selected == instanceIndex ? 3.0f : 1.0f);
output.InstanceIndex = instanceIndex;
// Calculate view vectors required for lighting
float4 fragPos = mul(mul(shaderData->view, modelMat), float4(input.Pos.xyz, 1.0));
output.LightVec = shaderData->lightPos.xyz - fragPos.xyz;
output.ViewVec = -fragPos.xyz;
return output;
}
[shader("fragment")]
float4 main(VSOutput input) {
// Phong lighting
float3 N = normalize(input.Normal);
float3 L = normalize(input.LightVec);
float3 V = normalize(input.ViewVec);
float3 R = reflect(-L, N);
float3 diffuse = max(dot(N, L), 0.0025);
float3 specular = pow(max(dot(R, V), 0.0), 16.0) * 0.75;
// Sample from texture
float3 color = textures[NonUniformResourceIndex(input.InstanceIndex)].Sample(input.UV).rgb * input.Factor;
return float4(diffuse * color.rgb + specular, 1.0);
}
-62
View File
@@ -1,62 +0,0 @@
# Blender 5.0.1 MTL File: 'suzanne4.blend'
# www.blender.org
newmtl Material
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Material.001
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.127605 0.127605 0.127605
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Material.002
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.000000 0.000000 0.000000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Material.003
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Material.004
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.800007 0.200800 0.327761
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Material.005
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 1.000000 1.000000 1.000000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
-8150
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+150
View File
@@ -0,0 +1,150 @@
# 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 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.
File diff suppressed because it is too large Load Diff
+60
View File
@@ -0,0 +1,60 @@
# Session Log — 2026-08-08
## Fullscreen Texture Viewer — Verification and RHI Fixes
Continuation of the fullscreen-blit plan (`documents/plans/fullscreen-blit.md`),
which rewrote `main.cpp` from the Suzanne mesh demo into a bindless texture
viewer with contain-fit. This session visually verified the renderer and fixed
three real bugs uncovered by that verification.
### Verification method
- Ran the app under the X11 SDL driver (`SDL_VIDEODRIVER=x11`) and captured the
window with `xwd -id <window>`, then sampled pixels with ImageMagick
(`magick -format "%[pixel:p{x,y}]" info:`). The model cannot view images, so
all render checks were programmatic (pure-black bars, centered content).
- XTEST synthesised keys (`XTestFakeKeyEvent`) are silently dropped by
KWin/XWayland (confirmed with `xev`: FocusIn arrives via `_NET_ACTIVE_WINDOW`,
KeyPress never does). So texture cycling could not be driven headlessly; fit
cases were verified by temporarily making each texture the initial selection.
### Contain-fit verified (pixel-sampled)
| Texture | Window | Result |
|---------------------|-----------|---------------------------------------|
| square 1024x1024 | 16:9 | pillarbox (black L/R bars) |
| test_wide 2048x512 | 16:9 | letterbox (black T/B bars) |
| test_tall 512x2048 | 16:9 | pillarbox (black L/R bars) |
| test_fill 1920x1080 | 16:9 | fills exactly (no bars) |
| square, then resized to portrait | portrait | fit recomputed per frame → flips to letterbox |
### Bugs found and fixed
1. **`VK_SUBOPTIMAL_KHR` crashed the app.** `prRhiAcquireNextImageVk` and
`prRhiPresentVk` routed SUBOPTIMAL into `_checkVk``__builtin_trap()`
(SIGILL, caught under X11 immediately). Both now return
`PR_RHI_SWAPCHAIN_OUT_OF_DATE` for SUBOPTIMAL, same as OUT_OF_DATE
(`pr_rhi_vk.c`).
2. **Swapchain recreate ignored surface extent.** `prRhiRecreateSwapchainVk`
hard-coded the passed width/height. Now queries
`vkGetPhysicalDeviceSurfaceCapabilitiesKHR` and falls back to the passed
size only when `currentExtent == 0xFFFFFFFF` (matches the initial-create
logic).
3. **App used logical window size for the swapchain.** Under HiDPI the drawable
differs from `SDL_GetWindowSize` (1920x1080 logical vs 2400x1350 drawable at
1.25x scale on XWayland) — the root cause of #1. main.cpp now uses
`SDL_GetWindowSizeInPixels` for swapchain width/height, the fit rect, the
viewport, and the scissor.
### CLI arg considered and removed
Added a `--texture N` startup arg to drive the verification, then removed it at
the user's request (`main()` is back to no-args, `app.selected = 0`). If
headless key injection is ever needed again, revisit (e.g. `ydotool`/`wtype` on
Wayland, or a WM on a real X server).
### Notes
- `main.cpp` header comment, `<cstdlib>` include, and plan doc all updated to
reflect the removed arg.
- Native Wayland run still clean after all fixes; `just build` passes.
+59
View File
@@ -0,0 +1,59 @@
# Session Log — 2026-08-09
## Background colour change (blit shader)
- User requested changing the letterbox/pillarbox background from black to neutral grey.
- Initial attempt: changed the render pass clear color to `(0.5, 0.5, 0.5, 1.0)`. This
triggered the NVIDIA validation layer warning
`BestPractices-NVIDIA-ClearColor-NotCompressed` — SRGB fast clears only work
with 0.0 or 1.0 on NVIDIA tile-based GPUs.
- Reverted the clear color and implemented the proper solution: draw a fullscreen
grey quad in the fragment shader before the texture quad. The render pass clear
stays at 0.0 (fast-compressed).
- Added `mode` field to `BlitData` push constant. Mode 0 samples the texture, mode
1 outputs solid grey.
- User noted that a branch in the shader is free (no warp divergence since `mode`
is uniform per draw call). Agreed — no need for a separate clear pipeline.
- Changed grey from 0.5 to 0.18 (18% grey card, standard in photography/compositing).
- Fixed a Slang compilation warning by updating the profile from `spirv_1_4` to
`spirv_1_6` and explicitly declaring the required capabilities.
## Shader filter node research
- User requested research on: Gaussian blur, CDL, Laplacian, Sobel, sharpen,
posterize, pixelize, Kuwahara.
- Launched a research agent that produced `documents/research/shader-filters.md`
covering all filters with formulas, Slang pseudocode, parameter tables, and
performance notes.
## Design decisions made during review
1. **Colour space**: all intermediate textures are linear float
(`R16G16B16A16_SFLOAT`, `R32G32B32A32_SFLOAT` for Kuwahara tensor). sRGB images
are linearized once at load by the Read node. Final blit to sRGB swapchain
handles display encoding.
2. **Alpha**: premultiplied everywhere by default. Explicit Unpremult/Premult
nodes for operations that need unpremultiplied values (Nuke model).
3. **Edge handling**: per-node parameter, clamp-to-edge default, clamp-to-border
option. Affects sampler state, not shader branches.
4. **Premult has no parameters**: removed the empty push constant struct.
## Research document fixes
- Fixed a contradictory sentence about push constant sizes and CDL block size.
- Added Unpremult (§9) and Premult (§10) sections with full implementations.
- Added `edge_mode` field to all 5 spatial filter push constant blocks (Gaussian,
Laplacian, Sobel, Sharpen, Kuwahara).
- Restructured the implications section (§12) into open items vs resolved decisions.
- Expanded all mathematics sections with plain-language explanations suitable for
someone without a strong math background.
## Open items for next session
- Begin implementing the actual shader nodes in Prism
- Node system needs: per-pass resource signatures, scratch texture hooks, per-node
sampler choice, compile-time-bounded loop limits
- Classic Kuwahara is the recommended first implementation (single pass)
+17 -3
View File
@@ -13,7 +13,7 @@ VENDOR_INC := BUILDDIR + "/include"
VENDOR_LIB := BUILDDIR + "/lib"
VK_FLAGS := "-DPR_RHI_VULKAN -DVK_NO_PROTOTYPES -I" + VK_SDK + "/include -I" + VK_SDK + "/include/vma -I" + VENDOR_INC
APP_INC := "-I" + VK_SDK + "/include -I" + VK_SDK + "/include/vma -I" + VK_SDK + "/include/slang -I" + VENDOR_INC + " -Isrc"
APP_INC := "-I" + VK_SDK + "/include -I" + VK_SDK + "/include/vma -I" + VENDOR_INC + " -Isrc"
# Build KTX from source
vendor:
@@ -30,8 +30,22 @@ vendor:
cmake --build {{BUILDDIR}}/ktx --config Release
cmake --install {{BUILDDIR}}/ktx
# Compile shaders from .slang to SPIR-V
shaders:
mkdir -p {{BUILDDIR}}/shaders
{{VK_SDK}}/bin/slangc -target spirv \
-profile spirv_1_6+\
SPV_GOOGLE_user_type+\
spvFragmentFullyCoveredEXT+\
spvDerivativeControl+\
spvImageQuery+\
spvImageGatherExtended+\
spvSparseResidency+\
spvMinLod \
-o {{BUILDDIR}}/shaders/blit.spv assets/blit.slang
# Build all objects, then link
build: vendor
build: vendor shaders
mkdir -p {{BUILDDIR}}/bin
bear -- {{CXX}} -g -c -Wno-nullability-completeness {{VK_FLAGS}} \
src/prism/rhi/vulkan/profiles/vulkan_profiles.cpp \
@@ -50,7 +64,7 @@ build: vendor
bear -a -- {{CXX}} -g {{VK_FLAGS}} \
-L{{VK_SDK}}/lib -L{{VENDOR_LIB}} \
build/*.o \
-lSDL3 -lglm -ltinyobjloader -lktx -lslang -lvulkan \
-lSDL3 -lktx -lvulkan \
-Wl,-rpath,{{VENDOR_LIB}} -Wl,-rpath,{{VK_SDK}}/lib \
-o {{BUILDDIR}}/bin/prism
@echo "--- build done: {{BUILDDIR}}/bin/prism ---"
+159 -251
View File
@@ -1,26 +1,18 @@
// vim:fileencoding=utf-8:foldmethod=marker
//
// Prism port of how-to-vulkan's main.cpp — uses the RHI API instead of
// direct Vulkan calls.
// Prism texture viewer — draws a texture fullscreen with contain-fit
// (letterbox/pillarbox to preserve aspect ratio, never crops or stretches).
// Texture selection cycles with +/-.
#include "prism/rhi/pr_rhi_types.h"
#include "prism/rhi/pr_rhi.h"
#include <SDL3/SDL_timer.h>
#include <glm/ext/matrix_clip_space.hpp>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <SDL3/SDL.h>
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_init.h>
#include <SDL3/SDL_keycode.h>
#include <SDL3/SDL_video.h>
#include <slang/slang.h>
#include <slang/slang-com-ptr.h>
#include <tiny_obj_loader.h>
#include <iostream>
#include <vector>
#include <cstdint>
// ============================================================================
// Exit codes
@@ -29,18 +21,12 @@
enum ExitCode {
EXIT_CODE_SUCCESS,
EXIT_CODE_SDL_INIT_FAILED,
EXIT_CODE_VULKAN_LIB_LOAD_FAILED,
EXIT_CODE_WINDOW_CREATION_FAILED,
EXIT_CODE_SURFACE_CREATION_FAILED,
EXIT_CODE_GET_WINDOW_SIZE_FAILED,
EXIT_CODE_NO_INSTANCE_SUPPORT,
EXIT_CODE_NO_PHYSICAL_DEVICES,
EXIT_CODE_ALLOCATION_FAILURE,
EXIT_CODE_NO_SUITABLE_PHYSICAL_DEVICE,
EXIT_CODE_NO_PRESENTATION_SUPPORT,
EXIT_CODE_NO_SUITABLE_DEPTH_FORMAT,
EXIT_CODE_MESH_LOAD_FAILED,
EXIT_CODE_SYNC_OBJ_CREATE_FAILED,
EXIT_CODE_ALLOCATION_FAILURE,
EXIT_CODE_SHADER_LOAD_FAILED,
};
static inline void check(bool result, i32 code) {
@@ -54,39 +40,40 @@ static inline void check(bool result, i32 code) {
// Types
// ============================================================================
struct Vertex {
glm::vec3 pos;
glm::vec3 normal;
glm::vec2 uv;
};
struct ShaderData {
glm::mat4 projection;
glm::mat4 view;
glm::mat4 model[3];
glm::vec4 light_pos{ 0.0f, -10.0f, 10.0f, 0.0f };
u32 selected{ 1 };
};
struct TextureResources {
PrRhiTexture *texture;
PrRhiSampler *sampler;
};
// Push constant block — matches BlitData in assets/blit.slang (16-byte aligned,
// so the struct is padded to 32 bytes).
struct BlitData {
f32 rect[4]; // NDC fit rect: x0, y0, x1, y1
u32 selected;
u32 mode; // 0 = sample texture, 1 = solid background
u32 pad[2];
};
// Typedefs for wapp arrays of our types
typedef Vertex *VertexArray;
typedef u16 *U16Array;
typedef glm::vec3 *GlmVec3Array;
typedef TextureResources *TextureResourcesArray;
typedef PrRhiBuffer **PrRhiBufferArray;
typedef PrRhiFence **PrRhiFenceArray;
typedef PrRhiSemaphore **PrRhiSemaphoreArray;
typedef PrRhiCommandBuffer **PrRhiCommandBufferArray;
typedef TextureResources *TextureResourcesArray;
typedef PrRhiSemaphore **PrRhiSemaphoreArray;
typedef PrRhiFence **PrRhiFenceArray;
typedef PrRhiCommandBuffer **PrRhiCommandBufferArray;
// ============================================================================
// Global state
// ============================================================================
static const char *const TEXTURE_PATHS[] = {
"assets/suzanne0.ktx",
"assets/suzanne1.ktx",
"assets/suzanne2.ktx",
"assets/test_square.ktx",
"assets/test_fill.ktx",
"assets/test_wide.ktx",
"assets/test_tall.ktx",
};
struct AppState {
PrRhiInstance *inst;
PrRhiPhysicalDevice *pdev;
@@ -96,40 +83,62 @@ struct AppState {
PrRhiFormat swapchain_format;
PrRhiBuffer *vert_index_buf;
u64 vertex_buf_size;
u64 index_count;
static constexpr u32 max_frames_in_flight = 2;
static constexpr u32 instance_count = 3;
static constexpr u32 texture_count = 3;
static constexpr u32 texture_count = (u32)(sizeof(TEXTURE_PATHS) / sizeof(TEXTURE_PATHS[0]));
PrRhiBufferArray shader_data_bufs;
PrRhiFenceArray fences;
PrRhiSemaphoreArray image_acquired_semaphores;
PrRhiSemaphoreArray render_completed_semaphores;
PrRhiCommandPool *cmd_pool;
PrRhiCommandBufferArray cmd_buffers;
PrRhiFenceArray fences;
PrRhiSemaphoreArray image_acquired_semaphores;
PrRhiSemaphoreArray render_completed_semaphores;
u32 render_semaphore_count;
PrRhiCommandPool *cmd_pool;
PrRhiCommandBufferArray cmd_buffers;
TextureResourcesArray textures;
PrRhiDescriptorSetLayout *desc_set_layout;
PrRhiDescriptorPool *desc_pool;
PrRhiDescriptorSet *desc_set;
TextureResourcesArray textures;
PrRhiDescriptorSetLayout *desc_set_layout;
PrRhiDescriptorPool *desc_pool;
PrRhiDescriptorSet *desc_set;
PrRhiShader *shader;
PrRhiPipelineLayout *pipeline_layout;
PrRhiPipeline *pipeline;
PrRhiShader *shader;
PrRhiPipelineLayout *pipeline_layout;
PrRhiPipeline *pipeline;
ShaderData shader_data;
u32 frame_index;
glm::ivec2 window_size;
GlmVec3Array object_rotations;
bool update_swapchain;
u32 selected;
u32 frame_index;
i32 window_width;
i32 window_height;
bool update_swapchain;
SDL_Window *window;
Slang::ComPtr<slang::IGlobalSession> slang_session;
SDL_Window *window;
};
// ============================================================================
// Helpers
// ============================================================================
// Fills rect with the NDC bounds (x0, y0, x1, y1) of the texture content when
// contained within the window, preserving aspect ratio and centering.
static void computeFitRect(f32 tex_w, f32 tex_h, f32 win_w, f32 win_h, f32 *rect) {
if (win_w <= 0.0f || win_h <= 0.0f || tex_w <= 0.0f || tex_h <= 0.0f) {
rect[0] = -1.0f;
rect[1] = -1.0f;
rect[2] = 1.0f;
rect[3] = 1.0f;
return;
}
f32 scale_x = win_w / tex_w;
f32 scale_y = win_h / tex_h;
f32 scale = scale_x < scale_y ? scale_x : scale_y;
f32 content_w = tex_w * scale;
f32 content_h = tex_h * scale;
rect[0] = -content_w / win_w;
rect[1] = -content_h / win_h;
rect[2] = content_w / win_w;
rect[3] = content_h / win_h;
}
// ============================================================================
// Main
// ============================================================================
@@ -144,7 +153,7 @@ int main() {
check(SDL_Init(SDL_INIT_VIDEO), EXIT_CODE_SDL_INIT_FAILED);
f32 display_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
app.window = SDL_CreateWindow("How To Vulkan (Prism)", (i32)(display_scale * 1920),
app.window = SDL_CreateWindow("Prism — Texture Viewer", (i32)(display_scale * 1920),
(i32)(display_scale * 1080),
SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE);
check(app.window != nullptr, EXIT_CODE_WINDOW_CREATION_FAILED);
@@ -185,7 +194,7 @@ int main() {
// }}}
// {{{ Surface creation
check(SDL_GetWindowSize(app.window, &app.window_size.x, &app.window_size.y),
check(SDL_GetWindowSizeInPixels(app.window, &app.window_width, &app.window_height),
EXIT_CODE_GET_WINDOW_SIZE_FAILED);
app.surface = prRhiCreateSurfaceFromWindow(app.inst, app.window);
// }}}
@@ -200,8 +209,8 @@ int main() {
// {{{ Swapchain creation
PrRhiSwapchainDesc swap_desc = {};
swap_desc.surface = app.surface;
swap_desc.width = (u32)app.window_size.x;
swap_desc.height = (u32)app.window_size.y;
swap_desc.width = (u32)app.window_width;
swap_desc.height = (u32)app.window_height;
swap_desc.has_depth = true;
swap_desc.depth_format = PR_RHI_FORMAT_D24_UNORM_S8_UINT;
@@ -209,77 +218,7 @@ int main() {
app.swapchain_format = prRhiGetSwapchainFormat(app.swapchain);
// }}}
// {{{ Vertex/Index buffers
// {{{ Load mesh
tinyobj::attrib_t attrib;
std::vector<tinyobj::shape_t> shapes;
std::vector<tinyobj::material_t> materials;
check(tinyobj::LoadObj(&attrib, &shapes, &materials, nullptr, nullptr, "assets/suzanne.obj"),
EXIT_CODE_MESH_LOAD_FAILED);
VertexArray vertices = wpArrayAllocCapacity(Vertex, &arena, 128, WP_ARRAY_INIT_NONE);
U16Array indices = wpArrayAllocCapacity(u16, &arena, 128, WP_ARRAY_INIT_NONE);
for (auto &idx : shapes[0].mesh.indices) {
Vertex v = {};
v.pos = {
attrib.vertices[idx.vertex_index * 3],
-attrib.vertices[idx.vertex_index * 3 + 1],
attrib.vertices[idx.vertex_index * 3 + 2]
};
v.normal = {
attrib.normals[idx.normal_index * 3],
-attrib.normals[idx.normal_index * 3 + 1],
attrib.normals[idx.normal_index * 3 + 2]
};
v.uv = {
attrib.texcoords[idx.texcoord_index * 2],
1.0f - attrib.texcoords[idx.texcoord_index * 2 + 1]
};
u16 index = (u16)wpArrayCount(indices);
vertices = wpArrayAppendAlloc(Vertex, &arena, vertices, &v, WP_ARRAY_INIT_NONE);
indices = wpArrayAppendAlloc(u16, &arena, indices, &index, WP_ARRAY_INIT_NONE);
}
app.vertex_buf_size = sizeof(Vertex) * wpArrayCount(vertices);
app.index_count = wpArrayCount(indices);
u64 index_buf_size = sizeof(u16) * app.index_count;
// }}}
// {{{ Create GPU buffer
PrRhiBufferDesc vert_desc = {};
vert_desc.size = app.vertex_buf_size + index_buf_size;
vert_desc.usage = (PrRhiBufferUsage)(PR_RHI_BUFFER_USAGE_VERTEX | PR_RHI_BUFFER_USAGE_INDEX);
vert_desc.memory = PR_RHI_MEMORY_CPU_TO_GPU;
app.vert_index_buf = prRhiCreateBuffer(app.device, vert_desc);
void *mapped = prRhiBufferMap(app.device, app.vert_index_buf);
memcpy(mapped, vertices, app.vertex_buf_size);
memcpy((u8 *)mapped + app.vertex_buf_size, indices, index_buf_size);
prRhiBufferUnmap(app.device, app.vert_index_buf);
// }}}
// }}}
// {{{ Shader data buffers
app.shader_data_bufs = wpArrayAllocCapacity(PrRhiBuffer *, &arena,
AppState::max_frames_in_flight,
WP_ARRAY_INIT_FILLED);
for (u32 i = 0; i < AppState::max_frames_in_flight; ++i) {
PrRhiBufferDesc buf_desc = {};
buf_desc.size = sizeof(ShaderData);
buf_desc.usage = (PrRhiBufferUsage)(PR_RHI_BUFFER_USAGE_STORAGE |
PR_RHI_BUFFER_USAGE_SHADER_DEVICE_ADDRESS);
buf_desc.memory = PR_RHI_MEMORY_CPU_TO_GPU;
app.shader_data_bufs[i] = prRhiCreateBuffer(app.device, buf_desc);
}
// }}}
// {{{ Synchronisation objects
// Fences
app.fences = wpArrayAllocCapacity(PrRhiFence *, &arena, AppState::max_frames_in_flight,
WP_ARRAY_INIT_FILLED);
for (u32 i = 0; i < AppState::max_frames_in_flight; ++i) {
@@ -288,7 +227,6 @@ int main() {
app.fences[i] = prRhiCreateFence(app.device, fd);
}
// Image acquired semaphores (per frame)
app.image_acquired_semaphores = wpArrayAllocCapacity(PrRhiSemaphore *, &arena,
AppState::max_frames_in_flight,
WP_ARRAY_INIT_FILLED);
@@ -296,12 +234,11 @@ int main() {
app.image_acquired_semaphores[i] = prRhiCreateSemaphore(app.device);
}
// Render completed semaphores (per swapchain image)
u32 swapchain_image_count = prRhiGetSwapchainImageCount(app.swapchain);
app.render_semaphore_count = prRhiGetSwapchainImageCount(app.swapchain);
app.render_completed_semaphores = wpArrayAllocCapacity(PrRhiSemaphore *, &arena,
swapchain_image_count,
app.render_semaphore_count,
WP_ARRAY_INIT_FILLED);
for (u32 i = 0; i < swapchain_image_count; ++i) {
for (u32 i = 0; i < app.render_semaphore_count; ++i) {
app.render_completed_semaphores[i] = prRhiCreateSemaphore(app.device);
}
// }}}
@@ -320,11 +257,8 @@ int main() {
PrRhiCommandBuffer *upload_cb = upload_cbs[0];
for (u32 i = 0; i < AppState::texture_count; ++i) {
char buf[2048] = {};
snprintf(buf, sizeof(buf), "assets/suzanne%u.ktx", i);
PrRhiTexture *tex = prRhiCreateTextureFromKtx(app.device, buf, app.cmd_pool, upload_cb);
PrRhiTexture *tex = prRhiCreateTextureFromKtx(app.device, TEXTURE_PATHS[i], app.cmd_pool, upload_cb);
// Create sampler
PrRhiSamplerDesc samp_desc = {};
samp_desc.mag_filter = PR_RHI_FILTER_LINEAR;
samp_desc.min_filter = PR_RHI_FILTER_LINEAR;
@@ -355,7 +289,6 @@ int main() {
PrRhiDescriptorSetLayoutDesc layout_desc = { ds_layouts };
app.desc_set_layout = prRhiCreateDescriptorSetLayout(app.device, layout_desc);
// Pool
PrRhiDescriptorPoolSize pool_size = {};
pool_size.type = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
pool_size.descriptor_count = AppState::texture_count;
@@ -366,12 +299,10 @@ int main() {
app.desc_pool = prRhiCreateDescriptorPool(app.device, ds_pool_desc);
// Allocate descriptor set (variable count)
WpU32Array var_counts = wpArray(u32, AppState::texture_count);
app.desc_set = prRhiAllocateDescriptorSet(app.device, app.desc_pool, app.desc_set_layout,
var_counts);
// Write descriptor set
PrRhiDescriptorImageInfoArray img_infos = wpArrayAllocCapacity(PrRhiDescriptorImageInfo,
&arena,
AppState::texture_count,
@@ -394,39 +325,29 @@ int main() {
prRhiUpdateDescriptorSet(app.device, writes);
// }}}
// {{{ Shader compilation (Slang)
slang::createGlobalSession(app.slang_session.writeRef());
// {{{ Load blit shader (pre-compiled SPIR-V)
WpStr8RO spirv_path = wpStr8LitRo("build/shaders/blit.spv");
WpFile *spirv_file = wpFileOpen(&arena, &spirv_path, WP_ACCESS_READ);
check(spirv_file != nullptr, EXIT_CODE_SHADER_LOAD_FAILED);
slang::TargetDesc target = {};
target.format = SLANG_SPIRV;
target.profile = {app.slang_session->findProfile("spirv_1_4")};
i64 spirv_len = wpFileGetLength(spirv_file);
void *spirv = wpMemAllocatorAlloc(&arena, (u64)spirv_len);
check(spirv != nullptr, EXIT_CODE_ALLOCATION_FAILURE);
wpFileRead(spirv, spirv_file, (u64)spirv_len);
wpFileClose(spirv_file);
Slang::ComPtr<slang::ISession> slang_session;
slang::SessionDesc session_desc = {};
session_desc.targets = &target;
session_desc.targetCount = 1;
session_desc.defaultMatrixLayoutMode = SLANG_MATRIX_LAYOUT_COLUMN_MAJOR;
app.slang_session->createSession(session_desc, slang_session.writeRef());
Slang::ComPtr<slang::IModule> slang_module {
slang_session->loadModuleFromSource("triangle", "assets/shader.slang", nullptr, nullptr),
};
Slang::ComPtr<slang::IBlob> spirv;
slang_module->getTargetCode(0, spirv.writeRef());
// }}}
// {{{ Create shader module
PrRhiShaderDesc shader_desc = {};
shader_desc.spirv_code = spirv->getBufferPointer();
shader_desc.spirv_size = spirv->getBufferSize();
shader_desc.spirv_code = spirv;
shader_desc.spirv_size = (u64)spirv_len;
app.shader = prRhiCreateShader(app.device, shader_desc);
// }}}
// {{{ Pipeline layout
PrRhiPushConstantRange pc_range = {};
pc_range.stage_flags = PR_RHI_SHADER_STAGE_VERTEX;
pc_range.size = sizeof(u64);
pc_range.stage_flags = (PrRhiShaderStage)(PR_RHI_SHADER_STAGE_VERTEX |
PR_RHI_SHADER_STAGE_FRAGMENT);
pc_range.size = sizeof(BlitData);
PrRhiDescriptorSetLayoutArray pl_layouts = wpArray(PrRhiDescriptorSetLayout *, app.desc_set_layout);
@@ -438,18 +359,6 @@ int main() {
// }}}
// {{{ Graphics pipeline
PrRhiVertexInputBindingArray vertex_bindings = wpArray(
PrRhiVertexInputBinding,
PrRhiVertexInputBinding{ 0, sizeof(Vertex) }
);
PrRhiVertexAttributeArray vertex_attrs = wpArray(
PrRhiVertexAttribute,
PrRhiVertexAttribute{ 0, 0, PR_RHI_FORMAT_R32G32B32_SFLOAT, 0 },
PrRhiVertexAttribute{ 1, 0, PR_RHI_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, normal) },
PrRhiVertexAttribute{ 2, 0, PR_RHI_FORMAT_R32G32_SFLOAT, offsetof(Vertex, uv) }
);
PrRhiColorBlendAttachmentArray blend_attachments = wpArray(
PrRhiColorBlendAttachment,
PrRhiColorBlendAttachment{ 0xf }
@@ -457,37 +366,34 @@ int main() {
PrRhiFormatArray color_fmt_array = wpArray(PrRhiFormat, app.swapchain_format);
PrRhiGraphicsPipelineDesc pipe_desc = {};
PrRhiGraphicsPipelineDesc pipe_desc = {};
pipe_desc.vertex_shader = app.shader;
pipe_desc.vertex_shader_entry_point = "main";
pipe_desc.fragment_shader = app.shader;
pipe_desc.fragment_shader_entry_point = "main";
pipe_desc.vertex_bindings = vertex_bindings;
pipe_desc.vertex_attributes = vertex_attrs;
pipe_desc.topology = PR_RHI_TOPOLOGY_TRIANGLE_LIST;
pipe_desc.topology = PR_RHI_TOPOLOGY_TRIANGLE_STRIP;
pipe_desc.color_attachment_formats = color_fmt_array;
pipe_desc.depth_attachment_format = swap_desc.depth_format;
pipe_desc.depth_test_enable = true;
pipe_desc.depth_write_enable = true;
pipe_desc.depth_compare_op = PR_RHI_COMPARE_OP_LESS_OR_EQUAL;
pipe_desc.depth_test_enable = false;
pipe_desc.depth_write_enable = false;
pipe_desc.blend_attachments = blend_attachments;
pipe_desc.dynamic_viewport = true;
pipe_desc.dynamic_scissor = true;
pipe_desc.cull_mode = PR_RHI_CULL_MODE_BACK;
pipe_desc.front_face = PR_RHI_FRONT_FACE_COUNTER_CLOCKWISE;
pipe_desc.cull_mode = PR_RHI_CULL_MODE_NONE;
pipe_desc.line_width = 1.0f;
pipe_desc.multisample_count = PR_RHI_SAMPLE_COUNT_1;
pipe_desc.layout = app.pipeline_layout;
app.pipeline = prRhiCreateGraphicsPipeline(app.device, pipe_desc);
// }}}
// {{{ Render loop
app.object_rotations = wpArrayAllocCapacity(glm::vec3, &arena, AppState::instance_count,
WP_ARRAY_INIT_FILLED);
u64 last_time = SDL_GetTicks();
SDL_Event event = {};
app.frame_index = 0;
app.selected = 0;
u32 image_index = 0;
PrRhiTextureSize tex_size = prRhiGetTextureSize(app.textures[app.selected].texture);
std::cout << "Texture " << app.selected << ": " << TEXTURE_PATHS[app.selected]
<< " (" << tex_size.width << 'x' << tex_size.height << ")\n";
while (true) {
// {{{ Wait on fence
@@ -508,20 +414,11 @@ int main() {
if (app.update_swapchain) {
// Skip this frame — will recreate below
} else {
// {{{ Update shader data
app.shader_data.projection = glm::perspective(glm::radians(45.0f),
(f32)app.window_size.x / (f32)app.window_size.y,
0.1f, 32.0f);
app.shader_data.view = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, -6.0f));
for (i32 i = 0; i < (i32)AppState::instance_count; ++i) {
glm::vec3 instance_pos = glm::vec3((f32)(i - 1) * 3.0f, 0.0f, 0.0f);
app.shader_data.model[i] = glm::translate(glm::mat4(1.0f), instance_pos) *
glm::mat4_cast(glm::quat(app.object_rotations[i]));
}
void *shader_data_ptr = prRhiBufferMap(app.device, app.shader_data_bufs[app.frame_index]);
memcpy(shader_data_ptr, &app.shader_data, sizeof(ShaderData));
prRhiBufferUnmap(app.device, app.shader_data_bufs[app.frame_index]);
// {{{ Compute contain-fit rect
PrRhiTextureSize tex_size = prRhiGetTextureSize(app.textures[app.selected].texture);
f32 fit_rect[4] = {};
computeFitRect((f32)tex_size.width, (f32)tex_size.height,
(f32)app.window_width, (f32)app.window_height, fit_rect);
// }}}
// {{{ Record command buffer
@@ -534,7 +431,6 @@ int main() {
PrRhiImageMemoryBarrierArray barriers_arr =
wpArrayWithCapacity(PrRhiImageMemoryBarrier, 2, WP_ARRAY_INIT_FILLED);
// Color attachment
PrRhiTexture *color_tex = prRhiGetSwapchainTexture(app.swapchain, image_index);
barriers_arr[0].texture = color_tex;
barriers_arr[0].old_layout = PR_RHI_LAYOUT_UNDEFINED;
@@ -544,7 +440,6 @@ int main() {
barriers_arr[0].dst_stage_mask = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT;
barriers_arr[0].dst_access_mask = (PrRhiAccess)(PR_RHI_ACCESS_COLOR_ATTACHMENT_READ | PR_RHI_ACCESS_COLOR_ATTACHMENT_WRITE);
// Depth attachment
PrRhiTexture *depth_tex = prRhiGetSwapchainDepthTexture(app.swapchain);
barriers_arr[1].texture = depth_tex;
barriers_arr[1].old_layout = PR_RHI_LAYOUT_UNDEFINED;
@@ -578,8 +473,8 @@ int main() {
prRhiCmdBeginRendering(cb, color_arr, &depth_att);
}
prRhiCmdSetViewport(cb, 0.0f, 0.0f, (f32)app.window_size.x, (f32)app.window_size.y);
prRhiCmdSetScissor(cb, 0, 0, (u32)app.window_size.x, (u32)app.window_size.y);
prRhiCmdSetViewport(cb, 0.0f, 0.0f, (f32)app.window_width, (f32)app.window_height);
prRhiCmdSetScissor(cb, 0, 0, (u32)app.window_width, (u32)app.window_height);
prRhiCmdBindPipeline(cb, PR_RHI_PIPELINE_BIND_POINT_GRAPHICS, app.pipeline);
@@ -587,16 +482,34 @@ int main() {
prRhiCmdBindDescriptorSets(cb, PR_RHI_PIPELINE_BIND_POINT_GRAPHICS,
app.pipeline_layout, 0, sets);
PrRhiBufferArray vert_buf_arr = wpArray(PrRhiBuffer *, app.vert_index_buf);
WpU64Array vert_offsets = wpArray(u64, 0);
prRhiCmdBindVertexBuffers(cb, 0, vert_buf_arr, vert_offsets);
prRhiCmdBindIndexBuffer(cb, app.vert_index_buf, app.vertex_buf_size, PR_RHI_INDEX_TYPE_UINT16);
// Draw background (fullscreen grey quad)
BlitData bg = {};
bg.rect[0] = -1.0f;
bg.rect[1] = -1.0f;
bg.rect[2] = 1.0f;
bg.rect[3] = 1.0f;
bg.selected = 0;
bg.mode = 1;
prRhiCmdPushConstants(cb, app.pipeline_layout,
(PrRhiShaderStage)(PR_RHI_SHADER_STAGE_VERTEX |
PR_RHI_SHADER_STAGE_FRAGMENT),
0, sizeof(BlitData), &bg);
prRhiCmdDraw(cb, 4, 1, 0, 0);
// Push shader data buffer device address
u64 buf_addr = prRhiGetBufferDeviceAddress(app.device, app.shader_data_bufs[app.frame_index]);
prRhiCmdPushConstants(cb, app.pipeline_layout, PR_RHI_SHADER_STAGE_VERTEX, 0, sizeof(u64), &buf_addr);
// Draw texture (contain-fit)
BlitData blit = {};
blit.rect[0] = fit_rect[0];
blit.rect[1] = fit_rect[1];
blit.rect[2] = fit_rect[2];
blit.rect[3] = fit_rect[3];
blit.selected = app.selected;
blit.mode = 0;
prRhiCmdPushConstants(cb, app.pipeline_layout,
(PrRhiShaderStage)(PR_RHI_SHADER_STAGE_VERTEX |
PR_RHI_SHADER_STAGE_FRAGMENT),
0, sizeof(BlitData), &blit);
prRhiCmdDrawIndexed(cb, (u32)app.index_count, AppState::instance_count, 0, 0, 0);
prRhiCmdDraw(cb, 4, 1, 0, 0);
prRhiCmdEndRendering(cb);
// Transition to present
@@ -632,33 +545,28 @@ int main() {
}
// {{{ Poll events
f32 elapsed_time = (SDL_GetTicks() - last_time) / 1000.0f;
last_time = SDL_GetTicks();
SDL_Event event = {};
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_EVENT_QUIT:
app.update_swapchain = false; // signal exit — use update_swapchain flag
goto done;
case SDL_EVENT_KEY_DOWN:
if (event.key.key == SDLK_ESCAPE) goto done;
if (event.key.key == SDLK_PLUS || event.key.key == SDLK_KP_PLUS || event.key.key == SDLK_EQUALS) {
app.shader_data.selected = (app.shader_data.selected < 2) ? app.shader_data.selected + 1 : 0;
app.selected = (app.selected + 1) % AppState::texture_count;
PrRhiTextureSize ts = prRhiGetTextureSize(app.textures[app.selected].texture);
std::cout << "Texture " << app.selected << ": " << TEXTURE_PATHS[app.selected]
<< " (" << ts.width << 'x' << ts.height << ")\n";
}
if (event.key.key == SDLK_MINUS || event.key.key == SDLK_KP_MINUS) {
app.shader_data.selected = (app.shader_data.selected > 0) ? app.shader_data.selected - 1 : 2;
app.selected = (app.selected + AppState::texture_count - 1) % AppState::texture_count;
PrRhiTextureSize ts = prRhiGetTextureSize(app.textures[app.selected].texture);
std::cout << "Texture " << app.selected << ": " << TEXTURE_PATHS[app.selected]
<< " (" << ts.width << 'x' << ts.height << ")\n";
}
break;
case SDL_EVENT_MOUSE_MOTION:
if (event.button.button == SDL_BUTTON_LEFT) {
app.object_rotations[app.shader_data.selected].x -= (f32)event.motion.yrel * elapsed_time;
app.object_rotations[app.shader_data.selected].y += (f32)event.motion.xrel * elapsed_time;
}
break;
case SDL_EVENT_MOUSE_WHEEL:
// Camera position handled via shader_data update in loop
break;
case SDL_EVENT_WINDOW_RESIZED:
check(SDL_GetWindowSize(app.window, &app.window_size.x, &app.window_size.y),
check(SDL_GetWindowSizeInPixels(app.window, &app.window_width, &app.window_height),
EXIT_CODE_GET_WINDOW_SIZE_FAILED);
app.update_swapchain = true;
break;
@@ -670,12 +578,14 @@ int main() {
if (app.update_swapchain) {
prRhiDeviceWaitIdle(app.device);
prRhiRecreateSwapchain(app.device, &app.swapchain,
(u32)app.window_size.x, (u32)app.window_size.y);
(u32)app.window_width, (u32)app.window_height);
// Re-create render completed semaphores for new image count
// TODO: proper cleanup — for now, just leak old ones
u32 new_count = 0;
prRhiAcquireNextImage(app.device, app.swapchain, NULL, &new_count);
// Re-create render completed semaphores for the new image count
u32 new_count = prRhiGetSwapchainImageCount(app.swapchain);
for (u32 i = 0; i < app.render_semaphore_count; ++i) {
prRhiDestroySemaphore(app.device, app.render_completed_semaphores[i]);
}
app.render_semaphore_count = new_count;
app.render_completed_semaphores = wpArrayAllocCapacity(PrRhiSemaphore *, &arena,
new_count, WP_ARRAY_INIT_FILLED);
for (u32 i = 0; i < new_count; ++i) {
@@ -708,15 +618,13 @@ int main() {
prRhiFreeCommandBuffers(app.device, app.cmd_pool, app.cmd_buffers);
prRhiDestroyCommandPool(app.device, app.cmd_pool);
for (u32 i = 0; i < swapchain_image_count; ++i) {
for (u32 i = 0; i < app.render_semaphore_count; ++i) {
prRhiDestroySemaphore(app.device, app.render_completed_semaphores[i]);
}
for (u32 i = 0; i < AppState::max_frames_in_flight; ++i) {
prRhiDestroySemaphore(app.device, app.image_acquired_semaphores[i]);
prRhiDestroyFence(app.device, app.fences[i]);
prRhiDestroyBuffer(app.device, app.shader_data_bufs[i]);
}
prRhiDestroyBuffer(app.device, app.vert_index_buf);
prRhiDestroySwapchain(app.device, app.swapchain);
prRhiDestroyDevice(app.device);
+1
View File
@@ -111,6 +111,7 @@ PrRhiTexture *prRhiCreateTexture(PrRhiDevice *device, PrRhiTextureDesc desc);
PrRhiTexture *prRhiCreateTextureFromKtx(PrRhiDevice *device, const char *path,
PrRhiCommandPool *pool, PrRhiCommandBuffer *cb);
void prRhiDestroyTexture(PrRhiDevice *device, PrRhiTexture *texture);
PrRhiTextureSize prRhiGetTextureSize(PrRhiTexture *texture);
// ======================================================================
// Samplers
+5
View File
@@ -352,6 +352,11 @@ typedef struct PrRhiShaderDesc {
u64 spirv_size;
} PrRhiShaderDesc;
typedef struct PrRhiTextureSize {
u32 width;
u32 height;
} PrRhiTextureSize;
typedef struct PrRhiPipelineLayoutDesc {
PrRhiDescriptorSetLayoutArray set_layouts;
PrRhiPushConstantRangeArray push_constant_ranges;
+25 -4
View File
@@ -834,7 +834,9 @@ PrRhiSwapchainResult prRhiAcquireNextImageVk(PrRhiDevice *device, PrRhiSwapchain
VkSemaphore vk_semaphore = signal_semaphore ? signal_semaphore->handle : VK_NULL_HANDLE;
VkResult res = vkAcquireNextImageKHR(device->handle, swapchain->handle, UINT64_MAX, vk_semaphore,
VK_NULL_HANDLE, &swapchain->current_image_index);
if (res == VK_ERROR_OUT_OF_DATE_KHR) { return PR_RHI_SWAPCHAIN_OUT_OF_DATE; }
if (res == VK_ERROR_OUT_OF_DATE_KHR || res == VK_SUBOPTIMAL_KHR) {
return PR_RHI_SWAPCHAIN_OUT_OF_DATE;
}
_checkVk(res, "vkAcquireNextImageKHR");
if (out_image_index) { *out_image_index = swapchain->current_image_index; }
return PR_RHI_SWAPCHAIN_SUCCESS;
@@ -854,7 +856,9 @@ PrRhiSwapchainResult prRhiPresentVk(PrRhiDevice *device, PrRhiSwapchain *swapcha
};
VkResult res = vkQueuePresentKHR(device->queue, &present_info);
if (res == VK_ERROR_OUT_OF_DATE_KHR) { return PR_RHI_SWAPCHAIN_OUT_OF_DATE; }
if (res == VK_ERROR_OUT_OF_DATE_KHR || res == VK_SUBOPTIMAL_KHR) {
return PR_RHI_SWAPCHAIN_OUT_OF_DATE;
}
_checkVk(res, "vkQueuePresentKHR");
return PR_RHI_SWAPCHAIN_SUCCESS;
}
@@ -866,13 +870,22 @@ void prRhiRecreateSwapchainVk(PrRhiDevice *device, PrRhiSwapchain **swapchain,
VkDevice vk_device = device->handle;
VkPhysicalDevice vk_pdev = device->physical_device;
VkSurfaceKHR vk_surface = ((PrRhiSurface *)old->surface)->handle;
VkExtent2D extent = { width, height };
VkSurfaceCapabilitiesKHR caps = {};
_checkVk(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(vk_pdev, vk_surface, &caps),
"vkGetPhysicalDeviceSurfaceCapabilitiesKHR");
VkExtent2D extent = caps.currentExtent;
if (extent.width == 0xffffffff) {
extent.width = width;
extent.height = height;
}
VkSwapchainCreateInfoKHR swapchain_info = {
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
.oldSwapchain = old->handle,
.surface = ((PrRhiSurface *)old->surface)->handle,
.surface = vk_surface,
.minImageCount = old->image_count,
.imageFormat = (VkFormat)old->format,
.imageColorSpace = _default_colorspace,
@@ -1315,6 +1328,14 @@ PrRhiTexture *prRhiCreateTextureFromKtxVk(PrRhiDevice *device, const char *path,
return texture;
}
PrRhiTextureSize prRhiGetTextureSizeVk(PrRhiTexture *texture) {
PrRhiTextureSize size = {};
if (!texture) { return size; }
size.width = texture->width;
size.height = texture->height;
return size;
}
void prRhiDestroyTextureVk(PrRhiDevice *device, PrRhiTexture *texture) {
if (!texture) { return; }
VkDevice vk_device = device->handle;
+1
View File
@@ -166,6 +166,7 @@ PrRhiTexture *prRhiCreateTextureVk(PrRhiDevice *device, PrRhiTextureDesc desc);
PrRhiTexture *prRhiCreateTextureFromKtxVk(PrRhiDevice *device, const char *path,
PrRhiCommandPool *pool, PrRhiCommandBuffer *cb);
void prRhiDestroyTextureVk(PrRhiDevice *device, PrRhiTexture *texture);
PrRhiTextureSize prRhiGetTextureSizeVk(PrRhiTexture *texture);
PrRhiSampler *prRhiCreateSamplerVk(PrRhiDevice *device, PrRhiSamplerDesc desc);
void prRhiDestroySamplerVk(PrRhiDevice *device, PrRhiSampler *sampler);
+1
View File
@@ -35,6 +35,7 @@
#define prRhiCreateTexture prRhiCreateTextureVk
#define prRhiCreateTextureFromKtx prRhiCreateTextureFromKtxVk
#define prRhiDestroyTexture prRhiDestroyTextureVk
#define prRhiGetTextureSize prRhiGetTextureSizeVk
#define prRhiCreateSampler prRhiCreateSamplerVk
#define prRhiDestroySampler prRhiDestroySamplerVk
#define prRhiCreateShader prRhiCreateShaderVk