How do GPU/PPU timings work on the Gameboy?

Discussion of programming and development for the original Game Boy and Game Boy Color.
squall926
Posts: 35
Joined: Wed Jan 03, 2018 3:50 pm

Re: How do GPU/PPU timings work on the Gameboy?

Post by squall926 »

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: Select all

    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!
squall926
Posts: 35
Joined: Wed Jan 03, 2018 3:50 pm

Re: How do GPU/PPU timings work on the Gameboy?

Post by squall926 »

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?
Attachments
logo.png
logo.png (4.12 KiB) Viewed 7329 times
Corporate.png
Corporate.png (10.72 KiB) Viewed 7329 times
Shonumi
Posts: 342
Joined: Sun Jan 26, 2014 9:31 am

Re: How do GPU/PPU timings work on the Gameboy?

Post by Shonumi »

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: Select all

* 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.
Post Reply