Step 0425: Spec-Correct JOYP + Address Wrap (Remove Hacks)
Executive Summary
Definitive correction of the behavior of JOYP (FF00) and address wrapping to 16-bit according toBread Docs, eliminating all the hacks introduced in previous Steps (especially 0419 and 0424). Step 0424 implemented artificial inversion of bits 4-5 in JOYP based on empirical testing observations, but this implementationcontradicts Pan Docswhich specifies that bits 4-5 are read as written.
Critical decision:When a test contradicts Pan Docs,the test is corrected, not the hardware. The goal of this Step is to restore spec-correct behavior by removing:
- ❌ Artificial inversion of bits 4-5 in JOYP (
Joypad.cpp) - ❌Bypass
test_mode_allow_rom_writes(3 locations: MMU.cpp, MMU.hpp, mmu.pyx) - ❌ Fixture
mmu_romwthat allowed writing to ROM (conftest.py)
Result:✅ 8/8 Joypad tests + 11/11 MMU tests pass with spec-correct behavior. ✅ 215/225 total tests (10 pre-existing failures not related to this Step).
Hardware Concept (Pan Docs)
JOYP (FF00) - Registration P1
Fountain:Pan Docs - "Joypad Input" / GBEDG
Registry Structure:
Bit 7-6: Not used (always 1)
Bit 5 (P15): 0 = Selects action buttons (A, B, Select, Start)
Bit 4 (P14): 0 = Selects direction buttons (Right, Left, Up, Down)
Bit 3-0: Button status (0 = pressed, 1 = released) [Read-Only]
Spec-Correct Behavior:
- Writing:Only bits 4-5 are writable. Bits 0-3 are read-only.
- Reading:Bits 4-5 are readjust as they were written(They are NOT inverted).
- Selection:A bit = 0 means "selected". Both rows can be selected simultaneously.
- Low nibble (bits 0-3):
- If no row selected (bits 4-5 = 11): nibble = 0xF
- If row(s) selected: nibble reflects button status (AND of active rows)
Practical Example:
// Select address row
write(FF00, 0x20) // bits 5-4 = 10 (bit 4 = 0 select address)
// State: "Right" button pressed (bit 0 = 0)
read(FF00) // Returns 0xEE = 1110 1110
// bits 7-6 = 11 (always)
// bits 5-4 = 10 (as written, NO inversion)
// bits 3-0 = 1110 (bit 0 = 0 indicates "Right" pressed)
Address Wrap to 16-bit
Fountain:Pan Docs - "Memory Map"
The Game Boy uses 16-bit addresses (0x0000-0xFFFF). Any address outside this range must
wrap automatically usingaddr &= 0xFFFFat the beginning ofMMU::read()andMMU::write().
Example:0x10000 → 0x0000, 0x1C000 → 0xC000
Read-Only ROM (Spec-Correct)
Fountain:Pan Docs - "Memory Bank Controllers"
On the real Game Boy, ROM (0x0000-0x7FFF) isalways read-only. The writings in this range are interpreted as commands to the Memory Bank Controller (MBC),NOlike scriptures direct in memory.
Implication for tests:Tests that require custom ROM must useload_rom_py()with a prepared bytearray, do not write directly to ROM.
Implementation
Modified Files
1. Hardware Core (C++)
src/core/cpp/Joypad.cpp:- Constructor: Initialization to
0xCF(bits 4-5 = 00, spec-correct) read_p1(): Removed inversion of bits 4-5, return(p1_register_ & 0x30)directly- Special case: If no row selected, nibble = 0xF
- Constructor: Initialization to
src/core/cpp/Joypad.hpp: Comments updated to reflect spec-correct behaviorsrc/core/cpp/MMU.cpp:- Line 935: Bypass eliminated
test_mode_allow_rom_writes_ - Line 1068: Removed bypass ROM_ONLY when
rom_data_.empty() - Line 3564: Removed method
set_test_mode_allow_rom_writes()
- Line 935: Bypass eliminated
src/core/cpp/MMU.hpp:- Removed flag
test_mode_allow_rom_writes_ - Deleted declaration of
set_test_mode_allow_rom_writes()
- Removed flag
2. Wrapper Cython
src/core/cython/mmu.pyx:Deleted methodset_test_mode_allow_rom_writes()src/core/cython/mmu.pxd: Deleted declarationset_test_mode_allow_rom_writes()
3. Tests (Python)
tests/conftest.py: Fixture removedmmu_romwtests/test_core_joypad.py:- 8 tests updated with spec-correct values
- Address with bit4=0: Expected
0xEE(before0xDE) - Action with bit5=0: Expected
0xDE(before0xEE) - Rationale: Pan Docs specifies that bits 4-5 are NOT inverted
tests/test_mmu_rom_is_readonly_by_default.py:- 4 updated tests to use
load_rom_py() - Eliminated use of fixture
mmu_romw - load_rom test: Prepare 512 byte ROM, load with
load_rom_py(bytes(custom_rom))
- 4 updated tests to use
tests/test_core_mmu.py:- 1 updated test:
test_mmu_address_wrapping - Change: Validate wrap with WRAM (0xC000) instead of ROM (0x0000)
- Rationale: ROM is read-only, you cannot validate wrap by writing to ROM
- 1 updated test:
Key Code Fragments
JOYP read_p1() Spec-Correct (Joypad.cpp)
uint8_t Joypad::read_p1() const {
// Start with bits 0-3 to 1 (all loose by default)
uint8_t nibble = 0x0F;
// Row selection according to Pan Docs: bit=0 select
bool direction_row_selected = (p1_register_ & 0x10) == 0;
bool action_row_selected = (p1_register_ & 0x20) == 0;
if (direction_row_selected) {
nibble &= direction_keys_;
}
if (action_row_selected) {
nibble &= action_keys_;
}
// Special case: if no row is selected
if (!direction_row_selected && !action_row_selected) {
nibble = 0x0F;
}
// Build spec-correct result (WITHOUT inversion bits 4-5)
uint8_t result = 0xC0 | (p1_register_ & 0x30) | (nibble & 0x0F);
return result;
}
ROM Read-Only Spec-Correct (MMU.cpp)
// In MMU::write() - Switch case for ROM (0x0000-0x7FFF)
case MBCType::ROM_ONLY:
default:
// Step 0425: ROM is ALWAYS read-only (spec-correct according to Pan Docs).
// ROM writes are ignored (or interpreted as MBC).
// DO NOT allow writes even if rom_data_ is empty.
return;
Tests and Verification
Commands Executed
$python3 setup.py build_ext --inplace
BUILD_EXIT=0 ✅
$python3 test_build.py
TEST_BUILD_EXIT=0 ✅
$ pytest -q tests/test_core_joypad.py
8 passed in 0.37s ✅
$ pytest -q tests/test_mmu_rom_is_readonly_by_default.py
4 passed in 0.32s ✅
$ pytest -q tests/test_core_mmu.py
7 passed in 0.28s ✅
$pytest -q
215 passed, 10 failed in 0.53s
(10 pre-existing NON-related bugs: PPU rendering, Registers init, CPU control)
Test Evidence (Key Fragments)
JOYP Address Test (Spec-Correct)
def test_joypad_selection_direction(self):
"""
Verify that writing to P1 selects the address row correctly (spec-correct).
Step 0425: Updated to reflect spec-correct behavior according to Pan Docs.
Bits 4-5 are read AS written (NOT inverted).
"""
joypad = PyJoypad()
# Press Right (address, index 0)
joypad.press_button(0)
# Select address row (bit 4 = 0)
# Write 0x20 = 0b00100000 (bit 5=1, bit 4=0)
joypad.write_p1(0x20)
# Read Q1. It should show Right pressed (bit 0 = 0)
# Expected result spec-correct: 0xEE = 1110 1110
# (bits 7-6=1, bit 5=1, bit 4=0, bit 0=0 pressed, bits 3-1=1 released)
result = joypad.read_p1()
assert result == 0xEE, f"Expected 0xEE (spec-correct), obtained 0x{result:02X}"
ROM Read-Only Test (Spec-Correct)
def test_rom_is_readonly_by_default(self, mmu):
"""
Validate that ROM (0x0000-0x7FFF) is read-only (spec-correct).
Hardware Concept (Pan Docs):
---------------------------
On real Game Boy, ROM (0x0000-0x7FFF) is read-only memory.
Writes in this range are interpreted as commands for the
Memory Bank Controller (MBC), NOT as direct writes.
Step 0425: Removed use of test_mode (hack not spec-correct).
"""
# Attempt to write to ROM (should be interpreted as MBC command, not direct write)
original_value = mmu.read(0x0000)
mmu.write(0x0000, 0x3E) # Try to write 0x3E
# Verify that it was NOT written (should still be the original value)
readback = mmu.read(0x0000)
assert readback == original_value, (
f"ROM must be read-only (spec-correct)."
f"We tried to write 0x3E to 0x0000, but 0x{readback:02X} was read"
)
C++ Compiled Module Validation
✅ All tests run against the native moduleviboy_core.cpython-312-x86_64-linux-gnu.so
✅ Validation of hardware-accurate behavior through direct unit tests
✅ Debug logs confirm spec-correct behavior (bits 4-5 without inversion)
Results and Analysis
Hacks Removed (100% Successful)
| Location | Hack Removed | State |
|---|---|---|
| Joypad.cpp:52 | Artificial inversion bits 4-5 | REMOVED |
| MMU.cpp:935 | Bypass test_mode_allow_rom_writes | REMOVED |
| MMU.cpp:1068 | Bypass ROM_ONLY empty check | REMOVED |
| MMU.cpp:3564 | set_test_mode_allow_rom_writes() method | REMOVED |
| MMU.hpp:372 | Flag test_mode_allow_rom_writes_ | REMOVED |
| mmu.pyx:403 | Wrapper Python set_test_mode_allow_rom_writes | REMOVED |
| conftest.py:74 | Fixture mmu_romw | REMOVED |
Updated Tests (13 tests, 100% pass)
- ✅
test_core_joypad.py: 8/8 tests with spec-correct values - ✅
test_mmu_rom_is_readonly_by_default.py: 4/4 tests without test_mode - ✅
test_core_mmu.py: 1/1 updated test (address wrap with WRAM)
Test Coverage
Category Tests Result
─────────────────────── ───────────────────────
Joypad (JOYP FF00) 8/8 ✅ PASS
MMU (ROM read-only) 4/4 ✅ PASS
MMU (Core functionality) 7/7 ✅ PASS
─────────────────────── ───────────────────────
TOTAL Step 0425 19/19 ✅ 100%
Complete Suite 215/225 ✅ 95.6%
(10 pre-existing bugs in PPU/Registers/CPU, NOT related)
Impact on the Code
- Removed lines:~150 lines (code + hack comments)
- Updated lines:~80 lines (tests)
- Reduced complexity:Elimination of conditional flags and bypasses
- Improved integrity:100% spec-correct code according to Pan Docs
Lessons Learned
1. Primacy of Pan Docs over Empirical Tests
Problem:Step 0424 implemented bit inversion 4-5 based on test observations, contradicting Pan Docs.
Solution:When a test contradicts official documentation,correct the test, not the hardware. Pan Docs is the source of truth for spec-correct behavior.
2. Test Mode is Technical Debt
Problem:The flagtest_mode_allow_rom_writesallowed writing to ROM, violating
real hardware behavior.
Solution:Tests that require custom ROM must useload_rom_py(),
which correctly simulates the loading of a cartridge. This maintains the integrity of the emulator.
3. Validation with WRAM for Address Wrap
Problem:Address wrap cannot be validated by writing to ROM (it is read-only).
Solution:Use WRAM (0xC000) which is writable. Example:0x1C000 & 0xFFFF = 0xC000.
4. Incremental Migration Strategy
Success:Eliminating hacks atomically (one Step) minimizes risk. The updated tests they explicitly document the change with references to Pan Docs.
References
- Bread Docs:Joypad Input -https://gbdev.io/pandocs/Joypad_Input.html
- Bread Docs:Memory Map -https://gbdev.io/pandocs/Memory_Map.html
- GBEDG:Game Boy Complete Technical Reference
- Step 0424:Inversion context bits 4-5 (hack to be removed)
- Step 0419:Introduction of test_mode_allow_rom_writes (hack to be removed)