1

I write GameBoy Color Emulator for fun and learn. Generating color for Gameboy classic work well, but in color version emulator generating bad colors.

I declared BPI, BPD and OPI, OPD. I set up color extraction from background memory and sprites and change it hex colors just like I do in classic version. There are some cases where some element has the correct colors.

BPI, BPD and OPI,OPD:

        public int[] backgroundMemory = new int[0x40];
        public int[] spriteMemory = new int[0x40];
        public int backgroundPaletteIndex;
        public int BackgroundPaletteData
        {
            get => backgroundMemory[backgroundPaletteIndex & 0x3F];
            set
            {
                backgroundMemory[backgroundPaletteIndex & 0x3F] = value;
                if ((backgroundPaletteIndex & 0x80) != 0)
                {
                    backgroundPaletteIndex = (0x80 | ((backgroundPaletteIndex + 1) & 0x3F));
                }
            }
        }
        public int objectPaletteIndex;
        public int ObjectPaletteData
        {
            get => spriteMemory[objectPaletteIndex & 0x3F];
            set
            {
                spriteMemory[objectPaletteIndex & 0x3F] = value;
                if ((objectPaletteIndex) != 0)
                {
                    ++objectPaletteIndex;
                }
            }
        }

write BPI, BPD, OPI, OPD from memory

case 0xFF68:
    ppu.backgroundPaletteIndex = value;
    break;
case 0xFF69:
    ppu.BackgroundPaletteData = value;
    break;
case 0xFF6A:
    ppu.objectPaletteIndex = value;
    break;
case 0xFF6B:
    ppu.ObjectPaletteData = value;
    break;

read BPI, BPD, OPI, OPD from memory

case 0xFF68:
    return ppu.backgroundPaletteIndex;
case 0xFF69:
    return ppu.BackgroundPaletteData;
case 0xFF6A:
    return ppu.objectPaletteIndex;
case 0xFF6B:
    return ppu.ObjectPaletteData;

Update background

public void UpdateBackground()
        {

            var tileMapAddress = backgroundTileMapDisplaySelect ? 0x1C00 : 0x1800;

            if (backgroundAndWindowTileDataSelect)
            {
                for (var i = 0; i < 32; ++i)
                {
                    for (var j = 0; j < 32; ++j, ++tileMapAddress)
                    {
                        if (!backgroundTileInvalidated[i, j] && !invalidateAllBackgroundTilesRequest) continue;
                        backgroundTileInvalidated[i, j] = false;
                        var tileDataAddress = _memory.videoRam[tileMapAddress] << 4;
                        var y = i << 3;
                        var x = j << 3;
                        for (var k = 0; k < 8; ++k)
                        {
                            var lowByte = _memory.videoRam[tileDataAddress++];
                            var highByte = _memory.videoRam[tileDataAddress++] << 1;
                            for (var b = 7; b >= 0; --b)
                            {
                                var index = (0x02 & highByte) | (0x01 & lowByte);
                                if (_colorMode)
                                {
                                    backgroundBuffer[y + k, x + b] = GetGbcColor(backgroundMemory, index);
                                }
                                else
                                {
                                    backgroundBuffer[y + k, x + b] = backgroundPalette[index];
                                }
                                lowByte >>= 1;
                                highByte >>= 1;
                            }
                        }
                    }
                }
            }
            else
            {
                for (var i = 0; i < 32; ++i)
                {
                    for (var j = 0; j < 32; ++j, ++tileMapAddress)
                    {
                        if (!backgroundTileInvalidated[i, j] && !invalidateAllBackgroundTilesRequest) continue;
                        backgroundTileInvalidated[i, j] = false;
                        int tileDataAddress = _memory.videoRam[tileMapAddress];
                        if (tileDataAddress > 127)
                        {
                            tileDataAddress -= 256;
                        }
                        tileDataAddress = 0x1000 + (tileDataAddress << 4);
                        var y = i << 3;
                        var x = j << 3;
                        for (var k = 0; k < 8; ++k)
                        {
                            var lowByte = _memory.videoRam[tileDataAddress++];
                            var highByte = _memory.videoRam[tileDataAddress++] << 1;
                            for (var b = 7; b >= 0; --b)
                            {
                                var index = (0x02 & highByte) | (0x01 & lowByte);
                                if (_colorMode)
                                {
                                    backgroundBuffer[y + k, x + b] = GetGbcColor(backgroundMemory, index);
                                }
                                else
                                {
                                    backgroundBuffer[y + k, x + b] = backgroundPalette[index];
                                }
                                lowByte >>= 1;
                                highByte >>= 1;
                            }
                        }
                    }
                }
            }

            invalidateAllBackgroundTilesRequest = false;
        }

Update Sprite

 public void UpdateSpriteTiles()
        {

            for (var i = 0; i < 256; ++i)
            {
                if (!spriteTileInvalidated[i] && !invalidateAllSpriteTilesRequest) continue;
                spriteTileInvalidated[i] = false;
                var address = i << 4;
                for (var y = 0; y < 8; ++y)
                {
                    var lowByte = _memory.videoRam[address++];
                    var highByte = _memory.videoRam[address++] << 1;
                    for (var x = 7; x >= 0; --x)
                    {
                        var paletteIndex = (0x02 & highByte) | (0x01 & lowByte);
                        lowByte >>= 1;
                        highByte >>= 1;
                        if (paletteIndex > 0)
                        {
                            if (_colorMode)
                            { 
                                spriteTile[i, y, x, 0] = GetGbcColor(spriteMemory,paletteIndex);
                                spriteTile[i, y, x, 1] = GetGbcColor(spriteMemory, paletteIndex);
                            }
                            else
                            {
                                spriteTile[i, y, x, 0] = objectPalette0[paletteIndex];
                                spriteTile[i, y, x, 1] = objectPalette1[paletteIndex];
                            }
                        }
                        else
                        {
                            spriteTile[i, y, x, 0] = 0;
                            spriteTile[i, y, x, 1] = 0;
                        }
                    }
                }
            }

            invalidateAllSpriteTilesRequest = false;
        }

Update Window

public void UpdateWindow()
        {

            var tileMapAddress = windowTileMapDisplaySelect ? 0x1C00 : 0x1800;

            if (backgroundAndWindowTileDataSelect)
            {
                for (var i = 0; i < 18; ++i)
                {
                    for (var j = 0; j < 21; ++j)
                    {
                        if (!backgroundTileInvalidated[i, j] && !invalidateAllBackgroundTilesRequest) continue;
                        var tileDataAddress = _memory.videoRam[tileMapAddress + ((i << 5) | j)] << 4;
                        var y = i << 3;
                        var x = j << 3;
                        for (var k = 0; k < 8; ++k)
                        {
                            var lowByte = _memory.videoRam[tileDataAddress++];
                            var highByte = _memory.videoRam[tileDataAddress++] << 1;
                            for (var b = 7; b >= 0; --b)
                            {
                                var index = (0x02 & highByte) | (0x01 & lowByte);
                                if (_colorMode)
                                {
                                    windowBuffer[y + k, x + b] = GetGbcColor(backgroundMemory, index);
                                }
                                else
                                {
                                    windowBuffer[y + k, x + b] = backgroundPalette[index];
                                }
                                lowByte >>= 1;
                                highByte >>= 1;
                            }
                        }
                    }
                }
            }
            else
            {
                for (var i = 0; i < 18; ++i)
                {
                    for (var j = 0; j < 21; ++j)
                    {
                        if (!backgroundTileInvalidated[i, j] && !invalidateAllBackgroundTilesRequest) continue;
                        int tileDataAddress = _memory.videoRam[tileMapAddress + ((i << 5) | j)];
                        if (tileDataAddress > 127)
                        {
                            tileDataAddress -= 256;
                        }
                        tileDataAddress = 0x1000 + (tileDataAddress << 4);
                        var y = i << 3;
                        var x = j << 3;
                        for (var k = 0; k < 8; ++k)
                        {
                            var lowByte = _memory.videoRam[tileDataAddress++];
                            var highByte = _memory.videoRam[tileDataAddress++] << 1;
                            for (var b = 7; b >= 0; --b)
                            {
                                var index = (0x02 & highByte) | (0x01 & lowByte);
                                if (_colorMode)
                                {
                                    windowBuffer[y + k, x + b] = GetGbcColor(backgroundMemory,index);
                                }
                                else
                                {
                                    windowBuffer[y + k, x + b] = backgroundPalette[index];
                                }
                                lowByte >>= 1;
                                highByte >>= 1;
                            }
                        }
                    }
                }
            }
        }

Update Graphiscs

private void UpdateModel(bool updateBitmap)
        {
            if (updateBitmap)
            {
                var backgroundBuffer = _ppu.backgroundBuffer;
                var windowBuffer = _ppu.windowBuffer;
                var oam = _memory.oam;

                for (int y = 0, pixelIndex = 0; y < Height; ++y)
                {
                    _ppu.ly = y;
                    _ppu.lcdcMode = LcdcModeType.SearchingOamRam;
                    if (_cpu.lcdcInterruptEnabled
                        && (_ppu.lcdcOamInterruptEnabled
                        || (_ppu.lcdcLycLyCoincidenceInterruptEnabled && _ppu.lyCompare == y)))
                    {
                        _cpu.lcdcInterruptRequested = true;
                    }
                    ExecuteProcessor(800);
                    _ppu.lcdcMode = LcdcModeType.TransferingData;
                    ExecuteProcessor(1720);

                    _ppu.UpdateWindow();
                    _ppu.UpdateBackground();
                    _ppu.UpdateSpriteTiles();

                    var backgroundDisplayed = _ppu.backgroundDisplayed;
                    var scrollX = _ppu.scrollX;
                    var scrollY = _ppu.scrollY;
                    var windowDisplayed = _ppu.windowDisplayed;
                    var windowX = _ppu.windowX - 7;
                    var windowY = _ppu.windowY;

                    for (var x = 0; x < Width; ++x, ++pixelIndex)
                    {
                        uint intensity = 0;

                        if (backgroundDisplayed)
                        {
                            intensity = backgroundBuffer [0xFF & (scrollY + y), 0xFF & (scrollX + x)];
                        }

                        if (windowDisplayed && y >= windowY && y < windowY + Height && x >= windowX && x < windowX + Width
                            && windowX >= -7 && windowX < Width && windowY >= 0 && windowY < Height)
                        {
                            intensity = windowBuffer [y - windowY, x - windowX];
                        }

                        _pixels [pixelIndex] = intensity;
                    }

                    if (_ppu.spritesDisplayed)
                    {
                        var spriteTile = _ppu.spriteTile;
                        if (_ppu.largeSprites)
                        {
                            for (var address = 0; address < Width; address += 4)
                            {
                                int spriteY = oam [address];
                                int spriteX = oam [address + 1];
                                if (spriteY == 0 || spriteX == 0 || spriteY >= 160 || spriteX >= 168)
                                {
                                    continue;
                                }
                                spriteY -= 16;
                                if (spriteY > y || spriteY + 15 < y)
                                {
                                    continue;
                                }
                                spriteX -= 8;

                                var spriteTileIndex0 = 0xFE & oam [address + 2];
                                var spriteTileIndex1 = spriteTileIndex0 | 0x01;
                                var spriteFlags = oam [address + 3];
                                var spritePriority = (0x80 & spriteFlags) == 0x80;
                                var spriteYFlipped = (0x40 & spriteFlags) == 0x40;
                                var spriteXFlipped = (0x20 & spriteFlags) == 0x20;
                                var spritePalette = (0x10 & spriteFlags) == 0x10 ? 1 : 0;

                                if (spriteYFlipped)
                                {
                                    var temp = spriteTileIndex0;
                                    spriteTileIndex0 = spriteTileIndex1;
                                    spriteTileIndex1 = temp;
                                }

                                var spriteRow = y - spriteY;
                                if (spriteRow >= 0 && spriteRow < 8)
                                {
                                    var screenAddress = (y << 7) + (y << 5) + spriteX;
                                    for (var x = 0; x < 8; ++x, ++screenAddress)
                                    {
                                        var screenX = spriteX + x;
                                        if (screenX >= 0 && screenX < Width)
                                        {
                                            var color = spriteTile [spriteTileIndex0,
          spriteYFlipped ? 7 - spriteRow : spriteRow,
          spriteXFlipped ? 7 - x : x, spritePalette];
                                            if (color <= 0) continue;
                                            if (spritePriority)
                                            {
                                                if (_pixels [screenAddress] == 0xFFFFFFFF)
                                                {
                                                    _pixels [screenAddress] = color;
                                                }
                                            } else
                                            {
                                                _pixels [screenAddress] = color;
                                            }
                                        }
                                    }
                                    continue;
                                }

                                spriteY += 8;

                                spriteRow = y - spriteY;
                                if (spriteRow < 0 || spriteRow >= 8) continue;
                                {
                                    var screenAddress = (y << 7) + (y << 5) + spriteX;
                                    for (var x = 0; x < 8; ++x, ++screenAddress)
                                    {
                                        var screenX = spriteX + x;
                                        if (screenX < 0 || screenX >= Width) continue;
                                        var color = spriteTile [spriteTileIndex1,
                                            spriteYFlipped ? 7 - spriteRow : spriteRow,
                                            spriteXFlipped ? 7 - x : x, spritePalette];
                                        if (color <= 0) continue;
                                        if (spritePriority)
                                        {
                                            if (_pixels [screenAddress] == 0xFFFFFFFF)
                                            {
                                                _pixels [screenAddress] = color;
                                            }
                                        } else
                                        {
                                            _pixels [screenAddress] = color;
                                        }
                                    }
                                }
                            }
                        } else
                        {
                            for (var address = 0; address < Width; address += 4)
                            {
                                int spriteY = oam [address];
                                int spriteX = oam [address + 1];
                                if (spriteY == 0 || spriteX == 0 || spriteY >= 160 || spriteX >= 168)
                                {
                                    continue;
                                }
                                spriteY -= 16;
                                if (spriteY > y || spriteY + 7 < y)
                                {
                                    continue;
                                }
                                spriteX -= 8;

                                var spriteTileIndex = oam [address + 2];
                                var spriteFlags = oam [address + 3];
                                var spritePriority = (0x80 & spriteFlags) == 0x80;
                                var spriteYFlipped = (0x40 & spriteFlags) == 0x40;
                                var spriteXFlipped = (0x20 & spriteFlags) == 0x20;
                                var spritePalette = (0x10 & spriteFlags) == 0x10 ? 1 : 0;

                                var spriteRow = y - spriteY;
                                var screenAddress = (y << 7) + (y << 5) + spriteX;
                                for (var x = 0; x < 8; ++x, ++screenAddress)
                                {
                                    var screenX = spriteX + x;
                                    if (screenX < 0 || screenX >= Width) continue;
                                    var color = spriteTile [spriteTileIndex,
                                        spriteYFlipped ? 7 - spriteRow : spriteRow,
                                        spriteXFlipped ? 7 - x : x, spritePalette];
                                    if (color <= 0) continue;
                                    if (spritePriority)
                                    {
                                        if (_pixels [screenAddress] == 0xFFFFFFFF)
                                        {
                                            _pixels [screenAddress] = color;
                                        }
                                    } else
                                    {
                                        _pixels [screenAddress] = color;
                                    }
                                }
                            }
                        }
                    }

                    _ppu.lcdcMode = LcdcModeType.HBlank;
                    if (_cpu.lcdcInterruptEnabled && _ppu.lcdcHBlankInterruptEnabled)
                    {
                        _cpu.lcdcInterruptRequested = true;
                    }
                    ExecuteProcessor(2040);
                    AddTicksPerScanLine();
                }
            } else
            {
                for (var y = 0; y < Height; ++y)
                {
                    _ppu.ly = y;
                    _ppu.lcdcMode = LcdcModeType.SearchingOamRam;
                    if (_cpu.lcdcInterruptEnabled
                        && (_ppu.lcdcOamInterruptEnabled
                        || (_ppu.lcdcLycLyCoincidenceInterruptEnabled && _ppu.lyCompare == y)))
                    {
                        _cpu.lcdcInterruptRequested = true;
                    }
                    ExecuteProcessor(800);
                    _ppu.lcdcMode = LcdcModeType.TransferingData;
                    ExecuteProcessor(1720);
                    _ppu.lcdcMode = LcdcModeType.HBlank;
                    if (_cpu.lcdcInterruptEnabled && _ppu.lcdcHBlankInterruptEnabled)
                    {
                        _cpu.lcdcInterruptRequested = true;
                    }
                    ExecuteProcessor(2040);
                    AddTicksPerScanLine();
                }
            }

            _ppu.lcdcMode = LcdcModeType.VBlank;
            if (_cpu.vBlankInterruptEnabled)
            {
                _cpu.vBlankInterruptRequested = true;
            }
            if (_cpu.lcdcInterruptEnabled && _ppu.lcdcVBlankInterruptEnabled)
            {
                _cpu.lcdcInterruptRequested = true;
            }
            for (var y = 144; y <= 153; ++y)
            {
                _ppu.ly = y;
                if (_cpu.lcdcInterruptEnabled && _ppu.lcdcLycLyCoincidenceInterruptEnabled
                    && _ppu.lyCompare == y)
                {
                    _cpu.lcdcInterruptRequested = true;
                }
                ExecuteProcessor(4560);
                AddTicksPerScanLine();
            }
            if (Audio != null)
                _memory.soundChip.OutputSound(Audio);
        }

Get GBCColor

        private static uint GetGbcColor(int[] paletteMemory, int paletteIndex)
        {
            var rawValue = (paletteMemory[paletteIndex * 2] | (paletteMemory[(paletteIndex*2)+1] & 0x7F)<<8);
            var r = ((rawValue & 0x1F)*255)/31;
            var g = (((rawValue >> 5) & 0x1F)*255)/31;
            var b = (((rawValue >> 10) & 0x1F)*255)/31;
            return (uint)(0xFF000000 | (r << 16) | (g << 8) | (b << 0));
        }

Just how it should work menu: https://photos.app.goo.gl/M2QTHVTzoPS1jiKS9
Just as they do now: https://photos.app.goo.gl/GW7SCm3ZXHyxRikGA
First room in gameboy color: https://photos.app.goo.gl/BNHDgmuPw5j1c9Lq6
In my emulator first room: https://photos.app.goo.gl/934tm2X1jEazkcPK7


I care about the simplest solution Thanks in advance

Dante
  • 43
  • 1
  • 7
  • If you want to make it simple then you don't need to copy-paste exact same code just because LCDC register is different (I assume that's the meaning of `backgroundAndWindowTileDataSelect`). Your if-else can be refactored into one single block with a bit of logic around LCDC. The code seems weird in general. Where's SCX, SCY, LY, tile attributes? Are you sure it's background and not window and sprites? The menu screen suggest that there're more problems. – creker May 03 '19 at 11:01
  • @creker SY, SX, LY are used in other places. First, it loads the data into the buffor and then I use these reiters to have the final image. I updated my post with the rest of the methods to update the graphics. – Dante May 03 '19 at 12:07
  • Too much code to go over but one thing caught my eye - `ExecuteProcessor`. Maybe I'm missing something but that's not the way you should synchronize. CPU should be the main driver of everything. Every tick is important, you can't jump forward even a couple of ticks. Games constantly overwrite stuff in memory between scanlines. So you have to be very careful with GPU states. Your GPU should work as a state machine driven by CPU one operation at a time (be it instruction or memory access). GPU should render only on HBLANK one scanline at a time. You can't just buffer everything at once. – creker May 03 '19 at 12:22
  • 1
    Remember, real CPU and GPU are working in parallel but probably driven by the same clock. Games at that time were programmed very carefully to exploit hardware timings to precisely overwrite memory at the right time to render some complex thing on the screen. So you have to be very careful and emulate these exact timings. CPU executes one instruction, 4 ticks - update every other component in your emulator. CPU fetches something from memory, 4 ticks - again, update everything. Only this way you will achieve proper results. – creker May 03 '19 at 12:27
  • @creker It is interesting because the classica games are work very well. I will try to improve it. Do you know any forums where they mainly deal with such places so that if I could find out someone else? There is usually little information on the internet about this topic or if I could ask you here? – Dante May 03 '19 at 13:04
  • I didn't use any forums when I was writing my emulator. The documents that out there are enough in most of the cases. The thing about timings is pretty much common knowledge for such old systems. They all relied on careful timings. When you get to the sound you will really understand how important it is. There're many nuances to memory access, timings, IO ports and it's all interconnected. Gameboy CPU manual and Pan Docs are good places to start. Also Blargg's test ROMs are extremely helpful as they test these exact nuances among other things. – creker May 03 '19 at 13:26
  • @creker I use these things, I do not always understand but I will go forward. Thank you very much. – Dante May 03 '19 at 13:29
  • Here's an interesting read for you http://blog.kevtris.org/blogfiles/Nitty%20Gritty%20Gameboy%20VRAM%20Timing.txt Makes you really understand that it's much more complicated than you think. I don't think you need to emulated everything exactly like this but somewhere close at least. More complicated games like Pokemon sure do exploit these things. But even the simplest thing can screw up a game. Like register not properly cleared after some state transition. – creker May 03 '19 at 13:46

0 Answers0