Imagetable malloc

Hello! I'm looking for some insight into how the imagetable.new() function works loading sequential image tables. The docs describe it well enough, and mention explicitly that it happens at compile time which, admittedly, I don't fully understand the impact of but sounds relevant. Some test code:

import 'CoreLibs/graphics'

-- 1.5mb malloc for a 135kb .GIF ?
imageTable1 = playdate.graphics.imagetable.new('test.gif')  

-- 1.5mb malloc for a 215kb image sequence folder ?
imageTable2 = playdate.graphics.imagetable.new('sequence/image')

function playdate.update() end

Packaged up if someone wants to run it:
imagetable-test.zip (418.8 KB)

Some caveats:

  • I did test with different .GIFs to confirm, and the test .GIF inside of the folder is a direct capture .GIF from the Playdate Simulator (while the image sequence folder just that same .GIF split out into .PNGs)
  • I realize this is deeply wasteful way to animate something like this, but for the purposes of where I need it (displaying a few short 5-10s "preview" cards of games in a game collection UI interface), it doesn't necessarily need to be highly performant/optimized at this very moment
  • Optimization also isn't my forte yet, so there's a very good chance I don't fully understand something about how this function is meant to work

I saw a few threads from @matt on some raw .GIF optimization tips, but I didn't see any detail on how those size optimizations translated into performance or memory allocation in practice in the games.

If anyone has any insight into how this function is working, I'd be grateful!

Thanks,

How many frames are in each GIF? GIFs can have a variety of compression added to them, but once compiled into your game the compression is removed by the Playdate compiler (pdc) and you’re left with a full uncompressed image per frame (either a pdi or pdt file). Now, these are 1-bit images with some basic run length compression I believe but they’re designed for quick decoding on device. That’s how I understand it anyway. :slight_smile:

Some quick thoughts: you could either reduce the number of frames in your GIF, change the dimensions, or consider using the SDKs video functionality.

I should say, though not ideal, consider that the device does have 16MB of memory so you have some room to play if you don’t have too many of these large animations in memory and discard them the moment you’re done with them.

1 Like

The only thing I'd add to Dustin's overview is that frames of an image table have their transparent edges/margins trimmed during the loading.

Dave said what is stored is "an offset table followed by a series of image structs, but each of those may have a transparent border trimmed so that can make things unpredictable"

Generalising, I had asked: "is it easy to calculate how much RAM a bitmap (actually, image table) will consume? WxHx3?" The answer is, theoretically, at 1 bit per pixel: W*H÷8 but we have to take into account the edge trimming and structure overhead.

  • W×H = 13680×836 = 11,436,480
  • 11,436,480/8 = 1,429,560
  • but the simulator showed a different value: 1,658,828
  • so the final ratio for this particular imagetable was (W×H÷8)×1.16
  • for reference: compiled pdt file for this imagetable was 263,671 bytes

A more practical example, one of my car sprite tests had 360 rotation steps, which given my use of 11 rows, meant 3960 frames each for car and shadow, so a total of 7920 frames. All frames are 38x38 pixels and have cars of varying sizes and rotations at roughly the centre of each frame. Total RAM usage: 1.6MB. This performed just fine on the device, with a slight loading delay which might be able to be optimised somewhat. I eventually settled to use less rotations, which sees my RAM usage for the player car (which is more than just one imagetable) at ~1MB RAM.

They don't translate at all. My tips are only for distributing GIFs, not for use in games.

1 Like

Thank you both, this is exactly the information I needed!

Not sure why I hadn't thought to try Video, that will be my next test.

This is a great piece of data to understand what types of RAM usage I might target. Again, what I'm doing it for is pretty low-stakes in terms of interactivity, so maybe I shouldn't be too worried about it, but generally I like to understand what's going on and learn to optimize when possible. :slight_smile:

One final thought, so generally speaking a pdt file is compressed on build, and thus much smaller. Then at run time it's being uncompressed and allocated to memory as a whole, so the system has quick/direct access to each "image" inside of it (or access to a particular offset position of the one image, I guess)? Just trying to properly mentally model this flow so I understand it in the future !

Thank you both for the knowledge share :+1:t5:

Exactly.

This is the case with any compressed image in any app. A GIF/JPG/PNG loaded into Photoshop, or your web browser, will take up the uncompressed amount of RAM. Photoshop shows you the uncompressed size in the status bar.

1 Like

Well that's eye-opening. I think it's time for me to do some deeper learning on compression. : )

A nice thing about the Playdate being 1-bit is that images in memory use far less than traditional color bitmaps. Each byte of the image represents 8 pixels. So in a way you have device constraint compression. :wink:

2 Likes