Download scripts/core/memory_manager.gd from geter62/monkeystudio: direct link, hf CLI and curl.
- Browser
- Download file 5.58 kB
-
https://huggingface.co/geter62/monkeystudio/resolve/main/scripts/core/memory_manager.gd
- Command line
-
hf download hf://geter62/monkeystudio/scripts/core/memory_manager.gd
-
curl -L -o memory_manager.gd https://huggingface.co/geter62/monkeystudio/resolve/main/scripts/core/memory_manager.gd
5.58 kB
| extends Node | |
| ## | |
| ## MemoryManager - central budget authority for low-RAM (4GB) Android devices. | |
| ## | |
| ## Responsibilities: | |
| ## - Track a hard RAM budget for loadable assets and evict (LRU) when exceeded. | |
| ## - Provide lazy `acquire(id)` / `release(id)` reference counting so a heavy | |
| ## feature (hair sim, cloth, sculpt grid) only lives in RAM while in use. | |
| ## - Never hold a strong reference to an evictable asset itself; it holds the | |
| ## *path* and hands out instances, so eviction actually frees memory. | |
| ## | |
| ## Design notes: | |
| ## - Godot is ref-counted; an asset is truly freed only when every strong ref | |
| ## is dropped. Callers MUST call release() so we can drop the cache ref. | |
| ## - We poll OS memory pressure and force-evict before the OOM killer arrives. | |
| ## | |
| signal budget_exceeded(evicted_count: int) | |
| signal memory_warning(used_mb: float, budget_mb: float) | |
| signal asset_loaded(id: String, cost_mb: float) | |
| signal asset_evicted(id: String, freed_mb: float) | |
| ## Total RAM this process is allowed to use for evictable assets. | |
| @export var asset_budget_mb: float = 512.0 | |
| ## Soft threshold (fraction of budget) that triggers a warning. | |
| @export var warn_ratio: float = 0.8 | |
| ## How often (seconds) to poll the OS memory monitor. | |
| @export var poll_interval: float = 1.0 | |
| ## id -> { path, cost_mb, refs, last_used, resource } | |
| var _entries: Dictionary = {} | |
| ## Monotonic counter used for LRU ordering. | |
| var _clock: int = 0 | |
| var _poll_accum: float = 0.0 | |
| ## When true, release() cannot drop below zero refs (double-free guard). | |
| var _initialized := false | |
| func _ready() -> void: | |
| set_process(true) | |
| _apply_platform_budget() | |
| _initialized = true | |
| print("[MemoryManager] budget=%.0f MB" % asset_budget_mb) | |
| func _apply_platform_budget() -> void: | |
| # A 64-bit OS reports total RAM in bytes; keep a conservative slice for us. | |
| var total_mb := float(OS.get_memory_info().get("physical", 0)) / 1048576.0 | |
| if total_mb > 0.0: | |
| # Never claim more than 1/8 of physical RAM for evictable assets. | |
| asset_budget_mb = min(asset_budget_mb, total_mb * 0.125) | |
| # Hard floor so tiny test environments still work. | |
| asset_budget_mb = max(asset_budget_mb, 64.0) | |
| ## Register an asset path as loadable. Does NOT load it (that is the point: lazy). | |
| ## `cost_mb` is a caller estimate; use VertexBufferManager.estimate_cost for meshes. | |
| func register(id: String, path: String, cost_mb: float) -> void: | |
| assert(not _entries.has(id), "MemoryManager: duplicate id '%s'" % id) | |
| _entries[id] = { | |
| "path": path, | |
| "cost_mb": cost_mb, | |
| "refs": 0, | |
| "last_used": 0, | |
| "resource": null, | |
| } | |
| ## Acquire an asset, loading it on first use. Every acquire must be matched | |
| ## with exactly one release(). | |
| func acquire(id: String) -> Resource: | |
| if not _entries.has(id): | |
| push_error("MemoryManager.acquire: unknown id '%s'" % id) | |
| return null | |
| var e: Dictionary = _entries[id] | |
| if e["resource"] == null: | |
| _ensure_room(e["cost_mb"]) | |
| var res: Resource = load(e["path"]) | |
| if res == null: | |
| push_error("MemoryManager: failed to load '%s'" % e["path"]) | |
| return null | |
| e["resource"] = res | |
| asset_loaded.emit(id, e["cost_mb"]) | |
| e["refs"] = int(e["refs"]) + 1 | |
| e["last_used"] = _next_clock() | |
| return e["resource"] | |
| ## Release one reference. When refs hit zero the asset becomes evictable. | |
| func release(id: String) -> void: | |
| if not _entries.has(id): | |
| return | |
| var e: Dictionary = _entries[id] | |
| e["refs"] = maxi(0, int(e["refs"]) - 1) | |
| e["last_used"] = _next_clock() | |
| ## Force-unload an asset regardless of refs. Only use when you know all live | |
| ## references are gone (e.g. a panel was destroyed). | |
| func purge(id: String) -> void: | |
| if not _entries.has(id): | |
| return | |
| var e: Dictionary = _entries[id] | |
| if e["resource"] != null: | |
| var freed := float(e["cost_mb"]) | |
| if e["refs"] == 0: | |
| e["resource"] = null | |
| asset_evicted.emit(id, freed) | |
| e["refs"] = 0 | |
| func get_loaded_mb() -> float: | |
| var total := 0.0 | |
| for id in _entries: | |
| if _entries[id]["resource"] != null: | |
| total += float(_entries[id]["cost_mb"]) | |
| return total | |
| func _next_clock() -> int: | |
| _clock += 1 | |
| return _clock | |
| func _process(delta: float) -> void: | |
| _poll_accum += delta | |
| if _poll_accum < poll_interval: | |
| return | |
| _poll_accum = 0.0 | |
| var used := get_loaded_mb() | |
| if used >= asset_budget_mb * warn_ratio: | |
| memory_warning.emit(used, asset_budget_mb) | |
| # Also react to real OS pressure independent of our accounting. | |
| if OS.get_memory_info().get("physical", 0) > 0: | |
| var avail_mb := float(OS.get_memory_info()["available"]) / 1048576.0 if OS.get_memory_info().has("available") else INF | |
| if avail_mb < 128.0: | |
| _ensure_room(asset_budget_mb * 0.25, true) | |
| ## Evict unreferenced (LRU) assets until `need_mb` of room exists. | |
| func _ensure_room(need_mb: float, force: bool = false) -> void: | |
| while get_loaded_mb() + need_mb > asset_budget_mb: | |
| var victim := _pick_lru_victim() | |
| if victim == "": | |
| break | |
| var e: Dictionary = _entries[victim] | |
| var freed := float(e["cost_mb"]) | |
| e["resource"] = null | |
| e["refs"] = 0 | |
| asset_evicted.emit(victim, freed) | |
| if force and not _has_more_victims(): | |
| break | |
| budget_exceeded.emit(0) | |
| func _pick_lru_victim() -> String: | |
| var best := "" | |
| var best_time := 0x7FFFFFFF | |
| for id in _entries: | |
| var e: Dictionary = _entries[id] | |
| if e["resource"] == null: | |
| continue | |
| if int(e["refs"]) > 0: | |
| continue | |
| if int(e["last_used"]) < best_time: | |
| best_time = int(e["last_used"]) | |
| best = id | |
| return best | |
| func _has_more_victims() -> bool: | |
| return _pick_lru_victim() != "" | |
| func debug_snapshot() -> Dictionary: | |
| return { | |
| "budget_mb": asset_budget_mb, | |
| "loaded_mb": get_loaded_mb(), | |
| "entries": _entries.size(), | |
| } | |