The workspace graph is a DAG on purpose. This document is the ruling
reference when a PR (human or agent-authored) is unsure which direction a
use or a Cargo.toml dependency is allowed to point.
This section is a reading aid, not the authoritative rule — the exact,
enforced graph is ALLOWED in scripts/check_dependency_rules.py
(./build.sh check-deps fails the build the moment a Cargo.toml edge
and that dict disagree, in either direction). The diagram below is kept
in sync with it, but if the two ever conflict, ALLOWED wins; fix the
diagram, not the other way around. The full, exact per-crate edge list
is in "All edges, exactly" below — read the
diagram for the shape of the graph, that section for the ground truth
of any one crate's dependencies.
meridian-foundation, meridian-memory-core, and meridian-task-core
are the graph's independent leaves — each depends on nothing (rule 0
covers foundation specifically: any other crate may take an edge to
it, so it isn't drawn as a dependency of anything below).
The real graph has too many crisscrossing edges (many crates each
depend directly on foundation, gac-core, resource-core, ... —
see "All edges, exactly") to draw as a single
arrow-accurate ASCII diagram without either lying by omission or
becoming unreadable clutter — the previous version of this section
tried the "one diagram, arrows glossed over with a disclaimer"
approach and it drifted from the real graph more than once. Instead,
here's every crate grouped by dependency depth (0 = a leaf; N = one
more than the deepest thing it depends on) — a crate in tier N can only
depend on crates in tiers 0..N, never a same-tier or higher-tier one
(that's what makes the graph a DAG):
tier 0 foundation, memory-core, task-core, dsl-macros
tier 1 numeric-core, platform-core, resource-core, dsl-core
tier 2 asset-core, audio-driver, gac-core, gpu-driver, physics-driver
tier 3 audio-core, compute-driver, ecs-core, graphics-driver
tier 4 compute-runtime
tier 5 gac-compute
tier 6 graphics-core, physics-core
tier 7 physics-compute
tier 8 engine-core
tier 9 sdk
meridian-dsl-core/meridian-dsl-macros are the two crates behind the
SDK's extensible scene DSL (see ADR 015
and meridian-sdk::dsl's own module doc): dsl-core is a domain-blind
tag/attribute-markup parser plus the generic DslTag/DslRegistry
machinery (tier 1: it depends only on foundation, for its
EngineError impls); dsl-macros is the #[dsl_tag(name = "...")]
proc-macro that implements DslTag for a plain struct (tier 0: a
proc-macro crate, no meridian-* runtime dependency at all — it depends
on syn/quote/proc-macro-crate externally, not shown here since
this diagram only tracks internal meridian-* edges). Neither knows
about Entity/Mesh/RigidBody or any other specific tag — those are
defined in meridian-sdk::dsl (built-ins) or in a game's own crate
(custom tags), both just ordinary #[dsl_tag]-annotated structs.
This shows depth, not which crate depends on which — two crates in
the same tier aren't necessarily related at all (audio-core and
ecs-core share tier 3 but neither depends on the other). For the
actual edges, use "All edges, exactly"; don't
infer them from tier adjacency.
meridian-graphics-driver and meridian-compute-driver both also depend
on meridian-gpu-driver (omitted from the diagram above for
readability, the same way the gac-compute edges below are) — the crate
that owns the actual wgpu device/buffer/shader/compute-pipeline
mechanics shared between rendering and general GPU compute, so neither
driver reimplements it independently or reaches into the other's crate
to get at it:
meridian-platform-core
|
meridian-gpu-driver
|
+----------+----------+
| |
graphics-driver compute-driver
See ADR 011 for the full decision.
meridian-gac-core and meridian-compute-runtime never depend on each
other — geometric algebra ("what to compute") and the compute dispatch
runtime ("where to compute it") are deliberately independent. The adapter
between them is its own crate:
meridian-gac-core meridian-compute-runtime
| |
+----------+----------+
|
meridian-gac-compute
|
+-----------+-----------+
| |
graphics-core physics-core
meridian-gac-compute depends on both and implements compute-runtime's
ComputeKernel trait for GAC batch operations (MotorTransformKernel,
MotorComposeKernel, ...). graphics-core and physics-core depend on
gac-compute for batched transform work and on compute-runtime directly
for non-GAC compute (e.g. GPU culling) — both edges omitted from the main
diagram above for readability, along with meridian-audio-core's
dependency on meridian-gac-core/meridian-audio-driver/
meridian-resource-core and meridian-graphics-core's/
meridian-physics-core's own direct dependency on meridian-resource-core
(handle types their MeshRegistry/ColliderMeshHandle etc. use) — see
"All edges, exactly" for the complete, non-abridged
list.
meridian-gac-compute also depends directly on meridian-gpu-driver —
not meridian-compute-driver (still forbidden by rule 5: a kernel
dispatches through compute-runtime::ComputeContext, never around it).
compute-runtime::ComputeContext::gpu() returns a
compute-driver::GpuComputeDevice, whose buffer/shader/pipeline methods
are typed in terms of gpu-driver's own resource types
(Buffer/BufferUsage/Shader/ComputePipeline) — a real GPU-dispatching
ComputeKernel has to name those types (a bind-group's usage flags, a
kernel's own pipeline field, ...), which needs gpu-driver in scope. This
doesn't reach around compute-runtime's dispatch mechanism (rule 5's
actual concern); it's the same category of edge graphics-driver/
compute-driver already have to gpu-driver for the same
resource-type-naming reason — see
ADR 011. See
ADR 007.
meridian-physics-compute is rule 11's first concrete
meridian-<domain>-compute adapter: physics-core's deterministic
soft-body GPU kernels (mass-spring integration), mirroring
gac-compute's shape one layer up:
meridian-physics-core meridian-compute-runtime
| |
+-----------+------------+
|
meridian-physics-compute
Same reasoning as gac-compute throughout: physics-compute depends
directly on meridian-gpu-driver for the same resource-type-naming
reason, and also on meridian-gac-core (the GaFlavor/Plane/Vec3
types its kernels' signatures are expressed in) and meridian-gac-compute
(its Fixed-flavor kernel reuses fixed_wgsl::FIXED_ARITHMETIC_LIB_WGSL
rather than re-deriving the same Q16.16 emulation — see
meridian-physics-compute's own module doc). It also takes rule 0's
open meridian-foundation edge: narrow_phase::GenerateContactsKernel
logs via log_warn! when a pair's manifold exceeds its fixed
MAX_CONTACTS_PER_PAIR output slot, the same MAX_LIGHTS-style
truncation policy graphics-core::submission uses.
meridian-engine-core depends on meridian-physics-compute too (moving
engine-core from tier 7 to tier 8, since it now sits above
physics-compute rather than beside it) — Runtime's
PhysicsComputeStepStage (see that crate's own module doc) dispatches
physics-compute's batched RigidBodyIntegratorKernel/
BroadPhasePairsKernel/GenerateContactsKernel/
ConstraintSolverBatchKernel instead of PhysicsSubsystem::step's
plain sequential loop — the concrete answer to "physics needs to scale
past a handful of bodies" docs/roadmap.md's "Scaling beyond a tech
demo" section names. Not a new coordination-logic violation of rule 7:
engine-core is still the only crate reaching into physics-compute
for this, and PhysicsComputeStepStage is a thin Stage wrapper
dispatching real batched kernels, not a reimplementation (CLAUDE.md's
"don't drag another crate's logic into your own" rule).
The ground truth: every crate's real [dependencies], mirroring
ALLOWED in scripts/check_dependency_rules.py (./build.sh check-deps fails if a real edge and this list disagree — regenerate
both together when either changes, in the same PR). Ordered roughly
bottom-up.
meridian-foundation-> (nothing)meridian-memory-core-> (nothing)meridian-task-core-> (nothing)meridian-numeric-core->meridian-foundationmeridian-platform-core->meridian-foundationmeridian-gac-core->meridian-numeric-coremeridian-resource-core->meridian-memory-coremeridian-ecs-core->meridian-gac-core,meridian-memory-coremeridian-gpu-driver->meridian-foundation,meridian-platform-coremeridian-graphics-driver->meridian-foundation,meridian-gpu-driver,meridian-platform-coremeridian-compute-driver->meridian-gpu-driver,meridian-platform-coremeridian-audio-driver->meridian-foundation,meridian-platform-coremeridian-physics-driver->meridian-platform-coremeridian-compute-runtime->meridian-compute-driver,meridian-memory-core,meridian-task-coremeridian-gac-compute->meridian-compute-runtime,meridian-gac-core,meridian-gpu-driver,meridian-numeric-coremeridian-asset-core->meridian-foundation,meridian-platform-coremeridian-graphics-core->meridian-asset-core,meridian-compute-runtime,meridian-ecs-core,meridian-foundation,meridian-gac-compute,meridian-gac-core,meridian-graphics-driver,meridian-memory-core,meridian-resource-coremeridian-audio-core->meridian-audio-driver,meridian-gac-core,meridian-resource-coremeridian-physics-core->meridian-compute-runtime,meridian-ecs-core,meridian-gac-compute,meridian-gac-core,meridian-numeric-core,meridian-physics-driver,meridian-resource-coremeridian-physics-compute->meridian-compute-runtime,meridian-foundation,meridian-gac-compute,meridian-gac-core,meridian-gpu-driver,meridian-numeric-core,meridian-physics-coremeridian-engine-core->meridian-asset-core,meridian-audio-core,meridian-compute-runtime,meridian-ecs-core,meridian-foundation,meridian-gac-core,meridian-graphics-core,meridian-memory-core,meridian-physics-compute,meridian-physics-core,meridian-platform-core,meridian-task-coremeridian-dsl-core->meridian-foundationmeridian-dsl-macros-> (nothing; externalsyn/quote/proc-macro-crateonly)meridian-sdk->meridian-asset-core,meridian-audio-core,meridian-dsl-core,meridian-dsl-macros,meridian-engine-core,meridian-foundation,meridian-gac-core,meridian-graphics-core,meridian-graphics-driver,meridian-physics-core,meridian-platform-core,meridian-task-core
meridian-engine-core lists meridian-graphics-core as an allowed edge
(rule 7) but doesn't use it yet — Runtime::tick doesn't render a
frame; see that crate's own module doc for why, and docs/roadmap.md's
graphics-core/Runtime::tick entry for the current state.
meridian-sdk is a second crate that depends on (nearly) every
*-core, alongside engine-core — this is not a violation of rule 7,
it's a deliberately different kind of edge. Rule 7 is about where
cross-*-core coordination logic lives (one place, so it doesn't
accrete as ad hoc cross-dependencies between domain crates); sdk adds
no coordination logic of its own — its pipeline::PhysicsStepStage,
for instance, is a thin wrapper calling straight into
engine_core::PhysicsSubsystem::step, not a reimplementation (CLAUDE.md's
"don't drag another crate's logic into your own" rule). sdk's reason
for touching every *-core is different and additive: it's the single
application-facing re-export surface (applications depend on sdk
alone, never on the individual crates it composes — see that crate's
own module doc) and the one place allowed to combine engine-core's
driver-independent orchestration with the *-driver crates
engine-core itself can never depend on (graphics-driver, here, for
windowed rendering). Both roles need the same broad reach engine-core
has, for reasons rule 7 doesn't cover — this is a second, narrower
"is allowed to know about a lot" exception, not a second unrestricted
hub. meridian-ecs-core/meridian-compute-runtime/meridian-memory-core
are conspicuously absent from sdk's edge list — it doesn't reach for
those because nothing it re-exports needs them (yet); this is not a
blanket "sdk may depend on anything."
-
meridian-foundationis the open bottom of the graph. It depends on nothing (its optionalfile-loggingfeature is the one external exception, pullingtokiofor the buffered async log-file sink), and any crate may take an edge to it — that edge is added toscripts/check_dependency_rules.pywhen actually taken, not pre-declared. It exists for exactly two kinds of content: shared conventions (EngineError,FeatureFlags) and process-wide diagnostics (the unifiedloggingsink andcrash_reportingpanic hook). Diagnostics are deliberately not a "global manager" in rule 3's sense: the sink owns no engine objects and hands out no handles — it appends lines, the same category asstd's own panic hook. -
A
*-corecrate never depends on the*-drivercrate of a different subsystem.graphics-coremay depend ongraphics-driver, never onaudio-driverorphysics-driver. -
A
*-drivercrate never depends on its own*-core. Drivers are hardware/OS abstractions; they know nothing about scenes, materials, rigid bodies, or any other high-level concept their*-corebuilds on top of them.graphics-driver → graphics-coreis forbidden. -
meridian-ecs-coredepends on nothing abovemeridian-gac-coreandmeridian-memory-core. It must never depend onengine-core,graphics-core,physics-core,audio-core, orasset-core. ECS is a storage mechanism (Entity,Component,Archetype,Query,Storage); it has no opinion about gameplay/domain component meaning (materials, rigid bodies, DSP graphs, ...) — that's what thegraphics-core/physics-core/audio-coreboundary above exists to enforce. The one exception isTransform, an engine-primitive spatial type built directly ongac-core'sMotor3and shared by every subsystem (see gac-design.md); ECS knows about it the same way it knows aboutEntityitself, not as a domain concept it interprets. -
meridian-asset-coreonly loads and decodes. It must never define aAssetManager,ResourceManager, orCacheManagertype, and must never depend onecs-core,graphics-core, orengine-core. Its job ends at "file bytes → decoder → CPU-side representation"; deciding where that representation lives and when it dies is the application's problem, not this crate's. Concretely: a loader function returnsCpuMeshData(or equivalent), never aMeshHandle/ResourceId— minting a handle isresource-core's job (rule 8), and an asset-core function that returns one would mean asset-core deciding runtime identity/lifetime, exactly what this rule exists to keep out of it. -
meridian-compute-runtimeis the only path to CPU-SIMD/GPU-compute for subsystem crates.physics-coreandgraphics-corereach compute throughcompute-runtime(directly, or via an adapter crate likegac-compute— see rule 10), never by depending oncompute-driverdirectly, and never by re-implementing scheduling/dispatch themselves. Any futureanimation-core,particles-core, orai-corefollows the same rule. -
meridian-gac-corestays pure geometric algebra. Scalar types, SIMD dispatch, and CPU feature detection live inmeridian-numeric-core(which itself sits onmeridian-foundation), not ingac-core. If you find yourself adding a#[cfg(target_feature = ...)]block togac-core, it belongs innumeric-coreinstead. -
meridian-engine-coreis the only crate allowed to depend on every*-core. No other crate is the "hub" — if two*-corecrates need to talk to each other outside the edges drawn above, that coordination belongs inengine-core, not in a new cross-dependency between them. -
meridian-resource-coredefines resource identity, not lifetime policy.Handle,ResourceId, versioning, and dependency-tracking types live here; deciding when a resource is loaded, evicted, or reloaded does not. It must never define a manager type (same rule as asset-core, rule 4) and must depend on nothing butmemory-core. See ADR 006. -
meridian-physics-driverowns no collision algorithms. BVH construction, spatial hashing, and broad-phase structures are domain logic and belong inphysics-core, even though they sound "low-level."physics-driveris execution only: memory backend, SIMD/GPU dispatch, synchronization — the same rolecompute-driverplays for compute in general. See physics-design.md. -
meridian-gac-coreandmeridian-compute-runtimenever depend on each other, in either direction.gac-corestays pure geometric algebra (rule 6) and must never know a GPU exists;compute-runtimestays a generic dispatch runtime (rule 5) and must never know what aMotor3is. Batch GAC operations that need both —MotorTransformKerneland friends — live in the adapter cratemeridian-gac-compute, which depends on both and is the only crate allowed to. See ADR 007. -
No domain-specific GPU/SIMD algorithm may live in
meridian-compute-runtime. It owns dispatch mechanics only (ComputeContext,ComputeKernel, buffers, and dispatch ordering within a compute submission — not general engine task scheduling, which staystask-core's job; see threading-model.md) and must never grow a kernel that encodes what aMotor3, a particle, or a rigid body is. Every domain that needs batched compute gets its ownmeridian-<domain>-computeadapter crate —gac-computeandphysics-computetoday, a futureparticle-compute/ai-computeas they're needed — depending on that domain's*-corepluscompute-runtime, per rule 10. This is what keepscompute-runtimesmall and stable as new domains adopt it instead of each one adding its own edge into its internals. The same boundary applies to memory, not just algorithms:compute-runtimeowns untyped buffers (Buffer, byte length, dispatch/sync state); it must never define a domain-shaped buffer type (MotorBuffer,ParticleBuffer, ...). Interpreting bytes as aMotor3or a particle is the adapter crate's job — same split as rule 4 (asset-coredecodes, it doesn't manage) and rule 8 (resource-coreis identity, not policy). -
meridian-gpu-driverownswgpudevice/buffer/shader/compute-pipeline mechanics only — not rendering policy, not compute-dispatch scheduling. It must never know what a render pass, a swapchain, aComputeKernel, or a scheduling threshold is;graphics-driverandcompute-driver(the only two crates allowed to depend on it) each add their own domain-specific layer on top. See ADR 011.
./build.sh check-deps
# or directly:
python3 scripts/check_dependency_rules.py
# to eyeball the raw graph:
cargo tree --workspace --edges normalcheck-deps is authoritative and exact — it fails on any edge missing
from ALLOWED/"All edges, exactly" and on any
edge listed there that a Cargo.toml no longer actually has, so the two
can never silently drift apart. If cargo tree shows an edge that
check-deps doesn't complain about, it's already documented; if
check-deps fails, either the edge is a real violation (fix the
Cargo.toml) or a legitimate new edge (add it to ALLOWED and to this
document, in the same change).
See also: architecture.md, ADR 005.