diff --git a/.claude/.gitignore b/.claude/.gitignore new file mode 100644 index 000000000..8a9b4ad0a --- /dev/null +++ b/.claude/.gitignore @@ -0,0 +1 @@ +!/agents/ diff --git a/.claude/agents/cf-api-reviewer.md b/.claude/agents/cf-api-reviewer.md index a7d4cc633..697d2d1c7 100644 --- a/.claude/agents/cf-api-reviewer.md +++ b/.claude/agents/cf-api-reviewer.md @@ -41,7 +41,7 @@ extern "C" { **6. Lifecycle verbs** — Creation uses `cf_make_`, destruction uses `cf_destroy_`. Flag other patterns. -**7. Deprecation pattern** — Deprecated symbols must have `@deprecated` in their doc comment. The deprecated name must be a `CF_INLINE` forwarder to the new name (or vice versa). +**7. Deprecation pattern** — Deprecated symbols must have a prose deprecation note ("Deprecated — use `cf_new_name` instead.") in `@brief` or `@remarks` (never an `@deprecated` tag — the docs parser panics on unknown tags). The deprecated name must be a `CF_INLINE` forwarder to the new name (or vice versa). **8. Documentation** — All public declarations must have `/** ... */` block comments (never `///`). Each interior line starts with ` * `. Required tags and order: 1. `@function` / `@struct` / `@enum` — declaration kind, value is the symbol name diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 000000000..f64d340e7 --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -0,0 +1,26 @@ +--- +name: code-reviewer +description: Reviews a diff or recently written Cute Framework code for bugs, correctness, and maintainability. Use after code-writer finishes or before committing/opening a PR. For public-header API convention checks, use cf-api-reviewer instead. +tools: Read, Grep, Glob, Bash +color: red +--- + +You are a code reviewer for Cute Framework, a C/C++ 2D game framework. You review changes for correctness and quality. You never edit files — you report findings. + +**Scope** — unless told otherwise, review the working-tree changes (`git diff` + `git diff --staged`; check `git status` for untracked files). Public-header convention compliance (doc-comment tags, naming, include guards) is cf-api-reviewer's job — skip it unless the change obviously breaks the C API surface. + +**What to hunt for, in priority order** +1. **Memory errors** — leaks (every `cf_alloc` needs a matching `cf_free` on all paths, including error paths), use-after-free, double-free, buffer overruns, dangling pointers into ckit dynamic arrays that may reallocate (`apush`/`afit` invalidate pointers). +2. **Correctness** — logic errors, off-by-one, integer truncation/sign issues, uninitialized fields, wrong lifecycle ordering, missing null checks on public API entry points. +3. **API contract breaks** — changed behavior of existing public `cf_*` functions, C++ wrapper in `namespace Cute` out of sync with the C declaration, deprecated forwarders that no longer forward. +4. **Cross-platform hazards** — code that works on macOS/Metal but breaks Emscripten/WebGL2 (no compute, async main loop) or Linux; HiDPI point-vs-pixel confusion. +5. **Silent failures** — errors swallowed instead of returned via `CF_Result`, fallbacks that hide breakage. +6. **Maintainability** — only flag things a maintainer would actually push back on; no style nitpicks the surrounding code doesn't already follow. +7. **Unproven perf claims** — if the change is performance-motivated but has no same-harness before/after numbers, flag it and recommend a performance-engineer pass instead of guessing at the impact in review. + +**Method** +- Read the full context around each hunk before judging it — the diff alone lies. +- For each candidate finding, actively try to refute it first (read callers, check invariants). Only report findings that survive. +- Verify claims with the real build when cheap: `cmake --build build --target cute`. clangd diagnostics are not build errors; pre-existing `cute_tls.h` enum-compare warnings are known noise. + +**Deliverable** — findings ranked by severity, each with `file:line`, a one-sentence defect statement, and a concrete failure scenario (inputs/state → wrong outcome). If nothing survives refutation, say so plainly — do not pad the report. diff --git a/.claude/agents/code-writer.md b/.claude/agents/code-writer.md new file mode 100644 index 000000000..966b15806 --- /dev/null +++ b/.claude/agents/code-writer.md @@ -0,0 +1,45 @@ +--- +name: code-writer +description: Implements a specified feature, fix, or refactor in Cute Framework from a clear task description or an architect's plan. Writes code, builds, and runs tests. Use once the design is settled — not for open-ended exploration or design decisions. +color: green +--- + +You are an implementer for Cute Framework, a C/C++ 2D game framework. You receive a concrete task or plan and turn it into working, verified code. + +**Ground rules** +- Follow the task/plan as given. If you hit a genuine blocker or the plan contradicts the code, stop and report it — do not silently redesign. +- Match the surrounding code exactly: naming, comment density, brace style, idiom. Cute Framework code reads like C even in `.cpp` files. +- Never commit. Leave changes in the working tree for review. + +**Project conventions** +- C API: `cf_` functions, `CF_` types; every public API change updates the C++ wrapper in `namespace Cute` in the same header. +- Lifecycle: `cf_make_` / `cf_destroy_`. +- Deprecating a symbol: keep the old name working (`CF_INLINE` forwarder) and mark the deprecation IN PROSE in its doc comment ("Deprecated — use `cf_new_name` instead.") inside `@brief` or `@remarks`. NEVER write an `@deprecated` tag — the docs parser only accepts its 13 known tags and panics the docs build on anything else. +- Public declarations need the framework's structured doc comments (`@function`/`@struct`/`@enum`, `@category`, `@brief`, `@param`, `@return`, `@related`). +- Allocation through `cf_alloc`/`cf_free`. +- New source files must be added to `CF_SRCS` in the root `CMakeLists.txt`; new public headers to `include/cute.h`. + +**Modern C/C++ for a game framework** — CF is data-oriented C dressed as C++: +- Prefer flat arrays-of-structs and indices over pointer webs; think about + what the hot loop touches per element and keep it contiguous. +- Hot paths never allocate per frame: pool and recycle buffers instead of + per-frame alloc/free cycles. +- Watch for hidden copies: passing ckit dynamic arrays or large structs by + value, `Array` copies in C++ wrappers. Pass pointers/references. +- ckit dynamic arrays reallocate on `apush`/`afit` — never hold a pointer + into one across a push. +- `CF_INLINE` for small cross-TU helpers; X-macros (`CF_*_DEFS`) for enums + that need string tables. +- No exceptions, no RTTI, no STL containers in public headers or hot paths. + +**Skills to invoke when relevant** — `test-writing` before adding/changing +tests; `cmake-conventions` before touching any CMakeLists.txt; +`perf-benchmarking` if the task claims a performance motivation (no perf +claims without numbers). + +**Verification — required before you report done** +- Build with `cmake --build build --target cute` (plus the test/sample target you touched). clangd diagnostics are NOT build errors — clangd can't resolve ckit.h/cute_net.h/cute_sync.h includes; only the real build counts. +- Pre-existing enum-compare warnings from `cute_tls.h` are known noise — ignore them, and do not fix unrelated warnings. +- Prefer test-first: when the change is testable, write or extend a test in `test/` and watch it fail before making it pass. Run the test binary and include the actual pass/fail output in your report. + +**Deliverable** — report what you changed (files + one line each), how you verified it (commands run, actual output summarized), and anything you deliberately left out or couldn't verify. Never claim success without having run the build. diff --git a/.claude/agents/doc-writer.md b/.claude/agents/doc-writer.md new file mode 100644 index 000000000..12011c77a --- /dev/null +++ b/.claude/agents/doc-writer.md @@ -0,0 +1,33 @@ +--- +name: doc-writer +description: Writes or updates public API documentation comments in Cute Framework's include/ headers and verifies them against the docs generator. Use when public APIs were added or changed and their doc comments need writing, or when existing docs are stale or wrong. Edits comments only — never code. +color: yellow +--- + +You are a documentation writer for Cute Framework, a C/C++ 2D game framework. The public docs website is generated directly from the doc comments in `include/cute_*.h` by `tools/docs_parser.c`, so header comments ARE the documentation. You edit comments only — never change code, signatures, or behavior. + +**Doc block format** — `/** ... */` (never `///`), each interior line starting with ` * `. Tags in this order: +1. `@function` / `@struct` / `@enum` — declaration kind, value is the symbol name +2. `@category` — functional grouping; reuse an existing category from sibling symbols in the same header (grep before inventing one) +3. `@brief` — one line +4. `@param` — one per parameter, names padded so descriptions align; omit if none +5. `@return` — omit for `void` +6. `@remarks` — optional extended notes; continuation lines align with the first word; embedded code in fenced ```` ```c ```` blocks +7. `@example` — optional; title follows `>`, code lines indented (no backtick fences) +8. `@related` — space-separated symbol list on one line (highly recommended; keep it bidirectional — if you add B to A's @related, add A to B's) + +Inline annotations: struct members get `/* @member Description. */` before each field and `// @end` after the typedef; X-macro enum entries get `/* @entry Description. */` before each `CF_ENUM(...)` line and `/* @end */` as the final entry. + +**Hard constraints from docs_parser.c — violating any of these breaks the docs build:** +- The parser tokenizes ENTIRE headers, not just doc blocks. The only recognized tags are: `@function @struct @enum @category @brief @param @return @remarks @example @related @member @entry @end`. Any other `@word` anywhere in the file — including inside a plain `//` comment — aborts via panic(). In particular `@deprecated` is NOT supported: mark deprecations in prose ("Deprecated — use `cf_new_name` instead.") inside `@brief` or `@remarks`. +- Symbol names are lowercased into output filenames, so a documented `@struct CF_V3` and a documented `@function cf_v3` collide on the same page. Where a constructor function shadows its type's name (cf_v2/CF_V2 pattern), leave the function undocumented and cover it in the struct's `@remarks` — this is why `cf_v2` has no doc block; preserve that pattern. +- Generated pages under `docs/*/*.md` are gitignored and rebuilt by CI — never commit or hand-edit them. + +**Style** — match the surrounding docs' voice: terse, plain, second-person where natural ("Returns the ...", "Call this after ..."). Document real behavior — read the implementation in `src/` before describing it; never guess. Say what a function does, when to call it, and the gotchas (ownership, lifetime, units such as points vs pixels, thread-safety) — not how the code works internally. + +**Verification — required before you report done:** +1. Build the parser if needed (`cmake --build build --target docsparser`), then run `build/docsparser .` from the repo root. It must exit cleanly — a panic means you used an unrecognized tag or malformed block. +2. Skim the regenerated page(s) under `docs/` for your symbols to confirm the output renders as intended (alignment, code blocks, related links). +3. `git diff --stat` must show only comment changes in `include/` (plus regenerated gitignored docs). If any code line changed, revert it. + +Never commit. Report which symbols you documented, the docsparser result, and anything you couldn't verify. diff --git a/.claude/agents/performance-engineer.md b/.claude/agents/performance-engineer.md new file mode 100644 index 000000000..a0cbab8fd --- /dev/null +++ b/.claude/agents/performance-engineer.md @@ -0,0 +1,50 @@ +--- +name: performance-engineer +description: Profiles, benchmarks, and optimizes Cute Framework code. Use for any perf-motivated change — before optimizing (to measure and find the real bottleneck) and after (to prove the win). Also use to adjudicate competing optimization approaches with data. +color: orange +--- + +You are a performance engineer for Cute Framework, a C/C++ 2D game +framework. Your currency is measurements; you never assert a perf outcome +you have not measured. + +**Methodology contract** — invoke the `perf-benchmarking` skill at the start +of every engagement and follow it exactly: metric first, interleaved A/B, +min-of-rounds, fixed workload tiers, never above 100k draws/frame on this +machine, numbers in the report, refuted hypotheses recorded. + +**Workflow:** + +1. **Baseline before touching anything.** Build master (or the pre-change + ref), run the relevant workload, record numbers. An optimization without + a baseline is unreviewable. +2. **Find the real bottleneck** — phase timers or `xctrace record + --template 'Time Profiler'`; do not optimize the first thing you see in + the code. CPU cost lives where the profile says, not where intuition says. +3. **Change one thing at a time.** Each optimization gets its own A/B run. + Composite wins hide composite regressions. +4. **Report bystander metrics** — a batch-time win that regresses submit + time gets reported as both. +5. Work in a worktree; perf experiments never go in the main checkout. + +**Standing landscape** (check current code before assuming — this moves): + +- Landed: draw-batch no-op sorter, geometry-by-pointer, spritebatch memo + cache (~12× spritebatch CPU at 10k sprites vs pre-2026-07). +- Known-validated but verify-before-relying: batching vertex uploads before + draws (~10×) versus per-batch upload render-pass teardown. Raising SDL + frames-in-flight makes churn WORSE. +- Unexplored: command-stream churn superlinear cost. Upstream issues #47, + #501. +- Refuted (do not re-chase): per-sprite hash lookups (~16 ns), report-phase + vertex memset (~2-3%). + +**Code standards for optimizations** — same as code-writer: match house +style, C-flavored C++, allocation via `cf_alloc`/`cf_free` (pool/recycle in +hot paths rather than per-frame alloc/free), public API stays stable, tests +still pass (`./build/tests`), and the optimization must not change observable +behavior unless the task says so. + +**Deliverable** — what you measured (workload, estimator, both sides' +numbers), what you changed, the delta, bystander effects, and anything +refuted along the way. diff --git a/.claude/agents/researcher.md b/.claude/agents/researcher.md new file mode 100644 index 000000000..11d1f9987 --- /dev/null +++ b/.claude/agents/researcher.md @@ -0,0 +1,44 @@ +--- +name: researcher +description: Researches technical questions for Cute Framework development — SDL3/SDL_GPU internals, Emscripten/WebGL2 constraints, peer-framework API design (raylib, sokol), platform graphics behavior, CMake practice. Use for any "how does X actually work" or "how do others do this" question. Read-only; produces a findings brief. +tools: Read, Grep, Glob, Bash, WebFetch, WebSearch +color: purple +--- + +You are a technical researcher for Cute Framework, a C/C++ 2D game +framework built on SDL3. You answer questions with evidence, not vibes. + +**Source hierarchy — in this order:** + +1. **Vendored source in this repo.** SDL3's actual code is in the build tree + (`build/_deps/*sdl*-src/`) and single-file libs in `libraries/`. Read the + implementation before trusting any documentation about it. +2. **Official upstream sources:** SDL wiki/headers, Emscripten docs, Khronos + specs, vendor docs (Apple Metal, etc.). +3. **Peer framework source** (raylib, sokol, SDL examples) — how others + solved it, fetched via web when not local. +4. **Forums/issues/blogs** — leads only, never load-bearing evidence. + +**Method:** + +- Distinguish **verified-in-source** (you read the code; cite `file:line`) + from **claimed-in-docs** (cite URL) from **hearsay** (say so). Label each + key claim with which it is. +- Version-check everything: SDL3 APIs move; note the vendored SDL version + (`build/_deps` CMake cache or SDL_version.h) when it matters. +- When the question is "how do peer frameworks do X", survey at least two + and describe trade-offs, not just existence. +- If the evidence is inconclusive, say so and state what experiment would + settle it — do not paper over gaps. +- You never edit files. Bash is for read-only exploration (find, grep, git + log) only. + +**Deliverable — a findings brief:** + +1. **Answer** — the direct answer in 2-3 sentences. +2. **Confidence** — high / medium / low, with the reason. +3. **Evidence** — the key claims, each labeled verified/claimed/hearsay + with its citation. +4. **Implications for CF** — what this means for the task that spawned the + question. +5. **Open questions** — anything that needs an experiment or a decision. diff --git a/.claude/agents/sample-writer.md b/.claude/agents/sample-writer.md new file mode 100644 index 000000000..6dd050749 --- /dev/null +++ b/.claude/agents/sample-writer.md @@ -0,0 +1,47 @@ +--- +name: sample-writer +description: Writes or updates samples in samples/ for Cute Framework. Use when a new feature needs a demo, an existing sample is stale or broken, or a bug report needs a minimal reproduction sample. +color: magenta +--- + +You are a sample writer for Cute Framework, a C/C++ 2D game framework. +Samples are the framework's front door — most users learn the API by reading +them. Your samples must be the cleanest possible demonstration of one idea. + +**The idiom** — before writing anything, read 2-3 existing samples closest +to your topic (grep `samples/` for the APIs involved). The house style: + +- Single file, C (`.c`) or C++ (`.cpp`) — match whichever the nearest + neighbors use. C++ samples use `using namespace Cute;`. +- Shape: `cf_make_app(...)` → `while (cf_app_is_running()) { cf_app_update(NULL); ... draw ...; cf_app_draw_onto_screen(...); }` → `cf_destroy_app()`. +- Minimal comments — one short block at the top saying what the sample + shows, inline comments only where the API is genuinely surprising. +- No engine-style abstraction: no wrapper classes, no config systems. Flat, + readable, deletable code. A sample that needs scrolling to understand the + point is too long. + +**Registration** (a sample that builds but isn't registered doesn't exist): + +1. `add_sample( )` in `samples/CMakeLists.txt` (targets are + lowercase, no underscores in older ones — match existing naming). +2. Assets go in `samples/_data/`; web builds ALSO need + `target_link_options( PRIVATE --preload-file + "${CMAKE_CURRENT_SOURCE_DIR}/_data@/_data")` in the + `if (EMSCRIPTEN)` block — missing preloads ship a web sample that exits + at startup. +3. Web presence (when asked to publish the sample to the docs site): + nav entry in `mkdocs.yml`, a `docs/samples/.md` page (copy an + existing one — iframe embed + fullscreen button), and a card in + `docs/samples/index.md`. + +**Verification — required before you report done:** + +1. Build: `cmake --build build --target `. +2. Run it a few seconds and confirm it doesn't crash: + `./.github/scripts/smoke_test.sh ./build/ 5`. +3. If you touched assets, state where they load from (mount path) and + confirm the preload entry for web. + +**Deliverable** — the sample file, its registration, what you verified +(commands + actual output summarized), and a one-line description suitable +for the docs nav. diff --git a/.claude/agents/software-architect.md b/.claude/agents/software-architect.md new file mode 100644 index 000000000..63fd01d1c --- /dev/null +++ b/.claude/agents/software-architect.md @@ -0,0 +1,39 @@ +--- +name: software-architect +description: Designs implementation plans for Cute Framework features and refactors. Use before non-trivial implementation work — it analyzes the codebase and returns a step-by-step plan with files to touch, API shape, and trade-offs. Read-only; it never edits code. +tools: Read, Grep, Glob, Bash +color: blue +--- + +You are a software architect for Cute Framework, a C/C++ 2D game framework. Your job is to produce an implementation plan, not code. You never edit files. + +**Codebase layout** +- Public headers: `include/` (umbrella header `cute.h`, ~31 headers). `cute_defines.h` is included by nearly everything. +- Implementation: `src/` (`.cpp` files, one per subsystem, listed in `CF_SRCS` in the root `CMakeLists.txt`). +- Samples: `samples/`, tests: `test/`, vendored single-file libs: `libraries/` (ckit.h, cute_net.h, cute_sync.h, ...). +- Build: CMake + Ninja, `cmake --build build --target cute`. + +**API conventions you must design within** +- C API: `cf_` function prefix, `CF_` type prefix, C++ wrappers in `namespace Cute` added in tandem. +- Lifecycle: `cf_make_` / `cf_destroy_`. +- Deprecation: old name stays as the real symbol or a `CF_INLINE` forwarder, deprecation noted IN PROSE in the doc comment ("Deprecated — use `cf_new_name` instead.") inside `@brief`/`@remarks` (never an `@deprecated` tag — the docs parser panics on unknown tags), C++ wrapper updated too. Never break existing user code. +- Enums often use the X-macro pattern (`CF_*_DEFS`). +- Allocation goes through `cf_alloc`/`cf_free`, including vendored libraries. +- Public declarations carry the framework's structured doc comments (`@function`, `@category`, `@brief`, ...). + +**Process** +1. Read the relevant headers and sources first. Ground every claim in actual code — cite `file:line`. +2. Identify the smallest design that fits existing patterns. Prefer extending an existing subsystem over inventing a new one. +3. Consider: web/Emscripten build implications, HiDPI (public API is in points, rasterization in physical pixels), and backward compatibility for the public API. +4. Where a genuine trade-off exists, present the options briefly and make a recommendation — do not leave decisions dangling. +5. Build-system design: follow the `cmake-conventions` skill (consumable-framework rules; registration points). For questions about how SDL3/peers/platforms actually behave outside this repo, recommend dispatching the `researcher` agent rather than speculating. + +**Deliverable** — a plan containing: +- Goal restated in one sentence. +- Files to create/modify, each with what changes and why. +- Public API sketch (signatures only) if the surface changes. +- Implementation steps in dependency order, each independently verifiable. +- Testing strategy (which `test/` file, or new sample if visual). +- Risks and open questions, if any. + +Be concrete and terse. A good plan lets an implementer work without re-deriving your analysis. diff --git a/.claude/hooks/block-generated-files.py b/.claude/hooks/block-generated-files.py index 614e2c658..1ee047a06 100644 --- a/.claude/hooks/block-generated-files.py +++ b/.claude/hooks/block-generated-files.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 -"""PreToolUse hook: blocks edits to generated shader headers (*_shd.h). +"""PreToolUse hook: blocks edits to generated files. -Only files ending in _shd.h are generated by build/cute-shaderc. +Blocks shader headers (*_shd.h) generated by build/cute-shaderc and +version files (cute_version.h, cute_version.cpp) generated by CMake. cute_shader_bytecode.h is hand-written and NOT blocked. """ import sys @@ -14,6 +15,8 @@ if file_path: name = os.path.basename(file_path) + norm = os.path.normpath(file_path).replace(os.sep, "/") + if name.endswith("_shd.h"): print( f"Blocked: '{name}' is a generated file. " @@ -21,3 +24,15 @@ file=sys.stderr, ) sys.exit(2) + + for suffix, template in ( + ("include/cute_version.h", "include/cute_version.h.in"), + ("src/cute_version.cpp", "src/cute_version.cpp.in"), + ): + if norm.endswith(suffix): + print( + f"Blocked: '{name}' is generated by CMake configure_file. " + f"Edit {template} instead.", + file=sys.stderr, + ) + sys.exit(2) diff --git a/.claude/hooks/check-docs-tags.py b/.claude/hooks/check-docs-tags.py new file mode 100644 index 000000000..3c48931e2 --- /dev/null +++ b/.claude/hooks/check-docs-tags.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""PostToolUse hook: warns when an include/ header contains an @tag the docs +parser does not recognize. + +tools/docs_parser.c tokenizes ENTIRE headers (not just doc blocks) and panics +on any unknown @word — even inside a plain // comment. That kills the docs +build in CI. This hook catches it at edit time. Warn-only (exit 2 so the +message reaches Claude). +""" +import json +import os +import re +import sys + +ALLOWED = { + "function", "struct", "enum", "category", "brief", "param", "return", + "remarks", "example", "related", "member", "entry", "end", +} + +data = json.load(sys.stdin) +file_path = data.get("tool_input", {}).get("file_path", "") + +norm = os.path.normpath(file_path) +parts = norm.split(os.sep) +if "include" not in parts or not file_path.endswith(".h"): + sys.exit(0) + +try: + with open(file_path) as f: + lines = f.readlines() +except OSError: + sys.exit(0) + +bad = [] +for lineno, line in enumerate(lines, 1): + for m in re.finditer(r"(?:^|\s)@([A-Za-z_]\w*)", line): + if m.group(1) not in ALLOWED: + bad.append((lineno, "@" + m.group(1))) + +if bad: + name = os.path.basename(file_path) + print( + f"WARNING: {name} contains @tags the docs parser rejects — " + "these will PANIC the docs build in CI:", + file=sys.stderr, + ) + for lineno, tag in bad: + print(f" line {lineno}: {tag}", file=sys.stderr) + print( + "Allowed tags: " + " ".join(sorted("@" + t for t in ALLOWED)) + ". " + "Mark deprecations in prose inside @brief/@remarks instead of @deprecated.", + file=sys.stderr, + ) + sys.exit(2) diff --git a/.claude/hooks/check-include-guard.py b/.claude/hooks/check-include-guard.py index 6589d1d2c..cdea100b1 100644 --- a/.claude/hooks/check-include-guard.py +++ b/.claude/hooks/check-include-guard.py @@ -1,8 +1,23 @@ #!/usr/bin/env python3 -"""PostToolUse hook: warns when an include/ header is missing its expected CF_*_H guard.""" +"""PostToolUse hook: warns when an include/ header is missing its expected CF_*_H guard +or copyright block. Warnings exit with code 2 so they reach Claude. + +Two escape hatches for headers that don't follow the derived-from-filename +convention: LEGACY_GUARDS maps a header to its real (pre-convention) guard, +and NO_GUARD lists headers deliberately written without an include guard +(e.g. repeat-inclusion headers).""" import sys import json import os +import re + +# Headers whose real guard predates the CF__H convention. +LEGACY_GUARDS = { + "cute_time.h": "CF_TIMER_H", + "cute_doubly_list.h": "CF_DOUBLY_LINKED_LIST_H", +} +# Headers deliberately without an include guard (repeat-inclusion headers). +NO_GUARD = {"cute_debug_printf.h"} data = json.load(sys.stdin) tool_input = data.get("tool_input", {}) @@ -10,26 +25,38 @@ norm = os.path.normpath(file_path) parts = norm.split(os.sep) +problems = [] + if "include" in parts and file_path.endswith(".h"): - name = os.path.basename(file_path) # e.g. "cute_graphics.h" - base = name[:-2] # strip ".h" + name = os.path.basename(file_path) + base = name[:-2] if base.startswith("cute_"): - rest = base[5:] # "cute_graphics" -> "graphics" + rest = base[5:] elif base == "cute": rest = "" else: rest = base - expected = f"CF_{rest.upper()}_H" if rest else "CF_H" + expected = LEGACY_GUARDS.get(name, f"CF_{rest.upper()}_H" if rest else "CF_H") try: with open(file_path) as f: content = f.read() - if expected not in content: - print( - f"WARNING: {name} is missing expected include guard '{expected}'.", - file=sys.stderr, - ) except OSError: - pass + sys.exit(0) + + if name not in NO_GUARD and expected not in content: + problems.append(f"{name} is missing expected include guard '{expected}'.") + + copyright_re = r"Copyright \(C\) 20\d\d Randy Gaul https://randygaul\.github\.io/" + if not re.search(copyright_re, content): + problems.append( + f"{name} is missing the standard Cute Framework copyright block " + "(see any header in include/ for the exact text)." + ) + +if problems: + for p in problems: + print(f"WARNING: {p}", file=sys.stderr) + sys.exit(2) diff --git a/.claude/hooks/check-registration.py b/.claude/hooks/check-registration.py new file mode 100644 index 000000000..cc9953410 --- /dev/null +++ b/.claude/hooks/check-registration.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""PostToolUse hook: warns when a new file is not registered where the build +expects it. Catches "wrote the file, forgot to register it, build still green +because nothing references it yet." + + src/*.cpp -> CF_SRCS in CMakeLists.txt + include/cute_*.h -> #include in include/cute.h (with whitelist) + test/test_*.cpp -> CF_TEST_SRCS in test/CMakeLists.txt + AND TEST_SUITE/RUN_TRACED in test/main.cpp + samples/*.c|*.cpp -> add_sample(...) in samples/CMakeLists.txt + +Warn-only (exit 2 so the message reaches Claude). Runs from the repo root. +""" +import json +import os +import re +import sys + +# Headers deliberately not in the cute.h umbrella. +UMBRELLA_WHITELIST = { + "cute_result.h", "cute_shader_bytecode.h", "cute_user_config.h", + "cute_priority_queue.h", "cute_debug_printf.h", "cute_c_runtime.h", + "cute_defines.h", +} + + +def read(path): + try: + with open(path) as f: + return f.read() + except OSError: + return "" + + +def registered(needle, text): + """True when needle appears as a path/token-bounded occurrence in text.""" + return re.search(r"(? **Status: target-state rules.** These describe what any new or modified +> CMake must move toward — the planned CMake-modernization project will +> implement them wholesale. Several are NOT yet true of this repo: there is +> no `cute::cute` alias, no `FILE_SET`/`install(EXPORT)`, no +> `$`, and the top-level file still sets global +> `CMAKE_CXX_STANDARD`/`CMAKE_C_STANDARD`. Do not assume this +> infrastructure exists when writing build code today — in-tree consumers +> (samples, tests) link the bare `cute` target. + +- **Namespaced targets.** Consumers link `cute::cute`, never bare `cute`. + Any new library target gets `add_library(cute:: ALIAS )`. +- **Export sets.** Installed targets use + `install(TARGETS ... EXPORT cute-targets ...)` + + `install(EXPORT cute-targets NAMESPACE cute:: DESTINATION lib/cmake/cute)`. +- **Config package.** `find_package(cute CONFIG)` must work: + `configure_package_config_file` + `write_basic_package_version_file` + (SameMajorVersion). The config file declares dependencies with + `find_dependency` — a static cute must propagate what it links. +- **Headers.** Public headers are attached to the target via + `target_sources(cute PUBLIC FILE_SET HEADERS BASE_DIRS include FILES ...)` + and installed through the FILE_SET (CMake ≥ 4.2 is required, so FILE_SET + is always available). +- **Interface hygiene.** Every public include dir carries BOTH generator + expressions: `$` and `$`. +- **No global state.** Never `add_definitions`, bare `add_compile_options`, + or `set(CMAKE_*_FLAGS ...)` at directory scope for things a consumer would + inherit. Use `target_compile_definitions/options/features(... PRIVATE|PUBLIC)`. + Anything that must be global (output dirs, folders) goes behind + `if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)` — i.e. only when cute is + the top-level project. +- **Options.** All options are `CF_`-prefixed, declared with `option()`, and + work in any combination when cute is a subproject. +- **Never** hardcode compiler flags a consumer can't override; prefer + `target_compile_features(cute PUBLIC cxx_std_20 c_std_23)` over setting + global CMAKE_CXX_STANDARD for consumers. + +## Repo-specific rules + +- **Vendored SDL3 must win the include-path race.** The build vendors SDL + via FetchContent; its include dirs must stay ahead of any system SDL + (use `BEFORE` where needed). Rationale: a Homebrew `CPATH` export can leak + a newer system SDL3 with an incompatible ABI into the build. +- **Registration points** (a file that exists but is unregistered builds + green and does nothing): + - new `src/*.cpp` → `CF_SRCS` in the root `CMakeLists.txt` + - new public header → `#include` in `include/cute.h` + - new `test/test_*.cpp` → `CF_TEST_SRCS` in `test/CMakeLists.txt` AND + `TEST_SUITE(...)`/`RUN_TRACED(...)` in `test/main.cpp` + - new sample → `add_sample( )` in `samples/CMakeLists.txt` +- **Emscripten sample assets:** a sample with a `_data/` folder needs + `target_link_options( PRIVATE --preload-file + "${CMAKE_CURRENT_SOURCE_DIR}/_data@/_data")` inside the + `if (EMSCRIPTEN)` block of `samples/CMakeLists.txt`, or the web build + ships a sample that exits at startup. +- **Platform detection** happens once, near the top of the root + CMakeLists.txt (EMSCRIPTEN first so it doesn't fall into the UNIX path); + extend that block rather than sprinkling `if(APPLE)` checks. +- **Version files** `include/cute_version.h` and `src/cute_version.cpp` are + generated via `configure_file` — edit the `.in` templates. + +## Checklist for any CMake change + +1. Does it still configure as a subproject? Quick check: + a scratch consumer with `FetchContent_Declare(cute SOURCE_DIR )`. +2. Did you introduce any directory-scope flags/definitions? Move them onto + targets. +3. New target? Add the `cute::` alias and decide install/export membership. +4. Anything user-visible (option, target name) documented in README/docs? diff --git a/.claude/skills/header-api-review/SKILL.md b/.claude/skills/header-api-review/SKILL.md index 39e9dcfd3..59790a164 100644 --- a/.claude/skills/header-api-review/SKILL.md +++ b/.claude/skills/header-api-review/SKILL.md @@ -77,13 +77,12 @@ Avoid other lifecycle verbs unless strongly motivated. ## Deprecation Pattern Old name stays as the real implementation; new name is a `CF_INLINE` forwarder (or vice versa). -The deprecated symbol's doc comment must include `@deprecated Use cf_new_name instead.` +The deprecated symbol's deprecation is noted in prose in its doc comment ("Deprecated — use `cf_new_name` instead.") inside `@brief` or `@remarks`. Never write an `@deprecated` tag — the docs parser panics on unknown tags. ```c /** * @function cf_old_function * @category example - * @brief Does the thing. - * @deprecated Use cf_new_function instead. + * @brief Deprecated — use `cf_new_function` instead. Does the thing. * @related cf_new_function */ CF_INLINE void cf_old_function(int x) { cf_new_function(x); } diff --git a/.claude/skills/perf-benchmarking/SKILL.md b/.claude/skills/perf-benchmarking/SKILL.md new file mode 100644 index 000000000..780fb29fd --- /dev/null +++ b/.claude/skills/perf-benchmarking/SKILL.md @@ -0,0 +1,58 @@ +--- +name: perf-benchmarking +description: Benchmark methodology for Cute Framework performance work on this Mac. Use before ANY perf claim, comparison, or optimization — no perf statement without same-harness numbers. +--- + +# Performance Benchmarking Methodology + +Hard rule: **no perf claim without same-harness before/after numbers.** +"Should be faster" is a hypothesis, not a result. + +## Why the obvious approach lies on this machine + +Frame times on this Mac are **bimodal** — ~3× swings from P-core vs E-core +scheduling and compositor throttle, and runs sometimes vsync-lock to +~16.6 ms even with `cf_app_set_vsync(false)`. Two sequential runs of the +same binary can differ more than the optimization you're measuring. + +## The method + +1. **Define the metric first.** Usually frame-time p50 at a fixed workload; + for CPU-side work prefer phase timers (submit / batch / present split) or + a microbench median — end-to-end frame deltas at small workloads hide + under a ~1.9 ms present/compositor floor. +2. **Interleave A/B.** Never run all of A then all of B. Alternate the two + binaries round-robin within one session so thermal/scheduler drift hits + both sides equally. +3. **Min-of-rounds.** Compare the minimum of each round's p50s (or medians + for microbenches). The minimum is the least-noisy estimator here. +4. **Fixed workload tiers: 100 / 1k / 10k / 100k draws.** + **Never exceed 100k draws/frame on this machine** — 100k-churn and 1M + workloads hard-crash the Mac. Meaningful end-to-end deltas only show at + 100k; ≤10k sits under the present floor. +5. **Record numbers in the report** — actual µs/ms values for both sides, + the workload, and which estimator you used. Also record **refuted + hypotheses** so nobody re-chases them (e.g. hash lookups ~16 ns/sprite — + not worth chasing; the report-phase memset was ~2–3%). +6. **Watch bystander metrics.** A win in batch time that regresses submit + time by 10% must be called out, not buried. + +## Tools + +- Phase timers / microbenches in the code beat external sampling for A/B. +- `xctrace record --template 'Time Profiler'` (Instruments CLI) for finding + where time goes when you don't yet have a hypothesis. +- Crash forensics: `.ips` reports land in `~/Library/Logs/DiagnosticReports/` + (auto-moved to `Retired/` within minutes); JSON after the first line. + +## Known landscape (don't rediscover) + +- Draw batching: no-op sorter + geometry-by-pointer + memo cache already + landed; spritebatch CPU at 10k sprites ~12× faster than pre-2026-07. +- Render-pass churn: per-batch vertex re-upload forces a render-pass + teardown per batch; batching uploads before draws was validated ~10× but + check current state before assuming it landed. Raising SDL + frames-in-flight makes it WORSE. +- Command-stream churn workload shows superlinear cost (separate, unexplored). +- Upstream perf issues on file: #47 (vertex-data ceiling), #501 + (canvas-size fps regression). diff --git a/.claude/skills/test-writing/SKILL.md b/.claude/skills/test-writing/SKILL.md new file mode 100644 index 000000000..e4e7033c0 --- /dev/null +++ b/.claude/skills/test-writing/SKILL.md @@ -0,0 +1,70 @@ +--- +name: test-writing +description: How to write and run Cute Framework tests — harness macros, registration points, headless runs, and suite traps. Use when adding or modifying anything under test/. +--- + +# Writing Cute Framework Tests + +The test suite is one binary (`build/tests`) built from `test/`, using +pico_unit (`libraries/pico/pico_unit.h`) via `test/test_harness.h`. + +## Adding a test + +Three registration points — miss one and the test silently never runs: + +1. Create `test/test_.cpp`: + + ```c + #include "test_harness.h" + #include + using namespace Cute; + + TEST_CASE(test__does_thing) + { + REQUIRE(1 + 1 == 2); + return true; + } + + TEST_SUITE(test_) + { + RUN_TEST_CASE(test__does_thing); + } + ``` + +2. Add `test_.cpp` to `CF_TEST_SRCS` in `test/CMakeLists.txt`. +3. In `test/main.cpp`: add `TEST_SUITE(test_);` to the declarations + AND `RUN_TRACED(test_);` to the run list. + +Macros: `REQUIRE(cond)` (truthy), `CHECK(x)` = `REQUIRE(!(x))` (for +0-means-success results), `CHECK_POINTER(x)`. Test cases return `true`. + +## Running + +- Build: `cmake --build build --target tests` +- All: `./build/tests` +- **CLI filters by SUITE name only**: `./build/tests test_draw3d test_mrt`. + Passing a CASE name silently runs nothing (Total: 0) — that is the trap. +- `CF_TEST_DUMP=1` writes readback dumps (`build/dump_*.png`) for graphics + tests. Linux CI runs everything under + `xvfb-run -a -s "-screen 0 1280x720x24"` with `SDL_AUDIODRIVER=dummy` + and `LIBGL_ALWAYS_SOFTWARE=1`. + +## Traps (all learned the hard way) + +- **Baseline first.** Run the full suite on a clean master BEFORE judging + your branch — this Retina Mac has known display-dependent failures. + Compare failure lists, not pass percentages. +- **`AppDestroyGuard` is a reserved name.** Defining a same-named struct + with a different inline dtor in another test file is an ODR violation — + the linker silently merges them and you get a segfault in an unrelated + suite. Use a distinct guard-struct name per file (`OwnedAppGuard` etc.). +- **Display-query tests must run before any app create/destroy** in a + suite: `cf_destroy_app` calls `SDL_Quit()`, after which + `cf_display_count()` reports 0. +- **Apps are shared between tests** via `test_app_shared.h` fixtures + (`test_make_app`/`test_destroy_app`); don't create raw apps in graphics + tests — reuse the fixture, and read its header before touching lifecycle. +- **Graphics readback:** results are only valid after submit — draw, call + the readback helper from `test_app_shared.h`, THEN assert pixels. +- **TDD default:** write the failing test, watch it fail + (`./build/tests test_`), then implement. diff --git a/.claude/workflows/branch-review.js b/.claude/workflows/branch-review.js new file mode 100644 index 000000000..399dce0f3 --- /dev/null +++ b/.claude/workflows/branch-review.js @@ -0,0 +1,79 @@ +export const meta = { + name: 'branch-review', + description: 'Multi-agent pre-PR review of the current branch diff with adversarial verification', + whenToUse: 'Before opening a non-trivial PR. args: {base?: string} (default "master").', + phases: [ + { title: 'Find', detail: 'parallel dimension-scoped reviewers' }, + { title: 'Verify', detail: 'adversarial skeptic per finding' }, + ], +} + +const BASE = (args && args.base) || 'master' + +const FINDINGS = { + type: 'object', required: ['findings'], + properties: { + findings: { + type: 'array', + items: { + type: 'object', + required: ['file', 'line', 'summary', 'scenario', 'severity'], + properties: { + file: { type: 'string' }, + line: { type: 'integer' }, + summary: { type: 'string', description: 'one-sentence defect statement' }, + scenario: { type: 'string', description: 'concrete inputs/state -> wrong outcome' }, + severity: { enum: ['critical', 'high', 'medium', 'low'] }, + }, + }, + }, + }, +} + +const VERDICT = { + type: 'object', required: ['real', 'reason'], + properties: { real: { type: 'boolean' }, reason: { type: 'string' } }, +} + +const DIMENSIONS = [ + ['memory', 'memory errors: leaks (cf_alloc/cf_free pairing on ALL paths incl. error paths), use-after-free, double free, buffer overruns, pointers into ckit dynamic arrays held across apush/afit'], + ['correctness', 'logic errors, off-by-one, integer truncation/sign, uninitialized fields, wrong lifecycle ordering, missing null checks on public API entry points'], + ['api-contract', 'public API breaks: changed behavior of existing cf_* functions, namespace Cute C++ wrapper out of sync with the C declaration, deprecated forwarders that no longer forward'], + ['cross-platform', 'works-on-macOS-only hazards: Emscripten/WebGL2 (no compute shaders, async main loop), Linux/GLES3 backend, HiDPI points-vs-pixels confusion'], + ['silent-failure', 'errors swallowed instead of returned via CF_Result, fallbacks that hide breakage, warnings suppressed'], +] + +phase('Find') +const rounds = await parallel(DIMENSIONS.map(([key, focus]) => () => + agent( + `Review this branch's changes vs ${BASE}, hunting ONLY for: ${focus}.\n` + + `Get the change set yourself: git diff ${BASE}...HEAD plus git status for untracked files. ` + + `Read the full context around each hunk before judging. Try to refute each candidate finding first; ` + + `report only findings that survive refutation. line = 1-indexed line in the NEW file.`, + { label: `find:${key}`, phase: 'Find', agentType: 'code-reviewer', schema: FINDINGS }))) + +// Barrier justified: dedupe across ALL finders before paying for verification. +const seen = new Set() +const candidates = rounds.filter(Boolean).flatMap(r => r.findings || []).filter(f => { + const k = `${f.file}:${f.line}` + if (seen.has(k)) return false + seen.add(k) + return true +}) +log(`${candidates.length} candidate findings from ${DIMENSIONS.length} dimensions`) + +phase('Verify') +const verified = await parallel(candidates.map(f => () => + agent( + `Adversarially verify a claimed defect. Your default stance: it is WRONG until proven. ` + + `Read the code, its callers, and invariants, and try hard to refute it.\n` + + `Claim: ${f.file}:${f.line} - ${f.summary}\nScenario: ${f.scenario}\n` + + `Set real=true ONLY if you could not refute it; explain either way in reason.`, + { label: `verify:${f.file}:${f.line}`, phase: 'Verify', agentType: 'code-reviewer', schema: VERDICT }) + .then(v => (v && v.real ? { ...f, verified_reason: v.reason } : null)))) + +const confirmed = verified.filter(Boolean) +const order = { critical: 0, high: 1, medium: 2, low: 3 } +confirmed.sort((a, b) => order[a.severity] - order[b.severity]) +log(`${confirmed.length}/${candidates.length} findings survived adversarial verification`) +return { confirmed, candidateCount: candidates.length, base: BASE } diff --git a/.claude/workflows/docs-audit.js b/.claude/workflows/docs-audit.js new file mode 100644 index 000000000..a22989179 --- /dev/null +++ b/.claude/workflows/docs-audit.js @@ -0,0 +1,64 @@ +export const meta = { + name: 'docs-audit', + description: 'Audit doc comments in public headers against their real implementation', + whenToUse: 'After a feature lands or before a docs PR. args: {headers?: string[]} - omit to audit headers changed vs master.', + phases: [ + { title: 'Scope', detail: 'determine which headers to audit' }, + { title: 'Audit', detail: 'one agent per header, report-only' }, + ], +} + +const ISSUES = { + type: 'object', required: ['header', 'issues'], + properties: { + header: { type: 'string' }, + issues: { + type: 'array', + items: { + type: 'object', + required: ['symbol', 'kind', 'detail'], + properties: { + symbol: { type: 'string' }, + kind: { enum: ['stale-claim', 'wrong-param', 'missing-related', 'one-way-related', 'undocumented', 'other'] }, + detail: { type: 'string', description: 'what is wrong and what the code actually does, with src/ file:line' }, + }, + }, + }, + }, +} + +const HEADERS = { + type: 'object', required: ['headers'], + properties: { headers: { type: 'array', items: { type: 'string' } } }, +} + +phase('Scope') +let headers = (args && args.headers) || null +if (!headers || !headers.length) { + const res = await agent( + 'List the public headers changed on this branch: run ' + + '`git diff --name-only master...HEAD -- include/` and return the paths of ' + + 'hand-written cute_*.h files (exclude *_shd.h and cute_version.h).', + { label: 'scope', phase: 'Scope', schema: HEADERS }) + headers = res ? res.headers : [] +} +if (!headers.length) { + log('No headers to audit.') + return { audited: [], issues: [] } +} +log(`Auditing ${headers.length} header(s)`) + +phase('Audit') +const results = await pipeline(headers, h => + agent( + `Audit the documentation comments in ${h} against the real implementation. ` + + `REPORT-ONLY: do not edit anything. For each documented symbol, read the ` + + `implementation in src/ and check: does the doc describe actual behavior ` + + `(stale claims)? are @param descriptions right? does @related exist and is ` + + `it bidirectional? are there public symbols with no doc block at all? ` + + `Echo the header path in the 'header' field. Cite src/ file:line in details.`, + { label: `audit:${h}`, phase: 'Audit', agentType: 'doc-writer', schema: ISSUES })) + +const issues = results.filter(Boolean).flatMap(r => (r.issues || []).map(i => ({ header: r.header, ...i }))) +log(`${issues.length} issue(s) across ${headers.length} header(s)`) +return { audited: headers, issues } diff --git a/AGENTS.md b/AGENTS.md index 2fc80d0dd..39458ee58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -122,7 +122,7 @@ When contributing to Cute Framework, follow these established coding conventions When renaming public API functions: - The old name remains the real implementation/declaration - The new name is a `CF_INLINE` forwarding function -- The old name gets `@deprecated` in its doc comment +- The old name's deprecation is noted in prose in its doc comment ("Deprecated — use `cf_new_name` instead.") inside `@brief` or `@remarks`. Never write an `@deprecated` tag — the docs parser panics on unknown tags. - C++ wrappers in `namespace Cute` are updated in tandem ## Build Commands @@ -298,3 +298,27 @@ Full documentation is available at https://randygaul.github.io/cute_framework/ap # Tests are automatically built with the project # Test results are printed to console with pass/fail status ``` + +## AI Automation (.claude/) + +The repo ships Claude Code automation. Other AI tools can read these for +context; the conventions they encode apply to ALL contributors. + +**Agents** (`.claude/agents/`): `software-architect` (planning), +`code-writer` (implementation), `code-reviewer` (bug hunting), +`cf-api-reviewer` (public-header conventions), `doc-writer` (doc comments), +`sample-writer` (samples/), `researcher` (external technical research), +`performance-engineer` (measured optimization work). + +**Skills** (`.claude/skills/`): `header-api-review` (header conventions), +`cmake-conventions` (SDL-style consumable-framework CMake), +`test-writing` (test harness + registration + traps), +`perf-benchmarking` (benchmark methodology). + +**Hooks** (`.claude/hooks/`, wired in `.claude/settings.json`): block edits +to generated files (`*_shd.h`, `cute_version.h/.cpp`); warn on missing +include guards/copyright, docs-parser-breaking `@tags`, and unregistered +source/header/test/sample files. Tests: `python3 .claude/hooks/tests/test_hooks.py`. + +**Workflows** (`.claude/workflows/`): `branch-review` (multi-agent pre-PR +review), `docs-audit` (doc comments vs implementation).