Files
agent_compositor_test/AGENTS.md
T
2026-08-08 20:25:25 +01:00

282 lines
10 KiB
Markdown

# 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**: `justfile` (Just) as a task runner
- **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
- **Tabs for indentation**, 8-column tab width.
- **Braces** on the same line as control statements (Attach style).
- **Braces on single-line if statements**: Always use braces, even for single-line bodies:
```c
// correct
if (!buffer) { return; }
if (!texture) { _abort("alloc failed"); }
// wrong
if (!buffer) return;
if (!texture) _abort("alloc failed");
```
- **Pointers**: `*` against the name, not the type (`PrRhiBuffer *buf`, not `PrRhiBuffer* buf`).
- **Line width**: 120 columns.
- **Continuation lines** align to the opening parenthesis.
- **Return-type alignment**: Within each `// =====` section, align function
declaration names so the first letter of every function occupies the same
column. For pointer return types, place `*` directly against the function
name (no space) and put all alignment padding between the type name and `*`.
```c
// correct — * against fn name, padding before *
PrRhiSwapchain *prRhiCreateSwapchain(…);
void prRhiDestroySwapchain(…);
PrRhiSwapchainResult prRhiAcquireNextImage(…);
```
### 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.
**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);
```
### 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.
### Frame-by-frame command batching
Commands that execute every frame must avoid arena allocation. Use stack
arrays with a while-loop to batch operations in fixed-size chunks:
```c
while (count > 0) {
VkTypeArray batch = wpArrayWithCapacity(VkType, 16, WP_ARRAY_INIT_FILLED);
u32 batch_size = count < wpArrayCapacity(batch) ? count : (u32)wpArrayCapacity(batch);
// ... process batch ...
vkCmd*(cb->handle, ...);
count -= batch_size;
first += batch_size;
}
```
This avoids per-frame arena churn while handling arbitrarily large inputs.
### 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`).
Typedef pattern:
- Opaque handles use `**` (pointer-to-pointer)
- Value types use `*` (contiguous block)
```c
typedef PrRhiBuffer **PrRhiBufferArray; // opaque handles → **
typedef PrRhiColorAttachment *PrRhiColorAttachmentArray; // value types → *
```
Group opaque handle arrays first, value type arrays second, separated by a
blank line.
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/
│ └── <topic>.md
└── session-logs/
└── YYYY-MM-DD.md
```
## Skills
Domain-specific conventions are stored as skills in `.opencode/skills/<name>/SKILL.md`.
These are loaded on-demand by the AI agent when a task matches their description,
keeping AGENTS.md lean.
- **`prism-rhi`** — RHI backend dispatch, by-value descs, file layout
- **`prism-dag`** — DAG adjacency lists, arena allocation, Kahn's algorithm
## Workflows for AI agents
### Research / planning
1. Save findings to `documents/research/<topic>.md`.
2. When asked for a plan, write it to `documents/<PLAN_NAME>.md` and present
a summary; iterate on the plan before writing any code.
3. Only start implementing after the plan is approved.
### Skill maintenance
When you observe the user correcting your output (e.g. formatting, conventions),
infer the rule and add it to the relevant skill's `SKILL.md`. If no skill
matches, add a new one. This keeps AGENTS.md focused on project identity and
critical workflow rules rather than accumulating domain details.
### 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.