Porting Super Hexagon to the Playdate

chmaynard1 pts0 comments

Porting Super Hexagon to the Playdate · Nemo's Home

Nemo's Home

Porting Super Hexagon to the Playdate ⚓

26 Jul 2026<br>Super Hexagon (by Terry Cavanagh) is one of my all-time favorite games. If you<br>haven’t played it, here’s the trailer (from 13 years ago):

It is a rhythm game with only two controls, and an official Linux port. The<br>controls are circular (left and right rotate the player triangle around the screen) and ever since I got the Playdate,<br>I’ve always thought that the crank on the playdate would make an amazing control for Super Hexagon.

Demo

Here’s me playing Super Hexagon on my playdate:

Super Hexagon playing in the simulator. 9MB Video, click to load. (Has Audio)

Me playing Super Hexagon on the Playdate. (Has Audio)

Notably: This is an actual port from a Linux x64 OpenGL build to the same<br>code running on the Playdate at the maximum FPS (50) a much more contrained<br>device: (16MB RAM, 400x240px black and white screen, ARM Cortex-M7F @ 168<br>MHz).

Here is the game ending playing across 3 modes (click on any to play/pause, no audio):

Original/Linux

Port/Linux

Playdate

The endings differ by a few frames, but the gameplay result is fairly accurate.

AI-usage

This was mostly driven over 2 weeks primarily using Claude Code<br>(Opus 4.8/Fable 5). I also tested out GLM5.2 (using magnitude.dev) as well as<br>GitHub Copilot. The primary learning here was around designing fast-feedback<br>loops for reverse-engineering work.

Decompilation

This was a somewhat lucky pick: The Linux binaries for Super Hexagon were shipped unstripped, and several older<br>binaries were published as well with a changelog. There’s some existing work on reverse engineering the game, which was helpful along<br>with notes from Ethan Lee (who did the Linux port) on their approach to packaging the game.

The first approach I tried was the obvious one: generate C++ from the<br>disassembly, recompile it, and diff the resulting disassembly byte-for-byte<br>against the original, function by function using the same compiler toolchain<br>as the original. That got 89 of 220 simple-functions matching exactly. These<br>were the low-hanging fruits: - simple setters and getters and menu switches.

The remaining were broken mainly on register allocations, which are much<br>harder to match on x64 (most decompilation matching ports are older gen<br>consoles). The reason why we only had ~220 functions is because the game is<br>built on open-frameworks, and I was hopeful of reusing upstream code for<br>everything except the core game. However, at this point, I wasn’t trying a<br>full-compilation.

I changed the approach from byte-matching to trace-matching with Frida: hook a<br>running binary and snapshot labeled windows of game state — enemy positions,<br>camera angle on every frame into a comparable trace.

Sample Frida trace

{"t":"call","f":79,"fn":"seekangle","args":[45]}<br>{"t":"call","f":79,"fn":"seekangle","args":[45]}<br>"t": "frame",<br>"f": 79,<br>"rng": 0,<br>"w": {<br>"gA": {<br>"state8": 0,<br>"soundenabled": 1,<br>"fullscreen": 0,<br>"field_1c": 768,<br>"field_20": 480,<br>"assigned": [0, 0, 0, 0, 0, 0, 0, 0],<br>"field_2d": 0,<br>"field_30": 0,<br>"field_34": 0,<br>"field_38": 1,

},<br>"gB": {<br>"field_e0": 1,<br>"start_pulse": 0,<br>"field_ec": 150,<br>"start_camera": 40,<br>"start_divisor": 6,<br>"str_108": [-1347501544, 32669, 0, 0, 0, 0, 0, 0],<br>"field_114": 60,<br>"field_118": 0,<br>"field_120": 180,<br>"field_124": 300,<br>"field_12c": 0,<br>"field_130": 0,<br>"field_134": -16,<br>"field_138": 0,<br>"field_140": 0,<br>"field_14c": 0<br>},<br>"gW": {<br>"enemy_count": 0,<br>"next_level_time": 0,<br>"wave_delay": 60,<br>"waves_generated": 0,<br>"wave_kind_flag": 0,<br>"wall_speed": 25,<br>"start_val_a": 30,<br>"start_val_b": 30,<br>"field_2890": 4,<br>"field_2894": 0,<br>"check_collisions_flag": -1,<br>"besttimes": [0, 0, 0, 0, 0, 0],<br>"current_time": 0,<br>"camera_offset": 59,<br>"field_28c0": 0,<br>"damage_flash_timer": 0,<br>"levelup_flag": 0,<br>"pulse_decay_timer": 0,<br>"cur_stage_num": 0,<br>"field_28d8": -1,<br>"field_28dc": 0,<br>"wave_pattern_seed": 0<br>},<br>"gS": {<br>"field_5380": 72.83199846221441,<br>"seekangle_cur": 45,<br>"visual_override_value": 3500,<br>"visual_pulse_state": 0,<br>"visual_pulse_value": 0,<br>"visual_pulse_trigger": 0,<br>"visual_sweep_state": 0,<br>"visual_sweep_value": 0,<br>"visual_sweep_timer": 0,<br>"rotation_effect_state": 0,<br>"rotation_effect_timer": 0,<br>"stage_transition_state": 0,<br>"pulse_cooldown": 0,<br>"curstage": -2,<br>"rotation": 0,<br>"compl_a": [0, 0, 0],<br>"compl_b": [0, 0, 0],<br>"field_53f8": 0,<br>"wheel_spin": 0,<br>"win_state": 0,<br>"first_wave_pending": 0,<br>"hyperflag": 0,<br>"side_change_pending": 0,<br>"field_5418": 24,<br>"field_541c": 6,<br>"field_5420": 0,<br>"field_5424": 30,<br>"field_5428": 30,<br>"field_542c": 24,<br>"levelthresh": [600, 1200, 1800, 2700, 3600, 7200],<br>"bar_start": 0,<br>"bar_end": 0,<br>"bar_cur": 0,<br>"graphicsmode": 0,<br>"camdepth": 850,<br>"field_5508": 480,<br>"field_550c": 0,<br>"field_550d": 0,<br>"field_5510": 0,<br>"field_5514": 0,<br>"tutorialflag": 1,<br>"field_551c": 0,<br>"field_5524": 0,<br>"field_5530": 0.479425538604203,<br>"field_5538": 384,<br>"field_553c": 0,<br>"field_5540": 0,<br>"field_5544": 0,<br>"credits": [0, 0, 0, 0, 0, 0],<br>"field_5568":...

super hexagon playdate game linux from

Related Articles