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,2566 @@
1
+ ---
2
+ category: sprite
3
+ chip: VIC-II
4
+ ---
5
+
6
+ <!-- doc-type: technique-reference -->
7
+
8
+ # Sprite Techniques
9
+
10
+ The VIC-II provides eight hardware sprites (MOBs — Movable Object Blocks) per
11
+ frame. Each sprite is a 24×21 pixel bitmap (or 12×21 in multicolor mode) stored
12
+ as 63 bytes plus one padding byte (64 bytes, 64-byte aligned). The chip fetches
13
+ sprite data via DMA only on the raster lines where a sprite is vertically
14
+ active (its 21, or 42 expanded, lines): two stolen CPU cycles per active sprite
15
+ per line plus a 3-cycle BA lead-in, up to 19 per line with all eight — the
16
+ s-access budget the CPU pays whether or not you touch a register. (The
17
+ pointer p-access happens every line for every sprite, enabled or not, and costs
18
+ the CPU nothing; an earlier version of this paragraph said data DMA runs on
19
+ every line a sprite is enabled.)
20
+
21
+ The eight sprites are individually positioned, colored, expanded, and
22
+ prioritized via registers at $D000–$D02E. The fundamental constraint every
23
+ sprite technique works around: only eight hardware sprites exist, the frame is
24
+ 312 lines on PAL (263 on NTSC), and sprite DMA competes with the CPU for bus
25
+ access. Everything below describes how to work within, around, or deliberately
26
+ against those constraints.
27
+
28
+ ---
29
+
30
+ ## sprite_multiplex_8 — 8-sprite multiplexer
31
+
32
+ **Complexity:** medium
33
+ **Region:** both
34
+ **Uses registers:** D015, D000, D001, D027, D012, D019, D01A
35
+ **Uses kernal:** (none)
36
+ **Demands:** midframe_raster_irqs, changes_sprite_set
37
+ **Cost:** cycles_per_frame=5301
38
+ **Cost basis:** arithmetic
39
+ **Cost measured on:** oscar64-sprite-multiplex-8 (the three calls' worst cases summed)
40
+ **Claims:** sprite_0-7 (owns), vic_raster_irq (owns)
41
+ **Claims basis:** derived-listing
42
+
43
+ ### Why
44
+
45
+ A game with a scrolling playfield, player character, several enemy types, and
46
+ projectiles quickly exceeds eight simultaneous on-screen objects. The VIC-II
47
+ provides only eight hardware sprites per frame, but a typical action game needs
48
+ twelve to twenty-four distinct moving objects visible at once. The 8-sprite
49
+ multiplexer is the first level of the solution: reuse each of the eight hardware
50
+ sprites multiple times within a single frame by reprogramming them between
51
+ uses.
52
+
53
+ ### How
54
+
55
+ The core idea is time-division multiplexing along the Y axis. Before the frame
56
+ begins, sort your logical sprite list by ascending Y position. Divide the screen
57
+ into horizontal bands: the top band gets the first pass of the eight hardware
58
+ sprites; below that, after each logical sprite has been drawn, re-arm the
59
+ hardware sprites with the attributes of the next logical sprite waiting in line.
60
+
61
+ In practice:
62
+
63
+ 1. At the start of the frame (or in the vertical blank), write the top eight
64
+ logical sprites to hardware registers ($D000-$D00F, $D027-$D02E, $D015).
65
+ 2. For each subsequent group of logical sprites, program a raster IRQ at a
66
+ scanline slightly above the Y position of the next group's topmost member.
67
+ 3. Inside the raster IRQ handler, reprogram each hardware sprite's Y position,
68
+ image pointer (via the sprite pointer block at video\_matrix + $3F8), and
69
+ color ($D027-$D02E) to match the next group's attributes.
70
+ 4. Acknowledge the interrupt ($D019), re-arm for the group after that, then
71
+ return.
72
+
73
+ The trick is that the VIC-II reads sprite data on the *current* raster line but
74
+ compares the hardware sprite Y register at the *start* of each line. Once the
75
+ chip has started drawing a sprite it continues drawing its pixel rows even if
76
+ you change the Y register partway through. You can therefore safely repoint the
77
+ hardware sprite to a new logical sprite's data as soon as the old one has
78
+ started its last pixel row — provided your IRQ fires before the new Y position
79
+ is reached.
80
+
81
+ ### Why it works
82
+
83
+ The VIC-II fetches sprite pointer bytes and pixel data during the portion of
84
+ each raster line where the chip has bus priority (p-accesses and s-accesses).
85
+ The sprite Y register ($D001, $D003, ...) is compared against the current raster
86
+ counter at the *start* of each line to determine whether that sprite's DMA
87
+ should fire this line. Once a sprite is vertically "active" (its top scanline
88
+ has been reached and the chip is counting through its 21 rows), changes to the
89
+ Y register take effect for the *next* activation comparison, not the current
90
+ one.
91
+
92
+ This means the hardware gives you a full 21 lines of safe window per sprite:
93
+ after you see the sprite start rendering (raster counter passes its Y), you have
94
+ 21 lines to change the Y register and pointer to point at the next logical
95
+ sprite before the VIC looks for its new activation. The raster IRQ, triggered at
96
+ the right Y value via $D012/$D011, fires the CPU just in time to perform those
97
+ register writes.
98
+
99
+ ### Variations
100
+
101
+ **Double-y-frame:** Instead of pure Y-band multiplexing, the interrupt fires
102
+ just below each sprite's *bottom* edge. This gives maximum flexibility but
103
+ requires careful ordering to avoid races when two logical sprites have nearly
104
+ the same Y position.
105
+
106
+ **Fixed three-pass:** Split the screen into three equal-height bands (roughly
107
+ top/middle/bottom thirds). Each band gets a fixed group of hardware sprites.
108
+ Less flexible but simpler code and more predictable cycle budget.
109
+
110
+ **Enable mask only:** A lighter version that skips image/color reprogramming
111
+ and only toggles $D015 bits and updates Y positions. Appropriate for
112
+ same-image, same-color sprite swarms (bullet patterns, particle effects).
113
+
114
+ ### Cycle budget
115
+
116
+ On PAL, each raster line is 63 cycles. Through the KERNAL vector ($0314) your
117
+ handler's first instruction runs 36 cycles after the interrupt is taken — 7 for
118
+ the interrupt sequence and 29 for the $FF48 dispatcher (PHA TXA PHA TYA PHA TSX
119
+ LDA $0104,X AND #$10 BEQ JMP ($0314)) — plus 0–6 cycles of jitter from the
120
+ interrupted instruction, and more if the interrupt lands on a badline.
121
+ Acknowledging $D019 costs about 6 more, and the bare exit through $EA81 (PLA
122
+ TAY PLA TAX PLA RTI) 22, so the round trip is about 64 cycles, a full raster
123
+ line, of which the 36–42 before your first write are what eat into the slack.
124
+ Banking the KERNAL out and pointing $FFFE/$FFFF at the handler removes the
125
+ 29-cycle dispatcher. (An earlier version of this section put the whole
126
+ entry/acknowledge/exit overhead at about 15 cycles, which is not consistent
127
+ with the 29-cycle dispatcher documented in `raster.md`.) With 8 sprites per
128
+ group, writing each sprite's Y ($D001+2n), image pointer (screen + $3F8 + n),
129
+ and color ($D027+n) costs 3 stores × 4 cycles each = 12 cycles per sprite × 8
130
+ sprites = 96 cycles. Entry of 36–42 cycles plus 96 cycles of writes is 132–138
131
+ cycles, already more than two 63-cycle lines, so you need at least 3 lines of
132
+ slack (not 2, as this section used to say) between the IRQ trigger line and the
133
+ first new sprite's Y position to complete all writes before the VIC latches the
134
+ next activation. In practice, target 3–4 lines of slack.
135
+
136
+ The Cost line's 5,301 cycles is the sum of the three per-frame calls'
137
+ worst cases in the Oscar64 recipe, each measured there with a CIA 2 timer
138
+ over 200 frames in VICE: `vspr_sort` 2,018, `vspr_update` 1,853,
139
+ `rirq_sort` 1,430 (each maximum includes interrupts landing inside the
140
+ call). The sum is arithmetic, not one measured frame. An earlier Cost line
141
+ said 9,162, which was the recipe's whole frame loop including its
142
+ sine-table motion, a demonstration payload.
143
+
144
+ ### Recipes
145
+
146
+ - `recipes/oscar64/sprite-multiplex-8.md`
147
+
148
+ ---
149
+
150
+ ## sprite_multiplex_24 — Up to 24+ sprites via raster reuse
151
+
152
+ **Complexity:** scene-tier
153
+ **Region:** both
154
+ **Uses registers:** D015, D000, D001, D002, D003, D004, D005, D006, D007, D008, D009, D00A, D00B, D00C, D00D, D00E, D00F, D010, D027, D028, D029, D02A, D02B, D02C, D02D, D02E
155
+ **Uses kernal:** (none)
156
+ **Demands:** midframe_raster_irqs, changes_sprite_set
157
+ **Cost:** cycles_per_frame=700, irq_slots=3, bytes_code=900, sprites_per_line=8
158
+ **Cost basis:** estimated
159
+ **Claims:** sprite_0-7 (owns), vic_raster_irq (owns)
160
+ **Claims basis:** derived-listing
161
+
162
+ ### Why
163
+
164
+ The basic 8-sprite multiplexer described above can extend to 16 in a
165
+ straightforward two-pass design. Going beyond 16 — to 24, 32, or more — requires
166
+ tighter IRQ scheduling, Y-sorted lists, and careful management of the MSB X
167
+ register ($D010) across passes. Scene-tier demoscene sprite engines routinely
168
+ display 30+ logical sprites on PAL systems by splitting the frame into three or
169
+ more reuse passes. Games needing large enemy crowds, parallax-layer overlays, or
170
+ sprite-based status bars use the same approach.
171
+
172
+ ### How
173
+
174
+ The algorithm scales naturally from the 8-sprite multiplexer:
175
+
176
+ 1. **Sort by Y:** Before each frame, sort the entire logical sprite array by
177
+ ascending Y position. Oscar64's `vspr_sort()` performs an insertion sort,
178
+ which is cache-friendly and fast on nearly-sorted lists (typical across
179
+ consecutive frames).
180
+
181
+ 2. **Assign the first eight to hardware directly:** The top eight logical
182
+ sprites (lowest Y values) are written to hardware at frame start.
183
+ `vspr_update()` handles this, writing all eight positions, images, colors,
184
+ and the MSB-X byte ($D010) in one pass.
185
+
186
+ 3. **Schedule reuse IRQs per hardware slot:** For each logical sprite beyond
187
+ the first eight (sorted index ti+8, hardware slot ti & 7), `vspr_update()`
188
+ calls `rirq_move(ti, spriteYPos[ti + 1] + 23)` — the raster line two below
189
+ the bottom of the sprite that slot is currently showing (the previous
190
+ occupant's Y + 21 lines + 2 lines of IRQ latency margin), not a line derived
191
+ from the incoming sprite's Y. It then stores the incoming sprite's Y, X (low
192
+ byte), image pointer, colour and the accumulated $D010 mask as that slot's
193
+ five data bytes (`rirq_data`). Once the next incoming sprite's Y is ≥ 250,
194
+ that slot and all later ones are cleared (`rirq_clear`). There are no fixed
195
+ eight-sprite "groups" or "passes": each slot is reused as soon as its own
196
+ previous sprite has finished, and the `80 + 4*i` rows set in `vspr_init()`
197
+ are placeholders overwritten every frame. (An earlier version of this step
198
+ said the IRQ was placed "just above the first sprite in the group"; the
199
+ incoming sprite's Y is only written as data — `sprites.c` L318/L330.)
200
+
201
+ 4. **MSB-X accumulation:** Sprites whose X position exceeds 255 require bit n
202
+ of $D010 to be set. Because $D010 covers all eight sprites in a single byte,
203
+ mid-frame updates must accumulate the correct combined mask. `vspr_update()`
204
+ computes a running `xymask` that encodes the MSB bits for all active sprites
205
+ and writes it into each raster IRQ block.
206
+
207
+ 5. **Synchronization:** `rirq_wait()` pauses the main loop until the last raster
208
+ IRQ has completed, ensuring `vspr_sort()` and `vspr_update()` are never called
209
+ while the raster IRQ list is being consumed by the chip.
210
+
211
+ ### Why it works
212
+
213
+ Oscar64's virtual sprite system (`vspr_*` API) implements this algorithm using
214
+ the `rasterirq` library's sorted IRQ slot table. Each virtual sprite beyond the
215
+ first eight gets one slot in the `spirq` array (`VSPRITES_MAX - 8` entries, 8
216
+ by default for 16 total; raise `VSPRITES_MAX` to extend). The raster IRQ
217
+ executor in `rasterirq.c` fires each slot when the raster counter matches the
218
+ programmed line, runs that slot's code template, and immediately re-arms for
219
+ the next slot. The template is not just "five STAs": each `rirq_build` template
220
+ starts with a raster busy-wait (LDY #/LDX #/CMP $D012/BCS) that holds the CPU
221
+ from the IRQ at row−1 (row−2 via the KERNAL vector) until $D012 reads row+1,
222
+ then does the five writes (Y, X low byte, image pointer, colour, $D010 mask),
223
+ re-arms $D012/$D019 and exits. Measured in VICE (PAL, `rirq_init_io`, RAM
224
+ vector): about 160 cycles of handler code plus the wait, roughly 225–280 cycles
225
+ stolen per slot (about 3.5–4.5 raster lines); via `rirq_init_kernal` about
226
+ 300–330 cycles. An earlier version of this section put a slot at "roughly 25
227
+ cycles", which counted only the five stores and none of the entry, busy-wait,
228
+ re-arm or exit.
229
+
230
+ Because `vspr_init` builds one slot per virtual sprite beyond the first eight
231
+ (hardware sprite i & 7, moved to that sprite's Y + 23 each frame), 24 vspr
232
+ sprites cost 16 reuse IRQs per frame — roughly 16 × 250 ≈ 4,000 cycles, about
233
+ 20 % of PAL's 312 × 63 = 19,656 cycles per frame, not the 120 cycles this
234
+ section used to claim. A hand-scheduled three-pass multiplexer that rewrites
235
+ all eight sprites per IRQ (as the KickAssembler recipe does) is a different
236
+ design with three IRQs per frame; the figures above are for the `vspr_*`
237
+ per-slot design. The other constraint is Y-band density: if ten logical
238
+ sprites cluster within a 21-line band, you only get one pass over them, not
239
+ ten; duplicates at the same Y simply are not all visible simultaneously.
240
+
241
+ ### Variations
242
+
243
+ **32+ sprites:** Raise `VSPRITES_MAX` beyond 16 (rebuild required; pass it as
244
+ `-dVSPRITES_MAX=24` on the Oscar64 command line, since a `#define` in your own
245
+ file does not reach `sprites.c`, and raise `NUM_IRQS` with it or `rasterirq.c`
246
+ warns "Index out of bounds"). Each additional logical sprite adds one raster
247
+ IRQ slot and needs a 21-line gap below its slot's previous occupant. With vspr
248
+ slots at ~250 cycles each, 10 % of a PAL frame is about eight reuse IRQs, i.e.
249
+ about 16 logical sprites — not the "roughly 48" this section used to say,
250
+ which assumed 25-cycle slots.
251
+
252
+ **Per-sprite priority within a pass:** Within one pass (one set of eight
253
+ hardware sprites), hardware priority is fixed: sprite 0 is always in front of
254
+ sprite 1. Design the sort order so that in areas of overlap, the intended
255
+ top-priority sprite ends up in a lower-numbered hardware slot.
256
+
257
+ **Cross-reference:** `recipes/kickassembler/sprite-multiplex-24.md` is the
258
+ fixed three-band variant (the "Fixed three-pass" option above) in
259
+ KickAssembler: one raster IRQ above each band rewrites all eight hardware
260
+ slots, with no per-frame Y sort and nothing cycle-exact; the recipe's own text
261
+ says what that restriction costs. (An earlier version of this paragraph called
262
+ it a hand-scheduled, cycle-exact demoscene multiplexer, which it is not.)
263
+
264
+ ### Cycle budget
265
+
266
+ With Oscar64's `vspr_*` system and `VSPRITES_MAX=24`, each of the 16 reuse
267
+ slots costs roughly 225–280 cycles (entry, busy-wait to row+1, five writes,
268
+ re-arm, exit — measured in VICE, PAL, RAM vector), about 4,000 cycles per
269
+ frame. `vspr_sort()` (`sprites.c`, a byte-array insertion sort) costs about
270
+ 1,100 cycles per frame on an already-sorted list (~44–48 cycles per element ×
271
+ 23), about 1,750 when one sprite has moved past a neighbour, and about 10,000
272
+ in the worst reverse-order case, measured in VICE with a CIA timer; the
273
+ 16-sprite default already costs ~750 cycles sorted. Sort overhead alone is
274
+ therefore ~5.7 % of a 19,656-cycle PAL frame, and with the reuse IRQs (and
275
+ `vspr_update()`, not measured here) the total is well over 5,000 cycles, more
276
+ than a quarter of the frame. An earlier version of this section gave "40 cycles
277
+ per pass, 80–100 cycles to sort, under 250 cycles total (~1.3 %)"; every one of
278
+ those figures was too small by an order of magnitude, and they described a
279
+ three-pass design that `vspr_*` does not implement.
280
+
281
+ ### Recipes
282
+
283
+ - `recipes/oscar64/sprite-multiplex-8.md` (scales directly; raise VSPRITES_MAX)
284
+
285
+ ---
286
+
287
+ ## sprite_multiplex_game — Game multiplexer in assembly: persistent sort, double-buffered table, zone IRQs, late guard
288
+
289
+ **Complexity:** high
290
+ **Region:** both
291
+ **Uses registers:** D000, D001, D010, D012, D015, D019, D01A, D027
292
+ **Uses kernal:** (none)
293
+ **Demands:** midframe_raster_irqs, changes_sprite_set
294
+ **Cost:** cycles_per_frame=16600, irq_slots=17
295
+ **Cost basis:** arithmetic
296
+ **Cost measured on:** kickassembler-sprite-multiplex-game (worst frame: a reversed sort)
297
+ **Claims:** sprite_0-7 (owns), vic_raster_irq (owns)
298
+ **Claims basis:** derived-listing
299
+
300
+ ### Why
301
+
302
+ `sprite_multiplex_24` covers Oscar64's `vspr_*` path, which takes one IRQ
303
+ per reused sprite and about 20 % of a PAL frame for 24 sprites. The
304
+ KickAssembler three-band recipe keeps every sprite inside a fixed band
305
+ and does not sort. A game needs sprites that go anywhere, a sort that is
306
+ cheap on the frames a game actually produces, and IRQ code it owns and
307
+ can budget. Cadaver found this structure in Ocean and Imagine games such
308
+ as Green Beret and Midnight Resistance (sources below).
309
+
310
+ ### How
311
+
312
+ 1. **Sort by Y, keeping last frame's order.** An index array `order[]` is
313
+ never reset. Each frame an insertion sort repairs it: an actor that has
314
+ not passed a neighbour costs one compare, one that has passed k
315
+ neighbours is shifted k places. This is the "Ocean" or continuous
316
+ insertion sort.
317
+ 2. **Build into the half the IRQs are not reading.** Walk `order[]` and
318
+ copy each accepted actor's Y, X, pointer, colour and a precalculated
319
+ $D010 byte into a sorted table. The table has two halves; the main loop
320
+ builds one while the IRQs show the other, then sets a ready flag. The
321
+ frame IRQ swaps halves only when the flag is set, so a late build shows
322
+ the previous frame again instead of a half-written table. The unsorted
323
+ actor tables are single; only the main loop touches them.
324
+ 3. **Reject the ninth sprite on a band.** Accepted entry a goes to slot
325
+ a mod 8, which last showed entry a − 8. If the new Y is less than 21
326
+ lines below that entry's Y, the slot cannot show it: reject the actor
327
+ for this frame (`sprite_dma_overflow` measured Y = 100 then 120 lost,
328
+ 100 then 121 shown).
329
+ 4. **Group sprites into zones.** The frame IRQ, below the last sprite
330
+ line, writes the first eight. Every later sprite belongs to a zone: a
331
+ run of sorted sprites close enough in Y to be written by one IRQ. The
332
+ zone's IRQ line is set early enough for all its writes, Y first.
333
+ 5. **Guard against a late IRQ.** At the end of each zone, store the next
334
+ zone's line in $D012, then compare line − 3 with $D012. If the raster is
335
+ already there, run the next zone now, without exit, acknowledge or
336
+ entry. Otherwise acknowledge and return.
337
+
338
+ ### Why it works
339
+
340
+ The VIC-II starts a sprite on the line after its Y matches the raster, and
341
+ only when that slot's DMA is off; it then draws 21 lines from whatever the
342
+ registers hold. A slot is free for a new Y once its old sprite has started,
343
+ provided the new Y is at least 21 lines lower. A Y write that lands after
344
+ the raster has passed it never matches that frame, so the sprite is lost,
345
+ not delayed. The sort and the 21-line rule decide what can be shown; the
346
+ zone lines decide whether each write is in time.
347
+
348
+ An IRQ line written after the raster has passed it does not fire until
349
+ the next frame. Without the guard, every zone after a late one is lost for
350
+ that frame. In the recipe, a build without the guard showed 11 of 24
351
+ actors in 2 of 16 swept shots (9.3 million cycles, PAL and NTSC) and 24
352
+ in the other 14; with it, 24 of 24 in all 16 (measured in VICE x64sc).
353
+
354
+ The guard fires in about 80 % of frames. That is zones being merged when
355
+ they sit close together, not IRQs arriving late by accident. A probe
356
+ measured the largest overshoot on the fall-through path as 5 lines past
357
+ line − 3, so at most 2 lines past the scheduled line, which leaves at
358
+ least 2 lines before the zone's first Y (PAL, 309 frames, VICE x64sc). An
359
+ 8-sprite zone under full sprite DMA on a badline was not measured here.
360
+
361
+ ### Choosing the sort
362
+
363
+ | Sort | Cost pattern | Wins when |
364
+ |---|---|---|
365
+ | Insertion from last frame's order (Ocean) | One compare per actor in place; about 30 cycles per place an actor moves. Measured in the recipe for 24 actors: 611 sorted, 5,228 shuffled, 8,783 reversed; 611 to 953 per frame in 16 shots of play, 1,411 the largest seen | Actors move a few lines a frame and rarely overtake many others: most games |
366
+ | Bucket on Y | Nearly the same every frame, whatever the order | Orders change wholesale: spawning waves, teleports, many actors re-entering at once. Falco Paul's Java model put it 18 % slower than Ocean on a game-like pattern, best with 128 buckets (not measured here) |
367
+ | Hybrid: Ocean, falling back to bucket when the swaps pile up | Ocean's cost on quiet frames, bucket's ceiling on bad ones | A frame budget that cannot absorb the insertion sort's worst case |
368
+
369
+ The Java figures are from a model of 64 sprites, not 6502 code. Cadaver
370
+ recommends the Ocean sort for real projects and uses it in MW4. Linus
371
+ Akesson's Field Sort and radix sorts are further options with published
372
+ 6502 figures (sources below; not measured here).
373
+
374
+ ### Variations
375
+
376
+ **Just after the old sprite.** Fire each reuse IRQ just below the slot's
377
+ previous sprite and write Y last. A late write then drops the new sprite
378
+ instead of drawing a glitch, but tight formations lose sprites. The
379
+ recipe uses "just before the new sprite", which keeps them but lets the
380
+ new X, colour and pointer land during the old sprite's last lines when
381
+ the gap is exactly 21.
382
+
383
+ **Priority mapping.** Instead of slot a mod 8, pick a free slot by the
384
+ actor's priority class, so that the player or explosions get
385
+ low-numbered slots and draw in front.
386
+
387
+ **Unrolled writes per slot.** One block of code per hardware slot with
388
+ constant register addresses, entered at the first slot to write, avoids
389
+ the slot lookups at the cost of code size.
390
+
391
+ ### Cycle budget
392
+
393
+ Measured in the recipe on PAL (VICE x64sc, CIA2 timers): sort 611 cycles
394
+ for 24 sorted actors (26 per compare × 23 + 13 = 611, which matches the
395
+ measurement; 28 per compare when the loop branches cross a page), 611 to
396
+ 953 per frame in 16 shots of play and 1,411 the largest seen; build
397
+ 3,815; multiplexer IRQs 2,201 to 2,764 per frame with 7 to 13 zones
398
+ (10 in the pinned shot), plus 47 cycles per IRQ taken that the timer
399
+ cannot see (arithmetic from the listing).
400
+
401
+ The Cost line is the worst frame, by arithmetic: 8,783 (a full
402
+ reversal) + 3,815 + 2,764 + 17 × 47 + 6 × 76 ≈ 16,600 cycles. The last
403
+ term is six zones more than the ten in the 2,764 frame, at about 76
404
+ cycles of timed per-zone code each (arithmetic from the listing). That
405
+ is about 84 % of a PAL frame, and more than an NTSC frame leaves after
406
+ badline and sprite DMA. The double buffer turns the overrun into one
407
+ repeated frame (MISSED), not a torn one. Without a reversal the worst
408
+ frame is about 8,800 (1,411 + 3,815 + 2,764 + 17 × 47). The Cost line includes the
409
+ reversal a respawn of every actor can cause. `irq_slots=17` is a ceiling: the frame
410
+ IRQ plus 16 single-sprite zones, before the late guard merges any. The
411
+ recipe's frames built 7 to 13 zones.
412
+
413
+ ### Sources
414
+
415
+ Cadaver, "Sprite multiplexing", https://cadaver.github.io/rants/sprite.html
416
+ (continuous insertion sort, 21-line rejection, double buffering,
417
+ precalculated $D010, the late check with a 3-line margin). Falco Paul in
418
+ "Speculative sprite sorting methods",
419
+ https://cadaver.github.io/rants/sorting.html (bucket, Ocean and hybrid
420
+ sorts in Java). Linus Akesson, Field Sort,
421
+ https://www.linusakesson.net/programming/fieldsort/index.php. Codebase64,
422
+ https://codebase.c64.org/doku.php?id=base%3Asprite_multiplexing (sort
423
+ families). cadaver/c64gameframework, https://github.com/cadaver/c64gameframework
424
+ (`screen.s`, `raster.s`; MIT; read for structure only).
425
+
426
+ ### Recipes
427
+
428
+ - `recipes/kickassembler/sprite-multiplex-game.md`
429
+
430
+ ---
431
+
432
+ ## sprite_expand — Hardware-expand sprites
433
+
434
+ **Complexity:** low
435
+ **Region:** both
436
+ **Uses registers:** D017, D01D
437
+ **Uses kernal:** (none)
438
+
439
+ ### Why
440
+
441
+ A standard C64 sprite is 24×21 pixels — small enough to look crisp at C64
442
+ resolution but too small for boss characters, large vehicles, title-screen
443
+ logos, or any object that needs to dominate the screen. Drawing a 48×42 sprite
444
+ by storing the full 48×42 bitmap would require four 64-byte sprite blocks laid
445
+ out as a 2×2 grid, plus positioning math. Hardware expansion achieves the same
446
+ visual result at zero extra memory cost and zero extra DMA bandwidth.
447
+
448
+ ### How
449
+
450
+ $D01D (XXPAND) controls X expansion and $D017 (YXPAND) controls Y expansion.
451
+ Each register is a bitmask: bit n = 1 expands hardware sprite n by 2× in the
452
+ respective axis.
453
+
454
+ To double the size of sprite 3 in both axes:
455
+
456
+ - OR bit 3 into $D01D: sprite 3 now renders 48 pixels wide.
457
+ - OR bit 3 into $D017: sprite 3 now renders 42 lines tall.
458
+
459
+ The registers are independent, so X-only, Y-only, or both expansions are all
460
+ valid. The sprite image data remains the original 63-byte block; the chip
461
+ simply repeats each pixel column twice (X expansion) or each pixel row twice
462
+ (Y expansion) during output.
463
+
464
+ ### Why it works
465
+
466
+ During sprite DMA the VIC-II reads the 63 data bytes for each active sprite once
467
+ per "sprite line" — its internal row counter. With X expansion enabled, the chip's
468
+ horizontal shift register clocks each pixel bit onto the output bus twice instead
469
+ of once, stretching each pixel to two display clocks wide (two pixels on screen).
470
+ With Y expansion enabled, the chip's internal row counter increments only on
471
+ every *second* raster line instead of every line, so the same bitmap row is output
472
+ twice, doubling the visible height.
473
+
474
+ The result is 2× linear scaling with no antialiasing, producing visibly blocky
475
+ edges at close range. This is generally acceptable for game objects (the C64
476
+ aesthetic tolerates visible pixels) and is often desirable for close-range
477
+ boss-fight scaling effects.
478
+
479
+ Combined X+Y expansion produces a 48×42 sprite that costs the same DMA bandwidth
480
+ as an unexpanded sprite: the chip still fetches exactly 63 bytes of data per 21
481
+ chip-internal rows.
482
+
483
+ ### Variations
484
+
485
+ **Single-axis expand for squash-and-stretch:** Animate the Y-expand bit across
486
+ frames to simulate a bouncing ball compressing and expanding. The binary
487
+ nature of hardware expansion means the effect is coarse, but readable at
488
+ C64 pixel sizes.
489
+
490
+ **Mixed expanded/unexpanded sprites:** Sprite 0 (the player) can be fully
491
+ expanded while sprites 1–7 (small bullets) are unexpanded — the registers are
492
+ per-sprite. Mix freely.
493
+
494
+ **Fake 48×84 via two expanded sprites stacked:** Two X+Y-expanded sprites at
495
+ the same X but offset 42 lines apart produce a visual object 84 lines tall —
496
+ about 42 % of the 200-line display window (three stacked reach 126). An
497
+ earlier version of this paragraph called 84 lines "two-thirds of the PAL screen
498
+ height"; two-thirds of 200 is 133.
499
+
500
+ ### Recipes
501
+
502
+ - `recipes/kickassembler/sideborder-open.md` (sets $D017 Y-expand for 42-line
503
+ sprite DMA). No Oscar64 recipe calls `spr_expand()`; an earlier version of
504
+ this list pointed at `recipes/oscar64/sprite-multiplex-8.md`, which does not
505
+ use it.
506
+
507
+ ---
508
+
509
+ ## sprite_collision_detect — Sprite-sprite and sprite-background collision
510
+
511
+ **Complexity:** low
512
+ **Region:** both
513
+ **Uses registers:** D01E, D01F
514
+ **Uses kernal:** (none)
515
+
516
+ ### Why
517
+
518
+ Every action game needs collision detection: player versus enemy, bullet versus
519
+ enemy, player versus terrain. Software bounding-box tests are fast but require
520
+ explicit bounding-box data per sprite. The VIC-II provides hardware collision
521
+ detection as a free by-product of its rendering pipeline: the chip tracks pixel
522
+ overlap automatically and latches results in two read-only registers.
523
+
524
+ ### How
525
+
526
+ **Sprite-sprite collision ($D01E, SPSPCL):** Each bit n is set whenever any
527
+ non-transparent pixel of sprite n overlaps any non-transparent pixel of any
528
+ other enabled sprite during rendering. The register is *read-to-clear*: as soon
529
+ as you read $D01E, all bits reset to zero. A nonzero value means at least one
530
+ collision occurred since the last read.
531
+
532
+ **Sprite-background collision ($D01F, SPBGCL):** Each bit n is set whenever any
533
+ non-transparent pixel of sprite n overlaps a *foreground* pixel of the display,
534
+ where foreground means: in standard hires text and hires bitmap, any pixel not
535
+ drawn in background colour 0 ($D021); in ECM, any pixel not drawn in one of
536
+ BGCOL0–3 — the four background colours are all background; in multicolor text
537
+ and multicolor bitmap, only the %10 and %11 bit-pairs — %01 pixels (BGCOL1/$D022
538
+ in MC text, the video-matrix high nibble in MC bitmap) count as background and
539
+ do NOT set $D01F. This is the same foreground/background split that $D01B
540
+ priority uses. Measured in VICE x64sc (solid sprites over MC-text, MC-bitmap
541
+ and ECM cells: $D01F = $2E, $2E, $28); an earlier version of this section said
542
+ any pixel not in background colour 0, which over-reports collisions on MC %01
543
+ and ECM BGCOL1–3 pixels. Also read-to-clear.
544
+
545
+ Typical polling pattern: once per frame (in the vertical blank or at the end of
546
+ the main game loop), read $D01E and $D01F and store both values. Then inspect
547
+ the stored values:
548
+
549
+ - A nonzero $D01E value with two or more bits set means those two sprites
550
+ physically overlapped at the pixel level.
551
+ - A nonzero $D01F value means the sprite with that bit set touched a solid
552
+ foreground pixel.
553
+
554
+ ### Why it works
555
+
556
+ The VIC-II's sprite renderer maintains two internal shift registers per sprite:
557
+ one for the sprite's own pixels and one tracking whether any other sprite pixel
558
+ was active at the same screen position. When both are nonzero on the same clock,
559
+ the sprite-sprite latch fires. The sprite-background latch fires when a sprite
560
+ pixel is nonzero at the same position as a non-transparent background pixel from
561
+ the display data.
562
+
563
+ The latch is set by the chip during the *raster scan* — before the CPU ever sees
564
+ the result. This means the hardware has already resolved sub-pixel-exact
565
+ rectangular overlap by the time the CPU reads the register at end-of-frame.
566
+
567
+ The read-to-clear mechanic is a hardware simplification: there is no separate
568
+ write-clear path. The register's internal flip-flops reset on the read cycle.
569
+
570
+ ### The "sticky" problem
571
+
572
+ A common pitfall: if you read $D01E inside an interrupt handler *and* again in
573
+ your main loop, the second read sees zeros because the interrupt already cleared
574
+ it. Read each register *once* per frame and cache the value in a RAM variable.
575
+
576
+ A second pitfall: the registers report which sprites were involved, not which
577
+ specific pair collided. With sprites A, B, C all overlapping, all three bits
578
+ are set. Distinguishing game-relevant pairs requires software reasoning on
579
+ the bit pattern.
580
+
581
+ ### Variations
582
+
583
+ **IRQ-driven collision:** Enable the sprite-sprite or sprite-background
584
+ collision IRQ via bits 1–2 of $D01A (IRQMSK). The VIC fires the CPU IRQ line
585
+ on the same cycle the collision is latched. This gives sub-frame-latency
586
+ collision response, useful for precise physics reactions. Acknowledge by writing
587
+ the corresponding bit in $D019.
588
+
589
+ **Multicolor sprite collision:** Multicolor sprites have transparent pixels
590
+ between their double-wide colored pixels (the %00 pattern bits are transparent).
591
+ The hardware collision test correctly ignores transparent pixels, so collision
592
+ boundaries track visual content rather than bounding boxes.
593
+
594
+ ### Recipes
595
+
596
+ - `recipes/oscar64/simple-shmup.md` (reads $D01E/$D01F each frame). An earlier
597
+ version of this list pointed at `recipes/oscar64/sprite-multiplex-8.md`,
598
+ which never reads the collision registers.
599
+ - `recipes/kickassembler/sprite-priority-classes.md` clears both registers, lets
600
+ two frames of a still picture latch, reads each once and compares with an
601
+ expectation; it measures that $D01F follows the playfield's bit pattern (pair
602
+ 01 in multicolour text latches nothing) and ignores $D01B.
603
+
604
+ ---
605
+
606
+ ## sprite_y_stretch_glitch — Y-expand glitch (sprite crunch)
607
+
608
+ **Complexity:** high
609
+ **Region:** both
610
+ **Uses registers:** D017
611
+ **Uses kernal:** (none)
612
+ **Demands:** midframe_raster_irqs
613
+ **Requires:** stable_raster_irq
614
+
615
+ ### Why
616
+
617
+ The VIC-II's Y-expansion mechanism contains a documented timing quirk: if a
618
+ sprite's $D017 bit is cleared on one particular cycle of one of its display
619
+ lines while its DMA is running, the sprite's remaining length changes — once,
620
+ by a data-dependent amount — and the sprite then ends on its own. Demoscene
621
+ coders call this the "sprite crunch"; the per-line variant that is meant to
622
+ hold a sprite on one row is the "Y stretch". An earlier version of this section
623
+ said a single clear at line Y−1 makes "the sprite's row counter stall,
624
+ repeating one or more rows indefinitely until the register is written again".
625
+ Measured in VICE x64sc (PAL), that is wrong on both counts: a clear at Y−1, or
626
+ anywhere on the first display line, simply gives a plain unexpanded 21-line
627
+ sprite, and a crunching clear on a later line lengthened the sprite by 4, 16 or
628
+ 21 lines and then let it finish well before $D017 was touched again. Nothing
629
+ is left "stuck" waiting for a re-write.
630
+
631
+ ### How
632
+
633
+ The VIC-II's Y expansion works via a per-sprite flip-flop — the "advance line"
634
+ flip-flop in Bauer's VIC-II article (older editions and the VICE source call it
635
+ the expansion flip-flop, `exp_flop`). It is held set while the sprite's $D017
636
+ bit is clear; while the bit is set and the sprite's DMA is on, it is inverted
637
+ in cycle 56 of every line. In cycle 16 of the next line, only if the flip-flop
638
+ is set, the 6-bit sprite data counter base MCBASE is loaded from the data
639
+ counter MC — which has advanced by 3 during that line's fetches — so the sprite
640
+ moves on to its next 3-byte row; if the flip-flop is clear, MCBASE stays and
641
+ the row is fetched again. MCBASE is a counter, not the toggle, and neither it
642
+ nor the flip-flop is documented in the Commodore 64 Programmer's Reference
643
+ Guide; the source is Bauer's article and the VICE emulator source. (An earlier
644
+ version of this paragraph called the toggle itself "MCBASE" and attributed the
645
+ name to the Programmer's Reference Guide; `docs/pitfalls/sprite.md` still uses
646
+ that wording.)
647
+
648
+ To crunch a sprite:
649
+
650
+ 1. Enable Y expansion on the target sprite ($D017 bit n set) and let it start
651
+ displaying.
652
+ 2. On one of the sprite's display lines *after the first*, clear bit n in
653
+ $D017 so that the STA's write cycle lands on the crunch cycle. In VICE 3.10's
654
+ PAL cycle table (`viciisc` `cycle_tab_pal`) the crunch check is cycle 15,
655
+ the MCBASE advance is cycle 16, the flip-flop inversion is cycle 56 and
656
+ sprite DMA switch-on is checked in cycles 55–57. Measured: in a 260-position
657
+ sweep over lines 98–103 for a sprite at Y=100, the only write position that
658
+ crunched was cycle ~15 of the sprite's second display line.
659
+ 3. The sprite's remaining length changes once, by an amount that depends on
660
+ the row it is on: +21 lines at one row in the single-sprite sweep, +4 and
661
+ +16 lines at other rows in the eight-sprite runs. Every crunch measured here
662
+ lengthened the sprite; none shortened it. The sprite then finishes on its
663
+ own — $D017 does not have to be re-written to release it, and re-writing it
664
+ later does nothing to a sprite that has already ended.
665
+
666
+ A write at any cycle of the line before the sprite starts (Y−1), or of its
667
+ first display line, does not crunch: it yields an unexpanded 21-line sprite.
668
+ That is the write an earlier version of these steps prescribed.
669
+
670
+ The write to $D017 must happen with cycle precision on the correct raster line.
671
+ A stable raster IRQ (see `raster.md`, `stable_raster_irq`) is a prerequisite.
672
+
673
+ ### Why it works
674
+
675
+ On an ordinary display line the cycle-16 step either copies MC into MCBASE
676
+ (advance a row) or leaves MCBASE alone (repeat the row). A $D017 clear that
677
+ lands on the crunch cycle immediately before it is the one case the chip does
678
+ not handle cleanly: the cycle-16 step then loads MCBASE with a bitwise blend of
679
+ the old MCBASE and the current MC rather than either value (the formula is in
680
+ Bauer's article and the VICE source; it was not derived here — rung 4 for the
681
+ formula, rung 1 for the effect). Because the blend depends on the two counter
682
+ values at that instant, the number of rows the sprite has left afterwards
683
+ depends on which row it was on, which is why the measured change was +21 at
684
+ one row and +4 or +16 at others. From the next line on, MC and MCBASE advance
685
+ normally again, so the effect is one-shot and the sprite ends by itself when
686
+ MCBASE reaches 63. An earlier version of this section described an
687
+ "inconsistent state" that "freezes" the row counter and placed the write "in
688
+ cycles 55–56 of the preceding line"; neither matched the measurement, and its
689
+ sentence about the 8565 being "more forgiving" is dropped as unverifiable on
690
+ this machine: every run was on one model, VICE's default C64C (VIC-II 8565),
691
+ and none was compared with a 6569. An earlier version of this sentence said
692
+ VICE was run as a 6569 only.
693
+
694
+ ### Variations
695
+
696
+ **Y stretch (unverified):** The demoscene "stretcher" — clearing and re-setting
697
+ the sprite's $D017 bit every line around cycle 55 so that the sprite repeats
698
+ one row for as long as the toggling continues, reaching the full display
699
+ height for waterfall and flag effects — is a widely described technique, but
700
+ no instrument run on this machine produced a clean stalled row from a per-line
701
+ clear+set (four write phases were tried; all gave irregular, data-dependent
702
+ rows). Treat it as an unverified demoscene technique pending a VICE-verified
703
+ recipe. An earlier version of this section stated it as fact, together with a
704
+ "partial stretch" heat-shimmer variant and a "crunch from line 50 to line 250"
705
+ full-screen sprite; those were not measured.
706
+
707
+ **Crunch with multicolor:** The crunch is a row-counter effect and does not
708
+ depend on the horizontal mode, so it applies to multicolor sprites as well
709
+ (not separately measured here). An earlier version of this paragraph claimed a
710
+ "12×tall result", which meant nothing.
711
+
712
+ ### Cycle budget
713
+
714
+ The crunching write must land on one specific cycle — cycle 15 of the chosen
715
+ display line in VICE's PAL numbering — so the tolerance is a single cycle, and
716
+ the write has to be on one of the sprite's own display lines after the first,
717
+ not the line before it. Place the STA absolute (4 cycles; the write is its last
718
+ cycle) with a stable raster IRQ (see `raster.md`) whose entry-to-STA cost you
719
+ have counted, padded with NOPs to the cycle. The effect is one-shot, so no
720
+ further writes are needed on the lines that follow. An earlier version of this
721
+ section gave a "2-cycle window at cycles 55–56 of the preceding line" and a
722
+ "15 cycles total" budget; both are withdrawn (the sweep found no crunching
723
+ position on the preceding line at all).
724
+
725
+ ---
726
+
727
+ ## mob_priority — Background-priority via $D01B
728
+
729
+ **Complexity:** low
730
+ **Region:** both
731
+ **Uses registers:** D01B
732
+ **Uses kernal:** (none)
733
+
734
+ ### Why
735
+
736
+ By default, VIC-II sprites render on top of everything — characters, bitmap
737
+ pixels, even other sprites of higher index. For many game effects you want
738
+ a sprite to appear *behind* the playfield: a character walking behind a tree,
739
+ an enemy partially obscured by a wall tile, a shadow below a platform. Without
740
+ hardware priority, you would need to composite the sprite manually into the
741
+ screen data, which is expensive. $D01B provides per-sprite priority control at
742
+ zero CPU cost during rendering.
743
+
744
+ ### How
745
+
746
+ $D01B (SPBGPR) is an 8-bit register where bit n controls sprite n's foreground
747
+ priority:
748
+
749
+ - Bit n = 0 (default): sprite n renders in front of all foreground pixels.
750
+ - Bit n = 1: sprite n renders *behind* foreground pixels but still in front of
751
+ background pixels. Which pixels are which is a matter of bit pattern, not
752
+ colour; see "Pixel classes" below.
753
+
754
+ To make sprite 4 appear behind solid tiles: OR bit 4 into $D01B (`$D01B |= %00010000`).
755
+ To restore it to the front: AND the complement (`$D01B &= ~%00010000`).
756
+
757
+ The write takes effect immediately for the remainder of the current frame at the
758
+ sprite's current raster position. For clean visual results, update $D01B in the
759
+ vertical blank or at least before the sprite's first rendered line.
760
+
761
+ ### Why it works
762
+
763
+ The VIC-II's output multiplexer operates in priority order during each pixel
764
+ clock. When rendering a pixel, the chip evaluates (from highest to lowest
765
+ priority): sprites 0–7 (in index order), then foreground pixels, then background.
766
+ However, if a sprite's $D01B bit is set, the chip inverts that sprite's position
767
+ in the priority stack: foreground pixels win over it, while background pixels
768
+ still lose. Background-priority sprites therefore appear to "punch through" the
769
+ sprite layer into the background layer, letting foreground pixels obscure them.
770
+
771
+ One important constraint: sprite-vs-sprite priority is **not** affected by
772
+ $D01B. Sprite 0 is always in front of sprite 1 regardless of their $D01B bits.
773
+ $D01B only modulates each sprite's relationship with the *background plane*.
774
+
775
+ **Pixel classes, measured.** The `kickassembler/sprite-priority-classes`
776
+ recipe put multicolour sprites whose columns are bit pairs 01, 10 and 11 over
777
+ cells whose rows are every playfield pattern, in standard and in multicolour
778
+ text, with the bit set and clear, and counted every pixel of the result in
779
+ VICE x64sc on PAL and NTSC. Three rules came out, and one correction:
780
+
781
+ - The sprite's own pixel class never matters. Pairs 01, 10 and 11 of a
782
+ multicolour sprite are treated alike; the only distinction on the sprite
783
+ side is drawn (any non-zero pair, or a 1 bit in hires) against transparent.
784
+ There is no mode in which only one of the sprite's colours goes behind the
785
+ playfield.
786
+ - The playfield's class decides. With the bit set, a 1 bit in standard text
787
+ and pairs 10 and 11 in multicolour text cover the sprite; a 0 bit and pairs
788
+ 00 and 01 show it. Pair 01 is background whatever colour `$D022` holds: a
789
+ sprite with its bit set is entirely visible over a cell of solid pair 01.
790
+ Multicolour bitmap follows the same pair rule (Bauer's VIC-II article,
791
+ section 3.8.2; not measured here).
792
+ - `$D01F` uses the same classes and ignores `$D01B`: the two sprites that
793
+ sat only on pair 01 latched nothing, the six on 1 bits or pairs 10 and 11
794
+ each latched their bit, set or clear.
795
+ - The order of decisions is sprite first, playfield second, and this is
796
+ where the "inverts that sprite's position in the priority stack" picture
797
+ two paragraphs up breaks down. Where sprite 4 (bit set) overlapped sprite 5
798
+ (bit clear) over foreground, the playfield showed and *neither* sprite was
799
+ drawn, although sprite 5 was drawn over the same foreground twelve pixels
800
+ away. The VIC chooses the lowest-numbered sprite with a drawn pixel, then
801
+ applies that sprite's bit; a lower-numbered sprite behind the playfield
802
+ punches a hole through every higher-numbered sprite it overlaps wherever
803
+ the playfield is foreground. `$D01E` latched both sprites all the same. An
804
+ earlier version of this section, read as a stack of layers, would have
805
+ drawn sprite 5 in front there.
806
+
807
+ A second constraint: the border is the front-most layer of the VIC-II's output
808
+ and is drawn over every sprite regardless of $D01B. A sprite that moves under
809
+ the (unopened) border disappears whether its priority bit is set or clear; it
810
+ does not become visible again. Border pixels are also not foreground for $D01F:
811
+ a sprite sitting entirely in the border latches no sprite-background collision
812
+ (measured in VICE x64sc). Only when the border has been opened (side- or
813
+ top/bottom-border tricks) are sprites drawn there, and then they are composited
814
+ against the idle-state graphics by the normal $D01B rule. An earlier version of
815
+ this paragraph said the border "counts as background" and that a
816
+ background-priority sprite "becomes fully visible again" in it; measured, both
817
+ a $D01B-set and a $D01B-clear sprite at X=0 are hidden by the border.
818
+
819
+ ### Variations
820
+
821
+ **Layered depth via mixed priority:** Sprites 0–3 with $D01B = 0 appear in front
822
+ of everything. Sprites 4–7 with $D01B bits set appear behind solid foreground
823
+ tiles. Used in platform games to give a sense of depth between character layers
824
+ and the environment.
825
+
826
+ **Dynamic priority switching:** Switch a sprite's $D01B bit mid-frame (via
827
+ raster IRQ) to create a sprite that enters a "tunnel" (goes behind tiles) and
828
+ exits (returns to front) at precise screen positions.
829
+
830
+ **Collision interaction:** $D01B does not disable $D01F (sprite-background
831
+ collision). A background-priority sprite still registers collisions with
832
+ foreground pixels even when rendered behind them.
833
+
834
+ ### Recipes
835
+
836
+ - `recipes/kickassembler/sprite-priority-classes.md` puts eight still sprites over cells of every pixel pattern in both text modes, with `$D01B` set and clear and two sprites of mixed priority overlapping, and tabulates what shows per sprite class and playfield class from the exit screenshot; `$D01E` and `$D01F` are read once and checked against a compiled-in expectation.
837
+ - `recipes/oscar64/mixed-fighters.md` sets `$D01B` per frame to put a sprite actor in front of or behind a character actor, and measures that bit pair 01 in multicolour text is background. Oscar64's `spr_set()` has no priority argument (its signature
838
+ is `spr_set(sp, show, xpos, ypos, image, color, multi, xexpand, yexpand)`);
839
+ write `vic.spr_priority` ($D01B) directly. An earlier version of this list
840
+ pointed at `recipes/oscar64/sprite-multiplex-8.md` for a "`spr_set()`
841
+ priority parameter" that does not exist.
842
+
843
+ ---
844
+
845
+ ## sprite_color_swap_mid_line — Recolor sprites within a scanline
846
+
847
+ **Complexity:** high
848
+ **Region:** both
849
+ **Uses registers:** D027, D028, D029, D02A, D02B, D02C, D02D, D02E
850
+ **Uses kernal:** (none)
851
+ **Demands:** midframe_raster_irqs
852
+ **Requires:** stable_raster_irq
853
+
854
+ ### Why
855
+
856
+ A VIC-II sprite has one individual color register ($D027–$D02E). That color
857
+ applies uniformly to every pixel of the sprite across all 21 rows. If you want
858
+ a sprite to show a color gradient — a flame that transitions from white at the
859
+ center to orange to red at the edges, for example — the single color register
860
+ is the bottleneck. The solution is to change the color register mid-frame, on
861
+ a specific raster line, while the sprite is actively rendering. This produces
862
+ a sprite with visually distinct color bands at the cost of a tightly-timed
863
+ write.
864
+
865
+ ### How
866
+
867
+ The technique requires a stable raster IRQ (see `raster.md`, `stable_raster_irq`).
868
+ The steps are:
869
+
870
+ 1. Set the sprite's color to the desired *top band* color in $D027+n before the
871
+ sprite's first row is rendered.
872
+ 2. Schedule a raster IRQ to fire on the scanline where the color change should
873
+ occur (e.g., 7 rows into the sprite's 21-row height).
874
+ 3. Inside the IRQ, write the new color to $D027+n. The VIC reads the color
875
+ register once per pixel, so the write takes effect on the *next* pixel clock
876
+ after the CPU write completes. With a stable IRQ and a cycle-counted write
877
+ placement (using NOP sled or `rirq_delay()`), the color boundary lands on a
878
+ precise horizontal position.
879
+ 4. Repeat for additional color bands.
880
+
881
+ To create a *vertical* gradient (top half one color, bottom half another),
882
+ schedule the IRQ on the correct row boundary. To create a *horizontal*
883
+ boundary (left half one color, right half another), place the write at the
884
+ correct cycle within the scanline after the sprite starts rendering.
885
+
886
+ ### Why it works
887
+
888
+ $D027–$D02E are standard memory-mapped I/O registers. The VIC-II reads the
889
+ color for sprite n once per pixel clock when sprite n's shift register is
890
+ outputting a non-transparent pixel. There is no internal color latch per sprite
891
+ that holds the value for the whole line — the chip reads the register each time.
892
+ A CPU write to $D027+n that completes partway through a horizontal scan therefore
893
+ splits the sprite's pixel output into two color regions: pixels rendered before
894
+ the write use the old color, pixels after use the new one.
895
+
896
+ The cycle precision requirement comes from the VIC-II bus timing. On a PAL
897
+ machine running at 0.985 MHz, each cycle is approximately 1 microsecond, and
898
+ one CPU cycle is eight pixels: the 40 character columns of 8 pixels are fetched
899
+ over the 40 g-access cycles 16–55. A sprite's 24 pixels are therefore 3 CPU
900
+ cycles wide, in both hires and multicolor — multicolor halves the resolution to
901
+ 12 double-width pixels, not the width — and an X-expanded sprite's 48 pixels
902
+ are 6 cycles. (An earlier version of this paragraph said 6 cycles in hires and
903
+ 12 in multicolor, which is wrong by a factor of two and inverts what multicolor
904
+ changes.) To achieve a specific horizontal split, the write must complete on
905
+ one particular CPU cycle. This demands a stable IRQ with no jitter and a fixed
906
+ cycle offset from the IRQ entry point to the STA instruction.
907
+
908
+ ### Variations
909
+
910
+ **Per-sprite gradient:** Apply independent color swap schedules to multiple
911
+ sprites, each with its own raster IRQ slot. With 8 sprites and 2 color bands
912
+ each, you can schedule 16 IRQ writes spread across the frame.
913
+
914
+ **Animated gradient:** Each frame, shift the color band assignments up or down
915
+ by one row by adjusting the IRQ trigger line. The result is a scrolling color
916
+ wash moving through the sprite.
917
+
918
+ **Multicolor sprite color swap:** In multicolor mode, the sprite has three
919
+ color registers: the individual color ($D027+n) and two shared colors ($D025,
920
+ $D026). All three can be swapped mid-line. Changing $D025 mid-frame affects
921
+ every multicolor sprite simultaneously — useful for palette flashes but
922
+ destructive if sprites need independent colors.
923
+
924
+ **Combined with Y-expand glitch:** A color swap on a Y-crunched sprite produces
925
+ banded gradients on tall stretched sprites — flame and waterfall effects. This
926
+ depends on the Y stretch, which `sprite_y_stretch_glitch` now marks as
927
+ unverified on this machine.
928
+
929
+ ### Cycle budget
930
+
931
+ On PAL (VICE 3.10 x64sc, default C64C model, VIC-II 8565; an earlier version
932
+ said 6569) a write to $D027+n that completes on CPU cycle
933
+ c — cycles numbered 1–63, the numbering in which the CSEL side-border pulse
934
+ lands on cycle 56 — takes effect from sprite X ≈ 8c − 111. So a write on cycle
935
+ 16 recolours a sprite at X=24 (the left edge of the display window) from its
936
+ first pixel, cycle 18 splits it at X=33, and cycle 34 splits a sprite at X=152
937
+ at X=161. Equivalently, the STA's write cycle must be ≈ 16 + (X_split − 24)/8;
938
+ subtract your stable IRQ's entry-to-STA cost to get the delay. Use
939
+ `rirq_delay()` (5 cycles per unit) plus NOP padding (2 cycles) for sub-5-cycle
940
+ alignment. An earlier version of this section placed a sprite at X 24–47 "during
941
+ approximately CPU cycles 30–35" and asked for a "30-cycle delay for the left
942
+ edge, 33 for a midpoint"; cycles 30–35 are where a sprite at X ≈ 128–175 is
943
+ drawn, not one at X=24. The whole swap still fits within one raster line: STA 4
944
+ cycles plus whatever your IRQ entry and exit cost (see the `sprite_multiplex_8`
945
+ cycle budget for the KERNAL-vector figures).
946
+
947
+ ### Recipes
948
+
949
+ - `recipes/oscar64/stable-raster-irq.md` and `recipes/oscar64/raster-bars.md`
950
+ (`rirq_write` patterns). No recipe currently demonstrates `rirq_delay()`; it
951
+ is documented only in `docs/toolchains/oscar64-headers-reference.md`. An
952
+ earlier version of this list pointed at `recipes/oscar64/sprite-multiplex-8.md`,
953
+ which uses neither call.
954
+
955
+ ---
956
+
957
+ ## sprite_sine_chain — Eight sprites phased along one sine table
958
+
959
+ **Complexity:** low
960
+ **Region:** both
961
+ **Uses registers:** D000, D001, D002, D003, D004, D005, D006, D007, D008, D009, D00A, D00B, D00C, D00D, D00E, D00F, D010, D012, D015, D017, D01D
962
+ **Uses kernal:** (none)
963
+ **Cost:** cycles_per_frame=200, bytes_data=512, irq_slots=1
964
+ **Cost basis:** estimated
965
+ **Cost measured on:** kickassembler-sprite-sine-chain (eight sprites; not timed)
966
+ **Claims:** sprite_0-7 (owns)
967
+ **Claims basis:** derived-listing
968
+
969
+ ### Why
970
+
971
+ The middle band of a cracktro (`docs/demo-design/intro-cracktro-patterns.md`,
972
+ the Sprite-Chain Animation subsection of "Crack Intro") is eight sprites moving as one ribbon, chain or bouncing logo.
973
+ All eight hardware sprites are on at once and none is reused inside the
974
+ frame, so the whole effect is one table lookup per sprite per frame and a
975
+ single block of register writes in the vertical blank. It needs no raster
976
+ interrupt inside the display.
977
+
978
+ This is not `sprite_multiplex_8`. A multiplexer re-arms the eight sprites
979
+ between raster bands and demands `midframe_raster_irqs` and
980
+ `changes_sprite_set`; the chain keeps a constant sprite set and writes the
981
+ registers once a frame. `intro-cracktro-patterns.md` named
982
+ `sprite_multiplex_8` for the chain in an earlier version of its Sprite-Chain Animation subsection (it names `sprite_sine_chain` now) and its technique
983
+ checklist; that is the wrong name, and a compatibility check run with it
984
+ reports conflicts the chain does not have. `cracktro-template.md` does not
985
+ drive sprites at all: its sine table moves the scroller.
986
+
987
+ ### How
988
+
989
+ One 256-entry sine table, built by the assembler (`table_generation` in
990
+ `docs/techniques/cpu-cycle-tricks.md`), serves all eight sprites. Each
991
+ frame a base index advances by `STRIDE` steps; sprite `n` reads the table at
992
+ `base + n * PHASE`. `STRIDE` sets the speed (2 steps per frame is one period
993
+ in 128 frames), `PHASE` sets the spacing along the curve (16 spreads the
994
+ eight over 112 steps, under half a period; 32 over 224 steps, seven-eighths
995
+ of it, so the chain nearly closes on itself). The index arithmetic is 8-bit and wraps on its
996
+ own; the table must be page-aligned so no indexed read crosses a page.
997
+
998
+ **X across the full width.** Sprite X is 9 bits: `$D000 + 2n` holds the low
999
+ byte and bit `n` of `$D010` the MSB. To sweep the whole window the table
1000
+ holds `X = round(171.5 + 171.5 * sin)`, 0 to 343, stored as two tables (low
1001
+ byte, high byte) so no run-time arithmetic is needed. Each frame the loop
1002
+ starts a mask at zero, ORs in bit `n` for every sprite whose high byte is
1003
+ non-zero, and writes the mask to `$D010` once after the loop. Writing
1004
+ `$D010` last, after all sixteen position registers, means the MSB and the
1005
+ low byte change on the same frame. The wrap from X 255 to 256 is the MSB
1006
+ setting and the low byte going to 0 in the same update; a chain whose
1007
+ positions are set per frame in the blank never shows it.
1008
+
1009
+ **Y from the same table.** A second table gives Y its own centre and
1010
+ amplitude (`Y = round(150 + 50 * sin)`, so the sprite stays inside the
1011
+ 200-line window with its 21 rows). Reading it at `index + 64`, a quarter
1012
+ period ahead of X, makes each sprite trace an ellipse and the chain a
1013
+ rotating ring; reading it at the same index gives a diagonal line;
1014
+ reading it at `2 * index` gives a figure of eight.
1015
+
1016
+ **Bounce.** Replace the Y table with a half-period table: a parabola
1017
+ (`jump_arc_table` in `docs/techniques/maths.md` builds one from 8.8
1018
+ gravity) or `|sin|`, for instance
1019
+ `round(200 - 90 * abs(sin(toRadians(i * 180 / 64))))` over 64 entries,
1020
+ played forwards and repeated. The X table stays as it is.
1021
+ A table played forwards then backwards bounces symmetrically; one played
1022
+ forwards only snaps back to the floor.
1023
+
1024
+ **Update in the blank.** Wait for a raster line below the window (the
1025
+ listing polls `$D012` for 255, which both models reach), then write all
1026
+ eight X and Y registers and `$D010`. A position written while that sprite
1027
+ is being drawn can split it between the old and the new place; one written
1028
+ below the window cannot. Then leave line 255 before polling again, or a fast update re-triggers on
1029
+ the same line.
1030
+
1031
+ ### Why it works
1032
+
1033
+ A sprite's first row is drawn on the raster line after the one numbered
1034
+ in `$D001 + 2n`, and its first column at its 9-bit X. Measured in VICE
1035
+ x64sc from the recipe's pictures: a sprite with Y 200 has its top row on
1036
+ screenshot row 185, which is line 201, and one with X 163 starts at
1037
+ screenshot column 171, which is X 163 plus the 8-pixel offset of the
1038
+ picture. Both registers are compared every line; no image data is fetched
1039
+ until a sprite starts. A constant set of eight enabled sprites costs the
1040
+ same DMA every frame (2 cycles per sprite plus 3 per group of consecutive
1041
+ sprites, on the lines where they are displayed), regardless of where the
1042
+ table puts them, so the CPU cost of the effect is the update loop alone.
1043
+
1044
+ The side borders have priority over sprites. A sprite at X 320 to 343 is
1045
+ progressively hidden under the right border and a sprite at X 0 to 23 under
1046
+ the left one; at X 343 one column shows, at X 344 none. Measured in VICE
1047
+ x64sc from the recipe's pictures: a sprite at X 343 covers screenshot
1048
+ column 351 only, and one at X 16 starts at column 32. The chain therefore
1049
+ slides off both edges without any clipping code, which is what the sweep
1050
+ across 0 to 343 is for. Sprites in the border are visible only with
1051
+ `sideborder_open`.
1052
+
1053
+ ### Variations
1054
+
1055
+ **Sprite-built logo, bounced as one object.** Set bit 0 to 7 in both
1056
+ `$D01D` and `$D017` (`sprite_expand`) so every sprite is 48 by 42. Lay
1057
+ sprites 0 to 3 across one row at X spacing 48 and sprites 4 to 7 across a
1058
+ second row 42 lines lower: a 192 by 84 pixel logo from eight 24 by 21
1059
+ images, 512 bytes of sprite data. The eight pointers at screen + `$03F8`
1060
+ are the eight tiles of the logo in reading order and never change; only
1061
+ the positions move. Each frame read one X and one Y from the tables for
1062
+ the logo's top-left corner and add `48 * (n & 3)` to X and `42 * (n >> 2)`
1063
+ to Y, with the 9-bit add producing the `$D010` bit for each sprite as
1064
+ above. Because every sprite gets the same table index the logo moves as
1065
+ one piece; a per-sprite `PHASE` of 0 is the whole difference from the
1066
+ chain. The X table must be narrowed so the right-hand tile stays on the
1067
+ picture: with a left edge of 0 to 152 the right-hand tile sits at 144 to
1068
+ 296 and its last column at 343.
1069
+ Not measured here; the arithmetic follows from the register widths and the
1070
+ recipe's measured positions.
1071
+
1072
+ **Two tables, two speeds.** Give X and Y different `STRIDE` values (read
1073
+ Y at `2 * base` or `3 * base`) for Lissajous figures. The 8-bit index
1074
+ still wraps for free.
1075
+
1076
+ **Colour cycling along the chain.** Rotate the eight colour registers
1077
+ `$D027` to `$D02E` one place every few frames so a hue runs down the
1078
+ chain. This is a register write per sprite in the same blank; no extra
1079
+ cost elsewhere.
1080
+
1081
+ ### Cycle budget
1082
+
1083
+ Rung 3, from the instruction costs of the recipe's loop: about 68 cycles
1084
+ per sprite, plus 12 for a sprite whose high byte is set, plus the
1085
+ `$D010` write and the counter, so roughly 600 cycles or ten raster lines
1086
+ per frame for all eight. It runs below the window, where the CPU is
1087
+ otherwise idle. Not measured with a timer here. Sprite DMA is unchanged
1088
+ by the effect: the eight sprites are on every frame whether or not they
1089
+ move.
1090
+
1091
+ ### Recipes
1092
+
1093
+ - `recipes/kickassembler/sprite-sine-chain.md` (the eight-sprite chain
1094
+ across the full width, MSB wrap, frame counter on screen, positions
1095
+ measured on PAL and NTSC). No logo or bounce recipe yet.
1096
+ - `recipes/kickassembler/sine-table-runtime.md` (builds the sine table on
1097
+ the machine instead of with the assembler, then drives eight sprites
1098
+ from it; the way to get the table without `.fill`)
1099
+
1100
+ ---
1101
+
1102
+ ## dypp_sprite_sine_scroller — DYPP: a text scroller of eight sprites, each column on its own sine
1103
+
1104
+ **Complexity:** medium
1105
+ **Region:** both
1106
+ **Uses registers:** D000, D001, D002, D003, D004, D005, D006, D007, D008, D009, D00A, D00B, D00C, D00D, D00E, D00F, D010, D012, D015, D017, D01B, D01C, D01D
1107
+ **Uses kernal:** (none)
1108
+ **Requires:** sprite_sine_chain
1109
+ **Cost:** cycles_per_frame=1869, cycles_per_frame_typical=1147, irq_slots=0
1110
+ **Cost basis:** measured-vice
1111
+ **Cost measured on:** kickassembler-dypp-sprite-scroller (in the vertical blank; worst frame is the constructed sum of the slowest position update, 1,154, and one character re-render, 715; typical is the update alone in 218 of 300 frames)
1112
+ **Claims:** sprite_0-7 (owns)
1113
+ **Claims basis:** derived-listing
1114
+
1115
+ ### Why
1116
+
1117
+ A scroller whose columns each ride their own wave, with no character
1118
+ buffer, no custom charset and no `$D016` fine scroll. DYPP, Different Y
1119
+ Pixel Position, does with the sprite hardware what `dycp_scroller`
1120
+ (`docs/techniques/scroll.md`) does with a charset: DYCP copies each glyph
1121
+ into a strip of character slots at the pixel row the wave gives and
1122
+ scrolls sideways through `$D016` and a ring buffer, several thousand
1123
+ cycles a frame for a 39-column band. Here a column's height is its
1124
+ sprite's Y register and its scroll is its X register, so the whole per-
1125
+ frame cost is one loop of register writes in the blank.
1126
+
1127
+ The price is fixed by the hardware: eight sprites, so eight columns at
1128
+ most, and a 24-pixel-wide sprite image, so the columns sit 48 pixels
1129
+ apart and the glyphs are doubled 16 by 16 capitals rather than a
1130
+ 40-column line of 8 by 8 text. DYCP has the columns and the small font;
1131
+ DYPP has the free vertical motion and the near-zero frame cost.
1132
+
1133
+ It is `sprite_sine_chain` with a payload. The chain phases eight
1134
+ identical images along one sine; this puts a different character in each
1135
+ sprite, moves them in a straight line horizontally with the sine on Y
1136
+ alone, re-renders a sprite's image from the message each time it leaves
1137
+ the left edge, and disables a sprite while it is in the 40-pixel gap
1138
+ between X 343 and the wrap at 384. The chain's `$D010` mask, its blank-
1139
+ line update and its measured position rules carry over unchanged.
1140
+
1141
+ ### How
1142
+
1143
+ **The render.** At start all eight 64-byte slots (`$2000 + 64k`,
1144
+ pointers `$80 + k`) are zeroed, so the sprites are blank until their
1145
+ first hand-off. A hand-off reads the eight bytes of the character's ROM
1146
+ glyph at `$D000 + code * 8` with `$01` set to `$33` (interrupts off, so
1147
+ nothing else runs while I/O is hidden), puts `$01` back to `$37`, then
1148
+ expands each glyph byte through three assembler-built tables into the
1149
+ three bytes of a sprite row and stores that row twice. Glyph row `gy`
1150
+ lands on sprite rows `2 + 2gy` and `3 + 2gy`, glyph column `gx` on sprite
1151
+ columns `4 + 2gx` and `5 + 2gx`: the 8 by 8 glyph becomes a 16 by 16
1152
+ block in rows 2 to 17 and columns 4 to 19. The recipe's `dbl(b)`
1153
+ function doubles every bit of `b` into 16 bits; byte 0 of the row is its
1154
+ top nibble, byte 1 its middle eight bits, byte 2 its bottom nibble in the
1155
+ high half. The render writes the 48 bytes of rows 2 to 17; the 15 margin
1156
+ bytes stay zero from the clear. Measured: 715 cycles, the same on every
1157
+ hand-off.
1158
+
1159
+ **The positions.** One 9-bit `p`, 0 to 383, steps down by 2 a frame.
1160
+ Sprite `k` is at `X = (p + 48k) mod 384`, kept as a 16-bit running sum:
1161
+ add 48 per column with the carry, subtract 384 once when the sum reaches
1162
+ it. The low byte goes to `$D000 + 2k`. The high byte decides three cases:
1163
+ 0, on screen with the `$D010` bit clear; 1 with a low byte under 88
1164
+ (X 256 to 343), on screen with the bit set; 1 with a low byte of 88 or
1165
+ more (X 344 to 383), disabled in `$D015` this frame, bit clear. The mask
1166
+ and the enable byte are built in two locals and written once each after
1167
+ the sixteen position registers, so low byte, MSB and enable change in the
1168
+ same update. With 48-pixel spacing and 384 for a lap, exactly one sprite
1169
+ is in the 40-pixel gap at any time, so at most seven are ever on screen.
1170
+
1171
+ **The two sines.** Only one is a sine: Y is `130 + siny[(p + 32k) &
1172
+ 255]`, `siny = round(40 sin)` as a signed byte, so each column bobs 40
1173
+ lines either side of 130 and neighbours are an eighth of a period apart.
1174
+ X is a straight line, `p` itself, which is what makes it a scroller;
1175
+ a sine on X as well is a variation below. The amplitude is well under
1176
+ 128, so no entry of `siny` reaches 256 and wraps to zero
1177
+ (`sine_table_peak_wraps_to_zero`, `docs/pitfalls/maths.md`).
1178
+
1179
+ **The wrap and the hand-off.** `X` is always even (`p` starts even and
1180
+ every step and spacing is even), so a column passes X 4 exactly once a
1181
+ lap. At X 4 the glyph's columns 4 to 19 sit at X 8 to 23, all under the
1182
+ left border, so the character can change with nothing visible. (A
1183
+ looser rule of "hand off anywhere below X 24" is realised as this single
1184
+ even X: at X 6 to 22 the glyph's right-hand columns are still inside the
1185
+ display and a re-render there would show.) The
1186
+ update loop notes which sprite is at X 4; after the registers are written
1187
+ that sprite is rendered from the next character of the message, whose
1188
+ index wraps at a terminator byte. The sprite then steps to X 2, X 0,
1189
+ wraps to 382, spends twenty frames disabled in the gap (X 382 down to
1190
+ 344) and re-enters at X 342 carrying the new character, twenty-three
1191
+ frames after the hand-off. Starting `p` at 6 makes sprite 0's
1192
+ first hand-off frame 1, and the message then streams in one character
1193
+ every 24 frames.
1194
+
1195
+ **Update in the blank.** As for the chain: poll `$D012` for 255, do the
1196
+ update and any hand-off there, and leave line 255 before polling again.
1197
+ The whole worst frame, update plus render, is about 30 lines on PAL and
1198
+ sits below the display on both models.
1199
+
1200
+ ### Why it works
1201
+
1202
+ Everything the chain measured holds: a sprite's first row is the line
1203
+ after its Y register, its first column is its 9-bit X, and the side
1204
+ borders hide it below X 24 and above X 343 (`sprite_sine_chain`, Why it
1205
+ works). The hand-off leans on the left border: a 16-pixel glyph placed
1206
+ at sprite columns 4 to 19 is entirely under it once X is 4 or less, so a
1207
+ re-render there is invisible, and the sprite is disabled through the
1208
+ right-hand gap, so the new character first appears at the right edge.
1209
+ Because `$D015`, `$D010` and all sixteen position registers are written
1210
+ below the display, no frame ever shows a sprite half-moved or a mask
1211
+ that disagrees with its low byte.
1212
+
1213
+ Measured in VICE x64sc from the recipe's pinned frame (`p = 174`): the
1214
+ six glyphs on screen have their first lit column at `X + 14` (`X + 16`
1215
+ for `I`, whose ROM glyph starts one column later) and their top lit row
1216
+ at `Y - 13` on PAL, `Y - 25` on NTSC; `$D010` reads `$0C` for the two
1217
+ sprites at X 270 and 318, and `$D015` reads `$EF` with sprite 4 at X 366
1218
+ off. The control build with the `$D010` write removed draws those two
1219
+ glyphs at X 14 and X 62, 256 pixels to the left, which is the failure
1220
+ `sprite_x_high_bit_wrong_register` describes.
1221
+
1222
+ ### Cycle budget
1223
+
1224
+ Measured with CIA 1 timer A in the recipe, both models giving identical
1225
+ logs, raw figures including a 5-cycle empty bracket:
1226
+
1227
+ - position update of eight sprites, X, Y, `$D010` and `$D015`: 1,147
1228
+ cycles in 218 of 300 frames; minimum 1,091, maximum 1,154. The spread
1229
+ is the per-column branches (the 384 subtraction, the MSB, the off
1230
+ case, the X 4 test).
1231
+ - one character re-render: 715 cycles, all thirteen hand-offs alike.
1232
+
1233
+ So a frame with a hand-off costs at most 1,869 cycles, about 30 PAL
1234
+ raster lines, all in the vertical blank, and 218 of 300 frames cost
1235
+ 1,147. Sprite DMA is the usual 2 cycles per sprite plus 3 per group on
1236
+ the lines where the sprites sit, unchanged by the effect; with the eight
1237
+ spread across 80 lines of Y and 344 of X, fewer than eight ever share a
1238
+ line.
1239
+
1240
+ ### Variations
1241
+
1242
+ **More columns by multiplexing.** `sprite_multiplex_8` re-arms sprites
1243
+ between raster bands; with the columns' Y range split into bands, a
1244
+ second row of eight can share the sprites, at the cost of the
1245
+ `midframe_raster_irqs` and `changes_sprite_set` demands the chain and
1246
+ this entry avoid. Not built here.
1247
+
1248
+ **Expanded sprites for a bigger font.** Set the sprite's bit in `$D01D`
1249
+ and `$D017` (`sprite_expand`) for a 32 by 32 glyph; the spacing must
1250
+ grow to 96 and the lap to 768, so the 9-bit position no longer wraps in
1251
+ a byte pair without a third case. Not built here.
1252
+
1253
+ **A second sine on X.** Add `sinx[(p + PHASE k) & 255]` to the column's
1254
+ X before the 384 wrap, small enough that columns cannot overtake each
1255
+ other (under 24 with 48-pixel spacing): the line of text sways as it
1256
+ scrolls. The hand-off test then has to be a range rather than `X == 4`.
1257
+ Not measured here.
1258
+
1259
+ ### Pitfalls
1260
+
1261
+ - `sprite_x_high_bit_wrong_register` (`docs/pitfalls/sprite.md`): every
1262
+ column crosses X 255 once a lap; the control build shows the 256-pixel
1263
+ jump.
1264
+ - `sprite_x_range_hidden_and_seam` (`docs/pitfalls/sprite.md`): the
1265
+ columns are deliberately parked under both borders and in the X 344 to
1266
+ 383 gap; the entry relies on the hidden range rather than being
1267
+ surprised by it.
1268
+
1269
+ ### Sources
1270
+
1271
+ - `recipes/kickassembler/dypp-sprite-scroller.md`: the measurements
1272
+ above, the pinned frame tables for PAL and NTSC, the mid-motion shot
1273
+ and the `$D010` control.
1274
+ - `sprite_sine_chain` above and `dycp_scroller` in
1275
+ `docs/techniques/scroll.md` for the two techniques this one is set
1276
+ against.
1277
+
1278
+ ### Recipes
1279
+
1280
+ - `recipes/kickassembler/dypp-sprite-scroller.md` (eight yellow doubled
1281
+ capitals, 48 apart, each on its own Y sine; hand-off at X 4; frozen at
1282
+ frame 300; CIA-timed update and render; `$D010` control build)
1283
+
1284
+ ---
1285
+
1286
+ ## sprite_border_scroller — Sprite border scroller: a text scroller of eight sprites in the opened lower border
1287
+
1288
+ **Complexity:** medium
1289
+ **Region:** both
1290
+ **Uses registers:** D000, D001, D002, D003, D004, D005, D006, D007, D008, D009, D00A, D00B, D00C, D00D, D00E, D00F, D010, D011, D012, D015, D017, D019, D01A, D01B, D01C, D01D, D027, D028, D029, D02A, D02B, D02C, D02D, D02E
1291
+ **Uses kernal:** (none)
1292
+ **Requires:** topbottom_border_open, dypp_sprite_sine_scroller
1293
+ **Demands:** midframe_raster_irqs
1294
+ **Raster band:** 20-46, 249 (the recipe's RESTORE_LINE 20 plus its measured worst frame of 1,586 cycles, about 25 lines; its OPEN_LINE 249)
1295
+ **Cost:** cycles_per_frame=1586, cycles_per_frame_typical=829, irq_slots=2, sprites_per_line=8
1296
+ **Cost basis:** measured-vice
1297
+ **Cost measured on:** kickassembler-sprite-border-scroller (both handlers' brackets summed per frame, above the display; worst frame is a real hand-off frame, typical is 182 of 300 frames)
1298
+ **Claims:** sprite_0-7 (owns), vic_raster_irq (owns)
1299
+ **Claims basis:** derived-listing
1300
+
1301
+ ### Why
1302
+
1303
+ Text that costs the main screen nothing. A scroller on the character
1304
+ screen takes a row of the forty by twenty-five and, if it is a fine
1305
+ scroller, `$D016` and a ring buffer as well; a DYPP takes the sprites
1306
+ but still lives inside the display, over whatever the screen shows. The
1307
+ lower border is a region of the frame that draws nothing but `$D020`,
1308
+ and once it is opened it is a strip of background under the display,
1309
+ below line 250, that no character cell reaches. Put the sprites there
1310
+ and the whole display is free: forty columns and twenty-five rows for
1311
+ the game or the picture, and a line of scrolling text underneath that
1312
+ touches none of it.
1313
+
1314
+ The price is the two raster interrupts that open the border each frame
1315
+ and the loss of the top border too, since the method opens both (see
1316
+ `topbottom_border_open`, raster.md: there is no bottom-only form). The
1317
+ text is limited to what eight sprites carry, so seven doubled capitals
1318
+ on screen at once, 48 pixels apart.
1319
+
1320
+ ### How
1321
+
1322
+ **The border.** Exactly `topbottom_border_open`'s recipe: a raster
1323
+ interrupt through `$0314` on line 249 reads `$D011`, clears RSEL (bit 3)
1324
+ with bit 7 masked, and writes it back; a second interrupt on line 20 of
1325
+ the next frame sets RSEL again. Line 249 is the middle of the three
1326
+ lines, 248 to 250, that recipe measured as opening with a plain KERNAL
1327
+ interrupt; line 20 is inside the window it swept (252 to 246 of the
1328
+ next frame) and is chosen so that the same handler can move the sprites
1329
+ while none of them is being drawn. `$3FFF`, the last byte of the VIC
1330
+ bank, is written to zero, because below the display the VIC is idle and
1331
+ fetches its graphics from that byte; zero draws plain background, a
1332
+ non-zero byte draws its set bits over the strip.
1333
+
1334
+ **The sprites.** Eight single-colour, unexpanded sprites, pointers
1335
+ `$80` to `$87` for eight 64-byte slots at `$2000` to `$21C0`, every Y
1336
+ register 254. Sprite Y is compared against the low byte of the raster,
1337
+ so 254 matches line 254 and the sprite's 21 rows are drawn on lines 255
1338
+ to 275: below the display, inside the opened strip, and, on PAL, inside
1339
+ the frame. On NTSC the frame ends at line 262 and the rows carry on
1340
+ through the wrap onto lines 0 to 12 of the next frame.
1341
+
1342
+ **The positions and the wrap.** As `dypp_sprite_sine_scroller`: one
1343
+ 9-bit `p`, stepped down by 2 a frame, sprite `k` at
1344
+ `X = (p + 48k) mod 384`; the low byte to `$D000 + 2k`, the `$D010` bit
1345
+ set for X 256 to 343, the sprite disabled in `$D015` for X 344 to 383.
1346
+ A sprite that reaches X 4 has its glyph, columns 4 to 19 of the image,
1347
+ at X 8 to 23 under the left border, and there it takes the next
1348
+ character of the message and is re-rendered from the character ROM into
1349
+ its slot, 2 by 2 doubling into rows 2 to 17 and columns 4 to 19. All of
1350
+ this runs in the line-20 handler, after the RSEL restore; the main loop
1351
+ is empty.
1352
+
1353
+ ### Why it works
1354
+
1355
+ The vertical border flip-flop is the only thing that hides a sprite in
1356
+ the border. While it is set the border colour is drawn over graphics
1357
+ and sprites alike; while it is clear the sequencer puts out background
1358
+ and sprites are drawn as anywhere else. The flip-flop is set only when
1359
+ the raster reaches the bottom comparison line, 251 with RSEL set or 247
1360
+ with it clear. RSEL is 1 when line 247 passes and 0 when line 251
1361
+ arrives, so neither comparison ever matches, the flip-flop stays clear
1362
+ from line 251 to the end of the frame, and a sprite at Y 254 is simply
1363
+ visible. The control build that never clears RSEL is the proof by
1364
+ absence: the same seven sprites, enabled, positioned and pointed at
1365
+ their glyphs, and zero white pixels below line 250 on either model.
1366
+
1367
+ Measured in VICE x64sc on the recipe's pinned frame (`p = 174`): white
1368
+ pixels on screenshot rows 241 to 254 on PAL (raster lines 257 to 270,
1369
+ sprite rows 2 to 15, the fourteen rows of a doubled seven-row ROM
1370
+ capital) and on rows 229 to 242 on NTSC with identical per-row pixel
1371
+ counts; six glyphs whose first lit column is `X + 14` (`X + 16` for
1372
+ `I`); `$D010` reading `$0C` for the sprites at X 270 and 318, `$D015`
1373
+ reading `$EF` with sprite 4 at X 366 off. On NTSC rows 235 to 242 of
1374
+ those are lines 0 to 7 of the following frame, so the sprite is drawn
1375
+ across the frame wrap; only sprite row 20, a blank margin row on line
1376
+ 12, falls outside the emulator's picture. Two runs per model gave
1377
+ byte-identical screenshots.
1378
+
1379
+ ### Cycle budget
1380
+
1381
+ Measured with CIA 1 timer A in the recipe, both models giving identical
1382
+ logs, raw figures with each bracket's own start and stop stores inside:
1383
+
1384
+ - the line-249 handler, the RSEL clear with the compare, vector and
1385
+ acknowledge: 39 cycles, every frame.
1386
+ - the line-20 handler, the RSEL restore plus the eight-sprite position
1387
+ update, `$D010` and `$D015`: 790 cycles in 182 of 300 frames, 734 to
1388
+ 811 without a hand-off; 1,506 to 1,547 in the thirteen frames with
1389
+ one, the render being the difference.
1390
+ - the two together: 829 cycles in 182 frames, worst 1,586, about 25 PAL
1391
+ lines from line 20, all above the display. The KERNAL's 29-cycle
1392
+ entry and the `$EA31` exit once a frame are outside the brackets, as
1393
+ is the DMA of up to eight sprites on lines 254 to 275 (all eight are
1394
+ enabled in four frames of every twenty-four, the eighth under the left
1395
+ border at X 0 to 6).
1396
+
1397
+ ### Variations
1398
+
1399
+ **The upper border.** The method opens both borders, so the same
1400
+ sprites could stand at the top instead: a Y of 4 draws from line 5 and
1401
+ again from line 261, which is the topbottom recipe's parked sprite and
1402
+ is drawn twice; a Y from 0 to 29 draws inside the open top border
1403
+ (lines 1 to 50) and, on PAL, a second time on lines 257 to 306 of the
1404
+ opened bottom strip, because Y is compared with the low eight bits of
1405
+ the raster; only the NTSC frame, ending at 262, drops the second copy
1406
+ for Y above 6. A scroller there leaves the bottom strip empty only on
1407
+ NTSC. Not built here.
1408
+
1409
+ **Expanded sprites for a taller font.** Setting the sprite's bit in
1410
+ `$D017` doubles the 21 rows to 42, lines 255 to 296 on PAL, still inside
1411
+ the frame; with `$D01D` as well the glyph is 32 by 32 and the spacing
1412
+ must grow to 96 with a lap of 768, as the DYPP entry says. On NTSC 42
1413
+ rows from line 255 reach line 34 of the next frame, into the top
1414
+ border, and the rows past line 262 depend on the set. Not measured
1415
+ here.
1416
+
1417
+ **DYPP bobbing inside the border.** `dypp_sprite_sine_scroller`'s Y sine
1418
+ fits in the strip only within a narrow band, because Y is eight bits
1419
+ and is compared with the low eight bits of the raster: on PAL the only
1420
+ Y values whose 21 rows stay below the display and match one line are
1421
+ 250 to 255 (lines 251 to 276), a bob of a few lines about 254; any
1422
+ larger bob needs Y 0 to 34 for the lower rows, and each of those also
1423
+ matches lines 0 to 34, drawing a mirror copy on lines 1 to 55 at the
1424
+ top, into the display from line 51. On NTSC only lines 251 to 262 are
1425
+ inside the frame and the rest wraps. Not built here.
1426
+
1427
+ ### Pitfalls
1428
+
1429
+ - `sprite_x_high_bit_wrong_register` (`docs/pitfalls/sprite.md`): every
1430
+ column crosses X 255 once a lap and the `$D010` bit is written per
1431
+ sprite from the high byte, in the same handler as the low byte.
1432
+ - `sprite_x_range_hidden_and_seam` (`docs/pitfalls/sprite.md`): the
1433
+ hand-off happens under the left border at X 4 and the sprite is
1434
+ parked disabled through X 344 to 383; the entry uses the hidden range
1435
+ on purpose.
1436
+ - `d012_wrap_around` (`docs/pitfalls/raster-and-badline.md`): both
1437
+ handlers read `$D011` and write it back, so bit 7, the raster's ninth
1438
+ bit on a read, is masked off; the topbottom recipe explains why a
1439
+ handler that forgets moves its compare above line 255.
1440
+
1441
+ ### Sources
1442
+
1443
+ - `recipes/kickassembler/sprite-border-scroller.md`: the measurements
1444
+ above, the pinned register and row tables for PAL and NTSC, the
1445
+ mid-motion description, the cycle logs and the `NOOPEN` control.
1446
+ - `topbottom_border_open` in `docs/techniques/raster.md` and its recipe
1447
+ for the flip-flop rules, the 248-to-250 window and `$3FFF`;
1448
+ `dypp_sprite_sine_scroller` above for the render and the position
1449
+ rule this entry reuses.
1450
+
1451
+ ### Recipes
1452
+
1453
+ - `recipes/kickassembler/sprite-border-scroller.md` (eight white doubled
1454
+ capitals at Y 254 in the opened lower border, 48 apart; the border
1455
+ opened on line 249 and restored on line 20 with the update in the same
1456
+ handler; hand-off at X 4; frozen at frame 300; CIA-timed handlers;
1457
+ `NOOPEN` control build)
1458
+
1459
+ ---
1460
+
1461
+ ## sprite_stretcher_d017 — Sprite stretcher: rows repeated by toggling the Y-expand bit every line
1462
+
1463
+ **Complexity:** high
1464
+ **Region:** both
1465
+ **Uses registers:** D000, D001, D010, D011, D012, D015, D017, D019, D01A, D01B, D01C, D01D, D021, D027, DC04, DC05, DC0D, DC0E
1466
+ **Uses kernal:** (none)
1467
+ **Requires:** stable_raster_irq
1468
+ **Demands:** midframe_raster_irqs, cpu_every_line
1469
+ **Raster band:** 97-181 (the recipe's first IRQ on line 97, its sync on 99, the toggled lines 100 to 180, the exit on 181)
1470
+ **Cost:** cycles_per_frame=5094, cycles_per_line=63, lines_active=85, irq_slots=2, sprites_per_line=1, bytes_code=2628, bytes_data=67
1471
+ **Cost basis:** measured-vice
1472
+ **Cost measured on:** kickassembler-sprite-stretcher (CIA1 timer A over the toggled lines 100 to 181, screen on, one sprite; the double-IRQ entry on 97 to 99 is outside the bracket; the bytes are the `-showmem` blocks, code $0900-$1343 and the sprite plus three result bytes)
1473
+ **Claims:** sprite_0 (owns), vic_raster_irq (owns)
1474
+ **Claims basis:** derived-listing
1475
+
1476
+ ### Why
1477
+
1478
+ A sprite made taller than its 21 rows without touching its data. The
1479
+ classic use is the tall logo or the waterfall: one sprite's rows are
1480
+ repeated down the screen for as long as the CPU keeps toggling, so a
1481
+ 24-pixel-wide column of any height costs 63 bytes of sprite data and no
1482
+ redraw. The trick is old, widely described and, in this knowledge base,
1483
+ was marked unverified: `sprite_y_stretch_glitch` above says that its
1484
+ per-line clear+set attempts gave irregular rows. This entry is the
1485
+ measurement that closes that. It is reproduced in VICE 3.10, with a
1486
+ sharp edge: the setting write must complete in or before cycle 55 of the
1487
+ line, the last CPU write cycle before the sprite's own DMA stalls the
1488
+ 6510, and one cycle later the write is pushed to cycle 61 and gives
1489
+ nothing.
1490
+
1491
+ ### How
1492
+
1493
+ 1. Put the sprite up as usual (position, colour, pointer, $D015 bit),
1494
+ with its $D017 bit clear.
1495
+ 2. Take a stable raster interrupt on the line before the first line you
1496
+ want stretched (`stable_raster_irq`; the recipe uses the double-IRQ
1497
+ entry of `stable-raster-irq.md` on line 99 for a sprite at Y 100).
1498
+ 3. From there, run straight-line code for the whole region, one block
1499
+ per line, each exactly one line long. In each block, clear the
1500
+ sprite's $D017 bit with a store whose write cycle lands on C-4 and set
1501
+ it again with a store landing on C, where C is at most 55 (measured)
1502
+ and at least 17 (arithmetic from the cycle-16 row advance; nothing
1503
+ below C=48 was built); the recipe measures C from 48 to 55 as one
1504
+ plateau and pins 52. Pad the
1505
+ rest of the line with reads. The sprite's own DMA stops the CPU from
1506
+ cycle 55 to 59 on every line it is displayed, so a 58-cycle block is a
1507
+ 63-cycle line; the recipe's `POST` constant is that arithmetic.
1508
+ 4. On a badline the CPU is stopped from cycle 12 to 59 and no write can
1509
+ land in the window; the recipe emits a 15-cycle block there and lets
1510
+ the row advance once. To hold one row across the region the badlines
1511
+ have to be moved out of it (`fld_flexible_line_distance`) or the
1512
+ sprite put where there are none; the recipe does not do this.
1513
+ 5. On the line after the last toggled one, write the bit clear early in
1514
+ the line and the sprite finishes its remaining rows unexpanded.
1515
+
1516
+ Measured (recipe, PAL, VICE 3.10): with C from 48 to 55 the sprite is 91
1517
+ lines tall for 81 toggled lines: row 0 on five lines, rows 1 to 9 on
1518
+ eight lines each, row 10 on four, rows 11 to 20 once. C=56 gives 24
1519
+ lines, only the three rows after a badline doubled. C from 57 to 62
1520
+ gives heights of 44 to 94 with rows skipped and, at 60, the sprite
1521
+ wrapping through its data twice: the crunch of `sprite_y_stretch_glitch`,
1522
+ reached because those builds' code runs longer than the line and the
1523
+ writes drift into cycle 15 of the next one. The `NOSTRETCH` control,
1524
+ identical code with the two stores aimed at a RAM byte, is 21 lines.
1525
+ Three `-define NTSC` builds (C 48, 52, 55) give the same 91 lines on the
1526
+ 6567R8; the entry's Region rests on those three builds and not on the
1527
+ recipe's pinned PRG, which is PAL-timed and on NTSC gives a 113-line
1528
+ crunch, not a stretch (the recipe's region is therefore `pal`).
1529
+
1530
+ ### Why it works
1531
+
1532
+ The expansion flip-flop and the counters are described under
1533
+ `sprite_y_stretch_glitch` above (Bauer's article and the VICE source).
1534
+ The audited hardware page names the flip-flop but not its cycle:
1535
+ `hardware/vic-ii-reference.md`, Expansion, says that on the Y axis "the
1536
+ chip uses an internal 'expansion flip-flop' that toggles each line", and
1537
+ that changing $D017 mid-line "can confuse the flip-flop and cause
1538
+ 'sprite crunch'". The rule that matters here is the pair: while the bit
1539
+ is clear the flip-flop is held set; while the bit is set and the
1540
+ sprite's DMA is on it is inverted in cycle 56 (VICE 3.10's PAL cycle
1541
+ table, as cited under `sprite_y_stretch_glitch`); and in cycle 16 of the
1542
+ next line the row counter base moves on only if the flip-flop is set.
1543
+ The clear on C-4 forces it set, the set on C hands it to the inversion,
1544
+ cycle 56 clears it, and cycle 16 repeats the row. The sweep confirms the
1545
+ placement from the CPU side: C=55 is the last write cycle that can
1546
+ complete before the 6510 is stopped by the sprite's DMA (BA low from
1547
+ 55; a write proceeds under BA low, the read after it does not), and
1548
+ C=56 is pushed to cycle 61 and does not stretch. What is measured is
1549
+ the CPU write cycle against the DMA stop; the inversion's own cycle is
1550
+ taken from the cycle table, and the data are consistent with any
1551
+ inversion cycle from 56 to 61.
1552
+
1553
+ The C=56 column is the mechanism seen from the other side: its setting
1554
+ write lands after the inversion, so on ordinary lines the bit is clear
1555
+ at cycle 56, nothing is inverted, and the row advances; only after a
1556
+ badline, where no clear was written and the bit stayed set across cycle
1557
+ 56, is a row doubled.
1558
+
1559
+ ### Cycle budget
1560
+
1561
+ Measured in the recipe's pinned PAL run: 5,094 cycles between the timer
1562
+ start on line 100 and its read on line 181 (arithmetic for that bracket
1563
+ 5,097; the three counts are the CIA's start latency, not separately
1564
+ measured). Per line the CPU executes 58 cycles of its own code, eight of
1565
+ them the two stores, and is stopped for five by the sprite's DMA; on the
1566
+ ten badlines of the region it executes 15 and is stopped for 48. The
1567
+ technique owns every cycle of every line it covers: with `cpu_every_line`
1568
+ over an 85-line band it leaves nothing on those lines for anything else,
1569
+ and nothing on the other lines is touched. The write window is wide (55
1570
+ at the top, measured; 17 at the bottom, by arithmetic, with nothing
1571
+ below 48 built), so the entry's stability matters more than its exact
1572
+ phase: the
1573
+ recipe's SYNC_PAD is the stable-raster recipe's 11, and a one-cycle
1574
+ error would move C by one inside the plateau.
1575
+
1576
+ ### Variations
1577
+
1578
+ **A toggle table.** The recipe's blocks are generated by a `.for` with a
1579
+ constant decision per line; make that decision a byte per line (toggle,
1580
+ or pad the same 58 cycles and do not) and the sprite's shape becomes a
1581
+ table: N toggled lines then one plain line draws each row N+1 tall, a
1582
+ run of plain lines draws the rows at their natural height, and the table
1583
+ can be rewritten between frames. The badline positions are fixed by
1584
+ YSCROLL and have to be in the table as plain lines unless the region is
1585
+ kept clear of them. Not built here.
1586
+
1587
+ **All eight sprites.** $D017 is one byte for eight flip-flops, so the two
1588
+ stores cost the same for eight sprites as for one; the DMA stop grows to
1589
+ 19 cycles (`sprites_per_line`, `hardware/vic-ii-reference.md`), which
1590
+ moves the padding arithmetic and closes more of the window. Not
1591
+ measured; with eight sprites `sprite_dma_overflow` applies.
1592
+
1593
+ **One row for the whole region.** Move the badlines out with
1594
+ `fld_flexible_line_distance` (an extra $D011 write per line, inside the
1595
+ same block) or run the stretch below the display with the lower border
1596
+ opened (`topbottom_border_open`), where there are no badlines. Not
1597
+ built here; the pinned picture is the eight-line staircase.
1598
+
1599
+ ### Pitfalls
1600
+
1601
+ - `raster_irq_first_line_jitter` (`docs/pitfalls/raster-and-badline.md`):
1602
+ the whole effect is a write on one cycle of every line, and the plain
1603
+ raster IRQ's 0-to-6 cycle entry jitter is larger than the distance
1604
+ from the pinned C=52 to the edge at 55; the double IRQ is what makes
1605
+ the write cycle a constant.
1606
+ - `badline_cycle_loss` (`docs/pitfalls/raster-and-badline.md`): the CPU
1607
+ is stopped for 48 of the badline's 63 cycles once the sprite's DMA
1608
+ follows the character fetch, no write can land in the window, and the
1609
+ row advances there. In the sweep every plateau build shows it as the
1610
+ eight-line step; a block that did not allow for the stall would slip
1611
+ the rest of the region by 43 cycles.
1612
+ - `sprite_y_expand_double_register_write` (`docs/pitfalls/sprite.md`):
1613
+ the same two writes one line too late, or with the setting write
1614
+ pushed past the sprite's DMA stall so that it lands after cycle 56,
1615
+ are the crunch; the C=57 to C=62 rows of the recipe's table are what
1616
+ that looks like.
1617
+
1618
+ ### Sources
1619
+
1620
+ - `recipes/kickassembler/sprite-stretcher.md`: the fifteen-build sweep,
1621
+ the `CALIB` calibration of the write cycle against the sprite's DMA
1622
+ stop, the control, the NTSC builds and the CIA timer figure.
1623
+ - `sprite_y_stretch_glitch` above, for the flip-flop, MCBASE and the
1624
+ crunch cycle; `stable_raster_irq` in `docs/techniques/raster.md` and
1625
+ its recipe for the entry this reuses.
1626
+
1627
+ ### Recipes
1628
+
1629
+ - `recipes/kickassembler/sprite-stretcher.md` (one white sprite with
1630
+ numbered rows at X 160, Y 100; clear on C-4 and set on C every line
1631
+ from 100 to 180 with C=52 pinned; 91 lines tall; `C48` to `C62`,
1632
+ `NOSTRETCH`, `CALIB` and `NTSC` builds)
1633
+
1634
+ ---
1635
+
1636
+ ## sprite_cache_flip — Sprite cache: frames depacked and mirrored on demand
1637
+
1638
+ **Complexity:** medium
1639
+ **Region:** both
1640
+ **Uses registers:** D01C
1641
+ **Uses kernal:** (none)
1642
+ **Requires:** table_generation
1643
+ **Cost:** cycles_per_frame=2672, cycles_per_frame_typical=20, bytes_data=1024
1644
+ **Cost basis:** arithmetic
1645
+ **Cost measured on:** kickassembler-sprite-cache-flip (one cache miss, screen blanked)
1646
+
1647
+ ### Why
1648
+
1649
+ A 16 KB VIC bank holds 256 sprite blocks of 64 bytes, fewer once the
1650
+ screen, a charset and code take their share. A game whose hero and
1651
+ enemies face both ways needs every frame twice. The fix is to store each
1652
+ frame once, facing right and packed, outside the VIC bank, and to depack
1653
+ it into a small cache of sprite blocks inside the bank only when it is
1654
+ about to be shown, mirroring it on the way when the object faces left.
1655
+ Metal Warrior 4 stores its frames facing right only and mirrors them at
1656
+ load time, so 114 stored frames become about 170; Hessian caches 64 frames at $D000 to $DFFF, under the I/O area
1657
+ (from Cadaver's articles, not measured here).
1658
+
1659
+ ### How
1660
+
1661
+ **Mirroring a hires row.** A row is 3 bytes, 24 pixels, bit 7 of byte 0
1662
+ the leftmost. The mirrored row is `T[b2], T[b1], T[b0]`: the three bytes
1663
+ in reverse order, each with its 8 bits reversed. `T` is a 256-byte table
1664
+ (HFLIP), so a row costs three indexed loads.
1665
+
1666
+ **Mirroring a multicolour row.** A multicolour pixel is a bit pair, four
1667
+ to a byte. The mirror reverses the order of the pairs and keeps the bit
1668
+ order inside each pair (MFLIP): `%aabbccdd` becomes `%ddccbbaa`. The
1669
+ bytes are swapped as for hires. The hires table is wrong here: reversing
1670
+ all 8 bits also reverses each pair, so %01 (colour from $D025) and %10
1671
+ (the sprite's own colour) change places while %00 and %11 do not. The
1672
+ shape comes out mirrored and the colours wrong. Measured in the recipe
1673
+ with the hires table forced on the multicolour frames: the same screen
1674
+ pixels are set, and 136 of the 156 the fish covers change colour.
1675
+
1676
+ **flip(flip(x)) = x does not catch the wrong table.** Both tables are
1677
+ their own inverse, so a round trip through the wrong one passes. The
1678
+ recipe's negative run showed it: 0 round-trip errors, 62 mirrored bytes
1679
+ wrong.
1680
+ Check a mirrored frame against a mirror made another way (the recipe has
1681
+ the assembler reverse the pixel strings of each frame).
1682
+
1683
+ **Build the tables at start.** Each is 256 bytes of loop output, so
1684
+ there is no reason to ship them: HFLIP takes 28,161 cycles and MFLIP
1685
+ 34,817, under two PAL frames each, once (measured, below). Page-align
1686
+ both so `lda table,x` never adds a page-cross cycle. `table_generation`
1687
+ in `techniques/cpu-cycle-tricks.md` covers the general case.
1688
+
1689
+ **Packing.** The recipe stores a 21-bit mask of the rows that are not
1690
+ empty, then 3 bytes for each such row. Depacking and mirroring are one
1691
+ pass: an empty row is three stores of zero, a present row three loads
1692
+ and three stores, plus three table lookups when mirrored. Cadaver packs
1693
+ into 6 slices of 7 bytes with one presence bit per slice (from his
1694
+ article, not measured here). Any scheme that decodes row by row can
1695
+ mirror as it goes.
1696
+
1697
+ **The cache.** A slot is one 64-byte block inside the VIC bank, and the
1698
+ sprite pointer for it is its offset in the bank divided by 64. Two small
1699
+ tables map both ways: `slot_of[key]` ($FF when not cached) and
1700
+ `key_of[slot]` ($FF when free), where `key = frame * 2 + facing`. A
1701
+ request looks up `slot_of`; a hit costs 20 cycles. A miss takes the next
1702
+ slot, clears `slot_of` for the key that slot held, depacks into the
1703
+ slot and records the new key. Forgetting to clear the old key is the
1704
+ classic bug: its lookup then returns a slot that now holds another
1705
+ frame.
1706
+
1707
+ **Eviction.** The recipe takes slots round-robin and has no protection,
1708
+ which is safe only because it requests each displayed frame once and
1709
+ has no more frames on screen than slots. A game must never evict a
1710
+ frame that is on screen now or queued for the next frame. Cadaver's
1711
+ c64gameframework stamps each slot with the frame counter when it is
1712
+ used and skips slots stamped this frame or the last one, continuing the
1713
+ search from where the last one stopped (read from its `sprite.s`, not
1714
+ run here). Size the cache above the most distinct frames that can be on
1715
+ screen in two frames, or the search finds no free slot.
1716
+
1717
+ **When to depack.** In the main loop, before the frame is built, never
1718
+ in a raster IRQ. A fill costs 2,100 to 2,700 cycles, 33 to 43 PAL lines
1719
+ (arithmetic from the measured figures at 63 cycles a line), far more
1720
+ than a multiplexer IRQ has to spare. Request every frame the next
1721
+ display needs, store the pointer each request returns in the object's
1722
+ shadow pointer, and let the IRQ copy pointers only. A slot overwritten
1723
+ while the VIC is fetching it can show parts of both frames for one frame
1724
+ (not measured here).
1725
+
1726
+ **Cache under I/O.** Placing the cache at $D000 to $DFFF in VIC bank 3
1727
+ uses RAM the CPU cannot see without banking out I/O through $01, so the
1728
+ fill must run with interrupts off around the bank switch (Cadaver's
1729
+ MW4 article, not measured here). See `ram_under_rom_traps` in
1730
+ `pitfalls/banking.md`.
1731
+
1732
+ ### Why it works
1733
+
1734
+ The VIC fetches sprite data through the pointer each line the sprite is
1735
+ displayed, so a pointer change or a new block takes effect on the next
1736
+ fetch; nothing is copied at display time. A horizontal mirror reverses
1737
+ the pixel order of each row, and a row's pixel order runs from bit 7 of
1738
+ byte 0 to bit 0 of byte 2 (hires) or pair by pair (multicolour), which
1739
+ is exactly what the byte swap and the table undo. The recipe checks the
1740
+ result three ways: every table entry against a table the assembler
1741
+ computed, every mirrored frame against the assembler's string-reversed
1742
+ frame, and the exit screenshot, where each left-facing sprite is the
1743
+ pixel mirror of its right-facing neighbour on PAL and NTSC.
1744
+
1745
+ ### Cycle budget
1746
+
1747
+ Measured in VICE x64sc 3.10 with CIA2 timers, screen blanked, IRQs off,
1748
+ the cost of an empty timed call subtracted. Identical on PAL and NTSC.
1749
+
1750
+ | Operation | Cycles |
1751
+ |---|---|
1752
+ | Build HFLIP, 256 entries | 28,161 |
1753
+ | Build MFLIP, 256 entries | 34,817 |
1754
+ | Mirror one 63-byte sprite into another buffer, either table | 1,597 |
1755
+ | Cache miss, 21 rows present, facing right | 2,441 |
1756
+ | Cache miss, same frame, facing left | 2,672 |
1757
+ | Cache miss, 15 of 21 rows present, right / left | 2,099 / 2,264 |
1758
+ | Cache hit | 20 |
1759
+
1760
+ Mirroring during the fill costs 11 cycles a row over a plain copy (231
1761
+ for 21 rows), so a cache that mirrors costs little more than one that
1762
+ does not, and saves 64 bytes of storage per mirrored frame. The miss
1763
+ figures include the lookup and the eviction bookkeeping. Moving the
1764
+ mirror loop so its branch crossed a page raised the 1,597 to 1,617; the
1765
+ recipe page-aligns its inner loops so the figures do not move as code
1766
+ grows. The Cost line's `bytes_data` is arithmetic from the table and slot
1767
+ sizes (256 + 256 + 8 x 64), run-time RAM outside the built segments; the
1768
+ `cycles_per_frame` figure is measured. The frame data is extra.
1769
+
1770
+ ### Recipes
1771
+
1772
+ - `recipes/kickassembler/sprite-cache-flip.md` (both tables built at
1773
+ start and checked, four frames depacked into an 8-slot cache facing
1774
+ both ways, one eviction, figures and PASS on screen, the mirror
1775
+ measured from the screenshot on PAL and NTSC).
1776
+
1777
+ ### Sources
1778
+
1779
+ - Cadaver, on the sprite cache: https://cadaver.github.io/rants/sprcache.html
1780
+ - Cadaver, on Metal Warrior 4's packed and mirrored frames:
1781
+ https://cadaver.github.io/rants/mw4trick.html
1782
+ - cadaver/c64gameframework (MIT), `sprite.s`,
1783
+ https://github.com/cadaver/c64gameframework
1784
+
1785
+ ---
1786
+
1787
+ ## per_frame_hitbox — Collision boxes per animation frame, emitted at draw time, tested by group
1788
+
1789
+ **Complexity:** medium
1790
+ **Region:** both
1791
+ **Uses registers:** D010
1792
+ **Uses kernal:** (none)
1793
+ **Cost:** cycles_per_frame=3693
1794
+ **Cost basis:** measured-vice
1795
+ **Cost measured on:** oscar64-per-frame-hitbox (eight boxes, 28 pairs)
1796
+
1797
+ ### Why
1798
+
1799
+ A game needs to know which object hit which, and whether that pair
1800
+ matters. `sprite_collision_detect`'s `$D01E` cannot say either. It sets
1801
+ one bit per sprite, so three touching sprites give three bits and no
1802
+ pairs. It counts every opaque pixel, so a cape or a muzzle flash hits.
1803
+ With a multiplexer the bit belongs to a hardware sprite that showed
1804
+ several objects this frame. It cannot tell an enemy bullet passing
1805
+ through an enemy from one hitting the player.
1806
+
1807
+ A single fixed box per actor (game-design-patterns.md, "Software
1808
+ bounding-box collision") fixes the identity problem and gets poses
1809
+ wrong: a crouching player is hit by a shot that passes over its head, and
1810
+ a sword swing has no reach. Shipped engines give each animation frame its
1811
+ own box and test boxes, not actors.
1812
+
1813
+ ### How
1814
+
1815
+ **A box table per frame.** Each animation frame has zero or more boxes:
1816
+ an offset from the sprite's origin (its top-left corner, or the engine's
1817
+ anchor point), a width, a height and a group. A frame with no box is
1818
+ harmless: an explosion or a pickup effect. An attack frame can carry a
1819
+ body box and a separate weapon box.
1820
+
1821
+ **Fill the box list at draw time.** Where the draw sets a sprite's
1822
+ position and pointer, it appends that frame's boxes to one list, in
1823
+ screen coordinates: left, right, top and bottom edges, the group, and
1824
+ the owning actor. An actor that is not drawn (off screen, or not
1825
+ visible this frame) adds nothing, so the collision pass never looks at
1826
+ it. The list is rebuilt every frame and is at most a few dozen entries.
1827
+
1828
+ **Groups and a pair mask.** Give each box one group bit: player, player
1829
+ bullet, enemy, enemy bullet. For each group keep the set of groups it is
1830
+ tested against: player with enemy and enemy bullet, player bullet with
1831
+ enemy. Store that mask with each box when it is emitted; a pair `i, j`
1832
+ is tested only when `mask[i] & group[j]` is not zero. Friendly fire is
1833
+ then impossible, enemy bullets pass through enemies, and most pairs cost
1834
+ one AND and a branch. The mask is symmetric, so each unordered pair is
1835
+ visited once (`j > i`).
1836
+
1837
+ **The AABB test.** Two boxes overlap when `top[i] < bottom[j]`,
1838
+ `top[j] < bottom[i]`, `left[i] < right[j]` and `left[j] < right[i]`,
1839
+ with right and bottom exclusive. Any false compare ends the test. Put the
1840
+ compare most likely to fail first: in a side-scrolling game most pairs
1841
+ are apart in Y; in a vertical shooter, in X.
1842
+
1843
+ **8-bit and 9-bit X.** Sprite X is 9 bits, and the right 88 pixels of
1844
+ the window are X 256 to 343. A test on the low byte alone wraps: an
1845
+ enemy at 304 (low byte 48) is "hit" by a bullet at 52. Either store left
1846
+ and right as a low and a high byte and compare the high bytes first (in
1847
+ `left[i] < right[j]`: high less, true; high greater, false; equal,
1848
+ compare the low bytes), or halve every X when the box is emitted and
1849
+ test one byte at 2-pixel precision. Y fits a byte, but a box on a
1850
+ sprite near the bottom can pass 255; clip it or halve Y too. Cadaver's
1851
+ c64gameframework halves Y as it emits its bounds (`sprite.s`, source
1852
+ read here).
1853
+
1854
+ ### Why it works
1855
+
1856
+ The box and the image are chosen by the same frame number in the same
1857
+ draw, so the collision shape cannot lag the picture. The pass tests what
1858
+ the last draw put on screen. Groups carry the rule "who can hurt whom" in
1859
+ data, so adding a type is a table entry, not a new branch in every test.
1860
+ This is what c64gameframework does (`actor.s`, `sprite.s`, source read
1861
+ here): the sprite draw appends each frame's bounds to one list, with an
1862
+ end mark; a bullet skips actors whose group flags equal its own (an EOR
1863
+ of the two flag bytes, masked) and actors with 0 hit points; an actor
1864
+ may have several boxes, and a flipped frame mirrors its box about the
1865
+ anchor.
1866
+
1867
+ ### Variations
1868
+
1869
+ **Bullets against actors only.** Walk the bullet list against the actor
1870
+ boxes instead of all pairs; with 8 bullets and 8 actors that is 64 pairs
1871
+ before masking instead of 120.
1872
+
1873
+ **Several boxes per actor.** A boss or a multi-sprite actor emits one
1874
+ box per part; the pass reports the owning actor and the box index, so a
1875
+ weak spot can take damage and armour not.
1876
+
1877
+ **Flip.** For a frame drawn mirrored, the box's left offset becomes
1878
+ `width_of_sprite - offset - box_width`; keep one table and mirror at
1879
+ emit time.
1880
+
1881
+ ### Cycle budget
1882
+
1883
+ Measured in VICE x64sc 3.10 with CIA1 timer B, interrupts masked, in the
1884
+ Oscar64 recipe: one 9-bit pair test costs 96 cycles when all four
1885
+ compares run and 28 when the first fails; the halved 8-bit test costs 59
1886
+ and 25 (100 calls less 100 empty calls, screen blanked). A frame with 8
1887
+ boxes, 28 pairs of which the masks leave 10, costs 2,037 cycles to test
1888
+ and 2,148 to emit on PAL as the recipe shows them. Those two figures
1889
+ include the demo's pair counters and the halved-X arrays that only the
1890
+ 8-bit variant uses; built without them the same frame measured 1,834 to
1891
+ test and 1,859 to emit, 3,693 in all, which is the Cost line. The same on
1892
+ NTSC except the emit, which runs past the NTSC vertical blank into a
1893
+ badline and reads 2,234. Figures move by a few cycles as the code grows
1894
+ and the layout shifts.
1895
+
1896
+ Hand-written assembly is much cheaper; this is arithmetic from the
1897
+ instruction table (rung 3), not measured here. With the box arrays
1898
+ indexed by X and Y, an 8-bit compare is `lda abs,y / cmp abs,x / bcs`,
1899
+ 10 cycles when it passes and 11 when it ends the test, so a full hit is
1900
+ 40 cycles and a first-compare miss 11. A 9-bit X compare with equal high
1901
+ bytes adds a high-byte `lda / cmp / bcc / bne` before the low bytes, 22
1902
+ cycles instead of 10, so a full 9-bit hit is 64. A masked-out pair is
1903
+ `lda / and / beq`, 11 cycles.
1904
+
1905
+ ### Recipes
1906
+
1907
+ - `recipes/oscar64/per-frame-hitbox.md` (stand, crouch and attack frames
1908
+ with their own boxes, a blade box in the player-bullet group, enemy
1909
+ bullets through enemies, a 9-bit miss that a low-byte test calls a hit,
1910
+ `$D01E` beside the box events, cycles per pair and per frame, the boxes
1911
+ drawn as outlines and measured on PAL and NTSC).
1912
+
1913
+ ### Sources
1914
+
1915
+ - cadaver/c64gameframework (MIT), `actor.s` (CheckActorCollision,
1916
+ CheckBulletCollision, AF_GROUPFLAGS) and `sprite.s` (bounds emitted by
1917
+ the sprite draw), https://github.com/cadaver/c64gameframework
1918
+
1919
+ ---
1920
+
1921
+ ## sprite_animation_table — Sprite animation from tables: frames, durations, end actions and events
1922
+
1923
+ **Complexity:** low
1924
+ **Region:** both
1925
+ **Uses kernal:** (none)
1926
+ **Cost:** cycles_per_frame=747, cycles_per_frame_typical=357
1927
+ **Cost basis:** measured-vice
1928
+ **Cost measured on:** oscar64-sprite-animation-table (six actors, the scenario's worst frame)
1929
+
1930
+ ### Why
1931
+
1932
+ Every game animates its sprites. Written as code in each actor's state
1933
+ routine ("if timer = 6, next frame; if frame = 4, frame = 1"), the
1934
+ frame logic is copied into every state, durations are hard to tune, and
1935
+ the moment an attack spawns its bullet drifts from the frame that shows
1936
+ the swing. Put the animation in data and the state code only asks for
1937
+ one: "walk", "attack", "die".
1938
+
1939
+ ### How
1940
+
1941
+ **The table.** An animation is a list of entries, each a frame (an
1942
+ image number) and a duration in frames, then an end entry that says
1943
+ what happens next:
1944
+
1945
+ | End action | Effect | Typical use |
1946
+ |---|---|---|
1947
+ | loop to entry N | continue from entry N; N > 0 plays an intro once | walk, idle, a rise then a loop |
1948
+ | hold | stay on the last entry for good, report completion | death, a pose held until the next request |
1949
+ | return to animation A | start A, report completion | attack, hurt, any one-shot |
1950
+
1951
+ A spare bit of the frame byte (bit 7 in the recipe) marks an entry that
1952
+ fires an event when it is entered. Two parallel byte arrays for frame
1953
+ and duration, plus a table of start indices, keep every lookup an
1954
+ indexed load.
1955
+
1956
+ **Per-actor state.** The running animation, the current entry, a
1957
+ countdown of frames left on it, and a facing: four or five bytes (the
1958
+ recipe keeps five, with a held flag).
1959
+
1960
+ **The step, once per frame.** Decrement the countdown. When it reaches
1961
+ zero, move to the next entry; if that is an end entry, apply its action
1962
+ (loop, return or hold) until a real entry is reached; load the
1963
+ countdown from its duration; fire its event if marked; write the sprite
1964
+ pointer. On most frames the step is only the decrement, and the pointer
1965
+ is written only when the entry changes.
1966
+
1967
+ **One-shot completion and events.** An attack whose third entry shows
1968
+ the swing marks that entry, and the game spawns the bullet when the
1969
+ event fires. Changing a duration then moves the shot with the picture,
1970
+ and an attack cut off before its third entry never fires. The return
1971
+ action reports completion ("done") and starts the default animation, so
1972
+ the state code does not have to count frames to know the attack is
1973
+ over. Cadaver's c64gameframework uses the same split: `AnimationDelay`,
1974
+ a per-actor delay counter the caller uses to step a looping animation,
1975
+ and `OneShotAnimation`, which stops on the last frame and
1976
+ returns a carry flag, and `TransformActor`, which changes an actor's
1977
+ type at the end (an enemy into an explosion) (`actor.s`, source read
1978
+ here).
1979
+
1980
+ **Priority.** Give each animation a priority and refuse a request whose
1981
+ priority is lower than the running animation's: a walk request during
1982
+ an attack is refused, a hurt interrupts an attack, nothing overrides
1983
+ death. Ignore a request for the animation already running, so a state
1984
+ routine can request "walk" every frame without restarting it. Log or
1985
+ count refusals while debugging; a refused request that the state code
1986
+ expected to succeed is a common stuck-actor bug.
1987
+
1988
+ **Facing.** Store left-facing frames at a fixed offset in the block
1989
+ numbers (the recipe uses +8) and add the offset when writing the
1990
+ pointer: a turn keeps the entry and changes only the pointer. With
1991
+ `sprite_cache_flip` on this page, only right-facing frames are stored
1992
+ and the cache supplies the mirrored block; the table then yields a
1993
+ frame number and a facing, and the cache request turns them into a
1994
+ pointer. Dissecting Cadaver's engine, each actor has a base frame per
1995
+ facing and the animation frame is added to it (from his article, not
1996
+ measured here).
1997
+
1998
+ **Hitboxes.** A per-frame hitbox (`per_frame_hitbox` on this page)
1999
+ belongs to the frame the table selects, so the draw that writes the
2000
+ pointer also emits that frame's boxes. The table then drives the image,
2001
+ the collision shape and the event in step.
2002
+
2003
+ ### Why it works
2004
+
2005
+ The VIC reads each sprite's block number from screen + `$3F8` + n
2006
+ (`$07F8` with the screen at `$0400`) and fetches 63 bytes from block ×
2007
+ 64 in the current VIC bank, so the whole animation is one byte written
2008
+ per image change. Moving the screen or the bank moves both the pointer
2009
+ bytes and the blocks (`vic_bank_visibility_collision` in
2010
+ `pitfalls/banking.md`). Write pointers in the vertical blank, or from
2011
+ the multiplexer's shadow table, so a change never lands while the VIC
2012
+ is fetching that sprite.
2013
+
2014
+ The recipe checks the engine against the tables with a second model
2015
+ that uses different arithmetic: it spends the elapsed frames entry by
2016
+ entry from the animation's start instead of counting down, and reports
2017
+ an event only when the time runs out exactly on an entry's first
2018
+ frame. An off-by-one in the countdown moves every event and fails the
2019
+ check. The check does not catch a wrong table, since both read it; the
2020
+ pinned screenshot does.
2021
+
2022
+ ### Variations
2023
+
2024
+ **Transform on completion.** Instead of returning to an animation, the
2025
+ end action changes the actor's type (enemy to explosion, pickup to
2026
+ nothing), as c64gameframework's `TransformActor` does.
2027
+
2028
+ **Speed per actor.** Scale durations by a per-actor rate, or step twice
2029
+ on a frame, for a haste effect; a table of durations in 1/2 frames with
2030
+ a fractional countdown does the same at finer grain.
2031
+
2032
+ **Direction-dependent animations.** Games with eight-way movement keep a
2033
+ table of animations per direction instead of one facing offset.
2034
+
2035
+ ### Cycle budget
2036
+
2037
+ Measured in VICE x64sc 3.10 with CIA1 timer B, interrupts masked, in the
2038
+ Oscar64 recipe, the same on PAL and NTSC. One step that only
2039
+ decrements costs 25 cycles; one that moves to the next entry and writes
2040
+ the pointer costs 124 (averaged over a two-entry loop, half the calls
2041
+ through the loop action). Both are 100 calls through a function pointer
2042
+ less 100 empty calls, screen blanked. Six actors on a frame where all
2043
+ only decrement cost 357 cycles, loop and calls included, about 60 each;
2044
+ the worst frame of the recipe's scenario is 799, with one return to idle
2045
+ and a log entry and three other actors changing entry. Without the
2046
+ recipe's log write the same frame measured 747, which is the Cost line.
2047
+ Hand-written assembly with the state in arrays indexed by X is
2048
+ cheaper: `dec count,x` (7 cycles) and a taken `bne` (3) are 10 cycles
2049
+ when the entry holds (instruction-table arithmetic, not measured here).
2050
+
2051
+ ### Recipes
2052
+
2053
+ - `recipes/oscar64/sprite-animation-table.md` (six actors on a scripted
2054
+ run: looping walk, idle, a rise looping from entry 1, an attack firing
2055
+ on its third entry and returning to idle, a hurt cutting an attack
2056
+ off, a held death, two requests refused by priority; the log and the
2057
+ final pointers compared with a second model; cycles per step; the
2058
+ sprite images measured on PAL and NTSC).
2059
+
2060
+ ### Sources
2061
+
2062
+ - Cadaver, dissecting his game engine (base frame per facing):
2063
+ https://cadaver.github.io/rants/dissect.html
2064
+ - Cadaver, on actor interaction and transformation:
2065
+ https://cadaver.github.io/rants/interaction.html
2066
+ - cadaver/c64gameframework (MIT), `actor.s` (AnimationDelay,
2067
+ OneShotAnimation, TransformActor),
2068
+ https://github.com/cadaver/c64gameframework
2069
+
2070
+ ---
2071
+
2072
+ ## software_sprite_preshifted — Pre-shifted masked software sprites in a character back buffer
2073
+
2074
+ **Complexity:** medium
2075
+ **Region:** both
2076
+ **Uses registers:** D018, D012
2077
+ **Uses kernal:** (none)
2078
+ **Requires:** unrolled_loops
2079
+ **Cost:** cycles_per_frame=1890, bytes_code=2608, bytes_data=2688
2080
+ **Cost basis:** derived-listing
2081
+ **Cost measured on:** kickassembler-software-sprite-preshifted (one object drawn and erased, screen blanked)
2082
+
2083
+ ### Why
2084
+
2085
+ Eight hardware sprites run out. A multiplexer (`sprite_multiplex_8`)
2086
+ stretches them down the screen, but it cannot put a ninth object on the
2087
+ same raster lines as eight others, and many small objects on one row is
2088
+ exactly what a shooter's bullet cloud or a puzzle game's falling pieces
2089
+ need. A software sprite is drawn by the CPU into memory the VIC is
2090
+ already displaying: a block of character definitions laid out as a
2091
+ canvas, or a bitmap. It costs CPU time instead of a hardware slot, and
2092
+ there is no limit per line.
2093
+
2094
+ The naive draw shifts each row of the object right by the pixel offset
2095
+ before writing it, which is a shift and a carry across three or four
2096
+ bytes for every row, every frame. Pre-shifting does that once, off-line:
2097
+ the object is stored eight times, once for each pixel offset within a
2098
+ byte, together with its mask. A draw is then a straight copy through an
2099
+ AND and an OR, and it costs the same at every one of the eight shifts.
2100
+
2101
+ ### How
2102
+
2103
+ 1. Lay out the back buffer. In character mode, point `$D018` at a
2104
+ custom font and give a block of consecutive character codes to the
2105
+ canvas: cell `(cx, cy)` is code `base + cy * W + cx`, so pixel row `y`
2106
+ of cell column `cx` is byte `font + (y / 8) * W * 8 + cx * 8 + (y & 7)`.
2107
+ One pixel row of the canvas is one byte per cell, eight bytes apart.
2108
+ Fill the canvas with the background tile and write the codes into the
2109
+ screen once; from then on only the font bytes change.
2110
+ 2. Build the pre-shift table. For a 24x21 object each shift is 4 bytes
2111
+ wide by 21 rows, 84 bytes of data and 84 of mask (mask bit 1 means
2112
+ leave the background alone). Eight shifts: `8 * 84 * 2 = 1,344` bytes
2113
+ an object (arithmetic; the recipe's assembler reports 2,688 for two).
2114
+ Store it transposed, the eight shifts of one byte position together,
2115
+ so the shift is a Y index and the position is a constant.
2116
+ 3. Draw: with `X = x & $F8` (the cell column times 8) and `Y = x & 7`
2117
+ (the shift), for each of the 84 byte positions `lda dest,x` /
2118
+ `and mask+p*8,y` / `ora data+p*8,y` / `sta dest,x`, unrolled, 17
2119
+ cycles each. The object's top row `y0` is baked into the unrolled
2120
+ addresses; a second object at another row is a second copy of the
2121
+ routine, about 1 KB each.
2122
+ 4. Erase before the next draw. With a tiled background the cheapest
2123
+ erase writes the tile's rows back over the 4-cell footprint: one
2124
+ immediate load and four stores a row. A background that is not a
2125
+ repeating tile needs a saved copy of the footprint instead, restored
2126
+ in the same order.
2127
+ 5. Run the erase and the draws where the VIC is not reading the canvas.
2128
+ The character generator bytes are fetched on every raster line the
2129
+ canvas cells are on, so a draw during those lines tears. Poll `$D012`
2130
+ for a line below the canvas and do the frame's work there; a canvas
2131
+ that fills the screen leaves only the vertical blank, and then two
2132
+ fonts and a `$D018` flip are needed, which is `screen_double_buffer_d018`
2133
+ applied to the font bits instead of the matrix bits.
2134
+
2135
+ ### Why it works
2136
+
2137
+ The VIC reads character definitions from the font on every line, so a
2138
+ byte written to the font shows on the next line that draws that row of
2139
+ the cell. Cell-aligned characters mean the CPU does not have to know
2140
+ where on screen the canvas is; the address arithmetic is all in font
2141
+ memory, and X indexing by `x & $F8` moves the whole draw one cell
2142
+ without touching any operand. The AND clears the object's silhouette
2143
+ out of the background and the OR paints the shape into the hole, so
2144
+ any number of objects can be layered in draw order, the later one on
2145
+ top, without a sprite-priority register.
2146
+
2147
+ The recipe times one masked blit of a 24x21 object at each of the eight
2148
+ shifts with CIA2 timer A, display blanked, net of the call: 1,428
2149
+ cycles at every shift, on PAL and NTSC alike (measured in VICE x64sc).
2150
+ That is `84 * 17`, and the equality is the point: the run-time cost of
2151
+ the shift is zero. The tile erase of the same footprint is 462 cycles.
2152
+ One object drawn and erased is therefore 1,890 cycles a frame, about
2153
+ 9.6 % of a PAL frame; two are 3,780. The same blit started at raster
2154
+ line 100 with the display on measured 1,557 on PAL and on NTSC: the
2155
+ 129 extra cycles are the three badlines the 23-line blit crosses,
2156
+ which is `badline_cycle_loss` in the display area, and the reason the
2157
+ timing figures were taken with DEN off.
2158
+
2159
+ ### Against the multiplexer
2160
+
2161
+ Choose the multiplexer when the objects are few per raster line, need
2162
+ free pixel placement in both axes and their own colours, and move over
2163
+ a background you cannot cheaply repaint: it costs a raster IRQ and
2164
+ register writes, not a redraw. Choose pre-shifted software sprites
2165
+ when several objects share raster lines, when they sit on a tiled or
2166
+ saved background, or when the sprite hardware is spoken for by the
2167
+ player and the bosses. The two combine: hardware sprites for the few
2168
+ that need sub-cell placement and priority, software sprites for the
2169
+ crowd. A software sprite has one colour per cell it touches, the cell's
2170
+ colour RAM entry, and it is erased and redrawn every frame it moves, so
2171
+ its cost scales with the count while a sprite in a hardware slot is
2172
+ free to move.
2173
+
2174
+ ### Variations
2175
+
2176
+ **Save-under erase.** Copy the 84 bytes under the footprint before the
2177
+ draw and write them back to erase. Costs a copy per object per frame
2178
+ (not measured here) but works over any background, and is what a
2179
+ bitmap-mode version needs.
2180
+
2181
+ **Bitmap canvas.** The same tables and the same masked copy, with the
2182
+ destination a bitmap: eight bytes per cell row, 320 bytes per cell row
2183
+ of the screen. `bobs_effect` on `effects-vector-3d.md` describes both
2184
+ the cell-aligned and the bitmap forms at the demo scale; this entry is
2185
+ the pixel-placed, cell-mode case with a measured blit.
2186
+
2187
+ **Three-byte shift zero.** At shift 0 the fourth byte is all mask and
2188
+ no data; a separate 63-byte routine for that shift saves 21 stores.
2189
+ Not done in the recipe, which keeps one routine so the eight figures
2190
+ are comparable.
2191
+
2192
+ ### Cycle budget
2193
+
2194
+ Per object per frame, measured: 1,428 to draw, 462 to erase, 1,890 in
2195
+ all. For the recipe's two objects, 3,780 cycles, which is about 60
2196
+ raster lines of the 213 that lie below a canvas ending on line 98, and
2197
+ inside the 6,700-cycle race-free blank the pitfall page quotes even if
2198
+ the canvas filled the screen. Six such objects would not be: at 11,340
2199
+ they would spill into the display, which is where
2200
+ `full_field_redraw_exceeds_vblank` starts. The `cycles_per_frame` figure
2201
+ on the Cost line is one object, drawn and erased. The byte figures are
2202
+ the built recipe's two objects: 2,608 bytes of code, which is 1,009 for
2203
+ one unrolled blit routine and 295 for its erase, twice; and 2,688 bytes
2204
+ of tables, 1,344 an object (the assembler's own byte counts for the
2205
+ recipe; the timing and print harness is not counted).
2206
+
2207
+ ### Recipes
2208
+
2209
+ - `recipes/kickassembler/software-sprite-preshifted.md` (16x4 cell
2210
+ canvas, tiled background, a ring and a diamond crossing it in opposite
2211
+ directions, the blit timed at all eight shifts, checksum verdict)
2212
+
2213
+ ---
2214
+
2215
+ ## multi_sprite_object — Bosses and large objects from several hardware sprites at fixed offsets from one origin
2216
+
2217
+ **Complexity:** medium
2218
+ **Region:** both
2219
+ **Uses registers:** D000, D001, D010, D015, D017, D01D, D027
2220
+ **Uses kernal:** (none)
2221
+ **Cost:** cycles_per_frame=1342, sprites_per_line=3
2222
+ **Cost basis:** measured-vice
2223
+ **Cost measured on:** oscar64-multi-sprite-object (worst frame, six parts, in the vertical blank)
2224
+
2225
+ ### Why
2226
+
2227
+ One sprite is 24 x 21 pixels, 48 x 42 expanded. A boss, a tank or a
2228
+ mothership is bigger. The game still wants to treat it as one actor: one
2229
+ position, one movement routine, one death. The answer is a part table.
2230
+ The object has one origin, and each part is a hardware sprite at a fixed
2231
+ offset from it.
2232
+
2233
+ The offsets break the one-sprite habits. A part can be past X 255 while
2234
+ the origin is not, or the reverse, so the ninth X bit belongs to each
2235
+ part. A part can be off screen while the object is on screen. The parts
2236
+ also use several of the eight sprites on the same raster lines, which a
2237
+ multiplexer has to account for.
2238
+
2239
+ ### How
2240
+
2241
+ **A part table.** Per part: `dx` and `dy` from the origin (signed), the
2242
+ base frame, the colour, the expand flags and, if it animates, its
2243
+ animation length. Keep the table per object type. Keep per object only
2244
+ the origin and each part's animation state.
2245
+
2246
+ **9-bit X for every part.** Compute `x = origin + dx` in 16 bits for
2247
+ each part. Its low byte goes to `$D000 + 2n` and its bit 8 to the part's
2248
+ own bit in `$D010`. A part at `dx = -24` under an origin at 264 is at
2249
+ 240, bit clear, while its neighbours have the bit set. Build the
2250
+ object's `$D010` bits in a byte and merge them under the object's sprite
2251
+ mask: `$D010 = ($D010 & ~mask) | bits`. `$D015` is merged the same way.
2252
+ A plain store would clear the bits of every other object's sprites.
2253
+
2254
+ **Clip each part.** Show a part only when some of it is inside the
2255
+ window: X from 24 to 343 and Y from 50 to 249 (CSEL = 1, RSEL = 1),
2256
+ with the part's own width and height. Otherwise clear its `$D015` bit and
2257
+ skip its writes. A part under the border would be invisible anyway,
2258
+ but it still takes a hardware sprite, its DMA and a multiplexer slot.
2259
+ Past X 511 the 16-bit sum no longer fits the 9 bits and wraps to the
2260
+ left side, and on PAL X 504 to 511 is never drawn
2261
+ (`sprite_x_range_hidden_and_seam` in `pitfalls/sprite.md`). A part
2262
+ partly left of X 0 needs the model's wrap: a probe for this entry put an
2263
+ X-expanded sprite at X 500 and found its left edge at pixel -4 on PAL
2264
+ and -12 on NTSC, so the wrap is at 504 on PAL and 512 on NTSC (VICE
2265
+ x64sc, exit screenshot). Hide such a part, or pick the modulus at
2266
+ start-up as that pitfall says. Clip Y in the 16-bit sum too: a part
2267
+ at `dy = 42` under an origin at Y 230 is at 272, which `$D001` cannot
2268
+ hold.
2269
+
2270
+ **Expanded parts cover more with the same sprite.** An X-expanded part
2271
+ is 48 wide and a Y-expanded one 42 tall, for the DMA of one sprite
2272
+ (`sprite_expand`). The pixel doubles too: 2 screen pixels wide for hires,
2273
+ 4 for multicolour (arithmetic), and 2 lines tall. Beside an unexpanded
2274
+ part the difference shows, so draw the art for it: armour plates and
2275
+ wings expanded, the face and the weak spot not. The clip test uses the
2276
+ expanded size.
2277
+
2278
+ **Animate per part.** Only the parts that move need a countdown and a
2279
+ current frame; the frame written is the base frame plus the step. An
2280
+ eye blinks, a turret turns, and the other parts' pointers are still
2281
+ written each frame without change. Colour and expand bits usually
2282
+ stay fixed for the object's life, so they are written once at spawn,
2283
+ under the mask.
2284
+
2285
+ **Hit boxes per part.** Emit one box per shown part where the part is
2286
+ placed, as `per_frame_hitbox` (this page) does in its "Several boxes per
2287
+ actor" variation. The collision pass then reports the part as well as
2288
+ the actor, so armour can ignore a shot and a weak spot can take it. A
2289
+ clipped part emits no box. Corescape gives each of its boss's parts its
2290
+ own enemy type and hit count, 8 (16 in hard mode) against the core's 32
2291
+ (64) (`enemies.cpp`, source read here).
2292
+
2293
+ **Overlap and flip.** Where parts overlap, the lower sprite number is in
2294
+ front (`hardware/vic-ii-reference.md`), so put the part that must show
2295
+ on top in the lower slot. To face the other way, each part's offset
2296
+ becomes `-dx - width` and its image is mirrored (`sprite_cache_flip`).
2297
+ c64gameframework stores a mirrored X offset beside the normal one for
2298
+ every part, so the flip is a choice of column, not arithmetic
2299
+ (`sprite.s`, source read here).
2300
+
2301
+ ### Why it works
2302
+
2303
+ The VIC-II has no idea the parts belong together. Each part is a
2304
+ complete sprite with its own X, Y, pointer, colour and expand bits. The
2305
+ object stays in one piece because every part is recomputed from the same
2306
+ origin in the same frame, before the raster reaches the object. Write
2307
+ the parts in the vertical blank, or below the object's last line, and
2308
+ the whole object moves at once. Writes that straddle the raster can show
2309
+ the top parts at the new origin and the lower parts at the old one for
2310
+ a frame (from the mechanism; not measured here).
2311
+
2312
+ The Oscar64 recipe checks this against a model each frame: every part's
2313
+ X low byte, `$D010` bit, Y, `$D015` bit, pointer, colour and expand bits,
2314
+ and the bits of a sprite that belongs to another object. It found no
2315
+ mismatch in any frame of a sweep across X 255 and past the right edge,
2316
+ on PAL and NTSC. Its screenshots put each part's pixels exactly where
2317
+ the model places it, including a part whose register X is 240 while its
2318
+ pixels cross X 256 (VICE x64sc).
2319
+
2320
+ ### With a multiplexer
2321
+
2322
+ Each part is one sprite to the multiplexer. An object that is k parts
2323
+ wide on a raster line leaves 8 - k sprites for everything else on those
2324
+ lines. The recipe's boss is 3 wide on every line it covers, and
2325
+ Corescape's boss puts five of its six sprites within 4 lines of each
2326
+ other (offsets in `enemies.cpp`), so on those lines only three are free.
2327
+
2328
+ `sprite_multiplex_game` rejects the ninth sprite on a band, one sprite
2329
+ at a time. A boss part can lose that test while its neighbours pass,
2330
+ and the boss shows with a hole in it. Either give the object's parts
2331
+ priority in the sort or the acceptance pass so they go in first, or keep
2332
+ the boss out of the multiplexer in fixed hardware sprites and multiplex
2333
+ only the rest. Corescape does the first kind: each boss part is its own
2334
+ virtual sprite (`vspr_set` for each part, `enemies.cpp`).
2335
+
2336
+ A Y-expanded part holds its hardware sprite for 42 lines, not 21, so the
2337
+ reuse gap for that slot is 42 lines (arithmetic from the Y-expand
2338
+ mechanism in `sprite_expand`; not measured here).
2339
+
2340
+ ### Variations
2341
+
2342
+ **Logical sprites.** c64gameframework separates the logical sprite, the
2343
+ object's picture, from the physical sprites it is made of, and each
2344
+ part carries an expand flag. Its clip test has a separate X limit for
2345
+ expanded parts. It drops a part that is outside the X or Y limits and
2346
+ keeps the rest, and it stops adding parts when no sprite is left
2347
+ (`sprite.s`, `screen.s`, source read here).
2348
+
2349
+ **Halved X.** Store every X halved, one byte, and double it when
2350
+ writing the registers. Adding `dx / 2` is then an 8-bit add, at 2-pixel
2351
+ resolution. c64gameframework keeps sprite X halved: its clip test
2352
+ compares against `MAX_SPRX / 2` (`sprite.s`).
2353
+
2354
+ **A detachable part.** Give a part its own hit points and a flag that
2355
+ drops it from the table when destroyed. The rest of the object keeps its
2356
+ offsets, and the freed hardware sprite goes back to the pool.
2357
+
2358
+ ### Cycle budget
2359
+
2360
+ Measured in VICE x64sc 3.10 with CIA1 timer B, interrupts masked, in the
2361
+ Oscar64 recipe. Screen blanked, one call each, less an empty call: 197
2362
+ cycles for each part shown, 78 for each part clipped, 67 fixed per
2363
+ object. During the run, at the top of the vertical blank, a whole
2364
+ six-part update took 808 to 1,342 cycles, the same on PAL and NTSC. The
2365
+ worst frame, the Cost line, had all six parts shown and above X 255, and
2366
+ the animated part stepping. A typical frame with all six shown is about
2367
+ 1,249 (6 x 197 + 67, arithmetic from the measured figures). The worst
2368
+ and best figures include the timer start and stop and the call, 34 cycles
2369
+ for an empty call timed the same way (measured here); the per-part figures
2370
+ do not. These are
2371
+ compiled C. Hand-written assembly with the table indexed by X is
2372
+ cheaper; it was not measured here. The model check and the on-screen
2373
+ counters are not in these figures.
2374
+
2375
+ ### Recipes
2376
+
2377
+ - `recipes/oscar64/multi-sprite-object.md` (a six-sprite boss, three
2378
+ parts on top and an X-expanded wing, a Y-expanded core and a second
2379
+ wing below, one part animated; on autopilot it sweeps across X 255 and
2380
+ past the right edge; every part's registers, including its `$D010`
2381
+ bit, checked against a model every frame; cycles per update and per
2382
+ part; each part's box measured with PIL on PAL and NTSC)
2383
+
2384
+ ### Sources
2385
+
2386
+ - drmortalwombat/corescape (GPL-3.0), `enemies.cpp`: boss parts at
2387
+ offsets X -48, -24, 0, 24, 48 and Y -4, -2, 21, -2, -4 from the core,
2388
+ each with its own type and hit count, each a `vspr_set` virtual sprite.
2389
+ https://github.com/drmortalwombat/corescape
2390
+ - cadaver/c64gameframework (MIT), `sprite.s` and `screen.s`: logical
2391
+ sprites made of physical sprites, per-part flipped X offset, expand
2392
+ flag, and per-part clip limits.
2393
+ https://github.com/cadaver/c64gameframework
2394
+
2395
+ ---
2396
+
2397
+ ## mixed_sprite_char_actors — Large actors drawn partly in hardware sprites and partly in reserved character cells
2398
+
2399
+ **Complexity:** medium
2400
+ **Region:** both
2401
+ **Uses registers:** D000, D001, D010, D018, D01B
2402
+ **Uses kernal:** (none)
2403
+ **Requires:** mob_priority
2404
+ **Cost:** cycles_per_frame=6108, sprites_per_line=2
2405
+ **Cost basis:** measured-vice
2406
+ **Cost measured on:** oscar64-mixed-fighters (worst tick of the whole actor update, PAL)
2407
+
2408
+ ### Why
2409
+
2410
+ A beat-'em-up or sports fighter is bigger than a sprite. Two fighters
2411
+ of 2 x 2 multicolour sprites take all eight, and they stand side by
2412
+ side on the same raster lines, so a multiplexer cannot reuse any of
2413
+ them: it reuses a sprite only below the lines where it was last shown.
2414
+ Nothing is left for a third actor, a ball or an effect. The way round
2415
+ is to draw part of the cast in character cells: a block of cells whose
2416
+ glyphs belong to that actor alone and are rewritten when it animates
2417
+ or moves inside a cell. It costs CPU time when the picture changes and
2418
+ no sprite at all, and its size per line is limited only by the cells.
2419
+
2420
+ What IK+ (System 3, 1987) did is not established here. Games That
2421
+ Weren't has preserved Archer Maclean's IK+ design notes as scanned
2422
+ galleries; the page's text does not describe how the fighters were
2423
+ drawn, and the source code was not published. A Lemon64 thread titled
2424
+ "IK: How sprites and character graphics worked together" is a forum
2425
+ report; it could not be read here (HTTP 403).
2426
+
2427
+ ### How
2428
+
2429
+ 1. Reserve a glyph range in a RAM font (`$D018`). A block W x H cells
2430
+ uses W x H codes, laid out once: cell `(cx, cy)` of the block shows
2431
+ code `base + cy * W + cx`. Nothing else on screen uses those codes.
2432
+ 2. Draw the actor's picture into those glyphs. Rewrite them when the
2433
+ animation frame changes, and when the actor moves inside a cell if
2434
+ it moves in steps finer than 8 pixels.
2435
+ 3. Move the block of codes when the actor crosses a cell boundary:
2436
+ clear the cells it leaves and write the codes at the new column.
2437
+ 4. Draw the other actor, or the parts that must move freely, in
2438
+ hardware sprites (`multi_sprite_object`).
2439
+ 5. Decide who is in front each frame with the sprites' `$D01B` bits,
2440
+ and finish every write before the raster reaches the actors' first
2441
+ line, or start it after their last line.
2442
+
2443
+ **Whole cells or pre-shifted.** Moving the block in whole cells needs
2444
+ no extra table, and the glyphs change only on an animation frame; but
2445
+ the actor steps 8 pixels, which shows beside a sprite moving in 1 or 2.
2446
+ Pre-shifting stores each frame at every offset inside a cell, one
2447
+ column wider, and copies the right one on every move. In multicolour
2448
+ text a pixel is 2 screen pixels wide, so four shifts cover every
2449
+ position. The recipe's 16 x 48-pixel actor in a 5 x 6 block is 240
2450
+ bytes a shift, 1,920 for two poses (the Oscar64 map).
2451
+
2452
+ **Colour.** A multicolour cell has one colour of its own, colour RAM
2453
+ 0 to 7, and shares `$D022` and `$D023` with every multicolour cell on
2454
+ the screen. A multicolour sprite has one colour of its own and shares
2455
+ `$D025` and `$D026` with every multicolour sprite. Two actors drawn
2456
+ different ways can therefore look alike only within those limits.
2457
+
2458
+ **Background.** The block replaces the cells under it. Its 00 pixels
2459
+ show `$D021`, not the scenery. Keep the actors on a plain band, or
2460
+ merge the background into the actor's glyphs as a masked draw
2461
+ (`software_sprite_preshifted`), which costs more per frame (not
2462
+ measured here). Two character actors that overlap share cells and need
2463
+ that merge too; one character actor and one sprite actor do not.
2464
+
2465
+ ### Overlap and priority
2466
+
2467
+ The character actor is playfield, so the sprite actor's `$D01B` bits
2468
+ decide who is in front, one bit per sprite (`mob_priority`). Bits clear:
2469
+ the sprite actor is in front of every pixel of the character actor.
2470
+ Bits set: the character actor's foreground pixels cover the sprite.
2471
+ In multicolour text only bit pairs 10 and 11 are foreground for this;
2472
+ pairs 00 and 01 are background, so the sprite shows through any part
2473
+ drawn in `$D022`. The recipe measured it: with the character fighter in
2474
+ front, 344 pixels of it covered the sprite fighter and 44 pixels of
2475
+ the sprite fighter showed through its `$D022` belt (VICE x64sc, PAL and
2476
+ NTSC, every arena pixel compared with a model). The 344 were 296 of
2477
+ pair 11 and 48 of pair 10. Draw the parts that
2478
+ must cover the other actor in pairs 10 and 11.
2479
+
2480
+ Sprite-to-sprite order is fixed by sprite number and ignores `$D01B`.
2481
+ It does not enter between the two actors here, since only one of them
2482
+ is sprites. Flip the bits in the same update as the positions, before
2483
+ the raster reaches the actors, so one frame never shows the old order.
2484
+
2485
+ ### Why it works
2486
+
2487
+ The VIC reads the glyph bytes on every line that shows the cell, so a
2488
+ glyph write shows on the next frame that draws it, and a code write
2489
+ moves the whole cell. Because the actor owns its codes, a redraw is a
2490
+ copy into a fixed address range: no mask, no read of the screen and no
2491
+ search for which cells are affected. The sprite actor costs its
2492
+ register writes only. The two meet only in the VIC's priority logic,
2493
+ which needs no CPU time.
2494
+
2495
+ The Oscar64 recipe hashes the arena's screen codes, the 240 glyph bytes
2496
+ and the sprite registers at 17 checkpoints of a scripted fight and
2497
+ compares them with a Python model; none differed, on PAL or NTSC. The
2498
+ pinned screenshots, and a probe with the sprite fighter in front, match
2499
+ the model at every one of the 15,360 arena pixels (VICE x64sc 3.10).
2500
+
2501
+ ### Against the alternatives
2502
+
2503
+ Choose all-sprite fighters when there are two actors and nothing else
2504
+ shares their lines: no redraw, free placement, their own colours.
2505
+ Choose a multiplexer when the extra actors are above or below one
2506
+ another, not side by side. Choose a character actor when actors share
2507
+ raster lines and the sprites are needed elsewhere, when an actor is
2508
+ bigger than sprites cover, or when it stands still much of the time: a
2509
+ standing character actor costs nothing. Choose masked software sprites
2510
+ (`software_sprite_preshifted`) when the actor must pass over scenery.
2511
+
2512
+ ### Variations
2513
+
2514
+ **Fewer stores on a move.** The recipe clears all 30 old cells and
2515
+ writes 30 new ones. Only the column left behind needs clearing, 36
2516
+ stores instead of 60 (arithmetic; not measured here).
2517
+
2518
+ **Bitmap actor.** The same idea in a bitmap: the actor owns an area of
2519
+ bitmap bytes. There is no code to move, so every move is a copy of the
2520
+ whole area, and colour comes from screen RAM per cell.
2521
+
2522
+ **Parts in sprites.** A character actor can carry a sprite for the part
2523
+ that moves fastest, a fist or a foot, so the body is redrawn only on
2524
+ pose changes. Not done in the recipe.
2525
+
2526
+ ### Cycle budget
2527
+
2528
+ Measured in VICE x64sc 3.10 with CIA1 timer B, interrupts masked, in the
2529
+ Oscar64 recipe. Screen blanked, one call each, less an empty call: 3,323
2530
+ cycles to copy the 240 glyph bytes, about 13.8 a byte; 1,899 to clear
2531
+ and rewrite the 5 x 6 block one cell over; 448 to place the four
2532
+ sprites, two of them past X 255. The whole update, started at raster
2533
+ line 256, took 508 to 6,108 cycles on PAL. The worst tick does all three
2534
+ parts, which every walking tick on which the actor crosses a cell
2535
+ boundary does; that is the Cost line. A walking tick inside a cell
2536
+ skips the block move, about 4,209 (6,108 − 1,899, arithmetic). A hold
2537
+ tick is 508. On NTSC the worst was 6,366: from line 256 an update that
2538
+ long runs past the end of the 263-line frame into the badlines of the
2539
+ top text rows, while on PAL it ends by line 41 (arithmetic from 63 and
2540
+ 65 cycles a line; the stall was not traced). These are compiled C. An
2541
+ unrolled `lda abs,x` / `sta abs,x` copy is 9 or 10 cycles a byte,
2542
+ about 2,200 to 2,400 for 240 bytes (arithmetic from the instruction
2543
+ table; not measured here). The model check and the on-screen text are
2544
+ not in these figures.
2545
+
2546
+ ### Recipes
2547
+
2548
+ - `recipes/oscar64/mixed-fighters.md` (one fighter in four multicolour
2549
+ sprites, one in a 5 x 6 block of pre-shifted multicolour cells moving
2550
+ in 2-pixel steps; on autopilot they overlap, strike, cross with `$D01B`
2551
+ swapped and hold overlapped; screen codes, glyph bytes and sprite
2552
+ registers checked against a Python model at 17 ticks; cycles per
2553
+ glyph copy, block move, sprite update and tick; every arena pixel
2554
+ compared with the model with PIL on PAL and NTSC)
2555
+
2556
+ ### Sources
2557
+
2558
+ - Games That Weren't, "IK+ design and development notes" (Frank
2559
+ Gasking, 19 December 2024): Archer Maclean's IK+ notes preserved as
2560
+ scanned galleries; source code withheld; the text does not describe
2561
+ the rendering method.
2562
+ https://www.gamesthatwerent.com/2024/12/ik-design-and-development-notes/
2563
+ - Lemon64 forum, "IK: How sprites and character graphics worked
2564
+ together" (forum report; returned HTTP 403 here, not read).
2565
+ https://www.lemon64.com/forum/viewtopic.php?t=38956
2566
+ - Christian Bauer, "The MOS 6567/6569 video controller (VIC-II)", section 3.8.2 (with MCM set, bit pairs 00 and 01 are background for priority and collisions): http://www.zimmers.net/cbmpics/cbm/c64/vic-ii.txt