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