Two machines can show the same picture and hold different state. A comparison only means something when you take it at the same, repeatable instant. We call that instant a boundary. This post defines the term, shows how to measure it, and explains the failures we hit when we measured it badly.
1. A boundary is a repeatable instant
A boundary is an instruction address that the program reaches once per update. At that address, memory is consistent: records are written, the object pool is compacted, and the random numbers have advanced. The emulator and your model stop at the same place, dump the same ranges, and compare.
A boundary is not a time. A wall-clock delay gives no guarantee about program state. A screen capture gives no guarantee about the frame. Use an address, and prove that the program reached it.
2. Four events that people call a boundary
These events are different. Do not use one name for all four.
- Outer frame entry. The top of the main loop. No work is in progress. In our Conqueror work this is
$57F28. - Simulation entry. The start of one game update, after input read and before tank and projectile work. Example:
$57FE6and$58010. - Hardware frame. The 50 Hz vertical blank that drives presentation. It controls the display phase, the Copper list, and audio service.
- Debugger stop. The moment your tool interrupts the CPU. This is not a boundary. It is an event that can change the boundary you wanted to observe.
Write the type of boundary in every report. A claim that uses an unstated boundary type is not testable.
3. Why we sample at outer frame entry
The outer frame entry gives one sample per update, and the program has finished its work. Records, groups, candidate tables, the 7,200-byte particle pool, camera words and both random-number words are all settled. That gives a clean comparison point.
A second reason is symmetry. The original program reaches the address, and our browser model can run its tick and then stop. Both sides can produce the same list of values, so a mismatch points at logic, not at scheduling.
4. Not every frame advances the simulation
A boundary index is not an update index. In Run966 we recorded the scheduler cursor and the countdown at every outer boundary. The cursor advanced from 10 to 18 and the countdown held at 20. Boundaries 2, 3 and 5 carried no simulation update at all.
If you count frames, you compare different states and invent a bug. Keep two numbers: the outer boundary and the simulation update. Our fixtures now store a list of no-update boundaries, and the browser replay subtracts them. A capture of 27 boundaries with four no-update entries means 23 updates.
5. Stopping at the boundary can change the boundary
A debugger stop is expensive on this machine. Runs 278 and 280 stopped at every boundary. The scheduler cursor then stopped advancing, and both runs were rejected as cadence evidence. Runs 509 to 513 recorded no advances for the same reason.
The same effect appears in presentation. In Run1006 the debugger stopped at boundaries 24, 25 and 26. Those three frames showed renderer-phase differences that the original program does not produce during normal play.
The working pattern is a small number of stops: entry, held and release. A memory ring in the guest writes the remaining boundaries. Runs 281 and later use that pattern, and the cadence then survives.
6. Boundary state includes phase
Two runs can hold identical gameplay state and still differ in pixels. The display phase selects which Copper list and which bitplane buffer is front. The renderer phase words $24ED6 and $24ED8 tell you which pass the program will draw next.
We lost a day to this. A live replay showed a 423-pixel terrain difference. The state matched at every boundary. The cause was a snapshot taken at a different display stage, not a rendering error. The comparator now checks the phase words before it compares pixels.
Compare pixels only when state and phase both match. Otherwise compare state and report the phase difference separately.
7. Integrity checks for every boundary capture
A boundary capture is an experiment. Treat it like one.
- Record the checkpoint hash, the emulator version, and the profile.
- Reload the checkpoint once and confirm the screen and the program counter.
- Record the entry, held and release boundary, and the program counter at each.
- Record which keys were down, and at which boundary each key changed.
- Record the memory ranges and lengths. Dump the same ranges on both sides.
- Compare the first differing byte, and name its owning field, before you read later differences.
One more check earns its cost: verify the inputs to the capture, not only the outputs. A waypoint mismatch at update 139 looked like an AI bug. The comparison harness had loaded an obstacle worklist from a previous run. The two lists had different hashes, 204c0263 and 87d88140. The game was correct; the harness was wrong.
8. What a boundary comparison proves, and what it does not
A matching boundary proves one path, for one recorded input set, up to the ranges you dumped. We call that bounded parity. It does not prove a whole function, a whole battle, or a whole game.
This matters when you publish progress numbers. In this project, 87 of 271 identified functions have bounded parity evidence, and the rough gameplay estimate is about 65 percent. Both numbers describe evidence, not completeness. A function with bounded parity can still hold an unvisited branch.
The useful habit is to state the boundary type, the inputs, the ranges, and the path. Then another person can repeat the run and challenge the claim.
9. Checklist
- Name the boundary type and the address.
- Separate boundary index from update index.
- Stop rarely: entry, held, release.
- Let a memory ring record the rest.
- Check the display and renderer phase before any pixel test.
- Reload the checkpoint and prove the state.
- Verify the capture inputs as well as the outputs.
- State the claim as bounded parity, with its inputs and ranges.
More in this series
This post is part of our Amiga reverse engineering series. Start with Reverse Engineering an Amiga Application with WinUAE and Ghidra for the checkpoint, key-injection and Ghidra workflow. Then read From Amiga Code to Browser: Rebuilding Conqueror’s Opening Screen for the art, audio and font recovery work.
The Conqueror work uses this boundary method to compare original 68000 code with a browser model, a host C model and the same C code compiled for the 68000. Each comparison stops at the same boundary and dumps the same ranges.