Gravity Express

Ditching the import statement to improve boot times

As per the docs, the import statement is a function available in the Playdate SDK that is not part of standard Lua. Conversely, the require statement is in every Lua distribution except for Playdate. The import statements are not run by the playdate, but instead by the pdc compiler. import "otherfile" basically means: read otherfile.lua and paste everything you find there at the current position in the current file. This way, you as developer can keep your projects organized, while the playdate only has to run a single file to load your entire game.

I can't remember whether the rationale is explained somewhere, but there are some apparent advantages to import: we would rather wait a second for the game to load and then have a smooth experience, rather than have stutters during gameplay when gunshot.wav is loaded for the first time when you press the A button. That's the wrong kind of bullet time. File access is considered slow on playdate, so we save on the overhead of loading 50 tiny lua files when we just load the main.lua. Note: pdc gives compiled lua files the extension pdz. The playdate actually never sees any lua file, just main.pdz.

This works well for a sample games like Level1-1, which is so small that the game can pull off a seamless transition between the launch card animation and the game's start screen. Level 1-1's main.pdz weighs in at 28 kB.

Gravity Express's total compiled code is 475% the size of Level 1-1's: or 133 KB. It's not just code that gets loaded though. When running the code, images, sounds and other assets will have to be fetched from disk.

Let's have a look at the screen recording using mirror:

Here is some timing debug logging from a playdate without Mirror running (Mirror puts extra strain on the cpu, so should be eliminated from the equation)

Before

0.8010885  hoi *
1.4860770  Start rendered

* "hoi" is "hi" in Dutch.
I wrote this line some 15 years ago. Remember that this started out as a homebrew game for the PlayStation Portable. I'm keeping it in there for nostalgic reasons

The "hoi" line is a benchmark for the earliest moment after the game is started that code is run. It is at the top of my main.lua.

We see that loading the pdz file takes 0.8 seconds, and then more loading is done until at time t=1.49 seconds, our first frame of the start screen is rendered.

Most players would find this acceptable, I think. Many Season games take a comparable time to load, albeit while showing a "loading" message on screen. What bothers me about it though, is that it breaks up the swish (launch animation) -> swoosh (start screen appears) transition. 1.4s is not long enough to get worried that the device has crashed, but it is long enough that you don't see the swish-swoosh as a single transition.

Let's also have a look at file size

main.pdz: 188 KB
other pdz's: 1.6 MB
total: 1.8 MB

That is an enormous amount of data, for compiled code. Remember that Level 1-1 only had a main.pdz of 28KB.

What is happening here is that the sdk's promise of importing lua files only once is not kept when your project structure contains subdirectories and you use import like this:

import "../credits/CreditsScreen.lua"

The lua extension is never used in the samples, but without it, pdc can't find the file in my case. The result is that code gets combined and duplicated all over the place. (we might import startScreen in main.lua and then again in the credits screen so that we can show the start screen again when exiting the credits. We now have 2 copies of the startScreen code in our pdx.)
I filed a bug for that here. I don't believe it has been addressed yet. Luckily, we can replace import by something much better and more suitable for the project, using only 8 lines of code.

I now have two reasons to look into this issue:

  • decreasing pdx file size
  • improving startup speed

After

Allright, let's look at that new code. I cheeckily called the function require because the name is still available and gets the proper syntax highlighting in a code editor:

-- must be placed in the root of the source directory, eg. at the top of main.lua
local run <const> = playdate.file.run


--- All paths that have already been required
local requiredPaths <const> = {}  

--- @param sourcePath path relative to source directory  
function require(sourcePath)  
  if requiredPaths[sourcePath] then  
    print("SKIP: Already required", sourcePath)  
    return  
  end  
  print("RUN " .. sourcePath)  
  requiredPaths[sourcePath] = true  
  return run(sourcePath)  
end

Here is a sample usage, that only loads the settings screen when the player selects an option from the menu:

menu:addMenuItem("Settings", 
    function()
        -- this code is only executed when The Settings menu item is selected
        require "lua/settings/SettingsScreen"
        -- pushScreen is part of my Scene management system
        pushScreen(SettingsScreen())
    end
)

Don't get me wrong, I still use inport to combine multiple files into one. The Startscreen imports StartView and StartViewModel, for example. From main, we require Startscreen. Main has no reference to LevelSelectScreen though, and that is the reason why main, and thus our boot time, can remain small.
The StartViewModel contains code to require LevelSelectScreen, but it is only executed when the user selects the level select option in the startscreen.

What we expect to find in our pdx file now are a main.pdx and several *Screen.pdz files. This is indeed the case. But the LevelSelectViewModel.pdz and -View are there as well. That's strange.
I think the reason is that pdc will compile all Lua files that are not imported as pdz in the pdx. The unexpected files are indeed imported, but there is no import chain going back to main.
I manually deleted the unexpected files and indeed the game still runs. So, I created a bash script to find and delete redundant files from the pdx on every build. And finally arrived at the expected numbers above:

0.3018594  hoi
0.4702041  RUN lua/start/startScreen
0.5456690  Start rendered

main.pdz: 55 KB
other pdzs: 76 KB
total: 133 KB

Avery nice result that saves 1.5 MB in file size and almost one second of boot time!
I feel it is just fast enough now to make the swish-swoosh feel like a connected animation. Judge for yourself by watching the video above. Again, remeber that screen capturing imposes an extra slowdown on the device

13 Likes