web-a2e 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,2126 @@
1
+ /*
2
+ * emulator.cpp - Core emulator coordinator tying together CPU, memory, video, audio, and peripherals
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ #include "emulator.hpp"
9
+ #include "cards/disk2_card.hpp"
10
+ #include "cards/mockingboard_card.hpp"
11
+ #include "cards/thunderclock_card.hpp"
12
+ #include "cards/mouse_card.hpp"
13
+ #include "cards/smartport/smartport_card.hpp"
14
+ #include "debug/condition_evaluator.hpp"
15
+ #include <algorithm>
16
+ #include <cstring>
17
+
18
+ // Include generated ROM data directly
19
+ #include "roms.cpp" // namespace roms
20
+
21
+ namespace a2e {
22
+
23
+ Emulator::Emulator() {
24
+ mmu_ = std::make_unique<MMU>();
25
+ video_ = std::make_unique<Video>(*mmu_);
26
+ audio_ = std::make_unique<Audio>();
27
+ keyboard_ = std::make_unique<Keyboard>();
28
+
29
+ // Create cards, keep raw pointers, then insert into slots
30
+ auto disk = std::make_unique<Disk2Card>();
31
+ auto mb = std::make_unique<MockingboardCard>();
32
+ disk_ = disk.get();
33
+ mockingboard_ = mb.get();
34
+
35
+ // Create CPU with memory callbacks
36
+ cpu_ = std::make_unique<CPU6502>(
37
+ [this](uint16_t addr) { return cpuRead(addr); },
38
+ [this](uint16_t addr, uint8_t val) { cpuWrite(addr, val); },
39
+ CPUVariant::CMOS_65C02);
40
+
41
+ // Set up keyboard callback to receive translated keys
42
+ keyboard_->setKeyCallback([this](int key) { keyDown(key); });
43
+
44
+ // Set up MMU callbacks
45
+ mmu_->setKeyboardCallback([this]() { return getKeyboardData(); });
46
+ mmu_->setKeyStrobeCallback([this]() { clearKeyboardStrobe(); });
47
+ mmu_->setAnyKeyDownCallback([this]() { return keyDown_; });
48
+ mmu_->setSpeakerCallback([this]() { toggleSpeaker(); });
49
+ mmu_->setButtonCallback([this](int btn) { return getButtonState(btn); });
50
+ mmu_->setCycleCallback([this]() { return cpu_->getTotalCycles(); });
51
+
52
+ // Wire video subsystem callbacks
53
+ video_->setCycleCallback([this]() { return cpu_->getTotalCycles(); });
54
+ mmu_->setVideoSwitchCallback([this]() { video_->onVideoSwitchChanged(); });
55
+
56
+ // Wire watchpoint callbacks (MMU -> Emulator)
57
+ mmu_->setWatchpointCallbacks(
58
+ [this](uint16_t addr, uint8_t val) { onWatchpointRead(addr, val); },
59
+ [this](uint16_t addr, uint8_t val) { onWatchpointWrite(addr, val); });
60
+
61
+ // Set up Mockingboard callbacks
62
+ mockingboard_->setCycleCallback([this]() { return cpu_->getTotalCycles(); });
63
+ mockingboard_->setIRQCallback([this]() { cpu_->irq(); });
64
+
65
+ // Set up level-triggered IRQ polling for VIA/mouse interrupts
66
+ cpu_->setIRQStatusCallback([this]() {
67
+ bool active = mockingboard_ ? mockingboard_->isIRQActive() : false;
68
+ if (mouse_) active = active || mouse_->isIRQActive();
69
+ return active;
70
+ });
71
+
72
+ // Set up disk timing callback - allows disk reads to get accurate cycle count
73
+ // during instruction execution (before disk_->update() is called)
74
+ disk_->setCycleCallback([this]() { return cpu_->getTotalCycles(); });
75
+
76
+ // Insert cards into slots (transfers ownership to MMU)
77
+ mmu_->insertCard(6, std::move(disk));
78
+ mmu_->insertCard(4, std::move(mb));
79
+
80
+ // Audio gets raw pointer
81
+ audio_->setMockingboard(mockingboard_);
82
+ }
83
+
84
+ Emulator::~Emulator() = default;
85
+
86
+ void Emulator::init() {
87
+ // Load system and character ROMs into MMU
88
+ mmu_->loadROM(roms::ROM_SYSTEM, roms::ROM_SYSTEM_SIZE,
89
+ roms::ROM_CHAR, roms::ROM_CHAR_SIZE);
90
+
91
+ // Load Disk II ROM into the card
92
+ disk_->loadROM(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
93
+
94
+ reset();
95
+ }
96
+
97
+ void Emulator::reset() {
98
+ mmu_->reset();
99
+ cpu_->resetCycleCount(); // Clear cycle counter for fresh power-on state
100
+ cpu_->reset();
101
+ audio_->reset();
102
+ if (disk_) disk_->reset();
103
+ keyboard_->reset();
104
+ if (mockingboard_) mockingboard_->reset();
105
+
106
+ // Clear Apple button states
107
+ setButton(0, false);
108
+ setButton(1, false);
109
+
110
+ keyboardLatch_ = 0;
111
+ keyDown_ = false;
112
+ speedMultiplier_ = 1;
113
+ lastFrameCycle_ = 0;
114
+ samplesGenerated_ = 0;
115
+ frameReady_ = false;
116
+ breakpointHit_ = false;
117
+ watchpointHit_ = false;
118
+ skipBreakpointOnce_ = false;
119
+ tempBreakpointActive_ = false;
120
+ tempBreakpoint_ = 0;
121
+ tempBreakpointHit_ = false;
122
+ // Keep beam breakpoints across reset (same as regular breakpoints)
123
+ for (auto& bp : beamBreakpoints_) {
124
+ bp.lastFireFrame = UINT64_MAX;
125
+ bp.lastFireScanline = -1;
126
+ }
127
+ beamBreakHit_ = false;
128
+ beamBreakHitId_ = -1;
129
+ beamBreakHitScanline_ = -1;
130
+ beamBreakHitHPos_ = -1;
131
+ paused_ = false;
132
+
133
+ // Clear BASIC debugging state
134
+ basicProgramRunning_ = false;
135
+ basicBreakpointHit_ = false;
136
+ basicErrorHit_ = false;
137
+ basicErrorLine_ = 0;
138
+ basicErrorCode_ = 0;
139
+ basicStepMode_ = BasicStepMode::None;
140
+ skipBasicBreakpointLine_ = 0xFFFF;
141
+ skipBasicBreakpointStmt_ = -1;
142
+ basicBreakLine_ = 0;
143
+
144
+ video_->beginNewFrame(0);
145
+ }
146
+
147
+ void Emulator::warmReset() {
148
+ // Warm reset - CPU jumps to reset vector, preserves memory and disk state
149
+ // On real hardware, the reset signal resets soft switches via the IOU chip
150
+ // but does not clear RAM, so programs in memory are preserved
151
+ mmu_->warmReset();
152
+ cpu_->reset();
153
+ audio_->reset();
154
+ keyboard_->reset();
155
+
156
+ // Stop disk motor (real Apple IIe reset signal turns off motor)
157
+ if (disk_) disk_->stopMotor();
158
+
159
+ // Clear Apple button states
160
+ setButton(0, false);
161
+ setButton(1, false);
162
+
163
+ // Reset video to clean frame state
164
+ video_->beginNewFrame(cpu_->getTotalCycles());
165
+
166
+ // Clear debugger hit flags
167
+ breakpointHit_ = false;
168
+ watchpointHit_ = false;
169
+ skipBreakpointOnce_ = false;
170
+ tempBreakpointActive_ = false;
171
+ tempBreakpointHit_ = false;
172
+ beamBreakHit_ = false;
173
+
174
+ // Clear BASIC debugger state
175
+ basicBreakpointHit_ = false;
176
+ basicStepMode_ = BasicStepMode::None;
177
+ basicBreakLine_ = 0;
178
+
179
+ paused_ = false;
180
+ frameReady_ = false;
181
+ samplesGenerated_ = 0;
182
+ }
183
+
184
+ void Emulator::setPaused(bool paused) {
185
+ if (!paused && paused_ && breakpointHit_) {
186
+ skipBreakpointOnce_ = true;
187
+ }
188
+ if (!paused && paused_ && basicBreakpointHit_) {
189
+ // Skip this BASIC breakpoint until we move to a different line/statement
190
+ skipBasicBreakpointLine_ = basicBreakLine_;
191
+ // Determine which statement we're on to set the skip correctly
192
+ uint16_t lineStart = findCurrentLineStart(basicBreakLine_);
193
+ uint16_t txtptr = mmu_->readRAM(0xB8, false) | (mmu_->readRAM(0xB9, false) << 8);
194
+ int stmtIdx = (lineStart > 0 && txtptr >= lineStart)
195
+ ? countColonsBetween(lineStart, txtptr) : 0;
196
+ // Check if the breakpoint that hit was a statement-level one
197
+ bool hasStmtBp = false;
198
+ for (const auto& bp : basicBreakpoints_) {
199
+ if (bp.lineNumber == basicBreakLine_ && bp.statementIndex >= 0 && bp.statementIndex == stmtIdx) {
200
+ hasStmtBp = true;
201
+ break;
202
+ }
203
+ }
204
+ skipBasicBreakpointStmt_ = hasStmtBp ? static_cast<int8_t>(stmtIdx) : -1;
205
+ }
206
+ breakpointHit_ = false;
207
+ basicBreakpointHit_ = false;
208
+ watchpointHit_ = false;
209
+ beamBreakHit_ = false;
210
+ beamBreakHitId_ = -1;
211
+ // Reset frame sample counter when unpausing to prevent backlog
212
+ if (!paused && paused_) {
213
+ samplesGenerated_ = 0;
214
+ }
215
+ paused_ = paused;
216
+ }
217
+
218
+ void Emulator::runCycles(int cycles) {
219
+ if (paused_)
220
+ return;
221
+
222
+ uint64_t startCycles = cpu_->getTotalCycles();
223
+ uint64_t targetCycles = startCycles + cycles;
224
+
225
+ while (cpu_->getTotalCycles() < targetCycles) {
226
+ // Check breakpoints (user breakpoints and temp breakpoint)
227
+ {
228
+ uint16_t pc = cpu_->getPC();
229
+
230
+ // Check temp breakpoint (step over / step out)
231
+ if (tempBreakpointActive_ && pc == tempBreakpoint_) {
232
+ tempBreakpointHit_ = true;
233
+ clearTempBreakpoint();
234
+ breakpointHit_ = true;
235
+ breakpointAddress_ = pc;
236
+ paused_ = true;
237
+ return;
238
+ }
239
+
240
+ // Check user breakpoints
241
+ if (!breakpoints_.empty()) {
242
+ if (skipBreakpointOnce_) {
243
+ skipBreakpointOnce_ = false;
244
+ } else if (breakpoints_.count(pc) && !disabledBreakpoints_.count(pc)) {
245
+ breakpointHit_ = true;
246
+ breakpointAddress_ = pc;
247
+ paused_ = true;
248
+ return;
249
+ }
250
+ }
251
+ }
252
+
253
+ // Track BASIC program running state by monitoring ROM entry points.
254
+ // $D912 (RUN command) = program starting, $D43C (RESTART) = returning to ] prompt.
255
+ // This is definitive because it hooks into the ROM's own execution flow.
256
+ {
257
+ uint16_t pc = cpu_->getPC();
258
+ if (pc == 0xD912 && !basicProgramRunning_) {
259
+ basicProgramRunning_ = true;
260
+ basicErrorHit_ = false; // Clear error state on new RUN
261
+ } else if (pc == 0xD43C && basicProgramRunning_) {
262
+ basicProgramRunning_ = false;
263
+ }
264
+
265
+ // ERROR handler entry at $D412 — X register holds error code offset,
266
+ // CURLIN and TXTPTR still point to the offending location.
267
+ // Only capture if ERRFLG ($D8) bit 7 is clear (no ONERR GOTO active).
268
+ if (pc == 0xD412 && basicProgramRunning_) {
269
+ uint8_t errflg = mmu_->readRAM(0xD8, false);
270
+ if (!(errflg & 0x80)) {
271
+ uint8_t curlinHi = mmu_->readRAM(0x76, false);
272
+ if (curlinHi != 0xFF) { // Not in direct mode
273
+ basicErrorHit_ = true;
274
+ basicErrorLine_ = mmu_->readRAM(0x75, false) | (static_cast<uint16_t>(curlinHi) << 8);
275
+ basicErrorTxtptr_ = mmu_->readRAM(0xB8, false) | (mmu_->readRAM(0xB9, false) << 8);
276
+ basicErrorCode_ = cpu_->getX();
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ // Check BASIC stepping and breakpoints
283
+ // CURLIN+1 ($76) = $FF means direct/immediate mode (only high byte matters,
284
+ // matching how the ROM checks it at NEWSTT $D7DC: LDX CURLIN+1 / INX / BEQ)
285
+ // Use readRAM to bypass ALTZP switch - BASIC always uses main RAM for zero page
286
+ uint8_t curlinHi = mmu_->readRAM(0x76, false);
287
+ bool basicDirectMode = (curlinHi == 0xFF);
288
+ uint16_t curlin = mmu_->readRAM(0x75, false) | (static_cast<uint16_t>(curlinHi) << 8);
289
+
290
+ // Clear skip line when returning to direct mode
291
+ if (basicDirectMode && skipBasicBreakpointLine_ != 0xFFFF) {
292
+ skipBasicBreakpointLine_ = 0xFFFF;
293
+ skipBasicBreakpointStmt_ = -1;
294
+ }
295
+
296
+ if (!basicDirectMode) {
297
+ uint16_t pc = cpu_->getPC();
298
+
299
+ // All BASIC stepping and line breakpoints fire at $D820 (EXECUTE_STATEMENT).
300
+ // At this ROM address, both new-line and colon paths have converged:
301
+ // CURLIN is correct and TXTPTR points to the first token of the statement
302
+ // about to execute. This ensures consistent state for statement highlighting.
303
+ if (pc == 0xD820) {
304
+ // Heat map: count every statement execution
305
+ if (basicHeatMapEnabled_) {
306
+ basicHeatMap_[curlin]++;
307
+ }
308
+
309
+ // BASIC line stepping - pause when CURLIN changes
310
+ if (basicStepMode_ == BasicStepMode::Line) {
311
+ if (curlin != basicStepFromLine_) {
312
+ basicStepMode_ = BasicStepMode::None;
313
+ basicBreakpointHit_ = true;
314
+ basicBreakLine_ = curlin;
315
+ paused_ = true;
316
+ return;
317
+ }
318
+ }
319
+
320
+ // BASIC statement stepping
321
+ if (basicStepMode_ == BasicStepMode::Statement) {
322
+ if (basicStepSkipFirst_) {
323
+ basicStepSkipFirst_ = false;
324
+ } else {
325
+ basicStepMode_ = BasicStepMode::None;
326
+ basicBreakpointHit_ = true;
327
+ basicBreakLine_ = curlin;
328
+ paused_ = true;
329
+ return;
330
+ }
331
+ }
332
+
333
+ // Check BASIC line and statement breakpoints
334
+ if (!basicBreakpoints_.empty()) {
335
+ bool matched = false;
336
+ int currentStmtIndex = -2; // Sentinel: not yet computed
337
+ for (const auto& bp : basicBreakpoints_) {
338
+ if (bp.lineNumber != curlin) continue;
339
+ if (bp.statementIndex == -1) {
340
+ // Whole-line breakpoint
341
+ matched = true;
342
+ break;
343
+ }
344
+ // Statement-level: lazily compute current statement index
345
+ if (currentStmtIndex == -2) {
346
+ uint16_t lineStart = findCurrentLineStart(curlin);
347
+ uint16_t txtptr = mmu_->readRAM(0xB8, false) | (mmu_->readRAM(0xB9, false) << 8);
348
+ currentStmtIndex = (lineStart > 0 && txtptr >= lineStart)
349
+ ? countColonsBetween(lineStart, txtptr) : 0;
350
+ }
351
+ if (bp.statementIndex == currentStmtIndex) {
352
+ matched = true;
353
+ break;
354
+ }
355
+ }
356
+
357
+ if (matched) {
358
+ // Skip if we're stepping
359
+ if (basicStepMode_ != BasicStepMode::None) {
360
+ // Don't break while stepping
361
+ }
362
+ // Skip logic: check if this matches the skip (line, stmt) pair
363
+ else if (curlin == skipBasicBreakpointLine_) {
364
+ if (skipBasicBreakpointStmt_ == -1) {
365
+ // Whole-line skip: skip all breakpoints on this line
366
+ } else {
367
+ // Statement-level skip: only skip this specific statement
368
+ if (currentStmtIndex == -2) {
369
+ uint16_t lineStart = findCurrentLineStart(curlin);
370
+ uint16_t txtptr = mmu_->readRAM(0xB8, false) | (mmu_->readRAM(0xB9, false) << 8);
371
+ currentStmtIndex = (lineStart > 0 && txtptr >= lineStart)
372
+ ? countColonsBetween(lineStart, txtptr) : 0;
373
+ }
374
+ if (currentStmtIndex != skipBasicBreakpointStmt_) {
375
+ // Different statement on same line - break!
376
+ basicBreakpointHit_ = true;
377
+ basicBreakLine_ = curlin;
378
+ paused_ = true;
379
+ return;
380
+ }
381
+ }
382
+ } else {
383
+ basicBreakpointHit_ = true;
384
+ basicBreakLine_ = curlin;
385
+ paused_ = true;
386
+ return;
387
+ }
388
+ }
389
+ }
390
+
391
+ // Condition-only rules: evaluate each expression in C++, pause only if one matches
392
+ // Skip while stepping to allow step to complete
393
+ // Skip on the line we just resumed from (same skip logic as line breakpoints)
394
+ if (!basicConditionRules_.empty() && !basicBreakpointHit_ &&
395
+ basicStepMode_ == BasicStepMode::None &&
396
+ curlin != skipBasicBreakpointLine_) {
397
+ for (auto& rule : basicConditionRules_) {
398
+ if (!rule.enabled) continue;
399
+ if (ConditionEvaluator::evaluate(rule.expression.c_str(), *this)) {
400
+ basicBreakpointHit_ = true;
401
+ basicBreakLine_ = curlin;
402
+ basicConditionRuleHitId_ = rule.id;
403
+ paused_ = true;
404
+ return;
405
+ }
406
+ }
407
+ }
408
+ }
409
+
410
+ // Clear skip-line when we move to a different line
411
+ if (skipBasicBreakpointLine_ != 0xFFFF && curlin != skipBasicBreakpointLine_) {
412
+ skipBasicBreakpointLine_ = 0xFFFF;
413
+ skipBasicBreakpointStmt_ = -1;
414
+ }
415
+ }
416
+
417
+ // Record trace before execution
418
+ if (traceEnabled_) recordTrace();
419
+
420
+ // Track cycles before instruction
421
+ uint64_t cyclesBefore = cpu_->getTotalCycles();
422
+
423
+ // Profile: record PC before execution
424
+ uint16_t profilePC = profileEnabled_ ? cpu_->getPC() : 0;
425
+
426
+ // Execute one instruction
427
+ cpu_->executeInstruction();
428
+
429
+ // Update disk controller with actual instruction cycles
430
+ uint64_t cyclesUsed = cpu_->getTotalCycles() - cyclesBefore;
431
+ if (disk_) disk_->update(static_cast<int>(cyclesUsed));
432
+
433
+ // Accumulate cycle profiling
434
+ if (profileEnabled_) {
435
+ profileCycles_[profilePC] += static_cast<uint32_t>(cyclesUsed);
436
+ }
437
+
438
+ // Update Mockingboard timers BEFORE next instruction
439
+ // This ensures timer IRQs fire before the CPU can disable them
440
+ if (mockingboard_) mockingboard_->update(static_cast<int>(cyclesUsed));
441
+
442
+ // Update mouse card for VBL interrupt detection
443
+ if (mouse_) mouse_->update(static_cast<int>(cyclesUsed));
444
+
445
+ // Progressive rendering: render scanlines up to current cycle
446
+ video_->renderUpToCycle(cpu_->getTotalCycles());
447
+
448
+ // Check for frame boundary
449
+ uint64_t currentCycle = cpu_->getTotalCycles();
450
+ if (currentCycle - lastFrameCycle_ >= CYCLES_PER_FRAME) {
451
+ // Advance by exactly CYCLES_PER_FRAME to stay aligned with VBL detection
452
+ // ($C019 uses cycles % CYCLES_PER_FRAME). Using currentCycle would drift
453
+ // by a few cycles each frame, desynchronizing raster effects.
454
+ lastFrameCycle_ += CYCLES_PER_FRAME;
455
+ video_->renderFrame(); // Uses this frame's change log
456
+ video_->beginNewFrame(lastFrameCycle_); // Reset log, aligned to frame boundary
457
+ frameReady_ = true;
458
+ }
459
+
460
+ // Check watchpoint hit (set by MMU callbacks during execution)
461
+ if (watchpointHit_) return;
462
+
463
+ // Check beam breakpoints
464
+ if (!beamBreakpoints_.empty()) {
465
+ uint64_t fc = cpu_->getTotalCycles() - lastFrameCycle_;
466
+ if (fc >= CYCLES_PER_FRAME) fc %= CYCLES_PER_FRAME;
467
+ int16_t sl = static_cast<int16_t>(fc / 65);
468
+ int16_t hp = static_cast<int16_t>(fc % 65);
469
+ for (auto& bp : beamBreakpoints_) {
470
+ if (!bp.enabled) continue;
471
+ bool scanOk = (bp.scanline < 0) || (sl == bp.scanline);
472
+ bool hPosOk = (bp.hPos < 0) || (hp >= bp.hPos);
473
+ bool valid = (bp.scanline >= 0 || bp.hPos >= 0);
474
+ if (!scanOk || !hPosOk || !valid) continue;
475
+
476
+ // For wildcard-scanline breakpoints (HBLANK, Column), fire once per scanline.
477
+ // For specific-scanline breakpoints (VBL, Scanline, ScanCol), fire once per frame.
478
+ bool alreadyFired;
479
+ if (bp.scanline < 0) {
480
+ alreadyFired = (lastFrameCycle_ == bp.lastFireFrame && sl == bp.lastFireScanline);
481
+ } else {
482
+ alreadyFired = (lastFrameCycle_ == bp.lastFireFrame);
483
+ }
484
+ if (!alreadyFired) {
485
+ beamBreakHit_ = true;
486
+ beamBreakHitId_ = bp.id;
487
+ beamBreakHitScanline_ = sl;
488
+ beamBreakHitHPos_ = hp;
489
+ bp.lastFireFrame = lastFrameCycle_;
490
+ bp.lastFireScanline = sl;
491
+ paused_ = true;
492
+ return;
493
+ }
494
+ }
495
+ }
496
+ }
497
+ }
498
+
499
+ void Emulator::setSpeedMultiplier(int multiplier) {
500
+ if (multiplier < 1) multiplier = 1;
501
+ if (multiplier > 8) multiplier = 8;
502
+ speedMultiplier_ = multiplier;
503
+ }
504
+
505
+ int Emulator::generateStereoAudioSamples(float *buffer, int sampleCount) {
506
+ // Calculate cycles needed for this audio buffer, scaled by speed multiplier
507
+ int cyclesToRun = static_cast<int>(sampleCount * CYCLES_PER_SAMPLE * speedMultiplier_);
508
+
509
+ // Run emulation for the required cycles
510
+ runCycles(cyclesToRun);
511
+
512
+ // Track samples for frame synchronization
513
+ samplesGenerated_ += sampleCount;
514
+
515
+ // Generate stereo audio samples (interleaved L/R)
516
+ return audio_->generateStereoSamples(buffer, sampleCount, cpu_->getTotalCycles());
517
+ }
518
+
519
+ int Emulator::consumeFrameSamples() {
520
+ // Returns number of complete frames worth of samples generated
521
+ // 48000 Hz / 60 Hz = 800 samples per frame
522
+ int frames = samplesGenerated_ / SAMPLES_PER_FRAME;
523
+ samplesGenerated_ %= SAMPLES_PER_FRAME;
524
+ return frames;
525
+ }
526
+
527
+ const uint8_t *Emulator::getFramebuffer() const {
528
+ return video_->getFramebuffer();
529
+ }
530
+
531
+ int Emulator::handleRawKeyDown(int browserKeycode, bool shift, bool ctrl,
532
+ bool alt, bool meta, bool capsLock) {
533
+ int result = keyboard_->handleKeyDown(browserKeycode, shift, ctrl, alt, meta, capsLock);
534
+
535
+ // Update button state from modifier keys
536
+ setButton(0, keyboard_->isOpenApplePressed()); // Open Apple
537
+ setButton(1, keyboard_->isClosedApplePressed()); // Closed Apple
538
+
539
+ return result;
540
+ }
541
+
542
+ void Emulator::handleRawKeyUp(int browserKeycode, bool shift, bool ctrl,
543
+ bool alt, bool meta) {
544
+ keyboard_->handleKeyUp(browserKeycode, shift, ctrl, alt, meta);
545
+
546
+ // Update button state from modifier keys
547
+ setButton(0, keyboard_->isOpenApplePressed()); // Open Apple
548
+ setButton(1, keyboard_->isClosedApplePressed()); // Closed Apple
549
+ }
550
+
551
+ void Emulator::keyDown(int keycode) {
552
+ // Direct Apple II keycode input (used for paste functionality)
553
+ // Sets the keyboard latch with high bit set
554
+ keyboardLatch_ = (keycode & 0x7F) | 0x80;
555
+ keyDown_ = true;
556
+ }
557
+
558
+ void Emulator::keyUp(int keycode) {
559
+ (void)keycode;
560
+ keyDown_ = false;
561
+ }
562
+
563
+ void Emulator::setButton(int button, bool pressed) {
564
+ if (button >= 0 && button < 3) {
565
+ buttonState_[button] = pressed;
566
+ }
567
+ }
568
+
569
+ void Emulator::setPaddleValue(int paddle, int value) {
570
+ mmu_->setPaddleValue(paddle, static_cast<uint8_t>(value & 0xFF));
571
+ }
572
+
573
+ int Emulator::getPaddleValue(int paddle) const {
574
+ return mmu_->getPaddleValue(paddle);
575
+ }
576
+
577
+ uint8_t Emulator::getButtonState(int button) {
578
+ if (button >= 0 && button < 3 && buttonState_[button]) {
579
+ return 0x80; // Bit 7 set = button pressed
580
+ }
581
+ return 0x00; // Button not pressed
582
+ }
583
+
584
+ bool Emulator::insertDisk(int drive, const uint8_t *data, size_t size,
585
+ const char *filename) {
586
+ if (!disk_) return false;
587
+ return disk_->insertDisk(drive, data, size, filename ? filename : "");
588
+ }
589
+
590
+ bool Emulator::insertBlankDisk(int drive) {
591
+ if (!disk_) return false;
592
+ return disk_->insertBlankDisk(drive);
593
+ }
594
+
595
+ void Emulator::ejectDisk(int drive) {
596
+ if (disk_) disk_->ejectDisk(drive);
597
+ }
598
+
599
+ const uint8_t *Emulator::getDiskData(int drive, size_t *size) const {
600
+ if (!disk_) return nullptr;
601
+ return disk_->getDiskData(drive, size);
602
+ }
603
+
604
+ const uint8_t *Emulator::exportDiskData(int drive, size_t *size) {
605
+ if (!disk_) return nullptr;
606
+ return disk_->exportDiskData(drive, size);
607
+ }
608
+
609
+ const char *Emulator::getDiskFilename(int drive) const {
610
+ if (!disk_) return nullptr;
611
+ const auto *image = disk_->getDiskImage(drive);
612
+ if (!image) {
613
+ return nullptr;
614
+ }
615
+ return image->getFilename().c_str();
616
+ }
617
+
618
+ // ============================================================================
619
+ // Beam Position
620
+ // ============================================================================
621
+
622
+ int Emulator::getFrameCycle() const {
623
+ return static_cast<int>(cpu_->getTotalCycles() % CYCLES_PER_FRAME);
624
+ }
625
+
626
+ int Emulator::getBeamScanline() const {
627
+ return getFrameCycle() / CYCLES_PER_SCANLINE;
628
+ }
629
+
630
+ int Emulator::getBeamHPos() const {
631
+ return getFrameCycle() % CYCLES_PER_SCANLINE;
632
+ }
633
+
634
+ int Emulator::getBeamColumn() const {
635
+ int hPos = getBeamHPos();
636
+ return hPos >= 25 ? hPos - 25 : -1;
637
+ }
638
+
639
+ bool Emulator::isInVBL() const {
640
+ return getBeamScanline() >= 192;
641
+ }
642
+
643
+ bool Emulator::isInHBLANK() const {
644
+ return getBeamHPos() < 25;
645
+ }
646
+
647
+ // ============================================================================
648
+ // Step Over / Step Out
649
+ // ============================================================================
650
+
651
+ uint16_t Emulator::stepOver() {
652
+ clearTempBreakpoint();
653
+ uint16_t pc = cpu_->getPC();
654
+ uint8_t opcode = mmu_->peek(pc);
655
+
656
+ if (opcode == 0x20) {
657
+ // JSR - set temp breakpoint at instruction after JSR (PC + 3)
658
+ uint16_t returnAddr = (pc + 3) & 0xFFFF;
659
+ tempBreakpoint_ = returnAddr;
660
+ tempBreakpointActive_ = true;
661
+ setPaused(false);
662
+ return returnAddr;
663
+ } else if (opcode == 0x00) {
664
+ // BRK - treat like JSR but with PC+2 as return address
665
+ uint16_t returnAddr = (pc + 2) & 0xFFFF;
666
+ tempBreakpoint_ = returnAddr;
667
+ tempBreakpointActive_ = true;
668
+ setPaused(false);
669
+ return returnAddr;
670
+ } else {
671
+ // Not a JSR/BRK, just single step
672
+ stepInstruction();
673
+ return 0;
674
+ }
675
+ }
676
+
677
+ uint16_t Emulator::stepOut() {
678
+ clearTempBreakpoint();
679
+ uint8_t sp = cpu_->getSP();
680
+ uint8_t pcl = mmu_->peek(0x0100 + ((sp + 1) & 0xFF));
681
+ uint8_t pch = mmu_->peek(0x0100 + ((sp + 2) & 0xFF));
682
+ // RTS adds 1 to the address
683
+ uint16_t returnAddr = ((pch << 8) | pcl) + 1;
684
+
685
+ if (returnAddr > 0 && returnAddr <= 0xFFFF) {
686
+ returnAddr &= 0xFFFF;
687
+ tempBreakpoint_ = returnAddr;
688
+ tempBreakpointActive_ = true;
689
+ setPaused(false);
690
+ return returnAddr;
691
+ } else {
692
+ // Invalid return address, just step
693
+ stepInstruction();
694
+ return 0;
695
+ }
696
+ }
697
+
698
+ void Emulator::clearTempBreakpoint() {
699
+ if (tempBreakpointActive_) {
700
+ tempBreakpointActive_ = false;
701
+ tempBreakpoint_ = 0;
702
+ tempBreakpointHit_ = false;
703
+ }
704
+ }
705
+
706
+ // ============================================================================
707
+ // Breakpoints
708
+ // ============================================================================
709
+
710
+ void Emulator::addBreakpoint(uint16_t address) { breakpoints_.insert(address); }
711
+
712
+ void Emulator::removeBreakpoint(uint16_t address) {
713
+ breakpoints_.erase(address);
714
+ disabledBreakpoints_.erase(address);
715
+ }
716
+
717
+ void Emulator::enableBreakpoint(uint16_t address, bool enabled) {
718
+ if (enabled) {
719
+ disabledBreakpoints_.erase(address);
720
+ } else {
721
+ if (breakpoints_.count(address)) {
722
+ disabledBreakpoints_.insert(address);
723
+ }
724
+ }
725
+ }
726
+
727
+ // ============================================================================
728
+ // BASIC Breakpoints
729
+ // ============================================================================
730
+
731
+ void Emulator::addBasicBreakpoint(uint16_t lineNumber, int statementIndex) {
732
+ basicBreakpoints_.insert({lineNumber, static_cast<int8_t>(statementIndex)});
733
+ }
734
+
735
+ void Emulator::removeBasicBreakpoint(uint16_t lineNumber, int statementIndex) {
736
+ basicBreakpoints_.erase({lineNumber, static_cast<int8_t>(statementIndex)});
737
+ }
738
+
739
+ void Emulator::clearBasicBreakpoints() {
740
+ basicBreakpoints_.clear();
741
+ basicBreakpointHit_ = false;
742
+ }
743
+
744
+ void Emulator::clearBasicBreakpointHit() {
745
+ basicBreakpointHit_ = false;
746
+ basicConditionRuleHitId_ = -1;
747
+ // Don't clear skipBasicBreakpointLine_ here - let it be cleared naturally
748
+ // when CURLIN changes. This allows Run to work from a breakpoint by:
749
+ // 1. setPaused(false) sets skip line
750
+ // 2. clearBasicBreakpointHit() clears step mode but keeps skip
751
+ // 3. Program continues, types RUN, skip cleared when line changes
752
+ basicStepMode_ = BasicStepMode::None;
753
+ }
754
+
755
+ void Emulator::addBasicConditionRule(int id, const char* expression) {
756
+ // Remove existing rule with same id
757
+ removeBasicConditionRule(id);
758
+ basicConditionRules_.push_back({id, std::string(expression), true});
759
+ }
760
+
761
+ void Emulator::removeBasicConditionRule(int id) {
762
+ basicConditionRules_.erase(
763
+ std::remove_if(basicConditionRules_.begin(), basicConditionRules_.end(),
764
+ [id](const BasicConditionRule& r) { return r.id == id; }),
765
+ basicConditionRules_.end());
766
+ }
767
+
768
+ void Emulator::clearBasicConditionRules() {
769
+ basicConditionRules_.clear();
770
+ basicConditionRuleHitId_ = -1;
771
+ }
772
+
773
+ void Emulator::stepBasicLine() {
774
+ // Get current BASIC line (use readRAM to bypass ALTZP)
775
+ uint16_t curlin = mmu_->readRAM(0x75, false) | (mmu_->readRAM(0x76, false) << 8);
776
+
777
+ // Set up line stepping mode - will pause when CURLIN changes
778
+ basicStepFromLine_ = curlin;
779
+ basicStepLineStart_ = 0; // Not used for line stepping, but reset for cleanliness
780
+ basicStepNextColon_ = 0; // Not used for line stepping
781
+ basicStepMode_ = BasicStepMode::Line;
782
+
783
+ // Clear any hit flags, reset sample counter to prevent backlog, and resume
784
+ basicBreakpointHit_ = false;
785
+ samplesGenerated_ = 0;
786
+ paused_ = false;
787
+ basicBreakLine_ = 0;
788
+ }
789
+
790
+ void Emulator::stepBasicStatement() {
791
+ // Step to the next BASIC statement by waiting for PC to hit $D820
792
+ // (JSR EXECUTE_STATEMENT in the ROM). Both new-line and colon paths
793
+ // converge there with correct CURLIN and TXTPTR.
794
+ uint16_t pc = cpu_->getPC();
795
+ basicStepSkipFirst_ = (pc == 0xD820);
796
+ basicStepMode_ = BasicStepMode::Statement;
797
+
798
+ // Clear any hit flags, reset sample counter to prevent backlog, and resume
799
+ basicBreakpointHit_ = false;
800
+ samplesGenerated_ = 0;
801
+ paused_ = false;
802
+ basicBreakLine_ = 0;
803
+ }
804
+
805
+ uint16_t Emulator::getBasicTxtptr() const {
806
+ // Read TXTPTR respecting current ALTZP state - BASIC writes to whichever
807
+ // bank is active, so we need to read from the same bank
808
+ return mmu_->peek(0xB8) | (mmu_->peek(0xB9) << 8);
809
+ }
810
+
811
+ int Emulator::getBasicStatementIndex() {
812
+ // Use readRAM to bypass ALTZP - BASIC always uses main RAM for zero page
813
+ uint16_t curlin = mmu_->readRAM(0x75, false) | (mmu_->readRAM(0x76, false) << 8);
814
+ uint16_t txtptr = mmu_->readRAM(0xB8, false) | (mmu_->readRAM(0xB9, false) << 8);
815
+
816
+ // Find line start for the current line
817
+ uint16_t lineStart = findCurrentLineStart(curlin);
818
+
819
+ // If TXTPTR hasn't entered the current line's text area yet, we're at statement 0
820
+ if (lineStart == 0 || txtptr < lineStart) {
821
+ return 0;
822
+ }
823
+
824
+ return countColonsBetween(lineStart, txtptr);
825
+ }
826
+
827
+ int Emulator::countColonsBetween(uint16_t lineStart, uint16_t txtptr) {
828
+ // If TXTPTR is at or before line start, we're at statement 0
829
+ if (lineStart == 0 || txtptr <= lineStart) return 0;
830
+
831
+ // Count colons from line start to TXTPTR, respecting strings
832
+ // Use readRAM to ensure we read from main RAM where BASIC program is stored
833
+ int colonCount = 0;
834
+ bool inQuote = false;
835
+ bool inRem = false;
836
+
837
+ for (uint16_t a = lineStart; a < txtptr; a++) {
838
+ uint8_t byte = mmu_->readRAM(a, false);
839
+
840
+ if (byte == 0) break; // End of line
841
+
842
+ if (inRem) continue; // Skip everything after REM
843
+
844
+ if (byte == 0x22) { // Quote
845
+ inQuote = !inQuote;
846
+ continue;
847
+ }
848
+
849
+ if (inQuote) continue; // Skip string contents
850
+
851
+ if (byte == 0xB2) { // REM token
852
+ inRem = true;
853
+ continue;
854
+ }
855
+
856
+ if (byte == 0x3A) { // Colon
857
+ colonCount++;
858
+ }
859
+ }
860
+
861
+ return colonCount;
862
+ }
863
+
864
+ uint16_t Emulator::findNextColonAfter(uint16_t lineStart, uint16_t afterPos) {
865
+ // Find the address of the next colon after afterPos within the line
866
+ // Returns 0 if no colon found (i.e., afterPos is in the last statement)
867
+ if (lineStart == 0) return 0;
868
+
869
+ // Start searching from afterPos (or lineStart if afterPos is before it)
870
+ uint16_t searchStart = (afterPos >= lineStart) ? afterPos : lineStart;
871
+ bool inQuote = false;
872
+ bool inRem = false;
873
+
874
+ // First, establish quote/REM state at searchStart by scanning from lineStart
875
+ for (uint16_t a = lineStart; a < searchStart; a++) {
876
+ uint8_t byte = mmu_->readRAM(a, false);
877
+ if (byte == 0) return 0; // Already past end of line
878
+ if (inRem) continue;
879
+ if (byte == 0x22) inQuote = !inQuote;
880
+ if (!inQuote && byte == 0xB2) inRem = true;
881
+ }
882
+
883
+ // Now search for the next colon
884
+ for (uint16_t a = searchStart; a < searchStart + 256; a++) { // Limit search
885
+ uint8_t byte = mmu_->readRAM(a, false);
886
+
887
+ if (byte == 0) return 0; // End of line, no more colons
888
+
889
+ if (inRem) continue;
890
+
891
+ if (byte == 0x22) {
892
+ inQuote = !inQuote;
893
+ continue;
894
+ }
895
+
896
+ if (inQuote) continue;
897
+
898
+ if (byte == 0xB2) {
899
+ inRem = true;
900
+ continue;
901
+ }
902
+
903
+ if (byte == 0x3A) { // Found a colon!
904
+ return a;
905
+ }
906
+ }
907
+
908
+ return 0; // No colon found
909
+ }
910
+
911
+ uint16_t Emulator::findCurrentLineStart(uint16_t lineNumber) {
912
+ // Get TXTTAB (start of BASIC program)
913
+ // Use readRAM to bypass ALTZP - BASIC always uses main RAM for zero page
914
+ uint16_t txttab = mmu_->readRAM(0x67, false) | (mmu_->readRAM(0x68, false) << 8);
915
+
916
+ if (lineNumber == 0xFFFF) return 0; // Not running
917
+
918
+ // Find the specified line in the program
919
+ uint16_t addr = txttab;
920
+
921
+ while (addr < 0xC000) { // Reasonable upper bound
922
+ uint16_t nextPtr = mmu_->readRAM(addr, false) | (mmu_->readRAM(addr + 1, false) << 8);
923
+ if (nextPtr == 0) break; // End of program
924
+
925
+ uint16_t lineNum = mmu_->readRAM(addr + 2, false) | (mmu_->readRAM(addr + 3, false) << 8);
926
+ if (lineNum == lineNumber) {
927
+ return addr + 4; // Start of tokenized text (after nextPtr and lineNum)
928
+ }
929
+ addr = nextPtr;
930
+ }
931
+
932
+ return 0; // Line not found
933
+ }
934
+
935
+ // ============================================================================
936
+ // BASIC Heat Map
937
+ // ============================================================================
938
+
939
+ int Emulator::getBasicHeatMapData(uint16_t* lines, uint32_t* counts, int maxEntries) const {
940
+ int i = 0;
941
+ for (const auto& [line, count] : basicHeatMap_) {
942
+ if (i >= maxEntries) break;
943
+ lines[i] = line;
944
+ counts[i] = count;
945
+ i++;
946
+ }
947
+ return i;
948
+ }
949
+
950
+ // ============================================================================
951
+ // Watchpoints
952
+ // ============================================================================
953
+
954
+ void Emulator::addWatchpoint(uint16_t startAddr, uint16_t endAddr, WatchpointType type) {
955
+ watchpoints_.push_back({startAddr, endAddr, type, true});
956
+ watchpointsActive_ = true;
957
+ mmu_->setWatchpointsActive(true);
958
+ }
959
+
960
+ void Emulator::removeWatchpoint(uint16_t startAddr) {
961
+ watchpoints_.erase(
962
+ std::remove_if(watchpoints_.begin(), watchpoints_.end(),
963
+ [startAddr](const Watchpoint& wp) { return wp.startAddr == startAddr; }),
964
+ watchpoints_.end());
965
+ watchpointsActive_ = !watchpoints_.empty();
966
+ mmu_->setWatchpointsActive(watchpointsActive_);
967
+ }
968
+
969
+ void Emulator::clearWatchpoints() {
970
+ watchpoints_.clear();
971
+ watchpointsActive_ = false;
972
+ mmu_->setWatchpointsActive(false);
973
+ }
974
+
975
+ void Emulator::onWatchpointRead(uint16_t address, uint8_t value) {
976
+ if (!watchpointsActive_ || watchpointHit_) return;
977
+ for (const auto& wp : watchpoints_) {
978
+ if (!wp.enabled) continue;
979
+ if ((wp.type & WP_READ) && address >= wp.startAddr && address <= wp.endAddr) {
980
+ watchpointHit_ = true;
981
+ watchpointAddress_ = address;
982
+ watchpointValue_ = value;
983
+ watchpointIsWrite_ = false;
984
+ paused_ = true;
985
+ return;
986
+ }
987
+ }
988
+ }
989
+
990
+ void Emulator::onWatchpointWrite(uint16_t address, uint8_t value) {
991
+ if (!watchpointsActive_ || watchpointHit_) return;
992
+ for (const auto& wp : watchpoints_) {
993
+ if (!wp.enabled) continue;
994
+ if ((wp.type & WP_WRITE) && address >= wp.startAddr && address <= wp.endAddr) {
995
+ watchpointHit_ = true;
996
+ watchpointAddress_ = address;
997
+ watchpointValue_ = value;
998
+ watchpointIsWrite_ = true;
999
+ paused_ = true;
1000
+ return;
1001
+ }
1002
+ }
1003
+ }
1004
+
1005
+ // ============================================================================
1006
+ // Beam Breakpoints
1007
+ // ============================================================================
1008
+
1009
+ int32_t Emulator::addBeamBreakpoint(int16_t scanline, int16_t hPos) {
1010
+ if (beamBreakpoints_.size() >= MAX_BEAM_BREAKPOINTS) return -1;
1011
+ int32_t id = beamBreakNextId_++;
1012
+ beamBreakpoints_.push_back({scanline, hPos, true, id, UINT64_MAX, -1});
1013
+ return id;
1014
+ }
1015
+
1016
+ void Emulator::removeBeamBreakpoint(int32_t id) {
1017
+ beamBreakpoints_.erase(
1018
+ std::remove_if(beamBreakpoints_.begin(), beamBreakpoints_.end(),
1019
+ [id](const BeamBreakpoint& bp) { return bp.id == id; }),
1020
+ beamBreakpoints_.end());
1021
+ }
1022
+
1023
+ void Emulator::enableBeamBreakpoint(int32_t id, bool enabled) {
1024
+ for (auto& bp : beamBreakpoints_) {
1025
+ if (bp.id == id) {
1026
+ bp.enabled = enabled;
1027
+ return;
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ void Emulator::clearAllBeamBreakpoints() {
1033
+ beamBreakpoints_.clear();
1034
+ beamBreakNextId_ = 1;
1035
+ beamBreakHit_ = false;
1036
+ beamBreakHitId_ = -1;
1037
+ beamBreakHitScanline_ = -1;
1038
+ beamBreakHitHPos_ = -1;
1039
+ }
1040
+
1041
+ // ============================================================================
1042
+ // Trace Log
1043
+ // ============================================================================
1044
+
1045
+ void Emulator::recordTrace() {
1046
+ if (traceBuffer_.empty()) {
1047
+ traceBuffer_.resize(10000);
1048
+ }
1049
+
1050
+ auto& entry = traceBuffer_[traceHead_];
1051
+ entry.pc = cpu_->getPC();
1052
+ entry.opcode = mmu_->peek(entry.pc);
1053
+ entry.a = cpu_->getA();
1054
+ entry.x = cpu_->getX();
1055
+ entry.y = cpu_->getY();
1056
+ entry.sp = cpu_->getSP();
1057
+ entry.p = cpu_->getP();
1058
+
1059
+ // Read operands
1060
+ entry.instrLen = 1;
1061
+ entry.operand1 = 0;
1062
+ entry.operand2 = 0;
1063
+
1064
+ // Determine instruction length from opcode
1065
+ static const uint8_t instrLengths[256] = {
1066
+ 1,2,1,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,2,2,2,2,1,3,1,1,3,3,3,3,
1067
+ 3,2,1,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,2,2,2,2,1,3,1,1,3,3,3,3,
1068
+ 1,2,1,1,1,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,1,2,2,2,1,3,1,1,1,3,3,3,
1069
+ 1,2,1,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,2,2,2,2,1,3,1,1,3,3,3,3,
1070
+ 2,2,1,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,2,2,2,2,1,3,1,1,3,3,3,3,
1071
+ 2,2,2,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,2,2,2,2,1,3,1,1,3,3,3,3,
1072
+ 2,2,1,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,1,2,2,2,1,3,1,1,1,3,3,3,
1073
+ 2,2,1,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,1,2,2,2,1,3,1,1,1,3,3,3,
1074
+ };
1075
+
1076
+ entry.instrLen = instrLengths[entry.opcode];
1077
+ if (entry.instrLen >= 2) entry.operand1 = mmu_->peek(entry.pc + 1);
1078
+ if (entry.instrLen >= 3) entry.operand2 = mmu_->peek(entry.pc + 2);
1079
+
1080
+ entry.cycle = static_cast<uint32_t>(cpu_->getTotalCycles());
1081
+ entry.padding = 0;
1082
+
1083
+ traceHead_ = (traceHead_ + 1) % traceBuffer_.size();
1084
+ if (traceCount_ < traceBuffer_.size()) traceCount_++;
1085
+ }
1086
+
1087
+ void Emulator::stepInstruction() {
1088
+ breakpointHit_ = false;
1089
+ watchpointHit_ = false;
1090
+ beamBreakHit_ = false;
1091
+ beamBreakHitId_ = -1;
1092
+
1093
+ // Record trace before execution
1094
+ if (traceEnabled_) recordTrace();
1095
+
1096
+ // Track cycles before instruction
1097
+ uint64_t cyclesBefore = cpu_->getTotalCycles();
1098
+
1099
+ // Profile: record PC before execution
1100
+ uint16_t profilePC = profileEnabled_ ? cpu_->getPC() : 0;
1101
+
1102
+ cpu_->executeInstruction();
1103
+
1104
+ // Update disk controller with actual instruction cycles
1105
+ uint64_t cyclesUsed = cpu_->getTotalCycles() - cyclesBefore;
1106
+ if (disk_) disk_->update(static_cast<int>(cyclesUsed));
1107
+
1108
+ // Accumulate cycle profiling
1109
+ if (profileEnabled_) {
1110
+ profileCycles_[profilePC] += static_cast<uint32_t>(cyclesUsed);
1111
+ }
1112
+
1113
+ // Update Mockingboard timers
1114
+ if (mockingboard_) mockingboard_->update(static_cast<int>(cyclesUsed));
1115
+
1116
+ // Update mouse card
1117
+ if (mouse_) mouse_->update(static_cast<int>(cyclesUsed));
1118
+
1119
+ // Progressive rendering: render scanlines up to current cycle
1120
+ video_->renderUpToCycle(cpu_->getTotalCycles());
1121
+
1122
+ // Check for frame boundary
1123
+ uint64_t currentCycle = cpu_->getTotalCycles();
1124
+ if (currentCycle - lastFrameCycle_ >= CYCLES_PER_FRAME) {
1125
+ lastFrameCycle_ += CYCLES_PER_FRAME;
1126
+ video_->renderFrame();
1127
+ video_->beginNewFrame(lastFrameCycle_);
1128
+ frameReady_ = true;
1129
+ }
1130
+ }
1131
+
1132
+ uint8_t Emulator::readMemory(uint16_t address) const {
1133
+ return const_cast<MMU *>(mmu_.get())->read(address);
1134
+ }
1135
+
1136
+ uint8_t Emulator::peekMemory(uint16_t address) const {
1137
+ return mmu_->peek(address);
1138
+ }
1139
+
1140
+ void Emulator::writeMemory(uint16_t address, uint8_t value) {
1141
+ mmu_->write(address, value);
1142
+ }
1143
+
1144
+ const char *Emulator::disassembleAt(uint16_t address) {
1145
+ disasmBuffer_ = cpu_->disassembleAt(address);
1146
+ return disasmBuffer_.c_str();
1147
+ }
1148
+
1149
+ uint64_t Emulator::getSoftSwitchState() const {
1150
+ const auto &sw = mmu_->getSoftSwitches();
1151
+ uint64_t state = 0;
1152
+
1153
+ // Pack soft switch state into a 64-bit value
1154
+ // Display switches (bits 0-5)
1155
+ if (sw.text)
1156
+ state |= (1ULL << 0);
1157
+ if (sw.mixed)
1158
+ state |= (1ULL << 1);
1159
+ if (sw.page2)
1160
+ state |= (1ULL << 2);
1161
+ if (sw.hires)
1162
+ state |= (1ULL << 3);
1163
+ if (sw.col80)
1164
+ state |= (1ULL << 4);
1165
+ if (sw.altCharSet)
1166
+ state |= (1ULL << 5);
1167
+
1168
+ // Memory switches (bits 6-12)
1169
+ if (sw.store80)
1170
+ state |= (1ULL << 6);
1171
+ if (sw.ramrd)
1172
+ state |= (1ULL << 7);
1173
+ if (sw.ramwrt)
1174
+ state |= (1ULL << 8);
1175
+ if (sw.intcxrom)
1176
+ state |= (1ULL << 9);
1177
+ if (sw.altzp)
1178
+ state |= (1ULL << 10);
1179
+ if (sw.slotc3rom)
1180
+ state |= (1ULL << 11);
1181
+ if (sw.intc8rom)
1182
+ state |= (1ULL << 12);
1183
+
1184
+ // Language card (bits 13-16)
1185
+ if (sw.lcram)
1186
+ state |= (1ULL << 13);
1187
+ if (sw.lcram2)
1188
+ state |= (1ULL << 14);
1189
+ if (sw.lcwrite)
1190
+ state |= (1ULL << 15);
1191
+ if (sw.lcprewrite)
1192
+ state |= (1ULL << 16);
1193
+
1194
+ // Annunciators (bits 17-20)
1195
+ if (sw.an0)
1196
+ state |= (1ULL << 17);
1197
+ if (sw.an1)
1198
+ state |= (1ULL << 18);
1199
+ if (sw.an2)
1200
+ state |= (1ULL << 19);
1201
+ if (sw.an3)
1202
+ state |= (1ULL << 20);
1203
+
1204
+ // I/O state (bits 21-23)
1205
+ if (sw.vblBar)
1206
+ state |= (1ULL << 21);
1207
+ if (sw.cassetteOut)
1208
+ state |= (1ULL << 22);
1209
+ if (sw.cassetteIn)
1210
+ state |= (1ULL << 23);
1211
+
1212
+ // Buttons (bits 24-26)
1213
+ if (buttonState_[0])
1214
+ state |= (1ULL << 24);
1215
+ if (buttonState_[1])
1216
+ state |= (1ULL << 25);
1217
+ if (buttonState_[2])
1218
+ state |= (1ULL << 26);
1219
+
1220
+ // Keyboard (bit 27)
1221
+ if (keyboardLatch_ & 0x80)
1222
+ state |= (1ULL << 27);
1223
+
1224
+ // DHIRES (bit 28) - computed from AN3 off + 80COL + HIRES
1225
+ bool dhires = !sw.an3 && sw.col80 && sw.hires;
1226
+ if (dhires)
1227
+ state |= (1ULL << 28);
1228
+
1229
+ // IOUDIS (bit 29)
1230
+ if (sw.ioudis)
1231
+ state |= (1ULL << 29);
1232
+
1233
+ return state;
1234
+ }
1235
+
1236
+ uint8_t Emulator::cpuRead(uint16_t address) { return mmu_->read(address); }
1237
+
1238
+ void Emulator::cpuWrite(uint16_t address, uint8_t value) {
1239
+ mmu_->write(address, value);
1240
+ }
1241
+
1242
+ uint8_t Emulator::getKeyboardData() { return keyboardLatch_; }
1243
+
1244
+ void Emulator::clearKeyboardStrobe() {
1245
+ keyboardLatch_ &= 0x7F; // Clear high bit
1246
+ }
1247
+
1248
+ void Emulator::toggleSpeaker() {
1249
+ audio_->toggleSpeaker(cpu_->getTotalCycles());
1250
+ }
1251
+
1252
+ // ============================================================================
1253
+ // Mouse Input
1254
+ // ============================================================================
1255
+
1256
+ void Emulator::mouseMove(int dx, int dy) {
1257
+ if (mouse_) {
1258
+ mouse_->addDelta(dx, dy);
1259
+ }
1260
+ }
1261
+
1262
+ void Emulator::mouseButton(bool pressed) {
1263
+ if (mouse_) {
1264
+ mouse_->setMouseButton(pressed);
1265
+ }
1266
+ }
1267
+
1268
+ // ============================================================================
1269
+ // Slot Management
1270
+ // ============================================================================
1271
+
1272
+ const char* Emulator::getSlotCardName(uint8_t slot) const {
1273
+ if (slot < 1 || slot > 7) {
1274
+ return "invalid";
1275
+ }
1276
+
1277
+ // Slot 3 is built-in 80-column
1278
+ if (slot == 3) {
1279
+ return "80col";
1280
+ }
1281
+
1282
+ // Check the slot array for all cards
1283
+ ExpansionCard* card = mmu_->getCard(slot);
1284
+ if (!card) {
1285
+ return "empty";
1286
+ }
1287
+
1288
+ // Identify card type by name
1289
+ const char* name = card->getName();
1290
+ if (strcmp(name, "Disk II") == 0) {
1291
+ return "disk2";
1292
+ }
1293
+ if (strcmp(name, "Mockingboard") == 0) {
1294
+ return "mockingboard";
1295
+ }
1296
+ if (strcmp(name, "Thunderclock") == 0) {
1297
+ return "thunderclock";
1298
+ }
1299
+ if (strcmp(name, "Mouse") == 0) {
1300
+ return "mouse";
1301
+ }
1302
+ if (strcmp(name, "SmartPort") == 0) {
1303
+ return "smartport";
1304
+ }
1305
+
1306
+ return "empty";
1307
+ }
1308
+
1309
+ bool Emulator::setSlotCard(uint8_t slot, const char* cardId) {
1310
+ if (slot < 1 || slot > 7) {
1311
+ return false;
1312
+ }
1313
+
1314
+ // Slot 3 is built-in 80-column and cannot be changed
1315
+ if (slot == 3) {
1316
+ return false;
1317
+ }
1318
+
1319
+ // Before any slot change, clean up existing special card pointers.
1320
+ // insertCard() destroys the old card, so dangling pointers must be cleared.
1321
+ ExpansionCard* existing = mmu_->getCard(slot);
1322
+ if (existing) {
1323
+ const char* existingName = existing->getName();
1324
+ if (strcmp(existingName, "Mockingboard") == 0 && slot == 4) {
1325
+ // Move Mockingboard to storage so it can be restored later
1326
+ if (!mbStorage_) {
1327
+ mbStorage_ = mmu_->removeCard(4);
1328
+ mockingboard_ = nullptr;
1329
+ audio_->setMockingboard(nullptr);
1330
+ }
1331
+ } else if (strcmp(existingName, "Disk II") == 0 && slot == 6) {
1332
+ if (!diskStorage_) {
1333
+ diskStorage_ = mmu_->removeCard(6);
1334
+ disk_ = nullptr;
1335
+ }
1336
+ } else if (strcmp(existingName, "Mouse") == 0) {
1337
+ mouse_ = nullptr;
1338
+ mmu_->removeCard(slot);
1339
+ } else if (strcmp(existingName, "SmartPort") == 0) {
1340
+ smartport_ = nullptr;
1341
+ mmu_->removeCard(slot);
1342
+ } else {
1343
+ mmu_->removeCard(slot);
1344
+ }
1345
+ }
1346
+
1347
+ // Handle empty slot - cleanup above already removed the card
1348
+ if (strcmp(cardId, "empty") == 0) {
1349
+ return true;
1350
+ }
1351
+
1352
+ // Handle Disk II card
1353
+ if (strcmp(cardId, "disk2") == 0) {
1354
+ if (slot != 6) {
1355
+ return false;
1356
+ }
1357
+ // Re-insert from storage
1358
+ if (diskStorage_) {
1359
+ disk_ = static_cast<Disk2Card*>(diskStorage_.get());
1360
+ mmu_->insertCard(6, std::move(diskStorage_));
1361
+ }
1362
+ return true;
1363
+ }
1364
+
1365
+ // Handle Mockingboard card
1366
+ if (strcmp(cardId, "mockingboard") == 0) {
1367
+ if (slot != 4) {
1368
+ return false;
1369
+ }
1370
+ // Re-insert from storage
1371
+ if (mbStorage_) {
1372
+ mockingboard_ = static_cast<MockingboardCard*>(mbStorage_.get());
1373
+ mmu_->insertCard(4, std::move(mbStorage_));
1374
+ audio_->setMockingboard(mockingboard_);
1375
+ }
1376
+ return true;
1377
+ }
1378
+
1379
+ // Handle Thunderclock card
1380
+ if (strcmp(cardId, "thunderclock") == 0) {
1381
+ auto card = std::make_unique<ThunderclockCard>();
1382
+ mmu_->insertCard(slot, std::move(card));
1383
+ return true;
1384
+ }
1385
+
1386
+ // Handle Mouse card
1387
+ if (strcmp(cardId, "mouse") == 0) {
1388
+ auto card = std::make_unique<MouseCard>();
1389
+ card->setSlotNumber(slot);
1390
+ card->setCycleCallback([this]() { return cpu_->getTotalCycles(); });
1391
+ card->setIRQCallback([this]() { cpu_->irq(); });
1392
+ mouse_ = card.get();
1393
+ mmu_->insertCard(slot, std::move(card));
1394
+ return true;
1395
+ }
1396
+
1397
+ // Handle SmartPort card
1398
+ if (strcmp(cardId, "smartport") == 0) {
1399
+ auto card = std::make_unique<SmartPortCard>();
1400
+ card->setSlotNumber(slot);
1401
+ card->setMemReadCallback([this](uint16_t addr) { return mmu_->read(addr); });
1402
+ card->setMemWriteCallback([this](uint16_t addr, uint8_t val) { mmu_->write(addr, val); });
1403
+ card->setGetA([this]() { return cpu_->getA(); });
1404
+ card->setSetA([this](uint8_t v) { cpu_->setA(v); });
1405
+ card->setGetP([this]() { return cpu_->getP(); });
1406
+ card->setSetP([this](uint8_t v) { cpu_->setP(v); });
1407
+ card->setGetSP([this]() { return cpu_->getSP(); });
1408
+ card->setSetSP([this](uint8_t v) { cpu_->setSP(v); });
1409
+ card->setGetPC([this]() { return cpu_->getPC(); });
1410
+ card->setSetPC([this](uint16_t v) { cpu_->setPC(v); });
1411
+ card->setSetX([this](uint8_t v) { cpu_->setX(v); });
1412
+ card->setSetY([this](uint8_t v) { cpu_->setY(v); });
1413
+ smartport_ = card.get();
1414
+ mmu_->insertCard(slot, std::move(card));
1415
+ return true;
1416
+ }
1417
+
1418
+ return false;
1419
+ }
1420
+
1421
+ bool Emulator::isSlotEmpty(uint8_t slot) const {
1422
+ if (slot < 1 || slot > 7) {
1423
+ return true;
1424
+ }
1425
+
1426
+ // Slot 3 is never empty (built-in 80-column)
1427
+ if (slot == 3) {
1428
+ return false;
1429
+ }
1430
+
1431
+ return mmu_->isSlotEmpty(slot);
1432
+ }
1433
+
1434
+ // ============================================================================
1435
+ // State Serialization
1436
+ // ============================================================================
1437
+
1438
+ // State format version - increment when format changes
1439
+ static constexpr uint32_t STATE_VERSION = 7; // LSS 8-phase clock
1440
+ static constexpr uint32_t STATE_MAGIC = 0x53324541; // "A2ES" in little-endian
1441
+
1442
+ // Helper to write little-endian values
1443
+ static void writeLE16(std::vector<uint8_t> &buf, uint16_t val) {
1444
+ buf.push_back(val & 0xFF);
1445
+ buf.push_back((val >> 8) & 0xFF);
1446
+ }
1447
+
1448
+ static void writeLE32(std::vector<uint8_t> &buf, uint32_t val) {
1449
+ buf.push_back(val & 0xFF);
1450
+ buf.push_back((val >> 8) & 0xFF);
1451
+ buf.push_back((val >> 16) & 0xFF);
1452
+ buf.push_back((val >> 24) & 0xFF);
1453
+ }
1454
+
1455
+ static void writeLE64(std::vector<uint8_t> &buf, uint64_t val) {
1456
+ for (int i = 0; i < 8; i++) {
1457
+ buf.push_back((val >> (i * 8)) & 0xFF);
1458
+ }
1459
+ }
1460
+
1461
+ // Helper to read little-endian values
1462
+ static uint16_t readLE16(const uint8_t *data) {
1463
+ return data[0] | (data[1] << 8);
1464
+ }
1465
+
1466
+ static uint32_t readLE32(const uint8_t *data) {
1467
+ return data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
1468
+ }
1469
+
1470
+ static uint64_t readLE64(const uint8_t *data) {
1471
+ uint64_t val = 0;
1472
+ for (int i = 0; i < 8; i++) {
1473
+ val |= static_cast<uint64_t>(data[i]) << (i * 8);
1474
+ }
1475
+ return val;
1476
+ }
1477
+
1478
+ const uint8_t *Emulator::exportState(size_t *size) {
1479
+ stateBuffer_.clear();
1480
+
1481
+ // Reserve approximate size (slightly over to avoid reallocations)
1482
+ // ~200KB total (128KB main/aux + 32KB LC RAM + overhead + disk images)
1483
+ stateBuffer_.reserve(500000);
1484
+
1485
+ // Header
1486
+ writeLE32(stateBuffer_, STATE_MAGIC);
1487
+ writeLE32(stateBuffer_, STATE_VERSION);
1488
+
1489
+ // CPU state
1490
+ stateBuffer_.push_back(cpu_->getA());
1491
+ stateBuffer_.push_back(cpu_->getX());
1492
+ stateBuffer_.push_back(cpu_->getY());
1493
+ stateBuffer_.push_back(cpu_->getSP());
1494
+ stateBuffer_.push_back(cpu_->getP());
1495
+ stateBuffer_.push_back(0); // Reserved
1496
+ writeLE16(stateBuffer_, cpu_->getPC());
1497
+ writeLE64(stateBuffer_, cpu_->getTotalCycles());
1498
+
1499
+ // Memory - Main RAM (64KB)
1500
+ for (uint32_t addr = 0; addr < MAIN_RAM_SIZE; addr++) {
1501
+ stateBuffer_.push_back(mmu_->readRAM(addr, false));
1502
+ }
1503
+
1504
+ // Memory - Aux RAM (64KB)
1505
+ for (uint32_t addr = 0; addr < AUX_RAM_SIZE; addr++) {
1506
+ stateBuffer_.push_back(mmu_->readRAM(addr, true));
1507
+ }
1508
+
1509
+ // Language Card RAM - Main (16KB total: 4KB bank1 + 4KB bank2 + 8KB high)
1510
+ const uint8_t *lcb1 = mmu_->getLCBank1(false);
1511
+ const uint8_t *lcb2 = mmu_->getLCBank2(false);
1512
+ const uint8_t *lchi = mmu_->getLCHighRAM(false);
1513
+ stateBuffer_.insert(stateBuffer_.end(), lcb1, lcb1 + 0x1000);
1514
+ stateBuffer_.insert(stateBuffer_.end(), lcb2, lcb2 + 0x1000);
1515
+ stateBuffer_.insert(stateBuffer_.end(), lchi, lchi + 0x2000);
1516
+
1517
+ // Language Card RAM - Aux (16KB total)
1518
+ const uint8_t *alcb1 = mmu_->getLCBank1(true);
1519
+ const uint8_t *alcb2 = mmu_->getLCBank2(true);
1520
+ const uint8_t *alchi = mmu_->getLCHighRAM(true);
1521
+ stateBuffer_.insert(stateBuffer_.end(), alcb1, alcb1 + 0x1000);
1522
+ stateBuffer_.insert(stateBuffer_.end(), alcb2, alcb2 + 0x1000);
1523
+ stateBuffer_.insert(stateBuffer_.end(), alchi, alchi + 0x2000);
1524
+
1525
+ // Soft switches - pack into bytes
1526
+ const auto &sw = mmu_->getSoftSwitches();
1527
+ uint32_t switches1 = 0;
1528
+ if (sw.text) switches1 |= (1 << 0);
1529
+ if (sw.mixed) switches1 |= (1 << 1);
1530
+ if (sw.page2) switches1 |= (1 << 2);
1531
+ if (sw.hires) switches1 |= (1 << 3);
1532
+ if (sw.col80) switches1 |= (1 << 4);
1533
+ if (sw.altCharSet) switches1 |= (1 << 5);
1534
+ if (sw.store80) switches1 |= (1 << 6);
1535
+ if (sw.ramrd) switches1 |= (1 << 7);
1536
+ if (sw.ramwrt) switches1 |= (1 << 8);
1537
+ if (sw.intcxrom) switches1 |= (1 << 9);
1538
+ if (sw.altzp) switches1 |= (1 << 10);
1539
+ if (sw.slotc3rom) switches1 |= (1 << 11);
1540
+ if (sw.intc8rom) switches1 |= (1 << 12);
1541
+ if (sw.lcram) switches1 |= (1 << 13);
1542
+ if (sw.lcram2) switches1 |= (1 << 14);
1543
+ if (sw.lcwrite) switches1 |= (1 << 15);
1544
+ if (sw.lcprewrite) switches1 |= (1 << 16);
1545
+ if (sw.an0) switches1 |= (1 << 17);
1546
+ if (sw.an1) switches1 |= (1 << 18);
1547
+ if (sw.an2) switches1 |= (1 << 19);
1548
+ if (sw.an3) switches1 |= (1 << 20);
1549
+ if (sw.ioudis) switches1 |= (1 << 21);
1550
+ writeLE32(stateBuffer_, switches1);
1551
+
1552
+ // Keyboard latch
1553
+ stateBuffer_.push_back(keyboardLatch_);
1554
+ stateBuffer_.push_back(keyDown_ ? 1 : 0);
1555
+
1556
+ // Button state
1557
+ stateBuffer_.push_back(buttonState_[0] ? 1 : 0);
1558
+ stateBuffer_.push_back(buttonState_[1] ? 1 : 0);
1559
+ stateBuffer_.push_back(buttonState_[2] ? 1 : 0);
1560
+
1561
+ // Emulator timing
1562
+ writeLE64(stateBuffer_, lastFrameCycle_);
1563
+ writeLE32(stateBuffer_, samplesGenerated_);
1564
+
1565
+ // Disk controller state
1566
+ auto &disk = *disk_;
1567
+ stateBuffer_.push_back(disk.isMotorOn() ? 1 : 0);
1568
+ stateBuffer_.push_back(static_cast<uint8_t>(disk.getSelectedDrive()));
1569
+ stateBuffer_.push_back(disk.getQ6() ? 1 : 0);
1570
+ stateBuffer_.push_back(disk.getQ7() ? 1 : 0);
1571
+ stateBuffer_.push_back(disk.getPhaseStates());
1572
+ stateBuffer_.push_back(disk.getDataLatch());
1573
+ stateBuffer_.push_back(disk.getSequencerState());
1574
+ stateBuffer_.push_back(disk.getBusData());
1575
+ stateBuffer_.push_back(disk.getLSSClock());
1576
+
1577
+ // Per-drive state (track positions, disk image data, and filenames)
1578
+ for (int drive = 0; drive < 2; drive++) {
1579
+ if (disk.hasDisk(drive)) {
1580
+ const auto *image = disk.getDiskImage(drive);
1581
+ stateBuffer_.push_back(1); // Disk present
1582
+ writeLE16(stateBuffer_, static_cast<uint16_t>(image->getQuarterTrack()));
1583
+
1584
+ // Export the disk image data
1585
+ size_t diskSize = 0;
1586
+ const uint8_t *diskData = disk.exportDiskData(drive, &diskSize);
1587
+ writeLE32(stateBuffer_, static_cast<uint32_t>(diskSize));
1588
+ if (diskData && diskSize > 0) {
1589
+ stateBuffer_.insert(stateBuffer_.end(), diskData, diskData + diskSize);
1590
+ }
1591
+
1592
+ // Save filename
1593
+ const std::string &filename = image->getFilename();
1594
+ writeLE16(stateBuffer_, static_cast<uint16_t>(filename.length()));
1595
+ stateBuffer_.insert(stateBuffer_.end(), filename.begin(), filename.end());
1596
+ } else {
1597
+ stateBuffer_.push_back(0); // No disk
1598
+ writeLE16(stateBuffer_, 0);
1599
+ writeLE32(stateBuffer_, 0); // Zero disk size
1600
+ writeLE16(stateBuffer_, 0); // Zero filename length
1601
+ }
1602
+ }
1603
+
1604
+ // Audio state (speaker)
1605
+ stateBuffer_.push_back(audio_->getSpeakerState() ? 1 : 0);
1606
+
1607
+ // Mockingboard state
1608
+ uint8_t mbState[MockingboardCard::STATE_SIZE];
1609
+ size_t mbSize = 0;
1610
+ if (mockingboard_) {
1611
+ mbSize = mockingboard_->serialize(mbState, sizeof(mbState));
1612
+ }
1613
+ writeLE16(stateBuffer_, static_cast<uint16_t>(mbSize));
1614
+ if (mbSize > 0) {
1615
+ stateBuffer_.insert(stateBuffer_.end(), mbState, mbState + mbSize);
1616
+ }
1617
+
1618
+ // Expansion card states (slots 1-7, excluding 4 and 6 which are handled above)
1619
+ // First, count how many cards have state to save
1620
+ uint8_t cardCount = 0;
1621
+ for (uint8_t slot = 1; slot <= 7; slot++) {
1622
+ if (slot == 4 || slot == 6) continue; // Handled by legacy system
1623
+ ExpansionCard* card = mmu_->getCard(slot);
1624
+ if (card && card->getStateSize() > 0) {
1625
+ cardCount++;
1626
+ }
1627
+ }
1628
+ stateBuffer_.push_back(cardCount);
1629
+
1630
+ // Save each card's state
1631
+ for (uint8_t slot = 1; slot <= 7; slot++) {
1632
+ if (slot == 4 || slot == 6) continue;
1633
+ ExpansionCard* card = mmu_->getCard(slot);
1634
+ if (card && card->getStateSize() > 0) {
1635
+ // Slot number
1636
+ stateBuffer_.push_back(slot);
1637
+
1638
+ // Card type identifier (use name for identification)
1639
+ const char* name = card->getName();
1640
+ uint8_t cardType = 0;
1641
+ if (strcmp(name, "Thunderclock") == 0) cardType = 1;
1642
+ if (strcmp(name, "Mouse") == 0) cardType = 2;
1643
+ if (strcmp(name, "SmartPort") == 0) cardType = 3;
1644
+ stateBuffer_.push_back(cardType);
1645
+
1646
+ // Card state
1647
+ size_t stateSize = card->getStateSize();
1648
+ writeLE16(stateBuffer_, static_cast<uint16_t>(stateSize));
1649
+ size_t currentSize = stateBuffer_.size();
1650
+ stateBuffer_.resize(currentSize + stateSize);
1651
+ card->serialize(stateBuffer_.data() + currentSize, stateSize);
1652
+ }
1653
+ }
1654
+
1655
+ *size = stateBuffer_.size();
1656
+ return stateBuffer_.data();
1657
+ }
1658
+
1659
+ bool Emulator::importState(const uint8_t *data, size_t size) {
1660
+ // Minimum size check
1661
+ if (size < 8) {
1662
+ return false;
1663
+ }
1664
+
1665
+ size_t offset = 0;
1666
+
1667
+ // Check magic and version before resetting
1668
+ uint32_t magic = readLE32(data + offset);
1669
+ offset += 4;
1670
+ if (magic != STATE_MAGIC) {
1671
+ return false;
1672
+ }
1673
+
1674
+ uint32_t version = readLE32(data + offset);
1675
+ offset += 4;
1676
+ if (version != STATE_VERSION) {
1677
+ return false;
1678
+ }
1679
+
1680
+ // Reset emulator to clean state before importing
1681
+ // This ensures no old state is left floating around
1682
+ reset();
1683
+
1684
+ // CPU state
1685
+ if (offset + 16 > size) return false;
1686
+ cpu_->setA(data[offset++]);
1687
+ cpu_->setX(data[offset++]);
1688
+ cpu_->setY(data[offset++]);
1689
+ cpu_->setSP(data[offset++]);
1690
+ cpu_->setP(data[offset++]);
1691
+ offset++; // Reserved
1692
+
1693
+ cpu_->setPC(readLE16(data + offset));
1694
+ offset += 2;
1695
+
1696
+ // Restore total cycles for accurate disk timing
1697
+ uint64_t totalCycles = readLE64(data + offset);
1698
+ offset += 8;
1699
+ cpu_->setTotalCycles(totalCycles);
1700
+
1701
+ // Memory - Main RAM (64KB)
1702
+ if (offset + MAIN_RAM_SIZE > size) return false;
1703
+ for (uint32_t addr = 0; addr < MAIN_RAM_SIZE; addr++) {
1704
+ mmu_->writeRAM(addr, data[offset++], false);
1705
+ }
1706
+
1707
+ // Memory - Aux RAM (64KB)
1708
+ if (offset + AUX_RAM_SIZE > size) return false;
1709
+ for (uint32_t addr = 0; addr < AUX_RAM_SIZE; addr++) {
1710
+ mmu_->writeRAM(addr, data[offset++], true);
1711
+ }
1712
+
1713
+ // Language Card RAM - Main (16KB: 4KB + 4KB + 8KB)
1714
+ if (offset + 0x4000 > size) return false;
1715
+ mmu_->setLCBank1(data + offset, false);
1716
+ offset += 0x1000;
1717
+ mmu_->setLCBank2(data + offset, false);
1718
+ offset += 0x1000;
1719
+ mmu_->setLCHighRAM(data + offset, false);
1720
+ offset += 0x2000;
1721
+
1722
+ // Language Card RAM - Aux (16KB)
1723
+ if (offset + 0x4000 > size) return false;
1724
+ mmu_->setLCBank1(data + offset, true);
1725
+ offset += 0x1000;
1726
+ mmu_->setLCBank2(data + offset, true);
1727
+ offset += 0x1000;
1728
+ mmu_->setLCHighRAM(data + offset, true);
1729
+ offset += 0x2000;
1730
+
1731
+ // Soft switches
1732
+ if (offset + 4 > size) return false;
1733
+ uint32_t switches1 = readLE32(data + offset);
1734
+ offset += 4;
1735
+
1736
+ // Restore soft switches by writing to appropriate addresses
1737
+ // TEXT/GRAPHICS
1738
+ mmu_->write((switches1 & (1 << 0)) ? 0xC051 : 0xC050, 0);
1739
+ // MIXED
1740
+ mmu_->write((switches1 & (1 << 1)) ? 0xC053 : 0xC052, 0);
1741
+ // PAGE1/PAGE2
1742
+ mmu_->write((switches1 & (1 << 2)) ? 0xC055 : 0xC054, 0);
1743
+ // LORES/HIRES
1744
+ mmu_->write((switches1 & (1 << 3)) ? 0xC057 : 0xC056, 0);
1745
+ // 80COL
1746
+ mmu_->write((switches1 & (1 << 4)) ? 0xC00D : 0xC00C, 0);
1747
+ // ALTCHARSET
1748
+ mmu_->write((switches1 & (1 << 5)) ? 0xC00F : 0xC00E, 0);
1749
+ // 80STORE
1750
+ mmu_->write((switches1 & (1 << 6)) ? 0xC001 : 0xC000, 0);
1751
+ // RAMRD
1752
+ mmu_->write((switches1 & (1 << 7)) ? 0xC003 : 0xC002, 0);
1753
+ // RAMWRT
1754
+ mmu_->write((switches1 & (1 << 8)) ? 0xC005 : 0xC004, 0);
1755
+ // INTCXROM
1756
+ mmu_->write((switches1 & (1 << 9)) ? 0xC007 : 0xC006, 0);
1757
+ // ALTZP
1758
+ mmu_->write((switches1 & (1 << 10)) ? 0xC009 : 0xC008, 0);
1759
+ // SLOTC3ROM
1760
+ mmu_->write((switches1 & (1 << 11)) ? 0xC00B : 0xC00A, 0);
1761
+ // INTC8ROM - this is set automatically by slot access
1762
+ // LCRAM, LCRAM2, LCWRITE - need special handling via language card switches
1763
+ // AN0-AN3
1764
+ mmu_->write((switches1 & (1 << 17)) ? 0xC059 : 0xC058, 0);
1765
+ mmu_->write((switches1 & (1 << 18)) ? 0xC05B : 0xC05A, 0);
1766
+ mmu_->write((switches1 & (1 << 19)) ? 0xC05D : 0xC05C, 0);
1767
+ mmu_->write((switches1 & (1 << 20)) ? 0xC05F : 0xC05E, 0);
1768
+
1769
+ // Language card state restoration
1770
+ bool lcram = switches1 & (1 << 13);
1771
+ bool lcram2 = switches1 & (1 << 14);
1772
+ bool lcwrite = switches1 & (1 << 15);
1773
+ // Select the appropriate language card mode
1774
+ // lcram2 means bank 2 is selected (not bank 1)
1775
+ // $C080: bank2, read RAM, no write
1776
+ // $C081: bank2, read ROM, write (needs double read)
1777
+ // $C083: bank2, read RAM, write (needs double read)
1778
+ // $C088: bank1, read RAM, no write
1779
+ // etc.
1780
+ if (lcram) {
1781
+ if (lcram2) {
1782
+ if (lcwrite) {
1783
+ mmu_->read(0xC083);
1784
+ mmu_->read(0xC083); // Double read to enable write
1785
+ } else {
1786
+ mmu_->read(0xC080);
1787
+ }
1788
+ } else {
1789
+ if (lcwrite) {
1790
+ mmu_->read(0xC08B);
1791
+ mmu_->read(0xC08B);
1792
+ } else {
1793
+ mmu_->read(0xC088);
1794
+ }
1795
+ }
1796
+ } else {
1797
+ if (lcram2) {
1798
+ mmu_->read(0xC082);
1799
+ } else {
1800
+ mmu_->read(0xC08A);
1801
+ }
1802
+ }
1803
+
1804
+ // Keyboard state
1805
+ if (offset + 5 > size) return false;
1806
+ keyboardLatch_ = data[offset++];
1807
+ keyDown_ = data[offset++] != 0;
1808
+
1809
+ // Button state
1810
+ buttonState_[0] = data[offset++] != 0;
1811
+ buttonState_[1] = data[offset++] != 0;
1812
+ buttonState_[2] = data[offset++] != 0;
1813
+
1814
+ // Emulator timing
1815
+ if (offset + 12 > size) return false;
1816
+ lastFrameCycle_ = readLE64(data + offset);
1817
+ offset += 8;
1818
+ samplesGenerated_ = static_cast<int>(readLE32(data + offset));
1819
+ offset += 4;
1820
+
1821
+ // Disk controller state
1822
+ if (offset + 9 > size) return false;
1823
+ bool motorOn = data[offset++] != 0;
1824
+ int selectedDrive = data[offset++];
1825
+ bool q6 = data[offset++] != 0;
1826
+ bool q7 = data[offset++] != 0;
1827
+ uint8_t phaseStates = data[offset++];
1828
+ uint8_t dataLatch = data[offset++];
1829
+ uint8_t seqState = data[offset++];
1830
+ uint8_t busDataVal = data[offset++];
1831
+ uint8_t lssClockVal = data[offset++];
1832
+
1833
+ // Restore disk controller state (including motor for mid-load restores)
1834
+ if (disk_) {
1835
+ disk_->setMotorOn(motorOn);
1836
+ disk_->setSelectedDrive(selectedDrive);
1837
+ disk_->setQ6(q6);
1838
+ disk_->setQ7(q7);
1839
+ disk_->setPhaseStates(phaseStates);
1840
+ disk_->setDataLatch(dataLatch);
1841
+ disk_->setSequencerState(seqState);
1842
+ disk_->setBusData(busDataVal);
1843
+ disk_->setLSSClock(lssClockVal);
1844
+ }
1845
+
1846
+ // Per-drive state (disk images and track positions)
1847
+ for (int drive = 0; drive < 2; drive++) {
1848
+ if (offset + 3 > size) return false;
1849
+ bool hasDisk = data[offset++] != 0;
1850
+ uint16_t quarterTrack = readLE16(data + offset);
1851
+ offset += 2;
1852
+
1853
+ // Read disk size
1854
+ if (offset + 4 > size) return false;
1855
+ uint32_t diskSize = readLE32(data + offset);
1856
+ offset += 4;
1857
+
1858
+ if (hasDisk && diskSize > 0) {
1859
+ if (offset + diskSize > size) return false;
1860
+
1861
+ // Read disk data offset for insertion
1862
+ const uint8_t *diskData = data + offset;
1863
+ offset += diskSize;
1864
+
1865
+ // Read filename
1866
+ if (offset + 2 > size) return false;
1867
+ uint16_t filenameLen = readLE16(data + offset);
1868
+ offset += 2;
1869
+
1870
+ std::string filename = "state.dsk";
1871
+ if (filenameLen > 0 && offset + filenameLen <= size) {
1872
+ filename = std::string(reinterpret_cast<const char*>(data + offset), filenameLen);
1873
+ offset += filenameLen;
1874
+ }
1875
+
1876
+ // Insert the disk image from the saved state
1877
+ if (disk_) {
1878
+ disk_->insertDisk(drive, diskData, diskSize, filename);
1879
+
1880
+ // Restore track position
1881
+ auto *image = disk_->getMutableDiskImage(drive);
1882
+ if (image) {
1883
+ image->setQuarterTrack(quarterTrack);
1884
+ }
1885
+ }
1886
+ } else {
1887
+ // Skip filename length field (will be 0)
1888
+ if (offset + 2 <= size) {
1889
+ offset += 2;
1890
+ }
1891
+ // Eject any existing disk
1892
+ if (disk_) disk_->ejectDisk(drive);
1893
+ }
1894
+ }
1895
+
1896
+ // Audio state
1897
+ if (offset + 1 > size) return false;
1898
+ bool speakerState = data[offset++] != 0;
1899
+ (void)speakerState;
1900
+
1901
+ // Mockingboard state
1902
+ if (offset + 2 <= size) {
1903
+ uint16_t mbSize = readLE16(data + offset);
1904
+ offset += 2;
1905
+ if (mbSize > 0 && offset + mbSize <= size) {
1906
+ if (mockingboard_) {
1907
+ mockingboard_->deserialize(data + offset, mbSize);
1908
+ }
1909
+ offset += mbSize;
1910
+ }
1911
+ }
1912
+
1913
+ // Expansion card states
1914
+ if (offset + 1 <= size) {
1915
+ uint8_t cardCount = data[offset++];
1916
+ for (uint8_t i = 0; i < cardCount && offset + 4 <= size; i++) {
1917
+ uint8_t slot = data[offset++];
1918
+ uint8_t cardType = data[offset++];
1919
+ uint16_t stateSize = readLE16(data + offset);
1920
+ offset += 2;
1921
+
1922
+ if (offset + stateSize > size) break;
1923
+
1924
+ // Create card based on type if slot is empty
1925
+ if (slot >= 1 && slot <= 7 && slot != 4 && slot != 6) {
1926
+ ExpansionCard* existingCard = mmu_->getCard(slot);
1927
+
1928
+ // Create appropriate card type if needed
1929
+ if (!existingCard) {
1930
+ switch (cardType) {
1931
+ case 1: { // Thunderclock
1932
+ auto card = std::make_unique<ThunderclockCard>();
1933
+ mmu_->insertCard(slot, std::move(card));
1934
+ existingCard = mmu_->getCard(slot);
1935
+ break;
1936
+ }
1937
+ case 2: { // Mouse
1938
+ auto card = std::make_unique<MouseCard>();
1939
+ card->setSlotNumber(slot);
1940
+ card->setCycleCallback([this]() { return cpu_->getTotalCycles(); });
1941
+ card->setIRQCallback([this]() { cpu_->irq(); });
1942
+ mouse_ = card.get();
1943
+ mmu_->insertCard(slot, std::move(card));
1944
+ existingCard = mmu_->getCard(slot);
1945
+ break;
1946
+ }
1947
+ case 3: { // SmartPort
1948
+ auto card = std::make_unique<SmartPortCard>();
1949
+ card->setSlotNumber(slot);
1950
+ card->setMemReadCallback([this](uint16_t addr) { return mmu_->read(addr); });
1951
+ card->setMemWriteCallback([this](uint16_t addr, uint8_t val) { mmu_->write(addr, val); });
1952
+ card->setGetA([this]() { return cpu_->getA(); });
1953
+ card->setSetA([this](uint8_t v) { cpu_->setA(v); });
1954
+ card->setGetP([this]() { return cpu_->getP(); });
1955
+ card->setSetP([this](uint8_t v) { cpu_->setP(v); });
1956
+ card->setGetSP([this]() { return cpu_->getSP(); });
1957
+ card->setSetSP([this](uint8_t v) { cpu_->setSP(v); });
1958
+ card->setGetPC([this]() { return cpu_->getPC(); });
1959
+ card->setSetPC([this](uint16_t v) { cpu_->setPC(v); });
1960
+ card->setSetX([this](uint8_t v) { cpu_->setX(v); });
1961
+ card->setSetY([this](uint8_t v) { cpu_->setY(v); });
1962
+ smartport_ = card.get();
1963
+ mmu_->insertCard(slot, std::move(card));
1964
+ existingCard = mmu_->getCard(slot);
1965
+ break;
1966
+ }
1967
+ }
1968
+ }
1969
+
1970
+ // Restore card state
1971
+ if (existingCard) {
1972
+ existingCard->deserialize(data + offset, stateSize);
1973
+ }
1974
+ }
1975
+
1976
+ offset += stateSize;
1977
+ }
1978
+ }
1979
+
1980
+ frameReady_ = true;
1981
+ breakpointHit_ = false;
1982
+ paused_ = false;
1983
+
1984
+ return true;
1985
+ }
1986
+
1987
+ // ============================================================================
1988
+ // SmartPort Hard Drive Management
1989
+ // ============================================================================
1990
+
1991
+ bool Emulator::insertSmartPortImage(int device, const uint8_t* data, size_t size, const char* filename) {
1992
+ if (!smartport_) return false;
1993
+ return smartport_->insertImage(device, data, size, filename ? filename : "");
1994
+ }
1995
+
1996
+ void Emulator::ejectSmartPortImage(int device) {
1997
+ if (smartport_) smartport_->ejectImage(device);
1998
+ }
1999
+
2000
+ bool Emulator::isSmartPortImageInserted(int device) const {
2001
+ if (!smartport_) return false;
2002
+ return smartport_->isImageInserted(device);
2003
+ }
2004
+
2005
+ const char* Emulator::getSmartPortImageFilename(int device) const {
2006
+ if (!smartport_) return nullptr;
2007
+ const auto& fn = smartport_->getImageFilename(device);
2008
+ return fn.empty() ? nullptr : fn.c_str();
2009
+ }
2010
+
2011
+ bool Emulator::isSmartPortImageModified(int device) const {
2012
+ if (!smartport_) return false;
2013
+ return smartport_->isImageModified(device);
2014
+ }
2015
+
2016
+ const uint8_t* Emulator::exportSmartPortImageData(int device, size_t* size) const {
2017
+ if (!smartport_) {
2018
+ if (size) *size = 0;
2019
+ return nullptr;
2020
+ }
2021
+ return smartport_->exportImageData(device, size);
2022
+ }
2023
+
2024
+ const uint8_t* Emulator::getSmartPortBlockData(int device, size_t* size) const {
2025
+ if (!smartport_) {
2026
+ if (size) *size = 0;
2027
+ return nullptr;
2028
+ }
2029
+ return smartport_->getBlockData(device, size);
2030
+ }
2031
+
2032
+ // ==========================================================================
2033
+ // Screen text extraction
2034
+ // ==========================================================================
2035
+
2036
+ // Apple II text screen row base addresses (non-linear memory layout)
2037
+ static constexpr uint16_t TEXT_ROW_BASES[24] = {
2038
+ 0x400, 0x480, 0x500, 0x580, 0x600, 0x680, 0x700, 0x780,
2039
+ 0x428, 0x4A8, 0x528, 0x5A8, 0x628, 0x6A8, 0x728, 0x7A8,
2040
+ 0x450, 0x4D0, 0x550, 0x5D0, 0x650, 0x6D0, 0x750, 0x7D0
2041
+ };
2042
+
2043
+ int Emulator::screenCodeToAscii(uint8_t code) {
2044
+ if (code >= 0xE0) return code - 0x80; // Normal lowercase
2045
+ if (code >= 0xC0) return code - 0x80; // Normal uppercase / MouseText
2046
+ if (code >= 0xA0) return code - 0x80; // Normal symbols/digits
2047
+ if (code >= 0x80) return code - 0x40; // Normal uppercase @A-Z[\]^_
2048
+ if (code >= 0x60) return code - 0x40; // Flash symbols
2049
+ if (code >= 0x40) return code; // Flash uppercase
2050
+ if (code >= 0x20) return code; // Inverse symbols/digits
2051
+ return code + 0x40; // Inverse uppercase
2052
+ }
2053
+
2054
+ const char* Emulator::readScreenText(int startRow, int startCol,
2055
+ int endRow, int endCol) {
2056
+ screenTextBuffer_.clear();
2057
+
2058
+ const auto& sw = mmu_->getSoftSwitches();
2059
+ if (!sw.text) {
2060
+ return screenTextBuffer_.c_str();
2061
+ }
2062
+
2063
+ bool col80 = sw.col80;
2064
+ bool page2 = sw.page2;
2065
+ int cols = col80 ? 80 : 40;
2066
+ uint16_t page2Offset = page2 ? 0x400 : 0;
2067
+
2068
+ // Clamp
2069
+ if (startRow < 0) startRow = 0;
2070
+ if (startCol < 0) startCol = 0;
2071
+ if (endRow > 23) endRow = 23;
2072
+ if (endCol >= cols) endCol = cols - 1;
2073
+
2074
+ for (int row = startRow; row <= endRow; row++) {
2075
+ int colStart = (row == startRow) ? startCol : 0;
2076
+ int colEnd = (row == endRow) ? endCol : cols - 1;
2077
+
2078
+ int lineEnd = colEnd; // Track last non-space for trimming
2079
+
2080
+ // First pass: find last non-space character for trimming
2081
+ for (int col = colEnd; col >= colStart; col--) {
2082
+ uint8_t charCode;
2083
+ if (col80) {
2084
+ int memCol = col / 2;
2085
+ bool isAux = (col % 2) == 0;
2086
+ uint16_t addr = TEXT_ROW_BASES[row] + page2Offset + memCol;
2087
+ charCode = isAux ? mmu_->peekAux(addr) : mmu_->peek(addr);
2088
+ } else {
2089
+ uint16_t addr = TEXT_ROW_BASES[row] + page2Offset + col;
2090
+ charCode = mmu_->peek(addr);
2091
+ }
2092
+ int ascii = screenCodeToAscii(charCode);
2093
+ if (ascii != 0x20) {
2094
+ lineEnd = col;
2095
+ break;
2096
+ }
2097
+ if (col == colStart) {
2098
+ lineEnd = colStart - 1; // All spaces
2099
+ }
2100
+ }
2101
+
2102
+ // Second pass: emit characters up to lineEnd
2103
+ for (int col = colStart; col <= lineEnd; col++) {
2104
+ uint8_t charCode;
2105
+ if (col80) {
2106
+ int memCol = col / 2;
2107
+ bool isAux = (col % 2) == 0;
2108
+ uint16_t addr = TEXT_ROW_BASES[row] + page2Offset + memCol;
2109
+ charCode = isAux ? mmu_->peekAux(addr) : mmu_->peek(addr);
2110
+ } else {
2111
+ uint16_t addr = TEXT_ROW_BASES[row] + page2Offset + col;
2112
+ charCode = mmu_->peek(addr);
2113
+ }
2114
+ int ascii = screenCodeToAscii(charCode);
2115
+ screenTextBuffer_ += static_cast<char>(ascii);
2116
+ }
2117
+
2118
+ if (row < endRow) {
2119
+ screenTextBuffer_ += '\n';
2120
+ }
2121
+ }
2122
+
2123
+ return screenTextBuffer_.c_str();
2124
+ }
2125
+
2126
+ } // namespace a2e