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__
2// #include "app/platform/app_delegate.h"
3#endif
4
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"
17#include "app/emu/snes.h"
21#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
23#endif
24#include "imgui/imgui.h"
25#include "rom/rom.h"
26#include "util/sdl_deleter.h"
27
28ABSL_FLAG(std::string, emu_rom, "", "Path to the ROM file to load.");
29ABSL_FLAG(bool, emu_no_gui, false, "Disable GUI and run in headless mode.");
30ABSL_FLAG(std::string, emu_load_state, "", "Load emulator state from a file.");
31ABSL_FLAG(std::string, emu_dump_state, "", "Dump emulator state to a file.");
32ABSL_FLAG(int, emu_frames, 0, "Number of frames to run the emulator for.");
33ABSL_FLAG(int, emu_max_frames, 180,
34 "Maximum frames to run before auto-exit (0=infinite, default=180/3 "
35 "seconds).");
36ABSL_FLAG(bool, emu_debug_apu, false, "Enable detailed APU/SPC700 logging.");
37ABSL_FLAG(bool, emu_debug_cpu, false, "Enable detailed CPU execution logging.");
38ABSL_FLAG(bool, emu_fix_red_tint, true,
39 "Fix red/blue channel swap (BGR->RGB).");
40
42
43int main(int argc, char** argv) {
44 absl::InitializeSymbolizer(argv[0]);
45
46 absl::FailureSignalHandlerOptions options;
47 options.symbolize_stacktrace = true;
48 options.use_alternate_stack =
49 false; // Disable alternate stack to avoid shutdown conflicts
50 options.alarm_on_failure_secs =
51 false; // Disable alarm to avoid false positives during SDL cleanup
52 options.call_previous_handler = true;
53 absl::InstallFailureSignalHandler(options);
54
55 absl::ParseCommandLine(argc, argv);
56
57 if (absl::GetFlag(FLAGS_emu_no_gui)) {
58 yaze::Rom rom;
59 if (!rom.LoadFromFile(absl::GetFlag(FLAGS_emu_rom)).ok()) {
60 return EXIT_FAILURE;
61 }
62
63 yaze::emu::Snes snes;
64 std::vector<uint8_t> rom_data = rom.vector();
65 snes.Init(rom_data);
66
67 if (!absl::GetFlag(FLAGS_emu_load_state).empty()) {
68 auto status = snes.loadState(absl::GetFlag(FLAGS_emu_load_state));
69 if (!status.ok()) {
70 printf("Failed to load state: %s\n",
71 std::string(status.message()).c_str());
72 return EXIT_FAILURE;
73 }
74 }
75
76 for (int i = 0; i < absl::GetFlag(FLAGS_emu_frames); ++i) {
77 snes.RunFrame();
78 }
79
80 if (!absl::GetFlag(FLAGS_emu_dump_state).empty()) {
81 auto status = snes.saveState(absl::GetFlag(FLAGS_emu_dump_state));
82 if (!status.ok()) {
83 printf("Failed to save state: %s\n",
84 std::string(status.message()).c_str());
85 return EXIT_FAILURE;
86 }
87 }
88
89 return EXIT_SUCCESS;
90 }
91
92 // Initialize window backend (SDL2 or SDL3)
95
97 config.title = "Yaze Emulator";
98 config.width = 512;
99 config.height = 480;
100 config.resizable = true;
101 config.high_dpi =
102 false; // Disabled - causes issues on macOS Retina with SDL_Renderer
103
104 if (!window_backend->Initialize(config).ok()) {
105 printf("Failed to initialize window backend\n");
106 return EXIT_FAILURE;
107 }
108
109 // Create and initialize the renderer (uses factory for SDL2/SDL3 selection)
110 auto renderer = yaze::gfx::RendererFactory::Create();
111 if (!window_backend->InitializeRenderer(renderer.get())) {
112 printf("Failed to initialize renderer\n");
113 window_backend->Shutdown();
114 return EXIT_FAILURE;
115 }
116
117 // Initialize ImGui (with viewports if supported)
118 if (!window_backend->InitializeImGui(renderer.get()).ok()) {
119 printf("Failed to initialize ImGui\n");
120 window_backend->Shutdown();
121 return EXIT_FAILURE;
122 }
123
124 // Initialize audio system using AudioBackend
127
129 audio_config.sample_rate = 48000;
130 audio_config.channels = 2;
131 audio_config.buffer_frames = 1024;
133
134 // Native SNES audio sample rate (SPC700)
135 constexpr int kNativeSampleRate = 32040;
136
137 if (!audio_backend->Initialize(audio_config)) {
138 printf("Failed to initialize audio backend\n");
139 // Continue without audio
140 } else {
141 printf("Audio initialized: %s\n", audio_backend->GetBackendName().c_str());
142 // Enable audio stream resampling (32040 Hz -> 48000 Hz)
143 // CRITICAL: Without this, audio plays at 1.5x speed (48000/32040 = 1.498)
144 if (audio_backend->SupportsAudioStream()) {
145 audio_backend->SetAudioStreamResampling(true, kNativeSampleRate, 2);
146 printf("Audio resampling enabled: %dHz -> %dHz\n", kNativeSampleRate,
147 audio_config.sample_rate);
148 }
149 }
150
151 // Allocate audio buffer using unique_ptr for automatic cleanup
152 std::unique_ptr<int16_t[]> audio_buffer(
153 new int16_t[audio_config.sample_rate / 50 * 4]);
154
155 // Create PPU texture for rendering
156 void* ppu_texture = renderer->CreateTexture(512, 480);
157 if (!ppu_texture) {
158 printf("SDL_CreateTexture failed: %s\n", SDL_GetError());
159 window_backend->Shutdown();
160 return EXIT_FAILURE;
161 }
162
163 yaze::Rom rom_;
164 yaze::emu::Snes snes_;
165 std::vector<uint8_t> rom_data_;
166 yaze::emu::input::InputManager input_manager_;
167
168 // Initialize input manager
169 // TODO: Use factory or detect backend
170 input_manager_.Initialize(
172
173 // Emulator state
174 bool running = true;
175 bool loaded = false;
176 int frame_count = 0;
177 const int max_frames = absl::GetFlag(FLAGS_emu_max_frames);
178 bool fix_red_tint = absl::GetFlag(FLAGS_emu_fix_red_tint);
179
180 // Timing management
181 const uint64_t count_frequency = SDL_GetPerformanceFrequency();
182 uint64_t last_count = SDL_GetPerformanceCounter();
183 double time_adder = 0.0;
184 double wanted_frame_time = 0.0;
185 int wanted_samples = 0;
187
188 // Load ROM from command-line argument or default
189 std::string rom_path = absl::GetFlag(FLAGS_emu_rom);
190 if (rom_path.empty()) {
191 rom_path = "assets/zelda3.sfc"; // Default to zelda3 in assets
192 }
193
194 if (!rom_.LoadFromFile(rom_path).ok()) {
195 printf("Failed to load ROM: %s\n", rom_path.c_str());
196 // Continue running without ROM to show UI
197 }
198
199 if (rom_.is_loaded()) {
200 printf("Loaded ROM: %s (%zu bytes)\n", rom_path.c_str(), rom_.size());
201 rom_data_ = rom_.vector();
202 snes_.Init(rom_data_);
203
204 // Calculate timing based on PAL/NTSC
205 const bool is_pal = snes_.memory().pal_timing();
206 const double refresh_rate = is_pal ? 50.0 : 60.0;
207 wanted_frame_time = 1.0 / refresh_rate;
208 // Use NATIVE sample rate (32040 Hz), not device rate
209 // Audio stream resampling handles conversion to device rate
210 wanted_samples = kNativeSampleRate / static_cast<int>(refresh_rate);
211
212 printf("Emulator initialized: %s mode (%.1f Hz)\n", is_pal ? "PAL" : "NTSC",
213 refresh_rate);
214 loaded = true;
215 }
216
217 while (running) {
218 while (window_backend->PollEvent(event)) {
219 switch (event.type) {
221 if (rom_.LoadFromFile(event.dropped_file).ok() && rom_.is_loaded()) {
222 rom_data_ = rom_.vector();
223 snes_.Init(rom_data_);
224
225 const bool is_pal = snes_.memory().pal_timing();
226 const double refresh_rate = is_pal ? 50.0 : 60.0;
227 wanted_frame_time = 1.0 / refresh_rate;
228 // Use NATIVE sample rate (32040 Hz), not device rate (48000 Hz)
229 // Audio stream resampling handles conversion to device rate
230 wanted_samples = kNativeSampleRate / static_cast<int>(refresh_rate);
231
232 printf("Loaded new ROM via drag-and-drop: %s\n",
233 event.dropped_file.c_str());
234 frame_count = 0; // Reset frame counter
235 loaded = true;
236 }
237 break;
240 running = false;
241 break;
242 default:
243 break;
244 }
245 }
246
247 const uint64_t current_count = SDL_GetPerformanceCounter();
248 const uint64_t delta = current_count - last_count;
249 last_count = current_count;
250 const double seconds =
251 static_cast<double>(delta) / static_cast<double>(count_frequency);
252 time_adder += seconds;
253
254 // Run frame if enough time has elapsed (allow 2ms grace period)
255 while (time_adder >= wanted_frame_time - 0.002) {
256 time_adder -= wanted_frame_time;
257
258 if (loaded) {
259 // Poll input before each frame for proper edge detection
260 input_manager_.Poll(&snes_, 1);
261 snes_.RunFrame();
262 frame_count++;
263
264 // Detect deadlock - CPU stuck in same location
265 static uint16_t last_cpu_pc = 0;
266 static int stuck_count = 0;
267 uint16_t current_cpu_pc = snes_.cpu().PC;
268
269 if (current_cpu_pc == last_cpu_pc && current_cpu_pc >= 0x88B0 &&
270 current_cpu_pc <= 0x88C0) {
271 stuck_count++;
272 if (stuck_count > 180 && frame_count % 60 == 0) {
273 printf(
274 "[WARNING] CPU stuck at $%02X:%04X for %d frames (APU "
275 "deadlock?)\n",
276 snes_.cpu().PB, current_cpu_pc, stuck_count);
277 }
278 } else {
279 stuck_count = 0;
280 }
281 last_cpu_pc = current_cpu_pc;
282
283 // Print status every 60 frames (1 second)
284 if (frame_count % 60 == 0) {
285 printf("[Frame %d] CPU=$%02X:%04X SPC=$%04X APU_cycles=%llu\n",
286 frame_count, snes_.cpu().PB, snes_.cpu().PC,
287 snes_.apu().spc700().PC, snes_.apu().GetCycles());
288 }
289
290 // Auto-exit after max_frames (if set)
291 if (max_frames > 0 && frame_count >= max_frames) {
292 printf("\n[EMULATOR] Reached max frames (%d), shutting down...\n",
293 max_frames);
294 printf("[EMULATOR] Final state: CPU=$%02X:%04X SPC=$%04X\n",
295 snes_.cpu().PB, snes_.cpu().PC, snes_.apu().spc700().PC);
296 running = false;
297 break; // Exit inner loop immediately
298 }
299
300 // Generate audio samples at native rate (32040 Hz)
301 snes_.SetSamples(audio_buffer.get(), wanted_samples);
302
303 if (audio_backend && audio_backend->IsInitialized()) {
304 auto status = audio_backend->GetStatus();
305 // Keep up to 6 frames queued
306 if (status.queued_frames <=
307 static_cast<uint32_t>(wanted_samples * 6)) {
308 // Use QueueSamplesNative for proper resampling (32040 Hz -> device rate)
309 // DO NOT use QueueSamples directly - that causes 1.5x speed bug!
310 if (!audio_backend->QueueSamplesNative(
311 audio_buffer.get(), wanted_samples, 2, kNativeSampleRate)) {
312 // If resampling failed, try to re-enable and retry once
313 if (audio_backend->SupportsAudioStream()) {
314 audio_backend->SetAudioStreamResampling(true, kNativeSampleRate,
315 2);
316 audio_backend->QueueSamplesNative(
317 audio_buffer.get(), wanted_samples, 2, kNativeSampleRate);
318 }
319 }
320 }
321 }
322
323 // Render PPU output to texture
324 void* ppu_pixels = nullptr;
325 int ppu_pitch = 0;
326 if (renderer->LockTexture(ppu_texture, nullptr, &ppu_pixels,
327 &ppu_pitch)) {
328 uint8_t* pixels = static_cast<uint8_t*>(ppu_pixels);
329 snes_.SetPixels(pixels);
330
331 // Fix red tint if enabled (BGR -> RGB swap)
332 // This assumes 32-bit BGRA/RGBA buffer. PPU outputs XRGB.
333 // SDL textures are often ARGB/BGRA.
334 // If we see red tint, blue and red are swapped.
335 if (fix_red_tint) {
336 for (int i = 0; i < 512 * 480; ++i) {
337 uint8_t b = pixels[i * 4 + 0];
338 uint8_t r = pixels[i * 4 + 2];
339 pixels[i * 4 + 0] = r;
340 pixels[i * 4 + 2] = b;
341 }
342 }
343
344 renderer->UnlockTexture(ppu_texture);
345 }
346 }
347 }
348
349 // Present rendered frame
350 window_backend->NewImGuiFrame();
351
352 // Simple debug overlay
353 ImGui::Begin("Emulator Stats");
354 ImGui::Text("Frame: %d", frame_count);
355 ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate);
356 ImGui::Checkbox("Fix Red Tint", &fix_red_tint);
357 if (loaded) {
358 ImGui::Separator();
359 ImGui::Text("CPU PC: $%02X:%04X", snes_.cpu().PB, snes_.cpu().PC);
360 ImGui::Text("SPC PC: $%04X", snes_.apu().spc700().PC);
361 }
362 ImGui::End();
363
364 renderer->Clear();
365
366 // Render texture (scaled to window)
367 // TODO: Use proper aspect ratio handling
368 renderer->RenderCopy(ppu_texture, nullptr, nullptr);
369
370 // Render ImGui draw data and handle viewports
371 window_backend->RenderImGui(renderer.get());
372
373 renderer->Present();
374
375#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
377#endif
378 }
379
380 // === Cleanup SDL resources (in reverse order of initialization) ===
381 printf("\n[EMULATOR] Shutting down...\n");
382
383 // Clean up texture
384 if (ppu_texture) {
385 renderer->DestroyTexture(ppu_texture);
386 ppu_texture = nullptr;
387 }
388
389 // Clean up audio
390 if (audio_backend) {
391 audio_backend->Shutdown();
392 }
393
394 // Clean up renderer and window (via backend)
395 window_backend->ShutdownImGui();
396 renderer->Shutdown();
397 window_backend->Shutdown();
398
399 printf("[EMULATOR] Shutdown complete.\n");
400 return EXIT_SUCCESS;
401}
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:155
const auto & vector() const
Definition rom.h:143
auto size() const
Definition rom.h:138
bool is_loaded() const
Definition rom.h:132
void SetSamples(int16_t *sample_data, int wanted_samples)
Definition snes.cc:856
absl::Status saveState(const std::string &path)
Definition snes.cc:931
void RunFrame()
Definition snes.cc:229
auto apu() -> Apu &
Definition snes.h:86
auto cpu() -> Cpu &
Definition snes.h:84
void Init(const std::vector< uint8_t > &rom_data)
Definition snes.cc:162
absl::Status loadState(const std::string &path)
Definition snes.cc:1010
auto memory() -> MemoryImpl &
Definition snes.h:88
void SetPixels(uint8_t *pixel_data)
Definition snes.cc:860
static std::unique_ptr< IAudioBackend > Create(BackendType type)
void Poll(Snes *snes, int player=1)
bool Initialize(InputBackendFactory::BackendType type=InputBackendFactory::BackendType::SDL2)
static std::unique_ptr< IRenderer > Create(RendererBackendType type=RendererBackendType::kDefault)
static WindowBackendType GetDefaultType()
Get the default backend type for this build.
static std::unique_ptr< IWindowBackend > Create(WindowBackendType type)
Create a window backend of the specified type.
static TestManager & Get()
int main(int argc, char **argv)
Definition emu.cc:43
ABSL_FLAG(std::string, emu_rom, "", "Path to the ROM file to load.")
SDL2/SDL3 compatibility layer.
Window configuration parameters.
Definition iwindow.h:24
Platform-agnostic window event data.
Definition iwindow.h:65
WindowEventType type
Definition iwindow.h:66
Deleter for SDL_Window and SDL_Renderer.
Definition sdl_deleter.h:19