Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- ================================================================
- -- OC-NES
- -- Mapper 0 / NROM
- -- Improved PPU + CPU/PPU timing
- --
- -- ROM: /home/game.nes
- -- Canvas: 160x50 terminal cells
- -- 320x200 Braille pixels
- --
- -- Controls:
- -- W/A/S/D = D-pad
- -- J = A
- -- K = B
- -- Space = Select
- -- Enter = Start
- -- Q = Quit
- -- ================================================================
- local component = require("component")
- local computer = require("computer")
- local event = require("event")
- local keyboard = require("keyboard")
- local unicode = require("unicode")
- local uchar = unicode.char
- local gpu = component.gpu
- if not gpu then
- error("A GPU is required.")
- end
- if not component.isAvailable("screen") then
- error("A screen is required.")
- end
- -- ================================================================
- -- CONFIG
- -- ================================================================
- local ROM_FILE = "/home/game.nes"
- local CW, CH = 160, 50
- local VRAM_W = CW * 2
- local VRAM_H = CH * 4
- local NES_W = 256
- local NES_H = 240
- -- NTSC NES CPU is ~1.789773 MHz.
- -- One frame is approximately 29780.5 CPU cycles.
- local CPU_CYCLES_FRAME = 29780
- local CPU_FRAME_ALTERNATE = 1
- local FRAME_TIME = 1 / 60.0988
- -- PPU:
- -- 341 PPU cycles per scanline
- -- 262 scanlines per frame
- local PPU_CYCLES_PER_SCANLINE = 341
- local PPU_SCANLINES = 262
- -- ================================================================
- -- SCREEN
- -- ================================================================
- local maxW, maxH = gpu.maxResolution()
- local drawW = math.min(CW, maxW)
- local drawH = math.min(CH, maxH)
- gpu.setResolution(drawW, drawH)
- gpu.setBackground(0x000000)
- gpu.setForeground(0xFFFFFF)
- gpu.fill(1, 1, drawW, drawH, " ")
- -- ================================================================
- -- BRAILLE
- -- ================================================================
- local W1, W2, W3, W4 = 1, 8, 2, 16
- local W5, W6, W7, W8 = 4, 32, 64, 128
- local brailleChars = {}
- for i = 0, 255 do
- brailleChars[i] = uchar(0x2800 + i)
- end
- local cellPix = {0,0,0,0,0,0,0,0}
- local colorCount = {}
- local function chooseColors()
- for k in pairs(colorCount) do
- colorCount[k] = nil
- end
- local p = cellPix
- for i = 1, 8 do
- local c = p[i]
- colorCount[c] = (colorCount[c] or 0) + 1
- end
- local bg = p[1]
- local fg = p[1]
- local best = -1
- local second = -1
- for c, n in pairs(colorCount) do
- if n > best then
- second = best
- fg = bg
- best = n
- bg = c
- elseif n > second then
- second = n
- fg = c
- end
- end
- return bg, fg
- end
- -- ================================================================
- -- FILE LOADING
- -- ================================================================
- local function readFile(path)
- local f, err = io.open(path, "rb")
- if not f then
- error("Cannot open " .. path .. ": " .. tostring(err))
- end
- local data = f:read("*a")
- f:close()
- return data
- end
- local rom = readFile(ROM_FILE)
- local function u8(s, p)
- return string.byte(s, p) or 0
- end
- -- ================================================================
- -- iNES HEADER
- -- ================================================================
- if #rom < 16 then
- error("ROM too small for iNES header.")
- end
- if rom:sub(1, 4) ~= "NES\26" then
- error("Not an iNES ROM.")
- end
- local prgBanks = u8(rom, 5)
- local chrBanks = u8(rom, 6)
- local flags6 = u8(rom, 7)
- local flags7 = u8(rom, 8)
- local prgRamBanks = u8(rom, 9)
- if prgRamBanks == 0 then prgRamBanks = 1 end
- local mapper =
- math.floor(flags6 / 16) +
- math.floor(flags7 / 16) * 16
- if mapper ~= 0 then
- error(
- "Unsupported mapper " ..
- tostring(mapper) ..
- ". Only Mapper 0 is supported."
- )
- end
- if prgBanks ~= 1 and prgBanks ~= 2 then
- error("Mapper 0 requires 1 or 2 PRG-ROM banks.")
- end
- if chrBanks ~= 0 and chrBanks ~= 1 then
- error("Mapper 0 supports 0 or 1 CHR-ROM banks (CHR-RAM or 8 KiB CHR-ROM).")
- end
- local trainer = (flags6 & 0x04) ~= 0
- local romOffset =
- 16 +
- (trainer and 512 or 0)
- local PRG_SIZE = prgBanks * 16384
- local CHR_SIZE = 8192
- local CHR_RAM = chrBanks == 0
- if #rom < romOffset + PRG_SIZE + chrBanks * CHR_SIZE then
- error("ROM file is truncated.")
- end
- local prg = {}
- local chr = {}
- for i = 0, PRG_SIZE - 1 do
- prg[i] = u8(rom, romOffset + i + 1)
- end
- local chrOffset = romOffset + PRG_SIZE
- for i = 0, CHR_SIZE - 1 do
- if CHR_RAM then
- chr[i] = 0
- else
- chr[i] = u8(rom, chrOffset + i + 1)
- end
- end
- -- ================================================================
- -- NES MEMORY
- -- ================================================================
- local ram = {}
- for i = 0, 0x7FF do
- ram[i] = 0
- end
- local PRG_RAM_SIZE = prgRamBanks * 8192
- local prgRam = {}
- for i = 0, PRG_RAM_SIZE - 1 do
- prgRam[i] = 0
- end
- local ppuRegs = {}
- for i = 0, 7 do
- ppuRegs[i] = 0
- end
- local apuRegs = {}
- for i = 0, 0x1F do
- apuRegs[i] = 0
- end
- -- Mapper 0 normally has 2 KiB CIRAM, but four-screen
- -- cartridges provide 4 KiB of dedicated nametable VRAM.
- local FOUR_SCREEN = (flags6 & 0x08) ~= 0
- local NAMETABLE_SIZE = FOUR_SCREEN and 4096 or 2048
- local nametable = {}
- for i = 0, NAMETABLE_SIZE - 1 do
- nametable[i] = 0
- end
- local palette = {}
- for i = 0, 31 do
- palette[i] = 0
- end
- local oam = {}
- for i = 0, 255 do
- oam[i] = 0
- end
- -- ================================================================
- -- CONTROLLER
- -- ================================================================
- local buttons = {
- A = false,
- B = false,
- SELECT = false,
- START = false,
- UP = false,
- DOWN = false,
- LEFT = false,
- RIGHT = false
- }
- local controllerLatch = 0
- local controllerIndex = 0
- local function controllerByte()
- local v = 0
- if buttons.A then v = v | 0x01 end
- if buttons.B then v = v | 0x02 end
- if buttons.SELECT then v = v | 0x04 end
- if buttons.START then v = v | 0x08 end
- if buttons.UP then v = v | 0x10 end
- if buttons.DOWN then v = v | 0x20 end
- if buttons.LEFT then v = v | 0x40 end
- if buttons.RIGHT then v = v | 0x80 end
- return v
- end
- local function controllerRead()
- if controllerLatch ~= 0 then
- return controllerByte() & 1
- end
- if controllerIndex < 8 then
- local v =
- (controllerByte() >> controllerIndex) & 1
- controllerIndex = controllerIndex + 1
- return v
- end
- return 1
- end
- -- ================================================================
- -- PPU ADDRESS / SCROLL STATE
- -- ================================================================
- local ppuAddr = 0
- local ppuAddrLatch = false
- local ppuDataBuffer = 0
- local ppuScrollLatch = false
- local ppuScrollX = 0
- local ppuScrollY = 0
- local oamAddr = 0
- -- PPU timing state.
- local ppuCycle = 0
- local ppuScanline = 0
- local ppuFrame = 0
- -- Prevent repeated NMI triggering during the same VBlank.
- local nmiOccurredThisVBlank = false
- local nmiPending = false
- local oamDmaPending = false
- -- ================================================================
- -- PPU MEMORY
- -- ================================================================
- local function mirrorNametable(addr)
- local a = (addr - 0x2000) & 0x0FFF
- local tableIndex = (a >> 10) & 3
- local offset = a & 0x03FF
- -- Four-screen NROM has a full 4 KiB of nametable VRAM.
- if FOUR_SCREEN then
- return (tableIndex * 0x400) + offset
- end
- -- iNES flags6 bit 0:
- --
- -- 0 = horizontal mirroring
- -- $2000 = $2400
- -- $2800 = $2C00
- --
- -- 1 = vertical mirroring
- -- $2000 = $2800
- -- $2400 = $2C00
- if (flags6 & 1) ~= 0 then
- -- Vertical
- if tableIndex == 0 or tableIndex == 2 then
- return offset
- else
- return 0x400 + offset
- end
- else
- -- Horizontal
- if tableIndex == 0 or tableIndex == 1 then
- return offset
- else
- return 0x400 + offset
- end
- end
- end
- local function paletteAddress(addr)
- local p = (addr - 0x3F00) & 0x1F
- -- Universal background mirrors.
- if p == 0x10 then p = 0x00 end
- if p == 0x14 then p = 0x04 end
- if p == 0x18 then p = 0x08 end
- if p == 0x1C then p = 0x0C end
- return p
- end
- local function ppuRead(addr)
- addr = addr & 0x3FFF
- if addr < 0x2000 then
- -- CHR-ROM.
- return chr[addr] or 0
- elseif addr < 0x3F00 then
- return nametable[
- mirrorNametable(addr)
- ] or 0
- else
- return palette[
- paletteAddress(addr)
- ] or 0
- end
- end
- local function ppuWrite(addr, value)
- addr = addr & 0x3FFF
- value = value & 0xFF
- if addr < 0x2000 then
- if CHR_RAM then
- chr[addr] = value
- end
- return
- elseif addr < 0x3F00 then
- nametable[
- mirrorNametable(addr)
- ] = value
- else
- palette[
- paletteAddress(addr)
- ] = value
- end
- end
- local function ppuIncrement()
- -- PPUCTRL bit 2.
- --
- -- 0 = +1
- -- 1 = +32
- if (ppuRegs[0] & 0x04) ~= 0 then
- return 32
- end
- return 1
- end
- -- ================================================================
- -- CPU MEMORY
- -- ================================================================
- local function cpuRead(addr)
- addr = addr & 0xFFFF
- -- --------------------------------------------------------------
- -- CPU RAM
- -- --------------------------------------------------------------
- if addr < 0x2000 then
- return ram[addr & 0x07FF] or 0
- -- --------------------------------------------------------------
- -- PPU registers
- -- --------------------------------------------------------------
- elseif addr < 0x4000 then
- local r =
- 0x2000 + (addr & 7)
- -- PPUSTATUS
- if r == 0x2002 then
- local v = ppuRegs[2] or 0
- -- Reading PPUSTATUS clears VBlank.
- ppuRegs[2] = v & 0x7F
- -- Reading status resets both $2005/$2006 write latches.
- ppuAddrLatch = false
- ppuScrollLatch = false
- return v
- -- OAMDATA
- elseif r == 0x2004 then
- local v = oam[oamAddr] or 0
- -- OAM attribute bits 2-4 are not stored on the PPU bus.
- if (oamAddr & 3) == 2 then
- v = v & 0xE3
- end
- return v
- -- PPUDATA
- elseif r == 0x2007 then
- local value = ppuRead(ppuAddr)
- if ppuAddr < 0x3F00 then
- local old = ppuDataBuffer
- ppuDataBuffer = value
- ppuAddr =
- (ppuAddr + ppuIncrement()) &
- 0x3FFF
- return old
- else
- -- Palette reads are not delayed.
- -- Fill the internal buffer with the mirrored
- -- nametable-side value.
- ppuDataBuffer =
- ppuRead(
- (ppuAddr - 0x1000) &
- 0x3FFF
- )
- ppuAddr =
- (ppuAddr + ppuIncrement()) &
- 0x3FFF
- return value
- end
- end
- return ppuRegs[r - 0x2000] or 0
- -- --------------------------------------------------------------
- -- APU / controllers
- -- --------------------------------------------------------------
- elseif addr < 0x4020 then
- if addr == 0x4016 then
- return controllerRead()
- elseif addr == 0x4017 then
- return 0x40
- else
- return apuRegs[
- addr - 0x4000
- ] or 0
- end
- -- --------------------------------------------------------------
- -- Cartridge / expansion space
- -- --------------------------------------------------------------
- elseif addr < 0x8000 then
- return prgRam[(addr - 0x6000) % PRG_RAM_SIZE] or 0
- -- --------------------------------------------------------------
- -- PRG ROM
- -- --------------------------------------------------------------
- else
- local p = addr - 0x8000
- if PRG_SIZE == 16384 then
- p = p & 0x3FFF
- else
- p = p & 0x7FFF
- end
- return prg[p] or 0
- end
- end
- local function cpuWrite(addr, value)
- addr = addr & 0xFFFF
- value = value & 0xFF
- -- --------------------------------------------------------------
- -- CPU RAM
- -- --------------------------------------------------------------
- if addr < 0x2000 then
- ram[addr & 0x07FF] = value
- -- --------------------------------------------------------------
- -- PPU registers
- -- --------------------------------------------------------------
- elseif addr < 0x4000 then
- local r =
- 0x2000 + (addr & 7)
- -- PPUCTRL
- if r == 0x2000 then
- local old = ppuRegs[0]
- ppuRegs[0] = value
- -- If NMI gets enabled while VBlank is already active,
- -- generate the NMI.
- if
- (old & 0x80) == 0 and
- (value & 0x80) ~= 0 and
- (ppuRegs[2] & 0x80) ~= 0
- then
- -- NMI is recognized at a CPU instruction boundary, not
- -- in the middle of the instruction that wrote $2000.
- nmiPending = true
- end
- -- PPUMASK
- elseif r == 0x2001 then
- ppuRegs[1] = value
- -- OAMADDR
- elseif r == 0x2003 then
- oamAddr = value
- ppuRegs[3] = value
- -- OAMDATA
- elseif r == 0x2004 then
- if (oamAddr & 3) == 2 then
- value = value & 0xE3
- end
- oam[oamAddr] = value
- oamAddr =
- (oamAddr + 1) & 0xFF
- ppuRegs[3] = oamAddr
- -- PPUSCROLL
- elseif r == 0x2005 then
- if not ppuScrollLatch then
- -- First write supplies horizontal scroll.
- ppuScrollX = value & 0xFF
- ppuScrollLatch = true
- else
- -- Second write supplies vertical scroll.
- ppuScrollY = value & 0xFF
- ppuScrollLatch = false
- end
- -- PPUADDR
- elseif r == 0x2006 then
- if not ppuAddrLatch then
- -- First write:
- -- bits 8-13.
- ppuAddr =
- ((value & 0x3F) << 8) |
- (ppuAddr & 0x00FF)
- ppuAddrLatch = true
- else
- -- Second write:
- -- bits 0-7.
- ppuAddr =
- (ppuAddr & 0x3F00) |
- value
- ppuAddr =
- ppuAddr & 0x3FFF
- ppuAddrLatch = false
- end
- -- PPUDATA
- elseif r == 0x2007 then
- ppuWrite(
- ppuAddr,
- value
- )
- ppuAddr =
- (ppuAddr + ppuIncrement()) &
- 0x3FFF
- end
- -- --------------------------------------------------------------
- -- APU / DMA / controller
- -- --------------------------------------------------------------
- elseif addr < 0x4020 then
- -- OAMDMA
- if addr == 0x4014 then
- local base = value << 8
- for i = 0, 255 do
- oam[(oamAddr + i) & 0xFF] =
- cpuRead(
- (base + i) & 0xFFFF
- )
- end
- oamAddr =
- (oamAddr + 256) & 0xFF
- -- OAM DMA suspends the CPU for 513 or 514 cycles after
- -- the $4014 write. The main loop accounts for the stall.
- oamDmaPending = true
- -- Controller strobe
- elseif addr == 0x4016 then
- local newLatch = value & 1
- if newLatch ~= 0 then
- controllerLatch = 1
- else
- if controllerLatch ~= 0 then
- controllerIndex = 0
- end
- controllerLatch = 0
- end
- else
- apuRegs[
- addr - 0x4000
- ] = value
- end
- -- --------------------------------------------------------------
- -- No NROM PRG-RAM.
- -- --------------------------------------------------------------
- elseif addr < 0x8000 then
- prgRam[(addr - 0x6000) % PRG_RAM_SIZE] = value
- -- --------------------------------------------------------------
- -- PRG-ROM writes ignored.
- -- --------------------------------------------------------------
- else
- return
- end
- end
- -- ================================================================
- -- 6502 CPU
- -- ================================================================
- local A, X, Y = 0, 0, 0
- local SP = 0xFD
- local PC = 0
- local P = 0x24
- local cpuTotalCycles = 0
- local C = 1
- local Z = 2
- local I = 4
- local D = 8
- local B = 16
- local U = 32
- local V = 64
- local N = 128
- local function setFlag(f, enabled)
- if enabled then
- P = P | f
- else
- P = P & (~f)
- end
- end
- local function getFlag(f)
- return (P & f) ~= 0
- end
- local function setZN(v)
- v = v & 0xFF
- setFlag(Z, v == 0)
- setFlag(N, (v & 0x80) ~= 0)
- end
- local function push(v)
- cpuWrite(
- 0x100 + SP,
- v
- )
- SP =
- (SP - 1) & 0xFF
- end
- local function pop()
- SP =
- (SP + 1) & 0xFF
- return cpuRead(
- 0x100 + SP
- )
- end
- local function fetch()
- local v = cpuRead(PC)
- PC =
- (PC + 1) & 0xFFFF
- return v
- end
- local function fetch16()
- local lo = fetch()
- local hi = fetch()
- return lo | (hi << 8)
- end
- local function zp()
- return fetch()
- end
- local function zpx()
- return (fetch() + X) & 0xFF
- end
- local function zpy()
- return (fetch() + Y) & 0xFF
- end
- local function abs()
- return fetch16()
- end
- local function absx()
- local base = fetch16()
- local addr = (base + X) & 0xFFFF
- return addr, ((base & 0xFF00) ~= (addr & 0xFF00))
- end
- local function absy()
- local base = fetch16()
- local addr = (base + Y) & 0xFFFF
- return addr, ((base & 0xFF00) ~= (addr & 0xFF00))
- end
- local function indx()
- local p =
- (fetch() + X) & 0xFF
- return
- cpuRead(p) |
- (cpuRead((p + 1) & 0xFF) << 8)
- end
- local function indy()
- local p = fetch()
- local base =
- cpuRead(p) |
- (cpuRead((p + 1) & 0xFF) << 8)
- local addr = (base + Y) & 0xFFFF
- return addr, ((base & 0xFF00) ~= (addr & 0xFF00))
- end
- local function branch(cond)
- local off = fetch()
- if off >= 128 then
- off = off - 256
- end
- if not cond then
- return 2
- end
- local oldPC = PC
- PC = (PC + off) & 0xFFFF
- return
- 3 + (((oldPC & 0xFF00) ~= (PC & 0xFF00)) and 1 or 0)
- end
- local function adc(v)
- local carry =
- getFlag(C) and 1 or 0
- local a = A
- local result =
- a + v + carry
- setFlag(
- C,
- result > 255
- )
- setFlag(
- V,
- ((~(a ~ v)) &
- (a ~ result) &
- 0x80) ~= 0
- )
- A =
- result & 0xFF
- setZN(A)
- end
- local function sbc(v)
- adc(v ~ 0xFF)
- end
- local function compare(reg, v)
- local r =
- (reg - v) & 0xFF
- setFlag(
- C,
- reg >= v
- )
- setZN(r)
- end
- local function asl(v)
- setFlag(
- C,
- (v & 0x80) ~= 0
- )
- v =
- (v << 1) & 0xFF
- setZN(v)
- return v
- end
- local function lsr(v)
- setFlag(
- C,
- (v & 1) ~= 0
- )
- v =
- (v >> 1) & 0xFF
- setZN(v)
- return v
- end
- local function rol(v)
- local c =
- getFlag(C) and 1 or 0
- setFlag(
- C,
- (v & 0x80) ~= 0
- )
- v =
- ((v << 1) | c) & 0xFF
- setZN(v)
- return v
- end
- local function ror(v)
- local c =
- getFlag(C) and 1 or 0
- setFlag(
- C,
- (v & 1) ~= 0
- )
- v =
- ((v >> 1) | (c << 7)) & 0xFF
- setZN(v)
- return v
- end
- local function lda(v)
- A = v & 0xFF
- setZN(A)
- end
- local function ldx(v)
- X = v & 0xFF
- setZN(X)
- end
- local function ldy(v)
- Y = v & 0xFF
- setZN(Y)
- end
- -- ---------------------------------------------------------------
- -- CPU reset
- -- ---------------------------------------------------------------
- local halted = false
- local function cpuReset()
- A = 0
- X = 0
- Y = 0
- SP = 0xFD
- P = 0x24
- cpuTotalCycles = 0
- PC =
- cpuRead(0xFFFC) |
- (cpuRead(0xFFFD) << 8)
- halted = false
- end
- -- ================================================================
- -- CPU STEP
- -- ================================================================
- local function cpuStep()
- if halted then
- return 2
- end
- local op = fetch()
- -- --------------------------------------------------------------
- -- LDA
- -- --------------------------------------------------------------
- if op == 0xA9 then
- lda(fetch())
- return 2
- elseif op == 0xA5 then
- lda(cpuRead(zp()))
- return 3
- elseif op == 0xB5 then
- lda(cpuRead(zpx()))
- return 4
- elseif op == 0xAD then
- lda(cpuRead(abs()))
- return 4
- elseif op == 0xBD then
- local a, cross = absx()
- lda(cpuRead(a))
- return 4 + (cross and 1 or 0)
- elseif op == 0xB9 then
- local a, cross = absy()
- lda(cpuRead(a))
- return 4 + (cross and 1 or 0)
- elseif op == 0xA1 then
- lda(cpuRead(indx()))
- return 6
- elseif op == 0xB1 then
- local a, cross = indy()
- lda(cpuRead(a))
- return 5 + (cross and 1 or 0)
- -- --------------------------------------------------------------
- -- LDX
- -- --------------------------------------------------------------
- elseif op == 0xA2 then
- ldx(fetch())
- return 2
- elseif op == 0xA6 then
- ldx(cpuRead(zp()))
- return 3
- elseif op == 0xB6 then
- ldx(cpuRead(zpy()))
- return 4
- elseif op == 0xAE then
- ldx(cpuRead(abs()))
- return 4
- elseif op == 0xBE then
- local a, cross = absy()
- ldx(cpuRead(a))
- return 4 + (cross and 1 or 0)
- -- --------------------------------------------------------------
- -- LDY
- -- --------------------------------------------------------------
- elseif op == 0xA0 then
- ldy(fetch())
- return 2
- elseif op == 0xA4 then
- ldy(cpuRead(zp()))
- return 3
- elseif op == 0xB4 then
- ldy(cpuRead(zpx()))
- return 4
- elseif op == 0xAC then
- ldy(cpuRead(abs()))
- return 4
- elseif op == 0xBC then
- local a, cross = absx()
- ldy(cpuRead(a))
- return 4 + (cross and 1 or 0)
- -- --------------------------------------------------------------
- -- STA
- -- --------------------------------------------------------------
- elseif op == 0x85 then
- cpuWrite(zp(), A)
- return 3
- elseif op == 0x95 then
- cpuWrite(zpx(), A)
- return 4
- elseif op == 0x8D then
- cpuWrite(abs(), A)
- return 4
- elseif op == 0x9D then
- cpuWrite(absx(), A)
- return 5
- elseif op == 0x99 then
- cpuWrite(absy(), A)
- return 5
- elseif op == 0x81 then
- cpuWrite(indx(), A)
- return 6
- elseif op == 0x91 then
- cpuWrite(indy(), A)
- return 6
- -- --------------------------------------------------------------
- -- STX
- -- --------------------------------------------------------------
- elseif op == 0x86 then
- cpuWrite(zp(), X)
- return 3
- elseif op == 0x96 then
- cpuWrite(zpy(), X)
- return 4
- elseif op == 0x8E then
- cpuWrite(abs(), X)
- return 4
- -- --------------------------------------------------------------
- -- STY
- -- --------------------------------------------------------------
- elseif op == 0x84 then
- cpuWrite(zp(), Y)
- return 3
- elseif op == 0x94 then
- cpuWrite(zpx(), Y)
- return 4
- elseif op == 0x8C then
- cpuWrite(abs(), Y)
- return 4
- -- --------------------------------------------------------------
- -- Transfers
- -- --------------------------------------------------------------
- elseif op == 0xAA then
- X = A
- setZN(X)
- return 2
- elseif op == 0xA8 then
- Y = A
- setZN(Y)
- return 2
- elseif op == 0x8A then
- A = X
- setZN(A)
- return 2
- elseif op == 0x98 then
- A = Y
- setZN(A)
- return 2
- elseif op == 0xBA then
- X = SP
- setZN(X)
- return 2
- elseif op == 0x9A then
- SP = X
- return 2
- -- --------------------------------------------------------------
- -- INC / DEC registers
- -- --------------------------------------------------------------
- elseif op == 0xE8 then
- X = (X + 1) & 0xFF
- setZN(X)
- return 2
- elseif op == 0xCA then
- X = (X - 1) & 0xFF
- setZN(X)
- return 2
- elseif op == 0xC8 then
- Y = (Y + 1) & 0xFF
- setZN(Y)
- return 2
- elseif op == 0x88 then
- Y = (Y - 1) & 0xFF
- setZN(Y)
- return 2
- -- --------------------------------------------------------------
- -- INC memory
- -- --------------------------------------------------------------
- elseif op == 0xE6 then
- local a = zp()
- local v = (cpuRead(a) + 1) & 0xFF
- cpuWrite(a, v)
- setZN(v)
- return 5
- elseif op == 0xF6 then
- local a = zpx()
- local v = (cpuRead(a) + 1) & 0xFF
- cpuWrite(a, v)
- setZN(v)
- return 6
- elseif op == 0xEE then
- local a = abs()
- local v = (cpuRead(a) + 1) & 0xFF
- cpuWrite(a, v)
- setZN(v)
- return 6
- elseif op == 0xFE then
- local a = absx()
- local v = (cpuRead(a) + 1) & 0xFF
- cpuWrite(a, v)
- setZN(v)
- return 7
- -- --------------------------------------------------------------
- -- DEC memory
- -- --------------------------------------------------------------
- elseif op == 0xC6 then
- local a = zp()
- local v = (cpuRead(a) - 1) & 0xFF
- cpuWrite(a, v)
- setZN(v)
- return 5
- elseif op == 0xD6 then
- local a = zpx()
- local v = (cpuRead(a) - 1) & 0xFF
- cpuWrite(a, v)
- setZN(v)
- return 6
- elseif op == 0xCE then
- local a = abs()
- local v = (cpuRead(a) - 1) & 0xFF
- cpuWrite(a, v)
- setZN(v)
- return 6
- elseif op == 0xDE then
- local a = absx()
- local v = (cpuRead(a) - 1) & 0xFF
- cpuWrite(a, v)
- setZN(v)
- return 7
- -- --------------------------------------------------------------
- -- ADC
- -- --------------------------------------------------------------
- elseif op == 0x69 then
- adc(fetch())
- return 2
- elseif op == 0x65 then
- adc(cpuRead(zp()))
- return 3
- elseif op == 0x75 then
- adc(cpuRead(zpx()))
- return 4
- elseif op == 0x6D then
- adc(cpuRead(abs()))
- return 4
- elseif op == 0x7D then
- local a, cross = absx()
- adc(cpuRead(a))
- return 4 + (cross and 1 or 0)
- elseif op == 0x79 then
- local a, cross = absy()
- adc(cpuRead(a))
- return 4 + (cross and 1 or 0)
- elseif op == 0x61 then
- adc(cpuRead(indx()))
- return 6
- elseif op == 0x71 then
- local a, cross = indy()
- adc(cpuRead(a))
- return 5 + (cross and 1 or 0)
- -- --------------------------------------------------------------
- -- SBC
- -- --------------------------------------------------------------
- elseif op == 0xE9 then
- sbc(fetch())
- return 2
- elseif op == 0xE5 then
- sbc(cpuRead(zp()))
- return 3
- elseif op == 0xF5 then
- sbc(cpuRead(zpx()))
- return 4
- elseif op == 0xED then
- sbc(cpuRead(abs()))
- return 4
- elseif op == 0xFD then
- local a, cross = absx()
- sbc(cpuRead(a))
- return 4 + (cross and 1 or 0)
- elseif op == 0xF9 then
- local a, cross = absy()
- sbc(cpuRead(a))
- return 4 + (cross and 1 or 0)
- elseif op == 0xE1 then
- sbc(cpuRead(indx()))
- return 6
- elseif op == 0xF1 then
- local a, cross = indy()
- sbc(cpuRead(a))
- return 5 + (cross and 1 or 0)
- -- --------------------------------------------------------------
- -- AND
- -- --------------------------------------------------------------
- elseif op == 0x29 then
- A = A & fetch()
- setZN(A)
- return 2
- elseif op == 0x25 then
- A = A & cpuRead(zp())
- setZN(A)
- return 3
- elseif op == 0x35 then
- A = A & cpuRead(zpx())
- setZN(A)
- return 4
- elseif op == 0x2D then
- A = A & cpuRead(abs())
- setZN(A)
- return 4
- elseif op == 0x3D then
- local a, cross = absx()
- A = A & cpuRead(a)
- setZN(A)
- return 4 + (cross and 1 or 0)
- elseif op == 0x39 then
- local a, cross = absy()
- A = A & cpuRead(a)
- setZN(A)
- return 4 + (cross and 1 or 0)
- elseif op == 0x21 then
- A = A & cpuRead(indx())
- setZN(A)
- return 6
- elseif op == 0x31 then
- local a, cross = indy()
- A = A & cpuRead(a)
- setZN(A)
- return 5 + (cross and 1 or 0)
- -- --------------------------------------------------------------
- -- ORA
- -- --------------------------------------------------------------
- elseif op == 0x09 then
- A = A | fetch()
- setZN(A)
- return 2
- elseif op == 0x05 then
- A = A | cpuRead(zp())
- setZN(A)
- return 3
- elseif op == 0x15 then
- A = A | cpuRead(zpx())
- setZN(A)
- return 4
- elseif op == 0x0D then
- A = A | cpuRead(abs())
- setZN(A)
- return 4
- elseif op == 0x1D then
- local a, cross = absx()
- A = A | cpuRead(a)
- setZN(A)
- return 4 + (cross and 1 or 0)
- elseif op == 0x19 then
- local a, cross = absy()
- A = A | cpuRead(a)
- setZN(A)
- return 4 + (cross and 1 or 0)
- elseif op == 0x01 then
- A = A | cpuRead(indx())
- setZN(A)
- return 6
- elseif op == 0x11 then
- local a, cross = indy()
- A = A | cpuRead(a)
- setZN(A)
- return 5 + (cross and 1 or 0)
- -- --------------------------------------------------------------
- -- EOR
- -- --------------------------------------------------------------
- elseif op == 0x49 then
- A = A ~ fetch()
- setZN(A)
- return 2
- elseif op == 0x45 then
- A = A ~ cpuRead(zp())
- setZN(A)
- return 3
- elseif op == 0x55 then
- A = A ~ cpuRead(zpx())
- setZN(A)
- return 4
- elseif op == 0x4D then
- A = A ~ cpuRead(abs())
- setZN(A)
- return 4
- elseif op == 0x5D then
- local a, cross = absx()
- A = A ~ cpuRead(a)
- setZN(A)
- return 4 + (cross and 1 or 0)
- elseif op == 0x59 then
- local a, cross = absy()
- A = A ~ cpuRead(a)
- setZN(A)
- return 4 + (cross and 1 or 0)
- elseif op == 0x41 then
- A = A ~ cpuRead(indx())
- setZN(A)
- return 6
- elseif op == 0x51 then
- local a, cross = indy()
- A = A ~ cpuRead(a)
- setZN(A)
- return 5 + (cross and 1 or 0)
- -- --------------------------------------------------------------
- -- CMP
- -- --------------------------------------------------------------
- elseif op == 0xC9 then
- compare(A, fetch())
- return 2
- elseif op == 0xC5 then
- compare(A, cpuRead(zp()))
- return 3
- elseif op == 0xD5 then
- compare(A, cpuRead(zpx()))
- return 4
- elseif op == 0xCD then
- compare(A, cpuRead(abs()))
- return 4
- elseif op == 0xDD then
- local a, cross = absx()
- compare(A, cpuRead(a))
- return 4 + (cross and 1 or 0)
- elseif op == 0xD9 then
- local a, cross = absy()
- compare(A, cpuRead(a))
- return 4 + (cross and 1 or 0)
- elseif op == 0xC1 then
- compare(A, cpuRead(indx()))
- return 6
- elseif op == 0xD1 then
- local a, cross = indy()
- compare(A, cpuRead(a))
- return 5 + (cross and 1 or 0)
- -- --------------------------------------------------------------
- -- CPX
- -- --------------------------------------------------------------
- elseif op == 0xE0 then
- compare(X, fetch())
- return 2
- elseif op == 0xE4 then
- compare(X, cpuRead(zp()))
- return 3
- elseif op == 0xEC then
- compare(X, cpuRead(abs()))
- return 4
- -- --------------------------------------------------------------
- -- CPY
- -- --------------------------------------------------------------
- elseif op == 0xC0 then
- compare(Y, fetch())
- return 2
- elseif op == 0xC4 then
- compare(Y, cpuRead(zp()))
- return 3
- elseif op == 0xCC then
- compare(Y, cpuRead(abs()))
- return 4
- -- --------------------------------------------------------------
- -- ASL
- -- --------------------------------------------------------------
- elseif op == 0x0A then
- A = asl(A)
- return 2
- elseif op == 0x06 then
- local a = zp()
- cpuWrite(a, asl(cpuRead(a)))
- return 5
- elseif op == 0x16 then
- local a = zpx()
- cpuWrite(a, asl(cpuRead(a)))
- return 6
- elseif op == 0x0E then
- local a = abs()
- cpuWrite(a, asl(cpuRead(a)))
- return 6
- elseif op == 0x1E then
- local a = absx()
- cpuWrite(a, asl(cpuRead(a)))
- return 7
- -- --------------------------------------------------------------
- -- LSR
- -- --------------------------------------------------------------
- elseif op == 0x4A then
- A = lsr(A)
- return 2
- elseif op == 0x46 then
- local a = zp()
- cpuWrite(a, lsr(cpuRead(a)))
- return 5
- elseif op == 0x56 then
- local a = zpx()
- cpuWrite(a, lsr(cpuRead(a)))
- return 6
- elseif op == 0x4E then
- local a = abs()
- cpuWrite(a, lsr(cpuRead(a)))
- return 6
- elseif op == 0x5E then
- local a = absx()
- cpuWrite(a, lsr(cpuRead(a)))
- return 7
- -- --------------------------------------------------------------
- -- ROL
- -- --------------------------------------------------------------
- elseif op == 0x2A then
- A = rol(A)
- return 2
- elseif op == 0x26 then
- local a = zp()
- cpuWrite(a, rol(cpuRead(a)))
- return 5
- elseif op == 0x36 then
- local a = zpx()
- cpuWrite(a, rol(cpuRead(a)))
- return 6
- elseif op == 0x2E then
- local a = abs()
- cpuWrite(a, rol(cpuRead(a)))
- return 6
- elseif op == 0x3E then
- local a = absx()
- cpuWrite(a, rol(cpuRead(a)))
- return 7
- -- --------------------------------------------------------------
- -- ROR
- -- --------------------------------------------------------------
- elseif op == 0x6A then
- A = ror(A)
- return 2
- elseif op == 0x66 then
- local a = zp()
- cpuWrite(a, ror(cpuRead(a)))
- return 5
- elseif op == 0x76 then
- local a = zpx()
- cpuWrite(a, ror(cpuRead(a)))
- return 6
- elseif op == 0x6E then
- local a = abs()
- cpuWrite(a, ror(cpuRead(a)))
- return 6
- elseif op == 0x7E then
- local a = absx()
- cpuWrite(a, ror(cpuRead(a)))
- return 7
- -- --------------------------------------------------------------
- -- BIT
- -- --------------------------------------------------------------
- elseif op == 0x24 then
- local v = cpuRead(zp())
- setFlag(
- Z,
- (A & v) == 0
- )
- setFlag(
- V,
- (v & 0x40) ~= 0
- )
- setFlag(
- N,
- (v & 0x80) ~= 0
- )
- return 3
- elseif op == 0x2C then
- local v = cpuRead(abs())
- setFlag(
- Z,
- (A & v) == 0
- )
- setFlag(
- V,
- (v & 0x40) ~= 0
- )
- setFlag(
- N,
- (v & 0x80) ~= 0
- )
- return 4
- -- --------------------------------------------------------------
- -- Branches
- -- --------------------------------------------------------------
- elseif op == 0x10 then
- return branch(not getFlag(N))
- elseif op == 0x30 then
- return branch(getFlag(N))
- elseif op == 0x50 then
- return branch(not getFlag(V))
- elseif op == 0x70 then
- return branch(getFlag(V))
- elseif op == 0x90 then
- return branch(not getFlag(C))
- elseif op == 0xB0 then
- return branch(getFlag(C))
- elseif op == 0xD0 then
- return branch(not getFlag(Z))
- elseif op == 0xF0 then
- return branch(getFlag(Z))
- -- --------------------------------------------------------------
- -- JMP
- -- --------------------------------------------------------------
- elseif op == 0x4C then
- PC = abs()
- return 3
- elseif op == 0x6C then
- local ptr = abs()
- local lo = cpuRead(ptr)
- -- 6502 indirect JMP page-wrap bug.
- local hiAddr =
- (ptr & 0xFF00) |
- ((ptr + 1) & 0xFF)
- local hi =
- cpuRead(hiAddr)
- PC =
- lo |
- (hi << 8)
- return 5
- -- --------------------------------------------------------------
- -- JSR
- -- --------------------------------------------------------------
- elseif op == 0x20 then
- local target = abs()
- local ret =
- (PC - 1) & 0xFFFF
- push((ret >> 8) & 0xFF)
- push(ret & 0xFF)
- PC = target
- return 6
- -- --------------------------------------------------------------
- -- RTS
- -- --------------------------------------------------------------
- elseif op == 0x60 then
- local lo = pop()
- local hi = pop()
- PC =
- ((hi << 8) | lo) + 1
- PC =
- PC & 0xFFFF
- return 6
- -- --------------------------------------------------------------
- -- Stack
- -- --------------------------------------------------------------
- elseif op == 0x48 then
- push(A)
- return 3
- elseif op == 0x68 then
- A = pop()
- setZN(A)
- return 4
- elseif op == 0x08 then
- push(P | B | U)
- return 3
- elseif op == 0x28 then
- P = pop() | U
- return 4
- -- --------------------------------------------------------------
- -- Flags
- -- --------------------------------------------------------------
- elseif op == 0x18 then
- setFlag(C, false)
- return 2
- elseif op == 0x38 then
- setFlag(C, true)
- return 2
- elseif op == 0x58 then
- setFlag(I, false)
- return 2
- elseif op == 0x78 then
- setFlag(I, true)
- return 2
- elseif op == 0xB8 then
- setFlag(V, false)
- return 2
- elseif op == 0xD8 then
- setFlag(D, false)
- return 2
- elseif op == 0xF8 then
- setFlag(D, true)
- return 2
- -- --------------------------------------------------------------
- -- NOP
- -- --------------------------------------------------------------
- elseif op == 0xEA then
- return 2
- -- --------------------------------------------------------------
- -- BRK
- -- --------------------------------------------------------------
- elseif op == 0x00 then
- PC =
- (PC + 1) & 0xFFFF
- push((PC >> 8) & 0xFF)
- push(PC & 0xFF)
- push(P | B | U)
- setFlag(I, true)
- PC =
- cpuRead(0xFFFE) |
- (cpuRead(0xFFFF) << 8)
- return 7
- -- --------------------------------------------------------------
- -- RTI
- -- --------------------------------------------------------------
- elseif op == 0x40 then
- P = pop() | U
- local lo = pop()
- local hi = pop()
- PC =
- lo |
- (hi << 8)
- return 6
- -- --------------------------------------------------------------
- -- Common unofficial NOPs
- -- --------------------------------------------------------------
- elseif
- op == 0x1A or
- op == 0x3A or
- op == 0x5A or
- op == 0x7A or
- op == 0xDA or
- op == 0xFA
- then
- return 2
- elseif
- op == 0x80 or
- op == 0x82 or
- op == 0x89 or
- op == 0xC2 or
- op == 0xE2
- then
- fetch()
- return 2
- elseif
- op == 0x04 or
- op == 0x44 or
- op == 0x64
- then
- zp()
- return 3
- elseif
- op == 0x14 or
- op == 0x34 or
- op == 0x54 or
- op == 0x74 or
- op == 0xD4 or
- op == 0xF4
- then
- zpx()
- return 4
- elseif op == 0x0C then
- abs()
- return 4
- elseif
- op == 0x1C or
- op == 0x3C or
- op == 0x5C or
- op == 0x7C or
- op == 0xDC or
- op == 0xFC
- then
- local _, cross = absx()
- return 4 + (cross and 1 or 0)
- else
- halted = true
- return 2
- end
- end
- -- ================================================================
- -- INTERRUPT SERVICE
- -- ================================================================
- local function serviceNMI()
- if not nmiPending then
- return 0
- end
- nmiPending = false
- push((PC >> 8) & 0xFF)
- push(PC & 0xFF)
- push((P & ~B) | U)
- setFlag(I, true)
- PC =
- cpuRead(0xFFFA) |
- (cpuRead(0xFFFB) << 8)
- return 7
- end
- -- ================================================================
- -- NES PALETTE
- -- ================================================================
- local NES_COLORS = {
- 0x545454,0x001E74,0x081090,0x300088,
- 0x440064,0x5C0030,0x540400,0x3C1800,
- 0x202A00,0x083A00,0x004000,0x003C00,
- 0x00323C,0x000000,0x000000,0x000000,
- 0x989698,0x084CC4,0x3032EC,0x5C1EE4,
- 0x8814B0,0xA01464,0x982220,0x783C00,
- 0x545A00,0x287200,0x087C00,0x007628,
- 0x006678,0x000000,0x000000,0x000000,
- 0xECEEEC,0x4C9AEC,0x787CEC,0xB062EC,
- 0xE454EC,0xEC58B4,0xEC6A64,0xD48820,
- 0xA0AA00,0x74C400,0x4CD020,0x38C898,
- 0x38B8CC,0x3C3C3C,0x000000,0x000000,
- 0xECEEEC,0xA8CCEC,0xBCBCEC,0xD4B2EC,
- 0xECB4EC,0xECB4D4,0xECB4B0,0xE4C490,
- 0xCCD48C,0xB4E494,0xA8E4A8,0xA8DCC0,
- 0xA8D4D8,0xA8A8A8,0x000000,0x000000
- }
- local function colorForNES(index)
- index = index & 0x3F
- -- PPUMASK bit 0 forces the output through the grayscale entries.
- if (ppuRegs[1] & 0x01) ~= 0 then
- index = index & 0x30
- end
- return NES_COLORS[index + 1] or 0
- end
- -- ================================================================
- -- FRAMEBUFFERS
- -- ================================================================
- -- Indexed NES palette entries.
- local nesFrame = {}
- -- True if background pixel has nonzero pattern bits.
- local nesBgOpaque = {}
- for y = 0, NES_H - 1 do
- local row = {}
- local opaque = {}
- for x = 0, NES_W - 1 do
- row[x] = 0
- opaque[x] = false
- end
- nesFrame[y] = row
- nesBgOpaque[y] = opaque
- end
- -- ================================================================
- -- BACKGROUND RENDERING
- -- ================================================================
- local function getNametableBase(nt)
- nt = nt & 3
- if nt == 0 then
- return 0x2000
- elseif nt == 1 then
- return 0x2400
- elseif nt == 2 then
- return 0x2800
- else
- return 0x2C00
- end
- end
- local function renderBackgroundScanline(scanY)
- local control = ppuRegs[0] or 0
- local mask = ppuRegs[1] or 0
- local row =
- nesFrame[scanY]
- local opaque =
- nesBgOpaque[scanY]
- -- --------------------------------------------------------------
- -- Background disabled.
- -- --------------------------------------------------------------
- if (mask & 0x08) == 0 then
- local universal =
- ppuRead(0x3F00)
- for x = 0, NES_W - 1 do
- row[x] = universal
- opaque[x] = false
- end
- return
- end
- -- --------------------------------------------------------------
- -- Starting nametable from PPUCTRL.
- -- --------------------------------------------------------------
- local baseNT =
- control & 3
- -- Background pattern table.
- local patternBase
- if (control & 0x10) ~= 0 then
- patternBase = 0x1000
- else
- patternBase = 0x0000
- end
- local scrollX =
- ppuScrollX & 0xFF
- local scrollY =
- ppuScrollY & 0xFF
- for x = 0, NES_W - 1 do
- -- ------------------------------------------------------------
- -- World coordinates.
- --
- -- Horizontal background space is 512 pixels.
- -- Vertical background space is 480 pixels.
- -- ------------------------------------------------------------
- local worldX =
- scrollX + x
- local worldY =
- scrollY + scanY
- worldX =
- worldX % 512
- worldY =
- worldY % 480
- local ntX =
- math.floor(worldX / 256)
- local ntY =
- math.floor(worldY / 240)
- -- Determine logical nametable.
- --
- -- PPUCTRL selects the upper-left logical nametable.
- --
- -- Moving right toggles bit 0.
- -- Moving down toggles bit 1.
- local nt =
- (baseNT ~
- (ntX == 1 and 1 or 0) ~
- (ntY == 1 and 2 or 0)) &
- 3
- local localX =
- worldX % 256
- local localY =
- worldY % 240
- local tileX =
- math.floor(localX / 8)
- local tileY =
- math.floor(localY / 8)
- local fineX =
- localX & 7
- local fineY =
- localY & 7
- local ntBase =
- getNametableBase(nt)
- -- ------------------------------------------------------------
- -- Tile number.
- -- ------------------------------------------------------------
- local tileAddr =
- ntBase +
- tileY * 32 +
- tileX
- local tile =
- ppuRead(tileAddr)
- -- ------------------------------------------------------------
- -- Pattern data.
- -- ------------------------------------------------------------
- local patternAddr =
- patternBase +
- tile * 16 +
- fineY
- local lo =
- ppuRead(patternAddr)
- local hi =
- ppuRead(patternAddr + 8)
- local bit =
- 7 - fineX
- local c =
- ((lo >> bit) & 1) |
- (((hi >> bit) & 1) << 1)
- if x < 8 and (mask & 0x02) == 0 then
- c = 0
- end
- -- ------------------------------------------------------------
- -- Attribute table.
- -- ------------------------------------------------------------
- local attrAddr =
- ntBase +
- 0x3C0 +
- math.floor(tileY / 4) * 8 +
- math.floor(tileX / 4)
- local attr =
- ppuRead(attrAddr)
- local quadrant =
- ((math.floor(tileY / 2) & 1) * 2) +
- (math.floor(tileX / 2) & 1)
- local shift =
- quadrant * 2
- local pal =
- (attr >> shift) & 3
- -- ------------------------------------------------------------
- -- Palette.
- -- ------------------------------------------------------------
- local paletteIndex
- if c == 0 then
- paletteIndex = 0
- opaque[x] = false
- else
- paletteIndex =
- pal * 4 + c
- opaque[x] = true
- end
- row[x] =
- ppuRead(
- 0x3F00 + paletteIndex
- )
- end
- end
- -- ================================================================
- -- SPRITE RENDERING
- -- ================================================================
- local function renderSpritesScanline(scanY)
- local mask =
- ppuRegs[1] or 0
- -- Sprite rendering disabled.
- if (mask & 0x10) == 0 then
- return
- end
- local control =
- ppuRegs[0] or 0
- local spritePattern
- if (control & 0x08) ~= 0 then
- spritePattern = 0x1000
- else
- spritePattern = 0
- end
- local spriteHeight
- if (control & 0x20) ~= 0 then
- spriteHeight = 16
- else
- spriteHeight = 8
- end
- local row =
- nesFrame[scanY]
- local bgOpaque =
- nesBgOpaque[scanY]
- local spritesOnLine = 0
- local sprite0Hit = false
- -- Draw low-priority sprites first.
- -- Sprite 0 is highest priority.
- --
- -- This also means later sprite pixels can overwrite
- -- earlier sprite pixels.
- for i = 63, 0, -1 do
- local base =
- i * 4
- -- NES OAM Y coordinate is one less than the visible
- -- sprite position.
- local sy =
- (oam[base] or 0) + 1
- local tile =
- oam[base + 1] or 0
- local attr =
- oam[base + 2] or 0
- local sx =
- oam[base + 3] or 0
- local r =
- scanY - sy
- if r >= 0 and r < spriteHeight then
- spritesOnLine = spritesOnLine + 1
- if spritesOnLine > 8 then
- ppuRegs[2] = ppuRegs[2] | 0x20
- end
- local flipH =
- (attr & 0x40) ~= 0
- local flipV =
- (attr & 0x80) ~= 0
- local priorityBehind =
- (attr & 0x20) ~= 0
- local palNum =
- attr & 3
- local sourceY
- if flipV then
- sourceY =
- spriteHeight - 1 - r
- else
- sourceY = r
- end
- local tileIndex
- local patBase
- if spriteHeight == 16 then
- -- 8x16 sprites:
- --
- -- bit 0 selects pattern table.
- -- tile number bit 0 is ignored.
- patBase =
- (tile & 1) * 0x1000
- tileIndex =
- (tile & 0xFE) +
- math.floor(sourceY / 8)
- sourceY =
- sourceY & 7
- else
- patBase =
- spritePattern
- tileIndex =
- tile
- end
- local addr =
- patBase +
- tileIndex * 16 +
- sourceY
- local lo =
- ppuRead(addr)
- local hi =
- ppuRead(addr + 8)
- for xx = 0, 7 do
- local sourceX
- if flipH then
- sourceX = xx
- else
- sourceX = 7 - xx
- end
- local b1 =
- (lo >> sourceX) & 1
- local b2 =
- (hi >> sourceX) & 1
- local c =
- b1 | (b2 << 1)
- -- Color zero is transparent.
- if c ~= 0 then
- local px =
- sx + xx
- if px >= 0 and px < NES_W then
- -- Sprite 0 hit ignores sprite priority: it is a
- -- collision between opaque sprite-0 and background
- -- pixels. The leftmost 8 pixels are clipped according
- -- to PPUMASK.
- if
- i == 0 and
- not sprite0Hit and
- bgOpaque[px] and
- (px >= 8 or ((mask & 0x02) ~= 0 and (mask & 0x04) ~= 0))
- then
- ppuRegs[2] = ppuRegs[2] | 0x40
- sprite0Hit = true
- end
- if px >= 8 or (mask & 0x04) ~= 0 then
- -- Background has priority over this sprite
- -- when sprite priority bit is set.
- if
- not priorityBehind or
- not bgOpaque[px]
- then
- row[px] =
- ppuRead(
- 0x3F10 +
- palNum * 4 +
- c
- )
- end
- end
- end
- end
- end
- end
- end
- end
- -- ================================================================
- -- FRAME RENDER
- -- ================================================================
- local function renderNESFrame()
- for y = 0, NES_H - 1 do
- renderBackgroundScanline(y)
- renderSpritesScanline(y)
- end
- end
- -- ================================================================
- -- VRAM
- -- ================================================================
- local VRAM = {}
- for y = 0, VRAM_H - 1 do
- local row = {}
- for x = 0, VRAM_W - 1 do
- row[x] = 0
- end
- VRAM[y] = row
- end
- -- ================================================================
- -- SCALE NES -> 320x200
- -- ================================================================
- local function scaleToVRAM()
- for dy = 0, VRAM_H - 1 do
- local ny =
- math.floor(
- dy * NES_H / VRAM_H
- )
- if ny >= NES_H then
- ny = NES_H - 1
- end
- local srcRow =
- nesFrame[ny]
- local dstRow =
- VRAM[dy]
- for dx = 0, VRAM_W - 1 do
- local nx =
- math.floor(
- dx * NES_W / VRAM_W
- )
- if nx >= NES_W then
- nx = NES_W - 1
- end
- dstRow[dx] =
- colorForNES(
- srcRow[nx]
- )
- end
- end
- end
- -- ================================================================
- -- BRAILLE BLIT
- -- ================================================================
- local runChars = {}
- local function blitVRAMToGPU()
- local _gpu = gpu
- local _braille = brailleChars
- local _VRAM = VRAM
- local _CW =
- math.min(CW, drawW)
- local _CH =
- math.min(CH, drawH)
- local lastBg = -1
- local lastFg = -1
- for cy = 1, _CH do
- local vY =
- (cy - 1) * 4
- local row0 =
- _VRAM[vY]
- local row1 =
- _VRAM[vY + 1]
- local row2 =
- _VRAM[vY + 2]
- local row3 =
- _VRAM[vY + 3]
- local runStart = 0
- local runBg = -1
- local runFg = -1
- local runLen = 0
- for cx = 1, _CW do
- local vX =
- (cx - 1) * 2
- local p1 = row0[vX] or 0
- local p2 = row0[vX + 1] or 0
- local p3 = row1[vX] or 0
- local p4 = row1[vX + 1] or 0
- local p5 = row2[vX] or 0
- local p6 = row2[vX + 1] or 0
- local p7 = row3[vX] or 0
- local p8 = row3[vX + 1] or 0
- local bg
- local fg
- local char
- if
- p1 == p2 and
- p1 == p3 and
- p1 == p4 and
- p1 == p5 and
- p1 == p6 and
- p1 == p7 and
- p1 == p8
- then
- bg = p1
- fg = p1
- char = " "
- else
- cellPix[1] = p1
- cellPix[2] = p2
- cellPix[3] = p3
- cellPix[4] = p4
- cellPix[5] = p5
- cellPix[6] = p6
- cellPix[7] = p7
- cellPix[8] = p8
- bg, fg =
- chooseColors()
- if fg == bg then
- char = " "
- else
- local pattern = 0
- if p1 == fg then
- pattern = pattern + W1
- end
- if p2 == fg then
- pattern = pattern + W2
- end
- if p3 == fg then
- pattern = pattern + W3
- end
- if p4 == fg then
- pattern = pattern + W4
- end
- if p5 == fg then
- pattern = pattern + W5
- end
- if p6 == fg then
- pattern = pattern + W6
- end
- if p7 == fg then
- pattern = pattern + W7
- end
- if p8 == fg then
- pattern = pattern + W8
- end
- char =
- _braille[pattern]
- end
- end
- if
- runLen > 0 and
- bg == runBg and
- fg == runFg and
- cx == runStart + runLen
- then
- runLen =
- runLen + 1
- runChars[runLen] =
- char
- else
- if runLen > 0 then
- if runBg ~= lastBg then
- _gpu.setBackground(runBg)
- lastBg = runBg
- end
- if runFg ~= lastFg then
- _gpu.setForeground(runFg)
- lastFg = runFg
- end
- _gpu.set(
- runStart,
- cy,
- table.concat(
- runChars,
- "",
- 1,
- runLen
- )
- )
- end
- runStart = cx
- runBg = bg
- runFg = fg
- runLen = 1
- runChars[1] = char
- end
- end
- if runLen > 0 then
- if runBg ~= lastBg then
- _gpu.setBackground(runBg)
- lastBg = runBg
- end
- if runFg ~= lastFg then
- _gpu.setForeground(runFg)
- lastFg = runFg
- end
- _gpu.set(
- runStart,
- cy,
- table.concat(
- runChars,
- "",
- 1,
- runLen
- )
- )
- end
- end
- end
- -- ================================================================
- -- SPRITE 0 TIMING HELPER
- -- ================================================================
- local function spriteOverflowOnScanline(scanY)
- if (ppuRegs[1] & 0x10) == 0 then
- return false
- end
- local height = (ppuRegs[0] & 0x20) ~= 0 and 16 or 8
- local count = 0
- for i = 0, 63 do
- local sy = (oam[i * 4] or 0) + 1
- if scanY >= sy and scanY < sy + height then
- count = count + 1
- if count > 8 then
- return true
- end
- end
- end
- return false
- end
- local function sprite0HitX(scanY)
- if (ppuRegs[1] & 0x18) ~= 0x18 then
- return nil
- end
- local y = (oam[0] or 0) + 1
- local tile = oam[1] or 0
- local attr = oam[2] or 0
- local sx = oam[3] or 0
- local control = ppuRegs[0] or 0
- local height = (control & 0x20) ~= 0 and 16 or 8
- local row = scanY - y
- if row < 0 or row >= height then
- return nil
- end
- if (scanY < 0 or scanY >= NES_H) then
- return nil
- end
- if (attr & 0x80) ~= 0 then
- row = height - 1 - row
- end
- local patBase
- local tileIndex
- if height == 16 then
- patBase = (tile & 1) * 0x1000
- tileIndex = (tile & 0xFE) + math.floor(row / 8)
- row = row & 7
- else
- patBase = (control & 0x08) ~= 0 and 0x1000 or 0
- tileIndex = tile
- end
- local addr = patBase + tileIndex * 16 + row
- local lo = ppuRead(addr)
- local hi = ppuRead(addr + 8)
- local scrollX = ppuScrollX & 0xFF
- local scrollY = ppuScrollY & 0xFF
- local baseNT = control & 3
- for xx = 0, 7 do
- local sourceX = (attr & 0x40) ~= 0 and xx or (7 - xx)
- local c = ((lo >> sourceX) & 1) | (((hi >> sourceX) & 1) << 1)
- if c ~= 0 then
- local px = sx + xx
- if px >= 0 and px < NES_W then
- if px >= 8 or ((ppuRegs[1] & 0x06) == 0x06) then
- local worldX = (scrollX + px) % 512
- local worldY = (scrollY + scanY) % 480
- local ntX = math.floor(worldX / 256)
- local ntY = math.floor(worldY / 240)
- local nt = (baseNT ~ (ntX == 1 and 1 or 0) ~ (ntY == 1 and 2 or 0)) & 3
- local localX = worldX % 256
- local localY = worldY % 240
- local tileX = math.floor(localX / 8)
- local tileY = math.floor(localY / 8)
- local fineX = localX & 7
- local fineY = localY & 7
- local ntBase = 0x2000 + nt * 0x400
- local tileNo = ppuRead(ntBase + tileY * 32 + tileX)
- local bgBase = (control & 0x10) ~= 0 and 0x1000 or 0
- local bgAddr = bgBase + tileNo * 16 + fineY
- local bgLo = ppuRead(bgAddr)
- local bgHi = ppuRead(bgAddr + 8)
- local bit = 7 - fineX
- local bgc = ((bgLo >> bit) & 1) | (((bgHi >> bit) & 1) << 1)
- if bgc ~= 0 then
- return px
- end
- end
- end
- end
- end
- return nil
- end
- -- ================================================================
- -- PPU TIMING
- -- ================================================================
- local function triggerNMI()
- if (ppuRegs[0] & 0x80) ~= 0 then
- nmiPending = true
- end
- end
- local function ppuStep(ppuCycles)
- for _ = 1, ppuCycles do
- -- NTSC has one skipped dot on odd frames when rendering is
- -- enabled. This is what produces the ~60.0988 Hz cadence.
- if
- ppuScanline == 261 and
- ppuCycle == 339 and
- (ppuRegs[1] & 0x18) ~= 0 and
- (ppuFrame & 1) ~= 0
- then
- ppuCycle = 0
- ppuScanline = 0
- ppuFrame = ppuFrame + 1
- ppuRegs[2] = ppuRegs[2] & 0x7F
- else
- ppuCycle = ppuCycle + 1
- -- Sprite 0 hit is latched during visible rendering. This
- -- computes the first opaque overlap for the current scanline
- -- and exposes it at the corresponding pixel dot.
- if ppuScanline < 240 and ppuCycle == 1 then
- if spriteOverflowOnScanline(ppuScanline) then
- ppuRegs[2] = ppuRegs[2] | 0x20
- end
- end
- if
- ppuScanline < 240 and
- ppuCycle >= 1 and
- ppuCycle <= 256 and
- (ppuRegs[2] & 0x40) == 0
- then
- local hitX = sprite0HitX(ppuScanline)
- if hitX ~= nil and ppuCycle == hitX + 1 then
- ppuRegs[2] = ppuRegs[2] | 0x40
- end
- end
- -- VBlank starts at scanline 241, dot 1.
- if ppuScanline == 241 and ppuCycle == 1 then
- ppuRegs[2] = ppuRegs[2] | 0x80
- nmiOccurredThisVBlank = false
- triggerNMI()
- end
- -- VBlank ends at dot 1 of pre-render.
- if ppuScanline == 261 and ppuCycle == 1 then
- ppuRegs[2] = ppuRegs[2] & 0x9F
- nmiOccurredThisVBlank = false
- end
- if ppuCycle >= PPU_CYCLES_PER_SCANLINE then
- ppuCycle = 0
- ppuScanline = ppuScanline + 1
- if ppuScanline >= PPU_SCANLINES then
- ppuScanline = 0
- ppuFrame = ppuFrame + 1
- ppuRegs[2] = ppuRegs[2] & 0x7F
- end
- end
- end
- end
- end
- -- ================================================================
- -- INPUT
- -- ================================================================
- local running = true
- local function keyDown(code)
- if code == keyboard.keys.q then
- running = false
- elseif code == keyboard.keys.w then
- buttons.UP = true
- elseif code == keyboard.keys.s then
- buttons.DOWN = true
- elseif code == keyboard.keys.a then
- buttons.LEFT = true
- elseif code == keyboard.keys.d then
- buttons.RIGHT = true
- elseif code == keyboard.keys.j then
- buttons.A = true
- elseif code == keyboard.keys.k then
- buttons.B = true
- elseif code == keyboard.keys.space then
- buttons.SELECT = true
- elseif code == keyboard.keys.enter then
- buttons.START = true
- end
- end
- local function keyUp(code)
- if code == keyboard.keys.w then
- buttons.UP = false
- elseif code == keyboard.keys.s then
- buttons.DOWN = false
- elseif code == keyboard.keys.a then
- buttons.LEFT = false
- elseif code == keyboard.keys.d then
- buttons.RIGHT = false
- elseif code == keyboard.keys.j then
- buttons.A = false
- elseif code == keyboard.keys.k then
- buttons.B = false
- elseif code == keyboard.keys.space then
- buttons.SELECT = false
- elseif code == keyboard.keys.enter then
- buttons.START = false
- end
- end
- local function pollEvents()
- while true do
- local ev =
- {event.pull(0)}
- if not ev[1] then
- break
- end
- if ev[1] == "key_down" then
- -- ev[3] is char, ev[4] is the scancode from keyboard.keys
- keyDown(ev[4])
- elseif ev[1] == "key_up" then
- keyUp(ev[4])
- end
- end
- end
- -- ================================================================
- -- PPU RESET
- -- ================================================================
- local function ppuReset()
- for i = 0, 7 do
- ppuRegs[i] = 0
- end
- ppuAddr = 0
- ppuAddrLatch = false
- ppuDataBuffer = 0
- ppuScrollLatch = false
- ppuScrollX = 0
- ppuScrollY = 0
- oamAddr = 0
- ppuCycle = 0
- ppuScanline = 0
- ppuFrame = 0
- nmiOccurredThisVBlank = false
- nmiPending = false
- oamDmaPending = false
- end
- -- ================================================================
- -- RESET
- -- ================================================================
- ppuReset()
- cpuReset()
- -- ================================================================
- -- TITLE
- -- ================================================================
- gpu.setBackground(0x000000)
- gpu.setForeground(0xFFFFFF)
- gpu.fill(
- 1,
- 1,
- drawW,
- drawH,
- " "
- )
- local title =
- "OC-NES PPU / NROM / BRAILLE"
- if drawW >= #title then
- gpu.set(
- 1,
- 1,
- title
- )
- end
- computer.pullSignal(0.5)
- -- ================================================================
- -- MAIN LOOP
- -- ================================================================
- local nextFrame =
- computer.uptime()
- while running do
- pollEvents()
- -- --------------------------------------------------------------
- -- Run one NES CPU frame.
- --
- -- Every CPU cycle advances the PPU by 3 cycles.
- -- This is much better than waiting until the end of the frame
- -- before setting VBlank/NMI.
- -- --------------------------------------------------------------
- local used = 0
- local frameTarget =
- CPU_CYCLES_FRAME + ((ppuFrame & 1) == 0 and CPU_FRAME_ALTERNATE or 0)
- while
- used < frameTarget and
- running
- do
- local cycles = cpuStep()
- used = used + cycles
- cpuTotalCycles = cpuTotalCycles + cycles
- ppuStep(cycles * 3)
- if oamDmaPending then
- oamDmaPending = false
- -- The DMA is 513 cycles, plus one alignment cycle when the
- -- transfer begins on the opposite CPU phase.
- local dmaCycles =
- 513 + (cpuTotalCycles & 1)
- used = used + dmaCycles
- cpuTotalCycles = cpuTotalCycles + dmaCycles
- ppuStep(dmaCycles * 3)
- end
- if nmiPending then
- local nmiCycles = serviceNMI()
- used = used + nmiCycles
- cpuTotalCycles = cpuTotalCycles + nmiCycles
- ppuStep(nmiCycles * 3)
- end
- end
- -- --------------------------------------------------------------
- -- Render the completed frame.
- -- --------------------------------------------------------------
- renderNESFrame()
- scaleToVRAM()
- blitVRAMToGPU()
- -- --------------------------------------------------------------
- -- Frame pacing.
- -- --------------------------------------------------------------
- nextFrame =
- nextFrame + FRAME_TIME
- local now =
- computer.uptime()
- local wait =
- nextFrame - now
- if wait > 0 then
- computer.pullSignal(wait)
- else
- nextFrame = now
- end
- end
- -- ================================================================
- -- EXIT
- -- ================================================================
- gpu.setBackground(0x000000)
- gpu.setForeground(0xFFFFFF)
- gpu.fill(
- 1,
- 1,
- drawW,
- drawH,
- " "
- )
- gpu.set(
- 1,
- 1,
- "NES EMULATOR EXITED"
- )
- computer.pullSignal(0.25)
Advertisement
Add Comment
Please, Sign In to add comment