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,1570 @@
1
+ ---
2
+ category: cpu
3
+ chip: 6510
4
+ ---
5
+
6
+ <!-- doc-type: technique-reference -->
7
+
8
+ # CPU Cycle Tricks and Optimization
9
+
10
+ The 6510 in the Commodore 64 runs at roughly 1 MHz. The discipline of C64 optimization is per-instruction accounting: how many cycles does this sequence cost, and what is the tightest arrangement that produces the same result?
11
+
12
+ The techniques here range from portable 6502-family idioms (zero-page addressing, loop unrolling, jump tables) to C64-specific methods that exploit the 6510's coexistence with the VIC-II (phase-inverted IRQs, DMA-steal avoidance). Together they are the toolkit for squeezing effects — open borders, 24-sprite multiplexing, SID music, hardware scrolling — into a single frame.
13
+
14
+ Cycle counts throughout refer to CPU phi2 cycles: 63 per line on PAL (985,248 Hz), 65 per line on NTSC (1,022,727 Hz). See [6510-cpu-reference.md](../hardware/6510-cpu-reference.md) for the full instruction timing table.
15
+
16
+ ---
17
+
18
+ ## self_modifying_code — In-place opcode/operand mutation
19
+
20
+ **Complexity:** medium
21
+ **Region:** both
22
+ **Uses kernal:** (none)
23
+
24
+ ### Why
25
+
26
+ Every load-and-store pair in a tight inner loop consumes cycles loading a parameter and then consuming it. If the parameter changes across outer-loop iterations but is constant within a given inner-loop run, the canonical structure is to load the parameter once, store it somewhere, then read it again on each inner-loop iteration. The load-and-store costs two instructions per iteration, even when the value has not changed since the last reload.
27
+
28
+ Self-modifying code eliminates the runtime reload. The outer loop writes the variable directly into the operand byte of the instruction that will use it. When the inner loop runs, the operand is already baked into the code stream.
29
+
30
+ ### How
31
+
32
+ The pattern targets `LDA #imm` (opcode $A9, 2 bytes: $A9 followed by the immediate value). The immediate operand lives at the byte address `instruction_address + 1`. The outer loop writes directly to that address:
33
+
34
+ ```asm
35
+ ; Outer loop: compute new fill color in A, patch the inner loop
36
+ lda new_color
37
+ sta fill_loop_lda + 1 ; overwrite the LDA #imm operand byte
38
+
39
+ ; Inner loop: runs N times using the baked-in operand
40
+ fill_loop_lda:
41
+ lda #$00 ; operand at fill_loop_lda+1 is modified above
42
+ sta $0400,x
43
+ dex
44
+ bne fill_loop_lda
45
+ ```
46
+
47
+ The same pattern applies to the low byte of an absolute-address operand (at `instruction + 1`) and the high byte (at `instruction + 2`). Modifying both low and high bytes lets you redirect a loop to target a different page on each outer iteration without any indirect addressing overhead.
48
+
49
+ KickAssembler provides a clean way to express this without computing the +1 offset manually:
50
+
51
+ ```asm
52
+ // KickAssembler: define the operand label explicitly
53
+ lda_color:
54
+ lda #$00
55
+ .const lda_color_operand = lda_color + 1
56
+ // later:
57
+ lda desired_color
58
+ sta lda_color_operand
59
+ ```
60
+
61
+ ### Why it works
62
+
63
+ The 6510 has no instruction cache or prefetch buffer. A write to any RAM address takes effect before the next instruction fetch from that address. The self-modified byte is visible immediately on the next iteration. CMOS derivatives (65C02, 65816) have prefetch buffers and do not share this property — self-modification is a reliable NMOS-6510-specific technique.
64
+
65
+ ### Variations
66
+
67
+ **Opcode mutation.** Write a new opcode byte at runtime to switch instruction behavior — e.g., `BNE` ($D0) to `BEQ` ($F0) to flip a branch condition without a test. Used in loaders and copy-protection stubs. Use sparingly: opcode mutation is hard to debug.
68
+
69
+ **ROM-resident risk.** Self-modification requires writable RAM. If the patched code is in a region where ROM is mapped (KERNAL/BASIC enabled via $01 bits 0-1), writes land in the RAM beneath but instruction fetches hit the ROM — the modification has no effect. Keep self-modifying code segments in regions with no ROM overlay ($1000-$7FFF is always RAM on a stock C64).
70
+
71
+ ### Cycle budget
72
+
73
+ Compared to a load-from-variable approach:
74
+
75
+ | Approach | Cycles per inner-loop parameter access |
76
+ |---|---|
77
+ | LDA variable_addr (absolute) | 4 cycles |
78
+ | LDA #baked (after self-mod patch) | 2 cycles |
79
+ | Savings per access | 2 cycles |
80
+
81
+ For a 256-iteration inner loop, the savings are 512 cycles per outer-loop iteration — approximately 8 full raster lines worth of CPU time on PAL (512 / 63 = 8.1; an earlier version of this page said 26 lines, which does not follow from 63 cycles per line).
82
+
83
+ ### Recipes
84
+
85
+ - No recipe yet. (An earlier version of this page pointed at `recipes/kickassembler/cracktro-template.md`; that recipe writes zero-page pointers for `(zp),y` indirection and rotates its bar colours through a `palette` table, and contains no self-modifying code.)
86
+
87
+ ---
88
+
89
+ ## unrolled_loops — Loop unrolling at assembly level
90
+
91
+ **Complexity:** low
92
+ **Region:** both
93
+ **Uses kernal:** (none)
94
+
95
+ ### Why
96
+
97
+ Every loop iteration carries overhead: DEX/DEY (2 cycles) plus BNE taken (3 cycles) = 5 cycles per iteration. For inner loops where N is known at assembly time, replicating the body N times eliminates this overhead entirely. The tradeoff is linear code-size growth — an 8x-unrolled body occupies 8x the bytes.
98
+
99
+ ### How
100
+
101
+ Manual unrolling replicates the body and adjusts any index increments:
102
+
103
+ ```asm
104
+ ; Rolled version: 8 iterations, 8 * (body + DEX + BNE) = 8 * (N + 5) cycles
105
+ ldx #8
106
+ loop:
107
+ lda source,x
108
+ sta dest,x
109
+ dex
110
+ bne loop
111
+
112
+ ; Unrolled 8x: no DEX/BNE, 8 * body_cycles only
113
+ lda source+8
114
+ sta dest+8
115
+ lda source+7
116
+ sta dest+7
117
+ lda source+6
118
+ sta dest+6
119
+ lda source+5
120
+ sta dest+5
121
+ lda source+4
122
+ sta dest+4
123
+ lda source+3
124
+ sta dest+3
125
+ lda source+2
126
+ sta dest+2
127
+ lda source+1
128
+ sta dest+1
129
+ ```
130
+
131
+ KickAssembler's `.for` directive makes this mechanical and readable:
132
+
133
+ ```asm
134
+ .for (var i = 8; i >= 1; i--) {
135
+ lda source + i
136
+ sta dest + i
137
+ }
138
+ ```
139
+
140
+ The assembler generates the fully unrolled sequence at build time. The resulting binary contains 8 `LDA/STA` pairs and no branch instructions.
141
+
142
+ ### Why it works
143
+
144
+ Branch instructions cost 3 cycles when taken (4 on page-cross); DEX/DEY cost 2 cycles. Removing them by replication is safe when the loop count is compile-time constant and the body has no mid-loop counter dependencies. Unrolled loops are also simpler to cycle-count: the budget is `N * (LDA + STA)` with no branch considerations.
145
+
146
+ ### Variations
147
+
148
+ **Partial unroll.** Unrolling 8x (rather than full-N) converts a 256-iteration loop to 32 iterations of an 8x body: `32 * (8*B + 5)` vs `256 * (B + 5)`. For B=10 this cuts overhead from 1280 to 160 cycles.
149
+
150
+ **Computed entry point.** For non-power-of-two trip counts, unroll to the next power of two and compute an entry offset to jump into the middle of the unrolled sequence, avoiding a remainder loop.
151
+
152
+ **Fall-through table.** Replicate the body N times; compute the entry as `table_start + (N - count) * body_bytes`. Execution falls through exactly the required number of iterations with no loop at all.
153
+
154
+ ### Cycle budget
155
+
156
+ For body B cycles, N iterations: rolled = `N*(B+5)`, 8x unrolled = `(N/8)*(8*B+5)`, fully unrolled = `N*B`. Speedup at 8x: 1.41x for B=10, 1.95x for B=4. Smaller bodies benefit more, which is why unrolling is most valuable for LDA/STA memory-copy loops (B=7).
157
+
158
+ ### Recipes
159
+
160
+ - `recipes/kickassembler/sine-scroller.md` (unrolled character-copy inner loop)
161
+
162
+ ---
163
+
164
+ ## illegal_opcode_tricks — Useful undocumented opcodes
165
+
166
+ **Complexity:** high
167
+ **Region:** both
168
+ **Uses kernal:** (none)
169
+
170
+ ### Why
171
+
172
+ The MOS 6510 has 256 possible opcode bytes, of which only 151 are officially documented. The remaining 105 execute and produce side effects determined by the chip's microcode decode matrix. On a stock C64, the safe subset (approximately 30 opcodes) is fully reliable across all NMOS 6510 and 8500 parts. Each saves 1-3 cycles and/or 1-2 bytes compared to the legal sequence it replaces.
173
+
174
+ See [6502-illegal-opcodes.md](../hardware/6502-illegal-opcodes.md) for the full reference, including per-opcode cycle counts, flag effects, and stability ratings. This section covers practical application patterns.
175
+
176
+ ### How
177
+
178
+ The most useful illegal opcodes by use case:
179
+
180
+ **LAX (Load A and X)** — `A = X = M`. Zero-page form ($A7): 3 cycles / 2 bytes vs. `LDA zp : LDX zp` at 6 cycles / 4 bytes. Classic sprite-multiplexer use: load a Y-coordinate and simultaneously have it as a table index. Note: `LAX #imm` ($AB) is **unstable** — floating internal bus produces wrong results on some 6510 runs. Use only memory-addressed forms.
181
+
182
+ **SAX (Store A AND X)** — `M = A & X`. No flags affected. When X holds a nibble mask, `SAX zp` writes the masked accumulator in 3 cycles/2 bytes vs. `AND #mask : STA zp` at 5 cycles/4 bytes (and A keeps its unmasked value, so SAX is not a drop-in replacement where the masked A is needed afterwards). Measured in VICE x64sc: AND #imm 2 + STA zp 3 = 5; an earlier version of this page said 7.
183
+
184
+ **AXS (AND A with X, subtract into X)** — `X = (A & X) - imm`. Sets flags like CMP. Decimal mode ignored — always binary. Fused mask-and-decrement: `AXS #1 : BNE loop` = 5 cycles vs. `TXA : AND #mask : TAX : DEX : BNE loop` = 11 cycles (measured in VICE x64sc: AXS #imm 2 + BNE taken 3; TXA/AND/TAX/DEX 8 + BNE taken 3; an earlier version of this page said 4 and 9).
185
+
186
+ **ALR (AND then LSR)** — `A = (A & imm) >> 1`. Two cycles / 2 bytes. Equivalent to `AND #imm : LSR A` (4 cycles / 3 bytes — AND #imm is 2 bytes and LSR A is 1; an earlier version of this page said 4 bytes).
187
+
188
+ **ARR (AND then ROR with quirky flags)** — `A = ROR(A & imm)`. In binary mode, C = bit 6 of result (not bit 0 as normal ROR). V = bit 6 XOR bit 5. The non-standard flag semantics come from an internal half-adder; useful in CRC routines.
189
+
190
+ **DCP (Decrement then Compare)** — `M-- ; compare A to new M`. Zero-page form ($C7): 5 cycles. Legal `DEC zp : CMP zp`: 8 cycles (DEC zp 5 + CMP zp 3, measured in VICE x64sc; an earlier version of this page said 7).
191
+
192
+ **RMW family — SLO, RLA, SRE, RRA:** Each combines a memory read-modify-write (ASL/ROL/LSR/ROR) with an accumulator combine (ORA/AND/EOR/ADC). Every one saves 2 bytes and 3 cycles in zero page (4 in abs and zp,X, 4-5 in abs,X) vs. the equivalent legal pair — the legal pair is 5 + 3 = 8 in zero page against the illegal's 5 (an earlier version of this page said 2-3 cycles and tabulated 2). These are the workhorses of cycle-tight sprite multiplexer and raster code.
193
+
194
+ | Mnemonic | Equivalent legal pair | Cycles saved (zp) |
195
+ |---|---|---|
196
+ | SLO | ASL zp : ORA zp | 3 |
197
+ | RLA | ROL zp : AND zp | 3 |
198
+ | SRE | LSR zp : EOR zp | 3 |
199
+ | RRA | ROR zp : ADC zp | 3 |
200
+
201
+ ### Why it works
202
+
203
+ The 6502 decode matrix assigns addressing modes to columns and operations to rows. "Illegal" cells activate whatever micro-operations the (column, row) pair selects — typically two operation strobes simultaneously. For the safe tier (LAX, SAX, ALR, ARR, ANC, AXS, DCP, ISC, SLO, RLA, SRE, RRA), the fused operations use disjoint internal paths and are deterministic across every NMOS 6510 and 8500 ever produced. For the unstable tier (XAA/ANE, LAX #imm, AHX, TAS, SHX, SHY), a floating internal bus produces results that vary by die revision, batch, and thermal state. Do not use unstable opcodes in production code.
204
+
205
+ ### Variations
206
+
207
+ **Undocumented NOPs for cycle padding.** The six 1-byte NOPs ($1A, $3A, $5A, $7A, $DA, $FA) cost 2 cycles / 1 byte — tighter than `BIT zp` (3 cycles / 2 bytes). Used in cycle-exact raster code to add exactly 2 cycles without consuming a branch slot or growing code by 2 bytes.
208
+
209
+ ### Cycle budget
210
+
211
+ Per call site: LAX zp saves 3 cycles vs LDA+LDX; SAX zp saves 2 vs AND+STA; ALR saves 2 vs AND+LSR; DCP zp saves 3 vs DEC+CMP; SLO zp saves 3 vs ASL+ORA (measured in VICE x64sc; an earlier version of this page had SAX saving 5 and DCP/SLO saving 2). In a 20-entry sprite multiplexer, these accumulate to 30-60 cycles per raster line — enough to free an extra badline slot.
212
+
213
+ ### Recipes
214
+
215
+ - No recipe yet. (An earlier version of this page pointed at `recipes/kickassembler/sprite-multiplex-24.md` and `recipes/kickassembler/cracktro-template.md`; neither uses an illegal opcode.)
216
+
217
+ ---
218
+
219
+ ## jump_table_dispatch — JMP indirect through a table
220
+
221
+ **Complexity:** low
222
+ **Region:** both
223
+ **Uses kernal:** (none)
224
+
225
+ ### Why
226
+
227
+ A CMP #imm/BEQ pair costs 4 cycles when it falls through and 5 when it matches (6 if the taken branch crosses a page), so an N-way chain costs 4N+1 cycles in the worst case — O(N). (An earlier version of this page charged 5 cycles per case; the untaken BEQ is 2, not 3.) A jump table reduces any N-way dispatch to a fixed ~26 cycles regardless of N, using two table lookups and a self-modified JMP.
228
+
229
+ ### How
230
+
231
+ Build a table of 16-bit routine addresses in memory (low byte followed by high byte per entry, standard 6510 little-endian):
232
+
233
+ ```asm
234
+ dispatch_table:
235
+ .word handle_state_0
236
+ .word handle_state_1
237
+ .word handle_state_2
238
+ .word handle_state_3
239
+ ; ... up to 256 entries
240
+
241
+ dispatch:
242
+ lda current_state ; 4 cycles (abs) or 3 (zp)
243
+ asl ; 2 cycles — multiply by 2 for word offset
244
+ tax ; 2 cycles
245
+ lda dispatch_table,x ; 4 cycles — low byte of address
246
+ sta jmp_target+1 ; 4 cycles — patch low byte of JMP operand
247
+ lda dispatch_table+1,x ; 4 cycles — high byte of address
248
+ sta jmp_target+2 ; 4 cycles — patch high byte of JMP operand
249
+ jmp_target:
250
+ jmp $0000 ; 3 cycles — lands at the patched address
251
+ ```
252
+
253
+ Total: 3+2+2+4+4+4+4+3 = 26 cycles for any dispatch, regardless of the number of cases.
254
+
255
+ The self-modification pattern used here (`STA jmp_target+1 / +2`) is the same technique described in `self_modifying_code`. The JMP absolute opcode ($4C) occupies 3 bytes: opcode at `jmp_target`, low byte at `jmp_target+1`, high byte at `jmp_target+2`.
256
+
257
+ An alternative is `JMP (ind)` ($6C, 5 cycles) through a zero-page pointer: copy the table entry into the pointer (`STA ptr` / `STA ptr+1`, 3 cycles each) and jump through it. The 6510 has no `JMP (abs,X)`, so the copy is unavoidable; the total is 3+2+2+4+3+4+3+5 = 26 cycles — the same as the STA-patch form (measured 27 vs 27 in VICE x64sc with an absolute state byte, 26 vs 26 with a zero-page one). Its only advantage is that the code stays read-only (usable from ROM or shared code). An earlier version of this page described X indexing the table directly through `JMP ($abs)` at 21 cycles; that addressing mode does not exist on the 6510. Caveat: the 6502 JMP indirect page-wrap bug — if the pointer's low byte sits at $xxFF, the high byte is fetched from $xx00 instead of $(xx+1)00. With this form the constraint is on the *pointer*, not the table; a zero-page pointer at $FF/$00 would trigger it, so avoid $FF. Separately, keep the table itself from straddling a page: each `LDA table,X` that crosses adds a cycle (measured 28 rather than 26 with the table at $09FE).
258
+
259
+ ### Why it works
260
+
261
+ `JMP $abs` ($4C, 3 cycles) transfers control to the 2-byte address at opcode+1. Writing new values there changes the destination. The 6510 has no instruction cache, so the patched address is visible immediately on the next JMP execution.
262
+
263
+ ### Variations
264
+
265
+ **RTS table dispatch.** Store `(address - 1)` in the table. Push high then low byte, execute `RTS`. The 6510's RTS adds 1 to the popped address. Avoids self-modification but is not faster: `LDA tbl+1,X / PHA / LDA tbl,X / PHA / RTS` is 4+3+4+3+6 = 20 cycles (measured in VICE x64sc; 4 for each `LDA abs,X` assumes the table does not cross a page boundary, +1 each if it does), and with the same LDA/ASL/TAX prologue (7) the dispatch is 27 cycles — one more than the 26 of the STA-patch form. An earlier version of this page said 12-15 cycles and "slower for large N"; the cost is constant in N.
266
+
267
+ **Persistent X.** If the state index is already in X, the prologue shrinks to `TXA : ASL : TAX` (6 cycles), saving 1-2 cycles (the listed prologue is 7 with a zero-page state byte, 8 with an absolute one; an earlier version said 1-4); store the index pre-doubled and the prologue disappears entirely (dispatch = 19 cycles).
268
+
269
+ ### Cycle budget
270
+
271
+ | Approach | Cycles (N=4) | Cycles (N=8) | Cycles (N=16) |
272
+ |---|---|---|---|
273
+ | CMP/BEQ chain (worst case) | 17 | 33 | 65 |
274
+ | CMP/BEQ chain (average case, uniform) | 11 | 19 | 35 |
275
+ | Jump table (self-mod) | 26 | 26 | 26 |
276
+ | Jump table (indirect JMP via pointer) | 26 | 26 | 26 |
277
+
278
+ Worst case is 4(N−1)+5 = 4N+1; the uniform average of 4k+5 over k = 0..N−1 is 2N+3. Either jump table (26 cycles) beats the chain from N=7 in the worst case (a 6-way chain is 25) and from N=12 on average. For state machines with 8+ states the jump table is preferred for its bounded worst case. An earlier version of this table charged the chain 5 cycles per case (20/40/80 worst case), gave the indirect-JMP table 21 cycles through a non-existent `JMP (abs,X)`, and put the break-even at N=3-6.
279
+
280
+ ### Recipes
281
+
282
+ - No recipe yet. (An earlier version of this page pointed at `recipes/oscar64/simple-shmup.md`; that recipe has no switch or jump-table dispatcher.)
283
+
284
+ ---
285
+
286
+ ## zero_page_burst — Zero-page abuse for cycle savings
287
+
288
+ **Complexity:** low
289
+ **Region:** both
290
+ **Uses kernal:** (none)
291
+
292
+ ### Why
293
+
294
+ The 6510 has two addressing modes that reference the first 256 bytes of the address space (the "zero page"): zero-page and zero-page indexed. These modes encode the address in one byte instead of two, making zero-page instructions 1 byte shorter than their absolute equivalents. More importantly, they execute 1 cycle faster: `LDA zp` costs 3 cycles versus `LDA abs` at 4 cycles; `STA zp` costs 3 versus 4; `LDA zp,X` costs 4 versus 4 for `LDA abs,X` (5 only when the indexed address crosses a page) — for indexed loads the zero-page form saves a byte, and a cycle only on page-crossing accesses; indexed stores are the exception, `STA zp,X` at 4 versus `STA abs,X` at a fixed 5. (Measured in VICE x64sc; an earlier version of this page gave `LDA abs,X` a flat 5.)
295
+
296
+ For a tight inner loop that accesses the same variable many times, moving that variable to zero page saves 1 cycle per access. In a loop that runs 256 iterations and reads two variables, that is 512 cycles — about 8 PAL raster lines (512 / 63 = 8.1; an earlier version of this page said 26).
297
+
298
+ ### How
299
+
300
+ Identify the hot variables in your inner loops and map them to zero-page addresses. The C64's zero-page layout has pre-allocated areas: $00 (CPU DDR) and $01 (I/O port / banking) are off-limits. $02 and $FB-$FE are the only bytes neither ROM touches after reset. $03-$8F is BASIC workspace (free once you never return to BASIC). $90-$FA is KERNAL working storage — the jiffy clock ($A0-$A2), keyboard buffer count ($C6), cursor/blink state ($CC-$CF), screen-line pointer ($D1-$D2), cursor column ($D3) and line-link table ($D9-$F2) are all above $BF and are written by the default IRQ every frame, so this range is unsafe while the KERNAL IRQ or CHROUT is in use, not merely without a full KERNAL replacement. $F7-$FA are the RS-232 buffer pointers, touched only by OPEN/CLOSE of device 2, which is why the demo convention of a 16-bit pointer at $FA-$FB survives in practice. $FF is BASIC's FOUT (number-to-string) scratch. (An earlier version of this page ended the KERNAL range at $BF and listed $FA-$FF as conventional free scratch.) See `docs/hardware/c64-memory-map.md` for the full layout.
301
+
302
+ Demos that take over the machine fully (disable BASIC and KERNAL ROMs, install custom IRQ/NMI/RESET handlers) can use $02-$FF minus $00/$01.
303
+
304
+ In KickAssembler, declare zero-page variables explicitly:
305
+
306
+ ```asm
307
+ .const zp_counter = $02
308
+ .const zp_color = $03
309
+ .const zp_ptr_lo = $FA // safe only because RS-232 (device 2) is never opened; $FB/$FC is the ROM-free choice
310
+ .const zp_ptr_hi = $FB
311
+
312
+ lda (zp_ptr_lo),y // 5 cycles — indirect indexed from zero page
313
+ sta zp_counter // 3 cycles — write to zero page
314
+ ```
315
+
316
+ ### Why it works
317
+
318
+ The 6510's bus cycle for zero-page addressing omits the high-byte address fetch. An absolute read requires: (1) fetch opcode, (2) fetch low address byte, (3) fetch high address byte, (4) read data. A zero-page read omits step 3 (the high byte is always $00) and does: (1) fetch opcode, (2) fetch zero-page address byte, (3) read data. One fewer bus cycle = one fewer CPU cycle. The instruction is also 1 byte shorter because the high address byte is absent from the instruction stream.
319
+
320
+ The (zp),Y addressing mode (indirect indexed) is 5 cycles regardless of page-crossing (unless the read causes a page cross, which adds +1), while abs,Y is 4 cycles with a +1 page-cross penalty. For pointer-dereferencing loops where Y walks through 256 bytes, (zp),Y is the standard approach: the pointer sits in zero page, Y is the offset, and the instruction fetches the 16-bit address from zero page and adds Y.
321
+
322
+ ### Variations
323
+
324
+ **Zero-page subroutine.** JSR is 6 cycles, RTS is 6 cycles. A subroutine call costs 12+ cycles of overhead. If the subroutine is small and called from a tight loop, inline it (loop unrolling), or if it must be separate, move its key shared state to zero page to minimize the parameter-passing overhead.
325
+
326
+ **Direct-page overlapping.** In the extreme case, move a small piece of the inner loop itself into zero page. Branch instructions have a range of ±127 bytes; if the branch target is in zero page and the branch source is in the low part of RAM, the branch will still reach. The instruction fetch cost is the same regardless of which RAM page the code occupies — but the zero-page code can be patched via zero-page STA instructions (3 cycles each versus 4 cycles for absolute STA), saving 1 cycle per self-modification.
327
+
328
+ ### Cycle budget
329
+
330
+ Switching 10 variables from absolute to zero-page in a 256-iteration loop:
331
+
332
+ - Savings per access: 1 cycle
333
+ - Accesses per iteration: assume 2 loads + 2 stores = 4 accesses
334
+ - Savings per iteration: 4 cycles
335
+ - Total savings over 256 iterations: 1024 cycles ≈ 16 PAL raster lines
336
+
337
+ For very tight raster effects (stable raster IRQ handlers, sprite multiplexers) where the cycle budget is measured in single digits per scanline, zero-page placement of all loop variables is non-negotiable.
338
+
339
+ ### Recipes
340
+
341
+ - No recipe yet. (An earlier version of this page pointed at `recipes/kickassembler/sprite-multiplex-24.md`; its sprite tables sit at `* = $1000`, not in zero page.)
342
+
343
+ ---
344
+
345
+ ## decimal_mode_pitfalls — SED/CLD in IRQs
346
+
347
+ **Complexity:** medium
348
+ **Region:** both
349
+ **Uses kernal:** (none)
350
+
351
+ ### Why
352
+
353
+ The 6510 inherits the 6502's BCD mode. `SED` makes `ADC`/`SBC` perform decimal arithmetic; `CLD` restores binary. Almost no C64 code uses BCD deliberately, but the D flag persists across interrupts unless explicitly cleared. If code sets D and an IRQ fires before the next `CLD`, the IRQ handler's arithmetic produces wrong results. The bug is intermittent — it surfaces only when an IRQ fires during the SED..CLD window.
354
+
355
+ ### How
356
+
357
+ The rule: any IRQ handler that uses ADC or SBC must clear D on entry with `CLD` and restore it before `RTI` if the interrupted code may have had D=1.
358
+
359
+ ```asm
360
+ my_irq_handler:
361
+ pha ; save A
362
+ txa ; save X
363
+ pha
364
+ tya ; save Y
365
+ pha
366
+ cld ; clear decimal mode — MANDATORY
367
+ ; ... handler body using ADC/SBC safely ...
368
+ pla ; restore Y
369
+ tay
370
+ pla ; restore X
371
+ tax
372
+ pla
373
+ rti
374
+ ```
375
+
376
+ (Generic 6502 syntax, one instruction per line; an earlier version wrote `txa : pha`, which is ACME/64tass statement syntax that KickAssembler and ca65 reject.)
377
+
378
+ The KERNAL IRQ path never executes CLD: neither the dispatcher at $FF48 nor the default service routine at $EA31 (nor the NMI path) contains a CLD — the KERNAL clears D only once, in its reset routine at $FCE6 (the sole CLD opcode on the reset/interrupt paths). Verified on the ROM bytes and in VICE x64sc: P captured at the service routine's exit ($EA7E) still has D=1 when the interrupted code had executed SED. The default KERNAL IRQ nevertheless does no harm to BCD code, by accident rather than design: RTI restores the interrupted P including D, and the only ADC/SBC on the whole default path are UDTIM's three compare-style SBCs ($F6AA/$F6AE/$F6B2), which use only the carry — and on the NMOS 6510 SBC's carry-out is identical in decimal and binary mode (all 65,536 operand pairs checked in VICE). Do not rely on that: any handler you chain through ($0314) or ($0318) inherits the caller's D, so a custom handler that uses ADC/SBC must CLD on entry. (An earlier version of this page said $EA31 executed CLD early in its sequence; the ROM bytes show no $D8 anywhere on that path.)
379
+
380
+ For code that deliberately uses BCD, bracket the BCD section as tightly as possible with `SED`/`CLD` to minimize the window where an IRQ can fire with D=1 active.
381
+
382
+ ### Why it works
383
+
384
+ The interrupt sequence pushes the status register P (7 cycles: PC hi, PC lo, P, then vector fetch). RTI pops P and restores all flags including D. The NMOS 6510 does NOT clear D on interrupt entry — this is a documented difference from the CMOS 65C02 (which does clear D on every interrupt). Any handler that uses ADC/SBC must do so explicitly via `CLD`.
385
+
386
+ ### Variations
387
+
388
+ **NMI handlers** at $FFFA/$FFFB have the same issue. NMI cannot be masked with `SEI`, so NMI handlers must also include `CLD` on entry if they use arithmetic.
389
+
390
+ **Diagnostic pattern.** Intermittent wrong arithmetic only during IRQ activity is the classic symptom of a missing `CLD`. Trace all `SED` paths to the next `CLD` in your codebase.
391
+
392
+ ### Cycle budget
393
+
394
+ `CLD` costs 2 cycles. `SED` costs 2 cycles. Adding them to an IRQ handler that runs every frame costs 4 cycles per frame — approximately 0.02% overhead at 50 Hz. This is categorically not worth skipping.
395
+
396
+ ---
397
+
398
+ ## bit_test_trick — BIT instruction as branchless 2-byte NOP
399
+
400
+ **Complexity:** medium
401
+ **Region:** both
402
+ **Uses kernal:** (none)
403
+
404
+ ### Why
405
+
406
+ The 6510's `BIT $abs` instruction (3 bytes, 4 cycles) tests bits in a memory value against the accumulator: bits 7 and 6 of the memory byte are copied into the N and V flags, and Z is set if `A AND M == 0`. Crucially, A itself is not modified. This makes `BIT` usable as a "swallow the next 2 bytes" instruction: if you embed a raw `$2C` byte ($2C is the opcode for `BIT abs`) in the code stream, the following 2 bytes are consumed as the operand and the instruction completes without side effects (beyond setting N, V, Z).
407
+
408
+ This allows two different entry points into a code sequence to produce two different values in A without a branch instruction.
409
+
410
+ ### How
411
+
412
+ The canonical pattern is a dual-entry loader:
413
+
414
+ ```asm
415
+ entry_a:
416
+ lda #$01 // 2 bytes: opcode $A9, operand $01
417
+ .byte $2C // 1 byte: BIT abs opcode — eats next 2 bytes
418
+ entry_b:
419
+ lda #$02 // 2 bytes: opcode $A9, operand $02
420
+ // When fallen through from entry_a: these 2 bytes
421
+ // are consumed as the "abs" operand of BIT, and
422
+ // execution continues at the instruction AFTER lda #$02
423
+ do_work:
424
+ sta result
425
+ ```
426
+
427
+ Execution from `entry_a`:
428
+ 1. `LDA #$01` — A = 1.
429
+ 2. `$2C` followed by the 2 bytes of `LDA #$02` are decoded as `BIT $02A9` — a read of address $02A9, setting N/V/Z from that read. A unchanged (still 1). 4 cycles.
430
+ 3. Execution continues at `sta result` with A = 1.
431
+
432
+ Execution from `entry_b`:
433
+ 1. `LDA #$02` — A = 2.
434
+ 2. Execution continues at `sta result` with A = 2.
435
+
436
+ Total cost for the "skip" path: 2 (LDA #$01) + 4 (BIT abs) = 6 cycles, versus 2 (LDA #$01) + 3 (JMP do_work) = 5 cycles for a branch. The BIT trick saves 2 bytes (no 3-byte JMP instruction needed; an earlier version of this page said 1 byte, counting JMP as one) at the cost of 1 extra cycle for the BIT. The real win is code density and eliminates a forward-reference label. (Against an always-taken 2-byte relative branch — `BNE` after `LDA #$01`, since a non-zero immediate clears Z — the saving is 1 byte at the same 5 cycles.)
437
+
438
+ The fence above is already the KickAssembler encoding (`.byte $2C` with `//` comments); an earlier version of this page repeated it with `!byte $2C` and a `;` comment under the same heading, which is ACME syntax and does not assemble in KickAssembler.
439
+
440
+ ### Why it works
441
+
442
+ `BIT $abs` has opcode $2C and takes 3 bytes (opcode + 2-byte address). It performs a read from the 16-bit address formed by bytes 2 and 3. The CPU executes the instruction in 4 cycles: (1) fetch opcode $2C, (2) fetch low byte of address, (3) fetch high byte of address, (4) read the target memory. Only N, V, and Z flags are modified. A is unchanged.
443
+
444
+ When the CPU fetches $2C from `entry_a+2`, it interprets $2C as the start of a BIT abs instruction and then consumes the next 2 bytes — which happen to be the 2 bytes of `LDA #$02` — as the 16-bit address operand. The read happens at address `$02 * 256 + $A9 = $02A9` (interpreting the little-endian value: low byte $A9 = opcode of `LDA imm`, high byte $02 = operand of `LDA #$02`). This read has no side effects. After the BIT completes, the PC is now pointing at `do_work`.
445
+
446
+ The technique is found throughout the KERNAL ROM, which uses it to share subroutine tails between two entry points that differ only in which initial value is loaded.
447
+
448
+ ### Variations
449
+
450
+ **Zero-page BIT as 1-byte NOP.** `BIT $zp` (opcode $24, 2 bytes, 3 cycles) consumes only 1 operand byte — it "skips" a 1-byte instruction. Use this to skip a 1-byte NOP, a 1-byte register-to-register transfer (TAX etc.), or a 1-byte stack operation (PHA/PLA). The same flag caveats apply.
451
+
452
+ **Immediate NOP for 2-cycle padding.** The undocumented `NOP #imm` opcodes ($80, $82, $89, $C2, $E2 — 2 bytes, 2 cycles each) skip 1 byte without touching flags. For cycle-padding scenarios where N/V/Z must not change, prefer these over `BIT zp`.
453
+
454
+ **Flag side effects.** The BIT instruction does modify N, V, and Z. Any code following the skip that reads these flags will see values from the `BIT` operand fetch, not from whatever the "skipped" instruction would have set. If the subsequent code branches on N/V/Z, this matters. In the dual-entry pattern, `do_work` must either reset the flags or not depend on them.
455
+
456
+ ### Cycle budget
457
+
458
+ Dual-entry with BIT trick vs explicit branch:
459
+
460
+ | Approach | Bytes | Cycles (path A) | Cycles (path B) |
461
+ |---|---|---|---|
462
+ | BIT skip trick | 5 total | 6 (LDA + BIT) | 2 (LDA only) |
463
+ | Explicit JMP label | 7 total (adds JMP) | 5 (LDA + JMP) | 2 (LDA only) |
464
+
465
+ The BIT trick saves 2 bytes at the cost of 1 extra cycle on the "path A" execution (2 + 1 + 2 = 5 bytes against 2 + 3 + 2 = 7; an earlier version of this table counted the JMP form as 6 bytes and the saving as 1). In code-size-constrained scenarios (fitting into a 255-byte page, keeping a sequence within branch reach) the byte saving is worth the extra cycle.
466
+
467
+ ---
468
+
469
+ ## phase_inverted_irq — Running tasks during VIC's bus-yield idle cycles
470
+
471
+ **Complexity:** scene-tier
472
+ **Region:** both
473
+ **Uses registers:** D012, D019, D01A
474
+ **Uses kernal:** (none)
475
+ **Demands:** midframe_raster_irqs
476
+
477
+ ### Why
478
+
479
+ VIC-II bus-stealing (also called "bad lines") occurs when the VIC needs to fetch character or bitmap data for the current raster line. During these fetches, the VIC asserts AEC (Address Enable Control) low for a fixed number of phi1 half-cycles, placing the address bus under VIC control and preventing the CPU from completing bus cycles. The CPU is effectively halted for 40 cycles on each badline (every 8th displayed line in the character set window).
480
+
481
+ The standard response is to work around bad lines: minimize computation, precompute, and accept that badline rows cost 40 cycles of CPU time. The advanced response is phase-inverted IRQ scheduling: instead of firing IRQs at the start of each line (where they may or may not land on a badline), fire IRQs timed to land in the free portion of the cycle budget where VIC bus activity is light or absent. On non-badlines, the full 63 cycles are available to the CPU; on badlines, 20 cycles are guaranteed (23 if the CPU happens to be in write cycles when BA drops on cycle 12). By scheduling IRQs to avoid the 40-cycle steal window, code can maintain a more predictable per-IRQ cycle budget.
482
+
483
+ ### How
484
+
485
+ Badlines occur at raster lines where `(raster_y & 7) == (YSCROLL & 7)`. With default YSCROLL = 3, they fall every 8th visible line (rows 51, 59, 67, ... 243 on PAL). Within each badline, VIC steals cycles 15-54 and holds BA low from cycle 12 — leaving 20 free cycles (1-11 and 55-63) plus cycles 12-14 for write accesses only.
486
+
487
+ A phase-inverted IRQ fires on the non-badline immediately preceding the target badline. The IRQ handler executes in the full 63-cycle non-badline, busy-waits through the badline steal window, then performs cycle-exact writes in the post-steal free cycles:
488
+
489
+ ```asm
490
+ // phase-inverted IRQ handler, installed at $0314/$0315
491
+ irq_pre_badline:
492
+ lda #$19
493
+ sta $D019 // acknowledge
494
+ !:
495
+ lda $D012
496
+ cmp #TARGET_LINE + 1 // spin until the badline itself is done
497
+ bne !-
498
+ lda new_color
499
+ sta $D020 // write lands in post-steal free cycles
500
+ lda #NEXT_LINE
501
+ sta $D012
502
+ jmp $EA81 // PLA/TAY/PLA/TAX/PLA/RTI: entered through $0314, so the dispatcher pushed A, X, Y
503
+ ```
504
+
505
+ The handler is installed at $0314/$0315 with the KERNAL in, which is why it exits through $EA81 (see `stable_raster_irq` in `docs/techniques/raster.md`, interrupt vector placement). An earlier version of this fence used an ACME local label (`.wait:`), `;` comments, and exited with `jmp ($0314)` — which from a handler installed at $0314 is an infinite loop; it did not assemble in KickAssembler.
506
+
507
+ ### Why it works
508
+
509
+ VIC's AEC signal halts the CPU for 40 cycles during each badline. The steal window is fixed on all PAL and NTSC variants. By firing IRQs in the pre-steal or post-steal free windows, handlers have a known stable cycle budget. IRQ jitter (see `stable_raster_irq` in `docs/techniques/raster.md`) is absorbed by the polling loop; the 11-cycle pre-steal window (cycles 1-11; stores may also land on 12-14) is wide enough to contain worst-case jitter (an earlier version said 15 cycles).
510
+
511
+ ### Variations
512
+
513
+ **Sprite fetch avoidance.** Active sprites steal additional cycles per line: 2 bus cycles of s-accesses per enabled sprite, plus a 3-cycle BA lead-in (write-only for the CPU) paid once per contiguous group of active sprite slots — up to 3 + 8 × 2 = 19 cycles per line with all eight on (measured in VICE x64sc: 105 / 399 / 210 cycles over the 21 DMA lines for one sprite / eight sprites / sprites 0+7, the last forming two BA groups; badline + eight sprites measured 40 + 19 = 59 stolen, 4 left). An earlier version of this page counted 4 cycles per sprite in two 2-cycle windows, which double-counts the single 2-cycle s-access window per sprite. Disable sprites on critical lines or account for their steal in the cycle budget.
514
+
515
+ **Blanking the display.** DEN ($D011 bit 4) is sampled once per frame, on raster line $30 (48): hold it clear across line $30 and that frame has no badlines at all, so every line gives the CPU 63 cycles (measured in VICE x64sc: a 14-cycle poll loop over lines 100-199 ran 450 iterations with DEN clear across $30 against 412 with DEN set). Clearing DEN later in the frame does not remove the remaining badlines of that frame — the same loop with DEN cleared at line 100 still ran 412 — so this is a per-frame choice for loaders and compute phases, not a per-line one; an earlier version of this page implied it worked mid-frame. Keep DEN clear across line 51 too if you want the border colour over the whole screen; clear on $30 but set again before 51 gives a badline-free frame whose window still opens on idle-state graphics (see `docs/hardware/vic-ii-reference.md`, $D011).
516
+
517
+ ### Cycle budget
518
+
519
+ PAL per-raster-line budget:
520
+
521
+ | Line type | Total cycles | VIC-stolen | CPU-available |
522
+ |---|---|---|---|
523
+ | Non-badline (no sprites) | 63 | 0 | 63 |
524
+ | Badline (no sprites) | 63 | 40 (bus 15–54; BA low from 12, so 12–14 are write-only) | 20 (+3 write-only) |
525
+ | Non-badline (8 sprites active) | 63 | up to 19 (3 BA lead-in + 8 × 2) | ~44 |
526
+ | Badline (8 sprites active) | 63 | 40 + 19 | 4 (measured in VICE x64sc) |
527
+
528
+ Sprite rows are the all-eight figure on lines where the sprites are displayed; an earlier version of this table had 32 stolen per line for eight sprites (~31 / ~11-15 left), from the 4-cycles-per-sprite count corrected above.
529
+
530
+ A full-screen effect that runs IRQs on every visible line (200 lines) at a badline rate of 1 in 8 has: 175 non-badlines * 63 + 25 badlines * 20 = 11025 + 500 = 11525 CPU cycles available per frame for the effect work, before overhead. This is approximately 59% of the total frame cycles (11525 / 19656). An earlier version of this table and sum used 23 per badline, which counted the three BA-low cycles 12–14 as free; they are usable only by write cycles, as the prose above says.
531
+
532
+ ### Recipes
533
+
534
+ - No recipe yet. (An earlier version of this page pointed at `recipes/kickassembler/cracktro-template.md`; that recipe has a text logo, not a sprite, and its bars avoid badlines by choosing `BAR_START = 88` from an IRQ ring — not phase-inverted scheduling.)
535
+
536
+ ---
537
+
538
+ ## dma_steal_avoidance — Sequencing code to avoid VIC bus-steal
539
+
540
+ **Complexity:** scene-tier
541
+ **Region:** both
542
+ **Uses registers:** D015, D011, D012
543
+ **Uses kernal:** (none)
544
+
545
+ ### Why
546
+
547
+ Every visual element active on a given raster line costs CPU cycles through VIC bus-stealing. Budgeting these steals is essential when an effect must write multiple VIC registers per line within a narrow cycle window. The steal cost is not random — it is fully deterministic given the display configuration. A coder who knows exactly which sprites are enabled on which lines, whether the line is a badline, and whether the border is open can compute the exact cycle count available per line.
548
+
549
+ The practical problem: many C64 effects need to run cycle-exact code (stable raster writes, multiplexer updates, scroll register tweaks) and simultaneously display sprites and bitmap graphics. The VIC steals cycles for each, and their steal windows overlap in complex ways. Avoidance means structuring the code so that operations requiring contiguous cycle blocks are scheduled on lines where VIC activity leaves those blocks clear.
550
+
551
+ ### How
552
+
553
+ The VIC-II bus-steal schedule for a fully-enabled PAL display (based on Christian Bauer's VIC-II article, the canonical reference):
554
+
555
+ **Badline steal:** Cycles 15-54 (40 cycles), active every 8th displayed line where `(raster_y & 7) == (YSCROLL & 7)`.
556
+
557
+ **Sprite DMA steal:** Each enabled sprite steals 2 bus cycles per line (its s-accesses; the p-access is a phi1 access and costs the CPU nothing), plus a 3-cycle BA lead-in — write-only for the CPU — paid once per contiguous group of active sprite slots, so all eight together cost up to 3 + 8 × 2 = 19 cycles per line (measured in VICE x64sc: 105 / 399 / 210 cycles over the 21 DMA lines for one sprite / eight sprites / sprites 0+7 as two BA groups). An earlier version of this page said approximately 4 cycles per sprite in two 2-cycle windows, which double-counts the single 2-cycle window. The exact per-sprite slots are in `docs/hardware/vic-ii-reference.md`.
558
+
559
+ **Avoidance strategies:**
560
+
561
+ 1. **Disable sprites on critical lines.** Write 0 to the relevant bits of $D015 on lines that need contiguous CPU cycle blocks. Re-enable on the following line.
562
+
563
+ 2. **Blank the display on heavy-compute frames.** DEN ($D011 bit 4) held clear across raster line $30 removes every badline of that frame: full 63 cycles/line for the CPU. DEN is sampled only on line $30, so clearing it on a particular line does not free the badlines that follow in the same frame; use it for whole-frame compute or load phases, and use strategies 1, 3 and 4 for per-line scheduling. (An earlier version of this item was titled "on heavy-compute lines" and implied a mid-frame clear worked.)
564
+
565
+ 3. **Sequence writes to non-stolen cycles.** Target IRQ handlers on non-badlines, schedule critical writes to cycles 1-11 or 55-63 (outside the steal window; 12-14 admit writes only), cycles numbered 1-63 as in `docs/hardware/vic-ii-reference.md` (an earlier version wrote 0-14 / 55-62). Requires stable-raster IRQ (see `docs/techniques/raster.md`).
566
+
567
+ 4. **Put a write in the BA tail, not a read.** When BA drops (cycle 12 on a badline; three cycles before the first sprite's DMA) the CPU keeps running until its next read cycle and stops there; write cycles are never halted. So a store whose fetches are done by cycle 11 and whose write cycle lands on cycle 12 completes before the halt, and the halt then costs 41-42 cycles instead of 43 — one write cycle for STA/STX/STY, two for a read-modify-write (INC/DEC/ASL/LSR/ROL/ROR abs), three only for the interrupt push sequence. That is where the "23 versus 20 cycles" figure above comes from. Nothing is deferred and no write is free: an instruction whose read cycle meets BA low simply stretches by the whole steal (measured in VICE x64sc: an STA stream spanning a PAL badline loses 42 or 43 cycles, a NOP stream 43, an INC stream 41-43). An earlier version of this item said a store issued before a steal was "deferred until AEC goes high" at no cost and called it "lazy writes"; neither the mechanism nor the term has a source in this repo or in Bauer's article.
568
+
569
+ ### Why it works
570
+
571
+ The 6510 and VIC-II share the address and data buses via the phi1/phi2 clock. During a DMA steal, VIC asserts AEC low, extending its bus control past the CPU's phi2 slot. The CPU withholds its bus transaction and retries on the next phi2. From software, the CPU's instruction timing stretches: an instruction that would take N cycles takes N + steal_cycles when a steal overlaps it. The total cycle count per line is always exactly 63 (PAL) — the CPU simply executes fewer instructions in those 63 cycles.
572
+
573
+ The authoritative per-cycle schedule is in Christian Bauer's "The MOS 6567/6569 video controller (VIC-II) and its application in the Commodore 64" (Project 64, 1996; widely reproduced on codebase64.org). That document contains the exact cycle-per-line table for PAL and NTSC, the sprite DMA scheduling formula, and the AEC signal diagram.
574
+
575
+ ### Variations
576
+
577
+ **Open borders + steal avoidance.** Side border opening needs one `$D016` write per covered line whose store cycle is cycle 56 (PAL) — CSEL taken from 1 to 0 between the X=335 and X=344 border comparisons — and CSEL set back to 1 before cycle 55 of the next line; `$D011` (RSEL) governs the top/bottom border and is not involved. It is a one-cycle target, not a window, so the CPU must be cycle-exact on every line covered; sprite DMA on those lines shifts the CPU's position, which is why the recipe (`recipes/kickassembler/sideborder-open.md`) keeps the sprite set identical on every line of the region, or you disable sprites there. (An earlier version of this page said `$D016` and `$D011` writes within a 23-cycle window.)
578
+
579
+ **Sprite crunch.** Enable sprites only on the lines where they are displayed. A 24-sprite multiplexer that activates each sprite for exactly the lines it occupies has far lower steal overhead than one that leaves all 8 hardware sprites enabled across all 200 visible lines.
580
+
581
+ ### Cycle budget
582
+
583
+ Total DMA steal per frame on a fully-featured PAL display (all borders open, 8 sprites active on all visible lines, character mode):
584
+
585
+ | Source | Steal cycles |
586
+ |---|---|
587
+ | Badlines (25 lines * 40 cycles) | 1000 |
588
+ | Sprite DMA (3 BA lead-in + 8 sprites * 2 cycles = 19 * 200 lines, upper bound) | 3800 |
589
+ | Total steal | ~4800 |
590
+ | Available CPU cycles per frame (63 * 312 = 19656 - 4800) | ~14856 |
591
+ | Available as % of frame | ~76% |
592
+
593
+ The sprite row is an upper bound: sprite DMA occurs only on lines where a sprite is displayed, and the 3-cycle lead-in is per contiguous group of active slots, so a sparse enable pattern can cost slightly more per sprite than the all-eight figure. An earlier version of this table counted 4 cycles per sprite in two 2-cycle windows (32 per line, 6400 per frame, ~62% available), which double-counts the single 2-cycle s-access window per sprite; the 19-per-line figure is measured in VICE x64sc (399 cycles over the 21 DMA lines of eight sprites).
594
+
595
+ A demo that disables sprites on 100 of the 200 visible lines recovers about 1900 steal cycles (an earlier version said 3200) — roughly a 13% improvement in usable CPU time. Combined with display blanking on heavy-compute segments, most C64 demo effects stay within budget by applying avoidance selectively on the lines where tight register writes are needed.
596
+
597
+ ### Recipes
598
+
599
+ - `recipes/kickassembler/fli-image.md` (forces a badline on every line and lays its $D018/$D011 writes out around the 40-cycle steal; the opposite of avoidance, useful as the worked cost example)
600
+ - An earlier version of this list also named `recipes/kickassembler/sprite-multiplex-24.md` for per-line sprite enable/disable; that recipe writes `sta $d015` with all eight slots on for the whole frame.
601
+
602
+ ## table_generation — Lookup-table generation at build time or at start-up
603
+
604
+ **Complexity:** low
605
+ **Region:** both
606
+ **Uses registers:** (none)
607
+ **Uses kernal:** (none)
608
+
609
+ ### Why
610
+
611
+ A sine, a reciprocal or a product costs hundreds to thousands of cycles to compute on a 6510 and 4 cycles to read from a table. Almost every effect on this machine reads a table instead of computing. The question is where the table comes from: the assembler, the compiler, a generator that runs at start-up, or a file built on the host and embedded. Each has a different cost in bytes, in start-up time and in exactness, and the choice is rarely written down. The figures below were measured on this machine: KickAssembler 5.25, Oscar64 (build 2026-05-19), VICE x64sc 3.10.
612
+
613
+ ### How
614
+
615
+ **KickAssembler builds the table at assembly time.** `.fill N, expr` evaluates `expr` once per entry with `i` running 0 to N-1 and emits the low byte. The scalings that matter:
616
+
617
+ ```asm
618
+ // Sine tables at assembly time. Each expression is evaluated by the
619
+ // assembler; nothing runs on the C64.
620
+ .const AMP = 40
621
+ .const MID = 100
622
+
623
+ * = $2000
624
+ // Unsigned, 0..255 centred on 128, floor form: 128 + 127.5 * sin.
625
+ // Range 0..255, entry 64 is 255, entry 192 is 0.
626
+ sin_u8: .fill 256, floor(128 + 127.5 * sin(toRadians(i * 360 / 256)))
627
+ // Unsigned, rounded, amplitude 127 about 128. Range 1..255, symmetric:
628
+ // entry 128+k is 256 minus entry k. Use this one when a quarter wave
629
+ // or an integer unfold must reproduce it exactly.
630
+ sin_r127: .fill 256, 128 + round(127 * sin(toRadians(i * 360 / 256)))
631
+ // Signed two's complement, -127..127. Read with a signed add.
632
+ sin_s8: .fill 256, round(127 * sin(toRadians(i * 360 / 256)))
633
+ // Arbitrary amplitude and offset: MID plus or minus AMP.
634
+ sin_a40: .fill 256, round(MID + AMP * sin(toRadians(i * 360 / 256)))
635
+ // Quarter wave, 65 entries covering 0 to 90 degrees inclusive.
636
+ sin_q: .fill 65, round(127 * sin(toRadians(i * 90 / 64)))
637
+ // 16-bit table split into a low page and a high page. The 65-byte
638
+ // sin_q in front of it is odd-sized, so realign or both halves
639
+ // straddle a page and every read with X >= $BF costs 5 cycles.
640
+ .align $100
641
+ sin_w: .lohifill 256, round(1000 + 1000 * sin(toRadians(i * 360 / 256)))
642
+
643
+ // Reading them: X is the angle, 0..255 is one turn.
644
+ ldx #$40
645
+ lda sin_u8,x // 255
646
+ lda sin_w.lo,x // low byte of 2000
647
+ lda sin_w.hi,x // high byte of 2000
648
+ rts
649
+ ```
650
+
651
+ The 256-entry form is the norm because the angle is then a byte that wraps by itself. The quarter-wave form saves 191 bytes and costs an unfold at start-up or a branch on every read (mirror the index for 90 to 180 degrees, negate the value for 180 to 360). `sine-scroller.md` and `cracktro-template.md` use the round form with an `AMPLITUDE` constant and no offset; `kickassembler-reference.md` documents `.fill` with the `127.5 + 127.5 * sin` form.
652
+
653
+ Rounding matters. `round()` behaved as `floor(x + 0.5)` in every table below, including the negative half of `sin_s8`; no entry fell on an exact half, so the tie rule is not established here. `floor()` on `128 + 127.5 * sin` gives the same bytes as a C `(char)` cast of the same expression, which truncates towards zero and is floor for a non-negative value. `.fill` wraps a value outside 0..255 without an error: `.fill 4, 254 + i` assembled to `$FE $FF $00 $01` (measured, KickAssembler 5.25). The `127.5 + 127.5 * sin` round form does not reach 256, because `sin(90 degrees)` evaluates to exactly 1.0 and the peak is 255 (measured; an earlier draft of this section assumed it wrapped).
654
+
655
+ **Oscar64 folds a table at compile time when the initialiser is constant.** `math.h` declares `sin` and `cos` as intrinsics (`#pragma intrinsic(sin)`, `math.h` line 33), and the preprocessor `#for(i,N) text` directive (Oscar64 manual, "Pre-Processor control") repeats a one-line template N times with `i` substituted. Together they put the table in the data segment with no floating point in the binary:
656
+
657
+ ```c
658
+ #include <math.h>
659
+
660
+ // Unsigned, 0..255 centred on 128. (char) truncates towards zero; the
661
+ // value is never negative, so this is the floor form.
662
+ __export const char sin_u8[256] = {
663
+ #for(i,256) (char)(128 + 127.5 * sin(i * PI / 128)),
664
+ };
665
+ #pragma align(sin_u8, 256)
666
+
667
+ // Rounded, amplitude 127 about 128: add 0.5 before the cast.
668
+ __export const char sin_r127[256] = {
669
+ #for(i,256) (char)(128 + 127 * sin(i * PI / 128) + 0.5),
670
+ };
671
+
672
+ // Signed: round half away from zero on both sides.
673
+ __export const signed char sin_s8[256] = {
674
+ #for(i,256) (signed char)(127 * sin(i * PI / 128) + (i < 128 ? 0.5 : -0.5)),
675
+ };
676
+ ```
677
+
678
+ Compiled with `oscar64 -tm=c64 -O2 -n` and an empty `main`, this gives a 1,025-byte PRG; the map file places `sin_u8` at `$0900` in `DATA:data` with the other two on the pages after it, and no `sin`, `crt_fmul` or `crt_fdiv` symbol is linked (measured). Two details cost a rebuild each. Without `__export`, a table nothing references is dropped by the linker and the PRG is 137 bytes. `#pragma align(name, 256)` page-aligns a table but must come after the declaration; before it the compiler reports `error 3005: Variable not found`. `PI` is `3.141592653` in `math.h`; a Python reference has to use that constant, not `math.pi`, although for these three tables both gave identical bytes. Not built by the listings gate: these fragments were compiled in scratch and their bytes diffed as described under Verification.
679
+
680
+ **What Oscar64 does not fold.** A start-up loop that calls `sin` is compiled as written. `samples/rasterirq/movingbars.c` and `samples/sprites/multiplexer.c` in the Oscar64 tree do this, and so does `sprite-multiplex-8.md` in this repo. The cost is the floating-point library plus the time: the loop below links `sin` (396 bytes), `crt_fmul`, `crt_fdiv` and `sint16_to_float`, and takes 3,739,920 cycles for 256 entries, about 14,600 per entry or 190 PAL frames, measured with a CIA1 timer A harness in VICE x64sc (harness below, base overhead of 54 cycles subtracted).
681
+
682
+ ```c
683
+ void gen_float(char *t)
684
+ {
685
+ for (int i = 0; i < 256; i++)
686
+ t[i] = (char)(128 + 127.5 * sin(i * PI / 128));
687
+ }
688
+ ```
689
+
690
+ **An integer start-up generator.** When the table has to live in RAM and 256 bytes of data is too much, unfold a quarter wave with integer arithmetic. It reproduces `sin_r127` exactly, because that scaling is symmetric by construction, and it links no floating point:
691
+
692
+ ```c
693
+ // 65 entries, round(127 * sin(i * PI / 128)) for i = 0..64,
694
+ // generated by Python and embedded (or written as a #for initialiser).
695
+ static const char quarter[65] = {
696
+ #embed "quarter.bin"
697
+ };
698
+
699
+ void gen_int(char *t)
700
+ {
701
+ for (char i = 0; i <= 64; i++)
702
+ {
703
+ char a = quarter[i];
704
+ t[i] = 128 + a;
705
+ t[(char)(128 - i)] = 128 + a;
706
+ t[(char)(128 + i)] = 128 - a;
707
+ t[(char)(256 - i)] = 128 - a;
708
+ }
709
+ }
710
+ ```
711
+
712
+ Measured cost: 5,190 cycles, about a quarter of a PAL frame, and a 65-byte table in place of 256. The `(char)` casts on the indices keep `256 - 0` at 0 rather than at 256.
713
+
714
+ **Embedding a host-built table.** `#embed "file"` (Oscar64 manual, "Embedding binary data") pastes a binary file into an initialiser; `#embed LIMIT SKIP "file"` takes a slice. A table Python wrote is then data, with no start-up cost and no dependence on the compiler's arithmetic. This is the pragmatic route for anything `#for` cannot express on one line, such as a curve that needs a loop to generate, and for tables shared with a KickAssembler build.
715
+
716
+ **Reciprocal table for the perspective divide.** `effects-vector-3d.md` projects with `screen_x = cx + (tx * FOCAL) / (tz + DEPTH_OFFSET)`. A division per point per frame is out of budget; a table indexed by depth turns it into a multiply, which `table_multiply_8x8` in `maths.md` does in 52 cycles. Store `FOCAL * 256 / (z + DEPTH)` rounded, and the projection becomes `(tx * recip[z]) >> 8`:
717
+
718
+ ```asm
719
+ // Reciprocal table: recip[z] = round(FOCAL * 256 / (z + DEPTH)).
720
+ // DEPTH >= 1 keeps z = 0 off a divide by zero; FOCAL * 256 / DEPTH
721
+ // must be at most 255 or the entry wraps (128 * 256 / 160 = 204.8).
722
+ .const FOCAL = 128
723
+ .const DEPTH = 160
724
+
725
+ * = $2000
726
+ recip: .fill 256, round(FOCAL * 256 / (i + DEPTH))
727
+
728
+ ldx #0
729
+ lda recip,x // 205 at z = 0, 79 at z = 255
730
+ rts
731
+ ```
732
+
733
+ The `DEPTH = 160` here is chosen so that the 8-bit form holds, and it is not the value `effects-vector-3d.md` suggests. With the `DEPTH_OFFSET` of 4 to 8 that page quotes, an 8-bit reciprocal does not fit: `128 * 256 / 4` is 8,192 and the whole near range wraps. Either build the table 16-bit (`.lohifill 256, round(FOCAL * 256 / (i + DEPTH))`, read as low and high pages) or clamp the near range so the largest entry is at most 255.
734
+
735
+ An 8-bit reciprocal carries a rounding error of at most half a unit in 79 at the far end, about 0.6 % of the projected coordinate (arithmetic from the table's own values, not measured on screen). The multiply table itself is covered by `table_multiply_8x8` in `maths.md` and is not repeated here.
736
+
737
+ ### Why it works
738
+
739
+ The assembler and the compiler evaluate the expression on the host with double precision and emit bytes; the 6510 never sees a float. That is why the two toolchains agree byte for byte with each other and with Python on every scaling above: all three round the same real number the same way, and the only decisions left are the cast (truncate or round) and the scaling. Oscar64's intrinsic declaration is what lets its constant folder treat `sin(constant)` as a constant; a call with a loop variable is not constant and is compiled against its 32-bit float library, which is where the 14,600 cycles per entry go.
740
+
741
+ The integer unfold works because a rounded, zero-offset sine is odd about 180 degrees and even about 90 degrees, so 65 values determine all 256. The floor form `128 + 127.5 * sin` is not symmetric once floored: entry 64 is 255, so the mirror rule `t[128 + k] = 256 - t[k]` would put 1 at entry 192, where the floored table has 0. That is why the unfold targets `sin_r127` and not `sin_u8`.
742
+
743
+ ### Variations
744
+
745
+ **Cosine from the same table.** `cos(a) = sin(a + 64)` for a 256-entry turn; index with `(x + 64) & 255`, which a byte index does for free. `effects-vector-3d.md` describes the 512-entry form that avoids even the add.
746
+
747
+ **Two amplitudes from one table.** Store amplitude 127 and shift right for 63, 31, 15. For a signed table the shift must be arithmetic (`CMP #$80 : ROR`), and each shift loses a bit of precision; for non-power-of-two amplitudes build a second table.
748
+
749
+ **Screen row tables.** `.fill 25, <(SCREEN + i * 40)` and the `>` twin, or `.lohifill 25, SCREEN + i * 40`, give the row address split into pages; Oscar64's `#for(i,25) Screen + 40 * i,` in a pointer array does the same (Oscar64 manual example).
750
+
751
+ ### Verification
752
+
753
+ Every table above was dumped from the built binary and compared with a table generated by stdlib Python; every comparison was zero differences.
754
+
755
+ KickAssembler: assemble with a symbol file, then slice the PRG at the label's address minus the load address.
756
+
757
+ ```
758
+ java -jar KickAss.jar tables.asm -o tables.prg -symbolfile
759
+ grep sin_u8 tables.sym # .label sin_u8=$2000
760
+ python3 -c "
761
+ import math,re
762
+ prg=open('tables.prg','rb').read(); load=prg[0]|prg[1]<<8
763
+ a=int(re.search(r'sin_u8=\\\$([0-9a-f]+)',open('tables.sym').read())[1],16)
764
+ got=prg[2+a-load:2+a-load+256]
765
+ ref=bytes(math.floor(128+127.5*math.sin(math.radians(i*360/256))) for i in range(256))
766
+ print(sum(1 for i in range(256) if got[i]!=ref[i]))" # 0
767
+ ```
768
+
769
+ Repeated for `sin_r127` (`128 + floor(127 * sin + 0.5)`), `sin_s8`, `sin_a40`, `sin_q` (65 entries), the `127.5 + 127.5` round form, the 512 bytes of `sin_w` and `recip`: 0, 0, 0, 0, 0, 0, 0 differences. `sin_q` is byte-identical to the 65-byte `quarter.bin` the Oscar64 generator embeds, and `sin_r127` is byte-identical to the 256-byte table that generator produces.
770
+
771
+ Oscar64: `-n` writes a `.map` beside the PRG with one line per symbol (`0900 (0100) : sin_u8, DATA:data`); slice the PRG at that address.
772
+
773
+ ```
774
+ oscar64 -tm=c64 -O2 -n -o=fold.prg fold.c
775
+ grep sin_u8 fold.map | head -1 # 0900 - 0a00 : sin_u8, DATA:data
776
+ python3 -c "
777
+ import math
778
+ prg=open('fold.prg','rb').read(); load=prg[0]|prg[1]<<8
779
+ got=prg[2+0x900-load:2+0x900-load+256]
780
+ ref=bytes(int(128+127.5*math.sin(i*3.141592653/128)) for i in range(256))
781
+ print(sum(1 for i in range(256) if got[i]!=ref[i]))" # 0
782
+ ```
783
+
784
+ `sin_u8` from Oscar64 is byte-identical to `sin_u8` from KickAssembler, and the Oscar64 `sin_r127` and `sin_s8` folds matched Python with 0 differences each.
785
+
786
+ The runtime generators were checked on the C64 itself: a self-checking program folds each 256-byte table into a 16-bit checksum (`chk = ((chk ^ byte) * 5 + 1) & 0xffff`) and prints PASS against the value Python computed. The float loop, the integer unfold and the `#embed` copy all printed PASS in VICE x64sc (`$6A73` for the floor form, `$A15E` for `sin_r127`, twice). The timer harness in the same program: stop CIA1, load timer A and timer B with `$FFFF`, start B counting A underflows (`CRB = $51`) and A counting phi2 (`CRA = $11`), call the generator, stop A, and read `(0xFFFF - TB) << 16 | (0xFFFF - TA)`; an empty call measured 54 cycles and was subtracted. Interrupts were off for the measurement, since the KERNAL's jiffy IRQ runs on the same timer A.
787
+
788
+ ### Cycle budget
789
+
790
+ | Method | Bytes in PRG | Start-up cycles (PAL frames) | Exact to Python |
791
+ |---|---|---|---|
792
+ | KickAssembler `.fill`, 256 entries | 256 | 0 | yes, 0 diffs |
793
+ | Oscar64 `#for` + `sin` in a const initialiser | 256 | 0 | yes, 0 diffs |
794
+ | Oscar64 `#embed` of a host-built file | 256 | 0 | by construction |
795
+ | Oscar64 integer quarter-wave unfold | 65 + code | 5,190 (0.26) | yes, checksum PASS |
796
+ | Oscar64 runtime `sin` loop | 256 bss + float library | 3,739,920 (190) | yes, checksum PASS |
797
+
798
+ Cycle figures are measured in VICE x64sc with the CIA harness above; the PAL frame is 19,656 cycles (63 x 312). A read from any of these tables is `LDA abs,X` at 4 cycles, 5 across a page boundary, which is why the KickAssembler tables sit at `* = $2000` and the Oscar64 one carries `#pragma align`; without the pragma the linker put `sin_u8` at `$0888` (measured). Placing the block on a page boundary is not enough on its own: each 256-byte table that follows is aligned only because the one before it is exactly 256 bytes. The 16-bit `sin_w` needs its own `.align $100` because the odd-sized 65-byte `sin_q` precedes it. Without the align it assembled to `$2441`, so its low half straddled `$2441` to `$2540` and its high half `$2541` to `$2640`, and every read with X at or above `$BF` crossed a page on both halves; with it, `sin_w` sits at `$2500` and its halves at `$2500` and `$2600` (measured from the symbol file of the page fragment, 0 diffs against Python at the new address). The align pads the gap with 191 bytes.
799
+
800
+ ### Recipes
801
+
802
+ - `recipes/kickassembler/sine-scroller.md` (assembly-time `.fill` with `round()` and an `AMPLITUDE` constant)
803
+ - `recipes/kickassembler/cracktro-template.md` (the same form, plus `row_lo` / `row_hi` screen tables)
804
+ - `recipes/oscar64/sprite-multiplex-8.md` (fills `sinx` / `siny` at start-up with the float `sin` loop; the compile-time or integer forms above are the cheaper replacement)
805
+ - No recipe yet for the reciprocal table; `techniques/effects-vector-3d.md` describes the projection it serves.
806
+
807
+ ## sine_table_generation — Building a sine table on the machine, with no table from the assembler
808
+
809
+ **Complexity:** medium
810
+ **Region:** both
811
+ **Uses registers:** (none)
812
+ **Uses kernal:** (none)
813
+ **Requires:** table_generation
814
+ **Cost:** bytes_code=221, bytes_data=256, zp_bytes=12
815
+ **Cost basis:** derived-listing
816
+ **Cost measured on:** kickassembler-sine-table-runtime (one-off at start, screen blanked)
817
+
818
+ ### Why
819
+
820
+ `table_generation` above gives a sine table three ways, and all three
821
+ come from the host: the assembler's `.fill`, the compiler's constant
822
+ folder, or a file. A program that must make its table in RAM has two
823
+ routes on that page, a float loop at 3,739,920 cycles or an integer
824
+ unfold that still embeds 65 host bytes. This entry is the third route:
825
+ compute the values on the 6510 with integer arithmetic, in a few frames,
826
+ close enough to the host table that a scroller or a sprite chain cannot
827
+ tell. It presupposes `table_generation`, whose scaling, symmetry and
828
+ verification it reuses; the Cost line is the quarter-wave route in the
829
+ recipe (its code from the symbol file, the 256-byte table it writes, and
830
+ zero page $02 to $0C and $11; the parabola's own two words at $0D to $10
831
+ are not counted), with no per-frame figure because the build runs
832
+ once. All cycle and error figures below were measured in VICE x64sc 3.10
833
+ by `recipes/kickassembler/sine-table-runtime.md`, which times each
834
+ generator with the CIA2 cascade and checks every entry against the
835
+ assembler's own `128 + round(127 * sin)`.
836
+
837
+ ### How
838
+
839
+ **Quarter-wave symmetry, first.** A rounded sine of amplitude 127 about
840
+ 128 is even about entry 64 and odd about entry 128, so 65 computed
841
+ values fix all 256: entry `128 - i` is entry `i`, and entries `128 + i`
842
+ and `256 - i` are `256 - entry i`. Whatever method makes the quarter,
843
+ generate 65 values and write four entries per value. The floor form
844
+ `128 + 127.5 * sin` is not symmetric once floored (`table_generation`,
845
+ Why it works), so a generated table targets the rounded scaling.
846
+
847
+ **Second-difference recurrence.** A sampled sine obeys
848
+ `y[n+1] = 2 y[n] - y[n-1] - d y[n]` with `d = 2 - 2 cos(2 pi / 256)`,
849
+ about `0.000602`. Keep the value `y` and its first difference `v` in
850
+ fixed point, and each step is `y += v` then `v -= d * y`. `d` is too
851
+ small for an 8-bit multiplier, but it is a sum of powers of two:
852
+ `2^-11 + 2^-14 + 2^-15 + 2^-16 + 2^-17` is `0.00060272`, 0.06 % over the
853
+ true value (arithmetic), and each term is an arithmetic shift of the one
854
+ before. The amplitude and the phase are set by the two starting values
855
+ alone: `y[0] = 0` and `v[0] = A * sin(2 pi / 256)`, a single constant the
856
+ assembler or compiler folds (`round(127 * sin(toRadians(360 / 256)) *
857
+ 65536)` is 204,258 in 8.16); `A * 2 pi / 256` is within 0.1 % of it
858
+ (arithmetic) for a build that will not evaluate a sine at all.
859
+
860
+ ```asm
861
+ // One step of the recurrence in 8.16 fixed point. y and v are 24-bit
862
+ // signed in zero page; the byte for this entry is taken before the
863
+ // state advances. d * y is five arithmetic shifts, each of the last.
864
+ .const y = $02
865
+ .const v = $05
866
+ .const a = $08
867
+ .const t = $0a
868
+ .const o = $11
869
+
870
+ * = $2000
871
+ step:
872
+ lda y+1 // 128 + round(y): carry = bit 15
873
+ asl
874
+ lda y+2
875
+ adc #0
876
+ eor #$80
877
+ sta o
878
+ clc // y += v
879
+ lda y
880
+ adc v
881
+ sta y
882
+ lda y+1
883
+ adc v+1
884
+ sta y+1
885
+ lda y+2
886
+ adc v+2
887
+ sta y+2
888
+ lda y+1 // a = y >> 11: drop a byte, shift 3
889
+ sta a
890
+ lda y+2
891
+ sta a+1
892
+ jsr asr3
893
+ lda a // t = y >> 11
894
+ sta t
895
+ lda a+1
896
+ sta t+1
897
+ jsr asr3 // + y >> 14
898
+ jsr addt
899
+ jsr asr1 // + y >> 15
900
+ jsr addt
901
+ jsr asr1 // + y >> 16
902
+ jsr addt
903
+ jsr asr1 // + y >> 17
904
+ jsr addt
905
+ sec // v -= t, sign-extended
906
+ lda v
907
+ sbc t
908
+ sta v
909
+ lda v+1
910
+ sbc t+1
911
+ sta v+1
912
+ ldy #0
913
+ lda t+1
914
+ bpl !+
915
+ dey
916
+ !: sty t+1
917
+ lda v+2
918
+ sbc t+1
919
+ sta v+2
920
+ lda o
921
+ rts
922
+
923
+ asr3:
924
+ jsr asr1
925
+ jsr asr1
926
+ asr1: // 16-bit arithmetic shift right of a
927
+ lda a+1
928
+ cmp #$80
929
+ ror a+1
930
+ ror a
931
+ rts
932
+
933
+ addt: // t += a
934
+ clc
935
+ lda t
936
+ adc a
937
+ sta t
938
+ lda t+1
939
+ adc a+1
940
+ sta t+1
941
+ rts
942
+ ```
943
+
944
+ Run for 256 steps this gives a table within one unit of the host's
945
+ everywhere, off by one in 21 entries, in 129,190 cycles (505 a step, 6.6
946
+ PAL frames). Run for 65 steps and unfolded, it is off by one in 4
947
+ entries and costs 38,067 cycles, 1.9 frames.
948
+
949
+ **Parabola.** `p(i) = i * (128 - i)` for `i` 0 to 127 has a constant
950
+ second difference, so a half wave is two 16-bit additions per entry:
951
+ the first difference starts at 127 and falls by 2. Scale to amplitude
952
+ 127 with `p - p / 128`, add 16, shift right five times, and mirror the
953
+ half. It costs 25,266 cycles for 256 entries, 99 an entry, and it is a
954
+ parabola: 8 units from the sine at worst, near the zero crossings, and
955
+ 3.8 on average. Good enough for a bounce; visibly wrong under a scroller
956
+ that reads it every pixel.
957
+
958
+ **Amplitude and offset.** For a sprite Y or a scroll position the table
959
+ wants `MID + AMP * sin`, not `128 + 127 * sin`. Three ways, cheapest
960
+ first. Generate at the amplitude: the recurrence's `v[0]` is
961
+ `AMP * sin(2 pi / 256)`, so a different `AMP` is a different constant,
962
+ and the `eor #$80` that turns the signed byte into `128 + y` becomes
963
+ `clc : adc #MID` for any offset (the recipe's sprites take the third
964
+ way instead: read the 127 table, `lsr` for amplitude 63, `adc #100` for
965
+ the offset, 6 cycles a read). Shift a signed table right for 63, 31, 15
966
+ (`table_generation`, Variations). Or multiply on the way out with
967
+ `table_multiply_8x8` from `maths.md` for an amplitude that is not a
968
+ power of two. Whichever way, check the range: an offset plus amplitude
969
+ past 255 wraps silently in a byte table.
970
+
971
+ ### Why it works
972
+
973
+ The recurrence is exact for a sine when `d` is exact and the arithmetic
974
+ is; its two errors are the approximation of `d`, which is a frequency
975
+ error of 0.03 % and shows as a phase drift, and the floor in each
976
+ arithmetic shift, which under-corrects `v` by a fraction of its last bit
977
+ per step and grows into `y` as the square of the step count. Sixteen
978
+ fraction bits keep both under a unit for one turn (arithmetic bound;
979
+ measured, 21 entries off by one). The quarter-wave form is more accurate
980
+ than the full turn because the drift has 64 steps to grow in rather
981
+ than 256, and the unfold copies the good early values into the second
982
+ half instead of computing it last.
983
+
984
+ The drift is the reason not to run the recurrence as an oscillator
985
+ across frames: on the host, the same integer algorithm reaches a worst
986
+ error of 2 units in the fifth turn and 267 units summed over the eighth
987
+ (Python, not run on the machine). Generate once and stop, or reset the
988
+ state each turn.
989
+
990
+ ### Variations
991
+
992
+ **Bhaskara's rational approximation.** `16 x (pi - x) / (5 pi^2 - 4 x
993
+ (pi - x))` for `x` in 0 to pi is within one unit of a rounded 127-sine
994
+ over the whole half wave (host arithmetic, not run on the machine). It
995
+ needs a 16-bit divide per entry, which is why the recurrence is the
996
+ usual choice on a 6510; it earns its place where a table must be exact
997
+ from a cold start with no drift at all.
998
+
999
+ **Cosine and phase.** Start the recurrence at `y[0] = A`, `v[0] = A *
1000
+ (cos(2 pi / 256) - 1)` for a cosine, or index the sine table with
1001
+ `(x + 64) & 255`, which a byte index does for free.
1002
+
1003
+ **A second harmonic.** A second table at twice the frequency is the same
1004
+ recurrence with `d = 2 - 2 cos(4 pi / 256)`, about 0.002409, which is
1005
+ the shifts `2^-9 + 2^-12 + 2^-13 + 2^-14 + 2^-15` (arithmetic, not built
1006
+ here); adding the two tables entry by entry gives the breathing wave
1007
+ `scroll.md` describes under `sine_scroller`.
1008
+
1009
+ ### Cycle budget
1010
+
1011
+ The build runs once, so it has no per-frame cost. Measured for 256
1012
+ entries in VICE x64sc with the CIA2 cascade, screen blanked: parabola
1013
+ 25,266 cycles, quarter-wave recurrence 38,067, full-turn recurrence
1014
+ 129,190. With the screen on, every badline in the build's span adds its
1015
+ 40 to 43 stolen cycles; a 38,067-cycle build that starts at the top of
1016
+ the display crosses about two frames' worth of them. The float loop in
1017
+ `table_generation` is 3,739,920 for the same table, the host-table
1018
+ unfold 5,190.
1019
+
1020
+ ### Recipes
1021
+
1022
+ - `recipes/kickassembler/sine-table-runtime.md` (all three generators
1023
+ timed and checked on screen against the assembler's table, then eight
1024
+ sprites on the quarter-wave table)
1025
+
1026
+ ---
1027
+
1028
+ ## speedcode_generation — Generating unrolled code into RAM at run time
1029
+
1030
+ **Complexity:** medium
1031
+ **Region:** both
1032
+ **Uses registers:** (none)
1033
+ **Uses kernal:** (none)
1034
+ **Cost:** cycles_per_frame=8000, bytes_code=849, bytes_data=1185
1035
+ **Cost basis:** derived-listing
1036
+ **Cost measured on:** kickassembler-speedcode-generator (one 1,000-byte copy, screen blanked; bytes are the whole PRG)
1037
+
1038
+ The figures on the Cost line are for the recipe's job, a 1,000-byte copy:
1039
+ 8,000 cycles each time the generated code runs, measured, and the PRG's
1040
+ own segment read off the listing, 849 bytes of generator and harness code
1041
+ and 1,185 bytes of row tables, source block and text. The 6,001-byte
1042
+ block the generator fills is RAM, not load, so it is not on the line; it
1043
+ scales with the byte count, as the cycle figure does, and belongs in the
1044
+ memory plan. The generation itself runs once and is not on the line
1045
+ either; it is 92,485 cycles for that job, measured.
1046
+
1047
+ ### Why
1048
+
1049
+ `unrolled_loops` removes the loop overhead by having the assembler
1050
+ replicate the body, and pays for it in bytes of PRG: a fully unrolled
1051
+ 1,000-byte copy is 6,000 bytes on disk and in the load. `self_modifying_code`
1052
+ patches an operand inside code that already exists. Speedcode generation
1053
+ is the run-time form of the first, built with the mechanism of the
1054
+ second: a short generator writes the whole unrolled instruction stream
1055
+ into RAM from tables when the program starts, or when a part starts, so
1056
+ the load carries a 90-byte generator and four small tables instead of
1057
+ 6 KB of straight-line code. It is `table_generation` for code rather than
1058
+ data: the same trade of start-up cycles for bytes in the file, and the
1059
+ same choice of when to pay.
1060
+
1061
+ ### How
1062
+
1063
+ The generator knows the opcode bytes and writes them itself. `LDA abs` is
1064
+ `$AD lo hi`, `STA abs` is `$8D lo hi`, `LDA #imm` is `$A9 imm`, and the
1065
+ stream ends with `RTS`, `$60`. For a copy, each byte moved is one
1066
+ `LDA abs / STA abs` pair, six bytes of code; for a fill, `LDA #imm` once
1067
+ and then `STA abs` per byte, three bytes each. The source and destination
1068
+ addresses come from tables, typically one low and one high byte per row,
1069
+ and the generator increments a working copy of each address as it emits:
1070
+
1071
+ ```asm
1072
+ // Emit COLS pairs of LDA abs / STA abs for one row. out is a zero-page
1073
+ // pointer to the next free byte of the code block; cur_src and cur_dst
1074
+ // were loaded from the row tables. Y counts bytes of code, so one row of
1075
+ // 40 pairs is Y = 0..239, so the pointer advances by 240 per row.
1076
+ .const OP_LDA_ABS = $ad
1077
+ .const OP_STA_ABS = $8d
1078
+ .const COLS = 40
1079
+ .const out = $fb
1080
+ ldy #$00
1081
+ pair: lda #OP_LDA_ABS
1082
+ sta (out),y
1083
+ iny
1084
+ lda cur_src
1085
+ sta (out),y
1086
+ iny
1087
+ lda cur_src+1
1088
+ sta (out),y
1089
+ iny
1090
+ lda #OP_STA_ABS
1091
+ sta (out),y
1092
+ iny
1093
+ lda cur_dst
1094
+ sta (out),y
1095
+ iny
1096
+ lda cur_dst+1
1097
+ sta (out),y
1098
+ iny
1099
+ inc cur_src
1100
+ bne !+
1101
+ inc cur_src+1
1102
+ !: inc cur_dst
1103
+ bne !+
1104
+ inc cur_dst+1
1105
+ !: cpy #COLS * 6
1106
+ bne pair
1107
+ rts
1108
+ cur_src: .word $0c18
1109
+ cur_dst: .word $4000
1110
+ ```
1111
+
1112
+ The size is fixed before the generator runs: bytes per emitted unit times
1113
+ the count, plus one for the `RTS`. A 1,000-byte copy is 1,000 × 6 + 1 =
1114
+ 6,001 bytes; a 1,000-byte fill is 2 + 1,000 × 3 + 1 = 3,003. That number
1115
+ goes in the memory plan next to the charset and the screen, because
1116
+ nothing else will check it. In the recipe the block is at `$5000` and the
1117
+ `RTS` was read back at `$6770` over the monitor.
1118
+
1119
+ Generated code is called like any subroutine. The pairs read and write
1120
+ with absolute addressing, so there is no index register to set up and no
1121
+ page-crossing penalty on the reads: `LDA abs` is 4 cycles at any address,
1122
+ where `LDA abs,X` is 5 when the sum crosses a page.
1123
+
1124
+ ### Why it works
1125
+
1126
+ The 6510 fetches every instruction from memory with no cache, so bytes
1127
+ written by `STA (zp),Y` are the instruction stream the moment the CPU
1128
+ reaches them; this is the same property `self_modifying_code` rests on.
1129
+ The generated stream has no loop counter, no compare and no branch, so
1130
+ its cost is exactly the sum of its instructions. Measured with a CIA2
1131
+ timer in VICE x64sc 3.10 (recipe below, harness overhead subtracted):
1132
+
1133
+ | Job: copy 1,000 bytes | Cycles | Per byte |
1134
+ |---|---|---|
1135
+ | Generated `LDA abs / STA abs` stream | 8,000 | 8.0 |
1136
+ | Indexed loop, `LDA abs,X / STA abs,X`, four 250-byte strides | 10,787 | 10.8 |
1137
+ | Generating the stream, once | 92,485 | 92.5 per pair |
1138
+
1139
+ 8,000 is the arithmetic (4 + 4 per pair) and the loop count is too: 2 for
1140
+ the `LDX`, 250 iterations of 43 less the final untaken branch, plus 36
1141
+ cycles of page crossings on the reads, which move when the source block
1142
+ moves. The generated code is 1.35 times the speed of a loop that is
1143
+ already partly unrolled; against a naive `(zp),Y` loop the ratio is
1144
+ larger, not measured here. The generator costs about 11.6 runs of the
1145
+ code it generates, so it pays back after a dozen copies.
1146
+
1147
+ The measurement was taken with the screen blanked (DEN cleared and a
1148
+ frame waited out). A CIA timer counts phi2 whether or not the CPU has the
1149
+ bus, so 8,000 cycles of generated code running across the display take
1150
+ 8,000 plus 40 to 43 for every badline they cross, and a plan that
1151
+ schedules them by the measured figure is short by that much;
1152
+ `badline_cycle_loss` in `pitfalls/raster-and-badline.md`.
1153
+
1154
+ ### When to generate
1155
+
1156
+ **Once at init.** The common case: the code depends only on constants
1157
+ (a fixed source, a fixed screen) and is generated before the first frame.
1158
+ The 92,485 cycles are under five PAL frames and invisible behind a
1159
+ blanked screen or a loading picture.
1160
+
1161
+ **Per part.** A demo whose parts want different speedcode regenerates
1162
+ between them, into the same block. The generator is small enough to keep
1163
+ resident; the block is reused, so the memory plan holds one block, not
1164
+ one per part.
1165
+
1166
+ **Per frame.** When the code itself has to move, for example a copy whose
1167
+ source row shifts each frame for a scroll, or a plot whose addresses come
1168
+ from this frame's sine positions, the generator runs every frame and its
1169
+ cost joins the frame budget. At 92.5 cycles per emitted pair that is
1170
+ affordable for tens or hundreds of pairs, not thousands; beyond that,
1171
+ generate once and patch the operands that change (`self_modifying_code`),
1172
+ or keep the addresses in tables and index them.
1173
+
1174
+ ### Variations
1175
+
1176
+ **Generated with page-crossing avoided.** Straight-line `LDA abs / STA
1177
+ abs` has no page-crossing cost, so the recipe's stream needs nothing. A
1178
+ generator that emits indexed reads (`LDA abs,X`, `$BD`) or branches to
1179
+ tie sections together does, and then it can choose the bases and the
1180
+ section lengths so no read crosses a page, or align each section's start;
1181
+ `branch_page_cross_extra_cycle` in `pitfalls/cpu.md` is the branch case
1182
+ and does not arise in a stream with no branches.
1183
+
1184
+ **Generated into a bank the VIC does not see.** The recipe's block at
1185
+ `$5000` to `$6770` is in VIC bank 1, which a stock machine never
1186
+ displays, so 6 KB of code costs no screen, charset or sprite space.
1187
+ The ROM windows at `$A000` to `$BFFF` (inside bank 2) and `$E000` to
1188
+ `$FFFF` (inside bank 3) give another 8 KB each, but sit under BASIC and
1189
+ the KERNAL: the generator's writes land in the
1190
+ RAM and the CPU fetches the ROM until `$01` banks it out, and then the
1191
+ KERNAL's interrupt vectors and `CHROUT` are gone too;
1192
+ `ram_under_rom_traps` in `pitfalls/banking.md`.
1193
+
1194
+ **STA-only sequences from a value table.** For a fill, or a screen whose
1195
+ contents are known at generation time, emit `LDA #imm` (`$A9 v`) only
1196
+ when the value changes and `STA abs` for every address: a run of equal
1197
+ bytes costs 4 cycles a byte instead of 8 and 3 bytes instead of 6. The
1198
+ generator reads the values from a table beside the addresses and
1199
+ compares each with the last one loaded. The result is a routine that
1200
+ paints a fixed picture at 4 cycles a byte with no source buffer at all.
1201
+
1202
+ ### Cycle budget
1203
+
1204
+ Per byte moved, at any address: `LDA abs` 4 + `STA abs` 4 = 8 cycles,
1205
+ measured as 8,000 for 1,000. A full 1,000-byte screen copy is therefore
1206
+ 8,000 cycles, about 127 PAL raster lines, and longer than the PAL
1207
+ vertical blank of about 7,100 cycles between line 250 and the next
1208
+ frame's first badline (`recipes/kickassembler/sine-scroller.md`), so a
1209
+ per-frame full-screen speedcopy run from a raster interrupt at the
1210
+ bottom of the display finishes some 14 lines into the next frame's
1211
+ top rows, and with badlines inside those rows later still;
1212
+ `full_field_redraw_exceeds_vblank` in `pitfalls/text-mode-render.md`.
1213
+ Split the copy across two interrupts, copy fewer rows, or write the top
1214
+ rows first so the VIC fetches finished cells. Generation: 92.5 cycles
1215
+ per emitted pair with the generator above, which is about 68 cycles of
1216
+ six `STA (zp),Y` stores with their loads and increments plus the
1217
+ address and loop bookkeeping; 1,000 pairs are 4.7 PAL frames.
1218
+
1219
+ ### Recipes
1220
+
1221
+ - `recipes/kickassembler/speedcode-generator.md` (generates a 1,000-byte copy from row tables, verifies it and times it against the indexed loop; the three figures above)
1222
+
1223
+ ---
1224
+
1225
+ ## nmi_handler_and_restore_key — Installing an NMI handler, and what to do about RESTORE
1226
+
1227
+ **Complexity:** low
1228
+ **Region:** both
1229
+ **Uses registers:** DD04, DD05, DD0D, DD0E
1230
+ **Cost:** cycles_per_frame=20
1231
+ **Cost basis:** measured-vice
1232
+ **Cost measured on:** kickassembler-nmi-timer-tick (per press)
1233
+
1234
+ ### Why
1235
+
1236
+ Two things reach the 6510's `/NMI` pin: CIA2's interrupt output and the
1237
+ RESTORE key. `SEI` masks neither. A program that does nothing about the
1238
+ vector leaves the KERNAL's handler in charge, and that handler treats a
1239
+ RESTORE press with RUN/STOP held as a request to warm-start BASIC over
1240
+ whatever is running. A program that takes the vector and gets the
1241
+ acknowledge wrong either loses every later NMI or, in the other
1242
+ direction, throws away a timer event it wanted. This entry is the
1243
+ minimal correct handler for each case: RESTORE disarmed, a CIA2 timer
1244
+ tick taken on purpose, and the two together.
1245
+
1246
+ ### How
1247
+
1248
+ **The vector.** With the KERNAL ROM mapped in, an NMI goes through
1249
+ `$FFFA` to `$FE43`, which is `SEI` and then `JMP ($0318)`. Nothing is
1250
+ pushed there, so the RAM vector at `$0318`/`$0319` is the whole
1251
+ dispatch from its second instruction on, and a handler installed there
1252
+ gets control with A, X and Y untouched. With the KERNAL banked out
1253
+ (`$01` bit 1 clear) the CPU reads `$FFFA`/`$FFFB` from RAM and the
1254
+ handler's address goes there instead; `memory_layout_plan` in
1255
+ `techniques/memory-banking.md` covers the all-RAM layout.
1256
+
1257
+ **What the KERNAL's handler does.** The default `$0318` target is
1258
+ `$FE47`. It pushes A, X and Y, writes `$7F` to `$DD0D` and reads it
1259
+ back. If a CIA2 flag was set it runs the RS-232 code. If none was, it
1260
+ takes the NMI to be a RESTORE press: it checks for a cartridge at
1261
+ `$8000`, samples the keyboard for RUN/STOP, and if that key is down runs
1262
+ RESTOR, IOINIT and CINT and jumps through `$A002`, the BASIC warm start,
1263
+ which puts `$0314` back to `$EA31` and the VIC back to text mode at
1264
+ `$0400`. RESTORE alone returns through `$FEBC`, having spent 189 cycles
1265
+ at an arbitrary point in the frame. The bytes and the cycle count are
1266
+ in `pitfalls/kernal-and-io.md`, `restore_nmi_not_maskable`, and
1267
+ `hardware/cia-reference.md`, "NMI vector (CIA2 + RESTORE)".
1268
+
1269
+ **Disarming RESTORE.** Point `$0318` at an `RTI`. Nothing about the key
1270
+ passes through CIA2, so no value written to `$DD0D` can mask it; the
1271
+ vector is the only software answer.
1272
+
1273
+ ```asm
1274
+ // Disarm RESTORE: the smallest complete NMI handler.
1275
+ install:
1276
+ sei
1277
+ lda #<nmi_stub
1278
+ sta $0318
1279
+ lda #>nmi_stub
1280
+ sta $0319
1281
+ cli
1282
+ rts
1283
+
1284
+ nmi_stub:
1285
+ rti
1286
+ ```
1287
+
1288
+ **Acknowledging a CIA2 NMI.** The CIA drops its interrupt output only
1289
+ when its interrupt control register is read. A handler for a CIA2 source
1290
+ reads `$DD0D` before `RTI`; `BIT $DD0D` does it without disturbing A, and
1291
+ `RTI` restores the flags `BIT` changed.
1292
+
1293
+ ```asm
1294
+ // Count a CIA2 timer tick and acknowledge it.
1295
+ nmi_tick:
1296
+ inc tick_count
1297
+ bit $dd0d // clears the flag; /NMI returns high
1298
+ rti
1299
+
1300
+ tick_count:
1301
+ .byte $00
1302
+ ```
1303
+
1304
+ **The NMI lock.** A handler that never reads `$DD0D` leaves the flag
1305
+ standing, `/NMI` stays low, and because the 6510 takes an NMI on the
1306
+ falling edge only, no further NMI is taken from any source, RESTORE
1307
+ included, until something reads `$DD0D`. That is a second way to kill
1308
+ RESTORE, at the price of every CIA2 interrupt, and it is also the
1309
+ commonest way to break an NMI-driven player by accident.
1310
+
1311
+ **A timer tick on purpose.** Latch CIA2 Timer A with the period less
1312
+ one, set bit 0 of `$DD0D` with bit 7 (`$81`) so the underflow drives
1313
+ `/NMI`, and start the timer in continuous mode (`$11` to `$DD0E`). The
1314
+ tick then arrives every latch + 1 cycles regardless of the raster, which
1315
+ is what a sample player or a music driver that must not depend on a
1316
+ raster interrupt needs. A RESTORE press then reaches the same handler
1317
+ as one extra, early entry; a handler that must not act on it tests bit 7
1318
+ of `$DD0D` first, since a press sets no CIA2 flag.
1319
+
1320
+ ```asm
1321
+ // A tick every 10,000 cycles through nmi_tick above.
1322
+ arm_tick:
1323
+ lda #<9999
1324
+ sta $dd04
1325
+ lda #>9999
1326
+ sta $dd05
1327
+ lda #$81 // set: Timer A underflow -> /NMI
1328
+ sta $dd0d
1329
+ lda #$11 // start, force load, continuous
1330
+ sta $dd0e
1331
+ rts
1332
+ ```
1333
+
1334
+ ### Why it works
1335
+
1336
+ The 6510's NMI input is edge-sensitive: the interrupt sequence starts on
1337
+ the high-to-low transition of `/NMI`, and a line that then stays low is
1338
+ not sampled again. CIA2 holds its output low while any enabled flag in
1339
+ `$DD0D` is set, and a read of `$DD0D` clears all flags at once. So the
1340
+ read is both the acknowledge and the re-arm, and its absence is the
1341
+ lock. The key is wired to the pin in parallel with the CIA, not through
1342
+ it, which is why it cannot be masked in `$DD0D` and why the KERNAL's
1343
+ handler recognises it by finding no flag.
1344
+
1345
+ Measured in VICE x64sc 3.10, PAL and NTSC alike (the recipe below): with
1346
+ a tick every 10,000 cycles and a window of 1,005,000 cycles, the
1347
+ acknowledging handler was entered 100 times; the same tick with a
1348
+ handler that never read `$DD0D` was entered once; and one `LDA $DD0D`
1349
+ from the main loop, with the timer still running, produced exactly one
1350
+ more entry.
1351
+
1352
+ ### Variations
1353
+
1354
+ - **Vector and lock together.** A game with no CIA2 use can take both:
1355
+ the `RTI` stub at `$0318` and a one-shot NMI left unacknowledged. The
1356
+ stub alone is enough and costs nothing while the key is up.
1357
+ - **Chaining.** `JMP $FE47` at the end of a handler hands the NMI on to
1358
+ the KERNAL, and a CIA2 flag the handler already cleared makes the
1359
+ KERNAL treat it as RESTORE; clear the flag only when you mean to
1360
+ drop the RS-232 path.
1361
+ - **Arithmetic in the handler.** The NMI path executes no `CLD`; a
1362
+ handler that uses `ADC` or `SBC` clears D on entry
1363
+ (`decimal_mode_pitfalls`).
1364
+ - **KERNAL out.** Put the handler's address at `$FFFA`/`$FFFB` in RAM
1365
+ and skip `$0318`; `$FE43` is not there to run its `SEI`, so the
1366
+ handler is entered with I as the interrupted code left it.
1367
+
1368
+ ### Cycle budget
1369
+
1370
+ The `RTI` stub costs 20 cycles per press: the NMI sequence (7), the
1371
+ `SEI` at `$FE43` (2), the `JMP ($0318)` (5) and the `RTI` (6), measured
1372
+ in VICE x64sc 3.10 as CIA1 Timer A across a 400-cycle block of `NOP`s
1373
+ with the NMI taken minus the same block with it masked, on PAL and
1374
+ NTSC. The count-and-acknowledge handler above costs 30 by the same
1375
+ measurement: 20 plus `INC abs` (6) and `BIT abs` (4). A tick every
1376
+ 10,000 cycles is 1.97 entries per PAL frame, 59 cycles a frame for the
1377
+ 30-cycle handler (arithmetic); a sample player at 8 kHz on PAL is one
1378
+ entry every 123 cycles, and its handler's cost is the player's, not
1379
+ this entry's. The `Cost` line above is the stub's: one press in the
1380
+ worst frame.
1381
+
1382
+ ### Recipes
1383
+
1384
+ - `recipes/kickassembler/nmi-timer-tick.md` (the stub, the acknowledging handler, the lock and the unlock, with the counts and the two costs above; CIA2 Timer A stands in for the key, which a headless run cannot press)
1385
+
1386
+ ## tod_alarm_interrupt — A wall-clock interrupt from the CIA time-of-day alarm
1387
+
1388
+ **Complexity:** low
1389
+ **Region:** both
1390
+ **Uses registers:** DC08, DC09, DC0A, DC0B, DC0D, DC0E, DC0F
1391
+ **Cost:** cycles_per_frame=122
1392
+ **Cost basis:** arithmetic
1393
+ **Cost measured on:** kickassembler-tod-alarm (on the frame the alarm fires)
1394
+
1395
+ ### Why
1396
+
1397
+ Every other clock on the machine is a frame or a cycle count: a raster
1398
+ IRQ runs 50 or 60 times a second depending on the model, a CIA timer
1399
+ counts cycles and wraps in 66 ms. The time-of-day clock is the only one
1400
+ that keeps wall time. It counts the mains tick, holds hours, minutes,
1401
+ seconds and tenths in BCD, and carries an alarm: four shadow registers
1402
+ and a compare that raises an interrupt when the clock reaches them. A
1403
+ program that wants "in three seconds" or "at the top of the next
1404
+ minute", the same on PAL and NTSC, with no raster line and no timer
1405
+ owned, gets it from this alarm. The one thing it does not do for free
1406
+ is run at the right rate: the KERNAL leaves the 50/60 Hz bit clear on
1407
+ every machine, and a PAL clock left that way loses a sixth of its time.
1408
+
1409
+ ### How
1410
+
1411
+ **The rate.** Bit 7 of `$DC0E` (TODIN) says what the TOD pin carries:
1412
+ 1 for 50 Hz, 0 for 60 Hz. Set it to match the machine, read-modify-write
1413
+ (`LDA $DC0E : ORA #$80 : STA $DC0E` on PAL), because bit 0 of the same
1414
+ register is the start bit of the KERNAL's jiffy timer. Decide PAL or
1415
+ NTSC at run time from the raster (a line at or above 272 is PAL); a
1416
+ build-time choice ships a clock that is wrong on the other model.
1417
+
1418
+ **The alarm.** Set bit 7 of `$DC0F` (ALARM), write hours, minutes,
1419
+ seconds, tenths to `$DC0B`, `$DC0A`, `$DC09`, `$DC08` in that order, and
1420
+ clear bit 7 again. The four writes go to the alarm registers and the
1421
+ clock is not touched. Then write `$84` to `$DC0D`: bit 7 means "set",
1422
+ bit 2 is the alarm source. There is no other arming step; the compare
1423
+ runs whenever the clock runs.
1424
+
1425
+ **The clock.** With ALARM clear, the same four writes set the clock. The
1426
+ hours write stops it and the tenths write restarts it, so the tenths
1427
+ write is the instant the clock begins.
1428
+
1429
+ **The handler.** Read `$DC0D` once and keep the copy: the read clears
1430
+ every CIA1 flag and drops the IRQ line, and a second read sees zero. Bit
1431
+ 2 is the alarm; bit 0 is the KERNAL's Timer A, which can arrive in the
1432
+ same interrupt. Read the time hours first and tenths last, so the four
1433
+ bytes are one instant and the latch is released. Hand a Timer A bit on
1434
+ to `$EA31` so the jiffy clock and keyboard scan survive; leave through
1435
+ `$EA81` otherwise. `CLD` on entry if the handler does arithmetic.
1436
+
1437
+ ```asm
1438
+ // Alarm at 01:02:06.0, alarm IRQ on, handler chained under the KERNAL.
1439
+ arm_alarm:
1440
+ lda $dc0f
1441
+ ora #$80 // ALARM = 1: the next four writes set the alarm
1442
+ sta $dc0f
1443
+ lda #$01
1444
+ sta $dc0b // hours (BCD, bit 7 = PM)
1445
+ lda #$02
1446
+ sta $dc0a // minutes
1447
+ lda #$06
1448
+ sta $dc09 // seconds
1449
+ lda #$00
1450
+ sta $dc08 // tenths
1451
+ lda $dc0f
1452
+ and #$7f // ALARM = 0: writes go to the clock again
1453
+ sta $dc0f
1454
+ lda #<alarm_irq
1455
+ sta $0314
1456
+ lda #>alarm_irq
1457
+ sta $0315
1458
+ lda $dc0d // drop any flag already standing
1459
+ lda #$84 // set bit 2: alarm interrupt enabled
1460
+ sta $dc0d
1461
+ rts
1462
+
1463
+ alarm_irq:
1464
+ cld
1465
+ lda $dc0d // one read: every flag cleared, line released
1466
+ sta icr_copy
1467
+ and #$04
1468
+ beq alarm_done
1469
+ lda $dc0b // hours first: the four registers latch
1470
+ sta now_h
1471
+ lda $dc0a
1472
+ sta now_m
1473
+ lda $dc09
1474
+ sta now_s
1475
+ lda $dc08 // tenths last: the latch is released
1476
+ sta now_t
1477
+ inc alarm_count
1478
+ alarm_done:
1479
+ lda icr_copy
1480
+ and #$01
1481
+ beq alarm_exit
1482
+ jmp $ea31 // Timer A too: the KERNAL's tick runs
1483
+ alarm_exit:
1484
+ jmp $ea81 // pop A, X, Y and RTI
1485
+
1486
+ icr_copy: .byte 0
1487
+ alarm_count: .byte 0
1488
+ now_h: .byte 0
1489
+ now_m: .byte 0
1490
+ now_s: .byte 0
1491
+ now_t: .byte 0
1492
+ ```
1493
+
1494
+ ### Why it works
1495
+
1496
+ The alarm compare is a digit compare against the running clock, done in
1497
+ the chip; it raises flag 2 of `$DC0D` at the tick on which all four
1498
+ registers match, and if bit 2 of the mask is set the chip pulls its
1499
+ interrupt line, which on CIA1 is `/IRQ`. The clock keeps running through
1500
+ the match, so the alarm fires once per day for a given setting unless it
1501
+ is moved. The rate bit only changes the divider between the pin and the
1502
+ tenths digit, five or six ticks a tenth; the compare does not know
1503
+ whether the digits are keeping real time, which is why a wrong TODIN is
1504
+ invisible to everything except a comparison with something that does.
1505
+
1506
+ Measured in VICE x64sc 3.10 (`recipes/kickassembler/tod-alarm.md`, rung
1507
+ 1): with the clock set to 01:02:03.0 and the alarm at 01:02:06.0, the
1508
+ handler was entered after 149 PAL frames and 179 NTSC frames, one frame
1509
+ inside the tolerance of one tenth, with `$DC0D` reading `$84` and the
1510
+ four registers reading 01:02:06.0 on both models. The same code with
1511
+ TODIN the wrong way took 180 PAL frames (six fifths of the time) and
1512
+ 149 NTSC frames (five sixths). The one-frame shortfall is the tick's
1513
+ phase: the clock starts on the tenths write but the mains tick does not
1514
+ restart, so the alarm lands early and never late, inside one tenth
1515
+ (arithmetic from the tick period, 19,705 PAL cycles or 17,045 NTSC).
1516
+ The bound is one tick if the five-or-six-tick divider behind the tenths
1517
+ digit restarts with the clock; if it keeps its count across the stop,
1518
+ the first tenth can take from one to five ticks and the alarm can be up
1519
+ to four ticks early. Neither VICE nor a 6526 has been measured on that
1520
+ point; the counts here fit both.
1521
+
1522
+ ### Variations
1523
+
1524
+ - **A game clock in real seconds.** Set the alarm one second on; in the
1525
+ handler, add one second to the alarm time in BCD (`SED : ADC #$01`,
1526
+ carry into minutes and hours by hand, `CLD` after) and write all four
1527
+ alarm registers again with ALARM set, tenths last. The seconds count
1528
+ is then the same on a PAL and an NTSC machine, and the same under a
1529
+ frame drop, which a frame-counted clock is not. Write all four, not
1530
+ just the seconds: the datasheet stops the clock on any hours write and
1531
+ does not say the alarm side is exempt, so the tenths write that ends
1532
+ the sequence is what guarantees a running clock (`pitfalls/cia.md`,
1533
+ `tod_read_order_latch`, which measured VICE and could not measure a
1534
+ 6526).
1535
+ - **Pausing.** Not reading the clock does not pause it, and reading the
1536
+ hours and stopping does not either: the latch freezes what you see
1537
+ while the counter runs on underneath, and the next tenths read shows
1538
+ the live time. To pause a clock, stop it: write its hours with ALARM
1539
+ clear, which halts the count, and write its tenths to resume; the
1540
+ fraction of a tenth in progress is lost. Or leave it running and
1541
+ subtract the paused span, read at pause and at resume, from the
1542
+ displayed time and add it to the alarm.
1543
+ - **CIA2's clock.** `$DD08`-`$DD0F` is a second, independent TOD with
1544
+ its own alarm, whose interrupt is `/NMI`. It cannot be masked, it
1545
+ shares the vector with RESTORE and it needs a `$DD0D` read of its own
1546
+ (`nmi_handler_and_restore_key`). It is the clock to use when CIA1's
1547
+ interrupt line is spoken for by a raster or timer scheme, and its
1548
+ TODIN bit, bit 7 of `$DD0E`, is also left clear by the KERNAL.
1549
+
1550
+ ### Cycle budget
1551
+
1552
+ The `Cost` line is the handler above, `alarm_irq`, for one alarm entry
1553
+ under the KERNAL: the IRQ sequence (7), the dispatcher at `$FF48` (29),
1554
+ the body from `CLD` to `INC alarm_count` (52), the exit test (12) and
1555
+ `$EA81` (22), 122 cycles on the frame the alarm fires and nothing on
1556
+ the frames in between. The body's 52 is measured: a build of the recipe
1557
+ with only that body between the two Timer A reads gave 68 cycles on PAL
1558
+ and on NTSC (VICE x64sc 3.10), and the harness puts 16 cycles inside
1559
+ that span, 12 to stash the entry read and 4 for the exit read to reach
1560
+ its read cycle. The other four terms are the instruction table, so the
1561
+ line's basis is arithmetic. The recipe's own handler, which also records
1562
+ the raster line, Timer A, the frame count and the `$DC0D` copy, reads
1563
+ 130 cycles on the same harness, and its whole entry with the cost
1564
+ bookkeeping (about 38) is about 240 (arithmetic). An earlier version of
1565
+ this page stated the recipe's 130 as the `Cost` and put the fragment at
1566
+ "about 110"; the fragment's sum is 122, and it has now been measured.
1567
+
1568
+ ### Recipes
1569
+
1570
+ - `recipes/kickassembler/tod-alarm.md` (clock set, alarm three seconds on, the alarm taken as a CIA1 IRQ under the KERNAL's jiffy, frames counted against the model, the drift with TODIN wrong)