Rust Development Thread

This thread is for general discussion on using Rust to develop games for the Playdate.

This post will cover getting started with crank and crankstart. These projects constitute a working solution that has already been developed by @rob. Only simulator builds will be covered because I do not have hardware.

This post assumes that Rust has already been installed, and the cargo command is available. First, install crank according to the README.

cd my_playdate_projects
git clone https://github.com/rtsuk/crank.git
cd crank
cargo install --path . --force

Next, clone the crankstart repository to run the examples.

cd my_playdate_projects
git clone https://github.com/rtsuk/crankstart.git
cd crankstart

The hello_world and life examples can simply be run.

crank run --release --example hello_world
crank run --release --example life

Assets from the C_API need to be copied in before running the sprite_game example.

cp -a "$PLAYDATE_SDK_PATH/C_API/Examples/Sprite Game/Source/images/." sprite_game_images
crank run --release --example sprite_game

Finally, @rob also wrote a Klondike solitaire game in Rust. Use the following commands to build and run it.

cd my_playdate_projects
git clone https://github.com/rtsuk/crankstart-klondike.git
cd crankstart-klondike
crank run --release

EDIT: Tested with SDK version 1.9.1 on MacOS.

6 Likes

This post will cover building for hardware using crank, even though I have no way of testing.

The post assumes that rustup has been installed. First, cargo-xbuild is required to build for hardware.

cargo install cargo-xbuild

xbuild requires source to be installed, so it needs to be installed with rustup.

rustup component add rust-src

Finally, the package subcommand can be used to build binaries for both the device and simulator. The run and build subcommands take a --device flag that can be used to target hardware exclusively. EDIT: Building for hardware emits a warning, but the following commands seem to work. Again, I am unable to actually test the builds for a lack of hardware.

cd my_playdate_projects/crankstart
crank run --device --release --example sprite_game
crank build --device --release --example hello_world
crank package --example life

cd my_playdate_projects/crankstart-klondike
crank run --device --release
crank build --device --release
crank package

:+1:

I'm surprised Klondike builds. I last touched it around SDK 1.2 and much has changed since then.

1 Like

Not only does Klondike compile, it can be beaten! I am not sure if it is supposed to do anything after winning a game.

Well, that's a nice surprise.

I did not yet implement any kind of "you win" feedback.

Having covered what is possible already, I really want to talk about architecture. Rust makes it very easy to package your application into a portable library that is called from a simple platform specific main() function or other entry point. This library can then be called on every different platform that it needs to be ported to.

The question I have is, what is a good way of structuring a game so it runs on the Playdate and is portable across multiple platforms? crank and crankstart appear to be tightly coupled to the Playdate in particular. Part of this is unavoidable. To create a useful application, concrete platform specific IO is necessarily needs to be bound to.

There is probably going to be a game engine that drives game specific code, including scripting. All of this this ought to be portable. In theory, all of this could drive player clients and headless server side NPC controller clients for an online multiplayer game, so much of the client code also ought to be portable. The portable code needs to be called from a platform specific entry point and bound to platform specific IO, which may be some sort of a logging function for a server side client.

Ideally, I would just write a game with SDL2. At that point, it should just run on everything SDL2 has been ported to, but it has not been ported to the Playdate yet. Also, a minor note is that the entry point on most platforms is some version of a main() function.

Not specific to rust but @donald.hays created an example of one code running on Playdate SDK and Löve2D

@matt Strictly speaking, the language should not matter when it comes to organizing modules into a good general portable project structure. Having said that, I am trying to figure out the specific details of such a structure that plays well with @rob's crank + crankstart, and the standard main() function entry point. Is there a link to the multiplatform project @donald.hays wrote?

I'll let Donald chime in but from memory there was a main path of Lua and duplicate draw functions for each SDK. Obviously this was easier as both use Lua.

If I understand correctly, it sounds like Donald wrote a custom wrapper that serves as an adaptor for platform specific IO calls. That is not an unreasonable general approach.

Contemporary platforms seem to be split into x86_64 (Playstation, Xbox, most PC-systems) and AArch64 (Switch, mobile devices, M1 Macs). If pure no_std Rust is used, it might be possible for the same two 32-bit .o object files to be used on all platforms (compatibility mode).

Another note is that I want the option to mix non-inline ASM into projects if need be. As long as all object files expose functions that use the C ABI, it should be possible to mix code written in all sorts of languages into the same binary. I only want to write code in Rust and ASM, but some platforms may assume main() is written in one language or another (C++, Swift, etc.). Furthermore, all sorts of libraries are theoretically useful, but my understanding is that in practice many good libraries have already been written in C and C++.

Finally, I seem to recall that @carols10cents is bot interested in Rust development for Playdate, and has opinions. Any discussion that can be generated on the topic is good.

@carols10cents spoilers ahead, don't read if you want to suffer like I have.

The architecture of crankstart was driven by the fact that Playdate C_API games do not have access to a C standard library. As such, they have to be no_std, Rust's term for binaries that run without a C standard library.

C_API games remind me a lot of 1990's Photoshop plugins. They have a single entry point, which the hosting environment calls, passing it a structure filled with pointers to everything else the game needs to operate. Cranstart defines this entry point here..

The additional interesting wrinkle is that the Playdate simulator expects to find games packaged as a dynamic shared libraries. Rust is very uninterested in building a no_std desktop dynamic shared library, so much hackery is required to produce one.

Luckily, one of the things the C_API provides is a function to allocate memory. This allows crankstart to implement a custom allocator and crankstart games to use the alloc crate and allowing crankstart to provide a much more ergonomic API.

Another challenge in providing decent Rust wrappers around the Playdate C_API is how callback-driven it is. It was hard for me to figure out how satisfy Rust's ownership rules in a callback which could be called in response to a call into the C_API. I'm kinda playing fast and loose with it to make the sprite collision stuff work.

A third challenge is that the main stack on Playdate is very small. In a non-public game I was working on, that used serde to read a JSON tile map file, I could easily exhaust it. I think this is due to Panic mapping the stack to some fast RAM on the chip, which makes sense, but it does mean one needs to be very careful about stack use. Were I to return to that game, I'd preprocess the JSON files on the host to something that the Crankstart game could read without serde.

On the question of portability, one should be able to use any Rust crate that can function in no_std with alloc. I was very happy to discover that Euclid worked in such an environment. The klondike crate is a half-assed example of that, since it links to the solver binary as a std crate, and the game itself in no_std.

Some small portions of SDL2 might work that way, but since both Playdate and SDL2 want to be in control of the game loop, I'm not sure how much use it would be.

5 Likes

Many crates function with no_std+alloc, but a lot of useful crates do not. hecs is a no_std ECS library, but not a game engine per se. Additionally, Rhai looks promising for no_std scripting.

In theory, some sort of C standard library could be provided that runs on the Playdate. A version of the Rust standard library could then be built on top of it. Someone would need to do the porting work, but an executive decision was probably made to exclude the C standard library in the first place. Furthermore, the issue of the small main stack indicates that the Playdate is probably the wrong platform of "heavy" (or maybe even standard) libraries.

For what it is worth, it will be great if the Playdate encourages no_std Rust game development because that means more portable code that runs anywhere. My understanding is that the Playdate has a relatively beefy processor for the screen and RAM, leading to an interesting set of constraints- preprocess data so the stack does not overflow, but also use lean data structures do as much as possible at runtime so RAM does not run out. Code written for the Playdate may need optimizations that are useful but unnecessary on other platforms.

I suspect that the Playdate entry point (event handler) could live alongside a main() function without hurting anything. It could probably also be conditionally removed. This leads to an architecture where the same update function is used on all platforms, with an adaptor for the right signature, if necessary.

I tried to do some Rust work on an NDA protected closed platform where stub functions were needed to generate symbols in the resulting binary. I wonder if it does not make sense to split the no_std desktop dynamic shared library "hackery" out into a separate file. For what it is worth, systems like the Playdate probably break a lot of the standard assumptions most programmers make about the execution environment- it is kind of embedded and kind of unix.

If the current alloc solution works, it is probably good enough. Just to throw something on the table, could it make sense for Rust to get the whole heap from the Playdate API so a custom allocation can be used?

In theory, it is possible to write 100% safe Rust code. In practice, unsafe Rust exists for a reason- hardware and external software may not conform to a conceptual model that cleanly maps to Rust's ownership rules. Is the callback problem more complicated than memory just being owned by caller. If so, is it really compatible with Rust's notions of "not your memory"?

My understanding is that SDL2 expects the programmer to write their own game loop. Obviously much of SDL2 does not make sense on Playdate. Multiple player controllers is one example.

@rob While skimming the crank source code, my first reaction was "I wish this was a makefile." If I want to write ASM and compile it into an object file, is there currently a way to link it to the binary using crank? My old lang_interop example uses rustc instead of cargo, so it looks like I never figured out a good solution. The Embedonomicon has a section .s files. Maybe the global_asm.s solution laid out in this post will just work? I can try to make a simple test project.

There has never been a time when I've been writing in Rust and wished I was writing in Make. :smile:

I think the new inline assembly stuff just got stabilized. If that's not enough for your ASM desires, a build.rs file should work with crank, it's just shelling out to cargo for the build.

2 Likes

:joy: On a quick skim that I will immediately forget, it looks like you're lots farther than me :slight_smile: I've only spent a few evenings, and I don't have my device yet. I'm mostly just drawing rectangles in the simulator, having fun designing a Rust API that I like, learning more about game dev and FFI, and remembering that graphics involves so much math :sweat_smile:

2 Likes

I recently summarized Getting Started with Rust on Playdate in a blog post. It may be easier to follow for anyone who is looking for a tutorial.

3 Likes

Has anyone started looking into the 1.13.0 SDK with crankstart? It's a bigger set of changes than most previous updates.

No. Have you tried building the Klondike solitaire project? It worked last time I tried building it.

I'm getting there - I'm currently trying to get a Playdate development setup working on Linux, since the simulator makes some unfortunate assumptions about the distribution it's running on.

But crankstart was just updated to SDK 1.13.0, so Klondike will probably work. :slight_smile:

For what it is worth, I have had no trouble running on macOS in the relatively recent past.

Klondike builds and appears to run fine on MacOS with 1.13.0 SDK. I did not play through a complete game. My own project also appears to build and run fine.

1 Like