The Application Programming Interface (API).
EQUINOX-3D (EQX) and its plugin API were designed for ease of use, speed and efficiency.
At the core of EQX, is a library/API called E3D. It is responsible for maintaining and manipulating a 3D scene with Models, Materials, Animations etc.
The API is a direct interface to E3D, meaning that plugin developers can access functionality directly, without having to go through an intermediate "abstraction" layer (1), found in many other 3D applications.
To ensure high performance and low memory overhead, EQX and the API are written in C or, to be more precise, in: object-oriented C99 with dynamic typing and runtime reflection (2).
However, plugins can be written in either C or C++.
And EQX is in good company: the most popular new commercial modeler is also written in C.
This is one of the reasons for the fast launch (1-3 seconds) of EQUINOX-3D (even with about 100 plugins!), the fast renderer, responsive user interface, fast content import/export and so on.
The speed differences between other modelers and EQX can be very significant.
In some recent tests, involving 3 of the major commercial modelers, even reading an XML file (less efficient than binary) was about 60-times (6000 per cent!) faster with EQX than another modeler reading the same scene from its own binary format!
Other scenes (50000 transform nodes) that took about 7 seconds to load in EQX, caused other modelers to grind to a halt for hours, or run out of memory and crash.
EQUINOX-3D even launches and runs faster under valgrind (3) than many other modelers run natively.
The API is accessible by the plugin developer via the provided header files:
When the internal structures of a program change significantly, all plugins need to be re-compiled and sometimes changed slightly.
Many applications have an extra API layer between the plugin and the internals of the software to "hide" these changes from plugin developers, but at the cost of more memory and CPU usage.
Over time however, the internal changes tend to make the abstraction layer a tangled web that creates large overheads and introduces bugs of its own (I'm speaking from experience, having worked for one of the major 3D modeler companies),
so the benefits of a direct API far outweigh the slightly more frequent need to adapt the plugins.
Also, one could argue that the interface of an application should have a clean-enough design, so there is no need to "hide it".
EQUINOX-3D's core has proven to be flexible-enough to support entire paradigm shifts (programmable shaders, subdivision surface modeling etc.) with minimal changes.
Readers sensitive about the C++ vs. object-oriented C (or other languages) subject may want to skip the following part.
(2) EQUINOX-3D is strongly object-oriented, so one might think that it would be written in C++.
Not so, because:
Compile times. EQUINOX-3D is big. The source code is very clean and compact, but it's equivalent to over a million lines of code (it has a lot of functionality). With a C++ compiler, it could take tens of minutes, or an hour to build. This is completely unacceptable and there's no way EQX would have all these features if I had to deal with such soul-crushing / morale-killing compile times.
Some cases in point:
The C++ object model is too rigid, too old and thus lacks a large portion of the powerful object-oriented (OO) concepts that were invented years after its introduction (the term "C++" was first used in 1983).
Complex OO designs can be made more efficient and elegant in C (specifically C99), because it allows you to build your object model from the ground up and think freely, rather than relying on something hard-wired into an old language.
C++ only offers language-level support for fixed (compile-time) inheritance, but dynamic inheritance and object-composition are much more suitable for large, dynamic systems, such as 3D modelers.
EQUINOX-3D is heavily runtime-typed. Plugins can add and deactivate / remove classes and access members of other classes through a general mechanism.
The latter is of course known as "runtime reflection" and most modern object-oriented languages have built-in support for it.
C++ does not.
Those languages that do are either not portable, or are interpreted (too slow for a 3D modeler) and rely on a large "virtual machine".
There have been many attempts to implement runtime reflection in C++, but generic program-level access to class members (name, type, location) is not trivial to implement safely (it involves dynamic_casts (5) and complex/intrusive frameworks).
This is because the memory layout of C++ objects might change (especially when there is multiple-inheritance) and only the runtime knows how.
On the other hand, C99 has just the right level of abstraction, where this can be done cleanly, you can fairly easily add your own object-oriented runtime and it only needs to be done once.
Generic method-chaining is very useful for everything from object serialization to dynamic updates, such as in a GUI widget hierarchy.
Unfortunately, C++ simply hard-wires a degenerate case of it (for constructors and destructors) and it provides no facilities for chaining user-defined methods.
C++ does not allow for "partially-destructed" objects that are aware of their original class, but have been sliced "up" to their superclass (e.g. because the code to manage the class was unloaded). This is very useful for systems with runtime-updateable code (i.e. plugins).
The C++ class management and symbol overloading scheme was clearly not designed with runtime linking/unlinking (plugins) in mind.
As a result, every operating system, even whitin the same genre has it's own hacks for dealing with name spaces and name mangling in DSOs/DLLs.
This is perfectly clear and fairly uniform for C programs.
Many engineers agree that things, like operator and function overloading may be a nice short-hand for the developer, but for other people reading the code, it's more like an "encryption scheme".
C++ is still very immature, decades years after its introduction, there is still no 100% compliant compiler available for it because of the difficulty of implementing one. This makes the language a "moving target" and as a result, many C++ applications completely fail to build when you upgrade your compiler.
A "textbook example" is a C++ project called SFS:
Its download page mentions that you need gcc-2.95.2 or later to compile it. What it does not mention is that the compiler also needs to be older than 3.4.2.
This project has not had an update in a while and C++ compilers keep changing, so it will simply not build any more with the latest compilers.
Basically, there was a "window", or a range of compiler versions that could compile SFS, and that was it.
This unacceptable behavior seems to be the norm. In recent years, I have not seen a single compiler update that didn't break at least one C++ project.
This is a clear indicator of a flawed design (of C++), known as a UFDC (4).
CPU and memory overhead: C++ compilers have come a long way, and C++ is a superset of C, so the infamous slowness and bloat are not directly the fault of the language, as much as the bad programming practices it encourages, but the end result is the same (see numbers above).
Basically, C++ is too old, it tries to do too much and it's not very good at it.
If you like analogies: C++ is like a chainsaw with a fixed, 2-foot long blade, while a large project, such as a 3D modeler is like a tree over 5 feet in diameter.
If you used this "stock" chainsaw, you'd have to cut the middle part that it can't reach, with an axe or a hand-saw (like hacking runtime-reflection onto C++).
Or, you could build your own customizable chainsaw from a kit (C99).
In the latter case, you may spend an extra day putting the tool together, but over the years of your "lumberjack carreer", this is insignificant and you can then adapt the tool to fit the job at hand perfectly, without having to wait for designers (standards commitee) or manufacturers (compiler developers) to upgrade it.
Most developers don't realize how little extra effort it is to build an OO runtime with C99.
Also, if jou just want to trim some small bushes, the 2-foot long chainsaw may be too big and awkward, while your customizable saw could be set for just the right size.
That said, C++ is fine for some small to medium size applications (~ less than 100k lines of code, no plugin interface, no runtime typing), where efficiency is not a major concern.
Needless to say, I use C++ in my day job. It's fine for most of projects involved and I do like some of the "syntax candy".
(3) Valgrind is an excellent memory debugger. It runs programs through a "simulated CPU" and performs memory checks at every relevant machine instruction.
As a result, it runs the program about 50 times slower, but it can catch nearly all memory-related bugs.
(4) UFDC: "unimplementable fuzzy design cloud". A design that was not tested enough with "real world" applications before finalizing it, and is impossible or impractical to implement completely.
(5) Dynamic casting is very useful in runtime-typed systems, yet it is well known how slow it is in C++ (in some implementations it actually does string-comparisons on class names!), while
E_DynamicCast() in E3D only does a few binary equality tests.
© 1996-2022 By Gabor Nagy