Working on 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 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 = nil
var x = int((400 - TEXT_WIDTH) / 2)
var y = int((240 - TEXT_HEIGHT) / 2)
var dx = 1
var dy = 2


proc update(api: PlaydateAPI): int =
    api.graphics.clear(kColorWhite.LCDColor)
    api.graphics.setFont(font)
    api.graphics.drawText("Hello World!", len("Hello World!"), kASCIIEncoding, 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
        
    api.system.drawFPS(0, 0)

    return 1

proc eventHandler*(api: PlaydateAPI, event: PDSystemEvent, arg: uint32): cint {.cdecl, exportc.} =
    if event == kEventInit:
        try:
            font = api.graphics.loadFont(FONT_PATH)
        except:
            api.system.error("%s %s", ($compilerInfo()).cstring, getCurrentExceptionMsg().cstring)

        api.system.setUpdateCallback(update, api)

    return 0

A few things can 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!

I'm still at the beginning of the SDK wrapping process, but I successfully compiled the example above for the simulator and for the device (although I don't have a device and I didn't test it on one).

It would be great if someone could test this pdx on a real device!
HelloWorld.pdx.zip (24.8 KB)

I built it on a M1 Mac, works on the simulator too.

Let me know if this project can interest the community!

5 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