gameboy wormboy manboy gameworm gameman wormgame mangame manworm
Dependencies: mbed SDFileSystem2
Diff: video.cpp
- Revision:
- 17:c9afe1a7b423
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/video.cpp Sun Jan 13 19:00:10 2019 +0000 @@ -0,0 +1,390 @@ +#include <stdlib.h> +#include <mbed.h> +#define NDEBUG +#include <assert.h> +#include "video.h" +#include "mem.h" +#include "graphics_display.h" +#include "main.h" + +u32 frameCount = 0; + +// the implementation of sprites is poor. +// pick a value for this that is between 0 and 256 and not equal to one of the colors +#define TRANSPARENT_SPRITE 37 + +#define BRIGHTEST_COLOR 6 + +VideoState globalVideoState; + +// colors on screen (white, light gray, dark gray, black) +// the first one must be "BRIGHTEST_COLOR" - other code depends on this! +static const u8 colors[4] = {BRIGHTEST_COLOR, 4, 2, 0}; + + + + +void initVideo(u8* frameBuffer) { + globalVideoState.mode = 0; + globalVideoState.modeClock = 0; + globalVideoState.line = 0; + globalVideoState.frameBuffer = frameBuffer; // <- todo change for STM32 + +// for(u32 i = 0; i < 160*144; i++) { +// globalVideoState.frameBuffer[i] = 255; +// } + +} + +inline u32 xy2px(u8 x, u8 y) { + return H_RES*(y+Y0) + x + X0; +} + +void dumpVideo() { + for(u16 i = 0x8000; i < 0x87ff; i++) { + if((i%8) == 1) { + printf("@ 0x%04x: ", i); + } + printf("0x%02x ", readByte(i)); + if(!(i%8)) printf("\n"); + } + printf("\n"); + assert(false); +} + +// read a pixel out of a tile and apply the given palette +u8 readTile(u16 tileAddr, u8 x, u8 y, u8 palette) { + assert(x <= 8); + assert(y <= 8); + x = (7 - x); + u16 loAddr = tileAddr + (y*(u16)2); + u16 hiAddr = loAddr + (u16)1; + u8 lo = readByte(loAddr); + u8 hi = readByte(hiAddr); + u8 loV = (lo >> x) & (u8)1; + u8 hiV = (hi >> x) & (u8)1; + //u8 result = loV * 120 + hiV * 60; + u8 colorIdx = loV + 2 * hiV; + u8 colorID = (palette >> (2 * colorIdx)) & 3; + return colors[colorID]; +} + +u8 readTilePtr(u8* tileAddr, u8 x, u8 y, u8 palette) { + assert(x <= 8); + assert(y <= 8); + x = (7 - x); + u8* loAddr = tileAddr + (y*(u16)2); + u8* hiAddr = loAddr + (u16)1; + u8 lo = *(loAddr); + u8 hi = *(hiAddr); + u8 loV = (lo >> x) & (u8)1; + u8 hiV = (hi >> x) & (u8)1; + //u8 result = loV * 120 + hiV * 60; + u8 colorIdx = loV + 2 * hiV; + u8 colorID = (palette >> (2 * colorIdx)) & 3; + return colors[colorID]; +} + +// read a pixel out of a tile and apply the given palette +// returns TRANSPARENT_SPRITE if the sprite should be transparent +u8 readSpriteTile(u16 tileAddr, u8 x, u8 y, u8 palette) { + //tileAddr = 0x8180; + assert(x <= 8); + assert(y <= 8); + x = (7 - x); + u16 loAddr = tileAddr + (y*(u16)2); + u16 hiAddr = loAddr + (u16)1; + u8 lo = readByte(loAddr); + u8 hi = readByte(hiAddr); + u8 loV = (lo >> x) & (u8)1; + u8 hiV = (hi >> x) & (u8)1; + u8 colorIdx = loV + 2 * hiV; + if(colorIdx == 0) { + return TRANSPARENT_SPRITE; + } + u8 colorID = (palette >> (2 * colorIdx)) & 3; + return colors[colorID]; +} + +u8 readSpriteTileAddr(u8* tileAddr, u8 x, u8 y, u8 palette) { + //tileAddr = 0x8180; + assert(x <= 8); + assert(y <= 8); + x = (7 - x); + u8* loAddr = tileAddr + (y*(u16)2); + u8* hiAddr = loAddr + (u16)1; + u8 lo = *(loAddr); + u8 hi = *(hiAddr); + u8 loV = (lo >> x) & (u8)1; + u8 hiV = (hi >> x) & (u8)1; + u8 colorIdx = loV + 2 * hiV; + if(colorIdx == 0) { + return TRANSPARENT_SPRITE; + } + u8 colorID = (palette >> (2 * colorIdx)) & 3; + return colors[colorID]; +} + +// compute the address of the tile from the tile's index +// this is confusing because depending on the tileData selected, +// the tileIdx is either signed or unsigned +u16 computeTileAddr(u8 tileIdx, bool tileData) { + if(tileData) { + return 0x8000 + 16 * tileIdx; + } else { + if(tileIdx <= 127) { + return 0x9000 + 16 * tileIdx; + } else { + return 0x8000 + 16 * (tileIdx); + } + } +} + +u8* computeTileAddrPtr(u8 tileIdx, bool tileData) { + if(tileData) { + return globalMemState.vram + 16 * tileIdx; + } else { + if(tileIdx <= 127) { + return globalMemState.vram + 0x1000 + 16 * tileIdx; + } else { + return globalMemState.vram + 16 * (tileIdx); + } + } +} + +// main function to render a line of the display. +// this implementation is missing a number of things, including (but not limited to) +// -- proper position of the WINDOW +// -- 16x8 sprites +// -- sprite sorting +// -- 10 sprite limit +void renderLine() { + if(frameCount % 3) return; + //return; + //if(drawing) return; + //printf("%x %x\n", im_line_va, im_back); + globalVideoState.frameBuffer = im_line_vas[bufferSelect]; + u8 lcdc = globalMemState.ioRegs[IO_LCDC]; // lcd control register + bool lcdOn = (lcdc >> 7) & (u8)1; // lcd display on? + bool windowTileMap = (lcdc >> 6) & (u8)1; // select tilemap source for window + bool windowEnable = (lcdc >> 5) & (u8)1; // draw window? + bool tileData = (lcdc >> 4) & (u8)1; // select tile data source + bool bgTileMap = (lcdc >> 3) & (u8)1; // select tilemap source for background + bool objSize = (lcdc >> 2) & (u8)1; // pick sprite size (nyi) + bool objEnable = (lcdc >> 1) & (u8)1; // enable sprite renderer + bool bgWinEnable = (lcdc >> 0) & (u8)1; // enable background and window renderer? + + u16 windowMapAddr = (u16)(windowTileMap ? 0x9c00 : 0x9800); + u16 bgTileMapAddr = (u16)(bgTileMap ? 0x9c00 : 0x9800); + + // background renderer + if(lcdOn && bgWinEnable) { + // render background onto framebuffer + u8 pal = globalMemState.ioRegs[IO_BGP]; // color palette + u16 tileMapRowAddr = (u16)(bgTileMapAddr + 32*((((u16)globalVideoState.line + + globalMemState.ioRegs[IO_SCROLLY]) & (u16)255) >> 3)); // address of the row of the tilemap + u8 tileMapColIdx = globalMemState.ioRegs[IO_SCROLLX] >> 3; // column index of the tilemap + u8 yPixOffset = ((u8)globalVideoState.line + globalMemState.ioRegs[IO_SCROLLY]) & (u8)7; // y-pixel of tile + u8 xPixOffset = globalMemState.ioRegs[IO_SCROLLX] & (u8)7; // x-pixel of tile + //u8* linePtr = globalVideoState.frameBuffer + 160 * globalVideoState.line; // frame buffer pointer + u8 tileIdx = readByte(tileMapRowAddr + tileMapColIdx); // tile index + + // loop over pixels in the line + for(u8 px = 0; px < 160; px++) { + globalVideoState.frameBuffer = im_line_vas[bufferSelect]; + globalVideoState.frameBuffer[xy2px(px,globalVideoState.line)] = + readTilePtr(computeTileAddrPtr(tileIdx, tileData), xPixOffset, yPixOffset, pal); + //readTile(computeTileAddr(tileIdx, tileData), xPixOffset, yPixOffset, pal); // set the frame buffer + + xPixOffset++; // increment tile pixel + //linePtr++; // increment frame buffer pixel + if(xPixOffset == 8) { // if we have overflowed the tile + xPixOffset = 0; // go to the beginning + tileMapColIdx = (tileMapColIdx + 1) & 31; // of the next tile (allow wraparound) + tileIdx = readByte(tileMapRowAddr + tileMapColIdx); // and look up the tile index in the tile map + } + } + } + +// // window renderer +// if(windowEnable) { +// u8 pal = globalMemState.ioRegs[IO_BGP]; // palette +// u8 wx = globalMemState.ioRegs[IO_WINX]; // location of the window (nyi) +// u8 wy = globalMemState.ioRegs[IO_WINY]; // location of the window (nyi) +// if(wx > 166 || wy > 143) { +// // if the window is out of this range, it is disabled too. +// } else { +// u16 tileMapRowAddr = windowMapAddr + 32*((((u16)globalVideoState.line)) >> 3); // address of the row of the tilemap +// u8 tileMapColIdx = 0; // column index of the tilemap +// u8 yPixOffset = ((u8)globalVideoState.line) & (u8)7; // y-pixel of tile +// u8 xPixOffset = 0; // x-pixel of tile +// u8* linePtr = globalVideoState.frameBuffer + 160 * globalVideoState.line; // frame buffer pointer +// u8 tileIdx = readByte(tileMapRowAddr + tileMapColIdx); // tile index +// +// // loop over pixels in the line +// for(u8 px = 0; px < 160; px++) { +// *linePtr = readTile(computeTileAddr(tileIdx, tileData), xPixOffset, yPixOffset, pal); // set the frame buffer +// +// xPixOffset++; // increment tile pixel +// linePtr++; // increment frame buffer pixel +// if(xPixOffset == 8) { // if we have overflowed the tile +// xPixOffset = 0; // go to the beginning +// tileMapColIdx = (tileMapColIdx + 1) & 31; // of the next tile (allow wraparound, but it shouldn't happen?) +// tileIdx = readByte(tileMapRowAddr + tileMapColIdx); // and look up the tile index in the tile map +// } +// } +// } +// } +// +// + // sprite renderer + if(objEnable) { + for(u16 spriteID = 0; spriteID < 40; spriteID++) { + u16 oamPtr = 0xfe00 + 4 * spriteID; // sprite information table + u8 spriteY = readByte(oamPtr); // y-coordinate of sprite + u8 spriteX = readByte(oamPtr + 1); // x-coordinate of sprite + u8 patternIdx = readByte(oamPtr + 2); // sprite pattern + u8 flags = readByte(oamPtr + 3); // flag bits + + bool pri = (flags >> 7) & (u8)1; // priority (transparency stuff) + bool yFlip = (flags >> 6) & (u8)1; // flip around y? + bool xFlip = (flags >> 5) & (u8)1; // flip around x? + bool palID = (flags >> 4) & (u8)1; // palette ID (OBP0/OBP2) + + u8 pal = palID ? globalMemState.ioRegs[IO_OBP1] : globalMemState.ioRegs[IO_OBP0]; + + + if(spriteX | spriteY) { + // the sprite coordinates have an offset + u8 spriteStartY = spriteY - 16; + u8 spriteLastY = spriteStartY + 8; // todo 16 row sprites + + // reject based on y if the sprite won't be visible in the current line + if(globalVideoState.line < spriteStartY || globalVideoState.line >= spriteLastY) { + continue; + } + + // get y px relative to the sprite pattern + u8 tileY = globalVideoState.line - spriteStartY; + if(yFlip) { + tileY = 7 - tileY; + } + + assert(tileY < 8); + + // loop over the 8 pixels that the sprite is on: + for(u8 tileX = 0; tileX < 8; tileX++) { + + u8 xPos = spriteX - 8 + tileX; // position on the screen + if(xPos >= 160) continue; // reject if we go off the end, don't wrap around + + u32 fbIdx = xy2px(xPos, globalVideoState.line); + + globalVideoState.frameBuffer = im_line_vas[bufferSelect]; + + + // current color at the screen + u8 old = globalVideoState.frameBuffer[fbIdx]; + + // get the pixel from the sprite pattern data + u8 tileLookupX = tileX; + if(xFlip) { + tileLookupX = 7 - tileX; + } + //u8 tileValue = readSpriteTile(0x8000 + patternIdx * 16, tileLookupX, tileY, pal); + u8 tileValue = readSpriteTileAddr(globalMemState.vram + patternIdx * 16, tileLookupX, tileY, pal); + //u8 tileValue = readSpriteTileAddr(globalMemState.vram + patternIdx*16, tileLookupX, tileY, pal); + //u8 tileValue = 4; + // don't draw transparent + if(tileValue == TRANSPARENT_SPRITE) continue; // (transparent sprites) + + // not sure this is 100% right... + if(!pri) { + globalVideoState.frameBuffer[fbIdx] = tileValue; + } else { + if(old == BRIGHTEST_COLOR) { + globalVideoState.frameBuffer[fbIdx] = tileValue; + } + } + } + } + } + } +} + +static u32 oldTics = 0; +float filteredFrameRate = 0.f; + +// step the video by a number of clock cycles +void stepVideo(u32 cycles) { + globalVideoState.modeClock += cycles; + switch(globalVideoState.mode) { + case 2: // OAM read, scanning + + if(globalVideoState.modeClock >= 80) { + globalVideoState.modeClock = 0; + globalVideoState.mode = 3; // VRAM read, scanning + } + break; + case 3: // VRAM read, scanning + if(globalVideoState.modeClock >= 172) { + globalVideoState.modeClock = 0; + globalVideoState.mode = 0; // hblank + renderLine(); // draw line into framebuffer + } + break; + case 0: // hblank + if(globalVideoState.modeClock >= 204) { + globalVideoState.modeClock = 0; + globalVideoState.line++; + + if(globalVideoState.line == 143) { + + globalVideoState.mode = 1; // vblank + globalMemState.ioRegs[IO_IF] |= 0x1; // set interrupt for vblank + if(!keyboard.turbo) // if we are in "turbo" mode, don't update graphics + updateGraphics(); // display framebuffer on screen + //bufferSelect = !bufferSelect; + + + u32 dTics = tics - oldTics; + float frameTime = 64.0e-6 * (float)dTics; + filteredFrameRate = (0.94 * filteredFrameRate) + (0.06 * frameTime); + printf("%d %f %f\n", dTics, 1.f/frameTime, 1.f/filteredFrameRate); + frameCount++; + oldTics = tics; + + } else { + globalVideoState.mode = 2; // oam + } + } + break; + case 1: // vblank + if(globalVideoState.modeClock >= 456) { + globalVideoState.modeClock = 0; + globalVideoState.line++; + + if(globalVideoState.line > 153) { + globalVideoState.mode = 2; + globalVideoState.line = 0; + } + } + break; + default: + assert(false); + } + + globalMemState.ioRegs[IO_LY] = (u8)globalVideoState.line; // update current line + + + // this is likely somewhat wrong. + u8 stat = globalMemState.ioRegs[IO_STAT]; // update STAT register (this is likely the source of issue on bubble bobble) + stat &= ~7; // clear mode, coincidence + stat += globalVideoState.mode; // set current mode + if(globalMemState.ioRegs[IO_LYC] == globalMemState.ioRegs[IO_LY]) { // check coincidence + stat += 4; + if((stat >> 6) & 1) { + globalMemState.ioRegs[IO_IF] |= 2; // stat interrupt + } + } +}