~/satyajit

usd-brep: NVIDIA open-sources SMLib, a million-line NURBS kernel with a USD schema on top

mdjsonmcp

2026-10-13 · 34 min · 3d · geometry · nvidia · mcp · agents · licensing · developer-tools

A 1:48 narrated explainer, drawn in code. Every number and picture in it is this article's own; the sources are below.

› transcript

Hi, I'm Fillet! An industrial geometry kernel just went open source. Here is how a kernel keeps a solid exact. A mesh is just triangles. A kernel's solid is exact surfaces plus topology: which trimmed pieces are kept, and which ones touch. A model splits space into regions, solid and void, each bounded by a shell of faces. A face is a piece of an exact surface: a plane, a cylinder, or a spline patch. Loops of edges say which part is kept. Outer loops run one way, holes the other. Every edge carries an exact curve and knows each face it bounds. Every time a face, loop or edge bounds something, that incidence is its own use. A face has two, one per side. So one edge can bound three or four faces mid boolean, which simpler structures cannot hold. A boolean runs in four careful phases. Find every place the two solids meet, in scratch space. Merge pieces within tolerance, and stop if anything failed. Only then imprint the cuts into the real solids. Classify each face as inside or outside, and keep what the operation wants. Today, design data becomes triangles before it enters the scene graph. The new schema stores topology as flat index arrays, with exact surfaces beside it. And the kernel is no toy: nearly a million lines of code. Within a day, outside developers had fixed real bugs. Open means anyone can read, fix and embed the hard parts. Exact surfaces, explicit neighbours, and solids in the scene graph. See you next time! Every source is in the full article. I'm Fillet. Bye!

Why read this

Notabletop 60%

B-reps from first principles with two widgets, a recount of usd-brep's 1.1M lines, its USD schema, a traced Boolean, and the bug smlibpy patched on day one.

  • Original analysis
  • Runs on a laptop CPU
  • A lasting reference

3D & spatialApache-2.0Practitioner tool

How this was scored
Is it new?
2 of 3: A real new idea, method or capability
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
2 of 3: Open code or weights with real limits
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
2 of 3: A reference for a year or more
Does it affect many?
1 of 3: A specialist community
Only here?
2 of 3: A teardown or measurement few others did

Score 67 of 100, ranked 159 of 513 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

Every mechanical CAD program you have used sits on a geometry kernel, and almost all of those kernels are closed. Parasolid is Siemens'. ACIS is Dassault's. Autodesk has its own. The one widely used open option, Open CASCADE, is LGPL and has a reputation (fair or not, and one of the replies under the launch post put it in three words: "occ is very bad") for being hard to make behave. So when NVIDIA published usd-brep on 9 October under Apache-2.0, and the people who make ClassCAD said on X that it is SMLib, a kernel they have built products on for more than 20 years, I wanted to know what was actually in the repository. Not the announcement. The code.

I expected a thin wrapper over a binary, or a cleaned-up subset. It is neither. The kernel directory alone is 967,172 lines of C++, about 534,000 of them code once comments and blank lines are stripped. There is a C API, roughly 300 pybind11 bindings, an OpenUSD schema for storing exact solids, a Hydra plugin so usdview can draw them, a validator, a reader for Open CASCADE's .brep files, and an unusually thorough set of docs written for AI coding agents. And within a day a downstream project, smlibpy, had shipped patched copies of two kernel files, one of which fixes a bug NVIDIA lists in its own KNOWN_ISSUES.md without knowing the cause.

This article is the long version: what a boundary-representation kernel is and why the hard parts are hard, what this one contains, how it puts solids into USD, what smlibpy adds on top, and what is missing next to the commercial kernels. I read both repositories at their release commits; I did not build or run either.

NVIDIA-Omniverse/usd-brep@c9c5979 · snapshot 2026-10-13
tracked files
1,522
license
Apache-2.0
branch
main
tests
95 files
source
47.2 MB
commit date
2026-10-09
source by language
C++38.8 MB(357)C6.6 MB(331)Python1.7 MB(82)Lua62.6 kB(29)Shell58.1 kB(17)PowerShell31.3 kB(2)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-10-13 at c9c5979 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount

shallow clone: counts describe the pinned tree, not the history

awv-informatik/smlibpy@0aaa37f · snapshot 2026-10-13
tracked files
1,028
license
MIT
branch
main
tests
43 files
source
3.9 MB
commit date
2026-10-13
source by language
Python2.0 MB(605)TypeScript1.4 MB(60)C++414.7 kB(19)C42.6 kB(17)JavaScript36.5 kB(5)CSS15.6 kB(1)HTML9.7 kB(2)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-10-13 at 0aaa37f — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, testFileCount

What a kernel has to represent

A mesh is a list of triangles. It tells you where the surface roughly is and nothing else. Ask a mesh for the radius of a hole and you get an estimate from vertices that happen to lie near it. Ask which faces touch a given edge and you have to infer it from shared vertex indices. Ask it to fillet that edge and it has nothing to work with.

A boundary representation (B-rep) keeps two things apart. Geometry is the exact mathematical shape: an infinite plane, a cylinder of radius 18, a NURBS patch with its control net. Topology is the bookkeeping that says which bounded piece of which surface is part of the solid and who its neighbours are. A face is a surface trimmed by loops of edges. An edge is a curve trimmed by two vertices. A shell is a connected set of faces, and a closed shell encloses a region of space. The geometry answers "where", the topology answers "what is connected to what", and a kernel's job is to keep the two agreeing while operations tear them apart and stitch them back together.

The AOUSD problem statement that usd-brep vendors puts the split in one table, and the line I would underline is the one about where each kind of geometry comes from: meshes are "derived from exact geometry (or authored directly)", while B-reps are the "authoritative source". Everything downstream (rendering, simulation meshes, toolpaths) is a view regenerated from the B-rep at whatever resolution the consumer needs.

NURBS, the shape language

Planes, cylinders, cones, spheres and tori cover most machined parts, and SMLib keeps them as analytic surfaces. Everything else, from a car body panel to the blend a fillet leaves behind, is a NURBS: a non-uniform rational B-spline.

A NURBS curve of degree pp is defined by control points PiP_i, a weight wiw_i on each, and a non-decreasing knot vector UU:

C(u)=∑iNi,p(u) wi Pi∑iNi,p(u) wiC(u) = \frac{\sum_i N_{i,p}(u)\, w_i\, P_i}{\sum_i N_{i,p}(u)\, w_i}

The Ni,pN_{i,p} are B-spline basis functions, built by the Cox–de Boor recursion from the knots. Each one is nonzero over only p+1p+1 knot spans, which is why moving one control point bends only a local piece of the curve. The weights are what make it rational, and rationality is what lets a NURBS represent a circle exactly instead of approximately. The knots set where the pieces join and how smooth the joins are.

The widget below runs the same algorithms a kernel does: Cox–de Boor for the basis (NLib's N_Basis* routines in source/SMLib/src/NL_Basis*.cpp implement the same recursion) and Boehm's algorithm for knot insertion.

Three things are worth trying. Drag one control point of the cubic and watch only the coloured bump under it in the basis plot react; that is local support, and it is why a designer can tweak one region of a panel without disturbing the rest. Then load the circle. It is the textbook nine-point quadratic: the corners of a square, with weights of 1 on the points that sit on the circle and 1/2\sqrt{1/2} on the corners. Every sample lands on the unit circle to within rounding error. Load the same control points with all weights set to 1 and the curve bulges outward; I computed the worst case at the diagonals as a radius of 1.0607, about 6% too big. A polynomial spline cannot make a circle. A rational one can, and a CAD kernel that has to report a hole as exactly 18 mm needs that.

Finally, insert a knot. A control point appears, the polygon changes, and the curve does not move: the readout reports the largest displacement, which is rounding error. Knot insertion is how kernels split curves at intersection points, make two surfaces' parameterisations compatible before blending them, and subdivide for intersection searches. It is the quiet workhorse under a lot of the operations below.

Surfaces are the same idea in two parameters, S(u,v)S(u,v), with a grid of control points and a knot vector in each direction.

Trimming, and why the topology needs "uses"

A NURBS surface is always a rectangle in its own (u,v)(u,v) parameter space. Real faces are not rectangles: the top of a plate has a hole in it, a fillet ends where it runs into a wall. So a face is a surface plus trimming loops that say which part of the parameter rectangle is kept. The outer loop runs one way, holes run the other, and the material is on the left as you walk along.

The trimming curves can live in two places, and the choice matters. SMLib stores the authoritative curve of every edge in 3D, and treats the 2D trim curve in each face's parameter space as optional derived data it can rebuild. The repository's B-rep format reference is blunt about it: a UV trim curve is "optional, derived data, not a defining characteristic of the BRep". The UsdSolid proposal says the same thing from the other side: "Trim curves are optional in this Brep model because the edge curve defines the model truth."

Then there is the question of how faces know their neighbours. The usual answer is that each edge points at the two faces it separates. SMLib uses something more general, Kevin Weiler's 1986 radial edge data structure, and the USD schema inherits it. Each topological object is stored once, and every time it bounds something higher up, that incidence is a separate use object: a faceuse for each side of a face, a loopuse, an edgeuse for each time an edge bounds a face, a vertexuse.

Object model diagram. Yellow topology boxes run down the left: Brep, Region, Shell. Green use boxes follow: Faceuse, Loopuse, Edgeuse, Vertexuse, each linked to a yellow topology object to its right (Face, Loop, Edge, Vertex), and those to blue geometry boxes (3D Surface, 3D Curve, 3D Point). The Edgeuse also links to a UV TrimCurve. A legend distinguishes one-to-one from one-to-many links; Faceuse and Loopuse are marked as pairs.
Topology objects (yellow), their uses (green) and geometry (blue). Each face has two faceuses, one per side; each loop two loopuses. (UsdSolid proposal, AOUSD Geometry Working Group, vendored in usd-brep as proposals/UsdSolid/images/image0.png.)

Why bother? Because a solid is not the only thing a kernel has to represent. When two boxes share a face, that face bounds two regions at once, and each side of it is a separate faceuse. When an edge is shared by three or four faces (which is what you see locally along any intersection curve before it has been resolved), the edgeuses around it form a radial list ordered by the right-hand rule. A twin-edge structure that allows exactly two faces per edge cannot represent either case, and booleans spend their whole middle phase in exactly that state.

Wireframe of two boxes placed side by side so that they share one face, drawn in blue between them.
Two cubes sharing a face: one face, two faceuses, three regions (the infinite void and each box). A manifold-only structure cannot hold this. (UsdSolid proposal, 'Non-manifold cubes' example.)

The radial-edge model also pays off in interchange. The proposal maps its entities onto every major kernel; part of that table:

AOUSD / usd-brepParasolidACISOpen CASCADE
BRepBodyBodyTopoDS_Compound
RegionLumpLumpTopoDS_Solid
ShellShellShellTopoDS_Shell
FaceFaceFaceTopoDS_Face
LoopLoopLoopTopoDS_Wire
EdgeuseFinCoedgeTopExp_Explorer
FaceuseSideSideOrientation

The explorer below is a hand-built model of a block with a hole through it, laid out by the schema's counting rules. It is not the output of a kernel run, and a real kernel might split each circle into two arcs, which would change the numbers. What it does show is the structure.

Click the bore. Its one loop is four edgeuses: the bottom circle, the seam, the top circle and the seam again, in the opposite direction. A cylinder's parameter space is periodic in uu, so the face needs an edge where u=0u = 0 meets u=2πu = 2\pi, and the face uses that edge twice. The proposal's rules make this mandatory ("A face has a single outer loop (seam edges are required)"), and seams turn out to be one of the places this kernel struggles most, as we will see. The counts on the right come straight from the model: 10 vertices, 15 edges, 7 faces and 9 loops give 10−15+7−(9−7)=010 - 15 + 7 - (9 - 7) = 0, which the Euler–Poincaré formula turns into genus 1. One hole, as expected. Every edge is used exactly twice, in opposite directions, which is what "closed and consistently oriented" means for a manifold solid.

Tolerance is where the trouble lives

If geometry were exact, an edge would lie exactly on both of its faces. It never does. The intersection of two curved surfaces has no closed form, so the kernel fits a B-spline to it within some tolerance, and that curve lies on each surface only to within that tolerance. Imported files carry the tolerances of whatever system wrote them. Every comparison in the kernel ("are these two points the same vertex?", "does this curve lie on that face?") is really "are they within tolerance?".

SMLib's defaults are in source/SMLib/inc/SmMath.h:52-54:

#define SM_ZONE_TOL_3D    1.0e-5                          /* Local Neighborhood Size, used by SmTol::GetZoneTol3d() methods */
#define SM_XSECT_TOL_3D   (2.0*SM_ZONE_TOL_3D)            /* Default Gap and SmTol::GetXSectTol3d Size                      */
#define SM_APPROX_TOL_3D  (SM_ZONE_TOL_3D/2.0)            /* Default SmTol::GetApproxTol3d Size                             */

Three numbers, three jobs: a zone tolerance of 1e-5 for "these are the same point", an intersection tolerance of 2e-5 for gaps and loop closure, and an approximation tolerance of 5e-6 for fitting curves. They are in model units, so they mean something different in millimetres than in metres. Individual vertices and edges can carry their own looser tolerances on top of these, which is how a kernel absorbs a sloppy import without rejecting it. The USD side keeps one number per solid, brep:intersectTol3d; the cube in the repository's test files stores 0.00002, which is the 2e-5 default.

This is the part of kernel engineering that never shows up in a feature list. smlibpy's docs give a concrete sense of the scale: faces the kernel fits (B-spline chamfers, shell offsets, intersections of curved faces) "close up only to about 1e-6 to 2e-5 mm", and the volume of such a body depends on that by 1e-7 to 4e-6 of its value. Small, and enough to make two operations that should agree disagree.

What is in the repository

I counted the release commit, c9c5979 (version 1.0.0, 9 October 2026). The "Lines" column includes comments and blank lines; "Code lines" strips them:

PartFilesLinesCode lines
source/SMLib (the kernel)462967,172534,172
of which NL_* (the NLib NURBS layer)40253,923
other source/ libraries (C++)64,01749,190
tests/ (C++)63,10244,065
Python, all directories7936,914

All C++ together is 1,096,957 lines. The test fixtures are another 580 files (STEP-derived solids, fillet and drop-curve cases, Open CASCADE .brep files, and 126 USD files for the validator, most of them deliberately broken in one way each). The C++ regression harness prog_test runs 17 suites from topology and booleans through fillets, sweeps, surface-surface intersection and stitching, and I counted about 530 Python test functions across the repository, 400 of them in test_omni_solid.py.

The layering is clean once you see it:

Where does SMLib come from? The repository never says. Every source file carries a 2026 NVIDIA SPDX header and the README says "Copyright (c) 2020-2026, NVIDIA". The rest I pieced together. An archived 2019 copy of smlib.com, the site of Solid Modeling Solutions, describes "SMLib™ Our flagship product built upon TSNLib™ provides fully functional non-manifold topological structure and solid modeling functionality including the ability to perform NURBS boolean operations, fillets, etc.", stacked on GSNLib and on "NLib™ A comprehensive BSpline NURBs library". The NL_ prefix and the NL_KNOTVECTOR types in the source match that stack, and AWV Informatik says it has used SMLib for 20 years. I could not find how or when the code moved to NVIDIA, so I won't guess.

The same archived page lists "Data Translators" for IGES, STEP, SAT, VDAFS, OpenNURBS and Parasolid XT. None of those are in this release. I come back to that below.

A Boolean, traced through the code

Booleans are the operation everyone uses and nobody sees. The Python call is one line:

import _omni_solid as sm
box = sm.create_box((0, 0, 0), 10, 10, 10)
sph = sm.create_sphere((5, 5, 5), 4)
diff = sm.boolean_difference(box.copy(), sph.copy())   # inputs are consumed; copy to keep them

SmApiBrep.cpp:231-238 dispatches on whether both inputs are manifold solids, to SmMerge::ManifoldBoolean or NonManifoldBoolean. The manifold path then calls SmTopologyIntersector::IntersectInsertRelate() (SmTopologyIntersector.cpp:1092), and its comments lay out the phases:

  1. Intersect topology (:1184): find every place the two solids' vertices, edges and faces meet, including surface-surface intersection curves. Results go into a scratch structure of proto-topology (proto-vertices, proto-edges) rather than into the solids.
  2. Resolve intersections (:1208): merge near-coincident proto-vertices, decide which overlapping proto-edges are one edge. The comment after each of the first two phases reads "If that didn't work, quit before we mess up the Breps."
  3. Imprint intersections (:1232), with the comment "now we'll modify the Breps": split faces and edges along the resolved curves.
  4. Relate (:1263): record which new pieces of each solid correspond, so SmMerge can classify each face as inside or outside the other solid and keep or delete it by operation.

That separation (find everything, decide everything, only then mutate) is what you want in a kernel, because a half-imprinted solid is unrecoverable. The decide phase is also where the judgement calls live, and one of them is right there in the source, at SmTopologyIntersector.cpp:1145-1158:

// Capture Distance: simply using entities' tolerances will miss intersections
// that could possibly go into the same ProtoVertex.  We should collect candidates
// into the same PVs, and let Resolve() figure it out, making either one or more
// SmVerttices out of each PV.
// This is a concept in progress.  We currently check the tolerance value in
// the TopologyIntersector, which ultimately comes from the tolerance in the Breps. [B669]
// We might want to look at Bounding Box sizes, or min Edge length, or such.
// But currently this works well.
double dCaptureDistance = 0.01;
 
// Make sure it's not smaller than our tolerance value.
double dTopoIntTol = this->GetThisApproxTol3d();
if ( dCaptureDistance < dTopoIntTol * 2000 )
  { dCaptureDistance = dTopoIntTol * 2000; }

With the default approximation tolerance of 5e-6, the floor is 2000 × 5e-6 = 0.01, so the two numbers coincide. It is an absolute distance in model units: a hundredth of a millimetre for a part modelled in millimetres, a centimetre for one modelled in metres. I like that this is visible. Every kernel has constants like this; in a closed one you find them by bisecting failures.

The repository's own guide for booleans adds the practical advice: avoid tangent or coincident surfaces ("Offset one body by a tiny amount if needed"), watch for features thinner than the tolerance, and check is_manifold_solid() after each step. Those are the classic failure modes of every kernel, open or not.

Solids inside USD

The other half of the release is the part only NVIDIA would have built. OpenUSD, the scene format Omniverse is built on, has meshes, subdivision surfaces and untrimmed NURBS patches. It has no solids. When CAD data enters USD today it is tessellated at the door, and the exact geometry stays behind in a STEP file.

usd-brep adds a schema, omniSolid, with one concrete prim type, BrepArray, and a family of applied API schemas for geometry (BrepSurfaceNurbAPI, BrepSurfacePlaneAPI, BrepSurfaceCylinderAPI, cone, sphere, torus; BrepCurve3dNurbAPI, line, circle, ellipse; BrepCurveUvNurbAPI for trim curves). The whole radial-edge structure is flattened into parallel arrays of indices: region:type, shell:faceuseCount, faceuse:faceIndex, face:loopCount, loop:edgeuseCount, edgeuse:edgeIndex, edgeuse:nextRadialEUIndex, and so on. The schema's documentation explains how it compresses the in-memory structure: "There are no attributes for Vertexuses and Loopuses", and each runtime pair of mated edgeuses becomes one array entry. The test cube shows the result: 2 regions, 2 shells of 6 faceuses each, 6 loops of 4 edgeuses each, and 24 edgeuse records for 12 edges.

Geometry is packed by type. All the NURBS surfaces of every face sit in one set of arrays (brep:surface:nurb:controlVertices, uKnots, vKnots, weights, …) in double precision; all the planes in another. Several solids can share one prim, their entries laid end to end.

Why so flat? The proposal, which usd-brep vendors unmodified, reports the designs the working group tried first. One prim per surface and curve "was an untenable design": a car surface model came to about 500 B-reps with 100 geometry prims each, 50,000 prims for one model. Moving geometry into one prim per B-rep "improved performance and shrunk file size by 1/3", and the array design packs further. Treating B-reps as an opaque blob (an embedded STEP file, say) was rejected because it gives up per-face materials, USD composition and overrides.

Some limits are stated plainly. The schema is for using CAD data in USD (viewing, measuring, clash detection, re-tessellation), not editing it on the stage; the proposal says live editing would need schemas for "the 11 topology and use objects". The AOUSD proposals (OpenUSD-proposals pull requests 108 and 109, opened 20 May 2026) are still marked Draft, which is why NVIDIA ships the schema under its own omniSolid name and keeps the proposal's prelimUsdSolid copy beside it for reference. And the round trip is lossy in documented ways: per-entity tolerances "collapse to one per-BRep value", edge curves are exported as NURBS, and analytic faces lose their UV trims. NVIDIA's own asset-generation guide tells agents not to round-trip through BrepArray at all when the goal is a mesh.

The supporting tools are solid. The validator (tools/brep_validator, also available as pip install "usd-brep[validation]") checks the topology arrays rule by rule, with a broken fixture file per rule. The Hydra plugin tessellates once per prim, in parallel across prims, and never writes meshes back to the stage. usd.heal_file heals every BrepArray in a composed stage and writes it back with layers, references and instancing intact.

The Python layer, and docs written for agents

The stable Python module is broad. The operation index lists 16 primitive constructors, 13 boolean variants (including CSG trees and 2D booleans), 13 sweeps, 9 fillet and chamfer calls, 22 curve calls, 11 surface constructors, offsets, stitching, 8 intersectors, cut, project and trim, tessellation, polygon-mesh booleans, healing and the USD import and export calls. Topology is walkable from Python: face.edges(), edge.faces(), vertex.point(), face.normal_at_uv(u, v).

Two conventions deserve a warning before you write code against it. Booleans consume their inputs ("Ownership of the named inputs transfers to the operation; use only the returned Brep afterward"), so you .copy() anything you want to keep. And the kernel is not thread-safe: the concurrency guide says the process-global context "holds unlocked mutable shared state", operations run single-threaded in the shipped build, and the unit of concurrency is the process. One worker process per job.

The pip wheel is usd-brep 1.0.0, for CPython 3.12 only, about 90 MB on Linux x86-64 because it bundles its own OpenUSD 25.11 runtime. Building from source uses NVIDIA's repo/premake/Packman tooling; on macOS the README notes that the premake package "is not on the public Packman server", and someone in the launch thread asked about exactly that.

Then there are the agent docs. AGENTS.md is 19 KB, and .agents/ holds 20 per-operation guides, an error-code decoder and 19 skills ("smlib-kernel-booleans", "smlib-kernel-healing", "usd-brep-schema", "text-to-3d-asset-generation" and so on). They are good. The B-rep format reference alone is 1,211 lines and explains invariants (radial list order, which edgeuse carries the trim curve, how seams at poles go wrong) that I have never seen written down for any kernel. NVIDIA clearly expects a lot of the code that calls this library to be written by models.

smlibpy: the first downstream, and what it found

smlibpy is AWV Informatik's Python library on top of usd-brep. AWV makes ClassCAD, and its post announcing the open-sourcing came with a short reel of the products it says run on SMLib; the post lists headless CAD, a GUI modeller, AI/MCP tools and a PDM system.

ClassCAD's reel of the products it builds on SMLib, posted the day usd-brep went public. It is their marketing; the article checks the code, not the products. (video from @0xca0a's post on X).

smlibpy is MIT-licensed and borrows its API from CadQuery and build123d. Its README example is a plate with rounded corners, four holes and a pocket:

from smlibpy import *
 
plate = Box(80, 60, 10)
plate = plate.fillet(plate.edges().parallel_to(Z), 8)
plate -= GridLocations(60, 40, 2, 2) * Cylinder(4, 10)
plate -= Rectangle(30, 20).on(plate.faces().top()).extrude(-4)
 
plate.export("plate.step")

Shapes combine with +, - and &. Faces and edges are selected by geometry (faces().top(), edges().parallel_to(Z).concave()), and every operation returns a new part instead of consuming its inputs. The README says the plate builds in about 40 ms; I did not run it. It adds a lot that the kernel's own bindings lack: sketches with a pen-style Profile, sheet metal with exact bends and flat patterns, draft analysis, text from font outlines, its own STEP reader and writer, DXF, glTF, and a software renderer that draws technical drawings with hidden lines.

A grid of nineteen rendered tiles of a pipe flange and an L-bracket: an isometric shaded view, first- and third-angle technical drawings, hidden edges dashed, a capped and hatched section, section views, a before-and-after pixel diff, a four-view sheet, highlighted faces and markers, a sketch over a part, size annotations, an x-ray view, one colour per body, mirrored views, zebra stripes on a circular fillet and on a G2 blend, and curvature maps of both.
What smlibpy's renderer draws, with numpy and no GPU. The zebra stripes kink on a circular fillet and bend smoothly across a curvature-continuous blend, which is the classic way to see surface continuity. (smlibpy README, docs/readme/renderer.png.)

The part I found most interesting is not the API. It is third_party/patches/. smlibpy compiles the kernel from the usd-brep submodule as published, except for two files it replaces with fixed copies, and its README documents every change. One fixes a write into freed memory "at the end of every Boolean", which they found with AddressSanitizer on Box(40, 30, 20) - Cylinder(5, 40). Another stops a vertex-gluing step from deleting curves another solid still uses; they say fillet removal on an imported valve body crashed the process in 3 of 3 runs before the fix.

The third is the one I checked line by line, because it explains an item in NVIDIA's own KNOWN_ISSUES.md: "remove_fillet removes nothing and reports success", workaround "none". Here is SmVertex::GetVertexuseOfFace upstream, at source/SMLib/src/SmVertex.cpp:256-267:

// Remove Composite
//      // when VU connects to TgtFace (or one of its member faces)
//      if(   (!bIsComposite && pThisFace == pFace)
//         || ( bIsComposite && ((SmCFace*)pFace)->ContainsFace(pThisFace)))
        {
          // remember any found Vertexuses
          pRet = pVU;
 
          // Use the first Vertexuse found on an upward Faceuse.
          if ( pThisFace->GetUpwardFaceuse() == pFU )
            { break; }
        }

The function is supposed to find the vertex's use on a given face. Someone removing support for composite faces commented out the whole condition, including the plain pThisFace == pFace test, and left the braces. The function now returns the use on whichever face comes first, whatever face you asked for. smlibpy restores the face test here and in GetLoopOfFace. Their measurement: kernel fillet removal on a box with four rounded edges now gives back 6 faces and a volume of 24000.0, where before it removed nothing and left 10 faces.

The comment // Remove Composite appears 458 times across 50 files of the kernel. I did not audit the others. But it suggests that at least part of preparing SMLib for release was stripping a feature by commenting code out, and that the test suite did not catch every place that went wrong. Finding that within a day of release is what an open kernel is for.

smlibpy also replaces some of the kernel's numerics instead of patching them. Its docs say the kernel's mass-property integration "controls its steps by the area alone", so "a volume can be 300 times worse than the accuracy asked for", and that SmCurve::Length counts a span starting with a zero tangent as zero (the outline of a cylinder came out with 30 mm top and bottom lines instead of 60). smlibpy integrates volumes and inertia itself, over the trim curves by Green's theorem with adaptive Gauss–Kronrod quadrature, in src/cpp/measure.cpp. NVIDIA's known-issues file independently reports area errors "up to about 1.4% at the default relative_accuracy of 1e-3" on faces bounded by circles, so the two accounts agree that the kernel's integrals need care. I did not reproduce either number.

A shaded pipe flange: a round plate with eight bolt holes and a cylindrical neck with a filleted base. A thin vertical line runs down the outside of the neck and down the rim of the plate.
smlibpy's flange example. The thin vertical lines on the neck and on the rim are seam edges, the place each cylindrical face's angle parameter wraps from 2π back to 0. (smlibpy docs, docs/images/flange.png.)

Seams come up again in smlibpy's table of kernel behaviour it works around: NX and other systems write cylindrical and spherical faces without the seam edge SMLib requires, so smlibpy inserts one on STEP import, and turns the surface's frame so the seam lies where the angle parameter starts. NVIDIA's known-issues list has ten separate bullets about healing faces on closed surfaces, seams and poles. If you take one thing from the topology explorer, take the bore's doubled seam edge: it is ordinary in theory and a steady source of bugs in practice.

Where agents come in

AWV's pitch for smlibpy is agents: "It comes with an MCP server, so your agent can build parts too." The server (smlibpy-mcp) gives a model a run_script tool that executes smlibpy Python with variables kept between calls, a snapshot tool that renders the model (drawings, sections, x-ray, highlighted selections, before-and-after diffs), inspect, checkpoints, and per-function guides. The skill has about 140 reference pages, and the repository's ai/training/ directory holds the scripts agents ran while writing them.

The smlibpy website. Left: the headline 'CAD in Python.' and a description. Right: a mock agent session asked to make a pipe flange, showing calls to smlibpy docs, run_script reporting a volume and 17 faces and 'valid', a four-view snapshot sheet, and a saved flange.step, beside a live wireframe viewer of the flange.
smlibpy's own picture of the loop: the agent writes a script, reads back the volume, face count and validity, looks at a rendered sheet, then saves STEP. The numbers in the mock session are theirs. (smlibpy.classcad.ch, from @0xca0a's post on X.)

I think the case for an open kernel here is real but narrower than the replies suggested. Language models are bad at geometry and decent at writing code that calls a geometry library. What makes the loop work is that a B-rep can answer questions precisely: the volume is 157,979.5 cubic millimetres (in their demo), the selection picked 8 circular edges, the solid is manifold. A mesh pipeline cannot give the model that kind of feedback. Agents that write OpenSCAD or Blender scripts already exist (the WorldClaw paper builds whole terrains that way), but they produce meshes, not parts you can send to a machine shop as STEP. An Apache-licensed kernel removes the per-seat licence question for anyone who wants to host such a server, ship it inside a product, or run thousands of copies as an environment to train CAD agents against, which one reply suggested.

Two caveats from the code. The kernel crashes; smlibpy runs risky operations in a forked copy with a timeout (smlibpy.defeature's guard) and rebuilds sessions from their scripts after a crash, and any hosted agent tool will need the same. And one process per concurrent job is the rule, so a server for many agents is a pool of worker processes, not threads.

Update, 13 October 2026: smlibpy in the browser

The afternoon after this went up, @0xca0a announced smlibpy's open-source release with three parts: the Python API, "a wasm compile for javascript/ts (1.8mb dl/brotli total)", and the MCP server. The Python API and the MCP server are what the sections above read at f339a0b. I re-read the repository at 0aaa37f ("Plugin: smlibpy 0.2.3"), 67 commits later. The new part is js/, published to npm as @classcad/smlibwasm.

How it is built (read in the code). js/CMakeLists.txt links the same smlib and sm_api static libraries the Python wheel uses, patched files included, plus smlibpy's own C++ from src/cpp, into one Emscripten module: embind, -fwasm-exceptions (the kernel throws in places it never catches, and this turns that into a JavaScript error instead of a dead module), MODULARIZE as an ES module, memory from 64 MB growable to 4 GB, a 4 MB stack, Emscripten SDK 6.0.12, a plain CMake Release build. Kernel objects cross into JavaScript as bare addresses, and a TypeScript layer of about 29,000 lines owns and frees them. The API is smlibpy's in camelCase, operators become fuse, cut and intersect, keyword arguments become a trailing options object, and js/PORTING.md says the port keeps "the same algorithms, checks, tolerances, messages and fallbacks". The package README's plate prints a volume of 45440.00. I get the same by hand (reasoned): 48,000 for the block, minus 549.4 for four 8 mm corner rounds, minus 2,010.6 for four 4 mm holes.

The 1.8 MB checks out (measured). I downloaded the 0.2.3 tarball from the npm registry and compressed each file with brotli at quality 11:

What a browser loadsRawBrotli
smlibwasm.wasm, the kernel6,222,331 B1,541,480 B
smlibwasm.mjs, its loader86,184 B21,233 B
the other 32 dist/*.js files969,432 B243,264 B
_fonts.js, the font for Text130,018 B46,308 B
Total7.41 MB1.85 MB

That is 1.77 MiB, so "1.8" holds in either unit. The docs put the library at 0.21 MB; I get 0.24, which does not move the total. The WebAssembly has no name section, and code is 5.9 MB of its 6.2. The whole is what the docs' size table says, not a budget: CI builds and tests the module but checks no size. The package is MIT for smlibpy's code, with SMLib under Apache-2.0 and the font under OFL, declared as MIT AND Apache-2.0 AND OFL-1.1, published with an npm provenance attestation.

A third kernel patch (read in the code; the effect is AWV's). Porting surfaced a bug the native build had only hidden. SmOffsetExecutive::RemoveExcessFaces deleted faces and kept them in the executive's lookup tables, so the rest of the offset read freed edgeuses and faces. Natively that was silent corruption; in WebAssembly it trapped. third_party/patches/ now replaces three kernel files instead of two, and AWV reports AddressSanitizer clean on a half cylinder thickened by -12.

The fillet-removal fix travels with it (read in the code). The WebAssembly build compiles the patched SmVertex.cpp, and js/test/defeature.test.ts repeats the Python check: kernel fillet removal on the rounded box must give 6 faces and 24000.0. I did not run it.

The MCP server barely moved, and the "training data" is documentation (read in the code). Since f339a0b the server learned to show a read STEP file (its curves, coordinate systems and points) and an imported assembly, and its search knows read_step and draft. The JavaScript package has no MCP server. What the thread calls "real world training data" is ai/training/: 34 sessions from 12 and 13 October with 536 probe scripts, each written by an agent following ai/HOW-TO-TRAIN.md ("everything is live... If the docstring is wrong, say so"), whose output is the 143 reference pages the server hands out. No model is trained. Nothing in that folder changed today.

Two smaller things for the kernel story: Part.draft is back as an operation, built on SmBrep::LocalOperation, and STEP import now gives seams to more of the faces NX writes without them (tori, revolutions and extrusions, not only cylinders and spheres), the seam trouble described above. The usd-brep submodule still points at c9c5979, so none of this changes the kernel the earlier sections counted.

What is missing next to Parasolid, ACIS and Open CASCADE

Plainly, from what I could check in the code and docs:

What I could not check: robustness and speed against Parasolid, ACIS or Open CASCADE on the same models. Nothing in either repository compares them, and I did not run anything. One reply under the launch post said that on a personal set of difficult chamfer tests, "Occt passes many more cases than Smlib". That is one person's anecdote, and the honest status is that nobody has published a comparison yet. Neither could I verify how NVIDIA acquired SMLib or how much of the commercial product's history survives in this snapshot (the repository is a single release commit).

The licence comparison is easy, though. Open CASCADE is LGPL 2.1 with an exception; usd-brep is Apache-2.0, apart from MIT-licensed bootstrap scripts and the usual third-party notices (OpenUSD, oneTBB, pybind11 and so on). For a company that wants to modify the kernel and ship it inside a closed product or a hosted service, that difference matters more than any benchmark.

What I would do with it

If you want to model parts from Python today, start with smlibpy, not the raw bindings: pip install smlibpy gives wheels for Python 3.10 to 3.14 on Linux, macOS and Windows at 5 to 7 MB, with the two kernel fixes compiled in and the integrals done properly. If you are in the USD world and need exact solids in a stage, usd-brep's schema, validator and Hydra plugin are the reason to look, and they are the part of this release with no open equivalent. If you are building an agent that designs parts, copy smlibpy's structure: scripts in a separate process, a renderer the model can look through, and a numeric check after every step.

And if you work on geometry kernels, read .agents/skills/smlib-brep-model/references/brep-format.md. It is the best description of a production B-rep's invariants I have seen in public, and now it comes with the million lines it describes.

How I checked

I shallow-cloned NVIDIA-Omniverse/usd-brep at c9c5979 and awv-informatik/smlibpy at f339a0b on 13 October 2026 and read them; I did not build or execute either. Line counts are git ls-files totals for .cpp/.h (and .py) files; "code lines" strip /* */ and // comments and blank lines with a short script. Test counts are def test_ occurrences and the gProgSuites table in tests/prog_test/src/prog_test.cpp. The smlibpy kernel patches were checked by diffing their copies against the upstream files; the effects of those patches (6 faces, volume 24000.0, the AddressSanitizer reports, crash counts) are AWV's measurements, not mine. The 6% bulge of the unweighted circle is my own calculation, sampled at 2,001 points, and the widget recomputes it live. The SMLib product description comes from the Internet Archive's December 2019 copy of smlib.com. PyPI metadata (wheel sizes, Python versions, upload dates) is from the PyPI JSON API. For the update I re-read smlibpy at 0aaa37f, diffed it against f339a0b, and measured the npm tarball of @classcad/smlibwasm 0.2.3 with Node's brotli at quality 11; I did not build or run the WebAssembly either. Quotes from the UsdSolid proposal are from the copy vendored in proposals/, which NVIDIA records as an unmodified snapshot of OpenUSD-proposals pull request 109 retrieved on 16 September 2026.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "usd-brep: NVIDIA open-sources SMLib, a million-line NURBS kernel with a USD schema on top", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026usdbrepnurbskernel,
  author = {Satyajit Ghana},
  title  = {usd-brep: NVIDIA open-sources SMLib, a million-line NURBS kernel with a USD schema on top},
  url    = {https://ai.thesatyajit.com/articles/usd-brep-nurbs-kernel},
  year   = {2026}
}
share