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,680 @@
1
+ /*
2
+ * video.cpp - Video output generation implementation
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ #include "video.hpp"
9
+ #include <algorithm>
10
+ #include <cstring>
11
+
12
+ namespace a2e {
13
+
14
+ Video::Video(MMU &mmu) : mmu_(mmu) {
15
+ // Initialize framebuffer to black
16
+ std::memset(framebuffer_.data(), 0, framebuffer_.size());
17
+ }
18
+
19
+ VideoMode Video::getCurrentMode() const {
20
+ const auto &sw = mmu_.getSoftSwitches();
21
+
22
+ if (sw.text) {
23
+ return sw.col80 ? VideoMode::TEXT_80 : VideoMode::TEXT_40;
24
+ }
25
+
26
+ if (sw.hires) {
27
+ // DHR requires: AN3 OFF (!an3), 80COL on, HIRES on
28
+ if (sw.col80 && !sw.an3) {
29
+ return VideoMode::DOUBLE_HIRES;
30
+ }
31
+ return VideoMode::HIRES;
32
+ }
33
+
34
+ // Double LoRes: AN3 OFF (!an3), 80COL on
35
+ if (sw.col80 && !sw.an3) {
36
+ return VideoMode::DOUBLE_LORES;
37
+ }
38
+ return VideoMode::LORES;
39
+ }
40
+
41
+ // ============================================================================
42
+ // Raster rendering infrastructure
43
+ // ============================================================================
44
+
45
+ VideoSwitchState Video::captureVideoState() const {
46
+ const auto &sw = mmu_.getSoftSwitches();
47
+ return {sw.text, sw.mixed, sw.page2, sw.hires,
48
+ sw.col80, sw.altCharSet, sw.store80, sw.an3};
49
+ }
50
+
51
+ void Video::onVideoSwitchChanged() {
52
+ VideoSwitchState newState = captureVideoState();
53
+
54
+ // Compare against last logged state to avoid redundant entries
55
+ const VideoSwitchState &lastState =
56
+ (switchChangeCount_ > 0) ? switchChanges_[switchChangeCount_ - 1].state
57
+ : frameStartState_;
58
+
59
+ if (std::memcmp(&newState, &lastState, sizeof(VideoSwitchState)) == 0) {
60
+ return; // No actual change
61
+ }
62
+
63
+ if (switchChangeCount_ >= MAX_SWITCH_CHANGES) {
64
+ return; // Log full, drop this change
65
+ }
66
+
67
+ uint32_t cycleOffset = 0;
68
+ if (cycleCallback_) {
69
+ uint64_t currentCycle = cycleCallback_();
70
+ // +2 models the Apple IIe two-stage video pipeline delay:
71
+ // 1) Phi-0/Phi-1 bus phasing: the video fetches memory on Phi-0 (first half
72
+ // of each clock cycle) before the CPU writes on Phi-1 (second half).
73
+ // A soft switch change on cycle N misses that cycle's video fetch.
74
+ // 2) Shift register latching: the byte fetched on Phi-0 of cycle N+1 is
75
+ // loaded into the shift register and doesn't produce visible dots until
76
+ // approximately cycle N+2.
77
+ // Combined: a CPU write on cycle N affects display output at cycle N+2.
78
+ cycleOffset = static_cast<uint32_t>(currentCycle - frameStartCycle_ + 2);
79
+ }
80
+
81
+ switchChanges_[switchChangeCount_] = {cycleOffset, newState};
82
+ switchChangeCount_++;
83
+ }
84
+
85
+ void Video::beginNewFrame(uint64_t cycleStart) {
86
+ frameStartCycle_ = cycleStart;
87
+ frameStartState_ = captureVideoState();
88
+ switchChangeCount_ = 0;
89
+
90
+ // Reset progressive rendering state
91
+ lastRenderedScanline_ = -1;
92
+ changeIdx_ = 0;
93
+ currentRenderState_ = frameStartState_;
94
+ }
95
+
96
+ // ============================================================================
97
+ // Character ROM offset helper (deduplicates 40-col and 80-col logic)
98
+ // ============================================================================
99
+
100
+ Video::CharROMInfo Video::getCharROMInfo(uint8_t ch, bool inverse, bool flash,
101
+ const VideoSwitchState &vs) const {
102
+ uint16_t romOffset;
103
+ bool needsXor = false;
104
+
105
+ if (vs.altCharSet) {
106
+ uint8_t charIndex;
107
+ if (ch >= 0x40 && ch < 0x60) {
108
+ charIndex = ch;
109
+ needsXor = true;
110
+ inverse = false;
111
+ } else if (ch >= 0x60 && ch < 0x80) {
112
+ charIndex = ch;
113
+ needsXor = true;
114
+ } else if (ch < 0x40) {
115
+ charIndex = ch;
116
+ needsXor = false;
117
+ } else {
118
+ if (ch < 0xA0) {
119
+ charIndex = ch & 0x1F;
120
+ } else if (ch < 0xC0) {
121
+ charIndex = (ch & 0x1F) + 32;
122
+ } else if (ch < 0xE0) {
123
+ charIndex = ch & 0x1F;
124
+ } else {
125
+ charIndex = (ch & 0x1F) + 96;
126
+ }
127
+ needsXor = false;
128
+ inverse = false;
129
+ }
130
+ romOffset = charIndex * 8;
131
+ } else {
132
+ uint8_t charIndex;
133
+ if (ch < 0x20) {
134
+ charIndex = ch;
135
+ } else if (ch < 0x40) {
136
+ charIndex = ch;
137
+ } else if (ch < 0x60) {
138
+ charIndex = ch & 0x1F;
139
+ } else if (ch < 0x80) {
140
+ charIndex = (ch & 0x1F) + 32;
141
+ } else if (ch < 0xA0) {
142
+ charIndex = ch & 0x1F;
143
+ inverse = false;
144
+ } else if (ch < 0xC0) {
145
+ charIndex = (ch & 0x1F) + 32;
146
+ inverse = false;
147
+ } else if (ch < 0xE0) {
148
+ charIndex = ch & 0x1F;
149
+ inverse = false;
150
+ } else {
151
+ charIndex = (ch & 0x1F) + 96;
152
+ inverse = false;
153
+ }
154
+ romOffset = charIndex * 8;
155
+ needsXor = false;
156
+ }
157
+
158
+ // Apply UK character set offset if enabled
159
+ if (ukCharSet_) {
160
+ romOffset += 0x1000;
161
+ }
162
+
163
+ // Handle flash - toggle inverse state when flash is active
164
+ if (flash && flashState_ && !vs.altCharSet) {
165
+ inverse = !inverse;
166
+ }
167
+
168
+ return {romOffset, needsXor, inverse};
169
+ }
170
+
171
+ // ============================================================================
172
+ // Per-character-line rendering
173
+ // ============================================================================
174
+
175
+ void Video::renderCharacterLine(int col, int textRow, int charLine,
176
+ uint8_t ch, bool inverse, bool flash,
177
+ const VideoSwitchState &vs, bool is80col) {
178
+ CharROMInfo info = getCharROMInfo(ch, inverse, flash, vs);
179
+
180
+ uint32_t fgColor, bgColor;
181
+ if (monochrome_) {
182
+ fgColor = getMonochromeColor(true);
183
+ bgColor = getMonochromeColor(false);
184
+ } else {
185
+ fgColor = 0xFFFFFFFF;
186
+ bgColor = 0xFF000000;
187
+ }
188
+
189
+ if (info.inverse) {
190
+ std::swap(fgColor, bgColor);
191
+ }
192
+
193
+ uint8_t rowData = mmu_.readCharROM(info.romOffset + charLine);
194
+ if (info.needsXor) {
195
+ rowData ^= 0xFF;
196
+ }
197
+
198
+ int screenY = textRow * 16 + charLine * 2;
199
+
200
+ if (is80col) {
201
+ // 80-col: col is 0-79, each character is 7 pixels wide (560 total)
202
+ int screenX = col * 7;
203
+ for (int charCol = 0; charCol < 7; charCol++) {
204
+ bool pixelOn = (rowData & (1 << charCol)) != 0;
205
+ uint32_t color = pixelOn ? fgColor : bgColor;
206
+ int px = screenX + charCol;
207
+ setPixel(px, screenY, color);
208
+ setPixel(px, screenY + 1, color);
209
+ }
210
+ } else {
211
+ // 40-col: col is 0-39, each character is 14 pixels wide (560 total)
212
+ int screenX = col * 14;
213
+ for (int charCol = 0; charCol < 7; charCol++) {
214
+ bool pixelOn = (rowData & (1 << charCol)) != 0;
215
+ uint32_t color = pixelOn ? fgColor : bgColor;
216
+ int px = screenX + charCol * 2;
217
+ setPixel(px, screenY, color);
218
+ setPixel(px + 1, screenY, color);
219
+ setPixel(px, screenY + 1, color);
220
+ setPixel(px + 1, screenY + 1, color);
221
+ }
222
+ }
223
+ }
224
+
225
+ // ============================================================================
226
+ // Per-scanline segment renderers
227
+ // Each renders a column range [startCol, endCol) on a single scanline.
228
+ // Columns are byte positions 0-40 matching the hardware's per-cycle reads.
229
+ // ============================================================================
230
+
231
+ void Video::renderText40Scanline(int scanline, int startCol, int endCol,
232
+ const VideoSwitchState &vs) {
233
+ int textRow = scanline / 8;
234
+ int charLine = scanline % 8;
235
+ if (textRow >= 24) return;
236
+
237
+ for (int col = startCol; col < endCol; col++) {
238
+ uint16_t addr = getTextAddress(textRow, col);
239
+
240
+ uint8_t ch;
241
+ if (vs.page2 && !vs.store80) {
242
+ ch = mmu_.readRAM(addr + 0x0400, false);
243
+ } else {
244
+ ch = mmu_.readRAM(addr, false);
245
+ }
246
+
247
+ bool inverse = (ch & 0xC0) == 0x00;
248
+ bool flash = (ch & 0xC0) == 0x40;
249
+
250
+ renderCharacterLine(col, textRow, charLine, ch, inverse, flash, vs, false);
251
+ }
252
+ }
253
+
254
+ void Video::renderText80Scanline(int scanline, int startCol, int endCol,
255
+ const VideoSwitchState &vs) {
256
+ int textRow = scanline / 8;
257
+ int charLine = scanline % 8;
258
+ if (textRow >= 24) return;
259
+
260
+ uint16_t pageOffset = (vs.page2 && !vs.store80) ? 0x0400 : 0x0000;
261
+
262
+ for (int col = startCol; col < endCol; col++) {
263
+ uint16_t addr = getTextAddress(textRow, col);
264
+
265
+ // Aux memory character (even columns in display)
266
+ uint8_t auxCh = mmu_.readRAM(addr + pageOffset, true);
267
+ bool auxInverse = (auxCh & 0xC0) == 0x00;
268
+ bool auxFlash = (auxCh & 0xC0) == 0x40;
269
+ renderCharacterLine(col * 2, textRow, charLine, auxCh, auxInverse, auxFlash, vs, true);
270
+
271
+ // Main memory character (odd columns in display)
272
+ uint8_t mainCh = mmu_.readRAM(addr + pageOffset, false);
273
+ bool mainInverse = (mainCh & 0xC0) == 0x00;
274
+ bool mainFlash = (mainCh & 0xC0) == 0x40;
275
+ renderCharacterLine(col * 2 + 1, textRow, charLine, mainCh, mainInverse, mainFlash, vs, true);
276
+ }
277
+ }
278
+
279
+ void Video::renderLoResScanline(int scanline, int startCol, int endCol,
280
+ const VideoSwitchState &vs) {
281
+ int textRow = scanline / 8;
282
+ int lineInRow = scanline % 8;
283
+ if (textRow >= 24) return;
284
+
285
+ int screenY = scanline * 2;
286
+
287
+ for (int col = startCol; col < endCol; col++) {
288
+ uint16_t addr = getTextAddress(textRow, col);
289
+
290
+ uint8_t colorByte;
291
+ if (vs.page2 && !vs.store80) {
292
+ colorByte = mmu_.readRAM(addr + 0x0400, false);
293
+ } else {
294
+ colorByte = mmu_.readRAM(addr, false);
295
+ }
296
+
297
+ uint8_t colorIndex = (lineInRow < 4) ? (colorByte & 0x0F)
298
+ : ((colorByte >> 4) & 0x0F);
299
+
300
+ uint32_t color = getLoResColor(colorIndex);
301
+ int screenX = col * 14;
302
+
303
+ for (int px = 0; px < 14; px++) {
304
+ setPixel(screenX + px, screenY, color);
305
+ setPixel(screenX + px, screenY + 1, color);
306
+ }
307
+ }
308
+ }
309
+
310
+ void Video::renderHiResScanline(int scanline, int startCol, int endCol,
311
+ const VideoSwitchState &vs) {
312
+ if (scanline >= 192) return;
313
+
314
+ // Build dot/highBit arrays for the columns in our range.
315
+ // Full 280-element arrays are zero-initialized so dots outside
316
+ // our segment read as off — correct behavior at mode boundaries.
317
+ uint8_t dots[280] = {0};
318
+ uint8_t highBits[280] = {0};
319
+
320
+ for (int col = startCol; col < endCol; col++) {
321
+ uint16_t addr = getHiResAddress(scanline, col);
322
+
323
+ uint8_t dataByte;
324
+ if (vs.page2 && !vs.store80) {
325
+ dataByte = mmu_.readRAM(addr + 0x2000, false);
326
+ } else {
327
+ dataByte = mmu_.readRAM(addr, false);
328
+ }
329
+
330
+ bool highBit = (dataByte & 0x80) != 0;
331
+
332
+ for (int bit = 0; bit < 7; bit++) {
333
+ int dotX = col * 7 + bit;
334
+ dots[dotX] = (dataByte & (1 << bit)) ? 1 : 0;
335
+ highBits[dotX] = highBit ? 1 : 0;
336
+ }
337
+ }
338
+
339
+ int screenY = scanline * 2;
340
+ int dotStart = startCol * 7;
341
+ int dotEnd = endCol * 7;
342
+
343
+ if (monochrome_) {
344
+ for (int x = dotStart; x < dotEnd; x++) {
345
+ uint32_t color = getMonochromeColor(dots[x] != 0);
346
+ int screenX = x * 2;
347
+ setPixel(screenX, screenY, color);
348
+ setPixel(screenX + 1, screenY, color);
349
+ setPixel(screenX, screenY + 1, color);
350
+ setPixel(screenX + 1, screenY + 1, color);
351
+ }
352
+ } else {
353
+ for (int x = dotStart; x < dotEnd; x++) {
354
+ uint32_t color;
355
+ bool highBit = highBits[x] != 0;
356
+ bool dotOn = dots[x] != 0;
357
+
358
+ bool prevOn = (x > 0) && dots[x - 1];
359
+ bool nextOn = (x < 279) && dots[x + 1];
360
+
361
+ if (!dotOn) {
362
+ bool prev2On = (x > 1) && dots[x - 2];
363
+ bool next2On = (x < 278) && dots[x + 2];
364
+
365
+ if (prevOn && nextOn && !prev2On && !next2On) {
366
+ if (highBits[x - 1] == highBits[x + 1]) {
367
+ bool neighborEven = ((x - 1) & 1) == 0;
368
+ bool neighborHighBit = highBits[x - 1];
369
+ if (neighborEven) {
370
+ color = neighborHighBit ? HIRES_COLORS[4] : HIRES_COLORS[2];
371
+ } else {
372
+ color = neighborHighBit ? HIRES_COLORS[5] : HIRES_COLORS[1];
373
+ }
374
+ } else {
375
+ color = HIRES_COLORS[0];
376
+ }
377
+ } else {
378
+ color = HIRES_COLORS[0];
379
+ }
380
+ } else if (prevOn || nextOn) {
381
+ color = HIRES_COLORS[3]; // White
382
+ } else {
383
+ bool evenColumn = (x & 1) == 0;
384
+ if (evenColumn) {
385
+ color = highBit ? HIRES_COLORS[4] : HIRES_COLORS[2];
386
+ } else {
387
+ color = highBit ? HIRES_COLORS[5] : HIRES_COLORS[1];
388
+ }
389
+ }
390
+
391
+ int screenX = x * 2;
392
+ setPixel(screenX, screenY, color);
393
+ setPixel(screenX + 1, screenY, color);
394
+ setPixel(screenX, screenY + 1, color);
395
+ setPixel(screenX + 1, screenY + 1, color);
396
+ }
397
+ }
398
+ }
399
+
400
+ void Video::renderDoubleLoResScanline(int scanline, int startCol, int endCol,
401
+ const VideoSwitchState &vs) {
402
+ int textRow = scanline / 8;
403
+ int lineInRow = scanline % 8;
404
+ if (textRow >= 24) return;
405
+
406
+ int screenY = scanline * 2;
407
+
408
+ for (int col = startCol; col < endCol; col++) {
409
+ uint16_t addr = getTextAddress(textRow, col);
410
+
411
+ uint8_t auxByte = mmu_.readRAM(addr, true);
412
+ uint8_t mainByte = mmu_.readRAM(addr, false);
413
+
414
+ uint8_t auxNibble = (lineInRow < 4) ? (auxByte & 0x0F)
415
+ : ((auxByte >> 4) & 0x0F);
416
+ uint8_t mainColor = (lineInRow < 4) ? (mainByte & 0x0F)
417
+ : ((mainByte >> 4) & 0x0F);
418
+
419
+ // Aux nibbles need 4-bit left rotation by 1 to compensate for
420
+ // half-color-clock phase shift in auxiliary video memory
421
+ uint8_t auxColor = ((auxNibble << 1) & 0x0F) | (auxNibble >> 3);
422
+
423
+ uint32_t auxRGB = monochrome_ ? getMonochromeColor(auxColor != 0) : LORES_COLORS[auxColor];
424
+ uint32_t mainRGB = monochrome_ ? getMonochromeColor(mainColor != 0) : LORES_COLORS[mainColor];
425
+
426
+ int screenX = col * 14;
427
+
428
+ // Aux pixels (left half, 7 pixels wide)
429
+ for (int px = 0; px < 7; px++) {
430
+ setPixel(screenX + px, screenY, auxRGB);
431
+ setPixel(screenX + px, screenY + 1, auxRGB);
432
+ }
433
+
434
+ // Main pixels (right half, 7 pixels wide)
435
+ for (int px = 7; px < 14; px++) {
436
+ setPixel(screenX + px, screenY, mainRGB);
437
+ setPixel(screenX + px, screenY + 1, mainRGB);
438
+ }
439
+ }
440
+ }
441
+
442
+ void Video::renderDoubleHiResScanline(int scanline, int startCol, int endCol,
443
+ const VideoSwitchState &vs) {
444
+ static const uint32_t DHGR_COLORS[16] = {
445
+ 0xFF000000, 0xFFE31E60, 0xFF607203, 0xFFFF6A3C,
446
+ 0xFF00A360, 0xFF9C9C9C, 0xFF14F53C, 0xFFD0DD8D,
447
+ 0xFF604EBD, 0xFFFF44FD, 0xFF9C9C9C, 0xFFFFA0D0,
448
+ 0xFF14CFFD, 0xFFD0C3FF, 0xFF72FFD0, 0xFFFFFFFF
449
+ };
450
+
451
+ if (scanline >= 192) return;
452
+
453
+ uint16_t pageOffset = (vs.page2 && !vs.store80) ? 0x2000 : 0;
454
+
455
+ // Read bytes for columns in our range (zero-init for edge handling)
456
+ uint8_t line[80] = {0};
457
+ for (int col = startCol; col < endCol; col++) {
458
+ uint16_t addr = getHiResAddress(scanline, col) + pageOffset;
459
+ line[col * 2] = mmu_.readRAM(addr, true);
460
+ line[col * 2 + 1] = mmu_.readRAM(addr, false);
461
+ }
462
+
463
+ // Extract dots for our column range
464
+ uint8_t dots[564] = {0};
465
+ int dotStart = startCol * 14;
466
+ int dotEnd = std::min(endCol * 14, 560);
467
+
468
+ for (int i = dotStart; i < dotEnd; i++) {
469
+ int byteIdx = i / 7;
470
+ int bitIdx = i % 7;
471
+ dots[i] = (line[byteIdx] >> bitIdx) & 1;
472
+ }
473
+
474
+ int screenY = scanline * 2;
475
+
476
+ if (monochrome_) {
477
+ for (int i = dotStart; i < dotEnd; i++) {
478
+ uint32_t color = getMonochromeColor(dots[i] != 0);
479
+ setPixel(i, screenY, color);
480
+ setPixel(i, screenY + 1, color);
481
+ }
482
+ } else {
483
+ for (int i = dotStart; i < dotEnd; i++) {
484
+ int alignedBase = (i / 4) * 4;
485
+ uint8_t colorIdx = (dots[alignedBase] << 3) |
486
+ (dots[alignedBase + 1] << 2) |
487
+ (dots[alignedBase + 2] << 1) |
488
+ dots[alignedBase + 3];
489
+
490
+ uint32_t color = DHGR_COLORS[colorIdx];
491
+ setPixel(i, screenY, color);
492
+ setPixel(i, screenY + 1, color);
493
+ }
494
+ }
495
+ }
496
+
497
+ // ============================================================================
498
+ // Scanline segment dispatcher
499
+ // ============================================================================
500
+
501
+ void Video::renderScanlineSegment(int scanline, int startCol, int endCol,
502
+ const VideoSwitchState &vs) {
503
+ if (scanline >= 192 || startCol >= endCol) return;
504
+
505
+ // Mixed mode: scanlines 160-191 always render as text
506
+ if (vs.mixed && scanline >= 160 && !vs.text) {
507
+ if (vs.col80) {
508
+ renderText80Scanline(scanline, startCol, endCol, vs);
509
+ } else {
510
+ renderText40Scanline(scanline, startCol, endCol, vs);
511
+ }
512
+ return;
513
+ }
514
+
515
+ if (vs.text) {
516
+ if (vs.col80) {
517
+ renderText80Scanline(scanline, startCol, endCol, vs);
518
+ } else {
519
+ renderText40Scanline(scanline, startCol, endCol, vs);
520
+ }
521
+ } else if (vs.hires) {
522
+ if (vs.col80 && !vs.an3) {
523
+ renderDoubleHiResScanline(scanline, startCol, endCol, vs);
524
+ } else {
525
+ renderHiResScanline(scanline, startCol, endCol, vs);
526
+ }
527
+ } else {
528
+ if (vs.col80 && !vs.an3) {
529
+ renderDoubleLoResScanline(scanline, startCol, endCol, vs);
530
+ } else {
531
+ renderLoResScanline(scanline, startCol, endCol, vs);
532
+ }
533
+ }
534
+ }
535
+
536
+ // ============================================================================
537
+ // Progressive per-scanline rendering
538
+ // ============================================================================
539
+
540
+ void Video::renderScanlineWithChanges(int scanline) {
541
+ // Apple IIe horizontal timing: each 65-cycle scanline starts with
542
+ // 25 cycles of horizontal blanking, then 40 cycles of visible display.
543
+ static constexpr int HBLANK_CYCLES = 25;
544
+
545
+ uint32_t scanlineStartCycle = scanline * CYCLES_PER_SCANLINE;
546
+ uint32_t visibleStartCycle = scanlineStartCycle + HBLANK_CYCLES;
547
+ uint32_t scanlineEndCycle = scanlineStartCycle + CYCLES_PER_SCANLINE;
548
+
549
+ // Phase 1: Consume hblank changes (cycles 0-24) and any earlier changes
550
+ while (changeIdx_ < switchChangeCount_) {
551
+ uint32_t changeCycle = switchChanges_[changeIdx_].cycleOffset;
552
+ if (changeCycle >= visibleStartCycle) {
553
+ break; // Change is in visible area or a later scanline
554
+ }
555
+ currentRenderState_ = switchChanges_[changeIdx_].state;
556
+ changeIdx_++;
557
+ }
558
+
559
+ // Phase 2: Process visible-area changes (cycles 25-64 → columns 0-39)
560
+ int col = 0;
561
+ while (changeIdx_ < switchChangeCount_) {
562
+ uint32_t changeCycle = switchChanges_[changeIdx_].cycleOffset;
563
+ if (changeCycle >= scanlineEndCycle) {
564
+ break; // Belongs to a later scanline
565
+ }
566
+
567
+ int changeCol = static_cast<int>(changeCycle - visibleStartCycle);
568
+ if (changeCol > 40) changeCol = 40;
569
+
570
+ if (changeCol > col) {
571
+ renderScanlineSegment(scanline, col, changeCol, currentRenderState_);
572
+ col = changeCol;
573
+ }
574
+
575
+ currentRenderState_ = switchChanges_[changeIdx_].state;
576
+ changeIdx_++;
577
+ }
578
+
579
+ // Render remaining visible columns
580
+ if (col < 40) {
581
+ renderScanlineSegment(scanline, col, 40, currentRenderState_);
582
+ }
583
+ }
584
+
585
+ void Video::renderUpToCycle(uint64_t currentCycle) {
586
+ if (currentCycle <= frameStartCycle_) return;
587
+
588
+ uint64_t frameCycle = currentCycle - frameStartCycle_;
589
+
590
+ // Render scanlines whose 65 cycles are fully complete.
591
+ // frameCycle / CYCLES_PER_SCANLINE gives the number of complete scanlines,
592
+ // so targetScanline = completedScanlines - 1 is the last fully-elapsed one.
593
+ // This ensures all CPU writes during a scanline are captured before we
594
+ // read video memory for that scanline (critical for raster bar effects).
595
+ int completedScanlines = static_cast<int>(frameCycle / CYCLES_PER_SCANLINE);
596
+ int targetScanline = completedScanlines - 1;
597
+ if (targetScanline > 191) targetScanline = 191;
598
+
599
+ while (lastRenderedScanline_ < targetScanline) {
600
+ lastRenderedScanline_++;
601
+ renderScanlineWithChanges(lastRenderedScanline_);
602
+ }
603
+ }
604
+
605
+ // ============================================================================
606
+ // Frame rendering
607
+ // ============================================================================
608
+
609
+ void Video::renderFrame() {
610
+ // Update flash state
611
+ flashCounter_++;
612
+ if (flashCounter_ >= FLASH_RATE) {
613
+ flashCounter_ = 0;
614
+ flashState_ = !flashState_;
615
+ }
616
+
617
+ // Finish any remaining unrendered scanlines (progressive rendering
618
+ // may have already handled most of them during CPU execution)
619
+ while (lastRenderedScanline_ < 191) {
620
+ lastRenderedScanline_++;
621
+ renderScanlineWithChanges(lastRenderedScanline_);
622
+ }
623
+
624
+ frameDirty_ = true;
625
+ }
626
+
627
+ void Video::forceRenderFrame() {
628
+ VideoSwitchState vs = captureVideoState();
629
+ for (int scanline = 0; scanline < 192; scanline++) {
630
+ renderScanlineSegment(scanline, 0, 40, vs);
631
+ }
632
+ frameDirty_ = true;
633
+ }
634
+
635
+ // ============================================================================
636
+ // Pixel and color helpers
637
+ // ============================================================================
638
+
639
+ void Video::setPixel(int x, int y, uint32_t color) {
640
+ if (x < 0 || x >= SCREEN_WIDTH || y < 0 || y >= SCREEN_HEIGHT) {
641
+ return;
642
+ }
643
+
644
+ size_t offset = (y * SCREEN_WIDTH + x) * 4;
645
+ framebuffer_[offset + 0] = (color >> 16) & 0xFF; // R
646
+ framebuffer_[offset + 1] = (color >> 8) & 0xFF; // G
647
+ framebuffer_[offset + 2] = color & 0xFF; // B
648
+ framebuffer_[offset + 3] = (color >> 24) & 0xFF; // A
649
+ }
650
+
651
+ uint32_t Video::getLoResColor(uint8_t colorIndex) const {
652
+ if (monochrome_) {
653
+ return (colorIndex > 0) ? getMonochromeColor(true)
654
+ : getMonochromeColor(false);
655
+ }
656
+ return LORES_COLORS[colorIndex & 0x0F];
657
+ }
658
+
659
+ uint32_t Video::getMonochromeColor(bool on) const {
660
+ if (!on) {
661
+ return 0xFF000000; // Black
662
+ }
663
+
664
+ if (greenPhosphor_) {
665
+ return 0xFF33FF33; // Green phosphor
666
+ }
667
+ return 0xFFFFFFFF; // White
668
+ }
669
+
670
+ uint16_t Video::getTextAddress(int row, int col) const {
671
+ return 0x0400 + TEXT_ROW_OFFSETS[row] + col;
672
+ }
673
+
674
+ uint16_t Video::getHiResAddress(int row, int col) const {
675
+ int block = row / 8;
676
+ int line = row % 8;
677
+ return 0x2000 + TEXT_ROW_OFFSETS[block] + line * 0x400 + col;
678
+ }
679
+
680
+ } // namespace a2e