11using System . Collections . Concurrent ;
2- using System . Collections . Immutable ;
32using Intersect . Client . Framework . GenericClasses ;
43using Intersect . Core ;
54using Intersect . Framework . Collections ;
5+ using Intersect . Framework . Core ;
6+ using Intersect . Framework . SystemInformation ;
67using Microsoft . Extensions . Logging ;
78
89namespace Intersect . Client . Framework . Graphics ;
910
1011public abstract partial class GameRenderer : IGameRenderer , ITextHelper
1112{
13+ // To test the unloading system, lower InactiveTimeCutoff to 5000 (5 seconds) and raise
14+ // MinimumAvailableVRAM to above your GPU RAM (if using Nvidia) or system RAM (if not using Nvidia)
15+
16+ /// <summary>
17+ /// 30 seconds in milliseconds
18+ /// </summary>
19+ private const long InactiveTimeCutoff = 30_000 ;
20+
1221 /// <summary>
13- /// 128MiB, which is 2x an uncompressed RGBA 4096x4096 (and half of an uncompressed 8192x8192)
22+ /// 128MiB, which is 2x 4096px² atlases, half an 8192px² atlas
1423 /// </summary>
15- private const int MinimumAvailableVRAM = 134217728 ;
24+ private const long MinimumAvailableVRAM = 134217728 ;
25+
26+ /// <summary>
27+ /// 30 minutes in milliseconds
28+ /// </summary>
29+ private const long StaleTimeCutoff = 30 * 60 * 1000 ;
30+
31+ /// <summary>
32+ /// 1GiB, which is 16x 4096px² atlases, 4x 8192px² atlases, and 1x 16384px² atlas
33+ /// </summary>
34+ private const long StaleVRAMCutoff = MinimumAvailableVRAM * 8 ;
35+
36+ /// <summary>
37+ /// 6 hours in milliseconds
38+ /// </summary>
39+ private const long UnusedTimeCutoff = 6 * 60 * 60 * 1000 ;
40+
41+ /// <summary>
42+ /// 8GiB, which is 128x 4096px² atlases, 32x 8192px² atlases, and 8x 16384px² atlases
43+ /// </summary>
44+ private const long UnusedVRAMCutoff = StaleVRAMCutoff * 8 ;
1645
1746 private readonly ConcurrentDictionary < string , ScreenshotRequest > _screenshotRequestLookup = [ ] ;
1847 private readonly ConcurrentConditionalDequeue < ScreenshotRequest > _screenshotRequests = [ ] ;
1948
2049 private readonly HashSet < IGameTexture > _textures = [ ] ;
21- private readonly List < IGameTexture > _texturesSortedByExpiration = [ ] ;
50+ private readonly List < IGameTexture > _texturesSortedByAccessTime = [ ] ;
2251
2352 protected float _scale = 1.0f ;
2453
@@ -47,10 +76,6 @@ public IGameTexture[] Textures
4776
4877 public ulong TextureAllocations { get ; private set ; }
4978
50- public abstract long AvailableMemory { get ; }
51-
52- public abstract long TotalMemory { get ; }
53-
5479 public bool HasOverrideResolution => OverrideResolution != Resolution . Empty ;
5580
5681 public bool HasScreenshotRequests => _screenshotRequests . Count > 0 ;
@@ -246,7 +271,7 @@ protected internal void MarkAllocated(IGameTexture texture)
246271
247272 if ( ! texture . IsPinned )
248273 {
249- _texturesSortedByExpiration . AddSorted ( texture ) ;
274+ _texturesSortedByAccessTime . AddSorted ( texture ) ;
250275 }
251276
252277 if ( texture is IGameRenderTexture )
@@ -263,7 +288,7 @@ protected internal void MarkFreed(IGameTexture texture)
263288 {
264289 if ( ! texture . IsPinned )
265290 {
266- _texturesSortedByExpiration . Remove ( texture ) ;
291+ _texturesSortedByAccessTime . Remove ( texture ) ;
267292 }
268293
269294 if ( texture is IGameRenderTexture )
@@ -290,9 +315,9 @@ protected internal void MarkFreed(IGameTexture texture)
290315 TextureFreed ? . Invoke ( this , new TextureEventArgs ( texture ) ) ;
291316 }
292317
293- internal void UpdateExpiration ( IGameTexture gameTexture )
318+ internal void UpdateAccessTime ( IGameTexture gameTexture )
294319 {
295- _texturesSortedByExpiration . Resort ( gameTexture ) ;
320+ _texturesSortedByAccessTime . Resort ( gameTexture ) ;
296321 }
297322
298323 public abstract void Init ( ) ;
@@ -307,17 +332,112 @@ internal void UpdateExpiration(IGameTexture gameTexture)
307332
308333 protected abstract bool RecreateSpriteBatch ( ) ;
309334
335+ public abstract long UsedMemory { get ; }
336+
310337 /// <summary>
311338 /// Called when the frame is done being drawn, generally used to finally display the content to the screen.
312339 /// </summary>
313340 public void End ( )
314341 {
315342 DoEnd ( ) ;
316343
317- while ( AvailableMemory < MinimumAvailableVRAM && _texturesSortedByExpiration . FirstOrDefault ( ) is { } texture )
344+ var usedVRAM = UsedMemory ;
345+ var availableVRAM = PlatformStatistics . AvailableGPUMemory ;
346+ var totalVRAM = PlatformStatistics . TotalGPUMemory ;
347+
348+ if ( availableVRAM < 0 )
349+ {
350+ if ( totalVRAM < 0 )
351+ {
352+ availableVRAM = PlatformStatistics . AvailableSystemMemory ;
353+ }
354+ else
355+ {
356+ availableVRAM = totalVRAM - usedVRAM ;
357+ }
358+ }
359+
360+ if ( totalVRAM < 0 )
318361 {
319- _texturesSortedByExpiration . Remove ( texture ) ;
320- texture . Unload ( ) ;
362+ totalVRAM = availableVRAM + usedVRAM ;
363+ }
364+
365+ var now = Timing . Global . MillisecondsUtc ;
366+
367+ var unusedCutoffTime = now - UnusedTimeCutoff ;
368+ var staleCutoffTime = now - StaleTimeCutoff ;
369+ var inactiveCutoffTime = now - InactiveTimeCutoff ;
370+
371+ var unusedCutoffSize = UnusedVRAMCutoff < totalVRAM ? UnusedVRAMCutoff : totalVRAM - MinimumAvailableVRAM ;
372+ var staleCutoffSize = StaleVRAMCutoff < totalVRAM ? StaleVRAMCutoff : totalVRAM - MinimumAvailableVRAM ;
373+
374+ while ( _texturesSortedByAccessTime . FirstOrDefault ( ) is { } texture )
375+ {
376+ var accessTime = texture . AccessTime ;
377+ if ( availableVRAM > MinimumAvailableVRAM )
378+ {
379+ if ( availableVRAM > staleCutoffSize )
380+ {
381+ if ( availableVRAM > unusedCutoffSize )
382+ {
383+ // Don't unload anything if we've still got (at the time of writing) 8GiB of VRAM
384+ break ;
385+ }
386+
387+ if ( texture . AccessTime > unusedCutoffTime )
388+ {
389+ // It has been used in the last 6 hours, don't unload
390+ break ;
391+ }
392+
393+ // We're below our unused VRAM threshold and the texture is considered unused, unload it
394+ ApplicationContext . CurrentContext . Logger . LogTrace (
395+ "Unloading {TextureName} because it's unused and we have {AvailableVRAM} bytes of VRAM available (cutoff is {CutoffVRAM})" ,
396+ texture . Name ,
397+ availableVRAM ,
398+ unusedCutoffSize
399+ ) ;
400+ }
401+ else if ( texture . AccessTime > staleCutoffTime )
402+ {
403+ // We are below our stale VRAM threshold, but the texture is not yet considered stale, don't unload
404+ }
405+ else
406+ {
407+ // We're below our stale VRAM threshold and the texture is considered stale, unload it
408+ ApplicationContext . CurrentContext . Logger . LogTrace (
409+ "Unloading {TextureName} because it's stale and we have {AvailableVRAM} bytes of VRAM available (cutoff is {CutoffVRAM})" ,
410+ texture . Name ,
411+ availableVRAM ,
412+ staleCutoffSize
413+ ) ;
414+ }
415+ }
416+ else if ( accessTime > inactiveCutoffTime )
417+ {
418+ // We are below our minimum VRAM threshold, but the texture is still active, don't unload
419+ break ;
420+ }
421+ else
422+ {
423+ ApplicationContext . CurrentContext . Logger . LogTrace (
424+ "Unloading {TextureName} because it's inactive and we have {AvailableVRAM} bytes of VRAM available (cutoff is {CutoffVRAM})" ,
425+ texture . Name ,
426+ availableVRAM ,
427+ MinimumAvailableVRAM
428+ ) ;
429+ }
430+
431+ // If we've reached this point in the code one of the following is true:
432+ // - We are below the unused memory threshold, and the texture is considered to be unused
433+ // - We are below the stale memory threshold, and the texture is considered to be stale
434+ // - We are below the minimum memory threshold, and the texture is considered to be inactive
435+ // In any of these three cases we want to unload the texture
436+
437+ if ( texture . Unload ( ) )
438+ {
439+ _texturesSortedByAccessTime . Remove ( texture ) ;
440+ }
321441 }
322442 }
323443
0 commit comments