Skip to content

Commit

Permalink
Fixes and tests managedTextureMemory going crazy in some cases and op…
Browse files Browse the repository at this point in the history
…timizes FastSmallSet (#1729)

* Fixes and tests managedTextureMemory going crazy in some cases

* Optimize FastSmallSet
  • Loading branch information
soywiz authored Jun 28, 2023
1 parent c8f237a commit ba4c7fc
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 15 deletions.
17 changes: 13 additions & 4 deletions kds/src/commonMain/kotlin/korlibs/datastructure/FastSmallSet.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package korlibs.datastructure

class FastSmallSet<T> : AbstractMutableSet<T>() {
@PublishedApi internal val _items = FastArrayList<T>()
val items: List<T> get() = _items
@PublishedApi internal val _items = LinkedHashSet<T>()
//@PublishedApi internal val _items = FastArrayList<T>()
//val itemsToIndex = LinkedHashMap<T, Int>()
//val items: List<T> get() = _items

override val size: Int get() = _items.size
override fun iterator(): MutableIterator<T> = _items.iterator()
Expand All @@ -13,6 +15,7 @@ class FastSmallSet<T> : AbstractMutableSet<T>() {

override fun add(element: T): Boolean {
if (element in this) return false
//itemsToIndex[element] = _items.size
_items.add(element)
return true
}
Expand All @@ -21,12 +24,16 @@ class FastSmallSet<T> : AbstractMutableSet<T>() {
fast0 = null
fast1 = null
fast2 = null
return _items.remove(element)
//val index = itemsToIndex.remove(element) ?: return false
//_items.removeAt(index)
_items.remove(element)
return true
}

override operator fun contains(element: T): Boolean {
if (element === fast0 || element === fast1 || element === fast0) return true
val result = element in _items
//val result = element in itemsToIndex
if (result) {
fast1 = fast0
fast2 = fast1
Expand All @@ -36,13 +43,15 @@ class FastSmallSet<T> : AbstractMutableSet<T>() {
}

override fun clear() {
//itemsToIndex.clear()
_items.clear()
fast0 = null
fast1 = null
fast2 = null
}

inline fun fastForEach(block: (T) -> Unit) {
_items.fastForEach(block)
_items.forEach { block(it) }
//_items.fastForEach(block)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import korlibs.image.bitmap.*
import korlibs.io.lang.*
import korlibs.math.geom.*
import korlibs.math.geom.slice.*
import korlibs.memory.unit.*

/**
* Class in charge of automatically handling [AGTexture] <-> [Bitmap] conversion.
Expand Down Expand Up @@ -39,10 +40,22 @@ class AgBitmapTextureManager(
private val referencedBitmapsSinceGC = FastSmallSet<Bitmap>()
private var referencedBitmaps = FastArrayList<Bitmap>()

val numCachedBitmaps: Int get() = cachedBitmaps.size
val numReferencedBitmapsSinceGC: Int get() = referencedBitmapsSinceGC.size
val numReferencedBitmaps: Int get() = referencedBitmaps.size

override fun toString(): String = toStringStats()

fun toStringStats(): String =
"AgBitmapTextureManager(numCachedBitmaps=$numCachedBitmaps, numReferencedBitmapsSinceGC=$numReferencedBitmapsSinceGC, numReferencedBitmaps=$numReferencedBitmaps, maxCachedMemory=${ByteUnits.fromBytes(maxCachedMemory)}, managedTextureMemory=${ByteUnits.fromBytes(managedTextureMemory)})"

/** Number of frames between each Texture Garbage Collection step */
var framesBetweenGC: Int = 60
var managedTextureMemory: Long = 0L
private set
private set (value) {
//if (field >= 0L && value < 0L) printStackTrace("OLD managedTextureMemory=$field -> $value")
field = value
}
//var framesBetweenGC = 30 * 60 // 30 seconds
//var framesBetweenGC = 360

Expand All @@ -56,13 +69,16 @@ class AgBitmapTextureManager(
var textureBase: TextureBase = TextureBase(null, 0, 0)
val slices = FastIdentityMap<BitmapCoords, TextureCoords>()
fun reset() {
usedMemory = 0
textureBase.base = null
textureBase.version = -1
textureBase.width = 0
textureBase.height = 0
slices.clear()
}
}

override fun toString(): String = "BitmapTextureInfo(textureBase=$textureBase, usedMemory=$usedMemory, slices=${slices.size})"
}

private val textureInfoPool = Pool(reset = { it.reset() }) { BitmapTextureInfo() }
private val bitmapsToTextureBase = FastIdentityMap<Bitmap, BitmapTextureInfo>()
Expand Down Expand Up @@ -96,6 +112,7 @@ class AgBitmapTextureManager(

val textureInfo = bitmapsToTextureBase.getOrPut(bitmap) {
textureInfoPool.alloc().also {
//println("ALLOC TEXTURE_INFO $it")
val base = it.textureBase
base.version = -1
base.base = AGTexture(targetKind = when (bitmap) {
Expand All @@ -116,6 +133,7 @@ class AgBitmapTextureManager(
if (bitmap.contentVersion != base.version) {
base.version = bitmap.contentVersion
// @TODO: Use dirtyRegion to upload only a fragment of the image
//println("OLD VERSION: textureInfo.usedMemory=${textureInfo.usedMemory}")
managedTextureMemory -= textureInfo.usedMemory
try {
base.update(bitmap, bitmap.mipmaps, bitmap.baseMipmapLevel, bitmap.maxMipmapLevel)
Expand Down Expand Up @@ -186,24 +204,25 @@ class AgBitmapTextureManager(
referencedBitmaps.fastForEach { bmp ->
when {
bmp in referencedBitmapsSinceGC -> Unit // Keep normally
maxCachedMemory < this.managedTextureMemory -> removeBitmap(bmp, "GC")
maxCachedMemory < this.managedTextureMemory || managedTextureMemory < 0L -> removeBitmap(bmp, "GC")
else -> cachedBitmaps.add(bmp)
}
}
referencedBitmaps.clear()
referencedBitmaps.addAll(referencedBitmapsSinceGC.items)
referencedBitmaps.addAll(cachedBitmaps.items)
//println("GC: $referencedBitmapsSinceGC")
referencedBitmapsSinceGC.fastForEach { referencedBitmaps.add(it) }
cachedBitmaps.fastForEach { referencedBitmaps.add(it) }
referencedBitmapsSinceGC.clear()
}

@KorgeExperimental
fun removeBitmap(bmp: Bitmap, reason: String) {
val info = bitmapsToTextureBase.getAndRemove(bmp) ?: return
managedTextureMemory -= info.usedMemory
referencedBitmapsSinceGC.remove(bmp)
if (cachedBitmapTextureInfo === info || cachedBitmapTextureInfo2 === info) clearFastCacheAccess()
info.textureBase.close()
textureInfoPool.free(info)
managedTextureMemory -= info.usedMemory
cachedBitmaps.remove(bmp)
//println("AgBitmapTextureManager.removeBitmap[$currentThreadId]:${bmp.size}, reason=$reason, textureInfoPool=${textureInfoPool.itemsInPool},${textureInfoPool.totalAllocatedItems}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class AgBufferManager(
}

fun gc() {
delete(referencedBuffersSinceGC.items)
referencedBuffersSinceGC.fastForEach { delete(it) }
referencedBuffersSinceGC.clear()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package korlibs.korge.render
import korlibs.kgl.KmlGl
import korlibs.kgl.KmlGlProxyLogToString
import korlibs.graphics.gl.*
import korlibs.graphics.log.AGLog
import korlibs.image.bitmap.Bitmap32
import korlibs.image.bitmap.ForcedTexNativeImage
import korlibs.image.bitmap.sliceWithSize
import korlibs.graphics.log.*
import korlibs.image.bitmap.*
import korlibs.image.color.Colors
import korlibs.korge.annotations.*
import kotlin.test.*

@KorgeExperimental
class AgBitmapTextureManagerTest {
val ag = AGLog()
val tm = AgBitmapTextureManager(ag)
Expand Down Expand Up @@ -137,4 +137,29 @@ class AgBitmapTextureManagerTest {
gl.log.joinToString("\n")
)
}

@Test
fun testTotalMemory() {
val texs = AgBitmapTextureManager(AGDummy())
val bitmap = Bitmap32(64, 64)
val bitmapMem = (bitmap.area * 4).toLong()

assertEquals(0L, texs.managedTextureMemory)

texs.getTexture(bitmap.slice())
assertEquals(bitmapMem, texs.managedTextureMemory)

texs.removeBitmap(bitmap, "test")
texs.removeBitmap(bitmap, "test")
assertEquals(0, texs.managedTextureMemory)

texs.getTexture(bitmap.slice())
assertEquals(bitmapMem, texs.managedTextureMemory)

texs.gc()
assertEquals(bitmapMem, texs.managedTextureMemory)

texs.gc()
assertEquals(0L, texs.managedTextureMemory)
}
}

0 comments on commit ba4c7fc

Please sign in to comment.