import { Const } from '../const' ; import { Address , Byte } from '../types' ; import { kPaletColors , kStaggered , kFlipXBits } from './const' ; import { HEventType , HEvents , HStatusMgr } from './hevent' ; import { MirrorMode , PpuReg , PpuCtrlBit , PpuMaskBit , PpuStatusBit , OamElem , OamAttrBit } from './types' ; import { Util } from '../../util/util' ; import { VBlank } from '../const' ; const REGISTER_COUNT = 8 ; const VRAM_SIZE = 0x4000 ; const OAM_SIZE = 0x0100 ; const MAX_SPRITE_ON_SCANLINE = 8 ; const MAX_SPRITE = 64 ; const PALET_ADR = 0x3f00 ; const PALET_END_ADR = 0x3fff ; const kMirrorModeBitTable = Uint8Array . from ( [ 0x50 , 0x44 , 0x00 , 0x55 , 0x05 ] ) ; const SPRITE_MASK = 0x10 ; const kSpritePriorityMask = Uint8Array . from ( [ SPRITE_MASK , 0xff ] ) ; export function getNameTable ( baseNameTable , bx , by , mirrorModeBit ) { const page = ( ( ( bx >> 5 ) & 1 ) + ( ( ( by / 30 ) & 1 ) << 1 ) ) ^ baseNameTable ; const m = ( mirrorModeBit << ( 10 - ( page << 1 ) ) ) & 0x0c00 ; return ( 0x2000 + m ) | 0 ; } export function getBgPatternTableAddress ( ppuCtrl ) { return ( ppuCtrl & PpuCtrlBit . BG_PATTERN_TABLE_ADDRESS ) << 8 ; } export function getBgPat ( chrData , chridx , py , chrBankOffset ) { const idx = chridx + py ; const bank = ( idx >> 10 ) & 7 ; const p = chrBankOffset [ bank ] + ( idx & 0x03ff ) ; return kStaggered [ chrData [ p ] ] | ( kStaggered [ chrData [ p + 8 ] ] << 1 ) ; } function getPpuAddr ( adr , mirrorModeBit ) { adr &= 0x3fff ; if ( 0x3000 <= adr && adr < 0x3f00 ) adr -= 0x1000 ; if ( 0x2000 <= adr && adr < 0x3000 ) { const page = ( adr >> 10 ) & 3 ; const m = ( mirrorModeBit << ( 10 - ( page << 1 ) ) ) & 0x0c00 ; return ( adr & 0xf3ff ) | m ; } if ( PALET_ADR <= adr && adr <= PALET_END_ADR ) { adr &= 0xff1f ; if ( ( adr & 0xfff3 ) === 0x3f10 ) adr &= 0xffef ; } return adr ; } function incPpuAddr ( ppuAddr , ppuCtrl ) { const add = ( ( ppuCtrl & PpuCtrlBit . INCREMENT_MODE ) !== 0 ) ? 32 : 1 ; return ( ppuAddr + add ) & ( VRAM_SIZE - 1 ) ; } function incScroll ( t , dy ) { const pageY = ( ( t >> 11 ) & 1 ) * 240 ; let y = ( ( t & 0x03e0 ) >> ( 5 - 3 ) ) | ( ( t >> 12 ) & 7 ) ; if ( y >= 240 ) y -= 256 ; const ny = pageY + y + dy ; const p = ( ny / 240 ) & 1 ; const sy = ny % 240 ; return ( t & ~ 0x7be0 ) | ( ( sy & 0xf8 ) << ( 5 - 3 ) ) | ( ( sy & 0x07 ) << 12 ) | ( p << 11 ) ; } function getSpritePat ( chrData , chridx , py , flipHorz , chrBankOffset ) { const idx = chridx + ( py & 7 ) + ( ( py & 8 ) << 1 ) ; const bank = ( idx >> 10 ) & 7 ; const p = chrBankOffset [ bank ] + ( idx & 0x03ff ) ; let patHi = chrData [ p + 8 ] ; let patLo = chrData [ p ] ; if ( flipHorz ) { patHi = kFlipXBits [ patHi ] ; patLo = kFlipXBits [ patLo ] ; } return kStaggered [ patLo ] | ( kStaggered [ patHi ] << 1 ) ; } function clearBg ( hline0 , hline1 , x , pixels , colorMask , palet ) { const LINE_BYTES = Const . WIDTH * 4 ; const col = palet [ 0 ] & colorMask ; const c = kPaletColors [ col ] ; const r = c >> 16 ; const g = ( c >> 8 ) & 0xff ; const b = c & 0xff ; for ( let i = hline0 ; i < hline1 ; ++ i ) { let index = i * LINE_BYTES ; for ( let j = 0 ; j < x ; ++ j ) { pixels [ index ] = r ; pixels [ index + 1 ] = g ; pixels [ index + 2 ] = b ; index += 4 ; } } } export class Ppu { public suppressSpriteFlicker = true ; private chrData = new Uint8Array ( 0 ) ; private regs = new Uint8Array ( REGISTER_COUNT ) ; private vram : Uint8Array ; private oam = new Uint8Array ( OAM_SIZE ) ; private mirrorMode = MirrorMode . VERT ; private hcount = 0 ; private latch = 0 ; private ppuAddr : Address = 0 ; private bufferedValue : Byte = 0 ; private hevents = new HEvents ( ) ; private hstatusMgr = new HStatusMgr ( ) ; private offscreen = new Uint8Array ( Const . WIDTH * Const . HEIGHT ) ; constructor ( private triggerNmi ) { const vramBuffer = new ArrayBuffer ( VRAM_SIZE ) ; this . vram = new Uint8Array ( vramBuffer ) ; this . reset ( ) ; } public reset ( ) { this . regs . fill ( 0 ) ; this . vram . fill ( 0 ) ; this . oam . fill ( 0 ) ; this . hcount = 0 ; this . ppuAddr = 0 ; this . latch = 0 ; this . bufferedValue = 0 ; this . hevents . clear ( ) ; this . hstatusMgr . reset ( ) ; this . offscreen . fill ( 0 ) ; } public save ( ) { const data = { regs : Util . convertUint8ArrayToBase64String ( this . regs ) , oam : Util . convertUint8ArrayToBase64String ( this . oam ) , mirrorMode : this . mirrorMode , } ; if ( this . isChrRam ( ) ) { data . vram = Util . convertUint8ArrayToBase64String ( this . vram ) ; } else { data . vramHigh = Util . convertUint8ArrayToBase64String ( this . vram . subarray ( 0x2000 ) ) ; } return data ; } public load ( saveData ) { const isRam = this . isChrRam ( ) ; this . regs = Util . convertBase64StringToUint8Array ( saveData . regs ) ; this . oam = Util . convertBase64StringToUint8Array ( saveData . oam ) ; this . mirrorMode = saveData . mirrorMode ; if ( isRam ) { const vram = Util . convertBase64StringToUint8Array ( saveData . vram ) ; for ( let i = 0 ; i < vram . length ; ++ i ) this . vram [ i ] = vram [ i ] ; } else { const vramHigh = Util . convertBase64StringToUint8Array ( saveData . vramHigh ) ; for ( let i = 0 ; i < vramHigh . length ; ++ i ) this . vram [ i + 0x2000 ] = vramHigh [ i ] ; } this . hstatusMgr . current . set ( HEventType . PPU_CTRL , this . regs [ PpuReg . CTRL ] , - 1 ) ; this . hstatusMgr . current . set ( HEventType . PPU_MASK , this . regs [ PpuReg . MASK ] , - 1 ) ; this . hstatusMgr . current . set ( HEventType . MIRROR_MODE_BIT , kMirrorModeBitTable [ this . mirrorMode ] , - 1 ) ; for ( let i = 0 ; i < 32 ; ++ i ) this . hstatusMgr . current . set ( HEventType . PALET , this . vram [ PALET_ADR + i ] , i ) ; } public setChrData ( chrData ) { const isRam = ! ( chrData && chrData . length > 0 ) ; if ( isRam ) this . chrData = this . vram ; else this . chrData = chrData ; } public setChrBank ( value ) { const base = value << 3 ; for ( let i = 0 ; i < 8 ; ++ i ) this . setChrBankOffset ( i , base + i ) ; } public setChrBankOffset ( bank , value ) { const max = this . chrData . length ; const offset = ( value << 10 ) & ( max - 1 ) ; this . incScrollCounter ( ) ; this . addHevent ( HEventType . CHR_BANK_OFFSET , offset , bank ) ; } public getMirrorMode ( ) { return this . mirrorMode ; } public setMirrorMode ( mode ) { this . mirrorMode = mode ; this . setMirrorModeBit ( kMirrorModeBitTable [ mode ] ) ; } public setMirrorModeBit ( bit ) { this . incScrollCounter ( ) ; this . addHevent ( HEventType . MIRROR_MODE_BIT , bit ) ; } public read ( reg ) { let result = this . regs [ reg ] ; switch ( reg as PpuReg ) { case PpuReg . STATUS : this . regs [ PpuReg . STATUS ] &= ~ PpuStatusBit . VBLANK ; this . latch = 0 ; break ; case PpuReg . OAMDATA : result = this . oam [ this . regs [ PpuReg . OAMADDR ] ] ; break ; case PpuReg . DATA : { const ppuAddr = this . hstatusMgr . current . scrollCurr ; const addr = getPpuAddr ( ppuAddr , this . hstatusMgr . current . mirrorModeBit ) ; if ( PALET_ADR <= addr && addr <= PALET_END_ADR ) { result = this . readPpuDirect ( addr ) ; this . bufferedValue = this . readPpuDirect ( getPpuAddr ( ppuAddr - 0x1000 , this . hstatusMgr . current . mirrorModeBit ) ) ; } else { result = this . bufferedValue ; this . bufferedValue = this . readPpuDirect ( addr ) ; } this . addHevent ( HEventType . SCROLL_CURR , incPpuAddr ( ppuAddr , this . regs [ PpuReg . CTRL ] ) ) ; } break ; default : break ; } return result ; } public write ( reg , value ) { if ( reg === PpuReg . STATUS ) { value &= ~ ( PpuStatusBit . VBLANK | PpuStatusBit . SPRITE0HIT | PpuStatusBit . SPRITE_OVERFLOW ) ; } this . regs [ reg ] = value ; switch ( reg as PpuReg ) { case PpuReg . CTRL : { this . incScrollCounter ( ) ; this . addHevent ( HEventType . PPU_CTRL , this . regs [ PpuReg . CTRL ] ) ; this . ppuAddr = ( ( this . ppuAddr & ~ 0x0c00 ) | ( ( value & PpuCtrlBit . BASE_NAMETABLE_ADDRESS ) << 10 ) ) ; this . updateCoarseX ( ) ; } break ; case PpuReg . MASK : this . incScrollCounter ( ) ; this . addHevent ( HEventType . PPU_MASK , this . regs [ PpuReg . MASK ] ) ; break ; case PpuReg . OAMDATA : { const oamAddr = this . regs [ PpuReg . OAMADDR ] ; this . oam [ oamAddr ] = value ; this . regs [ PpuReg . OAMADDR ] = ( oamAddr + 1 ) & 0xff ; } break ; case PpuReg . SCROLL : this . incScrollCounter ( ) ; if ( this . latch === 0 ) { this . ppuAddr = ( this . ppuAddr & ~ 0x001f ) | ( value >> 3 ) ; this . addHevent ( HEventType . SCROLL_FINE_X , value & 7 ) ; this . updateCoarseX ( ) ; } else { this . ppuAddr = ( ( this . ppuAddr & ~ 0x73e0 ) | ( ( value & 0xf8 ) << ( 5 - 3 ) ) | ( ( value & 0x07 ) << 12 ) ) ; } this . latch = 1 - this . latch ; break ; case PpuReg . ADDR : if ( this . latch === 0 ) { this . ppuAddr = ( this . ppuAddr & ~ 0x7f00 ) | ( ( value & 0x3f ) << 8 ) ; } else { this . ppuAddr = ( this . ppuAddr & ~ 0x00ff ) | value ; this . addHevent ( HEventType . SCROLL_CURR , this . ppuAddr ) ; } this . latch = 1 - this . latch ; break ; case PpuReg . DATA : { const ppuAddr = this . hstatusMgr . current . scrollCurr ; const addr = getPpuAddr ( ppuAddr , this . hstatusMgr . current . mirrorModeBit ) ; this . vram [ addr ] = value ; if ( PALET_ADR <= addr && addr < PALET_ADR + 32 ) this . addHevent ( HEventType . PALET , value , addr - PALET_ADR ) ; this . addHevent ( HEventType . SCROLL_CURR , incPpuAddr ( ppuAddr , this . regs [ PpuReg . CTRL ] ) ) ; } break ; default : break ; } } public copyWithDma ( array , start ) { const dst = this . oam ; let j = this . regs [ PpuReg . OAMADDR ] ; for ( let i = 0 ; i < 256 ; ++ i ) { dst [ j ] = array [ start + i ] ; j = ( j + 1 ) & 255 ; } } public setVBlank ( ) { this . regs [ PpuReg . STATUS ] = this . regs [ PpuReg . STATUS ] | PpuStatusBit . VBLANK ; this . hevents . swap ( ) ; this . hstatusMgr . swap ( ) ; } public clearVBlank ( ) { this . regs [ PpuReg . STATUS ] &= ~ ( PpuStatusBit . VBLANK | PpuStatusBit . SPRITE0HIT | PpuStatusBit . SPRITE_OVERFLOW ) ; if ( ( this . hstatusMgr . current . ppuMask & ( PpuMaskBit . SHOW_SPRITE | PpuMaskBit . SHOW_BG ) ) !== 0 ) this . addHevent ( HEventType . SCROLL_CURR , this . ppuAddr ) ; } public setHcount ( hcount ) { this . hcount = hcount ; this . checkSprite0Hit ( hcount ) ; switch ( hcount ) { case VBlank . START : this . setVBlank ( ) ; break ; case VBlank . NMI : if ( ( this . regs [ PpuReg . CTRL ] & PpuCtrlBit . VINT_ENABLE ) !== 0 ) this . triggerNmi ( ) ; break ; case VBlank . END : this . clearVBlank ( ) ; break ; default : break ; } } public render ( pixels ) { const greyscale = ( this . regs [ PpuReg . MASK ] & PpuMaskBit . GREYSCALE ) !== 0 ; const colorMask = greyscale ? 0x20 : 0x3f ; this . renderOffscreen ( this . offscreen , pixels , colorMask ) ; } public writePpuDirect ( addr , value ) { if ( addr >= 0x2000 ) { this . vram [ addr ] = value ; } else { const bankOffset = this . hstatusMgr . current . chrBankOffset [ ( addr >> 10 ) & 7 ] ; this . chrData [ ( addr & 0x3ff ) + bankOffset ] = value ; } } public getPaletTable ( ) { const h = this . hstatusMgr . lastFrame ; return h . palet ; } public getRegs ( ) { return this . regs ; } public getVram ( ) { return this . vram ; } public getChrData ( ) { return this . chrData ; } public getHStatusMgr ( ) { return this . hstatusMgr ; } private isChrRam ( ) { return this . chrData === this . vram ; } private renderOffscreen ( offscreen , pixels , colorMask ) { offscreen . fill ( 0 ) ; const h = this . hstatusMgr . lastFrame ; const n = this . hevents . getCount ( ) ; let sprChrStart = 0 ; for ( let i = 0 ; i < n ; ++ i ) { const hevent = this . hevents . getEvent ( i ) ; h . set ( hevent . type , hevent . value , hevent . index ) ; const hline0 = hevent . hcount ; const hline1 = this . hevents . getEvent ( i + 1 ) . hcount ; if ( hline0 >= hline1 ) continue ; if ( ( h . ppuMask & PpuMaskBit . SHOW_BG ) === 0 ) { clearBg ( hline0 , hline1 , Const . WIDTH , pixels , colorMask , h . palet ) ; } else { const baseNameTable = ( h . scrollCurr & 0x0c00 ) >> 10 ; const bgChrStart = getBgPatternTableAddress ( h . ppuCtrl ) ; let x0 = 0 ; if ( ( h . ppuMask & PpuMaskBit . SHOW_BG_LEFT_8PX ) === 0 ) { x0 = 8 ; clearBg ( hline0 , hline1 , x0 , pixels , colorMask , h . palet ) ; } const scrollX = h . scrollFineX | ( ( h . scrollCurr & 0x001f ) << 3 ) ; const scrollY = ( ( h . scrollCurr & 0x7000 ) >> 12 ) | ( ( h . scrollCurr & 0x03e0 ) >> ( 5 - 3 ) ) ; this . renderBg ( offscreen , scrollX , scrollY , baseNameTable , hline0 , hline1 , x0 , h . chrBankOffset , h . mirrorModeBit , bgChrStart , pixels , colorMask , h . palet ) ; } if ( ( h . ppuMask & PpuMaskBit . SHOW_SPRITE ) !== 0 ) { if ( ( h . ppuCtrl & PpuCtrlBit . SPRITE_SIZE ) === 0 ) sprChrStart = ( h . ppuCtrl & PpuCtrlBit . SPRITE_PATTERN_TABLE_ADDRESS ) << 9 ; const x0 = ( h . ppuMask & PpuMaskBit . SHOW_SPRITE_LEFT_8PX ) ? 0 : 8 ; this . renderSprite ( offscreen , hline0 , hline1 , x0 , h . chrBankOffset , sprChrStart , pixels , colorMask , h . palet ) ; } } } private renderBg ( offscreen , scrollX , scrollY , baseNameTable , hline0 , hline1 , x0 , chrBankOffset , mirrorModeBit , chrStart , pixels , colorMask , palet ) { scrollX |= 0 ; scrollY |= 0 ; hline0 |= 0 ; hline1 |= 0 ; x0 |= 0 ; mirrorModeBit |= 0 ; colorMask |= 0 ; const W = 8 ; const LINE_WIDTH = Const . WIDTH | 0 ; const vram = this . vram ; if ( scrollY >= 240 ) scrollY = ( scrollY - 256 ) | 0 ; const fineX = scrollX & 7 ; const coarseX = scrollX >> 3 ; for ( let yy = hline0 ; yy < hline1 ; ++ yy ) { const yyy = yy - hline0 + scrollY ; const by = ( ( yyy >> 3 ) + 60 ) % 60 ; const ay = by % 30 ; for ( let bbx = 0 ; bbx < Const . WIDTH / W + 1 ; ++ bbx ) { const bx = ( bbx + coarseX ) & 63 ; const ax = bx & 31 ; const nameTable = getNameTable ( baseNameTable , bx , by , mirrorModeBit ) | 0 ; const name = vram [ nameTable + ax + ( ay << 5 ) ] ; const chridx = ( name << 4 ) + chrStart ; const palShift = ( ax & 2 ) + ( ( ay & 2 ) << 1 ) ; const atrBlk = ( ax >> 2 ) + ( ( ay << 1 ) & 0x0f8 ) ; const attributeTable = ( nameTable + 0x3c0 ) | 0 ; const paletHigh = ( ( ( vram [ attributeTable + atrBlk ] >> palShift ) & 3 ) << 2 ) | 0 ; const px0 = ( bbx * W - fineX ) | 0 ; const pxStart = Math . max ( x0 - px0 , 0 ) | 0 ; const pxEnd = Math . min ( Const . WIDTH - px0 , W ) | 0 ; const pat = getBgPat ( this . chrData , chridx , yyy & 7 , chrBankOffset ) ; for ( let px = pxStart ; px < pxEnd ; ++ px ) { const xx = ( px + px0 ) | 0 ; let pal = ( ( pat >> ( ( W - 1 ) * 2 - ( px << 1 ) ) ) & 3 ) | 0 ; if ( pal !== 0 ) pal |= paletHigh ; const index = yy * LINE_WIDTH + xx ; offscreen [ index ] = pal ; const col = palet [ pal ] & colorMask ; const c = kPaletColors [ col ] ; const index2 = index * 4 ; pixels [ index2 + 0 ] = c >> 16 ; pixels [ index2 + 1 ] = ( c >> 8 ) & 0xff ; pixels [ index2 + 2 ] = c & 0xff ; } } } } private isSprite8x16 ( ) { return ( this . regs [ PpuReg . CTRL ] & PpuCtrlBit . SPRITE_SIZE ) !== 0 ; } private renderSprite ( offscreen , hline0 , hline1 , x0 , chrBankOffset , chrStart , pixels , colorMask , palet ) { const W = 8 ; const LINE_WIDTH = Const . WIDTH ; const PALET = 0x03 ; const oam = this . oam ; const isSprite8x16 = this . isSprite8x16 ( ) ; const sh = isSprite8x16 ? 16 : 8 ; for ( let h = hline0 ; h < hline1 ; ++ h ) { let n = 0 ; for ( let i = 0 ; i < MAX_SPRITE ; ++ i ) { const y = oam [ i * 4 + OamElem . Y ] + 1 ; if ( h < y || h >= y + sh ) continue ; const oamIndex = oam [ i * 4 + OamElem . INDEX ] ; const attr = oam [ i * 4 + OamElem . ATTR ] ; const flipVert = ( attr & OamAttrBit . FLIP_VERT ) !== 0 ; const flipHorz = ( attr & OamAttrBit . FLIP_HORZ ) !== 0 ; const x = oam [ i * 4 + OamElem . X ] ; const priorityMask = kSpritePriorityMask [ ( attr >> 5 ) & 1 ] ; const chridx = ( isSprite8x16 ? ( oamIndex & 0xfe ) * 16 + ( ( oamIndex & 1 ) << 12 ) : oamIndex * 16 + chrStart ) ; const paletHigh = ( ( attr & PALET ) << 2 ) | SPRITE_MASK ; const py = h - y ; const px0 = Math . max ( x0 - x , 0 ) ; const px1 = Math . min ( Const . WIDTH - x , W ) ; const ppy = flipVert ? ( sh - 1 ) - py : py ; const pat = getSpritePat ( this . chrData , chridx , ppy , flipHorz , chrBankOffset ) ; for ( let px = px0 ; px < px1 ; ++ px ) { let pal = ( pat >> ( ( W - 1 - px ) << 1 ) ) & 3 ; if ( pal === 0 ) continue ; const pixelIndex = ( y + py ) * LINE_WIDTH + ( x + px ) ; if ( ( offscreen [ pixelIndex ] & priorityMask ) !== 0 ) { offscreen [ pixelIndex ] |= SPRITE_MASK ; continue ; } pal |= paletHigh ; offscreen [ pixelIndex ] = pal ; const col = palet [ pal ] & colorMask ; const c = kPaletColors [ col ] ; const index2 = pixelIndex * 4 ; pixels [ index2 + 0 ] = c >> 16 ; pixels [ index2 + 1 ] = ( c >> 8 ) & 0xff ; pixels [ index2 + 2 ] = c & 0xff ; } if ( ++ n >= MAX_SPRITE_ON_SCANLINE && ! this . suppressSpriteFlicker ) { this . regs [ PpuReg . STATUS ] |= PpuStatusBit . SPRITE_OVERFLOW ; break ; } } } } private checkSprite0Hit ( hcount ) { const mask = PpuMaskBit . SHOW_BG | PpuMaskBit . SHOW_SPRITE ; if ( ( this . regs [ PpuReg . STATUS ] & PpuStatusBit . SPRITE0HIT ) !== 0 || ( this . regs [ PpuReg . MASK ] & mask ) !== mask ) return ; const sprite0y = this . oam [ OamElem . Y ] + 1 ; if ( hcount < sprite0y || hcount >= sprite0y + 16 ) return ; const sprite0x = this . oam [ OamElem . X ] ; if ( sprite0x >= 255 ) return ; const dy = this . getNonEmptySprite0Line ( ) ; if ( dy < 0 || hcount !== sprite0y + dy ) return ; this . regs [ PpuReg . STATUS ] |= PpuStatusBit . SPRITE0HIT ; } private getNonEmptySprite0Line ( ) { const oam = this . oam ; const chrStart = this . getSpritePatternTableAddress ( ) ; const isSprite8x16 = this . isSprite8x16 ( ) ; const h = isSprite8x16 ? 16 : 8 ; const index = oam [ OamElem . INDEX ] ; const attr = oam [ OamElem . ATTR ] ; const flipVert = ( attr & OamAttrBit . FLIP_VERT ) !== 0 ; const chridx = ( isSprite8x16 ? ( index & 0xfe ) * 16 + ( ( index & 1 ) << 12 ) : index * 16 + chrStart ) ; for ( let py = 0 ; py < h ; ++ py ) { const ppy = flipVert ? ( h - 1 ) - py : py ; const pat = getSpritePat ( this . chrData , chridx , ppy , false , this . hstatusMgr . current . chrBankOffset ) ; if ( pat !== 0 ) return py ; } return - 1 ; } private getSpritePatternTableAddress ( ) { if ( ( this . regs [ PpuReg . CTRL ] & PpuCtrlBit . SPRITE_SIZE ) === 0 ) return ( ( this . regs [ PpuReg . CTRL ] & PpuCtrlBit . SPRITE_PATTERN_TABLE_ADDRESS ) << 9 ) ; return 0 ; } private addHevent ( type , value , index = - 1 ) { if ( ! this . hstatusMgr . current . set ( type , value , index ) ) return ; let hcount = this . hcount + 1 ; if ( hcount > Const . HEIGHT ) { hcount = 0 ; } this . hevents . add ( hcount , type , value , index ) ; } private incScrollCounter ( ) { if ( ! this . visible ( ) ) return ; const lastHcount = this . hevents . getLastHcount ( ) ; if ( lastHcount < 0 ) return ; const hcount = this . hcount + 1 ; const dy = hcount - lastHcount ; if ( dy <= 0 ) return ; this . addHevent ( HEventType . SCROLL_CURR , incScroll ( this . hstatusMgr . current . scrollCurr , dy ) ) ; } private visible ( ) { return this . hcount < Const . HEIGHT && ( this . hstatusMgr . current . ppuMask & ( PpuMaskBit . SHOW_SPRITE | PpuMaskBit . SHOW_BG ) ) !== 0 ; } private updateCoarseX ( ) { if ( this . visible ( ) ) { const scrollCurr = ( ( this . hstatusMgr . current . scrollCurr & ~ 0x041f ) | ( this . ppuAddr & 0x041f ) ) ; this . addHevent ( HEventType . SCROLL_CURR , scrollCurr ) ; } } private readPpuDirect ( addr ) { if ( addr >= 0x2000 ) { return this . vram [ addr ] ; } else { const bankOffset = this . hstatusMgr . current . chrBankOffset [ ( addr >> 10 ) & 7 ] ; return this . chrData [ ( addr & 0x3ff ) + bankOffset ] ; } } }