Compare commits

..

27 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
abdelrahman f5ff6c70ea Fixes and research 2026-08-08 20:25:25 +01:00
abdelrahman 30408ff244 Revert "First pass at nodes"
This reverts commit 618f09689d.
2026-08-08 20:24:24 +01:00
abdelrahman 618f09689d First pass at nodes 2026-07-27 06:45:39 +01:00
abdelrahman 4bbd7dfe01 Add pool allocator 2026-07-26 17:37:09 +01:00
abdelrahman cc2d0cea6e Move wapp to vendor dir 2026-07-26 14:15:27 +01:00
abdelrahman 3c1997e2b1 Add ktx as submodule 2026-07-26 12:53:52 +01:00
abdelrahman 4b08866fc6 Restructure 2026-07-26 11:53:49 +01:00
Abdelrahman Said 96c89dcb86 Initial research on shaders 2026-07-14 00:03:40 +01:00
abdelrahman bb6d2eae14 Update session logs 2026-07-12 13:49:26 +01:00
abdelrahman 45a34bb151 RHI vulkan updates 2026-07-12 13:43:59 +01:00
abdelrahman bffe9b8174 Add RHI global context allocator 2026-07-11 11:34:30 +01:00
abdelrahman 943f00345c Reformat 2026-07-11 10:49:24 +01:00
abdelrahman c8680f06c2 Fixes 2026-07-11 02:52:35 +01:00
abdelrahman 1c7a7f6c46 Fix leaky abstractions 2026-07-11 01:43:34 +01:00
abdelrahman 7bd1d9f701 Save session log 2026-07-06 00:10:01 +01:00
abdelrahman 49aba1eb3c Initial RHI implementation 2026-07-06 00:07:01 +01:00
abdelrahman cb3ef2be1c Update .gitignore 2026-07-06 00:06:48 +01:00
abdelrahman 5a26bf54c8 Implement vulkan backend 2026-07-05 22:11:05 +01:00
abdelrahman a073dec6a0 Fix include 2026-07-05 20:56:02 +01:00
abdelrahman 7819f2abc3 Update wapp to 2.2.0 2026-07-05 20:53:16 +01:00
abdelrahman 3e1e09c974 Update .gitignore 2026-07-05 20:17:14 +01:00
104 changed files with 7059 additions and 231 deletions
+3
View File
@@ -1,5 +1,8 @@
build
compile_commands.json
.vscode .vscode
*.dSYM *.dSYM
assets/shaders
scratchpad/** scratchpad/**
!scratchpad/**/ !scratchpad/**/
!scratchpad/**/*.h !scratchpad/**/*.h
+3
View File
@@ -0,0 +1,3 @@
[submodule "src/vendor/ktx"]
path = src/vendor/ktx
url = https://github.com/KhronosGroup/KTX-Software
+92 -4
View File
@@ -16,6 +16,21 @@ Use this when working on any file in `src/prism/rhi/`, or when creating a new ba
## Conventions ## Conventions
### Global context
RHI functions do **not** take allocator parameters. A global `PrRhiContext` provides two allocators:
- `allocator` — for user-facing objects (buffers, textures, pipelines, etc.)
- `tmp` — for short-lived internal temporaries
```c
extern PrRhiContext _G_RHI_CONTEXT;
void prRhiInit(void); // sets up both allocators
void prRhiDestroy(void); // tears down context
```
All RHI functions access `_G_RHI_CONTEXT` directly. Do not pass allocators to RHI API calls.
### Backend dispatch ### Backend dispatch
Backend selection is compile-time via `-D PR_RHI_VULKAN` / `-D PR_RHI_D3D12` / `-D PR_RHI_METAL`. The umbrella header `pr_rhi.h` includes the appropriate alias file: Backend selection is compile-time via `-D PR_RHI_VULKAN` / `-D PR_RHI_D3D12` / `-D PR_RHI_METAL`. The umbrella header `pr_rhi.h` includes the appropriate alias file:
@@ -49,22 +64,95 @@ All descriptor structs are passed **by value**, not `const *`:
```c ```c
// correct // correct
PrRhiDevice *prRhiCreateDevice(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface, PrRhiDevice *prRhiCreateDevice(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface, PrRhiDeviceDesc desc);
PrRhiDeviceDesc desc, WpAllocator *alloc);
// wrong // wrong
PrRhiDevice *prRhiCreateDevice(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface, PrRhiDevice *prRhiCreateDevice(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface, const PrRhiDeviceDesc *desc);
const PrRhiDeviceDesc *desc, WpAllocator *alloc);
``` ```
### Frame-by-frame command batching
Commands that run every frame must avoid arena allocation. Use stack arrays with a while-loop to batch operations:
```c
// correct — stack array, batched submission
void prRhiCmdBindDescriptorSetsVk(PrRhiCommandBuffer *cb, PrRhiPipelineBindPoint bind_point,
PrRhiPipelineLayout *layout, u32 first_set,
PrRhiDescriptorSetArray sets) {
u32 set_count = sets ? (u32)wpArrayCount(sets) : 0;
while (set_count > 0) {
VkDescriptorSetArray vk_sets = wpArrayWithCapacity(VkDescriptorSet, 16, WP_ARRAY_INIT_FILLED);
u32 total_capacity = (u32)wpArrayCapacity(vk_sets);
u32 real_count = set_count < total_capacity ? set_count : total_capacity;
for (u32 i = 0; i < real_count; ++i) {
vk_sets[i] = sets[i]->handle;
}
vkCmdBindDescriptorSets(cb->handle, vk_bp, vk_layout, first_set, real_count, vk_sets, 0, NULL);
set_count -= real_count;
first_set += real_count;
}
}
// wrong — allocates from arena on every call
void prRhiCmdBindDescriptorSetsBad(PrRhiCommandBuffer *cb, ...) {
VkDescriptorSetArray vk_sets = wpArrayAllocCapacity(VkDescriptorSet, &_G_RHI_CONTEXT.allocator, count, ...);
// ... this leaks every frame
}
```
Apply this pattern to: `prRhiCmdBindDescriptorSets`, `prRhiCmdBindVertexBuffers`, `prRhiCmdCopyBufferToImage`, and any other command that processes user-provided arrays.
### Opaque struct handles — no casts
Handle types in opaque structs are already the correct Vulkan type. Do not cast:
```c
// correct
vk_device = device->handle;
vk_buffer = buffer->handle;
// wrong
vk_device = (VkDevice)device->handle;
vk_buffer = (VkBuffer)buffer->handle;
```
### Vulkan struct initialisation — designated initializers
Always use C99 designated initializers for Vulkan info structs:
```c
// correct
VkBufferCreateInfo buf_info = {
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size = desc.size,
.usage = _toVkBufferUsage(desc.usage),
};
// wrong
VkBufferCreateInfo buf_info = {};
buf_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
buf_info.size = desc.size;
buf_info.usage = _toVkBufferUsage(desc.usage);
```
### API patterns
- **`prRhiCreateCommandPool`**: Takes only `PrRhiDevice *device` (uses `device->queue_family_index` internally)
- **`prRhiFreeCommandBuffers`**: Takes `PrRhiCommandBufferArray buffers` (count derived from `wpArrayCount`)
- **`prRhiAllocateDescriptorSet`**: Takes `WpU32Array variable_descriptor_counts` for variable descriptor support
- **`prRhiCmdBindVertexBuffers`**: Takes `WpU64Array offsets` (count matched to buffers internally)
- **Shader entry points**: Configurable via `vertex_shader_entry_point` / `fragment_shader_entry_point` in pipeline desc (not hardcoded to "main")
### File layout ### File layout
``` ```
src/prism/rhi/ src/prism/rhi/
├── pr_rhi.h ← umbrella header (API declarations + backend dispatch) ├── pr_rhi.h ← umbrella header (API declarations + backend dispatch)
├── pr_rhi.c ← global context definition (prRhiInit, prRhiDestroy)
├── pr_rhi_types.h ← shared types (enums, element types, array aliases, desc structs, opaque handles) ├── pr_rhi_types.h ← shared types (enums, element types, array aliases, desc structs, opaque handles)
├── vulkan/ ├── vulkan/
│ ├── pr_rhi_vk.h ← Vulkan backend header (opaque struct defs + Vk-suffixed decls) │ ├── pr_rhi_vk.h ← Vulkan backend header (opaque struct defs + Vk-suffixed decls)
│ ├── pr_rhi_vk.c ← Vulkan backend implementation
│ ├── pr_rhi_vk_aliases.h ← #define alias mapping │ ├── pr_rhi_vk_aliases.h ← #define alias mapping
│ └── profiles/ ← generated Vulkan Profiles library │ └── profiles/ ← generated Vulkan Profiles library
├── d3d12/ ├── d3d12/
+38 -1
View File
@@ -36,6 +36,16 @@ All code follows the patterns established in `src/wapp/`. The project prefix is
- **Tabs for indentation**, 8-column tab width. - **Tabs for indentation**, 8-column tab width.
- **Braces** on the same line as control statements (Attach style). - **Braces** on the same line as control statements (Attach style).
- **Braces on single-line if statements**: Always use braces, even for single-line bodies:
```c
// correct
if (!buffer) { return; }
if (!texture) { _abort("alloc failed"); }
// wrong
if (!buffer) return;
if (!texture) _abort("alloc failed");
```
- **Pointers**: `*` against the name, not the type (`PrRhiBuffer *buf`, not `PrRhiBuffer* buf`). - **Pointers**: `*` against the name, not the type (`PrRhiBuffer *buf`, not `PrRhiBuffer* buf`).
- **Line width**: 120 columns. - **Line width**: 120 columns.
- **Continuation lines** align to the opening parenthesis. - **Continuation lines** align to the opening parenthesis.
@@ -84,6 +94,12 @@ void prNodeDestroy(PrNode *n, PrAllocator *alloc);
Use wapp allocators (`WpAllocator`, arena-based). Stack-allocate where Use wapp allocators (`WpAllocator`, arena-based). Stack-allocate where
possible; pass allocators explicitly. possible; pass allocators explicitly.
**Never use libc for memory or file I/O.** wapp always takes precedence:
- `wpMemAllocatorAlloc` / `wpMemAllocatorFree` instead of `malloc` / `free`
- `wpFileOpen` / `wpFileRead` / `wpFileClose` instead of `fopen` / `fread` / `fclose`
For one-shot loads (e.g. SPIR-V at init), use `&_G_RHI_CONTEXT.allocator`.
```c ```c
PrGraph *prGraphCreate(PrAllocator *alloc); PrGraph *prGraphCreate(PrAllocator *alloc);
void prGraphDestroy(PrGraph *g, PrAllocator *alloc); void prGraphDestroy(PrGraph *g, PrAllocator *alloc);
@@ -117,6 +133,24 @@ prefer SoA layouts, batch processing, and minimise pointer chasing. Keep
the DAG in contiguous arrays (e.g. adjacency lists packed in flat buffers) the DAG in contiguous arrays (e.g. adjacency lists packed in flat buffers)
rather than individually allocated linked structures. rather than individually allocated linked structures.
### Frame-by-frame command batching
Commands that execute every frame must avoid arena allocation. Use stack
arrays with a while-loop to batch operations in fixed-size chunks:
```c
while (count > 0) {
VkTypeArray batch = wpArrayWithCapacity(VkType, 16, WP_ARRAY_INIT_FILLED);
u32 batch_size = count < wpArrayCapacity(batch) ? count : (u32)wpArrayCapacity(batch);
// ... process batch ...
vkCmd*(cb->handle, ...);
count -= batch_size;
first += batch_size;
}
```
This avoids per-frame arena churn while handling arbitrarily large inputs.
### Graph / adjacency lists ### Graph / adjacency lists
Adjacency list nodes must be **separately allocated from the vertex array**. Adjacency list nodes must be **separately allocated from the vertex array**.
@@ -200,7 +234,7 @@ WpAllocator scratch = wpMemArenaAllocatorInitZero(KiB(16));
## Documentation ## Documentation
Save research notes and implementation plans as markdown in `documents/`: Save research notes, implementation plans and session logs as markdown in `documents/`:
``` ```
documents/ documents/
@@ -214,6 +248,9 @@ documents/
└── YYYY-MM-DD.md └── 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 ## Skills
Domain-specific conventions are stored as skills in `.opencode/skills/<name>/SKILL.md`. 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);
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+554
View File
@@ -0,0 +1,554 @@
# Plan: Texture Pool + Node Evaluation
## Goal
Design the texture pool and node-to-shader dispatch so the node DAG doubles as the
frame graph. Each node type maps to a single Slang shader (no fusion). The texture
pool enables concurrent branches by allowing multiple intermediate textures to
coexist.
---
## 1. Pool Allocator
### 1.1 Purpose
A reusable pool allocator for fixed-size blocks. This replaces the ad-hoc
`PrPool` in scratchpad/dag.c and can be used for any fixed-size allocation
throughout the project: node structs, edge structs, texture slots, descriptor
sets, etc.
Lives in `src/prism/allocators/`, **not** in wapp. wapp is vendored and may be
replaced — the pool allocator must not be part of it.
The pool owns its memory. No external allocator is passed — the pool allocates
blocks internally via wapp OS allocation and grows on demand when free slots
run out.
### 1.2 Design
The pool manages fixed-size slots arranged in contiguous blocks. Free slots are
tracked via an intrusive free list (first `sizeof(void*)` bytes of each free
slot hold a pointer to the next free slot). When the free list is empty, the
pool allocates a new block of `block_slots` slots and carves them into the
free list.
```c
typedef struct PrPool PrPool;
struct PrPool {
void **blocks; // array of allocated block pointers (for destroy)
u64 block_count; // number of allocated blocks
u64 block_cap; // capacity of blocks array
void *free_list; // intrusive free list head
u64 slot_size; // user-requested slot size
u64 alloc_size; // actual slot size used internally (>= slot_size, >= sizeof(void*))
u64 block_slots; // slots per block
u64 total; // total slots ever allocated (diagnostics)
u64 active; // currently in use (diagnostics)
};
```
### 1.3 API
```c
// Initialise a pool.
// slot_size: fixed size of each slot
// initial_slots: starting capacity in slots (also used as block size)
void prPoolInit(PrPool *pool, u64 slot_size, u64 initial_slots);
// Allocate one slot. Grows by a new block if the free list is empty.
// Returns NULL only on allocation failure.
void *prPoolAlloc(PrPool *pool);
// Return a slot to the pool's free list. Safe no-op on NULL.
void prPoolFree(PrPool *pool, void *slot);
// Free all blocks and zero the pool.
void prPoolDestroy(PrPool *pool);
// Diagnostics
u64 prPoolTotalSlots(const PrPool *pool);
u64 prPoolActiveSlots(const PrPool *pool);
```
### 1.4 Behavior
| Operation | Implementation |
|-----------|---------------|
| `prPoolAlloc` | Pop from free list if non-empty, otherwise allocate a new block of `block_slots` slots via wapp OS allocation, link it into the `blocks` array, carve it into the free list, and pop. |
| `prPoolFree` | Push slot onto the intrusive free list. Safe no-op on NULL. |
| `prPoolDestroy` | Free every block in the `blocks` array, free the array itself, zero the struct. |
Block growth: each new block has `block_slots` slots (same size as the initial
block). The minimum block size is 4096 bytes — if `slot_size * initial_slots`
is smaller, `block_slots` is rounded up to the nearest multiple of `slot_size`
that meets the minimum. The `blocks` array starts at capacity 4 and doubles
when full.
### 1.5 Usage examples
```c
// Edge pool (replaces PrPool in scratchpad/dag.c):
PrPool edge_pool;
prPoolInit(&edge_pool, sizeof(PrGraphEdge), 64);
PrGraphEdge *edge = prPoolAlloc(&edge_pool);
prPoolFree(&edge_pool, edge);
prPoolDestroy(&edge_pool);
// Texture slot pool:
PrPool tex_pool;
prPoolInit(&tex_pool, sizeof(PrTextureSlot), 16);
PrTextureSlot *slot = prPoolAlloc(&tex_pool);
prPoolDestroy(&tex_pool);
```
---
## 2. Texture Pool
### 2.1 Purpose
Intermediate textures (node outputs) need GPU resources. The texture pool manages
a set of textures that are reused across graph evaluations. Without a pool, a
linear chain of N nodes would need N textures. With refcount-based reuse,
textures are returned to the pool as soon as all their consumers have executed,
keeping the peak live count low.
### 2.2 Data structures
```c
typedef struct PrTextureSlot {
PrRhiTexture *texture; // the GPU texture (SAMPLED | COLOR_ATTACHMENT)
u32 refcount; // how many downstream nodes still need to read this
b8 in_use; // currently assigned to a node's output
} PrTextureSlot;
typedef struct PrTexturePool {
PrPool slot_pool; // pool allocator for PrTextureSlot structs
PrTextureSlot *slots; // flat array for iteration (backed by slot_pool)
u32 count; // number of allocated slots
u32 max; // hard cap (never allocate beyond this)
u32 width; // texture width (matches window)
u32 height; // texture height (matches window)
} PrTexturePool;
```
All pool textures are **RGBA16F, SAMPLED | COLOR_ATTACHMENT**. Any free slot works
for any node — no format/dimension matching needed.
The `slot_pool` is a `PrPool` allocator for `PrTextureSlot` structs. The `slots`
pointer provides flat-array access for iteration during evaluation. When the pool
grows, a new batch of slots is allocated via the pool allocator and the flat
array is extended.
### 2.3 Lifecycle
```
prTexturePoolInit(pool, device, initial_capacity, max, width, height)
→ creates pool allocator, allocates initial slot array
prTexturePoolReset(pool)
→ marks all slots as free, zeroes refcounts (called once per frame)
prTexturePoolAcquire(pool, device) -> PrTextureSlot*
→ returns a free slot (in_use = true)
→ if no free slot: allocate new slot + GPU texture, grow array
→ if max reached: abort with diagnostic message
prTexturePoolRelease(pool, slot)
→ marks slot as free (in_use = false)
→ called when refcount hits 0
prTexturePoolDestroy(pool, device)
→ destroys all GPU textures, destroys pool allocator
```
### 2.4 Allocation strategy (growth)
The pool does **not** pre-allocate all textures upfront. Instead:
1. Start with `initial_capacity` textures (e.g., 16)
2. When all slots are occupied and a new one is needed, allocate a batch of
`GROWTH_BATCH` (e.g., 8) additional textures
3. Never exceed `max` (e.g., 128)
4. If `max` is reached, abort with: `"texture pool exhausted: N in use, max M"`
Growth is amortized (batch allocation) and the pool never shrinks. The `count`
monotonically increases as textures are allocated on demand.
**Why growth instead of fixed pre-allocation:**
- Small graphs don't pay for 64 unused textures
- Complex graphs can grow beyond the initial allocation
- The hard cap prevents unbounded memory use
- vkCreateImage is only called when actually needed
### 2.5 Refcount management
Before evaluation, compute the **initial refcount** for each node's output:
```
refcount[node] = out_degree(node) // number of outgoing edges
```
During evaluation, when a node executes and reads an input texture:
```
input_slot->refcount -= 1
if (input_slot->refcount == 0):
prTexturePoolRelease(pool, input_slot)
```
This naturally handles:
- **Linear chains**: A→B→C. A's output refcount=1, freed after B executes.
- **Fan-out**: A→B, A→C. A's output refcount=2, freed after both B and C execute.
- **Fan-in**: B→D, C→D. B and C have independent refcounts, freed independently.
### 2.6 Texture dimensions
Pool textures are created at the **window/swapchain resolution**. All nodes
operate at this resolution. If a node needs a different resolution (e.g., a
half-resolution blur), it would need a separate mechanism — out of scope for V1.
---
## 3. Node-to-Shader Mapping
### 3.1 Type registry
A static table maps `PrNodeType` → shader modules + pipeline + resource
signatures:
```c
typedef enum PrShaderType {
PR_SHADER_TYPE_FRAGMENT, // fullscreen triangle, per-pixel
PR_SHADER_TYPE_COMPUTE, // dispatch, shared memory
} PrShaderType;
typedef struct PrNodeTypeEntry {
PrNodeType type;
PrShaderType shader_type;
// shaders (pre-compiled SPIR-V, built from .slang via slangc)
const char *vertex_shader_path; // NULL for compute
const char *fragment_shader_path; // NULL for compute
const char *compute_shader_path; // NULL for fragment
// pipeline (created at init, cached here)
PrRhiPipeline *pipeline;
// resource signature
u32 input_count; // number of texture inputs (1 for blur, 2 for blend)
u32 output_count; // always 1 for V1
// descriptor set layout (created at init)
PrRhiDescriptorSetLayout *set_layout;
// push constant size (bytes)
u32 push_constant_size;
} PrNodeTypeEntry;
```
### 3.2 Registry instance
```c
wp_persist PrNodeTypeEntry _node_type_table[COUNT_NODE_TYPES] = {
[PR_NODE_TYPE_READ] = {
.type = PR_NODE_TYPE_READ,
.shader_type = PR_SHADER_TYPE_FRAGMENT,
.vertex_shader_path = "assets/shaders/blit.vert.spv",
.fragment_shader_path= "assets/shaders/read.frag.spv",
.input_count = 0,
.output_count = 1,
.push_constant_size = 0,
},
[PR_NODE_TYPE_BLUR] = {
.type = PR_NODE_TYPE_BLUR,
.shader_type = PR_SHADER_TYPE_FRAGMENT,
.vertex_shader_path = "assets/shaders/blit.vert.spv",
.fragment_shader_path= "assets/shaders/blur.frag.spv",
.input_count = 1,
.output_count = 1,
.push_constant_size = sizeof(PrBlurPushConstants),
},
[PR_NODE_TYPE_GRADE] = {
.type = PR_NODE_TYPE_GRADE,
.shader_type = PR_SHADER_TYPE_FRAGMENT,
.vertex_shader_path = "assets/shaders/blit.vert.spv",
.fragment_shader_path= "assets/shaders/grade.frag.spv",
.input_count = 1,
.output_count = 1,
.push_constant_size = sizeof(PrGradePushConstants),
},
[PR_NODE_TYPE_BLEND] = {
.type = PR_NODE_TYPE_BLEND,
.shader_type = PR_SHADER_TYPE_FRAGMENT,
.vertex_shader_path = "assets/shaders/blit.vert.spv",
.fragment_shader_path= "assets/shaders/blend.frag.spv",
.input_count = 2,
.output_count = 1,
.push_constant_size = sizeof(PrBlendPushConstants),
},
};
```
### 3.3 Shader loading
Shaders are written in Slang (`src/shaders/*.slang`) and compiled to SPIR-V as
a build step via `slangc`. The `.spv` files are output to `assets/shaders/`. At
init, the application loads pre-compiled SPIR-V directly:
```
for each entry in _node_type_table:
load vertex shader SPIR-V from .spv file
load fragment/compute shader SPIR-V from .spv file
create PrRhiShader handles
create descriptor set layout (input_count combined image samplers)
create pipeline layout (set layout + push constant range)
create pipeline (vertex + fragment stages, dynamic rendering)
cache everything in the entry
```
### 3.4 Shaders per node type
| Node | Shader | Inputs | Push constants |
|------|--------|--------|----------------|
| READ | `read.frag.spv` | 0 (samples from KTX texture loaded separately) | — |
| BLUR | `blur.frag.spv` | 1 input texture | `f32 radius` |
| GRADE | `grade.frag.spv` | 1 input texture | `f32 gain, f32 lift, f32 gamma` |
| BLEND | `blend.frag.spv` | 2 input textures | `u32 mode` (over/under/add) |
All share `blit.vert.spv` (fullscreen triangle, no vertex buffer needed).
---
## 4. Evaluation Loop
### 4.1 Per-frame sequence
```
prGraphEvaluate(graph, device, pool, cb, swapchain_texture):
1. topo_order = prGraphTopologicalSort(graph)
2. // compute initial refcounts
for each node in graph:
node.output_refcount = out_degree(node)
3. prTexturePoolReset(pool)
4. // reset per-frame descriptor pool (allocated once at init, reset each frame)
prRhiResetDescriptorPool(device, desc_pool)
5. for each node_id in topo_order:
node = &nodes[node_id]
entry = &_node_type_table[node->type]
// acquire output texture from pool
output_slot = prTexturePoolAcquire(pool, device)
// gather input textures (from upstream nodes' output slots)
input_count = 0
input_slots[4] // max 4 inputs
for each upstream edge (upstream → node):
input_slots[input_count++] = upstream.output_slot
// allocate and update descriptor set
desc_set = prRhiAllocateDescriptorSet(device, desc_pool, entry->set_layout)
writes = stack_array(input_count)
for i in 0..input_count:
writes[i] = {
.dst_set = desc_set,
.dst_binding = i,
.dst_array_element = 0,
.type = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
.image_info = &(PrRhiDescriptorImageInfo){
.texture = input_slots[i]->texture,
.sampler = shared_sampler,
.layout = PR_RHI_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
},
}
prRhiUpdateDescriptorSet(device, writes)
// record commands
prRhiCmdBeginRendering(cb, output_slot->texture, ...)
prRhiCmdBindPipeline(cb, GRAPHICS, entry->pipeline)
prRhiCmdBindDescriptorSets(cb, GRAPHICS, entry->pipeline_layout, 0, 1, &desc_set, 0, NULL)
prRhiCmdPushConstants(cb, ..., node->params)
prRhiCmdDraw(cb, 3, 1, 0, 0) // fullscreen triangle
prRhiCmdEndRendering(cb)
// release input textures whose refcount hit 0
for each input_slot:
input_slot->refcount -= 1
if input_slot->refcount == 0:
prTexturePoolRelease(pool, input_slot)
// store output slot on node for downstream consumers
node->output_slot = output_slot
6. // final blit to swapchain
final_slot = last_node.output_slot
blit final_slot->texture → swapchain_texture
7. prRhiQueueSubmit(cb)
```
### 4.2 READ node special case
READ nodes load a texture from disk (KTX) via `prRhiCreateTextureFromKtx`.
The loaded texture is stored directly on the node (persistent, lives across
frames). Unlike other nodes, READ's input comes from this persistent texture
rather than from an upstream node's output slot.
READ nodes still render a fullscreen triangle that samples from the loaded
texture and writes to the output pool texture. This allows the user to view
the raw texture before any modifications, and ensures READ nodes participate
uniformly in the evaluation pipeline.
READ nodes participate in refcount tracking like any other node: their output
slot's refcount is set to `out_degree(READ)`, and downstream consumers
decrement it normally.
### 4.3 Barrier insertion
Between nodes that share a texture (one writes, next reads), a pipeline barrier
is needed to transition the texture layout:
```
after node A executes (writes to texture T):
barrier: T from COLOR_ATTACHMENT → SHADER_READ_ONLY
before node B executes (reads texture T):
(barrier already inserted above)
```
In practice, the barrier is inserted **after** each node's render pass:
- Transition the output texture from `COLOR_ATTACHMENT_OPTIMAL` to
`SHADER_READ_ONLY_OPTIMAL`
The **first** node in a chain (READ) needs a transition from `TRANSFER_DST` to
`SHADER_READ_ONLY` after loading from disk. This is already handled by
`prRhiCreateTextureFromKtx`.
Layout transitions per node:
```
READ: UNDEFINED → TRANSFER_DST → SHADER_READ_ONLY (done by KTX loader)
BLUR: SHADER_READ_ONLY (input) → COLOR_ATTACHMENT (output, during render)
output transitions to SHADER_READ_ONLY after render pass
GRADE: same as BLUR
BLEND: same as BLUR (two inputs)
```
### 4.4 Descriptor management
Each node needs a descriptor set binding its input textures. The flow:
**Init (once):**
- Create a **per-node-type descriptor set layout** with `input_count` combined
image sampler bindings. Stored in `PrNodeTypeEntry.set_layout`.
- Create a **persistent descriptor pool** large enough for the worst-case node
count (e.g., 128 sets). Created once, reused every frame.
**Per frame:**
1. Reset the descriptor pool via `prRhiResetDescriptorPool`. This is much
cheaper than create/destroy — it reuses the pool's internal memory.
2. For each node during evaluation:
- Allocate a descriptor set from the pool using the node type's layout.
- Write each input texture into the set via `prRhiUpdateDescriptorSet`.
Each write specifies:
- `dst_set` / `dst_binding` — which set and binding index
- `type``PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER`
- `image_info` — texture handle, shared sampler, layout
- Bind the set during rendering via `prRhiCmdBindDescriptorSets`.
The pool lives for the lifetime of the application. Only its contents are
reset each frame.
---
## 5. File layout
```
src/prism/allocators/
└── pr_pool_allocator.h / .c ← pool allocator (self-managing, wapp OS allocation)
src/prism/core/
├── pr_graph.h / .c ← promoted from scratchpad/dag.c
├── pr_node.h / .c ← PrNode, PrNodeType, PrNodeManager
├── pr_texture_pool.h / .c ← PrTexturePool
└── pr_node_eval.h / .c ← evaluation loop, type registry
src/shaders/ ← Slang source (compiled to assets/shaders/ via slangc)
├── blit.vert.slang ← fullscreen triangle (shared by all fragment nodes)
├── read.frag.slang ← passthrough (samples loaded texture)
├── blur.frag.slang ← gaussian blur
├── grade.frag.slang ← colour grading
└── blend.frag.slang ← alpha compositing
assets/shaders/ ← compiled SPIR-V output (loaded at runtime)
├── blit.vert.spv
├── read.frag.spv
├── blur.frag.spv
├── grade.frag.spv
└── blend.frag.spv
```
---
## 6. Implementation order
1. **Pool allocator**: Implement `PrPool` in `src/prism/allocators/`.
`prPoolInit`, `prPoolAlloc`, `prPoolFree`, `prPoolDestroy`. Self-managing
growth via wapp OS allocation. Replace the ad-hoc `PrPool` in scratchpad/dag.c.
2. **Promote graph to production**: Move `PrGraph`, `PrNodeManager`, topology
ops from `scratchpad/dag.c` to `src/prism/core/pr_graph.h/.c` and
`pr_node.h/.c`. Clean up — remove the `main()` test harness.
3. **Define node type registry**: Create `PrNodeTypeEntry` table with resource
signatures (input_count, output_count, push_constant_size). No shaders yet.
4. **Implement PrTexturePool**: Growth-based pool with refcount tracking.
`prTexturePoolInit`, `prTexturePoolReset`, `prTexturePoolAcquire`,
`prTexturePoolRelease`, `prTexturePoolDestroy`.
5. **Write blit.vert.slang**: Fullscreen triangle, no vertex buffer. Shared by
all fragment-shader nodes. Compile to SPIR-V via `slangc`.
6. **Write initial frag shaders**: `read.frag.slang`, `blur.frag.slang`,
`grade.frag.slang`, `blend.frag.slang`. Simple per-pixel operations.
Compile to SPIR-V via `slangc`.
7. **Wire up shader loading + pipeline creation**: At init, load pre-compiled
SPIR-V from `assets/shaders/`, create descriptor set layouts, pipeline
layouts, pipelines. Cache in the type registry.
8. **Implement evaluation loop**: `prGraphEvaluate` — topo sort, refcount
compute, pool reset, per-node dispatch, barrier insertion, final blit to
swapchain.
9. **Integrate with main loop**: Replace the current mesh-rendering demo with
a node graph evaluation. Create a test graph (Read→Blur→Blend) and render
it to the swapchain.
---
## 7. Decisions
- **Pool allocator**: Self-contained `PrPool` with standalone API. No external
allocator parameter — pool allocates blocks via wapp OS allocation (`wpOsMemAlloc`
/ `wpOsMemFree`) and grows on demand. Handles slot sizes smaller than
`sizeof(void*)` transparently via an internal `alloc_size`. Lives in
`src/prism/allocators/`, outside vendored wapp.
- **READ node texture lifetime**: READ nodes hold a persistent `PrRhiTexture`
(loaded via `prRhiCreateTextureFromKtx`) outside the pool. The pool slot's
`texture` pointer references this persistent texture. This means READ nodes
don't consume pool slots — they just participate in refcount tracking.
- **Sampler**: Single shared sampler (linear filtering, clamp-to-edge) for all
nodes in V1. Created once at init.
- **Push constant layout**: Each node type defines its own push constant struct.
The evaluation loop reads the node's params union and passes it via
`prRhiCmdPushConstants`. The shader declares matching layout.
+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.
@@ -0,0 +1,346 @@
# Shader Architecture Patterns for Node-Based Image Compositing
Research conducted 2026-07-13.
---
## 1. Single Shader vs Multiple Shaders
### How Professional Compositors Handle It
**Blender Compositor (GPU backend)** — The most relevant case study:
- Blender's GPU compositor collapses multiple connected nodes into a **"compile unit"** and generates a **single compute shader** per unit.
- The `ShaderOperation` class iterates through a `compile_unit_` (a set of nodes) and links their GLSL logic into one shader: `source/blender/compositor/intern/shader_operation.cc:122-135`.
- Simple per-pixel operations (Math, Color Mix, Invert, etc.) are fused into a single pass. Operations that can't be expressed as shaders fall back to `MultiFunctionProcedureOperation` on CPU.
- **Key insight**: Blender uses a **hybrid approach** — fuse what you can into single shaders, fall back to separate passes for complex operations (blur, glare, convolution).
**Natron** — CPU-based compositor using OpenFX plugins:
- Each node is a separate processing unit (separate plugin call).
- Multi-threaded tile-based processing per node.
- Not GPU-accelerated; no shader fusion.
**DaVinci Resolve / Fusion** — Proprietary:
- Uses a node graph where each node can have internal multi-pass processing.
- Fusion's "Flow Region" system groups nodes for optimization.
- Effectively separate shaders per node, with internal optimization.
### Recommended Approach for Prism
**Use separate shaders per node, with optional fusion of simple nodes.** Rationale:
- Nodes in a compositing graph have diverse operations (blur vs. blend vs. color grade). An uber-shader would have massive register pressure and poor occupancy.
- Simple per-pixel operations (math, color mix, gamma) can be fused into chains as an optimization.
- Complex operations (blur, convolutions, warps) need their own shader passes anyway.
---
## 2. Texture Ping-Ponging
### The Pattern
Texture ping-ponging is the fundamental technique for chaining GPU image operations:
1. Allocate two textures (A and B) at the target resolution.
2. Bind texture A as input, render to texture B.
3. Swap: bind texture B as input, render to texture A.
4. Repeat for as many passes as needed.
```
Pass 1: Read(A) → Write(B) [e.g., blur]
Pass 2: Read(B) → Write(A) [e.g., color grade]
Pass 3: Read(A) → Write(B) [e.g., blend]
Final: Display(B)
```
### How It Works in Practice
**WebGL/Fragment Shader approach** (from multiple sources):
- Create Framebuffer Objects (FBOs) with texture attachments.
- Bind FBO → render fullscreen quad → output goes to texture.
- Bind different FBO or default framebuffer → read from that texture.
**Vulkan approach**:
- Use `VkImage` objects as both sampler inputs and render targets.
- Between passes, issue a pipeline barrier (`VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT → VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT`).
- Manage image layouts: `SHADER_READ_ONLY_OPTIMAL``COLOR_ATTACHMENT_OPTIMAL``SHADER_READ_ONLY_OPTIMAL`.
**Metal approach** (from Kosikowski's article):
- Compute shaders read from `inTexture` and write to `outTexture`.
- Swap the texture references between passes.
### Important Considerations
- **Image layout transitions** are critical in Vulkan. Each pass requires the texture to be in the correct layout.
- **Load/store ops**: For intermediate textures, use `VK_ATTACHMENT_LOAD_OP_DONT_CARE` and `VK_ATTACHMENT_STORE_OP_DONT_CARE` when contents aren't needed — saves bandwidth.
- **Resolution management**: Different nodes may operate at different resolutions. The compositor must manage a texture pool and handle up/downsampling.
- **On tile-based GPUs (mobile)**: Multiple passes that write/read intermediate textures to external memory is expensive. Use Vulkan subpasses or `VK_KHR_dynamic_rendering_local_read` to keep data on-chip.
---
## 3. Shader Composition Strategies
### 3a. Runtime Shader Generation
**Blender's approach** (most relevant):
- The compositor has a `gpu_shader_compositor_code_generation.glsl` library.
- `ShaderOperation` generates GLSL code by iterating through a compile unit's nodes and concatenating their shader code contributions.
- The generated code is compiled via Blender's `GPUMaterial` system.
- Node settings are passed as UBOs; images are bound as `image2D`/`sampler2D`.
**Godot's compositor approach**:
- Uses a **template + injection** pattern:
```
const template_shader = """
#version 450
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
layout(rgba16f, set = 0, binding = 0) uniform image2D color_image;
void main() {
// ... boilerplate ...
vec4 color = imageLoad(color_image, uv);
#COMPUTE_CODE
imageStore(color_image, uv, color);
}
"""
```
- User shader code replaces `#COMPUTE_CODE` at runtime.
- Compiled via `rd.shader_create_from_spirv()` at runtime.
**OGRE's RTSS (Run Time Shader System)**:
- Not an uber-shader. Manages a set of opaque `SubRenderState` components.
- Each component implements a specific effect.
- Components are composed and code-generated at runtime.
- Avoids the "exploding `#ifdef`" problem of uber-shaders.
### 3b. Shader Permutations vs Branching
**The permutation problem** (from MJP's detailed analysis):
- Each feature combination = separate compiled shader.
- Exponential growth: N binary features = 2^N permutations.
- Costs: compilation time, memory, PSO creation, binding overhead, instruction cache pressure.
- **Register pressure**: Uber-shaders with many features need more registers, reducing occupancy even for materials that don't use all features.
**Branching rules for GPUs**:
- **Uniform branches** (same path for all pixels in a warp): Essentially free. The driver compiles both paths and selects one.
- **Divergent branches** (different paths within a warp): Both paths execute serially, wasting cycles.
- **Branches on uniforms/constant data**: OK and performant.
- **Branches based on per-pixel data**: Expensive when pixels in the same warp diverge.
**Best practice**: Use **Vulkan specialization constants** for compile-time branching (uber-shader with static branching). This gives you permutation-like performance with fewer actual shader binaries. The driver can optimize away dead code paths.
### 3c. Compute Shaders vs Fragment Shaders
**Fragment shaders are generally faster for simple image processing:**
- Fragment shaders benefit from hardware texture prefetch and caching optimized for 2D spatial locality.
- For simple per-pixel operations (passthrough, basic color transforms): fragment shaders ~30% faster than compute (Leadwerks benchmarks: 770 FPS vs 600 FPS).
- For multi-pass chained operations: fragment shaders maintain advantage (670 FPS vs 180 FPS at 10 passes).
**Compute shaders are better when:**
- You need **shared memory** access within workgroups (e.g., local convolution, shared reductions).
- You need **read-write access** to the same texture (e.g., iterative algorithms like Jump Flood).
- You're doing operations that aren't naturally per-pixel (histogram, reduction, sorting).
- You want explicit control over workgroup dispatch.
**On tile-based GPUs (mobile)**: Arm documentation explicitly warns: "Compute shaders can be slower and less energy-efficient than fragment shaders for simple post-processing workloads."
**For compositing**: Use fragment shaders for per-pixel operations (blend, color grade, transform). Use compute for multi-pass algorithms that need shared memory (blur separable passes, glare FFT, flood fill).
### 3d. Bindless Textures and Descriptor Arrays
**The concept**: Instead of binding one texture per descriptor set, bind a large array of descriptors once. Access textures by integer index in shaders.
**Vulkan implementation** (from `VK_EXT_descriptor_indexing`, core since Vulkan 1.2):
```glsl
// GLSL
#extension GL_EXT_nonuniform_qualifier : enable
layout(set = 1, binding = 10) uniform sampler2D textures[];
vec4 color = texture(textures[albedo_id], uv);
```
**Key features**:
- `VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT`: Update descriptors after binding.
- `VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT`: Not all slots need valid descriptors.
- `NonUniformResourceIndex`: For divergent indexing within a warp.
**For a compositor**: Bindless is extremely useful. All input textures from the graph can live in one descriptor set. Each node shader indexes into the set by texture ID. This avoids re-binding descriptor sets per node.
**Trade-off**: Indirect memory loads can be slower on some mobile GPUs. Desktop GPUs handle this well.
---
## 4. Slang-Specific Patterns
### Overview
Slang is a Khronos-hosted, open-source shading language. HLSL-like syntax with modern features:
- Targets: SPIR-V (Vulkan), DXIL (D3D12), Metal, CUDA, WGSL, CPU.
- Hosted by Khronos with broad industry governance.
- Based on years of NVIDIA/CMU/Stanford/MIT research.
### Key Features Relevant to Compositing
**Modules**: Slang supports `module` and `import` for separate compilation. Modules compile to a custom IR and can be linked at runtime to produce SPIR-V or DXIL. This is **exactly what a node compositor needs** — each node type can be a module, and compositions are linked at runtime.
**Generics and Interfaces**: Instead of #ifdef permutations, use generics:
```slang
interface IImageOp {
float4 evaluate(float4 input, PixelContext ctx);
}
struct BlendOp : IImageOp {
float4 evaluate(float4 input, PixelContext ctx) { ... }
}
// Generic function specialized at compile time
T evaluateGraph<T : IImageOp>(T op, float4 input) {
return op.evaluate(input, ctx);
}
```
**Runtime code generation**: Slang supports **runtime compilation and linking**. From the docs: "Slang modules can be independently compiled offline to a custom IR and then linked at runtime to generate code in formats such as DXIL or SPIR-V." This means you can:
1. Compile each node's shader as a Slang module.
2. At graph edit time, link modules together.
3. Generate the final SPIR-V/DXIL for the composed graph.
**Reflection API**: `TypeReflection`, `VariableReflection`, `getLayout()` allow querying shader structure at runtime — useful for automatically creating descriptor layouts.
**Automatic Differentiation**: `fwd_diff` and `bwd_diff` for gradient-based operations (relevant for differentiable compositing or learned operations).
### Slang vs GLSL/HLSL for Compositing
| Feature | Slang | GLSL | HLSL |
|---------|-------|------|------|
| Separate compilation | ✅ Modules | ❌ Single TU | ⚠️ Limited |
| Runtime linking | ✅ | ❌ | ❌ |
| Generics/interfaces | ✅ | ❌ | ⚠️ Templates (limited) |
| Cross-platform | ✅ (Vulkan/Metal/DX/CUDA) | ⚠️ (OpenGL/Vulkan) | ⚠️ (DX only) |
| Vulkan SPIR-V | ✅ First-class | ✅ via glslc | ⚠️ via dxc |
| Runtime compilation | ✅ | ❌ | ❌ |
| HLSL compatibility | ✅ Most HLSL compiles out-of-box | ❌ | ✅ |
### Recommendation
**Slang is the ideal choice for a Vulkan-based compositor.** Its module system directly solves the "runtime shader composition" problem. Each node type = a Slang module. Graph composition = module linking. No need for runtime string-based code generation.
---
## 5. Vulkan-Specific Considerations
### Multi-Pass Image Processing
**Render Pass approach** (traditional):
```c
// Pass 1: Blur
VkRenderPassBeginInfo rp1 = { .renderPass = blurPass, .framebuffer = blurFBO };
vkCmdBeginRenderPass(cmd, &rp1, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, blurPipeline);
vkCmdDraw(cmd, 4, 1, 0, 0); // fullscreen quad
vkCmdEndRenderPass(cmd);
// Barrier between passes
VkImageMemoryBarrier barrier = {
.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
};
vkCmdPipelineBarrier(cmd, ...);
// Pass 2: Color grade
VkRenderPassBeginInfo rp2 = { .renderPass = gradePass, .framebuffer = gradeFBO };
vkCmdBeginRenderPass(cmd, &rp2, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindDescriptorSets(cmd, ..., gradeDescriptorSet); // binds blur result as texture
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, gradePipeline);
vkCmdDraw(cmd, 4, 1, 0, 0);
vkCmdEndRenderPass(cmd);
```
**Dynamic Rendering approach** (Vulkan 1.3 / `VK_KHR_dynamic_rendering`):
- Skip `VkRenderPass` and `VkFramebuffer` objects entirely.
- Use `vkCmdBeginRendering` with `VkRenderingInfo` specifying attachments directly.
- Simpler API, fewer objects to manage.
### Descriptor Management Best Practices
From ARM and NVIDIA guidelines:
- **Don't allocate descriptor sets on hot paths.** Pre-allocate pools.
- **Use `VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC` / `VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC`** for per-draw offsets instead of new descriptor sets.
- **Pack descriptor bindings** as tightly as possible. No holes.
- **Reuse descriptor sets** — update them rather than reallocating.
- For a compositor with bindless: create ONE large descriptor set with all textures. Bind once, index by ID.
### Pipeline Layout Optimization
- Keep pipeline layouts consistent across similar shaders to reduce pipeline switches.
- Use **push constants** for small, per-pass data (resolution, time, parameters) — cheaper than UBOs for small data.
- Pre-create pipeline cache and use `VkPipelineCache` to speed up PSO creation.
### Synchronization for Multi-Pass
- Use **pipeline barriers** between passes that read/write the same images.
- For independent passes (operating on different textures), no barrier needed — can even record in parallel.
- Use **events** for fine-grained synchronization within a command buffer.
- **Timeline semaphores** (Vulkan 1.2+) for more flexible GPU-GPU synchronization.
### Tile-Based GPU Optimization (Mobile)
- Use **subpasses** to keep intermediate data in tile memory (on-chip).
- `VK_KHR_dynamic_rendering_local_read` allows subpass-like behavior with dynamic rendering.
- Set `loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE` and `storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE` for transient intermediates.
- Merge subpasses when they share attachments (ARM: ≤8 unique attachments).
---
## 6. Industry Best Practices
### The Render Graph Pattern
Modern engines use a **frame graph** / **render graph** (DAG) for multi-pass rendering:
1. **Declare passes** and their resource inputs/outputs.
2. **Analyze dependencies** — build execution order automatically.
3. **Infer synchronization** — barriers are generated from resource usage.
4. **Alias resources** — textures with non-overlapping lifetimes can share memory.
5. **Cull unused passes** — if an output isn't used, skip the pass.
This is the most mature pattern for managing multi-pass image processing. Referenced in:
- Vulkan Tutorial: "Engine Architecture: Rendering Pipeline"
- Cat Game's "Advanced Vulkan Rendering: Building a Modern Frame Graph"
- Frostbite's "FrameGraph" (EA/DICE)
### Fusing Operations
From TFLite GPU and Blender compositor:
- **Fuse element-wise operations** with computationally expensive ones (activations + convolution, color transforms + blend).
- **Inline parameters** directly into shader code instead of passing via uniforms (bakes constants, reduces memory I/O).
- **Bake uniforms into source code** when they don't change per-pixel.
### Texture Pool Management
For a compositor with potentially many intermediate textures:
- Pre-allocate a pool of textures at common resolutions.
- Reference-count or track lifetime of each texture.
- Reuse textures with matching format/resolution once their producer is done.
- On mobile, prefer smaller intermediate formats (RGBA16F over RGBA32F when precision allows).
### Papers and References
1. **"Performance Implications of Node Graph Complexity in Real-Time Compositing"** (IEEE, 2024) — Studies Blender EEVEE's node graph rendering performance vs. structural complexity.
2. **"Compute Shader in Image Processing Development"** (CEUR Workshop, 2020) — Compares CPU, fragment, compute, and Vulkan fragment for image processing. Found compute shader overhead makes it slower for simple operations.
3. **Blender Real-time Compositor** (code.blender.org, 2022) — GPU-accelerated compositor architecture with operation graph, domain system, and shader-based execution.
4. **"The Shader Permutation Problem"** (MJP, 2021) — Comprehensive analysis of uber-shader vs. permutation trade-offs.
5. **"GPU Rendering Pipeline: Blend Modes, Porter-Duff Compositing"** (Lucio Durán, 2025) — Browser rendering pipeline compositing patterns.
6. **"High-Performance Software Rasterization on GPUs"** (NVIDIA Research, 2011) — Software GPU pipeline, relevant for understanding GPU architecture.
7. **Vulkan Samples** (Khronos) — Descriptor management, subpasses, async compute, tile-based rendering best practices.
### Recommended Architecture for Prism
Based on all research:
1. **DAG-based execution**: Topological sort the node graph. Execute in dependency order.
2. **Separate shaders per node type**: Each node type (Blend, ColorGrade, Blur, etc.) has a dedicated Slang shader module.
3. **Runtime composition via Slang modules**: Simple chains of per-pixel operations can be fused into single compute/fragment passes by linking their Slang modules.
4. **Texture pool**: Pre-allocated RGBA16F textures. Reference-counted. Reuse when possible.
5. **Ping-pong for chains**: Two textures alternating for sequential per-pixel chains.
6. **Fragment shaders for per-pixel ops**, compute shaders for operations needing shared memory (blur, convolution, reduction).
7. **Bindless descriptors**: One large descriptor set with all input textures. Node shaders index by texture ID.
8. **Push constants** for per-pass uniforms (resolution, parameters).
9. **Pipeline barriers** between passes on the same texture. No barriers for independent passes.
10. **Render graph** for automatic dependency tracking and synchronization.
File diff suppressed because it is too large Load Diff
+115
View File
@@ -0,0 +1,115 @@
# Plan: RHI Global Context
## Goal
Replace per-call `WpAllocator *allocator` parameters with a global `PrRhiContext`
managed by the RHI. The RHI owns the lifetime of all objects it creates.
## Changes
### 1. `src/prism/rhi/pr_rhi_types.h` — Add context struct
Add after the existing typedefs:
```c
typedef struct PrRhiContext {
WpAllocator main; // objects returned to the user
WpAllocator scratch; // internal temporaries within functions
} PrRhiContext;
```
### 2. `src/prism/rhi/pr_rhi.h` — Public API changes
- Add `wp_extern PrRhiContext _G_RHI_CONTEXT;` declaration (near top, after includes)
- Add `wp_extern b8 prRhiInit(void);` and `wp_extern void prRhiDestroy(void);`
- Remove `WpAllocator *allocator` (and `const WpAllocator *allocator`) from **all** function signatures
### 3. `src/prism/rhi/pr_rhi.c` — New file (shared across backends)
```c
#include "pr_rhi.h"
PrRhiContext _G_RHI_CONTEXT;
b8 prRhiInit(void) {
_G_RHI_CONTEXT.main = wpMemArenaAllocatorInit(MiB(64));
_G_RHI_CONTEXT.scratch = wpMemArenaAllocatorInit(MiB(32));
return true;
}
void prRhiDestroy(void) {
wpMemAllocatorFree(&_G_RHI_CONTEXT.scratch, ...);
wpMemAllocatorFree(&_G_RHI_CONTEXT.main, ...);
}
```
### 4. `src/prism/rhi/vulkan/pr_rhi_vk.h` — Remove allocator from Vk declarations
Remove `WpAllocator *allocator` from all function declarations.
### 5. `src/prism/rhi/vulkan/pr_rhi_vk.c` — Implementation changes
For every function that previously took `WpAllocator *allocator`:
- Remove the parameter from the signature
- Replace `allocator` with `_G_RHI_CONTEXT.main` for:
- `wpMemAllocatorAlloc(allocator, sizeof(...))` for user-facing objects (returned to caller)
- `wpArrayAllocCapacity(...)` for arrays that are stored in user-facing structs
- Replace `allocator` with `_G_RHI_CONTEXT.scratch` for:
- `wpArrayAllocCapacity(...)` for internal temporary arrays
- `wpArrayDealloc(...)` for internal temporaries
- Replace `wpMemAllocatorFree(allocator, ...)` with `wpMemAllocatorFree(&_G_RHI_CONTEXT.main, ...)` for destroy functions
- Update internal calls (e.g. `prRhiCreateBufferVk` called from `prRhiCreateTextureFromKtxVk`)
**Specific internal/helper changes:**
- `_createSwapchainTexture` — remove `allocator` param, use `_G_RHI_CONTEXT.main`
- `prRhiCreateTextureFromKtxVk` — staging buffer uses `_G_RHI_CONTEXT.main` (it's a user-facing object destroyed by the user)
- `prRhiAllocateCommandBuffersVk` — VkCommandBuffer temp array uses scratch, PrRhiCommandBuffer array + structs use main
- `prRhiCreatePipelineLayoutVk` — VkDescriptorSetLayout/VkPushConstantRange overflow arrays use scratch
- `prRhiCreateDeviceVk` — VkQueueFamilyProperties2 array uses scratch
### 6. `src/prism/rhi/vulkan/pr_rhi_vk_aliases.h` — No changes needed
Aliases only map function names, not parameters.
## Allocator usage per function
| Function | Returned object | Allocator |
|----------|----------------|-----------|
| `prRhiCreateInstance` | PrRhiInstance | main |
| `prRhiDestroyInstance` | — | free from main |
| `prRhiGetPhysicalDevices` | PrRhiPhysicalDeviceArray + PrRhiPhysicalDevice structs | main |
| `prRhiCreateSurfaceFromWindow` | PrRhiSurface | main |
| `prRhiDestroySurface` | — | free from main |
| `prRhiCreateDevice` | PrRhiDevice | main (internal VkQueueFamilyProperties2 array → scratch) |
| `prRhiDestroyDevice` | — | free from main |
| `prRhiCreateSwapchain` | PrRhiSwapchain + images + depth | main (internal VkImage array → scratch) |
| `prRhiDestroySwapchain` | — | free from main |
| `prRhiRecreateSwapchain` | updates existing struct | main for new images/depth, scratch for temp arrays |
| `prRhiCreateBuffer` | PrRhiBuffer | main |
| `prRhiDestroyBuffer` | — | free from main |
| `prRhiCreateTexture` | PrRhiTexture | main |
| `prRhiCreateTextureFromKtx` | PrRhiTexture (staging buffer too) | main |
| `prRhiDestroyTexture` | — | free from main |
| `prRhiCreateSampler` | PrRhiSampler | main |
| `prRhiDestroySampler` | — | free from main |
| `prRhiCreateShader` | PrRhiShader | main |
| `prRhiDestroyShader` | — | free from main |
| `prRhiCreatePipelineLayout` | PrRhiPipelineLayout | main (internal overflow arrays → scratch) |
| `prRhiDestroyPipelineLayout` | — | free from main |
| `prRhiCreateGraphicsPipeline` | PrRhiPipeline | main |
| `prRhiCreateComputePipeline` | PrRhiPipeline | main |
| `prRhiDestroyPipeline` | — | free from main |
| `prRhiCreateDescriptorSetLayout` | PrRhiDescriptorSetLayout | main |
| `prRhiDestroyDescriptorSetLayout` | — | free from main |
| `prRhiCreateDescriptorPool` | PrRhiDescriptorPool | main |
| `prRhiDestroyDescriptorPool` | — | free from main |
| `prRhiAllocateDescriptorSet` | PrRhiDescriptorSet | main |
| `prRhiFreeDescriptorSet` | — | free from main |
| `prRhiCreateFence` | PrRhiFence | main |
| `prRhiDestroyFence` | — | free from main |
| `prRhiCreateSemaphore` | PrRhiSemaphore | main |
| `prRhiDestroySemaphore` | — | free from main |
| `prRhiCreateCommandPool` | PrRhiCommandPool | main |
| `prRhiDestroyCommandPool` | — | free from main |
| `prRhiAllocateCommandBuffers` | PrRhiCommandBufferArray + structs | main (internal VkCommandBuffer array → scratch) |
+52
View File
@@ -0,0 +1,52 @@
# Session Log — 2026-07-06
## Completed
- **Build system**: Fixed Just 1.55.1 `[working-directory]` bug (literal paths required).
Set up `just build` for 6 object files: volk.c, wapp.c, pr_rhi_vk.c, pr_rhi_vk_vma.cpp,
vulkan_profiles.cpp, main.cpp. Linked with `-lSDL3 -lglm -ltinyobjloader -lktx -lslang -lvulkan`.
- **RHI backend completeness**: Filled in all missing functions (profiles validation,
swapchain/recreate, dynamic rendering, copy, queue submit, barriers, descriptor,
pipeline, sync) — 68 functions total in single `pr_rhi_vk.c`.
- **Bugfix — `prRhiPresentVk`**: Used `current_image_index` from acquire instead of
hardcoded `0`.
- **Bugfix — `extern "C"` linkage**: Added `extern "C"` guards in `pr_rhi.h` so C++
callers (main.cpp) can link C-compiled backend symbols.
- **Bugfix — GPU name dangling pointers**: Added `device_name[256]`/`driver_info[256]`
to `PrRhiPhysicalDevice` struct, populated during enumeration. Getters return pointers
to these persistent buffers.
- **Bugfix — `volkInitialize()`**: Added call at start of `prRhiCreateInstanceVk`
`vpGetInstanceProfileSupport` crashed because volk hadn't loaded the Vulkan loader.
- **Bugfix — scratch arena OOM**: Changed initial 128KB → 64MB _and_ the line-320
reinit override 8MB → 64MB. Prevents OOM during mesh building.
- **Bugfix — stale mesh array pointers**: Captured return values of `wpArrayAppendAlloc`
in mesh-building loop — original code ignored the pointer, so `vertices`/`indices`
pointed to stale initial array after regrowth.
- **Bugfix — uninitialised Vulkan stack arrays**: Zero-initialised all 18 local Vulkan
struct array declarations (`VkImageMemoryBarrier2[16]`, `VkBufferMemoryBarrier2[16]`,
`VkRenderingAttachmentInfo[8]`, `VkBufferImageCopy[16]`, plus `VkDescriptorSetLayout[8]`,
`VkPushConstantRange[8]`, `VkVertexInputBindingDescription[8]`,
`VkVertexInputAttributeDescription[16]`, `VkDynamicState[2]`, `VkFormat[8]`,
`VkPipelineColorBlendAttachmentState[8]`, `VkDescriptorSetLayoutBinding[16]`,
`VkDescriptorBindingFlags[16]`, `VkDescriptorPoolSize[8]`, `VkDescriptorImageInfo[16]`,
`VkDescriptorBufferInfo[16]`, `VkFence[16]` (×2), `VkCommandBuffer[16]` (×2),
`VkDescriptorSet[16]`, `VkBuffer[16]`). Uninitialised `pNext`/`imageOffset` fields
caused GPU-side device-lost crashes.
- **Bugfix — `render_completed_semaphores` zero-length array**: `prRhiAcquireNextImage`
returns swapchain image INDEX (0 on first call), not image count. Used as array capacity,
this allocated 0 semaphores, causing out-of-bounds access in render loop → SIGSEGV.
Fixed by reading `app.swapchain->image_count` instead.
- **Demo renders**: `build/prism` now launches a window, loads `suzanne0.ktx` / `suzanne1.ktx`,
renders textured Suzanne mesh with mouse orbit + keyboard mesh selection.
## Key Decisions
- Just 1.55.1 bug: `[working-directory: '{{BUILDDIR}}']` causes "could not find the shell `sh`"
— literal paths or no `[working-directory]` attribute required.
- VMA implementation in separate `pr_rhi_vk_vma.cpp` (compiled as C++).
- `PrRhiPhysicalDevice` stores `device_name[256]`/`driver_info[256]` to avoid dangling pointers.
- All stack Vulkan struct arrays must be `= {0}` initialised — C backend does not zero
auto vars, and uninitialised `pNext`/offset fields cause GPU driver crashes.
## Next Steps
1. Review code for structural/design quality issues the user already noticed.
2. Add `wpMemArenaAllocatorTempBegin`/`TempEnd` markers around scratch allocations.
3. Consider adding `prRhiGetSwapchainImageCount` accessor for encapsulation.
+43
View File
@@ -0,0 +1,43 @@
# Session Log — 2026-07-12
## RHI Global Context Refactor
Reviewed the user's staged changes to the Vulkan RHI backend. The changes encompassed:
### Global Context Adoption
- RHI functions no longer take allocator parameters
- `_G_RHI_CONTEXT` provides `allocator` (user-facing objects) and `tmp` (short-lived temporaries)
- `prRhiInit(void)` / `prRhiDestroy(void)` manage the global context
### KTX Texture Bug Fix
- Original implementation only copied mip level 0
- Fixed to iterate all mip levels using `ktxTexture_GetImageOffset()`
- Final layout changed from `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL` to `VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL`
### Frame-by-Frame Command Batching Pattern
- Commands that run every frame avoid arena allocation
- Use stack arrays with while-loop to batch operations in fixed-size chunks
- Applied to: `prRhiCmdBindDescriptorSets`, `prRhiCmdBindVertexBuffers`, `prRhiCmdCopyBufferToImage`
### API Simplifications
- `prRhiCreateCommandPool`: removed desc parameter, uses `device->queue_family_index`
- `prRhiFreeCommandBuffers`: removed count parameter, uses `wpArrayCount`
- `prRhiAllocateDescriptorSet`: changed `u32 variable_count` to `WpU32Array variable_descriptor_counts`
- `prRhiCmdBindVertexBuffers`: changed raw pointer + count to `WpU64Array`
### New Pipeline Configuration
- Added `polygon_mode`, `cull_mode`, `front_face` to rasterization
- Added `depth_test_enable`, `depth_write_enable`, `depth_compare_op`
- Added `vertex_shader_entry_point`, `fragment_shader_entry_point` (not hardcoded to "main")
- Added `line_width`, `multisample_count`
### Code Style Updates
- All Vulkan info structs use C99 designated initializers
- Removed unnecessary type casts on opaque struct handles
- Added braces to all single-line if statements
- Braces rule moved from prism-rhi skill to AGENTS.md formatting section
## Documentation Updates
- Updated `prism-rhi` skill with all new conventions
- Updated `AGENTS.md` with frame-by-frame batching pattern
- Moved braces rule to `AGENTS.md` (not RHI-specific)
+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)
+74 -4
View File
@@ -3,11 +3,81 @@
default: build default: build
# Build the project CC := "clang"
build: CXX := "clang++"
@echo "TODO: implement build" BUILDDIR := "build"
# Run linter / typecheck # Resolve VULKAN_SDK once via backtick
VK_SDK := `echo $VULKAN_SDK`
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" + VENDOR_INC + " -Isrc"
# Build KTX from source
vendor:
mkdir -p {{BUILDDIR}}/ktx
cmake src/vendor/ktx -B {{BUILDDIR}}/ktx \
-D KTX_FEATURE_LOADTEST_APPS=OFF \
-D KTX_FEATURE_DOC=OFF \
-D CMAKE_EXPORT_COMPILE_COMMANDS=1 \
-D CMAKE_BUILD_TYPE=Release \
-D CMAKE_INSTALL_PREFIX=$(pwd)/{{BUILDDIR}} \
-D CMAKE_CXX_STANDARD=17 \
-D CMAKE_CXX_FLAGS="-msse4.1" \
-G Ninja
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 shaders
mkdir -p {{BUILDDIR}}/bin
bear -- {{CXX}} -g -c -Wno-nullability-completeness {{VK_FLAGS}} \
src/prism/rhi/vulkan/profiles/vulkan_profiles.cpp \
-o {{BUILDDIR}}/vulkan_profiles.o
bear -a -- {{CXX}} -g -c -Wno-nullability-completeness {{VK_FLAGS}} \
src/prism/rhi/vulkan/pr_rhi_vk_vma.cpp \
-o {{BUILDDIR}}/pr_rhi_vk_vma.o
bear -a -- {{CC}} -g -c {{VK_FLAGS}} {{VK_SDK}}/include/volk/volk.c -o {{BUILDDIR}}/volk.o
bear -a -- {{CC}} -g -c {{VK_FLAGS}} src/prism/rhi/pr_rhi.c -o {{BUILDDIR}}/pr_rhi.o
bear -a -- {{CC}} -g -c {{VK_FLAGS}} src/prism/rhi/vulkan/pr_rhi_vk.c -o {{BUILDDIR}}/pr_rhi_vk.o
bear -a -- {{CC}} -g -c src/vendor/wapp/wapp.c -o {{BUILDDIR}}/wapp.o
bear -a -- {{CXX}} -g -c {{VK_FLAGS}} -Wno-nullability-completeness -DVK_NO_PROTOTYPES \
{{APP_INC}} \
src/main.cpp \
-o {{BUILDDIR}}/main.o
bear -a -- {{CXX}} -g {{VK_FLAGS}} \
-L{{VK_SDK}}/lib -L{{VENDOR_LIB}} \
build/*.o \
-lSDL3 -lktx -lvulkan \
-Wl,-rpath,{{VENDOR_LIB}} -Wl,-rpath,{{VK_SDK}}/lib \
-o {{BUILDDIR}}/bin/prism
@echo "--- build done: {{BUILDDIR}}/bin/prism ---"
@rm {{BUILDDIR}}/*.o
run:
./{{BUILDDIR}}/bin/prism
# Clean
clean:
rm -rf {{BUILDDIR}}
# Run linter
lint: lint:
@echo "TODO: implement linter" @echo "TODO: implement linter"
+1 -1
View File
@@ -1,6 +1,6 @@
// vim:fileencoding=utf-8:foldmethod=marker // vim:fileencoding=utf-8:foldmethod=marker
#include "../src/wapp/wapp.h" #include "../src/vendor/wapp/wapp.h"
#include <inttypes.h> #include <inttypes.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
+642
View File
@@ -0,0 +1,642 @@
// vim:fileencoding=utf-8:foldmethod=marker
//
// 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 <SDL3/SDL.h>
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_init.h>
#include <SDL3/SDL_keycode.h>
#include <SDL3/SDL_video.h>
#include <iostream>
// ============================================================================
// Exit codes
// ============================================================================
enum ExitCode {
EXIT_CODE_SUCCESS,
EXIT_CODE_SDL_INIT_FAILED,
EXIT_CODE_WINDOW_CREATION_FAILED,
EXIT_CODE_GET_WINDOW_SIZE_FAILED,
EXIT_CODE_NO_PHYSICAL_DEVICES,
EXIT_CODE_NO_SUITABLE_PHYSICAL_DEVICE,
EXIT_CODE_ALLOCATION_FAILURE,
EXIT_CODE_SHADER_LOAD_FAILED,
};
static inline void check(bool result, i32 code) {
if (!result) {
std::cerr << "Call returned an error\n";
exit(code);
}
}
// ============================================================================
// Types
// ============================================================================
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 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;
PrRhiDevice *device;
PrRhiSurface *surface;
PrRhiSwapchain *swapchain;
PrRhiFormat swapchain_format;
static constexpr u32 max_frames_in_flight = 2;
static constexpr u32 texture_count = (u32)(sizeof(TEXTURE_PATHS) / sizeof(TEXTURE_PATHS[0]));
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;
PrRhiShader *shader;
PrRhiPipelineLayout *pipeline_layout;
PrRhiPipeline *pipeline;
u32 selected;
u32 frame_index;
i32 window_width;
i32 window_height;
bool update_swapchain;
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
// ============================================================================
int main() {
AppState app = {};
WpAllocator arena = wpMemArenaAllocatorInitZero(MiB(128));
// {{{ Initialisation
prRhiInit();
check(SDL_Init(SDL_INIT_VIDEO), EXIT_CODE_SDL_INIT_FAILED);
f32 display_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
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);
// }}}
// {{{ Instance creation
app.inst = prRhiCreateInstance(PrRhiInstanceDesc{});
// }}}
// {{{ Physical device selection
PrRhiPhysicalDeviceArray pdevs = prRhiGetPhysicalDevices(app.inst);
check(wpArrayCount(pdevs) > 0, EXIT_CODE_NO_PHYSICAL_DEVICES);
i32 selected = -1;
for (u32 i = 0; i < wpArrayCount(pdevs); ++i) {
PrRhiPhysicalDeviceProperties props = prRhiGetPhysicalDeviceProperties(pdevs[i]);
switch (props.device_type) {
case PR_RHI_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU:
selected = (i32)i;
break;
case PR_RHI_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU:
if (selected == -1)
selected = (i32)i;
break;
default: continue;
}
}
check(selected != -1, EXIT_CODE_NO_SUITABLE_PHYSICAL_DEVICE);
app.pdev = pdevs[selected];
// Print device info
WpStr8 dev_name = wpStr8Buf(512);
WpStr8 driver_info = wpStr8Buf(512);
prRhiGetPhysicalDeviceName(app.pdev, &dev_name);
prRhiGetPhysicalDeviceDriverInfo(app.pdev, &driver_info);
std::cout << "Selected GPU: " << std::string_view((const char *)dev_name.buf, dev_name.size) << '\n'
<< "Driver version: " << std::string_view((const char *)driver_info.buf, driver_info.size) << '\n';
// }}}
// {{{ Surface creation
check(SDL_GetWindowSizeInPixels(app.window, &app.window_width, &app.window_height),
EXIT_CODE_GET_WINDOW_SIZE_FAILED);
app.surface = prRhiCreateSurfaceFromWindow(app.inst, app.window);
// }}}
// {{{ Device creation
PrRhiDeviceDesc dev_desc = {};
dev_desc.present_mode = PR_RHI_PRESENT_MODE_FIFO;
app.device = prRhiCreateDevice(app.pdev, app.surface, dev_desc);
// }}}
// {{{ Swapchain creation
PrRhiSwapchainDesc swap_desc = {};
swap_desc.surface = app.surface;
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;
app.swapchain = prRhiCreateSwapchain(app.device, swap_desc);
app.swapchain_format = prRhiGetSwapchainFormat(app.swapchain);
// }}}
// {{{ Synchronisation objects
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) {
PrRhiFenceDesc fd = {};
fd.signaled = true;
app.fences[i] = prRhiCreateFence(app.device, fd);
}
app.image_acquired_semaphores = wpArrayAllocCapacity(PrRhiSemaphore *, &arena,
AppState::max_frames_in_flight,
WP_ARRAY_INIT_FILLED);
for (u32 i = 0; i < AppState::max_frames_in_flight; ++i) {
app.image_acquired_semaphores[i] = prRhiCreateSemaphore(app.device);
}
app.render_semaphore_count = prRhiGetSwapchainImageCount(app.swapchain);
app.render_completed_semaphores = wpArrayAllocCapacity(PrRhiSemaphore *, &arena,
app.render_semaphore_count,
WP_ARRAY_INIT_FILLED);
for (u32 i = 0; i < app.render_semaphore_count; ++i) {
app.render_completed_semaphores[i] = prRhiCreateSemaphore(app.device);
}
// }}}
// {{{ Command pool and buffers
app.cmd_pool = prRhiCreateCommandPool(app.device);
app.cmd_buffers = prRhiAllocateCommandBuffers(app.device, app.cmd_pool,
AppState::max_frames_in_flight);
// }}}
// {{{ Texture loading
app.textures = wpArrayAllocCapacity(TextureResources, &arena, AppState::texture_count,
WP_ARRAY_INIT_FILLED);
PrRhiCommandBufferArray upload_cbs = prRhiAllocateCommandBuffers(app.device, app.cmd_pool, 1);
PrRhiCommandBuffer *upload_cb = upload_cbs[0];
for (u32 i = 0; i < AppState::texture_count; ++i) {
PrRhiTexture *tex = prRhiCreateTextureFromKtx(app.device, TEXTURE_PATHS[i], app.cmd_pool, upload_cb);
PrRhiSamplerDesc samp_desc = {};
samp_desc.mag_filter = PR_RHI_FILTER_LINEAR;
samp_desc.min_filter = PR_RHI_FILTER_LINEAR;
samp_desc.mipmap_mode = PR_RHI_MIPMAP_MODE_LINEAR;
samp_desc.max_anisotropy = 8.0f;
samp_desc.max_lod = PR_RHI_LOD_CLAMP_NONE;
PrRhiSampler *sampler = prRhiCreateSampler(app.device, samp_desc);
app.textures[i].texture = tex;
app.textures[i].sampler = sampler;
}
prRhiFreeCommandBuffers(app.device, app.cmd_pool, upload_cbs);
// }}}
// {{{ Descriptor set layout, pool, set
PrRhiDescriptorSetLayoutBindingArray ds_layouts = wpArray(
PrRhiDescriptorSetLayoutBinding,
PrRhiDescriptorSetLayoutBinding{
PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
AppState::texture_count,
PR_RHI_SHADER_STAGE_FRAGMENT,
PR_RHI_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT,
}
);
PrRhiDescriptorSetLayoutDesc layout_desc = { ds_layouts };
app.desc_set_layout = prRhiCreateDescriptorSetLayout(app.device, layout_desc);
PrRhiDescriptorPoolSize pool_size = {};
pool_size.type = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
pool_size.descriptor_count = AppState::texture_count;
PrRhiDescriptorPoolDesc ds_pool_desc = {};
ds_pool_desc.max_sets = 1;
ds_pool_desc.pool_sizes = wpArray(PrRhiDescriptorPoolSize, pool_size);
app.desc_pool = prRhiCreateDescriptorPool(app.device, ds_pool_desc);
WpU32Array var_counts = wpArray(u32, AppState::texture_count);
app.desc_set = prRhiAllocateDescriptorSet(app.device, app.desc_pool, app.desc_set_layout,
var_counts);
PrRhiDescriptorImageInfoArray img_infos = wpArrayAllocCapacity(PrRhiDescriptorImageInfo,
&arena,
AppState::texture_count,
WP_ARRAY_INIT_NONE);
for (u32 i = 0; i < AppState::texture_count; ++i) {
PrRhiDescriptorImageInfo info = {};
info.texture = app.textures[i].texture;
info.sampler = app.textures[i].sampler;
info.layout = PR_RHI_LAYOUT_READ_ONLY_OPTIMAL;
wpArrayAppendCapped(PrRhiDescriptorImageInfo, img_infos, &info);
}
PrRhiWriteDescriptorSet write = {};
write.dst_set = app.desc_set;
write.dst_binding = 0;
write.type = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.image_info = img_infos;
PrRhiWriteDescriptorSetArray writes = wpArray(PrRhiWriteDescriptorSet, write);
prRhiUpdateDescriptorSet(app.device, writes);
// }}}
// {{{ 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);
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);
PrRhiShaderDesc shader_desc = {};
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 = (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);
PrRhiPipelineLayoutDesc pl_desc = {};
pl_desc.set_layouts = pl_layouts;
pl_desc.push_constant_ranges = wpArray(PrRhiPushConstantRange, pc_range);
app.pipeline_layout = prRhiCreatePipelineLayout(app.device, pl_desc);
// }}}
// {{{ Graphics pipeline
PrRhiColorBlendAttachmentArray blend_attachments = wpArray(
PrRhiColorBlendAttachment,
PrRhiColorBlendAttachment{ 0xf }
);
PrRhiFormatArray color_fmt_array = wpArray(PrRhiFormat, app.swapchain_format);
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.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 = 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_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.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
PrRhiFence *wait_fence = app.fences[app.frame_index];
prRhiWaitForFences(app.device, wpArray(PrRhiFence *, wait_fence), 1, true, UINT64_MAX);
prRhiResetFences(app.device, wpArray(PrRhiFence *, wait_fence), 1);
// }}}
// {{{ Acquire next image
PrRhiSwapchainResult acq = prRhiAcquireNextImage(app.device, app.swapchain,
app.image_acquired_semaphores[app.frame_index],
&image_index);
if (acq == PR_RHI_SWAPCHAIN_OUT_OF_DATE) {
app.update_swapchain = true;
}
// }}}
if (app.update_swapchain) {
// Skip this frame — will recreate below
} else {
// {{{ 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
PrRhiCommandBuffer *cb = app.cmd_buffers[app.frame_index];
prRhiResetCommandBuffer(cb);
prRhiBeginCommandBuffer(cb);
// Transition images to attachment optimal
{
PrRhiImageMemoryBarrierArray barriers_arr =
wpArrayWithCapacity(PrRhiImageMemoryBarrier, 2, WP_ARRAY_INIT_FILLED);
PrRhiTexture *color_tex = prRhiGetSwapchainTexture(app.swapchain, image_index);
barriers_arr[0].texture = color_tex;
barriers_arr[0].old_layout = PR_RHI_LAYOUT_UNDEFINED;
barriers_arr[0].new_layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL;
barriers_arr[0].src_stage_mask = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT;
barriers_arr[0].src_access_mask = PR_RHI_ACCESS_NONE;
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);
PrRhiTexture *depth_tex = prRhiGetSwapchainDepthTexture(app.swapchain);
barriers_arr[1].texture = depth_tex;
barriers_arr[1].old_layout = PR_RHI_LAYOUT_UNDEFINED;
barriers_arr[1].new_layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL;
barriers_arr[1].src_stage_mask = PR_RHI_PIPELINE_STAGE_LATE_FRAGMENT_TESTS;
barriers_arr[1].src_access_mask = PR_RHI_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE;
barriers_arr[1].dst_stage_mask = PR_RHI_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS;
barriers_arr[1].dst_access_mask = PR_RHI_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE;
prRhiCmdPipelineBarrier(cb, barriers_arr, NULL);
}
// Rendering
{
PrRhiColorAttachmentArray color_arr =
wpArrayWithCapacity(PrRhiColorAttachment, 1, WP_ARRAY_INIT_FILLED);
color_arr[0].texture = prRhiGetSwapchainTexture(app.swapchain, image_index);
color_arr[0].layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL;
color_arr[0].clear = true;
color_arr[0].clear_color[0] = 0.0f;
color_arr[0].clear_color[1] = 0.0f;
color_arr[0].clear_color[2] = 0.0f;
color_arr[0].clear_color[3] = 0.0f;
PrRhiDepthAttachment depth_att = {};
depth_att.texture = prRhiGetSwapchainDepthTexture(app.swapchain);
depth_att.layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL;
depth_att.clear = true;
depth_att.clear_depth = 1.0f;
prRhiCmdBeginRendering(cb, color_arr, &depth_att);
}
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);
PrRhiDescriptorSetArray sets = wpArray(PrRhiDescriptorSet *, app.desc_set);
prRhiCmdBindDescriptorSets(cb, PR_RHI_PIPELINE_BIND_POINT_GRAPHICS,
app.pipeline_layout, 0, sets);
// 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);
// 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);
prRhiCmdDraw(cb, 4, 1, 0, 0);
prRhiCmdEndRendering(cb);
// Transition to present
{
PrRhiImageMemoryBarrierArray present_barriers =
wpArrayWithCapacity(PrRhiImageMemoryBarrier, 1, WP_ARRAY_INIT_FILLED);
present_barriers[0].texture = prRhiGetSwapchainTexture(app.swapchain, image_index);
present_barriers[0].old_layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL;
present_barriers[0].new_layout = PR_RHI_LAYOUT_PRESENT_SRC;
present_barriers[0].src_stage_mask = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT;
present_barriers[0].src_access_mask = PR_RHI_ACCESS_COLOR_ATTACHMENT_WRITE;
present_barriers[0].dst_stage_mask = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT;
present_barriers[0].dst_access_mask = PR_RHI_ACCESS_NONE;
prRhiCmdPipelineBarrier(cb, present_barriers, NULL);
}
prRhiEndCommandBuffer(cb);
// }}}
// {{{ Submit
prRhiQueueSubmit(app.device, cb, app.image_acquired_semaphores[app.frame_index],
app.render_completed_semaphores[image_index], app.fences[app.frame_index]);
// }}}
// {{{ Present
PrRhiSwapchainResult pres = prRhiPresent(app.device, app.swapchain,
app.render_completed_semaphores[image_index]);
if (pres == PR_RHI_SWAPCHAIN_OUT_OF_DATE) {
app.update_swapchain = true;
}
// }}}
}
// {{{ Poll events
SDL_Event event = {};
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_EVENT_QUIT:
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.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.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_WINDOW_RESIZED:
check(SDL_GetWindowSizeInPixels(app.window, &app.window_width, &app.window_height),
EXIT_CODE_GET_WINDOW_SIZE_FAILED);
app.update_swapchain = true;
break;
}
}
// }}}
// {{{ Swapchain recreate
if (app.update_swapchain) {
prRhiDeviceWaitIdle(app.device);
prRhiRecreateSwapchain(app.device, &app.swapchain,
(u32)app.window_width, (u32)app.window_height);
// 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) {
app.render_completed_semaphores[i] = prRhiCreateSemaphore(app.device);
}
app.update_swapchain = false;
}
// }}}
app.frame_index = (app.frame_index + 1) % AppState::max_frames_in_flight;
}
done:
// }}}
// {{{ Cleanup
prRhiDeviceWaitIdle(app.device);
prRhiDestroyPipeline(app.device, app.pipeline);
prRhiDestroyPipelineLayout(app.device, app.pipeline_layout);
prRhiDestroyShader(app.device, app.shader);
prRhiDestroyDescriptorPool(app.device, app.desc_pool);
prRhiDestroyDescriptorSetLayout(app.device, app.desc_set_layout);
for (u32 i = 0; i < AppState::texture_count; ++i) {
prRhiDestroySampler(app.device, app.textures[i].sampler);
prRhiDestroyTexture(app.device, app.textures[i].texture);
}
prRhiFreeCommandBuffers(app.device, app.cmd_pool, app.cmd_buffers);
prRhiDestroyCommandPool(app.device, app.cmd_pool);
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]);
}
prRhiDestroySwapchain(app.device, app.swapchain);
prRhiDestroyDevice(app.device);
prRhiDestroySurface(app.inst, app.surface);
prRhiDestroyInstance(app.inst);
SDL_DestroyWindow(app.window);
SDL_Quit();
prRhiDestroy();
wpMemArenaAllocatorDestroy(&arena);
// }}}
return EXIT_CODE_SUCCESS;
}
+113
View File
@@ -0,0 +1,113 @@
// vim:fileencoding=utf-8:foldmethod=marker
#include "pr_pool_allocator.h"
#include "../../vendor/wapp/os/mem/mem_os.h"
#include <string.h>
// ---------------------------------------------------------------------------
// Intrusive free list node — reuses the first sizeof(void*) bytes of each slot
// ---------------------------------------------------------------------------
typedef struct PrPoolFreeNode PrPoolFreeNode;
struct PrPoolFreeNode {
PrPoolFreeNode *next;
};
// ---------------------------------------------------------------------------
// Internal: allocate a new block and carve it into the free list
// ---------------------------------------------------------------------------
wp_intern b8 _prPoolGrow(PrPool *pool) {
// Grow blocks array if full
if (pool->block_count == pool->block_cap) {
u64 new_cap = pool->block_cap ? pool->block_cap * 2 : 4;
u64 new_size = sizeof(void *) * new_cap;
void **new_arr = (void **)wpOsMemAlloc(NULL, new_size, WP_MEM_ACCESS_READ_WRITE,
WP_MEM_ALLOC_RESERVE, WP_MEM_INIT_UNINITIALISED);
if (!new_arr) { return false; }
if (pool->blocks) {
memcpy(new_arr, pool->blocks, sizeof(void *) * pool->block_count);
wpOsMemFree(pool->blocks, sizeof(void *) * pool->block_cap);
}
pool->blocks = new_arr;
pool->block_cap = new_cap;
}
// Allocate the block
u64 block_bytes = pool->alloc_size * pool->block_slots;
void *block = wpOsMemAlloc(NULL, block_bytes, WP_MEM_ACCESS_READ_WRITE,
WP_MEM_ALLOC_RESERVE, WP_MEM_INIT_UNINITIALISED);
if (!block) { return false; }
pool->blocks[pool->block_count++] = block;
pool->total += pool->block_slots;
// Carve into free list (in reverse so the first slot ends up on top)
u8 *bytes = (u8 *)block;
for (u64 i = pool->block_slots; i > 0; --i) {
PrPoolFreeNode *node = (PrPoolFreeNode *)bytes;
node->next = (PrPoolFreeNode *)pool->free_list;
pool->free_list = node;
bytes += pool->alloc_size;
}
return true;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void prPoolInit(PrPool *pool, u64 slot_size, u64 initial_slots) {
memset(pool, 0, sizeof(*pool));
pool->slot_size = slot_size;
pool->alloc_size = slot_size < sizeof(PrPoolFreeNode) ? sizeof(PrPoolFreeNode) : slot_size;
// Enforce minimum block size of 4096 bytes
u64 min_slots = (4096 + pool->alloc_size - 1) / pool->alloc_size;
pool->block_slots = initial_slots > min_slots ? initial_slots : min_slots;
_prPoolGrow(pool);
}
void *prPoolAlloc(PrPool *pool) {
// Grow if free list is empty
if (!pool->free_list) {
if (!_prPoolGrow(pool)) { return NULL; }
}
PrPoolFreeNode *node = (PrPoolFreeNode *)pool->free_list;
pool->free_list = node->next;
pool->active++;
return node;
}
void prPoolFree(PrPool *pool, void *slot) {
if (!slot) { return; }
PrPoolFreeNode *node = (PrPoolFreeNode *)slot;
node->next = (PrPoolFreeNode *)pool->free_list;
pool->free_list = node;
pool->active--;
}
void prPoolDestroy(PrPool *pool) {
u64 block_bytes = pool->alloc_size * pool->block_slots;
for (u64 i = 0; i < pool->block_count; ++i) {
wpOsMemFree(pool->blocks[i], block_bytes);
}
if (pool->blocks) {
wpOsMemFree(pool->blocks, sizeof(void *) * pool->block_cap);
}
memset(pool, 0, sizeof(*pool));
}
u64 prPoolTotalSlots(const PrPool *pool) {
return pool->total;
}
u64 prPoolActiveSlots(const PrPool *pool) {
return pool->active;
}
+63
View File
@@ -0,0 +1,63 @@
// vim:fileencoding=utf-8:foldmethod=marker
#ifndef PR_POOL_ALLOCATOR_H
#define PR_POOL_ALLOCATOR_H
#include "../../vendor/wapp/common/aliases/aliases.h"
#ifdef __cplusplus
extern "C" {
#endif
// ---------------------------------------------------------------------------
// Pool allocator — fixed-size slot allocator with self-managed growth
//
// Manages fixed-size slots arranged in contiguous blocks. The pool owns its
// memory (via wapp OS allocation) and grows on demand when the free list is empty.
//
// Usage:
// PrPool pool;
// prPoolInit(&pool, sizeof(Edge), 64);
// Edge *e = prPoolAlloc(&pool);
// prPoolFree(&pool, e);
// prPoolDestroy(&pool);
// ---------------------------------------------------------------------------
typedef struct PrPool PrPool;
struct PrPool {
void **blocks; // array of allocated block pointers (for destroy)
u64 block_count; // number of allocated blocks
u64 block_cap; // capacity of blocks array
void *free_list; // intrusive free list head
u64 slot_size; // user-requested slot size
u64 alloc_size; // actual slot size used internally (>= slot_size, >= sizeof(void*))
u64 block_slots; // slots per block
u64 total; // total slots ever allocated (diagnostics)
u64 active; // currently in use (diagnostics)
};
// Initialise a pool.
// slot_size: fixed size of each slot
// initial_slots: starting capacity in slots (also used as block size)
void prPoolInit(PrPool *pool, u64 slot_size, u64 initial_slots);
// Allocate one slot. Grows by a new block if the free list is empty.
// Returns NULL only on allocation failure.
void *prPoolAlloc(PrPool *pool);
// Return a slot to the pool's free list. Safe no-op on NULL.
void prPoolFree(PrPool *pool, void *slot);
// Free all blocks and zero the pool.
void prPoolDestroy(PrPool *pool);
// Diagnostics
u64 prPoolTotalSlots(const PrPool *pool);
u64 prPoolActiveSlots(const PrPool *pool);
#ifdef __cplusplus
}
#endif
#endif // !PR_POOL_ALLOCATOR_H
+15
View File
@@ -0,0 +1,15 @@
// vim:fileencoding=utf-8:foldmethod=marker
//
// RHI context — shared initialisation and global state.
#include "pr_rhi.h"
PrRhiContext _G_RHI_CONTEXT;
void prRhiInit(void) {
_G_RHI_CONTEXT.allocator = wpMemArenaAllocatorInit(MiB(64));
}
void prRhiDestroy(void) {
wpMemArenaAllocatorDestroy(&_G_RHI_CONTEXT.allocator);
}
+65 -61
View File
@@ -24,28 +24,39 @@
#define PR_RHI_H #define PR_RHI_H
#include "pr_rhi_types.h" #include "pr_rhi_types.h"
#include <SDL3/SDL_video.h>
#ifdef __cplusplus
extern "C" {
#endif
wp_extern PrRhiContext _G_RHI_CONTEXT;
wp_extern void prRhiInit(void);
wp_extern void prRhiDestroy(void);
// ====================================================================== // ======================================================================
// Instance // Instance
// ====================================================================== // ======================================================================
PrRhiInstance *prRhiCreateInstance(PrRhiInstanceDesc desc, WpAllocator *alloc); PrRhiInstance *prRhiCreateInstance(PrRhiInstanceDesc desc);
void prRhiDestroyInstance(PrRhiInstance *inst, WpAllocator *alloc); void prRhiDestroyInstance(PrRhiInstance *inst);
// ====================================================================== // ======================================================================
// Physical device enumeration // Physical device enumeration
// ====================================================================== // ======================================================================
PrRhiPhysicalDeviceArray prRhiGetPhysicalDevices(PrRhiInstance *inst, const WpAllocator *scratch); PrRhiPhysicalDeviceArray prRhiGetPhysicalDevices(PrRhiInstance *inst);
void prRhiGetPhysicalDeviceName(PrRhiPhysicalDevice *pdev, WpStr8 *out); void prRhiGetPhysicalDeviceName(PrRhiPhysicalDevice *pdev, WpStr8 *out);
void prRhiGetPhysicalDeviceDriverInfo(PrRhiPhysicalDevice *pdev, WpStr8 *out); void prRhiGetPhysicalDeviceDriverInfo(PrRhiPhysicalDevice *pdev, WpStr8 *out);
PrRhiPhysicalDeviceProperties prRhiGetPhysicalDeviceProperties(PrRhiPhysicalDevice *pdev);
// ====================================================================== // ======================================================================
// Surface (platform-specific) // Surface (platform-specific)
// ====================================================================== // ======================================================================
PrRhiSurface *prRhiCreateSurface(PrRhiInstance *inst, void *window_handle, WpAllocator *alloc); PrRhiSurface *prRhiCreateSurfaceFromWindow(PrRhiInstance *inst, SDL_Window *window);
void prRhiDestroySurface(PrRhiInstance *inst, PrRhiSurface *surface, WpAllocator *alloc); void prRhiDestroySurface(PrRhiInstance *inst, PrRhiSurface *surface);
PrRhiSurfaceCapabilities prRhiGetSurfaceCapabilities(PrRhiPhysicalDevice *pdev, PrRhiSurfaceCapabilities prRhiGetSurfaceCapabilities(PrRhiPhysicalDevice *pdev,
PrRhiSurface *surface); PrRhiSurface *surface);
@@ -54,19 +65,19 @@ PrRhiSurfaceCapabilities prRhiGetSurfaceCapabilities(PrRhiPhysicalDevice *pdev,
// ====================================================================== // ======================================================================
PrRhiDevice *prRhiCreateDevice(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface, PrRhiDevice *prRhiCreateDevice(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface,
PrRhiDeviceDesc desc, WpAllocator *alloc); PrRhiDeviceDesc desc);
void prRhiDestroyDevice(PrRhiDevice *device, WpAllocator *alloc); void prRhiDestroyDevice(PrRhiDevice *device);
void prRhiDeviceWaitIdle(PrRhiDevice *device); void prRhiDeviceWaitIdle(PrRhiDevice *device);
u32 prRhiGetQueueFamilyIndex(PrRhiDevice *device);
// ====================================================================== // ======================================================================
// Swapchain // Swapchain
// ====================================================================== // ======================================================================
PrRhiSwapchain *prRhiCreateSwapchain(PrRhiDevice *device, PrRhiSwapchainDesc desc, PrRhiSwapchain *prRhiCreateSwapchain(PrRhiDevice *device, PrRhiSwapchainDesc desc);
WpAllocator *alloc); void prRhiDestroySwapchain(PrRhiDevice *device, PrRhiSwapchain *swapchain);
void prRhiDestroySwapchain(PrRhiDevice *device, PrRhiSwapchain *swapchain,
WpAllocator *alloc);
u32 prRhiGetSwapchainImageCount(PrRhiSwapchain *swapchain);
PrRhiSwapchainResult prRhiAcquireNextImage(PrRhiDevice *device, PrRhiSwapchain *swapchain, PrRhiSwapchainResult prRhiAcquireNextImage(PrRhiDevice *device, PrRhiSwapchain *swapchain,
PrRhiSemaphore *signal_semaphore, u32 *out_image_index); PrRhiSemaphore *signal_semaphore, u32 *out_image_index);
@@ -74,18 +85,18 @@ PrRhiSwapchainResult prRhiPresent(PrRhiDevice *device, PrRhiSwapchain *swapchain
PrRhiSemaphore *wait_semaphore); PrRhiSemaphore *wait_semaphore);
void prRhiRecreateSwapchain(PrRhiDevice *device, PrRhiSwapchain **swapchain, void prRhiRecreateSwapchain(PrRhiDevice *device, PrRhiSwapchain **swapchain,
u32 width, u32 height, WpAllocator *alloc); u32 width, u32 height);
PrRhiTexture *prRhiGetSwapchainTexture(PrRhiSwapchain *swapchain, u32 image_index); PrRhiTexture *prRhiGetSwapchainTexture(PrRhiSwapchain *swapchain, u32 image_index);
PrRhiTexture *prRhiGetSwapchainDepthTexture(PrRhiSwapchain *swapchain); PrRhiTexture *prRhiGetSwapchainDepthTexture(PrRhiSwapchain *swapchain);
PrRhiFormat prRhiGetSwapchainFormat(PrRhiSwapchain *swapchain);
// ====================================================================== // ======================================================================
// Buffers // Buffers
// ====================================================================== // ======================================================================
PrRhiBuffer *prRhiCreateBuffer(PrRhiDevice *device, PrRhiBufferDesc desc, PrRhiBuffer *prRhiCreateBuffer(PrRhiDevice *device, PrRhiBufferDesc desc);
WpAllocator *alloc); void prRhiDestroyBuffer(PrRhiDevice *device, PrRhiBuffer *buffer);
void prRhiDestroyBuffer(PrRhiDevice *device, PrRhiBuffer *buffer, WpAllocator *alloc);
void *prRhiBufferMap(PrRhiDevice *device, PrRhiBuffer *buffer); void *prRhiBufferMap(PrRhiDevice *device, PrRhiBuffer *buffer);
void prRhiBufferUnmap(PrRhiDevice *device, PrRhiBuffer *buffer); void prRhiBufferUnmap(PrRhiDevice *device, PrRhiBuffer *buffer);
@@ -96,73 +107,64 @@ PrRhiDeviceAddress prRhiGetBufferDeviceAddress(PrRhiDevice *device, PrRhiBuffer
// Textures // Textures
// ====================================================================== // ======================================================================
PrRhiTexture *prRhiCreateTexture(PrRhiDevice *device, PrRhiTextureDesc desc, PrRhiTexture *prRhiCreateTexture(PrRhiDevice *device, PrRhiTextureDesc desc);
WpAllocator *alloc); PrRhiTexture *prRhiCreateTextureFromKtx(PrRhiDevice *device, const char *path,
void prRhiDestroyTexture(PrRhiDevice *device, PrRhiTexture *texture, PrRhiCommandPool *pool, PrRhiCommandBuffer *cb);
WpAllocator *alloc); void prRhiDestroyTexture(PrRhiDevice *device, PrRhiTexture *texture);
PrRhiTextureSize prRhiGetTextureSize(PrRhiTexture *texture);
// ====================================================================== // ======================================================================
// Samplers // Samplers
// ====================================================================== // ======================================================================
PrRhiSampler *prRhiCreateSampler(PrRhiDevice *device, PrRhiSamplerDesc desc, PrRhiSampler *prRhiCreateSampler(PrRhiDevice *device, PrRhiSamplerDesc desc);
WpAllocator *alloc); void prRhiDestroySampler(PrRhiDevice *device, PrRhiSampler *sampler);
void prRhiDestroySampler(PrRhiDevice *device, PrRhiSampler *sampler,
WpAllocator *alloc);
// ====================================================================== // ======================================================================
// Shaders (from SPIR-V) // Shaders (from SPIR-V)
// ====================================================================== // ======================================================================
PrRhiShader *prRhiCreateShader(PrRhiDevice *device, PrRhiShaderDesc desc, PrRhiShader *prRhiCreateShader(PrRhiDevice *device, PrRhiShaderDesc desc);
WpAllocator *alloc); void prRhiDestroyShader(PrRhiDevice *device, PrRhiShader *shader);
void prRhiDestroyShader(PrRhiDevice *device, PrRhiShader *shader, WpAllocator *alloc);
// ====================================================================== // ======================================================================
// Pipeline layouts // Pipeline layouts
// ====================================================================== // ======================================================================
PrRhiPipelineLayout *prRhiCreatePipelineLayout(PrRhiDevice *device, PrRhiPipelineLayout *prRhiCreatePipelineLayout(PrRhiDevice *device,
PrRhiPipelineLayoutDesc desc, PrRhiPipelineLayoutDesc desc);
WpAllocator *alloc);
void prRhiDestroyPipelineLayout(PrRhiDevice *device, void prRhiDestroyPipelineLayout(PrRhiDevice *device,
PrRhiPipelineLayout *layout, PrRhiPipelineLayout *layout);
WpAllocator *alloc);
// ====================================================================== // ======================================================================
// Pipelines // Pipelines
// ====================================================================== // ======================================================================
PrRhiPipeline *prRhiCreateGraphicsPipeline(PrRhiDevice *device, PrRhiPipeline *prRhiCreateGraphicsPipeline(PrRhiDevice *device,
PrRhiGraphicsPipelineDesc desc, PrRhiGraphicsPipelineDesc desc);
WpAllocator *alloc);
PrRhiPipeline *prRhiCreateComputePipeline(PrRhiDevice *device, PrRhiPipeline *prRhiCreateComputePipeline(PrRhiDevice *device,
PrRhiComputePipelineDesc desc, PrRhiComputePipelineDesc desc);
WpAllocator *alloc); void prRhiDestroyPipeline(PrRhiDevice *device, PrRhiPipeline *pipeline);
void prRhiDestroyPipeline(PrRhiDevice *device, PrRhiPipeline *pipeline,
WpAllocator *alloc);
// ====================================================================== // ======================================================================
// Descriptor set layouts // Descriptor set layouts
// ====================================================================== // ======================================================================
PrRhiDescriptorSetLayout *prRhiCreateDescriptorSetLayout(PrRhiDevice *device, PrRhiDescriptorSetLayout *prRhiCreateDescriptorSetLayout(PrRhiDevice *device,
PrRhiDescriptorSetLayoutDesc desc, PrRhiDescriptorSetLayoutDesc desc);
WpAllocator *alloc);
void prRhiDestroyDescriptorSetLayout(PrRhiDevice *device, void prRhiDestroyDescriptorSetLayout(PrRhiDevice *device,
PrRhiDescriptorSetLayout *layout, PrRhiDescriptorSetLayout *layout);
WpAllocator *alloc);
// ====================================================================== // ======================================================================
// Descriptor pools // Descriptor pools
// ====================================================================== // ======================================================================
PrRhiDescriptorPool *prRhiCreateDescriptorPool(PrRhiDevice *device, PrRhiDescriptorPool *prRhiCreateDescriptorPool(PrRhiDevice *device,
PrRhiDescriptorPoolDesc desc, PrRhiDescriptorPoolDesc desc);
WpAllocator *alloc);
void prRhiDestroyDescriptorPool(PrRhiDevice *device, void prRhiDestroyDescriptorPool(PrRhiDevice *device,
PrRhiDescriptorPool *pool, PrRhiDescriptorPool *pool);
WpAllocator *alloc); void prRhiResetDescriptorPool(PrRhiDevice *device,
PrRhiDescriptorPool *pool);
// ====================================================================== // ======================================================================
// Descriptor sets // Descriptor sets
@@ -170,40 +172,37 @@ void prRhiDestroyDescriptorPool(PrRhiDevice *device,
PrRhiDescriptorSet *prRhiAllocateDescriptorSet(PrRhiDevice *device, PrRhiDescriptorPool *pool, PrRhiDescriptorSet *prRhiAllocateDescriptorSet(PrRhiDevice *device, PrRhiDescriptorPool *pool,
PrRhiDescriptorSetLayout *layout, PrRhiDescriptorSetLayout *layout,
u32 variable_count, WpAllocator *alloc); WpU32Array variable_descriptor_counts);
void prRhiFreeDescriptorSet(PrRhiDevice *device, PrRhiDescriptorPool *pool, void prRhiFreeDescriptorSet(PrRhiDevice *device, PrRhiDescriptorPool *pool,
PrRhiDescriptorSet *set, WpAllocator *alloc); PrRhiDescriptorSet *set);
void prRhiUpdateDescriptorSet(PrRhiDevice *device, PrRhiWriteDescriptorSetArray writes); void prRhiUpdateDescriptorSet(PrRhiDevice *device, PrRhiWriteDescriptorSetArray writes);
// ====================================================================== // ======================================================================
// Fences and semaphores // Fences and semaphores
// ====================================================================== // ======================================================================
PrRhiFence *prRhiCreateFence(PrRhiDevice *device, PrRhiFenceDesc desc, PrRhiFence *prRhiCreateFence(PrRhiDevice *device, PrRhiFenceDesc desc);
WpAllocator *alloc); void prRhiDestroyFence(PrRhiDevice *device, PrRhiFence *fence);
void prRhiDestroyFence(PrRhiDevice *device, PrRhiFence *fence, WpAllocator *alloc);
void prRhiWaitForFences(PrRhiDevice *device, PrRhiFenceArray fences, u32 count, void prRhiWaitForFences(PrRhiDevice *device, PrRhiFenceArray fences, u32 count,
b8 wait_all, u64 timeout_ns); b8 wait_all, u64 timeout_ns);
void prRhiResetFences(PrRhiDevice *device, PrRhiFenceArray fences, u32 count); void prRhiResetFences(PrRhiDevice *device, PrRhiFenceArray fences, u32 count);
PrRhiSemaphore *prRhiCreateSemaphore(PrRhiDevice *device, WpAllocator *alloc); PrRhiSemaphore *prRhiCreateSemaphore(PrRhiDevice *device);
void prRhiDestroySemaphore(PrRhiDevice *device, PrRhiSemaphore *semaphore, void prRhiDestroySemaphore(PrRhiDevice *device, PrRhiSemaphore *semaphore);
WpAllocator *alloc);
// ====================================================================== // ======================================================================
// Command pools and command buffers // Command pools and command buffers
// ====================================================================== // ======================================================================
PrRhiCommandPool *prRhiCreateCommandPool(PrRhiDevice *device, PrRhiCommandPoolDesc desc, PrRhiCommandPool *prRhiCreateCommandPool(PrRhiDevice *device);
WpAllocator *alloc); void prRhiDestroyCommandPool(PrRhiDevice *device, PrRhiCommandPool *pool);
void prRhiDestroyCommandPool(PrRhiDevice *device, PrRhiCommandPool *pool,
WpAllocator *alloc);
PrRhiCommandBufferArray prRhiAllocateCommandBuffers(PrRhiDevice *device, PrRhiCommandPool *pool, PrRhiCommandBufferArray prRhiAllocateCommandBuffers(PrRhiDevice *device, PrRhiCommandPool *pool,
u32 count, WpAllocator *alloc); u32 count);
void prRhiFreeCommandBuffers(PrRhiDevice *device, PrRhiCommandPool *pool, void prRhiFreeCommandBuffers(PrRhiDevice *device, PrRhiCommandPool *pool,
u32 count, PrRhiCommandBufferArray buffers); PrRhiCommandBufferArray buffers);
// ====================================================================== // ======================================================================
// Command buffer recording // Command buffer recording
@@ -244,8 +243,8 @@ void prRhiCmdPushConstants(PrRhiCommandBuffer *cb, PrRhiPipelineLayout *layout,
// --- Vertex / index buffers --- // --- Vertex / index buffers ---
void prRhiCmdBindVertexBuffers(PrRhiCommandBuffer *cb, u32 first_binding, void prRhiCmdBindVertexBuffers(PrRhiCommandBuffer *cb, u32 first_binding, PrRhiBufferArray buffers,
PrRhiBufferArray buffers, const u64 *offsets, u32 count); WpU64Array offsets);
void prRhiCmdBindIndexBuffer(PrRhiCommandBuffer *cb, PrRhiBuffer *buffer, u64 offset, void prRhiCmdBindIndexBuffer(PrRhiCommandBuffer *cb, PrRhiBuffer *buffer, u64 offset,
PrRhiIndexType index_type); PrRhiIndexType index_type);
@@ -258,7 +257,8 @@ void prRhiCmdDrawIndexed(PrRhiCommandBuffer *cb, u32 index_count, u32 instance_c
// --- Copy --- // --- Copy ---
void prRhiCmdCopyBufferToImage(PrRhiCommandBuffer *cb, PrRhiBuffer *src, PrRhiTexture *dst); void prRhiCmdCopyBufferToImage(PrRhiCommandBuffer *cb, PrRhiBuffer *src, PrRhiTexture *dst,
PrRhiBufferImageCopyArray copies);
// ====================================================================== // ======================================================================
// Queue submission // Queue submission
@@ -282,5 +282,9 @@ void prRhiQueueSubmit(PrRhiDevice *device, PrRhiCommandBuffer *cb,
# error "Define one of: PR_RHI_VULKAN, PR_RHI_D3D12, PR_RHI_METAL" # error "Define one of: PR_RHI_VULKAN, PR_RHI_D3D12, PR_RHI_METAL"
#endif #endif
#ifdef __cplusplus
}
#endif
#endif #endif
+115 -8
View File
@@ -7,7 +7,9 @@
#ifndef PR_RHI_TYPES_H #ifndef PR_RHI_TYPES_H
#define PR_RHI_TYPES_H #define PR_RHI_TYPES_H
#include "../../src/wapp/wapp.h" #include "../../vendor/wapp/wapp.h"
#define PR_RHI_LOD_CLAMP_NONE 1000.0f
// ============================================================================ // ============================================================================
// Opaque handle types // Opaque handle types
@@ -36,6 +38,14 @@ typedef struct PrRhiSemaphore PrRhiSemaphore;
// Enums and flags // Enums and flags
// ============================================================================ // ============================================================================
typedef enum PrRhiPhysicalDeviceType {
PR_RHI_PHYSICAL_DEVICE_TYPE_OTHER = 0,
PR_RHI_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU = 1,
PR_RHI_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU = 2,
PR_RHI_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU = 3,
PR_RHI_PHYSICAL_DEVICE_TYPE_CPU = 4,
} PrRhiPhysicalDeviceType;
typedef enum PrRhiSwapchainResult { typedef enum PrRhiSwapchainResult {
PR_RHI_SWAPCHAIN_SUCCESS = 0, PR_RHI_SWAPCHAIN_SUCCESS = 0,
PR_RHI_SWAPCHAIN_OUT_OF_DATE, PR_RHI_SWAPCHAIN_OUT_OF_DATE,
@@ -96,6 +106,33 @@ typedef enum PrRhiShaderStage {
PR_RHI_SHADER_STAGE_COMPUTE = 1 << 2, PR_RHI_SHADER_STAGE_COMPUTE = 1 << 2,
} PrRhiShaderStage; } PrRhiShaderStage;
typedef enum PrRhiPipelineStage {
PR_RHI_PIPELINE_STAGE_NONE = 0,
PR_RHI_PIPELINE_STAGE_TOP_OF_PIPE = 1 << 0,
PR_RHI_PIPELINE_STAGE_TRANSFER = 1 << 1,
PR_RHI_PIPELINE_STAGE_VERTEX_SHADER = 1 << 2,
PR_RHI_PIPELINE_STAGE_FRAGMENT_SHADER = 1 << 3,
PR_RHI_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS = 1 << 4,
PR_RHI_PIPELINE_STAGE_LATE_FRAGMENT_TESTS = 1 << 5,
PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT = 1 << 6,
PR_RHI_PIPELINE_STAGE_COMPUTE_SHADER = 1 << 7,
PR_RHI_PIPELINE_STAGE_BOTTOM_OF_PIPE = 1 << 8,
} PrRhiPipelineStage;
typedef enum PrRhiAccess {
PR_RHI_ACCESS_NONE = 0,
PR_RHI_ACCESS_TRANSFER_READ = 1 << 0,
PR_RHI_ACCESS_TRANSFER_WRITE = 1 << 1,
PR_RHI_ACCESS_SHADER_READ = 1 << 2,
PR_RHI_ACCESS_SHADER_WRITE = 1 << 3,
PR_RHI_ACCESS_COLOR_ATTACHMENT_READ = 1 << 4,
PR_RHI_ACCESS_COLOR_ATTACHMENT_WRITE = 1 << 5,
PR_RHI_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ = 1 << 6,
PR_RHI_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE = 1 << 7,
PR_RHI_ACCESS_MEMORY_READ = 1 << 8,
PR_RHI_ACCESS_MEMORY_WRITE = 1 << 9,
} PrRhiAccess;
typedef enum PrRhiDescriptorType { typedef enum PrRhiDescriptorType {
PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
PR_RHI_DESCRIPTOR_TYPE_STORAGE_IMAGE, PR_RHI_DESCRIPTOR_TYPE_STORAGE_IMAGE,
@@ -212,14 +249,29 @@ typedef struct PrRhiImageMemoryBarrier {
PrRhiTexture *texture; PrRhiTexture *texture;
PrRhiImageLayout old_layout; PrRhiImageLayout old_layout;
PrRhiImageLayout new_layout; PrRhiImageLayout new_layout;
PrRhiPipelineStage src_stage_mask;
PrRhiAccess src_access_mask;
PrRhiPipelineStage dst_stage_mask;
PrRhiAccess dst_access_mask;
} PrRhiImageMemoryBarrier; } PrRhiImageMemoryBarrier;
typedef struct PrRhiBufferMemoryBarrier { typedef struct PrRhiBufferMemoryBarrier {
PrRhiBuffer *buffer; PrRhiBuffer *buffer;
u64 offset; u64 offset;
u64 size; u64 size;
PrRhiPipelineStage src_stage_mask;
PrRhiAccess src_access_mask;
PrRhiPipelineStage dst_stage_mask;
PrRhiAccess dst_access_mask;
} PrRhiBufferMemoryBarrier; } PrRhiBufferMemoryBarrier;
typedef struct PrRhiBufferImageCopy {
u64 buffer_offset;
u32 mip_level;
u32 width;
u32 height;
} PrRhiBufferImageCopy;
typedef struct PrRhiColorAttachment { typedef struct PrRhiColorAttachment {
PrRhiTexture *texture; PrRhiTexture *texture;
PrRhiImageLayout layout; PrRhiImageLayout layout;
@@ -240,8 +292,6 @@ typedef PrRhiSemaphore **PrRhiSemaphoreArray;
typedef PrRhiCommandBuffer **PrRhiCommandBufferArray; typedef PrRhiCommandBuffer **PrRhiCommandBufferArray;
typedef PrRhiDescriptorSetLayout **PrRhiDescriptorSetLayoutArray; typedef PrRhiDescriptorSetLayout **PrRhiDescriptorSetLayoutArray;
typedef PrRhiDescriptorSet **PrRhiDescriptorSetArray; typedef PrRhiDescriptorSet **PrRhiDescriptorSetArray;
typedef const char **PrRhiExtensionArray;
// Value type arrays (contiguous structs/enums) // Value type arrays (contiguous structs/enums)
typedef PrRhiPushConstantRange *PrRhiPushConstantRangeArray; typedef PrRhiPushConstantRange *PrRhiPushConstantRangeArray;
typedef PrRhiVertexInputBinding *PrRhiVertexInputBindingArray; typedef PrRhiVertexInputBinding *PrRhiVertexInputBindingArray;
@@ -255,6 +305,7 @@ typedef PrRhiDescriptorBufferInfo *PrRhiDescriptorBufferInfoArray;
typedef PrRhiImageMemoryBarrier *PrRhiImageMemoryBarrierArray; typedef PrRhiImageMemoryBarrier *PrRhiImageMemoryBarrierArray;
typedef PrRhiBufferMemoryBarrier *PrRhiBufferMemoryBarrierArray; typedef PrRhiBufferMemoryBarrier *PrRhiBufferMemoryBarrierArray;
typedef PrRhiColorAttachment *PrRhiColorAttachmentArray; typedef PrRhiColorAttachment *PrRhiColorAttachmentArray;
typedef PrRhiBufferImageCopy *PrRhiBufferImageCopyArray;
typedef struct PrRhiWriteDescriptorSet *PrRhiWriteDescriptorSetArray; typedef struct PrRhiWriteDescriptorSet *PrRhiWriteDescriptorSetArray;
// ============================================================================ // ============================================================================
@@ -264,7 +315,6 @@ typedef struct PrRhiWriteDescriptorSet *PrRhiWriteDescriptorSetArray;
typedef struct PrRhiInstanceDesc { typedef struct PrRhiInstanceDesc {
const char *app_name; const char *app_name;
u32 app_version; u32 app_version;
PrRhiExtensionArray extra_extensions;
} PrRhiInstanceDesc; } PrRhiInstanceDesc;
typedef struct PrRhiDeviceDesc { typedef struct PrRhiDeviceDesc {
@@ -302,14 +352,50 @@ typedef struct PrRhiShaderDesc {
u64 spirv_size; u64 spirv_size;
} PrRhiShaderDesc; } PrRhiShaderDesc;
typedef struct PrRhiTextureSize {
u32 width;
u32 height;
} PrRhiTextureSize;
typedef struct PrRhiPipelineLayoutDesc { typedef struct PrRhiPipelineLayoutDesc {
PrRhiDescriptorSetLayoutArray set_layouts; PrRhiDescriptorSetLayoutArray set_layouts;
PrRhiPushConstantRangeArray push_constant_ranges; PrRhiPushConstantRangeArray push_constant_ranges;
} PrRhiPipelineLayoutDesc; } PrRhiPipelineLayoutDesc;
typedef enum PrRhiPolygonMode {
PR_RHI_POLYGON_MODE_FILL = 0,
PR_RHI_POLYGON_MODE_LINE = 1,
PR_RHI_POLYGON_MODE_POINT = 2,
} PrRhiPolygonMode;
typedef enum PrRhiCullMode {
PR_RHI_CULL_MODE_NONE = 0,
PR_RHI_CULL_MODE_FRONT = 0x00000001,
PR_RHI_CULL_MODE_BACK = 0x00000002,
PR_RHI_CULL_MODE_FRONT_AND_BACK = 0x00000003,
} PrRhiCullMode;
typedef enum PrRhiFrontFace {
PR_RHI_FRONT_FACE_COUNTER_CLOCKWISE = 0,
PR_RHI_FRONT_FACE_CLOCKWISE = 1,
} PrRhiFrontFace;
typedef enum PrRhiMultisampleCount {
PR_RHI_SAMPLE_COUNT_1 = 0x00000001,
PR_RHI_SAMPLE_COUNT_2 = 0x00000002,
PR_RHI_SAMPLE_COUNT_4 = 0x00000004,
PR_RHI_SAMPLE_COUNT_8 = 0x00000008,
PR_RHI_SAMPLE_COUNT_16 = 0x00000010,
PR_RHI_SAMPLE_COUNT_32 = 0x00000020,
PR_RHI_SAMPLE_COUNT_64 = 0x00000040,
} PrRhiMultisampleCount;
typedef struct PrRhiGraphicsPipelineDesc { typedef struct PrRhiGraphicsPipelineDesc {
PrRhiShader *vertex_shader; PrRhiShader *vertex_shader;
const char *vertex_shader_entry_point;
PrRhiShader *fragment_shader; PrRhiShader *fragment_shader;
const char *fragment_shader_entry_point;
PrRhiVertexInputBindingArray vertex_bindings; PrRhiVertexInputBindingArray vertex_bindings;
PrRhiVertexAttributeArray vertex_attributes; PrRhiVertexAttributeArray vertex_attributes;
@@ -328,11 +414,22 @@ typedef struct PrRhiGraphicsPipelineDesc {
b8 dynamic_viewport; b8 dynamic_viewport;
b8 dynamic_scissor; b8 dynamic_scissor;
PrRhiPolygonMode polygon_mode;
PrRhiCullMode cull_mode;
PrRhiFrontFace front_face;
f32 line_width;
PrRhiMultisampleCount multisample_count;
PrRhiPipelineLayout *layout; PrRhiPipelineLayout *layout;
} PrRhiGraphicsPipelineDesc; } PrRhiGraphicsPipelineDesc;
typedef struct PrRhiComputePipelineDesc { typedef struct PrRhiComputePipelineDesc {
PrRhiShader *shader; PrRhiShader *shader;
const char *shader_entry_point;
PrRhiPipelineLayout *layout; PrRhiPipelineLayout *layout;
} PrRhiComputePipelineDesc; } PrRhiComputePipelineDesc;
@@ -358,15 +455,12 @@ typedef struct PrRhiFenceDesc {
b8 signaled; b8 signaled;
} PrRhiFenceDesc; } PrRhiFenceDesc;
typedef struct PrRhiCommandPoolDesc {
u32 queue_family_index;
} PrRhiCommandPoolDesc;
typedef struct PrRhiSwapchainDesc { typedef struct PrRhiSwapchainDesc {
PrRhiSurface *surface; PrRhiSurface *surface;
u32 width; u32 width;
u32 height; u32 height;
b8 has_depth; b8 has_depth;
PrRhiFormat depth_format; // PR_RHI_FORMAT_UNDEFINED = auto-pick
} PrRhiSwapchainDesc; } PrRhiSwapchainDesc;
typedef struct PrRhiSurfaceCapabilities { typedef struct PrRhiSurfaceCapabilities {
@@ -380,8 +474,21 @@ typedef struct PrRhiSurfaceCapabilities {
u32 max_height; u32 max_height;
} PrRhiSurfaceCapabilities; } PrRhiSurfaceCapabilities;
typedef struct PrRhiPhysicalDeviceProperties {
u32 api_version;
PrRhiPhysicalDeviceType device_type;
} PrRhiPhysicalDeviceProperties;
typedef u64 PrRhiDeviceAddress; typedef u64 PrRhiDeviceAddress;
// ============================================================================
// RHI context (global, owns all RHI object lifetimes)
// ============================================================================
typedef struct PrRhiContext {
WpAllocator allocator;
} PrRhiContext;
// --- Command buffer types --- // --- Command buffer types ---
typedef struct PrRhiDepthAttachment { typedef struct PrRhiDepthAttachment {
File diff suppressed because it is too large Load Diff
+92 -88
View File
@@ -8,216 +8,219 @@
#define PR_RHI_VK_H #define PR_RHI_VK_H
#include "../pr_rhi_types.h" #include "../pr_rhi_types.h"
#include <SDL3/SDL_video.h>
#include <volk/volk.h>
#include <vk_mem_alloc.h>
// ============================================================================ // ============================================================================
// Opaque struct definitions — visible only to the backend implementation. // Opaque struct definitions — visible only to the backend implementation.
// ============================================================================ // ============================================================================
#define PR_RHI_VK_PHYSICAL_DEVICE_NAME_LENGTH 256
#define PR_RHI_VK_PHYSICAL_DRIVER_INFO_LENGTH 256
struct PrRhiInstance { struct PrRhiInstance {
void *handle; // VkInstance VkInstance handle; // VkInstance
void *debug_messenger; // VkDebugUtilsMessengerEXT VkDebugUtilsMessengerEXT *debug_messenger; // VkDebugUtilsMessengerEXT
}; };
struct PrRhiPhysicalDevice { struct PrRhiPhysicalDevice {
void *handle; // VkPhysicalDevice VkPhysicalDevice handle;
PrRhiInstance *instance; PrRhiInstance *instance;
c8 device_name[PR_RHI_VK_PHYSICAL_DEVICE_NAME_LENGTH];
c8 driver_info[PR_RHI_VK_PHYSICAL_DRIVER_INFO_LENGTH];
}; };
struct PrRhiDevice { struct PrRhiDevice {
void *handle; // VkDevice VkDevice handle;
void *queue; // VkQueue VkQueue queue;
u32 queue_family_index; u32 queue_family_index;
void *allocator; // VmaAllocator VkPresentModeKHR present_mode;
VkPhysicalDevice physical_device;
VmaAllocator allocator;
}; };
struct PrRhiSurface { struct PrRhiSurface {
void *handle; // VkSurfaceKHR VkSurfaceKHR handle;
}; };
struct PrRhiSwapchain { struct PrRhiSwapchain {
PrRhiDevice *device; PrRhiDevice *device;
void *handle; // VkSwapchainKHR VkSwapchainKHR handle;
PrRhiSurface *surface;
u32 image_count; u32 image_count;
PrRhiTexture **images; PrRhiTexture **images;
PrRhiTexture *depth; PrRhiTexture *depth;
u32 format; // VkFormat VkFormat format;
VkFormat depth_format;
u32 width; u32 width;
u32 height; u32 height;
u32 current_image_index;
}; };
struct PrRhiBuffer { struct PrRhiBuffer {
void *handle; // VkBuffer VkBuffer handle;
void *allocation; // VmaAllocation VmaAllocation allocation;
u64 device_address; u64 device_address;
u64 size; u64 size;
void *mapped_data; void *mapped_data;
}; };
struct PrRhiTexture { struct PrRhiTexture {
void *image; // VkImage VkImage image;
void *view; // VkImageView VkImageView view;
void *allocation; // VmaAllocation VmaAllocation allocation;
u32 width;
u32 height;
VkFormat format;
}; };
struct PrRhiSampler { struct PrRhiSampler {
void *handle; // VkSampler VkSampler handle;
}; };
struct PrRhiShader { struct PrRhiShader {
void *handle; // VkShaderModule VkShaderModule handle;
}; };
struct PrRhiPipelineLayout { struct PrRhiPipelineLayout {
void *handle; // VkPipelineLayout VkPipelineLayout handle;
}; };
struct PrRhiPipeline { struct PrRhiPipeline {
void *handle; // VkPipeline VkPipeline handle;
}; };
struct PrRhiDescriptorSetLayout { struct PrRhiDescriptorSetLayout {
void *handle; // VkDescriptorSetLayout VkDescriptorSetLayout handle;
}; };
struct PrRhiDescriptorPool { struct PrRhiDescriptorPool {
void *handle; // VkDescriptorPool VkDescriptorPool handle;
}; };
struct PrRhiDescriptorSet { struct PrRhiDescriptorSet {
void *handle; // VkDescriptorSet VkDescriptorSet handle;
}; };
struct PrRhiCommandPool { struct PrRhiCommandPool {
void *handle; // VkCommandPool VkCommandPool handle;
}; };
struct PrRhiCommandBuffer { struct PrRhiCommandBuffer {
void *handle; // VkCommandBuffer VkCommandBuffer handle;
}; };
struct PrRhiFence { struct PrRhiFence {
void *handle; // VkFence VkFence handle;
}; };
struct PrRhiSemaphore { struct PrRhiSemaphore {
void *handle; // VkSemaphore VkSemaphore handle;
}; };
// ============================================================================ // ============================================================================
// Function declarations // Function declarations
// ============================================================================ // ============================================================================
PrRhiInstance *prRhiCreateInstanceVk(PrRhiInstanceDesc desc, WpAllocator *alloc); PrRhiInstance *prRhiCreateInstanceVk(PrRhiInstanceDesc desc);
void prRhiDestroyInstanceVk(PrRhiInstance *inst, WpAllocator *alloc); void prRhiDestroyInstanceVk(PrRhiInstance *inst);
PrRhiPhysicalDeviceArray prRhiGetPhysicalDevicesVk(PrRhiInstance *inst, const WpAllocator *scratch); PrRhiPhysicalDeviceArray prRhiGetPhysicalDevicesVk(PrRhiInstance *inst);
void prRhiGetPhysicalDeviceNameVk(PrRhiPhysicalDevice *pdev, WpStr8 *out); void prRhiGetPhysicalDeviceNameVk(PrRhiPhysicalDevice *pdev, WpStr8 *out);
void prRhiGetPhysicalDeviceDriverInfoVk(PrRhiPhysicalDevice *pdev, WpStr8 *out); void prRhiGetPhysicalDeviceDriverInfoVk(PrRhiPhysicalDevice *pdev, WpStr8 *out);
PrRhiPhysicalDeviceProperties prRhiGetPhysicalDevicePropertiesVk(PrRhiPhysicalDevice *pdev);
PrRhiSurface *prRhiCreateSurfaceVk(PrRhiInstance *inst, void *window_handle, WpAllocator *alloc); PrRhiSurface *prRhiCreateSurfaceFromWindowVk(PrRhiInstance *inst, SDL_Window *window);
void prRhiDestroySurfaceVk(PrRhiInstance *inst, PrRhiSurface *surface, WpAllocator *alloc); void prRhiDestroySurfaceVk(PrRhiInstance *inst, PrRhiSurface *surface);
PrRhiSurfaceCapabilities prRhiGetSurfaceCapabilitiesVk(PrRhiPhysicalDevice *pdev, PrRhiSurfaceCapabilities prRhiGetSurfaceCapabilitiesVk(PrRhiPhysicalDevice *pdev,
PrRhiSurface *surface); PrRhiSurface *surface);
PrRhiDevice *prRhiCreateDeviceVk(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface, PrRhiDevice *prRhiCreateDeviceVk(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface,
PrRhiDeviceDesc desc, WpAllocator *alloc); PrRhiDeviceDesc desc);
void prRhiDestroyDeviceVk(PrRhiDevice *device, WpAllocator *alloc); void prRhiDestroyDeviceVk(PrRhiDevice *device);
void prRhiDeviceWaitIdleVk(PrRhiDevice *device); void prRhiDeviceWaitIdleVk(PrRhiDevice *device);
u32 prRhiGetQueueFamilyIndexVk(PrRhiDevice *device);
PrRhiSwapchain *prRhiCreateSwapchainVk(PrRhiDevice *device, PrRhiSwapchainDesc desc, PrRhiSwapchain *prRhiCreateSwapchainVk(PrRhiDevice *device, PrRhiSwapchainDesc desc);
WpAllocator *alloc); void prRhiDestroySwapchainVk(PrRhiDevice *device, PrRhiSwapchain *swapchain);
void prRhiDestroySwapchainVk(PrRhiDevice *device, PrRhiSwapchain *swapchain, u32 prRhiGetSwapchainImageCountVk(PrRhiSwapchain *swapchain);
WpAllocator *alloc);
PrRhiSwapchainResult prRhiAcquireNextImageVk(PrRhiDevice *device, PrRhiSwapchain *swapchain, PrRhiSwapchainResult prRhiAcquireNextImageVk(PrRhiDevice *device, PrRhiSwapchain *swapchain,
PrRhiSemaphore *signal_semaphore, u32 *out_image_index); PrRhiSemaphore *signal_semaphore, u32 *out_image_index);
PrRhiSwapchainResult prRhiPresentVk(PrRhiDevice *device, PrRhiSwapchain *swapchain, PrRhiSwapchainResult prRhiPresentVk(PrRhiDevice *device, PrRhiSwapchain *swapchain,
PrRhiSemaphore *wait_semaphore); PrRhiSemaphore *wait_semaphore);
void prRhiRecreateSwapchainVk(PrRhiDevice *device, PrRhiSwapchain **swapchain, void prRhiRecreateSwapchainVk(PrRhiDevice *device, PrRhiSwapchain **swapchain,
u32 width, u32 height, WpAllocator *alloc); u32 width, u32 height);
PrRhiTexture *prRhiGetSwapchainTextureVk(PrRhiSwapchain *swapchain, u32 image_index); PrRhiTexture *prRhiGetSwapchainTextureVk(PrRhiSwapchain *swapchain, u32 image_index);
PrRhiTexture *prRhiGetSwapchainDepthTextureVk(PrRhiSwapchain *swapchain); PrRhiTexture *prRhiGetSwapchainDepthTextureVk(PrRhiSwapchain *swapchain);
PrRhiFormat prRhiGetSwapchainFormatVk(PrRhiSwapchain *swapchain);
PrRhiBuffer *prRhiCreateBufferVk(PrRhiDevice *device, PrRhiBufferDesc desc, PrRhiBuffer *prRhiCreateBufferVk(PrRhiDevice *device, PrRhiBufferDesc desc);
WpAllocator *alloc); void prRhiDestroyBufferVk(PrRhiDevice *device, PrRhiBuffer *buffer);
void prRhiDestroyBufferVk(PrRhiDevice *device, PrRhiBuffer *buffer, WpAllocator *alloc);
void *prRhiBufferMapVk(PrRhiDevice *device, PrRhiBuffer *buffer); void *prRhiBufferMapVk(PrRhiDevice *device, PrRhiBuffer *buffer);
void prRhiBufferUnmapVk(PrRhiDevice *device, PrRhiBuffer *buffer); void prRhiBufferUnmapVk(PrRhiDevice *device, PrRhiBuffer *buffer);
PrRhiDeviceAddress prRhiGetBufferDeviceAddressVk(PrRhiDevice *device, PrRhiBuffer *buffer); PrRhiDeviceAddress prRhiGetBufferDeviceAddressVk(PrRhiDevice *device, PrRhiBuffer *buffer);
PrRhiTexture *prRhiCreateTextureVk(PrRhiDevice *device, PrRhiTextureDesc desc, PrRhiTexture *prRhiCreateTextureVk(PrRhiDevice *device, PrRhiTextureDesc desc);
WpAllocator *alloc); PrRhiTexture *prRhiCreateTextureFromKtxVk(PrRhiDevice *device, const char *path,
void prRhiDestroyTextureVk(PrRhiDevice *device, PrRhiTexture *texture, PrRhiCommandPool *pool, PrRhiCommandBuffer *cb);
WpAllocator *alloc); void prRhiDestroyTextureVk(PrRhiDevice *device, PrRhiTexture *texture);
PrRhiTextureSize prRhiGetTextureSizeVk(PrRhiTexture *texture);
PrRhiSampler *prRhiCreateSamplerVk(PrRhiDevice *device, PrRhiSamplerDesc desc, PrRhiSampler *prRhiCreateSamplerVk(PrRhiDevice *device, PrRhiSamplerDesc desc);
WpAllocator *alloc); void prRhiDestroySamplerVk(PrRhiDevice *device, PrRhiSampler *sampler);
void prRhiDestroySamplerVk(PrRhiDevice *device, PrRhiSampler *sampler,
WpAllocator *alloc);
PrRhiShader *prRhiCreateShaderVk(PrRhiDevice *device, PrRhiShaderDesc desc, PrRhiShader *prRhiCreateShaderVk(PrRhiDevice *device, PrRhiShaderDesc desc);
WpAllocator *alloc); void prRhiDestroyShaderVk(PrRhiDevice *device, PrRhiShader *shader);
void prRhiDestroyShaderVk(PrRhiDevice *device, PrRhiShader *shader, WpAllocator *alloc);
PrRhiPipelineLayout *prRhiCreatePipelineLayoutVk(PrRhiDevice *device, PrRhiPipelineLayout *prRhiCreatePipelineLayoutVk(PrRhiDevice *device,
PrRhiPipelineLayoutDesc desc, PrRhiPipelineLayoutDesc desc);
WpAllocator *alloc);
void prRhiDestroyPipelineLayoutVk(PrRhiDevice *device, void prRhiDestroyPipelineLayoutVk(PrRhiDevice *device,
PrRhiPipelineLayout *layout, PrRhiPipelineLayout *layout);
WpAllocator *alloc);
PrRhiPipeline *prRhiCreateGraphicsPipelineVk(PrRhiDevice *device, PrRhiPipeline *prRhiCreateGraphicsPipelineVk(PrRhiDevice *device,
PrRhiGraphicsPipelineDesc desc, PrRhiGraphicsPipelineDesc desc);
WpAllocator *alloc);
PrRhiPipeline *prRhiCreateComputePipelineVk(PrRhiDevice *device, PrRhiPipeline *prRhiCreateComputePipelineVk(PrRhiDevice *device,
PrRhiComputePipelineDesc desc, PrRhiComputePipelineDesc desc);
WpAllocator *alloc); void prRhiDestroyPipelineVk(PrRhiDevice *device, PrRhiPipeline *pipeline);
void prRhiDestroyPipelineVk(PrRhiDevice *device, PrRhiPipeline *pipeline,
WpAllocator *alloc);
PrRhiDescriptorSetLayout *prRhiCreateDescriptorSetLayoutVk(PrRhiDevice *device, PrRhiDescriptorSetLayout *prRhiCreateDescriptorSetLayoutVk(PrRhiDevice *device,
PrRhiDescriptorSetLayoutDesc desc, PrRhiDescriptorSetLayoutDesc desc);
WpAllocator *alloc);
void prRhiDestroyDescriptorSetLayoutVk(PrRhiDevice *device, void prRhiDestroyDescriptorSetLayoutVk(PrRhiDevice *device,
PrRhiDescriptorSetLayout *layout, PrRhiDescriptorSetLayout *layout);
WpAllocator *alloc);
PrRhiDescriptorPool *prRhiCreateDescriptorPoolVk(PrRhiDevice *device, PrRhiDescriptorPool *prRhiCreateDescriptorPoolVk(PrRhiDevice *device,
PrRhiDescriptorPoolDesc desc, PrRhiDescriptorPoolDesc desc);
WpAllocator *alloc);
void prRhiDestroyDescriptorPoolVk(PrRhiDevice *device, void prRhiDestroyDescriptorPoolVk(PrRhiDevice *device,
PrRhiDescriptorPool *pool, PrRhiDescriptorPool *pool);
WpAllocator *alloc); void prRhiResetDescriptorPoolVk(PrRhiDevice *device,
PrRhiDescriptorPool *pool);
PrRhiDescriptorSet *prRhiAllocateDescriptorSetVk(PrRhiDevice *device, PrRhiDescriptorSet *prRhiAllocateDescriptorSetVk(PrRhiDevice *device,
PrRhiDescriptorPool *pool, PrRhiDescriptorPool *pool,
PrRhiDescriptorSetLayout *layout, PrRhiDescriptorSetLayout *layout,
u32 variable_count, WpAllocator *alloc); WpU32Array variable_descriptor_counts);
void prRhiFreeDescriptorSetVk(PrRhiDevice *device, PrRhiDescriptorPool *pool, void prRhiFreeDescriptorSetVk(PrRhiDevice *device, PrRhiDescriptorPool *pool,
PrRhiDescriptorSet *set, WpAllocator *alloc); PrRhiDescriptorSet *set);
void prRhiUpdateDescriptorSetVk(PrRhiDevice *device, PrRhiWriteDescriptorSetArray writes); void prRhiUpdateDescriptorSetVk(PrRhiDevice *device, PrRhiWriteDescriptorSetArray writes);
PrRhiFence *prRhiCreateFenceVk(PrRhiDevice *device, PrRhiFenceDesc desc, PrRhiFence *prRhiCreateFenceVk(PrRhiDevice *device, PrRhiFenceDesc desc);
WpAllocator *alloc); void prRhiDestroyFenceVk(PrRhiDevice *device, PrRhiFence *fence);
void prRhiDestroyFenceVk(PrRhiDevice *device, PrRhiFence *fence, WpAllocator *alloc);
void prRhiWaitForFencesVk(PrRhiDevice *device, PrRhiFenceArray fences, u32 count, void prRhiWaitForFencesVk(PrRhiDevice *device, PrRhiFenceArray fences, u32 count,
b8 wait_all, u64 timeout_ns); b8 wait_all, u64 timeout_ns);
void prRhiResetFencesVk(PrRhiDevice *device, PrRhiFenceArray fences, u32 count); void prRhiResetFencesVk(PrRhiDevice *device, PrRhiFenceArray fences, u32 count);
PrRhiSemaphore *prRhiCreateSemaphoreVk(PrRhiDevice *device, WpAllocator *alloc); PrRhiSemaphore *prRhiCreateSemaphoreVk(PrRhiDevice *device);
void prRhiDestroySemaphoreVk(PrRhiDevice *device, PrRhiSemaphore *semaphore, void prRhiDestroySemaphoreVk(PrRhiDevice *device, PrRhiSemaphore *semaphore);
WpAllocator *alloc);
PrRhiCommandPool *prRhiCreateCommandPoolVk(PrRhiDevice *device, PrRhiCommandPool *prRhiCreateCommandPoolVk(PrRhiDevice *device);
PrRhiCommandPoolDesc desc, void prRhiDestroyCommandPoolVk(PrRhiDevice *device, PrRhiCommandPool *pool);
WpAllocator *alloc);
void prRhiDestroyCommandPoolVk(PrRhiDevice *device, PrRhiCommandPool *pool,
WpAllocator *alloc);
PrRhiCommandBufferArray prRhiAllocateCommandBuffersVk(PrRhiDevice *device, PrRhiCommandPool *pool, PrRhiCommandBufferArray prRhiAllocateCommandBuffersVk(PrRhiDevice *device, PrRhiCommandPool *pool,
u32 count, WpAllocator *alloc); u32 count);
void prRhiFreeCommandBuffersVk(PrRhiDevice *device, PrRhiCommandPool *pool,
u32 count, PrRhiCommandBufferArray buffers); void prRhiFreeCommandBuffersVk(PrRhiDevice *device, PrRhiCommandPool *pool, PrRhiCommandBufferArray buffers);
void prRhiBeginCommandBufferVk(PrRhiCommandBuffer *cb); void prRhiBeginCommandBufferVk(PrRhiCommandBuffer *cb);
void prRhiEndCommandBufferVk(PrRhiCommandBuffer *cb); void prRhiEndCommandBufferVk(PrRhiCommandBuffer *cb);
@@ -243,8 +246,8 @@ void prRhiCmdBindDescriptorSetsVk(PrRhiCommandBuffer *cb, PrRhiPipelineBindPoint
void prRhiCmdPushConstantsVk(PrRhiCommandBuffer *cb, PrRhiPipelineLayout *layout, void prRhiCmdPushConstantsVk(PrRhiCommandBuffer *cb, PrRhiPipelineLayout *layout,
PrRhiShaderStage stage_flags, u32 offset, u32 size, PrRhiShaderStage stage_flags, u32 offset, u32 size,
const void *data); const void *data);
void prRhiCmdBindVertexBuffersVk(PrRhiCommandBuffer *cb, u32 first_binding, void prRhiCmdBindVertexBuffersVk(PrRhiCommandBuffer *cb, u32 first_binding, PrRhiBufferArray buffers,
PrRhiBufferArray buffers, const u64 *offsets, u32 count); WpU64Array offsets);
void prRhiCmdBindIndexBufferVk(PrRhiCommandBuffer *cb, PrRhiBuffer *buffer, u64 offset, void prRhiCmdBindIndexBufferVk(PrRhiCommandBuffer *cb, PrRhiBuffer *buffer, u64 offset,
PrRhiIndexType index_type); PrRhiIndexType index_type);
@@ -253,7 +256,8 @@ void prRhiCmdDrawVk(PrRhiCommandBuffer *cb, u32 vertex_count, u32 instance_count
void prRhiCmdDrawIndexedVk(PrRhiCommandBuffer *cb, u32 index_count, u32 instance_count, void prRhiCmdDrawIndexedVk(PrRhiCommandBuffer *cb, u32 index_count, u32 instance_count,
u32 first_index, i32 vertex_offset, u32 first_instance); u32 first_index, i32 vertex_offset, u32 first_instance);
void prRhiCmdCopyBufferToImageVk(PrRhiCommandBuffer *cb, PrRhiBuffer *src, PrRhiTexture *dst); void prRhiCmdCopyBufferToImageVk(PrRhiCommandBuffer *cb, PrRhiBuffer *src, PrRhiTexture *dst,
PrRhiBufferImageCopyArray copies);
void prRhiQueueSubmitVk(PrRhiDevice *device, PrRhiCommandBuffer *cb, void prRhiQueueSubmitVk(PrRhiDevice *device, PrRhiCommandBuffer *cb,
PrRhiSemaphore *wait_semaphore, PrRhiSemaphore *wait_semaphore,
+9 -1
View File
@@ -10,25 +10,32 @@
#define prRhiGetPhysicalDevices prRhiGetPhysicalDevicesVk #define prRhiGetPhysicalDevices prRhiGetPhysicalDevicesVk
#define prRhiGetPhysicalDeviceName prRhiGetPhysicalDeviceNameVk #define prRhiGetPhysicalDeviceName prRhiGetPhysicalDeviceNameVk
#define prRhiGetPhysicalDeviceDriverInfo prRhiGetPhysicalDeviceDriverInfoVk #define prRhiGetPhysicalDeviceDriverInfo prRhiGetPhysicalDeviceDriverInfoVk
#define prRhiCreateSurface prRhiCreateSurfaceVk #define prRhiGetPhysicalDeviceProperties prRhiGetPhysicalDevicePropertiesVk
#define prRhiCreateSurfaceFromWindow prRhiCreateSurfaceFromWindowVk
#define prRhiDestroySurface prRhiDestroySurfaceVk #define prRhiDestroySurface prRhiDestroySurfaceVk
#define prRhiGetSurfaceCapabilities prRhiGetSurfaceCapabilitiesVk
#define prRhiCreateDevice prRhiCreateDeviceVk #define prRhiCreateDevice prRhiCreateDeviceVk
#define prRhiDestroyDevice prRhiDestroyDeviceVk #define prRhiDestroyDevice prRhiDestroyDeviceVk
#define prRhiDeviceWaitIdle prRhiDeviceWaitIdleVk #define prRhiDeviceWaitIdle prRhiDeviceWaitIdleVk
#define prRhiGetQueueFamilyIndex prRhiGetQueueFamilyIndexVk
#define prRhiCreateSwapchain prRhiCreateSwapchainVk #define prRhiCreateSwapchain prRhiCreateSwapchainVk
#define prRhiDestroySwapchain prRhiDestroySwapchainVk #define prRhiDestroySwapchain prRhiDestroySwapchainVk
#define prRhiGetSwapchainImageCount prRhiGetSwapchainImageCountVk
#define prRhiAcquireNextImage prRhiAcquireNextImageVk #define prRhiAcquireNextImage prRhiAcquireNextImageVk
#define prRhiPresent prRhiPresentVk #define prRhiPresent prRhiPresentVk
#define prRhiRecreateSwapchain prRhiRecreateSwapchainVk #define prRhiRecreateSwapchain prRhiRecreateSwapchainVk
#define prRhiGetSwapchainTexture prRhiGetSwapchainTextureVk #define prRhiGetSwapchainTexture prRhiGetSwapchainTextureVk
#define prRhiGetSwapchainDepthTexture prRhiGetSwapchainDepthTextureVk #define prRhiGetSwapchainDepthTexture prRhiGetSwapchainDepthTextureVk
#define prRhiGetSwapchainFormat prRhiGetSwapchainFormatVk
#define prRhiCreateBuffer prRhiCreateBufferVk #define prRhiCreateBuffer prRhiCreateBufferVk
#define prRhiDestroyBuffer prRhiDestroyBufferVk #define prRhiDestroyBuffer prRhiDestroyBufferVk
#define prRhiBufferMap prRhiBufferMapVk #define prRhiBufferMap prRhiBufferMapVk
#define prRhiBufferUnmap prRhiBufferUnmapVk #define prRhiBufferUnmap prRhiBufferUnmapVk
#define prRhiGetBufferDeviceAddress prRhiGetBufferDeviceAddressVk #define prRhiGetBufferDeviceAddress prRhiGetBufferDeviceAddressVk
#define prRhiCreateTexture prRhiCreateTextureVk #define prRhiCreateTexture prRhiCreateTextureVk
#define prRhiCreateTextureFromKtx prRhiCreateTextureFromKtxVk
#define prRhiDestroyTexture prRhiDestroyTextureVk #define prRhiDestroyTexture prRhiDestroyTextureVk
#define prRhiGetTextureSize prRhiGetTextureSizeVk
#define prRhiCreateSampler prRhiCreateSamplerVk #define prRhiCreateSampler prRhiCreateSamplerVk
#define prRhiDestroySampler prRhiDestroySamplerVk #define prRhiDestroySampler prRhiDestroySamplerVk
#define prRhiCreateShader prRhiCreateShaderVk #define prRhiCreateShader prRhiCreateShaderVk
@@ -42,6 +49,7 @@
#define prRhiDestroyDescriptorSetLayout prRhiDestroyDescriptorSetLayoutVk #define prRhiDestroyDescriptorSetLayout prRhiDestroyDescriptorSetLayoutVk
#define prRhiCreateDescriptorPool prRhiCreateDescriptorPoolVk #define prRhiCreateDescriptorPool prRhiCreateDescriptorPoolVk
#define prRhiDestroyDescriptorPool prRhiDestroyDescriptorPoolVk #define prRhiDestroyDescriptorPool prRhiDestroyDescriptorPoolVk
#define prRhiResetDescriptorPool prRhiResetDescriptorPoolVk
#define prRhiAllocateDescriptorSet prRhiAllocateDescriptorSetVk #define prRhiAllocateDescriptorSet prRhiAllocateDescriptorSetVk
#define prRhiFreeDescriptorSet prRhiFreeDescriptorSetVk #define prRhiFreeDescriptorSet prRhiFreeDescriptorSetVk
#define prRhiUpdateDescriptorSet prRhiUpdateDescriptorSetVk #define prRhiUpdateDescriptorSet prRhiUpdateDescriptorSetVk
+2
View File
@@ -0,0 +1,2 @@
#define VMA_IMPLEMENTATION
#include <vk_mem_alloc.h>
Vendored Submodule
+1
Submodule src/vendor/ktx added at 4d6fc70eaf
@@ -7,14 +7,14 @@
#include "../../common/aliases/aliases.h" #include "../../common/aliases/aliases.h"
#include <stddef.h> #include <stddef.h>
#define _array_header(ARRAY) (WpArrayHeader *)(wpMiscUtilsOffsetPointer(ARRAY, (i64)sizeof(WpArrayHeader) * -1)) #define _arrayHeader(ARRAY) (WpArrayHeader *)(wpMiscUtilsOffsetPointer(ARRAY, (i64)sizeof(WpArrayHeader) * -1))
wp_persist inline void _array_validate(const WpArray array, u64 item_size); wp_persist inline void _arrayValidate(const WpArray array, u64 item_size);
u64 _arrayCount(WpArray array) { u64 _arrayCount(WpArray array) {
wpDebugAssert(array != NULL, "`array` should not be NULL"); wpDebugAssert(array != NULL, "`array` should not be NULL");
WpArrayHeader *header = _array_header(array); WpArrayHeader *header = _arrayHeader(array);
wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array"); wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array");
return header->count; return header->count;
@@ -23,7 +23,7 @@ u64 _arrayCount(WpArray array) {
u64 _arrayCapacity(WpArray array) { u64 _arrayCapacity(WpArray array) {
wpDebugAssert(array != NULL, "`array` should not be NULL"); wpDebugAssert(array != NULL, "`array` should not be NULL");
WpArrayHeader *header = _array_header(array); WpArrayHeader *header = _arrayHeader(array);
wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array"); wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array");
return header->capacity; return header->capacity;
@@ -32,7 +32,7 @@ u64 _arrayCapacity(WpArray array) {
u64 _arrayItemSize(WpArray array) { u64 _arrayItemSize(WpArray array) {
wpDebugAssert(array != NULL, "`array` should not be NULL"); wpDebugAssert(array != NULL, "`array` should not be NULL");
WpArrayHeader *header = _array_header(array); WpArrayHeader *header = _arrayHeader(array);
wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array"); wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array");
return header->item_size; return header->item_size;
@@ -41,7 +41,7 @@ u64 _arrayItemSize(WpArray array) {
void _arraySetCount(WpArray array, u64 count) { void _arraySetCount(WpArray array, u64 count) {
wpDebugAssert(array != NULL, "`array` should not be NULL"); wpDebugAssert(array != NULL, "`array` should not be NULL");
WpArrayHeader *header = _array_header(array); WpArrayHeader *header = _arrayHeader(array);
wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array"); wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array");
header->count = count; header->count = count;
@@ -49,9 +49,9 @@ void _arraySetCount(WpArray array, u64 count) {
void *_arrayGet(WpArray array, u64 index, u64 item_size) { void *_arrayGet(WpArray array, u64 index, u64 item_size) {
wpRuntimeAssert(array != NULL, "`array` should not be NULL"); wpRuntimeAssert(array != NULL, "`array` should not be NULL");
_array_validate(array, item_size); _arrayValidate(array, item_size);
WpArrayHeader *header = _array_header(array); WpArrayHeader *header = _arrayHeader(array);
wpRuntimeAssert(index < header->count, "`index` is out of bounds"); wpRuntimeAssert(index < header->count, "`index` is out of bounds");
return wpMiscUtilsOffsetPointer(array, header->item_size * index); return wpMiscUtilsOffsetPointer(array, header->item_size * index);
@@ -60,15 +60,15 @@ void *_arrayGet(WpArray array, u64 index, u64 item_size) {
void _arraySet(WpArray array, u64 index, void *value, u64 item_size) { void _arraySet(WpArray array, u64 index, void *value, u64 item_size) {
void *item = _arrayGet(array, index, item_size); void *item = _arrayGet(array, index, item_size);
WpArrayHeader *header = _array_header(array); WpArrayHeader *header = _arrayHeader(array);
memcpy(item, value, header->item_size); memcpy(item, value, header->item_size);
} }
void _arrayAppendCapped(WpArray array, void *value, u64 item_size) { void _arrayAppendCapped(WpArray array, void *value, u64 item_size) {
wpRuntimeAssert(array != NULL, "`array` should not be NULL"); wpRuntimeAssert(array != NULL, "`array` should not be NULL");
_array_validate(array, item_size); _arrayValidate(array, item_size);
WpArrayHeader *header = _array_header(array); WpArrayHeader *header = _arrayHeader(array);
if (header->count >= header->capacity) { return; } if (header->count >= header->capacity) { return; }
u64 index = (header->count)++; u64 index = (header->count)++;
@@ -77,11 +77,11 @@ void _arrayAppendCapped(WpArray array, void *value, u64 item_size) {
void _arrayExtendCappend(WpArray dst, const WpArray src, u64 item_size) { void _arrayExtendCappend(WpArray dst, const WpArray src, u64 item_size) {
wpRuntimeAssert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL"); wpRuntimeAssert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");
_array_validate(dst, item_size); _arrayValidate(dst, item_size);
_array_validate(src, item_size); _arrayValidate(src, item_size);
WpArrayHeader *src_header = _array_header(src); WpArrayHeader *src_header = _arrayHeader(src);
WpArrayHeader *dst_header = _array_header(dst); WpArrayHeader *dst_header = _arrayHeader(dst);
u64 remaining_capacity = dst_header->capacity - dst_header->count; u64 remaining_capacity = dst_header->capacity - dst_header->count;
u64 copy_count = src_header->count < remaining_capacity ? src_header->count : remaining_capacity; u64 copy_count = src_header->count < remaining_capacity ? src_header->count : remaining_capacity;
@@ -92,13 +92,13 @@ void _arrayExtendCappend(WpArray dst, const WpArray src, u64 item_size) {
void _arrayCopyCapped(WpArray dst, const WpArray src, u64 item_size) { void _arrayCopyCapped(WpArray dst, const WpArray src, u64 item_size) {
wpRuntimeAssert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL"); wpRuntimeAssert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");
_array_validate(dst, item_size); _arrayValidate(dst, item_size);
_array_validate(src, item_size); _arrayValidate(src, item_size);
_arrayClear(dst, item_size); _arrayClear(dst, item_size);
WpArrayHeader *src_header = _array_header(src); WpArrayHeader *src_header = _arrayHeader(src);
WpArrayHeader *dst_header = _array_header(dst); WpArrayHeader *dst_header = _arrayHeader(dst);
u64 copy_count = src_header->count < dst_header->capacity ? src_header->count : dst_header->capacity; u64 copy_count = src_header->count < dst_header->capacity ? src_header->count : dst_header->capacity;
memcpy((void *)dst, (void *)src, copy_count * src_header->item_size); memcpy((void *)dst, (void *)src, copy_count * src_header->item_size);
dst_header->count = copy_count; dst_header->count = copy_count;
@@ -107,11 +107,11 @@ void _arrayCopyCapped(WpArray dst, const WpArray src, u64 item_size) {
WpArray _arrayAppendAlloc(const WpAllocator *allocator, WpArray array, void *value, WpArray _arrayAppendAlloc(const WpAllocator *allocator, WpArray array, void *value,
WpArrayInitFlags flags, u64 item_size) { WpArrayInitFlags flags, u64 item_size) {
wpRuntimeAssert(allocator != NULL && array != NULL, "`allocator` and `array` should not be NULL"); wpRuntimeAssert(allocator != NULL && array != NULL, "`allocator` and `array` should not be NULL");
_array_validate(array, item_size); _arrayValidate(array, item_size);
WpArray output = array; WpArray output = array;
WpArrayHeader *header = _array_header(array); WpArrayHeader *header = _arrayHeader(array);
if (header->count >= header->capacity) { if (header->count >= header->capacity) {
u64 new_capacity = wpMiscUtilsU64RoundUpPow2(header->capacity * 2); u64 new_capacity = wpMiscUtilsU64RoundUpPow2(header->capacity * 2);
output = (WpArray )_arrayAllocCapacity(allocator, new_capacity, flags, output = (WpArray )_arrayAllocCapacity(allocator, new_capacity, flags,
@@ -136,13 +136,13 @@ RETURN_ARRAY_APPEND_ALLOC:
WpArray _arrayExtendAlloc(const WpAllocator *allocator, WpArray dst, const WpArray src, WpArray _arrayExtendAlloc(const WpAllocator *allocator, WpArray dst, const WpArray src,
WpArrayInitFlags flags, u64 item_size) { WpArrayInitFlags flags, u64 item_size) {
wpRuntimeAssert(allocator != NULL && dst != NULL && src != NULL, "`allocator`, `dst` and `src` should not be NULL"); wpRuntimeAssert(allocator != NULL && dst != NULL && src != NULL, "`allocator`, `dst` and `src` should not be NULL");
_array_validate(dst, item_size); _arrayValidate(dst, item_size);
_array_validate(src, item_size); _arrayValidate(src, item_size);
WpArray output = dst; WpArray output = dst;
WpArrayHeader *src_header = _array_header(src); WpArrayHeader *src_header = _arrayHeader(src);
WpArrayHeader *dst_header = _array_header(dst); WpArrayHeader *dst_header = _arrayHeader(dst);
u64 remaining_capacity = dst_header->capacity - dst_header->count; u64 remaining_capacity = dst_header->capacity - dst_header->count;
if (src_header->count >= remaining_capacity) { if (src_header->count >= remaining_capacity) {
u64 new_capacity = wpMiscUtilsU64RoundUpPow2(dst_header->capacity * 2); u64 new_capacity = wpMiscUtilsU64RoundUpPow2(dst_header->capacity * 2);
@@ -168,13 +168,13 @@ RETURN_ARRAY_EXTEND_ALLOC:
WpArray _arrayCopyAlloc(const WpAllocator *allocator, WpArray dst, const WpArray src, WpArray _arrayCopyAlloc(const WpAllocator *allocator, WpArray dst, const WpArray src,
WpArrayInitFlags flags, u64 item_size) { WpArrayInitFlags flags, u64 item_size) {
wpRuntimeAssert(allocator != NULL && dst != NULL && src != NULL, "`allocator`, `dst` and `src` should not be NULL"); wpRuntimeAssert(allocator != NULL && dst != NULL && src != NULL, "`allocator`, `dst` and `src` should not be NULL");
_array_validate(dst, item_size); _arrayValidate(dst, item_size);
_array_validate(src, item_size); _arrayValidate(src, item_size);
WpArray output = dst; WpArray output = dst;
WpArrayHeader *src_header = _array_header(src); WpArrayHeader *src_header = _arrayHeader(src);
WpArrayHeader *dst_header = _array_header(dst); WpArrayHeader *dst_header = _arrayHeader(dst);
if (src_header->count >= dst_header->capacity) { if (src_header->count >= dst_header->capacity) {
u64 new_capacity = wpMiscUtilsU64RoundUpPow2(dst_header->capacity * 2); u64 new_capacity = wpMiscUtilsU64RoundUpPow2(dst_header->capacity * 2);
output = (WpArray )_arrayAllocCapacity(allocator, new_capacity, output = (WpArray )_arrayAllocCapacity(allocator, new_capacity,
@@ -197,9 +197,9 @@ RETURN_ARRAY_COPY_ALLOC:
void *_arrayPop(WpArray array, u64 item_size) { void *_arrayPop(WpArray array, u64 item_size) {
wpRuntimeAssert(array != NULL, "`array` should not be NULL"); wpRuntimeAssert(array != NULL, "`array` should not be NULL");
_array_validate(array, item_size); _arrayValidate(array, item_size);
WpArrayHeader *header = _array_header(array); WpArrayHeader *header = _arrayHeader(array);
if (header->count == 0) { return NULL; } if (header->count == 0) { return NULL; }
u64 index = header->count - 1; u64 index = header->count - 1;
@@ -210,9 +210,9 @@ void *_arrayPop(WpArray array, u64 item_size) {
void _arrayClear(WpArray array, u64 item_size) { void _arrayClear(WpArray array, u64 item_size) {
wpRuntimeAssert(array != NULL, "`array` should not be NULL"); wpRuntimeAssert(array != NULL, "`array` should not be NULL");
_array_validate(array, item_size); _arrayValidate(array, item_size);
WpArrayHeader *header = _array_header(array); WpArrayHeader *header = _arrayHeader(array);
header->count = 0; header->count = 0;
} }
@@ -259,8 +259,18 @@ WpArray _arrayFromPreallocatedBuffer(void *buffer, u64 buffer_size, WpArrayInitF
return output; return output;
} }
wp_persist inline void _array_validate(const WpArray array, u64 item_size) { void _arrayDealloc(const WpAllocator *allocator, WpArray *array, u64 item_size) {
WpArrayHeader *header = _array_header(array); wpRuntimeAssert(allocator != NULL, "`allocator` should not be NULL");
u64 capacity = wpArrayCapacity(*array);
u64 allocation_size = _arrayCalcAllocSize(capacity, item_size);
void *header = (void *)_arrayHeader(*array);
wpMemAllocatorFree(allocator, &header, allocation_size);
*array = NULL;
}
wp_persist inline void _arrayValidate(const WpArray array, u64 item_size) {
WpArrayHeader *header = _arrayHeader(array);
wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array"); wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array");
wpRuntimeAssert(item_size == header->item_size, "Invalid item type provided"); wpRuntimeAssert(item_size == header->item_size, "Invalid item type provided");
} }
@@ -176,6 +176,10 @@ typedef enum {
((TYPE *)_arrayAllocCapacity(ALLOCATOR_PTR, CAPACITY, FLAGS, sizeof(TYPE))) ((TYPE *)_arrayAllocCapacity(ALLOCATOR_PTR, CAPACITY, FLAGS, sizeof(TYPE)))
#define wpArrayFromPreallcatedBuffer(TYPE, BUFFER, BUFFER_SIZE) \ #define wpArrayFromPreallcatedBuffer(TYPE, BUFFER, BUFFER_SIZE) \
((TYPE *)_array_from_preallcated_buffer(BUFFER, BUFFER_SIZE, sizeof(TYPE))) ((TYPE *)_array_from_preallcated_buffer(BUFFER, BUFFER_SIZE, sizeof(TYPE)))
// Only needed for allocators like malloc where each allocation has to be freed on its own.
// No need to use it for allocators like Arena.
#define wpArrayDealloc(TYPE, ALLOCATOR_PTR, ARRAY_DPTR) \
(_arrayDealloc(ALLOCATOR_PTR, (WpArray *)ARRAY_DPTR, sizeof(TYPE)))
typedef struct WpArrayHeader WpArrayHeader; typedef struct WpArrayHeader WpArrayHeader;
@@ -208,6 +212,7 @@ WpArray _arrayAllocCapacity(const WpAllocator *allocator, u64 capacity, WpArrayI
u64 item_size); u64 item_size);
WpArray _arrayFromPreallocatedBuffer(void *buffer, u64 buffer_size, WpArrayInitFlags flags, WpArray _arrayFromPreallocatedBuffer(void *buffer, u64 buffer_size, WpArrayInitFlags flags,
u64 item_size); u64 item_size);
void _arrayDealloc(const WpAllocator *allocator, WpArray *array, u64 item_size);
#ifdef WP_PLATFORM_CPP #ifdef WP_PLATFORM_CPP
END_C_LINKAGE END_C_LINKAGE
@@ -6,7 +6,7 @@
#include <stdlib.h> #include <stdlib.h>
void *wpMemAllocatorAlloc(const WpAllocator *allocator, u64 size) { void *wpMemAllocatorAlloc(const WpAllocator *allocator, u64 size) {
wpDebugAssert(allocator != NULL && (allocator->alloc) != NULL, "`allocator` and `allocator->alloc` should not be NULL"); wpDebugAssert(allocator != NULL, "`allocator` should not be NULL");
if (!wpMemAllocatorOpSupported(allocator, WP_MEM_OP_ALLOC)) { if (!wpMemAllocatorOpSupported(allocator, WP_MEM_OP_ALLOC)) {
return NULL; return NULL;
@@ -16,7 +16,7 @@ void *wpMemAllocatorAlloc(const WpAllocator *allocator, u64 size) {
} }
void *wpMemAllocatorAllocAligned(const WpAllocator *allocator, u64 size, u64 alignment) { void *wpMemAllocatorAllocAligned(const WpAllocator *allocator, u64 size, u64 alignment) {
wpDebugAssert(allocator != NULL && (allocator->alloc_aligned) != NULL, "`allocator` and `allocator->alloc_aligned` should not be NULL"); wpDebugAssert(allocator != NULL, "`allocator` should not be NULL");
if (!wpMemAllocatorOpSupported(allocator, WP_MEM_OP_ALLOC_ALIGNED)) { if (!wpMemAllocatorOpSupported(allocator, WP_MEM_OP_ALLOC_ALIGNED)) {
return NULL; return NULL;
@@ -26,7 +26,7 @@ void *wpMemAllocatorAllocAligned(const WpAllocator *allocator, u64 size, u64 ali
} }
void *wpMemAllocatorRealloc(const WpAllocator *allocator, void *ptr, u64 old_size, u64 new_size) { void *wpMemAllocatorRealloc(const WpAllocator *allocator, void *ptr, u64 old_size, u64 new_size) {
wpDebugAssert(allocator != NULL && (allocator->realloc) != NULL, "`allocator` and `allocator->realloc` should not be NULL"); wpDebugAssert(allocator != NULL, "`allocator` should not be NULL");
if (!wpMemAllocatorOpSupported(allocator, WP_MEM_OP_REALLOC)) { if (!wpMemAllocatorOpSupported(allocator, WP_MEM_OP_REALLOC)) {
return NULL; return NULL;
@@ -37,7 +37,7 @@ void *wpMemAllocatorRealloc(const WpAllocator *allocator, void *ptr, u64 old_siz
void *wpMemAllocatorReallocAligned(const WpAllocator *allocator, void *ptr, u64 old_size, void *wpMemAllocatorReallocAligned(const WpAllocator *allocator, void *ptr, u64 old_size,
u64 new_size, u64 alignment) { u64 new_size, u64 alignment) {
wpDebugAssert(allocator != NULL && (allocator->realloc_aligned) != NULL, "`allocator` and `allocator->realloc_aligned` should not be NULL"); wpDebugAssert(allocator != NULL, "`allocator` should not be NULL");
if (!wpMemAllocatorOpSupported(allocator, WP_MEM_OP_REALLOC_ALIGNED)) { if (!wpMemAllocatorOpSupported(allocator, WP_MEM_OP_REALLOC_ALIGNED)) {
return NULL; return NULL;
@@ -47,7 +47,7 @@ void *wpMemAllocatorReallocAligned(const WpAllocator *allocator, void *ptr, u64
} }
void wpMemAllocatorFree(const WpAllocator *allocator, void **ptr, u64 size) { void wpMemAllocatorFree(const WpAllocator *allocator, void **ptr, u64 size) {
wpDebugAssert(allocator != NULL && (allocator->free) != NULL, "`allocator` and `allocator->free` should not be NULL"); wpDebugAssert(allocator != NULL, "`allocator` should not be NULL");
if (!wpMemAllocatorOpSupported(allocator, WP_MEM_OP_FREE)) { if (!wpMemAllocatorOpSupported(allocator, WP_MEM_OP_FREE)) {
return; return;
@@ -88,6 +88,11 @@ typedef WpQueue WpStr8Queue;
#define wpQueuePop(TYPE, QUEUE_PTR) ( \ #define wpQueuePop(TYPE, QUEUE_PTR) ( \
(TYPE *)_queuePop(QUEUE_PTR, sizeof(TYPE)) \ (TYPE *)_queuePop(QUEUE_PTR, sizeof(TYPE)) \
) )
#define wpQueueDealloc(TYPE, ALLOCATOR_PTR, QUEUE_PTR) \
(wpArrayDealloc(TYPE, ALLOCATOR_PTR, &((QUEUE_PTR)->items)), \
(QUEUE_PTR)->front = 0, \
(QUEUE_PTR)->back = 0, \
(QUEUE_PTR)->count = 0)
void _queuePush(WpQueue *queue, void *item, u64 item_size); void _queuePush(WpQueue *queue, void *item, u64 item_size);
WpQueue *_queuePushAlloc(const WpAllocator *allocator, WpQueue *queue, void *item, u64 item_size); WpQueue *_queuePushAlloc(const WpAllocator *allocator, WpQueue *queue, void *item, u64 item_size);
View File
View File

Some files were not shown because too many files have changed in this diff Show More