With the release of NAP Framework 0.4 came several considerable improvements to the API, the most significant one being the move from OpenGL to Vulkan. Coen's post goes into in great detail about the challenges of rewriting the rendering back-end and the various considerations that had to be made regarding the API design. As someone who is still new to both NAP and Vulkan, I was especially wary of Vulkan's notoriously steep learning curve. Fortunately, NAP 0.4 maintains its same well-documented high-level interface to the rendering back-end and has served as a great introduction to Vulkan for me so far.
Recently, I was asked to port the software for Habitat from NAP 0.3 to 0.4 and make appropriate performance improvements with the new render engine in mind. Habitat is an interactive installation by Heleen Blanken with sound design by Stijn van Beek and software by NAIVI. The work presents large organic environments accompanied by dynamic audio compositions that respond to the movement of visitors on the museum floor. The software is completely data-driven and features an interface that allows the artist to compose lively environments using custom-made assets such as geometric structures, 4k video and high-res images.
As the application is expected to be deployed at several locations, it is essential to keep the hardware requirements relatively low to support as many devices as possible (within reasonable bounds). Additionally, the application includes a built-in editor that serves as an expressive medium for the artist, which calls for fast access to all sorts of data on demand. With the move to NAP 0.4 we hope to improve the software on both aspects by taking advantage of the increased flexibility of the new render engine.
NAP 0.4 retains many of the same naming conventions and high-level logic of its predecessor for rendering. Naturally, this is very fortunate as it made porting Habitat to NAP 0.4 quite straightforward and gave me the opportunity to map my OpenGL knowledge to the new Vulkan way of things. As a an application developer, it was the most important to be aware of the new method by which uniforms are pushed to shaders (in terms of rewriting parts of the application, at least). This is done via Uniform Buffer Objects (UBOs) which require a careful setup of data structures that are accessed in shader programs. An important distinction with OpenGL is that uniform variables are no longer defined in a global namespace and must be accessed from within uniform buffers. For Habitat, this meant uniform data had to be redefined using nap::UniformInstance
structures to be accessible through UBO declarations in all shaders. The shaders had to be modified to access the data as well.
In the previous version of the app, uniform structures were updated using nap::UniformStructInstance::getOrCreateUniform
in every frame. This is fine to do on initialization, but, once created, still requires at least two rtti
casts and several string compares per call to resolve for each uniform. When dealing with about 50 parameters for two different shader programs each, it's better to cache them to avoid this overhead. My modest optimization to this workflow uses a logical struct that stores references to the uniform instances across shaders on initialization. This way, they can be accessed at runtime much faster.
Other port considerations involved reimplementing existing mechanisms to use updated functionality in nap::RenderWindow
and the new nap::Videoplayer
. After fixing some minor incompatibility issues, Habitat was up and running with its render back-end completely moved from OpenGL to Vulkan.
I ran a very simple performance comparison between the two versions in nearly-identical conditions. The only performance benchmark I will be using is average frame time measured over 2000 frames. I ran all of my tests on a mid-tier laptop (from 2017) with an NVIDIA Geforce GTX 1050 and Intel Core i7-7700HQ on Windows 10. The benchmark scene renders a 3D scan that was optimized for rendering through a custom mesh import pipeline and simplified to roughly about 100k vertices. The application dynamically generates a special hair mesh on the surface of the scan mesh on initialization which are then rendered as lines e.g. with GL_LINES
or VK_PRIMITIVE_TOPOLOGY_LINE_LIST
as input assembly states. The shaders implement a simple phong lighting model.
The following two conditions are tested:
Both tests play and map a 4K video to the scan mesh and render at 1920×1080 using 4xMSAA. It must be noted that this is far inferior to the production setting of Habitat, where hair meshes typically consist of at least 4 million hairs rendered at 4K resolution.
Test | NAP version | Frame time (average) | fps (average) |
---|---|---|---|
#1 | 0.3 | 16.2146 ms | 61.67 fps |
0.4 | 10.3451 ms -36% | 96.66 fps | |
#2 | 0.3 | 50.7267 ms | 19.71 fps |
0.4 | 28.5838 ms -43% | 34.98 fps |
The results of the benchmark test show significant performance improvements in the new version of Habitat. In test #2, the frame time reduction of 43% even pulls it over the 30fps threshold, which could be acceptable for low-spec setups. It is really nice to see such an improvement without compromising the visual fidelity of the work. With the new Vulkan back-end, there is abundant room for low-level optimization tailored to the kind of rendering that is required by the application!
I also added some cool new features to Habitat which will make their way to NAP Framework soon. More on that in a future post!
Habitat by Heleen Blanken is currently on view at the Nxt museum in Amsterdam.
NAP Framework is open source software. Suggestions and contributions are welcome.
Lesley van Hoek
Junior Software Engineer Naivi