From 2ce77898c9f52b7ff8d74088cfb8800322a04980 Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sat, 17 Oct 2026 13:27:49 +0200 Subject: [PATCH 1/2] Construct matrices and iterate without heap allocations Matrix{2,3,4}.init(_ column0, ...) built an Array of columns and passed it to Storage(columns), so every matrix construction allocated, including every non-SIMD matrix multiply. init(_ values:) sliced into arrays as well, and iteration allocated through flatMap'd arrays (non-SIMD storages), [x, y, z, w] arrays (simd quaternions) and AnyIterator (`elements`). - Add init(_ column0:, _ column1:, ...) as a Storage*Protocol requirement. The simd matrix types already provide it; the non-SIMD storages implement it, and their array initializers delegate to it. - Matrix column/array/values initializers go through the column initializer; no intermediate arrays. - Non-SIMD storages use the default IndexingIterator over themselves instead of building an array; simd quaternions iterate their vector via SIMDScalarIterator. - Matrix `elements` is Array(storage); SIMD `elements` maps indices instead of going through AnyIterator. Non-SIMD 4x4 multiply in an external release-mode consumer: 156 ms -> 22 ms for 5M multiplies. The SIMD path is unchanged. Tests: MatrixConstructionTests checks that all construction paths agree and produce column-major layout, and that matrices, quaternions and vectors iterate in order. Claude-Session: https://claude.ai/code/session_011WSUtoLptjea3s3iC5wTNC --- Sources/FirebladeMath/Matrix/Matrix2x2.swift | 12 +- Sources/FirebladeMath/Matrix/Matrix3x3.swift | 14 ++- Sources/FirebladeMath/Matrix/Matrix4x4.swift | 16 +-- .../Matrix/MatrixStorage+NO_SIMD.swift | 51 ++++---- .../FirebladeMath/Matrix/MatrixStorage.swift | 18 +++ .../Quat/QuaternionStorage+SIMD.swift | 8 +- .../Vector/SIMD+Extensions.swift | 2 +- .../MatrixConstructionTests.swift | 117 ++++++++++++++++++ 8 files changed, 188 insertions(+), 50 deletions(-) create mode 100644 Tests/FirebladeMathTests/MatrixConstructionTests.swift diff --git a/Sources/FirebladeMath/Matrix/Matrix2x2.swift b/Sources/FirebladeMath/Matrix/Matrix2x2.swift index f19d02b..16b45a2 100644 --- a/Sources/FirebladeMath/Matrix/Matrix2x2.swift +++ b/Sources/FirebladeMath/Matrix/Matrix2x2.swift @@ -65,7 +65,7 @@ public struct Matrix2x2: RandomAccessCollection, Mu @inlinable public init(_ columns: [Vector]) { precondition(columns.count == 2, "Matrix needs exactly 2 Vector vectors") - storage = Storage(columns) + self.init(columns[0], columns[1]) } /// Creates a new 2x2 matrix from two column vectors. @@ -74,7 +74,7 @@ public struct Matrix2x2: RandomAccessCollection, Mu /// - column1: The second column vector. @inlinable public init(_ column0: Vector, _ column1: Vector) { - self.init([column0, column1]) + storage = Storage(column0, column1) } /// Creates a new 2x2 matrix from an array of values in column-major order. @@ -82,8 +82,10 @@ public struct Matrix2x2: RandomAccessCollection, Mu @inlinable public init(_ values: [Storage.Value]) { precondition(values.count == 4, "Matrix needs exactly 4 values") - self.init([Vector(values[0...1]), - Vector(values[2...3])]) + self.init( + Vector(values[0], values[1]), + Vector(values[2], values[3]) + ) } /// Accesses the value at the specified flat index in column-major order. @@ -117,7 +119,7 @@ public struct Matrix2x2: RandomAccessCollection, Mu /// An array containing the elements of the matrix in column-major order. @inlinable public var elements: [Value] { - [Value](AnyIterator(storage.makeIterator())) + Array(storage) } /// Provides access to the underlying memory as a contiguous buffer. diff --git a/Sources/FirebladeMath/Matrix/Matrix3x3.swift b/Sources/FirebladeMath/Matrix/Matrix3x3.swift index af573d8..83c16a6 100644 --- a/Sources/FirebladeMath/Matrix/Matrix3x3.swift +++ b/Sources/FirebladeMath/Matrix/Matrix3x3.swift @@ -65,7 +65,7 @@ public struct Matrix3x3: RandomAccessCollection, Mu @inlinable public init(_ columns: [Vector]) { precondition(columns.count == 3, "Matrix needs exactly 3 Vector vectors") - storage = Storage(columns) + self.init(columns[0], columns[1], columns[2]) } /// Creates a new 3x3 matrix from three column vectors. @@ -75,7 +75,7 @@ public struct Matrix3x3: RandomAccessCollection, Mu /// - column2: The third column vector. @inlinable public init(_ column0: Vector, _ column1: Vector, _ column2: Vector) { - self.init([column0, column1, column2]) + storage = Storage(column0, column1, column2) } /// Creates a new 3x3 matrix from an array of values in column-major order. @@ -83,9 +83,11 @@ public struct Matrix3x3: RandomAccessCollection, Mu @inlinable public init(_ values: [Storage.Value]) { precondition(values.count == 9, "Matrix needs exactly 9 values") - self.init([Vector(values[0...2]), - Vector(values[3...5]), - Vector(values[6...8])]) + self.init( + Vector(values[0], values[1], values[2]), + Vector(values[3], values[4], values[5]), + Vector(values[6], values[7], values[8]) + ) } /// Accesses the value at the specified flat index in column-major order. @@ -119,7 +121,7 @@ public struct Matrix3x3: RandomAccessCollection, Mu /// An array containing the elements of the matrix in column-major order. @inlinable public var elements: [Value] { - [Value](AnyIterator(storage.makeIterator())) + Array(storage) } /// Provides access to the underlying memory as a contiguous buffer. diff --git a/Sources/FirebladeMath/Matrix/Matrix4x4.swift b/Sources/FirebladeMath/Matrix/Matrix4x4.swift index 290d497..1f04a76 100644 --- a/Sources/FirebladeMath/Matrix/Matrix4x4.swift +++ b/Sources/FirebladeMath/Matrix/Matrix4x4.swift @@ -65,7 +65,7 @@ public struct Matrix4x4: RandomAccessCollection, Mu @inlinable public init(_ columns: [Vector]) { precondition(columns.count == 4, "Matrix needs exactly 4 Column vectors") - storage = Storage(columns) + self.init(columns[0], columns[1], columns[2], columns[3]) } /// Creates a new 4x4 matrix from four column vectors. @@ -76,7 +76,7 @@ public struct Matrix4x4: RandomAccessCollection, Mu /// - column3: The fourth column vector. @inlinable public init(_ column0: Vector, _ column1: Vector, _ column2: Vector, _ column3: Vector) { - self.init([column0, column1, column2, column3]) + storage = Storage(column0, column1, column2, column3) } /// Creates a new 4x4 matrix from an array of values in column-major order. @@ -84,10 +84,12 @@ public struct Matrix4x4: RandomAccessCollection, Mu @inlinable public init(_ values: [Value]) { precondition(values.count == 16, "Matrix needs exactly 16 values") - self.init([Vector(values[0...3]), - Vector(values[4...7]), - Vector(values[8...11]), - Vector(values[12...15])]) + self.init( + Vector(values[0], values[1], values[2], values[3]), + Vector(values[4], values[5], values[6], values[7]), + Vector(values[8], values[9], values[10], values[11]), + Vector(values[12], values[13], values[14], values[15]) + ) } /// Accesses the value at the specified flat index in column-major order. @@ -121,7 +123,7 @@ public struct Matrix4x4: RandomAccessCollection, Mu /// An array containing the elements of the matrix in column-major order. @inlinable public var elements: [Value] { - [Value](AnyIterator(storage.makeIterator())) + Array(storage) } /// Provides access to the underlying memory as a contiguous buffer. diff --git a/Sources/FirebladeMath/Matrix/MatrixStorage+NO_SIMD.swift b/Sources/FirebladeMath/Matrix/MatrixStorage+NO_SIMD.swift index 2429b25..5c0cc6a 100644 --- a/Sources/FirebladeMath/Matrix/MatrixStorage+NO_SIMD.swift +++ b/Sources/FirebladeMath/Matrix/MatrixStorage+NO_SIMD.swift @@ -18,8 +18,13 @@ public struct Storage2x2: Storage2x2Protocol { @inlinable public init(_ columns: [Column]) { precondition(columns.count == 2, "Storage needs exactly 2 column vectors") - column0 = columns[0] - column1 = columns[1] + self.init(columns[0], columns[1]) + } + + @inlinable + public init(_ column0: Column, _ column1: Column) { + self.column0 = column0 + self.column1 = column1 } @inlinable @@ -35,12 +40,6 @@ public struct Storage2x2: Storage2x2Protocol { self.init(diagonal: Column(zero, zero)) } - @inlinable - @inline(__always) - public func makeIterator() -> IndexingIterator<[Value]> { - [column0, column1].flatMap { $0 }.makeIterator() - } - @inlinable public subscript(column: Int, row: Int) -> Value { get { @@ -113,9 +112,14 @@ public struct Storage3x3: Storage3x3Protocol { @inlinable public init(_ columns: [Column]) { precondition(columns.count == 3, "Storage needs exactly 3 column vectors") - column0 = columns[0] - column1 = columns[1] - column2 = columns[2] + self.init(columns[0], columns[1], columns[2]) + } + + @inlinable + public init(_ column0: Column, _ column1: Column, _ column2: Column) { + self.column0 = column0 + self.column1 = column1 + self.column2 = column2 } @inlinable @@ -132,12 +136,6 @@ public struct Storage3x3: Storage3x3Protocol { self.init(diagonal: Column(zero, zero, zero)) } - @inlinable - @inline(__always) - public func makeIterator() -> IndexingIterator<[Value]> { - [column0, column1, column2].flatMap { $0 }.makeIterator() - } - @inlinable public subscript(column: Int, row: Int) -> Value { get { @@ -220,10 +218,15 @@ public struct Storage4x4: Storage4x4Protocol { @inlinable public init(_ columns: [Column]) { precondition(columns.count == 4, "Storage needs exactly 4 column vectors") - column0 = columns[0] - column1 = columns[1] - column2 = columns[2] - column3 = columns[3] + self.init(columns[0], columns[1], columns[2], columns[3]) + } + + @inlinable + public init(_ column0: Column, _ column1: Column, _ column2: Column, _ column3: Column) { + self.column0 = column0 + self.column1 = column1 + self.column2 = column2 + self.column3 = column3 } @inlinable @@ -241,12 +244,6 @@ public struct Storage4x4: Storage4x4Protocol { self.init(diagonal: Column(zero, zero, zero, zero)) } - @inlinable - @inline(__always) - public func makeIterator() -> IndexingIterator<[Value]> { - [column0, column1, column2, column3].flatMap { $0 }.makeIterator() - } - @inlinable public subscript(column: Int, row: Int) -> Value { get { diff --git a/Sources/FirebladeMath/Matrix/MatrixStorage.swift b/Sources/FirebladeMath/Matrix/MatrixStorage.swift index 727e190..ece660a 100644 --- a/Sources/FirebladeMath/Matrix/MatrixStorage.swift +++ b/Sources/FirebladeMath/Matrix/MatrixStorage.swift @@ -8,6 +8,13 @@ public protocol Storage4x4Protocol: RandomAccessCollection, MutableCollection, R /// Creates a new storage from an array of column vectors. init(_ columns: [Column]) + /// Creates a new storage from 4 column vectors. + /// - Parameters: + /// - column0: The first column vector. + /// - column1: The second column vector. + /// - column2: The third column vector. + /// - column3: The fourth column vector. + init(_ column0: Column, _ column1: Column, _ column2: Column, _ column3: Column) /// Creates a new storage with the specified diagonal values. init(diagonal: Column) @@ -63,6 +70,12 @@ public protocol Storage3x3Protocol: RandomAccessCollection, MutableCollection, R /// Creates a new storage from an array of column vectors. init(_ columns: [Column]) + /// Creates a new storage from 3 column vectors. + /// - Parameters: + /// - column0: The first column vector. + /// - column1: The second column vector. + /// - column2: The third column vector. + init(_ column0: Column, _ column1: Column, _ column2: Column) /// Creates a new storage with the specified diagonal values. init(diagonal: Column) @@ -118,6 +131,11 @@ public protocol Storage2x2Protocol: RandomAccessCollection, MutableCollection, R /// Creates a new storage from an array of column vectors. init(_ columns: [Column]) + /// Creates a new storage from 2 column vectors. + /// - Parameters: + /// - column0: The first column vector. + /// - column1: The second column vector. + init(_ column0: Column, _ column1: Column) /// Creates a new storage with the specified diagonal values. init(diagonal: Column) diff --git a/Sources/FirebladeMath/Quat/QuaternionStorage+SIMD.swift b/Sources/FirebladeMath/Quat/QuaternionStorage+SIMD.swift index 68f072a..60ed982 100644 --- a/Sources/FirebladeMath/Quat/QuaternionStorage+SIMD.swift +++ b/Sources/FirebladeMath/Quat/QuaternionStorage+SIMD.swift @@ -50,8 +50,8 @@ extension simd_quatf: QuaternionStorageProtocol, @retroactive Sequence { /// Creates an iterator over the components of the quaternion. @inlinable @inline(__always) - public func makeIterator() -> IndexingIterator<[Float]> { - [x, y, z, w].makeIterator() + public func makeIterator() -> SIMDScalarIterator> { + SIMDScalarIterator(vector) } } @@ -102,8 +102,8 @@ extension simd_quatd: QuaternionStorageProtocol, @retroactive Sequence { /// Creates an iterator over the components of the quaternion. @inlinable @inline(__always) - public func makeIterator() -> IndexingIterator<[Double]> { - [x, y, z, w].makeIterator() + public func makeIterator() -> SIMDScalarIterator> { + SIMDScalarIterator(vector) } } diff --git a/Sources/FirebladeMath/Vector/SIMD+Extensions.swift b/Sources/FirebladeMath/Vector/SIMD+Extensions.swift index 9f77073..df4c6ad 100644 --- a/Sources/FirebladeMath/Vector/SIMD+Extensions.swift +++ b/Sources/FirebladeMath/Vector/SIMD+Extensions.swift @@ -10,7 +10,7 @@ extension SIMD { /// An array containing the scalar components of the vector. @inlinable public var elements: [Scalar] { - Array(AnyIterator(makeIterator())) + indices.map { self[$0] } } } diff --git a/Tests/FirebladeMathTests/MatrixConstructionTests.swift b/Tests/FirebladeMathTests/MatrixConstructionTests.swift new file mode 100644 index 0000000..debe3d6 --- /dev/null +++ b/Tests/FirebladeMathTests/MatrixConstructionTests.swift @@ -0,0 +1,117 @@ +// +// MatrixConstructionTests.swift +// FirebladeMathTests +// + +// Every way of constructing a matrix must produce the same column-major layout, +// and iterating matrices, quaternions and vectors must yield their scalars in order. + +import FirebladeMath +import Testing + +struct MatrixConstructionTests { + @Test func mat2x2fConstructionPathsAgree() { + let values: [Float] = [1, 2, 3, 4] + let c0 = Vec2f(1, 2), c1 = Vec2f(3, 4) + let byColumns = Mat2x2f(c0, c1) + + #expect(Mat2x2f([c0, c1]) == byColumns) + #expect(Mat2x2f(values) == byColumns) + #expect([1, 2, 3, 4] as Mat2x2f == byColumns) + #expect(byColumns.columns.0 == c0) + #expect(byColumns.columns.1 == c1) + #expect(byColumns.elements == values) + #expect(Array(byColumns) == values) + #expect(byColumns[1, 0] == 3) + } + + @Test func mat2x2dConstructionPathsAgree() { + let values: [Double] = [1, 2, 3, 4] + let c0 = Vec2d(1, 2), c1 = Vec2d(3, 4) + let byColumns = Mat2x2d(c0, c1) + + #expect(Mat2x2d([c0, c1]) == byColumns) + #expect(Mat2x2d(values) == byColumns) + #expect([1, 2, 3, 4] as Mat2x2d == byColumns) + #expect(byColumns.elements == values) + #expect(Array(byColumns) == values) + } + + @Test func mat3x3fConstructionPathsAgree() { + let values: [Float] = (1...9).map(Float.init) + let c0 = Vec3f(1, 2, 3), c1 = Vec3f(4, 5, 6), c2 = Vec3f(7, 8, 9) + let byColumns = Mat3x3f(c0, c1, c2) + + #expect(Mat3x3f([c0, c1, c2]) == byColumns) + #expect(Mat3x3f(values) == byColumns) + #expect([1, 2, 3, 4, 5, 6, 7, 8, 9] as Mat3x3f == byColumns) + #expect(byColumns.columns.0 == c0) + #expect(byColumns.columns.1 == c1) + #expect(byColumns.columns.2 == c2) + #expect(byColumns.elements == values) + #expect(Array(byColumns) == values) + #expect(byColumns[2, 1] == 8) + } + + @Test func mat3x3dConstructionPathsAgree() { + let values: [Double] = (1...9).map(Double.init) + let c0 = Vec3d(1, 2, 3), c1 = Vec3d(4, 5, 6), c2 = Vec3d(7, 8, 9) + let byColumns = Mat3x3d(c0, c1, c2) + + #expect(Mat3x3d([c0, c1, c2]) == byColumns) + #expect(Mat3x3d(values) == byColumns) + #expect(byColumns.elements == values) + #expect(Array(byColumns) == values) + } + + @Test func mat4x4fConstructionPathsAgree() { + let values: [Float] = (1...16).map(Float.init) + let c0 = Vec4f(1, 2, 3, 4), c1 = Vec4f(5, 6, 7, 8), c2 = Vec4f(9, 10, 11, 12), c3 = Vec4f(13, 14, 15, 16) + let byColumns = Mat4x4f(c0, c1, c2, c3) + + #expect(Mat4x4f([c0, c1, c2, c3]) == byColumns) + #expect(Mat4x4f(values) == byColumns) + #expect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] as Mat4x4f == byColumns) + #expect(byColumns.columns.0 == c0) + #expect(byColumns.columns.1 == c1) + #expect(byColumns.columns.2 == c2) + #expect(byColumns.columns.3 == c3) + #expect(byColumns.elements == values) + #expect(Array(byColumns) == values) + #expect(byColumns[3, 2] == 15) + + var iterated: [Float] = [] + for value in byColumns { + iterated.append(value) + } + #expect(iterated == values) + } + + @Test func mat4x4dConstructionPathsAgree() { + let values: [Double] = (1...16).map(Double.init) + let c0 = Vec4d(1, 2, 3, 4), c1 = Vec4d(5, 6, 7, 8), c2 = Vec4d(9, 10, 11, 12), c3 = Vec4d(13, 14, 15, 16) + let byColumns = Mat4x4d(c0, c1, c2, c3) + + #expect(Mat4x4d([c0, c1, c2, c3]) == byColumns) + #expect(Mat4x4d(values) == byColumns) + #expect(byColumns.elements == values) + #expect(Array(byColumns) == values) + } + + @Test func quaternionIteratesComponentsInOrder() { + let qf = Quat4f(1, 2, 3, 4) + let qd = Quat4d(1, 2, 3, 4) + + #expect(Array(qf) == [1, 2, 3, 4]) + #expect(qf.elements == [1, 2, 3, 4]) + #expect(Array(qd) == [1, 2, 3, 4]) + #expect(qd.elements == [1, 2, 3, 4]) + } + + @Test func simdElementsAreInLaneOrder() { + #expect(Vec2f(1, 2).elements == [1, 2]) + #expect(Vec3d(1, 2, 3).elements == [1, 2, 3]) + #expect(Vec4i(1, 2, 3, 4).elements == [1, 2, 3, 4]) + #expect(SIMD8(1, 2, 3, 4, 5, 6, 7, 8).elements == [1, 2, 3, 4, 5, 6, 7, 8]) + } +} From 25e68e4bb2027836c9e8072e557ed006b8cee17e Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sat, 17 Oct 2026 16:26:13 +0200 Subject: [PATCH 2/2] Deprecate the public matrix index helpers (#70) --- Sources/FirebladeMath/Matrix/Matrices.swift | 19 +++++- .../Matrix/MatrixStorage+NO_SIMD.swift | 12 ++-- .../Matrix/MatrixStorage+SIMD.swift | 24 +++---- .../MatrixIndexingTests.swift | 64 +++++++++++++++++++ 4 files changed, 98 insertions(+), 21 deletions(-) create mode 100644 Tests/FirebladeMathTests/MatrixIndexingTests.swift diff --git a/Sources/FirebladeMath/Matrix/Matrices.swift b/Sources/FirebladeMath/Matrix/Matrices.swift index 8d098ba..7c1f6a0 100644 --- a/Sources/FirebladeMath/Matrix/Matrices.swift +++ b/Sources/FirebladeMath/Matrix/Matrices.swift @@ -43,6 +43,9 @@ public typealias Mat2x2d = Matrix2x2> /// - row: The row (zero based). /// - width: The width of a column. /// - Returns: The flat index into the matrix storage. +/// - Important: This computes a **row-major** index, which does not match the column-major +/// storage of FirebladeMath matrices (see ``columnRow(index:width:)``). +@available(*, deprecated, message: "Computes a row-major index that does not match FirebladeMath's column-major storage. Use the matrix's subscript(column:row:) instead.") @inlinable public func index(column: Int, row: Int, width: Int) -> Int { (row * width) + column @@ -53,9 +56,19 @@ public func index(column: Int, row: Int, width: Int) -> Int { /// - index: The index into the matrix. /// - width: The width of a column. /// - Returns: A tuple containing the column and row indices. +@available(*, deprecated, message: "Implementation detail of matrix storage. Use the matrix's subscript(column:row:) or subscript(index:) instead.") @inlinable public func columnRow(index: Int, width: Int) -> (column: Int, row: Int) { - let row: Int = index % width - let column: Int = index / width - return (column, row) + columnMajorPosition(of: index, rows: width) +} + +/// Splits a flat column-major index into its column and row. +/// - Parameters: +/// - index: The flat index into the matrix storage. +/// - rows: The number of rows, i.e. the length of a column. +/// - Returns: A tuple containing the column and row indices. +@inlinable +@inline(__always) +func columnMajorPosition(of index: Int, rows: Int) -> (column: Int, row: Int) { // swiftlint:disable:this explicit_top_level_acl + (index / rows, index % rows) } diff --git a/Sources/FirebladeMath/Matrix/MatrixStorage+NO_SIMD.swift b/Sources/FirebladeMath/Matrix/MatrixStorage+NO_SIMD.swift index 5c0cc6a..fb5602f 100644 --- a/Sources/FirebladeMath/Matrix/MatrixStorage+NO_SIMD.swift +++ b/Sources/FirebladeMath/Matrix/MatrixStorage+NO_SIMD.swift @@ -71,11 +71,11 @@ public struct Storage2x2: Storage2x2Protocol { @inlinable public subscript(index: Int) -> Value { get { - let (column, row) = columnRow(index: index, width: 2) + let (column, row) = columnMajorPosition(of: index, rows: 2) return self[column, row] } set { - let (column, row) = columnRow(index: index, width: 2) + let (column, row) = columnMajorPosition(of: index, rows: 2) self[column, row] = newValue } } @@ -173,11 +173,11 @@ public struct Storage3x3: Storage3x3Protocol { @inlinable public subscript(index: Int) -> Value { get { - let (column, row) = columnRow(index: index, width: 3) + let (column, row) = columnMajorPosition(of: index, rows: 3) return self[column, row] } set { - let (column, row) = columnRow(index: index, width: 3) + let (column, row) = columnMajorPosition(of: index, rows: 3) self[column, row] = newValue } } @@ -287,11 +287,11 @@ public struct Storage4x4: Storage4x4Protocol { @inlinable public subscript(index: Int) -> Value { get { - let (column, row) = columnRow(index: index, width: 4) + let (column, row) = columnMajorPosition(of: index, rows: 4) return self[column, row] } set { - let (column, row) = columnRow(index: index, width: 4) + let (column, row) = columnMajorPosition(of: index, rows: 4) self[column, row] = newValue } } diff --git a/Sources/FirebladeMath/Matrix/MatrixStorage+SIMD.swift b/Sources/FirebladeMath/Matrix/MatrixStorage+SIMD.swift index eec9870..e099a27 100644 --- a/Sources/FirebladeMath/Matrix/MatrixStorage+SIMD.swift +++ b/Sources/FirebladeMath/Matrix/MatrixStorage+SIMD.swift @@ -28,7 +28,7 @@ extension simd_float2x2: Storage2x2Protocol, @retroactive @unchecked Sendable, @ @inlinable public subscript(index: Int) -> Float { get { - let (col, row) = columnRow(index: index, width: 2) + let (col, row) = columnMajorPosition(of: index, rows: 2) switch col { case 0: return columns.0[row] @@ -41,7 +41,7 @@ extension simd_float2x2: Storage2x2Protocol, @retroactive @unchecked Sendable, @ } } set { - let (col, row) = columnRow(index: index, width: 2) + let (col, row) = columnMajorPosition(of: index, rows: 2) switch col { case 0: columns.0[row] = newValue @@ -76,7 +76,7 @@ extension simd_double2x2: Storage2x2Protocol, @retroactive @unchecked Sendable, @inlinable public subscript(index: Int) -> Double { get { - let (col, row) = columnRow(index: index, width: 2) + let (col, row) = columnMajorPosition(of: index, rows: 2) switch col { case 0: return columns.0[row] @@ -89,7 +89,7 @@ extension simd_double2x2: Storage2x2Protocol, @retroactive @unchecked Sendable, } } set { - let (col, row) = columnRow(index: index, width: 2) + let (col, row) = columnMajorPosition(of: index, rows: 2) switch col { case 0: columns.0[row] = newValue @@ -126,7 +126,7 @@ extension simd_float3x3: Storage3x3Protocol, @retroactive @unchecked Sendable, @ @inlinable public subscript(index: Int) -> Float { get { - let (col, row) = columnRow(index: index, width: 3) + let (col, row) = columnMajorPosition(of: index, rows: 3) switch col { case 0: return columns.0[row] @@ -142,7 +142,7 @@ extension simd_float3x3: Storage3x3Protocol, @retroactive @unchecked Sendable, @ } } set { - let (col, row) = columnRow(index: index, width: 3) + let (col, row) = columnMajorPosition(of: index, rows: 3) switch col { case 0: columns.0[row] = newValue @@ -180,7 +180,7 @@ extension simd_double3x3: Storage3x3Protocol, @retroactive @unchecked Sendable, @inlinable public subscript(index: Int) -> Double { get { - let (col, row) = columnRow(index: index, width: 3) + let (col, row) = columnMajorPosition(of: index, rows: 3) switch col { case 0: return columns.0[row] @@ -196,7 +196,7 @@ extension simd_double3x3: Storage3x3Protocol, @retroactive @unchecked Sendable, } } set { - let (col, row) = columnRow(index: index, width: 3) + let (col, row) = columnMajorPosition(of: index, rows: 3) switch col { case 0: columns.0[row] = newValue @@ -236,7 +236,7 @@ extension simd_float4x4: Storage4x4Protocol, @retroactive @unchecked Sendable, @ @inlinable public subscript(index: Int) -> Float { get { - let (col, row) = columnRow(index: index, width: 4) + let (col, row) = columnMajorPosition(of: index, rows: 4) switch col { case 0: return columns.0[row] @@ -255,7 +255,7 @@ extension simd_float4x4: Storage4x4Protocol, @retroactive @unchecked Sendable, @ } } set { - let (col, row) = columnRow(index: index, width: 4) + let (col, row) = columnMajorPosition(of: index, rows: 4) switch col { case 0: columns.0[row] = newValue @@ -296,7 +296,7 @@ extension simd_double4x4: Storage4x4Protocol, @retroactive @unchecked Sendable, @inlinable public subscript(index: Int) -> Double { get { - let (col, row) = columnRow(index: index, width: 4) + let (col, row) = columnMajorPosition(of: index, rows: 4) switch col { case 0: return columns.0[row] @@ -315,7 +315,7 @@ extension simd_double4x4: Storage4x4Protocol, @retroactive @unchecked Sendable, } } set { - let (col, row) = columnRow(index: index, width: 4) + let (col, row) = columnMajorPosition(of: index, rows: 4) switch col { case 0: columns.0[row] = newValue diff --git a/Tests/FirebladeMathTests/MatrixIndexingTests.swift b/Tests/FirebladeMathTests/MatrixIndexingTests.swift new file mode 100644 index 0000000..df4960b --- /dev/null +++ b/Tests/FirebladeMathTests/MatrixIndexingTests.swift @@ -0,0 +1,64 @@ +// +// MatrixIndexingTests.swift +// FirebladeMathTests +// + +import FirebladeMath +import Testing + +struct MatrixIndexingTests { + // The flat subscript must follow the column-major storage layout: + // index = column * rows + row. + + @Test func mat2x2FlatIndexIsColumnMajor() { + let mf: Mat2x2f = [1, 2, 3, 4] + let md: Mat2x2d = [1, 2, 3, 4] + for index in 0..<4 { + #expect(mf[index] == mf[index / 2, index % 2]) + #expect(md[index] == md[index / 2, index % 2]) + } + } + + @Test func mat3x3FlatIndexIsColumnMajor() { + let mf = Mat3x3f((1...9).map(Float.init)) + let md = Mat3x3d((1...9).map(Double.init)) + for index in 0..<9 { + #expect(mf[index] == mf[index / 3, index % 3]) + #expect(md[index] == md[index / 3, index % 3]) + } + } + + @Test func mat4x4FlatIndexIsColumnMajor() { + let mf = Mat4x4f((1...16).map(Float.init)) + let md = Mat4x4d((1...16).map(Double.init)) + for index in 0..<16 { + #expect(mf[index] == mf[index / 4, index % 4]) + #expect(md[index] == md[index / 4, index % 4]) + } + } + + @Test func flatSubscriptSetterWritesColumnMajor() { + var matrix = Mat4x4f.identity + matrix[6] = 42 // column 1, row 2 + #expect(matrix[1, 2] == 42) + #expect(matrix.columns.1.z == 42) + } + + // MARK: - Deprecated helpers keep their behavior + + @Test + @available(*, deprecated, message: "Testing deprecated API") + func deprecatedColumnRowIsColumnMajor() { + #expect(columnRow(index: 6, width: 4) == (column: 1, row: 2)) + #expect(columnRow(index: 0, width: 3) == (column: 0, row: 0)) + #expect(columnRow(index: 8, width: 3) == (column: 2, row: 2)) + } + + @Test + @available(*, deprecated, message: "Testing deprecated API") + func deprecatedIndexIsRowMajor() { + // Documented mismatch: index(column:row:width:) is row-major, columnRow is column-major. + #expect(index(column: 1, row: 2, width: 4) == 9) + #expect(columnRow(index: index(column: 1, row: 2, width: 4), width: 4) != (column: 1, row: 2)) + } +}