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
package/src/disc.js ADDED
@@ -0,0 +1,997 @@
1
+ // Translated from beebjit by Chris Evans.
2
+ // https://github.com/scarybeasts/beebjit
3
+
4
+ import * as utils from "./utils.js";
5
+
6
+ /*
7
+ * TODO: use in fingerprinting
8
+ class Crc32Builder {
9
+ constructor() {
10
+ this._crc = 0xffffffff;
11
+ }
12
+
13
+ add(data) {
14
+ for (let i = 0; i < data.length; ++i) {
15
+ const byte = data[i];
16
+ this._crc ^= byte;
17
+ for (let j = 0; j < 8; ++j) {
18
+ const doEor = this._crc & 1;
19
+ this._crc = this._crc >>> 1;
20
+ if (doEor) this._crc ^= 0xedb88320;
21
+ }
22
+ }
23
+ }
24
+
25
+ get crc() {
26
+ return ~this._crc;
27
+ }
28
+ }
29
+ */
30
+ class TrackBuilder {
31
+ /**
32
+ * @param {Track} track
33
+ */
34
+ constructor(track) {
35
+ this._track = track;
36
+ this._track.length = IbmDiscFormat.bytesPerTrack;
37
+ this._index = 0;
38
+ this._pulsesIndex = 0;
39
+ this._lastMfmBit = 0;
40
+ this._crc = 0;
41
+ }
42
+
43
+ get track() {
44
+ return this._track;
45
+ }
46
+
47
+ setTrackLength() {
48
+ if (this._index > this._track.pulses2Us.length)
49
+ throw new Error(`Track buffer overflow in ${this._track.description}`);
50
+ if (this._index !== 0) this._track.length = this._index;
51
+ return this;
52
+ }
53
+
54
+ resetCrc() {
55
+ this._crc = IbmDiscFormat.crcInit(false);
56
+ return this;
57
+ }
58
+
59
+ appendFmDataAndClocks(data, clocks) {
60
+ if (this._index >= this._track.pulses2Us.length)
61
+ throw new Error(`Track buffer overflow in ${this._track.description}`);
62
+ this._track.pulses2Us[this._index++] = IbmDiscFormat.fmTo2usPulses(clocks, data);
63
+ this._crc = IbmDiscFormat.crcAddByte(this._crc, data);
64
+ return this;
65
+ }
66
+
67
+ appendFmByte(data) {
68
+ this.appendFmDataAndClocks(data, 0xff);
69
+ return this;
70
+ }
71
+
72
+ appendRepeatFmByte(data, count) {
73
+ for (let i = 0; i < count; ++i) this.appendFmByte(data);
74
+ return this;
75
+ }
76
+
77
+ fillFmByte(data) {
78
+ if (this._index >= this._track.pulses2Us.length)
79
+ throw new Error(`Track buffer overflow in ${this._track.description}`);
80
+ // Fill to standard track size or buffer capacity, whichever is smaller
81
+ const fillCount = Math.min(IbmDiscFormat.bytesPerTrack, this._track.pulses2Us.length) - this._index;
82
+ this.appendRepeatFmByte(data, fillCount);
83
+ return this;
84
+ }
85
+
86
+ appendRepeatFmByteWithClocks(data, clocks, count) {
87
+ for (let i = 0; i < count; ++i) this.appendFmDataAndClocks(data, clocks);
88
+ return this;
89
+ }
90
+
91
+ appendFmChunk(bytes) {
92
+ for (const byte of bytes) this.appendFmByte(byte);
93
+ return this;
94
+ }
95
+
96
+ appendCrc(isMfm) {
97
+ // TODO consider remembering isMfM if nothing else needs to know/
98
+ // could then break this into MFM and FM builder
99
+ const firstByte = (this._crc >>> 8) & 0xff;
100
+ const secondByte = this._crc & 0xff;
101
+ if (isMfm) {
102
+ this.appendMfmByte(firstByte);
103
+ this.appendMfmByte(secondByte);
104
+ } else {
105
+ this.appendFmByte(firstByte);
106
+ this.appendFmByte(secondByte);
107
+ }
108
+ return this;
109
+ }
110
+
111
+ appendMfmPulses(pulses) {
112
+ if (this._index >= this._track.pulses2Us.length)
113
+ throw new Error(`Track buffer overflow in ${this._track.description}`);
114
+ const existingPulses = this._track.pulses2Us[this._index];
115
+ const mask = 0xffff << this._pulsesIndex;
116
+ this._pulsesIndex = (this._pulsesIndex + 16) & 31;
117
+ this._track.pulses2Us[this._index] = (existingPulses & mask) | (pulses << this._pulsesIndex);
118
+ if (this._pulsesIndex === 0) this._index++;
119
+ return this;
120
+ }
121
+
122
+ appendMfmByte(data) {
123
+ const { lastBit, pulses } = IbmDiscFormat.mfmTo2usPulses(this._lastMfmBit, data);
124
+ this._lastMfmBit = lastBit;
125
+ this.appendMfmPulses(pulses);
126
+ this._crc = IbmDiscFormat.crcAddByte(this._crc, data);
127
+ return this;
128
+ }
129
+
130
+ appendRepeatMfmByte(data, count) {
131
+ for (let i = 0; i < count; ++i) this.appendMfmByte(data);
132
+ return this;
133
+ }
134
+
135
+ appendMfm3xA1Sync() {
136
+ for (let i = 0; i < 3; ++i) {
137
+ this.appendMfmPulses(IbmDiscFormat.mfmA1Sync);
138
+ this._crc = IbmDiscFormat.crcAddByte(this._crc, 0xa1);
139
+ }
140
+ return this;
141
+ }
142
+
143
+ appendMfmChunk(bytes) {
144
+ for (const byte of bytes) this.appendMfmByte(byte);
145
+ return this;
146
+ }
147
+
148
+ fillMfmByte(data) {
149
+ if (this._index >= this._track.pulses2Us.length)
150
+ throw new Error(`Track buffer overflow in ${this._track.description}`);
151
+ // Fill to standard track size or buffer capacity, whichever is smaller
152
+ const maxFill = Math.min(IbmDiscFormat.bytesPerTrack, this._track.pulses2Us.length);
153
+ while (this._index < maxFill) this.appendMfmByte(data);
154
+ return this;
155
+ }
156
+
157
+ /**
158
+ * @param {number[]} pulseDeltas array of lengths between pulses
159
+ * @param {boolean} isMfm whether this is an MFM track
160
+ */
161
+ buildFromPulses(pulseDeltas, isMfm) {
162
+ let hasWarned = false;
163
+ for (const pulse of pulseDeltas) {
164
+ if (!IbmDiscFormat.checkPulse(pulse, isMfm)) {
165
+ console.log(`Found a bad pulse for ${this.track.description}`);
166
+ }
167
+ if (!this.appendPulseDelta(pulse, isMfm) && !hasWarned) {
168
+ console.log(`Truncated disc data for ${this.track.description}, ignoring the rest`);
169
+ hasWarned = true;
170
+ }
171
+ }
172
+ this.setTrackLength();
173
+ }
174
+
175
+ appendPulseDelta(deltaUs, quantizeMfm) {
176
+ let num2UsUnits = quantizeMfm ? Math.round(deltaUs / 2) : 2 * Math.round(deltaUs / 4);
177
+ while (num2UsUnits--) {
178
+ if (this._index >= this._track.pulses2Us.length) return false;
179
+ if (num2UsUnits === 0) {
180
+ this._track.pulses2Us[this._index] |= 0x80000000 >>> this._pulsesIndex;
181
+ }
182
+ this._pulsesIndex++;
183
+ if (this._pulsesIndex === 32) {
184
+ this._pulsesIndex = 0;
185
+ this._index++;
186
+ }
187
+ }
188
+ return true;
189
+ }
190
+ }
191
+
192
+ class RawDiscReader {
193
+ /**
194
+ * @param {Track} track
195
+ * @param {Number} bitOffset
196
+ */
197
+ constructor(track, bitOffset) {
198
+ this._track = track;
199
+ this._pos = bitOffset;
200
+ }
201
+
202
+ readPulses() {
203
+ let pulsesPos = this._pos >>> 5;
204
+ const bitPos = this._pos & 0x1f;
205
+ let sourcePulses = this._track.pulses2Us[pulsesPos];
206
+ let pulses = (sourcePulses << bitPos) & 0xfffffffff;
207
+ if (pulsesPos === this._track.length) {
208
+ pulsesPos = 0;
209
+ this._pos = bitPos;
210
+ } else {
211
+ pulsesPos++;
212
+ this._pos += 32;
213
+ }
214
+ if (bitPos > 0) {
215
+ sourcePulses = this._track.pulses2Us[pulsesPos];
216
+ pulses |= sourcePulses >>> (32 - bitPos);
217
+ }
218
+ return pulses;
219
+ }
220
+ }
221
+
222
+ class MfmReader {
223
+ /**
224
+ * @param {RawDiscReader} rawReader
225
+ */
226
+ constructor(rawReader) {
227
+ this._rawReader = rawReader;
228
+ }
229
+
230
+ read(numBytes) {
231
+ const data = new Uint8Array(numBytes);
232
+ let pulses = 0;
233
+ for (let offset = 0; offset < numBytes; ++offset) {
234
+ if ((offset & 1) === 0) {
235
+ pulses = this._rawReader.readPulses();
236
+ } else {
237
+ pulses = (pulses << 16) & 0xffffffff;
238
+ }
239
+ data[offset] = IbmDiscFormat._2usPulsesToMfm(pulses >>> 16);
240
+ }
241
+ return { data, clocks: null, iffyPulses: false };
242
+ }
243
+
244
+ get initialCrc() {
245
+ let crc = IbmDiscFormat.crcInit(false);
246
+ crc = IbmDiscFormat.crcAddByte(crc, 0xa1);
247
+ crc = IbmDiscFormat.crcAddByte(crc, 0xa1);
248
+ crc = IbmDiscFormat.crcAddByte(crc, 0xa1);
249
+ return crc;
250
+ }
251
+ }
252
+
253
+ class FmReader {
254
+ /**
255
+ * @param {RawDiscReader} rawReader
256
+ */
257
+ constructor(rawReader) {
258
+ this._rawReader = rawReader;
259
+ }
260
+
261
+ read(numBytes) {
262
+ const data = new Uint8Array(numBytes);
263
+ const clocks = new Uint8Array(numBytes);
264
+ let iffyPulses = false;
265
+ for (let offset = 0; offset < numBytes; ++offset) {
266
+ const pulses = this._rawReader.readPulses();
267
+ const { data: dataByte, clock: clockByte, iffyPulses: iffy } = IbmDiscFormat._2usPulsesToFm(pulses);
268
+ data[offset] = dataByte;
269
+ clocks[offset] = clockByte;
270
+ iffyPulses |= iffy;
271
+ }
272
+ return { data, clocks, iffyPulses };
273
+ }
274
+
275
+ get initialCrc() {
276
+ return IbmDiscFormat.crcInit(false);
277
+ }
278
+ }
279
+
280
+ class Sector {
281
+ /**
282
+ * @param {Track} track
283
+ * @param {boolean} isMfm
284
+ * @param {Number} idPosBitOffset
285
+ */
286
+ constructor(track, isMfm, idPosBitOffset) {
287
+ this.track = track;
288
+ this.isMfm = isMfm;
289
+ this.idPosBitOffset = idPosBitOffset;
290
+ this.dataPosBitOffset = null;
291
+ this.isDeleted = false;
292
+ this.sectorData = null;
293
+ this.hasDataCrcError = false;
294
+ this.byteLength = null;
295
+
296
+ const idReader = this._readerAt(this.idPosBitOffset);
297
+ const { data: headerData, iffyPulses } = idReader.read(6);
298
+ if (iffyPulses) {
299
+ console.log(`Iffy pulse in sector header ${this.description}`);
300
+ }
301
+ this.header = headerData;
302
+ let crc = idReader.initialCrc;
303
+ crc = IbmDiscFormat.crcAddByte(crc, IbmDiscFormat.idMarkDataPattern);
304
+ crc = IbmDiscFormat.crcAddBytes(crc, this.header.slice(0, 4));
305
+ const discCrc = (this.header[4] << 8) | this.header[5];
306
+ this.hasHeaderCrcError = crc !== discCrc;
307
+ }
308
+
309
+ _readerAt(bitOffset) {
310
+ const rawReader = new RawDiscReader(this.track, bitOffset);
311
+ return this.isMfm ? new MfmReader(rawReader) : new FmReader(rawReader);
312
+ }
313
+
314
+ get trackNumber() {
315
+ return this.header ? this.header[0] : undefined;
316
+ }
317
+
318
+ get sectorNumber() {
319
+ return this.header ? this.header[2] : undefined;
320
+ }
321
+
322
+ get description() {
323
+ return `${this.track.description} idpos ${this.idPosBitOffset} idtrack ${this.trackNumber} idsector ${this.sectorNumber} datapos ${this.dataPosBitOffset}`;
324
+ }
325
+
326
+ /**
327
+ * @param {Sector|undefined} nextSector
328
+ */
329
+ read(nextSector) {
330
+ const pulsesPerByte = this.isMfm ? 16 : 32; // todo put in reader
331
+ if (this.dataPosBitOffset === null) {
332
+ console.log(`"Sector header without data ${this.description}"`);
333
+ return;
334
+ }
335
+
336
+ const dataMarker = this.isDeleted
337
+ ? IbmDiscFormat.deletedDataMarkDataPattern
338
+ : IbmDiscFormat.dataMarkDataPattern;
339
+ const sectorStartByte = (this.dataPosBitOffset / pulsesPerByte) | 0;
340
+ const sectorEndByte =
341
+ (nextSector ? nextSector.idPosBitOffset / pulsesPerByte : (this.track.length * 32) / pulsesPerByte) | 0;
342
+ // Account for CRC and sync bytes.
343
+ let sectorSize = Sector.toSectorSize(sectorEndByte - sectorStartByte - 5);
344
+
345
+ this.hasDataCrcError = true;
346
+ let seenIffyData = false;
347
+ do {
348
+ const { crcOk, sectorData, iffyPulses } = this._tryLoadSectorData(dataMarker, sectorSize);
349
+ seenIffyData = iffyPulses;
350
+ if (crcOk) {
351
+ this.byteLength = sectorSize;
352
+ this.hasDataCrcError = false;
353
+ this.sectorData = sectorData;
354
+ break;
355
+ }
356
+ sectorSize = sectorSize >>> 1;
357
+ } while (sectorSize >= 128);
358
+ if (seenIffyData) {
359
+ console.log(`"Iffy pulse in sector data ${this.description}"`);
360
+ }
361
+ }
362
+
363
+ _tryLoadSectorData(dataMarker, sectorSize) {
364
+ const dataReader = this._readerAt(this.dataPosBitOffset);
365
+ let crc = IbmDiscFormat.crcAddByte(dataReader.initialCrc, dataMarker);
366
+ const { data: sectorData, iffyPulses } = dataReader.read(sectorSize + 2);
367
+ crc = IbmDiscFormat.crcAddBytes(crc, sectorData.slice(0, sectorSize));
368
+ const dataCrc = (sectorData[sectorSize] << 8) | sectorData[sectorSize + 1];
369
+ // The CRC bytes are used for error-checking and are not part of the actual sector data payload.
370
+ // Therefore, we exclude the last two bytes (CRC) from the returned `sectorData`.
371
+ return { crcOk: dataCrc === crc, sectorData: sectorData.slice(0, sectorSize), iffyPulses };
372
+ }
373
+
374
+ static toSectorSize(size) {
375
+ if (size < 256) return 128;
376
+ if (size < 512) return 256;
377
+ if (size < 1024) return 512;
378
+ if (size < 2048) return 1024;
379
+ return 2048;
380
+ }
381
+ }
382
+
383
+ class Track {
384
+ constructor(upper, trackNum, initialByte) {
385
+ this.length = IbmDiscFormat.bytesPerTrack; // Default size, will be updated when track is populated
386
+ this.upper = upper;
387
+ this.trackNum = trackNum;
388
+ // Make room for any extra pulses that might come from non-standard discs.
389
+ this.pulses2Us = new Uint32Array(IbmDiscFormat.bytesPerTrack * 2);
390
+ this.pulses2Us.fill(initialByte | (initialByte << 8) | (initialByte << 16) | (initialByte << 24));
391
+ }
392
+
393
+ get description() {
394
+ return `Track ${this.trackNum} ${this.upper ? "upper" : "lower"}`;
395
+ }
396
+
397
+ /**
398
+ * Debug functionality to try and interpret the track.
399
+ * @returns {Sector[]}
400
+ */
401
+ findSectors() {
402
+ const sectors = this.findSectorIds();
403
+ for (let sectorIndex = 0; sectorIndex !== sectors.length; ++sectorIndex) {
404
+ const nextSector = sectors[sectorIndex + 1]; // Will be unset for last
405
+ sectors[sectorIndex].read(nextSector);
406
+ }
407
+ return sectors;
408
+ }
409
+
410
+ /**
411
+ * @returns {Sector[]}
412
+ */
413
+ findSectorIds() {
414
+ const sectors = [];
415
+ // Pass 1: walk the track and find header and data markers.
416
+ const bitLength = this.length * 32;
417
+ let shiftRegister = 0;
418
+ let numShifts = 0;
419
+ let doMfmMarkerByte = false;
420
+ let isMfm = false;
421
+ let pulses = 0;
422
+ let markDetector = 0n;
423
+ let markDetectorPrev = 0n;
424
+ const all64b = 0xffffffffffffffffn;
425
+ const top32of64b = 0xffffffff00000000n;
426
+ const fmMarker = 0x8888888800000000n;
427
+ const mfmMarker = 0xaaaa448944894489n;
428
+ let dataByte = 0;
429
+ let sector = null;
430
+ for (let pulseIndex = 0; pulseIndex < bitLength; ++pulseIndex) {
431
+ if ((pulseIndex & 31) === 0) pulses = this.pulses2Us[pulseIndex >>> 5];
432
+ markDetectorPrev = (markDetectorPrev << 1n) & all64b;
433
+ markDetectorPrev |= markDetector >> 63n;
434
+ markDetector = (markDetector << 1n) & all64b;
435
+ shiftRegister = (shiftRegister << 1) & 0xffffffff;
436
+ numShifts++;
437
+ if (pulses & 0x80000000) {
438
+ markDetector |= 1n;
439
+ shiftRegister |= 1;
440
+ }
441
+ pulses = (pulses << 1) & 0xffffffff;
442
+ if ((markDetector & top32of64b) === fmMarker) {
443
+ const { clocks, data, iffyPulses } = IbmDiscFormat._2usPulsesToFm(Number(markDetector & 0xffffffffn));
444
+ if (iffyPulses || clocks !== IbmDiscFormat.markClockPattern) continue;
445
+ isMfm = false;
446
+ doMfmMarkerByte = false;
447
+ let num0s = 8;
448
+ for (let bits = markDetectorPrev; (bits & 0xfn) === 0x8n; bits >>= 4n) {
449
+ num0s++;
450
+ }
451
+ if (num0s <= 16) {
452
+ console.log(`Short zeros sync ${this.description}`);
453
+ }
454
+ dataByte = data;
455
+ } else if (markDetector === mfmMarker) {
456
+ // Next byte is MFM marker.
457
+ isMfm = true;
458
+ doMfmMarkerByte = true;
459
+ shiftRegister = 0;
460
+ numShifts = 0;
461
+ continue;
462
+ } else if (doMfmMarkerByte && numShifts === 16) {
463
+ dataByte = IbmDiscFormat._2usPulsesToMfm(shiftRegister);
464
+ doMfmMarkerByte = false;
465
+ } else {
466
+ continue;
467
+ }
468
+ switch (dataByte) {
469
+ case IbmDiscFormat.idMarkDataPattern: {
470
+ sector = new Sector(this, isMfm, pulseIndex + 1);
471
+ sectors.push(sector);
472
+ shiftRegister = 0;
473
+ numShifts = 0;
474
+ break;
475
+ }
476
+ case IbmDiscFormat.dataMarkDataPattern:
477
+ case IbmDiscFormat.deletedDataMarkDataPattern:
478
+ if (!sector || sector.dataPosBitOffset) {
479
+ console.log(
480
+ `Sector data without header ${this.description}; mark bitpos ${pulseIndex}; previous good sector ${sector ? sector.description : "none"}`,
481
+ );
482
+ } else {
483
+ sector.dataPosBitOffset = pulseIndex + 1;
484
+ if (dataByte === IbmDiscFormat.deletedDataMarkDataPattern) {
485
+ sector.isDeleted = true;
486
+ }
487
+ shiftRegister = 0;
488
+ numShifts = 0;
489
+ }
490
+ break;
491
+ default:
492
+ console.log(`Unknown marker byte ${utils.hexbyte(dataByte)} ${this.description}`);
493
+ }
494
+ }
495
+ return sectors;
496
+ }
497
+ }
498
+
499
+ class Side {
500
+ constructor(upper, initialByte) {
501
+ this.tracks = [];
502
+ for (let i = 0; i < IbmDiscFormat.tracksPerDisc; ++i) this.tracks[i] = new Track(upper, i, initialByte);
503
+ }
504
+ }
505
+
506
+ export class DiscConfig {
507
+ constructor() {
508
+ // TODO is this even useful?
509
+ this.logProtection = false;
510
+ this.logIffyPulses = false;
511
+ this.expandTo80 = false;
512
+ this.isQuantizeFm = false;
513
+ this.isSkipOddTracks = false;
514
+ this.isSkipUpperSide = false;
515
+ this.rev = 0;
516
+ this.revSpec = "";
517
+ }
518
+ }
519
+
520
+ class SsdFormat {
521
+ static get sectorSize() {
522
+ return 256;
523
+ }
524
+
525
+ static get sectorsPerTrack() {
526
+ return 10;
527
+ }
528
+
529
+ static get tracksPerDisc() {
530
+ return 80;
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Load a disc image in SSD (Single Sided Disc) or DSD (Double Sided Disc) format
536
+ * @param {Disc} disc - The disc object to load into
537
+ * @param {Uint8Array} data - The disc image data
538
+ * @param {boolean} isDsd - True if loading a double-sided disc
539
+ * @param {function(Uint8Array): void} onChange - Optional callback when disc content changes
540
+ */
541
+ export function loadSsd(disc, data, isDsd, onChange) {
542
+ const blankSector = new Uint8Array(SsdFormat.sectorSize);
543
+ const numSides = isDsd ? 2 : 1;
544
+ if (data.length % SsdFormat.sectorSize !== 0) {
545
+ throw new Error("SSD file size is not a multiple of sector size");
546
+ }
547
+ const maxSize = SsdFormat.sectorSize * SsdFormat.sectorsPerTrack * SsdFormat.tracksPerDisc * numSides;
548
+ if (data.length > maxSize) {
549
+ throw new Error("SSD file is too large");
550
+ }
551
+
552
+ let offset = 0;
553
+ for (let track = 0; track < SsdFormat.tracksPerDisc; ++track) {
554
+ for (let side = 0; side < numSides; ++side) {
555
+ const trackBuilder = disc.buildTrack(side === 1, track);
556
+ // Sync pattern at start of track, as the index pulse starts, aka GAP 5.
557
+ trackBuilder
558
+ .appendRepeatFmByte(0xff, IbmDiscFormat.stdGap1FFs)
559
+ .appendRepeatFmByte(0x00, IbmDiscFormat.stdSync00s);
560
+
561
+ for (let sector = 0; sector < SsdFormat.sectorsPerTrack; ++sector) {
562
+ // Sector header, aka ID.
563
+ trackBuilder
564
+ .resetCrc()
565
+ .appendFmDataAndClocks(IbmDiscFormat.idMarkDataPattern, IbmDiscFormat.markClockPattern)
566
+ .appendFmByte(track)
567
+ .appendFmByte(0)
568
+ .appendFmByte(sector)
569
+ .appendFmByte(1)
570
+ .appendCrc(false);
571
+
572
+ // Sync pattern between sector header and sector data, aka GAP 2.
573
+ trackBuilder
574
+ .appendRepeatFmByte(0xff, IbmDiscFormat.stdGap2FFs)
575
+ .appendRepeatFmByte(0x00, IbmDiscFormat.stdSync00s);
576
+
577
+ // Sector data.
578
+ const sectorData =
579
+ offset < data.length ? data.subarray(offset, offset + SsdFormat.sectorSize) : blankSector;
580
+ offset += SsdFormat.sectorSize;
581
+ trackBuilder
582
+ .resetCrc()
583
+ .appendFmDataAndClocks(IbmDiscFormat.dataMarkDataPattern, IbmDiscFormat.markClockPattern)
584
+ .appendFmChunk(sectorData)
585
+ .appendCrc(false);
586
+
587
+ if (sector !== SsdFormat.sectorsPerTrack - 1) {
588
+ // Sync pattern between sectors, aka GAP 3.
589
+ trackBuilder
590
+ .appendRepeatFmByte(0xff, IbmDiscFormat.std10SectorGap3FFs)
591
+ .appendRepeatFmByte(0x00, IbmDiscFormat.stdSync00s);
592
+ }
593
+ }
594
+ trackBuilder.fillFmByte(0xff);
595
+ }
596
+ }
597
+
598
+ if (onChange) {
599
+ // TODO, maybe construct the disc directly with this stuff?
600
+ // TODO maybe change this entirely and make it lazy; and have the onChange "pull" the disc as the format it wants
601
+ // instead of doing this here. Most stuff doesn't care about changes and only needs the image on save.
602
+ // Create a dataCopy large enough for all the sectors and tracks.
603
+ const dataCopy = new Uint8Array(maxSize);
604
+ dataCopy.set(data);
605
+ disc.setWriteTrackCallback(
606
+ /** @param {Track} trackObj */
607
+ (side, trackNum, trackObj) => {
608
+ const trackOffset =
609
+ SsdFormat.sectorSize * SsdFormat.sectorsPerTrack * (trackNum * numSides + (side ? 1 : 0));
610
+ for (const sector of trackObj.findSectors()) {
611
+ const sectorOffset = sector.sectorNumber * SsdFormat.sectorSize;
612
+ for (let x = 0; x < SsdFormat.sectorSize; ++x)
613
+ dataCopy[trackOffset + sectorOffset + x] = sector.sectorData[x];
614
+ }
615
+ onChange(dataCopy);
616
+ },
617
+ );
618
+ }
619
+ return disc;
620
+ }
621
+
622
+ class AdfFormat {
623
+ static get sectorSize() {
624
+ return 256;
625
+ }
626
+
627
+ static get sectorsPerTrack() {
628
+ return 16;
629
+ }
630
+
631
+ static get tracksPerDisc() {
632
+ return 80;
633
+ }
634
+ }
635
+
636
+ /**
637
+ * @param {Disc} disc
638
+ * @param {Uint8Array} data
639
+ * @param {boolean} isDsd
640
+ */
641
+ export function loadAdf(disc, data, isDsd) {
642
+ const blankSector = new Uint8Array(AdfFormat.sectorSize);
643
+ const numSides = isDsd ? 2 : 1;
644
+ if (data.length % AdfFormat.sectorSize !== 0) {
645
+ throw new Error("ADF file size is not a multiple of sector size");
646
+ }
647
+ const maxSize = AdfFormat.sectorSize * AdfFormat.sectorsPerTrack * AdfFormat.tracksPerDisc * numSides;
648
+ if (data.length > maxSize) {
649
+ throw new Error("ADF file is too large");
650
+ }
651
+
652
+ let offset = 0;
653
+ for (let track = 0; track < AdfFormat.tracksPerDisc; ++track) {
654
+ if (offset >= data.length) break;
655
+
656
+ for (let side = 0; side < numSides; ++side) {
657
+ // Using recommended values from the 177x datasheet.
658
+ const trackBuilder = disc.buildTrack(side === 1, track);
659
+ trackBuilder.appendRepeatMfmByte(0x4e, 60);
660
+ for (let sector = 0; sector < AdfFormat.sectorsPerTrack; ++sector) {
661
+ trackBuilder
662
+ .appendRepeatMfmByte(0x00, 12)
663
+ .resetCrc()
664
+ .appendMfm3xA1Sync()
665
+ .appendMfmByte(IbmDiscFormat.idMarkDataPattern)
666
+ .appendMfmByte(track)
667
+ .appendMfmByte(0)
668
+ .appendMfmByte(sector)
669
+ .appendMfmByte(1)
670
+ .appendCrc(true);
671
+
672
+ // Sync pattern between sector header and sector data, aka GAP 2.
673
+ trackBuilder.appendRepeatMfmByte(0x4e, 22).appendRepeatMfmByte(0x00, 12);
674
+
675
+ // Sector data.
676
+ const sectorData =
677
+ offset < data.length ? data.subarray(offset, offset + AdfFormat.sectorSize) : blankSector;
678
+ offset += AdfFormat.sectorSize;
679
+ trackBuilder
680
+ .resetCrc()
681
+ .appendMfm3xA1Sync()
682
+ .appendMfmByte(IbmDiscFormat.dataMarkDataPattern)
683
+ .appendMfmChunk(sectorData)
684
+ .appendCrc(true);
685
+
686
+ // Sync pattern between sectors, aka GAP 3.
687
+ trackBuilder.appendRepeatMfmByte(0x4e, 24);
688
+ }
689
+ trackBuilder.fillMfmByte(0x4e);
690
+ }
691
+ }
692
+
693
+ // TODO writeback
694
+ return disc;
695
+ }
696
+
697
+ /**
698
+ * @returns {Uint8Array}
699
+ * @param {Disc} disc
700
+ */
701
+ export function toSsdOrDsd(disc) {
702
+ const numSides = disc.isDoubleSided ? 2 : 1;
703
+ const result = new Uint8Array(
704
+ numSides * SsdFormat.tracksPerDisc * SsdFormat.sectorsPerTrack * SsdFormat.sectorSize,
705
+ );
706
+ let offset = 0;
707
+ for (let trackNum = 0; trackNum < disc.tracksUsed; ++trackNum) {
708
+ for (let side = 0; side < numSides; ++side) {
709
+ const trackObj = disc.getTrack(side === 1, trackNum);
710
+ for (const sector of trackObj.findSectors()) {
711
+ const sectorOffset = offset + sector.sectorNumber * SsdFormat.sectorSize;
712
+ if (sector.hasDataCrcError || sector.hasHeaderCrcError) {
713
+ console.log(`Skipping sector ${sector.description} with bad CRC`);
714
+ continue;
715
+ }
716
+ for (let x = 0; x < SsdFormat.sectorSize; ++x) result[sectorOffset + x] = sector.sectorData[x];
717
+ }
718
+ offset += SsdFormat.sectorsPerTrack * SsdFormat.sectorSize;
719
+ }
720
+ }
721
+ return result.slice(0, offset);
722
+ }
723
+
724
+ export class Disc {
725
+ /**
726
+ * @returns {Disc} a new blank disc
727
+ */
728
+ static createBlank() {
729
+ return new Disc(true, new DiscConfig());
730
+ }
731
+
732
+ /**
733
+ * @param {boolean} isWriteable
734
+ * @param {DiscConfig} config
735
+ * @param {string} name
736
+ */
737
+ constructor(isWriteable, config, name) {
738
+ this.config = config;
739
+
740
+ this.name = name;
741
+ this.isDirty = false;
742
+ this.dirtySide = -1;
743
+ this.dirtyTrack = -1;
744
+ this.tracksUsed = 0;
745
+ this.isDoubleSided = false;
746
+
747
+ this.writeTrackCallback = undefined;
748
+ this.isWriteable = isWriteable;
749
+
750
+ this.initSurface(0);
751
+ }
752
+
753
+ setWriteTrackCallback(callback) {
754
+ this.writeTrackCallback = callback;
755
+ }
756
+
757
+ get writeProtected() {
758
+ return !this.isWriteable;
759
+ }
760
+
761
+ /**
762
+ * @param {boolean} isSideUpper
763
+ * @param {Number} trackNum
764
+ * @returns {Track} */
765
+ getTrack(isSideUpper, trackNum) {
766
+ return isSideUpper ? this.upperSide.tracks[trackNum] : this.lowerSide.tracks[trackNum];
767
+ }
768
+
769
+ buildTrack(isSideUpper, trackNum) {
770
+ this.setTrackUsed(isSideUpper, trackNum);
771
+ return new TrackBuilder(this.getTrack(isSideUpper, trackNum));
772
+ }
773
+
774
+ setTrackUsed(isSideUpper, trackNum) {
775
+ if (isSideUpper) this.isDoubleSided = true;
776
+ this.tracksUsed = Math.max(this.tracksUsed, trackNum + 1);
777
+ }
778
+
779
+ initSurface(initialByte) {
780
+ this.lowerSide = new Side(false, initialByte);
781
+ this.upperSide = new Side(true, initialByte);
782
+
783
+ this.tracksUsed = 0;
784
+ this.isDoubleSided = false;
785
+ }
786
+
787
+ readPulses(isSideUpper, track, position) {
788
+ return this.getTrack(isSideUpper, track).pulses2Us[position];
789
+ }
790
+
791
+ /**
792
+ * @param {boolean} isSideUpper
793
+ * @param {Number} track
794
+ * @param {Number} position
795
+ * @param {Number} pulses
796
+ */
797
+ writePulses(isSideUpper, track, position, pulses) {
798
+ const trackObj = this.getTrack(isSideUpper, track);
799
+ if (position >= trackObj.length)
800
+ throw new Error(`Attempt to write off end of track ${position} > ${track.length}`);
801
+ if (this.isDirty) {
802
+ if (isSideUpper !== this.dirtySide || track !== this.dirtyTrack)
803
+ throw new Error("Switched dirty track or side");
804
+ }
805
+ this.isDirty = true;
806
+ this.dirtySide = isSideUpper;
807
+ this.dirtyTrack = track;
808
+ trackObj.pulses2Us[position] = pulses;
809
+ // TODO a debug log flag for this
810
+ // console.log(`wrote to ${track}:${position * 32}`);
811
+ }
812
+
813
+ flushWrites() {
814
+ if (!this.isDirty) {
815
+ if (this.dirtySide !== -1 || this.dirtyTrack !== -1) throw new Error("Bad state in disc dirty tracking");
816
+ return;
817
+ }
818
+
819
+ const dirtySide = this.dirtySide;
820
+ const dirtyTrack = this.dirtyTrack;
821
+ this.isDirty = false;
822
+ this.dirtySide = -1;
823
+ this.dirtyTrack = -1;
824
+ if (!this.writeTrackCallback) return;
825
+ const trackObj = this.getTrack(dirtySide, dirtyTrack);
826
+ this.writeTrackCallback(dirtySide, dirtyTrack, trackObj);
827
+ this.setTrackUsed(dirtySide, dirtyTrack);
828
+ }
829
+
830
+ logSummary() {
831
+ const maxTrack = this.tracksUsed;
832
+ const numSides = this.isDoubleSided ? 2 : 1;
833
+ for (let side = 0; side < numSides; ++side) {
834
+ for (let trackNum = 0; trackNum < maxTrack; ++trackNum) {
835
+ const track = this.getTrack(side === 1, trackNum);
836
+ const sectors = track.findSectors();
837
+ if (sectors.length) {
838
+ if (track.length >= IbmDiscFormat.bytesPerTrack * 1.015) {
839
+ console.log(`Long track ${track.description}, ${track.length} bytes`);
840
+ } else if (track.length <= IbmDiscFormat.bytesPerTrack * 0.985) {
841
+ console.log(`Short track ${track.description}, ${track.length} bytes`);
842
+ }
843
+ if (sectors[0].isMfm) {
844
+ if (sectors.length !== 16 && sectors.length !== 18) {
845
+ console.log(`Non-standard MFM sector count ${track.description} count ${sectors.length}`);
846
+ }
847
+ } else {
848
+ if (sectors.length !== 10) {
849
+ console.log(`Non-standard FM sector count ${track.description} count ${sectors.length}`);
850
+ }
851
+ }
852
+ /// MG stuff
853
+ for (const sector of sectors) {
854
+ if (sector.hasHeaderCrcError) console.log(`${sector.description} has bad header crc`);
855
+ if (sector.trackNumber !== trackNum) console.log(`${sector.description} has bad track id`);
856
+ if (sector.hasDataCrcError) console.log(`${sector.description} has bad data crc`);
857
+ }
858
+ } else {
859
+ console.log(`"Unformatted track ${track.description}"`);
860
+ }
861
+ }
862
+ // TODO add fingerprintings, catalog etcetc
863
+ }
864
+ }
865
+ }
866
+
867
+ export class IbmDiscFormat {
868
+ static get bytesPerTrack() {
869
+ return 3125;
870
+ }
871
+
872
+ static get tracksPerDisc() {
873
+ return 84;
874
+ }
875
+
876
+ static get markClockPattern() {
877
+ return 0xc7;
878
+ }
879
+
880
+ static get idMarkDataPattern() {
881
+ return 0xfe;
882
+ }
883
+
884
+ static get dataMarkDataPattern() {
885
+ return 0xfb;
886
+ }
887
+
888
+ static get deletedDataMarkDataPattern() {
889
+ return 0xf8;
890
+ }
891
+
892
+ static get mfmA1Sync() {
893
+ return 0x4489;
894
+ }
895
+
896
+ static get mfmC2Sync() {
897
+ return 0x5224;
898
+ }
899
+
900
+ static get stdSync00s() {
901
+ return 6;
902
+ }
903
+
904
+ static get stdGap1FFs() {
905
+ return 16;
906
+ }
907
+
908
+ static get stdGap2FFs() {
909
+ return 11;
910
+ }
911
+
912
+ static get std10SectorGap3FFs() {
913
+ return 21;
914
+ }
915
+
916
+ /**
917
+ * @param {boolean} isMfm
918
+ * @returns {Number} initial CRC for type
919
+ */
920
+ static crcInit(isMfm) {
921
+ // MFM starts with 3x 0xA1 sync bytes added.
922
+ return isMfm ? 0xcdb4 : 0xffff;
923
+ }
924
+
925
+ static crcAddByte(crc, byte) {
926
+ for (let i = 0; i < 8; ++i) {
927
+ const bit = byte & 0x80;
928
+ const bitTest = (crc & 0x8000) ^ (bit << 8);
929
+ crc = (crc << 1) & 0xffff;
930
+ if (bitTest) crc ^= 0x1021;
931
+ byte <<= 1;
932
+ }
933
+ return crc;
934
+ }
935
+
936
+ static crcAddBytes(crc, bytes) {
937
+ for (const byte of bytes) crc = IbmDiscFormat.crcAddByte(crc, byte);
938
+ return crc;
939
+ }
940
+
941
+ static fmTo2usPulses(clocks, data) {
942
+ let ret = 0;
943
+ for (let i = 0; i < 8; ++i) {
944
+ ret <<= 4;
945
+ if (clocks & 0x80) ret |= 0x04;
946
+ if (data & 0x80) ret |= 0x01;
947
+ clocks = (clocks << 1) & 0xff;
948
+ data = (data << 1) & 0xff;
949
+ }
950
+ return ret;
951
+ }
952
+
953
+ static _2usPulsesToFm(pulses) {
954
+ let clocks = 0;
955
+ let data = 0;
956
+ let iffyPulses = false;
957
+ for (let i = 0; i < 8; ++i) {
958
+ clocks <<= 1;
959
+ data <<= 1;
960
+ if (pulses & 0x80000000) clocks |= 0x01;
961
+ if (pulses & 0x20000000) data |= 0x01;
962
+ // Any pulses off the 2us clock are suspicious FM data.
963
+ if (pulses & 0x50000000) iffyPulses = true;
964
+ pulses = (pulses << 4) & 0xffffffff;
965
+ }
966
+ return { clocks, data, iffyPulses };
967
+ }
968
+
969
+ static mfmTo2usPulses(lastBit, data) {
970
+ let pulses = 0;
971
+ for (let i = 0; i < 8; ++i) {
972
+ const bit = !!(data & 0x80);
973
+ pulses = (pulses << 2) & 0xffff;
974
+ data <<= 1;
975
+ if (bit) pulses |= 0x01;
976
+ else if (!lastBit) pulses |= 0x02;
977
+ lastBit = bit;
978
+ }
979
+ return { lastBit, pulses };
980
+ }
981
+
982
+ static _2usPulsesToMfm(pulses) {
983
+ let byte = 0;
984
+ for (let i = 0; i < 8; ++i) {
985
+ byte <<= 1;
986
+ if ((pulses & 0xc000) === 0x4000) byte |= 1;
987
+ pulses = (pulses << 2) & 0xffff;
988
+ }
989
+ return byte;
990
+ }
991
+
992
+ static checkPulse(pulseUs, isMfm) {
993
+ if (pulseUs < 3.5 || pulseUs > 8.5) return false;
994
+ if (isMfm && pulseUs > 5.5 && pulseUs < 6.5) return true;
995
+ return !(pulseUs > 4.5 && pulseUs < 7.5);
996
+ }
997
+ }