Three facts to keep apart

  • “Five VMs” is an orientation, not a census. Five bytecode-and-state drivers share the actor model. The 0x2F extension, the 0x4C sub-dispatcher and the battle-action, world-map and tile-board state machines are separate dispatchers with their own tables.
  • “Ported” is not “live”. Port status and reachability are separate columns. A faithful, tested module that nothing outside its crate calls is inert.
  • Whether the disc uses an op is a third fact. A ported, live handler can still have no carrier - no shipped scene issuing that opcode. For the field VM a disc-wide opcode census answers it per arm, and its zeros separate “no fixture drives this” from “there is nothing to drive”.

How the dispatchers nest

Per-actor anim dispatch FUN_80021DF4, 7 dispatch bytes Move VM FUN_80023070, 71 opcodes 0x2F extension FUN_801D362C, 61 sub-opcodes 0x2F Field / event VM FUN_801DE840, 43 opcodes MENU_CTRL 16 outer nibbles Tile-board walk SM 15 states 0x4C 0x49 Battle-action SM FUN_801E295C, 256-slot table Cast-module phase byte ctx+0x279, one per image every frame until done stand-alone dispatchers Motion VM FUN_8003774C, pursue / patrol Scripted motion VM FUN_80038158, 32 slots Actor VM (window widgets) FUN_801D6628, 13 opcodes
Op and state counts are read off each dispatcher’s bound in the disassembly - the compare immediate ahead of the indirect jump - not off the port. “Five VMs” is an orientation: several boxes here are separate dispatchers with their own tables.

The inventory

SubsystemDriverOp / state spacePort
Actor / sprite VMFUN_801D662813 opcodes, table 0x801CED70engine-vm root; live in the shop widget choreography
Move VMFUN_8002307071 opcodes 0x00..0x46, table 0x80010778move_vm
Move-VM extensionFUN_801D362C61 sub-opcodes, table 0x801CE868move_vm::ext
Motion VM, pursue / patrolFUN_8003774C22-slot table 0x80010EE0motion_vm
Motion VM, scriptedFUN_8003815832-slot table 0x80010FE8, ops 0x01..=0x20ambient_motion
Field / event VMFUN_801DE84043 opcodes 0x21..0x4F with gapsfield
Field VM MENU_CTRLinline in the field VM16 outer nibbles; two reach the error printerfield::step::menu_ctrl
Effect VMFUN_801E0088none - see beloweffect_vm
Battle-action SMFUN_801E295C256-slot table 0x801CED44, sparse bands, no default armbattle_action
World-map entity SMFUN_801DA51C5 statesworld_map
Tile-board walk SMFUN_801EF2B015 states, table 0x801CF65Cengine-minigames::tile_board
Cast-module phase machineone tick per PROT 0903..0966 imageper image: the ctx+0x279 byte through a word table or a compare chaincast_module_ticks
Per-actor anim dispatchFUN_80021DF47 dispatch bytes at actor[+0x5A]anim_vm
Title-screen tickFUN_801DD35C25-slot table 0x801CF244title_overlay
Per-prim render dispatchFUN_8004339020 kind slots × 4 alpha banksprim_dispatch
Status-effect tickerFUN_801E752Cper-actor condition setstatus_effects

Every row has a live caller in the port. The battle-action state machine is the one row whose decode is graded partial: its table is sparse and not every band is mapped.

The cast band is 64 dispatchers and one row

Each of the 64 slot-B images carries its own phase machine over the same context byte, so the “cast-module phase machine” is not one interpreter - it is a family that shares an ABI. The battle-action state machine re-enters the paged module’s tick every frame and advances only on a zero return. See cast modules.

The effect VM has no opcode space

A low opcode count is not evidence of an incomplete decode, and here the count is zero by construction. FUN_801E0088 has no central switch on a per-slot opcode byte: the bytes that look like state tokens are 5.3 fixed-point wait counters, and the walker is a pair of countdown-driven cursor walks. Searching for its opcode table is a documented dead end.

Ported but inert

“Inert” is a reachability statement, not a correctness one. Two support modules stay inert today: the title sprite-emit primitives, and a standalone walker over the 0x2F dispatcher. The second is owed no host - a disc-wide reference scan finds exactly one caller of that dispatcher, and the port hosts that caller.

See also

The per-row decode status, the two-ports-of-one-function note on the title tick, the inert list and the count of dispatcher copies are in docs/subsystems/vm-inventory.md.