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,568 @@
1
+ /*
2
+ * disk2_card.cpp - Disk II controller card implementation
3
+ *
4
+ * Cycle-accurate Logic State Sequencer (LSS) driven by the P6 ROM (341-0028).
5
+ * The sequencer clocks at 2x CPU rate (8 ticks per 4-cycle bit cell), with
6
+ * disk read/write occurring at phase 4 of each 8-phase cycle.
7
+ *
8
+ * Written by
9
+ * Mike Daley <michael_daley@icloud.com>
10
+ */
11
+
12
+ #include "disk2_card.hpp"
13
+ #include "../disk-image/dsk_disk_image.hpp"
14
+ #include "../disk-image/woz_disk_image.hpp"
15
+ #include <algorithm>
16
+ #include <cstring>
17
+
18
+ namespace a2e {
19
+
20
+ // ===== P6 Sequencer ROM (341-0028, 16-sector) =====
21
+ //
22
+ // De-scrambled to BAPD (Beneath Apple ProDOS) logical format.
23
+ // Source ROM CRC32: b72a2c70
24
+ //
25
+ // Address: (state << 4) | (Q7 << 3) | (Q6 << 2) | (QA << 1) | pulse
26
+ // state = 4-bit sequencer state (0-F)
27
+ // Q7/Q6 = mode select (00=read, 01=WP sense, 10=write, 11=load)
28
+ // QA = data register bit 7 (MSB feedback)
29
+ // pulse = read bit from disk (1=flux transition)
30
+ //
31
+ // Data byte: high nibble = next state, low nibble = action code
32
+ // Actions: 0-7=CLR, 8/C=NOP, 9=SL0, A/E=SR+WP, B/F=LOAD, D=SL1
33
+ //
34
+ const uint8_t Disk2Card::P6_ROM[256] = {
35
+ // State 0 State 1
36
+ 0x18,0x18,0x18,0x18,0x0A,0x0A,0x0A,0x0A, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,
37
+ 0x2D,0x2D,0x38,0x38,0x0A,0x0A,0x0A,0x0A, 0x28,0x28,0x28,0x28,0x28,0x28,0x28,0x28,
38
+ // State 2 State 3
39
+ 0xD8,0x38,0x08,0x28,0x0A,0x0A,0x0A,0x0A, 0x39,0x39,0x39,0x39,0x3B,0x3B,0x3B,0x3B,
40
+ 0xD8,0x48,0x48,0x48,0x0A,0x0A,0x0A,0x0A, 0x48,0x48,0x48,0x48,0x48,0x48,0x48,0x48,
41
+ // State 4 State 5
42
+ 0xD8,0x58,0xD8,0x58,0x0A,0x0A,0x0A,0x0A, 0x58,0x58,0x58,0x58,0x58,0x58,0x58,0x58,
43
+ 0xD8,0x68,0xD8,0x68,0x0A,0x0A,0x0A,0x0A, 0x68,0x68,0x68,0x68,0x68,0x68,0x68,0x68,
44
+ // State 6 State 7
45
+ 0xD8,0x78,0xD8,0x78,0x0A,0x0A,0x0A,0x0A, 0x78,0x78,0x78,0x78,0x78,0x78,0x78,0x78,
46
+ 0xD8,0x88,0xD8,0x88,0x0A,0x0A,0x0A,0x0A, 0x08,0x08,0x88,0x88,0x08,0x08,0x88,0x88,
47
+ // State 8 State 9
48
+ 0xD8,0x98,0xD8,0x98,0x0A,0x0A,0x0A,0x0A, 0x98,0x98,0x98,0x98,0x98,0x98,0x98,0x98,
49
+ 0xD8,0x29,0xD8,0xA8,0x0A,0x0A,0x0A,0x0A, 0xA8,0xA8,0xA8,0xA8,0xA8,0xA8,0xA8,0xA8,
50
+ // State A State B
51
+ 0xCD,0xBD,0xD8,0xB8,0x0A,0x0A,0x0A,0x0A, 0xB9,0xB9,0xB9,0xB9,0xBB,0xBB,0xBB,0xBB,
52
+ 0xD9,0x59,0xD8,0xC8,0x0A,0x0A,0x0A,0x0A, 0xC8,0xC8,0xC8,0xC8,0xC8,0xC8,0xC8,0xC8,
53
+ // State C State D
54
+ 0xD9,0xD9,0xD8,0xA0,0x0A,0x0A,0x0A,0x0A, 0xD8,0xD8,0xD8,0xD8,0xD8,0xD8,0xD8,0xD8,
55
+ 0xD8,0x08,0xE8,0xE8,0x0A,0x0A,0x0A,0x0A, 0xE8,0xE8,0xE8,0xE8,0xE8,0xE8,0xE8,0xE8,
56
+ // State E State F
57
+ 0xFD,0xFD,0xF8,0xF8,0x0A,0x0A,0x0A,0x0A, 0xF8,0xF8,0xF8,0xF8,0xF8,0xF8,0xF8,0xF8,
58
+ 0xDD,0x4D,0xE0,0xE0,0x0A,0x0A,0x0A,0x0A, 0x88,0x88,0x08,0x08,0x88,0x88,0x08,0x08,
59
+ };
60
+
61
+ Disk2Card::Disk2Card() {
62
+ rom_.fill(0xFF);
63
+ reset();
64
+ }
65
+
66
+ Disk2Card::Disk2Card(const uint8_t* rom, size_t romSize) {
67
+ rom_.fill(0xFF);
68
+ if (rom && romSize > 0) {
69
+ loadROM(rom, romSize);
70
+ }
71
+ reset();
72
+ }
73
+
74
+ void Disk2Card::loadROM(const uint8_t* rom, size_t size) {
75
+ if (rom && size > 0) {
76
+ std::memcpy(rom_.data(), rom, std::min(size, rom_.size()));
77
+ }
78
+ }
79
+
80
+ // ===== ExpansionCard Interface =====
81
+
82
+ uint8_t Disk2Card::readIO(uint8_t offset) {
83
+ return handleSoftSwitch(offset & 0x0F, false);
84
+ }
85
+
86
+ void Disk2Card::writeIO(uint8_t offset, uint8_t value) {
87
+ handleSoftSwitch(offset & 0x0F, true);
88
+ busData_ = value;
89
+ }
90
+
91
+ uint8_t Disk2Card::peekIO(uint8_t offset) const {
92
+ uint8_t off = offset & 0x0F;
93
+
94
+ switch (off) {
95
+ case PHASE0_OFF:
96
+ case PHASE0_ON:
97
+ case PHASE1_OFF:
98
+ case PHASE1_ON:
99
+ case PHASE2_OFF:
100
+ case PHASE2_ON:
101
+ case PHASE3_OFF:
102
+ case PHASE3_ON:
103
+ return (phaseStates_ >> (off / 2)) & 1 ? 0x80 : 0x00;
104
+
105
+ case MOTOR_OFF:
106
+ case MOTOR_ON:
107
+ return isMotorOn() ? 0x80 : 0x00;
108
+
109
+ case DRIVE1_SELECT:
110
+ case DRIVE2_SELECT:
111
+ return selectedDrive_ == 1 ? 0x80 : 0x00;
112
+
113
+ case Q6L:
114
+ return dataRegister_;
115
+
116
+ case Q6H:
117
+ if (hasDisk(selectedDrive_)) {
118
+ const DiskImage* disk = diskImages_[selectedDrive_].get();
119
+ return disk->isWriteProtected() ? 0x80 : 0x00;
120
+ }
121
+ return 0x00;
122
+
123
+ case Q7L:
124
+ return q7_ ? 0x00 : 0x80;
125
+
126
+ case Q7H:
127
+ return q7_ ? 0x80 : 0x00;
128
+
129
+ default:
130
+ return 0x00;
131
+ }
132
+ }
133
+
134
+ uint8_t Disk2Card::readROM(uint8_t offset) {
135
+ return rom_[offset];
136
+ }
137
+
138
+ void Disk2Card::reset() {
139
+ motorOn_ = false;
140
+ motorOffCycle_ = 0;
141
+ selectedDrive_ = 0;
142
+ q6_ = false;
143
+ q7_ = false;
144
+ phaseStates_ = 0;
145
+
146
+ totalCycles_ = 0;
147
+ sequencerState_ = 0;
148
+ dataRegister_ = 0;
149
+ lastLSSCycle_ = 0;
150
+ busData_ = 0;
151
+ lssClock_ = 0;
152
+ writeLevel_ = 0;
153
+
154
+ // Reset disk image positioning state (but preserve loaded disks)
155
+ for (int i = 0; i < 2; i++) {
156
+ if (diskImages_[i]) {
157
+ diskImages_[i]->resetState();
158
+ }
159
+ }
160
+ }
161
+
162
+ void Disk2Card::update(int cycles) {
163
+ totalCycles_ += cycles;
164
+ }
165
+
166
+ void Disk2Card::setCycleCallback(CycleCallback callback) {
167
+ cycleCallback_ = std::move(callback);
168
+ }
169
+
170
+ size_t Disk2Card::getStateSize() const {
171
+ return 32;
172
+ }
173
+
174
+ size_t Disk2Card::serialize(uint8_t* buffer, size_t maxSize) const {
175
+ if (!buffer || maxSize < 16) return 0;
176
+
177
+ size_t offset = 0;
178
+
179
+ buffer[offset++] = isMotorOn() ? 1 : 0;
180
+ buffer[offset++] = static_cast<uint8_t>(selectedDrive_);
181
+ buffer[offset++] = q6_ ? 1 : 0;
182
+ buffer[offset++] = q7_ ? 1 : 0;
183
+ buffer[offset++] = phaseStates_;
184
+ buffer[offset++] = dataRegister_;
185
+
186
+ // Track positions for both drives
187
+ int track0 = 0, track1 = 0;
188
+ if (hasDisk(0)) {
189
+ const DiskImage* img = getDiskImage(0);
190
+ if (img) track0 = img->getQuarterTrack();
191
+ }
192
+ if (hasDisk(1)) {
193
+ const DiskImage* img = getDiskImage(1);
194
+ if (img) track1 = img->getQuarterTrack();
195
+ }
196
+ buffer[offset++] = static_cast<uint8_t>(track0);
197
+ buffer[offset++] = static_cast<uint8_t>(track1);
198
+
199
+ // LSS state (new in state version 7)
200
+ buffer[offset++] = sequencerState_;
201
+ buffer[offset++] = busData_;
202
+ buffer[offset++] = lssClock_;
203
+ buffer[offset++] = writeLevel_;
204
+
205
+ return offset;
206
+ }
207
+
208
+ size_t Disk2Card::deserialize(const uint8_t* buffer, size_t size) {
209
+ if (!buffer || size < 8) return 0;
210
+
211
+ size_t offset = 0;
212
+
213
+ setMotorOn(buffer[offset++] != 0);
214
+ setSelectedDrive(buffer[offset++]);
215
+ setQ6(buffer[offset++] != 0);
216
+ setQ7(buffer[offset++] != 0);
217
+ setPhaseStates(buffer[offset++]);
218
+ setDataLatch(buffer[offset++]);
219
+
220
+ int track0 = buffer[offset++];
221
+ int track1 = buffer[offset++];
222
+
223
+ if (hasDisk(0)) {
224
+ DiskImage* img = getMutableDiskImage(0);
225
+ if (img) img->setQuarterTrack(track0);
226
+ }
227
+ if (hasDisk(1)) {
228
+ DiskImage* img = getMutableDiskImage(1);
229
+ if (img) img->setQuarterTrack(track1);
230
+ }
231
+
232
+ // LSS state (new in state version 7)
233
+ if (offset + 3 <= size) {
234
+ setSequencerState(buffer[offset++]);
235
+ setBusData(buffer[offset++]);
236
+ setLSSClock(buffer[offset++]);
237
+ }
238
+ if (offset < size) {
239
+ writeLevel_ = buffer[offset++] & 0x01;
240
+ }
241
+
242
+ return offset;
243
+ }
244
+
245
+ // ===== Disk Operations =====
246
+
247
+ bool Disk2Card::insertDisk(int drive, const uint8_t* data, size_t size,
248
+ const std::string& filename) {
249
+ if (drive < 0 || drive > 1) {
250
+ return false;
251
+ }
252
+
253
+ // Determine file type from extension
254
+ std::string lowerFilename = filename;
255
+ std::transform(lowerFilename.begin(), lowerFilename.end(),
256
+ lowerFilename.begin(), ::tolower);
257
+
258
+ std::unique_ptr<DiskImage> image;
259
+
260
+ if (lowerFilename.find(".woz") != std::string::npos) {
261
+ image = std::make_unique<WozDiskImage>();
262
+ } else if (lowerFilename.find(".dsk") != std::string::npos ||
263
+ lowerFilename.find(".do") != std::string::npos ||
264
+ lowerFilename.find(".po") != std::string::npos) {
265
+ image = std::make_unique<DskDiskImage>();
266
+ } else {
267
+ // Try to detect format from content
268
+ if (size >= 12 && data[0] == 'W' && data[1] == 'O' && data[2] == 'Z') {
269
+ image = std::make_unique<WozDiskImage>();
270
+ } else if (size == 143360) {
271
+ image = std::make_unique<DskDiskImage>();
272
+ } else {
273
+ return false;
274
+ }
275
+ }
276
+
277
+ if (!image->load(data, size, filename)) {
278
+ return false;
279
+ }
280
+
281
+ diskImages_[drive] = std::move(image);
282
+
283
+ // Reset LSS timing for this drive
284
+ if (drive == selectedDrive_) {
285
+ lastLSSCycle_ = getCycles();
286
+ }
287
+
288
+ return true;
289
+ }
290
+
291
+ bool Disk2Card::insertBlankDisk(int drive) {
292
+ if (drive < 0 || drive > 1) {
293
+ return false;
294
+ }
295
+
296
+ auto image = std::make_unique<WozDiskImage>();
297
+ image->createBlank();
298
+
299
+ diskImages_[drive] = std::move(image);
300
+ return true;
301
+ }
302
+
303
+ void Disk2Card::ejectDisk(int drive) {
304
+ if (drive < 0 || drive > 1) {
305
+ return;
306
+ }
307
+
308
+ if (selectedDrive_ == drive) {
309
+ motorOn_ = false;
310
+ motorOffCycle_ = 0;
311
+ }
312
+
313
+ diskImages_[drive].reset();
314
+ }
315
+
316
+ bool Disk2Card::hasDisk(int drive) const {
317
+ if (drive < 0 || drive > 1) {
318
+ return false;
319
+ }
320
+ return diskImages_[drive] != nullptr && diskImages_[drive]->isLoaded();
321
+ }
322
+
323
+ const uint8_t* Disk2Card::getDiskData(int drive, size_t* size) const {
324
+ if (drive < 0 || drive > 1 || !hasDisk(drive)) {
325
+ *size = 0;
326
+ return nullptr;
327
+ }
328
+ return diskImages_[drive]->getSectorData(size);
329
+ }
330
+
331
+ const uint8_t* Disk2Card::exportDiskData(int drive, size_t* size) {
332
+ if (drive < 0 || drive > 1 || !hasDisk(drive)) {
333
+ *size = 0;
334
+ return nullptr;
335
+ }
336
+ return diskImages_[drive]->exportData(size);
337
+ }
338
+
339
+ const DiskImage* Disk2Card::getDiskImage(int drive) const {
340
+ if (drive < 0 || drive > 1) {
341
+ return nullptr;
342
+ }
343
+ return diskImages_[drive].get();
344
+ }
345
+
346
+ DiskImage* Disk2Card::getMutableDiskImage(int drive) {
347
+ if (drive < 0 || drive > 1) {
348
+ return nullptr;
349
+ }
350
+ return diskImages_[drive].get();
351
+ }
352
+
353
+ bool Disk2Card::isMotorOn() const {
354
+ if (motorOn_ && motorOffCycle_ != 0) {
355
+ if (getCycles() >= motorOffCycle_ + MOTOR_OFF_DELAY_CYCLES) {
356
+ motorOn_ = false;
357
+ motorOffCycle_ = 0;
358
+ }
359
+ }
360
+ return motorOn_;
361
+ }
362
+
363
+ void Disk2Card::stopMotor() {
364
+ motorOn_ = false;
365
+ motorOffCycle_ = 0;
366
+ }
367
+
368
+ int Disk2Card::getCurrentTrack() const {
369
+ if (hasDisk(selectedDrive_)) {
370
+ return diskImages_[selectedDrive_]->getTrack();
371
+ }
372
+ return -1;
373
+ }
374
+
375
+ int Disk2Card::getQuarterTrack() const {
376
+ if (hasDisk(selectedDrive_)) {
377
+ return diskImages_[selectedDrive_]->getQuarterTrack();
378
+ }
379
+ return -1;
380
+ }
381
+
382
+ // ===== Logic State Sequencer =====
383
+
384
+ void Disk2Card::clockLSS() {
385
+ DiskImage* disk = diskImages_[selectedDrive_].get();
386
+ if (!disk || !disk->hasData()) {
387
+ if (++lssClock_ > 7) lssClock_ = 0;
388
+ return;
389
+ }
390
+
391
+ // Read pulse from disk only at phase 4 of the 8-phase clock.
392
+ // On all other phases, pulse is 0 (inverted -> 1 in address).
393
+ // When Q7=1 (write mode), pulse never affects P6 ROM output,
394
+ // so we skip the read and let writeBit handle head advance.
395
+ uint8_t readPulse = 0;
396
+ if (lssClock_ == 4 && !q7_) {
397
+ readPulse = disk->readBit(); // reads and advances head
398
+ }
399
+
400
+ // P6 ROM lookup (every tick, BAPD format with inverted pulse)
401
+ uint8_t qa = (dataRegister_ >> 7) & 1;
402
+ uint8_t addr = (sequencerState_ << 4) | (uint8_t(q7_) << 3) |
403
+ (uint8_t(q6_) << 2) | (qa << 1) | (readPulse ? 0x00 : 0x01);
404
+ uint8_t opcode = P6_ROM[addr];
405
+ uint8_t nextState = (opcode >> 4) & 0x0F;
406
+
407
+ // Execute data register action (MAME-derived P6 ROM action decoding)
408
+ switch (opcode & 0x0F) {
409
+ case 0: case 1: case 2: case 3:
410
+ case 4: case 5: case 6: case 7:
411
+ dataRegister_ = 0x00; // CLR
412
+ break;
413
+ case 0x8: case 0xC:
414
+ break; // NOP
415
+ case 0x9:
416
+ dataRegister_ <<= 1; // SL0 (shift left, 0 in)
417
+ break;
418
+ case 0xA: case 0xE:
419
+ dataRegister_ = (dataRegister_ >> 1) |
420
+ (disk->isWriteProtected() ? 0x80 : 0x00); // SR + WP sense
421
+ break;
422
+ case 0xB: case 0xF:
423
+ dataRegister_ = busData_; // LOAD from CPU bus
424
+ break;
425
+ case 0xD:
426
+ dataRegister_ = (dataRegister_ << 1) | 0x01; // SL1 (shift left, 1 in)
427
+ break;
428
+ }
429
+
430
+ // Write mode: output bit and advance head at phase 4.
431
+ // The P6 ROM state bit 3 is the write amplifier LEVEL (magnetic polarity).
432
+ // Disk formats (WOZ/DSK) store flux TRANSITIONS (1 = polarity change).
433
+ // Convert level to transition via XOR with previous level.
434
+ if (lssClock_ == 4 && q7_) {
435
+ uint8_t level = (nextState >> 3) & 1;
436
+ disk->writeBit(level ^ writeLevel_);
437
+ writeLevel_ = level;
438
+ }
439
+
440
+ sequencerState_ = nextState;
441
+ if (++lssClock_ > 7) lssClock_ = 0;
442
+ }
443
+
444
+ void Disk2Card::catchUpLSS(uint64_t currentCycle) {
445
+ if (!isMotorOn() || !hasDisk(selectedDrive_)) return;
446
+ if (currentCycle <= lastLSSCycle_) {
447
+ lastLSSCycle_ = currentCycle;
448
+ return;
449
+ }
450
+
451
+ // LSS runs at 2x CPU rate (2 ticks per CPU cycle, 8 ticks per bit cell)
452
+ uint64_t elapsedCycles = currentCycle - lastLSSCycle_;
453
+ uint64_t ticks = elapsedCycles * 2;
454
+
455
+ // Cap to approximately one disk revolution
456
+ static constexpr uint64_t MAX_CATCHUP_TICKS =
457
+ static_cast<uint64_t>(MAX_CATCHUP_BITS) * 8;
458
+ if (ticks > MAX_CATCHUP_TICKS) ticks = MAX_CATCHUP_TICKS;
459
+
460
+ for (uint64_t i = 0; i < ticks; i++) {
461
+ clockLSS();
462
+ }
463
+
464
+ lastLSSCycle_ = currentCycle;
465
+ }
466
+
467
+ // ===== Soft Switch Handler =====
468
+
469
+ uint8_t Disk2Card::handleSoftSwitch(uint8_t offset, bool isWrite) {
470
+ uint64_t currentCycle = getCycles();
471
+
472
+ auto setPhase = [this, currentCycle](int phase, bool on) {
473
+ catchUpLSS(currentCycle);
474
+ if (hasDisk(selectedDrive_)) {
475
+ diskImages_[selectedDrive_]->setPhase(phase, on);
476
+ }
477
+ };
478
+
479
+ switch (offset) {
480
+ case PHASE0_OFF:
481
+ phaseStates_ &= ~0x01;
482
+ setPhase(0, false);
483
+ break;
484
+ case PHASE0_ON:
485
+ phaseStates_ |= 0x01;
486
+ setPhase(0, true);
487
+ break;
488
+ case PHASE1_OFF:
489
+ phaseStates_ &= ~0x02;
490
+ setPhase(1, false);
491
+ break;
492
+ case PHASE1_ON:
493
+ phaseStates_ |= 0x02;
494
+ setPhase(1, true);
495
+ break;
496
+ case PHASE2_OFF:
497
+ phaseStates_ &= ~0x04;
498
+ setPhase(2, false);
499
+ break;
500
+ case PHASE2_ON:
501
+ phaseStates_ |= 0x04;
502
+ setPhase(2, true);
503
+ break;
504
+ case PHASE3_OFF:
505
+ phaseStates_ &= ~0x08;
506
+ setPhase(3, false);
507
+ break;
508
+ case PHASE3_ON:
509
+ phaseStates_ |= 0x08;
510
+ setPhase(3, true);
511
+ break;
512
+
513
+ case MOTOR_OFF:
514
+ catchUpLSS(currentCycle);
515
+ if (motorOn_ && motorOffCycle_ == 0) {
516
+ motorOffCycle_ = currentCycle;
517
+ }
518
+ return dataRegister_;
519
+
520
+ case MOTOR_ON:
521
+ motorOffCycle_ = 0;
522
+ if (!motorOn_) {
523
+ lastLSSCycle_ = currentCycle;
524
+ }
525
+ motorOn_ = true;
526
+ break;
527
+
528
+ case DRIVE1_SELECT:
529
+ catchUpLSS(currentCycle);
530
+ selectedDrive_ = 0;
531
+ break;
532
+ case DRIVE2_SELECT:
533
+ catchUpLSS(currentCycle);
534
+ selectedDrive_ = 1;
535
+ break;
536
+
537
+ case Q6L:
538
+ catchUpLSS(currentCycle);
539
+ q6_ = false;
540
+ return dataRegister_;
541
+
542
+ case Q6H:
543
+ catchUpLSS(currentCycle);
544
+ q6_ = true;
545
+ if (!q7_) {
546
+ // Sense write protect
547
+ if (hasDisk(selectedDrive_)) {
548
+ return diskImages_[selectedDrive_]->isWriteProtected() ? 0x80 : 0x00;
549
+ }
550
+ return 0x00;
551
+ }
552
+ break;
553
+
554
+ case Q7L:
555
+ catchUpLSS(currentCycle);
556
+ q7_ = false;
557
+ return dataRegister_;
558
+
559
+ case Q7H:
560
+ catchUpLSS(currentCycle);
561
+ q7_ = true;
562
+ break;
563
+ }
564
+
565
+ return 0x00;
566
+ }
567
+
568
+ } // namespace a2e