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.
Files changed (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. 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