;       SCCSID = @(#)pathcach.asm	6.2 91/05/08
;      SCCSID = @(#)pathcach.asm	6.2 91/05/08
;********************************************************************
;
;	FAT File System Path Cacher
;
;********************************************************************

	PAGE	,132
TITLE	PathCache - Caching for directory paths.
NAME	PathCache
	.286p

;*	Revision History
;
;	12/3/87 pjc	Fixed problem with cd_handle.  Under certain
;			circumstances, OS/2 was performing a CD operation
;			and the destination was in the cache.  The code
;			performing the CD called PCacheFInd on the dest,
;			and PCacheFInd over-wrote ptda_pCDS.  The CD code
;			then added the new CDS record, and decremented the
;			ref count on the old record.  However, the handle
;			used to decrement was taken from ptda_pCDS, and
;			had been overwritten by the cached entry.  This
;			new handle was zero, so had no ill effect, except
;			now there was a "ghost" CDS record.  This prevented
;			that directory from being manipulated, for example
;			rmdir on it failed.  The solution was to preserve
;			the handle of the caller's CDS and stuff it into
;			the "fake" on put out by the cache.  Ugly but true.

;***	PathCache - Caching for directory paths.
;
;
;	Purpose:
;
;	Path caching tries to provide faster access to often-used
;	subdirectories, by maintaining a cache of info relating to the
;	most recently used subdirectories.  This info will be
;	substantially the same as that maintained for a "current
;	directory".
;
;	    PC_Size	 Cache Size (number of entries)
;	    PC_MRUptr	 Pointer to head of linked list
;	    PC_DataPtr	 Pointer to start of allocated memory block
;	    PC_RMPinfo	 Information required to maintain an RMP segment
;			  (treated as a "handle" into RMP by us)
;
;	NOTE:  The linked list itself is actually kept in the RMP
;	segment. The space for this list is allocated by calling
;	RMP_Add from inside PCacheInit.  We maintain a pointer to
;	the start of the returned data area in PC_DataPtr, and a
;	pointer into the area in PC_MRUptr;  both reside in local
;	data, and are considered "read-only" after init time.  We
;	can keep these pointers because (a) the RMP segment is
;	addressable via the GDT, and (b) since the linked list is
;	the first item in the RMP, we can be sure it will not move
;	within the RMP.  RMP has a habit of re-arranging its
;	internal space to do compaction when items are deleted, but
;	this is not a problem since we are the first entry.
;
;	WARNING:  This could be a bad assumption since it "knows"
;	about the internal behaviour of RMP.  Also, with caching
;	outside DOS, is it still valid to keep this pointer
;	around?
;
;
;
;	Data Structure:
;
;	The cache will be maintained as a singly-linked list in MRU order.
;	Assuming that the pattern of access is MRU-skewed, this list will
;	give a shorter search time.  If this assumption is incorrect, another
;	structure may be more appropriate.
;
;	    Linked-list entry structure:
;
;		PCE_Length:BYTE	  \
;		PCE_Device:BYTE	  -> Often accessed as a single "hash value"
;		PCE_Next:WORD
;
;
;	The linked list contains three items, two of which comprise "hash
;	value", and a third which is the pointer to next.  The hash value in
;	this implementation is the word value which contains the length of
;	the path and the device ID, and is used as a first-order comparison
;	to reduce the number of MRP_Find calls and string compares needed.
;	Also by having the string length handy we can use a reverse compare.
;
;	NOTE:  In this document, the term "hash value" is used to mean any
;	value which allows a fast first-order comparison of two large items.
;	Originally a more typical hashing algorithm was to be used, but the
;	cost for even the cheapest was higher than desired, so the current
;	scheme of using drive and path length evolved.  The generic term
;	"hash value" persists for this historical reasons.  Also because a
;	future decision may change the contents of the first-order
;	comparison, and "hash value" is a more generic term.
;
;	The bulk of the information about each entry is kept in an RMP
;	segment.  RMP is a DOS internal system for managing records.  The
;	data kept is virtually identical to the info kept on the current
;	directory, and is stored using the Curdir STRUC as definied in
;	CURDIR.INC
;
;	NOTE:  RMP requires a unique value to retrieve a data item, and for
;	this we are using the 16-bit value of the pointer into the MRU linked
;	list.  As long as we are maintaining this cache in it's own RMP
;	segment these addresses will be unique handles.  If another method of
;	handle allocation must be used, or if we are in a shared RMP segment
;	and may be stepping on someone else's "address space" for handles,
;	then the handle is not longer implicit and must be kept in the
;	linked-list structure.
;
;	A quick moment to discuss an alternate form for this list, which is
;	to keep one array of words (which is the hash value) and scan it
;	using a REPNE SCASW instruction.  Two disadvantages:  (1) we would
;	also need a parallel array of "handles" for RMP access, or some type
;	of data pointer if RMP is not used.  (2) The cost of keeping the
;	arrays in true MRU order could be high enough to offset the cheap
;	search technique.  Also, the cost of looking in MRP will be higher
;	than the list-traversal technique, so minimizing the number of calls
;	to RMP_Find is important.  So either the search should be kept in MRU
;	order (whether array or linked list), OR a very good hashing
;	technique is needed to avoid false forays into RMP.  Once again, note
;	the assumption of MRU-skewed behavior.
;
;	PCacheSize - A word in local data which will contain the number
;		    of entries allowed in the cache; set by PCacheInit.
;
;
;	Initialization:
;
;	The init routine is activated during init time.  It calls GDTAlloc
;	to obtain a descriptor for use by RMP.  It then allocates the
;	linked-list area as the first RMP record.
;
;	Profiling Information:
;
;	    When assembled with the symbol PROFILE_CACHE defined, extra code
;	    is included to profile the success of the PCacheFind call.
;	    Extra data space is allocated during initialization.  This
;	    space is in the same RMP block as the linked list, and
;	    is used to count the number of cache misses, and the number
;	    of cache hits at each possible search length.  Search length
;	    is defined as the number of first-order comparisons made to
;	    find the entry, and so ranges from 1..N.  The first array
;	    entry [0] is used to count misses.  The array begins at the
;	    end of the space allocated to the linked list.
;
;	Call Hierarchy:
;
;	PCacheFind
;		PCacheCompare
;
;	PCacheAdd			(Add entry to cache)
;		PCacheCompare
;		Get_Rmp_Rec
;		Rmp_Rem
;
;	PCachePurge			(Purge entire cache)
;		Get_Rmp_Rec
;		Rmp_Rem
;
;	PCacheVPurge			(Purge entries for one volume)
;		Get_Rmp_Rec
;		Rmp_Rem
;
;	PCacheCompare			(Compare pathname with cache entry)
;		Get_Rmp_Rec
;
;	Get_Rmp_Rec			(Get cache record from RMP)
;		Rmp_Find
;
;	_PCacheInit			(Init, called from C)
;		GDTAlloc
;		Rmp_Add
;
;
;	History
;
;	86Dec08 bryanwi Created file, with skeleton form.
;	86Dec09 paulc	Filled in skeleton, except PCachePurge
;	87Feb24 paulc	Updated comments, etc. for new pathcache approach

include dosseg.inc
.xlist
include defdbug.inc
include basemaca.inc
include osmaca.inc
include oldmaca.inc
include curdir.inc
include mi.inc
.list


	ReferTask	WFP_Start,WORD		; ptr -> target path string
	ReferTask	WFP_Path_End,WORD	; ptr -> end of dir comp.
	ReferTask	Curr_Dir_End,WORD
	ReferTask	NoSetDir,BYTE		; see PCacheAdd entry values
;### PTR B720732        ReferTask	ThisCDS,DWORD


	EXTRNFAR	GDTAlloc
	EXTRNFAR	Rmp_Rem
	EXTRNFAR	Rmp_Add
	EXTRNFAR	Rmp_Find

PCacheEntry	STRUC
PC_length	DB	?		;\
PC_drive_id	DB	?		;/ -- referenced as word PC_hashval
PC_next		DW	?
PCacheEntry	ENDS

PC_hashval	EQU	WORD PTR PC_length	; word alias for above

PCE_Size EQU	SIZE PCacheEntry
CD_Size EQU	SIZE curdir
Entry_Size EQU	PCE_Size + CD_Size

;	Handy macro to multiply something by the size of the
;	PCacheEntry structure.

if	PCE_Size ne 4
	.ERR	Following operations (rrr*4) not correct
endif

Mul_PCE_Size	macro	rrr
	shl	rrr,2
	endm


ENDPTR	EQU	0	; Must be changed if 0 is a valid offset into the
			; area allocated by Rmp.  Maybe -1?  Zero should
			; be OK, since the linked-list occupies the first
			; record position in the RMP segment.

DATA segment

PCache_RmpSeg	DD	0		; Rmp's special info (???)
PCache_RmpMaxSz DW	0		; Maximum amount of memory for RMP data
PCache_RmpAddr	EQU THIS DWORD		; Pointer to Rmp's info (???)
		DW	OFFSET HIGHDATA:PCache_RmpSeg
		DW	SEG HIGHDATA

PCacheSize	DW	0		; Max number of entries in cache
PCacheBeg	DW	?		; Offset of linked list area

	PUBLIC PCacheFirst
PCacheFirst	EQU THIS DWORD		; Make DWORD reference to next two
PCacheMRU	DW	?		; Offset of MRU	 Entry in linked list
PCacheSeg	DW	?		; Seg addr of linked list data

DATA ends


	  FARCODE HIGH2
HIGH2CODE SEGMENT


;***	PCacheFind - Find an entry in the Path Cache
;
;	<English Description Here>
;
;		     +---+
;	   (Begin)   | 0 |
;		     +---+
;		       |
;		       V
;		     +---+
;		 +-->| 1 |  possible match
;		 |   +---+ -----------------> +---+
;		 |     |		      | 3 |  Test for match
;	    next |     | definite miss	 +--- +---+
;		 |     V		 |	|
;		 |   +---+		 |	|
;		 +-- | 2 | <-------------+	| hit!
;		     +---+     near miss	|
;		       |			| found the entry
;		       | past last entry	|
;		       | in list		|
;		       V			|
;		     +---+			|
;		     | 6 | (Return		|
;		     +---+  NOT FOUND)		V
;		       |		    +-----+
;		       +-------------+	+---| 4,5 | (Copy Data Out, Make MRU)
;				     |	|   +-----+ (Return FOUND)
;				     V	V
;				     (end)
;
;
;	ENTRY	SS:WFP_Start	-> Path of interest (being opened, etc.)
;		SS:WFP_Path_End -> First byte after last path separator in
;				   target string.  WFP_Path_End - WFP_START =
;				   length of directory part of path.
;		SS:ThisCDS	-> Dummy CDS - Drive information
;		SS		=  TASKAREA
;
;	EXIT	if no cache hit
;		    carry clear
;		else
;		    carry set
;		    SS:ThisCDS	-> "Fake" CDS to use
;		    SS:ThisCDS->cd_handle = handle of original (caller's) CDS
;		    Curr_Dir_End = WFP_Path_End
;
;	USES	Flags only
;
;
;	WARNING ** The CDS that ThisCDS points to must be a *long*	**
;		** CDS, capable of holding a 128 byte path string, not **
;		** a short CDS, limited to a 64 byte path string.	**


Procedure PCacheFind,NEAR
	ASSUME	DS:Nothing,ES:Nothing,SS:TASKAREA

;;	int	3

	push	ds
	DosContext ds
	cmp	PCacheSize,0
	jne	pcf0			; Cache installed, go search it
	pop	ds
	clc				; carry clear means "not found"
	ret

pcf0:	push	es
	pusha

	mov	cx,[WFP_Path_End]
	mov	bp,[WFP_Start]
	sub	cx,bp			; CX (CL anyway) = path length
	dec	cx			; ignore terminating char
	mov	ch,ss:[bp]		; CH = one-byte drive ID
	les	di,PCacheFirst		; es:di = "current" = head MRU chain
	mov	si,ENDPTR		; es:si = "previous" = NULL
	ifdef PROFILE_CACHE		    ;  *** Profiling code begin ***
	mov	dx,1			; DX=1 Init search length counter
	endif				;  **** Profiling code end ****

;	During this loop (pcf1), we have:
;
;		   ES:DI -> "Current" PCE entry
;		   ES:SI -> "Previous" PCE entry
;		   CL	 =  Length of caller's path
;		   CH	 =  Caller's one-byte drive ID
;	 (profile) DX	 =  Length of search
;
;	Scan the list in MRU order.  If an item passes the first-order
;	comparison (length matches), then put it to the true test
;	(pcf3).  We may come back to the loop at pcf2 if the
;	entry fails to match.
;
;	NOTE:  Due to the fact that the loop may be re-entered at
;	pcf2, we do not assume the value of any regs except
;	those listed above.  That means DS is also unknown.
;
;	When profiling is enabled by the conditional assembly flag,
;	DX will be used to count the number of compares made during
;	the search.


    Assume	DS:Nothing,ES:Nothing

pcf1:	cmp	di,ENDPTR
	LJE	pcf6			; Exit loop if past end of chain
	cmp	cx,es:[di].PC_hashval
	je	pcf3			; match, go test path text
pcf2:	mov	si,di			; SI (PrevPtr) = current
	mov	di,es:[di].PC_next	; DI (Current) = current.next
ifdef PROFILE_CACHE			    ;  * Profiling code begin *
	inc	dx			; DX++ (Search length counter)
endif					;  * Profiling code end *
	jmp	pcf1

;	Test for equality of records by comparing the device pointer
;	and the path names.  Note that the path name is in WFP_STart,
;	not ThisCDS.
;
;	At this point we assume the usual stuff, some of which
;	is useful here and all of which must be preserved:
;
;		ES:DI -> Current PCE
;		ES:SI -> Previous PCE
;		CL    =	 Length of caller's path
;		CH    =	 Caller's one-byte drive ID
;

pcf3:	ifdef PROFILE_CACHE		    ; *** Profiling code begin ***
	SaveReg <dx>			; Save search length counter
endif					; **** Profiling code end ****
	SaveReg <es,si,di>
	call	PCacheCompare		; ES:BX -> cache CDS, if match
    Assume	DS:Nothing,ES:Nothing
	je	pcf4			; Match, copy out data
	RestoreReg <di,si,es>
    Assume	ES:Nothing
ifdef PROFILE_CACHE			    ; *** Profiling code begin ***
	RestoreReg <dx>			; DX = search length counter
endif					; **** Profiling code end ****
	jmp	pcf2


;	We found the entry;  now copy data out to ThisCDS.  At this
;	point we assume:
;
;		ES:BX	  -> Cache CDS
;		[ThisCDS] -> Destination of our copy
;		CL	  =  Length of text portion

pcf4:	xor	ch,ch			; CX=length; no longer need drive ID
	mov	si,bx			; ES:SI -> cache CDS
	mov	ax,es
	mov	ds,ax			; DS:SI -> cache CDS
    Assume	DS:Nothing
	mov     di,TASKAREA:CurrTCB	        ; (DI) = current TCB     ;### PTR B720732
	les	di,TASKAREA:[di].TCBThisCDS     ; (ES:DI) = CDS pointer  ;### PTR B720732
;### PTR B720732 les	di,[ThisCDS]		; ES:DI -> caller's CDS
	mov	ax,es:[di.cd_handle]	; AX = handle of caller's real CDS
    Assume	ES:Nothing
	add	cx,cd_fsi.cdi_text	; CX = total size of data
					;  (text length + offset of text base)
	rep	movsb			; ThisCDS = cache CDS
	mov     di,TASKAREA:CurrTCB	        ; (DI) = current TCB     ;### PTR B720732
	mov	di, word ptr [di.TCBThisCDS]                    ;### PTR B720732
;### PTR B720732 mov	di, word ptr [ThisCDS]
	mov	es:[di.cd_handle],ax	; Set ThisCDS's handle ot handle of
					; caller's CDS

;	Another variable that needs to be set is CURR_DIR_END, which
;	is supposed to point to the byte after the "current directory"
;	part of the string.  Coincidentally, we have that already,
;	in WFP_Path_End.

	mov	si,[WFP_Path_End]
	mov	[Curr_Dir_End],si

;	Restore the linked-list data saved at pcf3

	RestoreReg <di,si,es>
    Assume	ES:Nothing
ifdef PROFILE_CACHE			    ; *** Profiling code begin ***
	RestoreReg <dx>			; DX = search length counter
endif					; **** Profiling code end ****

;	Put this entry in the MRU position.
;
;	Assumptions at this point:
;		ES:DI -> the "current" entry in chain
;		ES:SI -> the "previous" entry, unless "current" is first in
;			 chain, in which case SI == ENDPTR.
;
;	  (Profiling only)
;		DX -> Length of search

	DosContext ds
	cmp	si,ENDPTR
	je	pcf5			; Already MRU, we're done.
	mov	ax,es:[di].PC_next	; \
	mov	es:[si].PC_next,ax	; -> prev.next = curr.next
	mov	ax,PCacheMRU		; \
	mov	es:[di].PC_next,ax	; -> curr.next = head
	mov	PCacheMRU,di		; -- head = curr
pcf5:

;	Profiling code at this point increments an element in the
;	array of profiling data.  The offset of this element is
;	the number of elements traversed to find the correct one.
;	(Actually offset is 2*n, it's an array of words).
;
;	Note:  profiling code uses AX,DX,BX,SI, none of which
;	are valuable at this point in the current implementation.

ifdef PROFILE_CACHE			    ; * Profiling code begin *
	mov	ax,PCacheSize		; *
	Mul_PCE_Size ax			; *
	shl	dx,1			; *
	add	dx,ax			; *
	mov	bx,dx			; *
	mov	si,PCacheBeg		; *
	inc	word ptr es:[bx][si]	; *
endif					; * Profiling code end *

	DBPRT	INIGRP,RDPLVL,<Entry found\n>,<>

	popa
	pop	es
	pop	ds

	stc				; Set "found" return code
	ret


;	MISS!  We have not found the entry.
;
;	Note that for profiling we assume that ES still points to the
;	segment containing the local tables.

    Assume	DS:Nothing,ES:Nothing

;	Profiling code increments the first element in the profiling
;	data array.  The first element (offset 0) is the counter
;	for cache misses.
;
;	Note:  profiling code uses DS,BX,SI, none of which
;	are valuable at this point in the current implementation.

pcf6:
	DBPRT	INIGRP,RDPLVL,<Entry NOT found\n>,<>

ifdef PROFILE_CACHE			    ; * Profiling code begin
	DosContext ds
     assume	ds:nothing		; *
	mov	bx,DS:PCacheSize	; *
	Mul_PCE_Size bx			; *
	mov	si,DS:PCacheBeg		; *
	inc	word ptr es:[bx][si]	; *
endif					; * Profiling code end

	popa
	pop	es
	pop	ds

	clc
	ret

EndProc	  PCacheFind,NoCheck


;***	PCacheAdd - Add an entry to the Path Cache
;
;		(Begin)
;		   |
;		   V
;		 +---+
;	     +-->| 1 |	possible match
;	     |	 +---+ -----------------> +---+
;	     |	   |			  | 3 |
;	next |	   | definite miss   +--- +---+
;	     |	   V		     |	    |
;	     |	 +---+		     |	    |
;	     +-- | 2 | <-------------+	    | hit!
;		 +---+	  near miss	    |
;		   |			    | already in cache
;		   | last entry		    |
;		   | in list		    |
;		   V			    |
;		+-----+			    |
;		| 4,5 | (Throw out LRU,	    |
;		+-----+	 copy in new data)  V
;		   |			  +---+
;		   +--------------------> | 6 | (Make MRU)
;					  +---+
;
;	The cache entry built will look like the following:
;
;		    cd_handle	 = special value used for RMP lookup
;		    cd_refcnt	 = 0
;		    cd_flags	 = CD_INUSE
;		    cd_devptr	 = ThisCDS -> cd_devptr
;		    cd_ID	 = DX - cluster user passes in
;		    cd_ID+2	 = 0
;		    cd_user_word = 0
;		    cd_end	 \ Filled in with text delimited by
;		    cd_text	 / WFP_Start and WFP_Path_End.
;
;
;	ENTRY	DX		=  Starting cluster of dir path to cache.
;		SS		=  TASKAREA
;		SS:ThisCDS	-> Dummy CDS - Drive information
;		SS:WFP_Start	-> Path to cache
;		SS:SP+2		=  Flag word from caller (see below).
;		SS:NoSetDir	=  (see below).
;		SS:WFP_Path_End -> First byte after last path separator in
;				   target string.  WFP_Path_End - WFP_START =
;				   length of directory part of path.
;
;		Current stack as follows:
;
;			   SP+0 return to GetPath
;			   SP+2 Flags
;
;		The flags are used in combination with NoSetDir to
;		determine what part of the string as WFP_Start to
;		use.  Normally we use up to WFP_Path_End.  But, if
;
;		    Not Carry (and) Zero (and) NoSetDir==0
;
;		... we use from WFP_Start to the nul byte which
;		is at or beyond WFP_Path_End.
;
;	EXIT	SS:WFP_Path_End may be modified.
;
;	WARNING	 ThisCDS -> cd_flags *must* == CD_INUSE in the constructed
;		 cache entry.
;
;	USES	Flags only


Procedure PCacheAdd,NEAR
	ASSUME	DS:Nothing,ES:Nothing,SS:TASKAREA

;;	int	3

;	Check for no cache installed (Size == 0)

	push	bp
	mov	bp,sp
	push	ds
	DosContext ds
	cmp	PCacheSize,0
	jne	pca0
	pop	ds
	pop	bp
	ret

pca0:	push	es
	pusha

	mov	cx,WFP_Path_End
	mov	ax,ss:[bp+04h]		; AX = caller's flag word
	push	ax
	popf				; Flags = callers flags
	jc	pca3			; Carry set, skip adjustment
	jnz	pca3			; No ZR, skip adjustment
	cmp	BYTE PTR TASKAREA:[NoSetDir],0
	jnz	pca3			; No, skip adjustment

;	Adjust CX by finding end of string.  At this point:
;		SS:CX	->  Current WFP_Path_End

	mov	bx,cx
pca1:	cmp	byte ptr ss:[bx],0	; Is byte zero (end of string)?
	jz	pca2			; Yes, adjustment done
	inc	bx
	jmp	pca1
					; General-purpose registers?  Hah!
pca2:	mov	cx,bx			; Adjustment back into CX
	inc	cx			; undo the dec that follows

;	Now, CX has offset of the end of the string of interest.

pca3:	mov	bp,WFP_Start
	sub	cx,bp			; CX = path length
	dec	cx			; ignore terminating char
	mov	ch,BYTE PTR ss:[bp]	; Get caller's one-byte drive ID
	mov	si,ENDPTR
	les	di,PCacheFirst
    Assume	ES:Nothing


;	During this loop (pca4), we will have assume the following:
;
;		ES:DI -> "Current" PCE entry
;		ES:SI -> "Previous" PCE entry
;		CL    =	 Length of caller's path        \__ "hash value"
;		CH    =	 Caller's one-byte drive ID     /
;		DX    =	 Caller's cluster number
;
;	Scan the list in MRU order.  If an item passes the first-order
;	comparison (length matches), then put it to the true test
;	(pca5).  We may come back to the loop at pca4a if the
;	entry fails to match.
;
;	WARNING:  Due to the fact that the loop may be re-entered
;	at pca5, we do not assume the value of any regs except
;	those listed above.  That means DS is also unknown.
;
;	NOTE:  Most of the code between here and pca8 will be used only when
;	our first-order comparison indicates a possible match and we must
;	check to see if we are adding an entry that's already in the cache!
;	If this possibility can be completely avoided, we can forget all
;	this nonsense and have a fast tight loop to get to the end of
;	the MRU chain.

    Assume	DS:Nothing		; See first NOTE above

pca4:	cmp	cx,es:[di].PC_hashval	; Same hash value?
	je	pca6			; If so, take a closer look.
pca5:	mov	bx,es:[di].PC_next
	cmp	bx,ENDPTR
	je	pca7
	mov	si,di			; Prev = current
	mov	di,bx			; Current = current.next
	jmp	pca4


;	Test for equality of records by comparing the device pointer
;	and the path names.  Note that the path name is in WFP_STart,
;	not ThisCDS.
;
;	At this point we assume the usual stuff, some of which
;	is useful here and all of which must be preserved:
;
;		ES:DI -> Current PCE
;		ES:SI -> Previous PCE
;		CL    =	 Length of caller's path        \__ "hash value"
;		CH    =	 Caller's one-byte drive ID     /
;		DX    =	 Caller's cluster number

pca6:	SaveReg <es,si,di,dx>
	call	PCacheCompare
    Assume	DS:Nothing,ES:Nothing
	RestoreReg <dx,di,si,es>
    Assume	ES:Nothing
	jne	pca5			; Not a match, re-enter the loop
	jmp	short pca9		; Match!  Skip data copy, just move
					; this entry to MRU

;	We have found the last entry -- our victim!  Kill him and reuse him.
;	 (1) remove current Rmp for this (last) entry (if occupied already)
;	 (2) get new Rmp
;	 (3) save data in it
;
;	At this point we assume:
;
;		ES:DI -> Current PCE to be re-used
;		ES:SI -> Previous PCE (used when re-ordering chain)
;		CL    =	 Length of caller's path
;		CH    =	 Caller's one-byte drive ID
;		DX    =	 Caller's cluster number

    Assume	DS:Nothing,ES:Nothing

pca7:	SaveReg <es,si,di>
	cmp	es:[di].PC_length,0
	je	pca8			; Skip find/remove if no entry exists
	SaveReg <di,cx,dx>
	call	Get_Rmp_Rec		; ES:DI -> RMP data block
    Assume	DS:Nothing,ES:Nothing
	DosContext ds
	lds	bx,PCache_RmpAddr	; DS:BX -> RMP Segment info
	CALLFAR Rmp_Rem			; Removes ES:DI block
    Assume	DS:Nothing,ES:Nothing
	RestoreReg <dx,cx,di>

;	Now we want to make the new Rmp entry, having gotten rid of
;	the old one (if necessary).  We will use ThisCDS as a template,
;	and fill in the other data after calling Rmp_Add.  Note that by
;	using ThisCDS as a template, we already have the proper value
;	in cd_devptr upon return from Rmp_Add.
;
;	At this point ...
;
;		CH =  drive ID to be added
;		CL =  length of text portion of new record
;			(used to compute size for Rmp_Add call)
;		DX =  Caller's cluster number
;		DI =  offset portion of current PCE pointer
;			(used as the CD handle value)

pca8:	SaveReg <cx,dx,di>
	xor	ch,ch			; CX = text length
	add	cx,cd_fsi.cdi_text	; CX = total data length
	DosContext ds
	les	di,PCache_RmpAddr	; ES:DI -> RMP segment info
	mov     si,TASKAREA:CurrTCB	        ; (SI) = current TCB     ;### PTR B720732
	lds	si,TASKAREA:[si].TCBThisCDS     ; (DS:SI) = CDS pointer  ;### PTR B720732
;### PTR B720732 lds	si,ThisCDS		; DS:SI -> CDS (data template)
	CALLFAR Rmp_Add			; ES:DI -> new data area
    Assume	DS:Nothing,ES:Nothing
	jc	pca11			; add failed, go clean stack and return
	RestoreReg <ax>			; AX = handle value
	mov	es:[di].cd_handle,ax	; CDS.handle = AX (handle value)
	RestoreReg <ax>			; AX = cluster value
	mov	es:[di].cd_fsd.cddFAT_id,ax ; CacheCDS.cd_ID = cluster
	mov	es:[di].cd_refcnt,0
	mov	es:[di].cd_flags,CD_INUSE
	RestoreReg <cx>			; CX = hash value
	mov	ax,cx			; AX = hash value
	xor	ch,ch			; CX = text length
	mov	bx,di			; ES:BX -> cache CDS
	mov	dx,ss			; \
	mov	ds,dx			;  > DS:SI -> caller's text
	mov	si,[WFP_Start]		; /
	add	di,cd_fsi.cdi_text	; ES:DI -> cache CDS text area
	rep	movsb			; Copy text
	mov	es:[bx].cd_fsi.cdi_end,2 ; Set cd_end (see macro.asm)
	RestoreReg <di,si,es>		; ES:DI -> current, ES:SI -> prev
	mov	es:[di].PC_hashval,ax	; Current.Hashval = new hash value

;	At this point we have either (a) found the entry already in the
;	list, or (b) replaced the LRU entry with the new stuff.  Now
;	in either case it's time to put this new entry into the MRU
;	position.
;
;	Assumptions at this point:
;
;		ES:DI -> the "current" entry in chain
;		ES:SI -> the "previous" entry.
;			       ( Note that if "current" is first
;				 in the chain, SI == ENDPTR. )

pca9:	DosContext ds

	cmp	si,ENDPTR		; Already MRU?
	je	pca10			; If so, we're done.
	mov	ax,es:[di].PC_next	; \
	mov	es:[si].PC_next,ax	; -> prev.next = curr.next
	mov	ax,PCacheMRU		; \
	mov	es:[di].PC_next,ax	; -> curr.next = head
	mov	PCacheMRU,di		; -- head = curr

pca10:	popa
	pop	es
	pop	ds
	pop	bp

	ret

pca11:	add	sp,6			; clean stack
	jmp short pca10			; go return

EndProc	  PCacheAdd


;***	PCachePurge  - Purge *all* entries from the Path Cache
;***	PCacheVPurge - Purge only entries from a given volume (drive ID)
;
;	Purging Algorithm:
;
;	   Scan the list in MRU order.  If an item has data, signified
;	   by a length value not equal to zero, AND the item belongs to
;	   the target volume, find and delete the associated data in the
;	   RMP segment.  Set the length value to zero.
;
;	   An item blongs to the target volume if (a) the target is all,
;	   signified by AH==0, or (b) the target in AH is equal to the
;	   drive ID element in the item.
;
;
;	ENTRY	PCachePurge:   Nothing
;		PCacheVPurge:  AH = drive letter
;
;	EXIT	Nothing
;
;	USES	Flags only


Procedure PCachePurge,NEAR
	ASSUME	DS:Nothing,ES:Nothing,SS:TASKAREA

;;	int	3

	pusha
	xor	ah,ah			; Set ah=0, means purge all volumes
	jmp	pcp5

Entry	PCacheVPurge

;;	int	3

	pusha
pcp5:	push	ds
	push	es

	DosContext ds
	cmp	PCacheSize,0
	jz	pcp40			; Return if no cache to purge

;	Note that the loop is in a do-while format that guarantees
;	at least one iteration;	 we already tested for a non-existent
;	cache above, and so shall take it on faith that there is at
;	least one entry in the chain.

    Assume	DS:Nothing,ES:Nothing

	les	di,DS:PCacheFirst	; es:di = "current" = head MRU chain

pcp10:	cmp	es:[di].PC_length,0
	je	pcp30			; No data for entry; get next
	or	ah,ah
	jz	pcp20			; AH==0, all volumes to be purged
	cmp	ah,es:[di].PC_drive_id
	jne	pcp30			; Not correct volume, do not purge
pcp20:
	SaveReg <es,di,ax>
	call	Get_Rmp_Rec		; ES:DI -> RMP data block
	DosContext ds
	lds	bx,PCache_RmpAddr	; DS:BX -> RMP Segment info
    ASSUME DS:NOTHING
	CALLFAR Rmp_Rem			; Removes ES:DI block
	RestoreReg <ax,di,es>		; ES:DI -> Current (again)
	mov	es:[di].PC_length,0
pcp30:	mov	di,es:[di].PC_next	; DI (Current) = current.next
	cmp	di,ENDPTR
	jne	pcp10			; Not at end of chain, continue loop

pcp40:	pop	es
	pop	ds
	popa

	ret

EndProc	  PCachePurge


;***	Get_Rmp_Rec - Get a record from RMP
;
;	This local procedure does all the dirty work to fetch a record
;	from the RMP segment.  It accepts the two-byte handle value in
;	a register (DI), puts it into memory (on the stack) and
;	build a pointer to it.  It returns generally the same info as
;	Rmp_Find, except it restores CX and pops garbage into AX.  Note
;	that a POP is used rather than (ADD SP,2) to preserve the flags
;	set by Rmp_Find.
;
;	NOTE:  Since we should always have the requested entry in RMP,
;	there should never be an error return, but note that the carry
;	flag does indicate error should RMP not be able to find our
;	data.
;
;
;	ENTRY	DI = two-byte "pattern" or "handle" to give Rmp_Find
;
;	EXIT	Carry Set - Error		  \
;		Carry Clear - OK		   > Actually set by Rmp_Find
;		   ES:DI ->  Data record from RMP /
;
;	USES	AX,BX,CX,DX,BP,DI,SI,ES,DS,Flags


Procedure Get_Rmp_Rec,near,local
    Assume	DS:Nothing,ES:Nothing
	DosContext ds
	push	di		    ; Put handle value on frame
	les	di,PCache_RmpAddr   ; Load pointer to Rmp segment handle
	mov	ax,ss		    ; \
	mov	ds,ax		    ;  > Build pointer to handle value
	mov	si,sp		    ; /
	mov	cx,2		    ; pattern (handle) size is 2 bytes
	xor	bx,bx		    ; offset within structure is zero
	CALLFAR Rmp_Find
    Assume	DS:Nothing,ES:Nothing
	pop	ax		    ; Can't "pop di" -- di has return value
	ret
EndProc Get_Rmp_Rec


;***	PCacheCompare - Compare data with a cache entry
;
;	This local procedure handles the comparison of the CDS-type
;	entry in the cache with the information passed in by our
;	"client".
;
;	First the device information is compared, then the text
;	is compared using a backwards cmpsb. The assumtion is
;	that we will know sooner when strings are different by
;	comparing them backwards, and also that we
;	will have sufficient occasion to find strings that do not
;	match.  If we always match, it's faster to avoid the setup
;	time for a backwards search, despite any improvement gain
;	when we miss.
;
;	However, given that our first-order tests tell us only that the
;	strings are the same length and on the same device, we will
;	probably have "collisions" unless the cache has a small
;	number of entries.
;
;	Hmmm... supposedly our MRU list will get us a high hit rate.
;
;
;	Error Handling:
;
;	    If RMP does not find the record corresponding to the PCE,
;	    PCacheCompare returns "not found".  What else can he do?
;
;
;	ENTRY	ES:DI	    -> current PCE entry
;		[ThisCDS]   -> client's CDS
;		[WFP_Start] -> client's path text
;		CL	    =  length of path text for both entries
;
;	EXIT	If items are the same:
;		    ZF=1
;		    ES:BX -> Matching cache entry's CDS struct
;		Else
;		    ZF=0
;
;	USES	AX,BX,DX,BP,DI,SI,ES,DS,Flags


Procedure PCacheCompare,near,local
    Assume	DS:Nothing,ES:Nothing

	SaveReg <cx>
	call	Get_Rmp_Rec		; ES:DI -> cache's CDS entry
	RestoreReg <cx>
	jc	pcc1			; Error, return "records not same"

	mov	ax,ss			;
	mov	ds,ax			;
    Assume	DS:TASKAREA		; DS:SI -> caller's path string
	mov	si,[WFP_Start]		;

;	At this point ...
;
;		DS:SI ->  path string from caller
;		ES:DI ->  CDS from the cache (via Rmp)
;		CL    =	 Length of caller's path        \__ "hash value"
;		CH    =	 Caller's one-byte drive ID     /

	mov	ax,cx			; AX = hash value
	mov	bx,di			; BX = offset of cache CDS
	add	di,cd_fsi.cdi_text	; Point to text in CDS
	xor	ch,ch			; CX = just length
	dec	cx			; Make (Len = Len-1) so we can ...
	add	si,cx			; .. point to the last byte of ...
	add	di,cx			; .. text in each string
	mov	cl,al			; Restore length for loop counter
	std				; \
	repe	cmpsb			; Test all bytes, backwards
	cld				; /
	mov	cx,ax			; CX = hash value

	ret				; Return w/ ZF as left by cmpsb

;	Error getting record from RMP, return "not found".  There has to
;	be a better way to clear the zero flag.

pcc1:	xor	ax,ax			; AX = 0
	inc	ax			; AX = 1, ZF=0
	ret

EndProc PCacheCompare

HIGH2CODE ENDS


	FARCODE INIT
INITCODE segment

;***	PCacheInit - Initialize Path Cache data structures
;
;	PCacheInit(NumEntries)
;	{
;	    NumEntries =
;	    PCacheSize = NumEntries
;	    if (PCacheSize != 0)
;	    {
;		block = Malloc(NumEntries * Element Size)
;		PCacheSeg = segment of allocated memory block
;		PCacheMRU = offset of first element in array
;
;		for (each entry except last)
;		   entry[i].length = 0;
;		   entry[i].next = 16-bit address of entry[i+1]
;
;		entry[last].length = 0;
;		entry[last].next = ENDPTR;
;	    }
;	}
;
;
;	Constraints:
;
;	    Maximum number of entries is 63, or 42 with profiling.
;
;	    Since the memory for the linked list and (optional) profile
;	    information is kept in a RMP data chunk, this area is limited
;	    to 255 bytes.  Given the current structure size of 4 bytes,
;	    that is enough room for (255/4) = 63 entries.  With profiling
;	    enabled, an extra 2 bytes per entry is used plus a fixed
;	    2 bytes (2 + (2*n)), so that is (2 + (6*n)) total.  Let's
;	    solve that for N, kids ... 42!  But what was the question?
;
;	Error Handling:
;
;	    If the initialization fails for some reason, PCacheSize will
;	    be set to zero, so all functions will behave as if no cache
;	    is installed and everybody is happy.  Slower but happy.
;
;
;	ENTRY	CX = Maximum amount of memory cache can consume in Kbytes
;		       (0 means no cache)
;
;	EXIT	AX == 0 means OK
;		AX != 0 means error
;
;	USES	AX,BX,CX,DX,SI,DI,DS,ES,Flags
;
;
Procedure PCacheInit,near
	ASSUME	DS:NOTHING,ES:Nothing,SS:TASKAREA

	DosContext ds
;
;	Save number of entries, test for zero (no directory cache)
;	Since we test here, no other places test for NumEntries == 0

	or	cx,cx
	LJZ	pci40			; No PCache allocated, exit.

;
; perform the following calculation to get the number of entries.
;
;				  max memory
; num entries = -------------------------------------------------------------
;		4 (linked list node size) + 283 (CDS with 260-byte path size)
;
; to allow 64K, we map 65536 to 65535.	We know that the memory in bytes
; will not be above 64K, because the config.sys processing checks for it.
; (CX) = max memory in Kbytes
;
	mov	ax,1024			; ax = 1k
	mul	cx			; (DX:AX) = max memory in bytes
	or	dx,dx			; 64K?
	jz	pci0			; no, go calculate num entries
	xor	dx,dx
	dec	ax			; (AX) = 65535
;
; (AX) = max memory in bytes
;
pci0:	mov	PCache_RmpMaxSz,ax	; save max memory for RMP
	mov	cx,Entry_Size		; (CX) = memory per path
	div	cx			; (AX) = max number of entries
	mov	cx,ax			; (CX) = max number of entries
	mov	PCacheSize,cx		; store number of entries
;
; The maximum number of entries is 228 (64K of memory).
; Calculate number of bytes needed for cache table.  The number
; is (PCE_Size * NumEntries) usually.  Profiling needs an
; additional ((NumEntries+1) * 2) bytes.  The maximum amount is
; 1370 bytes (for 228 entries with profiling).	Since RMP allows a maximum
; of 1536 (despite a word size field), we're OK.
;
ifdef PROFILE_CACHE			     ; * Profile code begin *
	mov	ax,cx			; \
	inc	ax			;  > AX = bytes needed for profile
	shl	ax,1			; /	  data area
endif					; * Profile code end *

	Mul_PCE_Size cx			; CX = array size in bytes

ifdef PROFILE_CACHE			     ; * Profile code begin *
	add	cx,ax			; CX += profiling needs
endif					; * Profile code end *

;	Our call to Rmp_Add to get memory will cause Rmp to copy
;	data from wherever DS:SI currently points, effectively garbage.
;	This is fine, as long as we know enough to be sure to init
;	all of it.
;

BUGBUG vich 8/8/90: Rmp_Alloc must be called instead of GDTAlloc
BUGBUG		    Rmp_Add no longer calls SegAlloc

	CALLFAR GDTAlloc		; AX = new GDT selector value
	jc	short pci40		; No caching if GDTAlloc failed

	les	di,PCache_RmpAddr	; ES:DI -> RMP segment info
	mov	es:[di+2],ax		; Put selector into RMP seg info area
	push	ds
;
; we're in trouble if HIGHDATA is smaller than 1370 bytes.
; (CX) = length of data to add
; (DS) = HighData
; (ES:DI) -> rmp handle (zero) and selector (allocated by GDTAlloc)
;
	xor	si,si			; (DS:SI) -> data to add (garbage)
	CALLFAR Rmp_Add			; (ES:DI) -> new data area
    Assume	ES:Nothing
	pop	ds
    ASSUME  DS:DOSGROUP
	jc	pci40			; Spurned by Rmp!  Exit w/ error set.

;	Set chain-head pointers

	mov	PCacheSeg,es
	mov	PCacheMRU,di
	mov	PCacheBeg,di

;	Traverse the array, building chain and zero-ing hash lengths

	mov	cx,PCacheSize		; CX = number of entries
	dec	cx			; CX = (number of entries) - 1
	jz	pci20			; Only one entry, treat specially

pci10:	mov	dx,di			; DX = current offset
	add	dx,PCE_Size		; DX = next offset
	mov	es:[di].PC_length,0	; Current.length = 0
	mov	es:[di].PC_next,dx	; Current.next = next offset
	mov	di,dx			; DI = next
	loop	pci10

;	Last (or only) entry gets special treatment to terminate chain

pci20:	mov	es:[di].PC_length,0	; Last.length = 0
	mov	es:[di].PC_next,ENDPTR	; Last.next = ENDPTR

;	If profiling is enabled, zero out the table of counters that
;	profile will use.

ifdef PROFILE_CACHE			     ; * Profile code begin *
	mov	ax,PCacheSize		; AX = num entries
	mov	cx,ax			; CX = num entries
	inc	cx			; CX = num words of profile data
	mov	di,PCacheBeg		; ES:DI -> start of data area
	Mul_PCE_Size ax
	add	di,ax			; ES:DI -> profile data area
	xor	ax,ax
	rep	stosw			; Zero profile table
endif					; * Profile code end *

	mov	ax,1			; Set OK return code
	ret

    Assume	DS:DOSGROUP,ES:Nothing
pci40:	xor	ax,ax			; Set error return code
	mov	PCacheSize,ax		; Insure no access is made later
;;	int	3			; DEBUG ONLY
	ret

EndProc PCacheInit

INITCODE ends

	END
