-- DiskIO.Mesa  Edited by Sandman on June 11, 1980  3:28 PM
-- Copyright  Xerox Corporation 1979, 1980

DIRECTORY
  AltoDefs USING [BYTE, PageNumber, PageSize],
  AltoFileDefs USING [DISK, eofDA, fillinDA, SN, vDA, vDC],
  DiskDefs USING [
    CB, CBinit, CBNil, CBptr, CBZ, CBZptr, DA, DC, DDC, DiskPageDesc, DiskRequest,
    DL, DS, DSfakeStatus, DSfreeStatus, DSgoodStatus, DSmaskStatus, FID,
    FreePageFID, InvalidDA, LastDiskAddress, lCBZ, nCB, NextDiskCommand,
    RetryCount, StandardDisk],
  InlineDefs USING [BITAND, COPY, DIVMOD],
  MiscDefs USING [],
  NucleusOps USING [],
  ProcessDefs USING [DisableInterrupts, EnableInterrupts],
  SegmentDefs USING [
    DataSegmentAddress, DataSegmentHandle, DefaultMDSBase, DeleteDataSegment,
    MakeDataSegment];

DiskIO: PROGRAM
  IMPORTS InlineDefs, ProcessDefs, SegmentDefs
  EXPORTS DiskDefs, MiscDefs, NucleusOps
  SHARES DiskDefs =
  BEGIN OPEN DiskDefs;

  PageNumber: TYPE = AltoDefs.PageNumber;
  DISK: TYPE = AltoFileDefs.DISK;
  SN: TYPE = AltoFileDefs.SN;
  vDA: TYPE = AltoFileDefs.vDA;
  vDC: TYPE = AltoFileDefs.vDC;
  DC: TYPE = DiskDefs.DC;
  DS: TYPE = DiskDefs.DS;
  CBptr: TYPE = DiskDefs.CBptr;
  CBZptr: TYPE = DiskDefs.CBZptr;

  nil: POINTER = LOOPHOLE[0];
  driveNumber: PUBLIC [0..1] ← 0;
  sysDisk: PUBLIC DISK ← StandardDisk;
  cbZone: ARRAY [0..lCBZ) OF UNSPECIFIED;
  zoneInUse: BOOLEAN ← FALSE;

  VirtualDA: PUBLIC PROCEDURE [da: DA] RETURNS [vDA] =
    BEGIN
    RETURN[
      IF da = DA[0, 0, 0, 0, 0] THEN AltoFileDefs.eofDA
      ELSE vDA[
	((da.disk*sysDisk.tracks + da.track)*sysDisk.heads +
	   da.head)*sysDisk.sectors + da.sector]];
    END;

  RealDA: PUBLIC PROCEDURE [v: vDA] RETURNS [da: DA] =
    BEGIN
    i: CARDINAL ← v;
    da ← DA[0, 0, 0, 0, 0];
    IF v # AltoFileDefs.eofDA THEN
      BEGIN
      [i, da.sector] ← InlineDefs.DIVMOD[i, sysDisk.sectors];
      [i, da.head] ← InlineDefs.DIVMOD[i, sysDisk.heads];
      [i, da.track] ← InlineDefs.DIVMOD[i, sysDisk.tracks];
      [i, da.disk] ← InlineDefs.DIVMOD[i, sysDisk.disks];
      IF i # 0 THEN da ← InvalidDA;
      END;
    RETURN
    END;

  -- Disk transfer "process"

  DCseal: AltoDefs.BYTE = 110B;

  DCs: ARRAY vDC OF DC =
    [DC[DCseal, DiskRead, DiskRead, DiskRead, 0, 0], -- ReadHLD
      DC[DCseal, DiskCheck, DiskRead, DiskRead, 0, 0], -- ReadLD
      DC[DCseal, DiskCheck, DiskCheck, DiskRead, 0, 0], -- ReadD
      DC[DCseal, DiskWrite, DiskWrite, DiskWrite, 0, 0], -- WriteHLD
      DC[DCseal, DiskCheck, DiskWrite, DiskWrite, 0, 0], -- WriteLD
      DC[DCseal, DiskCheck, DiskCheck, DiskWrite, 0, 0], -- WriteD
      DC[DCseal, DiskCheck, DiskCheck, DiskCheck, 1, 0], -- SeekOnly
      DC[DCseal, DiskCheck, DiskCheck, DiskCheck, 0, 0]]; -- DoNothing
  -- DoDiskCommand assumes that the version number in a FID (and
  -- in an FP) will never be used (is always one).  It further
  -- assumes that if fp is nil (zero), a FreePageFID was meant;
  -- this allows the rest of the world to use short (3 word) FPs.

  NonZeroWaitCell: WORD ← 1;
  waitCell: POINTER TO WORD ← @NonZeroWaitCell;

  ResetWaitCell: PUBLIC PROCEDURE = BEGIN waitCell ← @NonZeroWaitCell; END;

  SetWaitCell: PUBLIC PROCEDURE [p: POINTER TO WORD]
    RETURNS [preval: POINTER TO WORD] =
    BEGIN
    ProcessDefs.DisableInterrupts[];
    preval ← waitCell;
    waitCell ← p;
    ProcessDefs.EnableInterrupts[];
    RETURN;
    END;

  DoDiskCommand: PUBLIC PROCEDURE [arg: POINTER TO DiskDefs.DDC] =
    BEGIN OPEN arg;
    ptr, next, prev: CBptr;
    la: POINTER TO DL;
    zone: CBZptr = cb.zone;
    cb.headerAddress ← @cb.header;
    IF (la ← cb.labelAddress) = nil THEN cb.labelAddress ← la ← @cb.label;
    cb.dataAddress ← ca;
    IF cb.normalWakeups = 0 THEN cb.normalWakeups ← zone.normalWakeups;
    IF cb.errorWakeups = 0 THEN cb.errorWakeups ← zone.errorWakeups;
    IF fp = nil THEN la.fileID ← FreePageFID ELSE la.fileID ← FID[1, fp.serial];
    la.page ← cb.page ← page;
    IF da # AltoFileDefs.fillinDA THEN cb.header.diskAddress ← RealDA[da];
    IF restore THEN cb.header.diskAddress.restore ← 1;
    cb.command ← DCs[action];
    cb.command.exchange ← driveNumber;
    prev ← PrevCB[zone]; -- Put the command on the disk controller's queue 
    UNTIL waitCell↑ # 0 DO NULL ENDLOOP; -- Wait for Trident to finish
    ProcessDefs.DisableInterrupts[];
    IF (next ← NextDiskCommand↑) # CBNil THEN
      BEGIN
      DO ptr ← next; next ← ptr.nextCB; IF next = CBNil THEN EXIT; ENDLOOP;
      ptr.nextCB ← cb;
      END; -- Take care of a possible race with disk controller.  The disk
    -- may have gone idle (perhaps due to an error) even as we were
    -- adding a command to the chain.  To make sure there was no
    -- error, we check the status of the previous cb in this zone.
    IF NextDiskCommand↑ = CBNil THEN
      SELECT MaskDS[prev.status, DSmaskStatus] FROM
	DSfreeStatus, DSgoodStatus => NextDiskCommand↑ ← cb;
	ENDCASE;
    ProcessDefs.EnableInterrupts[];
    EnqueueCB[zone, cb];
    RETURN
    END;

  -- Disk command block queue


  InitializeCBstorage: PUBLIC PROCEDURE [
    zone: CBZptr, nCBs: CARDINAL, page: PageNumber, init: CBinit] =
    BEGIN
    cb: CBptr;
    i: CARDINAL;
    nq: CARDINAL = nCBs + 1;
    length: CARDINAL = SIZE[CBZ] + nCBs*(SIZE[CB] + SIZE[CBptr]);
    queue: DESCRIPTOR FOR ARRAY OF CBptr ← DESCRIPTOR[@zone.queueVec, nq];
    cbVector: DESCRIPTOR FOR ARRAY OF CB ← DESCRIPTOR[
      @zone.queueVec + SIZE[CBptr]*nq, nCBs];
    IF init = clear THEN Zero[zone, length];
    zone.currentPage ← page;
    zone.cbQueue ← queue;
    zone.qTail ← 0;
    zone.qHead ← 1;
    queue[0] ← NIL; -- end of queue;
    FOR i IN [1..nCBs] DO
      queue[i] ← cb ← @cbVector[i - 1];
      cb.zone ← zone;
      cb.status ← DSfreeStatus;
      ENDLOOP;
    RETURN
    END;

  NumCBs: PROCEDURE [zone: CBZptr] RETURNS [CARDINAL] =
    BEGIN RETURN[LENGTH[zone.cbQueue] - 1] END;

  ClearCB: PROCEDURE [cb: CBptr] =
    BEGIN zone: CBZptr = cb.zone; Zero[cb, SIZE[CB]]; cb.zone ← zone; RETURN END;

  EnqueueCB: PROCEDURE [zone: CBZptr, cb: CBptr] =
    BEGIN
    i: CARDINAL ← zone.qTail;
    IF zone.cbQueue[i] # NIL THEN ERROR;
    zone.cbQueue[i] ← cb;
    IF (i ← i + 1) = LENGTH[zone.cbQueue] THEN i ← 0;
    zone.qTail ← i;
    RETURN
    END;

  DequeueCB: PROCEDURE [zone: CBZptr] RETURNS [cb: CBptr] =
    BEGIN
    i: CARDINAL ← zone.qHead;
    IF (cb ← zone.cbQueue[i]) = NIL THEN ERROR;
    zone.cbQueue[i] ← NIL;
    IF (i ← i + 1) = LENGTH[zone.cbQueue] THEN i ← 0;
    zone.qHead ← i;
    RETURN
    END;

  PrevCB: PROCEDURE [zone: CBZptr] RETURNS [cb: CBptr] =
    BEGIN
    i: CARDINAL ← zone.qTail;
    i ← (IF i = 0 THEN LENGTH[zone.cbQueue] ELSE i) - 1;
    IF (cb ← zone.cbQueue[i]) = NIL THEN ERROR;
    RETURN
    END;

  CleanupCBqueue: PUBLIC PROCEDURE [zone: CBZptr] =
    BEGIN
    cb: CBptr;
    UNTIL zone.cbQueue[zone.qHead] = NIL DO cb ← GetCB[zone, dontClear]; ENDLOOP;
    RETURN
    END;

  -- Removing CBs from the queue.  If for some reason the disk has
  -- gone idle without executing the command, we fake an error
  -- in it so that the entire zone of CBs will get retryed.

  RetryableDiskError: PUBLIC SIGNAL [cb: CBptr] = CODE;
  UnrecoverableDiskError: PUBLIC SIGNAL [cb: CBptr] = CODE;

  MaskDS: PROCEDURE [DS, DS] RETURNS [DS] = LOOPHOLE[InlineDefs.BITAND];

  GetCB: PUBLIC PROCEDURE [zone: CBZptr, init: CBinit] RETURNS [cb: CBptr] =
    BEGIN
    s: DS;
    da: vDA;
    ddc: DDC;
    ec: CARDINAL;
    cb ← DequeueCB[zone];
    UNTIL cb.status.done # 0 DO
      -- not zero means done or fake or free
      IF NextDiskCommand↑ = CBNil AND cb.status.done = 0 THEN
	cb.status ← DSfakeStatus;
      ENDLOOP;
    cb.command.seal ← 0; -- remove command seal
    s ← MaskDS[cb.status, DSmaskStatus];
    SELECT s FROM
      DSgoodStatus =>
	BEGIN
	IF cb.header.diskAddress.restore = 0 THEN
	  BEGIN
	  zone.errorCount ← 0;
	  zone.checkError ← FALSE;
	  zone.currentBytes ← cb.labelAddress.bytes;
	  IF zone.cleanup # LOOPHOLE[0] THEN zone.cleanup[cb];
	  END;
	IF init = clear THEN ClearCB[cb];
	END;
      DSfreeStatus => ClearCB[cb]; -- really means DSneverBeenUsed

      ENDCASE =>
	BEGIN -- some error occurred
	-- busy wait until disk controller is idle
	UNTIL NextDiskCommand↑ = CBNil DO NULL ENDLOOP;
	IF s.dataLate = 0 THEN zone.errorCount ← zone.errorCount + 1;
	ec ← zone.errorCount;
	IF ec >= RetryCount THEN ERROR UnrecoverableDiskError[cb];
	da ← zone.errorDA ← VirtualDA[cb.header.diskAddress];
	IF cb.status.finalStatus = CheckError THEN zone.checkError ← TRUE;
	InitializeCBstorage[zone, NumCBs[zone], cb.page, dontClear];
	IF ec > RetryCount/2 THEN
	  BEGIN -- start a restore before signalling the error
	  LastDiskAddress↑ ← InvalidDA;
	  ddc ← DDC[
	    cb: GetCB[zone, clear], ca: nil, da: [0], page: cb.page, fp: NIL,
	    restore: TRUE, action: SeekOnly];
	  DoDiskCommand[@ddc];
	  END;
	ERROR RetryableDiskError[cb];
	END;
    RETURN
    END;

  -- Don't all Cleanup procedures need to be locked?
  -- Segment swapper
  -- Note that each CB is used twice: first to hold the disk label 
  -- for page i-1, and then to hold the DCB for page i.  It isn't
  -- reused until the DCB for page i-1 is correctly done, which
  -- is guaranteed to be after the disk label for page i-1 is no
  -- longer needed, since things are done strictly sequentially by
  -- page number.
  -- Currently, DiskRequest.lastAction is not used by SwapPages.

  DiskCheckError: PUBLIC SIGNAL [page: PageNumber] = CODE;

  SwapPages: PUBLIC PROCEDURE [arg: POINTER TO swap DiskDefs.DiskRequest]
    RETURNS [PageNumber, CARDINAL] =
    BEGIN OPEN SegmentDefs, arg;
    i: PageNumber;
    cb, nextcb: CBptr;
    seg: DataSegmentHandle ← NIL;
    zone: CBZptr;
    ddc: DiskDefs.DDC ← DiskDefs.DDC[, ca, da↑, , fp, FALSE, action];
    IF zoneInUse THEN {
      seg ← MakeDataSegment[DefaultMDSBase, 1, [hard, topdown, table]];
      zone ← DataSegmentAddress[seg]}
    ELSE {zoneInUse ← TRUE; zone ← @cbZone[0]};
    InitializeCBstorage[zone, nCB, firstPage, clear];
    IF desc # NIL THEN
      BEGIN zone.info ← desc; zone.cleanup ← GetDiskPageDesc; END;
    BEGIN
    ENABLE {
      RetryableDiskError --[cb]-- => {
	ddc.da ← zone.errorDA; ddc.ca ← cb.dataAddress; RETRY};
      UNWIND => IF seg # NIL THEN DeleteDataSegment[seg] ELSE zoneInUse ← FALSE};
    cb ← GetCB[zone, clear];
    FOR i ← zone.currentPage, i + 1 UNTIL i = lastPage + 1 DO
      IF ddc.da = AltoFileDefs.eofDA THEN EXIT;
      IF signalCheckError AND zone.checkError AND zone.errorCount = RetryCount/2
	THEN SIGNAL DiskCheckError[i];
      nextcb ← GetCB[zone, clear];
      cb.labelAddress ← LOOPHOLE[@nextcb.header.diskAddress];
      ddc.cb ← cb;
      ddc.page ← i;
      IF i # zone.currentPage THEN ddc.da ← AltoFileDefs.fillinDA;
      DoDiskCommand[@ddc];
      IF ~fixedCA THEN ddc.ca ← ddc.ca + AltoDefs.PageSize;
      cb ← nextcb;
      ENDLOOP;
    CleanupCBqueue[zone];
    END; -- of enable block
    IF seg # NIL THEN DeleteDataSegment[seg] ELSE zoneInUse ← FALSE;
    RETURN[i - 1, zone.currentBytes]
    END;

  GetDiskPageDesc: PROCEDURE [cb: CBptr] =
    BEGIN
    la: POINTER TO DL = cb.labelAddress;
    desc: POINTER TO DiskPageDesc ← cb.zone.info;
    desc↑ ← DiskPageDesc[
      VirtualDA[la.prev], VirtualDA[cb.header.diskAddress], VirtualDA[la.next],
      la.page, la.bytes];
    RETURN
    END;

  Zero: PUBLIC PROCEDURE [p: POINTER, l: CARDINAL] =
    BEGIN
    IF l = 0 THEN RETURN;
    p↑ ← 0;
    InlineDefs.COPY[from: p, to: p + 1, nwords: l - 1];
    RETURN
    END;


  END..