⚠️ Clean-Room / Educational

This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.

Time-Lapse Operation: Polling Loop Dissection and Time Log Monitor

Date:2025-12-25 StepID:0276 State: draft

Summary

This Step implements the "Time-Lapse Operation" to dissect the active polling loop in which Pokémon Red is trapped (PC: 614D - 6151). Analysis of Step 0275 revealed that the game is not in HALT, but it is pulling (constantly checking) a condition. The hypothesis is that the game is waiting for a hardware register (like LY, DIV or the 0xD732 flag) to change, but if our Timer or PPU are not progressing correctly, the game is stuck in time.

Added critical point instrumentation: Trapped Loop Sniper Trace (614D-6155) for capture exactly what opcodes it executes and what values it reads from memory (LY, DIV, STAT, D732). The goal is to identify which register is being polled and if the time is "frozen" for the CPU, causing the wait loop to become infinite.

Hardware Concept

On the Game Boy, there are two main ways the software and hardware synchronize:interruptionsandpolling. While interruptions are the method preferred (hardware notifies software when an event occurs), polling is an alternative that some games use to actively check the hardware status.

1. Polling vs Interruptions

Interruptions:The hardware notifies the CPU when an event occurs (such as V-Blank). The CPU may be executing other tasks and will be automatically interrupted when the event occurs. This is efficient because the CPU does not need to constantly check the hardware status.

Polling:Software actively checks hardware status by reading logs repeatedly until the value changes. This consumes CPU cycles but may be necessary when:

  • Interrupts are disabled (IE=0 or IME=0)
  • The game needs precise timing with a specific event
  • The game is in a critical routine where it cannot allow interruptions

2. Pollable Hardware Registers

Games can read various hardware registers for synchronization:

  • LY (0xFF44):Current scan line (0-153). Automatically increased by PPU every 456 T-Cycles. Games can read this register to wait for the PPU to complete a frame or reach a specific line.
  • DIV (0xFF04):Timer division register. It automatically increases every 256 T-Cycles (Timer base frequency). Games can read this log to implement time delays or waiting for a specific interval to pass.
  • STAT (0xFF41):PPU status (current mode, match flags). The games They can read this register to wait for the PPU to enter a specific mode (such as V-Blank).
  • Custom flags (ex: 0xD732):Some games use flags in WRAM/HRAM to communication between routines. An ISR can modify these flags, and the main code can check them.

3. The Danger of the "Phantom Timer"

If a game is pulling a hardware register (like DIV or LY) expecting it to change, but the emulator is not updating that record correctly, the game gets stuck in an infinite loop. This is especially dangerous when:

  1. The Timer does not advance:If the Timer is not being updated with the T-Cycles consumed by the CPU, the DIV register remains static. The game reads DIV repeatedly waiting for it to change, but he never does.
  2. The PPU does not advance:If the PPU is not being updated after each instruction, the LY register remains static. The game reads LY repeatedly waiting for it to change, but it never does.
  3. The loop is very tight:If the polling loop consumes all CPU cycles without Allow time for the Timer or PPU to advance, time is "frozen" from the game's perspective.

This is the concept of the "Phantom Timer": the hardware should be advancing, but from the perspective of the software, time is frozen.

4. Synchronization in run_scanline()

The functionrun_scanline()is critical to avoiding the "Phantom Timer". This function executes CPU instructions to accumulate 456 T-Cycles (a complete scanline), butafter each instructionupdates the PPU and the Timer with the cycles consumed. This ensures that:

  • PPU advances correctly, updating LY and PPU modes
  • The Timer advances correctly, updating DIV and TIMA
  • Even if the CPU is in a tight polling loop, the hardware still advances

If this timing is not working correctly, the game can get stuck in polling loops. infinitely waiting for the hardware to do something that never happens.

Implementation

Instrumentation was implemented to dissect the polling loop and understand which record is being verified:

Polling Loop Sniper Trace (CPU.cpp)

An instrumentation block was added to the end ofCPU::step()which captures the state of the CPU when the PC is in the range 0x614A-0x6155 (the polling loop trapped). The trace captures:

  • PC and Opcode:The current address and opcode being executed
  • CPU registers:A, BC, HL (the most relevant registers for the loop)
  • Hardware Logs:LY (0xFF44), DIV (0xFF04), STAT (0xFF41), and the 0xD732 flag

The trace is limited to 40 steps (about 10 turns of the loop) so as not to saturate the log. This allows us to see:

  • If LY, DIV or STAT are changing (confirming that the hardware is advancing)
  • If flag 0xD732 is changing (confirming that an ISR modifies it)
  • What opcodes are executed in the loop (to disassemble the exit condition)
  • The pattern of values ​​that the game is reading in each iteration

Code location:The trace is executed at the end ofstep(), after the instruction has been completely executed. This ensures that we capture the true state of the records after each statement in the loop.

Affected Files

  • src/core/cpp/CPU.cpp- Added Sniper Trace polling loop (614D-6155) to the end ofstep()with capture of hardware registers (LY, DIV, STAT, D732) and CPU registers (A, BC, HL)

Tests and Verification

The verification was carried out by running Pokémon Red and analyzing the generated logs:

  • Command executed: python main.py roms/pkmn.gb
  • Result:40 traces were captured[SNIPER-LOOP]successfully
  • Log analysis:The traces revealed critical information about the loop

Analysis Results

Loop Opcodes (Disassembly):

  • 614A:11- LD DE, nn (load a value into DE)
  • 614D:00- NOPE
  • 614E:00- NOPE
  • 614F:00- NOPE
  • 6150:1B- DEC DE (decreases DE)
  • 6151:7A- LD A, D (load D in A)
  • 6152:B3- OR E (A = A | E)
  • 6153:20- JR NZ, e (relative jump if Z=0)

Hardware logs status:

  • LY:20 (constant, does not change) - ⚠️ Possible PPU synchronization problem
  • DIV:15 → 16 (yes it changes) - ✅ Timer works correctly
  • STAT:03 → 00 (changes) - ✅ PPU is updating STAT
  • D732:00 (constant) - Does not change during the loop

Loop Interpretation

The loop in614D-6153 NOT pulling hardware. It is a delay loop based in the FROM record:

  1. Decreases DE (DEC DEin 6150)
  2. Load D at A (LD A, Din 6151)
  3. Does OR E (OR Ein 6152) - If D=0 and E=0, Z=1
  4. Jump if Z=0 (JR NZin 6153) - If Z=1, do not jump and exit the loop

Conclusion:The loop waits for DE to reach 0. It is not waiting for any registers hardware change. The Timer works correctly (DIV advances), but LY is static at 20, suggesting a possible PPU synchronization problem.

Compiled C++ module validation:✅ Successful compilation. The logs[SNIPER-LOOP]They appear correctly when the PC enters the range 614A-6155.

Sources consulted

Educational Integrity

What I Understand Now

  • Polling vs Interruptions:Games can use polling when interruptions are disabled or when they need precise synchronization. Polling consumes CPU cycles but it is necessary in certain contexts.
  • Phantom Timer:If the Timer or PPU is not being updated correctly, hardware registers (DIV, LY) remain static, causing polling loops to run. become infinite.
  • Cycle-to-cycle synchronization:The functionrun_scanline()must update the PPU and Timer after each instruction to ensure that the hardware advances even when the CPU is in tight loops.

Confirmed Findings

  • Loop exit condition:✅ Confirmed. The loop is NOT pulling hardware. It is a DE based delay loop. The loop ends when DE reaches 0 (when D=0 and E=0, OR E gives Z=1 and JR NZ does not jump).
  • Timer Status:✅ Confirmed. The Timer is working correctly. DIV changes from 15 to 16 during the loop, confirming that the Timer synchronization works.
  • PPU Status:⚠️ Problem identified. LY is static at 20 throughout the loop. This suggests that the PPU is not progressing correctly or that the game is in a moment. where LY should not change. STAT does change (from 03 to 00), indicating that the PPU is processing something, but LY remains constant.

Revised Hypothesis

Original hypothesis (REFUTED):The game is pulling a hardware register waiting for it to change. ❌ This hypothesis was refuted: the loop is NOT pulling hardware, it is a loop delay based on DE.

New hypothesis:The loop is a simple delay that should end when DE reaches 0. If the game is still stuck after DE reaches 0, the problem is somewhere else (possibly in the logic that follows after the loop, or in a PPU synchronization problem that causes LY is static).

Next Steps

  • [✅] Run Pokémon Red and analyze the traces[SNIPER-LOOP]- FILLED
  • [✅] Check if LY, DIV or STAT are changing - COMPLETED (DIV and STAT change, LY static)
  • [✅] Disassemble captured opcodes - COMPLETED (DE based delay loop)
  • [ ] Investigate why LY is static at 20 during the loop (possible PPU synchronization issue)
  • [ ] Check if the loop ends when DE reaches 0 (the game should continue)
  • [ ] If the game is still stuck after DE reaches 0, investigate what happens after the loop
  • [ ] If LY is static when it should change, correct PPU timing inrun_scanline()