First pass at nodes
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@ build
|
||||
compile_commands.json
|
||||
.vscode
|
||||
*.dSYM
|
||||
src/vendor/ktx/build
|
||||
assets/shaders
|
||||
scratchpad/**
|
||||
!scratchpad/**/
|
||||
!scratchpad/**/*.h
|
||||
|
||||
@@ -94,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);
|
||||
|
||||
@@ -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.
|
||||
@@ -5,6 +5,7 @@ default: build
|
||||
|
||||
CC := "clang"
|
||||
CXX := "clang++"
|
||||
SLANGC := "slangc"
|
||||
BUILDDIR := "build"
|
||||
|
||||
# Resolve VULKAN_SDK once via backtick
|
||||
@@ -30,8 +31,19 @@ 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
|
||||
build: vendor shaders
|
||||
mkdir -p {{BUILDDIR}}/bin
|
||||
bear -- {{CXX}} -g -c -Wno-nullability-completeness {{VK_FLAGS}} \
|
||||
src/prism/rhi/vulkan/profiles/vulkan_profiles.cpp \
|
||||
@@ -43,6 +55,11 @@ build: vendor
|
||||
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 \
|
||||
@@ -50,7 +67,7 @@ build: vendor
|
||||
bear -a -- {{CXX}} -g {{VK_FLAGS}} \
|
||||
-L{{VK_SDK}}/lib -L{{VENDOR_LIB}} \
|
||||
build/*.o \
|
||||
-lSDL3 -lglm -ltinyobjloader -lktx -lslang -lvulkan \
|
||||
-lSDL3 -lktx -lvulkan \
|
||||
-Wl,-rpath,{{VENDOR_LIB}} -Wl,-rpath,{{VK_SDK}}/lib \
|
||||
-o {{BUILDDIR}}/bin/prism
|
||||
@echo "--- build done: {{BUILDDIR}}/bin/prism ---"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// vim:fileencoding=utf-8:foldmethod=marker
|
||||
|
||||
#include "../src/wapp/wapp.h"
|
||||
#include "../src/vendor/wapp/wapp.h"
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
+356
-627
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,212 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// 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
|
||||
@@ -0,0 +1,358 @@
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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
|
||||
@@ -0,0 +1,101 @@
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#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
|
||||
@@ -162,6 +162,8 @@ PrRhiDescriptorPool *prRhiCreateDescriptorPool(PrRhiDevice *device,
|
||||
PrRhiDescriptorPoolDesc desc);
|
||||
void prRhiDestroyDescriptorPool(PrRhiDevice *device,
|
||||
PrRhiDescriptorPool *pool);
|
||||
void prRhiResetDescriptorPool(PrRhiDevice *device,
|
||||
PrRhiDescriptorPool *pool);
|
||||
|
||||
// ======================================================================
|
||||
// Descriptor sets
|
||||
|
||||
@@ -1399,27 +1399,33 @@ PrRhiPipelineLayout *prRhiCreatePipelineLayoutVk(PrRhiDevice *device,
|
||||
u32 layout_count = (desc.set_layouts && wpArrayCount(desc.set_layouts) > 0)
|
||||
? (u32)wpArrayCount(desc.set_layouts) : 0;
|
||||
|
||||
VkDescriptorSetLayoutArray vk_layouts =
|
||||
wpArrayAllocCapacity(VkDescriptorSetLayout, &_G_RHI_CONTEXT.allocator, layout_count,
|
||||
WP_ARRAY_INIT_FILLED);
|
||||
if (!vk_layouts) { _abort("alloc failed for VkDescriptorSetLayout array"); }
|
||||
VkDescriptorSetLayoutArray vk_layouts = NULL;
|
||||
if (layout_count > 0) {
|
||||
vk_layouts =
|
||||
wpArrayAllocCapacity(VkDescriptorSetLayout, &_G_RHI_CONTEXT.allocator, layout_count,
|
||||
WP_ARRAY_INIT_FILLED);
|
||||
if (!vk_layouts) { _abort("alloc failed for VkDescriptorSetLayout array"); }
|
||||
|
||||
for (u32 i = 0; i < layout_count; ++i) {
|
||||
vk_layouts[i] = desc.set_layouts[i]->handle;
|
||||
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 =
|
||||
wpArrayAllocCapacity(VkPushConstantRange, &_G_RHI_CONTEXT.allocator, pc_count, WP_ARRAY_INIT_NONE);
|
||||
if (!pc_ranges) { _abort("alloc failed for VkPushConstantRange array"); }
|
||||
VkPushConstantRangeArray pc_ranges = NULL;
|
||||
if (pc_count > 0) {
|
||||
pc_ranges =
|
||||
wpArrayAllocCapacity(VkPushConstantRange, &_G_RHI_CONTEXT.allocator, pc_count, WP_ARRAY_INIT_NONE);
|
||||
if (!pc_ranges) { _abort("alloc failed for VkPushConstantRange array"); }
|
||||
|
||||
for (u32 i = 0; i < pc_count; ++i) {
|
||||
pc_ranges[i].stageFlags = _toVkShaderStage(desc.push_constant_ranges[i].stage_flags);
|
||||
pc_ranges[i].offset = desc.push_constant_ranges[i].offset;
|
||||
pc_ranges[i].size = desc.push_constant_ranges[i].size;
|
||||
for (u32 i = 0; i < pc_count; ++i) {
|
||||
pc_ranges[i].stageFlags = _toVkShaderStage(desc.push_constant_ranges[i].stage_flags);
|
||||
pc_ranges[i].offset = desc.push_constant_ranges[i].offset;
|
||||
pc_ranges[i].size = desc.push_constant_ranges[i].size;
|
||||
}
|
||||
}
|
||||
|
||||
VkPipelineLayoutCreateInfo pl_info = {
|
||||
@@ -1433,8 +1439,8 @@ PrRhiPipelineLayout *prRhiCreatePipelineLayoutVk(PrRhiDevice *device,
|
||||
VkPipelineLayout vk_layout = VK_NULL_HANDLE;
|
||||
_checkVk(vkCreatePipelineLayout(vk_device, &pl_info, NULL, &vk_layout), "vkCreatePipelineLayout");
|
||||
|
||||
wpArrayDealloc(VkDescriptorSetLayout, &_G_RHI_CONTEXT.allocator, &vk_layouts);
|
||||
wpArrayDealloc(VkPushConstantRange, &_G_RHI_CONTEXT.allocator, &pc_ranges);
|
||||
if (vk_layouts) { wpArrayDealloc(VkDescriptorSetLayout, &_G_RHI_CONTEXT.allocator, &vk_layouts); }
|
||||
if (pc_ranges) { wpArrayDealloc(VkPushConstantRange, &_G_RHI_CONTEXT.allocator, &pc_ranges); }
|
||||
|
||||
PrRhiPipelineLayout *layout = wpMemAllocatorAlloc(&_G_RHI_CONTEXT.allocator, sizeof(PrRhiPipelineLayout));
|
||||
if (!layout) { _abort("alloc failed for PrRhiPipelineLayout"); }
|
||||
@@ -1758,6 +1764,10 @@ 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
|
||||
// ============================================================================
|
||||
|
||||
@@ -193,6 +193,8 @@ PrRhiDescriptorPool *prRhiCreateDescriptorPoolVk(PrRhiDevice *device,
|
||||
PrRhiDescriptorPoolDesc desc);
|
||||
void prRhiDestroyDescriptorPoolVk(PrRhiDevice *device,
|
||||
PrRhiDescriptorPool *pool);
|
||||
void prRhiResetDescriptorPoolVk(PrRhiDevice *device,
|
||||
PrRhiDescriptorPool *pool);
|
||||
|
||||
PrRhiDescriptorSet *prRhiAllocateDescriptorSetVk(PrRhiDevice *device,
|
||||
PrRhiDescriptorPool *pool,
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
#define prRhiDestroyDescriptorSetLayout prRhiDestroyDescriptorSetLayoutVk
|
||||
#define prRhiCreateDescriptorPool prRhiCreateDescriptorPoolVk
|
||||
#define prRhiDestroyDescriptorPool prRhiDestroyDescriptorPoolVk
|
||||
#define prRhiResetDescriptorPool prRhiResetDescriptorPoolVk
|
||||
#define prRhiAllocateDescriptorSet prRhiAllocateDescriptorSetVk
|
||||
#define prRhiFreeDescriptorSet prRhiFreeDescriptorSetVk
|
||||
#define prRhiUpdateDescriptorSet prRhiUpdateDescriptorSetVk
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user