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,1431 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: effect
|
|
3
|
+
chip: VIC-II
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- doc-type: technique-reference -->
|
|
7
|
+
|
|
8
|
+
# Vector and 3D Effects
|
|
9
|
+
|
|
10
|
+
The C64's hardware was never designed to draw three-dimensional graphics. There is no depth buffer, no triangle rasterizer, no framebuffer in the conventional sense. What the machine does have is a 1 MHz CPU with fast zero-page addressing, a sprite system whose pixel coordinates can be reprogrammed between scanlines, a character mode whose cell contents can be swapped every frame, a bitmap mode whose pixels can be filled row by row from lookup tables, and a community of demoscene coders who spent forty years pushing every one of those mechanisms further than Commodore ever imagined.
|
|
11
|
+
|
|
12
|
+
Every effect in this document is built from the same raw materials: lookup tables (sine, cosine, distance, angle), per-frame register writes, and careful cycle accounting. The VIC-II's display is the output device; the CPU is the renderer. That arrangement imposes hard limits on polygon count, resolution, and frame rate, but those limits have shaped a distinct aesthetic — the flat-shaded rotating cube, the sine-wave plasma wash, the tunnel drawing the viewer forward — that is recognizable as C64 3D even before any music starts.
|
|
13
|
+
|
|
14
|
+
The techniques below range from medium-complexity plasma writes (achievable in a weekend) to scene-tier voxel landscapes that require precomputed data tables, double-buffering, and careful frame-rate management. All are achievable on stock PAL or NTSC hardware. None require expansion hardware.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## dot_3d_rotator — Dotted 3D point cloud rotation
|
|
19
|
+
|
|
20
|
+
**Complexity:** scene-tier
|
|
21
|
+
**Region:** both
|
|
22
|
+
**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, D018
|
|
23
|
+
|
|
24
|
+
### Why
|
|
25
|
+
|
|
26
|
+
A 3D point cloud rotator is the classic entry-level demoscene 3D effect: a set of points, described in three-dimensional coordinates, rotated by a time-varying angle and projected flat onto the screen. Every point traces a continuous elliptical path as the object rotates. The visual result — a spinning wire-frame-like mass of dots — communicates solid 3D form through motion, even though no edges or faces are drawn. The effect's appeal is partly mathematical (the first time you see a paraboloid or sphere constructed from only dots, rotating smoothly) and partly the achievement of having a microprocessor do real-time 3D geometry.
|
|
27
|
+
|
|
28
|
+
### How
|
|
29
|
+
|
|
30
|
+
The rotation is computed via a 3x3 rotation matrix applied to each point in the cloud. For a rotation about two axes (a common choice: Y then X), the matrix multiplication reduces to six multiplications and six additions per point. On a C64, multiplications are performed via lookup tables: a 512-entry sine table indexed by angle gives both `sin(θ)` and `cos(θ)` (offset by 128 entries), allowing a multiply-by-sine to be implemented as a table lookup plus a scaling shift.
|
|
31
|
+
|
|
32
|
+
The per-frame sequence is:
|
|
33
|
+
|
|
34
|
+
1. Advance the rotation angles (two 8-bit counters, one per axis, incremented by a speed constant each frame).
|
|
35
|
+
2. Recompute the six rotation coefficients from the sine/cosine table: `cos(ax)`, `sin(ax)`, `cos(ay)`, `sin(ay)`, and the four products needed for the combined matrix.
|
|
36
|
+
3. For each point in the cloud, apply the rotation: compute transformed X, Y, Z from the original coordinates and the coefficients.
|
|
37
|
+
4. Apply perspective projection: `screen_x = cx + (tx * FOCAL) / (tz + DEPTH_OFFSET)`, `screen_y = cy + (ty * FOCAL) / (tz + DEPTH_OFFSET)`. Division is a second lookup table: a reciprocal table indexed by Z (built as in `table_generation`, `cpu-cycle-tricks.md`).
|
|
38
|
+
5. Output the projected point to the display.
|
|
39
|
+
|
|
40
|
+
Two display strategies exist:
|
|
41
|
+
|
|
42
|
+
**Sprite-based plot.** Each hardware sprite is treated as a single large pixel. With 8 sprites enabled and multiplexed across the frame, up to approximately 24–32 points can be displayed simultaneously before flicker sets in (the VIC-II's 8-sprite-per-line limit means points at the same Y coordinate compete for sprite slots). Individual point size is one sprite with all-pixels-on data (a solid 24×21 block, typically clipped to 8×8 via masking). Color can be varied per point by writing the sprite color registers.
|
|
43
|
+
|
|
44
|
+
**Charset-based plot.** Points are rendered by writing custom 8×8 character cells (a small dot pattern: a single lit pixel at a specific bit position) into character RAM and then filling the corresponding screen RAM positions. A fixed charset of 64 single-dot glyphs (plus one blank) gives every pixel position inside a cell — an 8×8 cell has only 64 pixel positions, so an earlier version of this paragraph claiming "256 distinct per-cell dot positions" from a 256-entry charset was wrong; the remaining codes can hold a selection of two-dot combinations. Points are therefore pixel-positioned, not cell-aligned; the cost is that two points falling in the same cell collide unless a glyph for that pair exists. The alternative is to allocate a fresh character to each occupied cell per frame and OR the point's bit into it, which never collides but is limited to 255 occupied cells per frame and requires clearing the used glyphs each frame (hence the double-buffered charset below). This allows far more than 8 points per scanline. Multicolor character mode halves horizontal resolution but allows colored dots against a background.
|
|
45
|
+
|
|
46
|
+
### Why it works
|
|
47
|
+
|
|
48
|
+
The mathematics of 3D rotation is linear: a rotation matrix transforms any 3D point into a new 3D point by mixing the original coordinates via sine and cosine coefficients. The result is exact (given integer precision) and requires no iterative or recursive computation — it is six multiplies and six adds per point, which on a 6510 translates to six table lookups and six ADC/SBC chains.
|
|
49
|
+
|
|
50
|
+
Perspective projection divides by Z to simulate depth recession: distant points appear closer together, near points spread apart. The reciprocal table makes this a lookup rather than a software divide. The effective resolution of the depth effect depends on the table's index range and the FOCAL constant; typical C64 implementations use a FOCAL of 128–256 and a DEPTH_OFFSET of 4–8 to prevent division by zero (or near-zero) at the front face.
|
|
51
|
+
|
|
52
|
+
The VIC-II cooperates by allowing sprite X and Y registers to be reprogrammed between frames. All 8 sprite positions, colors, and enable bits can be written in the vertical blank, then the chip draws all of them on the next frame without further CPU involvement during the display period.
|
|
53
|
+
|
|
54
|
+
### Variations
|
|
55
|
+
|
|
56
|
+
**Depth-cueing.** Map the post-projection Z value to a sprite or character color. Near points appear bright (white or yellow), far points dim (dark gray). The VIC-II's 16 colors support approximately 5–6 distinguishable brightness steps, which is enough to give the cloud convincing three-dimensionality.
|
|
57
|
+
|
|
58
|
+
**Object morphing.** Define two point clouds (e.g., a sphere and a cube). Each frame, interpolate between the two sets of unrotated coordinates using a morph parameter. The rotated display shows the object smoothly deforming between shapes.
|
|
59
|
+
|
|
60
|
+
**Double buffering with charset approach.** With two charset banks (two character RAM areas), one is being displayed while the other is cleared and redrawn. Swap via $D018 in the vertical blank. Eliminates the visible erasure flicker that occurs when dots are cleared in the same frame they are redrawn.
|
|
61
|
+
|
|
62
|
+
### Cycle budget
|
|
63
|
+
|
|
64
|
+
Per-point cost (approximate, varies by implementation):
|
|
65
|
+
|
|
66
|
+
- Sine/cosine lookups and matrix multiply: approximately 60–90 cycles per point using 8-bit fixed-point arithmetic (6 multiply-via-table lookups, each roughly 10–15 cycles including index computation and 8-bit shift for scaling). The table multiply itself, with its measured cost, is `table_multiply_8x8` in `techniques/maths.md`.
|
|
67
|
+
- Perspective divide (reciprocal table lookup): approximately 15–20 cycles.
|
|
68
|
+
- Sprite register writes (X, Y, pointer, color): approximately 20 cycles per sprite, all 8 sprites written in the vertical blank = ~160 cycles total for sprite output.
|
|
69
|
+
- Charset-based output (write one char cell + screen RAM byte): approximately 8–12 cycles per point.
|
|
70
|
+
|
|
71
|
+
For a 32-point cloud at ~75 cycles per point: approximately 2400 cycles per frame computation, well within the ~19,500 available on PAL. Frame rate is typically 25fps (every other PAL frame) for 32 points, or every frame for 16-point clouds.
|
|
72
|
+
|
|
73
|
+
### Recipes
|
|
74
|
+
|
|
75
|
+
- No recipe yet. (An earlier version of this page pointed at `recipes/kickassembler/cracktro-template.md`; that recipe has no sprite layer and no dot rotator — its only rotation is of the bar palette.)
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## vector_balls_sprites — Eight sprite balls on a tilted ring, depth-sorted onto the VIC's fixed sprite priority
|
|
80
|
+
|
|
81
|
+
**Complexity:** medium
|
|
82
|
+
**Region:** both
|
|
83
|
+
**Uses registers:** D000, D001, D002, D003, D004, D005, D006, D007, D008, D009, D00A, D00B, D00C, D00D, D00E, D00F, D012, D015, D017, D01B, D01C, D01D, D027, D028, D029, D02A, D02B, D02C, D02D, D02E
|
|
84
|
+
**Uses kernal:** (none)
|
|
85
|
+
**Requires:** dot_3d_rotator, fixed_point_8_8
|
|
86
|
+
**Cost:** cycles_per_frame=1389, cycles_per_frame_typical=1318, sprites_per_line=5
|
|
87
|
+
**Cost basis:** measured-vice
|
|
88
|
+
**Cost measured on:** kickassembler-vector-balls (worst is frame 1, the six swaps that settle the start order; typical is frame 300 with no swap; sprites per line is the most balls sharing a raster line over the 300 frames, counted from the tables)
|
|
89
|
+
|
|
90
|
+
### Why
|
|
91
|
+
|
|
92
|
+
Vector balls are the sprite-plot form of `dot_3d_rotator` with the one
|
|
93
|
+
thing a dot cloud never has to face: the points have area, so they
|
|
94
|
+
overlap, and an overlap drawn in the wrong order breaks the depth the
|
|
95
|
+
motion was selling. A bitmap renderer would sort and mask. Sprites do
|
|
96
|
+
not need the mask, because the VIC already decides every overlap
|
|
97
|
+
between two sprites by their numbers; what the CPU has to supply is the
|
|
98
|
+
sort, and then the right numbering. Eight balls cost eight lookups, one
|
|
99
|
+
bubble pass and thirty-two register writes a frame, all of it in the
|
|
100
|
+
vertical blank, which leaves the display period free for whatever else
|
|
101
|
+
the part is doing.
|
|
102
|
+
|
|
103
|
+
### How
|
|
104
|
+
|
|
105
|
+
Three tables, built by the assembler and indexed by a byte angle `a`
|
|
106
|
+
in 0 to 255:
|
|
107
|
+
|
|
108
|
+
- `px[a] = 172 + 40 sin(a) * 256 / (40 cos(a) + 200)`, the screen x of
|
|
109
|
+
a point on a ring of radius 40 in the XZ plane seen from 200 units in
|
|
110
|
+
front of its centre, and `py[a]` the same with amplitude 12 and centre
|
|
111
|
+
130, which lifts and drops the ring so it reads as tilted. Both are
|
|
112
|
+
bytes: px runs 120 to 224, so with a 24-pixel image no sprite reaches
|
|
113
|
+
X 255 and `$D010` is never touched.
|
|
114
|
+
- `zd[a] = 128 + 40 cos(a)`, the depth byte, 88 to 168. The eye is on
|
|
115
|
+
the negative z side, so a larger zd is farther away.
|
|
116
|
+
|
|
117
|
+
Ball k at frame t is at `a = (t + 32k) & 255`: eight balls a 32nd of a
|
|
118
|
+
turn apart, the ring turning one 256th of a turn a frame. Each frame,
|
|
119
|
+
after the beam has left the display:
|
|
120
|
+
|
|
121
|
+
1. Lookup: for each k, read px, py and zd into three eight-byte arrays
|
|
122
|
+
indexed by ball.
|
|
123
|
+
2. Sort: one bubble pass over `order`, the ball index per depth rank
|
|
124
|
+
kept from the previous frame, swapping adjacent entries whose zd are
|
|
125
|
+
the wrong way round, farthest first. One pass is enough because two
|
|
126
|
+
balls only change depth order by crossing, an adjacent swap, and the
|
|
127
|
+
ring moves one step a frame; the recipe checks the order after every
|
|
128
|
+
pass and counts the frames it left unsorted (six of 300, all of them
|
|
129
|
+
the reversed start order settling, then none).
|
|
130
|
+
3. Assign: rank i goes to hardware sprite 7 - i. For each rank write
|
|
131
|
+
the ball's X and Y into that sprite's position pair, its pointer from
|
|
132
|
+
its depth (the 24 by 21 image for zd below 128, a 16 by 14 image
|
|
133
|
+
centred in the sprite for 128 and above, two sprite blocks at $2000
|
|
134
|
+
and $2040, pointers $80 and $81) and its colour from two thresholds
|
|
135
|
+
on zd: white below 120, light grey from 120 to 135, grey from 136 up.
|
|
136
|
+
|
|
137
|
+
The eight sprites stay enabled, unexpanded and in front of the
|
|
138
|
+
background throughout; nothing in `$D015`, `$D017`, `$D01B` or `$D01D`
|
|
139
|
+
changes after setup.
|
|
140
|
+
|
|
141
|
+
### Why it works
|
|
142
|
+
|
|
143
|
+
Sprite-to-sprite priority on the VIC is fixed: sprite 0 is drawn over
|
|
144
|
+
sprite 1, 1 over 2, down to 7, and no register alters it
|
|
145
|
+
(`hardware/vic-ii-reference.md`, "Priority"). So the priorities cannot be
|
|
146
|
+
moved between the balls, but the balls can be moved between the
|
|
147
|
+
priorities: a sprite is only a position, a pointer and a colour, and
|
|
148
|
+
rewriting those three for all eight every frame gives each hardware
|
|
149
|
+
sprite whichever ball is at its depth rank. Because the hardware then
|
|
150
|
+
resolves every overlapping pixel by number, and the numbers are the
|
|
151
|
+
depth order, the nearer ball wins every overlap without the program
|
|
152
|
+
ever looking at a pixel. The recipe's control build leaves the sort out
|
|
153
|
+
and keeps sprite k on ball k; on the pinned frame it draws a ball of
|
|
154
|
+
depth 147 over balls of depth 116 and 93, and the sorted build draws
|
|
155
|
+
them the other way.
|
|
156
|
+
|
|
157
|
+
The projection is a division by depth, but the depth of a point on the
|
|
158
|
+
ring is a function of its angle alone, so the whole projection is a
|
|
159
|
+
function of the angle and the assembler evaluates it once. That is the
|
|
160
|
+
same move `fixed_point_8_8` makes for a position: keep the precision in
|
|
161
|
+
a table, spend bytes rather than cycles.
|
|
162
|
+
|
|
163
|
+
### Cycle budget
|
|
164
|
+
|
|
165
|
+
Measured in VICE x64sc 3.10 with CIA1 timer A in the `vector-balls`
|
|
166
|
+
recipe, the same on PAL and NTSC:
|
|
167
|
+
|
|
168
|
+
- Lookup, sort pass and the thirty-two register writes, no swap: 1,318
|
|
169
|
+
cycles (frame 300). With the six swaps of the first frame: 1,389, the
|
|
170
|
+
worst frame of the run.
|
|
171
|
+
- The control with the sort left out: 1,129, so the swap-free pass is
|
|
172
|
+
189 cycles and each swap about 12 (arithmetic from the measured
|
|
173
|
+
frames).
|
|
174
|
+
- About 21 raster lines in all, from line 255; the blank has room for
|
|
175
|
+
it several times over on either model.
|
|
176
|
+
|
|
177
|
+
The timer starts before the lookup and stops after the last colour
|
|
178
|
+
write, so about eight cycles of its own instructions are inside the
|
|
179
|
+
figure and not removed.
|
|
180
|
+
|
|
181
|
+
### Variations
|
|
182
|
+
|
|
183
|
+
**More balls.** Eight is the hardware count. `sprite_multiplex_8`
|
|
184
|
+
(`techniques/sprite.md`) reuses sprites down the frame, but a
|
|
185
|
+
multiplexer sorts by Y to schedule its slots and this effect sorts by
|
|
186
|
+
depth to number them, and the two orders disagree wherever a far ball
|
|
187
|
+
is lower on the screen than a near one; a design that wants both has to
|
|
188
|
+
give priority to the raster order and accept, or hide with the layout,
|
|
189
|
+
the depth errors it causes. Not built here.
|
|
190
|
+
|
|
191
|
+
**A second ring.** Two rings share the tables if they share radius and
|
|
192
|
+
tilt; give the second its own angle offset and a different centre, or
|
|
193
|
+
the same centre and the angle stepped the other way. With sixteen balls
|
|
194
|
+
that is the multiplexing problem above; with four and four it is this
|
|
195
|
+
technique with a second lookup loop.
|
|
196
|
+
|
|
197
|
+
**Ball size from depth.** A third image between the two, or several,
|
|
198
|
+
picked by more thresholds on zd, makes the approach to the eye read as
|
|
199
|
+
growth rather than a jump. A sprite pointer costs the same to write
|
|
200
|
+
whatever it points at.
|
|
201
|
+
|
|
202
|
+
### Pitfalls
|
|
203
|
+
|
|
204
|
+
None of this page's pitfall entries is met by the recipe as built: no
|
|
205
|
+
X reaches 255, so `sprite_x_high_bit_wrong_register` does not arise
|
|
206
|
+
(the listing's `.errorif` refuses a table that would), and eight balls
|
|
207
|
+
on eight sprites cannot overrun a line. A ball whose sprite Y is left
|
|
208
|
+
at the reset value of 0 is matched by the VIC at raster line 256 as
|
|
209
|
+
well as at line 0, and takes DMA there; the recipe's first timing
|
|
210
|
+
figure was wrong by 451 cycles for that reason and the setup now parks
|
|
211
|
+
every Y inside the display.
|
|
212
|
+
|
|
213
|
+
### Recipes
|
|
214
|
+
|
|
215
|
+
- `recipes/kickassembler/vector-balls.md`: the three tables, the pass,
|
|
216
|
+
the assignment, two overlapping pairs read off the pinned picture with
|
|
217
|
+
their depths, the control with the sort left out, and the unsorted
|
|
218
|
+
frame count.
|
|
219
|
+
|
|
220
|
+
### Sources
|
|
221
|
+
|
|
222
|
+
- `hardware/vic-ii-reference.md`, "Priority": the fixed sprite-to-sprite
|
|
223
|
+
order the assignment relies on.
|
|
224
|
+
- `techniques/maths.md`, `fixed_point_8_8`: precision kept in a table
|
|
225
|
+
rather than computed.
|
|
226
|
+
- `techniques/sprite.md`, `sprite_multiplex_8`: the route to more than
|
|
227
|
+
eight balls and why its order is not this one.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## solid_vector_3d — Solid-shaded polygon rendering
|
|
232
|
+
|
|
233
|
+
**Complexity:** scene-tier
|
|
234
|
+
**Region:** both
|
|
235
|
+
**Uses registers:** D011, D018, D016, DD00
|
|
236
|
+
|
|
237
|
+
### Why
|
|
238
|
+
|
|
239
|
+
Filled polygon rendering transforms the 3D wireframe aesthetic into something that more closely resembles a solid object. Instead of dots tracing the surface geometry, filled faces show which polygons are visible (front-facing) and shade them with a flat color derived from the polygon's orientation relative to a light source. The result — a rotating cube or pyramid whose faces shade and swap visibility as it turns — is the canonical "solid vector" demoscene effect. It appears in virtually every major C64 demo from the late 1980s onward.
|
|
240
|
+
|
|
241
|
+
The technique is more demanding than the dot rotator because filled polygons require two additional operations beyond point projection: face visibility determination (do not draw back-facing polygons) and scanline fill (for each visible face, fill every horizontal span between the face's left and right edges on each display row).
|
|
242
|
+
|
|
243
|
+
### How
|
|
244
|
+
|
|
245
|
+
**Face culling.** For each polygon face (triangle or quad), compute the face normal vector after rotation. If the Z component of the normal points away from the viewer (in the standard camera-at-origin setup, this means the normal Z is positive, assuming a left-handed coordinate system), the face is back-facing and not drawn. This eliminates roughly half the polygons in a convex solid without any rendering work.
|
|
246
|
+
|
|
247
|
+
**Z-sorting.** For transparent or non-convex objects, sort the remaining visible faces by their average Z depth (painter's algorithm: draw farthest first). This avoids visible-surface errors without a depth buffer. For a simple convex solid, Z-sorting is not necessary because correct back-face culling already prevents overlap errors.
|
|
248
|
+
|
|
249
|
+
**Scanline fill.** For each visible face, scan the face's edges to build a left-edge and right-edge table: for each row Y spanned by the face, record the leftmost X (left edge) and rightmost X (right edge). Then fill each row by writing pixels from left X to right X in bitmap RAM. The fill loop is the performance-critical inner loop.
|
|
250
|
+
|
|
251
|
+
The VIC-II bitmap mode is used for output. Standard bitmap mode ($D011 bit 5 set, $D011 bit 6 clear, $D016 bit 4 clear) provides 320×200 pixels at 1 bit per pixel, with one foreground and one background color per 8×8 character cell. Multicolor bitmap mode ($D016 bit 4 set) provides 160×200 pixels at 2 bits per pixel with four colors per cell, commonly used for colored polygon faces. The screen is cleared (or back-buffer swapped) before each new frame.
|
|
252
|
+
|
|
253
|
+
### Why it works
|
|
254
|
+
|
|
255
|
+
The fill loop has to respect the VIC-II's bitmap memory layout, which is cell-major, not a linear framebuffer: cell (col,row) occupies 8 consecutive bytes at (row*40+col)*8, one byte per scanline of the cell, so horizontally adjacent bytes on one scanline are 8 bytes apart (see `bitmap-modes.md`; an earlier version of this paragraph described the bitmap as row-major, left to right, top to bottom, which would make a `STA base,X` walk fill a vertical 8-line strip instead of a row). Filling a horizontal span of N pixels from column Xl to column Xr on scanline Y requires computing the byte addresses for Xl and Xr, masking the partial bytes at each end, and filling the interior bytes with $FF (all pixels on for the face color). A span fill therefore either steps X by 8 along a scanline, or (the fast form) fills a cell column with eight consecutive `STA base+0..7` stores, and the interior needs only one STA per 8 (hires) or 4 (multicolour) pixels with $FF or the fill pattern pre-loaded in A.
|
|
256
|
+
|
|
257
|
+
Color RAM for multicolor mode is updated once per visible face (one byte per 8×8 cell the face overlaps), not once per pixel. For a quad-polygon face covering 32×32 pixels, color RAM updates hit 16 cells — 16 stores — against a pixel fill covering potentially 128 bytes. The fill dominates the cycle budget.
|
|
258
|
+
|
|
259
|
+
Frame rate is the key constraint. A typical C64 solid vector demo running on PAL operates at 12.5fps (rendering a new frame every 4 VIC-II frames). At this rate, the CPU has approximately 4 × 19,600 = 78,400 cycles per rendered frame. A cube shows at most three faces after back-face culling. With each visible face averaging 50 scanlines of 80–160-pixel fill (10–20 bytes per row), at 5 cycles per interior byte for an unrolled `STA abs,X` fill (7 if each byte is `LDA #$FF : STA abs,X`) plus ~30 cycles per row for edge masking and pointer advance: roughly 4,000–8,500 cycles per face, 12,000–25,000 cycles per frame for fill, leaving ample room in the 78,400 available at 12.5fps. (An earlier version of this paragraph costed six faces at ~8 cycles per byte and disagreed with the Cycle budget below by a factor of two; the per-byte figures now follow the 6510 reference.) Increasing to 12 faces (icosahedron) or larger polygons begins to strain the budget.
|
|
260
|
+
|
|
261
|
+
### Variations
|
|
262
|
+
|
|
263
|
+
**Flat shading with Lambert lighting.** Compute the dot product of each face's (rotated) normal with a fixed light-direction vector. Map the result to a VIC-II color index. The face is filled in that color. On the C64, this is typically done with a 16-entry lookup table (angle range → color index), approximating continuous shading with the available palette.
|
|
264
|
+
|
|
265
|
+
**Double buffering.** Maintain two bitmap areas in VIC bank memory. While one is displayed, the other is cleared and redrawn. Swap $D018 in the vertical blank. Eliminates partial-frame updates visible as tearing. A full-height (25-row) double buffer does not fit one 16 KB bank, as an earlier version of this paragraph implied: the bitmap can only sit at offset $0000 or $2000, so two 8,000-byte bitmaps leave two 192-byte gaps and no 1 KB-aligned slot for the 1,000-byte video matrix (see `bitmap-modes.md`, Koala animation). The usual layout is therefore two VIC banks, each holding one bitmap at offset $2000 plus its own matrix (and its own copy of sprite pointers and sprite data), with the swap a $DD00 bank write alongside $D018. A single-bank alternative is to draw only 22 character rows (7,040 bytes) so the matrix fits at offset $1C00 or $3C00 in the same bank and hide the bottom three rows with a raster split or the border; this only works in banks 1 or 3, because in banks 0 and 2 the VIC reads character ROM at $1000-$1FFF, where the offset-$0000 bitmap would lie. Neither scheme double-buffers Colour RAM at $D800, which is a single 1 KB, so in multicolor mode per-cell colour changes are written once per swap.
|
|
266
|
+
|
|
267
|
+
**Outline vectors (wireframe).** Skip the fill loop entirely; draw only the polygon edges as lines. Each edge is Bresenham line-drawn into the bitmap. Wireframe rendering is 10–20× faster than filled, enabling higher polygon counts or higher frame rates.
|
|
268
|
+
|
|
269
|
+
### Cycle budget
|
|
270
|
+
|
|
271
|
+
Critical inner loop (fill loop, multicolor bitmap, 160-pixel-wide row):
|
|
272
|
+
|
|
273
|
+
- A fully covered 160-pixel hires row is 20 bytes (a full 160-pixel multicolour row is 40 bytes, 4 double-wide pixels per byte).
|
|
274
|
+
- Interior fill with A pre-loaded: `STA abs,X` is 5 cycles per byte = 100 cycles per 20 bytes (7 cycles per byte if the `LDA #imm` is repeated: `LDA #imm` 2 + `STA abs,X` 5; `STA abs,X` never takes a page-cross penalty). So 100–140 cycles per fully-covered row; an earlier version of this line said 6 cycles × 20 bytes = 120.
|
|
275
|
+
- Left/right partial-byte masking: approximately 20–30 cycles per row.
|
|
276
|
+
- Row pointer advancement: approximately 8 cycles per row.
|
|
277
|
+
- Total per-row: approximately 130–180 cycles on a non-badline.
|
|
278
|
+
- On a badline (every 8th row): add 40 cycles = 170–220 cycles per row.
|
|
279
|
+
- A 50-row, 20-byte face: approximately 7,000–9,000 cycles (approximate; depends on face width and badline distribution).
|
|
280
|
+
|
|
281
|
+
Per-frame total (cube, 3 visible faces): approximately 12,000–25,000 cycles for fill alone, depending on face width. Comfortable within the ~78,400 available at 12.5fps; at 25fps (~39,200 cycles) it fits but leaves little for edge tables and matrix math. (An earlier version of this paragraph costed a 6-face cube at 45,000–50,000 cycles; a convex cube shows at most three faces after culling.) Most scene-tier demos choose 12.5fps or 25fps based on polygon complexity.
|
|
282
|
+
|
|
283
|
+
### Recipes
|
|
284
|
+
|
|
285
|
+
- No recipe yet. (An earlier version of this page pointed at `recipes/kickassembler/cracktro-template.md`; it contains no filled-box or vector sequence.)
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## bobs_effect — BOB (Blitter OBject) parallel-sprite-like effect
|
|
290
|
+
|
|
291
|
+
**Complexity:** scene-tier
|
|
292
|
+
**Region:** both
|
|
293
|
+
**Uses registers:** D011, D018
|
|
294
|
+
|
|
295
|
+
### Why
|
|
296
|
+
|
|
297
|
+
The C64's hardware sprites are limited to 8 objects per frame, each 24 pixels wide. Many demoscene effects — particularly those popularized on the Amiga and later ported to or inspired on the C64 — require dozens of independently-positioned, independently-animated graphic objects on screen simultaneously. BOBs (Blitter OBjects, a term borrowed from the Amiga's blitter hardware but applied to any system using software-composited screen RAM objects) are the C64 solution: pre-render each "sprite-like" object into a block of screen RAM or character data, then position and display it as a character cell (or group of cells) rather than a hardware sprite.
|
|
298
|
+
|
|
299
|
+
This removes the 8-sprite hardware limit. The screen can hold as many BOBs as there are character cells to place them in, subject only to the CPU's ability to update those cells each frame.
|
|
300
|
+
|
|
301
|
+
### How
|
|
302
|
+
|
|
303
|
+
BOBs are implemented in bitmap mode or character mode. The two approaches differ in their flexibility and speed:
|
|
304
|
+
|
|
305
|
+
**Character-mode BOBs.** Each BOB occupies one or more 8×8 character cells. Each frame, the CPU writes the appropriate character indices into the screen RAM cells at the BOB's position, then writes the visual data for those characters into character RAM (a region of VIC bank memory not currently used for display). If the character RAM is pre-populated with all required animation frames, the per-frame update is only the screen RAM writes — one byte per cell — extremely fast. For a 16×16 pixel BOB (4 cells), this is 4 screen RAM writes = approximately 20 cycles per BOB. One hundred BOBs can be updated in under 2,000 cycles.
|
|
306
|
+
|
|
307
|
+
**Bitmap-mode BOBs.** The BOB's pixel data is blitted (copied with masking) directly into the bitmap RAM. A 16×16 BOB requires reading a 16-byte mask buffer, ORing with the existing bitmap data at the destination, and writing back — a masked blit. This allows pixel-accurate positioning (not cell-aligned) at the cost of much higher CPU cycles per BOB: a 16×16 masked blit is approximately 200–300 cycles per BOB. The advantage is full-resolution placement.
|
|
308
|
+
|
|
309
|
+
In both cases, erasing each BOB between frames is the bottleneck. Character-mode BOBs require restoring the background character indices at the old positions before drawing at the new positions. Bitmap-mode BOBs require re-clearing the blitted area. A common technique is to maintain a list of "dirty cells" or "dirty regions" from the previous frame and clear only those, rather than clearing the entire screen.
|
|
310
|
+
|
|
311
|
+
### Why it works
|
|
312
|
+
|
|
313
|
+
The VIC-II displays the screen using screen RAM (character indices) and character RAM (pixel data for those indices) or bitmap RAM. Because both are writable by the CPU at any time, the CPU can modify the displayed image dynamically. The VIC-II reads screen RAM (the character codes, together with Colour RAM) during the badline c-accesses, once per character row, and reads the pixel data (character generator bytes, or bitmap bytes in bitmap mode) in the g-accesses on every visible line. A screen-RAM write therefore shows only from the next badline for that row — in practice the next frame if that row's badline has already passed; a character-RAM or bitmap write shows at the next g-access of that pixel row, which is the next raster line if that row of the cell has not yet been drawn this frame, otherwise the next frame. (An earlier version of this sentence had the two fetches swapped, saying badlines read character RAM.) For BOBs, the simplest approach is to update all BOBs during the vertical blank (the interval between frames where the VIC is not actively rendering), guaranteeing all updates take effect on the next frame.
|
|
314
|
+
|
|
315
|
+
The critical limitation: **BOBs leave background residue.** When a BOB moves from position A to position B, the pixel data from the BOB remains in screen RAM at position A until explicitly erased. The programmer must track old positions and clear them each frame. Failure to do so produces the characteristic "smearing" artifact. Maintaining a double-buffered list of dirty regions and processing it systematically each frame is the standard solution.
|
|
316
|
+
|
|
317
|
+
### Variations
|
|
318
|
+
|
|
319
|
+
**Pre-multiplied character set.** For a single BOB image that appears many times (e.g., a starfield of identical dots), pre-populate a single character definition and write that character index to all screen RAM positions. Zero erase cost (the entire character set stays static) and minimal update cost.
|
|
320
|
+
|
|
321
|
+
**Animated BOBs via character cycling.** Change the character RAM content (rather than the screen RAM indices) each frame. All cells pointing to that character index change simultaneously. Efficient for animated foreground elements where every instance animates in lock-step — explosion particles, flame effects.
|
|
322
|
+
|
|
323
|
+
**Sprite + BOB composite.** Use hardware sprites for the player and a few enemies (requiring precise positioning), and BOBs for background debris, particles, or decorations. The character-mode BOBs free up the sprite layer for the objects that actually need sub-cell precision.
|
|
324
|
+
|
|
325
|
+
### Cycle budget
|
|
326
|
+
|
|
327
|
+
Character-mode BOBs (cell-aligned, pre-loaded character data):
|
|
328
|
+
|
|
329
|
+
- Screen RAM write per cell: 4 cycles (STA abs).
|
|
330
|
+
- A 16×16 BOB (4 cells): 16 cycles to draw, 16 cycles to erase at previous position = 32 cycles per BOB per frame.
|
|
331
|
+
- 50 BOBs: approximately 1,600 cycles per frame (approximately 8% of PAL frame budget).
|
|
332
|
+
|
|
333
|
+
Bitmap-mode BOBs (pixel-exact masked blit):
|
|
334
|
+
|
|
335
|
+
- 16×16 BOB, bitmasked: approximately 200–350 cycles per BOB (approximate; depends heavily on horizontal alignment and whether inner loop is unrolled).
|
|
336
|
+
- 20 BOBs: approximately 4,000–7,000 cycles per frame.
|
|
337
|
+
- Plus erase cost (similar): double the per-BOB figure.
|
|
338
|
+
|
|
339
|
+
The character-mode approach dominates for bulk object counts. The bitmap approach is used when pixel-exact positioning is essential (e.g., smooth-scrolling playfield with non-cell-aligned objects).
|
|
340
|
+
|
|
341
|
+
### Recipes
|
|
342
|
+
|
|
343
|
+
- No recipe yet. (An earlier version of this page pointed at `recipes/kickassembler/cracktro-template.md`; its logo is a static screen image copied to $0400, not character-mode BOBs.)
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## shadebobs — Additive bobs on a shade buffer shown through colour RAM
|
|
348
|
+
|
|
349
|
+
**Complexity:** medium
|
|
350
|
+
**Region:** both
|
|
351
|
+
**Uses registers:** D011, D020, D021, DC04, DC05, DC0D, DC0E
|
|
352
|
+
**Requires:** bobs_effect
|
|
353
|
+
**Cost:** cycles_per_frame=27104, cycles_per_frame_typical=590, bytes_code=1106, bytes_data=1647, zp_bytes=4
|
|
354
|
+
**Cost basis:** measured-vice
|
|
355
|
+
**Cost measured on:** kickassembler-shadebobs (the worst frame is the blob add, 590, plus the first decay pass over all 1,000 cells, 26,514, PAL, screen on; three frames in four are the add alone; bytes_data is the 1,000-byte shade buffer plus the 647 bytes of palette, mask, sine and row tables and variables in the built segment; zp_bytes the two row pointers)
|
|
356
|
+
|
|
357
|
+
### Why
|
|
358
|
+
|
|
359
|
+
A bob that overwrites leaves nothing behind, and erasing it is half its cost (`bobs_effect`). A shade bob adds instead: every cell it passes over gains a step of brightness, the steps accumulate where the path lingers or crosses itself, and a slow decay lets the trail fade. There is no erase, because there is nothing to restore; the picture is the history of where the bob has been. In character mode with colour RAM as the shade buffer's display, the whole thing is one 4-bit store per covered cell per frame, no character data and no bitmap: colour RAM is the only per-cell store that changes a cell's look without touching glyph data, so over a screen of one solid glyph the nibble is the picture, as in `fire_effect`.
|
|
360
|
+
|
|
361
|
+
### How
|
|
362
|
+
|
|
363
|
+
1. Fill screen RAM with screen code 160, the reverse space, and set `$D021` and `$D020` to black. Every pixel of every cell is set, so a cell shows its colour-RAM nibble and nothing else.
|
|
364
|
+
2. Keep a shade buffer of 40 by 25 bytes in RAM, values 0 to 15, all zero at the start.
|
|
365
|
+
3. Give the bob a shape as a mask of 0 and 1 bytes, four columns by three rows, and a position from two sine tables: the recipe uses `cx = 18 + round(16 sin t)`, `cy = 11 + round(9 sin 2t)`, one step of `t` per frame, so the path is a figure of eight that stays inside the screen without clipping. Amplitudes of 16 and 9 cannot carry out of a byte at the peak; a table of amplitude 128 can (`sine_table_peak_wraps_to_zero`, `../pitfalls/maths.md`).
|
|
366
|
+
4. Each frame, for every set mask entry, `shade = min(shade + 1, 15)` and `colour = palette[shade]`, written through two zero-page pointers set to the row's start plus `cx`, one into the buffer and one into colour RAM. The add saturates rather than wraps: a wrap would take the brightest cell back to `palette[0]`, black, and put a hole at the hottest point of the trail.
|
|
367
|
+
5. Every fourth frame, run a decay pass: every non-zero shade loses one, and every one of the 1,000 colour cells is rewritten from the buffer, row by row with the column in X so no index passes 39.
|
|
368
|
+
6. The palette is sixteen bytes mapping shade to a VIC colour in rising luminance order, `colour_fade`'s PAL ranking: 0, 6, 9, 2, 11, 8, 4, 14, 12, 5, 10, 3, 15, 13, 7, 1, black to white.
|
|
369
|
+
|
|
370
|
+
### Why it works
|
|
371
|
+
|
|
372
|
+
The shade buffer is a count of visits per cell with a ceiling, and the palette turns the count into brightness; because the palette is monotone in luminance, more visits reads as brighter, and the crossing of the path, visited twice, is the brightest point on the screen. Decay subtracts the same amount from every cell at once, so the ordering of brightnesses along the trail is kept while the whole trail dims, which is what makes it read as fading light rather than as a picture being erased. The saturating ceiling is what lets the decay be a plain subtract: nothing can be above 15, so nothing needs a clamp on the way down but zero. Colour RAM is the right display for it because a shade cell and its colour cell are the same shape, one byte per cell, and the map from one to the other is a sixteen-byte lookup.
|
|
373
|
+
|
|
374
|
+
### Cycle budget
|
|
375
|
+
|
|
376
|
+
Measured on the recipe with CIA1 timer A in VICE x64sc, screen on (badline stealing included):
|
|
377
|
+
|
|
378
|
+
- Blob add, eight set entries of a twelve-entry mask: 590 cycles PAL and NTSC. With all twelve set, 730; with none, 310 (PAL); so a set entry costs 35 cycles and a clear one 12, with about 166 cycles of row set-up around them (arithmetic from the three measurements). About nine raster lines: the add fits the vertical blank many times over.
|
|
379
|
+
- Decay pass, all 1,000 cells, first pass with 12 cells non-zero: 26,514 PAL, 27,028 NTSC. Per cell the zero path is 25 cycles and the non-zero path 33 by the instruction table, so a pass is at least 25,000 before badlines and cannot fit a PAL frame of 19,656 or an NTSC frame of 17,095, let alone the blank.
|
|
380
|
+
|
|
381
|
+
The decay pass runs once in four frames, so the average frame is under a frame (590 times 3 plus 27,104, over four, is 7,219 PAL, arithmetic) but the frame it runs on is not: with the loop synced to a line-256 crossing, a decay frame overruns into the next field and the loop picks up on the crossing after that, so that iteration takes two raster frames and the colour-RAM rewrite tears across the next field's display. In the recipe 75 of 300 iterations are decay iterations and the run is 376 PAL frames long (arithmetic from the measured done cycle). A plan that cannot take a hitch every fourth frame splits the decay across frames instead (see Variations).
|
|
382
|
+
|
|
383
|
+
### Variations
|
|
384
|
+
|
|
385
|
+
**A second bob.** Another position from two more sine reads, the same add routine: 590 more cycles a frame for the same mask, and where the two paths cross the shades stack. Two bobs on phases 128 apart draw the same figure of eight from opposite ends and meet in the middle.
|
|
386
|
+
|
|
387
|
+
**A bitmap shade bob by pixel.** Keep the buffer per pixel of a low-resolution grid and add the bob's pixel mask, then convert to the display each frame; the cell form above is the cheap version, and the pixel form pays a masked read-modify-write per pixel like a bitmap `bobs_effect` bob, plus the conversion. Not built here.
|
|
388
|
+
|
|
389
|
+
**Decay on alternate halves.** Run the decay over rows 0 to 12 on one frame and rows 13 to 24 on the next, each about half of 26,514, so no frame overruns by more than the half; or spread it across the four frames in quarters of 250 cells, about 6,600 each (arithmetic), and the add plus a quarter fits inside a PAL frame with the blank to spare. The trail then fades in bands a frame apart, which the eye does not see at a step every four frames.
|
|
390
|
+
|
|
391
|
+
### Pitfalls
|
|
392
|
+
|
|
393
|
+
`full_field_redraw_exceeds_vblank` (`../pitfalls/text-mode-render.md`): the decay pass is a full rewrite of colour RAM through an index and it does not fit a frame; running it every fourth frame keeps the average down, splitting it is the fit. `colour_ram_index_past_last_cell_hits_cia1` (same page): the decay stores by row with X at most 39, and the add's pointer is at most row 22, column 34 plus Y at most 3, so no store can reach 1,024 and CIA1; the recipe has CIA1's timer in use for the measurement, so a stray store there would show in the figures. `sine_table_peak_wraps_to_zero` (`../pitfalls/maths.md`): the position tables are amplitude 16 and 9, so the wrap cannot happen; a bob whose swing is scaled up to 128 needs one of that pitfall's two scalings.
|
|
394
|
+
|
|
395
|
+
### Recipes
|
|
396
|
+
|
|
397
|
+
- `recipes/kickassembler/shadebobs.md`: the design above, one bob, decay every fourth frame, 300 iterations then a halt, pinned at cycle 11,000,000 on both models with a per-colour cell census, a no-decay control, and the buffer checked against colour RAM and against a model of the design (0 differences).
|
|
398
|
+
|
|
399
|
+
### Sources
|
|
400
|
+
|
|
401
|
+
- The measurements on this entry are from the recipe named above, VICE x64sc 3.10, CIA1 timer A; the luminance order is `colour_fade`'s.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## plasma — Plasma effect via sine table additions
|
|
406
|
+
|
|
407
|
+
**Complexity:** medium
|
|
408
|
+
**Region:** both
|
|
409
|
+
**Uses registers:** D011, D018, D021, D022, D023, D024
|
|
410
|
+
|
|
411
|
+
### Why
|
|
412
|
+
|
|
413
|
+
The plasma effect fills the entire screen with a smoothly animated color pattern: rippling color waves that shift and interfere with each other, creating an organic, hypnotic motion with no sharp edges or hard boundaries. It is a staple of late 1980s and 1990s C64 demos and remains visually distinctive. Despite its complexity of appearance, the plasma algorithm is simple: the color at each screen cell is determined by the sum of two or more sine functions, one of which depends on X position, one on Y position, and one on time. The interaction of these functions produces the characteristic moiré-wave patterns.
|
|
414
|
+
|
|
415
|
+
### How
|
|
416
|
+
|
|
417
|
+
The screen is divided into a grid of color cells. In character mode (the most common approach), each cell corresponds to one screen RAM position and one color RAM position — a grid of 40×25 = 1,000 cells. In each rendered cell, the color is computed as:
|
|
418
|
+
|
|
419
|
+
`color = sin_table[(x * XSCALE + t) & 255] + sin_table[(y * YSCALE + t * 2) & 255]`
|
|
420
|
+
|
|
421
|
+
where `t` advances by a small constant each frame, `XSCALE` and `YSCALE` control the spatial frequency of each wave, and `sin_table` is a 256-entry table of values 0–15 (mapped to VIC-II colors). The sum of two such values (range 0–30) is used as an index into a 32-entry (or 16-entry wrapped) color palette table.
|
|
422
|
+
|
|
423
|
+
For richer plasma patterns, three or four sine terms are summed:
|
|
424
|
+
|
|
425
|
+
`color = sin(x + t) + sin(y + t * 3/2) + sin(x * 2 + y + t / 2)`
|
|
426
|
+
|
|
427
|
+
Each additional term requires an additional table lookup and addition, increasing the per-cell cycle cost linearly.
|
|
428
|
+
|
|
429
|
+
The result is written to color RAM (at $D800 in the standard VIC bank layout). Screen RAM is filled with an all-set glyph — $A0 (reverse space) from the ROM font, or a custom character of eight $FF bytes — so that the colour-RAM colour fills the whole cell. An earlier version of this paragraph said to fill the screen with spaces ($20) "so that the background color shows through"; that is backwards. In standard text mode Colour RAM sets the foreground (set-bit) pixels of a cell and the background (clear-bit) pixels are always $D021, so a plasma written to Colour RAM over space characters displays nothing but $D021. Writing to color RAM rather than screen RAM is still what makes the effect cheap: one 4-bit store per cell selects the visible colour without touching the character data.
|
|
430
|
+
|
|
431
|
+
Alternatively, for a full-color plasma that uses foreground pixels rather than background color, fill screen RAM with custom charset entries (varying dot patterns) and write both screen RAM and color RAM. This doubles the write count per cell but allows richer color patterns.
|
|
432
|
+
|
|
433
|
+
### Why it works
|
|
434
|
+
|
|
435
|
+
The VIC-II color RAM ($D800–$DBE7) stores a 4-bit color value for each of the 40×25 = 1,000 screen cells. In standard character mode, this color is applied to all foreground (set-bit) pixels in the character cell. The background color ($D021) is applied to all background (clear-bit) pixels. By using a character whose bit pattern is all ones (reverse space $A0, or a custom all-$FF glyph), every pixel of the cell takes the colour-RAM colour and the plasma is a pure Colour RAM write. (An earlier version of this section said an all-zero character shows the colour-RAM colour, then contradicted itself in the next sentence; the rule above is the one in `vic-ii-reference.md`.)
|
|
436
|
+
|
|
437
|
+
The trick for a screen-RAM-driven plasma is extended background color (ECM) mode, which provides four independently-settable background colors ($D021–$D024). Each character's high two bits (bits 7 and 6) select which of the four backgrounds to use for that cell, leaving only 6 bits of character index (64 glyphs). Colour RAM still sets the set-bit pixels in ECM, so each of the four 64-glyph banks should use a blank (all-zero) glyph — then the cell shows only the selected background register. This gives a 2-bit-per-cell color selection driven by screen RAM content, with four palette entries (the four background registers). The plasma write loop updates $D021–$D024 per line (via raster IRQ, cycling through sets of four colors) or per frame, and updates screen RAM bits 7-6 per cell. This approach is common in 4-color plasmas.
|
|
438
|
+
|
|
439
|
+
For full 16-color plasma, color RAM must be written every frame for every cell. At 1,000 color RAM writes per frame × 4 cycles each = 4,000 cycles minimum (stores only), the color RAM update loop is the dominant cost.
|
|
440
|
+
|
|
441
|
+
### Variations
|
|
442
|
+
|
|
443
|
+
**4-color ECM plasma.** Use ECM mode ($D011 bit 6 set). Four background color registers and screen RAM high-bits select color. 1,000 screen RAM writes + 4 background register writes per frame. Much faster than full color RAM updates. Limited to 4 simultaneous colors but visually convincing.
|
|
444
|
+
|
|
445
|
+
**Animated palette via raster IRQ.** Rather than updating color RAM every frame, hold color RAM static and cycle the four background registers ($D021–$D024) via a per-line raster IRQ chain. This effectively applies a different color mapping to each screen row, creating vertical color variation without per-cell CPU work. The visual result differs from a true plasma (the pattern is horizontally uniform within each row) but is extremely cycle-efficient.
|
|
446
|
+
|
|
447
|
+
**Hi-res character plasma.** Fill screen RAM with custom 8×8 dot characters whose bit patterns vary by position, and write color RAM with computed colors. The character data provides local spatial variation (pixel-level patterns within each cell), while color RAM provides per-cell color. Combined, the result has both pixel-scale and cell-scale structure — a richer visual but double the per-frame write cost.
|
|
448
|
+
|
|
449
|
+
### Cycle budget
|
|
450
|
+
|
|
451
|
+
Full 16-color plasma (40×25 color RAM update), per frame on PAL:
|
|
452
|
+
|
|
453
|
+
- Per-cell: two sine lookups (approximately 10 cycles each), one addition, one palette-table lookup, one color RAM write (4 cycles) = approximately 30–40 cycles per cell.
|
|
454
|
+
- 1,000 cells × 35 cycles = approximately 35,000 cycles per frame.
|
|
455
|
+
- PAL frame budget: ~19,600 cycles (63 × 312 = 19,656). Result: full plasma requires approximately 1.8 PAL frames of CPU time — so it renders every other frame, which at 50 frames/s is 25fps, not the "12–13fps" an earlier version of this line said. Anything over ~39,300 cycles drops to every third frame, 16.7fps.
|
|
456
|
+
|
|
457
|
+
4-color ECM plasma (1,000 screen RAM writes, no color RAM updates):
|
|
458
|
+
|
|
459
|
+
- Per-cell: one or two sine lookups, one masking/OR operation, one screen RAM write = approximately 20–25 cycles per cell.
|
|
460
|
+
- 1,000 cells × 22 cycles = approximately 22,000 cycles per frame.
|
|
461
|
+
- Achievable at 25fps (every other PAL frame, with ~10,000 cycles to spare for other work).
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## dot_flag_sine_plotter — A grid of dots on two sines, plotted and erased every frame through the hires plot
|
|
466
|
+
|
|
467
|
+
**Complexity:** medium
|
|
468
|
+
**Region:** both
|
|
469
|
+
**Uses registers:** D011, D016, D018
|
|
470
|
+
**Uses kernal:** (none)
|
|
471
|
+
**Requires:** hires_plot, standard_bitmap
|
|
472
|
+
**Cost:** cycles_per_frame=16098
|
|
473
|
+
**Cost basis:** measured-vice
|
|
474
|
+
**Cost measured on:** kickassembler-dot-flag (the worst of 299 display-on PAL frames; 15,233 with the display blanked)
|
|
475
|
+
**Cost includes:** hires_plot
|
|
476
|
+
|
|
477
|
+
### Why
|
|
478
|
+
|
|
479
|
+
The dot flag is the smallest effect that makes a bitmap plot earn its
|
|
480
|
+
keep: a grid of single pixels, each column swung left and right by one
|
|
481
|
+
sine and each dot lifted and dropped by another, so the grid ripples
|
|
482
|
+
like a flag in a wind. It has no lines, no fill and no colour, which
|
|
483
|
+
means every cycle of the frame is either the plot primitive or the work
|
|
484
|
+
of undoing it, and a routine that can carry 128 of these at the frame
|
|
485
|
+
rate can carry the points of a vector object. It is also where the two
|
|
486
|
+
costs an animated bitmap always has, putting pixels down and taking the
|
|
487
|
+
old ones up, are seen side by side and can be measured apart.
|
|
488
|
+
|
|
489
|
+
### How
|
|
490
|
+
|
|
491
|
+
Four tables, all built by the assembler:
|
|
492
|
+
|
|
493
|
+
- `sinx`, 256 signed bytes of `round(100 * sin)`, and `siny`, 256
|
|
494
|
+
signed bytes of `round(60 * sin)`. Both amplitudes are below 128, so
|
|
495
|
+
no entry reaches the edge of a signed byte and nothing wraps
|
|
496
|
+
(`sine_table_peak_wraps_to_zero` in `pitfalls/maths.md` is the
|
|
497
|
+
unsigned amplitude-128 table whose peak comes out as 0). The recipe
|
|
498
|
+
stores `siny` twice over so that a fixed offset added to the table's
|
|
499
|
+
base can be indexed by a whole byte without running off the end.
|
|
500
|
+
- The row address table and the mask table of `hires_plot`, each in its
|
|
501
|
+
own page, and an inverted copy of the mask table for the erase.
|
|
502
|
+
- A 40-entry table of `column * 8`, the byte offset of a column's cell
|
|
503
|
+
within a bitmap row, in two bytes because it reaches 312.
|
|
504
|
+
|
|
505
|
+
Dot (i, j) of the 16 by 8 grid is at `x = 160 + sinx[(t + 12 * i) & 255]`
|
|
506
|
+
and `y = 100 + siny[(2 * t + 24 * j + 6 * i) & 255]`; t advances by 2 a
|
|
507
|
+
frame. The 12 spreads the columns over most of a period so the whole
|
|
508
|
+
flag bends; the 24 does the same for the rows; the `6 * i` term in y is
|
|
509
|
+
what makes the ripple run diagonally across the grid rather than up and
|
|
510
|
+
down in step; the `2 * t` makes the vertical wave run twice as fast as
|
|
511
|
+
the horizontal one. The amplitudes keep x in 60 to 260 and y in 40 to
|
|
512
|
+
160 (arithmetic: 160 ± 100 and 100 ± 60), so there is no clipping code.
|
|
513
|
+
|
|
514
|
+
Each frame, in this order:
|
|
515
|
+
|
|
516
|
+
1. Wait for the last display line. The erase runs in the blank.
|
|
517
|
+
2. Erase: walk the saved list of 128 bitmap addresses and inverted
|
|
518
|
+
masks, and `AND` each byte with its inverted mask. This is the
|
|
519
|
+
erase-list variation of `hires_plot`.
|
|
520
|
+
3. Plot: for each column, look up x, sign extend it to 16 bits, take
|
|
521
|
+
the mask pair from `x & 7` and the column offset from `x >> 3`; then
|
|
522
|
+
for each of the column's eight dots look up y, add the row address
|
|
523
|
+
for y to the column offset, `ORA` the mask into that byte, and store
|
|
524
|
+
the address and the inverted mask in the list. x depends only on the
|
|
525
|
+
column, so it is computed sixteen times a frame and y one hundred
|
|
526
|
+
and twenty-eight.
|
|
527
|
+
4. Advance t.
|
|
528
|
+
|
|
529
|
+
Erasing before plotting matters: a dot that lands this frame where
|
|
530
|
+
another dot sat last frame is not then wiped by that dot's erase, and
|
|
531
|
+
two dots that share a pixel are erased cleanly next frame because both
|
|
532
|
+
list entries clear the same bit.
|
|
533
|
+
|
|
534
|
+
### Why it works
|
|
535
|
+
|
|
536
|
+
A hires bitmap holds pixel (x, y) at `row[y] + (x >> 3) * 8`, bit
|
|
537
|
+
`7 - (x & 7)`, because the VIC-II fetches it cell by cell, eight
|
|
538
|
+
consecutive bytes per cell and 320 per cell row (the `hires_plot`
|
|
539
|
+
entry in `bitmap-modes.md` has the fetch order). Reading the byte,
|
|
540
|
+
`ORA` with the mask and writing it back sets the pixel without touching
|
|
541
|
+
its seven neighbours, and `AND` with the inverted mask clears it the
|
|
542
|
+
same way, so a dot can be removed without knowing what else is in the
|
|
543
|
+
byte. That is what makes the erase list possible: the list is a record
|
|
544
|
+
of exactly which bits were set, and replaying it with `AND` puts the
|
|
545
|
+
bitmap back as it was, however the dots overlapped. Screen RAM supplies
|
|
546
|
+
one ink and one paper colour per cell and is written once; `$D011` bit
|
|
547
|
+
5 turns bitmap mode on, `$D018` bit 3 puts the bitmap at `$2000` in the
|
|
548
|
+
bank and `$D016` bit 4 clear keeps it hires.
|
|
549
|
+
|
|
550
|
+
The sines are phases into a 256-entry table, so a phase is a byte, an
|
|
551
|
+
add wraps it, and the whole of the motion is table lookups and 8-bit
|
|
552
|
+
adds. The one 16-bit quantity is x, which reaches 260; it costs a sign
|
|
553
|
+
extension and a three-bit shift across two bytes, once per column.
|
|
554
|
+
|
|
555
|
+
### Variations
|
|
556
|
+
|
|
557
|
+
**A third sine for a twist.** Add a term in `i * j` or in `t` alone to
|
|
558
|
+
the y phase, or add a small `siny` term to x, and the flag folds over
|
|
559
|
+
itself rather than rippling. One more lookup and add per dot; the
|
|
560
|
+
amplitudes still have to sum to a range inside the screen.
|
|
561
|
+
|
|
562
|
+
**Sprites for the dots.** Eight hardware sprites, one per row of the
|
|
563
|
+
flag, each carrying a row of dots in its 24-pixel width, move by
|
|
564
|
+
register writes alone and need no erase; the columns then bend only as
|
|
565
|
+
far as the sprite's width allows and the rows are limited to eight
|
|
566
|
+
without a multiplexer, so the flag is small and stiff. Cheap enough for
|
|
567
|
+
a game's title screen.
|
|
568
|
+
|
|
569
|
+
**A rotozoom-style grid.** Keep the plot and the erase list and replace
|
|
570
|
+
the two sines with a rotation: x and y of dot (i, j) become
|
|
571
|
+
`cx + i * cos - j * sin` and `cy + i * sin + j * cos` with the angle
|
|
572
|
+
advancing each frame, which is the `dot_3d_rotator` with z held at
|
|
573
|
+
zero. The per-dot cost is the same as here plus two adds, because the
|
|
574
|
+
products are a per-frame table.
|
|
575
|
+
|
|
576
|
+
### Cycle budget
|
|
577
|
+
|
|
578
|
+
Measured in VICE x64sc 3.10 with CIA1 timer A in the `dot-flag`
|
|
579
|
+
recipe, overhead of an empty measurement removed:
|
|
580
|
+
|
|
581
|
+
- Erase plus plot of 128 dots, display blanked: 15,233 cycles, the same
|
|
582
|
+
on PAL and NTSC. Per dot, 15,233 / 128 = 119.0 cycles (arithmetic).
|
|
583
|
+
- The same build without the erase pass, display blanked: 10,866, so the
|
|
584
|
+
erase is 4,367 cycles, 34.1 per dot, and the plot with its per-column
|
|
585
|
+
x work and loop overhead 84.9 per dot (arithmetic from the two
|
|
586
|
+
measured frames).
|
|
587
|
+
- With the display on, the frame costs 16,005 to 16,098 cycles on PAL
|
|
588
|
+
over 299 frames and 16,261 to 16,356 on NTSC: the plot pass runs while
|
|
589
|
+
the VIC is fetching and its badline stalls are counted by the timer.
|
|
590
|
+
Both fit a frame (19,656 on PAL, 17,095 on NTSC, arithmetic) so the
|
|
591
|
+
effect runs at the frame rate on both models, with about 700 cycles
|
|
592
|
+
to spare on NTSC.
|
|
593
|
+
|
|
594
|
+
How many dots fit a frame at this per-dot cost, from the blanked figure
|
|
595
|
+
and with nothing else in the frame (arithmetic, not built): 19,656 / 119
|
|
596
|
+
is 165 on PAL and 17,095 / 119 is 143 on NTSC, and the display-on
|
|
597
|
+
figures say to take about 1,000 cycles off each for badline stalls,
|
|
598
|
+
which is roughly 156 and 135. Beyond that the effect renders every other
|
|
599
|
+
frame.
|
|
600
|
+
|
|
601
|
+
The frame does not fit the blank. The erase, about 69 lines, does; the
|
|
602
|
+
plot, about 172 lines, runs on into the display window, and a dot whose
|
|
603
|
+
row the beam has already passed is missing from that field and present
|
|
604
|
+
from the next. That is the mechanism of `full_field_redraw_exceeds_vblank`
|
|
605
|
+
in `pitfalls/text-mode-render.md`, and the recipe's mid-motion picture
|
|
606
|
+
shows it as 91 lit pixels of 128. The cure the pitfall names, a second
|
|
607
|
+
buffer swapped by `$D018` in the blank, costs a second 8,000-byte bitmap
|
|
608
|
+
and a second erase list; the recipe does not build it.
|
|
609
|
+
|
|
610
|
+
### Recipes
|
|
611
|
+
|
|
612
|
+
- `recipes/kickassembler/dot-flag.md`: the tables, the erase list, the
|
|
613
|
+
two sines, every frame CIA-timed with the display blanked and on, the
|
|
614
|
+
128 pixels counted back out of the pinned picture, and the build
|
|
615
|
+
without the erase pass as the control (3,570 lit pixels after 300
|
|
616
|
+
frames).
|
|
617
|
+
|
|
618
|
+
### Sources
|
|
619
|
+
|
|
620
|
+
- `techniques/bitmap-modes.md`, `hires_plot`: the row table, the mask
|
|
621
|
+
table, the 63-cycle plot and the erase-list variation this technique
|
|
622
|
+
is built from.
|
|
623
|
+
- `pitfalls/maths.md`, `sine_table_peak_wraps_to_zero`: why the
|
|
624
|
+
amplitudes are 100 and 60 and the tables signed.
|
|
625
|
+
- `pitfalls/text-mode-render.md`, `full_field_redraw_exceeds_vblank`:
|
|
626
|
+
the 107-line, roughly 6,700-cycle blank the plot pass overruns.
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## fire_effect — Colour-RAM heat map with decay and a luminance-ordered palette
|
|
631
|
+
|
|
632
|
+
**Complexity:** medium
|
|
633
|
+
**Region:** both
|
|
634
|
+
**Uses registers:** D011, D020, D021, DC04, DC05, DC0D, DC0E
|
|
635
|
+
**Requires:** lfsr_random
|
|
636
|
+
**Cost:** cycles_per_frame=27301, bytes_code=2548, bytes_data=1065
|
|
637
|
+
**Cost basis:** measured-vice
|
|
638
|
+
**Cost measured on:** kickassembler-fire-effect (the larger of the two half-screen updates, seed row and rows 23 to 12, PAL, screen on; the whole screen is 53,479 and the other half 26,115; bytes_data is the 1,000-byte heat map plus the 64-byte palette and the LFSR state)
|
|
639
|
+
|
|
640
|
+
### Why
|
|
641
|
+
|
|
642
|
+
A flame is the cheapest full-screen effect that reads as alive. Every cell is a number, the number cools and spreads as it rises, and the colour is looked up from the number: no character data, no bitmap, no sprites. Done in colour RAM over a screen of one solid glyph it is a thousand 4-bit stores per update plus the arithmetic, and it fills the whole display with motion from a forty-byte source row.
|
|
643
|
+
|
|
644
|
+
### How
|
|
645
|
+
|
|
646
|
+
1. Fill screen RAM with screen code 160, the reverse space, and set `$D021` and `$D020` to black. Every pixel of every cell is now a set pixel, so the cell shows its colour-RAM nibble and nothing else.
|
|
647
|
+
2. Keep a heat map of 40 by 25 bytes in RAM, values 0 to 63.
|
|
648
|
+
3. Seed row 24 each update: one 8-bit LFSR step per column (`lfsr_random`, Galois form, taps `$B8`, a constant non-zero seed), and bit 0 of the state sets the cell to 63 or 0.
|
|
649
|
+
4. For rows 23 down to 0, for every column `c`: `heat[r][c] = (heat[r+1][c-1] + heat[r+1][c] + heat[r+1][c+1] + heat[r+2][c]) / 4 - decay`, floored at 0, with decay 1. Column 0 uses itself in place of `c-1`, column 39 uses itself in place of `c+1`, and row 23 reads row 24 twice because there is no row 25. Going downwards through the rows means each row reads rows that were already updated this pass, which is what lifts the heat.
|
|
650
|
+
5. Four values of at most 63 sum to at most 252, so the sum fits one byte: `lda`, three `adc`, two `lsr`, then `beq` past a `sec` / `sbc #decay` when the quotient is already zero. No 16-bit temporary; that is why the heat range is 0 to 63.
|
|
651
|
+
6. Write `palette[heat]` into colour RAM for every cell, row by row, with the column in X: `sta $D800 + r * 40, x`. The index never passes 39, so it never reaches 1,024 and CIA1.
|
|
652
|
+
7. The palette is 64 bytes mapping heat to a VIC colour in rising luminance order. The recipe uses seven colours, black, brown, red, orange, light red, yellow, white (0, 9, 2, 8, 10, 7, 1), eight steps each for the first five and twelve for yellow and white; the choice of seven and the weighting are design decisions, the order is `colour_fade`'s luminance ranking with the non-fire hues left out.
|
|
653
|
+
8. The whole screen costs more than a frame, so run it in two halves on alternate frames: the seed row and rows 23 to 12 after one line-256 crossing, rows 11 to 0 after the next.
|
|
654
|
+
|
|
655
|
+
### Why it works
|
|
656
|
+
|
|
657
|
+
A bitmap fire has to compute a heat value per pixel and pack bits; a character fire computes one per cell and the VIC does the rest, so the arithmetic is 1,000 cells rather than 64,000 pixels and the write is one store per cell. Colour RAM is the right target because it is the only per-cell store that changes a cell's appearance without touching character data: with a solid glyph the nibble is the picture. The averaging kernel is a low-pass filter run once per row per update; heat diffuses sideways as it rises, which rounds the columns into flame shapes, and the constant decay is what makes the flame finite in height rather than a smear. The divide by four also decays, because it truncates: with decay 0 the recipe's fire still tops out at orange and light red rather than white.
|
|
658
|
+
|
|
659
|
+
Luminance order matters because the eye reads the palette as a temperature scale. A table that runs black, brown, red, orange, yellow, white is monotone in brightness, so a cell's brightness is its heat and the picture reads as a gradient; put a bright colour below a dark one and the flame gets a stripe that reads as a shape rather than heat. `colour_fade` in `transitions.md` gives the VICE PAL luminance order for all sixteen colours; the fire's seven are a subset of it.
|
|
660
|
+
|
|
661
|
+
### Cycle budget
|
|
662
|
+
|
|
663
|
+
Measured on the recipe with CIA1 timer A in VICE x64sc, screen on (badline stealing included):
|
|
664
|
+
|
|
665
|
+
- Whole screen, seed row and rows 23 to 0: 53,479 cycles PAL, 53,695 NTSC. About 2.7 PAL frames.
|
|
666
|
+
- Bottom half, seed row and rows 23 to 12: 27,301 PAL, 27,773 NTSC.
|
|
667
|
+
- Top half, rows 11 to 0: 26,115 PAL, 26,329 NTSC.
|
|
668
|
+
- Per cell in the X-indexed loop, from the listing: `clc`, `lda abs,x`, three `adc abs,x`, two `lsr`, `beq`, `sec`, `sbc #`, `sta abs,x`, `tay`, `lda abs,y`, `sta abs,x`, `inx`, `cpx`, `bne` is 51 cycles (arithmetic from the instruction table). The 950 loop cells are 48,450, the fifty edge cells at about 41 each about 2,050 and the seed row about 1,000: 51,500 before badline stealing, against the measured 53,479.
|
|
669
|
+
|
|
670
|
+
A half is still 1.3 PAL frames, so with the update synced to a line-256 crossing each half occupies two frames and the whole screen refreshes every four: 12.5 updates a second PAL, 15 NTSC. Fully unrolling the columns to absolute stores and replacing the shift and subtract with a 256-entry table would bring the per-cell cost to about 38 cycles (arithmetic, not built here): 19,000 for a half, which only just fits a PAL frame of 19,656 and does not fit an NTSC frame of 17,095.
|
|
671
|
+
|
|
672
|
+
### Variations
|
|
673
|
+
|
|
674
|
+
**2x2 cell fire with a half-height glyph pair.** Give every cell one of two custom glyphs, the top half set or the bottom half set, so a 40 by 25 screen shows 40 by 50 heat cells with two colours per character cell: the colour-RAM nibble for the set half and `$D021` for the clear half. Only one of the two halves can vary per cell, so it is a vertical resolution gain for the bright tip at the cost of a fixed black lower half; a real 2x2 needs four glyphs and the two-colour limit still holds.
|
|
675
|
+
|
|
676
|
+
**A wider kernel.** Add `heat[r+1][c-2]` and `heat[r+1][c+2]` and divide by eight (three shifts; six values of at most 63 still fit a byte at 378 only if the range is cut to 42, or keep a carry byte). Wider kernels spread the flame sideways faster and soften it; the cost is two more `adc` per cell.
|
|
677
|
+
|
|
678
|
+
**Wind.** Bias the kernel: read `c-1` twice and drop `c+1` for a flame that leans right, or swap them for left. Alternating the bias every few updates with a slow sine gives a flicker. Costs nothing extra per cell, since the kernel is the same size.
|
|
679
|
+
|
|
680
|
+
### Pitfalls
|
|
681
|
+
|
|
682
|
+
`full_field_redraw_exceeds_vblank` (`../pitfalls/text-mode-render.md`): this is a full rewrite of colour RAM every update and it does not fit a frame; the split into halves and the raster-crossing wait are the answer, and a viewer sees the tear within an update as motion because the picture is changing anyway. `colour_ram_index_past_last_cell_hits_cia1` (same page): every store is by row with X at most 39, so the index cannot reach 1,024; write the loop any other way and the first stray store hits `$DC00`. `lfsr_zero_state_lockup` (`../pitfalls/cpu.md`): the seed is a constant `$5A` and a Galois LFSR never enters zero from a non-zero state, so the lockup cannot happen unless the constant is set to zero; a seed taken from a timer or a keypress must be checked.
|
|
683
|
+
|
|
684
|
+
### Recipes
|
|
685
|
+
|
|
686
|
+
- `recipes/kickassembler/fire-effect.md`: the design above, seed `$5A`, decay 1, pinned at cycle 6,565,800 on both models with a per-colour cell census of the picture and a decay-0 control.
|
|
687
|
+
|
|
688
|
+
### Sources
|
|
689
|
+
|
|
690
|
+
- The measurements on this entry are from the recipe named above, VICE x64sc 3.10, CIA1 timer A; the luminance order is `colour_fade`'s.
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
## twister — A turning square column drawn as a band of per-line edge positions from a phase table
|
|
695
|
+
|
|
696
|
+
**Complexity:** medium
|
|
697
|
+
**Region:** both
|
|
698
|
+
**Uses registers:** D011, D012, D016, D018, D020, D021, DC04, DC05, DC0E
|
|
699
|
+
**Requires:** standard_bitmap, table_generation
|
|
700
|
+
**Cost:** cycles_per_frame=13561, bytes_code=9383, bytes_data=512
|
|
701
|
+
**Cost basis:** measured-vice
|
|
702
|
+
**Cost measured on:** kickassembler-twister (worst display-on frame, PAL, 128 lines of 8 bytes copied through a pointer; the blanked frame is 13,001 and the worst NTSC frame 13,819; bytes_code is the code segment reported by -showmem, almost all of it the unrolled copy; bytes_data is the 512-byte phase table)
|
|
703
|
+
|
|
704
|
+
### Why
|
|
705
|
+
|
|
706
|
+
A twister is a column that turns about its vertical axis while its cross-section is twisted along its height, so the faces wind down it and appear to climb as it turns. It reads as a solid 3D object, and it costs no 3D arithmetic at run time: every line of the band is a picture of the column's cross-section at one angle, there are only as many such pictures as there are angles, and they can all be drawn before the program starts. The frame loop is then a copy of one small image per line, with the image chosen by the line's angle.
|
|
707
|
+
|
|
708
|
+
### How
|
|
709
|
+
|
|
710
|
+
1. A standard bitmap at `$2000`, screen RAM at `$0400` with one colour pair in every cell, `$D011 = $3B`, `$D018 = $18`. The band is 64 pixels wide and 128 lines tall, byte columns 16 to 23 of bitmap rows 36 to 163, centred on x = 160.
|
|
711
|
+
2. **Edges from one sine.** A square column seen side on has four vertical edges; at rotation angle a (0 to 255 for a turn) edge i is at `x_i = 160 + 32 sin((a + 64 i) & 255)`, the four edges being one sine a quarter turn apart. Face i spans `x_i` to `x_(i+1)`, and it is in front when `x_i < x_(i+1)`; at most two faces pass that test at once, which is the hidden-face rule for free.
|
|
712
|
+
3. **Faces from pattern masks.** Each face has a fixed byte pattern: solid `$FF`, fifty percent `$AA`, twenty-five percent `$88`, empty `$00`. A pixel of the band is lit if some front face covers it and that face's pattern has the pixel's bit set. Four shades on a one-ink bitmap is what tells the faces apart.
|
|
713
|
+
4. **A phase table, so the frame loop is a copy.** At assembly time, for each of 64 angles a = 0, 4, .., 252, evaluate steps 2 and 3 for the 64 pixels of the band and pack them into 8 bytes: 64 images, 512 bytes, page aligned. The recipe does it with nested `.for` loops that fill a KickAssembler `List` and one `.fill 512, phases.get(i)`.
|
|
714
|
+
5. Per frame, line L of the band takes phase `(t + L) & 63`, a straight helix of one turn over 64 lines, and its 8 bytes are copied into the line's bitmap bytes: `ldy #k`, `lda (src),y`, `sta row + 8 k` eight times, then the pointer stepped by 8 with a page toggle on the carry, unrolled over the 128 lines. t advances by 1 a frame. The copy starts at raster line 250.
|
|
715
|
+
|
|
716
|
+
### Why it works
|
|
717
|
+
|
|
718
|
+
The column's appearance at any height depends only on the angle at that height, and the angle takes 64 values, so 64 images are the whole effect; the twist is nothing but the choice of which image each line gets, and moving the whole choice by one image a frame is the rotation. The per-line work is therefore the smallest thing a bitmap line can cost, eight loads and eight stores, and the sine, the face test and the patterns are paid once by the assembler. The straight helix comes from the phase being linear in the line; the fixed patterns come from evaluating them per pixel at build time rather than per row at run time, which is why the fifty percent face is `$AA` on every row and not a checkerboard, so that one image can serve any line.
|
|
719
|
+
|
|
720
|
+
### Cycle budget
|
|
721
|
+
|
|
722
|
+
Measured on the recipe with CIA1 timer A in VICE x64sc 3.10:
|
|
723
|
+
|
|
724
|
+
- The 128-line copy, display blanked: 13,001 cycles, both models; 101.6 a line (arithmetic).
|
|
725
|
+
- Display on, frames 1 to 299: 13,557 to 13,561 PAL, 13,813 to 13,819 NTSC. Both fit a frame, with about 6,100 cycles to spare on PAL and 3,300 on NTSC (arithmetic against 19,656 and 17,095).
|
|
726
|
+
- The control with no per-line phase step (`STRAIGHT`), blanked: 11,305; the pointer advance is the 1,696 between them, 13.25 a line.
|
|
727
|
+
- Per line from the instruction table: 8 by (2 + 5 + 4) = 88 for the copy, 13 for the advance, 20 on a page crossing; 128 lines of 101 plus the setup is 12,960 before the crossings, against the measured 13,001.
|
|
728
|
+
|
|
729
|
+
The copy exceeds the vertical blank (about 6,700 cycles on PAL after line 250) and runs on into the display, but it stays ahead of the beam: when the beam reaches the band's first line about 92 of the 128 lines are written, and at 63 cycles a line against 102 the beam would need 242 lines to catch up (arithmetic from the measured cost). A phase table laid out by plane, eight 64-byte tables indexed by the phase in X with `lda plane_k,x`, would bring a line to about 70 cycles (arithmetic, not built).
|
|
730
|
+
|
|
731
|
+
### Variations
|
|
732
|
+
|
|
733
|
+
**A bending helix.** Replace `t + L` with `t + L + sin2[(t + L) & 255]` or a slower sine of L alone, so the pitch of the twist varies down the column and the column seems to bend. It costs one more table read per line if the phase is worked per line at run time, or nothing if a 128-entry per-frame phase list is built in the blank first.
|
|
734
|
+
|
|
735
|
+
**Colour per face through screen RAM.** Screen RAM gives one colour pair per 8 by 8 cell, so a face can only be coloured where a whole cell is inside it; an edge that falls inside a cell splits that cell between two faces that must share one ink. With four faces and 8-pixel cells the only clean form is a colour per cell column that changes as the edge passes the cell boundary, which is a second per-frame write of 8 cells per cell row and still shows a one-cell step at each edge. Multicolour bitmap mode gives two more inks at half the horizontal resolution and the same cell limit.
|
|
736
|
+
|
|
737
|
+
**Two columns.** A second band with its own t, or the same t with an offset, is a second copy at the same per-line cost; two 64-pixel bands are 26,000 cycles a frame at this listing's cost (arithmetic), more than a PAL frame, so two columns want the plane layout or alternate-frame updates.
|
|
738
|
+
|
|
739
|
+
### Pitfalls
|
|
740
|
+
|
|
741
|
+
`full_field_redraw_exceeds_vblank` (`../pitfalls/text-mode-render.md`): the copy is 13,001 cycles against a blank of about 6,700, so it does run in the display; it does not tear only because it starts at line 250 and stays ahead of the beam for a band that begins at row 36, and a taller band or a slower copy would be caught (the recipe gives the arithmetic). `sine_table_peak_wraps_to_zero` (`../pitfalls/maths.md`) is about an unsigned table of amplitude 128 about 128; here the amplitude is 32 about 160, the values run 128 to 192, and the edges are compared in script and never stored as bytes, so it does not arise.
|
|
742
|
+
|
|
743
|
+
### Recipes
|
|
744
|
+
|
|
745
|
+
- `recipes/kickassembler/twister.md`: the design above, pinned at cycle 12,000,000 on both models, with the edges of three rows measured against the table, the timer figures, and a `STRAIGHT` control whose 128 lines are one row pattern.
|
|
746
|
+
|
|
747
|
+
### Sources
|
|
748
|
+
|
|
749
|
+
- The measurements on this entry are from the recipe named above, VICE x64sc 3.10, CIA1 timer A and the exit screenshot measured with PIL.
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
## tunnel — Tunnel effect
|
|
754
|
+
|
|
755
|
+
**Complexity:** high
|
|
756
|
+
**Region:** both
|
|
757
|
+
**Uses registers:** D018, D021
|
|
758
|
+
|
|
759
|
+
### Why
|
|
760
|
+
|
|
761
|
+
The tunnel effect simulates the viewer moving forward through an infinitely long cylindrical (or irregularly shaped) tunnel whose walls are texture-mapped. Each frame, the texture pattern on the walls appears to scroll toward the viewer, and the camera can rotate to look around the curve of the tunnel. The visual impression is of continuous forward motion through a geometric space, a compelling illusion created entirely in software. The tunnel effect became a hallmark of mid-1990s PC demoscene productions and was subsequently achieved on the C64 within the same decade.
|
|
762
|
+
|
|
763
|
+
### How
|
|
764
|
+
|
|
765
|
+
The tunnel is implemented as a per-cell lookup table precomputed at startup or embedded as static data. For each cell position `(cx, cy)` in the 40×25 character grid, two values are precomputed:
|
|
766
|
+
|
|
767
|
+
- **Distance:** how far from the center of the screen this cell would be mapped to on the tunnel wall. Cells near the screen center map to distant parts of the tunnel (the "far end"); cells near the edges map to nearby parts (just around the first curve).
|
|
768
|
+
- **Angle:** the angular position around the tunnel's circumference that this cell maps to. Cells at the same distance but different angular positions map to different parts of the texture column.
|
|
769
|
+
|
|
770
|
+
At each frame, the displayed color at cell `(cx, cy)` is:
|
|
771
|
+
|
|
772
|
+
`color = texture[(distance[cx][cy] + time_offset) & (TEX_H - 1)][(angle[cx][cy] + rotation_offset) & (TEX_W - 1)]`
|
|
773
|
+
|
|
774
|
+
where `time_offset` advances each frame (simulating forward motion) and `rotation_offset` changes the apparent camera rotation. The texture is a 2D pattern (typically a checkerboard, brick, or stripe design) stored as a byte array.
|
|
775
|
+
|
|
776
|
+
Because color RAM must be updated for every frame, the write loop visits all 1,000 cells and performs two table lookups plus one color RAM store per cell. The distance and angle tables are kept in RAM, and the texture access is a 2D index into a flat texture array. Self-modifying code or zero-page index arithmetic reduces the per-cell overhead.
|
|
777
|
+
|
|
778
|
+
### Why it works
|
|
779
|
+
|
|
780
|
+
The mathematical foundation is polar coordinate projection. Each screen cell's distance from the screen center corresponds to a Z-depth in the tunnel: cells close to center are far away (the vanishing point), cells at the edge are near. The mapping follows `distance ~ 1 / r` where `r` is the pixel-space distance from center, producing a hyperbolic depth mapping that correctly simulates perspective recession. The angle maps directly to the circumferential U coordinate of the tunnel texture.
|
|
781
|
+
|
|
782
|
+
By pre-computing both tables at startup, the per-frame inner loop does only table lookups and array indexing — no trigonometry or division at runtime. The time offset (added to the distance table index before texture access) scrolls the texture "into" the screen, simulating forward motion. The rotation offset (added to the angle table index) rotates the texture around the tunnel axis, simulating camera roll.
|
|
783
|
+
|
|
784
|
+
The VIC-II's character mode is used exactly as in the plasma technique: color RAM holds per-cell color values; screen RAM holds a fixed all-set character ($A0 reverse space or a custom all-$FF glyph) so that the colour-RAM colour fills the cell. (An earlier version of this sentence said a blank character "so that only the background color contributes"; in standard text mode Colour RAM sets the set-bit pixels, and a blank cell shows only $D021.)
|
|
785
|
+
|
|
786
|
+
### Variations
|
|
787
|
+
|
|
788
|
+
**Non-circular tunnel.** By modifying the distance and angle tables, the tunnel can be made elliptical, rectangular (a corridor), or irregular. The precomputed tables absorb the shape complexity; the per-frame loop is unchanged.
|
|
789
|
+
|
|
790
|
+
**Zooming tunnel.** Instead of scrolling the time offset at a constant rate, accelerate or decelerate it. Combined with a rotation offset that also accelerates, this produces a swirling, warping tunnel animation.
|
|
791
|
+
|
|
792
|
+
**Textured with character cells.** Rather than writing color RAM alone, also write screen RAM to select a character whose pixel pattern varies by position. This adds pixel-level texture detail to the per-cell color. Cost: doubles the per-cell write count (screen RAM + color RAM = 8 cycles per cell vs 4).
|
|
793
|
+
|
|
794
|
+
### Cycle budget
|
|
795
|
+
|
|
796
|
+
Per-frame, full 40×25 color RAM update:
|
|
797
|
+
|
|
798
|
+
- Per-cell: two table lookups (approximately 10 cycles each, self-modifying index or indexed indirect), one 2D texture access (approximately 10–15 cycles, depending on texture layout), one color RAM write (4 cycles) = approximately 35–45 cycles per cell.
|
|
799
|
+
- 1,000 cells × 40 cycles = approximately 40,000 cycles per frame.
|
|
800
|
+
- PAL frame budget: ~19,600 cycles. ~40,000 cycles is just over two PAL frames (2 × 19,656 = 39,312), so the render falls to every third frame, 16.7fps; trimming the loop under ~39,000 cycles (leaving room for the IRQ and offset update) recovers every-other-frame, 25fps. (An earlier version of this line said "approximately 12fps (every other frame)"; every other PAL frame is 25fps.)
|
|
801
|
+
- With screen RAM writes as well (character selection per cell): add 4,000 cycles (1,000 × 4), approximately 44,000 cycles per frame — also every third frame, 16.7fps.
|
|
802
|
+
|
|
803
|
+
The dominant optimization is the inner loop structure. Using an unrolled loop over 40 cells per row (8 unrolled iterations of 5 cells each, or fully unrolled at 40 cells per row) eliminates loop branch and counter-increment overhead. A fully unrolled 40-cell row costs fewer cycles than a tight loop due to the absence of branch instructions.
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
## starfield — Multi-layer starfield with depth parallax
|
|
808
|
+
|
|
809
|
+
**Complexity:** medium
|
|
810
|
+
**Region:** both
|
|
811
|
+
**Uses registers:** D015, D000, D001, D002, D003, D004, D005, D006, D007, D008, D009, D00A, D00B, D00C, D00D, D00E, D00F, D010
|
|
812
|
+
|
|
813
|
+
### Why
|
|
814
|
+
|
|
815
|
+
A moving starfield is the background layer of a substantial fraction of all C64 games and demos. Stars scroll horizontally or vertically to simulate the player's motion through space, and parallax (different layers at different speeds) adds the illusion of depth: bright foreground stars rush past quickly while dim background stars drift slowly. The technique is fast enough to run in the idle cycles of a game main loop and visually effective enough that it remains a standard element of intros and cracktros forty years after the C64's introduction.
|
|
816
|
+
|
|
817
|
+
### How
|
|
818
|
+
|
|
819
|
+
Each star is a hardware sprite. With 8 sprites available, a typical starfield uses all 8 — one sprite per star if the stars are large enough to need their own bitmap, or all 8 as single-pixel-like objects for classic small-dot starfields.
|
|
820
|
+
|
|
821
|
+
**Sprite-based (8 stars maximum per layer).** Each frame:
|
|
822
|
+
|
|
823
|
+
1. Advance each star's X position by its layer speed constant (foreground speed > background speed).
|
|
824
|
+
2. If the star passes the right (or left) edge of the screen, wrap it to the opposite edge and randomize its Y position within the playfield area (so the wrapping event is not visible at the same Y each loop).
|
|
825
|
+
3. Write updated X positions to $D000, $D002, ..., $D00E. Write MSB-X bits to $D010.
|
|
826
|
+
4. Write updated Y positions to $D001, $D003, ..., $D00F.
|
|
827
|
+
|
|
828
|
+
Each sprite's image data is a single-pixel-style custom shape (one bit set in the 8×8 effective area, or a small cluster of pixels for larger stars). Sprite color varies by layer: white or light yellow for foreground, medium gray or light blue for middle layer, dark gray for background.
|
|
829
|
+
|
|
830
|
+
**Combined sprite + charset (more than 8 stars).** The 8 hardware sprite stars are the foreground layer. A second and third layer are implemented as character cells: random character indices are placed in screen RAM positions, where each character index corresponds to a single-pixel dot at a specific position within the 8×8 cell. These cells scroll via $D016 XSCROLL (for horizontal) combined with per-column screen RAM copy (cycle the entire 40-column row each frame to shift the display content one cell per 8 frames). The character-based layers can hold hundreds of stars at near-zero per-star CPU cost after the initial setup.
|
|
831
|
+
|
|
832
|
+
### Why it works
|
|
833
|
+
|
|
834
|
+
The VIC-II's sprite registers accept any X coordinate 0–511 (using the MSB in $D010) and Y coordinate 0–255. Sprites at any of these coordinates will render on the appropriate scanlines as long as they are enabled. There is no hardware enforcement of a "playfield boundary" for sprites — they render wherever they are positioned, including outside the standard character display area.
|
|
835
|
+
|
|
836
|
+
The parallax illusion works because the visual system interprets faster-moving objects as closer. The two-layer or three-layer speed differential — typically a ratio of 2:1 or 3:1:0.5 between foreground, mid, and background — is sufficient to generate convincing depth without any 3D computation.
|
|
837
|
+
|
|
838
|
+
The random Y re-seed on edge wrap prevents the visible "column of stars appearing" artifact that occurs when all stars re-enter from the same vertical position. A simple LFSR (linear feedback shift register) operating on the star's Z layer value provides low-cost pseudo-random Y values.
|
|
839
|
+
|
|
840
|
+
### Variations
|
|
841
|
+
|
|
842
|
+
**Vertical starfield.** Instead of scrolling horizontally, advance Y positions downward (or upward) each frame. Useful for space shooters with vertical scrolling. Y-wrapping requires checking for Y > 255 and resetting to 0 (plus random X re-seed).
|
|
843
|
+
|
|
844
|
+
**Twinkle effect.** Randomly change a star's sprite color between white, light gray, and medium gray each frame. This simulates the star twinkling without any positional update cost. One random-byte read per star per frame, written to $D027+n. Approximately 8 cycles per twinkling star.
|
|
845
|
+
|
|
846
|
+
**Dense starfield via character layer.** Fill all 1,000 screen RAM positions with a computed character index based on a pseudo-random pattern generated at startup. Stars appear at fixed positions relative to the character map. Horizontal scrolling of the entire character layer (via XSCROLL + row-shift) makes the dense background scroll. The character layer approach supports hundreds of visible stars at essentially zero per-star per-frame cost.
|
|
847
|
+
|
|
848
|
+
### Cycle budget
|
|
849
|
+
|
|
850
|
+
8-sprite starfield, per frame on PAL:
|
|
851
|
+
|
|
852
|
+
- Per-star position update: 3 additions (X += speed, check wrap, conditional Y reset) = approximately 15–20 cycles per star including conditional branch.
|
|
853
|
+
- Write all 8 X positions: 4 cycles × 8 = 32 cycles.
|
|
854
|
+
- Write all 8 Y positions: 4 cycles × 8 = 32 cycles.
|
|
855
|
+
- Write $D010 MSB: 4 cycles.
|
|
856
|
+
- Total: approximately (8 × 18) + 68 = approximately 212 cycles per frame for an 8-sprite starfield.
|
|
857
|
+
- Well within PAL budget; typically takes under 1% of frame cycles.
|
|
858
|
+
|
|
859
|
+
Multi-layer (8 sprites + 40-column row-shift for character layer):
|
|
860
|
+
|
|
861
|
+
- Row shift: copy 40 bytes of screen RAM one position left = approximately 200 cycles per character-mode layer.
|
|
862
|
+
- Combined total: approximately 400–500 cycles per frame for sprite + two character layers.
|
|
863
|
+
|
|
864
|
+
---
|
|
865
|
+
|
|
866
|
+
## text_zoom — Per-line $D016 manipulation for zooming and wobbling text
|
|
867
|
+
|
|
868
|
+
**Complexity:** high
|
|
869
|
+
**Region:** both
|
|
870
|
+
**Uses registers:** D016, D011
|
|
871
|
+
**Demands:** midframe_raster_irqs
|
|
872
|
+
**Requires:** stable_raster_irq
|
|
873
|
+
|
|
874
|
+
### Why
|
|
875
|
+
|
|
876
|
+
Standard character mode displays text at a fixed position. The VIC-II's hardware scroll registers ($D016 bits 2-0 for horizontal, $D011 bits 2-0 for vertical) allow a global shift of the entire display by up to 7 pixels in each axis — one setting per frame. By changing these registers on a per-scanline basis via a stable raster IRQ chain, a different scroll offset can be applied to each row of the display. The result is that each text row appears horizontally displaced by a different amount, producing sine-wave wobble, zoom, and distortion effects that make the text appear to pulse, wave, or undulate. This technique is the basis for the classic "wobbly text" intro effect and is widely used in cracktros and menu systems.
|
|
877
|
+
|
|
878
|
+
### How
|
|
879
|
+
|
|
880
|
+
The technique requires a stable raster IRQ set to fire on every scanline within the text zone (or every other scanline for less resolution). At each IRQ entry:
|
|
881
|
+
|
|
882
|
+
1. Acknowledge the VIC-II interrupt ($D019).
|
|
883
|
+
2. Advance the IRQ to the next target scanline ($D012 = current + 1 or current + 8).
|
|
884
|
+
3. Compute the new XSCROLL value for this line: index a sine table using `(line_index + time_offset) & 255`, extract the low 3 bits of the result as the XSCROLL value.
|
|
885
|
+
4. Write the XSCROLL to $D016 (preserving bits 3–7: CSEL, MCM; mask with `AND #$F8` or use a precomputed table — an earlier version of this step said bits 4–7, which drops CSEL and puts the display in 38 columns). Common implementation: precompute a table of $D016 values where each entry combines the desired XSCROLL with the constant $D016 bit settings, avoiding a read-modify-write cycle.
|
|
886
|
+
5. Optionally write a new YSCROLL to $D011 for vertical displacement (less common; creates a vertical sine wobble in addition to horizontal).
|
|
887
|
+
|
|
888
|
+
The raster IRQ chain fires on each of the 8 scanlines within a character row (one IRQ per scanline for maximum sine resolution) or once per character row (one IRQ per 8 scanlines, giving coarser per-row resolution but using far fewer IRQ slots). For a 25-row text display with per-row precision, 25 IRQ slots per frame are needed. Per-scanline precision requires up to 200 IRQ slots — approaching the limit of practical IRQ chains, but achievable with a highly optimized dispatcher.
|
|
889
|
+
|
|
890
|
+
### Why it works
|
|
891
|
+
|
|
892
|
+
XSCROLL is not latched once per line: the display sequencer applies the current $D016 value continuously, so a write that lands mid-line shifts the remainder of that line (measured in VICE x64sc: a write landing around cycle 38–39 of line 100 left cells 0–21 at offset 0 and cells 22–39 at +7 on the same line, with line 101 fully shifted). An earlier version of this section said the VIC sampled XSCROLL "during the horizontal sync period" and held it for the whole line, with a latch "at approximately cycle 13"; neither is true, and `pitfalls/scroll.md` says the same — the value that lives in XSCROLL when the beam draws a given character is the one that applies.
|
|
893
|
+
|
|
894
|
+
For a clean per-line effect the write must complete before the first character's pixels start to shift out — on PAL that is X coordinate $18 at cycle 17, so the STA must finish by cycle 16 — or on the previous line only after its last character's pixels have left the sequencer, i.e. once the right border has begun (about cycle 58 on PAL), not merely after the last g-access on cycle 55. (The cycle-17/58 figures are arithmetic from the VIC X-coordinate table; the mid-line split is the measurement.) A stable raster IRQ (which eliminates jitter) is what makes that window reliable; without it, some lines take the write in time and some take it mid-line, producing a torn cell boundary rather than a smooth sine curve.
|
|
895
|
+
|
|
896
|
+
The sine table produces a smooth displacement value for each line. By advancing the time offset each frame, the displacement function appears to "move" through the text, creating the animation. Using different amplitude scaling or multiple sine terms produces more complex deformation patterns (figure-eight wobble, standing waves, etc.).
|
|
897
|
+
|
|
898
|
+
### Variations
|
|
899
|
+
|
|
900
|
+
**Zoom simulation.** Rather than a sine wave, use a linear ramp for XSCROLL values: row 0 gets XSCROLL = 0, row 12 gets XSCROLL = 3, row 24 gets XSCROLL = 7. This shifts lower rows further right, simulating a perspective tilt. Animated by updating the ramp's offset each frame.
|
|
901
|
+
|
|
902
|
+
**Horizontal color bars combined with wobble.** While the raster IRQ chain is already firing per scanline for the XSCROLL writes, also write $D020/$D021 (border/background color) at each IRQ entry. This overlays a color bar effect with the text wobble — the classic "colored wobbly text" combo seen in many C64 intros.
|
|
903
|
+
|
|
904
|
+
**YSCROLL manipulation for vertical displacement.** Writing $D011 bits 2-0 at each character-row IRQ entry displaces each character row vertically by 0–7 pixels. Produces a vertical sine wave across the text. Warning: changing YSCROLL at the wrong cycle can trigger spurious badlines (see `raster.md`, `badline_synchronization`). The write must arrive before the badline condition is evaluated for the row to avoid this side effect.
|
|
905
|
+
|
|
906
|
+
### Cycle budget
|
|
907
|
+
|
|
908
|
+
Per-scanline IRQ (one IRQ per raster line, 200 lines covered):
|
|
909
|
+
|
|
910
|
+
- IRQ entry and acknowledgment: approximately 15 cycles.
|
|
911
|
+
- Sine table lookup and XSCROLL preparation: approximately 10–15 cycles.
|
|
912
|
+
- $D016 write: 4 cycles.
|
|
913
|
+
- $D012 advance and RTI: approximately 8 cycles.
|
|
914
|
+
- Total per IRQ: approximately 37–42 cycles out of 63 available on PAL (non-badline).
|
|
915
|
+
- On badlines: 37–42 cycles out of 20 available on PAL (63 − 43; 23 only if the three cycles before BA takes hold happen to be write cycles — see `raster.md`, `badline_synchronization`). Exceeds the badline budget. An earlier version of this line said ~23.
|
|
916
|
+
- Standard mitigation: skip XSCROLL writes on badlines. The visual effect shows a 1-line horizontal glitch at each badline row (every 8 lines), usually acceptable.
|
|
917
|
+
|
|
918
|
+
Per-character-row IRQ (one IRQ per 8 scanlines, 25 rows):
|
|
919
|
+
|
|
920
|
+
- Same per-IRQ cost: approximately 40 cycles.
|
|
921
|
+
- Only 25 IRQs per frame: total approximately 1,000 cycles.
|
|
922
|
+
- All lines within each row share the same XSCROLL. Coarser wobble resolution but far cheaper. The standard approach for game scrollers and low-complexity text animations.
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
## tech_tech_wobbler — Tech-tech: a per-line horizontal sine wider than seven pixels through eight pre-shifted video matrices
|
|
927
|
+
|
|
928
|
+
**Complexity:** high
|
|
929
|
+
**Region:** both
|
|
930
|
+
**Uses registers:** D011, D012, D016, D018, D019, D01A, DC04, DC05, DC06, DC07, DC0E, DC0F
|
|
931
|
+
**Demands:** midframe_raster_irqs, cpu_every_line
|
|
932
|
+
**Requires:** stable_raster_irq, text_zoom
|
|
933
|
+
**Raster band:** movable
|
|
934
|
+
**Cost:** cycles_per_frame=5446, cycles_per_line=63, lines_active=54, irq_slots=3, bytes_code=2338, bytes_data=8384, zp_bytes=97
|
|
935
|
+
**Cost basis:** measured-vice
|
|
936
|
+
**Cost measured on:** kickassembler-tech-tech (the band interrupt, 3,405 cycles from irq1's entry on line 109 to the register restore after line 162, plus the table build of 2,041 in the vertical blank, PAL, screen on, the band's own forced badline stalls inside the figure; the NTSC band is 3,508; bytes_code is the code segment reported by -showmem, almost all of it the two unrolled bands; bytes_data the 384 bytes of tables and the eight 1,000-byte matrices; zp_bytes the two 48-byte register tables and the phase)
|
|
937
|
+
|
|
938
|
+
### Why
|
|
939
|
+
|
|
940
|
+
`text_zoom` moves a text row by at most seven pixels, because XSCROLL is
|
|
941
|
+
three bits and the character codes a row shows are fixed for its eight
|
|
942
|
+
lines. A tech-tech is the wide form: the logo swings by a whole character
|
|
943
|
+
cell or more per line, so a word can be pushed from one side of the
|
|
944
|
+
screen to the other on a sine while it is still text. It is a scene
|
|
945
|
+
staple for logos above a scroller, and its mechanism is FLI's, so it is
|
|
946
|
+
also the shortest route to understanding what FLI does to the VIC.
|
|
947
|
+
|
|
948
|
+
### How
|
|
949
|
+
|
|
950
|
+
1. Draw the logo into eight 1 KB video matrices in one VIC bank, matrix k
|
|
951
|
+
holding it k cells further right than matrix 0, from cell 3. Colour
|
|
952
|
+
RAM is shared by all eight, so the logo is one colour per column.
|
|
953
|
+
2. Keep a 256-entry sine of 32 + round(31 sin), 1 to 63. For each line l
|
|
954
|
+
of the band, s = sine[(t + 3 l) & 255]; the `$D018` byte is matrix
|
|
955
|
+
s >> 3 and the `$D016` byte is the base value with s & 7 in its low
|
|
956
|
+
bits. Build both tables per frame in the vertical blank, into zero
|
|
957
|
+
page, so the per-line loads take three cycles.
|
|
958
|
+
3. From a stable raster IRQ a few lines above the band, delay to cycle 55
|
|
959
|
+
of the band's first line, which is a natural badline. Then one
|
|
960
|
+
unrolled block per line: `Delay(3)`, `LDA zp / STA $D018`,
|
|
961
|
+
`LDA zp / STA $D016`, `LDA # / STA $D011` with YSCROLL = line & 7, 23
|
|
962
|
+
cycles, the `$D011` write last. The VIC takes the bus until cycle 54 and
|
|
963
|
+
the next block begins on cycle 55 of that line without counting.
|
|
964
|
+
4. After the last block restore YSCROLL, `$D018` and `$D016` in the right
|
|
965
|
+
border, so the next line is a natural badline from an empty matrix.
|
|
966
|
+
5. Advance t once a frame.
|
|
967
|
+
|
|
968
|
+
### Why it works
|
|
969
|
+
|
|
970
|
+
`hardware/vic-ii-reference.md` states the constraint: "The chip fetches
|
|
971
|
+
one character pointer (c-access) per cell during the badline of each text
|
|
972
|
+
row, caches it in an internal 40x12-bit row buffer, and then performs
|
|
973
|
+
eight g-accesses per cell over the next eight raster lines"; the
|
|
974
|
+
condition is "($30 <= raster <= $F7) AND (raster & 7 == YSCROLL) AND DEN
|
|
975
|
+
was set on $30". Setting YSCROLL to line & 7 on every line makes every
|
|
976
|
+
line a badline, and each fetch reads the matrix `$D018` names at that
|
|
977
|
+
moment, so each line draws its row of cells from a differently shifted
|
|
978
|
+
copy. The rows still advance because the write lands after cycle 14: the
|
|
979
|
+
`fli-image` recipe gives the two-sided constraint, an RC reset if the
|
|
980
|
+
condition holds in cycle 14, and one lost cell per cycle of lateness
|
|
981
|
+
after that, three at best. The `tech-tech` recipe measured both sides on
|
|
982
|
+
the same listing: one cycle early, the band shows only pixel row 0 of
|
|
983
|
+
each cell and the whole logo is drawn again below it; one, two and three
|
|
984
|
+
cycles late lose four, five and six cells, of which the leading ones show
|
|
985
|
+
the last complete fetch rather than the FLI bug's code 255. In that
|
|
986
|
+
recipe the logo's left edge measured 24 + s display pixels on all 48
|
|
987
|
+
lines of both models, against the table, and the seven-pixel control
|
|
988
|
+
with `$D018` fixed measured an amplitude of seven.
|
|
989
|
+
|
|
990
|
+
The three cells at the left of every forced line are the price. They
|
|
991
|
+
show screen code 255 in a colour that is not the cell's colour RAM; in
|
|
992
|
+
VICE 3.10 it followed the opcode byte the CPU fetches after the `$D011`
|
|
993
|
+
write on 47 of the 48 lines measured, and nothing is established about a
|
|
994
|
+
real chip.
|
|
995
|
+
|
|
996
|
+
### Cycle budget
|
|
997
|
+
|
|
998
|
+
Per band line: 63 cycles elapse on PAL, 65 on NTSC; the VIC holds the bus
|
|
999
|
+
for 40 of them and the block spends the other 23 (25) on its three loads,
|
|
1000
|
+
three stores and padding. There is no CPU time on a band line for
|
|
1001
|
+
anything else, which is what `cpu_every_line` means here; a sprite over
|
|
1002
|
+
the band would move the stall and break the placement, as it does in
|
|
1003
|
+
FLI. Per frame, measured on the recipe: 3,405 cycles for the 54-line
|
|
1004
|
+
band interrupt on PAL (3,508 NTSC), including the double IRQ's entry and
|
|
1005
|
+
the natural first line, and 2,041 for the two 48-entry tables. The rest
|
|
1006
|
+
of the frame is free. Taller bands cost 63 cycles a line and 2 zero-page
|
|
1007
|
+
bytes a line, and nothing else until the tables leave zero page and the
|
|
1008
|
+
loads take a cycle more each.
|
|
1009
|
+
|
|
1010
|
+
### Variations
|
|
1011
|
+
|
|
1012
|
+
**A bitmap tech-tech.** The same forced badlines with `$D018` bit 3
|
|
1013
|
+
selecting between two bitmaps do not give eight shifts, because the
|
|
1014
|
+
bitmap base has two positions per bank; a bitmap logo needs the eight
|
|
1015
|
+
copies as eight matrices of colour pairs over one bitmap, or the shift
|
|
1016
|
+
done in the bitmap data. `mode7_lookalike` above sets out which half of
|
|
1017
|
+
`$D018` is deferred and which is immediate.
|
|
1018
|
+
|
|
1019
|
+
**A colour wash per line.** The block has no spare cycles, but the
|
|
1020
|
+
restore after the band and the entry before it do, and `$D021` written
|
|
1021
|
+
in the vertical blank changes the whole band's paper. A per-line wash
|
|
1022
|
+
inside the band needs a fourth store in the block, 4 cycles, which is
|
|
1023
|
+
only there on NTSC at the shipped padding.
|
|
1024
|
+
|
|
1025
|
+
### Pitfalls
|
|
1026
|
+
|
|
1027
|
+
- `badline_cycle_loss` (`../pitfalls/raster-and-badline.md`): the technique creates a badline on every line it runs on and lives inside the 23 cycles the stall leaves; any code the block gains past that pushes the write late and costs a cell per cycle, as the recipe's sweep shows.
|
|
1028
|
+
- `raster_irq_first_line_jitter` (same page): the block chain is timed from one known cycle; an entry that jitters puts the first block's write early or late, an RC reset or a lost cell, on the first forced line and every line after it, since the stall preserves the offset.
|
|
1029
|
+
- `d016_unmasked_rmw_clobbers_csel_mcm` (`../pitfalls/scroll.md`): the `$D016` table stores the base value with the shift OR'd in; a raw s & 7 clears CSEL and drops the band to 38 columns.
|
|
1030
|
+
|
|
1031
|
+
### Recipes
|
|
1032
|
+
|
|
1033
|
+
- `recipes/kickassembler/tech-tech.md`: the design above on the word WOBBLE, rows 8 to 13, pinned at 12,000,000 cycles on both models with the placement sweep, the seven-pixel control and the timer figures.
|
|
1034
|
+
|
|
1035
|
+
### Sources
|
|
1036
|
+
|
|
1037
|
+
- `hardware/vic-ii-reference.md`, "Badlines" and "c-access, g-access, p-access, s-access": the condition, the row buffer and the fetch cycles.
|
|
1038
|
+
- `recipes/kickassembler/fli-image.md`: the block structure and the two-sided placement constraint this technique reuses.
|
|
1039
|
+
- `text_zoom` above: the seven-pixel form and the XSCROLL timing.
|
|
1040
|
+
|
|
1041
|
+
---
|
|
1042
|
+
|
|
1043
|
+
## voxel_landscape — Voxel-space landscape rendering
|
|
1044
|
+
|
|
1045
|
+
**Complexity:** scene-tier
|
|
1046
|
+
**Region:** both
|
|
1047
|
+
**Uses registers:** D011, D018
|
|
1048
|
+
|
|
1049
|
+
### Why
|
|
1050
|
+
|
|
1051
|
+
Voxel-space rendering draws a landscape by projecting a heightfield — a 2D grid of elevation values — onto the screen from a camera perspective. Each vertical column on the screen corresponds to one direction in the horizontal field of view; within each column, the algorithm walks from the camera outward along that direction, sampling the heightfield and projecting each sample's elevation onto the screen column. The result is a continuous terrain surface rendered as a series of vertical lines, producing the characteristic "Comanche"-style ragged horizon and rolling terrain. On the C64, voxel landscapes are slow but achievable, and their visual distinctiveness is worth the cost for scene-tier productions.
|
|
1052
|
+
|
|
1053
|
+
### How
|
|
1054
|
+
|
|
1055
|
+
The algorithm's inner loop walks outward from the camera for each screen column (there are 40 character columns in character mode, or 160 in multicolor bitmap mode):
|
|
1056
|
+
|
|
1057
|
+
1. For the current screen column, compute the horizontal direction angle from the camera.
|
|
1058
|
+
2. For each depth step along that direction (stepping from near to far), compute the 2D heightfield coordinate: `hx = camera_x + depth * cos(angle)`, `hy = camera_y + depth * sin(angle)`.
|
|
1059
|
+
3. Look up the height at `(hx & MAP_MASK, hy & MAP_MASK)` in the heightfield array.
|
|
1060
|
+
4. Project the height to screen Y: `screen_y = HORIZON_Y - (height - camera_z) * FOCAL / depth`.
|
|
1061
|
+
5. If this screen Y is above the previous highest rendered Y for this column, draw a vertical line from the previous top to this screen Y using the height's color value from the texture map.
|
|
1062
|
+
6. Update the "highest Y" for this column. If it reaches the top of the screen, stop processing this column (no further depth steps can contribute visible pixels).
|
|
1063
|
+
|
|
1064
|
+
In character mode, drawing a vertical line means writing a character cell or a bitmap column. Most C64 voxel implementations use multicolor bitmap mode (160 pixels wide, 200 tall) to get sufficient horizontal resolution.
|
|
1065
|
+
|
|
1066
|
+
The depth stepping uses a fixed-point representation: depth increments by a small constant per step, and the `cos`/`sin` table provides the 2D direction vector components as fixed-point integers. Map coordinates wrap modulo the map size (power-of-two dimensions for cheap masking).
|
|
1067
|
+
|
|
1068
|
+
### Why it works
|
|
1069
|
+
|
|
1070
|
+
The key insight of voxel-space rendering is that each screen column is independent: it can be rendered in isolation without knowledge of adjacent columns. This allows the outer loop to iterate over columns and the inner loop to handle depth. The early-exit optimization (stop when the top of the column is reached) means that near terrain (which covers most of the column from top to bottom) is drawn quickly, while only open-sky columns incur the full depth-walk cost.
|
|
1071
|
+
|
|
1072
|
+
The C64 can store a 64×64 or 128×128 heightfield comfortably within the 64 KB address space. Larger maps require memory banking, adding complexity. The texture map (color for each height value) is a simple 256-entry table: height byte → VIC-II color index. The VIC-II's 4-bit color system means 16 distinct terrain colors, which is sufficient for a basic landscape (rock, grass, water, snow).
|
|
1073
|
+
|
|
1074
|
+
Frame rate is the dominant limitation. A 160-column 200-row bitmap with a 64-depth walk per column, with early exit typically cutting the average to 30–40 depth steps per column: 160 × 35 × (cost per step). Per-step cost: one heightfield lookup, one height compare, one conditional vertical fill increment — approximately 25–35 cycles per step. Total: approximately 160 × 35 × 30 = 168,000 cycles per frame. On PAL (19,656 cycles per frame) this is 8.5 PAL frames of work, so a new frame every ninth PAL frame: 50 / 9 = 5.6fps. An earlier version of this sentence said 2–4fps, which does not follow from its own cycle count. This is the realistic target for a C64 voxel renderer without extreme optimization.
|
|
1075
|
+
|
|
1076
|
+
### Variations
|
|
1077
|
+
|
|
1078
|
+
**Lower resolution.** Reduce to 40 character columns (using character mode, one character = one vertical column) and 100 depth steps. The visual resolution is coarser but the cycle cost drops proportionally: approximately 40 × 50 × 30 = 60,000 cycles per frame, just over 3 PAL frames of work (3 × 19,656 = 58,968), so a render every fourth frame: 12.5fps.
|
|
1079
|
+
|
|
1080
|
+
**Precomputed direction tables.** Move all `cos`/`sin` multiplications into precomputed per-column tables. At startup, for each column compute and store the per-step `(dx, dy)` fixed-point vector. The inner loop then needs only two additions (advance `hx` and `hy`) rather than a multiply. This is the standard approach for any real C64 voxel implementation.
|
|
1081
|
+
|
|
1082
|
+
**Color shading by distance.** Apply a depth fog: distant terrain uses darker colors (closer to black) than near terrain. Implemented by selecting the texture color entry based on both height and depth: `color = color_table[height & COLOR_MASK][depth >> DISTANCE_SHIFT]`. Adds approximately 5 cycles per step for the 2D color lookup.
|
|
1083
|
+
|
|
1084
|
+
### Cycle budget
|
|
1085
|
+
|
|
1086
|
+
Per column, per frame, 40-step depth walk (approximate):
|
|
1087
|
+
|
|
1088
|
+
- Per-step (after precomputed direction table optimization): map coordinate advance (2 additions: ~6 cycles), heightfield lookup (~5 cycles), height compare (~4 cycles), conditional bitmap column write (~10 cycles if writing, ~4 if skipping) = approximately 25–30 cycles per depth step.
|
|
1089
|
+
- 40 steps × 27 cycles = approximately 1,080 cycles per column.
|
|
1090
|
+
- 40 columns × 1,080 = approximately 43,200 cycles per frame.
|
|
1091
|
+
- PAL: 43,200 cycles is 2.2 PAL frames of work, so a render every third frame = 16.7fps at this resolution (an earlier version of this line said 11fps). Reasonable target for scene-tier production.
|
|
1092
|
+
- At 160-column multicolor bitmap mode: approximately 4× more columns = approximately 4× the cost = 172,800 cycles, 8.8 PAL frames, so a render every ninth frame = 5.6fps. (An earlier version of this line said 8–9fps, confusing PAL frames per render with frames per second.)
|
|
1093
|
+
|
|
1094
|
+
All cycle estimates above are approximate. Actual performance depends heavily on inner-loop implementation, memory layout, and whether the early-exit optimization eliminates a significant fraction of depth steps.
|
|
1095
|
+
|
|
1096
|
+
---
|
|
1097
|
+
|
|
1098
|
+
## mode7_lookalike — Pseudo-Mode-7 affine warp
|
|
1099
|
+
|
|
1100
|
+
**Complexity:** scene-tier
|
|
1101
|
+
**Region:** both
|
|
1102
|
+
**Uses registers:** D018, D016
|
|
1103
|
+
|
|
1104
|
+
### Why
|
|
1105
|
+
|
|
1106
|
+
The SNES's Mode 7 hardware applies a per-scanline affine transformation (scale, rotate, translate) to a flat texture map, producing the effect of a flat plane receding toward a horizon — the floor and ceiling warp used in F-Zero, Mario Kart, and dozens of other games. The C64 has no equivalent hardware. But by changing $D018 (video matrix / bitmap base pointer) and $D016 (XSCROLL) on every scanline via a stable raster IRQ chain, the apparent horizontal position and scale of the bitmap display can be varied per line, approximating the affine warp look. The result is not as smooth as hardware Mode 7, but is visually striking and was unambiguously impressive for a 1 MHz 8-bit machine.
|
|
1107
|
+
|
|
1108
|
+
### How
|
|
1109
|
+
|
|
1110
|
+
The standard approach uses a precomputed scaling table indexed by raster line number. The fundamental observation: as scanlines approach the horizon (the vertical midpoint of the display), the rendered row must show a wider field of view (the pixels are farther apart in world space), which means the bitmap should "shrink" horizontally toward the horizon. Below the horizon, rows are near the camera and show a narrow field of view (pixels close together), so the bitmap appears large.
|
|
1111
|
+
|
|
1112
|
+
In practice on the C64, this is implemented by changing $D018 bit 3 (which of the two 8 KB halves of the VIC bank holds the bitmap; bits 4-7 select the video matrix, i.e. the per-cell colour bytes, not the bitmap — an earlier version of this paragraph had the bits wrong) and $D016 bits 0-2 (XSCROLL) on each scanline. Changing bit 3 mid-frame redirects the bitmap fetch for the following lines to the other 8 KB half, so the two halves can hold differently laid-out row data. Combined with XSCROLL (which shifts by 0–7 pixels without changing the base address), the combined address + scroll offset gives enough control to simulate the affine scale.
|
|
1113
|
+
|
|
1114
|
+
A full implementation uses double-buffering: while one bitmap bank is displayed, the other is prepared with a set of pre-scaled row data. The row data is pre-rendered offline (or at startup) as a series of 40-byte rows at different scales: row 0 (near the camera) uses full-width bitmap data from the source texture; row 100 (near the horizon) uses a compressed version where the source texture is sampled at every 4th or 8th pixel. Each row in the pre-scaled buffer is drawn from this pre-compressed source. Swapping $D018 between frames (pointing to the newly-prepared buffer) completes the double-buffer.
|
|
1115
|
+
|
|
1116
|
+
### Why it works
|
|
1117
|
+
|
|
1118
|
+
The VIC-II reads bitmap data from the 8 KB half selected by $D018 bit 3 (bitmap base: $0000 or $2000 within the VIC bank; bits 7-4 select the video matrix, not the bitmap) combined with the current scan position. The bitmap base is read on every g-access, so a mid-row write changes the source from the very line it lands on (measured in VICE x64sc: a write landing on line 100, the second line of character row 6, switched the displayed bitmap on line 100 itself, not on line 107; see `raster.md`, `raster_split_modes`) — land it before cycle 16 of the target line, or after cycle 55 of the previous one. Only the video-matrix half (bits 7-4) is deferred: its 40 cells are fetched by the badline c-accesses and held in the row buffer, so a VM change shows from the next character row. An earlier version of this paragraph said a $D018 bitmap-base change waits for the next character row; that is true only of the video-matrix bits.
|
|
1119
|
+
|
|
1120
|
+
Per-scanline XSCROLL ($D016) changes are not latched either: the display sequencer applies the current value continuously, so a write that lands mid-line shifts only the remainder of that line (measured in VICE x64sc; see `text_zoom` above — an earlier version of this paragraph said XSCROLL was "sampled at approximately cycle 13", which is not how the sequencer behaves). By finishing the $D016 write by cycle 16 of a raster line, or in the right border of the preceding line (from about cycle 58 on PAL), a different XSCROLL can be applied cleanly to each individual scanline. This provides 0–7 pixel horizontal offset granularity per scanline.
|
|
1121
|
+
|
|
1122
|
+
The combination of per-line $D018 changes and per-scanline $D016 changes produces the visual scaling illusion — but the two levers are not symmetrical. The bitmap base has only two positions per bank (bit 3), so $D018 is a coarse two-way switch, useful for double-buffering or for splitting the display between two differently prepared buffers; it cannot supply a graded per-row offset. The per-row scale therefore has to come from the pre-scaled row data laid out in the buffer itself, with XSCROLL adding the 0–7 pixel fine shift that centres each row on the horizon point. (An earlier version of this paragraph described "small" and "progressively larger $D018 offsets" per row; no such graded offsets exist in the register.)
|
|
1123
|
+
|
|
1124
|
+
### Variations
|
|
1125
|
+
|
|
1126
|
+
**Static texture warp (no animation).** Use a precomputed static per-row layout in bitmap RAM. No per-frame double-buffer swap needed; $D018 and $D016 are written once at setup and not changed. The result is a stationary Mode-7-style floor graphic, useful for title screens or map displays.
|
|
1127
|
+
|
|
1128
|
+
**Rotation overlay.** Add a per-frame rotation offset to the per-row XSCROLL table (shifting all rows by a sine-derived offset). This rotates the apparent "camera heading" over the floor plane without redrawing the bitmap data. Fast: only 200 XSCROLL writes per frame (one per scanline via the IRQ chain), approximately 800 cycles.
|
|
1129
|
+
|
|
1130
|
+
**Animated texture.** Each frame, copy a new section of a larger source texture into the bitmap buffer, offset by a camera-position variable. Combined with the per-line scaling, this produces the impression of flying over a moving terrain. Very expensive: the 6510 has no block move, and even a fully unrolled `LDA abs,X` / `STA abs,X` copy costs 9-10 cycles per byte including page-cross penalties and loop overhead, so copying an entire 8,000-byte bitmap costs roughly 75,000-80,000 cycles (measured 79,041 in VICE with the screen blanked) — four PAL frames on its own, and nearer six or seven once the per-line IRQ chain below has taken its ~39% of each frame. An earlier version of this sentence said "approximately 8,000 cycles", i.e. one cycle per byte. A full-buffer animated texture therefore lands around 7-12fps on PAL; most implementations copy only the rows that change, or scroll the source through $D018/$DD00 bank selection instead of copying it.
|
|
1131
|
+
|
|
1132
|
+
### Cycle budget
|
|
1133
|
+
|
|
1134
|
+
Per-scanline IRQ chain (one IRQ per raster line, 200 active lines):
|
|
1135
|
+
|
|
1136
|
+
- Per-IRQ: acknowledge ($D019), advance $D012 (+1), look up XSCROLL from precomputed table (approximately 5 cycles), write $D016 (4 cycles), look up $D018 from table (5 cycles), write $D018 (4 cycles), RTI (6 cycles) = approximately 35–40 cycles per line.
|
|
1137
|
+
- 200 lines × 38 cycles = approximately 7,600 cycles per frame for IRQ-chain overhead alone.
|
|
1138
|
+
- PAL budget: ~19,600 cycles. The IRQ chain consumes approximately 39% of the frame. Remaining ~12,000 cycles are available for camera position update, input handling, and bitmap buffer preparation.
|
|
1139
|
+
- $D018 does not need writing on every line. With only two bitmap bases available (bit 3), the register only needs writing on the lines where the source actually changes, which a per-row layout limits to at most 25 per frame — and if $D018 is being used only as the double-buffer flip, it is one write per frame. Writing $D018 in 25 rather than 200 of the per-line IRQs saves approximately 5 × 175 = 875 cycles. Worth doing. (An earlier version of this line justified the saving by saying $D018 "takes effect at character row boundaries (every 8 lines)"; that timing holds for the video-matrix bits 7-4, not for the bitmap-base bit, which takes effect on the line it is written.)
|
|
1140
|
+
|
|
1141
|
+
All cycle counts above are approximate and will vary with handler implementation, table layout, and whether badlines are handled separately.
|
|
1142
|
+
|
|
1143
|
+
### Recipes
|
|
1144
|
+
|
|
1145
|
+
- `recipes/kickassembler/pseudo-3d-road.md` — coarse layer only (fine layer timing not resolved in that build; see "What it does not establish").
|
|
1146
|
+
|
|
1147
|
+
---
|
|
1148
|
+
|
|
1149
|
+
## pseudo_3d_road_raster — Per-line horizontal shift with segment tables for a curving road
|
|
1150
|
+
|
|
1151
|
+
**Complexity:** high
|
|
1152
|
+
**Region:** both
|
|
1153
|
+
**Uses registers:** D011, D012, D016, D018, D019, D01A, D021, D022, D023, DC04, DC05, DC0E
|
|
1154
|
+
**Requires:** stable_raster_irq, irq_chain_table
|
|
1155
|
+
**Cost:** cycles_per_frame=12855
|
|
1156
|
+
**Cost basis:** measured-vice
|
|
1157
|
+
**Cost measured on:** kickassembler-pseudo-3d-road (the coarse redraw, 6,332 cycles, plus the fine chain, 6,523, on PAL, summed; the per-frame table computation of about 5,000 cycles is an estimate and is not in the figure, see Cycle budget)
|
|
1158
|
+
|
|
1159
|
+
### Why
|
|
1160
|
+
|
|
1161
|
+
A pseudo-3D road effect places a road on the lower half of the display and
|
|
1162
|
+
makes it appear to recede toward a horizon and curve left or right. The visual
|
|
1163
|
+
impression of depth comes from two sources. First, the road narrows as it
|
|
1164
|
+
approaches the horizon: each character row is drawn narrower than the one
|
|
1165
|
+
below it, following a Z table of half-widths derived from the formula
|
|
1166
|
+
`hw[i] = max(4, round(152 - 1.5 * i))` for i = 0 (nearest) to 99 (farthest).
|
|
1167
|
+
Second, a per-line horizontal shift aligns the road's left edge to the pixel,
|
|
1168
|
+
not the character: because the left edge rarely falls on an 8-pixel character
|
|
1169
|
+
boundary, the remaining 0–7 pixels of misalignment are corrected by writing
|
|
1170
|
+
$D016 XSCROLL on every raster line of the road area.
|
|
1171
|
+
|
|
1172
|
+
The curvature is produced by a running delta: each step, the road centre cx
|
|
1173
|
+
advances by dx, and dx itself advances by the current segment's fixed-point
|
|
1174
|
+
curve value. Four segments of 25 lines each (straight, right bend, straight,
|
|
1175
|
+
left bend) give one full S-curve over the 100 road lines; as scroll_z advances
|
|
1176
|
+
one unit per frame and wraps at 100, the curve pattern scrolls toward the
|
|
1177
|
+
viewer continuously.
|
|
1178
|
+
|
|
1179
|
+
### How
|
|
1180
|
+
|
|
1181
|
+
**Coarse layer.** Once per frame in the vertical blank, thirteen character rows
|
|
1182
|
+
(the road area, character rows 6–18) are redrawn. For each row r (r = 0 =
|
|
1183
|
+
nearest = row 18, r = 12 = farthest = row 6), the routine samples the cx and
|
|
1184
|
+
hw values at the row's middle raster line (i_mid = min(8r + 4, 99)), computes
|
|
1185
|
+
`left_char = (cx[i_mid] - hw[i_mid]) >> 3` and `right_char = (cx[i_mid] +
|
|
1186
|
+
hw[i_mid]) >> 3`, and writes the 40 character columns: left kerb at
|
|
1187
|
+
left_char, road codes from left_char+1 to right_char-1, right kerb at
|
|
1188
|
+
right_char, and a centre dash at the midpoint of alternating rows. A
|
|
1189
|
+
five-character custom multicolour charset covers the five codes (grass,
|
|
1190
|
+
road, left kerb, right kerb, dash); $D021, $D022 and $D023 set the three
|
|
1191
|
+
colour values used by %00, %01 and %10 pixel pairs respectively.
|
|
1192
|
+
|
|
1193
|
+
**Fine layer.** A stable raster IRQ using the double-IRQ method fires at line 99
|
|
1194
|
+
(the horizon). The handler runs an unrolled loop of exactly one raster line per
|
|
1195
|
+
iteration: 63 CPU cycles on PAL (65 on NTSC). PAL and NTSC use separate
|
|
1196
|
+
unrolled loops, selected at boot by a model-detect routine. Each iteration
|
|
1197
|
+
loads a precomputed D016 value from a 100-entry table and stores it to $D016 at
|
|
1198
|
+
cycle 11 of the target raster line (before the approximately cycle-14 VIC
|
|
1199
|
+
deadline). The table entry for road line offset j is
|
|
1200
|
+
`D016_BASE | ((cx[99-j] - hw[99-j]) & 7)`, where D016_BASE carries the MCM and
|
|
1201
|
+
CSEL bits. The write shifts the entire character row left or right by 0-7
|
|
1202
|
+
pixels, so the kerb character boundary appears at the correct pixel even though
|
|
1203
|
+
the character grid is always 8-pixel aligned.
|
|
1204
|
+
|
|
1205
|
+
**Badline handling.** Twelve badlines fall in the road area (lines 107, 115,
|
|
1206
|
+
123, 131, 139, 147, 155, 163, 171, 179, 187, 195 with YSCROLL=3). On a badline
|
|
1207
|
+
the CPU is stalled from cycle 15 to 54 (40 cycles). The unrolled badline body
|
|
1208
|
+
uses Delay(15) (PAL) or Delay(17) (NTSC), giving 23 or 25 CPU cycles; the
|
|
1209
|
+
40-cycle steal fills the remainder to 63 or 65 elapsed cycles. The STA $D016
|
|
1210
|
+
write lands at cycle 11, before the steal begins at cycle 15, so the badline row
|
|
1211
|
+
and the row after it each receive their own XSCROLL value (measured: xscroll_d16
|
|
1212
|
+
entries for j=7 and j=8 differ in the monitor dump; the adjacent rows show
|
|
1213
|
+
distinct kerb pixel positions in the screenshot).
|
|
1214
|
+
|
|
1215
|
+
**cx computation.** The forward pass runs from i = 0 (nearest) to i = 99
|
|
1216
|
+
(farthest). cx begins at 160.0 in 8.8 fixed point. At each step: cx += dx,
|
|
1217
|
+
then dx += curve[segment], where segment = (i + scroll_z) / 25 & 3 and the
|
|
1218
|
+
four curve values are 0, +51, 0, -51 in 8.8 (±0.20 pixels per line per line).
|
|
1219
|
+
The integer part of cx is stored in cx_hi_buf[i]. The backward pass fills
|
|
1220
|
+
xscroll_d16[j] from cx_hi_buf[99-j] for j = 0..99.
|
|
1221
|
+
|
|
1222
|
+
**IRQ exit.** The handlers exit via `pla/tay/pla/tax/pla/rti`, which pops the
|
|
1223
|
+
A, X and Y saved by the KERNAL dispatcher at $FF48 and then RTIs through the
|
|
1224
|
+
CPU interrupt frame. The road_irq2 handler discards its own stack frame first
|
|
1225
|
+
(using the saved stack pointer from road_irq1) and returns through road_irq1's
|
|
1226
|
+
frame. Exiting with a bare RTI without first popping the KERNAL's three saves
|
|
1227
|
+
would leave three bytes on the stack per IRQ call, overflowing page 1 after
|
|
1228
|
+
approximately 85 frames.
|
|
1229
|
+
|
|
1230
|
+
### Why it works
|
|
1231
|
+
|
|
1232
|
+
The VIC-II applies XSCROLL (bits 2–0 of $D016) continuously: a write that
|
|
1233
|
+
lands before the first character's pixels leave the sequencer shifts the entire
|
|
1234
|
+
remaining line. The deadline is approximately cycle 16 of the raster line (from
|
|
1235
|
+
the VIC's horizontal timing; see `mode7_lookalike` for the measured boundary).
|
|
1236
|
+
By using a stable raster IRQ entry (`stable_raster_irq`, double-IRQ or
|
|
1237
|
+
$D012-polling with a fixed-cycle preamble), the tight loop starts at a known
|
|
1238
|
+
cycle relative to the horizon line, and each subsequent iteration stays
|
|
1239
|
+
synchronised because it is exactly one raster line long. Any jitter in the
|
|
1240
|
+
initial synchronisation displaces all 100 writes by the same constant offset; a
|
|
1241
|
+
wrong initial offset means all writes land slightly late, which is measurable
|
|
1242
|
+
(the kerb shows at the wrong pixel) and correctable by adjusting the preamble
|
|
1243
|
+
delay.
|
|
1244
|
+
|
|
1245
|
+
The limit of this technique: one XSCROLL value per line shifts every column of
|
|
1246
|
+
that line. The near side and far side of the road both shift together within a
|
|
1247
|
+
given character row. This is visible at the character row boundaries (the kerb
|
|
1248
|
+
steps in 8-pixel jumps between rows) but invisible within each of the 8 raster
|
|
1249
|
+
lines of a row (the kerb slides smoothly). For wider character rows the step
|
|
1250
|
+
artifact is more pronounced.
|
|
1251
|
+
|
|
1252
|
+
### Cycle budget
|
|
1253
|
+
|
|
1254
|
+
Per-frame (measured in VICE x64sc 3.10, `recipes/kickassembler/pseudo-3d-road.md`):
|
|
1255
|
+
|
|
1256
|
+
- Coarse redraw (CIA-timed): 6,332 cycles PAL; 6,162 cycles NTSC. The redraw covers 13 rows; without per-row grass clearing, it writes only the road, kerb and dash characters.
|
|
1257
|
+
- Fine chain (CIA from road_irq1 start to fine_done stop): 6,523 cycles PAL; 6,729 cycles NTSC. The loop itself is 100 x 63 = 6,300 cycles PAL (100 x 65 = 6,500 NTSC); the surplus is the irq1 code, NOP slide, irq2 KERNAL entry, sync code and CIA stop.
|
|
1258
|
+
- Total handler: table computation (approximately 5,000 cycles) + coarse (6,332) + fine chain (6,523) ≈ 18,000 cycles per frame on PAL (arithmetic).
|
|
1259
|
+
|
|
1260
|
+
Badline body: 23 CPU cycles PAL (25 NTSC), with the 40-cycle steal filling the remainder to one full raster line. The STA write at cycle 11 lands before the steal begins at cycle 15, so each badline row and its successor each receive their own XSCROLL value.
|
|
1261
|
+
|
|
1262
|
+
### Variations
|
|
1263
|
+
|
|
1264
|
+
**Write in the right border of the previous line.** Writing xscroll_d16[j] at cycle 58–63 of raster line 99 + j rather than at cycle 1–8 of line 100 + j is valid (the VIC carries the new value into the next line) and is immune to badline stalls (the steal happens at cycles 15–54, after the write). The loop structure is identical; only the initial synchronisation point changes.
|
|
1265
|
+
|
|
1266
|
+
**Wider segment table.** The four-segment table (straight, right, straight, left) can be replaced with any pattern. Eight or sixteen entries give richer curvature sequences. The per-frame computation cost scales with the number of distinct curve values, not the number of segments.
|
|
1267
|
+
|
|
1268
|
+
**Sprite horizon.** Adding a single expanded sprite or a colour-split at line 99 gives a solid coloured horizon line separating sky from road without additional raster handlers.
|
|
1269
|
+
|
|
1270
|
+
### Pitfalls
|
|
1271
|
+
|
|
1272
|
+
- `d016_unmasked_rmw_clobbers_csel_mcm` — the precomputed table stores D016_BASE (MCM + CSEL bits) OR'd into each entry. Omitting this and writing the raw XSCROLL value clears MCM (bit 4) and drops the display to 38-column mode on every road line.
|
|
1273
|
+
- `badline_cycle_loss` — a loop body not sized for badlines spans the steal window; post-badline writes arrive late. The shipped listing uses separate badline and normal bodies to absorb the 40-cycle steal without displacing subsequent writes.
|
|
1274
|
+
- `raster_irq_first_line_jitter` — without a stable raster entry, the first loop iteration starts 0–9 cycles late, displacing all 100 writes by the same offset and pushing them past cycle 16.
|
|
1275
|
+
|
|
1276
|
+
### Sources
|
|
1277
|
+
|
|
1278
|
+
- Measured in VICE x64sc 3.10 (`recipes/kickassembler/pseudo-3d-road.md`).
|
|
1279
|
+
|
|
1280
|
+
---
|
|
1281
|
+
|
|
1282
|
+
## wireframe_pipeline — Rotated, projected, culled and clipped wireframe objects in a hires bitmap
|
|
1283
|
+
|
|
1284
|
+
**Complexity:** high
|
|
1285
|
+
**Region:** both
|
|
1286
|
+
**Uses registers:** D011, D018, DD00
|
|
1287
|
+
**Requires:** standard_bitmap, table_multiply_8x8
|
|
1288
|
+
|
|
1289
|
+
### Why
|
|
1290
|
+
|
|
1291
|
+
A space trader, a flight game or a tank game draws solid-looking objects
|
|
1292
|
+
as edges only. Lines are cheap on a 1 MHz CPU where filled faces are
|
|
1293
|
+
not, and a convex object drawn with its hidden edges removed reads as
|
|
1294
|
+
solid. This is the pipeline Elite uses. Each stage has a standard
|
|
1295
|
+
6502-sized form, and the stages have to agree on number ranges, or a
|
|
1296
|
+
coordinate wraps and a line lands across the screen.
|
|
1297
|
+
|
|
1298
|
+
### How
|
|
1299
|
+
|
|
1300
|
+
Per object, per update:
|
|
1301
|
+
|
|
1302
|
+
1. **Object model.** Signed-byte vertices, faces listed by their vertex
|
|
1303
|
+
indices in a consistent winding, and edges that name their two
|
|
1304
|
+
vertices and the two faces they separate. The object must be convex
|
|
1305
|
+
for culling alone to remove every hidden edge.
|
|
1306
|
+
2. **Rotation.** Build a 3x3 fixed-point matrix from the object's angles
|
|
1307
|
+
with a sine table scaled to 64 and quarter-square multiplies
|
|
1308
|
+
(`table_multiply_8x8`, `techniques/maths.md`). Multiply each vertex
|
|
1309
|
+
by it: nine products, summed in 16 bits and shifted right 6.
|
|
1310
|
+
3. **Translation and projection.** Add the object's position. Screen x
|
|
1311
|
+
is `cx + x * d / z`. The division is a reciprocal table indexed by z
|
|
1312
|
+
and one more multiply (`table_generation`,
|
|
1313
|
+
`techniques/cpu-cycle-tricks.md`), or the divide in
|
|
1314
|
+
`division_8_16bit` (`techniques/maths.md`).
|
|
1315
|
+
4. **Back-face culling.** A face is visible when it faces the camera.
|
|
1316
|
+
Either rotate the face normals and test the sign of the normal dotted
|
|
1317
|
+
with the line of sight (Elite's way), or test the winding of the
|
|
1318
|
+
projected face (`dx1 * dy2 - dy1 * dx2`). The winding test needs no
|
|
1319
|
+
normals and is exact under perspective, up to rounding. An edge is
|
|
1320
|
+
visible when either of its faces is, and is drawn once.
|
|
1321
|
+
5. **Clipping.** Give each endpoint an outcode (left, right, above,
|
|
1322
|
+
below). If both endpoints share a bit, drop the line. If both are
|
|
1323
|
+
inside, draw it plainly. Otherwise, either cut the line at the window
|
|
1324
|
+
edge (Cohen-Sutherland, one divide per crossing) or draw it with a
|
|
1325
|
+
plotter that skips pixels outside the window.
|
|
1326
|
+
6. **Drawing.** Bresenham along the longer axis, plotting with EOR into
|
|
1327
|
+
the hires bitmap through row and column tables (`standard_bitmap`,
|
|
1328
|
+
`techniques/bitmap-modes.md`).
|
|
1329
|
+
7. **Erasing.** Either keep each object's drawn endpoints in a line heap
|
|
1330
|
+
and draw them again with EOR in the next update, or double-buffer:
|
|
1331
|
+
two bitmaps in two VIC banks, clear and draw the hidden one, and
|
|
1332
|
+
switch banks with `$DD00` (and `$D018`) at the frame boundary. The
|
|
1333
|
+
two bitmaps need two banks because one 16 KB bank cannot hold two
|
|
1334
|
+
8,000-byte bitmaps and a matrix (`solid_vector_3d` above).
|
|
1335
|
+
|
|
1336
|
+
### Why it works
|
|
1337
|
+
|
|
1338
|
+
EOR is its own inverse, so drawing the same line twice leaves the bitmap
|
|
1339
|
+
as it was, even where the line crosses another object's lines.
|
|
1340
|
+
Erase-by-redraw therefore costs one line draw per old line and nothing
|
|
1341
|
+
per empty pixel. Clearing a bitmap costs the whole area: a 4,000-byte
|
|
1342
|
+
half-screen at 5 cycles a byte (an unrolled `STA abs,X`, instruction
|
|
1343
|
+
table) is 20,000 cycles, whether the objects are small or large. The
|
|
1344
|
+
price of redraw is flicker: a line is off the screen between its erase
|
|
1345
|
+
and its redraw. Double buffering hides that but pays for the clear and
|
|
1346
|
+
for a second bank. Where lines meet, EOR clears every pixel covered an
|
|
1347
|
+
even number of times, so two-edge vertices show a missing pixel.
|
|
1348
|
+
|
|
1349
|
+
Culling edges through faces keeps the line count low: a cube shows at
|
|
1350
|
+
most three faces and nine of its twelve edges. Culling also removes
|
|
1351
|
+
every hidden edge of a convex object, so no depth sort is needed.
|
|
1352
|
+
|
|
1353
|
+
A per-pixel clip gives the same inside pixels as a cut line, because
|
|
1354
|
+
Bresenham's pixels do not depend on where the line is cut. It costs
|
|
1355
|
+
time on the outside part of the line, and it needs every coordinate to
|
|
1356
|
+
fit the plotter's range. A cut line costs a divide per crossing but
|
|
1357
|
+
handles coordinates far off screen.
|
|
1358
|
+
|
|
1359
|
+
### Variations
|
|
1360
|
+
|
|
1361
|
+
**Elite (C64).** Each ship has a line heap: one size byte, then four
|
|
1362
|
+
bytes (X1, Y1, X2, Y2) per visible line; a Sidewinder never shows more
|
|
1363
|
+
than 15 edges, so 61 bytes. Each frame, Elite redraws the old heap with
|
|
1364
|
+
EOR to erase it, works out face visibility from the dot product of the
|
|
1365
|
+
face normal and the line of sight, derives the visible vertices and
|
|
1366
|
+
edges from the faces, projects, clips (routines LL145 and LL118), fills
|
|
1367
|
+
the heap and draws it. The C64 screen is split by raster interrupts at
|
|
1368
|
+
lines 51 and 51 + 143 = 194: a black-and-white standard bitmap space
|
|
1369
|
+
view 144 pixels tall above a multicolour bitmap dashboard. Hardware
|
|
1370
|
+
sprites draw the laser sights. (Mark Moxon's annotated source; not
|
|
1371
|
+
measured here.)
|
|
1372
|
+
|
|
1373
|
+
**Double-buffered.** Two VIC banks, each with its own bitmap and matrix,
|
|
1374
|
+
clear-and-draw instead of erase, and a `$DD00` write at the frame
|
|
1375
|
+
boundary. There is no flicker and no EOR artefact, and plotting can use
|
|
1376
|
+
OR. A `$DD00` bank switch also moves every sprite pointer and the
|
|
1377
|
+
character set with the bank (`pitfalls/banking.md`,
|
|
1378
|
+
`vic_bank_visibility_collision`).
|
|
1379
|
+
|
|
1380
|
+
**Faster plotting.** The recipe's plotter looks up the row address on
|
|
1381
|
+
every y step. An incremental plotter moves the byte address by 8 and
|
|
1382
|
+
rotates the bit mask as x steps, and adds 1 (or 313 from the last line
|
|
1383
|
+
of a cell row to the first of the next) as y steps. It is faster, and
|
|
1384
|
+
was not measured here.
|
|
1385
|
+
|
|
1386
|
+
### Cycle budget
|
|
1387
|
+
|
|
1388
|
+
Measured in VICE x64sc with the display on, badlines included, for
|
|
1389
|
+
`recipes/kickassembler/wireframe-ships.md`: three objects (8, 5 and 7
|
|
1390
|
+
vertices; 12, 8 and 12 edges), each 37 to 70 pixels across on screen
|
|
1391
|
+
(model), in a 160 x 120 window.
|
|
1392
|
+
|
|
1393
|
+
- Worst update over 48: 155,708 cycles PAL, 157,212 NTSC; the frozen
|
|
1394
|
+
update 131,620 PAL. That is 7.9 PAL frames, about 6 updates a second.
|
|
1395
|
+
- Line call (erase or draw): 2,350 PAL, 2,370 NTSC on average, for 32.5
|
|
1396
|
+
pixels a line in the model, so about 72 cycles a pixel including
|
|
1397
|
+
set-up. Line calls average 97,500 PAL cycles an update: 74% of the
|
|
1398
|
+
last update, 63% of the worst.
|
|
1399
|
+
- Matrix build: 523 PAL, 522 NTSC.
|
|
1400
|
+
- Transform and project: 1,179 PAL, 1,228 NTSC per vertex (nine signed
|
|
1401
|
+
multiplies at 56 to 60 cycles each with the call, by the instruction
|
|
1402
|
+
table, then two projection multiplies, sums and shifts).
|
|
1403
|
+
- The ship (7 vertices, 7 faces, 12 edges): 47,467 PAL, 48,152 NTSC per
|
|
1404
|
+
update.
|
|
1405
|
+
|
|
1406
|
+
No Cost line: one update of this recipe takes 7.9 PAL frames and is
|
|
1407
|
+
not spread across frames, so no per-frame figure was measured. A game
|
|
1408
|
+
that spreads an update across frames states its own worst frame. The
|
|
1409
|
+
two routines to speed up first are the plotter and the per-vertex
|
|
1410
|
+
multiplies. The recipe's plotter is correct and fully measured; it is
|
|
1411
|
+
not a fast plotter (`recipes/kickassembler/wireframe-ships.md`, "Why
|
|
1412
|
+
the line drawer is slow").
|
|
1413
|
+
|
|
1414
|
+
### Recipes
|
|
1415
|
+
|
|
1416
|
+
- `recipes/kickassembler/wireframe-ships.md`: a cube, a pyramid and a
|
|
1417
|
+
ship hull in a hires window, winding cull, outcode reject and
|
|
1418
|
+
per-pixel clip, EOR erase-by-redraw, checked pixel-exact against a
|
|
1419
|
+
Python model on PAL and NTSC.
|
|
1420
|
+
|
|
1421
|
+
### Sources
|
|
1422
|
+
|
|
1423
|
+
- Mark Moxon, "Drawing ships" (line heap, EOR erase, frame order):
|
|
1424
|
+
https://elite.bbcelite.com/deep_dives/drawing_ships.html
|
|
1425
|
+
- Mark Moxon, "Back-face culling" (normal dotted with line of sight):
|
|
1426
|
+
https://elite.bbcelite.com/deep_dives/back-face_culling.html
|
|
1427
|
+
- Mark Moxon, "Line clipping" (LL145, LL118):
|
|
1428
|
+
https://elite.bbcelite.com/deep_dives/line-clipping.html
|
|
1429
|
+
- Mark Moxon, "The split-screen mode in the Commodore 64 version"
|
|
1430
|
+
(lines 51 and 194, 144-pixel space view, sprites for laser sights):
|
|
1431
|
+
https://elite.bbcelite.com/deep_dives/the_split-screen_mode_commodore_64.html
|