Revert "First pass at nodes"

This reverts commit 618f09689d.
This commit is contained in:
2026-08-08 20:24:24 +01:00
parent 618f09689d
commit 30408ff244
25 changed files with 654 additions and 2222 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ build
compile_commands.json
.vscode
*.dSYM
assets/shaders
src/vendor/ktx/build
scratchpad/**
!scratchpad/**/
!scratchpad/**/*.h
-6
View File
@@ -94,12 +94,6 @@ 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);
-554
View File
@@ -1,554 +0,0 @@
# 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.
+2 -19
View File
@@ -5,7 +5,6 @@ default: build
CC := "clang"
CXX := "clang++"
SLANGC := "slangc"
BUILDDIR := "build"
# Resolve VULKAN_SDK once via backtick
@@ -31,19 +30,8 @@ vendor:
cmake --build {{BUILDDIR}}/ktx --config Release
cmake --install {{BUILDDIR}}/ktx
# Compile Slang shaders to SPIR-V
shaders:
mkdir -p assets/shaders
{{SLANGC}} -target spirv -stage vertex -entry main src/shaders/blit.vert.slang -o assets/shaders/blit.vert.spv
{{SLANGC}} -target spirv -stage vertex -entry main src/shaders/blit_to_swap.vert.slang -o assets/shaders/blit_to_swap.vert.spv
{{SLANGC}} -target spirv -stage fragment -entry main src/shaders/read.frag.slang -o assets/shaders/read.frag.spv
{{SLANGC}} -target spirv -stage fragment -entry main src/shaders/blur.frag.slang -o assets/shaders/blur.frag.spv
{{SLANGC}} -target spirv -stage fragment -entry main src/shaders/grade.frag.slang -o assets/shaders/grade.frag.spv
{{SLANGC}} -target spirv -stage fragment -entry main src/shaders/blend.frag.slang -o assets/shaders/blend.frag.spv
{{SLANGC}} -target spirv -stage fragment -entry main src/shaders/blit_to_swap.frag.slang -o assets/shaders/blit_to_swap.frag.spv
# Build all objects, then link
build: vendor shaders
build: vendor
mkdir -p {{BUILDDIR}}/bin
bear -- {{CXX}} -g -c -Wno-nullability-completeness {{VK_FLAGS}} \
src/prism/rhi/vulkan/profiles/vulkan_profiles.cpp \
@@ -55,11 +43,6 @@ build: vendor shaders
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 -- {{CC}} -g -c {{VK_FLAGS}} src/prism/allocators/pr_pool_allocator.c -o {{BUILDDIR}}/pr_pool_allocator.o
bear -a -- {{CC}} -g -c {{VK_FLAGS}} src/prism/core/pr_graph.c -o {{BUILDDIR}}/pr_graph.o
bear -a -- {{CC}} -g -c {{VK_FLAGS}} src/prism/core/pr_node.c -o {{BUILDDIR}}/pr_node.o
bear -a -- {{CC}} -g -c {{VK_FLAGS}} src/prism/core/pr_node_eval.c -o {{BUILDDIR}}/pr_node_eval.o
bear -a -- {{CC}} -g -c {{VK_FLAGS}} src/prism/core/pr_texture_pool.c -o {{BUILDDIR}}/pr_texture_pool.o
bear -a -- {{CXX}} -g -c {{VK_FLAGS}} -Wno-nullability-completeness -DVK_NO_PROTOTYPES \
{{APP_INC}} \
src/main.cpp \
@@ -67,7 +50,7 @@ build: vendor shaders
bear -a -- {{CXX}} -g {{VK_FLAGS}} \
-L{{VK_SDK}}/lib -L{{VENDOR_LIB}} \
build/*.o \
-lSDL3 -lktx -lvulkan \
-lSDL3 -lglm -ltinyobjloader -lktx -lslang -lvulkan \
-Wl,-rpath,{{VENDOR_LIB}} -Wl,-rpath,{{VK_SDK}}/lib \
-o {{BUILDDIR}}/bin/prism
@echo "--- build done: {{BUILDDIR}}/bin/prism ---"
+1 -1
View File
@@ -1,6 +1,6 @@
// vim:fileencoding=utf-8:foldmethod=marker
#include "../src/vendor/wapp/wapp.h"
#include "../src/wapp/wapp.h"
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
+635 -364
View File
File diff suppressed because it is too large Load Diff
-212
View File
@@ -1,212 +0,0 @@
// vim:fileencoding=utf-8:foldmethod=marker
#include "pr_graph.h"
#include "../../vendor/wapp/wapp.h"
#include <string.h>
// ---------------------------------------------------------------------------
// Internal: unlink edge helpers
// ---------------------------------------------------------------------------
static 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 == edge) {
if (prev) {
prev->next_forward = curr->next_forward;
} else {
vtx->next_forward = curr->next_forward;
}
return;
}
prev = curr;
curr = curr->next_forward;
}
}
static 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 == edge) {
if (prev) {
prev->next_backward = curr->next_backward;
} else {
vtx->next_backward = curr->next_backward;
}
return;
}
prev = curr;
curr = curr->next_backward;
}
}
// ---------------------------------------------------------------------------
// Graph lifecycle
// ---------------------------------------------------------------------------
wp_extern void prGraphInit(PrGraph *graph, WpAllocator *allocator, u64 capacity) {
memset(graph, 0, sizeof(*graph));
graph->capacity = capacity;
graph->vertices = wpArrayAllocCapacity(PrGraphVertex, allocator, capacity, WP_ARRAY_INIT_FILLED);
if (!graph->vertices) {
graph->capacity = 0;
return;
}
prPoolInit(&graph->edge_pool, sizeof(PrGraphEdge), 64);
}
wp_extern void prGraphDestroy(PrGraph *graph) {
prPoolDestroy(&graph->edge_pool);
// vertices are owned by the wapp allocator passed to prGraphInit
memset(graph, 0, sizeof(*graph));
}
// ---------------------------------------------------------------------------
// Vertex lifecycle
// ---------------------------------------------------------------------------
wp_extern 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_extern 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
// ---------------------------------------------------------------------------
wp_extern 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;
}
wp_extern 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_idx;
edge->target_idx = to_idx;
// 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;
}
wp_extern u64 prGraphVertexCount(const PrGraph *graph) {
return graph->vertex_count;
}
// ---------------------------------------------------------------------------
// Kahn's algorithm — topological sort / cycle detection
// ---------------------------------------------------------------------------
wp_extern WpU64Array prGraphTopologicalSort(const PrGraph *graph, const WpAllocator *allocator) {
if (graph->vertex_count == 0) { return NULL; }
if (!graph->vertices || graph->capacity == 0) { return NULL; }
WpU64Array result = wpArrayAllocCapacity(u64, allocator, graph->vertex_count, WP_ARRAY_INIT_NONE);
if (!result) { return NULL; }
WpAllocator local_arena = wpMemArenaAllocatorInitZero(KiB(16));
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;
}
-63
View File
@@ -1,63 +0,0 @@
// vim:fileencoding=utf-8:foldmethod=marker
#ifndef PR_GRAPH_H
#define PR_GRAPH_H
#include "../../vendor/wapp/common/aliases/aliases.h"
#include "../../vendor/wapp/base/mem/allocator/mem_allocator.h"
#include "../../vendor/wapp/base/wapp_base.h"
#include "../allocators/pr_pool_allocator.h"
#ifdef __cplusplus
extern "C" {
#endif
// ---------------------------------------------------------------------------
// PrGraphEdge — separately allocated adjacency list node
// ---------------------------------------------------------------------------
typedef struct PrGraphEdge PrGraphEdge;
struct PrGraphEdge {
PrGraphEdge *next_forward;
PrGraphEdge *next_backward;
u64 source_idx;
u64 target_idx;
};
// ---------------------------------------------------------------------------
// PrGraphVertex — compact adjacency head
// ---------------------------------------------------------------------------
typedef struct PrGraphVertex PrGraphVertex;
struct PrGraphVertex {
PrGraphEdge *next_forward;
PrGraphEdge *next_backward;
b8 active;
};
// ---------------------------------------------------------------------------
// PrGraph — owns topology (edges + adjacency heads)
// ---------------------------------------------------------------------------
typedef struct {
PrPool edge_pool;
PrGraphVertex *vertices;
u64 capacity;
u64 max_vertex_ever;
u64 vertex_count;
} PrGraph;
void prGraphInit(PrGraph *graph, WpAllocator *allocator, u64 capacity);
void prGraphDestroy(PrGraph *graph);
void prGraphAddVertex(PrGraph *graph, u64 idx);
void prGraphRemoveVertex(PrGraph *graph, u64 idx);
b8 prGraphAddEdge(PrGraph *graph, u64 from_idx, u64 to_idx);
b8 prGraphEdgeExists(const PrGraph *graph, u64 from_idx, u64 to_idx);
u64 prGraphVertexCount(const PrGraph *graph);
WpU64Array prGraphTopologicalSort(const PrGraph *graph, const WpAllocator *allocator);
#ifdef __cplusplus
}
#endif
#endif // !PR_GRAPH_H
-99
View File
@@ -1,99 +0,0 @@
// vim:fileencoding=utf-8:foldmethod=marker
#include "pr_node.h"
#include "../../vendor/wapp/wapp.h"
#include <string.h>
// ---------------------------------------------------------------------------
// Node manager lifecycle
// ---------------------------------------------------------------------------
wp_extern void prNodeManagerInit(PrNodeManager *mgr, WpAllocator *allocator, u64 capacity) {
memset(mgr, 0, sizeof(*mgr));
mgr->capacity = capacity;
mgr->nodes = wpArrayAllocCapacity(PrNode, allocator, capacity, WP_ARRAY_INIT_FILLED);
if (!mgr->nodes) {
mgr->capacity = 0;
return;
}
mgr->free_head = 0;
for (u64 i = 0; i < capacity; ++i) {
mgr->nodes[i].next_free = i < capacity - 1 ? i + 1 : INVALID_NODE_INDEX;
}
prGraphInit(&mgr->graph, allocator, capacity);
}
wp_extern void prNodeManagerDestroy(PrNodeManager *mgr) {
prGraphDestroy(&mgr->graph);
// nodes are owned by the wapp allocator passed to prNodeManagerInit
memset(mgr, 0, sizeof(*mgr));
}
// ---------------------------------------------------------------------------
// Handle queries
// ---------------------------------------------------------------------------
wp_extern b8 prNodeManagerIsStaleNode(const PrNodeManager *mgr, PrNodeId id) {
u64 generation = mgr->nodes[id.index].generation;
return id.generation != generation;
}
wp_extern 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;
}
wp_extern PrNodeId prNodeManagerGetNode(const PrNodeManager *mgr, u64 index) {
return (PrNodeId){ .index = index, .generation = mgr->nodes[index].generation };
}
// ---------------------------------------------------------------------------
// Node lifecycle
// ---------------------------------------------------------------------------
wp_extern 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_extern 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--;
}
// ---------------------------------------------------------------------------
// Edge management
// ---------------------------------------------------------------------------
wp_extern 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);
}
-93
View File
@@ -1,93 +0,0 @@
// vim:fileencoding=utf-8:foldmethod=marker
#ifndef PR_NODE_H
#define PR_NODE_H
#include "../../vendor/wapp/common/aliases/aliases.h"
#include "pr_graph.h"
#ifdef __cplusplus
extern "C" {
#endif
// Forward declarations
typedef struct PrRhiTexture PrRhiTexture;
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#define INVALID_NODE_INDEX (u64)-1
#define INVALID_NODE_ID ((PrNodeId){ .index = INVALID_NODE_INDEX, .generation = INVALID_NODE_INDEX })
// ---------------------------------------------------------------------------
// PrNodeType
// ---------------------------------------------------------------------------
typedef enum {
PR_NODE_TYPE_NONE,
PR_NODE_TYPE_READ,
PR_NODE_TYPE_BLUR,
PR_NODE_TYPE_GRADE,
PR_NODE_TYPE_BLEND,
COUNT_NODE_TYPES
} PrNodeType;
// ---------------------------------------------------------------------------
// PrNodeId — generational handle
// ---------------------------------------------------------------------------
typedef struct {
u64 index;
u64 generation;
} PrNodeId;
// ---------------------------------------------------------------------------
// PrNode — compositor node data
// ---------------------------------------------------------------------------
typedef struct {
union {
//WpStr8 path; // READ: texture path
f32 radius; // BLUR: radius
struct { // GRADE: colour grading
f32 gain;
f32 offset;
f32 power;
} grade;
u32 mode; // BLEND: 0=over, 1=under, 2=add
} params;
PrRhiTexture *texture; // READ: persistent KTX texture
PrNodeType type;
u64 generation;
u64 next_free;
} PrNode;
// ---------------------------------------------------------------------------
// PrNodeManager — owns node data + handle lifecycle + topology
// ---------------------------------------------------------------------------
typedef struct {
PrNode *nodes;
PrGraph graph;
u64 capacity;
u64 max_count_ever;
u64 count;
u64 free_head;
} PrNodeManager;
void prNodeManagerInit(PrNodeManager *mgr, WpAllocator *allocator, u64 capacity);
void prNodeManagerDestroy(PrNodeManager *mgr);
b8 prNodeManagerIsStaleNode(const PrNodeManager *mgr, PrNodeId id);
b8 prNodeManagerIsActiveNode(const PrNodeManager *mgr, PrNodeId id);
PrNodeId prNodeManagerGetNode(const PrNodeManager *mgr, u64 index);
PrNodeId prNodeManagerAddNode(PrNodeManager *mgr, PrNodeType type);
void prNodeManagerRemoveNode(PrNodeManager *mgr, PrNodeId id);
void prNodeManagerAddEdge(PrNodeManager *mgr, PrNodeId from, PrNodeId to);
#ifdef __cplusplus
}
#endif
#endif // !PR_NODE_H
-358
View File
@@ -1,358 +0,0 @@
// vim:fileencoding=utf-8:foldmethod=marker
#include "pr_node_eval.h"
#include "../rhi/pr_rhi.h"
#include "../../vendor/wapp/wapp.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
wp_intern PrNodeTypeEntry _node_type_table_data[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 = 1,
.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),
},
};
PrNodeTypeEntry pr_node_type_table[COUNT_NODE_TYPES];
wp_persist PrRhiDescriptorPool *_eval_desc_pool;
wp_intern void *_loadSpirv(const WpAllocator *alloc, const char *path, u64 *out_size) {
u64 path_len = strlen(path);
WpStr8RO filepath = { path_len, path_len, (c8 *)path };
WpFile *f = wpFileOpen(alloc, &filepath, WP_ACCESS_READ);
if (!f) {
fprintf(stderr, "failed to open SPIR-V: %s\n", path);
abort();
}
i64 file_size = wpFileGetLength(f);
if (file_size <= 0) {
wpFileClose(f);
fprintf(stderr, "empty SPIR-V file: %s\n", path);
abort();
}
void *code = wpMemAllocatorAlloc(alloc, (u64)file_size);
if (!code) { wpFileClose(f); abort(); }
u64 bytes_read = wpFileRead(code, f, (u64)file_size);
wpFileClose(f);
if (bytes_read != (u64)file_size) {
fprintf(stderr, "short read on SPIR-V: %s\n", path);
abort();
}
*out_size = (u64)file_size;
return code;
}
wp_extern void prNodeEvalInit(PrRhiDevice *device, PrRhiFormat output_format) {
(void)output_format;
memcpy(pr_node_type_table, _node_type_table_data, sizeof(_node_type_table_data));
for (u32 i = 0; i < COUNT_NODE_TYPES; ++i) {
PrNodeTypeEntry *entry = &pr_node_type_table[i];
if (entry->shader_type != PR_SHADER_TYPE_FRAGMENT) { continue; }
if (!entry->vertex_shader_path || !entry->fragment_shader_path) { continue; }
// load SPIR-V
u64 vert_size = 0, frag_size = 0;
void *vert_code = _loadSpirv(&_G_RHI_CONTEXT.allocator, entry->vertex_shader_path, &vert_size);
void *frag_code = _loadSpirv(&_G_RHI_CONTEXT.allocator, entry->fragment_shader_path, &frag_size);
// create shaders
entry->vertex_shader = prRhiCreateShader(device, (PrRhiShaderDesc){
.spirv_code = vert_code,
.spirv_size = vert_size,
});
entry->fragment_shader = prRhiCreateShader(device, (PrRhiShaderDesc){
.spirv_code = frag_code,
.spirv_size = frag_size,
});
// free SPIR-V (now owned by Vulkan)
wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, &vert_code, vert_size);
wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, &frag_code, frag_size);
// create descriptor set layout
PrRhiDescriptorSetLayoutBindingArray bindings = NULL;
if (entry->input_count > 0) {
bindings = wpArrayWithCapacity(PrRhiDescriptorSetLayoutBinding, 4, WP_ARRAY_INIT_NONE);
for (u32 b = 0; b < entry->input_count; ++b) {
PrRhiDescriptorSetLayoutBinding binding = {
.type = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
.descriptor_count = 1,
.stage_flags = PR_RHI_SHADER_STAGE_FRAGMENT,
.binding_flags = (PrRhiDescriptorBindingFlag)0,
};
wpArrayAppendCapped(PrRhiDescriptorSetLayoutBinding, bindings, &binding);
}
}
entry->set_layout = prRhiCreateDescriptorSetLayout(device, (PrRhiDescriptorSetLayoutDesc){
.bindings = bindings,
});
// create pipeline layout
PrRhiPushConstantRange pc_range = {
.stage_flags = PR_RHI_SHADER_STAGE_FRAGMENT,
.offset = 0,
.size = entry->push_constant_size,
};
PrRhiDescriptorSetLayoutArray set_layouts = entry->set_layout
? wpArray(PrRhiDescriptorSetLayout *, entry->set_layout)
: NULL;
PrRhiPushConstantRangeArray pc_ranges = entry->push_constant_size > 0
? wpArray(PrRhiPushConstantRange, pc_range)
: NULL;
entry->pipeline_layout = prRhiCreatePipelineLayout(device, (PrRhiPipelineLayoutDesc){
.set_layouts = set_layouts,
.push_constant_ranges = pc_ranges,
});
// create graphics pipeline
// pool textures are always RGBA16F
PrRhiFormatArray color_formats = wpArray(PrRhiFormat, PR_RHI_FORMAT_R16G16B16A16_SFLOAT);
PrRhiColorBlendAttachmentArray blend_attachments = wpArray(PrRhiColorBlendAttachment,
((PrRhiColorBlendAttachment){ .color_write_mask = 0xF }));
entry->pipeline = prRhiCreateGraphicsPipeline(device, (PrRhiGraphicsPipelineDesc){
.vertex_shader = entry->vertex_shader,
.vertex_shader_entry_point = "main",
.fragment_shader = entry->fragment_shader,
.fragment_shader_entry_point = "main",
.vertex_bindings = NULL,
.vertex_attributes = NULL,
.topology = PR_RHI_TOPOLOGY_TRIANGLE_LIST,
.color_attachment_formats = color_formats,
.depth_attachment_format = PR_RHI_FORMAT_UNDEFINED,
.depth_test_enable = false,
.depth_write_enable = false,
.depth_compare_op = PR_RHI_COMPARE_OP_ALWAYS,
.blend_attachments = blend_attachments,
.dynamic_viewport = true,
.dynamic_scissor = true,
.polygon_mode = PR_RHI_POLYGON_MODE_FILL,
.cull_mode = PR_RHI_CULL_MODE_NONE,
.front_face = PR_RHI_FRONT_FACE_COUNTER_CLOCKWISE,
.line_width = 1.0,
.multisample_count = PR_RHI_SAMPLE_COUNT_1,
.layout = entry->pipeline_layout,
});
}
// create per-frame descriptor pool (reset each frame)
PrRhiDescriptorPoolSizeArray pool_sizes = wpArray(PrRhiDescriptorPoolSize,
((PrRhiDescriptorPoolSize){ .type = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptor_count = 64 }));
_eval_desc_pool = prRhiCreateDescriptorPool(device, (PrRhiDescriptorPoolDesc){
.max_sets = 64,
.pool_sizes = pool_sizes,
});
}
wp_extern void prNodeEvalDestroy(PrRhiDevice *device) {
if (_eval_desc_pool) {
prRhiDestroyDescriptorPool(device, _eval_desc_pool);
_eval_desc_pool = NULL;
}
for (u32 i = 0; i < COUNT_NODE_TYPES; ++i) {
PrNodeTypeEntry *entry = &pr_node_type_table[i];
if (entry->pipeline) {
prRhiDestroyPipeline(device, entry->pipeline);
entry->pipeline = NULL;
}
if (entry->pipeline_layout) {
prRhiDestroyPipelineLayout(device, entry->pipeline_layout);
entry->pipeline_layout = NULL;
}
if (entry->set_layout) {
prRhiDestroyDescriptorSetLayout(device, entry->set_layout);
entry->set_layout = NULL;
}
if (entry->fragment_shader) {
prRhiDestroyShader(device, entry->fragment_shader);
entry->fragment_shader = NULL;
}
if (entry->vertex_shader) {
prRhiDestroyShader(device, entry->vertex_shader);
entry->vertex_shader = NULL;
}
}
}
// ---------------------------------------------------------------------------
// Per-frame evaluation
// ---------------------------------------------------------------------------
wp_extern void prGraphEvaluate(PrNodeManager *mgr, PrRhiDevice *device,
PrTexturePool *pool, PrRhiCommandBuffer *cb,
PrRhiSampler *shared_sampler,
PrTextureSlot **out_output) {
PrGraph *graph = &mgr->graph;
u64 vertex_count = prGraphVertexCount(graph);
if (out_output) { *out_output = NULL; }
// 1. topological sort
WpAllocator scratch = wpMemArenaAllocatorInitZero(KiB(64));
WpU64Array topo = prGraphTopologicalSort(graph, &scratch);
u64 topo_count = topo ? wpArrayCount(topo) : 0;
// 2. compute initial refcounts (out-degree per node)
u32 *refcounts = wpMemAllocatorAlloc(&scratch, vertex_count * sizeof(u32));
memset(refcounts, 0, vertex_count * sizeof(u32));
for (u64 i = 0; i < graph->max_vertex_ever; ++i) {
if (!graph->vertices[i].active) { continue; }
PrGraphEdge *edge = graph->vertices[i].next_forward;
while (edge) {
refcounts[i]++;
edge = edge->next_forward;
}
}
// 3. reset texture pool and descriptor pool
prTexturePoolReset(pool);
if (_eval_desc_pool) {
prRhiResetDescriptorPool(device, _eval_desc_pool);
}
// output slot per node (transient, lives in scratch arena)
PrTextureSlot **output_slots = wpMemAllocatorAlloc(&scratch, vertex_count * sizeof(PrTextureSlot *));
memset(output_slots, 0, vertex_count * sizeof(PrTextureSlot *));
// 4. for each node in topological order
for (u64 t = 0; t < topo_count; ++t) {
u64 node_idx = topo[t];
PrNode *node = &mgr->nodes[node_idx];
if (node->generation == 0) { continue; } // inactive node
PrNodeTypeEntry *entry = &pr_node_type_table[node->type];
// acquire output texture
PrTextureSlot *output_slot = prTexturePoolAcquire(pool, device);
if (!output_slot) { abort(); }
// gather input textures (predecessors via backward edges)
PrTextureSlot *input_slots[4];
u32 input_count = 0;
PrGraphEdge *edge = graph->vertices[node_idx].next_backward;
while (edge && input_count < 4) {
u64 src_idx = edge->source_idx;
if (output_slots[src_idx]) {
input_slots[input_count++] = output_slots[src_idx];
}
edge = edge->next_backward;
}
// READ nodes: use the node's own persistent texture if no edges
if (input_count == 0 && node->texture) {
input_slots[0] = NULL;
input_count = 1;
}
// allocate descriptor set (skip for nodes with no inputs)
PrRhiDescriptorSet *desc_set = NULL;
if (entry->input_count > 0 && input_count > 0) {
desc_set = prRhiAllocateDescriptorSet(device, _eval_desc_pool, entry->set_layout, NULL);
// build write descriptors
PrRhiDescriptorImageInfo image_infos[4];
PrRhiWriteDescriptorSet writes[4];
for (u32 i = 0; i < input_count; ++i) {
PrRhiTexture *tex = input_slots[i] ? input_slots[i]->texture : node->texture;
image_infos[i] = (PrRhiDescriptorImageInfo){
.texture = tex,
.sampler = shared_sampler,
.layout = PR_RHI_LAYOUT_READ_ONLY_OPTIMAL,
};
writes[i] = (PrRhiWriteDescriptorSet){
.dst_set = desc_set,
.dst_binding = i,
.dst_array_element= 0,
.type = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
.image_info = &image_infos[i],
.buffer_info = NULL,
};
}
prRhiUpdateDescriptorSet(device, writes);
}
// record commands
PrRhiColorAttachment color_att = {
.texture = output_slot->texture,
.layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL,
.clear = true,
.clear_color = {0.0f, 0.0f, 0.0f, 0.0f},
};
prRhiCmdBeginRendering(cb, &color_att, NULL);
prRhiCmdBindPipeline(cb, PR_RHI_PIPELINE_BIND_POINT_GRAPHICS, entry->pipeline);
prRhiCmdSetViewport(cb, 0.0f, 0.0f, (f32)pool->width, (f32)pool->height);
prRhiCmdSetScissor(cb, 0, 0, pool->width, pool->height);
if (desc_set) {
PrRhiDescriptorSet *sets_arr[1] = { desc_set };
prRhiCmdBindDescriptorSets(cb, PR_RHI_PIPELINE_BIND_POINT_GRAPHICS,
entry->pipeline_layout, 0, sets_arr);
}
// push constants
if (entry->push_constant_size > 0) {
prRhiCmdPushConstants(cb, entry->pipeline_layout,
PR_RHI_SHADER_STAGE_FRAGMENT, 0,
entry->push_constant_size, &node->params);
}
prRhiCmdDraw(cb, 3, 1, 0, 0);
prRhiCmdEndRendering(cb);
// release input textures whose refcount hit 0
edge = graph->vertices[node_idx].next_backward;
u32 input_idx = 0;
while (edge && input_idx < input_count) {
u64 src_idx = edge->source_idx;
if (refcounts[src_idx] > 0) {
refcounts[src_idx]--;
if (refcounts[src_idx] == 0) {
prTexturePoolRelease(pool, output_slots[src_idx]);
}
}
edge = edge->next_backward;
input_idx++;
}
output_slots[node_idx] = output_slot;
}
// Return the last node's output as the compositor output
if (out_output && topo_count > 0) {
u64 last_idx = topo[topo_count - 1];
*out_output = output_slots[last_idx];
}
}
-95
View File
@@ -1,95 +0,0 @@
// vim:fileencoding=utf-8:foldmethod=marker
#ifndef PR_NODE_EVAL_H
#define PR_NODE_EVAL_H
#include "../../vendor/wapp/common/aliases/aliases.h"
#include "../rhi/pr_rhi_types.h"
#include "pr_node.h"
#include "pr_texture_pool.h"
#ifdef __cplusplus
extern "C" {
#endif
// ---------------------------------------------------------------------------
// Shader type
// ---------------------------------------------------------------------------
typedef enum PrShaderType {
PR_SHADER_TYPE_FRAGMENT, // fullscreen triangle, per-pixel
PR_SHADER_TYPE_COMPUTE, // dispatch, shared memory
} PrShaderType;
// ---------------------------------------------------------------------------
// Push constant structs (one per node type)
// ---------------------------------------------------------------------------
typedef struct {
f32 radius;
} PrBlurPushConstants;
typedef struct {
f32 gain;
f32 offset;
f32 power;
} PrGradePushConstants;
typedef struct {
u32 mode; // 0=over, 1=under, 2=add
} PrBlendPushConstants;
// ---------------------------------------------------------------------------
// Node type entry — maps a node type to its resource signature
// ---------------------------------------------------------------------------
typedef struct PrNodeTypeEntry {
PrNodeType type;
PrShaderType shader_type;
// shader paths (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
// resource signature
u32 input_count; // number of texture inputs
u32 output_count; // always 1 for V1
// push constant size (bytes)
u32 push_constant_size;
// created at init, cached here
PrRhiShader *vertex_shader;
PrRhiShader *fragment_shader;
PrRhiDescriptorSetLayout *set_layout;
PrRhiPipelineLayout *pipeline_layout;
PrRhiPipeline *pipeline;
} PrNodeTypeEntry;
// ---------------------------------------------------------------------------
// Global registry
// ---------------------------------------------------------------------------
extern PrNodeTypeEntry pr_node_type_table[COUNT_NODE_TYPES];
// ---------------------------------------------------------------------------
// Evaluation
// ---------------------------------------------------------------------------
wp_extern void prNodeEvalInit(PrRhiDevice *device, PrRhiFormat output_format);
wp_extern void prNodeEvalDestroy(PrRhiDevice *device);
// Per-frame graph evaluation. Records commands into cb.
// pool is reset each frame. desc_pool is reset each frame.
// out_output receives the final compositor output slot (last node in topo order).
wp_extern void prGraphEvaluate(PrNodeManager *mgr, PrRhiDevice *device,
PrTexturePool *pool, PrRhiCommandBuffer *cb,
PrRhiSampler *shared_sampler,
PrTextureSlot **out_output);
#ifdef __cplusplus
}
#endif
#endif // !PR_NODE_EVAL_H
-101
View File
@@ -1,101 +0,0 @@
#include "pr_texture_pool.h"
#include "../rhi/pr_rhi.h"
#include "../../vendor/wapp/wapp.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
wp_intern b8 _growPool(PrTexturePool *pool, PrRhiDevice *device) {
u32 old_count = pool->count;
u32 new_count = old_count + PR_TEXTURE_POOL_GROWTH_BATCH;
if (new_count > pool->max) { new_count = pool->max; }
if (old_count >= pool->max) {
fprintf(stderr, "texture pool exhausted: %u in use, max %u\n", pool->in_use, pool->max);
abort();
}
PrTextureSlot *new_slots = wpArrayAllocCapacity(PrTextureSlot, pool->alloc, new_count, WP_ARRAY_INIT_FILLED);
if (!new_slots) { return false; }
if (pool->slots) {
memcpy(new_slots, pool->slots, old_count * sizeof(PrTextureSlot));
wpArrayDealloc(PrTextureSlot, pool->alloc, &pool->slots);
}
pool->slots = new_slots;
for (u32 i = old_count; i < new_count; ++i) {
PrRhiTextureDesc desc = {
.format = PR_RHI_FORMAT_R16G16B16A16_SFLOAT,
.width = pool->width,
.height = pool->height,
.mip_levels = 1,
.usage = PR_RHI_TEXTURE_USAGE_SAMPLED | PR_RHI_TEXTURE_USAGE_COLOR_ATTACHMENT,
};
PrRhiTexture *tex = prRhiCreateTexture(device, desc);
if (!tex) { return false; }
pool->slots[i].texture = tex;
pool->slots[i].refcount = 0;
pool->slots[i].in_use = false;
}
pool->count = new_count;
return true;
}
wp_extern void prTexturePoolInit(PrTexturePool *pool, u32 initial_capacity, u32 max, u32 width, u32 height, const WpAllocator *alloc) {
pool->alloc = alloc;
pool->slots = NULL;
pool->count = 0;
pool->in_use = 0;
pool->max = max;
pool->width = width;
pool->height = height;
if (initial_capacity > 0) {
pool->slots = wpArrayAllocCapacity(PrTextureSlot, alloc, initial_capacity, WP_ARRAY_INIT_FILLED);
if (!pool->slots) {
fprintf(stderr, "texture pool initial allocation failed\n");
abort();
}
pool->count = initial_capacity;
}
}
wp_extern void prTexturePoolReset(PrTexturePool *pool) {
for (u32 i = 0; i < pool->count; ++i) {
pool->slots[i].refcount = 0;
pool->slots[i].in_use = false;
}
pool->in_use = 0;
}
wp_extern PrTextureSlot *prTexturePoolAcquire(PrTexturePool *pool, PrRhiDevice *device) {
for (u32 i = 0; i < pool->count; ++i) {
if (!pool->slots[i].in_use) {
pool->slots[i].in_use = true;
pool->in_use += 1;
return &pool->slots[i];
}
}
if (!_growPool(pool, device)) { return NULL; }
PrTextureSlot *slot = &pool->slots[pool->count - 1];
slot->in_use = true;
pool->in_use += 1;
return slot;
}
wp_extern void prTexturePoolRelease(PrTexturePool *pool, PrTextureSlot *slot) {
(void)pool;
slot->refcount = 0;
slot->in_use = false;
pool->in_use -= 1;
}
wp_extern void prTexturePoolDestroy(PrTexturePool *pool, PrRhiDevice *device) {
for (u32 i = 0; i < pool->count; ++i) {
if (pool->slots[i].texture) {
prRhiDestroyTexture(device, pool->slots[i].texture);
}
}
if (pool->slots) { wpArrayDealloc(PrTextureSlot, pool->alloc, &pool->slots); }
pool->slots = NULL;
pool->count = 0;
pool->in_use = 0;
}
-38
View File
@@ -1,38 +0,0 @@
#ifndef PR_TEXTURE_POOL_H
#define PR_TEXTURE_POOL_H
#include "../rhi/pr_rhi_types.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PR_TEXTURE_POOL_GROWTH_BATCH 8
typedef struct PrTextureSlot {
PrRhiTexture *texture;
u32 refcount;
b8 in_use;
} PrTextureSlot;
typedef struct PrTexturePool {
PrTextureSlot *slots;
const WpAllocator *alloc;
u32 count;
u32 in_use;
u32 max;
u32 width;
u32 height;
} PrTexturePool;
wp_extern void prTexturePoolInit(PrTexturePool *pool, u32 initial_capacity, u32 max, u32 width, u32 height, const WpAllocator *alloc);
wp_extern void prTexturePoolReset(PrTexturePool *pool);
wp_extern PrTextureSlot *prTexturePoolAcquire(PrTexturePool *pool, PrRhiDevice *device);
wp_extern void prTexturePoolRelease(PrTexturePool *pool, PrTextureSlot *slot);
wp_extern void prTexturePoolDestroy(PrTexturePool *pool, PrRhiDevice *device);
#ifdef __cplusplus
}
#endif
#endif
-2
View File
@@ -162,8 +162,6 @@ PrRhiDescriptorPool *prRhiCreateDescriptorPool(PrRhiDevice *device,
PrRhiDescriptorPoolDesc desc);
void prRhiDestroyDescriptorPool(PrRhiDevice *device,
PrRhiDescriptorPool *pool);
void prRhiResetDescriptorPool(PrRhiDevice *device,
PrRhiDescriptorPool *pool);
// ======================================================================
// Descriptor sets
+4 -14
View File
@@ -1399,9 +1399,7 @@ PrRhiPipelineLayout *prRhiCreatePipelineLayoutVk(PrRhiDevice *device,
u32 layout_count = (desc.set_layouts && wpArrayCount(desc.set_layouts) > 0)
? (u32)wpArrayCount(desc.set_layouts) : 0;
VkDescriptorSetLayoutArray vk_layouts = NULL;
if (layout_count > 0) {
vk_layouts =
VkDescriptorSetLayoutArray vk_layouts =
wpArrayAllocCapacity(VkDescriptorSetLayout, &_G_RHI_CONTEXT.allocator, layout_count,
WP_ARRAY_INIT_FILLED);
if (!vk_layouts) { _abort("alloc failed for VkDescriptorSetLayout array"); }
@@ -1409,15 +1407,12 @@ PrRhiPipelineLayout *prRhiCreatePipelineLayoutVk(PrRhiDevice *device,
for (u32 i = 0; i < layout_count; ++i) {
vk_layouts[i] = desc.set_layouts[i]->handle;
}
}
// Gather push constant ranges
u32 pc_count = (desc.push_constant_ranges && wpArrayCount(desc.push_constant_ranges) > 0)
? (u32)wpArrayCount(desc.push_constant_ranges) : 0;
VkPushConstantRangeArray pc_ranges = NULL;
if (pc_count > 0) {
pc_ranges =
VkPushConstantRangeArray pc_ranges =
wpArrayAllocCapacity(VkPushConstantRange, &_G_RHI_CONTEXT.allocator, pc_count, WP_ARRAY_INIT_NONE);
if (!pc_ranges) { _abort("alloc failed for VkPushConstantRange array"); }
@@ -1426,7 +1421,6 @@ PrRhiPipelineLayout *prRhiCreatePipelineLayoutVk(PrRhiDevice *device,
pc_ranges[i].offset = desc.push_constant_ranges[i].offset;
pc_ranges[i].size = desc.push_constant_ranges[i].size;
}
}
VkPipelineLayoutCreateInfo pl_info = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
@@ -1439,8 +1433,8 @@ PrRhiPipelineLayout *prRhiCreatePipelineLayoutVk(PrRhiDevice *device,
VkPipelineLayout vk_layout = VK_NULL_HANDLE;
_checkVk(vkCreatePipelineLayout(vk_device, &pl_info, NULL, &vk_layout), "vkCreatePipelineLayout");
if (vk_layouts) { wpArrayDealloc(VkDescriptorSetLayout, &_G_RHI_CONTEXT.allocator, &vk_layouts); }
if (pc_ranges) { wpArrayDealloc(VkPushConstantRange, &_G_RHI_CONTEXT.allocator, &pc_ranges); }
wpArrayDealloc(VkDescriptorSetLayout, &_G_RHI_CONTEXT.allocator, &vk_layouts);
wpArrayDealloc(VkPushConstantRange, &_G_RHI_CONTEXT.allocator, &pc_ranges);
PrRhiPipelineLayout *layout = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiPipelineLayout));
if (!layout) { _abort("alloc failed for PrRhiPipelineLayout"); }
@@ -1764,10 +1758,6 @@ void prRhiDestroyDescriptorPoolVk(PrRhiDevice *device, PrRhiDescriptorPool *pool
wpMemAllocatorFree(&_G_RHI_CONTEXT.allocator, (void**)&pool, sizeof(PrRhiDescriptorPool));
}
void prRhiResetDescriptorPoolVk(PrRhiDevice *device, PrRhiDescriptorPool *pool) {
vkResetDescriptorPool(device->handle, pool->handle, 0);
}
// ============================================================================
// Descriptor sets
// ============================================================================
-2
View File
@@ -193,8 +193,6 @@ PrRhiDescriptorPool *prRhiCreateDescriptorPoolVk(PrRhiDevice *device,
PrRhiDescriptorPoolDesc desc);
void prRhiDestroyDescriptorPoolVk(PrRhiDevice *device,
PrRhiDescriptorPool *pool);
void prRhiResetDescriptorPoolVk(PrRhiDevice *device,
PrRhiDescriptorPool *pool);
PrRhiDescriptorSet *prRhiAllocateDescriptorSetVk(PrRhiDevice *device,
PrRhiDescriptorPool *pool,
-1
View File
@@ -48,7 +48,6 @@
#define prRhiDestroyDescriptorSetLayout prRhiDestroyDescriptorSetLayoutVk
#define prRhiCreateDescriptorPool prRhiCreateDescriptorPoolVk
#define prRhiDestroyDescriptorPool prRhiDestroyDescriptorPoolVk
#define prRhiResetDescriptorPool prRhiResetDescriptorPoolVk
#define prRhiAllocateDescriptorSet prRhiAllocateDescriptorSetVk
#define prRhiFreeDescriptorSet prRhiFreeDescriptorSetVk
#define prRhiUpdateDescriptorSet prRhiUpdateDescriptorSetVk
-44
View File
@@ -1,44 +0,0 @@
// BLEND node — composites two textures.
// 2 input textures, push constant: u32 mode (0=over, 1=under, 2=add).
[[vk::binding(0, 0)]]
Texture2D<float4> background : register(t0);
[[vk::binding(1, 0)]]
Texture2D<float4> foreground : register(t1);
[[vk::binding(2, 0)]]
SamplerState input_sampler : register(s0);
struct PushConstants {
uint mode;
};
[[vk::push_constant]]
PushConstants pc;
struct VSOutput {
float4 position : SV_Position;
float2 uv : TEXCOORD0;
};
float4 main(VSOutput input) : SV_Target {
float4 bg = background.Sample(input_sampler, input.uv);
float4 fg = foreground.Sample(input_sampler, input.uv);
float4 result;
switch (pc.mode) {
case 0: // over: foreground over background
result = fg.a * fg + (1.0 - fg.a) * bg;
break;
case 1: // under: background over foreground
result = bg.a * bg + (1.0 - bg.a) * fg;
break;
case 2: // add: additive blend
result = bg + fg;
break;
default:
result = fg;
break;
}
return result;
}
-18
View File
@@ -1,18 +0,0 @@
// Fullscreen triangle — no vertex buffer needed.
// Uses gl_VertexIndex to generate a single triangle that covers the viewport.
struct VSOutput {
float4 position : SV_Position;
float2 uv : TEXCOORD0;
};
VSOutput main(uint vertex_id : SV_VertexID) {
VSOutput output;
// Generate UV from vertex ID (0, 1, 2)
output.uv = float2((vertex_id << 1) & 2, vertex_id & 2);
// Generate clip-space position
output.position = float4(output.uv * 2.0 - 1.0, 0.0, 1.0);
// Flip Y for Vulkan
output.position.y = -output.position.y;
return output;
}
-10
View File
@@ -1,10 +0,0 @@
// Blit-to-swapchain fragment shader.
// Samples the compositor output (RGBA16F pool texture) and writes it
// to the swapchain color attachment.
[vk::binding(0, 0)] Texture2D<float4> tex : register(t0);
[vk::binding(1, 0)] SamplerState smp : register(s0);
float4 main(float4 position : SV_Position, float2 uv : SV_Target0) : SV_Target0 {
return tex.Sample(smp, uv);
}
-29
View File
@@ -1,29 +0,0 @@
// Fullscreen triangle — no vertex buffer, no inputs.
// Draws a single triangle that covers the entire viewport.
// Reused for all blit / composit passes.
struct VsOut {
float4 position : SV_Position;
float2 uv : SV_Target0;
};
VsOut main(uint vertex_id : SV_VertexID) {
// Generate fullscreen triangle from vertex ID.
// vertex_id 0 → (-1,-1), 1 → (-1,3), 2 → (3,-1)
// UV flips Y so image top maps to screen top.
float2 positions[3] = {
float2(-1.0, -1.0),
float2(-1.0, 3.0),
float2( 3.0, -1.0)
};
float2 uvs[3] = {
float2(0.0, 0.0),
float2(0.0, 2.0),
float2(2.0, 0.0)
};
VsOut output;
output.position = float4(positions[vertex_id], 0.0, 1.0);
output.uv = uvs[vertex_id];
return output;
}
-40
View File
@@ -1,40 +0,0 @@
// BLUR node — Gaussian blur with configurable radius.
// 1 input texture, push constant: f32 radius.
[[vk::binding(0, 0)]]
Texture2D<float4> input_texture : register(t0);
[[vk::binding(1, 0)]]
SamplerState input_sampler : register(s0);
struct PushConstants {
float radius;
};
[[vk::push_constant]]
PushConstants pc;
struct VSOutput {
float4 position : SV_Position;
float2 uv : TEXCOORD0;
};
float4 main(VSOutput input) : SV_Target {
uint width, height;
input_texture.GetDimensions(width, height);
float2 texel_size = 1.0 / float2(width, height);
float4 result = float4(0.0, 0.0, 0.0, 0.0);
int radius = int(pc.radius);
float weight_sum = 0.0;
for (int x = -radius; x <= radius; ++x) {
for (int y = -radius; y <= radius; ++y) {
float2 offset = float2(x, y) * texel_size;
float weight = 1.0 / (1.0 + float(x * x + y * y));
result += input_texture.Sample(input_sampler, input.uv + offset) * weight;
weight_sum += weight;
}
}
return result / weight_sum;
}
-31
View File
@@ -1,31 +0,0 @@
// GRADE node — colour grading with gain, offset, power.
// 1 input texture, push constants: f32 gain, f32 offset, f32 power.
[[vk::binding(0, 0)]]
Texture2D<float4> input_texture : register(t0);
[[vk::binding(1, 0)]]
SamplerState input_sampler : register(s0);
struct PushConstants {
float gain;
float offset;
float power;
};
[[vk::push_constant]]
PushConstants pc;
struct VSOutput {
float4 position : SV_Position;
float2 uv : TEXCOORD0;
};
float4 main(VSOutput input) : SV_Target {
float4 color = input_texture.Sample(input_sampler, input.uv);
// Apply gain, offset, power per channel
color.rgb = color.rgb * pc.gain + pc.offset;
color.rgb = pow(max(color.rgb, float3(0.0, 0.0, 0.0)), pc.power);
return color;
}
-16
View File
@@ -1,16 +0,0 @@
// READ node — samples from a persistent KTX texture.
// No push constants, no inputs (texture loaded separately).
[[vk::binding(0, 0)]]
Texture2D<float4> input_texture : register(t0);
[[vk::binding(1, 0)]]
SamplerState input_sampler : register(s0);
struct VSOutput {
float4 position : SV_Position;
float2 uv : TEXCOORD0;
};
float4 main(VSOutput input) : SV_Target {
return input_texture.Sample(input_sampler, input.uv);
}