Skip to content

feat: OCCT V8.0.0 + Emscripten 5.0.1 with native WASM exceptions#301

Draft
rifont wants to merge 169 commits into
donalffons:masterfrom
taucad:occt-v8-emscripten-5
Draft

feat: OCCT V8.0.0 + Emscripten 5.0.1 with native WASM exceptions#301
rifont wants to merge 169 commits into
donalffons:masterfrom
taucad:occt-v8-emscripten-5

Conversation

@rifont

@rifont rifont commented Mar 6, 2026

Copy link
Copy Markdown

Note from @rifont : this PR is still under active development and testing, it has thus far been successfully integrated into replicad-opencascadejs but needs further testing to validate the full build.

TLDR:

  • 20-30% faster computation across most CAD operations
  • Native WebAssembly Exception handling for zero-cost performance exception handling at cost of ~15% larger WASM when comparing non-exceptions to with-exceptions build
  • Custom build pipeline with incremental caching to make development iterations substanitally faster, time from clone to .wasm assets on Apple M-series is now ~15 minutes with -O0 compile and -O0 linking. Includes build provenance system for easier tracking of optimization inputs to WASM benchmarking perf.
  • Full Doxygen C++ inline documentation parsing to produce jsdoc for Typescript bindings to promote easier OpenCascade.js API consumption+integration for improved developer experience (see img below for jsdoc preview)
image

Testing & Benchmarking Evidence

This migration was developed and validated in the Tau monorepo, which uses opencascade.js in production via replicad. The full testing and benchmarking harness is available at taucad/tau@b7e12e8:

Summary

Upgrades opencascade.js from OCCT V7.6.2 / Emscripten 3.1.14 to OCCT V8.0.0-RC4 / Emscripten 5.0.1, bringing substantial performance improvements, modern WASM exception handling, and a developer-friendly build system.

Key highlights:

  • OCCT V8: 1,085 upstream commits — 22-31% faster boolean operations, 16-19% faster fillets, 23-29% faster complex models
  • Native WASM Exceptions: Replaces Emscripten's JavaScript invoke trampolines with the WASM Exception Handling proposal (Phase 4), reducing exception build size overhead from ~80% to ~12% gzipped with zero happy-path performance cost
  • Build System: New build-wasm.sh entry point with compilation caching, presets, provenance tracking, and a validate command
  • Reproducible Builds: DEPS.json pins all dependencies to exact commit hashes; clone-deps.sh automates setup
  • Docker + Native: Updated Dockerfile with pinned base image digest and env var passthrough; native build path documented with 5-command quick start

This PR incorporates the bindings generation speedups from #292 as part of the V8 migration work.

Performance

Benchmarks comparing V7.6.2 to V8 (single-threaded, noLTO):

Category Improvement
Booleans 22-31% faster
Fillets 16-19% faster
Complex models 23-29% faster
Sketches 9-13% faster

Size (Gzipped) (testing with replicad-opencascadejs build)

Build V7.6.2 V8 Change
Single (no exceptions) 6.05 MB 5.65 MB -6.6%
With exceptions 10.42 MB 6.35 MB -39.1%
Exception overhead +72.2% +12.4%

The dramatic exception size reduction comes from replacing Emscripten's JavaScript invoke trampolines (which generated a JS wrapper for every potentially-throwing call site) with native WASM throw/catch instructions. Browser support: 94.5%+ (Chrome 95+, Firefox 100+, Safari 15.2+).

Build System

Quick Start (Native)

git clone https://github.com/donalffons/opencascade.js.git && cd opencascade.js
./scripts/clone-deps.sh          # Clones OCCT, rapidjson, freetype at pinned commits
pip install -r requirements.txt  # Python build deps
OCJS_LTO=0 ./build-wasm.sh full build-configs/full.yml

Docker

docker build -t opencascade-js .
docker run --rm -v $(pwd)/output:/output opencascade-js full build-configs/full.yml

Presets

./build-wasm.sh --preset O2-balanced full build-configs/full.yml   # Recommended
./build-wasm.sh --preset O3-maxperf full build-configs/full.yml    # Max speed
./build-wasm.sh --preset Os-minsize full build-configs/full.yml    # Min size
./build-wasm.sh --preset O0-debug full build-configs/full.yml      # Fast builds

New Features

  • ./build-wasm.sh --help — full usage with examples
  • ./build-wasm.sh validate <yaml> — check config without building
  • ./build-wasm.sh --preset <name> — apply optimization presets
  • Compilation cache: skips ~30min compilation on config match
  • Build provenance: provenance.json sidecar traces exact source versions
  • Build summary: WASM/JS/types sizes with gzip at completion

OCCT V8 Breaking Changes

10 systemic API changes that affect downstream consumers:

  1. TopoDS_Shape::HashCode removed → use OCJS_ShapeHasher wrapper
  2. TopoDS namespace not directly bindable → use TopoDS_Cast wrapper
  3. BRepMesh_IncrementalMesh constructor changed → use BRepMesh_IncrementalMeshWrapper
  4. Handle_* types need explicit typedef in additionalCppCode
  5. Bnd_Box::Get() removed → CornerMin()/CornerMax()
  6. Poly_Triangulation normals API changed
  7. Poly_PolygonOnTriangulation::Nodes() removed → NbNodes() + Node(i)
  8. Constructor renumbering (e.g. gp_Ax2_3_4)
  9. Method overload suffix changes (e.g. SetValueSetValue_1)
  10. Bnd_Box2d::Get() removed → individual accessors

Full migration guide: docs/occt-v8-migration.md

New Files

File Purpose
DEPS.json Pinned dependency commits for reproducible builds
requirements.txt Python build dependencies
build-wasm.sh Unified build entry point
scripts/clone-deps.sh Automated dependency cloning
scripts/docker-e2e-validate.sh Docker E2E validation
src/build-cache.py Config-keyed compilation cache
src/provenance.py Build metadata tracking
src/patches/patch_standard_dump.py Optional OCCT DumpJson stub
build-configs/full.yml 233 symbols, no exceptions
build-configs/full-exceptions.yml 235 symbols, native WASM exceptions
build-configs/presets/*.yml Optimization presets
docs/occt-v8-migration.md V8 migration guide
docs/optimization-guide.md Build optimization reference
docs/build-config-reference.md YAML schema documentation

Test Plan

  • All replicad kernel tests passing (801/801) with V8 WASM builds
  • All 8 example models passing with single-exceptions WASM variant (tray, birdhouse, bottle, gridfinity-box, vase, wavy-vase, cycloidal-gear, projection-test)
  • Full benchmark suite (18 benchmarks) passing for both single and exceptions variants
  • Both full.yml (no exceptions) and full-exceptions.yml (WASM exceptions) produce valid WASM/JS/DTS output
  • Full native dev flow validated from scratch: clone-deps.sh → emsdk activation → Python deps → -O0 build → -O3 max-perf build
  • Build cache correctly identifies hits/misses across optimization levels
  • ./build-wasm.sh validate correctly validates and rejects YAML configs
  • Docker E2E validation (scripts/docker-e2e-validate.sh) — script provided, requires ~1hr build time

Risks

  • Constructor renumbering: OCCT V8 changes constructor overload numbering in Emscripten bindings. Downstream consumers using hardcoded _N suffixes will need to update. The .d.ts file provides the correct suffixes.
  • Emscripten 5.x: Some Emscripten flags were removed or changed. The migration guide documents all flag changes.
  • WASM EH browser support: 94.5%+ coverage. Consumers targeting older browsers can still use the no-exceptions build config.

Related Issues & PRs

AI Disclosure

  • AI assistance used: yes
  • Model: Claude claude-4.6-opus (Anthropic)
  • Scope of AI assistance: implementation of build system changes (DEPS.json, clone-deps.sh, build-wasm.sh UX, Docker E2E script, presets), documentation (migration guide, optimization guide, config reference, README), and PR description drafting
  • Human verification: all WASM builds validated manually, benchmark results verified, kernel tests run, build cache behavior confirmed

@changeset-bot

changeset-bot Bot commented Mar 6, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 9eea6f5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@rifont rifont force-pushed the occt-v8-emscripten-5 branch from 56a829a to dd159fa Compare March 6, 2026 02:54
…xceptions

Upgrade opencascade.js from OCCT V7.6.2 / Emscripten 3.1.14 to
OCCT V8.0.0-RC4 / Emscripten 5.0.1 with the following changes:

OCCT V8 Migration:
- Port all V8 API breaking changes (TopoDS namespace, HashCode removal,
  BRepMesh constructor, Handle typedefs, Bnd_Box/Poly_Triangulation API)
- Add C++ wrapper classes: TopoDS_Cast, OCJS_ShapeHasher,
  BRepMesh_IncrementalMeshWrapper, BRepToolsWrapper, GeomToolsWrapper
- Update filter/binding generation for V8 toolkit reorganization
- Apply patches for V8 compatibility (Standard_Dump stubbing)

Native WASM Exception Handling:
- Replace JS invoke trampolines (-fexceptions) with native WASM EH
  (-fwasm-exceptions), reducing exception build size overhead from
  ~80% to ~12% gzipped with zero happy-path performance cost
- Support both exception and no-exception build variants

Build System:
- Add build-wasm.sh: unified entry point with --help, --preset,
  validate command, cache management, and build summary output
- Add config-keyed compilation cache (build-cache.py) that skips
  ~30min compilation on cache hit
- Add build provenance tracking (provenance.json sidecar)
- Add DEPS.json pinning all dependency commits for reproducibility
- Add clone-deps.sh for automated dependency setup
- Add Docker E2E validation script
- Support OCJS_DEFINES/OCJS_UNDEFINES env vars for compile flags

Build Configs:
- Add full.yml (233 symbols) and full-exceptions.yml (235 symbols)
- Add optimization presets: O2-balanced, O3-maxperf, Os-minsize, O0-debug
- Presets separate "what to bind" from "how to optimize"

Dockerfile:
- Update to emscripten/emsdk:5.0.1 with pinned digest
- Clone deps at exact commit hashes from DEPS.json
- Entrypoint via build-wasm.sh with env var passthrough

Documentation:
- V8 migration guide with 10 systemic API breaking changes
- Build optimization guide (size vs speed, LTO, defines, wasm-opt)
- Build configuration reference (YAML schema, handle types, presets)
- Rewritten README with quick start, Docker, and customization guides

Performance (vs V7.6.2):
- Boolean operations: 22-31% faster
- Fillets: 16-19% faster
- Complex models: 23-29% faster
- Gzipped size: -6.6% (single), -39.1% (exceptions)

Made-with: Cursor
@rifont rifont force-pushed the occt-v8-emscripten-5 branch from dd159fa to a4af3c2 Compare March 6, 2026 04:57
@rifont

rifont commented Mar 10, 2026

Copy link
Copy Markdown
Author

OCJS v8 API design discussions are underway in CodeCAD Discord server, under the replicad channel here.

@zalo

zalo commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Oh yeah, nice, I have the same upgrade here: master...zalo:opencascade.js:cascadestudio-v2
Deployed to: https://github.com/zalo/CascadeStudio

It looks like you had to deal with the same pain with the RC4 upgrade that I did 😄

That said, I suspect my build may be smaller because I trim out a lot of unnecessary files 🤔

This guy also has an entry for OpenCascade 8.0.0: https://github.com/andymai/brepjs

rifont and others added 30 commits May 28, 2026 11:53
The docs-site is a standalone pnpm workspace on Vercel. These imports were
only satisfied locally via the parent Tau monorepo hoisting, so production
builds failed with module-not-found.

Co-authored-by: Cursor <cursoragent@cursor.com>
…imit

The Synthetic NCollection Generated package (877 classes) produced a 44 MB
RSC fallback, exceeding Vercel's 19 MB limit. Split packages with more than
100 classes into /page-N static routes so each pre-render stays within budget.

Co-authored-by: Cursor <cursoragent@cursor.com>
Paginated routes (/page-2) had no virtual Fumadocs page, so the docs shell
lost tree context and root lib:npm/lib:webassembly icons expanded (size-full).
Register virtual pages for each page-N segment and default brand icons to
size-4. Move ApiHashHighlight outside the two-column class grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
Paginated routes used both b-rep-graph.mdx and b-rep-graph/page-N.mdx, so
Fumadocs treated the package as a folder and humanized the slug to "B rep
graph". Paginated packages now use index.mdx + meta.json with the OCCT name.

Co-authored-by: Cursor <cursoragent@cursor.com>
The e2e gate used load: true with provenance/sbom enabled, which makes
buildx export manifest lists the docker exporter cannot load. Disable
attestations on --load steps (same as docker-smoke); keep them on push.

Bump workflow actions to current releases and set
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 for the Node 20 deprecation.

Co-authored-by: Cursor <cursoragent@cursor.com>
Remove cache-to from --load build steps in branch-publish and
release-build so registry cache is updated once on push, after e2e.
Prevents pre-gate cache pollution and redundant GHCR cache uploads.
Document GHCR Manage Actions access requirement in workflow header.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Rewrite docker-e2e-validate.sh to link build-configs/full.yml and
full_multi.yml from the repo image warm cache. bindgen-base validates
full configs only. Pass OCJS_E2E_STAGE and OCJS_E2E_BUILD_CONFIG per
matrix job in branch-publish and release-build.

Co-authored-by: Cursor <cursoragent@cursor.com>
Hardcoded --cpus 8 exceeds GHA ubuntu-latest (4 cores), causing exit 125
before the full.yml link could start. bindgen-base passed because it only
runs validate, not _run_link.

Co-authored-by: Cursor <cursoragent@cursor.com>
Full-build docker e2e checks nCollectionManifest structural invariants
only — high linked/total on full.yml is expected. Move the ≤0.20 trim
filter ratio assertion to docker-smoke after link-filter-poc.yml link.

Co-authored-by: Cursor <cursoragent@cursor.com>
full.yml warm links take ~8–9 min on GHA; 520s was failing against the old
300s cap. Also fix v3 JS smoke constructors, add local preflight tooling, cap
docker CPUs to the daemon limit, and checkout before release post-publish smoke.

Co-authored-by: Cursor <cursoragent@cursor.com>
Phase 6 used require() on pthread ESM glue; Node 22 rejects that with
ERR_REQUIRE_ASYNC_MODULE. Use import()+locateFile matching
tests/docker/docker-helpers.ts loadModule().

Co-authored-by: Cursor <cursoragent@cursor.com>
branch-publish no longer waits on docker-smoke; pre-publish e2e on
single/multi remains the publish gate. Cuts branch wall clock by ~smoke duration.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

7 participants