C games: pdc can reserve too little memory for global variables (SDK 3.1.2)

Summary
If a C game has a global variable that needs 8- or 16-byte alignment (a double, an int64_t, or a SIMD/cglm matrix), the Playdate can reserve 4–12 bytes too little memory for the game’s global variables. The last few globals then sit outside the game’s memory and can get overwritten. On the device that looks like a global randomly turning into garbage. The Simulator is not affected.

Why it happens

  • The SDK linker script (C_API/buildsupport/link_map.ld) ends the initialized data (.data) on a 4-byte boundary.
  • The zero-initialized globals (.bss) start at their own alignment, so the linker can skip 4, 8 or 12 bytes between the two.
  • pdc sizes the game’s memory as “file size + .bss size”, which doesn’t count the skipped bytes. The reserved memory therefore ends that many bytes before the real end of .bss.
  • The last word of .bss is always pdrealloc from the SDK’s setup.c, so that pointer is always one of the affected bytes.

Repro
I was not allowed to attach a .zip, so here's a minimal repo that shows the bug: GitHub - magnusahlberg/pdc-bss-gap-repro: Minimal repro: Playdate pdc reserves too little memory when .bss is more than 4-byte aligned · GitHub

make layout compares the ELF with the header pdc writes into pdex.bin:

.bss ends         0x00005564
pdex.bin header:  memory size 0x00005558
=> 12 bytes of .bss outside the reserved memory

With plain double globals (make layout UDEFS=-DUSE_DOUBLE) it’s 4 bytes. On a device (firmware 3.1.1), the included BssGap.pdx reads its own header and reports the same 12 bytes.

How I found it
My 3D engine (which uses cglm) crashed on the device on its title screen. A global menu index, which was in the affected bytes, had turned into garbage. The crash moved around as I added or removed globals and never happened in the Simulator. The workaround below fixed it. Note about AI: The bug was found and the proposed solution came from Claude Opus 5.5, as this is way beyond my field of knowledge.

To be clear, the repro shows that the memory is reserved too small. In this small program the bytes weren’t overwritten during a short run; whether they are depends on how the game uses memory.

Workaround
Copy link_map.ld into the project, add . = ALIGN(16); or . = ALIGN(32); just before __data_end__ = .;, and set it after the include in the Makefile:

include $(SDK)/C_API/buildsupport/common.mk
LDSCRIPT := $(CURDIR)/link_map_fixed.ld

With that change the repro reports 0 bytes outside.

Suggested fix
Either have pdc compute the memory size from the actual end of .bss (its address plus its size), or pad .data in link_map.ld as above.

Setup: SDK 3.1.2 (also seen with 2.7.2), firmware 3.1.2, macOS, arm-none-eabi-gcc 9-2019-q4-major.