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/wd-fdc.js ADDED
@@ -0,0 +1,1344 @@
1
+ // Translated from beebjit by Chris Evans.
2
+ // https://github.com/scarybeasts/beebjit
3
+ // eslint-disable-next-line no-unused-vars
4
+ import { Cpu6502 } from "./6502.js";
5
+ // eslint-disable-next-line no-unused-vars
6
+ import { BaseDiscDrive, DiscDrive } from "./disc-drive.js";
7
+ import { IbmDiscFormat } from "./disc.js";
8
+ // eslint-disable-next-line no-unused-vars
9
+ import { Scheduler } from "./scheduler.js";
10
+ import * as utils from "./utils.js";
11
+
12
+ /**
13
+ * Commands.
14
+ *
15
+ * @readonly
16
+ * @enum {Number}
17
+ */
18
+ const Command = Object.freeze({
19
+ restore: 0x00,
20
+ seek: 0x10,
21
+ stepInNoUpdate: 0x40,
22
+ stepInWithUpdate: 0x50,
23
+ stepOutNoUpdate: 0x60,
24
+ stepOutWithUpdate: 0x70,
25
+ readSector: 0x80,
26
+ readSectorMulti: 0x90,
27
+ writeSector: 0xa0,
28
+ writeSectorMulti: 0xb0,
29
+ readAddress: 0xc0,
30
+ forceInterrupt: 0xd0,
31
+ readTrack: 0xe0,
32
+ writeTrack: 0xf0,
33
+ });
34
+
35
+ /**
36
+ * Command bits.
37
+ *
38
+ * @readonly
39
+ * @enum {Number}
40
+ */
41
+ const CommandBits = Object.freeze({
42
+ typeIIMulti: 0x10,
43
+ disableSpinUp: 0x08,
44
+ typeIVerify: 0x04,
45
+ typeIIorIIISettle: 0x04,
46
+ typeIIDeleted: 0x01,
47
+ });
48
+
49
+ /**
50
+ * The drive control register is documented here:
51
+ * https://www.cloud9.co.uk/james/BBCMicro/Documentation/wd1770.html
52
+ *
53
+ * @readonly
54
+ * @enum {Number}
55
+ */
56
+ const Control = Object.freeze({
57
+ reset: 0x20,
58
+ density: 0x08,
59
+ side: 0x04,
60
+ drive1: 0x02,
61
+ drive0: 0x01,
62
+ });
63
+
64
+ /**
65
+ * Status bits.
66
+ *
67
+ * @readonly
68
+ * @enum {Number}
69
+ */
70
+ const Status = Object.freeze({
71
+ motorOn: 0x80,
72
+ writeProtected: 0x40,
73
+ typeISpinUpDone: 0x20,
74
+ typeIIorIIIDeletedMark: 0x20,
75
+ recordNotFound: 0x10,
76
+ crcError: 0x08,
77
+ typeITrack0: 0x04,
78
+ typeIIorIIILostByte: 0x04,
79
+ typeIIndex: 0x02,
80
+ typeIIorIIIDrq: 0x02,
81
+ busy: 0x01,
82
+ });
83
+
84
+ /**
85
+ * Controller state.
86
+ *
87
+ * @readonly
88
+ * @enum {Number}
89
+ */
90
+ const State = Object.freeze({
91
+ null: 0,
92
+ idle: 1,
93
+ timerWait: 2,
94
+ spinUpWait: 3,
95
+ waitIndex: 4,
96
+ searchId: 5,
97
+ inId: 6,
98
+ searchData: 7,
99
+ inData: 8,
100
+ inReadTrack: 8,
101
+ writeSectorDelay: 9,
102
+ writeSectorLeadInFm: 10,
103
+ writeSectorLeadInMfm: 11,
104
+ writeSectorMarkerFm: 12,
105
+ writeSectorMarkerMfm: 13,
106
+ writeSectorBody: 14,
107
+ writeTrackSetup: 15,
108
+ inWriteTrack: 16,
109
+ checkMulti: 17,
110
+ done: 18,
111
+ });
112
+
113
+ /**
114
+ * Timer state.
115
+ *
116
+ * @readonly
117
+ * @enum {Number}
118
+ */
119
+ const TimerState = Object.freeze({
120
+ none: 1,
121
+ settle: 2,
122
+ seek: 3,
123
+ done: 4,
124
+ });
125
+
126
+ export class WdFdc {
127
+ /**
128
+ * @param {Cpu6502} cpu
129
+ * @param {Scheduler} scheduler
130
+ * @param {BaseDiscDrive[] | undefined} drives
131
+ * @param {*} debugFlags
132
+ */
133
+ constructor(cpu, scheduler, drives, debugFlags) {
134
+ this._cpu = cpu;
135
+ if (drives) this._drives = drives;
136
+ else this._drives = [new DiscDrive(0, scheduler), new DiscDrive(1, scheduler)];
137
+
138
+ this._isMaster = cpu.model.isMaster;
139
+ this._is1772 = false; // TODO - if we ever support Master Compact
140
+ this._isOpus = false; // TODO - if we ever support Opus
141
+
142
+ this._controlRegister = 0;
143
+ /** @type {Status|Number} */
144
+ this._statusRegister = 0;
145
+ this._trackRegister = 0;
146
+ this._sectorRegister = 0;
147
+ this._dataRegister = 0;
148
+ this._isIntRq = false;
149
+ this._isDrq = false;
150
+ this._doRaiseIntRq = false;
151
+
152
+ /** @type {BaseDiscDrive|null} */
153
+ this._currentDrive = null;
154
+ this._isIndexPulse = false;
155
+ this._isInterruptOnIndexPulse = false;
156
+ this._isWriteTrackCrcSecondByte = false;
157
+ this._command = 0;
158
+ this._commandType = 0;
159
+ this._isCommandSettle = false;
160
+ this._isCommandWrite = false;
161
+ this._isCommandVerify = false;
162
+ this._isCommandMulti = false;
163
+ this._isCommandDeleted = false;
164
+
165
+ this._commandStepRateMs = 0;
166
+ this._state = State.idle;
167
+ this._timerState = TimerState.none;
168
+ this._timerTask = scheduler.newTask(() => this._timerFired());
169
+ this._stateCount = 0;
170
+ this._indexPulseCount = 0;
171
+ this._markDetector = 0n;
172
+ this._dataShifter = 0;
173
+ this._dataShiftCount = 0;
174
+ this._deliverData = 0;
175
+ this._deliverIsMarker = false;
176
+ this._crc = 0;
177
+ this._onDiscTrack = 0;
178
+ this._onDiscSector = 0;
179
+ this._onDiscLength = 0;
180
+ this._onDiscCrc = 0;
181
+ this._lastMfmBit = false;
182
+
183
+ this._logCommands = debugFlags ? !!debugFlags.logFdcCommands : false;
184
+ this._logStateChanges = debugFlags ? !!debugFlags.logFdcStateChanges : false;
185
+
186
+ const callback = (pulses, count) => this._pulsesCallback(pulses, count);
187
+ for (const drive of this._drives) drive.setPulsesCallback(callback);
188
+
189
+ this.powerOnReset();
190
+ }
191
+
192
+ reset() {
193
+ // This will:
194
+ // - Spin down.
195
+ // - Raise reset, which:
196
+ // - Clears status register.
197
+ // - Sets other registers as per how a real machine behaves.
198
+ // - Clears IRQs.
199
+ this._writeControl(0);
200
+ }
201
+
202
+ powerOnReset() {
203
+ this.reset();
204
+ // The reset line doesn't seem to affect the track or data registers.
205
+ this._trackRegister = 0;
206
+ this._dataRegister = 0;
207
+ }
208
+
209
+ _updateNmi() {
210
+ const newLevel = this._isDrq | (this._isOpus ? false : this._isIntRq);
211
+ // TODO: the cpu handling of NMIs is bad here. Should update to handle multiple
212
+ // NMI/interrupt sources. And when we do go back and implement the checks in the beebjit
213
+ // source here too.
214
+ this._cpu.NMI(newLevel);
215
+ }
216
+
217
+ /**
218
+ * @param {boolean} level
219
+ */
220
+ _setIntRq(level) {
221
+ this._isIntRq = level;
222
+ this._updateNmi();
223
+ }
224
+
225
+ /**
226
+ * @param {boolean} level
227
+ */
228
+ _setDrq(level) {
229
+ this._isDrq = level;
230
+ if (level) {
231
+ if (this._statusRegister & Status.typeIIorIIIDrq) {
232
+ this._statusRegister |= Status.typeIIorIIILostByte;
233
+ }
234
+ this._statusRegister |= Status.typeIIorIIIDrq;
235
+ } else {
236
+ this._statusRegister &= ~Status.typeIIorIIIDrq;
237
+ }
238
+ this._updateNmi();
239
+ }
240
+
241
+ _log(message) {
242
+ console.log(`WD1770: ${message}`);
243
+ }
244
+
245
+ _logCommand(message) {
246
+ if (this._logCommands) this._log(message);
247
+ }
248
+
249
+ _opusRemapAddr(addr) {
250
+ return addr ^ 4;
251
+ }
252
+
253
+ _opusRemapVal(addr, val) {
254
+ // Only remap control register values.
255
+ if (addr >= 4) return val;
256
+ let remapped = Control.reset;
257
+ if (val & 0x01) remapped |= Control.drive0;
258
+ else remapped |= Control.drive1;
259
+ if (val & 0x02) remapped |= Control.side;
260
+ if (val & 0x40) remapped |= Control.density;
261
+ return remapped;
262
+ }
263
+
264
+ _masterRemapVal(addr, val) {
265
+ // Only remap control register values.
266
+ if (addr >= 4) return val;
267
+ let remapped = 0;
268
+ if (val & 0x04) remapped |= Control.reset;
269
+ if (val & 0x01) remapped |= Control.drive0;
270
+ if (val & 0x02) remapped |= Control.drive1;
271
+ if (val & 0x10) remapped |= Control.side;
272
+ if (val & 0x20) remapped |= Control.density;
273
+ return remapped;
274
+ }
275
+
276
+ _remapVal(addr, val) {
277
+ if (this._isMaster) return this._masterRemapVal(addr, val);
278
+ if (this._isOpus) return this._opusRemapVal(addr, val);
279
+ return val;
280
+ }
281
+
282
+ _remapAddr(addr) {
283
+ addr &= 0x07;
284
+ if (this._isMaster) return addr ^ 0x04;
285
+ if (this._isOpus) return this._opusRemapAddr(addr);
286
+ return addr;
287
+ }
288
+
289
+ /**
290
+ * @param {Number} addr hardware address
291
+ * @returns {Number} byte at the given hardware address
292
+ */
293
+ read(addr) {
294
+ switch (this._remapAddr(addr)) {
295
+ case 4:
296
+ // Reading status register clears INTRQ.
297
+ this._setIntRq(false);
298
+ return this._statusRegister;
299
+ case 5:
300
+ return this._trackRegister;
301
+ case 6:
302
+ return this._sectorRegister;
303
+ case 7:
304
+ if (this._commandType === 2 || this._commandType === 3) {
305
+ this._setDrq(false);
306
+ }
307
+ return this._dataRegister;
308
+ case 0:
309
+ case 1:
310
+ case 2:
311
+ case 3:
312
+ break;
313
+ }
314
+ return 0xfe;
315
+ }
316
+
317
+ /**
318
+ * @param {Number} addr hardware address
319
+ * @param {Number} val byte to write
320
+ */
321
+ write(addr, val) {
322
+ addr = this._remapAddr(addr);
323
+ val = this._remapVal(addr, val);
324
+ switch (addr) {
325
+ case 0:
326
+ case 1:
327
+ case 2:
328
+ case 3:
329
+ this._logCommand(`control register now ${utils.hexbyte(val)}`);
330
+ if (this._statusRegister & Status.busy && !this._isReset(val)) {
331
+ throw new Error(`Control register updated while busy; without reset`);
332
+ }
333
+ this._writeControl(val);
334
+ break;
335
+ case 4:
336
+ // Ignore commands while in reset.
337
+ if (!this._isReset(this._controlRegister)) this._doCommand(val);
338
+ break;
339
+ case 5:
340
+ this._logCommand(`track register now ${val}`);
341
+ this._trackRegister = val;
342
+ break;
343
+ case 6:
344
+ // Ignore sector reg changes in reset; note that track/data registers will still be accepted.
345
+ if (!this._isReset(this._controlRegister)) {
346
+ this._logCommand(`sector register now ${val}`);
347
+ this._sectorRegister = val;
348
+ } else {
349
+ this._logCommand(`ignoring sector write of ${val}`);
350
+ }
351
+ break;
352
+ case 7:
353
+ if (this._commandType === 2 || this._commandType === 3) {
354
+ this._setDrq(false);
355
+ }
356
+ this._dataRegister = val;
357
+ break;
358
+ }
359
+ }
360
+
361
+ _doCommand(val) {
362
+ if (!this._currentDrive) throw new Error("Command while no selected drive");
363
+ this._logCommand(
364
+ `command ${utils.hexbyte(val)} tr ${this._trackRegister} sr ${this._sectorRegister} dr ${this._dataRegister} ` +
365
+ `cr ${utils.hexbyte(this._controlRegister)} ` +
366
+ `ptrk ${this._currentDrive.track} hpos ${this._currentDrive.headPosition}`,
367
+ );
368
+ const command = val & 0xf0;
369
+
370
+ if (command === Command.forceInterrupt) {
371
+ this._handleForceInterrupt(val);
372
+ return;
373
+ }
374
+ if (this._statusRegister & Status.busy) {
375
+ // EMU NOTE: this is a very murky area. There does not appear to be a simple
376
+ // rule here. Whether a command will do anything when busy seems to depend on
377
+ // the current command, the new command and also the current place in the
378
+ // internal state machine!
379
+ this._log(`command ${utils.hexbyte(val)} while busy with ${utils.hexbyte(this._command)} - ignoring`);
380
+ return;
381
+ }
382
+
383
+ this._command = command;
384
+ this._isCommandSettle = false;
385
+ this._isCommandWrite = false;
386
+ this._isCommandVerify = false;
387
+ this._isCommandMulti = false;
388
+ this._isCommandDeleted = false;
389
+ this._isInterruptOnIndexPulse = false;
390
+ this._isWriteTrackCrcSecondByte = false;
391
+
392
+ switch (command) {
393
+ case Command.restore:
394
+ case Command.seek:
395
+ case Command.stepInNoUpdate:
396
+ case Command.stepInWithUpdate:
397
+ case Command.stepOutNoUpdate:
398
+ case Command.stepOutWithUpdate:
399
+ this._commandType = 1;
400
+ this._isCommandVerify = !!(val & CommandBits.typeIVerify);
401
+ this._commandStepRateMs = this._stepRateMsFor(val);
402
+ break;
403
+ case Command.readSector:
404
+ case Command.readSectorMulti:
405
+ case Command.writeSector:
406
+ case Command.writeSectorMulti:
407
+ this._commandType = 2;
408
+ this._isCommandMulti = !!(val & CommandBits.typeIIMulti);
409
+ break;
410
+ case Command.readAddress:
411
+ case Command.readTrack:
412
+ case Command.writeTrack:
413
+ this._commandType = 3;
414
+ break;
415
+ default:
416
+ throw new Error(`unimplemented command ${utils.hexbyte(val)}`);
417
+ }
418
+ if (this._commandType === 2 || (this._commandType === 3 && val & CommandBits.typeIIorIIISettle))
419
+ this._isCommandSettle = true;
420
+ if (
421
+ this._command === Command.writeSector ||
422
+ this._command === Command.writeSectorMulti ||
423
+ this._command === Command.writeTrack
424
+ ) {
425
+ this._isCommandWrite = true;
426
+ this._isCommandDeleted = !!(val & CommandBits.typeIIDeleted);
427
+ }
428
+ // All commands except force interrupt (handled above):
429
+ // - Clear INTRQ and DRQ.
430
+ // - Clear status register result bits.
431
+ // - Set busy.
432
+ // - Spin up if necessary and not inhibited.
433
+ this._setDrq(false);
434
+ this._setIntRq(false);
435
+ this._statusRegister = (this._statusRegister & Status.motorOn) | Status.busy;
436
+
437
+ this._indexPulseCount = 0;
438
+ if (this._statusRegister & Status.motorOn) {
439
+ // Short circuit spin-up if motor is on.
440
+ this._dispatchCommand();
441
+ } else {
442
+ this._statusRegister |= Status.motorOn;
443
+ this._currentDrive.startSpinning();
444
+ // Short circuit spin-up if command requests it.
445
+ // /* NOTE: disabling spin-up wait is a strange facility. It makes a lot of
446
+ // sense for a seek because the disc head can usefully get moving while the
447
+ // motor is spinning up. But other commands like a read track also seem to
448
+ // start immediately. It is unclear whether such a command would be
449
+ // unreliable on a drive that takes a while to come up to speed.
450
+ if (val & CommandBits.disableSpinUp) {
451
+ this._indexPulseCount = 6;
452
+ this._log(`command ${utils.hexbyte(val)} spin up wait disabled, motor was off`);
453
+ this._dispatchCommand();
454
+ } else {
455
+ this._setState(State.spinUpWait);
456
+ }
457
+ }
458
+ }
459
+
460
+ /**
461
+ * @param {State|Number} state
462
+ */
463
+ _setState(state) {
464
+ if (this._logStateChanges && state !== this._state) {
465
+ this._log(
466
+ `State ${this._state} -> ${state} @ tr ${this._trackRegister} ` +
467
+ `sr ${this._sectorRegister} dr ${this._dataRegister} ` +
468
+ `cr ${utils.hexbyte(this._controlRegister)} ` +
469
+ `ptrk ${this._currentDrive.track} hpos ${this._currentDrive.headPosition}`,
470
+ );
471
+ }
472
+ this._state = state;
473
+ this._stateCount = 0;
474
+ }
475
+
476
+ _clearTimer() {
477
+ if (this._timerState !== TimerState.none) {
478
+ this._timerTask.cancel();
479
+ this._timerState = TimerState.none;
480
+ }
481
+ }
482
+
483
+ _clearState() {
484
+ this._setState(State.idle);
485
+ this._clearTimer();
486
+ this._indexPulseCount = 0;
487
+ }
488
+
489
+ _stepRateMsFor(val) {
490
+ switch (val & 0x03) {
491
+ case 0:
492
+ return 6;
493
+ case 1:
494
+ return 12;
495
+ case 2:
496
+ return this._is1772 ? 2 : 20;
497
+ case 3:
498
+ return this._is1772 ? 3 : 30;
499
+ }
500
+ }
501
+
502
+ _handleForceInterrupt(val) {
503
+ const forceInterruptBits = val & 0x0f;
504
+ // EMU NOTE: force interrupt is pretty unclear on the datasheet. From
505
+ // testing on a real 1772:
506
+ // - The command is aborted right away in all cases.
507
+ // - The command completion INTRQ / NMI is _inhibited_ for $D0. In
508
+ // particular, Watford Electronics DDFS will be unhappy unless you behave
509
+ // correctly here.
510
+ // - Force interrupt will spin up the motor and enter an idle state if
511
+ // the motor is off. The idle state behaves a little like a type 1 command
512
+ // insofar as index pulse appears to be reported in the status register.
513
+ // - Interrupt on index pulse is only active for the current command.
514
+ if (this._statusRegister & Status.busy) {
515
+ this._commandDone(false);
516
+ } else {
517
+ if (this._state !== State.idle) throw new Error(`Unexpected state when force interrupt: ${this._state}`);
518
+ this._indexPulseCount = 0;
519
+ this._commandType = 1;
520
+ this._statusRegister &= Status.motorOn;
521
+ if (!(this._statusRegister & Status.motorOn)) {
522
+ this._statusRegister |= Status.motorOn;
523
+ this._currentDrive.startSpinning();
524
+ }
525
+ }
526
+ if (forceInterruptBits === 0) {
527
+ this._isInterruptOnIndexPulse = false;
528
+ } else if (forceInterruptBits === 4) {
529
+ this._isInterruptOnIndexPulse = true;
530
+ } else {
531
+ throw new Error(`1700 force interrupt flags not handled: ${forceInterruptBits}`);
532
+ }
533
+ }
534
+
535
+ _timerFired() {
536
+ if (!(this._statusRegister & Status.busy)) throw new Error("Should be busy");
537
+ const timerState = this._timerState;
538
+ this._timerState = TimerState.none;
539
+ switch (timerState) {
540
+ case TimerState.settle:
541
+ this._dispatchCommand();
542
+ break;
543
+ case TimerState.seek:
544
+ if (
545
+ this._command === Command.stepInNoUpdate ||
546
+ this._command === Command.stepInWithUpdate ||
547
+ this._command === Command.stepOutNoUpdate ||
548
+ this._command === Command.stepOutWithUpdate
549
+ )
550
+ this._checkVerify();
551
+ else this._doSeekStepOrVerify();
552
+ break;
553
+ case TimerState.done:
554
+ this._doneTimer();
555
+ break;
556
+ default:
557
+ throw new Error(`Unexpected timer state ${timerState}`);
558
+ }
559
+ }
560
+
561
+ /**
562
+ * @param {Control} value
563
+ */
564
+ _isSide(value) {
565
+ return !!(value & Control.side);
566
+ }
567
+
568
+ /**
569
+ * @param {Control} value
570
+ */
571
+ _isDoubleDensity(value) {
572
+ // Double density (MFM) is active low.
573
+ return !(value & Control.density);
574
+ }
575
+
576
+ /**
577
+ * @param {Control} value
578
+ */
579
+ _isReset(value) {
580
+ // Reset is active low.
581
+ return !(value & Control.reset);
582
+ }
583
+
584
+ /**
585
+ * @param {Control|Number} val
586
+ */
587
+ _writeControl(val) {
588
+ const isMotorOn = !!(this._statusRegister & Status.motorOn);
589
+ if (this._currentDrive && this._currentDrive.spinning) {
590
+ if (!isMotorOn) {
591
+ throw new Error(
592
+ `Unexpected motor control bit off when setting the control register to ${utils.hexbyte(val)}`,
593
+ );
594
+ }
595
+ this._currentDrive.stopSpinning();
596
+ }
597
+ if (val & Control.drive0 || val & Control.drive1) {
598
+ this._currentDrive = this._drives[val & Control.drive0 ? 0 : 1];
599
+ } else {
600
+ this._currentDrive = null;
601
+ }
602
+ if (this._currentDrive) {
603
+ if (isMotorOn) this._currentDrive.startSpinning();
604
+ this._currentDrive.selectSide(this._isSide(val));
605
+ }
606
+
607
+ // Set up single or double density
608
+ for (const drive of this._drives) drive.set32usMode(this._isDoubleDensity(val));
609
+
610
+ this._controlRegister = val;
611
+
612
+ if (this._isReset(val)) {
613
+ // Go idle, etc
614
+ this._clearState();
615
+ if (this._currentDrive && isMotorOn) this._currentDrive.stopSpinning();
616
+ this._statusRegister = 0;
617
+
618
+ // EMU NOTE: on a real machine, the reset condition appears to hold the
619
+ // sector register at 1 but leave track / data alone (and permit changes
620
+ // to them).
621
+ this._sectorRegister = 1;
622
+ this._isIntRq = false;
623
+ this._isDrq = false;
624
+ this._updateNmi();
625
+
626
+ this._markDetector = 0n;
627
+ this._dataShifter = 0;
628
+ this._dataShiftCount = 0;
629
+ this._isIndexPulse = false;
630
+ this._lastMfmBit = false;
631
+ this._deliverData = 0;
632
+ this._deliverIsMarker = false;
633
+ }
634
+ }
635
+
636
+ _makeSeekNoise(delta) {
637
+ if (this._currentDrive) this._currentDrive.notifySeekAmount(delta);
638
+ }
639
+
640
+ _dispatchCommand() {
641
+ if (!this._currentDrive) throw new Error("Unexpectedly dispatching a command with no drive set");
642
+ if (this._isCommandWrite && this._currentDrive.writeProtect) {
643
+ this._statusRegister |= Status.writeProtected;
644
+ this._commandDone(true);
645
+ return;
646
+ }
647
+
648
+ switch (this._command) {
649
+ case Command.restore:
650
+ this._trackRegister = 0xff;
651
+ this._logCommand(`track register now ${this._trackRegister}`);
652
+ this._dataRegister = 0;
653
+ // Falls through...
654
+ case Command.seek:
655
+ this._doSeekStepOrVerify();
656
+ this._makeSeekNoise(this._dataRegister - this._trackRegister);
657
+ break;
658
+ case Command.stepInNoUpdate:
659
+ this._doSeekStep(1, false);
660
+ this._makeSeekNoise(1);
661
+ break;
662
+ case Command.stepInWithUpdate:
663
+ this._doSeekStep(1, true);
664
+ this._makeSeekNoise(1);
665
+ break;
666
+ case Command.stepOutNoUpdate:
667
+ this._doSeekStep(-1, false);
668
+ this._makeSeekNoise(-1);
669
+ break;
670
+ case Command.stepOutWithUpdate:
671
+ this._doSeekStep(-1, true);
672
+ this._makeSeekNoise(-1);
673
+ break;
674
+ case Command.readSector:
675
+ case Command.readSectorMulti:
676
+ case Command.writeSector:
677
+ case Command.writeSectorMulti:
678
+ case Command.readAddress:
679
+ this._setState(State.searchId);
680
+ this._indexPulseCount = 0;
681
+ break;
682
+ case Command.readTrack:
683
+ this._setState(State.waitIndex);
684
+ this._indexPulseCount = 0;
685
+ break;
686
+ case Command.writeTrack:
687
+ this._setState(State.writeTrackSetup);
688
+ this._indexPulseCount = 0;
689
+ break;
690
+ default:
691
+ throw new Error(`Invalid command ${this._command} in dispatch`);
692
+ }
693
+ }
694
+
695
+ _pulsesCallback(pulses, count) {
696
+ // This callback routine is also used for seek/settle timing which not a precise 64us basis.
697
+ if (!this._currentDrive || !this._currentDrive.spinning || !(this._statusRegister & Status.motorOn)) {
698
+ throw new Error("Something unfortunate happened in the 1770 pulses callback");
699
+ }
700
+ const wasIndexPulse = this._isIndexPulse;
701
+ this._isIndexPulse = this._currentDrive.indexPulse;
702
+ const isIndexPulsePositiveEdge = this._isIndexPulse && !wasIndexPulse;
703
+ const isMfm = count === 16;
704
+
705
+ if (this._isInterruptOnIndexPulse && isIndexPulsePositiveEdge) this._setIntRq(true);
706
+
707
+ // EMU Note: if the chip is idle after copmletion of a type I command, this index pulse and
708
+ // track 0 bits appear maintained. They disappear on spin-down.
709
+ this._updateTypeIStatusBits();
710
+
711
+ switch (this._state) {
712
+ case State.idle:
713
+ this._pulsesCallbackIdle();
714
+ break;
715
+ case State.timerWait:
716
+ break;
717
+ case State.spinUpWait:
718
+ this._pulsesCallbackSpinUpWait();
719
+ break;
720
+ case State.waitIndex:
721
+ if (isIndexPulsePositiveEdge) {
722
+ this._setState(State.inReadTrack);
723
+ // Need to include this byte (directly after the index pulse) in the read
724
+ // track data. Confirmed with a real 1772 & Gotek.
725
+ this._bitstreamReceived(pulses, count, false);
726
+ }
727
+ break;
728
+ case State.searchId:
729
+ case State.inId:
730
+ case State.searchData:
731
+ case State.inData:
732
+ case State.readTrack:
733
+ this._bitstreamReceived(pulses, count, isIndexPulsePositiveEdge);
734
+ if (this._indexPulseCount >= 6) {
735
+ this._statusRegister |= Status.recordNotFound;
736
+ this._commandDone(true);
737
+ }
738
+ break;
739
+ case State.writeSectorDelay:
740
+ this._pulsesCallbackSectorDelay(isMfm);
741
+ break;
742
+ case State.writeSectorLeadInFm:
743
+ this._writeByte(isMfm, 0x00, false);
744
+ if (++this._stateCount === 6) this._setState(State.writeSectorMarkerFm);
745
+ break;
746
+ case State.writeSectorLeadInMfm:
747
+ if (this._stateCount >= 11) this._writeByte(isMfm, 0x00, false);
748
+ if (++this._stateCount === 23) this._setState(State.writeSectorMarkerMfm);
749
+ break;
750
+ case State.writeSectorMarkerFm: {
751
+ const dataByte = this._isCommandDeleted
752
+ ? IbmDiscFormat.deletedDataMarkDataPattern
753
+ : IbmDiscFormat.dataMarkDataPattern;
754
+ this._crc = IbmDiscFormat.crcAddByte(IbmDiscFormat.crcInit(false), dataByte);
755
+ this._writeByte(false, dataByte, true);
756
+ this._setState(State.writeSectorBody);
757
+ break;
758
+ }
759
+ case State.writeSectorMarkerMfm:
760
+ this._pulsesCallbackWriteSectorMarkerMfm();
761
+ break;
762
+ case State.writeSectorBody:
763
+ this._pulsesCallbackWriteSectorBody(isMfm);
764
+ break;
765
+ case State.checkMulti:
766
+ if (this._isCommandMulti) {
767
+ this._sectorRegister++;
768
+ this._indexPulseCount = 0;
769
+ this._setState(State.searchId);
770
+ } else {
771
+ this._commandDone(true);
772
+ }
773
+ break;
774
+ case State.writeTrackSetup:
775
+ this._pulsesCallbackWriteTrackSetup();
776
+ break;
777
+ case State.inWriteTrack:
778
+ this._pulsesCallbackInWriteTrack(isMfm, isIndexPulsePositiveEdge);
779
+ break;
780
+ case State.done:
781
+ this._commandDone(true);
782
+ break;
783
+ default:
784
+ throw new Error(`Unexpected state ${this._state}`);
785
+ }
786
+
787
+ if (isIndexPulsePositiveEdge) this._indexPulseCount++;
788
+ }
789
+
790
+ _pulsesCallbackIdle() {
791
+ if (this._statusRegister & Status.busy) throw new Error("Unexpectedly busy in idle state");
792
+ // different sources disagree on 10 vs 9 index pulses for spin down.
793
+ if (this._indexPulseCount < 9) return;
794
+ this._logCommand("automatic motor off");
795
+ this._currentDrive.stopSpinning();
796
+ this._statusRegister &= ~Status.motorOn;
797
+ // In @scarybeasts's testing on a 1772 the polled type 1 status bits get cleared on spin down.
798
+ if (this._commandType === 1) {
799
+ this._statusRegister &= ~(Status.typeITrack0 | Status.typeIIndex);
800
+ }
801
+ }
802
+
803
+ _pulsesCallbackSpinUpWait() {
804
+ if (this._indexPulseCount < 6) return;
805
+ if (this._commandType === 1) this._statusRegister |= Status.typeISpinUpDone;
806
+ if (this._isCommandSettle) {
807
+ const settleMs = this._is1772 ? 15 : 30;
808
+ this._startTimer(TimerState.settle, settleMs * 1000);
809
+ } else {
810
+ this._dispatchCommand();
811
+ }
812
+ }
813
+
814
+ _pulsesCallbackSectorDelay(isMfm) {
815
+ // Following the data sheet here for byte-for-byte behaviour.
816
+ if (this._stateCount === 0) {
817
+ this._indexPulseCount = 0;
818
+ } else if (this._stateCount === 1) {
819
+ this._setDrq(true);
820
+ } else if (this._stateCount === 10 && this._statusRegister & Status.typeIIorIIIDrq) {
821
+ this._statusRegister |= Status.typeIIorIIILostByte;
822
+ this._commandDone(true);
823
+ }
824
+ this._stateCount++;
825
+ if (this._stateCount === 12) this._setState(isMfm ? State.writeSectorLeadInMfm : State.writeSectorLeadInFm);
826
+ }
827
+
828
+ _pulsesCallbackWriteSectorMarkerMfm() {
829
+ if (this._stateCount < 3) this._writeByte(true, 0xa1, true);
830
+ if (++this._stateCount === 4) {
831
+ const dataByte = this._isCommandDeleted
832
+ ? IbmDiscFormat.deletedDataMarkDataPattern
833
+ : IbmDiscFormat.dataMarkDataPattern;
834
+ this._crc = IbmDiscFormat.crcAddByte(IbmDiscFormat.crcInit(true), dataByte);
835
+ this._writeByte(true, dataByte, false);
836
+ this._setState(State.writeSectorBody);
837
+ }
838
+ }
839
+
840
+ _pulsesCallbackWriteSectorBody(isMfm) {
841
+ if (this._stateCount < this._onDiscLength) {
842
+ let dataByte = this._dataRegister;
843
+ if (this._statusRegister & Status.typeIIorIIIDrq) {
844
+ dataByte = 0;
845
+ this._statusRegister |= Status.typeIIorIIILostByte;
846
+ }
847
+ this._crc = IbmDiscFormat.crcAddByte(this._crc, dataByte);
848
+ this._writeByte(isMfm, dataByte, false);
849
+ if (this._stateCount !== this._onDiscLength - 1) this._setDrq(true);
850
+ } else if (this._stateCount < this._onDiscLength + 2) {
851
+ this._writeByte(isMfm, (this._crc >>> 8) & 0xff, false);
852
+ this._crc = (this._crc << 8) & 0xffff;
853
+ } else {
854
+ this._writeByte(isMfm, 0xff, false);
855
+ this._setState(State.checkMulti);
856
+ }
857
+ this._stateCount++;
858
+ }
859
+
860
+ _pulsesCallbackWriteTrackSetup() {
861
+ if (this._stateCount === 0) {
862
+ this._indexPulseCount = 0;
863
+ this._setDrq(true);
864
+ } else if (this._stateCount === 3) {
865
+ if (this._statusRegister & Status.typeIIorIIIDrq) {
866
+ this._statusRegister |= Status.typeIIorIIILostByte;
867
+ this._commandDone(true);
868
+ } else {
869
+ this._setState(State.inWriteTrack);
870
+ return;
871
+ }
872
+ }
873
+ this._stateCount++;
874
+ }
875
+
876
+ _pulsesCallbackInWriteTrack(isMfm, isIndexPulsePositiveEdge) {
877
+ if (this._stateCount === 0 && !isIndexPulsePositiveEdge) return;
878
+ if (this._stateCount > 0 && isIndexPulsePositiveEdge) {
879
+ this._commandDone(true);
880
+ return;
881
+ }
882
+ if (this._isWriteTrackCrcSecondByte) {
883
+ this._writeByte(isMfm, this._crc & 0xff, false);
884
+ this._isWriteTrackCrcSecondByte = false;
885
+ this._setDrq(true);
886
+ return;
887
+ }
888
+ let dataByte = this._dataRegister;
889
+ if (this._statusRegister & Status.typeIIorIIIDrq) {
890
+ dataByte = 0;
891
+ this._statusRegister |= Status.typeIIorIIILostByte;
892
+ }
893
+ let isMarker = false;
894
+ let isPresetCrc = false;
895
+ switch (dataByte) {
896
+ // 0xF5 and 0xF6 are documented as "not allowed" in FM mode. They
897
+ // actually write 0xA1 / 0xC2 respectively, as per MFM, but it's not
898
+ // known whether any clock bits are omitted, or whether CRC is preset,
899
+ // so bailing for now rather than guessing.
900
+ case 0xf5:
901
+ if (!isMfm) throw new Error("Unhandled 0xf5 in FM");
902
+ isMarker = true;
903
+ isPresetCrc = true;
904
+ dataByte = 0xa1;
905
+ break;
906
+ case 0xf6:
907
+ if (!isMfm) throw new Error("Unhandled 0xf6 in FM");
908
+ isMarker = true;
909
+ dataByte = 0xc2;
910
+ break;
911
+ case 0xf8:
912
+ case 0xf9:
913
+ case 0xfa:
914
+ case 0xfb:
915
+ case 0xfe:
916
+ if (!isMfm) {
917
+ isMarker = true;
918
+ isPresetCrc = true;
919
+ }
920
+ break;
921
+ case 0xfc:
922
+ if (!isMfm) isMarker = true;
923
+ break;
924
+ default:
925
+ break;
926
+ }
927
+ if (isPresetCrc) {
928
+ this._crc = IbmDiscFormat.crcInit(isMfm);
929
+ }
930
+ if (dataByte === 0xf7) {
931
+ this._writeByte(isMfm, (this._crc >>> 8) & 0xff, false);
932
+ this._isWriteTrackCrcSecondByte = true;
933
+ } else {
934
+ this._writeByte(isMfm, dataByte, isMarker);
935
+ if (isMfm && isPresetCrc) {
936
+ // Nothing.
937
+ } else {
938
+ this._crc = IbmDiscFormat.crcAddByte(this._crc, dataByte);
939
+ }
940
+ this._setDrq(true);
941
+ }
942
+ this._stateCount++;
943
+ }
944
+
945
+ _markDetectorTriggered() {
946
+ if (this._isDoubleDensity(this._controlRegister)) {
947
+ // EMU NOTE: unsure as to exactly when MFM sync bytes are spotted. Here we look for MFM 0x00 then MFM 0xa1 (sync).
948
+ // The documented sequence is 12 0x00, 3x 0xa1 (sync).
949
+ if ((this._markDetector & 0xffffffffn) === 0xaaaa4489n) {
950
+ this._deliverData = 0xa1;
951
+ return true;
952
+ }
953
+ // TODO: sync to c2 (5224).
954
+ // Note than an early, naive attempt had it triggered in in the middle of the sector data,
955
+ // so we'll need to study how it actually works in detail.
956
+ // Tag the byte after 3 sync bytes as a marker.
957
+ if ((this._markDetector & 0xffffffffffff0000n) === 0x4489448944890000n) {
958
+ this._deliverIsMarker = true;
959
+ }
960
+ } else {
961
+ // The FM mark detector appears to need 4 data bits' worth of zeros, with clock bits set to 1, to be able to trigger.
962
+ // Tried on @scarybeasts's real 1772-based machine.
963
+ if ((this._markDetector & 0x0000ffff00000000n) === 0x0000888800000000n) {
964
+ const { clocks, data, iffyPulses } = IbmDiscFormat._2usPulsesToFm(
965
+ Number(this._markDetector & 0xffffffffn),
966
+ );
967
+ if (!iffyPulses && clocks === 0xc7) {
968
+ // TODO: see http://info-coach.fr/atari/documents/_mydoc/WD1772-JLG.pdf
969
+ // This suggests that a wider ranges of byte values will function as markers. It may also differ FM vs. MFM.
970
+ if (data === 0xf8 || data === 0xfb || data === 0xfe) {
971
+ // Resync to marker.
972
+ this._deliverData = data;
973
+ this._deliverIsMarker = true;
974
+ return true;
975
+ }
976
+ }
977
+ }
978
+ }
979
+ return false;
980
+ }
981
+
982
+ /**
983
+ * @param {boolean} bit
984
+ */
985
+ _bitReceived(bit) {
986
+ // Always run the mark detector. For a command like "read track", the 1770
987
+ // will re-sync in the middle of the command as appropriate.
988
+ this._markDetector = ((this._markDetector << 1n) & 0xffffffffffffffffn) | (bit ? 1n : 0n);
989
+ if (this._markDetectorTriggered()) {
990
+ this._dataShifter = 0;
991
+ this._dataShiftCount = 0;
992
+ return;
993
+ }
994
+
995
+ this._dataShifter = ((this._dataShifter << 1) | (bit ? 1 : 0)) & 0xffffffff;
996
+ this._dataShiftCount++;
997
+ if (this._isDoubleDensity(this._controlRegister)) {
998
+ if (this._dataShiftCount === 16) {
999
+ this._deliverData = IbmDiscFormat._2usPulsesToMfm(this._dataShifter);
1000
+ this._dataShifter = 0;
1001
+ this._dataShiftCount = 0;
1002
+ }
1003
+ } else {
1004
+ if (this._dataShiftCount === 32) {
1005
+ const { data, iffyPulses } = IbmDiscFormat._2usPulsesToFm(this._dataShifter);
1006
+ // If we're reading MFM as FM, the pulses won't all fall on 4us boundaries. This is fuzzy bits;
1007
+ // we'll return a non-stable read.
1008
+ if (iffyPulses) {
1009
+ const { data: unstableBits } = IbmDiscFormat._2usPulsesToFm(
1010
+ this._currentDrive.getQuasiRandomPulses(),
1011
+ );
1012
+ this._deliverData = unstableBits;
1013
+ } else {
1014
+ this._deliverData = data;
1015
+ }
1016
+ this._dataShifter = 0;
1017
+ this._dataShiftCount = 0;
1018
+ }
1019
+ }
1020
+ }
1021
+
1022
+ _bitstreamReceived(pulses, pulsesCount, isIndexPulsePositiveEdge) {
1023
+ pulses = (pulses << (32 - pulsesCount)) & 0xffffffff;
1024
+ for (let i = 0; i < pulsesCount; ++i) {
1025
+ this._bitReceived(!!(pulses & 0x80000000));
1026
+ pulses = (pulses << 1) & 0xffffffff;
1027
+ }
1028
+ this._byteReceived(isIndexPulsePositiveEdge);
1029
+ }
1030
+
1031
+ /**
1032
+ * @param {boolean} isMfm
1033
+ * @param {Number} byte
1034
+ * @param {boolean} isMarker
1035
+ */
1036
+ _writeByte(isMfm, byte, isMarker) {
1037
+ let pulses;
1038
+ if (isMfm) {
1039
+ if (isMarker) pulses = this._mfmMarkerFor(byte);
1040
+ else {
1041
+ const result = IbmDiscFormat.mfmTo2usPulses(this._lastMfmBit, byte);
1042
+ this._lastMfmBit = result.lastBit;
1043
+ pulses = result.pulses;
1044
+ }
1045
+ } else {
1046
+ const clocks = isMarker ? this._fmMarkerClocksFor(byte) : 0xff;
1047
+ pulses = IbmDiscFormat.fmTo2usPulses(clocks, byte);
1048
+ }
1049
+ this._currentDrive.writePulses(pulses);
1050
+ }
1051
+
1052
+ _fmMarkerClocksFor(byte) {
1053
+ switch (byte) {
1054
+ case 0xfc:
1055
+ return 0xd7;
1056
+ case 0xf8:
1057
+ case 0xf9:
1058
+ case 0xfa:
1059
+ case 0xfb:
1060
+ case 0xfe:
1061
+ return IbmDiscFormat.markClockPattern;
1062
+ }
1063
+ }
1064
+
1065
+ _mfmMarkerFor(byte) {
1066
+ switch (byte) {
1067
+ case 0xa1:
1068
+ return IbmDiscFormat.mfmA1Sync;
1069
+ case 0xc2:
1070
+ return IbmDiscFormat.mfmC2Sync;
1071
+ default:
1072
+ throw new Error(`Bad marker byte ${utils.hexbyte(byte)}`);
1073
+ }
1074
+ }
1075
+
1076
+ _updateTypeIStatusBits() {
1077
+ if (this._commandType !== 1) return;
1078
+ this._statusRegister &= ~(Status.typeITrack0 | Status.typeIIndex);
1079
+ if (this._currentDrive.track === 0) this._statusRegister |= Status.typeITrack0;
1080
+ if (this._currentDrive.indexPulse) this._statusRegister |= Status.typeIIndex;
1081
+ }
1082
+
1083
+ _startTimer(timerState, waitUs) {
1084
+ if (!(this._statusRegister & Status.busy)) throw new Error("Should be busy");
1085
+ if (this._timerState !== TimerState.none) throw new Error("Timer started but still running");
1086
+ this._timerTask.cancel();
1087
+ this._timerState = timerState;
1088
+ this._setState(State.timerWait);
1089
+ this._timerTask.schedule(waitUs * 2);
1090
+ }
1091
+
1092
+ _commandDone(doRaiseIntRq) {
1093
+ if (!(this._statusRegister & Status.busy)) throw new Error("Should be busy");
1094
+ this._doRaiseIntRq = doRaiseIntRq;
1095
+ this._startTimer(TimerState.done, 32);
1096
+ }
1097
+
1098
+ _doneTimer() {
1099
+ this._statusRegister &= ~Status.busy;
1100
+ this._clearState();
1101
+ // Make sure the status are up to date.
1102
+ this._updateTypeIStatusBits();
1103
+
1104
+ // EMU NOTE: leave DRQ alone, if it is raised, leave it raised.
1105
+ if (this._doRaiseIntRq) this._setIntRq(true);
1106
+
1107
+ this._logCommand(`result status ${utils.hexbyte(this._statusRegister)}`);
1108
+ }
1109
+
1110
+ _checkVerify() {
1111
+ if (this._isCommandVerify) {
1112
+ this._indexPulseCount = 0;
1113
+ this._setState(State.searchId);
1114
+ } else {
1115
+ this._commandDone(true);
1116
+ }
1117
+ }
1118
+
1119
+ _doSeekStep(stepDirection, doUpdateTr) {
1120
+ this._currentDrive.seekOneTrack(stepDirection);
1121
+ if (doUpdateTr) this._trackRegister += stepDirection;
1122
+ // TRK0 signal may have been raised or lowered.
1123
+ this._updateTypeIStatusBits();
1124
+ this._startTimer(TimerState.seek, this._commandStepRateMs * 1000);
1125
+ }
1126
+
1127
+ _doSeekStepOrVerify() {
1128
+ if (this._trackRegister === this._dataRegister) {
1129
+ this._checkVerify();
1130
+ return;
1131
+ }
1132
+ const stepDirection = this._trackRegister > this._dataRegister ? -1 : 1;
1133
+ if (this._currentDrive.track === 0 && stepDirection === -1) {
1134
+ this._trackRegister = 0;
1135
+ this._checkVerify();
1136
+ return;
1137
+ }
1138
+ this._doSeekStep(stepDirection, true);
1139
+ }
1140
+
1141
+ _byteReceived(isIndexPulsePositiveEdge) {
1142
+ const isMfm = this._isDoubleDensity(this._controlRegister);
1143
+ const isMarker = this._deliverIsMarker;
1144
+ const data = this._deliverData;
1145
+ this._deliverIsMarker = false;
1146
+
1147
+ switch (this._state) {
1148
+ case State.searchId:
1149
+ if (!isMarker || data !== IbmDiscFormat.idMarkDataPattern) break;
1150
+ this._setState(State.inId);
1151
+ this._crc = IbmDiscFormat.crcAddByte(IbmDiscFormat.crcInit(isMfm), IbmDiscFormat.idMarkDataPattern);
1152
+ break;
1153
+ case State.inId:
1154
+ this._byteReceivedInId(data);
1155
+ break;
1156
+ case State.searchData:
1157
+ this._byteReceivedSearchData(data, isMarker);
1158
+ break;
1159
+ case State.inData:
1160
+ this._byteReceivedInData(data);
1161
+ break;
1162
+ case State.inReadTrack:
1163
+ if (!isIndexPulsePositiveEdge) {
1164
+ this._sendDataToHost(data);
1165
+ } else {
1166
+ this._commandDone(true);
1167
+ }
1168
+ break;
1169
+ default:
1170
+ throw new Error(`Bad state ${this._state}`);
1171
+ }
1172
+ }
1173
+
1174
+ _byteReceivedInId(data) {
1175
+ const isReadAddress = this._command === Command.readAddress;
1176
+ switch (this._stateCount) {
1177
+ case 0:
1178
+ this._onDiscTrack = data;
1179
+ if (isReadAddress) {
1180
+ // The datasheet says "The Track Address of the ID field is written into the sector register"
1181
+ this._sectorRegister = data;
1182
+ }
1183
+ break;
1184
+ case 2:
1185
+ this._onDiscSector = data;
1186
+ break;
1187
+ case 3:
1188
+ // From http://info-coach.fr/atari/documents/_mydoc/WD1772-JLG.pdf, only the lower two bits affect anything.
1189
+ this._onDiscLength = 128 << (data & 0x03);
1190
+ break;
1191
+ }
1192
+ if (isReadAddress) {
1193
+ // Note that unlike the 8271, the CRC bytes are sent along too.
1194
+ this._sendDataToHost(data);
1195
+ }
1196
+ if (this._stateCount < 4) {
1197
+ this._crc = IbmDiscFormat.crcAddByte(this._crc, data);
1198
+ } else {
1199
+ this._onDiscCrc = ((this._onDiscCrc << 8) & 0xffff) | data;
1200
+ }
1201
+ if (++this._stateCount !== 6) return;
1202
+
1203
+ const isCrcError = this._crc !== this._onDiscCrc;
1204
+
1205
+ if (isReadAddress) {
1206
+ if (isCrcError) this._statusRegister |= Status.crcError;
1207
+ // Unlike the 8271, read address returns just a single record. It is also not synchronized
1208
+ // to the index pulse.
1209
+ // EMU TODO: it's likely that timing is generally off for most states,
1210
+ // i.e. the 1770 takes various numbers of internal clock cycles before it
1211
+ // delivers the CRC error, before it goes not busy, etc.
1212
+ // EMU NOTE: must not clear busy flag right away. The 1770 delivers the
1213
+ // last header byte DRQ separately from lowering the busy flag.
1214
+ this._setState(State.done);
1215
+ return;
1216
+ }
1217
+
1218
+ // The data sheet specifies no CRC error unless the fields match so check those first.
1219
+ if (this._trackRegister !== this._onDiscTrack) {
1220
+ this._setState(State.searchId);
1221
+ return;
1222
+ }
1223
+ if (this._commandType === 2 && this._sectorRegister !== this._onDiscSector) {
1224
+ this._setState(State.searchId);
1225
+ return;
1226
+ }
1227
+ if (isCrcError) {
1228
+ this._statusRegister |= Status.crcError;
1229
+ // Unlike the 8271, the 1770 keeps going.
1230
+ this._setState(State.searchId);
1231
+ return;
1232
+ }
1233
+ if (this._commandType === 1) this._commandDone(true);
1234
+ else if (this._isCommandWrite) this._setState(State.writeSectorDelay);
1235
+ else this._setState(State.searchData);
1236
+ }
1237
+
1238
+ _byteReceivedSearchData(data, isMarker) {
1239
+ this._stateCount++;
1240
+ const isMfm = this._isDoubleDensity(this._controlRegister);
1241
+ const multiplier = isMfm ? 2 : 1;
1242
+ // Like the 8271 the data mark is only recognized if 14 bytes have passed.
1243
+ // Unlike the 8271, it gives up after a while longer.
1244
+ if (this._stateCount < 14 * multiplier) return;
1245
+ if (this._stateCount > 31 * multiplier) {
1246
+ this._setState(State.searchId);
1247
+ return;
1248
+ }
1249
+ if (!isMarker) return;
1250
+ if (data === IbmDiscFormat.dataMarkDataPattern) {
1251
+ // Nothing...
1252
+ } else if (data === IbmDiscFormat.deletedDataMarkDataPattern) {
1253
+ // EMU NOTE: the datasheet is ambiguous on whether the deleted mark is
1254
+ // visible in the status register immediately, or at the end of a read.
1255
+ // The state machine diagram says "DAM in time" -> "Set Record Type in
1256
+ // Status Bit 5". But later on it says "At the end of the Read... is
1257
+ // recorded...".
1258
+ // Testing on @scarybeasts's 1772, the state machine diagram is correct: the bit is
1259
+ // visible in the status register immediately during the read.
1260
+ // EMU NOTE: on a multi-sector read, the deleted mark bit is set, and left
1261
+ // set, if _any_ deleted data sector was encountered. The datasheet would
1262
+ // seem to imply that only the most recent sector type is reflected in
1263
+ // the bit, but testing on @scarybeasts's 1772, the bit is set and left set even if
1264
+ // a non-deleted sector is encountered subsequently.
1265
+ this._statusRegister |= Status.typeIIorIIIDeletedMark;
1266
+ } else return;
1267
+ this._setState(State.inData);
1268
+ // CRC error is reset here. It's possible to hit a CRC error in a sector header and then find
1269
+ // an OK matching sector header.
1270
+ this._statusRegister &= ~Status.crcError;
1271
+ this._crc = IbmDiscFormat.crcAddByte(IbmDiscFormat.crcInit(isMfm), data);
1272
+ }
1273
+
1274
+ _byteReceivedInData(data) {
1275
+ this._stateCount++;
1276
+ if (this._stateCount <= this._onDiscLength) {
1277
+ this._crc = IbmDiscFormat.crcAddByte(this._crc, data);
1278
+ this._sendDataToHost(data);
1279
+ return;
1280
+ } else if (this._stateCount <= this._onDiscLength + 2) {
1281
+ this._onDiscCrc = ((this._onDiscCrc << 8) & 0xffff) | data;
1282
+ return;
1283
+ }
1284
+ if (this._crc !== this._onDiscCrc) {
1285
+ this._statusRegister |= Status.crcError;
1286
+ // Sector data CRC error is terminal, even for a multi-sector read.
1287
+ this._commandDone(true);
1288
+ return;
1289
+ }
1290
+ this._setState(State.checkMulti);
1291
+ }
1292
+
1293
+ _sendDataToHost(data) {
1294
+ if (this._commandType !== 2 && this._commandType !== 3) throw new Error("Bad command type");
1295
+ this._setDrq(true);
1296
+ this._dataRegister = data;
1297
+ }
1298
+
1299
+ /// jsbeeb compatibility stuff TODO combine with the noise aware stuff?
1300
+ /**
1301
+ *
1302
+ * @param {Number} drive
1303
+ * @param {Disc} disc
1304
+ */
1305
+ loadDisc(drive, disc) {
1306
+ this._drives[drive].setDisc(disc);
1307
+ }
1308
+
1309
+ get motorOn() {
1310
+ return [this._drives[0] ? this._drives[0].spinning : false, this._drives[0] ? this._drives[1].spinning : false];
1311
+ }
1312
+
1313
+ get drives() {
1314
+ return this._drives;
1315
+ }
1316
+ }
1317
+
1318
+ export class NoiseAwareWdFdc extends WdFdc {
1319
+ // TODO: consider deduplicating with the IntelFdc equivalent.
1320
+ constructor(cpu, ddNoise, scheduler, debugFlags) {
1321
+ super(cpu, scheduler, undefined, debugFlags);
1322
+ let nextSeekTime = 0;
1323
+ let numSpinning = 0;
1324
+ // Update the spin status shortly after the drive state changes to debounce it slightly.
1325
+ const updateSpinStatus = () => {
1326
+ if (numSpinning) ddNoise.spinUp();
1327
+ else ddNoise.spinDown();
1328
+ };
1329
+ for (const drive of this.drives) {
1330
+ drive.addEventListener("startSpinning", () => {
1331
+ numSpinning++;
1332
+ setTimeout(updateSpinStatus, 2);
1333
+ });
1334
+ drive.addEventListener("stopSpinning", () => {
1335
+ --numSpinning;
1336
+ setTimeout(updateSpinStatus, 2);
1337
+ });
1338
+ drive.addEventListener("step", (evt) => {
1339
+ const now = Date.now();
1340
+ if (now > nextSeekTime) nextSeekTime = now + ddNoise.seek(evt.stepAmount) * 1000;
1341
+ });
1342
+ }
1343
+ }
1344
+ }