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,603 @@
1
+ /*
2
+ * smartport_card.cpp - SmartPort expansion card for ProDOS block devices
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ #include "smartport_card.hpp"
9
+ #include <cstring>
10
+
11
+ namespace a2e {
12
+
13
+ const std::string SmartPortCard::emptyString_;
14
+
15
+ // ProDOS block device entry point offset in slot ROM
16
+ static constexpr uint8_t PRODOS_ENTRY = 0x10;
17
+ // SmartPort entry = ProDOS entry + 3
18
+ static constexpr uint8_t SMARTPORT_ENTRY = 0x13;
19
+ // Boot trigger offset in I/O space
20
+ static constexpr uint8_t BOOT_IO_OFFSET = 0x00;
21
+
22
+ // SmartPort error codes
23
+ static constexpr uint8_t SP_OK = 0x00;
24
+ static constexpr uint8_t SP_IO_ERROR = 0x27;
25
+ static constexpr uint8_t SP_NO_DEVICE = 0x28;
26
+ static constexpr uint8_t SP_WRITE_PROTECTED = 0x2B;
27
+
28
+ SmartPortCard::SmartPortCard() {
29
+ rom_.fill(0);
30
+ buildROM();
31
+ }
32
+
33
+ void SmartPortCard::setSlotNumber(uint8_t slot) {
34
+ if (slot < 1 || slot > 7) return;
35
+ slotNum_ = slot;
36
+ buildROM();
37
+ }
38
+
39
+ void SmartPortCard::buildROM() {
40
+ rom_.fill(0);
41
+
42
+ // Signature bytes for ProDOS device discovery.
43
+ // ProDOS checks odd bytes at $Cn01, $Cn03, $Cn05, $Cn07.
44
+ // We embed them as LDA# operands so they occupy the right offsets.
45
+
46
+ // $00: LDA #$20 -> $Cn01 = $20 (ProDOS signature)
47
+ rom_[0x00] = 0xA9; rom_[0x01] = 0x20;
48
+ // $02: LDA #$00 -> $Cn03 = $00 (block device)
49
+ rom_[0x02] = 0xA9; rom_[0x03] = 0x00;
50
+ // $04: LDA #$03 -> $Cn05 = $03 (identifies as SmartPort-capable)
51
+ rom_[0x04] = 0xA9; rom_[0x05] = 0x03;
52
+ // $06: LDA #$00 -> $Cn07 = $00 (SmartPort present)
53
+ rom_[0x06] = 0xA9; rom_[0x07] = 0x00;
54
+
55
+ // Boot code at $08: set X to slot*16, trigger I/O boot, JMP $0801
56
+ // This path is used by PR#n from BASIC (execution falls through from $Cn00)
57
+ uint8_t slotOffset = slotNum_ << 4; // slot * 16
58
+ uint8_t ioAddr = 0x80 + slotOffset; // $C0n0 base
59
+
60
+ // LDX #$n0 (set X to slot*16, needed by ProDOS boot block)
61
+ rom_[0x08] = 0xA2;
62
+ rom_[0x09] = slotOffset;
63
+ // STX $C0n0 (trigger I/O trap for boot block load)
64
+ rom_[0x0A] = 0x8E;
65
+ rom_[0x0B] = ioAddr;
66
+ rom_[0x0C] = 0xC0;
67
+ // RTS - if boot succeeded, writeIO pushed $0800 on stack so RTS goes to $0801.
68
+ // If no disk loaded, RTS returns to the autostart ROM caller which scans the next slot.
69
+ rom_[0x0D] = 0x60;
70
+
71
+ // ProDOS block device entry at $10: SEC + RTS (trapped by readROM)
72
+ rom_[PRODOS_ENTRY] = 0x38; // SEC
73
+ rom_[PRODOS_ENTRY + 1] = 0x60; // RTS
74
+
75
+ // Padding byte at $12
76
+ rom_[0x12] = 0xEA; // NOP
77
+
78
+ // SmartPort entry at $13: SEC + RTS (trapped by readROM)
79
+ rom_[SMARTPORT_ENTRY] = 0x38; // SEC
80
+ rom_[SMARTPORT_ENTRY + 1] = 0x60; // RTS
81
+
82
+ // $FF: ProDOS entry point offset (used by both autostart ROM boot and ProDOS driver)
83
+ // The readROM trap at $Cn10 distinguishes boot vs ProDOS calls via the booted_ flag.
84
+ rom_[0xFF] = PRODOS_ENTRY;
85
+ }
86
+
87
+ bool SmartPortCard::hasAnyDevice() const {
88
+ for (int i = 0; i < MAX_DEVICES; i++) {
89
+ if (devices_[i].isLoaded()) return true;
90
+ }
91
+ return false;
92
+ }
93
+
94
+ void SmartPortCard::reset() {
95
+ booted_ = false;
96
+ activity_ = false;
97
+ activityWrite_ = false;
98
+ }
99
+
100
+ uint8_t SmartPortCard::readIO(uint8_t offset) {
101
+ (void)offset;
102
+ return 0xFF;
103
+ }
104
+
105
+ void SmartPortCard::writeIO(uint8_t offset, uint8_t value) {
106
+ if (offset == BOOT_IO_OFFSET) {
107
+ // Boot trap: load block 0 of device 0 into $0800
108
+ if (!devices_[0].isLoaded() || !memWrite_ || !getSP_ || !setSP_) {
109
+ // Mark as booted so subsequent ProDOS calls to $Cn10 are handled
110
+ // as block device driver calls rather than triggering another boot.
111
+ booted_ = true;
112
+ // No disk loaded. Scan lower slots for the next bootable device,
113
+ // mimicking the autostart ROM's 7→1 scan that was interrupted.
114
+ if (setPC_ && memRead_) {
115
+ for (int slot = slotNum_ - 1; slot >= 1; slot--) {
116
+ uint16_t base = 0xC000 | (slot << 8);
117
+ uint8_t sig = memRead_(base + 1); // $Cn01
118
+ if (sig == 0x20) {
119
+ // ProDOS-compatible device found (Disk II, SmartPort, etc.)
120
+ uint8_t entry = memRead_(base + 0xFF);
121
+ setPC_(base + (entry ? entry : 0));
122
+ return;
123
+ }
124
+ }
125
+ // No bootable slot found — fall through to Applesoft prompt
126
+ setPC_(0xE003);
127
+ }
128
+ return;
129
+ }
130
+
131
+ uint8_t blockBuf[BlockDevice::BLOCK_SIZE];
132
+ if (devices_[0].readBlock(0, blockBuf)) {
133
+ for (size_t i = 0; i < BlockDevice::BLOCK_SIZE; i++) {
134
+ memWrite_(static_cast<uint16_t>(0x0800 + i), blockBuf[i]);
135
+ }
136
+ activity_ = true;
137
+ activityWrite_ = false;
138
+
139
+ // Set X to slot*16 (ProDOS boot block expects this)
140
+ if (setX_) setX_(slotNum_ << 4);
141
+
142
+ // Push $0800 onto the stack so the RTS in boot ROM goes to $0801
143
+ uint8_t sp = getSP_();
144
+ memWrite_(0x0100 + sp, 0x08); // high byte
145
+ sp = static_cast<uint8_t>(sp - 1);
146
+ memWrite_(0x0100 + sp, 0x00); // low byte
147
+ sp = static_cast<uint8_t>(sp - 1);
148
+ setSP_(sp);
149
+ }
150
+ }
151
+ }
152
+
153
+ uint8_t SmartPortCard::readROM(uint8_t offset) {
154
+ // When no devices are loaded, hide the ROM so ProDOS doesn't detect this slot
155
+ if (!hasAnyDevice()) return 0;
156
+
157
+ // Check if the CPU is executing at this ROM address (not just reading data).
158
+ // The CPU's fetch() does read(pc_++) so by the time the read callback fires,
159
+ // PC has already been incremented by 1. We account for this by comparing
160
+ // against expectedPC + 1.
161
+ uint16_t expectedPC = (0xC000 | (static_cast<uint16_t>(slotNum_) << 8)) + offset;
162
+
163
+ if (getPC_ && getPC_() == static_cast<uint16_t>(expectedPC + 1)) {
164
+ if (offset == PRODOS_ENTRY) {
165
+ if (!booted_) {
166
+ // First call to entry point = boot (from autostart ROM or PR#n fallthrough)
167
+ if (!handleBoot()) {
168
+ // No disk loaded. Scan lower slots for the next bootable
169
+ // device, continuing the autostart ROM's 7→1 scan.
170
+ if (setPC_ && memRead_) {
171
+ for (int slot = slotNum_ - 1; slot >= 1; slot--) {
172
+ uint16_t base = 0xC000 | (slot << 8);
173
+ uint8_t sig = memRead_(base + 1);
174
+ if (sig == 0x20) {
175
+ uint8_t entry = memRead_(base + 0xFF);
176
+ setPC_(base + (entry ? entry : 0));
177
+ return 0xEA; // NOP
178
+ }
179
+ }
180
+ setPC_(0xE003); // No bootable slot — Applesoft prompt
181
+ }
182
+ return 0xEA; // NOP (harmless; CPU continues from new PC)
183
+ }
184
+ } else {
185
+ // Subsequent calls = ProDOS block driver calls
186
+ handleProDOSBlock();
187
+ }
188
+ return 0x60; // RTS
189
+ }
190
+ if (offset == SMARTPORT_ENTRY) {
191
+ handleSmartPort();
192
+ return 0x60; // RTS
193
+ }
194
+ }
195
+
196
+ return rom_[offset];
197
+ }
198
+
199
+ bool SmartPortCard::handleBoot() {
200
+ // Called when the autostart ROM or PR#n reaches the entry point for the first time.
201
+ // Load block 0 of device 0 into $0800, set X to slot*16, and arrange
202
+ // for the CPU to jump to $0801 (ProDOS boot block entry) via RTS.
203
+ booted_ = true;
204
+
205
+ if (!devices_[0].isLoaded() || !memWrite_ || !getSP_ || !setSP_) return false;
206
+
207
+ // Load block 0 into $0800
208
+ uint8_t blockBuf[BlockDevice::BLOCK_SIZE];
209
+ if (!devices_[0].readBlock(0, blockBuf)) return false;
210
+
211
+ for (size_t i = 0; i < BlockDevice::BLOCK_SIZE; i++) {
212
+ memWrite_(static_cast<uint16_t>(0x0800 + i), blockBuf[i]);
213
+ }
214
+
215
+ activity_ = true;
216
+ activityWrite_ = false;
217
+
218
+ // Set X to slot*16 (ProDOS boot block expects this)
219
+ if (setX_) setX_(slotNum_ << 4);
220
+
221
+ // Push $0800 onto the stack so RTS goes to $0801 (RTS adds 1 to popped address)
222
+ uint8_t sp = getSP_();
223
+ memWrite_(0x0100 + sp, 0x08); // high byte
224
+ sp = static_cast<uint8_t>(sp - 1);
225
+ memWrite_(0x0100 + sp, 0x00); // low byte
226
+ sp = static_cast<uint8_t>(sp - 1);
227
+ setSP_(sp);
228
+ return true;
229
+ }
230
+
231
+ void SmartPortCard::setErrorResult(uint8_t errorCode) {
232
+ if (!setA_ || !getP_ || !setP_) return;
233
+
234
+ setA_(errorCode);
235
+
236
+ uint8_t p = getP_();
237
+ if (errorCode != SP_OK) {
238
+ p |= 0x01; // Set carry (error)
239
+ } else {
240
+ p &= ~0x01; // Clear carry (success)
241
+ }
242
+ setP_(p);
243
+ }
244
+
245
+ void SmartPortCard::handleProDOSBlock() {
246
+ // ProDOS block call convention:
247
+ // $42 = command (0=STATUS, 1=READ, 2=WRITE, 3=FORMAT)
248
+ // $43 = unit number (bit 7: 0=device 0, 1=device 1; bits 4-6: slot)
249
+ // $44-$45 = buffer pointer (lo/hi)
250
+ // $46-$47 = block number (lo/hi)
251
+ if (!memRead_ || !memWrite_ || !setA_ || !setP_) return;
252
+
253
+ uint8_t command = memRead_(0x42);
254
+ uint8_t unitNum = memRead_(0x43);
255
+ uint16_t bufPtr = memRead_(0x44) | (memRead_(0x45) << 8);
256
+ uint16_t blockNum = memRead_(0x46) | (memRead_(0x47) << 8);
257
+
258
+ // Device index from unit number bit 7
259
+ int device = (unitNum & 0x80) ? 1 : 0;
260
+
261
+ activity_ = true;
262
+ activityWrite_ = (command == 2);
263
+
264
+ switch (command) {
265
+ case 0: { // STATUS
266
+ if (!devices_[device].isLoaded()) {
267
+ setErrorResult(SP_NO_DEVICE);
268
+ return;
269
+ }
270
+ // ProDOS expects block count in X (lo) and Y (hi) registers
271
+ uint16_t blocks = devices_[device].getTotalBlocks();
272
+ if (setX_) setX_(blocks & 0xFF);
273
+ if (setY_) setY_((blocks >> 8) & 0xFF);
274
+ setErrorResult(SP_OK);
275
+ return;
276
+ }
277
+
278
+ case 1: { // READ
279
+ if (!devices_[device].isLoaded()) {
280
+ setErrorResult(SP_NO_DEVICE);
281
+ return;
282
+ }
283
+ uint8_t blockBuf[BlockDevice::BLOCK_SIZE];
284
+ if (!devices_[device].readBlock(blockNum, blockBuf)) {
285
+ setErrorResult(SP_IO_ERROR);
286
+ return;
287
+ }
288
+ for (size_t i = 0; i < BlockDevice::BLOCK_SIZE; i++) {
289
+ memWrite_(static_cast<uint16_t>(bufPtr + i), blockBuf[i]);
290
+ }
291
+ setErrorResult(SP_OK);
292
+ return;
293
+ }
294
+
295
+ case 2: { // WRITE
296
+ if (!devices_[device].isLoaded()) {
297
+ setErrorResult(SP_NO_DEVICE);
298
+ return;
299
+ }
300
+ if (devices_[device].isWriteProtected()) {
301
+ setErrorResult(SP_WRITE_PROTECTED);
302
+ return;
303
+ }
304
+ uint8_t blockBuf[BlockDevice::BLOCK_SIZE];
305
+ for (size_t i = 0; i < BlockDevice::BLOCK_SIZE; i++) {
306
+ blockBuf[i] = memRead_(static_cast<uint16_t>(bufPtr + i));
307
+ }
308
+ if (!devices_[device].writeBlock(blockNum, blockBuf)) {
309
+ setErrorResult(SP_IO_ERROR);
310
+ return;
311
+ }
312
+ setErrorResult(SP_OK);
313
+ return;
314
+ }
315
+
316
+ case 3: { // FORMAT
317
+ // We don't actually format - just return OK if device is present
318
+ if (!devices_[device].isLoaded()) {
319
+ setErrorResult(SP_NO_DEVICE);
320
+ return;
321
+ }
322
+ setErrorResult(SP_OK);
323
+ return;
324
+ }
325
+
326
+ default:
327
+ setErrorResult(SP_IO_ERROR);
328
+ return;
329
+ }
330
+ }
331
+
332
+ void SmartPortCard::handleSmartPort() {
333
+ // SmartPort call convention:
334
+ // After JSR $Cn13, the inline bytes are:
335
+ // +0: command byte
336
+ // +1,+2: parameter list pointer (lo/hi)
337
+ // We need to read these from after the JSR instruction,
338
+ // then adjust the return address on the stack by +3.
339
+ if (!memRead_ || !memWrite_ || !setA_ || !setP_ || !getSP_ || !setSP_) return;
340
+
341
+ uint8_t sp = getSP_();
342
+
343
+ // Read return address from stack (points to byte before inline params)
344
+ uint8_t retLo = memRead_(0x0100 + ((sp + 1) & 0xFF));
345
+ uint8_t retHi = memRead_(0x0100 + ((sp + 2) & 0xFF));
346
+ uint16_t retAddr = (retHi << 8) | retLo;
347
+
348
+ // Inline params start at retAddr + 1
349
+ uint16_t inlineAddr = retAddr + 1;
350
+ uint8_t command = memRead_(inlineAddr);
351
+ uint16_t paramPtr = memRead_(inlineAddr + 1) | (memRead_(inlineAddr + 2) << 8);
352
+
353
+ // Adjust return address past the 3 inline bytes
354
+ uint16_t newRet = retAddr + 3;
355
+ memWrite_(0x0100 + ((sp + 1) & 0xFF), newRet & 0xFF);
356
+ memWrite_(0x0100 + ((sp + 2) & 0xFF), (newRet >> 8) & 0xFF);
357
+
358
+ activity_ = true;
359
+ activityWrite_ = (command == 0x02);
360
+
361
+ switch (command) {
362
+ case 0x00: { // STATUS
363
+ uint8_t paramCount = memRead_(paramPtr);
364
+ uint8_t unitNum = memRead_(paramPtr + 1);
365
+ uint16_t statusBuf = memRead_(paramPtr + 2) | (memRead_(paramPtr + 3) << 8);
366
+ uint8_t statusCode = memRead_(paramPtr + 4);
367
+ (void)paramCount;
368
+
369
+ if (unitNum == 0) {
370
+ // Unit 0 STATUS: return number of devices
371
+ int count = 0;
372
+ for (int i = 0; i < MAX_DEVICES; i++) {
373
+ if (devices_[i].isLoaded()) count = i + 1;
374
+ }
375
+ // Write device count to status buffer
376
+ memWrite_(statusBuf, static_cast<uint8_t>(count));
377
+ setErrorResult(SP_OK);
378
+ return;
379
+ }
380
+
381
+ int device = unitNum - 1;
382
+ if (device < 0 || device >= MAX_DEVICES || !devices_[device].isLoaded()) {
383
+ setErrorResult(SP_NO_DEVICE);
384
+ return;
385
+ }
386
+
387
+ if (statusCode == 0x00) {
388
+ // General status: 4 bytes
389
+ uint8_t statusByte = 0xF8; // block device, read/write, online, format capable
390
+ if (devices_[device].isWriteProtected()) {
391
+ statusByte |= 0x04; // write protected
392
+ }
393
+ memWrite_(statusBuf, statusByte);
394
+ uint16_t blocks = devices_[device].getTotalBlocks();
395
+ memWrite_(statusBuf + 1, blocks & 0xFF);
396
+ memWrite_(statusBuf + 2, (blocks >> 8) & 0xFF);
397
+ memWrite_(statusBuf + 3, 0x00); // blocks high byte (always 0 for 16-bit)
398
+ }
399
+ setErrorResult(SP_OK);
400
+ return;
401
+ }
402
+
403
+ case 0x01: { // READ BLOCK
404
+ uint8_t unitNum = memRead_(paramPtr + 1);
405
+ uint16_t dataBuf = memRead_(paramPtr + 2) | (memRead_(paramPtr + 3) << 8);
406
+ uint16_t blockNum = memRead_(paramPtr + 4) | (memRead_(paramPtr + 5) << 8);
407
+
408
+ int device = unitNum - 1;
409
+ if (device < 0 || device >= MAX_DEVICES || !devices_[device].isLoaded()) {
410
+ setErrorResult(SP_NO_DEVICE);
411
+ return;
412
+ }
413
+
414
+ uint8_t blockBuf[BlockDevice::BLOCK_SIZE];
415
+ if (!devices_[device].readBlock(blockNum, blockBuf)) {
416
+ setErrorResult(SP_IO_ERROR);
417
+ return;
418
+ }
419
+ for (size_t i = 0; i < BlockDevice::BLOCK_SIZE; i++) {
420
+ memWrite_(static_cast<uint16_t>(dataBuf + i), blockBuf[i]);
421
+ }
422
+ setErrorResult(SP_OK);
423
+ return;
424
+ }
425
+
426
+ case 0x02: { // WRITE BLOCK
427
+ uint8_t unitNum = memRead_(paramPtr + 1);
428
+ uint16_t dataBuf = memRead_(paramPtr + 2) | (memRead_(paramPtr + 3) << 8);
429
+ uint16_t blockNum = memRead_(paramPtr + 4) | (memRead_(paramPtr + 5) << 8);
430
+
431
+ int device = unitNum - 1;
432
+ if (device < 0 || device >= MAX_DEVICES || !devices_[device].isLoaded()) {
433
+ setErrorResult(SP_NO_DEVICE);
434
+ return;
435
+ }
436
+ if (devices_[device].isWriteProtected()) {
437
+ setErrorResult(SP_WRITE_PROTECTED);
438
+ return;
439
+ }
440
+
441
+ uint8_t blockBuf[BlockDevice::BLOCK_SIZE];
442
+ for (size_t i = 0; i < BlockDevice::BLOCK_SIZE; i++) {
443
+ blockBuf[i] = memRead_(static_cast<uint16_t>(dataBuf + i));
444
+ }
445
+ if (!devices_[device].writeBlock(blockNum, blockBuf)) {
446
+ setErrorResult(SP_IO_ERROR);
447
+ return;
448
+ }
449
+ setErrorResult(SP_OK);
450
+ return;
451
+ }
452
+
453
+ case 0x03: { // FORMAT
454
+ uint8_t unitNum = memRead_(paramPtr + 1);
455
+ int device = unitNum - 1;
456
+ if (device < 0 || device >= MAX_DEVICES || !devices_[device].isLoaded()) {
457
+ setErrorResult(SP_NO_DEVICE);
458
+ return;
459
+ }
460
+ setErrorResult(SP_OK);
461
+ return;
462
+ }
463
+
464
+ case 0x04: // CONTROL
465
+ case 0x05: // INIT
466
+ setErrorResult(SP_OK);
467
+ return;
468
+
469
+ default:
470
+ setErrorResult(SP_IO_ERROR);
471
+ return;
472
+ }
473
+ }
474
+
475
+ // Device management
476
+
477
+ bool SmartPortCard::insertImage(int device, const uint8_t* data, size_t size, const std::string& filename) {
478
+ if (device < 0 || device >= MAX_DEVICES) return false;
479
+ return devices_[device].load(data, size, filename);
480
+ }
481
+
482
+ void SmartPortCard::ejectImage(int device) {
483
+ if (device >= 0 && device < MAX_DEVICES) {
484
+ devices_[device].eject();
485
+ }
486
+ }
487
+
488
+ bool SmartPortCard::isImageInserted(int device) const {
489
+ if (device < 0 || device >= MAX_DEVICES) return false;
490
+ return devices_[device].isLoaded();
491
+ }
492
+
493
+ const std::string& SmartPortCard::getImageFilename(int device) const {
494
+ if (device < 0 || device >= MAX_DEVICES) return emptyString_;
495
+ return devices_[device].getFilename();
496
+ }
497
+
498
+ bool SmartPortCard::isImageModified(int device) const {
499
+ if (device < 0 || device >= MAX_DEVICES) return false;
500
+ return devices_[device].isModified();
501
+ }
502
+
503
+ const uint8_t* SmartPortCard::exportImageData(int device, size_t* size) const {
504
+ if (device < 0 || device >= MAX_DEVICES) {
505
+ if (size) *size = 0;
506
+ return nullptr;
507
+ }
508
+ return devices_[device].exportData(size);
509
+ }
510
+
511
+ const uint8_t* SmartPortCard::getBlockData(int device, size_t* size) const {
512
+ if (device < 0 || device >= MAX_DEVICES) {
513
+ if (size) *size = 0;
514
+ return nullptr;
515
+ }
516
+ return devices_[device].getBlockData(size);
517
+ }
518
+
519
+ BlockDevice* SmartPortCard::getDevice(int device) {
520
+ if (device < 0 || device >= MAX_DEVICES) return nullptr;
521
+ return &devices_[device];
522
+ }
523
+
524
+ const BlockDevice* SmartPortCard::getDevice(int device) const {
525
+ if (device < 0 || device >= MAX_DEVICES) return nullptr;
526
+ return &devices_[device];
527
+ }
528
+
529
+ // State serialization
530
+
531
+ size_t SmartPortCard::getStateSize() const {
532
+ // slotNum(1) + per-device state
533
+ size_t total = 1;
534
+ for (int i = 0; i < MAX_DEVICES; i++) {
535
+ total += 4; // device state size prefix (LE32)
536
+ if (devices_[i].isLoaded()) {
537
+ total += devices_[i].getStateSize();
538
+ }
539
+ }
540
+ return total;
541
+ }
542
+
543
+ size_t SmartPortCard::serialize(uint8_t* buffer, size_t maxSize) const {
544
+ if (maxSize < 1) return 0;
545
+
546
+ size_t offset = 0;
547
+ buffer[offset++] = slotNum_;
548
+
549
+ for (int i = 0; i < MAX_DEVICES; i++) {
550
+ if (devices_[i].isLoaded()) {
551
+ size_t devStateSize = devices_[i].getStateSize();
552
+ if (offset + 4 + devStateSize > maxSize) return 0;
553
+
554
+ // Write device state size
555
+ uint32_t sz = static_cast<uint32_t>(devStateSize);
556
+ buffer[offset++] = sz & 0xFF;
557
+ buffer[offset++] = (sz >> 8) & 0xFF;
558
+ buffer[offset++] = (sz >> 16) & 0xFF;
559
+ buffer[offset++] = (sz >> 24) & 0xFF;
560
+
561
+ size_t written = devices_[i].serialize(buffer + offset, maxSize - offset);
562
+ if (written == 0) return 0;
563
+ offset += written;
564
+ } else {
565
+ if (offset + 4 > maxSize) return 0;
566
+ buffer[offset++] = 0;
567
+ buffer[offset++] = 0;
568
+ buffer[offset++] = 0;
569
+ buffer[offset++] = 0;
570
+ }
571
+ }
572
+
573
+ return offset;
574
+ }
575
+
576
+ size_t SmartPortCard::deserialize(const uint8_t* buffer, size_t size) {
577
+ if (size < 1) return 0;
578
+
579
+ size_t offset = 0;
580
+ slotNum_ = buffer[offset++];
581
+ buildROM();
582
+
583
+ for (int i = 0; i < MAX_DEVICES; i++) {
584
+ if (offset + 4 > size) return 0;
585
+
586
+ uint32_t devStateSize = buffer[offset] | (buffer[offset + 1] << 8) |
587
+ (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24);
588
+ offset += 4;
589
+
590
+ if (devStateSize > 0) {
591
+ if (offset + devStateSize > size) return 0;
592
+ size_t read = devices_[i].deserialize(buffer + offset, devStateSize);
593
+ if (read == 0) return 0;
594
+ offset += read;
595
+ } else {
596
+ devices_[i].eject();
597
+ }
598
+ }
599
+
600
+ return offset;
601
+ }
602
+
603
+ } // namespace a2e