Add DocC documentation and a runnable example game

- DocC catalog (Sources/PlayDate/PlayDate.docc) with a curated landing
  page and a Getting Started article; swift-docc-plugin wired up for
  `swift package generate-documentation`, and a GitHub Pages deployment
  workflow renders docs on every push to main.
- Examples/HelloPlaydate: a minimal game (bouncing box, crank needle,
  button handling, system menu item) whose build.sh compiles the game as
  a dylib and produces a runnable simulator .pdx via the SDK's pdc.
  Verified: the pdx builds and exports the eventHandler entry point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 11:29:13 +02:00
co-authored by Claude Fable 5
parent 6485997edc
commit 8cf0715cdc
11 changed files with 417 additions and 72 deletions
+66
View File
@@ -0,0 +1,66 @@
name: Documentation
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: docs
cancel-in-progress: true
env:
PLAYDATE_SDK_VERSION: "3.1.1"
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest
- name: Cache Playdate SDK
id: sdk-cache
uses: actions/cache@v4
with:
path: ~/PlaydateSDK
key: playdate-sdk-${{ env.PLAYDATE_SDK_VERSION }}
- name: Download Playdate SDK headers
if: steps.sdk-cache.outputs.cache-hit != 'true'
run: |
curl -sL "https://download.panic.com/playdate_sdk/Linux/PlaydateSDK-${PLAYDATE_SDK_VERSION}.tar.gz" | tar xz -C "$HOME"
mv "$HOME/PlaydateSDK-${PLAYDATE_SDK_VERSION}" "$HOME/PlaydateSDK"
test -f "$HOME/PlaydateSDK/C_API/pd_api.h"
- name: Install pkg-config module
run: |
PLAYDATE_SDK_PATH="$HOME/PlaydateSDK" Scripts/install-pkgconfig.sh "$HOME/pkgconfig"
echo "PKG_CONFIG_PATH=$HOME/pkgconfig" >> "$GITHUB_ENV"
- name: Generate documentation
run: |
swift package --allow-writing-to-directory docs \
generate-documentation --target PlayDate \
--disable-indexing \
--transform-for-static-hosting \
--hosting-base-path play-date \
--output-path docs
- uses: actions/upload-pages-artifact@v3
with:
path: docs
- id: deployment
uses: actions/deploy-pages@v4
+4 -1
View File
@@ -1,6 +1,9 @@
.DS_Store
/.build
.build/
/Packages
# Example build products
Examples/*/Source/pdex.dylib
*.pdx
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
+20
View File
@@ -0,0 +1,20 @@
// swift-tools-version: 6.4
import PackageDescription
let package = Package(
name: "HelloPlaydate",
products: [
// The Playdate Simulator loads the game as pdex.dylib.
.library(name: "pdex", type: .dynamic, targets: ["HelloPlaydate"]),
],
dependencies: [
.package(path: "../.."),
],
targets: [
.target(
name: "HelloPlaydate",
dependencies: [.product(name: "PlayDate", package: "play-date")]
),
]
)
+23
View File
@@ -0,0 +1,23 @@
# Hello Playdate
A minimal game built on the `play-date` bindings: a bouncing box, a
crank-aimed needle, button handling, a system menu item, and an FPS counter.
## Build and run (Playdate Simulator)
With the Playdate SDK installed (and the one-time
`Scripts/install-pkgconfig.sh` setup from the repository root done):
```sh
./build.sh
open -a "$HOME/Developer/PlaydateSDK/bin/Playdate Simulator.app" HelloPlaydate.pdx
```
The script compiles the game as a dylib, places it in `Source/` next to
`pdxinfo`, and runs the SDK's `pdc` to produce `HelloPlaydate.pdx`.
## Device builds
Running on hardware requires the Embedded Swift + ARM toolchain pipeline
(see the repository README and Apple's swift-playdate-examples for the
Makefile setup). This example covers the simulator workflow only.
+6
View File
@@ -0,0 +1,6 @@
name=Hello Playdate
author=Röck+Cöde
description=Minimal example for the play-date Swift bindings
bundleID=com.rock-n-code.hello-playdate
version=0.1
buildNumber=1
@@ -0,0 +1,97 @@
//
// Game.swift
// A minimal Playdate game built on the play-date Swift bindings: a
// bouncing box, a crank-aimed needle, button logging, and a system menu
// item.
//
import CPlaydate
import PlayDate
@_cdecl("eventHandler")
public func eventHandler(
pointer: UnsafeMutableRawPointer,
event: PDSystemEvent,
argument: UInt32
) -> Int32 {
if case .initialize = SystemEvent(event: event, argument: argument) {
Playdate.initialize(with: pointer) // must happen before anything else
Game.shared.start()
}
return 0
}
final class Game {
nonisolated(unsafe) static let shared = Game()
private var x: Float = 200
private var y: Float = 120
private var dx: Float = 3
private var dy: Float = 2
private let boxSize = 24
func start() {
Display.setRefreshRate(50)
System.addMenuItem(title: "reset") { _ in
Game.shared.reset()
}
System.setUpdateCallback {
Game.shared.update()
return true // redraw the display this frame
}
}
private func reset() {
x = 200
y = 120
}
private func update() {
moveBox()
handleInput()
draw()
}
private func moveBox() {
let width = Float(Display.width)
let height = Float(Display.height)
x += dx
y += dy
if x < 0 || x > width - Float(boxSize) { dx = -dx }
if y < 24 || y > height - Float(boxSize) { dy = -dy }
}
private func handleInput() {
let (_, pushed, _) = System.buttonState
if pushed.contains(.a) {
System.log("A pressed at \(System.currentTimeMilliseconds)ms")
}
if pushed.contains(.b) {
(dx, dy) = (-dx, -dy)
}
}
private func draw() {
Graphics.clear(color: .white)
Graphics.drawText("Hëllo from Swift — Ⓑ reverses", x: 8, y: 4)
Graphics.fillRect(x: Int(x), y: Int(y), width: boxSize, height: boxSize, color: .black)
if !System.isCrankDocked {
// A needle from the screen center pointing where the crank points.
let radians = System.crankAngle * .pi / 180
let centerX = Display.width / 2
let centerY = Display.height / 2
Graphics.drawLine(
x1: centerX, y1: centerY,
x2: centerX + Int(40 * sinf(radians)),
y2: centerY - Int(40 * cosf(radians)),
width: 2, color: .xor)
}
System.drawFPS(x: 380, y: 4)
}
}
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
#
# Builds HelloPlaydate.pdx for the Playdate Simulator: compiles the game as
# a dylib, places it in the pdc source folder, and runs the SDK's pdc.
#
# Device builds need the Embedded Swift + ARM toolchain setup described in
# the repository README; this script covers the simulator only.
set -eu
cd "$(dirname "$0")"
sdk_path="${PLAYDATE_SDK_PATH:-$HOME/Developer/PlaydateSDK}"
swift build -c release
bin_path="$(swift build -c release --show-bin-path)"
cp "$bin_path/libpdex.dylib" Source/pdex.dylib
"$sdk_path/bin/pdc" Source HelloPlaydate.pdx
echo "Built HelloPlaydate.pdx — run it with:"
echo " open -a \"$sdk_path/bin/Playdate Simulator.app\" HelloPlaydate.pdx"
+4
View File
@@ -10,6 +10,10 @@ let package = Package(
targets: ["PlayDate"]
),
],
dependencies: [
// Documentation generation only; not linked into the library.
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.4.0"),
],
targets: [
// The Playdate C API headers, resolved through the "playdate"
// pkg-config module. Run Scripts/install-pkgconfig.sh once to point
+49 -71
View File
@@ -4,12 +4,7 @@
Swift bindings to the [Playdate](https://play.date) C API.
The Playdate C API is delivered as a `PlaydateAPI*` struct of function
pointers that the firmware hands to your game at launch. This package wraps
that surface in idiomatic Swift: namespaced APIs, wrapper types with
ownership semantics, closures instead of function-pointer/userdata pairs,
`OptionSet`s and `enum`s instead of raw constants, and typed `throws` for
fallible calls.
The Playdate C API is delivered as a `PlaydateAPI*` struct of function pointers that the firmware hands to your game at launch. This package wraps that surface in idiomatic Swift: namespaced APIs, wrapper types with ownership semantics, closures instead of function-pointer/userdata pairs, `OptionSet`s and `enum`s instead of raw constants, and typed `throws` for fallible calls.
All ten C subsystems are covered:
@@ -28,28 +23,20 @@ All ten C subsystems are covered:
## Requirements
- The [Playdate SDK](https://play.date/dev/) (3.1.1 or later). The SDK is
not vendored into this repository.
- The [Playdate SDK](https://play.date/dev/) (3.1.1 or later). The SDK is not vendored into this repository.
- Swift 6.4 tools or later.
### One-time SDK setup
The `CPlaydate` target resolves `pd_api.h` through a `playdate` pkg-config
module, so the package works as a normal SwiftPM dependency without unsafe
build flags. Generate the pkg-config file once per machine:
The `CPlaydate` target resolves `pd_api.h` through a `playdate` pkg-config module, so the package works as a normal SwiftPM dependency without unsafe build flags. Generate the pkg-config file once per machine:
```sh
Scripts/install-pkgconfig.sh
```
The script locates the SDK via `PLAYDATE_SDK_PATH` (default
`~/Developer/PlaydateSDK`) and writes `playdate.pc` into a directory SwiftPM
searches by default (`/usr/local/lib/pkgconfig`). Pass a custom destination
as an argument if you prefer another location on your `PKG_CONFIG_PATH`.
The script locates the SDK via `PLAYDATE_SDK_PATH` (default `~/Developer/PlaydateSDK`) and writes `playdate.pc` into a directory SwiftPM searches by default (`/usr/local/lib/pkgconfig`). Pass a custom destination as an argument if you prefer another location on your `PKG_CONFIG_PATH`.
If Xcode had the package open before you ran the script, make it re-read the
manifest (File ▸ Packages ▸ Reset Package Caches) — Xcode caches package
resolution and won't notice the new `.pc` file on its own.
If Xcode had the package open before you ran the script, make it re-read the manifest (File ▸ Packages ▸ Reset Package Caches) — Xcode caches package resolution and won't notice the new `.pc` file on its own.
## Adding the dependency
@@ -70,18 +57,18 @@ targets: [
## Getting started
A Playdate game has a single C entry point, `eventHandler`. Export it with
`@_cdecl`, initialize the binding on the first event, and install an update
callback:
A Playdate game has a single C entry point, `eventHandler`. Export it with `@_cdecl`, initialize the binding on the first event, and install an update callback:
```swift
import CPlaydate
import PlayDate
@_cdecl("eventHandler")
func eventHandler(pointer: UnsafeMutableRawPointer,
event: PDSystemEvent,
argument: UInt32) -> Int32 {
func eventHandler(
pointer: UnsafeMutableRawPointer,
event: PDSystemEvent,
argument: UInt32
) -> Int32 {
switch SystemEvent(event: event, argument: argument) {
case .initialize:
Playdate.initialize(with: pointer) // must happen before anything else
@@ -91,6 +78,7 @@ func eventHandler(pointer: UnsafeMutableRawPointer,
default:
break
}
return 0
}
@@ -120,9 +108,7 @@ final class Game {
}
```
`Playdate.initialize(with:)` stores the API pointer once; every wrapper in
the module uses it from then on. Calling any wrapper before `initialize` is
a programmer error and will crash.
`Playdate.initialize(with:)` stores the API pointer once; every wrapper in the module uses it from then on. Calling any wrapper before `initialize` is a programmer error and will crash.
## Tour of the API
@@ -202,10 +188,7 @@ for collision in collisions where collision.other.tag == Tags.brick {
}
```
Sprite callbacks (`setUpdateFunction`, `setDrawFunction`,
`setCollisionResponseFunction`) receive the Swift wrapper back. The C-level
sprite userdata slot is reserved by the binding for that recovery — use the
`userdata` property on `Sprite` for your own per-sprite storage instead.
Sprite callbacks (`setUpdateFunction`, `setDrawFunction`, `setCollisionResponseFunction`) receive the Swift wrapper back. The C-level sprite userdata slot is reserved by the binding for that recovery — use the `userdata` property on `Sprite` for your own per-sprite storage instead.
### Sound
@@ -295,61 +278,56 @@ try Lua.addFunction(double, name: "mylib.double")
## Conventions
- **Namespaces.** The subsystem namespaces (`System`, `Graphics`, `Sound`,
…) live at the top level of the module; only the raw C API bootstrap
stays under `Playdate` (`Playdate.initialize(with:)`, `Playdate.api`).
On a name collision with another module, qualify with the module name:
`PlayDate.System`.
- **Errors.** Fallible operations use typed throws — `throws(PlaydateError)`
generally, `throws(Network.NetError)` for network I/O — so `catch`
gives you a concrete type, and no `any Error` existentials are needed.
- **Ownership.** A wrapper that *creates* a C object frees it on `deinit`;
keep the wrapper referenced for as long as you use it. Wrappers vending
OS-owned objects (a `Bitmap` from a `BitmapTable`, a track from a
`Sequence`, …) don't free them — keep the owner alive instead, as
documented on each API. Resources a C object keeps referencing (a sprite's
image, a synth's sample, modulators, menu-item option titles) are retained
by the wrapper automatically.
- **Callbacks.** Where the C API provides a userdata slot, closures are
supported everywhere and delivered back with the right wrapper. A few C
callbacks have no userdata (serial messages, headphone changes, scoreboard
completions, `getServerTime`); those track one Swift closure at a time, as
noted in their documentation.
- **Threading.** The Playdate runtime is single-threaded (audio callbacks
excepted); statics in the binding are `nonisolated(unsafe)` on that basis.
Don't call the API from other threads.
- **Namespaces.** The subsystem namespaces (`System`, `Graphics`, `Sound`, …) live at the top level of the module; only the raw C API bootstrap stays under `Playdate` (`Playdate.initialize(with:)`, `Playdate.api`). On a name collision with another module, qualify with the module name: `PlayDate.System`.
- **Errors.** Fallible operations use typed throws — `throws(PlaydateError)` generally, `throws(Network.NetError)` for network I/O — so `catch` gives you a concrete type, and no `any Error` existentials are needed.
- **Ownership.** A wrapper that *creates* a C object frees it on `deinit`; keep the wrapper referenced for as long as you use it. Wrappers vending OS-owned objects (a `Bitmap` from a `BitmapTable`, a track from a `Sequence`, …) don't free them — keep the owner alive instead, as documented on each API. Resources a C object keeps referencing (a sprite's image, a synth's sample, modulators, menu-item option titles) are retained by the wrapper automatically.
- **Callbacks.** Where the C API provides a userdata slot, closures are supported everywhere and delivered back with the right wrapper. A few C callbacks have no userdata (serial messages, headphone changes, scoreboard
completions, `getServerTime`); those track one Swift closure at a time, as noted in their documentation.
- **Threading.** The Playdate runtime is single-threaded (audio callbacks excepted); statics in the binding are `nonisolated(unsafe)` on that basis. Don't call the API from other threads.
## Building for the simulator and device
This package builds as a plain Swift library, which is how you develop and
unit-test game logic on the host (`swift test` works out of the box).
This package builds as a plain Swift library, which is how you develop and unit-test game logic on the host (`swift test` works out of the box).
Shipping a `.pdx` needs the Playdate toolchain on top:
- **Simulator** builds compile your game as a host dylib placed in the pdx.
- **Device** builds require Embedded Swift for ARM Cortex-M7
(`-enable-experimental-feature Embedded`, triple `armv7em-none-none-eabi`).
- **Device** builds require Embedded Swift for ARM Cortex-M7 (`-enable-experimental-feature Embedded`, triple `armv7em-none-none-eabi`).
The wrappers are written within the Embedded Swift subset for exactly this
reason: no Foundation, no reflection, no untyped throws. That claim is
enforced, not aspirational: `Scripts/build-embedded.sh` cross-compiles the
whole module for `armv7em-none-none-eabi` with Embedded Swift enabled, and
CI runs it on every push. Running it locally needs a swift.org development
snapshot toolchain (Xcode's toolchain doesn't ship the bare-metal embedded
stdlib) and the Arm GNU toolchain headers:
The wrappers are written within the Embedded Swift subset for exactly this reason: no Foundation, no reflection, no untyped throws. That claim is enforced, not aspirational: `Scripts/build-embedded.sh` cross-compiles the whole module for `armv7em-none-none-eabi` with Embedded Swift enabled, and CI runs it on every push. Running it locally needs a swift.org development snapshot toolchain (Xcode's toolchain doesn't ship the bare-metal embedded stdlib) and the Arm GNU toolchain headers:
```sh
SWIFT_BIN=~/Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-<date>.xctoolchain/usr/bin/swift \
Scripts/build-embedded.sh
```
See Apple's
[swift-playdate-examples](https://github.com/apple/swift-playdate-examples)
for a working Makefile/toolchain setup that this library slots into.
See Apple's [swift-playdate-examples](https://github.com/apple/swift-playdate-examples) for a working Makefile/toolchain setup that this library slots into.
## Example
[`Examples/HelloPlaydate`](Examples/HelloPlaydate) is a complete minimal game — bouncing box, crank needle, button handling, a system menu item — that builds into a runnable simulator `.pdx`:
```sh
cd Examples/HelloPlaydate
./build.sh
open -a "$HOME/Developer/PlaydateSDK/bin/Playdate Simulator.app" HelloPlaydate.pdx
```
## Documentation
The API reference is a DocC catalog. Generate it locally with:
```sh
swift package generate-documentation --target PlayDate
```
or browse it with Xcode's documentation viewer (Product ▸ Build Documentation). Pushes to `main` publish the rendered docs to GitHub Pages via `.github/workflows/docs.yml` (enable Pages ▸ Source: GitHub Actions in the repository settings once).
## Layout
```
Examples/
HelloPlaydate/ Minimal game buildable into a simulator .pdx
Scripts/
install-pkgconfig.sh One-time setup: points the "playdate" pkg-config
module at your SDK installation
@@ -362,12 +340,12 @@ Sources/
importing pd_api.h from the SDK, plus inline shims for
the variadic log/error functions
PlayDate/ The Swift bindings, one file per subsystem
(Sound and Graphics are split across several files)
(Sound and Graphics are split across several files),
plus the PlayDate.docc documentation catalog
Tests/
PlayDate/ Host-runnable tests for the pure value types
```
## License
MIT — see [LICENSE](LICENSE). The Playdate SDK itself is licensed separately
by Panic, Inc. and is not distributed with this package.
MIT — see [LICENSE](LICENSE). The Playdate SDK itself is licensed separately by Panic, Inc. and is not distributed with this package.
@@ -0,0 +1,66 @@
# Getting Started
Bootstrap the bindings from your game's entry point and drive a frame loop.
## Overview
A Playdate game has a single C entry point, `eventHandler`, which the
firmware calls with a `PlaydateAPI*` and an event code. Export it with
`@_cdecl`, call ``Playdate/initialize(with:)`` on the first event, and
install an update callback:
```swift
import CPlaydate
import PlayDate
@_cdecl("eventHandler")
func eventHandler(
pointer: UnsafeMutableRawPointer,
event: PDSystemEvent,
argument: UInt32
) -> Int32 {
if case .initialize = SystemEvent(event: event, argument: argument) {
Playdate.initialize(with: pointer) // must happen before anything else
Game.shared.start()
}
return 0
}
final class Game {
nonisolated(unsafe) static let shared = Game()
func start() {
Display.setRefreshRate(50)
System.setUpdateCallback {
self.update()
return true // true = redraw the display this frame
}
}
func update() {
let (_, pushed, _) = System.buttonState
if pushed.contains(.a) {
System.log("A pressed")
}
Graphics.clear(color: .white)
Graphics.drawText("Hello, Playdate", x: 8, y: 8)
System.drawFPS()
}
}
```
## Conventions to know
- **Initialization.** Calling any wrapper before
``Playdate/initialize(with:)`` is a programmer error and will crash.
- **Errors.** Fallible operations use typed throws — ``PlaydateError``
generally, ``Network/NetError`` for network I/O.
- **Ownership.** A wrapper that creates a C object frees it on `deinit`;
keep the wrapper referenced for as long as you use it. Wrappers vending
OS-owned objects don't free them — keep the owner alive instead, as
documented on each API.
- **Threading.** The Playdate runtime is single-threaded; don't call the
API from other threads.
@@ -0,0 +1,61 @@
# ``PlayDate``
Swift bindings to the Playdate C API.
## Overview
The Playdate C API is delivered as a `PlaydateAPI*` struct of function
pointers that the firmware hands to your game at launch. This module wraps
that surface in idiomatic Swift: top-level namespaces per subsystem, wrapper
types with ownership semantics, closures instead of function-pointer/userdata
pairs, `OptionSet`s and `enum`s instead of raw constants, and typed `throws`
for fallible calls.
Call ``Playdate/initialize(with:)`` from your game's `eventHandler` before
using anything else — see <doc:GettingStarted>.
The bindings are written within the Embedded Swift subset, so the same code
compiles for the Playdate Simulator and for the device
(`armv7em-none-none-eabi`).
## Topics
### Essentials
- <doc:GettingStarted>
- ``Playdate``
- ``SystemEvent``
- ``PlaydateError``
### System and display
- ``System``
- ``Display``
### Drawing
- ``Graphics``
- ``Rect``
### Sprites
- ``Sprite``
### Audio
- ``Sound``
### Storage
- ``File``
- ``JSON``
### Connectivity
- ``Network``
- ``Scoreboards``
- ``AccessReply``
### Lua interop
- ``Lua``