Playdate: Core Graphic Mechanics

The Playdate is a small handheld gaming system created by Panic Inc.. After enjoying some games on the Playdate, I decided to try my hand at creating some. This piece explores some basic aspects of Playdate’s graphics system and APIs. While the Playdate’s reference documentation is decent, the absence of conceptual overviews can make it difficult to get started as a beginner. This article records the results of my own investigations into the essential concepts behind the Playdate’s graphics system. Some of the assertions may be incorrect. I will correct this text over time if readers suggest corrections.

If you are experienced in game development, these notes will touch on ideas already intimately familiar to you, but they also cover a few particularities of the Playdate system that may prove helpful.

Graphics: Basics

The Playdate has a single LCD screen on which games and applications can display graphics to a user. Interpreting the contents of the screen is a primary mode of user-interaction for the vast majority of games and applications. Typically, the screen is used to report changes in a program’s state back to the user.

Since the graphical display is such an important mode of communication, systems like the Playdate check for potential changes to their graphics displays frequently, at a rate called frames per second (FPS) or frame rate. A system’s frame rate value indicates how often the system attempts to redisplay graphics each second. The Playdate has a default FPS value of thirty—that is, while a program runs, the system checks for graphics changes thirty times each second.

In order to fully understand what happens when we render graphics on the Playdate, however, we need to get more specific—what exactly is a frame?

The Frame Buffer

Internally, the Playdate needs a data region that it can check during program execution to determine if the graphics displayed on screen need to change. Within the Playdate C API, this region is called the frame buffer.

The frame buffer is effectively a canvas that you can use to compose graphics as your program runs. As the name implies, this buffer represents a graphics frame: some image content that the system may display to the screen. The Playdate system checks the contents of this buffer thirty times each second, and, if they have changed, updates the screen accordingly (we’ll explore the precise nature of how such an update happens later in this document).

The frame buffer is represented as an array of bytes. The size of the buffer mirrors the size of the Playdate screen, as measured in terms of pixels. Within this buffer, each individual pixel is represented by a single byte. This representation isn’t applied uniformly across the API, there are other cases in which pixels are represented by other datatypes.

The system’s frame buffer is unique. This uniqueness establishes a basic but important conceptual detail to understand when considering frames: though we talk about frames as multiple distinct entities in casual conversation, (for example, “the device renders thirty frames per second”) frames are not represented by multiple, distinct objects in the Playdate system. Instead, each graphics ‘frame’ is the result of changes to the contents of the singular frame buffer. The Playdate reads this buffer thirty times a second. Each ‘frame’ is represented by the contents of the buffer at the moment that the system reads its data.

You can get a pointer to the frame buffer using the getFrame() function in the Playdate C API. The following snippet prints the unique address of the frame buffer to the Playdate’s console.

char out[100];
unit8_t* frame_buff = pd->graphics->getFrame();
sprintf(out, "frame buffer address: %p", frame_buff);
pd->system->logToConsole(out);

Updating the Frame Buffer

You can update the frame buffer by writing different byte values to it directly. Unlike some other languages, C does not bundle array length information into its array type. If you want to traverse an array in C, you’ll need to know its length beforehand in order to avoid buffer overflow errors.

The Playdate C API provides a constants that you can use to control traversals across the frame buffer and avoid buffer overflows. The LCD_ROWS constant indicates how many logical rows of pixels exist in the buffer, while the LCD_ROWSIZE indicates how many pixels are in each row. Together, these values describe precisely how many bytes are in the frame buffer as LCD_ROWS*LCD_ROWSIZE.

You can modify specific pixels in a frame by changing the byte values in the frame buffer. Recall that each byte in the buffer represents one of the frame’s pixels. The LCD_ROWS and LCD_ROWSIZE constants provide a means of updating the frame buffer in terms of logical updates to each row of the Playdate screen.

The following snippet changes each pixel of a frame to a black pixel by overwriting each byte in the frame buffer:

for(int i = 0; i < LCD_ROWSIZE*LCD_ROWS; i++) {
      frame_buff[i] = kColorBlack;
}

The kColorBlack constant used in this example is a constant byte value that represents black or “on” pixels on the Playdate.

If you are familiar with Playdate’s specifications, you might recall that the screen has a size of 400 (width) by 240 (height) pixels. It may be tempting to try to use these values directly to iterate over the frame buffer, but it’s important to use the constants provided by the API instead. To account for data alignment, the frame buffer actually contains two extra bytes per row. These bytes are ignored by the system when rendering the contents of the buffer to the display. As a consequence of these extra ‘padding’ bytes, using the screen pixel dimensions directly would result in incorrect traversal of the buffer content, and probably produce unexpected results.

If you write a small program that updates the frame buffer directly in your update loop, you should see the changes be displayed on the Playdate screen. While this may make it seem like the frame buffer is a direct representation of the screen contents, the actual situation is slightly more complicated.

The Display Buffer

While the frame buffer holds the contents of each graphics frame, it does not represent the actual contents of the Playdate display. The frame buffer is essentially an intermediary area in which pixels can be arranged before they actually get displayed to the Playdate screen. The contents of the frame buffer ultimately have to find their way into another special byte array, called the display buffer before any display happens.

Just like the frame buffer, you can fetch a pointer to the system display buffer directly using the C API:

uint8_t* disp_buff = pd->graphics->getDisplayFrame();

The display buffer is unique, and it is distinct from the frame buffer. The display buffer always contains the contents of the previous frame displayed. In other words, it contains the contents of the frame buffer since the last time the system checked it and attempted to display its content. If the system time is $T_1$, then the display buffer contains whatever the frame buffer contained at time $T_0$, the previous iteration of the update loop.

Now, unlike the frame buffer, writing to the display buffer has different behavior. Try running the previous experiment using the display buffer instead of the frame buffer:

for(int i = 0; i < LCD_ROWSIZE*LCD_ROWS; i++) {
      disp_buff[i] = kColorBlack;
}

If you run this code in your update loop, you’ll notice that the display doesn’t change! This is a side-effect of the relationship between the frame buffer and the display buffer and how rendering works on the Playdate.

Earlier, we described how the system checks the frame buffer at the device frame rate to see if graphics need to be redisplayed. Roughly speaking, it does this in two steps:

(1) Check the current contents of the frame buffer against the contents of the display buffer (which holds the contents of the previous frame).

(2) If the contents differ, copy the new frame contents into the display buffer, updating the display.

The system performs this task automatically at the end of every update loop. So, approximately thirty times per second, it checks the two buffers and updates the device screen as-needed.

As a consequence of this, updating the display buffer directly has no effect. At the end of the main program loop, the system will check the frame buffer against the contents of the display buffer. If you changed the display buffer directly, but didn’t change the frame buffer, their contents will now differ. Since they differ, the system will overwrite whatever you put in the display buffer with the frame buffer contents. The ultimate effect of this is that any direct modifications to the display buffer don’t actually get rendered to the screen.

In essence, there is no reason to check the display buffer unless you want to retrieve the contents of the previous frame during your update loop.

The C API also provides a function, called display, that copies the current frame buffer contents into the display buffer immediately. As mentioned, though, the system does this for you at the end of each update loop. Most programs rarely need to use this facility.

Drawing, Bitmaps, and the Context Stack

We’ve seen that you can write to the Playdate’s frame buffer directly to update the device screen at the speed of the device frame rate. While this works, most applications use sophisticated graphics. It would be extremely difficult, and tedious, to determine how to construct precise graphics using only the bytes in the frame buffer. For each frame your program needs, you’d need to construct a precise buffer of thousands of bytes to represent it.

It’s much easier to put together the contents of a complicated frame piece by piece. The majority of the Playdate graphics API is dedicated to precisely this use case. Its drawing and image rendering functions are all utilities for working with the frame buffer. They allow you to build pieces of a frame separately and gradually, and then combine them, producing the final frame buffer contents.

In addition, while the Playdate does have functionality for drawing images directly, most applications use image assets created in other contexts and loaded into the device during runtime. Within the C API the LCDBitmap type both provides a general representation of such images and facilitates the gradual composition of the frame buffer contents.

The LCDBitmap type is opaque in the C API, however, the getBitmapData function allows us to examine portions of its structure. In particular, bitmaps have a data part that represents the contents of a one-bit image. Unlike the frame buffer, the system cannot assume that a bitmap has a fixed size—after all, some images may need to be larger than others. To accommodate variations in size, the bitmap data structure need to use a different representation than the frame buffer.

The contents of an LCDBitmap are represented by a two-dimensional array of bytes. Even though the array contains bytes, just like the frame buffer does, these bytes don’t represent pixels. Instead, each bit within one of these bytes represents a pixel. The size of this 2d array is important. Just as we need to know the size of the frame buffer to prevent buffer overflows when working with it, we need to know the size of a bitmap’s data array to avoid overflows too. Since the size of a bitmap isn’t fixed, we need a different means of determining the array size.

To construct a bitmap directly in the Playdate API, you need to provide its dimensions in pixels. These values also determine the size of the bitmap’s data array. Since this array contains bytes, the provided number of pixels need to be represented by “packed” segments of eight bits each. Unfortunately, this means that there is not a precise correspondence between the exact size in pixels and the series of bytes that represent them—there may be some “extra” bytes in the underlying array needed for data alignment.

Directly manipulating the contents of a bitmap is less straightforward than working with the frame buffer. Now, each byte represents several pixels, instead of one pixel, so changing a byte in a bitmap actually updates multiple pixels in the image. Bitwise operations can be used to target and modify the individual pixels in a bitmap, but, typically, using either prefabricated assets or the dedicated drawing APIs is simpler than directly modifying bitmap bytes yourself.

While this representation may seem a bit awkward, it’s necessary to support variable-width imagery in a compact way, and, since bytes are still the underlying unit, it’s clear that the system can put this content into a frame buffer. In fact, this is precisely what happens when you call a function like drawBitmap. The bytes representing the pixels in a bitmap are converted and arranged into the right order and written to the frame.

The bitmap representation gives us a clear way to represent the individual images or graphics components that we might use in a frame, but we still need a rigorous way to put them all together.

The Context Stack

While you can create a variety of different images as your program runs, they must all be composed into the frame buffer for display by the end of the update loop. Combining several bitmaps can quickly become complicated—one image may overlay another, or two images may collide, producing a new picture. Even in the Playdate’s one-bit graphic space, there are complexities that the system needs to resolve.

The Playdate API offers a mechanism called the graphics context stack to manage the composition of bitmaps into a final collection of pixels for rendering. The stack is a traditional stack data structure. Passing a bitmap to pushContext adds it to the top of the stack and makes it the active target for drawing routines. While a particular bitmap is on top of the stack, subsequent graphics calls, including drawBitmap will apply to that specific bitmap. So, after adding a bitmap to the context stack, you can call drawBitmap to draw another bitmap “on top of it”, yielding a composite image. This provides a flexible mechanism for gradually constructing graphics.

The popContext function removes the bitmap on the top of the stack from the context stack. Any subsequent calls to drawing routines will not apply to that bitmap. So, the basic procedure for working with images can be roughly summarized as follows: add the image to the stack, call whatever drawing routines you need to alter the image, pop the image off the stack when you are finished.

What happens when no images remain on the stack? As it turns out, the frame buffer is always “pinned” at the bottom of the stack. Once you’ve updated and popped a series of images off the stack, and no bitmaps remain in the context stack, all graphics routines will apply to the frame buffer. This also implies that, by default, before you add any images to the stack, graphics routines like drawBitmap apply directly to the frame buffer. Once you’re finished constructing all of your bitmaps and the stack is empty, you can make a call to drawBitmap for each image to add them each to the frame buffer. Then, once the update loop ends, the system will render your composite frame.

Lua

While this article focused on the Playdate’s C API, all the concepts discussed here also apply to Playdate programs written in the Lua programming language. Many of the core Lua APIs are wrappers around the C APIs. All of the core concepts related to the way graphics work in the C API also apply to Lua. For example, you can retrieve the frame buffer in Lua by calling getWorkingImage, a name that connotes what we’ve discussed here, that the frame buffer is your working area for building up graphics.

Conclusion

There is plenty more to cover when it comes to the Playdate’s graphics API, but I hope that this introduction captured the essential workings of the mechanisms behind the system’s display. By way of summary, a program writes pixel data to a frame buffer, either directly or using drawing routines and the context stack, then, at the end of the core update loop, the system renders any contents that changed since the last frame to the display.

Most of the time, you’ll work at a higher level than the one explored in this piece. The vast majority of applications should use dedicated drawing APIs and higher-level concepts like sprites to manage their graphics, but having an understanding of how everything works will hopefully help you use these mechanisms effectively.