OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / addons / keet_social / keet_texture_cache.gd

154 lines · gd ref:
class_name KeetTextureCache
extends RefCounted
## LRU texture cache for decoded WebP previews.
## Decodes base64 WebP data into ImageTexture with eviction.

const MAX_ENTRIES: int = 100
const MAX_BYTES: int = 50 * 1024 * 1024  # 50 MB

var _cache: Dictionary = {}  # key -> {tex: ImageTexture, size: int, access: int}
var _order: Array[String] = []  # LRU order (most recent at end)
var _total_bytes: int = 0
var _access_counter: int = 0


## Decode base64 WebP data and cache as ImageTexture.
## Returns null on decode failure.
func decode_and_cache(key: String, base64_webp: String) -> ImageTexture:
	if _cache.has(key):
		_touch(key)
		return _cache[key].tex

	var raw: PackedByteArray = Marshalls.base64_to_raw(base64_webp)
	if raw.is_empty():
		return null

	var img := Image.new()
	var err: Error = img.load_webp_from_buffer(raw)
	if err != OK:
		# Try PNG fallback (some previews may be PNG)
		err = img.load_png_from_buffer(raw)
		if err != OK:
			# Try JPEG fallback
			err = img.load_jpg_from_buffer(raw)
			if err != OK:
				return null

	var tex := ImageTexture.create_from_image(img)
	var entry_size: int = raw.size()

	# Evict until we have room
	while _total_bytes + entry_size > MAX_BYTES and _order.size() > 0:
		_evict_oldest()
	while _order.size() >= MAX_ENTRIES:
		_evict_oldest()

	_access_counter += 1
	_cache[key] = {"tex": tex, "size": entry_size, "access": _access_counter}
	_order.append(key)
	_total_bytes += entry_size

	return tex


## Decode raw image bytes (WebP/PNG/JPEG) and cache as ImageTexture.
## content_type hint avoids trying wrong formats (which spams Godot error log).
## Returns null on decode failure.
func decode_bytes_and_cache(key: String, raw: PackedByteArray, content_type: String = "") -> ImageTexture:
	if _cache.has(key):
		_touch(key)
		return _cache[key].tex

	if raw.is_empty():
		return null

	var img := Image.new()
	var err: Error = _load_image_smart(img, raw, content_type)
	if err != OK:
		return null

	var tex := ImageTexture.create_from_image(img)
	var entry_size: int = raw.size()

	while _total_bytes + entry_size > MAX_BYTES and _order.size() > 0:
		_evict_oldest()
	while _order.size() >= MAX_ENTRIES:
		_evict_oldest()

	_access_counter += 1
	_cache[key] = {"tex": tex, "size": entry_size, "access": _access_counter}
	_order.append(key)
	_total_bytes += entry_size

	return tex


## Get a cached texture by key, or null if not cached.
func get_cached(key: String) -> ImageTexture:
	if _cache.has(key):
		_touch(key)
		return _cache[key].tex
	return null


## Check if a key is cached.
func has_key(key: String) -> bool:
	return _cache.has(key)


## Clear all cached textures.
func clear() -> void:
	_cache.clear()
	_order.clear()
	_total_bytes = 0
	_access_counter = 0


## Number of cached entries.
func count() -> int:
	return _cache.size()


func _touch(key: String) -> void:
	_access_counter += 1
	if _cache.has(key):
		_cache[key].access = _access_counter
	var idx: int = _order.find(key)
	if idx >= 0:
		_order.remove_at(idx)
		_order.append(key)


func _evict_oldest() -> void:
	if _order.is_empty():
		return
	var oldest_key: String = _order[0]
	_order.remove_at(0)
	if _cache.has(oldest_key):
		_total_bytes -= _cache[oldest_key].size
		_cache.erase(oldest_key)


## Try correct image format first based on content_type, then fall back.
## This avoids Godot's internal error spam from trying wrong formats.
func _load_image_smart(img: Image, raw: PackedByteArray, content_type: String) -> Error:
	if "jpeg" in content_type or "jpg" in content_type:
		var err: Error = img.load_jpg_from_buffer(raw)
		if err == OK: return OK
		err = img.load_webp_from_buffer(raw)
		if err == OK: return OK
		return img.load_png_from_buffer(raw)
	elif "png" in content_type:
		var err: Error = img.load_png_from_buffer(raw)
		if err == OK: return OK
		err = img.load_webp_from_buffer(raw)
		if err == OK: return OK
		return img.load_jpg_from_buffer(raw)
	else:
		# Default: WebP first (most keet previews are WebP)
		var err: Error = img.load_webp_from_buffer(raw)
		if err == OK: return OK
		err = img.load_png_from_buffer(raw)
		if err == OK: return OK
		return img.load_jpg_from_buffer(raw)