Compare commits
49 Commits
1325902f14
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e6f051955 | |||
| e59865bff5 | |||
| e206e4647b | |||
| a0b7c0672a | |||
| 3d4f34c531 | |||
| ebd1801883 | |||
| f5ff6c70ea | |||
| 30408ff244 | |||
| 618f09689d | |||
| 4bbd7dfe01 | |||
| cc2d0cea6e | |||
| 3c1997e2b1 | |||
| 4b08866fc6 | |||
| 96c89dcb86 | |||
| bb6d2eae14 | |||
| 45a34bb151 | |||
| bffe9b8174 | |||
| 943f00345c | |||
| c8680f06c2 | |||
| 1c7a7f6c46 | |||
| 7bd1d9f701 | |||
| 49aba1eb3c | |||
| cb3ef2be1c | |||
| 5a26bf54c8 | |||
| a073dec6a0 | |||
| 7819f2abc3 | |||
| 3e1e09c974 | |||
| 0d1c5b84f2 | |||
| 5bf0ba40ca | |||
| 26f17628a4 | |||
| dc2fc22462 | |||
| 867b00279a | |||
| 03d1d728f6 | |||
| 79c5d368bf | |||
| 1e0c195f42 | |||
| 0f00ea9586 | |||
| fda56686a0 | |||
| 09eec850fa | |||
| f822defd1e | |||
| 10b5e27b5e | |||
| e9e9e624ca | |||
| d620234609 | |||
| f5b9912d12 | |||
| aa52455190 | |||
| f190656c3d | |||
| bc64619b43 | |||
| bbe5fcdf4c | |||
| ba7f2bedf1 | |||
| 25249b5e1e |
@@ -1,5 +1,13 @@
|
||||
build
|
||||
compile_commands.json
|
||||
.vscode
|
||||
*.dSYM
|
||||
assets/shaders
|
||||
scratchpad/**
|
||||
!scratchpad/**/
|
||||
!scratchpad/**/*.h
|
||||
!scratchpad/**/*.hh
|
||||
!scratchpad/**/*.hpp
|
||||
!scratchpad/**/*.c
|
||||
!scratchpad/**/*.cc
|
||||
!scratchpad/**/*.cpp
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "src/vendor/ktx"]
|
||||
path = src/vendor/ktx
|
||||
url = https://github.com/KhronosGroup/KTX-Software
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: prism-dag
|
||||
description: DAG / graph patterns for Prism — adjacency list rules, arena allocation, Kahn's algorithm, flat buffer layout
|
||||
license: MIT
|
||||
compatibility: opencode
|
||||
metadata:
|
||||
domain: core
|
||||
---
|
||||
## What I do
|
||||
|
||||
Captures the conventions for Prism's directed acyclic graph (DAG) implementation: how edges are stored, how the graph is validated, and how to avoid common pitfalls.
|
||||
|
||||
## When to use me
|
||||
|
||||
Use this when working on graph/DAG structures (`pr_graph.h`, `pr_graph.c`, or `scratchpad/dag.c`), adding new node types, or modifying the topological sort / cycle detection logic.
|
||||
|
||||
## Conventions
|
||||
|
||||
### Adjacency lists — separate edge nodes
|
||||
|
||||
Adjacency list nodes must be **separately allocated from the vertex array**. Never use the vertex struct itself as a linked-list node in another vertex's adjacency chain — that shares the `next` pointer between two roles and corrupts the graph.
|
||||
|
||||
```c
|
||||
// correct — per-edge copy on the arena
|
||||
static void addEdge(PrGraph *g, const WpAllocator *alloc, u64 from, u64 to) {
|
||||
PrVertex *src = &g->vertices[from];
|
||||
PrVertex *dst = wpMemAllocatorAlloc(alloc, sizeof(PrVertex));
|
||||
if (!dst) { /* handle OOM */ return; }
|
||||
dst->id = g->vertices[to].id;
|
||||
dst->value = g->vertices[to].value;
|
||||
dst->next = src->next;
|
||||
src->next = dst;
|
||||
}
|
||||
|
||||
// wrong — reuses the destination vertex as the list node
|
||||
static void addEdge_bad(PrGraph *g, u64 from, u64 to) {
|
||||
PrVertex *src = &g->vertices[from];
|
||||
PrVertex *dest = &g->vertices[to];
|
||||
dest->next = src->next;
|
||||
src->next = dest; // overwrites dest->next used elsewhere
|
||||
}
|
||||
```
|
||||
|
||||
Arena bump allocation (`wpMemAllocatorAlloc`) is the natural fit for edge nodes. Always NULL-check the result — arena allocators can fail if the backing buffer is exhausted.
|
||||
|
||||
### Flat buffer layout
|
||||
|
||||
For hot-path graph evaluation, prefer SoA layouts and keep the DAG in contiguous arrays rather than individually allocated linked structures.
|
||||
|
||||
### Cycle detection
|
||||
|
||||
Use Kahn's algorithm integrated into `prGraphAddEdge` for cycle detection with rollback. When an edge would create a cycle, the edge is not added and the function returns an error.
|
||||
|
||||
### Memory management
|
||||
|
||||
Stack-allocate where possible; pass `WpAllocator *` explicitly for heap allocations.
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
name: prism-rhi
|
||||
description: RHI (Rendering Hardware Interface) patterns — compile-time dispatch, by-value desc structs, wapp array aliases, backend file layout
|
||||
license: MIT
|
||||
compatibility: opencode
|
||||
metadata:
|
||||
domain: rendering
|
||||
---
|
||||
## What I do
|
||||
|
||||
Captures the RHI conventions for Prism: how backends are dispatched, how descriptor structs and arrays are handled, and how the files are organized.
|
||||
|
||||
## When to use me
|
||||
|
||||
Use this when working on any file in `src/prism/rhi/`, or when creating a new backend (Vulkan, D3D12, Metal).
|
||||
|
||||
## 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 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:
|
||||
|
||||
```c
|
||||
#if defined(PR_RHI_VULKAN)
|
||||
# include "vulkan/pr_rhi_vk_aliases.h"
|
||||
#elif defined(PR_RHI_D3D12)
|
||||
# include "d3d12/pr_rhi_d3d12_aliases.h"
|
||||
#elif defined(PR_RHI_METAL)
|
||||
# include "metal/pr_rhi_metal_aliases.h"
|
||||
#else
|
||||
# error "Define one of: PR_RHI_VULKAN, PR_RHI_D3D12, PR_RHI_METAL"
|
||||
#endif
|
||||
```
|
||||
|
||||
Each `_aliases.h` file maps generic names to backend-specific names:
|
||||
|
||||
```c
|
||||
#define prRhiCreateDevice prRhiCreateDeviceVk
|
||||
#define prRhiCreateSwapchain prRhiCreateSwapchainVk
|
||||
#define prRhiCreateBuffer prRhiCreateBufferVk
|
||||
// …
|
||||
```
|
||||
|
||||
Backend implementations are suffixed with the backend name: `pr_rhi_vk_device.c`, `pr_rhi_vk_swapchain.c`, etc.
|
||||
|
||||
### Desc structs
|
||||
|
||||
All descriptor structs are passed **by value**, not `const *`:
|
||||
|
||||
```c
|
||||
// correct
|
||||
PrRhiDevice *prRhiCreateDevice(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface, PrRhiDeviceDesc desc);
|
||||
|
||||
// wrong
|
||||
PrRhiDevice *prRhiCreateDevice(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface, const PrRhiDeviceDesc *desc);
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```
|
||||
src/prism/rhi/
|
||||
├── 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)
|
||||
├── vulkan/
|
||||
│ ├── 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
|
||||
│ └── profiles/ ← generated Vulkan Profiles library
|
||||
├── d3d12/
|
||||
│ └── …
|
||||
└── metal/
|
||||
└── …
|
||||
```
|
||||
|
||||
@@ -9,7 +9,7 @@ are rendered via GPU shaders.
|
||||
- **Language**: C11 / C++11 (dual-mode, like `src/wapp/`)
|
||||
- **GPU API**: Vulkan, abstracted behind a Rendering Hardware Interface (RHI)
|
||||
- **Shading language**: Slang, stored in external `.slang` files under `src/shaders/`
|
||||
- **Build**: TBD — either a standalone shell build script or a `justfile` (Just)
|
||||
- **Build**: `justfile` (Just) as a task runner
|
||||
- **Dependencies**: `src/wapp/` (local utility library, already vendored)
|
||||
|
||||
## Coding Conventions
|
||||
@@ -34,25 +34,31 @@ All code follows the patterns established in `src/wapp/`. The project prefix is
|
||||
|
||||
### Formatting
|
||||
|
||||
- **Indentation**: tabs (no spaces). Tab width is a viewer preference.
|
||||
- **Braces**: always required after `if`, `else`, `for`, `while`, `do` — even
|
||||
when the body is a single statement. This avoids ambiguity and makes diffs
|
||||
cleaner.
|
||||
- **Tabs for indentation**, 8-column tab width.
|
||||
- **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"); }
|
||||
|
||||
```c
|
||||
// correct
|
||||
if (condition) {
|
||||
do_thing();
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
process(i);
|
||||
}
|
||||
|
||||
// wrong — no braces, spaces instead of tabs
|
||||
if (condition)
|
||||
do_thing();
|
||||
```
|
||||
// wrong
|
||||
if (!buffer) return;
|
||||
if (!texture) _abort("alloc failed");
|
||||
```
|
||||
- **Pointers**: `*` against the name, not the type (`PrRhiBuffer *buf`, not `PrRhiBuffer* buf`).
|
||||
- **Line width**: 120 columns.
|
||||
- **Continuation lines** align to the opening parenthesis.
|
||||
- **Return-type alignment**: Within each `// =====` section, align function
|
||||
declaration names so the first letter of every function occupies the same
|
||||
column. For pointer return types, place `*` directly against the function
|
||||
name (no space) and put all alignment padding between the type name and `*`.
|
||||
```c
|
||||
// correct — * against fn name, padding before *
|
||||
PrRhiSwapchain *prRhiCreateSwapchain(…);
|
||||
void prRhiDestroySwapchain(…);
|
||||
PrRhiSwapchainResult prRhiAcquireNextImage(…);
|
||||
```
|
||||
|
||||
### Storage qualifiers
|
||||
|
||||
@@ -88,6 +94,12 @@ void prNodeDestroy(PrNode *n, PrAllocator *alloc);
|
||||
Use wapp allocators (`WpAllocator`, arena-based). Stack-allocate where
|
||||
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
|
||||
PrGraph *prGraphCreate(PrAllocator *alloc);
|
||||
void prGraphDestroy(PrGraph *g, PrAllocator *alloc);
|
||||
@@ -121,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)
|
||||
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
|
||||
|
||||
Adjacency list nodes must be **separately allocated from the vertex array**.
|
||||
@@ -167,12 +197,44 @@ can fail if the backing buffer is exhausted.
|
||||
passing a runtime variable triggers undefined behaviour and compiler warnings.
|
||||
Use `wpArrayAllocCapacity` with an arena allocator for runtime sizes.
|
||||
|
||||
Initialise arrays with `WP_ARRAY_INIT_FILLED` to set `count = capacity`
|
||||
immediately, allowing direct indexing.
|
||||
Always use typed array aliases (`WpU64Array`, `PrNodeIdArray`, etc.) rather
|
||||
than raw pointers when declaring array variables. Follow the existing typedef
|
||||
pattern in the module (`typedef Type *TypeArray`).
|
||||
|
||||
Typedef pattern:
|
||||
- Opaque handles use `**` (pointer-to-pointer)
|
||||
- Value types use `*` (contiguous block)
|
||||
```c
|
||||
typedef PrRhiBuffer **PrRhiBufferArray; // opaque handles → **
|
||||
typedef PrRhiColorAttachment *PrRhiColorAttachmentArray; // value types → *
|
||||
```
|
||||
Group opaque handle arrays first, value type arrays second, separated by a
|
||||
blank line.
|
||||
|
||||
Use named init flags (`WP_ARRAY_INIT_NONE`, `WP_ARRAY_INIT_FILLED`) instead of
|
||||
bare `0` — they make the initialisation policy explicit.
|
||||
|
||||
- `WP_ARRAY_INIT_FILLED`: sets `count = capacity` on allocation. Required when
|
||||
you plan to index into the array directly (not via append/push), since the
|
||||
array's `count` must reflect valid elements for any downstream use.
|
||||
- `WP_ARRAY_INIT_NONE`: leaves `count = 0`. Use when you'll fill the array
|
||||
incrementally via `wpArrayAppendCapped` / `wpArrayAppendAlloc`.
|
||||
|
||||
Use `wpArrayCapacity(arr)`, `wpArrayCount(arr)`, `wpArraySetCount(arr, n)` to
|
||||
query and control array state rather than computing sizes manually.
|
||||
|
||||
### Local/scratch arenas
|
||||
|
||||
For function-local scratch allocations, use `wpMemArenaAllocatorInitZero` with a
|
||||
fixed size rather than a stack buffer + `InitWithBuffer`:
|
||||
|
||||
```c
|
||||
WpAllocator scratch = wpMemArenaAllocatorInitZero(KiB(16));
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Save research notes and implementation plans as markdown in `documents/`:
|
||||
Save research notes, implementation plans and session logs as markdown in `documents/`:
|
||||
|
||||
```
|
||||
documents/
|
||||
@@ -180,10 +242,24 @@ documents/
|
||||
├── RENDERING_HARDWARE_INTERFACE.md
|
||||
├── NODE_SYSTEM.md
|
||||
├── ROADMAP.md
|
||||
└── research/
|
||||
└── vulkan-baseline.md
|
||||
├── research/
|
||||
│ └── <topic>.md
|
||||
└── session-logs/
|
||||
└── YYYY-MM-DD.md
|
||||
```
|
||||
|
||||
At the start of each new session, read the previous session logs to understand what
|
||||
we've implemented so far
|
||||
|
||||
## Skills
|
||||
|
||||
Domain-specific conventions are stored as skills in `.opencode/skills/<name>/SKILL.md`.
|
||||
These are loaded on-demand by the AI agent when a task matches their description,
|
||||
keeping AGENTS.md lean.
|
||||
|
||||
- **`prism-rhi`** — RHI backend dispatch, by-value descs, file layout
|
||||
- **`prism-dag`** — DAG adjacency lists, arena allocation, Kahn's algorithm
|
||||
|
||||
## Workflows for AI agents
|
||||
|
||||
### Research / planning
|
||||
@@ -193,20 +269,12 @@ documents/
|
||||
a summary; iterate on the plan before writing any code.
|
||||
3. Only start implementing after the plan is approved.
|
||||
|
||||
### README
|
||||
### Skill maintenance
|
||||
|
||||
Keep `README.md` in sync with the project as it evolves. Update it when:
|
||||
- The directory layout changes meaningfully
|
||||
- Language, toolchain, or build system decisions are settled
|
||||
- Dependencies are added or removed
|
||||
- The project reaches a notable milestone
|
||||
|
||||
### Learning from edits
|
||||
|
||||
The user may edit code produced by AI agents. When this happens, infer the
|
||||
reason for the change and update AGENTS.md with any new conventions, patterns,
|
||||
or constraints that the edit reveals. This keeps the guide aligned with the
|
||||
user's evolving preferences.
|
||||
When you observe the user correcting your output (e.g. formatting, conventions),
|
||||
infer the rule and add it to the relevant skill's `SKILL.md`. If no skill
|
||||
matches, add a new one. This keeps AGENTS.md focused on project identity and
|
||||
critical workflow rules rather than accumulating domain details.
|
||||
|
||||
### Committing
|
||||
|
||||
|
||||
@@ -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.
@@ -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.
|
||||
@@ -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 0–1 into the NDC rect.
|
||||
- **FS**: `textures[NonUniformResourceIndex(selected)].Sample(uv)` — reuses the
|
||||
existing bindless descriptor array.
|
||||
|
||||
### 4. `main.cpp` — clean texture viewer
|
||||
|
||||
Remove everything mesh-related: tinyobj loading, vertex/index buffers,
|
||||
`ShaderData` storage buffers / device addresses, the mesh shader + pipeline,
|
||||
mouse orbit, and the mesh draw. Drop `-ltinyobjloader -lglm` from the link.
|
||||
Remove unused `assets/shader.slang`, `suzanne.obj`, `suzanne.mtl`.
|
||||
|
||||
New flow: RHI init → window/instance/pdev/surface/device/swapchain → load 7
|
||||
textures → bindless descriptor set (variable count 7) → load `blit.spv` →
|
||||
blit pipeline layout (`VERTEX|FRAGMENT` 32-byte push range) → blit pipeline
|
||||
(no vertex input, `TRIANGLE_STRIP`, swapchain color format, no depth,
|
||||
`cull NONE`, dynamic viewport/scissor).
|
||||
|
||||
Render loop per frame:
|
||||
- `compute_fit_rect()` from the selected texture's dims + window size
|
||||
- bind blit pipeline + descriptor set, push `BlitData{ rect, selected }`,
|
||||
`prRhiCmdDraw(cb, 4, 1, 0, 0)`
|
||||
- `+/-` cycles the selected texture; resize recomputes fit automatically
|
||||
|
||||
### 5. Test textures (PIL + `build/bin/toktx`)
|
||||
|
||||
Four generated KTX files in `assets/`, loaded alongside the 3 Suzanne textures
|
||||
(`texture_count = 7`, explicit path array):
|
||||
|
||||
| File | Size | Shows |
|
||||
|-----------------|-----------|--------------------------|
|
||||
| `test_square.ktx` | 1024x1024 | bars on both axes |
|
||||
| `test_fill.ktx` | 1920x1080 | fills the 16:9 window |
|
||||
| `test_wide.ktx` | 2048x512 | letterbox (T/B) |
|
||||
| `test_tall.ktx` | 512x2048 | pillarbox (L/R) |
|
||||
|
||||
Each with distinct gradients + a border grid so any stretch/squash is visible.
|
||||
Generated with `--genmipmap` for mip-aware sampling.
|
||||
|
||||
## Verification
|
||||
|
||||
`just build && just run` — each texture fits without crop/stretch, `+/-` cycles,
|
||||
window resize keeps fit, no Slang runtime in the binary.
|
||||
|
||||
## Verification results (2026-08-08)
|
||||
|
||||
Fit behavior confirmed by capturing the window (X11 driver) and sampling pixels:
|
||||
|
||||
| Texture | Window | Result verified |
|
||||
|---------------------|---------------|------------------------------------------------|
|
||||
| square 1024x1024 | 16:9 wide | pillarbox — pure-black L/R bars, full height |
|
||||
| test_wide 2048x512 | 16:9 wide | letterbox — pure-black T/B bars, full width |
|
||||
| test_tall 512x2048 | 16:9 wide | pillarbox — pure-black L/R bars, full height |
|
||||
| test_fill 1920x1080 | 16:9 wide | fills exactly — no bars anywhere |
|
||||
| square (resized) | portrait | fit recomputed per frame — flips to letterbox |
|
||||
|
||||
Texture cycling (`+/-`, `SDLK_PLUS/KP_PLUS/EQUALS`, `SDLK_MINUS/KP_MINUS`) is a
|
||||
straightforward `selected` bump in the key handler; it was reviewed but not
|
||||
exercise-tested in the headless verification env (KWin/XWayland drops XTEST
|
||||
synthesised keys). The other fit cases were verified by temporarily launching
|
||||
each texture as the initial selection.
|
||||
|
||||
### Bugs found and fixed during verification
|
||||
|
||||
1. **`VK_SUBOPTIMAL_KHR` aborted the app.** `prRhiAcquireNextImageVk` /
|
||||
`prRhiPresentVk` routed `SUBOPTIMAL` into `_checkVk` → `__builtin_trap()`.
|
||||
Both now treat it like `OUT_OF_DATE` (return `PR_RHI_SWAPCHAIN_OUT_OF_DATE`).
|
||||
Triggered immediately under X11/XWayland.
|
||||
2. **Swapchain recreate ignored surface extent.** `prRhiRecreateSwapchainVk`
|
||||
hard-coded the passed width/height; on surfaces whose `currentExtent` is
|
||||
meaningful (X11) that mismatched the drawable and re-looped on SUBOPTIMAL.
|
||||
Now queries `vkGetPhysicalDeviceSurfaceCapabilitiesKHR` and falls back to the
|
||||
passed size only when `currentExtent == 0xFFFFFFFF`.
|
||||
3. **App used logical window size for the swapchain.** Under HiDPI the drawable
|
||||
differs from `SDL_GetWindowSize` (1920x1080 logical → 2400x1350 drawable at
|
||||
1.25x scale), which is the root cause of #1 under XWayland. main.cpp now uses
|
||||
`SDL_GetWindowSizeInPixels` for swapchain width/height, fit math, viewport
|
||||
and scissor.
|
||||
@@ -0,0 +1,612 @@
|
||||
# Rendering Hardware Interface (RHI) — Research
|
||||
|
||||
## 1. What is an RHI
|
||||
|
||||
A Render(ing) Hardware Interface (RHI) is the abstraction layer between a
|
||||
renderer and the platform-specific graphics API (Vulkan, DirectX 12, Metal,
|
||||
etc.). It allows the renderer to be completely API-independent while providing
|
||||
a simpler, more explicit interface than the raw API.
|
||||
|
||||
**Key goals:**
|
||||
- API portability (write once, run on Vulkan, D3D12, Metal)
|
||||
- Clean separation: renderer talks to RHI, RHI talks to driver
|
||||
- Zero/low overhead over the native API
|
||||
- Explicit control over GPU resources, synchronisation, and memory
|
||||
|
||||
**What an RHI is NOT:**
|
||||
- A high-level rendering engine or framework
|
||||
- An automatic resource manager
|
||||
- A scene graph or render graph
|
||||
|
||||
---
|
||||
|
||||
## 2. Common Architecture Patterns (from real-world RHIs)
|
||||
|
||||
### 2.1 Object-Based RHI (orhi, SnapRHI, tobyc11/RHI)
|
||||
|
||||
Each GPU concept maps to an explicit object with a create/destroy lifecycle.
|
||||
Objects are passed by handle/pointer to command recording functions.
|
||||
|
||||
```
|
||||
RHI Instance → PhysicalDevice → Device → Queue
|
||||
Device → CommandPool → CommandBuffer
|
||||
Device → Buffer, Texture, Sampler
|
||||
Device → ShaderModule, PipelineLayout, Pipeline
|
||||
Device → DescriptorPool, DescriptorSetLayout, DescriptorSet
|
||||
Device → Fence, Semaphore, SwapChain
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- [orhi](https://github.com/adriengivry/orhi) — C++20, Vulkan/D3D12/Metal, MIT
|
||||
- [SnapRHI](https://github.com/Snapchat/SnapRHI) — C++20, Metal/Vulkan/OpenGL, Apache 2.0
|
||||
- [NVRHI](https://github.com/NVIDIAGameWorks/nvrhi) — C++14, Vulkan/D3D12, NVIDIA
|
||||
- [RGL](https://github.com/RavEngine/RGL) — C++20, Vulkan/D3D12/Metal
|
||||
|
||||
**Pros:**
|
||||
- Familiar mapping to Vulkan/D3D12 concepts
|
||||
- Easy to add new backends
|
||||
- Each object owns its lifetime explicitly
|
||||
|
||||
**Cons:**
|
||||
- Boilerplate-heavy
|
||||
- API surface grows with each backend quirk exposed
|
||||
|
||||
### 2.2 Command-List-Oriented RHI (Adept Engine, Unreal Engine)
|
||||
|
||||
Commands are recorded into command-list objects. The renderer records draws,
|
||||
bindings, and state changes into command lists which are then submitted to the
|
||||
GPU. The RHI thread translates these into API-specific calls.
|
||||
|
||||
```
|
||||
Renderer → RHI Command List → RHI Thread → Backend (VkCmdBuf / ID3D12GraphicsCommandList)
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Natural threading model (record in parallel, submit once)
|
||||
- Easy to defer and reorder commands
|
||||
- Close to D3D12/Vulkan command buffer semantics
|
||||
|
||||
**Cons:**
|
||||
- More indirection
|
||||
- State shadowing complexity
|
||||
|
||||
### 2.3 Immediate-Mode RHI (VRHI, NVRHI immediate mode)
|
||||
|
||||
Functions execute synchronously. No command list abstraction — the API is
|
||||
called directly. Simpler but less performant for multi-threaded recording.
|
||||
|
||||
**Prism choice:** Object-based + command-list-oriented. For a compositing
|
||||
application the graph evaluation can record node commands into per-frame
|
||||
command buffers.
|
||||
|
||||
---
|
||||
|
||||
## 3. Core API Surface (what every RHI needs)
|
||||
|
||||
| Category | Objects | Notes |
|
||||
|---|---|---|
|
||||
| **Instance/Device** | `PrRhiInstance`, `PrRhiDevice`, `PrRhiPhysicalDevice` | Instance owns debug + layers; Device owns queues + memory |
|
||||
| **Swap chain** | `PrRhiSwapChain` | Presentation surface + frame sync |
|
||||
| **Resources** | `PrRhiBuffer`, `PrRhiTexture`, `PrRhiSampler` | GPU memory, sub-allocated via VMA-like pattern |
|
||||
| **Shaders** | `PrRhiShader` | Slang → SPIR-V → `VkShaderModule` |
|
||||
| **Pipeline** | `PrRhiPipelineLayout`, `PrRhiPipeline`, `PrRhiComputePipeline` | Compiled shader + vertex layout + state |
|
||||
| **Descriptors** | `PrRhiDescriptorPool`, `PrRhiDescriptorSetLayout`, `PrRhiDescriptorSet` | Bindless or bindful |
|
||||
| **Commands** | `PrRhiCommandPool`, `PrRhiCommandBuffer` | Per-frame recording |
|
||||
| **Sync** | `PrRhiFence`, `PrRhiSemaphore` | CPU-GPU and GPU-GPU sync |
|
||||
| **Query** | `PrRhiQueryPool` | Timestamps, occlusion |
|
||||
|
||||
### Minimal surface for Prism MVP
|
||||
|
||||
For a node-based compositor that renders images via shader passes, the minimum
|
||||
API surface is:
|
||||
|
||||
```
|
||||
Device
|
||||
├── CommandPool → CommandBuffer
|
||||
├── Buffer (vertex, index, uniform/staging)
|
||||
├── Texture (read, write, render-target)
|
||||
├── Sampler
|
||||
├── Shader (from SPIR-V)
|
||||
├── PipelineLayout + Pipeline (graphics)
|
||||
├── DescriptorSetLayout + DescriptorSet (or push descriptors)
|
||||
├── Fence
|
||||
└── SwapChain (for output display)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Design Decisions for Prism
|
||||
|
||||
### 4.1 C11 / C++11 with wapp allocators
|
||||
|
||||
The project follows C11/C++11 dual-mode from `src/wapp/`. The RHI should:
|
||||
|
||||
- Use `WpAllocator *` for all allocations (no `new`/`delete` or raw malloc)
|
||||
- Expose opaque handle types (`PrRhiBuffer` as struct, not `VkBuffer`)
|
||||
- Keep the backend implementation in separate `.c` files per API
|
||||
- Use `wp_extern`/`wp_intern`/`wp_persist` conventions
|
||||
|
||||
All existing wapp infrastructure (arena allocators, arrays, queues, string
|
||||
types) should be used throughout.
|
||||
|
||||
### 4.2 Vulkan-only for now, but design for multi-backend
|
||||
|
||||
The AGENTS.md says "Vulkan, abstracted behind an RHI". The interface should be
|
||||
designed so that a D3D12 or Metal backend could be added later without changing
|
||||
the renderer. This means:
|
||||
|
||||
- Backend-agnostic types in the public header (`pr_rhi.h`)
|
||||
- Backend-specific implementations in `rhi/vulkan/`, `rhi/d3d12/` etc.
|
||||
- A factory pattern or compile-time dispatch for backend selection
|
||||
- No Vulkan types in the public RHI API
|
||||
|
||||
API objects that need per-backend variance:
|
||||
- **Object creation/teardown** (always differs)
|
||||
- **Shader compilation** (SPIR-V is universal, but creation paths differ)
|
||||
- **Pipeline state** (VkPipeline vs ID3D12PipelineState)
|
||||
- **Command recording** (VkCmdBuf vs ID3D12GraphicsCommandList)
|
||||
- **Memory management** (VkDeviceMemory vs ID3D12Heap)
|
||||
|
||||
### 4.3 Explicit over implicit
|
||||
|
||||
The RHI should not hide Vulkan's explicit nature. If the renderer needs to
|
||||
manage descriptor sets, layout transitions, and fences, the RHI should expose
|
||||
those operations — not paper over them with OpenGL-style "bind and forget."
|
||||
|
||||
### 4.4 Memory management: use VMA
|
||||
|
||||
Vulkan Memory Allocator (VMA) from AMD is the de-facto standard for Vulkan
|
||||
memory management. Rather than writing our own sub-allocator, we should:
|
||||
|
||||
- Use VMA for host+device memory allocation
|
||||
- Wrap it behind the RHI so backends can swap it out
|
||||
- Expose `PrRhiAllocation` as an opaque handle
|
||||
|
||||
### 4.5 Descriptor management
|
||||
|
||||
For a compositor, the number of unique descriptors per frame is bounded by the
|
||||
node graph size. Two approaches:
|
||||
|
||||
**A) Push descriptors** (Vulkan 1.0+, no pool needed)
|
||||
- Limited to `maxPushDescriptors` (typically 32-256)
|
||||
- Simple — inline with command recording
|
||||
- Good for small numbers of parameters per node
|
||||
|
||||
**B) Descriptor sets with per-frame pools**
|
||||
- More flexible for many resources
|
||||
- Requires pool management and reset
|
||||
- Better for texture-heavy graphs
|
||||
|
||||
**Recommendation:** Use push descriptors for uniforms, small descriptor set
|
||||
pools for sampled textures (images). Start with descriptor set approach since
|
||||
it scales better.
|
||||
|
||||
### 4.6 Pipeline management
|
||||
|
||||
Pipelines in Vulkan are expensive to create. Strategy:
|
||||
|
||||
- Hash pipeline state (shaders, blend mode, depth, etc.) → cache
|
||||
- Create pipelines lazily on first use
|
||||
- Store in a lock-free hash table (or arena-backed sorted array for
|
||||
single-threaded graph eval)
|
||||
- Use pipeline libraries (`VK_EXT_graphics_pipeline_library`) for faster
|
||||
creation when available
|
||||
|
||||
For a compositor, the number of distinct pipeline configurations is small
|
||||
(blend modes, colour-grade LUTs, blit, etc.), so a simple hash map suffices.
|
||||
|
||||
---
|
||||
|
||||
## 5. Vulkan-Specific Considerations
|
||||
|
||||
### 5.1 Queue selection
|
||||
|
||||
| Queue type | Usage in compositor |
|
||||
|---|---|
|
||||
| Graphics | Main rendering (draw calls) |
|
||||
| Compute | Image processing, convolution, colour-grade |
|
||||
| Transfer | Image upload from disk, staging |
|
||||
|
||||
The device should expose at least one graphics queue. If separate compute
|
||||
queues are available, use them for async processing. Transfer queue is
|
||||
desirable for texture loading without stalling the render loop.
|
||||
|
||||
### 5.2 Command buffer strategy
|
||||
|
||||
Two-level approach:
|
||||
- **Per-frame primary command buffers**: one per swap-chain image, filled by
|
||||
graph evaluation
|
||||
- **One-shot secondary command buffers**: for transient operations (texture
|
||||
upload, blits) using `immediate_submit` pattern
|
||||
|
||||
Command pools should be per-frame to allow reset without synchronisation.
|
||||
|
||||
### 5.3 Synchronisation
|
||||
|
||||
- `VkSemaphore` for swap-chain acquire/present
|
||||
- `VkFence` for CPU-GPU sync (frame completion, upload completion)
|
||||
- Timeline semaphores (`VK_KHR_timeline_semaphore`) if compute queue is used
|
||||
async
|
||||
|
||||
### 5.4 Image layouts
|
||||
|
||||
For a compositor where images flow through nodes:
|
||||
- `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL` — node inputs
|
||||
- `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL` — node render targets
|
||||
- `VK_IMAGE_LAYOUT_GENERAL` — storage images (compute nodes)
|
||||
- `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR` — final output
|
||||
|
||||
Transitions happen via explicit barriers in the command buffer (or via
|
||||
`VK_KHR_synchronization2`). The RHI should expose barrier helpers.
|
||||
|
||||
### 5.5 Debug / validation layers
|
||||
|
||||
- Load `VK_LAYER_KHRONOS_validation` in debug builds
|
||||
- Use `VK_EXT_debug_utils` for object naming
|
||||
- Enable GPU-assisted validation for shader issues
|
||||
- Consider RenderDoc for frame debugging
|
||||
|
||||
---
|
||||
|
||||
## 6. Slang Shader Integration
|
||||
|
||||
### 6.1 Why Slang
|
||||
|
||||
- HLSL/GLSL compatible syntax
|
||||
- Module system for shared shading code (colour science, maths)
|
||||
- Single source for multiple stages (vertex+fragment in one file)
|
||||
- SPIR-V output (directly consumable by Vulkan)
|
||||
- Rich reflection API (bindings, buffer layouts, entry points)
|
||||
- Active development, Khronos exploratory forum
|
||||
|
||||
### 6.2 Compilation pipeline
|
||||
|
||||
```
|
||||
.slang file
|
||||
→ slangc (offline) or libslang (runtime)
|
||||
→ SPIR-V binary
|
||||
→ vkCreateShaderModule
|
||||
→ PrRhiShader
|
||||
```
|
||||
|
||||
Options:
|
||||
- **Offline**: Pre-compile `.slang` → `.spv` at build time. Simpler, no runtime
|
||||
compiler dependency. Good for shipped shaders.
|
||||
- **Runtime**: Use libslang to compile at app startup (or on first use).
|
||||
Enables shader hot-reload during development.
|
||||
|
||||
**Recommendation:** Offline for release, runtime for debug/dev. The
|
||||
nvpro-samples `vk_slang_editor` demonstrates both approaches.
|
||||
|
||||
### 6.3 Reflection-driven pipeline creation
|
||||
|
||||
Slang's reflection API (`slang::ProgramLayout`) provides:
|
||||
- Binding locations (set, binding, space)
|
||||
- Buffer member offsets and sizes
|
||||
- Entry point names and stage types
|
||||
- Specialisation constant info
|
||||
|
||||
The RHI can use this to automatically build:
|
||||
- `VkDescriptorSetLayout` from declared bindings
|
||||
- `VkPipelineLayout` from descriptor set layouts + push constants
|
||||
- Push constant ranges from reflected constant buffers
|
||||
|
||||
See the [Slang Reflection API docs](https://shader-slang.com/slang/user-guide/reflection)
|
||||
and the [vk_slang_editor](https://github.com/nvpro-samples/vk_slang_editor)
|
||||
source for concrete patterns.
|
||||
|
||||
### 6.4 Shader organisation for a compositor
|
||||
|
||||
```
|
||||
src/shaders/
|
||||
├── common/
|
||||
│ ├── math.slang — Matrix/vector utilities
|
||||
│ ├── colour.slang — Colour space conversions
|
||||
│ └── compositing.slang — Blend equations, alpha handling
|
||||
├── blit.slang — Full-screen quad draw
|
||||
├── blend.slang — Over/under/add blend modes
|
||||
├── grade.slang — Colour grading (lift/gamma/gain)
|
||||
├── blur.slang — Separable gaussian blur
|
||||
└── read.slang — Simple texture passthrough
|
||||
```
|
||||
|
||||
Each shader file contains both vertex and fragment stages:
|
||||
|
||||
```slang
|
||||
// blit.slang
|
||||
[shader("vertex")]
|
||||
void vs_main(...) { ... }
|
||||
|
||||
[shader("fragment")]
|
||||
void fs_main(...) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Reference Projects
|
||||
|
||||
| Project | Language | APIs | Notable features |
|
||||
|---|---|---|---|
|
||||
| **[orhi](https://github.com/adriengivry/orhi)** | C++20 | Vulkan, D3D12, Metal (planned) | Clean object hierarchy, CMake, MIT |
|
||||
| **[SnapRHI](https://github.com/Snapchat/SnapRHI)** | C++20 | Metal, Vulkan, OpenGL/ES | Compile-switchable validation (if constexpr), aggressive pooling |
|
||||
| **[tobyc11/RHI](https://github.com/tobyc11/RHI)** | C++ | Vulkan, D3D11 | SPIR-V as common shader format, SPIRV-Cross for translation |
|
||||
| **[NVRHI](https://github.com/NVIDIAGameWorks/nvrhi)** | C++14 | Vulkan, D3D12 | Production-grade, NVIDIA maintained, header-only-ish API |
|
||||
| **[RGL](https://github.com/RavEngine/RGL)** | C++20 | Vulkan, D3D12, Metal | Thin wrapper, focuses on simplicity |
|
||||
| **[The Forge](https://github.com/ConfettiFX/The-Forge)** | C99/C++11 | All major APIs | Cross-platform, used in shipping games, FS |
|
||||
| **[O3DE Atom RHI](https://docs.o3de.org/docs/atom-guide/dev-guide/rhi/rhi/)** | C++17 | Vulkan, D3D12, Metal | Full-featured engine RHI, frame scheduler, multi-threaded |
|
||||
| **[Magma](https://github.com/vcoda/magma)** | C++17 | Vulkan | C++ abstraction, uses VMA, SPIR-V reflection |
|
||||
| **[rafx](https://github.com/zeozeozeo/rafx)** | C/C++ | Vulkan, D3D12 | C API (good FFI), explicit design |
|
||||
|
||||
### What to borrow from each
|
||||
|
||||
| Project | Lesson |
|
||||
|---|---|
|
||||
| **orhi** | Object hierarchy + backend-agnostic headers pattern |
|
||||
| **SnapRHI** | Compile-switchable validation; per-frame resource pooling |
|
||||
| **NVRHI** | Header-only-ish API with implementation in .cpp |
|
||||
| **The Forge** | C99-friendly, explicit API with minimal hidden state |
|
||||
| **O3DE Atom** | Frame scheduler concept (render passes as graph nodes) |
|
||||
| **RGL** | Simplicity — don't over-abstract |
|
||||
| **Magma** | VMA integration pattern + SPIR-V reflection |
|
||||
| **rafx** | C API design (relevant since Prism is C11) |
|
||||
|
||||
---
|
||||
|
||||
## 8. Proposed Architecture for Prism
|
||||
|
||||
### 8.1 Directory layout
|
||||
|
||||
```
|
||||
src/prism/
|
||||
├── rhi/
|
||||
│ ├── pr_rhi.h ← Umbrella header: canonical API + dispatch
|
||||
│ ├── pr_rhi_types.h ← Shared types (PrRhiBufferDesc, etc.)
|
||||
│ ├── vulkan/
|
||||
│ │ ├── pr_rhi_vk.h ← Declares prRhiCreateDeviceVk, etc.
|
||||
│ │ ├── pr_rhi_vk_aliases.h ← #define prRhiCreateDevice prRhiCreateDeviceVk
|
||||
│ │ ├── pr_rhi_vk_device.c
|
||||
│ │ ├── pr_rhi_vk_buffer.c
|
||||
│ │ ├── pr_rhi_vk_texture.c
|
||||
│ │ ├── pr_rhi_vk_shader.c
|
||||
│ │ ├── pr_rhi_vk_pipeline.c
|
||||
│ │ ├── pr_rhi_vk_descriptor.c
|
||||
│ │ ├── pr_rhi_vk_command.c
|
||||
│ │ └── pr_rhi_vk_swapchain.c
|
||||
│ ├── d3d12/ ← (future)
|
||||
│ └── metal/ ← (future)
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 8.2 Object lifecycle pattern
|
||||
|
||||
```c
|
||||
// Creation: takes an allocator + device + desc, returns handle
|
||||
PrRhiBuffer *prRhiCreateBuffer(PrRhiDevice *device, const PrRhiBufferDesc *desc,
|
||||
WpAllocator *alloc);
|
||||
|
||||
// Destruction: frees all GPU resources + backing memory
|
||||
void prRhiDestroyBuffer(PrRhiBuffer *buffer, WpAllocator *alloc);
|
||||
|
||||
// Usage: command buffer records operations on handles
|
||||
void prRhiCmdCopyBuffer(PrRhiCommandBuffer *cb,
|
||||
PrRhiBuffer *src, PrRhiBuffer *dst);
|
||||
```
|
||||
|
||||
### 8.3 Backend dispatch (compile-time via preprocessor aliases)
|
||||
|
||||
Backend selection happens at compile time via preprocessor aliases — no vtbl,
|
||||
no runtime dispatch overhead. Each backend is a set of standalone `.c` files;
|
||||
the build system compiles only the selected backend's sources.
|
||||
|
||||
```
|
||||
src/prism/rhi/
|
||||
├── pr_rhi.h ← umbrella: canonical API + dispatch
|
||||
├── pr_rhi_types.h ← shared types (all backends include this)
|
||||
├── vulkan/
|
||||
│ ├── pr_rhi_vk.h ← declares prRhiCreateDeviceVk, etc.
|
||||
│ ├── pr_rhi_vk_aliases.h ← #define prRhiCreateDevice prRhiCreateDeviceVk
|
||||
│ ├── pr_rhi_vk_device.c
|
||||
│ └── pr_rhi_vk_buffer.c
|
||||
├── d3d12/
|
||||
│ ├── pr_rhi_d3d12.h ← declares prRhiCreateDeviceD3D12, etc.
|
||||
│ ├── pr_rhi_d3d12_aliases.h ← #define prRhiCreateDevice prRhiCreateDeviceD3D12
|
||||
│ └── pr_rhi_d3d12_device.c
|
||||
└── metal/
|
||||
├── pr_rhi_metal.h
|
||||
├── pr_rhi_metal_aliases.h
|
||||
└── pr_rhi_metal_device.c
|
||||
```
|
||||
|
||||
The umbrella header documents the public API and conditionally includes the
|
||||
selected backend's aliases:
|
||||
|
||||
```c
|
||||
// pr_rhi.h
|
||||
#ifndef PR_RHI_H
|
||||
#define PR_RHI_H
|
||||
|
||||
#include "pr_rhi_types.h"
|
||||
|
||||
// ── Public API (documented here) ────────────────────────────────────
|
||||
PrRhiDevice *prRhiCreateDevice(const PrRhiDeviceDesc *desc, WpAllocator *alloc);
|
||||
void prRhiDestroyDevice(PrRhiDevice *device, WpAllocator *alloc);
|
||||
PrRhiBuffer *prRhiCreateBuffer(PrRhiDevice *d, const PrRhiBufferDesc *desc, WpAllocator *a);
|
||||
void prRhiDestroyBuffer(PrRhiBuffer *b, WpAllocator *a);
|
||||
// ... etc
|
||||
|
||||
// ── Backend dispatch ──────────────────────────────────────────────
|
||||
#if defined(PR_RHI_VULKAN)
|
||||
# include "vulkan/pr_rhi_vk_aliases.h"
|
||||
#elif defined(PR_RHI_D3D12)
|
||||
# include "d3d12/pr_rhi_d3d12_aliases.h"
|
||||
#elif defined(PR_RHI_METAL)
|
||||
# include "metal/pr_rhi_metal_aliases.h"
|
||||
#else
|
||||
# error "Define one of: PR_RHI_VULKAN, PR_RHI_D3D12, PR_RHI_METAL"
|
||||
#endif
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
Each aliases header maps the generic names to the backend's concrete names:
|
||||
|
||||
```c
|
||||
// vulkan/pr_rhi_vk_aliases.h
|
||||
#ifndef PR_RHI_VK_ALIASES_H
|
||||
#define PR_RHI_VK_ALIASES_H
|
||||
|
||||
#include "pr_rhi_vk.h"
|
||||
|
||||
#define prRhiCreateDevice prRhiCreateDeviceVk
|
||||
#define prRhiDestroyDevice prRhiDestroyDeviceVk
|
||||
#define prRhiCreateBuffer prRhiCreateBufferVk
|
||||
#define prRhiDestroyBuffer prRhiDestroyBufferVk
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
The backend implementation headers declare only their own real names:
|
||||
|
||||
```c
|
||||
// vulkan/pr_rhi_vk.h
|
||||
#ifndef PR_RHI_VK_H
|
||||
#define PR_RHI_VK_H
|
||||
|
||||
#include "../pr_rhi_types.h"
|
||||
|
||||
PrRhiDevice *prRhiCreateDeviceVk(const PrRhiDeviceDesc *desc, WpAllocator *alloc);
|
||||
void prRhiDestroyDeviceVk(PrRhiDevice *device, WpAllocator *alloc);
|
||||
// ...
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
Backend `.c` files are normal standalone translation units — no `.c` inclusion:
|
||||
|
||||
```c
|
||||
// vulkan/pr_rhi_vk_device.c
|
||||
#include "pr_rhi_vk.h"
|
||||
|
||||
PrRhiDevice *prRhiCreateDeviceVk(const PrRhiDeviceDesc *desc, WpAllocator *alloc) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Render code uses the generic names via the umbrella:
|
||||
|
||||
```c
|
||||
#include "prism/rhi/pr_rhi.h"
|
||||
|
||||
int main(void) {
|
||||
PrRhiDevice *dev = prRhiCreateDevice(&desc, &scratch); // → prRhiCreateDeviceVk
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Or explicitly selects a backend by including its header directly:
|
||||
|
||||
```c
|
||||
#include "prism/rhi/vulkan/pr_rhi_vk.h"
|
||||
|
||||
int main(void) {
|
||||
PrRhiDevice *dev = prRhiCreateDeviceVk(&desc, &scratch); // real name, no alias
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The build system controls selection by defining the preprocessor macro and
|
||||
listing only the chosen backend's `.c` files:
|
||||
|
||||
```sh
|
||||
# Vulkan build
|
||||
clang -DPR_RHI_VULKAN \
|
||||
main.c \
|
||||
src/prism/rhi/vulkan/pr_rhi_vk_device.c \
|
||||
src/prism/rhi/vulkan/pr_rhi_vk_buffer.c \
|
||||
src/wapp/wapp.c \
|
||||
-o compositor
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- Zero runtime overhead (macro expansion is a text substitution)
|
||||
- Dead code elimination is automatic — unselected backends are never compiled
|
||||
- Transparent debugging — stack traces show `prRhiCreateDeviceVk` directly
|
||||
- Documentation lives in one place — the umbrella `pr_rhi.h`
|
||||
- Each backend is a proper compilation unit — no `#include` of `.c` files
|
||||
- Explicit override path for single-backend builds or testing
|
||||
|
||||
### 8.4 Frame lifecycle
|
||||
|
||||
```
|
||||
Loop:
|
||||
1. prRhiAcquireNextImage(swapchain) → image index, semaphore
|
||||
2. prRhiResetCommandPool(pool, frame_idx) → recycles command buffers
|
||||
3. For each node in topo-sorted graph:
|
||||
a. prRhiCmdBindPipeline(cb, pipeline)
|
||||
b. prRhiCmdBindDescriptorSets(cb, ...)
|
||||
c. prRhiCmdPushConstants(cb, ...)
|
||||
d. prRhiCmdDraw(cb, ...)
|
||||
4. prRhiQueueSubmit(queue, cb, wait_sem, signal_sem, fence)
|
||||
5. prRhiPresent(swapchain, signal_sem)
|
||||
6. prRhiWaitForFence(fence) → CPU-GPU sync
|
||||
```
|
||||
|
||||
### 8.5 Static allocation strategy
|
||||
|
||||
Following wapp conventions and the project's data-oriented design principles:
|
||||
|
||||
- **Command pools**: one per swap-chain image (2-3), allocated once
|
||||
- **Descriptor pools**: one per frame, reset each frame
|
||||
- **Upload buffers**: ring buffer for staging data, bumped each frame
|
||||
- **Pipeline cache**: arena-backed hash table, populated lazily
|
||||
- **Scratch buffers**: arena-allocated in the per-frame scratch space
|
||||
|
||||
No dynamic allocation on the hot path — all per-frame memory comes from
|
||||
frame-local arena allocators that are reset at the start of each frame.
|
||||
|
||||
---
|
||||
|
||||
## 9. Open Questions
|
||||
|
||||
1. **Multi-queue**: Should the RHI expose separate compute/transfer queues, or
|
||||
keep everything on a single graphics queue and serialise? For an MVP, single
|
||||
queue is simpler and likely sufficient.
|
||||
|
||||
2. **Bindless vs bindful**: Bindless descriptors (VK_EXT_descriptor_indexing)
|
||||
simplify shader resource access but require higher Vulkan version. For
|
||||
maximum compatibility, start with bindful descriptor sets.
|
||||
|
||||
3. **Shader compilation**: Use `slangc` at build time and ship SPIR-V, or link
|
||||
libslang for runtime compilation + reflection? Runtime enables hot-reload
|
||||
but adds ~15MB to binary size. Recommendation: both — offline for release,
|
||||
runtime for debug.
|
||||
|
||||
4. **Swap chain**: Headless mode (no window) for batch/compute-only operation?
|
||||
Useful for a compositor that renders to a file. The RHI should support
|
||||
both windowed and headless modes.
|
||||
|
||||
5. **Vulkan version**: Target Vulkan 1.3 (widely available on desktop, adds
|
||||
timeline semaphores, dynamic rendering, and sync2) with fallback to 1.2.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [O3DE Atom RHI Overview](https://docs.o3de.org/docs/atom-guide/dev-guide/rhi/rhi/)
|
||||
- [Adept Engine RHI Design](https://andrewcjp.wordpress.com/2019/11/09/designing-a-render-hardware-interface-for-explicit-multi-gpu-programming/)
|
||||
- [Unreal Engine RHI Architecture](https://dev.epicgames.com/documentation/unreal-engine/parallel-rendering-overview-for-unreal-engine)
|
||||
- [orhi — OpenRHI](https://github.com/adriengivry/orhi)
|
||||
- [SnapRHI](https://github.com/Snapchat/SnapRHI)
|
||||
- [NVRHI](https://github.com/NVIDIAGameWorks/nvrhi)
|
||||
- [The Forge](https://github.com/ConfettiFX/The-Forge)
|
||||
- [RGL](https://github.com/RavEngine/RGL)
|
||||
- [tobyc11/RHI](https://github.com/tobyc11/RHI)
|
||||
- [rafx](https://github.com/zeozeozeo/rafx)
|
||||
- [Magma](https://github.com/vcoda/magma)
|
||||
- [Vulkan Memory Allocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator)
|
||||
- [Slang Shading Language](https://github.com/shader-slang/slang)
|
||||
- [Slang Reflection API](https://shader-slang.com/slang/user-guide/reflection)
|
||||
- [vk_slang_editor](https://github.com/nvpro-samples/vk_slang_editor)
|
||||
- [Vulkan in 30 minutes](https://renderdoc.org/vulkan-in-30-minutes.html)
|
||||
- [Vulkan Memory Management Guide](https://docs.vulkan.org/guide/latest/memory_allocation.html)
|
||||
- [Khronos Vulkan Spec — Command Buffers](https://docs.vulkan.org/spec/latest/chapters/cmdbuffers.html)
|
||||
@@ -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
+10
-1
@@ -1,8 +1,17 @@
|
||||
# Useful resources
|
||||
|
||||
## Graphs
|
||||
|
||||
- [The Algorithm Design Manual](https://sureshcseit.wordpress.com/wp-content/uploads/2021/04/skienathealgorithmdesignmanual.pdf)
|
||||
- [Topological Sorting using BFS (Kahn's Algorithm)](https://www.geeksforgeeks.org/topological-sorting-indegree-based-solution/)
|
||||
- [Kahn's Algorithm Explained with Code & Examples](https://dev.to/rui_jiang/kahns-algorithm-for-topological-sorting-explained-with-code-examples-2if5)
|
||||
- [Understanding Kahn's Algorithm for Topological Sorting](https://blog.devgenius.io/dsa-kahns-algorithm-for-topological-sorting-33c8587985a1)
|
||||
- [Detect a Cycle in Directed Graph](https://takeuforward.org/data-structure/detect-a-cycle-in-directed-graph-topological-sort-kahns-algorithm-g-23)
|
||||
- [LeetCode's Kahn's Algorithm](https://leetcodethehardway.com/tutorials/graph-theory/kahns-algorithm)
|
||||
- [Kahn's Algorithm](https://leetcodethehardway.com/tutorials/graph-theory/kahns-algorithm)
|
||||
|
||||
## Rendering Hardware Interface (RHI)
|
||||
|
||||
- [O3DE Atom RHI Overview](https://docs.o3de.org/docs/atom-guide/dev-guide/rhi/rhi/)
|
||||
- [Adept Engine RHI Design](https://andrewcjp.wordpress.com/2019/11/09/designing-a-render-hardware-interface-for-explicit-multi-gpu-programming/)
|
||||
- [Unreal Engine RHI Architecture](https://dev.epicgames.com/documentation/unreal-engine/parallel-rendering-overview-for-unreal-engine)
|
||||
- [NVRHI](https://github.com/NVIDIAGameWorks/nvrhi) — NVIDIA's production RHI (Vulkan/D3D12), reference API design
|
||||
|
||||
@@ -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) |
|
||||
@@ -0,0 +1,36 @@
|
||||
# Session Log — 2026-07-05
|
||||
|
||||
## Completed
|
||||
- **DAG refactoring** (`scratchpad/dag.c`): Decoupled PrGraph from PrNodeManager.
|
||||
Graph tracks its own active vertices (`vertex_count`, `max_vertex_ever`, `b8 active`).
|
||||
`prGraphAddEdge`, `prGraphTopologicalSort` no longer take `PrNodeManager*`.
|
||||
Kahn's algorithm integrated into `prGraphAddEdge` for cycle detection with rollback.
|
||||
Output verified matching baseline.
|
||||
- **RHI research document** written to `documents/research/rendering-hardware-interface.md`.
|
||||
Covers 8 reference RHIs, architecture patterns, core API surface, Vulkan-specific
|
||||
considerations, Slang integration, and proposed directory layout / lifecycle design.
|
||||
- Added NVRHI to `documents/resources.md`.
|
||||
- **RHI API surface** (`scratchpad/rhi/pr_rhi.h`, `pr_rhi_types.h`): Full RHI API
|
||||
designed with compile-time alias dispatch, by-value desc structs, wapp array types
|
||||
for pointer+count replacement, `PrRhiSwapchainResult` enum, surface capabilities,
|
||||
compute pipeline support, all primitive topologies, and aligned function declarations
|
||||
grouped by subsystem section.
|
||||
- **Initial opencode setup** (opencode.json, justfile, skills, trimmed AGENTS.md).
|
||||
|
||||
## Key Decisions
|
||||
- Graph API is self-contained — no graphics or node-manager dependencies.
|
||||
- RHI will use compile-time alias dispatch (`#define prRhiCreateDevice prRhiCreateDeviceVk`)
|
||||
over vtbl — zero runtime overhead, dead-stripping, separate builds per backend.
|
||||
- `justfile` (Just) as task runner.
|
||||
- Object lifecycle: `prRhiCreate*` / `prRhiDestroy*` with explicit `WpAllocator*`.
|
||||
- All GPU state explicit (no hidden pipeline state).
|
||||
- Per-frame command pools and descriptor pools, arena-allocated scratch.
|
||||
- `#version-macro` convention removed from Slang files; `__slang` define used instead.
|
||||
- Desc structs passed by value (not `const *`) for simpler caller ergonomics.
|
||||
- Array aliases grouped: opaque handles (`**`) first, value types (`*`) second, separated
|
||||
by blank line — element types before array aliases before desc structs.
|
||||
|
||||
## Next Steps
|
||||
- Begin Vulkan backend implementation starting with `pr_rhi_vk_device.c` (instance/device/surface/swapchain creation).
|
||||
- Implement Slang shader compilation + reflection integration.
|
||||
- Wire up per-frame lifecycle (command pool reset, descriptor pool reset, scratch reset).
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -0,0 +1,86 @@
|
||||
# Prism — Build tasks
|
||||
# See https://just.systems
|
||||
|
||||
default: build
|
||||
|
||||
CC := "clang"
|
||||
CXX := "clang++"
|
||||
BUILDDIR := "build"
|
||||
|
||||
# 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:
|
||||
@echo "TODO: implement linter"
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
@echo "TODO: implement tests"
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"command": {
|
||||
"build": {
|
||||
"template": "!just build",
|
||||
"description": "Build the project"
|
||||
},
|
||||
"lint": {
|
||||
"template": "!just lint",
|
||||
"description": "Run linter / typecheck"
|
||||
},
|
||||
"test": {
|
||||
"template": "!just test",
|
||||
"description": "Run tests"
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"edit": {
|
||||
"src/wapp/**": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
+452
-213
@@ -1,7 +1,11 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
|
||||
#include "../src/vendor/wapp/wapp.h"
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include "../src/wapp/wapp.h"
|
||||
#include <string.h>
|
||||
|
||||
wp_intern WpLogger _log = { .name = wpStr8LitRo("dag_man") };
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Pool allocator (arena-backed, intrusive free list)
|
||||
@@ -18,13 +22,13 @@ typedef struct {
|
||||
u64 slot_size;
|
||||
} PrPool;
|
||||
|
||||
static void prPoolInit(PrPool *pool, WpAllocator *arena, u64 slot_size) {
|
||||
pool->allocator = arena;
|
||||
wp_intern void prPoolInit(PrPool *pool, WpAllocator *allocator, u64 slot_size) {
|
||||
pool->allocator = allocator;
|
||||
pool->free_head = NULL;
|
||||
pool->slot_size = slot_size;
|
||||
}
|
||||
|
||||
static void *prPoolAlloc(PrPool *pool) {
|
||||
wp_intern void *prPoolAlloc(PrPool *pool) {
|
||||
if (pool->free_head) {
|
||||
PrPoolFreeNode *node = pool->free_head;
|
||||
pool->free_head = node->next;
|
||||
@@ -33,7 +37,7 @@ static void *prPoolAlloc(PrPool *pool) {
|
||||
return wpMemAllocatorAlloc(pool->allocator, pool->slot_size);
|
||||
}
|
||||
|
||||
static void prPoolFree(PrPool *pool, void *slot) {
|
||||
wp_intern void prPoolFree(PrPool *pool, void *slot) {
|
||||
if (!slot) { return; }
|
||||
PrPoolFreeNode *node = (PrPoolFreeNode *)slot;
|
||||
node->next = pool->free_head;
|
||||
@@ -41,128 +45,237 @@ static void prPoolFree(PrPool *pool, void *slot) {
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Graph — unified nodes with values + forward/backward adjacency
|
||||
*
|
||||
* - Nodes live in a flat array; free slots are linked via next_free.
|
||||
* - Each node has a generation counter bumped on free; handles are
|
||||
* { index, generation } so stale references are detectable.
|
||||
* - Edges are pool-allocated PrEdgeNode structs with both forward
|
||||
* and backward linkage so deletion traces both directions.
|
||||
* Shared types
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
#define PR_INVALID_INDEX ((u64)-1)
|
||||
#define INVALID_NODE_INDEX (u64)-1
|
||||
#define INVALID_NODE_ID ((PrNodeId){ .index = INVALID_NODE_INDEX, .generation = INVALID_NODE_INDEX })
|
||||
|
||||
typedef struct PrEdgeNode PrEdgeNode;
|
||||
struct PrEdgeNode {
|
||||
PrEdgeNode *pool_next; /* used by pool free list when freed */
|
||||
PrEdgeNode *next_forward; /* chain in source's forward list */
|
||||
PrEdgeNode *next_backward; /* chain in target's backward list */
|
||||
u64 source_idx;
|
||||
u64 target_idx;
|
||||
};
|
||||
typedef enum {
|
||||
PR_NODE_TYPE_NONE,
|
||||
PR_NODE_TYPE_READ,
|
||||
PR_NODE_TYPE_BLUR,
|
||||
PR_NODE_TYPE_GRADE,
|
||||
|
||||
COUNT_NODE_TYPES
|
||||
} PrNodeType;
|
||||
|
||||
typedef struct {
|
||||
u64 index;
|
||||
u64 generation;
|
||||
} PrHandle;
|
||||
} PrNodeId;
|
||||
typedef struct {
|
||||
union {
|
||||
WpStr8 path;
|
||||
f32 blur;
|
||||
f32 gain;
|
||||
} params;
|
||||
PrNodeType type;
|
||||
u64 generation;
|
||||
u64 next_free;
|
||||
} PrNode;
|
||||
typedef PrNode *PrNodeArray;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Graph edge type
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
typedef struct PrGraphEdge PrGraphEdge;
|
||||
struct PrGraphEdge {
|
||||
PrGraphEdge *next_forward;
|
||||
PrGraphEdge *next_backward;
|
||||
u64 source_idx;
|
||||
u64 target_idx;
|
||||
};
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Graph vertex — compact adjacency head (two pointers + active flag)
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
typedef struct PrGraphVertex PrGraphVertex;
|
||||
struct PrGraphVertex {
|
||||
PrGraphEdge *next_forward;
|
||||
PrGraphEdge *next_backward;
|
||||
b8 active;
|
||||
};
|
||||
typedef PrGraphVertex *PrGraphVertexArray;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* PrGraph — owns only topology (edges + adjacency heads)
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
typedef struct {
|
||||
int value;
|
||||
u64 generation; /* bumped on every free */
|
||||
u64 next_free; /* free-list index (PR_INVALID_INDEX = active) */
|
||||
PrEdgeNode *forward_head; /* outgoing edges */
|
||||
PrEdgeNode *backward_head; /* incoming edges */
|
||||
} PrGraphNode;
|
||||
|
||||
typedef struct {
|
||||
PrGraphNode *nodes; /* WpArray of PrGraphNode */
|
||||
u64 max_nodes;
|
||||
u64 max_ever; /* highest index ever allocated */
|
||||
u64 free_head; /* PR_INVALID_INDEX = empty */
|
||||
u64 count; /* active node count */
|
||||
PrPool edge_pool;
|
||||
PrPool edge_pool;
|
||||
PrGraphVertexArray vertices;
|
||||
u64 capacity;
|
||||
u64 max_vertex_ever;
|
||||
u64 vertex_count;
|
||||
} PrGraph;
|
||||
|
||||
/* --- initialisation ---------------------------------------------------- */
|
||||
/* ---------------------------------------------------------------------------
|
||||
* PrNodeManager — owns compositor node data + handle lifecycle + topology
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
static void prGraphInit(PrGraph *g, WpAllocator *arena, u64 max_nodes) {
|
||||
g->nodes = wpArrayAllocCapacity(PrGraphNode, arena, max_nodes, WP_ARRAY_INIT_FILLED);
|
||||
g->max_nodes = max_nodes;
|
||||
g->max_ever = 0;
|
||||
g->free_head = PR_INVALID_INDEX;
|
||||
g->count = 0;
|
||||
prPoolInit(&g->edge_pool, arena, sizeof(PrEdgeNode));
|
||||
typedef struct {
|
||||
PrNodeArray nodes;
|
||||
PrGraph graph;
|
||||
u64 capacity;
|
||||
u64 max_count_ever;
|
||||
u64 count;
|
||||
u64 free_head;
|
||||
} PrNodeManager;
|
||||
|
||||
/* Build the free list — last slot's next_free stays PR_INVALID_INDEX */
|
||||
for (u64 i = 0; i < max_nodes; i++) {
|
||||
g->nodes[i].next_free = (i < max_nodes - 1) ? i + 1 : PR_INVALID_INDEX;
|
||||
}
|
||||
g->free_head = 0;
|
||||
}
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Function declarations (cross-referencing both types)
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
/* --- handle validation ------------------------------------------------ */
|
||||
wp_intern void prNodeManagerInit(PrNodeManager *mgr, WpAllocator *allocator, u64 capacity);
|
||||
wp_intern b8 prNodeManagerIsStaleNode(const PrNodeManager *mgr, PrNodeId id);
|
||||
wp_intern b8 prNodeManagerIsActiveNode(const PrNodeManager *mgr, PrNodeId id);
|
||||
wp_intern PrNodeId prNodeManagerGetNode(const PrNodeManager *mgr, u64 index);
|
||||
wp_intern PrNodeId prNodeManagerAddNode(PrNodeManager *mgr, PrNodeType type);
|
||||
wp_intern void prNodeManagerRemoveNode(PrNodeManager *mgr, PrNodeId id);
|
||||
wp_intern void prNodeManagerAddEdge(PrNodeManager *mgr, PrNodeId from, PrNodeId to);
|
||||
wp_intern void prNodeManagerDumpGraph(const PrNodeManager *mgr);
|
||||
|
||||
static b8 prHandleValid(PrGraph *g, PrHandle h) {
|
||||
if (h.index >= g->max_nodes) { return false; }
|
||||
PrGraphNode *node = &g->nodes[h.index];
|
||||
return node->generation == h.generation && node->next_free == PR_INVALID_INDEX;
|
||||
}
|
||||
wp_intern void prGraphInit(PrGraph *graph, WpAllocator *allocator, u64 capacity);
|
||||
wp_intern void prGraphAddVertex(PrGraph *graph, u64 idx);
|
||||
wp_intern void prGraphRemoveVertex(PrGraph *graph, u64 idx);
|
||||
wp_intern b8 prGraphAddEdge(PrGraph *graph, u64 from_idx, u64 to_idx);
|
||||
wp_intern b8 prGraphEdgeExists(const PrGraph *graph, u64 from_idx, u64 to_idx);
|
||||
wp_intern u64 prGraphVertexCount(const PrGraph *graph);
|
||||
wp_intern WpU64Array prGraphTopologicalSort(const PrGraph *graph, const WpAllocator *allocator);
|
||||
|
||||
/* --- node management -------------------------------------------------- */
|
||||
wp_intern void _unlinkForward(PrGraph *graph, u64 from_idx, PrGraphEdge *edge);
|
||||
wp_intern void _unlinkBackward(PrGraph *graph, u64 to_idx, PrGraphEdge *edge);
|
||||
/* ---------------------------------------------------------------------------
|
||||
* PrNodeManager implementation
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
static PrHandle prNodeAdd(PrGraph *g, int value) {
|
||||
if (g->free_head == PR_INVALID_INDEX) {
|
||||
return (PrHandle){ PR_INVALID_INDEX, 0 };
|
||||
wp_intern void prNodeManagerInit(PrNodeManager *mgr, WpAllocator *allocator, u64 capacity) {
|
||||
mgr->nodes = wpArrayAllocCapacity(PrNode, allocator, capacity, WP_ARRAY_INIT_FILLED);
|
||||
mgr->free_head = 0;
|
||||
mgr->capacity = capacity;
|
||||
mgr->max_count_ever = 0;
|
||||
mgr->count = 0;
|
||||
|
||||
if (!mgr->nodes) {
|
||||
mgr->capacity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
u64 idx = g->free_head;
|
||||
PrGraphNode *n = &g->nodes[idx];
|
||||
g->free_head = n->next_free;
|
||||
memset(mgr->nodes, 0, capacity * sizeof(PrNode));
|
||||
|
||||
n->value = value;
|
||||
n->next_free = PR_INVALID_INDEX;
|
||||
n->forward_head = NULL;
|
||||
n->backward_head = NULL;
|
||||
g->count++;
|
||||
for (u64 i = 0; i < capacity; ++i) {
|
||||
mgr->nodes[i].next_free = i < capacity - 1 ? i + 1 : INVALID_NODE_INDEX;
|
||||
}
|
||||
|
||||
if (idx >= g->max_ever) { g->max_ever = idx + 1; }
|
||||
|
||||
return (PrHandle){ idx, n->generation };
|
||||
prGraphInit(&mgr->graph, allocator, capacity);
|
||||
}
|
||||
|
||||
static int *prNodeGetValue(PrGraph *g, PrHandle h) {
|
||||
if (!prHandleValid(g, h)) { return NULL; }
|
||||
return &g->nodes[h.index].value;
|
||||
wp_intern b8 prNodeManagerIsStaleNode(const PrNodeManager *mgr, PrNodeId id) {
|
||||
u64 generation = mgr->nodes[id.index].generation;
|
||||
return id.generation != generation;
|
||||
}
|
||||
|
||||
/* --- internal: unlink edge helpers ------------------------------------ */
|
||||
wp_intern b8 prNodeManagerIsActiveNode(const PrNodeManager *mgr, PrNodeId id) {
|
||||
u64 next_free = mgr->nodes[id.index].next_free;
|
||||
return !prNodeManagerIsStaleNode(mgr, id) && next_free == INVALID_NODE_INDEX;
|
||||
}
|
||||
|
||||
static PrEdgeNode *_unlinkForward(PrGraph *g, u64 from_idx, u64 target_idx) {
|
||||
PrGraphNode *src = &g->nodes[from_idx];
|
||||
PrEdgeNode *prev = NULL;
|
||||
PrEdgeNode *curr = src->forward_head;
|
||||
wp_intern PrNodeId prNodeManagerGetNode(const PrNodeManager *mgr, u64 index) {
|
||||
return (PrNodeId){ .index = index, .generation = mgr->nodes[index].generation };
|
||||
}
|
||||
|
||||
wp_intern PrNodeId prNodeManagerAddNode(PrNodeManager *mgr, PrNodeType type) {
|
||||
u64 idx = mgr->free_head;
|
||||
if (idx == INVALID_NODE_INDEX) { return INVALID_NODE_ID; }
|
||||
|
||||
PrNode *node = &mgr->nodes[idx];
|
||||
|
||||
mgr->free_head = node->next_free;
|
||||
node->next_free = INVALID_NODE_INDEX;
|
||||
node->type = type;
|
||||
memset(&node->params, 0, sizeof(node->params));
|
||||
|
||||
mgr->count++;
|
||||
if (idx + 1 > mgr->max_count_ever) { mgr->max_count_ever = idx + 1; }
|
||||
|
||||
prGraphAddVertex(&mgr->graph, idx);
|
||||
|
||||
return (PrNodeId){ .index = idx, .generation = node->generation };
|
||||
}
|
||||
|
||||
wp_intern void prNodeManagerRemoveNode(PrNodeManager *mgr, PrNodeId id) {
|
||||
if (!prNodeManagerIsActiveNode(mgr, id)) { return; }
|
||||
|
||||
/* Tear down all edges incident to this node and mark vertex inactive */
|
||||
prGraphRemoveVertex(&mgr->graph, id.index);
|
||||
|
||||
/* Return node slot to free list with bumped generation */
|
||||
PrNode *node = &mgr->nodes[id.index];
|
||||
node->generation++;
|
||||
node->next_free = mgr->free_head;
|
||||
mgr->free_head = id.index;
|
||||
mgr->count--;
|
||||
}
|
||||
|
||||
wp_intern void prNodeManagerAddEdge(PrNodeManager *mgr, PrNodeId from, PrNodeId to) {
|
||||
if (!prNodeManagerIsActiveNode(mgr, from) || !prNodeManagerIsActiveNode(mgr, to)) { return; }
|
||||
if (from.index == to.index) { return; }
|
||||
if (prGraphEdgeExists(&mgr->graph, from.index, to.index)) { return; }
|
||||
prGraphAddEdge(&mgr->graph, from.index, to.index);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* PrGraph implementation
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
wp_intern void prGraphInit(PrGraph *graph, WpAllocator *allocator, u64 capacity) {
|
||||
graph->vertices = wpArrayAllocCapacity(PrGraphVertex, allocator, capacity, WP_ARRAY_INIT_FILLED);
|
||||
graph->capacity = capacity;
|
||||
graph->max_vertex_ever = 0;
|
||||
graph->vertex_count = 0;
|
||||
|
||||
if (!graph->vertices) {
|
||||
graph->capacity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
prPoolInit(&graph->edge_pool, allocator, sizeof(PrGraphEdge));
|
||||
memset(graph->vertices, 0, capacity * sizeof(PrGraphVertex));
|
||||
}
|
||||
|
||||
/* --- internal: unlink edge helpers (raw indices, caller guarantees validity) -- */
|
||||
|
||||
wp_intern void _unlinkForward(PrGraph *graph, u64 from_idx, PrGraphEdge *edge) {
|
||||
PrGraphVertex *vtx = &graph->vertices[from_idx];
|
||||
PrGraphEdge *curr = vtx->next_forward;
|
||||
PrGraphEdge *prev = NULL;
|
||||
while (curr) {
|
||||
if (curr->target_idx == target_idx) {
|
||||
if (prev) { prev->next_forward = curr->next_forward; }
|
||||
else { src->forward_head = curr->next_forward; }
|
||||
return curr;
|
||||
if (curr == edge) {
|
||||
if (prev) {
|
||||
prev->next_forward = curr->next_forward;
|
||||
} else {
|
||||
vtx->next_forward = curr->next_forward;
|
||||
}
|
||||
return;
|
||||
}
|
||||
prev = curr;
|
||||
curr = curr->next_forward;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void _unlinkBackward(PrGraph *g, u64 to_idx, u64 source_idx) {
|
||||
PrGraphNode *dst = &g->nodes[to_idx];
|
||||
PrEdgeNode *prev = NULL;
|
||||
PrEdgeNode *curr = dst->backward_head;
|
||||
// This is intended to handle cases where the target node might not be the one
|
||||
// immediately feeding the node represented by to_idx
|
||||
wp_intern void _unlinkBackward(PrGraph *graph, u64 to_idx, PrGraphEdge *edge) {
|
||||
PrGraphVertex *vtx = &graph->vertices[to_idx];
|
||||
PrGraphEdge *curr = vtx->next_backward;
|
||||
PrGraphEdge *prev = NULL;
|
||||
while (curr) {
|
||||
if (curr->source_idx == source_idx) {
|
||||
if (prev) { prev->next_backward = curr->next_backward; }
|
||||
else { dst->backward_head = curr->next_backward; }
|
||||
if (curr == edge) {
|
||||
if (prev) {
|
||||
prev->next_backward = curr->next_backward;
|
||||
} else {
|
||||
vtx->next_backward = curr->next_backward;
|
||||
}
|
||||
return;
|
||||
}
|
||||
prev = curr;
|
||||
@@ -170,158 +283,284 @@ static void _unlinkBackward(PrGraph *g, u64 to_idx, u64 source_idx) {
|
||||
}
|
||||
}
|
||||
|
||||
/* --- vertex lifecycle ------------------------------------------------- */
|
||||
|
||||
wp_intern void prGraphAddVertex(PrGraph *graph, u64 idx) {
|
||||
graph->vertices[idx].active = true;
|
||||
graph->vertex_count++;
|
||||
if (idx >= graph->max_vertex_ever) {
|
||||
graph->max_vertex_ever = idx + 1;
|
||||
}
|
||||
}
|
||||
|
||||
wp_intern void prGraphRemoveVertex(PrGraph *graph, u64 idx) {
|
||||
PrGraphVertex *vtx = &graph->vertices[idx];
|
||||
if (!vtx->active) { return; }
|
||||
|
||||
/* Free outgoing edges: unlink from each target's backward list */
|
||||
PrGraphEdge *curr = vtx->next_forward;
|
||||
while (curr) {
|
||||
PrGraphEdge *next = curr->next_forward;
|
||||
_unlinkBackward(graph, curr->target_idx, curr);
|
||||
prPoolFree(&graph->edge_pool, curr);
|
||||
curr = next;
|
||||
}
|
||||
|
||||
/* Free incoming edges: unlink from each source's forward list */
|
||||
curr = vtx->next_backward;
|
||||
while (curr) {
|
||||
PrGraphEdge *next = curr->next_backward;
|
||||
_unlinkForward(graph, curr->source_idx, curr);
|
||||
prPoolFree(&graph->edge_pool, curr);
|
||||
curr = next;
|
||||
}
|
||||
|
||||
vtx->next_forward = NULL;
|
||||
vtx->next_backward = NULL;
|
||||
vtx->active = false;
|
||||
graph->vertex_count--;
|
||||
}
|
||||
|
||||
/* --- edge management -------------------------------------------------- */
|
||||
|
||||
static b8 prEdgeAdd(PrGraph *g, PrHandle from, PrHandle to) {
|
||||
if (!prHandleValid(g, from) || !prHandleValid(g, to)) { return false; }
|
||||
wp_intern b8 prGraphEdgeExists(const PrGraph *graph, u64 from_idx, u64 to_idx) {
|
||||
PrGraphVertex *vtx = &graph->vertices[from_idx];
|
||||
PrGraphEdge *curr = vtx->next_forward;
|
||||
while (curr) {
|
||||
if (curr->target_idx == to_idx) { return true; }
|
||||
curr = curr->next_forward;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
PrEdgeNode *edge = (PrEdgeNode *)prPoolAlloc(&g->edge_pool);
|
||||
wp_intern b8 prGraphAddEdge(PrGraph *graph, u64 from_idx, u64 to_idx) {
|
||||
PrGraphEdge *edge = (PrGraphEdge *)prPoolAlloc(&graph->edge_pool);
|
||||
if (!edge) { return false; }
|
||||
|
||||
edge->source_idx = from.index;
|
||||
edge->target_idx = to.index;
|
||||
edge->source_idx = from_idx;
|
||||
edge->target_idx = to_idx;
|
||||
|
||||
PrGraphNode *src = &g->nodes[from.index];
|
||||
edge->next_forward = src->forward_head;
|
||||
src->forward_head = edge;
|
||||
|
||||
PrGraphNode *dst = &g->nodes[to.index];
|
||||
edge->next_backward = dst->backward_head;
|
||||
dst->backward_head = edge;
|
||||
/* Link into adjacency chains */
|
||||
PrGraphVertex *src = &graph->vertices[from_idx];
|
||||
PrGraphVertex *dst = &graph->vertices[to_idx];
|
||||
edge->next_forward = src->next_forward;
|
||||
edge->next_backward = dst->next_backward;
|
||||
src->next_forward = edge;
|
||||
dst->next_backward = edge;
|
||||
|
||||
/* Check whether the new edge created a cycle */
|
||||
WpAllocator scratch = wpMemArenaAllocatorInitZero(KiB(16));
|
||||
WpU64Array sorted = prGraphTopologicalSort(graph, &scratch);
|
||||
u64 sorted_n = sorted ? wpArrayCount(sorted) : 0;
|
||||
if (sorted_n < graph->vertex_count) {
|
||||
_unlinkForward(graph, from_idx, edge);
|
||||
_unlinkBackward(graph, to_idx, edge);
|
||||
prPoolFree(&graph->edge_pool, edge);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* --- node removal (tears down incident edges) ------------------------- */
|
||||
|
||||
static void prNodeRemove(PrGraph *g, PrHandle h) {
|
||||
if (!prHandleValid(g, h)) { return; }
|
||||
|
||||
u64 idx = h.index;
|
||||
PrGraphNode *n = &g->nodes[idx];
|
||||
|
||||
/* Free outgoing edges: unlink from each target's backward list */
|
||||
PrEdgeNode *edge = n->forward_head;
|
||||
while (edge) {
|
||||
PrEdgeNode *next = edge->next_forward;
|
||||
_unlinkBackward(g, edge->target_idx, idx);
|
||||
prPoolFree(&g->edge_pool, edge);
|
||||
edge = next;
|
||||
}
|
||||
|
||||
/* Free incoming edges: unlink from each source's forward list */
|
||||
edge = n->backward_head;
|
||||
while (edge) {
|
||||
PrEdgeNode *next = edge->next_backward;
|
||||
/* edge is also in the source's forward list — unlink by target_idx */
|
||||
_unlinkForward(g, edge->source_idx, idx);
|
||||
prPoolFree(&g->edge_pool, edge);
|
||||
edge = next;
|
||||
}
|
||||
|
||||
/* Return node slot to free list with bumped generation */
|
||||
n->generation++;
|
||||
n->next_free = g->free_head;
|
||||
g->free_head = idx;
|
||||
g->count--;
|
||||
wp_intern u64 prGraphVertexCount(const PrGraph *graph) {
|
||||
return graph->vertex_count;
|
||||
}
|
||||
|
||||
/* --- traversal helpers ------------------------------------------------ */
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Kahn's algorithm — topological sort / cycle detection
|
||||
*
|
||||
* Returns a WpArray of PrNodeId (sorted topologically). If the result count
|
||||
* is less than mgr->count, the graph contains a cycle.
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
static void prDump(PrGraph *g) {
|
||||
printf("Active nodes: %llu\n\n", (unsigned long long)g->count);
|
||||
wp_intern WpU64Array prGraphTopologicalSort(const PrGraph *graph, const WpAllocator *allocator) {
|
||||
if (graph->vertex_count == 0) { return NULL; }
|
||||
if (!graph->vertices || graph->capacity == 0) { return NULL; }
|
||||
|
||||
for (u64 i = 0; i < g->max_ever; i++) {
|
||||
PrGraphNode *n = &g->nodes[i];
|
||||
if (n->next_free != PR_INVALID_INDEX) { continue; }
|
||||
WpU64Array result = wpArrayAllocCapacity(u64, allocator, graph->vertex_count, WP_ARRAY_INIT_NONE);
|
||||
if (!result) { return NULL; }
|
||||
|
||||
printf(" [%llu] g=%llu value=%d\n",
|
||||
(unsigned long long)i,
|
||||
(unsigned long long)n->generation,
|
||||
n->value);
|
||||
WpAllocator local_arena = wpMemArenaAllocatorInitZero(KiB(16));
|
||||
|
||||
printf(" forward ──▶");
|
||||
PrEdgeNode *e = n->forward_head;
|
||||
if (!e) { printf(" (none)"); }
|
||||
while (e) {
|
||||
PrGraphNode *tv = &g->nodes[e->target_idx];
|
||||
printf(" %llu[%d]", (unsigned long long)e->target_idx, tv->value);
|
||||
e = e->next_forward;
|
||||
if (e) { printf(","); }
|
||||
WpU64Array in_degree = wpArrayAllocCapacity(u64, &local_arena, graph->capacity, WP_ARRAY_INIT_FILLED);
|
||||
if (!in_degree) { return result; }
|
||||
memset(in_degree, 0, wpArrayCapacity(in_degree) * sizeof(u64));
|
||||
|
||||
for (u64 i = 0; i < graph->max_vertex_ever; i++) {
|
||||
if (!graph->vertices[i].active) { continue; }
|
||||
|
||||
PrGraphEdge *curr = graph->vertices[i].next_forward;
|
||||
while (curr) {
|
||||
in_degree[curr->target_idx]++;
|
||||
curr = curr->next_forward;
|
||||
}
|
||||
}
|
||||
|
||||
WpQueue queue = wpQueueAlloc(u64, &local_arena, graph->vertex_count);
|
||||
|
||||
for (u64 i = 0; i < graph->max_vertex_ever; i++) {
|
||||
if (!graph->vertices[i].active) { continue; }
|
||||
if (in_degree[i] == 0) {
|
||||
wpQueuePush(u64, &queue, &i);
|
||||
}
|
||||
}
|
||||
|
||||
while (queue.count > 0) {
|
||||
u64 *node_idx = wpQueuePop(u64, &queue);
|
||||
if (!node_idx) { break; }
|
||||
|
||||
wpArrayAppendCapped(u64, result, node_idx);
|
||||
|
||||
PrGraphEdge *curr = graph->vertices[*node_idx].next_forward;
|
||||
while (curr) {
|
||||
u64 target_idx = curr->target_idx;
|
||||
if (in_degree[target_idx] > 0) {
|
||||
in_degree[target_idx]--;
|
||||
if (in_degree[target_idx] == 0) {
|
||||
wpQueuePush(u64, &queue, &target_idx);
|
||||
}
|
||||
}
|
||||
curr = curr->next_forward;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Dump
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
wp_intern void prNodeManagerDumpGraph(const PrNodeManager *mgr) {
|
||||
const PrGraph *graph = &mgr->graph;
|
||||
printf("==============INPUTS==============\n");
|
||||
for (u64 i = 0; i < mgr->max_count_ever; ++i) {
|
||||
PrNodeId id = prNodeManagerGetNode(mgr, i);
|
||||
if (!prNodeManagerIsActiveNode(mgr, id)) { continue; }
|
||||
|
||||
printf("%" PRIu64 ":", id.index + 1);
|
||||
|
||||
PrGraphEdge *curr = graph->vertices[i].next_backward;
|
||||
if (!curr) {
|
||||
printf(" (none)");
|
||||
} else {
|
||||
do {
|
||||
printf(" %" PRIu64, curr->source_idx + 1);
|
||||
curr = curr->next_backward;
|
||||
} while (curr);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
printf(" backward ◀──");
|
||||
e = n->backward_head;
|
||||
if (!e) { printf(" (none)"); }
|
||||
while (e) {
|
||||
PrGraphNode *sv = &g->nodes[e->source_idx];
|
||||
printf(" %llu[%d]", (unsigned long long)e->source_idx, sv->value);
|
||||
e = e->next_backward;
|
||||
if (e) { printf(","); }
|
||||
printf("==============OUTPUTS==============\n");
|
||||
for (u64 i = 0; i < mgr->max_count_ever; ++i) {
|
||||
PrNodeId id = prNodeManagerGetNode(mgr, i);
|
||||
if (!prNodeManagerIsActiveNode(mgr, id)) { continue; }
|
||||
|
||||
printf("%" PRIu64 ":", id.index + 1);
|
||||
|
||||
PrGraphEdge *curr = graph->vertices[i].next_forward;
|
||||
if (!curr) {
|
||||
printf(" (none)");
|
||||
} else {
|
||||
do {
|
||||
printf(" %" PRIu64, curr->target_idx + 1);
|
||||
curr = curr->next_forward;
|
||||
} while (curr);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Demo
|
||||
* Main
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
int main(void) {
|
||||
srand((unsigned)time(NULL));
|
||||
|
||||
WpAllocator arena = wpMemArenaAllocatorInitZero(KiB(64));
|
||||
i32 main(void) {
|
||||
WpAllocator arena = wpMemArenaAllocatorInitZero(MiB(16));
|
||||
if (wpMemAllocatorInvalid(&arena)) {
|
||||
fprintf(stderr, "arena init failed\n");
|
||||
wpLogFatal(&_log, wpStr8Lit("arena init failed"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
PrGraph g;
|
||||
prGraphInit(&g, &arena, 16);
|
||||
PrNodeManager mgr = {0};
|
||||
prNodeManagerInit(&mgr, &arena, 128);
|
||||
|
||||
printf("=== Add 6 nodes ===\n");
|
||||
PrHandle nodes[10];
|
||||
for (u64 i = 0; i < 6; i++) {
|
||||
nodes[i] = prNodeAdd(&g, rand() % 100);
|
||||
printf(" node[%llu] = handle{%llu g%llu} val=%d\n",
|
||||
(unsigned long long)i,
|
||||
(unsigned long long)nodes[i].index,
|
||||
(unsigned long long)nodes[i].generation,
|
||||
*prNodeGetValue(&g, nodes[i]));
|
||||
PrNodeId n1 = prNodeManagerAddNode(&mgr, PR_NODE_TYPE_READ);
|
||||
PrNodeId n2 = prNodeManagerAddNode(&mgr, PR_NODE_TYPE_READ);
|
||||
PrNodeId n3 = prNodeManagerAddNode(&mgr, PR_NODE_TYPE_READ);
|
||||
PrNodeId n4 = prNodeManagerAddNode(&mgr, PR_NODE_TYPE_READ);
|
||||
PrNodeId n5 = prNodeManagerAddNode(&mgr, PR_NODE_TYPE_READ);
|
||||
|
||||
prNodeManagerAddEdge(&mgr, n1, n2);
|
||||
prNodeManagerAddEdge(&mgr, n1, n3);
|
||||
prNodeManagerAddEdge(&mgr, n1, n5);
|
||||
prNodeManagerAddEdge(&mgr, n2, n4);
|
||||
prNodeManagerAddEdge(&mgr, n3, n4);
|
||||
prNodeManagerAddEdge(&mgr, n3, n5);
|
||||
|
||||
prNodeManagerDumpGraph(&mgr);
|
||||
|
||||
prNodeManagerRemoveNode(&mgr, n3);
|
||||
|
||||
printf("\n");
|
||||
prNodeManagerDumpGraph(&mgr);
|
||||
|
||||
PrNodeId n6 = prNodeManagerAddNode(&mgr, PR_NODE_TYPE_READ);
|
||||
|
||||
prNodeManagerAddEdge(&mgr, n4, n6);
|
||||
|
||||
printf("\n");
|
||||
prNodeManagerDumpGraph(&mgr);
|
||||
|
||||
prNodeManagerRemoveNode(&mgr, n5);
|
||||
|
||||
printf("\n");
|
||||
prNodeManagerDumpGraph(&mgr);
|
||||
|
||||
PrNodeId n7 = prNodeManagerAddNode(&mgr, PR_NODE_TYPE_READ);
|
||||
|
||||
prNodeManagerAddEdge(&mgr, n1, n7);
|
||||
prNodeManagerAddEdge(&mgr, n2, n7);
|
||||
|
||||
printf("\n");
|
||||
prNodeManagerDumpGraph(&mgr);
|
||||
|
||||
PrNodeId n8 = prNodeManagerAddNode(&mgr, PR_NODE_TYPE_READ);
|
||||
|
||||
prNodeManagerAddEdge(&mgr, n6, n8);
|
||||
prNodeManagerAddEdge(&mgr, n7, n8);
|
||||
|
||||
printf("\n");
|
||||
prNodeManagerDumpGraph(&mgr);
|
||||
|
||||
printf("\n=== Try adding cycle 8→1 (should be rejected) ===\n");
|
||||
prNodeManagerAddEdge(&mgr, n8, n1);
|
||||
|
||||
printf("\n=== Try adding 8→1 again (still rejected) ===\n");
|
||||
prNodeManagerAddEdge(&mgr, n8, n1);
|
||||
|
||||
printf("\n=== Try adding cycle 7→2 (should be rejected) ===\n\n");
|
||||
prNodeManagerAddEdge(&mgr, n7, n2);
|
||||
|
||||
prNodeManagerDumpGraph(&mgr);
|
||||
|
||||
printf("\n=== Topological sort ===\n");
|
||||
WpU64Array sorted = prGraphTopologicalSort(&mgr.graph, &arena);
|
||||
if (sorted) {
|
||||
u64 n = wpArrayCount(sorted);
|
||||
printf(" count: %llu / %llu active\n",
|
||||
(unsigned long long)n,
|
||||
(unsigned long long)mgr.count);
|
||||
for (u64 i = 0; i < n; i++) {
|
||||
PrNodeId id = prNodeManagerGetNode(&mgr, sorted[i]);
|
||||
printf(" [%llu] idx=%llu gen=%llu\n",
|
||||
(unsigned long long)i,
|
||||
(unsigned long long)sorted[i],
|
||||
(unsigned long long)id.generation);
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n=== Add edges: 0→1, 0→2, 1→3, 2→3, 3→4, 4→5 ===\n");
|
||||
u64 edge_list[][2] = { {0,1}, {0,2}, {1,3}, {2,3}, {3,4}, {4,5} };
|
||||
for (u64 i = 0; i < 6; i++) {
|
||||
u64 f = edge_list[i][0], t = edge_list[i][1];
|
||||
prEdgeAdd(&g, nodes[f], nodes[t]);
|
||||
}
|
||||
prDump(&g);
|
||||
|
||||
printf("=== Remove node 2 (index %llu) ===\n",
|
||||
(unsigned long long)nodes[2].index);
|
||||
prNodeRemove(&g, nodes[2]);
|
||||
prDump(&g);
|
||||
|
||||
printf("=== Stale check ===\n");
|
||||
printf(" nodes[2] handle{%llu g%llu} valid? %s\n",
|
||||
(unsigned long long)nodes[2].index,
|
||||
(unsigned long long)nodes[2].generation,
|
||||
prHandleValid(&g, nodes[2]) ? "YES" : "NO");
|
||||
|
||||
printf("=== Add edge 5→0 (reuses freed edge slots) ===\n");
|
||||
prEdgeAdd(&g, nodes[5], nodes[0]);
|
||||
prDump(&g);
|
||||
|
||||
printf("=== Add a new node (reuses freed node slot) ===\n");
|
||||
nodes[6] = prNodeAdd(&g, 42);
|
||||
printf(" new node = handle{%llu g%llu} val=%d\n",
|
||||
(unsigned long long)nodes[6].index,
|
||||
(unsigned long long)nodes[6].generation,
|
||||
*prNodeGetValue(&g, nodes[6]));
|
||||
prDump(&g);
|
||||
|
||||
wpMemArenaAllocatorDestroy(&arena);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
|
||||
#include "../src/wapp/wapp.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Pool allocator (arena-backed, intrusive free list)
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
typedef struct PrPoolFreeNode PrPoolFreeNode;
|
||||
struct PrPoolFreeNode {
|
||||
PrPoolFreeNode *next;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
WpAllocator *allocator;
|
||||
PrPoolFreeNode *free_head;
|
||||
u64 slot_size;
|
||||
} PrPool;
|
||||
|
||||
wp_intern void prPoolInit(PrPool *pool, WpAllocator *allocator, u64 slot_size) {
|
||||
pool->allocator = allocator;
|
||||
pool->free_head = NULL;
|
||||
pool->slot_size = slot_size;
|
||||
}
|
||||
|
||||
wp_intern void *prPoolAlloc(PrPool *pool) {
|
||||
if (pool->free_head) {
|
||||
PrPoolFreeNode *node = pool->free_head;
|
||||
pool->free_head = node->next;
|
||||
return node;
|
||||
}
|
||||
return wpMemAllocatorAlloc(pool->allocator, pool->slot_size);
|
||||
}
|
||||
|
||||
wp_intern void prPoolFree(PrPool *pool, void *slot) {
|
||||
if (!slot) { return; }
|
||||
PrPoolFreeNode *node = (PrPoolFreeNode *)slot;
|
||||
node->next = pool->free_head;
|
||||
pool->free_head = node;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Graph
|
||||
* -------------------------------------------------------------------------*/
|
||||
|
||||
#define INVALID_NODE_INDEX (u32)-1
|
||||
#define INVALID_NODE_ID ((PrNodeId){ .index = INVALID_NODE_INDEX, .generation = INVALID_NODE_INDEX })
|
||||
|
||||
typedef enum {
|
||||
PR_NODE_TYPE_NONE,
|
||||
PR_NODE_TYPE_READ,
|
||||
PR_NODE_TYPE_BLUR,
|
||||
PR_NODE_TYPE_GRADE,
|
||||
|
||||
COUNT_NODE_TYPES
|
||||
} PrNodeType;
|
||||
|
||||
typedef struct {
|
||||
u32 index;
|
||||
u32 generation;
|
||||
} PrNodeId;
|
||||
|
||||
typedef struct {
|
||||
union {
|
||||
WpStr8 path;
|
||||
f32 blur;
|
||||
f32 gain;
|
||||
} params;
|
||||
PrNodeType type;
|
||||
u32 generation;
|
||||
u32 next_free;
|
||||
} PrNode;
|
||||
typedef PrNode *PrNodeArray;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Graph Edge/Vertex type
|
||||
* If source or target are INVALID_NODE_ID, it should be treated as a vertex.
|
||||
* Otherwise, it should be treated as an edge. Think of it like homogeneous
|
||||
* coordinates where a vector {0, 0, 0, 0} is treated as a direction, while
|
||||
* a vector {0, 0, 0, 1} is treated as a point
|
||||
* ---------------------------------------------------------------------------*/
|
||||
typedef struct PrEdgeVertex PrEdgeVertex;
|
||||
struct PrEdgeVertex {
|
||||
PrEdgeVertex *next;
|
||||
PrEdgeVertex *prev;
|
||||
PrNodeId source;
|
||||
PrNodeId target;
|
||||
};
|
||||
typedef PrEdgeVertex *PrEdgeVertexArray;
|
||||
|
||||
typedef struct {
|
||||
PrPool vertex_pool;
|
||||
PrNodeArray nodes;
|
||||
PrEdgeVertexArray vertices;
|
||||
u64 capacity;
|
||||
u64 max_count_ever;
|
||||
u64 count;
|
||||
u32 free_head;
|
||||
} PrGraph;
|
||||
|
||||
wp_intern b8 prGraphIsStaleId(const PrGraph *graph, PrNodeId id) {
|
||||
u32 generation = graph->nodes[id.index].generation;
|
||||
return id.generation != generation;
|
||||
}
|
||||
|
||||
wp_intern b8 prGraphIsActiveNode(const PrGraph *graph, PrNodeId id) {
|
||||
u32 next_free = graph->nodes[id.index].next_free;
|
||||
return !prGraphIsStaleId(graph, id) && next_free == INVALID_NODE_INDEX;
|
||||
}
|
||||
|
||||
wp_intern PrNodeId prGraphGetNode(const PrGraph *graph, u32 index) {
|
||||
return (PrNodeId){ .index = index, .generation = graph->nodes[index].generation };
|
||||
}
|
||||
|
||||
wp_intern void prGraphInit(PrGraph *graph, WpAllocator *allocator, u64 capacity) {
|
||||
graph->nodes = wpArrayAllocCapacity(PrNode, allocator, capacity, WP_ARRAY_INIT_FILLED);
|
||||
graph->vertices = wpArrayAllocCapacity(PrEdgeVertex, allocator, capacity, WP_ARRAY_INIT_FILLED);
|
||||
graph->free_head = 0;
|
||||
graph->capacity = capacity;
|
||||
graph->max_count_ever = 0;
|
||||
graph->count = 0;
|
||||
|
||||
prPoolInit(&graph->vertex_pool, allocator, sizeof(PrEdgeVertex));
|
||||
memset(graph->nodes, 0, capacity * sizeof(PrNode));
|
||||
memset(graph->vertices, 0, capacity * sizeof(PrEdgeVertex));
|
||||
|
||||
for (u64 i = 0; i < capacity; ++i) {
|
||||
graph->nodes[i].next_free = i < capacity - 1 ? i + 1 : INVALID_NODE_INDEX;
|
||||
graph->vertices[i].source = graph->vertices[i].target = INVALID_NODE_ID;
|
||||
}
|
||||
}
|
||||
|
||||
wp_intern void prGraphAddEdge(PrGraph *graph, PrNodeId from, PrNodeId to) {
|
||||
if (!prGraphIsActiveNode(graph, from) || !prGraphIsActiveNode(graph, to)) { return; }
|
||||
|
||||
PrEdgeVertex *edge = (PrEdgeVertex *)prPoolAlloc(&graph->vertex_pool);
|
||||
|
||||
PrEdgeVertex *src = &graph->vertices[from.index];
|
||||
PrEdgeVertex *dst = &graph->vertices[to.index];
|
||||
|
||||
edge->source = from;
|
||||
edge->target = to;
|
||||
edge->next = src->next;
|
||||
edge->prev = dst->prev;
|
||||
src->next = edge;
|
||||
dst->prev = edge;
|
||||
}
|
||||
|
||||
wp_intern PrNodeId prGraphAddNode(PrGraph *graph, PrNodeType type) {
|
||||
u32 idx = graph->free_head;
|
||||
if (idx == INVALID_NODE_INDEX) { return INVALID_NODE_ID; }
|
||||
|
||||
PrNode *node = &graph->nodes[idx];
|
||||
|
||||
graph->free_head = node->next_free;
|
||||
node->next_free = INVALID_NODE_INDEX;
|
||||
node->type = type;
|
||||
memset(&node->params, 0, sizeof(node->params));
|
||||
|
||||
graph->count++;
|
||||
|
||||
if (idx + 1 > graph->max_count_ever) { graph->max_count_ever = idx + 1; }
|
||||
|
||||
return (PrNodeId){ .index = idx, .generation = node->generation };
|
||||
}
|
||||
|
||||
wp_intern void _unlinkForward(PrGraph *graph, PrNodeId from, PrEdgeVertex *edge) {
|
||||
if (!prGraphIsActiveNode(graph, from)) { return; }
|
||||
|
||||
PrEdgeVertex *src = &graph->vertices[from.index];
|
||||
PrEdgeVertex *curr = src->next;
|
||||
PrEdgeVertex *prev = NULL;
|
||||
while (curr) {
|
||||
if (curr == edge) {
|
||||
if (prev) {
|
||||
prev->next = curr->next;
|
||||
} else {
|
||||
src->next = curr->next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
prev = curr;
|
||||
curr = curr->next;
|
||||
}
|
||||
}
|
||||
|
||||
wp_intern void _unlinkBackward(PrGraph *graph, PrNodeId to, PrEdgeVertex *edge) {
|
||||
if (!prGraphIsActiveNode(graph, to)) { return; }
|
||||
|
||||
PrEdgeVertex *dst = &graph->vertices[to.index];
|
||||
PrEdgeVertex *curr = dst->prev;
|
||||
PrEdgeVertex *prev = NULL;
|
||||
while (curr) {
|
||||
if (curr == edge) {
|
||||
if (prev) {
|
||||
prev->prev = curr->prev;
|
||||
} else {
|
||||
dst->prev = curr->prev;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
prev = curr;
|
||||
curr = curr->prev;
|
||||
}
|
||||
}
|
||||
|
||||
wp_intern void prGraphRemoveNode(PrGraph *graph, PrNodeId id) {
|
||||
if (!prGraphIsActiveNode(graph, id)) { return; }
|
||||
|
||||
PrNode *node = &graph->nodes[id.index];
|
||||
u32 next_gen = node->generation + 1;
|
||||
memset(node, 0, sizeof(PrNode));
|
||||
|
||||
node->generation = next_gen;
|
||||
node->next_free = graph->free_head;
|
||||
graph->free_head = id.index;
|
||||
|
||||
PrEdgeVertex *vertex = &graph->vertices[id.index];
|
||||
if (!(vertex->next) && !(vertex->prev)) {
|
||||
goto REMOVE_NODE_UNSET_POINTERS;
|
||||
}
|
||||
|
||||
PrEdgeVertex *curr = vertex->next;
|
||||
while (curr) {
|
||||
PrEdgeVertex *next = curr->next;
|
||||
_unlinkBackward(graph, curr->target, curr);
|
||||
prPoolFree(&graph->vertex_pool, curr);
|
||||
curr = next;
|
||||
}
|
||||
|
||||
curr = vertex->prev;
|
||||
while (curr) {
|
||||
PrEdgeVertex *prev = curr->prev;
|
||||
_unlinkForward(graph, curr->source, curr);
|
||||
prPoolFree(&graph->vertex_pool, curr);
|
||||
curr = prev;
|
||||
}
|
||||
|
||||
REMOVE_NODE_UNSET_POINTERS:
|
||||
vertex->next = vertex->prev = NULL;
|
||||
|
||||
graph->count--;
|
||||
}
|
||||
|
||||
wp_intern void prGraphDump(const PrGraph *graph) {
|
||||
printf("==============INPUTS==============\n");
|
||||
for (u64 i = 0; i < graph->max_count_ever; ++i) {
|
||||
PrNodeId id = prGraphGetNode(graph, i);
|
||||
if (!prGraphIsActiveNode(graph, id)) { continue; }
|
||||
|
||||
PrEdgeVertex *vertex = &graph->vertices[i];
|
||||
|
||||
printf("%u:", id.index + 1);
|
||||
|
||||
if (!(vertex->prev)) {
|
||||
printf(" (none)");
|
||||
} else {
|
||||
PrEdgeVertex *curr = vertex->prev;
|
||||
while (curr) {
|
||||
printf(" %u", curr->source.index + 1);
|
||||
curr = curr->prev;
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
printf("==============OUTPUTS==============\n");
|
||||
for (u64 i = 0; i < graph->max_count_ever; ++i) {
|
||||
PrNodeId id = prGraphGetNode(graph, i);
|
||||
if (!prGraphIsActiveNode(graph, id)) { continue; }
|
||||
|
||||
PrEdgeVertex *vertex = &graph->vertices[i];
|
||||
|
||||
printf("%u:", id.index + 1);
|
||||
|
||||
if (!(vertex->next)) {
|
||||
printf(" (none)");
|
||||
} else {
|
||||
PrEdgeVertex *curr = vertex->next;
|
||||
while (curr) {
|
||||
printf(" %u", curr->target.index + 1);
|
||||
curr = curr->next;
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
i32 main(void) {
|
||||
WpAllocator arena = wpMemArenaAllocatorInitZero(MiB(16));
|
||||
|
||||
PrGraph graph = {0};
|
||||
prGraphInit(&graph, &arena, 128);
|
||||
|
||||
PrNodeId n1 = prGraphAddNode(&graph, PR_NODE_TYPE_READ);
|
||||
PrNodeId n2 = prGraphAddNode(&graph, PR_NODE_TYPE_READ);
|
||||
PrNodeId n3 = prGraphAddNode(&graph, PR_NODE_TYPE_READ);
|
||||
PrNodeId n4 = prGraphAddNode(&graph, PR_NODE_TYPE_READ);
|
||||
PrNodeId n5 = prGraphAddNode(&graph, PR_NODE_TYPE_READ);
|
||||
|
||||
prGraphAddEdge(&graph, n1, n2);
|
||||
prGraphAddEdge(&graph, n1, n3);
|
||||
prGraphAddEdge(&graph, n1, n5);
|
||||
prGraphAddEdge(&graph, n2, n4);
|
||||
prGraphAddEdge(&graph, n3, n4);
|
||||
prGraphAddEdge(&graph, n3, n5);
|
||||
|
||||
prGraphDump(&graph);
|
||||
|
||||
prGraphRemoveNode(&graph, n3);
|
||||
|
||||
printf("\n");
|
||||
prGraphDump(&graph);
|
||||
|
||||
PrNodeId n6 = prGraphAddNode(&graph, PR_NODE_TYPE_READ);
|
||||
|
||||
prGraphAddEdge(&graph, n4, n6);
|
||||
|
||||
printf("\n");
|
||||
prGraphDump(&graph);
|
||||
|
||||
prGraphRemoveNode(&graph, n5);
|
||||
|
||||
printf("\n");
|
||||
prGraphDump(&graph);
|
||||
|
||||
PrNodeId n7 = prGraphAddNode(&graph, PR_NODE_TYPE_READ);
|
||||
|
||||
prGraphAddEdge(&graph, n1, n7);
|
||||
prGraphAddEdge(&graph, n2, n7);
|
||||
|
||||
printf("\n");
|
||||
prGraphDump(&graph);
|
||||
|
||||
PrNodeId n8 = prGraphAddNode(&graph, PR_NODE_TYPE_READ);
|
||||
|
||||
prGraphAddEdge(&graph, n6, n8);
|
||||
prGraphAddEdge(&graph, n7, n8);
|
||||
|
||||
printf("\n");
|
||||
prGraphDump(&graph);
|
||||
|
||||
wpMemArenaAllocatorDestroy(&arena);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
|
||||
#ifndef API_H
|
||||
#define API_H
|
||||
|
||||
void prSayHello(void);
|
||||
|
||||
#ifdef PR_PLATFORM_LINUX
|
||||
#include "linux/aliases.h"
|
||||
#include "linux/api.h"
|
||||
#elif defined(PR_PLATFORM_MACOS)
|
||||
#include "macos/aliases.h"
|
||||
#include "macos/api.h"
|
||||
#else
|
||||
#error "Unrecognised platform"
|
||||
#endif
|
||||
|
||||
#endif // !API_H
|
||||
@@ -0,0 +1,8 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
|
||||
#ifndef LINUX_ALIASES_H
|
||||
#define LINUX_ALIASES_H
|
||||
|
||||
#define prSayHello prSayHelloLinux
|
||||
|
||||
#endif // !LINUX_ALIASES_H
|
||||
@@ -0,0 +1,7 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
void prSayHelloLinux(void) {
|
||||
printf("Hello from Linux\n");
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
|
||||
#ifndef LINUX_API_H
|
||||
#define LINUX_API_H
|
||||
|
||||
void prSayHelloLinux(void);
|
||||
|
||||
#endif // !LINUX_API_H
|
||||
@@ -0,0 +1,8 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
|
||||
#ifndef MACOS_ALIASES_H
|
||||
#define MACOS_ALIASES_H
|
||||
|
||||
#define prSayHello prSayHelloMac
|
||||
|
||||
#endif // !MACOS_ALIASES_H
|
||||
@@ -0,0 +1,7 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
void prSayHelloMac(void) {
|
||||
printf("Hello from macOS\n");
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
|
||||
#ifndef MACOS_API_H
|
||||
#define MACOS_API_H
|
||||
|
||||
void prSayHelloMac(void);
|
||||
|
||||
#endif // !MACOS_API_H
|
||||
+642
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
//
|
||||
// Prism RHI — Render Hardware Interface
|
||||
//
|
||||
// Design based on the How To Vulkan tutorial (https://www.howtovulkan.com/)
|
||||
// which uses these modern Vulkan features:
|
||||
// - Vulkan 1.3 core (dynamic rendering, synchronization2, buffer device
|
||||
// address)
|
||||
// - Descriptor indexing (bindless with variable descriptor count)
|
||||
// - Dynamic state (viewport, scissor)
|
||||
// - VMA for memory management
|
||||
// - Vulkan profiles for device selection
|
||||
// - Slang for shader compilation (SPIR-V output)
|
||||
//
|
||||
// This RHI abstracts those features behind platform-agnostic types.
|
||||
// Backend selection is compile-time — define PR_RHI_VULKAN, PR_RHI_D3D12,
|
||||
// or PR_RHI_METAL at build time.
|
||||
//
|
||||
// Creation functions return the object directly or abort on failure.
|
||||
// Only swapchain acquire/present return a b8 — the caller can detect
|
||||
// out-of-date and recreate.
|
||||
|
||||
#ifndef PR_RHI_H
|
||||
#define PR_RHI_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
|
||||
// ======================================================================
|
||||
|
||||
PrRhiInstance *prRhiCreateInstance(PrRhiInstanceDesc desc);
|
||||
void prRhiDestroyInstance(PrRhiInstance *inst);
|
||||
|
||||
// ======================================================================
|
||||
// Physical device enumeration
|
||||
// ======================================================================
|
||||
|
||||
PrRhiPhysicalDeviceArray prRhiGetPhysicalDevices(PrRhiInstance *inst);
|
||||
void prRhiGetPhysicalDeviceName(PrRhiPhysicalDevice *pdev, WpStr8 *out);
|
||||
void prRhiGetPhysicalDeviceDriverInfo(PrRhiPhysicalDevice *pdev, WpStr8 *out);
|
||||
PrRhiPhysicalDeviceProperties prRhiGetPhysicalDeviceProperties(PrRhiPhysicalDevice *pdev);
|
||||
|
||||
// ======================================================================
|
||||
// Surface (platform-specific)
|
||||
// ======================================================================
|
||||
|
||||
PrRhiSurface *prRhiCreateSurfaceFromWindow(PrRhiInstance *inst, SDL_Window *window);
|
||||
void prRhiDestroySurface(PrRhiInstance *inst, PrRhiSurface *surface);
|
||||
PrRhiSurfaceCapabilities prRhiGetSurfaceCapabilities(PrRhiPhysicalDevice *pdev,
|
||||
PrRhiSurface *surface);
|
||||
|
||||
// ======================================================================
|
||||
// Device
|
||||
// ======================================================================
|
||||
|
||||
PrRhiDevice *prRhiCreateDevice(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface,
|
||||
PrRhiDeviceDesc desc);
|
||||
void prRhiDestroyDevice(PrRhiDevice *device);
|
||||
void prRhiDeviceWaitIdle(PrRhiDevice *device);
|
||||
u32 prRhiGetQueueFamilyIndex(PrRhiDevice *device);
|
||||
|
||||
// ======================================================================
|
||||
// Swapchain
|
||||
// ======================================================================
|
||||
|
||||
PrRhiSwapchain *prRhiCreateSwapchain(PrRhiDevice *device, PrRhiSwapchainDesc desc);
|
||||
void prRhiDestroySwapchain(PrRhiDevice *device, PrRhiSwapchain *swapchain);
|
||||
|
||||
u32 prRhiGetSwapchainImageCount(PrRhiSwapchain *swapchain);
|
||||
PrRhiSwapchainResult prRhiAcquireNextImage(PrRhiDevice *device, PrRhiSwapchain *swapchain,
|
||||
PrRhiSemaphore *signal_semaphore, u32 *out_image_index);
|
||||
|
||||
PrRhiSwapchainResult prRhiPresent(PrRhiDevice *device, PrRhiSwapchain *swapchain,
|
||||
PrRhiSemaphore *wait_semaphore);
|
||||
|
||||
void prRhiRecreateSwapchain(PrRhiDevice *device, PrRhiSwapchain **swapchain,
|
||||
u32 width, u32 height);
|
||||
|
||||
PrRhiTexture *prRhiGetSwapchainTexture(PrRhiSwapchain *swapchain, u32 image_index);
|
||||
PrRhiTexture *prRhiGetSwapchainDepthTexture(PrRhiSwapchain *swapchain);
|
||||
PrRhiFormat prRhiGetSwapchainFormat(PrRhiSwapchain *swapchain);
|
||||
|
||||
// ======================================================================
|
||||
// Buffers
|
||||
// ======================================================================
|
||||
|
||||
PrRhiBuffer *prRhiCreateBuffer(PrRhiDevice *device, PrRhiBufferDesc desc);
|
||||
void prRhiDestroyBuffer(PrRhiDevice *device, PrRhiBuffer *buffer);
|
||||
|
||||
void *prRhiBufferMap(PrRhiDevice *device, PrRhiBuffer *buffer);
|
||||
void prRhiBufferUnmap(PrRhiDevice *device, PrRhiBuffer *buffer);
|
||||
|
||||
PrRhiDeviceAddress prRhiGetBufferDeviceAddress(PrRhiDevice *device, PrRhiBuffer *buffer);
|
||||
|
||||
// ======================================================================
|
||||
// Textures
|
||||
// ======================================================================
|
||||
|
||||
PrRhiTexture *prRhiCreateTexture(PrRhiDevice *device, PrRhiTextureDesc desc);
|
||||
PrRhiTexture *prRhiCreateTextureFromKtx(PrRhiDevice *device, const char *path,
|
||||
PrRhiCommandPool *pool, PrRhiCommandBuffer *cb);
|
||||
void prRhiDestroyTexture(PrRhiDevice *device, PrRhiTexture *texture);
|
||||
PrRhiTextureSize prRhiGetTextureSize(PrRhiTexture *texture);
|
||||
|
||||
// ======================================================================
|
||||
// Samplers
|
||||
// ======================================================================
|
||||
|
||||
PrRhiSampler *prRhiCreateSampler(PrRhiDevice *device, PrRhiSamplerDesc desc);
|
||||
void prRhiDestroySampler(PrRhiDevice *device, PrRhiSampler *sampler);
|
||||
|
||||
// ======================================================================
|
||||
// Shaders (from SPIR-V)
|
||||
// ======================================================================
|
||||
|
||||
PrRhiShader *prRhiCreateShader(PrRhiDevice *device, PrRhiShaderDesc desc);
|
||||
void prRhiDestroyShader(PrRhiDevice *device, PrRhiShader *shader);
|
||||
|
||||
// ======================================================================
|
||||
// Pipeline layouts
|
||||
// ======================================================================
|
||||
|
||||
PrRhiPipelineLayout *prRhiCreatePipelineLayout(PrRhiDevice *device,
|
||||
PrRhiPipelineLayoutDesc desc);
|
||||
void prRhiDestroyPipelineLayout(PrRhiDevice *device,
|
||||
PrRhiPipelineLayout *layout);
|
||||
|
||||
// ======================================================================
|
||||
// Pipelines
|
||||
// ======================================================================
|
||||
|
||||
PrRhiPipeline *prRhiCreateGraphicsPipeline(PrRhiDevice *device,
|
||||
PrRhiGraphicsPipelineDesc desc);
|
||||
PrRhiPipeline *prRhiCreateComputePipeline(PrRhiDevice *device,
|
||||
PrRhiComputePipelineDesc desc);
|
||||
void prRhiDestroyPipeline(PrRhiDevice *device, PrRhiPipeline *pipeline);
|
||||
|
||||
// ======================================================================
|
||||
// Descriptor set layouts
|
||||
// ======================================================================
|
||||
|
||||
PrRhiDescriptorSetLayout *prRhiCreateDescriptorSetLayout(PrRhiDevice *device,
|
||||
PrRhiDescriptorSetLayoutDesc desc);
|
||||
void prRhiDestroyDescriptorSetLayout(PrRhiDevice *device,
|
||||
PrRhiDescriptorSetLayout *layout);
|
||||
|
||||
// ======================================================================
|
||||
// Descriptor pools
|
||||
// ======================================================================
|
||||
|
||||
PrRhiDescriptorPool *prRhiCreateDescriptorPool(PrRhiDevice *device,
|
||||
PrRhiDescriptorPoolDesc desc);
|
||||
void prRhiDestroyDescriptorPool(PrRhiDevice *device,
|
||||
PrRhiDescriptorPool *pool);
|
||||
void prRhiResetDescriptorPool(PrRhiDevice *device,
|
||||
PrRhiDescriptorPool *pool);
|
||||
|
||||
// ======================================================================
|
||||
// Descriptor sets
|
||||
// ======================================================================
|
||||
|
||||
PrRhiDescriptorSet *prRhiAllocateDescriptorSet(PrRhiDevice *device, PrRhiDescriptorPool *pool,
|
||||
PrRhiDescriptorSetLayout *layout,
|
||||
WpU32Array variable_descriptor_counts);
|
||||
void prRhiFreeDescriptorSet(PrRhiDevice *device, PrRhiDescriptorPool *pool,
|
||||
PrRhiDescriptorSet *set);
|
||||
void prRhiUpdateDescriptorSet(PrRhiDevice *device, PrRhiWriteDescriptorSetArray writes);
|
||||
|
||||
// ======================================================================
|
||||
// Fences and semaphores
|
||||
// ======================================================================
|
||||
|
||||
PrRhiFence *prRhiCreateFence(PrRhiDevice *device, PrRhiFenceDesc desc);
|
||||
void prRhiDestroyFence(PrRhiDevice *device, PrRhiFence *fence);
|
||||
|
||||
void prRhiWaitForFences(PrRhiDevice *device, PrRhiFenceArray fences, u32 count,
|
||||
b8 wait_all, u64 timeout_ns);
|
||||
void prRhiResetFences(PrRhiDevice *device, PrRhiFenceArray fences, u32 count);
|
||||
|
||||
PrRhiSemaphore *prRhiCreateSemaphore(PrRhiDevice *device);
|
||||
void prRhiDestroySemaphore(PrRhiDevice *device, PrRhiSemaphore *semaphore);
|
||||
|
||||
// ======================================================================
|
||||
// Command pools and command buffers
|
||||
// ======================================================================
|
||||
|
||||
PrRhiCommandPool *prRhiCreateCommandPool(PrRhiDevice *device);
|
||||
void prRhiDestroyCommandPool(PrRhiDevice *device, PrRhiCommandPool *pool);
|
||||
|
||||
PrRhiCommandBufferArray prRhiAllocateCommandBuffers(PrRhiDevice *device, PrRhiCommandPool *pool,
|
||||
u32 count);
|
||||
|
||||
void prRhiFreeCommandBuffers(PrRhiDevice *device, PrRhiCommandPool *pool,
|
||||
PrRhiCommandBufferArray buffers);
|
||||
|
||||
// ======================================================================
|
||||
// Command buffer recording
|
||||
// ======================================================================
|
||||
|
||||
void prRhiBeginCommandBuffer(PrRhiCommandBuffer *cb);
|
||||
void prRhiEndCommandBuffer(PrRhiCommandBuffer *cb);
|
||||
void prRhiResetCommandBuffer(PrRhiCommandBuffer *cb);
|
||||
|
||||
// --- Pipeline barriers (synchronization2 style) ---
|
||||
|
||||
void prRhiCmdPipelineBarrier(PrRhiCommandBuffer *cb,
|
||||
PrRhiImageMemoryBarrierArray image_barriers,
|
||||
PrRhiBufferMemoryBarrierArray buffer_barriers);
|
||||
|
||||
// --- Dynamic rendering ---
|
||||
|
||||
void prRhiCmdBeginRendering(PrRhiCommandBuffer *cb,
|
||||
PrRhiColorAttachmentArray color_attachments,
|
||||
const PrRhiDepthAttachment *depth_attachment);
|
||||
void prRhiCmdEndRendering(PrRhiCommandBuffer *cb);
|
||||
|
||||
// --- Dynamic state ---
|
||||
|
||||
void prRhiCmdSetViewport(PrRhiCommandBuffer *cb, f32 x, f32 y, f32 width, f32 height);
|
||||
void prRhiCmdSetScissor(PrRhiCommandBuffer *cb, i32 x, i32 y, u32 width, u32 height);
|
||||
|
||||
// --- Binding ---
|
||||
|
||||
void prRhiCmdBindPipeline(PrRhiCommandBuffer *cb, PrRhiPipelineBindPoint bind_point,
|
||||
PrRhiPipeline *pipeline);
|
||||
void prRhiCmdBindDescriptorSets(PrRhiCommandBuffer *cb, PrRhiPipelineBindPoint bind_point,
|
||||
PrRhiPipelineLayout *layout, u32 first_set,
|
||||
PrRhiDescriptorSetArray sets);
|
||||
void prRhiCmdPushConstants(PrRhiCommandBuffer *cb, PrRhiPipelineLayout *layout,
|
||||
PrRhiShaderStage stage_flags, u32 offset, u32 size,
|
||||
const void *data);
|
||||
|
||||
// --- Vertex / index buffers ---
|
||||
|
||||
void prRhiCmdBindVertexBuffers(PrRhiCommandBuffer *cb, u32 first_binding, PrRhiBufferArray buffers,
|
||||
WpU64Array offsets);
|
||||
void prRhiCmdBindIndexBuffer(PrRhiCommandBuffer *cb, PrRhiBuffer *buffer, u64 offset,
|
||||
PrRhiIndexType index_type);
|
||||
|
||||
// --- Draw calls ---
|
||||
|
||||
void prRhiCmdDraw(PrRhiCommandBuffer *cb, u32 vertex_count, u32 instance_count,
|
||||
u32 first_vertex, u32 first_instance);
|
||||
void prRhiCmdDrawIndexed(PrRhiCommandBuffer *cb, u32 index_count, u32 instance_count,
|
||||
u32 first_index, i32 vertex_offset, u32 first_instance);
|
||||
|
||||
// --- Copy ---
|
||||
|
||||
void prRhiCmdCopyBufferToImage(PrRhiCommandBuffer *cb, PrRhiBuffer *src, PrRhiTexture *dst,
|
||||
PrRhiBufferImageCopyArray copies);
|
||||
|
||||
// ======================================================================
|
||||
// Queue submission
|
||||
// ======================================================================
|
||||
|
||||
void prRhiQueueSubmit(PrRhiDevice *device, PrRhiCommandBuffer *cb,
|
||||
PrRhiSemaphore *wait_semaphore,
|
||||
PrRhiSemaphore *signal_semaphore, PrRhiFence *fence);
|
||||
|
||||
// ======================================================================
|
||||
// Backend dispatch
|
||||
// ======================================================================
|
||||
|
||||
#if defined(PR_RHI_VULKAN)
|
||||
# include "vulkan/pr_rhi_vk_aliases.h"
|
||||
#elif defined(PR_RHI_D3D12)
|
||||
# error "D3D12 backend not yet implemented"
|
||||
#elif defined(PR_RHI_METAL)
|
||||
# error "Metal backend not yet implemented"
|
||||
#else
|
||||
# error "Define one of: PR_RHI_VULKAN, PR_RHI_D3D12, PR_RHI_METAL"
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,501 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
//
|
||||
// Shared RHI types — enums, description structs, and opaque handle
|
||||
// forward declarations. Included by both the umbrella pr_rhi.h and
|
||||
// backend headers.
|
||||
|
||||
#ifndef PR_RHI_TYPES_H
|
||||
#define PR_RHI_TYPES_H
|
||||
|
||||
#include "../../vendor/wapp/wapp.h"
|
||||
|
||||
#define PR_RHI_LOD_CLAMP_NONE 1000.0f
|
||||
|
||||
// ============================================================================
|
||||
// Opaque handle types
|
||||
// ============================================================================
|
||||
|
||||
typedef struct PrRhiInstance PrRhiInstance;
|
||||
typedef struct PrRhiPhysicalDevice PrRhiPhysicalDevice;
|
||||
typedef struct PrRhiDevice PrRhiDevice;
|
||||
typedef struct PrRhiSurface PrRhiSurface;
|
||||
typedef struct PrRhiSwapchain PrRhiSwapchain;
|
||||
typedef struct PrRhiBuffer PrRhiBuffer;
|
||||
typedef struct PrRhiTexture PrRhiTexture;
|
||||
typedef struct PrRhiSampler PrRhiSampler;
|
||||
typedef struct PrRhiShader PrRhiShader;
|
||||
typedef struct PrRhiPipelineLayout PrRhiPipelineLayout;
|
||||
typedef struct PrRhiPipeline PrRhiPipeline;
|
||||
typedef struct PrRhiDescriptorSetLayout PrRhiDescriptorSetLayout;
|
||||
typedef struct PrRhiDescriptorPool PrRhiDescriptorPool;
|
||||
typedef struct PrRhiDescriptorSet PrRhiDescriptorSet;
|
||||
typedef struct PrRhiCommandPool PrRhiCommandPool;
|
||||
typedef struct PrRhiCommandBuffer PrRhiCommandBuffer;
|
||||
typedef struct PrRhiFence PrRhiFence;
|
||||
typedef struct PrRhiSemaphore PrRhiSemaphore;
|
||||
|
||||
// ============================================================================
|
||||
// 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 {
|
||||
PR_RHI_SWAPCHAIN_SUCCESS = 0,
|
||||
PR_RHI_SWAPCHAIN_OUT_OF_DATE,
|
||||
} PrRhiSwapchainResult;
|
||||
|
||||
typedef enum PrRhiBufferUsage {
|
||||
PR_RHI_BUFFER_USAGE_VERTEX = 1 << 0,
|
||||
PR_RHI_BUFFER_USAGE_INDEX = 1 << 1,
|
||||
PR_RHI_BUFFER_USAGE_UNIFORM = 1 << 2,
|
||||
PR_RHI_BUFFER_USAGE_STORAGE = 1 << 3,
|
||||
PR_RHI_BUFFER_USAGE_TRANSFER_SRC = 1 << 4,
|
||||
PR_RHI_BUFFER_USAGE_TRANSFER_DST = 1 << 5,
|
||||
PR_RHI_BUFFER_USAGE_SHADER_DEVICE_ADDRESS = 1 << 6,
|
||||
} PrRhiBufferUsage;
|
||||
|
||||
typedef enum PrRhiTextureUsage {
|
||||
PR_RHI_TEXTURE_USAGE_SAMPLED = 1 << 0,
|
||||
PR_RHI_TEXTURE_USAGE_COLOR_ATTACHMENT = 1 << 1,
|
||||
PR_RHI_TEXTURE_USAGE_DEPTH_ATTACHMENT = 1 << 2,
|
||||
PR_RHI_TEXTURE_USAGE_STORAGE = 1 << 3,
|
||||
PR_RHI_TEXTURE_USAGE_TRANSFER_SRC = 1 << 4,
|
||||
PR_RHI_TEXTURE_USAGE_TRANSFER_DST = 1 << 5,
|
||||
} PrRhiTextureUsage;
|
||||
|
||||
typedef enum PrRhiMemoryUsage {
|
||||
PR_RHI_MEMORY_GPU_ONLY,
|
||||
PR_RHI_MEMORY_CPU_TO_GPU,
|
||||
PR_RHI_MEMORY_CPU_ONLY,
|
||||
} PrRhiMemoryUsage;
|
||||
|
||||
typedef enum PrRhiFormat {
|
||||
PR_RHI_FORMAT_UNDEFINED,
|
||||
PR_RHI_FORMAT_R8G8B8A8_SRGB,
|
||||
PR_RHI_FORMAT_R8G8B8A8_UNORM,
|
||||
PR_RHI_FORMAT_R16G16B16A16_SFLOAT,
|
||||
PR_RHI_FORMAT_R32G32B32A32_SFLOAT,
|
||||
PR_RHI_FORMAT_R32G32B32_SFLOAT,
|
||||
PR_RHI_FORMAT_R32G32_SFLOAT,
|
||||
PR_RHI_FORMAT_R32_SFLOAT,
|
||||
PR_RHI_FORMAT_D24_UNORM_S8_UINT,
|
||||
PR_RHI_FORMAT_D32_SFLOAT_S8_UINT,
|
||||
PR_RHI_FORMAT_B8G8R8A8_SRGB,
|
||||
} PrRhiFormat;
|
||||
|
||||
typedef enum PrRhiImageLayout {
|
||||
PR_RHI_LAYOUT_UNDEFINED,
|
||||
PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL,
|
||||
PR_RHI_LAYOUT_READ_ONLY_OPTIMAL,
|
||||
PR_RHI_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
PR_RHI_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
PR_RHI_LAYOUT_PRESENT_SRC,
|
||||
PR_RHI_LAYOUT_GENERAL,
|
||||
} PrRhiImageLayout;
|
||||
|
||||
typedef enum PrRhiShaderStage {
|
||||
PR_RHI_SHADER_STAGE_VERTEX = 1 << 0,
|
||||
PR_RHI_SHADER_STAGE_FRAGMENT = 1 << 1,
|
||||
PR_RHI_SHADER_STAGE_COMPUTE = 1 << 2,
|
||||
} 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 {
|
||||
PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
|
||||
PR_RHI_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
||||
PR_RHI_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
|
||||
PR_RHI_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
} PrRhiDescriptorType;
|
||||
|
||||
typedef enum PrRhiDescriptorBindingFlag {
|
||||
PR_RHI_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND = 1 << 0,
|
||||
PR_RHI_DESCRIPTOR_BINDING_PARTIALLY_BOUND = 1 << 1,
|
||||
PR_RHI_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT = 1 << 2,
|
||||
} PrRhiDescriptorBindingFlag;
|
||||
|
||||
typedef enum PrRhiPipelineBindPoint {
|
||||
PR_RHI_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
PR_RHI_PIPELINE_BIND_POINT_COMPUTE,
|
||||
} PrRhiPipelineBindPoint;
|
||||
|
||||
typedef enum PrRhiCompareOp {
|
||||
PR_RHI_COMPARE_OP_NEVER,
|
||||
PR_RHI_COMPARE_OP_LESS,
|
||||
PR_RHI_COMPARE_OP_EQUAL,
|
||||
PR_RHI_COMPARE_OP_LESS_OR_EQUAL,
|
||||
PR_RHI_COMPARE_OP_GREATER,
|
||||
PR_RHI_COMPARE_OP_NOT_EQUAL,
|
||||
PR_RHI_COMPARE_OP_GREATER_OR_EQUAL,
|
||||
PR_RHI_COMPARE_OP_ALWAYS,
|
||||
} PrRhiCompareOp;
|
||||
|
||||
typedef enum PrRhiPrimitiveTopology {
|
||||
PR_RHI_TOPOLOGY_POINT_LIST,
|
||||
PR_RHI_TOPOLOGY_LINE_LIST,
|
||||
PR_RHI_TOPOLOGY_LINE_STRIP,
|
||||
PR_RHI_TOPOLOGY_TRIANGLE_LIST,
|
||||
PR_RHI_TOPOLOGY_TRIANGLE_STRIP,
|
||||
PR_RHI_TOPOLOGY_TRIANGLE_FAN,
|
||||
PR_RHI_TOPOLOGY_LINE_LIST_WITH_ADJACENCY,
|
||||
PR_RHI_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY,
|
||||
PR_RHI_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY,
|
||||
PR_RHI_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY,
|
||||
PR_RHI_TOPOLOGY_PATCH_LIST,
|
||||
} PrRhiPrimitiveTopology;
|
||||
|
||||
typedef enum PrRhiIndexType {
|
||||
PR_RHI_INDEX_TYPE_UINT16,
|
||||
PR_RHI_INDEX_TYPE_UINT32,
|
||||
} PrRhiIndexType;
|
||||
|
||||
typedef enum PrRhiPresentMode {
|
||||
PR_RHI_PRESENT_MODE_IMMEDIATE,
|
||||
PR_RHI_PRESENT_MODE_FIFO,
|
||||
PR_RHI_PRESENT_MODE_MAILBOX,
|
||||
} PrRhiPresentMode;
|
||||
|
||||
typedef enum PrRhiFilter { PR_RHI_FILTER_NEAREST, PR_RHI_FILTER_LINEAR } PrRhiFilter;
|
||||
typedef enum PrRhiMipmapMode { PR_RHI_MIPMAP_MODE_NEAREST, PR_RHI_MIPMAP_MODE_LINEAR } PrRhiMipmapMode;
|
||||
typedef enum PrRhiAddressMode {
|
||||
PR_RHI_ADDRESS_MODE_REPEAT,
|
||||
PR_RHI_ADDRESS_MODE_CLAMP_TO_EDGE,
|
||||
PR_RHI_ADDRESS_MODE_CLAMP_TO_BORDER,
|
||||
} PrRhiAddressMode;
|
||||
|
||||
// ============================================================================
|
||||
// Element types (referenced by array aliases)
|
||||
// ============================================================================
|
||||
|
||||
typedef struct PrRhiPushConstantRange {
|
||||
PrRhiShaderStage stage_flags;
|
||||
u32 offset;
|
||||
u32 size;
|
||||
} PrRhiPushConstantRange;
|
||||
|
||||
typedef struct PrRhiVertexInputBinding {
|
||||
u32 binding;
|
||||
u32 stride;
|
||||
} PrRhiVertexInputBinding;
|
||||
|
||||
typedef struct PrRhiVertexAttribute {
|
||||
u32 location;
|
||||
u32 binding;
|
||||
PrRhiFormat format;
|
||||
u32 offset;
|
||||
} PrRhiVertexAttribute;
|
||||
|
||||
typedef struct PrRhiColorBlendAttachment {
|
||||
u8 color_write_mask;
|
||||
} PrRhiColorBlendAttachment;
|
||||
|
||||
typedef struct PrRhiDescriptorSetLayoutBinding {
|
||||
PrRhiDescriptorType type;
|
||||
u32 descriptor_count;
|
||||
PrRhiShaderStage stage_flags;
|
||||
PrRhiDescriptorBindingFlag binding_flags;
|
||||
} PrRhiDescriptorSetLayoutBinding;
|
||||
|
||||
typedef struct PrRhiDescriptorPoolSize {
|
||||
PrRhiDescriptorType type;
|
||||
u32 descriptor_count;
|
||||
} PrRhiDescriptorPoolSize;
|
||||
|
||||
typedef struct PrRhiDescriptorImageInfo {
|
||||
PrRhiTexture *texture;
|
||||
PrRhiSampler *sampler;
|
||||
PrRhiImageLayout layout;
|
||||
} PrRhiDescriptorImageInfo;
|
||||
|
||||
typedef struct PrRhiDescriptorBufferInfo {
|
||||
PrRhiBuffer *buffer;
|
||||
u64 offset;
|
||||
u64 range;
|
||||
} PrRhiDescriptorBufferInfo;
|
||||
|
||||
typedef struct PrRhiImageMemoryBarrier {
|
||||
PrRhiTexture *texture;
|
||||
PrRhiImageLayout old_layout;
|
||||
PrRhiImageLayout new_layout;
|
||||
PrRhiPipelineStage src_stage_mask;
|
||||
PrRhiAccess src_access_mask;
|
||||
PrRhiPipelineStage dst_stage_mask;
|
||||
PrRhiAccess dst_access_mask;
|
||||
} PrRhiImageMemoryBarrier;
|
||||
|
||||
typedef struct PrRhiBufferMemoryBarrier {
|
||||
PrRhiBuffer *buffer;
|
||||
u64 offset;
|
||||
u64 size;
|
||||
PrRhiPipelineStage src_stage_mask;
|
||||
PrRhiAccess src_access_mask;
|
||||
PrRhiPipelineStage dst_stage_mask;
|
||||
PrRhiAccess dst_access_mask;
|
||||
} PrRhiBufferMemoryBarrier;
|
||||
|
||||
typedef struct PrRhiBufferImageCopy {
|
||||
u64 buffer_offset;
|
||||
u32 mip_level;
|
||||
u32 width;
|
||||
u32 height;
|
||||
} PrRhiBufferImageCopy;
|
||||
|
||||
typedef struct PrRhiColorAttachment {
|
||||
PrRhiTexture *texture;
|
||||
PrRhiImageLayout layout;
|
||||
b8 clear;
|
||||
f32 clear_color[4];
|
||||
} PrRhiColorAttachment;
|
||||
|
||||
// ============================================================================
|
||||
// Typed array aliases
|
||||
// ============================================================================
|
||||
|
||||
// Opaque handle arrays (pointer-to-pointer)
|
||||
typedef PrRhiPhysicalDevice **PrRhiPhysicalDeviceArray;
|
||||
typedef PrRhiBuffer **PrRhiBufferArray;
|
||||
typedef PrRhiTexture **PrRhiTextureArray;
|
||||
typedef PrRhiFence **PrRhiFenceArray;
|
||||
typedef PrRhiSemaphore **PrRhiSemaphoreArray;
|
||||
typedef PrRhiCommandBuffer **PrRhiCommandBufferArray;
|
||||
typedef PrRhiDescriptorSetLayout **PrRhiDescriptorSetLayoutArray;
|
||||
typedef PrRhiDescriptorSet **PrRhiDescriptorSetArray;
|
||||
// Value type arrays (contiguous structs/enums)
|
||||
typedef PrRhiPushConstantRange *PrRhiPushConstantRangeArray;
|
||||
typedef PrRhiVertexInputBinding *PrRhiVertexInputBindingArray;
|
||||
typedef PrRhiVertexAttribute *PrRhiVertexAttributeArray;
|
||||
typedef PrRhiFormat *PrRhiFormatArray;
|
||||
typedef PrRhiColorBlendAttachment *PrRhiColorBlendAttachmentArray;
|
||||
typedef PrRhiDescriptorSetLayoutBinding *PrRhiDescriptorSetLayoutBindingArray;
|
||||
typedef PrRhiDescriptorPoolSize *PrRhiDescriptorPoolSizeArray;
|
||||
typedef PrRhiDescriptorImageInfo *PrRhiDescriptorImageInfoArray;
|
||||
typedef PrRhiDescriptorBufferInfo *PrRhiDescriptorBufferInfoArray;
|
||||
typedef PrRhiImageMemoryBarrier *PrRhiImageMemoryBarrierArray;
|
||||
typedef PrRhiBufferMemoryBarrier *PrRhiBufferMemoryBarrierArray;
|
||||
typedef PrRhiColorAttachment *PrRhiColorAttachmentArray;
|
||||
typedef PrRhiBufferImageCopy *PrRhiBufferImageCopyArray;
|
||||
typedef struct PrRhiWriteDescriptorSet *PrRhiWriteDescriptorSetArray;
|
||||
|
||||
// ============================================================================
|
||||
// Description structs
|
||||
// ============================================================================
|
||||
|
||||
typedef struct PrRhiInstanceDesc {
|
||||
const char *app_name;
|
||||
u32 app_version;
|
||||
} PrRhiInstanceDesc;
|
||||
|
||||
typedef struct PrRhiDeviceDesc {
|
||||
PrRhiPresentMode present_mode;
|
||||
} PrRhiDeviceDesc;
|
||||
|
||||
typedef struct PrRhiBufferDesc {
|
||||
u64 size;
|
||||
PrRhiBufferUsage usage;
|
||||
PrRhiMemoryUsage memory;
|
||||
} PrRhiBufferDesc;
|
||||
|
||||
typedef struct PrRhiTextureDesc {
|
||||
PrRhiFormat format;
|
||||
u32 width;
|
||||
u32 height;
|
||||
u32 mip_levels;
|
||||
PrRhiTextureUsage usage;
|
||||
} PrRhiTextureDesc;
|
||||
|
||||
typedef struct PrRhiSamplerDesc {
|
||||
PrRhiFilter mag_filter;
|
||||
PrRhiFilter min_filter;
|
||||
PrRhiMipmapMode mipmap_mode;
|
||||
PrRhiAddressMode address_mode_u;
|
||||
PrRhiAddressMode address_mode_v;
|
||||
PrRhiAddressMode address_mode_w;
|
||||
f32 max_anisotropy;
|
||||
f32 min_lod;
|
||||
f32 max_lod;
|
||||
} PrRhiSamplerDesc;
|
||||
|
||||
typedef struct PrRhiShaderDesc {
|
||||
const void *spirv_code;
|
||||
u64 spirv_size;
|
||||
} PrRhiShaderDesc;
|
||||
|
||||
typedef struct PrRhiTextureSize {
|
||||
u32 width;
|
||||
u32 height;
|
||||
} PrRhiTextureSize;
|
||||
|
||||
typedef struct PrRhiPipelineLayoutDesc {
|
||||
PrRhiDescriptorSetLayoutArray set_layouts;
|
||||
PrRhiPushConstantRangeArray push_constant_ranges;
|
||||
} 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 {
|
||||
PrRhiShader *vertex_shader;
|
||||
const char *vertex_shader_entry_point;
|
||||
|
||||
PrRhiShader *fragment_shader;
|
||||
const char *fragment_shader_entry_point;
|
||||
|
||||
PrRhiVertexInputBindingArray vertex_bindings;
|
||||
PrRhiVertexAttributeArray vertex_attributes;
|
||||
|
||||
PrRhiPrimitiveTopology topology;
|
||||
|
||||
PrRhiFormatArray color_attachment_formats;
|
||||
PrRhiFormat depth_attachment_format;
|
||||
|
||||
b8 depth_test_enable;
|
||||
b8 depth_write_enable;
|
||||
PrRhiCompareOp depth_compare_op;
|
||||
|
||||
PrRhiColorBlendAttachmentArray blend_attachments;
|
||||
|
||||
b8 dynamic_viewport;
|
||||
b8 dynamic_scissor;
|
||||
|
||||
PrRhiPolygonMode polygon_mode;
|
||||
|
||||
PrRhiCullMode cull_mode;
|
||||
|
||||
PrRhiFrontFace front_face;
|
||||
|
||||
f32 line_width;
|
||||
|
||||
PrRhiMultisampleCount multisample_count;
|
||||
|
||||
PrRhiPipelineLayout *layout;
|
||||
} PrRhiGraphicsPipelineDesc;
|
||||
|
||||
typedef struct PrRhiComputePipelineDesc {
|
||||
PrRhiShader *shader;
|
||||
const char *shader_entry_point;
|
||||
PrRhiPipelineLayout *layout;
|
||||
} PrRhiComputePipelineDesc;
|
||||
|
||||
typedef struct PrRhiDescriptorSetLayoutDesc {
|
||||
PrRhiDescriptorSetLayoutBindingArray bindings;
|
||||
} PrRhiDescriptorSetLayoutDesc;
|
||||
|
||||
typedef struct PrRhiDescriptorPoolDesc {
|
||||
u32 max_sets;
|
||||
PrRhiDescriptorPoolSizeArray pool_sizes;
|
||||
} PrRhiDescriptorPoolDesc;
|
||||
|
||||
typedef struct PrRhiWriteDescriptorSet {
|
||||
PrRhiDescriptorSet *dst_set;
|
||||
u32 dst_binding;
|
||||
u32 dst_array_element;
|
||||
PrRhiDescriptorType type;
|
||||
PrRhiDescriptorImageInfoArray image_info;
|
||||
PrRhiDescriptorBufferInfoArray buffer_info;
|
||||
} PrRhiWriteDescriptorSet;
|
||||
|
||||
typedef struct PrRhiFenceDesc {
|
||||
b8 signaled;
|
||||
} PrRhiFenceDesc;
|
||||
|
||||
typedef struct PrRhiSwapchainDesc {
|
||||
PrRhiSurface *surface;
|
||||
u32 width;
|
||||
u32 height;
|
||||
b8 has_depth;
|
||||
PrRhiFormat depth_format; // PR_RHI_FORMAT_UNDEFINED = auto-pick
|
||||
} PrRhiSwapchainDesc;
|
||||
|
||||
typedef struct PrRhiSurfaceCapabilities {
|
||||
u32 min_image_count;
|
||||
u32 max_image_count;
|
||||
u32 current_width;
|
||||
u32 current_height;
|
||||
u32 min_width;
|
||||
u32 min_height;
|
||||
u32 max_width;
|
||||
u32 max_height;
|
||||
} PrRhiSurfaceCapabilities;
|
||||
|
||||
typedef struct PrRhiPhysicalDeviceProperties {
|
||||
u32 api_version;
|
||||
PrRhiPhysicalDeviceType device_type;
|
||||
} PrRhiPhysicalDeviceProperties;
|
||||
|
||||
typedef u64 PrRhiDeviceAddress;
|
||||
|
||||
// ============================================================================
|
||||
// RHI context (global, owns all RHI object lifetimes)
|
||||
// ============================================================================
|
||||
|
||||
typedef struct PrRhiContext {
|
||||
WpAllocator allocator;
|
||||
} PrRhiContext;
|
||||
|
||||
// --- Command buffer types ---
|
||||
|
||||
typedef struct PrRhiDepthAttachment {
|
||||
PrRhiTexture *texture;
|
||||
PrRhiImageLayout layout;
|
||||
b8 clear;
|
||||
f32 clear_depth;
|
||||
} PrRhiDepthAttachment;
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,266 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
//
|
||||
// Vulkan backend declarations — concrete implementations of the RHI.
|
||||
// Backend .c files include this header directly (never the umbrella pr_rhi.h)
|
||||
// to avoid the #define aliases.
|
||||
|
||||
#ifndef PR_RHI_VK_H
|
||||
#define PR_RHI_VK_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.
|
||||
// ============================================================================
|
||||
|
||||
#define PR_RHI_VK_PHYSICAL_DEVICE_NAME_LENGTH 256
|
||||
#define PR_RHI_VK_PHYSICAL_DRIVER_INFO_LENGTH 256
|
||||
|
||||
struct PrRhiInstance {
|
||||
VkInstance handle; // VkInstance
|
||||
VkDebugUtilsMessengerEXT *debug_messenger; // VkDebugUtilsMessengerEXT
|
||||
};
|
||||
|
||||
struct PrRhiPhysicalDevice {
|
||||
VkPhysicalDevice handle;
|
||||
PrRhiInstance *instance;
|
||||
c8 device_name[PR_RHI_VK_PHYSICAL_DEVICE_NAME_LENGTH];
|
||||
c8 driver_info[PR_RHI_VK_PHYSICAL_DRIVER_INFO_LENGTH];
|
||||
};
|
||||
|
||||
struct PrRhiDevice {
|
||||
VkDevice handle;
|
||||
VkQueue queue;
|
||||
u32 queue_family_index;
|
||||
VkPresentModeKHR present_mode;
|
||||
VkPhysicalDevice physical_device;
|
||||
VmaAllocator allocator;
|
||||
};
|
||||
|
||||
struct PrRhiSurface {
|
||||
VkSurfaceKHR handle;
|
||||
};
|
||||
|
||||
struct PrRhiSwapchain {
|
||||
PrRhiDevice *device;
|
||||
VkSwapchainKHR handle;
|
||||
PrRhiSurface *surface;
|
||||
u32 image_count;
|
||||
PrRhiTexture **images;
|
||||
PrRhiTexture *depth;
|
||||
VkFormat format;
|
||||
VkFormat depth_format;
|
||||
u32 width;
|
||||
u32 height;
|
||||
u32 current_image_index;
|
||||
};
|
||||
|
||||
struct PrRhiBuffer {
|
||||
VkBuffer handle;
|
||||
VmaAllocation allocation;
|
||||
u64 device_address;
|
||||
u64 size;
|
||||
void *mapped_data;
|
||||
};
|
||||
|
||||
struct PrRhiTexture {
|
||||
VkImage image;
|
||||
VkImageView view;
|
||||
VmaAllocation allocation;
|
||||
u32 width;
|
||||
u32 height;
|
||||
VkFormat format;
|
||||
};
|
||||
|
||||
struct PrRhiSampler {
|
||||
VkSampler handle;
|
||||
};
|
||||
|
||||
struct PrRhiShader {
|
||||
VkShaderModule handle;
|
||||
};
|
||||
|
||||
struct PrRhiPipelineLayout {
|
||||
VkPipelineLayout handle;
|
||||
};
|
||||
|
||||
struct PrRhiPipeline {
|
||||
VkPipeline handle;
|
||||
};
|
||||
|
||||
struct PrRhiDescriptorSetLayout {
|
||||
VkDescriptorSetLayout handle;
|
||||
};
|
||||
|
||||
struct PrRhiDescriptorPool {
|
||||
VkDescriptorPool handle;
|
||||
};
|
||||
|
||||
struct PrRhiDescriptorSet {
|
||||
VkDescriptorSet handle;
|
||||
};
|
||||
|
||||
struct PrRhiCommandPool {
|
||||
VkCommandPool handle;
|
||||
};
|
||||
|
||||
struct PrRhiCommandBuffer {
|
||||
VkCommandBuffer handle;
|
||||
};
|
||||
|
||||
struct PrRhiFence {
|
||||
VkFence handle;
|
||||
};
|
||||
|
||||
struct PrRhiSemaphore {
|
||||
VkSemaphore handle;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Function declarations
|
||||
// ============================================================================
|
||||
|
||||
PrRhiInstance *prRhiCreateInstanceVk(PrRhiInstanceDesc desc);
|
||||
void prRhiDestroyInstanceVk(PrRhiInstance *inst);
|
||||
|
||||
PrRhiPhysicalDeviceArray prRhiGetPhysicalDevicesVk(PrRhiInstance *inst);
|
||||
void prRhiGetPhysicalDeviceNameVk(PrRhiPhysicalDevice *pdev, WpStr8 *out);
|
||||
void prRhiGetPhysicalDeviceDriverInfoVk(PrRhiPhysicalDevice *pdev, WpStr8 *out);
|
||||
PrRhiPhysicalDeviceProperties prRhiGetPhysicalDevicePropertiesVk(PrRhiPhysicalDevice *pdev);
|
||||
|
||||
PrRhiSurface *prRhiCreateSurfaceFromWindowVk(PrRhiInstance *inst, SDL_Window *window);
|
||||
void prRhiDestroySurfaceVk(PrRhiInstance *inst, PrRhiSurface *surface);
|
||||
|
||||
PrRhiSurfaceCapabilities prRhiGetSurfaceCapabilitiesVk(PrRhiPhysicalDevice *pdev,
|
||||
PrRhiSurface *surface);
|
||||
|
||||
PrRhiDevice *prRhiCreateDeviceVk(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface,
|
||||
PrRhiDeviceDesc desc);
|
||||
void prRhiDestroyDeviceVk(PrRhiDevice *device);
|
||||
void prRhiDeviceWaitIdleVk(PrRhiDevice *device);
|
||||
u32 prRhiGetQueueFamilyIndexVk(PrRhiDevice *device);
|
||||
|
||||
PrRhiSwapchain *prRhiCreateSwapchainVk(PrRhiDevice *device, PrRhiSwapchainDesc desc);
|
||||
void prRhiDestroySwapchainVk(PrRhiDevice *device, PrRhiSwapchain *swapchain);
|
||||
u32 prRhiGetSwapchainImageCountVk(PrRhiSwapchain *swapchain);
|
||||
PrRhiSwapchainResult prRhiAcquireNextImageVk(PrRhiDevice *device, PrRhiSwapchain *swapchain,
|
||||
PrRhiSemaphore *signal_semaphore, u32 *out_image_index);
|
||||
PrRhiSwapchainResult prRhiPresentVk(PrRhiDevice *device, PrRhiSwapchain *swapchain,
|
||||
PrRhiSemaphore *wait_semaphore);
|
||||
void prRhiRecreateSwapchainVk(PrRhiDevice *device, PrRhiSwapchain **swapchain,
|
||||
u32 width, u32 height);
|
||||
PrRhiTexture *prRhiGetSwapchainTextureVk(PrRhiSwapchain *swapchain, u32 image_index);
|
||||
PrRhiTexture *prRhiGetSwapchainDepthTextureVk(PrRhiSwapchain *swapchain);
|
||||
PrRhiFormat prRhiGetSwapchainFormatVk(PrRhiSwapchain *swapchain);
|
||||
|
||||
PrRhiBuffer *prRhiCreateBufferVk(PrRhiDevice *device, PrRhiBufferDesc desc);
|
||||
void prRhiDestroyBufferVk(PrRhiDevice *device, PrRhiBuffer *buffer);
|
||||
void *prRhiBufferMapVk(PrRhiDevice *device, PrRhiBuffer *buffer);
|
||||
void prRhiBufferUnmapVk(PrRhiDevice *device, PrRhiBuffer *buffer);
|
||||
PrRhiDeviceAddress prRhiGetBufferDeviceAddressVk(PrRhiDevice *device, PrRhiBuffer *buffer);
|
||||
|
||||
PrRhiTexture *prRhiCreateTextureVk(PrRhiDevice *device, PrRhiTextureDesc desc);
|
||||
PrRhiTexture *prRhiCreateTextureFromKtxVk(PrRhiDevice *device, const char *path,
|
||||
PrRhiCommandPool *pool, PrRhiCommandBuffer *cb);
|
||||
void prRhiDestroyTextureVk(PrRhiDevice *device, PrRhiTexture *texture);
|
||||
PrRhiTextureSize prRhiGetTextureSizeVk(PrRhiTexture *texture);
|
||||
|
||||
PrRhiSampler *prRhiCreateSamplerVk(PrRhiDevice *device, PrRhiSamplerDesc desc);
|
||||
void prRhiDestroySamplerVk(PrRhiDevice *device, PrRhiSampler *sampler);
|
||||
|
||||
PrRhiShader *prRhiCreateShaderVk(PrRhiDevice *device, PrRhiShaderDesc desc);
|
||||
void prRhiDestroyShaderVk(PrRhiDevice *device, PrRhiShader *shader);
|
||||
|
||||
PrRhiPipelineLayout *prRhiCreatePipelineLayoutVk(PrRhiDevice *device,
|
||||
PrRhiPipelineLayoutDesc desc);
|
||||
void prRhiDestroyPipelineLayoutVk(PrRhiDevice *device,
|
||||
PrRhiPipelineLayout *layout);
|
||||
|
||||
PrRhiPipeline *prRhiCreateGraphicsPipelineVk(PrRhiDevice *device,
|
||||
PrRhiGraphicsPipelineDesc desc);
|
||||
PrRhiPipeline *prRhiCreateComputePipelineVk(PrRhiDevice *device,
|
||||
PrRhiComputePipelineDesc desc);
|
||||
void prRhiDestroyPipelineVk(PrRhiDevice *device, PrRhiPipeline *pipeline);
|
||||
|
||||
PrRhiDescriptorSetLayout *prRhiCreateDescriptorSetLayoutVk(PrRhiDevice *device,
|
||||
PrRhiDescriptorSetLayoutDesc desc);
|
||||
void prRhiDestroyDescriptorSetLayoutVk(PrRhiDevice *device,
|
||||
PrRhiDescriptorSetLayout *layout);
|
||||
|
||||
PrRhiDescriptorPool *prRhiCreateDescriptorPoolVk(PrRhiDevice *device,
|
||||
PrRhiDescriptorPoolDesc desc);
|
||||
void prRhiDestroyDescriptorPoolVk(PrRhiDevice *device,
|
||||
PrRhiDescriptorPool *pool);
|
||||
void prRhiResetDescriptorPoolVk(PrRhiDevice *device,
|
||||
PrRhiDescriptorPool *pool);
|
||||
|
||||
PrRhiDescriptorSet *prRhiAllocateDescriptorSetVk(PrRhiDevice *device,
|
||||
PrRhiDescriptorPool *pool,
|
||||
PrRhiDescriptorSetLayout *layout,
|
||||
WpU32Array variable_descriptor_counts);
|
||||
void prRhiFreeDescriptorSetVk(PrRhiDevice *device, PrRhiDescriptorPool *pool,
|
||||
PrRhiDescriptorSet *set);
|
||||
void prRhiUpdateDescriptorSetVk(PrRhiDevice *device, PrRhiWriteDescriptorSetArray writes);
|
||||
|
||||
PrRhiFence *prRhiCreateFenceVk(PrRhiDevice *device, PrRhiFenceDesc desc);
|
||||
void prRhiDestroyFenceVk(PrRhiDevice *device, PrRhiFence *fence);
|
||||
|
||||
void prRhiWaitForFencesVk(PrRhiDevice *device, PrRhiFenceArray fences, u32 count,
|
||||
b8 wait_all, u64 timeout_ns);
|
||||
void prRhiResetFencesVk(PrRhiDevice *device, PrRhiFenceArray fences, u32 count);
|
||||
|
||||
PrRhiSemaphore *prRhiCreateSemaphoreVk(PrRhiDevice *device);
|
||||
void prRhiDestroySemaphoreVk(PrRhiDevice *device, PrRhiSemaphore *semaphore);
|
||||
|
||||
PrRhiCommandPool *prRhiCreateCommandPoolVk(PrRhiDevice *device);
|
||||
void prRhiDestroyCommandPoolVk(PrRhiDevice *device, PrRhiCommandPool *pool);
|
||||
PrRhiCommandBufferArray prRhiAllocateCommandBuffersVk(PrRhiDevice *device, PrRhiCommandPool *pool,
|
||||
u32 count);
|
||||
|
||||
void prRhiFreeCommandBuffersVk(PrRhiDevice *device, PrRhiCommandPool *pool, PrRhiCommandBufferArray buffers);
|
||||
|
||||
void prRhiBeginCommandBufferVk(PrRhiCommandBuffer *cb);
|
||||
void prRhiEndCommandBufferVk(PrRhiCommandBuffer *cb);
|
||||
void prRhiResetCommandBufferVk(PrRhiCommandBuffer *cb);
|
||||
|
||||
void prRhiCmdPipelineBarrierVk(PrRhiCommandBuffer *cb,
|
||||
PrRhiImageMemoryBarrierArray image_barriers,
|
||||
PrRhiBufferMemoryBarrierArray buffer_barriers);
|
||||
|
||||
void prRhiCmdBeginRenderingVk(PrRhiCommandBuffer *cb,
|
||||
PrRhiColorAttachmentArray color_attachments,
|
||||
const PrRhiDepthAttachment *depth_attachment);
|
||||
void prRhiCmdEndRenderingVk(PrRhiCommandBuffer *cb);
|
||||
|
||||
void prRhiCmdSetViewportVk(PrRhiCommandBuffer *cb, f32 x, f32 y, f32 width, f32 height);
|
||||
void prRhiCmdSetScissorVk(PrRhiCommandBuffer *cb, i32 x, i32 y, u32 width, u32 height);
|
||||
|
||||
void prRhiCmdBindPipelineVk(PrRhiCommandBuffer *cb, PrRhiPipelineBindPoint bind_point,
|
||||
PrRhiPipeline *pipeline);
|
||||
void prRhiCmdBindDescriptorSetsVk(PrRhiCommandBuffer *cb, PrRhiPipelineBindPoint bind_point,
|
||||
PrRhiPipelineLayout *layout, u32 first_set,
|
||||
PrRhiDescriptorSetArray sets);
|
||||
void prRhiCmdPushConstantsVk(PrRhiCommandBuffer *cb, PrRhiPipelineLayout *layout,
|
||||
PrRhiShaderStage stage_flags, u32 offset, u32 size,
|
||||
const void *data);
|
||||
void prRhiCmdBindVertexBuffersVk(PrRhiCommandBuffer *cb, u32 first_binding, PrRhiBufferArray buffers,
|
||||
WpU64Array offsets);
|
||||
void prRhiCmdBindIndexBufferVk(PrRhiCommandBuffer *cb, PrRhiBuffer *buffer, u64 offset,
|
||||
PrRhiIndexType index_type);
|
||||
|
||||
void prRhiCmdDrawVk(PrRhiCommandBuffer *cb, u32 vertex_count, u32 instance_count,
|
||||
u32 first_vertex, u32 first_instance);
|
||||
void prRhiCmdDrawIndexedVk(PrRhiCommandBuffer *cb, u32 index_count, u32 instance_count,
|
||||
u32 first_index, i32 vertex_offset, u32 first_instance);
|
||||
|
||||
void prRhiCmdCopyBufferToImageVk(PrRhiCommandBuffer *cb, PrRhiBuffer *src, PrRhiTexture *dst,
|
||||
PrRhiBufferImageCopyArray copies);
|
||||
|
||||
void prRhiQueueSubmitVk(PrRhiDevice *device, PrRhiCommandBuffer *cb,
|
||||
PrRhiSemaphore *wait_semaphore,
|
||||
PrRhiSemaphore *signal_semaphore, PrRhiFence *fence);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,84 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
|
||||
#ifndef PR_RHI_VK_ALIASES_H
|
||||
#define PR_RHI_VK_ALIASES_H
|
||||
|
||||
#include "pr_rhi_vk.h"
|
||||
|
||||
#define prRhiCreateInstance prRhiCreateInstanceVk
|
||||
#define prRhiDestroyInstance prRhiDestroyInstanceVk
|
||||
#define prRhiGetPhysicalDevices prRhiGetPhysicalDevicesVk
|
||||
#define prRhiGetPhysicalDeviceName prRhiGetPhysicalDeviceNameVk
|
||||
#define prRhiGetPhysicalDeviceDriverInfo prRhiGetPhysicalDeviceDriverInfoVk
|
||||
#define prRhiGetPhysicalDeviceProperties prRhiGetPhysicalDevicePropertiesVk
|
||||
#define prRhiCreateSurfaceFromWindow prRhiCreateSurfaceFromWindowVk
|
||||
#define prRhiDestroySurface prRhiDestroySurfaceVk
|
||||
#define prRhiGetSurfaceCapabilities prRhiGetSurfaceCapabilitiesVk
|
||||
#define prRhiCreateDevice prRhiCreateDeviceVk
|
||||
#define prRhiDestroyDevice prRhiDestroyDeviceVk
|
||||
#define prRhiDeviceWaitIdle prRhiDeviceWaitIdleVk
|
||||
#define prRhiGetQueueFamilyIndex prRhiGetQueueFamilyIndexVk
|
||||
#define prRhiCreateSwapchain prRhiCreateSwapchainVk
|
||||
#define prRhiDestroySwapchain prRhiDestroySwapchainVk
|
||||
#define prRhiGetSwapchainImageCount prRhiGetSwapchainImageCountVk
|
||||
#define prRhiAcquireNextImage prRhiAcquireNextImageVk
|
||||
#define prRhiPresent prRhiPresentVk
|
||||
#define prRhiRecreateSwapchain prRhiRecreateSwapchainVk
|
||||
#define prRhiGetSwapchainTexture prRhiGetSwapchainTextureVk
|
||||
#define prRhiGetSwapchainDepthTexture prRhiGetSwapchainDepthTextureVk
|
||||
#define prRhiGetSwapchainFormat prRhiGetSwapchainFormatVk
|
||||
#define prRhiCreateBuffer prRhiCreateBufferVk
|
||||
#define prRhiDestroyBuffer prRhiDestroyBufferVk
|
||||
#define prRhiBufferMap prRhiBufferMapVk
|
||||
#define prRhiBufferUnmap prRhiBufferUnmapVk
|
||||
#define prRhiGetBufferDeviceAddress prRhiGetBufferDeviceAddressVk
|
||||
#define prRhiCreateTexture prRhiCreateTextureVk
|
||||
#define prRhiCreateTextureFromKtx prRhiCreateTextureFromKtxVk
|
||||
#define prRhiDestroyTexture prRhiDestroyTextureVk
|
||||
#define prRhiGetTextureSize prRhiGetTextureSizeVk
|
||||
#define prRhiCreateSampler prRhiCreateSamplerVk
|
||||
#define prRhiDestroySampler prRhiDestroySamplerVk
|
||||
#define prRhiCreateShader prRhiCreateShaderVk
|
||||
#define prRhiDestroyShader prRhiDestroyShaderVk
|
||||
#define prRhiCreatePipelineLayout prRhiCreatePipelineLayoutVk
|
||||
#define prRhiDestroyPipelineLayout prRhiDestroyPipelineLayoutVk
|
||||
#define prRhiCreateGraphicsPipeline prRhiCreateGraphicsPipelineVk
|
||||
#define prRhiCreateComputePipeline prRhiCreateComputePipelineVk
|
||||
#define prRhiDestroyPipeline prRhiDestroyPipelineVk
|
||||
#define prRhiCreateDescriptorSetLayout prRhiCreateDescriptorSetLayoutVk
|
||||
#define prRhiDestroyDescriptorSetLayout prRhiDestroyDescriptorSetLayoutVk
|
||||
#define prRhiCreateDescriptorPool prRhiCreateDescriptorPoolVk
|
||||
#define prRhiDestroyDescriptorPool prRhiDestroyDescriptorPoolVk
|
||||
#define prRhiResetDescriptorPool prRhiResetDescriptorPoolVk
|
||||
#define prRhiAllocateDescriptorSet prRhiAllocateDescriptorSetVk
|
||||
#define prRhiFreeDescriptorSet prRhiFreeDescriptorSetVk
|
||||
#define prRhiUpdateDescriptorSet prRhiUpdateDescriptorSetVk
|
||||
#define prRhiCreateFence prRhiCreateFenceVk
|
||||
#define prRhiDestroyFence prRhiDestroyFenceVk
|
||||
#define prRhiWaitForFences prRhiWaitForFencesVk
|
||||
#define prRhiResetFences prRhiResetFencesVk
|
||||
#define prRhiCreateSemaphore prRhiCreateSemaphoreVk
|
||||
#define prRhiDestroySemaphore prRhiDestroySemaphoreVk
|
||||
#define prRhiCreateCommandPool prRhiCreateCommandPoolVk
|
||||
#define prRhiDestroyCommandPool prRhiDestroyCommandPoolVk
|
||||
#define prRhiAllocateCommandBuffers prRhiAllocateCommandBuffersVk
|
||||
#define prRhiFreeCommandBuffers prRhiFreeCommandBuffersVk
|
||||
#define prRhiBeginCommandBuffer prRhiBeginCommandBufferVk
|
||||
#define prRhiEndCommandBuffer prRhiEndCommandBufferVk
|
||||
#define prRhiResetCommandBuffer prRhiResetCommandBufferVk
|
||||
#define prRhiCmdPipelineBarrier prRhiCmdPipelineBarrierVk
|
||||
#define prRhiCmdBeginRendering prRhiCmdBeginRenderingVk
|
||||
#define prRhiCmdEndRendering prRhiCmdEndRenderingVk
|
||||
#define prRhiCmdSetViewport prRhiCmdSetViewportVk
|
||||
#define prRhiCmdSetScissor prRhiCmdSetScissorVk
|
||||
#define prRhiCmdBindPipeline prRhiCmdBindPipelineVk
|
||||
#define prRhiCmdBindDescriptorSets prRhiCmdBindDescriptorSetsVk
|
||||
#define prRhiCmdPushConstants prRhiCmdPushConstantsVk
|
||||
#define prRhiCmdBindVertexBuffers prRhiCmdBindVertexBuffersVk
|
||||
#define prRhiCmdBindIndexBuffer prRhiCmdBindIndexBufferVk
|
||||
#define prRhiCmdDraw prRhiCmdDrawVk
|
||||
#define prRhiCmdDrawIndexed prRhiCmdDrawIndexedVk
|
||||
#define prRhiCmdCopyBufferToImage prRhiCmdCopyBufferToImageVk
|
||||
#define prRhiQueueSubmit prRhiQueueSubmitVk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
#define VMA_IMPLEMENTATION
|
||||
#include <vk_mem_alloc.h>
|
||||
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"$schema": "https://schema.khronos.org/vulkan/profiles-0.8-latest.json#",
|
||||
"capabilities": {
|
||||
"VP_PRISM_desktop_2026_block": {
|
||||
"extensions": {
|
||||
"VK_KHR_global_priority": 1,
|
||||
"VK_KHR_get_surface_capabilities2": 1,
|
||||
"VK_KHR_swapchain": 70,
|
||||
"VK_KHR_maintenance5": 1
|
||||
},
|
||||
"features": {
|
||||
"VkPhysicalDeviceFeatures": {
|
||||
"robustBufferAccess": true,
|
||||
"fullDrawIndexUint32": true,
|
||||
"imageCubeArray": true,
|
||||
"independentBlend": true,
|
||||
"sampleRateShading": true,
|
||||
"drawIndirectFirstInstance": true,
|
||||
"depthClamp": true,
|
||||
"depthBiasClamp": true,
|
||||
"samplerAnisotropy": true,
|
||||
"occlusionQueryPrecise": true,
|
||||
"fragmentStoresAndAtomics": true,
|
||||
"shaderStorageImageExtendedFormats": true,
|
||||
"shaderUniformBufferArrayDynamicIndexing": true,
|
||||
"shaderSampledImageArrayDynamicIndexing": true,
|
||||
"shaderStorageBufferArrayDynamicIndexing": true,
|
||||
"shaderStorageImageArrayDynamicIndexing": true
|
||||
},
|
||||
"VkPhysicalDeviceVulkan11Features": {
|
||||
"multiview": true,
|
||||
"samplerYcbcrConversion": true
|
||||
},
|
||||
"VkPhysicalDeviceVulkan12Features": {
|
||||
"uniformBufferStandardLayout": true,
|
||||
"subgroupBroadcastDynamicId": true,
|
||||
"imagelessFramebuffer": true,
|
||||
"separateDepthStencilLayouts": true,
|
||||
"hostQueryReset": true,
|
||||
"timelineSemaphore": true,
|
||||
"shaderSubgroupExtendedTypes": true,
|
||||
"samplerMirrorClampToEdge": true,
|
||||
"descriptorIndexing": true,
|
||||
"shaderUniformTexelBufferArrayDynamicIndexing": true,
|
||||
"shaderStorageTexelBufferArrayDynamicIndexing": true,
|
||||
"shaderUniformBufferArrayNonUniformIndexing": true,
|
||||
"shaderSampledImageArrayNonUniformIndexing": true,
|
||||
"shaderStorageBufferArrayNonUniformIndexing": true,
|
||||
"shaderStorageImageArrayNonUniformIndexing": true,
|
||||
"shaderUniformTexelBufferArrayNonUniformIndexing": true,
|
||||
"shaderStorageTexelBufferArrayNonUniformIndexing": true,
|
||||
"descriptorBindingSampledImageUpdateAfterBind": true,
|
||||
"descriptorBindingStorageImageUpdateAfterBind": true,
|
||||
"descriptorBindingStorageBufferUpdateAfterBind": true,
|
||||
"descriptorBindingUniformTexelBufferUpdateAfterBind": true,
|
||||
"descriptorBindingStorageTexelBufferUpdateAfterBind": true,
|
||||
"descriptorBindingUpdateUnusedWhilePending": true,
|
||||
"descriptorBindingPartiallyBound": true,
|
||||
"descriptorBindingVariableDescriptorCount": true,
|
||||
"runtimeDescriptorArray": true,
|
||||
"scalarBlockLayout": true,
|
||||
"vulkanMemoryModel": true,
|
||||
"vulkanMemoryModelDeviceScope": true,
|
||||
"bufferDeviceAddress": true
|
||||
},
|
||||
"VkPhysicalDeviceVulkan13Features": {
|
||||
"robustImageAccess": true,
|
||||
"shaderTerminateInvocation": true,
|
||||
"shaderZeroInitializeWorkgroupMemory": true,
|
||||
"synchronization2": true,
|
||||
"shaderIntegerDotProduct": true,
|
||||
"maintenance4": true,
|
||||
"pipelineCreationCacheControl": true,
|
||||
"subgroupSizeControl": true,
|
||||
"computeFullSubgroups": true,
|
||||
"shaderDemoteToHelperInvocation": true,
|
||||
"inlineUniformBlock": true,
|
||||
"dynamicRendering": true,
|
||||
"descriptorBindingInlineUniformBlockUpdateAfterBind": true
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"VkPhysicalDeviceProperties": {
|
||||
"limits": {
|
||||
"maxImageDimension1D": 8192,
|
||||
"maxImageDimension2D": 8192,
|
||||
"maxImageDimensionCube": 8192,
|
||||
"maxImageArrayLayers": 2048,
|
||||
"maxUniformBufferRange": 65536,
|
||||
"bufferImageGranularity": 4096,
|
||||
"maxPerStageDescriptorSamplers": 64,
|
||||
"maxPerStageDescriptorUniformBuffers": 15,
|
||||
"maxPerStageDescriptorStorageBuffers": 30,
|
||||
"maxPerStageDescriptorSampledImages": 200,
|
||||
"maxPerStageDescriptorStorageImages": 16,
|
||||
"maxPerStageResources": 200,
|
||||
"maxDescriptorSetSamplers": 576,
|
||||
"maxDescriptorSetUniformBuffers": 90,
|
||||
"maxDescriptorSetStorageBuffers": 96,
|
||||
"maxDescriptorSetSampledImages": 1800,
|
||||
"maxDescriptorSetStorageImages": 144,
|
||||
"maxFragmentCombinedOutputResources": 16,
|
||||
"maxComputeWorkGroupInvocations": 256,
|
||||
"maxComputeWorkGroupSize": [
|
||||
256,
|
||||
256,
|
||||
64
|
||||
],
|
||||
"subTexelPrecisionBits": 8,
|
||||
"mipmapPrecisionBits": 6,
|
||||
"maxSamplerLodBias": 14,
|
||||
"standardSampleLocations": true,
|
||||
"maxColorAttachments": 7
|
||||
}
|
||||
},
|
||||
"VkPhysicalDeviceVulkan11Properties": {
|
||||
"maxMultiviewViewCount": 6,
|
||||
"maxMultiviewInstanceIndex": 134217727,
|
||||
"subgroupSize": 4,
|
||||
"subgroupSupportedStages": [
|
||||
"VK_SHADER_STAGE_COMPUTE_BIT",
|
||||
"VK_SHADER_STAGE_FRAGMENT_BIT"
|
||||
],
|
||||
"subgroupSupportedOperations": [
|
||||
"VK_SUBGROUP_FEATURE_BASIC_BIT",
|
||||
"VK_SUBGROUP_FEATURE_VOTE_BIT",
|
||||
"VK_SUBGROUP_FEATURE_ARITHMETIC_BIT",
|
||||
"VK_SUBGROUP_FEATURE_BALLOT_BIT",
|
||||
"VK_SUBGROUP_FEATURE_SHUFFLE_BIT",
|
||||
"VK_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT",
|
||||
"VK_SUBGROUP_FEATURE_QUAD_BIT"
|
||||
]
|
||||
},
|
||||
"VkPhysicalDeviceVulkan12Properties": {
|
||||
"maxTimelineSemaphoreValueDifference": 2147483647,
|
||||
"shaderSignedZeroInfNanPreserveFloat16": true,
|
||||
"shaderSignedZeroInfNanPreserveFloat32": true,
|
||||
"maxPerStageDescriptorUpdateAfterBindSamplers": 500000,
|
||||
"maxPerStageDescriptorUpdateAfterBindUniformBuffers": 12,
|
||||
"maxPerStageDescriptorUpdateAfterBindStorageBuffers": 500000,
|
||||
"maxPerStageDescriptorUpdateAfterBindSampledImages": 500000,
|
||||
"maxPerStageDescriptorUpdateAfterBindStorageImages": 500000,
|
||||
"maxPerStageDescriptorUpdateAfterBindInputAttachments": 7,
|
||||
"maxPerStageUpdateAfterBindResources": 500000,
|
||||
"maxDescriptorSetUpdateAfterBindSamplers": 500000,
|
||||
"maxDescriptorSetUpdateAfterBindUniformBuffers": 72,
|
||||
"maxDescriptorSetUpdateAfterBindUniformBuffersDynamic": 8,
|
||||
"maxDescriptorSetUpdateAfterBindStorageBuffers": 500000,
|
||||
"maxDescriptorSetUpdateAfterBindStorageBuffersDynamic": 4,
|
||||
"maxDescriptorSetUpdateAfterBindSampledImages": 500000,
|
||||
"maxDescriptorSetUpdateAfterBindStorageImages": 500000,
|
||||
"maxDescriptorSetUpdateAfterBindInputAttachments": 7
|
||||
},
|
||||
"VkPhysicalDeviceVulkan13Properties": {
|
||||
"maxBufferSize": 1073741824,
|
||||
"maxInlineUniformBlockSize": 256,
|
||||
"maxPerStageDescriptorInlineUniformBlocks": 4,
|
||||
"maxPerStageDescriptorUpdateAfterBindInlineUniformBlocks": 4,
|
||||
"maxDescriptorSetInlineUniformBlocks": 4,
|
||||
"maxDescriptorSetUpdateAfterBindInlineUniformBlocks": 4,
|
||||
"maxInlineUniformTotalSize": 256
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"VP_PRISM_desktop_2026": {
|
||||
"version": 1,
|
||||
"api-version": "1.3.204",
|
||||
"label": "Prism Desktop 2026",
|
||||
"description": "Generated profile doing an union between profiles: VP_KHR_roadmap_2022",
|
||||
"capabilities": [
|
||||
"VP_PRISM_desktop_2026_block"
|
||||
]
|
||||
}
|
||||
},
|
||||
"contributors": {},
|
||||
"history": [
|
||||
{
|
||||
"revision": 1,
|
||||
"date": "2026-05-25",
|
||||
"author": "LunarG Profiles Merge Script",
|
||||
"comment": "Generated profiles file"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,434 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) 2021-2026 Valve Corporation
|
||||
* Copyright (C) 2021-2026 LunarG, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License")
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is ***GENERATED***. Do Not Edit.
|
||||
* See scripts/gen_profiles_solution.py for modifications.
|
||||
*/
|
||||
|
||||
#ifndef VULKAN_PROFILES_H_
|
||||
#define VULKAN_PROFILES_H_ 1
|
||||
|
||||
#define VPAPI_ATTR
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <volk/volk.h>
|
||||
|
||||
#if defined(VK_VERSION_1_3) && \
|
||||
defined(VK_KHR_get_surface_capabilities2) && \
|
||||
defined(VK_KHR_global_priority) && \
|
||||
defined(VK_KHR_maintenance5) && \
|
||||
defined(VK_KHR_swapchain)
|
||||
#define VP_PRISM_desktop_2026 1
|
||||
#define VP_PRISM_DESKTOP_2026_NAME "VP_PRISM_desktop_2026"
|
||||
#define VP_PRISM_DESKTOP_2026_SPEC_VERSION 1
|
||||
#define VP_PRISM_DESKTOP_2026_MIN_API_VERSION VK_MAKE_VERSION(1, 3, 204)
|
||||
#endif
|
||||
|
||||
#define VP_HEADER_VERSION_COMPLETE VK_MAKE_API_VERSION(0, 2, 0, VK_HEADER_VERSION)
|
||||
|
||||
#define VP_MAX_PROFILE_NAME_SIZE 256U
|
||||
|
||||
typedef struct VpProfileProperties {
|
||||
char profileName[VP_MAX_PROFILE_NAME_SIZE];
|
||||
uint32_t specVersion;
|
||||
} VpProfileProperties;
|
||||
|
||||
typedef struct VpBlockProperties {
|
||||
VpProfileProperties profiles;
|
||||
uint32_t apiVersion;
|
||||
char blockName[VP_MAX_PROFILE_NAME_SIZE];
|
||||
} VpBlockProperties;
|
||||
|
||||
typedef struct VpVideoProfileProperties {
|
||||
char name[VP_MAX_PROFILE_NAME_SIZE];
|
||||
} VpVideoProfileProperties;
|
||||
|
||||
typedef enum VpInstanceCreateFlagBits {
|
||||
VP_INSTANCE_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
|
||||
} VpInstanceCreateFlagBits;
|
||||
typedef VkFlags VpInstanceCreateFlags;
|
||||
|
||||
typedef struct VpInstanceCreateInfo {
|
||||
const VkInstanceCreateInfo* pCreateInfo;
|
||||
VpInstanceCreateFlags flags;
|
||||
uint32_t enabledFullProfileCount;
|
||||
const VpProfileProperties* pEnabledFullProfiles;
|
||||
uint32_t enabledProfileBlockCount;
|
||||
const VpBlockProperties* pEnabledProfileBlocks;
|
||||
} VpInstanceCreateInfo;
|
||||
|
||||
typedef enum VpDeviceCreateFlagBits {
|
||||
VP_DEVICE_CREATE_DISABLE_ROBUST_BUFFER_ACCESS_BIT = 0x0000001,
|
||||
VP_DEVICE_CREATE_DISABLE_ROBUST_IMAGE_ACCESS_BIT = 0x0000002,
|
||||
VP_DEVICE_CREATE_DISABLE_ROBUST_ACCESS =
|
||||
VP_DEVICE_CREATE_DISABLE_ROBUST_BUFFER_ACCESS_BIT | VP_DEVICE_CREATE_DISABLE_ROBUST_IMAGE_ACCESS_BIT,
|
||||
|
||||
VP_DEVICE_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
|
||||
} VpDeviceCreateFlagBits;
|
||||
typedef VkFlags VpDeviceCreateFlags;
|
||||
|
||||
typedef struct VpDeviceCreateInfo {
|
||||
const VkDeviceCreateInfo* pCreateInfo;
|
||||
VpDeviceCreateFlags flags;
|
||||
uint32_t enabledFullProfileCount;
|
||||
const VpProfileProperties* pEnabledFullProfiles;
|
||||
uint32_t enabledProfileBlockCount;
|
||||
const VpBlockProperties* pEnabledProfileBlocks;
|
||||
} VpDeviceCreateInfo;
|
||||
|
||||
VK_DEFINE_HANDLE(VpCapabilities)
|
||||
|
||||
typedef enum VpCapabilitiesCreateFlagBits {
|
||||
VP_PROFILE_CREATE_STATIC_BIT = (1 << 0),
|
||||
//VP_PROFILE_CREATE_DYNAMIC_BIT = (1 << 1),
|
||||
VP_PROFILE_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
|
||||
} VpCapabilitiesCreateFlagBits;
|
||||
|
||||
typedef VkFlags VpCapabilitiesCreateFlags;
|
||||
|
||||
// Pointers to some Vulkan functions - a subset used by the library.
|
||||
// Used in VpCapabilitiesCreateInfo::pVulkanFunctions.
|
||||
|
||||
typedef struct VpVulkanFunctions {
|
||||
/// Required when using VP_DYNAMIC_VULKAN_FUNCTIONS.
|
||||
PFN_vkGetInstanceProcAddr GetInstanceProcAddr;
|
||||
/// Required when using VP_DYNAMIC_VULKAN_FUNCTIONS.
|
||||
PFN_vkGetDeviceProcAddr GetDeviceProcAddr;
|
||||
PFN_vkEnumerateInstanceVersion EnumerateInstanceVersion;
|
||||
PFN_vkEnumerateInstanceExtensionProperties EnumerateInstanceExtensionProperties;
|
||||
PFN_vkEnumerateDeviceExtensionProperties EnumerateDeviceExtensionProperties;
|
||||
PFN_vkGetPhysicalDeviceFeatures2 GetPhysicalDeviceFeatures2;
|
||||
PFN_vkGetPhysicalDeviceProperties2 GetPhysicalDeviceProperties2;
|
||||
PFN_vkGetPhysicalDeviceFormatProperties2 GetPhysicalDeviceFormatProperties2;
|
||||
PFN_vkGetPhysicalDeviceQueueFamilyProperties2 GetPhysicalDeviceQueueFamilyProperties2;
|
||||
PFN_vkCreateInstance CreateInstance;
|
||||
PFN_vkCreateDevice CreateDevice;
|
||||
} VpVulkanFunctions;
|
||||
|
||||
/// Description of a Allocator to be created.
|
||||
typedef struct VpCapabilitiesCreateInfo
|
||||
{
|
||||
/// Flags for created allocator. Use #VpInstanceCreateFlagBits enum.
|
||||
VpCapabilitiesCreateFlags flags;
|
||||
uint32_t apiVersion;
|
||||
const VpVulkanFunctions* pVulkanFunctions;
|
||||
} VpCapabilitiesCreateInfo;
|
||||
|
||||
VPAPI_ATTR VkResult vpCreateCapabilities(
|
||||
const VpCapabilitiesCreateInfo* pCreateInfo,
|
||||
const VkAllocationCallbacks* pAllocator,
|
||||
VpCapabilities* pCapabilities);
|
||||
|
||||
/// Destroys allocator object.
|
||||
VPAPI_ATTR void vpDestroyCapabilities(
|
||||
VpCapabilities capabilities,
|
||||
const VkAllocationCallbacks* pAllocator);
|
||||
|
||||
// Query the list of available profiles in the library
|
||||
VPAPI_ATTR VkResult vpGetProfiles(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
uint32_t* pPropertyCount,
|
||||
VpProfileProperties* pProperties);
|
||||
|
||||
// List the required profiles of a profile
|
||||
VPAPI_ATTR VkResult vpGetProfileRequiredProfiles(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
uint32_t* pPropertyCount,
|
||||
VpProfileProperties* pProperties);
|
||||
|
||||
// Query the profile required Vulkan API version
|
||||
VPAPI_ATTR uint32_t vpGetProfileAPIVersion(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile);
|
||||
|
||||
// List the recommended fallback profiles of a profile
|
||||
VPAPI_ATTR VkResult vpGetProfileFallbacks(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
uint32_t* pPropertyCount,
|
||||
VpProfileProperties* pProperties);
|
||||
|
||||
// Query whether the profile has multiple variants. Profiles with multiple variants can only use vpGetInstanceProfileSupport and vpGetPhysicalDeviceProfileSupport capabilities of the library. Other function will return a VK_ERROR_UNKNOWN error
|
||||
VPAPI_ATTR VkResult vpHasMultipleVariantsProfile(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
VkBool32* pHasMultipleVariants);
|
||||
|
||||
// Check whether a profile is supported at the instance level
|
||||
VPAPI_ATTR VkResult vpGetInstanceProfileSupport(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const char* pLayerName,
|
||||
const VpProfileProperties* pProfile,
|
||||
VkBool32* pSupported);
|
||||
|
||||
// Check whether a variant of a profile is supported at the instance level and report this list of blocks used to validate the profiles
|
||||
VPAPI_ATTR VkResult vpGetInstanceProfileVariantsSupport(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const char* pLayerName,
|
||||
const VpProfileProperties* pProfile,
|
||||
VkBool32* pSupported,
|
||||
uint32_t* pPropertyCount,
|
||||
VpBlockProperties* pProperties);
|
||||
|
||||
// Create a VkInstance with the profile instance extensions enabled
|
||||
VPAPI_ATTR VkResult vpCreateInstance(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpInstanceCreateInfo* pCreateInfo,
|
||||
const VkAllocationCallbacks* pAllocator,
|
||||
VkInstance* pInstance);
|
||||
|
||||
// Check whether a profile is supported by the physical device
|
||||
VPAPI_ATTR VkResult vpGetPhysicalDeviceProfileSupport(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
VkInstance instance,
|
||||
VkPhysicalDevice physicalDevice,
|
||||
const VpProfileProperties* pProfile,
|
||||
VkBool32* pSupported);
|
||||
|
||||
// Check whether a variant of a profile is supported by the physical device and report this list of blocks used to validate the profiles
|
||||
VPAPI_ATTR VkResult vpGetPhysicalDeviceProfileVariantsSupport(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
VkInstance instance,
|
||||
VkPhysicalDevice physicalDevice,
|
||||
const VpProfileProperties* pProfile,
|
||||
VkBool32* pSupported,
|
||||
uint32_t* pPropertyCount,
|
||||
VpBlockProperties* pProperties);
|
||||
|
||||
// Create a VkDevice with the profile features and device extensions enabled
|
||||
VPAPI_ATTR VkResult vpCreateDevice(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
VkPhysicalDevice physicalDevice,
|
||||
const VpDeviceCreateInfo* pCreateInfo,
|
||||
const VkAllocationCallbacks* pAllocator,
|
||||
VkDevice* pDevice);
|
||||
|
||||
// Query the list of instance extensions of a profile
|
||||
VPAPI_ATTR VkResult vpGetProfileInstanceExtensionProperties(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t* pPropertyCount,
|
||||
VkExtensionProperties* pProperties);
|
||||
|
||||
// Query the list of device extensions of a profile
|
||||
VPAPI_ATTR VkResult vpGetProfileDeviceExtensionProperties(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t* pPropertyCount,
|
||||
VkExtensionProperties* pProperties);
|
||||
|
||||
// Fill the feature structures with the requirements of a profile
|
||||
VPAPI_ATTR VkResult vpGetProfileFeatures(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
void* pNext);
|
||||
|
||||
// Query the list of feature structure types specified by the profile
|
||||
VPAPI_ATTR VkResult vpGetProfileFeatureStructureTypes(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t* pStructureTypeCount,
|
||||
VkStructureType* pStructureTypes);
|
||||
|
||||
// Fill the property structures with the requirements of a profile
|
||||
VPAPI_ATTR VkResult vpGetProfileProperties(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
void* pNext);
|
||||
|
||||
// Query the list of property structure types specified by the profile
|
||||
VPAPI_ATTR VkResult vpGetProfilePropertyStructureTypes(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t* pStructureTypeCount,
|
||||
VkStructureType* pStructureTypes);
|
||||
|
||||
// Fill the queue family property structures with the requirements of a profile
|
||||
VPAPI_ATTR VkResult vpGetProfileQueueFamilyProperties(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t* pPropertyCount,
|
||||
VkQueueFamilyProperties2KHR* pProperties);
|
||||
|
||||
// Query the list of queue family property structure types specified by the profile
|
||||
VPAPI_ATTR VkResult vpGetProfileQueueFamilyStructureTypes(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t* pStructureTypeCount,
|
||||
VkStructureType* pStructureTypes);
|
||||
|
||||
// Query the list of formats with specified requirements by a profile
|
||||
VPAPI_ATTR VkResult vpGetProfileFormats(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t* pFormatCount,
|
||||
VkFormat* pFormats);
|
||||
|
||||
// Query the requirements of a format for a profile
|
||||
VPAPI_ATTR VkResult vpGetProfileFormatProperties(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
VkFormat format,
|
||||
void* pNext);
|
||||
|
||||
// Query the list of format structure types specified by the profile
|
||||
VPAPI_ATTR VkResult vpGetProfileFormatStructureTypes(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t* pStructureTypeCount,
|
||||
VkStructureType* pStructureTypes);
|
||||
|
||||
#ifdef VK_KHR_video_queue
|
||||
// Query the list of video profiles specified by the profile
|
||||
VPAPI_ATTR VkResult vpGetProfileVideoProfiles(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t* pVideoProfileCount,
|
||||
VpVideoProfileProperties* pVideoProfiles);
|
||||
|
||||
// Query the video profile info structures for a video profile defined by a profile
|
||||
VPAPI_ATTR VkResult vpGetProfileVideoProfileInfo(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t videoProfileIndex,
|
||||
VkVideoProfileInfoKHR* pVideoProfileInfo);
|
||||
|
||||
// Query the list of video profile info structure types specified by the profile for a video profile
|
||||
VPAPI_ATTR VkResult vpGetProfileVideoProfileInfoStructureTypes(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t videoProfileIndex,
|
||||
uint32_t* pStructureTypeCount,
|
||||
VkStructureType* pStructureTypes);
|
||||
|
||||
// Query the video capabilities requirements for a video profile defined by a profile
|
||||
VPAPI_ATTR VkResult vpGetProfileVideoCapabilities(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t videoProfileIndex,
|
||||
void* pNext);
|
||||
|
||||
// Query the list of video capability structure types specified by the profile for a video profile
|
||||
VPAPI_ATTR VkResult vpGetProfileVideoCapabilityStructureTypes(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t videoProfileIndex,
|
||||
uint32_t* pStructureTypeCount,
|
||||
VkStructureType* pStructureTypes);
|
||||
|
||||
// Query the video format property requirements for a video profile defined by a profile
|
||||
VPAPI_ATTR VkResult vpGetProfileVideoFormatProperties(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t videoProfileIndex,
|
||||
uint32_t* pPropertyCount,
|
||||
VkVideoFormatPropertiesKHR* pProperties);
|
||||
|
||||
// Query the list of video format property structure types specified by the profile for a video profile
|
||||
VPAPI_ATTR VkResult vpGetProfileVideoFormatStructureTypes(
|
||||
#ifdef VP_USE_OBJECT
|
||||
VpCapabilities capabilities,
|
||||
#endif//VP_USE_OBJECT
|
||||
const VpProfileProperties* pProfile,
|
||||
const char* pBlockName,
|
||||
uint32_t videoProfileIndex,
|
||||
uint32_t* pStructureTypeCount,
|
||||
VkStructureType* pStructureTypes);
|
||||
#endif // VK_KHR_video_queue
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // VULKAN_PROFILES_H_
|
||||
File diff suppressed because it is too large
Load Diff
+1
Submodule src/vendor/ktx added at 4d6fc70eaf
@@ -7,14 +7,14 @@
|
||||
#include "../../common/aliases/aliases.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) {
|
||||
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");
|
||||
|
||||
return header->count;
|
||||
@@ -23,7 +23,7 @@ u64 _arrayCount(WpArray array) {
|
||||
u64 _arrayCapacity(WpArray array) {
|
||||
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");
|
||||
|
||||
return header->capacity;
|
||||
@@ -32,7 +32,7 @@ u64 _arrayCapacity(WpArray array) {
|
||||
u64 _arrayItemSize(WpArray array) {
|
||||
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");
|
||||
|
||||
return header->item_size;
|
||||
@@ -41,7 +41,7 @@ u64 _arrayItemSize(WpArray array) {
|
||||
void _arraySetCount(WpArray array, u64 count) {
|
||||
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");
|
||||
|
||||
header->count = count;
|
||||
@@ -49,9 +49,9 @@ void _arraySetCount(WpArray array, u64 count) {
|
||||
|
||||
void *_arrayGet(WpArray array, u64 index, u64 item_size) {
|
||||
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");
|
||||
|
||||
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 *item = _arrayGet(array, index, item_size);
|
||||
|
||||
WpArrayHeader *header = _array_header(array);
|
||||
WpArrayHeader *header = _arrayHeader(array);
|
||||
memcpy(item, value, header->item_size);
|
||||
}
|
||||
|
||||
void _arrayAppendCapped(WpArray array, void *value, u64 item_size) {
|
||||
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; }
|
||||
|
||||
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) {
|
||||
wpRuntimeAssert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");
|
||||
_array_validate(dst, item_size);
|
||||
_array_validate(src, item_size);
|
||||
_arrayValidate(dst, item_size);
|
||||
_arrayValidate(src, item_size);
|
||||
|
||||
WpArrayHeader *src_header = _array_header(src);
|
||||
WpArrayHeader *dst_header = _array_header(dst);
|
||||
WpArrayHeader *src_header = _arrayHeader(src);
|
||||
WpArrayHeader *dst_header = _arrayHeader(dst);
|
||||
u64 remaining_capacity = dst_header->capacity - dst_header->count;
|
||||
|
||||
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) {
|
||||
wpRuntimeAssert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");
|
||||
_array_validate(dst, item_size);
|
||||
_array_validate(src, item_size);
|
||||
_arrayValidate(dst, item_size);
|
||||
_arrayValidate(src, item_size);
|
||||
|
||||
_arrayClear(dst, item_size);
|
||||
|
||||
WpArrayHeader *src_header = _array_header(src);
|
||||
WpArrayHeader *dst_header = _array_header(dst);
|
||||
WpArrayHeader *src_header = _arrayHeader(src);
|
||||
WpArrayHeader *dst_header = _arrayHeader(dst);
|
||||
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);
|
||||
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,
|
||||
WpArrayInitFlags flags, u64 item_size) {
|
||||
wpRuntimeAssert(allocator != NULL && array != NULL, "`allocator` and `array` should not be NULL");
|
||||
_array_validate(array, item_size);
|
||||
_arrayValidate(array, item_size);
|
||||
|
||||
WpArray output = array;
|
||||
|
||||
WpArrayHeader *header = _array_header(array);
|
||||
WpArrayHeader *header = _arrayHeader(array);
|
||||
if (header->count >= header->capacity) {
|
||||
u64 new_capacity = wpMiscUtilsU64RoundUpPow2(header->capacity * 2);
|
||||
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,
|
||||
WpArrayInitFlags flags, u64 item_size) {
|
||||
wpRuntimeAssert(allocator != NULL && dst != NULL && src != NULL, "`allocator`, `dst` and `src` should not be NULL");
|
||||
_array_validate(dst, item_size);
|
||||
_array_validate(src, item_size);
|
||||
_arrayValidate(dst, item_size);
|
||||
_arrayValidate(src, item_size);
|
||||
|
||||
WpArray output = dst;
|
||||
|
||||
WpArrayHeader *src_header = _array_header(src);
|
||||
WpArrayHeader *dst_header = _array_header(dst);
|
||||
WpArrayHeader *src_header = _arrayHeader(src);
|
||||
WpArrayHeader *dst_header = _arrayHeader(dst);
|
||||
u64 remaining_capacity = dst_header->capacity - dst_header->count;
|
||||
if (src_header->count >= remaining_capacity) {
|
||||
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,
|
||||
WpArrayInitFlags flags, u64 item_size) {
|
||||
wpRuntimeAssert(allocator != NULL && dst != NULL && src != NULL, "`allocator`, `dst` and `src` should not be NULL");
|
||||
_array_validate(dst, item_size);
|
||||
_array_validate(src, item_size);
|
||||
_arrayValidate(dst, item_size);
|
||||
_arrayValidate(src, item_size);
|
||||
|
||||
WpArray output = dst;
|
||||
|
||||
WpArrayHeader *src_header = _array_header(src);
|
||||
WpArrayHeader *dst_header = _array_header(dst);
|
||||
WpArrayHeader *src_header = _arrayHeader(src);
|
||||
WpArrayHeader *dst_header = _arrayHeader(dst);
|
||||
if (src_header->count >= dst_header->capacity) {
|
||||
u64 new_capacity = wpMiscUtilsU64RoundUpPow2(dst_header->capacity * 2);
|
||||
output = (WpArray )_arrayAllocCapacity(allocator, new_capacity,
|
||||
@@ -197,9 +197,9 @@ RETURN_ARRAY_COPY_ALLOC:
|
||||
|
||||
void *_arrayPop(WpArray array, u64 item_size) {
|
||||
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; }
|
||||
|
||||
u64 index = header->count - 1;
|
||||
@@ -210,9 +210,9 @@ void *_arrayPop(WpArray array, u64 item_size) {
|
||||
|
||||
void _arrayClear(WpArray array, u64 item_size) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -259,8 +259,18 @@ WpArray _arrayFromPreallocatedBuffer(void *buffer, u64 buffer_size, WpArrayInitF
|
||||
return output;
|
||||
}
|
||||
|
||||
wp_persist inline void _array_validate(const WpArray array, u64 item_size) {
|
||||
WpArrayHeader *header = _array_header(array);
|
||||
void _arrayDealloc(const WpAllocator *allocator, WpArray *array, u64 item_size) {
|
||||
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(item_size == header->item_size, "Invalid item type provided");
|
||||
}
|
||||
@@ -176,6 +176,10 @@ typedef enum {
|
||||
((TYPE *)_arrayAllocCapacity(ALLOCATOR_PTR, CAPACITY, FLAGS, sizeof(TYPE)))
|
||||
#define wpArrayFromPreallcatedBuffer(TYPE, BUFFER, BUFFER_SIZE) \
|
||||
((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;
|
||||
@@ -208,6 +212,7 @@ WpArray _arrayAllocCapacity(const WpAllocator *allocator, u64 capacity, WpArrayI
|
||||
u64 item_size);
|
||||
WpArray _arrayFromPreallocatedBuffer(void *buffer, u64 buffer_size, WpArrayInitFlags flags,
|
||||
u64 item_size);
|
||||
void _arrayDealloc(const WpAllocator *allocator, WpArray *array, u64 item_size);
|
||||
|
||||
#ifdef WP_PLATFORM_CPP
|
||||
END_C_LINKAGE
|
||||
Vendored
+5
-5
@@ -6,7 +6,7 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
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)) {
|
||||
return NULL;
|
||||
@@ -16,7 +16,7 @@ void *wpMemAllocatorAlloc(const WpAllocator *allocator, u64 size) {
|
||||
}
|
||||
|
||||
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)) {
|
||||
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) {
|
||||
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)) {
|
||||
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,
|
||||
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)) {
|
||||
return NULL;
|
||||
@@ -47,7 +47,7 @@ void *wpMemAllocatorReallocAligned(const WpAllocator *allocator, void *ptr, u64
|
||||
}
|
||||
|
||||
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)) {
|
||||
return;
|
||||
Vendored
@@ -42,7 +42,7 @@ typedef WpQueue WpStr8Queue;
|
||||
|
||||
#ifdef WP_PLATFORM_CPP
|
||||
#define wpQueue(TYPE, CAPACITY) ([&]() { \
|
||||
wp_persist WpArray arr = wpArrayWithCapacity(TYPE, CAPACITY, WP_ARRAY_INIT_FILLED); \
|
||||
wp_persist WpArray arr = wpArrayWithCapacity(TYPE, CAPACITY, WP_ARRAY_INIT_FILLED); \
|
||||
wp_persist WpQueue queue = { \
|
||||
arr, \
|
||||
0, \
|
||||
@@ -54,7 +54,7 @@ typedef WpQueue WpStr8Queue;
|
||||
}())
|
||||
#define wpQueueAlloc(TYPE, ALLOCATOR_PTR, CAPACITY) ([&]() { \
|
||||
wp_persist WpQueue queue = { \
|
||||
wpArrayAllocCapacity(TYPE, ALLOCATOR_PTR, CAPACITY, WP_ARRAY_INIT_FILLED), \
|
||||
wpArrayAllocCapacity(TYPE, ALLOCATOR_PTR, CAPACITY, WP_ARRAY_INIT_FILLED), \
|
||||
0, \
|
||||
0, \
|
||||
0, \
|
||||
@@ -64,13 +64,13 @@ typedef WpQueue WpStr8Queue;
|
||||
}())
|
||||
#else
|
||||
#define wpQueue(TYPE, CAPACITY) ((WpQueue){ \
|
||||
.items = wpArrayWithCapacity(TYPE, CAPACITY, WP_ARRAY_INIT_FILLED), \
|
||||
.items = wpArrayWithCapacity(TYPE, CAPACITY, WP_ARRAY_INIT_FILLED), \
|
||||
.front = 0, \
|
||||
.back = 0, \
|
||||
.count = 0, \
|
||||
})
|
||||
#define wpQueueAlloc(TYPE, ALLOCATOR_PTR, CAPACITY) ((WpQueue){ \
|
||||
.items = wpArrayAllocCapacity(TYPE, ALLOCATOR_PTR, CAPACITY, WP_ARRAY_INIT_FILLED), \
|
||||
.items = wpArrayAllocCapacity(TYPE, ALLOCATOR_PTR, CAPACITY, WP_ARRAY_INIT_FILLED), \
|
||||
.front = 0, \
|
||||
.back = 0, \
|
||||
.count = 0, \
|
||||
@@ -88,6 +88,11 @@ typedef WpQueue WpStr8Queue;
|
||||
#define wpQueuePop(TYPE, QUEUE_PTR) ( \
|
||||
(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);
|
||||
WpQueue *_queuePushAlloc(const WpAllocator *allocator, WpQueue *queue, void *item, u64 item_size);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user