-- ReadDisk.Mesa  Edited by HGM on February 4, 1981  5:14 PM

DIRECTORY
  Inline USING [BITAND, BITSHIFT],
  Process USING [Yield],
  Storage USING [Node, Free],
  AltoDefs USING [BYTE, BytesPerPage, PageNumber, PageSize],
  AltoFileDefs USING [eofDA, fillinDA, FP, vDA, vDC],
  DiskDefs USING [
    CB, CBinit, CBptr, CBZ, CBZptr, DA, DC, DDC, DiskCheckError, DL, DS,
    DSfakeStatus, DSfreeStatus, DSgoodStatus, DSmaskStatus, FID, InvalidDA,
    RealDA, RetryCount, UnrecoverableDiskError, VirtualDA],
  MiscDefs USING [Zero],
  ProcessDefs USING [CV, DisableInterrupts, EnableInterrupts, InterruptLevel],
  SegmentDefs USING [
    DataSegmentAddress, DataSegmentHandle, HardUp, MakeDataSegment],
  SystemDefs USING [FreePages],
  ReadDefs,
  StatsDefs USING [StatCounterIndex, StatsStringToIndex, StatIncr];

ReadDisk: MONITOR
  IMPORTS
    Inline, Process, Storage, DiskDefs, MiscDefs, ProcessDefs, SegmentDefs, SystemDefs,
    StatsDefs
  EXPORTS ReadDefs
  SHARES DiskDefs =
  BEGIN OPEN DiskDefs;

  pagesRead: StatsDefs.StatCounterIndex;
  diskErrors: StatsDefs.StatCounterIndex;
  checkErrors: StatsDefs.StatCounterIndex;
  overruns: StatsDefs.StatCounterIndex;
  realWaits: StatsDefs.StatCounterIndex;

  diskFinished: CONDITION;
  wakeupLevel: ProcessDefs.InterruptLevel = 13;

  PageNumber: TYPE = AltoDefs.PageNumber;
  vDA: TYPE = AltoFileDefs.vDA;
  vDC: TYPE = AltoFileDefs.vDC;

  ZoneExtension: TYPE = RECORD [
    currentPage: PageNumber,
    cas: POINTER TO ARRAY [0..0) OF POINTER,
    eof: BOOLEAN,
    nCBs: CARDINAL,
    prevcb, nextcb: CBptr,
    ddc: DDC];

  ZEptr: TYPE = POINTER TO ZoneExtension;


  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

  nil: POINTER = LOOPHOLE[0];
  nextDiskCommand: POINTER TO CBptr = LOOPHOLE[521B];
  lastDiskAddress: POINTER TO DA = LOOPHOLE[523B];


  -- 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.

  --FreePageFID: FID = FID[-1,SN[1,1,1,17777B,-1]];

  DoDiskCommand: PROCEDURE [zone: CBZptr, ddc: POINTER TO DiskDefs.DDC] =
    BEGIN
    ze: ZEptr = zone.info;
    cb: CBptr = ddc.cb;
    la: POINTER TO DL;
    cb.headerAddress ← @cb.header;
    IF (la ← cb.labelAddress) = nil THEN cb.labelAddress ← la ← @cb.label;
    IF ddc.ca = NIL AND ddc.action # SeekOnly THEN
      BEGIN
      AttemptToSmashPageZero: ERROR = CODE;
      ERROR AttemptToSmashPageZero;
      END;
    cb.dataAddress ← ddc.ca;
    IF cb.normalWakeups = 0 THEN cb.normalWakeups ← zone.normalWakeups;
    IF cb.errorWakeups = 0 THEN cb.errorWakeups ← zone.errorWakeups;
    la.fileID ← FID[1, ddc.fp.serial];
    la.page ← cb.page ← ddc.page;
    IF ddc.da # AltoFileDefs.fillinDA THEN cb.header.diskAddress ← RealDA[ddc.da];
    IF ddc.restore THEN cb.header.diskAddress.restore ← 1;
    cb.command ← DCs[ddc.action];
    ProcessDefs.DisableInterrupts[];
    BEGIN
    ptr, next: CBptr;
    IF (next ← nextDiskCommand↑) # nil THEN
      BEGIN
      trap: CARDINAL ← 0;
      LongDiskChain: SIGNAL = CODE;
      DO
	ptr ← next;
	next ← ptr.nextCB;
	IF next = nil THEN EXIT;
	IF (trap ← trap + 1) > 100 THEN SIGNAL LongDiskChain;
	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↑ = nil THEN
      BEGIN
      prevcb: CBptr = ze.prevcb;
      IF prevcb = NIL THEN nextDiskCommand↑ ← cb
      ELSE
	SELECT MaskDS[prevcb.status, DSmaskStatus] FROM
	  DSfreeStatus, DSgoodStatus => nextDiskCommand↑ ← cb;
	  ENDCASE;
      END;
    END;
    ProcessDefs.EnableInterrupts[];
    EnqueueActiveCB[zone, cb];
    ze.prevcb ← cb;
    RETURN
    END;

  InitializeCBstorage: 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]) + SIZE[ZoneExtension];
    cbVector: DESCRIPTOR FOR ARRAY OF CB ← DESCRIPTOR[
      @zone.queueVec + SIZE[CBptr]*nq, nCBs];
    ze: ZEptr;
    IF init = clear THEN
      BEGIN
      wakeupBit: WORD = Inline.BITSHIFT[1, wakeupLevel];
      MiscDefs.Zero[zone, length];
      zone.info ← zone + length - SIZE[ZoneExtension];
      ze ← zone.info;
      zone.normalWakeups ← zone.errorWakeups ← wakeupBit;
      END
    ELSE ze ← zone.info;
    zone.currentPage ← page;
    zone.cbQueue ← DESCRIPTOR[@zone.queueVec, nq];
    zone.qHead ← 0;
    zone.qTail ← nCBs;
    FOR i IN [0..nCBs) DO
      zone.cbQueue[i] ← cb ← @cbVector[i];
      cb.zone ← zone;
      cb.status ← DSfreeStatus;
      ENDLOOP;
    zone.cbQueue[nCBs] ← NIL;
    RETURN
    END;

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


  EnqueueActiveCB: 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;

  DequeueActiveCB: 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;



  -- 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.

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

  GetNextPage: PUBLIC ENTRY PROCEDURE [zone: CBZptr] RETURNS [cb: CBptr] =
    BEGIN
    s: DS;
    ze: ZEptr = zone.info;
    DO
      -- loop for error recovery
      IF ze.eof THEN ERROR DiskCheckError[LAST[PageNumber]];
      cb ← DequeueActiveCB[zone];
      UNTIL cb.status.done # 0 DO
	-- not zero means done or fake or free
	IF nextDiskCommand↑ = nil AND cb.status.done = 0 THEN
	  cb.status ← DSfakeStatus
	ELSE BEGIN StatsDefs.StatIncr[realWaits]; WAIT diskFinished; END;
	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; StatsDefs.StatIncr[pagesRead]; RETURN END;
	  FillThisBuffer[cb];
	  END;
	ENDCASE =>
	  BEGIN -- some error occurred
	  first: CARDINAL;
	  ec: CARDINAL;
	  UNTIL (nextDiskCommand↑ = nil) DO Process.Yield[]; ENDLOOP;
	  StatsDefs.StatIncr[diskErrors];
	  IF cb.status.finalStatus = CheckError THEN
	    StatsDefs.StatIncr[checkErrors];
	  IF cb.status.dataLate = 1 THEN StatsDefs.StatIncr[overruns];
	  ec ← zone.errorCount ← zone.errorCount + 1;
	  IF ec >= RetryCount THEN ERROR UnrecoverableDiskError[cb];
	  IF ec = 1 THEN zone.errorDA ← VirtualDA[cb.header.diskAddress];
	  ze.currentPage ← cb.page;
	  ze.prevcb ← ze.nextcb ← NIL;
	  ze.ddc.da ← zone.errorDA;
	  InitializeCBstorage[zone, ze.nCBs, cb.page, dontClear];
	  IF ec > RetryCount/2 THEN
	    BEGIN -- start a restore
	    ddc: DDC;
	    lastDiskAddress↑ ← InvalidDA;
	    cb ← DequeueActiveCB[zone];
	    ClearCB[cb];
	    ddc ← DDC[cb, nil, [0], ze.currentPage, NIL, TRUE, SeekOnly];
	    DoDiskCommand[zone, @ddc];
	    first ← 1;
	    END
	  ELSE first ← 0;
	  FillBuffers[zone, first];
	  END;
      ENDLOOP;
    END;

  ReleasePage: PUBLIC PROCEDURE [cb: CBptr] =
    BEGIN
    zone: CBZptr ← cb.zone;
    ze: ZEptr = zone.info;
    IF cb.labelAddress.bytes # AltoDefs.BytesPerPage THEN ze.eof ← TRUE;
    IF ze.eof THEN RETURN;
    FillThisBuffer[cb];
    RETURN
    END;

  StopReading: PUBLIC PROCEDURE [zone: CBZptr] =
    BEGIN
    ze: ZEptr = zone.info;
    UNTIL (nextDiskCommand↑ = nil) DO Process.Yield[]; ENDLOOP;
    ProcessDefs.CV[wakeupLevel] ← NIL;
    SystemDefs.FreePages[ze.cas[0]];
    Storage.Free[zone];
    END;

  -- Page Reader

  -- 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.


  StartReading: PUBLIC PROCEDURE [fp: POINTER TO AltoFileDefs.FP, nCBs: CARDINAL]
    RETURNS [zone: CBZptr] =
    BEGIN
    buffer: POINTER;
    cas: POINTER TO ARRAY [0..0) OF POINTER;
    ze: ZEptr;
    zone ← Storage.Node[
      SIZE[CBZ] + nCBs*(SIZE[CBptr] + SIZE[CB]) + SIZE[ZoneExtension] + nCBs];
    InitializeCBstorage[zone, nCBs, 0, clear];
    ze ← zone.info;
    ze.cas ← cas ← LOOPHOLE[ze + SIZE[ZoneExtension]];
    buffer ← AllocateBuffers[nCBs];
    FOR i: CARDINAL IN [0..nCBs) DO
      cas[i] ← buffer; buffer ← buffer + AltoDefs.PageSize; ENDLOOP;
    ze.ddc ← [, , fp.leaderDA, , fp, FALSE, ReadD];
    ze.currentPage ← 0;
    ze.nCBs ← nCBs;
    ProcessDefs.CV[wakeupLevel] ← @diskFinished;
    FillBuffers[zone, 0];
    RETURN
    END;

  -- Allocate our buffer from low memory in order to minimize sandbaring problems.
  -- The recipe for trouble (using AllocateResidentPages) is as follows:
  -- 1) Start booting (allocates 5 pages as high as possible)
  -- 2) Allocate another page of frames or heap, this becomes the sandbar
  -- 3) Finish booting (free up 5 pages) leaving a 5 page hole
  -- 4) Allocate another page of frames/heap inside the 5 page hole.
  -- This leaves a 4 page hole.  If we start booting again, we will use 5 more pages.
  AllocateBuffers: PROCEDURE [pages: CARDINAL] RETURNS [POINTER] =
    BEGIN OPEN SegmentDefs;
    seg: DataSegmentHandle ← MakeDataSegment[pages: pages,info: HardUp];
    RETURN[DataSegmentAddress[seg]];
    END;

  FillBuffers: PROCEDURE [zone: CBZptr, first: CARDINAL] =
    BEGIN
    ze: ZEptr = zone.info;
    FOR i: CARDINAL IN [first..ze.nCBs) DO
      cb: CBptr ← DequeueActiveCB[zone]; FillThisBuffer[cb]; ENDLOOP;
    END;

  FillThisBuffer: PROCEDURE [nextcb: CBptr] =
    BEGIN
    zone: CBZptr = nextcb.zone;
    ze: ZEptr = zone.info;
    thiscb: CBptr;
    pageNumber: PageNumber ← ze.currentPage;
    IF ze.ddc.da = AltoFileDefs.eofDA THEN ERROR DiskCheckError[pageNumber];
    ClearCB[nextcb];
    thiscb ← ze.nextcb;
    ze.nextcb ← nextcb;
    IF thiscb = NIL THEN RETURN;
    thiscb.labelAddress ← LOOPHOLE[@nextcb.header.diskAddress];
    ze.ddc.cb ← thiscb;
    ze.ddc.page ← pageNumber;
    ze.ddc.ca ← ze.cas[pageNumber MOD ze.nCBs];
    IF pageNumber # zone.currentPage THEN ze.ddc.da ← AltoFileDefs.fillinDA;
    ze.currentPage ← pageNumber + 1;
    DoDiskCommand[zone, @ze.ddc];
    END;

  pagesRead ← StatsDefs.StatsStringToIndex["Disk pages read by Booter"];
  diskErrors ← StatsDefs.StatsStringToIndex["Disk errors encountered by Booter"];
  checkErrors ← StatsDefs.StatsStringToIndex[
    "Disk Check errors encountered by Booter"];
  overruns ← StatsDefs.StatsStringToIndex["Disk Overruns encountered by Booter"];
  realWaits ← StatsDefs.StatsStringToIndex["Waits for disk"];
  END..