Engineering 11 min read

Our own OpenCascade

KapyCAD 2 will rebuild our OpenCascade 8.0 without emulated exceptions and, in our measurements, regenerate with less than half the CPU.

SSergioSep 23, 2026
Our own OpenCascade

Part 3 of the series Inside KapyCAD 2, about a version that hasn't shipped yet. We've already covered how we test CAD software and why opening a file must never move anything.

This part deals with the heaviest piece of all, quite literally: the geometry kernel. Today's KapyCAD already runs on an OpenCascade we compile ourselves. What KapyCAD 2 brings is the step after that: a new build and a front door designed for it.

The kernel everyone uses

A CAD kernel is the software that knows how to do exact geometry: extrude a profile, subtract one solid from another, round an edge, export to STEP. It's the hardest part of a CAD program, and almost nobody writes one from scratch.

OpenCascade (OCCT) is the reference open-source kernel: decades of C++ and a huge number of awkward cases already solved. To bring it to the browser there's opencascade.js, a community package that compiles it to WebAssembly (WASM, the binary format browsers run at near-native speed) and exposes every OCCT class and method to JavaScript. You can write in JavaScript what you'd write in C++.

That's how KapyCAD started, as we described in the post about B-Rep CAD in the browser. At the start it gave us what we needed most: a full kernel inside a tab from day one, before we had to understand how it gets compiled.

Why build it ourselves

The package ships OCCT 7.7, sized to serve everybody. We soon wanted things a generic package can't give you.

To begin with, the version: we wanted OCCT 8.0. Today's KapyCAD already runs on our own 8.0 build, but with the same interface as opencascade.js, so nothing on top had to be rewritten. KapyCAD 2 will take the missing step and change that interface too.

We also wanted to keep only what we use. OCCT includes modules for drawing on screen, reading fonts, opening images or building desktop applications. Inside our worker we never draw or read a font, so those modules aren't even compiled. What stays is what models solids and what imports and exports STEP and STL.

And we needed two small patches. One makes area and volume calculations avoid an expensive hop WebAssembly pays at every integration point; with it, that function runs about three times faster. The other is about naming faces, and we'll get to it below.

Then came the big surprise: exceptions. OCCT reports a failure by throwing a C++ exception, and in the classic web build those exceptions are emulated in JavaScript. Every C++ call that might fail leaves WebAssembly for a small JavaScript trampoline and comes back in, even when nothing fails. Each round trip costs about 19 nanoseconds, and we counted how many a regeneration makes: 16.2 million on a 20-operation reference part; about 230 million, in 10.1 seconds of CPU, on a 37-operation electric-motor model; 364 million, in 16.2 seconds, on a herringbone gear. Across the five parts we measured, the trampolines took between 39% and 49% of all the CPU in a regeneration.

Recompiling OCCT with native WebAssembly exceptions, which stay inside the module, is only possible if you control the build. That's the build KapyCAD 2 will ship with:

Kernel size (.wasm)Current KapyCAD: 26.9 MBKapyCAD 2: 18.9 MBCPU to regenerate a 20-operation partCurrent KapyCAD: 677 msKapyCAD 2: ~300 ms
The current build versus KapyCAD 2's: a smaller kernel and regenerations with less than half the CPU

In our measurements, the rebuilt kernel cut the CPU for a cold regeneration of the reference part from 677 ms to about 300, a 56% drop. Two things account for it. The 302 ms (41%) spent in trampolines goes to zero. And OCCT's own time falls from 361 to 259 ms, 28% less, because the rebuild also removes the setjmp OCCT uses to catch system signals (OCC_CATCH_SIGNALS), which we come back to below. On the motor model, a regeneration came down to about 4.9 seconds of CPU, also less than half. We picked the exception flavour that Safari 15.2, Chrome 95, Firefox 100 and Node 22 already run, so every current browser is covered.

Talking to the kernel without a middleman

With the opencascade.js interface, which is what today's KapyCAD uses, everything KapyCAD's logic asks of the kernel goes through a JavaScript layer: create an OCCT object, call a method, read the result. One call for every method of every class. When tessellating a part to draw it, one call per vertex. Thousands per regeneration.

In KapyCAD 2 the document logic will live in a core written in Rust (that's the next part), and that core will talk to the kernel through a C API: a short list of functions that only take and return numbers and bytes.

diagram
In the current version, the logic reaches the kernel class by class through JavaScript; in KapyCAD 2, the Rust core will use a C API of numbers and bytes

We expected removing the glue layer to be the big speed-up, and we measured it before building anything: crossing that boundary cost between 0.4% and 2.6% of a regeneration. Next to nothing; the speed came from the exceptions. The C API earns its place for other reasons, and this is how it's built in the development build:

  • There's a single, narrow, documented entry point. Shapes live in a table inside the kernel. A function that builds returns a number identifying the new shape; one that asks leaves its answer in an area of memory the core reads in one go. The API carries a version number.
  • No exception crosses the boundary. Every function catches its own errors and returns a negative code with the message next to it, so on the other side an error arrives as plain data.
  • Sequences live in C++ and decisions in Rust. The kernel side only runs OCCT sequences: "build this fillet with these radii". What to try if it fails, in what order, when to give up: the core decides, and it travels as data.
  • TypeScript doesn't touch OCCT. No line of the interface or the glue uses a kernel class; what's left in JavaScript only copies bytes from one memory to the other. A test watches it: any new access turns red.

And to change the door without breaking anything, before touching it we made a recording of 125 real documents: for each one, the exact B-Rep, the naming tables and the evaluations. Every step of the change had to produce the same bytes.

Let the kernel tell us what it does

If you read the post about topological references, you'll remember that to keep your fillet from moving to another edge we need to know where every face comes from: which one the extrusion made, which one comes from which sketch edge, which one the cut created, which one was modified and which one disappeared.

Pocket floor and wallsgenerated by the cutCapmade by the extrusionSidescome from the sketch edges
In KapyCAD 2, every face will know where it came from: the kernel will write it down while it builds

OCCT knows all of that while it works. The current version rebuilds it afterwards: at the end of each operation, JavaScript walks the result asking the kernel element by element, edge by edge, and pieces the history together. In KapyCAD 2, collectors inside the kernel itself will write those facts down while it builds, and the core will pick them up in one go.

Since all naming rests on that history, the kernel checks itself at startup: it runs a boolean, a fillet and a face merge, and verifies that the history it reports is complete. If anything fails, it leaves a warning in the console, and our rule is that startup shows none. For a while one appeared on every boot, and it turned out the self-test was the one in the wrong. When you subtract a smaller cube from a cube, seven of the eight vertices come through untouched and one disappears, so an empty list of "modified vertices" is the correct answer. We fixed the self-test and left the warning in place.

The second patch we mentioned earlier has to do with this history. OCCT keeps many things in tables ordered by each object's memory address. The same shell operation, repeated in the same process after memory had moved on, created its faces in a different order: other names, another mesh, a volume that differed in the last decimal places. With the patch, every object is numbered by the order it was born in, and a call builds the same thing whatever ran before it.

One kernel everywhere

The same kernel file runs in your browser, on the server (the queue that regenerates documents, thumbnails, exports) and in all our tests. opencascade.js is still in the project for its types only; importing it to run it is forbidden, and the linter blocks it.

There's a reason for the strictness: the two versions aren't interchangeable, and they fail in ways a suite run against 7.7 doesn't see. A chamfer chain ending on a mirror plane: 7.7 builds it, 8.0 refuses. A method that existed in 7.7 and not in our 8.0: a retry path went dead on the kernel you were running, while the whole suite passed green… because the suite ran 7.7. Documents the tests called valid wouldn't open in the editor.

Hence the rule: if you verify against another version, you're signing off on geometry the app can't build.

So that it can't happen silently again, at startup every worker computes the fingerprint (a SHA-256 hash) of the kernel it loaded and compares it with the one recorded at build time. If they don't match, because of a stale cache or an outdated CDN, it says so. The tests print the same line, so a green suite against another binary is visible.

What it costs to maintain

Keeping our own build means, first of all, that it has to be reproducible: the kernel is compiled inside a Docker image with the Emscripten and OCCT versions pinned, it's linked twice, and both fingerprints must match byte for byte, or it doesn't ship. The binary also weighs almost 19 MB, so it changes rarely, and only when we decide it should. We don't commit a new one with every tweak, only at checkpoints, and each one must regenerate the documents in our reference corpus without a single difference.

Upgrading is work too. Moving to a newer Emscripten uncovered a compiler bug with that OCCT signal mechanism. WebAssembly has no signals, so we removed it, with a comment explaining why; the setjmp cost we mentioned above went with it. Recompiling all of OCCT takes hours.

The cache that makes opening a document fast doesn't accept surprises either: it stores the exact geometry, down to the last bit. If a future OCCT version brings a surface type that cache doesn't have registered, it refuses to write it, and the document opens by regenerating, as if there were no cache.

OCCT is distributed under the LGPL 2.1, with an additional Open Cascade exception. That's why the kernel ships as a separate file, loaded on its own and never merged into our Rust core. The patches, our binding code, the C API header and the build recipe live in a public repository, kapy-occt, so anyone can rebuild it.

Part 4 turns to the Rust core that, in KapyCAD 2, will hold the whole document and be the one talking to this kernel.

S

Written by

Sergio

Building Kapy CAD — parametric 3D modelling for 3D printing, in the browser.

Keep reading

Discord