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.
- package/.clangd +5 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +362 -0
- package/CMakeLists.txt +774 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/build-wasm/generated/roms.cpp +2447 -0
- package/docker-compose.staging.yml +9 -0
- package/docs/basic-rom-disassembly.md +6663 -0
- package/docs/softswitch-comparison.md +273 -0
- package/docs/thunderclock-debug.md +89 -0
- package/examples/cube.bas +72 -0
- package/examples/hello.s +55 -0
- package/examples/scroll.s +140 -0
- package/package.json +18 -0
- package/public/assets/apple-logo-old.png +0 -0
- package/public/assets/apple-logo.png +0 -0
- package/public/assets/drive-closed-light-on.png +0 -0
- package/public/assets/drive-closed.png +0 -0
- package/public/assets/drive-open-light-on.png +0 -0
- package/public/assets/drive-open.png +0 -0
- package/public/audio-worklet.js +82 -0
- package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
- package/public/disks/ProDOS 2.4.3.po +0 -0
- package/public/disks/h32mb.2mg +0 -0
- package/public/disks/library.json +26 -0
- package/public/docs/llms/llm-assembler.txt +90 -0
- package/public/docs/llms/llm-basic-program.txt +256 -0
- package/public/docs/llms/llm-disk-drives.txt +72 -0
- package/public/docs/llms/llm-file-explorer.txt +50 -0
- package/public/docs/llms/llm-hard-drives.txt +80 -0
- package/public/docs/llms/llm-main.txt +51 -0
- package/public/docs/llms/llm-slot-configuration.txt +66 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/index.html +661 -0
- package/public/llms.txt +49 -0
- package/public/manifest.json +29 -0
- package/public/shaders/burnin.glsl +22 -0
- package/public/shaders/crt.glsl +706 -0
- package/public/shaders/edge.glsl +109 -0
- package/public/shaders/vertex.glsl +8 -0
- package/public/sw.js +186 -0
- package/roms/341-0027.bin +0 -0
- package/roms/341-0160-A-US-UK.bin +0 -0
- package/roms/341-0160-A.bin +0 -0
- package/roms/342-0273-A-US-UK.bin +0 -0
- package/roms/342-0349-B-C0-FF.bin +0 -0
- package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
- package/roms/Thunderclock Plus ROM.bin +0 -0
- package/scripts/generate_roms.sh +69 -0
- package/src/bindings/wasm_interface.cpp +1940 -0
- package/src/core/assembler/assembler.cpp +1239 -0
- package/src/core/assembler/assembler.hpp +115 -0
- package/src/core/audio/audio.cpp +160 -0
- package/src/core/audio/audio.hpp +81 -0
- package/src/core/basic/basic_detokenizer.cpp +436 -0
- package/src/core/basic/basic_detokenizer.hpp +41 -0
- package/src/core/basic/basic_tokenizer.cpp +286 -0
- package/src/core/basic/basic_tokenizer.hpp +26 -0
- package/src/core/basic/basic_tokens.hpp +295 -0
- package/src/core/cards/disk2_card.cpp +568 -0
- package/src/core/cards/disk2_card.hpp +316 -0
- package/src/core/cards/expansion_card.hpp +185 -0
- package/src/core/cards/mockingboard/ay8910.cpp +616 -0
- package/src/core/cards/mockingboard/ay8910.hpp +159 -0
- package/src/core/cards/mockingboard/via6522.cpp +530 -0
- package/src/core/cards/mockingboard/via6522.hpp +163 -0
- package/src/core/cards/mockingboard_card.cpp +312 -0
- package/src/core/cards/mockingboard_card.hpp +159 -0
- package/src/core/cards/mouse_card.cpp +654 -0
- package/src/core/cards/mouse_card.hpp +190 -0
- package/src/core/cards/smartport/block_device.cpp +202 -0
- package/src/core/cards/smartport/block_device.hpp +60 -0
- package/src/core/cards/smartport/smartport_card.cpp +603 -0
- package/src/core/cards/smartport/smartport_card.hpp +120 -0
- package/src/core/cards/thunderclock_card.cpp +237 -0
- package/src/core/cards/thunderclock_card.hpp +122 -0
- package/src/core/cpu/cpu6502.cpp +1609 -0
- package/src/core/cpu/cpu6502.hpp +203 -0
- package/src/core/debug/condition_evaluator.cpp +470 -0
- package/src/core/debug/condition_evaluator.hpp +87 -0
- package/src/core/disassembler/disassembler.cpp +552 -0
- package/src/core/disassembler/disassembler.hpp +171 -0
- package/src/core/disk-image/disk_image.hpp +267 -0
- package/src/core/disk-image/dsk_disk_image.cpp +827 -0
- package/src/core/disk-image/dsk_disk_image.hpp +204 -0
- package/src/core/disk-image/gcr_encoding.cpp +147 -0
- package/src/core/disk-image/gcr_encoding.hpp +78 -0
- package/src/core/disk-image/woz_disk_image.cpp +1049 -0
- package/src/core/disk-image/woz_disk_image.hpp +343 -0
- package/src/core/emulator.cpp +2126 -0
- package/src/core/emulator.hpp +434 -0
- package/src/core/filesystem/dos33.cpp +178 -0
- package/src/core/filesystem/dos33.hpp +66 -0
- package/src/core/filesystem/pascal.cpp +262 -0
- package/src/core/filesystem/pascal.hpp +87 -0
- package/src/core/filesystem/prodos.cpp +369 -0
- package/src/core/filesystem/prodos.hpp +119 -0
- package/src/core/input/keyboard.cpp +227 -0
- package/src/core/input/keyboard.hpp +111 -0
- package/src/core/mmu/mmu.cpp +1387 -0
- package/src/core/mmu/mmu.hpp +236 -0
- package/src/core/types.hpp +196 -0
- package/src/core/video/video.cpp +680 -0
- package/src/core/video/video.hpp +156 -0
- package/src/css/assembler-editor.css +1617 -0
- package/src/css/base.css +470 -0
- package/src/css/basic-debugger.css +791 -0
- package/src/css/basic-editor.css +792 -0
- package/src/css/controls.css +783 -0
- package/src/css/cpu-debugger.css +1413 -0
- package/src/css/debug-base.css +160 -0
- package/src/css/debug-windows.css +6455 -0
- package/src/css/disk-drives.css +406 -0
- package/src/css/documentation.css +392 -0
- package/src/css/file-explorer.css +867 -0
- package/src/css/hard-drive.css +180 -0
- package/src/css/layout.css +217 -0
- package/src/css/memory-windows.css +798 -0
- package/src/css/modals.css +510 -0
- package/src/css/monitor.css +425 -0
- package/src/css/release-notes.css +101 -0
- package/src/css/responsive.css +400 -0
- package/src/css/rule-builder.css +340 -0
- package/src/css/save-states.css +201 -0
- package/src/css/settings-windows.css +1231 -0
- package/src/css/window-switcher.css +150 -0
- package/src/js/agent/agent-manager.js +643 -0
- package/src/js/agent/agent-tools.js +293 -0
- package/src/js/agent/agent-version-tools.js +131 -0
- package/src/js/agent/assembler-tools.js +357 -0
- package/src/js/agent/basic-program-tools.js +894 -0
- package/src/js/agent/disk-tools.js +417 -0
- package/src/js/agent/file-explorer-tools.js +269 -0
- package/src/js/agent/index.js +13 -0
- package/src/js/agent/main-tools.js +222 -0
- package/src/js/agent/slot-tools.js +303 -0
- package/src/js/agent/smartport-tools.js +257 -0
- package/src/js/agent/window-tools.js +80 -0
- package/src/js/audio/audio-driver.js +417 -0
- package/src/js/audio/audio-worklet.js +85 -0
- package/src/js/audio/index.js +8 -0
- package/src/js/config/default-layout.js +34 -0
- package/src/js/config/version.js +8 -0
- package/src/js/data/apple2-rom-routines.js +577 -0
- package/src/js/debug/assembler-editor-window.js +2993 -0
- package/src/js/debug/basic-breakpoint-manager.js +529 -0
- package/src/js/debug/basic-program-parser.js +436 -0
- package/src/js/debug/basic-program-window.js +2594 -0
- package/src/js/debug/basic-variable-inspector.js +447 -0
- package/src/js/debug/breakpoint-manager.js +472 -0
- package/src/js/debug/cpu-debugger-window.js +2396 -0
- package/src/js/debug/index.js +22 -0
- package/src/js/debug/label-manager.js +238 -0
- package/src/js/debug/memory-browser-window.js +416 -0
- package/src/js/debug/memory-heat-map-window.js +481 -0
- package/src/js/debug/memory-map-window.js +206 -0
- package/src/js/debug/mockingboard-window.js +882 -0
- package/src/js/debug/mouse-card-window.js +355 -0
- package/src/js/debug/rule-builder-window.js +648 -0
- package/src/js/debug/soft-switch-window.js +458 -0
- package/src/js/debug/stack-viewer-window.js +221 -0
- package/src/js/debug/symbols.js +416 -0
- package/src/js/debug/trace-panel.js +291 -0
- package/src/js/debug/zero-page-watch-window.js +297 -0
- package/src/js/disk-manager/disk-drives-window.js +212 -0
- package/src/js/disk-manager/disk-operations.js +284 -0
- package/src/js/disk-manager/disk-persistence.js +301 -0
- package/src/js/disk-manager/disk-surface-renderer.js +388 -0
- package/src/js/disk-manager/drive-sounds.js +139 -0
- package/src/js/disk-manager/hard-drive-manager.js +481 -0
- package/src/js/disk-manager/hard-drive-persistence.js +187 -0
- package/src/js/disk-manager/hard-drive-window.js +57 -0
- package/src/js/disk-manager/index.js +890 -0
- package/src/js/display/display-settings-window.js +383 -0
- package/src/js/display/index.js +10 -0
- package/src/js/display/screen-window.js +342 -0
- package/src/js/display/webgl-renderer.js +705 -0
- package/src/js/file-explorer/disassembler.js +574 -0
- package/src/js/file-explorer/dos33.js +266 -0
- package/src/js/file-explorer/file-viewer.js +359 -0
- package/src/js/file-explorer/index.js +1261 -0
- package/src/js/file-explorer/prodos.js +549 -0
- package/src/js/file-explorer/utils.js +67 -0
- package/src/js/help/documentation-window.js +1096 -0
- package/src/js/help/index.js +10 -0
- package/src/js/help/release-notes-window.js +85 -0
- package/src/js/help/release-notes.js +612 -0
- package/src/js/input/gamepad-handler.js +176 -0
- package/src/js/input/index.js +12 -0
- package/src/js/input/input-handler.js +396 -0
- package/src/js/input/joystick-window.js +404 -0
- package/src/js/input/mouse-handler.js +99 -0
- package/src/js/input/text-selection.js +462 -0
- package/src/js/main.js +653 -0
- package/src/js/state/index.js +15 -0
- package/src/js/state/save-states-window.js +393 -0
- package/src/js/state/state-manager.js +409 -0
- package/src/js/state/state-persistence.js +218 -0
- package/src/js/ui/confirm.js +43 -0
- package/src/js/ui/disk-drive-positioner.js +347 -0
- package/src/js/ui/reminder-controller.js +129 -0
- package/src/js/ui/slot-configuration-window.js +560 -0
- package/src/js/ui/theme-manager.js +61 -0
- package/src/js/ui/toast.js +44 -0
- package/src/js/ui/ui-controller.js +897 -0
- package/src/js/ui/window-switcher.js +275 -0
- package/src/js/utils/basic-autocomplete.js +832 -0
- package/src/js/utils/basic-highlighting.js +473 -0
- package/src/js/utils/basic-tokenizer.js +153 -0
- package/src/js/utils/basic-tokens.js +117 -0
- package/src/js/utils/constants.js +28 -0
- package/src/js/utils/indexeddb-helper.js +225 -0
- package/src/js/utils/merlin-editor-support.js +905 -0
- package/src/js/utils/merlin-highlighting.js +551 -0
- package/src/js/utils/storage.js +125 -0
- package/src/js/utils/string-utils.js +19 -0
- package/src/js/utils/wasm-memory.js +54 -0
- package/src/js/windows/base-window.js +690 -0
- package/src/js/windows/index.js +9 -0
- package/src/js/windows/window-manager.js +375 -0
- package/tests/catch2/catch.hpp +17976 -0
- package/tests/common/basic_program_builder.cpp +119 -0
- package/tests/common/basic_program_builder.hpp +209 -0
- package/tests/common/disk_image_builder.cpp +444 -0
- package/tests/common/disk_image_builder.hpp +141 -0
- package/tests/common/test_helpers.hpp +118 -0
- package/tests/gcr/gcr-test.cpp +142 -0
- package/tests/integration/check-rom.js +70 -0
- package/tests/integration/compare-boot.js +239 -0
- package/tests/integration/crash-trace.js +102 -0
- package/tests/integration/disk-boot-test.js +264 -0
- package/tests/integration/memory-crash.js +108 -0
- package/tests/integration/nibble-read-test.js +249 -0
- package/tests/integration/phase-test.js +159 -0
- package/tests/integration/test_emulator.cpp +291 -0
- package/tests/integration/test_emulator_basic.cpp +91 -0
- package/tests/integration/test_emulator_debug.cpp +344 -0
- package/tests/integration/test_emulator_disk.cpp +153 -0
- package/tests/integration/test_emulator_state.cpp +163 -0
- package/tests/klaus/6502_functional_test.bin +0 -0
- package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
- package/tests/klaus/klaus_6502_test.cpp +184 -0
- package/tests/klaus/klaus_65c02_test.cpp +197 -0
- package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
- package/tests/thunderclock/thunderclock_test.cpp +550 -0
- package/tests/unit/test_assembler.cpp +521 -0
- package/tests/unit/test_audio.cpp +196 -0
- package/tests/unit/test_ay8910.cpp +311 -0
- package/tests/unit/test_basic_detokenizer.cpp +265 -0
- package/tests/unit/test_basic_tokenizer.cpp +382 -0
- package/tests/unit/test_block_device.cpp +259 -0
- package/tests/unit/test_condition_evaluator.cpp +219 -0
- package/tests/unit/test_cpu6502.cpp +1301 -0
- package/tests/unit/test_cpu_addressing.cpp +361 -0
- package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
- package/tests/unit/test_cpu_decimal.cpp +166 -0
- package/tests/unit/test_cpu_interrupts.cpp +285 -0
- package/tests/unit/test_disassembler.cpp +323 -0
- package/tests/unit/test_disk2_card.cpp +330 -0
- package/tests/unit/test_dos33.cpp +273 -0
- package/tests/unit/test_dsk_disk_image.cpp +315 -0
- package/tests/unit/test_expansion_card.cpp +178 -0
- package/tests/unit/test_gcr_encoding.cpp +232 -0
- package/tests/unit/test_keyboard.cpp +262 -0
- package/tests/unit/test_mmu.cpp +555 -0
- package/tests/unit/test_mmu_slots.cpp +323 -0
- package/tests/unit/test_mockingboard.cpp +352 -0
- package/tests/unit/test_mouse_card.cpp +386 -0
- package/tests/unit/test_pascal.cpp +248 -0
- package/tests/unit/test_prodos.cpp +259 -0
- package/tests/unit/test_smartport_card.cpp +321 -0
- package/tests/unit/test_thunderclock.cpp +354 -0
- package/tests/unit/test_via6522.cpp +323 -0
- package/tests/unit/test_video.cpp +319 -0
- package/tests/unit/test_woz_disk_image.cpp +257 -0
- package/vite.config.js +96 -0
- package/wiki/AI-Agent.md +372 -0
- package/wiki/Architecture-Overview.md +303 -0
- package/wiki/Audio-System.md +449 -0
- package/wiki/CPU-Emulation.md +477 -0
- package/wiki/Debugger.md +516 -0
- package/wiki/Disk-Drives.md +161 -0
- package/wiki/Disk-System-Internals.md +547 -0
- package/wiki/Display-Settings.md +88 -0
- package/wiki/Expansion-Slots.md +187 -0
- package/wiki/File-Explorer.md +259 -0
- package/wiki/Getting-Started.md +156 -0
- package/wiki/Home.md +69 -0
- package/wiki/Input-Devices.md +183 -0
- package/wiki/Keyboard-Shortcuts.md +158 -0
- package/wiki/Memory-System.md +364 -0
- package/wiki/Save-States.md +172 -0
- 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> §ors) 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
|