# AGENTS.md — Prism ## Project Identity **Prism** is a node-based image compositing tool. Images flow through a directed acyclic graph (DAG) of processing nodes (read, blend, colour-grade, etc.) and are rendered via GPU shaders. - **Language**: C11 / C++11 (dual-mode, like `src/wapp/`) - **GPU API**: Vulkan, abstracted behind a Rendering Hardware Interface (RHI) - **Shading language**: Slang, stored in external `.slang` files under `src/shaders/` - **Build**: TBD — either a standalone shell build script or a `justfile` (Just) - **Dependencies**: `src/wapp/` (local utility library, already vendored) ## Coding Conventions All code follows the patterns established in `src/wapp/`. The project prefix is `pr` (functions/macros) / `Pr` (types). ### Naming | Category | Convention | Examples | |-----------------------|------------------------|---------------------------------| | Types | `Pr` + PascalCase | `PrNode`, `PrGraph`, `PrImage` | | Functions | `pr` + PascalCase | `prGraphCreate`, `prNodeEval` | | Macros | `pr` + PascalCase | `prArrayOf`, `prNodeType` | | Enum constants | `PR_` + SCREAMING_SNAKE| `PR_NODE_BLEND`, `PR_OK` | | Internal/static funcs | `_` + camelCase | `_resolveTopology`, `_execCmd` | | File-scope globals | `_` + camelCase | `_default_allocator` | | File-scope constants | SCREAMING_SNAKE | `MAX_NODE_NAME` | | Variables | snake_case | `vertex_count`, `edge_node` | - **Variable names**: Prefer descriptive names (e.g. `vertex_count` over `vc`, `edge_node` over `en`). Short names (`i`, `j`, `n`) are acceptable only in tight loop counters or trivial index variables. Avoid single-letter names in non-trivial scopes, and avoid abbreviated names that require the reader to hold a mental dictionary. ### Formatting - **Indentation**: tabs (no spaces). Tab width is a viewer preference. - **Braces**: always required after `if`, `else`, `for`, `while`, `do` — even when the body is a single statement. This avoids ambiguity and makes diffs cleaner. ```c // correct if (condition) { do_thing(); } for (int i = 0; i < n; i++) { process(i); } // wrong — no braces, spaces instead of tabs if (condition) do_thing(); ``` ### Storage qualifiers Use the `wp_extern` / `wp_intern` / `wp_persist` aliases from wapp: ```c wp_extern void prGraphInit(PrGraph *g); // extern linkage wp_intern void _helper(void); // static (file-scope) wp_persist u32 _counter; // static (file-scope, mutable) ``` ### Structs POD structs with typedef, following `wapp/base/` patterns. Functions operate on structs by pointer rather than through vtables: ```c typedef struct PrNode PrNode; struct PrNode { PrNodeType type; u32 input_count; PrNode **inputs; // ... }; void prNodeInit(PrNode *n, PrAllocator *alloc); void prNodeEval(PrNode *n, PrRhiCmdList *cl); void prNodeDestroy(PrNode *n, PrAllocator *alloc); ``` ### Memory management Use wapp allocators (`WpAllocator`, arena-based). Stack-allocate where possible; pass allocators explicitly. ```c PrGraph *prGraphCreate(PrAllocator *alloc); void prGraphDestroy(PrGraph *g, PrAllocator *alloc); ``` ### File organisation One `.h` / `.c` pair per module, grouped in subdirectories by subsystem. A top-level umbrella header re-exports everything (see `src/wapp/wapp.h`). ``` src/prism/prism.h ← umbrella include src/prism/core/ ← graph, node types, DAG src/prism/rhi/ ← rendering hardware interface src/prism/node/ ← node implementations src/prism/app/ ← entry point, window, main loop ``` Shaders live in a flat-ish `src/shaders/` tree: ``` src/shaders/blit.slang src/shaders/blend.slang src/shaders/common/ ← shared include files ``` ### Data-oriented design For hot paths (per-frame graph evaluation, image transfers, shader dispatch) prefer SoA layouts, batch processing, and minimise pointer chasing. Keep the DAG in contiguous arrays (e.g. adjacency lists packed in flat buffers) rather than individually allocated linked structures. ### Graph / adjacency lists Adjacency list nodes must be **separately allocated from the vertex array**. Never use the vertex struct itself as a linked-list node in another vertex's adjacency chain — that shares the `next` pointer between two roles (vertex state vs. edge linking) and causes edges to splice into unintended chains. ```c // correct — per-edge copy on the arena static void addEdge(PrGraph *g, const WpAllocator *alloc, u64 from, u64 to) { PrVertex *src = &g->vertices[from]; PrVertex *dst = wpMemAllocatorAlloc(alloc, sizeof(PrVertex)); if (!dst) { /* handle OOM */ return; } dst->id = g->vertices[to].id; dst->value = g->vertices[to].value; dst->next = src->next; src->next = dst; } // wrong — reuses the destination vertex as the list node static void addEdge_bad(PrGraph *g, u64 from, u64 to) { PrVertex *src = &g->vertices[from]; PrVertex *dest = &g->vertices[to]; dest->next = src->next; src->next = dest; // overwrites dest->next used elsewhere } ``` Arena bump allocation (`wpMemAllocatorAlloc`) is the natural fit for building graph structures: per-edge nodes live in the arena and are freed in one shot when the arena is destroyed. Always NULL-check the result of `wpMemAllocatorAlloc` — even arena allocators can fail if the backing buffer is exhausted. ### WpArray usage | Scenario | API | Size must be… | |----------|-----|---------------| | Stack (local, short-lived) | `wpArrayWithCapacity` | compile-time constant | | Heap (arena-backed) | `wpArrayAllocCapacity` | runtime value | `wpArrayWithCapacity` creates a VLA-like compound literal on the stack — passing a runtime variable triggers undefined behaviour and compiler warnings. Use `wpArrayAllocCapacity` with an arena allocator for runtime sizes. Always use typed array aliases (`WpU64Array`, `PrNodeIdArray`, etc.) rather than raw pointers when declaring array variables. Follow the existing typedef pattern in the module (`typedef Type *TypeArray`). Use named init flags (`WP_ARRAY_INIT_NONE`, `WP_ARRAY_INIT_FILLED`) instead of bare `0` — they make the initialisation policy explicit. - `WP_ARRAY_INIT_FILLED`: sets `count = capacity` on allocation. Required when you plan to index into the array directly (not via append/push), since the array's `count` must reflect valid elements for any downstream use. - `WP_ARRAY_INIT_NONE`: leaves `count = 0`. Use when you'll fill the array incrementally via `wpArrayAppendCapped` / `wpArrayAppendAlloc`. Use `wpArrayCapacity(arr)`, `wpArrayCount(arr)`, `wpArraySetCount(arr, n)` to query and control array state rather than computing sizes manually. ### Local/scratch arenas For function-local scratch allocations, use `wpMemArenaAllocatorInitZero` with a fixed size rather than a stack buffer + `InitWithBuffer`: ```c WpAllocator scratch = wpMemArenaAllocatorInitZero(KiB(16)); ``` ## Documentation Save research notes and implementation plans as markdown in `documents/`: ``` documents/ ├── ARCHITECTURE.md ├── RENDERING_HARDWARE_INTERFACE.md ├── NODE_SYSTEM.md ├── ROADMAP.md └── research/ └── vulkan-baseline.md ``` ## Workflows for AI agents ### Research / planning 1. Save findings to `documents/research/.md`. 2. When asked for a plan, write it to `documents/.md` and present a summary; iterate on the plan before writing any code. 3. Only start implementing after the plan is approved. ### README Keep `README.md` in sync with the project as it evolves. Update it when: - The directory layout changes meaningfully - Language, toolchain, or build system decisions are settled - Dependencies are added or removed - The project reaches a notable milestone ### Learning from edits The user may edit code produced by AI agents. When this happens, infer the reason for the change and update AGENTS.md with any new conventions, patterns, or constraints that the edit reveals. This keeps the guide aligned with the user's evolving preferences. ### Committing Only commit when explicitly asked. When asked: - Stage only intended files. - Write a short, conventional commit message in present tense. - Never amend, force-push, or create PRs without a request. ## Session Log — 2026-07-05 ### Completed - **DAG refactoring** (`scratchpad/dag.c`): Decoupled PrGraph from PrNodeManager. Graph tracks its own active vertices (`vertex_count`, `max_vertex_ever`, `b8 active`). `prGraphAddEdge`, `prGraphTopologicalSort` no longer take `PrNodeManager*`. Kahn's algorithm integrated into `prGraphAddEdge` for cycle detection with rollback. Output verified matching baseline. - **RHI research document** written to `documents/research/rendering-hardware-interface.md`. Covers 8 reference RHIs, architecture patterns, core API surface, Vulkan-specific considerations, Slang integration, and proposed directory layout / lifecycle design. - Added NVRHI to `documents/resources.md`. ### Key Decisions - Graph API is self-contained — no graphics or node-manager dependencies. - RHI will use object-based + command-list-oriented architecture with vtbl dispatch. - Object lifecycle: `prRhiCreate*` / `prRhiDestroy*` with explicit `WpAllocator*`. - All GPU state explicit (no hidden pipeline state). - Per-frame command pools and descriptor pools, arena-allocated scratch. - `#version-macro` convention removed from Slang files; `__slang` define used instead. ### Next Steps 1. User reviews RHI research document, then iterate if needed. 2. Begin implementing RHI layer: types header, device creation, Vulkan backend skeleton. 3. Implement Slang shader compilation + reflection integration. 4. Wire up per-frame lifecycle (command pool reset, descriptor pool reset, scratch reset).