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,680 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* video.cpp - Video output generation implementation
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
#include "video.hpp"
|
|
9
|
+
#include <algorithm>
|
|
10
|
+
#include <cstring>
|
|
11
|
+
|
|
12
|
+
namespace a2e {
|
|
13
|
+
|
|
14
|
+
Video::Video(MMU &mmu) : mmu_(mmu) {
|
|
15
|
+
// Initialize framebuffer to black
|
|
16
|
+
std::memset(framebuffer_.data(), 0, framebuffer_.size());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
VideoMode Video::getCurrentMode() const {
|
|
20
|
+
const auto &sw = mmu_.getSoftSwitches();
|
|
21
|
+
|
|
22
|
+
if (sw.text) {
|
|
23
|
+
return sw.col80 ? VideoMode::TEXT_80 : VideoMode::TEXT_40;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (sw.hires) {
|
|
27
|
+
// DHR requires: AN3 OFF (!an3), 80COL on, HIRES on
|
|
28
|
+
if (sw.col80 && !sw.an3) {
|
|
29
|
+
return VideoMode::DOUBLE_HIRES;
|
|
30
|
+
}
|
|
31
|
+
return VideoMode::HIRES;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Double LoRes: AN3 OFF (!an3), 80COL on
|
|
35
|
+
if (sw.col80 && !sw.an3) {
|
|
36
|
+
return VideoMode::DOUBLE_LORES;
|
|
37
|
+
}
|
|
38
|
+
return VideoMode::LORES;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Raster rendering infrastructure
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
VideoSwitchState Video::captureVideoState() const {
|
|
46
|
+
const auto &sw = mmu_.getSoftSwitches();
|
|
47
|
+
return {sw.text, sw.mixed, sw.page2, sw.hires,
|
|
48
|
+
sw.col80, sw.altCharSet, sw.store80, sw.an3};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
void Video::onVideoSwitchChanged() {
|
|
52
|
+
VideoSwitchState newState = captureVideoState();
|
|
53
|
+
|
|
54
|
+
// Compare against last logged state to avoid redundant entries
|
|
55
|
+
const VideoSwitchState &lastState =
|
|
56
|
+
(switchChangeCount_ > 0) ? switchChanges_[switchChangeCount_ - 1].state
|
|
57
|
+
: frameStartState_;
|
|
58
|
+
|
|
59
|
+
if (std::memcmp(&newState, &lastState, sizeof(VideoSwitchState)) == 0) {
|
|
60
|
+
return; // No actual change
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (switchChangeCount_ >= MAX_SWITCH_CHANGES) {
|
|
64
|
+
return; // Log full, drop this change
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
uint32_t cycleOffset = 0;
|
|
68
|
+
if (cycleCallback_) {
|
|
69
|
+
uint64_t currentCycle = cycleCallback_();
|
|
70
|
+
// +2 models the Apple IIe two-stage video pipeline delay:
|
|
71
|
+
// 1) Phi-0/Phi-1 bus phasing: the video fetches memory on Phi-0 (first half
|
|
72
|
+
// of each clock cycle) before the CPU writes on Phi-1 (second half).
|
|
73
|
+
// A soft switch change on cycle N misses that cycle's video fetch.
|
|
74
|
+
// 2) Shift register latching: the byte fetched on Phi-0 of cycle N+1 is
|
|
75
|
+
// loaded into the shift register and doesn't produce visible dots until
|
|
76
|
+
// approximately cycle N+2.
|
|
77
|
+
// Combined: a CPU write on cycle N affects display output at cycle N+2.
|
|
78
|
+
cycleOffset = static_cast<uint32_t>(currentCycle - frameStartCycle_ + 2);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
switchChanges_[switchChangeCount_] = {cycleOffset, newState};
|
|
82
|
+
switchChangeCount_++;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
void Video::beginNewFrame(uint64_t cycleStart) {
|
|
86
|
+
frameStartCycle_ = cycleStart;
|
|
87
|
+
frameStartState_ = captureVideoState();
|
|
88
|
+
switchChangeCount_ = 0;
|
|
89
|
+
|
|
90
|
+
// Reset progressive rendering state
|
|
91
|
+
lastRenderedScanline_ = -1;
|
|
92
|
+
changeIdx_ = 0;
|
|
93
|
+
currentRenderState_ = frameStartState_;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Character ROM offset helper (deduplicates 40-col and 80-col logic)
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
Video::CharROMInfo Video::getCharROMInfo(uint8_t ch, bool inverse, bool flash,
|
|
101
|
+
const VideoSwitchState &vs) const {
|
|
102
|
+
uint16_t romOffset;
|
|
103
|
+
bool needsXor = false;
|
|
104
|
+
|
|
105
|
+
if (vs.altCharSet) {
|
|
106
|
+
uint8_t charIndex;
|
|
107
|
+
if (ch >= 0x40 && ch < 0x60) {
|
|
108
|
+
charIndex = ch;
|
|
109
|
+
needsXor = true;
|
|
110
|
+
inverse = false;
|
|
111
|
+
} else if (ch >= 0x60 && ch < 0x80) {
|
|
112
|
+
charIndex = ch;
|
|
113
|
+
needsXor = true;
|
|
114
|
+
} else if (ch < 0x40) {
|
|
115
|
+
charIndex = ch;
|
|
116
|
+
needsXor = false;
|
|
117
|
+
} else {
|
|
118
|
+
if (ch < 0xA0) {
|
|
119
|
+
charIndex = ch & 0x1F;
|
|
120
|
+
} else if (ch < 0xC0) {
|
|
121
|
+
charIndex = (ch & 0x1F) + 32;
|
|
122
|
+
} else if (ch < 0xE0) {
|
|
123
|
+
charIndex = ch & 0x1F;
|
|
124
|
+
} else {
|
|
125
|
+
charIndex = (ch & 0x1F) + 96;
|
|
126
|
+
}
|
|
127
|
+
needsXor = false;
|
|
128
|
+
inverse = false;
|
|
129
|
+
}
|
|
130
|
+
romOffset = charIndex * 8;
|
|
131
|
+
} else {
|
|
132
|
+
uint8_t charIndex;
|
|
133
|
+
if (ch < 0x20) {
|
|
134
|
+
charIndex = ch;
|
|
135
|
+
} else if (ch < 0x40) {
|
|
136
|
+
charIndex = ch;
|
|
137
|
+
} else if (ch < 0x60) {
|
|
138
|
+
charIndex = ch & 0x1F;
|
|
139
|
+
} else if (ch < 0x80) {
|
|
140
|
+
charIndex = (ch & 0x1F) + 32;
|
|
141
|
+
} else if (ch < 0xA0) {
|
|
142
|
+
charIndex = ch & 0x1F;
|
|
143
|
+
inverse = false;
|
|
144
|
+
} else if (ch < 0xC0) {
|
|
145
|
+
charIndex = (ch & 0x1F) + 32;
|
|
146
|
+
inverse = false;
|
|
147
|
+
} else if (ch < 0xE0) {
|
|
148
|
+
charIndex = ch & 0x1F;
|
|
149
|
+
inverse = false;
|
|
150
|
+
} else {
|
|
151
|
+
charIndex = (ch & 0x1F) + 96;
|
|
152
|
+
inverse = false;
|
|
153
|
+
}
|
|
154
|
+
romOffset = charIndex * 8;
|
|
155
|
+
needsXor = false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Apply UK character set offset if enabled
|
|
159
|
+
if (ukCharSet_) {
|
|
160
|
+
romOffset += 0x1000;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Handle flash - toggle inverse state when flash is active
|
|
164
|
+
if (flash && flashState_ && !vs.altCharSet) {
|
|
165
|
+
inverse = !inverse;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {romOffset, needsXor, inverse};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// Per-character-line rendering
|
|
173
|
+
// ============================================================================
|
|
174
|
+
|
|
175
|
+
void Video::renderCharacterLine(int col, int textRow, int charLine,
|
|
176
|
+
uint8_t ch, bool inverse, bool flash,
|
|
177
|
+
const VideoSwitchState &vs, bool is80col) {
|
|
178
|
+
CharROMInfo info = getCharROMInfo(ch, inverse, flash, vs);
|
|
179
|
+
|
|
180
|
+
uint32_t fgColor, bgColor;
|
|
181
|
+
if (monochrome_) {
|
|
182
|
+
fgColor = getMonochromeColor(true);
|
|
183
|
+
bgColor = getMonochromeColor(false);
|
|
184
|
+
} else {
|
|
185
|
+
fgColor = 0xFFFFFFFF;
|
|
186
|
+
bgColor = 0xFF000000;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (info.inverse) {
|
|
190
|
+
std::swap(fgColor, bgColor);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
uint8_t rowData = mmu_.readCharROM(info.romOffset + charLine);
|
|
194
|
+
if (info.needsXor) {
|
|
195
|
+
rowData ^= 0xFF;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
int screenY = textRow * 16 + charLine * 2;
|
|
199
|
+
|
|
200
|
+
if (is80col) {
|
|
201
|
+
// 80-col: col is 0-79, each character is 7 pixels wide (560 total)
|
|
202
|
+
int screenX = col * 7;
|
|
203
|
+
for (int charCol = 0; charCol < 7; charCol++) {
|
|
204
|
+
bool pixelOn = (rowData & (1 << charCol)) != 0;
|
|
205
|
+
uint32_t color = pixelOn ? fgColor : bgColor;
|
|
206
|
+
int px = screenX + charCol;
|
|
207
|
+
setPixel(px, screenY, color);
|
|
208
|
+
setPixel(px, screenY + 1, color);
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
// 40-col: col is 0-39, each character is 14 pixels wide (560 total)
|
|
212
|
+
int screenX = col * 14;
|
|
213
|
+
for (int charCol = 0; charCol < 7; charCol++) {
|
|
214
|
+
bool pixelOn = (rowData & (1 << charCol)) != 0;
|
|
215
|
+
uint32_t color = pixelOn ? fgColor : bgColor;
|
|
216
|
+
int px = screenX + charCol * 2;
|
|
217
|
+
setPixel(px, screenY, color);
|
|
218
|
+
setPixel(px + 1, screenY, color);
|
|
219
|
+
setPixel(px, screenY + 1, color);
|
|
220
|
+
setPixel(px + 1, screenY + 1, color);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ============================================================================
|
|
226
|
+
// Per-scanline segment renderers
|
|
227
|
+
// Each renders a column range [startCol, endCol) on a single scanline.
|
|
228
|
+
// Columns are byte positions 0-40 matching the hardware's per-cycle reads.
|
|
229
|
+
// ============================================================================
|
|
230
|
+
|
|
231
|
+
void Video::renderText40Scanline(int scanline, int startCol, int endCol,
|
|
232
|
+
const VideoSwitchState &vs) {
|
|
233
|
+
int textRow = scanline / 8;
|
|
234
|
+
int charLine = scanline % 8;
|
|
235
|
+
if (textRow >= 24) return;
|
|
236
|
+
|
|
237
|
+
for (int col = startCol; col < endCol; col++) {
|
|
238
|
+
uint16_t addr = getTextAddress(textRow, col);
|
|
239
|
+
|
|
240
|
+
uint8_t ch;
|
|
241
|
+
if (vs.page2 && !vs.store80) {
|
|
242
|
+
ch = mmu_.readRAM(addr + 0x0400, false);
|
|
243
|
+
} else {
|
|
244
|
+
ch = mmu_.readRAM(addr, false);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
bool inverse = (ch & 0xC0) == 0x00;
|
|
248
|
+
bool flash = (ch & 0xC0) == 0x40;
|
|
249
|
+
|
|
250
|
+
renderCharacterLine(col, textRow, charLine, ch, inverse, flash, vs, false);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
void Video::renderText80Scanline(int scanline, int startCol, int endCol,
|
|
255
|
+
const VideoSwitchState &vs) {
|
|
256
|
+
int textRow = scanline / 8;
|
|
257
|
+
int charLine = scanline % 8;
|
|
258
|
+
if (textRow >= 24) return;
|
|
259
|
+
|
|
260
|
+
uint16_t pageOffset = (vs.page2 && !vs.store80) ? 0x0400 : 0x0000;
|
|
261
|
+
|
|
262
|
+
for (int col = startCol; col < endCol; col++) {
|
|
263
|
+
uint16_t addr = getTextAddress(textRow, col);
|
|
264
|
+
|
|
265
|
+
// Aux memory character (even columns in display)
|
|
266
|
+
uint8_t auxCh = mmu_.readRAM(addr + pageOffset, true);
|
|
267
|
+
bool auxInverse = (auxCh & 0xC0) == 0x00;
|
|
268
|
+
bool auxFlash = (auxCh & 0xC0) == 0x40;
|
|
269
|
+
renderCharacterLine(col * 2, textRow, charLine, auxCh, auxInverse, auxFlash, vs, true);
|
|
270
|
+
|
|
271
|
+
// Main memory character (odd columns in display)
|
|
272
|
+
uint8_t mainCh = mmu_.readRAM(addr + pageOffset, false);
|
|
273
|
+
bool mainInverse = (mainCh & 0xC0) == 0x00;
|
|
274
|
+
bool mainFlash = (mainCh & 0xC0) == 0x40;
|
|
275
|
+
renderCharacterLine(col * 2 + 1, textRow, charLine, mainCh, mainInverse, mainFlash, vs, true);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
void Video::renderLoResScanline(int scanline, int startCol, int endCol,
|
|
280
|
+
const VideoSwitchState &vs) {
|
|
281
|
+
int textRow = scanline / 8;
|
|
282
|
+
int lineInRow = scanline % 8;
|
|
283
|
+
if (textRow >= 24) return;
|
|
284
|
+
|
|
285
|
+
int screenY = scanline * 2;
|
|
286
|
+
|
|
287
|
+
for (int col = startCol; col < endCol; col++) {
|
|
288
|
+
uint16_t addr = getTextAddress(textRow, col);
|
|
289
|
+
|
|
290
|
+
uint8_t colorByte;
|
|
291
|
+
if (vs.page2 && !vs.store80) {
|
|
292
|
+
colorByte = mmu_.readRAM(addr + 0x0400, false);
|
|
293
|
+
} else {
|
|
294
|
+
colorByte = mmu_.readRAM(addr, false);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
uint8_t colorIndex = (lineInRow < 4) ? (colorByte & 0x0F)
|
|
298
|
+
: ((colorByte >> 4) & 0x0F);
|
|
299
|
+
|
|
300
|
+
uint32_t color = getLoResColor(colorIndex);
|
|
301
|
+
int screenX = col * 14;
|
|
302
|
+
|
|
303
|
+
for (int px = 0; px < 14; px++) {
|
|
304
|
+
setPixel(screenX + px, screenY, color);
|
|
305
|
+
setPixel(screenX + px, screenY + 1, color);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
void Video::renderHiResScanline(int scanline, int startCol, int endCol,
|
|
311
|
+
const VideoSwitchState &vs) {
|
|
312
|
+
if (scanline >= 192) return;
|
|
313
|
+
|
|
314
|
+
// Build dot/highBit arrays for the columns in our range.
|
|
315
|
+
// Full 280-element arrays are zero-initialized so dots outside
|
|
316
|
+
// our segment read as off — correct behavior at mode boundaries.
|
|
317
|
+
uint8_t dots[280] = {0};
|
|
318
|
+
uint8_t highBits[280] = {0};
|
|
319
|
+
|
|
320
|
+
for (int col = startCol; col < endCol; col++) {
|
|
321
|
+
uint16_t addr = getHiResAddress(scanline, col);
|
|
322
|
+
|
|
323
|
+
uint8_t dataByte;
|
|
324
|
+
if (vs.page2 && !vs.store80) {
|
|
325
|
+
dataByte = mmu_.readRAM(addr + 0x2000, false);
|
|
326
|
+
} else {
|
|
327
|
+
dataByte = mmu_.readRAM(addr, false);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
bool highBit = (dataByte & 0x80) != 0;
|
|
331
|
+
|
|
332
|
+
for (int bit = 0; bit < 7; bit++) {
|
|
333
|
+
int dotX = col * 7 + bit;
|
|
334
|
+
dots[dotX] = (dataByte & (1 << bit)) ? 1 : 0;
|
|
335
|
+
highBits[dotX] = highBit ? 1 : 0;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
int screenY = scanline * 2;
|
|
340
|
+
int dotStart = startCol * 7;
|
|
341
|
+
int dotEnd = endCol * 7;
|
|
342
|
+
|
|
343
|
+
if (monochrome_) {
|
|
344
|
+
for (int x = dotStart; x < dotEnd; x++) {
|
|
345
|
+
uint32_t color = getMonochromeColor(dots[x] != 0);
|
|
346
|
+
int screenX = x * 2;
|
|
347
|
+
setPixel(screenX, screenY, color);
|
|
348
|
+
setPixel(screenX + 1, screenY, color);
|
|
349
|
+
setPixel(screenX, screenY + 1, color);
|
|
350
|
+
setPixel(screenX + 1, screenY + 1, color);
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
for (int x = dotStart; x < dotEnd; x++) {
|
|
354
|
+
uint32_t color;
|
|
355
|
+
bool highBit = highBits[x] != 0;
|
|
356
|
+
bool dotOn = dots[x] != 0;
|
|
357
|
+
|
|
358
|
+
bool prevOn = (x > 0) && dots[x - 1];
|
|
359
|
+
bool nextOn = (x < 279) && dots[x + 1];
|
|
360
|
+
|
|
361
|
+
if (!dotOn) {
|
|
362
|
+
bool prev2On = (x > 1) && dots[x - 2];
|
|
363
|
+
bool next2On = (x < 278) && dots[x + 2];
|
|
364
|
+
|
|
365
|
+
if (prevOn && nextOn && !prev2On && !next2On) {
|
|
366
|
+
if (highBits[x - 1] == highBits[x + 1]) {
|
|
367
|
+
bool neighborEven = ((x - 1) & 1) == 0;
|
|
368
|
+
bool neighborHighBit = highBits[x - 1];
|
|
369
|
+
if (neighborEven) {
|
|
370
|
+
color = neighborHighBit ? HIRES_COLORS[4] : HIRES_COLORS[2];
|
|
371
|
+
} else {
|
|
372
|
+
color = neighborHighBit ? HIRES_COLORS[5] : HIRES_COLORS[1];
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
color = HIRES_COLORS[0];
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
color = HIRES_COLORS[0];
|
|
379
|
+
}
|
|
380
|
+
} else if (prevOn || nextOn) {
|
|
381
|
+
color = HIRES_COLORS[3]; // White
|
|
382
|
+
} else {
|
|
383
|
+
bool evenColumn = (x & 1) == 0;
|
|
384
|
+
if (evenColumn) {
|
|
385
|
+
color = highBit ? HIRES_COLORS[4] : HIRES_COLORS[2];
|
|
386
|
+
} else {
|
|
387
|
+
color = highBit ? HIRES_COLORS[5] : HIRES_COLORS[1];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
int screenX = x * 2;
|
|
392
|
+
setPixel(screenX, screenY, color);
|
|
393
|
+
setPixel(screenX + 1, screenY, color);
|
|
394
|
+
setPixel(screenX, screenY + 1, color);
|
|
395
|
+
setPixel(screenX + 1, screenY + 1, color);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
void Video::renderDoubleLoResScanline(int scanline, int startCol, int endCol,
|
|
401
|
+
const VideoSwitchState &vs) {
|
|
402
|
+
int textRow = scanline / 8;
|
|
403
|
+
int lineInRow = scanline % 8;
|
|
404
|
+
if (textRow >= 24) return;
|
|
405
|
+
|
|
406
|
+
int screenY = scanline * 2;
|
|
407
|
+
|
|
408
|
+
for (int col = startCol; col < endCol; col++) {
|
|
409
|
+
uint16_t addr = getTextAddress(textRow, col);
|
|
410
|
+
|
|
411
|
+
uint8_t auxByte = mmu_.readRAM(addr, true);
|
|
412
|
+
uint8_t mainByte = mmu_.readRAM(addr, false);
|
|
413
|
+
|
|
414
|
+
uint8_t auxNibble = (lineInRow < 4) ? (auxByte & 0x0F)
|
|
415
|
+
: ((auxByte >> 4) & 0x0F);
|
|
416
|
+
uint8_t mainColor = (lineInRow < 4) ? (mainByte & 0x0F)
|
|
417
|
+
: ((mainByte >> 4) & 0x0F);
|
|
418
|
+
|
|
419
|
+
// Aux nibbles need 4-bit left rotation by 1 to compensate for
|
|
420
|
+
// half-color-clock phase shift in auxiliary video memory
|
|
421
|
+
uint8_t auxColor = ((auxNibble << 1) & 0x0F) | (auxNibble >> 3);
|
|
422
|
+
|
|
423
|
+
uint32_t auxRGB = monochrome_ ? getMonochromeColor(auxColor != 0) : LORES_COLORS[auxColor];
|
|
424
|
+
uint32_t mainRGB = monochrome_ ? getMonochromeColor(mainColor != 0) : LORES_COLORS[mainColor];
|
|
425
|
+
|
|
426
|
+
int screenX = col * 14;
|
|
427
|
+
|
|
428
|
+
// Aux pixels (left half, 7 pixels wide)
|
|
429
|
+
for (int px = 0; px < 7; px++) {
|
|
430
|
+
setPixel(screenX + px, screenY, auxRGB);
|
|
431
|
+
setPixel(screenX + px, screenY + 1, auxRGB);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Main pixels (right half, 7 pixels wide)
|
|
435
|
+
for (int px = 7; px < 14; px++) {
|
|
436
|
+
setPixel(screenX + px, screenY, mainRGB);
|
|
437
|
+
setPixel(screenX + px, screenY + 1, mainRGB);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
void Video::renderDoubleHiResScanline(int scanline, int startCol, int endCol,
|
|
443
|
+
const VideoSwitchState &vs) {
|
|
444
|
+
static const uint32_t DHGR_COLORS[16] = {
|
|
445
|
+
0xFF000000, 0xFFE31E60, 0xFF607203, 0xFFFF6A3C,
|
|
446
|
+
0xFF00A360, 0xFF9C9C9C, 0xFF14F53C, 0xFFD0DD8D,
|
|
447
|
+
0xFF604EBD, 0xFFFF44FD, 0xFF9C9C9C, 0xFFFFA0D0,
|
|
448
|
+
0xFF14CFFD, 0xFFD0C3FF, 0xFF72FFD0, 0xFFFFFFFF
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
if (scanline >= 192) return;
|
|
452
|
+
|
|
453
|
+
uint16_t pageOffset = (vs.page2 && !vs.store80) ? 0x2000 : 0;
|
|
454
|
+
|
|
455
|
+
// Read bytes for columns in our range (zero-init for edge handling)
|
|
456
|
+
uint8_t line[80] = {0};
|
|
457
|
+
for (int col = startCol; col < endCol; col++) {
|
|
458
|
+
uint16_t addr = getHiResAddress(scanline, col) + pageOffset;
|
|
459
|
+
line[col * 2] = mmu_.readRAM(addr, true);
|
|
460
|
+
line[col * 2 + 1] = mmu_.readRAM(addr, false);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Extract dots for our column range
|
|
464
|
+
uint8_t dots[564] = {0};
|
|
465
|
+
int dotStart = startCol * 14;
|
|
466
|
+
int dotEnd = std::min(endCol * 14, 560);
|
|
467
|
+
|
|
468
|
+
for (int i = dotStart; i < dotEnd; i++) {
|
|
469
|
+
int byteIdx = i / 7;
|
|
470
|
+
int bitIdx = i % 7;
|
|
471
|
+
dots[i] = (line[byteIdx] >> bitIdx) & 1;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
int screenY = scanline * 2;
|
|
475
|
+
|
|
476
|
+
if (monochrome_) {
|
|
477
|
+
for (int i = dotStart; i < dotEnd; i++) {
|
|
478
|
+
uint32_t color = getMonochromeColor(dots[i] != 0);
|
|
479
|
+
setPixel(i, screenY, color);
|
|
480
|
+
setPixel(i, screenY + 1, color);
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
for (int i = dotStart; i < dotEnd; i++) {
|
|
484
|
+
int alignedBase = (i / 4) * 4;
|
|
485
|
+
uint8_t colorIdx = (dots[alignedBase] << 3) |
|
|
486
|
+
(dots[alignedBase + 1] << 2) |
|
|
487
|
+
(dots[alignedBase + 2] << 1) |
|
|
488
|
+
dots[alignedBase + 3];
|
|
489
|
+
|
|
490
|
+
uint32_t color = DHGR_COLORS[colorIdx];
|
|
491
|
+
setPixel(i, screenY, color);
|
|
492
|
+
setPixel(i, screenY + 1, color);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ============================================================================
|
|
498
|
+
// Scanline segment dispatcher
|
|
499
|
+
// ============================================================================
|
|
500
|
+
|
|
501
|
+
void Video::renderScanlineSegment(int scanline, int startCol, int endCol,
|
|
502
|
+
const VideoSwitchState &vs) {
|
|
503
|
+
if (scanline >= 192 || startCol >= endCol) return;
|
|
504
|
+
|
|
505
|
+
// Mixed mode: scanlines 160-191 always render as text
|
|
506
|
+
if (vs.mixed && scanline >= 160 && !vs.text) {
|
|
507
|
+
if (vs.col80) {
|
|
508
|
+
renderText80Scanline(scanline, startCol, endCol, vs);
|
|
509
|
+
} else {
|
|
510
|
+
renderText40Scanline(scanline, startCol, endCol, vs);
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (vs.text) {
|
|
516
|
+
if (vs.col80) {
|
|
517
|
+
renderText80Scanline(scanline, startCol, endCol, vs);
|
|
518
|
+
} else {
|
|
519
|
+
renderText40Scanline(scanline, startCol, endCol, vs);
|
|
520
|
+
}
|
|
521
|
+
} else if (vs.hires) {
|
|
522
|
+
if (vs.col80 && !vs.an3) {
|
|
523
|
+
renderDoubleHiResScanline(scanline, startCol, endCol, vs);
|
|
524
|
+
} else {
|
|
525
|
+
renderHiResScanline(scanline, startCol, endCol, vs);
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
if (vs.col80 && !vs.an3) {
|
|
529
|
+
renderDoubleLoResScanline(scanline, startCol, endCol, vs);
|
|
530
|
+
} else {
|
|
531
|
+
renderLoResScanline(scanline, startCol, endCol, vs);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ============================================================================
|
|
537
|
+
// Progressive per-scanline rendering
|
|
538
|
+
// ============================================================================
|
|
539
|
+
|
|
540
|
+
void Video::renderScanlineWithChanges(int scanline) {
|
|
541
|
+
// Apple IIe horizontal timing: each 65-cycle scanline starts with
|
|
542
|
+
// 25 cycles of horizontal blanking, then 40 cycles of visible display.
|
|
543
|
+
static constexpr int HBLANK_CYCLES = 25;
|
|
544
|
+
|
|
545
|
+
uint32_t scanlineStartCycle = scanline * CYCLES_PER_SCANLINE;
|
|
546
|
+
uint32_t visibleStartCycle = scanlineStartCycle + HBLANK_CYCLES;
|
|
547
|
+
uint32_t scanlineEndCycle = scanlineStartCycle + CYCLES_PER_SCANLINE;
|
|
548
|
+
|
|
549
|
+
// Phase 1: Consume hblank changes (cycles 0-24) and any earlier changes
|
|
550
|
+
while (changeIdx_ < switchChangeCount_) {
|
|
551
|
+
uint32_t changeCycle = switchChanges_[changeIdx_].cycleOffset;
|
|
552
|
+
if (changeCycle >= visibleStartCycle) {
|
|
553
|
+
break; // Change is in visible area or a later scanline
|
|
554
|
+
}
|
|
555
|
+
currentRenderState_ = switchChanges_[changeIdx_].state;
|
|
556
|
+
changeIdx_++;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Phase 2: Process visible-area changes (cycles 25-64 → columns 0-39)
|
|
560
|
+
int col = 0;
|
|
561
|
+
while (changeIdx_ < switchChangeCount_) {
|
|
562
|
+
uint32_t changeCycle = switchChanges_[changeIdx_].cycleOffset;
|
|
563
|
+
if (changeCycle >= scanlineEndCycle) {
|
|
564
|
+
break; // Belongs to a later scanline
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
int changeCol = static_cast<int>(changeCycle - visibleStartCycle);
|
|
568
|
+
if (changeCol > 40) changeCol = 40;
|
|
569
|
+
|
|
570
|
+
if (changeCol > col) {
|
|
571
|
+
renderScanlineSegment(scanline, col, changeCol, currentRenderState_);
|
|
572
|
+
col = changeCol;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
currentRenderState_ = switchChanges_[changeIdx_].state;
|
|
576
|
+
changeIdx_++;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Render remaining visible columns
|
|
580
|
+
if (col < 40) {
|
|
581
|
+
renderScanlineSegment(scanline, col, 40, currentRenderState_);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
void Video::renderUpToCycle(uint64_t currentCycle) {
|
|
586
|
+
if (currentCycle <= frameStartCycle_) return;
|
|
587
|
+
|
|
588
|
+
uint64_t frameCycle = currentCycle - frameStartCycle_;
|
|
589
|
+
|
|
590
|
+
// Render scanlines whose 65 cycles are fully complete.
|
|
591
|
+
// frameCycle / CYCLES_PER_SCANLINE gives the number of complete scanlines,
|
|
592
|
+
// so targetScanline = completedScanlines - 1 is the last fully-elapsed one.
|
|
593
|
+
// This ensures all CPU writes during a scanline are captured before we
|
|
594
|
+
// read video memory for that scanline (critical for raster bar effects).
|
|
595
|
+
int completedScanlines = static_cast<int>(frameCycle / CYCLES_PER_SCANLINE);
|
|
596
|
+
int targetScanline = completedScanlines - 1;
|
|
597
|
+
if (targetScanline > 191) targetScanline = 191;
|
|
598
|
+
|
|
599
|
+
while (lastRenderedScanline_ < targetScanline) {
|
|
600
|
+
lastRenderedScanline_++;
|
|
601
|
+
renderScanlineWithChanges(lastRenderedScanline_);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ============================================================================
|
|
606
|
+
// Frame rendering
|
|
607
|
+
// ============================================================================
|
|
608
|
+
|
|
609
|
+
void Video::renderFrame() {
|
|
610
|
+
// Update flash state
|
|
611
|
+
flashCounter_++;
|
|
612
|
+
if (flashCounter_ >= FLASH_RATE) {
|
|
613
|
+
flashCounter_ = 0;
|
|
614
|
+
flashState_ = !flashState_;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Finish any remaining unrendered scanlines (progressive rendering
|
|
618
|
+
// may have already handled most of them during CPU execution)
|
|
619
|
+
while (lastRenderedScanline_ < 191) {
|
|
620
|
+
lastRenderedScanline_++;
|
|
621
|
+
renderScanlineWithChanges(lastRenderedScanline_);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
frameDirty_ = true;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
void Video::forceRenderFrame() {
|
|
628
|
+
VideoSwitchState vs = captureVideoState();
|
|
629
|
+
for (int scanline = 0; scanline < 192; scanline++) {
|
|
630
|
+
renderScanlineSegment(scanline, 0, 40, vs);
|
|
631
|
+
}
|
|
632
|
+
frameDirty_ = true;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ============================================================================
|
|
636
|
+
// Pixel and color helpers
|
|
637
|
+
// ============================================================================
|
|
638
|
+
|
|
639
|
+
void Video::setPixel(int x, int y, uint32_t color) {
|
|
640
|
+
if (x < 0 || x >= SCREEN_WIDTH || y < 0 || y >= SCREEN_HEIGHT) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
size_t offset = (y * SCREEN_WIDTH + x) * 4;
|
|
645
|
+
framebuffer_[offset + 0] = (color >> 16) & 0xFF; // R
|
|
646
|
+
framebuffer_[offset + 1] = (color >> 8) & 0xFF; // G
|
|
647
|
+
framebuffer_[offset + 2] = color & 0xFF; // B
|
|
648
|
+
framebuffer_[offset + 3] = (color >> 24) & 0xFF; // A
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
uint32_t Video::getLoResColor(uint8_t colorIndex) const {
|
|
652
|
+
if (monochrome_) {
|
|
653
|
+
return (colorIndex > 0) ? getMonochromeColor(true)
|
|
654
|
+
: getMonochromeColor(false);
|
|
655
|
+
}
|
|
656
|
+
return LORES_COLORS[colorIndex & 0x0F];
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
uint32_t Video::getMonochromeColor(bool on) const {
|
|
660
|
+
if (!on) {
|
|
661
|
+
return 0xFF000000; // Black
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (greenPhosphor_) {
|
|
665
|
+
return 0xFF33FF33; // Green phosphor
|
|
666
|
+
}
|
|
667
|
+
return 0xFFFFFFFF; // White
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
uint16_t Video::getTextAddress(int row, int col) const {
|
|
671
|
+
return 0x0400 + TEXT_ROW_OFFSETS[row] + col;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
uint16_t Video::getHiResAddress(int row, int col) const {
|
|
675
|
+
int block = row / 8;
|
|
676
|
+
int line = row % 8;
|
|
677
|
+
return 0x2000 + TEXT_ROW_OFFSETS[block] + line * 0x400 + col;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
} // namespace a2e
|