⚠️ 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.

Fix: Rendering Bug in Signed Addressing and ALU Expansion

Date:2025-12-19 StepID:0138 State: Verified

Summary

Improved address validation in the methodrender_scanline()of the PPU to prevent Segmentation Faults when calculating tile addresses in modesigned addressing. The fix ensures that both the tile base address and the tile line address (including the next byte) are within the VRAM limits (0x8000-0x9FFF). Additionally, it was verified that the entire ALU block (0x80-0xBF) is implemented correctly, confirming that all 64 arithmetic and logical operations opcodes are available for game execution.

Hardware Concept

The Game Boy has two addressing modes for VRAM tiles:

  • Unsigned Addressing (LCDC bit 4 = 1): The tile IDs range from 0 to 255, and the tiles are stored from address 0x8000. Each tile ID is multiplied by 16 (each tile is 16 bytes) to obtain the address:address = 0x8000 + (tile_id * 16).
  • Signed Addressing (LCDC bit 4 = 0): Tile IDs are interpreted as signed values ​​(-128 to 127), and tile 0 is at address 0x9000 (not 0x8800). The formula is:address = 0x9000 + (signed_tile_id * 16).

The critical problem is that when signed addressing is used, a tile ID of 128 (0x80) is interpreted as -128, resulting in a0x9000 + (-128 * 16) = 0x9000 - 0x800 = 0x8800. However, if the tile ID is very negative or very positive, the calculation can result in addresses outside of VRAM (less than 0x8000 or greater than 0x9FFF), causing Segmentation Faults when the PPU attempts to read that data.

Fountain:Pan Docs - Tile Data Addressing, LCD Control Register (LCDC)

Implementation

Improved address validation inPPU::render_scanline()to ensure that:

  1. The base address of the tile is within VRAM and has enough space for the 16 bytes of the entire tile (verifying thattile_addr<= VRAM_END - 15).
  2. The direction of the tile line (which is calculated astile_addr + tile_y_offset * 2) and the next byte (tile_line_addr + 1) are both inside VRAM.

This double validation prevents out-of-bounds accesses that caused Segmentation Faults when the PPU attempted to render tiles with IDs that resulted in invalid addresses.

Modified components

  • src/core/cpp/PPU.cpp: Improved address validation inrender_scanline()to cover all edge cases, including tiles that extend to the limit of VRAM.

ALU Block Verification

Verified that the entire ALU block (0x80-0xBF) is implemented correctly inCPU.cpp. This block contains 64 opcodes that implement arithmetic and logical operations between register A and other registers or memory:

  • 0x80-0x87: ADD A, r (Sum)
  • 0x88-0x8F: ADC A, r (Sum with Carry)
  • 0x90-0x97: SUB r (Subtraction)
  • 0x98-0x9F: SBC A, r (Subtraction with Carry)
  • 0xA0-0xA7: AND r (logical AND)
  • 0xA8-0xAF: XOR r (logical XOR)
  • 0xB0-0xB7: OR r (logical OR)
  • 0xB8-0xBF: CP r (Compare)

All opcodes are implemented correctly, including variants that access memory via (HL).

Design decisions

Enhanced validation uses comparisons with tight limits (VRAM_END - 15for complete tiles andVRAM_END - 1for tile lines) to ensure that not only the base address, but also all the necessary bytes, are within VRAM. This is critical because a full tile is 16 bytes, and a tile line is 2 consecutive bytes.

Affected Files

  • src/core/cpp/PPU.cpp- Improved address validation inrender_scanline()to prevent Segmentation Faults in signed addressing mode

Tests and Verification

The fix was validated by:

  • Existing test: test_signed_addressing_fixintests/test_core_ppu_rendering.pyverifies that the address calculation in signed mode is correct and that no Segmentation Faults occur.
  • ALU block validation:It was verified bygrepthat all 64 opcodes of the 0x80-0xBF block are implemented inCPU.cpp.
  • Linter:No compiler or linter errors were found in the modified code.

test command

pytest tests/test_core_ppu_rendering.py::TestCorePPURendering::test_signed_addressing_fix -v

Relevant test code

def test_signed_addressing_fix(self) -> None:
    """Verify that the address calculation in signed mode is correct."""
    mmu = PyMMU()
    ppu = PyPPU(mmu)
    
    # LCDC with bit 4=0 (signed addressing active)
    mmu.write(0xFF40, 0x81)
    
    # Tile ID 128 (interpreted as -128 in signed mode)
    # Expected address: 0x9000 + (-128 * 16) = 0x8800
    mmu.write(0x9800, 128)
    
    # Write tile to 0x8800
    for line in range(8):
        mmu.write(0x8800 + (line * 2), 0xFF)
        mmu.write(0x8800 + (line * 2) + 1, 0xFF)
    
    # Advance PPU until a line is completed
    ppu.step(456)
    
    # Verify that there is no Segmentation Fault and that the rendering is correct
    framebuffer = ppu.get_framebuffer()
    assert framebuffer[0] == 3 # Color 3 (black)

Compiled C++ module validation:The test validates that the C++ PPU can render tiles in signed addressing mode without causing Segmentation Faults, confirming that address validation works correctly.

Sources consulted

Educational Integrity

What I Understand Now

  • Signed Addressing in PPU:Signed addressing mode uses 0x9000 as the base (not 0x8800), and tile IDs are interpreted as signed values ​​(-128 to 127). This allows access to tiles both above and below tile 0, but requires careful validation to prevent accesses outside of VRAM.
  • Address Validation:It is critical to validate not only the base address, but also all bytes to be read (in this case, 2 consecutive bytes for a tile line). Additionally, we must consider the full size of the tile (16 bytes) when validating the base address.
  • Complete ALU Block:The 0x80-0xBF block contains all the fundamental arithmetic and logical operations of the CPU. Each operation has 8 variants (one for each register B, C, D, E, H, L, (HL), A), resulting in 64 total opcodes.

What remains to be confirmed

  • Performance:Verify that additional validation does not significantly impact rendering performance, especially in the critical 160 pixels per line loop.
  • Edge cases:Test with extreme tile IDs (0, 127, 128, 255) in both addressing modes to ensure that the validation covers all possible cases.

Hypotheses and Assumptions

We assume that validation with tight limits (VRAM_END - 15andVRAM_END - 1) is sufficient to prevent all out-of-bounds access. This is based on the knowledge that a full tile is 16 bytes and a tile line is 2 consecutive bytes.

Next Steps

  • [ ] Recompile the C++ module and run all tests to verify that the fix does not break existing functionality
  • [ ] Run the emulator with the Tetris ROM to verify that the rendering works correctly without Segmentation Faults
  • [ ] Measure rendering performance to confirm that additional validation does not significantly impact performance
  • [ ] Document the "rendering bug fix complete" milestone with screenshots if possible