monkeystudio / docs /ARCHITECTURE.md
geter62's picture
Upload folder using huggingface_hub
84e8204 verified
|
Raw History Blame Contribute Delete
6.13 kB
# 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.