        .TITLE  ZFACP - Example "File System" ACP
        .SBTTL  External and local symbol definitions
        .IDENT  \X01-000\

;++
; ZFACP - Example "File System" ACP
;
; Register conventions used throughout this image:  
;
;       R0      VCB address
;       R3      IRP address
;       R4      PCB address
;       R5      UCB address
;       R6      AQB address
;--

;       standard VMS data structures

        .LIBRARY \SYS$LIBRARY:LIB\

        $ACBDEF                                 ; AST control block
        $AQBDEF                                 ; ACP queue block
        $CCBDEF                                 ; Channel control block
        $DCDEF                                  ; Device classes and types
        $DDBDEF                                 ; Device data block
        $DEVDEF                                 ; Device characteristics
        $DYNDEF                                 ; Data structure ID codes
        $IODEF                                  ; I/O function codes
        $IPLDEF                                 ; Interrupt priority levels
        $IRPDEF                                 ; I/O request packet
        $IRPEDEF                                ; IRP Extension
        $PCBDEF                                 ; Process control block
        $PSLDEF                                 ; Processor status longword
        $SSDEF                                  ; System status codes
        $UCBDEF                                 ; Unit control block
        $VCBDEF                                 ; Volume control block

        ; bits in UCB$L_DEVDEPEND

        $DEFINI UCB
        _VIELD  ZF,0,<<FOPEN,,M>>               ; file is open
        $DEFEND UCB

        ; redefine some VCB fields

VCB_Q_CURIRP = VCB$L_CUR_FID
VCB_Q_IOSB = VCB$L_IXHDR2LBN

ZF_K_ACPTYPE = 250
ZF_K_ACPCLASS = 251

        .PAGE
        .SBTTL  Local storage

        .PSECT  LOCAL_DATA      NOEXE, PIC, LONG
        .ALIGN  LONG

CODE_LOCK_RANGE:
        .ADDRESS DISMOUNT_CHECK                 ; argument for $LKWSET
        .ADDRESS DISMOUNT_CHECK_END

NLA0_CCB_ADDR:
        .BLKL   1

AQB_ADDR:
        .BLKL   1                               ; addr of our AQB

NLA0_CHAN:
        .BLKW   1                               ; channel to null device

NLA0_DVCNAM:
        .ASCID  /_NLA0/                         ; null device name

CLOSE_FILE_MSG:                                 ; message to write when
        .ASCII  /FILE CLOSED/                   ;  "closing" a "file"
CFM_K_LENGTH = . - CLOSE_FILE_MSG               ; length thereof

        .PSECT  CODE            SHR, NOWRT, PIC, LONG

        .PAGE
        .SBTTL  ACP_MAIN, Main Program

        .ENTRY  ACP_MAIN, ^M<>

        $CMKRNL_S       B^BEGIN                 ; change mode to kernel
        RET                                     ; all done

        .ENTRY  BEGIN, ^M<R2,R3,R4,R5,R6,R7>

;       when we first start, we have been invoked via the $WAKE call in
;       the ACP mount image.  We're expected to clear the CREATING bit
;       in the AQB, remember where the AQB is for future reference, and
;       then enter the grand loop, where we hibernate until the mount
;       image does the IO$_MOUNT $QIO.

        MOVL    G^CTL$GL_PCB, R4                ; get our PCB address
        MOVL    PCB$L_PID(R4), R4               ;  and from there, our IPID

        CALLS   #0, LOCKR_IODB                  ; lock I/O data base (IPL 2)
        MOVAL   G^IOC$GL_AQBLIST, R1            ; get AQB listhead addr in R1
        MOVL    (R1), R6                        ; get address of first AQB

10$:    CMPL    R4, AQB$L_ACPPID(R6)            ; found right AQB?
        BEQL    FOUND_AQB                       ; if eql, yes
        MOVL    AQB$L_LINK(R6), R6              ; get next link
        BNEQ    10$                             ;  and loop

        CALLS   #0, UNLOCK_IODB                 ; else release mutex, IPL 0
        MOVL    #SS$_NOAQB, R0                  ; AQB not found
        RET

FOUND_AQB:
        CALLS   #0, UNLOCK_IODB                 ; release mutex, lower IPL
        MOVL    R6, AQB_ADDR                    ; save AQB address
        BBSC    #AQB$V_CREATING,-               ; check the creating bit, tell
                AQB$B_STATUS(R6), 10$           ;  mount image we're running
        MOVZWL  #SS$_WRONGACP, R0               ; if not set, return err status
        RET

        ; if we're here, we're in good shape. 

10$:
        $LKWSET_S INADR=CODE_LOCK_RANGE         ; lock code that is to run at
                                                ;  elevated IPL

;       assign a channel to the null device so that we have a CCB for $QIO calls

        $ASSIGN_S  DEVNAM=NLA0_DVCNAM,-
                CHAN=NLA0_CHAN,-                ; assign channel to NLA0
                ACMODE=#PSL$C_USER              ;  at USER level
        BLBS    R0, 20$                         ; check for error
        RET                                     ;  return if n.g.

20$:    MOVZWL  NLA0_CHAN, R0                   ; obtain channel number
        JSB     G^IOC$VERIFYCHAN                ; get CCB addr in R1
        BLBS    R0, 30$                         ; check for error
        RET

30$:    MOVL    R1, NLA0_CCB_ADDR               ; save the CCB address

        BRB     ACP_LOOP                        ; go look for work

        .PAGE
        .SBTTL  ACP_LOOP, ACP Main Processing Loop

ACP_LOOP:

;       look for and process all IRPs in the AQB queue

        MOVL    AQB_ADDR, R6                    ; get addr of ACP Queue Block
10$:    REMQHI  AQB$L_ACPQFL(R6), R3            ; try to get an IRP
        BVS     20$                             ;  br if queue empty

        CMPB    IRP$B_TYPE(R3), #DYN$C_IRP      ; is it really an IRP?
        BEQL    15$                             ;  br if yes

        PUSHL   R3                              ; else assume it's a VCB ($QIO
        CALLS   #1, PROCESS_VCB                 ;  completion notification)
        BRB     10$                             ;  look for next IRP or VCB

15$:    PUSHL   R3                              ; else pass it to the 
        CALLS   #1, PROCESS_IRP                 ;  IRP processor
        BRB     10$                             ; and look for another
20$:

;       come here when queue empty

        CALLS   #0, CHECK_ACP_DONE              ; delete ACP if necessary

        $HIBER_S                                ; else hibernate; upon 
        BRW     10$                             ;  awakening, look for more work

        .PAGE
        .SBTTL  PROCESS_IRP, Process an IRP

;++
; Inputs:
;       4(AP)   Address of the IRP
;--

        .ENTRY  PROCESS_IRP, ^M<R2,R3,R4,R5,R6,R7,R8,R9>

        MOVL    4(AP), R3                       ; get the IRP address
        MOVL    IRP$L_UCB(R3), R5               ; get the UCB address

        MOVL    AQB_ADDR, R6                    ; get the AQB address

        EXTZV   S^#IO$V_FCODE, S^#IO$S_FCODE,-  ; get function code from IRP,
                IRP$W_FUNC(R3), R0              ;  ignoring modifier bits

        ; branch according to I/O function code

        CMPL    R0, #IO$_MOUNT                  ; mount volume request
        BEQL    DO_MOUNT

        CMPL    R0, #IO$_ACPCONTROL             ; dismount volume request
        BEQL    DO_DISMOUNT

        CMPL    R0, #IO$_CREATE                 ; "open file" request
        BEQL    DO_OPENFILE

        CMPL    R0, #IO$_DEACCESS               ; "close file" request
        BEQL    DO_CLOSEFILE

        MOVL    #SS$_ILLIOFUNC, IRP$L_IOST1(R3) ; no other legal funcs
        PUSHL   R3                              ;  for this ACP
        CALLS   #1, POST_BAD_IRP                ;  so complain

        RET

        .PAGE

;       handle volume mount requests

DO_MOUNT:
        CMPB    #ZF_K_ACPTYPE,-                 ; check ACP type against
                AQB$B_ACPTYPE(R6)               ;  our type code
        BEQL    10$                             ; br if ok
        MOVL    #SS$_WRONGACP, IRP$L_IOST1(R3)  ; else 

        PUSHL   R3                              ; post IRP with error
        CALLS   #1, POST_BAD_IRP                ;  status

        RET                                     ; and return

10$:    BISL2   #DEV$M_MNT, UCB$L_DEVCHAR(R5)   ; set MOUNTED bit
        PUSHL   R3                              ; send IRP 
        CALLS   #1, POST_GOOD_IRP               ;  to posting

        RET                                     ;  and return

;       handle volume dismount requests; not much to do here!

DO_DISMOUNT:
        PUSHL   R3                              ; send IRP 
        CALLS   #1, POST_GOOD_IRP               ;  to posting

        RET                                     ;  and return

;       handle "file open" requests (just like writes, but they happen
;       to come through the ACP)

DO_OPENFILE:
        JSB     G^EXE$INSIOQ                    ; send user's IRP to driver
        MOVL    UCB$L_VCB(R5), R0               ; get VCB address
        ADAWI   #-1, VCB$W_TRANS(R0)            ; note one fewer pending op
        PUSHL   R5                              ; dismount the volume
        CALLS   #1, DISMOUNT_CHECK              ;  if time to do so
        RET

        .PAGE

;       handle "file close" requests
;       (issue our own $QIO to the device)

DO_CLOSEFILE:
        MOVL    UCB$L_VCB(R3), R0               ; get the VCB address
        INSQTI  (R3), VCB_Q_CURIRP(R0)          ; stash the user's IRP 
        MOVL    NLA0_CCB_ADDR, R1               ; get the CCB address
        MOVL    R5, CCB$L_UCB(R1)               ; point CCB to the right dvc

        $QIO_S  CHAN=NLA0_CHAN,-                ; issue $QIO on the channel
                FUNC=#IO$_WRITEVBLK,-           ;  ordinary write function
                IOSB=VCB_Q_IOSB(R0),-           ;  IOSB is written to the VCB
                ASTADR=IO_COMPL_AST,-           ;  AST procedure entry point
                ASTPRM=R0,-                     ;  AST is notified of VCB addr
                P1=CLOSE_FILE_MSG,-             ;  write a simple
                P2=#CFM_K_LENGTH                ;   message

        RET                                     ; return to main loop

        ; when the above $QIO completes, the system will call this AST
        ; procedure, passing it the address of the VCB.  

        .ENTRY  IO_COMPL_AST, ^M<R6>

        MOVL    4(AP), R0                       ; get VCB address (AST param)
        MOVL    VCB$L_AQB(R0), R6               ; get the AQB address
        INSQTI  (R0), AQB$L_ACPQFL(R6)          ; put the VCB on the ACP's 
                                                ;  work queue
        $WAKE_S                                 ; wake ourselves
        MOVZWL  #SS$_NORMAL, R0                 ; and
        RET                                     ;  return

        ; when the ACP main loop finds the VCB on its work queue, 
        ; it will call this procedure, passing it the address of the VCB.  

        .ENTRY  PROCESS_VCB, ^M<R3,R5>

        MOVL    4(AP), R0                       ; get VCB address
        REMQHI  VCB_Q_CURIRP(R0), R3            ; get user's IRP
        MOVQ    VCB_Q_IOSB(R0), IRP$L_IOST1(R3) ; set up IOSB
        BLBC    IRP$L_IOST1(R3), 10$            ; br if completed unsuccessfully

        MOVL    IRP$L_UCB(R3), R5               ; else get the UCB address
        BICL2   #ZF_M_FOPEN, UCB$L_DEVDEPEND(R5);  and clear the file open bit

10$:    PUSHL   R3                              ; send the user's IRP
        CALLS   #1, POST_IRP                    ;  to posting
        MOVZWL  #SS$_NORMAL, R0                 ; and
        RET                                     ;  return

        .PAGE
        .SBTTL  POST_xxx_IRP, IRP posting routines

;++
; POST_IRP, post IRP with caller-specified status
; POST_GOOD_IRP, post IRP with caller-specified status
; POST_BAD_IRP, post IRP with caller-specified status
;
; Abstract:
;
;       POST_IRP is a common routine which posts completion for an IRP.
;       POST_GOOD_IRP writes SS$_NORMAL into the IOSB field in the IRP,
;        then falls through to POST_IRP.
;       POST_BAD_IRP clears the second longword of the IOSB in the IRP,
;        then falls through to POST_IRP.  (The caller should fill in 
;        the first longword.)
;
;       All of the above entry points decrement the VCB transaction 
;       count and call DISMOUNT_CHECK, which will dismount the volume
;       (deallocate the VCB, etc.) if the transaction count is zero
;       and the DMT bit is set.  
;
; Inputs:
;       4(AP)   - IRP address
;--

        .ENTRY  POST_GOOD_IRP, ^M<R3,R5>

        MOVL    4(AP), R3                       ; get IRP address
        MOVZWL  #SS$_NORMAL, R0                 ; set good status
        CLRL    IRP$L_IOST2(R3)                 ; clear second lw of IOSB
        BRB     POST_IRP_COMMON

        .ENTRY  POST_BAD_IRP, ^M<R3,R5>

        MOVL    4(AP), R3                       ; get IRP address
        CLRL    IRP$L_IOST2(R3)                 ; clear second lw of IOSB
        BRB     POST_IRP_COMMON

        .ENTRY  POST_IRP, ^M<R3,R5>

        MOVL    4(AP), R3                       ; get IRP address

POST_IRP_COMMON:
        MOVL    IRP$L_UCB(R3), R5               ; get UCB address
        JSB     G^COM$POST                      ; send IRP to posting

        MOVL    UCB$L_VCB(R5), R0               ; get VCB address
        ADAWI   #-1, VCB$W_TRANS(R0)            ; note one I/O done
        PUSHL   R5                              ; see if volume can be
        CALLS   #1, DISMOUNT_CHECK              ;  dismounted

        RET                                     ; and return to caller

        .PAGE
        .SBTTL  DISMOUNT_CHECK, Check for volume dismount
;++
; DISMOUNT_CHECK, Check for volume dismount
;
; Abstract:
;
;       Check to see if UCB is marked for dismount; 
;       if so, and if volume is idle, dismount the ACP from the UCB.  
;       We call this procedure whenever the ACP gets rid of an IRP,
;       either by sending it to posting or by requeuing it to the
;       driver (ie whenever we decrement VCB$W_TRANS).  
;
; Inputs:
;
;       4(AP)   - UCB address
;--

        .ENTRY  DISMOUNT_CHECK, ^M<R5,R6>

        MOVL    4(AP), R5                       ; get UCB address
        BBC_W   S^#DEV$V_DMT,-                  ; br if unit not
                UCB$L_DEVCHAR(R5), 100$         ;  marked for dismount

        CALLS   #0, LOCK_IODB                   ; lock the I/O database mutex
        FORKLOCK SAVIPL=-(SP)                   ; acquire the forklock
        MOVL    UCB$L_VCB(R5), R0               ; get VCB address
        CMPW    VCB$W_TRANS(R0), #1             ; is volume idle?
        BNEQ    20$                             ;  br if no

        ; note that 1, not 0, is the inited value of VCB$W_TRANS.

        BICL2   #DEV$M_MNT!DEV$M_DMT,-          ; clear mounted and
                UCB$L_DEVCHAR(R5)               ;  dismounting bits
        CLRL    UCB$L_VCB(R5)                   ; disconnect VCB from UCB
        MOVL    VCB$L_AQB(R0), R6               ; get AQB address
        JSB     G^COM$DRVDEALMEM                ; deallocate VCB
        DECB    AQB$B_MNTCNT(R6)                ; note one less vol on ACP

20$:    FORKUNLOCK NEWIPL=(SP)+                 ; release the forklock
        CALLS   #0, UNLOCK_IODB                 ; unlock I/O database mutex

100$:
        MOVZWL  #SS$_NORMAL, R0
        RET

DISMOUNT_CHECK_END:                             ; label for $LKWSET

        .PAGE
        .SBTTL  CHECK_ACP_DONE, Delete ACP if appropriate

;++
; CHECK_ACP_DONE, Delete ACP if appropriate
;
; Abstract:
;
;       This routine is called when the main loop finds no more work for the
;       ACP.  If there are no more volumes associated with the ACP, the 
;       routine deletes the ACP data structures and the ACP itself (and so
;       never returns).
;
; Implicit inputs:
;
;       AQB_ADDR - address of AQB
;--

        .ENTRY  CHECK_ACP_DONE, ^M<R6>

        MOVL    AQB_ADDR, R6                    ; get AQB address

        TSTB    AQB$B_MNTCNT(R6)                ; check ACP's volume count
        BEQL    10$                             ;  br if no more volumes
        RET                                     ; else return to caller

10$:    CALLS   #0, LOCK_IODB                   ; lock the I/O data base

        PUSHL   R6                              ; remove the AQB
        CALLS   #1, UNLINK_AQB                  ;  from the system's list

        MOVL    R6, R0                          ; set up for deallocate
        JSB     G^EXE$DEANONPAGED               ;  and do it

        CALLS   #0, UNLOCK_IODB                 ; unlock I/O data base

        $DELPRC_S                               ; and kill ourselves off

        .END    ACP_MAIN

