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