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.
Files changed (498) hide show
  1. package/.editorconfig +15 -0
  2. package/.git-blame-ignore-revs +3 -0
  3. package/.github/copilot-instructions.md +94 -0
  4. package/.github/workflows/claude-issue-triage.yml +105 -0
  5. package/.github/workflows/claude.yml +63 -0
  6. package/.github/workflows/release-please.yml +75 -0
  7. package/.github/workflows/test-and-deploy.yml +86 -0
  8. package/.gitmodules +6 -0
  9. package/.husky/pre-commit +1 -0
  10. package/.idea/codeStyleSettings.xml +9 -0
  11. package/.idea/codeStyles/Project.xml +62 -0
  12. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  13. package/.idea/compiler.xml +22 -0
  14. package/.idea/copyright/profiles_settings.xml +3 -0
  15. package/.idea/encodings.xml +6 -0
  16. package/.idea/inspectionProfiles/Project_Default.xml +7 -0
  17. package/.idea/jsLibraryMappings.xml +6 -0
  18. package/.idea/jsLinters/jshint.xml +85 -0
  19. package/.idea/jsLinters/jslint.xml +15 -0
  20. package/.idea/jsbeeb.iml +11 -0
  21. package/.idea/misc.xml +6 -0
  22. package/.idea/modules.xml +8 -0
  23. package/.idea/prettier.xml +7 -0
  24. package/.idea/runConfigurations/Debug.xml +5 -0
  25. package/.idea/scopes/scope_settings.xml +5 -0
  26. package/.idea/vcs.xml +8 -0
  27. package/.prettierignore +4 -0
  28. package/.prettierrc.json +1 -0
  29. package/.release-please-manifest.json +3 -0
  30. package/.vscode/launch.json +14 -0
  31. package/.vscode/settings.json +6 -0
  32. package/CHANGELOG.md +32 -0
  33. package/CLAUDE.md +136 -0
  34. package/COPYING +674 -0
  35. package/Dockerfile +22 -0
  36. package/Makefile +30 -0
  37. package/README.md +259 -0
  38. package/docker/nginx-default.conf +10 -0
  39. package/docs/pal-comb-filter-research.md +129 -0
  40. package/docs/pal-simulation-design.md +368 -0
  41. package/eslint.config.js +35 -0
  42. package/index.html +954 -0
  43. package/jsconfig.json +10 -0
  44. package/package.json +102 -0
  45. package/public/discs/README.Irq-Timing +3 -0
  46. package/public/discs/README.bcdtest +5 -0
  47. package/public/discs/README.elite +6 -0
  48. package/public/discs/README.eng_test +3 -0
  49. package/public/discs/README.protection +7 -0
  50. package/public/favicon.ico +0 -0
  51. package/public/images/botbar.png +0 -0
  52. package/public/images/cub-monitor.png +0 -0
  53. package/public/images/jsbeeb-example.png +0 -0
  54. package/public/images/placeholder.png +0 -0
  55. package/public/images/red-off-16.png +0 -0
  56. package/public/images/red-on-16.png +0 -0
  57. package/public/images/sb/CD-left.jpg +0 -0
  58. package/public/images/sb/CD-right.jpg +0 -0
  59. package/public/images/sidebar.png +0 -0
  60. package/public/images/tv.png +0 -0
  61. package/public/images/yellow-off-16.png +0 -0
  62. package/public/images/yellow-on-16.png +0 -0
  63. package/public/jsbeeb-icon.png +0 -0
  64. package/public/robots.txt +3 -0
  65. package/public/roms/ADFS1-53.rom +0 -0
  66. package/public/roms/BASIC.ROM +0 -0
  67. package/public/roms/README +4 -0
  68. package/public/roms/a01/BASIC1.rom +0 -0
  69. package/public/roms/ample.rom +0 -0
  70. package/public/roms/ats-3.0.rom +0 -0
  71. package/public/roms/b/DFS-0.9.rom +0 -0
  72. package/public/roms/b/DFS-1.2.rom +0 -0
  73. package/public/roms/b1770/dfs1770.rom +0 -0
  74. package/public/roms/b1770/zADFS.ROM +0 -0
  75. package/public/roms/bp/dfs.rom +0 -0
  76. package/public/roms/bp/zADFS.ROM +0 -0
  77. package/public/roms/bpos.rom +0 -0
  78. package/public/roms/compact/adfs210.rom +0 -0
  79. package/public/roms/compact/basic48.rom +0 -0
  80. package/public/roms/compact/basic486.rom +0 -0
  81. package/public/roms/compact/os51.rom +0 -0
  82. package/public/roms/compact/utils.rom +0 -0
  83. package/public/roms/deos.rom +0 -0
  84. package/public/roms/master/anfs-4.25.rom +0 -0
  85. package/public/roms/master/mos.txt +68819 -0
  86. package/public/roms/master/mos3.20 +0 -0
  87. package/public/roms/os.rom +0 -0
  88. package/public/roms/os01.rom +0 -0
  89. package/public/roms/tube/6502Tube.rom +0 -0
  90. package/public/roms/tube/ARMeval_100.rom +0 -0
  91. package/public/roms/tube/BIOS.ROM +0 -0
  92. package/public/roms/tube/ReCo6502ROM_816 +0 -0
  93. package/public/roms/tube/Z80_120.rom +0 -0
  94. package/public/roms/us/USBASIC.rom +0 -0
  95. package/public/roms/us/USDNFS.rom +0 -0
  96. package/public/roms/usmos.rom +0 -0
  97. package/public/sounds/disc525/motor.wav +0 -0
  98. package/public/sounds/disc525/motoroff.wav +0 -0
  99. package/public/sounds/disc525/motoron.wav +0 -0
  100. package/public/sounds/disc525/seek.wav +0 -0
  101. package/public/sounds/disc525/seek2.wav +0 -0
  102. package/public/sounds/disc525/seek3.wav +0 -0
  103. package/public/sounds/disc525/step.wav +0 -0
  104. package/public/teletext/txt0.dat +0 -0
  105. package/public/teletext/txt1.dat +0 -0
  106. package/public/teletext/txt2.dat +0 -0
  107. package/public/teletext/txt3.dat +0 -0
  108. package/release-please-config.json +13 -0
  109. package/run-container.sh +92 -0
  110. package/src/6502.js +1347 -0
  111. package/src/6502.opcodes.js +1411 -0
  112. package/src/acia.js +261 -0
  113. package/src/adc.js +149 -0
  114. package/src/analogue-source.js +21 -0
  115. package/src/app/app.js +175 -0
  116. package/src/app/electron.js +20 -0
  117. package/src/app/preload.js +8 -0
  118. package/src/app-bench.js +33 -0
  119. package/src/basic/multiline-tetris +9 -0
  120. package/src/basic-tokenise.js +104 -0
  121. package/src/canvas.js +177 -0
  122. package/src/cmos.js +141 -0
  123. package/src/config.js +165 -0
  124. package/src/ddnoise.js +138 -0
  125. package/src/disc-drive.js +371 -0
  126. package/src/disc-hfe.js +396 -0
  127. package/src/disc.js +997 -0
  128. package/src/econet/L3FS.dat +0 -0
  129. package/src/econet/scsi.dat +0 -0
  130. package/src/econet.js +714 -0
  131. package/src/fake6502.js +32 -0
  132. package/src/fdc.js +248 -0
  133. package/src/filestore.js +666 -0
  134. package/src/gamepad-source.js +59 -0
  135. package/src/gamepads.js +268 -0
  136. package/src/google-drive.js +160 -0
  137. package/src/intel-fdc.js +1717 -0
  138. package/src/jsbeeb.css +363 -0
  139. package/src/keyboard.js +411 -0
  140. package/src/lib/README +1 -0
  141. package/src/lib/webgl-debug.js +911 -0
  142. package/src/main.js +1759 -0
  143. package/src/microphone-input.js +149 -0
  144. package/src/models.js +200 -0
  145. package/src/mouse-joystick-source.js +107 -0
  146. package/src/music5000-worklet.js +43 -0
  147. package/src/music5000.js +207 -0
  148. package/src/scheduler.js +148 -0
  149. package/src/serial.js +31 -0
  150. package/src/soundchip.js +314 -0
  151. package/src/sth.js +59 -0
  152. package/src/tapes.js +265 -0
  153. package/src/teletext.js +348 -0
  154. package/src/teletext_adaptor.js +172 -0
  155. package/src/teletext_data.js +1064 -0
  156. package/src/touchscreen.js +86 -0
  157. package/src/tube.js +349 -0
  158. package/src/url-params.js +256 -0
  159. package/src/utils.js +1090 -0
  160. package/src/via.js +702 -0
  161. package/src/video-filters/pal-composite.js +94 -0
  162. package/src/video-filters/passthrough-filter.js +70 -0
  163. package/src/video-filters/shaders/pal-composite.frag.glsl +147 -0
  164. package/src/video-filters/shaders/pal-composite.vert.glsl +8 -0
  165. package/src/video-filters/shaders/passthrough.frag.glsl +6 -0
  166. package/src/video-filters/shaders/passthrough.vert.glsl +7 -0
  167. package/src/video.js +794 -0
  168. package/src/wd-fdc.js +1344 -0
  169. package/src/web/audio-handler.js +146 -0
  170. package/src/web/audio-renderer.js +115 -0
  171. package/src/web/debug.js +529 -0
  172. package/tests/integration/RmwX.asm +47 -0
  173. package/tests/integration/TestInstructionsSource +27 -0
  174. package/tests/integration/TestTimingsResults +27 -0
  175. package/tests/integration/TestTimingsSource +61 -0
  176. package/tests/integration/bcd.js +23 -0
  177. package/tests/integration/dormann.js +101 -0
  178. package/tests/integration/dp111timing.js +42 -0
  179. package/tests/integration/ensure-submodules.js +25 -0
  180. package/tests/integration/nops.bas +119 -0
  181. package/tests/integration/nops.js +24 -0
  182. package/tests/integration/protection.js +26 -0
  183. package/tests/integration/rmw.js +69 -0
  184. package/tests/integration/teletext/expected_bug_469.png +0 -0
  185. package/tests/integration/teletext/expected_flash_0.png +0 -0
  186. package/tests/integration/teletext/expected_flash_1.png +0 -0
  187. package/tests/integration/teletext/expected_hoglet_held_char.png +0 -0
  188. package/tests/integration/teletext/expected_reveal_flash_0.png +0 -0
  189. package/tests/integration/teletext/expected_reveal_flash_1.png +0 -0
  190. package/tests/integration/teletext.js +126 -0
  191. package/tests/integration/timings.js +56 -0
  192. package/tests/integration/via.js +1125 -0
  193. package/tests/suite/README.md +7 -0
  194. package/tests/suite/Test Suite 2.15.txt +373 -0
  195. package/tests/suite/bin/ start +0 -0
  196. package/tests/suite/bin/adca +0 -0
  197. package/tests/suite/bin/adcax +0 -0
  198. package/tests/suite/bin/adcay +0 -0
  199. package/tests/suite/bin/adcb +0 -0
  200. package/tests/suite/bin/adcix +0 -0
  201. package/tests/suite/bin/adciy +0 -0
  202. package/tests/suite/bin/adcz +0 -0
  203. package/tests/suite/bin/adczx +0 -0
  204. package/tests/suite/bin/alrb +0 -0
  205. package/tests/suite/bin/ancb +0 -0
  206. package/tests/suite/bin/anda +0 -0
  207. package/tests/suite/bin/andax +0 -0
  208. package/tests/suite/bin/anday +0 -0
  209. package/tests/suite/bin/andb +0 -0
  210. package/tests/suite/bin/andix +0 -0
  211. package/tests/suite/bin/andiy +0 -0
  212. package/tests/suite/bin/andz +0 -0
  213. package/tests/suite/bin/andzx +0 -0
  214. package/tests/suite/bin/aneb +0 -0
  215. package/tests/suite/bin/arrb +0 -0
  216. package/tests/suite/bin/asla +0 -0
  217. package/tests/suite/bin/aslax +0 -0
  218. package/tests/suite/bin/asln +0 -0
  219. package/tests/suite/bin/aslz +0 -0
  220. package/tests/suite/bin/aslzx +0 -0
  221. package/tests/suite/bin/asoa +0 -0
  222. package/tests/suite/bin/asoax +0 -0
  223. package/tests/suite/bin/asoay +0 -0
  224. package/tests/suite/bin/asoix +0 -0
  225. package/tests/suite/bin/asoiy +0 -0
  226. package/tests/suite/bin/asoz +0 -0
  227. package/tests/suite/bin/asozx +0 -0
  228. package/tests/suite/bin/axsa +0 -0
  229. package/tests/suite/bin/axsix +0 -0
  230. package/tests/suite/bin/axsz +0 -0
  231. package/tests/suite/bin/axszy +0 -0
  232. package/tests/suite/bin/bccr +0 -0
  233. package/tests/suite/bin/bcsr +0 -0
  234. package/tests/suite/bin/beqr +0 -0
  235. package/tests/suite/bin/bita +0 -0
  236. package/tests/suite/bin/bitz +0 -0
  237. package/tests/suite/bin/bmir +0 -0
  238. package/tests/suite/bin/bner +0 -0
  239. package/tests/suite/bin/bplr +0 -0
  240. package/tests/suite/bin/branchwrap +0 -0
  241. package/tests/suite/bin/brkn +0 -0
  242. package/tests/suite/bin/bvcr +0 -0
  243. package/tests/suite/bin/bvsr +0 -0
  244. package/tests/suite/bin/cia1pb6 +0 -0
  245. package/tests/suite/bin/cia1pb7 +0 -0
  246. package/tests/suite/bin/cia1ta +0 -0
  247. package/tests/suite/bin/cia1tab +0 -0
  248. package/tests/suite/bin/cia1tb +0 -0
  249. package/tests/suite/bin/cia1tb123 +0 -0
  250. package/tests/suite/bin/cia2pb6 +0 -0
  251. package/tests/suite/bin/cia2pb7 +0 -0
  252. package/tests/suite/bin/cia2ta +0 -0
  253. package/tests/suite/bin/cia2tb +0 -0
  254. package/tests/suite/bin/cia2tb123 +0 -0
  255. package/tests/suite/bin/clcn +0 -0
  256. package/tests/suite/bin/cldn +0 -0
  257. package/tests/suite/bin/clin +0 -0
  258. package/tests/suite/bin/clvn +0 -0
  259. package/tests/suite/bin/cmpa +0 -0
  260. package/tests/suite/bin/cmpax +0 -0
  261. package/tests/suite/bin/cmpay +0 -0
  262. package/tests/suite/bin/cmpb +0 -0
  263. package/tests/suite/bin/cmpix +0 -0
  264. package/tests/suite/bin/cmpiy +0 -0
  265. package/tests/suite/bin/cmpz +0 -0
  266. package/tests/suite/bin/cmpzx +0 -0
  267. package/tests/suite/bin/cntdef +0 -0
  268. package/tests/suite/bin/cnto2 +0 -0
  269. package/tests/suite/bin/cpuport +0 -0
  270. package/tests/suite/bin/cputiming +0 -0
  271. package/tests/suite/bin/cpxa +0 -0
  272. package/tests/suite/bin/cpxb +0 -0
  273. package/tests/suite/bin/cpxz +0 -0
  274. package/tests/suite/bin/cpya +0 -0
  275. package/tests/suite/bin/cpyb +0 -0
  276. package/tests/suite/bin/cpyz +0 -0
  277. package/tests/suite/bin/dcma +0 -0
  278. package/tests/suite/bin/dcmax +0 -0
  279. package/tests/suite/bin/dcmay +0 -0
  280. package/tests/suite/bin/dcmix +0 -0
  281. package/tests/suite/bin/dcmiy +0 -0
  282. package/tests/suite/bin/dcmz +0 -0
  283. package/tests/suite/bin/dcmzx +0 -0
  284. package/tests/suite/bin/deca +0 -0
  285. package/tests/suite/bin/decax +0 -0
  286. package/tests/suite/bin/decz +0 -0
  287. package/tests/suite/bin/deczx +0 -0
  288. package/tests/suite/bin/dexn +0 -0
  289. package/tests/suite/bin/deyn +0 -0
  290. package/tests/suite/bin/eora +0 -0
  291. package/tests/suite/bin/eorax +0 -0
  292. package/tests/suite/bin/eoray +0 -0
  293. package/tests/suite/bin/eorb +0 -0
  294. package/tests/suite/bin/eorix +0 -0
  295. package/tests/suite/bin/eoriy +0 -0
  296. package/tests/suite/bin/eorz +0 -0
  297. package/tests/suite/bin/eorzx +0 -0
  298. package/tests/suite/bin/finish +0 -0
  299. package/tests/suite/bin/flipos +0 -0
  300. package/tests/suite/bin/icr01 +0 -0
  301. package/tests/suite/bin/imr +0 -0
  302. package/tests/suite/bin/inca +0 -0
  303. package/tests/suite/bin/incax +0 -0
  304. package/tests/suite/bin/incz +0 -0
  305. package/tests/suite/bin/inczx +0 -0
  306. package/tests/suite/bin/insa +0 -0
  307. package/tests/suite/bin/insax +0 -0
  308. package/tests/suite/bin/insay +0 -0
  309. package/tests/suite/bin/insix +0 -0
  310. package/tests/suite/bin/insiy +0 -0
  311. package/tests/suite/bin/insz +0 -0
  312. package/tests/suite/bin/inszx +0 -0
  313. package/tests/suite/bin/inxn +0 -0
  314. package/tests/suite/bin/inyn +0 -0
  315. package/tests/suite/bin/irq +0 -0
  316. package/tests/suite/bin/jmpi +0 -0
  317. package/tests/suite/bin/jmpw +0 -0
  318. package/tests/suite/bin/jsrw +0 -0
  319. package/tests/suite/bin/lasay +0 -0
  320. package/tests/suite/bin/laxa +0 -0
  321. package/tests/suite/bin/laxay +0 -0
  322. package/tests/suite/bin/laxix +0 -0
  323. package/tests/suite/bin/laxiy +0 -0
  324. package/tests/suite/bin/laxz +0 -0
  325. package/tests/suite/bin/laxzy +0 -0
  326. package/tests/suite/bin/ldaa +0 -0
  327. package/tests/suite/bin/ldaax +0 -0
  328. package/tests/suite/bin/ldaay +0 -0
  329. package/tests/suite/bin/ldab +0 -0
  330. package/tests/suite/bin/ldaix +0 -0
  331. package/tests/suite/bin/ldaiy +0 -0
  332. package/tests/suite/bin/ldaz +0 -0
  333. package/tests/suite/bin/ldazx +0 -0
  334. package/tests/suite/bin/ldxa +0 -0
  335. package/tests/suite/bin/ldxay +0 -0
  336. package/tests/suite/bin/ldxb +0 -0
  337. package/tests/suite/bin/ldxz +0 -0
  338. package/tests/suite/bin/ldxzy +0 -0
  339. package/tests/suite/bin/ldya +0 -0
  340. package/tests/suite/bin/ldyax +0 -0
  341. package/tests/suite/bin/ldyb +0 -0
  342. package/tests/suite/bin/ldyz +0 -0
  343. package/tests/suite/bin/ldyzx +0 -0
  344. package/tests/suite/bin/loadth +0 -0
  345. package/tests/suite/bin/lsea +0 -0
  346. package/tests/suite/bin/lseax +0 -0
  347. package/tests/suite/bin/lseay +0 -0
  348. package/tests/suite/bin/lseix +0 -0
  349. package/tests/suite/bin/lseiy +0 -0
  350. package/tests/suite/bin/lsez +0 -0
  351. package/tests/suite/bin/lsezx +0 -0
  352. package/tests/suite/bin/lsra +0 -0
  353. package/tests/suite/bin/lsrax +0 -0
  354. package/tests/suite/bin/lsrn +0 -0
  355. package/tests/suite/bin/lsrz +0 -0
  356. package/tests/suite/bin/lsrzx +0 -0
  357. package/tests/suite/bin/lxab +0 -0
  358. package/tests/suite/bin/mmu +0 -0
  359. package/tests/suite/bin/mmufetch +0 -0
  360. package/tests/suite/bin/nmi +0 -0
  361. package/tests/suite/bin/nopa +0 -0
  362. package/tests/suite/bin/nopax +0 -0
  363. package/tests/suite/bin/nopb +0 -0
  364. package/tests/suite/bin/nopn +0 -0
  365. package/tests/suite/bin/nopz +0 -0
  366. package/tests/suite/bin/nopzx +0 -0
  367. package/tests/suite/bin/oneshot +0 -0
  368. package/tests/suite/bin/oraa +0 -0
  369. package/tests/suite/bin/oraax +0 -0
  370. package/tests/suite/bin/oraay +0 -0
  371. package/tests/suite/bin/orab +0 -0
  372. package/tests/suite/bin/oraix +0 -0
  373. package/tests/suite/bin/oraiy +0 -0
  374. package/tests/suite/bin/oraz +0 -0
  375. package/tests/suite/bin/orazx +0 -0
  376. package/tests/suite/bin/phan +0 -0
  377. package/tests/suite/bin/phpn +0 -0
  378. package/tests/suite/bin/plan +0 -0
  379. package/tests/suite/bin/plpn +0 -0
  380. package/tests/suite/bin/rlaa +0 -0
  381. package/tests/suite/bin/rlaax +0 -0
  382. package/tests/suite/bin/rlaay +0 -0
  383. package/tests/suite/bin/rlaix +0 -0
  384. package/tests/suite/bin/rlaiy +0 -0
  385. package/tests/suite/bin/rlaz +0 -0
  386. package/tests/suite/bin/rlazx +0 -0
  387. package/tests/suite/bin/rola +0 -0
  388. package/tests/suite/bin/rolax +0 -0
  389. package/tests/suite/bin/roln +0 -0
  390. package/tests/suite/bin/rolz +0 -0
  391. package/tests/suite/bin/rolzx +0 -0
  392. package/tests/suite/bin/rora +0 -0
  393. package/tests/suite/bin/rorax +0 -0
  394. package/tests/suite/bin/rorn +0 -0
  395. package/tests/suite/bin/rorz +0 -0
  396. package/tests/suite/bin/rorzx +0 -0
  397. package/tests/suite/bin/rraa +0 -0
  398. package/tests/suite/bin/rraax +0 -0
  399. package/tests/suite/bin/rraay +0 -0
  400. package/tests/suite/bin/rraix +0 -0
  401. package/tests/suite/bin/rraiy +0 -0
  402. package/tests/suite/bin/rraz +0 -0
  403. package/tests/suite/bin/rrazx +0 -0
  404. package/tests/suite/bin/rtin +0 -0
  405. package/tests/suite/bin/rtsn +0 -0
  406. package/tests/suite/bin/sbca +0 -0
  407. package/tests/suite/bin/sbcax +0 -0
  408. package/tests/suite/bin/sbcay +0 -0
  409. package/tests/suite/bin/sbcb +0 -0
  410. package/tests/suite/bin/sbcb(eb) +0 -0
  411. package/tests/suite/bin/sbcix +0 -0
  412. package/tests/suite/bin/sbciy +0 -0
  413. package/tests/suite/bin/sbcz +0 -0
  414. package/tests/suite/bin/sbczx +0 -0
  415. package/tests/suite/bin/sbxb +0 -0
  416. package/tests/suite/bin/secn +0 -0
  417. package/tests/suite/bin/sedn +0 -0
  418. package/tests/suite/bin/sein +0 -0
  419. package/tests/suite/bin/shaay +0 -0
  420. package/tests/suite/bin/shaiy +0 -0
  421. package/tests/suite/bin/shsay +0 -0
  422. package/tests/suite/bin/shxay +0 -0
  423. package/tests/suite/bin/shyax +0 -0
  424. package/tests/suite/bin/staa +0 -0
  425. package/tests/suite/bin/staax +0 -0
  426. package/tests/suite/bin/staay +0 -0
  427. package/tests/suite/bin/staix +0 -0
  428. package/tests/suite/bin/staiy +0 -0
  429. package/tests/suite/bin/staz +0 -0
  430. package/tests/suite/bin/stazx +0 -0
  431. package/tests/suite/bin/stxa +0 -0
  432. package/tests/suite/bin/stxz +0 -0
  433. package/tests/suite/bin/stxzy +0 -0
  434. package/tests/suite/bin/stya +0 -0
  435. package/tests/suite/bin/styz +0 -0
  436. package/tests/suite/bin/styzx +0 -0
  437. package/tests/suite/bin/taxn +0 -0
  438. package/tests/suite/bin/tayn +0 -0
  439. package/tests/suite/bin/trap1 +0 -0
  440. package/tests/suite/bin/trap10 +0 -0
  441. package/tests/suite/bin/trap11 +0 -0
  442. package/tests/suite/bin/trap12 +0 -0
  443. package/tests/suite/bin/trap13 +0 -0
  444. package/tests/suite/bin/trap14 +0 -0
  445. package/tests/suite/bin/trap15 +0 -0
  446. package/tests/suite/bin/trap16 +0 -0
  447. package/tests/suite/bin/trap17 +0 -0
  448. package/tests/suite/bin/trap2 +0 -0
  449. package/tests/suite/bin/trap3 +0 -0
  450. package/tests/suite/bin/trap4 +0 -0
  451. package/tests/suite/bin/trap5 +0 -0
  452. package/tests/suite/bin/trap6 +0 -0
  453. package/tests/suite/bin/trap7 +0 -0
  454. package/tests/suite/bin/trap8 +0 -0
  455. package/tests/suite/bin/trap9 +0 -0
  456. package/tests/suite/bin/tsxn +0 -0
  457. package/tests/suite/bin/txan +0 -0
  458. package/tests/suite/bin/txsn +0 -0
  459. package/tests/suite/bin/tyan +0 -0
  460. package/tests/suite/cbm-hackers-post.html +178 -0
  461. package/tests/suite/cbm-hackers-post.md +78 -0
  462. package/tests/test-machine.js +288 -0
  463. package/tests/test-suite.js +147 -0
  464. package/tests/test.css +7 -0
  465. package/tests/unit/gzip/test-1 +0 -0
  466. package/tests/unit/gzip/test-1.gz +0 -0
  467. package/tests/unit/gzip/test-2 +0 -0
  468. package/tests/unit/gzip/test-2.gz +0 -0
  469. package/tests/unit/gzip/test-3 +0 -0
  470. package/tests/unit/gzip/test-3.gz +0 -0
  471. package/tests/unit/gzip/test-4 +0 -0
  472. package/tests/unit/gzip/test-4.gz +0 -0
  473. package/tests/unit/test-adc.js +307 -0
  474. package/tests/unit/test-bcd.js +30 -0
  475. package/tests/unit/test-cmos.js +266 -0
  476. package/tests/unit/test-disc-drive.js +85 -0
  477. package/tests/unit/test-disc-hfe.js +347 -0
  478. package/tests/unit/test-disc.js +232 -0
  479. package/tests/unit/test-fifo.js +35 -0
  480. package/tests/unit/test-gamepad-source.js +67 -0
  481. package/tests/unit/test-gzip.js +22 -0
  482. package/tests/unit/test-intel-fdc.js +93 -0
  483. package/tests/unit/test-keyboard.js +410 -0
  484. package/tests/unit/test-mouse-joystick-source.js +128 -0
  485. package/tests/unit/test-scheduler.js +190 -0
  486. package/tests/unit/test-serial.js +154 -0
  487. package/tests/unit/test-teletext-adaptor.js +359 -0
  488. package/tests/unit/test-tokenise.js +65 -0
  489. package/tests/unit/test-url-params.js +398 -0
  490. package/tests/unit/test-utils.js +276 -0
  491. package/tests/unit/test-video.js +498 -0
  492. package/tests/unit/test-zip.js +56 -0
  493. package/tests/unit/zip/test-mixed.zip +0 -0
  494. package/tests/unit/zip/test-rom.zip +0 -0
  495. package/tests/unit/zip/test-ssd.zip +0 -0
  496. package/tools/fir-generator.js +80 -0
  497. package/tools/vite-plugin-fir-shader.js +131 -0
  498. 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
+ }