AGENTS.md
This commit is contained in:
parent
087c8b32c3
commit
b701059c8c
7 changed files with 331 additions and 62 deletions
145
AGENTS.md
Normal file
145
AGENTS.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# AGENTS.md
|
||||
|
||||
Guidance for coding agents and contributors working on Crafter.Math itself. The [README](README.md) is the user-facing manual, and [AGENTS.consumer.md](AGENTS.consumer.md) is what agents working in a *consumer* of this library read: crafter-build copies it into the dependency-notes section of the consumer's `AGENTS.md` on every build. When any of the three disagrees with the code, the code wins.
|
||||
|
||||
## Project overview
|
||||
|
||||
Crafter.Math is the Crafter engine's math library, shipped as one C++ module, `Crafter.Math`, with seven partitions. It has no implementation units and no dependencies: `project.cpp` declares a static library whose only sources are the interfaces, so every function is a template or an inline body that a consumer instantiates with its own target and `-march`. The library is the bottom of the Crafter stack: Crafter.Asset, Crafter.Graphics and crafter-build's own `project.cpp` all load it from its default branch.
|
||||
|
||||
| Path | What lives there |
|
||||
|---|---|
|
||||
| `interfaces/Crafter.Math.cppm` | The umbrella: `export import`s every partition. |
|
||||
| `interfaces/Crafter.Math-<Partition>.cppm` | One partition per file, see below. |
|
||||
| `project.cpp` | The build description: the library plus the test matrix (`AddMarchVariants`). |
|
||||
| `tests/<Name>/main.cpp` | One test program per directory: `Vector`, `Matrix`, `Quaternion`, `Intersection`. |
|
||||
| `AGENTS.consumer.md` | Notes for consumers' agents; crafter-build injects them into consumers' `AGENTS.md`. This file never gets such a section itself, since the library has no dependencies. |
|
||||
| `build/`, `bin/` | Generated and gitignored. `build/test-logs/` keeps the output of failed tests. |
|
||||
|
||||
Partitions, in import order:
|
||||
|
||||
- `:Common` picks the SIMD register type per ISA and holds what the SIMD vectors share: `VectorBase<Len, Packing, T>` with `VectorType`, `Max`, `MaxElement`, `AlignmentElement`, `BatchSize` and `OptimalPacking`, the shuffle/blend/negate mask builders, and the sin/cos polynomial helpers for SSE, AVX, AVX-512 and NEON. The intrinsics headers (`immintrin.h`, `wasm_simd128.h`, `arm_neon.h`, `riscv_vector.h`) are included in the global module fragment of every unit that needs them.
|
||||
- `:VectorF32` is `VectorF32<Len, Packing>`: five complete copies of the struct in one `#if` chain, one per backend. x86 (`__x86_64`), WebAssembly (`__wasm_simd128__`), RISC-V (`__riscv_vector`), AArch64 (`__aarch64__ && __ARM_NEON`), and the scalar `#else` over `std::array`.
|
||||
- `:VectorF16` is the same surface for `_Float16` with three backends: AVX512-FP16, NEON with ARMv8.2-FP16 arithmetic, and a generic `_Float16` copy for other non-x86, non-NEON targets. Where none applies, `:Basic` declares `using VectorF16 = VectorF32`.
|
||||
- `:Basic` holds that alias and `ToRadian`.
|
||||
- `:Vector` is the plain scalar `Vector<T, Len, Alignment>`, independent of the rest, with the quaternion functions as statics of `Vector<T, 4>`.
|
||||
- `:MatrixRowMajor` builds on `:VectorF32` and `:Vector`: each row is a `VectorF32<4, 1>`.
|
||||
- `:Intersection` builds on `:VectorF32` and `:MatrixRowMajor`: batched ray, sphere and OBB tests.
|
||||
|
||||
Numbers that follow from `:Common`, which the tests and consumers depend on (checked against the built PCMs on 2026-10-31):
|
||||
|
||||
| Backend | Register (`Max`) | `MaxElement` (floats) | `VectorF32<3,1>::OptimalPacking` | `VectorF32<3, Opt>`: size, `BatchSize` |
|
||||
|---|---|---|---|---|
|
||||
| x86 AVX-512 (`x86-64-v4`, `sapphirerapids`) | 64 B | 16 | 5 | 64 B, 3 (15 lanes) |
|
||||
| x86 AVX2 (`x86-64-v3`), and the scalar fallback | 32 B | 8 | 2 | 32 B, 4 (8 lanes) |
|
||||
| AArch64 NEON, WebAssembly SIMD128 | 16 B | 4 | 1 | 16 B, 4 |
|
||||
| RISC-V RVV | 16, 32 or 64 B from the march's `Zvl*` | 4, 8 or 16 | | |
|
||||
|
||||
`VectorF32<Len, 1>` is one 16-byte register on every backend, with `AlignmentElement` 4 and `BatchSize` 4. `Vector<float, 3>` is 12 bytes aligned 4; `Vector<float, 3, 3>` is 12 bytes aligned 1 (packed); `Vector<float, 3, 4>` is 16 bytes. `MatrixRowMajor<float, 4, 3, 1>` is 48 bytes and the 4x4 is 64, both aligned 16. With AVX512-FP16, `VectorF16<3, 1>` is a real struct with `AlignmentElement` 8 and `MaxElement` 32; on the other x86 tiers it is the alias.
|
||||
|
||||
## Dev environment
|
||||
|
||||
crafter-build is the only tool, from its Flatpak (`alias crafter-build='flatpak run net.catcrafts.CrafterBuild'`; a locally built one runs with `--branch=local`). The library has no dependencies, so nothing is fetched. The host is Linux x86_64; cross targets need their sysroot or toolchain on the host.
|
||||
|
||||
Things about the Flatpak sandbox that cost time before:
|
||||
|
||||
- It sees the host filesystem but not the host's `PATH`. Inside, `PATH` is `/app/bin:/usr/bin` and the host's `/usr/bin` is at `/run/host/usr/bin`. A runner probe for `qemu-aarch64` or `wine` fails there, which crafter-build reports as a *failed* test ("runner unavailable"), not a skip, and it builds nothing for that test.
|
||||
- A host binary from `/run/host/usr/bin/` runs inside the sandbox only when it needs no host shared libraries: `qemu-aarch64-static` and `wine` run, `qemu-aarch64` does not (`libcapstone.so.5` missing). `flatpak-spawn --host` is not permitted.
|
||||
- It has its own `/tmp`. Scratch files for the compiler go under `build/` (gitignored), not under `/tmp`.
|
||||
- Its caches are under `~/.var/app/net.catcrafts.CrafterBuild/cache/crafter.build/<target>-<march>-<clang version>-<hash>/`, one `std.pcm` per target and march. A native crafter-build uses `~/.cache/crafter.build/` instead.
|
||||
|
||||
Each test variant has its own directory, `build/<Test>-<march>-<target>-<march>-<mtune>-<hash>/`, with the PCMs of all eight interfaces compiled for that tier, their objects and a `std.pcm`. A scratch program against a tier compiles with the Flatpak's clang against that directory; `touch tests/<Name>/main.cpp && crafter-build -v test '<Name>-<march>'` prints the exact command to copy. The shape, for `x86-64-v4`:
|
||||
|
||||
```bash
|
||||
flatpak run --command=clang++ net.catcrafts.CrafterBuild -stdlib=libc++ --target=x86_64-unknown-linux-gnu \
|
||||
-march=x86-64-v4 -mtune=generic -std=c++26 \
|
||||
-fprebuilt-module-path=$HOME/.var/app/net.catcrafts.CrafterBuild/cache/crafter.build/x86_64-unknown-linux-gnu-x86-64-v4-<clang>-<hash> \
|
||||
-fprebuilt-module-path=build/Quaternion-x86-64-v4-x86_64-unknown-linux-gnu-x86-64-v4-generic-<hash> \
|
||||
-O1 build/probe.cpp -c -o build/probe.o
|
||||
# link: the same flags, build/<variant>/Crafter.Math*.o build/probe.o -fuse-ld=lld -L/app/lib -Wl,-rpath,/app/lib
|
||||
# run: flatpak run --command=$PWD/build/probe net.catcrafts.CrafterBuild
|
||||
```
|
||||
|
||||
## Build, test and style commands
|
||||
|
||||
```bash
|
||||
crafter-build # the static library; little to see, it has no implementation units
|
||||
crafter-build test # every tier for the host target: 12 tests on x86_64
|
||||
crafter-build test 'Matrix*' # name glob; names are <Test>-<march>
|
||||
crafter-build test --list
|
||||
crafter-build test --target=x86_64-w64-mingw32 --runner=cmd:/run/host/usr/bin/wine -- --target=x86_64-w64-mingw32
|
||||
crafter-build test --target=aarch64-linux-gnu --runner=cmd:<wrapper> -- --target=aarch64-linux-gnu --sysroot=/opt/aarch64-rootfs
|
||||
crafter-build clean
|
||||
```
|
||||
|
||||
`--target=` is given twice for a cross run: before `--` it filters the tests, after `--` it reaches `project.cpp`, which gates the tiers on `cfg.target`. `project.cpp` registers no lint rules, so `crafter-build lint` and `format` exit 1 with "no rules"; style is checked by eye (see Code style). There is no CI for this repository. Before a change is done, `crafter-build test` passes on the host, and on every cross target you can reach when you touched `:Common`, `:VectorF32` or `:VectorF16`.
|
||||
|
||||
The `<wrapper>` for AArch64 is a script on the host, because an explicit `--runner=cmd:` gets none of the environment the default qemu runner sets, and that default runner (`qemu-aarch64` on `PATH`) cannot be found inside the sandbox:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
exec env QEMU_LD_PREFIX=/opt/aarch64-rootfs LD_LIBRARY_PATH=/opt/aarch64-rootfs/lib:/opt/aarch64-rootfs/usr/lib /run/host/usr/bin/qemu-aarch64-static "$@"
|
||||
```
|
||||
|
||||
Without it the binaries die with `Could not open '/lib/ld-linux-aarch64.so.1'`, reported as 💥 signal 127. The sysroot is an Arch Linux ARM rootfs with `libc++` 22.1.3 and its `std.cppm`, which the Flatpak's clang 23 accepts. qemu's default CPU has FP16, so both AArch64 tiers run.
|
||||
|
||||
## Testing instructions
|
||||
|
||||
Tests are declared in `project.cpp`, not discovered: the loop over `{ "Vector", "Intersection", "Matrix", "Quaternion" }` calls `cfg.AddMarchVariants(name, mathInterfaces, tiers)` with the tier array of the target's architecture, and each call makes one executable per tier, named `<Name>-<march>`, that recompiles all eight interfaces with that tier's `-march` and `-mtune`. That is the point of the matrix: a PCM compiled once with the library's own march would exercise one backend. The tiers are three for x86 (`sapphirerapids` with AVX512-FP16, `x86-64-v4`, `x86-64-v3`), two for AArch64 (`armv8.2-a+fp16`, `armv8-a`) and three for RISC-V (`rv64gcv_zvl512b`, `rv64gcv_zvl256b`, `rv64gcv`). A new test is a new `tests/<Name>/main.cpp` plus its name in that loop; a new tier is a line in the array.
|
||||
|
||||
A test is an `int main()`: exit 0 passes, nonzero fails, 77 skips. The four follow one shape: a table of `std::string* TestX()` functions that return `nullptr` on success or a heap-allocated message, and a `main` that runs them in order, prints `[name] message` to stderr and returns 1 at the first failure. Tolerances are `1e-4` in Matrix and Quaternion, `1e-3` in Intersection and `0.01` in Vector, whose `TestAllCombinations` sweeps every `(Len, Packing)` up to `MaxElement` for both `VectorF16` and `VectorF32`. That sweep only exercises shapes whose `Len * Packing` is a power of two; `Len == 3` shapes are covered by the Matrix and Intersection tests and by the Quaternion test's `RotatePivot` cases, which run `VectorF32` and `VectorF16` at `Packing` 1 and at the widest packing a 3-vector and a quaternion share (8 halves on AVX512-FP16, where the `VectorF16` cross product used to break at the 128-bit lane boundary). Open a new test file with a comment on what it guards, as `tests/Quaternion/main.cpp` does, and when a fix changes behaviour, add the case that failed before it.
|
||||
|
||||
What passes where, as of 2026-10-31 with crafter-build's clang 23.1.2:
|
||||
|
||||
| Target | How it was run | Result |
|
||||
|---|---|---|
|
||||
| x86_64 Linux, three tiers | `crafter-build test` on a Xeon w7-3465X (has AVX512-FP16) | 12 passed |
|
||||
| x86_64 Windows (`x86_64-w64-mingw32`), three tiers | wine from `/run/host/usr/bin` | 12 passed |
|
||||
| AArch64, two tiers | qemu through the wrapper above | Vector, Intersection and Quaternion pass (6); the Matrix unit does not compile, see Gotchas |
|
||||
| RISC-V | never built here: no riscv64 sysroot (`qemu-riscv64-static` is installed) | unknown |
|
||||
| WebAssembly | no tests declared; the library builds for `wasm32-wasip1` as a dependency of Crafter.Graphics | n/a |
|
||||
|
||||
The `sapphirerapids` tier only runs on a CPU with AVX512-FP16; elsewhere it dies with an illegal instruction (💥), which is not a logic failure. Failed, crashed and timed-out tests write `build/test-logs/<Test>-<march>.log`; a later passing run does not delete them, so check the timestamp before trusting one.
|
||||
|
||||
Real hardware is driven the same way, through `--runner=cmd:<script>` where the script ships and runs the binary (Crafter.Build's README describes `TestRunner`). A musl device needs the sysroot's glibc loader and libraries copied next to the binary and run through `ld-linux-aarch64.so.1 --library-path`.
|
||||
|
||||
## Code style
|
||||
|
||||
- The first two lines of every source are exactly `//SPDX-License-Identifier: LGPL-3.0-only` and `//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®`, no space after `//`, then a blank line. Crafter.Build spells its header with a space; this repository does not.
|
||||
- Four-space indentation in new code. `Crafter.Math-Vector.cppm` and parts of the two SIMD vector files still use tabs; match the block you are in and do not reindent files, it buries the real diff. LF line endings are enforced by `.gitattributes`. Several files have no final newline; leave that alone too.
|
||||
- `import std;` everywhere. The only `#include`s are the intrinsics headers in the global module fragment and `<cmath>` in the tests.
|
||||
- `PascalCase` for types and functions, `camelCase` for variables and parameters; template parameters are `Len`, `Packing`, `Alignment`. `VectorF32` and `VectorF16` take `std::uint8_t` parameters, `Vector` and `MatrixRowMajor` take `std::uint32_t`; keep it, the `static_cast<std::uint8_t>` in `:Intersection` and `:MatrixRowMajor` bridge the two.
|
||||
- Shape-specific members are `requires` clauses on one class (`requires(Len == 3)`, `requires(CollumSize == 4 && RowSize == 3 && Repeats == 1 && std::same_as<T, float>)`), not partial specialisations. Follow that.
|
||||
- Misspelled identifiers are API: the `Quanternion*` functions, `MulitplyAdd`, `MulitplySub`, `CollumSize`, `Aligment`, `GetAlingment`. Crafter.Graphics and Crafter.Asset build against them from this repository's default branch, so a rename is a breaking change: do it only on purpose, with the consumers updated in the same move. Do not add new misspellings.
|
||||
- Comments say why: the constraint, the failure mode, the rejected alternative. See the VLEN block in `:Common`, the quaternion block in `:Vector` and the NEON `vfmsq_f32` note. Commit bodies follow the same rule.
|
||||
|
||||
## Design rules
|
||||
|
||||
Decided deliberately; change them only with the maintainer's say-so.
|
||||
|
||||
1. **Interfaces only.** No implementation units: the SIMD width is decided where the code is instantiated, by the consumer's `-march`, and crafter-build recompiles the PCMs per variant. A function that needs an out-of-line body does not belong here.
|
||||
2. **The ISA is chosen at compile time from predefined macros**, one backend per `#if` branch, each a complete struct. No runtime dispatch and no mixing of backends in one struct. An operation added to one backend is added to all five of `VectorF32` (and the three of `VectorF16`), with the scalar fallback as the reference semantics: every target without an intrinsics backend compiles through it, and `MatrixRowMajor` needs its batched `Dot`.
|
||||
3. **Fixed-size storage, one register per object.** `Len * Packing <= MaxElement` is a `static_assert`; RVV pins the register width to the guaranteed VLEN at compile time rather than going vector-length-agnostic.
|
||||
4. **Batched operations lock their arity to `BatchSize`.** `Dot` takes `2 * BatchSize` arguments; `Length`, `LengthSq` and `Normalize` take `BatchSize`. The public variadic wrappers forward to the per-shape `*Pack` bodies. There is no scalar-returning `Dot` on the SIMD vectors: callers pad with repeated pairs and read one lane, as `MatrixRowMajor` does.
|
||||
5. **`Load` and `Store` move the whole register** (`AlignmentElement` lanes) through unaligned loads and stores; the padding lanes carry no meaning.
|
||||
6. **`VectorF16` aliases `VectorF32` where there is no native half path.** The alias lives in `:Basic`; the same condition is spelled out in `:Common`, `:VectorF16` and `:Basic` because macros do not cross module boundaries. Change all three together.
|
||||
7. **Both matrix shapes mean the same transform.** The 4x4 transforms row vectors (`v * M`, translation in row 3), the 4x3 column vectors (`M * v`, translation in column 3, `VkTransformMatrixKHR`), so the 4x4's upper 3x3 is the 4x3's transposed. Every factory and inverse exists for both and agrees, and `tests/Matrix` checks that they agree. The 4x3 `Rotation` once copied the 4x4's rows and rotated the other way.
|
||||
8. **Quaternions are `Vector<T, 4>` in (x, y, z, w) order**, unit, applied as `Rotate` does (q v q*). `QuanternionMultiply(a, b)` is b then a, and `QuanternionFromEuler` takes half angles. New quaternion code keeps these.
|
||||
9. **No dependencies.** Crafter.Math is loaded by every consumer's build and by crafter-build's own; anything it depended on would be fetched and built by all of them.
|
||||
|
||||
## Commit and PR guidelines
|
||||
|
||||
- Branch off `master` (`claude/<topic>` has been the pattern for agent work) and open the PR against `master` on the project's Forgejo at `forgejo.catcrafts.net/Catcrafts/Crafter.Math`. Push over SSH (`forgejo@forgejo.catcrafts.net:Catcrafts/Crafter.Math.git`).
|
||||
- Subject: a short imperative summary, with a `feat:` or `fix:` type prefix as the recent commits have. Body: what was wrong, why, and why the fix has this shape, wrapped at about 75 columns; `git log -3` shows the voice. Add a `Co-Authored-By:` trailer when an agent wrote it.
|
||||
- Every consumer tracks this repository's default branch unpinned (Crafter.Build, Crafter.Asset, Crafter.Graphics), so a push to `master` is live in their next build. Keep the exported surface compatible, or update them in lockstep, and build Crafter.Graphics against your branch with `--local` when you change a signature.
|
||||
- Keep the README's module table and example true to the exported surface, and update `AGENTS.consumer.md` when a convention or a signature consumers lean on changes. Consumers pick the new notes up on their next build, crafter-build's own `AGENTS.md` included, whose tree then shows as dirty until they commit the section.
|
||||
- Never commit `build/` or `bin/`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **AArch64 does not compile the Matrix test with clang 23** (crafter-build's 23.1.2): `arm_neon.h`'s inline wrappers `vmulq_f32` and `vfmaq_f32` are called from both `:Common` (the sin/cos helpers) and `:VectorF32`, and compiling `tests/Matrix/main.cpp` fails with `definition with same mangled name '_ZL9vmulq_f32...' as another definition`, pointing into `arm_neon.h` through `:Common`. The other three tests compile and pass on both AArch64 tiers. Bisected on 2026-10-31: a unit fails when `VectorF32<3, 1>` arithmetic (`operator*`, `MulitplyAdd`) is emitted before a `VectorF32<3, 1>` `Sin`, `Cos` or `SinCos` (Common's helpers), which `MatrixRowMajor::Rotation(pitch, yaw, roll)` does after the test's earlier cases. The reverse order compiles, and so do the same pairs on `<1,1>`, `<2,1>`, `<4,1>`, `<1,4>` and `<2,2>`: clang merges the two copies in some deserialization orders and not others. Any wrapper both units call can collide; with `vmulq_f32` and `vfmaq_f32` taken out of the helpers the error moved to `vdupq_n_f32`. `:Common` already dodged the same error for `vfmsq_f32` by writing `vfmaq_f32` with a negated constant; that no longer suffices, and the Matrix test compiled for AArch64 with clang 22.1.8 when the NEON backend landed (2026-10-06). The fix is a helper block in `:Common` that calls no `arm_neon.h` wrapper at all (vector-extension operators and clang builtins), or routing every shared operation through one unit, and it needs that test green. An importing unit that `#include`s `arm_neon.h` itself before the import compiles, which is a consumer's workaround in a pinch.
|
||||
- The x86 `#else` of `Max` is 32 bytes, so an x86 build without AVX-512 instantiates `__m256` for `Len * Packing` of 5 to 8 whether or not AVX is enabled, and the SSE sin/cos helpers use FMA. `x86-64-v3` is the real floor; plain `x86-64` and `x86-64-v2` are untested and would need `__AVX__` guards to work.
|
||||
- Several x86 `VectorF32` members lack `const`: the scalar `operator+ - * /`, `Sin`, `Cos`, `SinCos` and `Shuffle`. The other backends have it, so code that compiles on NEON can fail on x86 through a `const` object. Fix them when you touch them.
|
||||
- `Vector` arithmetic between different lengths covers the shorter length and leaves the remaining lanes of the result uninitialised; `==` compares the shorter length; the member `Normalize()` has no zero check while the static `Normalize(a)` has; `Vector<T, 1>` has no value constructor.
|
||||
- `GetAlingment()` has no return for more than 64 bytes; the `static_assert` on `Len * Packing` catches it first, so keep that assert.
|
||||
- A `--runner=cmd:<tool>` is probed with `which <tool>` inside the sandbox. A missing runner fails the tests rather than skipping them, and their build is skipped too, so "runner unavailable" says nothing about whether the code compiles.
|
||||
- `crafter-build test` counts tier variants: "12 passed" is four tests.
|
||||
Loading…
Reference in a new issue