Compare commits

...
30 Commits
Author SHA1 Message Date
Riyyi 301c9343da Emulator: Separate pixel FIFO states into different functions 2022-10-18 21:24:06 +02:00
Riyyi 91884d65fb Emulator: Remove naive old tile draw code 2022-10-18 20:08:07 +02:00
Riyyi e566adad50 Emulator: Add support for both tile index mapping variants 2022-10-18 13:53:39 +02:00
Riyyi 1c14228bf7 Emulator: Fix pixel FIFO fetcher half step speed 2022-10-17 15:58:12 +02:00
Riyyi 00ceacaf52 Emulator: Fix pixel FIFO x-coordinate offset 2022-10-17 15:51:16 +02:00
Riyyi adb290a7c7 Emulator: Move LCDC inside PPU class 2022-10-17 13:33:26 +02:00
Riyyi 67539b6d43 Emulator: Change PPU::State values 2022-10-17 13:33:26 +02:00
Riyyi 7ed5c74faf Emulator: Switch to pixel FIFO 2022-10-17 13:33:26 +02:00
Riyyi f2a68e6294 Emulator: Add initial pixel FIFO to PPU 2022-10-17 13:33:26 +02:00
Riyyi 5e6972e4da Emulator: Fix PPU clocks and LCD offsets 2022-10-16 21:40:58 +02:00
Riyyi c0c592c438 Emulator: Make palette colors more accurate 2022-10-16 13:06:11 +02:00
Riyyi 900dbde54b Emulator: Set camera zoom-level and screen flip in scene JSON 2022-10-16 12:47:21 +02:00
Riyyi 16db21fcab Emulator: Fix predicate jump call opcodes 2022-10-16 12:46:10 +02:00
Riyyi d33de14694 Emulator: Fix swap opcodes 2022-10-16 12:46:10 +02:00
Riyyi 80c68035c5 Emulator: Fix non-prefix rotate opcodes 2022-10-16 12:46:10 +02:00
Riyyi 8f016b1095 Emulator: Add function to fetch pixel color from palette 2022-10-16 12:46:10 +02:00
Riyyi 869438e0e0 Emulator: Add state machine to PPU 2022-10-16 12:46:10 +02:00
Riyyi 6943cfd684 Emulator: Add naive background tile rendering to PPU 2022-10-16 12:46:10 +02:00
Riyyi fcfd27468d Emulator: Change main to become an Inferno application 2022-10-16 12:46:10 +02:00
Riyyi ce46f5033a Emulator+Test: Store ProcessingUnits as std::shared_ptr 2022-10-16 12:46:10 +02:00
Riyyi 5919f70537 CMake+Meta: Add inferno dependency, which includes ruc indirectly 2022-10-16 12:46:10 +02:00
Riyyi 136250355e Meta: Add assets for settings and scene 2022-10-16 11:32:52 +02:00
Riyyi 66c10e3c3e Doc: Update git submodule instructions in readme 2022-09-27 12:42:28 +02:00
Riyyi 7153b0e9b6 Emulator: Optimize interrupt request checking 2022-09-02 23:15:06 +02:00
Riyyi cd2673909c Emulator: Implement DI/EI opcodes, finish RETI opcode 2022-09-02 22:44:24 +02:00
Riyyi 5b4dbdc9a4 Emulator: Add interrupt handling 2022-09-02 22:44:24 +02:00
Riyyi 5b08c18127 Emulator: Fix DAA carry flag, opcode: 0x27 2022-09-02 16:35:43 +02:00
Riyyi 857865dfb9 Emulator: You can only have copyright on stuff you have worked on 2022-09-02 16:08:31 +02:00
Riyyi a29f01389b Emulator: Fix RET zero flag condition, opcode: 0xc0, 0xc8 2022-09-01 21:49:29 +02:00
Riyyi a806f173ce Emulator: Add getters for CPU registers 2022-09-01 18:13:32 +02:00
21 changed files with 624 additions and 134 deletions
+3 -3
View File
@@ -1,3 +1,3 @@
[submodule "vendor/ruc"]
path = vendor/ruc
url = https://github.com/riyyi/ruc
[submodule "vendor/inferno"]
path = vendor/inferno
url = https://github.com/riyyi/inferno
+15 -11
View File
@@ -52,21 +52,28 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# ------------------------------------------
# Library
set(RUC_BUILD_TESTS OFF)
add_subdirectory("vendor/ruc")
set(INFERNO_BUILD_EXAMPLES OFF)
add_subdirectory("vendor/inferno")
# ------------------------------------------
# Application target
# Define source files
file(GLOB_RECURSE PROJECT_SOURCES "src/*.cpp")
set(PROJECT_SOURCES ${PROJECT_SOURCES})
add_executable(${PROJECT} ${PROJECT_SOURCES})
target_include_directories(${PROJECT} PRIVATE
"src"
"vendor/ruc/src")
target_link_libraries(${PROJECT} ruc)
"src")
target_link_libraries(${PROJECT} inferno)
# ------------------------------------------
# Assets target
add_custom_target(${PROJECT}-assets
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/copy-assets.cmake
WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
add_dependencies(${PROJECT} ${PROJECT}-assets)
add_dependencies(${PROJECT}-assets inferno-assets) # Copy project assets *AFTER* engine assets!
# ------------------------------------------
# Execute target
@@ -87,9 +94,6 @@ if (GARBAGE_BUILD_TESTS)
add_executable(${PROJECT}-unit-test ${TEST_SOURCES})
target_include_directories(${PROJECT}-unit-test PRIVATE
"src"
"test"
"vendor/ruc/src"
"vendor/ruc/test")
target_link_libraries(${PROJECT}-unit-test ruc)
target_link_libraries(${PROJECT}-unit-test ruc-test)
"test")
target_link_libraries(${PROJECT}-unit-test inferno)
endif()
+3 -4
View File
@@ -10,14 +10,13 @@ Accurate GameBoy emulator.
*** Clone
#+BEGIN_SRC shell-script
$ git clone https://gitlab.com/riyyi/garbage
$ git clone https://github.com/riyyi/garbage
$ cd garbage
$ git submodule init
$ git submodule update
$ git submodule update --init --recursive
#+END_SRC
Or
#+BEGIN_SRC shell-script
$ git clone --recursive https://gitlab.com/riyyi/garbage
$ git clone --recursive https://github.com/riyyi/garbage
#+END_SRC
*** Update
+16
View File
@@ -0,0 +1,16 @@
{
"camera": {
"name": "Camera",
"translate": [ 0.0, 0.0, -1.0 ],
"rotate": [ 0.0, 0.0, 0.0 ],
"scale": [ 1.0, 1.0, 1.0 ],
"type": "orthographic",
"zoom-level": 0.45
},
"quad" : [
{
"name": "Screen",
"scale": [ 1.0, -0.9, 1.0 ]
}
]
}
+9
View File
@@ -0,0 +1,9 @@
{
"window": {
"fullscreen": "windowed",
"height": 432,
"title": "GarbAGE",
"vsync": true,
"width": 480
}
}
+1
View File
@@ -0,0 +1 @@
file(COPY ${CMAKE_CURRENT_LIST_DIR}/../assets DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
+1 -2
View File
@@ -365,8 +365,7 @@ void CPU::swap()
m_wait_cycles += 8;
// Swap upper 4 bits in register r8 with lower 4 bits
register_ = register_ & 0xff;
register_ = (register_ >> 4) | (register_ << 4);
register_ = ((register_ >> 4) | (register_ << 4)) & 0xff;
// Set flags
m_zf = (register_ == 0);
+83 -19
View File
@@ -12,6 +12,7 @@
#include "ruc/format/color.h"
#include "ruc/format/print.h"
#include "ruc/meta/assert.h"
#include "ruc/meta/core.h"
CPU::CPU(uint32_t frequency)
: ProcessingUnit(frequency)
@@ -55,8 +56,57 @@ CPU::~CPU()
// -----------------------------------------
void CPU::handleInterrupt(uint32_t interrupt_flag, uint8_t interrupt_source, uint8_t address)
{
// Clear interrupt
m_ime = 0;
Emu::the().writeMemory(0xff0f, interrupt_flag & (~interrupt_source));
// Call
m_wait_cycles += 20;
// Push address of the program counter on the stack, such that RET can pop it later
m_sp = (m_sp - 1) & 0xffff;
write(m_sp, m_pc >> 8); // msb(PC)
m_sp = (m_sp - 1) & 0xffff;
write(m_sp, m_pc & 0xff); // lsb(PC)
// Jump to address
m_pc = address;
}
void CPU::update()
{
// -------------------------------------
// Interrupt Service Routine
bool effective_ime = m_ime;
// IME only becomes active after the instruction following EI
if (m_should_enable_ime) {
m_ime = 1;
m_should_enable_ime = 0;
}
if (effective_ime) {
// Get the 5 lower bits of the IE (interrupt enable) address
uint32_t interrupt_enabled = Emu::the().readMemory(0xffff) & 0x1f;
// Get the 5 lower bits of the IF (interrupt flag) address
uint32_t interrupt_flag = Emu::the().readMemory(0xff0f) & 0x1f;
uint32_t interrupt = interrupt_enabled & interrupt_flag;
for (uint8_t i = 0; i < 5; ++i) {
if (interrupt & BIT(i)) {
handleInterrupt(interrupt_flag, BIT(i), i * 8 + 0x40);
break;
}
}
}
// -------------------------------------
// Run opcodes
m_wait_cycles--;
// TODO: convert to early-return
if (m_wait_cycles <= 0) {
@@ -69,8 +119,6 @@ void CPU::update()
switch (opcode) {
case 0x10: /* TODO */ m_pc += 2; break;
case 0xf3: /* TODO */ m_pc += 1; break;
case 0xfb: /* TODO */ m_pc += 1; break;
case 0x00: nop(); break;
case 0x01: ldi16(); break;
@@ -305,12 +353,14 @@ void CPU::update()
case 0xf0: ldff8(); break;
case 0xf1: pop(); break;
case 0xf2: ldff8(); break;
case 0xf3: misc(); break;
case 0xf5: push(); break;
case 0xf6: or8(); break;
case 0xf7: rst(); break;
case 0xf8: ldr16(); break;
case 0xf9: ldr16(); break;
case 0xfa: lda8(); break;
case 0xfb: misc(); break;
case 0xfe: cp(); break;
case 0xff: rst(); break;
@@ -548,6 +598,8 @@ void CPU::daa()
if (!m_nf) {
if (m_cf || m_a > 0x99) {
m_a += 0x60;
// Carry flag
m_cf = 1;
}
if (m_hf || (m_a & 0xf) > 0x9) {
@@ -564,9 +616,6 @@ void CPU::daa()
}
}
// Carry flag
m_cf = (m_a & 0x100) == 0x100;
m_a = m_a & 0xff;
// Set flags
@@ -1192,7 +1241,7 @@ void CPU::ra()
m_cf = (m_a & 0x80) == 0x80;
// Rotate register A left
m_a = (m_a >> 7) | (m_a << 1);
m_a = ((m_a >> 7) | (m_a << 1)) & 0xff;
break;
case 0x0f: // RRCA
@@ -1207,7 +1256,7 @@ void CPU::ra()
m_cf = (m_a & 0x1) == 0x1;
// Rotate register A right
m_a = (m_a >> 1) | (m_a << 7);
m_a = ((m_a >> 1) | (m_a << 7)) & 0xff;
break;
case 0x17: { // RLA
@@ -1223,7 +1272,7 @@ void CPU::ra()
m_cf = (m_a & 0x80) == 0x80;
// Rotate register A left through carry
m_a = old_carry | (m_a << 1);
m_a = (old_carry | (m_a << 1)) & 0xff;
break;
}
case 0x1f: { // RRA
@@ -1240,7 +1289,7 @@ void CPU::ra()
m_cf = (m_a & 0x1) == 0x1;
// Rotate register A right through carry
m_a = (m_a >> 1) | (old_carry << 7);
m_a = ((m_a >> 1) | (old_carry << 7)) & 0xff;
break;
}
default:
@@ -1424,14 +1473,16 @@ void CPU::call()
if (!should_call) {
m_wait_cycles += 12;
return;
}
m_wait_cycles += 24;
// Push address of the instruction after the CALL on the stack, such that RET can pop it later
// Push address of the instruction after the CALL on the stack,
// such that RET can pop it later
m_sp = (m_sp - 1) & 0xffff;
write(m_sp, m_pc >> 8);
write(m_sp, m_pc >> 8); // msb(PC)
m_sp = (m_sp - 1) & 0xffff;
write(m_sp, m_pc & 0xff);
write(m_sp, m_pc & 0xff); // lsb(PC)
// Jump to operand address
m_pc = data;
@@ -1439,8 +1490,8 @@ void CPU::call()
uint8_t opcode = pcRead();
switch (opcode) {
case 0xc4: /* CALL NZ,i16 */ function_call(!m_nf); break;
case 0xcc: /* CALL Z,i16 */ function_call(m_nf); break;
case 0xc4: /* CALL NZ,i16 */ function_call(!m_zf); break;
case 0xcc: /* CALL Z,i16 */ function_call(m_zf); break;
case 0xcd: /* CALL i16 */ function_call(true); break;
case 0xd4: /* CALL NC,i16 */ function_call(!m_cf); break;
case 0xdc: /* CALL C,i16 */ function_call(m_cf); break;
@@ -1528,23 +1579,23 @@ void CPU::ret()
// Return from subroutine if condition cc is met,
// this is basically a POP PC (if such an instruction existed)
m_pc = read(m_sp);
m_pc = read(m_sp); // lsb(SP)
m_sp = (m_sp + 1) & 0xffff;
m_pc = m_pc | (read(m_sp) << 8);
m_pc = m_pc | (read(m_sp) << 8); // msb(SP)
m_sp = (m_sp + 1) & 0xffff;
};
uint8_t opcode = pcRead();
switch (opcode) {
case 0xc0: /* RET NZ,i16 */ function_return(!m_nf); break;
case 0xc8: /* RET Z,i16 */ function_return(m_nf); break;
case 0xc0: /* RET NZ,i16 */ function_return(!m_zf); break;
case 0xc8: /* RET Z,i16 */ function_return(m_zf); break;
case 0xc9: /* RET i16 */ function_return(true); break;
case 0xd0: /* RET NC,i16 */ function_return(!m_cf); break;
case 0xd8: /* RET C,i16 */ function_return(m_cf); break;
case 0xd9: /* RETI */ {
// Return from subroutine
// TODO: and enable interrupts.
function_return(true);
m_ime = true;
break;
}
default:
@@ -1611,6 +1662,19 @@ void CPU::misc()
// Invert carry
m_cf = (m_cf) ? 0 : 1;
break;
case 0xf3: // DI
m_wait_cycles += 4;
// Disable Interrupts by clearing the IME flag
m_ime = 0;
break;
case 0xfb: // EI
m_wait_cycles += 4;
// Enable Interrupts by setting the IME flag.
// The flag is only set after the instruction following EI
m_should_enable_ime = true;
break;
default:
VERIFY_NOT_REACHED();
}
+16 -4
View File
@@ -22,6 +22,7 @@ public:
explicit CPU(uint32_t frequency);
virtual ~CPU();
void handleInterrupt(uint32_t interrupt_flag, uint8_t interrupt_source, uint8_t address);
void update() override;
// -------------------------------------
@@ -102,6 +103,10 @@ public:
// -------------------------------------
uint32_t a() const { return m_a; }
uint32_t b() const { return m_b; }
uint32_t c() const { return m_c; }
uint32_t af() const { return (m_cf << 4 | m_hf << 5 | m_nf << 6 | m_zf << 7) | m_a << 8; }
uint32_t bc() const { return m_c | m_b << 8; }
uint32_t de() const { return m_e | m_d << 8; }
@@ -109,6 +114,11 @@ public:
uint32_t pc() const { return m_pc; }
uint32_t sp() const { return m_sp; }
uint32_t zf() const { return m_zf; }
uint32_t nf() const { return m_nf; }
uint32_t hf() const { return m_hf; }
uint32_t cf() const { return m_cf; }
void setBC(uint32_t value);
void setDE(uint32_t value);
void setHL(uint32_t value);
@@ -138,11 +148,13 @@ private:
uint32_t m_sp { 0 }; // Stack Pointer
// Flags
uint32_t m_zf { 0 }; // Zero flag
uint32_t m_nf { 0 }; // Subtraction flag (BCD)
uint32_t m_hf { 0 }; // Half Carry flag (BCD)
uint32_t m_cf { 0 }; // Carry flag
uint32_t m_zf { 0 }; // Zero flag
uint32_t m_nf { 0 }; // Subtraction flag (BCD)
uint32_t m_hf { 0 }; // Half Carry flag (BCD)
uint32_t m_cf { 0 }; // Carry flag
uint32_t m_ime { 0 }; // Interrupt Master Enable flag
bool m_should_enable_ime { 0 };
int8_t m_wait_cycles { 0 };
};
-1
View File
@@ -1,6 +1,5 @@
/*
* Copyright (C) 2022 Riyyi
* Copyright (C) 2022 Th3FrankXD
*
* SPDX-License-Identifier: MIT
*/
-1
View File
@@ -1,6 +1,5 @@
/*
* Copyright (C) 2022 Riyyi
* Copyright (C) 2022 Th3FrankXD
*
* SPDX-License-Identifier: MIT
*/
+18 -1
View File
@@ -39,7 +39,7 @@ void Emu::update()
}
}
void Emu::addProcessingUnit(std::string_view name, ProcessingUnit* processing_unit)
void Emu::addProcessingUnit(std::string_view name, std::shared_ptr<ProcessingUnit> processing_unit)
{
m_processing_units.emplace(name, processing_unit);
}
@@ -64,6 +64,16 @@ void Emu::removeMemorySpace(std::string_view name)
void Emu::writeMemory(uint32_t address, uint32_t value)
{
// Bail if the CPU tries to write to a read-only address
switch (address) {
case 0xff44:
ruc::error("writing to read-only address: {:#06x}", address);
VERIFY_NOT_REACHED();
break;
default:
break;
}
for (auto& memory_space : m_memory_spaces) {
auto& memory = memory_space.second;
if (address >= memory.start_address && address <= memory.end_address) {
@@ -96,6 +106,13 @@ void Emu::writeMemory(uint32_t address, uint32_t value)
uint32_t Emu::readMemory(uint32_t address) const
{
switch (address) {
case 0xff44:
return *m_processing_units.at("PPU")->sharedRegister("LY");
default:
break;
};
for (const auto& memory_space : m_memory_spaces) {
const auto& memory = memory_space.second;
if (address >= memory.start_address && address <= memory.end_address) {
+11 -4
View File
@@ -8,7 +8,7 @@
#pragma once
#include <cstdint> // uint32_t
#include <string>
#include <memory> // std::shared_ptr
#include <string_view>
#include <unordered_map>
#include <vector>
@@ -30,11 +30,16 @@ class Emu final : public ruc::Singleton<Emu> {
public:
Emu(s) {}
enum Mode : uint8_t {
DMG, // Game Boy
CGB, // Game Boy Color
};
void init(uint32_t frequency);
void update();
void addProcessingUnit(std::string_view name, ProcessingUnit* processing_unit);
void addProcessingUnit(std::string_view name, std::shared_ptr<ProcessingUnit> processing_unit);
void addMemorySpace(std::string_view name, uint32_t start_address, uint32_t end_address, uint32_t amount_of_banks = 1);
void removeMemorySpace(std::string_view name);
@@ -43,10 +48,12 @@ public:
// -------------------------------------
ProcessingUnit* processingUnit(std::string_view name) const { return m_processing_units.at(name); }
Mode mode() const { return m_mode; }
std::shared_ptr<ProcessingUnit> processingUnit(std::string_view name) const { return m_processing_units.at(name); }
MemorySpace memorySpace(std::string_view name) { return m_memory_spaces[name]; }
private:
Mode m_mode { Mode::DMG };
uint32_t m_frequency { 0 };
double m_timestep { 0 };
uint32_t m_cycle { 0 };
@@ -55,6 +62,6 @@ private:
ruc::Timer m_timer;
std::unordered_map<std::string_view, ProcessingUnit*> m_processing_units;
std::unordered_map<std::string_view, std::shared_ptr<ProcessingUnit>> m_processing_units;
std::unordered_map<std::string_view, MemorySpace> m_memory_spaces;
};
+6 -14
View File
@@ -6,6 +6,7 @@
#include <cstddef> // size_t
#include <cstdint> // uint32_t
#include <memory> // std::make_shared
#include "cpu.h"
#include "emu.h"
@@ -42,13 +43,13 @@ void Loader::init()
{
destroy();
Emu::the().init(8000000);
Emu::the().init(4000000);
CPU cpu(8000000);
PPU ppu(4000000);
auto cpu = std::make_shared<CPU>(4000000);
auto ppu = std::make_shared<PPU>(4000000);
Emu::the().addProcessingUnit("cpu", &cpu);
Emu::the().addProcessingUnit("ppu", &ppu);
Emu::the().addProcessingUnit("CPU", cpu);
Emu::the().addProcessingUnit("PPU", ppu);
// https://gbdev.io/pandocs/Memory_Map.html
// https://gbdev.io/pandocs/Power_Up_Sequence.html
@@ -77,15 +78,6 @@ void Loader::init()
Emu::the().writeMemory(i, bootrom[i]);
}
update();
}
void Loader::update()
{
while (true) {
Emu::the().update();
}
}
void Loader::destroy()
-1
View File
@@ -22,7 +22,6 @@ public:
private:
void init();
void update();
void destroy();
void loadCartridgeHeader();
+47 -15
View File
@@ -5,24 +5,56 @@
* SPDX-License-Identifier: MIT
*/
#include "loader.h"
#include <string_view>
#include "inferno.h"
#include "inferno/entrypoint.h"
#include "ppu.h"
#include "ruc/argparser.h"
#include "ruc/format/print.h"
#include "ruc/timer.h"
#include <string_view>
int main(int argc, const char* argv[])
#include "emu.h"
#include "loader.h"
class GarbAGE final : public Inferno::Application {
public:
GarbAGE(int argc, char* argv[])
: Application()
{
std::string_view bootrom_path = "gbc_bios.bin";
std::string_view rom_path;
ruc::ArgParser argParser;
argParser.addOption(bootrom_path, 'b', "bootrom", nullptr, nullptr, "", ruc::ArgParser::Required::Yes);
argParser.addOption(rom_path, 'r', "rom", nullptr, nullptr, "", ruc::ArgParser::Required::Yes);
argParser.parse(argc, argv);
Loader::the().setBootromPath(bootrom_path);
Loader::the().loadRom(rom_path);
}
~GarbAGE()
{
}
void update() override
{
// 154 scanlines * 456 cycles = 70224 cycles per frame
for (int i = 0; i < (144 * 456 + 10 * 456); ++i) {
Emu::the().update();
}
}
void render() override
{
auto* ppu = static_cast<PPU*>(Emu::the().processingUnit("PPU").get());
ppu->render();
}
};
Inferno::Application* Inferno::createApplication(int argc, char* argv[])
{
std::string_view bootrom_path = "gbc_bios.bin";
std::string_view rom_path;
ruc::ArgParser argParser;
argParser.addOption(bootrom_path, 'b', "bootrom", nullptr, nullptr, "", ruc::ArgParser::Required::Yes);
argParser.addOption(rom_path, 'r', "rom", nullptr, nullptr, "", ruc::ArgParser::Required::Yes);
argParser.parse(argc, argv);
Loader::the().setBootromPath(bootrom_path);
Loader::the().loadRom(rom_path);
return 0;
GarbAGE* gameboy = new GarbAGE(argc, argv);
return gameboy;
}
+274 -10
View File
@@ -5,18 +5,31 @@
* SPDX-License-Identifier: MIT
*/
#include <cstdint> // uint32_t
#include <cstdint> // uint8_t, uint16_t, uint32_t
#include <memory> // std::make_shared
#include "glm/ext/vector_float3.hpp" // glm::vec3
#include "glm/ext/vector_float4.hpp" // glm::vec4
#include "inferno/application.h"
#include "inferno/component/spritecomponent.h"
#include "inferno/component/transformcomponent.h"
#include "inferno/scene/scene.h"
#include "ruc/format/print.h"
#include "emu.h"
#include "ppu.h"
#include "ruc/format/print.h"
#include "ruc/meta/assert.h"
PPU ::PPU(uint32_t frequency)
PPU::PPU(uint32_t frequency)
: ProcessingUnit(frequency)
{
m_shared_registers.emplace("LY", &m_lcd_y_coordinate);
auto& scene = Inferno::Application::the().scene();
m_entity = scene.findEntity("Screen");
}
PPU ::~PPU()
PPU::~PPU()
{
}
@@ -29,10 +42,261 @@ void PPU::update()
// print("PPU update\n");
// Increment LY (LCD Y Coordinate)
uint32_t ly_register = Emu::the().readMemory(0xff44) + 1;
if (ly_register >= 154) {
ly_register = 0;
}
Emu::the().writeMemory(0xff44, ly_register);
m_clocks_into_frame++;
switch (m_state) {
case State::OAMSearch:
// OAM logic goes here..
if (m_clocks_into_frame % 80 == 0) {
// Reset FIFO
m_pixel_fifo = {};
m_state = State::PixelTransfer;
}
break;
case State::PixelTransfer:
updatePixelFifo();
if (m_lcd_x_coordinate == 160) {
m_lcd_x_coordinate = 0;
m_state = State::HBlank;
}
break;
case State::HBlank:
// H-Blank logic goes here..
if (m_clocks_into_frame % (80 + 172 + 204) == 0) {
m_lcd_y_coordinate++;
if (m_lcd_y_coordinate == 144) {
m_state = State::VBlank;
}
else {
m_state = State::OAMSearch;
}
}
break;
case State::VBlank:
// V-Blank logic goes here..
if (m_clocks_into_frame % (80 + 172 + 204) == 0) {
m_lcd_y_coordinate++;
if (m_lcd_y_coordinate == 154) {
resetFrame();
}
}
break;
default:
VERIFY_NOT_REACHED();
};
}
void PPU::render()
{
LCDC lcd_control = static_cast<LCDC>(Emu::the().readMemory(0xff40));
if (!(lcd_control & LCDC::BGandWindowEnable)) {
// When Bit 0 is cleared, both background and window become blank (white)
auto pixel = getPixelColor(0, Palette::BGP);
for (size_t i = 0; i < m_screen.size(); i += 3) {
m_screen[i + 0] = pixel[0];
m_screen[i + 1] = pixel[1];
m_screen[i + 2] = pixel[2];
}
}
auto& scene = Inferno::Application::the().scene();
auto texture = std::make_shared<Inferno::Texture>(m_screen.data(), SCREEN_WIDTH, SCREEN_HEIGHT, FORMAT_SIZE);
scene.removeComponent<Inferno::SpriteComponent>(m_entity);
scene.addComponent<Inferno::SpriteComponent>(m_entity, glm::vec4 { 1.0f }, texture);
}
void PPU::resetFrame()
{
m_state = State::OAMSearch;
m_clocks_into_frame = 0;
m_lcd_x_coordinate = 0;
m_lcd_y_coordinate = 0;
}
// -----------------------------------------
uint32_t PPU::getBgTileDataAddress(uint8_t tile_index)
{
// https://gbdev.io/pandocs/Tile_Data.html
switch (m_pixel_fifo.tile_data_address) {
case 0x8000:
// 0x8000-0x8fff: index 0 => 255
return m_pixel_fifo.tile_data_address + (tile_index * TILE_SIZE); // Each tile is 16 bytes
case 0x8800:
// 0x8800-0x8fff: index 128 => 255 (or -128 => -1)
// 0x9000-0x97ff: index 0 => 127
if (tile_index <= 127) {
return m_pixel_fifo.tile_data_address + 0x800 + (tile_index * TILE_SIZE); // Each tile is 16 bytes
}
else {
return m_pixel_fifo.tile_data_address + ((tile_index - 128) * TILE_SIZE); // Each tile is 16 bytes
}
default:
VERIFY_NOT_REACHED();
return 0;
};
};
std::array<uint8_t, 3> PPU::getPixelColor(uint8_t color_index, Palette palette)
{
VERIFY(color_index < 4, "trying to fetch invalid color index '{}'", color_index);
switch (Emu::the().mode()) {
case Emu::Mode::DMG: {
uint8_t palette_data = Emu::the().readMemory(palette) & 0xff;
uint8_t palette_value = palette_data >> (color_index * 2) & 0x3;
switch (palette_value) {
case 0:
return { 200, 199, 168 };
case 1:
return { 160, 160, 136 };
case 2:
return { 104, 104, 80 };
case 3:
return { 39, 40, 24 };
default:
VERIFY_NOT_REACHED();
};
}
case Emu::Mode::CGB:
VERIFY_NOT_REACHED();
default:
VERIFY_NOT_REACHED();
}
return {};
}
void PPU::updatePixelFifo()
{
switch (m_pixel_fifo.state) {
case PixelFifo::State::TileIndex:
tileIndex();
break;
case PixelFifo::State::TileDataLow:
tileDataLow();
break;
case PixelFifo::State::TileDataHigh:
tileDataHigh();
break;
case PixelFifo::State::Sleep:
sleep();
break;
case PixelFifo::State::Push:
pushFifo();
break;
default:
VERIFY_NOT_REACHED();
};
pushPixel();
}
void PPU::tileIndex()
{
if (!m_pixel_fifo.step) {
m_pixel_fifo.step = true;
}
else {
m_pixel_fifo.step = false;
m_pixel_fifo.state = PixelFifo::State::TileDataLow;
LCDC lcd_control = static_cast<LCDC>(Emu::the().readMemory(0xff40));
// Tile map
uint32_t bg_tile_map_address = (lcd_control & LCDC::BGTileMapArea) ? 0x9c00 : 0x9800;
// uint32_t window_tile_map_address = (lcd_control & LCDC::WindowTileMapArea) ? 0x9c00 : 0x9800;
// Tile data
m_pixel_fifo.tile_data_address = (lcd_control & LCDC::BGandWindowTileDataArea) ? 0x8000 : 0x8800;
// Viewport
// https://gbdev.io/pandocs/Scrolling.html#mid-frame-behavior
m_pixel_fifo.viewport_x = Emu::the().readMemory(0xff43); // TODO: only read lower 3-bits at beginning of scanline
m_pixel_fifo.viewport_y = Emu::the().readMemory(0xff42);
// Read the tile map index
uint16_t offset = (((m_pixel_fifo.viewport_y + m_lcd_y_coordinate) / TILE_HEIGHT) * 32)
+ ((m_pixel_fifo.viewport_x + m_pixel_fifo.x_coordinate) / TILE_WIDTH);
m_pixel_fifo.x_coordinate += 8;
m_pixel_fifo.tile_index = Emu::the().readMemory(bg_tile_map_address + offset);
// Set the tile line we're currently on
m_pixel_fifo.tile_line = (m_pixel_fifo.viewport_y + m_lcd_y_coordinate) % TILE_HEIGHT;
}
}
void PPU::tileDataLow()
{
if (!m_pixel_fifo.step) {
m_pixel_fifo.step = true;
}
else {
m_pixel_fifo.step = false;
m_pixel_fifo.state = PixelFifo::State::TileDataHigh;
// Read tile data
m_pixel_fifo.pixels_lsb = Emu::the().readMemory(
getBgTileDataAddress(m_pixel_fifo.tile_index)
+ m_pixel_fifo.tile_line * 2); // Each tile line is 2 bytes
}
}
void PPU::tileDataHigh()
{
if (!m_pixel_fifo.step) {
m_pixel_fifo.step = true;
}
else {
m_pixel_fifo.step = false;
m_pixel_fifo.state = PixelFifo::State::Sleep;
// Read tile data
m_pixel_fifo.pixels_msb = Emu::the().readMemory(
getBgTileDataAddress(m_pixel_fifo.tile_index)
+ m_pixel_fifo.tile_line * 2 // Each tile line is 2 bytes
+ 1);
}
}
void PPU::sleep()
{
if (m_pixel_fifo.background.size() <= TILE_WIDTH + 1) {
m_pixel_fifo.state = PixelFifo::State::Push;
}
}
void PPU::pushFifo()
{
m_pixel_fifo.state = PixelFifo::State::TileIndex;
for (uint8_t i = 0; i < TILE_WIDTH; ++i) {
uint8_t color_index = (m_pixel_fifo.pixels_lsb >> (7 - i)
| ((m_pixel_fifo.pixels_msb >> (7 - i)) << 1))
& 0x3;
m_pixel_fifo.background.push({ color_index, Palette::BGP });
}
}
void PPU::pushPixel()
{
// The pixel FIFO needs to contain more than 8 pixels to shift one out
if (m_pixel_fifo.background.size() > 8) {
auto pixel = m_pixel_fifo.background.front();
m_pixel_fifo.background.pop();
uint32_t index = (m_lcd_y_coordinate * SCREEN_WIDTH + m_lcd_x_coordinate) * FORMAT_SIZE;
auto color = getPixelColor(pixel.first, pixel.second);
m_screen[index + 0] = color[0];
m_screen[index + 1] = color[1];
m_screen[index + 2] = color[2];
m_lcd_x_coordinate++;
}
}
+91 -15
View File
@@ -7,27 +7,103 @@
#pragma once
#include <cstdint> // uint8_t, uint32_t
#include <array>
#include <cstdint> // uint8_t, uint16_t, uint32_t
#include <queue>
#include <utility> // std::pair
#include "processing-unit.h"
#include "ruc/meta/core.h"
enum LCDC : uint8_t {
None = 0,
BGandWindowEnable = BIT(0),
OBJEnable = BIT(1),
OBJSize = BIT(2), // 0 = 8x8, 1 = 8x16
BGTileMapArea = BIT(3), // 0 = 9800-9bff, 1 = 9c00-9fff
BGandWindowTileDataArea = BIT(4), // 0 = 8800-97ff, 1 = 8000-8fff
WindowEnable = BIT(5), //
WindowTileMapArea = BIT(6), // 0 = 9800-9bff, 1 = 9c00-9fff
LCDandPPUEnable = BIT(7),
};
#include "processing-unit.h"
#define SCREEN_WIDTH 160
#define SCREEN_HEIGHT 144
#define FORMAT_SIZE 3
#define TILE_WIDTH 8
#define TILE_HEIGHT 8
#define TILE_SIZE 16
class PPU final : public ProcessingUnit {
public:
PPU(uint32_t frequency);
virtual ~PPU();
~PPU();
virtual void update() override;
enum LCDC : uint8_t {
None = 0,
BGandWindowEnable = BIT(0),
OBJEnable = BIT(1),
OBJSize = BIT(2), // 0 = 8x8, 1 = 8x16
BGTileMapArea = BIT(3), // 0 = 0x9800-9bff, 1 = 0x9c00-9fff
BGandWindowTileDataArea = BIT(4), // 0 = 0x8800-97ff, 1 = 0x8000-8fff
WindowEnable = BIT(5), //
WindowTileMapArea = BIT(6), // 0 = 0x9800-9bff, 1 = 0x9c00-9fff
LCDandPPUEnable = BIT(7),
};
enum State : uint8_t {
HBlank = 0,
VBlank = 1,
OAMSearch = 2,
PixelTransfer = 3,
};
enum Palette : uint16_t {
BGP = 0xff47, // BG and Window palette
OBP0 = 0xff48, // OBJ palette 0
OBP1 = 0xff49, // OBJ palette 1
};
struct PixelFifo {
enum State : uint8_t {
TileIndex,
TileDataLow,
TileDataHigh,
Sleep,
Push,
};
State state { State::TileIndex };
bool step { false };
uint32_t tile_data_address { 0 };
uint8_t viewport_x { 0 };
uint8_t viewport_y { 0 };
uint8_t x_coordinate { 0 };
uint8_t tile_index { 0 };
uint8_t tile_line { 0 };
uint8_t pixels_lsb { 0 };
uint8_t pixels_msb { 0 };
using Fifo = std::queue<std::pair<uint8_t, Palette>>; // colorID, source
Fifo background;
Fifo oam;
};
void update() override;
void render();
void resetFrame();
private:
uint32_t getBgTileDataAddress(uint8_t tile_index);
std::array<uint8_t, 3> getPixelColor(uint8_t color_index, Palette palette);
void updatePixelFifo();
void tileIndex();
void tileDataLow();
void tileDataHigh();
void sleep();
void pushFifo();
void pushPixel();
// -------------------------------------
State m_state { State::OAMSearch };
uint32_t m_clocks_into_frame { 0 };
uint32_t m_lcd_x_coordinate { 0 };
uint32_t m_lcd_y_coordinate { 0 }; // Note: includes V-Blank
PixelFifo m_pixel_fifo;
uint32_t m_entity { 0 };
std::array<uint8_t, SCREEN_WIDTH * SCREEN_HEIGHT * FORMAT_SIZE> m_screen;
};
+29 -28
View File
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: MIT
*/
#include <memory> // std::make_shared, std::shared_ptr
#include <vector>
#include "cpu.h"
@@ -26,13 +27,13 @@ struct CPUTest {
}
};
CPU runCPUTest(std::vector<uint8_t> test)
std::shared_ptr<CPU> runCPUTest(std::vector<uint8_t> test)
{
CPU cpu(4000000);
auto cpu = std::make_shared<CPU>(4000000);
Emu::the().destroy();
Emu::the().init(8000000);
Emu::the().addProcessingUnit("cpu", &cpu);
Emu::the().addProcessingUnit("cpu", cpu);
Emu::the().addMemorySpace("FULL", 0x0000, 0xffff);
// Load the test
@@ -41,7 +42,7 @@ CPU runCPUTest(std::vector<uint8_t> test)
}
// Run the test
while (cpu.pc() < test.size()) {
while (cpu->pc() < test.size()) {
Emu::the().update();
}
@@ -83,7 +84,7 @@ TEST_CASE(CPUIsCarry)
TEST_CASE(CPUAddPlusCarry)
{
CPU cpu(0);
std::shared_ptr<CPU> cpu(0);
// ADC A,E
@@ -96,11 +97,11 @@ TEST_CASE(CPUAddPlusCarry)
// clang-format on
};
cpu = runCPUTest(adc_r8);
EXPECT_EQ(cpu.a(), 0xf1);
EXPECT_EQ(cpu.zf(), 0x0);
EXPECT_EQ(cpu.nf(), 0x0);
EXPECT_EQ(cpu.hf(), 0x1);
EXPECT_EQ(cpu.cf(), 0x0);
EXPECT_EQ(cpu->a(), 0xf1);
EXPECT_EQ(cpu->zf(), 0x0);
EXPECT_EQ(cpu->nf(), 0x0);
EXPECT_EQ(cpu->hf(), 0x1);
EXPECT_EQ(cpu->cf(), 0x0);
// ADC A,i8
@@ -112,11 +113,11 @@ TEST_CASE(CPUAddPlusCarry)
// clang-format on
};
cpu = runCPUTest(adc_i8);
EXPECT_EQ(cpu.a(), 0x1d);
EXPECT_EQ(cpu.zf(), 0x0);
EXPECT_EQ(cpu.nf(), 0x0);
EXPECT_EQ(cpu.hf(), 0x0);
EXPECT_EQ(cpu.cf(), 0x1);
EXPECT_EQ(cpu->a(), 0x1d);
EXPECT_EQ(cpu->zf(), 0x0);
EXPECT_EQ(cpu->nf(), 0x0);
EXPECT_EQ(cpu->hf(), 0x0);
EXPECT_EQ(cpu->cf(), 0x1);
// ADC A,(HL)
@@ -129,18 +130,18 @@ TEST_CASE(CPUAddPlusCarry)
// clang-format on
};
cpu = runCPUTest(adc_hl);
EXPECT_EQ(cpu.a(), 0x0);
EXPECT_EQ(cpu.zf(), 0x1);
EXPECT_EQ(cpu.nf(), 0x0);
EXPECT_EQ(cpu.hf(), 0x1);
EXPECT_EQ(cpu.cf(), 0x1);
EXPECT_EQ(cpu->a(), 0x0);
EXPECT_EQ(cpu->zf(), 0x1);
EXPECT_EQ(cpu->nf(), 0x0);
EXPECT_EQ(cpu->hf(), 0x1);
EXPECT_EQ(cpu->cf(), 0x1);
}
TEST_CASE(CPUSetStackPointer)
{
std::vector<uint8_t> test = { 0x31, 0xfe, 0xff }; // LD SP,i16
CPU cpu = runCPUTest(test);
EXPECT_EQ(cpu.sp(), 0xfffe);
auto cpu = runCPUTest(test);
EXPECT_EQ(cpu->sp(), 0xfffe);
}
TEST_CASE(CPUPushToStack)
@@ -154,9 +155,9 @@ TEST_CASE(CPUPushToStack)
0xc5, // PUSH BC
// clang-format on
};
CPU cpu = runCPUTest(push_bc);
EXPECT_EQ(cpu.bc(), 0xfffc);
EXPECT_EQ(cpu.sp(), 0xfffc);
auto cpu = runCPUTest(push_bc);
EXPECT_EQ(cpu->bc(), 0xfffc);
EXPECT_EQ(cpu->sp(), 0xfffc);
EXPECT_EQ(Emu::the().readMemory(0xfffd), 0xff);
EXPECT_EQ(Emu::the().readMemory(0xfffc), 0xfc);
}
@@ -173,9 +174,9 @@ TEST_CASE(CPUPopFromStack)
0xc1, // POP BC
// clang-format on
};
CPU cpu = runCPUTest(pop_bc);
EXPECT_EQ(cpu.bc(), 0x3c5f);
EXPECT_EQ(cpu.sp(), 0xfffe);
auto cpu = runCPUTest(pop_bc);
EXPECT_EQ(cpu->bc(), 0x3c5f);
EXPECT_EQ(cpu->sp(), 0xfffe);
EXPECT_EQ(Emu::the().readMemory(0xfffd), 0x3c);
EXPECT_EQ(Emu::the().readMemory(0xfffc), 0x5f);
}
Vendored Submodule
+1
Submodule vendor/inferno added at f47babe5cf
Vendored
-1
Submodule vendor/ruc deleted from d3de1fb635