Novolis Docs
novolis-governance / inertial-frame-stack-spec.md

Novolis Inertial Frame Stack — adapted specification

dotnetgovernancenovolis

Maps the Inertial Frame Stack (IFS) to current Novolis repos, NuGet packages, and types. Use this when introducing novolis-spatial and the first IFS implementation.

Status: specification (no novolis-spatial repo yet). Related: simulation-layer-policy.md, naming.md.


Purpose

Inertial Frame Stack is the spatial locality layer.

It defines where a simulation frame is evaluated from, and provides coordinate projection to express relevant world-space state in a local, origin-stable frame.

It does not decide what participates, simulate physics, render, or replicate network state.

IFS           = where
PreCulling    = what        (future: Novolis.Spatial.PreCulling)
FrameScene    = prepared work (future: Novolis.Spatial.FrameScene)
Adapters      = execution   (render / physics / net hosts)

Placement in the Novolis stack

flowchart TB
  subgraph math [novolis-math]
    Geo[Novolis.Math.Geometry]
    Arr[Novolis.Math.Arrays]
  end

  subgraph physics [novolis-physics]
    Num[Novolis.Physics.Numerics]
    Abs[Novolis.Physics.Abstractions]
  end

  subgraph sim [novolis-simulation]
    SimAbs[Novolis.Simulation.Abstractions NEW]
    View[Novolis.Simulation.View]
    World[Novolis.Simulation.World]
  end

  subgraph spatial [novolis-spatial NEW]
    SpAbs[Novolis.Spatial]
    IFS[Novolis.InertialFrames]
    Pre[PreCulling future]
  end

  Num --> SpAbs
  SimAbs --> IFS
  SpAbs --> IFS
  IFS --> Pre
  View -.->|ViewPose to anchor only| IFS
  Geo -.->|float matrices at adapter edge| Adapters
LayerOwns IFS concern?
MathFloat meshes, Camera, Transform, view/projection matrices — not world snapshots or frames
Physics.NumericsVector3d, Quaterniond, AxisAlignedBox3d — frame-space numerics
Physics (domains)Integration, forces, collision — must not build inertial frames
SimulationWorlds, kinematics, view controllers — may consume FrameContext; does not own projection
Spatial (new)World-space types, IFS, pre-cull pipeline
Raylib / appsDraw and product rules — consume prepared local work via adapters

Novolis.Simulation.View.ViewPose is an observer convenience (float Vector3 + yaw/pitch). It is not FrameContext. Hosts may convert ViewPose → FrameAnchor.Pose at a double-precision boundary when building a frame.


Repositories and packages

Target repo

novolis-spatial

Per naming.md: domain repo novolis-spatial, packages Novolis.Spatial.*.

Packages (v1)

PackageResponsibility
Novolis.SpatialWorld-space primitives: WorldPosition, WorldVector, WorldPose, WorldRegion, spatial query surface
Novolis.Simulation.AbstractionsTick + entity identity + read-only world snapshot contract (thin; lives in novolis-simulation)
Novolis.InertialFramesIFS: InertialFrame, FrameContext, projection, bands, builders

Future (same repo, not v1):

PackageResponsibility
Novolis.Spatial.PreCullingWorldSnapshot + FrameContext → PreCullResult
Novolis.Spatial.FrameScenePrepared local packets for adapters

Must not depend on

Novolis.Raylib.*
Novolis.Physics.Collision.*
Novolis.Physics.Motion / Gravity / Ballistics / …
Novolis.Messaging.*
Rendering hosts, Stride, Vulkan
Game-specific assemblies (SCR product code, DoomLite3D)

Novolis.InertialFrames may reference:

Novolis.Physics.Numerics     (double 3D algebra only)
Novolis.Spatial
Novolis.Simulation.Abstractions

Do not reference the aggregate Novolis.Physics metapackage or simulation world/kinematics packages (avoids cycles and hidden physics coupling).


Type mapping (spec → Novolis)

Original specNovolis today / v1 choice
Novolis.Mathematics`Novolis.Physics.Numerics` — Vector3d, Quaterniond already shipped
Bounds3d`AxisAlignedBox3d` (Novolis.Physics.Numerics) for axis-aligned interest regions
Matrix4x4dNot in platform v1. IFrameProjector uses translation + inverse rotation (same pattern as RigidBodyState / Quaterniond.Rotate in physics). Float System.Numerics.Matrix4x4 stays at adapter boundary via Novolis.Math.Geometry.Camera when rendering.
Distance`Vector3d.LengthSquared()` / Length() — no separate distance type
Novolis.Spatial.Abstractions`Novolis.Spatial` (single abstractions package; split later only if needed)
Novolis.Simulation.AbstractionsNew packable project under novolis-simulation
WorldSnapshot, EntityId, …New in Novolis.Simulation.Abstractions
Novolis.InertialFramesNew under novolis-spatial

Precision split (aligned with existing code)

SpaceTypesHome
Authoritative worldWorldPosition, WorldPose, …Novolis.Spatial (double-backed; large-world safe)
Frame-local cacheFramePosition, FramePose, …Novolis.InertialFrames (uses Vector3d / Quaterniond)
GPU / debug floatVector3, Quaternion, CameraNovolis.Math.Geometry (adapters only)

World-space is truth. Frame-space is cache.


Dependencies (concrete)

Novolis.InertialFrames
  → Novolis.Physics.Numerics
  → Novolis.Spatial
  → Novolis.Simulation.Abstractions

Novolis.Spatial
  → Novolis.Physics.Numerics

Novolis.Simulation.Abstractions
  → (none required for v1; optional reference to Novolis.Spatial for WorldPosition on EntityView)

If EntityView carries positions, prefer Novolis.Simulation.Abstractions → Novolis.Spatial rather than duplicating coordinates in simulation.


Core responsibility

Given:

IWorldSnapshot          (Novolis.Simulation.Abstractions)
FrameAnchor             (entity / pose / position)
FrameBands + interest radius

Produce:

FrameContext            (Novolis.InertialFrames)

FrameContext supplies origin, orientation, optional velocity baseline, interest region, distance bands, IFrameProjector, and band classification helpers.


Core types

Namespaces shown as intended layout.

Identifiers and frame state (Novolis.InertialFrames)

namespace Novolis.InertialFrames;

public readonly record struct InertialFrameId(Guid Value);

public readonly record struct InertialFrame(
    InertialFrameId Id,
    WorldPose Origin,
    WorldVector Velocity);

World-space (Novolis.Spatial)

namespace Novolis.Spatial;

/// <summary>Authoritative world-space point. v1: wraps <see cref="Novolis.Physics.Numerics.Vector3d"/>; later may add sector/origin metadata.</summary>
public readonly record struct WorldPosition(Vector3d Value);

public readonly record struct WorldVector(Vector3d Value);

public readonly record struct WorldPose(WorldPosition Position, Quaterniond Rotation);

/// <summary>Interest/query region in world space. v1: axis-aligned box.</summary>
public readonly record struct WorldRegion(AxisAlignedBox3d Bounds)
{
    public bool Contains(WorldPosition position) => Bounds.Contains(position.Value);
}

ISpatialQuery (optional v1): minimal read surface used by builders — e.g. try get pose/position by EntityId. Implementations live in simulation or product hosts, not in IFS.

Frame-space (Novolis.InertialFrames)

Uses numerics directly (no duplicate vector types):

public readonly record struct FramePosition(Vector3d Value);

public readonly record struct FrameVector(Vector3d Value);

public readonly record struct FramePose(FramePosition Position, Quaterniond Rotation);

Bands

public readonly record struct FrameBand(
    string Name,
    double EnterDistance,
    double ExitDistance);

public sealed record FrameBands(
    FrameBand Interactive,
    FrameBand Tactical,
    FrameBand Strategic,
    FrameBand Background)
{
    public static FrameBands Default { get; } = /* governed defaults */;
}

Simulation contracts (Novolis.Simulation.Abstractions)

namespace Novolis.Simulation.Abstractions;

public readonly record struct SimulationTick(ulong Value);

public readonly record struct EntityId(Guid Value);

public readonly record struct EntityView(
    EntityId Id,
    WorldPosition Position,
    WorldPose? Pose = null);

public interface IWorldSnapshot
{
    SimulationTick Tick { get; }
    bool TryGetEntity(EntityId id, out EntityView view);
}

Sealed WorldSnapshot record implementations belong in simulation or app layers, not in IFS.

FrameContext

public sealed record FrameContext(
    InertialFrame Frame,
    WorldRegion InterestRegion,
    FrameBands Bands,
    SimulationTick Tick,
    IFrameProjector Projector)
{
    public double InterestRadiusSquared { get; init; }

    public FrameBandKind Classify(WorldPosition position)
        => FrameBandClassifier.Default.Classify(this, position);
}

Projection API (v1)

No Matrix4x4d. Projector holds origin pose and cached inverse orientation.

public interface IFrameProjector
{
    FramePosition Project(WorldPosition position);
    WorldPosition Unproject(FramePosition position);

    FrameVector Project(WorldVector vector);
    WorldVector Unproject(FrameVector vector);

    FramePose Project(WorldPose pose);
    WorldPose Unproject(WorldPose pose);

    double DistanceSquaredTo(WorldPosition position);
}

Implementation sketch: Project(position) => origin.Rotation⁻¹ * (position - origin.Position) using Quaterniond from Novolis.Physics.Numerics (same inverse-rotate approach as SemiImplicitEulerRigidBodyIntegrator).

v2 optional: ToSinglePrecisionTransform() → Novolis.Math.Geometry.Transform for render adapters only.


Frame building API

public interface IFrameContextBuilder
{
    FrameContext Build(IWorldSnapshot snapshot, FrameRequest request);
}

public sealed record FrameRequest(
    FrameAnchor Anchor,
    FrameBands Bands,
    double InterestRadius);

public abstract record FrameAnchor
{
    public sealed record Entity(EntityId EntityId) : FrameAnchor;
    public sealed record Pose(WorldPose Pose) : FrameAnchor;
    public sealed record Position(WorldPosition Position) : FrameAnchor;
}

Convenience:

public static class FrameRequests
{
    public static FrameRequest CenteredOn(EntityId entityId, double interestRadius = 100_000)
        => new(
            Anchor: new FrameAnchor.Entity(entityId),
            Bands: FrameBands.Default,
            InterestRadius: interestRadius);
}

Simple usage

var frame = frameBuilder.Build(snapshot, FrameRequests.CenteredOn(playerShipId));
var local = frame.Projector.Project(enemy.Position);
var band = frame.Classify(enemy.Position);

Band classification

IFS classifies distance, not participation.

public enum FrameBandKind
{
    Interactive,
    Tactical,
    Strategic,
    Background,
    Outside
}

public interface IFrameBandClassifier
{
    FrameBandKind Classify(FrameContext frame, WorldPosition position);
}

Hysteresis: use EnterDistance / ExitDistance per FrameBand (e.g. enter tactical 10 km, exit 12 km). Classifier reads prior band from host state if needed; classifier itself stays pure given that state input.


Relationship to PreCulling (future)

IWorldSnapshot
  → IFrameContextBuilder.Build
      FrameContext
  → IPreCullPipeline.Run
      PreCullResult
  → FrameScene
  → Adapters (Raylib, physics islands, replication)

Novolis.Spatial.PreCulling depends on Novolis.InertialFrames + Novolis.Simulation.Abstractions. It must not reference Raylib.


What IFS may / must not do

Unchanged from the conceptual spec:

May: resolve origin/orientation, interest region, bands, project/unproject, distance classification.

Must not: select renderables, physics bodies, replication sets; integrate motion; collision; draw calls; renderer/physics APIs; client/server roles.


Determinism

Same IWorldSnapshot contents
Same FrameRequest
Same FrameBands configuration
Same tick
= same FrameContext

No wall-clock time, RNG, renderer feedback, physics engine internals, network latency, or mutable statics in the build path.


Performance

  • Prefer readonly struct / records and IFrameProjector instances that cache inverse orientation.
  • Squared distance comparisons in band classifier.
  • No LINQ in hot paths; allocation-free steady state for Build + Project.
  • AxisAlignedBox3d for interest region tests.

Debug invariants (v1)

Debug.Assert(frame.Projector.Project(frame.Frame.Origin.Position).Value.LengthSquared() < 1e-12);
Debug.Assert(frame.InterestRegion.Contains(frame.Frame.Origin.Position));
// Monotonic enter distances: Interactive < Tactical < Strategic < Background

Minimal v1 surface (ship first)

DeliverablePackage
WorldPosition, WorldVector, WorldPose, WorldRegionNovolis.Spatial
SimulationTick, EntityId, EntityView, IWorldSnapshotNovolis.Simulation.Abstractions
IFrameContextBuilder, IFrameProjector, FrameContext, FrameRequest, FrameBands, default classifierNovolis.InertialFrames
TUnit tests: round-trip project/unproject, origin at zero, band hysteresis, determinismNovolis.InertialFrames.Tests

Defer: FrameVector project/unproject if unused, ISpatialQuery, Novolis.Spatial.PreCulling, GPU matrix helpers.


Integration notes for current repos

Existing APIRelationship to IFS
Novolis.Simulation.View.ViewPoseObserver input; convert to WorldPose / FrameAnchor.Pose when building a frame
Novolis.Math.Geometry.CameraDownstream of adapters; built from projected or float-converted poses
Novolis.Physics.Abstractions.RigidBodyStatePhysics island input; positions are world-space — project with IFrameProjector inside adapter, not inside integrators
Novolis.Simulation.World.PlanarOccupancyGrid queries stay 2D float; orthogonal concern unless extended to 3D world

Wave checklist (suggested)

  1. Add Novolis.Simulation.Abstractions to novolis-simulation (packable, no raylib).
  2. Create novolis-spatial from template; add Novolis.Spatial + Novolis.InertialFrames + TUnit tests.
  3. Local NuGet pack; dogfood from one host with a fake IWorldSnapshot.
  4. Add governance cross-link from simulation-layer-policy.md (one paragraph: spatial layer between simulation truth and adapters).
  5. Implement Novolis.Spatial.PreCulling when a second consumer needs shared “what” selection.

Original → adapted quick reference

OriginalAdapted
Novolis.MathematicsNovolis.Physics.Numerics
Bounds3dAxisAlignedBox3d
Matrix4x4dDropped for v1; quaternion + translation projector
Novolis.Spatial.AbstractionsNovolis.Spatial
WorldSnapshot (concrete)IWorldSnapshot + host implementations
EntityViewNovolis.Simulation.Abstractions
Package only Novolis.InertialFramesRepo novolis-spatial; abstractions split as above