GameBoy Emulator 1
Game Boy emulator core and tooling
Loading...
Searching...
No Matches
ppu.cpp
1#include "../../include/ppu.hpp"
2#include "../../include/mmu.hpp"
3#include "../../include/cartridge.hpp"
4#include <filesystem>
5#include <sstream>
6#include <iomanip>
7#include <fstream>
8#include <iostream>
9#include <algorithm>
10
11namespace {
12constexpr u32 screen_width = 160;
13constexpr u32 screen_height = 144;
14constexpr u32 ui_width = 600;
15constexpr u32 ui_height = 420;
16constexpr u32 window_width = 1600;
17constexpr u32 window_height = 900;
18constexpr size_t opcode_log_size = 28;
19constexpr float status_display_seconds = 2.5f;
20constexpr size_t rom_bytes_per_row = 8;
21constexpr size_t rom_rows_visible = 8;
22
23std::string mbcNameFromType(u8 type) {
24 switch (type) {
25 case 0x00: return "ROM ONLY";
26 case 0x01: return "MBC1";
27 case 0x02: return "MBC1+RAM";
28 case 0x03: return "MBC1+RAM+BATTERY";
29 case 0x05: return "MBC2";
30 case 0x06: return "MBC2+BATTERY";
31 case 0x08: return "ROM+RAM";
32 case 0x09: return "ROM+RAM+BATTERY";
33 case 0x0F: return "MBC3+TIMER+BATTERY";
34 case 0x10: return "MBC3+TIMER+RAM+BATTERY";
35 case 0x11: return "MBC3";
36 case 0x12: return "MBC3+RAM";
37 case 0x13: return "MBC3+RAM+BATTERY";
38 case 0x19: return "MBC5";
39 case 0x1A: return "MBC5+RAM";
40 case 0x1B: return "MBC5+RAM+BATTERY";
41 case 0x1C: return "MBC5+RUMBLE";
42 case 0x1D: return "MBC5+RUMBLE+RAM";
43 case 0x1E: return "MBC5+RUMBLE+RAM+BATTERY";
44 default: return "Unknown";
45 }
46}
47}
48
49PPU::PPU()
50 : dummy_ic(std::make_unique<InterruptController>())
51 , ic(*dummy_ic)
52 , screenSprite(screenTexture)
53{
54 framebuffer.fill(0xFFFFFFFF);
55 lcdc = 0x91;
56 stat = 0x85;
57 bgp = 0xFC;
58 obp0 = 0xFF;
59 obp1 = 0xFF;
60}
61
62PPU::PPU(InterruptController& interrupt_controller)
63 : ic(interrupt_controller)
64 , screenSprite(screenTexture)
65{
66 framebuffer.fill(0xFFFFFFFF);
67 lcdc = 0x91;
68 stat = 0x85;
69 bgp = 0xFC;
70 obp0 = 0xFF;
71 obp1 = 0xFF;
72}
73
74void PPU::step(int cycles) {
75 if (!(lcdc & 0x80)) {
76 ly = 0;
77 window_line_counter = 0;
78 mode_clock = 0;
79 set_mode(Mode::HBlank);
80 return;
81 }
82
83 mode_clock += cycles;
84
85 switch (get_mode()) {
86 case Mode::OAMSearch:
87 if (mode_clock >= 80) {
88 mode_clock -= 80;
89 set_mode(Mode::PixelTransfer);
90 }
91 break;
92 case Mode::PixelTransfer:
93 if (mode_clock >= 172) {
94 mode_clock -= 172;
95 set_mode(Mode::HBlank);
96 render_scanline();
97 render_sprites();
98 }
99 break;
100 case Mode::HBlank:
101 if (mode_clock >= 204) {
102 mode_clock -= 204;
103 ly++;
104 if (ly == 144) {
105 set_mode(Mode::VBlank);
106 frame_ready = true;
107 } else {
108 set_mode(Mode::OAMSearch);
109 }
110 update_stat();
111 }
112 break;
113 case Mode::VBlank:
114 if (mode_clock >= 456) {
115 mode_clock -= 456;
116 ly++;
117 if (ly > 153) {
118 ly = 0;
119 window_line_counter = 0;
120 set_mode(Mode::OAMSearch);
121 }
122 update_stat();
123 }
124 break;
125 }
126}
127
128u8 PPU::read(u16 address) const {
129 if (address >= 0x8000 && address <= 0x9FFF) return vram[address - 0x8000];
130 if (address >= 0xFE00 && address <= 0xFE9F) return oam[address - 0xFE00];
131 switch (address) {
132 case 0xFF40: return lcdc;
133 case 0xFF41: return stat;
134 case 0xFF42: return scy;
135 case 0xFF43: return scx;
136 case 0xFF44: return ly;
137 case 0xFF45: return lyc;
138 case 0xFF46: return dma;
139 case 0xFF47: return bgp;
140 case 0xFF48: return obp0;
141 case 0xFF49: return obp1;
142 case 0xFF4A: return wy;
143 case 0xFF4B: return wx;
144 }
145 return 0xFF;
146}
147
148void PPU::write(u16 address, u8 value) {
149 if (address >= 0x8000 && address <= 0x9FFF) { vram[address - 0x8000] = value; return; }
150 if (address >= 0xFE00 && address <= 0xFE9F) { oam[address - 0xFE00] = value; return; }
151 switch (address) {
152 case 0xFF40: lcdc = value; break;
153 case 0xFF41: stat = (stat & 0x07) | (value & 0xF8); break;
154 case 0xFF42: scy = value; break;
155 case 0xFF43: scx = value; break;
156 case 0xFF44: ly = 0; break;
157 case 0xFF45: lyc = value; break;
158 case 0xFF46: dma = value; break;
159 case 0xFF47: bgp = value; break;
160 case 0xFF48: obp0 = value; break;
161 case 0xFF49: obp1 = value; break;
162 case 0xFF4A: wy = value; break;
163 case 0xFF4B: wx = value; break;
164 }
165}
166
167void PPU::set_mode(Mode mode) {
168 stat = (stat & ~0x03) | static_cast<u8>(mode);
169 if (mode == Mode::VBlank) {
170 ic.request_interrupt(InterruptType::VBlank);
171 }
172 bool interrupt = false;
173 if (mode == Mode::HBlank && (stat & 0x08)) interrupt = true;
174 if (mode == Mode::VBlank && (stat & 0x10)) interrupt = true;
175 if (mode == Mode::OAMSearch && (stat & 0x20)) interrupt = true;
176 if (interrupt) {
177 ic.request_interrupt(InterruptType::LCDStat);
178 }
179}
180
181void PPU::update_stat() {
182 if (ly == lyc) {
183 stat |= 0x04;
184 if (stat & 0x40) {
185 ic.request_interrupt(InterruptType::LCDStat);
186 }
187 } else {
188 stat &= ~0x04;
189 }
190}
191
192void PPU::render_scanline() {
193 bool bg_enabled = (lcdc & 0x01);
194 bool window_enabled = (lcdc & 0x20) && (ly >= wy);
195 if (!bg_enabled && !window_enabled) return;
196 u16 tile_data = (lcdc & 0x10) ? 0x8000 : 0x8800;
197 bool unsigned_addressing = (lcdc & 0x10);
198 u16 bg_map = (lcdc & 0x08) ? 0x9C00 : 0x9800;
199 u16 win_map = (lcdc & 0x40) ? 0x9C00 : 0x9800;
200 bool window_drawn_this_line = false;
201 for (int pixel = 0; pixel < 160; pixel++) {
202 u16 current_map; u8 x_pos, y_pos;
203 bool is_window = window_enabled && (pixel + 7 >= wx);
204 if (is_window) {
205 current_map = win_map; x_pos = pixel + 7 - wx; y_pos = window_line_counter;
206 window_drawn_this_line = true;
207 } else if (bg_enabled) {
208 current_map = bg_map; x_pos = pixel + scx; y_pos = ly + scy;
209 } else continue;
210 u16 tile_row = (u16)(y_pos / 8) * 32;
211 u16 tile_col = (x_pos / 8);
212 u16 tile_address = current_map + tile_row + tile_col;
213 int16_t tile_num = unsigned_addressing ? vram[tile_address - 0x8000] : (int8_t)vram[tile_address - 0x8000];
214 u16 tile_location = tile_data + (unsigned_addressing ? (tile_num * 16) : ((tile_num + 128) * 16));
215 u8 line = (y_pos % 8) * 2;
216 u8 d1 = vram[tile_location + line - 0x8000], d2 = vram[tile_location + line + 1 - 0x8000];
217 int bit = 7 - (x_pos % 8);
218 u8 color_id = (BIT(d2, bit) << 1) | BIT(d1, bit);
219 framebuffer[ly * 160 + pixel] = get_color(color_id, 0xFF47);
220 }
221 if (window_drawn_this_line) window_line_counter++;
222}
223
224void PPU::render_sprites() {
225 if (!(lcdc & 0x02)) return;
226 bool use8x16 = (lcdc & 0x04);
227 for (int i = 0; i < 40; i++) {
228 const int index = i * 4;
229 const int y_pos = static_cast<int>(oam[index]) - 16;
230 const int x_pos = static_cast<int>(oam[index + 1]) - 8;
231 u8 tile_num = oam[index + 2];
232 const u8 flags = oam[index + 3];
233 const int sprite_height = use8x16 ? 16 : 8;
234 if (static_cast<int>(ly) >= y_pos && static_cast<int>(ly) < (y_pos + sprite_height)) {
235 int line = static_cast<int>(ly) - y_pos;
236 if (flags & 0x40) line = (sprite_height - 1) - line;
237 u8 effective_tile = tile_num;
238 int effective_line = line;
239 if (use8x16) {
240 effective_tile &= 0xFE;
241 if (effective_line >= 8) { effective_tile += 1; effective_line -= 8; }
242 }
243 u16 tile_location = 0x8000 + (effective_tile * 16) + (effective_line * 2);
244 u8 d1 = vram[tile_location - 0x8000], d2 = vram[tile_location + 1 - 0x8000];
245 for (int tile_pixel = 7; tile_pixel >= 0; tile_pixel--) {
246 int bit = (flags & 0x20) ? 7 - tile_pixel : tile_pixel;
247 u8 color_id = (BIT(d2, bit) << 1) | BIT(d1, bit);
248 u16 palette = (flags & 0x10) ? 0xFF49 : 0xFF48;
249 const int screen_x = x_pos + (7 - tile_pixel);
250 if (color_id != 0 && screen_x >= 0 && screen_x < 160) {
251 framebuffer[ly * 160 + screen_x] = get_color(color_id, palette);
252 }
253 }
254 }
255 }
256}
257
258u32 PPU::get_color(u8 color_id, u16 palette_address) const {
259 u8 p = read(palette_address);
260 u8 color = (BIT(p, color_id * 2 + 1) << 1) | BIT(p, color_id * 2);
261 switch (color) {
262 case 0: return 0xFFFFFFFF;
263 case 1: return 0xFF969696;
264 case 2: return 0xFF555555;
265 case 3: return 0xFF000000;
266 }
267 return 0xFFFFFFFF;
268}
269
270// ----------------------------------------------------
271// Debug / UI Features
272// ----------------------------------------------------
273
274void PPU::init_window(bool debug, const std::string& rom_title, bool fullscreen) {
275 debugMode = debug;
276 sf::State state = fullscreen ? sf::State::Fullscreen : sf::State::Windowed;
277
278 if (debugMode) {
279 sf::VideoMode mode = fullscreen ? sf::VideoMode({1920, 1080}) : sf::VideoMode({window_width, window_height});
280 window.create(mode, "GB Emulator - Debug View", state);
281
282 sf::View view(sf::FloatRect({0.0f, 0.0f}, {static_cast<float>(window_width), static_cast<float>(window_height)}));
283 window.setView(view);
284
285 screenSprite.setPosition(sf::Vector2f(32.0f, 32.0f));
286 screenSprite.setScale(sf::Vector2f(5.0f, 5.0f));
287 } else {
288 sf::VideoMode mode = fullscreen ? sf::VideoMode({1920, 1080}) : sf::VideoMode({screen_width * 4, screen_height * 4});
289 window.create(mode, "GB Emulator", state);
290
291 sf::View view(sf::FloatRect({0.0f, 0.0f}, {static_cast<float>(screen_width), static_cast<float>(screen_height)}));
292 window.setView(view);
293
294 screenSprite.setPosition(sf::Vector2f(0.0f, 0.0f));
295 screenSprite.setScale(sf::Vector2f(1.0f, 1.0f));
296 }
297 window.setFramerateLimit(60);
298
299 (void)screenTexture.resize({screen_width, screen_height});
300 screenSprite.setTexture(screenTexture);
301 screenSprite.setTextureRect(sf::IntRect({0, 0}, {static_cast<int>(screen_width), static_cast<int>(screen_height)}));
302
303 const std::vector<std::string> fontPaths = {
304 "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
305 "/usr/share/fonts/liberation/LiberationMono-Regular.ttf",
306 "/usr/share/fonts/liberation-fonts/LiberationMono-Regular.ttf",
307 "/usr/share/fonts/Adwaita/AdwaitaMono-Regular.ttf",
308 "DejaVuSansMono.ttf",
309 "LiberationMono-Regular.ttf"
310 };
311
312 for (const auto& path : fontPaths) {
313 fontLoaded = font.openFromFile(path);
314 if (fontLoaded) break;
315 }
316
317 if (!fontLoaded && debugMode) {
318 statusMessage = "Missing monospace font. Debug text won't render.";
319 statusTimer = status_display_seconds;
320 }
321
322 romInfo.mbc_name = mbcNameFromType(romInfo.type);
323
324 std::ifstream bpFile("utility_scripts/breakpoints.txt");
325 if (bpFile) {
326 std::string line;
327 while (std::getline(bpFile, line)) {
328 std::stringstream ss(line);
329 u32 value = 0;
330 if (line.rfind("0x", 0) == 0 || line.rfind("0X", 0) == 0) {
331 ss >> std::hex >> value;
332 } else {
333 ss >> std::hex >> value;
334 }
335 if (ss && value <= 0xFFFF) {
336 breakpoints.push_back(static_cast<u16>(value));
337 }
338 }
339 }
340}
341
342bool PPU::isOpen() const {
343 return window.isOpen();
344}
345
346void PPU::handleEvents(JoypadState& joypad) {
347 while (const auto event = window.pollEvent()) {
348 if (event->is<sf::Event::Closed>()) {
349 window.close();
350 }
351
352 if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>()) {
353 switch (keyPressed->code) {
354 case sf::Keyboard::Key::Right: physicalJoypad.right = true; break;
355 case sf::Keyboard::Key::Left: physicalJoypad.left = true; break;
356 case sf::Keyboard::Key::Up: physicalJoypad.up = true; break;
357 case sf::Keyboard::Key::Down: physicalJoypad.down = true; break;
358 case sf::Keyboard::Key::Z: physicalJoypad.a = true; break;
359 case sf::Keyboard::Key::X: physicalJoypad.b = true; break;
360 case sf::Keyboard::Key::Enter: physicalJoypad.start = true; break;
361 case sf::Keyboard::Key::RShift: physicalJoypad.select = true; break;
362 case sf::Keyboard::Key::Escape: window.close(); break;
363
364 // Debug actions
365 case sf::Keyboard::Key::Space:
366 paused = !paused;
367 statusMessage = paused ? "Paused" : "Running";
368 statusTimer = status_display_seconds;
369 break;
370 case sf::Keyboard::Key::N:
371 if (paused) {
372 stepRequested = true;
373 statusMessage = "Step";
374 statusTimer = status_display_seconds;
375 }
376 break;
377 case sf::Keyboard::Key::T:
378 turbo = !turbo;
379 statusMessage = turbo ? "Turbo Mode 2x" : "Normal Mode 1x";
380 statusTimer = status_display_seconds;
381 break;
382 case sf::Keyboard::Key::R:
383 resetRequested = true;
384 statusMessage = "Reset requested";
385 statusTimer = status_display_seconds;
386 break;
387 case sf::Keyboard::Key::E: {
388 activeSlot = -1; // -1 represents 'e'
389 bool ok = false;
390 if (keyPressed->shift || keyPressed->control) {
391 std::string filepath = "savestates/external.bin";
392 FILE* f = popen("zenity --file-selection --save --confirm-overwrite --file-filter='Save States (*.bin) | *.bin' 2>/dev/null", "r");
393 if (f) {
394 char buf[1024];
395 if (fgets(buf, sizeof(buf), f)) {
396 std::string s(buf);
397 if (!s.empty() && s.back() == '\n') s.pop_back();
398 if (!s.empty()) filepath = s;
399 }
400 pclose(f);
401 }
402 externalStatePath = filepath;
403 ok = saveStatePath(filepath);
404 std::string filename = std::filesystem::path(filepath).filename().string();
405 statusMessage = ok ? "Saved: " + filename : "Save failed";
406 } else {
407 std::string filepath = "savestates/external.bin";
408 FILE* f = popen("zenity --file-selection --file-filter='Save States (*.bin) | *.bin' 2>/dev/null", "r");
409 if (f) {
410 char buf[1024];
411 if (fgets(buf, sizeof(buf), f)) {
412 std::string s(buf);
413 if (!s.empty() && s.back() == '\n') s.pop_back();
414 if (!s.empty()) filepath = s;
415 }
416 pclose(f);
417 }
418 externalStatePath = filepath;
419 ok = loadStatePath(filepath);
420 std::string filename = std::filesystem::path(filepath).filename().string();
421 statusMessage = ok ? "Loaded: " + filename : "Load failed";
422 }
423 statusTimer = status_display_seconds;
424 break;
425 }
426 default:
427 if (keyPressed->code >= sf::Keyboard::Key::Num0 && keyPressed->code <= sf::Keyboard::Key::Num9) {
428 int slot = static_cast<int>(keyPressed->code) - static_cast<int>(sf::Keyboard::Key::Num0);
429 activeSlot = slot;
430 bool ok = false;
431 if (keyPressed->shift || keyPressed->control) {
432 ok = saveStateSlot(slot);
433 statusMessage = ok ? "Saved state to slot " + std::to_string(slot)
434 : "Failed to save slot " + std::to_string(slot);
435 } else {
436 ok = loadStateSlot(slot);
437 statusMessage = ok ? "Loaded state from slot " + std::to_string(slot)
438 : "Failed to load slot " + std::to_string(slot);
439 }
440 statusTimer = status_display_seconds;
441 }
442 break;
443 }
444 }
445
446 if (const auto* keyReleased = event->getIf<sf::Event::KeyReleased>()) {
447 switch (keyReleased->code) {
448 case sf::Keyboard::Key::Right: physicalJoypad.right = false; break;
449 case sf::Keyboard::Key::Left: physicalJoypad.left = false; break;
450 case sf::Keyboard::Key::Up: physicalJoypad.up = false; break;
451 case sf::Keyboard::Key::Down: physicalJoypad.down = false; break;
452 case sf::Keyboard::Key::Z: physicalJoypad.a = false; break;
453 case sf::Keyboard::Key::X: physicalJoypad.b = false; break;
454 case sf::Keyboard::Key::Enter: physicalJoypad.start = false; break;
455 case sf::Keyboard::Key::RShift: physicalJoypad.select = false; break;
456 default: break;
457 }
458 }
459
460 if (const auto* mousePressed = event->getIf<sf::Event::MouseButtonPressed>()) {
461 if (mousePressed->button == sf::Mouse::Button::Left) {
462 sf::Vector2f mPos = window.mapPixelToCoords(mousePressed->position);
463 const float rp2X = 1238.0f;
464 const float rp2W = window_width - rp2X - 32.0f;
465 const float gpX = rp2X + (rp2W - 220.0f) / 2.0f;
466 const float gpY = 32.0f + 60.0f;
467
468 auto rectContains = [](float rx, float ry, float rw, float rh, float px, float py) {
469 return px >= rx && px <= rx + rw && py >= ry && py <= ry + rh;
470 };
471
472 if (rectContains(gpX + 40.0f, gpY + 0.0f, 30.0f, 30.0f, mPos.x, mPos.y)) { clickableJoypad.up = !clickableJoypad.up; }
473 if (rectContains(gpX + 40.0f, gpY + 80.0f, 30.0f, 30.0f, mPos.x, mPos.y)) { clickableJoypad.down = !clickableJoypad.down; }
474 if (rectContains(gpX + 0.0f, gpY + 40.0f, 30.0f, 30.0f, mPos.x, mPos.y)) { clickableJoypad.left = !clickableJoypad.left; }
475 if (rectContains(gpX + 80.0f, gpY + 40.0f, 30.0f, 30.0f, mPos.x, mPos.y)) { clickableJoypad.right = !clickableJoypad.right; }
476 if (rectContains(gpX + 185.0f, gpY + 20.0f, 34.0f, 34.0f, mPos.x, mPos.y)) { clickableJoypad.a = !clickableJoypad.a; }
477 if (rectContains(gpX + 140.0f, gpY + 50.0f, 34.0f, 34.0f, mPos.x, mPos.y)) { clickableJoypad.b = !clickableJoypad.b; }
478 if (rectContains(gpX + 30.0f, gpY + 140.0f, 70.0f, 24.0f, mPos.x, mPos.y)) { clickableJoypad.select = !clickableJoypad.select; }
479 if (rectContains(gpX + 120.0f, gpY + 140.0f, 70.0f, 24.0f, mPos.x, mPos.y)) { clickableJoypad.start = !clickableJoypad.start; }
480 }
481 }
482 }
483
484 joypad.up = physicalJoypad.up || clickableJoypad.up;
485 joypad.down = physicalJoypad.down || clickableJoypad.down;
486 joypad.left = physicalJoypad.left || clickableJoypad.left;
487 joypad.right = physicalJoypad.right || clickableJoypad.right;
488 joypad.a = physicalJoypad.a || clickableJoypad.a;
489 joypad.b = physicalJoypad.b || clickableJoypad.b;
490 joypad.select = physicalJoypad.select || clickableJoypad.select;
491 joypad.start = physicalJoypad.start || clickableJoypad.start;
492 currentJoypadState = joypad;
493}
494
495void PPU::update(const float dtSeconds, const u64 cyclesExecuted) {
496 if (!debugMode) return;
497
498 timeAccumulator += dtSeconds;
499 if (statusTimer > 0.0f) {
500 statusTimer -= dtSeconds;
501 if (statusTimer <= 0.0f) {
502 statusMessage.clear();
503 }
504 }
505
506 if (timeAccumulator >= 0.5f) {
507 timeAccumulator = 0.0f;
508 lastCycles = cyclesExecuted;
509 if (mmu) {
510 lastReads = mmu->get_read_count();
511 lastWrites = mmu->get_write_count();
512 }
513 }
514}
515
516void PPU::render() {
517 if (debugMode) {
518 window.clear(sf::Color(0, 0, 0));
519 screenTexture.update(reinterpret_cast<const uint8_t*>(framebuffer.data()));
520 window.draw(screenSprite);
521 drawPanels();
522 window.display();
523 } else {
524 window.clear();
525 screenTexture.update(reinterpret_cast<const uint8_t*>(framebuffer.data()));
526 window.draw(screenSprite);
527 if (!statusMessage.empty()) {
528 window.setView(window.getDefaultView());
529 drawText(sf::Vector2f(20.0f, 20.0f), statusMessage, 28, sf::Color(255, 50, 50));
530 sf::View view(sf::FloatRect({0.0f, 0.0f}, {static_cast<float>(screen_width), static_cast<float>(screen_height)}));
531 window.setView(view);
532 }
533 window.display();
534 }
535}
536
537void PPU::recordOpcode(u16 pc, u8 opcode) {
538 opcodeLog.push_front({pc, opcode});
539 if (opcodeLog.size() > opcode_log_size) {
540 opcodeLog.pop_back();
541 }
542}
543
544void PPU::checkBreakpoint(const u16 pc) {
545 for (const auto bp : breakpoints) {
546 if (bp == pc) {
547 paused = true;
548 statusMessage = "Breakpoint hit: " + toHex(pc, 4);
549 statusTimer = status_display_seconds;
550 return;
551 }
552 }
553}
554
555void PPU::reset() {
556 vram.fill(0);
557 oam.fill(0);
558 lcdc = 0x91;
559 stat = 0x85;
560 bgp = 0xFC;
561 obp0 = 0xFF;
562 obp1 = 0xFF;
563 scy = 0x00;
564 scx = 0x00;
565 ly = 0x00;
566 lyc = 0x00;
567 wy = 0x00;
568 wx = 0x00;
569 mode_clock = 0;
570 window_line_counter = 0;
571 frame_ready = false;
572 framebuffer.fill(0xFFFFFFFF);
573}
574
575bool PPU::saveStatePath(const std::string& filepath) {
576 if (!cpu || !mmu) return false;
577
578 try {
579 std::filesystem::create_directories(std::filesystem::path(filepath).parent_path());
580 } catch (...) {}
581
582 std::ofstream out(filepath, std::ios::binary);
583 if (!out) {
584 return false;
585 }
586
587 const u16 pc = cpu->get_pc();
588 const u16 sp = cpu->get_sp();
589 const u16 af = cpu->get_af();
590 const u16 bc = cpu->get_bc();
591 const u16 de = cpu->get_de();
592 const u16 hl = cpu->get_hl();
593 const bool ime = cpu->getIME();
594
595 out.write(reinterpret_cast<const char*>(&pc), sizeof(pc));
596 out.write(reinterpret_cast<const char*>(&sp), sizeof(sp));
597 out.write(reinterpret_cast<const char*>(&af), sizeof(af));
598 out.write(reinterpret_cast<const char*>(&bc), sizeof(bc));
599 out.write(reinterpret_cast<const char*>(&de), sizeof(de));
600 out.write(reinterpret_cast<const char*>(&hl), sizeof(hl));
601 out.write(reinterpret_cast<const char*>(&ime), sizeof(ime));
602
603 // Save PPU state
604 out.write(reinterpret_cast<const char*>(vram.data()), vram.size());
605 out.write(reinterpret_cast<const char*>(oam.data()), oam.size());
606 out.write(reinterpret_cast<const char*>(&lcdc), sizeof(lcdc));
607 out.write(reinterpret_cast<const char*>(&stat), sizeof(stat));
608 out.write(reinterpret_cast<const char*>(&scy), sizeof(scy));
609 out.write(reinterpret_cast<const char*>(&scx), sizeof(scx));
610 out.write(reinterpret_cast<const char*>(&ly), sizeof(ly));
611 out.write(reinterpret_cast<const char*>(&lyc), sizeof(lyc));
612 out.write(reinterpret_cast<const char*>(&dma), sizeof(dma));
613 out.write(reinterpret_cast<const char*>(&bgp), sizeof(bgp));
614 out.write(reinterpret_cast<const char*>(&obp0), sizeof(obp0));
615 out.write(reinterpret_cast<const char*>(&obp1), sizeof(obp1));
616 out.write(reinterpret_cast<const char*>(&wy), sizeof(wy));
617 out.write(reinterpret_cast<const char*>(&wx), sizeof(wx));
618 out.write(reinterpret_cast<const char*>(&mode_clock), sizeof(mode_clock));
619 out.write(reinterpret_cast<const char*>(&window_line_counter), sizeof(window_line_counter));
620 out.write(reinterpret_cast<const char*>(&frame_ready), sizeof(frame_ready));
621
622 auto dump = mmu->dump_memory();
623 const u32 size = static_cast<u32>(dump.size());
624 out.write(reinterpret_cast<const char*>(&size), sizeof(size));
625 out.write(reinterpret_cast<const char*>(dump.data()), static_cast<std::streamsize>(dump.size()));
626
627 return true;
628}
629
630bool PPU::saveStateSlot(const int slot) {
631 std::ostringstream path;
632 path << "savestates/slot_" << slot << ".bin";
633 return saveStatePath(path.str());
634}
635
636bool PPU::loadStatePath(const std::string& filepath) {
637 if (!cpu || !mmu) return false;
638
639 std::ifstream in(filepath, std::ios::binary);
640 if (!in) {
641 return false;
642 }
643
644 u16 pc{};
645 u16 sp{};
646 u16 af{};
647 u16 bc{};
648 u16 de{};
649 u16 hl{};
650 bool ime{};
651
652 in.read(reinterpret_cast<char*>(&pc), sizeof(pc));
653 in.read(reinterpret_cast<char*>(&sp), sizeof(sp));
654 in.read(reinterpret_cast<char*>(&af), sizeof(af));
655 in.read(reinterpret_cast<char*>(&bc), sizeof(bc));
656 in.read(reinterpret_cast<char*>(&de), sizeof(de));
657 in.read(reinterpret_cast<char*>(&hl), sizeof(hl));
658 in.read(reinterpret_cast<char*>(&ime), sizeof(ime));
659
660 cpu->set_pc(pc);
661 cpu->set_sp(sp);
662 cpu->reg(ProcessingUnit::Register::A) = static_cast<u8>(af >> 8);
663 cpu->reg(ProcessingUnit::Register::F) = static_cast<u8>(af & 0xFF);
664 cpu->reg(ProcessingUnit::Register::B) = static_cast<u8>(bc >> 8);
665 cpu->reg(ProcessingUnit::Register::C) = static_cast<u8>(bc & 0xFF);
666 cpu->reg(ProcessingUnit::Register::D) = static_cast<u8>(de >> 8);
667 cpu->reg(ProcessingUnit::Register::E) = static_cast<u8>(de & 0xFF);
668 cpu->reg(ProcessingUnit::Register::H) = static_cast<u8>(hl >> 8);
669 cpu->reg(ProcessingUnit::Register::L) = static_cast<u8>(hl & 0xFF);
670 cpu->setIME(ime);
671
672 // Restore PPU state
673 in.read(reinterpret_cast<char*>(vram.data()), vram.size());
674 in.read(reinterpret_cast<char*>(oam.data()), oam.size());
675 in.read(reinterpret_cast<char*>(&lcdc), sizeof(lcdc));
676 in.read(reinterpret_cast<char*>(&stat), sizeof(stat));
677 in.read(reinterpret_cast<char*>(&scy), sizeof(scy));
678 in.read(reinterpret_cast<char*>(&scx), sizeof(scx));
679 in.read(reinterpret_cast<char*>(&ly), sizeof(ly));
680 in.read(reinterpret_cast<char*>(&lyc), sizeof(lyc));
681 in.read(reinterpret_cast<char*>(&dma), sizeof(dma));
682 in.read(reinterpret_cast<char*>(&bgp), sizeof(bgp));
683 in.read(reinterpret_cast<char*>(&obp0), sizeof(obp0));
684 in.read(reinterpret_cast<char*>(&obp1), sizeof(obp1));
685 in.read(reinterpret_cast<char*>(&wy), sizeof(wy));
686 in.read(reinterpret_cast<char*>(&wx), sizeof(wx));
687 in.read(reinterpret_cast<char*>(&mode_clock), sizeof(mode_clock));
688 in.read(reinterpret_cast<char*>(&window_line_counter), sizeof(window_line_counter));
689 in.read(reinterpret_cast<char*>(&frame_ready), sizeof(frame_ready));
690
691 u32 size{};
692 in.read(reinterpret_cast<char*>(&size), sizeof(size));
693 std::vector<u8> dump(size);
694 in.read(reinterpret_cast<char*>(dump.data()), size);
695 mmu->load_memory(dump);
696
697 return true;
698}
699
700bool PPU::loadStateSlot(const int slot) {
701 std::ostringstream path;
702path << "savestates/slot_" << slot << ".bin";
703 return loadStatePath(path.str());
704}
705
706void PPU::drawPanels() {
707 if (!cpu) return;
708
709 const sf::Color panelBgColor(12, 12, 15);
710 const sf::Color panelBorderColor(40, 40, 50);
711 const sf::Color titleColor(0, 255, 128); // Neon Green
712 const sf::Color textColor(200, 200, 200);
713 const sf::Color highlightColor(0, 200, 255); // Neon Blue
714 const sf::Color errorColor(255, 50, 50); // Neon Red
715
716 // Right Panel 1 (CPU, ROM, Memory, Trace)
717 const float rp1X = 32.0f + 800.0f + 32.0f; // 864.0f
718 const float rp1Y = 32.0f;
719 const float rp1W = 350.0f;
720 const float rp1H = window_height - 64.0f; // 836.0f
721
722 sf::RectangleShape rp1Bg(sf::Vector2f(rp1W, rp1H));
723 rp1Bg.setPosition(sf::Vector2f(rp1X, rp1Y));
724 rp1Bg.setFillColor(panelBgColor);
725 rp1Bg.setOutlineColor(panelBorderColor);
726 rp1Bg.setOutlineThickness(1.0f);
727 window.draw(rp1Bg);
728
729 float cursorY = rp1Y + 16.0f;
730 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "CPU Registers", 18, titleColor);
731 cursorY += 28.0f;
732
733 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "AF: " + toHex(cpu->get_af(), 4), 14, highlightColor);
734 cursorY += 20.0f;
735 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "BC: " + toHex(cpu->get_bc(), 4), 14, highlightColor);
736 cursorY += 20.0f;
737 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "DE: " + toHex(cpu->get_de(), 4), 14, highlightColor);
738 cursorY += 20.0f;
739 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "HL: " + toHex(cpu->get_hl(), 4), 14, highlightColor);
740 cursorY += 20.0f;
741 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "SP: " + toHex(cpu->get_sp(), 4) + " PC: " + toHex(cpu->get_pc(), 4), 14, highlightColor);
742 cursorY += 20.0f;
743
744 std::string flags = "Z=" + std::to_string(cpu->get_flag_z()) +
745 " N=" + std::to_string(cpu->get_flag_n()) +
746 " H=" + std::to_string(cpu->get_flag_h()) +
747 " C=" + std::to_string(cpu->get_flag_c());
748 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "Flags: " + flags, 14, textColor);
749 cursorY += 28.0f;
750
751 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "ROM Info", 18, titleColor);
752 cursorY += 28.0f;
753 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "Title: " + romInfo.title, 14, textColor);
754 cursorY += 20.0f;
755 std::string shortMbc = mbcNameFromType(romInfo.type);
756 if (shortMbc.length() > 15) shortMbc = shortMbc.substr(0, 12) + "...";
757 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "Type: 0x" + toHex(romInfo.type, 2) + " (" + shortMbc + ")", 14, textColor);
758 cursorY += 20.0f;
759 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "ROM: 0x" + toHex(romInfo.rom_size, 2) + " RAM: 0x" + toHex(romInfo.ram_size, 2), 14, textColor);
760 cursorY += 28.0f;
761
762 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "Memory/Bus", 18, titleColor);
763 cursorY += 28.0f;
764 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "Rds: " + std::to_string(lastReads) + " Wrs: " + std::to_string(lastWrites), 14, textColor);
765 cursorY += 20.0f;
766 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "Cycles: " + std::to_string(lastCycles), 14, textColor);
767 cursorY += 28.0f;
768
769 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), "Opcode Trace", 18, titleColor);
770 cursorY += 26.0f;
771 int shown = 0;
772 for (const auto& trace : opcodeLog) {
773 if (shown++ >= 14) break;
774 drawText(sf::Vector2f(rp1X + 16.0f, cursorY), toHex(trace.pc, 4) + ": " + toHex(trace.opcode, 2), 14, highlightColor);
775 cursorY += 18.0f;
776 }
777
778 if (!statusMessage.empty()) {
779 drawText(sf::Vector2f(rp1X + 16.0f, rp1Y + rp1H - 24.0f), statusMessage, 14, errorColor);
780 }
781
782 // Right Panel 2 (Gamepad, ROM Bytes)
783 const float rp2X = rp1X + rp1W + 24.0f; // 1238.0f
784 const float rp2Y = 32.0f;
785 const float rp2W = window_width - rp2X - 32.0f; // 330.0f
786 const float rp2H = window_height - 64.0f; // 836.0f
787
788 sf::RectangleShape rp2Bg(sf::Vector2f(rp2W, rp2H));
789 rp2Bg.setPosition(sf::Vector2f(rp2X, rp2Y));
790 rp2Bg.setFillColor(panelBgColor);
791 rp2Bg.setOutlineColor(panelBorderColor);
792 rp2Bg.setOutlineThickness(1.0f);
793 window.draw(rp2Bg);
794
795 drawText(sf::Vector2f(rp2X + 16.0f, rp2Y + 16.0f), "Virtual Gamepad", 18, titleColor);
796
797 const float gpX = rp2X + (rp2W - 220.0f) / 2.0f;
798 const float gpY = rp2Y + 60.0f;
799
800 auto drawButton = [&](float x, float y, float w, float h, const std::string& label, bool isActive) {
801 sf::RectangleShape btn(sf::Vector2f(w, h));
802 btn.setPosition(sf::Vector2f(x, y));
803 if (isActive) {
804 btn.setFillColor(titleColor);
805 btn.setOutlineColor(sf::Color(255, 255, 255));
806 } else {
807 btn.setFillColor(sf::Color(20, 20, 25));
808 btn.setOutlineColor(highlightColor);
809 }
810 btn.setOutlineThickness(1.0f);
811 window.draw(btn);
812
813 float textX = x + (w - label.length() * 7.5f) / 2.0f;
814 float textY = y + (h - 14.0f) / 2.0f - 1.0f;
815 drawText(sf::Vector2f(textX, textY), label, 12, isActive ? sf::Color(0, 0, 0) : textColor);
816 };
817
818 // D-Pad
819 drawButton(gpX + 40.0f, gpY + 0.0f, 30.0f, 30.0f, "U", currentJoypadState.up);
820 drawButton(gpX + 0.0f, gpY + 40.0f, 30.0f, 30.0f, "L", currentJoypadState.left);
821 drawButton(gpX + 80.0f, gpY + 40.0f, 30.0f, 30.0f, "R", currentJoypadState.right);
822 drawButton(gpX + 40.0f, gpY + 80.0f, 30.0f, 30.0f, "D", currentJoypadState.down);
823
824 // Action
825 drawButton(gpX + 140.0f, gpY + 50.0f, 34.0f, 34.0f, "B", currentJoypadState.b);
826 drawButton(gpX + 185.0f, gpY + 20.0f, 34.0f, 34.0f, "A", currentJoypadState.a);
827
828 // Select/Start
829 drawButton(gpX + 30.0f, gpY + 140.0f, 70.0f, 24.0f, "SELECT", currentJoypadState.select);
830 drawButton(gpX + 120.0f, gpY + 140.0f, 70.0f, 24.0f, "START", currentJoypadState.start);
831
832 // ROM Bytes
833 float romY = gpY + 200.0f;
834 drawText(sf::Vector2f(rp2X + 16.0f, romY), "ROM Bytes", 18, titleColor);
835 romY += 28.0f;
836 if (!romInfo.rom_bytes.empty()) {
837 const size_t maxRows = std::min(rom_rows_visible * 3, romInfo.rom_bytes.size() / rom_bytes_per_row + 1);
838 for (size_t row = 0; row < maxRows; ++row) {
839 std::ostringstream line;
840 const size_t offset = row * rom_bytes_per_row;
841 line << toHex(static_cast<u32>(offset), 4) << ": ";
842 for (size_t i = 0; i < rom_bytes_per_row && offset + i < romInfo.rom_bytes.size(); ++i) {
843 line << toHex(romInfo.rom_bytes[offset + i], 2) << " ";
844 }
845 drawText(sf::Vector2f(rp2X + 16.0f, romY), line.str(), 14, highlightColor);
846 romY += 18.0f;
847 }
848 }
849
850 // Emulator Control Panel (Below Game Screen)
851 const float cpX = 32.0f;
852 const float cpY = 32.0f + 720.0f + 24.0f; // 776.0f
853 const float cpW = 800.0f;
854 const float cpH = window_height - cpY - 32.0f; // 92.0f
855
856 sf::RectangleShape cpBg(sf::Vector2f(cpW, cpH));
857 cpBg.setPosition(sf::Vector2f(cpX, cpY));
858 cpBg.setFillColor(panelBgColor);
859 cpBg.setOutlineColor(panelBorderColor);
860 cpBg.setOutlineThickness(1.0f);
861 window.draw(cpBg);
862
863 float col1X = cpX + 16.0f;
864 drawText(sf::Vector2f(col1X, cpY + 12.0f), "Emulator Status", 16, titleColor);
865 std::string speedStr = turbo ? "2x (Turbo)" : "1x (Normal)";
866 drawText(sf::Vector2f(col1X, cpY + 36.0f), "Speed: " + speedStr, 14, textColor);
867 std::string slotStr = (activeSlot == -1) ? "E (External)" : std::to_string(activeSlot);
868 drawText(sf::Vector2f(col1X, cpY + 56.0f), "Slot: [" + slotStr + "]", 14, textColor);
869
870 if (activeSlot == -1 && !externalStatePath.empty()) {
871 std::string filename = std::filesystem::path(externalStatePath).filename().string();
872 if (filename.length() > 20) filename = filename.substr(0, 17) + "...";
873 drawText(sf::Vector2f(col1X + 120.0f, cpY + 56.0f), "File: " + filename, 12, sf::Color(140, 160, 180));
874 }
875
876 float col2X = cpX + 260.0f;
877 drawText(sf::Vector2f(col2X, cpY + 12.0f), "State Hotkeys", 16, titleColor);
878 drawText(sf::Vector2f(col2X, cpY + 36.0f), "0-9 / Shift+0-9: Load/Save Slot", 13, highlightColor);
879 drawText(sf::Vector2f(col2X, cpY + 56.0f), "E / Shift+E: Load/Save External", 13, highlightColor);
880
881 float col3X = cpX + 540.0f;
882 drawText(sf::Vector2f(col3X, cpY + 12.0f), "System Hotkeys", 16, titleColor);
883 drawText(sf::Vector2f(col3X, cpY + 36.0f), "R: Reset Emulator", 13, highlightColor);
884 drawText(sf::Vector2f(col3X, cpY + 56.0f), "Space: Pause | N: Step", 13, highlightColor);
885}
886
887void PPU::drawText(const sf::Vector2f& pos, const std::string& text, const unsigned size, const sf::Color& color) {
888 if (!fontLoaded) {
889 return;
890 }
891 sf::Text txt(font, text, size);
892 txt.setFillColor(color);
893 txt.setPosition(pos);
894 window.draw(txt);
895}
896
897std::string PPU::toHex(const u32 value, const int width) const {
898 std::ostringstream ss;
899 ss << std::hex << std::uppercase << std::setw(width) << std::setfill('0') << value;
900 return ss.str();
901}