From 618f09689d5381c31e9fcd9dda8477a494d9b604 Mon Sep 17 00:00:00 2001 From: Abdelrahman Date: Mon, 27 Jul 2026 06:45:39 +0100 Subject: [PATCH] First pass at nodes --- .gitignore | 2 +- AGENTS.md | 6 + documents/TEXTURE_POOL_AND_NODE_EVAL.md | 554 +++++++++++++ justfile | 21 +- scratchpad/dag.c | 2 +- src/main.cpp | 983 ++++++++--------------- src/prism/core/pr_graph.c | 212 +++++ src/prism/core/pr_graph.h | 63 ++ src/prism/core/pr_node.c | 99 +++ src/prism/core/pr_node.h | 93 +++ src/prism/core/pr_node_eval.c | 358 +++++++++ src/prism/core/pr_node_eval.h | 95 +++ src/prism/core/pr_texture_pool.c | 101 +++ src/prism/core/pr_texture_pool.h | 38 + src/prism/rhi/pr_rhi.h | 2 + src/prism/rhi/vulkan/pr_rhi_vk.c | 40 +- src/prism/rhi/vulkan/pr_rhi_vk.h | 2 + src/prism/rhi/vulkan/pr_rhi_vk_aliases.h | 1 + src/shaders/blend.frag.slang | 44 + src/shaders/blit.vert.slang | 18 + src/shaders/blit_to_swap.frag.slang | 10 + src/shaders/blit_to_swap.vert.slang | 29 + src/shaders/blur.frag.slang | 40 + src/shaders/grade.frag.slang | 31 + src/shaders/read.frag.slang | 16 + 25 files changed, 2214 insertions(+), 646 deletions(-) create mode 100644 documents/TEXTURE_POOL_AND_NODE_EVAL.md create mode 100644 src/prism/core/pr_graph.c create mode 100644 src/prism/core/pr_graph.h create mode 100644 src/prism/core/pr_node.c create mode 100644 src/prism/core/pr_node.h create mode 100644 src/prism/core/pr_node_eval.c create mode 100644 src/prism/core/pr_node_eval.h create mode 100644 src/prism/core/pr_texture_pool.c create mode 100644 src/prism/core/pr_texture_pool.h create mode 100644 src/shaders/blend.frag.slang create mode 100644 src/shaders/blit.vert.slang create mode 100644 src/shaders/blit_to_swap.frag.slang create mode 100644 src/shaders/blit_to_swap.vert.slang create mode 100644 src/shaders/blur.frag.slang create mode 100644 src/shaders/grade.frag.slang create mode 100644 src/shaders/read.frag.slang diff --git a/.gitignore b/.gitignore index 00b2e70..6a18340 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ build compile_commands.json .vscode *.dSYM -src/vendor/ktx/build +assets/shaders scratchpad/** !scratchpad/**/ !scratchpad/**/*.h diff --git a/AGENTS.md b/AGENTS.md index c1645ae..998596e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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); diff --git a/documents/TEXTURE_POOL_AND_NODE_EVAL.md b/documents/TEXTURE_POOL_AND_NODE_EVAL.md new file mode 100644 index 0000000..8a72e07 --- /dev/null +++ b/documents/TEXTURE_POOL_AND_NODE_EVAL.md @@ -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. diff --git a/justfile b/justfile index f9270a9..2241472 100644 --- a/justfile +++ b/justfile @@ -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 ---" diff --git a/scratchpad/dag.c b/scratchpad/dag.c index 5f8870d..6e0febd 100644 --- a/scratchpad/dag.c +++ b/scratchpad/dag.c @@ -1,6 +1,6 @@ // vim:fileencoding=utf-8:foldmethod=marker -#include "../src/wapp/wapp.h" +#include "../src/vendor/wapp/wapp.h" #include #include #include diff --git a/src/main.cpp b/src/main.cpp index 5c3d55e..de6acba 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,26 +1,23 @@ // vim:fileencoding=utf-8:foldmethod=marker // -// Prism port of how-to-vulkan's main.cpp — uses the RHI API instead of -// direct Vulkan calls. +// Prism compositor test app. +// Builds a small node graph (READ → GRADE), evaluates it each frame, +// and blits the result to the swapchain. #include "prism/rhi/pr_rhi_types.h" #include "prism/rhi/pr_rhi.h" -#include -#include -#include -#include -#include +#include "prism/core/pr_texture_pool.h" +#include "prism/core/pr_node.h" +#include "prism/core/pr_node_eval.h" #include #include #include #include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include // ============================================================================ // Exit codes @@ -29,705 +26,437 @@ enum ExitCode { EXIT_CODE_SUCCESS, EXIT_CODE_SDL_INIT_FAILED, - EXIT_CODE_VULKAN_LIB_LOAD_FAILED, EXIT_CODE_WINDOW_CREATION_FAILED, - EXIT_CODE_SURFACE_CREATION_FAILED, EXIT_CODE_GET_WINDOW_SIZE_FAILED, - EXIT_CODE_NO_INSTANCE_SUPPORT, EXIT_CODE_NO_PHYSICAL_DEVICES, - EXIT_CODE_ALLOCATION_FAILURE, EXIT_CODE_NO_SUITABLE_PHYSICAL_DEVICE, - EXIT_CODE_NO_PRESENTATION_SUPPORT, - EXIT_CODE_NO_SUITABLE_DEPTH_FORMAT, - EXIT_CODE_MESH_LOAD_FAILED, - EXIT_CODE_SYNC_OBJ_CREATE_FAILED, }; static inline void check(bool result, i32 code) { if (!result) { - std::cerr << "Call returned an error\n"; + fprintf(stderr, "fatal error (code %d)\n", code); exit(code); } } // ============================================================================ -// Types +// Blit-to-swapchain state // ============================================================================ -struct Vertex { - glm::vec3 pos; - glm::vec3 normal; - glm::vec2 uv; +struct BlitResources { + PrRhiShader *vertex_shader; + PrRhiShader *fragment_shader; + PrRhiDescriptorSetLayout *desc_set_layout; + PrRhiPipelineLayout *pipeline_layout; + PrRhiPipeline *pipeline; + PrRhiDescriptorPool *desc_pool; }; -struct ShaderData { - glm::mat4 projection; - glm::mat4 view; - glm::mat4 model[3]; - glm::vec4 light_pos{ 0.0f, -10.0f, 10.0f, 0.0f }; - u32 selected{ 1 }; -}; +static void _blitInit(BlitResources *blit, PrRhiDevice *device, PrRhiFormat swapchain_format) { + // Load pre-compiled SPIR-V + auto loadSpirv = [](const char *path, u64 *out_size) -> void * { + FILE *f = fopen(path, "rb"); + if (!f) { fprintf(stderr, "cannot open %s\n", path); abort(); } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + void *buf = malloc((size_t)sz); + if (!buf) { fclose(f); abort(); } + size_t read = fread(buf, 1, (size_t)sz, f); + fclose(f); + if ((long)read != sz) { free(buf); abort(); } + *out_size = (u64)sz; + return buf; + }; -struct TextureResources { - PrRhiTexture *texture; - PrRhiSampler *sampler; -}; + u64 vert_size = 0, frag_size = 0; + void *vert_code = loadSpirv("assets/shaders/blit_to_swap.vert.spv", &vert_size); + void *frag_code = loadSpirv("assets/shaders/blit_to_swap.frag.spv", &frag_size); -// Typedefs for wapp arrays of our types -typedef Vertex *VertexArray; -typedef u16 *U16Array; -typedef glm::vec3 *GlmVec3Array; -typedef TextureResources *TextureResourcesArray; -typedef PrRhiBuffer **PrRhiBufferArray; -typedef PrRhiFence **PrRhiFenceArray; -typedef PrRhiSemaphore **PrRhiSemaphoreArray; -typedef PrRhiCommandBuffer **PrRhiCommandBufferArray; + blit->vertex_shader = prRhiCreateShader(device, (PrRhiShaderDesc){ + .spirv_code = vert_code, + .spirv_size = vert_size, + }); + blit->fragment_shader = prRhiCreateShader(device, (PrRhiShaderDesc){ + .spirv_code = frag_code, + .spirv_size = frag_size, + }); + free(vert_code); + free(frag_code); + + // Descriptor set layout: 1 combined image sampler at binding 0 + PrRhiDescriptorSetLayoutBindingArray blit_bindings = wpArray(PrRhiDescriptorSetLayoutBinding, + ((PrRhiDescriptorSetLayoutBinding){ + .type = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .descriptor_count = 1, + .stage_flags = PR_RHI_SHADER_STAGE_FRAGMENT, + .binding_flags = (PrRhiDescriptorBindingFlag)0, + })); + blit->desc_set_layout = prRhiCreateDescriptorSetLayout(device, (PrRhiDescriptorSetLayoutDesc){ + .bindings = blit_bindings, + }); + + // Pipeline layout (no push constants) + PrRhiDescriptorSetLayoutArray blit_set_layouts = wpArray(PrRhiDescriptorSetLayout *, blit->desc_set_layout); + blit->pipeline_layout = prRhiCreatePipelineLayout(device, (PrRhiPipelineLayoutDesc){ + .set_layouts = blit_set_layouts, + .push_constant_ranges = NULL, + }); + + // Graphics pipeline + PrRhiFormatArray blit_color_formats = wpArray(PrRhiFormat, swapchain_format); + PrRhiColorBlendAttachmentArray blit_blend_attachments = wpArray(PrRhiColorBlendAttachment, + ((PrRhiColorBlendAttachment){ .color_write_mask = 0xF })); + + blit->pipeline = prRhiCreateGraphicsPipeline(device, (PrRhiGraphicsPipelineDesc){ + .vertex_shader = blit->vertex_shader, + .vertex_shader_entry_point = "main", + .fragment_shader = blit->fragment_shader, + .fragment_shader_entry_point = "main", + .vertex_bindings = NULL, + .vertex_attributes = NULL, + .topology = PR_RHI_TOPOLOGY_TRIANGLE_LIST, + .color_attachment_formats = blit_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 = blit_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.0f, + .multisample_count = PR_RHI_SAMPLE_COUNT_1, + .layout = blit->pipeline_layout, + }); + + // Per-frame descriptor pool + PrRhiDescriptorPoolSizeArray blit_pool_sizes = wpArray(PrRhiDescriptorPoolSize, + ((PrRhiDescriptorPoolSize){ + .type = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .descriptor_count = 1, + })); + blit->desc_pool = prRhiCreateDescriptorPool(device, (PrRhiDescriptorPoolDesc){ + .max_sets = 1, + .pool_sizes = blit_pool_sizes, + }); +} + +static void _blitDestroy(BlitResources *blit, PrRhiDevice *device) { + prRhiDestroyDescriptorPool(device, blit->desc_pool); + prRhiDestroyPipeline(device, blit->pipeline); + prRhiDestroyPipelineLayout(device, blit->pipeline_layout); + prRhiDestroyDescriptorSetLayout(device, blit->desc_set_layout); + prRhiDestroyShader(device, blit->fragment_shader); + prRhiDestroyShader(device, blit->vertex_shader); +} // ============================================================================ -// Global state +// Blit-to-swapchain pass // ============================================================================ -struct AppState { - PrRhiInstance *inst; - PrRhiPhysicalDevice *pdev; - PrRhiDevice *device; - PrRhiSurface *surface; - PrRhiSwapchain *swapchain; +static void _blitToSwapchain(BlitResources *blit, PrRhiDevice *device, + PrTextureSlot *compositor_output, + PrRhiCommandBuffer *cb, PrRhiSwapchain *swapchain, + u32 image_index, PrRhiSampler *sampler, + i32 win_w, i32 win_h) { + // Transition compositor output: ATTACHMENT_OPTIMAL → SHADER_READ_ONLY_OPTIMAL + { + PrRhiImageMemoryBarrier barrier = { + .texture = compositor_output->texture, + .old_layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL, + .new_layout = PR_RHI_LAYOUT_READ_ONLY_OPTIMAL, + .src_stage_mask = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT, + .src_access_mask = PR_RHI_ACCESS_COLOR_ATTACHMENT_WRITE, + .dst_stage_mask = PR_RHI_PIPELINE_STAGE_FRAGMENT_SHADER, + .dst_access_mask = PR_RHI_ACCESS_SHADER_READ, + }; + prRhiCmdPipelineBarrier(cb, &barrier, NULL); + } - PrRhiFormat swapchain_format; + // Reset and allocate descriptor set + prRhiResetDescriptorPool(device, blit->desc_pool); + PrRhiDescriptorSet *desc_set = prRhiAllocateDescriptorSet( + device, blit->desc_pool, blit->desc_set_layout, NULL); - PrRhiBuffer *vert_index_buf; - u64 vertex_buf_size; - u64 index_count; + // Write descriptor: binding 0 = compositor output texture + PrRhiDescriptorImageInfo image_info = { + .texture = compositor_output->texture, + .sampler = sampler, + .layout = PR_RHI_LAYOUT_READ_ONLY_OPTIMAL, + }; + PrRhiWriteDescriptorSet write = { + .dst_set = desc_set, + .dst_binding = 0, + .dst_array_element = 0, + .type = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .image_info = &image_info, + .buffer_info = NULL, + }; + prRhiUpdateDescriptorSet(device, &write); - static constexpr u32 max_frames_in_flight = 2; - static constexpr u32 instance_count = 3; - static constexpr u32 texture_count = 3; + // Transition swapchain image to attachment optimal + PrRhiTexture *swap_tex = prRhiGetSwapchainTexture(swapchain, image_index); + { + PrRhiImageMemoryBarrier barrier = { + .texture = swap_tex, + .old_layout = PR_RHI_LAYOUT_UNDEFINED, + .new_layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL, + .src_stage_mask = PR_RHI_PIPELINE_STAGE_TOP_OF_PIPE, + .src_access_mask = PR_RHI_ACCESS_NONE, + .dst_stage_mask = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT, + .dst_access_mask = PR_RHI_ACCESS_COLOR_ATTACHMENT_WRITE, + }; + prRhiCmdPipelineBarrier(cb, &barrier, NULL); + } - PrRhiBufferArray shader_data_bufs; - PrRhiFenceArray fences; - PrRhiSemaphoreArray image_acquired_semaphores; - PrRhiSemaphoreArray render_completed_semaphores; - PrRhiCommandPool *cmd_pool; - PrRhiCommandBufferArray cmd_buffers; + // Render fullscreen triangle to swapchain + { + PrRhiColorAttachment color_att = { + .texture = swap_tex, + .layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL, + .clear = true, + .clear_color = {0.0f, 0.0f, 0.0f, 1.0f}, + }; + prRhiCmdBeginRendering(cb, &color_att, NULL); + } - TextureResourcesArray textures; - PrRhiDescriptorSetLayout *desc_set_layout; - PrRhiDescriptorPool *desc_pool; - PrRhiDescriptorSet *desc_set; + prRhiCmdBindPipeline(cb, PR_RHI_PIPELINE_BIND_POINT_GRAPHICS, blit->pipeline); - PrRhiShader *shader; - PrRhiPipelineLayout *pipeline_layout; - PrRhiPipeline *pipeline; + prRhiCmdSetViewport(cb, 0.0f, 0.0f, (f32)win_w, (f32)win_h); + prRhiCmdSetScissor(cb, 0, 0, (u32)win_w, (u32)win_h); - ShaderData shader_data; - u32 frame_index; - glm::ivec2 window_size; - GlmVec3Array object_rotations; - bool update_swapchain; + PrRhiDescriptorSet *sets[1] = { desc_set }; + prRhiCmdBindDescriptorSets(cb, PR_RHI_PIPELINE_BIND_POINT_GRAPHICS, + blit->pipeline_layout, 0, sets); - SDL_Window *window; - Slang::ComPtr slang_session; -}; + prRhiCmdDraw(cb, 3, 1, 0, 0); + prRhiCmdEndRendering(cb); + + // Transition to present + { + PrRhiImageMemoryBarrier barrier = { + .texture = swap_tex, + .old_layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL, + .new_layout = PR_RHI_LAYOUT_PRESENT_SRC, + .src_stage_mask = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT, + .src_access_mask = PR_RHI_ACCESS_COLOR_ATTACHMENT_WRITE, + .dst_stage_mask = PR_RHI_PIPELINE_STAGE_BOTTOM_OF_PIPE, + .dst_access_mask = PR_RHI_ACCESS_NONE, + }; + prRhiCmdPipelineBarrier(cb, &barrier, NULL); + } +} // ============================================================================ // Main // ============================================================================ int main() { - AppState app = {}; - WpAllocator arena = wpMemArenaAllocatorInitZero(MiB(128)); - - // {{{ Initialisation + // {{{ RHI init prRhiInit(); - - check(SDL_Init(SDL_INIT_VIDEO), EXIT_CODE_SDL_INIT_FAILED); + check(SDL_Init(SDL_INIT_VIDEO) != false, EXIT_CODE_SDL_INIT_FAILED); f32 display_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay()); - app.window = SDL_CreateWindow("How To Vulkan (Prism)", (i32)(display_scale * 1920), - (i32)(display_scale * 1080), - SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE); - check(app.window != nullptr, EXIT_CODE_WINDOW_CREATION_FAILED); - // }}} + SDL_Window *window = SDL_CreateWindow("Prism Compositor", + (i32)(display_scale * 1280), + (i32)(display_scale * 720), + SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE); + check(window != nullptr, EXIT_CODE_WINDOW_CREATION_FAILED); - // {{{ Instance creation - app.inst = prRhiCreateInstance(PrRhiInstanceDesc{}); - // }}} + PrRhiInstance *inst = prRhiCreateInstance((PrRhiInstanceDesc){}); - // {{{ Physical device selection - PrRhiPhysicalDeviceArray pdevs = prRhiGetPhysicalDevices(app.inst); + // Physical device selection + PrRhiPhysicalDeviceArray pdevs = prRhiGetPhysicalDevices(inst); check(wpArrayCount(pdevs) > 0, EXIT_CODE_NO_PHYSICAL_DEVICES); - i32 selected = -1; for (u32 i = 0; i < wpArrayCount(pdevs); ++i) { PrRhiPhysicalDeviceProperties props = prRhiGetPhysicalDeviceProperties(pdevs[i]); - switch (props.device_type) { - case PR_RHI_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: - selected = (i32)i; - break; - case PR_RHI_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: - if (selected == -1) - selected = (i32)i; - break; - default: continue; + if (props.device_type == PR_RHI_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + selected = (i32)i; + break; + } + if (props.device_type == PR_RHI_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU && selected == -1) { + selected = (i32)i; } } check(selected != -1, EXIT_CODE_NO_SUITABLE_PHYSICAL_DEVICE); - app.pdev = pdevs[selected]; + PrRhiPhysicalDevice *pdev = pdevs[selected]; - // Print device info - WpStr8 dev_name = wpStr8Buf(512); - WpStr8 driver_info = wpStr8Buf(512); - prRhiGetPhysicalDeviceName(app.pdev, &dev_name); - prRhiGetPhysicalDeviceDriverInfo(app.pdev, &driver_info); - std::cout << "Selected GPU: " << std::string_view((const char *)dev_name.buf, dev_name.size) << '\n' - << "Driver version: " << std::string_view((const char *)driver_info.buf, driver_info.size) << '\n'; + i32 win_w = 0, win_h = 0; + check(SDL_GetWindowSize(window, &win_w, &win_h), EXIT_CODE_GET_WINDOW_SIZE_FAILED); + PrRhiSurface *surface = prRhiCreateSurfaceFromWindow(inst, window); + + PrRhiDevice *device = prRhiCreateDevice(pdev, surface, (PrRhiDeviceDesc){ + .present_mode = PR_RHI_PRESENT_MODE_FIFO, + }); + + PrRhiSwapchain *swapchain = prRhiCreateSwapchain(device, (PrRhiSwapchainDesc){ + .surface = surface, + .width = (u32)win_w, + .height = (u32)win_h, + .has_depth = false, + }); + PrRhiFormat swapchain_format = prRhiGetSwapchainFormat(swapchain); + u32 swapchain_image_count = prRhiGetSwapchainImageCount(swapchain); // }}} - // {{{ Surface creation - check(SDL_GetWindowSize(app.window, &app.window_size.x, &app.window_size.y), - EXIT_CODE_GET_WINDOW_SIZE_FAILED); - app.surface = prRhiCreateSurfaceFromWindow(app.inst, app.window); + // {{{ Sync objects + PrRhiFence *fence = prRhiCreateFence(device, (PrRhiFenceDesc){ .signaled = true }); + PrRhiSemaphore *image_acquired = prRhiCreateSemaphore(device); + PrRhiSemaphore *render_completed = prRhiCreateSemaphore(device); + + PrRhiCommandPool *cmd_pool = prRhiCreateCommandPool(device); + PrRhiCommandBufferArray cmd_buffers = prRhiAllocateCommandBuffers(device, cmd_pool, 1); // }}} - // {{{ Device creation - PrRhiDeviceDesc dev_desc = {}; - dev_desc.present_mode = PR_RHI_PRESENT_MODE_FIFO; + // {{{ Compositor init + PrTexturePool pool; + prTexturePoolInit(&pool, 8, 32, (u32)win_w, (u32)win_h, + &_G_RHI_CONTEXT.allocator); - app.device = prRhiCreateDevice(app.pdev, app.surface, dev_desc); + PrNodeManager mgr; + prNodeManagerInit(&mgr, &_G_RHI_CONTEXT.allocator, 64); + + PrRhiSampler *shared_sampler = prRhiCreateSampler(device, (PrRhiSamplerDesc){ + .mag_filter = PR_RHI_FILTER_LINEAR, + .min_filter = PR_RHI_FILTER_LINEAR, + .mipmap_mode = PR_RHI_MIPMAP_MODE_LINEAR, + .address_mode_u = PR_RHI_ADDRESS_MODE_CLAMP_TO_EDGE, + .address_mode_v = PR_RHI_ADDRESS_MODE_CLAMP_TO_EDGE, + .address_mode_w = PR_RHI_ADDRESS_MODE_CLAMP_TO_EDGE, + .max_anisotropy = 1.0f, + .min_lod = 0.0f, + .max_lod = PR_RHI_LOD_CLAMP_NONE, + }); + + prNodeEvalInit(device, swapchain_format); + + // {{{ Build example graph: READ → GRADE + PrNodeId read_node = prNodeManagerAddNode(&mgr, PR_NODE_TYPE_READ); + PrNodeId grade_node = prNodeManagerAddNode(&mgr, PR_NODE_TYPE_GRADE); + prNodeManagerAddEdge(&mgr, read_node, grade_node); + + // Load KTX texture for the READ node + PrRhiTexture *read_texture = prRhiCreateTextureFromKtx(device, "assets/suzanne0.ktx", + cmd_pool, cmd_buffers[0]); + mgr.nodes[read_node.index].texture = read_texture; // }}} - // {{{ Swapchain creation - PrRhiSwapchainDesc swap_desc = {}; - swap_desc.surface = app.surface; - swap_desc.width = (u32)app.window_size.x; - swap_desc.height = (u32)app.window_size.y; - swap_desc.has_depth = true; - swap_desc.depth_format = PR_RHI_FORMAT_D24_UNORM_S8_UINT; - - app.swapchain = prRhiCreateSwapchain(app.device, swap_desc); - app.swapchain_format = prRhiGetSwapchainFormat(app.swapchain); - // }}} - - // {{{ Vertex/Index buffers - // {{{ Load mesh - tinyobj::attrib_t attrib; - std::vector shapes; - std::vector materials; - - check(tinyobj::LoadObj(&attrib, &shapes, &materials, nullptr, nullptr, "assets/suzanne.obj"), - EXIT_CODE_MESH_LOAD_FAILED); - - VertexArray vertices = wpArrayAllocCapacity(Vertex, &arena, 128, WP_ARRAY_INIT_NONE); - U16Array indices = wpArrayAllocCapacity(u16, &arena, 128, WP_ARRAY_INIT_NONE); - - for (auto &idx : shapes[0].mesh.indices) { - Vertex v = {}; - v.pos = { - attrib.vertices[idx.vertex_index * 3], - -attrib.vertices[idx.vertex_index * 3 + 1], - attrib.vertices[idx.vertex_index * 3 + 2] - }; - v.normal = { - attrib.normals[idx.normal_index * 3], - -attrib.normals[idx.normal_index * 3 + 1], - attrib.normals[idx.normal_index * 3 + 2] - }; - v.uv = { - attrib.texcoords[idx.texcoord_index * 2], - 1.0f - attrib.texcoords[idx.texcoord_index * 2 + 1] - }; - - u16 index = (u16)wpArrayCount(indices); - vertices = wpArrayAppendAlloc(Vertex, &arena, vertices, &v, WP_ARRAY_INIT_NONE); - indices = wpArrayAppendAlloc(u16, &arena, indices, &index, WP_ARRAY_INIT_NONE); - } - - app.vertex_buf_size = sizeof(Vertex) * wpArrayCount(vertices); - app.index_count = wpArrayCount(indices); - u64 index_buf_size = sizeof(u16) * app.index_count; - // }}} - - // {{{ Create GPU buffer - PrRhiBufferDesc vert_desc = {}; - vert_desc.size = app.vertex_buf_size + index_buf_size; - vert_desc.usage = (PrRhiBufferUsage)(PR_RHI_BUFFER_USAGE_VERTEX | PR_RHI_BUFFER_USAGE_INDEX); - vert_desc.memory = PR_RHI_MEMORY_CPU_TO_GPU; - - app.vert_index_buf = prRhiCreateBuffer(app.device, vert_desc); - - void *mapped = prRhiBufferMap(app.device, app.vert_index_buf); - memcpy(mapped, vertices, app.vertex_buf_size); - memcpy((u8 *)mapped + app.vertex_buf_size, indices, index_buf_size); - prRhiBufferUnmap(app.device, app.vert_index_buf); - // }}} - // }}} - - // {{{ Shader data buffers - app.shader_data_bufs = wpArrayAllocCapacity(PrRhiBuffer *, &arena, - AppState::max_frames_in_flight, - WP_ARRAY_INIT_FILLED); - for (u32 i = 0; i < AppState::max_frames_in_flight; ++i) { - PrRhiBufferDesc buf_desc = {}; - buf_desc.size = sizeof(ShaderData); - buf_desc.usage = (PrRhiBufferUsage)(PR_RHI_BUFFER_USAGE_STORAGE | - PR_RHI_BUFFER_USAGE_SHADER_DEVICE_ADDRESS); - buf_desc.memory = PR_RHI_MEMORY_CPU_TO_GPU; - - app.shader_data_bufs[i] = prRhiCreateBuffer(app.device, buf_desc); - } - // }}} - - // {{{ Synchronisation objects - // Fences - app.fences = wpArrayAllocCapacity(PrRhiFence *, &arena, AppState::max_frames_in_flight, - WP_ARRAY_INIT_FILLED); - for (u32 i = 0; i < AppState::max_frames_in_flight; ++i) { - PrRhiFenceDesc fd = {}; - fd.signaled = true; - app.fences[i] = prRhiCreateFence(app.device, fd); - } - - // Image acquired semaphores (per frame) - app.image_acquired_semaphores = wpArrayAllocCapacity(PrRhiSemaphore *, &arena, - AppState::max_frames_in_flight, - WP_ARRAY_INIT_FILLED); - for (u32 i = 0; i < AppState::max_frames_in_flight; ++i) { - app.image_acquired_semaphores[i] = prRhiCreateSemaphore(app.device); - } - - // Render completed semaphores (per swapchain image) - u32 swapchain_image_count = prRhiGetSwapchainImageCount(app.swapchain); - app.render_completed_semaphores = wpArrayAllocCapacity(PrRhiSemaphore *, &arena, - swapchain_image_count, - WP_ARRAY_INIT_FILLED); - for (u32 i = 0; i < swapchain_image_count; ++i) { - app.render_completed_semaphores[i] = prRhiCreateSemaphore(app.device); - } - // }}} - - // {{{ Command pool and buffers - app.cmd_pool = prRhiCreateCommandPool(app.device); - app.cmd_buffers = prRhiAllocateCommandBuffers(app.device, app.cmd_pool, - AppState::max_frames_in_flight); - // }}} - - // {{{ Texture loading - app.textures = wpArrayAllocCapacity(TextureResources, &arena, AppState::texture_count, - WP_ARRAY_INIT_FILLED); - - PrRhiCommandBufferArray upload_cbs = prRhiAllocateCommandBuffers(app.device, app.cmd_pool, 1); - PrRhiCommandBuffer *upload_cb = upload_cbs[0]; - - for (u32 i = 0; i < AppState::texture_count; ++i) { - char buf[2048] = {}; - snprintf(buf, sizeof(buf), "assets/suzanne%u.ktx", i); - PrRhiTexture *tex = prRhiCreateTextureFromKtx(app.device, buf, app.cmd_pool, upload_cb); - - // Create sampler - PrRhiSamplerDesc samp_desc = {}; - samp_desc.mag_filter = PR_RHI_FILTER_LINEAR; - samp_desc.min_filter = PR_RHI_FILTER_LINEAR; - samp_desc.mipmap_mode = PR_RHI_MIPMAP_MODE_LINEAR; - samp_desc.max_anisotropy = 8.0f; - samp_desc.max_lod = PR_RHI_LOD_CLAMP_NONE; - - PrRhiSampler *sampler = prRhiCreateSampler(app.device, samp_desc); - - app.textures[i].texture = tex; - app.textures[i].sampler = sampler; - } - - prRhiFreeCommandBuffers(app.device, app.cmd_pool, upload_cbs); - // }}} - - // {{{ Descriptor set layout, pool, set - PrRhiDescriptorSetLayoutBindingArray ds_layouts = wpArray( - PrRhiDescriptorSetLayoutBinding, - PrRhiDescriptorSetLayoutBinding{ - PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - AppState::texture_count, - PR_RHI_SHADER_STAGE_FRAGMENT, - PR_RHI_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT, - } - ); - - PrRhiDescriptorSetLayoutDesc layout_desc = { ds_layouts }; - app.desc_set_layout = prRhiCreateDescriptorSetLayout(app.device, layout_desc); - - // Pool - PrRhiDescriptorPoolSize pool_size = {}; - pool_size.type = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - pool_size.descriptor_count = AppState::texture_count; - - PrRhiDescriptorPoolDesc ds_pool_desc = {}; - ds_pool_desc.max_sets = 1; - ds_pool_desc.pool_sizes = wpArray(PrRhiDescriptorPoolSize, pool_size); - - app.desc_pool = prRhiCreateDescriptorPool(app.device, ds_pool_desc); - - // Allocate descriptor set (variable count) - WpU32Array var_counts = wpArray(u32, AppState::texture_count); - app.desc_set = prRhiAllocateDescriptorSet(app.device, app.desc_pool, app.desc_set_layout, - var_counts); - - // Write descriptor set - PrRhiDescriptorImageInfoArray img_infos = wpArrayAllocCapacity(PrRhiDescriptorImageInfo, - &arena, - AppState::texture_count, - WP_ARRAY_INIT_NONE); - for (u32 i = 0; i < AppState::texture_count; ++i) { - PrRhiDescriptorImageInfo info = {}; - info.texture = app.textures[i].texture; - info.sampler = app.textures[i].sampler; - info.layout = PR_RHI_LAYOUT_READ_ONLY_OPTIMAL; - wpArrayAppendCapped(PrRhiDescriptorImageInfo, img_infos, &info); - } - - PrRhiWriteDescriptorSet write = {}; - write.dst_set = app.desc_set; - write.dst_binding = 0; - write.type = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - write.image_info = img_infos; - - PrRhiWriteDescriptorSetArray writes = wpArray(PrRhiWriteDescriptorSet, write); - prRhiUpdateDescriptorSet(app.device, writes); - // }}} - - // {{{ Shader compilation (Slang) - slang::createGlobalSession(app.slang_session.writeRef()); - - slang::TargetDesc target = {}; - target.format = SLANG_SPIRV; - target.profile = {app.slang_session->findProfile("spirv_1_4")}; - - Slang::ComPtr slang_session; - slang::SessionDesc session_desc = {}; - session_desc.targets = ⌖ - session_desc.targetCount = 1; - session_desc.defaultMatrixLayoutMode = SLANG_MATRIX_LAYOUT_COLUMN_MAJOR; - app.slang_session->createSession(session_desc, slang_session.writeRef()); - - Slang::ComPtr slang_module { - slang_session->loadModuleFromSource("triangle", "assets/shader.slang", nullptr, nullptr), - }; - Slang::ComPtr spirv; - slang_module->getTargetCode(0, spirv.writeRef()); - // }}} - - // {{{ Create shader module - PrRhiShaderDesc shader_desc = {}; - shader_desc.spirv_code = spirv->getBufferPointer(); - shader_desc.spirv_size = spirv->getBufferSize(); - - app.shader = prRhiCreateShader(app.device, shader_desc); - // }}} - - // {{{ Pipeline layout - PrRhiPushConstantRange pc_range = {}; - pc_range.stage_flags = PR_RHI_SHADER_STAGE_VERTEX; - pc_range.size = sizeof(u64); - - PrRhiDescriptorSetLayoutArray pl_layouts = wpArray(PrRhiDescriptorSetLayout *, app.desc_set_layout); - - PrRhiPipelineLayoutDesc pl_desc = {}; - pl_desc.set_layouts = pl_layouts; - pl_desc.push_constant_ranges = wpArray(PrRhiPushConstantRange, pc_range); - - app.pipeline_layout = prRhiCreatePipelineLayout(app.device, pl_desc); - // }}} - - // {{{ Graphics pipeline - PrRhiVertexInputBindingArray vertex_bindings = wpArray( - PrRhiVertexInputBinding, - PrRhiVertexInputBinding{ 0, sizeof(Vertex) } - ); - - PrRhiVertexAttributeArray vertex_attrs = wpArray( - PrRhiVertexAttribute, - PrRhiVertexAttribute{ 0, 0, PR_RHI_FORMAT_R32G32B32_SFLOAT, 0 }, - PrRhiVertexAttribute{ 1, 0, PR_RHI_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, normal) }, - PrRhiVertexAttribute{ 2, 0, PR_RHI_FORMAT_R32G32_SFLOAT, offsetof(Vertex, uv) } - ); - - PrRhiColorBlendAttachmentArray blend_attachments = wpArray( - PrRhiColorBlendAttachment, - PrRhiColorBlendAttachment{ 0xf } - ); - - PrRhiFormatArray color_fmt_array = wpArray(PrRhiFormat, app.swapchain_format); - - PrRhiGraphicsPipelineDesc pipe_desc = {}; - pipe_desc.vertex_shader = app.shader; - pipe_desc.vertex_shader_entry_point = "main"; - pipe_desc.fragment_shader = app.shader; - pipe_desc.fragment_shader_entry_point = "main"; - pipe_desc.vertex_bindings = vertex_bindings; - pipe_desc.vertex_attributes = vertex_attrs; - pipe_desc.topology = PR_RHI_TOPOLOGY_TRIANGLE_LIST; - pipe_desc.color_attachment_formats = color_fmt_array; - pipe_desc.depth_attachment_format = swap_desc.depth_format; - pipe_desc.depth_test_enable = true; - pipe_desc.depth_write_enable = true; - pipe_desc.depth_compare_op = PR_RHI_COMPARE_OP_LESS_OR_EQUAL; - pipe_desc.blend_attachments = blend_attachments; - pipe_desc.dynamic_viewport = true; - pipe_desc.dynamic_scissor = true; - pipe_desc.cull_mode = PR_RHI_CULL_MODE_BACK; - pipe_desc.front_face = PR_RHI_FRONT_FACE_COUNTER_CLOCKWISE; - pipe_desc.line_width = 1.0f; - pipe_desc.layout = app.pipeline_layout; - - app.pipeline = prRhiCreateGraphicsPipeline(app.device, pipe_desc); + // Blit-to-swapchain resources + BlitResources blit; + _blitInit(&blit, device, swapchain_format); // }}} // {{{ Render loop - app.object_rotations = wpArrayAllocCapacity(glm::vec3, &arena, AppState::instance_count, - WP_ARRAY_INIT_FILLED); - u64 last_time = SDL_GetTicks(); - SDL_Event event = {}; - app.frame_index = 0; + bool running = true; + u32 frame_index = 0; u32 image_index = 0; - while (true) { - // {{{ Wait on fence - PrRhiFence *wait_fence = app.fences[app.frame_index]; - prRhiWaitForFences(app.device, wpArray(PrRhiFence *, wait_fence), 1, true, UINT64_MAX); - prRhiResetFences(app.device, wpArray(PrRhiFence *, wait_fence), 1); - // }}} + while (running) { + // Wait for previous frame's GPU work + prRhiWaitForFences(device, wpArray(PrRhiFence *, fence), 1, true, UINT64_MAX); + prRhiResetFences(device, wpArray(PrRhiFence *, fence), 1); - // {{{ Acquire next image - PrRhiSwapchainResult acq = prRhiAcquireNextImage(app.device, app.swapchain, - app.image_acquired_semaphores[app.frame_index], - &image_index); + // Acquire swapchain image + PrRhiSwapchainResult acq = prRhiAcquireNextImage(device, swapchain, + image_acquired, &image_index); if (acq == PR_RHI_SWAPCHAIN_OUT_OF_DATE) { - app.update_swapchain = true; - } - // }}} - - if (app.update_swapchain) { - // Skip this frame — will recreate below - } else { - // {{{ Update shader data - app.shader_data.projection = glm::perspective(glm::radians(45.0f), - (f32)app.window_size.x / (f32)app.window_size.y, - 0.1f, 32.0f); - app.shader_data.view = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, -6.0f)); - for (i32 i = 0; i < (i32)AppState::instance_count; ++i) { - glm::vec3 instance_pos = glm::vec3((f32)(i - 1) * 3.0f, 0.0f, 0.0f); - app.shader_data.model[i] = glm::translate(glm::mat4(1.0f), instance_pos) * - glm::mat4_cast(glm::quat(app.object_rotations[i])); - } - - void *shader_data_ptr = prRhiBufferMap(app.device, app.shader_data_bufs[app.frame_index]); - memcpy(shader_data_ptr, &app.shader_data, sizeof(ShaderData)); - prRhiBufferUnmap(app.device, app.shader_data_bufs[app.frame_index]); - // }}} - - // {{{ Record command buffer - PrRhiCommandBuffer *cb = app.cmd_buffers[app.frame_index]; - prRhiResetCommandBuffer(cb); - prRhiBeginCommandBuffer(cb); - - // Transition images to attachment optimal - { - PrRhiImageMemoryBarrierArray barriers_arr = - wpArrayWithCapacity(PrRhiImageMemoryBarrier, 2, WP_ARRAY_INIT_FILLED); - - // Color attachment - PrRhiTexture *color_tex = prRhiGetSwapchainTexture(app.swapchain, image_index); - barriers_arr[0].texture = color_tex; - barriers_arr[0].old_layout = PR_RHI_LAYOUT_UNDEFINED; - barriers_arr[0].new_layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL; - barriers_arr[0].src_stage_mask = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT; - barriers_arr[0].src_access_mask = PR_RHI_ACCESS_NONE; - barriers_arr[0].dst_stage_mask = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT; - barriers_arr[0].dst_access_mask = (PrRhiAccess)(PR_RHI_ACCESS_COLOR_ATTACHMENT_READ | PR_RHI_ACCESS_COLOR_ATTACHMENT_WRITE); - - // Depth attachment - PrRhiTexture *depth_tex = prRhiGetSwapchainDepthTexture(app.swapchain); - barriers_arr[1].texture = depth_tex; - barriers_arr[1].old_layout = PR_RHI_LAYOUT_UNDEFINED; - barriers_arr[1].new_layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL; - barriers_arr[1].src_stage_mask = PR_RHI_PIPELINE_STAGE_LATE_FRAGMENT_TESTS; - barriers_arr[1].src_access_mask = PR_RHI_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE; - barriers_arr[1].dst_stage_mask = PR_RHI_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS; - barriers_arr[1].dst_access_mask = PR_RHI_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE; - - prRhiCmdPipelineBarrier(cb, barriers_arr, NULL); - } - - // Rendering - { - PrRhiColorAttachmentArray color_arr = - wpArrayWithCapacity(PrRhiColorAttachment, 1, WP_ARRAY_INIT_FILLED); - color_arr[0].texture = prRhiGetSwapchainTexture(app.swapchain, image_index); - color_arr[0].layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL; - color_arr[0].clear = true; - color_arr[0].clear_color[0] = 0.0f; - color_arr[0].clear_color[1] = 0.0f; - color_arr[0].clear_color[2] = 0.0f; - color_arr[0].clear_color[3] = 0.0f; - - PrRhiDepthAttachment depth_att = {}; - depth_att.texture = prRhiGetSwapchainDepthTexture(app.swapchain); - depth_att.layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL; - depth_att.clear = true; - depth_att.clear_depth = 1.0f; - - prRhiCmdBeginRendering(cb, color_arr, &depth_att); - } - - prRhiCmdSetViewport(cb, 0.0f, 0.0f, (f32)app.window_size.x, (f32)app.window_size.y); - prRhiCmdSetScissor(cb, 0, 0, (u32)app.window_size.x, (u32)app.window_size.y); - - prRhiCmdBindPipeline(cb, PR_RHI_PIPELINE_BIND_POINT_GRAPHICS, app.pipeline); - - PrRhiDescriptorSetArray sets = wpArray(PrRhiDescriptorSet *, app.desc_set); - prRhiCmdBindDescriptorSets(cb, PR_RHI_PIPELINE_BIND_POINT_GRAPHICS, - app.pipeline_layout, 0, sets); - - PrRhiBufferArray vert_buf_arr = wpArray(PrRhiBuffer *, app.vert_index_buf); - WpU64Array vert_offsets = wpArray(u64, 0); - prRhiCmdBindVertexBuffers(cb, 0, vert_buf_arr, vert_offsets); - prRhiCmdBindIndexBuffer(cb, app.vert_index_buf, app.vertex_buf_size, PR_RHI_INDEX_TYPE_UINT16); - - // Push shader data buffer device address - u64 buf_addr = prRhiGetBufferDeviceAddress(app.device, app.shader_data_bufs[app.frame_index]); - prRhiCmdPushConstants(cb, app.pipeline_layout, PR_RHI_SHADER_STAGE_VERTEX, 0, sizeof(u64), &buf_addr); - - prRhiCmdDrawIndexed(cb, (u32)app.index_count, AppState::instance_count, 0, 0, 0); - prRhiCmdEndRendering(cb); - - // Transition to present - { - PrRhiImageMemoryBarrierArray present_barriers = - wpArrayWithCapacity(PrRhiImageMemoryBarrier, 1, WP_ARRAY_INIT_FILLED); - present_barriers[0].texture = prRhiGetSwapchainTexture(app.swapchain, image_index); - present_barriers[0].old_layout = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL; - present_barriers[0].new_layout = PR_RHI_LAYOUT_PRESENT_SRC; - present_barriers[0].src_stage_mask = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT; - present_barriers[0].src_access_mask = PR_RHI_ACCESS_COLOR_ATTACHMENT_WRITE; - present_barriers[0].dst_stage_mask = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT; - present_barriers[0].dst_access_mask = PR_RHI_ACCESS_NONE; - - prRhiCmdPipelineBarrier(cb, present_barriers, NULL); - } - - prRhiEndCommandBuffer(cb); - // }}} - - // {{{ Submit - prRhiQueueSubmit(app.device, cb, app.image_acquired_semaphores[app.frame_index], - app.render_completed_semaphores[image_index], app.fences[app.frame_index]); - // }}} - - // {{{ Present - PrRhiSwapchainResult pres = prRhiPresent(app.device, app.swapchain, - app.render_completed_semaphores[image_index]); - if (pres == PR_RHI_SWAPCHAIN_OUT_OF_DATE) { - app.update_swapchain = true; - } - // }}} + prRhiDeviceWaitIdle(device); + i32 w = 0, h = 0; + SDL_GetWindowSize(window, &w, &h); + prRhiRecreateSwapchain(device, &swapchain, (u32)w, (u32)h); + swapchain_format = prRhiGetSwapchainFormat(swapchain); + // Recreate blit pipeline for new swapchain format + _blitDestroy(&blit, device); + _blitInit(&blit, device, swapchain_format); + continue; } - // {{{ Poll events - f32 elapsed_time = (SDL_GetTicks() - last_time) / 1000.0f; - last_time = SDL_GetTicks(); + // Record command buffer + PrRhiCommandBuffer *cb = cmd_buffers[0]; + prRhiResetCommandBuffer(cb); + prRhiBeginCommandBuffer(cb); + + // 1. Evaluate compositor graph (writes to pool texture, records into cb) + PrTextureSlot *compositor_output = NULL; + prGraphEvaluate(&mgr, device, &pool, cb, shared_sampler, &compositor_output); + + // 2. Blit compositor output to swapchain + if (compositor_output) { + _blitToSwapchain(&blit, device, compositor_output, + cb, swapchain, image_index, shared_sampler, + win_w, win_h); + } + + prRhiEndCommandBuffer(cb); + + // Submit + present + prRhiQueueSubmit(device, cb, image_acquired, render_completed, fence); + PrRhiSwapchainResult pres = prRhiPresent(device, swapchain, render_completed); + if (pres == PR_RHI_SWAPCHAIN_OUT_OF_DATE) { + prRhiDeviceWaitIdle(device); + i32 w = 0, h = 0; + SDL_GetWindowSize(window, &w, &h); + prRhiRecreateSwapchain(device, &swapchain, (u32)w, (u32)h); + swapchain_format = prRhiGetSwapchainFormat(swapchain); + _blitDestroy(&blit, device); + _blitInit(&blit, device, swapchain_format); + } + + // Poll events + SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_EVENT_QUIT: - app.update_swapchain = false; // signal exit — use update_swapchain flag - goto done; + running = false; + break; case SDL_EVENT_KEY_DOWN: - if (event.key.key == SDLK_ESCAPE) goto done; - if (event.key.key == SDLK_PLUS || event.key.key == SDLK_KP_PLUS || event.key.key == SDLK_EQUALS) { - app.shader_data.selected = (app.shader_data.selected < 2) ? app.shader_data.selected + 1 : 0; - } - if (event.key.key == SDLK_MINUS || event.key.key == SDLK_KP_MINUS) { - app.shader_data.selected = (app.shader_data.selected > 0) ? app.shader_data.selected - 1 : 2; - } - break; - case SDL_EVENT_MOUSE_MOTION: - if (event.button.button == SDL_BUTTON_LEFT) { - app.object_rotations[app.shader_data.selected].x -= (f32)event.motion.yrel * elapsed_time; - app.object_rotations[app.shader_data.selected].y += (f32)event.motion.xrel * elapsed_time; - } - break; - case SDL_EVENT_MOUSE_WHEEL: - // Camera position handled via shader_data update in loop + if (event.key.key == SDLK_ESCAPE) { running = false; } break; case SDL_EVENT_WINDOW_RESIZED: - check(SDL_GetWindowSize(app.window, &app.window_size.x, &app.window_size.y), - EXIT_CODE_GET_WINDOW_SIZE_FAILED); - app.update_swapchain = true; + prRhiDeviceWaitIdle(device); + SDL_GetWindowSize(window, &win_w, &win_h); + prRhiRecreateSwapchain(device, &swapchain, (u32)win_w, (u32)win_h); + swapchain_format = prRhiGetSwapchainFormat(swapchain); + prTexturePoolDestroy(&pool, device); + prTexturePoolInit(&pool, 8, 32, (u32)win_w, (u32)win_h, + &_G_RHI_CONTEXT.allocator); + _blitDestroy(&blit, device); + _blitInit(&blit, device, swapchain_format); break; } } - // }}} - // {{{ Swapchain recreate - if (app.update_swapchain) { - prRhiDeviceWaitIdle(app.device); - prRhiRecreateSwapchain(app.device, &app.swapchain, - (u32)app.window_size.x, (u32)app.window_size.y); - - // Re-create render completed semaphores for new image count - // TODO: proper cleanup — for now, just leak old ones - u32 new_count = 0; - prRhiAcquireNextImage(app.device, app.swapchain, NULL, &new_count); - app.render_completed_semaphores = wpArrayAllocCapacity(PrRhiSemaphore *, &arena, - new_count, WP_ARRAY_INIT_FILLED); - for (u32 i = 0; i < new_count; ++i) { - app.render_completed_semaphores[i] = prRhiCreateSemaphore(app.device); - } - - app.update_swapchain = false; - } - // }}} - - app.frame_index = (app.frame_index + 1) % AppState::max_frames_in_flight; + frame_index++; } - done: // }}} // {{{ Cleanup - prRhiDeviceWaitIdle(app.device); + prRhiDeviceWaitIdle(device); - prRhiDestroyPipeline(app.device, app.pipeline); - prRhiDestroyPipelineLayout(app.device, app.pipeline_layout); - prRhiDestroyShader(app.device, app.shader); - prRhiDestroyDescriptorPool(app.device, app.desc_pool); - prRhiDestroyDescriptorSetLayout(app.device, app.desc_set_layout); + _blitDestroy(&blit, device); + prNodeEvalDestroy(device); + prRhiDestroyTexture(device, read_texture); + prRhiDestroySampler(device, shared_sampler); + prTexturePoolDestroy(&pool, device); + prNodeManagerDestroy(&mgr); - for (u32 i = 0; i < AppState::texture_count; ++i) { - prRhiDestroySampler(app.device, app.textures[i].sampler); - prRhiDestroyTexture(app.device, app.textures[i].texture); - } + prRhiFreeCommandBuffers(device, cmd_pool, cmd_buffers); + prRhiDestroyCommandPool(device, cmd_pool); + prRhiDestroySemaphore(device, render_completed); + prRhiDestroySemaphore(device, image_acquired); + prRhiDestroyFence(device, fence); + prRhiDestroySwapchain(device, swapchain); + prRhiDestroyDevice(device); + prRhiDestroySurface(inst, surface); + prRhiDestroyInstance(inst); - prRhiFreeCommandBuffers(app.device, app.cmd_pool, app.cmd_buffers); - prRhiDestroyCommandPool(app.device, app.cmd_pool); - - for (u32 i = 0; i < swapchain_image_count; ++i) { - prRhiDestroySemaphore(app.device, app.render_completed_semaphores[i]); - } - for (u32 i = 0; i < AppState::max_frames_in_flight; ++i) { - prRhiDestroySemaphore(app.device, app.image_acquired_semaphores[i]); - prRhiDestroyFence(app.device, app.fences[i]); - prRhiDestroyBuffer(app.device, app.shader_data_bufs[i]); - } - prRhiDestroyBuffer(app.device, app.vert_index_buf); - - prRhiDestroySwapchain(app.device, app.swapchain); - prRhiDestroyDevice(app.device); - prRhiDestroySurface(app.inst, app.surface); - prRhiDestroyInstance(app.inst); - - SDL_DestroyWindow(app.window); + SDL_DestroyWindow(window); SDL_Quit(); - prRhiDestroy(); - wpMemArenaAllocatorDestroy(&arena); // }}} return EXIT_CODE_SUCCESS; diff --git a/src/prism/core/pr_graph.c b/src/prism/core/pr_graph.c new file mode 100644 index 0000000..89292b5 --- /dev/null +++ b/src/prism/core/pr_graph.c @@ -0,0 +1,212 @@ +// vim:fileencoding=utf-8:foldmethod=marker + +#include "pr_graph.h" +#include "../../vendor/wapp/wapp.h" +#include + +// --------------------------------------------------------------------------- +// 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; +} diff --git a/src/prism/core/pr_graph.h b/src/prism/core/pr_graph.h new file mode 100644 index 0000000..35c3a92 --- /dev/null +++ b/src/prism/core/pr_graph.h @@ -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 diff --git a/src/prism/core/pr_node.c b/src/prism/core/pr_node.c new file mode 100644 index 0000000..8b7cf99 --- /dev/null +++ b/src/prism/core/pr_node.c @@ -0,0 +1,99 @@ +// vim:fileencoding=utf-8:foldmethod=marker + +#include "pr_node.h" +#include "../../vendor/wapp/wapp.h" +#include + +// --------------------------------------------------------------------------- +// 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); +} diff --git a/src/prism/core/pr_node.h b/src/prism/core/pr_node.h new file mode 100644 index 0000000..3fb8f54 --- /dev/null +++ b/src/prism/core/pr_node.h @@ -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 diff --git a/src/prism/core/pr_node_eval.c b/src/prism/core/pr_node_eval.c new file mode 100644 index 0000000..10b2430 --- /dev/null +++ b/src/prism/core/pr_node_eval.c @@ -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 +#include +#include + +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]; + } +} diff --git a/src/prism/core/pr_node_eval.h b/src/prism/core/pr_node_eval.h new file mode 100644 index 0000000..2f69b72 --- /dev/null +++ b/src/prism/core/pr_node_eval.h @@ -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 diff --git a/src/prism/core/pr_texture_pool.c b/src/prism/core/pr_texture_pool.c new file mode 100644 index 0000000..26b995b --- /dev/null +++ b/src/prism/core/pr_texture_pool.c @@ -0,0 +1,101 @@ +#include "pr_texture_pool.h" +#include "../rhi/pr_rhi.h" +#include "../../vendor/wapp/wapp.h" +#include +#include +#include + +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; +} diff --git a/src/prism/core/pr_texture_pool.h b/src/prism/core/pr_texture_pool.h new file mode 100644 index 0000000..bb67943 --- /dev/null +++ b/src/prism/core/pr_texture_pool.h @@ -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 diff --git a/src/prism/rhi/pr_rhi.h b/src/prism/rhi/pr_rhi.h index ae4bcdd..064b91b 100644 --- a/src/prism/rhi/pr_rhi.h +++ b/src/prism/rhi/pr_rhi.h @@ -162,6 +162,8 @@ PrRhiDescriptorPool *prRhiCreateDescriptorPool(PrRhiDevice *device, PrRhiDescriptorPoolDesc desc); void prRhiDestroyDescriptorPool(PrRhiDevice *device, PrRhiDescriptorPool *pool); +void prRhiResetDescriptorPool(PrRhiDevice *device, + PrRhiDescriptorPool *pool); // ====================================================================== // Descriptor sets diff --git a/src/prism/rhi/vulkan/pr_rhi_vk.c b/src/prism/rhi/vulkan/pr_rhi_vk.c index 60b9cdd..8ef9e03 100644 --- a/src/prism/rhi/vulkan/pr_rhi_vk.c +++ b/src/prism/rhi/vulkan/pr_rhi_vk.c @@ -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 // ============================================================================ diff --git a/src/prism/rhi/vulkan/pr_rhi_vk.h b/src/prism/rhi/vulkan/pr_rhi_vk.h index b601723..361ca1b 100644 --- a/src/prism/rhi/vulkan/pr_rhi_vk.h +++ b/src/prism/rhi/vulkan/pr_rhi_vk.h @@ -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, diff --git a/src/prism/rhi/vulkan/pr_rhi_vk_aliases.h b/src/prism/rhi/vulkan/pr_rhi_vk_aliases.h index 4f0b38d..d43adda 100644 --- a/src/prism/rhi/vulkan/pr_rhi_vk_aliases.h +++ b/src/prism/rhi/vulkan/pr_rhi_vk_aliases.h @@ -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 diff --git a/src/shaders/blend.frag.slang b/src/shaders/blend.frag.slang new file mode 100644 index 0000000..d5aea10 --- /dev/null +++ b/src/shaders/blend.frag.slang @@ -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 background : register(t0); +[[vk::binding(1, 0)]] +Texture2D 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; +} diff --git a/src/shaders/blit.vert.slang b/src/shaders/blit.vert.slang new file mode 100644 index 0000000..77fd950 --- /dev/null +++ b/src/shaders/blit.vert.slang @@ -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; +} diff --git a/src/shaders/blit_to_swap.frag.slang b/src/shaders/blit_to_swap.frag.slang new file mode 100644 index 0000000..ef7e205 --- /dev/null +++ b/src/shaders/blit_to_swap.frag.slang @@ -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 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); +} diff --git a/src/shaders/blit_to_swap.vert.slang b/src/shaders/blit_to_swap.vert.slang new file mode 100644 index 0000000..7cca830 --- /dev/null +++ b/src/shaders/blit_to_swap.vert.slang @@ -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; +} diff --git a/src/shaders/blur.frag.slang b/src/shaders/blur.frag.slang new file mode 100644 index 0000000..d15ff20 --- /dev/null +++ b/src/shaders/blur.frag.slang @@ -0,0 +1,40 @@ +// BLUR node — Gaussian blur with configurable radius. +// 1 input texture, push constant: f32 radius. + +[[vk::binding(0, 0)]] +Texture2D 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; +} diff --git a/src/shaders/grade.frag.slang b/src/shaders/grade.frag.slang new file mode 100644 index 0000000..746106a --- /dev/null +++ b/src/shaders/grade.frag.slang @@ -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 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; +} diff --git a/src/shaders/read.frag.slang b/src/shaders/read.frag.slang new file mode 100644 index 0000000..534dd12 --- /dev/null +++ b/src/shaders/read.frag.slang @@ -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 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); +}