From f5ff6c70ea58f15759408760b99cdf3df5d6fdd4 Mon Sep 17 00:00:00 2001 From: Abdelrahman Date: Sat, 8 Aug 2026 20:22:37 +0100 Subject: [PATCH] Fixes and research --- .gitignore | 2 +- AGENTS.md | 6 + documents/TEXTURE_POOL_AND_NODE_EVAL.md | 554 +++++++++++++++++++++++ scratchpad/dag.c | 2 +- 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 + 8 files changed, 592 insertions(+), 17 deletions(-) create mode 100644 documents/TEXTURE_POOL_AND_NODE_EVAL.md 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/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/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