AIS. Connecting objects.

(This post was written 2+ months ago but got stuck in my drafts folder. Sorry for that)

The Nokia's once famous motto 'Connecting people' has probably gone forever (especially given recent acquisition by Microsoft), but let us grab its idea. This post will be about a nice 'connecting' concept offered by the AIS (Application Interactive Services) package, part of the visualization mechanism of Open CASCADE. Unfortunately, like many other gems scattered across the product, this one sits virtually undocumented and therefore its powerful features are significantly underutilized in OCC-based apps.

This is about AIS_ConnectedInteractive and AIS_MultipleConnected, which allows you to construct derived visual representations from already computed (or to be computed) ones. Imagine an assembly consisting of various sub-assemblies, which in their turn may consist of further sub-assemblies, and so on. Eventually each sub-assembly is broken down into part(s). Each part or sub-assembly are 'instantiated' in a parent assembly, where an instance is a reference to a referred part or assembly plus an attached transformation. Here is an example of an assembly (from STEP test suite as1*.stp):
The next image is an assembly tree where you can see instances of various sub-assemblies.
Then also goes a sub-assembly l-bracket-assembly consisting of instance of a part named l_bracket and 3 instances of nut-bolt-assembly (each with different transformation), where each is a combination of two instantiated parts (bolt and nut).
AIS_ConnectedInteractive and AIS_MultipleConnected offer efficient way to compute visual representations of such complex assemblies.

AIS_ConnectedInteractive essentially implements the proxy pattern (or alike) and contains a reference to another AIS_InteractiveObject and transformation matrix.

Handle_AIS_InteractiveObject aRefObject =
 CreateRepresentation (...); TopLoc_Location aLoc = ...; Handle_AIS_ConnectedInteractive anInstance =  new AIS_ConnectedInteractive; anInstance->Connect (theObject, aLoc);

Note that you don't need to know details of aRefObject representation, you just attach it to anInstance.
Thus, AIS_ConnectedInteractive is well suited to create visual representation of an instance in the assembly hierarchy.

AIS_MultipleConnected follows the composite pattern and allows to combine multiple representations into one. Therefore it is an efficient way to construct visual representation of the (sub-)assembly.

Handle_AIS_MultipleConnectedInteractive anAssebly =
 new AIS_MultipleConnectedInteractive; anAssebly->Connect (aChild1); anAssebly->Connect (aChild2);

Again, this allows to abstract from internal details of each child object.

I use the above approach to construct visual representations of objects in a new data model designed in CAD Exchanger (currently planned to be made part of public API in version 3.0), and so far it works great.

To further abstract the creation of a final representation of the root object (or any interim one selected in a tree browser), there is a factory method that returns a handle to an object of the base type AIS_InteractiveObject.

Handle_AIS_InteractiveObject anObject =
 ModelPrs_InteractiveObjectFactory::Create (...);

Thus, the callers do not know the real type of the object that will be created – it can be either AIS_MultipleConnected, or AIS_ ConnectedInteractive, or AIS_Shape, or any other subclass of AIS_InteractiveObject.

The key benefits of *Connected* are in reusing representations of referred objects, instead of recomputing them. This allows to:
  • Reduce computation time (as creation of part representations takes the greatest time)
  • Reduce memory footprint (you do not have to create extra objects supporting the representations)
  • Reduce code size (you do not have to design extra classes for composite objects, only parts representations are really necessary)
  • Achieve greater flexibility and abstract approach (internal implementation details of referred objects are hidden and can change independently from instances and assemblies) 

To have a quick test, you might want to try DRAWEXE:
pload ALL
wedge w 1 2 3 0.5
vdisplay w
vconnect iw -5 0 0 1 0 0 0 0 1 w #creates AIS_ConnectedInteractive of AIS_Shape
box b 2 0 0 3 1 2
vconnect a -5 0 0 1 0 0 0 0 1 w b #creates AIS_ConnectedInteractive of AIS_MultipleConnected

At last, some limitations to be aware of:
  • The referred objects must belong to the same AIS_InteractiveContext. This is unfortunate but is not specific to *Connected*.
  • Setting attributes to a referred object (e.g. part) affects the referring representation (which is sort of expected). Setting an attribute to a referring object seems to have no effect.
  • Some glitches with selection (as reproduced in DRAW). 

Currently these are not affecting CAD Exchanger development, so I did not investigate these in greater details.

Anyway, hopefully these hints will be helpful for those dealing with complex data structures and GUI. If you already worked with the *Connected* and have some experience to share, it would be great to hear your comments.



Significant side effects in OCAF/XDE 6.6.0

This is a heads-up for those who deal with OCAF documents and migrating to 6.6.0.

Issue: You may observe sporadic crashes especially when dealing with AIS_InteractiveContext and/or XDE documents.

Root-cause: The root-cause of the issues is side effects of the fix #23523. It has changed the order of destruction of the document contents.
In 6.5.4 and earlier, the destructor of TDocStd_Document let the TDF_Data destructor be called what caused the following order:
- the attributes got destructed in the reversed order in the tree (i.e. first destructed were the attributes at the deepest label 0:x1:x2:...:xn, last - at root label 0:)
- the attributes got destructed while still attached to their labels (i.e. Label() in destructor still returned a valid label)

With 6.6.0 the TDocStd_Document destructor explicitly calls TDocStd_Document::Destroy() which does the opposite:
- it calls TDF_Label::ForgetAllAttributes() which removes the attributes in the direct order, from the root to the deepest label
- it first detaches the attributes' label prior to nullifying the attribute, so Label() in destructor will now always return 0.

This new behavior has at least two side effects that affected my case:
1. If the OCAF document has an attached AIS_InteractiveContext, then a crash happens upon document destruction. The root-cause is that TPrsStd_AISViewer attached to a root label contains a handle of AIS_InteractiveContext and TPrsStd_AISPresentations (at sub-labels) contain AIS_InteractiveObject's. Each of the latter has a *pointer* (not a handle, to avoid circular dependencies) to that AIS_IC context. Now when the document is destroyed in a direct order, then AIS_IC gets destroyed first (as part of TPrsStd_AISViewer destruction). So all pointers in AIS_IO's become dangling. Then during destruction of TPrsStd_AISPresentations, the method AISErase() access those dangling pointers, leading to a crash.

The same will happen in similar cases, when you have a resource on "top" labels pointed to from the "deeper" labels.
The work-around for the AISViewer was to ensure another handle to AIS_IC outside of the document which would keep the object (i.e. refcount > 0) while the document gets destroyed.

2. In XDE, XCAFDoc_DocumentTool maintains a global map storing labels containing this attribute. This design already had a flaw and at some point I had addressed it with fix 23593. That time I made a note that a better fix would get rid of a global map.
With 6.6.0 behavior XCAFDoc_DocumentTool::Destroy() is no longer effective - as explained above, upon destruction the label is already null, so the global map keeps on containing orphan labels. Depending on memory allocation, a similar label node address can be created and an access to a map would incorrectly retrieve a mapped value for that label, leading to a crash.
The new fix (24007) resolves this by getting rid of global map and keeping self-contained XDE document.  It now just stores a reference (tree-node attribute) to uniquely find a document tool label  inside the document. The fix works fine for the documents stored with older versions of XDE.

1. You currently cannot rely on the order in which the document gets destroyed.
2. You should avoid accessing the resources stored at other labels during attributes destruction. Perhaps for some cases, this would require tweaks outside the OCAF doc (like in the case of AIS_IO described above).
3. Deallocation of resources by attributes should not happen in their destructors. Instead this should be done in a redefined method TDF_Label::BeforeRemoval(). So, the first implementation of 23953 was suboptimial anyway.

Recommendations/requests to OCC:
1. My personal point is that previous "bottom-up" destruction order is more practical than current "top-down". Like in C++, destruction order should be opposite to creation one, and in most applications you populate the document from top to bottom.
2. I believe OCAF must document and maintain a guarantee of the destruction policy, so that the user's applications can rely on this guarantee. Is this something OCC team can plan to address ?

Thank you in advance,


Simple memory allocations

The great convenience of standard collections for a developer is that they free him/her from extra efforts on managing underlying memory allocations.
It’s so much convenient to just write
    std::vector v(n);
    for (int i = 0; i < n; ++i)
        v[i] = i;

or likewise for OCC fixed size array:
    TColStd_Array1OfReal a (0, n-1);
    for (int i = 0; i < n; ++i)
        v(i) = sqrt (i);

and be done. Who cares where the memory gets allocated for underlying elements ? Not a big deal, right ?

Well, it can become a big deal if you have to work with multiple allocations/deallocations which become the hotspots.

I was recently improving thread-safety of a few core Open CASCADE algorithms: approximation, intersection, etc (see #23952) and observed an interesting pattern that motivated posting this blog.

Some OCC developer(s) realized in the past that memory allocations can be expensive in hot cycles and worked around this by using allocation in static memory. Excerpt from ApproxInt_ImpPrmSvSurfaces::Compute() (ApproxInt_ImpPrmSvSurfaces.gxx):

static math_Vector BornInf(1,2),BornSup(1,2),F(1,1),
  X(1,2), Tolerance(1,2);
static math_Matrix D(1, 1, 1, 2);

This would result in a single allocation (upon first entry into this function) which would stay until the program termination.

Obviously that does not work in multi-threaded environment. This would create data races – as two or more threads concurrently calling the same method – and results would be unpredictable (wrong and/or unstable results, sporadic crashes, etc).

Substituting with local variables
math_Vector BornInf(1,2),BornSup(1,2),F(1,1),
math_Matrix D(1, 1, 1, 2);

would resolve the reentrancy problem but would create another one – continuous memory allocation/deallocation would undermine performance as each constructor and destructor would call new[] and delete[] operators.

The good news is that Open CASCADE provides constructors for math_* and TCollection_Array* classes that accept an extra parameter – an address of allocated memory. In this case they simply reuse that memory and only provide access to it. Neither do they attempt to free it upon own destruction. This provides a possibility to allocate data on stack (which has essentially zero performance cost) and make respective objects use it, preserving source compatibility with the rest of the code.

Here is a fix that uses that:
  Standard_Real aBornInf[2],aBornSup[2],aF[1],aX[2],
  math_Vector BornInf(aBornInf,1,2),BornSup(aBornSup,1,2),
    F(aF,1,1), X(aX,1,2),Tolerance(aTolerance,1,2);
  Standard_Real aD[1][2];
  math_Matrix D(aD,1, 1, 1, 2);  

You might want to take advantage of this simple technique whenever dealing with small objects and knowing the required sizes in advance.