diff --git a/AGENTS.consumer.md b/AGENTS.consumer.md new file mode 100644 index 0000000..ecaf267 --- /dev/null +++ b/AGENTS.consumer.md @@ -0,0 +1,45 @@ +## Crafter.Math + +The Crafter engine's math library: a C++ module with no implementation units and no dependencies, so everything is a template or an inline body compiled into your translation units with your build's `--target` and `--march`. Source and issues: https://forgejo.catcrafts.net/Catcrafts/Crafter.Math. Namespace `Crafter`. The README there documents the whole surface; these notes are what an agent working in a consumer needs. + +### Which type + +**`Vector`** is plain data, for vertex and uniform structs, events, anything stored or passed around. `.x .y .z .w` (and `.r .g .b .a`) alias `v[]`; constructors exist for `Len` 2, 3 and 4 (`Vector{x, y}`); `+ - * /` with vectors and scalars, `+=` and friends, `==`, unary `-`; statics `Dot(a, b)`, `Cross(a, b)` (`Len == 3`), `Normalize(a)`; members `Length()`, `LengthSq()`, `Normalize()`, `Invert()`. `std::format("{}", v)` prints `{x,y,z}`. + +- `Alignment` is the number of stored elements. `0` means `Len`, naturally aligned: `Vector` is 12 bytes aligned 4. Any `N >= Len` stores `T v[N]` and marks the struct `__attribute__((packed))`, alignment 1: `Vector` is 12 bytes aligned 1 (the vertex type Crafter.Graphics uses), `Vector` is 16 bytes, a `vec4` slot. Conversion between alignments is implicit. +- Arithmetic between two lengths covers the shorter one and leaves the other lanes of the result uninitialised, and `==` compares the shorter. The member `Normalize()` divides without a zero check; the static `Normalize(a)` checks. + +**`VectorF32`** is one SIMD register holding `Packing` vectors of `Len` floats, for math-heavy loops. Construct from a `const float*` or broadcast one `float`; `Load(ptr)`, `Store(ptr)`, and `Store()` returning a `std::array`. Operators as above; statics `Cross` (`Len == 3`), `MulitplyAdd(a, b, c)` (`a*b + c`), `MulitplySub(a, b, c)` (`a*b - c`), `Rotate(v, q)` and `RotatePivot(v, q, pivot)` by a `VectorF32<4, Packing>` quaternion, `QuanternionFromEuler(halfAngles)`, `Blend<{{bools}}>(a, b)`; members `Sin()`, `Cos()`, `SinCos()`, `Shuffle<{{i, j, k}}>()`, `Negate<{{bools}}>()`, `ExtractLo()`. Inside a template write `v.template Store()`. + +- `Load`, `Store` and the pointer constructor move the whole register, `AlignmentElement` floats (4, 8 or 16), not `Len * Packing`. Give them a buffer that big (`alignas(16) float buf[4] = { x, y, z, 0 };` is the idiom in the tests), zero the padding lanes on the way in and ignore them on the way out. The pointer need not be aligned. +- `Dot`, `Length`, `LengthSq` and `Normalize` are batched with the arity fixed at compile time: `Normalize`, `Length` and `LengthSq` take exactly `BatchSize` vectors, `Dot` takes `2 * BatchSize` (a0, b0, a1, b1, ...). They return `VectorF32<1, Packing * BatchSize>` with one lane per input vector (`Normalize` returns `std::array, BatchSize>`). There is no two-argument `Dot`: for one product pass the same pair `BatchSize` times and read lane 0, as `MatrixRowMajor::operator*` does, or use `Vector::Dot`. +- `BatchSize` is `AlignmentElement / Packing`, 4 for every `VectorF32`. `OptimalPacking` is how many `Len`-vectors fill one register (table below). Shapes convert implicitly, copying the lanes they share. +- The x86 backend lacks `const` on the scalar operators, `Sin`, `Cos`, `SinCos` and `Shuffle`: call them on non-const objects. + +**`VectorF16`** is the same surface for `_Float16` where the target has native half arithmetic: x86 with AVX512-FP16, AArch64 with ARMv8.2-FP16. Everywhere else, including every x86 tier below `sapphirerapids` and all of RISC-V, it is `using VectorF16 = VectorF32`, converting on `Load` and `Store` from `_Float16*`. Never overload a function on `VectorF16` and `VectorF32` both, and never assume its lanes are halves. + +**`MatrixRowMajor`** is four-column and row-major, each row a `VectorF32<4, 1>` in the public `rows[]`; constructors take the 12 or 16 floats row by row. Only `T = float`, `CollumSize = 4`, `Repeats = 1` exist. + +- `` (48 bytes) is the column-vector shape: `M * VectorF32<3, 1>` applies it with an implicit `w = 1`, `TransformNormal(v)` with `w = 0`; translation sits in column 3. `Store(float*)` writes the 12 floats row-major, which is `VkTransformMatrixKHR`: `m.Store(reinterpret_cast(instance.transform.matrix))`. +- `` (64 bytes) is the row-vector shape for the GPU side (`v * M`, translation in row 3). There is no matrix-times-vector operator for it in C++. `operator*` between two 4x4s, `Transpose()`, `LookTo(eye, direction, up)` and `LookAt(eye, focus, up)` are 4x4 only. +- Both shapes have `Identity()`, `Scaling(x, y, z)`, `Translation(x, y, z)` and `Rotation(pitch, yaw, roll)` in radians (each also from a `VectorF32<3, 1>`), `Rotation(Vector quaternion)`, `TRS(t, q, s)` (scale, then rotate, then translate: a glTF node), `InverseAffine()` (any rotation, scale and shear plus a translation, never a projection) and `Decompose()` returning `(t, q, s)`, with a mirroring expressed as a negative X scale. Each means the same transform in both shapes. `std::format` prints both. + +**Quaternions** are `Vector` in `(x, y, z, w)` order, unit, `w` the scalar part, applied by `Vector::Rotate(v, q)` and `RotatePivot(v, q, pivot)`; identity is `(0, 0, 0, 1)`. The functions are statics of `Vector` and are spelled `Quanternion`: `QuanternionMultiply(a, b)` is the Hamilton product, b first then a (`Rotate(v, a*b) == Rotate(Rotate(v, b), a)`); `QuanternionConjugate(q)` is the inverse rotation; `QuanternionFromAxisAngle(unitAxis, radians)` is right-handed (+90° about Z takes X to Y); `QuanternionFromUnitVectors(from, to)` is the shortest arc, defined for opposite directions too; `QuanternionFromBasis(right, up, forward)`; `QuanternionFromEuler(rollHalf, pitchHalf, yawHalf)` takes **half** angles; `QuanternionNlerp(a, b, t)` and `QuanternionSlerp(a, b, t)` take the shorter arc and return unit quaternions. + +**Intersection tests** (`IntersectionTestRayTriangle`, `IntersectionTestRaySphere`, `IntersectionTestRayOrientedBox`, `IntersectionTestSphereOrientedBox`, `IntersectionTestOrientedBoxOrientedBox`, with `PackedOBBs` and `GetOBBCorners`) are batched over `Packing * BatchSize` primitives per call; `Packing` defaults to `VectorF32<3, 1>::OptimalPacking` (the ray-box test to the smaller of the 3- and 4-vector packings). A vertex argument is `std::array, BatchSize>`: element `b`, lanes `s*3 .. s*3+2`, holds primitive `b * Packing + s`. The result is a `VectorF32<1, Total>` with lane `i` for primitive `i`: the ray parameter `t`, or `std::numeric_limits::max()` for a miss (the two box-overlap tests return `0` for a hit). The triangle test culls back faces; the sphere test returns the near root, so a ray starting inside misses. `tests/Intersection/main.cpp` in the Crafter.Math repository has `PackVec3`, `PackVec4` and `PackScalars` helpers worth copying. + +### Targets and SIMD + +| Backend | Register | `MaxElement` (floats) | `VectorF32<3,1>::OptimalPacking` | `BatchSize` of `VectorF32<3, Opt>` | +|---|---|---|---|---| +| x86 AVX-512 (`x86-64-v4`, `sapphirerapids`) | 64 B | 16 | 5 | 3 (15 lanes) | +| x86 AVX2 (`x86-64-v3`), and the scalar fallback | 32 B | 8 | 2 | 4 (8 lanes) | +| AArch64 NEON, WebAssembly SIMD128 | 16 B | 4 | 1 | 4 | +| RISC-V RVV | 16, 32 or 64 B from the `Zvl*` in the march | 4, 8 or 16 | | | + +- The backend and register width are fixed when the module is compiled, from predefined macros, so from the `--march` your build uses. crafter-build's default is `native`: fine for a developer build, wrong for a binary that ships. `--march=x86-64-v3` is the lowest x86 tier the library is tested on (its SSE paths use FMA); the `sapphirerapids` tier runs only on CPUs with AVX512-FP16. +- Tested as of 2026-10-31: all three x86 tiers on Linux and on Windows (`x86_64-w64-mingw32` under wine); AArch64 `armv8-a` and `armv8.2-a+fp16` under qemu, except Crafter.Math's own Matrix test, which does not compile for AArch64 with clang 23 (`arm_neon.h` wrapper functions defined twice across module units; it bites a unit that emits `VectorF32<3, 1>` arithmetic before a `VectorF32<3, 1>` `Sin`, `Cos` or `SinCos`, as that test does through `MatrixRowMajor::Rotation(pitch, yaw, roll)`). If an AArch64 build of yours fails with `definition with same mangled name` in `arm_neon.h`, it is that issue. RISC-V has never been built. WebAssembly builds (crafter-build adds `-msimd128`; flags you add to your own units must match) and has no tests. + +### Spellings + +- `Quanternion*`, `MulitplyAdd`, `MulitplySub`, `CollumSize`, `Aligment` are the real identifiers. Don't correct them in a consumer; a rename is an upstream change that updates every consumer at once. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..597fd03 --- /dev/null +++ b/AGENTS.md @@ -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-.cppm` | One partition per file, see below. | +| `project.cpp` | The build description: the library plus the test matrix (`AddMarchVariants`). | +| `tests//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` 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`: 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`, independent of the rest, with the quaternion functions as statics of `Vector`. +- `: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` is one 16-byte register on every backend, with `AlignmentElement` 4 and `BatchSize` 4. `Vector` is 12 bytes aligned 4; `Vector` is 12 bytes aligned 1 (packed); `Vector` is 16 bytes. `MatrixRowMajor` 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/---/`, one `std.pcm` per target and march. A native crafter-build uses `~/.cache/crafter.build/` instead. + +Each test variant has its own directory, `build/-----/`, 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//main.cpp && crafter-build -v test '-'` 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-- \ + -fprebuilt-module-path=build/Quaternion-x86-64-v4-x86_64-unknown-linux-gnu-x86-64-v4-generic- \ + -O1 build/probe.cpp -c -o build/probe.o +# link: the same flags, build//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 - +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: -- --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 `` 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 `-`, 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//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/-.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: