jsbeeb 1.1.1
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/.editorconfig +15 -0
- package/.git-blame-ignore-revs +3 -0
- package/.github/copilot-instructions.md +94 -0
- package/.github/workflows/claude-issue-triage.yml +105 -0
- package/.github/workflows/claude.yml +63 -0
- package/.github/workflows/release-please.yml +75 -0
- package/.github/workflows/test-and-deploy.yml +86 -0
- package/.gitmodules +6 -0
- package/.husky/pre-commit +1 -0
- package/.idea/codeStyleSettings.xml +9 -0
- package/.idea/codeStyles/Project.xml +62 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/compiler.xml +22 -0
- package/.idea/copyright/profiles_settings.xml +3 -0
- package/.idea/encodings.xml +6 -0
- package/.idea/inspectionProfiles/Project_Default.xml +7 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/jsLinters/jshint.xml +85 -0
- package/.idea/jsLinters/jslint.xml +15 -0
- package/.idea/jsbeeb.iml +11 -0
- package/.idea/misc.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/prettier.xml +7 -0
- package/.idea/runConfigurations/Debug.xml +5 -0
- package/.idea/scopes/scope_settings.xml +5 -0
- package/.idea/vcs.xml +8 -0
- package/.prettierignore +4 -0
- package/.prettierrc.json +1 -0
- package/.release-please-manifest.json +3 -0
- package/.vscode/launch.json +14 -0
- package/.vscode/settings.json +6 -0
- package/CHANGELOG.md +32 -0
- package/CLAUDE.md +136 -0
- package/COPYING +674 -0
- package/Dockerfile +22 -0
- package/Makefile +30 -0
- package/README.md +259 -0
- package/docker/nginx-default.conf +10 -0
- package/docs/pal-comb-filter-research.md +129 -0
- package/docs/pal-simulation-design.md +368 -0
- package/eslint.config.js +35 -0
- package/index.html +954 -0
- package/jsconfig.json +10 -0
- package/package.json +102 -0
- package/public/discs/README.Irq-Timing +3 -0
- package/public/discs/README.bcdtest +5 -0
- package/public/discs/README.elite +6 -0
- package/public/discs/README.eng_test +3 -0
- package/public/discs/README.protection +7 -0
- package/public/favicon.ico +0 -0
- package/public/images/botbar.png +0 -0
- package/public/images/cub-monitor.png +0 -0
- package/public/images/jsbeeb-example.png +0 -0
- package/public/images/placeholder.png +0 -0
- package/public/images/red-off-16.png +0 -0
- package/public/images/red-on-16.png +0 -0
- package/public/images/sb/CD-left.jpg +0 -0
- package/public/images/sb/CD-right.jpg +0 -0
- package/public/images/sidebar.png +0 -0
- package/public/images/tv.png +0 -0
- package/public/images/yellow-off-16.png +0 -0
- package/public/images/yellow-on-16.png +0 -0
- package/public/jsbeeb-icon.png +0 -0
- package/public/robots.txt +3 -0
- package/public/roms/ADFS1-53.rom +0 -0
- package/public/roms/BASIC.ROM +0 -0
- package/public/roms/README +4 -0
- package/public/roms/a01/BASIC1.rom +0 -0
- package/public/roms/ample.rom +0 -0
- package/public/roms/ats-3.0.rom +0 -0
- package/public/roms/b/DFS-0.9.rom +0 -0
- package/public/roms/b/DFS-1.2.rom +0 -0
- package/public/roms/b1770/dfs1770.rom +0 -0
- package/public/roms/b1770/zADFS.ROM +0 -0
- package/public/roms/bp/dfs.rom +0 -0
- package/public/roms/bp/zADFS.ROM +0 -0
- package/public/roms/bpos.rom +0 -0
- package/public/roms/compact/adfs210.rom +0 -0
- package/public/roms/compact/basic48.rom +0 -0
- package/public/roms/compact/basic486.rom +0 -0
- package/public/roms/compact/os51.rom +0 -0
- package/public/roms/compact/utils.rom +0 -0
- package/public/roms/deos.rom +0 -0
- package/public/roms/master/anfs-4.25.rom +0 -0
- package/public/roms/master/mos.txt +68819 -0
- package/public/roms/master/mos3.20 +0 -0
- package/public/roms/os.rom +0 -0
- package/public/roms/os01.rom +0 -0
- package/public/roms/tube/6502Tube.rom +0 -0
- package/public/roms/tube/ARMeval_100.rom +0 -0
- package/public/roms/tube/BIOS.ROM +0 -0
- package/public/roms/tube/ReCo6502ROM_816 +0 -0
- package/public/roms/tube/Z80_120.rom +0 -0
- package/public/roms/us/USBASIC.rom +0 -0
- package/public/roms/us/USDNFS.rom +0 -0
- package/public/roms/usmos.rom +0 -0
- package/public/sounds/disc525/motor.wav +0 -0
- package/public/sounds/disc525/motoroff.wav +0 -0
- package/public/sounds/disc525/motoron.wav +0 -0
- package/public/sounds/disc525/seek.wav +0 -0
- package/public/sounds/disc525/seek2.wav +0 -0
- package/public/sounds/disc525/seek3.wav +0 -0
- package/public/sounds/disc525/step.wav +0 -0
- package/public/teletext/txt0.dat +0 -0
- package/public/teletext/txt1.dat +0 -0
- package/public/teletext/txt2.dat +0 -0
- package/public/teletext/txt3.dat +0 -0
- package/release-please-config.json +13 -0
- package/run-container.sh +92 -0
- package/src/6502.js +1347 -0
- package/src/6502.opcodes.js +1411 -0
- package/src/acia.js +261 -0
- package/src/adc.js +149 -0
- package/src/analogue-source.js +21 -0
- package/src/app/app.js +175 -0
- package/src/app/electron.js +20 -0
- package/src/app/preload.js +8 -0
- package/src/app-bench.js +33 -0
- package/src/basic/multiline-tetris +9 -0
- package/src/basic-tokenise.js +104 -0
- package/src/canvas.js +177 -0
- package/src/cmos.js +141 -0
- package/src/config.js +165 -0
- package/src/ddnoise.js +138 -0
- package/src/disc-drive.js +371 -0
- package/src/disc-hfe.js +396 -0
- package/src/disc.js +997 -0
- package/src/econet/L3FS.dat +0 -0
- package/src/econet/scsi.dat +0 -0
- package/src/econet.js +714 -0
- package/src/fake6502.js +32 -0
- package/src/fdc.js +248 -0
- package/src/filestore.js +666 -0
- package/src/gamepad-source.js +59 -0
- package/src/gamepads.js +268 -0
- package/src/google-drive.js +160 -0
- package/src/intel-fdc.js +1717 -0
- package/src/jsbeeb.css +363 -0
- package/src/keyboard.js +411 -0
- package/src/lib/README +1 -0
- package/src/lib/webgl-debug.js +911 -0
- package/src/main.js +1759 -0
- package/src/microphone-input.js +149 -0
- package/src/models.js +200 -0
- package/src/mouse-joystick-source.js +107 -0
- package/src/music5000-worklet.js +43 -0
- package/src/music5000.js +207 -0
- package/src/scheduler.js +148 -0
- package/src/serial.js +31 -0
- package/src/soundchip.js +314 -0
- package/src/sth.js +59 -0
- package/src/tapes.js +265 -0
- package/src/teletext.js +348 -0
- package/src/teletext_adaptor.js +172 -0
- package/src/teletext_data.js +1064 -0
- package/src/touchscreen.js +86 -0
- package/src/tube.js +349 -0
- package/src/url-params.js +256 -0
- package/src/utils.js +1090 -0
- package/src/via.js +702 -0
- package/src/video-filters/pal-composite.js +94 -0
- package/src/video-filters/passthrough-filter.js +70 -0
- package/src/video-filters/shaders/pal-composite.frag.glsl +147 -0
- package/src/video-filters/shaders/pal-composite.vert.glsl +8 -0
- package/src/video-filters/shaders/passthrough.frag.glsl +6 -0
- package/src/video-filters/shaders/passthrough.vert.glsl +7 -0
- package/src/video.js +794 -0
- package/src/wd-fdc.js +1344 -0
- package/src/web/audio-handler.js +146 -0
- package/src/web/audio-renderer.js +115 -0
- package/src/web/debug.js +529 -0
- package/tests/integration/RmwX.asm +47 -0
- package/tests/integration/TestInstructionsSource +27 -0
- package/tests/integration/TestTimingsResults +27 -0
- package/tests/integration/TestTimingsSource +61 -0
- package/tests/integration/bcd.js +23 -0
- package/tests/integration/dormann.js +101 -0
- package/tests/integration/dp111timing.js +42 -0
- package/tests/integration/ensure-submodules.js +25 -0
- package/tests/integration/nops.bas +119 -0
- package/tests/integration/nops.js +24 -0
- package/tests/integration/protection.js +26 -0
- package/tests/integration/rmw.js +69 -0
- package/tests/integration/teletext/expected_bug_469.png +0 -0
- package/tests/integration/teletext/expected_flash_0.png +0 -0
- package/tests/integration/teletext/expected_flash_1.png +0 -0
- package/tests/integration/teletext/expected_hoglet_held_char.png +0 -0
- package/tests/integration/teletext/expected_reveal_flash_0.png +0 -0
- package/tests/integration/teletext/expected_reveal_flash_1.png +0 -0
- package/tests/integration/teletext.js +126 -0
- package/tests/integration/timings.js +56 -0
- package/tests/integration/via.js +1125 -0
- package/tests/suite/README.md +7 -0
- package/tests/suite/Test Suite 2.15.txt +373 -0
- package/tests/suite/bin/ start +0 -0
- package/tests/suite/bin/adca +0 -0
- package/tests/suite/bin/adcax +0 -0
- package/tests/suite/bin/adcay +0 -0
- package/tests/suite/bin/adcb +0 -0
- package/tests/suite/bin/adcix +0 -0
- package/tests/suite/bin/adciy +0 -0
- package/tests/suite/bin/adcz +0 -0
- package/tests/suite/bin/adczx +0 -0
- package/tests/suite/bin/alrb +0 -0
- package/tests/suite/bin/ancb +0 -0
- package/tests/suite/bin/anda +0 -0
- package/tests/suite/bin/andax +0 -0
- package/tests/suite/bin/anday +0 -0
- package/tests/suite/bin/andb +0 -0
- package/tests/suite/bin/andix +0 -0
- package/tests/suite/bin/andiy +0 -0
- package/tests/suite/bin/andz +0 -0
- package/tests/suite/bin/andzx +0 -0
- package/tests/suite/bin/aneb +0 -0
- package/tests/suite/bin/arrb +0 -0
- package/tests/suite/bin/asla +0 -0
- package/tests/suite/bin/aslax +0 -0
- package/tests/suite/bin/asln +0 -0
- package/tests/suite/bin/aslz +0 -0
- package/tests/suite/bin/aslzx +0 -0
- package/tests/suite/bin/asoa +0 -0
- package/tests/suite/bin/asoax +0 -0
- package/tests/suite/bin/asoay +0 -0
- package/tests/suite/bin/asoix +0 -0
- package/tests/suite/bin/asoiy +0 -0
- package/tests/suite/bin/asoz +0 -0
- package/tests/suite/bin/asozx +0 -0
- package/tests/suite/bin/axsa +0 -0
- package/tests/suite/bin/axsix +0 -0
- package/tests/suite/bin/axsz +0 -0
- package/tests/suite/bin/axszy +0 -0
- package/tests/suite/bin/bccr +0 -0
- package/tests/suite/bin/bcsr +0 -0
- package/tests/suite/bin/beqr +0 -0
- package/tests/suite/bin/bita +0 -0
- package/tests/suite/bin/bitz +0 -0
- package/tests/suite/bin/bmir +0 -0
- package/tests/suite/bin/bner +0 -0
- package/tests/suite/bin/bplr +0 -0
- package/tests/suite/bin/branchwrap +0 -0
- package/tests/suite/bin/brkn +0 -0
- package/tests/suite/bin/bvcr +0 -0
- package/tests/suite/bin/bvsr +0 -0
- package/tests/suite/bin/cia1pb6 +0 -0
- package/tests/suite/bin/cia1pb7 +0 -0
- package/tests/suite/bin/cia1ta +0 -0
- package/tests/suite/bin/cia1tab +0 -0
- package/tests/suite/bin/cia1tb +0 -0
- package/tests/suite/bin/cia1tb123 +0 -0
- package/tests/suite/bin/cia2pb6 +0 -0
- package/tests/suite/bin/cia2pb7 +0 -0
- package/tests/suite/bin/cia2ta +0 -0
- package/tests/suite/bin/cia2tb +0 -0
- package/tests/suite/bin/cia2tb123 +0 -0
- package/tests/suite/bin/clcn +0 -0
- package/tests/suite/bin/cldn +0 -0
- package/tests/suite/bin/clin +0 -0
- package/tests/suite/bin/clvn +0 -0
- package/tests/suite/bin/cmpa +0 -0
- package/tests/suite/bin/cmpax +0 -0
- package/tests/suite/bin/cmpay +0 -0
- package/tests/suite/bin/cmpb +0 -0
- package/tests/suite/bin/cmpix +0 -0
- package/tests/suite/bin/cmpiy +0 -0
- package/tests/suite/bin/cmpz +0 -0
- package/tests/suite/bin/cmpzx +0 -0
- package/tests/suite/bin/cntdef +0 -0
- package/tests/suite/bin/cnto2 +0 -0
- package/tests/suite/bin/cpuport +0 -0
- package/tests/suite/bin/cputiming +0 -0
- package/tests/suite/bin/cpxa +0 -0
- package/tests/suite/bin/cpxb +0 -0
- package/tests/suite/bin/cpxz +0 -0
- package/tests/suite/bin/cpya +0 -0
- package/tests/suite/bin/cpyb +0 -0
- package/tests/suite/bin/cpyz +0 -0
- package/tests/suite/bin/dcma +0 -0
- package/tests/suite/bin/dcmax +0 -0
- package/tests/suite/bin/dcmay +0 -0
- package/tests/suite/bin/dcmix +0 -0
- package/tests/suite/bin/dcmiy +0 -0
- package/tests/suite/bin/dcmz +0 -0
- package/tests/suite/bin/dcmzx +0 -0
- package/tests/suite/bin/deca +0 -0
- package/tests/suite/bin/decax +0 -0
- package/tests/suite/bin/decz +0 -0
- package/tests/suite/bin/deczx +0 -0
- package/tests/suite/bin/dexn +0 -0
- package/tests/suite/bin/deyn +0 -0
- package/tests/suite/bin/eora +0 -0
- package/tests/suite/bin/eorax +0 -0
- package/tests/suite/bin/eoray +0 -0
- package/tests/suite/bin/eorb +0 -0
- package/tests/suite/bin/eorix +0 -0
- package/tests/suite/bin/eoriy +0 -0
- package/tests/suite/bin/eorz +0 -0
- package/tests/suite/bin/eorzx +0 -0
- package/tests/suite/bin/finish +0 -0
- package/tests/suite/bin/flipos +0 -0
- package/tests/suite/bin/icr01 +0 -0
- package/tests/suite/bin/imr +0 -0
- package/tests/suite/bin/inca +0 -0
- package/tests/suite/bin/incax +0 -0
- package/tests/suite/bin/incz +0 -0
- package/tests/suite/bin/inczx +0 -0
- package/tests/suite/bin/insa +0 -0
- package/tests/suite/bin/insax +0 -0
- package/tests/suite/bin/insay +0 -0
- package/tests/suite/bin/insix +0 -0
- package/tests/suite/bin/insiy +0 -0
- package/tests/suite/bin/insz +0 -0
- package/tests/suite/bin/inszx +0 -0
- package/tests/suite/bin/inxn +0 -0
- package/tests/suite/bin/inyn +0 -0
- package/tests/suite/bin/irq +0 -0
- package/tests/suite/bin/jmpi +0 -0
- package/tests/suite/bin/jmpw +0 -0
- package/tests/suite/bin/jsrw +0 -0
- package/tests/suite/bin/lasay +0 -0
- package/tests/suite/bin/laxa +0 -0
- package/tests/suite/bin/laxay +0 -0
- package/tests/suite/bin/laxix +0 -0
- package/tests/suite/bin/laxiy +0 -0
- package/tests/suite/bin/laxz +0 -0
- package/tests/suite/bin/laxzy +0 -0
- package/tests/suite/bin/ldaa +0 -0
- package/tests/suite/bin/ldaax +0 -0
- package/tests/suite/bin/ldaay +0 -0
- package/tests/suite/bin/ldab +0 -0
- package/tests/suite/bin/ldaix +0 -0
- package/tests/suite/bin/ldaiy +0 -0
- package/tests/suite/bin/ldaz +0 -0
- package/tests/suite/bin/ldazx +0 -0
- package/tests/suite/bin/ldxa +0 -0
- package/tests/suite/bin/ldxay +0 -0
- package/tests/suite/bin/ldxb +0 -0
- package/tests/suite/bin/ldxz +0 -0
- package/tests/suite/bin/ldxzy +0 -0
- package/tests/suite/bin/ldya +0 -0
- package/tests/suite/bin/ldyax +0 -0
- package/tests/suite/bin/ldyb +0 -0
- package/tests/suite/bin/ldyz +0 -0
- package/tests/suite/bin/ldyzx +0 -0
- package/tests/suite/bin/loadth +0 -0
- package/tests/suite/bin/lsea +0 -0
- package/tests/suite/bin/lseax +0 -0
- package/tests/suite/bin/lseay +0 -0
- package/tests/suite/bin/lseix +0 -0
- package/tests/suite/bin/lseiy +0 -0
- package/tests/suite/bin/lsez +0 -0
- package/tests/suite/bin/lsezx +0 -0
- package/tests/suite/bin/lsra +0 -0
- package/tests/suite/bin/lsrax +0 -0
- package/tests/suite/bin/lsrn +0 -0
- package/tests/suite/bin/lsrz +0 -0
- package/tests/suite/bin/lsrzx +0 -0
- package/tests/suite/bin/lxab +0 -0
- package/tests/suite/bin/mmu +0 -0
- package/tests/suite/bin/mmufetch +0 -0
- package/tests/suite/bin/nmi +0 -0
- package/tests/suite/bin/nopa +0 -0
- package/tests/suite/bin/nopax +0 -0
- package/tests/suite/bin/nopb +0 -0
- package/tests/suite/bin/nopn +0 -0
- package/tests/suite/bin/nopz +0 -0
- package/tests/suite/bin/nopzx +0 -0
- package/tests/suite/bin/oneshot +0 -0
- package/tests/suite/bin/oraa +0 -0
- package/tests/suite/bin/oraax +0 -0
- package/tests/suite/bin/oraay +0 -0
- package/tests/suite/bin/orab +0 -0
- package/tests/suite/bin/oraix +0 -0
- package/tests/suite/bin/oraiy +0 -0
- package/tests/suite/bin/oraz +0 -0
- package/tests/suite/bin/orazx +0 -0
- package/tests/suite/bin/phan +0 -0
- package/tests/suite/bin/phpn +0 -0
- package/tests/suite/bin/plan +0 -0
- package/tests/suite/bin/plpn +0 -0
- package/tests/suite/bin/rlaa +0 -0
- package/tests/suite/bin/rlaax +0 -0
- package/tests/suite/bin/rlaay +0 -0
- package/tests/suite/bin/rlaix +0 -0
- package/tests/suite/bin/rlaiy +0 -0
- package/tests/suite/bin/rlaz +0 -0
- package/tests/suite/bin/rlazx +0 -0
- package/tests/suite/bin/rola +0 -0
- package/tests/suite/bin/rolax +0 -0
- package/tests/suite/bin/roln +0 -0
- package/tests/suite/bin/rolz +0 -0
- package/tests/suite/bin/rolzx +0 -0
- package/tests/suite/bin/rora +0 -0
- package/tests/suite/bin/rorax +0 -0
- package/tests/suite/bin/rorn +0 -0
- package/tests/suite/bin/rorz +0 -0
- package/tests/suite/bin/rorzx +0 -0
- package/tests/suite/bin/rraa +0 -0
- package/tests/suite/bin/rraax +0 -0
- package/tests/suite/bin/rraay +0 -0
- package/tests/suite/bin/rraix +0 -0
- package/tests/suite/bin/rraiy +0 -0
- package/tests/suite/bin/rraz +0 -0
- package/tests/suite/bin/rrazx +0 -0
- package/tests/suite/bin/rtin +0 -0
- package/tests/suite/bin/rtsn +0 -0
- package/tests/suite/bin/sbca +0 -0
- package/tests/suite/bin/sbcax +0 -0
- package/tests/suite/bin/sbcay +0 -0
- package/tests/suite/bin/sbcb +0 -0
- package/tests/suite/bin/sbcb(eb) +0 -0
- package/tests/suite/bin/sbcix +0 -0
- package/tests/suite/bin/sbciy +0 -0
- package/tests/suite/bin/sbcz +0 -0
- package/tests/suite/bin/sbczx +0 -0
- package/tests/suite/bin/sbxb +0 -0
- package/tests/suite/bin/secn +0 -0
- package/tests/suite/bin/sedn +0 -0
- package/tests/suite/bin/sein +0 -0
- package/tests/suite/bin/shaay +0 -0
- package/tests/suite/bin/shaiy +0 -0
- package/tests/suite/bin/shsay +0 -0
- package/tests/suite/bin/shxay +0 -0
- package/tests/suite/bin/shyax +0 -0
- package/tests/suite/bin/staa +0 -0
- package/tests/suite/bin/staax +0 -0
- package/tests/suite/bin/staay +0 -0
- package/tests/suite/bin/staix +0 -0
- package/tests/suite/bin/staiy +0 -0
- package/tests/suite/bin/staz +0 -0
- package/tests/suite/bin/stazx +0 -0
- package/tests/suite/bin/stxa +0 -0
- package/tests/suite/bin/stxz +0 -0
- package/tests/suite/bin/stxzy +0 -0
- package/tests/suite/bin/stya +0 -0
- package/tests/suite/bin/styz +0 -0
- package/tests/suite/bin/styzx +0 -0
- package/tests/suite/bin/taxn +0 -0
- package/tests/suite/bin/tayn +0 -0
- package/tests/suite/bin/trap1 +0 -0
- package/tests/suite/bin/trap10 +0 -0
- package/tests/suite/bin/trap11 +0 -0
- package/tests/suite/bin/trap12 +0 -0
- package/tests/suite/bin/trap13 +0 -0
- package/tests/suite/bin/trap14 +0 -0
- package/tests/suite/bin/trap15 +0 -0
- package/tests/suite/bin/trap16 +0 -0
- package/tests/suite/bin/trap17 +0 -0
- package/tests/suite/bin/trap2 +0 -0
- package/tests/suite/bin/trap3 +0 -0
- package/tests/suite/bin/trap4 +0 -0
- package/tests/suite/bin/trap5 +0 -0
- package/tests/suite/bin/trap6 +0 -0
- package/tests/suite/bin/trap7 +0 -0
- package/tests/suite/bin/trap8 +0 -0
- package/tests/suite/bin/trap9 +0 -0
- package/tests/suite/bin/tsxn +0 -0
- package/tests/suite/bin/txan +0 -0
- package/tests/suite/bin/txsn +0 -0
- package/tests/suite/bin/tyan +0 -0
- package/tests/suite/cbm-hackers-post.html +178 -0
- package/tests/suite/cbm-hackers-post.md +78 -0
- package/tests/test-machine.js +288 -0
- package/tests/test-suite.js +147 -0
- package/tests/test.css +7 -0
- package/tests/unit/gzip/test-1 +0 -0
- package/tests/unit/gzip/test-1.gz +0 -0
- package/tests/unit/gzip/test-2 +0 -0
- package/tests/unit/gzip/test-2.gz +0 -0
- package/tests/unit/gzip/test-3 +0 -0
- package/tests/unit/gzip/test-3.gz +0 -0
- package/tests/unit/gzip/test-4 +0 -0
- package/tests/unit/gzip/test-4.gz +0 -0
- package/tests/unit/test-adc.js +307 -0
- package/tests/unit/test-bcd.js +30 -0
- package/tests/unit/test-cmos.js +266 -0
- package/tests/unit/test-disc-drive.js +85 -0
- package/tests/unit/test-disc-hfe.js +347 -0
- package/tests/unit/test-disc.js +232 -0
- package/tests/unit/test-fifo.js +35 -0
- package/tests/unit/test-gamepad-source.js +67 -0
- package/tests/unit/test-gzip.js +22 -0
- package/tests/unit/test-intel-fdc.js +93 -0
- package/tests/unit/test-keyboard.js +410 -0
- package/tests/unit/test-mouse-joystick-source.js +128 -0
- package/tests/unit/test-scheduler.js +190 -0
- package/tests/unit/test-serial.js +154 -0
- package/tests/unit/test-teletext-adaptor.js +359 -0
- package/tests/unit/test-tokenise.js +65 -0
- package/tests/unit/test-url-params.js +398 -0
- package/tests/unit/test-utils.js +276 -0
- package/tests/unit/test-video.js +498 -0
- package/tests/unit/test-zip.js +56 -0
- package/tests/unit/zip/test-mixed.zip +0 -0
- package/tests/unit/zip/test-rom.zip +0 -0
- package/tests/unit/zip/test-ssd.zip +0 -0
- package/tools/fir-generator.js +80 -0
- package/tools/vite-plugin-fir-shader.js +131 -0
- package/vite.config.js +34 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
|
2
|
+
import { Video, HDISPENABLE, VDISPENABLE, USERDISPENABLE, EVERYTHINGENABLED } from "../../src/video.js";
|
|
3
|
+
import * as utils from "../../src/utils.js";
|
|
4
|
+
|
|
5
|
+
// Setup with focus on testing behavior rather than implementation details
|
|
6
|
+
describe("Video", () => {
|
|
7
|
+
let video;
|
|
8
|
+
let mockCpu;
|
|
9
|
+
let mockVia;
|
|
10
|
+
let mockFb32;
|
|
11
|
+
let mockPaintExt;
|
|
12
|
+
let mockTeletext;
|
|
13
|
+
|
|
14
|
+
// Test framebuffer offset at pixel (100, 100) - assumes 1024 pixel width
|
|
15
|
+
const TEST_FB_OFFSET = 1024 * 100 + 100;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
// Reset all mocks
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
|
|
21
|
+
// Mock frame buffer
|
|
22
|
+
mockFb32 = new Uint32Array(1024 * 768);
|
|
23
|
+
|
|
24
|
+
// Mock CPU with videoRead method
|
|
25
|
+
mockCpu = {
|
|
26
|
+
videoRead: vi.fn().mockReturnValue(0),
|
|
27
|
+
interrupt: 0,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Mock VIA with cb2changecallback property
|
|
31
|
+
mockVia = {
|
|
32
|
+
cb2changecallback: null,
|
|
33
|
+
setVBlankInt: vi.fn(),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Mock paint_ext function
|
|
37
|
+
mockPaintExt = vi.fn();
|
|
38
|
+
|
|
39
|
+
// Spy on utils.makeFast32
|
|
40
|
+
vi.spyOn(utils, "makeFast32").mockImplementation((arr) => arr);
|
|
41
|
+
|
|
42
|
+
// Create a video instance (using Model B mode, not Master)
|
|
43
|
+
video = new Video(false, mockFb32, mockPaintExt);
|
|
44
|
+
|
|
45
|
+
// Create the mock teletext manually and replace the one in the video object
|
|
46
|
+
mockTeletext = {
|
|
47
|
+
setDEW: vi.fn(),
|
|
48
|
+
setDISPTMG: vi.fn(),
|
|
49
|
+
setRA0: vi.fn(),
|
|
50
|
+
fetchData: vi.fn(),
|
|
51
|
+
render: vi.fn(),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Replace the teletext instance
|
|
55
|
+
video.teletext = mockTeletext;
|
|
56
|
+
|
|
57
|
+
// Reset and connect CPU and VIA
|
|
58
|
+
video.reset(mockCpu, mockVia);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
vi.restoreAllMocks();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("ULA control register", () => {
|
|
66
|
+
it("should set teletextMode when bit 1 is set", () => {
|
|
67
|
+
// Initially teletext mode should be false
|
|
68
|
+
expect(video.teletextMode).toBe(false);
|
|
69
|
+
|
|
70
|
+
// Write to ULA control register (address 0) with value 2 (bit 1 set)
|
|
71
|
+
video.ula.write(0, 2);
|
|
72
|
+
|
|
73
|
+
// Verify teletext mode was set
|
|
74
|
+
expect(video.teletextMode).toBe(true);
|
|
75
|
+
|
|
76
|
+
// Clear bit 1
|
|
77
|
+
video.ula.write(0, 0);
|
|
78
|
+
|
|
79
|
+
// Verify teletext mode was cleared
|
|
80
|
+
expect(video.teletextMode).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should set correct ulaMode based on bits 2-3", () => {
|
|
84
|
+
// Test mode 0: bits 2-3 = 00
|
|
85
|
+
video.ula.write(0, 0); // 00000000
|
|
86
|
+
expect(video.ulaMode).toBe(0);
|
|
87
|
+
|
|
88
|
+
// Test mode 1: bits 2-3 = 01
|
|
89
|
+
video.ula.write(0, 4); // 00000100
|
|
90
|
+
expect(video.ulaMode).toBe(1);
|
|
91
|
+
|
|
92
|
+
// Test mode 2: bits 2-3 = 10
|
|
93
|
+
video.ula.write(0, 8); // 00001000
|
|
94
|
+
expect(video.ulaMode).toBe(2);
|
|
95
|
+
|
|
96
|
+
// Test mode 3: bits 2-3 = 11
|
|
97
|
+
video.ula.write(0, 12); // 00001100
|
|
98
|
+
expect(video.ulaMode).toBe(3);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should set pixelsPerChar and halfClock based on bit 4", () => {
|
|
102
|
+
// Test with bit 4 clear (default case)
|
|
103
|
+
video.ula.write(0, 0); // 00000000
|
|
104
|
+
expect(video.pixelsPerChar).toBe(16);
|
|
105
|
+
expect(video.halfClock).toBe(true);
|
|
106
|
+
|
|
107
|
+
// Test with bit 4 set
|
|
108
|
+
video.ula.write(0, 16); // 00010000
|
|
109
|
+
expect(video.pixelsPerChar).toBe(8);
|
|
110
|
+
expect(video.halfClock).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("Memory addressing", () => {
|
|
115
|
+
it("should use Mode 7 chunky addressing when MA13 is set", () => {
|
|
116
|
+
// Set teletext mode
|
|
117
|
+
video.ula.write(0, 2);
|
|
118
|
+
expect(video.teletextMode).toBe(true);
|
|
119
|
+
|
|
120
|
+
// Set up MA13 set (addr bit 13 set)
|
|
121
|
+
video.addr = 0x2000; // Bit 13 set
|
|
122
|
+
video.isMaster = true; // Set to Master mode
|
|
123
|
+
|
|
124
|
+
// Set up CPU to return a specific value
|
|
125
|
+
const expectedData = 0x7f;
|
|
126
|
+
mockCpu.videoRead.mockReturnValue(expectedData);
|
|
127
|
+
|
|
128
|
+
// Call readVideoMem which should use chunky addressing mode
|
|
129
|
+
const result = video.readVideoMem();
|
|
130
|
+
|
|
131
|
+
// Verify result
|
|
132
|
+
expect(result).toBe(expectedData);
|
|
133
|
+
|
|
134
|
+
// Check correct address was used for Master
|
|
135
|
+
expect(mockCpu.videoRead).toHaveBeenCalledWith(0x7c00);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should handle Model B quirk for reading 0x3c00 in Mode 7", () => {
|
|
139
|
+
// Set teletext mode
|
|
140
|
+
video.ula.write(0, 2);
|
|
141
|
+
|
|
142
|
+
// Set up addr with MA13 set but MA11 clear
|
|
143
|
+
video.addr = 0x2000; // Bit 13 set, bit 11 clear
|
|
144
|
+
video.isMaster = false; // Set to Model B mode
|
|
145
|
+
|
|
146
|
+
// Call readVideoMem
|
|
147
|
+
video.readVideoMem();
|
|
148
|
+
|
|
149
|
+
// For Model B, should use 0x3c00 instead of 0x7c00
|
|
150
|
+
expect(mockCpu.videoRead).toHaveBeenCalledWith(0x3c00);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should use scanline-based addressing for non-teletext modes", () => {
|
|
154
|
+
// Ensure not in teletext mode
|
|
155
|
+
video.ula.write(0, 0);
|
|
156
|
+
expect(video.teletextMode).toBe(false);
|
|
157
|
+
|
|
158
|
+
// Set test values
|
|
159
|
+
video.addr = 0x1234;
|
|
160
|
+
video.scanlineCounter = 5;
|
|
161
|
+
|
|
162
|
+
// Call readVideoMem
|
|
163
|
+
video.readVideoMem();
|
|
164
|
+
|
|
165
|
+
// Check address formation combines scanline and character address
|
|
166
|
+
const expectedAddr = (5 & 0x07) | (0x1234 << 3);
|
|
167
|
+
expect(mockCpu.videoRead).toHaveBeenCalledWith(expectedAddr & 0x7fff);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("Video mode rendering", () => {
|
|
172
|
+
it("should use different number of pixels per character in different modes", () => {
|
|
173
|
+
// Initialize frame buffer
|
|
174
|
+
mockFb32.fill(0);
|
|
175
|
+
|
|
176
|
+
// Setup for rendering
|
|
177
|
+
video.dispEnabled = EVERYTHINGENABLED;
|
|
178
|
+
|
|
179
|
+
// Use 0xFF (all bits set) as a simple, predictable test pattern
|
|
180
|
+
const testPattern = 0xff;
|
|
181
|
+
|
|
182
|
+
// Setup palette with known colours
|
|
183
|
+
// For 0xFF, the palette index will be 15 (0xF) in all modes
|
|
184
|
+
const testColour = 0xffff0000; // Red
|
|
185
|
+
video.ulaPal.fill(testColour); // Set all palette entries to make test robust
|
|
186
|
+
|
|
187
|
+
// Render the pattern in Mode 0 (8 pixels per character)
|
|
188
|
+
video.ula.write(0, 0); // Set Mode 0
|
|
189
|
+
video.pixelsPerChar = 8;
|
|
190
|
+
|
|
191
|
+
video.blitFb(testPattern, TEST_FB_OFFSET, 8);
|
|
192
|
+
|
|
193
|
+
// Verify all 8 pixels were rendered with the test colour
|
|
194
|
+
for (let i = 0; i < 8; i++) {
|
|
195
|
+
const pixel = mockFb32[TEST_FB_OFFSET + i];
|
|
196
|
+
expect(pixel).toBe(testColour);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Clear frame buffer
|
|
200
|
+
mockFb32.fill(0);
|
|
201
|
+
|
|
202
|
+
// Now render in Mode 2 (16 pixels per character)
|
|
203
|
+
video.ula.write(0, 8); // Set Mode 2
|
|
204
|
+
video.pixelsPerChar = 16;
|
|
205
|
+
|
|
206
|
+
video.blitFb(testPattern, TEST_FB_OFFSET, 16);
|
|
207
|
+
|
|
208
|
+
// Verify all 16 pixels were rendered with the test colour
|
|
209
|
+
for (let i = 0; i < 16; i++) {
|
|
210
|
+
const pixel = mockFb32[TEST_FB_OFFSET + i];
|
|
211
|
+
expect(pixel).toBe(testColour);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// The key difference: Mode 0 renders 8 pixels, Mode 2 renders 16 pixels
|
|
215
|
+
// Both should have all pixels set to the same colour for the 0xFF pattern
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should expand Mode 2 pixels horizontally compared to Mode 3", () => {
|
|
219
|
+
// Mode 2 doubles pixels horizontally: each palette index is used for 2 consecutive pixels
|
|
220
|
+
mockFb32.fill(0);
|
|
221
|
+
|
|
222
|
+
const testData = 0xaa; // 10101010
|
|
223
|
+
|
|
224
|
+
// Setup palette with distinct colours
|
|
225
|
+
video.ulaPal[0] = 0xffff0000; // Red
|
|
226
|
+
video.ulaPal[1] = 0xff00ff00; // Green
|
|
227
|
+
video.ulaPal[2] = 0xff0000ff; // Blue
|
|
228
|
+
video.ulaPal[3] = 0xffffff00; // Yellow
|
|
229
|
+
|
|
230
|
+
video.dispEnabled = EVERYTHINGENABLED;
|
|
231
|
+
|
|
232
|
+
// Render in Mode 2 (16 pixels)
|
|
233
|
+
video.ula.write(0, 8); // Set Mode 2
|
|
234
|
+
video.blitFb(testData, TEST_FB_OFFSET, 16);
|
|
235
|
+
|
|
236
|
+
// Capture Mode 2 result
|
|
237
|
+
const mode2Pixels = Array.from(mockFb32.slice(TEST_FB_OFFSET, TEST_FB_OFFSET + 16));
|
|
238
|
+
|
|
239
|
+
// Key property of Mode 2: consecutive pairs of pixels should be identical (doubling)
|
|
240
|
+
for (let i = 0; i < 16; i += 2) {
|
|
241
|
+
expect(mode2Pixels[i]).toBe(mode2Pixels[i + 1]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Clear buffer
|
|
245
|
+
mockFb32.fill(0);
|
|
246
|
+
|
|
247
|
+
// Render the same data in Mode 3 (8 pixels)
|
|
248
|
+
video.ula.write(0, 12); // Set Mode 3
|
|
249
|
+
video.blitFb(testData, TEST_FB_OFFSET, 8);
|
|
250
|
+
|
|
251
|
+
const mode3Pixels = Array.from(mockFb32.slice(TEST_FB_OFFSET, TEST_FB_OFFSET + 8));
|
|
252
|
+
|
|
253
|
+
// Verify that Mode 2's doubled pixels correspond to Mode 3's pixels
|
|
254
|
+
// mode2[0,1] should equal mode3[0], mode2[2,3] should equal mode3[1], etc.
|
|
255
|
+
for (let i = 0; i < 8; i++) {
|
|
256
|
+
expect(mode2Pixels[i * 2]).toBe(mode3Pixels[i]);
|
|
257
|
+
expect(mode2Pixels[i * 2 + 1]).toBe(mode3Pixels[i]);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should handle palette writes via ULA interface", () => {
|
|
262
|
+
// Setup Mode 2
|
|
263
|
+
video.ula.write(0, 8);
|
|
264
|
+
|
|
265
|
+
// Set palette entries directly to ensure visible colours
|
|
266
|
+
video.ulaPal[0] = 0xff0000ff; // Blue
|
|
267
|
+
video.ulaPal[1] = 0xff00ff00; // Green
|
|
268
|
+
|
|
269
|
+
// Verify palette entries have been initialized
|
|
270
|
+
expect(video.ulaPal[0]).toBe(0xff0000ff);
|
|
271
|
+
expect(video.ulaPal[1]).toBe(0xff00ff00);
|
|
272
|
+
|
|
273
|
+
// Now set a palette entry using the ULA interface
|
|
274
|
+
video.ula.write(1, 0x17); // Palette entry 1, colour 7 (white)
|
|
275
|
+
|
|
276
|
+
// Verify the actual palette entry was updated to the specific colour
|
|
277
|
+
expect(video.actualPal[1]).toBe(7);
|
|
278
|
+
|
|
279
|
+
// Verify that different palette indices have different values
|
|
280
|
+
expect(video.actualPal[0]).not.toBe(video.actualPal[1]);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("Teletext integration", () => {
|
|
285
|
+
beforeEach(() => {
|
|
286
|
+
// Set teletext mode
|
|
287
|
+
video.ula.write(0, 2);
|
|
288
|
+
expect(video.teletextMode).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("should call teletext.setDISPTMG when display enable state changes", () => {
|
|
292
|
+
// Clear the teletext mock history
|
|
293
|
+
mockTeletext.setDISPTMG.mockClear();
|
|
294
|
+
|
|
295
|
+
// Test display enable set - all required display flags set
|
|
296
|
+
video.dispEnabled = 0;
|
|
297
|
+
video.dispEnableSet(HDISPENABLE | VDISPENABLE | USERDISPENABLE);
|
|
298
|
+
|
|
299
|
+
// The mask in dispEnableChanged is HDISPENABLE | VDISPENABLE | USERDISPENABLE
|
|
300
|
+
expect(mockTeletext.setDISPTMG).toHaveBeenCalledWith(true);
|
|
301
|
+
|
|
302
|
+
// Clear the mock history
|
|
303
|
+
mockTeletext.setDISPTMG.mockClear();
|
|
304
|
+
|
|
305
|
+
// Test display enable clear
|
|
306
|
+
video.dispEnableClear(HDISPENABLE);
|
|
307
|
+
|
|
308
|
+
// Now setDISPTMG is called with false
|
|
309
|
+
expect(mockTeletext.setDISPTMG).toHaveBeenCalledWith(false);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should update teletext.setRA0 correctly based on scanlineCounter", () => {
|
|
313
|
+
// Initialize scanlineCounter to 0
|
|
314
|
+
video.scanlineCounter = 0;
|
|
315
|
+
|
|
316
|
+
// Clear the mock history
|
|
317
|
+
mockTeletext.setRA0.mockClear();
|
|
318
|
+
|
|
319
|
+
// For non-interlaced mode, the RA0 value is just the lowest bit of scanlineCounter
|
|
320
|
+
video.interlacedSyncAndVideo = false;
|
|
321
|
+
|
|
322
|
+
// We need to set up the registers to allow endOfScanline to work
|
|
323
|
+
video.regs[9] = 10; // Max scanline number that triggers endOfCharacterLine
|
|
324
|
+
|
|
325
|
+
// Call endOfScanline to increment scanlineCounter to 1
|
|
326
|
+
video.endOfScanline();
|
|
327
|
+
|
|
328
|
+
// Verify scanlineCounter was incremented
|
|
329
|
+
expect(video.scanlineCounter).toBe(1);
|
|
330
|
+
|
|
331
|
+
// Verify setRA0 was called with the correct value (bit 0 is 1)
|
|
332
|
+
expect(mockTeletext.setRA0).toHaveBeenCalledWith(true);
|
|
333
|
+
|
|
334
|
+
// Clear the mock history
|
|
335
|
+
mockTeletext.setRA0.mockClear();
|
|
336
|
+
|
|
337
|
+
// Call endOfScanline again to increment scanlineCounter to 2
|
|
338
|
+
video.endOfScanline();
|
|
339
|
+
|
|
340
|
+
// Verify scanlineCounter was incremented
|
|
341
|
+
expect(video.scanlineCounter).toBe(2);
|
|
342
|
+
|
|
343
|
+
// Verify setRA0 was called with the correct value (bit 0 is 0)
|
|
344
|
+
expect(mockTeletext.setRA0).toHaveBeenCalledWith(false);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("should handle interlaced RA0 correctly", () => {
|
|
348
|
+
// Set up for interlaced mode
|
|
349
|
+
video.interlacedSyncAndVideo = true;
|
|
350
|
+
video.scanlineCounter = 0;
|
|
351
|
+
video.frameCount = 1; // Odd frame number
|
|
352
|
+
|
|
353
|
+
// Initialize registers
|
|
354
|
+
video.regs[9] = 10; // Max scanline number
|
|
355
|
+
|
|
356
|
+
// Clear the mock history
|
|
357
|
+
mockTeletext.setRA0.mockClear();
|
|
358
|
+
|
|
359
|
+
// Call endOfScanline
|
|
360
|
+
video.endOfScanline();
|
|
361
|
+
|
|
362
|
+
// In interlaced mode with odd frame count, externalScanline is scanlineCounter + 1
|
|
363
|
+
// So even though scanlineCounter is now 2 (bit 0 = 0), externalScanline is 3 (bit 0 = 1)
|
|
364
|
+
expect(mockTeletext.setRA0).toHaveBeenCalledWith(true);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("should call setDEW when vsync state changes", () => {
|
|
368
|
+
// Setup necessary conditions for vsync
|
|
369
|
+
video.regs[7] = 10; // Vertical sync position
|
|
370
|
+
video.vertCounter = 10;
|
|
371
|
+
video.inVSync = false;
|
|
372
|
+
video.hadVSyncThisRow = false;
|
|
373
|
+
video.horizCounter = 1; // Non-zero to avoid end-of-line logic
|
|
374
|
+
|
|
375
|
+
// Clear mock history
|
|
376
|
+
mockTeletext.setDEW.mockClear();
|
|
377
|
+
|
|
378
|
+
// Calling polltime with the right conditions
|
|
379
|
+
video.polltime(1);
|
|
380
|
+
|
|
381
|
+
// Since we've set up the vertical counter to match R7, vsync should start
|
|
382
|
+
expect(video.inVSync).toBe(true);
|
|
383
|
+
|
|
384
|
+
// Verify setDEW was called with the correct parameter
|
|
385
|
+
expect(mockTeletext.setDEW).toHaveBeenCalledWith(true);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe("Teletext rendering", () => {
|
|
390
|
+
beforeEach(() => {
|
|
391
|
+
// Set teletext mode
|
|
392
|
+
video.ula.write(0, 2);
|
|
393
|
+
|
|
394
|
+
// Set up all display flags to make rendering active
|
|
395
|
+
video.dispEnabled = EVERYTHINGENABLED;
|
|
396
|
+
|
|
397
|
+
// Set coords to visible area
|
|
398
|
+
video.bitmapX = 100;
|
|
399
|
+
video.bitmapY = 100;
|
|
400
|
+
|
|
401
|
+
// Set test data for video memory
|
|
402
|
+
mockCpu.videoRead.mockReturnValue(0x42);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("should call fetchData in teletext mode", () => {
|
|
406
|
+
// Clear mock history
|
|
407
|
+
mockTeletext.fetchData.mockClear();
|
|
408
|
+
|
|
409
|
+
// Set up horizCounter to avoid vsync logic
|
|
410
|
+
video.horizCounter = 10;
|
|
411
|
+
|
|
412
|
+
// Poll to trigger rendering
|
|
413
|
+
video.polltime(1);
|
|
414
|
+
|
|
415
|
+
// Verify fetchData was called with the correct parameter
|
|
416
|
+
expect(mockTeletext.fetchData).toHaveBeenCalledWith(0x42);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("should call render in teletext mode", () => {
|
|
420
|
+
// Clear mock history
|
|
421
|
+
mockTeletext.render.mockClear();
|
|
422
|
+
|
|
423
|
+
// Set up horizCounter to avoid vsync logic
|
|
424
|
+
video.horizCounter = 10;
|
|
425
|
+
|
|
426
|
+
// Poll to trigger rendering
|
|
427
|
+
video.polltime(1);
|
|
428
|
+
|
|
429
|
+
// Verify render was called with the expected parameters
|
|
430
|
+
expect(mockTeletext.render).toHaveBeenCalledWith(expect.any(Uint32Array), expect.any(Number));
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("should not render in non-teletext mode", () => {
|
|
434
|
+
// Switch to non-teletext mode
|
|
435
|
+
video.ula.write(0, 0);
|
|
436
|
+
expect(video.teletextMode).toBe(false);
|
|
437
|
+
|
|
438
|
+
// Clear mock history
|
|
439
|
+
mockTeletext.render.mockClear();
|
|
440
|
+
|
|
441
|
+
// Set up horizCounter to avoid vsync logic
|
|
442
|
+
video.horizCounter = 10;
|
|
443
|
+
|
|
444
|
+
// Poll to trigger rendering
|
|
445
|
+
video.polltime(1);
|
|
446
|
+
|
|
447
|
+
// Verify render was not called
|
|
448
|
+
expect(mockTeletext.render).not.toHaveBeenCalled();
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe("Hardware scrolling address translation", () => {
|
|
453
|
+
beforeEach(() => {
|
|
454
|
+
// Set up for graphics mode (non-teletext)
|
|
455
|
+
video.ula.write(0, 0);
|
|
456
|
+
video.addr = 0x1000; // Set MA12 to trigger translation
|
|
457
|
+
video.scanlineCounter = 0;
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("should apply mode 0-2 scroll offset (subtract 10)", () => {
|
|
461
|
+
video.setScreenHwScroll(2); // C1=1, C0=0 -> MODE 0-2, subtract 0x5000 (10 from MA11-MA8)
|
|
462
|
+
mockCpu.videoRead.mockReturnValue(0x42);
|
|
463
|
+
|
|
464
|
+
const result = video.readVideoMem();
|
|
465
|
+
|
|
466
|
+
// MA=0x1000: MA12=1 (trigger), MA11-MA8=0x0, MA7-MA0=0x00
|
|
467
|
+
// adjustedHigh = (0x0 - 10) & 0x0f = 0x6
|
|
468
|
+
// Expected: ((0x6 << 11) | (0x00 << 3) | 0x0) = 0x3000
|
|
469
|
+
// Matches beebjit: (0x1000 * 8) - 0x5000 = 0x8000 - 0x5000 = 0x3000
|
|
470
|
+
expect(mockCpu.videoRead).toHaveBeenCalledWith(0x3000);
|
|
471
|
+
expect(result).toBe(0x42);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("should not affect addresses when MA12 is clear", () => {
|
|
475
|
+
video.setScreenHwScroll(2);
|
|
476
|
+
video.addr = 0x0500; // MA12 clear
|
|
477
|
+
|
|
478
|
+
video.readVideoMem();
|
|
479
|
+
|
|
480
|
+
// No translation: ((0x5 << 11) | (0x00 << 3) | 0x0) = 0x2800
|
|
481
|
+
expect(mockCpu.videoRead).toHaveBeenCalledWith(0x2800);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("should handle scanlineCounter offset correctly", () => {
|
|
485
|
+
video.setScreenHwScroll(0); // C1=0, C0=0 -> MODE 3, subtract 0x4000 (8 from MA11-MA8)
|
|
486
|
+
video.addr = 0x1000;
|
|
487
|
+
video.scanlineCounter = 5; // RA = 5
|
|
488
|
+
|
|
489
|
+
video.readVideoMem();
|
|
490
|
+
|
|
491
|
+
// MA=0x1000: MA12=1 (trigger), MA11-MA8=0x0, MA7-MA0=0x00, RA=5
|
|
492
|
+
// adjustedHigh = (0x0 - 8) & 0x0f = 0x8
|
|
493
|
+
// Expected: ((0x8 << 11) | (0x00 << 3) | 0x5) = 0x4005
|
|
494
|
+
// Matches beebjit: (0x1000 * 8) - 0x4000 + 5 = 0x8000 - 0x4000 + 5 = 0x4005
|
|
495
|
+
expect(mockCpu.videoRead).toHaveBeenCalledWith(0x4005);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as utils from "../../src/utils.js";
|
|
4
|
+
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
describe("zip tests", function () {
|
|
11
|
+
it("should unzip SSD files", () => {
|
|
12
|
+
const zipData = new Uint8Array(fs.readFileSync(join(__dirname, "zip", "test-ssd.zip")));
|
|
13
|
+
const result = utils.unzipDiscImage(zipData);
|
|
14
|
+
|
|
15
|
+
expect(result.name).toBe("test.ssd");
|
|
16
|
+
expect(result.data instanceof Uint8Array).toBeTruthy();
|
|
17
|
+
|
|
18
|
+
// Convert to string to check content
|
|
19
|
+
const content = Array.from(result.data)
|
|
20
|
+
.map((b) => String.fromCharCode(b))
|
|
21
|
+
.join("");
|
|
22
|
+
expect(content).toBe("This is a test SSD file\n");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should unzip ROM files", () => {
|
|
26
|
+
const zipData = new Uint8Array(fs.readFileSync(join(__dirname, "zip", "test-rom.zip")));
|
|
27
|
+
const result = utils.unzipRomImage(zipData);
|
|
28
|
+
|
|
29
|
+
expect(result.name).toBe("test.rom");
|
|
30
|
+
expect(result.data instanceof Uint8Array).toBeTruthy();
|
|
31
|
+
|
|
32
|
+
// Convert to string to check content
|
|
33
|
+
const content = Array.from(result.data)
|
|
34
|
+
.map((b) => String.fromCharCode(b))
|
|
35
|
+
.join("");
|
|
36
|
+
expect(content).toBe("This is a test ROM file\n");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should handle ZIP with multiple files by picking the first compatible one", () => {
|
|
40
|
+
const zipData = new Uint8Array(fs.readFileSync(join(__dirname, "zip", "test-mixed.zip")));
|
|
41
|
+
const result = utils.unzipDiscImage(zipData);
|
|
42
|
+
|
|
43
|
+
// Should get the first compatible file (order may vary)
|
|
44
|
+
expect(result.name === "test.ssd" || result.name === "test.rom").toBeTruthy();
|
|
45
|
+
expect(result.data instanceof Uint8Array).toBeTruthy();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should throw error for ZIP with no compatible files", () => {
|
|
49
|
+
// Create a simple ZIP with incompatible file
|
|
50
|
+
const zipData = new Uint8Array(fs.readFileSync(join(__dirname, "zip", "test-ssd.zip")));
|
|
51
|
+
|
|
52
|
+
expect(() => {
|
|
53
|
+
utils.unzipRomImage(zipData); // Try to extract ROM from SSD zip
|
|
54
|
+
}).toThrow(/Couldn't find any compatible files/);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FIR filter coefficient generator for PAL composite video chroma filtering.
|
|
3
|
+
*
|
|
4
|
+
* Based on reverse-engineering Rich's original coefficients (generated via ChatGPT):
|
|
5
|
+
* - Uses Kaiser window with β=5
|
|
6
|
+
* - Actual cutoff frequency is 0.5× the specified nominal frequency
|
|
7
|
+
* - Normalized coefficients (sum = 1.0)
|
|
8
|
+
* - Sample rate: 16 MHz
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const SAMPLE_RATE_HZ = 16e6;
|
|
12
|
+
const BETA = 5.0;
|
|
13
|
+
|
|
14
|
+
function sinc(x) {
|
|
15
|
+
// Sinc function: sin(pi*x) / (pi*x), with sinc(0) = 1
|
|
16
|
+
if (Math.abs(x) < 1e-10) {
|
|
17
|
+
return 1.0;
|
|
18
|
+
}
|
|
19
|
+
return Math.sin(Math.PI * x) / (Math.PI * x);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function kaiserWindow(n, M, beta) {
|
|
23
|
+
// Kaiser window with parameter beta
|
|
24
|
+
const arg = beta * Math.sqrt(1 - Math.pow((2 * n) / (M - 1) - 1, 2));
|
|
25
|
+
return Math.cosh(arg) / Math.cosh(beta);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function generateFirLowpass(numTaps, cutoffNormalized, beta) {
|
|
29
|
+
// Generate FIR lowpass filter using Kaiser windowed sinc
|
|
30
|
+
const center = (numTaps - 1) / 2;
|
|
31
|
+
const coefficients = [];
|
|
32
|
+
|
|
33
|
+
for (let n = 0; n < numTaps; n++) {
|
|
34
|
+
// Ideal lowpass filter (sinc function)
|
|
35
|
+
const t = n - center;
|
|
36
|
+
const h = 2 * cutoffNormalized * sinc(2 * cutoffNormalized * t);
|
|
37
|
+
|
|
38
|
+
// Apply Kaiser window
|
|
39
|
+
const w = kaiserWindow(n, numTaps, beta);
|
|
40
|
+
coefficients.push(h * w);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Normalize so sum equals 1.0
|
|
44
|
+
const total = coefficients.reduce((sum, c) => sum + c, 0);
|
|
45
|
+
return coefficients.map((c) => c / total);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate FIR filter coefficients and format as GLSL array initialization.
|
|
50
|
+
*
|
|
51
|
+
* @param {number} numTaps - Number of filter taps (must be odd)
|
|
52
|
+
* @param {number} nominalCutoffMhz - Nominal cutoff frequency in MHz
|
|
53
|
+
* @param {string} indent - Indentation string to prepend to each line
|
|
54
|
+
* @returns {string} GLSL array initialization code
|
|
55
|
+
*/
|
|
56
|
+
export function generateFirCoefficients(numTaps, nominalCutoffMhz, indent) {
|
|
57
|
+
// Validate inputs
|
|
58
|
+
if (numTaps <= 0 || numTaps % 2 === 0) {
|
|
59
|
+
throw new Error(`numTaps must be a positive odd number, got ${numTaps}`);
|
|
60
|
+
}
|
|
61
|
+
if (nominalCutoffMhz <= 0 || nominalCutoffMhz > 8) {
|
|
62
|
+
throw new Error(`nominalCutoffMhz must be between 0 and 8 MHz (Nyquist), got ${nominalCutoffMhz}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Key discovery: actual cutoff is 0.5× the specified frequency
|
|
66
|
+
const actualCutoffHz = nominalCutoffMhz * 1e6 * 0.5;
|
|
67
|
+
const cutoffNormalized = actualCutoffHz / (SAMPLE_RATE_HZ / 2.0);
|
|
68
|
+
|
|
69
|
+
const coeffs = generateFirLowpass(numTaps, cutoffNormalized, BETA);
|
|
70
|
+
|
|
71
|
+
// Format as GLSL array initialization (4 per line)
|
|
72
|
+
const lines = [];
|
|
73
|
+
for (let i = 0; i < coeffs.length; i += 4) {
|
|
74
|
+
const chunk = coeffs.slice(i, i + 4);
|
|
75
|
+
const formatted = chunk.map((c, j) => `FIR[${i + j}] = ${c.toPrecision(10)}`).join("; ");
|
|
76
|
+
lines.push(`${indent}${formatted};`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|