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,2126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* emulator.cpp - Core emulator coordinator tying together CPU, memory, video, audio, and peripherals
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
#include "emulator.hpp"
|
|
9
|
+
#include "cards/disk2_card.hpp"
|
|
10
|
+
#include "cards/mockingboard_card.hpp"
|
|
11
|
+
#include "cards/thunderclock_card.hpp"
|
|
12
|
+
#include "cards/mouse_card.hpp"
|
|
13
|
+
#include "cards/smartport/smartport_card.hpp"
|
|
14
|
+
#include "debug/condition_evaluator.hpp"
|
|
15
|
+
#include <algorithm>
|
|
16
|
+
#include <cstring>
|
|
17
|
+
|
|
18
|
+
// Include generated ROM data directly
|
|
19
|
+
#include "roms.cpp" // namespace roms
|
|
20
|
+
|
|
21
|
+
namespace a2e {
|
|
22
|
+
|
|
23
|
+
Emulator::Emulator() {
|
|
24
|
+
mmu_ = std::make_unique<MMU>();
|
|
25
|
+
video_ = std::make_unique<Video>(*mmu_);
|
|
26
|
+
audio_ = std::make_unique<Audio>();
|
|
27
|
+
keyboard_ = std::make_unique<Keyboard>();
|
|
28
|
+
|
|
29
|
+
// Create cards, keep raw pointers, then insert into slots
|
|
30
|
+
auto disk = std::make_unique<Disk2Card>();
|
|
31
|
+
auto mb = std::make_unique<MockingboardCard>();
|
|
32
|
+
disk_ = disk.get();
|
|
33
|
+
mockingboard_ = mb.get();
|
|
34
|
+
|
|
35
|
+
// Create CPU with memory callbacks
|
|
36
|
+
cpu_ = std::make_unique<CPU6502>(
|
|
37
|
+
[this](uint16_t addr) { return cpuRead(addr); },
|
|
38
|
+
[this](uint16_t addr, uint8_t val) { cpuWrite(addr, val); },
|
|
39
|
+
CPUVariant::CMOS_65C02);
|
|
40
|
+
|
|
41
|
+
// Set up keyboard callback to receive translated keys
|
|
42
|
+
keyboard_->setKeyCallback([this](int key) { keyDown(key); });
|
|
43
|
+
|
|
44
|
+
// Set up MMU callbacks
|
|
45
|
+
mmu_->setKeyboardCallback([this]() { return getKeyboardData(); });
|
|
46
|
+
mmu_->setKeyStrobeCallback([this]() { clearKeyboardStrobe(); });
|
|
47
|
+
mmu_->setAnyKeyDownCallback([this]() { return keyDown_; });
|
|
48
|
+
mmu_->setSpeakerCallback([this]() { toggleSpeaker(); });
|
|
49
|
+
mmu_->setButtonCallback([this](int btn) { return getButtonState(btn); });
|
|
50
|
+
mmu_->setCycleCallback([this]() { return cpu_->getTotalCycles(); });
|
|
51
|
+
|
|
52
|
+
// Wire video subsystem callbacks
|
|
53
|
+
video_->setCycleCallback([this]() { return cpu_->getTotalCycles(); });
|
|
54
|
+
mmu_->setVideoSwitchCallback([this]() { video_->onVideoSwitchChanged(); });
|
|
55
|
+
|
|
56
|
+
// Wire watchpoint callbacks (MMU -> Emulator)
|
|
57
|
+
mmu_->setWatchpointCallbacks(
|
|
58
|
+
[this](uint16_t addr, uint8_t val) { onWatchpointRead(addr, val); },
|
|
59
|
+
[this](uint16_t addr, uint8_t val) { onWatchpointWrite(addr, val); });
|
|
60
|
+
|
|
61
|
+
// Set up Mockingboard callbacks
|
|
62
|
+
mockingboard_->setCycleCallback([this]() { return cpu_->getTotalCycles(); });
|
|
63
|
+
mockingboard_->setIRQCallback([this]() { cpu_->irq(); });
|
|
64
|
+
|
|
65
|
+
// Set up level-triggered IRQ polling for VIA/mouse interrupts
|
|
66
|
+
cpu_->setIRQStatusCallback([this]() {
|
|
67
|
+
bool active = mockingboard_ ? mockingboard_->isIRQActive() : false;
|
|
68
|
+
if (mouse_) active = active || mouse_->isIRQActive();
|
|
69
|
+
return active;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Set up disk timing callback - allows disk reads to get accurate cycle count
|
|
73
|
+
// during instruction execution (before disk_->update() is called)
|
|
74
|
+
disk_->setCycleCallback([this]() { return cpu_->getTotalCycles(); });
|
|
75
|
+
|
|
76
|
+
// Insert cards into slots (transfers ownership to MMU)
|
|
77
|
+
mmu_->insertCard(6, std::move(disk));
|
|
78
|
+
mmu_->insertCard(4, std::move(mb));
|
|
79
|
+
|
|
80
|
+
// Audio gets raw pointer
|
|
81
|
+
audio_->setMockingboard(mockingboard_);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Emulator::~Emulator() = default;
|
|
85
|
+
|
|
86
|
+
void Emulator::init() {
|
|
87
|
+
// Load system and character ROMs into MMU
|
|
88
|
+
mmu_->loadROM(roms::ROM_SYSTEM, roms::ROM_SYSTEM_SIZE,
|
|
89
|
+
roms::ROM_CHAR, roms::ROM_CHAR_SIZE);
|
|
90
|
+
|
|
91
|
+
// Load Disk II ROM into the card
|
|
92
|
+
disk_->loadROM(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
|
|
93
|
+
|
|
94
|
+
reset();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
void Emulator::reset() {
|
|
98
|
+
mmu_->reset();
|
|
99
|
+
cpu_->resetCycleCount(); // Clear cycle counter for fresh power-on state
|
|
100
|
+
cpu_->reset();
|
|
101
|
+
audio_->reset();
|
|
102
|
+
if (disk_) disk_->reset();
|
|
103
|
+
keyboard_->reset();
|
|
104
|
+
if (mockingboard_) mockingboard_->reset();
|
|
105
|
+
|
|
106
|
+
// Clear Apple button states
|
|
107
|
+
setButton(0, false);
|
|
108
|
+
setButton(1, false);
|
|
109
|
+
|
|
110
|
+
keyboardLatch_ = 0;
|
|
111
|
+
keyDown_ = false;
|
|
112
|
+
speedMultiplier_ = 1;
|
|
113
|
+
lastFrameCycle_ = 0;
|
|
114
|
+
samplesGenerated_ = 0;
|
|
115
|
+
frameReady_ = false;
|
|
116
|
+
breakpointHit_ = false;
|
|
117
|
+
watchpointHit_ = false;
|
|
118
|
+
skipBreakpointOnce_ = false;
|
|
119
|
+
tempBreakpointActive_ = false;
|
|
120
|
+
tempBreakpoint_ = 0;
|
|
121
|
+
tempBreakpointHit_ = false;
|
|
122
|
+
// Keep beam breakpoints across reset (same as regular breakpoints)
|
|
123
|
+
for (auto& bp : beamBreakpoints_) {
|
|
124
|
+
bp.lastFireFrame = UINT64_MAX;
|
|
125
|
+
bp.lastFireScanline = -1;
|
|
126
|
+
}
|
|
127
|
+
beamBreakHit_ = false;
|
|
128
|
+
beamBreakHitId_ = -1;
|
|
129
|
+
beamBreakHitScanline_ = -1;
|
|
130
|
+
beamBreakHitHPos_ = -1;
|
|
131
|
+
paused_ = false;
|
|
132
|
+
|
|
133
|
+
// Clear BASIC debugging state
|
|
134
|
+
basicProgramRunning_ = false;
|
|
135
|
+
basicBreakpointHit_ = false;
|
|
136
|
+
basicErrorHit_ = false;
|
|
137
|
+
basicErrorLine_ = 0;
|
|
138
|
+
basicErrorCode_ = 0;
|
|
139
|
+
basicStepMode_ = BasicStepMode::None;
|
|
140
|
+
skipBasicBreakpointLine_ = 0xFFFF;
|
|
141
|
+
skipBasicBreakpointStmt_ = -1;
|
|
142
|
+
basicBreakLine_ = 0;
|
|
143
|
+
|
|
144
|
+
video_->beginNewFrame(0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
void Emulator::warmReset() {
|
|
148
|
+
// Warm reset - CPU jumps to reset vector, preserves memory and disk state
|
|
149
|
+
// On real hardware, the reset signal resets soft switches via the IOU chip
|
|
150
|
+
// but does not clear RAM, so programs in memory are preserved
|
|
151
|
+
mmu_->warmReset();
|
|
152
|
+
cpu_->reset();
|
|
153
|
+
audio_->reset();
|
|
154
|
+
keyboard_->reset();
|
|
155
|
+
|
|
156
|
+
// Stop disk motor (real Apple IIe reset signal turns off motor)
|
|
157
|
+
if (disk_) disk_->stopMotor();
|
|
158
|
+
|
|
159
|
+
// Clear Apple button states
|
|
160
|
+
setButton(0, false);
|
|
161
|
+
setButton(1, false);
|
|
162
|
+
|
|
163
|
+
// Reset video to clean frame state
|
|
164
|
+
video_->beginNewFrame(cpu_->getTotalCycles());
|
|
165
|
+
|
|
166
|
+
// Clear debugger hit flags
|
|
167
|
+
breakpointHit_ = false;
|
|
168
|
+
watchpointHit_ = false;
|
|
169
|
+
skipBreakpointOnce_ = false;
|
|
170
|
+
tempBreakpointActive_ = false;
|
|
171
|
+
tempBreakpointHit_ = false;
|
|
172
|
+
beamBreakHit_ = false;
|
|
173
|
+
|
|
174
|
+
// Clear BASIC debugger state
|
|
175
|
+
basicBreakpointHit_ = false;
|
|
176
|
+
basicStepMode_ = BasicStepMode::None;
|
|
177
|
+
basicBreakLine_ = 0;
|
|
178
|
+
|
|
179
|
+
paused_ = false;
|
|
180
|
+
frameReady_ = false;
|
|
181
|
+
samplesGenerated_ = 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
void Emulator::setPaused(bool paused) {
|
|
185
|
+
if (!paused && paused_ && breakpointHit_) {
|
|
186
|
+
skipBreakpointOnce_ = true;
|
|
187
|
+
}
|
|
188
|
+
if (!paused && paused_ && basicBreakpointHit_) {
|
|
189
|
+
// Skip this BASIC breakpoint until we move to a different line/statement
|
|
190
|
+
skipBasicBreakpointLine_ = basicBreakLine_;
|
|
191
|
+
// Determine which statement we're on to set the skip correctly
|
|
192
|
+
uint16_t lineStart = findCurrentLineStart(basicBreakLine_);
|
|
193
|
+
uint16_t txtptr = mmu_->readRAM(0xB8, false) | (mmu_->readRAM(0xB9, false) << 8);
|
|
194
|
+
int stmtIdx = (lineStart > 0 && txtptr >= lineStart)
|
|
195
|
+
? countColonsBetween(lineStart, txtptr) : 0;
|
|
196
|
+
// Check if the breakpoint that hit was a statement-level one
|
|
197
|
+
bool hasStmtBp = false;
|
|
198
|
+
for (const auto& bp : basicBreakpoints_) {
|
|
199
|
+
if (bp.lineNumber == basicBreakLine_ && bp.statementIndex >= 0 && bp.statementIndex == stmtIdx) {
|
|
200
|
+
hasStmtBp = true;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
skipBasicBreakpointStmt_ = hasStmtBp ? static_cast<int8_t>(stmtIdx) : -1;
|
|
205
|
+
}
|
|
206
|
+
breakpointHit_ = false;
|
|
207
|
+
basicBreakpointHit_ = false;
|
|
208
|
+
watchpointHit_ = false;
|
|
209
|
+
beamBreakHit_ = false;
|
|
210
|
+
beamBreakHitId_ = -1;
|
|
211
|
+
// Reset frame sample counter when unpausing to prevent backlog
|
|
212
|
+
if (!paused && paused_) {
|
|
213
|
+
samplesGenerated_ = 0;
|
|
214
|
+
}
|
|
215
|
+
paused_ = paused;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
void Emulator::runCycles(int cycles) {
|
|
219
|
+
if (paused_)
|
|
220
|
+
return;
|
|
221
|
+
|
|
222
|
+
uint64_t startCycles = cpu_->getTotalCycles();
|
|
223
|
+
uint64_t targetCycles = startCycles + cycles;
|
|
224
|
+
|
|
225
|
+
while (cpu_->getTotalCycles() < targetCycles) {
|
|
226
|
+
// Check breakpoints (user breakpoints and temp breakpoint)
|
|
227
|
+
{
|
|
228
|
+
uint16_t pc = cpu_->getPC();
|
|
229
|
+
|
|
230
|
+
// Check temp breakpoint (step over / step out)
|
|
231
|
+
if (tempBreakpointActive_ && pc == tempBreakpoint_) {
|
|
232
|
+
tempBreakpointHit_ = true;
|
|
233
|
+
clearTempBreakpoint();
|
|
234
|
+
breakpointHit_ = true;
|
|
235
|
+
breakpointAddress_ = pc;
|
|
236
|
+
paused_ = true;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Check user breakpoints
|
|
241
|
+
if (!breakpoints_.empty()) {
|
|
242
|
+
if (skipBreakpointOnce_) {
|
|
243
|
+
skipBreakpointOnce_ = false;
|
|
244
|
+
} else if (breakpoints_.count(pc) && !disabledBreakpoints_.count(pc)) {
|
|
245
|
+
breakpointHit_ = true;
|
|
246
|
+
breakpointAddress_ = pc;
|
|
247
|
+
paused_ = true;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Track BASIC program running state by monitoring ROM entry points.
|
|
254
|
+
// $D912 (RUN command) = program starting, $D43C (RESTART) = returning to ] prompt.
|
|
255
|
+
// This is definitive because it hooks into the ROM's own execution flow.
|
|
256
|
+
{
|
|
257
|
+
uint16_t pc = cpu_->getPC();
|
|
258
|
+
if (pc == 0xD912 && !basicProgramRunning_) {
|
|
259
|
+
basicProgramRunning_ = true;
|
|
260
|
+
basicErrorHit_ = false; // Clear error state on new RUN
|
|
261
|
+
} else if (pc == 0xD43C && basicProgramRunning_) {
|
|
262
|
+
basicProgramRunning_ = false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ERROR handler entry at $D412 — X register holds error code offset,
|
|
266
|
+
// CURLIN and TXTPTR still point to the offending location.
|
|
267
|
+
// Only capture if ERRFLG ($D8) bit 7 is clear (no ONERR GOTO active).
|
|
268
|
+
if (pc == 0xD412 && basicProgramRunning_) {
|
|
269
|
+
uint8_t errflg = mmu_->readRAM(0xD8, false);
|
|
270
|
+
if (!(errflg & 0x80)) {
|
|
271
|
+
uint8_t curlinHi = mmu_->readRAM(0x76, false);
|
|
272
|
+
if (curlinHi != 0xFF) { // Not in direct mode
|
|
273
|
+
basicErrorHit_ = true;
|
|
274
|
+
basicErrorLine_ = mmu_->readRAM(0x75, false) | (static_cast<uint16_t>(curlinHi) << 8);
|
|
275
|
+
basicErrorTxtptr_ = mmu_->readRAM(0xB8, false) | (mmu_->readRAM(0xB9, false) << 8);
|
|
276
|
+
basicErrorCode_ = cpu_->getX();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check BASIC stepping and breakpoints
|
|
283
|
+
// CURLIN+1 ($76) = $FF means direct/immediate mode (only high byte matters,
|
|
284
|
+
// matching how the ROM checks it at NEWSTT $D7DC: LDX CURLIN+1 / INX / BEQ)
|
|
285
|
+
// Use readRAM to bypass ALTZP switch - BASIC always uses main RAM for zero page
|
|
286
|
+
uint8_t curlinHi = mmu_->readRAM(0x76, false);
|
|
287
|
+
bool basicDirectMode = (curlinHi == 0xFF);
|
|
288
|
+
uint16_t curlin = mmu_->readRAM(0x75, false) | (static_cast<uint16_t>(curlinHi) << 8);
|
|
289
|
+
|
|
290
|
+
// Clear skip line when returning to direct mode
|
|
291
|
+
if (basicDirectMode && skipBasicBreakpointLine_ != 0xFFFF) {
|
|
292
|
+
skipBasicBreakpointLine_ = 0xFFFF;
|
|
293
|
+
skipBasicBreakpointStmt_ = -1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!basicDirectMode) {
|
|
297
|
+
uint16_t pc = cpu_->getPC();
|
|
298
|
+
|
|
299
|
+
// All BASIC stepping and line breakpoints fire at $D820 (EXECUTE_STATEMENT).
|
|
300
|
+
// At this ROM address, both new-line and colon paths have converged:
|
|
301
|
+
// CURLIN is correct and TXTPTR points to the first token of the statement
|
|
302
|
+
// about to execute. This ensures consistent state for statement highlighting.
|
|
303
|
+
if (pc == 0xD820) {
|
|
304
|
+
// Heat map: count every statement execution
|
|
305
|
+
if (basicHeatMapEnabled_) {
|
|
306
|
+
basicHeatMap_[curlin]++;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// BASIC line stepping - pause when CURLIN changes
|
|
310
|
+
if (basicStepMode_ == BasicStepMode::Line) {
|
|
311
|
+
if (curlin != basicStepFromLine_) {
|
|
312
|
+
basicStepMode_ = BasicStepMode::None;
|
|
313
|
+
basicBreakpointHit_ = true;
|
|
314
|
+
basicBreakLine_ = curlin;
|
|
315
|
+
paused_ = true;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// BASIC statement stepping
|
|
321
|
+
if (basicStepMode_ == BasicStepMode::Statement) {
|
|
322
|
+
if (basicStepSkipFirst_) {
|
|
323
|
+
basicStepSkipFirst_ = false;
|
|
324
|
+
} else {
|
|
325
|
+
basicStepMode_ = BasicStepMode::None;
|
|
326
|
+
basicBreakpointHit_ = true;
|
|
327
|
+
basicBreakLine_ = curlin;
|
|
328
|
+
paused_ = true;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check BASIC line and statement breakpoints
|
|
334
|
+
if (!basicBreakpoints_.empty()) {
|
|
335
|
+
bool matched = false;
|
|
336
|
+
int currentStmtIndex = -2; // Sentinel: not yet computed
|
|
337
|
+
for (const auto& bp : basicBreakpoints_) {
|
|
338
|
+
if (bp.lineNumber != curlin) continue;
|
|
339
|
+
if (bp.statementIndex == -1) {
|
|
340
|
+
// Whole-line breakpoint
|
|
341
|
+
matched = true;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
// Statement-level: lazily compute current statement index
|
|
345
|
+
if (currentStmtIndex == -2) {
|
|
346
|
+
uint16_t lineStart = findCurrentLineStart(curlin);
|
|
347
|
+
uint16_t txtptr = mmu_->readRAM(0xB8, false) | (mmu_->readRAM(0xB9, false) << 8);
|
|
348
|
+
currentStmtIndex = (lineStart > 0 && txtptr >= lineStart)
|
|
349
|
+
? countColonsBetween(lineStart, txtptr) : 0;
|
|
350
|
+
}
|
|
351
|
+
if (bp.statementIndex == currentStmtIndex) {
|
|
352
|
+
matched = true;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (matched) {
|
|
358
|
+
// Skip if we're stepping
|
|
359
|
+
if (basicStepMode_ != BasicStepMode::None) {
|
|
360
|
+
// Don't break while stepping
|
|
361
|
+
}
|
|
362
|
+
// Skip logic: check if this matches the skip (line, stmt) pair
|
|
363
|
+
else if (curlin == skipBasicBreakpointLine_) {
|
|
364
|
+
if (skipBasicBreakpointStmt_ == -1) {
|
|
365
|
+
// Whole-line skip: skip all breakpoints on this line
|
|
366
|
+
} else {
|
|
367
|
+
// Statement-level skip: only skip this specific statement
|
|
368
|
+
if (currentStmtIndex == -2) {
|
|
369
|
+
uint16_t lineStart = findCurrentLineStart(curlin);
|
|
370
|
+
uint16_t txtptr = mmu_->readRAM(0xB8, false) | (mmu_->readRAM(0xB9, false) << 8);
|
|
371
|
+
currentStmtIndex = (lineStart > 0 && txtptr >= lineStart)
|
|
372
|
+
? countColonsBetween(lineStart, txtptr) : 0;
|
|
373
|
+
}
|
|
374
|
+
if (currentStmtIndex != skipBasicBreakpointStmt_) {
|
|
375
|
+
// Different statement on same line - break!
|
|
376
|
+
basicBreakpointHit_ = true;
|
|
377
|
+
basicBreakLine_ = curlin;
|
|
378
|
+
paused_ = true;
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
basicBreakpointHit_ = true;
|
|
384
|
+
basicBreakLine_ = curlin;
|
|
385
|
+
paused_ = true;
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Condition-only rules: evaluate each expression in C++, pause only if one matches
|
|
392
|
+
// Skip while stepping to allow step to complete
|
|
393
|
+
// Skip on the line we just resumed from (same skip logic as line breakpoints)
|
|
394
|
+
if (!basicConditionRules_.empty() && !basicBreakpointHit_ &&
|
|
395
|
+
basicStepMode_ == BasicStepMode::None &&
|
|
396
|
+
curlin != skipBasicBreakpointLine_) {
|
|
397
|
+
for (auto& rule : basicConditionRules_) {
|
|
398
|
+
if (!rule.enabled) continue;
|
|
399
|
+
if (ConditionEvaluator::evaluate(rule.expression.c_str(), *this)) {
|
|
400
|
+
basicBreakpointHit_ = true;
|
|
401
|
+
basicBreakLine_ = curlin;
|
|
402
|
+
basicConditionRuleHitId_ = rule.id;
|
|
403
|
+
paused_ = true;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Clear skip-line when we move to a different line
|
|
411
|
+
if (skipBasicBreakpointLine_ != 0xFFFF && curlin != skipBasicBreakpointLine_) {
|
|
412
|
+
skipBasicBreakpointLine_ = 0xFFFF;
|
|
413
|
+
skipBasicBreakpointStmt_ = -1;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Record trace before execution
|
|
418
|
+
if (traceEnabled_) recordTrace();
|
|
419
|
+
|
|
420
|
+
// Track cycles before instruction
|
|
421
|
+
uint64_t cyclesBefore = cpu_->getTotalCycles();
|
|
422
|
+
|
|
423
|
+
// Profile: record PC before execution
|
|
424
|
+
uint16_t profilePC = profileEnabled_ ? cpu_->getPC() : 0;
|
|
425
|
+
|
|
426
|
+
// Execute one instruction
|
|
427
|
+
cpu_->executeInstruction();
|
|
428
|
+
|
|
429
|
+
// Update disk controller with actual instruction cycles
|
|
430
|
+
uint64_t cyclesUsed = cpu_->getTotalCycles() - cyclesBefore;
|
|
431
|
+
if (disk_) disk_->update(static_cast<int>(cyclesUsed));
|
|
432
|
+
|
|
433
|
+
// Accumulate cycle profiling
|
|
434
|
+
if (profileEnabled_) {
|
|
435
|
+
profileCycles_[profilePC] += static_cast<uint32_t>(cyclesUsed);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Update Mockingboard timers BEFORE next instruction
|
|
439
|
+
// This ensures timer IRQs fire before the CPU can disable them
|
|
440
|
+
if (mockingboard_) mockingboard_->update(static_cast<int>(cyclesUsed));
|
|
441
|
+
|
|
442
|
+
// Update mouse card for VBL interrupt detection
|
|
443
|
+
if (mouse_) mouse_->update(static_cast<int>(cyclesUsed));
|
|
444
|
+
|
|
445
|
+
// Progressive rendering: render scanlines up to current cycle
|
|
446
|
+
video_->renderUpToCycle(cpu_->getTotalCycles());
|
|
447
|
+
|
|
448
|
+
// Check for frame boundary
|
|
449
|
+
uint64_t currentCycle = cpu_->getTotalCycles();
|
|
450
|
+
if (currentCycle - lastFrameCycle_ >= CYCLES_PER_FRAME) {
|
|
451
|
+
// Advance by exactly CYCLES_PER_FRAME to stay aligned with VBL detection
|
|
452
|
+
// ($C019 uses cycles % CYCLES_PER_FRAME). Using currentCycle would drift
|
|
453
|
+
// by a few cycles each frame, desynchronizing raster effects.
|
|
454
|
+
lastFrameCycle_ += CYCLES_PER_FRAME;
|
|
455
|
+
video_->renderFrame(); // Uses this frame's change log
|
|
456
|
+
video_->beginNewFrame(lastFrameCycle_); // Reset log, aligned to frame boundary
|
|
457
|
+
frameReady_ = true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Check watchpoint hit (set by MMU callbacks during execution)
|
|
461
|
+
if (watchpointHit_) return;
|
|
462
|
+
|
|
463
|
+
// Check beam breakpoints
|
|
464
|
+
if (!beamBreakpoints_.empty()) {
|
|
465
|
+
uint64_t fc = cpu_->getTotalCycles() - lastFrameCycle_;
|
|
466
|
+
if (fc >= CYCLES_PER_FRAME) fc %= CYCLES_PER_FRAME;
|
|
467
|
+
int16_t sl = static_cast<int16_t>(fc / 65);
|
|
468
|
+
int16_t hp = static_cast<int16_t>(fc % 65);
|
|
469
|
+
for (auto& bp : beamBreakpoints_) {
|
|
470
|
+
if (!bp.enabled) continue;
|
|
471
|
+
bool scanOk = (bp.scanline < 0) || (sl == bp.scanline);
|
|
472
|
+
bool hPosOk = (bp.hPos < 0) || (hp >= bp.hPos);
|
|
473
|
+
bool valid = (bp.scanline >= 0 || bp.hPos >= 0);
|
|
474
|
+
if (!scanOk || !hPosOk || !valid) continue;
|
|
475
|
+
|
|
476
|
+
// For wildcard-scanline breakpoints (HBLANK, Column), fire once per scanline.
|
|
477
|
+
// For specific-scanline breakpoints (VBL, Scanline, ScanCol), fire once per frame.
|
|
478
|
+
bool alreadyFired;
|
|
479
|
+
if (bp.scanline < 0) {
|
|
480
|
+
alreadyFired = (lastFrameCycle_ == bp.lastFireFrame && sl == bp.lastFireScanline);
|
|
481
|
+
} else {
|
|
482
|
+
alreadyFired = (lastFrameCycle_ == bp.lastFireFrame);
|
|
483
|
+
}
|
|
484
|
+
if (!alreadyFired) {
|
|
485
|
+
beamBreakHit_ = true;
|
|
486
|
+
beamBreakHitId_ = bp.id;
|
|
487
|
+
beamBreakHitScanline_ = sl;
|
|
488
|
+
beamBreakHitHPos_ = hp;
|
|
489
|
+
bp.lastFireFrame = lastFrameCycle_;
|
|
490
|
+
bp.lastFireScanline = sl;
|
|
491
|
+
paused_ = true;
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
void Emulator::setSpeedMultiplier(int multiplier) {
|
|
500
|
+
if (multiplier < 1) multiplier = 1;
|
|
501
|
+
if (multiplier > 8) multiplier = 8;
|
|
502
|
+
speedMultiplier_ = multiplier;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
int Emulator::generateStereoAudioSamples(float *buffer, int sampleCount) {
|
|
506
|
+
// Calculate cycles needed for this audio buffer, scaled by speed multiplier
|
|
507
|
+
int cyclesToRun = static_cast<int>(sampleCount * CYCLES_PER_SAMPLE * speedMultiplier_);
|
|
508
|
+
|
|
509
|
+
// Run emulation for the required cycles
|
|
510
|
+
runCycles(cyclesToRun);
|
|
511
|
+
|
|
512
|
+
// Track samples for frame synchronization
|
|
513
|
+
samplesGenerated_ += sampleCount;
|
|
514
|
+
|
|
515
|
+
// Generate stereo audio samples (interleaved L/R)
|
|
516
|
+
return audio_->generateStereoSamples(buffer, sampleCount, cpu_->getTotalCycles());
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
int Emulator::consumeFrameSamples() {
|
|
520
|
+
// Returns number of complete frames worth of samples generated
|
|
521
|
+
// 48000 Hz / 60 Hz = 800 samples per frame
|
|
522
|
+
int frames = samplesGenerated_ / SAMPLES_PER_FRAME;
|
|
523
|
+
samplesGenerated_ %= SAMPLES_PER_FRAME;
|
|
524
|
+
return frames;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const uint8_t *Emulator::getFramebuffer() const {
|
|
528
|
+
return video_->getFramebuffer();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
int Emulator::handleRawKeyDown(int browserKeycode, bool shift, bool ctrl,
|
|
532
|
+
bool alt, bool meta, bool capsLock) {
|
|
533
|
+
int result = keyboard_->handleKeyDown(browserKeycode, shift, ctrl, alt, meta, capsLock);
|
|
534
|
+
|
|
535
|
+
// Update button state from modifier keys
|
|
536
|
+
setButton(0, keyboard_->isOpenApplePressed()); // Open Apple
|
|
537
|
+
setButton(1, keyboard_->isClosedApplePressed()); // Closed Apple
|
|
538
|
+
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
void Emulator::handleRawKeyUp(int browserKeycode, bool shift, bool ctrl,
|
|
543
|
+
bool alt, bool meta) {
|
|
544
|
+
keyboard_->handleKeyUp(browserKeycode, shift, ctrl, alt, meta);
|
|
545
|
+
|
|
546
|
+
// Update button state from modifier keys
|
|
547
|
+
setButton(0, keyboard_->isOpenApplePressed()); // Open Apple
|
|
548
|
+
setButton(1, keyboard_->isClosedApplePressed()); // Closed Apple
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
void Emulator::keyDown(int keycode) {
|
|
552
|
+
// Direct Apple II keycode input (used for paste functionality)
|
|
553
|
+
// Sets the keyboard latch with high bit set
|
|
554
|
+
keyboardLatch_ = (keycode & 0x7F) | 0x80;
|
|
555
|
+
keyDown_ = true;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
void Emulator::keyUp(int keycode) {
|
|
559
|
+
(void)keycode;
|
|
560
|
+
keyDown_ = false;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
void Emulator::setButton(int button, bool pressed) {
|
|
564
|
+
if (button >= 0 && button < 3) {
|
|
565
|
+
buttonState_[button] = pressed;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
void Emulator::setPaddleValue(int paddle, int value) {
|
|
570
|
+
mmu_->setPaddleValue(paddle, static_cast<uint8_t>(value & 0xFF));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
int Emulator::getPaddleValue(int paddle) const {
|
|
574
|
+
return mmu_->getPaddleValue(paddle);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
uint8_t Emulator::getButtonState(int button) {
|
|
578
|
+
if (button >= 0 && button < 3 && buttonState_[button]) {
|
|
579
|
+
return 0x80; // Bit 7 set = button pressed
|
|
580
|
+
}
|
|
581
|
+
return 0x00; // Button not pressed
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
bool Emulator::insertDisk(int drive, const uint8_t *data, size_t size,
|
|
585
|
+
const char *filename) {
|
|
586
|
+
if (!disk_) return false;
|
|
587
|
+
return disk_->insertDisk(drive, data, size, filename ? filename : "");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
bool Emulator::insertBlankDisk(int drive) {
|
|
591
|
+
if (!disk_) return false;
|
|
592
|
+
return disk_->insertBlankDisk(drive);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
void Emulator::ejectDisk(int drive) {
|
|
596
|
+
if (disk_) disk_->ejectDisk(drive);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const uint8_t *Emulator::getDiskData(int drive, size_t *size) const {
|
|
600
|
+
if (!disk_) return nullptr;
|
|
601
|
+
return disk_->getDiskData(drive, size);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const uint8_t *Emulator::exportDiskData(int drive, size_t *size) {
|
|
605
|
+
if (!disk_) return nullptr;
|
|
606
|
+
return disk_->exportDiskData(drive, size);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const char *Emulator::getDiskFilename(int drive) const {
|
|
610
|
+
if (!disk_) return nullptr;
|
|
611
|
+
const auto *image = disk_->getDiskImage(drive);
|
|
612
|
+
if (!image) {
|
|
613
|
+
return nullptr;
|
|
614
|
+
}
|
|
615
|
+
return image->getFilename().c_str();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ============================================================================
|
|
619
|
+
// Beam Position
|
|
620
|
+
// ============================================================================
|
|
621
|
+
|
|
622
|
+
int Emulator::getFrameCycle() const {
|
|
623
|
+
return static_cast<int>(cpu_->getTotalCycles() % CYCLES_PER_FRAME);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
int Emulator::getBeamScanline() const {
|
|
627
|
+
return getFrameCycle() / CYCLES_PER_SCANLINE;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
int Emulator::getBeamHPos() const {
|
|
631
|
+
return getFrameCycle() % CYCLES_PER_SCANLINE;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
int Emulator::getBeamColumn() const {
|
|
635
|
+
int hPos = getBeamHPos();
|
|
636
|
+
return hPos >= 25 ? hPos - 25 : -1;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
bool Emulator::isInVBL() const {
|
|
640
|
+
return getBeamScanline() >= 192;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
bool Emulator::isInHBLANK() const {
|
|
644
|
+
return getBeamHPos() < 25;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ============================================================================
|
|
648
|
+
// Step Over / Step Out
|
|
649
|
+
// ============================================================================
|
|
650
|
+
|
|
651
|
+
uint16_t Emulator::stepOver() {
|
|
652
|
+
clearTempBreakpoint();
|
|
653
|
+
uint16_t pc = cpu_->getPC();
|
|
654
|
+
uint8_t opcode = mmu_->peek(pc);
|
|
655
|
+
|
|
656
|
+
if (opcode == 0x20) {
|
|
657
|
+
// JSR - set temp breakpoint at instruction after JSR (PC + 3)
|
|
658
|
+
uint16_t returnAddr = (pc + 3) & 0xFFFF;
|
|
659
|
+
tempBreakpoint_ = returnAddr;
|
|
660
|
+
tempBreakpointActive_ = true;
|
|
661
|
+
setPaused(false);
|
|
662
|
+
return returnAddr;
|
|
663
|
+
} else if (opcode == 0x00) {
|
|
664
|
+
// BRK - treat like JSR but with PC+2 as return address
|
|
665
|
+
uint16_t returnAddr = (pc + 2) & 0xFFFF;
|
|
666
|
+
tempBreakpoint_ = returnAddr;
|
|
667
|
+
tempBreakpointActive_ = true;
|
|
668
|
+
setPaused(false);
|
|
669
|
+
return returnAddr;
|
|
670
|
+
} else {
|
|
671
|
+
// Not a JSR/BRK, just single step
|
|
672
|
+
stepInstruction();
|
|
673
|
+
return 0;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
uint16_t Emulator::stepOut() {
|
|
678
|
+
clearTempBreakpoint();
|
|
679
|
+
uint8_t sp = cpu_->getSP();
|
|
680
|
+
uint8_t pcl = mmu_->peek(0x0100 + ((sp + 1) & 0xFF));
|
|
681
|
+
uint8_t pch = mmu_->peek(0x0100 + ((sp + 2) & 0xFF));
|
|
682
|
+
// RTS adds 1 to the address
|
|
683
|
+
uint16_t returnAddr = ((pch << 8) | pcl) + 1;
|
|
684
|
+
|
|
685
|
+
if (returnAddr > 0 && returnAddr <= 0xFFFF) {
|
|
686
|
+
returnAddr &= 0xFFFF;
|
|
687
|
+
tempBreakpoint_ = returnAddr;
|
|
688
|
+
tempBreakpointActive_ = true;
|
|
689
|
+
setPaused(false);
|
|
690
|
+
return returnAddr;
|
|
691
|
+
} else {
|
|
692
|
+
// Invalid return address, just step
|
|
693
|
+
stepInstruction();
|
|
694
|
+
return 0;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
void Emulator::clearTempBreakpoint() {
|
|
699
|
+
if (tempBreakpointActive_) {
|
|
700
|
+
tempBreakpointActive_ = false;
|
|
701
|
+
tempBreakpoint_ = 0;
|
|
702
|
+
tempBreakpointHit_ = false;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ============================================================================
|
|
707
|
+
// Breakpoints
|
|
708
|
+
// ============================================================================
|
|
709
|
+
|
|
710
|
+
void Emulator::addBreakpoint(uint16_t address) { breakpoints_.insert(address); }
|
|
711
|
+
|
|
712
|
+
void Emulator::removeBreakpoint(uint16_t address) {
|
|
713
|
+
breakpoints_.erase(address);
|
|
714
|
+
disabledBreakpoints_.erase(address);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
void Emulator::enableBreakpoint(uint16_t address, bool enabled) {
|
|
718
|
+
if (enabled) {
|
|
719
|
+
disabledBreakpoints_.erase(address);
|
|
720
|
+
} else {
|
|
721
|
+
if (breakpoints_.count(address)) {
|
|
722
|
+
disabledBreakpoints_.insert(address);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ============================================================================
|
|
728
|
+
// BASIC Breakpoints
|
|
729
|
+
// ============================================================================
|
|
730
|
+
|
|
731
|
+
void Emulator::addBasicBreakpoint(uint16_t lineNumber, int statementIndex) {
|
|
732
|
+
basicBreakpoints_.insert({lineNumber, static_cast<int8_t>(statementIndex)});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
void Emulator::removeBasicBreakpoint(uint16_t lineNumber, int statementIndex) {
|
|
736
|
+
basicBreakpoints_.erase({lineNumber, static_cast<int8_t>(statementIndex)});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
void Emulator::clearBasicBreakpoints() {
|
|
740
|
+
basicBreakpoints_.clear();
|
|
741
|
+
basicBreakpointHit_ = false;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
void Emulator::clearBasicBreakpointHit() {
|
|
745
|
+
basicBreakpointHit_ = false;
|
|
746
|
+
basicConditionRuleHitId_ = -1;
|
|
747
|
+
// Don't clear skipBasicBreakpointLine_ here - let it be cleared naturally
|
|
748
|
+
// when CURLIN changes. This allows Run to work from a breakpoint by:
|
|
749
|
+
// 1. setPaused(false) sets skip line
|
|
750
|
+
// 2. clearBasicBreakpointHit() clears step mode but keeps skip
|
|
751
|
+
// 3. Program continues, types RUN, skip cleared when line changes
|
|
752
|
+
basicStepMode_ = BasicStepMode::None;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
void Emulator::addBasicConditionRule(int id, const char* expression) {
|
|
756
|
+
// Remove existing rule with same id
|
|
757
|
+
removeBasicConditionRule(id);
|
|
758
|
+
basicConditionRules_.push_back({id, std::string(expression), true});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
void Emulator::removeBasicConditionRule(int id) {
|
|
762
|
+
basicConditionRules_.erase(
|
|
763
|
+
std::remove_if(basicConditionRules_.begin(), basicConditionRules_.end(),
|
|
764
|
+
[id](const BasicConditionRule& r) { return r.id == id; }),
|
|
765
|
+
basicConditionRules_.end());
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
void Emulator::clearBasicConditionRules() {
|
|
769
|
+
basicConditionRules_.clear();
|
|
770
|
+
basicConditionRuleHitId_ = -1;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
void Emulator::stepBasicLine() {
|
|
774
|
+
// Get current BASIC line (use readRAM to bypass ALTZP)
|
|
775
|
+
uint16_t curlin = mmu_->readRAM(0x75, false) | (mmu_->readRAM(0x76, false) << 8);
|
|
776
|
+
|
|
777
|
+
// Set up line stepping mode - will pause when CURLIN changes
|
|
778
|
+
basicStepFromLine_ = curlin;
|
|
779
|
+
basicStepLineStart_ = 0; // Not used for line stepping, but reset for cleanliness
|
|
780
|
+
basicStepNextColon_ = 0; // Not used for line stepping
|
|
781
|
+
basicStepMode_ = BasicStepMode::Line;
|
|
782
|
+
|
|
783
|
+
// Clear any hit flags, reset sample counter to prevent backlog, and resume
|
|
784
|
+
basicBreakpointHit_ = false;
|
|
785
|
+
samplesGenerated_ = 0;
|
|
786
|
+
paused_ = false;
|
|
787
|
+
basicBreakLine_ = 0;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
void Emulator::stepBasicStatement() {
|
|
791
|
+
// Step to the next BASIC statement by waiting for PC to hit $D820
|
|
792
|
+
// (JSR EXECUTE_STATEMENT in the ROM). Both new-line and colon paths
|
|
793
|
+
// converge there with correct CURLIN and TXTPTR.
|
|
794
|
+
uint16_t pc = cpu_->getPC();
|
|
795
|
+
basicStepSkipFirst_ = (pc == 0xD820);
|
|
796
|
+
basicStepMode_ = BasicStepMode::Statement;
|
|
797
|
+
|
|
798
|
+
// Clear any hit flags, reset sample counter to prevent backlog, and resume
|
|
799
|
+
basicBreakpointHit_ = false;
|
|
800
|
+
samplesGenerated_ = 0;
|
|
801
|
+
paused_ = false;
|
|
802
|
+
basicBreakLine_ = 0;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
uint16_t Emulator::getBasicTxtptr() const {
|
|
806
|
+
// Read TXTPTR respecting current ALTZP state - BASIC writes to whichever
|
|
807
|
+
// bank is active, so we need to read from the same bank
|
|
808
|
+
return mmu_->peek(0xB8) | (mmu_->peek(0xB9) << 8);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
int Emulator::getBasicStatementIndex() {
|
|
812
|
+
// Use readRAM to bypass ALTZP - BASIC always uses main RAM for zero page
|
|
813
|
+
uint16_t curlin = mmu_->readRAM(0x75, false) | (mmu_->readRAM(0x76, false) << 8);
|
|
814
|
+
uint16_t txtptr = mmu_->readRAM(0xB8, false) | (mmu_->readRAM(0xB9, false) << 8);
|
|
815
|
+
|
|
816
|
+
// Find line start for the current line
|
|
817
|
+
uint16_t lineStart = findCurrentLineStart(curlin);
|
|
818
|
+
|
|
819
|
+
// If TXTPTR hasn't entered the current line's text area yet, we're at statement 0
|
|
820
|
+
if (lineStart == 0 || txtptr < lineStart) {
|
|
821
|
+
return 0;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return countColonsBetween(lineStart, txtptr);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
int Emulator::countColonsBetween(uint16_t lineStart, uint16_t txtptr) {
|
|
828
|
+
// If TXTPTR is at or before line start, we're at statement 0
|
|
829
|
+
if (lineStart == 0 || txtptr <= lineStart) return 0;
|
|
830
|
+
|
|
831
|
+
// Count colons from line start to TXTPTR, respecting strings
|
|
832
|
+
// Use readRAM to ensure we read from main RAM where BASIC program is stored
|
|
833
|
+
int colonCount = 0;
|
|
834
|
+
bool inQuote = false;
|
|
835
|
+
bool inRem = false;
|
|
836
|
+
|
|
837
|
+
for (uint16_t a = lineStart; a < txtptr; a++) {
|
|
838
|
+
uint8_t byte = mmu_->readRAM(a, false);
|
|
839
|
+
|
|
840
|
+
if (byte == 0) break; // End of line
|
|
841
|
+
|
|
842
|
+
if (inRem) continue; // Skip everything after REM
|
|
843
|
+
|
|
844
|
+
if (byte == 0x22) { // Quote
|
|
845
|
+
inQuote = !inQuote;
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (inQuote) continue; // Skip string contents
|
|
850
|
+
|
|
851
|
+
if (byte == 0xB2) { // REM token
|
|
852
|
+
inRem = true;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (byte == 0x3A) { // Colon
|
|
857
|
+
colonCount++;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return colonCount;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
uint16_t Emulator::findNextColonAfter(uint16_t lineStart, uint16_t afterPos) {
|
|
865
|
+
// Find the address of the next colon after afterPos within the line
|
|
866
|
+
// Returns 0 if no colon found (i.e., afterPos is in the last statement)
|
|
867
|
+
if (lineStart == 0) return 0;
|
|
868
|
+
|
|
869
|
+
// Start searching from afterPos (or lineStart if afterPos is before it)
|
|
870
|
+
uint16_t searchStart = (afterPos >= lineStart) ? afterPos : lineStart;
|
|
871
|
+
bool inQuote = false;
|
|
872
|
+
bool inRem = false;
|
|
873
|
+
|
|
874
|
+
// First, establish quote/REM state at searchStart by scanning from lineStart
|
|
875
|
+
for (uint16_t a = lineStart; a < searchStart; a++) {
|
|
876
|
+
uint8_t byte = mmu_->readRAM(a, false);
|
|
877
|
+
if (byte == 0) return 0; // Already past end of line
|
|
878
|
+
if (inRem) continue;
|
|
879
|
+
if (byte == 0x22) inQuote = !inQuote;
|
|
880
|
+
if (!inQuote && byte == 0xB2) inRem = true;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Now search for the next colon
|
|
884
|
+
for (uint16_t a = searchStart; a < searchStart + 256; a++) { // Limit search
|
|
885
|
+
uint8_t byte = mmu_->readRAM(a, false);
|
|
886
|
+
|
|
887
|
+
if (byte == 0) return 0; // End of line, no more colons
|
|
888
|
+
|
|
889
|
+
if (inRem) continue;
|
|
890
|
+
|
|
891
|
+
if (byte == 0x22) {
|
|
892
|
+
inQuote = !inQuote;
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (inQuote) continue;
|
|
897
|
+
|
|
898
|
+
if (byte == 0xB2) {
|
|
899
|
+
inRem = true;
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (byte == 0x3A) { // Found a colon!
|
|
904
|
+
return a;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return 0; // No colon found
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
uint16_t Emulator::findCurrentLineStart(uint16_t lineNumber) {
|
|
912
|
+
// Get TXTTAB (start of BASIC program)
|
|
913
|
+
// Use readRAM to bypass ALTZP - BASIC always uses main RAM for zero page
|
|
914
|
+
uint16_t txttab = mmu_->readRAM(0x67, false) | (mmu_->readRAM(0x68, false) << 8);
|
|
915
|
+
|
|
916
|
+
if (lineNumber == 0xFFFF) return 0; // Not running
|
|
917
|
+
|
|
918
|
+
// Find the specified line in the program
|
|
919
|
+
uint16_t addr = txttab;
|
|
920
|
+
|
|
921
|
+
while (addr < 0xC000) { // Reasonable upper bound
|
|
922
|
+
uint16_t nextPtr = mmu_->readRAM(addr, false) | (mmu_->readRAM(addr + 1, false) << 8);
|
|
923
|
+
if (nextPtr == 0) break; // End of program
|
|
924
|
+
|
|
925
|
+
uint16_t lineNum = mmu_->readRAM(addr + 2, false) | (mmu_->readRAM(addr + 3, false) << 8);
|
|
926
|
+
if (lineNum == lineNumber) {
|
|
927
|
+
return addr + 4; // Start of tokenized text (after nextPtr and lineNum)
|
|
928
|
+
}
|
|
929
|
+
addr = nextPtr;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return 0; // Line not found
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// ============================================================================
|
|
936
|
+
// BASIC Heat Map
|
|
937
|
+
// ============================================================================
|
|
938
|
+
|
|
939
|
+
int Emulator::getBasicHeatMapData(uint16_t* lines, uint32_t* counts, int maxEntries) const {
|
|
940
|
+
int i = 0;
|
|
941
|
+
for (const auto& [line, count] : basicHeatMap_) {
|
|
942
|
+
if (i >= maxEntries) break;
|
|
943
|
+
lines[i] = line;
|
|
944
|
+
counts[i] = count;
|
|
945
|
+
i++;
|
|
946
|
+
}
|
|
947
|
+
return i;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// ============================================================================
|
|
951
|
+
// Watchpoints
|
|
952
|
+
// ============================================================================
|
|
953
|
+
|
|
954
|
+
void Emulator::addWatchpoint(uint16_t startAddr, uint16_t endAddr, WatchpointType type) {
|
|
955
|
+
watchpoints_.push_back({startAddr, endAddr, type, true});
|
|
956
|
+
watchpointsActive_ = true;
|
|
957
|
+
mmu_->setWatchpointsActive(true);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
void Emulator::removeWatchpoint(uint16_t startAddr) {
|
|
961
|
+
watchpoints_.erase(
|
|
962
|
+
std::remove_if(watchpoints_.begin(), watchpoints_.end(),
|
|
963
|
+
[startAddr](const Watchpoint& wp) { return wp.startAddr == startAddr; }),
|
|
964
|
+
watchpoints_.end());
|
|
965
|
+
watchpointsActive_ = !watchpoints_.empty();
|
|
966
|
+
mmu_->setWatchpointsActive(watchpointsActive_);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
void Emulator::clearWatchpoints() {
|
|
970
|
+
watchpoints_.clear();
|
|
971
|
+
watchpointsActive_ = false;
|
|
972
|
+
mmu_->setWatchpointsActive(false);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
void Emulator::onWatchpointRead(uint16_t address, uint8_t value) {
|
|
976
|
+
if (!watchpointsActive_ || watchpointHit_) return;
|
|
977
|
+
for (const auto& wp : watchpoints_) {
|
|
978
|
+
if (!wp.enabled) continue;
|
|
979
|
+
if ((wp.type & WP_READ) && address >= wp.startAddr && address <= wp.endAddr) {
|
|
980
|
+
watchpointHit_ = true;
|
|
981
|
+
watchpointAddress_ = address;
|
|
982
|
+
watchpointValue_ = value;
|
|
983
|
+
watchpointIsWrite_ = false;
|
|
984
|
+
paused_ = true;
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
void Emulator::onWatchpointWrite(uint16_t address, uint8_t value) {
|
|
991
|
+
if (!watchpointsActive_ || watchpointHit_) return;
|
|
992
|
+
for (const auto& wp : watchpoints_) {
|
|
993
|
+
if (!wp.enabled) continue;
|
|
994
|
+
if ((wp.type & WP_WRITE) && address >= wp.startAddr && address <= wp.endAddr) {
|
|
995
|
+
watchpointHit_ = true;
|
|
996
|
+
watchpointAddress_ = address;
|
|
997
|
+
watchpointValue_ = value;
|
|
998
|
+
watchpointIsWrite_ = true;
|
|
999
|
+
paused_ = true;
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// ============================================================================
|
|
1006
|
+
// Beam Breakpoints
|
|
1007
|
+
// ============================================================================
|
|
1008
|
+
|
|
1009
|
+
int32_t Emulator::addBeamBreakpoint(int16_t scanline, int16_t hPos) {
|
|
1010
|
+
if (beamBreakpoints_.size() >= MAX_BEAM_BREAKPOINTS) return -1;
|
|
1011
|
+
int32_t id = beamBreakNextId_++;
|
|
1012
|
+
beamBreakpoints_.push_back({scanline, hPos, true, id, UINT64_MAX, -1});
|
|
1013
|
+
return id;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
void Emulator::removeBeamBreakpoint(int32_t id) {
|
|
1017
|
+
beamBreakpoints_.erase(
|
|
1018
|
+
std::remove_if(beamBreakpoints_.begin(), beamBreakpoints_.end(),
|
|
1019
|
+
[id](const BeamBreakpoint& bp) { return bp.id == id; }),
|
|
1020
|
+
beamBreakpoints_.end());
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
void Emulator::enableBeamBreakpoint(int32_t id, bool enabled) {
|
|
1024
|
+
for (auto& bp : beamBreakpoints_) {
|
|
1025
|
+
if (bp.id == id) {
|
|
1026
|
+
bp.enabled = enabled;
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
void Emulator::clearAllBeamBreakpoints() {
|
|
1033
|
+
beamBreakpoints_.clear();
|
|
1034
|
+
beamBreakNextId_ = 1;
|
|
1035
|
+
beamBreakHit_ = false;
|
|
1036
|
+
beamBreakHitId_ = -1;
|
|
1037
|
+
beamBreakHitScanline_ = -1;
|
|
1038
|
+
beamBreakHitHPos_ = -1;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// ============================================================================
|
|
1042
|
+
// Trace Log
|
|
1043
|
+
// ============================================================================
|
|
1044
|
+
|
|
1045
|
+
void Emulator::recordTrace() {
|
|
1046
|
+
if (traceBuffer_.empty()) {
|
|
1047
|
+
traceBuffer_.resize(10000);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
auto& entry = traceBuffer_[traceHead_];
|
|
1051
|
+
entry.pc = cpu_->getPC();
|
|
1052
|
+
entry.opcode = mmu_->peek(entry.pc);
|
|
1053
|
+
entry.a = cpu_->getA();
|
|
1054
|
+
entry.x = cpu_->getX();
|
|
1055
|
+
entry.y = cpu_->getY();
|
|
1056
|
+
entry.sp = cpu_->getSP();
|
|
1057
|
+
entry.p = cpu_->getP();
|
|
1058
|
+
|
|
1059
|
+
// Read operands
|
|
1060
|
+
entry.instrLen = 1;
|
|
1061
|
+
entry.operand1 = 0;
|
|
1062
|
+
entry.operand2 = 0;
|
|
1063
|
+
|
|
1064
|
+
// Determine instruction length from opcode
|
|
1065
|
+
static const uint8_t instrLengths[256] = {
|
|
1066
|
+
1,2,1,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,2,2,2,2,1,3,1,1,3,3,3,3,
|
|
1067
|
+
3,2,1,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,2,2,2,2,1,3,1,1,3,3,3,3,
|
|
1068
|
+
1,2,1,1,1,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,1,2,2,2,1,3,1,1,1,3,3,3,
|
|
1069
|
+
1,2,1,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,2,2,2,2,1,3,1,1,3,3,3,3,
|
|
1070
|
+
2,2,1,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,2,2,2,2,1,3,1,1,3,3,3,3,
|
|
1071
|
+
2,2,2,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,2,2,2,2,1,3,1,1,3,3,3,3,
|
|
1072
|
+
2,2,1,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,1,2,2,2,1,3,1,1,1,3,3,3,
|
|
1073
|
+
2,2,1,1,2,2,2,2,1,2,1,1,3,3,3,3,2,2,2,1,1,2,2,2,1,3,1,1,1,3,3,3,
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
entry.instrLen = instrLengths[entry.opcode];
|
|
1077
|
+
if (entry.instrLen >= 2) entry.operand1 = mmu_->peek(entry.pc + 1);
|
|
1078
|
+
if (entry.instrLen >= 3) entry.operand2 = mmu_->peek(entry.pc + 2);
|
|
1079
|
+
|
|
1080
|
+
entry.cycle = static_cast<uint32_t>(cpu_->getTotalCycles());
|
|
1081
|
+
entry.padding = 0;
|
|
1082
|
+
|
|
1083
|
+
traceHead_ = (traceHead_ + 1) % traceBuffer_.size();
|
|
1084
|
+
if (traceCount_ < traceBuffer_.size()) traceCount_++;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
void Emulator::stepInstruction() {
|
|
1088
|
+
breakpointHit_ = false;
|
|
1089
|
+
watchpointHit_ = false;
|
|
1090
|
+
beamBreakHit_ = false;
|
|
1091
|
+
beamBreakHitId_ = -1;
|
|
1092
|
+
|
|
1093
|
+
// Record trace before execution
|
|
1094
|
+
if (traceEnabled_) recordTrace();
|
|
1095
|
+
|
|
1096
|
+
// Track cycles before instruction
|
|
1097
|
+
uint64_t cyclesBefore = cpu_->getTotalCycles();
|
|
1098
|
+
|
|
1099
|
+
// Profile: record PC before execution
|
|
1100
|
+
uint16_t profilePC = profileEnabled_ ? cpu_->getPC() : 0;
|
|
1101
|
+
|
|
1102
|
+
cpu_->executeInstruction();
|
|
1103
|
+
|
|
1104
|
+
// Update disk controller with actual instruction cycles
|
|
1105
|
+
uint64_t cyclesUsed = cpu_->getTotalCycles() - cyclesBefore;
|
|
1106
|
+
if (disk_) disk_->update(static_cast<int>(cyclesUsed));
|
|
1107
|
+
|
|
1108
|
+
// Accumulate cycle profiling
|
|
1109
|
+
if (profileEnabled_) {
|
|
1110
|
+
profileCycles_[profilePC] += static_cast<uint32_t>(cyclesUsed);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Update Mockingboard timers
|
|
1114
|
+
if (mockingboard_) mockingboard_->update(static_cast<int>(cyclesUsed));
|
|
1115
|
+
|
|
1116
|
+
// Update mouse card
|
|
1117
|
+
if (mouse_) mouse_->update(static_cast<int>(cyclesUsed));
|
|
1118
|
+
|
|
1119
|
+
// Progressive rendering: render scanlines up to current cycle
|
|
1120
|
+
video_->renderUpToCycle(cpu_->getTotalCycles());
|
|
1121
|
+
|
|
1122
|
+
// Check for frame boundary
|
|
1123
|
+
uint64_t currentCycle = cpu_->getTotalCycles();
|
|
1124
|
+
if (currentCycle - lastFrameCycle_ >= CYCLES_PER_FRAME) {
|
|
1125
|
+
lastFrameCycle_ += CYCLES_PER_FRAME;
|
|
1126
|
+
video_->renderFrame();
|
|
1127
|
+
video_->beginNewFrame(lastFrameCycle_);
|
|
1128
|
+
frameReady_ = true;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
uint8_t Emulator::readMemory(uint16_t address) const {
|
|
1133
|
+
return const_cast<MMU *>(mmu_.get())->read(address);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
uint8_t Emulator::peekMemory(uint16_t address) const {
|
|
1137
|
+
return mmu_->peek(address);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
void Emulator::writeMemory(uint16_t address, uint8_t value) {
|
|
1141
|
+
mmu_->write(address, value);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const char *Emulator::disassembleAt(uint16_t address) {
|
|
1145
|
+
disasmBuffer_ = cpu_->disassembleAt(address);
|
|
1146
|
+
return disasmBuffer_.c_str();
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
uint64_t Emulator::getSoftSwitchState() const {
|
|
1150
|
+
const auto &sw = mmu_->getSoftSwitches();
|
|
1151
|
+
uint64_t state = 0;
|
|
1152
|
+
|
|
1153
|
+
// Pack soft switch state into a 64-bit value
|
|
1154
|
+
// Display switches (bits 0-5)
|
|
1155
|
+
if (sw.text)
|
|
1156
|
+
state |= (1ULL << 0);
|
|
1157
|
+
if (sw.mixed)
|
|
1158
|
+
state |= (1ULL << 1);
|
|
1159
|
+
if (sw.page2)
|
|
1160
|
+
state |= (1ULL << 2);
|
|
1161
|
+
if (sw.hires)
|
|
1162
|
+
state |= (1ULL << 3);
|
|
1163
|
+
if (sw.col80)
|
|
1164
|
+
state |= (1ULL << 4);
|
|
1165
|
+
if (sw.altCharSet)
|
|
1166
|
+
state |= (1ULL << 5);
|
|
1167
|
+
|
|
1168
|
+
// Memory switches (bits 6-12)
|
|
1169
|
+
if (sw.store80)
|
|
1170
|
+
state |= (1ULL << 6);
|
|
1171
|
+
if (sw.ramrd)
|
|
1172
|
+
state |= (1ULL << 7);
|
|
1173
|
+
if (sw.ramwrt)
|
|
1174
|
+
state |= (1ULL << 8);
|
|
1175
|
+
if (sw.intcxrom)
|
|
1176
|
+
state |= (1ULL << 9);
|
|
1177
|
+
if (sw.altzp)
|
|
1178
|
+
state |= (1ULL << 10);
|
|
1179
|
+
if (sw.slotc3rom)
|
|
1180
|
+
state |= (1ULL << 11);
|
|
1181
|
+
if (sw.intc8rom)
|
|
1182
|
+
state |= (1ULL << 12);
|
|
1183
|
+
|
|
1184
|
+
// Language card (bits 13-16)
|
|
1185
|
+
if (sw.lcram)
|
|
1186
|
+
state |= (1ULL << 13);
|
|
1187
|
+
if (sw.lcram2)
|
|
1188
|
+
state |= (1ULL << 14);
|
|
1189
|
+
if (sw.lcwrite)
|
|
1190
|
+
state |= (1ULL << 15);
|
|
1191
|
+
if (sw.lcprewrite)
|
|
1192
|
+
state |= (1ULL << 16);
|
|
1193
|
+
|
|
1194
|
+
// Annunciators (bits 17-20)
|
|
1195
|
+
if (sw.an0)
|
|
1196
|
+
state |= (1ULL << 17);
|
|
1197
|
+
if (sw.an1)
|
|
1198
|
+
state |= (1ULL << 18);
|
|
1199
|
+
if (sw.an2)
|
|
1200
|
+
state |= (1ULL << 19);
|
|
1201
|
+
if (sw.an3)
|
|
1202
|
+
state |= (1ULL << 20);
|
|
1203
|
+
|
|
1204
|
+
// I/O state (bits 21-23)
|
|
1205
|
+
if (sw.vblBar)
|
|
1206
|
+
state |= (1ULL << 21);
|
|
1207
|
+
if (sw.cassetteOut)
|
|
1208
|
+
state |= (1ULL << 22);
|
|
1209
|
+
if (sw.cassetteIn)
|
|
1210
|
+
state |= (1ULL << 23);
|
|
1211
|
+
|
|
1212
|
+
// Buttons (bits 24-26)
|
|
1213
|
+
if (buttonState_[0])
|
|
1214
|
+
state |= (1ULL << 24);
|
|
1215
|
+
if (buttonState_[1])
|
|
1216
|
+
state |= (1ULL << 25);
|
|
1217
|
+
if (buttonState_[2])
|
|
1218
|
+
state |= (1ULL << 26);
|
|
1219
|
+
|
|
1220
|
+
// Keyboard (bit 27)
|
|
1221
|
+
if (keyboardLatch_ & 0x80)
|
|
1222
|
+
state |= (1ULL << 27);
|
|
1223
|
+
|
|
1224
|
+
// DHIRES (bit 28) - computed from AN3 off + 80COL + HIRES
|
|
1225
|
+
bool dhires = !sw.an3 && sw.col80 && sw.hires;
|
|
1226
|
+
if (dhires)
|
|
1227
|
+
state |= (1ULL << 28);
|
|
1228
|
+
|
|
1229
|
+
// IOUDIS (bit 29)
|
|
1230
|
+
if (sw.ioudis)
|
|
1231
|
+
state |= (1ULL << 29);
|
|
1232
|
+
|
|
1233
|
+
return state;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
uint8_t Emulator::cpuRead(uint16_t address) { return mmu_->read(address); }
|
|
1237
|
+
|
|
1238
|
+
void Emulator::cpuWrite(uint16_t address, uint8_t value) {
|
|
1239
|
+
mmu_->write(address, value);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
uint8_t Emulator::getKeyboardData() { return keyboardLatch_; }
|
|
1243
|
+
|
|
1244
|
+
void Emulator::clearKeyboardStrobe() {
|
|
1245
|
+
keyboardLatch_ &= 0x7F; // Clear high bit
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
void Emulator::toggleSpeaker() {
|
|
1249
|
+
audio_->toggleSpeaker(cpu_->getTotalCycles());
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// ============================================================================
|
|
1253
|
+
// Mouse Input
|
|
1254
|
+
// ============================================================================
|
|
1255
|
+
|
|
1256
|
+
void Emulator::mouseMove(int dx, int dy) {
|
|
1257
|
+
if (mouse_) {
|
|
1258
|
+
mouse_->addDelta(dx, dy);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
void Emulator::mouseButton(bool pressed) {
|
|
1263
|
+
if (mouse_) {
|
|
1264
|
+
mouse_->setMouseButton(pressed);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// ============================================================================
|
|
1269
|
+
// Slot Management
|
|
1270
|
+
// ============================================================================
|
|
1271
|
+
|
|
1272
|
+
const char* Emulator::getSlotCardName(uint8_t slot) const {
|
|
1273
|
+
if (slot < 1 || slot > 7) {
|
|
1274
|
+
return "invalid";
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// Slot 3 is built-in 80-column
|
|
1278
|
+
if (slot == 3) {
|
|
1279
|
+
return "80col";
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Check the slot array for all cards
|
|
1283
|
+
ExpansionCard* card = mmu_->getCard(slot);
|
|
1284
|
+
if (!card) {
|
|
1285
|
+
return "empty";
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Identify card type by name
|
|
1289
|
+
const char* name = card->getName();
|
|
1290
|
+
if (strcmp(name, "Disk II") == 0) {
|
|
1291
|
+
return "disk2";
|
|
1292
|
+
}
|
|
1293
|
+
if (strcmp(name, "Mockingboard") == 0) {
|
|
1294
|
+
return "mockingboard";
|
|
1295
|
+
}
|
|
1296
|
+
if (strcmp(name, "Thunderclock") == 0) {
|
|
1297
|
+
return "thunderclock";
|
|
1298
|
+
}
|
|
1299
|
+
if (strcmp(name, "Mouse") == 0) {
|
|
1300
|
+
return "mouse";
|
|
1301
|
+
}
|
|
1302
|
+
if (strcmp(name, "SmartPort") == 0) {
|
|
1303
|
+
return "smartport";
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
return "empty";
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
bool Emulator::setSlotCard(uint8_t slot, const char* cardId) {
|
|
1310
|
+
if (slot < 1 || slot > 7) {
|
|
1311
|
+
return false;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Slot 3 is built-in 80-column and cannot be changed
|
|
1315
|
+
if (slot == 3) {
|
|
1316
|
+
return false;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Before any slot change, clean up existing special card pointers.
|
|
1320
|
+
// insertCard() destroys the old card, so dangling pointers must be cleared.
|
|
1321
|
+
ExpansionCard* existing = mmu_->getCard(slot);
|
|
1322
|
+
if (existing) {
|
|
1323
|
+
const char* existingName = existing->getName();
|
|
1324
|
+
if (strcmp(existingName, "Mockingboard") == 0 && slot == 4) {
|
|
1325
|
+
// Move Mockingboard to storage so it can be restored later
|
|
1326
|
+
if (!mbStorage_) {
|
|
1327
|
+
mbStorage_ = mmu_->removeCard(4);
|
|
1328
|
+
mockingboard_ = nullptr;
|
|
1329
|
+
audio_->setMockingboard(nullptr);
|
|
1330
|
+
}
|
|
1331
|
+
} else if (strcmp(existingName, "Disk II") == 0 && slot == 6) {
|
|
1332
|
+
if (!diskStorage_) {
|
|
1333
|
+
diskStorage_ = mmu_->removeCard(6);
|
|
1334
|
+
disk_ = nullptr;
|
|
1335
|
+
}
|
|
1336
|
+
} else if (strcmp(existingName, "Mouse") == 0) {
|
|
1337
|
+
mouse_ = nullptr;
|
|
1338
|
+
mmu_->removeCard(slot);
|
|
1339
|
+
} else if (strcmp(existingName, "SmartPort") == 0) {
|
|
1340
|
+
smartport_ = nullptr;
|
|
1341
|
+
mmu_->removeCard(slot);
|
|
1342
|
+
} else {
|
|
1343
|
+
mmu_->removeCard(slot);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Handle empty slot - cleanup above already removed the card
|
|
1348
|
+
if (strcmp(cardId, "empty") == 0) {
|
|
1349
|
+
return true;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// Handle Disk II card
|
|
1353
|
+
if (strcmp(cardId, "disk2") == 0) {
|
|
1354
|
+
if (slot != 6) {
|
|
1355
|
+
return false;
|
|
1356
|
+
}
|
|
1357
|
+
// Re-insert from storage
|
|
1358
|
+
if (diskStorage_) {
|
|
1359
|
+
disk_ = static_cast<Disk2Card*>(diskStorage_.get());
|
|
1360
|
+
mmu_->insertCard(6, std::move(diskStorage_));
|
|
1361
|
+
}
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Handle Mockingboard card
|
|
1366
|
+
if (strcmp(cardId, "mockingboard") == 0) {
|
|
1367
|
+
if (slot != 4) {
|
|
1368
|
+
return false;
|
|
1369
|
+
}
|
|
1370
|
+
// Re-insert from storage
|
|
1371
|
+
if (mbStorage_) {
|
|
1372
|
+
mockingboard_ = static_cast<MockingboardCard*>(mbStorage_.get());
|
|
1373
|
+
mmu_->insertCard(4, std::move(mbStorage_));
|
|
1374
|
+
audio_->setMockingboard(mockingboard_);
|
|
1375
|
+
}
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// Handle Thunderclock card
|
|
1380
|
+
if (strcmp(cardId, "thunderclock") == 0) {
|
|
1381
|
+
auto card = std::make_unique<ThunderclockCard>();
|
|
1382
|
+
mmu_->insertCard(slot, std::move(card));
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// Handle Mouse card
|
|
1387
|
+
if (strcmp(cardId, "mouse") == 0) {
|
|
1388
|
+
auto card = std::make_unique<MouseCard>();
|
|
1389
|
+
card->setSlotNumber(slot);
|
|
1390
|
+
card->setCycleCallback([this]() { return cpu_->getTotalCycles(); });
|
|
1391
|
+
card->setIRQCallback([this]() { cpu_->irq(); });
|
|
1392
|
+
mouse_ = card.get();
|
|
1393
|
+
mmu_->insertCard(slot, std::move(card));
|
|
1394
|
+
return true;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Handle SmartPort card
|
|
1398
|
+
if (strcmp(cardId, "smartport") == 0) {
|
|
1399
|
+
auto card = std::make_unique<SmartPortCard>();
|
|
1400
|
+
card->setSlotNumber(slot);
|
|
1401
|
+
card->setMemReadCallback([this](uint16_t addr) { return mmu_->read(addr); });
|
|
1402
|
+
card->setMemWriteCallback([this](uint16_t addr, uint8_t val) { mmu_->write(addr, val); });
|
|
1403
|
+
card->setGetA([this]() { return cpu_->getA(); });
|
|
1404
|
+
card->setSetA([this](uint8_t v) { cpu_->setA(v); });
|
|
1405
|
+
card->setGetP([this]() { return cpu_->getP(); });
|
|
1406
|
+
card->setSetP([this](uint8_t v) { cpu_->setP(v); });
|
|
1407
|
+
card->setGetSP([this]() { return cpu_->getSP(); });
|
|
1408
|
+
card->setSetSP([this](uint8_t v) { cpu_->setSP(v); });
|
|
1409
|
+
card->setGetPC([this]() { return cpu_->getPC(); });
|
|
1410
|
+
card->setSetPC([this](uint16_t v) { cpu_->setPC(v); });
|
|
1411
|
+
card->setSetX([this](uint8_t v) { cpu_->setX(v); });
|
|
1412
|
+
card->setSetY([this](uint8_t v) { cpu_->setY(v); });
|
|
1413
|
+
smartport_ = card.get();
|
|
1414
|
+
mmu_->insertCard(slot, std::move(card));
|
|
1415
|
+
return true;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
bool Emulator::isSlotEmpty(uint8_t slot) const {
|
|
1422
|
+
if (slot < 1 || slot > 7) {
|
|
1423
|
+
return true;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// Slot 3 is never empty (built-in 80-column)
|
|
1427
|
+
if (slot == 3) {
|
|
1428
|
+
return false;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
return mmu_->isSlotEmpty(slot);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// ============================================================================
|
|
1435
|
+
// State Serialization
|
|
1436
|
+
// ============================================================================
|
|
1437
|
+
|
|
1438
|
+
// State format version - increment when format changes
|
|
1439
|
+
static constexpr uint32_t STATE_VERSION = 7; // LSS 8-phase clock
|
|
1440
|
+
static constexpr uint32_t STATE_MAGIC = 0x53324541; // "A2ES" in little-endian
|
|
1441
|
+
|
|
1442
|
+
// Helper to write little-endian values
|
|
1443
|
+
static void writeLE16(std::vector<uint8_t> &buf, uint16_t val) {
|
|
1444
|
+
buf.push_back(val & 0xFF);
|
|
1445
|
+
buf.push_back((val >> 8) & 0xFF);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
static void writeLE32(std::vector<uint8_t> &buf, uint32_t val) {
|
|
1449
|
+
buf.push_back(val & 0xFF);
|
|
1450
|
+
buf.push_back((val >> 8) & 0xFF);
|
|
1451
|
+
buf.push_back((val >> 16) & 0xFF);
|
|
1452
|
+
buf.push_back((val >> 24) & 0xFF);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
static void writeLE64(std::vector<uint8_t> &buf, uint64_t val) {
|
|
1456
|
+
for (int i = 0; i < 8; i++) {
|
|
1457
|
+
buf.push_back((val >> (i * 8)) & 0xFF);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Helper to read little-endian values
|
|
1462
|
+
static uint16_t readLE16(const uint8_t *data) {
|
|
1463
|
+
return data[0] | (data[1] << 8);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
static uint32_t readLE32(const uint8_t *data) {
|
|
1467
|
+
return data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
static uint64_t readLE64(const uint8_t *data) {
|
|
1471
|
+
uint64_t val = 0;
|
|
1472
|
+
for (int i = 0; i < 8; i++) {
|
|
1473
|
+
val |= static_cast<uint64_t>(data[i]) << (i * 8);
|
|
1474
|
+
}
|
|
1475
|
+
return val;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
const uint8_t *Emulator::exportState(size_t *size) {
|
|
1479
|
+
stateBuffer_.clear();
|
|
1480
|
+
|
|
1481
|
+
// Reserve approximate size (slightly over to avoid reallocations)
|
|
1482
|
+
// ~200KB total (128KB main/aux + 32KB LC RAM + overhead + disk images)
|
|
1483
|
+
stateBuffer_.reserve(500000);
|
|
1484
|
+
|
|
1485
|
+
// Header
|
|
1486
|
+
writeLE32(stateBuffer_, STATE_MAGIC);
|
|
1487
|
+
writeLE32(stateBuffer_, STATE_VERSION);
|
|
1488
|
+
|
|
1489
|
+
// CPU state
|
|
1490
|
+
stateBuffer_.push_back(cpu_->getA());
|
|
1491
|
+
stateBuffer_.push_back(cpu_->getX());
|
|
1492
|
+
stateBuffer_.push_back(cpu_->getY());
|
|
1493
|
+
stateBuffer_.push_back(cpu_->getSP());
|
|
1494
|
+
stateBuffer_.push_back(cpu_->getP());
|
|
1495
|
+
stateBuffer_.push_back(0); // Reserved
|
|
1496
|
+
writeLE16(stateBuffer_, cpu_->getPC());
|
|
1497
|
+
writeLE64(stateBuffer_, cpu_->getTotalCycles());
|
|
1498
|
+
|
|
1499
|
+
// Memory - Main RAM (64KB)
|
|
1500
|
+
for (uint32_t addr = 0; addr < MAIN_RAM_SIZE; addr++) {
|
|
1501
|
+
stateBuffer_.push_back(mmu_->readRAM(addr, false));
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// Memory - Aux RAM (64KB)
|
|
1505
|
+
for (uint32_t addr = 0; addr < AUX_RAM_SIZE; addr++) {
|
|
1506
|
+
stateBuffer_.push_back(mmu_->readRAM(addr, true));
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// Language Card RAM - Main (16KB total: 4KB bank1 + 4KB bank2 + 8KB high)
|
|
1510
|
+
const uint8_t *lcb1 = mmu_->getLCBank1(false);
|
|
1511
|
+
const uint8_t *lcb2 = mmu_->getLCBank2(false);
|
|
1512
|
+
const uint8_t *lchi = mmu_->getLCHighRAM(false);
|
|
1513
|
+
stateBuffer_.insert(stateBuffer_.end(), lcb1, lcb1 + 0x1000);
|
|
1514
|
+
stateBuffer_.insert(stateBuffer_.end(), lcb2, lcb2 + 0x1000);
|
|
1515
|
+
stateBuffer_.insert(stateBuffer_.end(), lchi, lchi + 0x2000);
|
|
1516
|
+
|
|
1517
|
+
// Language Card RAM - Aux (16KB total)
|
|
1518
|
+
const uint8_t *alcb1 = mmu_->getLCBank1(true);
|
|
1519
|
+
const uint8_t *alcb2 = mmu_->getLCBank2(true);
|
|
1520
|
+
const uint8_t *alchi = mmu_->getLCHighRAM(true);
|
|
1521
|
+
stateBuffer_.insert(stateBuffer_.end(), alcb1, alcb1 + 0x1000);
|
|
1522
|
+
stateBuffer_.insert(stateBuffer_.end(), alcb2, alcb2 + 0x1000);
|
|
1523
|
+
stateBuffer_.insert(stateBuffer_.end(), alchi, alchi + 0x2000);
|
|
1524
|
+
|
|
1525
|
+
// Soft switches - pack into bytes
|
|
1526
|
+
const auto &sw = mmu_->getSoftSwitches();
|
|
1527
|
+
uint32_t switches1 = 0;
|
|
1528
|
+
if (sw.text) switches1 |= (1 << 0);
|
|
1529
|
+
if (sw.mixed) switches1 |= (1 << 1);
|
|
1530
|
+
if (sw.page2) switches1 |= (1 << 2);
|
|
1531
|
+
if (sw.hires) switches1 |= (1 << 3);
|
|
1532
|
+
if (sw.col80) switches1 |= (1 << 4);
|
|
1533
|
+
if (sw.altCharSet) switches1 |= (1 << 5);
|
|
1534
|
+
if (sw.store80) switches1 |= (1 << 6);
|
|
1535
|
+
if (sw.ramrd) switches1 |= (1 << 7);
|
|
1536
|
+
if (sw.ramwrt) switches1 |= (1 << 8);
|
|
1537
|
+
if (sw.intcxrom) switches1 |= (1 << 9);
|
|
1538
|
+
if (sw.altzp) switches1 |= (1 << 10);
|
|
1539
|
+
if (sw.slotc3rom) switches1 |= (1 << 11);
|
|
1540
|
+
if (sw.intc8rom) switches1 |= (1 << 12);
|
|
1541
|
+
if (sw.lcram) switches1 |= (1 << 13);
|
|
1542
|
+
if (sw.lcram2) switches1 |= (1 << 14);
|
|
1543
|
+
if (sw.lcwrite) switches1 |= (1 << 15);
|
|
1544
|
+
if (sw.lcprewrite) switches1 |= (1 << 16);
|
|
1545
|
+
if (sw.an0) switches1 |= (1 << 17);
|
|
1546
|
+
if (sw.an1) switches1 |= (1 << 18);
|
|
1547
|
+
if (sw.an2) switches1 |= (1 << 19);
|
|
1548
|
+
if (sw.an3) switches1 |= (1 << 20);
|
|
1549
|
+
if (sw.ioudis) switches1 |= (1 << 21);
|
|
1550
|
+
writeLE32(stateBuffer_, switches1);
|
|
1551
|
+
|
|
1552
|
+
// Keyboard latch
|
|
1553
|
+
stateBuffer_.push_back(keyboardLatch_);
|
|
1554
|
+
stateBuffer_.push_back(keyDown_ ? 1 : 0);
|
|
1555
|
+
|
|
1556
|
+
// Button state
|
|
1557
|
+
stateBuffer_.push_back(buttonState_[0] ? 1 : 0);
|
|
1558
|
+
stateBuffer_.push_back(buttonState_[1] ? 1 : 0);
|
|
1559
|
+
stateBuffer_.push_back(buttonState_[2] ? 1 : 0);
|
|
1560
|
+
|
|
1561
|
+
// Emulator timing
|
|
1562
|
+
writeLE64(stateBuffer_, lastFrameCycle_);
|
|
1563
|
+
writeLE32(stateBuffer_, samplesGenerated_);
|
|
1564
|
+
|
|
1565
|
+
// Disk controller state
|
|
1566
|
+
auto &disk = *disk_;
|
|
1567
|
+
stateBuffer_.push_back(disk.isMotorOn() ? 1 : 0);
|
|
1568
|
+
stateBuffer_.push_back(static_cast<uint8_t>(disk.getSelectedDrive()));
|
|
1569
|
+
stateBuffer_.push_back(disk.getQ6() ? 1 : 0);
|
|
1570
|
+
stateBuffer_.push_back(disk.getQ7() ? 1 : 0);
|
|
1571
|
+
stateBuffer_.push_back(disk.getPhaseStates());
|
|
1572
|
+
stateBuffer_.push_back(disk.getDataLatch());
|
|
1573
|
+
stateBuffer_.push_back(disk.getSequencerState());
|
|
1574
|
+
stateBuffer_.push_back(disk.getBusData());
|
|
1575
|
+
stateBuffer_.push_back(disk.getLSSClock());
|
|
1576
|
+
|
|
1577
|
+
// Per-drive state (track positions, disk image data, and filenames)
|
|
1578
|
+
for (int drive = 0; drive < 2; drive++) {
|
|
1579
|
+
if (disk.hasDisk(drive)) {
|
|
1580
|
+
const auto *image = disk.getDiskImage(drive);
|
|
1581
|
+
stateBuffer_.push_back(1); // Disk present
|
|
1582
|
+
writeLE16(stateBuffer_, static_cast<uint16_t>(image->getQuarterTrack()));
|
|
1583
|
+
|
|
1584
|
+
// Export the disk image data
|
|
1585
|
+
size_t diskSize = 0;
|
|
1586
|
+
const uint8_t *diskData = disk.exportDiskData(drive, &diskSize);
|
|
1587
|
+
writeLE32(stateBuffer_, static_cast<uint32_t>(diskSize));
|
|
1588
|
+
if (diskData && diskSize > 0) {
|
|
1589
|
+
stateBuffer_.insert(stateBuffer_.end(), diskData, diskData + diskSize);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Save filename
|
|
1593
|
+
const std::string &filename = image->getFilename();
|
|
1594
|
+
writeLE16(stateBuffer_, static_cast<uint16_t>(filename.length()));
|
|
1595
|
+
stateBuffer_.insert(stateBuffer_.end(), filename.begin(), filename.end());
|
|
1596
|
+
} else {
|
|
1597
|
+
stateBuffer_.push_back(0); // No disk
|
|
1598
|
+
writeLE16(stateBuffer_, 0);
|
|
1599
|
+
writeLE32(stateBuffer_, 0); // Zero disk size
|
|
1600
|
+
writeLE16(stateBuffer_, 0); // Zero filename length
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Audio state (speaker)
|
|
1605
|
+
stateBuffer_.push_back(audio_->getSpeakerState() ? 1 : 0);
|
|
1606
|
+
|
|
1607
|
+
// Mockingboard state
|
|
1608
|
+
uint8_t mbState[MockingboardCard::STATE_SIZE];
|
|
1609
|
+
size_t mbSize = 0;
|
|
1610
|
+
if (mockingboard_) {
|
|
1611
|
+
mbSize = mockingboard_->serialize(mbState, sizeof(mbState));
|
|
1612
|
+
}
|
|
1613
|
+
writeLE16(stateBuffer_, static_cast<uint16_t>(mbSize));
|
|
1614
|
+
if (mbSize > 0) {
|
|
1615
|
+
stateBuffer_.insert(stateBuffer_.end(), mbState, mbState + mbSize);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// Expansion card states (slots 1-7, excluding 4 and 6 which are handled above)
|
|
1619
|
+
// First, count how many cards have state to save
|
|
1620
|
+
uint8_t cardCount = 0;
|
|
1621
|
+
for (uint8_t slot = 1; slot <= 7; slot++) {
|
|
1622
|
+
if (slot == 4 || slot == 6) continue; // Handled by legacy system
|
|
1623
|
+
ExpansionCard* card = mmu_->getCard(slot);
|
|
1624
|
+
if (card && card->getStateSize() > 0) {
|
|
1625
|
+
cardCount++;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
stateBuffer_.push_back(cardCount);
|
|
1629
|
+
|
|
1630
|
+
// Save each card's state
|
|
1631
|
+
for (uint8_t slot = 1; slot <= 7; slot++) {
|
|
1632
|
+
if (slot == 4 || slot == 6) continue;
|
|
1633
|
+
ExpansionCard* card = mmu_->getCard(slot);
|
|
1634
|
+
if (card && card->getStateSize() > 0) {
|
|
1635
|
+
// Slot number
|
|
1636
|
+
stateBuffer_.push_back(slot);
|
|
1637
|
+
|
|
1638
|
+
// Card type identifier (use name for identification)
|
|
1639
|
+
const char* name = card->getName();
|
|
1640
|
+
uint8_t cardType = 0;
|
|
1641
|
+
if (strcmp(name, "Thunderclock") == 0) cardType = 1;
|
|
1642
|
+
if (strcmp(name, "Mouse") == 0) cardType = 2;
|
|
1643
|
+
if (strcmp(name, "SmartPort") == 0) cardType = 3;
|
|
1644
|
+
stateBuffer_.push_back(cardType);
|
|
1645
|
+
|
|
1646
|
+
// Card state
|
|
1647
|
+
size_t stateSize = card->getStateSize();
|
|
1648
|
+
writeLE16(stateBuffer_, static_cast<uint16_t>(stateSize));
|
|
1649
|
+
size_t currentSize = stateBuffer_.size();
|
|
1650
|
+
stateBuffer_.resize(currentSize + stateSize);
|
|
1651
|
+
card->serialize(stateBuffer_.data() + currentSize, stateSize);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
*size = stateBuffer_.size();
|
|
1656
|
+
return stateBuffer_.data();
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
bool Emulator::importState(const uint8_t *data, size_t size) {
|
|
1660
|
+
// Minimum size check
|
|
1661
|
+
if (size < 8) {
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
size_t offset = 0;
|
|
1666
|
+
|
|
1667
|
+
// Check magic and version before resetting
|
|
1668
|
+
uint32_t magic = readLE32(data + offset);
|
|
1669
|
+
offset += 4;
|
|
1670
|
+
if (magic != STATE_MAGIC) {
|
|
1671
|
+
return false;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
uint32_t version = readLE32(data + offset);
|
|
1675
|
+
offset += 4;
|
|
1676
|
+
if (version != STATE_VERSION) {
|
|
1677
|
+
return false;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// Reset emulator to clean state before importing
|
|
1681
|
+
// This ensures no old state is left floating around
|
|
1682
|
+
reset();
|
|
1683
|
+
|
|
1684
|
+
// CPU state
|
|
1685
|
+
if (offset + 16 > size) return false;
|
|
1686
|
+
cpu_->setA(data[offset++]);
|
|
1687
|
+
cpu_->setX(data[offset++]);
|
|
1688
|
+
cpu_->setY(data[offset++]);
|
|
1689
|
+
cpu_->setSP(data[offset++]);
|
|
1690
|
+
cpu_->setP(data[offset++]);
|
|
1691
|
+
offset++; // Reserved
|
|
1692
|
+
|
|
1693
|
+
cpu_->setPC(readLE16(data + offset));
|
|
1694
|
+
offset += 2;
|
|
1695
|
+
|
|
1696
|
+
// Restore total cycles for accurate disk timing
|
|
1697
|
+
uint64_t totalCycles = readLE64(data + offset);
|
|
1698
|
+
offset += 8;
|
|
1699
|
+
cpu_->setTotalCycles(totalCycles);
|
|
1700
|
+
|
|
1701
|
+
// Memory - Main RAM (64KB)
|
|
1702
|
+
if (offset + MAIN_RAM_SIZE > size) return false;
|
|
1703
|
+
for (uint32_t addr = 0; addr < MAIN_RAM_SIZE; addr++) {
|
|
1704
|
+
mmu_->writeRAM(addr, data[offset++], false);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// Memory - Aux RAM (64KB)
|
|
1708
|
+
if (offset + AUX_RAM_SIZE > size) return false;
|
|
1709
|
+
for (uint32_t addr = 0; addr < AUX_RAM_SIZE; addr++) {
|
|
1710
|
+
mmu_->writeRAM(addr, data[offset++], true);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// Language Card RAM - Main (16KB: 4KB + 4KB + 8KB)
|
|
1714
|
+
if (offset + 0x4000 > size) return false;
|
|
1715
|
+
mmu_->setLCBank1(data + offset, false);
|
|
1716
|
+
offset += 0x1000;
|
|
1717
|
+
mmu_->setLCBank2(data + offset, false);
|
|
1718
|
+
offset += 0x1000;
|
|
1719
|
+
mmu_->setLCHighRAM(data + offset, false);
|
|
1720
|
+
offset += 0x2000;
|
|
1721
|
+
|
|
1722
|
+
// Language Card RAM - Aux (16KB)
|
|
1723
|
+
if (offset + 0x4000 > size) return false;
|
|
1724
|
+
mmu_->setLCBank1(data + offset, true);
|
|
1725
|
+
offset += 0x1000;
|
|
1726
|
+
mmu_->setLCBank2(data + offset, true);
|
|
1727
|
+
offset += 0x1000;
|
|
1728
|
+
mmu_->setLCHighRAM(data + offset, true);
|
|
1729
|
+
offset += 0x2000;
|
|
1730
|
+
|
|
1731
|
+
// Soft switches
|
|
1732
|
+
if (offset + 4 > size) return false;
|
|
1733
|
+
uint32_t switches1 = readLE32(data + offset);
|
|
1734
|
+
offset += 4;
|
|
1735
|
+
|
|
1736
|
+
// Restore soft switches by writing to appropriate addresses
|
|
1737
|
+
// TEXT/GRAPHICS
|
|
1738
|
+
mmu_->write((switches1 & (1 << 0)) ? 0xC051 : 0xC050, 0);
|
|
1739
|
+
// MIXED
|
|
1740
|
+
mmu_->write((switches1 & (1 << 1)) ? 0xC053 : 0xC052, 0);
|
|
1741
|
+
// PAGE1/PAGE2
|
|
1742
|
+
mmu_->write((switches1 & (1 << 2)) ? 0xC055 : 0xC054, 0);
|
|
1743
|
+
// LORES/HIRES
|
|
1744
|
+
mmu_->write((switches1 & (1 << 3)) ? 0xC057 : 0xC056, 0);
|
|
1745
|
+
// 80COL
|
|
1746
|
+
mmu_->write((switches1 & (1 << 4)) ? 0xC00D : 0xC00C, 0);
|
|
1747
|
+
// ALTCHARSET
|
|
1748
|
+
mmu_->write((switches1 & (1 << 5)) ? 0xC00F : 0xC00E, 0);
|
|
1749
|
+
// 80STORE
|
|
1750
|
+
mmu_->write((switches1 & (1 << 6)) ? 0xC001 : 0xC000, 0);
|
|
1751
|
+
// RAMRD
|
|
1752
|
+
mmu_->write((switches1 & (1 << 7)) ? 0xC003 : 0xC002, 0);
|
|
1753
|
+
// RAMWRT
|
|
1754
|
+
mmu_->write((switches1 & (1 << 8)) ? 0xC005 : 0xC004, 0);
|
|
1755
|
+
// INTCXROM
|
|
1756
|
+
mmu_->write((switches1 & (1 << 9)) ? 0xC007 : 0xC006, 0);
|
|
1757
|
+
// ALTZP
|
|
1758
|
+
mmu_->write((switches1 & (1 << 10)) ? 0xC009 : 0xC008, 0);
|
|
1759
|
+
// SLOTC3ROM
|
|
1760
|
+
mmu_->write((switches1 & (1 << 11)) ? 0xC00B : 0xC00A, 0);
|
|
1761
|
+
// INTC8ROM - this is set automatically by slot access
|
|
1762
|
+
// LCRAM, LCRAM2, LCWRITE - need special handling via language card switches
|
|
1763
|
+
// AN0-AN3
|
|
1764
|
+
mmu_->write((switches1 & (1 << 17)) ? 0xC059 : 0xC058, 0);
|
|
1765
|
+
mmu_->write((switches1 & (1 << 18)) ? 0xC05B : 0xC05A, 0);
|
|
1766
|
+
mmu_->write((switches1 & (1 << 19)) ? 0xC05D : 0xC05C, 0);
|
|
1767
|
+
mmu_->write((switches1 & (1 << 20)) ? 0xC05F : 0xC05E, 0);
|
|
1768
|
+
|
|
1769
|
+
// Language card state restoration
|
|
1770
|
+
bool lcram = switches1 & (1 << 13);
|
|
1771
|
+
bool lcram2 = switches1 & (1 << 14);
|
|
1772
|
+
bool lcwrite = switches1 & (1 << 15);
|
|
1773
|
+
// Select the appropriate language card mode
|
|
1774
|
+
// lcram2 means bank 2 is selected (not bank 1)
|
|
1775
|
+
// $C080: bank2, read RAM, no write
|
|
1776
|
+
// $C081: bank2, read ROM, write (needs double read)
|
|
1777
|
+
// $C083: bank2, read RAM, write (needs double read)
|
|
1778
|
+
// $C088: bank1, read RAM, no write
|
|
1779
|
+
// etc.
|
|
1780
|
+
if (lcram) {
|
|
1781
|
+
if (lcram2) {
|
|
1782
|
+
if (lcwrite) {
|
|
1783
|
+
mmu_->read(0xC083);
|
|
1784
|
+
mmu_->read(0xC083); // Double read to enable write
|
|
1785
|
+
} else {
|
|
1786
|
+
mmu_->read(0xC080);
|
|
1787
|
+
}
|
|
1788
|
+
} else {
|
|
1789
|
+
if (lcwrite) {
|
|
1790
|
+
mmu_->read(0xC08B);
|
|
1791
|
+
mmu_->read(0xC08B);
|
|
1792
|
+
} else {
|
|
1793
|
+
mmu_->read(0xC088);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
} else {
|
|
1797
|
+
if (lcram2) {
|
|
1798
|
+
mmu_->read(0xC082);
|
|
1799
|
+
} else {
|
|
1800
|
+
mmu_->read(0xC08A);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// Keyboard state
|
|
1805
|
+
if (offset + 5 > size) return false;
|
|
1806
|
+
keyboardLatch_ = data[offset++];
|
|
1807
|
+
keyDown_ = data[offset++] != 0;
|
|
1808
|
+
|
|
1809
|
+
// Button state
|
|
1810
|
+
buttonState_[0] = data[offset++] != 0;
|
|
1811
|
+
buttonState_[1] = data[offset++] != 0;
|
|
1812
|
+
buttonState_[2] = data[offset++] != 0;
|
|
1813
|
+
|
|
1814
|
+
// Emulator timing
|
|
1815
|
+
if (offset + 12 > size) return false;
|
|
1816
|
+
lastFrameCycle_ = readLE64(data + offset);
|
|
1817
|
+
offset += 8;
|
|
1818
|
+
samplesGenerated_ = static_cast<int>(readLE32(data + offset));
|
|
1819
|
+
offset += 4;
|
|
1820
|
+
|
|
1821
|
+
// Disk controller state
|
|
1822
|
+
if (offset + 9 > size) return false;
|
|
1823
|
+
bool motorOn = data[offset++] != 0;
|
|
1824
|
+
int selectedDrive = data[offset++];
|
|
1825
|
+
bool q6 = data[offset++] != 0;
|
|
1826
|
+
bool q7 = data[offset++] != 0;
|
|
1827
|
+
uint8_t phaseStates = data[offset++];
|
|
1828
|
+
uint8_t dataLatch = data[offset++];
|
|
1829
|
+
uint8_t seqState = data[offset++];
|
|
1830
|
+
uint8_t busDataVal = data[offset++];
|
|
1831
|
+
uint8_t lssClockVal = data[offset++];
|
|
1832
|
+
|
|
1833
|
+
// Restore disk controller state (including motor for mid-load restores)
|
|
1834
|
+
if (disk_) {
|
|
1835
|
+
disk_->setMotorOn(motorOn);
|
|
1836
|
+
disk_->setSelectedDrive(selectedDrive);
|
|
1837
|
+
disk_->setQ6(q6);
|
|
1838
|
+
disk_->setQ7(q7);
|
|
1839
|
+
disk_->setPhaseStates(phaseStates);
|
|
1840
|
+
disk_->setDataLatch(dataLatch);
|
|
1841
|
+
disk_->setSequencerState(seqState);
|
|
1842
|
+
disk_->setBusData(busDataVal);
|
|
1843
|
+
disk_->setLSSClock(lssClockVal);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// Per-drive state (disk images and track positions)
|
|
1847
|
+
for (int drive = 0; drive < 2; drive++) {
|
|
1848
|
+
if (offset + 3 > size) return false;
|
|
1849
|
+
bool hasDisk = data[offset++] != 0;
|
|
1850
|
+
uint16_t quarterTrack = readLE16(data + offset);
|
|
1851
|
+
offset += 2;
|
|
1852
|
+
|
|
1853
|
+
// Read disk size
|
|
1854
|
+
if (offset + 4 > size) return false;
|
|
1855
|
+
uint32_t diskSize = readLE32(data + offset);
|
|
1856
|
+
offset += 4;
|
|
1857
|
+
|
|
1858
|
+
if (hasDisk && diskSize > 0) {
|
|
1859
|
+
if (offset + diskSize > size) return false;
|
|
1860
|
+
|
|
1861
|
+
// Read disk data offset for insertion
|
|
1862
|
+
const uint8_t *diskData = data + offset;
|
|
1863
|
+
offset += diskSize;
|
|
1864
|
+
|
|
1865
|
+
// Read filename
|
|
1866
|
+
if (offset + 2 > size) return false;
|
|
1867
|
+
uint16_t filenameLen = readLE16(data + offset);
|
|
1868
|
+
offset += 2;
|
|
1869
|
+
|
|
1870
|
+
std::string filename = "state.dsk";
|
|
1871
|
+
if (filenameLen > 0 && offset + filenameLen <= size) {
|
|
1872
|
+
filename = std::string(reinterpret_cast<const char*>(data + offset), filenameLen);
|
|
1873
|
+
offset += filenameLen;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// Insert the disk image from the saved state
|
|
1877
|
+
if (disk_) {
|
|
1878
|
+
disk_->insertDisk(drive, diskData, diskSize, filename);
|
|
1879
|
+
|
|
1880
|
+
// Restore track position
|
|
1881
|
+
auto *image = disk_->getMutableDiskImage(drive);
|
|
1882
|
+
if (image) {
|
|
1883
|
+
image->setQuarterTrack(quarterTrack);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
} else {
|
|
1887
|
+
// Skip filename length field (will be 0)
|
|
1888
|
+
if (offset + 2 <= size) {
|
|
1889
|
+
offset += 2;
|
|
1890
|
+
}
|
|
1891
|
+
// Eject any existing disk
|
|
1892
|
+
if (disk_) disk_->ejectDisk(drive);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// Audio state
|
|
1897
|
+
if (offset + 1 > size) return false;
|
|
1898
|
+
bool speakerState = data[offset++] != 0;
|
|
1899
|
+
(void)speakerState;
|
|
1900
|
+
|
|
1901
|
+
// Mockingboard state
|
|
1902
|
+
if (offset + 2 <= size) {
|
|
1903
|
+
uint16_t mbSize = readLE16(data + offset);
|
|
1904
|
+
offset += 2;
|
|
1905
|
+
if (mbSize > 0 && offset + mbSize <= size) {
|
|
1906
|
+
if (mockingboard_) {
|
|
1907
|
+
mockingboard_->deserialize(data + offset, mbSize);
|
|
1908
|
+
}
|
|
1909
|
+
offset += mbSize;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// Expansion card states
|
|
1914
|
+
if (offset + 1 <= size) {
|
|
1915
|
+
uint8_t cardCount = data[offset++];
|
|
1916
|
+
for (uint8_t i = 0; i < cardCount && offset + 4 <= size; i++) {
|
|
1917
|
+
uint8_t slot = data[offset++];
|
|
1918
|
+
uint8_t cardType = data[offset++];
|
|
1919
|
+
uint16_t stateSize = readLE16(data + offset);
|
|
1920
|
+
offset += 2;
|
|
1921
|
+
|
|
1922
|
+
if (offset + stateSize > size) break;
|
|
1923
|
+
|
|
1924
|
+
// Create card based on type if slot is empty
|
|
1925
|
+
if (slot >= 1 && slot <= 7 && slot != 4 && slot != 6) {
|
|
1926
|
+
ExpansionCard* existingCard = mmu_->getCard(slot);
|
|
1927
|
+
|
|
1928
|
+
// Create appropriate card type if needed
|
|
1929
|
+
if (!existingCard) {
|
|
1930
|
+
switch (cardType) {
|
|
1931
|
+
case 1: { // Thunderclock
|
|
1932
|
+
auto card = std::make_unique<ThunderclockCard>();
|
|
1933
|
+
mmu_->insertCard(slot, std::move(card));
|
|
1934
|
+
existingCard = mmu_->getCard(slot);
|
|
1935
|
+
break;
|
|
1936
|
+
}
|
|
1937
|
+
case 2: { // Mouse
|
|
1938
|
+
auto card = std::make_unique<MouseCard>();
|
|
1939
|
+
card->setSlotNumber(slot);
|
|
1940
|
+
card->setCycleCallback([this]() { return cpu_->getTotalCycles(); });
|
|
1941
|
+
card->setIRQCallback([this]() { cpu_->irq(); });
|
|
1942
|
+
mouse_ = card.get();
|
|
1943
|
+
mmu_->insertCard(slot, std::move(card));
|
|
1944
|
+
existingCard = mmu_->getCard(slot);
|
|
1945
|
+
break;
|
|
1946
|
+
}
|
|
1947
|
+
case 3: { // SmartPort
|
|
1948
|
+
auto card = std::make_unique<SmartPortCard>();
|
|
1949
|
+
card->setSlotNumber(slot);
|
|
1950
|
+
card->setMemReadCallback([this](uint16_t addr) { return mmu_->read(addr); });
|
|
1951
|
+
card->setMemWriteCallback([this](uint16_t addr, uint8_t val) { mmu_->write(addr, val); });
|
|
1952
|
+
card->setGetA([this]() { return cpu_->getA(); });
|
|
1953
|
+
card->setSetA([this](uint8_t v) { cpu_->setA(v); });
|
|
1954
|
+
card->setGetP([this]() { return cpu_->getP(); });
|
|
1955
|
+
card->setSetP([this](uint8_t v) { cpu_->setP(v); });
|
|
1956
|
+
card->setGetSP([this]() { return cpu_->getSP(); });
|
|
1957
|
+
card->setSetSP([this](uint8_t v) { cpu_->setSP(v); });
|
|
1958
|
+
card->setGetPC([this]() { return cpu_->getPC(); });
|
|
1959
|
+
card->setSetPC([this](uint16_t v) { cpu_->setPC(v); });
|
|
1960
|
+
card->setSetX([this](uint8_t v) { cpu_->setX(v); });
|
|
1961
|
+
card->setSetY([this](uint8_t v) { cpu_->setY(v); });
|
|
1962
|
+
smartport_ = card.get();
|
|
1963
|
+
mmu_->insertCard(slot, std::move(card));
|
|
1964
|
+
existingCard = mmu_->getCard(slot);
|
|
1965
|
+
break;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// Restore card state
|
|
1971
|
+
if (existingCard) {
|
|
1972
|
+
existingCard->deserialize(data + offset, stateSize);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
offset += stateSize;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
frameReady_ = true;
|
|
1981
|
+
breakpointHit_ = false;
|
|
1982
|
+
paused_ = false;
|
|
1983
|
+
|
|
1984
|
+
return true;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// ============================================================================
|
|
1988
|
+
// SmartPort Hard Drive Management
|
|
1989
|
+
// ============================================================================
|
|
1990
|
+
|
|
1991
|
+
bool Emulator::insertSmartPortImage(int device, const uint8_t* data, size_t size, const char* filename) {
|
|
1992
|
+
if (!smartport_) return false;
|
|
1993
|
+
return smartport_->insertImage(device, data, size, filename ? filename : "");
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
void Emulator::ejectSmartPortImage(int device) {
|
|
1997
|
+
if (smartport_) smartport_->ejectImage(device);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
bool Emulator::isSmartPortImageInserted(int device) const {
|
|
2001
|
+
if (!smartport_) return false;
|
|
2002
|
+
return smartport_->isImageInserted(device);
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
const char* Emulator::getSmartPortImageFilename(int device) const {
|
|
2006
|
+
if (!smartport_) return nullptr;
|
|
2007
|
+
const auto& fn = smartport_->getImageFilename(device);
|
|
2008
|
+
return fn.empty() ? nullptr : fn.c_str();
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
bool Emulator::isSmartPortImageModified(int device) const {
|
|
2012
|
+
if (!smartport_) return false;
|
|
2013
|
+
return smartport_->isImageModified(device);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
const uint8_t* Emulator::exportSmartPortImageData(int device, size_t* size) const {
|
|
2017
|
+
if (!smartport_) {
|
|
2018
|
+
if (size) *size = 0;
|
|
2019
|
+
return nullptr;
|
|
2020
|
+
}
|
|
2021
|
+
return smartport_->exportImageData(device, size);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
const uint8_t* Emulator::getSmartPortBlockData(int device, size_t* size) const {
|
|
2025
|
+
if (!smartport_) {
|
|
2026
|
+
if (size) *size = 0;
|
|
2027
|
+
return nullptr;
|
|
2028
|
+
}
|
|
2029
|
+
return smartport_->getBlockData(device, size);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// ==========================================================================
|
|
2033
|
+
// Screen text extraction
|
|
2034
|
+
// ==========================================================================
|
|
2035
|
+
|
|
2036
|
+
// Apple II text screen row base addresses (non-linear memory layout)
|
|
2037
|
+
static constexpr uint16_t TEXT_ROW_BASES[24] = {
|
|
2038
|
+
0x400, 0x480, 0x500, 0x580, 0x600, 0x680, 0x700, 0x780,
|
|
2039
|
+
0x428, 0x4A8, 0x528, 0x5A8, 0x628, 0x6A8, 0x728, 0x7A8,
|
|
2040
|
+
0x450, 0x4D0, 0x550, 0x5D0, 0x650, 0x6D0, 0x750, 0x7D0
|
|
2041
|
+
};
|
|
2042
|
+
|
|
2043
|
+
int Emulator::screenCodeToAscii(uint8_t code) {
|
|
2044
|
+
if (code >= 0xE0) return code - 0x80; // Normal lowercase
|
|
2045
|
+
if (code >= 0xC0) return code - 0x80; // Normal uppercase / MouseText
|
|
2046
|
+
if (code >= 0xA0) return code - 0x80; // Normal symbols/digits
|
|
2047
|
+
if (code >= 0x80) return code - 0x40; // Normal uppercase @A-Z[\]^_
|
|
2048
|
+
if (code >= 0x60) return code - 0x40; // Flash symbols
|
|
2049
|
+
if (code >= 0x40) return code; // Flash uppercase
|
|
2050
|
+
if (code >= 0x20) return code; // Inverse symbols/digits
|
|
2051
|
+
return code + 0x40; // Inverse uppercase
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
const char* Emulator::readScreenText(int startRow, int startCol,
|
|
2055
|
+
int endRow, int endCol) {
|
|
2056
|
+
screenTextBuffer_.clear();
|
|
2057
|
+
|
|
2058
|
+
const auto& sw = mmu_->getSoftSwitches();
|
|
2059
|
+
if (!sw.text) {
|
|
2060
|
+
return screenTextBuffer_.c_str();
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
bool col80 = sw.col80;
|
|
2064
|
+
bool page2 = sw.page2;
|
|
2065
|
+
int cols = col80 ? 80 : 40;
|
|
2066
|
+
uint16_t page2Offset = page2 ? 0x400 : 0;
|
|
2067
|
+
|
|
2068
|
+
// Clamp
|
|
2069
|
+
if (startRow < 0) startRow = 0;
|
|
2070
|
+
if (startCol < 0) startCol = 0;
|
|
2071
|
+
if (endRow > 23) endRow = 23;
|
|
2072
|
+
if (endCol >= cols) endCol = cols - 1;
|
|
2073
|
+
|
|
2074
|
+
for (int row = startRow; row <= endRow; row++) {
|
|
2075
|
+
int colStart = (row == startRow) ? startCol : 0;
|
|
2076
|
+
int colEnd = (row == endRow) ? endCol : cols - 1;
|
|
2077
|
+
|
|
2078
|
+
int lineEnd = colEnd; // Track last non-space for trimming
|
|
2079
|
+
|
|
2080
|
+
// First pass: find last non-space character for trimming
|
|
2081
|
+
for (int col = colEnd; col >= colStart; col--) {
|
|
2082
|
+
uint8_t charCode;
|
|
2083
|
+
if (col80) {
|
|
2084
|
+
int memCol = col / 2;
|
|
2085
|
+
bool isAux = (col % 2) == 0;
|
|
2086
|
+
uint16_t addr = TEXT_ROW_BASES[row] + page2Offset + memCol;
|
|
2087
|
+
charCode = isAux ? mmu_->peekAux(addr) : mmu_->peek(addr);
|
|
2088
|
+
} else {
|
|
2089
|
+
uint16_t addr = TEXT_ROW_BASES[row] + page2Offset + col;
|
|
2090
|
+
charCode = mmu_->peek(addr);
|
|
2091
|
+
}
|
|
2092
|
+
int ascii = screenCodeToAscii(charCode);
|
|
2093
|
+
if (ascii != 0x20) {
|
|
2094
|
+
lineEnd = col;
|
|
2095
|
+
break;
|
|
2096
|
+
}
|
|
2097
|
+
if (col == colStart) {
|
|
2098
|
+
lineEnd = colStart - 1; // All spaces
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Second pass: emit characters up to lineEnd
|
|
2103
|
+
for (int col = colStart; col <= lineEnd; col++) {
|
|
2104
|
+
uint8_t charCode;
|
|
2105
|
+
if (col80) {
|
|
2106
|
+
int memCol = col / 2;
|
|
2107
|
+
bool isAux = (col % 2) == 0;
|
|
2108
|
+
uint16_t addr = TEXT_ROW_BASES[row] + page2Offset + memCol;
|
|
2109
|
+
charCode = isAux ? mmu_->peekAux(addr) : mmu_->peek(addr);
|
|
2110
|
+
} else {
|
|
2111
|
+
uint16_t addr = TEXT_ROW_BASES[row] + page2Offset + col;
|
|
2112
|
+
charCode = mmu_->peek(addr);
|
|
2113
|
+
}
|
|
2114
|
+
int ascii = screenCodeToAscii(charCode);
|
|
2115
|
+
screenTextBuffer_ += static_cast<char>(ascii);
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
if (row < endRow) {
|
|
2119
|
+
screenTextBuffer_ += '\n';
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
return screenTextBuffer_.c_str();
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
} // namespace a2e
|