Skip to content

01 — Layers & Contracts

Why layers

NPS is a multi-language monorepo with a single deep C++ engine behind a frozen C ABI. The six layers below are enforced by scripts/layer-fitness.sh (ctest nps_n1m_8_layer_fitness, registered as the layer-fitness lefthook pre-push hook). They exist to keep UI awareness out of the engine, prevent L0 modules from re-entrantly pulling in the session header, and keep the C ABI the only host border.

The six layers

flowchart TB
    L5["L5 apps<br/>src/SlicerApp, src/SimulatorApp<br/>Avalonia + MainWindow.axaml.cs"]
    L4["L4 Nps.Shell<br/>src/Nps.Shell<br/>UI-agnostic shell helpers"]
    L3["L3 Nps.EngineHost<br/>src/Nps.EngineHost<br/>sole C# P/Invoke surface + RAII"]
    L2["L2 C ABI<br/>src/Engine/abi/nps_c_api.h<br/>extern C + PODs only"]
    L1["L1 Session<br/>src/Engine/engine.h, engine.cpp<br/>nps::Engine"]
    L0["L0 modules<br/>src/Engine/internal/*<br/>zip, threemf, gcode, analysis, viewport, color"]

    L5 -->|ProjectReference| L4
    L5 -->|ProjectReference| L3
    L4 -->|ProjectReference| L3
    L3 -->|P/Invoke| L2
    L2 -->|delegate| L1
    L1 -->|include downward| L0

L5 apps (src/SlicerApp, src/SimulatorApp)

  • Each is a thin Avalonia host: App.axaml, MainWindow.axaml, Program.cs, MainWindow.axaml.cs.
  • ProjectReferences Nps.EngineHost (sole host border) and Nps.Shell (shared helpers). No direct native calls.
  • Per-app EngineBridge.cs was removed; both apps use Nps.EngineHost.NpsAbi directly.
  • Avalonia package pins + AvaloniaUseCompiledBindingsByDefault live in src/Directory.Build.props. No ReactiveUI dependency; bindings are Avalonia's compiled bindings.

L4 shell (src/Nps.Shell)

  • Shared by both apps. Contains UI-agnostic helpers that are not engine handle holders:
  • FixtureLocator.cs (92 LOC) — repo-root 3MF resolver.
  • ShellTrace.cs (64 LOC) — stderr sink installer.
  • StatusSink.cs (64 LOC) — stdout/stderr dual-writer.
  • NonClosingTextWriter.cs (97 LOC) — preserves the shared Console.Error stream on listener disposal.
  • ViewportController.cs (357 LOC) — owns SafeGcodeViewportHandle, camera state, sim cursor, overlay wiring.
  • Controls/ActivityBarHost, PanelHost, ShellKeybindings, FindingItem view model.
  • Does not parse, does not construct meshes. Consumes engine-prepared buffers via ViewportController and renders.

L3 EngineHost (src/Nps.EngineHost)

The sole, shared C# host-border library. Consumed by both apps and the C# interop tests.

  • NpsAbi.cs (532 LOC) — [DllImport("NPSEngine", CallingConvention = CallingConvention.Cdecl)] declarations that mirror nps_c_api.h exactly. This is the only file in the entire C# tree that declares [DllImport("NPSEngine")]. Every other C# source uses Nps.EngineHost.NpsAbi.* only.
  • SafeEngineHandle.cs (67 LOC) and SafeGcodeViewportHandle.cs (49 LOC) — SafeHandle RAII wrappers around the opaque void* handles returned by CreateEngine / CreateGcodeViewport.
  • EngineSession.cs (595 LOC) — session-shaped facade over NpsAbi. Owns a SafeEngineHandle, exposes load / gen / analyze / findings summary / detail / warp grid queries.
  • POD mirrors for ABI structs (NpsGcodeMove, NpsLoadOptions, NpsMaterialProfile, ...). Sizes and layouts are guarded by static_assert on the C++ side and explicit field layout on the C# side, plus Marshal.SizeOf roundtrips in tests/NPSEngine.Tests/EngineBridgeTests.cs (the AbiPODLayout_Frozen test).

L2 C ABI (src/Engine/abi/nps_c_api.h)

  • Stable surface between C++ and C#. The C# host border is generated by reading this surface; never the other way around.
  • Header length: 418 LOC. Exports 59 production symbols and 9 test-gated symbols (NPS_ENABLE_TEST_HOOKS).
  • Across this boundary: only PODs, opaque handles, const char* strings owned by the engine, raw float* / uint32_t* / uint8_t* buffers pointing into engine-owned memory. No STL. No exceptions.
  • Frozen POD sizes asserted at compile time on the C++ side (sizeof(NpsGcodeMove) == 36, etc.) and at runtime on the C# side (Marshal.SizeOf roundtrips in tests).
  • NPS_ABI_VERSION is bumped on any breaking change; consumers assert parity at startup via GetAbiVersion(). The change policy lives in docs/adr/0001-abi-change-policy.md.

L1 Session (src/Engine/engine.h + engine.cpp)

  • nps::Engine is the single stateful session class, instantiated via CreateEngine and destroyed via DestroyEngine.
  • Public surface: load entry points, mesh query API, demo toolpath generator, G-code parser, G-code analyzer entry, findings accessors, warp grid accessors. Every C ABI export delegates to a method on this class.
  • Owns canonical state: loaded mesh + palette + plate + unit, last parsed/generated G-code text, parsed/generated move array, analysis manager state, last error message, inferred slicer/material/printer.
  • Single-threaded. Bad calls return safe defaults (counts = 0, unit = "millimeter", geometry pointers nullptr).

L0 modules (src/Engine/internal/*)

  • Reachable only through engine.cpp and the Catch2 test harness.
  • Folder layout today:
  • engine_zip.h + engine_zip.cpp — ZIP/OPC container reader (ZipArchiveReader).
  • engine_threemf.hModelXMLParser, MeshResolver, matrix, component graph types.
  • engine_gcode.h — G-code parser test hooks + simplified grid scoring.
  • engine_gcode_store.h — value member owning last generated / parsed G-code + move array.
  • engine_viewport.hGcodeViewport class + render data model.
  • engine_analysis.hAnalysisManager, IAnalysisEngine derivatives (Thermal, Structural, Motion, Hole, Retraction, Flow).
  • engine_color.hColorParser (hex, filament palette, paint-code decoder).
  • engine_mesh_store.h — value member owning loader output + cached totals.
  • engine_value_types.h — small PODs (Vec3, Position, Color3, BedGeometry, CameraState, OverlayState).
  • Plus sibling headers in src/Engine/ root: engine_data.h, engine_analysis_result.h, engine_material.h (catalog + profiles).

Include DAG (compile-time layer direction)

The two edges that most easily rot are forbidden at compile time:

  • No L0 -> session. A leaf module including engine.h would pull the whole session surface into every module TU. Only engine.cpp and tests/ may include engine.h.
  • No Engine -> UI. src/Engine/ may not contain any Avalonia / XAML / .cs / .axaml / .csproj files. The engine is host-agnostic.
flowchart BT
    ABI["src/Engine/abi/nps_c_api.h<br/>L2 C ABI PODs + NpsResult<br/>depends on nothing above"]

    L0["src/Engine/internal/*.h<br/>L0 module headers / TUs<br/>may include: own header,<br/>sibling L0 value/data headers,<br/>and nps_c_api.h.<br/>MUST NOT include engine.h."]

    L1["src/Engine/engine.h + engine.cpp<br/>L1 nps::Engine session facade.<br/>Includes the L0 module headers.<br/>The ONLY engine surface<br/>(besides tests/) permitted<br/>to include itself."]

    DATA["src/Engine/engine_data.h,<br/>engine_analysis_result.h,<br/>engine_material.h<br/>value PODs reachable from both sides"]

    DATA --> ABI
    L1 --> L0
    L0 --> ABI
    L0 --> DATA

Both rules are asserted by scripts/layer-fitness.sh, registered as ctest nps_n1m_8_layer_fitness, with adversarial coverage by scripts/layer-fitness-tests.sh (ctest nps_n1m_8_layer_fitness_mutation_tests).

Forbidden edges

From To Status Why
L5 app code L0 module FORBIDDEN All engine work must cross L3 -> L2 -> L1.
L5 / L4 code native ABI (DllImport) FORBIDDEN outside Nps.EngineHost Sole host border.
L4 Shell/Viewport L0 module FORBIDDEN No direct calls; UI consumes engine results only.
L3 EngineHost L1 / L0 direct FORBIDDEN EngineHost may only call L2 (C ABI).
L2 C ABI STL types / exceptions FORBIDDEN Only PODs / opaque handles / C strings cross.
L1 Session L4 / L5 FORBIDDEN Engine has no UI awareness, no Avalonia deps.
L0 module another L0 via UI FORBIDDEN Cross-module calls happen inside L1, never via UI.
Any C# layer parsing / mesh construction FORBIDDEN Deep engine only.
L2 / L1 / L0 filesystem beyond the loaded file FORBIDDEN outside engine UI owns file dialogs.

See also

  • overview.md
  • engine-modules.md
  • docs/architecture.md (full contract layer)
  • docs/abi-inventory.md (C ABI inventory)
  • scripts/layer-fitness.sh (enforcement)