diff --git a/Examples/HelloPlaydate/Source/pdex.elf b/Examples/HelloPlaydate/Source/pdex.elf index a0ff305..c35103c 100755 Binary files a/Examples/HelloPlaydate/Source/pdex.elf and b/Examples/HelloPlaydate/Source/pdex.elf differ diff --git a/Sources/PlayDate/Display.swift b/Sources/PlayDate/Display.swift index 3e31b4f..849930a 100644 --- a/Sources/PlayDate/Display.swift +++ b/Sources/PlayDate/Display.swift @@ -9,7 +9,7 @@ internal import CPlaydate public enum Display {} extension Display { - private static var api: UnsafePointer { Playdate.displayAPI } + private static var api: UnsafePointer { Playdate.displayAPI.unsafelyUnwrapped } /// The display width in pixels, taking the current scale into account. public static var width: Int { Int(api.pointee.getWidth.unsafelyUnwrapped()) } diff --git a/Sources/PlayDate/File.swift b/Sources/PlayDate/File.swift index a9f0ac2..89a8299 100644 --- a/Sources/PlayDate/File.swift +++ b/Sources/PlayDate/File.swift @@ -8,7 +8,7 @@ internal import CPlaydate -private var fileAPI: UnsafePointer { Playdate.fileAPI } +private var fileAPI: UnsafePointer { 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. diff --git a/Sources/PlayDate/Graphics.swift b/Sources/PlayDate/Graphics.swift index da8bc42..982f307 100644 --- a/Sources/PlayDate/Graphics.swift +++ b/Sources/PlayDate/Graphics.swift @@ -10,7 +10,7 @@ internal import CPlaydate /// The graphics API: drawing, bitmaps, fonts, tilemaps, and video. public enum Graphics {} -var gfx: UnsafePointer { Playdate.graphicsAPI } +var gfx: UnsafePointer { 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) } } diff --git a/Sources/PlayDate/GraphicsBitmap.swift b/Sources/PlayDate/GraphicsBitmap.swift index cf0b9c8..a036c51 100644 --- a/Sources/PlayDate/GraphicsBitmap.swift +++ b/Sources/PlayDate/GraphicsBitmap.swift @@ -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? path.withPlaydateCString { gfx.pointee.loadIntoBitmap.unsafelyUnwrapped($0, pointer, &error) } + cachedSize = nil if let error { throw PlaydateError(cString: error) } } diff --git a/Sources/PlayDate/GraphicsTileMap.swift b/Sources/PlayDate/GraphicsTileMap.swift index e709497..a1ec4da 100644 --- a/Sources/PlayDate/GraphicsTileMap.swift +++ b/Sources/PlayDate/GraphicsTileMap.swift @@ -5,7 +5,7 @@ internal import CPlaydate -private var tilemapAPI: UnsafePointer { gfx.pointee.tilemap.unsafelyUnwrapped } +private var tilemapAPI: UnsafePointer { Playdate.tilemapAPI.unsafelyUnwrapped } extension Graphics { /// A grid of tiles drawn from a bitmap table. Wraps `LCDTileMap`. diff --git a/Sources/PlayDate/GraphicsVideo.swift b/Sources/PlayDate/GraphicsVideo.swift index 96ab78e..5f65b5f 100644 --- a/Sources/PlayDate/GraphicsVideo.swift +++ b/Sources/PlayDate/GraphicsVideo.swift @@ -6,8 +6,8 @@ internal import CPlaydate -private var videoAPI: UnsafePointer { gfx.pointee.video.unsafelyUnwrapped } -private var streamAPI: UnsafePointer { gfx.pointee.videostream.unsafelyUnwrapped } +private var videoAPI: UnsafePointer { Playdate.videoAPI.unsafelyUnwrapped } +private var streamAPI: UnsafePointer { Playdate.videoStreamAPI.unsafelyUnwrapped } extension Graphics { /// Plays .pdv video files. Wraps `LCDVideoPlayer`. diff --git a/Sources/PlayDate/JSON.swift b/Sources/PlayDate/JSON.swift index a8a3e7c..9af6f13 100644 --- a/Sources/PlayDate/JSON.swift +++ b/Sources/PlayDate/JSON.swift @@ -9,7 +9,7 @@ internal import CPlaydate -private var jsonAPI: UnsafePointer { Playdate.jsonAPI } +private var jsonAPI: UnsafePointer { 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.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 } diff --git a/Sources/PlayDate/Lua.swift b/Sources/PlayDate/Lua.swift index d6e5ffd..d99f810 100644 --- a/Sources/PlayDate/Lua.swift +++ b/Sources/PlayDate/Lua.swift @@ -9,7 +9,7 @@ public import CPlaydate -private var luaAPI: UnsafePointer { Playdate.luaAPI } +private var luaAPI: UnsafePointer { 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) } diff --git a/Sources/PlayDate/Network.swift b/Sources/PlayDate/Network.swift index 593d855..9865382 100644 --- a/Sources/PlayDate/Network.swift +++ b/Sources/PlayDate/Network.swift @@ -9,9 +9,9 @@ internal import CPlaydate -private var networkAPI: UnsafePointer { Playdate.networkAPI } -private var httpAPI: UnsafePointer { networkAPI.pointee.http.unsafelyUnwrapped } -private var tcpAPI: UnsafePointer { networkAPI.pointee.tcp.unsafelyUnwrapped } +private var networkAPI: UnsafePointer { Playdate.networkAPI.unsafelyUnwrapped } +private var httpAPI: UnsafePointer { Playdate.httpAPI.unsafelyUnwrapped } +private var tcpAPI: UnsafePointer { Playdate.tcpAPI.unsafelyUnwrapped } /// The network API: wifi status, HTTP, and TCP. public enum Network {} diff --git a/Sources/PlayDate/PlayDate.swift b/Sources/PlayDate/PlayDate.swift index 910d465..6bfb3ac 100644 --- a/Sources/PlayDate/PlayDate.swift +++ b/Sources/PlayDate/PlayDate.swift @@ -37,6 +37,36 @@ public enum Playdate { nonisolated(unsafe) static var scoreboardsAPI: UnsafePointer! nonisolated(unsafe) static var networkAPI: UnsafePointer! + // 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! + nonisolated(unsafe) static var videoAPI: UnsafePointer! + nonisolated(unsafe) static var videoStreamAPI: UnsafePointer! + nonisolated(unsafe) static var channelAPI: UnsafePointer! + nonisolated(unsafe) static var sourceAPI: UnsafePointer! + nonisolated(unsafe) static var filePlayerAPI: UnsafePointer! + nonisolated(unsafe) static var sampleAPI: UnsafePointer! + nonisolated(unsafe) static var samplePlayerAPI: UnsafePointer! + nonisolated(unsafe) static var synthAPI: UnsafePointer! + nonisolated(unsafe) static var instrumentAPI: UnsafePointer! + nonisolated(unsafe) static var trackAPI: UnsafePointer! + nonisolated(unsafe) static var sequenceAPI: UnsafePointer! + nonisolated(unsafe) static var signalAPI: UnsafePointer! + nonisolated(unsafe) static var lfoAPI: UnsafePointer! + nonisolated(unsafe) static var envelopeAPI: UnsafePointer! + nonisolated(unsafe) static var controlSignalAPI: UnsafePointer! + nonisolated(unsafe) static var effectAPI: UnsafePointer! + nonisolated(unsafe) static var twoPoleFilterAPI: UnsafePointer! + nonisolated(unsafe) static var onePoleFilterAPI: UnsafePointer! + nonisolated(unsafe) static var bitCrusherAPI: UnsafePointer! + nonisolated(unsafe) static var ringModulatorAPI: UnsafePointer! + nonisolated(unsafe) static var delayLineAPI: UnsafePointer! + nonisolated(unsafe) static var overdriveAPI: UnsafePointer! + nonisolated(unsafe) static var httpAPI: UnsafePointer! + nonisolated(unsafe) static var tcpAPI: UnsafePointer! + /// 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 } } diff --git a/Sources/PlayDate/Scoreboards.swift b/Sources/PlayDate/Scoreboards.swift index 922da13..8356748 100644 --- a/Sources/PlayDate/Scoreboards.swift +++ b/Sources/PlayDate/Scoreboards.swift @@ -9,7 +9,7 @@ internal import CPlaydate -private var scoreboardsAPI: UnsafePointer { Playdate.scoreboardsAPI } +private var scoreboardsAPI: UnsafePointer { Playdate.scoreboardsAPI.unsafelyUnwrapped } /// The scoreboards API for games with online leaderboards. public enum Scoreboards {} diff --git a/Sources/PlayDate/Sound.swift b/Sources/PlayDate/Sound.swift index 441421b..37cdbe8 100644 --- a/Sources/PlayDate/Sound.swift +++ b/Sources/PlayDate/Sound.swift @@ -7,7 +7,7 @@ internal import CPlaydate -var snd: UnsafePointer { Playdate.soundAPI } +var snd: UnsafePointer { 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 { snd.pointee.channel.unsafelyUnwrapped } + private static var api: UnsafePointer { Playdate.channelAPI.unsafelyUnwrapped } let pointer: OpaquePointer let isOwned: Bool diff --git a/Sources/PlayDate/SoundEffect.swift b/Sources/PlayDate/SoundEffect.swift index c2b156f..21fc5f0 100644 --- a/Sources/PlayDate/SoundEffect.swift +++ b/Sources/PlayDate/SoundEffect.swift @@ -6,7 +6,7 @@ internal import CPlaydate -private var effectAPI: UnsafePointer { snd.pointee.effect.unsafelyUnwrapped } +private var effectAPI: UnsafePointer { 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 { effectAPI.pointee.twopolefilter.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { effectAPI.pointee.onepolefilter.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { effectAPI.pointee.bitcrusher.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { effectAPI.pointee.ringmodulator.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { effectAPI.pointee.delayline.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { effectAPI.pointee.delayline.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { effectAPI.pointee.overdrive.unsafelyUnwrapped } + private static var api: UnsafePointer { Playdate.overdriveAPI.unsafelyUnwrapped } private var retainedModulators: [SignalValue] = [] diff --git a/Sources/PlayDate/SoundSignal.swift b/Sources/PlayDate/SoundSignal.swift index d15754b..58e042b 100644 --- a/Sources/PlayDate/SoundSignal.swift +++ b/Sources/PlayDate/SoundSignal.swift @@ -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 { snd.pointee.signal.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { snd.pointee.lfo.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { snd.pointee.envelope.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { snd.pointee.controlsignal.unsafelyUnwrapped } + private static var api: UnsafePointer { Playdate.controlSignalAPI.unsafelyUnwrapped } public init() { let pointer = ControlSignal.api.pointee.newSignal.unsafelyUnwrapped() diff --git a/Sources/PlayDate/SoundSource.swift b/Sources/PlayDate/SoundSource.swift index a55822c..e484c2b 100644 --- a/Sources/PlayDate/SoundSource.swift +++ b/Sources/PlayDate/SoundSource.swift @@ -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 { snd.pointee.source.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { snd.pointee.fileplayer.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { snd.pointee.sample.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { snd.pointee.sampleplayer.unsafelyUnwrapped } + private static var api: UnsafePointer { Playdate.samplePlayerAPI.unsafelyUnwrapped } var loopCallback: ((SamplePlayer) -> Void)? private var retainedSample: AudioSample? diff --git a/Sources/PlayDate/SoundSynth.swift b/Sources/PlayDate/SoundSynth.swift index 5efe39b..4811ad9 100644 --- a/Sources/PlayDate/SoundSynth.swift +++ b/Sources/PlayDate/SoundSynth.swift @@ -8,7 +8,7 @@ internal import CPlaydate extension Sound { /// A synthesizer voice. Wraps `PDSynth`. public final class Synth: Source { - private static var api: UnsafePointer { snd.pointee.synth.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { snd.pointee.instrument.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { snd.pointee.track.unsafelyUnwrapped } + private static var api: UnsafePointer { 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 { snd.pointee.sequence.unsafelyUnwrapped } + private static var api: UnsafePointer { Playdate.sequenceAPI.unsafelyUnwrapped } let pointer: OpaquePointer private var retainedTracks: [SequenceTrack] = [] diff --git a/Sources/PlayDate/Sprite.swift b/Sources/PlayDate/Sprite.swift index 3ab5788..f89bb8e 100644 --- a/Sources/PlayDate/Sprite.swift +++ b/Sources/PlayDate/Sprite.swift @@ -10,7 +10,7 @@ internal import CPlaydate -private var spriteAPI: UnsafePointer { Playdate.spriteAPI } +private var spriteAPI: UnsafePointer { 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?, + count: Int32, _ visit: (CollisionInfo) -> Void) { + guard let pointer else { return } + for index in 0..?, count: Int32) -> [CollisionInfo] { - guard let pointer else { return [] } var infos = [CollisionInfo]() infos.reserveCapacity(Int(count)) - for index in 0.. 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?, - 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?, + count: Int32, _ visit: (Sprite) -> Void) { + guard let pointer else { return } for index in 0..?, + 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) + } } diff --git a/Sources/PlayDate/Support.swift b/Sources/PlayDate/Support.swift index 2b7de37..661d671 100644 --- a/Sources/PlayDate/Support.swift +++ b/Sources/PlayDate/Support.swift @@ -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 { - let utf8 = ContiguousArray(self.utf8) - let buffer = UnsafeMutablePointer.allocate(capacity: utf8.count + 1) - for (index, byte) in utf8.enumerated() { + let count = utf8.count + let buffer = UnsafeMutablePointer.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 } } diff --git a/Sources/PlayDate/System.swift b/Sources/PlayDate/System.swift index c66dca2..7eab3fe 100644 --- a/Sources/PlayDate/System.swift +++ b/Sources/PlayDate/System.swift @@ -9,7 +9,7 @@ internal import CPlaydate public enum System {} extension System { - private static var api: UnsafePointer { Playdate.systemAPI } + private static var api: UnsafePointer { Playdate.systemAPI.unsafelyUnwrapped } // MARK: - Types