diff --git a/src/Bus.cpp b/src/Bus.cpp index e10ef0b..bdd766c 100644 --- a/src/Bus.cpp +++ b/src/Bus.cpp @@ -48,7 +48,17 @@ uint8_t Bus::Tick() { controllerPort.Tick(); - uint8_t result = cpu.Tick(); + uint8_t result = 0x00; + if (DMACyclesLeft == 0) + { + result = cpu.Tick(); + } + else + { + DMATick(); + result = DMACyclesLeft; + } + // 3 ppu ticks per cpu tick ppu.Tick(); @@ -62,6 +72,25 @@ uint8_t Bus::Tick() return result; } +void Bus::DMATick() +{ + if (preDMACycles > 0) + { + preDMACycles--; + return; + } + + if (DMALatch != 0) + { + Byte data = ReadCPU(((Word)DMAPage << 8) | (0xFF - DMACyclesLeft)); + ppu.WriteRegister(0x2004, data); + + DMACyclesLeft--; + } + + DMALatch = 1 - DMALatch; +} + void Bus::PPUTick() { if (ppuClock == 0) @@ -125,6 +154,9 @@ Byte Bus::ReadCPU(Word addr) { switch (addr) { + case 0x4014: + return 0x00; + case 0x4016: case 0x4017: return controllerPort.Read(addr); @@ -180,6 +212,12 @@ void Bus::WriteCPU(Word addr, Byte val) { switch (addr) { + case 0x4014: + DMAPage = val; + DMACyclesLeft = 0xFF; + preDMACycles = 1 + (cpu.GetTotalCycles() % 2); + return; + case 0x4016: controllerPort.Write(addr, val); break; @@ -201,12 +239,9 @@ void Bus::WritePPU(Word addr, Byte val) } else if (0x2000 <= addr && addr < 0x3F00) { - if(cartridge.MapCIRAM(addr)) + if (cartridge.MapCIRAM(addr)) cartridge.WriteVRAM(addr, val); - if (val != 0x00) - volatile int jfkd = 3; - VRAM[addr & 0xFFF] = val; } else if (0x3F00 <= addr && addr < 0x4000) diff --git a/src/Bus.hpp b/src/Bus.hpp index 8590206..4bef333 100644 --- a/src/Bus.hpp +++ b/src/Bus.hpp @@ -47,6 +47,8 @@ public: */ uint8_t Tick(); + void DMATick(); + void PPUTick(); /** @@ -95,5 +97,10 @@ private: Cartridge cartridge; ControllerPort controllerPort; + Byte preDMACycles = 0; + Byte DMACyclesLeft = 0; + Byte DMAPage = 0; + Byte DMALatch = 0; + uint8_t ppuClock = 0; }; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fe34aa3..60823e2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,7 +13,7 @@ add_executable(nesemu "debugger/PPUWatcher.cpp" "debugger/Disassembler.cpp" "debugger/MemoryViewer.cpp" - "debugger/NametableViewer.cpp" "ControllerPort.cpp" "controllers/StandardController.cpp" "gfx/Input.cpp" "debugger/ControllerPortViewer.cpp" "gfx/Screen.cpp" "debugger/Palettes.cpp" "APU.cpp" "debugger/PatternTableViewer.cpp" "mappers/Mapper003.cpp" "mappers/Mapper001.cpp") + "debugger/NametableViewer.cpp" "ControllerPort.cpp" "controllers/StandardController.cpp" "gfx/Input.cpp" "debugger/ControllerPortViewer.cpp" "gfx/Screen.cpp" "debugger/Palettes.cpp" "APU.cpp" "debugger/PatternTableViewer.cpp" "mappers/Mapper003.cpp" "mappers/Mapper001.cpp" "debugger/OAMViewer.cpp") target_include_directories(nesemu PRIVATE mappers diff --git a/src/CPU.hpp b/src/CPU.hpp index a4128ef..65c21c4 100644 --- a/src/CPU.hpp +++ b/src/CPU.hpp @@ -107,6 +107,8 @@ public: */ void NMI(); + uint64_t GetTotalCycles() { return totalCycles; } + private: /** * @brief Create a lookup table of instructions. diff --git a/src/PPU.cpp b/src/PPU.cpp index 4b5320f..2974bf4 100644 --- a/src/PPU.cpp +++ b/src/PPU.cpp @@ -77,6 +77,9 @@ const std::vector PPU::colorTable = { PPU::PPU(Bus* bus, Screen* screen) : bus(bus), screen(screen), ppuctrl{ 0 }, ppustatus{ 0 } { + OAM = std::vector(64 * 4, 0); + secondaryOAM = std::vector(8 * 4, 0); + sprites = std::vector(8, { 0 }); } void PPU::Powerup() @@ -144,12 +147,17 @@ void PPU::Tick() // This cycle resets the VBlankStarted flag if (y == 261 && x == 1) + { ppustatus.Flag.VBlankStarted = 0; + ppustatus.Flag.SpriteZeroHit = 0; + } // Need to render if (scanlineType == ScanlineType::Visible || scanlineType == ScanlineType::PreRender) { - PerformRenderAction(); + EvaluateBackgroundTiles(); + EvaluateSprites(); + if (x == 257) { current.CoarseX = temporary.CoarseX; @@ -169,20 +177,11 @@ void PPU::Tick() if (x < 256 && y < 240) { - Byte loBit = (loTile.Hi & 0x80) >> 7; - Byte hiBit = (hiTile.Hi & 0x80) >> 7; - Byte loAttrBit = (loAttribute.Hi & 0x80) >> 7; - Byte hiAttrBit = (hiAttribute.Hi & 0x80) >> 7; + Pixel bgPixel = GetBackgroundPixel(); + Pixel spritePixel = GetSpritePixel(); - uint8_t color = (hiBit << 1) | loBit; - uint8_t palette = (hiAttrBit << 1) | loAttrBit; - if (color == 0x00) - palette = 0x00; - - uint8_t colorVal = Read(0x3F00 | (palette << 2) | color); - - if(ppumask.Flag.ShowBackground) - screen->SetPixel(x, y, colorTable[colorVal]); + Color pixel = MultiplexPixel(bgPixel, spritePixel); + screen->SetPixel(x, y, pixel); } if (cycleType == CycleType::Fetching || cycleType == CycleType::PreFetching) @@ -217,6 +216,13 @@ Byte PPU::ReadRegister(Byte id) addressLatch = 0; break; + case 3: + break; + + case 4: + data = ReadOAM(oamaddr++); + break; + case 5: break; @@ -258,6 +264,14 @@ void PPU::WriteRegister(Byte id, Byte val) ppustatus.Raw = val; break; + case 3: + oamaddr = val; + break; + + case 4: + WriteOAM(oamaddr++, val); + break; + // PPUADDR and PPUSCROLL both take 2 accesses to fully set // When writing to them the address latch is switched. The latch // determines whether the hi or lo byte should be written next @@ -311,6 +325,16 @@ void PPU::WriteRegister(Byte id, Byte val) ppustatus.Flag.Unused = val & 0x1F; } +Byte PPU::ReadOAM(Byte offset) +{ + return OAM[offset] | OAMOverrideSignal; +} + +void PPU::WriteOAM(Byte offset, Byte val) +{ + OAM[offset] = val; +} + Byte PPU::Read(Word addr) { return bus->ReadPPU(addr); @@ -366,7 +390,7 @@ void PPU::UpdateState() } } -void PPU::PerformRenderAction() +void PPU::EvaluateBackgroundTiles() { if (cycleType == CycleType::Idle) { @@ -443,3 +467,239 @@ void PPU::PerformRenderAction() fineX = 0; memoryAccessLatch = 1 - memoryAccessLatch; } + +void PPU::EvaluateSprites() +{ + if (cycleType == CycleType::Idle) + { + currentlyEvaluatedSprite = 0x00; + return; + } + + if (scanlineType != ScanlineType::PreRender) + { + OAMOverrideSignal = (x <= 64); + + // Secondary OAM clear + if (x < 64) + { + secondaryOAM[x >> 1] = ReadOAM(0x00); + return; + } + + // Sprite evaluation (just do it all at once) + if (x == 65) + { + freeSecondaryOAMSlot = 0x00; + + Byte n; + for (n = 0; n < 64; n++) + { + if (oamaddr + 4 * n >= 64 * 4) + break; + + Byte spriteYCoord = ReadOAM(oamaddr + 4 * n); + if (freeSecondaryOAMSlot >= 32) + break; + + // Find free slot + secondaryOAM[freeSecondaryOAMSlot] = spriteYCoord; + if (y - spriteYCoord < 8 + ppuctrl.Flag.SpriteSize * 8) // Choose between 8x8 and 8x16 mode + { + secondaryOAM[freeSecondaryOAMSlot + 1] = ReadOAM(4 * n + 1); + secondaryOAM[freeSecondaryOAMSlot + 2] = ReadOAM(4 * n + 2); + secondaryOAM[freeSecondaryOAMSlot + 3] = ReadOAM(4 * n + 3); + + freeSecondaryOAMSlot += 4; + } + + + } + + Byte m = 0; + for (; n < 64; n++) + { + Byte spriteYCoord = ReadOAM(4 * n + m); + if (spriteYCoord < 240) + ppustatus.Flag.SpriteOverflow = 1; + else + m = (m + 1) % 4; // Correctly implement sprite overflow bug + } + + return; + } + } + + if (cycleType == CycleType::SpriteFetching) + { + oamaddr = 0; + + // Sprite tile fetches + if (memoryAccessLatch == 1) + { + switch (fetchPhase) + { + case FetchingPhase::NametableByte: // Fetch garbage + nametableByte = Read(0x2000 | (current.Raw & 0x0FFF)); + sprites[currentlyEvaluatedSprite].Counter = secondaryOAM[4 * currentlyEvaluatedSprite + 3]; + sprites[currentlyEvaluatedSprite].FineX = 0; + + fetchPhase = FetchingPhase::AttributeTableByte; + break; + + case FetchingPhase::AttributeTableByte: // Fetch garbage + attributeTableByte = Read(0x23C0 | (current.Raw & 0x0C00) | ((current.CoarseY >> 2) << 3) | (current.CoarseX >> 2)); + sprites[currentlyEvaluatedSprite].Latch = secondaryOAM[4 * currentlyEvaluatedSprite + 2]; + + fetchPhase = FetchingPhase::PatternTableLo; + break; + + case FetchingPhase::PatternTableLo: + { + Byte spriteFineY = y - secondaryOAM[4 * currentlyEvaluatedSprite]; + Byte tileNumber = secondaryOAM[4 * currentlyEvaluatedSprite + 1] + (spriteFineY >> 3); + + sprites[currentlyEvaluatedSprite].Lo = Read(((Word)ppuctrl.Flag.SpritePatternTableAddr << 12) | (tileNumber << 4) + spriteFineY); + + fetchPhase = FetchingPhase::PatternTableHi; + } break; + + case FetchingPhase::PatternTableHi: + { + Byte spriteFineY = y - secondaryOAM[4 * currentlyEvaluatedSprite]; + Byte tileNumber = secondaryOAM[4 * currentlyEvaluatedSprite + 1] + (spriteFineY >> 3); + + sprites[currentlyEvaluatedSprite].Hi = Read(((Word)ppuctrl.Flag.SpritePatternTableAddr << 12) | (tileNumber << 4) + 8 + spriteFineY); + + if (currentlyEvaluatedSprite * 4 >= freeSecondaryOAMSlot) + { + sprites[currentlyEvaluatedSprite].Lo = 0x00; + sprites[currentlyEvaluatedSprite].Hi = 0x00; + } + + currentlyEvaluatedSprite++; + + fetchPhase = FetchingPhase::NametableByte; + } break; + } + } + + memoryAccessLatch = 1 - memoryAccessLatch; + } +} + +Pixel PPU::GetBackgroundPixel() +{ + Pixel returnValue{ 0 }; + returnValue.color = 0x00; + returnValue.palette = 0; + + if (!ppumask.Flag.ShowBackground) + return returnValue; + + Byte loBit = (loTile.Hi & 0x80) >> 7; + Byte hiBit = (hiTile.Hi & 0x80) >> 7; + Byte loAttrBit = (loAttribute.Hi & 0x80) >> 7; + Byte hiAttrBit = (hiAttribute.Hi & 0x80) >> 7; + + returnValue.color = (hiBit << 1) | loBit; + returnValue.palette = (hiAttrBit << 1) | loAttrBit; + if (returnValue.color == 0x00) + returnValue.palette = 0x00; + + return returnValue; +} + +Pixel PPU::GetSpritePixel() +{ + for (Sprite& sprite : sprites) + { + if (sprite.Counter != 0) + { + sprite.Counter--; + continue; + } + + if (sprite.FineX != 8) + sprite.FineX++; + + } + + Pixel returnValue{ 0 }; + returnValue.color = 0x00; + returnValue.palette = 4; + returnValue.priority = 1; + + if (!ppumask.Flag.ShowSprites) + return returnValue; + + bool firstIteration = true; + for (Sprite& sprite : sprites) + { + // Sprite is inacitve + if (sprite.Counter != 0 || sprite.FineX >= 8) + continue; + + // If sprite is active, determine the current pixel + Byte loBit = (sprite.Hi & 0x80) >> 7; + Byte hiBit = (sprite.Hi & 0x80) >> 7; + + uint8_t color = (hiBit << 1) | loBit; + if (color == 0x00) + { + firstIteration = false; + continue; + } + + uint8_t palette = 4 + (sprite.Latch & 0x3); + if (color == 0x00) + palette = 0x00; + + returnValue.color = color; + returnValue.palette = palette; + returnValue.priority = (sprite.Latch >> 5) & 0x1; + returnValue.isZeroSprite = firstIteration; + + break; + } + + return returnValue; +} + +Color PPU::MultiplexPixel(Pixel background, Pixel sprite) +{ + if (background.color == 0) + { + if (sprite.color == 0) + return colorTable[Read(0x3F00)]; + + else + return colorTable[Read(0x3F00 | (sprite.palette << 2) | sprite.color)]; + } + else + { + if (sprite.color == 0) + return colorTable[Read(0x3F00 | (background.palette << 2) | background.color)]; + + else + { + // Sprite Zero hit detection + if (sprite.isZeroSprite) + { + // All of the conditions that make sprite zero hits not evaluate + if (!( + (!ppustatus.Flag.SpriteZeroHit) && + (x == 255) && + (!ppumask.Flag.ShowBackground || !ppumask.Flag.ShowSprites) && + ((!ppumask.Flag.SpriteOnLeft || !ppumask.Flag.BackgroundOnLeft) && 0 <= x && x <= 7) + )) ppustatus.Flag.SpriteZeroHit = 1; + } + + if(sprite.priority == 0) + return colorTable[Read(0x3F00 | (sprite.palette << 2) | sprite.color)]; + + else + return colorTable[Read(0x3F00 | (background.palette << 2) | background.color)]; + } + } +} diff --git a/src/PPU.hpp b/src/PPU.hpp index ef59014..45f6d15 100644 --- a/src/PPU.hpp +++ b/src/PPU.hpp @@ -55,12 +55,29 @@ union ShiftRegister Word Raw; }; +struct Sprite +{ + Byte Lo, Hi; + Byte Latch; + Byte Counter; + Byte FineX; +}; + +struct Pixel +{ + Byte color; + Byte palette; + Byte priority; + bool isZeroSprite; +}; + /** * @brief The PPU of the NES. */ class PPU { friend class PPUWatcher; + friend class OAMViewer; public: static const std::vector colorTable; @@ -95,6 +112,9 @@ public: */ void WriteRegister(Byte id, Byte val); + Byte ReadOAM(Byte offset); + void WriteOAM(Byte offset, Byte val); + /** * @brief Check whether the PPU finished rendering a frame. * Returns true if the VBlankStart cycle was hit previously. The function resets @@ -114,7 +134,13 @@ private: void Write(Word addr, Byte val); void UpdateState(); - void PerformRenderAction(); + void EvaluateBackgroundTiles(); + void EvaluateSprites(); + + Pixel GetBackgroundPixel(); + Pixel GetSpritePixel(); + + Color MultiplexPixel(Pixel background, Pixel sprite); private: // Registers @@ -171,6 +197,7 @@ private: // Registers } ppuscroll; Address ppuaddr; + Byte oamaddr; VRAMAddress current{ 0 }; VRAMAddress temporary{ 0 }; @@ -190,6 +217,13 @@ private: // Registers ShiftRegister hiAttribute{ 0 }; ShiftRegister loAttribute{ 0 }; + std::vector OAM; + std::vector secondaryOAM; + std::vector sprites; + Byte OAMOverrideSignal = 0x00; + Byte freeSecondaryOAMSlot = 0x00; + Byte currentlyEvaluatedSprite = 0x00; + private: ScanlineType scanlineType; CycleType cycleType; diff --git a/src/debugger/Debugger.cpp b/src/debugger/Debugger.cpp index 4cef7a1..9a31bad 100644 --- a/src/debugger/Debugger.cpp +++ b/src/debugger/Debugger.cpp @@ -10,6 +10,7 @@ #include "Disassembler.hpp" #include "MemoryViewer.hpp" #include "NametableViewer.hpp" +#include "OAMViewer.hpp" #include "PatternTableViewer.hpp" #include "ControllerPortViewer.hpp" #include "Palettes.hpp" @@ -27,6 +28,7 @@ Debugger::Debugger(Bus* bus) : windows.push_back(new MemoryViewer(this, bus)); windows.push_back(new NametableViewer(this, bus)); + windows.push_back(new OAMViewer(this, &bus->ppu)); windows.push_back(new PatternTableViewer(this, bus->cartridge.GetMapper())); windows.push_back(new ControllerPortViewer(this, &bus->controllerPort)); windows.push_back(new Palettes(this, bus)); diff --git a/src/debugger/OAMViewer.cpp b/src/debugger/OAMViewer.cpp new file mode 100644 index 0000000..cff7a2a --- /dev/null +++ b/src/debugger/OAMViewer.cpp @@ -0,0 +1,33 @@ +#include "OAMViewer.hpp" + +#include "../PPU.hpp" +#include + +OAMViewer::OAMViewer(Debugger* debugger, PPU* ppu) : + DebugWindow("OAM Viewer", debugger), ppu(ppu) +{ +} + +void OAMViewer::OnRender() +{ + if (!ImGui::Begin("OAM Viewer", &isOpen)) + { + ImGui::End(); + return; + } + + char label[sizeof("Sprite 00")]; + for (int i = 0; i < 64; i++) + { + std::sprintf(label, "Sprite %02d", i); + if (ImGui::CollapsingHeader(label)) + { + ImGui::Text("Y pos : %02X", ppu->OAM[4 * i + 0]); + ImGui::Text("Tile : %02X", ppu->OAM[4 * i + 1]); + ImGui::Text("Attribute: %02X", ppu->OAM[4 * i + 2]); + ImGui::Text("X pos : %02X", ppu->OAM[4 * i + 3]); + } + } + + ImGui::End(); +} diff --git a/src/debugger/OAMViewer.hpp b/src/debugger/OAMViewer.hpp new file mode 100644 index 0000000..51b7c19 --- /dev/null +++ b/src/debugger/OAMViewer.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "DebugWindow.hpp" + +class PPU; + +class OAMViewer : + public DebugWindow +{ +public: + OAMViewer(Debugger* debugger, PPU* ppu); + + virtual void OnRender() override; + +private: + PPU* ppu; +}; diff --git a/src/debugger/PPUWatcher.cpp b/src/debugger/PPUWatcher.cpp index ad43ec2..3176134 100644 --- a/src/debugger/PPUWatcher.cpp +++ b/src/debugger/PPUWatcher.cpp @@ -121,6 +121,11 @@ void PPUWatcher::OnRender() ImGui::TableNextColumn(); ImGui::Text("$%04X", ppu->ppuctrl.Flag.BackgrPatternTableAddr ? 0x1000 : 0x0000); + ImGui::TableNextColumn(); + ImGui::Text("Sprite Size"); + ImGui::TableNextColumn(); + ImGui::Text(ppu->ppuctrl.Flag.SpriteSize ? "8x16" : "8x8"); + ImGui::TableNextColumn(); ImGui::Text("Master/Slave"); ImGui::TableNextColumn();