From f9f401c6c0b9f382710e48fa2734548544390391 Mon Sep 17 00:00:00 2001 From: Lauchmelder <robert.altner11@gmail.com> Date: Thu, 3 Mar 2022 02:33:08 +0100 Subject: [PATCH] started ppu --- roms/donkeykong.nes | Bin 0 -> 24592 bytes src/Application.cpp | 40 ++++++- src/Application.hpp | 4 + src/Bus.cpp | 6 +- src/Bus.hpp | 2 +- src/CMakeLists.txt | 2 +- src/ControllerPort.cpp | 3 + src/PPU.cpp | 90 +++++++++------ src/PPU.hpp | 12 +- src/controllers/StandardController.cpp | 5 - src/debugger/ControllerPortViewer.cpp | 4 +- src/gfx/Screen.cpp | 154 +++++++++++++++++++++++++ src/gfx/Screen.hpp | 34 ++++++ src/gfx/Window.cpp | 7 ++ src/gfx/Window.hpp | 4 + 15 files changed, 313 insertions(+), 54 deletions(-) create mode 100644 roms/donkeykong.nes create mode 100644 src/gfx/Screen.cpp create mode 100644 src/gfx/Screen.hpp diff --git a/roms/donkeykong.nes b/roms/donkeykong.nes new file mode 100644 index 0000000000000000000000000000000000000000..ecd6f0a740de52e799a814dd9f14aef4a811e7d2 GIT binary patch literal 24592 zcmdVC349b)_CI=S=_N@g-4F;JL{dqbC5q50U__)_Knz%71X*RNL1Ds-IvI3kP?0oC zAu_aP9KfI~F;bDr7!0F~@EbEUB!qNiDUnqKnk|M!C`2JZ=ziZ@-C1;e@BjXv&;Rpz zpLfHpyPbRPx#ymH?zyL`8Tshw&J06Zf{<?}!gGkT5kKdN@C8E{D{Ezmi6Jb*Fed!5 z40}{MHKv2W+$avY9S$a!achytpqg<tCu^jgukSolbERhVPV3Ipon3bJ+c|RQ(!k!p zZ?;K%Rp3P6#ckEwYqu}jR<Z4!ZF{#J*miW=$!%-4eYee4lV9^@O}CnonqB0rZ7a5w z){w)5FvNF=cbdbsU+@iaVPOsLj0@NLjQlhsKa_7UhkZSH!i2wA<C`|LXM_+RrsG6( zkkA*zv4(_v#4(M8hLSMqxY{qQz_9scN8b1jwL^JnuPy(5;$Fk&%2wa᨜FOkBY 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 literal 0 HcmV?d00001 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<Byte>(0x800); @@ -15,7 +15,7 @@ Bus::Bus() : VRAM = std::vector<Byte>(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 <glad/glad.h> +#include <string> +#include <stdexcept> + +#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 <cstdint> +#include <vector> + +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<Color> 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; };