Crafter.Math/interfaces/Crafter.Math-MatrixRowMajor.cppm

444 lines
22 KiB
Text
Raw Normal View History

2026-07-22 18:01:26 +02:00
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
2026-05-18 18:03:20 +02:00
export module Crafter.Math:MatrixRowMajor;
import :Basic;
import :Vector;
2026-05-18 18:03:20 +02:00
import :VectorF32;
import std;
namespace Crafter {
// Row-major matrix whose rows live in optimized SIMD vectors. All
// multiplications are expressed as broadcast + fused multiply-add against
// these row vectors so the heavy work stays in __m128/__m256/__m512 land.
//
// CollumSize is the column count; only CollumSize == 4 is implemented (the
// matrix gets one SIMD row per row). Repeats is reserved for future
// SoA-style batching.
export template <typename T, std::uint32_t CollumSize, std::uint32_t RowSize, std::uint32_t Repeats>
class MatrixRowMajor {
public:
// Rows are exposed publicly so users can compose with VectorF32 ops
// directly without going through an accessor. Each row is a single
// SIMD vector covering all columns.
VectorF32<static_cast<std::uint8_t>(CollumSize), 1> rows[RowSize];
MatrixRowMajor() = default;
MatrixRowMajor(
float x0, float y0, float z0, float w0,
float x1, float y1, float z1, float w1,
float x2, float y2, float z2, float w2,
float x3, float y3, float z3, float w3
) requires(CollumSize == 4 && RowSize == 4 && Repeats == 1 && std::same_as<T, float>) {
alignas(16) float r0[4] = { x0, y0, z0, w0 };
alignas(16) float r1[4] = { x1, y1, z1, w1 };
alignas(16) float r2[4] = { x2, y2, z2, w2 };
alignas(16) float r3[4] = { x3, y3, z3, w3 };
rows[0] = VectorF32<4, 1>(r0);
rows[1] = VectorF32<4, 1>(r1);
rows[2] = VectorF32<4, 1>(r2);
rows[3] = VectorF32<4, 1>(r3);
}
MatrixRowMajor(
float x0, float y0, float z0, float w0,
float x1, float y1, float z1, float w1,
float x2, float y2, float z2, float w2
) requires(CollumSize == 4 && RowSize == 3 && Repeats == 1 && std::same_as<T, float>) {
alignas(16) float r0[4] = { x0, y0, z0, w0 };
alignas(16) float r1[4] = { x1, y1, z1, w1 };
alignas(16) float r2[4] = { x2, y2, z2, w2 };
rows[0] = VectorF32<4, 1>(r0);
rows[1] = VectorF32<4, 1>(r1);
rows[2] = VectorF32<4, 1>(r2);
}
// Flatten to RowSize*CollumSize contiguous floats (row-major). Replaces
// the old `m[i][j]` raw array access for callers that need a packed
// float buffer (e.g. GPU upload via memcpy).
constexpr void Store(float* dst) const requires(CollumSize == 4 && Repeats == 1 && std::same_as<T, float>) {
for (std::uint32_t i = 0; i < RowSize; ++i) {
rows[i].Store(dst + i * 4);
}
}
constexpr std::array<float, RowSize * 4> Store() const requires(CollumSize == 4 && Repeats == 1 && std::same_as<T, float>) {
std::array<float, RowSize * 4> out{};
Store(out.data());
return out;
}
// Affine transform: extend `b` with implicit w=1 (translation) and dot
// each row against it. Three row-dots packed into one batched 4-pair
// Dot call (lane 3 of the result is the duplicated row 0 and gets
// discarded).
VectorF32<3, 1> operator*(VectorF32<3, 1> b) const requires(CollumSize == 4 && RowSize == 3 && Repeats == 1 && std::same_as<T, float>) {
std::array<float, 4> bArr = b.template Store<float>();
alignas(16) float bhBuf[4] = { bArr[0], bArr[1], bArr[2], 1.0f };
VectorF32<4, 1> bh(bhBuf);
VectorF32<1, 4> dots = VectorF32<4, 1>::Dot(
rows[0], bh, rows[1], bh, rows[2], bh, rows[0], bh);
std::array<float, 4> dotsArr = dots.template Store<float>();
alignas(16) float outBuf[4] = { dotsArr[0], dotsArr[1], dotsArr[2], 0.0f };
return VectorF32<3, 1>(outBuf);
}
// Linear transform (no translation): same as the affine version but
// with bh.w = 0 so the translation column does not contribute. Useful
// for direction vectors and normals.
VectorF32<3, 1> TransformNormal(VectorF32<3, 1> b) const requires(CollumSize == 4 && RowSize == 3 && Repeats == 1 && std::same_as<T, float>) {
std::array<float, 4> bArr = b.template Store<float>();
alignas(16) float bhBuf[4] = { bArr[0], bArr[1], bArr[2], 0.0f };
VectorF32<4, 1> bh(bhBuf);
VectorF32<1, 4> dots = VectorF32<4, 1>::Dot(
rows[0], bh, rows[1], bh, rows[2], bh, rows[0], bh);
std::array<float, 4> dotsArr = dots.template Store<float>();
alignas(16) float outBuf[4] = { dotsArr[0], dotsArr[1], dotsArr[2], 0.0f };
return VectorF32<3, 1>(outBuf);
}
// 4×4 matrix product via broadcast + FMA. Each result row is
// b[i][0]·rows[0] + b[i][1]·rows[1] + b[i][2]·rows[2] + b[i][3]·rows[3]
// produced with four shuffle-broadcasts and three fused multiply-adds.
MatrixRowMajor operator*(MatrixRowMajor b) const requires(CollumSize == 4 && RowSize == 4 && Repeats == 1 && std::same_as<T, float>) {
MatrixRowMajor result;
for (std::uint32_t i = 0; i < 4; ++i) {
VectorF32<4, 1> bi = b.rows[i];
VectorF32<4, 1> bx = bi.template Shuffle<{{0, 0, 0, 0}}>();
VectorF32<4, 1> by = bi.template Shuffle<{{1, 1, 1, 1}}>();
VectorF32<4, 1> bz = bi.template Shuffle<{{2, 2, 2, 2}}>();
VectorF32<4, 1> bw = bi.template Shuffle<{{3, 3, 3, 3}}>();
VectorF32<4, 1> row = bx * rows[0];
row = VectorF32<4, 1>::MulitplyAdd(by, rows[1], row);
row = VectorF32<4, 1>::MulitplyAdd(bz, rows[2], row);
row = VectorF32<4, 1>::MulitplyAdd(bw, rows[3], row);
result.rows[i] = row;
}
return result;
}
// 4×3 affine product. Same broadcast + FMA pattern, but the implicit
// 4th row of both matrices is [0, 0, 0, 1] so the b.w · row3 term
// contributes only to the translation slot.
MatrixRowMajor operator*(MatrixRowMajor b) const requires(CollumSize == 4 && RowSize == 3 && Repeats == 1 && std::same_as<T, float>) {
alignas(16) float wRowBuf[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
VectorF32<4, 1> wRow(wRowBuf);
MatrixRowMajor result;
for (std::uint32_t i = 0; i < 3; ++i) {
VectorF32<4, 1> bi = b.rows[i];
VectorF32<4, 1> bx = bi.template Shuffle<{{0, 0, 0, 0}}>();
VectorF32<4, 1> by = bi.template Shuffle<{{1, 1, 1, 1}}>();
VectorF32<4, 1> bz = bi.template Shuffle<{{2, 2, 2, 2}}>();
VectorF32<4, 1> bw = bi.template Shuffle<{{3, 3, 3, 3}}>();
VectorF32<4, 1> row = bx * rows[0];
row = VectorF32<4, 1>::MulitplyAdd(by, rows[1], row);
row = VectorF32<4, 1>::MulitplyAdd(bz, rows[2], row);
row = VectorF32<4, 1>::MulitplyAdd(bw, wRow, row);
result.rows[i] = row;
}
return result;
}
static MatrixRowMajor Identity() requires(CollumSize == 4 && RowSize == 3 && Repeats == 1 && std::same_as<T, float>) {
return MatrixRowMajor(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0
);
}
static MatrixRowMajor Identity() requires(CollumSize == 4 && RowSize == 4 && Repeats == 1 && std::same_as<T, float>) {
return MatrixRowMajor(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
}
static MatrixRowMajor Scaling(float x, float y, float z) requires(CollumSize == 4 && RowSize == 4 && Repeats == 1 && std::same_as<T, float>) {
return MatrixRowMajor(
x, 0, 0, 0,
0, y, 0, 0,
0, 0, z, 0,
0, 0, 0, 1
);
}
static MatrixRowMajor Scaling(float x, float y, float z) requires(CollumSize == 4 && RowSize == 3 && Repeats == 1 && std::same_as<T, float>) {
return MatrixRowMajor(
x, 0, 0, 0,
0, y, 0, 0,
0, 0, z, 0
);
}
static MatrixRowMajor Scaling(VectorF32<3, 1> vector) requires(CollumSize == 4 && Repeats == 1 && std::same_as<T, float>) {
std::array<float, 4> a = vector.template Store<float>();
return Scaling(a[0], a[1], a[2]);
}
static MatrixRowMajor Translation(float x, float y, float z) requires(CollumSize == 4 && RowSize == 4 && Repeats == 1 && std::same_as<T, float>) {
return MatrixRowMajor(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, z, 1
);
}
static MatrixRowMajor Translation(float x, float y, float z) requires(CollumSize == 4 && RowSize == 3 && Repeats == 1 && std::same_as<T, float>) {
return MatrixRowMajor(
1, 0, 0, x,
0, 1, 0, y,
0, 0, 1, z
);
}
static MatrixRowMajor Translation(VectorF32<3, 1> vector) requires(CollumSize == 4 && Repeats == 1 && std::same_as<T, float>) {
std::array<float, 4> a = vector.template Store<float>();
return Translation(a[0], a[1], a[2]);
}
// Pitch/yaw/roll Euler rotation. Computes all three sin/cos pairs as a
// single batched SinCos on a VectorF32<3, 1>, then assembles the rows.
static MatrixRowMajor Rotation(float Pitch, float Yaw, float Roll) requires(CollumSize == 4 && RowSize == 4 && Repeats == 1 && std::same_as<T, float>) {
alignas(16) float angles[4] = { Pitch, Yaw, Roll, 0.0f };
VectorF32<3, 1> v(angles);
std::tuple<VectorF32<3, 1>, VectorF32<3, 1>> sc = v.SinCos();
std::array<float, 4> s = std::get<0>(sc).template Store<float>();
std::array<float, 4> c = std::get<1>(sc).template Store<float>();
const float sp = s[0], cp = c[0];
const float sy = s[1], cy = c[1];
const float sr = s[2], cr = c[2];
return MatrixRowMajor(
cr * cy + sr * sp * sy, sr * cp, sr * sp * cy - cr * sy, 0.0f,
cr * sp * sy - sr * cy, cr * cp, sr * sy + cr * sp * cy, 0.0f,
cp * sy, -sp, cp * cy, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
);
}
static MatrixRowMajor Rotation(float Pitch, float Yaw, float Roll) requires(CollumSize == 4 && RowSize == 3 && Repeats == 1 && std::same_as<T, float>) {
alignas(16) float angles[4] = { Pitch, Yaw, Roll, 0.0f };
VectorF32<3, 1> v(angles);
std::tuple<VectorF32<3, 1>, VectorF32<3, 1>> sc = v.SinCos();
std::array<float, 4> s = std::get<0>(sc).template Store<float>();
std::array<float, 4> c = std::get<1>(sc).template Store<float>();
const float sp = s[0], cp = c[0];
const float sy = s[1], cy = c[1];
const float sr = s[2], cr = c[2];
// The 4x4's rows transposed: the 4x4 multiplies row vectors and the
// 4x3 column vectors, so the same rows here would rotate the other
// way.
2026-05-18 18:03:20 +02:00
return MatrixRowMajor(
cr * cy + sr * sp * sy, cr * sp * sy - sr * cy, cp * sy, 0.0f,
sr * cp, cr * cp, -sp, 0.0f,
sr * sp * cy - cr * sy, sr * sy + cr * sp * cy, cp * cy, 0.0f
2026-05-18 18:03:20 +02:00
);
}
static MatrixRowMajor Rotation(VectorF32<3, 1> v) requires(CollumSize == 4 && Repeats == 1 && std::same_as<T, float>) {
std::array<float, 4> a = v.template Store<float>();
return Rotation(a[0], a[1], a[2]);
}
// View matrix: builds the basis from a forward (negated) direction and
// an up reference, then dots each basis vector with -eye for the
// translation column. The four dots needed are produced by a single
// batched 4-pair Dot.
static MatrixRowMajor LookTo(VectorF32<3, 1> eyePosition, VectorF32<3, 1> eyeDirection, VectorF32<3, 1> upDirection) requires(CollumSize == 4 && RowSize == 4 && Repeats == 1 && std::same_as<T, float>) {
// R0 = up × R2 is linear in R2, so its normalized direction does
// not depend on whether we hand R2 in before or after its own
// normalize. Computing R0_raw from the un-normalized R2 lets us
2026-05-18 19:57:40 +02:00
// satisfy the VectorF32<3,1>::BatchSize Normalize requirement with
// one batched call (duplicating R2 and R0 in the padding slots).
2026-05-18 18:03:20 +02:00
VectorF32<3, 1> R0Raw = VectorF32<3, 1>::Cross(upDirection, eyeDirection);
auto normalized = VectorF32<3, 1>::Normalize(eyeDirection, R0Raw, eyeDirection, R0Raw);
2026-05-18 19:57:40 +02:00
VectorF32<3, 1> R2 = normalized[0];
VectorF32<3, 1> R0 = normalized[1];
2026-05-18 18:03:20 +02:00
VectorF32<3, 1> R1 = VectorF32<3, 1>::Cross(R2, R0);
VectorF32<3, 1> negEye = -eyePosition;
VectorF32<1, 4> dots = VectorF32<3, 1>::Dot(
R0, negEye, R1, negEye, R2, negEye, R0, negEye);
std::array<float, 4> d = dots.template Store<float>();
std::array<float, 4> r0a = R0.template Store<float>();
std::array<float, 4> r1a = R1.template Store<float>();
std::array<float, 4> r2a = R2.template Store<float>();
return MatrixRowMajor(
r0a[0], r1a[0], r2a[0], 0.0f,
r0a[1], r1a[1], r2a[1], 0.0f,
r0a[2], r1a[2], r2a[2], 0.0f,
d[0], d[1], d[2], 1.0f
);
}
static MatrixRowMajor LookAt(VectorF32<3, 1> eyePosition, VectorF32<3, 1> focusPosition, VectorF32<3, 1> upDirection) requires(CollumSize == 4 && RowSize == 4 && Repeats == 1 && std::same_as<T, float>) {
return LookTo(eyePosition, eyePosition - focusPosition, upDirection);
}
// ─── Quaternions, TRS and the affine inverse ───────────────────
// Each call means the same transform in both shapes: the 4x4 takes
// row vectors (v * M, translation in row 3) and the 4x3 column
// vectors (M * v, translation in column 3, the VkTransformMatrixKHR
// layout), so the 4x4's upper 3x3 is the 4x3's transposed. These are
// scalar code: they build matrices, they don't sit in inner loops.
// The rotation by unit quaternion q (x, y, z, w) that Vector::Rotate
// applies.
template <std::uint32_t QAlignment>
static MatrixRowMajor Rotation(Vector<float, 4, QAlignment> q) requires(CollumSize == 4 && (RowSize == 3 || RowSize == 4) && Repeats == 1 && std::same_as<T, float>) {
return TRS(Vector<float, 3, 0>(0, 0, 0), q, Vector<float, 3, 0>(1, 1, 1));
}
// Scale by s, then rotate by unit quaternion q, then translate by t:
// a glTF node's transform.
template <std::uint32_t TAlignment, std::uint32_t QAlignment, std::uint32_t SAlignment>
static MatrixRowMajor TRS(Vector<float, 3, TAlignment> t, Vector<float, 4, QAlignment> q, Vector<float, 3, SAlignment> s) requires(CollumSize == 4 && (RowSize == 3 || RowSize == 4) && Repeats == 1 && std::same_as<T, float>) {
const float xx = q.x * q.x, yy = q.y * q.y, zz = q.z * q.z;
const float xy = q.x * q.y, xz = q.x * q.z, yz = q.y * q.z;
const float wx = q.w * q.x, wy = q.w * q.y, wz = q.w * q.z;
// r[i][j]: q's rotation matrix in column-vector form.
const float r[3][3] = {
{ 1 - 2 * (yy + zz), 2 * (xy - wz), 2 * (xz + wy) },
{ 2 * (xy + wz), 1 - 2 * (xx + zz), 2 * (yz - wx) },
{ 2 * (xz - wy), 2 * (yz + wx), 1 - 2 * (xx + yy) },
};
const float sc[3] = { s.x, s.y, s.z };
if constexpr (RowSize == 3) {
// Column j is basis vector j, scaled and rotated.
return MatrixRowMajor(
r[0][0] * sc[0], r[0][1] * sc[1], r[0][2] * sc[2], t.x,
r[1][0] * sc[0], r[1][1] * sc[1], r[1][2] * sc[2], t.y,
r[2][0] * sc[0], r[2][1] * sc[1], r[2][2] * sc[2], t.z
);
} else {
// Row i is basis vector i, scaled and rotated.
return MatrixRowMajor(
r[0][0] * sc[0], r[1][0] * sc[0], r[2][0] * sc[0], 0,
r[0][1] * sc[1], r[1][1] * sc[1], r[2][1] * sc[1], 0,
r[0][2] * sc[2], r[1][2] * sc[2], r[2][2] * sc[2], 0,
t.x, t.y, t.z, 1
);
}
}
// The inverse of an affine transform: any rotation, scale or shear
// plus a translation, which is everything TRS builds. Not valid for a
// projection.
MatrixRowMajor InverseAffine() const requires(CollumSize == 4 && (RowSize == 3 || RowSize == 4) && Repeats == 1 && std::same_as<T, float>) {
const std::array<float, RowSize * 4> a = Store();
// l[i][j] and t: the linear part and the translation, column-vector form.
float l[3][3];
float t[3];
for (std::uint32_t i = 0; i < 3; ++i) {
for (std::uint32_t j = 0; j < 3; ++j) {
l[i][j] = RowSize == 3 ? a[i * 4 + j] : a[j * 4 + i];
}
t[i] = RowSize == 3 ? a[i * 4 + 3] : a[12 + i];
}
// n = l^-1 through the adjugate.
const float c00 = l[1][1] * l[2][2] - l[1][2] * l[2][1];
const float c01 = l[1][2] * l[2][0] - l[1][0] * l[2][2];
const float c02 = l[1][0] * l[2][1] - l[1][1] * l[2][0];
const float invDet = 1.0f / (l[0][0] * c00 + l[0][1] * c01 + l[0][2] * c02);
const float n[3][3] = {
{ c00 * invDet, (l[0][2] * l[2][1] - l[0][1] * l[2][2]) * invDet, (l[0][1] * l[1][2] - l[0][2] * l[1][1]) * invDet },
{ c01 * invDet, (l[0][0] * l[2][2] - l[0][2] * l[2][0]) * invDet, (l[0][2] * l[1][0] - l[0][0] * l[1][2]) * invDet },
{ c02 * invDet, (l[0][1] * l[2][0] - l[0][0] * l[2][1]) * invDet, (l[0][0] * l[1][1] - l[0][1] * l[1][0]) * invDet },
};
// The inverse translates by -n * t.
float nt[3];
for (std::uint32_t i = 0; i < 3; ++i) {
nt[i] = -(n[i][0] * t[0] + n[i][1] * t[1] + n[i][2] * t[2]);
}
if constexpr (RowSize == 3) {
return MatrixRowMajor(
n[0][0], n[0][1], n[0][2], nt[0],
n[1][0], n[1][1], n[1][2], nt[1],
n[2][0], n[2][1], n[2][2], nt[2]
);
} else {
return MatrixRowMajor(
n[0][0], n[1][0], n[2][0], 0,
n[0][1], n[1][1], n[2][1], 0,
n[0][2], n[1][2], n[2][2], 0,
nt[0], nt[1], nt[2], 1
);
}
}
MatrixRowMajor Transpose() const requires(CollumSize == 4 && RowSize == 4 && Repeats == 1 && std::same_as<T, float>) {
const std::array<float, 16> a = Store();
return MatrixRowMajor(
a[0], a[4], a[8], a[12],
a[1], a[5], a[9], a[13],
a[2], a[6], a[10], a[14],
a[3], a[7], a[11], a[15]
);
}
// Splits a non-degenerate affine transform into the translation,
// rotation (unit quaternion) and scale that TRS builds it from. Shear
// is not represented. A mirroring transform (negative determinant) has
// no unique split; it comes back as a negative X scale, which TRS
// turns into the same transform.
std::tuple<Vector<float, 3, 0>, Vector<float, 4, 0>, Vector<float, 3, 0>> Decompose() const requires(CollumSize == 4 && (RowSize == 3 || RowSize == 4) && Repeats == 1 && std::same_as<T, float>) {
const std::array<float, RowSize * 4> a = Store();
// basis[j]: where the transform takes unit vector j.
Vector<float, 3, 0> basis[3];
for (std::uint32_t j = 0; j < 3; ++j) {
basis[j] = RowSize == 3
? Vector<float, 3, 0>(a[j], a[4 + j], a[8 + j])
: Vector<float, 3, 0>(a[j * 4], a[j * 4 + 1], a[j * 4 + 2]);
}
const Vector<float, 3, 0> t = RowSize == 3
? Vector<float, 3, 0>(a[3], a[7], a[11])
: Vector<float, 3, 0>(a[12], a[13], a[14]);
Vector<float, 3, 0> s(basis[0].Length(), basis[1].Length(), basis[2].Length());
if (Vector<float, 3, 0>::Dot(Vector<float, 3, 0>::Cross(basis[0], basis[1]), basis[2]) < 0) {
s.x = -s.x;
}
const Vector<float, 4, 0> q = Vector<float, 4, 0>::QuanternionFromBasis(basis[0] / s.x, basis[1] / s.y, basis[2] / s.z);
return { t, q, s };
}
2026-05-18 18:03:20 +02:00
};
}
// Pretty printer using Store() so it does not depend on the legacy m[i][j]
// access pattern.
template <>
struct std::formatter<Crafter::MatrixRowMajor<float, 4, 4, 1>> : std::formatter<std::string> {
auto format(const Crafter::MatrixRowMajor<float, 4, 4, 1>& obj, format_context& ctx) const {
std::array<float, 16> v = obj.Store();
return std::formatter<std::string>::format(std::format(
"{{{}, {}, {}, {}\n{}, {}, {}, {}\n{}, {}, {}, {}\n{}, {}, {}, {}}}",
v[0], v[1], v[2], v[3],
v[4], v[5], v[6], v[7],
v[8], v[9], v[10], v[11],
v[12], v[13], v[14], v[15]
), ctx);
}
};
template <>
struct std::formatter<Crafter::MatrixRowMajor<float, 4, 3, 1>> : std::formatter<std::string> {
auto format(const Crafter::MatrixRowMajor<float, 4, 3, 1>& obj, format_context& ctx) const {
std::array<float, 12> v = obj.Store();
return std::formatter<std::string>::format(std::format(
"{{{}, {}, {}, {}\n{}, {}, {}, {}\n{}, {}, {}, {}}}",
v[0], v[1], v[2], v[3],
v[4], v[5], v[6], v[7],
v[8], v[9], v[10], v[11]
), ctx);
}
};