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