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,658 @@
|
|
|
1
|
+
# Video Rendering
|
|
2
|
+
|
|
3
|
+
This page describes the video rendering subsystem in detail, covering all six Apple IIe display modes, the per-scanline progressive rendering pipeline, NTSC artifact color generation, character ROM handling, screen address calculation, and the change-log based mid-frame mode-switching system.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Overview](#overview)
|
|
10
|
+
- [Framebuffer Layout](#framebuffer-layout)
|
|
11
|
+
- [Video Modes](#video-modes)
|
|
12
|
+
- [Mode Selection Logic](#mode-selection-logic)
|
|
13
|
+
- [Text 40-Column](#text-40-column)
|
|
14
|
+
- [Text 80-Column](#text-80-column)
|
|
15
|
+
- [Lo-Res Graphics](#lo-res-graphics)
|
|
16
|
+
- [Hi-Res Graphics](#hi-res-graphics)
|
|
17
|
+
- [Double Lo-Res Graphics](#double-lo-res-graphics)
|
|
18
|
+
- [Double Hi-Res Graphics](#double-hi-res-graphics)
|
|
19
|
+
- [Mixed Mode](#mixed-mode)
|
|
20
|
+
- [Color Palettes](#color-palettes)
|
|
21
|
+
- [Lo-Res Palette](#lo-res-palette)
|
|
22
|
+
- [Hi-Res Artifact Colors](#hi-res-artifact-colors)
|
|
23
|
+
- [Double Hi-Res Palette](#double-hi-res-palette)
|
|
24
|
+
- [Monochrome Rendering](#monochrome-rendering)
|
|
25
|
+
- [Character ROM](#character-rom)
|
|
26
|
+
- [Inverse and Flash](#inverse-and-flash)
|
|
27
|
+
- [Primary Character Set Mapping](#primary-character-set-mapping)
|
|
28
|
+
- [Alternate Character Set](#alternate-character-set)
|
|
29
|
+
- [UK Character Set](#uk-character-set)
|
|
30
|
+
- [Screen Address Calculation](#screen-address-calculation)
|
|
31
|
+
- [Text and Lo-Res Addresses](#text-and-lo-res-addresses)
|
|
32
|
+
- [Hi-Res Addresses](#hi-res-addresses)
|
|
33
|
+
- [Row Offset Table](#row-offset-table)
|
|
34
|
+
- [Per-Scanline Progressive Rendering](#per-scanline-progressive-rendering)
|
|
35
|
+
- [Horizontal Timing](#horizontal-timing)
|
|
36
|
+
- [Switch Change Log](#switch-change-log)
|
|
37
|
+
- [Video Pipeline Delay](#video-pipeline-delay)
|
|
38
|
+
- [Progressive Rendering Loop](#progressive-rendering-loop)
|
|
39
|
+
- [Scanline-With-Changes Rendering](#scanline-with-changes-rendering)
|
|
40
|
+
- [Scanline Segment Dispatch](#scanline-segment-dispatch)
|
|
41
|
+
- [Frame Lifecycle](#frame-lifecycle)
|
|
42
|
+
- [Frame Boundaries](#frame-boundaries)
|
|
43
|
+
- [Flash Counter](#flash-counter)
|
|
44
|
+
- [Force Render](#force-render)
|
|
45
|
+
- [Video Soft Switches](#video-soft-switches)
|
|
46
|
+
- [Display Pipeline](#display-pipeline)
|
|
47
|
+
- [WebGL Shader Pipeline](#webgl-shader-pipeline)
|
|
48
|
+
- [Display Settings Integration](#display-settings-integration)
|
|
49
|
+
- [See Also](#see-also)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Overview
|
|
54
|
+
|
|
55
|
+
The `Video` class (`src/core/video/video.cpp`) renders the Apple IIe display into a 560x384 RGBA framebuffer. Rendering is progressive: as the CPU executes instructions, completed scanlines are rendered on-the-fly by `renderUpToCycle()`. At the end of each frame, `renderFrame()` finishes any remaining scanlines.
|
|
56
|
+
|
|
57
|
+
The renderer supports all six Apple IIe video modes:
|
|
58
|
+
|
|
59
|
+
| Mode | Resolution | Colors | Memory Used |
|
|
60
|
+
|------|-----------|--------|-------------|
|
|
61
|
+
| TEXT_40 | 40x24 characters | 2 (FG/BG) | $0400-$07FF main |
|
|
62
|
+
| TEXT_80 | 80x24 characters | 2 (FG/BG) | $0400-$07FF main + aux |
|
|
63
|
+
| LORES | 40x48 color blocks | 16 | $0400-$07FF main |
|
|
64
|
+
| HIRES | 280x192 pixels | 6 (artifact) | $2000-$3FFF main |
|
|
65
|
+
| DOUBLE_LORES | 80x48 color blocks | 16 | $0400-$07FF main + aux |
|
|
66
|
+
| DOUBLE_HIRES | 560x192 pixels | 16 | $2000-$3FFF main + aux |
|
|
67
|
+
|
|
68
|
+
All rendering is performed in C++ and exposed to JavaScript through the WASM framebuffer pointer. The JavaScript layer then uploads the framebuffer as a WebGL texture and applies CRT shader effects.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Framebuffer Layout
|
|
73
|
+
|
|
74
|
+
The framebuffer is a flat RGBA byte array stored in `std::array<uint8_t, FRAMEBUFFER_SIZE>`:
|
|
75
|
+
|
|
76
|
+
| Constant | Value | Description |
|
|
77
|
+
|----------|-------|-------------|
|
|
78
|
+
| `SCREEN_WIDTH` | 560 | Pixels per row (280 x 2) |
|
|
79
|
+
| `SCREEN_HEIGHT` | 384 | Pixel rows (192 x 2) |
|
|
80
|
+
| `FRAMEBUFFER_SIZE` | 860,160 | Total bytes (560 x 384 x 4) |
|
|
81
|
+
|
|
82
|
+
Each Apple II pixel is doubled in both dimensions. A single Apple II dot at position (x, y) maps to a 2x2 block in the framebuffer. The `setPixel()` helper writes individual framebuffer pixels:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
offset = (y * SCREEN_WIDTH + x) * 4
|
|
86
|
+
framebuffer[offset + 0] = R (bits 16-23 of color)
|
|
87
|
+
framebuffer[offset + 1] = G (bits 8-15 of color)
|
|
88
|
+
framebuffer[offset + 2] = B (bits 0-7 of color)
|
|
89
|
+
framebuffer[offset + 3] = A (bits 24-31 of color)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Colors are stored internally as packed 32-bit `0xAARRGGBB` values and unpacked into RGBA component order at write time.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Video Modes
|
|
97
|
+
|
|
98
|
+
### Mode Selection Logic
|
|
99
|
+
|
|
100
|
+
The current video mode is determined by `getCurrentMode()` examining soft switch state:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
if TEXT -> col80 ? TEXT_80 : TEXT_40
|
|
104
|
+
if HIRES -> (col80 && !AN3) ? DOUBLE_HIRES : HIRES
|
|
105
|
+
else (LORES) -> (col80 && !AN3) ? DOUBLE_LORES : LORES
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The key switches involved:
|
|
109
|
+
|
|
110
|
+
| Switch | Address | Effect |
|
|
111
|
+
|--------|---------|--------|
|
|
112
|
+
| TEXT | $C050/$C051 | Graphics / Text mode |
|
|
113
|
+
| MIXED | $C052/$C053 | Full screen / Mixed (4 text lines at bottom) |
|
|
114
|
+
| PAGE2 | $C054/$C055 | Display page 1 / page 2 |
|
|
115
|
+
| HIRES | $C056/$C057 | Lo-Res / Hi-Res |
|
|
116
|
+
| 80COL | $C00C/$C00D | 40-column / 80-column |
|
|
117
|
+
| AN3 | $C05E/$C05F | Annunciator 3 (enables double-width modes when OFF) |
|
|
118
|
+
| 80STORE | $C000/$C001 | Redirects PAGE2 to aux memory bank switching |
|
|
119
|
+
|
|
120
|
+
Double-resolution modes (DOUBLE_LORES, DOUBLE_HIRES) require both `col80` on **and** `an3` off.
|
|
121
|
+
|
|
122
|
+
### Text 40-Column
|
|
123
|
+
|
|
124
|
+
Renders 40 columns by 24 rows of characters from the 40-column text page. Each character is 7 ROM pixels wide, doubled to 14 framebuffer pixels. Characters are 8 scanlines tall, doubled to 16 framebuffer rows.
|
|
125
|
+
|
|
126
|
+
**Memory layout**: Text page 1 at `$0400-$07FF`, page 2 at `$0800-$0BFF`. Page selection respects the `80STORE` switch -- when `80STORE` is on, `PAGE2` is ignored for page selection (it is used for auxiliary memory bank switching instead).
|
|
127
|
+
|
|
128
|
+
**Rendering path** (`renderText40Scanline`):
|
|
129
|
+
1. Compute `textRow` (0-23) and `charLine` (0-7) from the scanline number (`scanline / 8` and `scanline % 8`)
|
|
130
|
+
2. For each column (0-39), read the character byte from the text page address
|
|
131
|
+
3. Determine inverse/flash status from the top two bits of the character byte
|
|
132
|
+
4. Look up the character glyph row in the character ROM via `getCharROMInfo()` and `renderCharacterLine()`
|
|
133
|
+
5. Render 7 dots, each doubled to 2 framebuffer pixels horizontally, and output on 2 framebuffer rows vertically
|
|
134
|
+
|
|
135
|
+
### Text 80-Column
|
|
136
|
+
|
|
137
|
+
Renders 80 columns by 24 rows using interleaved main and auxiliary memory. Each character is 7 pixels wide (no horizontal doubling), totaling 560 pixels across.
|
|
138
|
+
|
|
139
|
+
**Memory interleaving**: For each memory column position (0-39), the auxiliary byte provides the even (left) display column and the main byte provides the odd (right) display column. Both are read from the same text page address but from different memory banks.
|
|
140
|
+
|
|
141
|
+
**Rendering path** (`renderText80Scanline`):
|
|
142
|
+
1. For each memory column (0-39), read the aux byte and main byte at the same text page address
|
|
143
|
+
2. Aux character renders at display column `col * 2` (even position, 7 pixels)
|
|
144
|
+
3. Main character renders at display column `col * 2 + 1` (odd position, 7 pixels)
|
|
145
|
+
4. Each character is 7 framebuffer pixels wide -- no horizontal doubling is needed since 80 characters x 7 pixels = 560
|
|
146
|
+
|
|
147
|
+
### Lo-Res Graphics
|
|
148
|
+
|
|
149
|
+
Renders 40x48 color blocks using text page memory. Each byte encodes two vertically stacked 4-bit color indices.
|
|
150
|
+
|
|
151
|
+
**Color block structure**: Each byte in the text page represents two blocks stacked vertically within an 8-scanline text row. The low nibble (bits 0-3) is the top block color (scanlines 0-3 within the row) and the high nibble (bits 4-7) is the bottom block color (scanlines 4-7).
|
|
152
|
+
|
|
153
|
+
**Rendering path** (`renderLoResScanline`):
|
|
154
|
+
1. Compute `textRow` (0-23) and `lineInRow` (0-7) from the scanline
|
|
155
|
+
2. Select low nibble if `lineInRow < 4`, high nibble otherwise
|
|
156
|
+
3. Look up the 16-color palette via `getLoResColor()`
|
|
157
|
+
4. Fill a 14x2 framebuffer pixel block (7 Apple II dots doubled to 14 pixels wide, scanline doubled to 2 rows)
|
|
158
|
+
|
|
159
|
+
### Hi-Res Graphics
|
|
160
|
+
|
|
161
|
+
Renders 280x192 pixels with NTSC artifact color. Each byte encodes 7 dots plus a high bit that shifts the color phase.
|
|
162
|
+
|
|
163
|
+
**Memory layout**: Hi-Res page 1 at `$2000-$3FFF`, page 2 at `$4000-$5FFF`. The address mapping is interleaved -- see [Screen Address Calculation](#screen-address-calculation).
|
|
164
|
+
|
|
165
|
+
**Byte format**:
|
|
166
|
+
|
|
167
|
+
| Bit | Purpose |
|
|
168
|
+
|-----|---------|
|
|
169
|
+
| 0-6 | 7 pixel dots (bit 0 = leftmost) |
|
|
170
|
+
| 7 | High bit (shifts NTSC color phase by half a color clock) |
|
|
171
|
+
|
|
172
|
+
**Rendering path** (`renderHiResScanline`):
|
|
173
|
+
1. Build a 280-element `dots[]` array and a corresponding `highBits[]` array for the visible column range
|
|
174
|
+
2. For monochrome mode, each dot maps directly to on/off color in a 2x2 pixel block
|
|
175
|
+
3. For color mode, each dot's color is determined by NTSC artifact rules:
|
|
176
|
+
|
|
177
|
+
**NTSC artifact color rules** (evaluated left to right for each dot):
|
|
178
|
+
|
|
179
|
+
| Condition | Color |
|
|
180
|
+
|-----------|-------|
|
|
181
|
+
| Dot off, flanked by two single on-dots with matching high bits | Artifact color fill (fringing) |
|
|
182
|
+
| Dot off otherwise | Black |
|
|
183
|
+
| Dot on with adjacent on-dot (left or right) | White |
|
|
184
|
+
| Single dot on, even column, high bit 0 | Violet |
|
|
185
|
+
| Single dot on, odd column, high bit 0 | Green |
|
|
186
|
+
| Single dot on, even column, high bit 1 | Blue |
|
|
187
|
+
| Single dot on, odd column, high bit 1 | Orange |
|
|
188
|
+
|
|
189
|
+
The artifact fringing case handles the situation where a dot between two isolated on-dots takes on the neighboring dot's color, simulating how NTSC chroma bleeds across adjacent pixels on real hardware.
|
|
190
|
+
|
|
191
|
+
### Double Lo-Res Graphics
|
|
192
|
+
|
|
193
|
+
Renders 80x48 color blocks using interleaved main and auxiliary text page memory. Each memory column produces two display columns (aux on the left, main on the right), each 7 framebuffer pixels wide.
|
|
194
|
+
|
|
195
|
+
**Aux nibble rotation**: Auxiliary memory nibbles require a 1-bit left rotation within 4 bits to compensate for the half-color-clock phase shift between auxiliary and main video memory timing:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
auxColor = ((auxNibble << 1) & 0x0F) | (auxNibble >> 3)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Without this rotation, auxiliary colors would display with incorrect NTSC phase, producing the wrong colors on screen.
|
|
202
|
+
|
|
203
|
+
**Rendering path** (`renderDoubleLoResScanline`):
|
|
204
|
+
1. Read aux and main bytes at the same text page address
|
|
205
|
+
2. Extract the appropriate nibble (low nibble if `lineInRow < 4`, high nibble otherwise)
|
|
206
|
+
3. Rotate the aux nibble by 1 bit left within 4 bits
|
|
207
|
+
4. Render aux color in the left 7 framebuffer pixels, main color in the right 7 pixels
|
|
208
|
+
5. Uses the `LORES_COLORS` palette for both halves
|
|
209
|
+
|
|
210
|
+
### Double Hi-Res Graphics
|
|
211
|
+
|
|
212
|
+
Renders 560x192 pixels using interleaved main and auxiliary Hi-Res memory. Each pair of bytes (aux + main) provides 14 dots that map to four 4-bit color values.
|
|
213
|
+
|
|
214
|
+
**Memory interleaving**: For each column address, the aux byte comes first (providing 7 dots), followed by the main byte (7 more dots), giving 14 dots per column pair. This produces 80 bytes per scanline (40 aux + 40 main).
|
|
215
|
+
|
|
216
|
+
**Byte layout**:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
Byte 0 (aux col 0): 7 dots -> framebuffer dots 0-6
|
|
220
|
+
Byte 1 (main col 0): 7 dots -> framebuffer dots 7-13
|
|
221
|
+
Byte 2 (aux col 1): 7 dots -> framebuffer dots 14-20
|
|
222
|
+
Byte 3 (main col 1): 7 dots -> framebuffer dots 21-27
|
|
223
|
+
...
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Color encoding**: Every group of 4 consecutive dots forms a 4-bit color index into the 16-color Double Hi-Res palette. The bit ordering within each group is:
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
colorIndex = (dot[base] << 3) | (dot[base+1] << 2) | (dot[base+2] << 1) | dot[base+3]
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The 4-dot groups are aligned to absolute dot positions (`(i / 4) * 4`), meaning all dots within a group share the same color. Since 560 dots / 4 dots-per-group = 140 color cells, Double Hi-Res provides an effective 140x192 pixel color display at 16 colors.
|
|
233
|
+
|
|
234
|
+
**Rendering path** (`renderDoubleHiResScanline`):
|
|
235
|
+
1. Read all 80 bytes for the scanline into a linear `line[]` array (interleaved aux/main)
|
|
236
|
+
2. Extract 560 individual dots from the 7 data bits of each byte
|
|
237
|
+
3. For each dot, compute its aligned 4-dot group base and derive the 4-bit color index
|
|
238
|
+
4. Look up the color in the `DHGR_COLORS` palette
|
|
239
|
+
5. Each dot maps to a single framebuffer pixel horizontally, doubled vertically to 2 rows
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Mixed Mode
|
|
244
|
+
|
|
245
|
+
When the MIXED switch is on (`$C053`), the bottom 4 text rows (scanlines 160-191) always render as text, regardless of the current graphics mode. The upper portion (scanlines 0-159) renders in the selected graphics mode.
|
|
246
|
+
|
|
247
|
+
The dispatch logic in `renderScanlineSegment()` checks:
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
if (mixed && scanline >= 160 && !text) {
|
|
251
|
+
render as text (40-col or 80-col based on col80 switch)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Mixed mode works with all four graphics modes: LORES, HIRES, DOUBLE_LORES, and DOUBLE_HIRES all show text in the bottom 32 scanlines when mixed mode is active.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Color Palettes
|
|
261
|
+
|
|
262
|
+
### Lo-Res Palette
|
|
263
|
+
|
|
264
|
+
The 16-color Lo-Res palette (`LORES_COLORS` in `types.hpp`) provides NTSC-accurate colors:
|
|
265
|
+
|
|
266
|
+
| Index | Name | RGB |
|
|
267
|
+
|-------|------|-----|
|
|
268
|
+
| 0 | Black | `#000000` |
|
|
269
|
+
| 1 | Magenta | `#E31E60` |
|
|
270
|
+
| 2 | Dark Blue | `#604EBD` |
|
|
271
|
+
| 3 | Purple | `#FF44FD` |
|
|
272
|
+
| 4 | Dark Green | `#00A360` |
|
|
273
|
+
| 5 | Grey 1 | `#9C9C9C` |
|
|
274
|
+
| 6 | Medium Blue | `#14CFFD` |
|
|
275
|
+
| 7 | Light Blue | `#D0C3FF` |
|
|
276
|
+
| 8 | Brown | `#607203` |
|
|
277
|
+
| 9 | Orange | `#FF6A3C` |
|
|
278
|
+
| 10 | Grey 2 | `#9C9C9C` |
|
|
279
|
+
| 11 | Pink | `#FFA0D0` |
|
|
280
|
+
| 12 | Light Green | `#14F53C` |
|
|
281
|
+
| 13 | Yellow | `#D0DD8D` |
|
|
282
|
+
| 14 | Aqua | `#72FFD0` |
|
|
283
|
+
| 15 | White | `#FFFFFF` |
|
|
284
|
+
|
|
285
|
+
Indices 5 and 10 are both grey but appear at different positions in the NTSC chroma cycle. This palette is also used for Double Lo-Res mode.
|
|
286
|
+
|
|
287
|
+
### Hi-Res Artifact Colors
|
|
288
|
+
|
|
289
|
+
Hi-Res mode uses a 6-entry color table (`HIRES_COLORS` in `types.hpp`) that models NTSC artifact coloring:
|
|
290
|
+
|
|
291
|
+
| Index | Name | Usage |
|
|
292
|
+
|-------|------|-------|
|
|
293
|
+
| 0 | Black | Off dots |
|
|
294
|
+
| 1 | Green (`#14F53C`) | Odd pixels, high bit = 0 |
|
|
295
|
+
| 2 | Violet (`#FF44FD`) | Even pixels, high bit = 0 |
|
|
296
|
+
| 3 | White (`#FFFFFF`) | Adjacent on-dots |
|
|
297
|
+
| 4 | Blue (`#14CFFD`) | Even pixels, high bit = 1 |
|
|
298
|
+
| 5 | Orange (`#FF6A3C`) | Odd pixels, high bit = 1 |
|
|
299
|
+
|
|
300
|
+
The high bit (bit 7) of each byte selects between color group 1 (Violet/Green) and color group 2 (Blue/Orange). Two adjacent on-dots always produce white regardless of group.
|
|
301
|
+
|
|
302
|
+
### Double Hi-Res Palette
|
|
303
|
+
|
|
304
|
+
Double Hi-Res uses its own 16-color palette (`DHGR_COLORS`, defined locally in `renderDoubleHiResScanline()`). The ordering differs from Lo-Res due to the 14 MHz dot rate versus 7 MHz, which changes the NTSC phase relationship:
|
|
305
|
+
|
|
306
|
+
| Index | Color | Lo-Res Equivalent |
|
|
307
|
+
|-------|-------|-------------------|
|
|
308
|
+
| 0 | Black | LORES[0] |
|
|
309
|
+
| 1 | Magenta | LORES[1] |
|
|
310
|
+
| 2 | Brown | LORES[8] |
|
|
311
|
+
| 3 | Orange | LORES[9] |
|
|
312
|
+
| 4 | Dark Green | LORES[4] |
|
|
313
|
+
| 5 | Grey | LORES[5] |
|
|
314
|
+
| 6 | Light Green | LORES[12] |
|
|
315
|
+
| 7 | Yellow | LORES[13] |
|
|
316
|
+
| 8 | Dark Blue | LORES[2] |
|
|
317
|
+
| 9 | Purple | LORES[3] |
|
|
318
|
+
| 10 | Grey | LORES[10] |
|
|
319
|
+
| 11 | Light Blue | LORES[7] |
|
|
320
|
+
| 12 | Medium Blue | LORES[6] |
|
|
321
|
+
| 13 | Pink | LORES[11] |
|
|
322
|
+
| 14 | Aqua | LORES[14] |
|
|
323
|
+
| 15 | White | LORES[15] |
|
|
324
|
+
|
|
325
|
+
### Monochrome Rendering
|
|
326
|
+
|
|
327
|
+
All modes support monochrome rendering via `setMonochrome(true)`. When enabled, `getMonochromeColor()` returns:
|
|
328
|
+
- **On pixels**: White (`0xFFFFFFFF`) or green phosphor (`0xFF33FF33`) if `greenPhosphor_` is set
|
|
329
|
+
- **Off pixels**: Black (`0xFF000000`)
|
|
330
|
+
|
|
331
|
+
Lo-Res monochrome treats any non-zero color index as "on." Hi-Res and Double Hi-Res monochrome render individual dots as on/off.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Character ROM
|
|
336
|
+
|
|
337
|
+
The character ROM is 8 KB total, containing two 4 KB character sets (US and UK). Each character is 8 bytes (one byte per scanline row), with each byte encoding 7 dots. Bit 0 is the leftmost pixel.
|
|
338
|
+
|
|
339
|
+
### Inverse and Flash
|
|
340
|
+
|
|
341
|
+
Character display attributes are determined by the top two bits of the character byte stored in text page memory:
|
|
342
|
+
|
|
343
|
+
| Bits 7-6 | Byte Range | Display |
|
|
344
|
+
|----------|------------|---------|
|
|
345
|
+
| `00` | $00-$3F | Inverse (foreground and background swapped) |
|
|
346
|
+
| `01` | $40-$7F | Flash (toggles between normal and inverse at ~3.75 Hz) |
|
|
347
|
+
| `10`-`11` | $80-$FF | Normal |
|
|
348
|
+
|
|
349
|
+
The `getCharROMInfo()` helper method decodes the character byte and returns a `CharROMInfo` struct containing the ROM offset, whether XOR bit-flipping is needed, and the final inverse state.
|
|
350
|
+
|
|
351
|
+
### Primary Character Set Mapping
|
|
352
|
+
|
|
353
|
+
With the primary character set active (ALTCHARSET off), the character ROM index is derived as follows:
|
|
354
|
+
|
|
355
|
+
| Byte Range | ROM Index | Display Style |
|
|
356
|
+
|------------|-----------|---------------|
|
|
357
|
+
| $00-$1F | `ch` (0-31) | Inverse |
|
|
358
|
+
| $20-$3F | `ch` (32-63) | Inverse |
|
|
359
|
+
| $40-$5F | `ch & 0x1F` (0-31) | Flash |
|
|
360
|
+
| $60-$7F | `(ch & 0x1F) + 32` (32-63) | Flash |
|
|
361
|
+
| $80-$9F | `ch & 0x1F` (0-31) | Normal |
|
|
362
|
+
| $A0-$BF | `(ch & 0x1F) + 32` (32-63) | Normal |
|
|
363
|
+
| $C0-$DF | `ch & 0x1F` (0-31) | Normal |
|
|
364
|
+
| $E0-$FF | `(ch & 0x1F) + 96` (96-127) | Normal |
|
|
365
|
+
|
|
366
|
+
The ROM offset for each character is `charIndex * 8`.
|
|
367
|
+
|
|
368
|
+
### Alternate Character Set
|
|
369
|
+
|
|
370
|
+
When the ALTCHARSET switch is on (`$C00F`), the mapping changes significantly to provide MouseText characters:
|
|
371
|
+
|
|
372
|
+
| Byte Range | ROM Index | Rendering |
|
|
373
|
+
|------------|-----------|-----------|
|
|
374
|
+
| $00-$3F | `ch` | Inverse (no XOR) |
|
|
375
|
+
| $40-$5F | `ch` | XOR rendering, not inverse (MouseText) |
|
|
376
|
+
| $60-$7F | `ch` | XOR rendering (MouseText) |
|
|
377
|
+
| $80-$FF | Mapped via ranges | Normal (no XOR, no inverse) |
|
|
378
|
+
|
|
379
|
+
In the alternate set, flash behavior is disabled. Characters in the `$40-$7F` range display MouseText glyphs instead of flashing. The XOR flag means the ROM data bits are inverted (`rowData ^= 0xFF`) before rendering.
|
|
380
|
+
|
|
381
|
+
### UK Character Set
|
|
382
|
+
|
|
383
|
+
Setting `ukCharSet_` to true adds a `0x1000` (4096) byte offset to the character ROM address, selecting the second 4 KB half of the ROM. This models the physical character set switch on UK Apple IIe machines, which replaces certain ASCII characters (notably `#` with the pound sterling symbol).
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Screen Address Calculation
|
|
388
|
+
|
|
389
|
+
### Text and Lo-Res Addresses
|
|
390
|
+
|
|
391
|
+
Text and Lo-Res modes share the same address calculation. The base address for page 1 is `$0400`:
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
address = $0400 + TEXT_ROW_OFFSETS[row] + col
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Page 2 adds `$0400` to the address (giving base `$0800`). For 80-column and Double Lo-Res modes, both main and auxiliary memory are read at the same address.
|
|
398
|
+
|
|
399
|
+
### Hi-Res Addresses
|
|
400
|
+
|
|
401
|
+
Hi-Res addresses are computed from three components:
|
|
402
|
+
|
|
403
|
+
```
|
|
404
|
+
block = row / 8 (which 8-line group: 0-23)
|
|
405
|
+
line = row % 8 (line within the group: 0-7)
|
|
406
|
+
address = $2000 + TEXT_ROW_OFFSETS[block] + line * $400 + col
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
This interleaved layout means consecutive scanlines are **not** at consecutive memory addresses. Lines within an 8-line group are spaced `$400` (1024 bytes) apart. Page 2 uses `$4000` as the base instead of `$2000`.
|
|
410
|
+
|
|
411
|
+
### Row Offset Table
|
|
412
|
+
|
|
413
|
+
The `TEXT_ROW_OFFSETS` lookup table provides the base offset for each of the 24 text rows (or 8-line blocks in Hi-Res mode):
|
|
414
|
+
|
|
415
|
+
| Rows 0-7 | Rows 8-15 | Rows 16-23 |
|
|
416
|
+
|----------|-----------|------------|
|
|
417
|
+
| `$000` | `$028` | `$050` |
|
|
418
|
+
| `$080` | `$0A8` | `$0D0` |
|
|
419
|
+
| `$100` | `$128` | `$150` |
|
|
420
|
+
| `$180` | `$1A8` | `$1D0` |
|
|
421
|
+
| `$200` | `$228` | `$250` |
|
|
422
|
+
| `$280` | `$2A8` | `$2D0` |
|
|
423
|
+
| `$300` | `$328` | `$350` |
|
|
424
|
+
| `$380` | `$3A8` | `$3D0` |
|
|
425
|
+
|
|
426
|
+
This non-linear layout is a consequence of the original Apple II hardware design, which used a simple counter with specific bit-wiring to generate addresses for both the text and Hi-Res display circuitry.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Per-Scanline Progressive Rendering
|
|
431
|
+
|
|
432
|
+
### Horizontal Timing
|
|
433
|
+
|
|
434
|
+
Each of the 262 scanlines per frame takes exactly 65 CPU cycles:
|
|
435
|
+
|
|
436
|
+
| Cycles | Purpose |
|
|
437
|
+
|--------|---------|
|
|
438
|
+
| 0-24 | Horizontal blanking (25 cycles) |
|
|
439
|
+
| 25-64 | Visible display (40 cycles, one byte column per cycle) |
|
|
440
|
+
|
|
441
|
+
Only the first 192 scanlines contain visible display data. Scanlines 192-261 are vertical blanking. The constants are defined in `types.hpp`:
|
|
442
|
+
|
|
443
|
+
| Constant | Value |
|
|
444
|
+
|----------|-------|
|
|
445
|
+
| `CYCLES_PER_SCANLINE` | 65 |
|
|
446
|
+
| `SCANLINES_PER_FRAME` | 262 |
|
|
447
|
+
| `CYCLES_PER_FRAME` | 17,030 |
|
|
448
|
+
|
|
449
|
+
### Switch Change Log
|
|
450
|
+
|
|
451
|
+
The `Video` class maintains a change log of video switch modifications during each frame. When the MMU detects a write to a video-relevant soft switch, it calls `onVideoSwitchChanged()`, which:
|
|
452
|
+
|
|
453
|
+
1. Captures the full `VideoSwitchState` from the MMU's current soft switch state
|
|
454
|
+
2. Compares against the last logged state (or `frameStartState_` if the log is empty) to avoid redundant entries
|
|
455
|
+
3. Records the state snapshot along with its cycle offset from the frame start
|
|
456
|
+
4. Stores up to `MAX_SWITCH_CHANGES` (1024) entries per frame; excess changes are silently dropped
|
|
457
|
+
|
|
458
|
+
The `VideoSwitchState` struct captures exactly the 8 switches that affect rendering:
|
|
459
|
+
|
|
460
|
+
| Field | Switch | Purpose |
|
|
461
|
+
|-------|--------|---------|
|
|
462
|
+
| `text` | TEXT | Text vs. graphics mode |
|
|
463
|
+
| `mixed` | MIXED | Bottom 4 rows forced to text |
|
|
464
|
+
| `page2` | PAGE2 | Display page selection |
|
|
465
|
+
| `hires` | HIRES | Hi-Res vs. Lo-Res |
|
|
466
|
+
| `col80` | 80COL | 80-column / double-width enable |
|
|
467
|
+
| `altCharSet` | ALTCHARSET | Alternate character set (MouseText) |
|
|
468
|
+
| `store80` | 80STORE | PAGE2 redirected to bank switch |
|
|
469
|
+
| `an3` | AN3 | Double-resolution enable |
|
|
470
|
+
|
|
471
|
+
### Video Pipeline Delay
|
|
472
|
+
|
|
473
|
+
When logging a switch change, the cycle offset includes a **+2 cycle adjustment** to model the Apple IIe's two-stage video pipeline:
|
|
474
|
+
|
|
475
|
+
1. **Phi-0/Phi-1 bus phasing**: The video hardware fetches memory on Phi-0 (first half of each clock cycle) before the CPU writes on Phi-1 (second half). A soft switch change on cycle N misses that cycle's video fetch.
|
|
476
|
+
|
|
477
|
+
2. **Shift register latching**: The byte fetched on Phi-0 of cycle N+1 is loaded into the shift register and does not produce visible dots until approximately cycle N+2.
|
|
478
|
+
|
|
479
|
+
Combined: a CPU write on cycle N affects display output at cycle N+2. This delay is essential for accurate emulation of demo effects that rely on precise switch timing.
|
|
480
|
+
|
|
481
|
+
### Progressive Rendering Loop
|
|
482
|
+
|
|
483
|
+
`renderUpToCycle(currentCycle)` is called after each CPU instruction executes. It renders all scanlines whose 65 cycles are fully complete:
|
|
484
|
+
|
|
485
|
+
1. Calculate `completedScanlines = frameCycle / CYCLES_PER_SCANLINE`
|
|
486
|
+
2. Target the last fully-elapsed scanline (`completedScanlines - 1`), capping at 191
|
|
487
|
+
3. Render each unrendered scanline sequentially via `renderScanlineWithChanges()`
|
|
488
|
+
4. Track progress with `lastRenderedScanline_` (initialized to -1 at frame start)
|
|
489
|
+
|
|
490
|
+
Rendering one scanline behind the current execution point ensures all CPU writes during a scanline are captured before video memory is read for that scanline. This is critical for raster bar effects and other mid-frame tricks that modify video memory or switches just before the beam reaches them.
|
|
491
|
+
|
|
492
|
+
### Scanline-With-Changes Rendering
|
|
493
|
+
|
|
494
|
+
`renderScanlineWithChanges()` processes a single scanline in two phases using the switch change log:
|
|
495
|
+
|
|
496
|
+
**Phase 1 -- HBLANK changes** (cycles 0-24): Consume all switch changes occurring during horizontal blanking by advancing `changeIdx_` through the log and updating `currentRenderState_`. No pixels are rendered during hblank.
|
|
497
|
+
|
|
498
|
+
**Phase 2 -- Visible area** (cycles 25-64, mapped to columns 0-39): Walk the remaining changes within the visible area. Each change splits the scanline into segments:
|
|
499
|
+
|
|
500
|
+
```
|
|
501
|
+
col = 0
|
|
502
|
+
For each change in the visible area:
|
|
503
|
+
changeCol = changeCycle - visibleStartCycle
|
|
504
|
+
Render columns [col, changeCol) with currentRenderState_
|
|
505
|
+
Update currentRenderState_ from the change log entry
|
|
506
|
+
col = changeCol
|
|
507
|
+
Render remaining columns [col, 40) with final currentRenderState_
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
This segment-based approach allows mid-scanline mode switches -- a program can change from text to Hi-Res partway across a scanline, and both modes will render correctly on their respective sides of the switch point.
|
|
511
|
+
|
|
512
|
+
### Scanline Segment Dispatch
|
|
513
|
+
|
|
514
|
+
`renderScanlineSegment()` dispatches a column range `[startCol, endCol)` on a single scanline to the correct mode-specific renderer based on the current `VideoSwitchState`:
|
|
515
|
+
|
|
516
|
+
1. If `scanline >= 192` or empty range, return immediately
|
|
517
|
+
2. If `mixed` and `scanline >= 160` and not in text mode, render as text (40-col or 80-col)
|
|
518
|
+
3. If `text`, dispatch to `renderText40Scanline()` or `renderText80Scanline()`
|
|
519
|
+
4. If `hires`, dispatch to `renderDoubleHiResScanline()` (if `col80 && !an3`) or `renderHiResScanline()`
|
|
520
|
+
5. Otherwise dispatch to `renderDoubleLoResScanline()` (if `col80 && !an3`) or `renderLoResScanline()`
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Frame Lifecycle
|
|
525
|
+
|
|
526
|
+
### Frame Boundaries
|
|
527
|
+
|
|
528
|
+
Frame boundaries are managed by the `Emulator` class (see [[Architecture-Overview]]). At each frame boundary:
|
|
529
|
+
|
|
530
|
+
1. **`renderFrame()`** is called to finalize the current frame:
|
|
531
|
+
- Increment the flash counter and toggle flash state if threshold reached
|
|
532
|
+
- Render any remaining unrendered scanlines (progressive rendering usually handles most by this point)
|
|
533
|
+
- Set `frameDirty_ = true` to signal that the framebuffer is ready for upload
|
|
534
|
+
|
|
535
|
+
2. **`beginNewFrame(cycleStart)`** resets state for the next frame:
|
|
536
|
+
- Save the frame start cycle (`frameStartCycle_`)
|
|
537
|
+
- Capture the initial video switch state (`frameStartState_`)
|
|
538
|
+
- Clear the switch change log (`switchChangeCount_ = 0`)
|
|
539
|
+
- Reset `lastRenderedScanline_` to -1
|
|
540
|
+
- Reset `changeIdx_` to 0
|
|
541
|
+
- Set `currentRenderState_` to the frame start state
|
|
542
|
+
|
|
543
|
+
### Flash Counter
|
|
544
|
+
|
|
545
|
+
Text characters in the flash range ($40-$7F in the primary character set) toggle between normal and inverse display. The flash state toggles every `FLASH_RATE` (16) frames, producing approximately 3.75 Hz blinking at 60 fps.
|
|
546
|
+
|
|
547
|
+
Flash only applies when using the primary character set. The alternate character set disables flash and displays MouseText glyphs in the $40-$7F range instead.
|
|
548
|
+
|
|
549
|
+
### Force Render
|
|
550
|
+
|
|
551
|
+
`forceRenderFrame()` re-renders the entire screen from current memory state in a single pass, ignoring the progressive rendering pipeline and change log. It captures the current video switch state and renders all 192 scanlines with that uniform state. This is used by the debugger for screen refresh after stepping instructions, where the progressive rendering state may be stale.
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## Video Soft Switches
|
|
556
|
+
|
|
557
|
+
Video-related soft switches and their addresses:
|
|
558
|
+
|
|
559
|
+
| Address | Function | Type |
|
|
560
|
+
|---------|----------|------|
|
|
561
|
+
| $C050 | Graphics mode | Write |
|
|
562
|
+
| $C051 | Text mode | Write |
|
|
563
|
+
| $C052 | Full-screen (no mixed) | Write |
|
|
564
|
+
| $C053 | Mixed mode (4 text lines at bottom) | Write |
|
|
565
|
+
| $C054 | Display page 1 | Write |
|
|
566
|
+
| $C055 | Display page 2 | Write |
|
|
567
|
+
| $C056 | Lo-Res mode | Write |
|
|
568
|
+
| $C057 | Hi-Res mode | Write |
|
|
569
|
+
| $C05E | Annunciator 3 ON (disables double-res) | Write |
|
|
570
|
+
| $C05F | Annunciator 3 OFF (enables double-res) | Write |
|
|
571
|
+
| $C00C | 40-column mode | Write |
|
|
572
|
+
| $C00D | 80-column mode | Write |
|
|
573
|
+
| $C00E | Primary character set | Write |
|
|
574
|
+
| $C00F | Alternate character set (MouseText) | Write |
|
|
575
|
+
| $C000 | 80STORE off | Write |
|
|
576
|
+
| $C001 | 80STORE on | Write |
|
|
577
|
+
| $C019 | VBL status (bit 7: 1 = not in VBL) | Read |
|
|
578
|
+
| $C01A | TEXT status | Read |
|
|
579
|
+
| $C01B | MIXED status | Read |
|
|
580
|
+
| $C01C | PAGE2 status | Read |
|
|
581
|
+
| $C01D | HIRES status | Read |
|
|
582
|
+
| $C01E | ALTCHARSET status | Read |
|
|
583
|
+
| $C01F | 80COL status | Read |
|
|
584
|
+
|
|
585
|
+
See [[Memory-System]] for the full soft switch reference including memory banking switches.
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## Display Pipeline
|
|
590
|
+
|
|
591
|
+
After the C++ Video class produces the framebuffer, the JavaScript layer handles display:
|
|
592
|
+
|
|
593
|
+
```
|
|
594
|
+
C++ Video (560x384 RGBA framebuffer)
|
|
595
|
+
--> WASM _getFramebuffer() returns pointer into WASM heap
|
|
596
|
+
--> JS reads via HEAPU8[ptr .. ptr + FRAMEBUFFER_SIZE]
|
|
597
|
+
--> WebGLRenderer.updateTexture(data)
|
|
598
|
+
--> Fragment shader applies CRT effects
|
|
599
|
+
--> Canvas displays final output
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Frame display is triggered by the audio-driven timing system. When enough audio samples have been generated to represent one frame (~800 samples at 48 kHz / 60 Hz), the `AudioDriver` fires `onFrameReady`, which uploads the latest framebuffer to the WebGL texture. See [[Architecture-Overview]] for details on the audio-driven timing chain.
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## WebGL Shader Pipeline
|
|
607
|
+
|
|
608
|
+
The JavaScript renderer (`src/js/display/webgl-renderer.js`) uploads the completed framebuffer to a WebGL texture each frame, then applies a multi-pass shader pipeline:
|
|
609
|
+
|
|
610
|
+
### Pass 1: CRT Fragment Shader
|
|
611
|
+
|
|
612
|
+
The main rendering pass applies CRT and analog display effects in a single draw call:
|
|
613
|
+
|
|
614
|
+
- Screen curvature (barrel distortion)
|
|
615
|
+
- Scanlines and shadow mask
|
|
616
|
+
- Phosphor glow (bloom)
|
|
617
|
+
- Vignette
|
|
618
|
+
- Chromatic aberration (RGB offset)
|
|
619
|
+
- Flicker and static noise
|
|
620
|
+
- Jitter and horizontal sync distortion
|
|
621
|
+
- Glowing scan beam line
|
|
622
|
+
- Ambient light reflection
|
|
623
|
+
- Color bleed (vertical inter-scanline blending)
|
|
624
|
+
- NTSC fringing
|
|
625
|
+
- Monochrome phosphor tinting (green, amber, or white)
|
|
626
|
+
- Screen border and rounded corners
|
|
627
|
+
|
|
628
|
+
All parameters are passed as WebGL uniforms and update in real time.
|
|
629
|
+
|
|
630
|
+
### Pass 2: Burn-In Shader
|
|
631
|
+
|
|
632
|
+
A double-buffered framebuffer pair accumulates bright pixel values and decays them over time. The resulting burn-in texture is sampled by the main CRT shader to blend phosphor persistence into the display.
|
|
633
|
+
|
|
634
|
+
### Pass 3: Edge Overlay
|
|
635
|
+
|
|
636
|
+
A final compositing pass draws a subtle highlight along the screen border, simulating light reflecting off CRT glass edges.
|
|
637
|
+
|
|
638
|
+
### Texture Configuration
|
|
639
|
+
|
|
640
|
+
- **Source texture**: 560 x 384 RGBA, updated from WASM memory each frame
|
|
641
|
+
- **Filtering**: Bilinear by default; nearest-neighbor when Sharp Pixels is enabled
|
|
642
|
+
- **Selection overlay**: A separate texture for text selection highlighting, composited in the CRT shader
|
|
643
|
+
- **Burn-in textures**: Two framebuffer-sized textures ping-ponged for temporal accumulation
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## Display Settings Integration
|
|
648
|
+
|
|
649
|
+
All shader parameters are exposed through the [[Display-Settings]] window. Settings are stored in `localStorage` as JSON and applied at startup. The `WebGLRenderer.setParam(name, value)` method maps setting names to uniform locations, allowing real-time adjustment as the user drags sliders.
|
|
650
|
+
|
|
651
|
+
---
|
|
652
|
+
|
|
653
|
+
## See Also
|
|
654
|
+
|
|
655
|
+
- [[Architecture-Overview]] -- Two-layer design, audio-driven timing, frame synchronization
|
|
656
|
+
- [[CPU-Emulation]] -- 65C02 processor driving the rendering timing
|
|
657
|
+
- [[Memory-System]] -- MMU, soft switches, auxiliary memory bank switching
|
|
658
|
+
- [[Display-Settings]] -- CRT shader effect configuration
|