Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e566adad50 | ||
|
|
1c14228bf7 | ||
|
|
00ceacaf52 | ||
|
|
adb290a7c7 | ||
|
|
67539b6d43 | ||
|
|
7ed5c74faf | ||
|
|
f2a68e6294 | ||
|
|
5e6972e4da | ||
|
|
c0c592c438 | ||
|
|
900dbde54b | ||
|
|
16db21fcab | ||
|
|
d33de14694 | ||
|
|
80c68035c5 | ||
|
|
8f016b1095 | ||
|
|
869438e0e0 | ||
|
|
6943cfd684 | ||
|
|
fcfd27468d | ||
|
|
ce46f5033a | ||
|
|
5919f70537 | ||
|
|
136250355e | ||
|
|
66c10e3c3e |
+3
-3
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 ]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"window": {
|
||||
"fullscreen": "windowed",
|
||||
"height": 432,
|
||||
"title": "GarbAGE",
|
||||
"vsync": true,
|
||||
"width": 480
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
file(COPY ${CMAKE_CURRENT_LIST_DIR}/../assets DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
|
||||
+1
-2
@@ -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);
|
||||
|
||||
+5
-4
@@ -1241,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
|
||||
|
||||
@@ -1256,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
|
||||
|
||||
@@ -1272,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
|
||||
@@ -1289,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:
|
||||
@@ -1473,6 +1473,7 @@ void CPU::call()
|
||||
|
||||
if (!should_call) {
|
||||
m_wait_cycles += 12;
|
||||
return;
|
||||
}
|
||||
m_wait_cycles += 24;
|
||||
|
||||
|
||||
+18
-1
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -22,7 +22,6 @@ public:
|
||||
|
||||
private:
|
||||
void init();
|
||||
void update();
|
||||
void destroy();
|
||||
|
||||
void loadCartridgeHeader();
|
||||
|
||||
+37
-5
@@ -5,14 +5,23 @@
|
||||
* 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;
|
||||
|
||||
@@ -23,6 +32,29 @@ int main(int argc, const char* argv[])
|
||||
|
||||
Loader::the().setBootromPath(bootrom_path);
|
||||
Loader::the().loadRom(rom_path);
|
||||
}
|
||||
|
||||
return 0;
|
||||
~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[])
|
||||
{
|
||||
GarbAGE* gameboy = new GarbAGE(argc, argv);
|
||||
return gameboy;
|
||||
}
|
||||
|
||||
+297
-8
@@ -6,17 +6,30 @@
|
||||
*/
|
||||
|
||||
#include <cstdint> // 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,286 @@ 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;
|
||||
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;
|
||||
}
|
||||
Emu::the().writeMemory(0xff44, ly_register);
|
||||
break;
|
||||
case State::PixelTransfer:
|
||||
pixelFifo();
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
else {
|
||||
// // Tile map
|
||||
// uint32_t tile_map_size = 32 * 32; // 1 KiB
|
||||
// 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
|
||||
// // uint32_t tile_data_size = 4096; // 4KiB / 16B = 256 tiles
|
||||
// bool tile_data_mode = lcd_control & LCDC::BGandWindowTileDataArea;
|
||||
// uint32_t tile_data_address = (tile_data_mode) ? 0x8000 : 0x8800;
|
||||
|
||||
// for (uint32_t i = 0; i < tile_map_size; ++i) {
|
||||
// uint8_t tile_index = Emu::the().readMemory(bg_tile_map_address + i);
|
||||
// // drawTile((i % 32) * TILE_WIDTH, (i / 32) * TILE_HEIGHT, tile_data_address + (tile_index * TILE_SIZE));
|
||||
// }
|
||||
}
|
||||
|
||||
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::pixelFifo()
|
||||
{
|
||||
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
|
||||
uint32_t tile_data_address = (lcd_control & LCDC::BGandWindowTileDataArea) ? 0x8000 : 0x8800;
|
||||
|
||||
// https://gbdev.io/pandocs/Tile_Data.html
|
||||
auto getBgTileDataAddress = [&](uint8_t tile_index) -> uint32_t {
|
||||
switch (tile_data_address) {
|
||||
case 0x8000:
|
||||
// 0x8000-0x8fff: index 0 => 255
|
||||
return 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 tile_data_address + 0x800 + (tile_index * TILE_SIZE); // Each tile is 16 bytes
|
||||
}
|
||||
else {
|
||||
return tile_data_address + ((tile_index - 128) * TILE_SIZE); // Each tile is 16 bytes
|
||||
}
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
return 0;
|
||||
};
|
||||
};
|
||||
|
||||
// -------------------------------------
|
||||
// FIFO Pixel Fetcher
|
||||
|
||||
switch (m_pixel_fifo.state) {
|
||||
case PixelFifo::State::TileIndex:
|
||||
if (!m_pixel_fifo.step) {
|
||||
m_pixel_fifo.step = true;
|
||||
}
|
||||
else {
|
||||
m_pixel_fifo.step = false;
|
||||
m_pixel_fifo.state = PixelFifo::State::TileDataLow;
|
||||
|
||||
// Viewport
|
||||
// https://gbdev.io/pandocs/Scrolling.html#mid-frame-behavior
|
||||
m_viewport_x = Emu::the().readMemory(0xff43); // TODO: only read lower 3-bits at beginning of scanline
|
||||
m_viewport_y = Emu::the().readMemory(0xff42);
|
||||
|
||||
// Read the tile map index
|
||||
uint16_t offset = (((m_viewport_y + m_lcd_y_coordinate) / TILE_HEIGHT) * 32)
|
||||
+ ((m_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) & 0xff;
|
||||
|
||||
// Set the tile line we're currently on
|
||||
m_pixel_fifo.tile_line = (m_viewport_y + m_lcd_y_coordinate) % TILE_HEIGHT;
|
||||
}
|
||||
break;
|
||||
case PixelFifo::State::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
|
||||
}
|
||||
break;
|
||||
case PixelFifo::State::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);
|
||||
}
|
||||
break;
|
||||
case PixelFifo::State::Sleep:
|
||||
if (m_pixel_fifo.background.size() <= 9) {
|
||||
m_pixel_fifo.state = PixelFifo::State::Push;
|
||||
}
|
||||
break;
|
||||
case PixelFifo::State::Push:
|
||||
m_pixel_fifo.state = PixelFifo::State::TileIndex;
|
||||
|
||||
for (uint8_t i = 0; i < 8; ++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 });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
};
|
||||
|
||||
// -------------------------------------
|
||||
// Mode 3 Operation
|
||||
|
||||
// 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++;
|
||||
}
|
||||
}
|
||||
|
||||
void PPU::drawTile(uint32_t x, uint32_t y, uint32_t tile_address)
|
||||
{
|
||||
uint32_t viewport_x = Emu::the().readMemory(0xff43);
|
||||
uint32_t viewport_y = Emu::the().readMemory(0xff42);
|
||||
|
||||
// Tile is not within viewport
|
||||
if ((x < viewport_x || x > viewport_x + SCREEN_WIDTH)
|
||||
|| (y < viewport_y || y > viewport_y + SCREEN_HEIGHT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t screen_index = ((x - viewport_x) * FORMAT_SIZE) + ((y - viewport_y) * SCREEN_WIDTH * FORMAT_SIZE);
|
||||
for (uint8_t tile_y = 0; tile_y < TILE_SIZE; tile_y += 2) {
|
||||
uint8_t pixels_lsb = Emu::the().readMemory(tile_address + tile_y);
|
||||
uint8_t pixels_msb = Emu::the().readMemory(tile_address + tile_y + 1);
|
||||
|
||||
for (uint8_t tile_x = 0; tile_x < 8; ++tile_x) {
|
||||
size_t index = screen_index + (tile_x * FORMAT_SIZE);
|
||||
|
||||
// FIXME: Tile is partly out of viewport?
|
||||
if (index + 2 >= m_screen.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t pixel_index = (pixels_lsb >> (7 - tile_x) | ((pixels_msb >> (7 - tile_x)) << 1)) & 0x3;
|
||||
auto pixel_color = getPixelColor(pixel_index, Palette::BGP);
|
||||
m_screen[index + 0] = pixel_color[0];
|
||||
m_screen[index + 1] = pixel_color[1];
|
||||
m_screen[index + 2] = pixel_color[2];
|
||||
}
|
||||
|
||||
// Move to next line
|
||||
screen_index += SCREEN_WIDTH * FORMAT_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
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::resetFrame()
|
||||
{
|
||||
m_state = State::OAMSearch;
|
||||
m_clocks_into_frame = 0;
|
||||
m_lcd_x_coordinate = 0;
|
||||
m_lcd_y_coordinate = 0;
|
||||
}
|
||||
|
||||
@@ -7,27 +7,94 @@
|
||||
|
||||
#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;
|
||||
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 pixelFifo();
|
||||
void drawTile(uint32_t screen_x, uint32_t screen_y, uint32_t tile_address);
|
||||
std::array<uint8_t, 3> getPixelColor(uint8_t color_index, Palette palette);
|
||||
|
||||
void resetFrame();
|
||||
|
||||
private:
|
||||
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
|
||||
|
||||
uint8_t m_viewport_x { 0 };
|
||||
uint8_t m_viewport_y { 0 };
|
||||
PixelFifo m_pixel_fifo;
|
||||
|
||||
uint32_t m_entity;
|
||||
std::array<uint8_t, SCREEN_WIDTH * SCREEN_HEIGHT * FORMAT_SIZE> m_screen;
|
||||
};
|
||||
|
||||
+29
-28
@@ -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);
|
||||
}
|
||||
|
||||
+1
Submodule vendor/inferno added at f47babe5cf
Vendored
-1
Submodule vendor/ruc deleted from d3de1fb635
Reference in New Issue
Block a user