New data structures for node group interface declaration #109135
Labels
No Label
Interest
Alembic
Interest
Animation & Rigging
Interest
Asset Browser
Interest
Asset Browser Project Overview
Interest
Audio
Interest
Automated Testing
Interest
Blender Asset Bundle
Interest
BlendFile
Interest
Collada
Interest
Compatibility
Interest
Compositing
Interest
Core
Interest
Cycles
Interest
Dependency Graph
Interest
Development Management
Interest
EEVEE
Interest
EEVEE & Viewport
Interest
Freestyle
Interest
Geometry Nodes
Interest
Grease Pencil
Interest
ID Management
Interest
Images & Movies
Interest
Import Export
Interest
Line Art
Interest
Masking
Interest
Metal
Interest
Modeling
Interest
Modifiers
Interest
Motion Tracking
Interest
Nodes & Physics
Interest
OpenGL
Interest
Overlay
Interest
Overrides
Interest
Performance
Interest
Physics
Interest
Pipeline, Assets & IO
Interest
Platforms, Builds & Tests
Interest
Python API
Interest
Render & Cycles
Interest
Render Pipeline
Interest
Sculpt, Paint & Texture
Interest
Text Editor
Interest
Translations
Interest
Triaging
Interest
Undo
Interest
USD
Interest
User Interface
Interest
UV Editing
Interest
VFX & Video
Interest
Video Sequencer
Interest
Virtual Reality
Interest
Vulkan
Interest
Wayland
Interest
Workbench
Interest: X11
Legacy
Blender 2.8 Project
Legacy
Milestone 1: Basic, Local Asset Browser
Legacy
OpenGL Error
Meta
Good First Issue
Meta
Papercut
Meta
Retrospective
Meta
Security
Module
Animation & Rigging
Module
Core
Module
Development Management
Module
EEVEE & Viewport
Module
Grease Pencil
Module
Modeling
Module
Nodes & Physics
Module
Pipeline, Assets & IO
Module
Platforms, Builds & Tests
Module
Python API
Module
Render & Cycles
Module
Sculpt, Paint & Texture
Module
Triaging
Module
User Interface
Module
VFX & Video
Platform
FreeBSD
Platform
Linux
Platform
macOS
Platform
Windows
Priority
High
Priority
Low
Priority
Normal
Priority
Unbreak Now!
Status
Archived
Status
Confirmed
Status
Duplicate
Status
Needs Info from Developers
Status
Needs Information from User
Status
Needs Triage
Status
Resolved
Type
Bug
Type
Design
Type
Known Issue
Type
Patch
Type
Report
Type
To Do
No Milestone
No project
No Assignees
5 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: blender/blender#109135
Loading…
Reference in New Issue
No description provided.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A proposal for replacing the "interface" definition of node groups.
Currently the interface of a node group is defined by a list of
bNodeSocket
inputs and outputs. This is not good since the declaration of a socket is easy to confuse with instances of the socket (i.e. actual sockets on an instance of the group node). Because replacing these data structures has no immediate impact on users it is difficult to justify on its own.However, we are going to add panels to (group) nodes, which requires changes to these node group declarations anyway. This proposal is describing data layout and API changes to enable panels and give node groups more flexibility for the future.
⚠️ Forward-compatibility
In order to have forward-compatibility, should we choose to require it, we need to keep the old inputs/outputs data around, and create matching socket declarations on blend file write.
Dropping forward compatibility requirement with 4.0 is the easier option.
bNodeTree
bNodeTreeInterface
to replace current node tree "interface".inputs
/outputs
replaced with a singlesockets
list.panels
list remains.bNodeSocketDeclaration
stores pointer to its panel.bNodePanel
stores pointer to its parent panel (future nesting support)blender::nodes::NodeDeclaration
bNodeTreeInterface
.bNode
inputs/outputs lists:bNode
inputs
/outputs
lists remain unchangedDrawing loop pseudo code:
UI design task for node panels: #108895
I have a few initial thoughts after reading this:
bNodeSocketDeclaration
would have some type specific data as well, like the default value?Yes, there would be a runtime cache to provide fast access to panel-of-socket and sockets-of-panel. This could be both in the DNA data and the
NodeDeclaration
, depending on where it is needed (e.g. if we use declarations for rendering panels on node instances).If nested panels need to be supported then the index-in-sockets approach alone won't work. With this way of describing sections the parent panels need to go into a separate list, which indexes the 1st layer of panels.
The model also does not allow gaps between panels of the same level: each panel extends until the next panel starts. For sockets this is part of the design (bare sockets first, then the panels) , but for higher-level panels this could be an unwanted limitation. The panel could store an optional length, using maximum extent by default.
Alternative is to store pointers to parent panels, both in sockets and in panels themselves. We probably want to keep the sockets and panels ordered topologically in this case.
I was looking for a way to combine these two into the same system, but the fact that declarations are virtual classes makes that risky. Might play it safe and leave the DNA as separate class, and rename it some way. Suggestions welcome.
Yes, i haven't fully worked this out yet. Derived structs for the major data types. I definitely want to avoid the mess of RNA types we have right now for sockets.
My main comment is that I'd like to avoid having a duplicate runtime and DNA version of the declaration system. That means the whole declaration system would be ported to DNA, and that builtin nodes and node groups would use the same declaration system. That's a nice simplification in my opinion, and it avoids the need to wrap the DNA system with the runtime system, for example. The problem of virtual classes doesn't seem big TBH, that could be solved with old fashioned polymorphism. We shouldn't need a socket type for each RNA subtype, which avoids the mess you mentioned in RNA.
What do you see as the benefit of this, for the code? In the UI it makes some sense, to simplify aligning sockets, but it seems like keeping a clear separation between inputs and outputs internally would be helpful.
What's the benefit of storing it this way, compared to the parent pointer approach? While the panel->socket connection you mention seems less redundant, it also has some limitations like not allowing gaps of top-level sockets between panels.
EDIT: Also, for forward compatibility, if it's required maybe the best way to handle it is patching 3.6 to allow reading the new format.
Not sure if this has been stated explicitly, but the design looks like we want to mix inputs and outputs in arbitrary order. If inputs and outputs are kept in separate lists we'd need another mapping for final positions, somehow. If sockets are in the same lists then we already have the order.
There is also the concept of creating "pass-through" sockets, where both input and output is generated on the same row (
UV
in the example above). With a shared socket list this becomes a simple matter of setting bothIN
andOUT
flag.On the other hand, what do we actually need separate lists for? When looping over just inputs or just outputs we can easily skip the other kind. Note that actual sockets in
bNode
are not affected, they remain in separate lists (not sure how we are going to do alignment there, maybe each socket gets a vertical position slot)I'm currently leaning towards dropping this idea and instead going for conventional parent pointer. This would be combined with some stable topological sorting (socket comes first if its parent comes first) and runtime caches for fast lookups.
The benefit of storing child start index would be simplicity: i don't need much further processing to get "all sockets in this panel". But as mentioned it is based on some assumptions about structure, like the "no gaps" requirement and a single parent level.
But that would mean only 3.6 can load the newer files while older versions still break, right? I think we could construct old inputs/outputs lists on write that would keep files truly forward compatible, haven't tried yet.
Okay, I'm convinced, thanks!
That's already the case for meshes though, and means we don't constantly have to convert back and forth between the old and new versions when saving files for the foreseeable future, which is ugly and and error prone as well as slower.
Fair enough, i don't have a strong opinion on this. If forward compatibility for 3.6 is sufficient we can do it this way.
Is this a good decision in the long run? Having a version-free wrapper is quite useful.
bNodeTreeDeclaration
tobNodeTreeInterface
It looks like we're going to need a combined container for sockets and panels after all, if we want to be able to place sockets in front of a panel. An array is a way of defining relative order of items, but if we store panels and sockets in separate lists we have to make assumptions about their final order.
In the example below i'm storing two arrays. Drawing generally follows ordering in the array, with a general rule that panels come before sockets.
We might want to, for example, move socket 1 in front of panel 4, within their shared parent panel 1. There is currently no way to specify this. The same applies with only one panel layer (no nesting) when we want to move sockets to the top of the list.
Currently, I think we can make the simplifying assumption that within a panel (including the root "panel") sockets will always come first and then subpanels. This way a panel can could have separate lists for sockets and subpanels which are later drawn one after the other.
Panels in the properties editor work like that anyway, subpanels are always at the bottom.
Regarding forward compatibility, see also #109151 for the policy we are trying to define for Blender as a whole. It's not clear to me how we can keep 3.6.0 compatible since we are so close to the release, would the idea be to add compatibility in 3.6.1?
I've settled on using a single flat array of items for the node group interface. Items can be sockets or panels. Sockets in turn have specialized subclasses for each data type (like bNodeSocket default_value classes but hopefully a less pervasive).
The panel hierarchy is implemented with
parent
pointers in items that place an item inside a panel.This flat structure is nice for iterating over sockets when we don't care about the hierarchical structure (which is most of the time). The downside is that constructing a hierarchical layout requires some processing to determine root items, children of panels, etc.
To make this easier the interface code will keep the items list sorted topologically: Items appear in order of their nesting depth and the relative order of children from the same parent remains unchanged (i.e. stable sorting).
The interface items have utility functions for getting root items, children of a panel, and respective index ranges. These ranges are also calculated during topological sorting. For example,
item_children(Panel7)
would returnSocket6, Socket7 Socket 8
,item_children_range(Panel1)
ranges from 6 to 8. The sorting makes sure that children can always be returned as a simple span.I think the general structure of having a base class for sockets and panels is ok if we don't want the limitation that sockets can't come after panels at the same hierarchy level. This is an ok thing to strive for, even if we might decide to limit this a bit at the user level.
I'm not convinced by the flat sorted list yet. The main argument for it seems to be that it makes iterating over sockets easier. However, I'd argue that this argument is not very strong for multiple reasons:
On top of these reasons, there is an argument against the flat sorted list: it is a non-obvious data structure which can lead to unnecessary complexity if there is not enough motivation for it. It feels like code that works on this data structure will also become less obvious. While it's not super difficult to work out, the code to move e.g. a socket or panel from one place in the hierarchy to another is not "trivial", at least I can't map it out easily by just looking at the image right now. Moving stuff around likely has to be done often in versioning code, which is best kept straight forward.
Unless additional motivation for this data structure is provided, I'd still vote for a more obvious structure that more explicitly encodes the hierarchy. What I mean is that a panel data structure just has it's own array of children, and the children have a back-pointer to the panel. Having everything in a single flat list is not important to me here, especially if it makes things less obvious. I wouldn't be so worried about iteration, because we can easily cache separate arrays for the things we want to iterate over easily.
RNA question: How important is the "mix-in" style of providing socket default values and the like?
We have those
default_value
storage pointers in sockets/interfaces for storing default value, subtype, limits, etc. That's nice because we can swap out the socket type in DNA without destroying the socket itself (which would invalidate all python objects, among other things). I'm using the same approach for the new group interfaces, using the existingbNodeSocketValueXXX
structs but in a newbNodeTreeInterfaceSocket
.However, in RNA the socket value properties are integrated into the node socket itself, such that it looks like the properties are part of the node socket instead of a separate DNA struct. This is intuitive to users, but it requires a long list of specialized RNA types: one for each socket type and lots more if the subtype is also expected to affect property display.
We've discussed this and agreed that displaying values with the exact subtype is not required, at least for the interface declarations (nodes themselves keep their current behavior). That still leaves the question of specialized RNA types per socket type. I'd like to avoid generating all these RNA types.
Options i can think of:
NodeTreeInterfaceSocket
for each socket type:NodeTreeInterfaceSocketFloat
,NodeTreeInterfaceSocketVector
,NodeTreeInterfaceSocketObject
, etc.socket_data
pointer insideNodeTreeInterfaceSocket
. Changing socket type invalidates this pointer but not the socket itself. There would be a RNA struct for each socket type (but not subtypes).I prefer (1), even all the subtypes don't bother me. I see no good reason to be inconsistent with
NodeSocket*
or break the API.If you do (2) you now have to introduce a bunch of
NodeSocketValue*
types instead, or break the API really badly.Quick draft of what the node declaration API might look like for panel support and aligned sockets. Sockets would also appear on nodes in the order they are declared, instead of the currently fixed outputs | buttons | inputs order. This will require a pass over all nodes to make sure they are consistent.
Buttons placement: does this require an explicit API method or do we just always draw them on top? Could also be optional: draw at the top by default, unless declaration explicitly adds them somewhere else. I'd maybe keep it simple for now and allow for explicit placement later. Also note that node layouts will change and might mess up existing node connection visuals in carefully layed out trees.
NodeDeclarationBuilder
additions:Example usage (raycast node):
I'm not quite happy with the
add_input_output
method: it would limit the alignment feature (input + output in the same vertical slot) to only matching socket types/names.A better option would be to keep the current
add_input
/add_output
methods, but add aalign_with_previous
option (default false). By default adding a new socket will move to the next vertical position, leaving a gap when alternating inputs/outputs. Whenalign_with_previous
is set totrue
it will instead try and use the same vertical slot as the previous socket (ignored if the previous slot already has a socket of the same input/output kind). This would allow alignment of arbitrary sockets, provided there is enough space in the layout. It would be easier to use because the builders only describe one socket at a time instead of a "double socket".For panels, with the naming add/close it's not clear to me that they need to be paired. I suggest
begin_panel
andend_panel
.For alignment, an alternative would to have a
b.add_output_aligned
that doesn't take a name or identifier, because I guess we want to make sure those match?Yes, that's more clear. It would also be possible to put
add_socket
methods into thePanelDeclarationBuilder
and create a hierarchy by adding sockets on the panel builder instead of the rootNodeDeclarationBuilder
. This is just to ensure correct scoping, the declaration output is still a flat list.Yes, that would be a good convenience method for the most common case.
The final output of the declaration would be a draw-order list of items (sockets + panels). We currently store inputs/outputs separate, but that way the order of declaration is lost and we can't alternate between inputs and outputs. Separate inputs/outputs isn't needed in declarations, it's easy enough to filter a single list by
in_out
type.Not really suggesting that we do this, but I wanted to share how I solved this when I tested aligned sockets: https://projects.blender.org/blender/blender/compare/main...JacquesLucke/blender:socket-align#diff-5348c1b18807bc1a143b9c75d173939b80882f8c
Note how I didn't do the alignment as part of the declaration but as part of the drawing function instead. Doing it as part of the declaration now is better though.
I think this makes sense. For the time being we could also just match corresponding sockets using their identifiers (only applies to built-in nodes).
I'm fine with those methods, however it seems like it would be better to use something that enforces scoping. That would most likely mean that we have an
add_panel(FunctionRef<void()>)
method on the builder. All sockets added within the callback are part of the panel.I've hit a snag when building in MSVC: the "interface" keyword is #defined there as "struct", which breaks much of my code ...
https://learn.microsoft.com/en-us/cpp/cpp/interface?view=msvc-170&redirectedfrom=MSDN
I've read
/Za
could disable all those language extensions, but haven't tried yet and not sure if it disables parts we rely on and it might be unrelated as well.https://learn.microsoft.com/en-us/cpp/build/reference/za-ze-disable-language-extensions?view=msvc-170
Regarding API: I could try and keep the existing API, but redirect the implementation to the new interface DNA. This should help avoid breaking scripts, but it also means there will be 2 separate APIs doing the same thing. The type names of the RNA structs will also change, but those is not usually needed in Python (we don't explicitly construct stuff other than through RNA methods).
Here's a comparison of common API uses:
Old interface
New interface
I recommend to just use another word, and avoid the added complexity of compiler flags.
As discussed offline, I don't think we need to preserve API compatibility here. When we make significant changes to the actual functionality, it's fine to change the API along with that to reflect the changes.
The main thing is to have .blend file compatibility here.
API question:
It looks like the
bNodeSocketType::interface_init_socket
method is no longer used. It is intended to initialize abNodeSocket
based on the settings in the interface.The new node declarations system replaced the direct creation and initialization of sockets from the interface, and it does not use this method any more. In particular for custom sockets this may have been an (unnoticed?) breaking change, but i don't recall seeing any bug reports. Maybe this part of the API is too arcane, but it looks like custom sockets are basically not customizable in node groups now. Any custom (python) socket will use the same default RNA values in each socket instance, not a per-group-node value defined by the node group interface.
The "reverse" method initializes a node group interface entry from an existing socket with the
interface_from_socket
callback. This is still in use.Do we still need this method? Is there an alternative to defining these functions in a separate python class? Part of the problem is that we need 2 separate python classes to implement both the socket type and the matching interface:
Discussed this in chat:
a95bf1ac01/source/blender/nodes/intern/node_common.cc (L134)
Custom
socket declaration use this callback again to restore ability to define custom socket interfaces.Issue reported here some time ago:
#105283