diff --git a/roms/donkeykong.nes b/roms/donkeykong.nes new file mode 100644 index 0000000..ecd6f0a Binary files /dev/null and b/roms/donkeykong.nes differ diff --git a/src/Application.cpp b/src/Application.cpp index 968f7c4..fcb52e3 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -10,6 +10,7 @@ #include "Log.hpp" #include "Bus.hpp" +#include "Screen.hpp" #include "Debugger.hpp" #include "gfx/Window.hpp" @@ -48,7 +49,7 @@ Application::Application() : LOG_CORE_INFO("Creating window"); try { - window = new Window(1280, 720, "NES Emulator"); + window = new Window(256, 240, "NES Emulator"); } catch (const std::runtime_error& err) { @@ -60,6 +61,8 @@ Application::Application() : if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) throw std::runtime_error("Failed to set up OpenGL loader"); + window->SetScale(scale); + LOG_CORE_INFO("Setting up ImGui"); IMGUI_CHECKVERSION(); ImGui::CreateContext(); @@ -80,7 +83,17 @@ Application::Application() : style.Colors[ImGuiCol_WindowBg].w = 1.0f; } - bus = new Bus; + try + { + screen = new Screen; + } + catch(const std::runtime_error& err) + { + LOG_CORE_ERROR("Screen creation failed"); + throw err; + } + + bus = new Bus(screen); debugger = new Debugger(bus); } @@ -96,12 +109,35 @@ Application::~Application() bool Application::Update() { + if (window->GetScale() != scale) + window->SetScale(scale); + glfwPollEvents(); if (!debugger->Update()) return false; window->Begin(); + screen->Render(); + + if (ImGui::BeginMainMenuBar()) + { + if (ImGui::BeginMenu("Screen")) + { + if (ImGui::BeginMenu("Scale")) + { + ImGui::RadioButton("x1", &scale, 1); + ImGui::RadioButton("x2", &scale, 2); + ImGui::RadioButton("x3", &scale, 3); + ImGui::RadioButton("x4", &scale, 4); + ImGui::EndMenu(); + } + ImGui::EndMenu(); + } + + ImGui::EndMainMenuBar(); + } + debugger->Render(); window->End(); diff --git a/src/Application.hpp b/src/Application.hpp index 4ed7b95..1001bd4 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -3,6 +3,7 @@ class Bus; class Window; class Debugger; +class Screen; /** * @brief Contains the program loop and invokes other objects update functions. @@ -26,7 +27,10 @@ private: bool Update(); private: + int scale = 3; + Window* window; Bus* bus; + Screen* screen; Debugger* debugger; }; diff --git a/src/Bus.cpp b/src/Bus.cpp index d26379b..5e7d069 100644 --- a/src/Bus.cpp +++ b/src/Bus.cpp @@ -5,8 +5,8 @@ #include "controllers/StandardController.hpp" -Bus::Bus() : - cpu(this), ppu(this), cartridge(this) +Bus::Bus(Screen* screen) : + cpu(this), ppu(this, screen), cartridge(this) { LOG_CORE_INFO("Allocating RAM"); RAM = std::vector(0x800); @@ -15,7 +15,7 @@ Bus::Bus() : VRAM = std::vector(0x800); LOG_CORE_INFO("Inserting cartridge"); - cartridge.Load("roms/nestest.nes"); + cartridge.Load("roms/donkeykong.nes"); LOG_CORE_INFO("Powering up CPU"); cpu.Powerup(); diff --git a/src/Bus.hpp b/src/Bus.hpp index 7fd5b7c..a161782 100644 --- a/src/Bus.hpp +++ b/src/Bus.hpp @@ -23,7 +23,7 @@ class Bus friend class NametableViewer; public: - Bus(); + Bus(Screen* screen); /** * @brief Reboot the NES. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3bc6206..ec20574 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") + "debugger/NametableViewer.cpp" "ControllerPort.cpp" "controllers/StandardController.cpp" "gfx/Input.cpp" "debugger/ControllerPortViewer.cpp" "gfx/Screen.cpp") target_include_directories(nesemu PRIVATE mappers diff --git a/src/ControllerPort.cpp b/src/ControllerPort.cpp index f08f3b6..046d813 100644 --- a/src/ControllerPort.cpp +++ b/src/ControllerPort.cpp @@ -28,6 +28,9 @@ Byte ControllerPort::Write(Word addr, Byte val) Byte ControllerPort::Read(Word addr) { + if (connectedDevices[addr & 1] == nullptr) + return 0xFF; + return connectedDevices[addr & 1]->CLK(); } diff --git a/src/PPU.cpp b/src/PPU.cpp index a0c0205..9ef5ebd 100644 --- a/src/PPU.cpp +++ b/src/PPU.cpp @@ -2,9 +2,12 @@ #include "Log.hpp" #include "Bus.hpp" -PPU::PPU(Bus* bus) : - bus(bus), ppuctrl{0}, ppustatus{0} +#include "gfx/Screen.hpp" + +PPU::PPU(Bus* bus, Screen* screen) : + bus(bus), screen(screen), ppuctrl{ 0 }, ppustatus{ 0 } { + LOG_CORE_INFO("{0}", sizeof(VRAMAddress)); } void PPU::Powerup() @@ -57,38 +60,6 @@ void PPU::Tick() y = 0; } - current.CoarseX++; - if (current.CoarseX > 31) - { - current.CoarseX = 0; - current.NametableSel ^= 0x1; - } - - if (x == 256) - { - if (current.FineY < 7) - { - current.FineY++; - } - else - { - current.FineY = 0; - if (current.CoarseY == 29) - { - current.CoarseY = 0; - current.NametableSel ^= 0x2; - } - else if (current.CoarseY == 31) - { - current.CoarseY = 0; - } - else - { - current.CoarseY++; - } - } - } - UpdateState(); // On this cycle the VBlankStarted bit is set in the ppustatus @@ -113,6 +84,14 @@ void PPU::Tick() PerformRenderAction(); } + if (x < 256 && y < 240) + { + uint8_t xOffset = 7 - fineX; + + uint8_t color = (((patternTableHi >> xOffset) & 0x1) << 1) | ((patternTableLo >> xOffset) & 0x1); + color *= 80; + screen->SetPixel(x, y, { color, color, color}); + } } Byte PPU::ReadRegister(Byte id) @@ -283,6 +262,12 @@ void PPU::UpdateState() void PPU::PerformRenderAction() { if (cycleType == CycleType::Idle) + { + fineX = 0; + return; + } + + if (cycleType == CycleType::SpriteFetching) return; if (memoryAccessLatch == 1) @@ -302,16 +287,49 @@ void PPU::PerformRenderAction() break; case FetchingPhase::PatternTableLo: - patternTableLo = Read(ppuctrl.Flag.BackgrPatternTableAddr | nametableByte); + patternTableLo = Read(((Word)ppuctrl.Flag.BackgrPatternTableAddr << 12) | ((Word)nametableByte << 4) + current.FineY); fetchPhase = FetchingPhase::PatternTableHi; break; case FetchingPhase::PatternTableHi: - patternTableLo = Read((ppuctrl.Flag.BackgrPatternTableAddr | nametableByte) + 8); + patternTableLo = Read((((Word)ppuctrl.Flag.BackgrPatternTableAddr << 12) | ((Word)nametableByte << 4)) + 8 + current.FineY); + + current.CoarseX++; + if (x == 256) + { + current.CoarseX = temporary.CoarseX; + current.NametableSel ^= 0x1; + + if (current.FineY < 7) + { + current.FineY++; + } + else + { + current.FineY = temporary.FineY; + if (current.CoarseY == 29) + { + current.CoarseY = temporary.CoarseY; + current.NametableSel ^= 0x2; + } + else if (current.CoarseY == 31) + { + current.CoarseY = temporary.CoarseY; + } + else + { + current.CoarseY++; + } + } + } + fetchPhase = FetchingPhase::NametableByte; break; } } + fineX++; + if (fineX >= 8) + fineX = 0; memoryAccessLatch = 1 - memoryAccessLatch; } diff --git a/src/PPU.hpp b/src/PPU.hpp index fab4445..fc06b49 100644 --- a/src/PPU.hpp +++ b/src/PPU.hpp @@ -3,6 +3,7 @@ #include "Types.hpp" class Bus; +class Screen; enum class ScanlineType { @@ -33,10 +34,10 @@ union VRAMAddress { struct { - Byte CoarseX : 5; - Byte CoarseY : 5; - Byte NametableSel : 2; - Byte FineY : 3; + Word CoarseX : 5; + Word CoarseY : 5; + Word NametableSel : 2; + Word FineY : 3; }; Word Raw; @@ -50,7 +51,7 @@ class PPU friend class PPUWatcher; public: - PPU(Bus* bus); + PPU(Bus* bus, Screen* screen); /** * @brief Powerup PPU. @@ -177,4 +178,5 @@ private: uint8_t memoryAccessLatch = 0; bool isFrameDone = false; Bus* bus; + Screen* screen; }; diff --git a/src/controllers/StandardController.cpp b/src/controllers/StandardController.cpp index 0fa58a3..ac1f2ee 100644 --- a/src/controllers/StandardController.cpp +++ b/src/controllers/StandardController.cpp @@ -22,9 +22,4 @@ void StandardController::OUT(PortLatch latch) pressed.Buttons.Down = Input::IsKeyDown(GLFW_KEY_DOWN); pressed.Buttons.Left = Input::IsKeyDown(GLFW_KEY_LEFT); pressed.Buttons.Right = Input::IsKeyDown(GLFW_KEY_RIGHT); - - if (pressed.Raw != 0) - volatile int jdfi = 3; - - outRegister = pressed.Raw; } diff --git a/src/debugger/ControllerPortViewer.cpp b/src/debugger/ControllerPortViewer.cpp index f18b213..a5b7286 100644 --- a/src/debugger/ControllerPortViewer.cpp +++ b/src/debugger/ControllerPortViewer.cpp @@ -34,7 +34,7 @@ void ControllerPortViewer::OnRender() ImGui::Separator(); ImGui::InputScalar("Shift Register", ImGuiDataType_U8, &controller->outRegister, (const void*)0, (const void*)0, "%02X", ImGuiInputTextFlags_CharsHexadecimal); - + if (ImGui::BeginTable(controllerName.c_str(), 8)) { for(int i = 0; i < 8; i++) @@ -52,6 +52,8 @@ void ControllerPortViewer::OnRender() ImGui::EndTable(); } } + + counter++; } ImGui::End(); diff --git a/src/gfx/Screen.cpp b/src/gfx/Screen.cpp new file mode 100644 index 0000000..a8819b6 --- /dev/null +++ b/src/gfx/Screen.cpp @@ -0,0 +1,154 @@ +#include "Screen.hpp" + +#include +#include +#include + +#include "../Log.hpp" + +Screen::Screen() +{ + pixels.resize(256 * 240); + + LOG_CORE_INFO("Creating vertex arrays"); + CreateVertexArray(); + + LOG_CORE_INFO("Creating screen texture"); + CreateTexture(); + + LOG_CORE_INFO("Creating screen shader"); + CreateShader(); +} + +Screen::~Screen() +{ + glDeleteTextures(1, &texture); + glDeleteProgram(shader); + glDeleteBuffers(1, &vbo); + glDeleteVertexArrays(1, &vao); +} + +void Screen::SetPixel(uint16_t x, uint16_t y, Color color) +{ + pixels[y * 256 + x] = color; +} + +void Screen::Render() +{ + glTextureSubImage2D(texture, 0, 0, 0, 256, 240, GL_RGB, GL_UNSIGNED_BYTE, (const void*)pixels.data()); + + glBindTexture(GL_TEXTURE_2D, texture); + glUseProgram(shader); + glBindVertexArray(vao); + + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); +} + +void Screen::CreateVertexArray() +{ + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + float vertices[4 * (3 + 2)] = { + -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, + -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, + 1.0f, -1.0f, 0.0f, 1.0f, 1.0f + }; + + glGenBuffers(1, &vbo); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (const void*)0); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (const void*)(3 * sizeof(float))); + + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); +} + +void Screen::CreateTexture() +{ + glCreateTextures(GL_TEXTURE_2D, 1, &texture); + glTextureStorage2D(texture, 1, GL_RGB8, 256, 240); + + glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST); +} + +void Screen::CreateShader() +{ + GLint status; + shader = glCreateProgram(); + + GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); + const char* vertexShaderSource = R"( + #version 460 core + + layout (location = 0) in vec3 a_Pos; + layout (location = 1) in vec2 a_UV; + + out vec2 uvCoords; + + void main() + { + uvCoords = a_UV; + gl_Position = vec4(a_Pos, 1.0f); + } + )"; + glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); + glCompileShader(vertexShader); + glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &status); + if (status == GL_FALSE) + { + char errorBuf[512]; + glGetShaderInfoLog(vertexShader, 512, NULL, errorBuf); + glDeleteShader(vertexShader); + + throw std::runtime_error("Vertex shader compilation error: " + std::string(errorBuf)); + } + + GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); + const char* fragmentShaderSource = R"( + #version 460 core + + out vec4 FragColor; + in vec2 uvCoords; + + uniform sampler2D screen; + + void main() + { + FragColor = texture(screen, uvCoords); + } + )"; + glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); + glCompileShader(fragmentShader); + glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &status); + if (status == GL_FALSE) + { + char errorBuf[512]; + glGetShaderInfoLog(fragmentShader, 512, NULL, errorBuf); + glDeleteShader(fragmentShader); + glDeleteShader(vertexShader); + + throw std::runtime_error("Fragment shader compilation error: " + std::string(errorBuf)); + } + + glAttachShader(shader, vertexShader); + glAttachShader(shader, fragmentShader); + glLinkProgram(shader); + glGetProgramiv(shader, GL_LINK_STATUS, &status); + if (status == GL_FALSE) + { + char errorBuf[512]; + glGetProgramInfoLog(shader, 512, NULL, errorBuf); + glDeleteShader(fragmentShader); + glDeleteShader(vertexShader); + + throw std::runtime_error("Shader link error: " + std::string(errorBuf)); + } + + glDeleteShader(fragmentShader); + glDeleteShader(vertexShader); +} diff --git a/src/gfx/Screen.hpp b/src/gfx/Screen.hpp new file mode 100644 index 0000000..eb24ef1 --- /dev/null +++ b/src/gfx/Screen.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +struct Color +{ + uint8_t r; + uint8_t g; + uint8_t b; +}; + +class Screen +{ +public: + Screen(); + ~Screen(); + + void SetPixel(uint16_t x, uint16_t y, Color color); + void Render(); + +private: + void CreateVertexArray(); + void CreateTexture(); + void CreateShader(); + +private: + uint32_t texture = 0; + uint32_t shader = 0; + uint32_t vao = 0; + uint32_t vbo = 0; + + std::vector pixels; +}; diff --git a/src/gfx/Window.cpp b/src/gfx/Window.cpp index 6154de0..795067d 100644 --- a/src/gfx/Window.cpp +++ b/src/gfx/Window.cpp @@ -10,6 +10,7 @@ Window::Window(uint16_t width, uint16_t height, const std::string& title) : handle(nullptr) { + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); handle = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr); if (handle == nullptr) { @@ -28,6 +29,12 @@ Window::~Window() glfwDestroyWindow(handle); } +void Window::SetScale(int scale) +{ + glfwSetWindowSize(handle, 256 * scale, 240 * scale + 20); + glViewport(0, 0, 256 * scale, 240 * scale); +} + void Window::Begin() { glClearColor(0.0f, 0.0f, 0.0f, 0.0f); diff --git a/src/gfx/Window.hpp b/src/gfx/Window.hpp index 02fffc9..ae9191a 100644 --- a/src/gfx/Window.hpp +++ b/src/gfx/Window.hpp @@ -12,12 +12,16 @@ public: Window(uint16_t width, uint16_t height, const std::string& title); ~Window(); + inline int GetScale() { return scale; } inline bool ShouldClose() { return glfwWindowShouldClose(handle); } inline GLFWwindow* GetNativeWindow() { return handle; } + void SetScale(int scale); + void Begin(); void End(); private: + int scale = -1; GLFWwindow* handle; };