-- File: DiskRequestor.mesa
-- Last edited by Levin:  30-Apr-81 16:21:09

DIRECTORY
  AltoDefs USING [BytesPerPage, PageNumber],
  ControlDefs USING [FrameHandle, NullFrame],
  DiskIODefs USING [
    CompletionProcedure, CompletionStatus, DiskRequest, eofvDA, FID,
    fillInvDA, NormalCompletionProcedure, RequestError, RequestID, vDA,
    VerboseCompletionProcedure],
  DiskIOPrivate USING [
    CB, CBPtr, DA, DCs, DL, DS, DSfakeStatus, DSgoodStatus, DSmaskStatus,
    EnqueueCB, EnqueueForDisk, FreeCB, GetCBs,
    InvalidDA, IOSynch, loggingEnabled, MaskDS, nSafetyDCBs, SimulateCompletion,
    SpliceLists, SynchRecord, sysDisk, VirtualDA],
  FrameOps USING [GetReturnLink, MyLocalFrame, SetReturnFrame],
  Inline USING [DIVMOD],
  ProcessDefs USING [Detach];

DiskRequestor: MONITOR LOCKS synch.LOCK USING synch: DiskIOPrivate.IOSynch
  IMPORTS DiskIOPrivate, FrameOps, Inline, ProcessDefs
  EXPORTS DiskIODefs, DiskIOPrivate =

  BEGIN OPEN DiskIODefs, DiskIOPrivate;

  -- Global Variables --

  scratchPage: PUBLIC POINTER;
  maxOps: PUBLIC CARDINAL;

  -- Statistics Logging --

  diskReads, diskWrites: PUBLIC LONG CARDINAL;

  -- Miscellaneous Declarations --

  BytesPerDiskPage: CARDINAL = AltoDefs.BytesPerPage;

  notLastSkip: RequestID = 10101B;
  lastSkip: RequestID = 50403B;
  dummyID: RequestID = 112233B;

  ImpossibleStatus: ERROR = CODE;
  ImpossibleRequestID: ERROR = CODE;

  -- Externally Visible Procedures --

  InitiateDiskIO: PUBLIC PROCEDURE [operation: POINTER TO DiskRequest] =
    -- initiates the requested disk transfers and returns to the caller.  The
    -- CompletionProcedure in the argument DiskRequest is invoked as each
    -- transfer terminates.
    BEGIN
    ValidateRequest[operation];
    WITH op: operation SELECT FROM
      DoNothing => RETURN;
      SeekOnly => -- not implemented yet --NULL;
      ENDCASE =>
	BEGIN
	xferTail, skipTail: CBPtr;
	[xferTail, skipTail] ← BuildXferChain[operation];
	IF skipTail = NIL THEN {EnqueueForDisk[@xferTail]; RETURN};
	EnqueueXferChain[operation, xferTail, skipTail];
	END;
    END;

  BogusRequest: PUBLIC ERROR [reason: RequestError, operation: POINTER TO DiskRequest] =
    CODE;

  DiskError: PUBLIC ERROR [status: CompletionStatus] = CODE;

  -- Internal Procedures --

  ValidateRequest: PROCEDURE [operation: POINTER TO DiskRequest] =
    -- checks that the argument DiskRequest is valid, and raises BogusRequest if
    -- it isn't.
    BEGIN
    maxvda: vDA ← MaxLegalvDA[];

    MaxLegalvDA: PROCEDURE RETURNS [vDA] =
      -- returns the maximum legal vDA for the current disk structure.
      BEGIN
      RETURN[
	VirtualDA[
	  DA[sysDisk.sectors - 1, sysDisk.tracks - 1, sysDisk.heads - 1,
	     sysDisk.disks - 1, 0]]]
      END;

    CheckvDA: PROCEDURE [vda: vDA] RETURNS [isFillIn: BOOLEAN] =
      -- validates a virtual disk address.  (This validation is not intended to
      -- be complete, but rather serves only as a blunder check).
      BEGIN
      IF isFillIn ← (vda = fillInvDA) THEN RETURN;
      IF vda = eofvDA OR vda > maxvda THEN ERROR BogusRequest[badvDA, operation];
      END;

    CheckStrictvDA: PROCEDURE [vda: vDA, allowEOF: BOOLEAN]
      RETURNS [isEOF: BOOLEAN] =
      -- checks that vda is a legitimate one (fillInvDA is not permitted).
      BEGIN
      IF vda = fillInvDA THEN ERROR BogusRequest[illegalvDASequence, operation];
      IF isEOF ← (vda = eofvDA) THEN
	IF allowEOF THEN RETURN ELSE ERROR BogusRequest[badvDA, operation];
      END;

    EnsureLegalvDASequence: PROCEDURE [noFillIns: BOOLEAN] =
      -- ensures that fillInvDAs appear only where permitted.
      BEGIN
      first: CARDINAL ← 0;
      nXfers: CARDINAL ← LENGTH[operation.xfers];
      i: CARDINAL;
      IF nXfers = 0 THEN ERROR BogusRequest[illegalTransfer, operation];
      IF noFillIns THEN
	BEGIN
	IF operation.pagesToSkip ~= 0 THEN GO TO IllegalSequence;
	FOR i IN [0..nXfers) DO
	  [] ← CheckStrictvDA[operation.xfers[i].diskAddress, FALSE];
	  ENDLOOP;
	RETURN
	END;
      IF operation.pagesToSkip ~= 0 THEN
	[] ← CheckStrictvDA[operation.firstPagevDA, FALSE]
      ELSE
	BEGIN
	[] ← CheckStrictvDA[operation.xfers[0].diskAddress, FALSE];
	FOR i IN [1..nXfers) DO
	  IF CheckvDA[operation.xfers[i].diskAddress] THEN {first ← i + 1; EXIT};
	  REPEAT FINISHED => RETURN;
	  ENDLOOP;
	END;
      FOR i IN [first..nXfers) DO
	IF ~CheckvDA[operation.xfers[i].diskAddress] THEN GO TO IllegalSequence;
	ENDLOOP;
      EXITS IllegalSequence => ERROR BogusRequest[illegalvDASequence, operation];
      END;

    WITH op: operation SELECT FROM
      DoNothing => RETURN;
      SeekOnly =>
	BEGIN
	IF LENGTH[op.xfers] ~= 0 THEN ERROR BogusRequest[illegalTransfer, operation];
	[] ← CheckStrictvDA[op.firstPagevDA, FALSE];
	END;
      ReadLD, ReadD, WriteD =>
	BEGIN
	EnsureLegalvDASequence[noFillIns: FALSE];
	IF LENGTH[op.xfers] > maxOps - nSafetyDCBs THEN GO TO TooManyXfers;
	END;
      WriteHLD, WriteLD =>
	BEGIN
	EnsureLegalvDASequence[noFillIns: TRUE];
	[] ← CheckStrictvDA[op.next, TRUE];
	[] ← CheckStrictvDA[op.prev, TRUE];
	IF LENGTH[op.xfers] > maxOps THEN GO TO TooManyXfers;
	END;
      ReadHLD =>
	BEGIN
	EnsureLegalvDASequence[noFillIns: TRUE];
	IF LENGTH[op.xfers] > maxOps THEN GO TO TooManyXfers;
	END;
      ENDCASE;
    EXITS TooManyXfers => ERROR BogusRequest[tooManyTransfers, operation];
    END;

  BuildXferChain: PROCEDURE [operation: POINTER TO DiskRequest]
    RETURNS [xferTail, skipTail: CBPtr] =
    -- (synchronously) constructs the chain of transfer CBs required by 'operation'.
    BEGIN
    thisCB: CBPtr;
    page: AltoDefs.PageNumber ← operation.firstPage + operation.pagesToSkip;
    label: POINTER TO DL;
    thisvDA: vDA;
    nXfers: CARDINAL = LENGTH[operation.xfers];
    last: CARDINAL = nXfers - 1;
    lastIsFillin: BOOLEAN = (operation.xfers[last].diskAddress = fillInvDA);
    i: CARDINAL;

    FillLabelAndCB: PROCEDURE =
      -- prepares 'thisCB' for execution.
      BEGIN
      thisCB.headerAddress ← @thisCB.header;
      thisCB.labelAddress ← label;
      thisCB.dataAddress ← operation.xfers[i].buffer;
      IF loggingEnabled THEN
	SELECT operation.action FROM
	  ReadHLD, ReadLD, ReadD => diskReads ← diskReads + 1;
	  WriteHLD, WriteLD, WriteD => diskWrites ← diskWrites + 1;
	  ENDCASE;
      thisCB.command ← DCs[operation.action];
      thisCB.requestID ← operation.xfers[i].id;
      thisCB.postProc ← operation.proc;
      thisCB.omitRestore ← operation.noRestore;
      label.page ← page;
      page ← page + 1;
      label.fileID ← LOOPHOLE[operation.fileID];
      WITH op: operation SELECT FROM
	WriteLD, WriteHLD =>
	  BEGIN
	  label.prev ← RealDA[IF i = 0 THEN op.prev ELSE op.xfers[i - 1].diskAddress];
	  label.next ← RealDA[IF i = last THEN op.next ELSE op.xfers[i + 1].diskAddress];
	  label.bytes ← IF i = last AND op.next = eofvDA THEN op.lastByteCount
			ELSE BytesPerDiskPage;
	  END;
	ENDCASE;
      END;

    IF operation.pagesToSkip = 0 THEN
      BEGIN
      xferTail ← GetCBs[n: (IF lastIsFillin THEN nXfers + 1 ELSE nXfers), wait: TRUE];
      skipTail ← NIL;
      END
    ELSE
      BEGIN
      xferTail ← GetCBs[n: nXfers + nSafetyDCBs, wait: TRUE];
      skipTail ← xferTail.nextOnQueue;
      xferTail.nextOnQueue ← skipTail.nextOnQueue.nextOnQueue;
      skipTail.nextOnQueue.nextOnQueue ← skipTail;
      END;
    thisCB ← xferTail.nextOnQueue;
    thisvDA ← operation.xfers[0].diskAddress;
    FOR i ← 0, i + 1 DO
      nextvDA: vDA;
      IF thisvDA ~= fillInvDA THEN thisCB.header.diskAddress ← RealDA[thisvDA];
      label ← @thisCB.label;
      IF i = last THEN EXIT;
      nextvDA ← operation.xfers[i + 1].diskAddress;
      IF nextvDA = fillInvDA THEN
        label ← LOOPHOLE[@thisCB.nextOnQueue.header.diskAddress];
      FillLabelAndCB[];
      thisCB.nextCB ← thisCB.nextOnQueue; thisCB ← thisCB.nextOnQueue;
      thisvDA ← nextvDA;
      ENDLOOP;
    IF lastIsFillin THEN
      BEGIN
      label ← LOOPHOLE[@thisCB.nextOnQueue.header];
      FillLabelAndCB[];
      thisCB.nextCB ← thisCB.nextOnQueue;
      thisCB ← thisCB.nextOnQueue;
      -- The extra CB allocated for the label of the last chained CB is now appended
      -- to the command chain.  It will initiate a seek to the predecessor of the
      -- last transferred page, which, at worst, will cause the heads to move to a
      -- new cylinder.  This kludge makes it easy to recover the extra CB at the
      -- proper time, at the possible expense of some potentially unnecessary arm motion.
      thisCB.command ← DCs[SeekOnly];
      thisCB.requestID ← dummyID;
      thisCB.postProc ← [normal[DummyCompletionProc]];
      END
    ELSE FillLabelAndCB[];
    END;

  EnqueueXferChain: PROCEDURE [operation: POINTER TO DiskRequest, xTail, sTail: CBPtr] =
    -- outer shell for DoXferEnqueuing.
    BEGIN
    frame: ControlDefs.FrameHandle;

    DoXferEnqueuing: PROCEDURE RETURNS [frame: ControlDefs.FrameHandle] =
      -- does the work of skipping pages from 'firstPage', whose vDA is known,
      -- up to the page preceding the one specified in 'xfers[0]'.  It then enqueues
      -- the xfer chain and exits.
      BEGIN
      thisCB, labelCB: CBPtr;
      page: AltoDefs.PageNumber ← operation.firstPage;
      last: AltoDefs.PageNumber ← operation.firstPage + operation.pagesToSkip - 1;
      label: POINTER TO DL;
      fileID: FID ← operation.fileID;
      nonXferID: RequestID ← operation.nonXferID;
      userPostProc: CompletionProcedure ← operation.proc;
      ourPostProc: CompletionProcedure ← userPostProc;
      noRestore: BOOLEAN ← operation.noRestore;
      synch: SynchRecord;
      skipTail: CBPtr ← sTail;
      xferTail: CBPtr ← xTail;
      Returnee: PROCEDURE [ControlDefs.FrameHandle];

      PrepareAndEnqueueThisCB: PROCEDURE =
	-- sets up 'thisCB' as part of a skip chain.
	BEGIN
	thisCB.headerAddress ← @thisCB.header;
	thisCB.labelAddress ← label;
	thisCB.dataAddress ← scratchPage;
	IF loggingEnabled THEN diskReads ← diskReads + 1;
	thisCB.command ← DCs[ReadD];
	SetPostProcAndID[thisCB];
	thisCB.omitRestore ← noRestore;
	label.page ← page;
	page ← page + 1;
	label.fileID ← LOOPHOLE[fileID];
	IF skipTail ~= NIL THEN skipTail.nextCB ← thisCB;
	EnqueueCB[@skipTail, thisCB];
	END;

      SetPostProcAndID: PROCEDURE [cb: CBPtr] =
	-- fills in cb.postProc and cb.requestID.
	BEGIN
 	cb.requestID ←
	  IF (cb.postProc ← ourPostProc) = userPostProc THEN nonXferID ELSE notLastSkip;
        END;

      ResetPostProcs: PROCEDURE =
	-- recomputes the postProc and requestID fields for all cb's on skipTail.
	BEGIN
	cb: CBPtr ← skipTail;
	IF cb = NIL THEN RETURN;
	DO
	  SetPostProcAndID[cb];
	  IF (cb ← cb.nextOnQueue) = skipTail THEN EXIT;
	  ENDLOOP;
	END;

      SkipChecker: VerboseCompletionProcedure =
	-- invoked when an asynchronously queued skip request has completed.
	BEGIN
	WITH userPostProc: userPostProc SELECT FROM
	  verbose => userPostProc.proc[nonXferID, status, header, label];
	  ENDCASE;
	IF id = lastSkip THEN DoNotify[@synch, status];
	END;

      DoNotify: ENTRY PROCEDURE [synch: IOSynch, status: CompletionStatus] =
        INLINE
	-- wake up EnqueueAndWait, passing the status of the last CB that it enqueued.
	{synch.status ← status; NOTIFY synch.lastDone};

      EnqueueAndWait: ENTRY PROCEDURE [synch: IOSynch]
        RETURNS [CompletionStatus] = INLINE
	-- enqueues skipTail, waits until skips are complete, then returns the outcome.
	BEGIN
	synch.status ← noStatus; synch.lastDone.timeout ← 0;
	skipTail.requestID ← lastSkip;
	EnqueueForDisk[@skipTail];
	skipTail ← NIL;
	WHILE synch.status = noStatus DO WAIT synch.lastDone; ENDLOOP;
	RETURN[synch.status]
	END;

      BEGIN
      -- inner block so 'SpliceAndEnqueueXfers' can access local variables of this frame
      frame ← ControlDefs.NullFrame;
      thisCB ← skipTail.nextOnQueue;
      labelCB ← skipTail;
      skipTail ← NIL;
      thisCB.header.diskAddress ← RealDA[operation.firstPagevDA];
      WHILE page < last DO
	label ← LOOPHOLE[@labelCB.header.diskAddress];
	PrepareAndEnqueueThisCB[];
	thisCB ← labelCB;
	IF (labelCB ← GetCBs[n: 1, wait: FALSE]) = NIL THEN EXIT;
	REPEAT FINISHED => {FreeCB[labelCB]; GO TO SpliceAndEnqueueXfers};
	ENDLOOP;
      -- If control comes here, we have insufficient CBs available to build the
      -- entire chain now.  We fork a process to wait for more.  However, in order
      -- to keep our frame around, we have to do some fancy shuffling of return
      -- links.  The dirty deed is done by the next three lines of code plus
      -- 'ShuffleLinks'.
      frame ← FrameOps.MyLocalFrame[];
      Returnee ← LOOPHOLE[FrameOps.GetReturnLink[]];
      Returnee[frame];
      -- The code below is actually executed in a different process.  When it
      -- has completed, control will exit to the Mesa runtime system's process
      -- destruction machinery.
      ourPostProc ← CompletionProcedure[verbose[SkipChecker]];
      ResetPostProcs[];
      WHILE page < last DO
	IF (labelCB ← GetCBs[n: 1, wait: FALSE]) = NIL THEN
	  BEGIN
	  IF skipTail ~= NIL AND EnqueueAndWait[@synch] ~= ok THEN
	    GO TO ErrorWhileSkipping;
	  labelCB ← GetCBs[n: 1, wait: TRUE];
	  END;
	label ← LOOPHOLE[@labelCB.header.diskAddress];
	PrepareAndEnqueueThisCB[];
	thisCB ← labelCB;
	REPEAT
	  FINISHED =>
	    {ourPostProc ← userPostProc; ResetPostProcs[]; GO TO SpliceAndEnqueueXfers};
	ENDLOOP;
      EXITS
	SpliceAndEnqueueXfers =>
	  BEGIN
	  label ← LOOPHOLE[@xferTail.nextOnQueue.header.diskAddress];
	  PrepareAndEnqueueThisCB[];
	  skipTail.nextCB ← xferTail.nextOnQueue;
	  SpliceLists[@skipTail, @xferTail];
	  EnqueueForDisk[@skipTail];
	  END;
	ErrorWhileSkipping => {FreeCB[thisCB]; SimulateCompletion[@xferTail]};
      END; -- inner block for 'SpliceAndEnqueueXfers'

      END;

    ShuffleLinks: PROCEDURE [frame: ControlDefs.FrameHandle] =
      -- invoked to patch up return links for forked process.  The argument frame
      -- belongs to DoXferEnqueuing, but the forking machinery has set the return
      -- link of ShuffleLinks to be the process destruction code.  We patch
      -- ShuffleLinks to return to DoXferEnqueueing, and patch DoXferEnqueuing
      -- to return to the process destroyer.  Clear?
      BEGIN
      frame.returnlink ← FrameOps.GetReturnLink[];
      FrameOps.SetReturnFrame[frame];
      END;

    frame ← DoXferEnqueuing[];
    IF frame ~= ControlDefs.NullFrame THEN ProcessDefs.Detach[FORK ShuffleLinks[frame]];
    END;

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

  TransformStatus: PUBLIC PROCEDURE [status: DS] RETURNS [CompletionStatus] =
    -- This procedure maps hardware status information to a simpler form for
    -- completion procedures to handle.
    BEGIN
    SELECT MaskDS[status, DSmaskStatus] FROM
      DSgoodStatus => RETURN[ok];
      DSfakeStatus => RETURN[neverStarted];
      ENDCASE;
    SELECT status.finalStatus FROM
      CheckError => RETURN[checkError];
      IllegalSector => RETURN[badDiskAddress];
      ENDCASE;
    IF status.seekFailed = 1 THEN RETURN[seekFailure];
    IF status.notReady = 1 THEN RETURN[diskOffline];
    IF status.checksumError = 1 THEN RETURN[checksumError];
    IF status.dataLate = 1 THEN RETURN[dataLate];
    ERROR ImpossibleStatus
    END;

  DummyCompletionProc: NormalCompletionProcedure =
    -- This procedure is invoked only when the relevant CB is the "extra" one
    -- required to hold the label for the last legitimate CB in a linked-label chain.
    {IF id ~= dummyID THEN ERROR ImpossibleRequestID}; 

  END.