Byte-sized Swift: Building Tiny Games for the Playdate

Hi all, I wrote a blog post about my experience bringing Swift to the Playdate using the embedded language mode.

Please feel free to discuss the blog post here.


Superb project!

I love how Swift is evolving to be true general purpose language in the past few years.


This is great and all. Thank you.
Now I will try to build and release a game on Playdate.
I hope they will support Swift directly.

Very cool.

Random numbers

I noticed you do UInt8(rand() & 0xff) rather than UInt8.random(in: .min ... .max). Is that just a stylistic choice?

Why UnsafeMutablePointer?

Is there a performance concern with using UnsafeMutableBufferPointer instead of UnsafeMutablePointer for things like accessing the frame buffer? Or is there a different reason for not having the bounds checking?

Bit vectors and matrices

It might be an interesting thought exercise to think about what it would take to be able to use BitArray from swift-collections instead of manual pointers, for things like the frame buffer. Aside from potential questions about binary size and sufficient inlining / dead-code-stripping for a relatively large [for Embedded] package like swift-collections, one obvious limitation is that BitArray currently doesn't let you provide an existing memory allocation for its backing.

(there's also the aspect of BitArray being length-mutable, which is logically incompatible with use over things like frame buffers, but one can imagine either a fixed-length variant or pragmatically just not touching the length in practice)

I wonder if an Array of BitArrays could ultimately inline down to zero overhead, to completely eliminate all the manual index calculation and bit-shifting?

Unsafe*Ptr ergonomics / C bridging challenges

The post notes some lingering challenges, like dealing with nullable-but-not-really pointers from the C API. But another annoyance in using pointers in Swift is that you end up with .pointee. everywhere. I wonder what your thoughts are on that, and on ideas like having an -> operator instead? Or using dynamic member lookup with keypaths for statically safe .?


Embedded swift does not currently support Swift.random stuff. It would be nice to, but it's tricky because quality of it would vary a lot by platform, and when a good source of randomness is available, initializing it might have a significant cost. So there is work to be done to both come up with policies for it, and to implement it per platform.


We'll have a proposal coming soon-ish to address this.


Nice, your submission made the front page of the orange site. Something like -Xcc --sysroot -Xcc $GCC_LIB/gcc/arm-none-eabi/9.2.1/ -Xcc -I -Xcc include-fixed/ -Xcc -I -Xcc ../../../../arm-none-eabi/include/ might work better: try it and see. Gah, I hate that Swift code generation is set through -Xcc options like -Xcc -mcpu=cortex-m7 -Xcc -mfloat-abi=hard -Xcc -mfpu=fpv5-sp-d16 -Xcc -mthumb.

1 Like

As noted by Ben and Steve, this is a current limitation of embedded swift, but won't be forever!

I think I could use a buffer pointer here, but as noted later in the post the cost of redundant null checks actually adds up on this platform and similarly the bounds checks might have an appreciable effect on Conways performance.

I didn't consider using other packages because I was building with makefiles. I also am not sure if that package uses features not available in embedded mode.

I actually tried to make this, but couldn't get it to work with function pointers! Have you had success with dynamic member look up and closures/function pointers?


I didn't consider setting the sysroot, good idea!

I also dislike this, I'm not sure how much reinventing the wheel we should do on the Swift side tho.

First of all, thanks a lot for this work!

I've tried building the examples and I'm getting this error:

<unknown>:0: error: unable to load standard library for target 'armv7em-none-none-eabi'

Any clues how I could fix it?

My guess is that TOOLCHAINS isn't getting set to the right toolchain identifier, so the swiftc you're getting isn't from the downloaded toolchain. You can check that you're getting the right swiftc by running this from the command line:

TOOLCHAINS=<the identifier you're using> xcrun -f swiftc

and it should point into /Library/Developer/Toolchains/<the snapshot toolchain you downloaded>/usr/bin/swiftc



I noticed that TOOLCHAINS has no effect when xcode-select -p point to CLI tools instead of an actual Xcode installation, which could be the case here.

I was able to get the examples building and running in Panic's Nova (following all the excellent instructions/documentation – thanks for that! :heart:), but even though both projects build and run, there are a fair amount of errors displayed in the code editor.

Any idea what's going on there, or how to fix them? I haven't actually used Swift in Nova before (I've only used it for playdate development before), so I'm not even sure how to debug properly.

Thanks for bringing this up, it prompted me to figure out a solution with help from @ahoppen and @bnbarham!

The issue is that the build and the editor actually derive their build settings and complier selection from different places!

Build IDE Support
Toolchain Makefile Icarus Extension
Build Settings Makefile SourceKit + SwiftPM

The toolchain selection is easier to fix, you need to configure Icarus to use the nightly toolchain:

The next issue to fix Package.swift to enable Swift 6 mode (if available) to match the Makefile:

diff --git a/Examples/SwiftBreak/Package.swift b/Examples/SwiftBreak/Package.swift
index 607f9cb..0319aaa 100644
--- a/Examples/SwiftBreak/Package.swift
+++ b/Examples/SwiftBreak/Package.swift
@@ -39,4 +39,5 @@ let package = Package(
         .product(name: "Playdate", package: "swift-playdate-examples")
       swiftSettings: swiftSettingsSimulator)
-  ])
+  ],
+  swiftLanguageVersions: [.version("6"), .v5])

We then need to perform an initial release build with SwiftPM, so the Playdate module is available:

$ cd swift-playdate-examples/Examples/SwiftBreak
$ swift build -c release

And lastly we need to configure sourcekit-lsp launched via Icarus to use the release folder. The issue here is that Icarus doesn't have any flags for this, so the solution is to symlink the release folder to debug to trick things into working.

$ cd swift-playdate-examples/Examples/SwiftBreak/.build/arm64-apple-macosx
$ ln -s release debug

I will update the instructions in the repo with this information!


I just want to add that if you are using Visual Studio Code, it is not necessary to add the debug -> release symlink. Instead you can tell sourcekit-lsp to search the release build folder by adding --configuration and release to the Sourcekit-lsp: Server Arguments in your settings (Cmd-Shift-P -> Open Settings (UI)) or by adding the following to your settings.json

"swift.sourcekit-lsp.serverArguments": [

Thanks for continuing to work on this!

My red error indicators went away after the first step (just switching Icarus to use the Swift Trunk Toolchain), but when trying to build for release, I ran into a bunch of errors. (I could post them if it would be helpful.) Here's just the last one, (which is the only one listed as fatal):

/Users/grid/Developer/projects/externals/playdate/swift-playdate-examples/Sources/Playdate/System.swift:1:1: error: Access level on imports require '-enable-experimental-feature AccessLevelOnImport'
public import CPlaydate
error: fatalError

What does that step enable? Debugging? Auto-completion? I have noticed that I can't CMD-click on System.buttonState.pushed, for example, so it's not picking up the symbols correctly yet, I don't think.

Could you double check you have set the TOOLCHAINS environment variable prior to running swift build -c release

Ahhh, I was following the instructions in your comment above, where it says to run

$ swift build -c release

...and not the ones in the (updated) documentation, where it says to run:

$ TOOLCHAINS="org.swift.59202312211a" swift build -c release

...which did work for me. (There was a warning, but it compiled fine.)

1 Like

It's very exciting to see Swift's good progress on multi-platform, cross-compiling, and supporting embedded environments with limited resources.

Definitely in the right direction!

Hey folks,

As of last night's (the 19th) nightly toolchain, the standard library now has support for noncopyable generics on its pointer types and on Optional (gated under the -enable-experimental-feature NoncopyableGenerics.

Here is a PR I put up on the Swift Playdate sample, applying this feature to the SwiftBreak game. This adds a very basic non-copyable array type, and then uses that to store the brick sprites.

This feature is very relevant to the embedded world, as it can dramatically reduce overhead otherwise needed from Swift's reference-counting approach to memory ownership.

Previously the Sprite had to be represented as an enum (indicating ownership) plus a class (to handle deallocation). The noncopyable version of that pattern is significantly simpler: it just holds the underlying opaque pointer (the playdate SDK's sprite handle) plus a bool to track ownership, then uses a struct with a deinit to free it if necessary.

This PR also adds a very basic fixed-size array type to hold the bricks. This was done for two reasons. The first is that Swift.Array does not yet support non-copyable types. But more importantly, the standard array type has more overhead than is necessary for all uses, including in this game. Tracking the buffer with reference counting, accounting for CoW uses, and automatically growing the array when needed, adds to binary size. For most uses cases, these costs are negligible so not a problem. But in an embedded context, they can make a significant difference.

The net effect of these changes is that binary size for the SwiftBreak game drops about 14%. Around half of this is from the simpler array type, and about half from the simpler sprite type.

If you're interested in the evolution of this feature, check out the ongoing review thread.