Puyo Puyo Tsu/Gamepad Input
This page discusses gamepad input routines as implemented by Puyo Puyo Tsu. It covers the way the game actually reads gamepad inputs, but does not cover how other routines use the gamepad statuses to trigger events, which differs for every routine.
Interrupts vs. Polling
On recent hardware, input events are handled in near real-time, thanks to the interrupt mechanism: the keyboard or mouse sends a signal on a line directly connected to the CPU which immediately stops its execution to switch to a special routine (an interrupt handler routine) which will in turn read the device's status. It can be a true physical line on the PCB representing a single bit of data, which signals an active interrupt when set to 1.
Once the CPU is done with the interrupt handler routine, it restores its previous state (registers) and resumes execution where it was left off right before the interrupt occurred.
On the Genesis, and most hardware from that era, there was not enough physical lines available to wire hardware interrupts from the gamepads; all the lines were affected to other processors such as sound and video chips. Hence the need to poll the gamepads periodically to see if anything happened there.
The interrupt mechanism means that such an interrupt can occur during the execution of almost ANY game routine, which will be interrupted abruptly to handle the event. This allows for asynchronous programming: the main game logic can loop, with a single iteration taking enough time for an interrupt to occur. This loop will be made aware of an updated status (due to the interrupt) at the beginning of the next iteration, while the update of such status is done in real time.
Overall readout process
The Genesis has a few interrupts available though, mostly related to video output: one is triggered when the VDP has reached the end of a display line (HINT), and a another is triggered when a frame has been fully drawn (VINT).
The games strategically exploit those interrupts to periodically call routines from their handlers. Puyo Puyo Tsu is no exception and calls its gamepad polling routine from the VINT handler. Thus, readout is done once per gamepad for every frame that gets drawn on the screen.
The graph below summarizes the callgraph from the VINT interrupt handler:
From VINT, two successive functions are called from one another: read_active_pad_keys() and read_gamepad(). These routines touch various memory locations shown on the graph; those will be described below. The main purpose of the polling routines is to maintain a frame-by-frame depiction of the gamepad status, so that the main game logic can access that status afterward to check whether the player wants to rotate the current piece, soft drop it, or pause the game.
Gamepad readout routine
The read_gamepad() routine's purpose is to... read the status of a single gamepad.
Gamepad readout shall be done atomically: in order not to send erroneous commands or run into race conditions (which can sometimes damage hardware), the programs must ensure only coherent sequences of commands are sent to the gamepads. Indeed, if interrupt handling takes a long time, another such interrupt could theoretically occur before the first handler finished its execution. While this results in a console lockup, if inconsistent signals are sent to the gamepad pins, they could be damaged.
The routine cleverly uses the Z80 as a semaphore to ensure atomic access to the gamepad: it requests access to the Z80 sound processor, waits for confirmation, then reads the gamepad status, and releases the Z80. If the same routine tries to read the gamepad at the same time, it will have to wait for the Z80 to be available, which ensures only a single gamepad readout occurs concurrently at any given time.
The gamepad pins are mapped in memory at addresses 0xA10003 and 0xA10005 (the gamepad mapping is stored in register a3). However, there are not enough pins to have all buttons available at once, so readout is performed in two steps:
- the Start (S), A, Down arrow (D) and Up arrow (U) buttons, with the latter two being ignored
- the C, B, Right arrow (R), Left arrow (L), D, U
Note: a bit value of 1 means the button is currently released, while 0 means it is pushed down.
The game combines those two readouts in a byte-long bitfield in D0, with the bits assigned to the buttons in that order: SACBRLDU. The bit values are also negated, so that their values feel much more intuitive.
A guard check is done to ensure both U and D are not active at the same time, and if that's the case, they are reset to the released state.
The game also calculates which buttons were newly pressed during the current frame, by comparing the current and previous gamepad states.
These states are both stored in memory (address in a2) :
- 0xFFF070 / 0(a2) : pad 1 currently pressed buttons (1 byte)
- 0xFFF071 / 1(a2) : pad 1 newly pressed buttons (1 byte)
- 0xFFF076 / 0(a2) : pad 2 currently pressed buttons (1 byte)
- 0xFFF077 / 1(a2) : pad 2 newly pressed buttons (1 byte)
Note: the routine ignores release events. Releasing a button is not detected and does not trigger specific events.
read_active_pad_key() reads both gamepads and calculates various states to make them available for the rest of the game logic.
The routine first reads both gamepads, then will compute the currently active buttons so that subsequent game logic will know which button presses it should consider. This mechanism actually handles button input repeat: this will mask pressed buttons for a pre-defined frame count so that they do not trigger their respective event.
The game does two distinct checks:
- the status of R/L arrows
- the status of S, A, C, B buttons and U, D arrows
For each check, the following may happen:
- if the buttons were newly pressed at the current frame, they are marked active, and their respective repeat frame counter is reset to its highest value ;
- if the buttons are not newly pressed, they are forcefully marked inactive, and their respective frame counter is decremented by one. If that counter hits zero while the buttons are in this state, it is reset to a smaller value than in the previous case.
Note: released buttons as well as held buttons fall in the latter case. Also, the routine is called once per frame (at each VINT); consequently, the counter is actually decremented once per frame.
Think of it this way: when typing text on your computer, if you hold a key, the corresponding character will first appear, then after a long delay, it will be repeated multiple times at much shorter intervals.
This routine actually implements the very same mechanism: if you hold down a button, it is first active for a single frame, then inactive for the duration of the longest counter, then active again for a single frame, and inactive for the duration of the shorter counter before being active again and so on.
Values of those counters are described in the page about Frame Data Tables.
Puyo Puyo Tsu's input handling scheme implies the following consequences:
- the player input only matters once per frame, about 60 times a second on NTSC systems (50 times a second for PAL systems);
- any input is active for a single frame, then inactive for the duration of the very next frame;
- it is thus impossible to have an input active for two subsequent frames, and this cannot be overcome except for a single special case (discussed below);
- a player could theoretically go faster than the repeat mechanism (mainly by skipping the initial delay) by repeatedly inputing the desired button. To beat the repeat mechanism during a battle, one has to input commands faster than the 8 frame initial delay and 2 frame subsequent delay.
Since the read_active_pad_keys() routine watches if any of the SACBUL buttons have been newly pressed, pressing any one of those will make all of the currently pressed buttons active and will reset the counter (left branch in the code). It is thus theoretically possible to input two rotations in two consecutive frames, by pressing any other button on the gamepad besides the R/L arrows. One could use the Up arrow, but this prevents the player to use the down arrow to speed up drop speed. On Genesis, it is possible to have a rotation input active in two consecutive frames for one direction only: the gamepad has two buttons mapped for the same rotation direction.
Note: while theoretically possible, this behavior is not confirmed as the routines handling pair rotation or placement may mask inputs for an arbitrary long time. Still, precise input timing, and skipping such delays/animations may lead to this behavior to be exploitable.