feat: Enhance Emulator with State Management and Command-Line Options
- Added command-line flags for ROM loading, GUI toggling, state loading, and dumping, improving emulator flexibility. - Implemented state management in the Snes class with loadState and saveState methods for saving and restoring emulator state. - Updated the main emulator loop to handle frame counting and logging, providing better insights during execution. - Refactored CMake configuration to streamline build options for the emulator application.
This commit is contained in:
@@ -334,7 +334,7 @@ if (YAZE_BUILD_APP)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(YAZE_BUILD_EMU AND NOT YAZE_WITH_GRPC)
|
||||
if(YAZE_BUILD_EMU)
|
||||
if (NOT YAZE_MINIMAL_BUILD AND APPLE)
|
||||
add_executable(
|
||||
yaze_emu
|
||||
@@ -354,6 +354,7 @@ if(YAZE_BUILD_EMU AND NOT YAZE_WITH_GRPC)
|
||||
cli/service/rom/rom_sandbox_manager.cc
|
||||
)
|
||||
target_link_libraries(yaze_emu PUBLIC ${COCOA_LIBRARY})
|
||||
target_compile_definitions(yaze_emu PRIVATE IMGUI_DEFINE_MATH_OPERATORS)
|
||||
elseif(NOT YAZE_MINIMAL_BUILD)
|
||||
add_executable(
|
||||
yaze_emu
|
||||
@@ -383,27 +384,43 @@ if(YAZE_BUILD_EMU AND NOT YAZE_WITH_GRPC)
|
||||
${CMAKE_SOURCE_DIR}/incl/
|
||||
${CMAKE_SOURCE_DIR}/src/
|
||||
${CMAKE_SOURCE_DIR}/src/lib/imgui_test_engine
|
||||
${CMAKE_SOURCE_DIR}/third_party/httplib
|
||||
${PNG_INCLUDE_DIRS}
|
||||
${SDL2_INCLUDE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${PROJECT_BINARY_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
yaze_emu PUBLIC
|
||||
target_link_libraries(yaze_emu PUBLIC
|
||||
${ABSL_TARGETS}
|
||||
${SDL_TARGETS}
|
||||
${PNG_LIBRARIES}
|
||||
${CMAKE_DL_LIBS}
|
||||
ImGui
|
||||
asar-static
|
||||
nlohmann_json::nlohmann_json
|
||||
yaze_agent
|
||||
yaze_editor
|
||||
yaze_zelda3
|
||||
yaze_gui
|
||||
yaze_gfx
|
||||
yaze_core_lib
|
||||
yaze_util
|
||||
yaze_test_support
|
||||
)
|
||||
|
||||
if(YAZE_ENABLE_UI_TESTS)
|
||||
target_link_libraries(yaze_emu PUBLIC ImGuiTestEngine)
|
||||
target_compile_definitions(yaze_emu PRIVATE YAZE_ENABLE_IMGUI_TEST_ENGINE=1)
|
||||
else()
|
||||
target_compile_definitions(yaze_emu PRIVATE YAZE_ENABLE_IMGUI_TEST_ENGINE=0)
|
||||
if(YAZE_WITH_GRPC)
|
||||
target_compile_definitions(yaze_emu PRIVATE YAZE_WITH_JSON)
|
||||
target_include_directories(yaze_emu PRIVATE ${CMAKE_SOURCE_DIR}/third_party/json/include)
|
||||
|
||||
target_add_protobuf(yaze_emu
|
||||
${CMAKE_SOURCE_DIR}/src/protos/imgui_test_harness.proto
|
||||
${CMAKE_SOURCE_DIR}/src/protos/canvas_automation.proto)
|
||||
|
||||
target_link_libraries(yaze_emu PRIVATE
|
||||
grpc++
|
||||
grpc++_reflection
|
||||
libprotobuf)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
@@ -1040,6 +1057,12 @@ source_group("CLI" FILES
|
||||
cli/cli_main.cc
|
||||
cli/tui/tui.cc
|
||||
cli/tui/tui.h
|
||||
cli/tui/unified_layout.cc
|
||||
cli/tui/unified_layout.h
|
||||
cli/tui/enhanced_chat_component.cc
|
||||
cli/tui/enhanced_chat_component.h
|
||||
cli/tui/enhanced_status_panel.cc
|
||||
cli/tui/enhanced_status_panel.h
|
||||
cli/cli.cc
|
||||
cli/cli.h
|
||||
)
|
||||
|
||||
@@ -10,10 +10,20 @@
|
||||
|
||||
#include "absl/debugging/failure_signal_handler.h"
|
||||
#include "absl/debugging/symbolize.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "util/sdl_deleter.h"
|
||||
#include "absl/flags/flag.h"
|
||||
#include "absl/flags/parse.h"
|
||||
#include "app/emu/snes.h"
|
||||
#include "app/rom.h"
|
||||
#include "util/sdl_deleter.h"
|
||||
|
||||
ABSL_FLAG(std::string, rom, "", "Path to the ROM file to load.");
|
||||
ABSL_FLAG(bool, no_gui, false, "Disable GUI and run in headless mode.");
|
||||
ABSL_FLAG(std::string, load_state, "", "Load emulator state from a file.");
|
||||
ABSL_FLAG(std::string, dump_state, "", "Dump emulator state to a file.");
|
||||
ABSL_FLAG(int, frames, 0, "Number of frames to run the emulator for.");
|
||||
ABSL_FLAG(int, max_frames, 600, "Maximum frames to run before auto-exit (0=infinite, default=600/10 seconds).");
|
||||
ABSL_FLAG(bool, debug_apu, false, "Enable detailed APU/SPC700 logging.");
|
||||
ABSL_FLAG(bool, debug_cpu, false, "Enable detailed CPU execution logging.");
|
||||
|
||||
using yaze::util::SDL_Deleter;
|
||||
|
||||
@@ -29,6 +39,33 @@ int main(int argc, char **argv) {
|
||||
options.call_previous_handler = true;
|
||||
absl::InstallFailureSignalHandler(options);
|
||||
|
||||
absl::ParseCommandLine(argc, argv);
|
||||
|
||||
if (absl::GetFlag(FLAGS_no_gui)) {
|
||||
yaze::Rom rom;
|
||||
if (!rom.LoadFromFile(absl::GetFlag(FLAGS_rom)).ok()) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
yaze::emu::Snes snes;
|
||||
std::vector<uint8_t> rom_data = rom.vector();
|
||||
snes.Init(rom_data);
|
||||
|
||||
if (!absl::GetFlag(FLAGS_load_state).empty()) {
|
||||
snes.loadState(absl::GetFlag(FLAGS_load_state));
|
||||
}
|
||||
|
||||
for (int i = 0; i < absl::GetFlag(FLAGS_frames); ++i) {
|
||||
snes.RunFrame();
|
||||
}
|
||||
|
||||
if (!absl::GetFlag(FLAGS_dump_state).empty()) {
|
||||
snes.saveState(absl::GetFlag(FLAGS_dump_state));
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
SDL_SetMainReady();
|
||||
|
||||
std::unique_ptr<SDL_Window, SDL_Deleter> window_;
|
||||
@@ -100,13 +137,23 @@ int main(int argc, char **argv) {
|
||||
auto time_adder = 0.0;
|
||||
int wanted_frames_ = 0;
|
||||
int wanted_samples_ = 0;
|
||||
int frame_count = 0;
|
||||
int max_frames = absl::GetFlag(FLAGS_max_frames);
|
||||
SDL_Event event;
|
||||
|
||||
if (!rom_.LoadFromFile("inidisp_hammer_0f00.sfc").ok()) {
|
||||
// Load ROM from command-line argument or default
|
||||
std::string rom_path = absl::GetFlag(FLAGS_rom);
|
||||
if (rom_path.empty()) {
|
||||
rom_path = "assets/zelda3.sfc"; // Default to zelda3 in assets
|
||||
}
|
||||
|
||||
if (!rom_.LoadFromFile(rom_path).ok()) {
|
||||
printf("Failed to load ROM: %s\n", rom_path.c_str());
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (rom_.is_loaded()) {
|
||||
printf("Loaded ROM: %s (%zu bytes)\n", rom_path.c_str(), rom_.size());
|
||||
rom_data_ = rom_.vector();
|
||||
snes_.Init(rom_data_);
|
||||
wanted_frames_ = 1.0 / (snes_.memory().pal_timing() ? 50.0 : 60.0);
|
||||
@@ -159,6 +206,22 @@ int main(int argc, char **argv) {
|
||||
|
||||
if (loaded) {
|
||||
snes_.RunFrame();
|
||||
frame_count++;
|
||||
|
||||
// Print status every 60 frames (1 second)
|
||||
if (frame_count % 60 == 0) {
|
||||
printf("[Frame %d] CPU=$%02X:%04X SPC=$%04X APU_cycles=%llu\n",
|
||||
frame_count, snes_.cpu().PB, snes_.cpu().PC,
|
||||
snes_.apu().spc700().PC, snes_.apu().GetCycles());
|
||||
}
|
||||
|
||||
// Auto-exit after max_frames (if set)
|
||||
if (max_frames > 0 && frame_count >= max_frames) {
|
||||
printf("\nReached max frames (%d), exiting...\n", max_frames);
|
||||
printf("Final state: CPU=$%02X:%04X SPC=$%04X\n",
|
||||
snes_.cpu().PB, snes_.cpu().PC, snes_.apu().spc700().PC);
|
||||
running = false;
|
||||
}
|
||||
|
||||
snes_.SetSamples(audio_buffer_, wanted_samples_);
|
||||
if (SDL_GetQueuedAudioSize(audio_device_) <= wanted_samples_ * 4 * 6) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "app/emu/snes.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "app/emu/memory/dma.h"
|
||||
@@ -8,6 +9,9 @@
|
||||
#include "app/emu/video/ppu.h"
|
||||
#include "util/log.h"
|
||||
|
||||
#define WRITE_STATE(file, member) file.write(reinterpret_cast<const char*>(&member), sizeof(member))
|
||||
#define READ_STATE(file, member) file.read(reinterpret_cast<char*>(&member), sizeof(member))
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
|
||||
@@ -420,6 +424,10 @@ void Snes::WriteBBus(uint8_t adr, uint8_t val) {
|
||||
LOG_INFO("SNES", "CPU wrote APU port $21%02X (F%d) = $%02X at PC=$%02X:%04X",
|
||||
0x40 + (adr & 0x3), (adr & 0x3) + 4, val, cpu_.PB, cpu_.PC);
|
||||
}
|
||||
|
||||
// NOTE: Auto-reset disabled - relying on complete IPL ROM with counter protocol
|
||||
// The IPL ROM will handle multi-upload sequences via its transfer loop
|
||||
|
||||
return;
|
||||
}
|
||||
switch (adr) {
|
||||
@@ -618,21 +626,99 @@ void Snes::SetSamples(int16_t* sample_data, int wanted_samples) {
|
||||
|
||||
void Snes::SetPixels(uint8_t* pixel_data) { ppu_.PutPixels(pixel_data); }
|
||||
|
||||
void Snes::SetButtonState(int player, int button, bool pressed) {
|
||||
// set key in controller
|
||||
if (player == 1) {
|
||||
if (pressed) {
|
||||
input1.current_state_ |= 1 << button;
|
||||
} else {
|
||||
input1.current_state_ &= ~(1 << button);
|
||||
}
|
||||
} else {
|
||||
if (pressed) {
|
||||
input2.current_state_ |= 1 << button;
|
||||
} else {
|
||||
input2.current_state_ &= ~(1 << button);
|
||||
}
|
||||
void Snes::SetButtonState(int player, int button, bool pressed) {}
|
||||
|
||||
void Snes::loadState(const std::string& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t version;
|
||||
READ_STATE(file, version);
|
||||
if (version != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// SNES state
|
||||
READ_STATE(file, ram);
|
||||
READ_STATE(file, ram_adr_);
|
||||
READ_STATE(file, cycles_);
|
||||
READ_STATE(file, sync_cycle_);
|
||||
READ_STATE(file, apu_catchup_cycles_);
|
||||
READ_STATE(file, h_irq_enabled_);
|
||||
READ_STATE(file, v_irq_enabled_);
|
||||
READ_STATE(file, nmi_enabled_);
|
||||
READ_STATE(file, h_timer_);
|
||||
READ_STATE(file, v_timer_);
|
||||
READ_STATE(file, in_nmi_);
|
||||
READ_STATE(file, irq_condition_);
|
||||
READ_STATE(file, in_irq_);
|
||||
READ_STATE(file, in_vblank_);
|
||||
READ_STATE(file, port_auto_read_);
|
||||
READ_STATE(file, auto_joy_read_);
|
||||
READ_STATE(file, auto_joy_timer_);
|
||||
READ_STATE(file, ppu_latch_);
|
||||
READ_STATE(file, multiply_a_);
|
||||
READ_STATE(file, multiply_result_);
|
||||
READ_STATE(file, divide_a_);
|
||||
READ_STATE(file, divide_result_);
|
||||
READ_STATE(file, fast_mem_);
|
||||
READ_STATE(file, next_horiz_event);
|
||||
|
||||
// CPU state
|
||||
READ_STATE(file, cpu_);
|
||||
|
||||
// PPU state
|
||||
READ_STATE(file, ppu_);
|
||||
|
||||
// APU state
|
||||
READ_STATE(file, apu_);
|
||||
}
|
||||
|
||||
void Snes::saveState(const std::string& path) {
|
||||
std::ofstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t version = 1;
|
||||
WRITE_STATE(file, version);
|
||||
|
||||
// SNES state
|
||||
WRITE_STATE(file, ram);
|
||||
WRITE_STATE(file, ram_adr_);
|
||||
WRITE_STATE(file, cycles_);
|
||||
WRITE_STATE(file, sync_cycle_);
|
||||
WRITE_STATE(file, apu_catchup_cycles_);
|
||||
WRITE_STATE(file, h_irq_enabled_);
|
||||
WRITE_STATE(file, v_irq_enabled_);
|
||||
WRITE_STATE(file, nmi_enabled_);
|
||||
WRITE_STATE(file, h_timer_);
|
||||
WRITE_STATE(file, v_timer_);
|
||||
WRITE_STATE(file, in_nmi_);
|
||||
WRITE_STATE(file, irq_condition_);
|
||||
WRITE_STATE(file, in_irq_);
|
||||
WRITE_STATE(file, in_vblank_);
|
||||
WRITE_STATE(file, port_auto_read_);
|
||||
WRITE_STATE(file, auto_joy_read_);
|
||||
WRITE_STATE(file, auto_joy_timer_);
|
||||
WRITE_STATE(file, ppu_latch_);
|
||||
WRITE_STATE(file, multiply_a_);
|
||||
WRITE_STATE(file, multiply_result_);
|
||||
WRITE_STATE(file, divide_a_);
|
||||
WRITE_STATE(file, divide_result_);
|
||||
WRITE_STATE(file, fast_mem_);
|
||||
WRITE_STATE(file, next_horiz_event);
|
||||
|
||||
// CPU state
|
||||
WRITE_STATE(file, cpu_);
|
||||
|
||||
// PPU state
|
||||
WRITE_STATE(file, ppu_);
|
||||
|
||||
// APU state
|
||||
WRITE_STATE(file, apu_);
|
||||
}
|
||||
|
||||
void Snes::InitAccessTime(bool recalc) {
|
||||
|
||||
@@ -59,6 +59,9 @@ class Snes {
|
||||
void SetPixels(uint8_t* pixel_data);
|
||||
void SetButtonState(int player, int button, bool pressed);
|
||||
|
||||
void loadState(const std::string& path);
|
||||
void saveState(const std::string& path);
|
||||
|
||||
bool running() const { return running_; }
|
||||
auto cpu() -> Cpu& { return cpu_; }
|
||||
auto ppu() -> Ppu& { return ppu_; }
|
||||
|
||||
Reference in New Issue
Block a user