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 PackedVector3Arrays; 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 PackedArrays (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:

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.