ref: d5a3d7f76cc92928222a981981ce8b8f682d2085
dir: /snes/ppu.c/
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stdbool.h> #include <stddef.h> #include "ppu.h" #include "snes.h" // array for layer definitions per mode: // 0-7: mode 0-7; 8: mode 1 + l3prio; 9: mode 7 + extbg // 0-3; layers 1-4; 4: sprites; 5: nonexistent static const int layersPerMode[10][12] = { {4, 0, 1, 4, 0, 1, 4, 2, 3, 4, 2, 3}, {4, 0, 1, 4, 0, 1, 4, 2, 4, 2, 5, 5}, {4, 0, 4, 1, 4, 0, 4, 1, 5, 5, 5, 5}, {4, 0, 4, 1, 4, 0, 4, 1, 5, 5, 5, 5}, {4, 0, 4, 1, 4, 0, 4, 1, 5, 5, 5, 5}, {4, 0, 4, 1, 4, 0, 4, 1, 5, 5, 5, 5}, {4, 0, 4, 4, 0, 4, 5, 5, 5, 5, 5, 5}, {4, 4, 4, 0, 4, 5, 5, 5, 5, 5, 5, 5}, {2, 4, 0, 1, 4, 0, 1, 4, 4, 2, 5, 5}, {4, 4, 1, 4, 0, 4, 1, 5, 5, 5, 5, 5} }; static const int prioritysPerMode[10][12] = { {3, 1, 1, 2, 0, 0, 1, 1, 1, 0, 0, 0}, {3, 1, 1, 2, 0, 0, 1, 1, 0, 0, 5, 5}, {3, 1, 2, 1, 1, 0, 0, 0, 5, 5, 5, 5}, {3, 1, 2, 1, 1, 0, 0, 0, 5, 5, 5, 5}, {3, 1, 2, 1, 1, 0, 0, 0, 5, 5, 5, 5}, {3, 1, 2, 1, 1, 0, 0, 0, 5, 5, 5, 5}, {3, 1, 2, 1, 0, 0, 5, 5, 5, 5, 5, 5}, {3, 2, 1, 0, 0, 5, 5, 5, 5, 5, 5, 5}, {1, 3, 1, 1, 2, 0, 0, 1, 0, 0, 5, 5}, {3, 2, 1, 1, 0, 0, 0, 5, 5, 5, 5, 5} }; static const int layerCountPerMode[10] = { 12, 10, 8, 8, 8, 8, 6, 5, 10, 7 }; static const int bitDepthsPerMode[10][4] = { {2, 2, 2, 2}, {4, 4, 2, 5}, {4, 4, 5, 5}, {8, 4, 5, 5}, {8, 2, 5, 5}, {4, 2, 5, 5}, {4, 5, 5, 5}, {8, 5, 5, 5}, {4, 4, 2, 5}, {8, 7, 5, 5} }; static const int spriteSizes[8][2] = { {8, 16}, {8, 32}, {8, 64}, {16, 32}, {16, 64}, {32, 64}, {16, 32}, {16, 32} }; static void ppu_handlePixel(Ppu* ppu, int x, int y); static int ppu_getPixel(Ppu* ppu, int x, int y, bool sub, int* r, int* g, int* b); static uint16_t ppu_getOffsetValue(Ppu* ppu, int col, int row); static int ppu_getPixelForBgLayer(Ppu* ppu, int x, int y, int layer, bool priority); static void ppu_handleOPT(Ppu* ppu, int layer, int* lx, int* ly); static void ppu_calculateMode7Starts(Ppu* ppu, int y); static int ppu_getPixelForMode7(Ppu* ppu, int x, int layer, bool priority); static bool ppu_getWindowState(Ppu* ppu, int layer, int x); static void ppu_evaluateSprites(Ppu* ppu, int line); static uint16_t ppu_getVramRemap(Ppu* ppu); Ppu* ppu_init(Snes* snes) { Ppu* ppu = (Ppu * )malloc(sizeof(Ppu)); ppu->snes = snes; return ppu; } void ppu_free(Ppu* ppu) { free(ppu); } void ppu_reset(Ppu* ppu) { memset(ppu->vram, 0, sizeof(ppu->vram)); ppu->vramPointer = 0; ppu->vramIncrementOnHigh = false; ppu->vramIncrement = 1; ppu->vramRemapMode = 0; ppu->vramReadBuffer = 0; memset(ppu->cgram, 0, sizeof(ppu->cgram)); ppu->cgramPointer = 0; ppu->cgramSecondWrite = false; ppu->cgramBuffer = 0; memset(ppu->oam, 0, sizeof(ppu->oam)); memset(ppu->highOam, 0, sizeof(ppu->highOam)); ppu->oamAdr = 0; ppu->oamAdrWritten = 0; ppu->oamInHigh = false; ppu->oamInHighWritten = false; ppu->oamSecondWrite = false; ppu->oamBuffer = 0; ppu->objPriority = false; ppu->objTileAdr1 = 0; ppu->objTileAdr2 = 0; ppu->objSize = 0; memset(ppu->objPixelBuffer, 0, sizeof(ppu->objPixelBuffer)); memset(ppu->objPriorityBuffer, 0, sizeof(ppu->objPriorityBuffer)); ppu->timeOver = false; ppu->rangeOver = false; ppu->objInterlace = false; for(int i = 0; i < 4; i++) { ppu->bgLayer[i].hScroll = 0; ppu->bgLayer[i].vScroll = 0; ppu->bgLayer[i].tilemapWider = false; ppu->bgLayer[i].tilemapHigher = false; ppu->bgLayer[i].tilemapAdr = 0; ppu->bgLayer[i].tileAdr = 0; ppu->bgLayer[i].bigTiles = false; ppu->bgLayer[i].mosaicEnabled = false; } ppu->scrollPrev = 0; ppu->scrollPrev2 = 0; ppu->mosaicSize = 1; ppu->mosaicStartLine = 1; for(int i = 0; i < 5; i++) { ppu->layer[i].mainScreenEnabled = false; ppu->layer[i].subScreenEnabled = false; ppu->layer[i].mainScreenWindowed = false; ppu->layer[i].subScreenWindowed = false; } memset(ppu->m7matrix, 0, sizeof(ppu->m7matrix)); ppu->m7prev = 0; ppu->m7largeField = false; ppu->m7charFill = false; ppu->m7xFlip = false; ppu->m7yFlip = false; ppu->m7extBg = false; ppu->m7startX = 0; ppu->m7startY = 0; for(int i = 0; i < 6; i++) { ppu->windowLayer[i].window1enabled = false; ppu->windowLayer[i].window2enabled = false; ppu->windowLayer[i].window1inversed = false; ppu->windowLayer[i].window2inversed = false; ppu->windowLayer[i].maskLogic = 0; } ppu->window1left = 0; ppu->window1right = 0; ppu->window2left = 0; ppu->window2right = 0; ppu->clipMode = 0; ppu->preventMathMode = 0; ppu->addSubscreen = false; ppu->subtractColor = false; ppu->halfColor = false; memset(ppu->mathEnabled, 0, sizeof(ppu->mathEnabled)); ppu->fixedColorR = 0; ppu->fixedColorG = 0; ppu->fixedColorB = 0; ppu->forcedBlank = true; ppu->brightness = 0; ppu->mode = 0; ppu->bg3priority = false; ppu->evenFrame = false; ppu->pseudoHires = false; ppu->overscan = false; ppu->frameOverscan = false; ppu->interlace = false; ppu->frameInterlace = false; ppu->directColor = false; ppu->hCount = 0; ppu->vCount = 0; ppu->hCountSecond = false; ppu->vCountSecond = false; ppu->countersLatched = false; ppu->ppu1openBus = 0; ppu->ppu2openBus = 0; memset(ppu->pixelBuffer, 0, sizeof(ppu->pixelBuffer)); } void ppu_saveload(Ppu *ppu, SaveLoadFunc *func, void *ctx) { func(ctx, &ppu->vram, offsetof(Ppu, pixelBuffer) - offsetof(Ppu, vram)); } bool ppu_checkOverscan(Ppu* ppu) { // called at (0,225) ppu->frameOverscan = ppu->overscan; // set if we have a overscan-frame return ppu->frameOverscan; } void ppu_handleVblank(Ppu* ppu) { // called either right after ppu_checkOverscan at (0,225), or at (0,240) if(!ppu->forcedBlank) { ppu->oamAdr = ppu->oamAdrWritten; ppu->oamInHigh = ppu->oamInHighWritten; ppu->oamSecondWrite = false; } ppu->frameInterlace = ppu->interlace; // set if we have a interlaced frame } void ppu_runLine(Ppu* ppu, int line) { if(line == 0) { // pre-render line // TODO: this now happens halfway into the first line ppu->mosaicStartLine = 1; ppu->rangeOver = false; ppu->timeOver = false; ppu->evenFrame = !ppu->evenFrame; } else { // evaluate sprites memset(ppu->objPixelBuffer, 0, sizeof(ppu->objPixelBuffer)); if(!ppu->forcedBlank) ppu_evaluateSprites(ppu, line - 1); // actual line if(ppu->mode == 7) ppu_calculateMode7Starts(ppu, line); for(int x = 0; x < 256; x++) { ppu_handlePixel(ppu, x, line); } } } static void ppu_handlePixel(Ppu* ppu, int x, int y) { int r = 0, r2 = 0; int g = 0, g2 = 0; int b = 0, b2 = 0; if(!ppu->forcedBlank) { int mainLayer = ppu_getPixel(ppu, x, y, false, &r, &g, &b); bool colorWindowState = ppu_getWindowState(ppu, 5, x); if( ppu->clipMode == 3 || (ppu->clipMode == 2 && colorWindowState) || (ppu->clipMode == 1 && !colorWindowState) ) { r = g = b = 0; } int secondLayer = 5; // backdrop bool mathEnabled = mainLayer < 6 && ppu->mathEnabled[mainLayer] && !( ppu->preventMathMode == 3 || (ppu->preventMathMode == 2 && colorWindowState) || (ppu->preventMathMode == 1 && !colorWindowState) ); if((mathEnabled && ppu->addSubscreen) || ppu->pseudoHires || ppu->mode == 5 || ppu->mode == 6) { secondLayer = ppu_getPixel(ppu, x, y, true, &r2, &g2, &b2); } // TODO: subscreen pixels can be clipped to black as well // TODO: math for subscreen pixels (add/sub sub to main) if(mathEnabled) { if(ppu->subtractColor) { r -= (ppu->addSubscreen && secondLayer != 5) ? r2 : ppu->fixedColorR; g -= (ppu->addSubscreen && secondLayer != 5) ? g2 : ppu->fixedColorG; b -= (ppu->addSubscreen && secondLayer != 5) ? b2 : ppu->fixedColorB; } else { r += (ppu->addSubscreen && secondLayer != 5) ? r2 : ppu->fixedColorR; g += (ppu->addSubscreen && secondLayer != 5) ? g2 : ppu->fixedColorG; b += (ppu->addSubscreen && secondLayer != 5) ? b2 : ppu->fixedColorB; } if(ppu->halfColor && (secondLayer != 5 || !ppu->addSubscreen)) { r >>= 1; g >>= 1; b >>= 1; } if(r > 31) r = 31; if(g > 31) g = 31; if(b > 31) b = 31; if(r < 0) r = 0; if(g < 0) g = 0; if(b < 0) b = 0; } if(!(ppu->pseudoHires || ppu->mode == 5 || ppu->mode == 6)) { r2 = r; g2 = g; b2 = b; } } int row = (y - 1) + (ppu->evenFrame ? 0 : 239); ppu->pixelBuffer[row * 2048 + x * 8 + 1] = ((b2 << 3) | (b2 >> 2)) * ppu->brightness / 15; ppu->pixelBuffer[row * 2048 + x * 8 + 2] = ((g2 << 3) | (g2 >> 2)) * ppu->brightness / 15; ppu->pixelBuffer[row * 2048 + x * 8 + 3] = ((r2 << 3) | (r2 >> 2)) * ppu->brightness / 15; ppu->pixelBuffer[row * 2048 + x * 8 + 5] = ((b << 3) | (b >> 2)) * ppu->brightness / 15; ppu->pixelBuffer[row * 2048 + x * 8 + 6] = ((g << 3) | (g >> 2)) * ppu->brightness / 15; ppu->pixelBuffer[row * 2048 + x * 8 + 7] = ((r << 3) | (r >> 2)) * ppu->brightness / 15; } static int ppu_getPixel(Ppu* ppu, int x, int y, bool sub, int* r, int* g, int* b) { // figure out which color is on this location on main- or subscreen, sets it in r, g, b // returns which layer it is: 0-3 for bg layer, 4 or 6 for sprites (depending on palette), 5 for backdrop int actMode = ppu->mode == 1 && ppu->bg3priority ? 8 : ppu->mode; actMode = ppu->mode == 7 && ppu->m7extBg ? 9 : actMode; int layer = 5; int pixel = 0; for(int i = 0; i < layerCountPerMode[actMode]; i++) { int curLayer = layersPerMode[actMode][i]; int curPriority = prioritysPerMode[actMode][i]; bool layerActive = false; if(!sub) { layerActive = ppu->layer[curLayer].mainScreenEnabled && ( !ppu->layer[curLayer].mainScreenWindowed || !ppu_getWindowState(ppu, curLayer, x) ); } else { layerActive = ppu->layer[curLayer].subScreenEnabled && ( !ppu->layer[curLayer].subScreenWindowed || !ppu_getWindowState(ppu, curLayer, x) ); } if(layerActive) { if(curLayer < 4) { // bg layer int lx = x; int ly = y; if(ppu->bgLayer[curLayer].mosaicEnabled && ppu->mosaicSize > 1) { lx -= lx % ppu->mosaicSize; ly -= (ly - ppu->mosaicStartLine) % ppu->mosaicSize; } if(ppu->mode == 7) { pixel = ppu_getPixelForMode7(ppu, lx, curLayer, curPriority); } else { lx += ppu->bgLayer[curLayer].hScroll; if(ppu->mode == 5 || ppu->mode == 6) { lx *= 2; lx += (sub || ppu->bgLayer[curLayer].mosaicEnabled) ? 0 : 1; if(ppu->interlace) { ly *= 2; ly += (ppu->evenFrame || ppu->bgLayer[curLayer].mosaicEnabled) ? 0 : 1; } } ly += ppu->bgLayer[curLayer].vScroll; if(ppu->mode == 2 || ppu->mode == 4 || ppu->mode == 6) { ppu_handleOPT(ppu, curLayer, &lx, &ly); } pixel = ppu_getPixelForBgLayer( ppu, lx & 0x3ff, ly & 0x3ff, curLayer, curPriority ); } } else { // get a pixel from the sprite buffer pixel = 0; if(ppu->objPriorityBuffer[x] == curPriority) pixel = ppu->objPixelBuffer[x]; } } if(pixel > 0) { layer = curLayer; break; } } if(ppu->directColor && layer < 4 && bitDepthsPerMode[actMode][layer] == 8) { *r = ((pixel & 0x7) << 2) | ((pixel & 0x100) >> 7); *g = ((pixel & 0x38) >> 1) | ((pixel & 0x200) >> 8); *b = ((pixel & 0xc0) >> 3) | ((pixel & 0x400) >> 8); } else { uint16_t color = ppu->cgram[pixel & 0xff]; *r = color & 0x1f; *g = (color >> 5) & 0x1f; *b = (color >> 10) & 0x1f; } if(layer == 4 && pixel < 0xc0) layer = 6; // sprites with palette color < 0xc0 return layer; } static void ppu_handleOPT(Ppu* ppu, int layer, int* lx, int* ly) { int x = *lx; int y = *ly; int column = 0; if(ppu->mode == 6) { column = ((x - (x & 0xf)) - ((ppu->bgLayer[layer].hScroll * 2) & 0xfff0)) >> 4; } else { column = ((x - (x & 0x7)) - (ppu->bgLayer[layer].hScroll & 0xfff8)) >> 3; } if(column > 0) { // fetch offset values from layer 3 tilemap int valid = layer == 0 ? 0x2000 : 0x4000; uint16_t hOffset = ppu_getOffsetValue(ppu, column - 1, 0); uint16_t vOffset = 0; if(ppu->mode == 4) { if(hOffset & 0x8000) { vOffset = hOffset; hOffset = 0; } } else { vOffset = ppu_getOffsetValue(ppu, column - 1, 1); } if(ppu->mode == 6) { // TODO: not sure if correct if(hOffset & valid) *lx = (((hOffset & 0x3f8) + (column * 8)) * 2) | (x & 0xf); } else { if(hOffset & valid) *lx = ((hOffset & 0x3f8) + (column * 8)) | (x & 0x7); } // TODO: not sure if correct for interlace if(vOffset & valid) *ly = (vOffset & 0x3ff) + (y - ppu->bgLayer[layer].vScroll); } } static uint16_t ppu_getOffsetValue(Ppu* ppu, int col, int row) { int x = col * 8 + ppu->bgLayer[2].hScroll; int y = row * 8 + ppu->bgLayer[2].vScroll; int tileBits = ppu->bgLayer[2].bigTiles ? 4 : 3; int tileHighBit = ppu->bgLayer[2].bigTiles ? 0x200 : 0x100; uint16_t tilemapAdr = ppu->bgLayer[2].tilemapAdr + (((y >> tileBits) & 0x1f) << 5 | ((x >> tileBits) & 0x1f)); if((x & tileHighBit) && ppu->bgLayer[2].tilemapWider) tilemapAdr += 0x400; if((y & tileHighBit) && ppu->bgLayer[2].tilemapHigher) tilemapAdr += ppu->bgLayer[2].tilemapWider ? 0x800 : 0x400; return ppu->vram[tilemapAdr & 0x7fff]; } static int ppu_getPixelForBgLayer(Ppu* ppu, int x, int y, int layer, bool priority) { BgLayer *layerp = &ppu->bgLayer[layer]; // figure out address of tilemap word and read it bool wideTiles = layerp->bigTiles || ppu->mode == 5 || ppu->mode == 6; int tileBitsX = wideTiles ? 4 : 3; int tileHighBitX = wideTiles ? 0x200 : 0x100; int tileBitsY = layerp->bigTiles ? 4 : 3; int tileHighBitY = layerp->bigTiles ? 0x200 : 0x100; uint16_t tilemapAdr = layerp->tilemapAdr + (((y >> tileBitsY) & 0x1f) << 5 | ((x >> tileBitsX) & 0x1f)); if((x & tileHighBitX) && layerp->tilemapWider) tilemapAdr += 0x400; if((y & tileHighBitY) && layerp->tilemapHigher) tilemapAdr += layerp->tilemapWider ? 0x800 : 0x400; uint16_t tile = ppu->vram[tilemapAdr & 0x7fff]; // check priority, get palette if(((bool) (tile & 0x2000)) != priority) return 0; // wrong priority int paletteNum = (tile & 0x1c00) >> 10; // figure out position within tile int row = (tile & 0x8000) ? 7 - (y & 0x7) : (y & 0x7); int col = (tile & 0x4000) ? (x & 0x7) : 7 - (x & 0x7); int tileNum = tile & 0x3ff; if(wideTiles) { // if unflipped right half of tile, or flipped left half of tile if(((bool) (x & 8)) ^ ((bool) (tile & 0x4000))) tileNum += 1; } if(layerp->bigTiles) { // if unflipped bottom half of tile, or flipped upper half of tile if(((bool) (y & 8)) ^ ((bool) (tile & 0x8000))) tileNum += 0x10; } // read tiledata, ajust palette for mode 0 int bitDepth = bitDepthsPerMode[ppu->mode][layer]; if(ppu->mode == 0) paletteNum += 8 * layer; // plane 1 (always) int paletteSize = 4; uint16_t plane1 = ppu->vram[(layerp->tileAdr + ((tileNum & 0x3ff) * 4 * bitDepth) + row) & 0x7fff]; int pixel = (plane1 >> col) & 1; pixel |= ((plane1 >> (8 + col)) & 1) << 1; // plane 2 (for 4bpp, 8bpp) if(bitDepth > 2) { paletteSize = 16; uint16_t plane2 = ppu->vram[(layerp->tileAdr + ((tileNum & 0x3ff) * 4 * bitDepth) + 8 + row) & 0x7fff]; pixel |= ((plane2 >> col) & 1) << 2; pixel |= ((plane2 >> (8 + col)) & 1) << 3; } // plane 3 & 4 (for 8bpp) if(bitDepth > 4) { paletteSize = 256; uint16_t plane3 = ppu->vram[(layerp->tileAdr + ((tileNum & 0x3ff) * 4 * bitDepth) + 16 + row) & 0x7fff]; pixel |= ((plane3 >> col) & 1) << 4; pixel |= ((plane3 >> (8 + col)) & 1) << 5; uint16_t plane4 = ppu->vram[(layerp->tileAdr + ((tileNum & 0x3ff) * 4 * bitDepth) + 24 + row) & 0x7fff]; pixel |= ((plane4 >> col) & 1) << 6; pixel |= ((plane4 >> (8 + col)) & 1) << 7; } // return cgram index, or 0 if transparent, palette number in bits 10-8 for 8-color layers return pixel == 0 ? 0 : paletteSize * paletteNum + pixel; } static void ppu_calculateMode7Starts(Ppu* ppu, int y) { // expand 13-bit values to signed values int hScroll = ((int16_t) (ppu->m7matrix[6] << 3)) >> 3; int vScroll = ((int16_t) (ppu->m7matrix[7] << 3)) >> 3; int xCenter = ((int16_t) (ppu->m7matrix[4] << 3)) >> 3; int yCenter = ((int16_t) (ppu->m7matrix[5] << 3)) >> 3; // do calculation int clippedH = hScroll - xCenter; int clippedV = vScroll - yCenter; clippedH = (clippedH & 0x2000) ? (clippedH | ~1023) : (clippedH & 1023); clippedV = (clippedV & 0x2000) ? (clippedV | ~1023) : (clippedV & 1023); if(ppu->bgLayer[0].mosaicEnabled && ppu->mosaicSize > 1) { y -= (y - ppu->mosaicStartLine) % ppu->mosaicSize; } uint8_t ry = ppu->m7yFlip ? 255 - y : y; ppu->m7startX = ( ((ppu->m7matrix[0] * clippedH) & ~63) + ((ppu->m7matrix[1] * ry) & ~63) + ((ppu->m7matrix[1] * clippedV) & ~63) + (xCenter << 8) ); ppu->m7startY = ( ((ppu->m7matrix[2] * clippedH) & ~63) + ((ppu->m7matrix[3] * ry) & ~63) + ((ppu->m7matrix[3] * clippedV) & ~63) + (yCenter << 8) ); } static int ppu_getPixelForMode7(Ppu* ppu, int x, int layer, bool priority) { uint8_t rx = ppu->m7xFlip ? 255 - x : x; int xPos = (ppu->m7startX + ppu->m7matrix[0] * rx) >> 8; int yPos = (ppu->m7startY + ppu->m7matrix[2] * rx) >> 8; bool outsideMap = xPos < 0 || xPos >= 1024 || yPos < 0 || yPos >= 1024; xPos &= 0x3ff; yPos &= 0x3ff; if(!ppu->m7largeField) outsideMap = false; uint8_t tile = outsideMap ? 0 : ppu->vram[(yPos >> 3) * 128 + (xPos >> 3)] & 0xff; uint8_t pixel = outsideMap && !ppu->m7charFill ? 0 : ppu->vram[tile * 64 + (yPos & 7) * 8 + (xPos & 7)] >> 8; if(layer == 1) { if(((bool) (pixel & 0x80)) != priority) return 0; return pixel & 0x7f; } return pixel; } static bool ppu_getWindowState(Ppu* ppu, int layer, int x) { if(!ppu->windowLayer[layer].window1enabled && !ppu->windowLayer[layer].window2enabled) { return false; } if(ppu->windowLayer[layer].window1enabled && !ppu->windowLayer[layer].window2enabled) { bool test = x >= ppu->window1left && x <= ppu->window1right; return ppu->windowLayer[layer].window1inversed ? !test : test; } if(!ppu->windowLayer[layer].window1enabled && ppu->windowLayer[layer].window2enabled) { bool test = x >= ppu->window2left && x <= ppu->window2right; return ppu->windowLayer[layer].window2inversed ? !test : test; } bool test1 = x >= ppu->window1left && x <= ppu->window1right; bool test2 = x >= ppu->window2left && x <= ppu->window2right; if(ppu->windowLayer[layer].window1inversed) test1 = !test1; if(ppu->windowLayer[layer].window2inversed) test2 = !test2; switch(ppu->windowLayer[layer].maskLogic) { case 0: return test1 || test2; case 1: return test1 && test2; case 2: return test1 != test2; case 3: return test1 == test2; } return false; } static void ppu_evaluateSprites(Ppu* ppu, int line) { // TODO: iterate over oam normally to determine in-range sprites, // then iterate those in-range sprites in reverse for tile-fetching // TODO: rectangular sprites, wierdness with sprites at -256 uint8_t index = ppu->objPriority ? (ppu->oamAdr & 0xfe) : 0; int spritesFound = 0; int tilesFound = 0; for(int i = 0; i < 128; i++) { uint8_t y = ppu->oam[index] >> 8; // check if the sprite is on this line and get the sprite size uint8_t row = line - y; int spriteSize = spriteSizes[ppu->objSize][(ppu->highOam[index >> 3] >> ((index & 7) + 1)) & 1]; int spriteHeight = ppu->objInterlace ? spriteSize / 2 : spriteSize; if(row < spriteHeight) { // in y-range, get the x location, using the high bit as well int x = ppu->oam[index] & 0xff; x |= ((ppu->highOam[index >> 3] >> (index & 7)) & 1) << 8; if(x > 255) x -= 512; // if in x-range if(x > -spriteSize) { // break if we found 32 sprites already spritesFound++; if(spritesFound > 32) { ppu->rangeOver = true; break; } // update row according to obj-interlace if(ppu->objInterlace) row = row * 2 + (ppu->evenFrame ? 0 : 1); // get some data for the sprite and y-flip row if needed int tile = ppu->oam[index + 1] & 0xff; int palette = (ppu->oam[index + 1] & 0xe00) >> 9; bool hFlipped = ppu->oam[index + 1] & 0x4000; if(ppu->oam[index + 1] & 0x8000) row = spriteSize - 1 - row; // fetch all tiles in x-range for(int col = 0; col < spriteSize; col += 8) { if(col + x > -8 && col + x < 256) { // break if we found 34 8*1 slivers already tilesFound++; if(tilesFound > 34) { ppu->timeOver = true; break; } // figure out which tile this uses, looping within 16x16 pages, and get it's data int usedCol = hFlipped ? spriteSize - 1 - col : col; uint8_t usedTile = (((tile >> 4) + (row / 8)) << 4) | (((tile & 0xf) + (usedCol / 8)) & 0xf); uint16_t objAdr = (ppu->oam[index + 1] & 0x100) ? ppu->objTileAdr2 : ppu->objTileAdr1; uint16_t plane1 = ppu->vram[(objAdr + usedTile * 16 + (row & 0x7)) & 0x7fff]; uint16_t plane2 = ppu->vram[(objAdr + usedTile * 16 + 8 + (row & 0x7)) & 0x7fff]; // go over each pixel for(int px = 0; px < 8; px++) { int shift = hFlipped ? px : 7 - px; int pixel = (plane1 >> shift) & 1; pixel |= ((plane1 >> (8 + shift)) & 1) << 1; pixel |= ((plane2 >> shift) & 1) << 2; pixel |= ((plane2 >> (8 + shift)) & 1) << 3; // draw it in the buffer if there is a pixel here, and the buffer there is still empty int screenCol = col + x + px; if(pixel > 0 && screenCol >= 0 && screenCol < 256 && ppu->objPixelBuffer[screenCol] == 0) { ppu->objPixelBuffer[screenCol] = 0x80 + 16 * palette + pixel; ppu->objPriorityBuffer[screenCol] = (ppu->oam[index + 1] & 0x3000) >> 12; } } } } if(tilesFound > 34) break; // break out of sprite-loop if max tiles found } } index += 2; } } static uint16_t ppu_getVramRemap(Ppu* ppu) { uint16_t adr = ppu->vramPointer; switch(ppu->vramRemapMode) { case 0: return adr; case 1: return (adr & 0xff00) | ((adr & 0xe0) >> 5) | ((adr & 0x1f) << 3); case 2: return (adr & 0xfe00) | ((adr & 0x1c0) >> 6) | ((adr & 0x3f) << 3); case 3: return (adr & 0xfc00) | ((adr & 0x380) >> 7) | ((adr & 0x7f) << 3); } return adr; } uint8_t ppu_read(Ppu* ppu, uint8_t adr) { switch(adr) { case 0x04: case 0x14: case 0x24: case 0x05: case 0x15: case 0x25: case 0x06: case 0x16: case 0x26: case 0x08: case 0x18: case 0x28: case 0x09: case 0x19: case 0x29: case 0x0a: case 0x1a: case 0x2a: { return ppu->ppu1openBus; } case 0x34: case 0x35: case 0x36: { int result = ppu->m7matrix[0] * (ppu->m7matrix[1] >> 8); ppu->ppu1openBus = (result >> (8 * (adr - 0x34))) & 0xff; return ppu->ppu1openBus; } case 0x37: { // TODO: only when ppulatch is set ppu->hCount = ppu->snes->hPos / 4; ppu->vCount = ppu->snes->vPos; ppu->countersLatched = true; if (ppu->snes->disableHpos) ppu->vCount = 192; return ppu->snes->openBus; } case 0x38: { uint8_t ret = 0; if(ppu->oamInHigh) { ret = ppu->highOam[((ppu->oamAdr & 0xf) << 1) | (uint8_t)ppu->oamSecondWrite]; if(ppu->oamSecondWrite) { ppu->oamAdr++; if(ppu->oamAdr == 0) ppu->oamInHigh = false; } } else { if(!ppu->oamSecondWrite) { ret = ppu->oam[ppu->oamAdr] & 0xff; } else { ret = ppu->oam[ppu->oamAdr++] >> 8; if(ppu->oamAdr == 0) ppu->oamInHigh = true; } } ppu->oamSecondWrite = !ppu->oamSecondWrite; ppu->ppu1openBus = ret; return ret; } case 0x39: { uint16_t val = ppu->vramReadBuffer; if(!ppu->vramIncrementOnHigh) { ppu->vramReadBuffer = ppu->vram[ppu_getVramRemap(ppu) & 0x7fff]; ppu->vramPointer += ppu->vramIncrement; } ppu->ppu1openBus = val & 0xff; return val & 0xff; } case 0x3a: { uint16_t val = ppu->vramReadBuffer; if(ppu->vramIncrementOnHigh) { ppu->vramReadBuffer = ppu->vram[ppu_getVramRemap(ppu) & 0x7fff]; ppu->vramPointer += ppu->vramIncrement; } ppu->ppu1openBus = val >> 8; return val >> 8; } case 0x3b: { uint8_t ret = 0; if(!ppu->cgramSecondWrite) { ret = ppu->cgram[ppu->cgramPointer] & 0xff; } else { ret = ((ppu->cgram[ppu->cgramPointer++] >> 8) & 0x7f) | (ppu->ppu2openBus & 0x80); } ppu->cgramSecondWrite = !ppu->cgramSecondWrite; ppu->ppu2openBus = ret; return ret; } case 0x3c: { #if 0 uint8_t val = 0; if(ppu->hCountSecond) { val = ((ppu->hCount >> 8) & 1) | (ppu->ppu2openBus & 0xfe); } else { val = ppu->hCount & 0xff; } #else uint8_t val = 0x17;// (ppu->ppu2openBus + ppu->cgramPointer * 7) * 0x31337 >> 8; #endif ppu->hCountSecond = !ppu->hCountSecond; ppu->ppu2openBus = val; return val; } case 0x3d: { uint8_t val = 0; uint16_t vCount = 192;// ppu->vCount if(ppu->vCountSecond) { val = ((vCount >> 8) & 1) | (ppu->ppu2openBus & 0xfe); } else { val = vCount & 0xff; } ppu->vCountSecond = !ppu->vCountSecond; ppu->ppu2openBus = val; return val; } case 0x3e: { uint8_t val = 0x1; // ppu1 version (4 bit) val |= ppu->ppu1openBus & 0x10; val |= ppu->rangeOver << 6; val |= ppu->timeOver << 7; ppu->ppu1openBus = val; return val; } case 0x3f: { uint8_t val = 0x3; // ppu2 version (4 bit), bit 4: ntsc/pal val |= ppu->ppu2openBus & 0x20; val |= ppu->countersLatched << 6; val |= ppu->evenFrame << 7; ppu->countersLatched = false; // TODO: only when ppulatch is set ppu->hCountSecond = false; ppu->vCountSecond = false; ppu->ppu2openBus = val; return val; } default: { return ppu->snes->openBus; } } } void ppu_write(Ppu* ppu, uint8_t adr, uint8_t val) { switch(adr) { case 0x00: { // TODO: oam address reset when written on first line of vblank, (and when forced blank is disabled?) ppu->brightness = val & 0xf; ppu->forcedBlank = val & 0x80; break; } case 0x01: { ppu->objSize = val >> 5; ppu->objTileAdr1 = (val & 7) << 13; ppu->objTileAdr2 = ppu->objTileAdr1 + (((val & 0x18) + 8) << 9); break; } case 0x02: { ppu->oamAdr = val; ppu->oamAdrWritten = ppu->oamAdr; ppu->oamInHigh = ppu->oamInHighWritten; ppu->oamSecondWrite = false; break; } case 0x03: { ppu->objPriority = val & 0x80; ppu->oamInHigh = val & 1; ppu->oamInHighWritten = ppu->oamInHigh; ppu->oamAdr = ppu->oamAdrWritten; ppu->oamSecondWrite = false; break; } case 0x04: { if(ppu->oamInHigh) { ppu->highOam[((ppu->oamAdr & 0xf) << 1) | (uint8_t)ppu->oamSecondWrite] = val; if(ppu->oamSecondWrite) { ppu->oamAdr++; if(ppu->oamAdr == 0) ppu->oamInHigh = false; } } else { if(!ppu->oamSecondWrite) { ppu->oamBuffer = val; } else { ppu->oam[ppu->oamAdr++] = (val << 8) | ppu->oamBuffer; if(ppu->oamAdr == 0) ppu->oamInHigh = true; } } ppu->oamSecondWrite = !ppu->oamSecondWrite; break; } case 0x05: { ppu->mode = val & 0x7; ppu->bg3priority = val & 0x8; ppu->bgLayer[0].bigTiles = val & 0x10; ppu->bgLayer[1].bigTiles = val & 0x20; ppu->bgLayer[2].bigTiles = val & 0x40; ppu->bgLayer[3].bigTiles = val & 0x80; break; } case 0x06: { // TODO: mosaic line reset specifics ppu->bgLayer[0].mosaicEnabled = val & 0x1; ppu->bgLayer[1].mosaicEnabled = val & 0x2; ppu->bgLayer[2].mosaicEnabled = val & 0x4; ppu->bgLayer[3].mosaicEnabled = val & 0x8; ppu->mosaicSize = (val >> 4) + 1; ppu->mosaicStartLine = 0;// ppu->snes->vPos; break; } case 0x07: case 0x08: case 0x09: case 0x0a: { ppu->bgLayer[adr - 7].tilemapWider = val & 0x1; ppu->bgLayer[adr - 7].tilemapHigher = val & 0x2; ppu->bgLayer[adr - 7].tilemapAdr = (val & 0xfc) << 8; break; } case 0x0b: { ppu->bgLayer[0].tileAdr = (val & 0xf) << 12; ppu->bgLayer[1].tileAdr = (val & 0xf0) << 8; break; } case 0x0c: { ppu->bgLayer[2].tileAdr = (val & 0xf) << 12; ppu->bgLayer[3].tileAdr = (val & 0xf0) << 8; break; } case 0x0d: { ppu->m7matrix[6] = ((val << 8) | ppu->m7prev) & 0x1fff; ppu->m7prev = val; // fallthrough to normal layer BG-HOFS } case 0x0f: case 0x11: case 0x13: { ppu->bgLayer[(adr - 0xd) / 2].hScroll = ((val << 8) | (ppu->scrollPrev & 0xf8) | (ppu->scrollPrev2 & 0x7)) & 0x3ff; ppu->scrollPrev = val; ppu->scrollPrev2 = val; break; } case 0x0e: { ppu->m7matrix[7] = ((val << 8) | ppu->m7prev) & 0x1fff; ppu->m7prev = val; // fallthrough to normal layer BG-VOFS } case 0x10: case 0x12: case 0x14: { ppu->bgLayer[(adr - 0xe) / 2].vScroll = ((val << 8) | ppu->scrollPrev) & 0x3ff; ppu->scrollPrev = val; break; } case 0x15: { if((val & 3) == 0) { ppu->vramIncrement = 1; } else if((val & 3) == 1) { ppu->vramIncrement = 32; } else { ppu->vramIncrement = 128; } ppu->vramRemapMode = (val & 0xc) >> 2; ppu->vramIncrementOnHigh = val & 0x80; break; } case 0x16: { ppu->vramPointer = (ppu->vramPointer & 0xff00) | val; ppu->vramReadBuffer = ppu->vram[ppu_getVramRemap(ppu) & 0x7fff]; break; } case 0x17: { ppu->vramPointer = (ppu->vramPointer & 0x00ff) | (val << 8); ppu->vramReadBuffer = ppu->vram[ppu_getVramRemap(ppu) & 0x7fff]; break; } case 0x18: { // TODO: vram access during rendering (also cgram and oam) uint16_t vramAdr = ppu_getVramRemap(ppu); if (val != 0xef) { val += 0; } ppu->vram[vramAdr & 0x7fff] = (ppu->vram[vramAdr & 0x7fff] & 0xff00) | val; if(!ppu->vramIncrementOnHigh) ppu->vramPointer += ppu->vramIncrement; break; } case 0x19: { uint16_t vramAdr = ppu_getVramRemap(ppu); ppu->vram[vramAdr & 0x7fff] = (ppu->vram[vramAdr & 0x7fff] & 0x00ff) | (val << 8); if(ppu->vramIncrementOnHigh) ppu->vramPointer += ppu->vramIncrement; break; } case 0x1a: { ppu->m7largeField = val & 0x80; ppu->m7charFill = val & 0x40; ppu->m7yFlip = val & 0x2; ppu->m7xFlip = val & 0x1; break; } case 0x1b: case 0x1c: case 0x1d: case 0x1e: { ppu->m7matrix[adr - 0x1b] = (val << 8) | ppu->m7prev; ppu->m7prev = val; break; } case 0x1f: case 0x20: { ppu->m7matrix[adr - 0x1b] = ((val << 8) | ppu->m7prev) & 0x1fff; ppu->m7prev = val; break; } case 0x21: { ppu->cgramPointer = val; ppu->cgramSecondWrite = false; break; } case 0x22: { if(!ppu->cgramSecondWrite) { ppu->cgramBuffer = val; } else { ppu->cgram[ppu->cgramPointer++] = (val << 8) | ppu->cgramBuffer; } ppu->cgramSecondWrite = !ppu->cgramSecondWrite; break; } case 0x23: case 0x24: case 0x25: { ppu->windowLayer[(adr - 0x23) * 2].window1inversed = val & 0x1; ppu->windowLayer[(adr - 0x23) * 2].window1enabled = val & 0x2; ppu->windowLayer[(adr - 0x23) * 2].window2inversed = val & 0x4; ppu->windowLayer[(adr - 0x23) * 2].window2enabled = val & 0x8; ppu->windowLayer[(adr - 0x23) * 2 + 1].window1inversed = val & 0x10; ppu->windowLayer[(adr - 0x23) * 2 + 1].window1enabled = val & 0x20; ppu->windowLayer[(adr - 0x23) * 2 + 1].window2inversed = val & 0x40; ppu->windowLayer[(adr - 0x23) * 2 + 1].window2enabled = val & 0x80; break; } case 0x26: { ppu->window1left = val; break; } case 0x27: { ppu->window1right = val; break; } case 0x28: { ppu->window2left = val; break; } case 0x29: { ppu->window2right = val; break; } case 0x2a: { ppu->windowLayer[0].maskLogic = val & 0x3; ppu->windowLayer[1].maskLogic = (val >> 2) & 0x3; ppu->windowLayer[2].maskLogic = (val >> 4) & 0x3; ppu->windowLayer[3].maskLogic = (val >> 6) & 0x3; break; } case 0x2b: { ppu->windowLayer[4].maskLogic = val & 0x3; ppu->windowLayer[5].maskLogic = (val >> 2) & 0x3; break; } case 0x2c: { ppu->layer[0].mainScreenEnabled = val & 0x1; ppu->layer[1].mainScreenEnabled = val & 0x2; ppu->layer[2].mainScreenEnabled = val & 0x4; ppu->layer[3].mainScreenEnabled = val & 0x8; ppu->layer[4].mainScreenEnabled = val & 0x10; break; } case 0x2d: { ppu->layer[0].subScreenEnabled = val & 0x1; ppu->layer[1].subScreenEnabled = val & 0x2; ppu->layer[2].subScreenEnabled = val & 0x4; ppu->layer[3].subScreenEnabled = val & 0x8; ppu->layer[4].subScreenEnabled = val & 0x10; break; } case 0x2e: { ppu->layer[0].mainScreenWindowed = val & 0x1; ppu->layer[1].mainScreenWindowed = val & 0x2; ppu->layer[2].mainScreenWindowed = val & 0x4; ppu->layer[3].mainScreenWindowed = val & 0x8; ppu->layer[4].mainScreenWindowed = val & 0x10; break; } case 0x2f: { ppu->layer[0].subScreenWindowed = val & 0x1; ppu->layer[1].subScreenWindowed = val & 0x2; ppu->layer[2].subScreenWindowed = val & 0x4; ppu->layer[3].subScreenWindowed = val & 0x8; ppu->layer[4].subScreenWindowed = val & 0x10; break; } case 0x30: { ppu->directColor = val & 0x1; ppu->addSubscreen = val & 0x2; ppu->preventMathMode = (val & 0x30) >> 4; ppu->clipMode = (val & 0xc0) >> 6; break; } case 0x31: { ppu->subtractColor = val & 0x80; ppu->halfColor = val & 0x40; for(int i = 0; i < 6; i++) { ppu->mathEnabled[i] = val & (1 << i); } break; } case 0x32: { if(val & 0x80) ppu->fixedColorB = val & 0x1f; if(val & 0x40) ppu->fixedColorG = val & 0x1f; if(val & 0x20) ppu->fixedColorR = val & 0x1f; break; } case 0x33: { ppu->interlace = val & 0x1; ppu->objInterlace = val & 0x2; ppu->overscan = val & 0x4; ppu->pseudoHires = val & 0x8; ppu->m7extBg = val & 0x40; break; } default: { break; } } } void ppu_putPixels(Ppu* ppu, uint8_t* pixels) { for(int y = 0; y < (ppu->frameOverscan ? 239 : 224); y++) { int dest = y * 2 + (ppu->frameOverscan ? 2 : 16); int y1 = y, y2 = y + 239; if(!ppu->frameInterlace) { y1 = y + (ppu->evenFrame ? 0 : 239); y2 = y1; } memcpy(pixels + (dest * 2048), &ppu->pixelBuffer[y1 * 2048], 2048); memcpy(pixels + ((dest + 1) * 2048), &ppu->pixelBuffer[y2 * 2048], 2048); } // clear top 2 lines, and following 14 and last 16 lines if not overscanning memset(pixels, 0, 2048 * 2); if(!ppu->frameOverscan) { memset(pixels + (2 * 2048), 0, 2048 * 14); memset(pixels + (464 * 2048), 0, 2048 * 16); } }