How do GPU/PPU timings work on the Gameboy?

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
How do GPU/PPU timings work on the Gameboy?
by on (#225007)
I’m writing a gameboy emulator and I’ve come to implementing the graphics. However I can’t quite figure out how it works with the cpu as far as timing/clock cycles go. Does the CPU execute a certain amount of cycles (if so how many) and then hand it of to the GPU? Or is the gameboy always in a hblank/vblank state and the GPU uses the CPU in between them? I can’t find any information that helps me with this. Nothing really explains how the GPU works, only that setting certain values changes certain things.
Re: How do GPU/PPU timings work on the Gameboy?
by on (#225008)
The Game Boy CPU and PPU run in parallel. The 4.2 MHz master clock is also the dot clock. It's divided by 2 to form the PPU's 2.1 MHz memory access clock, and divided by 4 to form a multi-phase 1.05 MHz clock used by the CPU.

Each scanline is 456 dots (114 CPU cycles) long and consists of mode 2 (OAM search), mode 3 (active picture), and mode 0 (horizontal blanking). Mode 2 is 80 dots long (2 for each OAM entry), mode 3 is about 168 plus about 10 more for each sprite on a given line, and mode 0 is the rest. After 144 scanlines are drawn are 10 lines of mode 1 (vertical blanking), for a total of 154 lines or 70224 dots per screen. The CPU can't see VRAM (writes are ignored and reads are $FF) during mode 3, but it can during other modes. The CPU can't see OAM during modes 2 and 3, but it can during blanking modes (0 and 1).

To make the estimate for mode 3 more precise, see "Nitty Gritty Gameboy Cycle Timing" by Kevin Horton.
Re: How do GPU/PPU timings work on the Gameboy?
by on (#225009)
So just to help clarify. The gpu switches modes and then while doing its own stuff, the CPU functions normally except for some limited read and write capabilities? Also for the point of emulators, should I execute the 2 GPU cycles and then a CPU cycle or vice-versa?
Re: How do GPU/PPU timings work on the Gameboy?
by on (#232091)
IntensifyingRug, did you win the chalenger?
I'm done one chip8 emu and started one GameBoy, finished the CPU and half mmu but stop on GPU, no have ideia how do this part.
Re: How do GPU/PPU timings work on the Gameboy?
by on (#232134)
I'm going to say what tepples just said, just differently. The CPU and PPU run completely indepedently, the PPU pushes pixels and cycles its own modes no matter what the CPU does; the only direct influence the CPU has on the PPU is turning it on and off via LCDC bit 7.

As for the PPU's modes, each of the 144 scanlines is divided into Mode 2 (OAM scan), followed by Mode 3 (rendering) where the PPU pushes pixels to the LCD at a rate of 1 per cycle of the 4MHz clock (with exceptions), and finally Mode 0 (HBlank) where the PPU does nothing.
Between scanlines 143 and 0 is VBlank, which consists of 10 "scanlines" in Mode 1, numbered 144 through 153. An interesting quirk is that LY starts reporting 0 in the middle of scanline 153.
Re: How do GPU/PPU timings work on the Gameboy?
by on (#232219)
Hello!
I'm a layman in image processing, V-Blank, H-Blank, scanline, sprite, bg... never worked with this. but I found great sites explaining the operation of the GPU and Interruption, more and more I understand the operation better. looking now I have to implement the IO mapping in the MMU.
What is the minimum to see the logo on screen?
Re: How do GPU/PPU timings work on the Gameboy?
by on (#232220)
You at least need to emulate the boot ROM at $0000-$00FF, which can read cartridge header at $0100-$014F (in fact the entirety of ROM space at $0100-$7FFF) and write VRAM at $8000-$9FFF. You will also need PPU ports (LCDC, SCY, BGP) and HRAM ($FF80-$FFFF). You don't need sprites, as the logo is background. And I don't think it touches WRAM ($C000-$DFFF) or OAM ($FE00-$FE9F) at all. The last thing the boot ROM does is disable itself, enabling the cartridge ROM at $0000-$00FF as well.

If you need a boot ROM to test with, you can extract one from SameBoy. It's not a perfect copy (only Nintendo has the right to distribute that) but it does most of the same things.
Re: How do GPU/PPU timings work on the Gameboy?
by on (#232225)
I wouldn't advise trying to emulate the boot ROM. It adds extra complexity, especially since the way the boot ROM is mapped to the address space varies depending on the model.

And there's also that the boot ROM is quite sloppily coded.
Re: How do GPU/PPU timings work on the Gameboy?
by on (#232363)
I uses some ROM test and read on &FF02 byte writed on $FF01, always 0. And Program no finish. I believe got some inst nos work properlly, ill find.
About dissassembler i see an Lot emulador that put Full code dissassemble on a list. Me debug Just print opcode in execution and result.
The ideia ia read ALL ROM so fast execult and put on list? Back and run normally?
Re: How do GPU/PPU timings work on the Gameboy?
by on (#232381)
Sorry, but I don't understand what you meant.
The only thing I grasped was that you're manipulating $FF01 and FF02, which are related to serial. Which is completely irrelevant to this topic. So..??
Re: How do GPU/PPU timings work on the Gameboy?
by on (#232390)
Just a guess, but the issue might be that a particular test ROM expects working (or at least faked) serial in order to write out the result over serial.
Re: How do GPU/PPU timings work on the Gameboy?
by on (#232410)
That might make sense, as Blargg's ROMs write their output to serial
Re: How do GPU/PPU timings work on the Gameboy?
by on (#232426)
Kkkkk, sorry for my terrible eng, I was referring to blargg's rom. and my question was about the dissassembler. all i see has an list of all the code already with values ​​how is this done. if I do a reset, the list goes back to $0100 and already has the other steps.
Re: How do GPU/PPU timings work on the Gameboy?
by on (#232431)
The way an emulator implements its disassembler probably has nothing to do with how it executes code. Both no$gmb and bgb (the two most common debugging emulators) are closed-source so I can't speak with any certainty, but I would imagine both of them handle their disassembly displays entirely outside of the main emulation logic.
Re: How do GPU/PPU timings work on the Gameboy?
by on (#232450)
And indeed, they do. I know nothing about no$gmb; BGB begins its disassembling from the address at the top of the screen, and assumes everything is code.
Some known addresses are enforced as starting points, such as known entry points (vectors = rst + handlers), labels (if a SYM file is loaded) and (maybe? I'm unsure...) memory boundaries.
For example, if a label is inserted in the middle of a `ld a, $AF` instruction, BGB will display
Code:
    db $3E
Label:
    xor a
and resume from there.
The disassembler itself is separate from the emulation core, however the debugger is somewhat tied to the core, since it must be able to break on certain events, etc.
Re: How do GPU/PPU timings work on the Gameboy?
by on (#232579)
I managed to run the bootrom, the Nintendo logo appeared. but ... does not roll from top to bottom, jumping on the screen. I could not find the GPU error. Needless to say, the CPU and the memory did not have much difficulty, but the GPU is my achilles canyon.
Code:
    private static final int MODE0 = 3;
    private static final int MODE1 = 4;
    private static final int MODE2 = 5;
    private static final int LCDC_BGDISPLAY = 0;
    private static final int LCDC_OBJENABLE = 1;
    private static final int LCDC_OBJ_SPRIT = 2; //Bit 2 - OBJ (Sprite) Size (0=8x8, 1=8x16)
    private static final int LCDC_BG_TILE_M = 3; //Bit 3 - BG Tile Map Display Select (0=9800-9BFF, 1=9C00-9FFF)
    private static final int LCDC_BG_TILE_S = 4; //Bit 4 - BG & Window Tile Data Select (0=8800-97FF, 1=8000-8FFF)
    private static final int LCDC_WINDOW_EN = 5; //Bit 5 - Window Display Enable (0=Off, 1=On)
    private static final int LCDC_WINDOW_SE = 6; //Bit 6 - Window Tile Map Display Select (0=9800-9BFF, 1=9C00-9FFF)

public void updateGraphics(int cycles){
        setLCDStatus();
        if(isLCDEnabled())
            scanlineCounter -= cycles;
        else
            return;       
       
        if(scanlineCounter <= 0){
           
            int currentLine = read_reg(0xFF44); // LY
            scanlineCounter = 456;
           
            if(currentLine == 144){ // V-Blank periodo
                MMU.getInstance().requestInterrupter(0);
            }else if(currentLine > 153){ // se passou de 153, reset to 0
                gpu_registros[4] = 0;
            }else if( currentLine < 144){
                drawScanLine() ;
            }
            gpu_registros[4] += 1;
        }       
    }

///////////////////////////////////////////////////////////////////////////////////////
private void drawScanLine() {
        int control = read_reg(0xFF40); // 0xFF40
        if(testBit(control, LCDC_BGDISPLAY))
            renderTiles(control);
       
        if(testBit(control, LCDC_OBJENABLE))
            renderSprites();
           
    }

//////////////////////////////////////////////////////////////////////

private void renderTiles(int c){
        int tileData = 0;
        int backgroundMemory = 0;
        boolean using = true;
       
        int lcdControl = c;
       
        // onde vou desenhar o visual area no window
        int scrollY = read_reg(0xFF42); // SCY
        int scrollX = read_reg(0xFF43); // SCX
        int windowY = read_reg(0xff4A); // WY
        int windowX = read_reg(0xFF4B) - 7; // WX
       
        boolean usingWindow = false;
        // background enable
        if(testBit(lcdControl, LCDC_WINDOW_EN)){ // Bit 5 - Window Display Enable (0=Off, 1=On)
            // is the current scanline we're drawing within the windows Y pos?,
            if(windowY <= read_reg(0xFF44))
                usingWindow = true;
        }
        // qual tile data estamos usando?
        if(testBit(lcdControl, LCDC_BG_TILE_S)){
            tileData = 0x8000;
        }else{
            // IMPORTANT: This memory region uses signed bytes as tile identifiers
            tileData = 0x8800;
            using = false;
        }
        // qual background regiao da memoria?
        if(false == usingWindow){
            if(testBit(lcdControl, LCDC_BG_TILE_M))
                backgroundMemory = 0x9C00;
            else
                backgroundMemory = 0x9800;
        }else{
            // qual window memory
            if(testBit(lcdControl, LCDC_WINDOW_SE))
                backgroundMemory = 0x9C00;
            else
                backgroundMemory = 0x9800;
        }
        int yPos = 0;
        //yPos é usado para calcular qual de 32 vertcal tiles o corrente scanline esta desenhando
        if(!usingWindow)
            yPos = scrollY + read_reg(0xFF44);
        else
            yPos = read_reg(0xFF44) - windowY;
       
        // qual dos 8 vertical pixels do corrente tile esta no scanline
        int tileRow = (yPos / 8)*32;
        //hora de iniciar a desenhar a 160 horizontal pixels para este scanline
        for(int pixel = 0; pixel < 160; pixel++){
            int xPos = pixel + scrollX;
            // traduz o corrente X pos para a janela de space se necessario
            if(usingWindow){
                if(pixel >= windowX){
                    xPos = pixel - windowX;
                }
            }
            // qual dos 32 horizontais tiles este xPos esta dentro
            int tileCol = (xPos / 8);
           
            int tileNumUnsigned=0;
            byte tileNumSigned=0;
            // get the tile identity number. Remember it can be signed or unsigned
            int tileAddress = backgroundMemory + tileRow + tileCol;
            if(using){
                tileNumUnsigned = MMU.getInstance().readByte(tileAddress); // signed
            }else{
                tileNumSigned = (byte)MMU.getInstance().readByte(tileAddress); // unsigned
            }
            // deduce where this tile identifier is in memory.
            int tileLocation = tileData;
            if(using)
                tileLocation += (tileNumUnsigned * 16);
            else
                tileLocation += (tileNumSigned + 128) * 16;
           
            // find the correct vertical line we're on of the tile to get the tile data from in memory
            int line = yPos % 8;
            line *= 2;// each vertical line takes up two bytes of memory
            int data1 = MMU.getInstance().readByte(tileLocation + line) ;
            int data2 = MMU.getInstance().readByte(tileLocation + line + 1) ;
           
            // pixel 0 in the tile is it 7 of data 1 and data2. Pixel 1 is bit 6 etc..
            int colourBit = xPos % 8 ;
            colourBit -= 7 ;
            colourBit *= -1 ;
           
            // combine data 2 and data 1 to get the colour id for this pixel in the tile
            int colourNum = testBit(data2,colourBit) ? 1:0 ;
            colourNum <<= 1;
            colourNum |= testBit(data1,colourBit) ? 1:0 ;
           
            // now we have the colour id get the actual colour from palette 0xFF47
            int col = GetColour(colourNum, 0xFF47) ;
            int red = 0;
            int green = 0;
            int blue = 0;
           
            switch(col){
                case 0:   red = 0xFF; green = 0xFF ; blue = 0xFF; break ;
                case 1: red = 0xCC; green = 0xCC ; blue = 0xCC; break ;
                case 2:   red = 0x77; green = 0x77 ; blue = 0x77; break ;
            }
            int finaly = read_reg(0xFF44);
            if ((finaly<0)||(finaly>143)||(pixel<0)||(pixel>159))
            {
              return;
            }
            screenData[pixel][finaly][0] = red;
            screenData[pixel][finaly][1] = green;
            screenData[pixel][finaly][2] = blue;
        }
    }

/////////////////////////////////////////////////////////////////////////
public void setLCDStatus(){
        int status = read_reg(0xFF41);
       
        if(isLCDEnabled() == false){
            // set o modo para 1 durante lcd disable. e reset scanline
            scanlineCounter = 456; // reset scanline
            gpu_registros[4] = 0; // scanline = 0;
            status &= 252;
            status = setBit(status, 0);
            write_reg(0xFF41, status); // set interrupter H-Blank     
        }
        int currentLine = read_reg(0xFF44);
        int currentMode = status & 0x3;
       
        int mode = 0;
        boolean reqInt = false;
       
        // se estiver em V-Blank, então set MODO para 1
        if(currentLine >= 144){
            mode = 1;
            status = setBit(status, 0);
            status = resetBit(status, 1);
            reqInt = testBit(status, MODE1);
        }else{
            int mode2bounds = 456-80;
            int mode3bounds = mode2bounds - 172;
            // MODO 2
            if(scanlineCounter >= mode2bounds){
                mode = 2;
                status = setBit(status, 1); // enable V-Blank
                status = resetBit(status, 0);// disable H-Blank
                reqInt = testBit(status, MODE2);
            }// MODO 3
            else if(scanlineCounter >= mode3bounds){
                mode = 3;
                status = setBit(status, 1);
                status = setBit(status, 0);
            }//MODO 0
            else{
                mode = 0;
                status = resetBit(status, 1);
                status = resetBit(status, 0);
                reqInt = testBit(status, MODE0);
            }
        }
        // entrada em um novo modo, então solicita Interrupt
        if(reqInt && (mode != currentMode))
            MMU.getInstance().requestInterrupter(1); // Bit 1: LCD Interupt
       
        if(currentLine == read_reg(0xFF45)){
            status = setBit(status, 2);
            if(testBit(status, 6))
                MMU.getInstance().requestInterrupter(1);
           
        }else{
            status = resetBit(status, 2);
        }
        write_reg(0xFF41, status);
       
    }


Some ideia where is the error on renderTiles, on main loop i call gpu.updateGraphics();

thx!
Re: How do GPU/PPU timings work on the Gameboy?
by on (#233047)
Hello, again!
After a while, manage to stabilize the Nintendo logo, but now on the rights screen I can not find the problem in the Tile. To see this screen I need to disable the bootrom.
The test roms only display lists on the screen.
Any idea what can be happening?
Re: How do GPU/PPU timings work on the Gameboy?
by on (#233208)
There are 3 (very broad) possibilities.

1) Your emulator implements some hardware behavior incorrectly (bad CPU instruction, bad timer, bad memory writes/reads, bad interrupts, etc) which causes the data in VRAM to become corrupted.

2) VRAM is not corrupted, however, the way your emulator renders VRAM data as an image is incorrect.

3) Your emulator implements some hardware behavior incorrectly and the way your emulator renders VRAM data as an image is incorrect (double whammy!)

#2 is the easiest to check. Just grab another emulator with a debugger and compare the bytes in VRAM (you can print yours out as a log file, might even be a good idea to bind that to a hotkey to do it on command). If your bytes match something like BGB, then you know the issue is with rendering. Otherwise, you know your emulator is writing incorrect bytes into VRAM, and the source of that problem could be anywhere.

Fortunately, #1 is easy to trace. Simply choose a corrupted tile, and observe all writes to VRAM for that tile. Your emulator will diverge from something like BGB, but with BGB's debugger, you should be able to figure out why. E.g. this is how I go about debugging stuff, question and answer style. The below example is hypothetical; use it as a reference for yourself on bug-hunting.

Code:
* Is my emulator's VRAM the same as BGB's?
- No, 1 byte at 0x8800 is 0xFF in my emulator when it should be 0x00

* When does my emulator write 0xFF to 0x8800?
- Using log files, it wrote 0xFF to 0x8800 at PC = 0x1000 :: LD (HL), A. HL at that time = 0x8800, A = 0xFF

* When does A equal 0xFF before this instruction in my emulator?
-  A is set to 0xFF at PC = 0x9F0 :: LD A, (DE). DE = 0xC000 at that time.

* When does the byte at 0xC000 equal 0xFF before this instruction in my emulator?
- ...


So on and so forth. Eventually you'll run into a point where your emulator and BGB were giving the same results, then they split. Once you find that split, you'll also find the source of your problems. My advice is to see if you can at least pass blargg's CPU tests first. The output may be somewhat obscure, but as long as it lists "OK" for each test, that should be good enough to correctly boot up Tetris.