yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
emu.cc
Go to the documentation of this file.
1#if __APPLE__
3#endif
4
5#include <SDL.h>
6
7#include <memory>
8#include <string>
9#include <vector>
10
11#include "absl/debugging/failure_signal_handler.h"
12#include "absl/debugging/symbolize.h"
13#include "absl/flags/flag.h"
14#include "absl/flags/parse.h"
15#include "app/emu/snes.h"
16#include "app/rom.h"
19#include "util/sdl_deleter.h"
20
21ABSL_FLAG(std::string, emu_rom, "", "Path to the ROM file to load.");
22ABSL_FLAG(bool, emu_no_gui, false, "Disable GUI and run in headless mode.");
23ABSL_FLAG(std::string, emu_load_state, "", "Load emulator state from a file.");
24ABSL_FLAG(std::string, emu_dump_state, "", "Dump emulator state to a file.");
25ABSL_FLAG(int, emu_frames, 0, "Number of frames to run the emulator for.");
26ABSL_FLAG(int, emu_max_frames, 180, "Maximum frames to run before auto-exit (0=infinite, default=180/3 seconds).");
27ABSL_FLAG(bool, emu_debug_apu, false, "Enable detailed APU/SPC700 logging.");
28ABSL_FLAG(bool, emu_debug_cpu, false, "Enable detailed CPU execution logging.");
29
31
32int main(int argc, char **argv) {
33 absl::InitializeSymbolizer(argv[0]);
34
35 absl::FailureSignalHandlerOptions options;
36 options.symbolize_stacktrace = true;
37 options.use_alternate_stack =
38 false; // Disable alternate stack to avoid shutdown conflicts
39 options.alarm_on_failure_secs =
40 false; // Disable alarm to avoid false positives during SDL cleanup
41 options.call_previous_handler = true;
42 absl::InstallFailureSignalHandler(options);
43
44 absl::ParseCommandLine(argc, argv);
45
46 if (absl::GetFlag(FLAGS_emu_no_gui)) {
47 yaze::Rom rom;
48 if (!rom.LoadFromFile(absl::GetFlag(FLAGS_emu_rom)).ok()) {
49 return EXIT_FAILURE;
50 }
51
52 yaze::emu::Snes snes;
53 std::vector<uint8_t> rom_data = rom.vector();
54 snes.Init(rom_data);
55
56 if (!absl::GetFlag(FLAGS_emu_load_state).empty()) {
57 snes.loadState(absl::GetFlag(FLAGS_emu_load_state));
58 }
59
60 for (int i = 0; i < absl::GetFlag(FLAGS_emu_frames); ++i) {
61 snes.RunFrame();
62 }
63
64 if (!absl::GetFlag(FLAGS_emu_dump_state).empty()) {
65 snes.saveState(absl::GetFlag(FLAGS_emu_dump_state));
66 }
67
68 return EXIT_SUCCESS;
69 }
70
71 // Initialize SDL subsystems
72 SDL_SetMainReady();
73 if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS) != 0) {
74 printf("SDL_Init failed: %s\n", SDL_GetError());
75 return EXIT_FAILURE;
76 }
77
78 // Create window and renderer with RAII smart pointers
79 std::unique_ptr<SDL_Window, SDL_Deleter> window_(
80 SDL_CreateWindow("Yaze Emulator",
81 SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
82 512, 480,
83 SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI),
84 SDL_Deleter());
85 if (!window_) {
86 printf("SDL_CreateWindow failed: %s\n", SDL_GetError());
87 SDL_Quit();
88 return EXIT_FAILURE;
89 }
90
91 // Create and initialize the renderer
92 auto renderer = std::make_unique<yaze::gfx::SDL2Renderer>();
93 if (!renderer->Initialize(window_.get())) {
94 printf("Failed to initialize renderer\n");
95 SDL_Quit();
96 return EXIT_FAILURE;
97 }
98
99 // Initialize audio system
100 constexpr int kAudioFrequency = 48000;
101 SDL_AudioSpec want = {};
102 want.freq = kAudioFrequency;
103 want.format = AUDIO_S16;
104 want.channels = 2;
105 want.samples = 2048;
106 want.callback = nullptr; // Use audio queue
107
108 SDL_AudioSpec have;
109 SDL_AudioDeviceID audio_device = SDL_OpenAudioDevice(nullptr, 0, &want, &have, 0);
110 if (audio_device == 0) {
111 printf("SDL_OpenAudioDevice failed: %s\n", SDL_GetError());
112 SDL_Quit();
113 return EXIT_FAILURE;
114 }
115
116 // Allocate audio buffer using unique_ptr for automatic cleanup
117 std::unique_ptr<int16_t[]> audio_buffer(new int16_t[kAudioFrequency / 50 * 4]);
118 SDL_PauseAudioDevice(audio_device, 0);
119
120 // Create PPU texture for rendering
121 void* ppu_texture = renderer->CreateTexture(512, 480);
122 if (!ppu_texture) {
123 printf("SDL_CreateTexture failed: %s\n", SDL_GetError());
124 SDL_CloseAudioDevice(audio_device);
125 SDL_Quit();
126 return EXIT_FAILURE;
127 }
128
129 yaze::Rom rom_;
130 yaze::emu::Snes snes_;
131 std::vector<uint8_t> rom_data_;
132
133 // Emulator state
134 bool running = true;
135 bool loaded = false;
136 int frame_count = 0;
137 const int max_frames = absl::GetFlag(FLAGS_emu_max_frames);
138
139 // Timing management
140 const uint64_t count_frequency = SDL_GetPerformanceFrequency();
141 uint64_t last_count = SDL_GetPerformanceCounter();
142 double time_adder = 0.0;
143 double wanted_frame_time = 0.0;
144 int wanted_samples = 0;
145 SDL_Event event;
146
147 // Load ROM from command-line argument or default
148 std::string rom_path = absl::GetFlag(FLAGS_emu_rom);
149 if (rom_path.empty()) {
150 rom_path = "assets/zelda3.sfc"; // Default to zelda3 in assets
151 }
152
153 if (!rom_.LoadFromFile(rom_path).ok()) {
154 printf("Failed to load ROM: %s\n", rom_path.c_str());
155 return EXIT_FAILURE;
156 }
157
158 if (rom_.is_loaded()) {
159 printf("Loaded ROM: %s (%zu bytes)\n", rom_path.c_str(), rom_.size());
160 rom_data_ = rom_.vector();
161 snes_.Init(rom_data_);
162
163 // Calculate timing based on PAL/NTSC
164 const bool is_pal = snes_.memory().pal_timing();
165 const double refresh_rate = is_pal ? 50.0 : 60.0;
166 wanted_frame_time = 1.0 / refresh_rate;
167 wanted_samples = kAudioFrequency / static_cast<int>(refresh_rate);
168
169 printf("Emulator initialized: %s mode (%.1f Hz)\n", is_pal ? "PAL" : "NTSC", refresh_rate);
170 loaded = true;
171 }
172
173 while (running) {
174 while (SDL_PollEvent(&event)) {
175 switch (event.type) {
176 case SDL_DROPFILE:
177 if (rom_.LoadFromFile(event.drop.file).ok() && rom_.is_loaded()) {
178 rom_data_ = rom_.vector();
179 snes_.Init(rom_data_);
180
181 const bool is_pal = snes_.memory().pal_timing();
182 const double refresh_rate = is_pal ? 50.0 : 60.0;
183 wanted_frame_time = 1.0 / refresh_rate;
184 wanted_samples = kAudioFrequency / static_cast<int>(refresh_rate);
185
186 printf("Loaded new ROM via drag-and-drop: %s\n", event.drop.file);
187 frame_count = 0; // Reset frame counter
188 loaded = true;
189 }
190 SDL_free(event.drop.file);
191 break;
192 case SDL_KEYDOWN:
193 break;
194 case SDL_KEYUP:
195 break;
196 case SDL_WINDOWEVENT:
197 switch (event.window.event) {
198 case SDL_WINDOWEVENT_CLOSE:
199 running = false;
200 break;
201 case SDL_WINDOWEVENT_SIZE_CHANGED:
202 break;
203 default:
204 break;
205 }
206 break;
207 default:
208 break;
209 }
210 }
211
212 const uint64_t current_count = SDL_GetPerformanceCounter();
213 const uint64_t delta = current_count - last_count;
214 last_count = current_count;
215 const double seconds = static_cast<double>(delta) / static_cast<double>(count_frequency);
216 time_adder += seconds;
217
218 // Run frame if enough time has elapsed (allow 2ms grace period)
219 while (time_adder >= wanted_frame_time - 0.002) {
220 time_adder -= wanted_frame_time;
221
222 if (loaded) {
223 snes_.RunFrame();
224 frame_count++;
225
226 // Detect deadlock - CPU stuck in same location
227 static uint16_t last_cpu_pc = 0;
228 static int stuck_count = 0;
229 uint16_t current_cpu_pc = snes_.cpu().PC;
230
231 if (current_cpu_pc == last_cpu_pc && current_cpu_pc >= 0x88B0 && current_cpu_pc <= 0x88C0) {
232 stuck_count++;
233 if (stuck_count > 180 && frame_count % 60 == 0) {
234 printf("[WARNING] CPU stuck at $%02X:%04X for %d frames (APU deadlock?)\n",
235 snes_.cpu().PB, current_cpu_pc, stuck_count);
236 }
237 } else {
238 stuck_count = 0;
239 }
240 last_cpu_pc = current_cpu_pc;
241
242 // Print status every 60 frames (1 second)
243 if (frame_count % 60 == 0) {
244 printf("[Frame %d] CPU=$%02X:%04X SPC=$%04X APU_cycles=%llu\n",
245 frame_count, snes_.cpu().PB, snes_.cpu().PC,
246 snes_.apu().spc700().PC, snes_.apu().GetCycles());
247 }
248
249 // Auto-exit after max_frames (if set)
250 if (max_frames > 0 && frame_count >= max_frames) {
251 printf("\n[EMULATOR] Reached max frames (%d), shutting down...\n", max_frames);
252 printf("[EMULATOR] Final state: CPU=$%02X:%04X SPC=$%04X\n",
253 snes_.cpu().PB, snes_.cpu().PC, snes_.apu().spc700().PC);
254 running = false;
255 break; // Exit inner loop immediately
256 }
257
258 // Generate audio samples and queue them
259 snes_.SetSamples(audio_buffer.get(), wanted_samples);
260 const uint32_t queued_size = SDL_GetQueuedAudioSize(audio_device);
261 const uint32_t max_queued = wanted_samples * 4 * 6; // Keep up to 6 frames queued
262 if (queued_size <= max_queued) {
263 SDL_QueueAudio(audio_device, audio_buffer.get(), wanted_samples * 4);
264 }
265
266 // Render PPU output to texture
267 void *ppu_pixels = nullptr;
268 int ppu_pitch = 0;
269 if (renderer->LockTexture(ppu_texture, nullptr, &ppu_pixels, &ppu_pitch)) {
270 snes_.SetPixels(static_cast<uint8_t*>(ppu_pixels));
271 renderer->UnlockTexture(ppu_texture);
272 }
273 }
274 }
275
276 // Present rendered frame
277 renderer->Clear();
278 renderer->RenderCopy(ppu_texture, nullptr, nullptr);
279 renderer->Present();
280 }
281
282 // === Cleanup SDL resources (in reverse order of initialization) ===
283 printf("\n[EMULATOR] Shutting down...\n");
284
285 // Clean up texture
286 if (ppu_texture) {
287 renderer->DestroyTexture(ppu_texture);
288 ppu_texture = nullptr;
289 }
290
291 // Clean up audio (audio_buffer cleaned up automatically by unique_ptr)
292 SDL_PauseAudioDevice(audio_device, 1);
293 SDL_ClearQueuedAudio(audio_device);
294 SDL_CloseAudioDevice(audio_device);
295
296 // Clean up renderer and window (done automatically by unique_ptr destructors)
297 renderer->Shutdown();
298 window_.reset();
299
300 // Quit SDL subsystems
301 SDL_Quit();
302
303 printf("[EMULATOR] Shutdown complete.\n");
304 return EXIT_SUCCESS;
305}
The Rom class is used to load, save, and modify Rom data.
Definition rom.h:71
absl::Status LoadFromFile(const std::string &filename, bool z3_load=true)
Definition rom.cc:289
auto vector() const
Definition rom.h:207
auto size() const
Definition rom.h:202
bool is_loaded() const
Definition rom.h:197
void SetSamples(int16_t *sample_data, int wanted_samples)
Definition snes.cc:692
void saveState(const std::string &path)
Definition snes.cc:768
void RunFrame()
Definition snes.cc:98
auto apu() -> Apu &
Definition snes.h:73
auto cpu() -> Cpu &
Definition snes.h:71
void loadState(const std::string &path)
Definition snes.cc:720
void Init(std::vector< uint8_t > &rom_data)
Definition snes.cc:36
auto memory() -> MemoryImpl &
Definition snes.h:74
void SetPixels(uint8_t *pixel_data)
Definition snes.cc:696
ABSL_FLAG(std::string, emu_rom, "", "Path to the ROM file to load.")
Deleter for SDL_Window and SDL_Renderer.
Definition sdl_deleter.h:12