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,1049 @@
1
+ /*
2
+ * woz_disk_image.cpp - WOZ 1.0/2.0 bit-accurate disk image implementation
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ #include "woz_disk_image.hpp"
9
+ #include "gcr_encoding.hpp"
10
+ #include <cstring>
11
+
12
+ namespace a2e {
13
+
14
+ // 6-and-2 decoding table (reverse of GCR::ENCODE_6_AND_2)
15
+ static constexpr std::array<int8_t, 256> DECODE_6_AND_2 = []() {
16
+ std::array<int8_t, 256> table{};
17
+ for (int i = 0; i < 256; i++) {
18
+ table[i] = -1; // Invalid nibble
19
+ }
20
+ // Build reverse lookup from encode table
21
+ for (int i = 0; i < 64; i++) {
22
+ table[GCR::ENCODE_6_AND_2[i]] = static_cast<int8_t>(i);
23
+ }
24
+ return table;
25
+ }();
26
+
27
+ // DOS 3.3 physical to logical sector mapping
28
+ // Physical sector order on disk -> logical sector in file
29
+ static constexpr std::array<int, 16> DOS_PHYSICAL_TO_LOGICAL = {
30
+ 0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15};
31
+
32
+ // ProDOS physical to logical sector mapping
33
+ static constexpr std::array<int, 16> PRODOS_PHYSICAL_TO_LOGICAL = {
34
+ 0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15};
35
+
36
+ WozDiskImage::WozDiskImage() { reset(); }
37
+
38
+ void WozDiskImage::reset() {
39
+ format_ = Format::Unknown;
40
+ loaded_ = false;
41
+ modified_ = false;
42
+ std::memset(&info_, 0, sizeof(info_));
43
+ tmap_.fill(NO_TRACK);
44
+ tracks_.clear();
45
+
46
+ // Reset head positioning state
47
+ phase_states_ = 0;
48
+ quarter_track_ = 0;
49
+ current_phase_ = 0;
50
+ bit_position_ = 0;
51
+ last_cycle_count_ = 0;
52
+
53
+ // Clear decoded sector cache
54
+ decoded_sectors_.clear();
55
+ sectors_decoded_ = false;
56
+ }
57
+
58
+ void WozDiskImage::resetState() {
59
+ phase_states_ = 0;
60
+ quarter_track_ = 0;
61
+ current_phase_ = 0;
62
+ bit_position_ = 0;
63
+ last_cycle_count_ = 0;
64
+ }
65
+
66
+ bool WozDiskImage::load(const uint8_t *data, size_t size,
67
+ const std::string &filename) {
68
+ reset();
69
+ filename_ = filename;
70
+
71
+ if (size < sizeof(WozHeader)) {
72
+ return false;
73
+ }
74
+
75
+ // Validate header
76
+ const auto *header = reinterpret_cast<const WozHeader *>(data);
77
+ if (header->signature == WOZ1_SIGNATURE) {
78
+ format_ = Format::WOZ1;
79
+ } else if (header->signature == WOZ2_SIGNATURE) {
80
+ format_ = Format::WOZ2;
81
+ } else {
82
+ return false;
83
+ }
84
+
85
+ // Validate magic bytes
86
+ if (header->high_bits != 0xFF || header->lfcrlf[0] != 0x0A ||
87
+ header->lfcrlf[1] != 0x0D || header->lfcrlf[2] != 0x0A) {
88
+ return false;
89
+ }
90
+
91
+ // Parse chunks
92
+ size_t offset = sizeof(WozHeader);
93
+ bool has_info = false;
94
+ bool has_tmap = false;
95
+ bool has_trks = false;
96
+
97
+ // Store TRKS chunk info for later processing (needs TMAP first)
98
+ const uint8_t *trks_data = nullptr;
99
+ uint32_t trks_size = 0;
100
+
101
+ while (offset + sizeof(ChunkHeader) <= size) {
102
+ const auto *chunk =
103
+ reinterpret_cast<const ChunkHeader *>(data + offset);
104
+ const uint8_t *chunk_data = data + offset + sizeof(ChunkHeader);
105
+
106
+ if (offset + sizeof(ChunkHeader) + chunk->size > size) {
107
+ break; // Chunk extends past end of file
108
+ }
109
+
110
+ switch (chunk->chunk_id) {
111
+ case INFO_CHUNK_ID:
112
+ if (!parseInfoChunk(chunk_data, chunk->size)) {
113
+ return false;
114
+ }
115
+ has_info = true;
116
+ break;
117
+
118
+ case TMAP_CHUNK_ID:
119
+ if (!parseTmapChunk(chunk_data, chunk->size)) {
120
+ return false;
121
+ }
122
+ has_tmap = true;
123
+ break;
124
+
125
+ case TRKS_CHUNK_ID:
126
+ // Save for later - need TMAP first
127
+ trks_data = chunk_data;
128
+ trks_size = chunk->size;
129
+ has_trks = true;
130
+ break;
131
+
132
+ default:
133
+ // Skip unknown chunks (META, WRIT, etc.)
134
+ break;
135
+ }
136
+
137
+ offset += sizeof(ChunkHeader) + chunk->size;
138
+ }
139
+
140
+ // Validate required chunks
141
+ if (!has_info || !has_tmap || !has_trks) {
142
+ return false;
143
+ }
144
+
145
+ // Parse TRKS chunk (depends on format and TMAP)
146
+ bool trks_ok = false;
147
+ if (format_ == Format::WOZ1) {
148
+ trks_ok = parseTrksChunkWoz1(trks_data, trks_size);
149
+ } else {
150
+ trks_ok = parseTrksChunkWoz2(data, size, trks_data, trks_size);
151
+ }
152
+
153
+ if (!trks_ok) {
154
+ return false;
155
+ }
156
+
157
+ loaded_ = true;
158
+ return true;
159
+ }
160
+
161
+ bool WozDiskImage::parseInfoChunk(const uint8_t *data, uint32_t size) {
162
+ // Minimum size is 60 bytes for WOZ2, but accept smaller for WOZ1
163
+ if (size < 37) {
164
+ return false;
165
+ }
166
+
167
+ // Copy the info structure (handle size differences)
168
+ std::memset(&info_, 0, sizeof(info_));
169
+ std::memcpy(&info_, data,
170
+ std::min(size, static_cast<uint32_t>(sizeof(info_))));
171
+
172
+ // Validate disk type
173
+ if (info_.disk_type != 1 && info_.disk_type != 2) {
174
+ return false;
175
+ }
176
+
177
+ return true;
178
+ }
179
+
180
+ bool WozDiskImage::parseTmapChunk(const uint8_t *data, uint32_t size) {
181
+ if (size < QUARTER_TRACK_COUNT) {
182
+ return false;
183
+ }
184
+
185
+ std::memcpy(tmap_.data(), data, QUARTER_TRACK_COUNT);
186
+ return true;
187
+ }
188
+
189
+ bool WozDiskImage::parseTrksChunkWoz1(const uint8_t *data, uint32_t size) {
190
+ // WOZ1 TRKS: each track entry is 6656 bytes total
191
+ // - Bytes 0-6645: Bitstream data (up to 6646 bytes)
192
+ // - Bytes 6646-6647: bytes_used (uint16 LE)
193
+ // - Bytes 6648-6649: bit_count (uint16 LE)
194
+ // - Bytes 6650-6651: splice_point (uint16 LE)
195
+ // - Byte 6652: splice_nibble
196
+ // - Byte 6653: splice_bit_count
197
+ // - Bytes 6654-6655: reserved
198
+ static constexpr size_t WOZ1_ENTRY_SIZE = 6656;
199
+ static constexpr size_t WOZ1_BYTES_USED_OFFSET = 6646;
200
+ static constexpr size_t WOZ1_BIT_COUNT_OFFSET = 6648;
201
+
202
+ // Count how many tracks we have
203
+ size_t track_count = size / WOZ1_ENTRY_SIZE;
204
+
205
+ tracks_.resize(track_count);
206
+
207
+ for (size_t i = 0; i < track_count; i++) {
208
+ const uint8_t *entry = data + i * WOZ1_ENTRY_SIZE;
209
+ uint16_t bytes_used =
210
+ entry[WOZ1_BYTES_USED_OFFSET] | (entry[WOZ1_BYTES_USED_OFFSET + 1] << 8);
211
+ uint16_t bit_count =
212
+ entry[WOZ1_BIT_COUNT_OFFSET] | (entry[WOZ1_BIT_COUNT_OFFSET + 1] << 8);
213
+
214
+ if (bytes_used > 0 && bytes_used <= 6646) {
215
+ tracks_[i].bit_count = bit_count;
216
+ tracks_[i].bits.assign(entry, entry + bytes_used);
217
+ tracks_[i].valid = true;
218
+ }
219
+ }
220
+
221
+ return true;
222
+ }
223
+
224
+ bool WozDiskImage::parseTrksChunkWoz2(const uint8_t *file_data, size_t file_size,
225
+ const uint8_t *trks_data,
226
+ uint32_t trks_size) {
227
+ // WOZ2 TRKS: 160 track entries (8 bytes each) = 1280 bytes
228
+ // Track data follows at block offsets specified in entries
229
+ static constexpr size_t WOZ2_TRK_TABLE_SIZE = 160 * sizeof(Woz2TrackEntry);
230
+
231
+ if (trks_size < WOZ2_TRK_TABLE_SIZE) {
232
+ return false;
233
+ }
234
+
235
+ const auto *entries = reinterpret_cast<const Woz2TrackEntry *>(trks_data);
236
+
237
+ // Find max track index referenced by TMAP
238
+ int max_track_index = -1;
239
+ for (int i = 0; i < QUARTER_TRACK_COUNT; i++) {
240
+ if (tmap_[i] != NO_TRACK && tmap_[i] > max_track_index) {
241
+ max_track_index = tmap_[i];
242
+ }
243
+ }
244
+
245
+ if (max_track_index < 0) {
246
+ return true; // No tracks - empty disk
247
+ }
248
+
249
+ tracks_.resize(max_track_index + 1);
250
+
251
+ // Load each track referenced by TMAP
252
+ for (int i = 0; i <= max_track_index; i++) {
253
+ const auto &entry = entries[i];
254
+
255
+ if (entry.starting_block == 0 || entry.block_count == 0) {
256
+ continue; // Empty track
257
+ }
258
+
259
+ // Calculate offset in file (blocks start at offset 0 relative to start of
260
+ // file)
261
+ size_t track_offset =
262
+ static_cast<size_t>(entry.starting_block) * WOZ2_TRACK_BLOCK_SIZE;
263
+ size_t track_size =
264
+ static_cast<size_t>(entry.block_count) * WOZ2_TRACK_BLOCK_SIZE;
265
+
266
+ if (track_offset + track_size > file_size) {
267
+ continue; // Track extends past end of file
268
+ }
269
+
270
+ tracks_[i].bit_count = entry.bit_count;
271
+ tracks_[i].bits.assign(file_data + track_offset,
272
+ file_data + track_offset + track_size);
273
+ tracks_[i].valid = true;
274
+ }
275
+
276
+ return true;
277
+ }
278
+
279
+ void WozDiskImage::createBlank() {
280
+ reset();
281
+
282
+ // Set up as WOZ2 format
283
+ format_ = Format::WOZ2;
284
+
285
+ // Initialize INFO chunk for a 5.25" disk
286
+ info_.version = 2;
287
+ info_.disk_type = 1; // 5.25"
288
+ info_.write_protected = 0; // Not write protected
289
+ info_.synchronized = 0;
290
+ info_.cleaned = 1;
291
+ std::strncpy(info_.creator, "A2E Emulator", sizeof(info_.creator));
292
+ info_.disk_sides = 1;
293
+ info_.boot_sector_format = 0; // Unknown
294
+ info_.optimal_bit_timing = 32; // 4 microseconds
295
+ info_.compatible_hardware = 0;
296
+ info_.required_ram = 0;
297
+ info_.largest_track = 13; // 13 blocks per track
298
+
299
+ // Standard 5.25" disk parameters
300
+ static constexpr int NUM_TRACKS = 35;
301
+ static constexpr uint32_t BITS_PER_TRACK = 51200; // Standard track length in bits
302
+ static constexpr size_t BYTES_PER_TRACK = (BITS_PER_TRACK + 7) / 8; // 6400 bytes
303
+
304
+ // Initialize TMAP - map quarter-tracks to whole tracks
305
+ for (int qt = 0; qt < QUARTER_TRACK_COUNT; qt++) {
306
+ int track = qt / 4;
307
+ if (track < NUM_TRACKS) {
308
+ tmap_[qt] = static_cast<uint8_t>(track);
309
+ } else {
310
+ tmap_[qt] = NO_TRACK;
311
+ }
312
+ }
313
+
314
+ // Initialize tracks with sync bytes (0xFF)
315
+ tracks_.resize(NUM_TRACKS);
316
+ for (int t = 0; t < NUM_TRACKS; t++) {
317
+ tracks_[t].bit_count = BITS_PER_TRACK;
318
+ tracks_[t].bits.resize(BYTES_PER_TRACK, 0xFF); // Fill with sync bytes
319
+ tracks_[t].valid = true;
320
+ }
321
+
322
+ loaded_ = true;
323
+ modified_ = true; // Mark as modified so it will be saved
324
+ }
325
+
326
+ bool WozDiskImage::isLoaded() const { return loaded_; }
327
+
328
+ DiskImage::Format WozDiskImage::getFormat() const { return format_; }
329
+
330
+ int WozDiskImage::getTrackCount() const {
331
+ // Standard 5.25" disk has 35 tracks
332
+ return 35;
333
+ }
334
+
335
+ // ===== Head Positioning =====
336
+
337
+ void WozDiskImage::setPhase(int phase, bool on) {
338
+ if (phase < 0 || phase > 3) {
339
+ return;
340
+ }
341
+
342
+ uint8_t phase_bit = 1 << phase;
343
+
344
+ if (on) {
345
+ phase_states_ |= phase_bit;
346
+ } else {
347
+ phase_states_ &= ~phase_bit;
348
+ // Stepping happens when the current phase is turned OFF
349
+ // and an adjacent phase is ON (like apple2ts)
350
+ updateHeadPosition();
351
+ }
352
+ }
353
+
354
+ void WozDiskImage::updateHeadPosition() {
355
+ // The Disk II stepper motor only moves when:
356
+ // 1. The current phase (where head is settled) is turned OFF
357
+ // 2. An adjacent phase is ON
358
+ //
359
+ // This matches apple2ts behavior and real hardware.
360
+
361
+ // Check if current phase is now off
362
+ uint8_t current_phase_bit = 1 << current_phase_;
363
+ if (phase_states_ & current_phase_bit) {
364
+ // Current phase is still on, don't step
365
+ return;
366
+ }
367
+
368
+ // Current phase is off - check adjacent phases
369
+ int next_phase = (current_phase_ + 1) % 4;
370
+ int prev_phase = (current_phase_ + 3) % 4;
371
+
372
+ bool next_on = (phase_states_ & (1 << next_phase)) != 0;
373
+ bool prev_on = (phase_states_ & (1 << prev_phase)) != 0;
374
+
375
+ if (next_on && !prev_on) {
376
+ // Step inward (toward higher track numbers)
377
+ current_phase_ = next_phase;
378
+ if (quarter_track_ < 158) {
379
+ quarter_track_ += 2;
380
+ } else if (quarter_track_ < 159) {
381
+ quarter_track_ = 159;
382
+ }
383
+ } else if (prev_on && !next_on) {
384
+ // Step outward (toward track 0)
385
+ current_phase_ = prev_phase;
386
+ if (quarter_track_ > 1) {
387
+ quarter_track_ -= 2;
388
+ } else if (quarter_track_ > 0) {
389
+ quarter_track_ = 0;
390
+ }
391
+ }
392
+ // If both or neither adjacent phases are on, don't step
393
+ }
394
+
395
+ int WozDiskImage::getQuarterTrack() const { return quarter_track_; }
396
+
397
+ int WozDiskImage::getTrack() const { return quarter_track_ / 4; }
398
+
399
+ void WozDiskImage::setQuarterTrack(int quarter_track) {
400
+ quarter_track_ = std::max(0, std::min(quarter_track, QUARTER_TRACK_COUNT - 1));
401
+ }
402
+
403
+ bool WozDiskImage::hasData() const {
404
+ if (quarter_track_ < 0 || quarter_track_ >= QUARTER_TRACK_COUNT) {
405
+ return false;
406
+ }
407
+ return tmap_[quarter_track_] != NO_TRACK;
408
+ }
409
+
410
+ const WozDiskImage::TrackData *WozDiskImage::getCurrentTrackData() const {
411
+ if (quarter_track_ < 0 || quarter_track_ >= QUARTER_TRACK_COUNT) {
412
+ return nullptr;
413
+ }
414
+
415
+ uint8_t track_index = tmap_[quarter_track_];
416
+ if (track_index == NO_TRACK) {
417
+ return nullptr;
418
+ }
419
+ if (track_index >= static_cast<uint8_t>(tracks_.size())) {
420
+ return nullptr;
421
+ }
422
+
423
+ const TrackData &track = tracks_[track_index];
424
+ if (!track.valid) {
425
+ return nullptr;
426
+ }
427
+ return &track;
428
+ }
429
+
430
+ // Timing constants for WOZ format
431
+ // Disk spins at ~300 RPM = 5 revolutions/second
432
+ // Each bit takes approximately 4 microseconds = 4 cycles at 1.023 MHz
433
+ static constexpr uint64_t CYCLES_PER_BIT = 4;
434
+
435
+ void WozDiskImage::advanceBitPosition(uint64_t elapsed_cycles) {
436
+ const TrackData *track = getCurrentTrackData();
437
+ if (!track || track->bit_count == 0) {
438
+ return;
439
+ }
440
+
441
+ // Calculate how many bits have passed
442
+ uint32_t bits_elapsed = static_cast<uint32_t>(elapsed_cycles / CYCLES_PER_BIT);
443
+
444
+ // Advance bit position (wrapping around track)
445
+ bit_position_ = (bit_position_ + bits_elapsed) % track->bit_count;
446
+ }
447
+
448
+ uint8_t WozDiskImage::readBitInternal() const {
449
+ const TrackData *track = getCurrentTrackData();
450
+ if (!track || track->bit_count == 0) {
451
+ return 0;
452
+ }
453
+
454
+ // Wrap bit position to track length
455
+ uint32_t pos = bit_position_ % track->bit_count;
456
+
457
+ // Calculate byte and bit offsets
458
+ uint32_t byte_offset = pos / 8;
459
+ uint8_t bit_offset = 7 - (pos % 8); // MSB first
460
+
461
+ if (byte_offset >= track->bits.size()) {
462
+ return 0;
463
+ }
464
+
465
+ return (track->bits[byte_offset] >> bit_offset) & 1;
466
+ }
467
+
468
+ uint8_t WozDiskImage::readNibble() {
469
+ const TrackData *track = getCurrentTrackData();
470
+ if (!track || track->bit_count == 0) {
471
+ return 0;
472
+ }
473
+
474
+ // Read bits until we get a nibble (byte with high bit set)
475
+ // The Disk II hardware shifts bits into a register until bit 7 is set
476
+ uint8_t value = 0;
477
+ int bits_read = 0;
478
+ static constexpr int MAX_BITS = 64; // Safety limit
479
+
480
+ while (bits_read < MAX_BITS) {
481
+ uint8_t bit = readBitInternal();
482
+ bit_position_ = (bit_position_ + 1) % track->bit_count;
483
+ bits_read++;
484
+
485
+ if (bit) {
486
+ // Got a 1 bit - start/continue building nibble
487
+ value = (value << 1) | 1;
488
+ } else if (value != 0) {
489
+ // Got a 0 bit after a 1 - continue building nibble
490
+ value = value << 1;
491
+ }
492
+ // If value is 0 and bit is 0, we're still in sync bits - skip
493
+
494
+ // Check if we have a complete nibble (bit 7 set)
495
+ if (value & 0x80) {
496
+ return value;
497
+ }
498
+ }
499
+
500
+ // Timeout - return whatever we have
501
+ return value;
502
+ }
503
+
504
+ bool WozDiskImage::isWriteProtected() const {
505
+ return info_.write_protected != 0;
506
+ }
507
+
508
+ std::string WozDiskImage::getFormatName() const {
509
+ switch (format_) {
510
+ case Format::WOZ1:
511
+ return "WOZ 1.0";
512
+ case Format::WOZ2:
513
+ return "WOZ 2.0";
514
+ default:
515
+ return "Unknown";
516
+ }
517
+ }
518
+
519
+ uint8_t WozDiskImage::getDiskType() const { return info_.disk_type; }
520
+
521
+ uint8_t WozDiskImage::getOptimalBitTiming() const {
522
+ // Default to 32 (4 microseconds) if not specified
523
+ return info_.optimal_bit_timing ? info_.optimal_bit_timing : 32;
524
+ }
525
+
526
+ std::string WozDiskImage::getDiskTypeString() const {
527
+ switch (info_.disk_type) {
528
+ case 1:
529
+ return "5.25\"";
530
+ case 2:
531
+ return "3.5\"";
532
+ default:
533
+ return "Unknown";
534
+ }
535
+ }
536
+
537
+ // ===== Bit-Level Access (for LSS) =====
538
+
539
+ uint8_t WozDiskImage::readBit() {
540
+ const TrackData *track = getCurrentTrackData();
541
+ if (!track || track->bit_count == 0) return 0;
542
+ uint8_t bit = readBitInternal();
543
+ bit_position_ = (bit_position_ + 1) % track->bit_count;
544
+ return bit;
545
+ }
546
+
547
+ void WozDiskImage::writeBit(uint8_t bit) {
548
+ TrackData *track = getMutableCurrentTrackData();
549
+ if (!track || track->bit_count == 0) return;
550
+ writeBitInternal(bit);
551
+ bit_position_ = (bit_position_ + 1) % track->bit_count;
552
+ modified_ = true;
553
+ }
554
+
555
+ // ===== Write Operations =====
556
+
557
+ WozDiskImage::TrackData *WozDiskImage::getMutableCurrentTrackData() {
558
+ if (quarter_track_ < 0 || quarter_track_ >= QUARTER_TRACK_COUNT) {
559
+ return nullptr;
560
+ }
561
+
562
+ uint8_t track_index = tmap_[quarter_track_];
563
+ if (track_index == NO_TRACK ||
564
+ track_index >= static_cast<uint8_t>(tracks_.size())) {
565
+ return nullptr;
566
+ }
567
+
568
+ TrackData &track = tracks_[track_index];
569
+ return track.valid ? &track : nullptr;
570
+ }
571
+
572
+ void WozDiskImage::writeBitInternal(uint8_t bit) {
573
+ TrackData *track = getMutableCurrentTrackData();
574
+ if (!track || track->bit_count == 0) {
575
+ return;
576
+ }
577
+
578
+ // Wrap bit position to track length
579
+ uint32_t pos = bit_position_ % track->bit_count;
580
+
581
+ // Calculate byte and bit offsets
582
+ uint32_t byte_offset = pos / 8;
583
+ uint8_t bit_offset = 7 - (pos % 8); // MSB first
584
+
585
+ if (byte_offset >= track->bits.size()) {
586
+ return;
587
+ }
588
+
589
+ // Clear the bit first, then set if needed
590
+ track->bits[byte_offset] &= ~(1 << bit_offset);
591
+ if (bit) {
592
+ track->bits[byte_offset] |= (1 << bit_offset);
593
+ }
594
+ }
595
+
596
+ void WozDiskImage::writeNibble(uint8_t nibble) {
597
+ if (!loaded_ || isWriteProtected()) {
598
+ return;
599
+ }
600
+
601
+ TrackData *track = getMutableCurrentTrackData();
602
+ if (!track || track->bit_count == 0) {
603
+ return;
604
+ }
605
+
606
+ // Write 8 bits, MSB first
607
+ for (int i = 7; i >= 0; i--) {
608
+ writeBitInternal((nibble >> i) & 1);
609
+ bit_position_ = (bit_position_ + 1) % track->bit_count;
610
+ }
611
+
612
+ modified_ = true;
613
+ }
614
+
615
+ const uint8_t *WozDiskImage::getSectorData(size_t *size) const {
616
+ if (!loaded_) {
617
+ *size = 0;
618
+ return nullptr;
619
+ }
620
+
621
+ // Try to decode sectors if not already done
622
+ if (!sectors_decoded_) {
623
+ if (!decodeSectors()) {
624
+ *size = 0;
625
+ return nullptr;
626
+ }
627
+ }
628
+
629
+ *size = decoded_sectors_.size();
630
+ return decoded_sectors_.data();
631
+ }
632
+
633
+ uint8_t WozDiskImage::getNibbleAt(int track, int position) const {
634
+ // WOZ stores bit-level data, not nibbles directly
635
+ // This would require significant work to implement
636
+ (void)track;
637
+ (void)position;
638
+ return 0;
639
+ }
640
+
641
+ int WozDiskImage::getTrackNibbleCount(int track) const {
642
+ // WOZ stores bit-level data
643
+ (void)track;
644
+ return 0;
645
+ }
646
+
647
+ const uint8_t *WozDiskImage::exportData(size_t *size) {
648
+ if (!loaded_) {
649
+ *size = 0;
650
+ return nullptr;
651
+ }
652
+
653
+ // Calculate required size for WOZ2 format
654
+ // Header: 12 bytes
655
+ // INFO chunk: 8 (header) + 60 (data) = 68 bytes
656
+ // TMAP chunk: 8 (header) + 160 (data) = 168 bytes
657
+ // TRKS chunk: 8 (header) + 1280 (track table) = 1288 bytes
658
+ // Total header area: pad to block 3 (1536 bytes)
659
+ static constexpr size_t HEADER_SIZE = 12;
660
+ static constexpr size_t INFO_CHUNK_SIZE = 68;
661
+ static constexpr size_t TMAP_CHUNK_SIZE = 168;
662
+ static constexpr size_t TRKS_HEADER_SIZE = 8;
663
+ static constexpr size_t TRKS_TABLE_SIZE = 160 * 8; // 1280 bytes
664
+ static constexpr size_t TRACK_DATA_START_BLOCK = 3;
665
+ static constexpr size_t BLOCK_SIZE = 512;
666
+
667
+ // Count tracks and calculate total track data size
668
+ size_t total_track_blocks = 0;
669
+ for (size_t i = 0; i < tracks_.size(); i++) {
670
+ if (tracks_[i].valid && tracks_[i].bit_count > 0) {
671
+ size_t track_bytes = (tracks_[i].bit_count + 7) / 8;
672
+ size_t track_blocks = (track_bytes + BLOCK_SIZE - 1) / BLOCK_SIZE;
673
+ total_track_blocks += track_blocks;
674
+ }
675
+ }
676
+
677
+ size_t total_size = TRACK_DATA_START_BLOCK * BLOCK_SIZE + total_track_blocks * BLOCK_SIZE;
678
+ export_buffer_.resize(total_size);
679
+ std::fill(export_buffer_.begin(), export_buffer_.end(), 0);
680
+
681
+ size_t offset = 0;
682
+
683
+ // === WOZ2 Header (12 bytes) ===
684
+ export_buffer_[offset++] = 0x57; // 'W'
685
+ export_buffer_[offset++] = 0x4F; // 'O'
686
+ export_buffer_[offset++] = 0x5A; // 'Z'
687
+ export_buffer_[offset++] = 0x32; // '2'
688
+ export_buffer_[offset++] = 0xFF; // High bit
689
+ export_buffer_[offset++] = 0x0A; // LF
690
+ export_buffer_[offset++] = 0x0D; // CR
691
+ export_buffer_[offset++] = 0x0A; // LF
692
+ // CRC32 placeholder (not validated by loader)
693
+ export_buffer_[offset++] = 0x00;
694
+ export_buffer_[offset++] = 0x00;
695
+ export_buffer_[offset++] = 0x00;
696
+ export_buffer_[offset++] = 0x00;
697
+
698
+ // === INFO Chunk ===
699
+ export_buffer_[offset++] = 0x49; // 'I'
700
+ export_buffer_[offset++] = 0x4E; // 'N'
701
+ export_buffer_[offset++] = 0x46; // 'F'
702
+ export_buffer_[offset++] = 0x4F; // 'O'
703
+ // Chunk size: 60 bytes
704
+ export_buffer_[offset++] = 60;
705
+ export_buffer_[offset++] = 0;
706
+ export_buffer_[offset++] = 0;
707
+ export_buffer_[offset++] = 0;
708
+ // Copy INFO data
709
+ std::memcpy(&export_buffer_[offset], &info_, sizeof(info_));
710
+ offset += 60;
711
+
712
+ // === TMAP Chunk ===
713
+ export_buffer_[offset++] = 0x54; // 'T'
714
+ export_buffer_[offset++] = 0x4D; // 'M'
715
+ export_buffer_[offset++] = 0x41; // 'A'
716
+ export_buffer_[offset++] = 0x50; // 'P'
717
+ // Chunk size: 160 bytes
718
+ export_buffer_[offset++] = 160;
719
+ export_buffer_[offset++] = 0;
720
+ export_buffer_[offset++] = 0;
721
+ export_buffer_[offset++] = 0;
722
+ // Copy TMAP data
723
+ std::memcpy(&export_buffer_[offset], tmap_.data(), QUARTER_TRACK_COUNT);
724
+ offset += QUARTER_TRACK_COUNT;
725
+
726
+ // === TRKS Chunk ===
727
+ export_buffer_[offset++] = 0x54; // 'T'
728
+ export_buffer_[offset++] = 0x52; // 'R'
729
+ export_buffer_[offset++] = 0x4B; // 'K'
730
+ export_buffer_[offset++] = 0x53; // 'S'
731
+ // Chunk size: track table only (1280 bytes)
732
+ export_buffer_[offset++] = TRKS_TABLE_SIZE & 0xFF;
733
+ export_buffer_[offset++] = (TRKS_TABLE_SIZE >> 8) & 0xFF;
734
+ export_buffer_[offset++] = (TRKS_TABLE_SIZE >> 16) & 0xFF;
735
+ export_buffer_[offset++] = (TRKS_TABLE_SIZE >> 24) & 0xFF;
736
+
737
+ // Build track entries and copy track data
738
+ size_t current_block = TRACK_DATA_START_BLOCK;
739
+ size_t track_table_offset = offset;
740
+
741
+ for (size_t i = 0; i < 160; i++) {
742
+ if (i < tracks_.size() && tracks_[i].valid && tracks_[i].bit_count > 0) {
743
+ const TrackData &track = tracks_[i];
744
+ size_t track_bytes = (track.bit_count + 7) / 8;
745
+ size_t block_count = (track_bytes + BLOCK_SIZE - 1) / BLOCK_SIZE;
746
+
747
+ // Write track entry
748
+ export_buffer_[track_table_offset + i * 8 + 0] = current_block & 0xFF;
749
+ export_buffer_[track_table_offset + i * 8 + 1] = (current_block >> 8) & 0xFF;
750
+ export_buffer_[track_table_offset + i * 8 + 2] = block_count & 0xFF;
751
+ export_buffer_[track_table_offset + i * 8 + 3] = (block_count >> 8) & 0xFF;
752
+ export_buffer_[track_table_offset + i * 8 + 4] = track.bit_count & 0xFF;
753
+ export_buffer_[track_table_offset + i * 8 + 5] = (track.bit_count >> 8) & 0xFF;
754
+ export_buffer_[track_table_offset + i * 8 + 6] = (track.bit_count >> 16) & 0xFF;
755
+ export_buffer_[track_table_offset + i * 8 + 7] = (track.bit_count >> 24) & 0xFF;
756
+
757
+ // Copy track data
758
+ size_t data_offset = current_block * BLOCK_SIZE;
759
+ size_t copy_size = std::min(track.bits.size(), block_count * BLOCK_SIZE);
760
+ std::memcpy(&export_buffer_[data_offset], track.bits.data(), copy_size);
761
+
762
+ current_block += block_count;
763
+ } else {
764
+ // Empty track entry
765
+ std::memset(&export_buffer_[track_table_offset + i * 8], 0, 8);
766
+ }
767
+ }
768
+
769
+ *size = export_buffer_.size();
770
+ return export_buffer_.data();
771
+ }
772
+
773
+ // ===== Sector Decoding Implementation =====
774
+
775
+ uint8_t WozDiskImage::decode4and4(uint8_t odd, uint8_t even) {
776
+ // 4-and-4 encoding: odd bits in first nibble, even bits in second
777
+ // Decode by combining the low bits of each
778
+ return ((odd << 1) | 0x01) & even;
779
+ }
780
+
781
+ bool WozDiskImage::decode6and2(const uint8_t *nibbles, uint8_t *output) {
782
+ // 6-and-2 decoding: 343 nibbles -> 256 bytes
783
+ // This reverses the encoding done in gcr_encoding.cpp:
784
+ //
785
+ // Encoding creates a 342-byte buffer:
786
+ // buffer[0..85]: auxiliary buffer - 2 bits from each of 3 data bytes
787
+ // buffer[86..341]: primary buffer - 6 high bits from each of 256 data bytes
788
+ // Then XOR encodes: each byte XOR'd with previous byte's value
789
+ // Final nibble (343rd) is the checksum (last pre-XOR value)
790
+
791
+ std::array<uint8_t, 342> buffer;
792
+
793
+ // Step 1: Decode nibbles and reverse XOR encoding
794
+ // The encoder does: nibble[i] = encode(buffer[i] ^ prev), prev = buffer[i]
795
+ // To decode: buffer[i] = decode(nibble[i]) ^ prev_decoded
796
+ uint8_t prev = 0;
797
+ for (int i = 0; i < 342; i++) {
798
+ int8_t val = DECODE_6_AND_2[nibbles[i]];
799
+ if (val < 0) {
800
+ return false; // Invalid nibble
801
+ }
802
+ buffer[i] = static_cast<uint8_t>(val) ^ prev;
803
+ prev = buffer[i];
804
+ }
805
+
806
+ // Step 2: Verify checksum
807
+ // The checksum nibble encodes the last buffer value (before XOR was applied)
808
+ int8_t chk_val = DECODE_6_AND_2[nibbles[342]];
809
+ if (chk_val < 0 || prev != static_cast<uint8_t>(chk_val)) {
810
+ return false; // Checksum mismatch
811
+ }
812
+
813
+ // Step 3: Reassemble 256 bytes from the buffer
814
+ // Primary buffer (86-341) contains bits 2-7 of each byte
815
+ // Auxiliary buffer (0-85) contains bits 0-1 from groups of 3 bytes:
816
+ // buffer[i] bits 0,1 -> data[i] bits 1,0 (swapped)
817
+ // buffer[i] bits 2,3 -> data[i+86] bits 1,0 (swapped)
818
+ // buffer[i] bits 4,5 -> data[i+172] bits 1,0 (swapped)
819
+
820
+ for (int i = 0; i < 256; i++) {
821
+ // Get the 6 high bits from primary buffer
822
+ uint8_t high = buffer[86 + i] << 2;
823
+
824
+ // Get the 2 low bits from auxiliary buffer
825
+ // The aux index for byte i depends on which group of 86 it's in
826
+ int aux_idx = i % 86;
827
+ int group = i / 86; // 0, 1, or 2
828
+
829
+ uint8_t aux = buffer[aux_idx];
830
+ uint8_t low_bits;
831
+
832
+ // Extract the 2 bits for this group and unswap them
833
+ // Encoding did: ((data[i] & 0x01) << 1) | ((data[i] & 0x02) >> 1)
834
+ // So bit 0 of aux = bit 1 of data, bit 1 of aux = bit 0 of data
835
+ switch (group) {
836
+ case 0:
837
+ // Bits 0,1 of aux -> bits 0,1 of data (with swap)
838
+ low_bits = ((aux & 0x02) >> 1) | ((aux & 0x01) << 1);
839
+ break;
840
+ case 1:
841
+ // Bits 2,3 of aux -> bits 0,1 of data (with swap)
842
+ low_bits = ((aux & 0x08) >> 3) | ((aux & 0x04) >> 1);
843
+ break;
844
+ case 2:
845
+ default:
846
+ // Bits 4,5 of aux -> bits 0,1 of data (with swap)
847
+ low_bits = ((aux & 0x20) >> 5) | ((aux & 0x10) >> 3);
848
+ break;
849
+ }
850
+
851
+ output[i] = high | low_bits;
852
+ }
853
+
854
+ return true;
855
+ }
856
+
857
+ std::vector<uint8_t> WozDiskImage::readTrackNibbles(size_t track_index) const {
858
+ std::vector<uint8_t> nibbles;
859
+
860
+ if (track_index >= tracks_.size() || !tracks_[track_index].valid) {
861
+ return nibbles;
862
+ }
863
+
864
+ const TrackData &track = tracks_[track_index];
865
+ if (track.bit_count == 0) {
866
+ return nibbles;
867
+ }
868
+
869
+ nibbles.reserve(track.bit_count / 8); // Approximate
870
+
871
+ // Read nibbles by scanning through bit stream
872
+ uint32_t bit_pos = 0;
873
+ uint8_t value = 0;
874
+ int bits_read = 0;
875
+
876
+ // Read through entire track twice to ensure we get all sectors
877
+ // (sectors may wrap around the track boundary)
878
+ uint32_t total_bits = track.bit_count * 2;
879
+
880
+ while (bit_pos < total_bits) {
881
+ // Read a bit
882
+ uint32_t actual_pos = bit_pos % track.bit_count;
883
+ uint32_t byte_offset = actual_pos / 8;
884
+ uint8_t bit_offset = 7 - (actual_pos % 8);
885
+
886
+ if (byte_offset >= track.bits.size()) {
887
+ break;
888
+ }
889
+
890
+ uint8_t bit = (track.bits[byte_offset] >> bit_offset) & 1;
891
+ bit_pos++;
892
+
893
+ if (bit) {
894
+ // Got a 1 bit - start/continue building nibble
895
+ value = (value << 1) | 1;
896
+ bits_read++;
897
+ } else if (value != 0) {
898
+ // Got a 0 bit after a 1 - continue building nibble
899
+ value = value << 1;
900
+ bits_read++;
901
+ }
902
+ // If value is 0 and bit is 0, we're in sync - skip
903
+
904
+ // Check if we have a complete nibble (bit 7 set)
905
+ if (value & 0x80) {
906
+ nibbles.push_back(value);
907
+ value = 0;
908
+ bits_read = 0;
909
+
910
+ // Limit total nibbles to prevent runaway
911
+ if (nibbles.size() > 8192) {
912
+ break;
913
+ }
914
+ }
915
+
916
+ // Timeout - reset if too many bits without a valid nibble
917
+ if (bits_read > 16) {
918
+ value = 0;
919
+ bits_read = 0;
920
+ }
921
+ }
922
+
923
+ return nibbles;
924
+ }
925
+
926
+ int WozDiskImage::decodeSectorsFromNibbles(
927
+ const std::vector<uint8_t> &nibbles, int expected_track,
928
+ std::array<std::array<uint8_t, 256>, 16> &sectors) const {
929
+
930
+ // Track which sectors we've successfully decoded
931
+ std::array<bool, 16> sector_found{};
932
+ int sectors_decoded = 0;
933
+
934
+ // Search for address field prologues: D5 AA 96
935
+ for (size_t i = 0; i + 350 < nibbles.size(); i++) {
936
+ // Look for address field prologue
937
+ if (nibbles[i] != 0xD5 || nibbles[i + 1] != 0xAA || nibbles[i + 2] != 0x96) {
938
+ continue;
939
+ }
940
+
941
+ // Decode address field (4-and-4 encoded)
942
+ uint8_t volume = decode4and4(nibbles[i + 3], nibbles[i + 4]);
943
+ uint8_t track = decode4and4(nibbles[i + 5], nibbles[i + 6]);
944
+ uint8_t sector = decode4and4(nibbles[i + 7], nibbles[i + 8]);
945
+ uint8_t checksum = decode4and4(nibbles[i + 9], nibbles[i + 10]);
946
+
947
+ // Verify address checksum
948
+ if ((volume ^ track ^ sector) != checksum) {
949
+ continue;
950
+ }
951
+
952
+ // Verify track number matches (allow some tolerance for copy protection)
953
+ if (track != expected_track && track != expected_track + 1 &&
954
+ track != expected_track - 1) {
955
+ continue;
956
+ }
957
+
958
+ // Verify sector number is valid
959
+ if (sector >= 16) {
960
+ continue;
961
+ }
962
+
963
+ // Skip if we already have this sector
964
+ if (sector_found[sector]) {
965
+ continue;
966
+ }
967
+
968
+ // Search for data field prologue: D5 AA AD
969
+ // It should be within ~50 nibbles after address field
970
+ size_t data_start = 0;
971
+ for (size_t j = i + 11; j < i + 60 && j + 2 < nibbles.size(); j++) {
972
+ if (nibbles[j] == 0xD5 && nibbles[j + 1] == 0xAA && nibbles[j + 2] == 0xAD) {
973
+ data_start = j + 3;
974
+ break;
975
+ }
976
+ }
977
+
978
+ if (data_start == 0 || data_start + 343 > nibbles.size()) {
979
+ continue;
980
+ }
981
+
982
+ // Decode 6-and-2 data (343 nibbles: 342 data + 1 checksum)
983
+ if (decode6and2(&nibbles[data_start], sectors[sector].data())) {
984
+ sector_found[sector] = true;
985
+ sectors_decoded++;
986
+
987
+ // If we have all 16 sectors, we're done
988
+ if (sectors_decoded == 16) {
989
+ break;
990
+ }
991
+ }
992
+ }
993
+
994
+ return sectors_decoded;
995
+ }
996
+
997
+ bool WozDiskImage::decodeSectors() const {
998
+ if (!loaded_) {
999
+ return false;
1000
+ }
1001
+
1002
+ // Standard DOS 3.3 disk: 35 tracks, 16 sectors, 256 bytes = 143,360 bytes
1003
+ static constexpr size_t DISK_SIZE = 35 * 16 * 256;
1004
+ decoded_sectors_.resize(DISK_SIZE);
1005
+ std::fill(decoded_sectors_.begin(), decoded_sectors_.end(), 0);
1006
+
1007
+ int total_sectors_decoded = 0;
1008
+
1009
+ // Decode each track
1010
+ for (int track = 0; track < 35; track++) {
1011
+ // Find the track index from TMAP (use whole track position)
1012
+ int quarter_track = track * 4;
1013
+ if (quarter_track >= QUARTER_TRACK_COUNT) {
1014
+ continue;
1015
+ }
1016
+
1017
+ uint8_t track_index = tmap_[quarter_track];
1018
+ if (track_index == NO_TRACK || track_index >= tracks_.size()) {
1019
+ continue;
1020
+ }
1021
+
1022
+ // Read nibbles from track
1023
+ std::vector<uint8_t> nibbles = readTrackNibbles(track_index);
1024
+ if (nibbles.empty()) {
1025
+ continue;
1026
+ }
1027
+
1028
+ // Decode sectors from nibbles
1029
+ std::array<std::array<uint8_t, 256>, 16> track_sectors{};
1030
+ int decoded = decodeSectorsFromNibbles(nibbles, track, track_sectors);
1031
+ total_sectors_decoded += decoded;
1032
+
1033
+ // Copy decoded sectors to output buffer
1034
+ // Use DOS 3.3 physical-to-logical mapping (most common for WOZ files)
1035
+ for (int phys_sector = 0; phys_sector < 16; phys_sector++) {
1036
+ int logical_sector = DOS_PHYSICAL_TO_LOGICAL[phys_sector];
1037
+ size_t offset = (track * 16 + logical_sector) * 256;
1038
+ std::memcpy(&decoded_sectors_[offset], track_sectors[phys_sector].data(), 256);
1039
+ }
1040
+ }
1041
+
1042
+ // Consider decoding successful if we got at least 50% of sectors
1043
+ // This allows for some bad sectors while still enabling catalog reading
1044
+ sectors_decoded_ = (total_sectors_decoded >= 35 * 8);
1045
+
1046
+ return sectors_decoded_;
1047
+ }
1048
+
1049
+ } // namespace a2e