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
+57 -57
View File
@@ -9,9 +9,9 @@
internal import CPlaydate
private var networkAPI: playdate_network { Playdate.api.network.pointee }
private var httpAPI: playdate_http { networkAPI.http.pointee }
private var tcpAPI: playdate_tcp { networkAPI.tcp.pointee }
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 }
/// The network API: wifi status, HTTP, and TCP.
public enum Network {}
@@ -66,7 +66,7 @@ extension Network {
}
public static var status: WifiStatus {
WifiStatus(rawValue: networkAPI.getStatus.unsafelyUnwrapped().rawValue) ?? .notConnected
WifiStatus(rawValue: networkAPI.pointee.getStatus.unsafelyUnwrapped().rawValue) ?? .notConnected
}
/// Turns the wifi radio on or off. The completion receives `nil` on
@@ -74,13 +74,13 @@ extension Network {
public static func setEnabled(_ enabled: Bool, completion: ((NetError?) -> Void)? = nil) {
setEnabledCompletion = completion
if completion != nil {
networkAPI.setEnabled.unsafelyUnwrapped(enabled, { error in
networkAPI.pointee.setEnabled.unsafelyUnwrapped(enabled, { error in
let completion = Network.setEnabledCompletion
Network.setEnabledCompletion = nil
completion?(Network.optionalError(error))
})
} else {
networkAPI.setEnabled.unsafelyUnwrapped(enabled, nil)
networkAPI.pointee.setEnabled.unsafelyUnwrapped(enabled, nil)
}
}
@@ -137,7 +137,7 @@ extension Network {
purpose: String? = nil,
completion: @escaping (Bool) -> Void) -> AccessReply {
Network.requestAccess(
rawRequest: { httpAPI.requestAccess.unsafelyUnwrapped($0, $1, $2, $3, $4, $5) },
rawRequest: { httpAPI.pointee.requestAccess.unsafelyUnwrapped($0, $1, $2, $3, $4, $5) },
server: server, port: port, useSSL: useSSL, purpose: purpose,
completion: completion)
}
@@ -146,21 +146,21 @@ extension Network {
/// granted.
public init?(server: String, port: Int = 443, useSSL: Bool = true) {
let pointer = server.withPlaydateCString {
httpAPI.newConnection.unsafelyUnwrapped($0, Int32(port), useSSL)
httpAPI.pointee.newConnection.unsafelyUnwrapped($0, Int32(port), useSSL)
}
guard let pointer else { return nil }
self.pointer = pointer
httpAPI.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque())
httpAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque())
}
deinit {
httpAPI.setUserdata.unsafelyUnwrapped(pointer, nil)
httpAPI.release.unsafelyUnwrapped(pointer)
httpAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, nil)
httpAPI.pointee.release.unsafelyUnwrapped(pointer)
}
private static func wrapper(for pointer: OpaquePointer?) -> HTTPConnection? {
guard let pointer,
let userdata = httpAPI.getUserdata.unsafelyUnwrapped(pointer) else { return nil }
let userdata = httpAPI.pointee.getUserdata.unsafelyUnwrapped(pointer) else { return nil }
return Unmanaged<HTTPConnection>.fromOpaque(userdata).takeUnretainedValue()
}
@@ -168,27 +168,27 @@ extension Network {
/// The time to wait for the connection to open, in milliseconds.
public func setConnectTimeout(milliseconds: Int) {
httpAPI.setConnectTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
httpAPI.pointee.setConnectTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
}
/// Whether to keep the connection open after a request completes.
public func setKeepAlive(_ keepAlive: Bool) {
httpAPI.setKeepAlive.unsafelyUnwrapped(pointer, keepAlive)
httpAPI.pointee.setKeepAlive.unsafelyUnwrapped(pointer, keepAlive)
}
/// Adds a `Range: bytes=start-end` header to future requests.
public func setByteRange(start: Int, end: Int) {
httpAPI.setByteRange.unsafelyUnwrapped(pointer, Int32(start), Int32(end))
httpAPI.pointee.setByteRange.unsafelyUnwrapped(pointer, Int32(start), Int32(end))
}
/// The time to wait for incoming data, in milliseconds.
public func setReadTimeout(milliseconds: Int) {
httpAPI.setReadTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
httpAPI.pointee.setReadTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
}
/// The size of the connection's read buffer, in bytes.
public func setReadBufferSize(bytes: Int) {
httpAPI.setReadBufferSize.unsafelyUnwrapped(pointer, Int32(bytes))
httpAPI.pointee.setReadBufferSize.unsafelyUnwrapped(pointer, Int32(bytes))
}
// MARK: Requests
@@ -198,7 +198,7 @@ extension Network {
public func get(path: String, headers: String = "") throws(NetError) {
let error = path.withPlaydateCString { cPath in
headers.withPlaydateCString { cHeaders in
httpAPI.get.unsafelyUnwrapped(pointer, cPath, cHeaders, headers.utf8.count)
httpAPI.pointee.get.unsafelyUnwrapped(pointer, cPath, cHeaders, headers.utf8.count)
}
}
try Network.check(error)
@@ -209,7 +209,7 @@ extension Network {
let error = path.withPlaydateCString { cPath in
headers.withPlaydateCString { cHeaders in
body.withUnsafeBytes { bodyBuffer in
httpAPI.post.unsafelyUnwrapped(
httpAPI.pointee.post.unsafelyUnwrapped(
pointer, cPath, cHeaders, headers.utf8.count,
bodyBuffer.baseAddress?.assumingMemoryBound(to: CChar.self),
bodyBuffer.count)
@@ -226,7 +226,7 @@ extension Network {
path.withPlaydateCString { cPath in
headers.withPlaydateCString { cHeaders in
body.withUnsafeBytes { bodyBuffer in
httpAPI.query.unsafelyUnwrapped(
httpAPI.pointee.query.unsafelyUnwrapped(
pointer, cMethod, cPath, cHeaders, headers.utf8.count,
bodyBuffer.baseAddress?.assumingMemoryBound(to: CChar.self),
bodyBuffer.count)
@@ -241,31 +241,31 @@ extension Network {
/// The last error on the connection, if any.
public var error: NetError? {
Network.optionalError(httpAPI.getError.unsafelyUnwrapped(pointer))
Network.optionalError(httpAPI.pointee.getError.unsafelyUnwrapped(pointer))
}
/// The number of bytes read of the current response, and the total
/// expected (0 if the response has no Content-Length).
public var progress: (read: Int, total: Int) {
var read: Int32 = 0, total: Int32 = 0
httpAPI.getProgress.unsafelyUnwrapped(pointer, &read, &total)
httpAPI.pointee.getProgress.unsafelyUnwrapped(pointer, &read, &total)
return (Int(read), Int(total))
}
/// The HTTP status code of the response.
public var responseStatus: Int {
Int(httpAPI.getResponseStatus.unsafelyUnwrapped(pointer))
Int(httpAPI.pointee.getResponseStatus.unsafelyUnwrapped(pointer))
}
/// The number of response bytes available to read.
public var bytesAvailable: Int {
Int(httpAPI.getBytesAvailable.unsafelyUnwrapped(pointer))
Int(httpAPI.pointee.getBytesAvailable.unsafelyUnwrapped(pointer))
}
/// Reads up to `buffer.count` response bytes. Returns the number of
/// bytes read.
public func read(into buffer: UnsafeMutableRawBufferPointer) throws(NetError) -> Int {
let result = httpAPI.read.unsafelyUnwrapped(pointer, buffer.baseAddress,
let result = httpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress,
UInt32(buffer.count))
if result < 0 {
throw NetError(rawValue: result) ?? .unknown
@@ -277,7 +277,7 @@ extension Network {
public func read(length: Int) throws(NetError) -> [UInt8] {
var bytes = [UInt8](repeating: 0, count: length)
let result = bytes.withUnsafeMutableBytes { buffer in
httpAPI.read.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
httpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
}
if result < 0 {
throw NetError(rawValue: result) ?? .unknown
@@ -288,7 +288,7 @@ extension Network {
/// Closes the connection.
public func close() {
httpAPI.close.unsafelyUnwrapped(pointer)
httpAPI.pointee.close.unsafelyUnwrapped(pointer)
}
// MARK: Callbacks
@@ -297,14 +297,14 @@ extension Network {
public func setHeaderReceivedCallback(_ callback: ((HTTPConnection, _ key: String, _ value: String) -> Void)?) {
headerReceivedCallback = callback
if callback != nil {
httpAPI.setHeaderReceivedCallback.unsafelyUnwrapped(pointer, { connection, key, value in
httpAPI.pointee.setHeaderReceivedCallback.unsafelyUnwrapped(pointer, { connection, key, value in
guard let wrapper = HTTPConnection.wrapper(for: connection),
let key = String(playdateCString: key),
let value = String(playdateCString: value) else { return }
wrapper.headerReceivedCallback?(wrapper, key, value)
})
} else {
httpAPI.setHeaderReceivedCallback.unsafelyUnwrapped(pointer, nil)
httpAPI.pointee.setHeaderReceivedCallback.unsafelyUnwrapped(pointer, nil)
}
}
@@ -312,12 +312,12 @@ extension Network {
public func setHeadersReadCallback(_ callback: ((HTTPConnection) -> Void)?) {
headersReadCallback = callback
if callback != nil {
httpAPI.setHeadersReadCallback.unsafelyUnwrapped(pointer, { connection in
httpAPI.pointee.setHeadersReadCallback.unsafelyUnwrapped(pointer, { connection in
guard let wrapper = HTTPConnection.wrapper(for: connection) else { return }
wrapper.headersReadCallback?(wrapper)
})
} else {
httpAPI.setHeadersReadCallback.unsafelyUnwrapped(pointer, nil)
httpAPI.pointee.setHeadersReadCallback.unsafelyUnwrapped(pointer, nil)
}
}
@@ -325,12 +325,12 @@ extension Network {
public func setResponseCallback(_ callback: ((HTTPConnection) -> Void)?) {
responseCallback = callback
if callback != nil {
httpAPI.setResponseCallback.unsafelyUnwrapped(pointer, { connection in
httpAPI.pointee.setResponseCallback.unsafelyUnwrapped(pointer, { connection in
guard let wrapper = HTTPConnection.wrapper(for: connection) else { return }
wrapper.responseCallback?(wrapper)
})
} else {
httpAPI.setResponseCallback.unsafelyUnwrapped(pointer, nil)
httpAPI.pointee.setResponseCallback.unsafelyUnwrapped(pointer, nil)
}
}
@@ -338,12 +338,12 @@ extension Network {
public func setRequestCompleteCallback(_ callback: ((HTTPConnection) -> Void)?) {
requestCompleteCallback = callback
if callback != nil {
httpAPI.setRequestCompleteCallback.unsafelyUnwrapped(pointer, { connection in
httpAPI.pointee.setRequestCompleteCallback.unsafelyUnwrapped(pointer, { connection in
guard let wrapper = HTTPConnection.wrapper(for: connection) else { return }
wrapper.requestCompleteCallback?(wrapper)
})
} else {
httpAPI.setRequestCompleteCallback.unsafelyUnwrapped(pointer, nil)
httpAPI.pointee.setRequestCompleteCallback.unsafelyUnwrapped(pointer, nil)
}
}
@@ -351,12 +351,12 @@ extension Network {
public func setConnectionClosedCallback(_ callback: ((HTTPConnection) -> Void)?) {
connectionClosedCallback = callback
if callback != nil {
httpAPI.setConnectionClosedCallback.unsafelyUnwrapped(pointer, { connection in
httpAPI.pointee.setConnectionClosedCallback.unsafelyUnwrapped(pointer, { connection in
guard let wrapper = HTTPConnection.wrapper(for: connection) else { return }
wrapper.connectionClosedCallback?(wrapper)
})
} else {
httpAPI.setConnectionClosedCallback.unsafelyUnwrapped(pointer, nil)
httpAPI.pointee.setConnectionClosedCallback.unsafelyUnwrapped(pointer, nil)
}
}
}
@@ -377,7 +377,7 @@ extension Network {
purpose: String? = nil,
completion: @escaping (Bool) -> Void) -> AccessReply {
Network.requestAccess(
rawRequest: { tcpAPI.requestAccess.unsafelyUnwrapped($0, $1, $2, $3, $4, $5) },
rawRequest: { tcpAPI.pointee.requestAccess.unsafelyUnwrapped($0, $1, $2, $3, $4, $5) },
server: server, port: port, useSSL: useSSL, purpose: purpose,
completion: completion)
}
@@ -386,38 +386,38 @@ extension Network {
/// granted. Call `open(_:)` to connect.
public init?(server: String, port: Int, useSSL: Bool = true) {
let pointer = server.withPlaydateCString {
tcpAPI.newConnection.unsafelyUnwrapped($0, Int32(port), useSSL)
tcpAPI.pointee.newConnection.unsafelyUnwrapped($0, Int32(port), useSSL)
}
guard let pointer else { return nil }
self.pointer = pointer
tcpAPI.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque())
tcpAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque())
}
deinit {
tcpAPI.setUserdata.unsafelyUnwrapped(pointer, nil)
tcpAPI.release.unsafelyUnwrapped(pointer)
tcpAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, nil)
tcpAPI.pointee.release.unsafelyUnwrapped(pointer)
}
private static func wrapper(for pointer: OpaquePointer?) -> TCPConnection? {
guard let pointer,
let userdata = tcpAPI.getUserdata.unsafelyUnwrapped(pointer) else { return nil }
let userdata = tcpAPI.pointee.getUserdata.unsafelyUnwrapped(pointer) else { return nil }
return Unmanaged<TCPConnection>.fromOpaque(userdata).takeUnretainedValue()
}
/// The last error on the connection, if any.
public var error: NetError? {
Network.optionalError(tcpAPI.getError.unsafelyUnwrapped(pointer))
Network.optionalError(tcpAPI.pointee.getError.unsafelyUnwrapped(pointer))
}
/// The time to wait for the connection to open, in milliseconds.
public func setConnectTimeout(milliseconds: Int) {
tcpAPI.setConnectTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
tcpAPI.pointee.setConnectTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
}
/// Opens the connection. The completion receives `nil` on success.
public func open(_ completion: @escaping (TCPConnection, NetError?) -> Void) throws(NetError) {
openCompletion = completion
let error = tcpAPI.open.unsafelyUnwrapped(pointer, { connection, error, _ in
let error = tcpAPI.pointee.open.unsafelyUnwrapped(pointer, { connection, error, _ in
guard let wrapper = TCPConnection.wrapper(for: connection) else { return }
let completion = wrapper.openCompletion
wrapper.openCompletion = nil
@@ -428,7 +428,7 @@ extension Network {
/// Closes the connection.
public func close() throws(NetError) {
try Network.check(tcpAPI.close.unsafelyUnwrapped(pointer))
try Network.check(tcpAPI.pointee.close.unsafelyUnwrapped(pointer))
}
/// Called when the connection closes, with the reason if it closed
@@ -436,39 +436,39 @@ extension Network {
public func setConnectionClosedCallback(_ callback: ((TCPConnection, NetError?) -> Void)?) {
connectionClosedCallback = callback
if callback != nil {
tcpAPI.setConnectionClosedCallback.unsafelyUnwrapped(pointer, { connection, error in
tcpAPI.pointee.setConnectionClosedCallback.unsafelyUnwrapped(pointer, { connection, error in
guard let wrapper = TCPConnection.wrapper(for: connection) else { return }
wrapper.connectionClosedCallback?(wrapper, Network.optionalError(error))
})
} else {
tcpAPI.setConnectionClosedCallback.unsafelyUnwrapped(pointer, nil)
tcpAPI.pointee.setConnectionClosedCallback.unsafelyUnwrapped(pointer, nil)
}
}
/// The time to wait for incoming data, in milliseconds.
public func setReadTimeout(milliseconds: Int) {
tcpAPI.setReadTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
tcpAPI.pointee.setReadTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
}
/// The size of the connection's read buffer, in bytes.
public func setReadBufferSize(bytes: Int) {
tcpAPI.setReadBufferSize.unsafelyUnwrapped(pointer, Int32(bytes))
tcpAPI.pointee.setReadBufferSize.unsafelyUnwrapped(pointer, Int32(bytes))
}
/// The number of bytes available to read.
public var bytesAvailable: Int {
Int(tcpAPI.getBytesAvailable.unsafelyUnwrapped(pointer))
Int(tcpAPI.pointee.getBytesAvailable.unsafelyUnwrapped(pointer))
}
/// The number of written bytes not yet sent on the wire.
public var sentBytesPending: Int {
Int(tcpAPI.getSentBytesPending.unsafelyUnwrapped(pointer))
Int(tcpAPI.pointee.getSentBytesPending.unsafelyUnwrapped(pointer))
}
/// Reads up to `buffer.count` bytes, waiting up to the read timeout.
/// Returns the number of bytes read.
public func read(into buffer: UnsafeMutableRawBufferPointer) throws(NetError) -> Int {
let result = tcpAPI.read.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
let result = tcpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
if result < 0 {
throw NetError(rawValue: result) ?? .unknown
}
@@ -479,7 +479,7 @@ extension Network {
public func read(length: Int) throws(NetError) -> [UInt8] {
var bytes = [UInt8](repeating: 0, count: length)
let result = bytes.withUnsafeMutableBytes { buffer in
tcpAPI.read.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
tcpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
}
if result < 0 {
throw NetError(rawValue: result) ?? .unknown
@@ -492,7 +492,7 @@ extension Network {
/// accepted.
@discardableResult
public func write(_ buffer: UnsafeRawBufferPointer) throws(NetError) -> Int {
let result = tcpAPI.write.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
let result = tcpAPI.pointee.write.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
if result < 0 {
throw NetError(rawValue: result) ?? .unknown
}
@@ -504,7 +504,7 @@ extension Network {
@discardableResult
public func write(_ bytes: [UInt8]) throws(NetError) -> Int {
let result = bytes.withUnsafeBytes { buffer in
tcpAPI.write.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
tcpAPI.pointee.write.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
}
if result < 0 {
throw NetError(rawValue: result) ?? .unknown