Nuestro propio OpenCascade
KapyCAD 2 recompilará nuestro OpenCascade 8.0 sin excepciones emuladas y, en nuestras mediciones, regenerará con menos de la mitad de CPU.

Tercera entrega de la serie «Por dentro de KapyCAD 2», sobre una versión que aún no ha salido. Ya contamos cómo probamos un CAD y por qué abrir un archivo no debe mover nada.
Esta entrega se ocupa de la pieza más pesada de todas, en sentido literal: el kernel de geometría. La versión actual de KapyCAD ya funciona sobre un OpenCascade compilado por nosotros. Lo que llega con KapyCAD 2 es lo que va después: una compilación nueva y una puerta de entrada diseñada a medida.
El kernel que usa todo el mundo
Un kernel de CAD es la pieza de software que sabe hacer geometría exacta: extruir un perfil, restar un sólido de otro, redondear una arista, exportar a STEP. Es la parte más difícil de un programa de CAD, y casi nadie la escribe desde cero.
OpenCascade (OCCT) es el kernel de código abierto de referencia: décadas de C++ y una cantidad enorme de casos raros ya resueltos. Para llevarlo al navegador existe opencascade.js, un paquete de la comunidad que lo compila a WebAssembly (WASM, el formato binario que el navegador ejecuta casi a velocidad nativa) y expone cada clase y cada método de OCCT a JavaScript. Puedes escribir en JavaScript lo mismo que escribirías en C++.
Con eso empezó KapyCAD, y lo contamos en el post sobre el CAD B-Rep en el navegador. Al arrancar nos dio lo que más falta hacía: un kernel completo dentro de una pestaña desde el primer día, sin tener que entender todavía cómo se compila.
Por qué compilarlo nosotros
El paquete viene con OCCT 7.7 y con un tamaño pensado para servir a todo el mundo. Pronto quisimos cosas que un paquete genérico no puede darte.
Para empezar, la versión: queríamos OCCT 8.0. La versión actual de KapyCAD ya corre sobre nuestra propia compilación de 8.0, pero con la misma interfaz que opencascade.js, para no tener que reescribir nada encima. KapyCAD 2 dará el paso que faltaba y cambiará también esa interfaz.
También queríamos quedarnos solo con lo que usamos. OCCT incluye módulos para dibujar en pantalla, leer fuentes tipográficas, abrir imágenes o montar aplicaciones de escritorio. Dentro de nuestro worker nunca dibujamos ni leemos una fuente, así que esos módulos ni se compilan. Se queda lo que modela sólidos y lo que importa y exporta STEP y STL.
Y hacían falta dos parches pequeños. Uno hace que el cálculo de áreas y volúmenes evite un salto caro que WebAssembly paga en cada punto de integración; con él, esa función va unas tres veces más rápida. El otro tiene que ver con nombrar caras, y lo vemos más abajo.
Luego vino la gran sorpresa: las excepciones. OCCT avisa de un fallo lanzando una excepción de C++, y en la compilación clásica para la web esas excepciones se emulan con JavaScript. Cada llamada de C++ que podría fallar sale de WebAssembly a un pequeño trampolín de JavaScript y vuelve a entrar, aunque al final no falle nada. Cada ida y vuelta cuesta unos 19 nanosegundos, y medimos cuántas hay en una regeneración: 16,2 millones en una pieza de referencia de 20 operaciones; unos 230 millones, en 10,1 segundos de CPU, en el modelo de un motor eléctrico, de 37 operaciones; 364 millones, en 16,2 segundos, en un engranaje helicoidal en espiga. En las cinco piezas que medimos, los trampolines se llevaban entre el 39 y el 49 % de toda la CPU de una regeneración.
Recompilar OCCT con excepciones nativas de WebAssembly, que se quedan dentro del módulo, solo se puede hacer si controlas la compilación. Esa es la compilación que llevará KapyCAD 2:
En nuestras mediciones, con el kernel recompilado la CPU de una regeneración en
frío de la pieza de referencia bajó de 677 ms a unos 300, un 56 % menos. Lo
explican dos cosas. Los 302 ms (el 41 %) que se iban en trampolines pasan a
cero. Y el tiempo del propio OCCT baja de 361 a 259 ms, un 28 % menos, porque la
recompilación quita también el setjmp con el que OCCT atrapa las señales del
sistema (OCC_CATCH_SIGNALS), del que hablamos más abajo. En el modelo del
motor, la regeneración quedó en unos 4,9 segundos de CPU, también menos de la
mitad. Elegimos la variante de excepciones que ya ejecutan Safari 15.2, Chrome
95, Firefox 100 y Node 22, para cubrir todos los navegadores actuales.
Hablar con el kernel sin intermediarios
Con la interfaz de opencascade.js, que es la que usa la versión actual, cada cosa que la lógica de KapyCAD le pide al kernel pasa por una capa de JavaScript: crear un objeto de OCCT, llamar a un método, leer el resultado. Una llamada por cada método de cada clase. Al teselar una pieza para dibujarla, una llamada por vértice. Miles por regeneración.
En KapyCAD 2, la lógica del documento vivirá en un núcleo escrito en Rust (lo contaremos en la siguiente entrega), y ese núcleo hablará con el kernel por una API en C: una lista corta de funciones que solo aceptan y devuelven números y bytes.
Esperábamos que quitar el intermediario fuera la gran mejora de velocidad, y lo medimos antes de construir nada: cruzar esa frontera costaba entre el 0,4 y el 2,6 % de una regeneración. Casi nada; la velocidad salió de las excepciones. La API en C se justifica por otras razones, y así está hecha en la versión en desarrollo:
- Es una sola puerta, estrecha y documentada. Las formas viven en una tabla dentro del kernel. Una función que construye devuelve un número que identifica la forma nueva; una que pregunta deja la respuesta en una zona de memoria que el núcleo lee de una vez. La API tiene número de versión.
- Ninguna excepción cruza la frontera. Cada función atrapa sus errores y devuelve un código negativo con el mensaje al lado, así que al otro lado el error llega como un dato más.
- Las secuencias van en C++ y las decisiones en Rust. El lado del kernel solo hace secuencias de OCCT: «construye este redondeo con estos radios». Qué probar si falla, en qué orden, cuándo rendirse, lo decide el núcleo y viaja como dato.
- TypeScript no toca OCCT. Ninguna línea de la interfaz ni del pegamento usa una clase del kernel; lo que queda en JavaScript solo copia bytes de una memoria a otra. Un test lo vigila: cualquier acceso nuevo sale en rojo.
Y para cambiar la puerta sin romper nada, antes de tocarla hicimos una grabación de 125 documentos reales: de cada uno, el B-Rep exacto, las tablas de nombres y las evaluaciones. Cada paso del cambio tenía que dar los mismos bytes.
Que el kernel nos cuente lo que hace
Si leíste el post sobre referencias topológicas, recordarás que para que tu redondeo no se mude de arista necesitamos saber de dónde sale cada cara: cuál generó la extrusión, cuál viene de qué arista del croquis, cuál creó el corte, cuál se modificó y cuál desapareció.
OCCT sabe todo eso mientras trabaja. La versión actual lo reconstruye después: al acabar cada operación, JavaScript recorre el resultado preguntándole al kernel elemento a elemento, arista a arista, y arma la historia. En KapyCAD 2, unos colectores dentro del propio kernel apuntarán esos hechos mientras construye, y el núcleo los recogerá de una sola vez.
Como todo el nombrado se apoya en esa historia, el kernel se comprueba a sí mismo al arrancar: hace una booleana, un redondeo y una fusión de caras, y verifica que la historia que reporta está completa. Si algo falla, deja un aviso en la consola, y nuestra regla es que al arrancar no salga ni uno. Durante un tiempo salía uno en cada arranque, y resultó que la que se equivocaba era la autocomprobación. En un cubo al que le restas otro más pequeño, siete de los ocho vértices salen intactos y uno desaparece, así que una lista vacía de «vértices modificados» es la respuesta correcta. Corregimos la comprobación y dejamos el aviso como estaba.
El segundo parche que mencionamos arriba tiene que ver con esta historia. OCCT guarda muchas cosas en tablas ordenadas por la dirección de memoria de cada objeto. Una misma operación de vaciado, repetida en el mismo proceso después de que la memoria se hubiera movido, creaba sus caras en otro orden: otros nombres, otra malla, un volumen distinto en los últimos decimales. Con el parche, cada objeto se numera por el orden en que nació, y una llamada construye lo mismo pase lo que pase antes.
Un solo kernel en todas partes
El mismo archivo de kernel corre en tu navegador, en el servidor (la cola que regenera documentos, las miniaturas, las exportaciones) y en todos nuestros tests. opencascade.js sigue en el proyecto solo por sus tipos; importarlo para ejecutarlo está prohibido y el linter lo bloquea.
La insistencia tiene motivo: las dos versiones no son intercambiables y fallan de formas que una batería contra 7.7 no ve. Una cadena de chaflanes que termina en un plano de simetría: 7.7 la construye, 8.0 la rechaza. Un método que existía en 7.7 y no en nuestra 8.0: un camino de reintento quedó muerto en el kernel que usabas tú, mientras toda la batería pasaba en verde… porque la batería corría 7.7. Documentos que los tests daban por buenos no abrían en el editor.
De ahí la regla: si verificas contra otra versión, estás dando por buena geometría que la aplicación no sabe construir.
Para que eso no vuelva a pasar en silencio, cada worker calcula al arrancar la huella (un hash SHA-256) del kernel que ha cargado y la compara con la que se apuntó al compilar. Si no coincide, por una caché vieja o un CDN desfasado, lo dice. Los tests imprimen la misma línea, así que una batería en verde contra otro binario se ve.
Lo que cuesta mantenerlo
Mantener nuestra propia compilación nos obliga, para empezar, a que sea reproducible: el kernel se compila dentro de una imagen de Docker con las versiones de Emscripten y de OCCT fijadas, se enlaza dos veces y las dos huellas tienen que coincidir byte a byte; si no, no se publica. Además, el binario pesa casi 19 MB, así que cambia poco y solo cuando lo decidimos. No subimos uno nuevo con cada retoque, solo en puntos de control, y cada uno debe regenerar los documentos de nuestro corpus de referencia sin una sola diferencia.
Actualizar también es trabajo. Subir de versión de Emscripten destapó un fallo
del compilador con ese mecanismo de señales de OCCT. WebAssembly no tiene
señales, así que lo quitamos, con un comentario que explica por qué; de paso, se
fue el coste del setjmp que mencionamos arriba. Recompilar OCCT entero cuesta
horas.
La caché que hace que abrir un documento sea rápido tampoco admite sorpresas: guarda la geometría exacta, hasta el último bit. Si una versión futura de OCCT trae un tipo de superficie que esa caché no tiene registrado, se niega a escribirlo, y el documento se abre regenerando, como si no hubiera caché.
OCCT se distribuye bajo la LGPL 2.1, con una excepción adicional de Open
Cascade. Por eso el kernel va en un archivo aparte, que se carga por separado y
nunca se fusiona con nuestro núcleo en Rust. Los parches, nuestro código de
enlace, la cabecera de la API en C y la receta de compilación están en un
repositorio público, kapy-occt, para que cualquiera pueda reconstruirlo.
La cuarta entrega trata del núcleo en Rust que, en KapyCAD 2, guardará el documento entero y será quien hable con este kernel.
Escrito por
Sergio
Building Kapy CAD — parametric 3D modelling for 3D printing, in the browser.


