Remove per-call overhead from API access and text conversion

Two hot-path optimizations for the device's Cortex-M7 (and debug simulator
builds):

- The ten sub-API pointers are cached once in Playdate.initialize(with:);
  wrapper accessors now return UnsafePointer and call sites read a single
  field through it, instead of unwrapping the optional API struct and
  copying a whole sub-API struct of function pointers on every call.
  Nested sub-APIs (sound classes, effects, video, tilemap, http/tcp)
  derive from the cached pointers with one field load.

- String -> C conversions (withPlaydateCString, and a new withPlaydateUTF8
  used by the text drawing/measuring APIs) use withUnsafeTemporaryAllocation
  instead of building a ContiguousArray, so logging and drawText in the
  update loop no longer heap-allocate per call. Verified within the
  Embedded Swift subset by the device cross-compile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 07:23:19 +02:00
co-authored by Claude Fable 5
parent e4e99cc894
commit d914e0f8fb
20 changed files with 682 additions and 643 deletions
+21 -21
View File
@@ -6,8 +6,8 @@
internal import CPlaydate
private var videoAPI: playdate_video { gfx.video.pointee }
private var streamAPI: playdate_videostream { gfx.videostream.pointee }
private var videoAPI: UnsafePointer<playdate_video> { gfx.pointee.video.unsafelyUnwrapped }
private var streamAPI: UnsafePointer<playdate_videostream> { gfx.pointee.videostream.unsafelyUnwrapped }
extension Graphics {
/// Plays .pdv video files. Wraps `LCDVideoPlayer`.
@@ -24,7 +24,7 @@ extension Graphics {
/// Opens the .pdv file at `path`.
public convenience init(path: String) throws(PlaydateError) {
let pointer = path.withPlaydateCString { videoAPI.loadVideo.unsafelyUnwrapped($0) }
let pointer = path.withPlaydateCString { videoAPI.pointee.loadVideo.unsafelyUnwrapped($0) }
guard let pointer else {
throw PlaydateError(message: "unable to load video: \(path)")
}
@@ -33,13 +33,13 @@ extension Graphics {
deinit {
if isOwned {
videoAPI.freePlayer.unsafelyUnwrapped(pointer)
videoAPI.pointee.freePlayer.unsafelyUnwrapped(pointer)
}
}
/// Sets the bitmap the video renders into.
public func setContext(_ context: Bitmap) throws(PlaydateError) {
guard videoAPI.setContext.unsafelyUnwrapped(pointer, context.pointer) != 0 else {
guard videoAPI.pointee.setContext.unsafelyUnwrapped(pointer, context.pointer) != 0 else {
throw PlaydateError(message: error ?? "unable to set video context")
}
retainedContext = context
@@ -47,33 +47,33 @@ extension Graphics {
/// The bitmap the video renders into.
public var context: Bitmap? {
guard let context = videoAPI.getContext.unsafelyUnwrapped(pointer) else { return nil }
guard let context = videoAPI.pointee.getContext.unsafelyUnwrapped(pointer) else { return nil }
return Bitmap(pointer: context, isOwned: false)
}
/// Renders directly into the display framebuffer.
public func useScreenContext() {
retainedContext = nil
videoAPI.useScreenContext.unsafelyUnwrapped(pointer)
videoAPI.pointee.useScreenContext.unsafelyUnwrapped(pointer)
}
/// Renders frame `frame` into the current context.
public func renderFrame(_ frame: Int) throws(PlaydateError) {
guard videoAPI.renderFrame.unsafelyUnwrapped(pointer, Int32(frame)) != 0 else {
guard videoAPI.pointee.renderFrame.unsafelyUnwrapped(pointer, Int32(frame)) != 0 else {
throw PlaydateError(message: error ?? "unable to render frame \(frame)")
}
}
/// The most recent error message, if any.
public var error: String? {
String(playdateCString: videoAPI.getError.unsafelyUnwrapped(pointer))
String(playdateCString: videoAPI.pointee.getError.unsafelyUnwrapped(pointer))
}
/// The video's dimensions, frame rate, frame count, and current frame.
public var info: (width: Int, height: Int, frameRate: Float, frameCount: Int, currentFrame: Int) {
var width: Int32 = 0, height: Int32 = 0, frameCount: Int32 = 0, currentFrame: Int32 = 0
var frameRate: Float = 0
videoAPI.getInfo.unsafelyUnwrapped(pointer, &width, &height, &frameRate,
videoAPI.pointee.getInfo.unsafelyUnwrapped(pointer, &width, &height, &frameRate,
&frameCount, &currentFrame)
return (Int(width), Int(height), frameRate, Int(frameCount), Int(currentFrame))
}
@@ -87,62 +87,62 @@ extension Graphics {
private var retainedSource: AnyObject?
public init() {
pointer = streamAPI.newPlayer.unsafelyUnwrapped().unsafelyUnwrapped
pointer = streamAPI.pointee.newPlayer.unsafelyUnwrapped().unsafelyUnwrapped
}
deinit {
streamAPI.freePlayer.unsafelyUnwrapped(pointer)
streamAPI.pointee.freePlayer.unsafelyUnwrapped(pointer)
}
/// Sets the sizes of the stream's video and audio buffers, in bytes.
public func setBufferSize(video: Int, audio: Int) {
streamAPI.setBufferSize.unsafelyUnwrapped(pointer, Int32(video), Int32(audio))
streamAPI.pointee.setBufferSize.unsafelyUnwrapped(pointer, Int32(video), Int32(audio))
}
/// Streams from an open file.
public func setFile(_ file: File.Handle) {
retainedSource = file
streamAPI.setFile.unsafelyUnwrapped(pointer, file.pointer)
streamAPI.pointee.setFile.unsafelyUnwrapped(pointer, file.pointer)
}
/// Streams from an HTTP connection.
public func setHTTPConnection(_ connection: Network.HTTPConnection) {
retainedSource = connection
streamAPI.setHTTPConnection.unsafelyUnwrapped(pointer, connection.pointer)
streamAPI.pointee.setHTTPConnection.unsafelyUnwrapped(pointer, connection.pointer)
}
/// Streams from a TCP connection.
public func setTCPConnection(_ connection: Network.TCPConnection) {
retainedSource = connection
streamAPI.setTCPConnection.unsafelyUnwrapped(pointer, connection.pointer)
streamAPI.pointee.setTCPConnection.unsafelyUnwrapped(pointer, connection.pointer)
}
/// The player used for the stream's audio track. Owned by the stream.
public var filePlayer: Sound.FilePlayer? {
guard let player = streamAPI.getFilePlayer.unsafelyUnwrapped(pointer) else { return nil }
guard let player = streamAPI.pointee.getFilePlayer.unsafelyUnwrapped(pointer) else { return nil }
return Sound.FilePlayer(pointer: player, isOwned: false)
}
/// The player used for the stream's video track. Owned by the stream.
public var videoPlayer: VideoPlayer? {
guard let player = streamAPI.getVideoPlayer.unsafelyUnwrapped(pointer) else { return nil }
guard let player = streamAPI.pointee.getVideoPlayer.unsafelyUnwrapped(pointer) else { return nil }
return VideoPlayer(pointer: player, isOwned: false)
}
/// Advances the stream. Returns `true` if a frame was drawn.
@discardableResult
public func update() -> Bool {
streamAPI.update.unsafelyUnwrapped(pointer)
streamAPI.pointee.update.unsafelyUnwrapped(pointer)
}
/// The number of video frames currently buffered.
public var bufferedFrameCount: Int {
Int(streamAPI.getBufferedFrameCount.unsafelyUnwrapped(pointer))
Int(streamAPI.pointee.getBufferedFrameCount.unsafelyUnwrapped(pointer))
}
/// The total number of bytes read from the source.
public var bytesRead: UInt32 {
streamAPI.getBytesRead.unsafelyUnwrapped(pointer)
streamAPI.pointee.getBytesRead.unsafelyUnwrapped(pointer)
}
}
}