Blender is one of the world’s most successful high-performance open-source 3D suites. Running a CppDepend analysis across its core modules reveals a surprisingly strong overall maintainability rating—a remarkable feat for a massive codebase encompassing over 16,000 types and 136,000 methods.
However, when we inspect the structural relationships between its core modules, the Dependency Structure Matrix (DSM) paints a much more complex picture, uncovering widespread dependency cycles:
When evaluating C++ software architecture with CppDepend, a Dependency Structure Matrix (DSM) provides undeniable evidence of dependency health. In a well-structured, layered system, dependencies flow in one direction—resulting in a clean triangular matrix with cells populated only on one side of the main diagonal.
However, inspecting Blender’s core engine matrix reveals a striking visual pattern around bf_blenkernel.
How does a project built by world-class engineers end up with so many architectural cycles?
The root cause isn’t developer carelessness. The culprit is C and C++’s legacy preprocessor model: the #include directive.
For decades, modern programming languages have used explicit module systems, namespaces, and explicit export controls to manage dependency graphs. C and C++, however, inherited a textual substitution model from the 1970s preprocessor: the #include directive.
The #include directive’s greatest strength is also its most dangerous flaw: extreme simplicity and flexibility. While textual substitution allows developers to rapidly pull dependencies anywhere in a codebase without rigid compile-time enforcement, this unrestricted freedom quickly becomes a trap as a project scales. Without strict architectural oversight, #include makes it effortless to introduce subtle dependency cycles that silently weave modules into a single, tightly coupled monolith. In large-scale C++ projects, these circular header linkages compound over time into massive technical debt—exponentially increasing build times, crippling unit testing capabilities, and making refactoring a minefield. Consequently, maintaining and evolving such a codebase ceases to be a routine engineering task; it demands constant, vigilant intervention from senior developers who must manually enforce structural boundaries where the language compiler fails to do so.
The Structural Flaws of Textual Inclusion
To understand why #include damages architecture, consider how it interacts with class definitions and modularity.
1. Inclusion Is Transitive and Leaky
When FileA.h includes FileB.h, and FileB.h includes FileC.h, FileA.h indirectly depends on FileC.h. The internal details of FileB leak into FileA. Over time, developers lose track of what a module actually depends on, creating a web of implicit, hidden dependencies.
2. Physical Structure Dictates Logical Architecture
In a clean design, interface boundaries dictate dependencies. With #include, physical file organization forces structural decisions. If Class A needs a single enum defined inside B.h, A must include B.h, bringing along every other type, pointer, and template header that B.h relies on.
3. Circular Dependencies Are Forced by Physical Need
Because C++ requires full type definitions to determine layout sizes, developers frequently put #include directives in header files where forward declarations (class X;) would have sufficed. The moment two headers need each other’s full definitions, the preprocessor creates a circular inclusion loop, leading to missing type errors, guard tricks, or fragile header ordering.
Deep Dive: The bf_blenkernel ↔ bf_bmesh Cycle
An example of how #include mechanics create structural dependency loops in Blender exists between the core kernel module (bf_blenkernel) and the mesh editing system (bf_bmesh).
In a clean, layered architecture, bf_bmesh (the higher-level, interactive editing system) should depend on bf_blenkernel (the lower-level data structures and math kernel). However, because #include directives make it easy to reach across boundaries without structural gatekeeping, bf_blenkernel directly references BMesh data structures.
The call graph for bf_bmesh demonstrates a remarkably clean, layered architecture—distinctly color-coded with dependencies (used modules) in blue and dependents (used-by modules) in green—with the notable exception of a bi-directional cycle with bf_blenkernel.
To isolate the exact types and methods consumed by the kernel from the mesh editing module, we can execute the following CQLinq query:
Let’s check for example the BMesh struct used by this function inside bf_blenkernel (armature.cc):
Why This Creates an Architectural Cycle
- Direct Concrete Type Usage: The kernel function calls BKE_editmesh_bmesh_get(...)to obtain a pointer toconst BMesh *bmand accessesbm->vdatadirectly.
- Forced Header Inclusion: To access the internal member bm->vdata, the kernel file MUST include"bmesh.h"(or"bmesh_class.h").
- The Inverse Dependency: Simultaneously, bf_bmeshheaders and source files include kernel headers (BKE_*.h) for basic data types (Object,ID,CustomData), memory management, and math helpers.
This forms a hard bi-directional dependency cycle. bf_blenkernel cannot be compiled, unit-tested, or reused independently of bf_bmesh.
How to Refactor and Break the Cycle
To break this cycle and enforce a clean unidirectional hierarchy (bf_bmesh → bf_blenkernel), the dependency on BMesh inside the kernel must be inverted or abstracted.
Solution 1: Opaque Abstraction / CustomData Offset Extraction
Notice that BKE_armature_deform_coords_with_editmesh only accesses bm->vdata to extract a single int offset: cd_dvert_offset. It does not perform actual mesh topology manipulations inside this wrapper.
Instead of passing or retrieving a BMesh* inside blenkernel, pass the cd_dvert_offset directly into the kernel function, or use a function pointer / callback abstraction:
The caller inside bf_bmesh (where including both bmesh.h and BKE_armature.h is architecturally valid) extracts the cd_dvert_offset and calls the kernel. bf_blenkernel no longer needs to know BMesh exists.
Solution 2: Dependency Inversion via Callback or Delegate
If bf_blenkernel requires dynamic access to BMesh data during complex operations, define an abstract interface or function delegate in blenkernel:
// In BKE_armature.h (bf_blenkernel)
using BMeshOffsetGetter = std::function<int(const Object &ob)>;
// BMesh logic is injected from higher layers
// without blenkernel knowing the concrete BMesh layoutHigh-Level Architectural Rigor: GRASP Patterns and High Cohesion
Despite the header-level dependency loops introduced by C++’s preprocessor mechanism, Blender’s underlying code design is exceptionally well-engineered. A deeper look at its class hierarchy and module organization shows strict adherence to general responsibility assignment software patterns (GRASP) and modern domain-driven design principles:
Polymorphism and Abstraction: Blender makes heavy use of abstract interface classes (bContext, space type definitions, and operator abstractions) to decouple high-level workflows from implementation details.
High Cohesion: Individual modules maintain sharp domain focus—bf_bmesh handles low-level mesh topology, bf_nodes manages execution graphs, and bf_gpu isolates hardware abstraction. Internal data structures within these modules demonstrate high functional cohesion. Indeed less than 3% of types are considered not cohesive:
Protected Variation: Core subsystems insulate themselves against underlying platform and hardware variations through well-defined internal APIs, keeping lower-level graphics and OS abstraction clean.
The presence of cyclic dependencies across modules is not a symptom of sloppy design, but rather an inevitable side effect of scaling a multi-million-line C/C++ codebase using textual #include directives. Without language-level module boundaries, even highly cohesive, interface-driven architectures eventually succumb to transitive header leakage.
Architectural Lessons for C++ Developers
The #include mechanism teaches us an enduring lesson: When the compiler doesn’t enforce architectural boundaries, entropy wins.
To mitigate the architectural damage of #include in your own projects:
- Prefer Forward Declarations: In header files, always prefer class MyClass;over#include "MyClass.h"unless inheriting or storing a direct value instance.
- Enforce Layering Rules: Use static analysis tools (like CppDepend with CQLinq) to set up quality gates that fail builds if lower-level modules include higher-level headers.
- Transition to C++20 Modules: Where possible, replace #includewithimport, which enforces explicit exports, avoids macro leakage, and eliminates textual inclusion cycles entirely.