2026-10-18
Physics does not get a window
I did the lazy version for a while. Physics needed a debug draw, so physics took the renderer. The renderer wanted the simulation clock. A sample lived next to a library, so the library grew a window toolkit. After that, a headless test pulled in a UI stack to check a number, and nothing built unless everything built.
That is how a platform turns into a single solution you are afraid to open. One project reference "just for the gizmo," and six months later a ballistics test does not compile unless Avalonia does. I got tired of that. The split I keep now is boring on purpose.
The stack, from the bottom
Math is numbers. Transforms, geometry, topology, grids. If the BCL already has the type, I use the BCL. Vector3, Quaternion, Matrix4x4, Plane. I am not maintaining Vector3d so I can feel original. Math also has a hard rule that sounds fussy until you break it: no time. No deltaTime, no ticks, no "what happens next frame." A camera is not math. A camera is someone watching, and watching is a lifecycle. That belongs above.
Physics is forces over time. Bodies, mass, velocity, constraints, integrators, collision response, ballistics, orbits. It may take Math. It may not take a camera, a player, a maze, a network, or a drawing API. Stepping a rigid body with dt is the job. Deciding that the step is a game tick, or that the body should be on screen, is not.
Simulation runs worlds and clocks. Systems, snapshots, replay, tick order. Cameras live here. All of them. Orbit, tracking, first person, the motor that consumes look intent. Tiles and voxels live here too, including the mesh you can build on the CPU from a chunk. The mesh is geometry. A GPU is not required to know the faces exist. Simulation may take Math and Physics. It may not take Raylib, and it may not take a window.
Rendering draws. Scenes, materials, backends, a compiled scene, a frame. It sits on Math. It does not sit on Simulation, and Simulation does not sit on it.
Those two are peers. The error code, when someone couples them, is NOV2008. I mention the code because "peers" is a sentence people nod at and then violate with a single using. The code is the sentence that survives the nod.
Gaming may use both. Novolis.Game.* is the glue for authoring and shipping a game, and it is still not a host. It may take Rendering. It may not take Silk (NOV2017) and it may not take Raylib (NOV2009). A game library that opens its own window has already stopped being a library.
Above that are the hosts, and they are islands.
Avalonia is the studio UI. Only Novolis.Avalonia.* may reference Avalonia packages. Math, Physics, Simulation, Gaming, Economy, Astro, Rendering, Raylib, Agent, Cad, Audio, Markup do not get to "just show a window." Product apps may reference Avalonia themselves, because a product is allowed to be a host. A library is not.
MAUI is a separate island. Only Novolis.Maui.* may take Microsoft.Maui.*. It does not take Avalonia. Avalonia does not take MAUI. I have one grandfathered voice adapter on MAUI, and it is named as an exception, not as a tunnel. Blazor is a third island, with the same shape: components stay in Novolis.Blazor.*, and they do not reach across to the other two toolkits.
Silk can open a window. Only novolis-silk may reference Silk.NET. Rendering does not take Silk. Silk does not take the rendering libraries. There is no Presentation.Silk exception hiding in the middle. Raylib is the same kind of outsider. It is a graphics and input host. It is not on the spine, and nothing on the spine references it to "save a sample."
What is allowed to cross
The app is the only place that is allowed to know both sides. A game, a lab, a studio. That project references the host and the libraries, and it is the composition. Lower layers do not reach up to find out who is watching.
What crosses the peer boundary is dull data. A draw list. A color. A vector. Some texture coordinates. The names I actually use for that handshake are PlanarDrawList, Rgba32, Vector3, and UV floats. Math and the BCL. No scene type from Rendering leaking into Simulation so the two can "share" a shortcut. No simulation clock type leaking into a shader uniform "temporarily."
Shared shortcuts are how the lazy version comes back. The type looks innocent. It is a record with a position and a color. Then it grows a material id from one side and a body handle from the other, and now both projects have to move on the same day. I would rather duplicate a Vector3 at the boundary than marry the repositories.
Vulkan and shader compile live with Silk, in Novolis.Silk.Compute. The ray-tracing backend in Rendering keeps a CPU fallback so it does not need that window stack to exist. A headless test of a lighting integral should not download a GPU toolchain. I have lost afternoons to that, and the integral was not even wrong.
Where I used to hide the window
The sample is the usual culprit. Physics grows a debug draw because I cannot see the contact. Rendering grows a clock because a single frame is a boring demo. A Program.cs lands next to the library "so dotnet run works," and the library's package now depends on a toolkit the next consumer does not want.
Labs are where that host is allowed to be. Platformer and RTS samples in novolis-lab compose planar rendering, Silk, and simulation. That composition is the point of a lab. It is not a license for novolis-physics to reference Silk so the lab is easier to wire. The lab takes the packages. The packages do not take the lab.
There is a longer version of this rule in library boundaries. The one-line form is the one I want in my head: Math has no time. Physics has no camera. Simulation and Rendering cannot see each other. Hosts are islands. The product is the only mixer.
The analyzer is the rule
A written rule is something I skip at 1am. I have done it. The justification is always the same. "Just this once, the draw is two lines, I will move it in the morning." Morning is a new bug, and the two lines have friends.
Novolis.Analyzers.StackBoundaries fails the build. NOV2008 is the peer split. NOV2006 is Avalonia outside Novolis.Avalonia.*. NOV2007 is a lower layer taking a higher one. NOV2010 through NOV2013 are the MAUI and Blazor islands, including the ban on them taking each other. NOV2015 is Silk.NET outside novolis-silk. NOV2016 is Silk and Rendering seeing each other. verify-layer-boundaries walks the tree as well, because an analyzer only sees the compilation it is attached to, and I also want the repo graph checked when the package references are the crime.
I will cheat if the build lets me. So the build doesn't. The doc stays, because on Monday I still need to remember why the code exists. I do not trust myself to remember at 1am. I trust the red squiggle.
If you want the place this rule sits next to the others, it is the same pile as the checks that fail the build and where a window is allowed to live. Physics stays headless. That is not a slogan. It is which project is allowed to call Run.