Skip to content

Curves and surfaces evidence (G0-G4)

This ledger records the G0 contract freeze and reference corpus, the G1 cubic and bicubic kernel, the G2 prepared paths, tracks and distance maps, the G3 frames, projection and sweep meshes, and the G4 patch topology and CPU surface meshes. See the architecture, ADR 0024 and the consulted-source review.

G5 cooked geometry content and editor adapters

Implementation date: 2026-11-03. Base: f8763a8 (develop). Scope: the header-only Ludus::GeometryFormat target (modules/content/geometry_format), the Ludus::GeometryContent codecs/cooker/loader (modules/geometry/content), a content::Kind::CookedGeometry catalog extension at catalog schema version 2, and the editor-private authoring adapters (apps/editor ludus_editor_geometry_model: CurveDocument, GeometryPrepareJob, GeometryPreview, GeometryLiveBuild and ReloadCursor). No corpus reference changed. No renderer binding is added; GPU presentation of a prepared curve/surface remains G6.

Format and cache contract

One LUGEO blob holds exactly one geometry kind. The header carries the magic, the format and cooker versions, the kind and precision tags, a flags word, the record counts, the owner revision, the chunk origin, asset scale, position floor and effective position error, the whole 12-field ToleranceProfile, a checked section directory of {id, offset, count, element bytes} entries, and three SHA-256 digests. Record widths are static_asserted against constants derived from the field schema (PATH_SPAN_RECORD_BYTES = 64, JOINT_RECORD_BYTES = 40, TRACK_KEY_RECORD_BYTES = 24, PATCH_RECORD_BYTES = 200, ADJACENCY_RECORD_BYTES = 20, SOURCE_IDENTITY_RECORD_BYTES = 16, SECTION_ENTRY_BYTES = 16), so the directory's element width equals the bytes the field-by-field codec actually writes. The cooked header holds exactly the cache key the architecture lists (source digest, dependency digest, format and cooker versions, schema version, precision and every tolerance).

Gate evidence

Gate Test evidence
Source round-trip WriteGeometrySource then ParseGeometrySource reproduces the mode, closure, author IDs and span controls; re-writing the reparsed source yields byte-identical canonical JSON
Blob round-trip and determinism Every G0 corpus cubic fixture cooks to a LUGEO blob, loads back to a PreparedPath, and evaluates its start endpoint bitwise; re-cooking the same source is byte-identical
Corrupt/overflowing blobs Truncation below the header, a corrupt magic and an unsupported version each fail with a structured diagnostic. Flipping the high bits of every byte in a cooked blob (including the section directory and count fields) always either loads a valid owner or fails with a diagnostic; the loader never asserts, reads out of bounds or mutates outLoad on failure. count x element bytes, the section extent and the header size use checked TryMultiply/TryAdd/TryIntegerCast, so an overflowing count is OutOfRange, not a wrap
Section validation Each section is a known id with the matching element width, lies after the header, is four-byte aligned, in range and non-overlapping; a kind's required sections must be present. The payload digest is recompared on load for cache consistency
Unknown fields/versions, duplicate IDs An unknown schema version is UnsupportedRepresentation; non-JSON and an unknown mode are Invalid; duplicate span/key author IDs and non-monotone key times are rejected with diagnostics
Catalog versioned extension A version-2 catalog admits cooked-geometry; a version-1 catalog rejects it as Invalid; Write emits version 2 only when a geometry entry is present so geometry-free catalogs round-trip as version 1
Allocation failure Every fallible allocation in CookGeometry and LoadGeometry is failed in turn: the call returns a transport failure and leaves the active owner (the blob or the load) unchanged. Evaluation after a successful load allocates nothing
Undo/redo Semantic edits are undoable and the document revision is monotonic; a drag coalesces into one undo step; a checked head rejects a mismatched current state; duplicate control IDs are rejected with a repair suggestion
Failed and cancelled prepare A failed replacement keeps the active view and raises StaleFailed; a cancelled job reports Failed with a Cancelled transport and keeps the active view
Late completion A completion whose generation/revision no longer match the expected identity is rejected and never publishes; the active view and its stale flag are unchanged
Stale cursor ReloadCursor under RetainParameter clamps an out-of-range span index and u into the new path and stamps the new revision; the distance and projection policies return MissingCache without an implicit fallback
Active+candidate peak memory The preview holds exactly one owner when idle and two while a candidate is pending, dropping back to one after the tick publication

Departures and limits

  • The text source schema and cooker implement the cubic-path, scalar-track and (parse/cook/load) bicubic-surface kinds directly. Hermite and centripetal Catmull-Rom waypoint sources are authored through the editor adapter, which converts them to cubic spans before cooking; the text schema reports UnsupportedRepresentation for those modes until a waypoint schema is added.
  • ReloadCursor implements RetainParameter from the view alone. The distance and projection policies require their caches (a PreparedDistanceMap or a projection query), which sampling never builds; the adapter reports MissingCache so the consumer supplies the cache explicitly.
  • The dependency digest is the canonical empty-input hash for a self-contained asset; cross-asset dependency hashing is a follow-up when a geometry source references another.
  • GeometryPrepareJob pumps one bounded stage on the owner thread, as the current preview adapters do. Moving preparation onto the FoundationThreading JobGraph is an available follow-up; the immutable-input/exclusive-output contract already matches that graph's requirements.
  • No renderer upload or on-screen presentation of a prepared curve/surface is added; that is G6.

G5 validation record

Host: Amazon Linux 2023 on x86_64. The pinned reference toolchain is Clang/LLVM 23.1.3 with CMake 3.29.6 (ADR 0025, config/tool_versions.json), which is not available in this implementation sandbox (only system Clang 15.0.7 and CMake 3.22.2, which cannot parse CMakePresets.json). The warning-clean pinned-Clang-23 build, ./scripts/check --format (clang-format 23), ./scripts/check --tidy (clang-tidy 23), the ASan/UBSan lanes, the strict MkDocs/Doxygen tooling and the installed SDK consumer therefore run in batch CI, as the prior phases' records also deferred ASan/Wasm/SDK.

Check Result
Standalone clang++-15 -std=c++2b -fno-exceptions -fsyntax-only of every new translation unit Passed (header-only format, the three GeometryContent sources, the edited catalog.cpp, the editor adapter, and all five test files against a Catch2 shim)
Record-width static_asserts (Ludus::GeometryFormat) Pass at compile time; covered again by ludus_geometry_format_tests
Warning-clean pinned-Clang-23 build Not run locally (toolchain unavailable); CI
ludus_geometry_format_tests, ludus_geometry_content_tests, ludus_geometry_content_alloc_tests, ludus_editor_geometry_tests Not run locally (Catch2 and the pinned toolchain unavailable); CI
./scripts/check --format (clang-format 23) Not run locally; CI
python3 scripts/python/source_policy.py --base origin/develop Attempted; recorded with the PR
./scripts/check <preset> --tidy-changed --base origin/develop Not run locally; CI
check-docs, strict MkDocs, scripts/build-api --bootstrap, scripts/check-wiki check-docs --fix-index run where Python deps resolve; MkDocs/Doxygen/wiki in CI
ASan/UBSan, installed SDK consumer, Wasm probe Not run locally; CI

The format acknowledges the two in-tree precedents it mirrors: Ludus::SceneFormat (modules/content/scene_format, the LUSCN field-by-field little-endian contract) and Ludus::RenderGraphFormat (modules/content/render_graph_format, the blob header with versions, SHA-256 identities and count-prefixed sections). The cook and digest flow follows Ludus::SceneCook; the loader validation follows modules/runtime/resources SceneDefinition::Decode; the editor adapters follow DocumentHistory, RenderGraphLiveBuild, RenderGraphJob, PreviewGpuGeneration and RenderGraphPreview. No source code from those modules is copied.

G4 patch topology and CPU surface meshes

Implementation date: 2026-10-17. Base: c3de79d (develop after G2; rebased onto 6615a2c before review). Scope: FoundationGeometry surface.hpp and tessellation.hpp, their implementation and tests, a WorkLimits::MaxPatches field, and the SDK-consumer and Wasm probe exercise. The G2 box helpers moved into a shared private internal/control_box.hpp without changing their arithmetic. Usage is documented in the module guide. No frame, projection, sweep, cooked format or editor binding is added, and no corpus reference changed.

Certified cell bound

For a cell with exact corners P00, P10, P01 and P11, the emitted triangle pair T' differs from the patch S by at most three terms:

  • Control net. |S - B|, where B is the bilinear interpolant of the corners. Degree-elevating B to bicubic puts its controls at the bilinear values ((3-i)(3-j) P00 + i(3-j) P10 + (3-i) j P01 + i j P11)/9. S - B is a bicubic with the control differences, so by convexity it is at most their largest length. Corner differences are exactly zero and are skipped.
  • Twist. |B - T| for the triangles (00, 10, 11) and (00, 11, 01). On the first triangle (u >= v), B - T = -v(1 - u) w with w = P00 - P10 - P01 + P11. On the second it is -u(1 - v) w. Both factors are at most 1/4, at the centre, so the bound is Length(w)/4.
  • Emitted vertices. |T - T'|. A triangle point is a convex combination of its vertices, so it moves by at most the largest vertex error.

Subpatch controls are enclosed by restricting each row to [v0, v1], then each resulting column to [u0, u1], with the Ramshaw blossoms G2 already uses. Every product, sum, square root and the 1/9 weight (enclosed by the neighbours of its float64 rounding) uses FoundationMath's outward helpers. Each emitted position rounds its enclosure's midpoint to float32. Its error bound is the outward length of the largest per-axis distance from that value to the enclosure. The same function produces the positions that are emitted, so every cell bound sees the exact emitted values. The bound measures the parametric distance between S(u, v) and T'(u, v), which is an upper bound of their Hausdorff distance.

Gate evidence

Gate Test evidence
Reversed shared edges reversed_shared_edge: edge_first U1 joins edge_second U0 with IsReversed. TryMapEdgeParameter maps every fixture t to (1, FirstV) and (0, SecondV) exactly. Both patches evaluate to the fixture position within 1e-5 (Rounded), and the endpoints match bitwise. The same curve without the declared reversal is InvalidTopology, with the 6.08-unit control gap reported as Rounded
Corner identity The pair has 6 corner records for 8 patch corners. First U1V0 and second U0V1 share the record at (3, 0, 0); first U1V1 and second U0V0 share (3, 3, 0). Stable edge and corner IDs (owner = smallest patch ID and side) and the canonical curve are identical when the patch order is swapped. A side joined to another side of the same patch closes it with 3 edges and 2 corners
UV/normal seams On the shared edge, both patches' render vertices name the same PositionIndex and copy the same position bitwise. Their parameters are (1, v) and (0, 1 - v), and at least one normal pair differs (the pair is not C1, CrossDerivativeDifference is sqrt 2 within 1e-12, and its orientation is inconsistent). Two halves of one bilinear saddle are reported C1 with difference 0 and consistent orientation
Twisted bilinear interiors saddle at level 0 has a Certified bound within 1e-12 relative of the exact 9/4. The emitted diagonal passes z = 9/2 at the centre, where the patch is at 9/4, so the bound is attained. A tolerance of 2.24 with MaxLevel = 0 is BudgetExceeded. At 0.01 the grid is (4, 4), the first level sum where (9/4)/2^(LevelU+LevelV) fits
Matching boundary samples The shared edge's samples are emitted once, 2^L - 1 of them, and the total position count equals corners plus edge plus interior samples. Each fixture edge sample lies within its emitted position's Certified ErrorBound of the exact reference. In a three-patch chain, the planar third patch inherits the curved edge's v level through the second patch's opposite side, with its own LevelU staying 0
Singular patches apex: the whole u = 0 row has Degenerate normals and a zero normal; one triangle per cell along it collapses. Report emits and counts both; Reject fails with Degenerate and the offending parameter, keeping the previous mesh. Corpus normals reproduce Degenerate, IllConditioned and the regular normal through TryEvaluateSurfaceNormal
Strict/visual failure Strict exhaustion of MaxLevel is BudgetExceeded with the achieved Certified bound (at least 9/16 at level 1). Visual without IsCoarserMeshAccepted also fails; with it the mesh keeps the (1, 1) grid and reports IsWithinPositionError = false. Strict with a normal bound is UnsupportedPrecision (Estimated only), and Strict with a coarser-mesh request is InvalidArgument. Cell-test, vertex, triangle and byte budgets, and invalid profiles, fail before output changes

For every corpus patch, the level-0 bound matches the generator's InteriorBound within 1e-12 relative. Where an analytic maximum exists, it is at most the bound: bulge reports 3 against its true 27/16, which the emitted triangles reproduce at the centre. Every mesh in the tests is also sampled 25 times per cell. The sampled distance between the patch and its triangles never exceeds the cell bound plus 1e-5 for float32 evaluation. Halving the twist factor makes the saddle and certificate checks fail. The allocation probe fails every allocation of surface preparation and mesh building in turn. Each returns OutOfMemory and keeps the active owner, and evaluation and edge mapping allocate nothing.

Numerical limits observed

Strict meshes with MaxLevel = 12 on the loaded development host (load average above 75), as workload observations rather than budgets:

Fixture Position error Levels (u, v) Vertices Cell tests Time
plane 0.001 (0, 0) 4 1 < 0.1 ms
saddle 0.1 / 0.01 / 0.001 (3, 2) / (4, 4) / (6, 6) 45 / 289 / 4225 125 / 1021 / 16381 17 ms / 0.32 s / 7.0 s
bulge 0.1 / 0.01 / 0.001 (4, 3) / (5, 5) / (7, 6) 153 / 1089 / 8385 509 / 4093 / 32765 0.14 s / 2.0 s / 11.8 s
apex 0.1 / 0.01 / 0.001 (0, 5) / (0, 8) / (0, 12) 66 / 514 / 8194 125 / 1021 / 16381 8 ms / 0.14 s / 1.4 s
three-patch chain 0.1 / 0.01 / 0.001 (0, 5) each / (0, 8) each / (0, 12) each 198 / 1542 / 24582 283 / 2299 / 36859 20 ms / 0.17 s / 2.9 s

The ruled apex and edge_* patches are linear in u, so only v is refined. Halving the tolerance costs about one level in each curved direction, as the 1/4-per-level twist and second-difference decay predicts. A cell test costs roughly 0.1-0.4 ms: every control box is rebuilt with outward arithmetic, and both refinement candidates are evaluated. Sharing row restrictions across a column and caching vertex errors per grid point are measured follow-ups.

Departures and limits

  • Shared edges must match bitwise after narrowing. The architecture's canonical-boundary adjustment term is therefore exactly zero; joins within a distance threshold are rejected rather than snapped.
  • IsC1 compares cross-boundary derivative controls with ContinuityDerivative. G1 tangent-plane classification across an edge is not provided.
  • Only rectangular dyadic grids are produced, and a direction is refined across a whole patch. Quadtree transitions, camera-dependent targets, displacement and animated controls are not implemented.
  • The normal estimate samples five analytic normals per cell and skips singular ones. It is Estimated and is never accepted by a Strict profile.
  • Collapsed triangles are detected only by bitwise-equal positions, not by area.
  • The Certified claim, like G2's, depends on the CI ludus_web_math_corpus passing on the pinned Wasm toolchain. The probe's Geometry exercise now also builds the saddle mesh and checks its bound against 9/4.

G4 validation record

Host: macOS 26 on arm64 with the pinned host tools (Clang/LLVM 18.1.8), preset macos-clang-development, heavily loaded (load average 75-88). The local preset does not treat warnings as errors; the build reported none, and CI enforces warnings as errors.

Check Result
ludus_foundation_geometry and its tests Built with no warnings
ludus_foundation_geometry_tests Passed: 41 test cases, 108,899 assertions (18 new surface and tessellation cases)
ludus_geometry_alloc_tests Passed: every surface-preparation and mesh-build allocation failure injected; evaluation allocates nothing
Mutation check Halving the twist factor fails the tight saddle bound and the sampled certificate
Pinned clang-tidy on every changed translation unit Passed after removing two swappable-parameter findings and documenting one suppression on TryMapEdgeParameter
Format, naming (check_naming.py), foundational includes, container policy and changed-line source policy Passed against c3de79d
Docs index, Markdown links, strict MkDocs, docs regressions Passed
API reference with pinned Doxygen Passed: no new gaps
ASan/UBSan, full CTest, installed SDK consumer, build-time budget Not run locally (the documented macOS ASan startup deadlock and host load); CI runs them
Wasm math probe Not run locally because the host has no Emscripten SDK. The webgpu-probe workflow runs ludus_web_math_corpus, whose Geometry exercise now builds the saddle mesh; the Certified claim depends on that result

The repository formatter (scripts/check --format --fix) deleted the last member of a multiline designated initializer written inside a ternary. The affected line was rewritten without a ternary, and the formatter defect is tracked separately.

G3 frames, projection and sweeps

Implementation date: 2026-10-17. Base: c3de79d (develop). Scope: frame.hpp, query.hpp and sweep.hpp in Ludus::FoundationGeometry, a private float64 span and Certified box helper, an ADR 0024 amendment that gives sweeps their own header, new frame references in the G0 corpus, tests and the SDK consumer. Usage is documented in the module guide. No surface, patch mesh, cursor, cooked format or editor binding is added.

Reference corpus additions

The generator gained FRAME_FIXTURES; no existing reference changed. Each frame fixture is a unit-width cubic path whose joints are exactly C1. The generator integrates Bishop's rotation-minimizing transport r' = -(r . T') T with the classical Runge-Kutta rule in 50-digit decimals, at 1024 and 2048 steps per span, and combines them by Richardson extrapolation. Its self-tests check step-halving agreement within 1e-13, unit length and tangent orthogonality within 1e-15, the planar closed form (the out-of-plane normal stays exactly +Z), the arctangent against pi/4, pi/3 and -3pi/4, and a nonzero loop holonomy. These references are integrated, not closed form; their error is far below the 1e-3 rad frame tolerance used by the C++ tests.

Fixture Exercises
twisted_rmf The G0 twisted cubic with seed +Y; normals at u = 1/4, 1/2, 3/4, 1
inflection_rmf The G0 inflection cubic with seed +Z; the normal stays +Z where a Frenet frame flips
loop_holonomy A closed Hermite saddle loop without mirror symmetry; residual twist 0.112407 rad. Two symmetric candidate loops had zero holonomy and were rejected

Acceptance gates

Gate Test evidence
Straight line_uniform keeps the projected seed at every sample; the Estimated angular error is 0
Inflected inflection_rmf normals equal +Z within float32 storage; with an in-plane seed the binormal stays +Z through u = 1/2, where a Frenet frame flips
Vertical start vertical_start with a +Y seed is DegenerateFrame with the previous frames kept; a parallel fallback also fails; a +Z fallback succeeds and sets IsFallbackSeeded
Cusped cusp under Reject is DegenerateFrame with a 2^-30-wide range within 1e-6 of u = 1/2; SplitRuns gives two runs, Degenerate at the cusp and +Z normals on both sides. line_ease zero-speed endpoints are Degenerate and the run lies strictly inside the span
Opposite tangents line_backtrack gives three runs, Degenerate at both zero-speed parameters and a reseeded middle run with tangent -X. An exact reversal at a joint fails RotateMinimally and Reject and splits under SplitRuns. A 90 degree corner rotates the +Y normal to -X
Random-access order independence 64 frames on loop_holonomy evaluated forward, backward and in a stride-37 order are bitwise equal; a quarter turn of roll maps the normal onto the binormal
Closed residual twist PreserveHolonomy measures 0.1124056 rad against the 0.1124068 reference and matches the reference normals within 1e-3 rad. DistributeTwist closes the seam within 1e-5 rad, also with one authored winding turn. A non-G1 closing joint cannot be periodic
Multi-minimum projection Every arch query (symmetric ties, apex minima, endpoint minimum) and the three-way line_backtrack tie return a Certified gap that encloses the exact distance, and the earliest minimum within 1e-2 in u. The endpoint minimum returns the stored endpoint. A tie across two spans and a joint minimum return the earlier span. A far-origin path gives the same answer

twisted_rmf frames differ from the integrated references by at most 3.1e-5 rad, below the reported Estimated error of 6.5e-4 rad. Strict frame profiles return UnsupportedPrecision. Other tests cover MissingCache, StaleRevision, invalid preparations and every work limit for frames, transactional strict projection failure (BudgetExceeded with the result kept), the approximate mode's candidate and gap, and scratch capacity. Sweep tests cover a straight ribbon's exact positions, UVs and both windings. A capped tube uses the fewest segments within the cross-section budget, keeps vertices at its radius and its UV seam and cap rims bitwise equal to ring positions, faces outward, and stays within twice its Estimated longitudinal budget at seven extra samples per ring interval. A closed tube's seam ring copies the first ring. Width and roll profiles scale and roll the rings. The arch apex flags self-intersection risk for a unit tube. Singular or aperiodic frames, bad profiles, caps on ribbons or closed paths, and every budget are rejected without replacing the plan, and emission writes nothing on CapacityExceeded, StaleRevision or MissingCache. The allocation probe fails every frame and sweep-plan allocation in turn, and finds zero allocations during frame evaluation, projection with reserved scratch and emission.

Numerical limits observed

These are workload observations at TangentAngleError 1e-3 rad, ProjectionGap 1e-5 and 1e-3 sweep budgets, not budgets.

Case Observation
twisted_rmf frames 7 samples, 11 interval tests, level 3; Estimated error 6.5e-4 rad
loop_holonomy frames 19 samples over four spans, level 3; Estimated error 2.6e-4 rad
arch projections 0 (endpoint), 12-47 (apex) and 2529-2758 (symmetric ties) splits
line_backtrack tie 9 splits
Tube of radius 0.05 16 segments; 35 (twisted), 51 (inflection) and 65 (arch) rings

Symmetric ties are expensive because the tie search refines the earliest ambiguous interval down to the acceptance region. Performance and memory budgets remain unmeasured.

Departures and limits

  • Frame angular error is Estimated: the summed one-step versus two-half-step differences plus the largest one. The regularity decision is Certified from outward-rounded derivative boxes, but derivative enclosures do not yet bound the angle, as ADR 0024 requires for a strict frame profile.
  • Random access transports the preceding baked sample to the analytic tangent with one reflection step instead of interpolating quaternions. Twist compensation is linear in u between samples, whose distances are estimates.
  • A range that cannot be shown regular is 2^-MaxDepth wide, but box overestimation near a simple zero widens the excluded region to about 1e-6 in u at depth 30.
  • Projection closes the global gap to half of ProjectionGap, then resolves ties: every point within the full gap is acceptable, and the earliest live interval that may hold a point within half the gap is refined until it holds an acceptable candidate. The margin makes that search terminate; it shares MaxIterations. The result is the earliest acceptable candidate, not necessarily the exact earliest minimizer. Newton refinement and Bernstein root isolation are not used.
  • Sweeps need one frame run with G1 joints; mitred corners and split sweeps are later work. Longitudinal and cross-section errors and self-intersection risk are Estimated. Ring frames come from float32 frame evaluation.
  • Sweep mesh generation has its own sweep.hpp, amending ADR 0024's header list, so G4 patch tessellation keeps tessellation.hpp.

G3 validation record

Host: macOS 26 on arm64 with the pinned host tools (Clang/LLVM 18.1.8), preset macos-clang-development. The machine was heavily loaded by other agents.

Check Result
Full macos-clang-development build Passed with no compiler warnings; the only linker warnings are the existing Homebrew Qt deployment-target notices from editor targets
ludus_foundation_geometry_tests Passed: 45 test cases, 108,122 assertions
ludus_geometry_alloc_tests Passed: 4 test cases; every frame and sweep-plan allocation failure injected
Fixture generator self-tests (test_fixtures.py) and committed header Passed: 17 tests, header current
Pinned clang-tidy on every new translation unit Passed after documented bugprone-easily-swappable-parameters suppressions
Format, naming (check_naming.py) and changed-line source policy Passed against the c3de79d branch point
Header self-sufficiency and foundational include gate Passed: 171 public headers compile standalone; foundational include boundary OK
Docs index, strict MkDocs, check-wiki --documentation-only, docs/wiki regressions Passed
API reference with pinned Doxygen Not run locally. The first CI run found one gap, the undocumented ProjectionAccess friend declaration, which now has a comment
ASan/UBSan Not run locally: Clang 18 ASan deadlocks at startup on this macOS 26 host (see building), so the CI sanitizer lanes are required
Wasm math probe, installed SDK consumer, build-time budget Not run locally (no Emscripten SDK). The SDK consumer now bakes frames, projects a point and emits a ribbon; these run in CI

The frame code acknowledges Wang, Juttler, Zheng and Liu's double-reflection paper and Carl Dougan's Game Programming Gems 2 chapter 2.5. The projection code acknowledges Philip J. Schneider's Graphics Gems chapter XI.7. The frame references acknowledge Bishop's transport equation and Kutta's Runge-Kutta rule. No source code from those works is copied.

G2 prepared paths, tracks and distance maps

Implementation date: 2026-10-16. Base: cb6f02a (the G0 branch). Scope: the new Ludus::FoundationGeometry module (status.hpp, path.hpp, distance.hpp, track.hpp), FoundationMath enclosure.hpp, and their tests, SDK-consumer and Wasm probe coverage. Usage is documented in the module guide. No frame, projection, surface, mesh, cooked format, cursor or editor binding is added.

Outward-rounding helper

ADR 0024 makes Certified bounds conditional on reviewed outward-rounded arithmetic validated with the pinned native and Wasm toolchains. enclosure.hpp adds float64 enclosures with outward add, subtract, multiply, square and square root. Each operation rounds to nearest, then steps each bound at most one unit outward with std::nextafter; the process rounding mode is never changed. For a correctly rounded operation without overflow, the exact result lies strictly between the neighbors of the rounded one. Knuth's TwoSum detects exact sums and differences, which are not widened. Products and roots are always widened unless an operand is zero, and widened square lower bounds are clamped at zero. Overflow is OutOfRange, never a truncated bound.

The oracle (math/tests/enclosure_oracle.hpp) never calls the helpers' rounding code. It rebuilds each exact sum or product as an unevaluated pair with Dekker's Fast2Sum and Veltkamp-split product, then compares every returned bound with that pair without rounding. A square root is checked by squaring its bounds exactly. The deterministic corpus has 200,000 cases. Each case checks point sums, differences, products and roots, plus a random interval product against all four exact endpoint products. Magnitudes range over 2^-200 to 2^200, and half the cases are near-cancelling. The native [enclosure] tests and the Wasm ludus_web_math_corpus run the same corpus. A mutation that drops the outward step from sums and products fails it.

The helper lives in FoundationMath rather than Geometry, as the architecture's "behind Math's implementation boundary" asks. It is compiled with -ffp-contract=off, and the existing Wasm math probe can then run it. Geometry is also compiled with contraction disabled.

Prepared owners and conversions

Gate Test evidence
Uneven Catmull-Rom knots centripetal_uneven: knots 0, 1, 3, 6 exactly; the middle span's controls and tangents match the exact references within 1e-12 (Rounded float64 arithmetic); converted joints are C1 in the knot domain
Duplicate and closed endpoints duplicate_waypoint is InvalidDomain with outputs unchanged; a closed loop's last span ends bitwise at the first waypoint and its closing joint is C1; a closed source that repeats its first waypoint is InvalidDomain; Wrap on an open path is InvalidArgument; a closed path whose ends do not meet is InvalidTopology
Holds Track Hold returns the endpoint with zero derivative; equal-value keys form a stationary interval whose incoming and outgoing sides differ at its end; a stationary span occupies no distance interval
Prefix uncertainty For all nine cubic fixtures, TotalLower <= ArcLength <= TotalUpper, and every reference distance lies inside TryBoundDistance at its reference parameter, which includes all preceding leaves
Zero speed All five zero-speed parameters return Degenerate tangents with valid positions. Distances at, before and after them resolve within the residual bound, including the cusp's exact zero speed
Inversion residual Every fixture distance resolves with a Certified residual of at most 1e-7. Parameters match the reference within 1e-5, or 2e-3 at the cusp, where u(s) grows like the square root of the residual
Budget exhaustion Leaf, depth, subdivision, byte and zero limits return BudgetExceeded, report the achieved Certified gap and keep the previous map; inversion with too few iterations is BudgetExceeded with its output unchanged

The corpus also covers: exact endpoints and stored controls, and reference samples within 1e-6 relative (Rounded). Far-origin controls narrow bitwise to the fixture's relative controls only after subtracting the origin; without it, the 256-unit floor yields UnsupportedPrecision. Further cases cover the position floor for every fixture, Hermite conversion, capacity checks, line spans, corner and domain-scaled continuity, and every frozen status and guarantee name. They also cover all 19 mixed_track samples (Step, Linear, Hermite, both span sides and every boundary policy), the 43/27 Hermite overshoot left unclamped, duplicate_time, MissingCache, StaleRevision and deterministic refinement. The allocation probe fails every preparation allocation in turn. It finds OutOfMemory with the active owner preserved, and zero allocations during sampling, inversion and caller-owned conversion.

Numerical limits observed

Building each single-span fixture to a whole-path gap of 1e-7 used the leaf counts below, at a maximum depth of 12-13 below the span. These are workload observations, not budgets.

Fixture Leaves Fixture Leaves
line_uniform, line_ease 1 inflection 4281
line_backtrack 25 twisted 3590
cusp 2012 vertical_start 2788
parabola 2107 arch 4771

The collinear ease line needs no split, because its control polygon equals its chord. Polygon-minus-chord gaps shrink about fourfold per uniform halving, so the leaf count grows like the inverse square root of the requested gap. Performance and memory budgets for prepared assets and integrated consumers remain unmeasured.

Departures and limits

  • The strict profile is the only build: every length bound is Certified, and the map reports that class. There is no Estimated Visual build.
  • Inversion bisects on the Estimated partial-span midpoint, then accepts only by the Certified residual. It returns the earliest parameter within float64 resolution, and an exact stored boundary estimate maps to its boundary. Safeguarded Newton steps, cursors and linear-table Approximate lookup are later work.
  • Continuity classification compares float32-rounded controls, so thresholds below float32 relative rounding report C1 joints as not C1.
  • A closed Catmull-Rom source must not repeat its first waypoint: the repeated pair is coincident and is rejected rather than silently dropped.

G2 validation record

Host: macOS 26 on arm64 with the pinned host tools (Clang/LLVM 18.1.8, CMake 3.29.6), preset macos-clang-development.

The local preset does not treat warnings as errors. The build reported no warnings, and CI enforces warnings as errors.

Check Result
Full macos-clang-development build Passed with no warnings (968 build steps)
ludus_foundation_geometry_tests Passed: 23 test cases, 83,831 assertions
ludus_geometry_alloc_tests Passed: every preparation allocation failure injected; sampling allocates nothing
ludus_foundation_math_tests including [enclosure] Passed: 112 test cases, 420,773 assertions; the enclosure corpus has 200,000 cases
Full CTest 87 of 88 passed. ludus_platform_cocoa_tests reports failure because every case skips without LUDUS_TEST_COCOA=1 and a WindowServer session, so it is unrelated to this change
Header self-sufficiency and foundational include gate Passed: 158 public headers
Pinned clang-tidy on every new translation unit Passed after naming-rule fixes and documented bugprone-easily-swappable-parameters suppressions
Format, naming (check_naming.py) and changed-line source policy Passed against the G0 branch
Docs index, strict MkDocs, check-wiki --documentation-only, docs/wiki regressions Passed
API reference with pinned Doxygen Passed: no new gaps
ASan/UBSan Built warning-clean. Execution hit the documented local Clang 18 ASan startup deadlock on macOS 26 (no output after 90 s), so the CI sanitizer lanes remain required
Wasm math probe (enclosure corpus and Geometry exercise) Not run locally because the host has no Emscripten SDK. The webgpu-probe workflow now triggers on Geometry changes and runs ludus_web_math_corpus. The Certified claim depends on that CI result
Installed SDK consumer, build-time budget Not run locally. The consumer gains geometry.cpp and links Ludus::FoundationGeometry; both run in CI
./scripts/build macos-clang-development
out/host-tools/venv/bin/ctest --test-dir out/build/macos-clang-development \
  -R 'geometry|math|foundational_includes' --output-on-failure
python3 tests/build_contract/verify_header_self_sufficiency.py out/build/macos-clang-development
out/host-tools/bin/clang-tidy -p out/build/macos-clang-development --quiet \
  modules/foundation/geometry/src/*.cpp modules/foundation/geometry/tests/*.cpp \
  modules/foundation/math/src/enclosure.cpp modules/foundation/math/tests/enclosure_tests.cpp
./scripts/check --format
python3 scripts/python/source_policy.py --base cb6f02a

The arc-length code acknowledges Jens Gravesen's Graphics Gems V chapter IV.7 (reviewed in the Gems review) and Lyle Ramshaw's blossoming report. The conversion code acknowledges Barry and Goldman, and Yuksel, Schaefer and Keyser. The enclosure code acknowledges Moore, Kearfott and Cloud's interval analysis text and Knuth's TwoSum, and its oracle acknowledges Dekker. No source code from those works is copied.

G0 contract freeze and reference corpus

Implementation date: 2026-10-15. Base: 5117c8c. Scope: ADR 0024, the test-only corpus in tests/fixtures/curves_surfaces/, a FoundationMath cross-check test and the boundary audit below. No Geometry module, prepared owner, cache, codec or editor binding is added.

Reference corpus

generate_fixtures.py uses only the Python standard library. Polynomial samples, derivatives, Catmull-Rom conversion, track semantics, shared edges and tessellation bounds use exact rationals. Real roots (zero-speed parameters and projection stationary points) are isolated with Sturm sequences and bisected to 2^-180. Arc lengths split at zero-speed roots and integrate the exact speed with 50-digit tanh-sinh quadrature; distance inversion uses safeguarded Newton with a residual below 1e-38. Each value is rounded to float64 once. Controls and track keys are exact float32 values.

test_fixtures.py (CTest ludus_geometry_reference_fixtures) checks the oracles without Ludus code:

  • Quadrature agrees with closed forms to 1e-35: lines, the ease line, the cusp 2(5 sqrt 5 - 8), the parabola 3(sqrt 5/2 + asinh(2)/4) and the reversing line's total variation at roots (5 -+ sqrt 5)/10.
  • Projection ties are certified by exact polynomial identities: mirror symmetry for the arch queries and |C(u)-Q|^2 - 1 = (x(u) - 1/2)^2 for the three-way tie on the reversing line. Global minima also hold on a 2049-point rational grid.
  • Catmull-Rom Bezier controls equal the Barry-Goldman pyramid exactly at 17 parameters; knots are 0, 1, 3, 6.
  • Saddle and bulge tessellation maxima equal 9/4 and 27/16, are attained, and never exceed the control-net plus twist bound on a 33 x 33 grid.
  • Track samples follow the frozen span-side and boundary-policy rules; the Hermite overshoot is 43/27 at 5/3 s.
  • The committed header matches the generator, ignoring only whitespace and formatter-inserted trailing commas.

curves_surfaces_fixture_tests.cpp in ludus_foundation_math_tests checks the G1 kernel against the header. Endpoints, corners and origin-relative controls must match bitwise (Exact). Interior samples, derivatives, Hermite controls and normals must fall inside an estimated envelope of one float32 ulp of the larger of the reference magnitude and the control scale (36x for derivatives). This is a regression bound for Rounded results, not a certified error. Dense chord polygons stay within the reference arc lengths; dense samples never beat a projection reference; the triangle-pair deviation never exceeds the tessellation bound; normal classifications match TryPatchNormal.

Numeric guarantees versus estimates

ADR 0024 assigns every planned result a class. G1 results are Exact at stored endpoints and corners and otherwise Rounded. No Certified helper exists, so no strict profile is available yet. The float32 position floor 2 * ulp32(E) follows from half-unit storage plus half-unit output rounding within the convex hull, at most sqrt(3) units per point.

Boundary audit

Boundary Finding Action
Math dependencies ludus_foundation_math links only Ludus::FoundationBase None
Kernel headers cubic.hpp and patch.hpp include only core.h, status.hpp and vector.hpp; neither is in core.h or the foundational PCH None
Kernel consumers Only Math tests, the allocation probe, the Wasm math probe and the SDK consumer include them; no G1 value is serialized None
Public API documentation 60 baseline entries for cubic and patch structs, members and overloads lacked Doxygen Documented each declaration and removed the entries from docs/api-undocumented.json
Parameter naming TrySplitCubic output references were named left and right Renamed to outLeft and outRight; source compatible
Cooked format owner The architecture placed the cooked format inside GeometryContent, which conflicts with the rule that formats shared by tools and runtime have their own header-only target ADR 0024 assigns it to a Base-only Ludus::GeometryFormat
Fixture header Test-only INTERFACE target linking Base and Containers; not installed or exported None

G0 validation record

Host: macOS 26 on arm64 with the pinned host tools (Clang/LLVM 18.1.8, CMake 3.29.6), preset macos-clang-development, warnings as errors.

Check Result
Reference corpus self-checks (ludus_geometry_reference_fixtures) Passed: 12 tests
G1 cross-check against the corpus ([fixtures]) Passed: 8 test cases, 87,452 assertions
Full FoundationMath unit tests Passed: 107 test cases, 420,733 assertions
FoundationMath allocation probe and foundational include gate Passed
Header self-sufficiency Passed: 153 public headers; run directly because the loaded host exceeded the 120 s CTest limit
Pinned clang-tidy on changed translation units Passed: cubic.cpp, curves_surfaces_fixture_tests.cpp
Formatting of changed C++ sources Passed: scripts/check --format --fix, then repository formatter rerun unchanged
Changed-line source policy Passed against 5117c8c
Docs index, strict MkDocs and wiki checks Passed
API reference with pinned Doxygen Passed: no new gaps; baseline only shrinks against origin/main
ASan/UBSan Built warning-clean; execution blocked by the documented local Clang 18 ASan startup deadlock on macOS 26, so the CI sanitizer lanes remain required
Wasm, SDK consumer Not rerun: the kernel's arithmetic and exported signatures are unchanged; parameter renames and comments do not affect the Wasm corpus or installed consumer
python3 tests/fixtures/curves_surfaces/generate_fixtures.py --check
./scripts/build macos-clang-development --target ludus_foundation_math_tests ludus_math_alloc_tests
out/host-tools/venv/bin/ctest --test-dir out/build/macos-clang-development \
  -R 'ludus_foundation_math_tests|ludus_math_alloc_tests|ludus_geometry_reference_fixtures|ludus_foundational_includes' \
  --output-on-failure
python3 tests/build_contract/verify_header_self_sufficiency.py out/build/macos-clang-development
python3 scripts/python/source_policy.py --base 5117c8c

The generator acknowledges H. Takahasi and M. Mori (tanh-sinh quadrature), P. J. Barry and R. N. Goldman (Catmull-Rom pyramid) and C. Yuksel, S. Schaefer and J. Keyser (centripetal parameterization) in its module comment. No source code from those works is copied.

G1 cubic and bicubic kernel

Implementation date: 2026-10-05. Base: 95683aa. Scope: cubic.hpp, patch.hpp in Ludus::FoundationMath; no additional module dependencies, prepared assets, mesh/collision/render adapters or editor binding.

The implementation uses fixed stack storage, de Casteljau interpolation, analytic derivative control nets and float64 intermediates. Patch samples reuse their V rows across the six outputs. All public operations are exception-free and noexcept. Float32 output narrowing checks range before committing results. This is the readable CPU reference implementation; SIMD/GPU/prepared polynomial alternatives and integrated performance budgets remain unmeasured.

Numerical contract and tests

Parameters are finite in [0,1]. Controls/results are float32; interpolation uses float64. Endpoints/corners preserve the stored values, including signed zero. All failed calls preserve every output. Full samples fail when a partial or derivative exceeds float32 range, while the position-only overload can still succeed. Split controls are rounded, so shape equivalence is numerical rather than symbolic. No interval enclosure, certified length, distance inversion or global projection guarantee is provided by G1.

bezier_tests.cpp uses independently written Bernstein basis polynomials and analytic basis derivatives, rather than the production de Casteljau/difference net helpers. It compares 100 deterministic cubic control sets and 25 patch nets at endpoints and interior parameters. The corpus also covers scalar powers, 2D/3D Hermite domain scaling, split parameter remapping/derivative scaling, collapsed endpoint children, matched split controls, saddle mixed derivatives, normal orientation, extreme finite/subnormal partials, nonfinite inputs, invalid domains, range failures, supported input/output aliasing and unchanged outputs. The existing allocation probe exercises conversion, sampling, normals and subdivision under a counting allocator. SDK and Wasm probes call exported APIs.

Validation record

Pinned native toolchain: Clang/LLVM 18, CMake 3.29.6, Ninja 1.11.1.3, Conan 2.8.1, C++23, precise floating point (-ffp-contract=off), exceptions disabled in engine targets, warnings as errors. Browser toolchain: Emscripten 4.0.23 from config/web_toolchain.json.

Check Result
New deterministic kernel unit corpus Passed: 8 test cases, 8,773 assertions
FoundationMath allocation probe Passed: 27 assertions, zero allocations/deallocations
Full Development build/tests and public-header gates Passed: 65 CTest entries, 2 live-display tests skipped
Debug math/allocation tests and public-header gates Passed: 4 CTest entries, including the final normal regressions
ASan/UBSan build/tests Passed: 57 CTest entries, 2 live-display tests skipped; changed kernel rechecked
Pinned format/tidy Passed: format; full tidy scan's 3 affected TUs corrected and passed targeted recheck
Installed SDK consumer Passed: headers installed, linked and executed
WebAssembly numeric corpus Passed: Development and Release Wasm/Node
PR CI and mergeability Tracked by live PR checks; required before ready for review

Validation commands (after native init.sh --locked --with-tests --preset-only and browser init.sh --with-web-probes --preset-only setup):

./scripts/build linux-clang-development
./scripts/test linux-clang-development
./scripts/build linux-clang-asan-ubsan
./scripts/test linux-clang-asan-ubsan
./scripts/check linux-clang-development --all
# Corrected the three reported TUs, then checked them with pinned clang-tidy:
out/host-tools/bin/clang-tidy -p out/build/linux-clang-development --quiet \
  --warnings-as-errors='*' modules/foundation/math/src/cubic.cpp \
  modules/foundation/math/src/patch.cpp modules/foundation/math/tests/bezier_tests.cpp
./scripts/check --format
./scripts/install-sdk linux-clang-development
./scripts/build linux-clang-debug --target ludus_foundation_math_tests ludus_math_alloc_tests
out/host-tools/venv/bin/ctest --test-dir out/build/linux-clang-debug \
  -R 'ludus_foundation_math_tests|ludus_math_alloc_tests|ludus_header_self_sufficiency|ludus_foundational_includes' --output-on-failure
./scripts/build web-emscripten-development --target ludus_web_math_probe
out/host-tools/venv/bin/ctest --test-dir out/build/web-emscripten-development \
  -R '^ludus_web_math_corpus$' --output-on-failure
./scripts/build web-emscripten-release --target ludus_web_math_probe
out/host-tools/venv/bin/ctest --test-dir out/build/web-emscripten-release \
  -R '^ludus_web_math_corpus$' --output-on-failure

After the helper-signature cleanup, rebuild/recheck the affected kernel tests in Development, Debug and ASan/UBSan, plus both Wasm corpus profiles. These results validate G1 only; the remaining architecture phases keep their own gates.

Attribution

Reference comments near subdivision, Hermite conversion and patch normals thank Philip J. Schneider, Thomas Lowe and Martin Brownlow and identify the work, book/chapter/pages, adopted idea and departures. The review links the consulted book pages and distinguishes actual chapter reading from index-only leads. No article source code was copied; length bounds, root isolation, transported frames and approximate shading fields are not implemented in this phase.