|
| 1 | +// Main Class and funcitons for rendering the gameboy display |
| 2 | +import { FRAME_LOCATION, GAMEBOY_INTERNAL_MEMORY_LOCATION } from '../constants'; |
| 3 | +import { getSaveStateMemoryOffset } from '../core'; |
| 4 | +import { Lcd, setLcdStatus } from './lcd'; |
| 5 | +import { renderBackground, renderWindow } from './backgroundWindow'; |
| 6 | +import { renderSprites } from './sprites'; |
| 7 | +import { clearPriorityMap } from './priority'; |
| 8 | +import { resetTileCache } from './tiles'; |
| 9 | +import { initializeColors } from './colors'; |
| 10 | +import { Cpu } from '../cpu/index'; |
| 11 | +import { Config } from '../config'; |
| 12 | +import { Memory, eightBitLoadFromGBMemory, eightBitStoreIntoGBMemory } from '../memory/index'; |
| 13 | + |
| 14 | +export class Graphics { |
| 15 | + // Current cycles |
| 16 | + // This will be used for batch processing |
| 17 | + static currentCycles: i32 = 0; |
| 18 | + |
| 19 | + // Number of cycles to run in each batch process |
| 20 | + // This number should be in sync so that graphics doesn't run too many cyles at once |
| 21 | + // and does not exceed the minimum number of cyles for either scanlines, or |
| 22 | + // How often we change the frame, or a channel's update process |
| 23 | + static batchProcessCycles(): i32 { |
| 24 | + return Graphics.MAX_CYCLES_PER_SCANLINE(); |
| 25 | + } |
| 26 | + |
| 27 | + // Count the number of cycles to keep synced with cpu cycles |
| 28 | + // Found GBC cycles by finding clock speed from Gb Cycles |
| 29 | + // See TCAGBD For cycles |
| 30 | + static scanlineCycleCounter: i32 = 0x00; |
| 31 | + |
| 32 | + // TCAGBD says 456 per scanline, but 153 only a handful |
| 33 | + static MAX_CYCLES_PER_SCANLINE(): i32 { |
| 34 | + if (Graphics.scanlineRegister === 153) { |
| 35 | + return 4 << (<i32>Cpu.GBCDoubleSpeed); |
| 36 | + } else { |
| 37 | + return 456 << (<i32>Cpu.GBCDoubleSpeed); |
| 38 | + } |
| 39 | + } |
| 40 | + |
| 41 | + static MIN_CYCLES_SPRITES_LCD_MODE(): i32 { |
| 42 | + // TODO: Confirm these clock cyles, double similar to scanline, which TCAGBD did |
| 43 | + return 376 << (<i32>Cpu.GBCDoubleSpeed); |
| 44 | + } |
| 45 | + |
| 46 | + static MIN_CYCLES_TRANSFER_DATA_LCD_MODE(): i32 { |
| 47 | + // TODO: Confirm these clock cyles, double similar to scanline, which TCAGBD did |
| 48 | + return 249 << (<i32>Cpu.GBCDoubleSpeed); |
| 49 | + } |
| 50 | + |
| 51 | + // LCD |
| 52 | + // scanlineRegister also known as LY |
| 53 | + // See: http://bgb.bircd.org/pandocs.txt , and search " LY " |
| 54 | + static readonly memoryLocationScanlineRegister: i32 = 0xff44; |
| 55 | + static scanlineRegister: i32 = 0; |
| 56 | + static readonly memoryLocationDmaTransfer: i32 = 0xff46; |
| 57 | + |
| 58 | + // Scroll and Window |
| 59 | + static readonly memoryLocationScrollX: i32 = 0xff43; |
| 60 | + static scrollX: i32 = 0; |
| 61 | + static readonly memoryLocationScrollY: i32 = 0xff42; |
| 62 | + static scrollY: i32 = 0; |
| 63 | + static readonly memoryLocationWindowX: i32 = 0xff4b; |
| 64 | + static windowX: i32 = 0; |
| 65 | + static readonly memoryLocationWindowY: i32 = 0xff4a; |
| 66 | + static windowY: i32 = 0; |
| 67 | + |
| 68 | + // Tile Maps And Data |
| 69 | + static readonly memoryLocationTileMapSelectZeroStart: i32 = 0x9800; |
| 70 | + static readonly memoryLocationTileMapSelectOneStart: i32 = 0x9c00; |
| 71 | + static readonly memoryLocationTileDataSelectZeroStart: i32 = 0x8800; |
| 72 | + static readonly memoryLocationTileDataSelectOneStart: i32 = 0x8000; |
| 73 | + |
| 74 | + // Sprites |
| 75 | + static readonly memoryLocationSpriteAttributesTable: i32 = 0xfe00; |
| 76 | + |
| 77 | + // Palettes |
| 78 | + static readonly memoryLocationBackgroundPalette: i32 = 0xff47; |
| 79 | + static readonly memoryLocationSpritePaletteOne: i32 = 0xff48; |
| 80 | + static readonly memoryLocationSpritePaletteTwo: i32 = 0xff49; |
| 81 | + |
| 82 | + // Screen data needs to be stored in wasm memory |
| 83 | + |
| 84 | + // Save States |
| 85 | + |
| 86 | + static readonly saveStateSlot: i32 = 1; |
| 87 | + |
| 88 | + // Function to save the state of the class |
| 89 | + static saveState(): void { |
| 90 | + store<i32>(getSaveStateMemoryOffset(0x00, Graphics.saveStateSlot), Graphics.scanlineCycleCounter); |
| 91 | + store<u8>(getSaveStateMemoryOffset(0x04, Graphics.saveStateSlot), <u8>Lcd.currentLcdMode); |
| 92 | + |
| 93 | + eightBitStoreIntoGBMemory(Graphics.memoryLocationScanlineRegister, Graphics.scanlineRegister); |
| 94 | + } |
| 95 | + |
| 96 | + // Function to load the save state from memory |
| 97 | + static loadState(): void { |
| 98 | + Graphics.scanlineCycleCounter = load<i32>(getSaveStateMemoryOffset(0x00, Graphics.saveStateSlot)); |
| 99 | + Lcd.currentLcdMode = load<u8>(getSaveStateMemoryOffset(0x04, Graphics.saveStateSlot)); |
| 100 | + |
| 101 | + Graphics.scanlineRegister = eightBitLoadFromGBMemory(Graphics.memoryLocationScanlineRegister); |
| 102 | + Lcd.updateLcdControl(eightBitLoadFromGBMemory(Lcd.memoryLocationLcdControl)); |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +// Batch Process Graphics |
| 107 | +// http://gameboy.mongenel.com/dmg/asmmemmap.html and http://gbdev.gg8.se/wiki/articles/Video_Display |
| 108 | +// Function to batch process our graphics after we skipped so many cycles |
| 109 | +// This is not currently checked in memory read/write |
| 110 | +export function batchProcessGraphics(): void { |
| 111 | + var batchProcessCycles = Graphics.batchProcessCycles(); |
| 112 | + while (Graphics.currentCycles >= batchProcessCycles) { |
| 113 | + updateGraphics(batchProcessCycles); |
| 114 | + Graphics.currentCycles -= batchProcessCycles; |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +// Inlined because closure compiler inlines |
| 119 | +export function initializeGraphics(): void { |
| 120 | + // Reset Stateful Variables |
| 121 | + Graphics.currentCycles = 0; |
| 122 | + Graphics.scanlineCycleCounter = 0x00; |
| 123 | + Graphics.scanlineRegister = 0; |
| 124 | + Graphics.scrollX = 0; |
| 125 | + Graphics.scrollY = 0; |
| 126 | + Graphics.windowX = 0; |
| 127 | + Graphics.windowY = 0; |
| 128 | + |
| 129 | + Graphics.scanlineRegister = 0x90; |
| 130 | + |
| 131 | + if (Cpu.GBCEnabled) { |
| 132 | + eightBitStoreIntoGBMemory(0xff41, 0x81); |
| 133 | + // 0xFF42 -> 0xFF43 = 0x00 |
| 134 | + eightBitStoreIntoGBMemory(0xff44, 0x90); |
| 135 | + // 0xFF45 -> 0xFF46 = 0x00 |
| 136 | + eightBitStoreIntoGBMemory(0xff47, 0xfc); |
| 137 | + // 0xFF48 -> 0xFF4B = 0x00 |
| 138 | + } else { |
| 139 | + eightBitStoreIntoGBMemory(0xff41, 0x85); |
| 140 | + // 0xFF42 -> 0xFF45 = 0x00 |
| 141 | + eightBitStoreIntoGBMemory(0xff46, 0xff); |
| 142 | + eightBitStoreIntoGBMemory(0xff47, 0xfc); |
| 143 | + eightBitStoreIntoGBMemory(0xff48, 0xff); |
| 144 | + eightBitStoreIntoGBMemory(0xff49, 0xff); |
| 145 | + // 0xFF4A -> 0xFF4B = 0x00 |
| 146 | + // GBC VRAM Banks (Handled by Memory, initializeCartridge) |
| 147 | + } |
| 148 | + |
| 149 | + // Scanline |
| 150 | + // Bgb says LY is 90 on boot |
| 151 | + Graphics.scanlineRegister = 0x90; |
| 152 | + eightBitStoreIntoGBMemory(0xff40, 0x90); |
| 153 | + |
| 154 | + // GBC VRAM Banks |
| 155 | + eightBitStoreIntoGBMemory(0xff4f, 0x00); |
| 156 | + eightBitStoreIntoGBMemory(0xff70, 0x01); |
| 157 | + |
| 158 | + // Override/reset some variables if the boot ROM is enabled |
| 159 | + if (Cpu.BootROMEnabled) { |
| 160 | + if (Cpu.GBCEnabled) { |
| 161 | + // GBC |
| 162 | + Graphics.scanlineRegister = 0x00; |
| 163 | + eightBitStoreIntoGBMemory(0xff40, 0x00); |
| 164 | + eightBitStoreIntoGBMemory(0xff41, 0x80); |
| 165 | + eightBitStoreIntoGBMemory(0xff44, 0x00); |
| 166 | + } else { |
| 167 | + // GB |
| 168 | + Graphics.scanlineRegister = 0x00; |
| 169 | + eightBitStoreIntoGBMemory(0xff40, 0x00); |
| 170 | + eightBitStoreIntoGBMemory(0xff41, 0x84); |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + initializeColors(); |
| 175 | +} |
| 176 | + |
| 177 | +export function updateGraphics(numberOfCycles: i32): void { |
| 178 | + if (Lcd.enabled) { |
| 179 | + Graphics.scanlineCycleCounter += numberOfCycles; |
| 180 | + |
| 181 | + let graphicsDisableScanlineRendering = Config.graphicsDisableScanlineRendering; |
| 182 | + |
| 183 | + while (Graphics.scanlineCycleCounter >= Graphics.MAX_CYCLES_PER_SCANLINE()) { |
| 184 | + // Reset the scanlineCycleCounter |
| 185 | + // Don't set to zero to catch extra cycles |
| 186 | + Graphics.scanlineCycleCounter -= Graphics.MAX_CYCLES_PER_SCANLINE(); |
| 187 | + |
| 188 | + // Move to next scanline |
| 189 | + // let scanlineRegister: i32 = eightBitLoadFromGBMemory(Graphics.memoryLocationScanlineRegister); |
| 190 | + let scanlineRegister = Graphics.scanlineRegister; |
| 191 | + |
| 192 | + // Check if we've reached the last scanline |
| 193 | + if (scanlineRegister === 144) { |
| 194 | + // Draw the scanline |
| 195 | + if (!graphicsDisableScanlineRendering) { |
| 196 | + _drawScanline(scanlineRegister); |
| 197 | + } else { |
| 198 | + _renderEntireFrame(); |
| 199 | + } |
| 200 | + |
| 201 | + // Clear the priority map |
| 202 | + clearPriorityMap(); |
| 203 | + |
| 204 | + // Reset the tile cache |
| 205 | + resetTileCache(); |
| 206 | + } else if (scanlineRegister < 144) { |
| 207 | + // Draw the scanline |
| 208 | + if (!graphicsDisableScanlineRendering) { |
| 209 | + _drawScanline(scanlineRegister); |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + // Post increment the scanline register after drawing |
| 214 | + // TODO: Need to fix graphics timing |
| 215 | + if (scanlineRegister > 153) { |
| 216 | + // Check if we overflowed scanlines |
| 217 | + // if so, reset our scanline number |
| 218 | + scanlineRegister = 0; |
| 219 | + } else { |
| 220 | + scanlineRegister += 1; |
| 221 | + } |
| 222 | + |
| 223 | + // Store our new scanline value |
| 224 | + Graphics.scanlineRegister = scanlineRegister; |
| 225 | + // eightBitStoreIntoGBMemory(Graphics.memoryLocationScanlineRegister, scanlineRegister); |
| 226 | + } |
| 227 | + } |
| 228 | + |
| 229 | + // Games like Pokemon crystal want the vblank right as it turns to the value, and not have it increment after |
| 230 | + // It will break and lead to an infinite loop in crystal |
| 231 | + // Therefore, we want to be checking/Setting our LCD status after the scanline updates |
| 232 | + setLcdStatus(); |
| 233 | +} |
| 234 | + |
| 235 | +// TODO: Make this a _drawPixelOnScanline, as values can be updated while drawing a scanline |
| 236 | +function _drawScanline(scanlineRegister: i32): void { |
| 237 | + // Get our seleted tile data memory location |
| 238 | + let tileDataMemoryLocation = Graphics.memoryLocationTileDataSelectZeroStart; |
| 239 | + if (Lcd.bgWindowTileDataSelect) { |
| 240 | + tileDataMemoryLocation = Graphics.memoryLocationTileDataSelectOneStart; |
| 241 | + } |
| 242 | + |
| 243 | + // Check if the background is enabled |
| 244 | + // NOTE: On Gameboy color, Pandocs says this does something completely different |
| 245 | + // LCDC.0 - 2) CGB in CGB Mode: BG and Window Master Priority |
| 246 | + // When Bit 0 is cleared, the background and window lose their priority - |
| 247 | + // the sprites will be always displayed on top of background and window, |
| 248 | + // independently of the priority flags in OAM and BG Map attributes. |
| 249 | + // TODO: Enable this different feature for GBC |
| 250 | + if (Cpu.GBCEnabled || Lcd.bgDisplayEnabled) { |
| 251 | + // Get our map memory location |
| 252 | + let tileMapMemoryLocation = Graphics.memoryLocationTileMapSelectZeroStart; |
| 253 | + if (Lcd.bgTileMapDisplaySelect) { |
| 254 | + tileMapMemoryLocation = Graphics.memoryLocationTileMapSelectOneStart; |
| 255 | + } |
| 256 | + |
| 257 | + // Finally, pass everything to draw the background |
| 258 | + renderBackground(scanlineRegister, tileDataMemoryLocation, tileMapMemoryLocation); |
| 259 | + } |
| 260 | + |
| 261 | + // Check if the window is enabled, and we are currently |
| 262 | + // Drawing lines on the window |
| 263 | + if (Lcd.windowDisplayEnabled) { |
| 264 | + // Get our map memory location |
| 265 | + let tileMapMemoryLocation = Graphics.memoryLocationTileMapSelectZeroStart; |
| 266 | + if (Lcd.windowTileMapDisplaySelect) { |
| 267 | + tileMapMemoryLocation = Graphics.memoryLocationTileMapSelectOneStart; |
| 268 | + } |
| 269 | + |
| 270 | + // Finally, pass everything to draw the background |
| 271 | + renderWindow(scanlineRegister, tileDataMemoryLocation, tileMapMemoryLocation); |
| 272 | + } |
| 273 | + |
| 274 | + if (Lcd.spriteDisplayEnable) { |
| 275 | + // Sprites are enabled, render them! |
| 276 | + renderSprites(scanlineRegister, Lcd.tallSpriteSize); |
| 277 | + } |
| 278 | +} |
| 279 | + |
| 280 | +// Function to render everything for a frame at once |
| 281 | +// This is to improve performance |
| 282 | +// See above for comments on how things are donw |
| 283 | +function _renderEntireFrame(): void { |
| 284 | + // Scanline needs to be in sync while we draw, thus, we can't shortcut anymore than here |
| 285 | + for (let i = 0; i <= 144; ++i) { |
| 286 | + _drawScanline(<u8>i); |
| 287 | + } |
| 288 | +} |
| 289 | + |
| 290 | +// Function to get the start of a RGB pixel (R, G, B) |
| 291 | +// Inlined because closure compiler inlines |
| 292 | +export function getRgbPixelStart(x: i32, y: i32): i32 { |
| 293 | + // Get the pixel number |
| 294 | + // let pixelNumber: i32 = (y * 160) + x; |
| 295 | + // Each pixel takes 3 slots, therefore, multiply by 3! |
| 296 | + return (y * 160 + x) * 3; |
| 297 | +} |
| 298 | + |
| 299 | +// Also need to store current frame in memory to be read by JS |
| 300 | +export function setPixelOnFrame(x: i32, y: i32, colorId: i32, color: i32): void { |
| 301 | + // Currently only supports 160x144 |
| 302 | + // Storing in X, then y |
| 303 | + // So need an offset |
| 304 | + store<u8>(FRAME_LOCATION + getRgbPixelStart(x, y) + colorId, color); |
| 305 | +} |
| 306 | + |
| 307 | +// Function to shortcut the memory map, and load directly from the VRAM Bank |
| 308 | +export function loadFromVramBank(gameboyOffset: i32, vramBankId: i32): u8 { |
| 309 | + let wasmBoyAddress = gameboyOffset - Memory.videoRamLocation + GAMEBOY_INTERNAL_MEMORY_LOCATION + 0x2000 * (vramBankId & 0x01); |
| 310 | + return load<u8>(wasmBoyAddress); |
| 311 | +} |
0 commit comments