UNPKG

@soapbox.pub/wasmboy

Version:

Soapbox fork of Wasmboy.

349 lines (301 loc) 14.5 kB
// Main Class and funcitons for rendering the gameboy display import { FRAME_LOCATION, GAMEBOY_INTERNAL_MEMORY_LOCATION } from '../constants'; import { getSaveStateMemoryOffset } from '../core'; import { Lcd, setLcdStatus } from './lcd'; import { renderBackground, renderWindow } from './backgroundWindow'; import { renderSprites } from './sprites'; import { clearPriorityMap } from './priority'; import { resetTileCache } from './tiles'; import { initializeColors } from './colors'; import { Cpu } from '../cpu/index'; import { Config } from '../config'; import { Memory, eightBitLoadFromGBMemory, eightBitStoreIntoGBMemory, loadBooleanDirectlyFromWasmMemory, storeBooleanDirectlyToWasmMemory } from '../memory/index'; export class Graphics { // Current cycles // This will be used for batch processing static currentCycles: i32 = 0; // Number of cycles to run in each batch process // This number should be in sync so that graphics doesn't run too many cyles at once // and does not exceed the minimum number of cyles for either scanlines, or // How often we change the frame, or a channel's update process static batchProcessCycles(): i32 { return Graphics.MAX_CYCLES_PER_SCANLINE(); } // Count the number of cycles to keep synced with cpu cycles // Found GBC cycles by finding clock speed from Gb Cycles // See TCAGBD For cycles static scanlineCycleCounter: i32 = 0x00; // TCAGBD says 456 per scanline, but 153 only a handful static MAX_CYCLES_PER_SCANLINE(): i32 { if (Graphics.scanlineRegister === 153) { return 4 << (<i32>Cpu.GBCDoubleSpeed); } else { return 456 << (<i32>Cpu.GBCDoubleSpeed); } } static MIN_CYCLES_SPRITES_LCD_MODE(): i32 { // TODO: Confirm these clock cyles, double similar to scanline, which TCAGBD did return 376 << (<i32>Cpu.GBCDoubleSpeed); } static MIN_CYCLES_TRANSFER_DATA_LCD_MODE(): i32 { // TODO: Confirm these clock cyles, double similar to scanline, which TCAGBD did return 249 << (<i32>Cpu.GBCDoubleSpeed); } // LCD // scanlineRegister also known as LY // See: http://bgb.bircd.org/pandocs.txt , and search " LY " static readonly memoryLocationScanlineRegister: i32 = 0xff44; static scanlineRegister: i32 = 0; static readonly memoryLocationDmaTransfer: i32 = 0xff46; // Scroll and Window static readonly memoryLocationScrollX: i32 = 0xff43; static scrollX: i32 = 0; static readonly memoryLocationScrollY: i32 = 0xff42; static scrollY: i32 = 0; static readonly memoryLocationWindowX: i32 = 0xff4b; static windowX: i32 = 0; static readonly memoryLocationWindowY: i32 = 0xff4a; static windowY: i32 = 0; // Tile Maps And Data static readonly memoryLocationTileMapSelectZeroStart: i32 = 0x9800; static readonly memoryLocationTileMapSelectOneStart: i32 = 0x9c00; static readonly memoryLocationTileDataSelectZeroStart: i32 = 0x8800; static readonly memoryLocationTileDataSelectOneStart: i32 = 0x8000; // Sprites static readonly memoryLocationSpriteAttributesTable: i32 = 0xfe00; // Palettes static readonly memoryLocationBackgroundPalette: i32 = 0xff47; static readonly memoryLocationSpritePaletteOne: i32 = 0xff48; static readonly memoryLocationSpritePaletteTwo: i32 = 0xff49; // Screen data needs to be stored in wasm memory // Save States static readonly saveStateSlot: i32 = 1; // Function to save the state of the class static saveState(): void { // Graphics store<i32>(getSaveStateMemoryOffset(0x00, Graphics.saveStateSlot), Graphics.scanlineCycleCounter); store<u8>(getSaveStateMemoryOffset(0x04, Graphics.saveStateSlot), <u8>Graphics.scanlineRegister); store<u8>(getSaveStateMemoryOffset(0x05, Graphics.saveStateSlot), <u8>Graphics.scrollX); store<u8>(getSaveStateMemoryOffset(0x06, Graphics.saveStateSlot), <u8>Graphics.scrollY); store<u8>(getSaveStateMemoryOffset(0x07, Graphics.saveStateSlot), <u8>Graphics.windowX); store<u8>(getSaveStateMemoryOffset(0x08, Graphics.saveStateSlot), <u8>Graphics.windowY); // LCD store<u8>(getSaveStateMemoryOffset(0x09, Graphics.saveStateSlot), <u8>Lcd.currentLcdMode); store<u8>(getSaveStateMemoryOffset(0x0a, Graphics.saveStateSlot), <u8>Lcd.coincidenceCompare); storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x0b, Graphics.saveStateSlot), Lcd.enabled); storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x0c, Graphics.saveStateSlot), Lcd.windowTileMapDisplaySelect); storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x0d, Graphics.saveStateSlot), Lcd.windowDisplayEnabled); storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x0e, Graphics.saveStateSlot), Lcd.bgWindowTileDataSelect); storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x0f, Graphics.saveStateSlot), Lcd.bgTileMapDisplaySelect); storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x10, Graphics.saveStateSlot), Lcd.tallSpriteSize); storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x11, Graphics.saveStateSlot), Lcd.spriteDisplayEnable); storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x12, Graphics.saveStateSlot), Lcd.bgDisplayEnabled); } // Function to load the save state from memory static loadState(): void { // Graphics Graphics.scanlineCycleCounter = load<i32>(getSaveStateMemoryOffset(0x00, Graphics.saveStateSlot)); Graphics.scanlineRegister = load<u8>(getSaveStateMemoryOffset(0x04, Graphics.scanlineRegister)); Graphics.scrollX = load<u8>(getSaveStateMemoryOffset(0x05, Graphics.saveStateSlot)); Graphics.scrollY = load<u8>(getSaveStateMemoryOffset(0x06, Graphics.saveStateSlot)); Graphics.windowX = load<u8>(getSaveStateMemoryOffset(0x07, Graphics.saveStateSlot)); Graphics.windowY = load<u8>(getSaveStateMemoryOffset(0x08, Graphics.saveStateSlot)); // LCD Lcd.currentLcdMode = load<u8>(getSaveStateMemoryOffset(0x09, Graphics.saveStateSlot)); Lcd.coincidenceCompare = load<u8>(getSaveStateMemoryOffset(0x0a, Graphics.saveStateSlot)); Lcd.enabled = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x0b, Graphics.saveStateSlot)); Lcd.windowTileMapDisplaySelect = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x0c, Graphics.saveStateSlot)); Lcd.windowDisplayEnabled = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x0d, Graphics.saveStateSlot)); Lcd.bgWindowTileDataSelect = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x0e, Graphics.saveStateSlot)); Lcd.bgTileMapDisplaySelect = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x0f, Graphics.saveStateSlot)); Lcd.tallSpriteSize = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x10, Graphics.saveStateSlot)); Lcd.spriteDisplayEnable = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x11, Graphics.saveStateSlot)); Lcd.bgDisplayEnabled = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x12, Graphics.saveStateSlot)); } } // Batch Process Graphics // http://gameboy.mongenel.com/dmg/asmmemmap.html and http://gbdev.gg8.se/wiki/articles/Video_Display // Function to batch process our graphics after we skipped so many cycles // This is not currently checked in memory read/write export function batchProcessGraphics(): void { var batchProcessCycles = Graphics.batchProcessCycles(); while (Graphics.currentCycles >= batchProcessCycles) { updateGraphics(batchProcessCycles); Graphics.currentCycles -= batchProcessCycles; } } // Inlined because closure compiler inlines export function initializeGraphics(): void { // Reset Stateful Variables Graphics.currentCycles = 0; Graphics.scanlineCycleCounter = 0x00; Graphics.scanlineRegister = 0; Graphics.scrollX = 0; Graphics.scrollY = 0; Graphics.windowX = 0; Graphics.windowY = 0; Graphics.scanlineRegister = 0x90; if (Cpu.GBCEnabled) { eightBitStoreIntoGBMemory(0xff41, 0x81); // 0xFF42 -> 0xFF43 = 0x00 eightBitStoreIntoGBMemory(0xff44, 0x90); // 0xFF45 -> 0xFF46 = 0x00 eightBitStoreIntoGBMemory(0xff47, 0xfc); // 0xFF48 -> 0xFF4B = 0x00 } else { eightBitStoreIntoGBMemory(0xff41, 0x85); // 0xFF42 -> 0xFF45 = 0x00 eightBitStoreIntoGBMemory(0xff46, 0xff); eightBitStoreIntoGBMemory(0xff47, 0xfc); eightBitStoreIntoGBMemory(0xff48, 0xff); eightBitStoreIntoGBMemory(0xff49, 0xff); // 0xFF4A -> 0xFF4B = 0x00 // GBC VRAM Banks (Handled by Memory, initializeCartridge) } // Scanline // Bgb says LY is 90 on boot Graphics.scanlineRegister = 0x90; // LCDC register eightBitStoreIntoGBMemory(0xff40, 0x91); // GBC VRAM Banks eightBitStoreIntoGBMemory(0xff4f, 0x00); eightBitStoreIntoGBMemory(0xff70, 0x01); // Override/reset some variables if the boot ROM is enabled if (Cpu.BootROMEnabled) { if (Cpu.GBCEnabled) { // GBC Graphics.scanlineRegister = 0x00; eightBitStoreIntoGBMemory(0xff40, 0x00); eightBitStoreIntoGBMemory(0xff41, 0x80); eightBitStoreIntoGBMemory(0xff44, 0x00); } else { // GB Graphics.scanlineRegister = 0x00; eightBitStoreIntoGBMemory(0xff40, 0x00); eightBitStoreIntoGBMemory(0xff41, 0x84); } } initializeColors(); } export function updateGraphics(numberOfCycles: i32): void { if (Lcd.enabled) { Graphics.scanlineCycleCounter += numberOfCycles; let graphicsDisableScanlineRendering = Config.graphicsDisableScanlineRendering; while (Graphics.scanlineCycleCounter >= Graphics.MAX_CYCLES_PER_SCANLINE()) { // Reset the scanlineCycleCounter // Don't set to zero to catch extra cycles Graphics.scanlineCycleCounter -= Graphics.MAX_CYCLES_PER_SCANLINE(); // Move to next scanline // let scanlineRegister: i32 = eightBitLoadFromGBMemory(Graphics.memoryLocationScanlineRegister); let scanlineRegister = Graphics.scanlineRegister; // Check if we've reached the last scanline if (scanlineRegister === 144) { // Draw the scanline if (!graphicsDisableScanlineRendering) { _drawScanline(scanlineRegister); } else { _renderEntireFrame(); } // Clear the priority map clearPriorityMap(); // Reset the tile cache resetTileCache(); } else if (scanlineRegister < 144) { // Draw the scanline if (!graphicsDisableScanlineRendering) { _drawScanline(scanlineRegister); } } // Post increment the scanline register after drawing // TODO: Need to fix graphics timing if (scanlineRegister > 153) { // Check if we overflowed scanlines // if so, reset our scanline number scanlineRegister = 0; } else { scanlineRegister += 1; } // Store our new scanline value Graphics.scanlineRegister = scanlineRegister; // eightBitStoreIntoGBMemory(Graphics.memoryLocationScanlineRegister, scanlineRegister); } } // Games like Pokemon crystal want the vblank right as it turns to the value, and not have it increment after // It will break and lead to an infinite loop in crystal // Therefore, we want to be checking/Setting our LCD status after the scanline updates setLcdStatus(); } // TODO: Make this a _drawPixelOnScanline, as values can be updated while drawing a scanline function _drawScanline(scanlineRegister: i32): void { // Get our seleted tile data memory location let tileDataMemoryLocation = Graphics.memoryLocationTileDataSelectZeroStart; if (Lcd.bgWindowTileDataSelect) { tileDataMemoryLocation = Graphics.memoryLocationTileDataSelectOneStart; } // Check if the background is enabled // NOTE: On Gameboy color, Pandocs says this does something completely different // LCDC.0 - 2) CGB in CGB Mode: BG and Window Master Priority // When Bit 0 is cleared, the background and window lose their priority - // the sprites will be always displayed on top of background and window, // independently of the priority flags in OAM and BG Map attributes. // TODO: Enable this different feature for GBC if (Cpu.GBCEnabled || Lcd.bgDisplayEnabled) { // Get our map memory location let tileMapMemoryLocation = Graphics.memoryLocationTileMapSelectZeroStart; if (Lcd.bgTileMapDisplaySelect) { tileMapMemoryLocation = Graphics.memoryLocationTileMapSelectOneStart; } // Finally, pass everything to draw the background renderBackground(scanlineRegister, tileDataMemoryLocation, tileMapMemoryLocation); } // Check if the window is enabled, and we are currently // Drawing lines on the window if (Lcd.windowDisplayEnabled) { // Get our map memory location let tileMapMemoryLocation = Graphics.memoryLocationTileMapSelectZeroStart; if (Lcd.windowTileMapDisplaySelect) { tileMapMemoryLocation = Graphics.memoryLocationTileMapSelectOneStart; } // Finally, pass everything to draw the background renderWindow(scanlineRegister, tileDataMemoryLocation, tileMapMemoryLocation); } if (Lcd.spriteDisplayEnable) { // Sprites are enabled, render them! renderSprites(scanlineRegister, Lcd.tallSpriteSize); } } // Function to render everything for a frame at once // This is to improve performance // See above for comments on how things are donw function _renderEntireFrame(): void { // Scanline needs to be in sync while we draw, thus, we can't shortcut anymore than here for (let i = 0; i <= 144; ++i) { _drawScanline(<u8>i); } } // Function to get the start of a RGB pixel (R, G, B) // Inlined because closure compiler inlines export function getRgbPixelStart(x: i32, y: i32): i32 { // Get the pixel number // let pixelNumber: i32 = (y * 160) + x; // Each pixel takes 3 slots, therefore, multiply by 3! return (y * 160 + x) * 3; } // Also need to store current frame in memory to be read by JS export function setPixelOnFrame(x: i32, y: i32, colorId: i32, color: i32): void { // Currently only supports 160x144 // Storing in X, then y // So need an offset store<u8>(FRAME_LOCATION + getRgbPixelStart(x, y) + colorId, color); } // Function to shortcut the memory map, and load directly from the VRAM Bank export function loadFromVramBank(gameboyOffset: i32, vramBankId: i32): u8 { let wasmBoyAddress = gameboyOffset - Memory.videoRamLocation + GAMEBOY_INTERNAL_MEMORY_LOCATION + 0x2000 * (vramBankId & 0x01); return load<u8>(wasmBoyAddress); }