Created
August 9, 2026 21:28
-
-
Save lardratboy/d3df2fc6605f956872c7705e96ad8754 to your computer and use it in GitHub Desktop.
mosprites at compile time
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // mosprite.hpp — compile-time sprite synthesis and n-dimensional rasterisation. | |
| // | |
| // A C++20 transcription of the MoSprites NG pipeline (mosprites-ng.md) in which | |
| // every stage parameter is a type. That is not decoration. Three things follow | |
| // from moving the policy into the type system, and only the first is about speed: | |
| // | |
| // 1. The inner loop contains no branch on operator, order, or colour space. | |
| // `if constexpr` deletes the untaken paths outright. | |
| // | |
| // 2. Dimension is a template parameter. ng §9 argues that the whole apparatus | |
| // lifts to n dimensions because coverage is separable, a = product of f_i. | |
| // Here that argument *is* the implementation: Walk<N,Axis> unrolls the loop | |
| // nest, and the 2-D sprite and the 3-D voxel model are one function. | |
| // | |
| // 3. Scale is a compile-time rational, so q = denom(p) is a constant, and the | |
| // symmetry results derived in aamazing.md §9 and corrected in ng §0 become | |
| // predicates the compiler can check. A configuration that claims a mirror | |
| // it does not have fails to compile. See `require_mirror`. | |
| // | |
| // Requires C++20. Header-only, no dependencies. | |
| #pragma once | |
| #include <array> | |
| #include <cstdint> | |
| #include <vector> | |
| #include <string_view> | |
| namespace mosprite { | |
| // ─────────────────────────────────────────────────────── constexpr arithmetic | |
| // Written out rather than pulled from <cmath> so every path is usable in a | |
| // constant expression on any conforming C++20 implementation. | |
| constexpr int cgcd(int a, int b) { return b ? cgcd(b, a % b) : (a < 0 ? -a : a); } | |
| constexpr int cfloor(double x) { int i = int(x); return (x < 0 && double(i) != x) ? i - 1 : i; } | |
| constexpr int cceil (double x) { int i = int(x); return (x > 0 && double(i) != x) ? i + 1 : i; } | |
| constexpr double cabs(double x) { return x < 0 ? -x : x; } | |
| constexpr double cmin(double a, double b) { return a < b ? a : b; } | |
| constexpr int imin(int a, int b) { return a < b ? a : b; } | |
| constexpr int imax(int a, int b) { return a > b ? a : b; } | |
| constexpr double cmax(double a, double b) { return a > b ? a : b; } | |
| // overlap of [a0,a1) with [b0,b1) — the 1-D factor of a separable coverage | |
| constexpr double span(double a0, double a1, double b0, double b1) { | |
| return cmax(0.0, cmin(a1, b1) - cmax(a0, b0)); | |
| } | |
| // Exact form. Scale is a rational, so if cell and pixel bounds are carried in | |
| // units of 1/den the overlap is an integer and coverage is the exact rational | |
| // overlap/den. Going through `double p = num/den` instead loses this: 21/5 is | |
| // not representable, 15 * 4.2 is not 63, and a sprite that should tile the | |
| // device grid exactly acquires a phantom column. The rational is in the type; | |
| // the arithmetic should use it. | |
| constexpr long ispan(long a0, long a1, long b0, long b1) { | |
| long lo = a0 > b0 ? a0 : b0, hi = a1 < b1 ? a1 : b1; | |
| return hi > lo ? hi - lo : 0; | |
| } | |
| constexpr int idiv_floor(long a, long b) { long q = a / b; return int((a % b && (a < 0) != (b < 0)) ? q - 1 : q); } | |
| constexpr int idiv_ceil (long a, long b) { return -idiv_floor(-a, b); } | |
| constexpr double cpow(double b, int e) { double r = 1; for (int i = 0; i < e; ++i) r *= b; return r; } | |
| // enough for the sRGB transfer curve; Newton on x^(1/2.4) and its inverse | |
| constexpr double cpow_f(double b, double e) { | |
| if (b <= 0) return 0; | |
| // ln via atanh series, exp via Taylor — both converge fast on [0,1] | |
| double z = (b - 1) / (b + 1), z2 = z * z, s = 0, t = z; | |
| for (int k = 0; k < 40; ++k) { s += t / (2 * k + 1); t *= z2; } | |
| double ln = 2 * s, x = e * ln, r = 1, term = 1; | |
| for (int k = 1; k < 40; ++k) { term *= x / k; r += term; } | |
| return r; | |
| } | |
| // ───────────────────────────────────────────────────── scale as a rational | |
| // The seam lattice period q is the reduced denominator (aamazing.md §5). Making | |
| // it a compile-time constant is what lets the symmetry theorems be checkable. | |
| template <int Num, int Den> | |
| struct Scale { | |
| static_assert(Den > 0 && Num > 0, "scale must be positive"); | |
| static constexpr int num = Num / cgcd(Num, Den); | |
| static constexpr int den = Den / cgcd(Num, Den); | |
| static constexpr int q = den; // seam lattice period | |
| static constexpr double value = double(Num) / double(Den); | |
| }; | |
| template <int N> using Int = Scale<N, 1>; | |
| // ───────────────────────────────────────────────────── composite operators | |
| // Each carries its closed form (ng §6) and, crucially, whether it commutes — | |
| // which is what decides whether traversal order is a light direction and | |
| // whether a mirror survives fractional scale. | |
| struct Px { double r = 0, g = 0, b = 0, a = 0; }; // premultiplied, working space | |
| struct Over { | |
| static constexpr std::string_view name = "over"; | |
| static constexpr bool commutative = false; | |
| static constexpr Px apply(Px d, double sr, double sg, double sb, double a) { | |
| return { sr * a + d.r * (1 - a), sg * a + d.g * (1 - a), | |
| sb * a + d.b * (1 - a), a + d.a * (1 - a) }; | |
| } | |
| }; | |
| struct Add { | |
| static constexpr std::string_view name = "add"; | |
| static constexpr bool commutative = true; | |
| static constexpr Px apply(Px d, double sr, double sg, double sb, double a) { | |
| return { d.r + sr * a, d.g + sg * a, d.b + sb * a, cmin(1.0, d.a + a) }; | |
| } | |
| }; | |
| struct Mul { | |
| static constexpr std::string_view name = "mul"; | |
| static constexpr bool commutative = false; | |
| // the /255 is the normalisation that keeps a product of two 8-bit | |
| // quantities an 8-bit quantity; omitting it returns ~40000 (ng §6) | |
| static constexpr Px apply(Px d, double sr, double sg, double sb, double a) { | |
| return { d.r * (1 - a) + (d.r * sr / 255.0) * a, | |
| d.g * (1 - a) + (d.g * sg / 255.0) * a, | |
| d.b * (1 - a) + (d.b * sb / 255.0) * a, a + d.a * (1 - a) }; | |
| } | |
| }; | |
| // ───────────────────────────────────────────────────────── colour spaces | |
| struct sRGB { | |
| static constexpr std::string_view name = "srgb"; | |
| static constexpr bool linear = false; | |
| static constexpr double encode(double c) { return c; } | |
| static constexpr double decode(double c) { return c; } | |
| }; | |
| struct Linear { | |
| static constexpr std::string_view name = "linear"; | |
| static constexpr bool linear = true; | |
| static constexpr double encode(double c) { // sRGB → light | |
| double u = c / 255.0; | |
| return (u <= 0.04045 ? u / 12.92 : cpow_f((u + 0.055) / 1.055, 2.4)) * 255.0; | |
| } | |
| static constexpr double decode(double c) { // light → sRGB | |
| double u = c / 255.0; | |
| return (u <= 0.0031308 ? u * 12.92 : 1.055 * cpow_f(u, 1.0 / 2.4) - 0.055) * 255.0; | |
| } | |
| }; | |
| // ─────────────────────────────────────────────────────── traversal orders | |
| // A total order on cells. Only meaningful under a non-commutative operator — | |
| // see `order_is_observable` below, which the compiler can tell you about. | |
| struct RowMajor { | |
| static constexpr std::string_view name = "rowMajor"; | |
| template <int N> | |
| static constexpr long key(const std::array<int, N>& c, const std::array<int, N>& dim) { | |
| long k = 0; for (int i = N - 1; i >= 0; --i) k = k * dim[i] + c[i]; return k; | |
| } | |
| }; | |
| struct ColMajor { | |
| static constexpr std::string_view name = "colMajor"; | |
| template <int N> | |
| static constexpr long key(const std::array<int, N>& c, const std::array<int, N>& dim) { | |
| long k = 0; for (int i = 0; i < N; ++i) k = k * dim[i] + c[i]; return k; | |
| } | |
| }; | |
| struct Serpentine { | |
| static constexpr std::string_view name = "serpentine"; | |
| template <int N> | |
| static constexpr long key(const std::array<int, N>& c, const std::array<int, N>& dim) { | |
| long row = 0; for (int i = N - 1; i >= 1; --i) row = row * dim[i] + c[i]; | |
| long x = (row % 2) ? (dim[0] - 1 - c[0]) : c[0]; | |
| return row * dim[0] + x; | |
| } | |
| }; | |
| struct Radial { | |
| static constexpr std::string_view name = "radial"; | |
| template <int N> | |
| static constexpr long key(const std::array<int, N>& c, const std::array<int, N>& dim) { | |
| long d2 = 0; | |
| for (int i = 0; i < N; ++i) { long t = 2 * c[i] - (dim[i] - 1); d2 += t * t; } | |
| return d2; | |
| } | |
| }; | |
| struct Morton { | |
| static constexpr std::string_view name = "morton"; | |
| template <int N> | |
| static constexpr long key(const std::array<int, N>& c, const std::array<int, N>&) { | |
| long k = 0; | |
| for (int bit = 0; bit < 16; ++bit) | |
| for (int i = 0; i < N; ++i) | |
| k |= long((c[i] >> bit) & 1) << (bit * N + i); | |
| return k; | |
| } | |
| }; | |
| // paint in order of field value: the apparent light is generated by the same | |
| // arithmetic as the form (ng §5). Needs Φ's function, not Φ's output — which is | |
| // why the field is a template parameter of the sprite, not a runtime lookup. | |
| struct ByField { static constexpr std::string_view name = "byField"; }; | |
| // ──────────────────────────────────────────────────────────────── fields | |
| // n-generic by construction: every field is defined on a coordinate array, so | |
| // the same set serves 2-D sprites and 3-D voxel models (ng §9). | |
| struct Circle { static constexpr std::string_view name = "circle"; | |
| template <int N> static constexpr long f(const std::array<long, N>& u) { | |
| long s = 0; for (int i = 0; i < N; ++i) s += u[i] * u[i]; return s; } }; | |
| struct Diamond { static constexpr std::string_view name = "diamond"; | |
| template <int N> static constexpr long f(const std::array<long, N>& u) { | |
| long s = 0; for (int i = 0; i < N; ++i) s += (u[i] < 0 ? -u[i] : u[i]); return s; } }; | |
| struct Cheby { static constexpr std::string_view name = "cheby"; | |
| template <int N> static constexpr long f(const std::array<long, N>& u) { | |
| long m = 0; for (int i = 0; i < N; ++i) { long a = u[i] < 0 ? -u[i] : u[i]; if (a > m) m = a; } return m; } }; | |
| struct Xor { static constexpr std::string_view name = "xor"; | |
| template <int N> static constexpr long f(const std::array<long, N>& u) { | |
| long s = 0; for (int i = 0; i < N; ++i) s ^= (u[i] < 0 ? -u[i] : u[i]); return s; } }; | |
| struct Product { static constexpr std::string_view name = "product"; | |
| template <int N> static constexpr long f(const std::array<long, N>& u) { | |
| long s = 1; for (int i = 0; i < N; ++i) s *= u[i]; return s; } }; | |
| struct Trefoil { static constexpr std::string_view name = "trefoil"; // x³ − 3xy² lifted | |
| template <int N> static constexpr long f(const std::array<long, N>& u) { | |
| long s = 0; for (int i = 0; i < N; ++i) { long t = u[(i + 1) % N]; | |
| s += u[i] * u[i] * u[i] - 3 * u[i] * t * t; } return s; } }; | |
| struct Hyper { static constexpr std::string_view name = "hyper"; // alternating squares | |
| template <int N> static constexpr long f(const std::array<long, N>& u) { | |
| long s = 0; for (int i = 0; i < N; ++i) s += (i % 2 ? -1 : 1) * u[i] * u[i]; return s; } }; | |
| // ────────────────────────────────────────────── hyperoctahedral group B_n | |
| // The signed permutation group: |B_n| = 2^n · n!. B_2 is the 8-element dihedral | |
| // symmetry menu of the original tool; B_3 is the 48 elements of the voxel work; | |
| // B_4 has 384. Generated at compile time so ng §9's "Γ generalises from the | |
| // dihedral groups to the B_n family" is a fact about the code, not a plan. | |
| template <int N> constexpr int factorial() { int r = 1; for (int i = 2; i <= N; ++i) r *= i; return r; } | |
| template <int N> inline constexpr int B_order = (1 << N) * factorial<N>(); | |
| template <int N> | |
| struct Element { // x'_i = sign_i * x_{perm_i} | |
| std::array<int, N> perm{}; | |
| std::array<int, N> sign{}; | |
| constexpr std::array<long, N> operator()(const std::array<long, N>& x) const { | |
| std::array<long, N> y{}; | |
| for (int i = 0; i < N; ++i) y[i] = sign[i] * x[perm[i]]; | |
| return y; | |
| } | |
| }; | |
| template <int N> | |
| constexpr std::array<Element<N>, B_order<N>> make_B() { | |
| std::array<Element<N>, B_order<N>> out{}; | |
| std::array<int, N> perm{}; | |
| for (int i = 0; i < N; ++i) perm[i] = i; | |
| int idx = 0; | |
| // lexicographic permutations × all sign patterns | |
| for (int pcount = 0; pcount < factorial<N>(); ++pcount) { | |
| for (int mask = 0; mask < (1 << N); ++mask) { | |
| Element<N> e{}; | |
| e.perm = perm; | |
| for (int i = 0; i < N; ++i) e.sign[i] = (mask >> i) & 1 ? -1 : 1; | |
| out[idx++] = e; | |
| } | |
| // next permutation, in place | |
| int i = N - 2; while (i >= 0 && perm[i] >= perm[i + 1]) --i; | |
| if (i < 0) break; | |
| int j = N - 1; while (perm[j] <= perm[i]) --j; | |
| { int t = perm[i]; perm[i] = perm[j]; perm[j] = t; } | |
| for (int a = i + 1, b = N - 1; a < b; ++a, --b) { int t = perm[a]; perm[a] = perm[b]; perm[b] = t; } | |
| } | |
| return out; | |
| } | |
| template <int N> inline constexpr auto B = make_B<N>(); | |
| // A symmetry is a subgroup, given as a predicate over B_n elements. The sprite | |
| // is a section of the quotient: each cell takes the value of the lexicographic | |
| // least point in its orbit, so the grid is symmetric by construction rather | |
| // than by a fold pass that can be got wrong. | |
| struct Asymmetric { // trivial subgroup: no symmetry | |
| template <int N> static constexpr bool member(const Element<N>& e) { | |
| for (int i = 0; i < N; ++i) { if (e.perm[i] != i || e.sign[i] != 1) return false; } | |
| return true; } }; | |
| struct MirrorX { // reflect axis 0 only | |
| template <int N> static constexpr bool member(const Element<N>& e) { | |
| for (int i = 0; i < N; ++i) { if (e.perm[i] != i) return false; } | |
| for (int i = 1; i < N; ++i) { if (e.sign[i] != 1) return false; } | |
| return true; } }; | |
| struct MirrorAll { // reflect every axis: 2^N elements | |
| template <int N> static constexpr bool member(const Element<N>& e) { | |
| for (int i = 0; i < N; ++i) { if (e.perm[i] != i) return false; } | |
| return true; } }; | |
| struct Dihedral { template <int N> static constexpr bool member(const Element<N>&) { return true; } }; | |
| // ────────────────────────────────────────── theorems, as compile-time predicates | |
| // | |
| // aamazing.md §9 proves the *phase field* is mirror-symmetric iff q ≤ 2 and | |
| // q | (w−1). ng §0 corrects the conclusion drawn from it: that is a statement | |
| // about the seam lattice, not about pixels. Under a non-commutative operator | |
| // every mirrored pair of seams is resolved in the opposite sense, so the | |
| // rendered image is symmetric only when there are no seams at all. | |
| template <class S, int W> inline constexpr bool phase_field_symmetric_v = | |
| (S::q <= 2) && ((W - 1) % S::q == 0); | |
| template <class Op, class S, int W> inline constexpr bool mirror_survives_v = | |
| Op::commutative ? ((long(W) * S::num) % S::den == 0) // q | W | |
| : (S::q == 1); // no seams at all | |
| // Traversal order is observable only under a non-commutative operator (ng §5). | |
| template <class Op, class Ord> inline constexpr bool order_is_observable_v = | |
| !Op::commutative && !std::is_same_v<Ord, RowMajor>; | |
| // Fraction of device pixels that lie on the seam sublattice, per axis. | |
| template <class S> inline constexpr double seam_share_v = | |
| S::q == 1 ? 0.0 : double(S::q - 1) / (double(S::q) * S::value); | |
| // Opt-in guard. Instantiate it and a configuration that claims a mirror it | |
| // does not have becomes a compile error rather than a surprising PNG. | |
| template <class Op, class S, int W> | |
| struct require_mirror { | |
| static_assert(mirror_survives_v<Op, S, W>, | |
| "This (operator, scale, width) does not render a mirror. Under a " | |
| "non-commutative operator the mirror survives only at integer scale; " | |
| "under a commutative one it survives iff q divides the width. " | |
| "See mosprites-ng.md §0 and §6."); | |
| }; | |
| // ──────────────────────────────────────────────────────── sprite generator Φ | |
| // mulberry32, verbatim from the reference implementation, so a seed means the | |
| // same thing here as it does in the browser. | |
| struct Rng { | |
| std::uint32_t s; | |
| constexpr explicit Rng(std::uint32_t seed) : s(seed) {} | |
| constexpr double operator()() { | |
| s += 0x6D2B79F5u; | |
| std::uint32_t t = s; | |
| t = std::uint32_t((t ^ (t >> 15)) * (t | 1u)); | |
| t ^= t + std::uint32_t((t ^ (t >> 7)) * (t | 61u)); | |
| return double((t ^ (t >> 14)) >> 8) / double(1u << 24); | |
| } | |
| }; | |
| struct RGB { std::uint8_t r = 0, g = 0, b = 0; }; | |
| // bit-plane palette: bpc bits per channel, so colours land on a regular lattice | |
| constexpr RGB palette_entry(std::uint32_t seed, int i, int bpc) { | |
| Rng rng(seed + std::uint32_t(i) * 2654435761u); | |
| int levels = (1 << bpc) - 1; | |
| auto ch = [&] { return std::uint8_t(double(int(rng() * (levels + 1)) % (levels + 1)) / levels * 255.0); }; | |
| return { ch(), ch(), ch() }; | |
| } | |
| template <int N> struct Extent { std::array<int, N> n{}; | |
| constexpr int total() const { int t = 1; for (int i = 0; i < N; ++i) t *= n[i]; return t; } }; | |
| // Φ's configuration is a type. Note `Field` is carried in the type, which is | |
| // how order:ByField gets the *function* rather than a reconstruction of it | |
| // (ng §2.2 — a reconstructed function is a wrong function with no error). | |
| template <class FieldT, class SymT, int Modulus, int Ncol = 4, int Bpc = 2, | |
| int Stride = 1, int CoveragePct = 50> | |
| struct SpriteCfg { | |
| using Field = FieldT; | |
| using Sym = SymT; | |
| static constexpr int modulus = Modulus, ncol = Ncol, bpc = Bpc, | |
| stride = Stride, coverage_pct = CoveragePct; | |
| static_assert(Modulus > 1, "modulus must exceed 1"); | |
| static_assert(Ncol >= 1 && Ncol <= 16, "palette size out of range"); | |
| }; | |
| template <class Cfg, int N, int... Dims> | |
| struct Sprite { | |
| static constexpr int rank = N; | |
| static constexpr Extent<N> dim{ { Dims... } }; | |
| static constexpr int cells = (Dims * ...); | |
| static_assert(sizeof...(Dims) == N, "one extent per axis"); | |
| std::array<std::uint8_t, cells> grid{}; // 0 = empty, else palette index+1 | |
| std::array<RGB, Cfg::ncol> colors{}; | |
| // centred, doubled coordinates: integral, so the group acts exactly | |
| static constexpr std::array<long, N> centred(const std::array<int, N>& c) { | |
| std::array<long, N> u{}; | |
| for (int i = 0; i < N; ++i) u[i] = 2L * c[i] - (dim.n[i] - 1); | |
| return u; | |
| } | |
| // the field, published as a function of cell coordinates (ng §2.2) | |
| static constexpr long field_at(const std::array<int, N>& c) { | |
| auto u = centred(c); | |
| for (int i = 0; i < N; ++i) u[i] *= Cfg::stride; | |
| return Cfg::Field::template f<N>(u); | |
| } | |
| static constexpr int index(const std::array<int, N>& c) { | |
| int k = 0; for (int i = N - 1; i >= 0; --i) k = k * dim.n[i] + c[i]; return k; | |
| } | |
| static constexpr std::array<int, N> coord(int k) { | |
| std::array<int, N> c{}; | |
| for (int i = 0; i < N; ++i) { c[i] = k % dim.n[i]; k /= dim.n[i]; } | |
| return c; | |
| } | |
| constexpr std::uint8_t at(const std::array<int, N>& c) const { return grid[index(c)]; } | |
| }; | |
| template <class Cfg, int N, int... Dims> | |
| constexpr Sprite<Cfg, N, Dims...> generate(std::uint32_t seed) { | |
| using Sp = Sprite<Cfg, N, Dims...>; | |
| Sp s{}; | |
| Rng rng(seed); | |
| for (int i = 0; i < Cfg::ncol; ++i) s.colors[i] = palette_entry(seed ^ 0x9E3779B9u, i, Cfg::bpc); | |
| // luminance sort keeps palette index monotone in brightness | |
| for (int i = 1; i < Cfg::ncol; ++i) | |
| for (int j = i; j > 0; --j) { | |
| auto L = [](RGB c) { return 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b; }; | |
| if (L(s.colors[j]) < L(s.colors[j - 1])) { auto t = s.colors[j]; s.colors[j] = s.colors[j - 1]; s.colors[j - 1] = t; } | |
| } | |
| const long phase_off = long(rng() * 17) - 8; | |
| for (int k = 0; k < Sp::cells; ++k) { | |
| auto c = Sp::coord(k); | |
| auto u = Sp::centred(c); | |
| // section of the quotient: every orbit resolves to one representative, | |
| // so the grid carries the symmetry exactly, with no fold pass | |
| std::array<long, N> best = u; | |
| for (const auto& e : B<N>) { | |
| if (!Cfg::Sym::template member<N>(e)) continue; | |
| auto v = e(u); | |
| for (int i = 0; i < N; ++i) { if (v[i] < best[i]) { best = v; break; } if (v[i] > best[i]) break; } | |
| } | |
| for (int i = 0; i < N; ++i) best[i] = best[i] * Cfg::stride + phase_off; | |
| long n = Cfg::Field::template f<N>(best); | |
| long vm = ((n % Cfg::modulus) + Cfg::modulus) % Cfg::modulus; | |
| double t = double(vm) / double(Cfg::modulus); | |
| if (t >= double(Cfg::coverage_pct) / 100.0) { s.grid[k] = 0; continue; } | |
| int band = int(t / (double(Cfg::coverage_pct) / 100.0) * Cfg::ncol); | |
| s.grid[k] = std::uint8_t(1 + (band < Cfg::ncol ? band : Cfg::ncol - 1)); | |
| } | |
| return s; | |
| } | |
| // ───────────────────────────────────────────────── the rasteriser, rank-N | |
| // Coverage is separable: a = product over axes of the 1-D overlap. Walk<N,Axis> | |
| // unrolls that product into a compile-time loop nest, which is the entirety of | |
| // what ng §9 means by "the n-dimensional generalisation replaces the two nested | |
| // loops with a recursive walk accumulating a = prod f_i. Nothing else changes." | |
| // Splat<N,Axis> composites one cell into the buffer. The recursion is over | |
| // axes, not pixels: at each level it multiplies in that axis's 1-D overlap and | |
| // descends, so the terminal case receives a = prod f_i already formed. In two | |
| // dimensions this is the familiar doubly-nested loop; in three it is the same | |
| // function with one more instantiation. That equivalence is ng §9's argument. | |
| // | |
| // The buffer is threaded as a pointer rather than captured in a lambda: GCC 13 | |
| // ICEs on constexpr evaluation of a mutating by-reference capture over an array | |
| // member, and depending on a compiler bug is not a design. | |
| template <int N, int Axis> | |
| struct Splat { | |
| // lo and num are in units of 1/den, so every overlap below is an integer | |
| template <class Op, class Buf> | |
| static constexpr void go(const std::array<long, N>& lo, long num, long den, | |
| const std::array<int, N>& dev, std::array<int, N>& idx, | |
| double acc, Buf& buf, double sr, double sg, double sb) { | |
| const int a = imax(0, idiv_floor(lo[Axis], den)); | |
| const int b = imin(dev[Axis], idiv_ceil(lo[Axis] + num, den)); | |
| for (int i = a; i < b; ++i) { | |
| const long ov = ispan(lo[Axis], lo[Axis] + num, long(i) * den, long(i + 1) * den); | |
| if (ov <= 0) continue; | |
| idx[Axis] = i; | |
| Splat<N, Axis + 1>::template go<Op>(lo, num, den, dev, idx, | |
| acc * (double(ov) / double(den)), buf, sr, sg, sb); | |
| } | |
| } | |
| }; | |
| template <int N> | |
| struct Splat<N, N> { | |
| template <class Op, class Buf> | |
| static constexpr void go(const std::array<long, N>&, long, long, | |
| const std::array<int, N>& dev, std::array<int, N>& idx, | |
| double acc, Buf& buf, double sr, double sg, double sb) { | |
| std::size_t k = 0; | |
| for (int i = N - 1; i >= 0; --i) k = k * std::size_t(dev[i]) + std::size_t(idx[i]); | |
| buf[k] = Op::apply(buf[k], sr, sg, sb, acc); | |
| } | |
| }; | |
| // Phase applies to where a sprite is PLACED, never to the cells inside it. | |
| // Snapping each cell would round away the fractional offsets that are the seam | |
| // field, which is the entire effect — the sprite would render as if at integer | |
| // scale. Local snaps the sprite's origin so it rasterises identically wherever | |
| // it sits; Global leaves it, and the texture flows across a sheet. ng §3. | |
| enum class Phase { Global, Local }; | |
| template <class ScaleT, class OpT, class OrderT, class SpaceT, | |
| Phase Ph = Phase::Local, int MatteR = -1, int MatteG = 0, int MatteB = 0> | |
| struct Policy { | |
| using S = ScaleT; using Op = OpT; using Order = OrderT; using Space = SpaceT; | |
| static constexpr Phase phase = Ph; | |
| static constexpr bool matted = MatteR >= 0; | |
| static constexpr RGB matte { std::uint8_t(MatteR < 0 ? 0 : MatteR), | |
| std::uint8_t(MatteG), std::uint8_t(MatteB) }; | |
| static constexpr double p = ScaleT::value; | |
| static constexpr int q = ScaleT::q; | |
| }; | |
| template <int N> struct Raster { | |
| std::array<int, N> dev{}; | |
| std::vector<Px> px; // premultiplied, working space | |
| constexpr int total() const { int t = 1; for (int i = 0; i < N; ++i) t *= dev[i]; return t; } | |
| }; | |
| // The output contract (ng §4): the kernel accumulates premultiplied colour in | |
| // the working space and converts nothing. resolve() is the single boundary | |
| // step, and it unpremultiplies *before* applying the transfer curve because | |
| // both operations are defined only on unpremultiplied colour. | |
| template <class Space> | |
| constexpr std::array<std::uint8_t, 4> resolve_px(Px v) { | |
| if (v.a <= 1e-9) return { 0, 0, 0, 0 }; | |
| double r = v.r / v.a, g = v.g / v.a, b = v.b / v.a; // 1. unpremultiply | |
| if constexpr (Space::linear) { // 2. transfer curve | |
| r = Space::decode(r); g = Space::decode(g); b = Space::decode(b); | |
| } | |
| auto q8 = [](double c) { return std::uint8_t(c < 0 ? 0 : (c > 255 ? 255 : int(c + 0.5))); }; | |
| return { q8(r), q8(g), q8(b), q8(v.a * 255.0) }; | |
| } | |
| template <class Pol, class Sp> | |
| Raster<Sp::rank> rasterize(const Sp& s, std::array<double, Sp::rank> origin = {}) { | |
| constexpr int N = Sp::rank; | |
| using Op = typename Pol::Op; using Space = typename Pol::Space; | |
| constexpr long num = Pol::S::num, den = Pol::S::den; | |
| std::array<long, N> org{}; // in units of 1/den | |
| for (int i = 0; i < N; ++i) org[i] = long(origin[i] * den + (origin[i] < 0 ? -0.5 : 0.5)); | |
| if constexpr (Pol::phase == Phase::Local) | |
| for (int i = 0; i < N; ++i) org[i] = long(idiv_floor(org[i] + den / 2, den)) * den; | |
| Raster<N> out; | |
| for (int i = 0; i < N; ++i) out.dev[i] = idiv_ceil(org[i] + long(Sp::dim.n[i]) * num, den); | |
| out.px.assign(std::size_t(out.total()), Px{}); | |
| if constexpr (Pol::matted) { | |
| Px m{ Space::encode(Pol::matte.r), Space::encode(Pol::matte.g), | |
| Space::encode(Pol::matte.b), 1.0 }; | |
| for (auto& v : out.px) v = m; | |
| } | |
| // traversal order: a permutation of cell indices, chosen at compile time | |
| std::vector<int> seq(std::size_t(Sp::cells)); | |
| for (int i = 0; i < Sp::cells; ++i) seq[std::size_t(i)] = i; | |
| { | |
| std::vector<long> key(std::size_t(Sp::cells)); | |
| for (int i = 0; i < Sp::cells; ++i) { | |
| auto c = Sp::coord(i); | |
| if constexpr (std::is_same_v<typename Pol::Order, ByField>) | |
| key[std::size_t(i)] = Sp::field_at(c); // Φ's function, not a copy of it | |
| else | |
| key[std::size_t(i)] = Pol::Order::template key<N>(c, Sp::dim.n); | |
| } | |
| // stable insertion sort: order must be deterministic, and ties must not | |
| // depend on the sort's internals (the CA work showed scan-order ties | |
| // silently destroy equivariance) | |
| for (std::size_t i = 1; i < seq.size(); ++i) | |
| for (std::size_t j = i; j > 0 && key[std::size_t(seq[j])] < key[std::size_t(seq[j - 1])]; --j) { | |
| int t = seq[j]; seq[j] = seq[j - 1]; seq[j - 1] = t; | |
| } | |
| } | |
| for (int ci : seq) { | |
| const std::uint8_t v = s.grid[std::size_t(ci)]; | |
| if (!v) continue; | |
| const RGB col = s.colors[std::size_t(v - 1)]; | |
| const double sr = Space::encode(col.r), sg = Space::encode(col.g), sb = Space::encode(col.b); | |
| auto c = Sp::coord(ci); | |
| std::array<long, N> lo{}; | |
| for (int i = 0; i < N; ++i) lo[i] = org[i] + long(c[i]) * num; | |
| std::array<int, N> idx{}; | |
| Splat<N, 0>::template go<Op>(lo, num, den, out.dev, idx, 1.0, out.px, sr, sg, sb); | |
| } | |
| return out; | |
| } | |
| // ───────────────────────────────────────────── constexpr path: one pixel | |
| // The closed forms in aamazing.md §3 and §6 are claims about a single device | |
| // pixel, so the compile-time entry point is a single device pixel. sample() | |
| // walks the cells in traversal order and composites those whose box overlaps | |
| // the requested pixel — the same arithmetic as rasterize(), without allocating | |
| // a buffer, which keeps it usable in a constant expression. | |
| // | |
| // (It is O(cells) per pixel. That is the right trade for an oracle: the test | |
| // suite checks a handful of pixels, and the whole-image path is rasterize().) | |
| template <class Pol, class Sp> | |
| constexpr Px sample(const Sp& s, const std::array<int, Sp::rank>& q, | |
| std::array<double, Sp::rank> origin = {}) { | |
| constexpr int N = Sp::rank; | |
| using Op = typename Pol::Op; | |
| using Space = typename Pol::Space; | |
| constexpr long num = Pol::S::num, den = Pol::S::den; | |
| std::array<long, N> org{}; | |
| for (int i = 0; i < N; ++i) org[i] = long(origin[i] * den + (origin[i] < 0 ? -0.5 : 0.5)); | |
| if constexpr (Pol::phase == Phase::Local) | |
| for (int i = 0; i < N; ++i) org[i] = long(idiv_floor(org[i] + den / 2, den)) * den; | |
| Px acc{}; | |
| if constexpr (Pol::matted) | |
| acc = Px{ Space::encode(Pol::matte.r), Space::encode(Pol::matte.g), | |
| Space::encode(Pol::matte.b), 1.0 }; | |
| // traversal order: visit cells by ascending key, ties by index. Selection | |
| // rather than a sort, so no scratch storage is needed. | |
| long last_key = 0; int last_idx = -1; | |
| for (int drawn = 0; drawn < Sp::cells; ++drawn) { | |
| long best_key = 0; int best = -1; | |
| for (int ci = 0; ci < Sp::cells; ++ci) { | |
| auto c = Sp::coord(ci); | |
| long k = 0; | |
| if constexpr (std::is_same_v<typename Pol::Order, ByField>) k = Sp::field_at(c); | |
| else k = Pol::Order::template key<N>(c, Sp::dim.n); | |
| if (last_idx >= 0 && (k < last_key || (k == last_key && ci <= last_idx))) continue; | |
| if (best < 0 || k < best_key) { best_key = k; best = ci; } | |
| } | |
| if (best < 0) break; | |
| last_key = best_key; last_idx = best; | |
| const std::uint8_t v = s.grid[std::size_t(best)]; | |
| if (!v) continue; | |
| const RGB col = s.colors[std::size_t(v - 1)]; | |
| auto c = Sp::coord(best); | |
| double a = 1.0; | |
| for (int i = 0; i < N && a > 0; ++i) { | |
| const long lo = org[i] + long(c[i]) * num; | |
| a *= double(ispan(lo, lo + num, long(q[i]) * den, long(q[i] + 1) * den)) / double(den); | |
| } | |
| if (a <= 0) continue; | |
| acc = Op::apply(acc, Space::encode(col.r), Space::encode(col.g), Space::encode(col.b), a); | |
| } | |
| return acc; | |
| } | |
| } // namespace mosprite |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment