Cached API tables and cut allocations in the PlayDate bindings target.

This commit is contained in:
2026-07-25 10:43:32 +02:00
parent 6594794cec
commit dfeadabfb6
20 changed files with 272 additions and 89 deletions
+113 -18
View File
@@ -10,7 +10,7 @@
internal import CPlaydate
private var spriteAPI: UnsafePointer<playdate_sprite> { Playdate.spriteAPI }
private var spriteAPI: UnsafePointer<playdate_sprite> { Playdate.spriteAPI.unsafelyUnwrapped }
/// A floating-point rectangle mirroring `PDRect`.
public struct Rect: Sendable {
@@ -40,6 +40,10 @@ public final class Sprite {
let pointer: OpaquePointer
let isOwned: Bool
/// Position in the static `displayList`, or -1 when not in it; makes
/// `add()`/`remove()` O(1) instead of scanning the list.
private var displayListIndex = -1
/// Per-sprite callbacks and retained resources.
var updateFunction: ((Sprite) -> Void)?
var drawFunction: ((Sprite, _ bounds: Rect, _ drawRect: Rect) -> Void)?
@@ -205,7 +209,8 @@ public final class Sprite {
/// Adds the sprite to the display list.
public func add() {
spriteAPI.pointee.addSprite.unsafelyUnwrapped(pointer)
if !Sprite.displayList.contains(where: { $0 === self }) {
if displayListIndex < 0 {
displayListIndex = Sprite.displayList.count
Sprite.displayList.append(self)
}
}
@@ -213,7 +218,16 @@ public final class Sprite {
/// Removes the sprite from the display list.
public func remove() {
spriteAPI.pointee.removeSprite.unsafelyUnwrapped(pointer)
Sprite.displayList.removeAll { $0 === self }
guard displayListIndex >= 0 else { return }
// Swap-remove: the keep-alive list is unordered (the OS keeps the
// draw order), so the last sprite can take the vacated slot.
let index = displayListIndex
let last = Sprite.displayList.removeLast()
if last !== self {
Sprite.displayList[index] = last
last.displayListIndex = index
}
displayListIndex = -1
}
/// Removes the given sprites from the display list.
@@ -224,6 +238,7 @@ public final class Sprite {
/// Removes every sprite from the display list.
public static func removeAll() {
spriteAPI.pointee.removeAllSprites.unsafelyUnwrapped()
for sprite in displayList { sprite.displayListIndex = -1 }
displayList = []
}
@@ -318,9 +333,12 @@ public final class Sprite {
/// Sets an 8×8 stencil pattern (8 rows of image data).
public func setStencilPattern(_ rows: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) {
var pattern: [UInt8] = [rows.0, rows.1, rows.2, rows.3, rows.4, rows.5, rows.6, rows.7]
pattern.withUnsafeMutableBufferPointer { buffer in
spriteAPI.pointee.setStencilPattern.unsafelyUnwrapped(pointer, buffer.baseAddress)
// The tuple is already 8 contiguous bytes; the C side copies the
// pattern, so passing the stack storage directly is safe.
withUnsafeBytes(of: rows) { buffer in
let pattern = UnsafeMutablePointer(
mutating: buffer.baseAddress.unsafelyUnwrapped.assumingMemoryBound(to: UInt8.self))
spriteAPI.pointee.setStencilPattern.unsafelyUnwrapped(pointer, pattern)
}
}
@@ -459,16 +477,22 @@ public final class Sprite {
}
}
/// Visits and frees a C collision info array.
private static func visitCollisions(_ pointer: UnsafeMutablePointer<SpriteCollisionInfo>?,
count: Int32, _ visit: (CollisionInfo) -> Void) {
guard let pointer else { return }
for index in 0..<Int(count) {
visit(CollisionInfo(pointer[index]))
}
System.systemFree(pointer)
}
/// Converts and frees a C collision info array.
private static func collisionInfos(_ pointer: UnsafeMutablePointer<SpriteCollisionInfo>?,
count: Int32) -> [CollisionInfo] {
guard let pointer else { return [] }
var infos = [CollisionInfo]()
infos.reserveCapacity(Int(count))
for index in 0..<Int(count) {
infos.append(CollisionInfo(pointer[index]))
}
System.systemFree(pointer)
visitCollisions(pointer, count: count) { infos.append($0) }
return infos
}
@@ -482,6 +506,17 @@ public final class Sprite {
return ((actualX, actualY), Sprite.collisionInfos(result, count: count))
}
/// Like `checkCollisions(goalX:goalY:)`, but visits each collision
/// instead of building an array, avoiding per-call allocations.
public func checkCollisions(goalX: Float, goalY: Float,
_ visit: (CollisionInfo) -> Void) -> (x: Float, y: Float) {
var actualX: Float = 0, actualY: Float = 0, count: Int32 = 0
let result = spriteAPI.pointee.checkCollisions.unsafelyUnwrapped(
pointer, goalX, goalY, &actualX, &actualY, &count)
Sprite.visitCollisions(result, count: count, visit)
return (actualX, actualY)
}
/// Moves the sprite toward (goalX, goalY), resolving collisions, and
/// returns where it ended up and what it hit.
@discardableResult
@@ -493,18 +528,36 @@ public final class Sprite {
return ((actualX, actualY), Sprite.collisionInfos(result, count: count))
}
/// Converts and frees a C sprite pointer array.
private static func sprites(_ pointer: UnsafeMutablePointer<OpaquePointer?>?,
count: Int32) -> [Sprite] {
guard let pointer else { return [] }
var sprites = [Sprite]()
sprites.reserveCapacity(Int(count))
/// Like `moveWithCollisions(goalX:goalY:)`, but visits each collision
/// instead of building an array, avoiding per-call allocations.
@discardableResult
public func moveWithCollisions(goalX: Float, goalY: Float,
_ visit: (CollisionInfo) -> Void) -> (x: Float, y: Float) {
var actualX: Float = 0, actualY: Float = 0, count: Int32 = 0
let result = spriteAPI.pointee.moveWithCollisions.unsafelyUnwrapped(
pointer, goalX, goalY, &actualX, &actualY, &count)
Sprite.visitCollisions(result, count: count, visit)
return (actualX, actualY)
}
/// Visits and frees a C sprite pointer array.
private static func visitSprites(_ pointer: UnsafeMutablePointer<OpaquePointer?>?,
count: Int32, _ visit: (Sprite) -> Void) {
guard let pointer else { return }
for index in 0..<Int(count) {
if let spritePointer = pointer[index] {
sprites.append(wrapper(for: spritePointer))
visit(wrapper(for: spritePointer))
}
}
System.systemFree(pointer)
}
/// Converts and frees a C sprite pointer array.
private static func sprites(_ pointer: UnsafeMutablePointer<OpaquePointer?>?,
count: Int32) -> [Sprite] {
var sprites = [Sprite]()
sprites.reserveCapacity(Int(count))
visitSprites(pointer, count: count) { sprites.append($0) }
return sprites
}
@@ -515,6 +568,14 @@ public final class Sprite {
return sprites(result, count: count)
}
/// Like `query(atPoint:_:)`, visiting each sprite without building an
/// array.
public static func query(atPoint x: Float, _ y: Float, _ visit: (Sprite) -> Void) {
var count: Int32 = 0
let result = spriteAPI.pointee.querySpritesAtPoint.unsafelyUnwrapped(x, y, &count)
visitSprites(result, count: count, visit)
}
/// Sprites with collision rects intersecting the rect.
public static func query(inRect x: Float, _ y: Float, width: Float, height: Float) -> [Sprite] {
var count: Int32 = 0
@@ -522,6 +583,15 @@ public final class Sprite {
return sprites(result, count: count)
}
/// Like `query(inRect:_:width:height:)`, visiting each sprite without
/// building an array.
public static func query(inRect x: Float, _ y: Float, width: Float, height: Float,
_ visit: (Sprite) -> Void) {
var count: Int32 = 0
let result = spriteAPI.pointee.querySpritesInRect.unsafelyUnwrapped(x, y, width, height, &count)
visitSprites(result, count: count, visit)
}
/// Sprites with collision rects intersecting the line segment.
public static func query(alongLine x1: Float, _ y1: Float, _ x2: Float, _ y2: Float) -> [Sprite] {
var count: Int32 = 0
@@ -529,6 +599,15 @@ public final class Sprite {
return sprites(result, count: count)
}
/// Like `query(alongLine:_:_:_:)`, visiting each sprite without building
/// an array.
public static func query(alongLine x1: Float, _ y1: Float, _ x2: Float, _ y2: Float,
_ visit: (Sprite) -> Void) {
var count: Int32 = 0
let result = spriteAPI.pointee.querySpritesAlongLine.unsafelyUnwrapped(x1, y1, x2, y2, &count)
visitSprites(result, count: count, visit)
}
/// Like `query(alongLine:)`, with entry/exit information for each sprite.
public static func queryInfo(alongLine x1: Float, _ y1: Float,
_ x2: Float, _ y2: Float) -> [QueryInfo] {
@@ -551,10 +630,26 @@ public final class Sprite {
return Sprite.sprites(result, count: count)
}
/// Like `overlappingSprites`, visiting each sprite without building an
/// array.
public func overlappingSprites(_ visit: (Sprite) -> Void) {
var count: Int32 = 0
let result = spriteAPI.pointee.overlappingSprites.unsafelyUnwrapped(pointer, &count)
Sprite.visitSprites(result, count: count, visit)
}
/// All sprites in the display list that overlap another sprite.
public static var allOverlappingSprites: [Sprite] {
var count: Int32 = 0
let result = spriteAPI.pointee.allOverlappingSprites.unsafelyUnwrapped(&count)
return sprites(result, count: count)
}
/// Like `allOverlappingSprites`, visiting each sprite without building
/// an array.
public static func allOverlappingSprites(_ visit: (Sprite) -> Void) {
var count: Int32 = 0
let result = spriteAPI.pointee.allOverlappingSprites.unsafelyUnwrapped(&count)
visitSprites(result, count: count, visit)
}
}