What it would actually take to bring Jordan Mechner’s 1989 Apple II Prince of Persia to the PC3 in MMBasic, measured against the released source rather than guessed at: what the original is, what the PicoMite gives us, what has to be transcribed, what can be deleted, and the one constraint that is not technical at all.
Every number in this document was measured from the released repository — the 6502 sources, the sixteen image tables and the fifteen level files — not taken from secondary accounts.
Prince of Persia is not a performance problem. It is a transcription problem, and an unusually well-posed one, because what was released is the actual source — labelled, commented, and structured into named modules — not a reverse-engineered disassembly. Against that, it is the biggest body of game logic we have looked at: roughly 20,000 lines of 6502 across 29 files, of which about 12,000 lines are game behaviour that has to come across.
The decisive number is in the game’s own timekeeping. SPECIALK.S defines a game minute as
725 frames and its author annotated the constant with a note that the actual frame rate is approximately eleven
a second. That is 83 milliseconds a frame. Thrust had 30 ms and Exile 40 ms. We have more than
twice Exile’s budget for a game that, once the Apple II’s memory-banking and disk machinery is
stripped out, does considerably less per frame than Exile does.
The renderer is cheap, because the PicoMite already has the primitive the original spends a third of its
code emulating. Prince of Persia’s HIRES.S is 1,293 lines of shift tables, carry bytes,
five opacity modes and a “peel” buffer, all of it there to move a masked bitmap across a screen
whose bytes hold seven pixels apiece. BLIT FLASH with a transparent colour does the same job in
one call, and the RAM image slots added in b11 hold every image the game owns with room to spare.
The risk is not the frame rate and not the memory. It is volume, and it is the two subsystems that were never released: the music, and the artwork’s provenance.
Technically the easiest of the four ports we have scoped, and by a wide margin the longest. Whether it can ever leave your desk is a separate question, answered in the next section, and it should be answered before any code is written.
Mechner’s README is unusually direct about this, and it is worth taking at face value rather than treating it as boilerplate. He released the code as a historical artifact, says he has no objection to anyone studying, modifying or running it, and then states plainly that this grants no rights to Prince of Persia itself, and that Ubisoft alone has the right to make and distribute Prince of Persia games.
That is a materially different position from the ones behind the earlier ports. The Thrust and Exile disassemblies carried no licence at all, which is a grey area; this carries an explicit reservation, which is not. It also lands differently because of what the assets are.
When Arkanoid became Picanoid, the artwork could be replaced in an afternoon by a Python script, because the artwork was bricks. Prince of Persia’s identity is 290 rotoscoped character images traced from film of a real person running, jumping and falling. Re-drawing those as original work is not a scripting job; it is an animation project of its own, and a port with different animation is not recognisably the same game. The engine can be made original. The thing people would want to see cannot, cheaply.
Build it, run it, keep it. The engineering record and the tooling live in the repo; the game itself is not published, not packaged for the A: drive, and not put in a release. This is what the released source explicitly permits and it costs nothing to honour.
Ship the animation-sequence interpreter, the block and blueprint renderer and the level loader as a documented MMBasic platformer engine with original art and original levels. Genuinely useful to other PicoMite users, and it is the part of the work that is ours. The rotoscoped frames never ship.
Mechner has been generous with this material for over a decade and points technical and legal questions at the community rather than answering them himself. Ubisoft is the party that would have to say yes. Low odds, but the cost of asking is one email and it can run in parallel with Phase 0.
This is a judgement for you, not for me. It is placed first only because it changes the shape of the work: Option B implies building the engine against placeholder art from day one, which is a different Phase 1 from Option A.
| Item | Value | Where it comes from |
|---|---|---|
| Frame budget | 83 ms at the original’s own rate, against 30 ms for Thrust and 40 ms for Exile | 725 frames per game minute, SPECIALK.S |
| Screen | MODE 2, 320×240, 16 colours. The game’s 280×192 sits inside it at 1:1 with a 20-pixel side border, no scaling and no resampling | EQ.S screen constants |
| Colours needed | Four. Bit 7 is set on every one of the 95,869 image bytes, so the whole game is drawn in one Apple palette group: black, white, blue, orange | Measured across all sixteen tables |
| Image slots | Eight on the RP2350 — three in flash, five in PSRAM — at 144 KB each, so 1.1 MB of addressable artwork | MAXIMAGESLOTS, added in 6.03.02b11 |
| Artwork to store | 157 KB for one level’s working set, 273 KB with both facings pre-mirrored, 571 KB for every image in the game with both facings | Measured, converted to RGB121 at two pixels a byte |
| Level data | 15 files of 2,304 bytes — 34,560 bytes for the entire game world, permanently resident | Measured; matches the EQ.S blueprint layout byte for byte |
| Sprite drawing | BLIT FLASH takes a transparent colour but has no mirror; SPRITE SHOW mirrors in modes 4 to 7 but needs heap buffers | graphics/Blit.c, graphics/Sprite.c |
| Double buffering | FRAMEBUFFER CREATE F plus COPY F,N; MODE 2 with an F buffer is 76,800 B and fits the cut-down pool | Proven in Thrust, Exile, Arkanoid |
| Program memory | 144 KB tokenised on HDMIWEB. Thrust was 66 KB of source, Chuckie Egg 53 KB | MAX_PROG_SIZE |
| Input | KEYDOWN reports up to six held keys; the original needs three at once at most | Established |
| Sound | PLAY SOUND and PLAY BBC SOUND on four channels; PLAY MODFILE for music | Established |
Only one row of that table is tight, and it is the program memory. Twelve thousand lines of 6502 do not become twelve thousand lines of MMBasic — a great deal of it is register shuffling and bank switching that collapses to nothing — but this is the first port where the tokenised size cap is a live concern rather than a footnote. Every table stays out of the program text from the first line written, and the CSUB-in-the-library route is held in reserve.
| Module | Lines | What it is | Port |
|---|---|---|---|
FRAMEADV | 1,528 | Builds the draw lists for a frame and redraws only the blocks that changed | Partly |
CTRL | 1,440 | The player’s state machine: what a held key means given the current pose, floor and obstacle | Transcribe |
MOVER | 1,344 | The world’s moving parts — spikes, slicers, gates, loose floors, pressure plates, falling debris | Transcribe |
HIRES | 1,293 | The bitmap blitter: shifts, carries, five opacity modes, background peel and restore | Delete |
GRAFIX | 1,292 | The auxiliary-memory thunk layer — every cross-bank call in the game goes through it | Delete |
AUTO | 1,283 | The guard AI and the demo playback recorder | Transcribe |
CTRLSUBS | 1,283 | The helpers the control machine leans on: reach tests, ledge grabs, landing choices | Transcribe |
SUBS | 1,230 | Level transitions, screen linking, timers, the mirror, the potions | Transcribe |
SEQTABLE | 1,187 | 114 named animation sequences written in a 15-opcode byte code | Convert |
TOPCTRL | 1,068 | The main loop, level restart, the 60-minute clock | Transcribe |
COLL | 924 | Collision: character against block, character against character, sword reach | Transcribe |
SPECIALK | 898 | Keyboard and joystick, the sound queue, the game clock, the cheat keys | Partly |
MASTER | 824 | Disk loading, bank switching, the attract sequence | Mostly delete |
GAMEBG | 771 | Strength meters, messages, torches, the hourglass, the flasks | Transcribe |
MISC | 690 | Odds and ends, including the random number generator | Transcribe |
UNPACK | 577 | The title-screen decompressor and fade routines | Delete |
FRAMEDEF | 437 | The frame table: for each pose, its image, sword image, movement and collision box | Convert |
Plus HRTABLES (215), SOUND (191), BOOT (155) and nine smaller equate and data files. Whole-repository total: 30,889 lines, 20,022 of them neither blank nor comment. | |||
Everything that makes Prince of Persia feel like Prince of Persia lives in two tables and an interpreter,
and all three are small. FRAMEDEF.S holds 415 frame definitions in five bytes each: the image to
draw, which sword image goes with it, how far the character moves in x and y that frame, and a packed
collision descriptor. SEQTABLE.S holds 114 named sequences — the run cycle, the hanging drop,
the block-to-strike, the stair climb — written in a byte code of fifteen instructions that can jump, turn
the character round, change floor level, alter x or y, set the action class, trigger a fall, test
weightlessness, kill, and advance the level.
That is a virtual machine, and porting a virtual machine is the most reliable kind of transcription there is: the interpreter is perhaps 150 lines of MMBasic, and the sequences themselves are data that a Python script converts once and never touches again. Get the interpreter exactly right and every movement in the game is exactly right, including the ones nobody can describe in words.
A level is 24 screens of 30 blocks, ten across and three down. Each block carries a type from a list of 30 — floor, spikes, gate, loose floor, pressure plate, torch, mirror, exit, slicer — and a one-byte modifier. Alongside that sits a link table saying which screen lies left, right, above and below each screen, a 96-byte map, and 256 bytes of starting information: where the Kid begins, which way he faces, where the sword lies, and the block, facing, position and behaviour program for each of up to 24 guards.
The whole thing is 2,304 bytes, and the layout in the released equates accounts for all of it with nothing left over. That is a format you can trust without a single guess, which is not something we have had before. Across the fifteen levels only 28 of the 30 block types are ever used, and 42 guards are placed in the entire game.
The reason the original’s renderer is more complicated than it looks is that a block is not one image. It is up to five, drawn in a fixed order: a back section, a middle section, a front-of-middle section, a floor section, and a foreground piece that draws over the character. Each has its own image number, its own vertical offset, and in two cases its own mask. This is how a character walks behind a pillar and in front of the back wall on a machine with no sprite priority hardware, and it has to be reproduced exactly or the depth reads wrong. A block’s appearance also depends on the block to its left and the row below, which is why the draw loop carries the preceding block’s identity forward.
Sixteen image tables parse cleanly against the format documented in HIRES.S: a pointer table
indexed from one, then each image as a width in bytes, a height in lines, and packed rows. No compression, no
surprises. One level’s working set is 468 of them.
The Apple’s palette-select bit is set on 100.0% of image bytes in every table. There is no per-byte palette decision to carry across. A pixel is on or off, and its colour follows from its position: black, white, blue and orange, four of MODE 2’s sixteen.
The frame tables note it explicitly, and the right-facing half of the game is produced by a mirror bit in
the opacity byte. BLIT FLASH has no mirror. Pre-mirroring the 290 character images at build time
costs 117 KB and removes the problem entirely.
725 frames to a game minute, sixty game minutes to a game: 43,500 frames, 12.08 a second. The Apple never locked to the display at all — the rate was whatever the redraw cost, and the clock was calibrated to the average.
The blueprint reserves 24 guard slots a level and the busiest level uses six, but the engine tracks a single opponent. The per-frame character load is the Kid, at most one opponent, and the occasional mouse or falling debris.
The effects are speaker-toggle routines with hand-counted delays — they describe a waveform, not a note, and cannot be transcribed. The music player is in the support tree; the songs it plays are not in the repository at all.
The largest single file in the game exists to avoid work we no longer need to avoid. FRAMEADV.S
maintains eight redraw buffers, tracks which of the 30 blocks changed, saves the background behind each
character before drawing it and peels it off afterwards — all so the Apple did not have to repaint
8 KB at 1 MHz. With 83 ms and a framebuffer, the port clears the F buffer, walks the 30 blocks
drawing their five sections in order, draws the characters, draws the foreground pieces over them, draws the
meters, and copies. The peel machinery, the redraw buffers and the dirty-block bookkeeping all go.
What must survive from that file is the order, not the optimisation: the plane model of background, middle, objects, foreground and messages, and the rule that a block’s appearance depends on its neighbours. Those are correctness, not speed.
The cost is roughly 150 BLIT FLASH calls a frame for the background plus a handful for
characters. Exile measured a full frame — one tilemap draw and sixty sprites — at about 5 ms, so
this should land in the low tens of milliseconds against an 83 ms budget. That is an inference from a
different workload, not a measurement, and it is the first thing Phase 0 should settle.
At 378 MHz the PC3 runs 640×480 at 60 Hz, and presenting one frame in five gives exactly 12.000 fps against the original’s 12.083. A sixty-minute game would run about twenty-five seconds long, which nobody can perceive and which keeps the interpreter at its fastest clock. The alternative — 315 MHz for a 75 Hz display — divides to 12.5, which is further out and costs interpreter speed.
As with Thrust and Arkanoid, the per-frame constants are integers and must not be rescaled. The sequence byte code moves a character by whole pixels per frame; the timers count frames. Change the tick rate and every one of them is wrong together, which is the failure mode that is hardest to see because nothing looks broken — the game just plays wrong.
The b11 RAM image slots land exactly where this port needs them. Everything the game owns, both facings pre-mirrored, is 571 KB — four of the five PSRAM slots, leaving the three flash slots free and the library slot untouched. Nothing needs loading between levels, which deletes the whole of the original’s set-management logic and the disk track tables that feed it.
Slots are volatile, so the installer writes them once per run from a bitmap on the A: drive, the same pattern Arkanoid uses. A 320×240 image loads into a RAM slot in 28 ms, so a cold start costs well under a second.
280×192 centred in 320×240 is the authentic grid at 1:1. The 20-pixel side borders and 24-pixel top and bottom are roughly what an Apple II displayed into an overscanned television. Resist the temptation to stretch it: the artwork was drawn for pixels of this shape, and MODE 2 pixels on the PC3 are close enough to the Apple’s that the proportions come out right by doing nothing.
Four converters, all offline Python, all validated before a line of game logic is written. This is the pattern that worked for Thrust, Exile and Arkanoid and there is no reason to depart from it.
Walk the pointer table, decode each image from its width and height header, expand the packed rows applying the Apple colour rule, emit both facings for character tables, and pack the lot into sheets with an index. The output is a bitmap per slot plus a BASIC include giving every image its origin, width and height.
Validation: render a known frame and compare against an emulator screenshot pixel for pixel, as the Exile sprite sheet was validated.
The layout is fully documented in the equates and the files match it exactly. Emit all fifteen as a single
file read with MEMORY INPUT into an integer array at startup — 34,560 bytes, trivial on a
machine with 6 MB of PSRAM free.
415 rows of five bytes, plus the two alternate sets that re-point the same poses at the opponent’s image table. Straight array data. The packed collision descriptor should be unpacked into its fields once, at conversion time, rather than masked at runtime in the interpreter.
The 114 sequences assemble into a byte stream with absolute addresses for the jumps. The converter resolves
the labels to array indices and emits the same stream with the same semantics. The MMBasic interpreter for it
is a SELECT CASE over fifteen opcodes.
This is the highest-value converter in the set and the one to write first, because the interpreter can be exercised against it long before anything is drawn.
HIRES, HRTABLES, HRPARAMS, 1,560
lines). Shift tables, carry tables, mirror tables, mask tables, five opacity modes and the peel buffer.
Replaced by one firmware call.GRAFIX, 1,292 lines). The game runs in auxiliary
memory and reaches main memory through a jump table; every graphics call in the game is really two calls and a
pair of soft-switch writes. All of it evaporates.UNPACK, 577 lines), which unpacks double
hi-res images the repository does not contain.PLAY SOUND effects by ear. Thrust and Arkanoid both hit this and it
was a day’s work each; twenty effects here rather than seventeen.The demo and attract playback in AUTO.S is a recorded input stream replayed through the normal
control path. That is free once the control path works, and it doubles as the best regression test the port could
have: record a run, replay it, and any divergence in the state machine shows up as the Kid walking into a
wall.
Convert one background set, load it into a PSRAM image slot, draw one static screen — 30 blocks, five sections each — into the F buffer and time it. That single number decides whether the renderer is a non-issue (expected) or needs a CSUB (not expected). It also confirms the 1:1 pixel grid looks right on a real display, and that the four-colour mapping reads correctly.
Images, levels, frame definitions, sequences. Each validated on its own terms: the image sheet against an emulator screenshot, the blueprint parse against the documented layout, the sequence stream against a hand-walk of two or three known animations. Nothing runs on the board yet except the image install.
Fifteen opcodes, the frame advance, the character record. Test harness: drive a character through every one of the 114 sequences with no world around him and check he ends where the tables say. This is the piece that must be exactly right, and it can be exactly right before anything else exists.
The five-section block decomposition, the neighbour dependencies, the foreground plane, screen transitions. At the end of this phase the game draws any screen of any level correctly and the Kid can be placed on it, standing still.
CTRL and CTRLSUBS, 2,700 lines between them: what a key means given the pose, the
floor test, the ledge grab with its lead time, the landing choice by fall distance, the bump, the stoop, the
drink. Running, jumping and climbing all work at the end of this phase. Falling to your death works too.
MOVER and SUBS: spikes, slicers, loose floors, pressure plates and the gates they
drive, rubble, the exit, the mirror, the potions, the level timer and the level transitions.
AUTO and the fighting half of COLL: the guard behaviour programs, the strike,
block and advance sequences, the strength meters, and the grace periods and patience timers that make guards
feel like opponents rather than obstacles.
Twenty effects by ear, the message boxes, the hourglass and torches, the sixty-minute clock, and the recorded demo replayed as both attract mode and regression test.
Roughly four weeks of evenings, against Thrust’s seven phases and Exile’s larger plan. The estimate is honest about volume and optimistic about nothing except the renderer, which genuinely is easy here.
| Risk | Assessment | Mitigation |
|---|---|---|
| Rights | High The one risk that cannot be engineered away | Settle the distribution question before Phase 1, not after Phase 7 |
| Tokenised program size | Medium Twelve thousand lines of source logic against a 144 KB cap | Every table external from day one; the library CSUB route held in reserve; measure at the end of each phase rather than at the end of the project |
| The control state machine | Medium 2,700 lines where a wrong threshold is invisible until it is unplayable | The recorded demo as a regression test: it diverges visibly the moment a threshold is wrong |
| Sound authenticity | Medium Speaker-click routines re-created by ear | Accept it. Thrust and Arkanoid both ended here and both sound right |
| Music | Medium Not in the repository at all | Either drop it, or recover it from a disk image as a separate exercise. Do not let it block the port |
| Renderer cost | Low 150 blits into an 83 ms budget | Phase 0 measures it in half a day |
| Artwork storage | Low 571 KB against 720 KB of PSRAM slots | Already fits with both facings pre-mirrored and nothing swapped |
| Data format guesswork | Low | There is none. This is the actual source, and the formats are documented in it |
BLIT FLASH and a transparent colour. Do not use sprite buffers.KEYDOWN with the original’s key layout and a
modern alternative; gamepad optional and later.jmechner/Prince-of-Persia-Apple-II: 582 files, of which the 29
assembler sources, the sixteen image tables and the fifteen level files under 01 POP Source are
the game proper. Everything measured in this document was measured from those.configuration.h for the slot and memory
limits, graphics/Blit.c and graphics/Sprite.c for what the drawing primitives
actually take.