DIRECTORY Ascii, Basics, BasicTime, CountedVM, FS, ImagerFrameBuffer, ImagerMaskCache, ImagerMask, ImagerPixelMap, ImagerTransformation, IO, Process, Rope, SafeStorage, Scaled, VM, ImagerMaskCacheImpl: CEDAR MONITOR IMPORTS VM, BasicTime, CountedVM, IO, Ascii, FS, ImagerTransformation, Rope, Process, SafeStorage, ImagerMask, ImagerPixelMap, Basics, ImagerFrameBuffer EXPORTS ImagerMaskCache ~ BEGIN OPEN ImagerMaskCache; WordsForPages: PROC [pages: INT] RETURNS [INT] ~ {RETURN [VM.WordsForPages[pages]]}; PagesForWords: PROC [words: INT] RETURNS [INT] ~ {RETURN [VM.PagesForWords[words]]}; currentStatus: Status _ disabled; statusChange: CONDITION; lockCount: NAT _ 0; lockOwner: UNSAFE PROCESS _ NIL; lockFree: CONDITION; GetHeader: PUBLIC PROC RETURNS [header: Header] ~ { Locked: PROC ~ TRUSTED { status: Status _ GetStatus[waitDuringTransition: TRUE]; IF status = disabled THEN header _ [0, 0, 0, 0] ELSE { hdr: LONG POINTER TO Header _ residentPointer; header _ hdr^ }; }; DoUnderLock[Locked]; }; Lock: ENTRY PROC ~ { me: UNSAFE PROCESS ~ Process.GetCurrent[]; IF lockOwner # me THEN { WHILE lockCount # 0 DO WAIT lockFree ENDLOOP; lockOwner _ me; }; lockCount _ lockCount + 1; }; UnLock: ENTRY PROC ~ { IF lockOwner # Process.GetCurrent[] THEN ERROR; lockCount _ lockCount - 1; IF lockCount = 0 THEN {lockOwner _ NIL; NOTIFY lockFree}; }; GetStatus: PUBLIC ENTRY PROC [waitDuringTransition: BOOLEAN] RETURNS [Status] ~ { WHILE waitDuringTransition AND currentStatus = transition DO WAIT statusChange; ENDLOOP; RETURN [currentStatus] }; SetStatus: PUBLIC ENTRY PROC [status: Status] ~ { currentStatus _ status; BROADCAST statusChange; }; DoUnderLock: PUBLIC PROC [action: PROC] ~ { Lock[]; action[ ! UNWIND => UnLock[]]; UnLock[]; }; DoEnabled: PUBLIC PROC [action: PROC] ~ { Lock[]; BEGIN ENABLE UNWIND => UnLock[]; IF currentStatus = extendable THEN NULL ELSE Enable[]; action[]; END; UnLock[]; }; DoReadOnlyOrEnabled: PUBLIC PROC [action: PROC] ~ { Lock[]; BEGIN ENABLE UNWIND => UnLock[]; IF currentStatus = readOnly OR currentStatus = extendable THEN NULL ELSE Enable[]; action[]; END; UnLock[]; }; Disable: PUBLIC PROC ~ { Lock[]; BEGIN ENABLE UNWIND => UnLock[]; status: Status _ GetStatus[waitDuringTransition: TRUE]; currentStatus _ transition; IF status = extendable THEN { IF WriteCacheToDisk[].couldNotExtend THEN NULL; FS.Close[file]; file _ [NIL]; residentPointer _ NIL; resident _ NIL; SafeStorage.ReclaimCollectibleObjects[]; status _ disabled; }; IF status = readOnly THEN { FS.Close[file]; file _ [NIL]; residentPointer _ NIL; resident _ NIL; SafeStorage.ReclaimCollectibleObjects[]; ResetTables[]; status _ disabled; }; SetStatus[status]; END; UnLock[]; }; MakeReadOnly: PUBLIC PROC ~ { Lock[]; BEGIN ENABLE UNWIND => UnLock[]; status: Status _ GetStatus[waitDuringTransition: TRUE]; currentStatus _ transition; IF status = extendable THEN { date: BasicTime.GMT; IF WriteCacheToDisk[].couldNotExtend THEN { FS.Close[file]; file _ [NIL]; residentPointer _ NIL; resident _ NIL; SafeStorage.ReclaimCollectibleObjects[]; status _ disabled; } ELSE { date _ FS.GetInfo[file].created; FS.Close[file]; file _ FS.Open[fontCacheName, $read, date]; status _ readOnly; }; }; IF status = disabled THEN { OpenFileAndBuildTables[]; status _ readOnly; }; SetStatus[status]; END; UnLock[]; }; Enable: PUBLIC PROC ~ { Lock[]; BEGIN ENABLE UNWIND => UnLock[]; status: Status _ GetStatus[waitDuringTransition: TRUE]; currentStatus _ transition; IF status = disabled THEN { OpenFileAndBuildTables[]; status _ extendable; }; IF status = readOnly THEN { date: BasicTime.GMT _ FS.GetInfo[file].created; FS.Close[file]; file _ FS.Open[fontCacheName, $write, date]; status _ extendable; }; SetStatus[status]; END; UnLock[]; }; transTable: TransTable _ NEW[TransTableRep[20]]; TransTable: TYPE ~ REF TransTableRep; TransTableRep: TYPE ~ RECORD [ length: NAT _ 0, highWaterMark: NAT _ 0, seq: SEQUENCE maxLength: NAT OF REF TransformationCodeEntry ]; CheckTransTable: PROC ~ { -- This is to document the invariants on the transformation table, and is not normally executed. FOR i: NAT IN [0..transTable.maxLength) DO ti: REF TransformationCodeEntry _ transTable[i]; ok: BOOL _ SELECT i FROM IN [0..transTable.length) => ti.codeValue = i, -- These have code values associated with them IN [transTable.length..transTable.highWaterMark) => ti.codeValue = CARDINAL.LAST, -- These currently have no code values, but have been handed out to clients and so must be kept around. IN [transTable.highWaterMark..transTable.maxLength) => ti = NIL, -- Free slots in the sequence. ENDCASE => ERROR; IF NOT ok THEN ERROR; ENDLOOP; }; TransformationFromTransID: PUBLIC PROC[transID: TransID] RETURNS[Transformation] ~ { t: Coefficients _ transID.coefficients; RETURN [ImagerTransformation.Create[t.a, t.b, t.c, t.d, t.e, t.f]]; }; TransIDFromTransformation: PUBLIC PROC [transformation: Transformation, scanConversionType: ScanConversionType, hint: TransID] RETURNS [transID: TransID] ~ { t: Transformation ~ transformation; c: Coefficients _ [t.a, t.b, t.c, t.d, t.e, t.f]; IF hint # NIL AND hint.coefficients = c AND hint.scanConversionType = scanConversionType THEN RETURN [hint]; RETURN [InsertTransformation[c, scanConversionType]]; }; InsertTransformation: PROC [c: Coefficients, scanConversionType: ScanConversionType] RETURNS [transID: TransID] ~ { Locked: PROC ~ { FOR i: NAT IN [0..transTable.highWaterMark) DO ti: TransID _ transTable[i]; IF ti.coefficients = c AND ti.scanConversionType = scanConversionType THEN {transID _ ti; RETURN}; ENDLOOP; IF transTable.highWaterMark >= transTable.maxLength THEN { new: TransTable _ NEW[TransTableRep[transTable.maxLength + 20]]; FOR i: NAT IN [0..transTable.highWaterMark) DO new[i] _ transTable[i]; transTable[i] _ NIL; ENDLOOP; new.length _ transTable.length; new.highWaterMark _ transTable.highWaterMark; transTable _ new; }; transID _ transTable[transTable.highWaterMark] _ NEW [TransformationCodeEntry _ [CARDINAL.LAST, c, scanConversionType]]; transTable.highWaterMark _ transTable.highWaterMark + 1; }; DoUnderLock[Locked]; }; SetTransIDCode: PROC [transID: TransID] ~ { IF transID.codeValue = CARDINAL.LAST THEN { FOR i: NAT IN [transTable.length..transTable.highWaterMark) DO IF transTable[i] = transID THEN { s: REF TransformationCodeEntry _ transTable[transTable.length]; transTable[transTable.length] _ transID; transTable[i] _ s; transID.codeValue _ transTable.length; transTable.length _ transTable.length + 1; RETURN; }; ENDLOOP; ERROR FontCacheInconsistency[$BadTransID, 0]; }; }; GetTransCode: PROC [transID: TransID] RETURNS [transIDCode: CARDINAL] ~ { IF transID.codeValue = CARDINAL.LAST THEN TRUSTED { SetTransIDCode[transID]; [] _ AppendEntry[transformationCode, SIZE[TransformationCodeEntry], LOOPHOLE[transID]]; }; transIDCode _ transID.codeValue; }; fontIDTableOldLength: NAT _ 0; fontIDTable: FontIDTable _ NEW[FontIDTableRep[20]]; FontIDTable: TYPE ~ REF FontIDTableRep; FontIDTableRep: TYPE ~ RECORD [ length: NAT _ 0, highWaterMark: NAT _ 0, seq: SEQUENCE maxLength: NAT OF REF FontIDCodeEntry ]; CheckFontIDTable: PROC ~ { -- This is to document the invariants on the fontID table, and is not normally executed. FOR i: NAT IN [0..fontIDTable.maxLength) DO fi: REF FontIDCodeEntry _ fontIDTable[i]; ok: BOOL _ SELECT i FROM IN [0..fontIDTable.length) => fi.codeValue = i, -- These have code values associated with them IN [fontIDTable.length..fontIDTable.highWaterMark) => fi.codeValue = CARDINAL.LAST, -- These currently have no code values, but have been handed out to clients and so must be kept around. IN [fontIDTable.highWaterMark..fontIDTable.maxLength) => fi = NIL, -- Free slots in the sequence. ENDCASE => ERROR; IF NOT ok THEN ERROR; ENDLOOP; }; RopeFromFontID: PUBLIC PROC [fontID: FontID] RETURNS [name: ROPE] ~ { i: NAT _ 0; append: PROC RETURNS [CHAR] ~ {RETURN [fontID[(i_i+1)-1]]}; name _ Rope.FromProc[fontID.chars, append]; }; GMTFromFontID: PUBLIC PROC [fontID: FontID] RETURNS [createdTime: BasicTime.GMT] ~ { createdTime _ fontID.createdTime; }; CreateFontID: PROC [chars: NAT, charProc: PROC [NAT] RETURNS [CHAR], createdTime: BasicTime.GMT] RETURNS [fontID: FontID] ~ { Match: PROC [fid: REF FontIDCodeEntry] RETURNS [BOOLEAN] ~ { IF fid.createdTime # createdTime THEN RETURN [FALSE]; IF fid.chars # chars THEN RETURN [FALSE]; FOR i: NAT IN [0..chars) DO IF fid[i] # charProc[i] THEN RETURN [FALSE] ENDLOOP; RETURN [TRUE] }; Lookup: PROC ~ { FOR i: NAT IN [0..fontIDTable.highWaterMark) DO fi: FontID _ fontIDTable[i]; IF Match[fi] THEN {fontID _ fi; RETURN}; ENDLOOP; fontID _ NEW[FontIDCodeEntry[chars]]; fontID.codeValue _ CARDINAL.LAST; fontID.createdTime _ createdTime; FOR i: NAT IN [0..chars) DO fontID[i] _ charProc[i]; ENDLOOP; IF fontIDTable.highWaterMark >= fontIDTable.maxLength THEN { new: FontIDTable _ NEW[FontIDTableRep[fontIDTable.highWaterMark + 20]]; FOR i: NAT IN [0..fontIDTable.highWaterMark) DO new[i] _ fontIDTable[i]; ENDLOOP; new.length _ fontIDTable.length; new.highWaterMark _ fontIDTable.highWaterMark; fontIDTable _ new; }; fontIDTable[fontIDTable.highWaterMark] _ fontID; fontIDTable.highWaterMark _ fontIDTable.highWaterMark + 1; }; DoUnderLock[Lookup]; }; FontIDFromRopeAndGMT: PUBLIC PROC [name: ROPE, createdTime: BasicTime.GMT] RETURNS [fontID: FontID] ~ { charProc: PROC [i: NAT] RETURNS [CHAR] ~ {RETURN [Ascii.Lower[name.Fetch[i]]]}; fontID _ CreateFontID[name.Length, charProc, createdTime]; }; FontIDFromFontIDCodeEntry: PROC [f: LONG POINTER TO FontIDCodeEntry] RETURNS [fontID: FontID] ~ TRUSTED { charProc: SAFE PROC [i: NAT] RETURNS [CHAR] ~ TRUSTED {RETURN [f[i]]}; fontID _ CreateFontID[f.chars, charProc, f.createdTime]; }; SetFontIDCode: PROC [fontID: FontID] ~ { IF fontID.codeValue = CARDINAL.LAST THEN { FOR i: NAT IN [fontIDTable.length..fontIDTable.highWaterMark) DO IF fontIDTable[i] = fontID THEN { s: REF FontIDCodeEntry _ fontIDTable[fontIDTable.length]; fontIDTable[fontIDTable.length] _ fontID; fontIDTable[i] _ s; fontID.codeValue _ fontIDTable.length; fontIDTable.length _ fontIDTable.length + 1; RETURN; }; ENDLOOP; ERROR FontCacheInconsistency[$BadFontID, 0]; }; }; GetFontCode: PROC [fontID: FontID] RETURNS [fontIDCode: CARDINAL] ~ { IF fontID.codeValue = CARDINAL.LAST THEN TRUSTED { SetFontIDCode[fontID]; [] _ AppendEntry[fontIDCode, SIZE[FontIDCodeEntry[fontID.chars]], LOOPHOLE[fontID]]; }; fontIDCode _ fontID.codeValue; }; fontCacheName: ROPE _ "[]<>FontCache.DontDeleteMe"; file: FS.OpenFile; resident: CountedVM.Handle _ NIL; residentPointer: LONG POINTER _ NIL; CreateNewFile: PROC ~ TRUSTED { header: Header _ [ password: passwordValue, numberOfEntries: 0, numberOfWords: SIZE[Header] + SIZE[CARDINAL], totalUsage: 0 ]; pointer: LONG POINTER _ @header; stream: IO.STREAM _ FS.StreamOpen[fontCacheName, $create]; stream.UnsafePutBlock[[base: pointer, startIndex: 0, count: SIZE[Header]*Basics.bytesPerWord]]; stream.PutChar['\000]; stream.PutChar['\000]; stream.Close; }; CantAppend: ERROR ~ CODE; AppendEntry: PROC [entryType: EntryType, entryLengthInWords: CARDINAL, dataPtr: LONG POINTER] RETURNS [offset: INT] ~ { nwords: CARDINAL ~ entryLengthInWords + SIZE[EntryPrefix]; TryAppend: PROC RETURNS [failed: BOOLEAN] ~ TRUSTED { hdr: LONG POINTER TO Header _ residentPointer; destOffset: LONG CARDINAL _ hdr.numberOfWords - SIZE[CARDINAL]; newSize: INT _ hdr.numberOfWords + nwords; IF newSize < 0 THEN ERROR; IF newSize > resident.Words THEN failed _ TRUE ELSE { ep: LONG POINTER TO EntryPrefix _ residentPointer+destOffset; ep^ _ [entryType, entryLengthInWords]; PrincOpsUtils.LongCopy[from: dataPtr, nwords: entryLengthInWords, to: residentPointer+destOffset+SIZE[EntryPrefix]]; LOOPHOLE[residentPointer+destOffset+nwords, LONG POINTER TO CARDINAL]^ _ 0; hdr.numberOfWords _ newSize; hdr.numberOfEntries _ hdr.numberOfEntries + 1; offset _ destOffset + SIZE[EntryPrefix]; failed _ FALSE }; }; IF currentStatus # extendable THEN ERROR CantAppend; IF TryAppend[].failed THEN { WriteOutAndReadIn[]; IF TryAppend[].failed THEN ERROR CantAppend; }; }; WriteCacheToDisk: PROC RETURNS [couldNotExtend: BOOLEAN _ FALSE] ~ TRUSTED { hdr: LONG POINTER TO Header _ residentPointer; pages: INT _ PagesForWords[hdr.numberOfWords]; FS.SetPageCount[file, pages ! FS.Error => IF error.group = environment THEN {couldNotExtend _ TRUE; CONTINUE} ]; IF couldNotExtend THEN RETURN; FS.Write[file: file, to: 0, nPages: pages, from: residentPointer]; FS.SetByteCountAndCreatedTime[file, hdr.numberOfWords*Basics.bytesPerWord, BasicTime.Now[]]; }; WriteOutAndReadIn: PROC ~ TRUSTED { hdr: LONG POINTER TO Header _ residentPointer; pages: INT _ PagesForWords[hdr.numberOfWords]; couldNotExtend: BOOLEAN _ TRUE; IF WriteCacheToDisk[].couldNotExtend THEN RETURN; residentPointer _ hdr _ NIL; resident _ NIL; SafeStorage.ReclaimCollectibleObjects[]; Process.Pause[Process.MsecToTicks[1000]]; AllocateResidentCacheSpace[minPages: pages]; FS.Read[file: file, from: 0, nPages: pages, to: residentPointer]; }; FontCacheInconsistency: PUBLIC ERROR [reason: ATOM, wordOffset: INT] ~ CODE; hashEntries: INT _ 0; maxProbes: NAT _ 100; hashTableSize: NAT ~ 32749; -- a prime hashTable: REF ARRAY [0..hashTableSize) OF INT _ NEW[ARRAY [0..hashTableSize) OF INT]; stats: RECORD [sLookups, sProbes, uLookups, uProbes, fLookups: LONG CARDINAL _ 0]; HashTableLookup: PROC [fontIDCode, transformationCode: CARDINAL, charCode: INT] RETURNS [hashIndex: CARDINAL] ~ TRUSTED { OPEN stats; rand: CARDINAL _ Basics.LowHalf[LOOPHOLE[charCode]] + Basics.HighHalf[LOOPHOLE[charCode]]*1024 + fontIDCode + fontIDCode*32 + transformationCode * 2048 - transformationCode; h0: CARDINAL _ fontIDCode MOD 8 + transformationCode MOD 8 * 8 + (Basics.LowHalf[LOOPHOLE[charCode]] MOD 128 - 32)* 64; WHILE h0 >= hashTableSize DO h0 _ h0 - hashTableSize ENDLOOP; hashIndex _ h0; FOR i: CARDINAL IN [0..hashTableSize) DO t: INT _ hashTable[hashIndex]; IF t = 0 THEN {uProbes _ uProbes + (i + 1); uLookups _ uLookups + 1; RETURN} ELSE { te: LONG POINTER TO MaskEntry _ residentPointer + t; IF te.fontIDCode = fontIDCode AND te.transformationCode = transformationCode AND te.charCode = charCode THEN {sProbes _ sProbes + (i + 1); sLookups _ sLookups + 1; RETURN}; }; rand _ Basics.LongDivMod[Basics.LongMult[3141, rand] + 271, hashTableSize].remainder; IF (hashIndex _ h0 + rand) >= hashTableSize THEN hashIndex _ hashIndex - hashTableSize; ENDLOOP; hashIndex _ CARDINAL.LAST; fLookups _ fLookups + 1; }; runGroupOverhead: INT _ 3600; maskBitsLimit: INT _ 60000; InsertMask: PUBLIC PROC [fontID: FontID, transID: TransID, charCode: INT, sWidth, fWidth: Scaled.Value, mask: Mask, amplified, correctSpace, correctMask: BOOLEAN] RETURNS [ok: BOOLEAN _ FALSE] ~ { bb: ImagerPixelMap.DeviceRectangle _ ImagerMask.BoundingBox[mask]; runCount: INT _ ImagerMask.CountRuns[mask]; rasterBits: INT _ Basics.LongMult[bb.fSize, bb.sSize]; runGroupBits: INT _ SIZE[Run]*16*runCount + runGroupOverhead; maskEntryRef: REF MaskEntry _ NIL; nwords: CARDINAL; IF MIN[runGroupBits, rasterBits] > maskBitsLimit THEN RETURN [FALSE]; maskEntryRef _ IF runGroupBits < rasterBits THEN GetRuns[mask, bb, runCount] ELSE GetRaster[mask, bb]; WITH maskEntryRef SELECT FROM ra: REF MaskEntry.raster => nwords _ SIZE[MaskEntry.raster[ra.sSizeBB*((ra.fSizeBB+Basics.bitsPerWord-1)/Basics.bitsPerWord)]]; ru: REF MaskEntry.runs => nwords _ SIZE[MaskEntry.runs[ru.nRuns]]; ENDCASE => ERROR; maskEntryRef.charCode _ charCode; maskEntryRef.usage _ 0; maskEntryRef.sWidth _ sWidth; maskEntryRef.fWidth _ fWidth; maskEntryRef.amplified _ amplified; maskEntryRef.correctSpace _ correctSpace; maskEntryRef.correctMask _ correctMask; BEGIN Insert: PROC ~ TRUSTED { offset: INT; hashIndex: CARDINAL; maskEntryRef.fontIDCode _ GetFontCode[fontID]; maskEntryRef.transformationCode _ GetTransCode[transID]; hashIndex _ HashTableLookup[maskEntryRef.fontIDCode, maskEntryRef.transformationCode, maskEntryRef.charCode]; IF hashIndex = CARDINAL.LAST THEN ok _ FALSE ELSE IF hashTable[hashIndex] = 0 THEN { offset _ AppendEntry[mask, nwords, LOOPHOLE[maskEntryRef]]; hashTable[hashIndex] _ offset; ok _ TRUE; }; }; DoReadOnlyOrEnabled[Insert ! CantAppend => CONTINUE]; IF NOT ok THEN { header: Header _ GetHeader[]; TrimCache[maxNumberOfMasks: INT[hashTableSize]*8/10, maxNumberOfWords: header.numberOfWords*8/10]; DoReadOnlyOrEnabled[Insert ! CantAppend => CONTINUE]; }; END; }; GetRuns: PROC [mask: Mask, bb: ImagerPixelMap.DeviceRectangle, runCount: INT] RETURNS [REF MaskEntry] ~ { maxRuns: NAT _ runCount+bb.sSize; maskEntryRef: REF MaskEntry.runs _ NEW[MaskEntry.runs[maxRuns]]; i: NAT _ 0; s: INTEGER _ bb.sMin; AppendRun: PROC [sMin, fMin: INTEGER, fSize: NAT] ~ { WHILE s # sMin DO IF s > sMin THEN ERROR; IF i = 0 THEN {s _ bb.sMin _ bb.sMin + 1; bb.sSize _ bb.sSize - 1} ELSE IF NOT maskEntryRef[i-1].lastRun THEN { maskEntryRef[i-1].lastRun _ TRUE; s _ s + 1; } ELSE { IF i >= maxRuns THEN ERROR; maskEntryRef[i] _ [fMin: 0, lastRun: TRUE, fSize: 0]; s _ s + 1; i _ i + 1; }; ENDLOOP; IF i >= maxRuns THEN ERROR; maskEntryRef[i] _ [fMin: fMin-bb.fMin, lastRun: FALSE, fSize: fSize]; i _ i + 1; }; ImagerMask.GenerateRuns[mask, mask, AppendRun, 0, 0]; IF i > 0 THEN {maskEntryRef[i-1].lastRun _ TRUE; s _ s + 1}; maskEntryRef.nRuns _ i; maskEntryRef.sMinBB _ bb.sMin; maskEntryRef.fMinBB _ bb.fMin; maskEntryRef.sSizeBB _ s - bb.sMin; maskEntryRef.fSizeBB _ bb.fSize; RETURN [maskEntryRef] }; GetRaster: PROC [mask: Mask, bb: ImagerPixelMap.DeviceRectangle] RETURNS [REF MaskEntry] ~ TRUSTED { rast: CARDINAL _ (bb.fSize+Basics.bitsPerWord-1)/Basics.bitsPerWord; words: CARDINAL _ bb.sSize*rast; maskEntryRef: REF MaskEntry.raster _ NEW[MaskEntry.raster[words]]; pixelMap: ImagerPixelMap.PixelMap _ [sOrigin: bb.sMin, fOrigin: bb.fMin, sMin: 0, fMin: 0, sSize: bb.sSize, fSize: bb.fSize, refRep: NEW[ImagerPixelMap.PixelMapRep _ [ref: resident, pointer: @maskEntryRef[0], words: words, lines: bb.sSize, lgBitsPerPixel: 0, rast: rast]]]; pixelMap.Clear; ImagerMask.ApplyConstant[mask, mask, pixelMap, 1]; maskEntryRef.sMinBB _ bb.sMin; maskEntryRef.fMinBB _ bb.fMin; maskEntryRef.sSizeBB _ bb.sSize; maskEntryRef.fSizeBB _ bb.fSize; RETURN [maskEntryRef] }; AgeUseCounts: PROC ~ TRUSTED { pointer: LONG POINTER _ residentPointer; offset: INT _ SIZE[Header]; hdr: LONG POINTER TO Header _ residentPointer; words: INT _ hdr.numberOfWords; entries: INT _ hdr.numberOfEntries; newTotal: LONG CARDINAL _ 0; WHILE offset < words DO prefix: LONG POINTER TO EntryPrefix _ pointer+offset; offset _ offset + SIZE[EntryPrefix]; SELECT prefix.entryType FROM trailer => EXIT; mask => { entry: LONG POINTER TO MaskEntry _ pointer + offset; entry.usage _ entry.usage/2 + entry.usage MOD 2; newTotal _ newTotal + entry.usage; }; ENDCASE => NULL; offset _ offset + prefix.entryLengthInWords; entries _ entries - 1; ENDLOOP; IF offset # words THEN ERROR FontCacheInconsistency[$UnexpectedEndMarker, offset]; IF entries # 0 THEN ERROR FontCacheInconsistency[$WrongNumberOfEntries, offset]; hdr.totalUsage _ newTotal; }; GetMask: PUBLIC UNSAFE PROC [fontID: FontID, transID: TransID, charCode: INT] RETURNS [maskDesc: MaskDesc _ [NIL, NIL]] ~ UNCHECKED { Get: PROC ~ TRUSTED { hashIndex: CARDINAL _ HashTableLookup[fontID.codeValue, transID.codeValue, charCode]; offset: INT; IF hashIndex # CARDINAL.LAST AND (offset _ hashTable[hashIndex]) # 0 THEN { me: LONG POINTER TO MaskEntry _ residentPointer + offset; hdr: LONG POINTER TO Header _ residentPointer; hdr.totalUsage _ hdr.totalUsage + 1; IF (me.usage _ me.usage + 1) = CARDINAL.LAST THEN AgeUseCounts[]; maskDesc _ [resident, me]; }; }; DoReadOnlyOrEnabled[Get]; }; GetStringBodyMasks: PUBLIC PROC [ fontID: FontID, transID: TransID, stringBodyDesc: StringBodyDesc, maskProc: UNSAFE PROC [MaskDesc] ] RETURNS [StringBodyDesc] ~ TRUSTED { WHILE stringBodyDesc.length # 0 DO charCode: CARDINAL _ Current[stringBodyDesc]; maskDesc: MaskDesc _ GetMask[fontID, transID, charCode]; IF maskDesc.maskEntryPtr = NIL THEN EXIT; maskProc[maskDesc]; stringBodyDesc _ Advance[stringBodyDesc]; ENDLOOP; RETURN [stringBodyDesc] }; cacheVersion: CARDINAL _ 0; ResetTables: PROC ~ TRUSTED { cacheVersion _ cacheVersion + 1; PrincOpsUtils.LongZero[where: LOOPHOLE[hashTable], nwords: SIZE[ARRAY [0..hashTableSize) OF INT]]; WHILE fontIDTable.length > 0 DO fontIDTable[fontIDTable.length _ fontIDTable.length - 1].codeValue _ CARDINAL.LAST; ENDLOOP; WHILE transTable.length > 0 DO transTable[transTable.length _ transTable.length - 1].codeValue _ CARDINAL.LAST; ENDLOOP; }; BuildTables: PROC ~ TRUSTED { pointer: LONG POINTER _ residentPointer; offset: INT _ SIZE[Header]; hdr: LONG POINTER TO Header _ residentPointer; words: INT _ hdr.numberOfWords; entries: INT _ hdr.numberOfEntries; totalUsage: INT _ hdr.totalUsage; IF hdr.password # passwordValue THEN ERROR FontCacheInconsistency[$NotAFontCache, 0]; IF Basics.bytesPerWord*words # FS.GetInfo[file].bytes THEN ERROR FontCacheInconsistency[$InconsistentLengths, 0]; ResetTables[]; WHILE offset < words DO prefix: LONG POINTER TO EntryPrefix _ pointer+offset; offset _ offset + SIZE[EntryPrefix]; SELECT prefix.entryType FROM trailer => EXIT; fontIDCode => { ffidce: LONG POINTER TO FontIDCodeEntry _ pointer+offset; fontID: FontID _ FontIDFromFontIDCodeEntry[ffidce]; SetFontIDCode[fontID]; IF fontID.codeValue # ffidce.codeValue THEN ERROR FontCacheInconsistency[$FontIDCodesOutOfSequence, offset]; }; transformationCode => { tce: LONG POINTER TO TransformationCodeEntry _ pointer+offset; transID: TransID _ InsertTransformation[tce.coefficients, tce.scanConversionType]; SetTransIDCode[transID]; IF transID.codeValue # tce.codeValue THEN ERROR FontCacheInconsistency[$TransformationCodesOutOfSequence, offset]; }; mask => { maskEntry: LONG POINTER TO MaskEntry _ pointer+offset; hashIndex: CARDINAL _ HashTableLookup[maskEntry.fontIDCode, maskEntry.transformationCode, maskEntry.charCode]; IF maskEntry.fontIDCode >= fontIDTable.length OR maskEntry.transformationCode >= transTable.length THEN FontCacheInconsistency[$MaskWithUndefinedFontOrTransformation, offset]; IF hashIndex # CARDINAL.LAST THEN { IF hashTable[hashIndex] # 0 THEN FontCacheInconsistency[$MultiplyDefinedMask, offset]; hashTable[hashIndex] _ offset; totalUsage _ totalUsage - maskEntry.usage; }; }; ENDCASE => ERROR FontCacheInconsistency[$BadEntryType, offset]; offset _ offset + prefix.entryLengthInWords; entries _ entries - 1; ENDLOOP; IF offset # words THEN ERROR FontCacheInconsistency[$UnexpectedEndMarker, offset]; IF entries # 0 THEN ERROR FontCacheInconsistency[$WrongNumberOfEntries, offset]; IF totalUsage # 0 THEN ERROR FontCacheInconsistency[$WrongTotalUsage, offset]; }; CVMAllocate: PROC [words: INT] RETURNS [CountedVM.Handle] ~ {RETURN [CountedVM.Allocate[words]]}; AllocateResidentCacheSpace: PROC [minPages: INT] ~ TRUSTED { newpages: INT _ MAX[20, (minPages*126+50)/100]; residentPointer _ NIL; resident _ NIL; resident _ CVMAllocate[WordsForPages[newpages] ! VM.CantAllocate => {newpages _ MAX[minPages, bestInterval.count]; CONTINUE}]; IF resident = NIL THEN { SafeStorage.ReclaimCollectibleObjects[]; Process.Pause[Process.MsecToTicks[2000]]; resident _ CVMAllocate[WordsForPages[newpages]]; }; residentPointer _ resident.Pointer; }; OpenFileAndBuildTables: PROC ~ TRUSTED { openOK: BOOLEAN _ TRUE; pages: INT _ 0; bytes: INT _ 0; residentPages: INT _ 0; file _ FS.Open[fontCacheName, $write ! FS.Error => {openOK _ FALSE; CONTINUE}]; IF NOT openOK THEN { openOK _ TRUE; CreateNewFile[]; file _ FS.Open[fontCacheName, $write]; }; [pages: pages, bytes: bytes] _ FS.GetInfo[file]; AllocateResidentCacheSpace[pages]; FS.Read[file: file, from: 0, nPages: pages, to: residentPointer]; BuildTables[]; }; Current: PUBLIC PROC [stringBodyDesc: StringBodyDesc] RETURNS [charCode: INT] ~ { Fet: PROC [i: INT] RETURNS [CARDINAL] ~ { char: CHAR; IF i >= stringBodyDesc.start+stringBodyDesc.length THEN RETURN [0]; WITH stringBodyDesc.stringBody SELECT FROM r: Rope.ROPE => char _ r.Fetch[i]; t: REF TEXT => char _ t[i]; ENDCASE => ERROR; RETURN [ORD[char]] }; charCode _ Fet[stringBodyDesc.start]; IF charCode = 255 THEN { charCode _ Fet[stringBodyDesc.start+1]*256+Fet[stringBodyDesc.start+2]; } ELSE charCode _ stringBodyDesc.fontSet*256+charCode; }; Advance: PUBLIC PROC [stringBodyDesc: StringBodyDesc] RETURNS [StringBodyDesc] ~ { Fet: PROC [i: INT] RETURNS [CARDINAL] ~ { char: CHAR; IF i >= stringBodyDesc.start+stringBodyDesc.length THEN RETURN [0]; WITH stringBodyDesc.stringBody SELECT FROM r: Rope.ROPE => char _ r.Fetch[i]; t: REF TEXT => char _ t[i]; ENDCASE => ERROR; RETURN [ORD[char]] }; code: CARDINAL _ Fet[stringBodyDesc.start]; WHILE code = 225 DO stringBodyDesc.fontSet _ Fet[stringBodyDesc.start+1]; stringBodyDesc.start _ stringBodyDesc.start+2; stringBodyDesc.length _ stringBodyDesc.length-2; ENDLOOP; stringBodyDesc.start _ stringBodyDesc.start+1; stringBodyDesc.length _ stringBodyDesc.length-1; RETURN [stringBodyDesc] }; TrimCache: PUBLIC PROC [maxNumberOfMasks: INT, maxNumberOfWords: INT] ~ { enabled: PROC ~ TRUSTED { hdr: LONG POINTER TO Header _ residentPointer; maxMasks: CARDINAL _ MIN[hdr.numberOfEntries, hashTableSize]; index: CardSeq _ NEW[CardSeqRep[maxMasks]]; GetMaskIndices[index]; SortByUsage[index]; DiscardExcessEntries[index, maxNumberOfMasks, maxNumberOfWords]; DiscardUnusedCodes[index]; WriteNewFile[index]; Disable[]; }; DoEnabled[enabled]; }; CardSeq: TYPE ~ REF CardSeqRep; CardSeqRep: TYPE ~ RECORD [length: NAT_ 0, c: SEQUENCE maxLength: NAT OF LONG CARDINAL]; GetMaskIndices: PROC [index: CardSeq] ~ TRUSTED { hdr: LONG POINTER TO Header _ residentPointer; words: INT _ hdr.numberOfWords; entries: INT _ hdr.numberOfEntries; offset: INT _ SIZE[Header]; WHILE offset < words DO prefix: LONG POINTER TO EntryPrefix _ residentPointer+offset; offset _ offset + SIZE[EntryPrefix]; SELECT prefix.entryType FROM trailer => EXIT; mask => { IF index.length < index.maxLength THEN { index[index.length] _ offset; index.length _ index.length + 1; }; }; ENDCASE => NULL; offset _ offset + prefix.entryLengthInWords; entries _ entries - 1; ENDLOOP; IF offset # words THEN ERROR FontCacheInconsistency[$UnexpectedEndMarker, offset]; IF entries # 0 THEN ERROR FontCacheInconsistency[$WrongNumberOfEntries, offset]; }; shellD: ARRAY [1..11] OF CARDINAL = [1, 4, 13, 40, 121, 364, 1093, 3280, 9841, 29524, 65535]; SortByUsage: PROC [index: CardSeq] ~ { Key: PROC [i: NAT] RETURNS [CARDINAL] ~ TRUSTED { entry: LONG POINTER TO MaskEntry _ residentPointer+index[i]; RETURN [entry.usage] }; length: NAT _ index.length; passes: NAT _ 1; UNTIL shellD[passes+2] >= index.length DO passes _ passes+1 ENDLOOP; FOR pass: NAT DECREASING IN [1..passes] DO h: NAT _ shellD[pass]; FOR i: NAT IN [0..length) DO key: CARDINAL _ Key[i]; data: LONG CARDINAL _ index[i]; j: NAT _ i; WHILE j>=h AND key < Key[j-h] DO index[j] _ index[j-h]; j _ j-h; ENDLOOP; index[j] _ data; ENDLOOP; ENDLOOP; }; DiscardExcessEntries: PROC [index: CardSeq, maxNumberOfMasks: INT, maxNumberOfWords: INT] ~ TRUSTED { n: INT _ 0; words: INT _ 0; maxNumberOfMasks _ MIN[maxNumberOfMasks, index.length]; WHILE n < maxNumberOfMasks DO prefix: LONG POINTER TO EntryPrefix _ residentPointer + index[n] - SIZE[EntryPrefix]; words _ words + SIZE[EntryPrefix] + prefix.entryLengthInWords; IF words > maxNumberOfWords THEN EXIT; n _ n + 1; ENDLOOP; index.length _ n; }; DiscardUnusedCodes: PROC [index: CardSeq] ~ TRUSTED { used: CARDINAL ~ 0; unused: CARDINAL ~ CARDINAL.LAST; fn, tn: NAT _ 0; FOR i: NAT IN [0..fontIDTable.length) DO fontIDTable[i].codeValue _ unused; ENDLOOP; FOR i: NAT IN [0..transTable.length) DO transTable[i].codeValue _ unused; ENDLOOP; FOR i: NAT IN [0..index.length) DO maskEntry: LONG POINTER TO MaskEntry _ residentPointer+index[i]; fontIDTable[maskEntry.fontIDCode].codeValue _ used; transTable[maskEntry.transformationCode].codeValue _ used; ENDLOOP; FOR i: NAT IN [0..fontIDTable.length) DO IF fontIDTable[i].codeValue = used THEN {fontIDTable[i].codeValue _ fn; fn _ fn+1}; ENDLOOP; tn _ 0; FOR i: NAT IN [0..transTable.length) DO IF transTable[i].codeValue = used THEN {transTable[i].codeValue _ tn; tn _ tn + 1}; ENDLOOP; FOR i: NAT IN [0..index.length) DO maskEntry: LONG POINTER TO MaskEntry _ residentPointer+index[i]; maskEntry.fontIDCode _ fontIDTable[maskEntry.fontIDCode].codeValue; maskEntry.transformationCode _ transTable[maskEntry.transformationCode].codeValue; ENDLOOP; FOR i: NAT IN [0..fontIDTable.length) DO dest: CARDINAL _ fontIDTable[i].codeValue; IF dest # unused THEN { s: REF FontIDCodeEntry _ fontIDTable[i]; fontIDTable[i] _ fontIDTable[dest]; fontIDTable[dest] _ s; }; ENDLOOP; FOR i: NAT IN [0..transTable.length) DO dest: CARDINAL _ transTable[i].codeValue; IF dest # unused THEN { s: REF TransformationCodeEntry _ transTable[i]; transTable[i] _ transTable[dest]; transTable[dest] _ s; }; ENDLOOP; fontIDTable.length _ fn; transTable.length _ tn; }; WriteNewFile: PROC [index: CardSeq] ~ TRUSTED { header: Header _ [ password: passwordValue, numberOfEntries: 0, numberOfWords: SIZE[Header], totalUsage: 0 ]; pointer: LONG POINTER _ @header; stream: IO.STREAM _ FS.StreamOpen[fontCacheName, $create]; stream.UnsafePutBlock[[base: pointer, startIndex: 0, count: SIZE[Header]*Basics.bytesPerWord]]; header _ PutNamesAndTransformations[stream, header]; header _ CopySelectedMasks[stream, index, header]; stream.PutChar['\000]; stream.PutChar['\000]; header.numberOfWords _ header.numberOfWords + SIZE[CARDINAL]; stream.SetIndex[0]; stream.UnsafePutBlock[[base: pointer, startIndex: 0, count: SIZE[Header]*Basics.bytesPerWord]]; stream.Close; }; PutNamesAndTransformations: PROC [stream: IO.STREAM, header: Header] RETURNS [Header] ~ TRUSTED { FOR i: NAT IN [0..fontIDTable.length) DO f: REF FontIDCodeEntry _ fontIDTable[i]; words: CARDINAL _ SIZE[FontIDCodeEntry[f.chars]]; prefix: EntryPrefix _ [fontIDCode, words]; prefixPtr: LONG POINTER _ @prefix; stream.UnsafePutBlock[[base: prefixPtr, startIndex: 0, count: SIZE[EntryPrefix]*Basics.bytesPerWord]]; stream.UnsafePutBlock[[base: LOOPHOLE[f], startIndex: 0, count: words*Basics.bytesPerWord]]; header.numberOfEntries _ header.numberOfEntries + 1; header.numberOfWords _ header.numberOfWords + SIZE[EntryPrefix] + words; ENDLOOP; FOR i: NAT IN [0..transTable.length) DO t: REF TransformationCodeEntry _ transTable[i]; words: CARDINAL ~ SIZE[TransformationCodeEntry]; prefix: EntryPrefix _ [transformationCode, words]; prefixPtr: LONG POINTER _ @prefix; stream.UnsafePutBlock[[base: prefixPtr, startIndex: 0, count: SIZE[EntryPrefix]*Basics.bytesPerWord]]; stream.UnsafePutBlock[[base: LOOPHOLE[t], startIndex: 0, count: words*Basics.bytesPerWord]]; header.numberOfEntries _ header.numberOfEntries + 1; header.numberOfWords _ header.numberOfWords + SIZE[EntryPrefix] + words; ENDLOOP; RETURN [header] }; CopySelectedMasks: PROC [stream: IO.STREAM, index: CardSeq, header: Header] RETURNS [Header] ~ TRUSTED { FOR i: NAT IN [0..index.length) DO offset: LONG CARDINAL _ index[i]; prefix: LONG POINTER TO EntryPrefix _ residentPointer+offset-SIZE[EntryPrefix]; maskEntry: LONG POINTER TO MaskEntry _ residentPointer+offset; stream.UnsafePutBlock[[base: LOOPHOLE[prefix], startIndex: 0, count: (SIZE[EntryPrefix] + prefix.entryLengthInWords)*Basics.bytesPerWord]]; header.numberOfEntries _ header.numberOfEntries + 1; header.numberOfWords _ header.numberOfWords + SIZE[EntryPrefix] + prefix.entryLengthInWords; header.totalUsage _ header.totalUsage + maskEntry.usage; ENDLOOP; RETURN [header] }; GetListOfContents: PUBLIC PROC RETURNS [list: LIST OF MaskDescriptor _ NIL] ~ { SeqOfRope: TYPE ~ RECORD[SEQUENCE length: NAT OF ROPE]; n: REF SeqOfRope _ NEW[SeqOfRope[fontIDTable.length]]; cons: PROC [fontID: FontID, transID: TransID, maskDesc: MaskDesc] RETURNS [BOOL _ TRUE] ~ TRUSTED { entry: LONG POINTER TO MaskEntry _ maskDesc.maskEntryPtr; IF n[entry.fontIDCode] = NIL THEN n[entry.fontIDCode] _ RopeFromFontID[fontIDTable[entry.fontIDCode]]; list _ CONS[[ name: NARROW[n[entry.fontIDCode]], transID: transID, usage: entry.usage, set: entry.charCode/256, c: VAL[Basics.LowHalf[entry.charCode] MOD 256] ], list]; }; Enumerate[cons]; }; Enumerate: PUBLIC PROC [action: PROC [FontID, TransID, MaskDesc] RETURNS [continue: BOOL]] ~ { Enumerate: PROC ~ TRUSTED { hdr: LONG POINTER TO Header _ residentPointer; words: INT _ hdr.numberOfWords; entries: INT _ hdr.numberOfEntries; offset: INT _ SIZE[Header]; running: BOOLEAN _ TRUE; WHILE running AND offset < words DO prefix: LONG POINTER TO EntryPrefix _ residentPointer+offset; offset _ offset + SIZE[EntryPrefix]; SELECT prefix.entryType FROM trailer => EXIT; mask => { entry: LONG POINTER TO MaskEntry _ residentPointer+offset; running _ action[fontIDTable[entry.fontIDCode], transTable[entry.transformationCode], [resident, entry]]; }; ENDCASE => NULL; offset _ offset + prefix.entryLengthInWords; entries _ entries - 1; ENDLOOP; }; DoReadOnlyOrEnabled[Enumerate]; }; SetMaskUsage: PUBLIC PROC [fontID: FontID, transID: TransID, charCode: INT, usage: CARDINAL] ~ { Proc: PROC ~ TRUSTED { maskDesc: MaskDesc _ GetMask[fontID, transID, charCode]; IF maskDesc.maskEntryPtr # NIL THEN { hdr: LONG POINTER TO Header _ residentPointer; hdr.totalUsage _ hdr.totalUsage - maskDesc.maskEntryPtr.usage + usage; maskDesc.maskEntryPtr.usage _ usage; IF usage = CARDINAL.LAST THEN AgeUseCounts[]; }; }; DoReadOnlyOrEnabled[Proc]; }; showHash: BOOLEAN _ FALSE; ShowHash: PROC ~ TRUSTED { showHash _ TRUE; Process.Detach[FORK ShowHashProcess[]]; }; ShowHashProcess: PROC ~ TRUSTED { pm: ImagerPixelMap.PixelMap _ ImagerPixelMap.Create[0, [808-64, 256, 64, 512]]; display: ImagerPixelMap.PixelMap _ ImagerFrameBuffer.LFDisplay[]; b: LONG POINTER TO PACKED ARRAY [0..32767] OF [0..1] _ pm.refRep.pointer; WHILE showHash DO Process.Pause[Process.MsecToTicks[500]]; pm.Clear; FOR i: NAT IN [0..hashTableSize) DO IF hashTable[i] > 0 THEN b[i] _ 1; ENDLOOP; display.Transfer[pm]; ENDLOOP; }; END. dImagerMaskCacheImpl.mesa Copyright (C) 1984, 1985 by Xerox Corporation. All rights reserved. Michael Plass, August 17, 1984 3:57:12 pm PDT Doug Wyatt, January 26, 1985 5:42:55 pm PST Assigns next available code to transID. Assigns next available code to transID. Writes out the resident version, reallocates resident vm, and reads the resident version back in. The data structures do not need to change for this operation. If it could not extend the file, it returns without doing anything. The pause is in the hope that finalization will free up the old vm before reallocation. The first probe is intentionally biased to hit the first part of the table; for the simple (common) case of no more than 8 font names, 8 transformations, and char codes in the normal range, most of the hash table need not be swapped in. Successive probes are generated by a multiplicative-congruence random number generator. Used only in deciding which representation to use; a big number weights the choice in favor of rasters. Used in deciding whether to cache the mask or not Assume resident cache is present and locked Could do this much faster . . This attempts to allocate resident vm to be 26% bigger than minPages. If it fails to allocate this amount, it decreases its expectations and tries again. Let the error through if we still can't get VM. Shell sort Abuses the data structures, but only within this proc. First mark everything as unused. Find those that are used. Give the used ones their new codes. Put the new codes in the mask entries. Shuffle the entries into their rightful places in the table. Finally, fix up the lengths. Κ-}˜Icode™™DK™-J™+K™—šΟk ˜ Kšœ˜Kšœ˜Kšœ ˜ Kšœ ˜ Kšœ˜Kšœ˜Kšœ˜Kšœ ˜ Kšœ˜Kšœ˜Kšœ˜Kšœ˜Kšœ˜Kšœ ˜ Kšœ˜Kšœ˜K˜—Kšœ ˜"Kšœ‘˜˜Kšœ˜šœœœ˜K˜—šΟn œœ œœœœœ˜TK˜—šž œœ œœœœœ˜TK˜—šœ!˜!K˜—šœ œ˜K˜—Kšœ œ˜Kšœ œœœ˜ šœ  œ˜K˜—šž œœœœ˜3šžœœœ˜Kšœ1œ˜7Kšœœ˜/šœ˜Kšœœœœ˜.Kšœ ˜ Kšœ˜—Kšœ˜—Kšœ˜Kšœ˜K˜—šžœœœ˜Kšœœœ˜*šœœ˜Kšœœœ œ˜-Kšœ˜K˜—Kšœ˜Kšœ˜K˜—šžœœœ˜Kšœ"œœ˜/Kšœ˜Kšœœœœ ˜9Kšœ˜K˜—š ž œœœœœœ ˜Qšœœ˜šœœ˜!Kšœœ9˜?Kšœ(˜(Kšœ˜Kšœ&˜&Kšœ*˜*Kšœ˜Kšœ˜—Kšœ˜—Kšœ(˜-Kšœ˜—Kšœ˜K˜—šž œœœœ˜Iš œœœœœ˜3Kšœ˜Kšœ%œœ ˜WKšœ˜—Kšœ ˜ Kšœ˜K˜—Kšœœ˜Kšœœ˜3Kšœ œœ˜'šœœœ˜Kšœœ˜Kšœœ˜Kš œœ œœœ˜3Kšœ˜K˜—šžœœ˜KšŸX˜Xšœœœ˜+Kšœœ"˜)šœœ˜ šœ˜ šœ˜Kšœ˜KšŸ.˜.—šœ3˜5Kšœœœ˜KšŸg˜g—šœ6˜8Kšœœ˜ KšŸ˜—Kšœœ˜——Kšœœœœ˜Kšœ˜—Kšœ˜—K˜š žœœœœœ˜EKšœœ˜ Kš œœœœœ˜;Kšœ+˜+Kšœ˜K˜—š ž œœœœœ˜TKšœ!˜!Kšœ˜K˜—šž œœ œ œœœœœœ˜}š žœœœœœ˜KšœR˜RKšœ˜Kšœ#œœC˜rKšœ˜—šœ ˜ Kšœ œœœ˜6Kšœ œ[˜nKšœ+˜-Kšœ2˜4KšœH˜Lšœ œœœ˜#šœ˜ Kšœ5˜5—Kšœ˜Kšœ*˜*Kšœ˜—Kšœ˜—Kšœœ/˜?—Kšœ,˜,Kšœ˜Kšœ˜—Kšœœœ6˜RKšœ œœ7˜PKšœœœ2˜NKšœ˜K˜—šž œœ œœ˜9Kšœœ˜'K˜—šžœœ œœ˜Kšœœœ˜&K˜ Kšœ˜—Kšœ˜Kšœ˜K˜—šžœœœ˜5K™6Kšœœ˜Kšœœœœ˜!Kšœœ˜™ šœœœ˜(Kšœ"˜"Kšœ˜—šœœœ˜'Kšœ!˜!Kšœ˜——™šœœœ˜"Kšœ œœœ&˜@Kšœ3˜3Kšœ:˜:Kšœ˜——™#šœœœ˜(Kšœ!œ,˜SKšœ˜—K˜šœœœ˜'Kšœ œ-˜SKšœ˜——™&šœœœ˜"Kšœ œœœ&˜@KšœC˜CKšœR˜RKšœ˜——™<šœœœ˜(Kšœœ˜*šœœ˜Kšœœ"˜(Kšœ#˜#Kšœ˜Kšœ˜—Kšœ˜—šœœœ˜'Kšœœ˜)šœœ˜Kšœœ)˜/Kšœ!˜!Kšœ˜Kšœ˜—Kšœ˜——™Kšœ˜Kšœ˜—Kšœ˜K˜—šž œœœ˜/šœ˜Kšœ˜Kšœ˜Kšœœ ˜Kšœ ˜ Kšœ˜—Kšœ œœ ˜ Kšœœœœ$˜:Kšœ<œ˜_Kšœ4˜4Kšœ2˜2Kšœ˜Kšœ˜Kšœ.œœ˜=K˜Kšœ<œ˜_Kšœ ˜ K˜K˜—š žœœ œœœ œ˜ašœœœ˜(Kšœœ"˜(Kšœœœ˜1Kšœ*˜*Kšœ œœ ˜"Kšœ>œ$˜fKšœœ7˜\Kšœ4˜4Kšœ.œ˜HKšœ˜—šœœœ˜'Kšœœ)˜/Kšœœœ˜0Kšœ2˜2Kšœ œœ ˜"Kšœ>œ$˜fKšœœ7˜\Kšœ4˜4Kšœ.œ˜HKšœ˜—Kšœ ˜Kšœ˜—K˜š žœœ œœ"œ œ˜hšœœœ˜"Kšœœœ ˜!Kš œœœœ&œ˜OKšœ œœœ$˜>Kšœœ!œA˜‹Kšœ4˜4Kšœ.œ*˜\Kšœ8˜8Kšœ˜—Kšœ ˜Kšœ˜K˜—šžœœœœœœœ˜OKš œ œœœ œœœ˜7Kšœœ œ ˜6š œœ8œœœœ˜cKšœœœœ#˜9KšœœœE˜fšœœ˜ Kšœœ˜"Kšœ˜Kšœ˜Kšœ˜Kšœœ œ˜.Kšœ ˜ —Kšœ˜—Kšœ˜Kšœ˜K˜—š ž œœœ œœ œ˜^šž œœœ˜Kšœœœœ˜.Kšœœ˜Kšœ œ˜#Kšœœœ ˜Kšœ œœ˜šœ œ˜#Kšœœœœ&˜=Kšœœ˜$šœ˜Kšœ œ˜šœ ˜ Kšœœœœ$˜:Kšœi˜iKšœ˜—Kšœœ˜—Kšœ,˜,Kšœ˜Kšœ˜—Kšœ˜—Kšœ˜Kšœ˜K˜—š ž œœœ.œ œ˜`šžœœœ˜Kšœ8˜8šœœœ˜%Kšœœœœ˜.KšœF˜FKšœ$˜$Kšœ œœœ˜-Kšœ˜—Kšœ˜—Kšœ˜Kšœ˜K˜—Kšœ œœ˜šžœœœ˜Kšœ œ˜Kšœœ˜'Kšœ˜K˜—šžœœœ˜!KšœO˜OKšœA˜AKš œœœœœœ œ˜Išœ ˜K˜(K˜ šœœœ˜#Kšœœ ˜"Kšœ˜—K˜Kšœ˜—Kšœ˜K˜—Kšœ˜—…—„ŒΈm