Blender’s architecture
Hello guys,
After some ‘heated’ discussions on the other thread, the one about the weld modifier, I took the liberty to come up with some improvement ideas concerning the upcoming everything nodes features.
First, just a bit about myself: I am a SW engineer myself with over 20+ years of experience including knowledge of C++/Obj-C/Java/JavaScript, now Python and may other things I don’t even remember. Also, I have some experience with modo and houdini. I don’t have an website, but if you curious, you can check some stuff I did in houdini here: Skeleton curve implementation based on VDB medial axis | Forums | SideFX
Also, this doc is meant exclusively to raise awareness, not a demand or intention to be inconsiderate to the dev team, like someone suggested in the other thread.
I am not sure if any of these subjects have been touched by the docs or discussed before, but I am aware of the road-map concerning everything nodes: Source/Nodes/EverythingNodes - Blender Developer Wiki, more here Source/Nodes - Blender Developer Wiki
As you can see above, everything nodes it’s a ‘grassroots’ project. It’s a rewrite of everything based on nodes and these low level ‘functions’ which are acting as low level execution units.
Now, I think I know where this is coming from. These functions are easy to compile/transpile and execute on CPU/GPU LLVM and other platforms. Also, there is the intention to integrate these
functions with other parts in blender. Everything nodes is designed with optimizations in mind.
Ok, here is my main concern with this approach. It sounds good on paper, but in reality things will not stay the same. Why I am saying this?
If you analyze the current state of affairs in blender you will immediately see some concerns:
Language inflexibility
- The main problem in blender is the fact that is written in C. We know that C is a system language and very strict about everything, strong typing, static dispatch, etc.
All these things are great when you are writing oses and low level embedded code, but not so great for writing applications: slow dev sprints, lots of segmentation faults, lack of proper
memory management, slow refactoring, inflexibility in general. What is the solution here? Loosen up. Decouple everything. Dynamic dispatch through unified publish/subscribe bus (like this one https://zeromq.org/), better mem mgr with reference counting, declarative interfaces everywhere if possible through simple definition languages like JSON
EDIT: Dynamic dispatch is intended for exclusively for HI-LEVEL functionality(stuff you usualy expose through plugin interfaces), not for every C function call.
Event system
- Lack of an unified event system. If you are writing a plugin, the first thing you are going to notice is the fact that you don’t have an unified event bus. you can’t listen to keyboard events
without writing a ‘modal’ operator. If you want to update state from a draw function, you get a segmentation fault. Everything is in lockdown mode while drawing. Why? You can batch updates
for later to be executed. Having an unified: UI, timer, draw, input, ops/modifiers/animation life cycle events, for internal or external use, accessible through a common socket based interface is not much of an issue. I think.
Interfaces/contract
- Lack of an unified interface for ops, modifiers, silent modifiers(like auto merge in edit mode, keyframing/animation/constraints/drivers) and external addons. I believe that most of these can share a common interface based on pub/sub event subscription.
Splitting them into separate concerns is not exactly following the SoC principle. Breaking them apart into: function, simulation, with binding etc. Data(ram) and time(cpu ticks) are just resources and event triggers. There is no difference between a bevel modifier and fluid simulation. One is data dependant and the other one is data and time dependant, one extra dimension to deal with. But in essence, they are doing the same thing, data transformation. I know that now modifiers are working with DerivedMesh, but switching to the new architecture would be easy when you have proper lifecycle events for operators.
You can chain them, batch them, wait for parallel finalization, run them on ‘live queries’, and depsgraph can be used to manage the data and make sure it’s staying immutable(I think)
Operators have the same problem. In reality, there is no SoC between modal/interactive and simple execute operator. They differ only in terms of decorations.
Live events
- Event masking/filtering/conditionls/livequery. Introducing the concept of ‘live queries’. Live queries are just simple events expressions which can combine multiple sources under a single event subscription. E.g if (keyboard shortcut event && ops.add mesh completed) then show a popup message. This will be essential if you want param expressions for node params like H does it.
Execution
- Fire and forget, stateless, task based, queued execution units(ops/modifiers/addons/etc) managed through:
- zeromq(event bus)
- depsgraph(data mgr)
- thread & task queue mgr similar to iOS GCD. I don’t know exactly how this would work in C because you don’t have managed mem and closures, but if you come up with declarative input and output definitions for the depsgraph to know what you want to keep alive or not for longer periods of time,
this might work… also data(objects/mesh) can be bound to the node itself.
Data binding
-
Drivers and constraints in houdini are nothing more nothing less but math expressions with param linking/binding
-
Fully declarative interfaces and event definitions(JSON/protobuf/flatbuffers like) for the above. You can exclude the rendering(EEVEE/Cycles) side, for now, for performance reasons.
-
Node parameter linking & node paths. You should be able to link/bind node params across node groups /networks via node path definition similar to a file path definition.
-
Default params everywhere. convention over configuration here. The concept of ‘sensible defaults’ applies here very well. The user is only modifying the defaults. In this way you make sure that ‘operators’
are not producing crashes or unstable results. -
Data with attributes. Deferred responsibility vs micromanagement is the question here. So far, I have not touched this problem at all because, from my pov, it the least important. It’s just data with attributes. The way you look at it is relevant only in context.
You don’t want to attach more meaning than it’s necessary to the data. In H they have points/primitives/vertex/detail and object attributes with precedence from the lowest to highest, and that’s the whole philosophy.
Geometry attributes
Plugins
- Same generic operator pub/sub interface for plugins as well, sockets & shared mem instead of language based(ctypes) language bridging. This will allow for any kind of scripting engine to communicate with blender, including clustering multiple blender ‘nodes’, running in parallel, and cloud support.
UI/UX
- UI/UX Everything nodes is not going to mean anything, if the end user is not happy From what I have seen in the docs, these low level ‘function’ execution units are very simple and similar
to math utility functions. Attaching only one of those to a node is not going to solve a real problem. Like bevel, or boolean modifier does. Of corse, you can chain them, or create sub-networks
of such low level nodes, but still not very user friendly. The user wants a minimum number of nodes on screen because is hard to follow a large tree of nodes everywhere. What can be done here?
Well, the node tree should be context aware and pass-through. What this means? It means, that you can connect anything to anything, and the node: based on it’s function, context and parameters
will apply the transformation only to the part is concerned with. This way, you are going to eliminate lots of unnecessary conversion nodes, and many input nodes if you can pass these inputs as params to the node itself. Exactly, the way houdini is doing it. Or, even better, treat collections, objects, mesh data on the same level. Multilevel is inconvenient sometimes in H.
Edit mode in H is just a single node!! You can have multiple, such nodes chained
TODO
Steps for ‘quick’ refactoring:
- study zeromq or similar
- study declarative programming and interfaces
- study JSON/protobuf/FlatBuffers or similar for serialization across networks, also shared memory
- integrate zeromq
- define the new generic ops/modifiers/addons interface
- prepare the run loops
- run every event through the new run loops system
- add support for execution queues(tasks you want execute async in the right ctx)
- slowly move the current ops/modifiers through the new interface
- add nodes and new stuff
Conclusion
Overall, I believe that a proper architecture should be a little bit more courageous in terms of technologies and modern design practices. Switching from basic proceduralism
to a more declarative and functional approach is not such a big deal, but is coming with great advantages in terms of time to market, flexibility, stability and quality overall. Yes, you
might have to sacrifice a little bit of performance. IMO, you don’t want to start with low level optimizations until you have a working PoC with everything else in the picture. I wouldn’t jump the gun here, for such major refactoring. Think about it. What happens to your low level functions if your current data model in blender is not generic enough or performant enough, are you going to rewrite everything again to accommodate the new data model? I believe this matter is being approached from the wrong end… blender, right now, lacks the ‘infrastructure’ to support something so dynamic as ‘nodes’ with interoperability across all systems in mind.
EDIT: This is suppose to be hi-level architectural change. To add a ‘cake layer’ on top of the existing code base. I explained better in the post bellow.