From 6b9e945d1d052202b8ca14e01a3908650d683a0f Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Mon, 18 Nov 2024 18:19:34 -0600 Subject: [PATCH 01/99] specialize snapped for Int32 and Int64 --- Sources/SwiftGodot/Native/Snapped.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/SwiftGodot/Native/Snapped.swift b/Sources/SwiftGodot/Native/Snapped.swift index 80aee7739..90d5c5d7b 100644 --- a/Sources/SwiftGodot/Native/Snapped.swift +++ b/Sources/SwiftGodot/Native/Snapped.swift @@ -14,6 +14,8 @@ extension Numeric where Self: BinaryFloatingPoint { } extension Numeric where Self: SignedInteger { + @_specialize(exported: true, kind: full, where Self == Int32) + @_specialize(exported: true, kind: full, where Self == Int64) public func snapped(step: Self) -> Self { return Self(Double(self).snapped(step: Double(step))) } From 666457398bb29ffeaaca4e8c5f3dc77bc49873e6 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Mon, 18 Nov 2024 21:57:25 -0600 Subject: [PATCH 02/99] clean up generator argument bindings --- Generator/Generator/main.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Generator/Generator/main.swift b/Generator/Generator/main.swift index e1c504a13..5d82e61f7 100644 --- a/Generator/Generator/main.swift +++ b/Generator/Generator/main.swift @@ -38,10 +38,9 @@ var defaultDocRootUrl: URL { } let jsonFile = args.count > 1 ? args [1] : defaultExtensionApiJsonUrl.path -var generatorOutput = args.count > 2 ? args [2] : defaultGeneratorOutputlUrl.path -var docRoot = args.count > 3 ? args [3] : defaultDocRootUrl.path -let outputDir = args.count > 2 ? args [2] : generatorOutput -let generateResettableCache = false +let outputDir = args.count > 2 ? args [2] : defaultGeneratorOutputlUrl.path +let docRoot = args.count > 3 ? args [3] : defaultDocRootUrl.path +let generateResettableCache = false // IF we want a single file, or one file per type var singleFile = args.contains("--singlefile") From 13e07679e0eace95e4d0af40d76c268a62d627ec Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Mon, 18 Nov 2024 16:28:57 -0600 Subject: [PATCH 03/99] set up NativeCovers target/product and start on Vector2i --- Package.swift | 17 +++++++++++++ .../NativeCovers/Vector2i.arithmetic.swift | 25 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 Sources/NativeCovers/Vector2i.arithmetic.swift diff --git a/Package.swift b/Package.swift index 3313fa0e2..2778ae357 100644 --- a/Package.swift +++ b/Package.swift @@ -228,6 +228,23 @@ targets.append(contentsOf: [ ), ]) +// This target contains “native” Swift covers of methods implemented in the engine. These native implementations avoid the overhead of calling through the Godot FFI. +targets += [ + .target( + name: "NativeCovers", + dependencies: ["SwiftGodot"] + ) +] + +// This product allows building of the `NativeCovers` target. It is not intended for production use. +products += [ + .library( + name: "NativeCovers", + type: libraryType, + targets: ["NativeCovers"] + ) +] + let package = Package( name: "SwiftGodot", platforms: [ diff --git a/Sources/NativeCovers/Vector2i.arithmetic.swift b/Sources/NativeCovers/Vector2i.arithmetic.swift new file mode 100644 index 000000000..40598b71a --- /dev/null +++ b/Sources/NativeCovers/Vector2i.arithmetic.swift @@ -0,0 +1,25 @@ +import SwiftGodot + +extension Vector2i { + public typealias Component = Int32 + + public init(from: Vector2i) { + self = from + } + + static public func == (lhs: Self, rhs: Self) -> Bool { + return lhs.x == rhs.x && lhs.y == rhs.y + } + + static public func != (lhs: Self, rhs: Self) -> Bool { + return !(lhs == rhs) + } + + static public func / (lhs: Self, rhs: Int64) -> Self { + let rhs = Int32(truncatingIfNeeded: rhs) + return Self( + x: lhs.x.dividedReportingOverflow(by: rhs).partialValue, + y: lhs.y.dividedReportingOverflow(by: rhs).partialValue + ) + } +} From 5c1cabf17908308aa76531d54a975adaccec1754 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Mon, 18 Nov 2024 18:20:05 -0600 Subject: [PATCH 04/99] add CNativeCovers with C shims --- Package.swift | 6 ++++-- Sources/CNativeCovers/CNativeCovers.c | 1 + Sources/CNativeCovers/include/CNativeCovers.h | 15 +++++++++++++++ Sources/CNativeCovers/module.modulemap | 3 +++ 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 Sources/CNativeCovers/CNativeCovers.c create mode 100644 Sources/CNativeCovers/include/CNativeCovers.h create mode 100644 Sources/CNativeCovers/module.modulemap diff --git a/Package.swift b/Package.swift index 2778ae357..9b8d959c3 100644 --- a/Package.swift +++ b/Package.swift @@ -232,8 +232,10 @@ targets.append(contentsOf: [ targets += [ .target( name: "NativeCovers", - dependencies: ["SwiftGodot"] - ) + dependencies: ["SwiftGodot", "CNativeCovers"] + ), + + .target(name: "CNativeCovers"), ] // This product allows building of the `NativeCovers` target. It is not intended for production use. diff --git a/Sources/CNativeCovers/CNativeCovers.c b/Sources/CNativeCovers/CNativeCovers.c new file mode 100644 index 000000000..d8345401d --- /dev/null +++ b/Sources/CNativeCovers/CNativeCovers.c @@ -0,0 +1 @@ +#include "CNativeCovers.h" diff --git a/Sources/CNativeCovers/include/CNativeCovers.h b/Sources/CNativeCovers/include/CNativeCovers.h new file mode 100644 index 000000000..70cf10cfc --- /dev/null +++ b/Sources/CNativeCovers/include/CNativeCovers.h @@ -0,0 +1,15 @@ +#ifndef CNativeCovers_h +#define CNativeCovers_h + +#include + +/// - returns: `f`, cast to `int32_t`. +static inline int32_t int32_for_float(float f) { return f; } + +/// - returns: `n / d`. +static inline int32_t int32_divide(int32_t n, int32_t d) { return n / d; } + +/// - returns: `n % d`. +static inline int32_t int32_remainder(int32_t n, int32_t d) { return n % d; } + +#endif // CNativeCovers_h diff --git a/Sources/CNativeCovers/module.modulemap b/Sources/CNativeCovers/module.modulemap new file mode 100644 index 000000000..9e9caeb3f --- /dev/null +++ b/Sources/CNativeCovers/module.modulemap @@ -0,0 +1,3 @@ +module CNativeCovers { + header "CNativeCovers.h" +} From 76522409686cb1bc241ce4d53d52f3a762193fc3 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Mon, 18 Nov 2024 18:20:44 -0600 Subject: [PATCH 05/99] finish writing Vector2i native covers --- .../NativeCovers/Vector2i.arithmetic.swift | 261 +++++++++++++++++- 1 file changed, 253 insertions(+), 8 deletions(-) diff --git a/Sources/NativeCovers/Vector2i.arithmetic.swift b/Sources/NativeCovers/Vector2i.arithmetic.swift index 40598b71a..200b971d4 100644 --- a/Sources/NativeCovers/Vector2i.arithmetic.swift +++ b/Sources/NativeCovers/Vector2i.arithmetic.swift @@ -1,25 +1,270 @@ +@_implementationOnly import CNativeCovers import SwiftGodot +extension Comparable { + func clamped(min: Self, max: Self) -> Self { + return self < min ? min : self > max ? max : self + } +} + +extension Int64 { + func checkIndex(max: Int64) -> Int64 { + assert(self >= 0) + assert(self <= max) + return self + } +} + extension Vector2i { + public typealias Component = Int32 + var tuple: (Component, Component) { (x, y) } + + private static func cCastToComponent(_ float: Float) -> Int32 { + return int32_for_float(float) + } + + private static func cDivide(numerator: Int32, denominator: Int32) -> Int32 { + return int32_divide(numerator, denominator) + } + + private static func cRemainder(numerator: Int32, denominator: Int32) -> Int32 { + return int32_remainder(numerator, denominator) + } + public init(from: Vector2i) { self = from } - static public func == (lhs: Self, rhs: Self) -> Bool { - return lhs.x == rhs.x && lhs.y == rhs.y + public init(from: Vector2) { + /* + The Godot engine casts `int32_t` to `float` using `fcvtzs` (on ARM). Swift `Int32(_: Float)` does the same, but then checks for and aborts on overflow, which is unacceptable. To get good Swift code gen, I call an inlinable imported C function. In an optimized build, I compile down to this: + + fcvtzs w8, s0 + fcvtzs w9, s1 + orr x0, x8, x9, lsl #32 + ret + */ + + self.init(x: Self.cCastToComponent(from.x), y: Self.cCastToComponent(from.y)) + } + + public func aspect() -> Double { + return Double(Float(x) / Float(y)) } - static public func != (lhs: Self, rhs: Self) -> Bool { - return !(lhs == rhs) + public func maxAxisIndex() -> Int64 { (x < y ? Axis.y : .x).rawValue } + + public func minAxisIndex() -> Int64 { (x < y ? Axis.x : .y).rawValue } + + public func distanceTo(_ to: Vector2i) -> Double { (to - self).length() } + + public func distanceSquaredTo(_ to: Vector2i) -> Double { Double((to - self).lengthSquared()) } + + public func length() -> Double { Double(lengthSquared()).squareRoot() } + + public func lengthSquared() -> Int64 { + let x = Int64(x) + let y = Int64(y) + return x &* x &+ y &* y + } + + public func sign() -> Vector2i { + return Vector2i(x: x.signum(), y: y.signum()) + } + + public func abs() -> Vector2i { + // This handles Component.min exactly the way C would. + return Vector2i( + x: Component(truncatingIfNeeded: x.magnitude), + y: Component(truncatingIfNeeded: y.magnitude) + ) + } + + public func clamp(min: Vector2i, max: Vector2i) -> Vector2i { + return Vector2i( + x: x.clamped(min: min.x, max: max.x), + y: y.clamped(min: min.y, max: max.y) + ) + } + + public func clampi(min: Int64, max: Int64) -> Vector2i { + return Vector2i( + x: Component(truncatingIfNeeded: Int64(x).clamped(min: min, max: max)), + y: Component(truncatingIfNeeded: Int64(y).clamped(min: min, max: max)) + ) + } + + // snapped is special-cased. + + public func snappedi(step: Int64) -> Vector2i { + let step = Int32(truncatingIfNeeded: step) + return Vector2i( + x: x.snapped(step: step), + y: y.snapped(step: step) + ) + } + + public func min(with: Vector2i) -> Vector2i { + return Vector2i( + x: Swift.min(x, with.x), + y: Swift.min(y, with.y) + ) + } + + public func mini(with: Int64) -> Vector2i { + let i = Int32(truncatingIfNeeded: with) + return Vector2i( + x: Swift.min(x, i), + y: Swift.min(y, i) + ) + } + + public func max(with: Vector2i) -> Vector2i { + return Vector2i( + x: Swift.max(x, with.x), + y: Swift.max(y, with.y) + ) + } + + public func maxi(with: Int64) -> Vector2i { + let i = Int32(truncatingIfNeeded: with) + return Vector2i( + x: Swift.max(x, i), + y: Swift.max(y, i) + ) + } + + public subscript(index: Int64) -> Int64 { + get { + return Int64(SIMD2(x, y)[Int(index)]) + } + set { + var simd = SIMD2(x, y) + simd[Int(index)] = Int32(truncatingIfNeeded: newValue) + (x, y) = (simd.x, simd.y) + } + } + + public static func * (lhs: Vector2i, rhs: Int64) -> Vector2i { + let f = Int32(truncatingIfNeeded: rhs) + return Vector2i( + x: lhs.x &* f, + y: lhs.y &* f + ) } - static public func / (lhs: Self, rhs: Int64) -> Self { - let rhs = Int32(truncatingIfNeeded: rhs) + public static func / (lhs: Vector2i, rhs: Int64) -> Vector2i { + /* + Swift doesn't provide an `&/` operator like it does `&*`, `&+` and `&-`. Using `lhs.x.dividedReportingOverflow(by: rhs).partialValue` gives the correct answer with suboptimal code gen. I call an inlinable imported C function, which gives extremely good code gen: + + lsr x8, x0, #0x20 + sdiv w9, w0, w1 + sdiv w8, w8, w1 + orr x0, x9, x8, lsl #32 + ret + */ + + let rhs = Component(truncatingIfNeeded: rhs) return Self( - x: lhs.x.dividedReportingOverflow(by: rhs).partialValue, - y: lhs.y.dividedReportingOverflow(by: rhs).partialValue + x: cDivide(numerator: lhs.x, denominator: rhs), + y: cDivide(numerator: lhs.y, denominator: rhs) + ) + } + + public static func % (lhs: Vector2i, rhs: Int64) -> Vector2i { + /* + See comment in `/`. Code gen: + + lsr x8, x0, #0x20 + sdiv w9, w0, w1 + msub w9, w9, w1, w0 + sdiv w10, w8, w1 + msub w8, w10, w1, w8 + orr x0, x9, x8, lsl #32 + ret + */ + + let rhs = Component(truncatingIfNeeded: rhs) + return Self( + x: cRemainder(numerator: lhs.x, denominator: rhs), + y: cRemainder(numerator: lhs.y, denominator: rhs) + ) + } + + public static func * (lhs: Vector2i, rhs: Double) -> Vector2 { + let rhs = Float(rhs) + return Vector2( + x: Float(lhs.x) * rhs, + y: Float(lhs.y) * rhs + ) + } + + public static func / (lhs: Vector2i, rhs: Double) -> Vector2 { + let rhs = Float(rhs) + return Vector2( + x: Float(lhs.x) / rhs, + y: Float(lhs.y) / rhs + ) + } + + public static func == (lhs: Vector2i, rhs: Vector2i) -> Bool { + return lhs.tuple == rhs.tuple + } + + public static func != (lhs: Vector2i, rhs: Vector2i) -> Bool { + return !(lhs == rhs) + } + + public static func < (lhs: Vector2i, rhs: Vector2i) -> Bool { + return lhs.tuple < rhs.tuple + } + + public static func <= (lhs: Vector2i, rhs: Vector2i) -> Bool { + return lhs.tuple <= rhs.tuple + } + + public static func > (lhs: Vector2i, rhs: Vector2i) -> Bool { + return lhs.tuple > rhs.tuple + } + + public static func >= (lhs: Vector2i, rhs: Vector2i) -> Bool { + return lhs.tuple >= rhs.tuple + } + + public static func + (lhs: Vector2i, rhs: Vector2i) -> Vector2i { + return Vector2i( + x: lhs.x &+ rhs.x, + y: lhs.y &+ rhs.y + ) + } + + public static func - (lhs: Vector2i, rhs: Vector2i) -> Vector2i { + return Vector2i( + x: lhs.x &- rhs.x, + y: lhs.y &- rhs.y + ) + } + + public static func * (lhs: Vector2i, rhs: Vector2i) -> Vector2i { + return Vector2i( + x: lhs.x &* rhs.x, + y: lhs.y &* rhs.y + ) + } + + public static func / (lhs: Vector2i, rhs: Vector2i) -> Vector2i { + return Vector2i( + x: cDivide(numerator: lhs.x, denominator: rhs.x), + y: cDivide(numerator: lhs.y, denominator: rhs.y) + ) + } + + public static func % (lhs: Vector2i, rhs: Vector2i) -> Vector2i { + return Vector2i( + x: cRemainder(numerator: lhs.x, denominator: rhs.x), + y: cRemainder(numerator: lhs.y, denominator: rhs.y) ) } } From b5092b96b7b3e30dd36e86553f8d49bdb9fcaa28 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Mon, 18 Nov 2024 22:12:50 -0600 Subject: [PATCH 06/99] rename NativeCovers to SwiftCovers --- Package.swift | 12 ++++++------ Sources/CNativeCovers/CNativeCovers.c | 1 - Sources/CNativeCovers/module.modulemap | 3 --- Sources/CSwiftCovers/CSwiftCovers.c | 1 + .../include/CSwiftCovers.h} | 6 +++--- Sources/CSwiftCovers/module.modulemap | 3 +++ .../Vector2i.arithmetic.swift | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 Sources/CNativeCovers/CNativeCovers.c delete mode 100644 Sources/CNativeCovers/module.modulemap create mode 100644 Sources/CSwiftCovers/CSwiftCovers.c rename Sources/{CNativeCovers/include/CNativeCovers.h => CSwiftCovers/include/CSwiftCovers.h} (81%) create mode 100644 Sources/CSwiftCovers/module.modulemap rename Sources/{NativeCovers => SwiftCovers}/Vector2i.arithmetic.swift (99%) diff --git a/Package.swift b/Package.swift index 9b8d959c3..26c20f6a0 100644 --- a/Package.swift +++ b/Package.swift @@ -231,19 +231,19 @@ targets.append(contentsOf: [ // This target contains “native” Swift covers of methods implemented in the engine. These native implementations avoid the overhead of calling through the Godot FFI. targets += [ .target( - name: "NativeCovers", - dependencies: ["SwiftGodot", "CNativeCovers"] + name: "SwiftCovers", + dependencies: ["SwiftGodot", "CSwiftCovers"] ), - .target(name: "CNativeCovers"), + .target(name: "CSwiftCovers"), ] -// This product allows building of the `NativeCovers` target. It is not intended for production use. +// This product allows building of the `SwiftCovers` target. It is not intended for production use. products += [ .library( - name: "NativeCovers", + name: "SwiftCovers", type: libraryType, - targets: ["NativeCovers"] + targets: ["SwiftCovers"] ) ] diff --git a/Sources/CNativeCovers/CNativeCovers.c b/Sources/CNativeCovers/CNativeCovers.c deleted file mode 100644 index d8345401d..000000000 --- a/Sources/CNativeCovers/CNativeCovers.c +++ /dev/null @@ -1 +0,0 @@ -#include "CNativeCovers.h" diff --git a/Sources/CNativeCovers/module.modulemap b/Sources/CNativeCovers/module.modulemap deleted file mode 100644 index 9e9caeb3f..000000000 --- a/Sources/CNativeCovers/module.modulemap +++ /dev/null @@ -1,3 +0,0 @@ -module CNativeCovers { - header "CNativeCovers.h" -} diff --git a/Sources/CSwiftCovers/CSwiftCovers.c b/Sources/CSwiftCovers/CSwiftCovers.c new file mode 100644 index 000000000..4b961c3e9 --- /dev/null +++ b/Sources/CSwiftCovers/CSwiftCovers.c @@ -0,0 +1 @@ +#include "CSwiftCovers.h" diff --git a/Sources/CNativeCovers/include/CNativeCovers.h b/Sources/CSwiftCovers/include/CSwiftCovers.h similarity index 81% rename from Sources/CNativeCovers/include/CNativeCovers.h rename to Sources/CSwiftCovers/include/CSwiftCovers.h index 70cf10cfc..10f24c392 100644 --- a/Sources/CNativeCovers/include/CNativeCovers.h +++ b/Sources/CSwiftCovers/include/CSwiftCovers.h @@ -1,5 +1,5 @@ -#ifndef CNativeCovers_h -#define CNativeCovers_h +#ifndef CSwiftCovers_h +#define CSwiftCovers_h #include @@ -12,4 +12,4 @@ static inline int32_t int32_divide(int32_t n, int32_t d) { return n / d; } /// - returns: `n % d`. static inline int32_t int32_remainder(int32_t n, int32_t d) { return n % d; } -#endif // CNativeCovers_h +#endif // CSwiftCovers_h diff --git a/Sources/CSwiftCovers/module.modulemap b/Sources/CSwiftCovers/module.modulemap new file mode 100644 index 000000000..fd1770464 --- /dev/null +++ b/Sources/CSwiftCovers/module.modulemap @@ -0,0 +1,3 @@ +module CSwiftCovers { + header "CSwiftCovers.h" +} diff --git a/Sources/NativeCovers/Vector2i.arithmetic.swift b/Sources/SwiftCovers/Vector2i.arithmetic.swift similarity index 99% rename from Sources/NativeCovers/Vector2i.arithmetic.swift rename to Sources/SwiftCovers/Vector2i.arithmetic.swift index 200b971d4..978eb2d62 100644 --- a/Sources/NativeCovers/Vector2i.arithmetic.swift +++ b/Sources/SwiftCovers/Vector2i.arithmetic.swift @@ -1,4 +1,4 @@ -@_implementationOnly import CNativeCovers +@_implementationOnly import CSwiftCovers import SwiftGodot extension Comparable { From 22d7e662e252c95aad0d218f9979be8aee1f42c8 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Mon, 18 Nov 2024 21:58:09 -0600 Subject: [PATCH 07/99] wip: load SwiftCovers source into Generator --- Generator/Generator/SwiftCovers.swift | 11 +++++++++++ Generator/Generator/main.swift | 5 +++++ 2 files changed, 16 insertions(+) create mode 100644 Generator/Generator/SwiftCovers.swift diff --git a/Generator/Generator/SwiftCovers.swift b/Generator/Generator/SwiftCovers.swift new file mode 100644 index 000000000..4e39bb332 --- /dev/null +++ b/Generator/Generator/SwiftCovers.swift @@ -0,0 +1,11 @@ +import Foundation +import SwiftSyntax + +/// Source snippets of Swift that can replace FFI calls to Godot engine methods. +struct SwiftCovers { + + init(sourceDir: URL) { + + } + +} diff --git a/Generator/Generator/main.swift b/Generator/Generator/main.swift index 5d82e61f7..f8f63682f 100644 --- a/Generator/Generator/main.swift +++ b/Generator/Generator/main.swift @@ -37,6 +37,11 @@ var defaultDocRootUrl: URL { .appendingPathComponent("Docs") } +var defaultSwiftCoversDirUrl: URL { + rootUrl + .appending(components: "Sources", "SwiftCovers") +} + let jsonFile = args.count > 1 ? args [1] : defaultExtensionApiJsonUrl.path let outputDir = args.count > 2 ? args [2] : defaultGeneratorOutputlUrl.path let docRoot = args.count > 3 ? args [3] : defaultDocRootUrl.path From 4600108f1d72c40b809213080abd655f0a0cf73c Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Mon, 18 Nov 2024 23:24:54 -0600 Subject: [PATCH 08/99] wip: parse SwiftCovers --- Generator/Generator/SwiftCovers.swift | 57 ++++++++++++++++++- Generator/Generator/main.swift | 2 + Sources/SwiftCovers/Vector2i.arithmetic.swift | 4 +- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/Generator/Generator/SwiftCovers.swift b/Generator/Generator/SwiftCovers.swift index 4e39bb332..39840d4df 100644 --- a/Generator/Generator/SwiftCovers.swift +++ b/Generator/Generator/SwiftCovers.swift @@ -1,11 +1,66 @@ import Foundation +import SwiftParser import SwiftSyntax /// Source snippets of Swift that can replace FFI calls to Godot engine methods. struct SwiftCovers { + /// Load the Swift source files from `sourceDir` and extract snippets usable as method implementations. init(sourceDir: URL) { - + let urls: [URL] + do { + urls = try FileManager.default.contentsOfDirectory( + at: sourceDir, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles, .skipsPackageDescendants, .skipsSubdirectoryDescendants] + ) + } catch { + print("warning: couldn't scan folder at \(sourceDir): \(error)") + return + } + + for url in urls { + let source: String + do { + source = try String(contentsOf: url, encoding: .utf8) + } catch { + print("warning: couldn't read contents of \(url): \(error)") + continue + } + + let root = Parser.parse(source: source) + + for statement in root.statements { + guard + let extensionSx = statement.item.as(ExtensionDeclSyntax.self), + extensionSx.genericWhereClause == nil, + let extendedType = extensionSx.extendedType.as(IdentifierTypeSyntax.self) + else { + continue + } + + for member in extensionSx.memberBlock.members { + guard + let function = member.decl.as(FunctionDeclSyntax.self), + function.modifiers.map({ $0.name.tokenKind }) == [.keyword(.public)], + function.genericParameterClause == nil, + function.genericWhereClause == nil, + case let signature = function.signature, + signature.effectSpecifiers == nil, + case let parameterTypes = signature.parameterClause.parameters + .compactMap({ $0.type.as(IdentifierTypeSyntax.self)?.name }), + parameterTypes.count == signature.parameterClause.parameters.count + else { + continue + } + + let functionName = function.name + let returnType = signature.returnClause?.type ?? "Void" + + print("found method for \(extendedType): '\(functionName)' returning \(returnType) and taking \(parameterTypes.map { "\($0)" }.joined(separator: ", ") )") + } + } + } } } diff --git a/Generator/Generator/main.swift b/Generator/Generator/main.swift index f8f63682f..506b233d6 100644 --- a/Generator/Generator/main.swift +++ b/Generator/Generator/main.swift @@ -63,6 +63,8 @@ if args.count < 2 { """) } +let swiftCovers = SwiftCovers(sourceDir: defaultSwiftCoversDirUrl) + let jsonData = try! Data(url: URL(fileURLWithPath: jsonFile)) let jsonApi = try! JSONDecoder().decode(JGodotExtensionAPI.self, from: jsonData) diff --git a/Sources/SwiftCovers/Vector2i.arithmetic.swift b/Sources/SwiftCovers/Vector2i.arithmetic.swift index 978eb2d62..e90396994 100644 --- a/Sources/SwiftCovers/Vector2i.arithmetic.swift +++ b/Sources/SwiftCovers/Vector2i.arithmetic.swift @@ -1,13 +1,13 @@ @_implementationOnly import CSwiftCovers import SwiftGodot -extension Comparable { +extension Swift.Comparable { func clamped(min: Self, max: Self) -> Self { return self < min ? min : self > max ? max : self } } -extension Int64 { +extension Swift.Int64 { func checkIndex(max: Int64) -> Int64 { assert(self >= 0) assert(self <= max) From 5fb6dd278049a8af82ecf4eb334744b27e6c4a47 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 19 Nov 2024 09:39:06 -0600 Subject: [PATCH 09/99] rename Vector2i.arithmetic.swift to Vector2i.covers.swift --- .../{Vector2i.arithmetic.swift => Vector2i.covers.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/SwiftCovers/{Vector2i.arithmetic.swift => Vector2i.covers.swift} (100%) diff --git a/Sources/SwiftCovers/Vector2i.arithmetic.swift b/Sources/SwiftCovers/Vector2i.covers.swift similarity index 100% rename from Sources/SwiftCovers/Vector2i.arithmetic.swift rename to Sources/SwiftCovers/Vector2i.covers.swift From 4bebf3866076562545eeee18c582fcb7f959a877 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 19 Nov 2024 12:01:13 -0600 Subject: [PATCH 10/99] extract method, operator, and init covers --- Generator/Generator/SwiftCovers.swift | 164 ++++++++++++++++++++++---- 1 file changed, 142 insertions(+), 22 deletions(-) diff --git a/Generator/Generator/SwiftCovers.swift b/Generator/Generator/SwiftCovers.swift index 39840d4df..1472438d1 100644 --- a/Generator/Generator/SwiftCovers.swift +++ b/Generator/Generator/SwiftCovers.swift @@ -5,6 +5,26 @@ import SwiftSyntax /// Source snippets of Swift that can replace FFI calls to Godot engine methods. struct SwiftCovers { + struct Key: Hashable, CustomStringConvertible { + /// A type name. + var type: String + + /// A method name, operator, or `init`. + var name: String + + /// The parameter types. + var parameterTypes: [String] + + /// The return type. + var returnType: String + + var description: String { + "\(type).\(name)(\(parameterTypes.joined(separator: ", "))) -> \(returnType)" + } + } + + var covers: [Key: CodeBlockSyntax] = [:] + /// Load the Swift source files from `sourceDir` and extract snippets usable as method implementations. init(sourceDir: URL) { let urls: [URL] @@ -32,35 +52,135 @@ struct SwiftCovers { for statement in root.statements { guard - let extensionSx = statement.item.as(ExtensionDeclSyntax.self), - extensionSx.genericWhereClause == nil, - let extendedType = extensionSx.extendedType.as(IdentifierTypeSyntax.self) + let exn = statement.item.as(ExtensionDeclSyntax.self), + exn.genericWhereClause == nil, + let type = exn.extendedType.as(IdentifierTypeSyntax.self)?.name.text else { continue } - for member in extensionSx.memberBlock.members { - guard - let function = member.decl.as(FunctionDeclSyntax.self), - function.modifiers.map({ $0.name.tokenKind }) == [.keyword(.public)], - function.genericParameterClause == nil, - function.genericWhereClause == nil, - case let signature = function.signature, - signature.effectSpecifiers == nil, - case let parameterTypes = signature.parameterClause.parameters - .compactMap({ $0.type.as(IdentifierTypeSyntax.self)?.name }), - parameterTypes.count == signature.parameterClause.parameters.count - else { - continue - } - - let functionName = function.name - let returnType = signature.returnClause?.type ?? "Void" - - print("found method for \(extendedType): '\(functionName)' returning \(returnType) and taking \(parameterTypes.map { "\($0)" }.joined(separator: ", ") )") + for member in exn.memberBlock.members { + extractCover(from: member, of: type) } } } } + private mutating func extractCover(from member: MemberBlockItemSyntax, of type: String) { + if + let function = member.decl.as(FunctionDeclSyntax.self), + function.modifiers.contains(where: { $0.name.tokenKind == .keyword(.public) }), + function.genericWhereClause == nil, + function.genericParameterClause == nil, + function.signature.effectSpecifiers == nil + { + _ = extractFunctionCover(from: function, of: type) + || extractBinaryOperatorCover(from: function, of: type) + return + } + + if extractInitCover(from: member, of: type) { + return + } + } + + private mutating func extractInitCover(from member: MemberBlockItemSyntax, of type: String) -> Bool { + guard + let initer = member.decl.as(InitializerDeclSyntax.self), + initer.genericWhereClause == nil, + initer.genericParameterClause == nil, + case let signature = initer.signature, + signature.effectSpecifiers == nil, + case let parameterTypes = signature.parameterClause.parameters + .compactMap({ $0.type.as(IdentifierTypeSyntax.self)?.name.text }), + parameterTypes.count == signature.parameterClause.parameters.count, + let body = initer.body + else { + return false + } + + let key = Key( + type: type, + name: "init", + parameterTypes: parameterTypes, + returnType: type + ) + + print("found cover for \(key)") + + covers[key] = body + + return true + } + + private mutating func extractFunctionCover(from function: FunctionDeclSyntax, of type: String) -> Bool { + guard + function.modifiers.map({ $0.name.tokenKind }) == [.keyword(.public)], + case .identifier(let name) = function.name.tokenKind, + case let signature = function.signature, + case let parameterTypes = signature.parameterClause.parameters + .compactMap({ $0.type.as(IdentifierTypeSyntax.self)?.name.text }), + parameterTypes.count == signature.parameterClause.parameters.count, + let body = function.body + else { + return false + } + + let returnType: String + if let returnTypeSx = signature.returnClause?.type.as(IdentifierTypeSyntax.self) { + returnType = returnTypeSx.name.text + } else if signature.returnClause == nil { + returnType = "Void" + } else { + print("warning: couldn't handle return type \(signature.returnClause!)") + return true + } + + let key = Key( + type: type, + name: name, + parameterTypes: parameterTypes, + returnType: returnType + ) + + print("found cover for \(key)") + covers[key] = body + return true + } + + private mutating func extractBinaryOperatorCover(from function: FunctionDeclSyntax, of type: String) -> Bool { + guard + Set(function.modifiers.map({ $0.name.tokenKind })) == [.keyword(.public), .keyword(.static)], + case .binaryOperator(let op) = function.name.tokenKind, + case let signature = function.signature, + case let parameterTypes = signature.parameterClause.parameters + .compactMap({ $0.type.as(IdentifierTypeSyntax.self)?.name.text }), + parameterTypes.count == signature.parameterClause.parameters.count, + let body = function.body + else { + return false + } + + let returnType: String + if let returnTypeSx = signature.returnClause?.type.as(IdentifierTypeSyntax.self) { + returnType = returnTypeSx.name.text + } else if signature.returnClause == nil { + returnType = "Void" + } else { + print("warning: couldn't handle return type \(signature.returnClause!)") + return true + } + + let key = Key( + type: type, + name: op, + parameterTypes: parameterTypes, + returnType: returnType + ) + + print("found cover for \(key)") + covers[key] = body + return true + } + } From fb3312c92d8510f4aad6cc67b4e1ae8c99212a5c Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Tue, 19 Nov 2024 13:12:36 -0600 Subject: [PATCH 11/99] use covers for operators --- Generator/Generator/BuiltinGen.swift | 10 +++++++++- Generator/Generator/SwiftCovers.swift | 17 +++++++++++------ Sources/SwiftCovers/Vector2.covers.swift | 7 +++++++ 3 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 Sources/SwiftCovers/Vector2.covers.swift diff --git a/Generator/Generator/BuiltinGen.swift b/Generator/Generator/BuiltinGen.swift index 5da8d17e3..320e55b24 100644 --- a/Generator/Generator/BuiltinGen.swift +++ b/Generator/Generator/BuiltinGen.swift @@ -360,7 +360,14 @@ func generateBuiltinOperators (_ p: Printer, let lhsTypeName = typeName let rhsTypeName = getGodotType(SimpleType(type: right), kind: .builtIn) - let customImplementation = customBuiltinOperatorImplementations[OperatorSignature(name: swiftOperator, lhs: lhsTypeName, rhs: rhsTypeName)] + let customImplementation: String? // = customBuiltinOperatorImplementations[OperatorSignature(name: swiftOperator, lhs: lhsTypeName, rhs: rhsTypeName)] + + let key = SwiftCovers.Key(type: typeName, name: swiftOperator, parameterTypes: [lhsTypeName, rhsTypeName], returnType: retType) + if let body = swiftCovers.covers[key] { + customImplementation = body.description + } else { + customImplementation = nil + } if let desc = op.description, desc != "" { doc (p, bc, desc) @@ -368,6 +375,7 @@ func generateBuiltinOperators (_ p: Printer, p ("public static func \(swiftOperator) (lhs: \(lhsTypeName), rhs: \(rhsTypeName)) -> \(retType) "){ if customImplementation != nil { + p("#if !CUSTOM_BUILTIN_IMPLEMENTATIONS") } diff --git a/Generator/Generator/SwiftCovers.swift b/Generator/Generator/SwiftCovers.swift index 1472438d1..9883aea67 100644 --- a/Generator/Generator/SwiftCovers.swift +++ b/Generator/Generator/SwiftCovers.swift @@ -23,7 +23,7 @@ struct SwiftCovers { } } - var covers: [Key: CodeBlockSyntax] = [:] + var covers: [Key: String] = [:] /// Load the Swift source files from `sourceDir` and extract snippets usable as method implementations. init(sourceDir: URL) { @@ -106,9 +106,7 @@ struct SwiftCovers { returnType: type ) - print("found cover for \(key)") - - covers[key] = body + covers[key] = fixCodeBlockIndentation(body) return true } @@ -144,7 +142,7 @@ struct SwiftCovers { ) print("found cover for \(key)") - covers[key] = body + covers[key] = fixCodeBlockIndentation(body) return true } @@ -179,8 +177,15 @@ struct SwiftCovers { ) print("found cover for \(key)") - covers[key] = body + covers[key] = fixCodeBlockIndentation(body) return true } + + private func fixCodeBlockIndentation(_ block: CodeBlockSyntax) -> String { + var lines = block.description.split(separator: "\n") + let whitespace = lines.last!.prefix(while: { $0.isWhitespace } ) + lines[0] = whitespace + "do " + lines[0] + return lines.joined(separator: "\n") + } } diff --git a/Sources/SwiftCovers/Vector2.covers.swift b/Sources/SwiftCovers/Vector2.covers.swift new file mode 100644 index 000000000..a92fe9a67 --- /dev/null +++ b/Sources/SwiftCovers/Vector2.covers.swift @@ -0,0 +1,7 @@ +// +// File.swift +// SwiftGodot +// +// Created by Danny Youstra on 11/19/24. +// + From 0b862bba16a8c062cc6624787f6f8655bafc6208 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Tue, 19 Nov 2024 13:38:56 -0600 Subject: [PATCH 12/99] add method covers --- Generator/Generator/BuiltinGen.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Generator/Generator/BuiltinGen.swift b/Generator/Generator/BuiltinGen.swift index 320e55b24..65af4ac87 100644 --- a/Generator/Generator/BuiltinGen.swift +++ b/Generator/Generator/BuiltinGen.swift @@ -359,15 +359,9 @@ func generateBuiltinOperators (_ p: Printer, let lhsTypeName = typeName let rhsTypeName = getGodotType(SimpleType(type: right), kind: .builtIn) - - let customImplementation: String? // = customBuiltinOperatorImplementations[OperatorSignature(name: swiftOperator, lhs: lhsTypeName, rhs: rhsTypeName)] let key = SwiftCovers.Key(type: typeName, name: swiftOperator, parameterTypes: [lhsTypeName, rhsTypeName], returnType: retType) - if let body = swiftCovers.covers[key] { - customImplementation = body.description - } else { - customImplementation = nil - } + let customImplementation = swiftCovers.covers[key]?.description if let desc = op.description, desc != "" { doc (p, bc, desc) @@ -504,7 +498,13 @@ func generateBuiltinMethods (_ p: Printer, } let methodName = escapeSwift(snakeToCamel(m.name)) - let customImplementation = customBuiltinMethodImplementations[MethodSignature(typeName: bc.name, methodName: methodName)] + + let parameterTypes = m.arguments?.map { arg in + getGodotType(SimpleType (type: arg.type), kind: .builtIn) + } ?? [] + + let key = SwiftCovers.Key(type: typeName, name: methodName, parameterTypes: parameterTypes, returnType: ret) + let customImplementation = swiftCovers.covers[key]?.description p ("public\(keyword) func \(methodName)(\(args))\(retSig)") { if customImplementation != nil { From 59ad93142d91dd5a1eecd000e51e2aad5ca5c8bb Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Tue, 19 Nov 2024 13:55:42 -0600 Subject: [PATCH 13/99] add constructor covers --- Generator/Generator/BuiltinGen.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Generator/Generator/BuiltinGen.swift b/Generator/Generator/BuiltinGen.swift index 65af4ac87..1a01a2b56 100644 --- a/Generator/Generator/BuiltinGen.swift +++ b/Generator/Generator/BuiltinGen.swift @@ -180,6 +180,17 @@ func generateBuiltinCtors (_ p: Printer, try! MethodArgument(from: $0, typeName: typeName, methodName: "#constructor\(m.index)", options: .builtInClassOptions) } + let parameterTypes = m.arguments?.map { arg in + getGodotType(SimpleType (type: arg.type), kind: .builtIn) + } ?? [] + + let key = SwiftCovers.Key(type: typeName, name: "init", parameterTypes: parameterTypes, returnType: bc.name) + let customImplementation = swiftCovers.covers[key]?.description + + if customImplementation != nil { + p("#if !CUSTOM_BUILTIN_IMPLEMENTATIONS") + } + if arguments.isEmpty { preparingArguments(p, arguments: arguments) { p ("\(typeName).\(ptrName)(&\(ptr), nil)") @@ -191,6 +202,12 @@ func generateBuiltinCtors (_ p: Printer, } } } + + if let customImplementation { + p("#else // CUSTOM_BUILTIN_IMPLEMENTATIONS") + p(customImplementation) + p("#endif") + } } } } From bd665f0e0cf85d0654eebe47e59f212755f58a64 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Tue, 19 Nov 2024 14:06:11 -0600 Subject: [PATCH 14/99] get rid of typealias --- Sources/SwiftCovers/Vector2i.covers.swift | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Sources/SwiftCovers/Vector2i.covers.swift b/Sources/SwiftCovers/Vector2i.covers.swift index e90396994..dc0268aee 100644 --- a/Sources/SwiftCovers/Vector2i.covers.swift +++ b/Sources/SwiftCovers/Vector2i.covers.swift @@ -17,11 +17,9 @@ extension Swift.Int64 { extension Vector2i { - public typealias Component = Int32 + var tuple: (Int32, Int32) { (x, y) } - var tuple: (Component, Component) { (x, y) } - - private static func cCastToComponent(_ float: Float) -> Int32 { + private static func cCastToInt32(_ float: Float) -> Int32 { return int32_for_float(float) } @@ -47,7 +45,7 @@ extension Vector2i { ret */ - self.init(x: Self.cCastToComponent(from.x), y: Self.cCastToComponent(from.y)) + self.init(x: Self.cCastToInt32(from.x), y: Self.cCastToInt32(from.y)) } public func aspect() -> Double { @@ -75,10 +73,10 @@ extension Vector2i { } public func abs() -> Vector2i { - // This handles Component.min exactly the way C would. + // This handles Int32.min exactly the way C would. return Vector2i( - x: Component(truncatingIfNeeded: x.magnitude), - y: Component(truncatingIfNeeded: y.magnitude) + x: Int32(truncatingIfNeeded: x.magnitude), + y: Int32(truncatingIfNeeded: y.magnitude) ) } @@ -91,8 +89,8 @@ extension Vector2i { public func clampi(min: Int64, max: Int64) -> Vector2i { return Vector2i( - x: Component(truncatingIfNeeded: Int64(x).clamped(min: min, max: max)), - y: Component(truncatingIfNeeded: Int64(y).clamped(min: min, max: max)) + x: Int32(truncatingIfNeeded: Int64(x).clamped(min: min, max: max)), + y: Int32(truncatingIfNeeded: Int64(y).clamped(min: min, max: max)) ) } @@ -166,7 +164,7 @@ extension Vector2i { ret */ - let rhs = Component(truncatingIfNeeded: rhs) + let rhs = Int32(truncatingIfNeeded: rhs) return Self( x: cDivide(numerator: lhs.x, denominator: rhs), y: cDivide(numerator: lhs.y, denominator: rhs) @@ -186,7 +184,7 @@ extension Vector2i { ret */ - let rhs = Component(truncatingIfNeeded: rhs) + let rhs = Int32(truncatingIfNeeded: rhs) return Self( x: cRemainder(numerator: lhs.x, denominator: rhs), y: cRemainder(numerator: lhs.y, denominator: rhs) From ffa4e9835b1954b96a0cf6780fb080e8b781c822 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 19 Nov 2024 14:22:36 -0600 Subject: [PATCH 15/99] inform SwiftPM of new generated source file dependencies --- Plugins/CodeGeneratorPlugin/plugin.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Plugins/CodeGeneratorPlugin/plugin.swift b/Plugins/CodeGeneratorPlugin/plugin.swift index 2dca817d3..24322a3a9 100644 --- a/Plugins/CodeGeneratorPlugin/plugin.swift +++ b/Plugins/CodeGeneratorPlugin/plugin.swift @@ -19,7 +19,11 @@ import PackagePlugin let generator: Path = try context.tool(named: "Generator").path let api = context.package.directory.appending(["Sources", "ExtensionApi", "extension_api.json"]) - + let coverSourcesDir = context.package.directory.appending(["Sources", "SwiftCovers"]) + let coverSources = try FileManager.default.contentsOfDirectory(atPath: coverSourcesDir.string).map { + coverSourcesDir.appending(subpath: $0) + } + var arguments: [CustomStringConvertible] = [ api, genSourcesDir ] var outputFiles: [Path] = [] #if os(Windows) @@ -43,7 +47,7 @@ import PackagePlugin displayName: "Generating Swift API from \(api) to \(genSourcesDir)", executable: generator, arguments: arguments, - inputFiles: [api], + inputFiles: coverSources + [api], outputFiles: outputFiles)) #endif From e25d9bd6eb30218e5df18cac54183270f3690aca Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 19 Nov 2024 16:32:48 -0600 Subject: [PATCH 16/99] only generate member default initializations for default init and for FFI-based init --- Generator/Generator/BuiltinGen.swift | 39 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/Generator/Generator/BuiltinGen.swift b/Generator/Generator/BuiltinGen.swift index 1a01a2b56..6cb41529a 100644 --- a/Generator/Generator/BuiltinGen.swift +++ b/Generator/Generator/BuiltinGen.swift @@ -137,7 +137,25 @@ func generateBuiltinCtors (_ p: Printer, // I used to have a nicer model, rather than everything having a // handle, I had a named handle, like "_godot_string" let ptr = isStruct ? "self" : "content" - + + let parameterTypes = m.arguments?.map { arg in + getGodotType(SimpleType (type: arg.type), kind: .builtIn) + } ?? [] + let key = SwiftCovers.Key(type: typeName, name: "init", parameterTypes: parameterTypes, returnType: bc.name) + let customImplementation = swiftCovers.covers[key]?.description + + if customImplementation != nil { + p("#if !CUSTOM_BUILTIN_IMPLEMENTATIONS") + } + + defer { + if let customImplementation { + p("#else // CUSTOM_BUILTIN_IMPLEMENTATIONS") + p(customImplementation) + p("#endif") + } + } + // We need to initialize some variables before we call if let members { if bc.name == "Color" { @@ -174,23 +192,12 @@ func generateBuiltinCtors (_ p: Printer, return } } - + let arguments = (m.arguments ?? []).map { // must not fail try! MethodArgument(from: $0, typeName: typeName, methodName: "#constructor\(m.index)", options: .builtInClassOptions) } - let parameterTypes = m.arguments?.map { arg in - getGodotType(SimpleType (type: arg.type), kind: .builtIn) - } ?? [] - - let key = SwiftCovers.Key(type: typeName, name: "init", parameterTypes: parameterTypes, returnType: bc.name) - let customImplementation = swiftCovers.covers[key]?.description - - if customImplementation != nil { - p("#if !CUSTOM_BUILTIN_IMPLEMENTATIONS") - } - if arguments.isEmpty { preparingArguments(p, arguments: arguments) { p ("\(typeName).\(ptrName)(&\(ptr), nil)") @@ -202,12 +209,6 @@ func generateBuiltinCtors (_ p: Printer, } } } - - if let customImplementation { - p("#else // CUSTOM_BUILTIN_IMPLEMENTATIONS") - p(customImplementation) - p("#endif") - } } } } From 519d0d9a9cd2b874916ae14b7347a704c633f2ee Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 19 Nov 2024 16:34:13 -0600 Subject: [PATCH 17/99] rename CSwiftCovers to CWrappers and import it only in SwiftGodot --- Package.swift | 9 +++-- Sources/CSwiftCovers/CSwiftCovers.c | 1 - Sources/CSwiftCovers/module.modulemap | 3 -- Sources/CWrappers/CWrappers.c | 1 + .../include/CWrappers.h} | 6 +-- Sources/CWrappers/module.modulemap | 3 ++ Sources/SwiftGodot/SwiftCoverSupport.swift | 40 +++++++++++++++++++ 7 files changed, 52 insertions(+), 11 deletions(-) delete mode 100644 Sources/CSwiftCovers/CSwiftCovers.c delete mode 100644 Sources/CSwiftCovers/module.modulemap create mode 100644 Sources/CWrappers/CWrappers.c rename Sources/{CSwiftCovers/include/CSwiftCovers.h => CWrappers/include/CWrappers.h} (82%) create mode 100644 Sources/CWrappers/module.modulemap create mode 100644 Sources/SwiftGodot/SwiftCoverSupport.swift diff --git a/Package.swift b/Package.swift index 26c20f6a0..d692f5044 100644 --- a/Package.swift +++ b/Package.swift @@ -209,7 +209,7 @@ targets.append(contentsOf: [ // a better Swift experience .target( name: "SwiftGodot", - dependencies: ["GDExtension"], + dependencies: ["GDExtension", "CWrappers"], //linkerSettings: linkerSettings, swiftSettings: [ .define("CUSTOM_BUILTIN_IMPLEMENTATIONS") @@ -229,16 +229,17 @@ targets.append(contentsOf: [ ]) // This target contains “native” Swift covers of methods implemented in the engine. These native implementations avoid the overhead of calling through the Godot FFI. +// You don't normally need to build this target. `Generator` reads this target's source files and copies method implementations into the generated files. targets += [ .target( name: "SwiftCovers", - dependencies: ["SwiftGodot", "CSwiftCovers"] + dependencies: ["SwiftGodot"] ), - .target(name: "CSwiftCovers"), + .target(name: "CWrappers"), ] -// This product allows building of the `SwiftCovers` target. It is not intended for production use. +// This product allows building of the `SwiftCovers` target. You shouldn't normally need to build this if you're just building a game with SwiftGodot. You may want to build it if you are editing the cover sources, so that you get IDE assistance. products += [ .library( name: "SwiftCovers", diff --git a/Sources/CSwiftCovers/CSwiftCovers.c b/Sources/CSwiftCovers/CSwiftCovers.c deleted file mode 100644 index 4b961c3e9..000000000 --- a/Sources/CSwiftCovers/CSwiftCovers.c +++ /dev/null @@ -1 +0,0 @@ -#include "CSwiftCovers.h" diff --git a/Sources/CSwiftCovers/module.modulemap b/Sources/CSwiftCovers/module.modulemap deleted file mode 100644 index fd1770464..000000000 --- a/Sources/CSwiftCovers/module.modulemap +++ /dev/null @@ -1,3 +0,0 @@ -module CSwiftCovers { - header "CSwiftCovers.h" -} diff --git a/Sources/CWrappers/CWrappers.c b/Sources/CWrappers/CWrappers.c new file mode 100644 index 000000000..5fdc2a128 --- /dev/null +++ b/Sources/CWrappers/CWrappers.c @@ -0,0 +1 @@ +#include "CWrappers.h" diff --git a/Sources/CSwiftCovers/include/CSwiftCovers.h b/Sources/CWrappers/include/CWrappers.h similarity index 82% rename from Sources/CSwiftCovers/include/CSwiftCovers.h rename to Sources/CWrappers/include/CWrappers.h index 10f24c392..0d1a67cc7 100644 --- a/Sources/CSwiftCovers/include/CSwiftCovers.h +++ b/Sources/CWrappers/include/CWrappers.h @@ -1,5 +1,5 @@ -#ifndef CSwiftCovers_h -#define CSwiftCovers_h +#ifndef CWrappers_h +#define CWrappers_h #include @@ -12,4 +12,4 @@ static inline int32_t int32_divide(int32_t n, int32_t d) { return n / d; } /// - returns: `n % d`. static inline int32_t int32_remainder(int32_t n, int32_t d) { return n % d; } -#endif // CSwiftCovers_h +#endif // CWrappers_h diff --git a/Sources/CWrappers/module.modulemap b/Sources/CWrappers/module.modulemap new file mode 100644 index 000000000..9770c5b5b --- /dev/null +++ b/Sources/CWrappers/module.modulemap @@ -0,0 +1,3 @@ +module CWrappers { + header "CWrappers.h" +} diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift new file mode 100644 index 000000000..68b1fe0da --- /dev/null +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -0,0 +1,40 @@ +// Make imported inlinable C functions available to generated source files in the SwiftGodot target. + +@_implementationOnly import CWrappers + +/// The Swift standard library offers no efficient way to cast a `Float` to an `Int32` with the same semantics as C and C++. This method calls an imported inlinable C function. +@_spi(SwiftCovers) +@inline(__always) +public func cCastToInt32(_ float: Float) -> Int32 { + return int32_for_float(float) +} + +/// The Swift standard library offers no efficient way to divide an `Int32` by an `Int32` with the same semantics as C and C++. This method calls an imported inlinable C function. +@_spi(SwiftCovers) +@inline(__always) +public func cDivide(numerator: Int32, denominator: Int32) -> Int32 { + return int32_divide(numerator, denominator) +} + +/// The Swift standard library offers no efficient way to compute the remainder of dividing an `Int32` by an `Int32` with the same semantics as C and C++. This method calls an imported inlinable C function. +@_spi(SwiftCovers) +@inline(__always) +public func cRemainder(numerator: Int32, denominator: Int32) -> Int32 { + return int32_remainder(numerator, denominator) +} + +/// Various `clamp` cover methods use this. +extension Comparable { + @_spi(SwiftCovers) + @inline(__always) + public func clamped(min: Self, max: Self) -> Self { + return self < min ? min : self > max ? max : self + } +} + +extension Vector2i { + /// Godot compares `Vector2i` lexicographically. This `tuple` property allows us a trivial cover implementation in Swift, because the Swift standard library has lexicographic comparison operators for tuples of up to six elements. + @_spi(SwiftCovers) + @inline(__always) + public var tuple: (Int32, Int32) { (x, y) } +} From 2d62f9bb8e5fd9f3e4d40bbf84a99f7af761448d Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 19 Nov 2024 16:40:44 -0600 Subject: [PATCH 18/99] get Vector2i covers to compile --- Sources/SwiftCovers/Vector2i.covers.swift | 53 ++++++++--------------- 1 file changed, 17 insertions(+), 36 deletions(-) diff --git a/Sources/SwiftCovers/Vector2i.covers.swift b/Sources/SwiftCovers/Vector2i.covers.swift index dc0268aee..e69587f25 100644 --- a/Sources/SwiftCovers/Vector2i.covers.swift +++ b/Sources/SwiftCovers/Vector2i.covers.swift @@ -1,36 +1,7 @@ -@_implementationOnly import CSwiftCovers -import SwiftGodot - -extension Swift.Comparable { - func clamped(min: Self, max: Self) -> Self { - return self < min ? min : self > max ? max : self - } -} - -extension Swift.Int64 { - func checkIndex(max: Int64) -> Int64 { - assert(self >= 0) - assert(self <= max) - return self - } -} +@_spi(SwiftCovers) import SwiftGodot extension Vector2i { - var tuple: (Int32, Int32) { (x, y) } - - private static func cCastToInt32(_ float: Float) -> Int32 { - return int32_for_float(float) - } - - private static func cDivide(numerator: Int32, denominator: Int32) -> Int32 { - return int32_divide(numerator, denominator) - } - - private static func cRemainder(numerator: Int32, denominator: Int32) -> Int32 { - return int32_remainder(numerator, denominator) - } - public init(from: Vector2i) { self = from } @@ -45,22 +16,32 @@ extension Vector2i { ret */ - self.init(x: Self.cCastToInt32(from.x), y: Self.cCastToInt32(from.y)) + self.init(x: cCastToInt32(from.x), y: cCastToInt32(from.y)) } public func aspect() -> Double { return Double(Float(x) / Float(y)) } - public func maxAxisIndex() -> Int64 { (x < y ? Axis.y : .x).rawValue } + public func maxAxisIndex() -> Int64 { + return (x < y ? Axis.y : .x).rawValue + } - public func minAxisIndex() -> Int64 { (x < y ? Axis.x : .y).rawValue } + public func minAxisIndex() -> Int64 { + return (x < y ? Axis.x : .y).rawValue + } - public func distanceTo(_ to: Vector2i) -> Double { (to - self).length() } + public func distanceTo(_ to: Vector2i) -> Double { + return (to - self).length() + } - public func distanceSquaredTo(_ to: Vector2i) -> Double { Double((to - self).lengthSquared()) } + public func distanceSquaredTo(_ to: Vector2i) -> Double { + return Double((to - self).lengthSquared()) + } - public func length() -> Double { Double(lengthSquared()).squareRoot() } + public func length() -> Double { + return Double(lengthSquared()).squareRoot() + } public func lengthSquared() -> Int64 { let x = Int64(x) From 7b2b5a624cb59456f4c8542713e9d1b9c605c4b2 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 19 Nov 2024 18:13:00 -0600 Subject: [PATCH 19/99] clean up conditional compilation generation --- Generator/Generator/BuiltinGen.swift | 151 +++++++++++++-------------- Generator/Generator/Printer.swift | 17 +++ 2 files changed, 88 insertions(+), 80 deletions(-) diff --git a/Generator/Generator/BuiltinGen.swift b/Generator/Generator/BuiltinGen.swift index 6cb41529a..b9dc482d8 100644 --- a/Generator/Generator/BuiltinGen.swift +++ b/Generator/Generator/BuiltinGen.swift @@ -115,97 +115,88 @@ func generateBuiltinCtors (_ p: Printer, } p ("\(visibility) init (\(args))") { - // Determine if we have a constructors whose sole job is to initialize the members - // of the struct, in that case, just do that, do not call into Godot. - if let margs = m.arguments, let members, margs.count == members.count { - var constructorMatchesFields = true - for x in 0..(_ customImp: T?, _ ifCustom: (T) -> (), else otherwise: () -> ()) { + if let customImp { + p ("#if CUSTOM_BUILTIN_IMPLEMENTATIONS") + ifCustom(customImp) + p ("#else // CUSTOM_BUILTIN_IMPLEMENTATIONS") + otherwise() + p ("#endif // CUSTOM_BUILTIN_IMPLEMENTATIONS\n") + } else { + otherwise() + } + } + // Prints a block, automatically indents the code in the closure func b (_ str: String, arg: String? = nil, suffix: String = "", block: () -> ()) { p (str + " {" + (arg ?? "")) From 17758e07e60fa7201cf42875bd2c8258855a98d9 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 19 Nov 2024 18:45:17 -0600 Subject: [PATCH 20/99] emit cover for subscript --- Generator/Generator/BuiltinGen.swift | 30 +++++++++++++++-- Generator/Generator/SwiftCovers.swift | 46 +++++++++++++++++++-------- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/Generator/Generator/BuiltinGen.swift b/Generator/Generator/BuiltinGen.swift index b9dc482d8..906e65672 100644 --- a/Generator/Generator/BuiltinGen.swift +++ b/Generator/Generator/BuiltinGen.swift @@ -568,8 +568,34 @@ func generateBuiltinMethods (_ p: Printer, } """) } - if let returnType = bc.indexingReturnType, !bc.isKeyed, !bc.name.hasSuffix ("Array"), bc.name != "String" { - let godotType = getGodotType (JGodotReturnValue (type: returnType, meta: nil)) + + generateBuiltinIndexedSubscript(p, bc, typeName) +} + +private func generateBuiltinIndexedSubscript ( + _ p: Printer, + _ bc: JGodotBuiltinClass, + _ typeName: String +) { + guard + let returnType = bc.indexingReturnType, + !bc.isKeyed, + !bc.name.hasSuffix ("Array"), + bc.name != "String" + else { return } + + let godotType = getGodotType (JGodotReturnValue (type: returnType, meta: nil)) + + let key = SwiftCovers.Key( + type: typeName, + name: "subscript", + parameterTypes: ["Int64"], + returnType: godotType + ) + + p.ifCustomBuiltinImplementation(swiftCovers.covers[key]) { + p($0) + } else: { let variantType = builtinTypecode (bc.name) p.staticVar (visibility: "private ", name: "indexed_getter", type: "GDExtensionPtrIndexedGetter") { p ("return gi.variant_get_ptr_indexed_getter (\(variantType))!") diff --git a/Generator/Generator/SwiftCovers.swift b/Generator/Generator/SwiftCovers.swift index 9883aea67..76a299e91 100644 --- a/Generator/Generator/SwiftCovers.swift +++ b/Generator/Generator/SwiftCovers.swift @@ -82,11 +82,16 @@ struct SwiftCovers { if extractInitCover(from: member, of: type) { return } + + if extractSubscriptCover(from: member, of: type) { + return + } } private mutating func extractInitCover(from member: MemberBlockItemSyntax, of type: String) -> Bool { guard let initer = member.decl.as(InitializerDeclSyntax.self), + initer.modifiers.map({ $0.name.tokenKind }) == [.keyword(.public)], initer.genericWhereClause == nil, initer.genericParameterClause == nil, case let signature = initer.signature, @@ -95,9 +100,7 @@ struct SwiftCovers { .compactMap({ $0.type.as(IdentifierTypeSyntax.self)?.name.text }), parameterTypes.count == signature.parameterClause.parameters.count, let body = initer.body - else { - return false - } + else { return false } let key = Key( type: type, @@ -111,6 +114,29 @@ struct SwiftCovers { return true } + private mutating func extractSubscriptCover(from member: MemberBlockItemSyntax, of type: String) -> Bool { + guard + let subs = member.decl.as(SubscriptDeclSyntax.self), + subs.modifiers.map({ $0.name.tokenKind }) == [.keyword(.public)], + subs.genericWhereClause == nil, + subs.genericParameterClause == nil, + case let parameterTypes = subs.parameterClause.parameters + .compactMap({ $0.type.as(IdentifierTypeSyntax.self)?.name.text }), + parameterTypes.count == subs.parameterClause.parameters.count, + let returnType = subs.returnClause.type.as(IdentifierTypeSyntax.self)?.name.text + else { return false } + + let key = Key( + type: type, + name: "subscript", + parameterTypes: parameterTypes, + returnType: returnType + ) + + covers[key] = subs.description + return true + } + private mutating func extractFunctionCover(from function: FunctionDeclSyntax, of type: String) -> Bool { guard function.modifiers.map({ $0.name.tokenKind }) == [.keyword(.public)], @@ -120,9 +146,7 @@ struct SwiftCovers { .compactMap({ $0.type.as(IdentifierTypeSyntax.self)?.name.text }), parameterTypes.count == signature.parameterClause.parameters.count, let body = function.body - else { - return false - } + else { return false } let returnType: String if let returnTypeSx = signature.returnClause?.type.as(IdentifierTypeSyntax.self) { @@ -141,7 +165,6 @@ struct SwiftCovers { returnType: returnType ) - print("found cover for \(key)") covers[key] = fixCodeBlockIndentation(body) return true } @@ -155,9 +178,7 @@ struct SwiftCovers { .compactMap({ $0.type.as(IdentifierTypeSyntax.self)?.name.text }), parameterTypes.count == signature.parameterClause.parameters.count, let body = function.body - else { - return false - } + else { return false } let returnType: String if let returnTypeSx = signature.returnClause?.type.as(IdentifierTypeSyntax.self) { @@ -176,12 +197,11 @@ struct SwiftCovers { returnType: returnType ) - print("found cover for \(key)") covers[key] = fixCodeBlockIndentation(body) return true } - - private func fixCodeBlockIndentation(_ block: CodeBlockSyntax) -> String { + + private func fixCodeBlockIndentation(_ block: some SyntaxProtocol) -> String { var lines = block.description.split(separator: "\n") let whitespace = lines.last!.prefix(while: { $0.isWhitespace } ) lines[0] = whitespace + "do " + lines[0] From 303b6ed95a146561dd49087a8f27e1dcfef12030 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 19 Nov 2024 18:48:54 -0600 Subject: [PATCH 21/99] only process .swift files in SwiftCovers --- Generator/Generator/SwiftCovers.swift | 2 +- Plugins/CodeGeneratorPlugin/plugin.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Generator/Generator/SwiftCovers.swift b/Generator/Generator/SwiftCovers.swift index 76a299e91..68328b73c 100644 --- a/Generator/Generator/SwiftCovers.swift +++ b/Generator/Generator/SwiftCovers.swift @@ -39,7 +39,7 @@ struct SwiftCovers { return } - for url in urls { + for url in urls where url.pathExtension == "swift" { let source: String do { source = try String(contentsOf: url, encoding: .utf8) diff --git a/Plugins/CodeGeneratorPlugin/plugin.swift b/Plugins/CodeGeneratorPlugin/plugin.swift index 24322a3a9..c2cf4c288 100644 --- a/Plugins/CodeGeneratorPlugin/plugin.swift +++ b/Plugins/CodeGeneratorPlugin/plugin.swift @@ -20,9 +20,9 @@ import PackagePlugin let api = context.package.directory.appending(["Sources", "ExtensionApi", "extension_api.json"]) let coverSourcesDir = context.package.directory.appending(["Sources", "SwiftCovers"]) - let coverSources = try FileManager.default.contentsOfDirectory(atPath: coverSourcesDir.string).map { - coverSourcesDir.appending(subpath: $0) - } + let coverSources = try FileManager.default.contentsOfDirectory(atPath: coverSourcesDir.string) + .filter { $0.hasSuffix(".swift") } + .map { coverSourcesDir.appending(subpath: $0) } var arguments: [CustomStringConvertible] = [ api, genSourcesDir ] var outputFiles: [Path] = [] From a92da0b6f01f55beb33846b048e40c36329750cf Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 19 Nov 2024 19:06:57 -0600 Subject: [PATCH 22/99] write README for SwiftCovers --- Sources/SwiftCovers/README.md | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 Sources/SwiftCovers/README.md diff --git a/Sources/SwiftCovers/README.md b/Sources/SwiftCovers/README.md new file mode 100644 index 000000000..c11d92fd7 --- /dev/null +++ b/Sources/SwiftCovers/README.md @@ -0,0 +1,50 @@ + +## What's in this folder + +The Swift source files in this folder are not compiled as part of the SwiftGodot library. + +`Generator` (the thing that reads `extension_api.json` and spits out a zillion Swift source files) parses the Swift source files in this directory and extracts method bodies. It copies those method bodies into the generated files as alternative implementations of methods that would otherwise be calls into the Godot engine through the Godot foreign function interface (FFI). Here's an example for `Vector2i`: + +```swift + /// Returns the component-wise minimum of this and `with`, equivalent to `Vector2i(mini(x, with.x), mini(y, with.y))`. + public func min(with: Vector2i)-> Vector2i { + #if !CUSTOM_BUILTIN_IMPLEMENTATIONS + var result: Vector2i = Vector2i() + withUnsafePointer(to: with) { pArg0 in + withUnsafePointer(to: UnsafeRawPointersN1(pArg0)) { pArgs in + pArgs.withMemoryRebound(to: UnsafeRawPointer?.self, capacity: 1) { pArgs in + var mutSelfCopy = self + withUnsafeMutablePointer (to: &mutSelfCopy) { ptr in + Vector2i.method_min(ptr, pArgs, &result, 1) + } + } + + } + + } + + return result + #else // CUSTOM_BUILTIN_IMPLEMENTATIONS + do { + return Vector2i( + x: Swift.min(x, with.x), + y: Swift.min(y, with.y) + ) + } + #endif + } +``` + +The code between `#if` and `#else` calls an engine function. The code between `#else` and `#endif` is a hand-written Swift implementation that `Generator` copied from `Vector2i.covers.swift`. We call the hand-written implementation a “cover” for the engine function. + +## Editing covers + +If you're editing the contents of this folder, you may want to set your IDE to build the `SwiftCovers` library. This should give you syntax highlighting and auto-completion. You shouldn't use the `SwiftCovers` library for anything else. + +To add a new cover, write a new method in a file in this folder. The method needs to be an `extension` for the appropriate type, be `public`, and have the same name, argument types, and return types as the function generated by `Generator`. + +## Some implementation details + +`Generator` copies over an entire method body, including the braces surrounding the method body. This preserves any comments at the start or end of the method body. Undecorated braces normally create a closure in Swift, so `Generator` inserts the `do` keyword in front of the opening brace, which tells Swift to just run the code inside the block instead of creating a closure. + +Subscript declarations in Swift can be substantially more complicated than method declarations, because a subscript declaration can have a `get` block, or a `set` block, or both, or just an implicit `get` block. So, for subscripts, `Generator` copies over the entire subscript declaration instead of just the body. From 27d735b15f9756e661c01e9b7d3db914ba0d2356 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 19 Nov 2024 19:06:57 -0600 Subject: [PATCH 23/99] add GodotTestPattern environment variable to filter tests that require the engine --- .../GodotTestRunner.swift | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/Sources/SwiftGodotTestability/GodotTestRunner.swift b/Sources/SwiftGodotTestability/GodotTestRunner.swift index b9bbfb9b0..b15be74c9 100644 --- a/Sources/SwiftGodotTestability/GodotTestRunner.swift +++ b/Sources/SwiftGodotTestability/GodotTestRunner.swift @@ -41,28 +41,51 @@ class __GodotTestRunner: XCTestCase { } } + private static let testNamePatternFromEnvironment: Regex? = ProcessInfo.processInfo + .environment["GodotTestPattern"] + .map { try! Regex($0) } + + private static func testNamePatternFromEnvironmentMatches(_ test: XCTest) -> Bool { + guard let testNamePatternFromEnvironment else { return true } + // Convert name from e.g. "-[ColorTests testSaturation]" to "ColorTests/testSaturation". + let name = test.name + .replacingOccurrences(of: "-[", with: "") + .replacingOccurrences(of: "]", with: "") + .replacingOccurrences(of: " ", with: "/") + return name.wholeMatch(of: testNamePatternFromEnvironment) != nil + } + /// Extract all the godot tests from a tree of XCTest objects. /// This flattens the tree into a a suite containing only the suites /// with tests that are subclasses of GodotTestCase. /// Any suites that don't contain any Godot tests are skipped, since /// they will be run in the normal XCTest runtime. @discardableResult static func extractGodotTests(_ from: XCTest, into: XCTestSuite) -> Bool { - guard let suite = from as? XCTestSuite else { return false } - if suite.containsGodotTests { - into.addTest(suite) - return true - } else { + switch from { + case let suite as XCTestSuite: + if suite.containsGodotTests && testNamePatternFromEnvironmentMatches(suite) { + into.addTest(suite) + return true + } + var hadTests = false for test in suite.tests { - if !extractGodotTests(test, into: into) { + if extractGodotTests(test, into: into) { + hadTests = true + } else { #if DEBUG_SKIPPING - print("Skipped \(test.name) as it has no Godot tests") + print("Skipped \(test.name) as it has no Godot tests or is excluded by the environment test pattern") #endif - } else { - hadTests = true } } return hadTests + + case let test: + if test is GodotTestCase && testNamePatternFromEnvironmentMatches(test) { + into.addTest(test) + return true + } + return false } } @@ -154,4 +177,4 @@ extension XCTestSuite { } return false } -} \ No newline at end of file +} From fa5e8171b0cb2519069cc1823dff43664f4a6a3b Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 19 Nov 2024 19:06:57 -0600 Subject: [PATCH 24/99] pass CUSTOM_BUILTIN_IMPLEMENTATIONS compilation condition to SwiftGodotTests too This lets the tests for Swift cover implementations be self-updating when using Godot engine implementations. --- Package.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index d692f5044..fff9f114a 100644 --- a/Package.swift +++ b/Package.swift @@ -171,6 +171,11 @@ let libgodot_tests = Target .binaryTarget( ) #endif +let customBuiltinImplementationsSettings: [SwiftSetting] = [ + // Comment this out to use engine methods for everything. + .define("CUSTOM_BUILTIN_IMPLEMENTATIONS"), +] + targets.append(contentsOf: [ // Godot runtime as a library @@ -190,7 +195,8 @@ targets.append(contentsOf: [ name: "SwiftGodotTests", dependencies: [ "SwiftGodotTestability", - ] + ], + swiftSettings: customBuiltinImplementationsSettings ), // Runtime dependant tests based on the engine tests from Godot's repository @@ -211,9 +217,7 @@ targets.append(contentsOf: [ name: "SwiftGodot", dependencies: ["GDExtension", "CWrappers"], //linkerSettings: linkerSettings, - swiftSettings: [ - .define("CUSTOM_BUILTIN_IMPLEMENTATIONS") - ], + swiftSettings: customBuiltinImplementationsSettings, plugins: swiftGodotPlugins ), From aad8a51690460afa0c5300542afd47c5e8eedd21 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 11:37:54 -0600 Subject: [PATCH 25/99] sketch out self-updating tests --- .../BuiltIn/Vector2iTests.swift | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift index 470462430..8604fc1c1 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift @@ -7,10 +7,81 @@ import XCTest import SwiftGodotTestability +import RegexBuilder @testable import SwiftGodot final class Vector2iTests: GodotTestCase { - + + static let rx_ws = ZeroOrMore { ChoiceOf { " "; "\t" } } + static let prefixCapture = Reference() + static let suffixCapture = Reference() + static let rx_check: Regex = Regex { + Anchor.startOfLine + Capture(as: prefixCapture) { + rx_ws + "check(" + OneOrMore { + CharacterClass.anyNonNewline + } + ", is:" + rx_ws + } + "\"" + ZeroOrMore { + CharacterClass.anyNonNewline + } + "\"" + Capture(as: suffixCapture) { + ")" + rx_ws + } + Anchor.endOfLine + } + +#if CUSTOM_BUILTIN_IMPLEMENTATIONS + // I only update the test cases if I'm calling the Godot engine, which is authoritative. In this case, I'm using Swift covers, and the point of the tests is to verify that the covers match the engine's behavior. So it would be incorrect to update the test cases in this case. + static let shouldUpdateTestCases: Bool = false +#else + static let shouldUpdateTestCases: Bool = ProcessInfo.processInfo.environment["UpdateSwiftCoverTestCases", default: ""] != "" +#endif + + func check( + _ actual: V, + is expected: String, + file: StaticString = #filePath, + lineNumber: UInt = #line + ) { + guard Self.shouldUpdateTestCases else { + XCTAssertEqual(String(describing: actual), expected, file: file, line: lineNumber) + return + } + + // I'm using Godot engine functions, not Swift cover implementations. + // I treat the Godot engine as a test oracle: it produces the correct answers. + + let originalFile = try! String(contentsOfFile: file.description) + var lines = originalFile.split(separator: "\n", omittingEmptySubsequences: false) + let line = String(lines[Int(lineNumber) - 1]) + guard let match = line.wholeMatch(of: Self.rx_check) else { + XCTFail("couldn't match check call", file: file, line: lineNumber) + return + } + // String(reflecting:) quotes and escapes the description of `actual`. + let newExpected = String(reflecting: String(describing: actual)) + let newLine = "\(match[Self.prefixCapture])\(newExpected)\(match[Self.suffixCapture])" + lines[Int(lineNumber) - 1] = newLine[...] + let newFile = lines.joined(separator: "\n") + try! newFile.write(toFile: file.description, atomically: true, encoding: .utf8) + } + + func testInitFrom() { + check(Vector2i(from: Vector2i(x: 0, y: 0)), is: "Vector2i(x: 0, y: 0)") + check(Vector2i(from: Vector2i(x: 1, y: 1)), is: "Vector2i(x: 1, y: 1)") + check(Vector2i(from: Vector2i(x: 2, y: 2147483647)), is: "Vector2i(x: 2, y: 2147483647)") + check(Vector2i(from: Vector2i(x: 3, y: -1)), is: "Vector2i(x: 3, y: -1)") + check(Vector2i(from: Vector2i(x: 4, y: -2147483648)), is: "Vector2i(x: 4, y: -2147483648)") + } + func testOperatorUnaryMinus () { var value: Vector2i From 13388c99968b646c37a881ed7d31b89a318878bb Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 12:25:50 -0600 Subject: [PATCH 26/99] change ifCustomBuiltinImplementation to useSwiftCoverIfAvailable and use it everywhere --- Generator/Generator/BuiltinGen.swift | 116 +++++++++++---------------- Generator/Generator/Printer.swift | 12 +-- 2 files changed, 51 insertions(+), 77 deletions(-) diff --git a/Generator/Generator/BuiltinGen.swift b/Generator/Generator/BuiltinGen.swift index 906e65672..5ecafbacd 100644 --- a/Generator/Generator/BuiltinGen.swift +++ b/Generator/Generator/BuiltinGen.swift @@ -119,10 +119,7 @@ func generateBuiltinCtors (_ p: Printer, getGodotType(SimpleType (type: arg.type), kind: .builtIn) } ?? [] let key = SwiftCovers.Key(type: typeName, name: "init", parameterTypes: parameterTypes, returnType: bc.name) - p.ifCustomBuiltinImplementation(swiftCovers.covers[key]?.description) { - p($0) - } else: { - + p.useSwiftCoverIfAvailable(for: key) { // Determine if we have a constructors whose sole job is to initialize the members // of the struct, in that case, just do that, do not call into Godot. if let margs = m.arguments, let members, margs.count == members.count { @@ -369,63 +366,52 @@ func generateBuiltinOperators (_ p: Printer, let lhsTypeName = typeName let rhsTypeName = getGodotType(SimpleType(type: right), kind: .builtIn) - let key = SwiftCovers.Key(type: typeName, name: swiftOperator, parameterTypes: [lhsTypeName, rhsTypeName], returnType: retType) - let customImplementation = swiftCovers.covers[key]?.description - if let desc = op.description, desc != "" { doc (p, bc, desc) } p ("public static func \(swiftOperator) (lhs: \(lhsTypeName), rhs: \(rhsTypeName)) -> \(retType) "){ - if customImplementation != nil { - - p("#if !CUSTOM_BUILTIN_IMPLEMENTATIONS") - } - - let ptrResult: String - if op.returnType == "String" && mapStringToSwift { - p ("let result = GString ()") - } else { - var declType: String = "var" - if builtinGodotTypeNames [op.returnType] == .isClass { - declType = "let" + let key = SwiftCovers.Key(type: typeName, name: swiftOperator, parameterTypes: [lhsTypeName, rhsTypeName], returnType: retType) + p.useSwiftCoverIfAvailable(for: key) { + let ptrResult: String + if op.returnType == "String" && mapStringToSwift { + p ("let result = GString ()") + } else { + var declType: String = "var" + if builtinGodotTypeNames [op.returnType] == .isClass { + declType = "let" + } + p ("\(declType) result: \(retType) = \(retType)()") + } + let isStruct = isStructMap [op.returnType] ?? false + if isStruct { + ptrResult = "&result" + } else { + ptrResult = "&result.content" + } + let lhsa = try! MethodArgument( + from: JGodotArgument(name: "lhs", type: godotTypeName, defaultValue: nil, meta: nil), + typeName: godotTypeName, + methodName: "#operator\(swiftOperator)", + options: .builtInClassOptions + ) + + let rhsa = try! MethodArgument( + from: JGodotArgument(name: "rhs", type: right, defaultValue: nil, meta: nil), + typeName: godotTypeName, + methodName: "#operator\(swiftOperator)", + options: .builtInClassOptions + ) + + preparingArguments(p, arguments: [lhsa, rhsa]) { + p("\(typeName).\(ptrName)(pArg0, pArg1, \(ptrResult))") + } + + if op.returnType == "String" && mapStringToSwift { + p ("return result.description") + } else { + p ("return result") } - p ("\(declType) result: \(retType) = \(retType)()") - } - let isStruct = isStructMap [op.returnType] ?? false - if isStruct { - ptrResult = "&result" - } else { - ptrResult = "&result.content" - } - let lhsa = try! MethodArgument( - from: JGodotArgument(name: "lhs", type: godotTypeName, defaultValue: nil, meta: nil), - typeName: godotTypeName, - methodName: "#operator\(swiftOperator)", - options: .builtInClassOptions - ) - - let rhsa = try! MethodArgument( - from: JGodotArgument(name: "rhs", type: right, defaultValue: nil, meta: nil), - typeName: godotTypeName, - methodName: "#operator\(swiftOperator)", - options: .builtInClassOptions - ) - - preparingArguments(p, arguments: [lhsa, rhsa]) { - p("\(typeName).\(ptrName)(pArg0, pArg1, \(ptrResult))") - } - - if op.returnType == "String" && mapStringToSwift { - p ("return result.description") - } else { - p ("return result") - } - - if let customImplementation { - p("#else // CUSTOM_BUILTIN_IMPLEMENTATIONS") - p(customImplementation) - p("#endif") } } } @@ -512,20 +498,10 @@ func generateBuiltinMethods (_ p: Printer, getGodotType(SimpleType (type: arg.type), kind: .builtIn) } ?? [] - let key = SwiftCovers.Key(type: typeName, name: methodName, parameterTypes: parameterTypes, returnType: ret) - let customImplementation = swiftCovers.covers[key]?.description - p ("public\(keyword) func \(methodName)(\(args))\(retSig)") { - if customImplementation != nil { - p("#if !CUSTOM_BUILTIN_IMPLEMENTATIONS") - } - - generateMethodCall (p, typeName: typeName, methodToCall: ptrName, godotReturnType: m.returnType, isStatic: m.isStatic, isVararg: m.isVararg, arguments: m.arguments ?? []) - - if let customImplementation { - p("#else // CUSTOM_BUILTIN_IMPLEMENTATIONS") - p(customImplementation) - p("#endif") + let key = SwiftCovers.Key(type: typeName, name: methodName, parameterTypes: parameterTypes, returnType: ret) + p.useSwiftCoverIfAvailable(for: key) { + generateMethodCall (p, typeName: typeName, methodToCall: ptrName, godotReturnType: m.returnType, isStatic: m.isStatic, isVararg: m.isVararg, arguments: m.arguments ?? []) } } } @@ -593,9 +569,7 @@ private func generateBuiltinIndexedSubscript ( returnType: godotType ) - p.ifCustomBuiltinImplementation(swiftCovers.covers[key]) { - p($0) - } else: { + p.useSwiftCoverIfAvailable(for: key) { let variantType = builtinTypecode (bc.name) p.staticVar (visibility: "private ", name: "indexed_getter", type: "GDExtensionPtrIndexedGetter") { p ("return gi.variant_get_ptr_indexed_getter (\(variantType))!") diff --git a/Generator/Generator/Printer.swift b/Generator/Generator/Printer.swift index a1d9a6f1e..1471f01cb 100644 --- a/Generator/Generator/Printer.swift +++ b/Generator/Generator/Printer.swift @@ -88,15 +88,15 @@ class Printer { } } - /// Emit conditional compilation directives if needed. + /// Emit conditional compilation directives and a Swift cover implementation, if there is a Swift cover implementation. /// - /// If `customImp` is nil, I just call `otherwise`. + /// If there is a cover for `key`, I emit an `#if ... #else ... #endif` structure which compiles `cover` when `CUSTOM_BUILTIN_IMPLEMENTATIONS` is set, and compiles the output of `otherwise` when `CUSTOM_BUILTIN_IMPLEMENTATIONS` is not set. /// - /// If `customImp` is **not** nil, I emit an `#if ... #else ... #endif` structure which compiles the output of `ifCustom` when `CUSTOM_BUILTIN_IMPLEMENTATIONS` is set, and compiles the output of `otherwise` when `CUSTOM_BUILTIN_IMPLEMENTATIONS` is not set. - func ifCustomBuiltinImplementation(_ customImp: T?, _ ifCustom: (T) -> (), else otherwise: () -> ()) { - if let customImp { + /// If there is no cover for `key`, I just emit `otherwise()`. + func useSwiftCoverIfAvailable(for key: SwiftCovers.Key, otherwise: () -> ()) { + if let cover = swiftCovers.covers[key] { p ("#if CUSTOM_BUILTIN_IMPLEMENTATIONS") - ifCustom(customImp) + p(cover) p ("#else // CUSTOM_BUILTIN_IMPLEMENTATIONS") otherwise() p ("#endif // CUSTOM_BUILTIN_IMPLEMENTATIONS\n") From 246238741328b3f8716a5fee61c9f6743af9cc13 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 13:02:07 -0600 Subject: [PATCH 27/99] emit covers for subscript accessors individually This will let me implement TESTABLE_SWIFT_COVERS. --- Generator/Generator/BuiltinGen.swift | 43 ++++++++++++++++----------- Generator/Generator/SwiftCovers.swift | 35 +++++++++++++++------- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/Generator/Generator/BuiltinGen.swift b/Generator/Generator/BuiltinGen.swift index 5ecafbacd..b8ec9f6ae 100644 --- a/Generator/Generator/BuiltinGen.swift +++ b/Generator/Generator/BuiltinGen.swift @@ -562,28 +562,37 @@ private func generateBuiltinIndexedSubscript ( let godotType = getGodotType (JGodotReturnValue (type: returnType, meta: nil)) - let key = SwiftCovers.Key( - type: typeName, - name: "subscript", - parameterTypes: ["Int64"], - returnType: godotType - ) - p.useSwiftCoverIfAvailable(for: key) { - let variantType = builtinTypecode (bc.name) - p.staticVar (visibility: "private ", name: "indexed_getter", type: "GDExtensionPtrIndexedGetter") { - p ("return gi.variant_get_ptr_indexed_getter (\(variantType))!") - } - p.staticVar (visibility: "private ", name: "indexed_setter", type: "GDExtensionPtrIndexedSetter") { - p ("return gi.variant_get_ptr_indexed_setter (\(variantType))!") - } - p (" public subscript (index: Int64) -> \(godotType)") { - p ("mutating get") { + + let variantType = builtinTypecode (bc.name) + p.staticVar (visibility: "private ", name: "indexed_getter", type: "GDExtensionPtrIndexedGetter") { + p ("return gi.variant_get_ptr_indexed_getter (\(variantType))!") + } + p.staticVar (visibility: "private ", name: "indexed_setter", type: "GDExtensionPtrIndexedSetter") { + p ("return gi.variant_get_ptr_indexed_setter (\(variantType))!") + } + p (" public subscript (index: Int64) -> \(godotType)") { + p ("mutating get") { + let key = SwiftCovers.Key( + type: typeName, + name: "subscript.get", + parameterTypes: ["Int64"], + returnType: godotType + ) + p.useSwiftCoverIfAvailable(for: key) { p ("var result = \(godotType) ()") p ("Self.indexed_getter (&self, index, &result)") p ("return result") } - p ("set") { + } + p ("set") { + let key = SwiftCovers.Key( + type: typeName, + name: "subscript.set", + parameterTypes: ["Int64"], + returnType: godotType + ) + p.useSwiftCoverIfAvailable(for: key) { p ("var value = newValue") p ("Self.indexed_setter (&self, index, &value)") } diff --git a/Generator/Generator/SwiftCovers.swift b/Generator/Generator/SwiftCovers.swift index 68328b73c..3ddbac890 100644 --- a/Generator/Generator/SwiftCovers.swift +++ b/Generator/Generator/SwiftCovers.swift @@ -83,7 +83,7 @@ struct SwiftCovers { return } - if extractSubscriptCover(from: member, of: type) { + if extractSubscriptCovers(from: member, of: type) { return } } @@ -114,7 +114,7 @@ struct SwiftCovers { return true } - private mutating func extractSubscriptCover(from member: MemberBlockItemSyntax, of type: String) -> Bool { + private mutating func extractSubscriptCovers(from member: MemberBlockItemSyntax, of type: String) -> Bool { guard let subs = member.decl.as(SubscriptDeclSyntax.self), subs.modifiers.map({ $0.name.tokenKind }) == [.keyword(.public)], @@ -123,17 +123,32 @@ struct SwiftCovers { case let parameterTypes = subs.parameterClause.parameters .compactMap({ $0.type.as(IdentifierTypeSyntax.self)?.name.text }), parameterTypes.count == subs.parameterClause.parameters.count, - let returnType = subs.returnClause.type.as(IdentifierTypeSyntax.self)?.name.text + let returnType = subs.returnClause.type.as(IdentifierTypeSyntax.self)?.name.text, + let accessorBlock = subs.accessorBlock else { return false } - let key = Key( - type: type, - name: "subscript", - parameterTypes: parameterTypes, - returnType: returnType - ) + func record(_ cover: CodeBlockItemListSyntax, forAccessType accessType: String) { + let key = Key( + type: type, + name: "subscript.\(accessType)", + parameterTypes: parameterTypes, + returnType: returnType + ) + + covers[key] = cover.description + } + + switch accessorBlock.accessors { + case .accessors(let accessors): + for accessor in accessors { + if let body = accessor.body { + record(body.statements, forAccessType: accessor.accessorSpecifier.text) + } + } + case .getter(let getter): + record(getter, forAccessType: "get") + } - covers[key] = subs.description return true } From 5a3941d3091d38ac4a60f09130cd0586ea814c28 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 12:47:37 -0600 Subject: [PATCH 28/99] add TESTABLE_SWIFT_COVERS --- Generator/Generator/BuiltinGen.swift | 10 ++++++++-- Generator/Generator/Printer.swift | 8 ++++++++ Package.swift | 15 +++++++++------ Sources/SwiftGodot/SwiftCoverSupport.swift | 2 ++ 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/Generator/Generator/BuiltinGen.swift b/Generator/Generator/BuiltinGen.swift index b8ec9f6ae..afec44425 100644 --- a/Generator/Generator/BuiltinGen.swift +++ b/Generator/Generator/BuiltinGen.swift @@ -169,8 +169,14 @@ func generateBuiltinCtors (_ p: Printer, p ("self.z = Vector4 (x: 0, y: 0, z: 1, w: 0)") p ("self.w = Vector4 (x: 0, y: 0, z: 0, w: 1)") } else { - for x in members { - p ("self.\(x.name) = \(MemberBuiltinJsonTypeToSwift(x.type)) ()") + if args == "" { + // This is the default initializer, so I must initialize each stored property. + for x in members { + p ("self.\(x.name) = \(MemberBuiltinJsonTypeToSwift(x.type)) ()") + } + } else { + // This is some other initializer, so I can delegate to the default initializer to initialize my properties. + p("self.init()") } } // Another special case: empty constructors in generated structs (those we added fields for) diff --git a/Generator/Generator/Printer.swift b/Generator/Generator/Printer.swift index 1471f01cb..15d38c386 100644 --- a/Generator/Generator/Printer.swift +++ b/Generator/Generator/Printer.swift @@ -95,11 +95,19 @@ class Printer { /// If there is no cover for `key`, I just emit `otherwise()`. func useSwiftCoverIfAvailable(for key: SwiftCovers.Key, otherwise: () -> ()) { if let cover = swiftCovers.covers[key] { +#if TESTABLE_SWIFT_COVERS + self.if("useSwiftCovers") { + p(cover) + } else: { + otherwise() + } +#else p ("#if CUSTOM_BUILTIN_IMPLEMENTATIONS") p(cover) p ("#else // CUSTOM_BUILTIN_IMPLEMENTATIONS") otherwise() p ("#endif // CUSTOM_BUILTIN_IMPLEMENTATIONS\n") +#endif } else { otherwise() } diff --git a/Package.swift b/Package.swift index fff9f114a..d8f34b8aa 100644 --- a/Package.swift +++ b/Package.swift @@ -19,6 +19,14 @@ libraryType = .static libraryType = .dynamic #endif +let customBuiltinImplementationsSettings: [SwiftSetting] = [ + // Comment this out to use engine methods for everything. If this is set, Swift cover implementations are used where available. + .define("CUSTOM_BUILTIN_IMPLEMENTATIONS"), + + // Define this to generate code that can choose between a Swift cover and a Godot engine call at runtime. This is slower but allows easy testing that each Swift cover behaves exactly like the Godot engine function it replaces. This controls the behavior of the Generator build tool; there is no other good way to configure how Generator operates. + // .define("TESTABLE_SWIFT_COVERS"), +] + // Products define the executables and libraries a package produces, and make them visible to other packages. var products: [Product] = [ .library( @@ -90,7 +98,7 @@ var targets: [Target] = [ swiftSettings: [ // Uncomment for using legacy array-based marshalling //.define("LEGACY_MARSHALING") - ] + ] + customBuiltinImplementationsSettings ), // This is a build-time plugin that invokes the generator and produces @@ -171,11 +179,6 @@ let libgodot_tests = Target .binaryTarget( ) #endif -let customBuiltinImplementationsSettings: [SwiftSetting] = [ - // Comment this out to use engine methods for everything. - .define("CUSTOM_BUILTIN_IMPLEMENTATIONS"), -] - targets.append(contentsOf: [ // Godot runtime as a library diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index 68b1fe0da..046639f62 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -38,3 +38,5 @@ extension Vector2i { @inline(__always) public var tuple: (Int32, Int32) { (x, y) } } + +internal var useSwiftCovers = true From 5ca310362721c45a334950da8eb604d5b77ff2ce Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 14:54:31 -0600 Subject: [PATCH 29/99] add useSwiftCovers global for use by tests --- Sources/SwiftGodot/SwiftCoverSupport.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index 046639f62..ee802b294 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -39,4 +39,15 @@ extension Vector2i { public var tuple: (Int32, Int32) { (x, y) } } -internal var useSwiftCovers = true +#if TESTABLE_SWIFT_COVERS + +/// If true (the default), use Swift cover implementations where available instead of calling Godot engine functions. You should only use this for testing covers. It is not intended for production use. +@TaskLocal +internal var useSwiftCovers: Bool = true + +#else +@_spi(SwiftCovers) +public let useSwiftCovers: Bool = { + preconditionFailure("useSwiftCovers is only available if compilation condition TESTABLE_SWIFT_COVERS is set.") +}() +#endif From b71a743cae8ab446ccd4c3921228df36c793336c Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 14:55:01 -0600 Subject: [PATCH 30/99] use useSwiftCovers for testing --- .../SwiftGodotTestability/GodotTestCase.swift | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Sources/SwiftGodotTestability/GodotTestCase.swift b/Sources/SwiftGodotTestability/GodotTestCase.swift index 9dd0832ed..0a06742d9 100644 --- a/Sources/SwiftGodotTestability/GodotTestCase.swift +++ b/Sources/SwiftGodotTestability/GodotTestCase.swift @@ -178,3 +178,33 @@ public extension GodotTestCase { } } + +extension GodotTestCase { + + public func requireTestableSwiftCovers(filePath: StaticString = #filePath, line: UInt = #line) throws { +#if !TESTABLE_SWIFT_COVERS + throw XCTSkip("This test requires the compilation condition TESTABLE_SWIFT_COVERS.", file: filePath, line: line) +#endif + } + + /** + * Check that a value is computed the same by a Swift cover and the Godot engine method it replaces. + */ + public func checkCover( + filePath: StaticString = #filePath, + line: UInt = #line, + _ expression: () throws -> some Equatable + ) throws { + try requireTestableSwiftCovers(filePath: filePath, line: line) + + let coverValue = try $useSwiftCovers.withValue(true) { + try expression() + } + let engineValue = try $useSwiftCovers.withValue(false) { + try expression() + } + + XCTAssertEqual(coverValue, engineValue, file: #filePath, line: line) + } + +} From 9dab4de8ca68d1fe0685ba5dd2035196b6af8a2a Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 15:06:42 -0600 Subject: [PATCH 31/99] apply customBuiltinImplementationsSettings to SwiftGodotTestability It needs to see TESTABLE_SWIFT_COVERS. --- Package.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index d8f34b8aa..1c4f5f4f9 100644 --- a/Package.swift +++ b/Package.swift @@ -191,8 +191,10 @@ targets.append(contentsOf: [ "SwiftGodot", "libgodot_tests", "GDExtension" - ]), - + ], + swiftSettings: customBuiltinImplementationsSettings + ), + // General purpose runtime dependant tests .testTarget( name: "SwiftGodotTests", From 5d7e949852b0b5cbfae57996d30941dc73fb09df Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 15:17:17 -0600 Subject: [PATCH 32/99] fix indentation --- Sources/SwiftGodotTestability/GodotTestRunner.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftGodotTestability/GodotTestRunner.swift b/Sources/SwiftGodotTestability/GodotTestRunner.swift index b15be74c9..40a649806 100644 --- a/Sources/SwiftGodotTestability/GodotTestRunner.swift +++ b/Sources/SwiftGodotTestability/GodotTestRunner.swift @@ -52,7 +52,7 @@ class __GodotTestRunner: XCTestCase { .replacingOccurrences(of: "-[", with: "") .replacingOccurrences(of: "]", with: "") .replacingOccurrences(of: " ", with: "/") - return name.wholeMatch(of: testNamePatternFromEnvironment) != nil + return name.wholeMatch(of: testNamePatternFromEnvironment) != nil } /// Extract all the godot tests from a tree of XCTest objects. From 6875c3382fbe8f4df430445874746cb799bece6f Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 15:41:21 -0600 Subject: [PATCH 33/99] check for bytewise equality to handle nans --- .../SwiftGodotTestability/GodotTestCase.swift | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftGodotTestability/GodotTestCase.swift b/Sources/SwiftGodotTestability/GodotTestCase.swift index 0a06742d9..104ab7040 100644 --- a/Sources/SwiftGodotTestability/GodotTestCase.swift +++ b/Sources/SwiftGodotTestability/GodotTestCase.swift @@ -181,12 +181,6 @@ public extension GodotTestCase { extension GodotTestCase { - public func requireTestableSwiftCovers(filePath: StaticString = #filePath, line: UInt = #line) throws { -#if !TESTABLE_SWIFT_COVERS - throw XCTSkip("This test requires the compilation condition TESTABLE_SWIFT_COVERS.", file: filePath, line: line) -#endif - } - /** * Check that a value is computed the same by a Swift cover and the Godot engine method it replaces. */ @@ -195,8 +189,7 @@ extension GodotTestCase { line: UInt = #line, _ expression: () throws -> some Equatable ) throws { - try requireTestableSwiftCovers(filePath: filePath, line: line) - +#if TESTABLE_SWIFT_COVERS let coverValue = try $useSwiftCovers.withValue(true) { try expression() } @@ -204,7 +197,20 @@ extension GodotTestCase { try expression() } + // NaNs never compare equal, so first check for bytewise equality. + let bytewiseEqual = withUnsafeBytes(of: coverValue) { coverBytes in + withUnsafeBytes(of: engineValue) { engineBytes in + coverBytes.elementsEqual(engineBytes) + } + } + + guard !bytewiseEqual else { return } + + // Not bytewise-equal, but could still compare equal. XCTAssertEqual(coverValue, engineValue, file: #filePath, line: line) +#else + throw XCTSkip("This test requires the compilation condition TESTABLE_SWIFT_COVERS.", file: filePath, line: line) +#endif } } From c09587c4401c9e40b948d17742b85204beff595f Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 15:41:46 -0600 Subject: [PATCH 34/99] wip: write Vector2i tests using checkCover --- .../BuiltIn/Vector2iTests.swift | 116 +++++++++--------- 1 file changed, 60 insertions(+), 56 deletions(-) diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift index 8604fc1c1..184311eba 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift @@ -12,74 +12,78 @@ import RegexBuilder final class Vector2iTests: GodotTestCase { - static let rx_ws = ZeroOrMore { ChoiceOf { " "; "\t" } } - static let prefixCapture = Reference() - static let suffixCapture = Reference() - static let rx_check: Regex = Regex { - Anchor.startOfLine - Capture(as: prefixCapture) { - rx_ws - "check(" - OneOrMore { - CharacterClass.anyNonNewline - } - ", is:" - rx_ws + static let testInt32s: [Int32] = [ + .min, + -2, + -1, + 0, + 1, + 2, + .max, + ] + + static let testVectors: [Vector2i] = testInt32s.flatMap { y in + testInt32s.map { x in + Vector2i(x: x, y: y) + } + } + + func testInitFromVector2i() throws { + for y in Self.testInt32s { + try checkCover { Vector2i(from: Vector2i(x: 0, y: y)) } } - "\"" - ZeroOrMore { - CharacterClass.anyNonNewline + } + + func testInitFromVector2() throws { + for y in Self.testInt32s { + try checkCover { Vector2i(from: Vector2(x: 0, y: Float(y))) } } - "\"" - Capture(as: suffixCapture) { - ")" - rx_ws + + for y: Float in [-.infinity, -1e25, -0.0, 1e25, .infinity, .nan] { + try checkCover { Vector2i(from: Vector2(x: 0, y: y)) } + } + } + + func testAspect() throws { + for v in Self.testVectors { + try checkCover { v.aspect() } } - Anchor.endOfLine } -#if CUSTOM_BUILTIN_IMPLEMENTATIONS - // I only update the test cases if I'm calling the Godot engine, which is authoritative. In this case, I'm using Swift covers, and the point of the tests is to verify that the covers match the engine's behavior. So it would be incorrect to update the test cases in this case. - static let shouldUpdateTestCases: Bool = false -#else - static let shouldUpdateTestCases: Bool = ProcessInfo.processInfo.environment["UpdateSwiftCoverTestCases", default: ""] != "" -#endif + func testMaxAxisIndex() throws { + for v in Self.testVectors { + try checkCover { v.maxAxisIndex() } + } + } - func check( - _ actual: V, - is expected: String, - file: StaticString = #filePath, - lineNumber: UInt = #line - ) { - guard Self.shouldUpdateTestCases else { - XCTAssertEqual(String(describing: actual), expected, file: file, line: lineNumber) - return + func testMinAxisIndex() throws { + for v in Self.testVectors { + try checkCover { v.minAxisIndex() } } + } - // I'm using Godot engine functions, not Swift cover implementations. - // I treat the Godot engine as a test oracle: it produces the correct answers. + func testDistanceTo() throws { + for v in Self.testVectors { + for u in Self.testVectors { + try checkCover { v.distanceTo(u) } + } + } + } - let originalFile = try! String(contentsOfFile: file.description) - var lines = originalFile.split(separator: "\n", omittingEmptySubsequences: false) - let line = String(lines[Int(lineNumber) - 1]) - guard let match = line.wholeMatch(of: Self.rx_check) else { - XCTFail("couldn't match check call", file: file, line: lineNumber) - return + func testDistanceSquaredTo() throws { + for v in Self.testVectors { + for u in Self.testVectors { + try checkCover { v.distanceSquaredTo(u) } + } } - // String(reflecting:) quotes and escapes the description of `actual`. - let newExpected = String(reflecting: String(describing: actual)) - let newLine = "\(match[Self.prefixCapture])\(newExpected)\(match[Self.suffixCapture])" - lines[Int(lineNumber) - 1] = newLine[...] - let newFile = lines.joined(separator: "\n") - try! newFile.write(toFile: file.description, atomically: true, encoding: .utf8) } - func testInitFrom() { - check(Vector2i(from: Vector2i(x: 0, y: 0)), is: "Vector2i(x: 0, y: 0)") - check(Vector2i(from: Vector2i(x: 1, y: 1)), is: "Vector2i(x: 1, y: 1)") - check(Vector2i(from: Vector2i(x: 2, y: 2147483647)), is: "Vector2i(x: 2, y: 2147483647)") - check(Vector2i(from: Vector2i(x: 3, y: -1)), is: "Vector2i(x: 3, y: -1)") - check(Vector2i(from: Vector2i(x: 4, y: -2147483648)), is: "Vector2i(x: 4, y: -2147483648)") + func testPlus() throws { + for v in Self.testVectors { + for u in Self.testVectors { + try checkCover { v + u } + } + } } func testOperatorUnaryMinus () { From ead7276b7adba4ac4b8da100258d988e76537b01 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 15:47:35 -0600 Subject: [PATCH 35/99] add int32_for_double --- Sources/CWrappers/include/CWrappers.h | 3 +++ Sources/SwiftGodot/SwiftCoverSupport.swift | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/Sources/CWrappers/include/CWrappers.h b/Sources/CWrappers/include/CWrappers.h index 0d1a67cc7..b7ec9517f 100644 --- a/Sources/CWrappers/include/CWrappers.h +++ b/Sources/CWrappers/include/CWrappers.h @@ -6,6 +6,9 @@ /// - returns: `f`, cast to `int32_t`. static inline int32_t int32_for_float(float f) { return f; } +/// - returns: `d`, cast to `int32_t`. +static inline int32_t int32_for_double(double d) { return d; } + /// - returns: `n / d`. static inline int32_t int32_divide(int32_t n, int32_t d) { return n / d; } diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index ee802b294..3c9ee7b79 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -9,6 +9,13 @@ public func cCastToInt32(_ float: Float) -> Int32 { return int32_for_float(float) } +/// The Swift standard library offers no efficient way to cast a `Double` to an `Int32` with the same semantics as C and C++. This method calls an imported inlinable C function. +@_spi(SwiftCovers) +@inline(__always) +public func cCastToInt32(_ double: Double) -> Int32 { + return int32_for_double(double) +} + /// The Swift standard library offers no efficient way to divide an `Int32` by an `Int32` with the same semantics as C and C++. This method calls an imported inlinable C function. @_spi(SwiftCovers) @inline(__always) From f1a5f10bf8e52a7a63dd087f5e676a642409999d Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 18:08:30 -0600 Subject: [PATCH 36/99] fix Vector2i.clampi, Vector2i.snappedi --- Sources/SwiftCovers/Vector2i.covers.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftCovers/Vector2i.covers.swift b/Sources/SwiftCovers/Vector2i.covers.swift index e69587f25..ba0cff64c 100644 --- a/Sources/SwiftCovers/Vector2i.covers.swift +++ b/Sources/SwiftCovers/Vector2i.covers.swift @@ -69,19 +69,21 @@ extension Vector2i { } public func clampi(min: Int64, max: Int64) -> Vector2i { + let min = Int32(truncatingIfNeeded: min) + let max = Int32(truncatingIfNeeded: max) return Vector2i( - x: Int32(truncatingIfNeeded: Int64(x).clamped(min: min, max: max)), - y: Int32(truncatingIfNeeded: Int64(y).clamped(min: min, max: max)) + x: x.clamped(min: min, max: max), + y: y.clamped(min: min, max: max) ) } // snapped is special-cased. public func snappedi(step: Int64) -> Vector2i { - let step = Int32(truncatingIfNeeded: step) + let step = Double(Int32(truncatingIfNeeded: step)) return Vector2i( - x: x.snapped(step: step), - y: y.snapped(step: step) + x: cCastToInt32(Double(x).snapped(step: Double(step))), + y: cCastToInt32(Double(y).snapped(step: Double(step))) ) } From e60b96d553d230c5b1013f79ff3440e03031cbab Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 18:08:58 -0600 Subject: [PATCH 37/99] wip: more Vector2i cover tests --- .../BuiltIn/Vector2iTests.swift | 147 ++++++++++++++++-- 1 file changed, 130 insertions(+), 17 deletions(-) diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift index 184311eba..8d15a287d 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift @@ -22,6 +22,20 @@ final class Vector2iTests: GodotTestCase { .max, ] + static let testInt64s: [Int64] = [ + .min, + Int64(Int32.min) - 1, + Int64(Int32.min), + -2, + -1, + 0, + 1, + 2, + Int64(Int32.max), + Int64(Int32.max) + 1, + .max + ] + static let testVectors: [Vector2i] = testInt32s.flatMap { y in testInt32s.map { x in Vector2i(x: x, y: y) @@ -29,8 +43,8 @@ final class Vector2iTests: GodotTestCase { } func testInitFromVector2i() throws { - for y in Self.testInt32s { - try checkCover { Vector2i(from: Vector2i(x: 0, y: y)) } + for v in Self.testVectors { + try checkCover { Vector2i(from: v) } } } @@ -44,46 +58,145 @@ final class Vector2iTests: GodotTestCase { } } - func testAspect() throws { + func testNullaryCovers() throws { + // Methods of the form v.method(). + + func checkMethod( + _ method: (Vector2i) -> () -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + try checkCover(filePath: filePath, line: line) { method(v)() } + } + } + + try checkMethod(Vector2i.aspect) + try checkMethod(Vector2i.maxAxisIndex) + try checkMethod(Vector2i.minAxisIndex) + try checkMethod(Vector2i.length) + try checkMethod(Vector2i.lengthSquared) + try checkMethod(Vector2i.sign) + try checkMethod(Vector2i.abs) + } + + func testUnaryCovers_Vector2i() throws { + // Methods of the form v.method(u) where u is also a Vector2i. + + func checkMethod( + _ method: (Vector2i) -> (Vector2i) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for u in Self.testVectors { + try checkCover(filePath: filePath, line: line) { method(v)(u) } + } + } + } + + try checkMethod(Vector2i.distanceTo) + try checkMethod(Vector2i.distanceSquaredTo) + try checkMethod(Vector2i.min(with:)) + try checkMethod(Vector2i.max(with:)) + } + + func testClamp() throws { for v in Self.testVectors { - try checkCover { v.aspect() } + for u in Self.testVectors { + for w in Self.testVectors { + try checkCover { v.clamp(min: u, max: w) } + } + } } } - func testMaxAxisIndex() throws { + func testClampi() throws { for v in Self.testVectors { - try checkCover { v.maxAxisIndex() } + for i in Self.testInt64s { + for j in Self.testInt64s { + try checkCover { v.clampi(min: i, max: j) } + } + } } } - func testMinAxisIndex() throws { + func testSnappedi() throws { for v in Self.testVectors { - try checkCover { v.minAxisIndex() } + for i in Self.testInt64s { + try checkCover { v.snappedi(step: i) } + } } } - func testDistanceTo() throws { + func testMini() throws { for v in Self.testVectors { - for u in Self.testVectors { - try checkCover { v.distanceTo(u) } + for i in Self.testInt64s { + try checkCover { v.mini(with: i) } } } } - func testDistanceSquaredTo() throws { + func testMaxi() throws { for v in Self.testVectors { - for u in Self.testVectors { - try checkCover { v.distanceSquaredTo(u) } + for i in Self.testInt64s { + try checkCover { v.maxi(with: i) } } } } - func testPlus() throws { + func testSubscriptGet() throws { for v in Self.testVectors { - for u in Self.testVectors { - try checkCover { v + u } + for i in Vector2i.Axis.allCases { + try checkCover { + var v = v + return v[i.rawValue] + } + } + } + } + + func testSubscriptSet() throws { + for v in Self.testVectors { + for i in Vector2i.Axis.allCases { + for j in Self.testInt64s { + try checkCover { + var v = v + v[i.rawValue] = j + return v + } + } + } + } + } + + func testBinaryOperators_Vector2i_Vector2i() throws { + // Operators of the form v * u for two Vector2i. + + func checkOperator( + _ op: (Vector2i, Vector2i) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for u in Self.testVectors { + try checkCover(filePath: filePath, line: line) { op(v, u) } + } } } + + try checkOperator(==) + try checkOperator(!=) + try checkOperator(<) + try checkOperator(<=) + try checkOperator(>) + try checkOperator(>=) + try checkOperator(+) + try checkOperator(-) + try checkOperator(*) + try checkOperator(/) + + // The `Vector2i % Vector2i` operator is implemented incorrectly by Godot, for any gdextension that uses the ptrcall API. It performs `Vector2i / Vector2i` instead of what it's supposed to do. + // See https://github.com/godotengine/godot/issues/99518 for details. + // + // try checkOperator(%) } func testOperatorUnaryMinus () { From cf9b93aa8d8e151987325b4da85d01889022865b Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 18:13:41 -0600 Subject: [PATCH 38/99] fix operator mapping for "or", "<", "<=" --- Generator/Generator/TypeHelpers.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Generator/Generator/TypeHelpers.swift b/Generator/Generator/TypeHelpers.swift index aedcf3e63..66b3ce73a 100644 --- a/Generator/Generator/TypeHelpers.swift +++ b/Generator/Generator/TypeHelpers.swift @@ -377,9 +377,9 @@ func infixOperatorMap (_ name: String) -> (String, String)? { case "==": return ("GDEXTENSION_VARIANT_OP_EQUAL", "==") case "!=": return ("GDEXTENSION_VARIANT_OP_NOT_EQUAL", "!=") case "and": return ("GDEXTENSION_VARIANT_OP_AND", "&&") - case "or": return ("GDEXTENSION_VARIANT_OP_AND", "||") - case "<": return ("GDEXTENSION_VARIANT_OP_LESS_EQUAL", "<") - case "<=": return ("GDEXTENSION_VARIANT_OP_LESS", "<=") + case "or": return ("GDEXTENSION_VARIANT_OP_OR", "||") + case "<": return ("GDEXTENSION_VARIANT_OP_LESS", "<") + case "<=": return ("GDEXTENSION_VARIANT_OP_LESS_EQUAL", "<=") case ">": return ("GDEXTENSION_VARIANT_OP_GREATER", ">") case ">=": return ("GDEXTENSION_VARIANT_OP_GREATER_EQUAL", ">=") case "+": return ("GDEXTENSION_VARIANT_OP_ADD", "+") From 9307f199df2454f8c4280a05e82264537a70cdae Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 19:36:17 -0600 Subject: [PATCH 39/99] finish Vector2iTests --- .../BuiltIn/Vector2iTests.swift | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift index 8d15a287d..2bc34f31b 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift @@ -36,6 +36,15 @@ final class Vector2iTests: GodotTestCase { .max ] + static let testDoubles: [Double] = testInt64s.map { Double($0) } + [ + -.infinity, + -1e100, + -0.0, + 1e100, + .infinity, + .nan + ] + static let testVectors: [Vector2i] = testInt32s.flatMap { y in testInt32s.map { x in Vector2i(x: x, y: y) @@ -59,7 +68,7 @@ final class Vector2iTests: GodotTestCase { } func testNullaryCovers() throws { - // Methods of the form v.method(). + // Methods of the form Vector2i.method(). func checkMethod( _ method: (Vector2i) -> () -> some Equatable, @@ -80,7 +89,7 @@ final class Vector2iTests: GodotTestCase { } func testUnaryCovers_Vector2i() throws { - // Methods of the form v.method(u) where u is also a Vector2i. + // Methods of the form Vector2i.method(Vector2i). func checkMethod( _ method: (Vector2i) -> (Vector2i) -> some Equatable, @@ -169,7 +178,7 @@ final class Vector2iTests: GodotTestCase { } func testBinaryOperators_Vector2i_Vector2i() throws { - // Operators of the form v * u for two Vector2i. + // Operators of the form Vector2i * Vector2i. func checkOperator( _ op: (Vector2i, Vector2i) -> some Equatable, @@ -199,6 +208,41 @@ final class Vector2iTests: GodotTestCase { // try checkOperator(%) } + func testBinaryOperators_Vector2i_Int64() throws { + // Operators of the form Vector2i * Int64. + + func checkOperator( + _ op: (Vector2i, Int64) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for i in Self.testInt64s { + try checkCover(filePath: filePath, line: line) { op(v, i) } + } + } + } + + try checkOperator(*) + try checkOperator(/) + try checkOperator(%) + } + + func testTimesInt64() throws { + for v in Self.testVectors { + for d in Self.testDoubles { + try checkCover { v * d } + } + } + } + + func testDividedByInt64() throws { + for v in Self.testVectors { + for d in Self.testDoubles { + try checkCover { v / d } + } + } + } + func testOperatorUnaryMinus () { var value: Vector2i From fb30a808ab76611000b31f9042bb74c788a24499 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Fri, 22 Nov 2024 17:36:56 -0600 Subject: [PATCH 40/99] Add Vector2 Method Covers --- Sources/SwiftCovers/Vector2.covers.swift | 159 ++++++++++++++++++++- Sources/SwiftGodot/SwiftCoverSupport.swift | 6 + 2 files changed, 164 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftCovers/Vector2.covers.swift b/Sources/SwiftCovers/Vector2.covers.swift index a92fe9a67..b00234797 100644 --- a/Sources/SwiftCovers/Vector2.covers.swift +++ b/Sources/SwiftCovers/Vector2.covers.swift @@ -1,7 +1,164 @@ // -// File.swift +// Vector2.covers.swift // SwiftGodot // // Created by Danny Youstra on 11/19/24. // +@_spi(SwiftCovers) import SwiftGodot +#if canImport(Darwin) +import Darwin +#elseif os(Windows) +import ucrt +import WinSDK +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#else +#error("Unable to identify your C library.") +#endif + + +extension Vector2 { + + public func angle() -> Double { + return Double(atan2(x, y)) + } + + public static func fromAngle(_ angle: Double) -> Vector2 { + let fAngle = Float(angle) + return Vector2(x: cos(fAngle), y: sin(fAngle)) + } + + public func length() -> Double { + return Double(sqrt(x * x + y * y)) + } + + public func lengthSquared() -> Double { + return Double(x * x + y * y) + } + + public func normalized() -> Vector2 { + var len = x * x + y * y + var result = self + if len != 0 { + len = sqrt(len) + result = Vector2(x: x / len, y: y / len) + } + return result + } + + public func is_normalized() -> Bool { +// isEqualApprox(to: <#T##Vector2#>) + return false + } + + public func distanceTo(_ to: Vector2) -> Double { + return Double(sqrt((x - to.x) * (x - to.x) + (y - to.y) * (y - to.y))) + } + + public func distanceSquaredTo(_ to: Vector2) -> Double { + return Double((x - to.x) * (x - to.x) + (y - to.y) * (y - to.y)) + } + + public func angleTo(_ to: Vector2) -> Double { + return atan2(cross(with: to), dot(with: to)) + } + + public func angleToPoint(to: Vector2) -> Double { + return (to - self).angle() + } + + public func dot(with: Vector2) -> Double { + return Double(x * with.x + y * with.y) + } + + public func cross(with: Vector2) -> Double { + return Double(x * with.y - y * with.x) + } + + public func sign() -> Vector2 { + return Vector2(x: SwiftGodot.sign(x), y: SwiftGodot.sign(y)) + } + + public func floor() -> Vector2 { + return Vector2(x: _math.floor(x), y: _math.floor(y)) + } + + public func ceil() -> Vector2 { + return Vector2(x: _math.ceil(x), y: _math.ceil(y)) + } + + public func round() -> Vector2 { + return Vector2(x: _math.round(x), y: _math.round(y)) + } + + public func rotated(angle: Double) -> Vector2 { + let sin = Float(sin(angle)) + let cos = Float(cos(angle)) + return Vector2( + x: x * cos - y * sin, + y: x * sin + y * cos + ) + } + + public func project(b: Vector2) -> Vector2 { + return b * (dot(with: b) / b.lengthSquared()) + } + + public func clamp(min: Vector2, max: Vector2) -> Vector2 { + return Vector2( + x: x.clamped(min: min.x, max: max.x), + y: y.clamped(min: min.y, max: max.y) + ) + } + + public func clampf(min: Double, max: Double) -> Vector2 { + return Vector2( + x: x.clamped(min: Float(min), max: Float(max)), + y: y.clamped(min: Float(min), max: Float(max)) + ) + } + + public func snappedf(step: Double) -> Vector2 { + return Vector2( + x: x.snapped(step: Float(step)), + y: y.snapped(step: Float(step)) + ) + } + + public func limitLength(_ length: Double = 1.0) -> Vector2 { + let beforeLen = self.length() + var result = self + if (beforeLen > 0 && length < beforeLen) { + result = result / beforeLen + result = result * length + } + return result + } + + public func moveToward(to: Vector2, delta: Double) -> Vector2 { + /// This epsilon should match the one used by Godot for consistency. + let CMP_EPSILON = Double(0.00001) + + var result = to - self + let newLen = result.length() + return newLen <= delta || newLen < CMP_EPSILON ? to : self + result / newLen * delta + } + + public func slide(n: Vector2) -> Vector2 { + return self - n * self.dot(with: n) + } + + public func bounce(n: Vector2) -> Vector2 { + return -reflect(line: n) + } + + public func reflect(line: Vector2) -> Vector2 { + /// Reflection requires a scale by 2, but float * Vector2 is not overloaded + return Vector2(x: 2, y: 2) * line * self.dot(with: line) - self + } + + +} diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index 3c9ee7b79..fff54c3bd 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -46,6 +46,12 @@ extension Vector2i { public var tuple: (Int32, Int32) { (x, y) } } +@_spi(SwiftCovers) +@inline(__always) +public func sign(_ x: Float) -> Float { + return x == 0 ? 0 : (x > 0 ? 1.0 : -1.0) +} + #if TESTABLE_SWIFT_COVERS /// If true (the default), use Swift cover implementations where available instead of calling Godot engine functions. You should only use this for testing covers. It is not intended for production use. From c70f4a9230bc2deb449401a454342e0d476a7b6d Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Fri, 22 Nov 2024 17:38:16 -0600 Subject: [PATCH 41/99] fix var constant --- Sources/SwiftCovers/Vector2.covers.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftCovers/Vector2.covers.swift b/Sources/SwiftCovers/Vector2.covers.swift index b00234797..7a861f820 100644 --- a/Sources/SwiftCovers/Vector2.covers.swift +++ b/Sources/SwiftCovers/Vector2.covers.swift @@ -142,7 +142,7 @@ extension Vector2 { /// This epsilon should match the one used by Godot for consistency. let CMP_EPSILON = Double(0.00001) - var result = to - self + let result = to - self let newLen = result.length() return newLen <= delta || newLen < CMP_EPSILON ? to : self + result / newLen * delta } From 2ff9a6f3edab4790ecc4bfab2c3b66873a11d8de Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Fri, 22 Nov 2024 17:51:13 -0600 Subject: [PATCH 42/99] Omit is_normalized --- Sources/SwiftCovers/Vector2.covers.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Sources/SwiftCovers/Vector2.covers.swift b/Sources/SwiftCovers/Vector2.covers.swift index 7a861f820..e15689df6 100644 --- a/Sources/SwiftCovers/Vector2.covers.swift +++ b/Sources/SwiftCovers/Vector2.covers.swift @@ -49,11 +49,6 @@ extension Vector2 { return result } - public func is_normalized() -> Bool { -// isEqualApprox(to: <#T##Vector2#>) - return false - } - public func distanceTo(_ to: Vector2) -> Double { return Double(sqrt((x - to.x) * (x - to.x) + (y - to.y) * (y - to.y))) } From 30c6a5a8da23c1457269ccf482ac9b0706932c32 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Fri, 22 Nov 2024 13:42:36 -0600 Subject: [PATCH 43/99] exclude Sources/SwiftCovers/README.md from build --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 1c4f5f4f9..7686d02c3 100644 --- a/Package.swift +++ b/Package.swift @@ -242,7 +242,8 @@ targets.append(contentsOf: [ targets += [ .target( name: "SwiftCovers", - dependencies: ["SwiftGodot"] + dependencies: ["SwiftGodot"], + exclude: ["README.md"] ), .target(name: "CWrappers"), From c7d162976468ceb847c1d4221cd8f255980dc99a Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Fri, 22 Nov 2024 13:42:36 -0600 Subject: [PATCH 44/99] ignore *.d and *.swiftdeps Sometimes when I build in vscode, it drops the compiler output in the repo root. I haven't figured out why yet. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 43c787b47..6ca2e714c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ Package.resolved /.vscode .swiftpm/xcode/xcshareddata/xcschemes GeneratedForDebug/ +*.d +*.swiftdeps From 87ff490f79b9c55038b469f4e517a3c51cfde12f Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 21 Nov 2024 18:09:34 -0600 Subject: [PATCH 45/99] more comments about broken Vector2i % Vector2i --- Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift index 2bc34f31b..4aa143559 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift @@ -202,10 +202,14 @@ final class Vector2iTests: GodotTestCase { try checkOperator(*) try checkOperator(/) + // try checkOperator(%) + // // The `Vector2i % Vector2i` operator is implemented incorrectly by Godot, for any gdextension that uses the ptrcall API. It performs `Vector2i / Vector2i` instead of what it's supposed to do. + // // See https://github.com/godotengine/godot/issues/99518 for details. // - // try checkOperator(%) + // Note that it isn't enough for the bug to be fixed in the Godot project. The libgodot project also needs to be fixed, because that's what SwiftGodot actually uses. + // https://github.com/migueldeicaza/libgodot } func testBinaryOperators_Vector2i_Int64() throws { From 52e47c9c4a92b0456c11ed9d37f342788e6c2cfa Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Fri, 22 Nov 2024 13:42:36 -0600 Subject: [PATCH 46/99] remove unneeded casts --- Sources/SwiftCovers/Vector2i.covers.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftCovers/Vector2i.covers.swift b/Sources/SwiftCovers/Vector2i.covers.swift index ba0cff64c..980d068e0 100644 --- a/Sources/SwiftCovers/Vector2i.covers.swift +++ b/Sources/SwiftCovers/Vector2i.covers.swift @@ -82,8 +82,8 @@ extension Vector2i { public func snappedi(step: Int64) -> Vector2i { let step = Double(Int32(truncatingIfNeeded: step)) return Vector2i( - x: cCastToInt32(Double(x).snapped(step: Double(step))), - y: cCastToInt32(Double(y).snapped(step: Double(step))) + x: cCastToInt32(Double(x).snapped(step: step)), + y: cCastToInt32(Double(y).snapped(step: step)) ) } From c7044884a20ff9f846c7ce92ebfcf2b8fb37d322 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Fri, 22 Nov 2024 13:42:36 -0600 Subject: [PATCH 47/99] enable TESTABLE_SWIFT_COVERS --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 7686d02c3..72eb4e64c 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,7 @@ let customBuiltinImplementationsSettings: [SwiftSetting] = [ .define("CUSTOM_BUILTIN_IMPLEMENTATIONS"), // Define this to generate code that can choose between a Swift cover and a Godot engine call at runtime. This is slower but allows easy testing that each Swift cover behaves exactly like the Godot engine function it replaces. This controls the behavior of the Generator build tool; there is no other good way to configure how Generator operates. - // .define("TESTABLE_SWIFT_COVERS"), + .define("TESTABLE_SWIFT_COVERS"), ] // Products define the executables and libraries a package produces, and make them visible to other packages. From 8dfc42fa400b7f485efb8431e8928e442f0658ae Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Fri, 22 Nov 2024 13:42:36 -0600 Subject: [PATCH 48/99] ignore /libgodot.xcframework even if it's not a folder because it might be a symlink --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6ca2e714c..9122609dc 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ build-docs.log *rej *orig \#* -/libgodot.xcframework/ +/libgodot.xcframework Package.resolved /.vscode .swiftpm/xcode/xcshareddata/xcschemes From 988f165579b16cc57458775624247887b29617bc Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Fri, 22 Nov 2024 13:42:36 -0600 Subject: [PATCH 49/99] write Vector3i.covers.swift --- Sources/SwiftCovers/Vector3i.covers.swift | 237 ++++++++++++ Sources/SwiftGodot/SwiftCoverSupport.swift | 6 + .../BuiltIn/Vector3iTests.swift | 336 +++++++++++++++++- 3 files changed, 578 insertions(+), 1 deletion(-) create mode 100644 Sources/SwiftCovers/Vector3i.covers.swift diff --git a/Sources/SwiftCovers/Vector3i.covers.swift b/Sources/SwiftCovers/Vector3i.covers.swift new file mode 100644 index 000000000..3fa827e2d --- /dev/null +++ b/Sources/SwiftCovers/Vector3i.covers.swift @@ -0,0 +1,237 @@ +@_spi(SwiftCovers) import SwiftGodot + +extension Vector3i { + + public init(from: Vector3i) { + self = from + } + + public init(from: Vector3) { + self.init( + x: cCastToInt32(from.x), + y: cCastToInt32(from.y), + z: cCastToInt32(from.z) + ) + } + + public func minAxisIndex() -> Int64 { + return (x < y ? (x < z ? Axis.x : Axis.z) : (y < z ? Axis.y : Axis.z)).rawValue + } + + public func maxAxisIndex() -> Int64 { + return (x < y ? (y < z ? Axis.z : Axis.y) : (x < z ? Axis.z : Axis.x)).rawValue + } + + public func distanceTo(_ to: Vector3i) -> Double { + return (to - self).length() + } + + public func distanceSquaredTo(_ to: Vector3i) -> Int64 { + return (to - self).lengthSquared() + } + + public func length() -> Double { + return Double(lengthSquared()).squareRoot() + } + + public func lengthSquared() -> Int64 { + let x = Int64(x) + let y = Int64(y) + let z = Int64(z) + return x &* x &+ y &* y &+ z &* z + } + + public func sign() -> Vector3i { + return Vector3i(x: x.signum(), y: y.signum(), z: z.signum()) + } + + public func abs() -> Vector3i { + return Vector3i( + x: Int32(truncatingIfNeeded: x.magnitude), + y: Int32(truncatingIfNeeded: y.magnitude), + z: Int32(truncatingIfNeeded: z.magnitude) + ) + } + + public func clamp(min: Vector3i, max: Vector3i) -> Vector3i { + return Vector3i( + x: x.clamped(min: min.x, max: max.x), + y: y.clamped(min: min.y, max: max.y), + z: z.clamped(min: min.z, max: max.z) + ) + } + + public func clampi(min: Int64, max: Int64) -> Vector3i { + let min = Int32(truncatingIfNeeded: min) + let max = Int32(truncatingIfNeeded: max) + return Vector3i( + x: x.clamped(min: min, max: max), + y: y.clamped(min: min, max: max), + z: z.clamped(min: min, max: max) + ) + } + + public func snappedi(step: Int64) -> Vector3i { + let step = Double(Int32(truncatingIfNeeded: step)) + return Vector3i( + x: cCastToInt32(Double(x).snapped(step: step)), + y: cCastToInt32(Double(y).snapped(step: step)), + z: cCastToInt32(Double(z).snapped(step: step)) + ) + } + + public func min(with: Vector3i) -> Vector3i { + return Vector3i( + x: Swift.min(x, with.x), + y: Swift.min(y, with.y), + z: Swift.min(z, with.z) + ) + } + + public func mini(with: Int64) -> Vector3i { + let i = Int32(truncatingIfNeeded: with) + return Vector3i( + x: Swift.min(x, i), + y: Swift.min(y, i), + z: Swift.min(z, i) + ) + } + + public func max(with: Vector3i) -> Vector3i { + return Vector3i( + x: Swift.max(x, with.x), + y: Swift.max(y, with.y), + z: Swift.max(z, with.z) + ) + } + + public func maxi(with: Int64) -> Vector3i { + let i = Int32(truncatingIfNeeded: with) + return Vector3i( + x: Swift.max(x, i), + y: Swift.max(y, i), + z: Swift.max(z, i) + ) + } + + public subscript(index: Int64) -> Int64 { + get { + return Int64(SIMD3(x, y, z)[Int(index)]) + } + set { + var simd = SIMD3(x, y, z) + simd[Int(index)] = Int32(truncatingIfNeeded: newValue) + (x, y, z) = (simd.x, simd.y, simd.z) + } + } + + public static func * (lhs: Vector3i, rhs: Int64) -> Vector3i { + let f = Int32(truncatingIfNeeded: rhs) + return Vector3i( + x: lhs.x &* f, + y: lhs.y &* f, + z: lhs.z &* f + ) + } + + public static func / (lhs: Vector3i, rhs: Int64) -> Vector3i { + let rhs = Int32(truncatingIfNeeded: rhs) + return Self( + x: cDivide(numerator: lhs.x, denominator: rhs), + y: cDivide(numerator: lhs.y, denominator: rhs), + z: cDivide(numerator: lhs.z, denominator: rhs) + ) + } + + public static func % (lhs: Vector3i, rhs: Int64) -> Vector3i { + let rhs = Int32(truncatingIfNeeded: rhs) + return Self( + x: cRemainder(numerator: lhs.x, denominator: rhs), + y: cRemainder(numerator: lhs.y, denominator: rhs), + z: cRemainder(numerator: lhs.z, denominator: rhs) + ) + } + + public static func * (lhs: Vector3i, rhs: Double) -> Vector3 { + let rhs = Float(rhs) + return Vector3( + x: Float(lhs.x) * rhs, + y: Float(lhs.y) * rhs, + z: Float(lhs.z) * rhs + ) + } + + public static func / (lhs: Vector3i, rhs: Double) -> Vector3 { + let rhs = Float(rhs) + return Vector3( + x: Float(lhs.x) / rhs, + y: Float(lhs.y) / rhs, + z: Float(lhs.z) / rhs + ) + } + + public static func == (lhs: Vector3i, rhs: Vector3i) -> Bool { + return lhs.tuple == rhs.tuple + } + + public static func != (lhs: Vector3i, rhs: Vector3i) -> Bool { + return !(lhs == rhs) + } + + public static func < (lhs: Vector3i, rhs: Vector3i) -> Bool { + return lhs.tuple < rhs.tuple + } + + public static func <= (lhs: Vector3i, rhs: Vector3i) -> Bool { + return lhs.tuple <= rhs.tuple + } + + public static func > (lhs: Vector3i, rhs: Vector3i) -> Bool { + return lhs.tuple > rhs.tuple + } + + public static func >= (lhs: Vector3i, rhs: Vector3i) -> Bool { + return lhs.tuple >= rhs.tuple + } + + public static func + (lhs: Vector3i, rhs: Vector3i) -> Vector3i { + return Vector3i( + x: lhs.x &+ rhs.x, + y: lhs.y &+ rhs.y, + z: lhs.z &+ rhs.z + ) + } + + public static func - (lhs: Vector3i, rhs: Vector3i) -> Vector3i { + return Vector3i( + x: lhs.x &- rhs.x, + y: lhs.y &- rhs.y, + z: lhs.z &- rhs.z + ) + } + + public static func * (lhs: Vector3i, rhs: Vector3i) -> Vector3i { + return Vector3i( + x: lhs.x &* rhs.x, + y: lhs.y &* rhs.y, + z: lhs.z &* rhs.z + ) + } + + public static func / (lhs: Vector3i, rhs: Vector3i) -> Vector3i { + return Vector3i( + x: cDivide(numerator: lhs.x, denominator: rhs.x), + y: cDivide(numerator: lhs.y, denominator: rhs.y), + z: cDivide(numerator: lhs.z, denominator: rhs.z) + ) + } + + public static func % (lhs: Vector3i, rhs: Vector3i) -> Vector3i { + return Vector3i( + x: cRemainder(numerator: lhs.x, denominator: rhs.x), + y: cRemainder(numerator: lhs.y, denominator: rhs.y), + z: cRemainder(numerator: lhs.z, denominator: rhs.z) + ) + } + +} diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index fff54c3bd..56f8bab1a 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -46,6 +46,12 @@ extension Vector2i { public var tuple: (Int32, Int32) { (x, y) } } +extension Vector3i { + @_spi(SwiftCovers) + @inline(__always) + public var tuple: (Int32, Int32, Int32) { (x, y, z) } +} + @_spi(SwiftCovers) @inline(__always) public func sign(_ x: Float) -> Float { diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector3iTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector3iTests.swift index 4ee4eb4b0..431395a1d 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector3iTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector3iTests.swift @@ -10,7 +10,341 @@ import SwiftGodotTestability @testable import SwiftGodot final class Vector3iTests: GodotTestCase { - + + static let testInt32s: [Int32] = [ + .min, + -2, + -1, + 0, + 1, + 2, + .max, + ] + + /// Fewer values to reduce combinatorial explosion. + static let testFewerInt32s: [Int32] = [ + -2, + 0, + 2, + ] + + /// Adding or subtracting any two of these won't overflow. + static let testSmallerInt32s: [Int32] = [ + -(Int32.max / 2), + -2, + -1, + 0, + 1, + 2, + Int32.max / 2, + ] + + static let testInt64s: [Int64] = [ + .min, + Int64(Int32.min) - 1, + Int64(Int32.min), + -2, + -1, + 0, + 1, + 2, + Int64(Int32.max), + Int64(Int32.max) + 1, + .max + ] + + static let testDoubles: [Double] = testInt64s.map { Double($0) } + [ + -.infinity, + -1e100, + -0.0, + 1e100, + .infinity, + .nan + ] + + static let testVectors: [Vector3i] = testInt32s.flatMap { z in + testInt32s.flatMap { y in + testInt32s.map { x in + Vector3i(x: x, y: y, z: z) + } + } + } + + /// Fewer vectors than `testVectors` for tests where the combinatorial explosion from `testVectors` would be too slow. + static let testFewerVectors: [Vector3i] = testFewerInt32s.flatMap { z in + testInt32s.flatMap { y in + testFewerInt32s.map { x in + Vector3i(x: x, y: y, z: z) + } + } + } + + /// Vectors where adding or subtracting any two of them won't overflow. + static let testSmallerVectors: [Vector3i] = testSmallerInt32s.flatMap { z in + testSmallerInt32s.flatMap { y in + testSmallerInt32s.map { x in + Vector3i(x: x, y: y, z: z) + } + } + } + + func testInitFromVector3i() throws { + for v in Self.testVectors { + try checkCover { Vector3i(from: v) } + } + } + + func testInitFromVector3() throws { + for y in Self.testInt32s { + try checkCover { Vector3i(from: Vector3(x: 0, y: Float(y), z: 1)) } + } + + for y: Float in [-.infinity, -1e25, -0.0, 1e25, .infinity, .nan] { + try checkCover { Vector3i(from: Vector3(x: 0, y: y, z: 1)) } + } + } + + func testNullaryCovers() throws { + // Methods of the form Vector3i.method(). + + func checkMethod( + _ method: (Vector3i) -> () -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + try checkCover(filePath: filePath, line: line) { method(v)() } + } + } + + try checkMethod(Vector3i.maxAxisIndex) + try checkMethod(Vector3i.minAxisIndex) + try checkMethod(Vector3i.length) + try checkMethod(Vector3i.lengthSquared) + try checkMethod(Vector3i.sign) + try checkMethod(Vector3i.abs) + } + + func testUnaryCovers_Vector3i() throws { + // Methods of the form Vector3i.method(Vector3i). + + func checkMethod( + _ method: (Vector3i) -> (Vector3i) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for u in Self.testVectors { + try checkCover(filePath: filePath, line: line) { method(v)(u) } + } + } + } + + func checkMethodAvoidingOverflow( + _ method: (Vector3i) -> (Vector3i) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testSmallerVectors { + for u in Self.testSmallerVectors { + try checkCover(filePath: filePath, line: line) { method(v)(u) } + } + } + } + + /// ## Why I restrict the test inputs for `distanceTo` and `distanceSquaredTo` + /// + /// Consider this program: + /// + /// ```swift + /// let a = Vector3i(x: .min, y: .min, z: .min) + /// let b = Vector3i(x: 1, y: .min, z: .min) + /// let answer = a.distanceTo(b) + /// ``` + /// + /// Remarkably, this produces different output depending on whether libgodot was compiled with optimization or not. + /// + /// The Godot implementation looks like this: + /// + /// ```c++ + /// double Vector3i::distance_to(const Vector3i &p_to) const { + /// return (p_to - *this).length(); + /// } + /// + /// int64_t Vector3i::length_squared() const { + /// return x * (int64_t)x + y * (int64_t)y + z * (int64_t)z; + /// } + /// + /// double Vector3i::length() const { + /// return Math::sqrt((double)length_squared()); + /// } + /// ``` + /// + /// Note in particular the cast `(int64_t)` in `length_squared`. So the treatment of the X coordinate in a non-optimized build is (using Swift notation): + /// + /// ```swift + /// square(signExtend(Int32(1) &- Int32.min)) + /// == + /// square(signExtend(0x0000_0001 &- 0x8000_0000)) + /// == // overflow! + /// square(signExtend(0x8000_0001)) + /// == + /// square(0xffff_ffff_8000_0001) + /// == + /// 0x3fff_ffff_0000_0001 + /// ``` + /// + /// But `1 &- Int32.min` is signed integer overflow, and in C++, signed integer overflow is undefined behavior. The optimizer is allowed to assume that undefined behavior doesn't happen. Clang chooses to assume that `b.x - a.x` does not overflow (where, remember, `b.x` and `a.x` are `Int32`). If `b.x - a.x` doesn't overflow, then `Int64(b.x) - Int64(a.x)` is mathematically equal to `b.x - a.x`. So clang's optimizer treats the X coordinate like this: + /// + /// ```swift + /// square(signExtend(Int32(1)) &- signExtend(Int32.min)) + /// = + /// square(0x0000_0000_0000_0001 &- 0xffff_ffff_8000_0000 + /// = // no overflow! + /// square(0x0000_0000_8000_0001) + /// = + /// 0x4000_0001_0000_0001 + /// ``` + /// + /// The difference between the two computations is big enough that the `distanceTo` answer is 2147483647.0 in a debug build and 2147483649.0 in a release build. + /// + /// I can't know here whether I've been linked to a debug libgodot or a release libgodot. So I simply avoid testing `distanceTo` and `distanceSquaredTo` with inputs that could cause signed integer overflow. + + + try checkMethodAvoidingOverflow(Vector3i.distanceTo) + try checkMethodAvoidingOverflow(Vector3i.distanceSquaredTo) + try checkMethod(Vector3i.min(with:)) + try checkMethod(Vector3i.max(with:)) + } + + func testClamp() throws { + for v in Self.testFewerVectors { + for u in Self.testFewerVectors { + for w in Self.testFewerVectors { + try checkCover { v.clamp(min: u, max: w) } + } + } + } + } + + func testClampi() throws { + for v in Self.testVectors { + for i in Self.testInt64s { + for j in Self.testInt64s { + try checkCover { v.clampi(min: i, max: j) } + } + } + } + } + + func testSnappedi() throws { + for v in Self.testVectors { + for i in Self.testInt64s { + try checkCover { v.snappedi(step: i) } + } + } + } + + func testMini() throws { + for v in Self.testVectors { + for i in Self.testInt64s { + try checkCover { v.mini(with: i) } + } + } + } + + func testMaxi() throws { + for v in Self.testVectors { + for i in Self.testInt64s { + try checkCover { v.maxi(with: i) } + } + } + } + + func testSubscriptGet() throws { + for v in Self.testVectors { + for i in Vector3i.Axis.allCases { + try checkCover { + var v = v + return v[i.rawValue] + } + } + } + } + + func testSubscriptSet() throws { + for v in Self.testVectors { + for i in Vector3i.Axis.allCases { + for j in Self.testInt64s { + try checkCover { + var v = v + v[i.rawValue] = j + return v + } + } + } + } + } + + func testBinaryOperators_Vector3i_Vector3i() throws { + // Operators of the form Vector3i * Vector3i. + + func checkOperator( + _ op: (Vector3i, Vector3i) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for u in Self.testVectors { + try checkCover(filePath: filePath, line: line) { op(v, u) } + } + } + } + + try checkOperator(==) + try checkOperator(!=) + try checkOperator(<) + try checkOperator(<=) + try checkOperator(>) + try checkOperator(>=) + try checkOperator(+) + try checkOperator(-) + try checkOperator(*) + try checkOperator(/) + try checkOperator(%) + } + + func testBinaryOperators_Vector3i_Int64() throws { + // Operators of the form Vector3i * Int64. + + func checkOperator( + _ op: (Vector3i, Int64) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for i in Self.testInt64s { + try checkCover(filePath: filePath, line: line) { op(v, i) } + } + } + } + + try checkOperator(*) + try checkOperator(/) + try checkOperator(%) + } + + func testTimesInt64() throws { + for v in Self.testVectors { + for d in Self.testDoubles { + try checkCover { v * d } + } + } + } + + func testDividedByInt64() throws { + for v in Self.testVectors { + for d in Self.testDoubles { + try checkCover { v / d } + } + } + } + func testOperatorUnaryMinus () { var value: Vector3i From 0b4b9747479f0412f9197561cbf2c1c7189c9772 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Sat, 30 Nov 2024 07:41:06 -0600 Subject: [PATCH 50/99] handle cover methods that return optional --- Generator/Generator/SwiftCovers.swift | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Generator/Generator/SwiftCovers.swift b/Generator/Generator/SwiftCovers.swift index 3ddbac890..2a6fe896c 100644 --- a/Generator/Generator/SwiftCovers.swift +++ b/Generator/Generator/SwiftCovers.swift @@ -163,15 +163,7 @@ struct SwiftCovers { let body = function.body else { return false } - let returnType: String - if let returnTypeSx = signature.returnClause?.type.as(IdentifierTypeSyntax.self) { - returnType = returnTypeSx.name.text - } else if signature.returnClause == nil { - returnType = "Void" - } else { - print("warning: couldn't handle return type \(signature.returnClause!)") - return true - } + let returnType = signature.returnClause?.type.trimmed.description ?? "Void" let key = Key( type: type, From 0dce502c693e594b7fb9e66382f18d8b8f935aba Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Sun, 1 Dec 2024 00:28:35 -0600 Subject: [PATCH 51/99] add Vector4i covers --- Sources/SwiftCovers/Vector4i.covers.swift | 274 ++++++++++++++++ Sources/SwiftGodot/SwiftCoverSupport.swift | 6 + .../BuiltIn/Vector4iTests.swift | 294 +++++++++++++++++- 3 files changed, 573 insertions(+), 1 deletion(-) create mode 100644 Sources/SwiftCovers/Vector4i.covers.swift diff --git a/Sources/SwiftCovers/Vector4i.covers.swift b/Sources/SwiftCovers/Vector4i.covers.swift new file mode 100644 index 000000000..77dfe110a --- /dev/null +++ b/Sources/SwiftCovers/Vector4i.covers.swift @@ -0,0 +1,274 @@ +@_spi(SwiftCovers) import SwiftGodot + +extension Vector4i { + + public init(from: Vector4i) { + self = from + } + + public init(from: Vector4) { + self.init( + x: cCastToInt32(from.x), + y: cCastToInt32(from.y), + z: cCastToInt32(from.z), + w: cCastToInt32(from.w) + ) + } + + public func minAxisIndex() -> Int64 { + var minIndex: Int64 = 0 + var minValue = Int64(x) + var me = self + for i: Int64 in 1 ..< 4 { + if me[i] <= minValue { + minIndex = i + minValue = Int64(me[i]) + } + } + return minIndex + } + + public func maxAxisIndex() -> Int64 { + var maxIndex: Int64 = 0 + var maxValue = Int64(x) + var me = self + for i: Int64 in 1 ..< 4 { + if me[i] > maxValue { + maxIndex = i + maxValue = Int64(me[i]) + } + } + return maxIndex + } + + public func length() -> Double { + return Double(lengthSquared()).squareRoot() + } + + public func lengthSquared() -> Int64 { + let x = Int64(x) + let y = Int64(y) + let z = Int64(z) + let w = Int64(w) + return x &* x &+ y &* y &+ z &* z &+ w &* w + } + + public func sign() -> Vector4i { + return Vector4i(x: x.signum(), y: y.signum(), z: z.signum(), w: w.signum()) + } + + public func abs() -> Vector4i { + return Vector4i( + x: Int32(truncatingIfNeeded: x.magnitude), + y: Int32(truncatingIfNeeded: y.magnitude), + z: Int32(truncatingIfNeeded: z.magnitude), + w: Int32(truncatingIfNeeded: w.magnitude) + ) + } + + public func clamp(min: Vector4i, max: Vector4i) -> Vector4i { + return Vector4i( + x: x.clamped(min: min.x, max: max.x), + y: y.clamped(min: min.y, max: max.y), + z: z.clamped(min: min.z, max: max.z), + w: w.clamped(min: min.w, max: max.w) + ) + } + + public func clampi(min: Int64, max: Int64) -> Vector4i { + let min = Int32(truncatingIfNeeded: min) + let max = Int32(truncatingIfNeeded: max) + return Vector4i( + x: x.clamped(min: min, max: max), + y: y.clamped(min: min, max: max), + z: z.clamped(min: min, max: max), + w: w.clamped(min: min, max: max) + ) + } + + public func snappedi(step: Int64) -> Vector4i { + let step = Double(Int32(truncatingIfNeeded: step)) + return Vector4i( + x: cCastToInt32(Double(x).snapped(step: step)), + y: cCastToInt32(Double(y).snapped(step: step)), + z: cCastToInt32(Double(z).snapped(step: step)), + w: cCastToInt32(Double(w).snapped(step: step)) + ) + } + + public func min(with: Vector4i) -> Vector4i { + return Vector4i( + x: Swift.min(x, with.x), + y: Swift.min(y, with.y), + z: Swift.min(z, with.z), + w: Swift.min(w, with.w) + ) + } + + public func mini(with: Int64) -> Vector4i { + let i = Int32(truncatingIfNeeded: with) + return Vector4i( + x: Swift.min(x, i), + y: Swift.min(y, i), + z: Swift.min(z, i), + w: Swift.min(w, i) + ) + } + + public func max(with: Vector4i) -> Vector4i { + return Vector4i( + x: Swift.max(x, with.x), + y: Swift.max(y, with.y), + z: Swift.max(z, with.z), + w: Swift.max(w, with.w) + ) + } + + public func maxi(with: Int64) -> Vector4i { + let i = Int32(truncatingIfNeeded: with) + return Vector4i( + x: Swift.max(x, i), + y: Swift.max(y, i), + z: Swift.max(z, i), + w: Swift.max(w, i) + ) + } + + public func distanceTo(_ to: Vector4i) -> Double { + return (to - self).length() + } + + public func distanceSquaredTo(_ to: Vector4i) -> Int64 { + return (to - self).lengthSquared() + } + + public subscript(index: Int64) -> Int64 { + mutating get { + return Int64(SIMD4(x, y, z, w)[Int(index)]) + } + set { + var simd = SIMD4(x, y, z, w) + simd[Int(index)] = Int32(truncatingIfNeeded: newValue) + (x, y, z, w) = (simd.x, simd.y, simd.z, simd.w) + } + } + + public static func * (lhs: Vector4i, rhs: Int64) -> Vector4i { + let f = Int32(truncatingIfNeeded: rhs) + return Vector4i( + x: lhs.x &* f, + y: lhs.y &* f, + z: lhs.z &* f, + w: lhs.w &* f + ) + } + + public static func / (lhs: Vector4i, rhs: Int64) -> Vector4i { + let rhs = Int32(truncatingIfNeeded: rhs) + return Vector4i( + x: cDivide(numerator: lhs.x, denominator: rhs), + y: cDivide(numerator: lhs.y, denominator: rhs), + z: cDivide(numerator: lhs.z, denominator: rhs), + w: cDivide(numerator: lhs.w, denominator: rhs) + ) + } + + public static func % (lhs: Vector4i, rhs: Int64) -> Vector4i { + let rhs = Int32(truncatingIfNeeded: rhs) + return Vector4i( + x: cRemainder(numerator: lhs.x, denominator: rhs), + y: cRemainder(numerator: lhs.y, denominator: rhs), + z: cRemainder(numerator: lhs.z, denominator: rhs), + w: cRemainder(numerator: lhs.w, denominator: rhs) + ) + } + + public static func * (lhs: Vector4i, rhs: Double) -> Vector4 { + let rhs = Float(rhs) + return Vector4( + x: Float(lhs.x) * rhs, + y: Float(lhs.y) * rhs, + z: Float(lhs.z) * rhs, + w: Float(lhs.w) * rhs + ) + } + + public static func / (lhs: Vector4i, rhs: Double) -> Vector4 { + let rhs = Float(rhs) + return Vector4( + x: Float(lhs.x) / rhs, + y: Float(lhs.y) / rhs, + z: Float(lhs.z) / rhs, + w: Float(lhs.w) / rhs + ) + } + + public static func == (lhs: Vector4i, rhs: Vector4i) -> Bool { + return lhs.tuple == rhs.tuple + } + + public static func != (lhs: Vector4i, rhs: Vector4i) -> Bool { + return !(lhs.tuple == rhs.tuple) + } + + public static func < (lhs: Vector4i, rhs: Vector4i) -> Bool { + return lhs.tuple < rhs.tuple + } + + public static func <= (lhs: Vector4i, rhs: Vector4i) -> Bool { + return lhs.tuple <= rhs.tuple + } + + public static func > (lhs: Vector4i, rhs: Vector4i) -> Bool { + return lhs.tuple > rhs.tuple + } + + public static func >= (lhs: Vector4i, rhs: Vector4i) -> Bool { + return lhs.tuple >= rhs.tuple + } + + public static func + (lhs: Vector4i, rhs: Vector4i) -> Vector4i { + return Vector4i( + x: lhs.x &+ rhs.x, + y: lhs.y &+ rhs.y, + z: lhs.z &+ rhs.z, + w: lhs.w &+ rhs.w + ) + } + + public static func - (lhs: Vector4i, rhs: Vector4i) -> Vector4i { + return Vector4i( + x: lhs.x &- rhs.x, + y: lhs.y &- rhs.y, + z: lhs.z &- rhs.z, + w: lhs.w &- rhs.w + ) + } + + public static func * (lhs: Vector4i, rhs: Vector4i) -> Vector4i { + return Vector4i( + x: lhs.x &* rhs.x, + y: lhs.y &* rhs.y, + z: lhs.z &* rhs.z, + w: lhs.w &* rhs.w + ) + } + + public static func / (lhs: Vector4i, rhs: Vector4i) -> Vector4i { + return Vector4i( + x: cDivide(numerator: lhs.x, denominator: rhs.x), + y: cDivide(numerator: lhs.y, denominator: rhs.y), + z: cDivide(numerator: lhs.z, denominator: rhs.z), + w: cDivide(numerator: lhs.w, denominator: rhs.w) + ) + } + + public static func % (lhs: Vector4i, rhs: Vector4i) -> Vector4i { + return Vector4i( + x: cRemainder(numerator: lhs.x, denominator: rhs.x), + y: cRemainder(numerator: lhs.y, denominator: rhs.y), + z: cRemainder(numerator: lhs.z, denominator: rhs.z), + w: cRemainder(numerator: lhs.w, denominator: rhs.w) + ) + } +} diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index 56f8bab1a..40f0b03f3 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -52,6 +52,12 @@ extension Vector3i { public var tuple: (Int32, Int32, Int32) { (x, y, z) } } +extension Vector4i { + @_spi(SwiftCovers) + @inline(__always) + public var tuple: (Int32, Int32, Int32, Int32) { (x, y, z, w) } +} + @_spi(SwiftCovers) @inline(__always) public func sign(_ x: Float) -> Float { diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector4iTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector4iTests.swift index e83b89066..9fa0d919a 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector4iTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector4iTests.swift @@ -10,7 +10,299 @@ import SwiftGodotTestability @testable import SwiftGodot final class Vector4iTests: GodotTestCase { - + + static let testInt32s: [Int32] = [ + .min, + -2, + -1, + 0, + 1, + 2, + .max, + ] + + /// Fewer values to reduce combinatorial explosion. + static let testFewerInt32s: [Int32] = [ + -2, + 0, + 2, + ] + + /// Adding or subtracting any two of these won't overflow. + static let testSmallerInt32s: [Int32] = [ + -(Int32.max / 2), + -2, + -1, + 0, + 1, + 2, + Int32.max / 2, + ] + + static let testInt64s: [Int64] = [ + .min, + Int64(Int32.min) - 1, + Int64(Int32.min), + -2, + -1, + 0, + 1, + 2, + Int64(Int32.max), + Int64(Int32.max) + 1, + .max + ] + + static let testDoubles: [Double] = testInt64s.map { Double($0) } + [ + -.infinity, + -1e100, + -0.0, + 1e100, + .infinity, + .nan + ] + + static let testVectors: [Vector4i] = testInt32s.flatMap { w in + testInt32s.flatMap { z in + testInt32s.flatMap { y in + testInt32s.map { x in + Vector4i(x: x, y: y, z: z, w: w) + } + } + } + } + + /// Fewer vectors than `testVectors` for tests where the combinatorial explosion from `testVectors` would be too slow. + static let testFewerVectors: [Vector4i] = testFewerInt32s.flatMap { w in + testFewerInt32s.flatMap { z in + testInt32s.flatMap { y in + testFewerInt32s.map { x in + Vector4i(x: x, y: y, z: z, w: w) + } + } + } + } + + /// Vectors where adding or subtracting any two of them won't overflow. + static let testSmallerVectors: [Vector4i] = testSmallerInt32s.flatMap { w in + testSmallerInt32s.flatMap { z in + testSmallerInt32s.flatMap { y in + testSmallerInt32s.map { x in + Vector4i(x: x, y: y, z: z, w: w) + } + } + } + } + + /// Fewer vectors and they won't overflow. + static let testFewerSmallerVectors: [Vector4i] = testFewerInt32s.flatMap { w in + testFewerInt32s.flatMap { z in + testSmallerInt32s.flatMap { y in + testFewerInt32s.map { x in + Vector4i(x: x, y: y, z: z, w: w) + } + } + } + } + + func testInitFromVector4i() throws { + for v in Self.testVectors { + try checkCover { Vector4i(from: v) } + } + } + + func testInitFromVector4() throws { + for y in Self.testInt32s { + try checkCover { Vector4i(from: Vector4(x: 0, y: Float(y), z: 2, w: 1)) } + } + + for y: Float in [-.infinity, -1e25, -0.0, 1e25, .infinity, .nan] { + try checkCover { Vector4i(from: Vector4(x: 0, y: y, z: 2, w: 1))} + } + } + + func testNullaryCovers() throws { + // Methods of the form Vector4i.method(). + + func checkMethod( + _ method: (Vector4i) -> () -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + try checkCover(filePath: filePath, line: line) { method(v)() } + } + } + + try checkMethod(Vector4i.maxAxisIndex) + try checkMethod(Vector4i.minAxisIndex) + try checkMethod(Vector4i.length) + try checkMethod(Vector4i.lengthSquared) + try checkMethod(Vector4i.sign) + try checkMethod(Vector4i.abs) + } + + func testUnaryCovers_Vector4i() throws { + // Methods of the form Vector4i.method(Vector4i). + + func checkMethod( + _ method: (Vector4i) -> (Vector4i) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for w in Self.testFewerVectors { + try checkCover(filePath: filePath, line: line) { method(v)(w) } + } + } + } + + func checkMethodAvoidingOverflow( + _ method: (Vector4i) -> (Vector4i) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testSmallerVectors { + for u in Self.testFewerSmallerVectors { + try checkCover(filePath: filePath, line: line) { method(v)(u) } + } + } + } + + try checkMethodAvoidingOverflow(Vector4i.distanceTo) + try checkMethodAvoidingOverflow(Vector4i.distanceSquaredTo) + try checkMethod(Vector4i.min(with:)) + try checkMethod(Vector4i.max(with:)) + } + + func testClamp() throws { + for v in Self.testFewerVectors { + for u in Self.testFewerVectors { + for w in Self.testFewerVectors { + try checkCover { v.clamp(min: u, max: w) } + } + } + } + } + + func testClampi() throws { + for v in Self.testFewerVectors { + for i in Self.testInt64s { + for j in Self.testInt64s { + try checkCover { v.clampi(min: i, max: j) } + } + } + } + } + + func testSnappedi() throws { + for v in Self.testVectors { + for i in Self.testInt64s { + try checkCover { v.snappedi(step: i) } + } + } + } + + func testMini() throws { + for v in Self.testVectors { + for i in Self.testInt64s { + try checkCover { v.mini(with: i) } + } + } + } + + func testMaxi() throws { + for v in Self.testVectors { + for i in Self.testInt64s { + try checkCover { v.maxi(with: i) } + } + } + } + + func testSubscriptGet() throws { + for v in Self.testVectors { + for i in Vector4i.Axis.allCases { + try checkCover { + var v = v + return v[i.rawValue] + } + } + } + } + + func testSubscriptSet() throws { + for v in Self.testVectors { + for i in Vector4i.Axis.allCases { + for j in Self.testInt64s { + try checkCover { + var v = v + v[i.rawValue] = j + return v + } + } + } + } + } + + func testBinaryOperators_Vector4i_Vector4i() throws { + // Operators of the form Vector4i * Vector4i. + + func checkOperator( + _ op: (Vector4i, Vector4i) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for u in Self.testFewerVectors { + try checkCover(filePath: filePath, line: line) { op(v, u) } + } + } + } + + try checkOperator(==) + try checkOperator(!=) + try checkOperator(<) + try checkOperator(<=) + try checkOperator(>) + try checkOperator(>=) + try checkOperator(+) + try checkOperator(-) + try checkOperator(*) + try checkOperator(/) + try checkOperator(%) + } + + func testBinaryOperators_Vector4i_Int64() throws { + // Operators of the form Vector4i * Int64. + + func checkOperator( + _ op: (Vector4i, Int64) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for i in Self.testInt64s { + try checkCover(filePath: filePath, line: line) { op(v, i) } + } + } + } + + try checkOperator(*) + try checkOperator(/) + try checkOperator(%) + } + + func testTimesInt64() throws { + for v in Self.testVectors { + for d in Self.testDoubles { + try checkCover { v * d } + } + } + } + + func testDividedByInt64() throws { + for v in Self.testVectors { + for d in Self.testDoubles { + try checkCover { v / d } + } + } + } + func testOperatorUnaryMinus () { var value: Vector4i From 2afce2aed13c5de5e3c136445536b1c08e6b6cd7 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Sun, 1 Dec 2024 00:29:25 -0600 Subject: [PATCH 52/99] move CMP_EPSILON to SwiftCoverSupport.swift --- Sources/SwiftCovers/Vector2.covers.swift | 5 +---- Sources/SwiftGodot/SwiftCoverSupport.swift | 4 ++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftCovers/Vector2.covers.swift b/Sources/SwiftCovers/Vector2.covers.swift index e15689df6..9cbc15414 100644 --- a/Sources/SwiftCovers/Vector2.covers.swift +++ b/Sources/SwiftCovers/Vector2.covers.swift @@ -133,10 +133,7 @@ extension Vector2 { return result } - public func moveToward(to: Vector2, delta: Double) -> Vector2 { - /// This epsilon should match the one used by Godot for consistency. - let CMP_EPSILON = Double(0.00001) - + public func moveToward(to: Vector2, delta: Double) -> Vector2 { let result = to - self let newLen = result.length() return newLen <= delta || newLen < CMP_EPSILON ? to : self + result / newLen * delta diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index 40f0b03f3..4dda330b7 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -64,6 +64,10 @@ public func sign(_ x: Float) -> Float { return x == 0 ? 0 : (x > 0 ? 1.0 : -1.0) } +/// This epsilon should match Godot's `CMP_EPSILON` (which is unfortunately not exported). +@_spi(SwiftCovers) +public let CMP_EPSILON: Double = 0.00001 + #if TESTABLE_SWIFT_COVERS /// If true (the default), use Swift cover implementations where available instead of calling Godot engine functions. You should only use this for testing covers. It is not intended for production use. From 7dd2fd5bdc5dac25ab2f9699a25a8de6fb050d1d Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Tue, 3 Dec 2024 12:56:28 -0600 Subject: [PATCH 53/99] Add Vector2Tests --- Sources/SwiftCovers/Vector2.covers.swift | 7 + .../BuiltIn/Vector2Tests.swift | 155 ++++++++++++++++++ 2 files changed, 162 insertions(+) diff --git a/Sources/SwiftCovers/Vector2.covers.swift b/Sources/SwiftCovers/Vector2.covers.swift index 9cbc15414..4c90d1d80 100644 --- a/Sources/SwiftCovers/Vector2.covers.swift +++ b/Sources/SwiftCovers/Vector2.covers.swift @@ -152,5 +152,12 @@ extension Vector2 { return Vector2(x: 2, y: 2) * line * self.dot(with: line) - self } + public static func / (lhs: Vector2, rhs: Int64) -> Vector2 { + return Vector2( + x: lhs.x / Float(rhs), + y: lhs.y / Float(rhs) + ) + } + } diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector2Tests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector2Tests.swift index 928a94611..03abe4a37 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector2Tests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector2Tests.swift @@ -11,6 +11,161 @@ import SwiftGodotTestability final class Vector2Tests: GodotTestCase { + static let testInt64s: [Int64] = [ + .min, + Int64(Int32.min) - 1, + Int64(Int32.min), + -2, + -1, + 0, + 1, + 2, + Int64(Int32.max), + Int64(Int32.max) + 1, + .max + ] + + static let testDoubles: [Double] = testInt64s.map { Double($0) } + [ + -.infinity, + -1e100, + -0.0, + 0.0, + 1e100, + .infinity, + .nan + ] + + static let testFloats: [Float] = testDoubles.map { Float($0) } + + static let testVectors: [Vector2] = testFloats.flatMap { y in + testFloats.map { x in + Vector2(x: x, y: y) + } + } + + // Vector2.method() + func testNullaryCovers() throws { + + func checkMethod(_ method: (Vector2) -> () -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + try checkCover(filePath: filePath, line: line) { method(v)() } + } + } + + try checkMethod(Vector2.angle) + try checkMethod(Vector2.length) + try checkMethod(Vector2.lengthSquared) + try checkMethod(Vector2.normalized) + try checkMethod(Vector2.sign) + try checkMethod(Vector2.floor) + try checkMethod(Vector2.ceil) + try checkMethod(Vector2.round) + } + + // Vector2.method(Double) + func testUnaryDoubleCovers() throws { + + func checkMethod(_ method: (Vector2) -> (Double) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for d in Self.testDoubles { + try checkCover(filePath: filePath, line: line) { method(v)(d) } + } + } + } + + try checkMethod(Vector2.rotated) + try checkMethod(Vector2.snappedf) + try checkMethod(Vector2.limitLength) + } + + // Vector2.method(Vector2) + func testUnaryCovers() throws { + + func checkMethod(_ method: (Vector2) -> (Vector2) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for u in Self.testVectors { + try checkCover(filePath: filePath, line: line) { method(v)(u) } + } + } + } + + try checkMethod(Vector2.distanceTo(_:)) + try checkMethod(Vector2.distanceSquaredTo(_:)) + try checkMethod(Vector2.angleTo(_:)) + try checkMethod(Vector2.angleToPoint) + try checkMethod(Vector2.dot) + try checkMethod(Vector2.cross) + try checkMethod(Vector2.project) + try checkMethod(Vector2.slide) + try checkMethod(Vector2.bounce) + try checkMethod(Vector2.reflect(line:)) + } + + // Static + func testFromAngle() throws { + for d in Self.testDoubles { + try checkCover { Vector2.fromAngle(d) } + } + } + + func testClamp() throws { + for v in Self.testVectors { + for u in Self.testVectors { + for w in Self.testVectors { + try checkCover { v.clamp(min: u, max: w) } + } + } + } + } + + func testClampf() throws { + for v in Self.testVectors { + for d in Self.testDoubles { + for e in Self.testDoubles { + try checkCover { v.clampf(min: d, max: e) } + } + } + } + } + + func testMoveToward() throws { + for v in Self.testVectors { + for u in Self.testVectors { + for d in Self.testDoubles { + try checkCover { v.moveToward(to: u, delta: d) } + } + } + } + } + + // Operator Covers + + func testBinaryOperators_Vector2i_Int64() throws { + // Operators of the form Vector2i * Int64. + + func checkOperator( + _ op: (Vector2, Int64) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for i in Self.testInt64s { + try checkCover(filePath: filePath, line: line) { op(v, i) } + } + } + } + + try checkOperator(/) + } + + + // Non-covers tests + func testOperatorUnaryMinus () { var value: Vector2 From 14e3ebfc094d703f129659b7b1c181e7f86b00cd Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Tue, 3 Dec 2024 19:53:02 -0600 Subject: [PATCH 54/99] Add basis cover --- Sources/SwiftCovers/Basis.covers.swift | 68 ++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 Sources/SwiftCovers/Basis.covers.swift diff --git a/Sources/SwiftCovers/Basis.covers.swift b/Sources/SwiftCovers/Basis.covers.swift new file mode 100644 index 000000000..aeb285a5c --- /dev/null +++ b/Sources/SwiftCovers/Basis.covers.swift @@ -0,0 +1,68 @@ +// +// Basis.covers.swift +// SwiftGodot +// +// Created by Danny Youstra on 12/3/24. +// + +@_spi(SwiftCovers) import SwiftGodot +#if canImport(Darwin) +import Darwin +#elseif os(Windows) +import ucrt +import WinSDK +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#else +#error("Unable to identify your C library.") +#endif + + +extension Basis { + + public init(axis: Vector3, angle: Float) { + // Rotation matrix from axis and angle, see https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_angle + // The axis Vector3 should be normalized. + + let axisSq = Vector3( + x: axis.x * axis.x, + y: axis.y * axis.y, + z: axis.z * axis.z + ) + + let cosine = cos(angle) + let sine = sin(angle) + let t = 1.0 - cosine + + // Diagonals + let xx = axisSq.x + cosine * (1.0 - axisSq.x) + let yy = axisSq.y + cosine * (1.0 - axisSq.y) + let zz = axisSq.z + cosine * (1.0 - axisSq.z) + + // Off-diagonals + var xyzt = axis.x * axis.y * t + var zyxs = axis.z * sine + let xy = xyzt - zyxs + let yx = xyzt + zyxs + + xyzt = axis.x * axis.z * t + zyxs = axis.y * sine + let xz = xyzt + zyxs + let zx = xyzt - zyxs + + xyzt = axis.y * axis.z * t + zyxs = axis.x * sine + let yz = xyzt - zyxs + let zy = xyzt + zyxs + + // Column vectors to create Basis + self.init( + xAxis: Vector3(x: xx, y: yx, z: zx), + yAxis: Vector3(x: xy, y: yy, z: zy), + zAxis: Vector3(x: xz, y: yz, z: zz) + ) + } + +} From 83bb012a99ef7ebd11b305f7253dfb422ded9f96 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Tue, 3 Dec 2024 19:53:38 -0600 Subject: [PATCH 55/99] Add Vector3 Covers --- Sources/SwiftCovers/Vector2.covers.swift | 2 +- Sources/SwiftCovers/Vector3.covers.swift | 306 +++++++++++++++++++++++ 2 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 Sources/SwiftCovers/Vector3.covers.swift diff --git a/Sources/SwiftCovers/Vector2.covers.swift b/Sources/SwiftCovers/Vector2.covers.swift index 4c90d1d80..f3de0e8cf 100644 --- a/Sources/SwiftCovers/Vector2.covers.swift +++ b/Sources/SwiftCovers/Vector2.covers.swift @@ -148,7 +148,7 @@ extension Vector2 { } public func reflect(line: Vector2) -> Vector2 { - /// Reflection requires a scale by 2, but float * Vector2 is not overloaded + /// Reflection requires a scale by 2, but Float * Vector2 is not overloaded return Vector2(x: 2, y: 2) * line * self.dot(with: line) - self } diff --git a/Sources/SwiftCovers/Vector3.covers.swift b/Sources/SwiftCovers/Vector3.covers.swift new file mode 100644 index 000000000..560709fc7 --- /dev/null +++ b/Sources/SwiftCovers/Vector3.covers.swift @@ -0,0 +1,306 @@ +// +// Vector3.covers.swift +// SwiftGodot +// +// Created by Danny Youstra on 12/3/24. +// +@_spi(SwiftCovers) import SwiftGodot + +#if canImport(Darwin) +import Darwin +#elseif os(Windows) +import ucrt +import WinSDK +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#else +#error("Unable to identify your C library.") +#endif + +extension Vector3 { + + public func cross(with: Vector3) -> Vector3 { + return Vector3( + x: (y * with.z) - (z * with.y), + y: (z * with.x) - (x * with.z), + z: (x * with.y) - (y * with.x) + ) + } + + public func dot(with: Vector3) -> Double { + return Double(x * with.x + y * with.y + z * with.z) + } + + public func abs() -> Vector3 { + return Vector3( + x: Swift.abs(x), + y: Swift.abs(y), + z: Swift.abs(z) + ) + } + + public func sign() -> Vector3 { + return Vector3( + x: SwiftGodot.sign(x), + y: SwiftGodot.sign(y), + z: SwiftGodot.sign(z) + ) + } + + public func floor() -> Vector3 { + return Vector3( + x: _math.floor(x), + y: _math.floor(y), + z: _math.floor(z) + ) + } + + public func ceil() -> Vector3 { + return Vector3( + x: _math.ceil(x), + y: _math.ceil(y), + z: _math.ceil(z) + ) + } + + public func round() -> Vector3 { + return Vector3( + x: _math.round(x), + y: _math.round(y), + z: _math.round(z) + ) + } + + public func slerp(to: Vector3, weight: Double) -> Vector3 { + // This method seems more complicated than it really is, since we write out + // the internals of some methods for efficiency (mainly, checking length). + let startLengthSq = lengthSquared() + let endLengthSq = to.lengthSquared() + + // Zero length vectors have no angle, so the best we can do is either lerp or throw an error. + if startLengthSq == 0.0 || endLengthSq == 0.0 { + return lerp(to: to, weight: weight) + } + + var axis = cross(with: to) + let axisLengthSq = axis.lengthSquared() + + // Colinear vectors have no rotation axis or angle between them, so the best we can do is lerp. + if axisLengthSq == 0.0 { + return lerp(to: to, weight: weight) + } + + axis /= sqrt(axisLengthSq) + let startLength = sqrt(startLengthSq) + let resultLength = startLength.lerp(to: sqrt(endLengthSq), weight: weight) + let angle = angleTo(to) + + return rotated(axis: axis, angle: angle * weight) * (resultLength / startLength) + } + + public func rotated(axis: Vector3, angle: Double) -> Vector3 { + // basis subscript getter is mutating by default + var basisV = Basis(axis: axis, angle: Float(angle)) + return Vector3(x: Float(basisV[0].dot(with: self)), + y: Float(basisV[1].dot(with: self)), + z: Float(basisV[2].dot(with: self)) + ) + } + + public func clamp(min: Vector3, max: Vector3) -> Vector3 { + return Vector3(x: x.clamped(min: min.x, max: max.x), + y: y.clamped(min: min.y, max: max.y), + z: z.clamped(min: min.z, max: max.z) + ) + } + + public func clampf(min: Double, max: Double) -> Vector3 { + return Vector3(x: x.clamped(min: Float(min), max: Float(max)), + y: y.clamped(min: Float(min), max: Float(max)), + z: z.clamped(min: Float(min), max: Float(max)) + ) + } + + public func snappedf(step: Double) -> Vector3 { + return Vector3(x: x.snapped(step: Float(step)), + y: y.snapped(step: Float(step)), + z: z.snapped(step: Float(step)) + ) + } + + public func limitLength(_ length: Double = 1.0) -> Vector3 { + let beforeLen = self.length() + var result = self + if (beforeLen > 0 && length < beforeLen) { + result = result / beforeLen + result = result * length + } + return result + } + + public func moveToward(to: Vector3, delta: Double) -> Vector3 { + let result = to - self + let newLen = result.length() + return newLen <= delta || newLen < CMP_EPSILON ? to : self + result / newLen * delta + } + + public func slide(n: Vector3) -> Vector3 { + return self - n * self.dot(with: n) + } + + public func bounce(n: Vector3) -> Vector3 { + return -reflect(n: n) + } + + public func reflect(n: Vector3) -> Vector3 { + /// Reflection requires a scale by 2, but Float * Vector3 is not overloaded + return Vector3(x: 2, y: 2, z: 2) * n * self.dot(with: n) - self + } + + public func octahedronEncode() -> Vector2 { + let n = self / Double((Swift.abs(x) + Swift.abs(y) + Swift.abs(z))) + var o = Vector2() + + if n.z >= 0.0 { + o.x = n.x + o.y = n.y + } else { + o.x = (1.0 - Swift.abs(n.y)) * (n.x >= 0.0 ? 1.0 : -1.0) + o.y = (1.0 - Swift.abs(n.x)) * (n.y >= 0.0 ? 1.0 : -1.0) + } + + o.x = o.x * 0.5 + 0.5 + o.y = o.y * 0.5 + 0.5 + return o + } + + public static func octahedronDecode(uv: Vector2) -> Vector3 { + let f = Vector2(x: uv.x * 2.0 - 1.0, y: uv.y * 2.0 - 1.0) + var n = Vector3(x: f.x, y: f.y, z: 1.0 - Swift.abs(f.x) - Swift.abs(f.y)) + let t = -n.z.clamped(min: 0, max: 1) + + n.x += n.x >= 0 ? -t : t + n.y += n.y >= 0 ? -t : t + return n.normalized() + } + + public func outer(with: Vector3) -> Basis { + return Basis(xAxis: Vector3(x: x * with.x, y: x * with.y, z: x * with.z), + yAxis: Vector3(x: y * with.x, y: y * with.y, z: y * with.z), + zAxis: Vector3(x: z * with.x, y: z * with.y, z: z * with.z) + ) + } + + public func normalized() -> Vector3 { + var result = self + let lensq = Float(lengthSquared()) + if lensq != 0 { + let len = sqrt(lensq) + result = Vector3(x: x / len, y: y / len, z: z / len) + } + return result + } + + // Arithmetic Operators + + public static func + (lhs: Vector3, rhs: Vector3) -> Vector3 { + return Vector3(x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z) + } + + public static func - (lhs: Vector3, rhs: Vector3) -> Vector3 { + return Vector3(x: lhs.x - rhs.x, y: lhs.y - rhs.y, z: lhs.z - rhs.z) + } + + public static func * (lhs: Vector3, rhs: Vector3) -> Vector3 { + return Vector3(x: lhs.x * rhs.x, y: lhs.y * rhs.y, z: lhs.z * rhs.z) + } + + public static func / (lhs: Vector3, rhs: Vector3) -> Vector3 { + return Vector3(x: lhs.x / rhs.x, y: lhs.y / rhs.y, z: lhs.z / rhs.z) + } + + public static func * (lhs: Vector3, rhs: Int64) -> Vector3 { + return Vector3( + x: lhs.x * Float(rhs), + y: lhs.y * Float(rhs), + z: lhs.z * Float(rhs) + ) + } + + public static func * (lhs: Vector3, rhs: Double) -> Vector3 { + return Vector3( + x: lhs.x * Float(rhs), + y: lhs.y * Float(rhs), + z: lhs.z * Float(rhs) + ) + } + + public static func / (lhs: Vector3, rhs: Int64) -> Vector3 { + return Vector3( + x: lhs.x / Float(rhs), + y: lhs.y / Float(rhs), + z: lhs.z / Float(rhs) + ) + } + + public static func / (lhs: Vector3, rhs: Double) -> Vector3 { + return Vector3( + x: lhs.x / Float(rhs), + y: lhs.y / Float(rhs), + z: lhs.z / Float(rhs) + ) + } + + // Comparison Operators + + public static func == (lhs: Vector3, rhs: Vector3) -> Bool { + return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z + } + + public static func != (lhs: Vector3, rhs: Vector3) -> Bool { + return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z + } + + public static func < (lhs: Vector3, rhs: Vector3) -> Bool { + if lhs.x == rhs.x { + if lhs.y == rhs.y { + return lhs.z < rhs.z + } + return lhs.y < rhs.y + } + return lhs.x < rhs.x + } + + public static func > (lhs: Vector3, rhs: Vector3) -> Bool { + if lhs.x == rhs.x { + if lhs.y == rhs.y { + return lhs.z > rhs.z + } + return lhs.y > rhs.y + } + return lhs.x > rhs.x + } + + public static func <= (lhs: Vector3, rhs: Vector3) -> Bool { + if lhs.x == rhs.x { + if lhs.y == rhs.y { + return lhs.z <= rhs.z + } + return lhs.y < rhs.y + } + return lhs.x < rhs.x + } + + public static func >= (lhs: Vector3, rhs: Vector3) -> Bool { + if lhs.x == rhs.x { + if lhs.y == rhs.y { + return lhs.z >= rhs.z + } + return lhs.y > rhs.y + } + return lhs.x > rhs.x + } + +} From f5c712e3a78bc8d46f9e576d5cc63f34b31e957f Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Tue, 3 Dec 2024 21:12:39 -0600 Subject: [PATCH 56/99] Add operator covers for Vector2 --- Sources/SwiftCovers/Vector2.covers.swift | 77 ++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/Sources/SwiftCovers/Vector2.covers.swift b/Sources/SwiftCovers/Vector2.covers.swift index f3de0e8cf..9d0795930 100644 --- a/Sources/SwiftCovers/Vector2.covers.swift +++ b/Sources/SwiftCovers/Vector2.covers.swift @@ -152,6 +152,31 @@ extension Vector2 { return Vector2(x: 2, y: 2) * line * self.dot(with: line) - self } + // Arithmetic Operators + + public static func + (lhs: Vector2, rhs: Vector2) -> Vector2 { + return Vector2(x: lhs.x + rhs.x, y: lhs.y + rhs.y) + } + + public static func - (lhs: Vector2, rhs: Vector2) -> Vector2 { + return Vector2(x: lhs.x - rhs.x, y: lhs.y - rhs.y) + } + + public static func * (lhs: Vector2, rhs: Vector2) -> Vector2 { + return Vector2(x: lhs.x * rhs.x, y: lhs.y * rhs.y) + } + + public static func / (lhs: Vector2, rhs: Vector2) -> Vector2 { + return Vector2(x: lhs.x / rhs.x, y: lhs.y / rhs.y) + } + + public static func * (lhs: Vector2, rhs: Int64) -> Vector2 { + return Vector2( + x: lhs.x * Float(rhs), + y: lhs.y * Float(rhs) + ) + } + public static func / (lhs: Vector2, rhs: Int64) -> Vector2 { return Vector2( x: lhs.x / Float(rhs), @@ -159,5 +184,57 @@ extension Vector2 { ) } + public static func * (lhs: Vector2, rhs: Double) -> Vector2 { + return Vector2( + x: lhs.x * Float(rhs), + y: lhs.y * Float(rhs) + ) + } + + public static func / (lhs: Vector2, rhs: Double) -> Vector2 { + return Vector2( + x: lhs.x / Float(rhs), + y: lhs.y / Float(rhs) + ) + } + + // Comparison Operators + + public static func == (lhs: Vector2, rhs: Vector2) -> Bool { + return lhs.x == rhs.x && lhs.y == rhs.y + } + + public static func != (lhs: Vector2, rhs: Vector2) -> Bool { + return lhs.x != rhs.x || lhs.y != rhs.y + } + + public static func < (lhs: Vector2, rhs: Vector2) -> Bool { + if lhs.x == rhs.x { + return lhs.y < rhs.y + } + return lhs.x < rhs.x + } + + public static func > (lhs: Vector2, rhs: Vector2) -> Bool { + if lhs.x == rhs.x { + return lhs.y > rhs.y + } + return lhs.x > rhs.x + } + + public static func <= (lhs: Vector2, rhs: Vector2) -> Bool { + if lhs.x == rhs.x { + return lhs.y <= rhs.y + } + return lhs.x < rhs.x + } + + public static func >= (lhs: Vector2, rhs: Vector2) -> Bool { + if lhs.x == rhs.x { + return lhs.y >= rhs.y + } + return lhs.x > rhs.x + } + } From 7cb69803c5cf6c466146aee00bb820b9ffc1b63d Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Tue, 3 Dec 2024 23:21:31 -0600 Subject: [PATCH 57/99] Add operator tests to Vector2Tests --- .../BuiltIn/Vector2Tests.swift | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector2Tests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector2Tests.swift index 03abe4a37..e17a8f5c2 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector2Tests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector2Tests.swift @@ -146,6 +146,34 @@ final class Vector2Tests: GodotTestCase { // Operator Covers + func testBinaryOperators_Vector2i_Vector2i() throws { + // Operators of the form Vector2i * Vector2i. + + func checkOperator( + _ op: (Vector2, Vector2) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for u in Self.testVectors { + try checkCover(filePath: filePath, line: line) { op(v, u) } + } + } + } + + // Arithmetic Operators + try checkOperator(+) + try checkOperator(-) + try checkOperator(*) + try checkOperator(/) + // Comparison Operators + try checkOperator(==) + try checkOperator(!=) + try checkOperator(<) + try checkOperator(<=) + try checkOperator(>) + try checkOperator(>=) + } + func testBinaryOperators_Vector2i_Int64() throws { // Operators of the form Vector2i * Int64. @@ -161,6 +189,25 @@ final class Vector2Tests: GodotTestCase { } try checkOperator(/) + try checkOperator(*) + } + + func testBinaryOperators_Vector2i_Double() throws { + // Operators of the form Vector2i * Int64. + + func checkOperator( + _ op: (Vector2, Double) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + for v in Self.testVectors { + for d in Self.testDoubles { + try checkCover(filePath: filePath, line: line) { op(v, d) } + } + } + } + + try checkOperator(/) + try checkOperator(*) } From 265f4f8e994b5e39c91c458f4dd658b1c775034b Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Sun, 1 Dec 2024 00:30:05 -0600 Subject: [PATCH 58/99] write TinyGen --- .../SwiftGodotTestability/GodotTestCase.swift | 40 ++ .../SwiftGodotTestability/TestEquatable.swift | 117 +++++ Sources/SwiftGodotTestability/TinyGen.swift | 419 ++++++++++++++++++ 3 files changed, 576 insertions(+) create mode 100644 Sources/SwiftGodotTestability/TestEquatable.swift create mode 100644 Sources/SwiftGodotTestability/TinyGen.swift diff --git a/Sources/SwiftGodotTestability/GodotTestCase.swift b/Sources/SwiftGodotTestability/GodotTestCase.swift index 104ab7040..3aeaf2aa6 100644 --- a/Sources/SwiftGodotTestability/GodotTestCase.swift +++ b/Sources/SwiftGodotTestability/GodotTestCase.swift @@ -208,6 +208,46 @@ extension GodotTestCase { // Not bytewise-equal, but could still compare equal. XCTAssertEqual(coverValue, engineValue, file: #filePath, line: line) +#else + throw XCTSkip("This test requires the compilation condition TESTABLE_SWIFT_COVERS.", file: filePath, line: line) +#endif + } + + public func forAll( + filePath: StaticString = #filePath, line: UInt = #line, + function: StaticString = #function, + @TinyGenBuilder _ build: () -> TinyGen, + checkCover expression: (Input) throws -> some TestEquatable + ) rethrows { +#if TESTABLE_SWIFT_COVERS + let gen = build() + + for k: UInt64 in 1 ... 1_000 { + var rng = SipRNG(key0: k, key1: 1234) + // Mix in the function name so every test that starts by asking for, say, a Plane doesn't get the same Plane. + function.withUTF8Buffer { buffer in + for byte in buffer { + for bit in 0 ..< 8 { + rng = (byte & (1 << bit) == 0) ? rng.left() : rng.right() + } + } + } + + let input = gen(rng) + + let coverOutput = try $useSwiftCovers.withValue(true) { + try expression(input) + } + let engineOutput = try $useSwiftCovers.withValue(false) { + try expression(input) + } + + guard coverOutput.closeEnough(to: engineOutput) else { + XCTFail("Test failure: cover output \(coverOutput) is not close enough to engine output \(engineOutput)", file: filePath, line: line) + return + } + } + #else throw XCTSkip("This test requires the compilation condition TESTABLE_SWIFT_COVERS.", file: filePath, line: line) #endif diff --git a/Sources/SwiftGodotTestability/TestEquatable.swift b/Sources/SwiftGodotTestability/TestEquatable.swift new file mode 100644 index 000000000..a6b6d6fab --- /dev/null +++ b/Sources/SwiftGodotTestability/TestEquatable.swift @@ -0,0 +1,117 @@ +import SwiftGodot + +/// Like `Equatable`, but designed for testing. +/// +/// I consider floating-point NaN equal to itself. +/// +/// I also consider two floating-point numbers equal if they are within a few ulps of each other. We can't guarantee that the engine and the Swift covers will use exactly the same sequence of instructions for floating-point arithmetic, so one or the other can lose accuracy due to rounding of intermediate results. For example, in a debug build, `Vector4i / Float` uses a division instruction for each of the four components. But in an optimized engine build on ARM, `Vector4i / Float` first computes the reciprocal of the denominator, which incurs an extra rounding, and then uses SIMD multiplication to compute the quotients. That extra rounding can change the results by an ulp. +public protocol TestEquatable { + func closeEnough(to other: Self) -> Bool +} + +extension TestEquatable where Self: Equatable { + // This default implementation works for integer-based types. + public func closeEnough(to other: Self) -> Bool { self == other } +} + +extension Bool: TestEquatable { } +extension Int64: TestEquatable { } +extension Vector2i: TestEquatable { } +extension Vector3i: TestEquatable { } +extension Vector4i: TestEquatable { } + +extension Optional: TestEquatable where Wrapped: TestEquatable { + public func closeEnough(to other: Optional) -> Bool { + switch (self, other) { + case (nil, nil): return true + case (.some(let a), .some(let b)): return a.closeEnough(to: b) + default: return false + } + } +} + +extension Float: TestEquatable { + @TaskLocal public static var closeEnoughUlps: Self = 1 + + public func closeEnough(to other: Float) -> Bool { + if self == other { + return true + } + if self.isNaN && other.isNaN { + return true + } + // Don't allow opposite signs. + guard (self <= 0 && other <= 0) || (self >= 0 && other >= 0) else { return false } + let d = (self - other).magnitude + let ulps = d / min(self.ulp, other.ulp) + let answer = ulps <= Self.closeEnoughUlps + return answer + } +} + +extension Double: TestEquatable { + @TaskLocal public static var closeEnoughUlps: Self = 1 + + public func closeEnough(to other: Double) -> Bool { + if self == other { + return true + } + if self.isNaN && other.isNaN { + return true + } + // Don't allow opposite signs. + guard (self <= 0 && other <= 0) || (self >= 0 && other >= 0) else { return false } + let d = (self - other).magnitude + let ulps = d / min(self.ulp, other.ulp) + let answer = ulps <= Self.closeEnoughUlps + return answer + } +} + +extension Vector2: TestEquatable { + public func closeEnough(to other: Vector2) -> Bool { + return self.x.closeEnough(to: other.x) && self.y.closeEnough(to: other.y) + } +} + +extension Vector3: TestEquatable { + public func closeEnough(to other: Vector3) -> Bool { + return self.x.closeEnough(to: other.x) && self.y.closeEnough(to: other.y) && self.z.closeEnough(to: other.z) + } +} + +extension Vector4: TestEquatable { + public func closeEnough(to other: Vector4) -> Bool { + return self.x.closeEnough(to: other.x) && self.y.closeEnough(to: other.y) && self.z.closeEnough(to: other.z) && self.w.closeEnough(to: other.w) + } +} + +extension Basis: TestEquatable { + public func closeEnough(to other: Basis) -> Bool { + return self.x.closeEnough(to: other.x) && self.y.closeEnough(to: other.y) && self.z.closeEnough(to: other.z) + } +} + +extension Transform2D: TestEquatable { + public func closeEnough(to other: Transform2D) -> Bool { + return self.x.closeEnough(to: other.x) && self.y.closeEnough(to: other.y) && self.origin.closeEnough(to: other.origin) + } +} + +extension Transform3D: TestEquatable { + public func closeEnough(to other: Transform3D) -> Bool { + return self.basis.closeEnough(to: other.basis) && self.origin.closeEnough(to: other.origin) + } +} + +extension Plane: TestEquatable { + public func closeEnough(to other: Plane) -> Bool { + return self.normal.closeEnough(to: other.normal) && self.d.closeEnough(to: other.d) + } +} + +extension Quaternion: TestEquatable { + public func closeEnough(to other: Quaternion) -> Bool { + return self.x.closeEnough(to: other.x) && self.y.closeEnough(to: other.y) && self.z.closeEnough(to: other.z) && self.w.closeEnough(to: other.w) + } +} diff --git a/Sources/SwiftGodotTestability/TinyGen.swift b/Sources/SwiftGodotTestability/TinyGen.swift new file mode 100644 index 000000000..4da6735e2 --- /dev/null +++ b/Sources/SwiftGodotTestability/TinyGen.swift @@ -0,0 +1,419 @@ +#if canImport(Darwin) +import Darwin +#elseif os(Windows) +import ucrt +import WinSDK +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#else +#error("Unable to identify your C library.") +#endif + +/// A tiny implementation of QuickCheck-like combinators for making random values. +/// +/// Shrinking is not implemented. +public struct TinyGen: Sendable { + private let _gen: @Sendable (_ rng: SipRNG) -> Output + + public init(_ gen: @escaping @Sendable (_ rng: SipRNG) -> Output) { + _gen = gen + } + + public func callAsFunction(_ rng: SipRNG) -> Output { return _gen(rng) } + + public static func build(@TinyGenBuilder _ build: () -> Self) -> Self { + return build() + } +} + +@resultBuilder +public struct TinyGenBuilder { + + public init() { } + + public func callAsFunction(@TinyGenBuilder _ build: () -> TinyGen) -> TinyGen { + return build() + } + + public static func buildExpression(_ gen: TinyGen) -> TinyGen { + return gen + } + + // Variadic generics require macOS 14. + @available(macOS 14, *) + public static func buildBlock(_ gen: repeat TinyGen) -> TinyGen<(repeat each Output)> { + // Copy gen to a local tuple to eliminate the following error as of Xcode 16.1: + // Capture of 'gen' with non-sendable type 'repeat T' in a `@Sendable` closure + let gen = (repeat each gen) + + return TinyGen { rng in + var rng = rng + + func draw(_ gen: TinyGen) -> T { + defer { rng = rng.right() } + return gen(rng.left()) + } + + return (repeat draw(each gen)) + } + } + +} + +extension TinyGen { + /// - parameter transform: A function that transforms my output into something new. + /// - returns: A `TinyGen` that takes my output and applies `transform` to it. + public func map(_ transform: @Sendable @escaping (Output) -> New) -> TinyGen { + return TinyGen { rng in + let old = self(rng) + return transform(old) + } + } + + /// - parameter transform: A function that transforms my output into a generator of something new. + /// - returns: A `TinyGen` that takes my output, applies `transform` to it, and then returns the output of the new generator. + public func flatMap(_ transform: @Sendable @escaping (Output) -> TinyGen) -> TinyGen { + return TinyGen { rng in + return transform(self(rng.left()))(rng.right()) + } + } +} + +extension TinyGen { + /// - parameter output: The output to be returned. + /// - returns: A `TinyGen` that always returns `output`. + public static func const(_ output: Output) -> Self { + return TinyGen { rng in output } + } + + /// - parameter max: The maximum value to return. This value can actually be returned. + /// - returns: A `TinyGen` that returns a random value in the range `0 ... max`. + public static func primitive(max: UInt64) -> Self where Output == UInt64{ + guard max != .max else { + return TinyGen { rng in rng.draw() } + } + + let upperBound = max &+ 1 + + return TinyGen { rng in + // https://github.com/swiftlang/swift/pull/39143/commits/87b3f607042e653a42b505442cc803ec20319c1c + let (result, fraction) = upperBound.multipliedFullWidth(by: rng.left().draw()) + guard fraction > 0 &- upperBound else { return result } + let pHi = upperBound.multipliedFullWidth(by: rng.right().draw()).high + let carry = fraction.addingReportingOverflow(pHi).overflow + return result + (carry ? 1 : 0) + } + } + + /// - parameter values: An array of possible outputs. + /// - returns: A `TinyGen` that returns a random element of `values`. + public static func oneOf(values: [Output]) -> Self { + precondition(!values.isEmpty) + return TinyGen.primitive(max: UInt64(values.count - 1)) + .map { values[Int($0)] } + } + + /// - parameter gens: An array of generators. + /// - returns: A `TinyGen` that picks of one of `gens` at random and returns the output of that generator. + public static func oneOf(gens: [TinyGen]) -> Self { + precondition(!gens.isEmpty) + return TinyGen.primitive(max: UInt64(gens.count - 1)).flatMap { gens[Int($0)] } + } + + /// - parameter values: An array of possible outputs, each with an associated frequency. An output with frequency `10` is chosen twice as often as an output with frequency `5`. + /// - returns: One of the outputs in `values`, randomly chosen according to the associated frequencies. + public static func biasedOneOf(values: [(Int, Output)]) -> Self { + precondition(!values.isEmpty) + precondition(values.allSatisfy { $0.0 >= 0 }) + let totalFrequency = values.reduce(0) { $0 + $1.0 } + return TinyGen.primitive(max: UInt64(totalFrequency - 1)).map { + var i = $0 + for (frequency, value) in values { + if i < frequency { + return value + } + i -= UInt64(frequency) + } + fatalError("unreachable") + } + } + + /// - parameter values: An array of generators, each with an associated frequency. A generator with frequency `10` is chosen twice as often as a generator with frequency `5`. + /// - returns: The output of one of the generators in `values`, randomly chosen according to the associated frequencies. + public static func biasedOneOf(gens: [(Int, TinyGen)]) -> Self { + precondition(!gens.isEmpty) + precondition(gens.allSatisfy { $0.0 >= 0 }) + let totalFrequency = gens.reduce(0) { $0 + $1.0 } + return TinyGen + .primitive(max: UInt64(totalFrequency - 1)) + .flatMap { + var i = $0 + for (frequency, gen) in gens { + if i < frequency { + return gen + } + i -= UInt64(frequency) + } + fatalError("unreachable") + } + } +} + +extension TinyGen where Output == Int32 { + + /// A generator of `Int32`s, unbiased. + public static let allInt32s: TinyGen = TinyGen.primitive(max: .max) + .map { Int32(truncatingIfNeeded: $0) } + + /// A generator of `Int32`s that are near the min and max possible values. + public static let extremeInt32s: TinyGen = oneOf(values: [ + Int32.min, + Int32.min + 1, + Int32.max - 1, + Int32.max, + ]) + + /// A generator of `Int32`s, capable of generating any value but extra likely to generate values that cause overflow. + public static let edgyInt32s: TinyGen = biasedOneOf(gens: [ + (1, extremeInt32s), + (9, allInt32s), + ]) + + /// A generator of “safe” `Int32`s, where any two can be added, subtracted, multiplied, or divided without risk of overflow. + public static let safeInt32s: TinyGen = TinyGen.primitive(max: 2 * 46340) + .map { Int32(truncatingIfNeeded: $0) - 46340} + // 46340² < Int32.max; 46341² > Int32.max. +} + +extension TinyGen where Output == Int64 { + /// A generator of `Int64`s, unbiased. + public static let allInt64s: TinyGen = TinyGen.primitive(max: .max) + .map { Int64(bitPattern: $0) } + + /// A generator of `Int64`s that are near the min and max possible values. + public static let extremeInt64s: TinyGen = oneOf(values: [ + Int64.min, + Int64.min + 1, + Int64.max - 1, + Int64.max, + ]) + + public static let int64sNearInt32Bounds: TinyGen = oneOf(values: [ + Int64(Int32.min) - 1, + Int64(Int32.min), + Int64(Int32.min) + 1, + Int64(Int32.max) - 1, + Int64(Int32.max), + Int64(Int32.max) + 1, + ]) + + /// A generator of `Int64`s, capable of generating any value but extra likely to generate values that cause overflow. + public static let edgyInt64s: TinyGen = biasedOneOf(gens: [ + (1, int64sNearInt32Bounds), + (1, extremeInt64s), + (9, allInt64s), + ]) +} + +extension TinyGen where Output == Double { + /// A generator of `Double`s in the range 0.0 ... 1.0 inclusive. + public static let closedUnitRangeDoubles: TinyGen = TinyGen { rng in + // https://mumble.net/~campbell/2014/04/28/uniform-random-float + // https://mumble.net/~campbell/2014/04/28/random_real.c + var rng = rng + var exponent = -64 + var significand: UInt64 + while true { + significand = rng.left().draw() + rng = rng.right() + guard significand == 0 else { break } + exponent -= 64 + guard exponent >= -1074 else { return 0 } + } + let lzs = significand.leadingZeroBitCount + if lzs != 0 { + exponent -= lzs + significand &<<= lzs + let moreBits = rng.draw() + significand |= (moreBits &>> (64 - lzs)) + } + significand |= 1 + return Double(sign: .plus, exponent: exponent, significand: Double(significand)) + } + + /// - returns: A “random” `Double` drawn from a Gaussian distribution. + @available(macOS 14, *) + public static let gaussianDoubles: TinyGen = TinyGenBuilder { + TinyGen.closedUnitRangeDoubles + TinyGen.closedUnitRangeDoubles + }.map { x1, x2 in + // Box-Muller transform. + let f = (-2.0 * log(x1)).squareRoot() + return f * cos(2.0 * .pi * x2) + } + + /// A generator of “weird” `Double`s. + public static let weirdDoubles: TinyGen = oneOf(values: [ + -.infinity, + -1e100, + -0.0, // negative zero is an unusual value + 1e100, + .infinity, + .nan, + ]) + + /// A generator of “reasonable” `Double`s (in the range `-5000.0 ... 5000.0`). + @available(macOS 14, *) + public static let reasonableDoubles: TinyGen = TinyGen.gaussianDoubles.map { 1000.0 * $0 } + + /// A generator that mostly generates “reasonable” `Double`s but generates some “weird” `Double`s. + @available(macOS 14, *) + public static let mixedDoubles: TinyGen = biasedOneOf(gens: [ + (1, weirdDoubles), + (9, reasonableDoubles), + ]) +} + +extension TinyGen where Output == Float { + /// - returns: A “random” `Float` drawn from a Gaussian distribution. + @available(macOS 14, *) + public static let gaussianFloats: TinyGen = TinyGen.gaussianDoubles.map { Float($0) } + + /// A generator of “weird” `Float`s. + public static let weirdFloats: TinyGen = oneOf(values: [ + -.infinity, + -1e30, + -0.0, // negative zero is an unusual value + 1e30, + .infinity, + .nan, + ]) + + /// A generator of “reasonable” `Float`s. + @available(macOS 14, *) + public static let reasonableFloats: TinyGen = TinyGen.gaussianFloats.map { 100.0 * $0 } + + /// A generator that mostly generates “reasonable” `Float`s but generates some “weird” `Float`s. + @available(macOS 14, *) + public static let mixedFloats: TinyGen = biasedOneOf(gens: [ + (9, reasonableFloats), + (1, weirdFloats), + ]) +} + +extension TinyGen where Output == Float { +} + +/// A splittable random number generator based on the ideas of [*Splittable pseudorandom number generators using cryptographic hashing*](https://publications.lib.chalmers.se/records/fulltext/183348/local_183348.pdf but using SipHash-2-4 as the cryptographic hash. +public struct SipRNG { + private var sipHash: SipHash_2_4 + + public init(key0: UInt64, key1: UInt64) { + sipHash = .init(key0: key0, key1: key1) + } + + /// - returns: A “random” `UInt64` drawn from a uniform distribution. This can be any 64-bit value. I return the same value every time! If you want different values, you need to ask different `SipRNG` instances, by using my `left` and `right` methods to split me. + public func draw() -> UInt64 { + return sipHash.hash() + } + + /// - returns: A new `SipRNG` which (probably) returns a different value from its `draw`, `left`, and `right` methods than either I or my `right()` child do. + public func left() -> Self { + var answer = self + answer.sipHash.append(false) + return answer + } + + /// - returns: A new `SipRNG` which (probably) returns a different value from its `draw`, `left`, and `right` methods than either I or my `left()` child do. + public func right() -> Self { + var answer = self + answer.sipHash.append(true) + return answer + } +} + +/// A fast cryptographic hash algorithm. The Swift standard library also uses this algorithm for hashing. +/// https://github.com/veorq/SipHash +private struct SipHash_2_4 { + /// SipHash state. + private var v0, v1, v2, v3: UInt64 + + /// Bits appended that haven't yet been “compressed” into the state. Bits are inserted from LSB to MSB. + private var buffer: UInt64 = 0 + + /// Bit mask of the next bit to insert into `buffer`. + private var nextBit: UInt64 = 1 + + /// Total number of bits inserted. + private var totalBits: Int = 0 + + init(key0: UInt64, key1: UInt64) { + v0 = key0 ^ 0x736f6d6570736575 + v1 = key1 ^ 0x646f72616e646f6d + v2 = key0 ^ 0x6c7967656e657261 + v3 = key1 ^ 0x7465646279746573 + } + + mutating func append(_ bit: Bool) { + if bit { buffer |= nextBit } + nextBit <<= 1 + totalBits += 1 + guard nextBit == 0 else { return } + compressBuffer() + } + + func hash() -> UInt64 { + var copy = self + return copy.finalize() + } + + private mutating func compressBuffer() { + nextBit = 1 + + v3 ^= buffer + sipRound() + sipRound() + v0 ^= buffer + + buffer = 0 + } + + private mutating func sipRound() { + // Formatted to match the SipHash paper. + + v0 &+= v1 ; v2 &+= v3 + v1.rotLeft(13) ; v3.rotLeft(16) + v1 ^= v0 ; v3 ^= v2 + v0.rotLeft(32) + v2 &+= v1 ; v0 &+= v3 + v1.rotLeft(17) ; v3.rotLeft(21) + v1 ^= v2 ; v3 ^= v0 + v2.rotLeft(32) + } + + private mutating func finalize() -> UInt64 { + // If buffer has less than 8 bits free, I need to start a new buffer to finalize, because I need to put the bit count mod 256 into the high byte of the buffer. + if totalBits & 63 < 8 { + compressBuffer() + } + + // SipHash operates on a byte stream, and puts the total number of compressed bytes, mod 256, in the high byte of the last word. Since I operate on a bit stream, I put the total number of compressed bits mod 256. + buffer |= (UInt64(totalBits) & 0xff) << 56 + compressBuffer() + + v2 ^= 0xff + sipRound() + sipRound() + sipRound() + sipRound() + return v0 ^ v1 ^ v2 ^ v3 + } +} + +extension UInt64 { + fileprivate mutating func rotLeft(_ count: Int) { + self = (self &<< count) | (self &>> (64 - count)) + } +} From b281639150458517ac2814c5dd2f5dfdf5ded86c Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Sun, 1 Dec 2024 00:30:05 -0600 Subject: [PATCH 59/99] redo Vector2i cover tests using TinyGen --- .../BuiltIn/Vector2iCoverTests.swift | 250 ++++++++++++++++++ .../BuiltIn/Vector2iTests.swift | 235 ---------------- 2 files changed, 250 insertions(+), 235 deletions(-) create mode 100644 Tests/SwiftGodotTests/BuiltIn/Vector2iCoverTests.swift diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector2iCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector2iCoverTests.swift new file mode 100644 index 000000000..d29d393ea --- /dev/null +++ b/Tests/SwiftGodotTests/BuiltIn/Vector2iCoverTests.swift @@ -0,0 +1,250 @@ +@testable import SwiftGodot +import SwiftGodotTestability +import XCTest + +extension Vector2i { + static func gen(_ coordinateGen: TinyGen) -> TinyGen { + return TinyGen { rng in + return Vector2i(x: coordinateGen(rng.left()), y: coordinateGen(rng.right())) + } + } + + static let edgy: TinyGen = gen(.edgyInt32s) +} + +@available(macOS 14, *) +extension Vector2 { + static func gen(_ coordinateGen: TinyGen) -> TinyGen { + return TinyGen { rng in + return Vector2(x: coordinateGen(rng.left()), y: coordinateGen(rng.right())) + } + } + + static let mixed: TinyGen = gen(.mixedFloats) +} + +@available(macOS 14, *) +final class Vector2iCoverTests: GodotTestCase { + + func testInitFromVector2i() { + forAll { + Vector2i.edgy + } checkCover: { + Vector2i(from: $0) + } + } + + func testInitFromVector2() { + forAll { + TinyGen.oneOf(gens: [ + Vector2.mixed, + Vector2.gen(TinyGen.edgyInt32s.map { Float($0) }) + ]) + } checkCover: { + Vector2i(from: $0) + } + } + + func testNullaryCovers() { + // Methods of the form Vector2i.method(). + + func checkMethod( + _ method: (Vector2i) -> () -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector2i.edgy + } checkCover: { + method($0)() + } + } + + checkMethod(Vector2i.aspect) + checkMethod(Vector2i.maxAxisIndex) + checkMethod(Vector2i.minAxisIndex) + checkMethod(Vector2i.length) + checkMethod(Vector2i.lengthSquared) + checkMethod(Vector2i.sign) + checkMethod(Vector2i.abs) + } + + func testUnaryCovers_Vector2i() { + // Methods of the form Vector2i.method(Vector2i). + + func checkMethod( + _ method: (Vector2i) -> (Vector2i) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector2i.edgy + Vector2i.edgy + } checkCover: { + method($0)($1) + } + } + + checkMethod(Vector2i.distanceTo) + checkMethod(Vector2i.distanceSquaredTo) + checkMethod(Vector2i.min(with:)) + checkMethod(Vector2i.max(with:)) + } + + func testClamp() { + forAll { + Vector2i.edgy + Vector2i.edgy + Vector2i.edgy + } checkCover: { + $0.clamp(min: $1, max: $2) + } + } + + func testClampi() { + forAll { + Vector2i.edgy + TinyGen.edgyInt64s + TinyGen.edgyInt64s + } checkCover: { + $0.clampi(min: $1, max: $2) + } + } + + func testUnaryCovers_Int64() { + // Methods of the form Vector2i.method(Int64). + + func checkMethod( + _ method: (Vector2i) -> (Int64) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector2i.edgy + TinyGen.edgyInt64s + } checkCover: { + method($0)($1) + } + } + + checkMethod(Vector2i.snappedi) + checkMethod(Vector2i.mini) + checkMethod(Vector2i.maxi) + } + + func testSubscriptGet() { + forAll { + Vector2i.edgy + TinyGen.oneOf(values: Vector2i.Axis.allCases) + } checkCover: { + var v = $0 + return v[$1.rawValue] + } + } + + func testSubscriptSet() { + forAll { + Vector2i.edgy + TinyGen.oneOf(values: Vector2i.Axis.allCases) + TinyGen.edgyInt64s + } checkCover: { + var v = $0 + v[$1.rawValue] = $2 + return v + } + } + + func testBinaryOperators_Vector2i_Vector2i() throws { + // Operators of the form Vector2i * Vector2i. + + func checkOperator( + _ op: (Vector2i, Vector2i) -> Vector2i, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + forAll(filePath: filePath, line: line) { + Vector2i.edgy + Vector2i.edgy + } checkCover: { + op($0, $1) + } + } + + try checkOperator(+) + try checkOperator(-) + try checkOperator(*) + try checkOperator(/) + + // try checkOperator(%) + // + // The `Vector2i % Vector2i` operator is implemented incorrectly by Godot, for any gdextension that uses the ptrcall API. It performs `Vector2i / Vector2i` instead of what it's supposed to do. + // + // See https://github.com/godotengine/godot/issues/99518 for details. + // + // Note that it isn't enough for the bug to be fixed in the Godot project. The libgodot project also needs to be fixed, because that's what SwiftGodot actually uses. + // https://github.com/migueldeicaza/libgodot + } + + func testComparisonOperators_Vector2i_Vector2i() throws { + // Operators of the form Vector2i == Vector2i. + + func checkOperator( + _ op: (Vector2i, Vector2i) -> Bool, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + forAll(filePath: filePath, line: line) { + Vector2i.edgy + Vector2i.edgy + } checkCover: { + op($0, $1) + } + + forAll(filePath: filePath, line: line) { + Vector2i.edgy + } checkCover: { + op($0, $0) + } + } + + try checkOperator(==) + try checkOperator(!=) + try checkOperator(<) + try checkOperator(<=) + try checkOperator(>) + try checkOperator(>=) + } + + func testBinaryOperators_Vector2i_Int64() throws { + // Operators of the form Vector2i * Int64. + + func checkOperator( + _ op: (Vector2i, Int64) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + forAll(filePath: filePath, line: line) { + Vector2i.edgy + TinyGen.edgyInt64s + } checkCover: { + op($0, $1) + } + } + + try checkOperator(*) + try checkOperator(/) + try checkOperator(%) + } + + func testTimesInt64() { + forAll { + Vector2i.edgy + TinyGen.mixedDoubles + } checkCover: { + $0 * $1 + } + } + + func testDividedByInt64() { + forAll { + Vector2i.edgy + TinyGen.mixedDoubles + } checkCover: { + $0 / $1 + } + } +} diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift index 4aa143559..60edc609e 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector2iTests.swift @@ -12,241 +12,6 @@ import RegexBuilder final class Vector2iTests: GodotTestCase { - static let testInt32s: [Int32] = [ - .min, - -2, - -1, - 0, - 1, - 2, - .max, - ] - - static let testInt64s: [Int64] = [ - .min, - Int64(Int32.min) - 1, - Int64(Int32.min), - -2, - -1, - 0, - 1, - 2, - Int64(Int32.max), - Int64(Int32.max) + 1, - .max - ] - - static let testDoubles: [Double] = testInt64s.map { Double($0) } + [ - -.infinity, - -1e100, - -0.0, - 1e100, - .infinity, - .nan - ] - - static let testVectors: [Vector2i] = testInt32s.flatMap { y in - testInt32s.map { x in - Vector2i(x: x, y: y) - } - } - - func testInitFromVector2i() throws { - for v in Self.testVectors { - try checkCover { Vector2i(from: v) } - } - } - - func testInitFromVector2() throws { - for y in Self.testInt32s { - try checkCover { Vector2i(from: Vector2(x: 0, y: Float(y))) } - } - - for y: Float in [-.infinity, -1e25, -0.0, 1e25, .infinity, .nan] { - try checkCover { Vector2i(from: Vector2(x: 0, y: y)) } - } - } - - func testNullaryCovers() throws { - // Methods of the form Vector2i.method(). - - func checkMethod( - _ method: (Vector2i) -> () -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - try checkCover(filePath: filePath, line: line) { method(v)() } - } - } - - try checkMethod(Vector2i.aspect) - try checkMethod(Vector2i.maxAxisIndex) - try checkMethod(Vector2i.minAxisIndex) - try checkMethod(Vector2i.length) - try checkMethod(Vector2i.lengthSquared) - try checkMethod(Vector2i.sign) - try checkMethod(Vector2i.abs) - } - - func testUnaryCovers_Vector2i() throws { - // Methods of the form Vector2i.method(Vector2i). - - func checkMethod( - _ method: (Vector2i) -> (Vector2i) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for u in Self.testVectors { - try checkCover(filePath: filePath, line: line) { method(v)(u) } - } - } - } - - try checkMethod(Vector2i.distanceTo) - try checkMethod(Vector2i.distanceSquaredTo) - try checkMethod(Vector2i.min(with:)) - try checkMethod(Vector2i.max(with:)) - } - - func testClamp() throws { - for v in Self.testVectors { - for u in Self.testVectors { - for w in Self.testVectors { - try checkCover { v.clamp(min: u, max: w) } - } - } - } - } - - func testClampi() throws { - for v in Self.testVectors { - for i in Self.testInt64s { - for j in Self.testInt64s { - try checkCover { v.clampi(min: i, max: j) } - } - } - } - } - - func testSnappedi() throws { - for v in Self.testVectors { - for i in Self.testInt64s { - try checkCover { v.snappedi(step: i) } - } - } - } - - func testMini() throws { - for v in Self.testVectors { - for i in Self.testInt64s { - try checkCover { v.mini(with: i) } - } - } - } - - func testMaxi() throws { - for v in Self.testVectors { - for i in Self.testInt64s { - try checkCover { v.maxi(with: i) } - } - } - } - - func testSubscriptGet() throws { - for v in Self.testVectors { - for i in Vector2i.Axis.allCases { - try checkCover { - var v = v - return v[i.rawValue] - } - } - } - } - - func testSubscriptSet() throws { - for v in Self.testVectors { - for i in Vector2i.Axis.allCases { - for j in Self.testInt64s { - try checkCover { - var v = v - v[i.rawValue] = j - return v - } - } - } - } - } - - func testBinaryOperators_Vector2i_Vector2i() throws { - // Operators of the form Vector2i * Vector2i. - - func checkOperator( - _ op: (Vector2i, Vector2i) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for u in Self.testVectors { - try checkCover(filePath: filePath, line: line) { op(v, u) } - } - } - } - - try checkOperator(==) - try checkOperator(!=) - try checkOperator(<) - try checkOperator(<=) - try checkOperator(>) - try checkOperator(>=) - try checkOperator(+) - try checkOperator(-) - try checkOperator(*) - try checkOperator(/) - - // try checkOperator(%) - // - // The `Vector2i % Vector2i` operator is implemented incorrectly by Godot, for any gdextension that uses the ptrcall API. It performs `Vector2i / Vector2i` instead of what it's supposed to do. - // - // See https://github.com/godotengine/godot/issues/99518 for details. - // - // Note that it isn't enough for the bug to be fixed in the Godot project. The libgodot project also needs to be fixed, because that's what SwiftGodot actually uses. - // https://github.com/migueldeicaza/libgodot - } - - func testBinaryOperators_Vector2i_Int64() throws { - // Operators of the form Vector2i * Int64. - - func checkOperator( - _ op: (Vector2i, Int64) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for i in Self.testInt64s { - try checkCover(filePath: filePath, line: line) { op(v, i) } - } - } - } - - try checkOperator(*) - try checkOperator(/) - try checkOperator(%) - } - - func testTimesInt64() throws { - for v in Self.testVectors { - for d in Self.testDoubles { - try checkCover { v * d } - } - } - } - - func testDividedByInt64() throws { - for v in Self.testVectors { - for d in Self.testDoubles { - try checkCover { v / d } - } - } - } - func testOperatorUnaryMinus () { var value: Vector2i From 2420a44ea6b110150edac972d8fd17ea2beb2054 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 3 Dec 2024 15:20:46 -0600 Subject: [PATCH 60/99] rewrite Vector3i cover tests using TinyGen --- .../BuiltIn/Vector3iCoverTests.swift | 286 +++++++++++++++ .../BuiltIn/Vector3iTests.swift | 334 ------------------ 2 files changed, 286 insertions(+), 334 deletions(-) create mode 100644 Tests/SwiftGodotTests/BuiltIn/Vector3iCoverTests.swift diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector3iCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector3iCoverTests.swift new file mode 100644 index 000000000..9b6d44d8a --- /dev/null +++ b/Tests/SwiftGodotTests/BuiltIn/Vector3iCoverTests.swift @@ -0,0 +1,286 @@ +@testable import SwiftGodot +import SwiftGodotTestability +import XCTest + +extension Vector3i { + static func gen(_ coordinateGen: TinyGen) -> TinyGen { + return TinyGen { rng in + let right = rng.right() + return Vector3i( + x: coordinateGen(rng.left()), + y: coordinateGen(right.left()), + z: coordinateGen(right.right()) + ) + } + } + + static let edgy: TinyGen = gen(.edgyInt32s) + static let safe: TinyGen = gen(.safeInt32s) +} + +@available(macOS 14, *) +extension Vector3 { + static func gen(_ coordinateGen: TinyGen) -> TinyGen { + return TinyGen { rng in + let right = rng.right() + return Vector3( + x: coordinateGen(rng.left()), + y: coordinateGen(right.left()), + z: coordinateGen(right.right()) + ) + } + } + + static let mixed: TinyGen = gen(.mixedFloats) +} + +@available(macOS 14, *) +final class Vector3iCoverTests: GodotTestCase { + + func testInitFromVector3i() { + forAll { + Vector3i.edgy + } checkCover: { + Vector3i(from: $0) + } + } + + func testInitFromVector3() { + forAll { + TinyGen.oneOf(gens: [ + Vector3.mixed, + Vector3.gen(TinyGen.edgyInt32s.map { Float($0) }) + ]) + } checkCover: { + Vector3i(from: $0) + } + } + + func testNullaryCovers() { + // Methods of the form Vector3i.method(). + + func checkMethod( + _ method: (Vector3i) -> () -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector3i.edgy + } checkCover: { + method($0)() + } + } + + checkMethod(Vector3i.maxAxisIndex) + checkMethod(Vector3i.minAxisIndex) + checkMethod(Vector3i.length) + checkMethod(Vector3i.lengthSquared) + checkMethod(Vector3i.sign) + checkMethod(Vector3i.abs) + } + + func testUnaryCovers_Vector3i() { + // Methods of the form Vector3i.method(Vector3i). + + func checkMethod( + _ method: (Vector3i) -> (Vector3i) -> some TestEquatable, + forVectors vectors: TinyGen, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + vectors + vectors + } checkCover: { + method($0)($1) + } + } + + /// ## Why I restrict the test inputs for `distanceTo` and `distanceSquaredTo` + /// + /// Consider this program: + /// + /// ```swift + /// let a = Vector3i(x: .min, y: .min, z: .min) + /// let b = Vector3i(x: 1, y: .min, z: .min) + /// let answer = a.distanceTo(b) + /// ``` + /// + /// Remarkably, this produces different output depending on whether libgodot was compiled with optimization or not. + /// + /// The Godot implementation looks like this: + /// + /// ```c++ + /// double Vector3i::distance_to(const Vector3i &p_to) const { + /// return (p_to - *this).length(); + /// } + /// + /// int64_t Vector3i::length_squared() const { + /// return x * (int64_t)x + y * (int64_t)y + z * (int64_t)z; + /// } + /// + /// double Vector3i::length() const { + /// return Math::sqrt((double)length_squared()); + /// } + /// ``` + /// + /// Note in particular the cast `(int64_t)` in `length_squared`. So the treatment of the X coordinate in a non-optimized build is (using Swift notation): + /// + /// ```swift + /// square(signExtend(Int32(1) &- Int32.min)) + /// == + /// square(signExtend(0x0000_0001 &- 0x8000_0000)) + /// == // overflow! + /// square(signExtend(0x8000_0001)) + /// == + /// square(0xffff_ffff_8000_0001) + /// == + /// 0x3fff_ffff_0000_0001 + /// ``` + /// + /// But `1 &- Int32.min` is signed integer overflow, and in C++, signed integer overflow is undefined behavior. The optimizer is allowed to assume that undefined behavior doesn't happen. Clang chooses to assume that `b.x - a.x` does not overflow (where, remember, `b.x` and `a.x` are `Int32`). If `b.x - a.x` doesn't overflow, then `Int64(b.x) - Int64(a.x)` is mathematically equal to `b.x - a.x`. So clang's optimizer treats the X coordinate like this: + /// + /// ```swift + /// square(signExtend(Int32(1)) &- signExtend(Int32.min)) + /// = + /// square(0x0000_0000_0000_0001 &- 0xffff_ffff_8000_0000 + /// = // no overflow! + /// square(0x0000_0000_8000_0001) + /// = + /// 0x4000_0001_0000_0001 + /// ``` + /// + /// The difference between the two computations is big enough that the `distanceTo` answer is 2147483647.0 in a debug build and 2147483649.0 in a release build. + /// + /// I can't know here whether I've been linked to a debug libgodot or a release libgodot. So I simply avoid testing `distanceTo` and `distanceSquaredTo` with inputs that could cause signed integer overflow. + + checkMethod(Vector3i.distanceTo, forVectors: Vector3i.safe) + checkMethod(Vector3i.distanceSquaredTo, forVectors: Vector3i.safe) + checkMethod(Vector3i.min(with:), forVectors: Vector3i.edgy) + checkMethod(Vector3i.max(with:), forVectors: Vector3i.edgy) + } + + func testClamp() { + forAll { + Vector3i.edgy + Vector3i.edgy + Vector3i.edgy + } checkCover: { + $0.clamp(min: $1, max: $2) + } + } + + func testClampi() { + forAll { + Vector3i.edgy + TinyGen.edgyInt64s + TinyGen.edgyInt64s + } checkCover: { + $0.clampi(min: $1, max: $2) + } + } + + func testUnaryCovers_Int64() { + func checkMethod( + _ method: (Vector3i) -> (Int64) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector3i.edgy + TinyGen.edgyInt64s + } checkCover: { + method($0)($1) + } + } + + checkMethod(Vector3i.snappedi) + checkMethod(Vector3i.mini) + checkMethod(Vector3i.maxi) + } + + func testSubscriptGet() { + forAll { + Vector3i.edgy + TinyGen.oneOf(values: Vector3i.Axis.allCases) + } checkCover: { + var v = $0 + return v[$1.rawValue] + } + } + + func testSubscriptSet() { + forAll { + Vector3i.edgy + TinyGen.oneOf(values: Vector3i.Axis.allCases) + TinyGen.edgyInt64s + } checkCover: { + var v = $0 + v[$1.rawValue] = $2 + return v + } + } + + func testBinaryOperators_Vector3i_Vector3i() { + // Operators of the form Vector3i * Vector3i. + + func checkOperator( + _ op: (Vector3i, Vector3i) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector3i.edgy + Vector3i.edgy + } checkCover: { + op($0, $1) + } + } + + checkOperator(==) + checkOperator(!=) + checkOperator(<) + checkOperator(<=) + checkOperator(>) + checkOperator(>=) + checkOperator(+) + checkOperator(-) + checkOperator(*) + checkOperator(/) + checkOperator(%) + } + + func testBinaryOperators_Vector3i_Int64() { + // Operators of the form Vector3i * Int64. + + func checkOperator( + _ op: (Vector3i, Int64) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector3i.edgy + TinyGen.edgyInt64s + } checkCover: { + op($0, $1) + } + } + + checkOperator(*) + checkOperator(/) + checkOperator(%) + } + + func testTimesInt64() { + forAll { + Vector3i.edgy + TinyGen.mixedDoubles + } checkCover: { + $0 * $1 + } + } + + func testDividedByInt64() { + forAll { + Vector3i.edgy + TinyGen.mixedDoubles + } checkCover: { + $0 / $1 + } + } +} diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector3iTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector3iTests.swift index 431395a1d..0e2af7523 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector3iTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector3iTests.swift @@ -11,340 +11,6 @@ import SwiftGodotTestability final class Vector3iTests: GodotTestCase { - static let testInt32s: [Int32] = [ - .min, - -2, - -1, - 0, - 1, - 2, - .max, - ] - - /// Fewer values to reduce combinatorial explosion. - static let testFewerInt32s: [Int32] = [ - -2, - 0, - 2, - ] - - /// Adding or subtracting any two of these won't overflow. - static let testSmallerInt32s: [Int32] = [ - -(Int32.max / 2), - -2, - -1, - 0, - 1, - 2, - Int32.max / 2, - ] - - static let testInt64s: [Int64] = [ - .min, - Int64(Int32.min) - 1, - Int64(Int32.min), - -2, - -1, - 0, - 1, - 2, - Int64(Int32.max), - Int64(Int32.max) + 1, - .max - ] - - static let testDoubles: [Double] = testInt64s.map { Double($0) } + [ - -.infinity, - -1e100, - -0.0, - 1e100, - .infinity, - .nan - ] - - static let testVectors: [Vector3i] = testInt32s.flatMap { z in - testInt32s.flatMap { y in - testInt32s.map { x in - Vector3i(x: x, y: y, z: z) - } - } - } - - /// Fewer vectors than `testVectors` for tests where the combinatorial explosion from `testVectors` would be too slow. - static let testFewerVectors: [Vector3i] = testFewerInt32s.flatMap { z in - testInt32s.flatMap { y in - testFewerInt32s.map { x in - Vector3i(x: x, y: y, z: z) - } - } - } - - /// Vectors where adding or subtracting any two of them won't overflow. - static let testSmallerVectors: [Vector3i] = testSmallerInt32s.flatMap { z in - testSmallerInt32s.flatMap { y in - testSmallerInt32s.map { x in - Vector3i(x: x, y: y, z: z) - } - } - } - - func testInitFromVector3i() throws { - for v in Self.testVectors { - try checkCover { Vector3i(from: v) } - } - } - - func testInitFromVector3() throws { - for y in Self.testInt32s { - try checkCover { Vector3i(from: Vector3(x: 0, y: Float(y), z: 1)) } - } - - for y: Float in [-.infinity, -1e25, -0.0, 1e25, .infinity, .nan] { - try checkCover { Vector3i(from: Vector3(x: 0, y: y, z: 1)) } - } - } - - func testNullaryCovers() throws { - // Methods of the form Vector3i.method(). - - func checkMethod( - _ method: (Vector3i) -> () -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - try checkCover(filePath: filePath, line: line) { method(v)() } - } - } - - try checkMethod(Vector3i.maxAxisIndex) - try checkMethod(Vector3i.minAxisIndex) - try checkMethod(Vector3i.length) - try checkMethod(Vector3i.lengthSquared) - try checkMethod(Vector3i.sign) - try checkMethod(Vector3i.abs) - } - - func testUnaryCovers_Vector3i() throws { - // Methods of the form Vector3i.method(Vector3i). - - func checkMethod( - _ method: (Vector3i) -> (Vector3i) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for u in Self.testVectors { - try checkCover(filePath: filePath, line: line) { method(v)(u) } - } - } - } - - func checkMethodAvoidingOverflow( - _ method: (Vector3i) -> (Vector3i) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testSmallerVectors { - for u in Self.testSmallerVectors { - try checkCover(filePath: filePath, line: line) { method(v)(u) } - } - } - } - - /// ## Why I restrict the test inputs for `distanceTo` and `distanceSquaredTo` - /// - /// Consider this program: - /// - /// ```swift - /// let a = Vector3i(x: .min, y: .min, z: .min) - /// let b = Vector3i(x: 1, y: .min, z: .min) - /// let answer = a.distanceTo(b) - /// ``` - /// - /// Remarkably, this produces different output depending on whether libgodot was compiled with optimization or not. - /// - /// The Godot implementation looks like this: - /// - /// ```c++ - /// double Vector3i::distance_to(const Vector3i &p_to) const { - /// return (p_to - *this).length(); - /// } - /// - /// int64_t Vector3i::length_squared() const { - /// return x * (int64_t)x + y * (int64_t)y + z * (int64_t)z; - /// } - /// - /// double Vector3i::length() const { - /// return Math::sqrt((double)length_squared()); - /// } - /// ``` - /// - /// Note in particular the cast `(int64_t)` in `length_squared`. So the treatment of the X coordinate in a non-optimized build is (using Swift notation): - /// - /// ```swift - /// square(signExtend(Int32(1) &- Int32.min)) - /// == - /// square(signExtend(0x0000_0001 &- 0x8000_0000)) - /// == // overflow! - /// square(signExtend(0x8000_0001)) - /// == - /// square(0xffff_ffff_8000_0001) - /// == - /// 0x3fff_ffff_0000_0001 - /// ``` - /// - /// But `1 &- Int32.min` is signed integer overflow, and in C++, signed integer overflow is undefined behavior. The optimizer is allowed to assume that undefined behavior doesn't happen. Clang chooses to assume that `b.x - a.x` does not overflow (where, remember, `b.x` and `a.x` are `Int32`). If `b.x - a.x` doesn't overflow, then `Int64(b.x) - Int64(a.x)` is mathematically equal to `b.x - a.x`. So clang's optimizer treats the X coordinate like this: - /// - /// ```swift - /// square(signExtend(Int32(1)) &- signExtend(Int32.min)) - /// = - /// square(0x0000_0000_0000_0001 &- 0xffff_ffff_8000_0000 - /// = // no overflow! - /// square(0x0000_0000_8000_0001) - /// = - /// 0x4000_0001_0000_0001 - /// ``` - /// - /// The difference between the two computations is big enough that the `distanceTo` answer is 2147483647.0 in a debug build and 2147483649.0 in a release build. - /// - /// I can't know here whether I've been linked to a debug libgodot or a release libgodot. So I simply avoid testing `distanceTo` and `distanceSquaredTo` with inputs that could cause signed integer overflow. - - - try checkMethodAvoidingOverflow(Vector3i.distanceTo) - try checkMethodAvoidingOverflow(Vector3i.distanceSquaredTo) - try checkMethod(Vector3i.min(with:)) - try checkMethod(Vector3i.max(with:)) - } - - func testClamp() throws { - for v in Self.testFewerVectors { - for u in Self.testFewerVectors { - for w in Self.testFewerVectors { - try checkCover { v.clamp(min: u, max: w) } - } - } - } - } - - func testClampi() throws { - for v in Self.testVectors { - for i in Self.testInt64s { - for j in Self.testInt64s { - try checkCover { v.clampi(min: i, max: j) } - } - } - } - } - - func testSnappedi() throws { - for v in Self.testVectors { - for i in Self.testInt64s { - try checkCover { v.snappedi(step: i) } - } - } - } - - func testMini() throws { - for v in Self.testVectors { - for i in Self.testInt64s { - try checkCover { v.mini(with: i) } - } - } - } - - func testMaxi() throws { - for v in Self.testVectors { - for i in Self.testInt64s { - try checkCover { v.maxi(with: i) } - } - } - } - - func testSubscriptGet() throws { - for v in Self.testVectors { - for i in Vector3i.Axis.allCases { - try checkCover { - var v = v - return v[i.rawValue] - } - } - } - } - - func testSubscriptSet() throws { - for v in Self.testVectors { - for i in Vector3i.Axis.allCases { - for j in Self.testInt64s { - try checkCover { - var v = v - v[i.rawValue] = j - return v - } - } - } - } - } - - func testBinaryOperators_Vector3i_Vector3i() throws { - // Operators of the form Vector3i * Vector3i. - - func checkOperator( - _ op: (Vector3i, Vector3i) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for u in Self.testVectors { - try checkCover(filePath: filePath, line: line) { op(v, u) } - } - } - } - - try checkOperator(==) - try checkOperator(!=) - try checkOperator(<) - try checkOperator(<=) - try checkOperator(>) - try checkOperator(>=) - try checkOperator(+) - try checkOperator(-) - try checkOperator(*) - try checkOperator(/) - try checkOperator(%) - } - - func testBinaryOperators_Vector3i_Int64() throws { - // Operators of the form Vector3i * Int64. - - func checkOperator( - _ op: (Vector3i, Int64) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for i in Self.testInt64s { - try checkCover(filePath: filePath, line: line) { op(v, i) } - } - } - } - - try checkOperator(*) - try checkOperator(/) - try checkOperator(%) - } - - func testTimesInt64() throws { - for v in Self.testVectors { - for d in Self.testDoubles { - try checkCover { v * d } - } - } - } - - func testDividedByInt64() throws { - for v in Self.testVectors { - for d in Self.testDoubles { - try checkCover { v / d } - } - } - } - func testOperatorUnaryMinus () { var value: Vector3i From d5a7e1cd5132eca323342e3566b4dfb91e0d239e Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 3 Dec 2024 16:10:23 -0600 Subject: [PATCH 61/99] redo Vector4i cover tests using TinyGen --- .../BuiltIn/Vector4iCoverTests.swift | 232 ++++++++++++++ .../BuiltIn/Vector4iTests.swift | 292 ------------------ 2 files changed, 232 insertions(+), 292 deletions(-) create mode 100644 Tests/SwiftGodotTests/BuiltIn/Vector4iCoverTests.swift diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector4iCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector4iCoverTests.swift new file mode 100644 index 000000000..c08911dd4 --- /dev/null +++ b/Tests/SwiftGodotTests/BuiltIn/Vector4iCoverTests.swift @@ -0,0 +1,232 @@ +@testable import SwiftGodot +import SwiftGodotTestability +import XCTest + +extension Vector4i { + static func gen(_ coordinateGen: TinyGen) -> TinyGen { + return TinyGen { rng in + let left = rng.left() + let right = rng.right() + return Vector4i( + x: coordinateGen(left.left()), + y: coordinateGen(left.right()), + z: coordinateGen(right.left()), + w: coordinateGen(right.right()) + ) + } + } + + static let edgy: TinyGen = gen(.edgyInt32s) + static let safe: TinyGen = gen(.safeInt32s) +} + +@available(macOS 14, *) +extension Vector4 { + static func gen(_ coordinateGen: TinyGen) -> TinyGen { + return TinyGen { rng in + let left = rng.left() + let right = rng.right() + return Vector4( + x: coordinateGen(left.left()), + y: coordinateGen(left.right()), + z: coordinateGen(right.left()), + w: coordinateGen(right.right()) + ) + } + } + + static let mixed: TinyGen = gen(.mixedFloats) +} + +@available(macOS 14, *) +final class Vector4iCoverTests: GodotTestCase { + + func testInitFromVector4i() throws { + forAll { + Vector4i.edgy + } checkCover: { + Vector4i(from: $0) + } + } + + func testInitFromVector4() throws { + forAll { + TinyGen.oneOf(gens: [ + Vector4.mixed, + Vector4.gen(TinyGen.edgyInt32s.map { Float($0) }) + ]) + } checkCover: { + Vector4i(from: $0) + } + } + + func testNullaryCovers() throws { + // Methods of the form Vector4i.method(). + + func checkMethod( + _ method: (Vector4i) -> () -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + forAll(filePath: filePath, line: line) { + Vector4i.edgy + } checkCover: { + method($0)() + } + } + + try checkMethod(Vector4i.maxAxisIndex) + try checkMethod(Vector4i.minAxisIndex) + try checkMethod(Vector4i.length) + try checkMethod(Vector4i.lengthSquared) + try checkMethod(Vector4i.sign) + try checkMethod(Vector4i.abs) + } + + func testUnaryCovers_Vector4i() throws { + // Methods of the form Vector4i.method(Vector4i). + + func checkMethod( + _ method: (Vector4i) -> (Vector4i) -> some TestEquatable, + forVectors vectors: TinyGen, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + forAll(filePath: filePath, line: line) { + vectors + vectors + } checkCover: { + method($0)($1) + } + } + + try checkMethod(Vector4i.distanceTo, forVectors: Vector4i.safe) + try checkMethod(Vector4i.distanceSquaredTo, forVectors: Vector4i.safe) + try checkMethod(Vector4i.min(with:), forVectors: Vector4i.edgy) + try checkMethod(Vector4i.max(with:), forVectors: Vector4i.edgy) + } + + func testClamp() throws { + forAll { + Vector4i.edgy + Vector4i.edgy + Vector4i.edgy + } checkCover: { + $0.clamp(min: $1, max: $2) + } + } + + func testClampi() throws { + forAll { + Vector4i.edgy + TinyGen.edgyInt64s + TinyGen.edgyInt64s + } checkCover: { + $0.clampi(min: $1, max: $2) + } + } + + func testUnaryCovers_Int64() { + func checkMethod( + _ method: (Vector4i) -> (Int64) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector4i.edgy + TinyGen.edgyInt64s + } checkCover: { + method($0)($1) + } + } + + checkMethod(Vector4i.snappedi) + checkMethod(Vector4i.mini) + checkMethod(Vector4i.maxi) + } + + func testSubscriptGet() throws { + forAll { + Vector4i.edgy + TinyGen.oneOf(values: Vector4i.Axis.allCases) + } checkCover: { + var v = $0 + return v[$1.rawValue] + } + } + + func testSubscriptSet() throws { + forAll { + Vector4i.edgy + TinyGen.oneOf(values: Vector4i.Axis.allCases) + TinyGen.edgyInt64s + } checkCover: { + var v = $0 + v[$1.rawValue] = $2 + return v + } + } + + func testBinaryOperators_Vector4i_Vector4i() throws { + // Operators of the form Vector4i * Vector4i. + + func checkOperator( + _ op: (Vector4i, Vector4i) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + forAll(filePath: filePath, line: line) { + Vector4i.edgy + Vector4i.edgy + } checkCover: { + op($0, $1) + } + } + + try checkOperator(==) + try checkOperator(!=) + try checkOperator(<) + try checkOperator(<=) + try checkOperator(>) + try checkOperator(>=) + try checkOperator(+) + try checkOperator(-) + try checkOperator(*) + try checkOperator(/) + try checkOperator(%) + } + + func testBinaryOperators_Vector4i_Int64() throws { + // Operators of the form Vector4i * Int64. + + func checkOperator( + _ op: (Vector4i, Int64) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) throws { + forAll(filePath: filePath, line: line) { + Vector4i.edgy + TinyGen.edgyInt64s + } checkCover: { + op($0, $1) + } + } + + try checkOperator(*) + try checkOperator(/) + try checkOperator(%) + } + + func testTimesInt64() throws { + forAll { + Vector4i.edgy + TinyGen.mixedDoubles + } checkCover: { + $0 * $1 + } + } + + func testDividedByInt64() throws { + forAll { + Vector4i.edgy + TinyGen.mixedDoubles + } checkCover: { + $0 / $1 + } + } +} diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector4iTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector4iTests.swift index 9fa0d919a..0ff1ee2ed 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector4iTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector4iTests.swift @@ -11,298 +11,6 @@ import SwiftGodotTestability final class Vector4iTests: GodotTestCase { - static let testInt32s: [Int32] = [ - .min, - -2, - -1, - 0, - 1, - 2, - .max, - ] - - /// Fewer values to reduce combinatorial explosion. - static let testFewerInt32s: [Int32] = [ - -2, - 0, - 2, - ] - - /// Adding or subtracting any two of these won't overflow. - static let testSmallerInt32s: [Int32] = [ - -(Int32.max / 2), - -2, - -1, - 0, - 1, - 2, - Int32.max / 2, - ] - - static let testInt64s: [Int64] = [ - .min, - Int64(Int32.min) - 1, - Int64(Int32.min), - -2, - -1, - 0, - 1, - 2, - Int64(Int32.max), - Int64(Int32.max) + 1, - .max - ] - - static let testDoubles: [Double] = testInt64s.map { Double($0) } + [ - -.infinity, - -1e100, - -0.0, - 1e100, - .infinity, - .nan - ] - - static let testVectors: [Vector4i] = testInt32s.flatMap { w in - testInt32s.flatMap { z in - testInt32s.flatMap { y in - testInt32s.map { x in - Vector4i(x: x, y: y, z: z, w: w) - } - } - } - } - - /// Fewer vectors than `testVectors` for tests where the combinatorial explosion from `testVectors` would be too slow. - static let testFewerVectors: [Vector4i] = testFewerInt32s.flatMap { w in - testFewerInt32s.flatMap { z in - testInt32s.flatMap { y in - testFewerInt32s.map { x in - Vector4i(x: x, y: y, z: z, w: w) - } - } - } - } - - /// Vectors where adding or subtracting any two of them won't overflow. - static let testSmallerVectors: [Vector4i] = testSmallerInt32s.flatMap { w in - testSmallerInt32s.flatMap { z in - testSmallerInt32s.flatMap { y in - testSmallerInt32s.map { x in - Vector4i(x: x, y: y, z: z, w: w) - } - } - } - } - - /// Fewer vectors and they won't overflow. - static let testFewerSmallerVectors: [Vector4i] = testFewerInt32s.flatMap { w in - testFewerInt32s.flatMap { z in - testSmallerInt32s.flatMap { y in - testFewerInt32s.map { x in - Vector4i(x: x, y: y, z: z, w: w) - } - } - } - } - - func testInitFromVector4i() throws { - for v in Self.testVectors { - try checkCover { Vector4i(from: v) } - } - } - - func testInitFromVector4() throws { - for y in Self.testInt32s { - try checkCover { Vector4i(from: Vector4(x: 0, y: Float(y), z: 2, w: 1)) } - } - - for y: Float in [-.infinity, -1e25, -0.0, 1e25, .infinity, .nan] { - try checkCover { Vector4i(from: Vector4(x: 0, y: y, z: 2, w: 1))} - } - } - - func testNullaryCovers() throws { - // Methods of the form Vector4i.method(). - - func checkMethod( - _ method: (Vector4i) -> () -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - try checkCover(filePath: filePath, line: line) { method(v)() } - } - } - - try checkMethod(Vector4i.maxAxisIndex) - try checkMethod(Vector4i.minAxisIndex) - try checkMethod(Vector4i.length) - try checkMethod(Vector4i.lengthSquared) - try checkMethod(Vector4i.sign) - try checkMethod(Vector4i.abs) - } - - func testUnaryCovers_Vector4i() throws { - // Methods of the form Vector4i.method(Vector4i). - - func checkMethod( - _ method: (Vector4i) -> (Vector4i) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for w in Self.testFewerVectors { - try checkCover(filePath: filePath, line: line) { method(v)(w) } - } - } - } - - func checkMethodAvoidingOverflow( - _ method: (Vector4i) -> (Vector4i) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testSmallerVectors { - for u in Self.testFewerSmallerVectors { - try checkCover(filePath: filePath, line: line) { method(v)(u) } - } - } - } - - try checkMethodAvoidingOverflow(Vector4i.distanceTo) - try checkMethodAvoidingOverflow(Vector4i.distanceSquaredTo) - try checkMethod(Vector4i.min(with:)) - try checkMethod(Vector4i.max(with:)) - } - - func testClamp() throws { - for v in Self.testFewerVectors { - for u in Self.testFewerVectors { - for w in Self.testFewerVectors { - try checkCover { v.clamp(min: u, max: w) } - } - } - } - } - - func testClampi() throws { - for v in Self.testFewerVectors { - for i in Self.testInt64s { - for j in Self.testInt64s { - try checkCover { v.clampi(min: i, max: j) } - } - } - } - } - - func testSnappedi() throws { - for v in Self.testVectors { - for i in Self.testInt64s { - try checkCover { v.snappedi(step: i) } - } - } - } - - func testMini() throws { - for v in Self.testVectors { - for i in Self.testInt64s { - try checkCover { v.mini(with: i) } - } - } - } - - func testMaxi() throws { - for v in Self.testVectors { - for i in Self.testInt64s { - try checkCover { v.maxi(with: i) } - } - } - } - - func testSubscriptGet() throws { - for v in Self.testVectors { - for i in Vector4i.Axis.allCases { - try checkCover { - var v = v - return v[i.rawValue] - } - } - } - } - - func testSubscriptSet() throws { - for v in Self.testVectors { - for i in Vector4i.Axis.allCases { - for j in Self.testInt64s { - try checkCover { - var v = v - v[i.rawValue] = j - return v - } - } - } - } - } - - func testBinaryOperators_Vector4i_Vector4i() throws { - // Operators of the form Vector4i * Vector4i. - - func checkOperator( - _ op: (Vector4i, Vector4i) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for u in Self.testFewerVectors { - try checkCover(filePath: filePath, line: line) { op(v, u) } - } - } - } - - try checkOperator(==) - try checkOperator(!=) - try checkOperator(<) - try checkOperator(<=) - try checkOperator(>) - try checkOperator(>=) - try checkOperator(+) - try checkOperator(-) - try checkOperator(*) - try checkOperator(/) - try checkOperator(%) - } - - func testBinaryOperators_Vector4i_Int64() throws { - // Operators of the form Vector4i * Int64. - - func checkOperator( - _ op: (Vector4i, Int64) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for i in Self.testInt64s { - try checkCover(filePath: filePath, line: line) { op(v, i) } - } - } - } - - try checkOperator(*) - try checkOperator(/) - try checkOperator(%) - } - - func testTimesInt64() throws { - for v in Self.testVectors { - for d in Self.testDoubles { - try checkCover { v * d } - } - } - } - - func testDividedByInt64() throws { - for v in Self.testVectors { - for d in Self.testDoubles { - try checkCover { v / d } - } - } - } - func testOperatorUnaryMinus () { var value: Vector4i From 7023c497a3e6d1cd4c34f0aea7601c77d8f8f314 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 3 Dec 2024 19:12:14 -0600 Subject: [PATCH 62/99] fix key construction for init methods taking Float arguments --- Generator/Generator/BuiltinGen.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Generator/Generator/BuiltinGen.swift b/Generator/Generator/BuiltinGen.swift index afec44425..4350c67a8 100644 --- a/Generator/Generator/BuiltinGen.swift +++ b/Generator/Generator/BuiltinGen.swift @@ -116,7 +116,7 @@ func generateBuiltinCtors (_ p: Printer, p ("\(visibility) init (\(args))") { let parameterTypes = m.arguments?.map { arg in - getGodotType(SimpleType (type: arg.type), kind: .builtIn) + getGodotType(SimpleType (type: arg.type), kind: .builtInField) } ?? [] let key = SwiftCovers.Key(type: typeName, name: "init", parameterTypes: parameterTypes, returnType: bc.name) p.useSwiftCoverIfAvailable(for: key) { From d105f73cc55c8fe687a943ed22c37e3bdc8fe638 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Thu, 5 Dec 2024 22:46:18 -0600 Subject: [PATCH 63/99] implement Plane covers --- Sources/SwiftCovers/Plane.covers.swift | 133 +++++++++ Sources/SwiftGodot/SwiftCoverSupport.swift | 6 + .../BuiltIn/PlaneCoverTests.swift | 268 ++++++++++++++++++ 3 files changed, 407 insertions(+) create mode 100644 Sources/SwiftCovers/Plane.covers.swift create mode 100644 Tests/SwiftGodotTests/BuiltIn/PlaneCoverTests.swift diff --git a/Sources/SwiftCovers/Plane.covers.swift b/Sources/SwiftCovers/Plane.covers.swift new file mode 100644 index 000000000..82b19f987 --- /dev/null +++ b/Sources/SwiftCovers/Plane.covers.swift @@ -0,0 +1,133 @@ +@_spi(SwiftCovers) import SwiftGodot + +extension Plane { + + public init(from: Plane) { + self = from + } + + public init(normal: Vector3) { + self.init(normal: normal, d: 0) + } + + public init(normal: Vector3, point: Vector3) { + self.init(normal: normal, d: Float(normal.dot(with: point))) + } + + public init(point1: Vector3, point2: Vector3, point3: Vector3) { + let normal = ((point1 - point3).cross(with: point1 - point2)).normalized(); + self.init(normal: normal, d: Float(normal.dot(with: point1))) + } + + + public init(a: Float, b: Float, c: Float, d: Float) { + self.init(normal: Vector3(x: a, y: b, z: c), d: d) + } + + + public func normalized() -> Plane { + let l = Float(normal.length()) + return l == 0 ? Plane() : Plane( + normal: Vector3(x: normal.x / l, y: normal.y / l, z: normal.z / l), + d: d / l + ) + } + + public func getCenter() -> Vector3 { + return normal * Double(d) + } + + public func isEqualApprox(toPlane: Plane) -> Bool { + return normal.isEqualApprox(to: toPlane.normal) && GD.isEqualApprox(a: Double(d), b: Double(toPlane.d)) + } + + public func isFinite() -> Bool { + return normal.isFinite() && GD.isFinite(x: Double(d)) + } + + public func isPointOver(point: Vector3) -> Bool { + return normal.dot(with: point) > Double(d) + } + + public func distanceTo(point: Vector3) -> Double { + return normal.dot(with: point) - Double(d) + } + + public func hasPoint(_ point: Vector3, tolerance: Double) -> Bool { + let dist = (normal.dot(with: point) - Double(d)).magnitude + return dist <= tolerance + } + + public func project(point: Vector3) -> Vector3 { + return point - normal * distanceTo(point: point) + } + + public func intersect3(b: Plane, c: Plane) -> Variant? { + let a = self + + let normal0 = a.normal + let normal1 = b.normal + let normal2 = c.normal + + let denom = normal0.cross(with: normal1).dot(with: normal2) + + guard !GD.isZeroApprox(x: denom) else { + return nil + } + + let p0 = normal1.cross(with: normal2) * Double(a.d) + let p1 = normal2.cross(with: normal0) * Double(b.d) + let p2 = normal0.cross(with: normal1) * Double(c.d) + return Variant((p0 + p1 + p2) / denom) + } + + public func intersectsRay(from: Vector3, dir: Vector3) -> Variant? { + let den = normal.dot(with: dir) + + guard !GD.isZeroApprox(x: den) else { + return nil + } + + // Godot does this in Float, so I do too. + let dist = (Float(normal.dot(with: from)) - d) / Float(den) + if dist > Float(CMP_EPSILON) { + return nil + } + + return Variant(from - dir * Double(dist)) + } + + public func intersectsSegment(from: Vector3, to: Vector3) -> Variant? { + let segment = from - to + let den = normal.dot(with: segment) + + guard !GD.isZeroApprox(x: den) else { + return nil + } + + // Godot does this in Float, so I do too. + let dist = (Float(normal.dot(with: from)) - d) / Float(den) + if dist < -Float(CMP_EPSILON) || dist > (1.0 + Float(CMP_EPSILON)) { + return nil + } + + return Variant(from - segment * Double(dist)) + } + + public static func == (lhs: Plane, rhs: Plane) -> Bool { + return lhs.tuple == rhs.tuple + } + + public static func != (lhs: Plane, rhs: Plane) -> Bool { + return !(lhs.tuple == rhs.tuple) + } + + public static func * (lhs: Plane, rhs: Transform3D) -> Plane { + let inv = rhs.affineInverse() + let basisTransposed = rhs.basis.transposed() + let point = inv * (lhs.normal * Double(lhs.d)) + let normal = (basisTransposed * lhs.normal).normalized() + let d = normal.dot(with: point) + return Plane(normal: normal, d: Float(d)) + } +} diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index 4dda330b7..49042d8cf 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -58,6 +58,12 @@ extension Vector4i { public var tuple: (Int32, Int32, Int32, Int32) { (x, y, z, w) } } +extension Plane { + @_spi(SwiftCovers) + @inline(__always) + public var tuple: (Vector3, Float) { (normal, d) } +} + @_spi(SwiftCovers) @inline(__always) public func sign(_ x: Float) -> Float { diff --git a/Tests/SwiftGodotTests/BuiltIn/PlaneCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/PlaneCoverTests.swift new file mode 100644 index 000000000..2f3d69413 --- /dev/null +++ b/Tests/SwiftGodotTests/BuiltIn/PlaneCoverTests.swift @@ -0,0 +1,268 @@ +@testable import SwiftGodot +@_spi(SwiftCovers) import SwiftGodot +import SwiftGodotTestability +import XCTest + +@available(macOS 14, *) +extension Vector3 { + static let normalizedGen: TinyGen = TinyGen.build { + TinyGen.gaussianFloats + TinyGen.gaussianFloats + TinyGen.gaussianFloats + }.map { x, y, z in + // https://stackoverflow.com/q/6283080/77567 + let d = (x * x + y * y + z * z).squareRoot() + return Vector3(x: x / d, y: y / d, z: z / d) + } + + static let mixedGen: TinyGen = gen(TinyGen.mixedFloats) + + static let tinyGen: TinyGen = gen(TinyGen.gaussianFloats.map { $0 * 0.0001 }) +} + +@available(macOS 14, *) +extension Plane { + static func gen(normal: TinyGen, d: TinyGen) -> TinyGen { + return TinyGen { rng in + return Plane(normal: normal(rng.left()), d: d(rng.right())) + } + } + + // Vanishingly small chance that the normal is zero. + static let nonZeroGen: TinyGen = gen(normal: Vector3.mixedGen, d: TinyGen.mixedFloats) + + static let maybeZeroGen: TinyGen = TinyGen.biasedOneOf(gens: [ + (99, nonZeroGen), + (1, TinyGen.gaussianFloats.map { Plane(normal: .zero, d: $0) }) + ]) +} + +@available(macOS 14, *) +extension Basis { + static let mostlyRotationGen: TinyGen = TinyGenBuilder { + Vector3.normalizedGen // rotation axis + TinyGen.gaussianDoubles.map { $0 } // rotation angle + TinyGen.gaussianFloats.map { exp($0 * 0.0001) } // x scale + TinyGen.gaussianFloats.map { exp($0 * 0.0001) } // y scale + TinyGen.gaussianFloats.map { exp($0 * 0.0001) } // z scale + }.map { axis, angle, x, y, z in + Basis() + .rotated(axis: axis, angle: angle) + .scaled(scale: Vector3(x: x, y: y, z: z)) + } +} + +@available(macOS 14, *) +extension Transform3D { + static let gaussianGen: TinyGen = TinyGenBuilder { + Basis.mostlyRotationGen + Vector3.mixedGen + }.map { Transform3D(basis: $0, origin: $1) } +} + +@available(macOS 14, *) +final class PlaneCoverTests: GodotTestCase { + + func testInitFromPlane() { + forAll { + Plane.maybeZeroGen + } checkCover: { + Plane(from: $0) + } + } + + func testInitNormal() { + forAll { + Vector3.mixedGen + } checkCover: { + Plane(normal: $0) + } + } + + func testInitNormalPoint() { + forAll { + Vector3.mixedGen + Vector3.mixed + } checkCover: { + Plane(normal: $0, point: $1) + } + } + + func testInitThreePoints() { + forAll { + Vector3.mixed + Vector3.mixed + Vector3.mixed + } checkCover: { + Plane(point1: $0, point2: $1, point3: $2) + } + } + + func testInitFourFloats() { + forAll { + TinyGen.mixedFloats + TinyGen.mixedFloats + TinyGen.mixedFloats + TinyGen.mixedFloats + } checkCover: { + Plane(a: $0, b: $1, c: $2, d: $3) + } + } + + func testNullaryCovers() { + // Methods of the form Plane.method(). + + func checkMethod( + _ method: (Plane) -> () -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Plane.maybeZeroGen + } checkCover: { + method($0)() + } + } + + checkMethod(Plane.normalized) + checkMethod(Plane.getCenter) + checkMethod(Plane.isFinite) + } + + func testIsEqualApprox() { + let perturbationGen = TinyGen.closedUnitRangeDoubles.map { + Float(1.0 + 4.0 * ($0 - 0.5) * CMP_EPSILON) + } + + forAll { + Plane.maybeZeroGen + Vector3.gen(perturbationGen) + perturbationGen // for d + } checkCover: { p0, n, d in + var p1 = p0 + p1.normal *= n + p1.d *= d + return p0.isEqualApprox(toPlane: p1) + } + } + + func testPlaneTimesTransform() { + forAll { + Plane.maybeZeroGen + Transform3D.gaussianGen + } checkCover: { plane, xform in + plane * xform + } + } + + func testIsPointOver() { + forAll { + Plane.maybeZeroGen + Vector3.mixed + } checkCover: { + $0.isPointOver(point: $1) + } + } + + func testDistanceToPoint() { + forAll { + Plane.maybeZeroGen + Vector3.mixed + } checkCover: { + // Godot ncasts to float, so I do too. + Float($0.distanceTo(point: $1)) + } + } + + func testHasPoint() { + forAll { + Plane.nonZeroGen + Vector3.normalizedGen + TinyGen.gaussianDoubles.map { 0.0001 * $0 } // offset + TinyGen.gaussianDoubles.map { (0.0001 * $0).magnitude } // tolerance + } checkCover: { plane, ray, offset, tolerance in + // Find the ray/plane intersection point, and move it along the plane normal so the distance from the point to the plane is offset. + let point = ray * ((offset + Double(plane.d)) / plane.normal.dot(with: ray)) + return plane.hasPoint(point, tolerance: tolerance) + } + } + + func testProject() { + forAll { + Plane.nonZeroGen + Vector3.normalizedGen + TinyGen.gaussianDoubles.map { 100.0 * $0 } + } checkCover: { plane, ray, distance in + let point = ray * distance + return plane.project(point: point) + } + } + + func testIntersect3() { + forAll { + Plane.maybeZeroGen + Plane.maybeZeroGen + Plane.maybeZeroGen + } checkCover: { (a: Plane, b: Plane, c: Plane) in + a.intersect3(b: b, c: c).flatMap { Vector3($0) } + } + } + + func testIntersectsRay() { + forAll { + Plane.maybeZeroGen + Vector3.normalizedGen + Vector3.normalizedGen + } checkCover: { plane, start, heading in + plane.intersectsRay(from: start, dir: heading).flatMap { Vector3($0) } + } + } + + func testIntersectsSegment() { + forAll { + Plane.maybeZeroGen + Vector3.normalizedGen + Vector3.normalizedGen + } checkCover: { plane, start, end in + plane.intersectsSegment(from: start, to: end).flatMap { Vector3($0) } + } + } + + func testEquals() { + forAll { + Plane.maybeZeroGen + Plane.maybeZeroGen + } checkCover: { + $0 == $1 + } + + forAll { + Plane.maybeZeroGen + } checkCover: { + $0 == $0 + } + } + + func testNotEquals() { + forAll { + Plane.maybeZeroGen + Plane.maybeZeroGen + } checkCover: { + $0 != $1 + } + + forAll { + Plane.maybeZeroGen + } checkCover: { + $0 != $0 + } + } + + func testPlaneTimesTransform3D() { + forAll { + Plane.maybeZeroGen + Transform3D.gaussianGen + } checkCover: { + $0 * $1 + } + } +} From f705d6cdf96fc4663e9ee2b3f4efb0367aec8750 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Fri, 6 Dec 2024 22:58:47 -0600 Subject: [PATCH 64/99] add more versions of GD.isEqualApprox --- Sources/SwiftGodot/Extensions/GD+Utils.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Sources/SwiftGodot/Extensions/GD+Utils.swift b/Sources/SwiftGodot/Extensions/GD+Utils.swift index 3c74c3514..e6f41796e 100644 --- a/Sources/SwiftGodot/Extensions/GD+Utils.swift +++ b/Sources/SwiftGodot/Extensions/GD+Utils.swift @@ -99,4 +99,20 @@ extension GD { let finalMessage = transformedItems.joined(separator: separator) GD.printerr(arg1: Variant (finalMessage)) } + + public static func isEqualApprox(_ a: Float, _ b: Float) -> Bool { + // This is imported with Double arguments but we need it with Float arguments. + return isEqualApprox(a, b, tolerance: Float(CMP_EPSILON)) + } + + public static func isEqualApprox(_ a: Float, _ b: Float, tolerance: Float) -> Bool { + // Godot doesn't export this three-argument version of isEqualApprox. + + // Check for exact equality first, required to handle "infinity" values. + if a == b { + return true + } + // Then check for approximate equality. + return (a - b).magnitude < tolerance + } } From 05959af24999501982625652c2e2d40e7cb1ccf7 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Fri, 6 Dec 2024 23:43:28 -0600 Subject: [PATCH 65/99] add Float cubic interpolation helpers --- Sources/SwiftGodot/Extensions/GD+Utils.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Sources/SwiftGodot/Extensions/GD+Utils.swift b/Sources/SwiftGodot/Extensions/GD+Utils.swift index e6f41796e..dbf3588fe 100644 --- a/Sources/SwiftGodot/Extensions/GD+Utils.swift +++ b/Sources/SwiftGodot/Extensions/GD+Utils.swift @@ -115,4 +115,23 @@ extension GD { // Then check for approximate equality. return (a - b).magnitude < tolerance } + + public static func cubicInterpolate(from: Float, to: Float, pre: Float, post: Float, weight: Float) -> Float { + let constTerm = 2 * from + let linearTerm = (-pre + to) * weight + let quadraticTerm = (2 * pre - 5 * from + 4 * to - post) * (weight * weight) + let cubicTerm = (-pre + 3 * from - 3 * to + post) * (weight * weight * weight) + return 0.5 * (constTerm + linearTerm + quadraticTerm + cubicTerm) + } + + public static func cubicInterpolateInTime(from: Float, to: Float, pre: Float, post: Float, weight: Float, toT: Float, preT: Float, postT: Float) -> Float { + /* Barry-Goldman method */ + let t = (0 as Float).lerp(to: toT, weight: weight) + let a1 = pre.lerp(to: from, weight: preT == 0 ? 0 : (t - preT) / -preT) + let a2 = from.lerp(to: to, weight: toT == 0 ? 0.5 : t / toT) + let a3 = to.lerp(to: post, weight: postT - toT == 0 ? 1 : (t - toT) / (postT - toT)) + let b1 = a1.lerp(to: a2, weight: toT - preT == 0 ? 0 : (t - preT) / (toT - preT)) + let b2 = a2.lerp(to: a3, weight: postT == 0 ? 1 : t / postT) + return b1.lerp(to: b2, weight: toT == 0 ? 0.5 : t / toT) + } } From 9eef1efb81c37e5c9d9bc30db265f60bda19452a Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Fri, 6 Dec 2024 23:43:28 -0600 Subject: [PATCH 66/99] add sign(Double) helper --- Sources/SwiftGodot/SwiftCoverSupport.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index 49042d8cf..8786bf327 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -70,6 +70,12 @@ public func sign(_ x: Float) -> Float { return x == 0 ? 0 : (x > 0 ? 1.0 : -1.0) } +@_spi(SwiftCovers) +@inline(__always) +public func sign(_ x: Double) -> Double { + return x == 0 ? 0 : (x > 0 ? 1.0 : -1.0) +} + /// This epsilon should match Godot's `CMP_EPSILON` (which is unfortunately not exported). @_spi(SwiftCovers) public let CMP_EPSILON: Double = 0.00001 From 3a7d2eb10de3cac3fbaea5d8ef5e58811851b400 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Fri, 6 Dec 2024 23:43:28 -0600 Subject: [PATCH 67/99] allow covers for static methods --- Generator/Generator/BuiltinGen.swift | 8 +++++++- Generator/Generator/SwiftCovers.swift | 10 +++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Generator/Generator/BuiltinGen.swift b/Generator/Generator/BuiltinGen.swift index 4350c67a8..1bb10e668 100644 --- a/Generator/Generator/BuiltinGen.swift +++ b/Generator/Generator/BuiltinGen.swift @@ -505,7 +505,13 @@ func generateBuiltinMethods (_ p: Printer, } ?? [] p ("public\(keyword) func \(methodName)(\(args))\(retSig)") { - let key = SwiftCovers.Key(type: typeName, name: methodName, parameterTypes: parameterTypes, returnType: ret) + let key = SwiftCovers.Key( + type: typeName, + name: methodName, + parameterTypes: parameterTypes, + returnType: ret, + isStatic: m.isStatic + ) p.useSwiftCoverIfAvailable(for: key) { generateMethodCall (p, typeName: typeName, methodToCall: ptrName, godotReturnType: m.returnType, isStatic: m.isStatic, isVararg: m.isVararg, arguments: m.arguments ?? []) } diff --git a/Generator/Generator/SwiftCovers.swift b/Generator/Generator/SwiftCovers.swift index 2a6fe896c..24bc6cff4 100644 --- a/Generator/Generator/SwiftCovers.swift +++ b/Generator/Generator/SwiftCovers.swift @@ -18,8 +18,10 @@ struct SwiftCovers { /// The return type. var returnType: String + var isStatic: Bool = false + var description: String { - "\(type).\(name)(\(parameterTypes.joined(separator: ", "))) -> \(returnType)" + "\(isStatic ? "static " : "")\(type).\(name)(\(parameterTypes.joined(separator: ", "))) -> \(returnType)" } } @@ -154,7 +156,8 @@ struct SwiftCovers { private mutating func extractFunctionCover(from function: FunctionDeclSyntax, of type: String) -> Bool { guard - function.modifiers.map({ $0.name.tokenKind }) == [.keyword(.public)], + case let modifiers = Set(function.modifiers.map({ $0.name.tokenKind })), + modifiers == [.keyword(.public)] || modifiers == [.keyword(.public), .keyword(.static)], case .identifier(let name) = function.name.tokenKind, case let signature = function.signature, case let parameterTypes = signature.parameterClause.parameters @@ -169,7 +172,8 @@ struct SwiftCovers { type: type, name: name, parameterTypes: parameterTypes, - returnType: returnType + returnType: returnType, + isStatic: modifiers.contains(.keyword(.static)) ) covers[key] = fixCodeBlockIndentation(body) From e66c31e9ddf4861ab44b1740227727d9a19fd881 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Fri, 6 Dec 2024 23:43:28 -0600 Subject: [PATCH 68/99] implement Quaternion covers --- Sources/SwiftCovers/Quaternion.covers.swift | 396 ++++++++++++++++++ .../SwiftGodot/Extensions/Arithmetic.swift | 7 +- Sources/SwiftGodot/Extensions/GD+Utils.swift | 48 ++- .../Native/LinearInterpolation.swift | 4 + Sources/SwiftGodot/SwiftCoverSupport.swift | 14 + .../BuiltIn/QuaternionCoverTests.swift | 336 +++++++++++++++ 6 files changed, 796 insertions(+), 9 deletions(-) create mode 100644 Sources/SwiftCovers/Quaternion.covers.swift create mode 100644 Tests/SwiftGodotTests/BuiltIn/QuaternionCoverTests.swift diff --git a/Sources/SwiftCovers/Quaternion.covers.swift b/Sources/SwiftCovers/Quaternion.covers.swift new file mode 100644 index 000000000..eee18cc3b --- /dev/null +++ b/Sources/SwiftCovers/Quaternion.covers.swift @@ -0,0 +1,396 @@ +#if canImport(Darwin) +import Darwin +#elseif os(Windows) +import ucrt +import WinSDK +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#else +#error("Unable to identify your C library.") +#endif + +@_spi(SwiftCovers) import SwiftGodot + +extension Quaternion { + + public init(from: Quaternion) { + self = from + } + + public init(from: Basis) { + var m = from + let trace = Float(m[0][0]) + Float(m[1][1]) + Float(m[2][2]) + var v = SIMD4() + if trace > 0 { + var s = (trace + 1.0).squareRoot() + v[3] = s * 0.5 + s = 0.5 / s + v[0] = Float(m[1][2] - m[2][1]) * s + v[1] = Float(m[2][0] - m[0][2]) * s + v[2] = Float(m[0][1] - m[1][0]) * s + } else { + let i: Int64 = m[0][0] < m[1][1] ? (m[1][1] < m[2][2] ? 2 : 1) : (m[0][0] < m[2][2] ? 2 : 0) + let j: Int64 = (i + 1) % 3 + let k: Int64 = (i + 2) % 3 + + var s = (Float(m[i][i]) - Float(m[j][j]) - Float(m[k][k]) + 1.0).squareRoot() + v[Int(truncatingIfNeeded: i)] = s * 0.5 + s = 0.5 / s + + v[3] = (Float(m[j][k]) - Float(m[k][j])) * s + v[Int(truncatingIfNeeded: j)] = (Float(m[i][j]) + Float(m[j][i])) * s + v[Int(truncatingIfNeeded: k)] = (Float(m[i][k]) + Float(m[k][i])) * s + } + self.init(x: v[0], y: v[1], z: v[2], w: v[3]) + } + + public init(axis: Vector3, angle: Float) { + let d = Float(axis.length()) + if d == 0 { + self.init(x: 0, y: 0, z: 0, w: 0) + } else { + let sinAngle = sin(angle * 0.5) + let cosAngle = cos(angle * 0.5) + let s = sinAngle / d + self.init(x: axis.x * s, y: axis.y * s, z: axis.z * s, w: cosAngle) + } + } + + public init(arcFrom: Vector3, arcTo: Vector3) { + let c = arcFrom.cross(with: arcTo) + let d = Float(arcFrom.dot(with: arcTo)) + + if d < -1.0 + Float(CMP_EPSILON) { + self.init(x: 0, y: 1, z: 0, w: 0) + } else { + let s = ((1.0 + d) * 2.0).squareRoot() + let rs = 1 / s + self.init(x: c.x * rs, y: c.y * rs, z: c.z * rs, w: s * 0.5) + } + } + + public func length() -> Double { + return Double(Float(lengthSquared()).squareRoot()) + } + + public func lengthSquared() -> Double { + return self.dot(with: self) + } + + public func normalized() -> Quaternion { + return self / length() + } + + public func isNormalized() -> Bool { + return GD.isEqualApprox(Float(lengthSquared()), 1, tolerance: Float(UNIT_EPSILON)) + } + + public func isEqualApprox(to: Quaternion) -> Bool { + return GD.isEqualApprox(x, to.x) && GD.isEqualApprox(y, to.y) && GD.isEqualApprox(z, to.z) && GD.isEqualApprox(w, to.w) + } + + public func isFinite() -> Bool { + return x.isFinite && y.isFinite && z.isFinite && w.isFinite + } + + public func inverse() -> Quaternion { + return Quaternion(x: -x, y: -y, z: -z, w: w) + } + + public func log() -> Quaternion { + let v = getAxis() * getAngle() + return Quaternion(x: v.x, y: v.y, z: v.z, w: 0) + } + + public func exp() -> Quaternion { + var v = Vector3(x: x, y: y, z: z) + let theta = Float(v.length()) + v = v.normalized() + if theta < Float(CMP_EPSILON) || !v.isNormalized() { + return Quaternion(x: 0, y: 0, z: 0, w: 1) + } else { + return Quaternion(axis: v, angle: theta) + } + } + + public func angleTo(_ to: Quaternion) -> Double { + let d = Float(self.dot(with: to)) + let u = d * d * 2 - 1 + return Double(GD.acosf(u)) + } + + public func dot(with: Quaternion) -> Double { + return Double((self.simd * with.simd).sum()) + } + + public func slerp(to: Quaternion, weight: Double) -> Quaternion { + // calc cosine + var cosom = Float(self.dot(with: to)) + let to1: Quaternion + // adjust signs (if necessary) + if cosom < 0 { + cosom = -cosom + to1 = -to + } else { + to1 = to + } + + let scale0, scale1: Float + let weight = Float(weight) + + // calculate coefficients + if 1 - cosom > Float(CMP_EPSILON) { + // standard case (slerp) + let omega = GD.acosf(cosom) + let sinom = sinf(omega) + scale0 = Float(sin((1 - Double(weight)) * Double(omega)) / Double(sinom)) + scale1 = sinf(weight * omega) / sinom + } else { + // "from" and "to" quaternions are very close + // ... so we can do a linear interpolation + scale0 = 1 - weight + scale1 = weight + } + + // calculate final values + return Quaternion( + x: scale0 * x + scale1 * to1.x, + y: scale0 * y + scale1 * to1.y, + z: scale0 * z + scale1 * to1.z, + w: scale0 * w + scale1 * to1.w + ) + } + + public func slerpni(to: Quaternion, weight: Double) -> Quaternion { + let dot = Float(self.dot(with: to)) + + if dot.magnitude > 0.9999 { + return self + } + + let weight = Float(weight) + let theta = GD.acosf(dot) + let sinT = 1 / sinf(theta) + let newFactor = sinf(weight * theta) * sinT + let invFactor = sinf((1 - weight) * theta) * sinT + + return Quaternion( + x: invFactor * self.x + newFactor * to.x, + y: invFactor * self.y + newFactor * to.y, + z: invFactor * self.z + newFactor * to.z, + w: invFactor * self.w + newFactor * to.w + ) + } + + public func sphericalCubicInterpolate(b: Quaternion, preA: Quaternion, postB: Quaternion, weight: Double) -> Quaternion { + // Align flip phases. + let qFrom = Basis(from: self).getRotationQuaternion() + + var qPre = Basis(from: preA).getRotationQuaternion() + var qTo = Basis(from: b).getRotationQuaternion() + var qPost = Basis(from: postB).getRotationQuaternion() + + // Flip quaternions to shortest path if necessary. + let flip1 = qFrom.dot(with: qPre).sign == .minus + if flip1 { qPre = -qPre } + let flip2 = qFrom.dot(with: qTo).sign == .minus + if flip2 { qTo = -qTo } + let flip3 = flip2 ? qTo.dot(with: qPost) <= 0 : qTo.dot(with: qPost).sign == .minus + if flip3 { qPost = -qPost } + + let fWeight = Float(weight) + + // Calc by Expmap in from_q space. + let q1: Quaternion + do { + let lnFrom = Quaternion(x: 0, y: 0, z: 0, w: 0) + let qFromInverse = qFrom.inverse() + let lnTo = (qFromInverse * qTo).log() + let lnPre = (qFromInverse * qPre).log() + let lnPost = (qFromInverse * qPost).log() + let ln = Quaternion( + x:GD.cubicInterpolate(from: lnFrom.x, to: lnTo.x, pre: lnPre.x, post: lnPost.x, weight: fWeight), + y:GD.cubicInterpolate(from: lnFrom.y, to: lnTo.y, pre: lnPre.y, post: lnPost.y, weight: fWeight), + z:GD.cubicInterpolate(from: lnFrom.z, to: lnTo.z, pre: lnPre.z, post: lnPost.z, weight: fWeight), + w: 0 + ) + q1 = qFrom * ln.exp() + } + + // Calc by Expmap in to_q space. + let q2: Quaternion + do { + let qToInverse = qTo.inverse() + let lnFrom = (qToInverse * qFrom).log() + let lnTo = Quaternion(x: 0, y: 0, z: 0, w: 0) + let lnPre = (qToInverse * qPre).log() + let lnPost = (qToInverse * qPost).log() + let ln = Quaternion( + x: GD.cubicInterpolate(from: lnFrom.x, to: lnTo.x, pre: lnPre.x, post: lnPost.x, weight: fWeight), + y: GD.cubicInterpolate(from: lnFrom.y, to: lnTo.y, pre: lnPre.y, post: lnPost.y, weight: fWeight), + z: GD.cubicInterpolate(from: lnFrom.z, to: lnTo.z, pre: lnPre.z, post: lnPost.z, weight: fWeight), + w: 0 + ) + q2 = qTo * ln.exp() + } + + return q1.slerp(to: q2, weight: weight) + } + + public func sphericalCubicInterpolateInTime(b: Quaternion, preA: Quaternion, postB: Quaternion, weight: Double, bT: Double, preAT: Double, postBT: Double) -> Quaternion { + // Align flip phases. + let qFrom = Basis(from: self).getRotationQuaternion() + var qPre = Basis(from: preA).getRotationQuaternion() + var qTo = Basis(from: b).getRotationQuaternion() + var qPost = Basis(from: postB).getRotationQuaternion() + + // Flip quaternions to shortest path if necessary. + let flip1 = qFrom.dot(with: qPre).sign == .minus + if flip1 { qPre = -qPre } + let flip2 = qFrom.dot(with: qTo).sign == .minus + if flip2 { qTo = -qTo } + let flip3 = flip2 ? qTo.dot(with: qPost) <= 0 : qTo.dot(with: qPost).sign == .minus + if flip3 { qPost = -qPost } + + let fWeight = Float(weight) + let toT = Float(bT) + let preT = Float(preAT) + let postT = Float(postBT) + + // Calc by Expmap in from_q space. + let q1: Quaternion + do { + let lnFrom = Quaternion(x: 0, y: 0, z: 0, w: 0) + let qFromInverse = qFrom.inverse() + let lnTo = (qFromInverse * qTo).log() + let lnPre = (qFromInverse * qPre).log() + let lnPost = (qFromInverse * qPost).log() + let ln = Quaternion( + x: GD.cubicInterpolateInTime(from: lnFrom.x, to: lnTo.x, pre: lnPre.x, post: lnPost.x, weight: fWeight, toT: toT, preT: preT, postT: postT), + y: GD.cubicInterpolateInTime(from: lnFrom.y, to: lnTo.y, pre: lnPre.y, post: lnPost.y, weight: fWeight, toT: toT, preT: preT, postT: postT), + z: GD.cubicInterpolateInTime(from: lnFrom.z, to: lnTo.z, pre: lnPre.z, post: lnPost.z, weight: fWeight, toT: toT, preT: preT, postT: postT), + w: 0 + ) + q1 = qFrom * ln.exp() + } + + // Calc by Expmap in to_q space. + let q2: Quaternion + do { + let qToInverse = qTo.inverse() + let lnFrom = (qToInverse * qFrom).log() + let lnTo = Quaternion(x: 0, y: 0, z: 0, w: 0) + let lnPre = (qToInverse * qPre).log() + let lnPost = (qToInverse * qPost).log() + let ln = Quaternion( + x: GD.cubicInterpolateInTime(from: lnFrom.x, to: lnTo.x, pre: lnPre.x, post: lnPost.x, weight: fWeight, toT: toT, preT: preT, postT: postT), + y: GD.cubicInterpolateInTime(from: lnFrom.y, to: lnTo.y, pre: lnPre.y, post: lnPost.y, weight: fWeight, toT: toT, preT: preT, postT: postT), + z: GD.cubicInterpolateInTime(from: lnFrom.z, to: lnTo.z, pre: lnPre.z, post: lnPost.z, weight: fWeight, toT: toT, preT: preT, postT: postT), + w: 0 + ) + q2 = qTo * ln.exp() + } + + return q1.slerp(to: q2, weight: weight) + } + + public func getEuler(order: Int64 = 2) -> Vector3 { + return Basis(from: self).getEuler(order: order) + } + + public static func fromEuler(_ euler: Vector3) -> Quaternion { + // R = Y(a1).X(a2).Z(a3) convention for Euler angles. + // Conversion to quaternion as listed in https://ntrs.nasa.gov/archive/nasa/casi.ntrs.nasa.gov/19770024290.pdf (page A-6) + // a3 is the angle of the first rotation, following the notation in this reference. + + let half_a1 = euler.y * 0.5 + let half_a2 = euler.x * 0.5 + let half_a3 = euler.z * 0.5 + + let cos_a1 = cosf(half_a1) + let sin_a1 = sinf(half_a1) + let cos_a2 = cosf(half_a2) + let sin_a2 = sinf(half_a2) + let cos_a3 = cosf(half_a3) + let sin_a3 = sinf(half_a3) + + return Quaternion( + x: sin_a1 * cos_a2 * sin_a3 + cos_a1 * sin_a2 * cos_a3, + y: sin_a1 * cos_a2 * cos_a3 - cos_a1 * sin_a2 * sin_a3, + z: -sin_a1 * sin_a2 * cos_a3 + cos_a1 * cos_a2 * sin_a3, + w: sin_a1 * sin_a2 * sin_a3 + cos_a1 * cos_a2 * cos_a3 + ) + } + + public func getAxis() -> Vector3 { + if w.magnitude > 1 - Float(CMP_EPSILON) { + return Vector3(x: x, y: y, z: z) + } + + let r = 1 / (1 - w * w).squareRoot() + return Vector3(x: x * r, y: y * r, z: z * r) + } + + public func getAngle() -> Double { + return Double(2 * GD.acosf(w)) + } + + public subscript(index: Int64) -> Double { + mutating get { + return Double(simd[Int(index)]) + } + set { + var simd = simd + simd[Int(index)] = Float(newValue) + (x, y, z, w) = (simd.x, simd.y, simd.z, simd.w) + } + } + + public static func * (lhs: Quaternion, rhs: Int64) -> Quaternion { + return lhs * Float(rhs) + } + + public static func / (lhs: Quaternion, rhs: Int64) -> Quaternion { + return lhs * (1 / Float(rhs)) + } + + public static func * (lhs: Quaternion, rhs: Double) -> Quaternion { + return lhs * Float(rhs) + } + + public static func / (lhs: Quaternion, rhs: Double) -> Quaternion { + return lhs * (1 / Float(rhs)) + } + + public static func * (lhs: Quaternion, rhs: Vector3) -> Vector3 { + let u = Vector3(x: lhs.x, y: lhs.y, z: lhs.z) + let uv = u.cross(with: rhs) + return rhs + ((uv * Double(lhs.w)) + u.cross(with: uv)) * 2 + } + + public static func == (lhs: Quaternion, rhs: Quaternion) -> Bool { + return lhs.tuple == rhs.tuple + } + + public static func != (lhs: Quaternion, rhs: Quaternion) -> Bool { + return !(lhs.tuple == rhs.tuple) + } + + public static func + (lhs: Quaternion, rhs: Quaternion) -> Quaternion { + return Quaternion(x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z, w: lhs.w + rhs.w) + } + + public static func - (lhs: Quaternion, rhs: Quaternion) -> Quaternion { + return Quaternion(x: lhs.x - rhs.x, y: lhs.y - rhs.y, z: lhs.z - rhs.z, w: lhs.w - rhs.w) + } + + public static func * (lhs: Quaternion, rhs: Quaternion) -> Quaternion { + let xx = lhs.w * rhs.x + lhs.x * rhs.w + lhs.y * rhs.z - lhs.z * rhs.y + let yy = lhs.w * rhs.y + lhs.y * rhs.w + lhs.z * rhs.x - lhs.x * rhs.z + let zz = lhs.w * rhs.z + lhs.z * rhs.w + lhs.x * rhs.y - lhs.y * rhs.x + let ww = lhs.w * rhs.w - lhs.x * rhs.x - lhs.y * rhs.y - lhs.z * rhs.z + return Quaternion(x: xx, y: yy, z: zz, w: ww) + } +} diff --git a/Sources/SwiftGodot/Extensions/Arithmetic.swift b/Sources/SwiftGodot/Extensions/Arithmetic.swift index dccdf2cb2..32eeb2837 100644 --- a/Sources/SwiftGodot/Extensions/Arithmetic.swift +++ b/Sources/SwiftGodot/Extensions/Arithmetic.swift @@ -235,7 +235,12 @@ public extension Quaternion { static prefix func - (_ q: Self) -> Self { return Self (x: -q.x, y: -q.y, z: -q.z, w: -q.w) } - + + /// Multiplies each of my components by `rhs`. + static func * (lhs: Self, rhs: Float) -> Self { + return Quaternion(x: lhs.x * rhs, y: lhs.y * rhs, z: lhs.z * rhs, w: lhs.w * rhs) + } + } public extension Color { diff --git a/Sources/SwiftGodot/Extensions/GD+Utils.swift b/Sources/SwiftGodot/Extensions/GD+Utils.swift index dbf3588fe..437673af5 100644 --- a/Sources/SwiftGodot/Extensions/GD+Utils.swift +++ b/Sources/SwiftGodot/Extensions/GD+Utils.swift @@ -5,6 +5,23 @@ // Created by Marquis Kurt on 5/16/23. // +#if CUSTOM_BUILTIN_IMPLEMENTATIONS +#if canImport(Darwin) +import Darwin +#elseif os(Windows) +import ucrt +import WinSDK +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#else +#error("Unable to identify your C library.") +#endif +#endif + +private func system_acosf(_ x: Float) -> Float { acosf(x) } + extension GD { /// Loads a resource from the filesystem located at `path`. /// @@ -102,7 +119,18 @@ extension GD { public static func isEqualApprox(_ a: Float, _ b: Float) -> Bool { // This is imported with Double arguments but we need it with Float arguments. - return isEqualApprox(a, b, tolerance: Float(CMP_EPSILON)) + + // Check for exact equality first, required to handle "infinity" values. + if a == b { + return true + } + + // Then check for approximate equality. + var tolerance = Float(CMP_EPSILON) * a.magnitude + if tolerance < Float(CMP_EPSILON) { + tolerance = Float(CMP_EPSILON) + } + return (a - b).magnitude < tolerance } public static func isEqualApprox(_ a: Float, _ b: Float, tolerance: Float) -> Bool { @@ -126,12 +154,16 @@ extension GD { public static func cubicInterpolateInTime(from: Float, to: Float, pre: Float, post: Float, weight: Float, toT: Float, preT: Float, postT: Float) -> Float { /* Barry-Goldman method */ - let t = (0 as Float).lerp(to: toT, weight: weight) - let a1 = pre.lerp(to: from, weight: preT == 0 ? 0 : (t - preT) / -preT) - let a2 = from.lerp(to: to, weight: toT == 0 ? 0.5 : t / toT) - let a3 = to.lerp(to: post, weight: postT - toT == 0 ? 1 : (t - toT) / (postT - toT)) - let b1 = a1.lerp(to: a2, weight: toT - preT == 0 ? 0 : (t - preT) / (toT - preT)) - let b2 = a2.lerp(to: a3, weight: postT == 0 ? 1 : t / postT) - return b1.lerp(to: b2, weight: toT == 0 ? 0.5 : t / toT) + let t = (0 as Float).lerp(to: toT, withoutClampingWeight: weight) + let a1 = pre.lerp(to: from, withoutClampingWeight: preT == 0 ? 0 : (t - preT) / -preT) + let a2 = from.lerp(to: to, withoutClampingWeight: toT == 0 ? 0.5 : t / toT) + let a3 = to.lerp(to: post, withoutClampingWeight: postT - toT == 0 ? 1 : (t - toT) / (postT - toT)) + let b1 = a1.lerp(to: a2, withoutClampingWeight: toT - preT == 0 ? 0 : (t - preT) / (toT - preT)) + let b2 = a2.lerp(to: a3, withoutClampingWeight: postT == 0 ? 1 : t / postT) + return b1.lerp(to: b2, withoutClampingWeight: toT == 0 ? 0.5 : t / toT) + } + + public static func acosf(_ x: Float) -> Float { + return x < -1 ? Float(Double.pi) : x > 1 ? 0 : system_acosf(x) } } diff --git a/Sources/SwiftGodot/Native/LinearInterpolation.swift b/Sources/SwiftGodot/Native/LinearInterpolation.swift index 9a192ce02..07e4dac71 100644 --- a/Sources/SwiftGodot/Native/LinearInterpolation.swift +++ b/Sources/SwiftGodot/Native/LinearInterpolation.swift @@ -51,6 +51,10 @@ extension Float: LinearInterpolation { let clampedWeight = max(0.0, min(1.0, Float(weight))) return self + (to - self) * clampedWeight } + + public func lerp(to: Float, withoutClampingWeight weight: any BinaryFloatingPoint) -> Float { + return self + (to - self) * Float(weight) + } } extension Vector2: LinearInterpolation { diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index 8786bf327..14b35b24f 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -64,6 +64,16 @@ extension Plane { public var tuple: (Vector3, Float) { (normal, d) } } +extension Quaternion { + @_spi(SwiftCovers) + @inline(__always) + public var tuple: (Float, Float, Float, Float) { (x, y, z, w) } + + @_spi(SwiftCovers) + @inline(__always) + public var simd: SIMD4 { SIMD4(x, y, z, w) } +} + @_spi(SwiftCovers) @inline(__always) public func sign(_ x: Float) -> Float { @@ -80,6 +90,10 @@ public func sign(_ x: Double) -> Double { @_spi(SwiftCovers) public let CMP_EPSILON: Double = 0.00001 +/// This epsilon should match Godot's `UNIT_EPSILON` (which is unfortunately not exported). +@_spi(SwiftCovers) +public let UNIT_EPSILON: Double = 0.001 + #if TESTABLE_SWIFT_COVERS /// If true (the default), use Swift cover implementations where available instead of calling Godot engine functions. You should only use this for testing covers. It is not intended for production use. diff --git a/Tests/SwiftGodotTests/BuiltIn/QuaternionCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/QuaternionCoverTests.swift new file mode 100644 index 000000000..a0fe0f000 --- /dev/null +++ b/Tests/SwiftGodotTests/BuiltIn/QuaternionCoverTests.swift @@ -0,0 +1,336 @@ +@testable import SwiftGodot +@_spi(SwiftCovers) import SwiftGodot +import SwiftGodotTestability +import XCTest + +@available(macOS 14, *) +extension Quaternion { + static func gen(_ coordinateGen: TinyGen) -> TinyGen { + return TinyGenBuilder { + coordinateGen + coordinateGen + coordinateGen + coordinateGen + }.map { Quaternion(x: $0, y: $1, z: $2, w: $3) } + } + + static let mixedGen = gen(.mixedFloats) + + static let normalizedGen = TinyGenBuilder { + Vector3.normalizedGen + TinyGen.gaussianFloats + }.map { Quaternion(axis: $0, angle: $1).normalized() } +} + +@available(macOS 14, *) +final class QuaternionCoverTests: GodotTestCase { + + let weightGen = TinyGen.closedUnitRangeDoubles + .map { $0 * 1.2 - 0.1 } + let extendedWeightGen = TinyGen.closedUnitRangeDoubles + .map { $0 * 3 - 1 } + + func testInitFromBasis() { + forAll { + Basis.mostlyRotationGen + } checkCover: { + Quaternion(from: $0.orthonormalized()) + } + } + + func testInitFromAxisAndAngle() { + forAll { + Vector3.normalizedGen + TinyGen.mixedFloats + } checkCover: { + Quaternion(axis: $0, angle: $1) + } + } + + func testInitFromArc() { + forAll { + Vector3.normalizedGen + Vector3.normalizedGen + } checkCover: { + Quaternion(arcFrom: $0, arcTo: $1) + } + } + + func testNullaryCovers() { + // Methods of the form quaternion.method(). + + func checkMethod( + _ method: (Quaternion) -> () -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + TinyGen.oneOf(gens: [ + Quaternion.mixedGen, + Quaternion.normalizedGen, + ]) + } checkCover: { + method($0)() + } + } + + checkMethod(Quaternion.length) + checkMethod(Quaternion.lengthSquared) + checkMethod(Quaternion.normalized) + checkMethod(Quaternion.isFinite) + checkMethod(Quaternion.getAxis) + checkMethod(Quaternion.getAngle) + checkMethod(Quaternion.log) + checkMethod(Quaternion.exp) + } + + func testIsNormalized() { + let perturbation = TinyGen.gaussianDoubles + .map { Float(exp(0.001 * $0)) } + + forAll { + TinyGen.oneOf(gens: [ + // Some arbitrary values including weird values. + Quaternion.mixedGen, + + // Some definitely normalized values. + Quaternion.normalizedGen, + + // Some normalized values with slight tweaking that might be enough to make them seem denormalized. + TinyGenBuilder { + Quaternion.normalizedGen + perturbation + perturbation + perturbation + perturbation + }.map { q, dx, dy, dz, dw in + Quaternion(x: q.x * dx, y: q.y * dy, z: q.z * dz, w: q.w * dw) + } + ]) + } checkCover: { + $0.isNormalized() + } + } + + func testIsEqualApprox() { + let perturbation = TinyGen.gaussianDoubles + .map { Float(exp(0.000007 * $0)) } + + forAll { + Quaternion.mixedGen + perturbation + perturbation + perturbation + perturbation + } checkCover: { q1, dx, dy, dz, dw in + let q2 = Quaternion(x: q1.x * dx, y: q1.y * dy, z: q1.z * dz, w: q1.w * dw) + return q1.isEqualApprox(to: q2) + } + } + + func testInverse() { + forAll { + Quaternion.normalizedGen + } checkCover: { + $0.inverse() + } + } + + func testAngleTo() { + forAll { + Quaternion.mixedGen + Quaternion.mixedGen + } checkCover: { + $0.angleTo($1) + } + } + + func testDot() { + forAll { + Quaternion.mixedGen + Quaternion.mixedGen + } checkCover: { + $0.dot(with: $1) + } + } + + func testSlerp() { + forAll { + Quaternion.normalizedGen + Quaternion.normalizedGen + weightGen + } checkCover: { + $0.slerp(to: $1, weight: $2) + } + } + + func testSlerpni() { + forAll { + Quaternion.normalizedGen + Quaternion.normalizedGen + weightGen + } checkCover: { + $0.slerpni(to: $1, weight: $2) + } + } + + func testSphericalCubicInterpolate() { + forAll { + Quaternion.normalizedGen + Quaternion.normalizedGen + Quaternion.normalizedGen + Quaternion.normalizedGen + weightGen + } checkCover: { + $0.sphericalCubicInterpolate(b: $1, preA: $2, postB: $3, weight: $4) + } + } + + func testSphericalCubicInterpolateInTime() { + forAll { + Quaternion.normalizedGen + Quaternion.normalizedGen + Quaternion.normalizedGen + Quaternion.normalizedGen + weightGen + extendedWeightGen + extendedWeightGen + extendedWeightGen + } checkCover: { + $0.sphericalCubicInterpolateInTime(b: $1, preA: $2, postB: $3, weight: $4, bT: $5, preAT: $6, postBT: $7) + } + } + + func testGetEuler() { + forAll { + Quaternion.normalizedGen + TinyGen.oneOf(values: EulerOrder.allCases) + } checkCover: { + $0.getEuler(order: $1.rawValue) + } + } + + func testFromEuler() { + forAll { + Vector3.mixedGen + } checkCover: { + Quaternion.fromEuler($0) + } + } + + func testSubscriptGet() { + forAll { + Quaternion.mixedGen + TinyGen.oneOf(values: Array(0 ... 3)) + } checkCover: { q, axis in + var q = q + return q[axis] + } + } + + func testSubscriptSet() { + forAll { + Quaternion.mixedGen + TinyGen.oneOf(values: Array(0 ... 3)) + TinyGen.mixedDoubles + } checkCover: { q, axis, newValue in + var q = q + q[axis] = newValue + return q + } + } + + func testTimesInt64() { + forAll { + Quaternion.mixedGen + TinyGen.edgyInt64s + } checkCover: { + $0 * $1 + } + } + + func testDividedByInt64() { + forAll { + Quaternion.mixedGen + TinyGen.edgyInt64s + } checkCover: { + $0 / $1 + } + } + + func testTimesDouble() { + forAll { + Quaternion.mixedGen + TinyGen.mixedDoubles + } checkCover: { + $0 * $1 + } + } + + func testDividedByDouble() { + forAll { + Quaternion.mixedGen + TinyGen.mixedDoubles + } checkCover: { + $0 / $1 + } + } + + func testTimesVector3() { + forAll { + Quaternion.normalizedGen + Vector3.mixedGen + } checkCover: { + $0 * $1 + } + } + + func testEquality() { + forAll { + TinyGen.oneOf(gens: [ + // Same value twice so they are equal. + Quaternion.mixedGen.map { ($0, $0) }, + TinyGenBuilder { + Quaternion.mixedGen + Quaternion.mixedGen + } + ]) + } checkCover: { + $0 == $1 + } + } + + func testInequality() { + forAll { + TinyGen.oneOf(gens: [ + // Same value twice so they are equal. + Quaternion.mixedGen.map { ($0, $0) }, + TinyGenBuilder { + Quaternion.mixedGen + Quaternion.mixedGen + } + ]) + } checkCover: { + $0 != $1 + } + } + + func testBinaryOperators() { + // Operators of the form q1 ~ q2. + + func checkOperator( + _ op: (Quaternion, Quaternion) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Quaternion.mixedGen + Quaternion.mixedGen + } checkCover: { + op($0, $1) + } + } + + checkOperator(+) + checkOperator(-) + checkOperator(*) + } +} From c045d90bb2b9dcb36968c391018a9a3c684e0db0 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Sun, 8 Dec 2024 17:37:17 -0600 Subject: [PATCH 69/99] fix SipHash_2_4 when buffer is nearly full --- Sources/SwiftGodotTestability/TinyGen.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftGodotTestability/TinyGen.swift b/Sources/SwiftGodotTestability/TinyGen.swift index 4da6735e2..62ce5689c 100644 --- a/Sources/SwiftGodotTestability/TinyGen.swift +++ b/Sources/SwiftGodotTestability/TinyGen.swift @@ -395,7 +395,7 @@ private struct SipHash_2_4 { private mutating func finalize() -> UInt64 { // If buffer has less than 8 bits free, I need to start a new buffer to finalize, because I need to put the bit count mod 256 into the high byte of the buffer. - if totalBits & 63 < 8 { + if totalBits & 63 > 64 - 8 { compressBuffer() } From 53363f4712604b9d27af77caa8648e4a7a97bab1 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Sun, 8 Dec 2024 17:37:17 -0600 Subject: [PATCH 70/99] fix QuaternionCoverTests for optimized libgodot --- .../BuiltIn/QuaternionCoverTests.swift | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/Tests/SwiftGodotTests/BuiltIn/QuaternionCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/QuaternionCoverTests.swift index a0fe0f000..384c928b6 100644 --- a/Tests/SwiftGodotTests/BuiltIn/QuaternionCoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/QuaternionCoverTests.swift @@ -39,11 +39,13 @@ final class QuaternionCoverTests: GodotTestCase { } func testInitFromAxisAndAngle() { - forAll { - Vector3.normalizedGen - TinyGen.mixedFloats - } checkCover: { - Quaternion(axis: $0, angle: $1) + Float.$closeEnoughUlps.withValue(2) { + forAll { + Vector3.normalizedGen + TinyGen.mixedFloats + } checkCover: { + Quaternion(axis: $0, angle: $1) + } } } @@ -80,7 +82,10 @@ final class QuaternionCoverTests: GodotTestCase { checkMethod(Quaternion.getAxis) checkMethod(Quaternion.getAngle) checkMethod(Quaternion.log) - checkMethod(Quaternion.exp) + + Float.$closeEnoughUlps.withValue(2) { + checkMethod(Quaternion.exp) + } } func testIsNormalized() { @@ -174,29 +179,33 @@ final class QuaternionCoverTests: GodotTestCase { } func testSphericalCubicInterpolate() { - forAll { - Quaternion.normalizedGen - Quaternion.normalizedGen - Quaternion.normalizedGen - Quaternion.normalizedGen - weightGen - } checkCover: { - $0.sphericalCubicInterpolate(b: $1, preA: $2, postB: $3, weight: $4) + Float.$closeEnoughUlps.withValue(21) { + forAll { + Quaternion.normalizedGen + Quaternion.normalizedGen + Quaternion.normalizedGen + Quaternion.normalizedGen + weightGen + } checkCover: { + $0.sphericalCubicInterpolate(b: $1, preA: $2, postB: $3, weight: $4) + } } } func testSphericalCubicInterpolateInTime() { - forAll { - Quaternion.normalizedGen - Quaternion.normalizedGen - Quaternion.normalizedGen - Quaternion.normalizedGen - weightGen - extendedWeightGen - extendedWeightGen - extendedWeightGen - } checkCover: { - $0.sphericalCubicInterpolateInTime(b: $1, preA: $2, postB: $3, weight: $4, bT: $5, preAT: $6, postBT: $7) + Float.$closeEnoughUlps.withValue(14) { + forAll { + Quaternion.normalizedGen + Quaternion.normalizedGen + Quaternion.normalizedGen + Quaternion.normalizedGen + weightGen + extendedWeightGen + extendedWeightGen + extendedWeightGen + } checkCover: { + $0.sphericalCubicInterpolateInTime(b: $1, preA: $2, postB: $3, weight: $4, bT: $5, preAT: $6, postBT: $7) + } } } @@ -210,10 +219,12 @@ final class QuaternionCoverTests: GodotTestCase { } func testFromEuler() { - forAll { - Vector3.mixedGen - } checkCover: { - Quaternion.fromEuler($0) + Float.$closeEnoughUlps.withValue(512) { + forAll { + Vector3.mixedGen + } checkCover: { + Quaternion.fromEuler($0) + } } } From 4feb3ff25bd411f376463dcf8a8352949d365697 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Tue, 10 Dec 2024 15:11:26 -0600 Subject: [PATCH 71/99] Redo Vector2 Cover tests for TinyGen --- Sources/SwiftCovers/Vector2.covers.swift | 8 + .../BuiltIn/Vector2Tests.swift | 203 ----------------- Vector2CoverTests.swift | 210 ++++++++++++++++++ 3 files changed, 218 insertions(+), 203 deletions(-) create mode 100644 Vector2CoverTests.swift diff --git a/Sources/SwiftCovers/Vector2.covers.swift b/Sources/SwiftCovers/Vector2.covers.swift index 9d0795930..53fa34bf9 100644 --- a/Sources/SwiftCovers/Vector2.covers.swift +++ b/Sources/SwiftCovers/Vector2.covers.swift @@ -22,6 +22,14 @@ import Musl extension Vector2 { + public init(from: Vector2) { + self = from + } + +// public init(from: Vector2i) { +// +// } + public func angle() -> Double { return Double(atan2(x, y)) } diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector2Tests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector2Tests.swift index e17a8f5c2..b36a87a12 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector2Tests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector2Tests.swift @@ -10,209 +10,6 @@ import SwiftGodotTestability @testable import SwiftGodot final class Vector2Tests: GodotTestCase { - - static let testInt64s: [Int64] = [ - .min, - Int64(Int32.min) - 1, - Int64(Int32.min), - -2, - -1, - 0, - 1, - 2, - Int64(Int32.max), - Int64(Int32.max) + 1, - .max - ] - - static let testDoubles: [Double] = testInt64s.map { Double($0) } + [ - -.infinity, - -1e100, - -0.0, - 0.0, - 1e100, - .infinity, - .nan - ] - - static let testFloats: [Float] = testDoubles.map { Float($0) } - - static let testVectors: [Vector2] = testFloats.flatMap { y in - testFloats.map { x in - Vector2(x: x, y: y) - } - } - - // Vector2.method() - func testNullaryCovers() throws { - - func checkMethod(_ method: (Vector2) -> () -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - try checkCover(filePath: filePath, line: line) { method(v)() } - } - } - - try checkMethod(Vector2.angle) - try checkMethod(Vector2.length) - try checkMethod(Vector2.lengthSquared) - try checkMethod(Vector2.normalized) - try checkMethod(Vector2.sign) - try checkMethod(Vector2.floor) - try checkMethod(Vector2.ceil) - try checkMethod(Vector2.round) - } - - // Vector2.method(Double) - func testUnaryDoubleCovers() throws { - - func checkMethod(_ method: (Vector2) -> (Double) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for d in Self.testDoubles { - try checkCover(filePath: filePath, line: line) { method(v)(d) } - } - } - } - - try checkMethod(Vector2.rotated) - try checkMethod(Vector2.snappedf) - try checkMethod(Vector2.limitLength) - } - - // Vector2.method(Vector2) - func testUnaryCovers() throws { - - func checkMethod(_ method: (Vector2) -> (Vector2) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for u in Self.testVectors { - try checkCover(filePath: filePath, line: line) { method(v)(u) } - } - } - } - - try checkMethod(Vector2.distanceTo(_:)) - try checkMethod(Vector2.distanceSquaredTo(_:)) - try checkMethod(Vector2.angleTo(_:)) - try checkMethod(Vector2.angleToPoint) - try checkMethod(Vector2.dot) - try checkMethod(Vector2.cross) - try checkMethod(Vector2.project) - try checkMethod(Vector2.slide) - try checkMethod(Vector2.bounce) - try checkMethod(Vector2.reflect(line:)) - } - - // Static - func testFromAngle() throws { - for d in Self.testDoubles { - try checkCover { Vector2.fromAngle(d) } - } - } - - func testClamp() throws { - for v in Self.testVectors { - for u in Self.testVectors { - for w in Self.testVectors { - try checkCover { v.clamp(min: u, max: w) } - } - } - } - } - - func testClampf() throws { - for v in Self.testVectors { - for d in Self.testDoubles { - for e in Self.testDoubles { - try checkCover { v.clampf(min: d, max: e) } - } - } - } - } - - func testMoveToward() throws { - for v in Self.testVectors { - for u in Self.testVectors { - for d in Self.testDoubles { - try checkCover { v.moveToward(to: u, delta: d) } - } - } - } - } - - // Operator Covers - - func testBinaryOperators_Vector2i_Vector2i() throws { - // Operators of the form Vector2i * Vector2i. - - func checkOperator( - _ op: (Vector2, Vector2) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for u in Self.testVectors { - try checkCover(filePath: filePath, line: line) { op(v, u) } - } - } - } - - // Arithmetic Operators - try checkOperator(+) - try checkOperator(-) - try checkOperator(*) - try checkOperator(/) - // Comparison Operators - try checkOperator(==) - try checkOperator(!=) - try checkOperator(<) - try checkOperator(<=) - try checkOperator(>) - try checkOperator(>=) - } - - func testBinaryOperators_Vector2i_Int64() throws { - // Operators of the form Vector2i * Int64. - - func checkOperator( - _ op: (Vector2, Int64) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for i in Self.testInt64s { - try checkCover(filePath: filePath, line: line) { op(v, i) } - } - } - } - - try checkOperator(/) - try checkOperator(*) - } - - func testBinaryOperators_Vector2i_Double() throws { - // Operators of the form Vector2i * Int64. - - func checkOperator( - _ op: (Vector2, Double) -> some Equatable, - filePath: StaticString = #filePath, line: UInt = #line - ) throws { - for v in Self.testVectors { - for d in Self.testDoubles { - try checkCover(filePath: filePath, line: line) { op(v, d) } - } - } - } - - try checkOperator(/) - try checkOperator(*) - } - - - // Non-covers tests - func testOperatorUnaryMinus () { var value: Vector2 diff --git a/Vector2CoverTests.swift b/Vector2CoverTests.swift new file mode 100644 index 000000000..bb903820b --- /dev/null +++ b/Vector2CoverTests.swift @@ -0,0 +1,210 @@ +// +// Vector2CoverTests.swift +// SwiftGodot +// +// Created by Danny Youstra on 12/10/24. +// + +import XCTest +import SwiftGodotTestability +@testable import SwiftGodot + +@available(macOS 14, *) +extension Vector2 { + static func gen(_ coordinateGen: TinyGen) -> TinyGen { + return TinyGen { rng in + return Vector2(x: coordinateGen(rng.left()), y: coordinateGen(rng.right())) + } + } + + static let mixed: TinyGen = gen(.mixedFloats) +} + +@available(macOS 14, *) +final class Vector2CoverTests: GodotTestCase { + + func testInit() { + forAll { + Vector2.mixed + } checkCover { + Vector2(from: $0) + } + } + + // Vector2.method() + func testNullaryCovers() { + + func checkMethod(_ method: (Vector2) -> () -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector2.mixed + } checkCover: { v in + method(v)() + } + + } + + try checkMethod(Vector2.angle) + try checkMethod(Vector2.length) + try checkMethod(Vector2.lengthSquared) + try checkMethod(Vector2.normalized) + try checkMethod(Vector2.sign) + try checkMethod(Vector2.floor) + try checkMethod(Vector2.ceil) + try checkMethod(Vector2.round) + } + + // Vector2.method(Double) + func testUnaryDoubleCovers() { + + func checkMethod(_ method: (Vector2) -> (Double) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector2.mixed + TinyGen.mixedDoubles + } checkCover: { + method($0)($1) + } + } + + try checkMethod(Vector2.rotated) + try checkMethod(Vector2.snappedf) + try checkMethod(Vector2.limitLength) + } + + // Vector2.method(Vector2) + func testUnaryCovers() { + + func checkMethod(_ method: (Vector2) -> (Vector2) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector2.mixed + Vector2.mixed + } checkCover { + method($0)($1) + } + } + + try checkMethod(Vector2.distanceTo(_:)) + try checkMethod(Vector2.distanceSquaredTo(_:)) + try checkMethod(Vector2.angleTo(_:)) + try checkMethod(Vector2.angleToPoint) + try checkMethod(Vector2.dot) + try checkMethod(Vector2.cross) + try checkMethod(Vector2.project) + try checkMethod(Vector2.slide) + try checkMethod(Vector2.bounce) + try checkMethod(Vector2.reflect(line:)) + } + + // Static + func testFromAngle() { + forAll { + TinyGen.mixedDoubles + } checkCover { + Vector2.fromAngle($0) + } + } + + func testClamp() { + forAll { + Vector2.mixed + Vector2.mixed + Vector2.mixed + } checkCover { + $0.clamp(min: $1, max: $2) + } + } + + func testClampf() { + forAll { + Vector2.mixed + TinyGen.mixedDoubles + TinyGen.mixedDoubles + } checkCover { + $0.clampf($1, $2) + } + } + + func testMoveToward() { + forAll { + Vector2.mixed + Vector2.mixed + TinyGen.mixedDoubles + } checkCover { + $0.moveToward(to: $1, delta: $2) + } + } + + // Operator Covers + + func testBinaryOperators_Vector2i_Vector2i() { + // Operators of the form Vector2i * Vector2i. + + func checkOperator( + _ op: (Vector2, Vector2) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector2.mixed + Vector2.mixed + } checkCover { + op($0, $1) + } + } + + // Arithmetic Operators + try checkOperator(+) + try checkOperator(-) + try checkOperator(*) + try checkOperator(/) + // Comparison Operators + try checkOperator(==) + try checkOperator(!=) + try checkOperator(<) + try checkOperator(<=) + try checkOperator(>) + try checkOperator(>=) + } + + func testBinaryOperators_Vector2i_Int64() { + // Operators of the form Vector2i * Int64. + + func checkOperator( + _ op: (Vector2, Int64) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector2.mixed + TinyGen.mixedInt64s + } checkCover { + op($0, $1) + } + } + + try checkOperator(/) + try checkOperator(*) + } + + func testBinaryOperators_Vector2i_Double() { + // Operators of the form Vector2i * Int64. + + func checkOperator( + _ op: (Vector2, Double) -> some Equatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector2.mixed + TinyGen.mixedDoubles + } checkCover { + op($0, $1) + } + } + + try checkOperator(/) + try checkOperator(*) + } +} From 981a10594f7d5347e51908046e2cdad9b48acbe6 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Tue, 10 Dec 2024 15:37:00 -0600 Subject: [PATCH 72/99] Add Vector4 Covers --- Sources/SwiftCovers/Vector4.covers.swift | 232 +++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 Sources/SwiftCovers/Vector4.covers.swift diff --git a/Sources/SwiftCovers/Vector4.covers.swift b/Sources/SwiftCovers/Vector4.covers.swift new file mode 100644 index 000000000..7fe1336a9 --- /dev/null +++ b/Sources/SwiftCovers/Vector4.covers.swift @@ -0,0 +1,232 @@ +// +// Vector4.covers.swift +// SwiftGodot +// +// Created by Danny Youstra on 12/10/24. +// + +@_spi(SwiftCovers) import SwiftGodot + +#if canImport(Darwin) +import Darwin +#elseif os(Windows) +import ucrt +import WinSDK +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#else +#error("Unable to identify your C library.") +#endif + +extension Vector4 { + + public init(from: Vector4) { + self = from + } + + public func dot(with: Vector4) -> Double { + return Double(x * with.x + y * with.y + z * with.z + w * with.w) + } + + public func abs() -> Vector4 { + return Vector4( + x: Swift.abs(x), + y: Swift.abs(y), + z: Swift.abs(z), + w: Swift.abs(w) + ) + } + + public func sign() -> Vector4 { + return Vector4( + x: SwiftGodot.sign(x), + y: SwiftGodot.sign(y), + z: SwiftGodot.sign(z), + w: SwiftGodot.sign(w) + ) + } + + public func floor() -> Vector4 { + return Vector4( + x: _math.floor(x), + y: _math.floor(y), + z: _math.floor(z), + w: _math.floor(w) + ) + } + + public func ceil() -> Vector4 { + return Vector4( + x: _math.ceil(x), + y: _math.ceil(y), + z: _math.ceil(z), + w: _math.ceil(w) + ) + } + + public func round() -> Vector4 { + return Vector4( + x: _math.round(x), + y: _math.round(y), + z: _math.round(z), + w: _math.round(w) + ) + } + + public func clamp(min: Vector4, max: Vector4) -> Vector4 { + return Vector4( + x: x.clamped(min: min.x, max: max.x), + y: y.clamped(min: min.y, max: max.y), + z: z.clamped(min: min.z, max: max.z), + w: w.clamped(min: min.w, max: max.w) + ) + } + + public func clampf(min: Double, max: Double) -> Vector4 { + return Vector4( + x: x.clamped(min: Float(min), max: Float(max)), + y: y.clamped(min: Float(min), max: Float(max)), + z: z.clamped(min: Float(min), max: Float(max)), + w: w.clamped(min: Float(min), max: Float(max)) + ) + } + + public func snappedf(step: Double) -> Vector4 { + return Vector4( + x: x.snapped(step: Float(step)), + y: y.snapped(step: Float(step)), + z: z.snapped(step: Float(step)), + w: w.snapped(step: Float(step)) + ) + } + + public func normalized() -> Vector4 { + var result = self + let lensq = Float(lengthSquared()) + if lensq != 0 { + let len = sqrt(lensq) + result = Vector4(x: x / len, y: y / len, z: z / len, w: w / len) + } + return result + } + + // Arithmetic Operators + + public static func + (lhs: Vector4, rhs: Vector4) -> Vector4 { + return Vector4(x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z, w: lhs.w + rhs.w) + } + + public static func - (lhs: Vector4, rhs: Vector4) -> Vector4 { + return Vector4(x: lhs.x - rhs.x, y: lhs.y - rhs.y, z: lhs.z - rhs.z, w: lhs.w - rhs.w) + } + + public static func * (lhs: Vector4, rhs: Vector4) -> Vector4 { + return Vector4(x: lhs.x * rhs.x, y: lhs.y * rhs.y, z: lhs.z * rhs.z, w: lhs.w * rhs.w) + } + + public static func / (lhs: Vector4, rhs: Vector4) -> Vector4 { + return Vector4(x: lhs.x / rhs.x, y: lhs.y / rhs.y, z: lhs.z / rhs.z, w: lhs.w / rhs.w) + } + + public static func * (lhs: Vector4, rhs: Int64) -> Vector4 { + return Vector4( + x: lhs.x * Float(rhs), + y: lhs.y * Float(rhs), + z: lhs.z * Float(rhs), + w: lhs.w * Float(rhs) + ) + } + + public static func * (lhs: Vector4, rhs: Double) -> Vector4 { + return Vector4( + x: lhs.x * Float(rhs), + y: lhs.y * Float(rhs), + z: lhs.z * Float(rhs), + w: lhs.w * Float(rhs) + ) + } + + public static func / (lhs: Vector4, rhs: Int64) -> Vector4 { + return Vector4( + x: lhs.x / Float(rhs), + y: lhs.y / Float(rhs), + z: lhs.z / Float(rhs), + w: lhs.w / Float(rhs) + ) + } + + public static func / (lhs: Vector4, rhs: Double) -> Vector4 { + return Vector4( + x: lhs.x / Float(rhs), + y: lhs.y / Float(rhs), + z: lhs.z / Float(rhs), + w: lhs.w / Float(rhs) + ) + } + + // Comparison Operators + + public static func == (lhs: Vector4, rhs: Vector4) -> Bool { + return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z && lhs.w == rhs.w + } + + public static func != (lhs: Vector4, rhs: Vector4) -> Bool { + return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z || lhs.w != rhs.w + } + + public static func < (lhs: Vector4, rhs: Vector4) -> Bool { + if lhs.x == rhs.x { + if lhs.y == rhs.y { + if lhs.z == rhs.z { + return lhs.w < rhs.w + } + return lhs.z < rhs.z + } + return lhs.y < rhs.y + } + return lhs.x < rhs.x + } + + public static func > (lhs: Vector4, rhs: Vector4) -> Bool { + if lhs.x == rhs.x { + if lhs.y == rhs.y { + if lhs.z == rhs.z { + return lhs.w > rhs.w + } + return lhs.z > rhs.z + } + return lhs.y > rhs.y + } + return lhs.x > rhs.x + } + + public static func <= (lhs: Vector4, rhs: Vector4) -> Bool { + if lhs.x == rhs.x { + if lhs.y == rhs.y { + if lhs.z == rhs.z { + return lhs.w <= rhs.w + } + return lhs.z < rhs.z + } + return lhs.y < rhs.y + } + return lhs.x < rhs.x + } + + public static func >= (lhs: Vector4, rhs: Vector4) -> Bool { + if lhs.x == rhs.x { + if lhs.y == rhs.y { + if lhs.z == rhs.z { + return lhs.w >= rhs.w + } + return lhs.z > rhs.z + } + return lhs.y > rhs.y + } + return lhs.x > rhs.x + } + + +} From 2cecad93c8977b7ecc93c96dee38e36fbc8c96c6 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Tue, 10 Dec 2024 15:37:10 -0600 Subject: [PATCH 73/99] add init for Vector3 Covers --- Sources/SwiftCovers/Vector3.covers.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/SwiftCovers/Vector3.covers.swift b/Sources/SwiftCovers/Vector3.covers.swift index 560709fc7..aef639e3e 100644 --- a/Sources/SwiftCovers/Vector3.covers.swift +++ b/Sources/SwiftCovers/Vector3.covers.swift @@ -21,6 +21,10 @@ import Musl extension Vector3 { + public init(from: Vector3) { + self = from + } + public func cross(with: Vector3) -> Vector3 { return Vector3( x: (y * with.z) - (z * with.y), From b86e70eb33efb4df5a6f4828369bade72002789e Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Tue, 10 Dec 2024 18:11:18 -0600 Subject: [PATCH 74/99] Transform2D covers (no operator) --- Sources/SwiftCovers/Transform2D.covers.swift | 190 +++++++++++++++++++ Sources/SwiftGodot/SwiftCoverSupport.swift | 26 +++ 2 files changed, 216 insertions(+) create mode 100644 Sources/SwiftCovers/Transform2D.covers.swift diff --git a/Sources/SwiftCovers/Transform2D.covers.swift b/Sources/SwiftCovers/Transform2D.covers.swift new file mode 100644 index 000000000..109be481e --- /dev/null +++ b/Sources/SwiftCovers/Transform2D.covers.swift @@ -0,0 +1,190 @@ +// +// Transform2D.covers.swift +// SwiftGodot +// +// Created by Danny Youstra on 12/10/24. +// + +@_spi(SwiftCovers) import SwiftGodot + +#if canImport(Darwin) +import Darwin +#elseif os(Windows) +import ucrt +import WinSDK +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#else +#error("Unable to identify your C library.") +#endif + +extension Transform2D { + + public init(from: Transform2D) { + self = from + } + + public init(rotation: Float, position: Vector2) { + self.init() + let cr = cos(rotation) + let sr = sin(rotation) + + self.x = Vector2(x: cr, y: sr) + self.y = Vector2(x: -sr, y: cr) + self.origin = position + } + + public init(rotation: Float, scale: Vector2, skew: Float, position: Vector2) { + self.init() + self.x = Vector2( + x: cos(rotation) * scale.x, + y: sin(rotation) * scale.x + ) + self.y = Vector2( + x: -sin(rotation + skew) * scale.y, + y: cos(rotation + skew) * scale.y + ) + self.origin = position + } + + public func inverse() -> Transform2D { + var result = self + // SWAP() macro: swap elements + result.x.y = y.x + result.y.x = x.y + result.origin = basisXform(v: -result.origin) + return result + } + + public func affineInverse() -> Transform2D { + var result = self + let det = Float(determinant()) + let idet = 1.0 / det + + // Swap diagonals + result.x.x = y.y + result.y.y = x.x + + // Scale basis vectors + result.x *= Vector2(x: idet, y: -idet) + result.y *= Vector2(x: -idet, y: idet) + + // Transform the origin + result.origin = basisXform(v: -result.origin) + return result + } + + public func getSkew() -> Double { + let det = determinant() + return acos(x.normalized().dot(with: y.normalized() * SwiftGodot.sign(det))) - (Double.pi * 0.5) + } + + public func getRotation() -> Double { + return Double(atan2(x.y, x.x)) + } + + public func getOrigin() -> Vector2 { + return origin + } + + /// Returns a copy of this transform with orthonormalized basis vectors using the Gram-Schmidt process. + /// This ensures the basis vectors (x and y) are orthogonal (perpendicular) and normalized (length of 1). + public func orthonormalized() -> Transform2D { + // Applies Gram-Schmidt orthonormalization to the transform's basis vectors + var result = self + result.x = result.x.normalized() + result.y = result.y - result.x * result.x.dot(with: result.y) + result.y = result.y.normalized() + + return result + } + + public func rotated(angle: Double) -> Transform2D { + return Transform2D(rotation: Float(angle), position: Vector2()) * self + } + + /// Just the above method reversed (not commutative) + public func rotatedLocal(angle: Double) -> Transform2D { + return self * Transform2D(rotation: Float(angle), position: Vector2()) + } + + public func scaled(scale: Vector2) -> Transform2D { + // scale basis + var result = scaleBasis(scale: scale) + // scale origin + result.origin *= scale + return result + } + + public func scaledLocal(scale: Vector2) -> Transform2D { + return Transform2D(xAxis: x * Double(scale.x), yAxis: y * Double(scale.y), origin: origin) + } + + public func translated(offset: Vector2) -> Transform2D { + return Transform2D(xAxis: x, yAxis: y, origin: origin + offset) + } + + public func translatedLocal(offset: Vector2) -> Transform2D { + return Transform2D(xAxis: x, yAxis: y, origin: origin + basisXform(v: offset)) + } + + public func determinant() -> Double { + return Double((x.x * y.y) - (x.y * y.x)) + } + + public func basisXform(v: Vector2) -> Vector2 { + return Vector2(x: tdotx(v: v), y: tdoty(v: v)) + } + + public func basisXformInv(v: Vector2) -> Vector2 { + return Vector2(x: Float(x.dot(with: v)), y: Float(y.dot(with: v))) + } + + public func interpolateWith(xform: Transform2D, weight: Double) -> Transform2D { + let p1 = origin + let p2 = xform.origin + + let r1 = Float(getRotation()) + let r2 = Float(xform.getRotation()) + + let s1 = getScale() + let s2 = xform.getScale() + + // Slerp rotation + let v1 = Vector2(x: cos(r1), y: sin(r1)) + let v2 = Vector2(x: cos(r2), y: sin(r2)) + + var dot = v1.dot(with: v2) + dot = dot.clamped(min: -1.0, max: 1.0) + + var v: Vector2 + + if dot > 0.9995 { + // Linearly interpolate to avoid numerical precision issues + v = v1.lerp(to: v2, weight: weight).normalized() + } else { + let angle = weight * acos(dot) + let v3 = (v2 - v1 * dot).normalized() + v = v1 * cos(angle) + v3 * sin(angle) + } + + // Construct matrix + var res = Transform2D(rotation: Float(v.angle()), position: p1.lerp(to: p2, weight: weight)) + res.scaleBasis(scale: s1.lerp(to: s2, weight: weight)) + return res + } + + public func isFinite() -> Bool { + return x.isFinite && y.isFinite && z.isFinite + } + + public func lookingAt(target: Vector2 = Vector2 (x: 0, y: 0)) -> Transform2D { + var returnTrans = Transform2D(rotation: getRotation(), position: origin) + let targetPosition = affineInverse().xform(p_target) + returnTrans.rotation = returnTrans.getRotation() + (targetPosition * scale).angle() + return returnTrans + } + +} diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index 14b35b24f..cc6eb6f1e 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -74,6 +74,32 @@ extension Quaternion { public var simd: SIMD4 { SIMD4(x, y, z, w) } } +extension Transform2D { + @_spi(SwiftCovers) + @inline(__always) + public func scaleBasis(scale: Vector2) -> Transform2D { + var result = self + result.x.x *= scale.x + result.x.y *= scale.y + result.y.x *= scale.x + result.y.y *= scale.y + return result + } + + @_spi(SwiftCovers) + @inline(__always) + public func tdotx(v: Vector2) -> Float { + return x.x * v.x + y.x * v.y + } + + @_spi(SwiftCovers) + @inline(__always) + public func tdoty(v: Vector2) -> Float { + return x.y * v.x + y.y * v.y + } + +} + @_spi(SwiftCovers) @inline(__always) public func sign(_ x: Float) -> Float { From 0a84b66148e4673cda4fa84335ebc926a75def76 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 03:09:41 -0600 Subject: [PATCH 75/99] Add Transform2D covers --- Sources/SwiftCovers/Transform2D.covers.swift | 68 ++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/Sources/SwiftCovers/Transform2D.covers.swift b/Sources/SwiftCovers/Transform2D.covers.swift index 109be481e..bf916468b 100644 --- a/Sources/SwiftCovers/Transform2D.covers.swift +++ b/Sources/SwiftCovers/Transform2D.covers.swift @@ -187,4 +187,72 @@ extension Transform2D { return returnTrans } + public subscript(index: Int64) -> Vector2 { + return index == 0 ? x : index == 1 ? y : origin + } + + // Operators + + public static func * (lhs: Transform2D, rhs: Int64) -> Transform2D { + var result = lhs + result.x *= Double(rhs) + result.y *= Double(rhs) + result.origin *= Double(rhs) + return result + } + + public static func / (lhs: Transform2D, rhs: Int64) -> Transform2D { + var result = lhs + result.x /= Double(rhs) + result.y /= Double(rhs) + result.origin /= Double(rhs) + return result + } + + public static func * (lhs: Transform2D, rhs: Double) -> Transform2D { + var result = lhs + result.x *= rhs + result.y *= rhs + result.origin *= rhs + return result + } + + public static func / (lhs: Transform2D, rhs: Double) -> Transform2D { + var result = lhs + result.x /= rhs + result.y /= rhs + result.origin /= rhs + return result + } + + public static func == (lhs: Transform2D, rhs: Transform2D) -> Bool { + return lhs.x == rhs.x && lhs.y == rhs.y && lhs.origin == rhs.origin + } + + public static func != (lhs: Transform2D, rhs: Transform2D) -> Bool { + return !(lhs == rhs) + } + + public static func * (lhs: Transform2D, rhs: Transform2D) -> Transform2D { + var result = lhs + result.origin = result.xform(rhs.origin) + + let x0 = result.tdotx(rhs.x) + let x1 = result.tdoty(rhs.x) + let y0 = result.tdotx(rhs.y) + let y1 = result.tdoty(rhs.y) + + result.x.x = x0 + result.x.y = x1 + result.y.x = y0 + result.y.y = y1 + + return result + } + +// public static func * (lhs: Transform2D, rhs: PackedVector2Array) -> PackedVector2Array { +// +// } + + } From 63e70ed5ee42f04682b336415a6a22ffc00cf1c5 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 11:52:56 -0600 Subject: [PATCH 76/99] Fix Transform2D Covers --- Sources/SwiftCovers/Transform2D.covers.swift | 24 ++++++++----------- Sources/SwiftGodot/SwiftCoverSupport.swift | 25 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/Sources/SwiftCovers/Transform2D.covers.swift b/Sources/SwiftCovers/Transform2D.covers.swift index bf916468b..da5ced494 100644 --- a/Sources/SwiftCovers/Transform2D.covers.swift +++ b/Sources/SwiftCovers/Transform2D.covers.swift @@ -172,18 +172,19 @@ extension Transform2D { // Construct matrix var res = Transform2D(rotation: Float(v.angle()), position: p1.lerp(to: p2, weight: weight)) - res.scaleBasis(scale: s1.lerp(to: s2, weight: weight)) + res = res.scaleBasis(scale: s1.lerp(to: s2, weight: weight)) return res } public func isFinite() -> Bool { - return x.isFinite && y.isFinite && z.isFinite + return x.isFinite() && y.isFinite() && origin.isFinite() } public func lookingAt(target: Vector2 = Vector2 (x: 0, y: 0)) -> Transform2D { - var returnTrans = Transform2D(rotation: getRotation(), position: origin) - let targetPosition = affineInverse().xform(p_target) - returnTrans.rotation = returnTrans.getRotation() + (targetPosition * scale).angle() + var returnTrans = Transform2D(rotation: Float(getRotation()), position: origin) + let targetPosition = affineInverse().xform(target) + let newRotation = (targetPosition * getScale()).angle() + returnTrans = returnTrans.rotated(angle: newRotation) return returnTrans } @@ -237,10 +238,10 @@ extension Transform2D { var result = lhs result.origin = result.xform(rhs.origin) - let x0 = result.tdotx(rhs.x) - let x1 = result.tdoty(rhs.x) - let y0 = result.tdotx(rhs.y) - let y1 = result.tdoty(rhs.y) + let x0 = result.tdotx(v: rhs.x) + let x1 = result.tdoty(v: rhs.x) + let y0 = result.tdotx(v: rhs.y) + let y1 = result.tdoty(v: rhs.y) result.x.x = x0 result.x.y = x1 @@ -250,9 +251,4 @@ extension Transform2D { return result } -// public static func * (lhs: Transform2D, rhs: PackedVector2Array) -> PackedVector2Array { -// -// } - - } diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index cc6eb6f1e..99ab87a79 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -98,6 +98,31 @@ extension Transform2D { return x.y * v.x + y.y * v.y } + @_spi(SwiftCovers) + @inline(__always) + public func xform(_ v: Vector2) -> Vector2 { + var result = basisXform(v: v) + result += origin + return result + } +} + +extension Basis { + @_spi(SwiftCovers) + @inline(__always) + public func xform(_ v: Vector3) -> Vector3 { + return Vector3( + x: Float(x.dot(with: v)), + y: Float(y.dot(with: v)), + z: Float(z.dot(with: v)) + ) + } + + @_spi(SwiftCovers) + @inline(__always) + public func scaledLocal(scale: Vector3) -> Basis { + return self * Basis.fromScale(scale) + } } @_spi(SwiftCovers) From 7d44e3dd0666407beec9fd7398d3d76cdf091ee2 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 11:53:19 -0600 Subject: [PATCH 77/99] Add Transform3D Covers --- Sources/SwiftCovers/Transform3D.covers.swift | 177 +++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 Sources/SwiftCovers/Transform3D.covers.swift diff --git a/Sources/SwiftCovers/Transform3D.covers.swift b/Sources/SwiftCovers/Transform3D.covers.swift new file mode 100644 index 000000000..c5d6eb46f --- /dev/null +++ b/Sources/SwiftCovers/Transform3D.covers.swift @@ -0,0 +1,177 @@ +// +// Transform3D.covers.swift +// SwiftGodot +// +// Created by Danny Youstra on 12/11/24. +// + +@_spi(SwiftCovers) import SwiftGodot + +#if canImport(Darwin) +import Darwin +#elseif os(Windows) +import ucrt +import WinSDK +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#else +#error("Unable to identify your C library.") +#endif + +extension Transform3D { + + public init(from: Transform3D) { + self = from + } + + public init(xAxis: Vector3, yAxis: Vector3, zAxis: Vector3, origin: Vector3) { + self.init() + self.basis = Basis(xAxis: xAxis, yAxis: yAxis, zAxis: zAxis) + self.origin = origin + } + + public func inverse() -> Transform3D { + var result = self + result.basis = result.basis.transposed() + result.origin = result.basis.xform(-origin) + return result + } + + public func affineInverse() -> Transform3D { + var result = self + result.basis = result.basis.inverse() + result.origin = result.basis.xform(-origin) + return result + } + + public func orthonormalized() -> Transform3D { + var result = self + result.basis = result.basis.orthonormalized() + return result + } + + public func scaled(scale: Vector3) -> Transform3D { + return Transform3D( + basis: basis.scaled(scale: scale), + origin: origin * scale + ) + } + + public func rotated(axis: Vector3, angle: Double) -> Transform3D { + let rotationBasis = Basis(axis: axis, angle: Float(angle)) + return Transform3D(basis: rotationBasis * basis, origin: rotationBasis.xform(origin)) + } + + public func rotatedLocal(axis: Vector3, angle: Double) -> Transform3D { + let rotationBasis = Basis(axis: axis, angle: Float(angle)) + return Transform3D(basis: basis * rotationBasis, origin: origin) + } + + public func scaledLocal(scale: Vector3) -> Transform3D { + return Transform3D( + basis: basis.scaledLocal(scale: scale), + origin: origin + ) + } + + public func translated(offset: Vector3) -> Transform3D { + return Transform3D(basis: basis, origin: origin + offset) + } + + public func translatedLocal(offset: Vector3) -> Transform3D { + return Transform3D(basis: basis, origin: origin + basis.xform(offset)) + } + + public func lookingAt(target: Vector3, up: Vector3 = Vector3(x: 0, y: 1, z: 0)) -> Transform3D { + var result = self + result.basis = Basis.lookingAt(target: target - origin, up: up) + return result + } + + public func interpolateWith(xform: Transform3D, weight: Double) -> Transform3D { + let srcScale = basis.getScale() + let srcRot = basis.getRotationQuaternion() + let srcLoc = origin + + let dstScale = xform.basis.getScale() + let dstRot = xform.basis.getRotationQuaternion() + let dstLoc = xform.origin + + var result = Transform3D() + // Create basis from interpolated quaternion and scale it + result.basis = Basis(from: srcRot.slerp(to: dstRot, weight: weight).normalized()) + result.basis = result.basis.scaled(scale: srcScale.lerp(to: dstScale, weight: weight)) + result.origin = srcLoc.lerp(to: dstLoc, weight: weight) + + return result + } + + public func isFinite() -> Bool { + return basis.isFinite() && origin.isFinite() + } + + public static func * (lhs: Transform3D, rhs: Double) -> Transform3D { + var result = lhs + result.basis = result.basis * rhs + result.origin = result.origin * rhs + return result + } + + public static func / (lhs: Transform3D, rhs: Double) -> Transform3D { + var result = lhs + result.basis = result.basis / rhs + result.origin = result.origin / rhs + return result + } + + public static func * (lhs: Transform3D, rhs: Int64) -> Transform3D { + var result = lhs + result.basis = result.basis * rhs + result.origin = result.origin * rhs + return result + } + + public static func / (lhs: Transform3D, rhs: Int64) -> Transform3D { + var result = lhs + result.basis = result.basis / rhs + result.origin = result.origin / rhs + return result + } + + public static func * (lhs: Transform3D, rhs: Vector3) -> Vector3 { + return lhs.basis.xform(rhs) + lhs.origin + } + +// public static func * (lhs: Transform3D, rhs: Plane) -> Plane { +// +// } +// +// public static func * (lhs: Transform3D, rhs: AABB) -> AABB { +// +// } + + public static func == (lhs: Transform3D, rhs: Transform3D) -> Bool { + return lhs.basis == rhs.basis && lhs.origin == rhs.origin + } + + public static func != (lhs: Transform3D, rhs: Transform3D) -> Bool { + return !(lhs == rhs) + } + + public static func * (lhs: Transform3D, rhs: Transform3D) -> Transform3D { + var result = lhs + result.origin = result.basis.xform(rhs.origin) + result.origin + result.basis = result.basis * rhs.basis + return result + } + + public static func * (lhs: Transform3D, rhs: PackedVector3Array) -> PackedVector3Array { + for (i, v3) in rhs.enumerated() { + rhs[i] = lhs.basis.xform(v3) + lhs.origin + } + return rhs + } + +} From 10e7e42e01ccace862653d218109438844155e5f Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 11:56:03 -0600 Subject: [PATCH 78/99] Moved V2CoverTests --- .../SwiftGodotTests/BuiltIn/Vector2CoverTests.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Vector2CoverTests.swift => Tests/SwiftGodotTests/BuiltIn/Vector2CoverTests.swift (100%) diff --git a/Vector2CoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector2CoverTests.swift similarity index 100% rename from Vector2CoverTests.swift rename to Tests/SwiftGodotTests/BuiltIn/Vector2CoverTests.swift From b0e3cd54a323d25c3a7346332a98ecbea7fe7ed4 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 12:11:45 -0600 Subject: [PATCH 79/99] Fix V2CoverTests for TinyGen --- .../BuiltIn/Vector2CoverTests.swift | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector2CoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector2CoverTests.swift index bb903820b..2e327c690 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector2CoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector2CoverTests.swift @@ -26,7 +26,7 @@ final class Vector2CoverTests: GodotTestCase { func testInit() { forAll { Vector2.mixed - } checkCover { + } checkCover: { Vector2(from: $0) } } @@ -34,7 +34,7 @@ final class Vector2CoverTests: GodotTestCase { // Vector2.method() func testNullaryCovers() { - func checkMethod(_ method: (Vector2) -> () -> some Equatable, + func checkMethod(_ method: (Vector2) -> () -> some TestEquatable, filePath: StaticString = #filePath, line: UInt = #line ) { forAll(filePath: filePath, line: line) { @@ -45,20 +45,20 @@ final class Vector2CoverTests: GodotTestCase { } - try checkMethod(Vector2.angle) - try checkMethod(Vector2.length) - try checkMethod(Vector2.lengthSquared) - try checkMethod(Vector2.normalized) - try checkMethod(Vector2.sign) - try checkMethod(Vector2.floor) - try checkMethod(Vector2.ceil) - try checkMethod(Vector2.round) + checkMethod(Vector2.angle) + checkMethod(Vector2.length) + checkMethod(Vector2.lengthSquared) + checkMethod(Vector2.normalized) + checkMethod(Vector2.sign) + checkMethod(Vector2.floor) + checkMethod(Vector2.ceil) + checkMethod(Vector2.round) } // Vector2.method(Double) func testUnaryDoubleCovers() { - func checkMethod(_ method: (Vector2) -> (Double) -> some Equatable, + func checkMethod(_ method: (Vector2) -> (Double) -> some TestEquatable, filePath: StaticString = #filePath, line: UInt = #line ) { forAll(filePath: filePath, line: line) { @@ -69,42 +69,42 @@ final class Vector2CoverTests: GodotTestCase { } } - try checkMethod(Vector2.rotated) - try checkMethod(Vector2.snappedf) - try checkMethod(Vector2.limitLength) + checkMethod(Vector2.rotated) + checkMethod(Vector2.snappedf) + checkMethod(Vector2.limitLength) } // Vector2.method(Vector2) func testUnaryCovers() { - func checkMethod(_ method: (Vector2) -> (Vector2) -> some Equatable, + func checkMethod(_ method: (Vector2) -> (Vector2) -> some TestEquatable, filePath: StaticString = #filePath, line: UInt = #line ) { forAll(filePath: filePath, line: line) { Vector2.mixed Vector2.mixed - } checkCover { + } checkCover: { method($0)($1) } } - try checkMethod(Vector2.distanceTo(_:)) - try checkMethod(Vector2.distanceSquaredTo(_:)) - try checkMethod(Vector2.angleTo(_:)) - try checkMethod(Vector2.angleToPoint) - try checkMethod(Vector2.dot) - try checkMethod(Vector2.cross) - try checkMethod(Vector2.project) - try checkMethod(Vector2.slide) - try checkMethod(Vector2.bounce) - try checkMethod(Vector2.reflect(line:)) + checkMethod(Vector2.distanceTo(_:)) + checkMethod(Vector2.distanceSquaredTo(_:)) + checkMethod(Vector2.angleTo(_:)) + checkMethod(Vector2.angleToPoint) + checkMethod(Vector2.dot) + checkMethod(Vector2.cross) + checkMethod(Vector2.project) + checkMethod(Vector2.slide) + checkMethod(Vector2.bounce) + checkMethod(Vector2.reflect(line:)) } // Static func testFromAngle() { forAll { TinyGen.mixedDoubles - } checkCover { + } checkCover: { Vector2.fromAngle($0) } } @@ -114,7 +114,7 @@ final class Vector2CoverTests: GodotTestCase { Vector2.mixed Vector2.mixed Vector2.mixed - } checkCover { + } checkCover: { $0.clamp(min: $1, max: $2) } } @@ -124,8 +124,8 @@ final class Vector2CoverTests: GodotTestCase { Vector2.mixed TinyGen.mixedDoubles TinyGen.mixedDoubles - } checkCover { - $0.clampf($1, $2) + } checkCover: { (vec: Vector2, min: Double, max: Double) -> Vector2 in + vec.clampf(min: min, max: max) } } @@ -134,7 +134,7 @@ final class Vector2CoverTests: GodotTestCase { Vector2.mixed Vector2.mixed TinyGen.mixedDoubles - } checkCover { + } checkCover: { $0.moveToward(to: $1, delta: $2) } } @@ -145,66 +145,66 @@ final class Vector2CoverTests: GodotTestCase { // Operators of the form Vector2i * Vector2i. func checkOperator( - _ op: (Vector2, Vector2) -> some Equatable, + _ op: (Vector2, Vector2) -> some TestEquatable, filePath: StaticString = #filePath, line: UInt = #line ) { forAll(filePath: filePath, line: line) { Vector2.mixed Vector2.mixed - } checkCover { + } checkCover: { op($0, $1) } } // Arithmetic Operators - try checkOperator(+) - try checkOperator(-) - try checkOperator(*) - try checkOperator(/) + checkOperator(+) + checkOperator(-) + checkOperator(*) + checkOperator(/) // Comparison Operators - try checkOperator(==) - try checkOperator(!=) - try checkOperator(<) - try checkOperator(<=) - try checkOperator(>) - try checkOperator(>=) + checkOperator(==) + checkOperator(!=) + checkOperator(<) + checkOperator(<=) + checkOperator(>) + checkOperator(>=) } func testBinaryOperators_Vector2i_Int64() { // Operators of the form Vector2i * Int64. func checkOperator( - _ op: (Vector2, Int64) -> some Equatable, + _ op: (Vector2, Int64) -> some TestEquatable, filePath: StaticString = #filePath, line: UInt = #line ) { forAll(filePath: filePath, line: line) { Vector2.mixed - TinyGen.mixedInt64s - } checkCover { + TinyGen.edgyInt64s + } checkCover: { op($0, $1) } } - try checkOperator(/) - try checkOperator(*) + checkOperator(/) + checkOperator(*) } func testBinaryOperators_Vector2i_Double() { // Operators of the form Vector2i * Int64. func checkOperator( - _ op: (Vector2, Double) -> some Equatable, + _ op: (Vector2, Double) -> some TestEquatable, filePath: StaticString = #filePath, line: UInt = #line ) { forAll(filePath: filePath, line: line) { Vector2.mixed TinyGen.mixedDoubles - } checkCover { + } checkCover: { op($0, $1) } } - try checkOperator(/) - try checkOperator(*) + checkOperator(/) + checkOperator(*) } } From 897ca86718c488a533621eb6b8d321420e76ca84 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 14:32:53 -0600 Subject: [PATCH 80/99] Add Vector3CoverTests --- .../BuiltIn/Vector2iCoverTests.swift | 11 -- .../BuiltIn/Vector3CoverTests.swift | 175 ++++++++++++++++++ .../BuiltIn/Vector3iCoverTests.swift | 16 -- 3 files changed, 175 insertions(+), 27 deletions(-) create mode 100644 Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector2iCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector2iCoverTests.swift index d29d393ea..7ddb1bc50 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector2iCoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector2iCoverTests.swift @@ -12,17 +12,6 @@ extension Vector2i { static let edgy: TinyGen = gen(.edgyInt32s) } -@available(macOS 14, *) -extension Vector2 { - static func gen(_ coordinateGen: TinyGen) -> TinyGen { - return TinyGen { rng in - return Vector2(x: coordinateGen(rng.left()), y: coordinateGen(rng.right())) - } - } - - static let mixed: TinyGen = gen(.mixedFloats) -} - @available(macOS 14, *) final class Vector2iCoverTests: GodotTestCase { diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift new file mode 100644 index 000000000..cd74bb013 --- /dev/null +++ b/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift @@ -0,0 +1,175 @@ +// +// File.swift +// SwiftGodot +// +// Created by Danny Youstra on 12/11/24. +// + +import XCTest +import SwiftGodotTestability +@testable import SwiftGodot + +@available(macOS 14, *) +extension Vector3 { + static func gen(_ coordinateGen: TinyGen) -> TinyGen { + return TinyGen { rng in + let right = rng.right() + return Vector3(x: coordinateGen(rng.left()), + y: coordinateGen(right.left()), + z: coordinateGen(right.right()) + ) + } + } + + static let mixed: TinyGen = gen(.mixedFloats) +} + +@available(macOS 14, *) +final class Vector3CoverTests: GodotTestCase { + + func testInit() { + forAll { + Vector3.mixed + } checkCover: { + Vector3(from: $0) + } + } + + // Vector3.method() + func testNullaryCovers() { + func checkMethod(_ method: (Vector3) -> () -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector3.mixed + } checkCover: { v in + method(v)() + } + + } + + checkMethod(Vector3.abs) + checkMethod(Vector3.sign) + checkMethod(Vector3.floor) + checkMethod(Vector3.ceil) + checkMethod(Vector3.round) + checkMethod(Vector3.normalized) + checkMethod(Vector3.octahedronEncode) + } + + // Vector3.method(Vector3) + func testUnaryVector3Covers() { + + func checkMethod(_ method: (Vector3) -> (Vector3) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector3.mixed + Vector3.mixed + } checkCover: { + method($0)($1) + } + } + + checkMethod(Vector3.cross) + checkMethod(Vector3.dot) + checkMethod(Vector3.slide) + checkMethod(Vector3.bounce) + checkMethod(Vector3.reflect) + checkMethod(Vector3.outer) + } + + // Vector3.method(Double) + func testUnaryDoubleCovers() { + + func checkMethod(_ method: (Vector3) -> (Double) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector3.mixed + TinyGen.mixedDoubles + } checkCover: { + method($0)($1) + } + } + + checkMethod(Vector3.snappedf) + checkMethod(Vector3.limitLength(_:)) + } + + func testBinaryVector3DoubleCovers() { + func checkMethod(_ method: (Vector3) -> (Vector3, Double) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector3.mixed + Vector3.mixed + TinyGen.mixedDoubles + } checkCover: { + method($0)($1, $2) + } + } + + checkMethod(Vector3.slerp) + checkMethod(Vector3.rotated) + checkMethod(Vector3.moveToward) + } + + func testClamp() { + forAll { + Vector3.mixed + Vector3.mixed + Vector3.mixed + } checkCover: { + $0.clamp(min: $1, max: $2) + } + } + + func testClampf() { + forAll { + Vector3.mixed + TinyGen.mixedDoubles + TinyGen.mixedDoubles + } checkCover: { + $0.clampf(min: $1, max: $2) + } + } + + // Static + func testOctahedronDecode() { + forAll { + Vector2.mixed + } checkCover: { + Vector3.octahedronDecode(uv: $0) + } + } + + // Vector3 * Vector3. + func testBinaryOperatorsVector3Vector3() { + func checkOperator( + _ op: (Vector3, Vector3) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector3.mixed + Vector3.mixed + } checkCover: { + op($0, $1) + } + } + + // Arithmetic + checkOperator(+) + checkOperator(-) + checkOperator(*) + checkOperator(/) + // Comparison + checkOperator(==) + checkOperator(!=) + checkOperator(<) + checkOperator(>) + checkOperator(<=) + checkOperator(>=) + } + +} diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector3iCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector3iCoverTests.swift index 9b6d44d8a..1e15169ba 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector3iCoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector3iCoverTests.swift @@ -18,22 +18,6 @@ extension Vector3i { static let safe: TinyGen = gen(.safeInt32s) } -@available(macOS 14, *) -extension Vector3 { - static func gen(_ coordinateGen: TinyGen) -> TinyGen { - return TinyGen { rng in - let right = rng.right() - return Vector3( - x: coordinateGen(rng.left()), - y: coordinateGen(right.left()), - z: coordinateGen(right.right()) - ) - } - } - - static let mixed: TinyGen = gen(.mixedFloats) -} - @available(macOS 14, *) final class Vector3iCoverTests: GodotTestCase { From 8e95446ebc2581d6aa50d1759e2738e0aebbc8be Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 15:37:18 -0600 Subject: [PATCH 81/99] Move TinyGen test stuff around, normalize naming --- .../BuiltIn/PlaneCoverTests.swift | 39 +++----- .../BuiltIn/QuaternionCoverTests.swift | 94 +++++++++---------- .../BuiltIn/Vector3CoverTests.swift | 30 +++++- 3 files changed, 86 insertions(+), 77 deletions(-) diff --git a/Tests/SwiftGodotTests/BuiltIn/PlaneCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/PlaneCoverTests.swift index 2f3d69413..7675247ca 100644 --- a/Tests/SwiftGodotTests/BuiltIn/PlaneCoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/PlaneCoverTests.swift @@ -3,23 +3,6 @@ import SwiftGodotTestability import XCTest -@available(macOS 14, *) -extension Vector3 { - static let normalizedGen: TinyGen = TinyGen.build { - TinyGen.gaussianFloats - TinyGen.gaussianFloats - TinyGen.gaussianFloats - }.map { x, y, z in - // https://stackoverflow.com/q/6283080/77567 - let d = (x * x + y * y + z * z).squareRoot() - return Vector3(x: x / d, y: y / d, z: z / d) - } - - static let mixedGen: TinyGen = gen(TinyGen.mixedFloats) - - static let tinyGen: TinyGen = gen(TinyGen.gaussianFloats.map { $0 * 0.0001 }) -} - @available(macOS 14, *) extension Plane { static func gen(normal: TinyGen, d: TinyGen) -> TinyGen { @@ -29,7 +12,7 @@ extension Plane { } // Vanishingly small chance that the normal is zero. - static let nonZeroGen: TinyGen = gen(normal: Vector3.mixedGen, d: TinyGen.mixedFloats) + static let nonZeroGen: TinyGen = gen(normal: Vector3.mixed, d: TinyGen.mixedFloats) static let maybeZeroGen: TinyGen = TinyGen.biasedOneOf(gens: [ (99, nonZeroGen), @@ -40,7 +23,7 @@ extension Plane { @available(macOS 14, *) extension Basis { static let mostlyRotationGen: TinyGen = TinyGenBuilder { - Vector3.normalizedGen // rotation axis + Vector3.normalized // rotation axis TinyGen.gaussianDoubles.map { $0 } // rotation angle TinyGen.gaussianFloats.map { exp($0 * 0.0001) } // x scale TinyGen.gaussianFloats.map { exp($0 * 0.0001) } // y scale @@ -56,7 +39,7 @@ extension Basis { extension Transform3D { static let gaussianGen: TinyGen = TinyGenBuilder { Basis.mostlyRotationGen - Vector3.mixedGen + Vector3.mixed }.map { Transform3D(basis: $0, origin: $1) } } @@ -73,7 +56,7 @@ final class PlaneCoverTests: GodotTestCase { func testInitNormal() { forAll { - Vector3.mixedGen + Vector3.mixed } checkCover: { Plane(normal: $0) } @@ -81,7 +64,7 @@ final class PlaneCoverTests: GodotTestCase { func testInitNormalPoint() { forAll { - Vector3.mixedGen + Vector3.mixed Vector3.mixed } checkCover: { Plane(normal: $0, point: $1) @@ -176,7 +159,7 @@ final class PlaneCoverTests: GodotTestCase { func testHasPoint() { forAll { Plane.nonZeroGen - Vector3.normalizedGen + Vector3.normalized TinyGen.gaussianDoubles.map { 0.0001 * $0 } // offset TinyGen.gaussianDoubles.map { (0.0001 * $0).magnitude } // tolerance } checkCover: { plane, ray, offset, tolerance in @@ -189,7 +172,7 @@ final class PlaneCoverTests: GodotTestCase { func testProject() { forAll { Plane.nonZeroGen - Vector3.normalizedGen + Vector3.normalized TinyGen.gaussianDoubles.map { 100.0 * $0 } } checkCover: { plane, ray, distance in let point = ray * distance @@ -210,8 +193,8 @@ final class PlaneCoverTests: GodotTestCase { func testIntersectsRay() { forAll { Plane.maybeZeroGen - Vector3.normalizedGen - Vector3.normalizedGen + Vector3.normalized + Vector3.normalized } checkCover: { plane, start, heading in plane.intersectsRay(from: start, dir: heading).flatMap { Vector3($0) } } @@ -220,8 +203,8 @@ final class PlaneCoverTests: GodotTestCase { func testIntersectsSegment() { forAll { Plane.maybeZeroGen - Vector3.normalizedGen - Vector3.normalizedGen + Vector3.normalized + Vector3.normalized } checkCover: { plane, start, end in plane.intersectsSegment(from: start, to: end).flatMap { Vector3($0) } } diff --git a/Tests/SwiftGodotTests/BuiltIn/QuaternionCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/QuaternionCoverTests.swift index 384c928b6..e80e21db9 100644 --- a/Tests/SwiftGodotTests/BuiltIn/QuaternionCoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/QuaternionCoverTests.swift @@ -14,10 +14,10 @@ extension Quaternion { }.map { Quaternion(x: $0, y: $1, z: $2, w: $3) } } - static let mixedGen = gen(.mixedFloats) + static let mixed = gen(.mixedFloats) - static let normalizedGen = TinyGenBuilder { - Vector3.normalizedGen + static let normalized = TinyGenBuilder { + Vector3.normalized TinyGen.gaussianFloats }.map { Quaternion(axis: $0, angle: $1).normalized() } } @@ -41,7 +41,7 @@ final class QuaternionCoverTests: GodotTestCase { func testInitFromAxisAndAngle() { Float.$closeEnoughUlps.withValue(2) { forAll { - Vector3.normalizedGen + Vector3.normalized TinyGen.mixedFloats } checkCover: { Quaternion(axis: $0, angle: $1) @@ -51,8 +51,8 @@ final class QuaternionCoverTests: GodotTestCase { func testInitFromArc() { forAll { - Vector3.normalizedGen - Vector3.normalizedGen + Vector3.normalized + Vector3.normalized } checkCover: { Quaternion(arcFrom: $0, arcTo: $1) } @@ -67,8 +67,8 @@ final class QuaternionCoverTests: GodotTestCase { ) { forAll(filePath: filePath, line: line) { TinyGen.oneOf(gens: [ - Quaternion.mixedGen, - Quaternion.normalizedGen, + Quaternion.mixed, + Quaternion.normalized, ]) } checkCover: { method($0)() @@ -95,14 +95,14 @@ final class QuaternionCoverTests: GodotTestCase { forAll { TinyGen.oneOf(gens: [ // Some arbitrary values including weird values. - Quaternion.mixedGen, + Quaternion.mixed, // Some definitely normalized values. - Quaternion.normalizedGen, + Quaternion.normalized, // Some normalized values with slight tweaking that might be enough to make them seem denormalized. TinyGenBuilder { - Quaternion.normalizedGen + Quaternion.normalized perturbation perturbation perturbation @@ -121,7 +121,7 @@ final class QuaternionCoverTests: GodotTestCase { .map { Float(exp(0.000007 * $0)) } forAll { - Quaternion.mixedGen + Quaternion.mixed perturbation perturbation perturbation @@ -134,7 +134,7 @@ final class QuaternionCoverTests: GodotTestCase { func testInverse() { forAll { - Quaternion.normalizedGen + Quaternion.normalized } checkCover: { $0.inverse() } @@ -142,8 +142,8 @@ final class QuaternionCoverTests: GodotTestCase { func testAngleTo() { forAll { - Quaternion.mixedGen - Quaternion.mixedGen + Quaternion.mixed + Quaternion.mixed } checkCover: { $0.angleTo($1) } @@ -151,8 +151,8 @@ final class QuaternionCoverTests: GodotTestCase { func testDot() { forAll { - Quaternion.mixedGen - Quaternion.mixedGen + Quaternion.mixed + Quaternion.mixed } checkCover: { $0.dot(with: $1) } @@ -160,8 +160,8 @@ final class QuaternionCoverTests: GodotTestCase { func testSlerp() { forAll { - Quaternion.normalizedGen - Quaternion.normalizedGen + Quaternion.normalized + Quaternion.normalized weightGen } checkCover: { $0.slerp(to: $1, weight: $2) @@ -170,8 +170,8 @@ final class QuaternionCoverTests: GodotTestCase { func testSlerpni() { forAll { - Quaternion.normalizedGen - Quaternion.normalizedGen + Quaternion.normalized + Quaternion.normalized weightGen } checkCover: { $0.slerpni(to: $1, weight: $2) @@ -181,10 +181,10 @@ final class QuaternionCoverTests: GodotTestCase { func testSphericalCubicInterpolate() { Float.$closeEnoughUlps.withValue(21) { forAll { - Quaternion.normalizedGen - Quaternion.normalizedGen - Quaternion.normalizedGen - Quaternion.normalizedGen + Quaternion.normalized + Quaternion.normalized + Quaternion.normalized + Quaternion.normalized weightGen } checkCover: { $0.sphericalCubicInterpolate(b: $1, preA: $2, postB: $3, weight: $4) @@ -195,10 +195,10 @@ final class QuaternionCoverTests: GodotTestCase { func testSphericalCubicInterpolateInTime() { Float.$closeEnoughUlps.withValue(14) { forAll { - Quaternion.normalizedGen - Quaternion.normalizedGen - Quaternion.normalizedGen - Quaternion.normalizedGen + Quaternion.normalized + Quaternion.normalized + Quaternion.normalized + Quaternion.normalized weightGen extendedWeightGen extendedWeightGen @@ -211,7 +211,7 @@ final class QuaternionCoverTests: GodotTestCase { func testGetEuler() { forAll { - Quaternion.normalizedGen + Quaternion.normalized TinyGen.oneOf(values: EulerOrder.allCases) } checkCover: { $0.getEuler(order: $1.rawValue) @@ -221,7 +221,7 @@ final class QuaternionCoverTests: GodotTestCase { func testFromEuler() { Float.$closeEnoughUlps.withValue(512) { forAll { - Vector3.mixedGen + Vector3.mixed } checkCover: { Quaternion.fromEuler($0) } @@ -230,7 +230,7 @@ final class QuaternionCoverTests: GodotTestCase { func testSubscriptGet() { forAll { - Quaternion.mixedGen + Quaternion.mixed TinyGen.oneOf(values: Array(0 ... 3)) } checkCover: { q, axis in var q = q @@ -240,7 +240,7 @@ final class QuaternionCoverTests: GodotTestCase { func testSubscriptSet() { forAll { - Quaternion.mixedGen + Quaternion.mixed TinyGen.oneOf(values: Array(0 ... 3)) TinyGen.mixedDoubles } checkCover: { q, axis, newValue in @@ -252,7 +252,7 @@ final class QuaternionCoverTests: GodotTestCase { func testTimesInt64() { forAll { - Quaternion.mixedGen + Quaternion.mixed TinyGen.edgyInt64s } checkCover: { $0 * $1 @@ -261,7 +261,7 @@ final class QuaternionCoverTests: GodotTestCase { func testDividedByInt64() { forAll { - Quaternion.mixedGen + Quaternion.mixed TinyGen.edgyInt64s } checkCover: { $0 / $1 @@ -270,7 +270,7 @@ final class QuaternionCoverTests: GodotTestCase { func testTimesDouble() { forAll { - Quaternion.mixedGen + Quaternion.mixed TinyGen.mixedDoubles } checkCover: { $0 * $1 @@ -279,7 +279,7 @@ final class QuaternionCoverTests: GodotTestCase { func testDividedByDouble() { forAll { - Quaternion.mixedGen + Quaternion.mixed TinyGen.mixedDoubles } checkCover: { $0 / $1 @@ -288,8 +288,8 @@ final class QuaternionCoverTests: GodotTestCase { func testTimesVector3() { forAll { - Quaternion.normalizedGen - Vector3.mixedGen + Quaternion.normalized + Vector3.mixed } checkCover: { $0 * $1 } @@ -299,10 +299,10 @@ final class QuaternionCoverTests: GodotTestCase { forAll { TinyGen.oneOf(gens: [ // Same value twice so they are equal. - Quaternion.mixedGen.map { ($0, $0) }, + Quaternion.mixed.map { ($0, $0) }, TinyGenBuilder { - Quaternion.mixedGen - Quaternion.mixedGen + Quaternion.mixed + Quaternion.mixed } ]) } checkCover: { @@ -314,10 +314,10 @@ final class QuaternionCoverTests: GodotTestCase { forAll { TinyGen.oneOf(gens: [ // Same value twice so they are equal. - Quaternion.mixedGen.map { ($0, $0) }, + Quaternion.mixed.map { ($0, $0) }, TinyGenBuilder { - Quaternion.mixedGen - Quaternion.mixedGen + Quaternion.mixed + Quaternion.mixed } ]) } checkCover: { @@ -333,8 +333,8 @@ final class QuaternionCoverTests: GodotTestCase { filePath: StaticString = #filePath, line: UInt = #line ) { forAll(filePath: filePath, line: line) { - Quaternion.mixedGen - Quaternion.mixedGen + Quaternion.mixed + Quaternion.mixed } checkCover: { op($0, $1) } diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift index cd74bb013..d870845f1 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift @@ -20,8 +20,19 @@ extension Vector3 { ) } } + + static let normalized: TinyGen = TinyGen.build { + TinyGen.gaussianFloats + TinyGen.gaussianFloats + TinyGen.gaussianFloats + }.map { x, y, z in + // https://stackoverflow.com/q/6283080/77567 + let d = (x * x + y * y + z * z).squareRoot() + return Vector3(x: x / d, y: y / d, z: z / d) + } static let mixed: TinyGen = gen(.mixedFloats) + static let tinyGen: TinyGen = gen(TinyGen.gaussianFloats.map { $0 * 0.0001 }) } @available(macOS 14, *) @@ -59,7 +70,6 @@ final class Vector3CoverTests: GodotTestCase { // Vector3.method(Vector3) func testUnaryVector3Covers() { - func checkMethod(_ method: (Vector3) -> (Vector3) -> some TestEquatable, filePath: StaticString = #filePath, line: UInt = #line ) { @@ -73,10 +83,26 @@ final class Vector3CoverTests: GodotTestCase { checkMethod(Vector3.cross) checkMethod(Vector3.dot) + checkMethod(Vector3.outer) + } + + // Vector3.method(Vector3) + // Parameter V3 must be normalized + func testUnaryNormalizedVector3Covers() { + func checkMethod(_ method: (Vector3) -> (Vector3) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector3.mixed + Vector3.normalized + } checkCover: { + method($0)($1) + } + } + checkMethod(Vector3.slide) checkMethod(Vector3.bounce) checkMethod(Vector3.reflect) - checkMethod(Vector3.outer) } // Vector3.method(Double) From 3803fe99d028c9874f4edf646bc0143291358706 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 15:37:36 -0600 Subject: [PATCH 82/99] Change v3 rotated to use basis.xform --- Sources/SwiftCovers/Vector3.covers.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftCovers/Vector3.covers.swift b/Sources/SwiftCovers/Vector3.covers.swift index aef639e3e..4486496fd 100644 --- a/Sources/SwiftCovers/Vector3.covers.swift +++ b/Sources/SwiftCovers/Vector3.covers.swift @@ -106,11 +106,8 @@ extension Vector3 { public func rotated(axis: Vector3, angle: Double) -> Vector3 { // basis subscript getter is mutating by default - var basisV = Basis(axis: axis, angle: Float(angle)) - return Vector3(x: Float(basisV[0].dot(with: self)), - y: Float(basisV[1].dot(with: self)), - z: Float(basisV[2].dot(with: self)) - ) + let basis = Basis(axis: axis, angle: Float(angle)) + return basis.xform(self) } public func clamp(min: Vector3, max: Vector3) -> Vector3 { From 9713213fae4aed934b490ab69875c7cb9147ebae Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 15:38:32 -0600 Subject: [PATCH 83/99] Change XCTFail message for TinyGen --- Sources/SwiftGodotTestability/GodotTestCase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftGodotTestability/GodotTestCase.swift b/Sources/SwiftGodotTestability/GodotTestCase.swift index 3aeaf2aa6..ea42274ff 100644 --- a/Sources/SwiftGodotTestability/GodotTestCase.swift +++ b/Sources/SwiftGodotTestability/GodotTestCase.swift @@ -243,7 +243,7 @@ extension GodotTestCase { } guard coverOutput.closeEnough(to: engineOutput) else { - XCTFail("Test failure: cover output \(coverOutput) is not close enough to engine output \(engineOutput)", file: filePath, line: line) + XCTFail("Test failure: cover output \(coverOutput) is not close enough to engine output \(engineOutput) for input \(input)", file: filePath, line: line) return } } From 8e731ea2c167862ee29b75269f078f8e3a515ec7 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 10 Dec 2024 13:29:11 -0600 Subject: [PATCH 84/99] only compute ulps when needed seems to be slow to compute ulps sometimes --- .../SwiftGodotTestability/TestEquatable.swift | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftGodotTestability/TestEquatable.swift b/Sources/SwiftGodotTestability/TestEquatable.swift index a6b6d6fab..70b63b2c3 100644 --- a/Sources/SwiftGodotTestability/TestEquatable.swift +++ b/Sources/SwiftGodotTestability/TestEquatable.swift @@ -43,9 +43,14 @@ extension Float: TestEquatable { // Don't allow opposite signs. guard (self <= 0 && other <= 0) || (self >= 0 && other >= 0) else { return false } let d = (self - other).magnitude + let closeEnough = Self.closeEnoughUlps * min(self.ulp, other.ulp) + if d <= closeEnough { + return true + } + // Compute actual ulps difference for debugging test failures. let ulps = d / min(self.ulp, other.ulp) - let answer = ulps <= Self.closeEnoughUlps - return answer + _ = ulps + return false } } @@ -62,9 +67,14 @@ extension Double: TestEquatable { // Don't allow opposite signs. guard (self <= 0 && other <= 0) || (self >= 0 && other >= 0) else { return false } let d = (self - other).magnitude + let closeEnough = Self.closeEnoughUlps * min(self.ulp, other.ulp) + if d <= closeEnough { + return true + } + // Compute actual ulps difference for debugging test failures. let ulps = d / min(self.ulp, other.ulp) - let answer = ulps <= Self.closeEnoughUlps - return answer + _ = ulps + return false } } From 614d67df6b66c10ef49f8136d17c43b54ac5e9af Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 10 Dec 2024 13:29:11 -0600 Subject: [PATCH 85/99] compute sign like Godot SIGN macro for matching NaN handling --- Sources/SwiftGodot/SwiftCoverSupport.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index 99ab87a79..d9f7455b9 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -128,13 +128,13 @@ extension Basis { @_spi(SwiftCovers) @inline(__always) public func sign(_ x: Float) -> Float { - return x == 0 ? 0 : (x > 0 ? 1.0 : -1.0) + return x > 0 ? 1 : x < 0 ? -1 : 0 } @_spi(SwiftCovers) @inline(__always) public func sign(_ x: Double) -> Double { - return x == 0 ? 0 : (x > 0 ? 1.0 : -1.0) + return x > 0 ? 1 : x < 0 ? -1 : 0 } /// This epsilon should match Godot's `CMP_EPSILON` (which is unfortunately not exported). From a81c5895236431768bb89fd50b1da83a9fef0a40 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 10 Dec 2024 13:29:11 -0600 Subject: [PATCH 86/99] fix some Vector2 covers --- Sources/SwiftCovers/Vector2.covers.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftCovers/Vector2.covers.swift b/Sources/SwiftCovers/Vector2.covers.swift index 53fa34bf9..3c4e016ef 100644 --- a/Sources/SwiftCovers/Vector2.covers.swift +++ b/Sources/SwiftCovers/Vector2.covers.swift @@ -31,7 +31,7 @@ extension Vector2 { // } public func angle() -> Double { - return Double(atan2(x, y)) + return Double(atan2f(y, x)) } public static func fromAngle(_ angle: Double) -> Vector2 { @@ -66,7 +66,7 @@ extension Vector2 { } public func angleTo(_ to: Vector2) -> Double { - return atan2(cross(with: to), dot(with: to)) + return Double(atan2f(Float(cross(with: to)), Float(dot(with: to)))) } public func angleToPoint(to: Vector2) -> Double { @@ -98,8 +98,9 @@ extension Vector2 { } public func rotated(angle: Double) -> Vector2 { - let sin = Float(sin(angle)) - let cos = Float(cos(angle)) + let angle = Float(angle) + let sin = sinf(angle) + let cos = cosf(angle) return Vector2( x: x * cos - y * sin, y: x * sin + y * cos @@ -132,11 +133,12 @@ extension Vector2 { } public func limitLength(_ length: Double = 1.0) -> Vector2 { - let beforeLen = self.length() + let length = Float(length) + let beforeLen = Float(self.length()) var result = self - if (beforeLen > 0 && length < beforeLen) { - result = result / beforeLen - result = result * length + if beforeLen > 0 && length < beforeLen { + result = result / Double(beforeLen) + result = result * Double(length) } return result } From 7bfaab11ff81f3aaabc9f108abf48d226491dfec Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 16:33:40 -0600 Subject: [PATCH 87/99] Add Vector4 Cover Tests --- .../BuiltIn/Vector4CoverTests.swift | 173 ++++++++++++++++++ .../BuiltIn/Vector4iCoverTests.swift | 18 -- 2 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 Tests/SwiftGodotTests/BuiltIn/Vector4CoverTests.swift diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector4CoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector4CoverTests.swift new file mode 100644 index 000000000..84af27635 --- /dev/null +++ b/Tests/SwiftGodotTests/BuiltIn/Vector4CoverTests.swift @@ -0,0 +1,173 @@ +// +// File.swift +// SwiftGodot +// +// Created by Danny Youstra on 12/11/24. +// + +@testable import SwiftGodot +import SwiftGodotTestability +import XCTest + +@available(macOS 14, *) +extension Vector4 { + static func gen(_ coordinateGen: TinyGen) -> TinyGen { + return TinyGen { rng in + let left = rng.left() + let right = rng.right() + return Vector4( + x: coordinateGen(left.left()), + y: coordinateGen(left.right()), + z: coordinateGen(right.left()), + w: coordinateGen(right.right()) + ) + } + } + + static let mixed: TinyGen = gen(.mixedFloats) +} + +@available(macOS 14, *) +final class Vector4CoverTests: GodotTestCase { + + func testInit() { + forAll { + Vector4.mixed + } checkCover: { + Vector4.init(from: $0) + } + } + + // Vector4.method() + func testNullaryCovers() { + func checkMethod(_ method: (Vector4) -> () -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector4.mixed + } checkCover: { v in + method(v)() + } + } + + checkMethod(Vector4.abs) + checkMethod(Vector4.sign) + checkMethod(Vector4.floor) + checkMethod(Vector4.ceil) + checkMethod(Vector4.round) + checkMethod(Vector4.normalized) + } + + func testUnaryDoubleCovers() { + func checkMethod(_ method: (Vector4) -> (Double) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector4.mixed + TinyGen.mixedDoubles + } checkCover: { (v, d) in + method(v)(d) + } + } + + checkMethod(Vector4.snappedf) + } + + func testUnaryVector4Covers() { + func checkMethod(_ method: (Vector4) -> (Vector4) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector4.mixed + Vector4.mixed + } checkCover: { (v, d) in + method(v)(d) + } + } + + checkMethod(Vector4.dot) + } + + func testClamp() { + forAll { + Vector4.mixed + Vector4.mixed + Vector4.mixed + } checkCover: { + $0.clamp(min: $1, max: $2) + } + } + + func testClampf() { + forAll { + Vector4.mixed + TinyGen.mixedDoubles + TinyGen.mixedDoubles + } checkCover: { + $0.clampf(min: $1, max: $2) + } + } + + func testBinaryOperatorsVector4Vector4() { + func checkOperator( + _ op: (Vector4, Vector4) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector4.mixed + Vector4.mixed + } checkCover: { + op($0, $1) + } + } + + // Arithmetic + checkOperator(+) + checkOperator(-) + checkOperator(*) + checkOperator(/) + // Comparison + checkOperator(==) + checkOperator(!=) + checkOperator(<) + checkOperator(>) + checkOperator(<=) + checkOperator(>=) + } + + func testBinaryOperatorsVector4Int64() { + func checkOperator( + _ op: (Vector4, Int64) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector4.mixed + TinyGen.edgyInt64s + } checkCover: { + op($0, $1) + } + } + + checkOperator(*) + checkOperator(/) + } + + func testBinaryOperatorsVector4Double() { + func checkOperator( + _ op: (Vector4, Double) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Vector4.mixed + TinyGen.mixedDoubles + } checkCover: { + op($0, $1) + } + } + + checkOperator(*) + checkOperator(/) + } +} + + diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector4iCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector4iCoverTests.swift index c08911dd4..3f1206584 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector4iCoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector4iCoverTests.swift @@ -20,24 +20,6 @@ extension Vector4i { static let safe: TinyGen = gen(.safeInt32s) } -@available(macOS 14, *) -extension Vector4 { - static func gen(_ coordinateGen: TinyGen) -> TinyGen { - return TinyGen { rng in - let left = rng.left() - let right = rng.right() - return Vector4( - x: coordinateGen(left.left()), - y: coordinateGen(left.right()), - z: coordinateGen(right.left()), - w: coordinateGen(right.right()) - ) - } - } - - static let mixed: TinyGen = gen(.mixedFloats) -} - @available(macOS 14, *) final class Vector4iCoverTests: GodotTestCase { From c92644f8759e0060d66943f224007689328a507b Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 16:33:53 -0600 Subject: [PATCH 88/99] Add Transform2DCoverTests --- .../BuiltIn/Transform2DCoverTests.swift | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 Tests/SwiftGodotTests/BuiltIn/Transform2DCoverTests.swift diff --git a/Tests/SwiftGodotTests/BuiltIn/Transform2DCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Transform2DCoverTests.swift new file mode 100644 index 000000000..c787cd3c3 --- /dev/null +++ b/Tests/SwiftGodotTests/BuiltIn/Transform2DCoverTests.swift @@ -0,0 +1,206 @@ +// +// File.swift +// SwiftGodot +// +// Created by Danny Youstra on 12/11/24. +// + +@testable import SwiftGodot +import SwiftGodotTestability +import XCTest + +@available(macOS 14, *) +extension Transform2D { + static func gen(_ coordinateGen: TinyGen) -> TinyGen { + return TinyGen { rng in + let left = rng.left() + return Transform2D(xAxis: coordinateGen(left.left()), + yAxis: coordinateGen(left.right()), + origin: coordinateGen(rng.right()) + ) + } + } + + static let mixed: TinyGen = gen(Vector2.mixed) +} + + +@available(macOS 14, *) +final class Transform2DCoverTests: GodotTestCase { + + func testInit() { + forAll { + Transform2D.mixed + } checkCover: { + Transform2D.init(from: $0) + } + } + + func testInitFloatVector2() { + forAll { + TinyGen.mixedFloats + Vector2.mixed + } checkCover: { + Transform2D.init(rotation: $0, position: $1) + } + } + + func testInitFloatVector2FloatVector2() { + forAll { + TinyGen.mixedFloats + Vector2.mixed + TinyGen.mixedFloats + Vector2.mixed + } checkCover: { + Transform2D.init(rotation: $0, scale: $1, skew: $2, position: $3) + } + } + + // Transform2D.method() + func testNullaryCovers() { + func checkMethod(_ method: (Transform2D) -> () -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Transform2D.mixed + } checkCover: { t in + method(t)() + } + } + + checkMethod(Transform2D.inverse) + checkMethod(Transform2D.affineInverse) + checkMethod(Transform2D.getSkew) + checkMethod(Transform2D.getRotation) + checkMethod(Transform2D.getOrigin) + checkMethod(Transform2D.orthonormalized) + checkMethod(Transform2D.determinant) + checkMethod(Transform2D.isFinite) + } + + // Transform2D.method(Vector2) + func testUnaryVector2Covers() { + func checkMethod(_ method: (Transform2D) -> (Vector2) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Transform2D.mixed + Vector2.mixed + } checkCover: { (t, v) in + method(t)(v) + } + } + + checkMethod(Transform2D.scaled) + checkMethod(Transform2D.scaledLocal) + checkMethod(Transform2D.translated) + checkMethod(Transform2D.translatedLocal) + checkMethod(Transform2D.basisXform) + checkMethod(Transform2D.basisXformInv) + checkMethod(Transform2D.lookingAt) + } + + // Transform2D.method(Double) + func testUnaryDoubleCovers() { + func checkMethod(_ method: (Transform2D) -> (Double) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Transform2D.mixed + TinyGen.mixedDoubles + } checkCover: { (t, d) in + method(t)(d) + } + } + + checkMethod(Transform2D.rotated) + checkMethod(Transform2D.rotatedLocal) + } + + func testInterpolateWith() { + forAll { + Transform2D.mixed + Transform2D.mixed + TinyGen.mixedDoubles + } checkCover: { + $0.interpolateWith(xform: $1, weight: $2) + } + } + + func testSubscriptGet() { + forAll { + Transform2D.mixed + TinyGen.edgyInt64s + } checkCover: { (t, i) in + var mutT = t + return mutT[i] + } + } + + func testSubscriptSet() { + forAll { + Transform2D.mixed + TinyGen.oneOf(values: Vector3.Axis.allCases) + Vector2.mixed + } checkCover: { (t, index, val) in + var mutT = t + mutT[index.rawValue] = val + return mutT + } + } + + func testBinaryOperatorsTransform2DTransform2D() { + func checkOperator( + _ op: (Transform2D, Transform2D) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Transform2D.mixed + Transform2D.mixed + } checkCover: { + op($0, $1) + } + } + + // Arithmetic + checkOperator(*) + // Comparison + checkOperator(==) + checkOperator(!=) + } + + func testBinaryOperatorsTransform2DDouble() { + func checkOperator( + _ op: (Transform2D, Double) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Transform2D.mixed + TinyGen.mixedDoubles + } checkCover: { + op($0, $1) + } + } + + checkOperator(*) + checkOperator(/) + } + + func testBinaryOperatorsTransform2DInt64() { + func checkOperator( + _ op: (Transform2D, Int64) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Transform2D.mixed + TinyGen.edgyInt64s + } checkCover: { + op($0, $1) + } + } + + checkOperator(*) + checkOperator(/) + } + +} From 532f327511619e5603f0d0ebb63460fc0468fefa Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 16:57:00 -0600 Subject: [PATCH 89/99] Add Transform3DCoverTests --- .../BuiltIn/Transform2DCoverTests.swift | 2 +- .../BuiltIn/Transform3DCoverTests.swift | 191 ++++++++++++++++++ 2 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 Tests/SwiftGodotTests/BuiltIn/Transform3DCoverTests.swift diff --git a/Tests/SwiftGodotTests/BuiltIn/Transform2DCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Transform2DCoverTests.swift index c787cd3c3..1010046de 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Transform2DCoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Transform2DCoverTests.swift @@ -1,5 +1,5 @@ // -// File.swift +// Transform2DCoverTests.swift // SwiftGodot // // Created by Danny Youstra on 12/11/24. diff --git a/Tests/SwiftGodotTests/BuiltIn/Transform3DCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Transform3DCoverTests.swift new file mode 100644 index 000000000..84f1c5c4b --- /dev/null +++ b/Tests/SwiftGodotTests/BuiltIn/Transform3DCoverTests.swift @@ -0,0 +1,191 @@ +// +// Transform3DCoverTests.swift +// SwiftGodot +// +// Created by Danny Youstra on 12/11/24. +// + +@testable import SwiftGodot +import SwiftGodotTestability +import XCTest + +@available(macOS 14, *) +extension Transform3D { + static func gen(_ coordinateGen: TinyGen) -> TinyGen { + return TinyGen { rng in + let left = rng.left() + let right = rng.right() + return Transform3D(xAxis: coordinateGen(left.left()), + yAxis: coordinateGen(left.right()), + zAxis: coordinateGen(right.left()), + origin: coordinateGen(right.right()) + ) + } + } + + static let mixed: TinyGen = gen(Vector3.mixed) +} + +@available(macOS 14, *) +final class Transform3DCoverTests: GodotTestCase { + + func testInit() { + forAll { + Transform3D.mixed + } checkCover: { + Transform3D.init(from: $0) + } + } + + func testInitV3V3V3V3() { + forAll { + Vector3.mixed + Vector3.mixed + Vector3.mixed + Vector3.mixed + } checkCover: { + Transform3D.init(xAxis: $0, yAxis: $1, zAxis: $2, origin: $3) + } + } + + // Transform3D.method() + func testNullaryCovers() { + func checkMethod(_ method: (Transform3D) -> () -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Transform3D.mixed + } checkCover: { t in + method(t)() + } + } + + checkMethod(Transform3D.inverse) + checkMethod(Transform3D.affineInverse) + checkMethod(Transform3D.orthonormalized) + checkMethod(Transform3D.isFinite) + } + + // Transform3D.method(Vector3) + func testUnaryVector3Covers() { + func checkMethod(_ method: (Transform3D) -> (Vector3) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Transform3D.mixed + Vector3.mixed + } checkCover: { (t, v) in + method(t)(v) + } + } + + checkMethod(Transform3D.scaled) + checkMethod(Transform3D.scaledLocal) + checkMethod(Transform3D.translated) + checkMethod(Transform3D.translatedLocal) + } + + // Transform3D.method(Vector3, Double) + func testBinaryVector3DoubleCovers() { + func checkMethod(_ method: (Transform3D) -> (Vector3, Double) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Transform3D.mixed + Vector3.mixed + TinyGen.mixedDoubles + } checkCover: { (t, v, d) in + method(t)(v,d) + } + } + + checkMethod(Transform3D.rotated) + checkMethod(Transform3D.rotatedLocal) + } + + func testLookingAt() { + forAll { + Transform3D.mixed + Vector3.mixed + Vector3.mixed + } checkCover: { + $0.lookingAt(target: $1, up: $2) + } + } + + func testInterpolateWith() { + forAll { + Transform3D.mixed + Transform3D.mixed + TinyGen.mixedDoubles + } checkCover: { + $0.interpolateWith(xform: $1, weight: $2) + } + } + + // Operators + + func testBinaryOperatorsTransform3DTransform3D() { + func checkOperator( + _ op: (Transform3D, Transform3D) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Transform3D.mixed + Transform3D.mixed + } checkCover: { + op($0, $1) + } + } + + // Arithmetic + checkOperator(*) + // Comparison + checkOperator(==) + checkOperator(!=) + } + + func testBinaryOperatorsTransform3DDouble() { + func checkOperator( + _ op: (Transform3D, Double) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Transform3D.mixed + TinyGen.mixedDoubles + } checkCover: { + op($0, $1) + } + } + + checkOperator(*) + checkOperator(/) + } + + func testBinaryOperatorsTransform3DInt64() { + func checkOperator( + _ op: (Transform3D, Int64) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Transform3D.mixed + TinyGen.edgyInt64s + } checkCover: { + op($0, $1) + } + } + + checkOperator(*) + checkOperator(/) + } + + func testBinaryOperatorMultiplyVector3() { + forAll { + Transform3D.mixed + Vector3.mixed + } checkCover: { + $0 * $1 + } + } + +} From 2d5beec97209b2adcbf88f699a13bf841bd789c5 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 21:21:25 -0600 Subject: [PATCH 90/99] Add methods to fix V3 tests --- Sources/SwiftCovers/Vector3.covers.swift | 27 ++++++++++++++++--- .../BuiltIn/Vector3CoverTests.swift | 4 +++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftCovers/Vector3.covers.swift b/Sources/SwiftCovers/Vector3.covers.swift index 4486496fd..7bd647189 100644 --- a/Sources/SwiftCovers/Vector3.covers.swift +++ b/Sources/SwiftCovers/Vector3.covers.swift @@ -37,6 +37,10 @@ extension Vector3 { return Double(x * with.x + y * with.y + z * with.z) } + public func distanceTo(_ to: Vector3) -> Double { + return Double((to - self).length()) + } + public func abs() -> Vector3 { return Vector3( x: Swift.abs(x), @@ -77,6 +81,22 @@ extension Vector3 { ) } + public func length() -> Double { + let x2 = x * x + let y2 = y * y + let z2 = z * z + + return Double(sqrt(x2 + y2 + z2)) + } + + public func lengthSquared() -> Double { + let x2 = x * x + let y2 = y * y + let z2 = z * z + + return Double(x2 + y2 + z2) + } + public func slerp(to: Vector3, weight: Double) -> Vector3 { // This method seems more complicated than it really is, since we write out // the internals of some methods for efficiency (mainly, checking length). @@ -142,9 +162,10 @@ extension Vector3 { } public func moveToward(to: Vector3, delta: Double) -> Vector3 { - let result = to - self - let newLen = result.length() - return newLen <= delta || newLen < CMP_EPSILON ? to : self + result / newLen * delta + let v = self + let vd = to - v + let len = vd.length() + return len <= delta || len < CMP_EPSILON ? to : v + vd / len * delta } public func slide(n: Vector3) -> Vector3 { diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift index d870845f1..bbbb5f4e0 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift @@ -64,6 +64,9 @@ final class Vector3CoverTests: GodotTestCase { checkMethod(Vector3.floor) checkMethod(Vector3.ceil) checkMethod(Vector3.round) + checkMethod(Vector3.length) + checkMethod(Vector3.lengthSquared) + checkMethod(Vector3.round) checkMethod(Vector3.normalized) checkMethod(Vector3.octahedronEncode) } @@ -84,6 +87,7 @@ final class Vector3CoverTests: GodotTestCase { checkMethod(Vector3.cross) checkMethod(Vector3.dot) checkMethod(Vector3.outer) + checkMethod(Vector3.distanceTo(_:)) } // Vector3.method(Vector3) From 36a8de2b5c4c48758eb89b4211f11a6cd88e520e Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 21:58:49 -0600 Subject: [PATCH 91/99] Fix Transform2D subscript cover --- Sources/SwiftCovers/Transform2D.covers.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftCovers/Transform2D.covers.swift b/Sources/SwiftCovers/Transform2D.covers.swift index da5ced494..de3b0e2bf 100644 --- a/Sources/SwiftCovers/Transform2D.covers.swift +++ b/Sources/SwiftCovers/Transform2D.covers.swift @@ -189,7 +189,22 @@ extension Transform2D { } public subscript(index: Int64) -> Vector2 { - return index == 0 ? x : index == 1 ? y : origin + get { + switch index { + case 0: return x + case 1: return y + case 2: return origin + default: return Vector2.zero + } + } + set { + switch index { + case 0: x = newValue + case 1: y = newValue + case 2: origin = newValue + default: fatalError("Invalid index") + } + } } // Operators From 852f891769f09b8107d55c44282c9ecb5fb12411 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Wed, 11 Dec 2024 23:31:45 -0600 Subject: [PATCH 92/99] Transform2D fix testInit --- .../BuiltIn/Transform2DCoverTests.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Tests/SwiftGodotTests/BuiltIn/Transform2DCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Transform2DCoverTests.swift index 1010046de..124c44388 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Transform2DCoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Transform2DCoverTests.swift @@ -46,13 +46,15 @@ final class Transform2DCoverTests: GodotTestCase { } func testInitFloatVector2FloatVector2() { - forAll { - TinyGen.mixedFloats - Vector2.mixed - TinyGen.mixedFloats - Vector2.mixed - } checkCover: { - Transform2D.init(rotation: $0, scale: $1, skew: $2, position: $3) + Float.$closeEnoughUlps.withValue(2) { + forAll { + TinyGen.mixedFloats + Vector2.mixed + TinyGen.mixedFloats + Vector2.mixed + } checkCover: { + Transform2D.init(rotation: $0, scale: $1, skew: $2, position: $3) + } } } @@ -68,9 +70,9 @@ final class Transform2DCoverTests: GodotTestCase { } } + checkMethod(Transform2D.getSkew) checkMethod(Transform2D.inverse) checkMethod(Transform2D.affineInverse) - checkMethod(Transform2D.getSkew) checkMethod(Transform2D.getRotation) checkMethod(Transform2D.getOrigin) checkMethod(Transform2D.orthonormalized) From 92adcb6b71138d69eca5216818bd19a07a87f20b Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 10 Dec 2024 20:53:19 -0600 Subject: [PATCH 93/99] pass className GD instead of Godot for cover lookup --- Generator/Generator/UtilityGen.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Generator/Generator/UtilityGen.swift b/Generator/Generator/UtilityGen.swift index a0396415c..8270fc956 100644 --- a/Generator/Generator/UtilityGen.swift +++ b/Generator/Generator/UtilityGen.swift @@ -27,7 +27,7 @@ func generateUtility(values: [JGodotUtilityFunction], outputDir: String?) async } performExplaniningNonCriticalErrors { - _ = try generateMethod (p, method: method, className: "Godot", cdef: nil, usedMethods: emptyUsedMethods, generatedMethodKind: .utilityFunction, asSingleton: false) + _ = try generateMethod (p, method: method, className: "GD", cdef: nil, usedMethods: emptyUsedMethods, generatedMethodKind: .utilityFunction, asSingleton: false) } } } From fbb0bf1bb3180473c625e4e2c7cb84b764b0432f Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 10 Dec 2024 20:53:19 -0600 Subject: [PATCH 94/99] use cover if available for class method --- Generator/Generator/MethodGen.swift | 204 +++++++++++++++------------- 1 file changed, 107 insertions(+), 97 deletions(-) diff --git a/Generator/Generator/MethodGen.swift b/Generator/Generator/MethodGen.swift index 519d96163..2dd9f75a5 100644 --- a/Generator/Generator/MethodGen.swift +++ b/Generator/Generator/MethodGen.swift @@ -746,113 +746,123 @@ func generateMethod(_ p: Printer, method: MethodDefinition, className: String, c } p ("\(declarationTokens)(\(argumentsList))\(returnClause)") { - if method.optionalHash == nil { - if let godotReturnType { - p(makeDefaultReturn(godotType: godotReturnType)) - } - } else { - if returnType != "" { - p(returnTypeDecl()) - } else if (method.isVararg) { - p("var _result: Variant.ContentType = Variant.zero") - } - - let instanceArg: String - if method.isStatic { - instanceArg = "nil" + let parameterTypes = arguments.map { getGodotType(SimpleType(type: $0.type)) } + let key = SwiftCovers.Key( + type: className, + name: swiftMethodName, + parameterTypes: parameterTypes, + returnType: returnType.isEmpty ? "Void" : returnType, + isStatic: staticAttribute != nil + ) + p.useSwiftCoverIfAvailable(for: key) { + if method.optionalHash == nil { + if let godotReturnType { + p(makeDefaultReturn(godotType: godotReturnType)) + } } else { - let accessor: String - if asSingleton { - accessor = "shared.handle" - } else { - accessor = "handle" + if returnType != "" { + p(returnTypeDecl()) + } else if (method.isVararg) { + p("var _result: Variant.ContentType = Variant.zero") } - instanceArg = "UnsafeMutableRawPointer(mutating: \(accessor))" - } - - func getMethodNameArgument() -> String { - assert(generatedMethodKind == .classMethod) - - if staticAttribute == nil { - return "\(className).method_\(method.name)" + + let instanceArg: String + if method.isStatic { + instanceArg = "nil" } else { - return "method_\(method.name)" + let accessor: String + if asSingleton { + accessor = "shared.handle" + } else { + accessor = "handle" + } + instanceArg = "UnsafeMutableRawPointer(mutating: \(accessor))" } - } - - generateMethodCall(p, isVariadic: method.isVararg, arguments: arguments, methodArguments: methodArguments) { argsRef, count in - if method.isVararg { - switch generatedMethodKind { - case .classMethod: - let countArg: String - - switch count { - case .literal(let literal): - countArg = "\(literal)" - case .expression(let expr): - countArg = "Int64(\(expr))" - } - - let argsList = [ - getMethodNameArgument(), - instanceArg, - argsRef, - countArg, - getCallResultArgument(), - "nil" - ].joined(separator: ", ") - - return "gi.object_method_bind_call(\(argsList))" - case .utilityFunction: - let countArg: String - - switch count { - case .literal(let literal): - countArg = "\(literal)" - case .expression(let expr): - countArg = "Int32(\(expr))" - } - - let argsList = [ - getCallResultArgument(), - argsRef, - countArg - ].joined(separator: ", ") - - return "method_\(method.name)(\(argsList))" + + func getMethodNameArgument() -> String { + assert(generatedMethodKind == .classMethod) + + if staticAttribute == nil { + return "\(className).method_\(method.name)" + } else { + return "method_\(method.name)" } - } else { - switch generatedMethodKind { - case .classMethod: - guard case .literal = count else { - fatalError("Literal is expected") + } + + generateMethodCall(p, isVariadic: method.isVararg, arguments: arguments, methodArguments: methodArguments) { argsRef, count in + if method.isVararg { + switch generatedMethodKind { + case .classMethod: + let countArg: String + + switch count { + case .literal(let literal): + countArg = "\(literal)" + case .expression(let expr): + countArg = "Int64(\(expr))" + } + + let argsList = [ + getMethodNameArgument(), + instanceArg, + argsRef, + countArg, + getCallResultArgument(), + "nil" + ].joined(separator: ", ") + + return "gi.object_method_bind_call(\(argsList))" + case .utilityFunction: + let countArg: String + + switch count { + case .literal(let literal): + countArg = "\(literal)" + case .expression(let expr): + countArg = "Int32(\(expr))" + } + + let argsList = [ + getCallResultArgument(), + argsRef, + countArg + ].joined(separator: ", ") + + return "method_\(method.name)(\(argsList))" } - - let argsList = [ - getMethodNameArgument(), - instanceArg, - argsRef, - getCallResultArgument() - ].joined(separator: ", ") - - return "gi.object_method_bind_ptrcall(\(argsList))" - case .utilityFunction: - guard case let .literal(count) = count else { - fatalError("Literal is expected") + } else { + switch generatedMethodKind { + case .classMethod: + guard case .literal = count else { + fatalError("Literal is expected") + } + + let argsList = [ + getMethodNameArgument(), + instanceArg, + argsRef, + getCallResultArgument() + ].joined(separator: ", ") + + return "gi.object_method_bind_ptrcall(\(argsList))" + case .utilityFunction: + guard case let .literal(count) = count else { + fatalError("Literal is expected") + } + + let argsList = [ + getCallResultArgument(), + argsRef, + "\(count)" // just a literal, no need to convert to Int32 + ].joined(separator: ", ") + + return "method_\(method.name)(\(argsList))" } - - let argsList = [ - getCallResultArgument(), - argsRef, - "\(count)" // just a literal, no need to convert to Int32 - ].joined(separator: ", ") - - return "method_\(method.name)(\(argsList))" } } + + p(getReturnStatement()) } - - p(getReturnStatement()) } } return registerVirtualMethodName From 4cbbbf5cbf6e64dff2948edd57d40a6b39f862eb Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Tue, 10 Dec 2024 20:53:19 -0600 Subject: [PATCH 95/99] write some GD covers --- Sources/CWrappers/include/CWrappers.h | 3 ++ Sources/SwiftCovers/utility.covers.swift | 24 ++++++++++++++ Sources/SwiftGodot/SwiftCoverSupport.swift | 7 ++++ Tests/SwiftGodotTests/UtilityCoverTests.swift | 33 +++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 Sources/SwiftCovers/utility.covers.swift create mode 100644 Tests/SwiftGodotTests/UtilityCoverTests.swift diff --git a/Sources/CWrappers/include/CWrappers.h b/Sources/CWrappers/include/CWrappers.h index b7ec9517f..fea007318 100644 --- a/Sources/CWrappers/include/CWrappers.h +++ b/Sources/CWrappers/include/CWrappers.h @@ -9,6 +9,9 @@ static inline int32_t int32_for_float(float f) { return f; } /// - returns: `d`, cast to `int32_t`. static inline int32_t int32_for_double(double d) { return d; } +/// - returns: `d`, cast to `int64_t`. +static inline int64_t int64_for_double(double d) { return d; } + /// - returns: `n / d`. static inline int32_t int32_divide(int32_t n, int32_t d) { return n / d; } diff --git a/Sources/SwiftCovers/utility.covers.swift b/Sources/SwiftCovers/utility.covers.swift new file mode 100644 index 000000000..5ac63aa53 --- /dev/null +++ b/Sources/SwiftCovers/utility.covers.swift @@ -0,0 +1,24 @@ +@_spi(SwiftCovers) import SwiftGodot + +extension GD { + + public static func signf(x: Double) -> Double { + return x > 0 ? 1 : x < 0 ? -1 : 0 + } + + public static func signi(x: Int64) -> Int64 { + return x.signum() + } + + public static func snappedi(x: Double, step: Int64) -> Int64 { + let answer: Double + if step == 0 { + answer = x + } else { + let step = Double(step) + answer = (x / step + 0.5).rounded(.down) * step; + } + return cCastToInt64(answer) + } + +} diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index d9f7455b9..f7493f2f2 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -16,6 +16,13 @@ public func cCastToInt32(_ double: Double) -> Int32 { return int32_for_double(double) } +/// The Swift standard library offers no efficient way to cast a `Double` to an `Int64` with the same semantics as C and C++. This method calls an imported inlinable C function. +@_spi(SwiftCovers) +@inline(__always) +public func cCastToInt64(_ double: Double) -> Int64 { + return int64_for_double(double) +} + /// The Swift standard library offers no efficient way to divide an `Int32` by an `Int32` with the same semantics as C and C++. This method calls an imported inlinable C function. @_spi(SwiftCovers) @inline(__always) diff --git a/Tests/SwiftGodotTests/UtilityCoverTests.swift b/Tests/SwiftGodotTests/UtilityCoverTests.swift new file mode 100644 index 000000000..d26895313 --- /dev/null +++ b/Tests/SwiftGodotTests/UtilityCoverTests.swift @@ -0,0 +1,33 @@ +import SwiftGodot +import SwiftGodotTestability +import XCTest + +@available(macOS 14, *) +final class UtilityCoverTests: GodotTestCase { + + func testSignf() { + forAll { + TinyGen.mixedDoubles + } checkCover: { + GD.signf(x: $0) + } + } + + func testSigni() { + forAll { + TinyGen.edgyInt64s + } checkCover: { + GD.signi(x: $0) + } + } + + func testSnappedi() { + forAll { + TinyGen.mixedDoubles + TinyGen.edgyInt64s + } checkCover: { + GD.snappedi(x: $0, step: $1) + } + } + +} From ea70e26e73cb3eb953d603ade8eb71962d57477d Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Wed, 11 Dec 2024 12:13:06 -0600 Subject: [PATCH 96/99] write Basis covers --- Sources/SwiftCovers/Basis.covers.swift | 164 ++++++++++++++++-- Sources/SwiftGodot/Extensions/GD+Utils.swift | 4 + Sources/SwiftGodot/SwiftCoverSupport.swift | 6 + .../SwiftGodotTestability/GodotTestCase.swift | 2 +- .../BuiltIn/BasisCoverTests.swift | 162 +++++++++++++++++ 5 files changed, 325 insertions(+), 13 deletions(-) create mode 100644 Tests/SwiftGodotTests/BuiltIn/BasisCoverTests.swift diff --git a/Sources/SwiftCovers/Basis.covers.swift b/Sources/SwiftCovers/Basis.covers.swift index aeb285a5c..09df19fe5 100644 --- a/Sources/SwiftCovers/Basis.covers.swift +++ b/Sources/SwiftCovers/Basis.covers.swift @@ -25,15 +25,11 @@ extension Basis { public init(axis: Vector3, angle: Float) { // Rotation matrix from axis and angle, see https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_angle // The axis Vector3 should be normalized. + + let axisSq = axis * axis - let axisSq = Vector3( - x: axis.x * axis.x, - y: axis.y * axis.y, - z: axis.z * axis.z - ) - - let cosine = cos(angle) - let sine = sin(angle) + let cosine = cosf(angle) + let sine = sinf(angle) let t = 1.0 - cosine // Diagonals @@ -59,10 +55,154 @@ extension Basis { // Column vectors to create Basis self.init( - xAxis: Vector3(x: xx, y: yx, z: zx), - yAxis: Vector3(x: xy, y: yy, z: zy), - zAxis: Vector3(x: xz, y: yz, z: zz) + xAxis: Vector3(x: xx, y: xy, z: xz), + yAxis: Vector3(x: yx, y: yy, z: yz), + zAxis: Vector3(x: zx, y: zy, z: zz) ) } - + + public init(from: Basis) { + self = from + } + + public init(from: Quaternion) { + let d = Float(from.lengthSquared()) + let s = 2 / d + let xs = from.x * s, ys = from.y * s, zs = from.z * s + let wx = from.w * xs, wy = from.w * ys, wz = from.w * zs + let xx = from.x * xs, xy = from.x * ys, xz = from.x * zs + let yy = from.y * ys, yz = from.y * zs, zz = from.z * zs + self.init( + xAxis: Vector3(x: 1 - (yy + zz), y: xy - wz, z: xz + wy), + yAxis: Vector3(x: xy + wz, y: 1 - (xx + zz), z: yz - wx), + zAxis: Vector3(x: xz - wy, y: yz + wx, z: 1 - (xx + yy)) + ) + } + + public func inverse() -> Basis { + var cols = self + + func cofac(_ r1: Int64, _ c1: Int64, _ r2: Int64, _ c2: Int64) -> Float { + return Float(cols[c1][r1]) * Float(cols[c2][r2]) - Float(cols[c2][r1]) * Float(cols[c1][r2]) + } + + let co = (cofac(1, 1, 2, 2), cofac(1, 2, 2, 0), cofac(1, 0, 2, 1)) + + let det = Float(cols[0][0]) * co.0 + Float(cols[1][0]) * co.1 + Float(cols[2][0]) * co.2 + + let s = 1 / det + + return Basis( + xAxis: Vector3(x: co.0 * s, y: cofac(0, 2, 2, 1) * s, z: cofac(0, 1, 1, 2) * s), + yAxis: Vector3(x: co.1 * s, y: cofac(0, 0, 2, 2) * s, z: cofac(0, 2, 1, 0) * s), + zAxis: Vector3(x: co.2 * s, y: cofac(0, 1, 2, 0) * s, z: cofac(0, 0, 1, 1) * s) + ) + } + + public func transposed() -> Basis { + var answer = self + swap(&answer.x.y, &answer.y.x) + swap(&answer.x.z, &answer.z.x) + swap(&answer.y.z, &answer.z.y) + return answer + } + + public func orthonormalized() -> Basis { + // Gram-Schmidt Process + + var x = Vector3(x: self.x.x, y: self.y.x, z: self.z.x) + var y = Vector3(x: self.x.y, y: self.y.y, z: self.z.y) + var z = Vector3(x: self.x.z, y: self.y.z, z: self.z.z) + + x = x.normalized() + y = y - x * x.dot(with: y) + y = y.normalized() + z = (z - x * x.dot(with: z)) - y * y.dot(with: z) + z = z.normalized() + + return Basis( + xAxis: Vector3(x: x.x, y: y.x, z: z.x), + yAxis: Vector3(x: x.y, y: y.y, z: z.y), + zAxis: Vector3(x: x.z, y: y.z, z: z.z) + ) + } + + public func determinant() -> Double { + var me = self + let minor0 = Float(me[0][0]) * (Float(me[1][1]) * Float(me[2][2]) - Float(me[1][2]) * Float(me[2][1])) + let minor1 = Float(me[0][1]) * (Float(me[1][0]) * Float(me[2][2]) - Float(me[1][2]) * Float(me[2][0])) + let minor2 = Float(me[0][2]) * (Float(me[1][0]) * Float(me[2][1]) - Float(me[1][1]) * Float(me[2][0])) + return Double(minor0 - minor1 + minor2) + } + + public func rotated(axis: Vector3, angle: Double) -> Basis { + return Basis(axis: axis, angle: Float(angle)) * self + } + + public func scaled(scale: Vector3) -> Basis { + var answer = self + answer.x.x *= scale.x + answer.x.y *= scale.x + answer.x.z *= scale.x + answer.y.x *= scale.y + answer.y.y *= scale.y + answer.y.z *= scale.y + answer.z.x *= scale.z + answer.z.y *= scale.z + answer.z.z *= scale.z + return answer + } + + public func getScale() -> Vector3 { + func axisScale(_ axis: KeyPath) -> Float { + func square(_ n: Float) -> Float { n * n } + return (square(x[keyPath: axis]) + square(y[keyPath: axis]) + square(z[keyPath: axis])).squareRoot() + } + let detSign = Float(sign(determinant())) + return Vector3( + x: detSign * axisScale(\.x), + y: detSign * axisScale(\.y), + z: detSign * axisScale(\.z) + ) + } + + // public func getEuler(order: Int64 = 2) -> Vector3 + // Omitted because it's six ugly cases and probably dominated by atan2 anyway. + + public func tdotx(with: Vector3) -> Double { + return Double(x.x * with.x + y.x * with.y + z.x * with.z) + } + + public func tdoty(with: Vector3) -> Double { + return Double(x.y * with.x + y.y * with.y + z.y * with.z) + } + + public func tdotz(with: Vector3) -> Double { + return Double(x.z * with.x + y.z * with.y + z.z * with.z) + } + + public func slerp(to: Basis, weight: Double) -> Basis { + let qFrom = Quaternion(from: self) + let qTo = Quaternion(from: to) + var b = Basis(from: qFrom.slerp(to: qTo, weight: weight)) + b.x *= Double(Float(self.x.length()).lerp(to: Float(to.x.length()), weight: weight)) + b.y *= Double(Float(self.y.length()).lerp(to: Float(to.y.length()), weight: weight)) + b.z *= Double(Float(self.z.length()).lerp(to: Float(to.z.length()), weight: weight)) + return b + } + + public func isConformal() -> Bool { + let x = Vector3(x: self.x.x, y: self.y.x, z: self.z.x) + let y = Vector3(x: self.x.y, y: self.y.y, z: self.z.y) + let z = Vector3(x: self.x.z, y: self.y.z, z: self.z.z) + let xLengthSquared = Float(x.lengthSquared()) + return ( + GD.isEqualApprox(xLengthSquared, Float(y.lengthSquared())) + && GD.isEqualApprox(xLengthSquared, Float(z.lengthSquared())) + && GD.isZeroApprox(Float(x.dot(with: y))) + && GD.isZeroApprox(Float(x.dot(with: z))) + && GD.isZeroApprox(Float(y.dot(with: z))) + ) + } + } diff --git a/Sources/SwiftGodot/Extensions/GD+Utils.swift b/Sources/SwiftGodot/Extensions/GD+Utils.swift index 437673af5..a3f072462 100644 --- a/Sources/SwiftGodot/Extensions/GD+Utils.swift +++ b/Sources/SwiftGodot/Extensions/GD+Utils.swift @@ -144,6 +144,10 @@ extension GD { return (a - b).magnitude < tolerance } + public static func isZeroApprox(_ s: Float) -> Bool { + return s.magnitude < Float(CMP_EPSILON) + } + public static func cubicInterpolate(from: Float, to: Float, pre: Float, post: Float, weight: Float) -> Float { let constTerm = 2 * from let linearTerm = (-pre + to) * weight diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index f7493f2f2..37fdd8967 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -65,6 +65,12 @@ extension Vector4i { public var tuple: (Int32, Int32, Int32, Int32) { (x, y, z, w) } } +extension Vector3 { + @_spi(SwiftCovers) + @inline(__always) + public var simd: SIMD3 { SIMD3(x, y, z) } +} + extension Plane { @_spi(SwiftCovers) @inline(__always) diff --git a/Sources/SwiftGodotTestability/GodotTestCase.swift b/Sources/SwiftGodotTestability/GodotTestCase.swift index ea42274ff..789437732 100644 --- a/Sources/SwiftGodotTestability/GodotTestCase.swift +++ b/Sources/SwiftGodotTestability/GodotTestCase.swift @@ -222,7 +222,7 @@ extension GodotTestCase { #if TESTABLE_SWIFT_COVERS let gen = build() - for k: UInt64 in 1 ... 1_000 { + for k: UInt64 in 204 ... 1_000 { var rng = SipRNG(key0: k, key1: 1234) // Mix in the function name so every test that starts by asking for, say, a Plane doesn't get the same Plane. function.withUTF8Buffer { buffer in diff --git a/Tests/SwiftGodotTests/BuiltIn/BasisCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/BasisCoverTests.swift new file mode 100644 index 000000000..7ff624e3c --- /dev/null +++ b/Tests/SwiftGodotTests/BuiltIn/BasisCoverTests.swift @@ -0,0 +1,162 @@ +@testable import SwiftGodot +import SwiftGodotTestability +import XCTest + +@available(macOS 14, *) +extension Basis { + static func gen(_ coordinateGen: TinyGen) -> TinyGen { + TinyGenBuilder { + Vector3.gen(coordinateGen) + Vector3.gen(coordinateGen) + Vector3.gen(coordinateGen) + }.map { Basis(xAxis: $0, yAxis: $1, zAxis: $2) } + } + + static let mixedGen = gen(TinyGen.mixedFloats) + + static let rotationGen = TinyGenBuilder { + Vector3.normalizedGen + TinyGen.gaussianFloats + }.map { Basis(axis: $0, angle: $1) } + + static let scaleRotationGen = TinyGenBuilder { + rotationGen + Vector3.gen(TinyGen.gaussianFloats.map { exp($0 * 0.1) }) + }.map { $0.scaled(scale: $1) } +} + +@available(macOS 14, *) +final class BasisCoverTests: GodotTestCase { + + func testInitAxisAngle() { + Float.$closeEnoughUlps.withValue(250) { + forAll { + Vector3.normalizedGen + TinyGen.mixedFloats + } checkCover: { + Basis(axis: $0, angle: $1) + } + } + } + + func testInitFromBasis() { + forAll { + Basis.mixedGen + } checkCover: { + Basis(from: $0) + } + } + + func testInitFromQuaternion() { + forAll { + Quaternion.mixedGen + } checkCover: { + Basis(from: $0) + } + } + + func testInverse() { + forAll { + Basis.scaleRotationGen + } checkCover: { + $0.inverse() + } + } + + func testNullaryCovers() { + // Methods of the form basis.method(). + + func checkMethod( + _ method: (Basis) -> () -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Basis.mixedGen + } checkCover: { + method($0)() + } + } + + checkMethod(Basis.transposed) + checkMethod(Basis.orthonormalized) + checkMethod(Basis.determinant) + checkMethod(Basis.getScale) + checkMethod(Basis.isFinite) + + func checkScaleRotationMethod( + _ method: (Basis) -> () -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Basis.scaleRotationGen + } checkCover: { + method($0)() + } + } + + checkScaleRotationMethod(Basis.getRotationQuaternion) + } + + func testIsConformal() { + forAll { + TinyGen.oneOf(gens: [ + // These will generally not be conformal. + Basis.mixedGen, + + // These will generally be conformal. + TinyGenBuilder { + Basis.rotationGen + TinyGen.gaussianFloats.map { $0 * 0.001 } + }.map { $0.scaled(scale: Vector3(x: $1, y: $1, z: $1)) } + ]) + } checkCover: { + $0.isConformal() + } + } + + func testRotated() { + Float.$closeEnoughUlps.withValue(640) { + forAll { + Basis.mixedGen + Vector3.normalizedGen // axis + TinyGen.mixedDoubles // angle + } checkCover: { + $0.rotated(axis: $1, angle: $2) + } + } + } + + func testUnaryMethod_Vector3() { + // Methods of the form basis.method(). + + func checkMethod( + _ method: (Basis) -> (Vector3) -> some TestEquatable, + filePath: StaticString = #filePath, line: UInt = #line + ) { + forAll(filePath: filePath, line: line) { + Basis.mixedGen + Vector3.mixedGen + } checkCover: { + method($0)($1) + } + } + + checkMethod(Basis.scaled(scale:)) + checkMethod(Basis.tdotx) + checkMethod(Basis.tdoty) + checkMethod(Basis.tdotz) + } + + func testSlerp() { + Float.$closeEnoughUlps.withValue(2) { + forAll { + Basis.rotationGen + Basis.rotationGen + TinyGen.closedUnitRangeDoubles.map { $0 * 2 - 0.5} + } checkCover: { + $0.slerp(to: $1, weight: $2) + } + } + } + +} From 36b5aae89fd548258aee0450677719ad21c4d4bf Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Wed, 11 Dec 2024 22:32:53 -0600 Subject: [PATCH 97/99] fix some Vector3 covers --- Sources/SwiftCovers/Basis.covers.swift | 6 +- Sources/SwiftCovers/Vector3.covers.swift | 56 +++++++++++-------- Sources/SwiftGodot/SwiftCoverSupport.swift | 7 ++- .../SwiftGodotTestability/GodotTestCase.swift | 4 +- .../BuiltIn/Vector3CoverTests.swift | 28 +++++++--- 5 files changed, 65 insertions(+), 36 deletions(-) diff --git a/Sources/SwiftCovers/Basis.covers.swift b/Sources/SwiftCovers/Basis.covers.swift index 09df19fe5..62f48ce8e 100644 --- a/Sources/SwiftCovers/Basis.covers.swift +++ b/Sources/SwiftCovers/Basis.covers.swift @@ -24,7 +24,11 @@ extension Basis { public init(axis: Vector3, angle: Float) { // Rotation matrix from axis and angle, see https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_angle - // The axis Vector3 should be normalized. + + guard axis.isNormalized() else { + self.init() + return + } let axisSq = axis * axis diff --git a/Sources/SwiftCovers/Vector3.covers.swift b/Sources/SwiftCovers/Vector3.covers.swift index 7bd647189..e3854668a 100644 --- a/Sources/SwiftCovers/Vector3.covers.swift +++ b/Sources/SwiftCovers/Vector3.covers.swift @@ -85,51 +85,61 @@ extension Vector3 { let x2 = x * x let y2 = y * y let z2 = z * z - + return Double(sqrt(x2 + y2 + z2)) } - + public func lengthSquared() -> Double { let x2 = x * x let y2 = y * y let z2 = z * z - + return Double(x2 + y2 + z2) } - - public func slerp(to: Vector3, weight: Double) -> Vector3 { + + public func slerp(to: Vector3, weight: Double) -> Vector3 { // This method seems more complicated than it really is, since we write out // the internals of some methods for efficiency (mainly, checking length). - let startLengthSq = lengthSquared() - let endLengthSq = to.lengthSquared() - - // Zero length vectors have no angle, so the best we can do is either lerp or throw an error. + let startLengthSq = Float(lengthSquared()) + let endLengthSq = Float(to.lengthSquared()) + + let weight = Float(weight) + + func simpleLerp() -> Vector3 { + return Vector3( + x: self.x.lerp(to: to.x, withoutClampingWeight: weight), + y: self.y.lerp(to: to.y, withoutClampingWeight: weight), + z: self.z.lerp(to: to.z, withoutClampingWeight: weight) + ) + } + if startLengthSq == 0.0 || endLengthSq == 0.0 { - return lerp(to: to, weight: weight) + // Zero length vectors have no angle, so the best we can do is either lerp or throw an error. + return simpleLerp() } - + var axis = cross(with: to) - let axisLengthSq = axis.lengthSquared() - - // Colinear vectors have no rotation axis or angle between them, so the best we can do is lerp. + let axisLengthSq = Float(axis.lengthSquared()) + if axisLengthSq == 0.0 { - return lerp(to: to, weight: weight) + // Colinear vectors have no rotation axis or angle between them, so the best we can do is lerp. + return simpleLerp() } - - axis /= sqrt(axisLengthSq) - let startLength = sqrt(startLengthSq) - let resultLength = startLength.lerp(to: sqrt(endLengthSq), weight: weight) + + axis /= Double(axisLengthSq.squareRoot()) + let startLength = startLengthSq.squareRoot() + let resultLength = startLength.lerp(to: endLengthSq.squareRoot(), withoutClampingWeight: weight) let angle = angleTo(to) - - return rotated(axis: axis, angle: angle * weight) * (resultLength / startLength) + + return rotated(axis: axis, angle: Double(Float(angle) * weight)) * Double(resultLength / startLength) } - + public func rotated(axis: Vector3, angle: Double) -> Vector3 { // basis subscript getter is mutating by default let basis = Basis(axis: axis, angle: Float(angle)) return basis.xform(self) } - + public func clamp(min: Vector3, max: Vector3) -> Vector3 { return Vector3(x: x.clamped(min: min.x, max: max.x), y: y.clamped(min: min.y, max: max.y), diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index 37fdd8967..2033cf007 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -124,10 +124,11 @@ extension Basis { @_spi(SwiftCovers) @inline(__always) public func xform(_ v: Vector3) -> Vector3 { + let t = self.transposed() return Vector3( - x: Float(x.dot(with: v)), - y: Float(y.dot(with: v)), - z: Float(z.dot(with: v)) + x: Float(t.x.dot(with: v)), + y: Float(t.y.dot(with: v)), + z: Float(t.z.dot(with: v)) ) } diff --git a/Sources/SwiftGodotTestability/GodotTestCase.swift b/Sources/SwiftGodotTestability/GodotTestCase.swift index 789437732..918fa4c0a 100644 --- a/Sources/SwiftGodotTestability/GodotTestCase.swift +++ b/Sources/SwiftGodotTestability/GodotTestCase.swift @@ -222,9 +222,9 @@ extension GodotTestCase { #if TESTABLE_SWIFT_COVERS let gen = build() - for k: UInt64 in 204 ... 1_000 { + for k: UInt64 in 1 ... 1_000 { var rng = SipRNG(key0: k, key1: 1234) - // Mix in the function name so every test that starts by asking for, say, a Plane doesn't get the same Plane. + // Mix in the function name so every test that starts by asking for, say, a Plane doesn't get the same Plane. function.withUTF8Buffer { buffer in for byte in buffer { for bit in 0 ..< 8 { diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift index bbbb5f4e0..85a8127d3 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift @@ -5,9 +5,10 @@ // Created by Danny Youstra on 12/11/24. // -import XCTest -import SwiftGodotTestability @testable import SwiftGodot +import Darwin +import SwiftGodotTestability +import XCTest @available(macOS 14, *) extension Vector3 { @@ -134,17 +135,30 @@ final class Vector3CoverTests: GodotTestCase { forAll(filePath: filePath, line: line) { Vector3.mixed Vector3.mixed - TinyGen.mixedDoubles + TinyGen.closedUnitRangeDoubles.map { $0 * 2 - 0.5 } } checkCover: { method($0)($1, $2) } } - - checkMethod(Vector3.slerp) - checkMethod(Vector3.rotated) + + Float.$closeEnoughUlps.withValue(260) { + checkMethod(Vector3.slerp) + } checkMethod(Vector3.moveToward) } - + + func testRotated() { + Float.$closeEnoughUlps.withValue(1024) { + forAll { + Vector3.mixed + Vector3.normalized + TinyGen.mixedDoubles + } checkCover: { + $0.rotated(axis: $1, angle: $2) + } + } + } + func testClamp() { forAll { Vector3.mixed From 9b4e34c6270214819fad762f4205f14d6a6d8964 Mon Sep 17 00:00:00 2001 From: Rob Mayoff Date: Wed, 11 Dec 2024 23:38:15 -0600 Subject: [PATCH 98/99] comment out or fix some broken stuff --- Sources/SwiftCovers/Transform2D.covers.swift | 7 ++- Sources/SwiftCovers/Vector3.covers.swift | 60 +++++++------------ Sources/SwiftGodot/SwiftCoverSupport.swift | 11 ++-- .../BuiltIn/BasisCoverTests.swift | 10 ++-- .../BuiltIn/Vector3CoverTests.swift | 18 +++--- 5 files changed, 47 insertions(+), 59 deletions(-) diff --git a/Sources/SwiftCovers/Transform2D.covers.swift b/Sources/SwiftCovers/Transform2D.covers.swift index de3b0e2bf..014d12669 100644 --- a/Sources/SwiftCovers/Transform2D.covers.swift +++ b/Sources/SwiftCovers/Transform2D.covers.swift @@ -141,7 +141,9 @@ extension Transform2D { public func basisXformInv(v: Vector2) -> Vector2 { return Vector2(x: Float(x.dot(with: v)), y: Float(y.dot(with: v))) } - + +#if false + // Needs fixing. public func interpolateWith(xform: Transform2D, weight: Double) -> Transform2D { let p1 = origin let p2 = xform.origin @@ -175,7 +177,8 @@ extension Transform2D { res = res.scaleBasis(scale: s1.lerp(to: s2, weight: weight)) return res } - +#endif + public func isFinite() -> Bool { return x.isFinite() && y.isFinite() && origin.isFinite() } diff --git a/Sources/SwiftCovers/Vector3.covers.swift b/Sources/SwiftCovers/Vector3.covers.swift index e3854668a..c71fc11cf 100644 --- a/Sources/SwiftCovers/Vector3.covers.swift +++ b/Sources/SwiftCovers/Vector3.covers.swift @@ -97,6 +97,8 @@ extension Vector3 { return Double(x2 + y2 + z2) } +#if false + // Not accurate enough yet. public func slerp(to: Vector3, weight: Double) -> Vector3 { // This method seems more complicated than it really is, since we write out // the internals of some methods for efficiency (mainly, checking length). @@ -133,12 +135,16 @@ extension Vector3 { return rotated(axis: axis, angle: Double(Float(angle) * weight)) * Double(resultLength / startLength) } +#endif +#if false + // Not accurate enough yet. public func rotated(axis: Vector3, angle: Double) -> Vector3 { // basis subscript getter is mutating by default let basis = Basis(axis: axis, angle: Float(angle)) return basis.xform(self) } +#endif public func clamp(min: Vector3, max: Vector3) -> Vector3 { return Vector3(x: x.clamped(min: min.x, max: max.x), @@ -162,11 +168,12 @@ extension Vector3 { } public func limitLength(_ length: Double = 1.0) -> Vector3 { - let beforeLen = self.length() + let limit = Float(length) + let l = Float(self.length()) var result = self - if (beforeLen > 0 && length < beforeLen) { - result = result / beforeLen - result = result * length + if l > 0 && limit < l { + result = result / Double(l) + result = result * Double(limit) } return result } @@ -190,7 +197,7 @@ extension Vector3 { /// Reflection requires a scale by 2, but Float * Vector3 is not overloaded return Vector3(x: 2, y: 2, z: 2) * n * self.dot(with: n) - self } - + public func octahedronEncode() -> Vector2 { let n = self / Double((Swift.abs(x) + Swift.abs(y) + Swift.abs(z))) var o = Vector2() @@ -207,7 +214,9 @@ extension Vector3 { o.y = o.y * 0.5 + 0.5 return o } - + +#if false + // Needs fixing. public static func octahedronDecode(uv: Vector2) -> Vector3 { let f = Vector2(x: uv.x * 2.0 - 1.0, y: uv.y * 2.0 - 1.0) var n = Vector3(x: f.x, y: f.y, z: 1.0 - Swift.abs(f.x) - Swift.abs(f.y)) @@ -217,7 +226,8 @@ extension Vector3 { n.y += n.y >= 0 ? -t : t return n.normalized() } - +#endif + public func outer(with: Vector3) -> Basis { return Basis(xAxis: Vector3(x: x * with.x, y: x * with.y, z: x * with.z), yAxis: Vector3(x: y * with.x, y: y * with.y, z: y * with.z), @@ -288,51 +298,27 @@ extension Vector3 { // Comparison Operators public static func == (lhs: Vector3, rhs: Vector3) -> Bool { - return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z + return lhs.tuple == rhs.tuple } public static func != (lhs: Vector3, rhs: Vector3) -> Bool { - return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z + return !(lhs.tuple == rhs.tuple) } public static func < (lhs: Vector3, rhs: Vector3) -> Bool { - if lhs.x == rhs.x { - if lhs.y == rhs.y { - return lhs.z < rhs.z - } - return lhs.y < rhs.y - } - return lhs.x < rhs.x + return lhs.tuple < rhs.tuple } public static func > (lhs: Vector3, rhs: Vector3) -> Bool { - if lhs.x == rhs.x { - if lhs.y == rhs.y { - return lhs.z > rhs.z - } - return lhs.y > rhs.y - } - return lhs.x > rhs.x + return lhs.tuple > rhs.tuple } public static func <= (lhs: Vector3, rhs: Vector3) -> Bool { - if lhs.x == rhs.x { - if lhs.y == rhs.y { - return lhs.z <= rhs.z - } - return lhs.y < rhs.y - } - return lhs.x < rhs.x + return lhs.tuple <= rhs.tuple } public static func >= (lhs: Vector3, rhs: Vector3) -> Bool { - if lhs.x == rhs.x { - if lhs.y == rhs.y { - return lhs.z >= rhs.z - } - return lhs.y > rhs.y - } - return lhs.x > rhs.x + return lhs.tuple >= rhs.tuple } } diff --git a/Sources/SwiftGodot/SwiftCoverSupport.swift b/Sources/SwiftGodot/SwiftCoverSupport.swift index 2033cf007..ae1ccd053 100644 --- a/Sources/SwiftGodot/SwiftCoverSupport.swift +++ b/Sources/SwiftGodot/SwiftCoverSupport.swift @@ -66,6 +66,10 @@ extension Vector4i { } extension Vector3 { + @_spi(SwiftCovers) + @inline(__always) + public var tuple: (Float, Float, Float) { (x, y, z) } + @_spi(SwiftCovers) @inline(__always) public var simd: SIMD3 { SIMD3(x, y, z) } @@ -124,11 +128,10 @@ extension Basis { @_spi(SwiftCovers) @inline(__always) public func xform(_ v: Vector3) -> Vector3 { - let t = self.transposed() return Vector3( - x: Float(t.x.dot(with: v)), - y: Float(t.y.dot(with: v)), - z: Float(t.z.dot(with: v)) + x: Float(x.dot(with: v)), + y: Float(y.dot(with: v)), + z: Float(z.dot(with: v)) ) } diff --git a/Tests/SwiftGodotTests/BuiltIn/BasisCoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/BasisCoverTests.swift index 7ff624e3c..52a28c3b2 100644 --- a/Tests/SwiftGodotTests/BuiltIn/BasisCoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/BasisCoverTests.swift @@ -15,7 +15,7 @@ extension Basis { static let mixedGen = gen(TinyGen.mixedFloats) static let rotationGen = TinyGenBuilder { - Vector3.normalizedGen + Vector3.normalized TinyGen.gaussianFloats }.map { Basis(axis: $0, angle: $1) } @@ -31,7 +31,7 @@ final class BasisCoverTests: GodotTestCase { func testInitAxisAngle() { Float.$closeEnoughUlps.withValue(250) { forAll { - Vector3.normalizedGen + Vector3.normalized TinyGen.mixedFloats } checkCover: { Basis(axis: $0, angle: $1) @@ -49,7 +49,7 @@ final class BasisCoverTests: GodotTestCase { func testInitFromQuaternion() { forAll { - Quaternion.mixedGen + Quaternion.mixed } checkCover: { Basis(from: $0) } @@ -118,7 +118,7 @@ final class BasisCoverTests: GodotTestCase { Float.$closeEnoughUlps.withValue(640) { forAll { Basis.mixedGen - Vector3.normalizedGen // axis + Vector3.normalized // axis TinyGen.mixedDoubles // angle } checkCover: { $0.rotated(axis: $1, angle: $2) @@ -135,7 +135,7 @@ final class BasisCoverTests: GodotTestCase { ) { forAll(filePath: filePath, line: line) { Basis.mixedGen - Vector3.mixedGen + Vector3.mixed } checkCover: { method($0)($1) } diff --git a/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift b/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift index 85a8127d3..6875f0262 100644 --- a/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift +++ b/Tests/SwiftGodotTests/BuiltIn/Vector3CoverTests.swift @@ -141,21 +141,17 @@ final class Vector3CoverTests: GodotTestCase { } } - Float.$closeEnoughUlps.withValue(260) { - checkMethod(Vector3.slerp) - } + checkMethod(Vector3.slerp) checkMethod(Vector3.moveToward) } func testRotated() { - Float.$closeEnoughUlps.withValue(1024) { - forAll { - Vector3.mixed - Vector3.normalized - TinyGen.mixedDoubles - } checkCover: { - $0.rotated(axis: $1, angle: $2) - } + forAll { + Vector3.mixed + Vector3.normalized + TinyGen.mixedDoubles + } checkCover: { + $0.rotated(axis: $1, angle: $2) } } From 724ef787d686f38daf87c592a37ce39a6bf29365 Mon Sep 17 00:00:00 2001 From: Danny Youstra Date: Thu, 12 Dec 2024 13:26:48 -0600 Subject: [PATCH 99/99] Transpose Basis construction in Transform3D construction --- Sources/SwiftCovers/Transform3D.covers.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftCovers/Transform3D.covers.swift b/Sources/SwiftCovers/Transform3D.covers.swift index c5d6eb46f..b354bc7b4 100644 --- a/Sources/SwiftCovers/Transform3D.covers.swift +++ b/Sources/SwiftCovers/Transform3D.covers.swift @@ -28,7 +28,12 @@ extension Transform3D { public init(xAxis: Vector3, yAxis: Vector3, zAxis: Vector3, origin: Vector3) { self.init() - self.basis = Basis(xAxis: xAxis, yAxis: yAxis, zAxis: zAxis) + // Transpose the axes when creating the basis to match engine's row-major order + self.basis = Basis( + xAxis: Vector3(x: xAxis.x, y: yAxis.x, z: zAxis.x), + yAxis: Vector3(x: xAxis.y, y: yAxis.y, z: zAxis.y), + zAxis: Vector3(x: xAxis.z, y: yAxis.z, z: zAxis.z) + ) self.origin = origin }