# MonkeyStudio Architecture ## 1. The core problem: 100+ features on 4 GB of RAM A phone with 4 GB total RAM gives an app roughly **1.2-1.5 GB** before the OS starts killing it. Blender-style tools (sculpt grids, cloth solvers, hair particles, rigging) are memory-hungry. The only way to offer many features without OOM is to **never have them all resident at once**. MonkeyStudio's answer is a lazy-load + reference-count + LRU-evict memory layer, plus a fixed pool for GPU objects. Below is how each piece works. ## 2. Memory layer (`scripts/core/`) ### 2.1 `MemoryManager` (autoload) - Owns a hard **budget** (`asset_budget_mb`, default 512 MB, auto-capped to 1/8 of physical RAM). - Assets are registered by **id -> path + estimated cost**, *not* loaded. - `acquire(id)` loads on first use and increments a reference count. - `release(id)` drops a reference. At zero references the asset becomes an **eviction candidate**. - `_ensure_room(need)` evicts **least-recently-used, zero-reference** assets until the new asset fits. Assets with live references are never evicted, so eviction can never yank something out from under a running feature. - Every second it polls `OS.get_memory_info()`; when the OS says < 128 MB is available it force-evicts to buy headroom. ### 2.2 `FeatureRegistry` (autoload) - A **catalog** of every tool. Each entry is just metadata (`name`, `module_path`, `cost_mb`). - `activate(id)` asks MemoryManager for the module script, instantiates it, and adds it to the `Features` host node. - `deactivate(id)` destroys the module node and releases its memory. This is what makes "close the tool, free the RAM" automatic. ### 2.3 `MeshPool` (autoload) - Recycles `ArrayMesh` objects. A released mesh has its surfaces cleared (freeing GPU buffers) but the object is kept for reuse, avoiding per-frame allocation spikes and fragmentation. - Bounded by `max_pooled` so the pool itself can't leak. ### 2.4 `VertexBufferManager` (autoload) - `estimate_cost_mb(vertex_count)` predicts RAM+VRAM before a mesh is built so MemoryManager can evict first. - `build_surface()` / `commit()` do a **single** `add_surface_from_arrays()` upload. Mobile GPUs hate thousands of tiny buffer updates; sculpting strokes accumulate on the CPU and flush once. - `recompute_normals()` restores correct lighting after edits. ### 2.5 Adaptive quality `StudioBus` emits `fps_throttled` when FPS stays below 30. `StudioMain` reacts by disabling shadows, then the simulation substep budget drops (see below). This keeps the frame budget bounded on weak GPUs. ## 3. Feature modules (`scripts/modules/`) Modules implement `_activate(host)` / `_deactivate()` and receive the scene through the `StudioBus` signal hub (low coupling, safe lazy unload). | Module | Features | Notes | |---|---|---| | `mesh_gen.gd` | 1-20 | Procedural cube/sphere/cylinder/plane/torus/ico + a code-generated monkey head | | `sculpt.gd` | 21-40 | draw / grab / pinch / smooth / flatten, batched GPU flush | | `rigging.gd` | 41-60 | Skeleton3D builder + inverse-distance auto-weighting + `Skin` bind | | `simulation.gd` | 61-80 | Verlet mass-spring cloth & hair with wind + pinned constraints | | `timeline.gd` | 81-100 | Engine-native `Animation` + `AnimationPlayer` keyframing | ### 3.1 Mesh generation Pure functions returning `{verts, indices, uvs}`. No GPU allocation until `spawn()` commits. Costs are predictable, so a 5000-vertex sphere is refused before it is built if it would blow the budget. ### 3.2 Sculpting A stroke is `begin -> apply* -> end`. All `apply` calls mutate CPU `PackedVector3Array`s; `end` performs one upload. Brushes use a smoothstep falloff. `raycast_screen()` prefers a physics ray against the mesh collider and falls back to a bounding-sphere test when the mesh has no collider yet. ### 3.3 Rigging `generate_default_armature()` builds a spine + limbs from the mesh bounds. `bind_weights()` ranks bones by distance, keeps the nearest 4, normalizes, and writes `ARRAY_BONES` + `ARRAY_WEIGHTS` (the standard 4-influence mobile skinning path) plus a `Skin` resource. Result is a normal engine skinned mesh. ### 3.4 Cloth & hair Fixed-timestep (`1/30 s`) Verlet integration with Jacobi constraint relaxation. Sub-steps are capped at 4 and backlog is dropped, so a slow frame can never spiral into a death march. Particles and springs live in `PackedArray`s (no per-frame allocation). Cloth uploads a triangle grid; hair uploads ribbons. ### 3.5 Timeline Wraps a real `Animation` resource and `AnimationPlayer`, so interpolation, looping and playback are engine-native and cached. The module exposes `play/pause/scrub/add_keyframe/add_bone_keyframe` for a touch UI. ## 4. UI (`scripts/studio/studio_ui.gd`) Material-ish dark theme built in code (resolution-independent, scaled by `UI_SCALE`). Layout: top bar (FPS + memory HUD), left tool rail, right collapsible panels (Tools / Layers / Timeline), bottom playback + scrubber. Touch targets are >= 48 px. ## 5. Camera (`scripts/studio/touch_camera.gd`) - 1 finger: orbit (or paint when a sculpt brush is active) - 2 fingers: pinch = dolly zoom, drag = pan - 3 fingers: pan - All targets are exponentially smoothed so jittery touch sampling still feels fluid. No allocation in the input hot path. ## 6. Testing `tests/self_test.tscn` runs headless and verifies every subsystem: ```bash godot --headless res://tests/self_test.tscn ``` It checks mesh generation, all sculpt brushes (vertices actually move), the skinning channels, cloth falling under gravity, hair creation, and timeline keyframing. Exit code 0 = pass. ## 7. What is production-ready vs. roadmap Honest status: - Memory layer, mesh generation, sculpting, rigging, simulation, timeline and UI shell are **implemented and tested** as described above. - Production polish still to do: adaptivity grid (`DynamicLOD`), UV unwrap, boolean ops, baking, USD/OBJ/glTF import-export, undo stack, a broader brush set, and device-specific tuning. The architecture is designed so these plug in as new `FeatureRegistry` entries without touching the core.