/*      ACCESS HANDLING COROUTINE

        The disc handling coroutine loops processing
        disc requests.

        Requests from the file handler WORK coroutine
        have priority over requests implicit in the
        cache. When such requests cannot be satisfied,
        generally for want of a cache slot, then an
        implicit cache slot request is processed
        hence freeing a slot. The WORK coroutine request
        will (probably) then be able to be satisfied.

        The types of request available are:
            GETBLOCK    Access a disc block.
            UNTILFREE   Wait for a block to be written
                        to disc. Return is immediate
                        should the block not be present
                        in the cache, or should the block
                        be present and available.
*/


SECTION "ACCESS"


GET "libhdr"

GET "fh2hdr"

GET "manhdr"

GET "iohdr"

LET Access() BE
$( // Assume work to do
   Access.Idle := FALSE
   // Assume we are to resume the main coroutine
   Next.Co := Main.Co

   Block := 0

   // Identify type of access request
   SWITCHON Access.Action INTO
   $( DEFAULT:
         // Unrecognised request
         Abort(Abort.ActionNotKnown)
         LOOP

      CASE Action.Nil:
         ENDCASE

      CASE Action.ResumeWork:
         Next.Co := Work.Co
         Access.Action := Action.Nil
         ENDCASE

      CASE Action.GetBlock:
         Block := FindSlot(AnyKey, Available)
         // If no slot is available go process
         // an outstanding cache operation
         IF Block=0 ENDCASE
         // Unless this is a get available slot
         // request, read the block
         UNLESS Access.Key=0 DO
         $( block!cache.status := read  /***************************/
              Device(Act.Read, Access.Key, Block)
         $)
         // Schedule to return the result to the WORK coroutine
         SetCoResult(Work.Co, Block)
         Access.Action := Action.Nil
         Block := 0
         ENDCASE

      CASE Action.UntilFree:
         Block := FindSlot(Access.Key, Read|Write)
         IF Block=0 DO
         // The block is now free
         $( // Schedule to resume the WORK coroutine
            SetCoResult(Work.Co)
            Access.Action := Action.Nil
            ENDCASE
         $)
         // If we can hasten the freeing of the
         // awaited block ...
         UNLESS Block!Cache.AfterKey=0 DO
              // We cannot hasten its availability
              // at this stage
              Block := 0
   $)

   // Process an implicit cache request

   // Find the most outstanding disc operation
   // prefering the awaited block if possible
   IF Block=0 DO Block := FindSlot(AnyKey, Read|Write)
   IF Block=0 DO
   // If there is no such block there is no work to
   // be done and we can become idle
   $( Access.Idle := TRUE
      PktWait(0, 0)
      LOOP
   $)
   // Otherwise we should read/write the block to disc
   Device([Block!Cache.Status=write -> Act.write, Act.read],
          Block!Cache.Key, Block)
   // and then go repeat the process ...
$) REPEAT


AND Device(Action, Key, Buffer) BE
// Perform requested function on this handler's
// drive.
$( LET KeyREMCyl = Key REM N.BlocksPerCylinder
   LET Status, Res = 0, ?


   UNLESS KeyLowerBound<=Key<=KeyUpperBound DO
        Abort(Abort.KeyOutOfRange, Key) REPEAT
        // (REPEAT in order that a continue from
        // abort is less dangerous)

   // Set the Buffer key
   Buffer!Cache.Key := Key

   FOR i = 1 TO 8 DO
   // Try the disc operation
   $( // Check the checksum if writing
      // Mark as ready-to-write all cache Buffers which
      // depended on this Buffer being written
      IF Action=Act.Write DO CheckBlock(Buffer)

    /****************************************************/
      // Reset the cache status
   // Buffer!Cache.Status := Available
    /****************************************************/

      // Send the request packet to the driver
      $( LET Cyl = LowerCylinder+Key/N.BlocksPerCylinder
         Seek(Cyl)
         // Reset the resumption coroutine
         Next.Co := Main.Co

         Res := SendPkt(-1, DeviceId, Action,
                     ?, ?,
                     Buffer, Size.Block,
                     UnitNo,
                     Cyl,
                     KeyREMCyl REM N.Surfaces,
                     [KeyREMCyl/N.Surfaces]*N.SectorsPerBlock+Sector.Origin)
      $)
      // Should the transfer be successful ...
      IF Res=0 DO
      // A successful operation (perhaps at last)
      // Do any cache updating necessary
      $( // Check the checksum anyway
         UNLESS Checksum(Buffer, Size.Block)=0
            TEST Action=Act.Read
            THEN GOTO failaccess
            ELSE Abort(Abort.InvalidCheckSum) REPEAT
         /************************************************/
         buffer!cache.status := available
         /************************************************/
         IF Action=Act.Write
              FOR i = 1 TO !Cache DO
              $( LET Cachei = Cache!i
                 IF Cachei!Cache.Status=Write &
                    Cachei!Cache.AfterKey=Key DO
                         Cachei!Cache.AfterKey := 0
              $)

         RETURN
      $)
   failaccess:
      // If the transfer was unsuccessful, keep a
      // record of the error bits
      Status := Status | Res
      // Status thus contains a 'cumulative'
      // contoller status word

      // Also re-establish the status
      Buffer!Cache.Status := Action=Act.Write -> Write, Read
   $)

   Abort(Abort.DiscError, Status)
   seek(2)
   seek(-80)
$) REPEAT // The abort may be continued.


AND Seek(Cylinder) BE
     SendPkt(-1, DeviceId, Act.Seek, ?, ?,
             ?, ?, UnitNo, Cylinder)


AND FindSlot(Key, Status) = VALOF
// Find the least recently used disc block
// with the given status
$( let c = 0
   unless key=0 do
   $( for i = 1 to !cache if cache!i!cache.key=key do c := c + 1
      if c>=2 do abort(999)
   $)
   FOR i = !Cache TO 1 BY -1 DO
   $( // Match if KEY=0 or a key match
      // AND block available if available wanted
      // or some overlap on status bits.
      LET Cachei = Cache!i
      LET CacheKey = Cachei!Cache.Key
      LET CacheStatus = Cachei!Cache.Status
      IF [Key=0 | Key=CacheKey] &
           [CacheStatus=0 -> Status=0,
            (CacheStatus&Status) \= 0]
                RESULTIS ShuffleCache(i, 0)
   $)
   // If there isn't one ...
   RESULTIS 0
$)


AND ShuffleCache(Index, Key) = VALOF
// Move the allocated slot to the top of the cache
// list to indicate most recently used
// The cache status will always be 'available'
$( LET Slot = ?
   IF Index=0 DO
   // If a block is given as opposed to an index,
   // convert the block to an index
   $( Index := 1
      FOR i = 1 TO !Cache
           IF Key=Cache!i!Cache.Key DO
           $( Index := i
              BREAK
           $)
   $)
   Slot := Cache!Index
   // Promote the block to cache slot 1
   FOR i = Index-1 TO 1 BY -1 DO
        Cache![i+1] := Cache![i]
   Cache!1 := Slot
   RESULTIS Slot
$)


AND SetCoResult(Coroutine, Result) BE
$( Access.Action := Action.Nil
   Next.Co, Co.Result := Coroutine, Result
$)


AND Resume(Action, Key) = VALOF
// Resume the relevent coroutine after a request
// is made pending. The choice depends upon
// whether the access coroutine is idle or not.
$( Access.Action, Access.Key := Action, Key
   RESULTIS ResumeCo([Access.Idle -> Access.Co, Main.Co])
$)