Fixed bugs found for the bitmap mask, JSON reader and wifi wrappers in the library.
This commit is contained in:
@@ -2,15 +2,18 @@ internal import CPlaydate
|
|||||||
|
|
||||||
extension Graphics {
|
extension Graphics {
|
||||||
/// A drawable image and drawing target. Wraps `LCDBitmap`. Bitmaps borrowed from
|
/// A drawable image and drawing target. Wraps `LCDBitmap`. Bitmaps borrowed from
|
||||||
/// tables, fonts, masks, video players, or the system live only as long as their owner.
|
/// tables, fonts, video players, or the system live only as long as their owner.
|
||||||
public final class Bitmap {
|
public final class Bitmap {
|
||||||
let pointer: OpaquePointer
|
let pointer: OpaquePointer
|
||||||
/// Whether deinit frees the `LCDBitmap`.
|
/// Whether deinit frees the `LCDBitmap`.
|
||||||
let isOwned: Bool
|
let isOwned: Bool
|
||||||
|
/// Kept alive because this bitmap shares its pixels.
|
||||||
|
private let owner: Bitmap?
|
||||||
|
|
||||||
init(pointer: OpaquePointer, isOwned: Bool) {
|
init(pointer: OpaquePointer, isOwned: Bool, owner: Bitmap? = nil) {
|
||||||
self.pointer = pointer
|
self.pointer = pointer
|
||||||
self.isOwned = isOwned
|
self.isOwned = isOwned
|
||||||
|
self.owner = owner
|
||||||
}
|
}
|
||||||
|
|
||||||
public convenience init(width: Int, height: Int, backgroundColor: Color = .clear) {
|
public convenience init(width: Int, height: Int, backgroundColor: Color = .clear) {
|
||||||
@@ -126,10 +129,11 @@ extension Graphics {
|
|||||||
gfx.pointee.setBitmapMask.unsafelyUnwrapped(pointer, mask?.pointer) != 0
|
gfx.pointee.setBitmapMask.unsafelyUnwrapped(pointer, mask?.pointer) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shares this bitmap's mask data: drawing into it edits the mask.
|
/// Shares this bitmap's mask data, and keeps this bitmap alive.
|
||||||
public var mask: Bitmap? {
|
public var mask: Bitmap? {
|
||||||
|
// Owned by the caller; pixels are shared with `self`.
|
||||||
guard let mask = gfx.pointee.getBitmapMask.unsafelyUnwrapped(pointer) else { return nil }
|
guard let mask = gfx.pointee.getBitmapMask.unsafelyUnwrapped(pointer) else { return nil }
|
||||||
return Bitmap(pointer: mask, isOwned: false)
|
return Bitmap(pointer: mask, isOwned: true, owner: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether opaque pixels of both bitmaps overlap within the non-empty `rect`.
|
/// Whether opaque pixels of both bitmaps overlap within the non-empty `rect`.
|
||||||
|
|||||||
@@ -120,9 +120,9 @@ extension JSON {
|
|||||||
// Borrowing keeps the `SDFile` open for the whole decode.
|
// Borrowing keeps the `SDFile` open for the whole decode.
|
||||||
reader.userdata = file.pointer
|
reader.userdata = file.pointer
|
||||||
reader.read = { userdata, buffer, size in
|
reader.read = { userdata, buffer, size in
|
||||||
guard let userdata, let buffer else { return -1 }
|
// `file->read` returns 0 at end of data, as the decoder expects.
|
||||||
let count = fileAPI.pointee.read.unsafelyUnwrapped(userdata, buffer, UInt32(size))
|
guard let userdata, let buffer else { return 0 }
|
||||||
return count > 0 ? count : -1
|
return fileAPI.pointee.read.unsafelyUnwrapped(userdata, buffer, UInt32(size))
|
||||||
}
|
}
|
||||||
var outval = json_value()
|
var outval = json_value()
|
||||||
let ok = withExtendedLifetime(context) {
|
let ok = withExtendedLifetime(context) {
|
||||||
|
|||||||
@@ -22,23 +22,28 @@ extension Network {
|
|||||||
WifiStatus(rawValue: UInt32(networkAPI.pointee.getStatus.unsafelyUnwrapped().rawValue)) ?? .notConnected
|
WifiStatus(rawValue: UInt32(networkAPI.pointee.getStatus.unsafelyUnwrapped().rawValue)) ?? .notConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `true` connects to the configured access point; `false` turns wifi off before
|
/// Connects to the access point now. `completion` gets `nil` on success, in call order.
|
||||||
/// the 30 s idle timeout. `completion` (documented for `true` only) gets `nil`
|
public static func enable(completion: ((NetError?) -> Void)? = nil) {
|
||||||
/// on success; completions fire in call order.
|
|
||||||
public static func setEnabled(_ enabled: Bool, completion: ((NetError?) -> Void)? = nil) {
|
|
||||||
if let completion {
|
if let completion {
|
||||||
setEnabledCompletions.append(completion)
|
enableCompletions.append(completion)
|
||||||
networkAPI.pointee.setEnabled.unsafelyUnwrapped(enabled, { error in
|
networkAPI.pointee.setEnabled.unsafelyUnwrapped(true, { error in
|
||||||
guard !Network.setEnabledCompletions.isEmpty else { return }
|
guard !Network.enableCompletions.isEmpty else { return }
|
||||||
let completion = Network.setEnabledCompletions.removeFirst()
|
let completion = Network.enableCompletions.removeFirst()
|
||||||
completion(Network.optionalError(error))
|
completion(Network.optionalError(error))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
networkAPI.pointee.setEnabled.unsafelyUnwrapped(enabled, nil)
|
networkAPI.pointee.setEnabled.unsafelyUnwrapped(true, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated(unsafe) private static var setEnabledCompletions: [(NetError?) -> Void] = []
|
/// Turns wifi off now, not after the 30 s idle timeout.
|
||||||
|
public static func disable() {
|
||||||
|
// No callback: C documents it for enabling only, and a queued one would take
|
||||||
|
// the next `enable` result.
|
||||||
|
networkAPI.pointee.setEnabled.unsafelyUnwrapped(false, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated(unsafe) private static var enableCompletions: [(NetError?) -> Void] = []
|
||||||
|
|
||||||
/// Shared by HTTP and TCP. Retains `completion` until the C callback, which
|
/// Shared by HTTP and TCP. Retains `completion` until the C callback, which
|
||||||
/// fires only for `.ask`.
|
/// fires only for `.ask`.
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ enum Mock {
|
|||||||
nonisolated(unsafe) static let tilemapAPI = UnsafeMutablePointer<playdate_tilemap>.allocate(capacity: 1)
|
nonisolated(unsafe) static let tilemapAPI = UnsafeMutablePointer<playdate_tilemap>.allocate(capacity: 1)
|
||||||
nonisolated(unsafe) static let fileAPI = UnsafeMutablePointer<playdate_file>.allocate(capacity: 1)
|
nonisolated(unsafe) static let fileAPI = UnsafeMutablePointer<playdate_file>.allocate(capacity: 1)
|
||||||
nonisolated(unsafe) static let jsonAPI = UnsafeMutablePointer<playdate_json>.allocate(capacity: 1)
|
nonisolated(unsafe) static let jsonAPI = UnsafeMutablePointer<playdate_json>.allocate(capacity: 1)
|
||||||
|
nonisolated(unsafe) static let networkAPI = UnsafeMutablePointer<playdate_network>.allocate(capacity: 1)
|
||||||
nonisolated(unsafe) static let apiStruct = UnsafeMutablePointer<PlaydateAPI>.allocate(capacity: 1)
|
nonisolated(unsafe) static let apiStruct = UnsafeMutablePointer<PlaydateAPI>.allocate(capacity: 1)
|
||||||
|
|
||||||
// MARK: - Recordings
|
// MARK: - Recordings
|
||||||
@@ -41,6 +42,10 @@ enum Mock {
|
|||||||
/// Caps the bytes a file read returns (0 = end of file, negative =
|
/// Caps the bytes a file read returns (0 = end of file, negative =
|
||||||
/// error); `nil` fills the whole request.
|
/// error); `nil` fills the whole request.
|
||||||
nonisolated(unsafe) static var fileReadLimit: Int32?
|
nonisolated(unsafe) static var fileReadLimit: Int32?
|
||||||
|
/// Bytes left to read before 0 (end of file); `nil` never ends.
|
||||||
|
nonisolated(unsafe) static var fileBytesRemaining: Int32?
|
||||||
|
/// The last `network->setEnabled` callback.
|
||||||
|
nonisolated(unsafe) static var networkEnabledCallback: (@convention(c) (PDNetErr) -> Void)?
|
||||||
/// The index buffer and count last handed to `setTiles`.
|
/// The index buffer and count last handed to `setTiles`.
|
||||||
nonisolated(unsafe) static var tilesPointer: UnsafeMutablePointer<UInt16>?
|
nonisolated(unsafe) static var tilesPointer: UnsafeMutablePointer<UInt16>?
|
||||||
nonisolated(unsafe) static var tilesCount: Int32 = 0
|
nonisolated(unsafe) static var tilesCount: Int32 = 0
|
||||||
@@ -94,6 +99,8 @@ enum Mock {
|
|||||||
patternBytes = []
|
patternBytes = []
|
||||||
stencilRows = []
|
stencilRows = []
|
||||||
fileReadLimit = nil
|
fileReadLimit = nil
|
||||||
|
fileBytesRemaining = nil
|
||||||
|
networkEnabledCallback = nil
|
||||||
tilesPointer = nil
|
tilesPointer = nil
|
||||||
tilesCount = 0
|
tilesCount = 0
|
||||||
spriteUserdata = [:]
|
spriteUserdata = [:]
|
||||||
@@ -115,6 +122,7 @@ enum Mock {
|
|||||||
installSound()
|
installSound()
|
||||||
installFile()
|
installFile()
|
||||||
installJSON()
|
installJSON()
|
||||||
|
installNetwork()
|
||||||
apiStruct.initialize(to: PlaydateAPI(
|
apiStruct.initialize(to: PlaydateAPI(
|
||||||
system: UnsafePointer(sysAPI),
|
system: UnsafePointer(sysAPI),
|
||||||
file: UnsafePointer(fileAPI),
|
file: UnsafePointer(fileAPI),
|
||||||
@@ -125,7 +133,7 @@ enum Mock {
|
|||||||
lua: nil,
|
lua: nil,
|
||||||
json: UnsafePointer(jsonAPI),
|
json: UnsafePointer(jsonAPI),
|
||||||
scoreboards: nil,
|
scoreboards: nil,
|
||||||
network: nil))
|
network: UnsafePointer(networkAPI)))
|
||||||
Playdate.initialize(with: UnsafeMutableRawPointer(apiStruct))
|
Playdate.initialize(with: UnsafeMutableRawPointer(apiStruct))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,6 +228,10 @@ enum Mock {
|
|||||||
gfxAPI.pointee.freeBitmap = { _ in
|
gfxAPI.pointee.freeBitmap = { _ in
|
||||||
Mock.record("freeBitmap")
|
Mock.record("freeBitmap")
|
||||||
}
|
}
|
||||||
|
gfxAPI.pointee.getBitmapMask = { _ in
|
||||||
|
Mock.record("getBitmapMask")
|
||||||
|
return Mock.fakePointer()
|
||||||
|
}
|
||||||
|
|
||||||
gfxAPI.pointee.newBitmapTable = { count, width, height in
|
gfxAPI.pointee.newBitmapTable = { count, width, height in
|
||||||
Mock.record("newBitmapTable(\(count))")
|
Mock.record("newBitmapTable(\(count))")
|
||||||
@@ -430,7 +442,11 @@ enum Mock {
|
|||||||
}
|
}
|
||||||
fileAPI.pointee.read = { _, buffer, length in
|
fileAPI.pointee.read = { _, buffer, length in
|
||||||
Mock.record("read(\(length))")
|
Mock.record("read(\(length))")
|
||||||
let count = min(Int32(length), Mock.fileReadLimit ?? Int32(length))
|
var count = min(Int32(length), Mock.fileReadLimit ?? Int32(length))
|
||||||
|
if let remaining = Mock.fileBytesRemaining {
|
||||||
|
count = min(count, remaining)
|
||||||
|
Mock.fileBytesRemaining = remaining - max(count, 0)
|
||||||
|
}
|
||||||
if count > 0 { memset(buffer, 0xAB, Int(count)) }
|
if count > 0 { memset(buffer, 0xAB, Int(count)) }
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
@@ -440,6 +456,16 @@ enum Mock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Network
|
||||||
|
|
||||||
|
private static func installNetwork() {
|
||||||
|
networkAPI.initialize(to: playdate_network())
|
||||||
|
networkAPI.pointee.setEnabled = { flag, callback in
|
||||||
|
Mock.record("setEnabled(\(flag),\(callback == nil ? "nil" : "callback"))")
|
||||||
|
Mock.networkEnabledCallback = callback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - JSON
|
// MARK: - JSON
|
||||||
|
|
||||||
/// Simulates the OS parser's callback sequence for the document
|
/// Simulates the OS parser's callback sequence for the document
|
||||||
@@ -447,14 +473,18 @@ enum Mock {
|
|||||||
private static func installJSON() {
|
private static func installJSON() {
|
||||||
jsonAPI.initialize(to: playdate_json())
|
jsonAPI.initialize(to: playdate_json())
|
||||||
|
|
||||||
// Pulls one chunk through the reader, as the OS would, and decodes
|
// Reads until the reader returns 0 or less, then decodes `null`.
|
||||||
// it as `null`.
|
|
||||||
jsonAPI.pointee.decode = { _, reader, outval in
|
jsonAPI.pointee.decode = { _, reader, outval in
|
||||||
var buffer = [UInt8](repeating: 0, count: 16)
|
var buffer = [UInt8](repeating: 0, count: 16)
|
||||||
let count = buffer.withUnsafeMutableBufferPointer { buffer in
|
var total: Int32 = 0, last: Int32 = 0
|
||||||
|
for _ in 0..<64 {
|
||||||
|
last = buffer.withUnsafeMutableBufferPointer { buffer in
|
||||||
reader.read?(reader.userdata, buffer.baseAddress, Int32(buffer.count)) ?? -1
|
reader.read?(reader.userdata, buffer.baseAddress, Int32(buffer.count)) ?? -1
|
||||||
}
|
}
|
||||||
Mock.record("decode(\(count))")
|
guard last > 0 else { break }
|
||||||
|
total += last
|
||||||
|
}
|
||||||
|
Mock.record("decode(total:\(total),end:\(last))")
|
||||||
outval?.pointee = json_value()
|
outval?.pointee = json_value()
|
||||||
outval?.pointee.type = CChar(kJSONNull.rawValue)
|
outval?.pointee.type = CChar(kJSONNull.rawValue)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -127,6 +127,23 @@ struct WrapperTests {
|
|||||||
#expect(UnsafePointer(Mock.tilesPointer) == storage)
|
#expect(UnsafePointer(Mock.tilesPointer) == storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func bitmapMaskIsFreedAndKeepsItsBitmapAlive() {
|
||||||
|
weak var weakBitmap: Graphics.Bitmap?
|
||||||
|
var mask: Graphics.Bitmap?
|
||||||
|
do {
|
||||||
|
let bitmap = Graphics.Bitmap(width: 8, height: 8)
|
||||||
|
weakBitmap = bitmap
|
||||||
|
mask = bitmap.mask
|
||||||
|
}
|
||||||
|
#expect(mask != nil)
|
||||||
|
#expect(weakBitmap != nil) // the mask keeps it alive
|
||||||
|
#expect(Mock.eventCount("freeBitmap") == 0)
|
||||||
|
|
||||||
|
mask = nil
|
||||||
|
#expect(weakBitmap == nil)
|
||||||
|
#expect(Mock.eventCount("freeBitmap") == 2) // the mask, then the bitmap
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Sprite
|
// MARK: Sprite
|
||||||
|
|
||||||
@Test func spriteUserdataRecoversWrapperInCallbacks() {
|
@Test func spriteUserdataRecoversWrapperInCallbacks() {
|
||||||
@@ -385,13 +402,14 @@ struct WrapperTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test func jsonDecodeFileReadsThroughTheHandleAndClosesIt() throws {
|
@Test func jsonDecodeFileReadsThroughTheHandleAndClosesIt() throws {
|
||||||
Mock.fileReadLimit = 5
|
Mock.fileBytesRemaining = 20
|
||||||
let value = try JSON.decodeFile(path: "save.json")
|
let value = try JSON.decodeFile(path: "save.json")
|
||||||
guard case .null = value else {
|
guard case .null = value else {
|
||||||
Issue.record("expected .null, got \(value)")
|
Issue.record("expected .null, got \(value)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
#expect(Mock.events.contains("decode(5)"))
|
// End of file reaches the decoder as 0, not -1.
|
||||||
|
#expect(Mock.events.contains("decode(total:20,end:0)"))
|
||||||
#expect(Mock.eventCount("close") == 1)
|
#expect(Mock.eventCount("close") == 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,6 +435,20 @@ struct WrapperTests {
|
|||||||
#expect(throws: PlaydateError.self) { try handle.read(length: 8) }
|
#expect(throws: PlaydateError.self) { try handle.read(length: 8) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Network
|
||||||
|
|
||||||
|
@Test func networkDisableRegistersNoCallbackSoEnableGetsItsOwnResult() {
|
||||||
|
var results: [String] = []
|
||||||
|
Network.disable()
|
||||||
|
#expect(Mock.events.last == "setEnabled(false,nil)")
|
||||||
|
|
||||||
|
Network.enable { error in results.append(error == nil ? "ok" : "failed") }
|
||||||
|
#expect(Mock.events.last == "setEnabled(true,callback)")
|
||||||
|
|
||||||
|
Mock.networkEnabledCallback?(NET_OK)
|
||||||
|
#expect(results == ["ok"])
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: JSON
|
// MARK: JSON
|
||||||
|
|
||||||
@Test func jsonDecodeBuildsTheValueTree() throws {
|
@Test func jsonDecodeBuildsTheValueTree() throws {
|
||||||
|
|||||||
Reference in New Issue
Block a user