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,1387 @@
1
+ /*
2
+ * mmu.cpp - Memory management unit implementation
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ #include "mmu.hpp"
9
+ #include "../cards/expansion_card.hpp"
10
+ #include <cstring>
11
+
12
+ namespace a2e {
13
+
14
+ MMU::MMU() { reset(); }
15
+
16
+ MMU::~MMU() = default;
17
+
18
+ void MMU::reset() {
19
+ // Clear RAM
20
+ mainRAM_.fill(0);
21
+ auxRAM_.fill(0);
22
+ lcBank1_.fill(0);
23
+ lcBank2_.fill(0);
24
+ lcHighRAM_.fill(0);
25
+ auxLcBank1_.fill(0);
26
+ auxLcBank2_.fill(0);
27
+ auxLcHighRAM_.fill(0);
28
+
29
+ // Reset soft switches to default state
30
+ switches_ = SoftSwitches{};
31
+
32
+ // Keyboard
33
+ keyboardLatch_ = 0;
34
+
35
+ // Reset expansion slot state
36
+ activeExpansionSlot_ = 0;
37
+
38
+ // Reset all installed cards
39
+ for (auto& card : slots_) {
40
+ if (card) {
41
+ card->reset();
42
+ }
43
+ }
44
+
45
+ // Clear tracking (but preserve enabled state)
46
+ clearTracking();
47
+ }
48
+
49
+ void MMU::warmReset() {
50
+ // Reset soft switches to default state (preserves all RAM)
51
+ // On real Apple IIe hardware, the reset signal resets the IOU/MMU
52
+ // soft switches but does not clear memory
53
+ switches_ = SoftSwitches{};
54
+
55
+ // Keyboard
56
+ keyboardLatch_ = 0;
57
+
58
+ // Reset expansion slot state
59
+ activeExpansionSlot_ = 0;
60
+
61
+ // Reset all installed cards
62
+ for (auto& card : slots_) {
63
+ if (card) {
64
+ card->reset();
65
+ }
66
+ }
67
+ }
68
+
69
+ // ===== Expansion Slot Management =====
70
+
71
+ std::unique_ptr<ExpansionCard> MMU::insertCard(uint8_t slot, std::unique_ptr<ExpansionCard> card) {
72
+ if (slot < 1 || slot > 7) {
73
+ return card; // Invalid slot, return card unchanged
74
+ }
75
+
76
+ std::unique_ptr<ExpansionCard> previous = std::move(slots_[slot - 1]);
77
+ slots_[slot - 1] = std::move(card);
78
+
79
+ // If the removed card owned the expansion ROM, clear it
80
+ if (activeExpansionSlot_ == slot) {
81
+ activeExpansionSlot_ = 0;
82
+ }
83
+
84
+ return previous;
85
+ }
86
+
87
+ std::unique_ptr<ExpansionCard> MMU::removeCard(uint8_t slot) {
88
+ if (slot < 1 || slot > 7) {
89
+ return nullptr;
90
+ }
91
+
92
+ std::unique_ptr<ExpansionCard> card = std::move(slots_[slot - 1]);
93
+
94
+ // If this card owned the expansion ROM, clear it
95
+ if (activeExpansionSlot_ == slot) {
96
+ activeExpansionSlot_ = 0;
97
+ }
98
+
99
+ return card;
100
+ }
101
+
102
+ ExpansionCard* MMU::getCard(uint8_t slot) const {
103
+ if (slot < 1 || slot > 7) {
104
+ return nullptr;
105
+ }
106
+ return slots_[slot - 1].get();
107
+ }
108
+
109
+ bool MMU::isSlotEmpty(uint8_t slot) const {
110
+ if (slot < 1 || slot > 7) {
111
+ return true;
112
+ }
113
+ return !slots_[slot - 1];
114
+ }
115
+
116
+ void MMU::clearTracking() {
117
+ readCounts_.fill(0);
118
+ writeCounts_.fill(0);
119
+ }
120
+
121
+ void MMU::decayTracking(uint8_t amount) {
122
+ for (size_t i = 0; i < 65536; ++i) {
123
+ if (readCounts_[i] > amount) {
124
+ readCounts_[i] -= amount;
125
+ } else {
126
+ readCounts_[i] = 0;
127
+ }
128
+ if (writeCounts_[i] > amount) {
129
+ writeCounts_[i] -= amount;
130
+ } else {
131
+ writeCounts_[i] = 0;
132
+ }
133
+ }
134
+ }
135
+
136
+ void MMU::loadROM(const uint8_t *systemRom, size_t systemSize,
137
+ const uint8_t *charRom, size_t charSize) {
138
+ if (systemRom && systemSize > 0) {
139
+ std::memcpy(systemROM_.data(), systemRom,
140
+ std::min(systemSize, systemROM_.size()));
141
+ }
142
+ if (charRom && charSize > 0) {
143
+ std::memcpy(charROM_.data(), charRom, std::min(charSize, charROM_.size()));
144
+ }
145
+ }
146
+
147
+ uint8_t MMU::readCharROM(uint16_t address) const {
148
+ return charROM_[address & (CHAR_ROM_SIZE - 1)];
149
+ }
150
+
151
+ uint8_t MMU::readRAM(uint16_t address, bool aux) const {
152
+ if (aux) {
153
+ return auxRAM_[address];
154
+ }
155
+ return mainRAM_[address];
156
+ }
157
+
158
+ void MMU::writeRAM(uint16_t address, uint8_t value, bool aux) {
159
+ if (aux) {
160
+ auxRAM_[address] = value;
161
+ } else {
162
+ mainRAM_[address] = value;
163
+ }
164
+ }
165
+
166
+ uint8_t MMU::peek(uint16_t address) const {
167
+ // Non-side-effecting read for debugger/memory viewer
168
+ // Same logic as read() but without any state changes or callbacks
169
+
170
+ // Zero page and stack
171
+ if (address < 0x0200) {
172
+ if (switches_.altzp) {
173
+ return auxRAM_[address];
174
+ }
175
+ return mainRAM_[address];
176
+ }
177
+
178
+ // Main RAM: $0200-$BFFF
179
+ if (address < 0xC000) {
180
+ // Text page 1: $0400-$07FF
181
+ if (address >= 0x0400 && address < 0x0800) {
182
+ if (switches_.store80) {
183
+ return switches_.page2 ? auxRAM_[address] : mainRAM_[address];
184
+ }
185
+ return switches_.ramrd ? auxRAM_[address] : mainRAM_[address];
186
+ }
187
+
188
+ // Text page 2: $0800-$0BFF
189
+ if (address >= 0x0800 && address < 0x0C00) {
190
+ return switches_.ramrd ? auxRAM_[address] : mainRAM_[address];
191
+ }
192
+
193
+ // HiRes page 1: $2000-$3FFF
194
+ if (address >= 0x2000 && address < 0x4000) {
195
+ if (switches_.store80 && switches_.hires) {
196
+ return switches_.page2 ? auxRAM_[address] : mainRAM_[address];
197
+ }
198
+ return switches_.ramrd ? auxRAM_[address] : mainRAM_[address];
199
+ }
200
+
201
+ // HiRes page 2: $4000-$5FFF
202
+ if (address >= 0x4000 && address < 0x6000) {
203
+ return switches_.ramrd ? auxRAM_[address] : mainRAM_[address];
204
+ }
205
+
206
+ // All other main RAM
207
+ return switches_.ramrd ? auxRAM_[address] : mainRAM_[address];
208
+ }
209
+
210
+ // I/O and soft switches: $C000-$C0FF
211
+ if (address < 0xC100) {
212
+ return peekSoftSwitch(address);
213
+ }
214
+
215
+ // Slot ROM space: $C100-$CFFF
216
+ if (address < 0xD000) {
217
+ if (switches_.intcxrom) {
218
+ return systemROM_[address - 0xC000];
219
+ }
220
+
221
+ // Slot 3 ($C300-$C3FF)
222
+ if (address >= 0xC300 && address < 0xC400) {
223
+ if (!switches_.slotc3rom) {
224
+ return systemROM_[address - 0xC000];
225
+ }
226
+ return 0xFF; // No card, return high byte
227
+ }
228
+
229
+ // Slot ROM: $C100-$C7FF - check slot system
230
+ if (address < 0xC800) {
231
+ uint8_t slot = (address >> 8) & 0x07;
232
+ uint8_t offset = address & 0xFF;
233
+ if (slot >= 1 && slot <= 7 && slots_[slot - 1]) {
234
+ return slots_[slot - 1]->readROM(offset);
235
+ }
236
+ return 0xFF;
237
+ }
238
+
239
+ // $C800-$CFFF: Expansion ROM space
240
+ if (address >= 0xC800) {
241
+ if (switches_.intc8rom) {
242
+ return systemROM_[address - 0xC000];
243
+ }
244
+ return 0xFF;
245
+ }
246
+
247
+ // Other slot ROM space - no cards
248
+ return 0xFF;
249
+ }
250
+
251
+ // Language card area: $D000-$FFFF
252
+ if (switches_.lcram) {
253
+ bool useAux = switches_.altzp;
254
+ if (address < 0xE000) {
255
+ uint16_t offset = address - 0xD000;
256
+ if (switches_.lcram2) {
257
+ return useAux ? auxLcBank2_[offset] : lcBank2_[offset];
258
+ } else {
259
+ return useAux ? auxLcBank1_[offset] : lcBank1_[offset];
260
+ }
261
+ } else {
262
+ uint16_t offset = address - 0xE000;
263
+ return useAux ? auxLcHighRAM_[offset] : lcHighRAM_[offset];
264
+ }
265
+ }
266
+ return systemROM_[address - 0xC000];
267
+ }
268
+
269
+ uint8_t MMU::peekAux(uint16_t address) const {
270
+ // Direct read of auxiliary memory for text selection in 80-column mode
271
+ // This always reads from aux RAM regardless of soft switch state
272
+ return auxRAM_[address];
273
+ }
274
+
275
+ uint8_t MMU::peekSoftSwitch(uint16_t address) const {
276
+ // Non-side-effecting soft switch read for debugger
277
+ uint8_t reg = address & 0xFF;
278
+
279
+ switch (reg) {
280
+ // Keyboard - return current latch without updating
281
+ // Per AppleWin/hardware behavior, reading any address in $C000-$C00F returns keyboard latch
282
+ case 0x00:
283
+ case 0x01:
284
+ case 0x02:
285
+ case 0x03:
286
+ case 0x04:
287
+ case 0x05:
288
+ case 0x06:
289
+ case 0x07:
290
+ case 0x08:
291
+ case 0x09:
292
+ case 0x0A:
293
+ case 0x0B:
294
+ case 0x0C:
295
+ case 0x0D:
296
+ case 0x0E:
297
+ case 0x0F:
298
+ return keyboardLatch_;
299
+ case 0x10: {
300
+ // Peek returns AKD in bit 7, key code in bits 0-6 (without clearing strobe)
301
+ bool anyKeyDown = anyKeyDownCallback_ ? anyKeyDownCallback_() : false;
302
+ return (anyKeyDown ? 0x80 : 0x00) | (keyboardLatch_ & 0x7F);
303
+ }
304
+
305
+ // Memory switch status reads (these are safe - just report state)
306
+ case 0x11:
307
+ return switches_.lcram2 ? 0x80 : 0x00;
308
+ case 0x12:
309
+ return switches_.lcram ? 0x80 : 0x00;
310
+ case 0x13:
311
+ return switches_.ramrd ? 0x80 : 0x00;
312
+ case 0x14:
313
+ return switches_.ramwrt ? 0x80 : 0x00;
314
+ case 0x15:
315
+ return switches_.intcxrom ? 0x80 : 0x00;
316
+ case 0x16:
317
+ return switches_.altzp ? 0x80 : 0x00;
318
+ case 0x17:
319
+ return switches_.slotc3rom ? 0x80 : 0x00;
320
+ case 0x18:
321
+ return switches_.store80 ? 0x80 : 0x00;
322
+ case 0x19:
323
+ return 0x00; // VBL status - return arbitrary value for peek
324
+ case 0x1A:
325
+ return switches_.text ? 0x80 : 0x00;
326
+ case 0x1B:
327
+ return switches_.mixed ? 0x80 : 0x00;
328
+ case 0x1C:
329
+ return switches_.page2 ? 0x80 : 0x00;
330
+ case 0x1D:
331
+ return switches_.hires ? 0x80 : 0x00;
332
+ case 0x1E:
333
+ return switches_.altCharSet ? 0x80 : 0x00;
334
+ case 0x1F:
335
+ return switches_.col80 ? 0x80 : 0x00;
336
+
337
+ // Buttons - peek returns current state
338
+ case 0x61:
339
+ return buttonCallback_ ? (buttonCallback_(0) & 0x80) : 0x00;
340
+ case 0x62:
341
+ return buttonCallback_ ? (buttonCallback_(1) & 0x80) : 0x00;
342
+ case 0x63:
343
+ return buttonCallback_ ? (buttonCallback_(2) & 0x80) : 0x00;
344
+
345
+ // Paddle inputs
346
+ case 0x64:
347
+ case 0x65:
348
+ case 0x66:
349
+ case 0x67:
350
+ return 0x00;
351
+
352
+ // Slot I/O space: $C090-$C0FF (slots 1-7)
353
+ case 0x90: case 0x91: case 0x92: case 0x93: case 0x94: case 0x95: case 0x96: case 0x97:
354
+ case 0x98: case 0x99: case 0x9A: case 0x9B: case 0x9C: case 0x9D: case 0x9E: case 0x9F:
355
+ case 0xA0: case 0xA1: case 0xA2: case 0xA3: case 0xA4: case 0xA5: case 0xA6: case 0xA7:
356
+ case 0xA8: case 0xA9: case 0xAA: case 0xAB: case 0xAC: case 0xAD: case 0xAE: case 0xAF:
357
+ case 0xB0: case 0xB1: case 0xB2: case 0xB3: case 0xB4: case 0xB5: case 0xB6: case 0xB7:
358
+ case 0xB8: case 0xB9: case 0xBA: case 0xBB: case 0xBC: case 0xBD: case 0xBE: case 0xBF:
359
+ case 0xC0: case 0xC1: case 0xC2: case 0xC3: case 0xC4: case 0xC5: case 0xC6: case 0xC7:
360
+ case 0xC8: case 0xC9: case 0xCA: case 0xCB: case 0xCC: case 0xCD: case 0xCE: case 0xCF:
361
+ case 0xD0: case 0xD1: case 0xD2: case 0xD3: case 0xD4: case 0xD5: case 0xD6: case 0xD7:
362
+ case 0xD8: case 0xD9: case 0xDA: case 0xDB: case 0xDC: case 0xDD: case 0xDE: case 0xDF:
363
+ case 0xE0: case 0xE1: case 0xE2: case 0xE3: case 0xE4: case 0xE5: case 0xE6: case 0xE7:
364
+ case 0xE8: case 0xE9: case 0xEA: case 0xEB: case 0xEC: case 0xED: case 0xEE: case 0xEF:
365
+ case 0xF0: case 0xF1: case 0xF2: case 0xF3: case 0xF4: case 0xF5: case 0xF6: case 0xF7:
366
+ case 0xF8: case 0xF9: case 0xFA: case 0xFB: case 0xFC: case 0xFD: case 0xFE: case 0xFF: {
367
+ // Calculate slot number
368
+ uint8_t slot = ((reg - 0x80) >> 4);
369
+ uint8_t offset = reg & 0x0F;
370
+
371
+ if (slot >= 1 && slot <= 7 && slots_[slot - 1]) {
372
+ return slots_[slot - 1]->peekIO(offset);
373
+ }
374
+
375
+ return 0x00;
376
+ }
377
+
378
+ // Language card switches - report switch state
379
+ case 0x80:
380
+ case 0x81:
381
+ case 0x82:
382
+ case 0x83:
383
+ case 0x84:
384
+ case 0x85:
385
+ case 0x86:
386
+ case 0x87:
387
+ case 0x88:
388
+ case 0x89:
389
+ case 0x8A:
390
+ case 0x8B:
391
+ case 0x8C:
392
+ case 0x8D:
393
+ case 0x8E:
394
+ case 0x8F:
395
+ return switches_.lcram ? 0x80 : 0x00;
396
+
397
+ default:
398
+ return 0x00;
399
+ }
400
+ }
401
+
402
+ uint8_t MMU::read(uint16_t address) {
403
+ // Track read access
404
+ if (trackingEnabled_ && readCounts_[address] < 255) {
405
+ ++readCounts_[address];
406
+ }
407
+
408
+ // Watchpoint check on read
409
+ if (watchpointsActive_ && watchpointReadCallback_) {
410
+ // Determine value without side effects for the callback
411
+ uint8_t val = peek(address);
412
+ watchpointReadCallback_(address, val);
413
+ }
414
+
415
+ // Zero page and stack
416
+ if (address < 0x0200) {
417
+ if (switches_.altzp) {
418
+ return auxRAM_[address];
419
+ }
420
+ return mainRAM_[address];
421
+ }
422
+
423
+ // Main RAM: $0200-$BFFF
424
+ if (address < 0xC000) {
425
+ // Text page 1: $0400-$07FF
426
+ if (address >= 0x0400 && address < 0x0800) {
427
+ if (switches_.store80) {
428
+ // 80STORE on: PAGE2 controls main/aux, RAMRD is ignored
429
+ return switches_.page2 ? auxRAM_[address] : mainRAM_[address];
430
+ }
431
+ // 80STORE off: RAMRD controls main/aux
432
+ return switches_.ramrd ? auxRAM_[address] : mainRAM_[address];
433
+ }
434
+
435
+ // Text page 2: $0800-$0BFF
436
+ if (address >= 0x0800 && address < 0x0C00) {
437
+ if (switches_.ramrd) {
438
+ return auxRAM_[address];
439
+ }
440
+ return mainRAM_[address];
441
+ }
442
+
443
+ // HiRes page 1: $2000-$3FFF
444
+ if (address >= 0x2000 && address < 0x4000) {
445
+ if (switches_.store80 && switches_.hires) {
446
+ // 80STORE+HIRES on: PAGE2 controls main/aux, RAMRD is ignored
447
+ return switches_.page2 ? auxRAM_[address] : mainRAM_[address];
448
+ }
449
+ // 80STORE off or HIRES off: RAMRD controls main/aux
450
+ return switches_.ramrd ? auxRAM_[address] : mainRAM_[address];
451
+ }
452
+
453
+ // HiRes page 2: $4000-$5FFF
454
+ if (address >= 0x4000 && address < 0x6000) {
455
+ if (switches_.ramrd) {
456
+ return auxRAM_[address];
457
+ }
458
+ return mainRAM_[address];
459
+ }
460
+
461
+ // All other main RAM
462
+ if (switches_.ramrd) {
463
+ return auxRAM_[address];
464
+ }
465
+ return mainRAM_[address];
466
+ }
467
+
468
+ // I/O and soft switches: $C000-$C0FF
469
+ if (address < 0xC100) {
470
+ return readSoftSwitch(address);
471
+ }
472
+
473
+ // Slot ROM space: $C100-$CFFF
474
+ if (address < 0xD000) {
475
+ // When INTCXROM is ON, all of $C100-$CFFF uses internal ROM
476
+ if (switches_.intcxrom) {
477
+ return systemROM_[address - 0xC000];
478
+ }
479
+
480
+ // INTCXROM is OFF - use slot ROMs
481
+
482
+ // Slot 3 ($C300-$C3FF) has special handling via SLOTC3ROM
483
+ if (address >= 0xC300 && address < 0xC400) {
484
+ if (!switches_.slotc3rom) {
485
+ // SLOTC3ROM off: use internal ROM for slot 3
486
+ // Also activates internal ROM for $C800-$CFFF
487
+ switches_.intc8rom = true;
488
+ return systemROM_[address - 0xC000];
489
+ }
490
+ // SLOTC3ROM on: use slot 3 ROM (no card, return floating bus)
491
+ return getFloatingBusValue();
492
+ }
493
+
494
+ // Slot ROM space: $C100-$C7FF
495
+ // Each slot gets 256 bytes: slot N at $CN00-$CNFF
496
+ if (address < 0xC800) {
497
+ uint8_t slot = (address >> 8) & 0x07;
498
+ uint8_t offset = address & 0xFF;
499
+
500
+ // Access to slot ROM activates that card's expansion ROM
501
+ if (slot >= 1 && slot <= 7 && slots_[slot - 1]) {
502
+ activeExpansionSlot_ = slot;
503
+ return slots_[slot - 1]->readROM(offset);
504
+ }
505
+
506
+ return getFloatingBusValue();
507
+ }
508
+
509
+ // $C800-$CFFF: Expansion ROM space
510
+
511
+ // Return internal ROM if slot 3 internal ROM was accessed
512
+ if (switches_.intc8rom) {
513
+ // Access to $CFFF clears the internal ROM select AFTER the read
514
+ if (address == 0xCFFF) {
515
+ uint8_t value = systemROM_[address - 0xC000];
516
+ switches_.intc8rom = false;
517
+ activeExpansionSlot_ = 0;
518
+ return value;
519
+ }
520
+ return systemROM_[address - 0xC000];
521
+ }
522
+
523
+ // Check if a card owns the expansion ROM space
524
+ if (activeExpansionSlot_ >= 1 && activeExpansionSlot_ <= 7) {
525
+ auto& card = slots_[activeExpansionSlot_ - 1];
526
+ if (card && card->hasExpansionROM()) {
527
+ uint8_t value = card->readExpansionROM(address - 0xC800);
528
+ // Access to $CFFF clears the expansion ROM select AFTER the read
529
+ if (address == 0xCFFF) {
530
+ activeExpansionSlot_ = 0;
531
+ }
532
+ return value;
533
+ }
534
+ }
535
+
536
+ // Access to $CFFF with no active expansion ROM still clears the select
537
+ if (address == 0xCFFF) {
538
+ switches_.intc8rom = false;
539
+ activeExpansionSlot_ = 0;
540
+ }
541
+
542
+ // No expansion ROM active, return floating bus
543
+ return getFloatingBusValue();
544
+ }
545
+
546
+ // Language card area: $D000-$FFFF
547
+ return readLanguageCard(address);
548
+ }
549
+
550
+ void MMU::write(uint16_t address, uint8_t value) {
551
+ // Track write access
552
+ if (trackingEnabled_ && writeCounts_[address] < 255) {
553
+ ++writeCounts_[address];
554
+ }
555
+
556
+ // Watchpoint check on write
557
+ if (watchpointsActive_ && watchpointWriteCallback_) {
558
+ watchpointWriteCallback_(address, value);
559
+ }
560
+
561
+ // Zero page and stack
562
+ if (address < 0x0200) {
563
+ if (switches_.altzp) {
564
+ auxRAM_[address] = value;
565
+ } else {
566
+ mainRAM_[address] = value;
567
+ }
568
+ return;
569
+ }
570
+
571
+ // Main RAM: $0200-$BFFF
572
+ if (address < 0xC000) {
573
+ // Text page 1: $0400-$07FF
574
+ if (address >= 0x0400 && address < 0x0800) {
575
+ if (switches_.store80) {
576
+ // 80STORE on: PAGE2 controls main/aux, RAMWRT is ignored
577
+ if (switches_.page2) {
578
+ auxRAM_[address] = value;
579
+ } else {
580
+ mainRAM_[address] = value;
581
+ }
582
+ return;
583
+ }
584
+ // 80STORE off: RAMWRT controls main/aux
585
+ if (switches_.ramwrt) {
586
+ auxRAM_[address] = value;
587
+ } else {
588
+ mainRAM_[address] = value;
589
+ }
590
+ return;
591
+ }
592
+
593
+ // Text page 2: $0800-$0BFF
594
+ if (address >= 0x0800 && address < 0x0C00) {
595
+ if (switches_.ramwrt) {
596
+ auxRAM_[address] = value;
597
+ } else {
598
+ mainRAM_[address] = value;
599
+ }
600
+ return;
601
+ }
602
+
603
+ // HiRes page 1: $2000-$3FFF
604
+ if (address >= 0x2000 && address < 0x4000) {
605
+ if (switches_.store80 && switches_.hires) {
606
+ // 80STORE+HIRES on: PAGE2 controls main/aux, RAMWRT is ignored
607
+ if (switches_.page2) {
608
+ auxRAM_[address] = value;
609
+ } else {
610
+ mainRAM_[address] = value;
611
+ }
612
+ return;
613
+ }
614
+ // 80STORE off or HIRES off: RAMWRT controls main/aux
615
+ if (switches_.ramwrt) {
616
+ auxRAM_[address] = value;
617
+ } else {
618
+ mainRAM_[address] = value;
619
+ }
620
+ return;
621
+ }
622
+
623
+ // HiRes page 2: $4000-$5FFF
624
+ if (address >= 0x4000 && address < 0x6000) {
625
+ if (switches_.ramwrt) {
626
+ auxRAM_[address] = value;
627
+ } else {
628
+ mainRAM_[address] = value;
629
+ }
630
+ return;
631
+ }
632
+
633
+ // All other main RAM
634
+ if (switches_.ramwrt) {
635
+ auxRAM_[address] = value;
636
+ } else {
637
+ mainRAM_[address] = value;
638
+ }
639
+ return;
640
+ }
641
+
642
+ // I/O and soft switches: $C000-$C0FF
643
+ if (address < 0xC100) {
644
+ writeSoftSwitch(address, value);
645
+ return;
646
+ }
647
+
648
+ // Slot ROM space: $C100-$CFFF
649
+ // Most cards don't handle writes, but some (like Mockingboard) use ROM space for I/O
650
+ if (address < 0xD000) {
651
+ // Access to $CFFF clears the expansion ROM select
652
+ if (address == 0xCFFF) {
653
+ switches_.intc8rom = false;
654
+ activeExpansionSlot_ = 0;
655
+ }
656
+
657
+ // Route writes to slot ROM space through cards
658
+ if (address < 0xC800) {
659
+ uint8_t slot = (address >> 8) & 0x07;
660
+ uint8_t offset = address & 0xFF;
661
+
662
+ if (slot >= 1 && slot <= 7 && slots_[slot - 1]) {
663
+ slots_[slot - 1]->writeROM(offset, value);
664
+ }
665
+ }
666
+ return;
667
+ }
668
+
669
+ // Language card area: $D000-$FFFF
670
+ writeLanguageCard(address, value);
671
+ }
672
+
673
+ uint8_t MMU::getFloatingBusValue() {
674
+ // The floating bus returns whatever byte the video hardware is currently
675
+ // reading. This is determined by the current scanline and horizontal position
676
+ // within the frame.
677
+ //
678
+ // Apple IIe timing:
679
+ // - 65 cycles per scanline (25 hblank + 40 visible)
680
+ // - 262 scanlines per frame (192 visible + 70 vertical blank)
681
+ // - Cycles 0-24: horizontal blanking, cycles 25-64: visible display
682
+
683
+ if (!cycleCallback_) {
684
+ return 0x00;
685
+ }
686
+
687
+ uint64_t cycles = cycleCallback_();
688
+ uint32_t frameCycle = cycles % CYCLES_PER_FRAME;
689
+ uint32_t scanline = frameCycle / CYCLES_PER_SCANLINE;
690
+ uint32_t hPos = frameCycle % CYCLES_PER_SCANLINE;
691
+
692
+ // During horizontal blank (cycles 0-24), video reads from
693
+ // unpredictable locations. Return 0 for simplicity during hblank.
694
+ if (hPos < 25) {
695
+ return 0x00;
696
+ }
697
+
698
+ // Convert to visible column (0-39)
699
+ uint32_t col = hPos - 25;
700
+
701
+ // During vertical blank (scanlines 192-261), return data from the
702
+ // last few lines' worth of addresses (video wraps during vblank)
703
+ if (scanline >= 192) {
704
+ scanline = scanline % 192;
705
+ }
706
+
707
+ // Calculate memory address based on current video mode
708
+ uint16_t address;
709
+
710
+ if (switches_.text || !switches_.hires) {
711
+ // Text mode or LoRes mode - reads from text page
712
+ // Each scanline covers 8 text rows due to character height
713
+ int textRow = scanline / 8;
714
+ if (textRow >= 24)
715
+ textRow = 23;
716
+
717
+ // Text/LoRes base address
718
+ uint16_t base = switches_.page2 ? 0x0800 : 0x0400;
719
+
720
+ // Apple II text memory is interleaved in groups of 8 rows
721
+ // Rows 0-7: $000, $080, $100, $180, $200, $280, $300, $380
722
+ // Rows 8-15: $028, $0A8, $128, $1A8, $228, $2A8, $328, $3A8
723
+ // Rows 16-23: $050, $0D0, $150, $1D0, $250, $2D0, $350, $3D0
724
+ static const uint16_t rowOffsets[24] = {
725
+ 0x000, 0x080, 0x100, 0x180, 0x200, 0x280, 0x300, 0x380,
726
+ 0x028, 0x0A8, 0x128, 0x1A8, 0x228, 0x2A8, 0x328, 0x3A8,
727
+ 0x050, 0x0D0, 0x150, 0x1D0, 0x250, 0x2D0, 0x350, 0x3D0};
728
+
729
+ address = base + rowOffsets[textRow] + col;
730
+ } else {
731
+ // HiRes mode - reads from hi-res page
732
+ uint16_t base = switches_.page2 ? 0x4000 : 0x2000;
733
+
734
+ // Hi-res memory is also interleaved
735
+ // Each group of 64 lines shares a similar pattern
736
+ int group = scanline / 64; // 0, 1, or 2
737
+ int lineInGroup = scanline % 64; // 0-63
738
+ int subGroup = lineInGroup / 8; // 0-7
739
+ int lineInSubGroup = lineInGroup % 8;
740
+
741
+ // Calculate offset within page
742
+ // Lines 0,8,16,24,32,40,48,56 of each group start at group*$28
743
+ // Plus $80 for each subgroup, plus $400 for each line within subgroup
744
+ uint16_t offset = (group * 0x28) + (subGroup * 0x80) + (lineInSubGroup * 0x400);
745
+ address = base + offset + col;
746
+ }
747
+
748
+ // Read from the appropriate memory bank
749
+ if (switches_.store80 && switches_.page2) {
750
+ // When 80STORE is on and PAGE2, read from aux memory for display pages
751
+ if ((address >= 0x0400 && address < 0x0800) ||
752
+ (switches_.hires && address >= 0x2000 && address < 0x4000)) {
753
+ return auxRAM_[address];
754
+ }
755
+ }
756
+
757
+ if (switches_.ramrd) {
758
+ return auxRAM_[address];
759
+ }
760
+ return mainRAM_[address];
761
+ }
762
+
763
+ uint8_t MMU::readSoftSwitch(uint16_t address) {
764
+ uint8_t reg = address & 0xFF;
765
+
766
+ switch (reg) {
767
+ // Keyboard
768
+ case 0x00: // KEYBOARD
769
+ if (keyboardCallback_) {
770
+ keyboardLatch_ = keyboardCallback_();
771
+ }
772
+ return keyboardLatch_;
773
+
774
+ case 0x10: { // KBDSTRB - clear keyboard strobe, return any-key-down status
775
+ if (keyStrobeCallback_) {
776
+ keyStrobeCallback_();
777
+ }
778
+ keyboardLatch_ &= 0x7F;
779
+ // Bit 7 = any key down status, bits 0-6 = key code
780
+ bool anyKeyDown = anyKeyDownCallback_ ? anyKeyDownCallback_() : false;
781
+ return (anyKeyDown ? 0x80 : 0x00) | (keyboardLatch_ & 0x7F);
782
+ }
783
+
784
+ // Memory switches - reading returns switch state in bit 7, floating bus in bits 0-6
785
+ case 0x11:
786
+ return (switches_.lcram2 ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RDLCBNK2
787
+ case 0x12:
788
+ return (switches_.lcram ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RDLCRAM
789
+ case 0x13:
790
+ return (switches_.ramrd ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RDRAMRD
791
+ case 0x14:
792
+ return (switches_.ramwrt ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RDRAMWRT
793
+ case 0x15:
794
+ return (switches_.intcxrom ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RDCXROM
795
+ case 0x16:
796
+ return (switches_.altzp ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RDALTZP
797
+ case 0x17:
798
+ return (switches_.slotc3rom ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RDC3ROM
799
+ case 0x18:
800
+ return (switches_.store80 ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RD80STORE
801
+ case 0x19: { // RDVBLBAR - vertical blank status
802
+ // Bit 7 = 0 during vertical blank (scanlines 192-261), 1 during active display
803
+ uint64_t cycles = cycleCallback_ ? cycleCallback_() : 0;
804
+ uint32_t scanline = (cycles % CYCLES_PER_FRAME) / CYCLES_PER_SCANLINE;
805
+ bool inVBL = (scanline >= 192);
806
+ return (inVBL ? 0x00 : 0x80) | (getFloatingBusValue() & 0x7F);
807
+ }
808
+ case 0x1A:
809
+ return (switches_.text ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RDTEXT
810
+ case 0x1B:
811
+ return (switches_.mixed ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RDMIXED
812
+ case 0x1C:
813
+ return (switches_.page2 ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RDPAGE2
814
+ case 0x1D:
815
+ return (switches_.hires ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RDHIRES
816
+ case 0x1E:
817
+ return (switches_.altCharSet ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RDALTCHAR
818
+ case 0x1F:
819
+ return (switches_.col80 ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F); // RD80COL
820
+
821
+ // Cassette output toggle ($C020)
822
+ case 0x20: // CASSETTE OUT - toggle cassette output
823
+ switches_.cassetteOut = !switches_.cassetteOut;
824
+ return getFloatingBusValue();
825
+
826
+ // Unused/reserved ($C021-$C02F)
827
+ case 0x21: case 0x22: case 0x23: case 0x24: case 0x25: case 0x26: case 0x27:
828
+ case 0x28: case 0x29: case 0x2A: case 0x2B: case 0x2C: case 0x2D: case 0x2E: case 0x2F:
829
+ return getFloatingBusValue();
830
+
831
+ // Speaker - returns floating bus
832
+ case 0x30: // SPKR
833
+ if (speakerCallback_) {
834
+ speakerCallback_();
835
+ }
836
+ return getFloatingBusValue();
837
+
838
+ // Unused/reserved ($C031-$C03F)
839
+ case 0x31: case 0x32: case 0x33: case 0x34: case 0x35: case 0x36: case 0x37:
840
+ case 0x38: case 0x39: case 0x3A: case 0x3B: case 0x3C: case 0x3D: case 0x3E: case 0x3F:
841
+ return getFloatingBusValue();
842
+
843
+ // Utility strobe ($C040)
844
+ case 0x40: // STROBE - utility strobe
845
+ return getFloatingBusValue();
846
+
847
+ // Reserved ($C041-$C04F)
848
+ case 0x41: case 0x42: case 0x43: case 0x44: case 0x45: case 0x46: case 0x47:
849
+ case 0x48: case 0x49: case 0x4A: case 0x4B: case 0x4C: case 0x4D: case 0x4E: case 0x4F:
850
+ return getFloatingBusValue();
851
+
852
+ // Annunciators - return floating bus
853
+ case 0x58:
854
+ switches_.an0 = false;
855
+ return getFloatingBusValue();
856
+ case 0x59:
857
+ switches_.an0 = true;
858
+ return getFloatingBusValue();
859
+ case 0x5A:
860
+ switches_.an1 = false;
861
+ return getFloatingBusValue();
862
+ case 0x5B:
863
+ switches_.an1 = true;
864
+ return getFloatingBusValue();
865
+ case 0x5C:
866
+ switches_.an2 = false;
867
+ return getFloatingBusValue();
868
+ case 0x5D:
869
+ switches_.an2 = true;
870
+ return getFloatingBusValue();
871
+ case 0x5E:
872
+ switches_.an3 = false;
873
+ if (videoSwitchCallback_) videoSwitchCallback_();
874
+ return getFloatingBusValue(); // AN3 OFF = DHIRES enabled
875
+ case 0x5F:
876
+ switches_.an3 = true;
877
+ if (videoSwitchCallback_) videoSwitchCallback_();
878
+ return getFloatingBusValue(); // AN3 ON = DHIRES disabled
879
+
880
+ // Display switches - reading also sets the switch, returns floating bus
881
+ case 0x50:
882
+ switches_.text = false;
883
+ if (videoSwitchCallback_) videoSwitchCallback_();
884
+ return getFloatingBusValue(); // TXTCLR (graphics)
885
+ case 0x51:
886
+ switches_.text = true;
887
+ if (videoSwitchCallback_) videoSwitchCallback_();
888
+ return getFloatingBusValue(); // TXTSET (text)
889
+ case 0x52:
890
+ switches_.mixed = false;
891
+ if (videoSwitchCallback_) videoSwitchCallback_();
892
+ return getFloatingBusValue(); // MIXCLR
893
+ case 0x53:
894
+ switches_.mixed = true;
895
+ if (videoSwitchCallback_) videoSwitchCallback_();
896
+ return getFloatingBusValue(); // MIXSET
897
+ case 0x54:
898
+ switches_.page2 = false;
899
+ if (videoSwitchCallback_) videoSwitchCallback_();
900
+ return getFloatingBusValue(); // LOWSCR (page 1)
901
+ case 0x55:
902
+ switches_.page2 = true;
903
+ if (videoSwitchCallback_) videoSwitchCallback_();
904
+ return getFloatingBusValue(); // HISCR (page 2)
905
+ case 0x56:
906
+ switches_.hires = false;
907
+ if (videoSwitchCallback_) videoSwitchCallback_();
908
+ return getFloatingBusValue(); // LORES
909
+ case 0x57:
910
+ switches_.hires = true;
911
+ if (videoSwitchCallback_) videoSwitchCallback_();
912
+ return getFloatingBusValue(); // HIRES
913
+
914
+ // 80-column / memory switches (IIe specific)
915
+ // $C000-$C00F are WRITE-ONLY switches - writes modify state, reads return keyboard data
916
+ // Per AppleWin/hardware behavior, reading any address in $C000-$C00F returns keyboard latch
917
+ case 0x01: // 80STORE on (write-only)
918
+ case 0x02: // RAMRD off (write-only)
919
+ case 0x03: // RAMRD on (write-only)
920
+ case 0x04: // RAMWRT off (write-only)
921
+ case 0x05: // RAMWRT on (write-only)
922
+ case 0x06: // INTCXROM off (write-only)
923
+ case 0x07: // INTCXROM on (write-only)
924
+ case 0x08: // ALTZP off (write-only)
925
+ case 0x09: // ALTZP on (write-only)
926
+ case 0x0A: // SLOTC3ROM off (write-only)
927
+ case 0x0B: // SLOTC3ROM on (write-only)
928
+ case 0x0C: // 80COL off (write-only)
929
+ case 0x0D: // 80COL on (write-only)
930
+ case 0x0E: // ALTCHAR off (write-only)
931
+ case 0x0F: // ALTCHAR on (write-only)
932
+ return keyboardLatch_;
933
+
934
+ // Cassette input ($C060)
935
+ case 0x60: // CASSETTE IN - cassette input (active high)
936
+ // Always return low (no cassette) - bit 7 indicates cassette signal
937
+ return getFloatingBusValue() & 0x7F;
938
+
939
+ // Buttons (Open Apple, Closed Apple, Button 2) - bit 7 = state, bits 0-6 = floating bus
940
+ case 0x61: // PB0 / Open Apple
941
+ if (buttonCallback_) {
942
+ return (buttonCallback_(0) & 0x80) | (getFloatingBusValue() & 0x7F);
943
+ }
944
+ return getFloatingBusValue() & 0x7F;
945
+ case 0x62: // PB1 / Closed Apple
946
+ if (buttonCallback_) {
947
+ return (buttonCallback_(1) & 0x80) | (getFloatingBusValue() & 0x7F);
948
+ }
949
+ return getFloatingBusValue() & 0x7F;
950
+ case 0x63: // PB2 / Shift key modifier
951
+ if (buttonCallback_) {
952
+ return (buttonCallback_(2) & 0x80) | (getFloatingBusValue() & 0x7F);
953
+ }
954
+ return getFloatingBusValue() & 0x7F;
955
+
956
+ // Paddle inputs - bit 7 = timer status, bits 0-6 = floating bus
957
+ case 0x64: // PDL0 (joystick X)
958
+ case 0x65: // PDL1 (joystick Y)
959
+ case 0x66: // PDL2
960
+ case 0x67: { // PDL3
961
+ int paddle = reg - 0x64;
962
+ uint64_t currentCycle = cycleCallback_ ? cycleCallback_() : 0;
963
+ uint64_t elapsedCycles = currentCycle - paddleTriggerCycle_;
964
+ uint64_t timerDuration = paddleValues_[paddle] * PADDLE_CYCLES_PER_UNIT;
965
+ // Bit 7 = 1 while timer is running, 0 when expired
966
+ bool timerRunning = (elapsedCycles < timerDuration);
967
+ return (timerRunning ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F);
968
+ }
969
+
970
+ // Paddle trigger reset - starts all paddle timers
971
+ case 0x70: // PTRIG - reset paddle timers
972
+ paddleTriggerCycle_ = cycleCallback_ ? cycleCallback_() : 0;
973
+ return getFloatingBusValue();
974
+
975
+ // Reserved/unused ($C068-$C06F) - State register on IIc
976
+ case 0x68: // STATEREG (IIc only) - on IIe returns floating bus
977
+ case 0x69: case 0x6A: case 0x6B: case 0x6C: case 0x6D: case 0x6E: case 0x6F:
978
+ return getFloatingBusValue();
979
+
980
+ // Bank switch registers ($C071-$C07E) - mostly IIc/IIgs specific
981
+ case 0x71: case 0x72: case 0x73: case 0x74: case 0x75: case 0x76: case 0x77:
982
+ case 0x78: case 0x79: case 0x7A: case 0x7B: case 0x7C: case 0x7D: case 0x7E:
983
+ return getFloatingBusValue();
984
+
985
+ // IOUDIS ($C07F) - IOU disable (IIc specific, ignored on IIe)
986
+ case 0x7F:
987
+ // On IIe, reading $C07F returns DHIRES status in bit 7 (same as AN3 inverted)
988
+ return (!switches_.an3 ? 0x80 : 0x00) | (getFloatingBusValue() & 0x7F);
989
+
990
+ // Slot I/O space: $C090-$C0FF (slots 1-7)
991
+ // Each slot gets 16 bytes: slot N at $C080 + (N * 16)
992
+ case 0x90: case 0x91: case 0x92: case 0x93: case 0x94: case 0x95: case 0x96: case 0x97:
993
+ case 0x98: case 0x99: case 0x9A: case 0x9B: case 0x9C: case 0x9D: case 0x9E: case 0x9F:
994
+ case 0xA0: case 0xA1: case 0xA2: case 0xA3: case 0xA4: case 0xA5: case 0xA6: case 0xA7:
995
+ case 0xA8: case 0xA9: case 0xAA: case 0xAB: case 0xAC: case 0xAD: case 0xAE: case 0xAF:
996
+ case 0xB0: case 0xB1: case 0xB2: case 0xB3: case 0xB4: case 0xB5: case 0xB6: case 0xB7:
997
+ case 0xB8: case 0xB9: case 0xBA: case 0xBB: case 0xBC: case 0xBD: case 0xBE: case 0xBF:
998
+ case 0xC0: case 0xC1: case 0xC2: case 0xC3: case 0xC4: case 0xC5: case 0xC6: case 0xC7:
999
+ case 0xC8: case 0xC9: case 0xCA: case 0xCB: case 0xCC: case 0xCD: case 0xCE: case 0xCF:
1000
+ case 0xD0: case 0xD1: case 0xD2: case 0xD3: case 0xD4: case 0xD5: case 0xD6: case 0xD7:
1001
+ case 0xD8: case 0xD9: case 0xDA: case 0xDB: case 0xDC: case 0xDD: case 0xDE: case 0xDF:
1002
+ case 0xE0: case 0xE1: case 0xE2: case 0xE3: case 0xE4: case 0xE5: case 0xE6: case 0xE7:
1003
+ case 0xE8: case 0xE9: case 0xEA: case 0xEB: case 0xEC: case 0xED: case 0xEE: case 0xEF:
1004
+ case 0xF0: case 0xF1: case 0xF2: case 0xF3: case 0xF4: case 0xF5: case 0xF6: case 0xF7:
1005
+ case 0xF8: case 0xF9: case 0xFA: case 0xFB: case 0xFC: case 0xFD: case 0xFE: case 0xFF: {
1006
+ // Calculate slot number: $C090 = slot 1, $C0A0 = slot 2, etc.
1007
+ uint8_t slot = ((reg - 0x80) >> 4);
1008
+ uint8_t offset = reg & 0x0F;
1009
+
1010
+ if (slot >= 1 && slot <= 7 && slots_[slot - 1]) {
1011
+ return slots_[slot - 1]->readIO(offset);
1012
+ }
1013
+
1014
+ return getFloatingBusValue();
1015
+ }
1016
+
1017
+ // Language card
1018
+ case 0x80:
1019
+ case 0x81:
1020
+ case 0x82:
1021
+ case 0x83:
1022
+ case 0x84:
1023
+ case 0x85:
1024
+ case 0x86:
1025
+ case 0x87:
1026
+ case 0x88:
1027
+ case 0x89:
1028
+ case 0x8A:
1029
+ case 0x8B:
1030
+ case 0x8C:
1031
+ case 0x8D:
1032
+ case 0x8E:
1033
+ case 0x8F:
1034
+ return handleLanguageCardSwitch(reg);
1035
+
1036
+ default:
1037
+ // Unimplemented soft switches return floating bus value
1038
+ return getFloatingBusValue();
1039
+ }
1040
+
1041
+ return 0x00;
1042
+ }
1043
+
1044
+ uint8_t MMU::handleLanguageCardSwitch(uint8_t reg) {
1045
+ // Language card switches at $C080-$C08F
1046
+ // Bit 3: bank select (0 = bank 2, 1 = bank 1)
1047
+ // Bits 0-1 determine read source and write enable:
1048
+ // 0: Read RAM, write disabled
1049
+ // 1: Read ROM, write enabled (after 2 reads)
1050
+ // 2: Read ROM, write disabled
1051
+ // 3: Read RAM, write enabled (after 2 reads)
1052
+
1053
+ bool bank2 = !(reg & 0x08);
1054
+ uint8_t op = reg & 0x03;
1055
+
1056
+ // RAM vs ROM read selection
1057
+ // Pattern: 0=RAM, 1=ROM, 2=ROM, 3=RAM
1058
+ bool readRAM = (op == 0 || op == 3);
1059
+
1060
+ switch (op) {
1061
+ case 0: // $C080, $C088: Read RAM, write disabled
1062
+ switches_.lcwrite = false;
1063
+ switches_.lcprewrite = false;
1064
+ break;
1065
+
1066
+ case 1: // $C081, $C089: Read ROM, write enable on second read
1067
+ if (switches_.lcprewrite) {
1068
+ switches_.lcwrite = true;
1069
+ }
1070
+ switches_.lcprewrite = true;
1071
+ break;
1072
+
1073
+ case 2: // $C082, $C08A: Read ROM, write disabled
1074
+ switches_.lcwrite = false;
1075
+ switches_.lcprewrite = false;
1076
+ break;
1077
+
1078
+ case 3: // $C083, $C08B: Read RAM, write enable on second read
1079
+ if (switches_.lcprewrite) {
1080
+ switches_.lcwrite = true;
1081
+ }
1082
+ switches_.lcprewrite = true;
1083
+ break;
1084
+ }
1085
+
1086
+ switches_.lcram = readRAM;
1087
+ switches_.lcram2 = bank2;
1088
+
1089
+ return getFloatingBusValue();
1090
+ }
1091
+
1092
+ void MMU::writeSoftSwitch(uint16_t address, uint8_t value) {
1093
+ uint8_t reg = address & 0xFF;
1094
+
1095
+ switch (reg) {
1096
+ // Keyboard strobe
1097
+ case 0x10:
1098
+ if (keyStrobeCallback_) {
1099
+ keyStrobeCallback_();
1100
+ }
1101
+ keyboardLatch_ &= 0x7F;
1102
+ break;
1103
+
1104
+ // Speaker
1105
+ case 0x30:
1106
+ if (speakerCallback_) {
1107
+ speakerCallback_();
1108
+ }
1109
+ break;
1110
+
1111
+ // 80STORE
1112
+ case 0x00:
1113
+ switches_.store80 = false;
1114
+ if (videoSwitchCallback_) videoSwitchCallback_();
1115
+ break;
1116
+ case 0x01:
1117
+ switches_.store80 = true;
1118
+ if (videoSwitchCallback_) videoSwitchCallback_();
1119
+ break;
1120
+
1121
+ // Memory switches
1122
+ case 0x02:
1123
+ switches_.ramrd = false;
1124
+ break;
1125
+ case 0x03:
1126
+ switches_.ramrd = true;
1127
+ break;
1128
+ case 0x04:
1129
+ switches_.ramwrt = false;
1130
+ break;
1131
+ case 0x05:
1132
+ switches_.ramwrt = true;
1133
+ break;
1134
+ case 0x06:
1135
+ switches_.intcxrom = false;
1136
+ break;
1137
+ case 0x07:
1138
+ switches_.intcxrom = true;
1139
+ break;
1140
+ case 0x08:
1141
+ switches_.altzp = false;
1142
+ break;
1143
+ case 0x09:
1144
+ switches_.altzp = true;
1145
+ break;
1146
+ case 0x0A:
1147
+ switches_.slotc3rom = false;
1148
+ break;
1149
+ case 0x0B:
1150
+ switches_.slotc3rom = true;
1151
+ break;
1152
+ case 0x0C:
1153
+ switches_.col80 = false;
1154
+ if (videoSwitchCallback_) videoSwitchCallback_();
1155
+ break;
1156
+ case 0x0D:
1157
+ switches_.col80 = true;
1158
+ if (videoSwitchCallback_) videoSwitchCallback_();
1159
+ break;
1160
+ case 0x0E:
1161
+ switches_.altCharSet = false;
1162
+ if (videoSwitchCallback_) videoSwitchCallback_();
1163
+ break;
1164
+ case 0x0F:
1165
+ switches_.altCharSet = true;
1166
+ if (videoSwitchCallback_) videoSwitchCallback_();
1167
+ break;
1168
+
1169
+ // Display switches
1170
+ case 0x50:
1171
+ switches_.text = false;
1172
+ if (videoSwitchCallback_) videoSwitchCallback_();
1173
+ break;
1174
+ case 0x51:
1175
+ switches_.text = true;
1176
+ if (videoSwitchCallback_) videoSwitchCallback_();
1177
+ break;
1178
+ case 0x52:
1179
+ switches_.mixed = false;
1180
+ if (videoSwitchCallback_) videoSwitchCallback_();
1181
+ break;
1182
+ case 0x53:
1183
+ switches_.mixed = true;
1184
+ if (videoSwitchCallback_) videoSwitchCallback_();
1185
+ break;
1186
+ case 0x54:
1187
+ switches_.page2 = false;
1188
+ if (videoSwitchCallback_) videoSwitchCallback_();
1189
+ break;
1190
+ case 0x55:
1191
+ switches_.page2 = true;
1192
+ if (videoSwitchCallback_) videoSwitchCallback_();
1193
+ break;
1194
+ case 0x56:
1195
+ switches_.hires = false;
1196
+ if (videoSwitchCallback_) videoSwitchCallback_();
1197
+ break;
1198
+ case 0x57:
1199
+ switches_.hires = true;
1200
+ if (videoSwitchCallback_) videoSwitchCallback_();
1201
+ break;
1202
+
1203
+ // Paddle trigger (write also triggers)
1204
+ case 0x70:
1205
+ paddleTriggerCycle_ = cycleCallback_ ? cycleCallback_() : 0;
1206
+ break;
1207
+
1208
+ // Annunciators
1209
+ case 0x58:
1210
+ switches_.an0 = false;
1211
+ break;
1212
+ case 0x59:
1213
+ switches_.an0 = true;
1214
+ break;
1215
+ case 0x5A:
1216
+ switches_.an1 = false;
1217
+ break;
1218
+ case 0x5B:
1219
+ switches_.an1 = true;
1220
+ break;
1221
+ case 0x5C:
1222
+ switches_.an2 = false;
1223
+ break;
1224
+ case 0x5D:
1225
+ switches_.an2 = true;
1226
+ break;
1227
+ case 0x5E:
1228
+ switches_.an3 = false;
1229
+ if (videoSwitchCallback_) videoSwitchCallback_();
1230
+ break;
1231
+ case 0x5F:
1232
+ switches_.an3 = true;
1233
+ if (videoSwitchCallback_) videoSwitchCallback_();
1234
+ break;
1235
+
1236
+ // Slot I/O space: $C090-$C0FF (slots 1-7)
1237
+ case 0x90: case 0x91: case 0x92: case 0x93: case 0x94: case 0x95: case 0x96: case 0x97:
1238
+ case 0x98: case 0x99: case 0x9A: case 0x9B: case 0x9C: case 0x9D: case 0x9E: case 0x9F:
1239
+ case 0xA0: case 0xA1: case 0xA2: case 0xA3: case 0xA4: case 0xA5: case 0xA6: case 0xA7:
1240
+ case 0xA8: case 0xA9: case 0xAA: case 0xAB: case 0xAC: case 0xAD: case 0xAE: case 0xAF:
1241
+ case 0xB0: case 0xB1: case 0xB2: case 0xB3: case 0xB4: case 0xB5: case 0xB6: case 0xB7:
1242
+ case 0xB8: case 0xB9: case 0xBA: case 0xBB: case 0xBC: case 0xBD: case 0xBE: case 0xBF:
1243
+ case 0xC0: case 0xC1: case 0xC2: case 0xC3: case 0xC4: case 0xC5: case 0xC6: case 0xC7:
1244
+ case 0xC8: case 0xC9: case 0xCA: case 0xCB: case 0xCC: case 0xCD: case 0xCE: case 0xCF:
1245
+ case 0xD0: case 0xD1: case 0xD2: case 0xD3: case 0xD4: case 0xD5: case 0xD6: case 0xD7:
1246
+ case 0xD8: case 0xD9: case 0xDA: case 0xDB: case 0xDC: case 0xDD: case 0xDE: case 0xDF:
1247
+ case 0xE0: case 0xE1: case 0xE2: case 0xE3: case 0xE4: case 0xE5: case 0xE6: case 0xE7:
1248
+ case 0xE8: case 0xE9: case 0xEA: case 0xEB: case 0xEC: case 0xED: case 0xEE: case 0xEF:
1249
+ case 0xF0: case 0xF1: case 0xF2: case 0xF3: case 0xF4: case 0xF5: case 0xF6: case 0xF7:
1250
+ case 0xF8: case 0xF9: case 0xFA: case 0xFB: case 0xFC: case 0xFD: case 0xFE: case 0xFF: {
1251
+ // Calculate slot number: $C090 = slot 1, $C0A0 = slot 2, etc.
1252
+ uint8_t slot = ((reg - 0x80) >> 4);
1253
+ uint8_t offset = reg & 0x0F;
1254
+
1255
+ if (slot >= 1 && slot <= 7 && slots_[slot - 1]) {
1256
+ slots_[slot - 1]->writeIO(offset, value);
1257
+ }
1258
+ break;
1259
+ }
1260
+
1261
+ // Language card - writes do NOT enable write, and reset prewrite state
1262
+ case 0x80:
1263
+ case 0x81:
1264
+ case 0x82:
1265
+ case 0x83:
1266
+ case 0x84:
1267
+ case 0x85:
1268
+ case 0x86:
1269
+ case 0x87:
1270
+ case 0x88:
1271
+ case 0x89:
1272
+ case 0x8A:
1273
+ case 0x8B:
1274
+ case 0x8C:
1275
+ case 0x8D:
1276
+ case 0x8E:
1277
+ case 0x8F:
1278
+ handleLanguageCardSwitchWrite(reg);
1279
+ break;
1280
+
1281
+ default:
1282
+ break;
1283
+ }
1284
+ }
1285
+
1286
+ void MMU::handleLanguageCardSwitchWrite(uint8_t reg) {
1287
+ // Language card soft switches on WRITE access
1288
+ // On Apple IIe, writes to LC soft switches:
1289
+ // - Do NOT count toward the "double read" requirement for write-enable
1290
+ // - ALL writes reset the prewrite state (clearing any pending write-enable)
1291
+ // - Bank and read source selection still applies
1292
+ //
1293
+ // This means LDA $C083 + STA $C083 + LDA $C083 does NOT enable writes
1294
+ // (the STA resets the counter), but INC $C083 DOES enable writes
1295
+ // (because INC does two reads before the write).
1296
+
1297
+ bool bank2 = !(reg & 0x08);
1298
+ uint8_t op = reg & 0x03;
1299
+
1300
+ // RAM vs ROM read selection (same as reads)
1301
+ bool readRAM = (op == 0 || op == 3);
1302
+
1303
+ switch (op) {
1304
+ case 0: // $C080, $C088: Read RAM, write disabled
1305
+ switches_.lcwrite = false;
1306
+ switches_.lcprewrite = false;
1307
+ break;
1308
+
1309
+ case 1: // $C081, $C089: Read ROM
1310
+ // Write resets prewrite but doesn't disable existing lcwrite
1311
+ switches_.lcprewrite = false;
1312
+ break;
1313
+
1314
+ case 2: // $C082, $C08A: Read ROM, write disabled
1315
+ switches_.lcwrite = false;
1316
+ switches_.lcprewrite = false;
1317
+ break;
1318
+
1319
+ case 3: // $C083, $C08B: Read RAM
1320
+ // Write resets prewrite but doesn't disable existing lcwrite
1321
+ switches_.lcprewrite = false;
1322
+ break;
1323
+ }
1324
+
1325
+ switches_.lcram = readRAM;
1326
+ switches_.lcram2 = bank2;
1327
+ }
1328
+
1329
+ uint8_t MMU::readLanguageCard(uint16_t address) {
1330
+ if (switches_.lcram) {
1331
+ // Read from RAM
1332
+ bool useAux = switches_.altzp;
1333
+
1334
+ if (address < 0xE000) {
1335
+ // $D000-$DFFF
1336
+ uint16_t offset = address - 0xD000;
1337
+ if (switches_.lcram2) {
1338
+ return useAux ? auxLcBank2_[offset] : lcBank2_[offset];
1339
+ } else {
1340
+ return useAux ? auxLcBank1_[offset] : lcBank1_[offset];
1341
+ }
1342
+ } else {
1343
+ // $E000-$FFFF
1344
+ uint16_t offset = address - 0xE000;
1345
+ return useAux ? auxLcHighRAM_[offset] : lcHighRAM_[offset];
1346
+ }
1347
+ } else {
1348
+ // Read from ROM
1349
+ return systemROM_[address - 0xC000];
1350
+ }
1351
+ }
1352
+
1353
+ void MMU::writeLanguageCard(uint16_t address, uint8_t value) {
1354
+ if (!switches_.lcwrite) {
1355
+ return; // Write not enabled
1356
+ }
1357
+
1358
+ bool useAux = switches_.altzp;
1359
+
1360
+ if (address < 0xE000) {
1361
+ // $D000-$DFFF
1362
+ uint16_t offset = address - 0xD000;
1363
+ if (switches_.lcram2) {
1364
+ if (useAux) {
1365
+ auxLcBank2_[offset] = value;
1366
+ } else {
1367
+ lcBank2_[offset] = value;
1368
+ }
1369
+ } else {
1370
+ if (useAux) {
1371
+ auxLcBank1_[offset] = value;
1372
+ } else {
1373
+ lcBank1_[offset] = value;
1374
+ }
1375
+ }
1376
+ } else {
1377
+ // $E000-$FFFF
1378
+ uint16_t offset = address - 0xE000;
1379
+ if (useAux) {
1380
+ auxLcHighRAM_[offset] = value;
1381
+ } else {
1382
+ lcHighRAM_[offset] = value;
1383
+ }
1384
+ }
1385
+ }
1386
+
1387
+ } // namespace a2e