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,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 = §or_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(§or_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
|