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
Binary file not shown.
+1 -1
View File
@@ -9,7 +9,7 @@ internal import CPlaydate
public enum Display {}
extension Display {
private static var api: UnsafePointer<playdate_display> { Playdate.displayAPI }
private static var api: UnsafePointer<playdate_display> { Playdate.displayAPI.unsafelyUnwrapped }
/// The display width in pixels, taking the current scale into account.
public static var width: Int { Int(api.pointee.getWidth.unsafelyUnwrapped()) }
+6 -2
View File
@@ -8,7 +8,7 @@
internal import CPlaydate
private var fileAPI: UnsafePointer<playdate_file> { Playdate.fileAPI }
private var fileAPI: UnsafePointer<playdate_file> { Playdate.fileAPI.unsafelyUnwrapped }
/// The most recent file system error as a thrown error.
private func lastFileError() -> PlaydateError {
@@ -184,7 +184,11 @@ extension File {
/// Writes the string's UTF-8 to the file. Returns the bytes written.
@discardableResult
public func write(_ string: String) throws(PlaydateError) -> Int {
try write(Array(string.utf8))
let result = string.withPlaydateUTF8 { bytes, count in
fileAPI.pointee.write.unsafelyUnwrapped(pointer, bytes, UInt32(count))
}
if result < 0 { throw lastFileError() }
return Int(result)
}
/// Flushes buffered writes to disk. Returns the bytes written.
+10 -10
View File
@@ -10,7 +10,7 @@ internal import CPlaydate
/// The graphics API: drawing, bitmaps, fonts, tilemaps, and video.
public enum Graphics {}
var gfx: UnsafePointer<playdate_graphics> { Playdate.graphicsAPI }
var gfx: UnsafePointer<playdate_graphics> { Playdate.graphicsAPI.unsafelyUnwrapped }
extension Graphics {
// MARK: - Screen constants
@@ -305,15 +305,15 @@ extension Graphics {
/// back to the first.
public static func fillPolygon(points: [(x: Int, y: Int)], color: Color,
fillRule: PolygonFillRule = .nonZero) {
var coordinates = [Int32]()
coordinates.reserveCapacity(points.count * 2)
for point in points {
coordinates.append(Int32(point.x))
coordinates.append(Int32(point.y))
}
color.withLCDColor { cColor in
coordinates.withUnsafeMutableBufferPointer { buffer in
gfx.pointee.fillPolygon.unsafelyUnwrapped(Int32(points.count), buffer.baseAddress,
withUnsafeTemporaryAllocation(of: Int32.self, capacity: points.count * 2) { coordinates in
var index = 0
for point in points {
coordinates[index] = Int32(point.x)
coordinates[index + 1] = Int32(point.y)
index += 2
}
color.withLCDColor { cColor in
gfx.pointee.fillPolygon.unsafelyUnwrapped(Int32(points.count), coordinates.baseAddress,
cColor, fillRule.cValue)
}
}
+16 -2
View File
@@ -63,8 +63,21 @@ extension Graphics {
mask: mask, data: data)
}
public var width: Int { data.width }
public var height: Int { data.height }
/// Cached dimensions, so `width`/`height` don't pay a full
/// `getBitmapData` round-trip per access. Only `load(path:)` can
/// change a bitmap's size, which resets the cache.
private var cachedSize: (width: Int, height: Int)?
private var size: (width: Int, height: Int) {
if let cachedSize { return cachedSize }
let data = self.data
let size = (data.width, data.height)
cachedSize = size
return size
}
public var width: Int { size.width }
public var height: Int { size.height }
/// The color of the pixel at (x, y).
public func pixel(x: Int, y: Int) -> SolidColor {
@@ -77,6 +90,7 @@ extension Graphics {
public func load(path: String) throws(PlaydateError) {
var error: UnsafePointer<CChar>?
path.withPlaydateCString { gfx.pointee.loadIntoBitmap.unsafelyUnwrapped($0, pointer, &error) }
cachedSize = nil
if let error { throw PlaydateError(cString: error) }
}
+1 -1
View File
@@ -5,7 +5,7 @@
internal import CPlaydate
private var tilemapAPI: UnsafePointer<playdate_tilemap> { gfx.pointee.tilemap.unsafelyUnwrapped }
private var tilemapAPI: UnsafePointer<playdate_tilemap> { Playdate.tilemapAPI.unsafelyUnwrapped }
extension Graphics {
/// A grid of tiles drawn from a bitmap table. Wraps `LCDTileMap`.
+2 -2
View File
@@ -6,8 +6,8 @@
internal import CPlaydate
private var videoAPI: UnsafePointer<playdate_video> { gfx.pointee.video.unsafelyUnwrapped }
private var streamAPI: UnsafePointer<playdate_videostream> { gfx.pointee.videostream.unsafelyUnwrapped }
private var videoAPI: UnsafePointer<playdate_video> { Playdate.videoAPI.unsafelyUnwrapped }
private var streamAPI: UnsafePointer<playdate_videostream> { Playdate.videoStreamAPI.unsafelyUnwrapped }
extension Graphics {
/// Plays .pdv video files. Wraps `LCDVideoPlayer`.
+22 -14
View File
@@ -9,7 +9,7 @@
internal import CPlaydate
private var jsonAPI: UnsafePointer<playdate_json> { Playdate.jsonAPI }
private var jsonAPI: UnsafePointer<playdate_json> { Playdate.jsonAPI.unsafelyUnwrapped }
/// The JSON API: decoding to and encoding from a `Value` tree.
public enum JSON {}
@@ -33,23 +33,31 @@ extension JSON {
init(_ value: Value) { self.value = value }
}
/// A container under construction. A class, so appends mutate uniquely
/// referenced storage in place instead of copying the collection out of
/// and back into an enum payload on every element.
private final class Container {
let isArray: Bool
var items: [Value] = []
var entries: [String: Value] = [:]
init(isArray: Bool) { self.isArray = isArray }
var value: Value { isArray ? .array(items) : .table(entries) }
}
private final class DecodeContext {
/// Containers under construction, innermost last.
var stack: [Value] = []
var stack: [Container] = []
var errorMessage: String?
var errorLine: Int32 = 0
func append(_ value: Value, key: String?) {
guard !stack.isEmpty else { return }
switch stack[stack.count - 1] {
case .array(var items):
items.append(value)
stack[stack.count - 1] = .array(items)
case .table(var entries):
if let key { entries[key] = value }
stack[stack.count - 1] = .table(entries)
default:
break
guard let container = stack.last else { return }
if container.isArray {
container.items.append(value)
} else if let key {
container.entries[key] = value
}
}
}
@@ -81,7 +89,7 @@ extension JSON {
decoder.willDecodeSublist = { decoder, _, type in
guard let userdata = decoder?.pointee.userdata else { return }
let context = Unmanaged<DecodeContext>.fromOpaque(userdata).takeUnretainedValue()
context.stack.append(type == kJSONArray ? .array([]) : .table([:]))
context.stack.append(Container(isArray: type == kJSONArray))
}
decoder.didDecodeTableValue = { decoder, key, value in
guard let userdata = decoder?.pointee.userdata else { return }
@@ -99,7 +107,7 @@ extension JSON {
guard let finished = context.stack.popLast() else { return nil }
// Handed to the parent container (or the decode outval) as the
// sublist's value; consumed by `convert`.
return Unmanaged.passRetained(ValueBox(finished)).toOpaque()
return Unmanaged.passRetained(ValueBox(finished.value)).toOpaque()
}
return decoder
}
+13 -8
View File
@@ -9,7 +9,7 @@
public import CPlaydate
private var luaAPI: UnsafePointer<playdate_lua> { Playdate.luaAPI }
private var luaAPI: UnsafePointer<playdate_lua> { Playdate.luaAPI.unsafelyUnwrapped }
/// The Lua bridge: registering C functions and classes, and exchanging
/// values with Lua code.
@@ -178,10 +178,13 @@ extension Lua {
/// `UDObject` handle for retaining it.
public static func objectArgument(at position: Int, type: String)
-> (object: UnsafeMutableRawPointer?, userdataObject: UDObject?) {
let cType = type.copiedPlaydateCString()
defer { cType.deallocate() }
var userdataObject: OpaquePointer?
let object = luaAPI.pointee.getArgObject.unsafelyUnwrapped(Int32(position), cType, &userdataObject)
// The C API takes a non-const class name but only reads it, so the
// stack copy can be passed with a mutating cast.
let object = type.withPlaydateCString { cType in
luaAPI.pointee.getArgObject.unsafelyUnwrapped(
Int32(position), UnsafeMutablePointer(mutating: cType), &userdataObject)
}
return (object, userdataObject.map { UDObject(pointer: $0) })
}
@@ -240,11 +243,13 @@ extension Lua {
@discardableResult
public static func pushObject(_ object: UnsafeMutableRawPointer, type: String,
valueCount: Int = 0) -> UDObject? {
let cType = type.copiedPlaydateCString()
defer { cType.deallocate() }
guard let pointer = luaAPI.pointee.pushObject.unsafelyUnwrapped(object, cType, Int32(valueCount)) else {
return nil
// The C API takes a non-const class name but only reads it, so the
// stack copy can be passed with a mutating cast.
let pointer = type.withPlaydateCString { cType in
luaAPI.pointee.pushObject.unsafelyUnwrapped(
object, UnsafeMutablePointer(mutating: cType), Int32(valueCount))
}
guard let pointer else { return nil }
return UDObject(pointer: pointer)
}
+3 -3
View File
@@ -9,9 +9,9 @@
internal import CPlaydate
private var networkAPI: UnsafePointer<playdate_network> { Playdate.networkAPI }
private var httpAPI: UnsafePointer<playdate_http> { networkAPI.pointee.http.unsafelyUnwrapped }
private var tcpAPI: UnsafePointer<playdate_tcp> { networkAPI.pointee.tcp.unsafelyUnwrapped }
private var networkAPI: UnsafePointer<playdate_network> { Playdate.networkAPI.unsafelyUnwrapped }
private var httpAPI: UnsafePointer<playdate_http> { Playdate.httpAPI.unsafelyUnwrapped }
private var tcpAPI: UnsafePointer<playdate_tcp> { Playdate.tcpAPI.unsafelyUnwrapped }
/// The network API: wifi status, HTTP, and TCP.
public enum Network {}
+55
View File
@@ -37,6 +37,36 @@ public enum Playdate {
nonisolated(unsafe) static var scoreboardsAPI: UnsafePointer<playdate_scoreboards>!
nonisolated(unsafe) static var networkAPI: UnsafePointer<playdate_network>!
// Second-level tables, cached for the same reason. Assigned with
// optional chaining because partial API tables (e.g. test mocks) may
// leave some of them null; using an absent table traps at the call
// site, as before.
nonisolated(unsafe) static var tilemapAPI: UnsafePointer<playdate_tilemap>!
nonisolated(unsafe) static var videoAPI: UnsafePointer<playdate_video>!
nonisolated(unsafe) static var videoStreamAPI: UnsafePointer<playdate_videostream>!
nonisolated(unsafe) static var channelAPI: UnsafePointer<playdate_sound_channel>!
nonisolated(unsafe) static var sourceAPI: UnsafePointer<playdate_sound_source>!
nonisolated(unsafe) static var filePlayerAPI: UnsafePointer<playdate_sound_fileplayer>!
nonisolated(unsafe) static var sampleAPI: UnsafePointer<playdate_sound_sample>!
nonisolated(unsafe) static var samplePlayerAPI: UnsafePointer<playdate_sound_sampleplayer>!
nonisolated(unsafe) static var synthAPI: UnsafePointer<playdate_sound_synth>!
nonisolated(unsafe) static var instrumentAPI: UnsafePointer<playdate_sound_instrument>!
nonisolated(unsafe) static var trackAPI: UnsafePointer<playdate_sound_track>!
nonisolated(unsafe) static var sequenceAPI: UnsafePointer<playdate_sound_sequence>!
nonisolated(unsafe) static var signalAPI: UnsafePointer<playdate_sound_signal>!
nonisolated(unsafe) static var lfoAPI: UnsafePointer<playdate_sound_lfo>!
nonisolated(unsafe) static var envelopeAPI: UnsafePointer<playdate_sound_envelope>!
nonisolated(unsafe) static var controlSignalAPI: UnsafePointer<playdate_control_signal>!
nonisolated(unsafe) static var effectAPI: UnsafePointer<playdate_sound_effect>!
nonisolated(unsafe) static var twoPoleFilterAPI: UnsafePointer<playdate_sound_effect_twopolefilter>!
nonisolated(unsafe) static var onePoleFilterAPI: UnsafePointer<playdate_sound_effect_onepolefilter>!
nonisolated(unsafe) static var bitCrusherAPI: UnsafePointer<playdate_sound_effect_bitcrusher>!
nonisolated(unsafe) static var ringModulatorAPI: UnsafePointer<playdate_sound_effect_ringmodulator>!
nonisolated(unsafe) static var delayLineAPI: UnsafePointer<playdate_sound_effect_delayline>!
nonisolated(unsafe) static var overdriveAPI: UnsafePointer<playdate_sound_effect_overdrive>!
nonisolated(unsafe) static var httpAPI: UnsafePointer<playdate_http>!
nonisolated(unsafe) static var tcpAPI: UnsafePointer<playdate_tcp>!
/// Stores the API pointer handed to the game's `eventHandler`.
///
/// Call this first, on the `.initialize` event, before using any other
@@ -54,6 +84,31 @@ public enum Playdate {
luaAPI = api.lua
scoreboardsAPI = api.scoreboards
networkAPI = api.network
tilemapAPI = graphicsAPI?.pointee.tilemap
videoAPI = graphicsAPI?.pointee.video
videoStreamAPI = graphicsAPI?.pointee.videostream
channelAPI = soundAPI?.pointee.channel
sourceAPI = soundAPI?.pointee.source
filePlayerAPI = soundAPI?.pointee.fileplayer
sampleAPI = soundAPI?.pointee.sample
samplePlayerAPI = soundAPI?.pointee.sampleplayer
synthAPI = soundAPI?.pointee.synth
instrumentAPI = soundAPI?.pointee.instrument
trackAPI = soundAPI?.pointee.track
sequenceAPI = soundAPI?.pointee.sequence
signalAPI = soundAPI?.pointee.signal
lfoAPI = soundAPI?.pointee.lfo
envelopeAPI = soundAPI?.pointee.envelope
controlSignalAPI = soundAPI?.pointee.controlsignal
effectAPI = soundAPI?.pointee.effect
twoPoleFilterAPI = effectAPI?.pointee.twopolefilter
onePoleFilterAPI = effectAPI?.pointee.onepolefilter
bitCrusherAPI = effectAPI?.pointee.bitcrusher
ringModulatorAPI = effectAPI?.pointee.ringmodulator
delayLineAPI = effectAPI?.pointee.delayline
overdriveAPI = effectAPI?.pointee.overdrive
httpAPI = networkAPI?.pointee.http
tcpAPI = networkAPI?.pointee.tcp
}
}
+1 -1
View File
@@ -9,7 +9,7 @@
internal import CPlaydate
private var scoreboardsAPI: UnsafePointer<playdate_scoreboards> { Playdate.scoreboardsAPI }
private var scoreboardsAPI: UnsafePointer<playdate_scoreboards> { Playdate.scoreboardsAPI.unsafelyUnwrapped }
/// The scoreboards API for games with online leaderboards.
public enum Scoreboards {}
+2 -2
View File
@@ -7,7 +7,7 @@
internal import CPlaydate
var snd: UnsafePointer<playdate_sound> { Playdate.soundAPI }
var snd: UnsafePointer<playdate_sound> { Playdate.soundAPI.unsafelyUnwrapped }
/// The sound API: channels, players, synths, sequences, and effects.
public enum Sound {}
@@ -176,7 +176,7 @@ extension Sound {
/// A mixer channel holding sources and effects. Wraps `SoundChannel`.
public final class Channel {
private static var api: UnsafePointer<playdate_sound_channel> { snd.pointee.channel.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_channel> { Playdate.channelAPI.unsafelyUnwrapped }
let pointer: OpaquePointer
let isOwned: Bool
+8 -8
View File
@@ -6,7 +6,7 @@
internal import CPlaydate
private var effectAPI: UnsafePointer<playdate_sound_effect> { snd.pointee.effect.unsafelyUnwrapped }
private var effectAPI: UnsafePointer<playdate_sound_effect> { Playdate.effectAPI.unsafelyUnwrapped }
extension Sound {
/// An effect that processes a channel's audio: the base class of the
@@ -74,7 +74,7 @@ extension Sound {
/// A two-pole IIR filter. Wraps `TwoPoleFilter`.
public final class TwoPoleFilter: Effect {
private static var api: UnsafePointer<playdate_sound_effect_twopolefilter> { effectAPI.pointee.twopolefilter.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_effect_twopolefilter> { Playdate.twoPoleFilterAPI.unsafelyUnwrapped }
public enum Kind: UInt32, Sendable {
case lowPass = 0
@@ -142,7 +142,7 @@ extension Sound {
/// A one-pole low/high-pass filter. Wraps `OnePoleFilter`.
public final class OnePoleFilter: Effect {
private static var api: UnsafePointer<playdate_sound_effect_onepolefilter> { effectAPI.pointee.onepolefilter.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_effect_onepolefilter> { Playdate.onePoleFilterAPI.unsafelyUnwrapped }
private var retainedParameterModulator: SignalValue?
@@ -176,7 +176,7 @@ extension Sound {
/// A bit-crushing and downsampling effect. Wraps `BitCrusher`.
public final class BitCrusher: Effect {
private static var api: UnsafePointer<playdate_sound_effect_bitcrusher> { effectAPI.pointee.bitcrusher.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_effect_bitcrusher> { Playdate.bitCrusherAPI.unsafelyUnwrapped }
private var retainedModulators: [SignalValue] = []
@@ -231,7 +231,7 @@ extension Sound {
/// A ring modulator effect. Wraps `RingModulator`.
public final class RingModulator: Effect {
private static var api: UnsafePointer<playdate_sound_effect_ringmodulator> { effectAPI.pointee.ringmodulator.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_effect_ringmodulator> { Playdate.ringModulatorAPI.unsafelyUnwrapped }
private var retainedFrequencyModulator: SignalValue?
@@ -265,7 +265,7 @@ extension Sound {
/// A tap into a delay line; produces audio and can be added to a channel
/// as a source. Wraps `DelayLineTap`.
public final class DelayLineTap: Source {
private static var api: UnsafePointer<playdate_sound_effect_delayline> { effectAPI.pointee.delayline.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_effect_delayline> { Playdate.delayLineAPI.unsafelyUnwrapped }
/// The delay line is retained so the tap stays valid.
private let delayLine: DelayLine
@@ -301,7 +301,7 @@ extension Sound {
/// A delay line effect. Wraps `DelayLine`.
public final class DelayLine: Effect {
private static var api: UnsafePointer<playdate_sound_effect_delayline> { effectAPI.pointee.delayline.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_effect_delayline> { Playdate.delayLineAPI.unsafelyUnwrapped }
/// Creates a delay line holding `length` frames.
public init(length: Int, stereo: Bool = false) {
@@ -340,7 +340,7 @@ extension Sound {
/// An overdrive/distortion effect. Wraps `Overdrive`.
public final class Overdrive: Effect {
private static var api: UnsafePointer<playdate_sound_effect_overdrive> { effectAPI.pointee.overdrive.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_effect_overdrive> { Playdate.overdriveAPI.unsafelyUnwrapped }
private var retainedModulators: [SignalValue] = []
+4 -4
View File
@@ -28,7 +28,7 @@ extension Sound {
/// A signal object; also provides custom signals driven by Swift
/// callbacks. Wraps `PDSynthSignal`.
public final class Signal: SignalValue {
private static var api: UnsafePointer<playdate_sound_signal> { snd.pointee.signal.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_signal> { Playdate.signalAPI.unsafelyUnwrapped }
/// Custom signal callbacks.
public struct Callbacks {
@@ -123,7 +123,7 @@ extension Sound {
/// A low-frequency oscillator signal. Wraps `PDSynthLFO`.
public final class LFO: SignalValue {
private static var api: UnsafePointer<playdate_sound_lfo> { snd.pointee.lfo.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_lfo> { Playdate.lfoAPI.unsafelyUnwrapped }
/// The oscillator's waveform.
public enum Shape: UInt32, Sendable {
@@ -232,7 +232,7 @@ extension Sound {
/// An ADSR envelope signal. Wraps `PDSynthEnvelope`.
public final class Envelope: SignalValue {
private static var api: UnsafePointer<playdate_sound_envelope> { snd.pointee.envelope.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_envelope> { Playdate.envelopeAPI.unsafelyUnwrapped }
/// Creates an envelope with the given attack and decay times
/// (seconds), sustain level (0...1), and release time (seconds).
@@ -305,7 +305,7 @@ extension Sound {
/// A signal whose values are set on a sequence timeline. Wraps
/// `ControlSignal`.
public final class ControlSignal: SignalValue {
private static var api: UnsafePointer<playdate_control_signal> { snd.pointee.controlsignal.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_control_signal> { Playdate.controlSignalAPI.unsafelyUnwrapped }
public init() {
let pointer = ControlSignal.api.pointee.newSignal.unsafelyUnwrapped()
+4 -4
View File
@@ -9,7 +9,7 @@ extension Sound {
/// A source of audio: the base class of `FilePlayer`, `SamplePlayer`,
/// `Synth`, `DelayLineTap`, and `CallbackSource`. Wraps `SoundSource`.
public class Source {
private static var api: UnsafePointer<playdate_sound_source> { snd.pointee.source.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_source> { Playdate.sourceAPI.unsafelyUnwrapped }
/// The underlying C object. Set once, immediately after creation.
var pointer: OpaquePointer!
@@ -106,7 +106,7 @@ extension Sound {
/// Streams audio from a file. Wraps `FilePlayer`.
public final class FilePlayer: Source {
private static var api: UnsafePointer<playdate_sound_fileplayer> { snd.pointee.fileplayer.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_fileplayer> { Playdate.filePlayerAPI.unsafelyUnwrapped }
var loopCallback: ((FilePlayer) -> Void)?
var fadeCallback: ((FilePlayer) -> Void)?
@@ -256,7 +256,7 @@ extension Sound {
/// Audio data loaded into memory. Wraps `AudioSample`.
public final class AudioSample {
private static var api: UnsafePointer<playdate_sound_sample> { snd.pointee.sample.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_sample> { Playdate.sampleAPI.unsafelyUnwrapped }
let pointer: OpaquePointer
let isOwned: Bool
@@ -337,7 +337,7 @@ extension Sound {
/// Plays an `AudioSample` from memory. Wraps `SamplePlayer`.
public final class SamplePlayer: Source {
private static var api: UnsafePointer<playdate_sound_sampleplayer> { snd.pointee.sampleplayer.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_sampleplayer> { Playdate.samplePlayerAPI.unsafelyUnwrapped }
var loopCallback: ((SamplePlayer) -> Void)?
private var retainedSample: AudioSample?
+4 -4
View File
@@ -8,7 +8,7 @@ internal import CPlaydate
extension Sound {
/// A synthesizer voice. Wraps `PDSynth`.
public final class Synth: Source {
private static var api: UnsafePointer<playdate_sound_synth> { snd.pointee.synth.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_synth> { Playdate.synthAPI.unsafelyUnwrapped }
/// The synth's waveform.
public enum Waveform: UInt32, Sendable {
@@ -268,7 +268,7 @@ extension Sound {
/// A bank of synth voices for playing a sequence track. Wraps
/// `PDSynthInstrument`.
public final class Instrument {
private static var api: UnsafePointer<playdate_sound_instrument> { snd.pointee.instrument.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_instrument> { Playdate.instrumentAPI.unsafelyUnwrapped }
let pointer: OpaquePointer
let isOwned: Bool
@@ -372,7 +372,7 @@ extension Sound {
/// A track of notes played by an instrument. Wraps `SequenceTrack`.
public final class SequenceTrack {
private static var api: UnsafePointer<playdate_sound_track> { snd.pointee.track.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_track> { Playdate.trackAPI.unsafelyUnwrapped }
let pointer: OpaquePointer
let isOwned: Bool
@@ -486,7 +486,7 @@ extension Sound {
/// A collection of tracks with tempo and loop control, playable from a
/// MIDI file. Wraps `SoundSequence`.
public final class Sequence {
private static var api: UnsafePointer<playdate_sound_sequence> { snd.pointee.sequence.unsafelyUnwrapped }
private static var api: UnsafePointer<playdate_sound_sequence> { Playdate.sequenceAPI.unsafelyUnwrapped }
let pointer: OpaquePointer
private var retainedTracks: [SequenceTrack] = []
+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)
}
}
+6 -4
View File
@@ -58,12 +58,14 @@ extension String {
/// Copies the string into a newly allocated null-terminated C string.
/// The caller owns the memory and must free it with `deallocate()`.
func copiedPlaydateCString() -> UnsafeMutablePointer<CChar> {
let utf8 = ContiguousArray(self.utf8)
let buffer = UnsafeMutablePointer<CChar>.allocate(capacity: utf8.count + 1)
for (index, byte) in utf8.enumerated() {
let count = utf8.count
let buffer = UnsafeMutablePointer<CChar>.allocate(capacity: count + 1)
var index = 0
for byte in utf8 {
buffer[index] = CChar(bitPattern: byte)
index += 1
}
buffer[utf8.count] = 0
buffer[count] = 0
return buffer
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ internal import CPlaydate
public enum System {}
extension System {
private static var api: UnsafePointer<playdate_sys> { Playdate.systemAPI }
private static var api: UnsafePointer<playdate_sys> { Playdate.systemAPI.unsafelyUnwrapped }
// MARK: - Types