Inside XSI Architecture
October 30th, 2005 by Luc-Eric - Viewed 3543 times - Popularity: 6%An aggressive crash course to XSI architecture
Not a DAG?
One day, I made the comment that XSI is not an operator graph. That technically true, but on the surface it’s not something one wants to hear, because of this simple formula :
Operators = good not operators = bad.
That’s over-simplifying things. The technical truth underneath it all is that XSI is a data graph with operator relations, and not an Operator Graph. The Fx Tree, however, is a true operator graph.

And operator graph is often implemented as a DAG, or Directed Acyclic Graph which means it is a graph that has no cycles. (You may be aware that there are cycles in XSI that can happen in a number of senarios, including IK and constraints, so at least that Acyclic part can’t be true.)
An operator graph is a graph where each operator (drawn as circles) in the graph takes a number of inputs, does some work, and creates a new output it passes to the next step. If it takes no input, it’s considered a “generator”. In the image to the left, the graph is evaluated from the bottom to the top, as it is in compositing trees. The data is born at the leaves, and eventually “falls out of the pipe” at the end of the operator graph.
In the classic implementation, an operator creates new data. So if you had an operator that moved a point on a mesh, it would take the mesh as an input, and make a copy of the entier mesh with one point modified. It has to do this because the data flows only one way : the inputs are “Read Only”. This is required because the data of the input could be, for example, a cache that the system is keeping, or maybe that input is going to multiple operators. So don”t screw with that input data.
In some implementations an operator graph may optimize memory and processing by allowing an operator to change the input and give that directly as its output instead of a copy. This is called “pass by reference”, as oposed to “pass by value”, and it can be used if caching an input is not necessary and it isn’t re-used anywhere else in the graph, or cheap to re-generate. But I disgress.
In a pure operator graph architecture, all your data is expressed with Operators, and their parameters, and operators are usually connected directly to other operators.
For example, in the Fx Tree, there is no “image” object, there are only operators that generate images. When you save the scene, no pixel data is saved in the scene, only operator settings are saved
The Operator Stack..

XSI is a data graph. That means the fundamental builting blocks is an object that contains data. In XSI, it is possible to have no operators at all in your scene. This happens for example when you “Freeze” everything, i.e. delete the construction history. The data structure (minus the actual geometry data structures) is pretty much what you see in the Scene Explorer. It consists of property sets arranged in a hierarchy, what we call nested data.
A data object constains zero or more parameters, and may have internal data as well that is not parameters. For example, a weight map object contains arrays of values.
Each object has what we call an operator stack, which represents the construction history of that specific object. It is an array of connections, which we read from bottom-to-top. Operators may be connected to the entier object, or only to some of its parameters.
Operators may read from an object, or write to it.
The first operator to write to an object is called the Generator.
Generators
When you animate a parameter, it creates an operator that contains an animation curve, and this operator is connected to the parameter of the object. On the connection stack, a “write” entry is created, with specification that it is for a specific parameter, instead of the entier object.
That animation curve operator is said to “generate”that parameter, because it overwrites whatever value was there previously. When you create a weight map property, a “Weight Map Generator” operator is created, and with its parameters it “generates” the weight map by giving it its initial values.
Modifiers On Bicycles And Operators In Relationships
We’ll call any operator that processes an existing object a Modifier. For example, the Move Point or the Paint Weight operator are modifiers.
In XSI, the operators modify data “in place”, in order words, an operator like the Paint Weight tool will take the weight map, and modify only the values that you touched with your brush. It will not create a new copy of the Weight Map, plus modifications, which is what a pure operator graph would do.
This implies that the operator reads from the same data object that it writes to. If this were expressed in terms of an operator graph, instead of a stack, it would mean that it creates cycles for almost all operations.
When an operator reads from another object that it writes to, it establishes a dependency between two objects.

Things are Getting Lazy and Dirty
In this sample image, we have three data objects. A, B and C. B is nested under A. C is somewhere else in the scene.
Object A has three operators : (1) is a generator, and (2), and (3) modify the object subsequently in that order. The Scene Explorer also shows you the operator stack bottom-up like this.
Object B and C are in relation with an operator, (4). This could for example be an operator that reads the camera position, and moves the object in relation to it.
If a parameter changes in operator (2), the operator stack is said to be “dirty” starting at the output of (2).
When an object becomes dirty like this, a notification is sent upwards. It travels through the nested data, going up the tree and telling anything who cares that something will need to be re-evaluated. Generally in XSI, the pieces that care are the Viewports, the render region, or a property page.
This is a called Lazy Evaluation and it has two fundamental parts. First, when parameters are changed, the data is only set to be “dirty” and no precessing is done. That’s pretty lazy. Secondly, when other pieces of the software find out that things are dirty, they will only ask processing for the part that are essential to what they need.
For example, an expression operator that is reading from a parameter on an object will only ever cause the evaluation of that parameter, and not the entire object.
Let’s say we only have a viewport on screen. The viewport will hear the news that data is dirty from the Scene Root, and figure the scene needs to be redrawn.
The scene will begin to redraw, and each time an object is about to be drawn, the operator stack will be evaluated as necessary to “clean” the dirty object. In this example, this means that all the operators of A need to be re-evaluated.
Now, XSI is smarter than this, it in many case it will have made a cached copy of the object just prior to giving it to operator (2). In that optimized case, that means that only operator (2) and (3) will re-evaluated. The minimal set of “dirty” operators.
Invisible objects will remain dirty until their data is necessary for someone.
In the third box of the figure, a parameter has changed on operator (4). This will make object B dirty. As an object nested under object A, it will also indirectly cause A to become dirty. Object C is completely unaffected by this, as operator (4) is reading from it, and Object C has not itself changed in any way.
When a parameter in a custom property nested under a 3D object changes, for example, it does not cause the geometry to be re-generated because even though the custom property may appear to be “under” the object in the Scene Explorer, it in fact isn’t. In addition to this, XSI uses a separate data structure for geometry data such as triangles with additional caching technology that minimize unnecessary evaluation.
Screwing the Architecture
If we say that object A and B are visible 3D objects, when the viewport need to redraw it will evaluate B, followed by object A.
The evaluation will look like this
- Object C - nothing to do
- Operator 4
- We can draw object B now, it’s clean
- Operator 1
- Operator 2
- Operator 3
- We can draw object A now, it’s clean
The best way you can break the architecture is dirtying the graph while the graph is trying to clean it, or reading and writing to objects without being connected to them.

For example, what would happen if operator (4) decided to access (i.e. Read) the parent of B to get some info from it, without using an operator connection? It would get access to a copy of “A” that is “dirty”. This would work some of the time, but would break during playback or render. Operator (4) would be getting the values of the previous frame evaluated, or some random, uninitialized value. This is why it’s extremely important to explicitly have an connection to everything an operator will use.
What would happen if operator (4) decided to modify object C? First of all, no one would be notified about this, so the scene wouldn’t refresh. Secondly, since “C” has no reason to ever ask for an evaluation of operator (4), the result of the scene would be random. Sometimes it would work, sometimes it would not, it depends if the rest of the scene has somehow caused operator (4) to evaluate.
What would happen if operator (4) decided to modify object A? Again, random results. The only operators that are declared to modify A are (1), (2) and (3). This means that the modification that (4) could do to A could be overwritten by other operators at any moment.
And what is the worse way to screw the architecture? Operators that create new object. As you can see here from this explanation, operators are attached to objects at creation time and stay with them until their death. Data objects are the fundamental objects, not operator. And operators must be connected to the data they create or modify. Therefore, it is not possible from an operator to create new objects because it is being called to validate an existing object.
When a scripted operator calls a command to create new objects, it is adding new elements in the scene and causing “dirty” notifications to be sent. Which means that the scene can never be clean after an evaluation of the operator stacks, which in turns breaks the fundamental rule of the architecture.

Out Of The Beaten Path
Sometimes, you will read from object, but not write to it. In the previous pictures, operator (4) is only reading from C.
What happens when more operators are added to object C? Let’s make a new picture for this.
Here we have object “H” and an operator numbered (10). Operator (10) only reads from object H, but we have now added a new operator, (11).
There is a problem with this.
At the first evaluation, you have object H, Version 1, which operator (10) reads.
Then operator (11) modifies H into Version 2.
We have now lost version 1, so we can never call operator (10) again.
This problem occurs because H did not have a generator. In other words, there is no operator that exists that can create a bran new object H, which is what is needed to re-evaluate (10).
In this case, the operator architecture must create a “safe copy” of the object and it will add an internal copy operator to simulate that missing generator.
The problem addressed here is the answer to the question “Why can’t I freeze only part of the operator stack”?
You can’t freeze only part of the operator stack because an operator stack (or graph) architecture needs to be able to re-generate the data at any moment. If you’ve lost some step of the construction history, then you can’t do that. It’s as good as having lost all of them.
Like making a cake, you can’t skip steps.
When an object is completely generated by an operator stack, XSI only saves the operators and not the data. At load time, it can re-generate everything by evaluating the operator stack, just as the Fx Tree re-generates the images from the fx operators.
I hope this helps, or confuses in new and interesting ways.





October 31st, 2005 at 4:58 am
This last set of articles was very interesting. Now if you would allow a suggestion I would like to ask for a follow up on how the (C++) custom operator port architecture fits in.
For me it would seem logical to provide an architecture for custom operators that reflects the just explained lazy eval design. This means XSI developers would have the ability to read the dirtiness state of input connections, cache an optimized version of input data and reevaluate only when necessary.
Now I wonder if native/factory operators have the same limitations that custom operators have?
Is there some project that mastered the multi input case that you would consider the reference implementation?
Does the Syflex operator-to-operator-connection scheme have anything to do with getting a dirty flag?
Thank you very much.
Felix
October 31st, 2005 at 2:11 pm
The SDK operators are pretty much the same as in the operators internally.
If the user changes the parameters of an operator with a property page, or an interactive tool, normally XSI will have installed a cache on all the inputs of that operator, so there is no need for an operator to do internal caching.
However I understand that there are cases, like a simulation operator, that needs more advanced knowledge of what, exactly, to maintain a performance-critical internal cache. I personnally haven”t been involved in writing one of those, so it’’s best to discuss this issue directly with the XSI SDK support Team.
November 4th, 2005 at 7:06 am
Very good article! More articles like this one (maybe more in-depth) would be very appreciated.
Now there’’s one thing I would like to ask you: I”m playing right now with a simulation operator and I”ve noticed a strange behavior, which I”ve been able to reproduce in a very simplified way (repro steps are below).
The operator refresh (when the timeline plays for example) is caused by a key in the custom parameter set of the scripted operator.
If at the end of the script you go to the next frame or hit play, everything works as expected, and the operator creates more and more triangles.
If at some point you stop the timeline and inspect the smooth operator, it seems that the smoothOp is only affecting the old geometry; on more complex operators, sometimes this can lead (directly or indirectly) to crashes.
I”ve found a workaround for this (seems that connecting a weightmap to the smoothOp strength parameter fixes the problem) but I can”t understand what’’s wrong in this example.
” //////////////// code start ////////////////
newscene, false
set oGrid = activeSceneRoot.AddGeometry(”Grid”,”MeshSurface”)
ApplyTopoOp “DeleteComponent”, oGrid & “.poly[*]“, siUnspecified, siPersistentOperation
freezeObj oGrid
” //// creating a 1st custom operator
scop_array = Array( _
“sub my_custom_op_Update(ctx, out, InGeom)”, _
“k = 0.02″, _
“n = ctx.UserData”, _
“ctx.UserData = n + 1″, _
“set oPrim = InGeom.Value”, _
“set oGeom = oPrim.Geometry”, _
“redim vd(2,-1)”, _
“redim pd(-1)”, _
“for j = 0 to n”, _
“i = j * 3″, _
“ii = j * 4″, _
“redim preserve vd(2,i+2)”, _
“redim preserve pd(ii+3)”, _
“vd(0,i+0) = 0″, _
“vd(1,i+0) = i * k”, _
“vd(2,i+0) = 0″, _
“vd(0,i+1) = 0″, _
“vd(1,i+1) = i * k”, _
“vd(2,i+1) = 10″, _
“vd(0,i+2) = 10″, _
“vd(1,i+2) = i * k”, _
“vd(2,i+2) = 10″, _
“pd(ii+0) = 3″, _
“pd(ii+1) = i+0″, _
“pd(ii+2) = i+1″, _
“pd(ii+3) = i+2″, _
“next”, _
“if uBound(pd) > -1 then oGeom.Set vd, pd”, _
“end sub” _
)
scop_str1 = “”
for i = 0 to uBound(scop_array)
scop_str1 = scop_str1 & scop_array(i)
if i uBound(scop_array) then
scop_str1 = scop_str1 & vbNewLine
end if
next
set oScop1 = XSIFactory.CreateScriptedOp(”my_custom_op”, scop_str1, “VBScript”)
set oGroup = oScop1.addPortGroup(”MainGroup”)
oScop1.AddOutputPort oGrid.activePrimitive, “OutGeom”, oGroup.index
oScop1.AddInputPort oGrid.activePrimitive, “InGeom”, oGroup.index
set oSmoothParam = oScop1.AddParameter ( XSIFactory.CreateParamDef2(”smooth”, siBool, true) )
oScop1.Connect
SetKey oSmoothParam, 1, True
” //// adding a smooth operator on top of 1st custom operator
smooth_cnxset = oGrid
set oSmoothOp = ApplyOperator(”Smooth”, smooth_cnxset, siNode)
”oSmoothOp.strength.value = 1
”oSmoothOp.preservebordervertices.value = true
”oSmoothOp.reproject.value = true
SelectObj oSmoothop
” //////////////// code end ////////////////
Thank you!
Ciao
Michele
November 4th, 2005 at 6:19 pm
That script has been massacred by the HTML formatting.
I would suggest making sure that you change it modify only the object that is in the Output and not assume that the Input in the same object as the output, as the code I believe is doing above.
When you open a ppg onto the object, a cache will be installed at the input of your object. So it’’s not the same object anymore as the output object, it’’s a frozen version of the geometry without any changes.
November 5th, 2005 at 4:07 am
Thanks for the answer Luc-Eric, but I still can”t figure it out.
I”ve tried using some other deformers in place of the smooth operator, but smooth seems to be the only one causing problems with this scop.
Here’’s a copy of the above script, http://www.motionblur.it/tmp/repro_operator.txt
To test the problem, simply run this script, press play and when it reaches the end of the timeline, press enter many times, to inspect the selected smoothOp.
Probably the problem is somehow in relation with the cache installed when i open a ppg, but I don”t understand why a scene evaluation is triggered when I open a ppg, without changing anything.
One more thing: I”m using a key on my custom operator parameter set which forces updating the operator every frame. With scops I”ve got also the choice of using the alwaysEvaluate flag.
But I can”t understand how the CustomOperator PutAlwaysEvaluate() function works with c++ compiled operators (v4.2).
Thanks a lot!
Michele
November 7th, 2005 at 4:56 pm
I”ve looked at this and I don”t know the answer, so this would unfortunately require the specialists in the SDK group to look into this.
November 8th, 2005 at 3:18 pm
My colleagues have looked into this, and we conclude that this operator is violating one of the rules of the architecture.
The rule is : given a set parameters and a time, an operator must always produce the same result.
It’’s very possible that the operator will be called more than once for the same frame, even during the same evaluation, so doing something incremental based on a member variable that you increment on any evaluation is invalid. It should be anchored to the current frame, for example.
November 9th, 2005 at 2:26 am
This scripted operator works in interactive mode, so if you move the operator parameter slider in the same frame, it will continue to evaluate.
I”ll add geometry caching inside my operator to recycle the last vertex and polygon data arrays (but there’’s another workaround, which is to plug a weightmap into the strength parameter of the smooth operator - this seem to force multiple evalutions in the same frame).
Thanks a lot for help and time!
Michele
November 9th, 2005 at 5:35 pm
Every operator re-evaluates if the parameters changes during the same frame. My point is that the operator must create the same result with a given set of parameters at a given frame. It can could called multiple times during a single evaluation, for example if the system needs to creating multiple data copies. The system expects that operators will get the same repeatable results.
Unforuntately, right now the SDK doesn”t have the services to write an operator that would have a ”live” mode that updates in real time (for example on a timer) in the viewport why XSI is not in playback mode. (However I”m thinking this may be possible with real time shaders.)