Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scene Display Objects

Scope and terminology

The scene runtime maintains a 100-entry display list which connects BIN commands to animation, command threads, ART frames, and the renderer. Some records visually represent characters, enemies, or props, but the structure itself is a display list. Calling every entry a gameplay entity would be misleading: the same list also contains animation-group and thread records.

The recovered model is based on the bytecode handlers, the scene update and render paths, and a live QEMU capture of LOGO.BIN.

Runtime layout

The list count is a word at DS:00E2. Records begin at DS:A2AC, have a ten-byte stride, and are addressed as A2AC + index * 10. The update routine at load offset 0x3AFF rejects counts greater than 100, establishing the capacity.

For directly rendered record types 0x03 and 0x43, the layout is:

OffsetSizeMeaning
+02Signed X coordinate.
+22Signed Y coordinate.
+428.8 scale; 0x0100 is native size.
+61Loaded ART slot. Bit 7 is also the hidden marker.
+71One-based ART frame number; zero suppresses drawing.
+81Render flags; bits 0 and 1 flip the two axes.
+91Display-record type.

This assignment follows the arguments passed to the renderer at 0xBCAC. That routine selects a loaded ART resource with the low seven bits of byte +6, subtracts one from the frame number, applies coordinates and scale, and derives the two flip bits from byte +8. An ART-slot byte in 0x80..0xEF, or frame zero, releases/suppresses the render slot instead of drawing it.

Visibility is therefore encoded in the ART-slot byte rather than the separate render-flags byte. The slot byte should not be modeled as a plain array index without masking or checking its high bit.

Record types

Four type values are written by the recovered scene handlers:

TypeProducerRole
0x02Opcode 0x02Connects one of the 16-byte scene-command thread slots to the display/update list.
0x03Opcode 0x03Direct ART-frame object with an implicit native scale of 0x0100.
0x43Opcodes 0x04 and 0x43Direct ART-frame object with an explicit scale.
0x06Opcode 0x06Connects an animation sequence to the display/update list.

The update routine handles type 0x02 through the scene-thread path. Types 0x03 and 0x43 are submitted directly to the object renderer. Type 0x06 is owned by the animation-sequence path. The ten-byte fields not used by a type can contain zero or stale data and must not be interpreted as a direct object’s coordinates.

Type 0x02 records also participate in the primary pointer-target table even when no opcode 0x10 selector label was attached. The main input path around 0x87D30x8842 gives scene-thread targets priority over opcode-0x3A actions. Its selected-target dispatcher at 0x851E distinguishes a scene-thread target from an ordinary script action and writes the selected thread/node index into the movement controller rather than jumping to a BIN offset.

This matters in GANTRY.BIN. Navigation nodes 2 and 3 share geometry (190,70) and have no selector strings. Entry seg finishes on node 2, whereas arrival at node 3 enters CP1. The current unlabeled node is omitted from hit-testing, so table order selects node 3 when the player clicks the opening. Requiring a nonempty selector makes this scene appear locked even though its navigation graph and callback are complete. The missing label also means DOS displays no transient action arrow for this target.

The selected-target index is also what the main input path dispatches after Enter and what the Space-cycle routine uses as its starting record. These paths do not re-filter the target by selector-string presence. Consequently the unlabeled GANTRY opening is keyboard-activatable when the pointer is over it, and Space advances from rather than ignores an unlabeled current target.

Scene commands

The definition and direct frame-control commands are:

OpcodeOperandsEffect
0x02thread, x, y, scaleInitializes a 16-byte command-thread slot and appends a type-0x02 display record.
0x03frame, art, x, y, flagsAppends a native-scale type-0x03 display object; unused by shipped scripts.
0x04 / 0x43frame, art, x, y, scale, flagsAppends a scaled type-0x43 display object.
0x06delayBegins an animation sequence and appends a type-0x06 display record.
0x07nine-byte recordSupplies an animation step retained within the BIN stream.
0x65first, countSets frame byte +7 to zero for a consecutive display-record range; unused by shipped scripts.
0x66first, count, minimum, maximumIncrements each selected frame and resets values outside the inclusive range to minimum; unused by shipped scripts.
0x85indexSets ART-slot bit 7, hiding a direct display object.
0x86indexClears ART-slot bit 7, showing a direct display object.

The animation step is not copied when opcode 0x07 executes. The handler advances over its nine bytes; the animation state created by opcode 0x06 retains a BIN-stream location and consumes the records later. This explains why the command decoder originally could establish the nine-byte boundary before the animation lifecycle was understood.

Six consecutive 0x07 records at CP1.BIN:0x0026..0x0062 precede the first 0x06 in that resource. The DOS handler accepts them because it only advances the BIN cursor; they are not owned by an animation definition. Treating 0x07 as an append operation that requires an already created host animation incorrectly rejects the Unibot boarding scene.

The animation slot and step layouts, lifecycle commands, and linked-transform path are now documented in the combat-runtime chapter.

Victim scenes use 0x85 and 0x86 on groups of display indices when their visual state changes. For example, NAGE.BIN hides a set of crystal-related objects in a subroutine selected by progression flags. This is evidence for display visibility, not by itself for entity death or activation state.

Lifecycle

initialize_scene calls the reset routine at 0x3AD2 before loading and executing the next BIN resource. The reset routine visits every current display index, releases its render slot, and sets the count to zero.

The offset-zero loader invocation ends at opcode 0x05 before the requested entry traversal is started. That invocation remains stopped while the actor moves between the two nodes selected by the matching 0x0C record. Only the ordinary departure or arrival callbacks start a fresh command stream afterward. The entry movement request synchronously runs its departure callbacks before initializing the selected edge; they must not share the terminating loader invocation. Keeping the loader stream runnable at the byte after 0x05 makes an arrival handler execute once as fallthrough and a second time on actual arrival. Starting a departure callback inside the loader’s 0x05 handler instead lets that return deactivate the new invocation. CP1.BIN exposes the first error as a duplicated Unibot introduction, while FIRST.BIN exposes the second as a missing beamer materialization.

During scene updates, 0x3AFF walks the current list:

  • type 0x02 invokes the associated thread/update slot;
  • types 0x03 and 0x43 pass the recovered ten-byte fields to 0xBCAC;
  • other supported types are serviced by their dedicated animation paths.

The main frame update calls those controller paths separately, but call order is not layer order. Each path writes into the render slot reserved by its mixed display-record index. Direct rendering passes index + 1 at 0x3B720x3B76; animation rendering passes its saved display index plus one at 0x3D640x3D6B; scene-thread movement does the same at 0x763D0x7642. The dirty-region compositor at 0xC000 then advances through the 26-byte render slots in increasing order.

This distinction supplies the opening oval mask. In LOGO.BIN, the moving RUN.ART controller is display record 4, while the direct dome and bridge pieces are records 7 through 9. The later scenery covers the character outside the oval as it enters and leaves. It is display-list occlusion rather than a separate geometric clipping primitive. Regrouping records as direct, animation, and movement passes incorrectly forces the character above the mask.

The list is runtime scene state and is reconstructed from the BIN program. It is not serialized in the save file.

QEMU validation

The game was started with the repository’s visible and silent trace mode:

./run.sh --trace-dos

At the Bridgestone logo screen, QEMU reported DS=14E1. A one-megabyte physical-memory dump therefore placed the count at physical 0x14EF2 and the record table at physical 0x1F0BC. The live count was 13.

The three directly rendered records were:

IndexXYScaleFlagsFrameART slotType
730300x01001410x43
8300x01000410x43
9300x01000110x43

These values exactly match the three 0x43 commands at BIN offsets 0x0122, 0x012C, and 0x0136. Records 0–3 and 10 are the five animation sequences begun by opcode 0x06; records 4–6 and 11–12 are the five scene threads created by opcode 0x02. Thus all 13 definitions in the linear startup path match the live type order and count.

The dump and screen capture are retained as ignored analysis artifacts under build/qemu-trace/.

Inspector

Show the normal command listing followed by its linear display definitions:

tools/inspect_bin.py build/dd1/all/001_LOGO.BIN --objects

The summary includes source offset, type, and all definition fields known for that type. It follows linear file order; branches and calls can skip or repeat definitions at runtime, so indices outside a straight startup sequence are not guaranteed live indices.

Primary movement controller

The recursive search at 0x6DA5 and wrapper at 0x6EBD establish the route algorithm. The wrapper tries recursion limits zero through 19. At the limit, the recursive routine looks for one final direct edge, so the accepted route lengths are one through 20 edges. Every level scans the opcode-0x0B table in insertion order, testing a record’s stored first endpoint before its second. It has no visited-node set. The wrapper returns only the immediate next node and edge direction; updater 0x7469 calls it again at every intermediate node rather than retaining a complete route.

Request routine 0x779D keeps the desired destination in controller byte DS:7A45. While the interpolation count at DS:7A3A is nonnegative, another request writes only that byte. It does not restart the edge or disturb its coordinates, timing, animation phase, turn pose, or callbacks. Edge completion enters state 5, runs directional and node arrival callbacks, and performs the next search on a later controller update. This is why rapid pointer or keyboard navigation redirects Captain only after he reaches the next graph node. The byte is initialized to FFh, is not reset by opcode 0x53, and remains resident across scene replacement. Final idle resolution changes it to the current node.

Initial movement calls the source-node departure and then the directional edge departure before initializer 0x6F46. State-five continuation first initializes the selected edge, then calls its directional departure and the source-node departure. Completion makes the interpolation count negative, then calls the directional edge arrival and destination-node arrival. Handler 0x7A5C runs every matching callback synchronously as a fresh invocation through its first yield or return before scanning the next record. Arrival callbacks can therefore start and then retarget a new edge in one controller pass. The caller’s BIN cursor is saved separately, so a negative delay left by a same-slot nested callback does not prevent the outer command stream from continuing. If the outer stream later yields, its cursor replaces the nested cursor and its delay change accumulates with the callback’s.

Edge initializer 0x6F46 never resets the phase accumulator at DS:7A2C. Updater 0x7469 advances it modulo 0x0600 across intermediate edges and writes 0x0200 only in idle state 2. The initializer’s orientation table contains offsets 0, 0, 24, and 12; the four phase increments are all 28. When horizontal direction changes but that offset remains zero, a reflection change selects RUN.ART frame 20. A direct offset-12/offset-24 reversal, toward versus away, selects frame 19. Both set the transition countdown to 28 << 3, or 224 controller units. Horizontal/depth changes have no special turn pose. The raw table bytes were checked in the unpacked executable at file offsets 0xEF34 and 0xF4CA as well as through the instructions at 0x71BB..0x721E.

Opcode 0x53 at 0x5440 writes both current-node bytes, sets the previous edge index to -1, and calls 0x6F46 with identical start and end nodes. It therefore snaps to the requested geometry but remains in state 1 for a minimum same-node traversal. Its completion cannot match a directional edge callback because the edge index is -1, but it still dispatches the node-arrival callback. A following opcode 0x54 at 0x5468 merely queues its destination until this traversal finishes. An unreachable ordinary request takes the same minimum-traversal path before returning to idle. Opcode 0x53 does not replace the retained desired-node byte.

Controller timer rate

The hardware initialization at load offset 0x3600 installs the interrupt-8 handler at 0xAAE4. It programs PIT channel zero in mode 3 with divisor 0x26D7 (9,943), producing approximately 120 interrupts per second. Each interrupt increments the counter at 0x4B66. The elapsed-delta helper at 0x00B6 multiplies that counter by 24 and caps the result at 400 before the frame controller at 0x7A8D distributes it to animations, scene threads, and movement. The resulting controller rate is approximately 2,880 units per second, not one unit per millisecond.

The ordinary controller at 0x7ACB reads and resets that elapsed delta, calls the animation updater at 0x3DA8, and calls the dirty compositor at 0xC5CA before returning. The blocking input poll repeatedly invokes this controller, so under normal load each nonzero update contains one interrupt: 24 units, or about 8.33 ms, followed by one presentation.

The animation updater adds the complete controller delta to a sequence’s countdown. If more than one record became due, it catches up through those records and submits only the final resolved display slot. DOS can therefore skip an intermediate frame after a genuine stall, but its normal 24-unit cadence does not skip the explicit 30- and 40-unit sequences in COMBAT4.BIN. This distinguishes normal visual cadence from the controller’s intentional long-stall catch-up behavior.

Remaining questions

  • The opcode-0x02 16-byte interactive/display record still needs names for every field and a clearer distinction from the true BIN scheduler table.
  • Combat targeting uses a separate ten-byte screen-action table. No evidence yet connects direct display objects to collision or enemy health.
  • The last two bytes of each 12-byte animation slot and several detailed mode transitions remain unnamed.
  • A display record can visually represent a character, but no identity or gameplay-stat field exists in this ten-byte structure.

Relevant executable functions

Load offsetCurrent name
0x3AD2reset_scene_display_records
0x3AFFrender_scene_display_records
0x451Bexecute_bin_commands
0x6631initialize_scene
0xB948release_render_slot
0xBCACrender_scene_display_object

Offsets use the unpacked load-module convention.