Playdate Nim bindings - C performance, Python like syntax

Hi everyone!
It's been a while since I started lurking in this forum to see the nice things all you guys are making!

I also started working on game concepts to specifically target the Playdate but I asked myself whether I could give something to the community at the same time!

Nim is a statically typed language that compiles down to C but aims to a simpler syntax and adds quite a few nice to have features on top such as memory management, powerful macros and much more!
Seemed like the perfect fit for the Playdate!

So, a few days ago I started experimenting creating Nim bindings for the SDK and after a bit of trial and error I managed to make it work as expected!

Here's the repository containing the Nim package with instructions and a basic example!

PDX: PlaydateNim.pdx.zip (8.4 MB)

And here's a code comparison for the ultra basic hello world SDK example:

C
#include <stdio.h>
#include <stdlib.h>

#include "pd_api.h"

const char* fontpath = "/System/Fonts/Asheville-Sans-14-Bold.pft";
LCDFont* font = NULL;

#define TEXT_WIDTH 86
#define TEXT_HEIGHT 16

int x = (400-TEXT_WIDTH)/2;
int y = (240-TEXT_HEIGHT)/2;
int dx = 1;
int dy = 2;

static int update(void* userdata)
{
	PlaydateAPI* pd = userdata;
	
	pd->graphics->clear(kColorWhite);
	pd->graphics->setFont(font);
	pd->graphics->drawText("Hello World!", strlen("Hello World!"), kASCIIEncoding, x, y);

	x += dx;
	y += dy;
	
	if ( x < 0 || x > LCD_COLUMNS - TEXT_WIDTH )
		dx = -dx;
	
	if ( y < 0 || y > LCD_ROWS - TEXT_HEIGHT )
		dy = -dy;
        
	pd->system->drawFPS(0,0);

	return 1;
}

#ifdef _WINDLL
__declspec(dllexport)
#endif
int eventHandler(PlaydateAPI* pd, PDSystemEvent event, uint32_t arg)
{
	(void)arg; // arg is currently only used for event = kEventKeyPressed

	if ( event == kEventInit )
	{
		const char* err;
		font = pd->graphics->loadFont(fontpath, &err);
		
		if ( font == NULL )
			pd->system->error("%s:%i Couldn't load font %s: %s", __FILE__, __LINE__, fontpath, err);

		pd->system->setUpdateCallback(update, pd);
	}
	
	return 0;
}
Nim
import playdate/api

const FONT_PATH = "/System/Fonts/Asheville-Sans-14-Bold.pft"
const TEXT_WIDTH = 86
const TEXT_HEIGHT = 16

var font: LCDFont
var x = int((400 - TEXT_WIDTH) / 2)
var y = int((240 - TEXT_HEIGHT) / 2)
var dx = 1
var dy = 2


proc update(): int =
    playdate.graphics.clear(kColorWhite.LCDColor)
    playdate.graphics.setFont(font)
    playdate.graphics.drawText("Hello World!", x, y)

    x += dx
    y += dy
    
    if x < 0 or x > LCD_COLUMNS - TEXT_WIDTH:
        dx = -dx
    
    if y < 0 or y > LCD_ROWS - TEXT_HEIGHT:
        dy = -dy
        
    playdate.system.drawFPS(0, 0)

    return 1

proc handler*(event: PDSystemEvent, keycode: uint) {.raises: [].} =
    if event == kEventInit:
        try:
            font = playdate.graphics.loadFont(FONT_PATH)
        except:
            playdate.system.error(fmt"{$compilerInfo()} {getCurrentExceptionMsg()}")

        playdate.system.setUpdateCallback(update)

initSDK()

A few things can and will be further improved!
I'm taking advantage of a few Nim features like error handling with exceptions and I'm also simplifying function calls where feasible.

Nim has great interoperability with C, so any library can be easily wrapped!

This is still a WIP, but most of the APIs are already available and devs are successfully compiling the example above and other projects for the simulator and for the device.

Let me know if this project can interest the community!

10 Likes

Would like to see this come out! Seems like a great use case for Nim. If Goodboy Galaxy can get up and running on a GBA using Nim then the language should be more than fast enough for the Playdate.

I'm currently using crankstart but think Rust for gamedev is a bit high friction for me, especially when burning through prototypes. Seems like Nim would be a great fit here. Give us an update if you release this! Even an incomplete version would be helpful as a starting point.

1 Like

Sure! I'm working on this, I even had the idea of making a small game engine using the bindings.
So yeah! I'll update this thread when something is ready.

Should be fairly soon, as I need people with the physical device to test them!

Also have to iron out a SIGSEGV I'm having manipulating Nim strings in the simulator, only when Malloc Pool is enabled.

So, I'm almost done with bindings for:

  • pd_api_file.h
  • pd_api_display.h
  • pd_api_system.h

I'm aiming to provide a more ergonomic API on top of the existing one, so users will be able to do this:

let menuItem = api.system.addOptionsMenuItem("Options", @["One", "Two", "Three"], proc() =
    api.system.logToConsole("Callback received!")
)
menuItem.callback = proc() = api.system.logToConsole("Changed callback!")
menuItem.getValue() # index of the selected option
menuItem.setValue(1) # set the currently selected option
...
menuItem.remove() # remove the menu item

Now, I have a weird "bug".
Polymorphism doesn't work, for some reason, with the build configuration I'm using.

So the last statement is false:

let menuItem: PDMenuItem = api.system.addOptionsMenuItem(...)
menuItem of PDMenuItemOptions # returns false. WHAT

Will have to investigate.

looks neat! did you have to do anything crazy to get it to compile? what's your setup like?

I ask because right now I'm trying to do the same thing you are but with "Jai" instead of Nim. I tried looking at the stuff the guy who did the stuff with Rust made but a.) I don't know Rust and b.) it seems really complex, spread across two repos, etc.

Nothing too complex!
Nim is basically already targeted to embedded systems and compiles to C, so it was just a matter of configuring how Nim would generate C source code and how it should interact with the SDK.

For more details: I'm going to open source the project soon!

1 Like

ah I see, I didn't realize Nim compiled to C. that probably won't be too useful to me but it will still be nice to see nonetheless. I don't have my device yet and I'm just trying to figure out how to get the binary to compile, how to structure the entry point, etc. I am interested in seeing how you wrap the API though because I'm looking to do something very similar!

1 Like

Fixed the polymorphism issue.
Now continuing with other bindings and tests on automatic memory management!

Also have to decide how much to change the API to provide a more ergonomic one.
Maybe it is better to leave a good chunks of these changes to frameworks and engines.

Done with bindings of:

  • pd_api_file.h
  • pd_api_display.h
  • pd_api_system.h
  • pd_api_gfx.h

Now working on pd_api_sprite.h, then after a little bit of audio stuff I think it will be time to release a first incomplete version of the bindings and let you guys try them out!
Surely a few fixes are going to be needed.

3 Likes

So, I'm working on automatic memory management, this will be one of the main features of the bindings!

You'll be able to get C-like performance and automatic reference counted memory management (no GC) for "free"!

My aim is to create a bridge between the ease of use of Lua and the performance of C, so you hopefully will be able to create more complex games in a more accessible way!

4 Likes

Side-note: just preordered my Playdate, hope it arrives soon enough so that I can test all this stuff!
(and develop something)

are you still wrapping the API to be more Nim-friendly? very interested if so

1 Like

Yes, that's also the reason it's taking me some time!

Making 1:1 bindings would have taken much less time, but there would have been no significant gain.

I'm still a bit uncertain whether to keep the API semi-unchanged (and leave heavier changes to framework and engines, but still keeping basic improvements and automatic memory management) or just do all the heavy lifting now.
Each approach has pros and cons.

gotcha. the approach I've been playing with for Jai bindings (almost entirely untested, because ARM compilation is not yet supported) has been:

first, make a wrapper (Playdate.jai) which should be a 100%-coverage wrapper of the Playdate API, directly mimicking the C API (except for constants and enums which have been renamed to be less verbose). as of now it has some typos and small things to clean up and of course it requires Actual Testing but it should work just fine (again, once ARM compilation actually works!).

second, make a wrapper around that (Wrapper.jai—hugely unfinished lol) which will further wrap the Playdate.jai functionality to be more easy-to-use, using Jai-specific features where possible.

finally, Jai allows modules (libraries) to have "module parameters", user-facing constants in the module that the module user can set. so my idea for now is to do something like #import "Playdate"; to access the fancy wrapped version, and #import "Playdate"(FANCY_WRAPPING=false); (or something) if the user doesn't want to learn about all the changes I (will have) made to the API, instead opting for something that makes it easier to copy existing code samples into.

but like I said, I haven't actually started on the fancy-wrapped part yet, as I'm still waiting to be able to actually compile anything… but I'm interested in seeing how you tackle it! this is my first time writing a wrapper for a C library to another language with more features than C and it's an interesting problem to think about how to do it well!

1 Like

Yes that's more or less my approach too!
I also have a lower level mapping of the C API, and a more Nim-friendly interface on top (where it is appropriate).

In my case, choosing to use the underlying C API is discouraged as it would break the higher level features the Nim API builds on top of it.
This is due to the Nim wrapper needing to take complete control over how you interact with the low level API to manage memory and do other useful things.

1 Like

Following this with great curiosity!

Keep on truckin'

2 Likes

I'm almost done with the small but functional portion of the API to let you test the bindings!

Of course you will be able to debug Nim applications!

Code 2023-01-03 at 20.59.16

2 Likes

Talking about the API, which of the two options below would you prefer to instantiate a new SDK object?

//1: esplicative, follows how the C API is structured
let filePlayer = playdate.sound.newFilePlayer("/audio/nausicaa_theme")

//2: less verbose, less clear where this function comes from
let filePlayer = newFilePlayer("/audio/nausicaa_theme")

The first. More explicit is better! Especially given that with modern monitors space is not as much of an issue.

1 Like

Yeah sounds good. I'm going that way for now!