c64-kb 0.17.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 (970) hide show
  1. package/CHANGELOG.md +2082 -0
  2. package/LICENSE +29 -0
  3. package/README.md +559 -0
  4. package/VERSION +25 -0
  5. package/dist/cli/setup.js +66 -0
  6. package/dist/cli/setup.js.map +1 -0
  7. package/dist/cli-memorize-audit.js +97 -0
  8. package/dist/cli-memorize-audit.js.map +1 -0
  9. package/dist/cli.js +335 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.js +83 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/context.js +53 -0
  14. package/dist/context.js.map +1 -0
  15. package/dist/domain/budget-notes.js +104 -0
  16. package/dist/domain/budget-notes.js.map +1 -0
  17. package/dist/domain/budget.js +417 -0
  18. package/dist/domain/budget.js.map +1 -0
  19. package/dist/domain/game-design.js +77 -0
  20. package/dist/domain/game-design.js.map +1 -0
  21. package/dist/domain/timing.js +39 -0
  22. package/dist/domain/timing.js.map +1 -0
  23. package/dist/graph/apply.js +132 -0
  24. package/dist/graph/apply.js.map +1 -0
  25. package/dist/graph/claims.js +231 -0
  26. package/dist/graph/claims.js.map +1 -0
  27. package/dist/graph/extract/archetype.js +118 -0
  28. package/dist/graph/extract/archetype.js.map +1 -0
  29. package/dist/graph/extract/common.js +69 -0
  30. package/dist/graph/extract/common.js.map +1 -0
  31. package/dist/graph/extract/failure.js +37 -0
  32. package/dist/graph/extract/failure.js.map +1 -0
  33. package/dist/graph/extract/game-design.js +189 -0
  34. package/dist/graph/extract/game-design.js.map +1 -0
  35. package/dist/graph/extract/hardware.js +124 -0
  36. package/dist/graph/extract/hardware.js.map +1 -0
  37. package/dist/graph/extract/pitfall.js +62 -0
  38. package/dist/graph/extract/pitfall.js.map +1 -0
  39. package/dist/graph/extract/raster-band.js +56 -0
  40. package/dist/graph/extract/raster-band.js.map +1 -0
  41. package/dist/graph/extract/recipe.js +71 -0
  42. package/dist/graph/extract/recipe.js.map +1 -0
  43. package/dist/graph/extract/technique-entities.js +180 -0
  44. package/dist/graph/extract/technique-entities.js.map +1 -0
  45. package/dist/graph/extract/technique.js +179 -0
  46. package/dist/graph/extract/technique.js.map +1 -0
  47. package/dist/graph/extract/toolchain.js +67 -0
  48. package/dist/graph/extract/toolchain.js.map +1 -0
  49. package/dist/graph/extract/types.js +3 -0
  50. package/dist/graph/extract/types.js.map +1 -0
  51. package/dist/graph/extract/vocabulary.js +65 -0
  52. package/dist/graph/extract/vocabulary.js.map +1 -0
  53. package/dist/graph/extract.js +43 -0
  54. package/dist/graph/extract.js.map +1 -0
  55. package/dist/graph/kernal-clobbers.js +64 -0
  56. package/dist/graph/kernal-clobbers.js.map +1 -0
  57. package/dist/graph/machine-variants.js +208 -0
  58. package/dist/graph/machine-variants.js.map +1 -0
  59. package/dist/ingest/files.js +99 -0
  60. package/dist/ingest/files.js.map +1 -0
  61. package/dist/ingest/passes.js +104 -0
  62. package/dist/ingest/passes.js.map +1 -0
  63. package/dist/ingest/points.js +50 -0
  64. package/dist/ingest/points.js.map +1 -0
  65. package/dist/ingest/report.js +140 -0
  66. package/dist/ingest/report.js.map +1 -0
  67. package/dist/ingest/tally.js +81 -0
  68. package/dist/ingest/tally.js.map +1 -0
  69. package/dist/ingest/verified-on.js +43 -0
  70. package/dist/ingest/verified-on.js.map +1 -0
  71. package/dist/ingest.js +135 -0
  72. package/dist/ingest.js.map +1 -0
  73. package/dist/schemas/cost-basis.js +5 -0
  74. package/dist/schemas/cost-basis.js.map +1 -0
  75. package/dist/schemas/machine-variant.js +25 -0
  76. package/dist/schemas/machine-variant.js.map +1 -0
  77. package/dist/schemas/plan-budget.js +121 -0
  78. package/dist/schemas/plan-budget.js.map +1 -0
  79. package/dist/schemas/tool-outputs.js +526 -0
  80. package/dist/schemas/tool-outputs.js.map +1 -0
  81. package/dist/server/define-tool.js +52 -0
  82. package/dist/server/define-tool.js.map +1 -0
  83. package/dist/server/defined-only.js +9 -0
  84. package/dist/server/defined-only.js.map +1 -0
  85. package/dist/server/tools-briefings.js +70 -0
  86. package/dist/server/tools-briefings.js.map +1 -0
  87. package/dist/server/tools-lookup.js +180 -0
  88. package/dist/server/tools-lookup.js.map +1 -0
  89. package/dist/server/tools-maintenance.js +116 -0
  90. package/dist/server/tools-maintenance.js.map +1 -0
  91. package/dist/server/tools-pitfalls.js +89 -0
  92. package/dist/server/tools-pitfalls.js.map +1 -0
  93. package/dist/server/tools-recipes.js +296 -0
  94. package/dist/server/tools-recipes.js.map +1 -0
  95. package/dist/server/tools-runtime.js +48 -0
  96. package/dist/server/tools-runtime.js.map +1 -0
  97. package/dist/server/tools.js +43 -0
  98. package/dist/server/tools.js.map +1 -0
  99. package/dist/server.js +95 -0
  100. package/dist/server.js.map +1 -0
  101. package/dist/services/analytics/schema.js +78 -0
  102. package/dist/services/analytics/schema.js.map +1 -0
  103. package/dist/services/analytics/statements.js +99 -0
  104. package/dist/services/analytics/statements.js.map +1 -0
  105. package/dist/services/analytics.js +195 -0
  106. package/dist/services/analytics.js.map +1 -0
  107. package/dist/services/bm25.js +158 -0
  108. package/dist/services/bm25.js.map +1 -0
  109. package/dist/services/chunker.js +81 -0
  110. package/dist/services/chunker.js.map +1 -0
  111. package/dist/services/embeddings.js +58 -0
  112. package/dist/services/embeddings.js.map +1 -0
  113. package/dist/services/falkor/base.js +85 -0
  114. package/dist/services/falkor/base.js.map +1 -0
  115. package/dist/services/falkor/links.js +361 -0
  116. package/dist/services/falkor/links.js.map +1 -0
  117. package/dist/services/falkor/nodes.js +183 -0
  118. package/dist/services/falkor/nodes.js.map +1 -0
  119. package/dist/services/falkor/params.js +45 -0
  120. package/dist/services/falkor/params.js.map +1 -0
  121. package/dist/services/falkor/schema.js +77 -0
  122. package/dist/services/falkor/schema.js.map +1 -0
  123. package/dist/services/falkor.js +100 -0
  124. package/dist/services/falkor.js.map +1 -0
  125. package/dist/services/qdrant.js +238 -0
  126. package/dist/services/qdrant.js.map +1 -0
  127. package/dist/services/versions.js +73 -0
  128. package/dist/services/versions.js.map +1 -0
  129. package/dist/services/vice-bin.js +51 -0
  130. package/dist/services/vice-bin.js.map +1 -0
  131. package/dist/tools/briefings/archetype.js +174 -0
  132. package/dist/tools/briefings/archetype.js.map +1 -0
  133. package/dist/tools/briefings/budget.js +177 -0
  134. package/dist/tools/briefings/budget.js.map +1 -0
  135. package/dist/tools/briefings/build-order.js +135 -0
  136. package/dist/tools/briefings/build-order.js.map +1 -0
  137. package/dist/tools/briefings/build.js +185 -0
  138. package/dist/tools/briefings/build.js.map +1 -0
  139. package/dist/tools/briefings/discovery.js +290 -0
  140. package/dist/tools/briefings/discovery.js.map +1 -0
  141. package/dist/tools/briefings/plan-pitfalls.js +69 -0
  142. package/dist/tools/briefings/plan-pitfalls.js.map +1 -0
  143. package/dist/tools/briefings/render.js +151 -0
  144. package/dist/tools/briefings/render.js.map +1 -0
  145. package/dist/tools/briefings/rows.js +41 -0
  146. package/dist/tools/briefings/rows.js.map +1 -0
  147. package/dist/tools/briefings/toolchain.js +63 -0
  148. package/dist/tools/briefings/toolchain.js.map +1 -0
  149. package/dist/tools/briefings/why-proposed.js +69 -0
  150. package/dist/tools/briefings/why-proposed.js.map +1 -0
  151. package/dist/tools/briefings.js +44 -0
  152. package/dist/tools/briefings.js.map +1 -0
  153. package/dist/tools/feedback.js +97 -0
  154. package/dist/tools/feedback.js.map +1 -0
  155. package/dist/tools/hydrate.js +75 -0
  156. package/dist/tools/hydrate.js.map +1 -0
  157. package/dist/tools/intelligence.js +67 -0
  158. package/dist/tools/intelligence.js.map +1 -0
  159. package/dist/tools/lint/asm-decimal.js +86 -0
  160. package/dist/tools/lint/asm-decimal.js.map +1 -0
  161. package/dist/tools/lint/asm-rules.js +221 -0
  162. package/dist/tools/lint/asm-rules.js.map +1 -0
  163. package/dist/tools/lint/asm-shared.js +3 -0
  164. package/dist/tools/lint/asm-shared.js.map +1 -0
  165. package/dist/tools/lint/c-rules.js +215 -0
  166. package/dist/tools/lint/c-rules.js.map +1 -0
  167. package/dist/tools/lint/text.js +122 -0
  168. package/dist/tools/lint/text.js.map +1 -0
  169. package/dist/tools/lint/types.js +40 -0
  170. package/dist/tools/lint/types.js.map +1 -0
  171. package/dist/tools/lint.js +76 -0
  172. package/dist/tools/lint.js.map +1 -0
  173. package/dist/tools/memorization-mcp.js +111 -0
  174. package/dist/tools/memorization-mcp.js.map +1 -0
  175. package/dist/tools/pitfalls/bm25.js +53 -0
  176. package/dist/tools/pitfalls/bm25.js.map +1 -0
  177. package/dist/tools/pitfalls/format.js +58 -0
  178. package/dist/tools/pitfalls/format.js.map +1 -0
  179. package/dist/tools/pitfalls/graph.js +124 -0
  180. package/dist/tools/pitfalls/graph.js.map +1 -0
  181. package/dist/tools/pitfalls.js +134 -0
  182. package/dist/tools/pitfalls.js.map +1 -0
  183. package/dist/tools/prompts.js +81 -0
  184. package/dist/tools/prompts.js.map +1 -0
  185. package/dist/tools/query/compatibility/closure.js +62 -0
  186. package/dist/tools/query/compatibility/closure.js.map +1 -0
  187. package/dist/tools/query/compatibility/facts.js +28 -0
  188. package/dist/tools/query/compatibility/facts.js.map +1 -0
  189. package/dist/tools/query/compatibility/fetch.js +164 -0
  190. package/dist/tools/query/compatibility/fetch.js.map +1 -0
  191. package/dist/tools/query/compatibility/hard-rules.js +159 -0
  192. package/dist/tools/query/compatibility/hard-rules.js.map +1 -0
  193. package/dist/tools/query/compatibility/index.js +27 -0
  194. package/dist/tools/query/compatibility/index.js.map +1 -0
  195. package/dist/tools/query/compatibility/kernal-zp-rule.js +90 -0
  196. package/dist/tools/query/compatibility/kernal-zp-rule.js.map +1 -0
  197. package/dist/tools/query/compatibility/render.js +106 -0
  198. package/dist/tools/query/compatibility/render.js.map +1 -0
  199. package/dist/tools/query/compatibility/rules.js +337 -0
  200. package/dist/tools/query/compatibility/rules.js.map +1 -0
  201. package/dist/tools/query/compatibility/unit-rules.js +253 -0
  202. package/dist/tools/query/compatibility/unit-rules.js.map +1 -0
  203. package/dist/tools/query/game-design.js +83 -0
  204. package/dist/tools/query/game-design.js.map +1 -0
  205. package/dist/tools/query/lookups.js +220 -0
  206. package/dist/tools/query/lookups.js.map +1 -0
  207. package/dist/tools/query/plan-budget.js +266 -0
  208. package/dist/tools/query/plan-budget.js.map +1 -0
  209. package/dist/tools/query/recipes.js +214 -0
  210. package/dist/tools/query/recipes.js.map +1 -0
  211. package/dist/tools/query/retrieval.js +128 -0
  212. package/dist/tools/query/retrieval.js.map +1 -0
  213. package/dist/tools/query/shared.js +121 -0
  214. package/dist/tools/query/shared.js.map +1 -0
  215. package/dist/tools/query/techniques.js +331 -0
  216. package/dist/tools/query/techniques.js.map +1 -0
  217. package/dist/tools/query/timing.js +106 -0
  218. package/dist/tools/query/timing.js.map +1 -0
  219. package/dist/tools/query/types.js +11 -0
  220. package/dist/tools/query/types.js.map +1 -0
  221. package/dist/tools/query.js +21 -0
  222. package/dist/tools/query.js.map +1 -0
  223. package/dist/tools/resources.js +186 -0
  224. package/dist/tools/resources.js.map +1 -0
  225. package/dist/tools/run-game.js +249 -0
  226. package/dist/tools/run-game.js.map +1 -0
  227. package/dist/tools/selfimprovement.js +367 -0
  228. package/dist/tools/selfimprovement.js.map +1 -0
  229. package/docker-compose.yml +35 -0
  230. package/docs/ARCHITECTURE.md +164 -0
  231. package/docs/CONVENTIONS-archetypes.md +158 -0
  232. package/docs/CONVENTIONS-failures.md +46 -0
  233. package/docs/CONVENTIONS-game-designs.md +75 -0
  234. package/docs/CONVENTIONS-hardware-reference.md +184 -0
  235. package/docs/CONVENTIONS-pitfalls.md +91 -0
  236. package/docs/CONVENTIONS-recipes.md +106 -0
  237. package/docs/CONVENTIONS-techniques.md +309 -0
  238. package/docs/CONVENTIONS-toolchain-reference.md +89 -0
  239. package/docs/ONTOLOGY.md +655 -0
  240. package/docs/art/art-production-reference.md +157 -0
  241. package/docs/art/asset-pipelines.md +586 -0
  242. package/docs/c64-failure-patterns.md +693 -0
  243. package/docs/demo-design/demo-composition.md +349 -0
  244. package/docs/demo-design/demo-design-philosophy.md +320 -0
  245. package/docs/demo-design/intro-cracktro-patterns.md +435 -0
  246. package/docs/figures/ctm-spd-embed-probe.png +0 -0
  247. package/docs/figures/dot-flag-mid-5000000.png +0 -0
  248. package/docs/figures/dot-flag-no-erase-12000000.png +0 -0
  249. package/docs/figures/edges.json +1 -0
  250. package/docs/figures/fig6-architecture.png +0 -0
  251. package/docs/figures/fig7-ontology.png +0 -0
  252. package/docs/figures/screen-dissolve-mid-frame25.png +0 -0
  253. package/docs/figures/screen-dissolve-wipe-frame25.png +0 -0
  254. package/docs/figures/spindle-two-parts-part1.png +0 -0
  255. package/docs/figures/spindle-two-parts-part2.png +0 -0
  256. package/docs/figures/starters/action-puzzle.png +0 -0
  257. package/docs/figures/starters/adventure.png +0 -0
  258. package/docs/figures/starters/beat-em-up.png +0 -0
  259. package/docs/figures/starters/demo.png +0 -0
  260. package/docs/figures/starters/platformer.png +0 -0
  261. package/docs/figures/starters/shmup-vertical.png +0 -0
  262. package/docs/figures/tape-kernal-master-ntsc.png +0 -0
  263. package/docs/figures/tape-kernal-master-pal.png +0 -0
  264. package/docs/figures/tape-turbo-loader-ntsc.png +0 -0
  265. package/docs/figures/tape-turbo-loader-pal.png +0 -0
  266. package/docs/figures/tape-turbo-master-ntsc.png +0 -0
  267. package/docs/figures/tape-turbo-master-pal.png +0 -0
  268. package/docs/figures/tech-tech-pad2-12000000.png +0 -0
  269. package/docs/figures/tech-tech-pad4-12000000.png +0 -0
  270. package/docs/figures/tech-tech-seven-12000000.png +0 -0
  271. package/docs/figures/twister-mid-5000000.png +0 -0
  272. package/docs/figures/twister-straight-12000000.png +0 -0
  273. package/docs/figures/vector-balls-nosort-ntsc.png +0 -0
  274. package/docs/figures/vector-balls-nosort.png +0 -0
  275. package/docs/formats/c64-file-formats.md +1212 -0
  276. package/docs/formats/iec-disk-reference.md +754 -0
  277. package/docs/game-design/c64-game-archetypes.md +273 -0
  278. package/docs/game-design/designs/falling-blocks.md +54 -0
  279. package/docs/game-design/designs/platformer-scaffold.md +48 -0
  280. package/docs/game-design/designs/simple-shmup.md +27 -0
  281. package/docs/game-design/enemy-behaviour-and-difficulty.md +349 -0
  282. package/docs/game-design/game-design-patterns.md +1189 -0
  283. package/docs/game-design/game-structure.md +354 -0
  284. package/docs/game-design/production-planning.md +349 -0
  285. package/docs/game-design/reference-game-sources.md +90 -0
  286. package/docs/hardware/6502-illegal-opcodes.md +1353 -0
  287. package/docs/hardware/6510-cpu-reference.md +2112 -0
  288. package/docs/hardware/c64-memory-map.md +3184 -0
  289. package/docs/hardware/c64-registers-reference.md +996 -0
  290. package/docs/hardware/cia-reference.md +1281 -0
  291. package/docs/hardware/kernal-routines-reference.md +1836 -0
  292. package/docs/hardware/pal-ntsc-reference.md +813 -0
  293. package/docs/hardware/sid-reference.md +1137 -0
  294. package/docs/hardware/vic-ii-reference.md +1714 -0
  295. package/docs/music/music-production-reference.md +403 -0
  296. package/docs/pitfalls/banking.md +1067 -0
  297. package/docs/pitfalls/cia.md +1147 -0
  298. package/docs/pitfalls/cpu.md +874 -0
  299. package/docs/pitfalls/input.md +282 -0
  300. package/docs/pitfalls/kernal-and-io.md +1615 -0
  301. package/docs/pitfalls/loader.md +1191 -0
  302. package/docs/pitfalls/logic.md +94 -0
  303. package/docs/pitfalls/maths.md +187 -0
  304. package/docs/pitfalls/raster-and-badline.md +721 -0
  305. package/docs/pitfalls/region-timing.md +660 -0
  306. package/docs/pitfalls/scroll.md +207 -0
  307. package/docs/pitfalls/sid.md +738 -0
  308. package/docs/pitfalls/sprite.md +979 -0
  309. package/docs/pitfalls/text-mode-render.md +1172 -0
  310. package/docs/recipes/cc65/cartridge-8k.md +398 -0
  311. package/docs/recipes/cc65/hello-world-conio.md +76 -0
  312. package/docs/recipes/cc65/memory-layout.md +312 -0
  313. package/docs/recipes/cc65/screenshots/README.md +14 -0
  314. package/docs/recipes/cc65/screenshots/cartridge-8k-ntsc.png +0 -0
  315. package/docs/recipes/cc65/screenshots/cartridge-8k.png +0 -0
  316. package/docs/recipes/cc65/screenshots/hello-world-conio.png +0 -0
  317. package/docs/recipes/cc65/screenshots/memory-layout-ntsc.png +0 -0
  318. package/docs/recipes/cc65/screenshots/memory-layout.png +0 -0
  319. package/docs/recipes/kickassembler/basic-float-calls.md +578 -0
  320. package/docs/recipes/kickassembler/basic-wedge.md +398 -0
  321. package/docs/recipes/kickassembler/big-font-scroller.md +620 -0
  322. package/docs/recipes/kickassembler/charset-copy-rom-to-ram.md +623 -0
  323. package/docs/recipes/kickassembler/cia-revision-detect.md +366 -0
  324. package/docs/recipes/kickassembler/colour-cycling.md +343 -0
  325. package/docs/recipes/kickassembler/colour-fade.md +303 -0
  326. package/docs/recipes/kickassembler/compare-16bit-signed.md +1007 -0
  327. package/docs/recipes/kickassembler/cracktro-template.md +526 -0
  328. package/docs/recipes/kickassembler/crt-banked.md +276 -0
  329. package/docs/recipes/kickassembler/dos-error-codes.md +466 -0
  330. package/docs/recipes/kickassembler/dot-flag.md +576 -0
  331. package/docs/recipes/kickassembler/drive-job-queue.md +667 -0
  332. package/docs/recipes/kickassembler/drive-via-probe.md +690 -0
  333. package/docs/recipes/kickassembler/dycp-scroller.md +568 -0
  334. package/docs/recipes/kickassembler/dypp-sprite-scroller.md +681 -0
  335. package/docs/recipes/kickassembler/dysp.md +670 -0
  336. package/docs/recipes/kickassembler/easyflash-save.md +800 -0
  337. package/docs/recipes/kickassembler/eight-way-scroll.md +1484 -0
  338. package/docs/recipes/kickassembler/file-io-roundtrip.md +546 -0
  339. package/docs/recipes/kickassembler/fire-effect.md +420 -0
  340. package/docs/recipes/kickassembler/fld.md +414 -0
  341. package/docs/recipes/kickassembler/fli-image.md +336 -0
  342. package/docs/recipes/kickassembler/four-player-read.md +672 -0
  343. package/docs/recipes/kickassembler/headless-verify.md +228 -0
  344. package/docs/recipes/kickassembler/hello-world.md +98 -0
  345. package/docs/recipes/kickassembler/high-score-insert.md +660 -0
  346. package/docs/recipes/kickassembler/hires-plot-line.md +723 -0
  347. package/docs/recipes/kickassembler/irq-chain.md +316 -0
  348. package/docs/recipes/kickassembler/irq-owns-port.md +668 -0
  349. package/docs/recipes/kickassembler/isometric-room.md +1145 -0
  350. package/docs/recipes/kickassembler/light-pen-read.md +926 -0
  351. package/docs/recipes/kickassembler/logic-rate-decoupling.md +670 -0
  352. package/docs/recipes/kickassembler/mci-interlace.md +543 -0
  353. package/docs/recipes/kickassembler/memory-layout.md +277 -0
  354. package/docs/recipes/kickassembler/mouse-1351-read.md +801 -0
  355. package/docs/recipes/kickassembler/nmi-timer-tick.md +562 -0
  356. package/docs/recipes/kickassembler/own-keyscan.md +881 -0
  357. package/docs/recipes/kickassembler/paddle-read.md +751 -0
  358. package/docs/recipes/kickassembler/palette-cells.md +219 -0
  359. package/docs/recipes/kickassembler/pseudo-3d-road.md +930 -0
  360. package/docs/recipes/kickassembler/pwm-digi.md +463 -0
  361. package/docs/recipes/kickassembler/raster-bars.md +264 -0
  362. package/docs/recipes/kickassembler/reu-dma.md +1019 -0
  363. package/docs/recipes/kickassembler/runtime-relocation.md +743 -0
  364. package/docs/recipes/kickassembler/screen-dissolve.md +545 -0
  365. package/docs/recipes/kickassembler/screen-wipe.md +408 -0
  366. package/docs/recipes/kickassembler/screenshots/README.md +72 -0
  367. package/docs/recipes/kickassembler/screenshots/basic-float-calls-ntsc.png +0 -0
  368. package/docs/recipes/kickassembler/screenshots/basic-float-calls.png +0 -0
  369. package/docs/recipes/kickassembler/screenshots/basic-wedge-ntsc.png +0 -0
  370. package/docs/recipes/kickassembler/screenshots/basic-wedge.png +0 -0
  371. package/docs/recipes/kickassembler/screenshots/big-font-scroller-ntsc.png +0 -0
  372. package/docs/recipes/kickassembler/screenshots/big-font-scroller.png +0 -0
  373. package/docs/recipes/kickassembler/screenshots/charset-copy-rom-to-ram-ntsc.png +0 -0
  374. package/docs/recipes/kickassembler/screenshots/charset-copy-rom-to-ram.png +0 -0
  375. package/docs/recipes/kickassembler/screenshots/cia-revision-detect-ntsc.png +0 -0
  376. package/docs/recipes/kickassembler/screenshots/cia-revision-detect.png +0 -0
  377. package/docs/recipes/kickassembler/screenshots/colour-cycling-ntsc.png +0 -0
  378. package/docs/recipes/kickassembler/screenshots/colour-cycling.png +0 -0
  379. package/docs/recipes/kickassembler/screenshots/colour-fade-ntsc.png +0 -0
  380. package/docs/recipes/kickassembler/screenshots/colour-fade.png +0 -0
  381. package/docs/recipes/kickassembler/screenshots/compare-16bit-signed-ntsc.png +0 -0
  382. package/docs/recipes/kickassembler/screenshots/compare-16bit-signed.png +0 -0
  383. package/docs/recipes/kickassembler/screenshots/cracktro-template.png +0 -0
  384. package/docs/recipes/kickassembler/screenshots/crt-banked-ntsc.png +0 -0
  385. package/docs/recipes/kickassembler/screenshots/crt-banked.png +0 -0
  386. package/docs/recipes/kickassembler/screenshots/dos-error-codes-ntsc.png +0 -0
  387. package/docs/recipes/kickassembler/screenshots/dos-error-codes.png +0 -0
  388. package/docs/recipes/kickassembler/screenshots/dot-flag-ntsc.png +0 -0
  389. package/docs/recipes/kickassembler/screenshots/dot-flag.png +0 -0
  390. package/docs/recipes/kickassembler/screenshots/drive-job-queue-ntsc.png +0 -0
  391. package/docs/recipes/kickassembler/screenshots/drive-job-queue.png +0 -0
  392. package/docs/recipes/kickassembler/screenshots/drive-via-probe-ntsc.png +0 -0
  393. package/docs/recipes/kickassembler/screenshots/drive-via-probe.png +0 -0
  394. package/docs/recipes/kickassembler/screenshots/dycp-scroller-ntsc.png +0 -0
  395. package/docs/recipes/kickassembler/screenshots/dycp-scroller.png +0 -0
  396. package/docs/recipes/kickassembler/screenshots/dypp-sprite-scroller-ntsc.png +0 -0
  397. package/docs/recipes/kickassembler/screenshots/dypp-sprite-scroller.png +0 -0
  398. package/docs/recipes/kickassembler/screenshots/dysp-ntsc.png +0 -0
  399. package/docs/recipes/kickassembler/screenshots/dysp.png +0 -0
  400. package/docs/recipes/kickassembler/screenshots/easyflash-save-ntsc-run2.png +0 -0
  401. package/docs/recipes/kickassembler/screenshots/easyflash-save-ntsc.png +0 -0
  402. package/docs/recipes/kickassembler/screenshots/easyflash-save-run2.png +0 -0
  403. package/docs/recipes/kickassembler/screenshots/easyflash-save.png +0 -0
  404. package/docs/recipes/kickassembler/screenshots/eight-way-scroll-ntsc.png +0 -0
  405. package/docs/recipes/kickassembler/screenshots/eight-way-scroll.png +0 -0
  406. package/docs/recipes/kickassembler/screenshots/file-io-roundtrip-ntsc.png +0 -0
  407. package/docs/recipes/kickassembler/screenshots/file-io-roundtrip.png +0 -0
  408. package/docs/recipes/kickassembler/screenshots/fire-effect-ntsc.png +0 -0
  409. package/docs/recipes/kickassembler/screenshots/fire-effect.png +0 -0
  410. package/docs/recipes/kickassembler/screenshots/fld-ntsc.png +0 -0
  411. package/docs/recipes/kickassembler/screenshots/fld.png +0 -0
  412. package/docs/recipes/kickassembler/screenshots/fli-image.png +0 -0
  413. package/docs/recipes/kickassembler/screenshots/four-player-read-ntsc.png +0 -0
  414. package/docs/recipes/kickassembler/screenshots/four-player-read.png +0 -0
  415. package/docs/recipes/kickassembler/screenshots/headless-verify-ntsc.png +0 -0
  416. package/docs/recipes/kickassembler/screenshots/headless-verify.png +0 -0
  417. package/docs/recipes/kickassembler/screenshots/hello-world.png +0 -0
  418. package/docs/recipes/kickassembler/screenshots/high-score-insert-ntsc.png +0 -0
  419. package/docs/recipes/kickassembler/screenshots/high-score-insert.png +0 -0
  420. package/docs/recipes/kickassembler/screenshots/hires-plot-line-ntsc.png +0 -0
  421. package/docs/recipes/kickassembler/screenshots/hires-plot-line.png +0 -0
  422. package/docs/recipes/kickassembler/screenshots/irq-chain-ntsc.png +0 -0
  423. package/docs/recipes/kickassembler/screenshots/irq-chain.png +0 -0
  424. package/docs/recipes/kickassembler/screenshots/irq-owns-port-ntsc.png +0 -0
  425. package/docs/recipes/kickassembler/screenshots/irq-owns-port.png +0 -0
  426. package/docs/recipes/kickassembler/screenshots/isometric-room-ntsc.png +0 -0
  427. package/docs/recipes/kickassembler/screenshots/isometric-room.png +0 -0
  428. package/docs/recipes/kickassembler/screenshots/light-pen-read-ntsc.png +0 -0
  429. package/docs/recipes/kickassembler/screenshots/light-pen-read.png +0 -0
  430. package/docs/recipes/kickassembler/screenshots/logic-rate-decoupling-ntsc.png +0 -0
  431. package/docs/recipes/kickassembler/screenshots/logic-rate-decoupling.png +0 -0
  432. package/docs/recipes/kickassembler/screenshots/mci-interlace-average.png +0 -0
  433. package/docs/recipes/kickassembler/screenshots/mci-interlace-frame-b.png +0 -0
  434. package/docs/recipes/kickassembler/screenshots/mci-interlace-ntsc.png +0 -0
  435. package/docs/recipes/kickassembler/screenshots/mci-interlace.png +0 -0
  436. package/docs/recipes/kickassembler/screenshots/memory-layout-ntsc.png +0 -0
  437. package/docs/recipes/kickassembler/screenshots/memory-layout.png +0 -0
  438. package/docs/recipes/kickassembler/screenshots/mouse-1351-read-ntsc.png +0 -0
  439. package/docs/recipes/kickassembler/screenshots/mouse-1351-read.png +0 -0
  440. package/docs/recipes/kickassembler/screenshots/nmi-timer-tick-ntsc.png +0 -0
  441. package/docs/recipes/kickassembler/screenshots/nmi-timer-tick.png +0 -0
  442. package/docs/recipes/kickassembler/screenshots/own-keyscan-ntsc.png +0 -0
  443. package/docs/recipes/kickassembler/screenshots/own-keyscan.png +0 -0
  444. package/docs/recipes/kickassembler/screenshots/paddle-read-ntsc.png +0 -0
  445. package/docs/recipes/kickassembler/screenshots/paddle-read.png +0 -0
  446. package/docs/recipes/kickassembler/screenshots/palette-cells-ntsc.png +0 -0
  447. package/docs/recipes/kickassembler/screenshots/palette-cells.png +0 -0
  448. package/docs/recipes/kickassembler/screenshots/pseudo-3d-road-ntsc.png +0 -0
  449. package/docs/recipes/kickassembler/screenshots/pseudo-3d-road.png +0 -0
  450. package/docs/recipes/kickassembler/screenshots/pwm-digi-ntsc.png +0 -0
  451. package/docs/recipes/kickassembler/screenshots/pwm-digi.png +0 -0
  452. package/docs/recipes/kickassembler/screenshots/raster-bars.png +0 -0
  453. package/docs/recipes/kickassembler/screenshots/reu-dma-ntsc.png +0 -0
  454. package/docs/recipes/kickassembler/screenshots/reu-dma.png +0 -0
  455. package/docs/recipes/kickassembler/screenshots/runtime-relocation-ntsc.png +0 -0
  456. package/docs/recipes/kickassembler/screenshots/runtime-relocation.png +0 -0
  457. package/docs/recipes/kickassembler/screenshots/screen-dissolve-ntsc.png +0 -0
  458. package/docs/recipes/kickassembler/screenshots/screen-dissolve.png +0 -0
  459. package/docs/recipes/kickassembler/screenshots/screen-wipe-ntsc.png +0 -0
  460. package/docs/recipes/kickassembler/screenshots/screen-wipe.png +0 -0
  461. package/docs/recipes/kickassembler/screenshots/scroll-panel-split-ntsc.png +0 -0
  462. package/docs/recipes/kickassembler/screenshots/scroll-panel-split.png +0 -0
  463. package/docs/recipes/kickassembler/screenshots/sfx-in-player-ntsc.png +0 -0
  464. package/docs/recipes/kickassembler/screenshots/sfx-in-player.png +0 -0
  465. package/docs/recipes/kickassembler/screenshots/shadebobs-ntsc.png +0 -0
  466. package/docs/recipes/kickassembler/screenshots/shadebobs.png +0 -0
  467. package/docs/recipes/kickassembler/screenshots/sideborder-open.png +0 -0
  468. package/docs/recipes/kickassembler/screenshots/sine-scroller.png +0 -0
  469. package/docs/recipes/kickassembler/screenshots/sine-table-runtime-ntsc.png +0 -0
  470. package/docs/recipes/kickassembler/screenshots/sine-table-runtime.png +0 -0
  471. package/docs/recipes/kickassembler/screenshots/software-sprite-preshifted-ntsc.png +0 -0
  472. package/docs/recipes/kickassembler/screenshots/software-sprite-preshifted.png +0 -0
  473. package/docs/recipes/kickassembler/screenshots/speedcode-generator-ntsc.png +0 -0
  474. package/docs/recipes/kickassembler/screenshots/speedcode-generator.png +0 -0
  475. package/docs/recipes/kickassembler/screenshots/sprite-border-scroller-ntsc.png +0 -0
  476. package/docs/recipes/kickassembler/screenshots/sprite-border-scroller.png +0 -0
  477. package/docs/recipes/kickassembler/screenshots/sprite-cache-flip-ntsc.png +0 -0
  478. package/docs/recipes/kickassembler/screenshots/sprite-cache-flip.png +0 -0
  479. package/docs/recipes/kickassembler/screenshots/sprite-multiplex-24.png +0 -0
  480. package/docs/recipes/kickassembler/screenshots/sprite-multiplex-game-ntsc.png +0 -0
  481. package/docs/recipes/kickassembler/screenshots/sprite-multiplex-game.png +0 -0
  482. package/docs/recipes/kickassembler/screenshots/sprite-priority-classes-ntsc.png +0 -0
  483. package/docs/recipes/kickassembler/screenshots/sprite-priority-classes.png +0 -0
  484. package/docs/recipes/kickassembler/screenshots/sprite-sine-chain-ntsc.png +0 -0
  485. package/docs/recipes/kickassembler/screenshots/sprite-sine-chain.png +0 -0
  486. package/docs/recipes/kickassembler/screenshots/sprite-stretcher-ntsc.png +0 -0
  487. package/docs/recipes/kickassembler/screenshots/sprite-stretcher.png +0 -0
  488. package/docs/recipes/kickassembler/screenshots/sprites-only-screen-ntsc.png +0 -0
  489. package/docs/recipes/kickassembler/screenshots/sprites-only-screen.png +0 -0
  490. package/docs/recipes/kickassembler/screenshots/sqrt-atan2-ntsc.png +0 -0
  491. package/docs/recipes/kickassembler/screenshots/sqrt-atan2.png +0 -0
  492. package/docs/recipes/kickassembler/screenshots/stable-raster-irq-control.png +0 -0
  493. package/docs/recipes/kickassembler/screenshots/stable-raster-irq.png +0 -0
  494. package/docs/recipes/kickassembler/screenshots/tech-tech-ntsc.png +0 -0
  495. package/docs/recipes/kickassembler/screenshots/tech-tech.png +0 -0
  496. package/docs/recipes/kickassembler/screenshots/tod-alarm-ntsc.png +0 -0
  497. package/docs/recipes/kickassembler/screenshots/tod-alarm.png +0 -0
  498. package/docs/recipes/kickassembler/screenshots/topbottom-border-open.png +0 -0
  499. package/docs/recipes/kickassembler/screenshots/twister-ntsc.png +0 -0
  500. package/docs/recipes/kickassembler/screenshots/twister.png +0 -0
  501. package/docs/recipes/kickassembler/screenshots/unit-test-driver-ntsc.png +0 -0
  502. package/docs/recipes/kickassembler/screenshots/unit-test-driver.png +0 -0
  503. package/docs/recipes/kickassembler/screenshots/vector-balls-ntsc.png +0 -0
  504. package/docs/recipes/kickassembler/screenshots/vector-balls.png +0 -0
  505. package/docs/recipes/kickassembler/screenshots/wireframe-ships-ntsc.png +0 -0
  506. package/docs/recipes/kickassembler/screenshots/wireframe-ships.png +0 -0
  507. package/docs/recipes/kickassembler/scroll-panel-split.md +381 -0
  508. package/docs/recipes/kickassembler/sfx-in-player.md +907 -0
  509. package/docs/recipes/kickassembler/shadebobs.md +522 -0
  510. package/docs/recipes/kickassembler/sideborder-open.md +338 -0
  511. package/docs/recipes/kickassembler/sine-scroller.md +321 -0
  512. package/docs/recipes/kickassembler/sine-table-runtime.md +815 -0
  513. package/docs/recipes/kickassembler/software-sprite-preshifted.md +725 -0
  514. package/docs/recipes/kickassembler/speedcode-generator.md +606 -0
  515. package/docs/recipes/kickassembler/sprite-border-scroller.md +696 -0
  516. package/docs/recipes/kickassembler/sprite-cache-flip.md +1071 -0
  517. package/docs/recipes/kickassembler/sprite-multiplex-24.md +326 -0
  518. package/docs/recipes/kickassembler/sprite-multiplex-game.md +986 -0
  519. package/docs/recipes/kickassembler/sprite-priority-classes.md +463 -0
  520. package/docs/recipes/kickassembler/sprite-sine-chain.md +338 -0
  521. package/docs/recipes/kickassembler/sprite-stretcher.md +501 -0
  522. package/docs/recipes/kickassembler/sprites-only-screen.md +550 -0
  523. package/docs/recipes/kickassembler/sqrt-atan2.md +1014 -0
  524. package/docs/recipes/kickassembler/stable-raster-irq.md +360 -0
  525. package/docs/recipes/kickassembler/tape-turbo-loader.md +875 -0
  526. package/docs/recipes/kickassembler/tech-tech.md +679 -0
  527. package/docs/recipes/kickassembler/tod-alarm.md +754 -0
  528. package/docs/recipes/kickassembler/topbottom-border-open.md +407 -0
  529. package/docs/recipes/kickassembler/twister.md +518 -0
  530. package/docs/recipes/kickassembler/unit-test-driver.md +314 -0
  531. package/docs/recipes/kickassembler/vector-balls.md +545 -0
  532. package/docs/recipes/kickassembler/wireframe-ships.md +2324 -0
  533. package/docs/recipes/oscar64/actor-activation-window.md +807 -0
  534. package/docs/recipes/oscar64/adventure-engine.md +1094 -0
  535. package/docs/recipes/oscar64/attract-replay.md +465 -0
  536. package/docs/recipes/oscar64/beat-em-up-lanes.md +563 -0
  537. package/docs/recipes/oscar64/bfs-distance-map.md +723 -0
  538. package/docs/recipes/oscar64/bitmap-koala-viewer.md +355 -0
  539. package/docs/recipes/oscar64/car-contact.md +950 -0
  540. package/docs/recipes/oscar64/cave-scan.md +609 -0
  541. package/docs/recipes/oscar64/char-bullets.md +477 -0
  542. package/docs/recipes/oscar64/charset-animation.md +383 -0
  543. package/docs/recipes/oscar64/charset-parallax.md +401 -0
  544. package/docs/recipes/oscar64/destructible-terrain.md +1099 -0
  545. package/docs/recipes/oscar64/difficulty-tables.md +654 -0
  546. package/docs/recipes/oscar64/dig-and-guards.md +975 -0
  547. package/docs/recipes/oscar64/directory-reader.md +535 -0
  548. package/docs/recipes/oscar64/divide-check.md +581 -0
  549. package/docs/recipes/oscar64/double-buffer-nomirror.md +205 -0
  550. package/docs/recipes/oscar64/double-buffer.md +255 -0
  551. package/docs/recipes/oscar64/falling-blocks.md +874 -0
  552. package/docs/recipes/oscar64/fixed-point-jump-velocity.md +289 -0
  553. package/docs/recipes/oscar64/fixed-point-jump.md +210 -0
  554. package/docs/recipes/oscar64/flip-screen-rooms.md +793 -0
  555. package/docs/recipes/oscar64/frame-sync-loop-overrun.md +214 -0
  556. package/docs/recipes/oscar64/frame-sync-loop.md +254 -0
  557. package/docs/recipes/oscar64/game-tree-search.md +743 -0
  558. package/docs/recipes/oscar64/ghost-targeting.md +867 -0
  559. package/docs/recipes/oscar64/headless-verify.md +345 -0
  560. package/docs/recipes/oscar64/hello-world.md +83 -0
  561. package/docs/recipes/oscar64/high-score-persist.md +772 -0
  562. package/docs/recipes/oscar64/iffl-kernal-skip.md +421 -0
  563. package/docs/recipes/oscar64/joystick-input.md +358 -0
  564. package/docs/recipes/oscar64/lane-pursuit.md +1028 -0
  565. package/docs/recipes/oscar64/level-rle-decoder.md +620 -0
  566. package/docs/recipes/oscar64/lfsr-random-seed2.md +338 -0
  567. package/docs/recipes/oscar64/lfsr-random.md +403 -0
  568. package/docs/recipes/oscar64/load-asset-runtime.md +492 -0
  569. package/docs/recipes/oscar64/memory-layout.md +232 -0
  570. package/docs/recipes/oscar64/mixed-fighters.md +781 -0
  571. package/docs/recipes/oscar64/multi-sprite-object.md +533 -0
  572. package/docs/recipes/oscar64/nav-area-pathfinding.md +881 -0
  573. package/docs/recipes/oscar64/object-pool.md +572 -0
  574. package/docs/recipes/oscar64/pal-ntsc-detect.md +257 -0
  575. package/docs/recipes/oscar64/password-state.md +532 -0
  576. package/docs/recipes/oscar64/per-frame-hitbox.md +651 -0
  577. package/docs/recipes/oscar64/petscii-screen-codes.md +428 -0
  578. package/docs/recipes/oscar64/platformer-scaffold.md +1467 -0
  579. package/docs/recipes/oscar64/print-number.md +367 -0
  580. package/docs/recipes/oscar64/raster-bars.md +239 -0
  581. package/docs/recipes/oscar64/raster-profile-bars.md +404 -0
  582. package/docs/recipes/oscar64/rel-side-sectors.md +391 -0
  583. package/docs/recipes/oscar64/relative-file-records.md +378 -0
  584. package/docs/recipes/oscar64/save-load-seq-file.md +365 -0
  585. package/docs/recipes/oscar64/screenshots/README.md +99 -0
  586. package/docs/recipes/oscar64/screenshots/actor-activation-window-ntsc.png +0 -0
  587. package/docs/recipes/oscar64/screenshots/actor-activation-window.png +0 -0
  588. package/docs/recipes/oscar64/screenshots/adventure-engine-ntsc.png +0 -0
  589. package/docs/recipes/oscar64/screenshots/adventure-engine.png +0 -0
  590. package/docs/recipes/oscar64/screenshots/attract-replay-ntsc.png +0 -0
  591. package/docs/recipes/oscar64/screenshots/attract-replay.png +0 -0
  592. package/docs/recipes/oscar64/screenshots/beat-em-up-lanes-ntsc.png +0 -0
  593. package/docs/recipes/oscar64/screenshots/beat-em-up-lanes.png +0 -0
  594. package/docs/recipes/oscar64/screenshots/bfs-distance-map-ntsc.png +0 -0
  595. package/docs/recipes/oscar64/screenshots/bfs-distance-map.png +0 -0
  596. package/docs/recipes/oscar64/screenshots/bitmap-koala-viewer.png +0 -0
  597. package/docs/recipes/oscar64/screenshots/car-contact-ntsc.png +0 -0
  598. package/docs/recipes/oscar64/screenshots/car-contact.png +0 -0
  599. package/docs/recipes/oscar64/screenshots/cave-scan-ntsc.png +0 -0
  600. package/docs/recipes/oscar64/screenshots/cave-scan.png +0 -0
  601. package/docs/recipes/oscar64/screenshots/char-bullets-ntsc.png +0 -0
  602. package/docs/recipes/oscar64/screenshots/char-bullets.png +0 -0
  603. package/docs/recipes/oscar64/screenshots/charset-animation-ntsc.png +0 -0
  604. package/docs/recipes/oscar64/screenshots/charset-animation.png +0 -0
  605. package/docs/recipes/oscar64/screenshots/charset-parallax-ntsc.png +0 -0
  606. package/docs/recipes/oscar64/screenshots/charset-parallax.png +0 -0
  607. package/docs/recipes/oscar64/screenshots/destructible-terrain-ntsc.png +0 -0
  608. package/docs/recipes/oscar64/screenshots/destructible-terrain.png +0 -0
  609. package/docs/recipes/oscar64/screenshots/difficulty-tables-ntsc.png +0 -0
  610. package/docs/recipes/oscar64/screenshots/difficulty-tables.png +0 -0
  611. package/docs/recipes/oscar64/screenshots/dig-and-guards-ntsc.png +0 -0
  612. package/docs/recipes/oscar64/screenshots/dig-and-guards.png +0 -0
  613. package/docs/recipes/oscar64/screenshots/directory-reader-ntsc.png +0 -0
  614. package/docs/recipes/oscar64/screenshots/directory-reader.png +0 -0
  615. package/docs/recipes/oscar64/screenshots/divide-check-ntsc.png +0 -0
  616. package/docs/recipes/oscar64/screenshots/divide-check.png +0 -0
  617. package/docs/recipes/oscar64/screenshots/double-buffer-nomirror.png +0 -0
  618. package/docs/recipes/oscar64/screenshots/double-buffer-ntsc.png +0 -0
  619. package/docs/recipes/oscar64/screenshots/double-buffer.png +0 -0
  620. package/docs/recipes/oscar64/screenshots/falling-blocks-ntsc.png +0 -0
  621. package/docs/recipes/oscar64/screenshots/falling-blocks.png +0 -0
  622. package/docs/recipes/oscar64/screenshots/fixed-point-jump-ntsc.png +0 -0
  623. package/docs/recipes/oscar64/screenshots/fixed-point-jump-velocity-ntsc.png +0 -0
  624. package/docs/recipes/oscar64/screenshots/fixed-point-jump-velocity.png +0 -0
  625. package/docs/recipes/oscar64/screenshots/fixed-point-jump.png +0 -0
  626. package/docs/recipes/oscar64/screenshots/flip-screen-rooms-ntsc.png +0 -0
  627. package/docs/recipes/oscar64/screenshots/flip-screen-rooms.png +0 -0
  628. package/docs/recipes/oscar64/screenshots/frame-sync-loop-ntsc.png +0 -0
  629. package/docs/recipes/oscar64/screenshots/frame-sync-loop-overrun.png +0 -0
  630. package/docs/recipes/oscar64/screenshots/frame-sync-loop.png +0 -0
  631. package/docs/recipes/oscar64/screenshots/game-tree-search-ntsc.png +0 -0
  632. package/docs/recipes/oscar64/screenshots/game-tree-search.png +0 -0
  633. package/docs/recipes/oscar64/screenshots/ghost-targeting-ntsc.png +0 -0
  634. package/docs/recipes/oscar64/screenshots/ghost-targeting.png +0 -0
  635. package/docs/recipes/oscar64/screenshots/headless-verify-ntsc.png +0 -0
  636. package/docs/recipes/oscar64/screenshots/headless-verify.png +0 -0
  637. package/docs/recipes/oscar64/screenshots/hello-world.png +0 -0
  638. package/docs/recipes/oscar64/screenshots/high-score-persist-ntsc.png +0 -0
  639. package/docs/recipes/oscar64/screenshots/high-score-persist.png +0 -0
  640. package/docs/recipes/oscar64/screenshots/iffl-kernal-skip-ntsc.png +0 -0
  641. package/docs/recipes/oscar64/screenshots/iffl-kernal-skip.png +0 -0
  642. package/docs/recipes/oscar64/screenshots/joystick-input-ntsc.png +0 -0
  643. package/docs/recipes/oscar64/screenshots/joystick-input.png +0 -0
  644. package/docs/recipes/oscar64/screenshots/lane-pursuit-ntsc.png +0 -0
  645. package/docs/recipes/oscar64/screenshots/lane-pursuit.png +0 -0
  646. package/docs/recipes/oscar64/screenshots/level-rle-decoder-ntsc.png +0 -0
  647. package/docs/recipes/oscar64/screenshots/level-rle-decoder.png +0 -0
  648. package/docs/recipes/oscar64/screenshots/lfsr-random-ntsc.png +0 -0
  649. package/docs/recipes/oscar64/screenshots/lfsr-random-seed2.png +0 -0
  650. package/docs/recipes/oscar64/screenshots/lfsr-random.png +0 -0
  651. package/docs/recipes/oscar64/screenshots/load-asset-runtime-ntsc.png +0 -0
  652. package/docs/recipes/oscar64/screenshots/load-asset-runtime.png +0 -0
  653. package/docs/recipes/oscar64/screenshots/memory-layout-ntsc.png +0 -0
  654. package/docs/recipes/oscar64/screenshots/memory-layout.png +0 -0
  655. package/docs/recipes/oscar64/screenshots/mixed-fighters-ntsc.png +0 -0
  656. package/docs/recipes/oscar64/screenshots/mixed-fighters.png +0 -0
  657. package/docs/recipes/oscar64/screenshots/multi-sprite-object-ntsc.png +0 -0
  658. package/docs/recipes/oscar64/screenshots/multi-sprite-object.png +0 -0
  659. package/docs/recipes/oscar64/screenshots/nav-area-pathfinding-ntsc.png +0 -0
  660. package/docs/recipes/oscar64/screenshots/nav-area-pathfinding.png +0 -0
  661. package/docs/recipes/oscar64/screenshots/object-pool-ntsc.png +0 -0
  662. package/docs/recipes/oscar64/screenshots/object-pool.png +0 -0
  663. package/docs/recipes/oscar64/screenshots/pal-ntsc-detect-ntsc.png +0 -0
  664. package/docs/recipes/oscar64/screenshots/pal-ntsc-detect-oldntsc.png +0 -0
  665. package/docs/recipes/oscar64/screenshots/pal-ntsc-detect-pal.png +0 -0
  666. package/docs/recipes/oscar64/screenshots/password-state-ntsc.png +0 -0
  667. package/docs/recipes/oscar64/screenshots/password-state.png +0 -0
  668. package/docs/recipes/oscar64/screenshots/per-frame-hitbox-ntsc.png +0 -0
  669. package/docs/recipes/oscar64/screenshots/per-frame-hitbox.png +0 -0
  670. package/docs/recipes/oscar64/screenshots/petscii-screen-codes-ntsc.png +0 -0
  671. package/docs/recipes/oscar64/screenshots/petscii-screen-codes.png +0 -0
  672. package/docs/recipes/oscar64/screenshots/platformer-scaffold-d64-ntsc.png +0 -0
  673. package/docs/recipes/oscar64/screenshots/platformer-scaffold-d64.png +0 -0
  674. package/docs/recipes/oscar64/screenshots/platformer-scaffold-ntsc.png +0 -0
  675. package/docs/recipes/oscar64/screenshots/platformer-scaffold.png +0 -0
  676. package/docs/recipes/oscar64/screenshots/print-number-ntsc.png +0 -0
  677. package/docs/recipes/oscar64/screenshots/print-number.png +0 -0
  678. package/docs/recipes/oscar64/screenshots/raster-bars.png +0 -0
  679. package/docs/recipes/oscar64/screenshots/raster-profile-bars-ntsc.png +0 -0
  680. package/docs/recipes/oscar64/screenshots/raster-profile-bars.png +0 -0
  681. package/docs/recipes/oscar64/screenshots/rel-side-sectors-ntsc.png +0 -0
  682. package/docs/recipes/oscar64/screenshots/rel-side-sectors.png +0 -0
  683. package/docs/recipes/oscar64/screenshots/relative-file-records-ntsc.png +0 -0
  684. package/docs/recipes/oscar64/screenshots/relative-file-records.png +0 -0
  685. package/docs/recipes/oscar64/screenshots/save-load-seq-file-ntsc.png +0 -0
  686. package/docs/recipes/oscar64/screenshots/save-load-seq-file.png +0 -0
  687. package/docs/recipes/oscar64/screenshots/seeded-level-fill-ntsc.png +0 -0
  688. package/docs/recipes/oscar64/screenshots/seeded-level-fill.png +0 -0
  689. package/docs/recipes/oscar64/screenshots/sfx-engine-ntsc.png +0 -0
  690. package/docs/recipes/oscar64/screenshots/sfx-engine.png +0 -0
  691. package/docs/recipes/oscar64/screenshots/sid-music-player.png +0 -0
  692. package/docs/recipes/oscar64/screenshots/simple-shmup.png +0 -0
  693. package/docs/recipes/oscar64/screenshots/slope-collision-ntsc.png +0 -0
  694. package/docs/recipes/oscar64/screenshots/slope-collision.png +0 -0
  695. package/docs/recipes/oscar64/screenshots/soft-scroll-h.png +0 -0
  696. package/docs/recipes/oscar64/screenshots/sprite-animation-table-ntsc.png +0 -0
  697. package/docs/recipes/oscar64/screenshots/sprite-animation-table.png +0 -0
  698. package/docs/recipes/oscar64/screenshots/sprite-multiplex-8.png +0 -0
  699. package/docs/recipes/oscar64/screenshots/stable-raster-irq.png +0 -0
  700. package/docs/recipes/oscar64/screenshots/text-input-ntsc.png +0 -0
  701. package/docs/recipes/oscar64/screenshots/text-input.png +0 -0
  702. package/docs/recipes/oscar64/screenshots/text-overlay-playfield-gameover.png +0 -0
  703. package/docs/recipes/oscar64/screenshots/text-overlay-playfield-notrail-control.png +0 -0
  704. package/docs/recipes/oscar64/screenshots/text-overlay-playfield-ntsc.png +0 -0
  705. package/docs/recipes/oscar64/screenshots/text-overlay-playfield-ungated-control.png +0 -0
  706. package/docs/recipes/oscar64/screenshots/text-overlay-playfield.png +0 -0
  707. package/docs/recipes/oscar64/screenshots/text-window-menu-ntsc.png +0 -0
  708. package/docs/recipes/oscar64/screenshots/text-window-menu.png +0 -0
  709. package/docs/recipes/oscar64/screenshots/tile-grid-collision-ntsc.png +0 -0
  710. package/docs/recipes/oscar64/screenshots/tile-grid-collision.png +0 -0
  711. package/docs/recipes/oscar64/screenshots/tile-map-render-ntsc.png +0 -0
  712. package/docs/recipes/oscar64/screenshots/tile-map-render.png +0 -0
  713. package/docs/recipes/oscar64/screenshots/two-player-ntsc.png +0 -0
  714. package/docs/recipes/oscar64/screenshots/two-player.png +0 -0
  715. package/docs/recipes/oscar64/screenshots/two-word-parser-ntsc.png +0 -0
  716. package/docs/recipes/oscar64/screenshots/two-word-parser.png +0 -0
  717. package/docs/recipes/oscar64/screenshots/vehicle-control-ntsc.png +0 -0
  718. package/docs/recipes/oscar64/screenshots/vehicle-control.png +0 -0
  719. package/docs/recipes/oscar64/screenshots/wave-director-ntsc.png +0 -0
  720. package/docs/recipes/oscar64/screenshots/wave-director.png +0 -0
  721. package/docs/recipes/oscar64/seeded-level-fill.md +364 -0
  722. package/docs/recipes/oscar64/sfx-engine.md +514 -0
  723. package/docs/recipes/oscar64/sid-music-player.md +441 -0
  724. package/docs/recipes/oscar64/simple-shmup.md +1000 -0
  725. package/docs/recipes/oscar64/slope-collision.md +610 -0
  726. package/docs/recipes/oscar64/soft-scroll-h.md +261 -0
  727. package/docs/recipes/oscar64/sprite-animation-table.md +671 -0
  728. package/docs/recipes/oscar64/sprite-multiplex-8.md +314 -0
  729. package/docs/recipes/oscar64/stable-raster-irq.md +236 -0
  730. package/docs/recipes/oscar64/text-input.md +274 -0
  731. package/docs/recipes/oscar64/text-overlay-playfield.md +617 -0
  732. package/docs/recipes/oscar64/text-window-menu.md +551 -0
  733. package/docs/recipes/oscar64/tile-grid-collision.md +563 -0
  734. package/docs/recipes/oscar64/tile-map-render.md +395 -0
  735. package/docs/recipes/oscar64/two-player.md +528 -0
  736. package/docs/recipes/oscar64/two-word-parser.md +510 -0
  737. package/docs/recipes/oscar64/vehicle-control.md +771 -0
  738. package/docs/recipes/oscar64/wave-director.md +787 -0
  739. package/docs/recipes/runs.json +1606 -0
  740. package/docs/runtime/sim6502-reference.md +466 -0
  741. package/docs/runtime/vice-mcp-reference.md +351 -0
  742. package/docs/runtime/vice-reference.md +1393 -0
  743. package/docs/techniques/bitmap-modes.md +685 -0
  744. package/docs/techniques/cpu-cycle-tricks.md +1570 -0
  745. package/docs/techniques/effects-vector-3d.md +1431 -0
  746. package/docs/techniques/file-io.md +1427 -0
  747. package/docs/techniques/input.md +1072 -0
  748. package/docs/techniques/loaders-packers.md +768 -0
  749. package/docs/techniques/logic.md +3091 -0
  750. package/docs/techniques/maths.md +1524 -0
  751. package/docs/techniques/memory-banking.md +1686 -0
  752. package/docs/techniques/music-sid.md +1144 -0
  753. package/docs/techniques/raster.md +1543 -0
  754. package/docs/techniques/scroll.md +1292 -0
  755. package/docs/techniques/sprite.md +2566 -0
  756. package/docs/techniques/text-mode-render.md +717 -0
  757. package/docs/techniques/text.md +1095 -0
  758. package/docs/techniques/transitions.md +405 -0
  759. package/docs/toolchains/cartconv-reference.md +262 -0
  760. package/docs/toolchains/cc1541-reference.md +316 -0
  761. package/docs/toolchains/cc65-reference.md +667 -0
  762. package/docs/toolchains/error-sources/cc65/README.md +1 -0
  763. package/docs/toolchains/error-sources/cc65/missing-setcpu.s +4 -0
  764. package/docs/toolchains/error-sources/cc65/range-error.s +4 -0
  765. package/docs/toolchains/error-sources/cc65/segment-overflow.c +6 -0
  766. package/docs/toolchains/error-sources/cc65/unresolved-external.c +6 -0
  767. package/docs/toolchains/error-sources/kickassembler/README.md +1 -0
  768. package/docs/toolchains/error-sources/kickassembler/branch-out-of-range.asm +4 -0
  769. package/docs/toolchains/error-sources/kickassembler/failed-assert.asm +3 -0
  770. package/docs/toolchains/error-sources/kickassembler/flat-lib.asm +4 -0
  771. package/docs/toolchains/error-sources/kickassembler/import-source-no-namespace.asm +5 -0
  772. package/docs/toolchains/error-sources/kickassembler/macro-without-parentheses.asm +6 -0
  773. package/docs/toolchains/error-sources/kickassembler/memory-block-overlap.asm +4 -0
  774. package/docs/toolchains/error-sources/kickassembler/symbol-clash-lib.asm +5 -0
  775. package/docs/toolchains/error-sources/kickassembler/symbol-clash-main.asm +6 -0
  776. package/docs/toolchains/error-sources/kickassembler/unknown-symbol.asm +4 -0
  777. package/docs/toolchains/error-sources/oscar64/README.md +1 -0
  778. package/docs/toolchains/error-sources/oscar64/asm-addressing-mode.c +7 -0
  779. package/docs/toolchains/error-sources/oscar64/region-overflow.c +6 -0
  780. package/docs/toolchains/error-sources/oscar64/undefined-extern-var.c +5 -0
  781. package/docs/toolchains/error-sources/oscar64/undefined-extern.c +6 -0
  782. package/docs/toolchains/error-sources/oscar64/unknown-vic-field.c +6 -0
  783. package/docs/toolchains/error-sources/oscar64/void-main.c +3 -0
  784. package/docs/toolchains/kickassembler-reference.md +1020 -0
  785. package/docs/toolchains/memory-layout-planning.md +362 -0
  786. package/docs/toolchains/oscar64-headers-reference.md +583 -0
  787. package/docs/toolchains/oscar64-reference.md +892 -0
  788. package/docs/toolchains/petcat-reference.md +290 -0
  789. package/docs/toolchains/png2prg.md +188 -0
  790. package/docs/toolchains/release-disk.md +314 -0
  791. package/docs/toolchains/sidreloc.md +236 -0
  792. package/docs/toolchains/spindle-reference.md +487 -0
  793. package/docs/toolchains/tape-mastering.md +592 -0
  794. package/docs/toolchains/unit-testing-6502.md +281 -0
  795. package/docs/workflow/agent-harness.md +478 -0
  796. package/package.json +105 -0
  797. package/templates/_harness/CLAUDE.md.template +91 -0
  798. package/templates/_harness/PLAN.md.template +72 -0
  799. package/templates/_harness/check.py +442 -0
  800. package/templates/_harness/claude/settings.json +17 -0
  801. package/templates/_harness/gen-asm-header.py +96 -0
  802. package/templates/_harness/harness.mk +273 -0
  803. package/templates/_harness/hooks/plan-gate.py +185 -0
  804. package/templates/_harness/meter/frame_meter.asm +311 -0
  805. package/templates/_harness/meter/frame_meter.c +155 -0
  806. package/templates/_harness/meter/frame_meter.h +80 -0
  807. package/templates/_harness/zp-used.py +104 -0
  808. package/templates/action-puzzle/.claude/settings.json +17 -0
  809. package/templates/action-puzzle/.mcp.json +10 -0
  810. package/templates/action-puzzle/CLAUDE.md +120 -0
  811. package/templates/action-puzzle/Makefile +125 -0
  812. package/templates/action-puzzle/PLAN.md +388 -0
  813. package/templates/action-puzzle/README.md +240 -0
  814. package/templates/action-puzzle/expect.json +25 -0
  815. package/templates/action-puzzle/src/cave.c +280 -0
  816. package/templates/action-puzzle/src/cave.h +80 -0
  817. package/templates/action-puzzle/src/gen_autopilot.h +20 -0
  818. package/templates/action-puzzle/src/gen_caves.h +59 -0
  819. package/templates/action-puzzle/src/gen_notes.h +16 -0
  820. package/templates/action-puzzle/src/hiscore.c +152 -0
  821. package/templates/action-puzzle/src/hiscore.h +41 -0
  822. package/templates/action-puzzle/src/level.c +64 -0
  823. package/templates/action-puzzle/src/level.h +24 -0
  824. package/templates/action-puzzle/src/main.c +599 -0
  825. package/templates/action-puzzle/src/render.c +239 -0
  826. package/templates/action-puzzle/src/render.h +31 -0
  827. package/templates/action-puzzle/src/sound.c +158 -0
  828. package/templates/action-puzzle/src/sound.h +22 -0
  829. package/templates/action-puzzle/tools/disk_check.py +77 -0
  830. package/templates/action-puzzle/tools/drive.py +106 -0
  831. package/templates/action-puzzle/tools/gen.py +454 -0
  832. package/templates/action-puzzle/tools/model_check.py +49 -0
  833. package/templates/action-puzzle/tools/verdict_codes.py +24 -0
  834. package/templates/adventure/.claude/settings.json +17 -0
  835. package/templates/adventure/.mcp.json +10 -0
  836. package/templates/adventure/CLAUDE.md +118 -0
  837. package/templates/adventure/Makefile +113 -0
  838. package/templates/adventure/PLAN.md +396 -0
  839. package/templates/adventure/README.md +277 -0
  840. package/templates/adventure/expect.json +25 -0
  841. package/templates/adventure/src/engine.c +543 -0
  842. package/templates/adventure/src/engine.h +48 -0
  843. package/templates/adventure/src/gen_script.h +141 -0
  844. package/templates/adventure/src/gen_world.h +511 -0
  845. package/templates/adventure/src/main.c +439 -0
  846. package/templates/adventure/src/picture.c +83 -0
  847. package/templates/adventure/src/picture.h +16 -0
  848. package/templates/adventure/src/save.c +159 -0
  849. package/templates/adventure/src/save.h +25 -0
  850. package/templates/adventure/src/sound.c +71 -0
  851. package/templates/adventure/src/sound.h +16 -0
  852. package/templates/adventure/src/text.c +349 -0
  853. package/templates/adventure/src/text.h +40 -0
  854. package/templates/adventure/tools/disk_check.py +128 -0
  855. package/templates/adventure/tools/drive.py +119 -0
  856. package/templates/adventure/tools/gen.py +1033 -0
  857. package/templates/adventure/tools/world.py +356 -0
  858. package/templates/beat-em-up/.claude/settings.json +17 -0
  859. package/templates/beat-em-up/.mcp.json +10 -0
  860. package/templates/beat-em-up/CLAUDE.md +136 -0
  861. package/templates/beat-em-up/Makefile +96 -0
  862. package/templates/beat-em-up/PLAN.md +502 -0
  863. package/templates/beat-em-up/README.md +315 -0
  864. package/templates/beat-em-up/expect-gameover.json +8 -0
  865. package/templates/beat-em-up/expect.json +29 -0
  866. package/templates/beat-em-up/src/anim.c +70 -0
  867. package/templates/beat-em-up/src/art.c +344 -0
  868. package/templates/beat-em-up/src/autopilot.h +154 -0
  869. package/templates/beat-em-up/src/brute.c +273 -0
  870. package/templates/beat-em-up/src/enemy.c +281 -0
  871. package/templates/beat-em-up/src/engine.asm +512 -0
  872. package/templates/beat-em-up/src/fighter.c +384 -0
  873. package/templates/beat-em-up/src/game.h +269 -0
  874. package/templates/beat-em-up/src/hud.c +135 -0
  875. package/templates/beat-em-up/src/main.c +341 -0
  876. package/templates/beat-em-up/src/sound.c +44 -0
  877. package/templates/beat-em-up/src/street.c +93 -0
  878. package/templates/beat-em-up/src/verdict.h +523 -0
  879. package/templates/beat-em-up/src/view.c +362 -0
  880. package/templates/beat-em-up/tools/drive.py +105 -0
  881. package/templates/beat-em-up/tools/flickercheck.py +273 -0
  882. package/templates/demo/.claude/settings.json +17 -0
  883. package/templates/demo/.mcp.json +10 -0
  884. package/templates/demo/CLAUDE.md +129 -0
  885. package/templates/demo/Makefile +66 -0
  886. package/templates/demo/PLAN.md +401 -0
  887. package/templates/demo/README.md +322 -0
  888. package/templates/demo/expect.json +945 -0
  889. package/templates/demo/src/config.asm +80 -0
  890. package/templates/demo/src/framework.asm +390 -0
  891. package/templates/demo/src/main.asm +139 -0
  892. package/templates/demo/src/music.asm +197 -0
  893. package/templates/demo/src/part_main.asm +279 -0
  894. package/templates/demo/src/part_title.asm +79 -0
  895. package/templates/demo/src/parts.asm +43 -0
  896. package/templates/demo/src/tables.asm +73 -0
  897. package/templates/demo/src/verdict.asm +179 -0
  898. package/templates/demo/tools/audio.py +80 -0
  899. package/templates/demo/tools/gen_expect.py +167 -0
  900. package/templates/demo/tools/probe.py +95 -0
  901. package/templates/hello/.claude/settings.json +17 -0
  902. package/templates/hello/.mcp.json +10 -0
  903. package/templates/hello/CLAUDE.md +100 -0
  904. package/templates/hello/Makefile +38 -0
  905. package/templates/hello/PLAN.md +200 -0
  906. package/templates/hello/expect.json +20 -0
  907. package/templates/hello/src/main.c +161 -0
  908. package/templates/hello/src/sprite.asm +27 -0
  909. package/templates/hello-kick/.claude/settings.json +17 -0
  910. package/templates/hello-kick/.mcp.json +10 -0
  911. package/templates/hello-kick/CLAUDE.md +100 -0
  912. package/templates/hello-kick/Makefile +17 -0
  913. package/templates/hello-kick/PLAN.md +199 -0
  914. package/templates/hello-kick/expect.json +20 -0
  915. package/templates/hello-kick/src/main.asm +233 -0
  916. package/templates/platformer/.claude/settings.json +17 -0
  917. package/templates/platformer/.mcp.json +10 -0
  918. package/templates/platformer/CLAUDE.md +125 -0
  919. package/templates/platformer/Makefile +86 -0
  920. package/templates/platformer/PLAN.md +360 -0
  921. package/templates/platformer/README.md +277 -0
  922. package/templates/platformer/expect.json +32 -0
  923. package/templates/platformer/src/actors.c +289 -0
  924. package/templates/platformer/src/anim.c +51 -0
  925. package/templates/platformer/src/art.c +212 -0
  926. package/templates/platformer/src/autopilot.h +30 -0
  927. package/templates/platformer/src/engine.asm +253 -0
  928. package/templates/platformer/src/game.h +248 -0
  929. package/templates/platformer/src/hud.c +90 -0
  930. package/templates/platformer/src/level.c +184 -0
  931. package/templates/platformer/src/main.c +306 -0
  932. package/templates/platformer/src/player.c +268 -0
  933. package/templates/platformer/src/sound.c +42 -0
  934. package/templates/platformer/src/verdict.h +206 -0
  935. package/templates/platformer/src/view.c +331 -0
  936. package/templates/platformer/stage-expect.json +6 -0
  937. package/templates/platformer/tools/drive.py +112 -0
  938. package/templates/platformer/tools/tearcheck.py +229 -0
  939. package/templates/shmup-vertical/.claude/settings.json +17 -0
  940. package/templates/shmup-vertical/.mcp.json +10 -0
  941. package/templates/shmup-vertical/CLAUDE.md +137 -0
  942. package/templates/shmup-vertical/Makefile +131 -0
  943. package/templates/shmup-vertical/PLAN.md +569 -0
  944. package/templates/shmup-vertical/README.md +189 -0
  945. package/templates/shmup-vertical/expect.json +38 -0
  946. package/templates/shmup-vertical/src/bullets.c +158 -0
  947. package/templates/shmup-vertical/src/bullets.h +35 -0
  948. package/templates/shmup-vertical/src/display.c +239 -0
  949. package/templates/shmup-vertical/src/display.h +40 -0
  950. package/templates/shmup-vertical/src/game.h +85 -0
  951. package/templates/shmup-vertical/src/glyph.asm +358 -0
  952. package/templates/shmup-vertical/src/hiscore.c +141 -0
  953. package/templates/shmup-vertical/src/hiscore.h +16 -0
  954. package/templates/shmup-vertical/src/hit.asm +180 -0
  955. package/templates/shmup-vertical/src/hitbox.c +59 -0
  956. package/templates/shmup-vertical/src/hitbox.h +25 -0
  957. package/templates/shmup-vertical/src/kernel.asm +330 -0
  958. package/templates/shmup-vertical/src/level.c +174 -0
  959. package/templates/shmup-vertical/src/level.h +27 -0
  960. package/templates/shmup-vertical/src/main.c +824 -0
  961. package/templates/shmup-vertical/src/mux.asm +321 -0
  962. package/templates/shmup-vertical/src/sound.asm +255 -0
  963. package/templates/shmup-vertical/src/step.asm +169 -0
  964. package/templates/shmup-vertical/src/waves.c +195 -0
  965. package/templates/shmup-vertical/src/waves.h +33 -0
  966. package/templates/shmup-vertical/stage-expect.json +6 -0
  967. package/templates/shmup-vertical/tools/drive.py +152 -0
  968. package/templates/shmup-vertical/tools/joytest.py +159 -0
  969. package/templates/shmup-vertical/tools/meter.py +15 -0
  970. package/templates/shmup-vertical/tools/phases.py +73 -0
@@ -0,0 +1,1543 @@
1
+ ---
2
+ category: raster
3
+ chip: VIC-II
4
+ ---
5
+
6
+ <!-- doc-type: technique-reference -->
7
+
8
+ # Raster Techniques
9
+
10
+ The VIC-II's raster counter is not merely a diagnostic readout — it is the primary synchronization primitive for every visual effect on the C64. The chip advances through 312 scanlines per frame on PAL (263 on NTSC), and the CPU can receive an interrupt the moment that counter matches a programmed compare value. That single mechanism, when exploited precisely, allows software to reconfigure VIC-II registers mid-frame: changing colors, switching display modes, adjusting scrolling offsets, repositioning sprites, or opening the hardware borders. Without raster control, the C64 renders one static screen per frame like any unadorned character terminal. With it, the same 1 MHz CPU can drive a completely different visual setup on every single scanline if the coder is willing to account for every cycle.
11
+
12
+ The discipline required is severe. The VIC-II reads its registers continuously and asynchronously — writes take effect on the current dot clock cycle, not at a "safe" point in the frame. Raster compare IRQs fire with 0–6 cycles of jitter due to the variable instruction-completion behavior of the 6510 (an earlier version of this sentence said 1-2). Badlines steal 40-43 cycles per line from the CPU (plan on 43) without warning unless the coder explicitly tracks them. Opening the side borders requires a write whose write cycle is one specific cycle of the 63. Every technique in this document exists because the raw mechanism is too imprecise or too resource-hungry on its own, and the C64 demo tradition has refined ways to work around each limitation. Stable raster IRQ is the foundation on which all others rest.
13
+
14
+ ---
15
+
16
+ ## stable_raster_irq — Stable raster IRQ
17
+
18
+ **Complexity:** medium
19
+ **Region:** both
20
+ **Uses registers:** SCROLY, RASTER, VICIRQ, IRQMSK
21
+ **Demands:** midframe_raster_irqs
22
+ **Cost:** cycles_per_frame=124, lines_active=2, irq_slots=1, zp_bytes=0
23
+ **Cost basis:** arithmetic
24
+ **Claims:** vic_raster_irq (shares)
25
+ **Claims basis:** derived-listing
26
+
27
+ ### Why
28
+
29
+ An ordinary raster IRQ is generated when the VIC-II's internal raster counter matches the 9-bit compare value built from $D012 (low 8 bits) and bit 7 of $D011 (RST8, the 9th bit). The IRQ line goes low, the CPU finishes its current instruction, and then begins the interrupt service sequence (7 cycles: two dummy-read cycles, push PC high, push PC low, push P, fetch vector low, fetch vector high; the handler's first opcode fetch follows — an earlier version listed "fetch PC high, fetch PC low" as steps, which they are not: the PC is pushed, not fetched). The problem is "finishes its current instruction." The 6510 instruction set has variable-length execution: a simple `LDA #imm` completes in 2 cycles while a `STA ($zp,X)` takes 6. When an IRQ fires mid-instruction the CPU waits out the remainder of that instruction before responding. This means the gap between the VIC raising its IRQ line and the first instruction of the handler executing is not fixed — it varies by however many cycles were left in the interrupted instruction, anywhere from 0 to 6 cycles in practice (most instructions are 2-6 cycles; the 7-cycle BRK is a pathological case).
30
+
31
+ Up to six cycles of jitter sounds trivial. It is not. A write to $D020 (border color) that lands one cycle early produces a visible vertical stripe at the left edge of the border. A sprite multiplex update that arrives one cycle late collides with the previous sprite's DMA window. A mode-switch that jitters by a cycle can corrupt the first row of the display. Stable raster IRQ eliminates this jitter entirely. It is the prerequisite for every technique in this document that requires cycle-exact register writes.
32
+
33
+ ### How
34
+
35
+ The standard stable raster IRQ technique uses an initial IRQ set one line before the target line. This first IRQ fires, re-acknowledges the VIC interrupt flag, programs $D012 to the actual target line, and then executes a tight busy-wait loop that reads $D012 continuously until the counter advances. When the counter matches, the handler is already spinning at a known point in the loop — the jitter has been consumed waiting. The writes that follow land at a predictable cycle offset from the line boundary.
36
+
37
+ The sequence is:
38
+
39
+ 1. Set bit 0 of $D01A (IRQMSK) to enable raster interrupts.
40
+ 2. Write the desired interrupt line number (low 8 bits) into $D012. If the line is >= 256, also set RST8 in $D011.
41
+ 3. In the IRQ handler: write $01 to $D019 (VICIRQ) to acknowledge the interrupt and clear the VIC's interrupt latch; if this is not done, the IRQ line stays low and the CPU re-enters the handler immediately after RTI.
42
+ 4. Write the next scheduled interrupt line into $D012.
43
+ 5. If sub-cycle precision is needed (double-IRQ variant), see the `double_irq` technique.
44
+
45
+ **Who owns the raster compare.** A stable raster IRQ is a way into a handler, not an effect. The effect that runs in the handler (raster bars, an FLI display, an open border, a multiplexer zone) owns the compare; this technique is how that handler is entered. Its Claims line therefore says `shares`: two effects that each use a stable entry still contend for the one compare, and a stable entry inside an effect's own handler does not.
46
+
47
+ The cycle-exact busy-wait variation uses two NOP instructions of known cycle count inserted after the $D012 write to absorb the jitter window, landing the following store instructions on a predictable cycle of the target line.
48
+
49
+ ### Why it works
50
+
51
+ The VIC-II maintains an internal 9-bit raster counter. At the start of each new raster line, the hardware increments this counter and compares it against the 9-bit compare value. If they match AND the raster IRQ mask bit in $D01A bit 0 is set, the chip asserts the IRQ line on the CPU's /IRQ input. The assertion happens on a fixed dot-clock cycle within the line — specifically at the start of the line's first half-cycle, which on PAL corresponds to cycle 1 of the 63-cycle line.
52
+
53
+ The CPU sees the /IRQ pin go low and responds after completing its current instruction. This variable completion time is the source of jitter. The stable-IRQ technique removes jitter by using the raster counter itself as the synchronization point: after the jitter-introducing interrupt fires and the CPU is in the handler, the handler then polls $D012 in a tight loop. Because the raster line has not yet incremented to the next value, the loop spins for whatever fraction of a cycle budget remains. When $D012 finally increments, every subsequent instruction in the handler runs at a fixed cycle offset from that increment — jitter eliminated.
54
+
55
+ The re-acknowledge step (write $01 to $D019) is critical. $D019 bit 0 is the raster interrupt flag. It is set by the VIC when the interrupt fires and cleared by writing a 1 to that bit (the register uses write-1-to-clear semantics, similar to CIA interrupt clearing). If the flag is not cleared, the VIC continues asserting /IRQ and the CPU re-enters the handler immediately after RTI. A common defensive pattern also reads $D019 before writing it, to check which interrupt source fired, though in a single-source setup the read is skippable.
56
+
57
+ ### Variations
58
+
59
+ **Single IRQ with NOP pad.** For effects that only need 0-1 cycle precision, the simplest approach is: fire the IRQ, acknowledge, write a few NOPs of known total cycle count, then perform the register writes. The NOP padding absorbs worst-case jitter without a polling loop. This works when the desired action can tolerate 1-cycle imprecision.
60
+
61
+ **Double IRQ.** When zero jitter is required, use two IRQs on adjacent lines. The first IRQ sets up the second; the second uses a tightly-counted busy-wait-then-NOP sequence to land on cycle 1 of the target line. See the `double_irq` technique for the full protocol.
62
+
63
+ **Interrupt vector placement.** With the KERNAL ROM in, the hardware vector at $FFFE/$FFFF points at the KERNAL dispatcher at $FF48, which pushes A, X and Y, checks for BRK and jumps through $0314/$0315: 29 cycles before the first instruction of whatever $0314 points at. Patching $0314 is the normal way in and pays all 29. Banking the KERNAL out and pointing $FFFE/$FFFF at the handler removes the dispatcher, leaving the 7-cycle interrupt sequence plus whatever registers the handler saves itself, at the cost of servicing CIA interrupts and the keyboard yourself.
64
+
65
+ **NMI-based raster timing.** Some advanced techniques use the CIA2 timer firing an NMI for raster work to avoid contention with the IRQ chain. Outside scope of this document — see CIA2 reference.
66
+
67
+ ### Cycle budget
68
+
69
+ On PAL (63 cycles/line), the accounting is:
70
+
71
+ - VIC pulls /IRQ low at the start of cycle 1 of the target line (cycle 2 for line 0). An earlier version said "cycle 0"; cycle numbering in this knowledge base starts at 1.
72
+ - CPU finishes current instruction: 0-6 cycles of jitter consumed here.
73
+ - CPU executes interrupt sequence (7 cycles): two dummy-read cycles, push PC high, push PC low, push P, fetch vector low, fetch vector high; the handler's first opcode fetch follows. (An earlier version counted the handler's first fetch inside the 7; it is the handler's own first cycle.)
74
+ - Entry path: with the KERNAL banked out and $FFFE/$FFFF pointing at the handler, nothing more; through the KERNAL vector, the $FF48 dispatcher adds 29 cycles before the first instruction at $0314 (see Interrupt vector placement).
75
+ - Handler entry overhead (LDA/STA for acknowledgment): 6-8 cycles.
76
+ - Total from IRQ assertion to first usable write: approximately 13-21 cycles with the KERNAL out, 42-50 through $0314 (handler entered on cycle 37-43; measured in VICE, `recipes/kickassembler/stable-raster-irq.md`). An earlier version gave only 13-21 and did not name the entry path.
77
+
78
+ Once the busy-wait has synced to the next line boundary the budget on that line is ~55 cycles whichever way the interrupt was entered — the entry cost is paid on the arming line, which is why the IRQ is set one line early. Without a sync, on the arming line itself, about 50 cycles remain with $FFFE pointing at the handler (KERNAL out) and about 20 through $0314 (handler entered on cycle 37-43; see Interrupt vector placement). An earlier version gave 40-50 without saying which entry path. On NTSC (65 cycles/line), the budget is 2 cycles wider per line.
79
+
80
+ Badlines cost 40-43 cycles of CPU stall within the line (plan on 43; see `badline_synchronization`). A handler that fires on a badline loses those cycles before any stores execute. The standard defense is to target the IRQ one line before the badline, perform the writes during that non-bad line, and let the badline pass without stores.
81
+
82
+ ### Recipes
83
+
84
+ - `recipes/oscar64/stable-raster-irq.md`
85
+ - `recipes/kickassembler/stable-raster-irq.md`
86
+
87
+ ---
88
+
89
+ ## raster_bars — Raster color bars
90
+
91
+ **Complexity:** low
92
+ **Region:** both
93
+ **Uses registers:** EXTCOL, BGCOL0, RASTER, VICIRQ
94
+ **Demands:** midframe_raster_irqs
95
+ **Cost:** cycles_per_frame=990, lines_active=10, irq_slots=10, bytes_code=600
96
+ **Cost basis:** estimated
97
+ **Claims:** vic_raster_irq (owns)
98
+ **Claims basis:** derived-listing
99
+
100
+ ### Why
101
+
102
+ The raster color bar effect — horizontal bands of color cycling down the screen — is the "hello world" of raster programming. It demonstrates that the VIC-II reads $D020 (border color) and $D021 (background color 0) continuously and reflects any change immediately on the current raster line. A palette of 16 colors times up to 200+ raster lines per frame means a wide design space with minimal hardware cost: the CPU is simply writing two bytes per line.
103
+
104
+ Beyond its introductory status, raster bars are a practical building block. Status bars in games typically change the background color at the display mode boundary. Gradient fills simulate additional colors. Parallax bars suggest depth. The technique also serves as a timing diagnostic: if a bar bleeds across lines, a cycle-exact write is arriving late.
105
+
106
+ ### How
107
+
108
+ The basic sequence in an IRQ handler is:
109
+
110
+ 1. Write the new border color to $D020.
111
+ 2. Write the new background color to $D021.
112
+ 3. Advance $D012 to the next target line (the next bar boundary) and acknowledge $D019.
113
+ 4. Return from interrupt (RTI).
114
+
115
+ For a smooth color gradient covering many lines, a more efficient approach is to chain multiple IRQs in a ring: each handler writes the current line's colors, advances $D012 by N lines, and exits. The IRQ fires again N lines later for the next color in the palette. This chaining pattern is the foundation for all multi-split raster effects.
116
+
117
+ For an effect that changes color on every single line (a "rainbow" effect), the handler must write $D020/$D021 in fewer than 63 cycles total — feasible without badline interference, but the 40-43-cycle badline stall (plan on 43) makes single-line changes on badline rows require special handling (typically by writing the badline's color in the preceding line's IRQ, since the CPU is stalled on the badline itself).
118
+
119
+ ### Why it works
120
+
121
+ The VIC-II reads $D020 and $D021 once per dot-clock cycle during horizontal raster generation. The color value fetched at any given dot position determines the color of that dot. Because the 6510 and VIC-II share the bus via the phi1/phi2 clock scheme, a CPU write to $D020 takes effect at the phi2 edge of the write cycle — which corresponds to the dot position being generated approximately 2 clock cycles after the write. This 2-cycle offset is the same on both PAL and NTSC and is constant, making it predictable for cycle-exact color placement. Note that this latency is measured in cycles, each of which is eight pixels wide (not measured here; the pixels-per-cycle figure below is).
122
+
123
+ The key architectural fact: there is no buffering. Unlike systems with scanline-latched registers, the VIC-II's color registers are transparent — they reflect writes immediately. This means writing too late by one cycle shifts the colour change right by 8 pixels — one CPU cycle is eight dots on both PAL and NTSC (dot clock / CPU clock = 8.000; measured in VICE x64sc: a colour toggled every 4 cycles gave a 64-pixel period) — not onto the next line. The visible effect of a 1-cycle timing error is an 8-pixel step in the seam, not a full-line displacement. An earlier version of this paragraph said "2 dots", probably by confusing the 2-cycle latency above with a pixel count.
124
+
125
+ ### Variations
126
+
127
+ **Background only.** Write $D021 only, leaving $D020 constant. Produces color bands within the display area without affecting the border.
128
+
129
+ **Border only.** Write $D020 only. Useful for status bars and frame decorations that must not disturb the display area.
130
+
131
+ **Gradient via lookup table.** A 16-byte (or 32-byte cycled) table of color values in zero page, indexed by a counter incremented in each IRQ entry, produces a smooth color gradient that can be shifted by adjusting the starting index — a "rotating rainbow" without any compute per frame.
132
+
133
+ **Parallax bars.** Two separate bar sequences at different speeds — one for background, one for border — shifted by different amounts each frame. Gives a depth illusion cheaply.
134
+
135
+ **Wide bars with single IRQ per bar.** Program $D012 to the start of each bar region. Within the bar, use NOP padding or a per-line loop to maintain the color for the bar's height. More efficient for tall bars than firing one IRQ per line.
136
+
137
+ ### Cycle budget
138
+
139
+ Through $0314 the handler is entered on cycle 37-43 (`recipes/kickassembler/raster-bars.md`, measured in VICE), leaving about 20 cycles on the line — enough for the two colour stores (8 cycles, two `STA abs` at 4 each) and the $D012/$D019 bookkeeping (10) and little else; with the KERNAL out and $FFFE pointing at the handler about 50 remain. An earlier version said 50-55 usable and did not name the entry path. On a badline, the 40-43-cycle stall removes almost all work budget; designs that change color on badline rows typically write the color value one line early.
140
+
141
+ ### Recipes
142
+
143
+ - `recipes/oscar64/raster-bars.md`
144
+ - `recipes/kickassembler/raster-bars.md`
145
+
146
+ ---
147
+
148
+ ## badline_synchronization — Badline synchronization
149
+
150
+ **Complexity:** high
151
+ **Region:** both
152
+
153
+ **Uses registers:** SCROLY, RASTER
154
+ **Demands:** midframe_raster_irqs
155
+
156
+ ### Why
157
+
158
+ The badline is not optional — it is the price the VIC-II extracts from the CPU eight times per character row. During a badline, the VIC-II needs to fetch the full 40-character screen codes for the current text row. It does this by pulling BA (Bus Available) low on cycle 12 and taking the phi2 bus for the 40 c-accesses on cycles 15-54. The CPU may only complete write cycles on 12-14 and cannot read again until cycle 55, so the stall it sees is 40-43 cycles — 43 for any ordinary instruction stream (measured 43 for a NOP stream, 42-43 for STA zp in VICE), 40 only when three consecutive write cycles happen to fall on 12-14. Plan on 43 lost and 20 left (63 - 43) on PAL, 22 on NTSC. An earlier version of this paragraph said BA was low "for 40 cycles" and called the stall 40; that counts only the c-access cycles.
159
+
160
+ The cycle loss is large enough to invalidate any timing assumption made without accounting for it. A raster effect that writes 8 registers per line works on non-bad lines but silently slips by 40-43 cycles on badlines, producing visible glitches. Badline synchronization is the practice of knowing exactly which lines are bad (and therefore where the stalls fall), structuring IRQ handlers to either avoid critical writes on bad lines or explicitly account for the 40-43-cycle deduction (plan on 43) when they must happen on one.
161
+
162
+ ### How
163
+
164
+ The first step is understanding when badlines occur. A badline fires when two conditions are simultaneously true: the display is enabled (DEN, $D011 bit 4, was set at some point during raster line $30 — any cycle of that line arms badlines for the frame; an earlier version said cycle 14 specifically), and the low three bits of the current raster line number equal the YSCROLL value in $D011 bits 2-0. With the default YSCROLL of 3, badlines occur at raster lines 51, 59, 67, ... 243 — every 8th line starting at $33, for a total of 25 badlines per frame on PAL.
165
+
166
+ Because badlines are deterministic given a fixed YSCROLL, the coder can build a lookup table of "line type" (bad vs non-bad) and schedule IRQ handlers to fire only on non-bad lines when cycle budgets are tight. Alternatively, a handler that fires on every line can subtract 43, not 40, from its available cycle count on badlines (63 - 43 = 20) and ensure only that many cycles of work are attempted.
167
+
168
+ The second control lever is YSCROLL itself. Writing a new YSCROLL value to $D011 bits 2-0 changes which lines are bad. This is dangerous to do mid-frame without care — see the `raster_split_modes` technique — but it can be used deliberately to move the badline cluster away from a critical raster window.
169
+
170
+ A third approach used in scene-quality code is to account for badlines at assembly time by building the raster handler as a sequence of cycle-counted instruction blocks, with alternate blocks for bad and non-bad variants selected by a look-up at handler entry.
171
+
172
+ ### Why it works
173
+
174
+ The VIC-II needs character codes to generate text-mode output. The chip cannot display any character without knowing which character is in each of the 40 cells of the current row. It fetches these from screen RAM (video matrix), which lives in the VIC bank and is not accessible during the CPU's phi2 cycles — the VIC needs the bus to itself. The chip raises BA (bus available) signal three cycles before it actually needs the bus. The CPU, seeing BA low, knows it cannot issue further memory accesses but completes any instruction that has no remaining memory cycles. After 3 cycles, the VIC takes the phi2 bus for 40 cycles of screen RAM fetch, then releases it. The CPU resumes.
175
+
176
+ The timing is locked to the YSCROLL field because the VIC increments its internal row counter on each badline. The row counter increments when `(current_raster_line & 7) == YSCROLL`. The first badline of a frame must occur while DEN is set, or badlines are suppressed for the entire frame — a technique called "blinking DEN" that blanks the display and gives the CPU all cycles back.
177
+
178
+ NTSC behaves identically in terms of which lines are bad (same YSCROLL logic), but the cycle loss (40-43 cycles) and the available cycles per line (65 on NTSC vs 63 on PAL, so 65 - 43 = 22 left on NTSC) mean the badline penalty as a fraction of a line's budget is slightly lower on NTSC. NTSC has fewer lines per frame, but the badline window ($30–$F7) and the 25 character rows inside it do not depend on the frame length, so an NTSC frame has the same 25 badlines as PAL — 51, 59, …, 243 (measured in VICE x64sc: 2,500 stalls in 100 frames on both the PAL default, a C64C with the 8565, and the 6567R8, and none with DEN clear; an earlier version said the PAL run was a 6569). The shorter NTSC frame loses lines from the vertical blank, not from the display; per frame the CPU has fewer non-bad lines than on PAL (238 against 287), and the 40-cycle stall is a slightly smaller fraction of each 65-cycle bad line. (An earlier version of this paragraph said 24.) An earlier revision of this entry also carried a PAL-only Region tag; the technique applies to both regions, as the figures above show.
179
+
180
+ ### Variations
181
+
182
+ **Badline avoidance.** Schedule IRQs to fire on non-bad lines only. Works when the effect can tolerate one line of imprecision in its split position. Most game status bars use this — nobody notices a 1-pixel vertical shift in the status bar boundary.
183
+
184
+ **Badline accounting.** Explicitly include the 40-43-cycle stall (plan on 43) in the cycle budget for every handler that can fire on a bad line. The handler can poll $D012 at entry to determine if it is on a bad line and take one of two code paths.
185
+
186
+ **YSCROLL manipulation.** Write $D011 bits 2-0 to shift the YSCROLL value, moving badlines to a range that doesn't interfere with a critical effect window. Must be done carefully — changing YSCROLL mid-frame can cause FLD (Flexible Line Distance) effects if not done at the precise right cycle. See also `raster_split_modes`.
187
+
188
+ **DEN suppression.** Clear bit 4 of $D011 (DEN). The display blanks entirely and badlines stop firing — the CPU gets all 63/65 cycles per line. Used in raster bars that cover the entire screen or in effects where the display is generated entirely by sprites or is intentionally blanked.
189
+
190
+ ### Cycle budget
191
+
192
+ PAL, non-badline: 63 cycles total.
193
+ PAL, badline: 20 cycles guaranteed (cycles 1-11 and 55-63). The VIC pulls BA low on cycle 12 and takes the bus for its 40 c-accesses on cycles 15-54; on cycles 12-14 the CPU may only complete write cycles, so "23" is the figure for code that happens to be writing then, and 20 is the one to plan on.
194
+ NTSC, non-badline: 65 cycles total.
195
+ NTSC, badline: 22 cycles guaranteed, 25 with three write cycles.
196
+
197
+ For cycle-tight code running on every line, the badline constraint means the worst case is 20 cycles per line on PAL. Any per-line loop must complete in 20 cycles or less to be badline-safe, or must handle the bad-line case separately. Note also that a badline moves every later instruction on that line by 43 cycles (an earlier version said 40): a write planned for cycle 56 cannot be placed there at all, because no read can happen between cycles 12 and 54 and every store's write follows a read.
198
+
199
+ ### Recipes
200
+
201
+ (No standalone recipe yet — badline synchronization is a prerequisite skill embedded in stable-raster-irq and other recipes.)
202
+
203
+ ---
204
+
205
+ ## double_irq — Double IRQ for jitter elimination
206
+
207
+ **Complexity:** scene-tier
208
+ **Region:** both
209
+ **Uses registers:** RASTER, VICIRQ, IRQMSK
210
+ **Demands:** midframe_raster_irqs
211
+ **Cost:** cycles_per_frame=160, lines_active=2, irq_slots=2
212
+ **Cost basis:** arithmetic
213
+ **Cost measured on:** kickassembler-stable-raster-irq (one zero-jitter entry)
214
+ **Claims:** vic_raster_irq (shares)
215
+ **Claims basis:** derived-listing
216
+
217
+ ### Why
218
+
219
+ The stable_raster_irq technique reduces jitter to a fraction of a cycle by using a polling loop. However, the polling loop itself has a granularity of one complete loop iteration — typically 5-7 cycles. This means the "stable" IRQ synchronizes to within one loop iteration, not to within one cycle. For the most demanding scene-quality raster work — side-border opening, VSP glitch timing, hardware-sprite multiplexing at exact cycle offsets — even one or two cycles of residual jitter is unacceptable.
220
+
221
+ The double IRQ technique achieves true zero-jitter synchronization. The CPU's position on the target raster line is known to within one cycle.
222
+
223
+ ### How
224
+
225
+ The double IRQ uses two raster IRQ handlers, set on two consecutive raster lines. The first IRQ (line N) does minimal work: it re-acknowledges $D019, sets $D012 to line N+1, and exits with RTI. Because this IRQ does almost nothing, its execution is fast and repeatable. The second IRQ (line N+1) uses a precise sequence of instructions chosen to consume exactly the right number of cycles to land at a specific cycle position within line N+1, regardless of the jitter that affected the first IRQ.
226
+
227
+ The classic implementation of the second handler uses a sequence like:
228
+
229
+ - At IRQ entry, the handler immediately acknowledges $D019.
230
+ - It then executes a tight sequence of instructions with a total known cycle count, padded with NOP instructions if needed, to reach cycle C of line N+1.
231
+ - The register write that must be cycle-exact happens at cycle C.
232
+
233
+ Like `stable_raster_irq`, of which it is the zero-jitter form, this is a way into a handler: the effect the second handler runs owns the raster compare, and the Claims line says `shares`. In `recipes/kickassembler/fli-image.md` and `recipes/kickassembler/sideborder-open.md` the double IRQ is the entry of the FLI and open-border code.
234
+
235
+ The reason two IRQs work better than one: the first IRQ absorbs all the jitter from the unknown instruction-completion state at IRQ entry. By the time the first IRQ completes and the second fires, the processor is executing a known, counted instruction stream from the end of the first RTI. The second IRQ fires at a fully predictable time relative to the raster line.
236
+
237
+ ### Why it works
238
+
239
+ The 6510's interrupt response adds a 7-cycle fixed overhead once jitter is absorbed. After the first IRQ's RTI executes, the processor returns to whatever was running between IRQs (usually a tight NOP loop or a `JMP *` halt). The second IRQ fires at line N+1's line 0 cycle, the CPU finishes the current instruction (in the loop body, this is a known NOP or similar), and enters the second handler with precisely known timing.
240
+
241
+ The mathematical foundation: let `J1` be the jitter in the first IRQ (0-6 cycles). The first handler executes `K1` cycles of work. At RTI, the processor has consumed `J1 + 7 + K1` cycles since the first IRQ fired. Because the first IRQ fired near line N's boundary, RTI returns somewhere within line N or just into line N+1. The second IRQ fires at the same cycle within line N+1 — but now the CPU was executing a known instruction (NOP) between the handlers. The jitter on the second IRQ entry is determined by how far into the NOP the second IRQ arrived, which is known.
242
+
243
+ The combination of knowing the instruction between IRQs (always a NOP in the loop) and knowing the second IRQ fires at a fixed cycle within its line is what eliminates jitter entirely.
244
+
245
+ Measured form (`recipes/kickassembler/stable-raster-irq.md`, VICE x64sc): through the KERNAL vector the handler's first instruction starts on cycle 37-43 of the line (7 interrupt sequence + 29 dispatcher + 0-6 jitter), so the first handler cannot finish its setup and be sliding through NOPs before the *next* line's interrupt; it arms the second IRQ two lines down, not one. Entered from a NOP, the second handler has one cycle of residual jitter, which two consecutive reads of $D012 four cycles apart plus a `BEQ` remove: with the right padding the reads straddle the line boundary in one case and not the other, and the branch costs 3 or 2 cycles to compensate. The padding is found by measurement — the recipe's bars align at `SYNC_PAD = 11` and split into two columns at 10 or 12. Two traps: the KERNAL dispatcher executes `TSX` itself, so a stack pointer saved in X by the first handler does not survive into the second (save it in memory); and every line the two handlers and the timed code occupy must be a non-badline except the one inside the NOP slide, where a stall is harmless.
246
+
247
+ ### Variations
248
+
249
+ **NOP-padded single entry.** Some implementations fold the double-IRQ logic into a single handler that spins until the raster counter advances, then executes a counted NOP sequence to reach the target cycle. This is cleaner to write but harder to reason about cycle-exactly. The explicit two-handler approach is preferred for documentation and maintenance.
250
+
251
+ **IRQ set on same line.** A variant sets both IRQs to the same $D012 value. The first fires normally; because the VIC's raster interrupt latches immediately, the second $D012 write (to the same value) causes the IRQ to fire again on the next frame at that same line — but this requires the handler to track which "fire" it is on, typically via a flag byte.
252
+
253
+ **CIA timer second stage.** Some extreme cases use a CIA timer started in the first IRQ handler to fire a second IRQ a counted number of cycles later, decoupling the second trigger from raster line boundaries entirely. This is used for effects that require a write at a specific horizontal dot position across multiple lines.
254
+
255
+ ### Cycle budget
256
+
257
+ The double-IRQ technique's purpose is to minimize jitter rather than save cycles — it actually spends more cycles than a sloppy single IRQ. The overhead is:
258
+ - First IRQ: through $0314, 36-42 (7 interrupt sequence + 29 dispatcher + 0-6 jitter) + ~12 (ack + set $D012 + RTI) = ~48-54 cycles consumed on the arming line; with the KERNAL out, 7-13 + ~12 = ~19-25. An earlier version gave only the ~19 figure without naming the entry path.
259
+ - Between IRQs: the NOP loop in the main loop body executes for however many cycles remain in line N after the first RTI — variable but bounded.
260
+ - Second IRQ: 7 (enter) + NOP pad (0-6 cycles) + actual writes.
261
+
262
+ Total cost per raster split in double-IRQ mode: through $0314, 36-42 + ~12 on the arming line for the first handler, then 36-42 + pad for the second — about 100 cycles over three lines (the first handler arms the second IRQ two lines down, see Why it works), not 35-45 over two as an earlier version said; roughly 60 over two lines with the KERNAL out. Compare 20-30 cycles for a sloppy single-IRQ split with the KERNAL out.
263
+
264
+ ### Recipes
265
+
266
+ - `recipes/kickassembler/stable-raster-irq.md` (includes double-IRQ pattern)
267
+
268
+ ---
269
+
270
+ ## vsp_glitch — VSP (Variable Screen Position)
271
+
272
+ **Complexity:** scene-tier
273
+ **Region:** both
274
+
275
+ **Uses registers:** SCROLY, VMCSB
276
+ **Demands:** midframe_raster_irqs
277
+
278
+ ### Why
279
+
280
+ The VIC-II's character-mode display is generated by fetching screen codes from a 40-column video matrix. The VIC fetches one row of 40 characters per character row during badlines, assembling pixel data from character ROM or character RAM to fill the 8 pixel rows between badlines. This architecture means horizontal scrolling can be accomplished by adjusting $D016 bits 2-0 (XSCROLL) to shift the display by up to 7 pixels, but going further — shifting by more than 7 pixels — requires changing the start address of the video matrix, which only takes effect at the beginning of the next character row.
281
+
282
+ VSP (Variable Screen Position) is a hardware glitch, not a designed feature, that allows mid-character-row screen address changes to take effect immediately by manipulating when the VIC-II thinks a new character row is beginning. The result is pixel-perfect horizontal scrolling independent of the 7-pixel XSCROLL limit, and it requires no extra memory or display-mode changes — it works within standard text mode.
283
+
284
+ ### How
285
+
286
+ VSP is a $D011 trick — the CSEL toggle described here before belongs to
287
+ `sideborder_open`. On the line that is about to be a badline for a
288
+ character row, arrange for the badline condition to be *false* in cycle 14
289
+ (YSCROLL not equal to `line & 7` at that moment), then at a chosen cycle
290
+ between 15 and 53 write $D011 with YSCROLL = `line & 7`, so the condition
291
+ becomes true late. The VIC starts its c-accesses three cycles after BA drops,
292
+ from whichever column slot the beam has reached, and the columns before it
293
+ are not fetched for this row. Because the video counter VC advances only by
294
+ the number of c-accesses actually performed, the row ends with VC short by
295
+ that many characters, and every row after it — and every frame after it,
296
+ until the counter is re-based — starts that many characters earlier in
297
+ screen RAM. The display has moved left by N characters, N being the cycle
298
+ the condition became true minus 15. One cycle-exact write per character row,
299
+ plus a matching adjustment of the screen base, scrolls the whole screen by
300
+ whole characters at no per-line cost; XSCROLL still handles the seven pixel
301
+ steps in between.
302
+
303
+ ### Why it works
304
+
305
+ The VIC loads VC from VCBASE and clears VMLI in cycle 14, and only then; a
306
+ badline condition that becomes true later leaves those alone and starts the
307
+ c-access sequence mid-row, with VMLI counting from where the accesses start.
308
+ Christian Bauer's VIC-II article documents this as "DMA delay" (§3.14.6),
309
+ and it is the same mechanism FLI uses to lose its three leftmost columns —
310
+ VSP uses it to lose N columns and keep the offset.
311
+
312
+ The write cycle is N, so the write has to be placed from a stable raster
313
+ entry. The technique is not PAL-specific.
314
+
315
+ **The VSP crash.** On some machines the VSP write corrupts RAM. Linus
316
+ Åkesson's "Safe VSP" article (2013) traced it to DRAM metastability and
317
+ gives the rule a programmer can use: call every address ending in `$7` or
318
+ `$F` fragile; during a VSP, each bit of a fragile byte may take the value of
319
+ the same bit in another fragile byte of the same 256-byte page. No other
320
+ address is affected. It offers three workarounds: make every fragile byte in
321
+ a page identical (all `$EA` in code, a blank bottom line in each character of
322
+ a font); leave the fragile bytes unused, skipping them in code with `$80`
323
+ (NOP immediate) and leaving gaps in data; or keep safe copies of data that
324
+ cannot have gaps, such as graphics, and restore from them continuously. The
325
+ article gives no way to detect a susceptible machine. It says the timing
326
+ depends on temperature, VIC revision, trace capacitance and resistance,
327
+ power-supply ripple, and the colour carrier's phase against the dot clock,
328
+ which is set at random at power-on. A test at start cannot therefore show a
329
+ machine is safe (an inference from those factors, not measured here).
330
+ Kodiak64 draws the same conclusion ("no automated VSP vulnerability
331
+ detection routine makes much sense") and puts the cost of the gap method in
332
+ code at "128 NOPs ... per 1K of executable code", 12.5 % of the code (his
333
+ arithmetic, not measured here). VICE x64sc 3.10 can emulate the corruption
334
+ (`-VICIIvspbug`, "Enable VSP bug emulation" in its `-help`) and logs "VSP
335
+ Bug: safe channels are: ...". (An earlier version of this paragraph said
336
+ the crash depended on the DRAM chips and not the VIC revision, that Safe
337
+ VSP showed how to detect susceptible machines, and that productions test
338
+ for it at start; the article names the VIC revision as a factor and
339
+ describes no detection.)
340
+
341
+ ### Variations
342
+
343
+ **Whole-screen scroll.** One write per character row, on the row's badline,
344
+ plus the base-pointer adjustment. **Partial zone.** Only the rows of the
345
+ play field; rows above and below are ordinary. **Combined with XSCROLL.**
346
+ Whole characters by VSP, pixels by $D016 bits 2-0.
347
+
348
+ ### Cycle budget
349
+
350
+ One cycle-exact `STA $D011` per character row on the badline row, plus the
351
+ stable entry that positions it; the badline still costs its 40-43 cycles (plan on 43).
352
+ Nothing per line. The earlier figure of 12 cycles per line via a CSEL toggle
353
+ described the side-border mechanism, misattributed. Not yet measured in this
354
+ knowledge base — there is no VSP recipe, and the account above is from
355
+ Bauer's article and the VICE source, not from a run.
356
+
357
+ ### Recipes
358
+
359
+ (No standalone recipe yet — VSP is primarily a KickAssembler technique given its cycle-exact assembly requirements.)
360
+
361
+ ### Sources
362
+
363
+ - Linus Åkesson, "Safe VSP" (2013): https://www.linusakesson.net/scene/safevsp/index.php
364
+ - Kodiak64, "The future of VSP scrolling": https://kodiak64.co.uk/blog/future-of-VSP-scrolling
365
+
366
+ ---
367
+
368
+ ## fld_flexible_line_distance — FLD (Flexible Line Distance)
369
+
370
+ **Complexity:** high
371
+ **Region:** both
372
+
373
+ **Uses registers:** SCROLY, RASTER
374
+ **Demands:** midframe_raster_irqs
375
+ **Requires:** badline_synchronization, stable_raster_irq
376
+ **Cost:** cycles_per_line=63, lines_active=40, irq_slots=2
377
+ **Cost basis:** arithmetic
378
+ **Cost measured on:** kickassembler-fld (40 lines, the recipe's largest)
379
+
380
+ ### Why
381
+
382
+ The text display starts on the first badline of the frame, line 51 with the default YSCROLL of 3, and nothing in the register set moves it further down than YSCROLL's seven lines. FLD moves it by any number of lines. It is the oldest of the badline tricks and the parent of the rest: linecrunch, FPP and AGSP all begin with the same write.
383
+
384
+ ### How
385
+
386
+ A badline needs `(line & 7) == YSCROLL` (see `badline_synchronization`). Each line, rewrite YSCROLL so that the current line never matches. The VIC then finds no badline, fetches no character row, and stays in its idle state; the raster lines go by and the first row of text has not been drawn. Stop rewriting after N lines and the next matching line is the first badline of the frame: the whole display appears N lines lower, and its last N lines are cut off by the lower border, which does not move.
387
+
388
+ The recipe writes, on line L, the value `(L + 2) & 7`. That value differs from `L & 7`, so it does not make L a badline, and from `(L + 1) & 7`, so line L + 1 starts clean and the next write has the whole of it to land in. After the last write on line 49 + N, YSCROLL holds `(51 + N) & 7`, and line 51 + N is the first badline. The rest of the frame keeps that YSCROLL, so the rows below stay eight lines apart; the handler restores YSCROLL 3 before line 50 of the next frame.
389
+
390
+ In the gap the VIC is in idle state and its g-accesses read one fixed address, `$3FFF` in VIC bank 0 (`$39FF` with ECM set; `$7FFF`, `$BFFF`, `$FFFF` in the other banks). That byte is drawn across the 320 pixels of every gap line, bit 1 in colour 0 (black) over the background colour. A stock machine has zero there and the gap is blank; the recipe plants `%10101010` and the gap shows 160 black and 160 blue pixels on every line (measured in VICE x64sc 3.10, PAL and NTSC). That striped band is the proof that no badline occurred: a row fetch would have replaced it with characters.
391
+
392
+ ### Why it works
393
+
394
+ The badline condition is evaluated on every cycle of a line in the display window, not once. A match still in force when the row fetch is due starts it; a value that matches at the line's first cycle and is changed early enough in the line does not. The cycle at which that decision falls is not measured here; `badline_synchronization` puts BA low at cycle 12 and the c-accesses at 15-54. That sets the safe constraint: if the value in force when a line starts already differs from the line's own bits, the write during that line can land at any cycle. Measured in VICE x64sc 3.10 with the recipe's listing rebuilt to write `(L + 1) & 7` instead of `(L + 2) & 7`, so that each line begins matching: the display did not move at all, the measured first badline disagreed with the expected one and the verdict byte read `$02`. An earlier build of the same variant, writing two cycles earlier in each line, moved the display one line, so the write on line 51 landed in time there and not in the shipped build. The shift is 0 or 1 by the cycle that write lands on, never N. With `(L + 2) & 7` the display moved exactly N lines on every frame of an 8,000,000-cycle run on both models.
395
+
396
+ Given a value that is safe for the next line, the write itself may land anywhere in the current line. The recipe polls `$D012` for the line change and writes about ten to twenty cycles in; a cycle-counted 63-cycle loop from a stable raster is the classic form and works the same way, but has to be recounted at 65 cycles for NTSC. Either way the entry has to start on a known line, which is why the technique presupposes a stable raster IRQ and the badline rule.
397
+
398
+ ### Variations
399
+
400
+ **Linecrunch.** Make a badline happen and then, on the same line, rewrite YSCROLL so the row counter advances without the row being displayed; each crunched line skips one character row. The display moves up instead of down. Not measured here.
401
+
402
+ **FPP (flexible pixel position).** Rewrite YSCROLL on every line of a row so the VIC repeats or skips single pixel lines of the character data, which stretches and squashes the picture vertically. Not measured here.
403
+
404
+ **AGSP (any given screen position).** Combine FLD or linecrunch with VSP (`vsp_glitch`) for a whole-screen scroll of any distance in both axes in one frame. Not measured here.
405
+
406
+ **Border stripes.** With the top and bottom borders open (`topbottom_border_open`) the same idle fetch draws `$3FFF` there too; the byte can be changed per line for a cheap full-height pattern.
407
+
408
+ ### Cycle budget
409
+
410
+ The CPU is held for every line of the gap: the loop's work is 35 cycles per line (the six-instruction YSCROLL update, the counter and the branch) and the rest is spent polling for the next line, so the technique costs the whole line, 63 cycles on PAL and 65 on NTSC, for N lines. Measured in VICE x64sc 3.10 with CIA2 timer A from just before the first write to the end of the loop: 1,131 cycles for 18 lines on PAL (62.8 a line) and 1,423 cycles for 22 lines on NTSC (64.7 a line); the start and stop follow `$D012` polls, so the figure is within a poll's seven cycles of N times the line. The Cost line states 40 lines, the recipe's largest N. The double IRQ that enters the loop is the two slots.
411
+
412
+ ### Recipes
413
+
414
+ - `recipes/kickassembler/fld.md` — a bouncing display driven by a sine table, `$3FFF` striped, the first badline read back and checked against 51 + N each frame, PAL and NTSC.
415
+
416
+ ---
417
+
418
+ ## sideborder_open — Open the side border
419
+
420
+ **Complexity:** high
421
+ **Region:** both
422
+ **Uses registers:** SCROLX
423
+ **Demands:** cpu_every_line, constant_sprite_set, badline_free_region
424
+ **Requires:** double_irq
425
+ **Raster band:** movable (the program picks the lines; the sideborder-open recipe loops on lines 101-142)
426
+ **Cost:** cycles_per_line=63, lines_active=42, cycles_per_frame=2646, irq_slots=2, sprites_per_line=8
427
+ **Cost basis:** arithmetic
428
+ **Cost measured on:** kickassembler-sideborder-open (42 lines, eight sprites on the line)
429
+ **Claims:** sprite_0-7 (owns), vic_raster_irq (owns)
430
+ **Claims basis:** derived-listing
431
+
432
+ ### Why
433
+
434
+ The VIC-II renders the side (left and right) borders as a solid color region flanking the active display area. In the default 40-column mode (CSEL=1 in $D016 bit 3), the visible left border spans from the left edge of the screen to approximately dot position 24, and the right border spans from dot position 344 to the right edge. Hardware sprites can be positioned anywhere horizontally, including within the border area — but only if the border is suppressed. If the border is not suppressed, sprite pixels that land in the border region are occluded by the border color.
435
+
436
+ Opening the side borders means suppressing the border rendering so that sprites appear on a background-color area rather than behind a fixed-color wall. This is indispensable for sprite multiplexing that uses all 8 sprites across the full horizontal width of the screen, for 24+ sprite systems that require sprites in the border to achieve higher counts, and for any visual design that extends graphics to the display edges.
437
+
438
+ ### How
439
+
440
+ One write per line. Change CSEL ($D016 bit 3) from 1 to 0 with the write
441
+ cycle landing on cycle 56 of the line (PAL): `DEC $D016` on a value of $C8,
442
+ started on cycle 51, writes $C7 on exactly that cycle. Restore CSEL=1 any time
443
+ before the next line's cycle 55; `INC $D016` straight after does. Every line
444
+ of the region gets the write, from a loop of exactly 63 cycles per line
445
+ entered through a stable raster (`double_irq`).
446
+
447
+ Two constraints on the region. No line in it may be a badline: the VIC holds
448
+ the bus from cycle 12 to 54, no read cycle is possible in between, and every
449
+ store's write follows a read, so the write cannot be placed on cycle 56 —
450
+ either idle the character display inside the region by rewriting YSCROLL
451
+ every line so no line matches, or accept a closed border on those rows. And
452
+ if sprites are active in the region, the same sprites must be active on
453
+ every line of it: sprite DMA stalls the CPU from cycle 55 to cycle 10 of the
454
+ next line and that stall sets the loop's phase; the `DEC`'s two write cycles
455
+ on 55 and 56 fall inside the three write cycles the CPU is still allowed
456
+ after BA drops. Both are measured in
457
+ `recipes/kickassembler/sideborder-open.md`.
458
+
459
+ ### Why it works
460
+
461
+ The main border flip-flop is *set* when the beam reaches X=344 with CSEL=1
462
+ or X=335 with CSEL=0, and *reset* at X=24 or X=31 respectively, while the
463
+ vertical border flip-flop is clear. On PAL the beam is at X=335 during cycle
464
+ 55 and at X=344 during cycle 56. If CSEL is 1 at the 335 comparison and 0 at
465
+ the 344 comparison, neither sets the flip-flop; it stays clear for the rest
466
+ of the line, so the right border is not drawn, and because it was never set,
467
+ the next line's reset has nothing to do and the left border is not drawn
468
+ either. There is no separate left-border toggle and no multi-cycle window:
469
+ one write cycle, cycle 56. Earlier text here described a 23-cycle window and
470
+ a left-border write at cycle 1; neither exists.
471
+
472
+ ### Variations
473
+
474
+ **Sprites in the border.** The usual reason for the technique. X coordinates
475
+ run 0-503; the visible left border is X 480-503 then 0-23, the right border
476
+ 344-375. A sprite at X=500 straddles the wrap; X=344 starts the right
477
+ border. Both need bit 8 in $D010.
478
+
479
+ **Region-limited opening.** Open only the lines the sprites occupy so the
480
+ character display keeps its badlines everywhere else. The stable entry is
481
+ per region.
482
+
483
+ **Graphics on the same lines.** Needs the badline rows handled by other
484
+ means, since the write cannot happen on them; beyond this document.
485
+
486
+ ### Cycle budget
487
+
488
+ PAL: the border flip-flop is set at X=335 when CSEL=0 and at X=344 when CSEL=1, which the beam reaches on cycles 55 and 56. CSEL must go from 1 to 0 between those two comparisons, so the write cycle that clears it has to be cycle 56, a one-cycle window. There is no separate left-border toggle: once the flip-flop has not been set on the right, the next line's left border is not drawn either. CSEL goes back to 1 any time before the next line's cycle 55.
489
+
490
+ Per-line cost: one `DEC $D016` (6 cycles, new value written on its last cycle) and one `INC $D016` to restore, 12 cycles, plus whatever keeps the loop at exactly 63. On a badline it cannot be done at all: the CPU has no read cycle between 12 and 54, and a write on 56 needs a read on 55 at the latest, which puts the write on 58. Side-border regions are therefore badline-free (idle display, or YSCROLL rewritten each line) or accept a closed border on badline rows. With sprites active the write still lands: BA drops on cycle 55 for sprite 0, the 6510 completes up to three write cycles after BA drops, and `DEC`'s two writes are on 55 and 56. See `recipes/kickassembler/sideborder-open.md`, where this is measured.
491
+
492
+ Border-opening IRQ overhead combined with a sprite multiplex update on the same line can push the line's cycle budget into deficit on badlines. The standard mitigation is to move the sprite Y coordinate update to the preceding line.
493
+
494
+ ### Recipes
495
+
496
+ - `recipes/kickassembler/sideborder-open.md`
497
+
498
+ ---
499
+
500
+ ## dysp_side_border_sprites — DYSP: sprites at different Y in the opened side border
501
+
502
+ **Complexity:** high
503
+ **Region:** both
504
+ **Uses registers:** SCROLX, D011, D012, D000, D001, D010, D015, DC04, DC05, DC0E
505
+ **Uses kernal:** (none)
506
+ **Demands:** cpu_every_line, badline_free_region, midframe_raster_irqs
507
+ **Requires:** sideborder_open, stable_raster_irq, pal_ntsc_detection
508
+ **Raster band:** 40-200
509
+ **Cost:** cycles_per_line=63, lines_active=161, cycles_per_frame=13713, cycles_per_frame_typical=13703, irq_slots=3, sprites_per_line=3
510
+ **Cost basis:** measured-vice
511
+ **Cost measured on:** kickassembler-dysp (the 161-line band at 63 wall cycles a line, every `DEC $D016` traced on cycle 56, plus the CIA-timed table rebuild: 3,570 worst and 3,560 in 254 of 357 frames; the design's largest sprite set on one line is three)
512
+ **Claims:** sprite_0-3 (owns), vic_raster_irq (owns)
513
+ **Claims basis:** derived-listing
514
+
515
+ ### Why
516
+
517
+ `sideborder_open` demands a constant sprite set: every sprite in the
518
+ region must be on every line of it, which is why its recipe stacks all
519
+ eight at one Y. A scroller or a logo that bobs in the side border wants
520
+ the opposite, sprites at different heights that move, and on any line
521
+ the set of sprites the VIC fetches then changes with the frame. DYSP
522
+ (different Y sprite positions) is the demo staple that keeps the border
523
+ open anyway, and the sideborder trick's usual reason for existing.
524
+
525
+ ### How
526
+
527
+ The write is `sideborder_open`'s: `DEC $D016` on a value of $C8 started
528
+ on cycle 51, new value on cycle 56 (PAL), `INC $D016` afterwards, one
529
+ line at a time from a `double_irq` entry. What changes is the padding
530
+ between one `DEC` and the next. The recipe keeps a per-line table,
531
+ rebuilt every frame from the sprites' Y positions, of which sprites the
532
+ VIC fetches after that line's write (sprite s on lines Y to Y + 20), and
533
+ a sixteen-entry conversion from that set to the cycles the line must
534
+ leave unspent: a slide of six `NOP`s entered part way, plus a taken
535
+ branch for an odd cycle. Code cycles per line are 51 plus that padding,
536
+ and padding plus stall is 12 on every line, so every iteration is 63
537
+ wall cycles and the next `DEC` starts on 51 again. Between the `DEC` and
538
+ cycle 3 of the next line the loop does nothing but read, because a write
539
+ inside the stall window would go through during BA low and change the
540
+ count. YSCROLL is rewritten on every line so no line of the band is a
541
+ badline, as in the sideborder recipe. The table rebuild runs below the
542
+ band and costs 3,560 cycles a frame for four sprites over 161 lines.
543
+
544
+ The stable entry's lines must carry no sprite DMA, or the sync is
545
+ stalled by an amount that changes with the frame: the band starts at
546
+ the lowest Y any sprite reaches, so the interrupt sits above it.
547
+
548
+ ### Why it works
549
+
550
+ `hardware/vic-ii-reference.md`, Sprite DMA, states the p-access slots as
551
+ cycles 58, 60, 62, 1, 3, 5, 7, 9 for sprites 0 to 7 on PAL, that BA falls
552
+ three cycles before the first fetch, that the CPU completes up to three
553
+ write cycles after BA falls, and that with sprites 0..k active the CPU
554
+ resumes two cycles after sprite k's slot. From that, for a line whose
555
+ first fetched sprite is f and last is l: with sprite 0 in the set BA
556
+ falls on 55, the `DEC`'s writes on 55 and 56 go through, and the CPU
557
+ loses 3 + 2l cycles; without sprite 0 BA falls on 55 + 2f, after the
558
+ write, and the loss is 5 + 2(l - f); with no sprite, nothing. A line
559
+ with sprite 1 alone therefore costs five cycles and a line with sprite 0
560
+ alone three, and no per-sprite constant covers both. The recipe measured
561
+ that: a table of two cycles per sprite holds the border open on every
562
+ line whose set contains sprite 0 and loses it on the first line that does
563
+ not, three or four cycles per sprite lose it on the first sprite line,
564
+ and the set-indexed table holds it on all 150 lines of the display band
565
+ with all four sprites showing. The stall lengths themselves are inferred
566
+ from the border and the VIC page's statements, not timed per line.
567
+
568
+ ### Cycle budget
569
+
570
+ PAL: 63 cycles on every line of the band, all of them; NTSC 65, with the
571
+ `DEC` one cycle later and sprite 0's lead-in starting one cycle after the
572
+ write, so a set with sprite 0 costs 4 + 2l there. The band is 161 lines
573
+ here, 10,143 cycles a frame (arithmetic), plus the rebuild, 3,570 worst
574
+ and 3,560 typical (CIA, both models), plus the two raster interrupts'
575
+ entries. Lines 40 to 50 of the band lie in the upper border, where the
576
+ write does nothing: the visible open band is 51 to 200.
577
+
578
+ ### Variations
579
+
580
+ **All eight sprites.** The tables become eight bits wide and the largest
581
+ stall 19 cycles (the VIC page's measured figure), more than the six-`NOP`
582
+ slide can give back on a sprite-free line unless the loop's other work
583
+ moves out of it. Not built.
584
+
585
+ **Multiplexing in the border.** `sprite_multiplex_8` re-arms Y and
586
+ pointers between bands; inside a DYSP band those writes must fall
587
+ outside the stall window, and the set table must be rebuilt from the
588
+ multiplexer's per-frame plan rather than from eight fixed Y values. Not
589
+ built.
590
+
591
+ **DYSP with DYCP.** `dycp_scroller` moves characters on sines in the
592
+ display while this moves sprites on sines in the border; the badline-free
593
+ band forbids the character display here, so a combined effect needs the
594
+ DYCP rows above or below the band. Not built.
595
+
596
+ ### Pitfalls
597
+
598
+ - `badline_cycle_loss` (`pitfalls/raster-and-badline.md`): a badline
599
+ inside the band moves the write off cycle 56; the recipe rewrites
600
+ YSCROLL on every line so none occurs.
601
+ - `vic_bus_takeover_on_dma` (`pitfalls/raster-and-badline.md`): the
602
+ stall this entry is built around; the per-set table is the account of
603
+ it.
604
+ - `raster_irq_first_line_jitter` (`pitfalls/raster-and-badline.md`):
605
+ the entry is a double IRQ, and its sync lines must also be free of
606
+ sprite DMA.
607
+ - `idle_fetch_byte_shows_in_gaps` (`pitfalls/raster-and-badline.md`):
608
+ the band is badline-free and idle, so `$3FFF` is what the display
609
+ shows across it.
610
+ - `sprite_x_range_hidden_and_seam` (`pitfalls/sprite.md`): the sprites
611
+ stand at X 344, wholly under the right border by that entry's
612
+ mechanism, and are visible only because the border is open; their
613
+ top rows are under the upper border whenever Y falls below 50.
614
+ - `sprite_x_high_bit_wrong_register` (`pitfalls/sprite.md`): X 344
615
+ needs bit 8, so all four bits of `$D010` are set.
616
+
617
+ ### Sources
618
+
619
+ - `recipes/kickassembler/dysp.md`: the sweep, the write-cycle traces,
620
+ the table dump and the CIA figures.
621
+ - `hardware/vic-ii-reference.md`, Sprite DMA: the slots, BA and the
622
+ resume cycles quoted above.
623
+ - `sideborder_open` above and `recipes/kickassembler/sideborder-open.md`
624
+ for the write and the badline-free band.
625
+
626
+ ### Recipes
627
+
628
+ - `recipes/kickassembler/dysp.md` (four ring sprites at X 344 on
629
+ independent sines about Y 60, 90, 120 and 150; band 40 to 200; the
630
+ set-indexed table against a count table and a fixed delay; PAL and
631
+ NTSC pinned at frame 300)
632
+
633
+ ---
634
+
635
+ ## topbottom_border_open — Open the top/bottom border
636
+
637
+ **Complexity:** high
638
+ **Region:** both
639
+ **Uses registers:** SCROLY
640
+ **Demands:** midframe_raster_irqs
641
+ **Cost:** cycles_per_frame=132, lines_active=2, irq_slots=2
642
+ **Cost basis:** arithmetic
643
+ **Cost measured on:** kickassembler-topbottom-border-open (two handlers, without the $EA31 exit)
644
+ **Claims:** vic_raster_irq (owns)
645
+ **Claims basis:** derived-listing
646
+
647
+ ### Why
648
+
649
+ The VIC-II's top and bottom borders are solid-color regions above and below the active display rows. In 25-row mode (RSEL=1, $D011 bit 3 set), the display area spans raster lines 51-250 and the borders fill lines 16-50 (top) and 251-299 (bottom) on PAL. In 24-row mode (RSEL=0), the display area shrinks to lines 55-246 and the borders expand correspondingly.
650
+
651
+ Opening the top and bottom borders lets the whole frame, minus only the vertical blank, show sprites and the idle-state graphics byte (`$3FFF`, which can be changed per line) instead of border colour. The character or bitmap display itself does not grow — there are no badlines outside lines 48–247, so no new rows are fetched — but sprites and background colour reach every drawable line. Measured in VICE x64sc 3.10, every row of the emulator's viewport is drawable with the borders open: 272 lines on PAL and 247 on NTSC, against the display window's 200 (an earlier version of this paragraph gave "roughly 240 on PAL, approximately 192 on NTSC"). This is used for effects that require full-frame coverage: overscan demos, raster bars that extend into the borders, sprite effects that "bleed" above and below the traditional display area.
652
+
653
+ Hardware sprites can be positioned at any Y value 0-255 and will render wherever they land. Opening the top/bottom border does not enable additional sprite rendering per se — sprites already render in the border area when their Y position places them there. What border opening does is suppress the border color so that the background color shows through instead, making any sprites or bitmap data in that region visible.
654
+
655
+ ### How
656
+
657
+ One mechanism, two writes per frame, both at the bottom of the display:
658
+
659
+ - Clear RSEL ($D011 bit 3) on a line from 248 to 250 — after the raster has passed line 247 and before it reaches line 251 — with a read-modify-write that keeps YSCROLL, DEN and the mode bits and masks off bit 7 (which reads back as the raster's ninth bit, not the compare value).
660
+ - Set RSEL again anywhere from line 252 to line 246 of the next frame, so that the next frame's line 247 is not a bottom comparison either.
661
+
662
+ That is all. The bottom border of this frame and the top border of the next frame both open, because the vertical border flip-flop is never set. Nothing is written near line 51 or 55, and plain raster IRQs through $0314 are precise enough: the target is a line, not a cycle.
663
+
664
+ An earlier version of this section described a separate top-border write (RSEL=0 on line 55, "symmetric" with the bottom) and a top-only variant. The vertical border flip-flop has no top-side set, so the top opens as a consequence of suppressing the bottom set — measured in VICE x64sc 3.10: the line-55 write on its own leaves both borders closed and moves the bottom border up to line 247, while the two bottom-side writes on their own open both borders.
665
+
666
+ ### Why it works
667
+
668
+ The VIC-II's vertical border flip-flop (Bauer §3.9) is **set** only when the raster reaches the bottom comparison line — 251 with RSEL=1, 247 with RSEL=0 — checked in cycle 63 of the line and again when the beam reaches the left comparison X; it is **reset** only when the raster reaches the top comparison line — 51 with RSEL=1, 55 with RSEL=0 — at the same two moments, and only while DEN is set. Comparisons match on equality, never over a range, and no frame-start event touches the flip-flop. While it is set, the main border flip-flop cannot be reset at the left edge and the graphics sequencer outputs background colour, so the border is drawn; while it is clear, whatever the sequencer and the sprites produce is shown.
669
+
670
+ RSEL=1 while line 247 passes means both of that line's checks look for 251; RSEL=0 while line 251 passes means both of its checks look for 247. Neither matches, the flip-flop stays clear, and there is no other set event until the next frame's bottom comparison — the vertical blank and lines 0–50 go by with the flip-flop clear, so the top border is not drawn either. At line 51 the top comparison resets a flip-flop that is already clear. Restoring RSEL=1 before the next line 247 keeps the cycle going frame after frame.
671
+
672
+ The window for the clearing write is smaller than "before line 251 ends": the left-edge check on line 251 comes at X=24, about cycle 16, before a raster IRQ handler through $0314 has been entered (cycle 37–43). Measured in VICE x64sc 3.10: a clear on line 247 closes the border from line 248 (the cycle-63 check on 247 saw RSEL=0), clears on 248, 249 and 250 open it, and clears on 251 and 252 leave an ordinary frame with the border from line 251.
673
+
674
+ The side borders are not affected; RSEL only governs the vertical comparison lines. In the opened area the VIC is in its idle state and shows the byte at `$3FFF` in colour 0 over the background colour, so `$3FFF` should be zero — VICE's RAM starts so; hardware RAM is not guaranteed to. Sprites are visible there because the border is no longer drawn over them, not because they render anywhere new.
675
+
676
+ ### Variations
677
+
678
+ **Bottom only, top only.** Neither exists with RSEL alone: the flip-flop has one set (a bottom comparison) and one reset (a top comparison), and once the bottom set has been suppressed nothing can set it again before the next frame's line 247, so the two borders open as a pair. An earlier version of this section listed both as variations; the recipe below writes nothing near line 51 and the top opens anyway. A demo that shows one of them closed is painting it back — `$D021` set to the border colour over those lines from another raster interrupt — not closing it.
679
+
680
+ **Full vertical open with sprite coverage.** Open both borders and position 8 sprites to tile vertically across the entire frame (possible because sprites at Y positions above the visible area wrap around in the sprite's own 0-255 coordinate space). Combined with sprite multiplexing this covers nearly the full frame height with sprites.
681
+
682
+ **RSEL held at 0 for the full frame.** Opens nothing: both comparison lines simply move (top 55, bottom 247) and the border is drawn four lines further in at top and bottom. An earlier version of this paragraph said it "permanently opens both borders"; the control build with RSEL=0 from line 55 to line 0 shows a closed frame whose bottom border begins on line 247 (VICE x64sc 3.10). Worth knowing as the side effect of a raster split that leaves RSEL clear when line 247 arrives.
683
+
684
+ ### Cycle budget
685
+
686
+ Coarse: the writes need a line, not a cycle. A raster IRQ on any of lines 248–250 clears RSEL in time with the KERNAL dispatcher's latency included; 247 is too early and 251 too late for a write that lands after cycle 37 (see Why it works). RSEL is part of $D011 with YSCROLL (bits 2–0), DEN (bit 4), BMM and ECM (bits 5 and 6) and RST8 (bit 7), so the toggle is a read-modify-write — `LDA $D011`, `AND` or `ORA` immediate, `STA $D011`: 4 + 2 + 4 = 10 cycles — with bit 7 masked off. With the interrupt bookkeeping ($D012, $0314/$0315, the $D019 acknowledge and the exit) each handler body is about 40 cycles plus the 29-cycle dispatcher, twice per frame — except that the restore handler exits through `$EA31`, the full KERNAL service, which costs about 190 cycles once per frame while no key is held and about 1,600 while one is (measured in VICE x64sc for `recipes/kickassembler/raster-bars.md`; an earlier version of this sentence said "about a thousand", a figure nobody had measured); the opening handler exits through `$EA81`. An earlier version of this paragraph said the write "just needs to land before the end of line 248" and gave the RMW as "3 cycles"; both are replaced by the measured window and the cycle count above.
687
+
688
+ ### Recipes
689
+
690
+ - `recipes/kickassembler/topbottom-border-open.md`
691
+
692
+ ---
693
+
694
+ ## sprites_only_screen_mode — Sprites-only screen: no badlines, no vertical border
695
+
696
+ **Complexity:** medium
697
+ **Region:** both
698
+ **Uses registers:** SCROLY, RASTER
699
+ **Demands:** midframe_raster_irqs, badline_free_region
700
+ **Requires:** topbottom_border_open
701
+ **Raster band:** 40-256 (interrupts on lines 40, 50, 53, 249 and 253; the line-253 handler with the meter latch, 178 cycles, exits about three lines later; the mode itself covers the whole frame)
702
+ **Cost:** cycles_per_frame=756, lines_active=5, irq_slots=5, sprites_per_line=2
703
+ **Cost basis:** measured-vice
704
+ **Cost measured on:** kickassembler-sprites-only-screen (five handlers through $0314 with the KERNAL dispatcher, the recipe's meter latch included; 755 on NTSC)
705
+ **Claims:** vic_raster_irq (owns)
706
+ **Claims basis:** derived-listing
707
+
708
+ ### Why
709
+
710
+ A frame whose only content is sprites has no use for the character display, and the character display is what costs: twenty-five badlines a frame, 40 to 43 cycles each, and a border that hides any sprite outside lines 51 to 250. Switching the display off for the whole frame removes every badline, and opening the top and bottom border as `topbottom_border_open` does lets sprites stand on any of the drawable lines, 272 on PAL and 247 on NTSC in VICE's picture. The CPU keeps every cycle except sprite DMA and the interrupts that run the mode. Measured in VICE x64sc 3.10 with eight sprites on screen: 18,080 cycles a frame free on PAL against 16,940 for the ordinary text screen with the same sprites, 15,520 against 14,460 on NTSC.
711
+
712
+ This is the natural frame for a sprite multiplexer with nothing behind it, a sprite-built logo or scroller, a vector-ball display, or any effect that wants the screen as a black backdrop and the CPU to itself.
713
+
714
+ ### How
715
+
716
+ Five writes to `$D011` per frame from plain raster interrupts through `$0314`, the same handler shape as `topbottom_border_open` with three writes added at the top of the frame. `$D020` and `$D021` are the same colour, black, and `$3FFF`, the idle graphics byte, is zero.
717
+
718
+ | line | `$D011` | what it does |
719
+ |---|---|---|
720
+ | 40 | `$0B` | DEN clear before line 48: the badline condition fails for the whole frame |
721
+ | 50 | `$1B` | DEN set before line 51: the vertical border flip-flop is reset there |
722
+ | 53 | `$0B` | DEN clear again; nothing sets the flip-flop until a bottom comparison |
723
+ | 249 | `$03` | RSEL clear after line 247 and before line 251: 251 is not a match |
724
+ | 253 | `$0B` | RSEL set again so the next frame's line 247 is not a match either |
725
+
726
+ DEN has to be clear across the whole of line 48, because a write setting it on any cycle of that line enables the frame's badlines. DEN has to be set while line 51 passes, because the flip-flop's reset is the only thing that opens the display, and it happens only then and only with DEN set. Once reset, the flip-flop is set again only by a bottom comparison, so DEN can go back to clear on line 53 and stay clear; leaving DEN clear throughout instead, with no write at line 50, leaves the flip-flop set from the first frame's line 251 and the whole picture is border colour with the sprites under it (the recipe's `NOBORDER` control: zero sprite pixels on both models). The bottom is handled as on `topbottom_border_open`: RSEL cleared in the measured window, lines 248 to 250, and restored after 251.
727
+
728
+ The writes are whole values, not read-modify-write, because each one sets DEN and RSEL together with a fixed YSCROLL; the table is also what a control build swaps. Bit 7 goes out as zero in every write, correct for compare lines below 256 (`d012_wrap_around`).
729
+
730
+ ### Why it works
731
+
732
+ Badlines: `hardware/vic-ii-reference.md` states that "DEN must be set at some point during raster line $30 (decimal 48) for badlines to be enabled for the frame" and that "holding DEN clear for the whole of line $30 removes every badline of that frame" (Bauer §3.5). With no badline the VIC never leaves its idle state: no video matrix fetch, no character pointers, no graphics data, and nothing taken from the CPU on lines 51 to 250. The recipe measures this from inside the CPU with a CIA timer in the handler: the line-50 handler, which runs into line 51, costs 105 cycles under the ordinary screen and 62 in this mode, the 43-cycle badline stall gone.
733
+
734
+ The border: the same page states that "the vertical border flip-flop is reset only if DEN is set at cycle 63 of the top comparison line (51 with RSEL = 1, 55 with RSEL = 0), so with DEN clear across that line the border colour ($D020) covers the whole screen, sprites hidden under it". The flip-flop is set only at a bottom comparison, 251 with RSEL set or 247 with it clear, checked at cycle 63 and at the left edge (Bauer §3.9, and the measured table on `topbottom_border_open`). With DEN set for line 51 the reset happens; with RSEL set while 247 passes and clear while 251 passes neither bottom check matches; and DEN being clear again from line 53 does not matter, because no rule that sets the flip-flop reads DEN. The rest is measured: two colours in the picture, black and white, and the sprites at Y 8 and Y 252 drawn on both models.
735
+
736
+ The idle sequencer draws the byte at `$3FFF` in colour 0 over the background wherever it has no row to show, which in this mode is everywhere (Bauer §3.7.3.9; `idle_fetch_byte_shows_in_gaps`). With the byte zero it draws background, and with background and border the same colour the only thing that distinguishes an open frame from a closed one is whether the sprites show. Sprite fetch and display do not depend on DEN or on the flip-flop.
737
+
738
+ ### Cycle budget
739
+
740
+ From the recipe's free-CPU meter, a fixed twenty-cycle loop whose iterations per frame are counted (VICE x64sc 3.10; one iteration, 20 cycles, is the resolution):
741
+
742
+ | build | PAL free cycles | NTSC free cycles |
743
+ |---|---|---|
744
+ | this mode, eight sprites | 18,080 | 15,520 |
745
+ | ordinary text screen, same sprites | 16,940 | 14,460 |
746
+ | this mode, sprites off | 18,900 | 16,340 |
747
+ | ordinary screen, sprites off | 17,820 | 15,260 |
748
+
749
+ By subtraction (arithmetic on those measurements): the badlines cost 1,140 cycles on PAL and 1,060 on NTSC with the sprites on, 1,080 on both without; the eight sprites' DMA costs 820 on both models in this mode; and the five interrupts through the KERNAL dispatcher, with the meter's own latch, cost 756 on PAL and 755 on NTSC out of the 19,656 and 17,095 cycle frames. Each handler body measures 58 cycles from timer start to timer read when no sprite stalls it, 62 or 63 when one does.
750
+
751
+ An interrupt every frame at line 253 is also a free frame tick; the recipe counts frames there.
752
+
753
+ ### Variations
754
+
755
+ **Side border too.** `sideborder_open` on top of this mode gives the whole picture to sprites. Its region condition is met for free: no line in this mode is a badline, so the cycle-56 `$D016` write can land on every line of the region, and the constant sprite set it needs is a matter of placement.
756
+
757
+ **A multiplexer in this mode.** `sprite_multiplex_8` gains 272 lines of drawable height on PAL instead of 200, and the raster interrupts it re-arms sprites from share the frame with these five; the two table-driven chains merge into one. The mode does not change the eight-per-line limit or the DMA cost per sprite line.
758
+
759
+ **Something behind the sprites.** `$3FFF` can be rewritten per line for a one-byte pattern in colour 0, as `topbottom_border_open` notes; the character display cannot be brought back for part of the frame without a badline, and a badline needs DEN set on line 48, which brings back all of them.
760
+
761
+ ### Pitfalls
762
+
763
+ - `d012_wrap_around` (`pitfalls/raster-and-badline.md`): every write here puts a zero in bit 7; a slot moved above line 255 needs the bit set and the compare written as line and $FF.
764
+ - `idle_fetch_byte_shows_in_gaps` (`pitfalls/raster-and-badline.md`): the whole frame is the gap in this mode; a non-zero `$3FFF` puts a stripe pattern across all of it.
765
+ - `vic_bus_takeover_on_dma` (`pitfalls/raster-and-badline.md`): sprite DMA is the one stall left, 820 cycles a frame for the recipe's eight sprites.
766
+ - `badline_cycle_loss` (`pitfalls/raster-and-badline.md`) is what the mode removes; a DEN write that reaches line 48 set brings every badline back for that frame.
767
+ - `sprite_x_high_bit_wrong_register` (`pitfalls/sprite.md`): the recipe's first build had a sprite at X 256 with `$D010` clear and it stood under the left side border.
768
+
769
+ ### Recipes
770
+
771
+ - `recipes/kickassembler/sprites-only-screen.md`
772
+
773
+ ### Sources
774
+
775
+ - Christian Bauer, *The MOS 6567/6569 video controller (VIC-II) and its application in the Commodore 64*, 1996: §3.5 (badline condition), §3.9 (border flip-flops), §3.7.3.9 (idle state).
776
+ - `hardware/vic-ii-reference.md`, the `$D011` section (DEN on line `$30`; the reset needs DEN at cycle 63 of the top comparison line), measured in VICE.
777
+ - `recipes/kickassembler/sprites-only-screen.md`: every cycle, row and colour figure above.
778
+
779
+ ---
780
+
781
+ ## raster_split_modes — Mid-frame display mode change
782
+
783
+ **Complexity:** medium
784
+ **Region:** both
785
+ **Uses registers:** SCROLY, SCROLX, VMCSB
786
+ **Demands:** midframe_raster_irqs
787
+ **Claims:** vic_raster_irq (owns)
788
+ **Claims basis:** estimated
789
+
790
+ ### Why
791
+
792
+ The VIC-II supports four display modes: standard character mode (text), multicolor character mode, standard bitmap mode, and multicolor bitmap mode. (Extended Background Color mode is a fifth; it is exclusive with multicolor and bitmap — ECM+MCM and ECM+BMM are the black "invalid" modes — and restricts the character set to 64 glyphs, since character code bits 7–6 select one of four background colours. Sprites, single- or multicolour, are unaffected: measured in VICE x64sc, a sprite renders identically with ECM set and clear. An earlier version of this sentence said ECM was "effectively mutually exclusive with sprites"; it is not.) Each mode is selected by the combination of $D011 bit 5 (BMM, bitmap mode), $D011 bit 6 (ECM, extended background color), and $D016 bit 4 (MCM, multicolor mode).
793
+
794
+ Changing these mode bits mid-frame via a raster IRQ switches the display from one mode to another at the target scanline. This is the fundamental mechanism behind one of the most common C64 screen layouts: a full-resolution or multicolor bitmap for the game or demo canvas, with a character-mode status bar at the top or bottom of the screen. On a single-display-mode system this layout would be impossible. With raster mode splits it is standard.
795
+
796
+ The technique also underlies FLI (Flexible Line Interpretation) and IFLI effects, where mode bits and memory pointers are changed on every single line to defeat the VIC-II's 8-pixel-tall color attribute resolution. FLI is a separate technique (out of scope for this document), but the per-line mode change it uses is a direct extension of raster_split_modes at the limit of 1 IRQ per line.
797
+
798
+ ### How
799
+
800
+ The mode change sequence in a raster IRQ handler:
801
+
802
+ 1. Write $D011 to set or clear BMM and/or ECM for the new mode.
803
+ 2. Write $D016 to set or clear MCM for the new mode.
804
+ 3. Write $D018 to point to the video matrix and character/bitmap base for the new mode's data.
805
+
806
+ All three writes should be performed as close together as possible and as close to the start of the target line as possible, to avoid partial-line glitches. Writing $D011 a cycle late while $D016 has already been written produces an undefined intermediate mode for one cycle, which can manifest as pixel garbage on the first character position of the split line.
807
+
808
+ The mode change can happen anywhere in the frame: top to bottom, multiple splits, alternating modes, or even per-line cycling. The only constraint is cycle budget per line.
809
+
810
+ ### Why it works
811
+
812
+ The VIC-II decides how to decode pixel data (and whether to fetch character ROM/RAM or bitmap data) on a per-character-cell basis within each row. The mode registers ($D011 bits 5-6, $D016 bit 4) are read by the chip as it generates each 8-pixel horizontal span. A write to these registers takes effect on the current or immediately next character cell boundary.
813
+
814
+ The display mode and the data pointer ($D018) govern three separate things: how pixel bits are interpreted (character vs bitmap), whether two bits per pixel (multicolor) or one bit per pixel (hires) is used, and where in the VIC bank the data lives. Changing $D018 mid-frame redirects character or bitmap fetch to new addresses starting with the next character cell; the change is not batched to the next frame. This allows per-row (or per-line) memory pointer changes without a frame boundary.
815
+
816
+ The YSCROLL field ($D011 bits 2-0) interacts with mode changes: if YSCROLL changes simultaneously with the mode bits, the VIC may trigger a spurious badline (if the new YSCROLL value matches the current `(raster & 7)` condition). To avoid this, keep YSCROLL constant across mode splits, or change it in a separate write on a line where a badline is acceptable.
817
+
818
+ ### Variations
819
+
820
+ **Bitmap canvas with text status bar.** The most common application: bitmap mode for lines 50-200, character mode for lines 201-250 (or vice versa). $D018 points to bitmap data in the upper half of the frame and character data in the lower half. One raster IRQ handles the switch; another switches back at the top of the next frame.
821
+
822
+ **Multiple mode zones.** Three or more display mode regions in a single frame. Each transition requires one raster IRQ. With 16 IRQ slots in Oscar64's rasterirq system, up to 16 transitions per frame are possible.
823
+
824
+ **Per-line FLI preparation.** Set up mode bits and $D018 on every single line to defeat attribute color resolution. Full FLI implementation requires writing $D018 and possibly $D011 YSCROLL on every line within the FLI zone. This is a distinct and very demanding technique but shares the mode-change mechanism exactly.
825
+
826
+ **Multicolor-to-hires split.** Switch from multicolor character mode to hires character mode mid-frame. Used for effects where the upper portion of the screen uses 4-color characters and the lower uses high-resolution black-and-white data.
827
+
828
+ ### Cycle budget
829
+
830
+ Each raster split costs: 3 writes ($D011, $D016, $D018) × 4 cycles = 12 cycles minimum. With IRQ overhead — 13-20 cycles with the KERNAL out and $FFFE pointing at the handler, 42-50 through $0314, where the handler is entered on cycle 37-43 — a mode split consumes approximately 25-35 or 55-65 cycles on the split line respectively. An earlier version gave only 13-20 without naming the entry path.
831
+
832
+ On a badline, a mode-split IRQ has only 20 usable cycles on PAL (cycles 1-11 and 55-63; 12-14 for writes only). A 12-cycle triple write fits, but combined with IRQ overhead it is tight. The standard mitigation is to position the mode split on a non-badline.
833
+
834
+ YSCROLL manipulation during a mode split requires a fourth write to $D011 — but since $D011 carries both YSCROLL and mode bits, the YSCROLL write and the mode-bit write must be combined into one read-modify-write, costing 10 cycles instead of 4 for two separate stores. If the mode bits and YSCROLL value are known in advance, a precomputed combined value can be stored directly in 4 cycles.
835
+
836
+ The $D018 write is the most timing-sensitive of the three, and its two halves behave differently. The character/bitmap base (CB bits 3-1) is read on every g-access, so a mid-row write changes the glyph or bitmap source from the very line the write lands on — measured in VICE x64sc: a CB switch on line 100 redrew lines 100-106 of that row from the new set, and a CB switch on line 98 redrew line 98, the last line of the previous row. A CB-only split therefore takes effect where it lands, and the advice to fire the IRQ "on the line before the first badline" tears that previous row's last line unless the store completes after the last g-access (cycle 55 on PAL) of that line; land the write in cycles 56-63 of the previous line or before cycle 16 of the zone's first line. The video-matrix pointer (VM bits 7-4) is consumed by the badline's c-accesses (cycles 15-54), whose 40 codes are held in the row buffer, so a VM change shows on the next character row — measured: a VM switch on line 100 left row 6 unchanged and row 7 drawn from the new matrix. Note the same VM bits also address the sprite pointers at VM+$3F8, which the p-accesses read on every line, so a VM move shifts sprite pointer reads at once. Put a split that moves the video matrix anywhere in the 8 lines before the zone's first badline, before cycle 15 of that badline. An earlier version of this paragraph said the whole of $D018 was latched once per row at the badline fetch, so a late write was ignored for eight lines; that is true only of the VM half.
837
+
838
+ ### Recipes
839
+
840
+ (Standalone recipe not yet written — raster_split_modes is demonstrated as part of larger demo or game layout recipes.)
841
+
842
+ ---
843
+
844
+ ## pal_ntsc_detection — Detect PAL vs NTSC at boot
845
+
846
+ **Complexity:** low
847
+ **Region:** both
848
+ **Uses registers:** D011, D012
849
+
850
+ ### Why
851
+
852
+ A C64 does not know which video standard it was built for, and neither does the program it is running: no register says PAL or NTSC. Yet almost everything timed by the frame or by the CPU clock differs between the two — 312 raster lines a frame against 263 (6567R8) or 262 (6567R56A), 63 cycles a line against 65 or 64, 985,248 Hz against 1,022,727 Hz (settled figures; `hardware/pal-ntsc-reference.md` has the tables). Music ticked once a frame runs a fifth too fast on NTSC, a CIA reload drifts 3.8 %, a raster interrupt set for line 280 never fires on a chip whose frame ends at 262; `pitfalls/region-timing.md` walks through all three. The cure in each case is the same: find out once, at boot, which chip this is, store the answer in a byte, and branch on it. This technique is that one measurement.
853
+
854
+ ### How
855
+
856
+ The VIC-II's raster counter is nine bits wide: `$D012` holds the low eight and bit 7 of `$D011` (RST8) is the ninth. RST8 is therefore set for exactly the raster lines from 256 upward, and the frames of the three chips differ only in how many of those lines they have — 56 on PAL (256–311), 7 on the 6567R8 (256–262), 6 on the 6567R56A (256–261). The routine reads the length of that band:
857
+
858
+ 1. Disable interrupts (`SEI`). A handler that ran for longer than a raster line would hide a line from the loop.
859
+ 2. Wait until RST8 is clear. This is not for calls that land in the middle of the band — the lines such a call skips are the smaller values, and a loop that keeps the latest one is indifferent to them (measured: the loop with this wait deleted, entered on PAL lines 256 and 300, still returned `$37`). It closes a race at the far end of the band. A call landing in the last cycles of the frame's final line takes its first `$D012` sample on that line and its RST8 check on line 0, so step 5 is reached before any value has been kept and the result is whatever the register held before the call (measured: the same wait-less loop entered on PAL line 311 with its result register preloaded to `$EE`, and the entry phase swept in 4-cycle steps, returned `$EE` at two of sixteen phases and `$37` at the other fourteen; with the wait restored, both racing phases returned `$37`). Waiting for RST8 to be clear first means step 3 can only exit at line 256, never at line 311. This page gave the mid-band reason until 2026-09-22; it was wrong.
860
+ 3. Wait until RST8 is set. That is line 256 on every chip, and `$D012` reads `$00` there.
861
+ 4. While RST8 stays set, read `$D012` and keep the value — the most recent one, or the highest; inside the band they are the same. Read `$D012` first and RST8 second, and keep the sample only if RST8 was still set after it, so a read that has already wrapped to `$00` on line 0 is never recorded. The Oscar64 recipe tests RST8 at the top of its loop instead, because that is where the compiler puts a `while` condition, and keeps the highest value, which makes the wrapped `$00` harmless without the ordering; the two forms agree.
862
+ 5. When RST8 clears, the kept value is the low byte of the last line of the frame: `$37` on the 6569, `$06` on the 6567R8, `$05` on the 6567R56A. Store it, or reduce it to one flag, and re-enable interrupts. Anything that became pending during the wait — the KERNAL's 60 Hz timer interrupt, if its vector is still installed — is serviced the moment interrupts are back on, so call the routine before the KERNAL interrupt is replaced, or expect one KERNAL service to run right after it returns.
863
+
864
+ For a two-way PAL/NTSC answer a shortcut suffices: any `$D012` value of `$10` or more seen while RST8 is set means PAL, because lines 272–311 exist on no NTSC chip. Keeping the whole value costs nothing more and tells the two NTSC chips apart.
865
+
866
+ Measured in VICE x64sc 3.10 (rung 1): the kept value was `$37` on the default PAL model, `$06` with `-model ntsc` and `$05` with `-model oldntsc`, read back from the screen as hex digits, and the same when the run was stopped at 5,000,000 and at 8,000,000 cycles. The same wait-and-track loop, entered deliberately from raster lines 100, 300 and 311 on PAL and from 100 and 262 on NTSC, gave the right answer every time.
867
+
868
+ **What does not work, and stood in this knowledge base until 2026-09-21:** polling for RST8 to become set and then reading `$D012` once. That read lands on line 256, the first line of the band, and returns `$00` on every chip — measured as `00` on all three VICE models — so a `cmp #$10` after it says NTSC unless the routine was called from inside lines 272–311 by luck. Both copies of the "shortest reliable detect" here did exactly that (`hardware/pal-ntsc-reference.md` Method 2, and the fix in `pitfalls/region-timing.md`, whose copy also fell through a `bne` after `lda #0` and so answered NTSC from either branch). Both are corrected on their own pages, with the measurements.
869
+
870
+ ### Why it works
871
+
872
+ The counter is incremented at the start of each raster line and reset to zero for line 0 (Bauer, §3.6.3; not measured here beyond the wrap values above). RST8 is nothing more than bit 8 of that counter, so it is a level, not an event: it reads 1 for the whole of lines 256 onward and 0 for the whole of lines 0–255, and a polling loop can watch it change without a raster interrupt and without touching `$D019`. The last line of the frame is the only place the three chips disagree, and it is the last line on which RST8 reads 1 — sampling the low byte until RST8 falls therefore reads that line's number without knowing it in advance. The display window is not involved: lines 256 and up are lower border or vertical blanking on every chip, and no badline can occur there since the badline condition needs a raster line between `$30` and `$F7` (Bauer, §3.5; not measured here), so with no sprites enabled — the state at boot; sprite DMA would take cycles from these lines too, since the sprite Y compare uses the low byte of the raster counter (Bauer, §3.8; not measured here) — the CPU keeps every cycle of every line in the band and a loop of fifteen to twenty-two cycles samples each line two to four times.
873
+
874
+ The frame is the same length however the routine is entered, so the answer does not depend on when the program started; the two waits guarantee that sampling begins at line 256 and ends at line 0, so the band's last line is always among the samples. How long the routine holds the CPU does depend on the entry line. Called from line L below 256 it runs N − L lines, the rest of the frame; called from inside the band it runs 2N − L lines — the rest of that band, the 256 lines with RST8 clear, and the whole of the next band. So it takes at least N − 255 lines — 57 on PAL (3,591 cycles, about 3.6 ms), 8 on the 6567R8 (520 cycles, about 0.5 ms), 7 on the 6567R56A — and at most 2N − 256 lines — 368 on PAL (23,184 cycles, about 23.5 ms, 1.18 frames), 270 on the 6567R8 (17,550 cycles, about 17.2 ms, 1.03 frames), 268 on the 6567R56A (arithmetic from the settled constants). Measured in VICE x64sc 3.10 with CIA 1 timer A wrapped around the 26-byte tracking loop that `detect_region` in `pitfalls/region-timing.md` extends with its flag store, CIA interrupts masked and any pending one acknowledged first, and the wrapper's own 17 cycles taken off by a null-call control: 3,575 cycles from PAL line 255 and 23,172 from line 256; 500 and 17,535 on the 6567R8; 432 and 17,131 on the 6567R56A; 13,337 from PAL line 100 and 19,707 from line 311 — each within one line of the arithmetic. **Correction (2026-09-22):** until this date this page said the measurement "takes between one and two frames — about 40 ms on PAL, 33 ms on NTSC". That was a bound written from the shape of the loop, not measured, and it is wrong at both ends: the routine never reaches two frames, and from most entry lines it takes well under one.
875
+
876
+ ### Variations
877
+
878
+ **Time a frame with a CIA timer.** Start a CIA timer at one raster line 0 and read it at the next: about 19,656 cycles on PAL (312 × 63), 17,095 on the 6567R8 (263 × 65), 16,768 on the 6567R56A (262 × 64) — arithmetic from the settled constants; this variant was not run here. `hardware/pal-ntsc-reference.md` Method 1 lists it. It yields the cycle count, which the raster method does not, at the cost of a CIA timer, more code and a threshold to choose. On a stock machine the raster band is the shorter and more direct read.
879
+
880
+ **Store, do not repeat.** Take the measurement once, before interrupts are installed, into a byte the rest of the program branches on: the music tick (`pal_ntsc_tempo_mismatch`), CIA reloads (`cia_timer_phi2_difference`), raster tables (`raster_line_count_difference`). Nothing about the chip changes later.
881
+
882
+ **Three-way or two-way.** Keep the raw last-line byte if the program counts cycles per line or lines per frame on NTSC — the R8 and the R56A differ in both, 65 against 64 and 263 against 262 (settled); reduce it to PAL/NTSC with one compare against `$10` otherwise.
883
+
884
+ **The KERNAL's own answer.** The stock KERNAL takes a two-way measurement of its own at reset and leaves it at `$02A6` (PALNTS): 1 for PAL, 0 for NTSC. The mechanism is in the ROM bytes (`kernal-901227-03.bin`, read here): the reset path at `$FF5B` initialises the VIC from a table that sets the raster compare to line 311 and acknowledges `$D019`, clears the screen, waits for `$D012` to read zero, then reads `$D019`, keeps bit 0 and stores it at `$02A6` — the raster-compare flag can only have been raised if a line 311 exists. It cannot tell the R8 from the R56A, a replacement KERNAL or an earlier program may have left anything there, and its reliability was not measured here; `hardware/pal-ntsc-reference.md` Method 3 has the detail. Measure the chip yourself when the answer matters.
885
+
886
+ ### Cycle budget
887
+
888
+ None per line. The routine runs once, with interrupts disabled, and holds the CPU for between 57 and 368 lines on PAL (about 3.6 to 23.5 ms) or between 8 and 270 lines on the 6567R8 (about 0.5 to 17 ms) depending on where in the frame it is entered — a fifth of a frame at best, 1.2 frames at worst, measured as described above; nothing else is expected to run during it. It takes no raster interrupt and writes neither `$D011` nor `$D012`. 36 bytes with the compare and flag store as `detect_region` in `pitfalls/region-timing.md` (assembler count); the hardware page's Method 1 `detect_region` returns a three-way code in A and its Method 2 `detect_pal` a carry flag, both with the same `wait_lo` guard.
889
+
890
+ ### Recipes
891
+
892
+ - `recipes/oscar64/pal-ntsc-detect.md`
893
+
894
+ ### Sources
895
+
896
+ - Christian Bauer, "The MOS 6567/6569 video controller (VIC-II) and its application in the Commodore 64", https://www.cebix.net/VIC-Article.txt — §3.2 (RST8), §3.4 (lines per frame per chip), §3.5 (bad line condition), §3.6.3 (raster counter increment and reset), §3.8 (sprite DMA and the Y compare).
897
+ - VICE 3.10, `x64sc`, models `default`, `ntsc`, `oldntsc` — the instrument for every figure marked measured above; the durations were read from CIA 1 timer A, the verdicts from the screen.
898
+ - KickAssembler 5.25 — the listing the durations were measured on is the hardware page's, and the byte counts are its.
899
+ - VICE's `kernal-901227-03.bin` — the bytes at `$FF5B`, `$ECB9` and `$FDDD` behind the `$02A6` variation.
900
+ - This repository: `hardware/pal-ntsc-reference.md`, `pitfalls/region-timing.md`, `recipes/oscar64/pal-ntsc-detect.md`.
901
+
902
+ ---
903
+
904
+ ## frame_sync_loop — Raster-synced frame loop
905
+
906
+ **Complexity:** low
907
+ **Region:** both
908
+ **Uses registers:** D011, D012, D020
909
+ **Cost:** bytes_code=985
910
+ **Cost basis:** arithmetic
911
+ **Cost measured on:** oscar64-frame-sync-loop (bytes are the whole PRG)
912
+
913
+ ### Why
914
+
915
+ A game loop that runs as fast as the CPU allows draws at a rate the VIC-II
916
+ does not share: sprites move while the beam is drawing them, screen writes
917
+ land half-way down a character row, and the speed of the game changes with
918
+ the amount on screen. Locking the loop to the frame fixes all three. Once a
919
+ frame, at a raster line of your choosing, the loop wakes, does its work, and
920
+ goes back to waiting. Everything that follows needs an answer to three
921
+ questions: how does the loop know a new frame has begun, how much of the
922
+ frame did the work take, and did any frame go by without it. This technique
923
+ is the standard answer to each, and it is the first thing to put in a game
924
+ before anything else is written, because the budget bar it gives you is the
925
+ profiler you will use for the rest of the project.
926
+
927
+ ### How
928
+
929
+ **The wait, without an interrupt.** The raster counter is nine bits, the low
930
+ eight in `$D012` and RST8 in bit 7 of `$D011`, and RST8 is a level: it reads
931
+ 1 for the whole of lines 256 upward and 0 for lines 0 to 255 (see
932
+ `pal_ntsc_detection` above). Spinning until RST8 is set therefore returns at
933
+ line 256 on every chip. That is what Oscar64's `vic_waitBottom()` does
934
+ (`vic.c` lines 62 to 66), and `vic_waitFrame()` first spins until RST8 is
935
+ clear and then until it is set (lines 74 to 80), so two calls in a row are
936
+ always one frame apart. The distinction matters: the RST8 band is 56 lines
937
+ on PAL and 7 on the 6567R8 (settled frame lengths), so a loop that calls
938
+ `vic_waitBottom()` twice with less than seven lines of work between the
939
+ calls on NTSC gets two returns from one frame (arithmetic from the source
940
+ and the constants; not run here). Prefer `vic_waitFrame()`, or a wait that
941
+ compares a line.
942
+
943
+ **Why the wrap line matters.** `$D012` on its own is ambiguous. Its low byte
944
+ wraps to zero twice a frame, once at line 256 and once at line 0, so the
945
+ values 0 to 55 each occur on two lines of a PAL frame (0 to 6 on the
946
+ 6567R8), and a loop that watches for the counter to "wrap" fires twice a
947
+ frame unless it also reads RST8. A spin on `$D012 == N` for a line at or
948
+ above 256 needs the ninth bit or it matches the low line too; Oscar64's
949
+ `vic_waitLine()` does exactly this, matching the low byte and then checking
950
+ RST8 against bit 8 of the target (`vic.c` lines 91 to 101). An equality spin
951
+ has a second weakness: if anything holds the CPU for longer than a line
952
+ while it is spinning, the target line goes by unseen and the loop waits a
953
+ whole extra frame. A line is 63 cycles on PAL; the KERNAL's own timer
954
+ service, if its vector is still installed, is about 190 cycles idle and
955
+ about 1,600 with a key held (measured in VICE for
956
+ `recipes/kickassembler/raster-bars.md`, quoted in `topbottom_border_open`
957
+ above), so either is enough. A compare that accepts "at or past" the line, which is the
958
+ form `vic_waitBelow()` uses (`vic.c` lines 103 to 121), can end late but
959
+ cannot miss. And a target line the chip does not have, 300 on NTSC, never
960
+ matches at all: the loop hangs (`pitfalls/region-timing.md`).
961
+
962
+ **The wait, with an interrupt.** The cleaner form takes one raster interrupt
963
+ at the sync line and lets it do one thing: increment a byte. The main loop
964
+ keeps its own copy of the byte and spins while the two are equal. The
965
+ interrupt only ever increments and the main loop only ever catches up;
966
+ nothing is cleared across the boundary, so a tick cannot be lost between a
967
+ read and a clear, and the byte is one byte so the read needs no `SEI`. In
968
+ Oscar64, `rirq_count` and `rirq_wait()` (`rasterirq.c` lines 606 to 614) are
969
+ this exact loop, with the byte incremented once per frame by the dispatcher
970
+ after the last slot of the schedule; a `rirq_call` to an `__interrupt`
971
+ function that increments a byte of your own is the same thing with the
972
+ counting on the page. The recipe below does the latter. A plain flag,
973
+ set by the interrupt and cleared by the loop, works too, but it cannot count
974
+ how many frames were missed; the tick byte can.
975
+
976
+ **A frame counter.** Count the loop's iterations in a 16-bit variable owned
977
+ by the main loop. That is the game's clock: animation phases, spawn timers
978
+ and music tempo divide it. Count ticks separately if you also want a clock
979
+ that keeps running while the loop is late.
980
+
981
+ **Dropped-frame detection.** When the wait ends, subtract the copy from the
982
+ tick byte in eight bits. The result is 1 when the loop kept up and more when
983
+ it did not; every count above one is a frame that passed while the loop was
984
+ still working. Add them to a dropped counter, then set the copy equal to the
985
+ tick so every tick seen is consumed. The eight-bit subtraction makes the
986
+ counter's wrap from 255 to 0 harmless for up to 255 missed frames, and the
987
+ whole arithmetic was run over all 65,536 (tick, copy) byte pairs on the 6502
988
+ against the same fold in Python (`recipes/oscar64/frame-sync-loop.md`). With
989
+ the tick form a loop that overran does not wait: the tick has already moved,
990
+ so the next frame starts at once and the game runs at the speed of the work.
991
+ With a line-compare wait the same loop would sit out the rest of the frame
992
+ and run at a whole number of frames per iteration, two for anything between
993
+ one and two frames of work (arithmetic; only the tick form was run here).
994
+
995
+ **The budget bar.** Write a bright colour to `$D020` as the first thing after
996
+ the wait and the background colour as the last thing before it. The beam
997
+ paints whatever `$D020` holds into the border as it goes, so the border is
998
+ lit for exactly the lines the loop was working and the band's lower end is
999
+ the budget used. Every C64 programmer does this; it costs two stores and it
1000
+ turns the raster into a profiler with a resolution of one line, which is 63
1001
+ cycles. Measured in VICE x64sc 3.10, rung 1 (`recipes/oscar64/frame-sync-loop.md`,
1002
+ PNG read down the border column): an IRQ on rirq row 250, landing on
1003
+ line 251, gives a bar that begins on line 254 and, for a fixed 8-unit
1004
+ workload, ends on line 106 on PAL and line 158 on the 6567R8: 165 and 168
1005
+ lines, 53 % of the PAL frame and 64 % of the NTSC one. The same cycles are a
1006
+ bigger slice of the shorter frame, and cover slightly more lines because
1007
+ the NTSC bar crosses twice as many badlines. Tripled to 24 units
1008
+ (`recipes/oscar64/frame-sync-loop-overrun.md`, the same listing with the
1009
+ constant changed, pinned separately) the bar has no end: the border is lit
1010
+ on every visible line, the only black is a
1011
+ one-line gap where the loop's two stores fall a few cycles apart, and that
1012
+ gap walks about two thirds of a frame down the picture every loop because
1013
+ each loop is 1.65 frames long. The dropped counter read 100 against 153
1014
+ loops in that run. An all-white border is the usual face of an overrun; do
1015
+ not wait for a gap to appear before believing the counter.
1016
+
1017
+ ### Why it works
1018
+
1019
+ The VIC-II increments its raster counter at the start of every line and
1020
+ resets it to zero for line 0 (Bauer, §3.6.3; not measured here beyond the
1021
+ wrap values on this page). RST8 is bit 8 of that counter, read back through
1022
+ `$D011` bit 7, which is why a read of `$D011` and a read of `$D012` together
1023
+ name a line without ambiguity and a read of `$D012` alone does not. A raster
1024
+ interrupt fires when the counter equals the nine-bit compare value written
1025
+ to `$D012` and `$D011` bit 7, once per frame per compare value, so an
1026
+ interrupt at the sync line is a once-a-frame event by construction; a spin
1027
+ is the same comparison done by the CPU. `$D020` is read by the VIC every
1028
+ pixel it draws as border, so a change shows within the same line, which is
1029
+ what makes the border a display of the CPU's timeline.
1030
+
1031
+ Where the sync line sits decides what the loop can safely touch. From line
1032
+ 251, the first line after the 25-row display window, the beam spends the
1033
+ bottom border, the vertical blank and the top border before the first
1034
+ badline of the next frame at line 51: 112 lines on PAL, 63 on the 6567R8
1035
+ (settled frame lengths). Screen RAM, colour RAM, the sprite registers and
1036
+ the scroll registers written in that window are all read by the VIC after
1037
+ the write, so nothing tears. Work that spills past line 51 pays the
1038
+ badlines it crosses, 40 to 43 cycles each, and any sprite fetches on those
1039
+ lines; the bar shows the cost as extra lines.
1040
+
1041
+ ### Variations
1042
+
1043
+ **Flag instead of tick.** The interrupt sets a byte to 1 and the main loop
1044
+ clears it. Simplest possible form; cannot count missed frames, and a loop
1045
+ that runs long finds the flag already set and starts the next frame at once,
1046
+ exactly as the tick form does.
1047
+
1048
+ **Spin-only, no interrupt.** `vic_waitFrame()` or `vic_waitLine(n)` at the
1049
+ top of the loop, nothing installed. Fine for a demo or a tool, and the
1050
+ only choice while the KERNAL interrupt is left running for the keyboard. It
1051
+ cannot detect a dropped frame, because a wait that ends does not know how
1052
+ many lines went by before it started; pair it with the tick byte if that
1053
+ matters.
1054
+
1055
+ **Bar per subsystem.** Change the colour between stages, red for the
1056
+ sprite multiplexer sort, green for the game logic, blue for the music call,
1057
+ and the border becomes a stacked bar chart of the frame. The music player's
1058
+ band in particular should be flat from frame to frame; one that is not is a
1059
+ player with a data-dependent path.
1060
+
1061
+ **Sync to a line inside the display.** A loop whose display writes all go to
1062
+ the lower half of the screen can sync higher, to the last line above them,
1063
+ and gain the top of the display as working time. The rule is only that the
1064
+ writes land before the beam reaches what they change.
1065
+
1066
+ ### Cycle budget
1067
+
1068
+ None per line. The wait costs nothing useful, only the cycles until the
1069
+ line arrives. The interrupt form pays the interrupt's entry and exit once
1070
+ per frame, 36 cycles to the handler through `$0314` (settled) plus whatever
1071
+ the dispatcher and the handler body add; not broken down here. The bar is
1072
+ two absolute stores. What the loop has left is the frame: 312 × 63 =
1073
+ 19,656 cycles on PAL and 263 × 65 = 17,095 on the 6567R8, less 40 to 43
1074
+ for each of the 25 badlines and less any sprite DMA (arithmetic from
1075
+ the settled constants); `game-design/game-design-patterns.md` budgets
1076
+ about 19,700 after interrupt overhead on PAL, which is a rounding of the
1077
+ same figure.
1078
+
1079
+ ### Recipes
1080
+
1081
+ - `recipes/oscar64/frame-sync-loop.md`
1082
+ - `recipes/oscar64/frame-sync-loop-overrun.md`
1083
+
1084
+ ### Sources
1085
+
1086
+ - Oscar64 (build 2026-05-19), `include/c64/vic.h` lines 107 to 128 (the
1087
+ declarations; the comment above the first is line 106) and
1088
+ `include/c64/vic.c` lines 56 to 138, read here; `include/c64/rasterirq.c`
1089
+ lines 606 to 614 (`rirq_wait`) and the `inc rirq_count` after the last
1090
+ slot of the schedule.
1091
+ - VICE 3.10, `x64sc`, models `default` and `ntsc`: the instrument for every
1092
+ figure marked measured above, read from the exit PNG.
1093
+ - Christian Bauer, "The MOS 6567/6569 video controller (VIC-II) and its
1094
+ application in the Commodore 64", https://www.cebix.net/VIC-Article.txt,
1095
+ §3.6.3 (raster counter) and §3.2 (RST8).
1096
+ - This repository: `pitfalls/region-timing.md`,
1097
+ `game-design/game-design-patterns.md` (game loop patterns),
1098
+ `recipes/oscar64/simple-shmup.md` (a full game on the same loop shape).
1099
+
1100
+ ## irq_chain_table — Table-driven raster IRQ chain
1101
+
1102
+ **Complexity:** medium
1103
+ **Region:** both
1104
+ **Uses registers:** D011, D012, D019, D01A, D020
1105
+ **Demands:** midframe_raster_irqs
1106
+ **Cost:** cycles_per_frame=273, lines_active=3, irq_slots=3
1107
+ **Cost basis:** estimated
1108
+ **Cost measured on:** kickassembler-irq-chain (three empty slots)
1109
+ **Claims:** vic_raster_irq (owns)
1110
+ **Claims basis:** derived-listing
1111
+
1112
+ ### Why
1113
+
1114
+ A raster interrupt fires once per frame at one line. A program that wants
1115
+ several things done at several lines has two choices: one handler per line,
1116
+ each re-pointing `$0314` at the next handler and arming `$D012` for it, which
1117
+ is the ring in `recipes/kickassembler/raster-bars.md` and
1118
+ `recipes/kickassembler/cracktro-template.md`; or one dispatcher that walks a
1119
+ table of (line, handler) pairs. The ring spreads the arming and acknowledge
1120
+ logic across every handler, and a slot cannot be added, removed or moved
1121
+ without editing its neighbours. The table puts that logic in one place, so a
1122
+ slot is one table row and a handler is a plain subroutine that ends in
1123
+ `RTS`. Oscar64's `rasterirq.h` is this shape with a sorter in front of it
1124
+ (`recipes/oscar64/raster-bars.md`); this entry is the same thing on the page
1125
+ for KickAssembler, with each step measured in VICE.
1126
+
1127
+ ### How
1128
+
1129
+ **The table.** Two parallel byte arrays for the line, low eight bits and
1130
+ the ninth bit already shifted into bit 7 so it can be ORed straight into
1131
+ `$D011`, and two for the handler address. Rows are in raster order. A
1132
+ slot index byte says which row runs next. The chain below is the one the
1133
+ recipe runs: three slots at lines 40, 130 and 260, so one slot is above the
1134
+ display, one inside it and one needs RST8.
1135
+
1136
+ **The dispatcher.** One routine behind `$0314`. In this order:
1137
+
1138
+ 1. Acknowledge: `LDA #$01 / STA $D019`.
1139
+ 2. Compute the next index (wrap to 0 at the table's end) and arm its line:
1140
+ low byte to `$D012`, then `$D011` with bit 7 replaced by the row's ninth
1141
+ bit and the other seven bits kept.
1142
+ 3. Call this slot's handler through the table: copy the address into the
1143
+ operand of a `JSR` and run it. The 6510 has no `JMP (abs,X)`, and a
1144
+ `JSR` lets the handler end in `RTS` and leave the exit to the dispatcher.
1145
+ 4. Store the next index. If it wrapped to 0, the frame is complete:
1146
+ increment the frame counter, call the music player, do anything else that
1147
+ runs once per frame.
1148
+ 5. Exit through `$EA81` (register restore and `RTI`).
1149
+
1150
+ ```asm
1151
+ .const NSLOTS = 3
1152
+
1153
+ irq:
1154
+ lda #$01
1155
+ sta $d019 // 1. acknowledge
1156
+
1157
+ ldx slot // 2. arm the next slot's line, all nine bits
1158
+ inx
1159
+ cpx #NSLOTS
1160
+ bne !+
1161
+ ldx #0
1162
+ !: stx next
1163
+ lda line_lo,x
1164
+ sta $d012
1165
+ lda $d011
1166
+ and #$7f
1167
+ ora line_hi,x
1168
+ sta $d011
1169
+
1170
+ ldx slot // 3. call this slot's handler
1171
+ lda handler_lo,x
1172
+ sta call + 1
1173
+ lda handler_hi,x
1174
+ sta call + 2
1175
+ call:
1176
+ jsr $ffff
1177
+
1178
+ ldx next // 4. advance; a wrap to 0 is the once-a-frame point
1179
+ stx slot
1180
+ bne done
1181
+ inc frame_lo
1182
+ bne !+
1183
+ inc frame_hi
1184
+ !: jsr music_tick
1185
+ done:
1186
+ jmp $ea81 // 5. restore A, X, Y and RTI
1187
+
1188
+ line_lo: .byte <40, <130, <260
1189
+ line_hi: .byte (40 >> 8) << 7, (130 >> 8) << 7, (260 >> 8) << 7
1190
+ handler_lo: .byte <slot0, <slot1, <slot2
1191
+ handler_hi: .byte >slot0, >slot1, >slot2
1192
+
1193
+ slot0: lda #2
1194
+ sta $d020
1195
+ rts
1196
+ slot1: lda #5
1197
+ sta $d020
1198
+ rts
1199
+ slot2: lda #6
1200
+ sta $d020
1201
+ rts
1202
+ music_tick: rts
1203
+
1204
+ slot: .byte 0
1205
+ next: .byte 0
1206
+ frame_lo: .byte 0
1207
+ frame_hi: .byte 0
1208
+ ```
1209
+
1210
+ **Install.** `SEI`; mask CIA1 with `$7F` to `$DC0D` and read `$DC0D` once,
1211
+ so the only interrupt that reaches `$0314` is the raster one; set the slot
1212
+ index to 0 and arm row 0's line the same way step 2 does; point `$0314/$0315`
1213
+ at the dispatcher; `$01` to `$D01A`; `$01` to `$D019` to drop any flag left
1214
+ from before; `CLI`. If the KERNAL's timer interrupt is left enabled it
1215
+ arrives through the same vector and the dispatcher runs a slot early.
1216
+
1217
+ **Acknowledge first, arm second.** The order matters when a handler runs
1218
+ long. Measured in VICE x64sc 3.10 (PAL, 8,000,000 cycles) with slot 1's
1219
+ handler padded to about 9,000 cycles so that it returns after slot 2's line
1220
+ 260 has gone by: with the order above, the raster flag raised at line 260 is
1221
+ still set when the dispatcher exits, the CPU takes the interrupt again at
1222
+ once, slot 2 runs late (its colour write first appears at line 287, the
1223
+ last line in the picture; from line 130, 9,000 cycles is 143 lines and the
1224
+ fifteen badlines crossed add about ten more) and the frame counter reads
1225
+ 254, the same as the unpadded chain. With the acknowledge moved to the end
1226
+ of the dispatcher, the same padded chain reads 127: the flag from line 260
1227
+ is cleared by the late acknowledge, slot 2 waits for line 260 of the next
1228
+ frame, and the chain takes two frames per lap. A late slot is recoverable; a
1229
+ lost one halves the frame rate. Arming the next line before the handler
1230
+ runs is what makes the pending interrupt possible: with the arm after the
1231
+ handler, `$D012` still holds the current line while the handler overruns and
1232
+ nothing is raised at all, which is the same lost frame.
1233
+
1234
+ **The next-line arm and RST8.** `$D012` holds bits 0 to 7 of the compare
1235
+ line and `$D011` bit 7 holds bit 8. Writing only `$D012` for a line at or
1236
+ above 256 arms line minus 256. The `AND #$7F / ORA` keeps YSCROLL, DEN, RSEL
1237
+ and the mode bits, so the dispatcher can arm any line without knowing what
1238
+ the display is doing. The recipe's slot at 260 lands where the table says
1239
+ (measured: blue border from line 261 in the right border, 262 at the left,
1240
+ both models).
1241
+
1242
+ **The wrap at the frame top.** The last row arms the first row's line,
1243
+ which is smaller than its own. The compare is not reached again in this
1244
+ frame, so the next interrupt is at that line in the next frame. Nothing
1245
+ special is needed for the wrap beyond the index going back to 0; the
1246
+ raster counter's own reset to 0 does it. This is also why the index and
1247
+ the table order must agree: a row out of raster order is armed after its
1248
+ line has passed and the whole chain waits a frame for it.
1249
+
1250
+ **A frame counter and the music tick.** Step 4 runs once per lap of the
1251
+ table, which is once per frame as long as no slot is lost. Put the 16-bit
1252
+ frame counter increment and the `JSR` to the music player there and only
1253
+ there. A music call in a slot handler runs once per frame too, but it then
1254
+ sits on that slot's line and its data-dependent length eats that slot's
1255
+ margin (frame_sync_loop above measures a player's band with the border).
1256
+ The counter is the game's clock: in the recipe it reads 254 after
1257
+ 8,000,000 cycles on PAL and 458 after 12,000,000, a difference of 204 for
1258
+ 4,000,000 cycles, which is 4,000,000 / 19,656 = 203.5 frames (measured in
1259
+ VICE; the frame length is the settled constant).
1260
+
1261
+ **A slot's handler must finish before the next slot's line.** The deadline
1262
+ for a handler is the next row's line minus the dispatcher's exit and
1263
+ re-entry: the handler's `RTS`, steps 4 and 5 (about 40 cycles idle, plus the
1264
+ frame work at the wrap), the interrupt sequence and the KERNAL dispatcher
1265
+ (36 cycles to `$0314`, settled) and steps 1 and 2 again. A handler that ends
1266
+ later than that makes the next slot late by the overrun; one that ends
1267
+ after the next line has been and gone makes it late by the whole overrun
1268
+ plus the re-entry, as measured above. Budget each slot as (next line minus
1269
+ this line) × 63 cycles on PAL, less 40 to 43 for each badline in between
1270
+ and less about 150 for the dispatcher (arithmetic from the settled
1271
+ constants and the cycle counts below).
1272
+
1273
+ ### Why it works
1274
+
1275
+ The VIC raises IRST in `$D019` when its raster counter equals the nine-bit
1276
+ compare value, once per frame per value, and holds /IRQ low while IRST and
1277
+ ERST (`$D01A` bit 0) are both set. Writing 1 to `$D019` bit 0 clears IRST
1278
+ and nothing else. The compare value can be changed at any time; the next
1279
+ match is at the new line, in this frame if it is still ahead of the beam
1280
+ and in the next frame if not. That is the whole mechanism: one compare
1281
+ register, re-pointed once per interrupt, walks the beam through the table.
1282
+ Because the interrupt is level-triggered, a match that arrives while the
1283
+ CPU has interrupts disabled is not lost as long as IRST is still set when
1284
+ `RTI` clears the I flag; that is why the acknowledge belongs at the start of
1285
+ the dispatcher and not at its end.
1286
+
1287
+ The colour write of the recipe's handlers completes about 111 cycles after
1288
+ the start of the interrupt's line, plus 0 to 6 cycles of jitter: 36 to the
1289
+ first instruction of the dispatcher (settled), 69 through steps 1 to 3 to
1290
+ the handler's first instruction, and 6 for its `LDA #` and `STA` (counted
1291
+ from the listing). That is cycle 48 of the line after the one in the table,
1292
+ so every band in the recipe begins one line below its table entry, part-way
1293
+ across. The picture agrees: on line 41, which is all border, the new colour
1294
+ begins at x = 305 in the PAL PNG (x = 304 is one light grey pixel, VICE's
1295
+ rendering of the VIC's grey dot on a colour-register write, not examined
1296
+ further here) and at x = 281 on NTSC, and lines 132 and 261 show the change
1297
+ in the right border and not the left. With the KERNAL out (the variant
1298
+ below) the same write lands at x = 169 on PAL, 136 pixels or 17 cycles
1299
+ earlier, against 16 from the listing: the 29-cycle KERNAL dispatcher
1300
+ replaced by 13 cycles of the handler's own register saves. A chain that
1301
+ needs the change at the left edge of the line arms each row one line early
1302
+ and spins on `$D012` inside the handler, as `raster_bars` describes, or
1303
+ uses `stable_raster_irq` for the slots that need it; this technique on its
1304
+ own does neither, and the recipe says where its edges are.
1305
+
1306
+ ### Variations
1307
+
1308
+ **Hardware vector, KERNAL out.** Point `$FFFE/$FFFF` at the dispatcher and
1309
+ set `$01` to `$35`. The dispatcher must then save and restore A, X and Y
1310
+ itself and end in `RTI`; there is no `$EA81`. CIA2's NMI needs masking too
1311
+ (`$7F` to `$DD0D`, read once) or a vector at `$FFFA/$FFFB`. Measured in VICE
1312
+ with the recipe's table: identical band lines and an identical frame count
1313
+ of 254 at 8,000,000 cycles, and the colour write 17 cycles earlier as above.
1314
+ The entry and exit of that variant:
1315
+
1316
+ ```asm
1317
+ irq:
1318
+ pha // no KERNAL dispatcher: save the registers yourself
1319
+ txa
1320
+ pha
1321
+ tya
1322
+ pha
1323
+ lda #$01
1324
+ sta $d019
1325
+ // ... steps 2 to 4 as in the $0314 form ...
1326
+ pla // what $EA81 would have done
1327
+ tay
1328
+ pla
1329
+ tax
1330
+ pla
1331
+ rti
1332
+
1333
+ install:
1334
+ sei
1335
+ lda #$7f
1336
+ sta $dc0d
1337
+ sta $dd0d
1338
+ lda $dc0d
1339
+ lda $dd0d
1340
+ lda #<irq
1341
+ sta $fffe // RAM under the ROM; read once $01 = $35
1342
+ lda #>irq
1343
+ sta $ffff
1344
+ lda #<nmi
1345
+ sta $fffa
1346
+ lda #>nmi
1347
+ sta $fffb
1348
+ lda #$35
1349
+ sta $01
1350
+ lda #$01
1351
+ sta $d01a
1352
+ sta $d019
1353
+ cli
1354
+ rts
1355
+ nmi:
1356
+ rti
1357
+ ```
1358
+
1359
+ **KERNAL housekeeping once a frame.** Exit through `$EA31` instead of
1360
+ `$EA81` at the wrap only, with CIA1 left masked, to keep the jiffy clock and
1361
+ keyboard scan alive: about 186 cycles idle and about 1,600 with a key held
1362
+ (`recipes/kickassembler/raster-bars.md`, measured there). Every other exit
1363
+ stays on `$EA81`.
1364
+
1365
+ **Data-only slots.** For slots that only write registers, replace the
1366
+ handler address with a (register, value) list and let the dispatcher write
1367
+ it, which is what Oscar64's `rirq_write` compiles to. Fewer bytes per slot
1368
+ and a fixed time per write; no code per slot.
1369
+
1370
+ **Stable slots.** A row whose handler needs cycle-exact timing can carry a
1371
+ flag that makes the dispatcher enter it through the `double_irq` protocol
1372
+ while the other rows use the plain entry. Not run here.
1373
+
1374
+ ### Cycle budget
1375
+
1376
+ Per interrupt, from the interrupt's line start, counted from the listing:
1377
+ 36 cycles to the dispatcher (settled), 69 through the acknowledge, arm and
1378
+ call to the handler's first instruction, 6 for its `RTS`, 14 through the
1379
+ advance to `JMP $EA81` when the index does not wrap, 25 for `$EA81` through
1380
+ `RTI` (measured for `recipes/kickassembler/raster-bars.md`). About 150
1381
+ cycles of overhead per slot with the handler empty, two and a half PAL
1382
+ lines; the x position of the colour write above is consistent with the
1383
+ count but was not converted to a cycle number here. The wrap adds the
1384
+ 16-bit increment and whatever the music player and frame work cost; the
1385
+ recipe's decimal print is a few hundred cycles, not measured.
1386
+
1387
+ ### Recipes
1388
+
1389
+ - `recipes/kickassembler/irq-chain.md`
1390
+
1391
+ ### Sources
1392
+
1393
+ - VICE 3.10, `x64sc`, models `default` and `ntsc`, 8,000,000 and
1394
+ 12,000,000 cycles: every figure marked measured, read from the exit PNG
1395
+ with PIL and the character ROM.
1396
+ - This repository: `recipes/kickassembler/raster-bars.md` (the ring form,
1397
+ `$EA31` and `$EA81` costs, where the write lands),
1398
+ `recipes/kickassembler/cracktro-template.md` (a thirteen-handler ring and
1399
+ its badline lesson), `recipes/oscar64/raster-bars.md` (`rasterirq.h`,
1400
+ the same table with a sorter), `frame_sync_loop` above (the once-a-frame
1401
+ tick).
1402
+
1403
+ ## raster_profile_bars — Per-subsystem border bars and a CIA timer table
1404
+
1405
+ **Complexity:** low
1406
+ **Region:** both
1407
+ **Uses registers:** D020, DD04, DD05, DD0E
1408
+ **Requires:** frame_sync_loop
1409
+ **Cost:** cycles_per_frame=467
1410
+ **Cost basis:** measured-vice
1411
+ **Cost measured on:** oscar64-raster-profile-bars (worst frame, screen blanked)
1412
+
1413
+ ### Why
1414
+
1415
+ `frame_sync_loop` above gives one budget bar and a dropped-frame count:
1416
+ it says a frame overran, not which part of the game did it. Games answer
1417
+ that with one border colour per subsystem, so the border becomes a
1418
+ stacked bar of where the frame goes, behind a build switch so the release
1419
+ carries none of it. c64gameframework has one assembly-time switch per
1420
+ subsystem (`SHOW_PLAYROUTINE_TIME`, `SHOW_SPRITEIRQ_TIME`,
1421
+ `SHOW_SCROLLWORK_TIME`, `SHOW_CHARSETANIM_TIME`, `SHOW_SKIPPED_FRAME`,
1422
+ `SHOW_FREE_TIME` in `main.s`, used in `raster.s` and `screen.s`); Corescape
1423
+ colours the border between its stages under one `TIME_DEBUG` define
1424
+ (`enemies.h`, used in `display.cpp`). A bar is readable at a glance but
1425
+ only to the line. For exact numbers, bracket each subsystem with a CIA
1426
+ timer as well and keep the last and worst count per subsystem in a table.
1427
+
1428
+ ### How
1429
+
1430
+ **Bars.** At the start of each subsystem, store its colour to `$D020`;
1431
+ after the last one, store the idle colour. Give every subsystem its own
1432
+ colour and keep the order fixed, so a band's position identifies it. Wrap
1433
+ the stores in a macro that compiles to nothing when the switch is 0.
1434
+
1435
+ **Reading a bar.** Its height in raster lines is the subsystem's
1436
+ duration: 63 cycles a line on PAL, 65 on the 6567R8. A screenshot turns
1437
+ this into numbers with no eye involved: read the border column (x = 2) of
1438
+ VICE's exit PNG with PIL, map each pixel to a subsystem by its palette
1439
+ triple, and convert PNG row to raster line (PAL line = row + 16; NTSC line
1440
+ = row + 28, and NTSC rows 235 to 246 are lines 0 to 11 of the next frame;
1441
+ `runtime/vice-reference.md`, "Reading the exit screenshot"). The recipe
1442
+ carries the snippet. Lines 288 to 311 and 0 to 15 on PAL, and 12 to 27 on
1443
+ NTSC, are not in the PNG at all, so a bar there cannot be read headless:
1444
+ a loop synced at line 251 profiles into that gap, and either moves the
1445
+ sync line for a profiling build or relies on the table.
1446
+
1447
+ **Bars are wall time.** The beam does not wait for the CPU. On a badline
1448
+ the VIC-II takes 40 to 43 cycles (`pitfalls/raster-and-badline.md`,
1449
+ `badline_cycle_loss`), so the same code covers more lines inside the
1450
+ display window than in the border. Measured in the recipe (VICE x64sc
1451
+ 3.10): a busy loop of 3,255 CPU cycles took 3,556 cycles in the display,
1452
+ seven badlines at 43 each. A bar that grows when its subsystem moves down
1453
+ the screen has not got slower. Sprite DMA stretches bars and the timer the
1454
+ same way, 5 to 19 cycles a line with sprites on (`vic_bus_takeover_on_dma`).
1455
+
1456
+ **The table.** Bracket each subsystem with CIA2 timer A: load the latch
1457
+ with `$FFFF` once, write `$11` to `$DD0E` to force-load and start, write
1458
+ `$00` to stop, then read `$DD04`/`$DD05` and subtract from `$FFFF`.
1459
+ Subtract the count of an empty start/stop pair (5 cycles in the recipe's
1460
+ build, measured). Store the result as the subsystem's last value and
1461
+ raise its maximum if larger. The timer counts phi2 cycles, stolen or not,
1462
+ so it measures the same wall time as the bar, to the cycle. For CPU
1463
+ cycles alone, run the same bracket once with the display blanked and
1464
+ sprites off (clear DEN and wait until line $30 (48) has passed with DEN
1465
+ clear, two `vic_waitFrame()` calls; the VIC-II samples DEN once per frame;
1466
+ and write 0 to `$D015`). An interrupt
1467
+ that fires inside a bracket is counted in that subsystem; mask them, or
1468
+ read `MAX` knowing one may be in it. CIA2 timer A is free while RS-232 is
1469
+ unused (`hardware/cia-reference.md`); mask its interrupt so it raises no
1470
+ NMI. It is also taken by any CIA2 timer NMI, such as NMI sample playback
1471
+ or the NMI lock that disables RESTORE (`hardware/cia-reference.md`); use a
1472
+ timer nothing else runs.
1473
+
1474
+ **Reading the table headless.** Print it, or dump it from the VICE
1475
+ monitor: a `-moncommands` file with `trace store` on the last byte the
1476
+ frame writes and `command 1 "m <table> <end>"` logs the table every
1477
+ frame, and the last dump in the log is the exit state (recipe, rung 1;
1478
+ the screenshot was byte-identical with and without the trace). Take the
1479
+ addresses from the build's map file.
1480
+
1481
+ ### Why it works
1482
+
1483
+ `$D020` is read by the VIC-II for every border pixel it draws, so a store
1484
+ shows within the same line (`frame_sync_loop` above). The
1485
+ interval between two stores is therefore drawn as one band, and its
1486
+ height counts the line starts inside the interval: a bar of W cycles is
1487
+ W / 63 lines, give or take one. The CIA timer runs on the same phi2 clock
1488
+ as the VIC-II's raster, so the two methods must agree to that
1489
+ quantisation, and in the recipe they do: bar cycles less the timer's
1490
+ figure came to 84 to 135 across all eight bars (PAL and NTSC), against
1491
+ 117 cycles of profiling code per subsystem outside the timer and one line
1492
+ of 63 or 65.
1493
+
1494
+ ### Variations
1495
+
1496
+ **Free time.** Mark the waits instead of the work and the band shows
1497
+ what is left. c64gameframework's `SHOW_FREE_TIME` does it with `DEC $D020`
1498
+ before each wait loop and `INC $D020` after, which needs no colour table
1499
+ and works over any base colour.
1500
+
1501
+ **Bars only.** Five `$D020` stores cost 42 cycles a frame for four
1502
+ subsystems (measured, below). This is the form to leave in a debug build
1503
+ all the time.
1504
+
1505
+ **Table only.** For a subsystem shorter than a line, or bars that would
1506
+ fall in the lines the PNG does not show.
1507
+
1508
+ **Worst frame.** `MAX` is the number a budget needs, not `LAST`. A
1509
+ subsystem with a rare expensive frame (a spawn, a column carry) shows it
1510
+ only in `MAX`: the recipe's actors subsystem spikes by 10 blocks one frame
1511
+ in 64, and its `MAX` read 4,695 against a `LAST` of 3,556 on PAL.
1512
+
1513
+ ### Cycle budget
1514
+
1515
+ Measured in VICE x64sc 3.10 by timing the recipe's whole frame of work
1516
+ with CIA1 while blanked, built four ways
1517
+ (`recipes/oscar64/raster-profile-bars.md`): 7,374 cycles with both
1518
+ switches off, 7,416 with bars only (+42), 7,818 with the table only
1519
+ (+444) and 7,841 with both (+467), for four subsystems. The measured run
1520
+ updated all four maxima, the dearest path, so 467 is the worst frame;
1521
+ without a new maximum it is about 60 cycles less (arithmetic from the
1522
+ listing, not measured). In the recipe's Oscar64 build most of the table's
1523
+ cost is the call and 16-bit compare of the record routine; a hand-written
1524
+ assembly record would be cheaper (not measured here).
1525
+
1526
+ ### Recipes
1527
+
1528
+ - `recipes/oscar64/raster-profile-bars.md`
1529
+
1530
+ ### Sources
1531
+
1532
+ - c64gameframework (MIT), https://github.com/cadaver/c64gameframework,
1533
+ `main.s` (the `SHOW_*` switches), `raster.s` and `screen.s` (where they
1534
+ colour the border), read here for names only.
1535
+ - Corescape (GPL-3.0), https://github.com/drmortalwombat/corescape,
1536
+ `enemies.h` (`TIME_DEBUG`) and `display.cpp` (the border colours between
1537
+ stages), read here for names only.
1538
+ - VICE 3.10, `x64sc`, models `default` and `ntsc`, 8,000,000 cycles:
1539
+ every figure marked measured, from the exit PNG and the monitor log.
1540
+ - This repository: `frame_sync_loop` above (the single budget bar),
1541
+ `pitfalls/raster-and-badline.md` (`badline_cycle_loss`),
1542
+ `hardware/cia-reference.md` (CIA2 timer A and RS-232),
1543
+ `runtime/vice-reference.md` (screenshot geometry, palette, monitor).