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,827 @@
1
+ /*
2
+ * dsk_disk_image.cpp - DSK/DO/PO disk image format implementation
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ #include "dsk_disk_image.hpp"
9
+ #include "gcr_encoding.hpp"
10
+ #include <algorithm>
11
+ #include <cstring>
12
+
13
+ namespace a2e {
14
+
15
+ // DOS 3.3 logical to physical sector mapping
16
+ // When reading a DSK file, logical sector N is at file offset N * 256
17
+ // But on disk, sectors are interleaved for performance
18
+ static constexpr std::array<int, 16> DOS_LOGICAL_TO_PHYSICAL = {
19
+ 0, 13, 11, 9, 7, 5, 3, 1, 14, 12, 10, 8, 6, 4, 2, 15};
20
+
21
+ // Reverse mapping: physical to logical
22
+ static constexpr std::array<int, 16> DOS_PHYSICAL_TO_LOGICAL = {
23
+ 0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15};
24
+
25
+ // ProDOS logical to physical sector mapping
26
+ static constexpr std::array<int, 16> PRODOS_LOGICAL_TO_PHYSICAL = {
27
+ 0, 2, 4, 6, 8, 10, 12, 14, 1, 3, 5, 7, 9, 11, 13, 15};
28
+
29
+ // ProDOS reverse mapping
30
+ static constexpr std::array<int, 16> PRODOS_PHYSICAL_TO_LOGICAL = {
31
+ 0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15};
32
+
33
+ // 6-and-2 decoding table (reverse of ENCODE_6_AND_2)
34
+ static constexpr std::array<int8_t, 256> DECODE_6_AND_2 = []() {
35
+ std::array<int8_t, 256> table{};
36
+ for (int i = 0; i < 256; i++) {
37
+ table[i] = -1; // Invalid by default
38
+ }
39
+ // Fill in valid mappings from the encode table
40
+ for (int i = 0; i < 64; i++) {
41
+ table[GCR::ENCODE_6_AND_2[i]] = static_cast<int8_t>(i);
42
+ }
43
+ return table;
44
+ }();
45
+
46
+ DskDiskImage::DskDiskImage() { sector_data_.fill(0); }
47
+
48
+ void DskDiskImage::resetState() {
49
+ quarter_track_ = 0;
50
+ phase_states_ = 0;
51
+ current_phase_ = 0;
52
+ nibble_position_ = 0;
53
+ bit_position_ = 0;
54
+ last_cycle_count_ = 0;
55
+ }
56
+
57
+ bool DskDiskImage::load(const uint8_t *data, size_t size,
58
+ const std::string &filename) {
59
+ // Check file size
60
+ if (size != DISK_SIZE) {
61
+ return false;
62
+ }
63
+
64
+ // Copy sector data
65
+ std::memcpy(sector_data_.data(), data, DISK_SIZE);
66
+
67
+ loaded_ = true;
68
+ modified_ = false;
69
+ filename_ = filename;
70
+
71
+ // Detect format from disk content (not file extension)
72
+ format_ = detectFormat(filename);
73
+
74
+ // Invalidate all nibble and bit tracks (will be regenerated on demand)
75
+ for (auto &track : nibble_tracks_) {
76
+ track.valid = false;
77
+ track.dirty = false;
78
+ track.nibbles.clear();
79
+ }
80
+ for (auto &track : bit_tracks_) {
81
+ track.valid = false;
82
+ track.dirty = false;
83
+ track.bits.clear();
84
+ track.bit_count = 0;
85
+ }
86
+
87
+ // Reset head position
88
+ quarter_track_ = 0;
89
+ phase_states_ = 0;
90
+ current_phase_ = 0; // Reset to phase 0 for correct stepper tracking
91
+ nibble_position_ = 0;
92
+ bit_position_ = 0;
93
+ last_cycle_count_ = 0;
94
+
95
+ return true;
96
+ }
97
+
98
+ DiskImage::Format DskDiskImage::detectFormat(const std::string &filename) const {
99
+ // Content-based format detection
100
+ // We check for filesystem signatures to determine if data is in DOS or ProDOS
101
+ // order
102
+
103
+ // Check for ProDOS volume header assuming ProDOS sector order
104
+ // Block 2 in ProDOS = offset 1024
105
+ constexpr int PRODOS_BLOCK2_OFFSET = 1024;
106
+ if (sector_data_.size() > PRODOS_BLOCK2_OFFSET + 5) {
107
+ uint8_t storage_type = sector_data_[PRODOS_BLOCK2_OFFSET + 4];
108
+ // High nibble 0xF = volume directory header, low nibble = name length
109
+ if ((storage_type & 0xF0) == 0xF0) {
110
+ int name_len = storage_type & 0x0F;
111
+ if (name_len > 0 && name_len <= 15) {
112
+ // Verify the name contains valid ProDOS characters (letters, digits,
113
+ // periods). ProDOS stores characters with high bit set (0x80 OR'd),
114
+ // so we mask it off before checking.
115
+ bool valid_name = true;
116
+ for (int i = 0; i < name_len && valid_name; i++) {
117
+ uint8_t c = sector_data_[PRODOS_BLOCK2_OFFSET + 5 + i];
118
+ uint8_t ch = c & 0x7F; // Strip high bit
119
+ // ProDOS names: A-Z, 0-9, period
120
+ bool is_letter = (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
121
+ bool is_digit = (ch >= '0' && ch <= '9');
122
+ bool is_period = (ch == '.');
123
+ if (!is_letter && !is_digit && !is_period) {
124
+ valid_name = false;
125
+ }
126
+ }
127
+ if (valid_name) {
128
+ return Format::PO;
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ // Check for DOS 3.3 VTOC assuming DOS sector order
135
+ // Track 17, Sector 0 = offset 69632
136
+ constexpr int DOS_VTOC_OFFSET = 17 * 16 * 256;
137
+ if (sector_data_.size() > DOS_VTOC_OFFSET + 4) {
138
+ uint8_t catalog_track = sector_data_[DOS_VTOC_OFFSET + 1];
139
+ uint8_t catalog_sector = sector_data_[DOS_VTOC_OFFSET + 2];
140
+ uint8_t dos_version = sector_data_[DOS_VTOC_OFFSET + 3];
141
+
142
+ // Valid DOS 3.3 VTOC:
143
+ // - catalog track is typically 17 (0x11) or nearby
144
+ // - catalog sector is typically 15 (0x0F)
145
+ // - DOS version is 3 (0x03)
146
+ bool valid_catalog_track = (catalog_track >= 0x11 && catalog_track <= 0x14);
147
+ bool valid_catalog_sector = (catalog_sector <= 0x0F);
148
+ bool valid_dos_version = (dos_version == 0x03);
149
+
150
+ if (valid_catalog_track && valid_catalog_sector && valid_dos_version) {
151
+ return Format::DSK;
152
+ }
153
+ }
154
+
155
+ // Fall back to extension-based detection
156
+ std::string ext = filename;
157
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
158
+
159
+ if (ext.find(".po") != std::string::npos) {
160
+ return Format::PO;
161
+ }
162
+
163
+ // Default to DOS order for .dsk and .do files
164
+ return Format::DSK;
165
+ }
166
+
167
+ std::string DskDiskImage::getFormatName() const {
168
+ switch (format_) {
169
+ case Format::DSK:
170
+ return "DSK (DOS order)";
171
+ case Format::DO:
172
+ return "DO (DOS order)";
173
+ case Format::PO:
174
+ return "PO (ProDOS order)";
175
+ default:
176
+ return "Unknown";
177
+ }
178
+ }
179
+
180
+ int DskDiskImage::getLogicalSector(int physical_sector) const {
181
+ if (physical_sector < 0 || physical_sector >= SECTORS_PER_TRACK)
182
+ return 0;
183
+
184
+ if (format_ == Format::PO) {
185
+ return PRODOS_PHYSICAL_TO_LOGICAL[physical_sector];
186
+ } else {
187
+ return DOS_PHYSICAL_TO_LOGICAL[physical_sector];
188
+ }
189
+ }
190
+
191
+ void DskDiskImage::nibblizeTrack(int track) {
192
+ if (track < 0 || track >= TRACKS)
193
+ return;
194
+
195
+ auto &nt = nibble_tracks_[track];
196
+ nt.nibbles.clear();
197
+ nt.is_sync.clear();
198
+ nt.nibbles.reserve(NIBBLES_PER_TRACK);
199
+ nt.is_sync.reserve(NIBBLES_PER_TRACK);
200
+
201
+ // Helper: push a data byte (8-bit in bit stream)
202
+ auto pushData = [&](uint8_t val) {
203
+ nt.nibbles.push_back(val);
204
+ nt.is_sync.push_back(false);
205
+ };
206
+
207
+ // Helper: push a sync byte (10-bit self-sync FF in bit stream)
208
+ auto pushSync = [&]() {
209
+ nt.nibbles.push_back(0xFF);
210
+ nt.is_sync.push_back(true);
211
+ };
212
+
213
+ // Build each sector using the exact structure from the working version
214
+ for (int physical_sector = 0; physical_sector < SECTORS_PER_TRACK;
215
+ physical_sector++) {
216
+ // Map physical sector to DOS logical sector
217
+ int dos_sector = getLogicalSector(physical_sector);
218
+
219
+ // Get sector data
220
+ int offset = (track * SECTORS_PER_TRACK + dos_sector) * BYTES_PER_SECTOR;
221
+ const uint8_t *data = &sector_data_[offset];
222
+
223
+ // Gap 1 (first sector) or Gap 3 (between sectors)
224
+ int gap;
225
+ if (physical_sector == 0) {
226
+ gap = 0x80; // Gap 1: 128 bytes
227
+ } else {
228
+ gap = (track == 0) ? 0x28 : 0x26; // Gap 3: 40 or 38 bytes
229
+ }
230
+ for (int i = 0; i < gap; ++i) {
231
+ pushSync();
232
+ }
233
+
234
+ // === Address Field ===
235
+ // Prologue
236
+ pushData(0xD5);
237
+ pushData(0xAA);
238
+ pushData(0x96);
239
+
240
+ // 4-and-4 encoded values
241
+ auto encode44 = [&](uint8_t val) {
242
+ pushData((val >> 1) | 0xAA);
243
+ pushData(val | 0xAA);
244
+ };
245
+
246
+ uint8_t checksum = volume_number_ ^ track ^ physical_sector;
247
+ encode44(volume_number_);
248
+ encode44(track);
249
+ encode44(physical_sector);
250
+ encode44(checksum);
251
+
252
+ // Epilogue
253
+ pushData(0xDE);
254
+ pushData(0xAA);
255
+ pushData(0xEB);
256
+
257
+ // Gap 2: 5 bytes
258
+ for (int i = 0; i < 5; ++i) {
259
+ pushSync();
260
+ }
261
+
262
+ // === Data Field ===
263
+ // Prologue
264
+ pushData(0xD5);
265
+ pushData(0xAA);
266
+ pushData(0xAD);
267
+
268
+ // 6-and-2 encode the sector data
269
+ auto encoded = GCR::encode6and2(data);
270
+ for (auto byte : encoded) {
271
+ pushData(byte);
272
+ }
273
+
274
+ // Epilogue
275
+ pushData(0xDE);
276
+ pushData(0xAA);
277
+ pushData(0xEB);
278
+
279
+ // Gap 3 end: 1 byte
280
+ pushSync();
281
+ }
282
+
283
+ // Pad or truncate to standard track size
284
+ while (nt.nibbles.size() < NIBBLES_PER_TRACK) {
285
+ pushSync();
286
+ }
287
+ if (nt.nibbles.size() > NIBBLES_PER_TRACK) {
288
+ nt.nibbles.resize(NIBBLES_PER_TRACK);
289
+ nt.is_sync.resize(NIBBLES_PER_TRACK);
290
+ }
291
+
292
+ nt.valid = true;
293
+ nt.dirty = false;
294
+
295
+ // Invalidate corresponding bit track (will be regenerated on demand)
296
+ bit_tracks_[track].valid = false;
297
+ bit_tracks_[track].dirty = false;
298
+ bit_tracks_[track].bits.clear();
299
+ bit_tracks_[track].bit_count = 0;
300
+ }
301
+
302
+ uint8_t DskDiskImage::decode4and4(uint8_t odd, uint8_t even) {
303
+ // Reverse of encode4and4:
304
+ // odd has bits 7,5,3,1 of original in positions 6,4,2,0 (masked with 0x55,
305
+ // OR'd with 0xAA) even has bits 6,4,2,0 of original in positions 6,4,2,0
306
+ uint8_t result = ((odd << 1) & 0xAA) | (even & 0x55);
307
+ return result;
308
+ }
309
+
310
+ bool DskDiskImage::decode6and2(const uint8_t *encoded, uint8_t *output) {
311
+ // Decode 343 nibbles back to 256 bytes
312
+ // First, convert disk nibbles to 6-bit values
313
+ uint8_t buffer[342];
314
+
315
+ // XOR decode (reverse of encode)
316
+ uint8_t prev = 0;
317
+ for (int i = 0; i < 342; i++) {
318
+ int8_t decoded = DECODE_6_AND_2[encoded[i]];
319
+ if (decoded < 0) {
320
+ return false; // Invalid nibble
321
+ }
322
+ buffer[i] = decoded ^ prev;
323
+ prev = buffer[i];
324
+ }
325
+
326
+ // Verify checksum
327
+ int8_t checksum_decoded = DECODE_6_AND_2[encoded[342]];
328
+ if (checksum_decoded < 0 || (prev & 0x3F) != (checksum_decoded & 0x3F)) {
329
+ // Checksum mismatch - still try to decode
330
+ // Some disk images have minor errors
331
+ }
332
+
333
+ // Reconstruct 256 bytes from auxiliary (86) and primary (256) buffers
334
+ for (int i = 0; i < 256; i++) {
335
+ // High 6 bits from primary buffer
336
+ uint8_t high = buffer[86 + i] << 2;
337
+
338
+ // Low 2 bits from auxiliary buffer
339
+ // The encoder swaps bits 0 and 1, so we need to swap them back
340
+ uint8_t aux_byte = buffer[i % 86];
341
+ int shift = (i / 86) * 2;
342
+ uint8_t low = (aux_byte >> shift) & 0x03;
343
+ // Reverse the bit swap: ((low & 0x01) << 1) | ((low & 0x02) >> 1)
344
+ low = ((low & 0x01) << 1) | ((low & 0x02) >> 1);
345
+
346
+ output[i] = high | low;
347
+ }
348
+
349
+ return true;
350
+ }
351
+
352
+ void DskDiskImage::denibblizeTrack(int track) {
353
+ if (track < 0 || track >= TRACKS)
354
+ return;
355
+
356
+ auto &nt = nibble_tracks_[track];
357
+ if (!nt.valid || !nt.dirty)
358
+ return;
359
+
360
+ // Create an extended buffer that handles wrap-around at track boundary.
361
+ // When ProDOS writes near the end of a track, data can wrap to the beginning.
362
+ // We append a copy of the first ~500 nibbles to handle this case.
363
+ std::vector<uint8_t> nibbles = nt.nibbles; // Make a copy
364
+ const size_t original_size = nibbles.size();
365
+ const size_t wrap_extension = 500; // Enough for one full sector
366
+ if (original_size > wrap_extension) {
367
+ nibbles.insert(nibbles.end(), nt.nibbles.begin(),
368
+ nt.nibbles.begin() + wrap_extension);
369
+ }
370
+
371
+ size_t pos = 0;
372
+ size_t size = original_size; // Only search within one revolution
373
+
374
+ // Find and decode each sector
375
+ while (pos < size) {
376
+ // Look for address field prologue: D5 AA 96
377
+ bool found_addr = false;
378
+ while (pos + 3 < size) {
379
+ if (nibbles[pos] == 0xD5 && nibbles[pos + 1] == 0xAA &&
380
+ nibbles[pos + 2] == 0x96) {
381
+ found_addr = true;
382
+ pos += 3;
383
+ break;
384
+ }
385
+ pos++;
386
+ }
387
+
388
+ if (!found_addr)
389
+ break;
390
+
391
+ // Read address field (4-and-4 encoded: volume, track, sector, checksum)
392
+ // Use extended buffer size in case address field wraps around
393
+ if (pos + 8 > nibbles.size())
394
+ break;
395
+
396
+ uint8_t volume = decode4and4(nibbles[pos], nibbles[pos + 1]);
397
+ pos += 2;
398
+ uint8_t addr_track = decode4and4(nibbles[pos], nibbles[pos + 1]);
399
+ pos += 2;
400
+ uint8_t sector = decode4and4(nibbles[pos], nibbles[pos + 1]);
401
+ pos += 2;
402
+ uint8_t checksum = decode4and4(nibbles[pos], nibbles[pos + 1]);
403
+ pos += 2;
404
+
405
+ // Verify address checksum using the volume read from the disk,
406
+ // not volume_number_ which may be different
407
+ if ((volume ^ addr_track ^ sector) != checksum) {
408
+ continue; // Invalid address field
409
+ }
410
+
411
+ // Verify track number matches
412
+ if (addr_track != track) {
413
+ continue; // Wrong track
414
+ }
415
+
416
+ // Skip address epilogue and look for data prologue: D5 AA AD
417
+ // Use nibbles.size() (extended buffer) to allow finding data prologue
418
+ // that wraps around the track boundary
419
+ bool found_data = false;
420
+ size_t search_limit = pos + 50; // Don't search too far
421
+ while (pos + 3 <= nibbles.size() && pos < search_limit) {
422
+ if (nibbles[pos] == 0xD5 && nibbles[pos + 1] == 0xAA &&
423
+ nibbles[pos + 2] == 0xAD) {
424
+ found_data = true;
425
+ pos += 3;
426
+ break;
427
+ }
428
+ pos++;
429
+ }
430
+
431
+ if (!found_data)
432
+ continue;
433
+
434
+ // Read 343 nibbles of data field
435
+ // Use nibbles.size() (extended buffer) instead of size (original) for bounds check
436
+ if (pos + 343 > nibbles.size())
437
+ break;
438
+
439
+ // Decode the sector data
440
+ uint8_t decoded[256];
441
+ if (decode6and2(nibbles.data() + pos, decoded)) {
442
+ // Write to sector data array
443
+ int log_sector = getLogicalSector(sector);
444
+ int offset = (track * SECTORS_PER_TRACK + log_sector) * BYTES_PER_SECTOR;
445
+ std::memcpy(&sector_data_[offset], decoded, BYTES_PER_SECTOR);
446
+ }
447
+
448
+ pos += 343;
449
+ }
450
+
451
+ nt.dirty = false;
452
+ modified_ = true;
453
+ }
454
+
455
+ void DskDiskImage::setPhase(int phase, bool on) {
456
+ if (phase < 0 || phase > 3)
457
+ return;
458
+
459
+ uint8_t phase_bit = 1 << phase;
460
+
461
+ if (on) {
462
+ phase_states_ |= phase_bit;
463
+ } else {
464
+ phase_states_ &= ~phase_bit;
465
+ // Stepping happens when the current phase is turned OFF
466
+ // and an adjacent phase is ON (like apple2ts)
467
+ updateHeadPosition();
468
+ }
469
+ }
470
+
471
+ void DskDiskImage::updateHeadPosition() {
472
+ // The Disk II stepper motor only moves when:
473
+ // 1. The current phase (where head is settled) is turned OFF
474
+ // 2. An adjacent phase is ON
475
+ //
476
+ // This matches apple2ts behavior and real hardware.
477
+
478
+ constexpr int MAX_QUARTER_TRACK = (TRACKS * 4) - 1;
479
+
480
+ // Check if current phase is now off
481
+ uint8_t current_phase_bit = 1 << current_phase_;
482
+ if (phase_states_ & current_phase_bit) {
483
+ // Current phase is still on, don't step
484
+ return;
485
+ }
486
+
487
+ // Current phase is off - check adjacent phases
488
+ int next_phase = (current_phase_ + 1) % 4;
489
+ int prev_phase = (current_phase_ + 3) % 4;
490
+
491
+ bool next_on = (phase_states_ & (1 << next_phase)) != 0;
492
+ bool prev_on = (phase_states_ & (1 << prev_phase)) != 0;
493
+
494
+ if (next_on && !prev_on) {
495
+ // Step inward (toward higher track numbers)
496
+ current_phase_ = next_phase;
497
+ if (quarter_track_ < MAX_QUARTER_TRACK - 1) {
498
+ quarter_track_ += 2;
499
+ } else if (quarter_track_ < MAX_QUARTER_TRACK) {
500
+ quarter_track_ = MAX_QUARTER_TRACK;
501
+ }
502
+ } else if (prev_on && !next_on) {
503
+ // Step outward (toward track 0)
504
+ current_phase_ = prev_phase;
505
+ if (quarter_track_ > 1) {
506
+ quarter_track_ -= 2;
507
+ } else if (quarter_track_ > 0) {
508
+ quarter_track_ = 0;
509
+ }
510
+ }
511
+ // If both or neither adjacent phases are on, don't step
512
+ }
513
+
514
+ bool DskDiskImage::hasData() const {
515
+ int track = quarter_track_ / 4;
516
+ return track >= 0 && track < TRACKS;
517
+ }
518
+
519
+ void DskDiskImage::ensureTrackNibblized() {
520
+ int track = quarter_track_ / 4;
521
+ if (track < 0 || track >= TRACKS) {
522
+ return;
523
+ }
524
+
525
+ if (!nibble_tracks_[track].valid) {
526
+ nibblizeTrack(track);
527
+ }
528
+ }
529
+
530
+ void DskDiskImage::advanceBitPosition(uint64_t current_cycles) {
531
+ if (!loaded_)
532
+ return;
533
+
534
+ // Calculate elapsed cycles since last update
535
+ if (current_cycles <= last_cycle_count_) {
536
+ last_cycle_count_ = current_cycles;
537
+ return;
538
+ }
539
+
540
+ uint64_t elapsed = current_cycles - last_cycle_count_;
541
+ last_cycle_count_ = current_cycles;
542
+
543
+ // Disk spins at ~300 RPM = 5 revolutions/second
544
+ // At 1.023 MHz, one revolution = ~204,600 cycles
545
+ // With 6656 nibbles per track, each nibble = ~30.7 cycles
546
+ // Using 31 gives ~297 RPM which is within spec tolerance
547
+ constexpr uint64_t CYCLES_PER_NIBBLE = 31;
548
+
549
+ ensureTrackNibblized();
550
+
551
+ int track = quarter_track_ / 4;
552
+ if (track < 0 || track >= TRACKS)
553
+ return;
554
+
555
+ const auto &nt = nibble_tracks_[track];
556
+ if (!nt.valid || nt.nibbles.empty())
557
+ return;
558
+
559
+ // Advance position based on elapsed time
560
+ uint64_t nibbles_elapsed = elapsed / CYCLES_PER_NIBBLE;
561
+ nibble_position_ = (nibble_position_ + nibbles_elapsed) % nt.nibbles.size();
562
+ }
563
+
564
+ uint8_t DskDiskImage::readNibble() {
565
+ if (!loaded_)
566
+ return 0xFF; // Return sync byte pattern when not loaded
567
+
568
+ int track = quarter_track_ / 4;
569
+ if (track < 0 || track >= TRACKS)
570
+ return 0xFF; // Return sync byte pattern for invalid track
571
+
572
+ ensureTrackNibblized();
573
+
574
+ const auto &nt = nibble_tracks_[track];
575
+ if (!nt.valid || nt.nibbles.empty())
576
+ return 0xFF; // Return sync byte pattern if track not ready
577
+
578
+ // Read nibble at current position
579
+ uint8_t nibble = nt.nibbles[nibble_position_];
580
+
581
+ // Advance to next nibble
582
+ nibble_position_ = (nibble_position_ + 1) % nt.nibbles.size();
583
+
584
+ // All valid disk nibbles must have bit 7 set
585
+ // This is guaranteed by GCR encoding, but verify for safety
586
+ return nibble | 0x80;
587
+ }
588
+
589
+ void DskDiskImage::writeNibble(uint8_t nibble) {
590
+ if (!loaded_ || write_protected_)
591
+ return;
592
+
593
+ int track = quarter_track_ / 4;
594
+ if (track < 0 || track >= TRACKS) {
595
+ return;
596
+ }
597
+
598
+ ensureTrackNibblized();
599
+
600
+ auto &nt = nibble_tracks_[track];
601
+ if (!nt.valid || nt.nibbles.empty()) {
602
+ return;
603
+ }
604
+
605
+ // Write nibble at current position
606
+ nt.nibbles[nibble_position_] = nibble;
607
+ nt.dirty = true;
608
+ modified_ = true;
609
+
610
+ // Advance to next nibble
611
+ nibble_position_ = (nibble_position_ + 1) % nt.nibbles.size();
612
+ }
613
+
614
+ // ===== Bit-Level Access (for LSS) =====
615
+
616
+ void DskDiskImage::ensureTrackBitified() {
617
+ int track = quarter_track_ / 4;
618
+ if (track < 0 || track >= TRACKS) return;
619
+
620
+ auto &bt = bit_tracks_[track];
621
+ if (bt.valid) return;
622
+
623
+ // Ensure nibble track is ready first
624
+ ensureTrackNibblized();
625
+
626
+ const auto &nt = nibble_tracks_[track];
627
+ if (!nt.valid || nt.nibbles.empty()) return;
628
+
629
+ // Calculate total bit count: sync bytes = 10 bits, data bytes = 8 bits
630
+ uint32_t total_bits = 0;
631
+ bool have_sync_flags = (nt.is_sync.size() == nt.nibbles.size());
632
+ for (size_t i = 0; i < nt.nibbles.size(); i++) {
633
+ total_bits += (have_sync_flags && nt.is_sync[i]) ? 10 : 8;
634
+ }
635
+
636
+ bt.bit_count = total_bits;
637
+ size_t byte_count = (total_bits + 7) / 8;
638
+ bt.bits.assign(byte_count, 0);
639
+
640
+ // Helper: set a bit in the packed array
641
+ auto setBit = [&](uint32_t bit_idx) {
642
+ uint32_t byte_off = bit_idx / 8;
643
+ uint8_t bit_off = 7 - (bit_idx % 8);
644
+ bt.bits[byte_off] |= (1 << bit_off);
645
+ };
646
+
647
+ // Convert nibbles to packed bit stream
648
+ uint32_t bit_pos = 0;
649
+ for (size_t i = 0; i < nt.nibbles.size(); i++) {
650
+ uint8_t nibble = nt.nibbles[i];
651
+ // Write 8 data bits MSB first
652
+ for (int b = 7; b >= 0; b--) {
653
+ if (nibble & (1 << b)) {
654
+ setBit(bit_pos);
655
+ }
656
+ bit_pos++;
657
+ }
658
+ // Sync bytes get 2 extra zero bits (already 0 in the cleared array)
659
+ if (have_sync_flags && nt.is_sync[i]) {
660
+ bit_pos += 2;
661
+ }
662
+ }
663
+
664
+ bt.dirty = false;
665
+ bt.valid = true;
666
+ }
667
+
668
+ void DskDiskImage::bitTrackToNibbleTrack(int track) {
669
+ if (track < 0 || track >= TRACKS) return;
670
+
671
+ auto &bt = bit_tracks_[track];
672
+ if (!bt.valid || !bt.dirty) return;
673
+
674
+ auto &nt = nibble_tracks_[track];
675
+ bool have_sync_flags = (nt.is_sync.size() == nt.nibbles.size());
676
+
677
+ // Read nibbles back from bit stream, respecting sync byte widths
678
+ uint32_t bit_pos = 0;
679
+ size_t nibble_count = nt.nibbles.size();
680
+ if (nibble_count == 0) {
681
+ // Fallback: estimate nibble count from bit count
682
+ nibble_count = bt.bit_count / 8;
683
+ nt.nibbles.resize(nibble_count);
684
+ nt.is_sync.assign(nibble_count, false);
685
+ have_sync_flags = true;
686
+ }
687
+
688
+ auto getBit = [&](uint32_t idx) -> uint8_t {
689
+ if (idx >= bt.bit_count) return 0;
690
+ uint32_t byte_off = idx / 8;
691
+ uint8_t bit_off = 7 - (idx % 8);
692
+ return (byte_off < bt.bits.size() && (bt.bits[byte_off] & (1 << bit_off))) ? 1 : 0;
693
+ };
694
+
695
+ for (size_t i = 0; i < nibble_count && bit_pos < bt.bit_count; i++) {
696
+ uint8_t nibble = 0;
697
+ for (int b = 7; b >= 0; b--) {
698
+ if (getBit(bit_pos)) {
699
+ nibble |= (1 << b);
700
+ }
701
+ bit_pos++;
702
+ }
703
+ nt.nibbles[i] = nibble;
704
+ // Skip the 2 extra zero bits for sync bytes
705
+ if (have_sync_flags && i < nt.is_sync.size() && nt.is_sync[i]) {
706
+ bit_pos += 2;
707
+ }
708
+ }
709
+
710
+ nt.valid = true;
711
+ nt.dirty = true; // Mark nibble track dirty so denibblizeTrack() will process it
712
+ bt.dirty = false;
713
+ }
714
+
715
+ uint8_t DskDiskImage::readBit() {
716
+ if (!loaded_) return 0;
717
+
718
+ int track = quarter_track_ / 4;
719
+ if (track < 0 || track >= TRACKS) return 0;
720
+
721
+ ensureTrackBitified();
722
+
723
+ const auto &bt = bit_tracks_[track];
724
+ if (!bt.valid || bt.bit_count == 0) return 0;
725
+
726
+ uint32_t pos = bit_position_ % bt.bit_count;
727
+ uint32_t byte_off = pos / 8;
728
+ uint8_t bit_off = 7 - (pos % 8);
729
+
730
+ uint8_t bit = 0;
731
+ if (byte_off < bt.bits.size()) {
732
+ bit = (bt.bits[byte_off] >> bit_off) & 1;
733
+ }
734
+
735
+ bit_position_ = (bit_position_ + 1) % bt.bit_count;
736
+ return bit;
737
+ }
738
+
739
+ void DskDiskImage::writeBit(uint8_t bit) {
740
+ if (!loaded_ || write_protected_) return;
741
+
742
+ int track = quarter_track_ / 4;
743
+ if (track < 0 || track >= TRACKS) return;
744
+
745
+ ensureTrackBitified();
746
+
747
+ auto &bt = bit_tracks_[track];
748
+ if (!bt.valid || bt.bit_count == 0) return;
749
+
750
+ uint32_t pos = bit_position_ % bt.bit_count;
751
+ uint32_t byte_off = pos / 8;
752
+ uint8_t bit_off = 7 - (pos % 8);
753
+
754
+ if (byte_off < bt.bits.size()) {
755
+ bt.bits[byte_off] &= ~(1 << bit_off);
756
+ if (bit) {
757
+ bt.bits[byte_off] |= (1 << bit_off);
758
+ }
759
+ }
760
+
761
+ bt.dirty = true;
762
+ modified_ = true;
763
+ bit_position_ = (bit_position_ + 1) % bt.bit_count;
764
+ }
765
+
766
+ const uint8_t *DskDiskImage::getSectorData(size_t *size) const {
767
+ if (!loaded_) {
768
+ *size = 0;
769
+ return nullptr;
770
+ }
771
+
772
+ // First flush any dirty bit tracks back to nibble tracks
773
+ for (int t = 0; t < TRACKS; t++) {
774
+ if (bit_tracks_[t].dirty) {
775
+ const_cast<DskDiskImage *>(this)->bitTrackToNibbleTrack(t);
776
+ }
777
+ }
778
+
779
+ // Then denibblize any dirty nibble tracks
780
+ for (int t = 0; t < TRACKS; t++) {
781
+ if (nibble_tracks_[t].dirty) {
782
+ const_cast<DskDiskImage *>(this)->denibblizeTrack(t);
783
+ }
784
+ }
785
+
786
+ *size = DISK_SIZE;
787
+ return sector_data_.data();
788
+ }
789
+
790
+ const uint8_t *DskDiskImage::exportData(size_t *size) {
791
+ // DSK format is already in its native format, just return sector data
792
+ return getSectorData(size);
793
+ }
794
+
795
+ uint8_t DskDiskImage::getNibbleAt(int track, int position) const {
796
+ if (track < 0 || track >= TRACKS) {
797
+ return 0;
798
+ }
799
+
800
+ // Ensure track is nibblized
801
+ if (!nibble_tracks_[track].valid) {
802
+ const_cast<DskDiskImage *>(this)->nibblizeTrack(track);
803
+ }
804
+
805
+ const auto &nt = nibble_tracks_[track];
806
+ if (nt.nibbles.empty() || position < 0 ||
807
+ position >= static_cast<int>(nt.nibbles.size())) {
808
+ return 0;
809
+ }
810
+
811
+ return nt.nibbles[position];
812
+ }
813
+
814
+ int DskDiskImage::getTrackNibbleCount(int track) const {
815
+ if (track < 0 || track >= TRACKS) {
816
+ return 0;
817
+ }
818
+
819
+ // Ensure track is nibblized
820
+ if (!nibble_tracks_[track].valid) {
821
+ const_cast<DskDiskImage *>(this)->nibblizeTrack(track);
822
+ }
823
+
824
+ return static_cast<int>(nibble_tracks_[track].nibbles.size());
825
+ }
826
+
827
+ } // namespace a2e