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)) + } +}