This repository has been archived by the owner on Dec 24, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 91
/
Copy pathhdc.js
3966 lines (3738 loc) · 170 KB
/
hdc.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* @fileoverview Implements the PCx86 Hard Drive Controller (HDC) component
* @author <a href="mailto:[email protected]">Jeff Parsons</a>
* @copyright © 2012-2019 Jeff Parsons
*
* This file is part of PCjs, a computer emulation software project at <https://www.pcjs.org>.
*
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCjs. If not,
* see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <https://www.pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
if (typeof module !== "undefined") {
var Str = require("../../shared/lib/strlib");
var Web = require("../../shared/lib/weblib");
var DiskAPI = require("../../shared/lib/diskapi");
var Component = require("../../shared/lib/component");
var State = require("../../shared/lib/state");
var PCX86 = require("./defines");
var Interrupts = require("./interrupts");
var Messages = require("./messages");
var ChipSet = require("./chipset");
var Disk = require("./disk");
}
/**
* @typedef {Object} DriveConfig
* @property {string} name
* @property {string} path
* @property {number} type
* @property {number} size (for custom disk geometries; not yet implemented)
* @property {string} mode (for enabling on-demand disk I/O with special server APIs; not currently used)
*/
/**
* @typedef {Object} Drive
* @property {number} iDrive
* @property {number} errorCode
* @property {number} senseCode
* @property {boolean} fRemovable
* @property {Array.<number>} abDriveParms
* @property {Array.<number>} buffer (BYTE array)
* @property {number} bHead
* @property {number} nHeads
* @property {number} wCylinder
* @property {number} nCylinders
* @property {number} bSector
* @property {number} bSectorEnd
* @property {number} nBytes
* @property {number} bSectorBias
* @property {string} name (from DriveConfig.name)
* @property {string} path (from DriveConfig.path)
* @property {string} mode (from DriveConfig.mode)
* @property {number} type (from DriveConfig.type)
* @property {string} sDiskPath (initialized to path, but can change if media removable; eg, ATAPI CD-ROM drive)
* @property {number} nSectors
* @property {number} cbSector
* @property {number} cbTransfer (normally the same as cbSector, except for PACKET commands)
* @property {Disk|null} disk
* @property {Sector|null} sector
* @property {number} iByte
* @property {boolean} useBuffer (true if buffer rather than sector must be used; make sure initBuffer() has been called)
* @property {Array} chunksCached (sparse array of cached chunks)
* @property {Array} chunksMRU (array of cached chunk indexes, starting with the most-recently-used; capped at 128 entries)
*/
/**
* @class HDC
* @property {Array.<DriveConfig>} aDriveConfigs
* @unrestricted (allows the class to define properties, both dot and named, outside of the constructor)
*/
class HDC extends Component {
/**
* HDC(parmsHDC)
*
* The HDC component simulates an STC-506/412 interface to an IBM-compatible fixed disk drive. The first
* such drive was a 10Mb 5.25-inch drive containing two platters and 4 heads. Data spanned 306 cylinders
* for a total of 1224 tracks, with 17 sectors/track and 512 bytes/sector. Support has since been expanded
* to include the original PC AT Western Digital controller.
*
* HDC supports the following component-specific properties:
*
* drives: an array of DriveConfig objects, each containing 'name', 'path', 'type' and 'size' properties
* type: either "XT" (for the PC XT Xebec controller), or "AT" (for the PC AT Western Digital controller)
*
* The 'type' parameter defaults to "XT", enabling support for the PC XT controller. All ports for the
* PC XT controller are referred to as XTC ports. We may also say that the XTC implements the XTA interface,
* to differentiate it from ATA controllers, which came later.
*
* Choosing "AT" as the controller type enables ATA compatibility, and by default, the primary ATA interface is
* enabled (ie, "AT" is equivalent to "AT1"); if you want to enable the secondary ATA interface, specify "AT2".
* PC AT controller ports are referred to as ATC ports, and the ATC implements the ATA interface (along with
* ATAPI, if requested).
*
* If you want to connect an ATAPI (CD-ROM) drive to the controller, specify "ATAPI" instead of "AT"; unlike
* "AT", "ATAPI" defaults to secondary interface (ie, "ATAPI" is equivalent to "ATAPI2"), but you can override
* the default (e.g., "ATAPI1").
*
* If 'path' is empty, a scratch disk image is created; otherwise, we make a note of the path, but we will NOT
* pre-load it like we do for floppy disk images.
*
* My current plan is to read all disk data on-demand, keeping a cache of what we've read, and possibly adding
* some read-ahead as well. Any portions of the disk image that are written before being read will never be read.
*
* TRIVIA: On p.1-179 of the PC XT Technical Reference Manual (revised APR83), it reads:
*
* "WARNING: The last cylinder on the fixed disk drive is reserved for diagnostic use.
* Diagnostic write tests will destroy any data on this cylinder."
*
* Does FDISK insure that the last cylinder is reserved? I'm sure we'll eventually find out.
*
* @this {HDC}
* @param {Object} parmsHDC
*/
constructor(parmsHDC)
{
super("HDC", parmsHDC, Messages.HDC);
this['dmaRead'] = HDC.prototype.doDMARead;
this['dmaWrite'] = HDC.prototype.doDMAWrite;
this['dmaWriteBuffer'] = HDC.prototype.doDMAWriteBuffer;
this['dmaWriteFormat'] = HDC.prototype.doDMAWriteFormat;
this.aDriveConfigs = [];
/*
* We used to eval() sDriveConfigs immediately, but now we wait until initBus() is called, so that
* we can check for any machine overrides.
*/
this.sDriveConfigs = parmsHDC['drives'];
/*
* Set fATC (AT Controller flag) according to the 'type' parameter. This in turn determines other
* defaults. For example, the default XT drive type is 3 (for a 10Mb disk drive), whereas the default
* AT drive type is 2 (for a 20Mb disk drive).
*/
this.fATC = this.fATAPI = false;
this.sType = (parmsHDC['type'] || "XT").toUpperCase();
if (this.sType.slice(0, 2) == "AT") {
this.fATC = true;
this.fATAPI = (this.sType.slice(0, 5) == "ATAPI");
}
this.nInterface = (this.fATAPI? 1 : 0); // default to the secondary interface if type is "ATAPI"
let nInterface = this.sType.slice(-1); // but if an interface is specified (e.g., "AT2", "ATAPI1"), honor it
if (nInterface == '1') {
this.nInterface = 0;
} else if (nInterface == '2') {
this.nInterface = 1;
}
/*
* Support for local disk images is currently limited to desktop browsers with FileReader support;
* when this flag is set, setBinding() allows local disk bindings and informs initBus() to update the
* "listDisks" binding accordingly.
*/
this.fLocalDisks = (!Web.isMobile() && window && 'FileReader' in window);
/*
* The remainder of HDC initialization now takes place in our initBus() handler.
*/
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* @this {HDC}
* @param {string} sHTMLType is the type of the HTML control (e.g., "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (e.g., "listDisks")
* @param {HTMLElement} control is the HTML control DOM object (e.g., HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
let hdc = this;
switch (sBinding) {
case "listDisks":
this.bindings[sBinding] = control;
break;
case "saveHD0":
case "saveHD1":
/*
* Yes, technically, this feature does not require "Local disk support" (which is really a reference
* to FileReader support), but since fLocalDisks is also false for all mobile devices, and since there
* is an "orthogonality" to disabling both features in tandem, let's just let it slide, OK?
*/
if (!this.fLocalDisks) {
if (DEBUG) this.log("Local disk support not available");
/*
* We could also simply remove the control; eg:
*
* control.parentNode.removeChild(@type {Node} (control));
*
* but as long as the parentNode remains, with its accompanying style, the visual layout of the machine
* could look odd. So let's change the parent's style instead.
*/
control.parentNode.style.display = "none";
return false;
}
this.bindings[sBinding] = control;
control.onclick = function(iDrive) {
return function onClickSaveDrive(event) {
let drive = hdc.aDrives && hdc.aDrives[iDrive];
if (drive && drive.disk) {
/*
* Note the similarity (and hence factoring opportunity) between this code and the FDC's
* "saveDisk" binding.
*
* One important difference between the FDC and the HDC is that an FDC may or may not contain
* a disk, whereas an HDC always contains a disk. However, the contents of an HDC's disk may
* never have been initialized with the contents of an external disk image, and therefore the
* disk's sDiskFile/sDiskPath properties may be undefined. sDiskName should always be defined
* though, defaulting to the name of the drive (e.g., "10Mb Hard Disk").
*/
let disk = drive.disk;
let sDiskName = disk.sDiskFile || disk.sDiskName;
let i = sDiskName.lastIndexOf('.');
if (i >= 0) sDiskName = sDiskName.substr(0, i);
sDiskName += ".img";
if (DEBUG) hdc.println("saving disk " + sDiskName + "...");
let sAlert = Web.downloadFile(disk.encodeAsBinary(), "octet-stream", true, sDiskName);
Component.alertUser(sAlert);
} else {
hdc.notice("Hard drive " + iDrive + " is not available.");
}
};
}(+sBinding.slice(-1));
return true;
}
return false;
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {HDC}
* @param {Computer} cmp
* @param {Bus} bus
* @param {CPUX86} cpu
* @param {DebuggerX86} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
this.cmp = cmp;
/*
* Any machine-specific 'drives' settings apply only the first HDC interface.
*/
let aDriveConfigs = cmp.getMachineParm(this.nInterface? 'cdromDrives' : 'drives');
if (aDriveConfigs) {
if (typeof aDriveConfigs == "string") {
this.sDriveConfigs = aDriveConfigs;
} else {
this.aDriveConfigs = aDriveConfigs;
this.sDriveConfigs = "";
}
}
if (this.sDriveConfigs) {
try {
/*
* We must take care when parsing user-supplied JSON-encoded drive data.
*/
this.aDriveConfigs = eval("(" + this.sDriveConfigs + ")");
/*
* Nothing more to do with aDriveConfigs now. initController() and autoMount() (if there are
* any disk image "path" properties to process) will take care of the rest.
*/
this.sDriveConfigs = "";
} catch (e) {
Component.error("HDC drive configuration error: " + e.message + " (" + this.sDriveConfigs + ")");
}
}
/*
* We need access to the ChipSet component, because we need to communicate with
* the PIC and DMA controller.
*/
this.chipset = cmp.getMachineComponent("ChipSet");
this.iDriveTable = 0;
this.iDriveTypeDefault = 3;
if (!this.fATC) {
bus.addPortInputTable(this, HDC.aXTCPortInput);
bus.addPortOutputTable(this, HDC.aXTCPortOutput);
} else {
if (!this.nInterface) {
bus.addPortInputTable(this, HDC.aATCPortInputPrimary);
bus.addPortOutputTable(this, HDC.aATCPortOutputPrimary);
bus.addPortInputWidth(HDC.ATC.DATA.PORT1, 2);
bus.addPortOutputWidth(HDC.ATC.DATA.PORT1, 2);
} else {
bus.addPortInputTable(this, HDC.aATCPortInputSecondary);
bus.addPortOutputTable(this, HDC.aATCPortOutputSecondary);
bus.addPortInputWidth(HDC.ATC.DATA.PORT2, 2);
bus.addPortOutputWidth(HDC.ATC.DATA.PORT2, 2);
}
this.iDriveTable++;
if (this.chipset && this.chipset.model == ChipSet.MODEL_COMPAQ_DESKPRO386) this.iDriveTable++;
this.iDriveTypeDefault = 2;
}
cpu.addIntNotify(Interrupts.DISK, this.intBIOSDisk.bind(this));
cpu.addIntNotify(Interrupts.ALT_DISK, this.intBIOSDiskette.bind(this));
/*
* The following code used to be performed in the HDC constructor, but now we need to wait for information
* about the Computer to be available (e.g., getMachineID() and getUserID()) before we start loading and/or
* connecting to disk images.
*
* If we didn't need auto-mount support, we could defer controller initialization until we received a powerUp()
* notification, at which point reset() would call initController(), or restore() would restore the controller;
* in that case, all we'd need to do here is call setReady().
*/
this.reset();
this.fdc = cmp.getMachineComponent("FDC");
if (this.fdc && this.fATAPI && this.bindings["listDisks"]) {
for (let iDrive = 0; iDrive < this.aDrives.length; iDrive++) {
let drive = this.aDrives[iDrive];
if (!drive.type) this.fdc.addDrive(drive, this, this.bindings["listDisks"]);
}
}
if (!this.autoMount()) this.setReady();
}
/**
* powerUp(data, fRepower)
*
* @this {HDC}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
if (!data) {
this.initController();
if (this.cmp.fReload) {
/*
* If the computer's fReload flag is set, we're required to toss all currently
* loaded disks and remount all disks specified in the auto-mount configuration.
*/
this.autoMount(true);
}
} else {
if (!this.restore(data)) return false;
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {HDC}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean}
*/
powerDown(fSave, fShutdown)
{
return fSave? this.save() : true;
}
/**
* getMachineID()
*
* @return {string}
*/
getMachineID()
{
return this.cmp? this.cmp.getMachineID() : "";
}
/**
* getUserID()
*
* @return {string}
*/
getUserID()
{
return this.cmp? this.cmp.getUserID() : "";
}
/**
* reset()
*
* @this {HDC}
*/
reset()
{
/*
* TODO: The controller is also initialized by the constructor, to assist with auto-mount support,
* so think about whether we can skip powerUp initialization.
*/
this.initController(null, true);
}
/**
* save()
*
* This implements save support for the HDC component.
*
* @this {HDC}
* @return {Object}
*/
save()
{
let state = new State(this);
state.set(0, this.saveController());
return state.data();
}
/**
* restore(data)
*
* This implements restore support for the HDC component.
*
* @this {HDC}
* @param {Object} data
* @return {boolean} true if successful, false if failure
*/
restore(data)
{
return this.initController(data[0]);
}
/**
* initController(data, fHard)
*
* @this {HDC}
* @param {Array} [data]
* @param {boolean} [fHard] true if a machine reset (not just a controller reset)
* @return {boolean} true if successful, false if failure
*/
initController(data, fHard)
{
let i = 0;
let fSuccess = true;
/*
* TODO: This is used to re-select the controller's active drive whenever the machine is restored, but alas,
* we currently only update it and save it for the ATC, not the XTC.
*/
this.iDrive = -1;
/*
* At this point, it's worth calling into question my decision to NOT split the HDC component into separate XTC
* and ATC components, given all the differences, and given that I'm about to write some "if (ATC) else (XTC) ..."
* code. And all I can say in my defense is, yes, it's definitely worth calling that into question.
*
* However, there's also some common code, mostly in the area of disk management rather than controller management,
* and if the components were split, then I'd have to create a third component for that common code (although again,
* disk management probably belongs in its own component anyway).
*
* However, let's not forget that since my overall plan is to have only one PCx86 "binary", everything's going to end
* up in the same bucket anyway, so let's not be too obsessive about organizational details. As long as the number
* of these conditionals is small and they're not performance-critical, this seems much ado about nothing.
*/
if (this.fATC) {
/*
* Since there's no way (and never will be a way) for an HDC to change its "personality" (from 'xt' to 'at'
* or vice versa), we're under no obligation to use the same number of registers, or save/restore format, etc,
* as the original XT controller.
*/
if (data == null) data = [0, 0, 0, 0, 0, 0, 0, HDC.ATC.STATUS.READY, 0, [0, -1]];
this.regError = data[i++];
this.regWPreC = data[i++];
this.regSecCnt = data[i++];
this.regSecNum = data[i++];
this.regCylLo = data[i++];
this.regCylHi = data[i++];
this.regDrvHd = data[i++];
this.regStatus = data[i++];
this.regCommand = data[i++];
this.regFDR = data[i++];
if (typeof this.regFDR == "object") {
let a = this.regFDR;
this.regFDR = a[0];
this.iDrive = a[1];
}
/*
* Additional state is maintained by the Drive object (e.g., buffer, iByte)
*/
} else {
if (data == null) data = [0, HDC.XTC.STATUS.NONE, new Array(14), 0, 0];
this.regConfig = data[i++];
this.regStatus = data[i++];
this.regDataArray = data[i++]; // there can be up to 14 command bytes (6 for normal commands, plus 8 more for HDC.XTC.DATA.CMD.INIT_DRIVE)
this.regDataIndex = data[i++]; // used to control the next data byte to be received
this.regDataTotal = data[i++]; // used to control the next data byte to be sent (internally, we use regDataIndex to read data bytes, up to this total)
this.regReset = data[i++];
this.regPulse = data[i++];
this.regPattern = data[i++];
/*
* Initialize iDriveAllowFail only if it's never been initialized, otherwise its entire purpose will be defeated.
* See the related HACK in intBIOSDisk() for more details.
*/
let iDriveAllowFail = data[i++];
if (iDriveAllowFail !== undefined) {
this.iDriveAllowFail = iDriveAllowFail;
} else {
if (this.iDriveAllowFail === undefined) this.iDriveAllowFail = -1;
}
}
if (this.aDrives === undefined) {
this.aDrives = new Array(this.aDriveConfigs.length);
}
let dataDrives = data[i];
if (dataDrives === undefined) dataDrives = [];
for (let iDrive = 0; iDrive < this.aDrives.length; iDrive++) {
if (this.aDrives[iDrive] === undefined) {
this.aDrives[iDrive] = {};
}
let drive = this.aDrives[iDrive];
let driveConfig = this.aDriveConfigs[iDrive];
if (!this.initDrive(iDrive, drive, driveConfig, dataDrives[iDrive], fHard)) {
fSuccess = false;
}
/*
* XTC only: the original STC-506/412 controller had two pairs of DIP switches to indicate a drive
* type (0, 1, 2 or 3) for drives 0 and 1. Those switch settings are recorded in regConfig, now that
* drive.type has been validated by initDrive().
*/
if (this.regConfig != null && iDrive <= 1) {
this.regConfig |= (drive.type & 0x3) << ((1 - iDrive) << 1);
}
}
if (this.iDrive >= 0) {
this.drive = this.aDrives[this.iDrive];
}
if (this.messageEnabled()) {
this.printMessage("HDC initialized for " + this.aDrives.length + " drive(s)");
}
return fSuccess;
}
/**
* saveController()
*
* @this {HDC}
* @return {Array}
*/
saveController()
{
let i = 0;
let data = [];
if (this.fATC) {
data[i++] = this.regError;
data[i++] = this.regWPreC;
data[i++] = this.regSecCnt;
data[i++] = this.regSecNum;
data[i++] = this.regCylLo;
data[i++] = this.regCylHi;
data[i++] = this.regDrvHd;
data[i++] = this.regStatus;
data[i++] = this.regCommand;
data[i++] = [this.regFDR, this.iDrive];
} else {
data[i++] = this.regConfig;
data[i++] = this.regStatus;
data[i++] = this.regDataArray;
data[i++] = this.regDataIndex;
data[i++] = this.regDataTotal;
data[i++] = this.regReset;
data[i++] = this.regPulse;
data[i++] = this.regPattern;
data[i++] = this.iDriveAllowFail;
}
data[i] = this.saveDrives();
return data;
}
/**
* initBuffer(drive, length)
*
* @this {HDC}
* @param {Drive} drive
* @param {number} [length]
*/
initBuffer(drive, length=drive.nBytes)
{
if (!drive.buffer || drive.buffer.length < length) {
drive.buffer = new Array(length);
}
drive.buffer.fill(0, 0, length);
drive.iByte = 0;
}
/**
* initDrive(iDrive, drive, driveConfig, data, fHard)
*
* TODO: Consider a separate Drive class that both FDC and HDC can use, since there's a lot of commonality
* between the drive objects created by both controllers. This will clean up overall drive management and allow
* us to factor out some common Drive methods (e.g., advanceSector()).
*
* @this {HDC}
* @param {number} iDrive
* @param {Drive} drive
* @param {DriveConfig} driveConfig
* @param {Array} [data]
* @param {boolean} [fHard] true if a machine reset (not just a controller reset)
* @return {boolean} true if successful, false if failure
*/
initDrive(iDrive, drive, driveConfig, data, fHard)
{
let i = 0;
let fSuccess = true;
if (data === undefined) data = [HDC.XTC.DATA.ERR.NONE, 0, false, new Array(8)];
drive.iDrive = iDrive;
/*
* errorCode could be an HDC global, but in order to insulate HDC state from the operation of various functions
* that operate on drive objects (e.g., readData and writeData), I've made it a per-drive variable. This choice
* may be contrary to how the actual hardware works, but I prefer this approach, as long as it doesn't expose any
* incompatibilities that any software actually cares about.
*/
drive.errorCode = data[i++];
drive.senseCode = data[i++];
drive.fRemovable = data[i++];
drive.abDriveParms = data[i++]; // captures drive parameters programmed via HDC.XTC.DATA.CMD.INIT_DRIVE
/*
* TODO: Make buffer a DWORD array rather than a BYTE array (we could even allocate a Memory block for it);
* alternatively, eliminate the buffer entirely and re-establish a reference to the appropriate Disk sector object.
*/
drive.buffer = data[i++];
/*
* The next group of properties are set by various HDC command sequences.
*/
drive.bHead = data[i++];
drive.nHeads = data[i++];
drive.wCylinder = data[i++];
drive.bSector = data[i++];
drive.bSectorEnd = data[i++]; // aka EOT
drive.nBytes = data[i++];
drive.bSectorBias = (this.fATC? 0: 1);
drive.name = driveConfig['name'];
if (drive.name === undefined) drive.name = HDC.DEFAULT_DRIVE_NAME;
drive.path = drive.sDiskPath = driveConfig['path'];
/*
* If no 'mode' is specified, we fall back to the original behavior, which is to completely preload
* any specific disk image, or create an empty (purely local) disk image.
*/
drive.mode = driveConfig['mode'] || (drive.path? DiskAPI.MODE.PRELOAD : DiskAPI.MODE.LOCAL);
/*
* On-demand I/O of raw disk images is supported only if there's a valid user ID; fall back to an empty
* local disk image if there's not.
*/
if (drive.mode == DiskAPI.MODE.DEMANDRO || drive.mode == DiskAPI.MODE.DEMANDRW) {
if (!this.getUserID()) drive.mode = DiskAPI.MODE.LOCAL;
}
drive.type = driveConfig['type'];
if (drive.type === undefined || HDC.aDriveTypes[this.iDriveTable][drive.type] === undefined) drive.type = this.iDriveTypeDefault;
let driveType = HDC.aDriveTypes[this.iDriveTable][drive.type];
drive.nSectors = driveType[2] || 17; // sectors/track
drive.cbSector = drive.cbTransfer = driveType[3] || 512; // bytes/sector (default is 512 if unspecified in the table)
/*
* On a full machine reset, pass the current drive type to setCMOSDriveType() (a no-op on pre-CMOS machines).
*/
if (fHard && this.chipset) {
this.chipset.setCMOSDriveType(this.nInterface*2+iDrive, drive.type);
}
/*
* The next group of properties are set by user requests to load/unload disk images.
*
* We no longer reinitialize drive.disk, in order to retain previously mounted disk across resets.
*/
if (drive.disk === undefined) {
drive.disk = null;
this.notice("Type " + drive.type + " \"" + drive.name + "\" is fixed disk " + iDrive, true);
}
/*
* With the advent of save/restore, we need to verify every drive at initialization, not just whenever
* drive characteristics are initialized. Thus, if we've restored a sensible set of drive characteristics,
* then verifyDrive will create an empty disk if none has been provided, insuring we are ready for
* disk.restore().
*/
this.verifyDrive(drive);
/*
* The next group of properties are managed by worker functions (e.g., doRead()) to maintain state across DMA requests.
*/
drive.iByte = data[i++]; // location of the next byte to be accessed in the above sector
drive.sector = null; // initialized to null by worker, and then set to the next sector satisfying the request
drive.useBuffer = false;
drive.chunksCached = [];
drive.chunksMRU = [];
if (drive.disk) {
let deltas = data[i];
if (deltas !== undefined && drive.disk.restore(deltas) < 0) {
fSuccess = false;
}
if (fSuccess && drive.iByte !== undefined) {
drive.sector = drive.disk.seek(drive.wCylinder, drive.bHead, drive.bSector + drive.bSectorBias);
}
}
return fSuccess;
}
/**
* saveDrives()
*
* @this {HDC}
* @return {Array}
*/
saveDrives()
{
let i = 0;
let data = [];
for (let iDrive = 0; iDrive < this.aDrives.length; iDrive++) {
data[i++] = this.saveDrive(this.aDrives[iDrive]);
}
return data;
}
/**
* saveDrive(drive)
*
* @this {HDC}
* @return {Array}
*/
saveDrive(drive)
{
let i = 0;
let data = [];
data[i++] = drive.errorCode;
data[i++] = drive.senseCode;
data[i++] = drive.fRemovable;
data[i++] = drive.abDriveParms;
data[i++] = drive.buffer;
data[i++] = drive.bHead;
data[i++] = drive.nHeads;
data[i++] = drive.wCylinder;
data[i++] = drive.bSector;
data[i++] = drive.bSectorEnd;
data[i++] = drive.nBytes;
data[i++] = drive.iByte;
data[i] = drive.disk? drive.disk.save() : null;
return data;
}
/**
* copyDrive(iDrive)
*
* @this {HDC}
* @param {number} iDrive
* @return {Object|undefined} (undefined if the requested drive does not exist)
*/
copyDrive(iDrive)
{
let driveNew;
let driveOld = this.aDrives[iDrive];
if (driveOld !== undefined) {
driveNew = {};
for (let p in driveOld) {
driveNew[p] = driveOld[p];
}
}
return driveNew;
}
/**
* verifyDrive(drive, type)
*
* If no disk image is attached, create an empty disk with the specified drive characteristics.
* Normally, we'd rely on the drive characteristics programmed via the HDC.XTC.DATA.CMD.INIT_DRIVE
* command, but if an explicit drive type is specified, then we use the characteristics (geometry)
* associated with that type.
*
* @this {HDC}
* @param {Drive} drive
* @param {number} [type] to create a disk of the specified type, if no disk exists yet
*/
verifyDrive(drive, type)
{
if (drive) {
let nHeads = 0, nCylinders = 0;
if (type == null) {
/*
* If the caller wants us to use the programmed drive parameters, we use those,
* but if there aren't any drive parameters (yet), then use default parameters based
* on drive.type.
*
* We used to do the last step ONLY if there was no drive.path -- otherwise, we'd waste
* time creating an empty disk if autoMount() was going to load an image from drive.path;
* but hopefully the Disk component is smarter now.
*/
nHeads = drive.abDriveParms[2];
if (nHeads) {
nCylinders = (drive.abDriveParms[0] << 8) | drive.abDriveParms[1];
} else {
type = drive.type;
}
}
if (type != null && !nHeads) {
nHeads = HDC.aDriveTypes[this.iDriveTable][type][1];
nCylinders = HDC.aDriveTypes[this.iDriveTable][type][0];
}
if (nHeads) {
/*
* The assumption here is that if the 3rd drive parameter byte (abDriveParms[2]) has been set
* (ie, if nHeads is valid) then the first two bytes (ie, the low and high cylinder byte values)
* must have been set as well.
*
* Do these values agree with those for the given drive type? Even if they don't, all we do is warn.
*/
let driveType = HDC.aDriveTypes[this.iDriveTable][drive.type];
if (driveType) {
if (nCylinders != driveType[0] && nHeads != driveType[1]) {
this.notice("Warning: drive parameters (" + nCylinders + "," + nHeads + ") do not match drive type " + drive.type + " (" + driveType[0] + "," + driveType[1] + ")");
}
}
drive.nCylinders = nCylinders;
drive.nHeads = nHeads;
if (drive.disk == null) {
drive.disk = new Disk(this, drive, drive.mode);
}
}
}
}
/**
* seekDrive(drive, iSector, nSectors)
*
* The HDC doesn't need this function, since all HDC requests from the CPU are handled by doXTCmd(). This function
* is used by other components (e.g., Debugger) to mimic an HDC request, using a drive object obtained from copyDrive(),
* to avoid disturbing the internal state of the HDC's drive objects.
*
* Also note that in an actual HDC request, drive.nBytes is initialized to the size of a single sector; the extent
* of the entire transfer is actually determined by a count that has been pre-loaded into the DMA controller. The HDC
* isn't aware of the extent of the transfer, so in the case of a read request, all readData() can do is return bytes
* until the current track (or, in the case of a multi-track request, the current cylinder) has been exhausted.
*
* Since seekDrive() is for use with non-DMA requests, we use nBytes to specify the length of the entire transfer.
*
* @this {HDC}
* @param {Drive} drive
* @param {number} iSector (a "logical" sector number, relative to the entire disk, NOT a physical sector number)
* @param {number} nSectors
* @return {boolean} true if successful, false if invalid position request
*/
seekDrive(drive, iSector, nSectors)
{
if (drive.disk) {
let aDiskInfo = drive.disk.info();
let nCylinders = aDiskInfo[0];
/*
* If nCylinders is zero, we probably have an empty disk image, awaiting initialization (see verifyDrive())
*/
if (nCylinders) {
let nHeads = aDiskInfo[1];
let nSectorsPerTrack = aDiskInfo[2];
let nSectorsPerCylinder = nHeads * nSectorsPerTrack;
let nSectorsPerDisk = nCylinders * nSectorsPerCylinder;
if (iSector + nSectors <= nSectorsPerDisk) {
drive.wCylinder = Math.floor(iSector / nSectorsPerCylinder);
iSector %= nSectorsPerCylinder;
drive.bHead = Math.floor(iSector / nSectorsPerTrack);
/*
* Important difference between the FDC and the XTC: the XTC uses 0-based sector numbers, so unlike
* FDC.seekDrive(), we must NOT add 1 to bSector below. I could change how sector numbers are stored in
* hard drive images, but it seems preferable to keep the image format consistent and controller-independent.
*/
drive.bSector = (iSector % nSectorsPerTrack);
drive.nBytes = nSectors * aDiskInfo[3];
/*
* NOTE: We don't set nSectorEnd, as an HDC command would, but it's irrelevant, because we don't actually
* do anything with nSectorEnd at this point. Perhaps someday, when we faithfully honor/restrict requests
* to a single track (or a single cylinder, in the case of multi-track requests).
*/
drive.errorCode = HDC.XTC.DATA.ERR.NONE;
/*
* At this point, we've finished simulating what an HDC.XTC.DATA.CMD.READ_DATA command would have performed,
* up through doRead(). Now it's the caller responsibility to call readData(), like the DMA Controller would.
*/
return true;
}
}
}
return false;
}
/**
* autoMount(fRemount)
*
* @this {HDC}
* @param {boolean} [fRemount] is true if we're remounting all auto-mounted disks
* @return {boolean} true if one or more disk images are being auto-mounted, false if none
*/
autoMount(fRemount)
{
if (!fRemount) this.cAutoMount = 0;
for (let iDrive = 0; iDrive < this.aDrives.length; iDrive++) {
let drive = this.aDrives[iDrive];
if (drive.name && drive.sDiskPath) {
if (fRemount && drive.disk && drive.disk.isRemote()) {
/*
* The Disk component has its own logic for remounting remote disks, so skip this disk.
*
* TODO: Consider rewriting how ALL disks are automounted/remounted, now that the Disk component
* is receiving its own powerDown() and powerUp() notifications (originally, it didn't receive them).
*/
continue;
}
if (!this.loadDisk(iDrive, drive.name, drive.sDiskPath, true) && fRemount)
this.setReady(false);
continue;
}
if (fRemount && drive.type !== undefined) {
drive.disk = null;
this.verifyDrive(drive, drive.type);
}
}
return !!this.cAutoMount;
}
/**
* loadDisk(iDrive, sDiskName, sDiskPath, fAutoMount)
*
* @this {HDC}
* @param {number} iDrive
* @param {string} sDiskName
* @param {string} sDiskPath
* @param {boolean} fAutoMount
* @return {boolean} true if disk (already) loaded, false if queued up (or busy)
*/
loadDisk(iDrive, sDiskName, sDiskPath, fAutoMount)
{
let drive = this.aDrives[iDrive];
if (!drive.type) return true;
if (drive.fBusy) {
this.notice("Drive " + iDrive + " busy");
return true;
}
drive.fBusy = true;
if (fAutoMount) {
drive.fAutoMount = true;
this.cAutoMount++;
if (this.messageEnabled()) this.printMessage("loading " + sDiskName);
}
let disk = drive.disk || new Disk(this, drive, drive.mode);
sDiskPath = Web.redirectResource(sDiskPath);
disk.load(sDiskName, sDiskPath, null, this.doneLoadDisk);
return false;
}
/**
* loadSelectedDisk(iDrive, controlDisks)
*
* @this {HDC}
* @param {number} iDrive
* @param {HTMLSelectElement} controlDisks