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