Add Debugger interface, RoomObject class

- Log instructions to debugger using experiment flag
- Use BitmapManager for more functionality
- Draw framebuffer and integrated debugger
This commit is contained in:
scawful
2023-11-13 14:51:01 -05:00
parent 75ef4fd9b0
commit 299770922c
27 changed files with 740 additions and 234 deletions

View File

@@ -3,6 +3,7 @@
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <vector>
namespace yaze {
@@ -1092,26 +1093,57 @@ void CPU::ExecuteInstruction(uint8_t opcode) {
break;
}
// Log the address and opcode.
std::cout << "$" << std::uppercase << std::setw(2) << std::setfill('0')
<< static_cast<int>(DB) << ":" << std::hex << PC << ": 0x"
<< std::hex << static_cast<int>(opcode) << " "
<< opcode_to_mnemonic.at(opcode) << " ";
if (flags()->kLogInstructions) {
std::ostringstream oss;
oss << "$" << std::uppercase << std::setw(2) << std::setfill('0')
<< static_cast<int>(DB) << ":" << std::hex << PC << ": 0x" << std::hex
<< static_cast<int>(opcode) << " " << opcode_to_mnemonic.at(opcode)
<< " ";
// Log the operand.
if (operand) {
if (immediate) {
std::cout << "#";
}
std::cout << "$";
if (accumulator_mode) {
std::cout << std::hex << std::setw(2) << std::setfill('0') << operand;
} else {
std::cout << std::hex << std::setw(4) << std::setfill('0')
// Log the operand.
std::string ops;
if (operand) {
if (immediate) {
ops += "#";
}
std::ostringstream oss_ops;
oss_ops << "$";
if (accumulator_mode) {
oss_ops << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(operand);
} else {
oss_ops << std::hex << std::setw(4) << std::setfill('0')
<< static_cast<int>(operand);
}
ops = oss_ops.str();
}
oss << ops << std::endl;
InstructionEntry entry(PC, opcode, ops, oss.str());
instruction_log_.push_back(entry);
} else {
// Log the address and opcode.
std::cout << "$" << std::uppercase << std::setw(2) << std::setfill('0')
<< static_cast<int>(DB) << ":" << std::hex << PC << ": 0x"
<< std::hex << static_cast<int>(opcode) << " "
<< opcode_to_mnemonic.at(opcode) << " ";
// Log the operand.
if (operand) {
if (immediate) {
std::cout << "#";
}
std::cout << "$";
if (accumulator_mode) {
std::cout << std::hex << std::setw(2) << std::setfill('0') << operand;
} else {
std::cout << std::hex << std::setw(4) << std::setfill('0')
<< static_cast<int>(operand);
}
}
std::cout << std::endl;
}
std::cout << std::endl;
}
// Interrupt Vectors

View File

@@ -6,6 +6,7 @@
#include <unordered_map>
#include <vector>
#include "app/core/common.h"
#include "app/emu/clock.h"
#include "app/emu/debug/log.h"
#include "app/emu/memory/memory.h"
@@ -14,6 +15,25 @@ namespace yaze {
namespace app {
namespace emu {
class InstructionEntry {
public:
// Constructor
InstructionEntry(uint32_t addr, uint8_t op, const std::string& ops,
const std::string& instr)
: address(addr), opcode(op), operands(ops), instruction(instr) {}
// Getters for the class members
uint32_t GetAddress() const { return address; }
uint8_t GetOpcode() const { return opcode; }
const std::string& GetOperands() const { return operands; }
const std::string& GetInstruction() const { return instruction; }
uint32_t address; // Memory address of the instruction
uint8_t opcode; // Opcode of the instruction
std::string operands; // Operand(s) of the instruction, if any
std::string instruction; // Human-readable instruction text
};
const std::unordered_map<uint8_t, std::string> opcode_to_mnemonic = {
{0x00, "BRK"}, {0x01, "ORA"}, {0x02, "COP"}, {0x03, "ORA"}, {0x04, "TSB"},
{0x05, "ORA"}, {0x06, "ASL"}, {0x07, "ORA"}, {0x08, "PHP"}, {0x09, "ORA"},
@@ -72,19 +92,20 @@ const std::unordered_map<uint8_t, std::string> opcode_to_mnemonic = {
const int kCpuClockSpeed = 21477272; // 21.477272 MHz
class CPU : public Memory, public Loggable {
class CPU : public Memory, public Loggable, public core::ExperimentFlags {
public:
explicit CPU(Memory& mem, Clock& vclock) : memory(mem), clock(vclock) {}
void Init() {
clock.SetFrequency(kCpuClockSpeed);
memory.ClearMemory();
}
void Update();
void ExecuteInstruction(uint8_t opcode);
void HandleInterrupts();
std::vector<InstructionEntry> instruction_log_;
// ==========================================================================
// Addressing Modes

View File

@@ -13,7 +13,7 @@ namespace emu {
void Emulator::Run() {
// Initialize the emulator if a ROM is loaded
if (!snes_.running()) {
if (!snes_.running() && loading_) {
if (rom()->isLoaded()) {
snes_.Init(*rom());
running_ = true;
@@ -23,13 +23,17 @@ void Emulator::Run() {
// Render the emulator output
RenderNavBar();
RenderEmulator();
if (running_) {
// Handle user input events
HandleEvents();
// Update the emulator state
UpdateEmulator();
RenderEmulator();
}
if (debugger_) {
RenderDebugger();
}
}
@@ -38,14 +42,32 @@ void Emulator::RenderEmulator() {
// You can use the ImGui::Image function to display the emulator output as a
// texture
// ...
ImVec2 size = ImVec2(320, 240);
if (snes_.running()) {
ImGui::Image((void*)snes_.Ppu().GetScreen()->texture(), size, ImVec2(0, 0),
ImVec2(1, 1));
ImGui::Separator();
} else {
ImGui::Dummy(size);
ImGui::Separator();
ImGui::Text("Emulator output not available.");
}
}
void Emulator::RenderNavBar() {
MENU_BAR()
if (ImGui::BeginMenu("Game")) {
MENU_ITEM("Power Off") {}
MENU_ITEM("Pause") {}
MENU_ITEM("Load ROM") { loading_ = true; }
MENU_ITEM("Power Off") {
running_ = false;
loading_ = false;
debugger_ = false;
}
MENU_ITEM("Pause") {
running_ = false;
debugger_ = false;
}
MENU_ITEM("Reset") {}
MENU_ITEM("Save State") {}
@@ -56,7 +78,15 @@ void Emulator::RenderNavBar() {
if (ImGui::BeginMenu("Debug")) {
ImGui::MenuItem("PPU Register Viewer", nullptr, &show_ppu_reg_viewer_);
MENU_ITEM("Debugger") {}
MENU_ITEM("Debugger") { debugger_ = !debugger_; }
if (ImGui::MenuItem("Integrated Debugger", nullptr,
&integrated_debugger_mode_)) {
separate_debugger_mode_ = !integrated_debugger_mode_;
}
if (ImGui::MenuItem("Separate Debugger Windows", nullptr,
&separate_debugger_mode_)) {
integrated_debugger_mode_ = !separate_debugger_mode_;
}
MENU_ITEM("Memory Viewer") {}
MENU_ITEM("Tile Viewer") {}
ImGui::EndMenu();
@@ -82,6 +112,102 @@ void Emulator::UpdateEmulator() {
snes_.Run();
}
void Emulator::RenderDebugger() {
// Define a lambda with the actual debugger
auto debugger = [&]() {
if (ImGui::BeginTable(
"DebugTable", 2,
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY)) {
ImGui::TableNextColumn();
RenderCpuState(snes_.Cpu());
ImGui::TableNextColumn();
RenderCPUInstructionLog(snes_.Cpu().instruction_log_);
ImGui::EndTable();
}
};
if (integrated_debugger_mode_) {
debugger();
} else if (separate_debugger_mode_) {
ImGui::Begin("Debugger");
debugger();
ImGui::End();
}
}
void Emulator::RenderCpuState(CPU& cpu) {
ImGui::Columns(2, "RegistersColumns");
ImGui::Separator();
ImGui::Text("A: 0x%04X", cpu.A);
ImGui::NextColumn();
ImGui::Text("D: 0x%04X", cpu.D);
ImGui::NextColumn();
ImGui::Text("X: 0x%04X", cpu.X);
ImGui::NextColumn();
ImGui::Text("DB: 0x%02X", cpu.DB);
ImGui::NextColumn();
ImGui::Text("Y: 0x%04X", cpu.Y);
ImGui::NextColumn();
ImGui::Text("PB: 0x%02X", cpu.PB);
ImGui::NextColumn();
ImGui::Text("PC: 0x%04X", cpu.PC);
ImGui::NextColumn();
ImGui::Text("E: %d", cpu.E);
ImGui::NextColumn();
ImGui::Columns(1);
ImGui::Separator();
// Call Stack
if (ImGui::CollapsingHeader("Call Stack")) {
// For each return address in the call stack:
ImGui::Text("Return Address: 0x%08X", 0xFFFFFF); // Placeholder
}
}
void Emulator::RenderMemoryViewer() {
// Render memory viewer
}
namespace {
bool ShouldDisplay(const InstructionEntry& entry, const char* filter,
bool showAll) {
// Implement logic to determine if the entry should be displayed based on the
// filter and showAll flag
return true;
}
} // namespace
void Emulator::RenderCPUInstructionLog(
const std::vector<InstructionEntry>& instructionLog) {
if (ImGui::CollapsingHeader("CPU Instruction Log")) {
// Filtering options
static char filterBuf[256];
ImGui::InputText("Filter", filterBuf, IM_ARRAYSIZE(filterBuf));
ImGui::SameLine();
if (ImGui::Button("Clear")) { /* Clear filter logic */
}
// Toggle for showing all opcodes
static bool showAllOpcodes = false;
ImGui::Checkbox("Show All Opcodes", &showAllOpcodes);
// Instruction list
ImGui::BeginChild("InstructionList");
for (const auto& entry : instructionLog) {
if (ShouldDisplay(entry, filterBuf, showAllOpcodes)) {
if (ImGui::Selectable(absl::StrFormat("%04X: %02X %s %s", entry.address,
entry.opcode, entry.operands,
entry.instruction)
.c_str())) {
// Logic to handle click (e.g., jump to address, set breakpoint)
}
}
}
ImGui::EndChild();
}
}
} // namespace emu
} // namespace app
} // namespace yaze

View File

@@ -29,11 +29,22 @@ class Emulator : public SharedROM {
// Updates the emulator state (CPU, PPU, APU, etc.)
void UpdateEmulator();
void RenderDebugger();
void RenderCpuState(CPU& cpu);
void RenderMemoryViewer();
void RenderCPUInstructionLog(
const std::vector<InstructionEntry>& instructionLog);
// Member variables to store internal state and resources
SNES snes_;
bool running_ = false;
bool debugger_ = false;
bool loading_ = false;
bool show_ppu_reg_viewer_ = false;
bool integrated_debugger_mode_ = true;
bool separate_debugger_mode_ = false;
};
} // namespace emu

View File

@@ -0,0 +1,149 @@
#ifndef YAZE_TEST_MOCK_MOCK_MEMORY_H
#define YAZE_TEST_MOCK_MOCK_MEMORY_H
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "app/emu/clock.h"
#include "app/emu/cpu.h"
#include "app/emu/memory/memory.h"
using yaze::app::emu::Clock;
using yaze::app::emu::CPU;
using yaze::app::emu::Memory;
class MockClock : public Clock {
public:
MOCK_METHOD(void, UpdateClock, (double delta), (override));
MOCK_METHOD(unsigned long long, GetCycleCount, (), (const, override));
MOCK_METHOD(void, ResetAccumulatedTime, (), (override));
MOCK_METHOD(void, SetFrequency, (float new_frequency), (override));
MOCK_METHOD(float, GetFrequency, (), (const, override));
};
class MockMemory : public Memory {
public:
MOCK_CONST_METHOD1(ReadByte, uint8_t(uint16_t address));
MOCK_CONST_METHOD1(ReadWord, uint16_t(uint16_t address));
MOCK_CONST_METHOD1(ReadWordLong, uint32_t(uint16_t address));
MOCK_METHOD(std::vector<uint8_t>, ReadByteVector,
(uint16_t address, uint16_t length), (const, override));
MOCK_METHOD2(WriteByte, void(uint32_t address, uint8_t value));
MOCK_METHOD2(WriteWord, void(uint32_t address, uint16_t value));
MOCK_METHOD1(PushByte, void(uint8_t value));
MOCK_METHOD0(PopByte, uint8_t());
MOCK_METHOD1(PushWord, void(uint16_t value));
MOCK_METHOD0(PopWord, uint16_t());
MOCK_METHOD1(PushLong, void(uint32_t value));
MOCK_METHOD0(PopLong, uint32_t());
MOCK_CONST_METHOD0(SP, int16_t());
MOCK_METHOD1(SetSP, void(int16_t value));
MOCK_METHOD1(SetMemory, void(const std::vector<uint8_t>& data));
MOCK_METHOD1(LoadData, void(const std::vector<uint8_t>& data));
MOCK_METHOD0(ClearMemory, void());
MOCK_CONST_METHOD1(at, uint8_t(int i));
uint8_t operator[](int i) const override { return at(i); }
void SetMemoryContents(const std::vector<uint8_t>& data) {
memory_.resize(64000);
std::copy(data.begin(), data.end(), memory_.begin());
}
void SetMemoryContents(const std::vector<uint16_t>& data) {
memory_.resize(64000);
int i = 0;
for (const auto& each : data) {
memory_[i] = each & 0xFF;
memory_[i + 1] = (each >> 8) & 0xFF;
i += 2;
}
}
void InsertMemory(const uint64_t address, const std::vector<uint8_t>& data) {
int i = 0;
for (const auto& each : data) {
memory_[address + i] = each;
i++;
}
}
void Init() {
ON_CALL(*this, ReadByte(::testing::_))
.WillByDefault(
[this](uint16_t address) { return memory_.at(address); });
ON_CALL(*this, ReadWord(::testing::_))
.WillByDefault([this](uint16_t address) {
return static_cast<uint16_t>(memory_.at(address)) |
(static_cast<uint16_t>(memory_.at(address + 1)) << 8);
});
ON_CALL(*this, ReadWordLong(::testing::_))
.WillByDefault([this](uint16_t address) {
return static_cast<uint32_t>(memory_.at(address)) |
(static_cast<uint32_t>(memory_.at(address + 1)) << 8) |
(static_cast<uint32_t>(memory_.at(address + 2)) << 16);
});
ON_CALL(*this, ReadByteVector(::testing::_, ::testing::_))
.WillByDefault([this](uint16_t address, uint16_t length) {
std::vector<uint8_t> data;
for (int i = 0; i < length; i++) {
data.push_back(memory_.at(address + i));
}
return data;
});
ON_CALL(*this, WriteByte(::testing::_, ::testing::_))
.WillByDefault([this](uint32_t address, uint8_t value) {
memory_[address] = value;
});
ON_CALL(*this, WriteWord(::testing::_, ::testing::_))
.WillByDefault([this](uint32_t address, uint16_t value) {
memory_[address] = value & 0xFF;
memory_[address + 1] = (value >> 8) & 0xFF;
});
ON_CALL(*this, PushByte(::testing::_)).WillByDefault([this](uint8_t value) {
memory_.at(SP_) = value;
});
ON_CALL(*this, PopByte()).WillByDefault([this]() {
uint8_t value = memory_.at(SP_);
this->SetSP(SP_ + 1);
return value;
});
ON_CALL(*this, PushWord(::testing::_))
.WillByDefault([this](uint16_t value) {
memory_.at(SP_) = value & 0xFF;
memory_.at(SP_ + 1) = (value >> 8) & 0xFF;
});
ON_CALL(*this, PopWord()).WillByDefault([this]() {
uint16_t value = static_cast<uint16_t>(memory_.at(SP_)) |
(static_cast<uint16_t>(memory_.at(SP_ + 1)) << 8);
this->SetSP(SP_ + 2);
return value;
});
ON_CALL(*this, PushLong(::testing::_))
.WillByDefault([this](uint32_t value) {
memory_.at(SP_) = value & 0xFF;
memory_.at(SP_ + 1) = (value >> 8) & 0xFF;
memory_.at(SP_ + 2) = (value >> 16) & 0xFF;
});
ON_CALL(*this, PopLong()).WillByDefault([this]() {
uint32_t value = static_cast<uint32_t>(memory_.at(SP_)) |
(static_cast<uint32_t>(memory_.at(SP_ + 1)) << 8) |
(static_cast<uint32_t>(memory_.at(SP_ + 2)) << 16);
this->SetSP(SP_ + 3);
return value;
});
ON_CALL(*this, ClearMemory()).WillByDefault([this]() {
memory_.resize(64000, 0x00);
});
}
std::vector<uint8_t> memory_;
uint16_t SP_ = 0x01FF;
};
#endif // YAZE_TEST_MOCK_MOCK_MEMORY_H

View File

@@ -63,6 +63,9 @@ class SNES : public DMA {
bool running() const { return running_; }
auto Cpu() -> CPU& { return cpu; }
auto Ppu() -> PPU& { return ppu; }
private:
void WriteToRegister(uint16_t address, uint8_t value) {
memory_.WriteByte(address, value);

View File

@@ -46,7 +46,11 @@ void PPU::UpdateInternalState(int cycles) {
void PPU::RenderScanline() {
for (int y = 0; y < 240; ++y) {
for (int x = 0; x < 256; ++x) {
frame_buffer_[y * 256 + x] = (x + y) % 256; // Simple gradient pattern
// Calculate the color index based on the x and y coordinates
uint8_t color_index = (x + y) % 8;
// Set the pixel in the frame buffer to the calculated color index
frame_buffer_[y * 256 + x] = color_index;
}
}
@@ -267,7 +271,12 @@ void PPU::ApplyEffects() {}
void PPU::ComposeLayers() {}
void PPU::DisplayFrameBuffer() {}
void PPU::DisplayFrameBuffer() {
if (!screen_->IsActive()) {
screen_->Create(256, 240, 24, frame_buffer_);
rom()->RenderBitmap(screen_.get());
}
}
} // namespace emu
} // namespace app

View File

@@ -8,6 +8,7 @@
#include "app/emu/clock.h"
#include "app/emu/memory/memory.h"
#include "app/emu/video/ppu_registers.h"
#include "app/rom.h"
namespace yaze {
namespace app {
@@ -234,7 +235,7 @@ struct BackgroundLayer {
const int kPpuClockSpeed = 5369318; // 5.369318 MHz
class PPU : public Observer {
class PPU : public Observer, public SharedROM {
public:
// Initializes the PPU with the necessary resources and dependencies
PPU(Memory& memory, Clock& clock) : memory_(memory), clock_(clock) {}
@@ -243,6 +244,8 @@ class PPU : public Observer {
void Init() {
clock_.SetFrequency(kPpuClockSpeed);
frame_buffer_.resize(256 * 240, 0);
screen_ = std::make_shared<gfx::Bitmap>(256, 240, 8, 0x100);
screen_->SetActive(false);
}
// Resets the PPU to its initial state
@@ -261,6 +264,8 @@ class PPU : public Observer {
// Returns the pixel data for the current frame
const std::vector<uint8_t>& GetFrameBuffer() const { return frame_buffer_; }
auto GetScreen() const { return screen_; }
private:
// Updates internal state based on PPU register settings
void UpdateModeSettings();
@@ -300,6 +305,7 @@ class PPU : public Observer {
std::vector<SpriteAttributes> sprites_;
std::vector<uint8_t> tile_data_;
std::vector<uint8_t> frame_buffer_;
std::shared_ptr<gfx::Bitmap> screen_;
uint16_t oam_address_;
uint16_t tile_data_size_;