# El motor: el fork de ManimGX

> Qué cambia el fork de ManimGX que estampa los pliegos y dónde engancha cada pieza del estándar.

URL: https://torculo.joseluissaorin.com/referencia/fork · Revisado: 2026-10-16 · English: https://torculo.joseluissaorin.com/en/reference.md

Fuente: FORK.md (generada al construir la web).


Repositorio: `~/Developer/torculo/manimgx` (remoto `upstream` = academa-labs/manimgx, base 9be4111).
Ramas: `seams` (enganches genéricos, candidatos a contribuir al original) y `saorin` (lo artístico).
`seams` es la rama del motor: el 14-10-2026 se le fusionaron `seams-post`, `seams-materia` y `seams-plataforma`
(06bba13), y `saorin` se adelantó hasta ella. Los worktrees `wt/post`, `wt/materia` y `wt/plataforma` se quitaron;
queda `wt/materia-scratch/base` (9be4111, con `base_hashes.json` y `corpus_hashes.py`) para comparar el corpus.
Compilar y probar: `uv sync` (unos 4 min la primera vez), `uv run manimgx render x.py`,
`just test-rust`, `just test`, `just bench main` (requiere que no haya regresión cuando lo nuevo está apagado).
Lee `AGENTS.md` del repo antes de tocar nada.

## Reglas del fork
- La lógica nueva vive en ficheros nuevos (`post.rs`, `material.rs`, `vt.rs`, `pick.rs`, `*.wgsl.tmpl`,
  paquete Python `manimgx/saorin/` o similar). En `render.rs`, `export.rs`, `player.rs`, `vector.wgsl`,
  `feed.py` solo unas pocas líneas de enganche. Upstream reescribe esos ficheros a menudo.
- Las tomas del fork usan un rango propio de versión: hoy 0x8000_0002 (`take::VERSION`; registros de 424 B, POST = 15,
  MATERIAL = 16). Se siguen leyendo las de 0x8000_0001 de seams-materia (MATERIAL era el 15).
- Claves de canalización de la composición (vector.rs): LINEAR = 32, BLENDS = 64, MATERIALS = 128, KEYS = 256.
- Todo lo nuevo apagado por defecto no puede cambiar ni un píxel del corpus de referencia.
- Determinismo: el tiempo que ven los shaders es el de la escena (o el índice de fotograma), nunca el reloj.
- Commits en español, sin emojis, terminados en «Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>».
- Canon de la casa: `~/Developer/torculo/canon/gen/canon.{wgsl,py,rs}`. Papel #F5F0E8, tinta #2C1810.
- Materias WGSL compartidas: `~/Developer/torculo/materias` (convención de pasada: entrada rgba16float lineal
  premultiplicada, sampler, uniformes {resolucion, fotograma, tiempo, semilla, params[4]}, salida).

## Hechos del código (leídos con fichero:línea)
- GPU singleton: `static GPU` render.rs:1016, `with_gpu` :1020, wasm thread-local :1029. Requiere R32Float
  blending y Depth32FloatStencil8 (render.rs:1070-1086).
- Composición 2D en 8 bits: `COLOR = Rgba8Unorm` render.rs:44; solo 3D iluminado en `RADIANCE = Rgba16Float` :46;
  la composición escribe `texture_storage_2d<rgba8unorm>` vector_compute.wgsl:5 vía `store()`/`seen()`.
  Pintura en sRGB premultiplicado (paint.wgsl:49).
- Pintura por píxel en la composición: `painted()` vector.wgsl:1301 → `paint()` :957 → `paint_of()` :1324;
  capas con `over()` :1094; modelo de dos regiones `lay*()`/`split_by()` :1149-1233.
- Ancho de trazo constante por objeto: `half = params.z` vector.wgsl:803; `body_slot`/`joint_slot`/`cap_slot`
  :669/:727/:754; raster de trazo a atlas R32Float (vector.rs:722, :1207; `fs_stroke` vector.wgsl:923).
- Guiones en espacio de parámetro, no de longitud de arco (vector.wgsl:241-261).
- Registro de 336 B por objeto y fotograma: render.rs:113-135, take.rs:50, feed.py:79-109, `Instance` render.rs:91,
  `vector::Object` vector.rs:21 (432 B), accesor en vector_buffers.wgsl:31-43. Claves de forma: take.rs:49 KEYS=7,
  `slots()` :53; feed.py KEYS (:111). Desalojo KEEP=120, SWEEP=240, BUDGET=768 MB (feed.py:53-56).
- Fotograma final: `encode_frame()` render.rs:2061 (devuelve en :2506) a `Targets.frame.color`; lo leen
  export.rs:107 (convert), render.rs:1783 (`copy_pixels`) y player.rs:268 (`Screen::composite`).
- Codificación por daño: `damage()` export.rs:18, `whole` export.rs:83, NV12 BT.601 limitado nv12.wgsl:3,
  x264 shim.c:39, MP4 mp4.rs:399; fotogramas retenidos con `repeat` (film.py:482 `_send`); el reproductor
  cachea el fotograma (project.rs:434).
- Peticiones reproductor→anfitrión: `asks()` web.rs:141; browser/src/engine.ts.
- LaTeX mal formado pasa: `mitex_math` typeset.rs:752; avisos de Typst descartados typeset.rs:568-575;
  `_balanced` existe en text.py:1180.
- Voces: `Voice = Callable[[str], Speech]` sound.py:404; `Scene.speech` cae en `Fal()` de pago sin avisar
  (scene.py:1683). `Word` en manimgx.audio.sound. Estimación por letras sound.py:417-450; caché :513-549.
- Pixels de `frames=` en RGBA premultiplicado (153,80,28,153 para #FF862F a 0,6).

## Plan por punto (resumen; los detalles están en el informe de investigación)
1. Postproceso: `post.rs` (modelo `bloom.rs`), llamado al final de `encode_frame`; `config.post = [Pass(...)]`;
   mensaje de toma `POST`; en el reproductor sobre el lienzo cacheado (`picture()` player.rs ~946).
   Composición lineal opcional en Rgba16Float (`config.compositing="linear"`), OETF sRGB a nv12.wgsl y al
   COMPOSITE del reproductor. Daño: pasadas locales dilatan macrobloques; pasadas que esparcen → `whole`;
   temporales → expandir retenidos (`push_frames` export.rs:45 y `_send` film.py:482).
2. Materiales: registro + `material:u32`, `blend:u32`, `user_id:u64`, `user:[f32;16]` (336→416 B);
   `register_material(nombre, wgsl)` → mensaje `MATERIAL`; switch generado inyectado en la composición;
   clave de caché de pipelines (render.rs:1000); texturas de material en `texture_2d_array`.
   Mezcla multiplicar: `blend(mode, p, below)` en `lay`, `lay_line`, `split_by`; rasters vía `lay_raster` :1432.
3. Trazo variable: clave `width_rows` (KEYS=8), `half(u)` en `stroke_slot`, `body()` :461, `joint_record` :493,
   `cap_record`/`end_cap` :520/:533; huella con el ancho máximo (render.rs:836); longitud de arco acumulada en
   `add_path` (render.rs:2625); textura de pincel en `fs_stroke`; canal extra de atlas Rg32Float.
4. Hervor: N poses subidas una vez, cambia `key1` (sin fork); versión GPU desplazando puntos en el aplanado
   (`curve()`/`at()` vector.wgsl:187/:200) con `hash(objeto, curva, floor(t·fps))`.
5. Biblioteca: crate-type rlib+cdylib, `python` fuera de default, fachada `manimgx-render` con
   `Gpu::from_device`, `Player`, `Replay`, `typeset()`; reproductor npm de tomas sin Pyodide
   (`<manimgx-film src="x.take">`, `Player.feed` web.rs:69 → project.rs:325); `manimgx render --take`.
6. Interactividad: textura de selección R32Uint en `composite` (vector_compute.wgsl:60) con `user_id`;
   eventos en player.rs 695-728 vía `asks()`; `await self.espera(Clic(pieza))`.
7. VideoToolbox: features `export-x264` / `export-vt`; `vt.rs` con objc2; `p010.wgsl` BT.709; HEVC Main10,
   ProRes 422 HQ y 4444 con alfa (desmultiplicar); AVAssetWriter; IOSurface para el laboratorio de película.
8. Móvil: reproductor de tomas iOS (xcframework, uniffi, CAMetalLayer); Android con camino alternativo de
   cobertura por compute si falta la mezcla de float32.
9. Voz: `sidecar(dir)` que lee WAV + `.tiempos.json` de .mesa ({beats:{id:{dur,pause,palabras:[{w,t,d}]}},
   wavs:{id:path}}); la voz por defecto da error en vez de llamar a Fal.
10. Errores estrictos: `_balanced`, `\left`/`\right`, `^`/`_` colgantes, `zws` como argumento; avisos de Typst
    devueltos; `--strict` / `MANIMGX_STRICT=1`; `inspect` marca glifos vacíos.

## La emulsión: pasadas de clase «grafo» (rama `emulsion`, 15-10-2026)
- `rust/engine/src/grafo.rs` (nuevo): un grafo de pasadas de cómputo de un mismo shader, con texturas propias a
  fracciones del tamaño; la pasada de la cadena trae el WGSL en `source`, el grafo en `entry` (una pasada por línea:
  `punto lee,lee escribe divisor p0 p1 p2 p3`), los números en `params` (búfer de almacenamiento) y la gelatina como
  volumen. Enganches en `post.rs` (clase aceptada, `Stage::Graph`, su `encode`) y una línea en `render.rs`.
- Python: `manimgx/saorin/pelicula.py` (`m.Pelicula`), que pide la ficha a `torculo_emulsion` (`TORCULO/emulsion`).
- El mismo grafo lo ejecutan `materias/src/grafo.rs` (`materia revela`) y `emulsion/web/grafo.js` (WebGPU).
