semaphores test
This commit is contained in:
138
PBMap.pb
138
PBMap.pb
@@ -123,7 +123,7 @@ Module PBMap
|
|||||||
key.s
|
key.s
|
||||||
URL.s
|
URL.s
|
||||||
CacheFile.s
|
CacheFile.s
|
||||||
GetImageThread.i
|
*GetImageThread
|
||||||
Download.i
|
Download.i
|
||||||
Time.i
|
Time.i
|
||||||
Size.i
|
Size.i
|
||||||
@@ -292,7 +292,12 @@ Module PBMap
|
|||||||
Dragging.i
|
Dragging.i
|
||||||
Dirty.i ; To signal that drawing need a refresh
|
Dirty.i ; To signal that drawing need a refresh
|
||||||
|
|
||||||
MemoryCacheManagement.i ; To pause web loading threads
|
*MemoryCacheManagementThread
|
||||||
|
ResourceAccessSemaphore.i ; To pause web loading threads
|
||||||
|
ReadCountAccessSemaphore.i
|
||||||
|
ServiceQueueSemaphore.i
|
||||||
|
ReadCount.i
|
||||||
|
|
||||||
DownloadSlots.i ; Actual nb of used download slots
|
DownloadSlots.i ; Actual nb of used download slots
|
||||||
DownloadSlotsMutex.i ; To be sure that only one thread at a time can access to the DownloadSlots var
|
DownloadSlotsMutex.i ; To be sure that only one thread at a time can access to the DownloadSlots var
|
||||||
|
|
||||||
@@ -1045,13 +1050,18 @@ Module PBMap
|
|||||||
|
|
||||||
;-***
|
;-***
|
||||||
|
|
||||||
Procedure MemoryCacheManagement()
|
Procedure MemoryCacheManagement(*Void)
|
||||||
|
With PBMap
|
||||||
|
WaitSemaphore(\ServiceQueueSemaphore) ;serviceQueue.P(); // wait in line to be serviced
|
||||||
|
WaitSemaphore(\ResourceAccessSemaphore) ;resourceAccess.P(); // request exclusive access to resource
|
||||||
|
SignalSemaphore(\ServiceQueueSemaphore) ;serviceQueue.V(); // let next in line be serviced
|
||||||
|
EndWith
|
||||||
; If cache size exceeds limit, try to delete the oldest tiles used (first in the time stack)
|
; If cache size exceeds limit, try to delete the oldest tiles used (first in the time stack)
|
||||||
Protected CacheSize = MapSize(PBMap\MemCache\Images()) * Pow(PBMap\TileSize, 2) * 4 ; Size of a tile = TileSize * TileSize * 4 bytes (RGBA)
|
Protected CacheSize = MapSize(PBMap\MemCache\Images()) * Pow(PBMap\TileSize, 2) * 4 ; Size of a tile = TileSize * TileSize * 4 bytes (RGBA)
|
||||||
Protected CacheLimit = PBMap\Options\MaxMemCache * 1024
|
Protected CacheLimit = PBMap\Options\MaxMemCache * 1024
|
||||||
MyDebug("Cache size : " + Str(CacheSize/1024) + " / CacheLimit : " + Str(CacheLimit/1024), 5)
|
MyDebug("Cache size : " + Str(CacheSize/1024) + " / CacheLimit : " + Str(CacheLimit/1024), 5)
|
||||||
If CacheSize > CacheLimit
|
If CacheSize > CacheLimit
|
||||||
PBMap\MemoryCacheManagement = #True
|
;PBMap\MemoryCacheManagement = #True
|
||||||
MyDebug(" Cache full. Trying cache cleaning", 5)
|
MyDebug(" Cache full. Trying cache cleaning", 5)
|
||||||
ResetList(PBMap\MemCache\ImagesTimeStack())
|
ResetList(PBMap\MemCache\ImagesTimeStack())
|
||||||
; Try to free half the cache memory (one pass)
|
; Try to free half the cache memory (one pass)
|
||||||
@@ -1061,9 +1071,10 @@ Module PBMap
|
|||||||
; Is the loading over
|
; Is the loading over
|
||||||
If PBMap\MemCache\Images(CacheMapKey)\Tile = -1
|
If PBMap\MemCache\Images(CacheMapKey)\Tile = -1
|
||||||
MyDebug(" Delete " + CacheMapKey, 5)
|
MyDebug(" Delete " + CacheMapKey, 5)
|
||||||
If IsImage(PBMap\MemCache\Images(CacheMapKey)\nImage)
|
If PBMap\MemCache\Images(CacheMapKey)\nImage;IsImage(PBMap\MemCache\Images(CacheMapKey)\nImage)
|
||||||
FreeImage(PBMap\MemCache\Images(CacheMapKey)\nImage)
|
FreeImage(PBMap\MemCache\Images(CacheMapKey)\nImage)
|
||||||
MyDebug(" and free image nb " + Str(PBMap\MemCache\Images(CacheMapKey)\nImage), 5)
|
MyDebug(" and free image nb " + Str(PBMap\MemCache\Images(CacheMapKey)\nImage), 5)
|
||||||
|
PBMap\MemCache\Images(CacheMapKey)\nImage = 0
|
||||||
EndIf
|
EndIf
|
||||||
DeleteMapElement(PBMap\MemCache\Images(), CacheMapKey)
|
DeleteMapElement(PBMap\MemCache\Images(), CacheMapKey)
|
||||||
DeleteElement(PBMap\MemCache\ImagesTimeStack(), 1)
|
DeleteElement(PBMap\MemCache\ImagesTimeStack(), 1)
|
||||||
@@ -1079,14 +1090,13 @@ Module PBMap
|
|||||||
EndIf
|
EndIf
|
||||||
CacheSize = MapSize(PBMap\MemCache\Images()) * Pow(PBMap\TileSize, 2) * 4 ; Size of a tile = TileSize * TileSize * 4 bytes (RGBA)
|
CacheSize = MapSize(PBMap\MemCache\Images()) * Pow(PBMap\TileSize, 2) * 4 ; Size of a tile = TileSize * TileSize * 4 bytes (RGBA)
|
||||||
Wend
|
Wend
|
||||||
PBMap\MemoryCacheManagement = #False
|
;PBMap\MemoryCacheManagement = #False
|
||||||
MyDebug(" New cache size : " + Str(CacheSize/1024) + " / CacheLimit : " + Str(CacheLimit/1024), 5)
|
MyDebug(" New cache size : " + Str(CacheSize/1024) + " / CacheLimit : " + Str(CacheLimit/1024), 5)
|
||||||
If CacheSize > CacheLimit
|
If CacheSize > CacheLimit
|
||||||
MyDebug(" Cache cleaning unsuccessfull, can't add new tiles.", 5)
|
MyDebug(" Cache cleaning unsuccessfull, can't add new tiles.", 5)
|
||||||
ProcedureReturn #False
|
|
||||||
EndIf
|
EndIf
|
||||||
EndIf
|
EndIf
|
||||||
ProcedureReturn #True
|
SignalSemaphore(PBMap\ResourceAccessSemaphore) ; resourceAccess.V(); // release resource access for next reader/writer
|
||||||
EndProcedure
|
EndProcedure
|
||||||
|
|
||||||
Procedure.i GetTileFromHDD(CacheFile.s)
|
Procedure.i GetTileFromHDD(CacheFile.s)
|
||||||
@@ -1104,7 +1114,7 @@ Module PBMap
|
|||||||
EndIf
|
EndIf
|
||||||
; Everything is OK, loads the file
|
; Everything is OK, loads the file
|
||||||
nImage = LoadImage(#PB_Any, CacheFile)
|
nImage = LoadImage(#PB_Any, CacheFile)
|
||||||
If nImage And IsImage(nImage)
|
If nImage
|
||||||
MyDebug(" Success loading " + CacheFile + " as nImage " + Str(nImage), 3)
|
MyDebug(" Success loading " + CacheFile + " as nImage " + Str(nImage), 3)
|
||||||
ProcedureReturn nImage
|
ProcedureReturn nImage
|
||||||
Else
|
Else
|
||||||
@@ -1148,15 +1158,26 @@ Module PBMap
|
|||||||
|
|
||||||
;-*** These are threaded
|
;-*** These are threaded
|
||||||
|
|
||||||
Threaded Progress = 0, Size = 0
|
Threaded Progress = 0, Size = 0, Quit.i = #False
|
||||||
|
|
||||||
Procedure GetImageThread(*Tile.Tile)
|
Procedure GetImageThread(*Tile.Tile)
|
||||||
|
With PBMap
|
||||||
|
WaitSemaphore(\ServiceQueueSemaphore) ;serviceQueue.P(); // wait in line to be serviced
|
||||||
|
WaitSemaphore(\ReadCountAccessSemaphore) ;readCountAccess.P(); // request exclusive access to readCount
|
||||||
|
If \ReadCount = 0 ;If (readCount == 0) // If there are no readers already reading:
|
||||||
|
WaitSemaphore(\ResourceAccessSemaphore) ; resourceAccess.P(); // request resource access for readers (writers blocked)
|
||||||
|
EndIf
|
||||||
|
\ReadCount + 1 ;readCount++; // update count of active readers
|
||||||
|
SignalSemaphore(\ServiceQueueSemaphore) ;serviceQueue.V(); // let next in line be serviced
|
||||||
|
SignalSemaphore(\ReadCountAccessSemaphore) ;readCountAccess.V(); // release access to readCount
|
||||||
|
EndWith
|
||||||
MyDebug("Thread starting for image " + *Tile\CacheFile + "(" + *Tile\key + ")", 5)
|
MyDebug("Thread starting for image " + *Tile\CacheFile + "(" + *Tile\key + ")", 5)
|
||||||
; Waits for a free download slot
|
; Waits for a free download slot
|
||||||
LockMutex(PBMap\DownloadSlotsMutex)
|
LockMutex(PBMap\DownloadSlotsMutex)
|
||||||
While PBMap\DownloadSlots >= PBMap\Options\MaxDownloadSlots
|
While PBMap\DownloadSlots >= PBMap\Options\MaxDownloadSlots
|
||||||
UnlockMutex(PBMap\DownloadSlotsMutex)
|
UnlockMutex(PBMap\DownloadSlotsMutex)
|
||||||
If ElapsedMilliseconds() - *Tile\Time > 10000
|
If ElapsedMilliseconds() - *Tile\Time > 10000
|
||||||
|
*Tile\Size = 0 ; \Size = 0 signals that the download has failed
|
||||||
MyDebug(" Thread for image " + *Tile\CacheFile + " canceled after 10 seconds waiting for a slot.", 5)
|
MyDebug(" Thread for image " + *Tile\CacheFile + " canceled after 10 seconds waiting for a slot.", 5)
|
||||||
PostEvent(#PB_Event_Gadget, PBMap\Window, PBmap\Gadget, #PB_MAP_TILE_CLEANUP, *Tile) ; To free memory outside the thread
|
PostEvent(#PB_Event_Gadget, PBMap\Window, PBmap\Gadget, #PB_MAP_TILE_CLEANUP, *Tile) ; To free memory outside the thread
|
||||||
ProcedureReturn #False
|
ProcedureReturn #False
|
||||||
@@ -1170,57 +1191,57 @@ Module PBMap
|
|||||||
*Tile\Download = ReceiveHTTPFile(*Tile\URL, *Tile\CacheFile, #PB_HTTP_Asynchronous)
|
*Tile\Download = ReceiveHTTPFile(*Tile\URL, *Tile\CacheFile, #PB_HTTP_Asynchronous)
|
||||||
If *Tile\Download
|
If *Tile\Download
|
||||||
Repeat
|
Repeat
|
||||||
;If PBMap\MemoryCacheManagement = #False ; Wait until cache cleaning is done
|
;If PBMap\MemoryCacheManagement = #False ; Wait until cache cleaning is done ;TODO
|
||||||
Progress = HTTPProgress(*Tile\Download)
|
Progress = HTTPProgress(*Tile\Download)
|
||||||
Select Progress
|
Select Progress
|
||||||
Case #PB_Http_Success
|
Case #PB_Http_Success
|
||||||
*Tile\Size = FinishHTTP(*Tile\Download) ; \Size signals that the download is OK
|
*Tile\Size = FinishHTTP(*Tile\Download) ; \Size signals that the download is OK
|
||||||
MyDebug(" Thread for image " + *Tile\CacheFile + " finished. Size : " + Str(Size), 5)
|
MyDebug(" Thread for image " + *Tile\CacheFile + " finished. Size : " + Str(Size), 5)
|
||||||
LockMutex(PBMap\DownloadSlotsMutex)
|
Quit = #True
|
||||||
PBMap\DownloadSlots - 1
|
Case #PB_Http_Failed
|
||||||
UnlockMutex(PBMap\DownloadSlotsMutex)
|
FinishHTTP(*Tile\Download)
|
||||||
PostEvent(#PB_Event_Gadget, PBMap\Window, PBmap\Gadget, #PB_MAP_TILE_CLEANUP, *Tile) ; To free memory outside the thread
|
*Tile\Size = 0 ; \Size = 0 signals that the download has failed
|
||||||
ProcedureReturn #True
|
MyDebug(" Thread for image " + *Tile\CacheFile + " failed.", 5)
|
||||||
Case #PB_Http_Failed
|
Quit = #True
|
||||||
FinishHTTP(*Tile\Download)
|
Case #PB_Http_Aborted
|
||||||
*Tile\Size = 0 ; \Size = 0 signals that the download has failed
|
FinishHTTP(*Tile\Download)
|
||||||
MyDebug(" Thread for image " + *Tile\CacheFile + " failed.", 5)
|
*Tile\Size = 0 ; \Size = 0 signals that the download has failed
|
||||||
LockMutex(PBMap\DownloadSlotsMutex)
|
MyDebug(" Thread for image " + *Tile\CacheFile + " aborted.", 5)
|
||||||
PBMap\DownloadSlots - 1
|
Quit = #True
|
||||||
UnlockMutex(PBMap\DownloadSlotsMutex)
|
Default
|
||||||
PostEvent(#PB_Event_Gadget, PBMap\Window, PBmap\Gadget, #PB_MAP_TILE_CLEANUP, *Tile) ; To free memory outside the thread
|
MyDebug(" Thread for image " + *Tile\CacheFile + " downloading " + Str(Progress) + " bytes", 5)
|
||||||
ProcedureReturn #False
|
If ElapsedMilliseconds() - *Tile\Time > 10000
|
||||||
Case #PB_Http_Aborted
|
MyDebug(" Thread for image " + *Tile\CacheFile + " canceled after 10 seconds.", 5)
|
||||||
FinishHTTP(*Tile\Download)
|
AbortHTTP(*Tile\Download)
|
||||||
*Tile\Size = 0 ; \Size = 0 signals that the download has failed
|
EndIf
|
||||||
MyDebug(" Thread for image " + *Tile\CacheFile + " aborted.", 5)
|
EndSelect
|
||||||
LockMutex(PBMap\DownloadSlotsMutex)
|
|
||||||
PBMap\DownloadSlots - 1
|
|
||||||
UnlockMutex(PBMap\DownloadSlotsMutex)
|
|
||||||
PostEvent(#PB_Event_Gadget, PBMap\Window, PBmap\Gadget, #PB_MAP_TILE_CLEANUP, *Tile) ; To free memory outside the thread
|
|
||||||
ProcedureReturn #False
|
|
||||||
Default
|
|
||||||
MyDebug(" Thread for image " + *Tile\CacheFile + " downloading " + Str(Progress) + " bytes", 5)
|
|
||||||
If ElapsedMilliseconds() - *Tile\Time > 10000
|
|
||||||
MyDebug(" Thread for image " + *Tile\CacheFile + " canceled after 10 seconds.", 5)
|
|
||||||
AbortHTTP(*Tile\Download)
|
|
||||||
EndIf
|
|
||||||
EndSelect
|
|
||||||
;EndIf
|
;EndIf
|
||||||
Delay(500) ; Frees CPU
|
Delay(500) ; Frees CPU
|
||||||
ForEver
|
Until Quit
|
||||||
|
LockMutex(PBMap\DownloadSlotsMutex)
|
||||||
|
PBMap\DownloadSlots - 1
|
||||||
|
UnlockMutex(PBMap\DownloadSlotsMutex)
|
||||||
|
PostEvent(#PB_Event_Gadget, PBMap\Window, PBmap\Gadget, #PB_MAP_TILE_CLEANUP, *Tile) ; To free memory outside the thread
|
||||||
EndIf
|
EndIf
|
||||||
|
With PBMap
|
||||||
|
WaitSemaphore(\ReadCountAccessSemaphore) ;readCountAccess.P(); // request exclusive access to readCount
|
||||||
|
\ReadCount - 1 ;readCount--; // update count of active readers
|
||||||
|
If \ReadCount = 0 ;If (readCount == 0) // If there are no readers left:
|
||||||
|
SignalSemaphore(\ResourceAccessSemaphore) ; resourceAccess.V(); // release resource access for all
|
||||||
|
EndIf
|
||||||
|
SignalSemaphore(\ReadCountAccessSemaphore) ;readCountAccess.V();
|
||||||
|
EndWith
|
||||||
EndProcedure
|
EndProcedure
|
||||||
|
|
||||||
;-***
|
;-***
|
||||||
|
|
||||||
Procedure.i GetTile(key.s, URL.s, CacheFile.s)
|
Procedure.i GetTile(key.s, URL.s, CacheFile.s)
|
||||||
; Try to find the tile in memory cache
|
; Try to find the tile in memory cache
|
||||||
Protected *timg.ImgMemCach = FindMapElement(PBMap\MemCache\Images(), key)
|
Protected *timg.ImgMemCach = FindMapElement(PBMap\MemCache\Images(), key)
|
||||||
If *timg
|
If *timg
|
||||||
MyDebug("Key : " + key + " found in memory cache", 4)
|
MyDebug("Key : " + key + " found in memory cache", 4)
|
||||||
; Is the associated image already been loaded in memory ?
|
; Is the associated image already been loaded in memory ?
|
||||||
If *timg\nImage And IsImage(*timg\nImage)
|
If *timg\nImage
|
||||||
; Yes, returns the image's nb
|
; Yes, returns the image's nb
|
||||||
MyDebug(" as image " + *timg\nImage, 4)
|
MyDebug(" as image " + *timg\nImage, 4)
|
||||||
*timg\Tile = -1
|
*timg\Tile = -1
|
||||||
@@ -1378,7 +1399,7 @@ Module PBMap
|
|||||||
EndSelect
|
EndSelect
|
||||||
EndWith
|
EndWith
|
||||||
*timg = GetTile(key, URL, CacheFile)
|
*timg = GetTile(key, URL, CacheFile)
|
||||||
If *timg And *timg\nImage And IsImage(*timg\nImage)
|
If *timg And *timg\nImage
|
||||||
MovePathCursor(px, py)
|
MovePathCursor(px, py)
|
||||||
If *timg\Alpha <= 224
|
If *timg\Alpha <= 224
|
||||||
DrawVectorImage(ImageID(*timg\nImage), *timg\Alpha * PBMap\Layers()\Alpha)
|
DrawVectorImage(ImageID(*timg\nImage), *timg\Alpha * PBMap\Layers()\Alpha)
|
||||||
@@ -2457,11 +2478,14 @@ Module PBMap
|
|||||||
PBMap\Redraw = #True
|
PBMap\Redraw = #True
|
||||||
Case #PB_MAP_RETRY
|
Case #PB_MAP_RETRY
|
||||||
PBMap\Redraw = #True
|
PBMap\Redraw = #True
|
||||||
|
;- Tile cleanup
|
||||||
Case #PB_MAP_TILE_CLEANUP
|
Case #PB_MAP_TILE_CLEANUP
|
||||||
*Tile = EventData()
|
*Tile = EventData()
|
||||||
key = *Tile\key
|
key = *Tile\key
|
||||||
; After a Web tile loading thread, clean the tile structure memory, see GetImageThread()
|
; After a Web tile loading thread, clean the tile structure memory, see GetImageThread()
|
||||||
*Tile\Download = 0
|
*Tile\Download = 0
|
||||||
|
;Debug key + " cleanup event"
|
||||||
|
|
||||||
If *Tile\Size ; <> 0
|
If *Tile\Size ; <> 0
|
||||||
FreeMemory(PBMap\MemCache\Images(key)\Tile) ; Frees the data needed for the thread
|
FreeMemory(PBMap\MemCache\Images(key)\Tile) ; Frees the data needed for the thread
|
||||||
PBMap\MemCache\Images(key)\Tile = -1 ; Clears the data ptr, and says that the web loading thread has finished successfully
|
PBMap\MemCache\Images(key)\Tile = -1 ; Clears the data ptr, and says that the web loading thread has finished successfully
|
||||||
@@ -2469,6 +2493,7 @@ Module PBMap
|
|||||||
FreeMemory(PBMap\MemCache\Images(key)\Tile) ; Frees the data needed for the thread
|
FreeMemory(PBMap\MemCache\Images(key)\Tile) ; Frees the data needed for the thread
|
||||||
PBMap\MemCache\Images(key)\Tile = 0 ; Clears the data ptr, and says that the web loading thread has finished unsuccessfully
|
PBMap\MemCache\Images(key)\Tile = 0 ; Clears the data ptr, and says that the web loading thread has finished unsuccessfully
|
||||||
EndIf
|
EndIf
|
||||||
|
;Debug "=" + Str(PBMap\MemCache\Images(key)\Tile)
|
||||||
;Protected timg = PBMap\MemCache\Images(key)\Tile\nImage ; Get this new tile image nb
|
;Protected timg = PBMap\MemCache\Images(key)\Tile\nImage ; Get this new tile image nb
|
||||||
;PBMap\MemCache\Images(key)\nImage = timg ; Stores it in the cache using the key
|
;PBMap\MemCache\Images(key)\nImage = timg ; Stores it in the cache using the key
|
||||||
PBMap\ThreadsNB - 1
|
PBMap\ThreadsNB - 1
|
||||||
@@ -2479,8 +2504,8 @@ Module PBMap
|
|||||||
; Redraws at regular intervals
|
; Redraws at regular intervals
|
||||||
Procedure TimerEvents()
|
Procedure TimerEvents()
|
||||||
If EventTimer() = PBMap\Timer And (PBMap\Redraw Or PBMap\Dirty)
|
If EventTimer() = PBMap\Timer And (PBMap\Redraw Or PBMap\Dirty)
|
||||||
MemoryCacheManagement()
|
|
||||||
Drawing()
|
Drawing()
|
||||||
|
PBMap\MemoryCacheManagementThread = CreateThread(@MemoryCacheManagement(), Null)
|
||||||
EndIf
|
EndIf
|
||||||
EndProcedure
|
EndProcedure
|
||||||
|
|
||||||
@@ -2507,7 +2532,7 @@ Module PBMap
|
|||||||
|
|
||||||
Procedure Quit()
|
Procedure Quit()
|
||||||
PBMap\Drawing\End = #True
|
PBMap\Drawing\End = #True
|
||||||
PBMap\MemoryCacheManagement = #True ; Tells web loading threads to pause
|
;PBMap\MemoryCacheManagement = #True ; Tells web loading threads to pause
|
||||||
; Wait for loading threads to finish nicely. Passed 2 seconds, kills them.
|
; Wait for loading threads to finish nicely. Passed 2 seconds, kills them.
|
||||||
Protected TimeCounter = ElapsedMilliseconds()
|
Protected TimeCounter = ElapsedMilliseconds()
|
||||||
Repeat
|
Repeat
|
||||||
@@ -2543,6 +2568,9 @@ Module PBMap
|
|||||||
PBMap\Timer = 1
|
PBMap\Timer = 1
|
||||||
PBMap\Mode = #MODE_DEFAULT
|
PBMap\Mode = #MODE_DEFAULT
|
||||||
PBMap\DownloadSlotsMutex = CreateMutex()
|
PBMap\DownloadSlotsMutex = CreateMutex()
|
||||||
|
PBMap\ResourceAccessSemaphore = CreateSemaphore(1)
|
||||||
|
PBMap\ReadCountAccessSemaphore = CreateSemaphore(1)
|
||||||
|
PBMap\ServiceQueueSemaphore = CreateSemaphore(1)
|
||||||
If PBMap\DownloadSlotsMutex = #False
|
If PBMap\DownloadSlotsMutex = #False
|
||||||
MyDebug("Cannot create a mutex", 0)
|
MyDebug("Cannot create a mutex", 0)
|
||||||
End
|
End
|
||||||
@@ -2862,8 +2890,8 @@ CompilerIf #PB_Compiler_IsMainFile
|
|||||||
CompilerEndIf
|
CompilerEndIf
|
||||||
|
|
||||||
; IDE Options = PureBasic 5.60 (Windows - x64)
|
; IDE Options = PureBasic 5.60 (Windows - x64)
|
||||||
; CursorPosition = 1047
|
; CursorPosition = 1231
|
||||||
; FirstLine = 1047
|
; FirstLine = 1151
|
||||||
; Folding = -------------------
|
; Folding = -------------------
|
||||||
; EnableThread
|
; EnableThread
|
||||||
; EnableXP
|
; EnableXP
|
||||||
|
Reference in New Issue
Block a user