Trusted Software Excellence across Desktop and Embedded
Take a glance at the areas of expertise where KDAB excels ranging from swift troubleshooting, ongoing consulting and training to multi-year, large-scale software development projects.
Find out why customers from innovative industries rely on our extensive expertise, including Medical, Biotech, Science, Renewable Energy, Transportation, Mobility, Aviation, Automation, Electronics, Agriculture and Defense.
High-quality Embedded Engineering across the Stack
To successfully develop an embedded device that meets your expectations regarding quality, budget and time to market, all parts of the project need to fit perfectly together.
Learn more about KDAB's expertise in embedded software development.
Where the capabilities of modern mobile devices or web browsers fall short, KDAB engineers help you expertly architect and build high-functioning desktop and workstation applications.
Extensible, Safety-compliant Software for the Medical Sector
Create intelligent, patient-focused medical software and devices and stay ahead with technology that adapts to your needs.
KDAB offers you expertise in developing a broad spectrum of clinical and home-healthcare devices, including but not limited to, internal imaging systems, robotic surgery devices, ventilators and non-invasive monitoring systems.
Building digital dashboards and cockpits with fluid animations and gesture-controlled touchscreens is a big challenge.
In over two decades of developing intricate UI solutions for cars, trucks, tractors, scooters, ships, airplanes and more, the KDAB team has gained market leading expertise in this realm.
Build on Advanced Expertise when creating Modern UIs
KDAB assists you in the creation of user-friendly interfaces designed specifically for industrial process control, manufacturing, and fabrication.
Our specialties encompass the custom design and development of HMIs, enabling product accessibility from embedded systems, remote desktops, and mobile devices on the move.
Legacy software is a growing but often ignored problem across all industries. KDAB helps you elevate your aging code base to meet the dynamic needs of the future.
Whether you want to migrate from an old to a modern GUI toolkit, update to a more recent version, or modernize your code base, you can rely on over 25 years of modernization experience.
KDAB offers a wide range of services to address your software needs including consulting, development, workshops and training tailored to your requirements.
Our expertise spans cross-platform desktop, embedded and 3D application development, using the proven technologies for the job.
When working with KDAB, the first-ever Qt consultancy, you benefit from a deep understanding of Qt internals, that allows us to provide effective solutions, irrespective of the depth or scale of your Qt project.
Qt Services include developing applications, building runtimes, mixing native and web technologies, solving performance issues, and porting problems.
KDAB helps create commercial, scientific or industrial desktop applications from scratch, or update its code or framework to benefit from modern features.
Discover clean, efficient solutions that precisely meet your requirements.
Boost your team's programming skills with in-depth, constantly updated, hands-on training courses delivered by active software engineers who love to teach and share their knowledge.
Our courses cover Modern C++, Qt/QML, Rust, 3D programming, Debugging, Profiling and more.
The collective expertise of KDAB's engineering team is at your disposal to help you choose the software stack for your project or master domain-specific challenges.
Our particular focus is on software technologies you use for cross-platform applications or for embedded devices.
Since 1999, KDAB has been the largest independent Qt consultancy worldwide and today is a Qt Platinum partner. Our experts can help you with any aspect of software development with Qt and QML.
KDAB specializes in Modern C++ development, with a focus on desktop applications, GUI, embedded software, and operating systems.
Our experts are industry-recognized contributors and trainers, leveraging C++'s power and relevance across these domains to deliver high-quality software solutions.
KDAB can guide you incorporating Rust into your project, from as overlapping element to your existing C++ codebase to a complete replacement of your legacy code.
Unique Expertise for Desktop and Embedded Platforms
Whether you are using Linux, Windows, MacOS, Android, iOS or real-time OS, KDAB helps you create performance optimized applications on your preferred platform.
If you are planning to create projects with Slint, a lightweight alternative to standard GUI frameworks especially on low-end hardware, you can rely on the expertise of KDAB being one of the earliest adopters and official service partner of Slint.
KDAB has deep expertise in embedded systems, which coupled with Flutter proficiency, allows us to provide comprehensive support throughout the software development lifecycle.
Our engineers are constantly contributing to the Flutter ecosystem, for example by developing flutter-pi, one of the most used embedders.
KDAB invests significant time in exploring new software technologies to maintain its position as software authority. Benefit from this research and incorporate it eventually into your own project.
Start here to browse infos on the KDAB website(s) and take advantage of useful developer resources like blogs, publications and videos about Qt, C++, Rust, 3D technologies like OpenGL and Vulkan, the KDAB developer tools and more.
The KDAB Youtube channel has become a go-to source for developers looking for high-quality tutorial and information material around software development with Qt/QML, C++, Rust and other technologies.
Click to navigate the all KDAB videos directly on this website.
In over 25 years KDAB has served hundreds of customers from various industries, many of them having become long-term customers who value our unique expertise and dedication.
Learn more about KDAB as a company, understand why we are considered a trusted partner by many and explore project examples in which we have proven to be the right supplier.
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications.
Read more about the history, the values, the team and the founder of the company.
When working with KDAB you can expect quality software and the desired business outcomes thanks to decades of experience gathered in hundreds of projects of different sizes in various industries.
Have a look at selected examples where KDAB has helped customers to succeed with their projects.
KDAB is committed to developing high-quality and high-performance software, and helping other developers deliver to the same high standards.
We create software with pride to improve your engineering and your business, making your products more resilient and maintainable with better performance.
KDAB has been the first certified Qt consulting and software development company in the world, and continues to deliver quality processes that meet or exceed the highest expectations.
In KDAB we value practical software development experience and skills higher than academic degrees. We strive to ensure equal treatment of all our employees regardless of age, ethnicity, gender, sexual orientation, nationality.
Interested? Read more about working at KDAB and how to apply for a job in software engineering or business administration.
One particular facet of modern graphics development that is often a pain - even for AAA games -- is shader variants!
If you have bought an AAA game in recent years and wondered what the heck it is doing when it says it is compiling shaders for a long time (up to an hour or more for some recent PC titles on slower machines!), then this blog will explain it a little.
Modern graphics APIs (Vulkan, D3D12, Metal) like to know about everything that has to do with GPU state, up front. A large chunk of the GPU state is provided by so-called shader programs. These shader programs fill in various gaps in the graphics pipeline that used to be provided by fixed-function hardware back in the days of OpenGL 1.x.
As OpenGL (and DirectX) evolved, people wanted to do a wider range of things when processing vertices into colorful pixels on-screen. So, over time, the fixed function silicon on GPUs has gradually been replaced by more and more general purpose processors. As with CPUs, we now need to tell these processors what to do by writing small (sometimes larger), specialized programs called shader programs.
In OpenGL, we would write our shaders in the high-level GLSL language and feed that to the OpenGL driver as a string at runtime. The OpenGL driver would then compile the GLSL to GPU machine code and we could then throw big piles of vertices and other resources like textures at it and marvel at the results -- or, more likely, swear a bit and wonder why we are staring at a black window yet again.
The necessity of including a complete compiler in the graphics driver was a huge burden for each of the GPU vendors, resulting in a great deal of overhead for them. It also led to some strange problems for developers when running code on a new platform with a different GLSL compiler in the driver and hitting new and different bugs or shortcomings.
With the advent of modern graphics APIs, there has been a move toward consuming shader code in the form of a bytecode intermediate representation, such as SPIR-V. SPIR-V is still not the final form of executable code required by the GPU silicon but it is much closer to it than GLSL and means the Vulkan drivers no longer need the entire compiler front-end.
Tooling, such as nSight and RenderDoc, are able to decompile the SPIR-V shader code back to GLSL (or HLSL) to make it easier for you to debug your applications.
The conversion from GLSL (or any other suitable language) to SPIR-V can still happen at runtime if that's what you need -- for example, in dynamic editor tools. However, for constrained applications, we can now compile the GLSL to SPIR-V up front at build time.
That's nice! We can simply add a few targets to our CMakeLists.txt and go home, right? Well, not quite.
The Need for Shader Variants
You see, shader developers are just as lazy as any other kinds of developers and like to reduce the amount of copy/paste coding that we have to do. So, we add optional features to our shaders that can be compiled in or out by way of pre-processor #defines, just as with C/C++.
Why is this even needed, though? Well, we don't always have full control over the data that our application will be fed. Imagine a generic glTF file viewer application. Some models that get loaded will use textures for the materials and include texture coordinates in the model's vertex data. Other models may just use vertex colors, completely leaving out texture coordinates.
To handle this, our vertex shader's prologue may look something like this:
The fragment shader would have similar changes to handle the cases with and without texture coordinates.
Super, so we have one set of shader source files that can handle both models with textures and models without textures. How do we compile the shaders to get these shader variants?
Just as with C/C++ we have a compiler toolchain and, similarly, we invoke the compiler with the various -D options as needed, e.g.:
glslangValidator -o material-with-uvs.vert.spirv -DTEXCOORD_0_ENABLED material.vert # With texture coordsglslangValidator -o material-without-uvs.vert.spirv material.vert # Without texture coords
Then, within our application, we can load the glTF model, inspect its data to see whether it uses textures, and then load the appropriate SPIR-V compiled shader.
Hooray! The job is done and we can go home now, right? Well, actually, no -- the project manager just called to say we also need to handle models that include the alpha cut-off feature and models that don't include it.
Alpha cut-off is a feature of glTF files by which any pixels determined to have an alpha value less than some specified threshold simply get discarded. This is often used to cut away the transparent parts of quads used to render leaves of plants.
Ok then -- let's simply repeat a process similar to that which we did for handling the presence, or absence, of texture coordinates.
The fragment shader implementation of alpha cut-off is trivial:
We can then add suitable CMake targets to compile with and without this option.
Of course, there's a catch. We have a combinatorial explosion of feature combinations. This only gets worse when we add the next optional feature or optional features that have various settings we wish to set at compile time, such as the number of taps used when sampling from a texture to perform a Gaussian blur.
Clearly, we do not want to have to add several thousand combinations of features as CMake targets by hand! So, what can we do?
Exploring the Problem
Let's consider the above combination of the texture coordinates and alpha cut-off features. Our table of features and compiler flags looks like this:
Tex Coord Off
Tex Coord On
Alpha Cut-off Off
-DTEXCOORD_0_ENABLED
Alpha Cut-off On
-DALPHA_CUTOFF_ENABLED
-DTEXCOORD_0_ENABLED -DALPHA_CUTOFF_ENABLED
Adding another option would add another dimension to this table. The above mentioned option of blur filter taps with, say, 3, 5, 7, or 9 taps would add a 3rd dimension to the table and increase the number of options by another factor of 4, for a total of 16 possible configurations of this one shader program.
Adding just a handful of features, we can see that it would be all too easy to end up with thousands of combinations of compiled shaders from the single set of GLSL files!
How can we solve this in a nice and extensible way?
It is easy enough to have nested loops to iterate over the available options for each of the specified axes of variations. But what if we don't know all of the axes of variation up front? What if they vary from shader to shader? Not all shaders will care about alpha cut-off or blur filter taps, for example.
We can't simply hard-wire a set number of nested loops to iterate over the combinations in our CMake files. We need something a bit more flexible and smarter.
Let's think about the problem in a slightly different way.
To start with, let's represent a given configuration of our option space by a vector of length N, where N is the number of options. For now, let's set this to 3, for our options we have discussed:
Texture Coordinates (Off or On)
Alpha Cut-off (Off or On)
Blur filter taps (3, 5, 7, or 9)
That is, we will have a vector like this:
[TexCoords Off, Alpha Cut-off Off, blur taps =3]
To save some typing, let's now replace the wordy description of each element with a number representing the index of the option for that axis of variation:
With this scheme in place, our above option set will be:
[0, 0, 0]
And the vector representing texture coordinates on, no alpha cut-off, and 7 blur filter taps option will be:
[1, 0, 2]
How does this help us? Well, it allows us to succinctly represent any combination of options; but it's even better than that. We can now easily go through the list of all possible combinations in a logical order. We begin by incrementing the final element of the vector over all possible values. Then we increment the previous element and repeat, like this:
Note that the total number of option combinations is just the product of the number of options in each dimension or axis of variation, e.g. 2x2x4 = 16 in this example.
The above sequence is exactly what we would get if we had 3 nested for-loops to iterate over the options at each level. How does this help us?
Well, looking at the above sequence of options vectors, you may well notice the similarity to plain old counting of numbers. For each "decimal place" (element in the vector), starting with the final or least significant digit, we go up through each of the available values. Then, we increment the next least significant digit and repeat.
The only difference to how we are used to counting in decimal (base 10), binary, octal, or hexadecimal is that the base of each digit is potentially different. The base for each digit is simply the number of options available for that axis of variation (e.g. the texture coordinates can only be on or off (base = 2)). It's the same for the alpha cut-off. The blur taps option has a base of 4 (4 possible options).
We know how many combinations we need in total and we know that each combination can be represented by a vector that acts like a variable-base number. Therefore, if we can find a way to convert from a decimal number to the corresponding combination vector, we are in a good situation, as we will have converted a recursive approach (nested for-loops) into a flat linear approach. All we would need would be something like this pseudo-code:
for i =0 to combination_count
option_vector =calculate_option_vector(i)output_compiler_options(option_vector)next i
All we have to do, in our case, is use a base that differs for each digit of our combination vector. However, before we show this, we need a way to specify the options for each shader that we wish to consider. We have done this by way of a simple JSON file, for now. Here is an example showing our above case for these options as applied to the fragment shader, but only the texture coordinates and alpha cut-off for the vertex shader. This is just an example for illustration. In reality, the vertex shader has nothing to do with alpha cut-off and our simple shaders do not do anything with the blur tap option at all:
The default in our system, if no explicit options are provided in the JSON file, is defined (on) or not defined (off).
Each input shader file section then specifies which of the options it cares about. So, in this example, the fragment shader considers all 3 options and will have 16 variants compiled.
In order to generate the possible build combinations, we have written a small Ruby script to implement the necessary logic. Why Ruby? Because I couldn't face trying to do the necessary math in CMake's scripting language and Ruby is lovely!
The core of the script that implements the decimal to a variable-base number (combination vector) is pretty simple:
defcalculate_digits(bases, index) digits =Array.new(bases.size,0) base_index = digits.size -1 current_value = index
while current_value !=0 quotient, remainder = current_value.divmod(bases[base_index]) digits[base_index]= remainder
current_value = quotient
base_index -=1endreturn digits
end
In the above code, the bases argument is a vector representing the base of each digit in the final combination vector. Here, bases = [2, 2, 4]. We then loop over the decimal number, performing the divmod operation at each step to find the value of each digit in our combination vector. When we have reduced the input decimal number to 0, we are done. This is exactly analogous to the decimal to binary conversion linked above but for variable base at each digit.
With the resulting combination vector in hand, it is simple for us to then look up the corresponding compiler -D option for that selection and output that into a JSON string. Here is an example of the output of running the ruby script against the above configuration file:
require'json'require'pp'defexpand_options(data)# Expand the options so that if no explicit options are specified we default# to options where the #define symbole is defined or not data[:options].eachdo|option|if!option.has_key?(:values) option[:values]=[:nil,:defined]end option[:count]= option[:values].size
endenddefextract_options(data, shader) shader_options =Hash.new shader_options[:options]=Array.new shader[:options].eachdo|option_index| shader_options[:options].push data[:options][option_index]end# STDERR.puts "Options for shader:"# STDERR.puts shader_optionsreturn shader_options
enddeffind_bases(data) bases =Array.new(data[:options].size)(0..(data[:options].size -1)).eachdo|index| bases[index]= data[:options][index][:count]endreturn bases
enddefcalculate_steps(bases) step_count = bases[0](1..(bases.size -1)).eachdo|index| step_count *= bases[index]endreturn step_count
end# Calculate the number for "index" in our variable-bases counting systemdefcalculate_digits(bases, index) digits =Array.new(bases.size,0) base_index = digits.size -1 current_value = index
while current_value !=0 quotient, remainder = current_value.divmod(bases[base_index]) digits[base_index]= remainder
current_value = quotient
base_index -=1endreturn digits
enddefbuild_options_string(data, selected_options) str ="" selected_options.each_with_index do|selected_option, index|# Don't add anything if option is disablednextif selected_option ==:nil# If we have the special :defined option, then we add a -D optionif selected_option ==:defined str +=" -D#{data[:options][index][:define]}"else str +=" -D#{data[:options][index][:define]}=#{selected_option}"endendreturn str.strip
enddefbuild_filename(shader, data, selected_options) str =File.basename(shader[:filename],File.extname(shader[:filename])) selected_options.each_with_index do|selected_option, index|# Don't add anything if option is disablednextif selected_option ==:nil# If we have the special :defined option, then we add a section for that optionif selected_option ==:defined str +="_#{data[:options][index][:define].downcase}"else str +="_#{data[:options][index][:define].downcase}_#{selected_option.to_s}"endend str +=File.extname(shader[:filename])+".spv"return str
end# Load the configuration data and expand default optionsifARGV.size !=1 puts "No filename specified." puts " Usage: generate_shader_variants.rb " exit(1)endvariants_filename =ARGV[0]file =File.read(variants_filename)data =JSON.parse(file,{symbolize_names:true})expand_options(data)# Prepare a hash to output as json at the endoutput_data =Hash.newoutput_data[:variants]=Array.newdata[:shaders].eachdo|shader|# STDERR.puts "Processing #{shader[:filename]}"# Copy over the options referenced by this shader to a local hash that we can operate on shader_options = extract_options(data, shader)# Create a "digits" array we can use for counting. Each element (digit) in the array# will correspond to an option in the loaded data configuration. The values each# digit can take are those specified in the "values" array for that option.## The number of steps we need to take to count from "0" to the maximum value is the# product of the number of options for each "digit" (option). bases = find_bases(shader_options)# STDERR.puts "Bases = #{bases}" step_count = calculate_steps(bases)# STDERR.puts "There are #{step_count} combinations of options"# Count up through out range of options(0..(step_count -1)).eachdo|index| digits = calculate_digits(bases, index) selected_options =Array.new(bases.size)(0..(bases.size -1)).eachdo|digit_index| settings = data[:options][digit_index] setting_index = digits[digit_index] selected_options[digit_index]= settings[:values][setting_index]end# Construct the options to pass to glslangValidator defines = build_options_string(shader_options, selected_options) output_filename = build_filename(shader, shader_options, selected_options)# STDERR.puts " Step #{index}: #{digits}, selected_options = #{selected_options}, defines = #{defines}, output_filename = #{output_filename}" variant ={input: shader[:filename],defines: defines,output: output_filename } output_data[:variants].push variant
end# STDERR.puts ""endputs output_data.to_json
Integrating into the Build System
CMake is now able to read and parse JSON documents -- a fact that I didn't know at first. This means that we can quite conveniently ask our build system to execute our Ruby script as an external process at configure time, capture the JSON output as shown above, iterate over the generated combinations, and add a build target for each one.
The cut-down code for doing this is:
function(CompileShaderVariants target variants_filename)# Run the helper script to generate json data for all configured shader variantsexecute_process( COMMAND ruby ${CMAKE_SOURCE_DIR}/generate_shader_variants.rb ${variants_filename}WORKING_DIRECTORY${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE SHADER_VARIANTS
RESULT_VARIABLE SHADER_VARIANT_RESULT
)if(NOT SHADER_VARIANT_RESULT EQUAL"0")message(NOTICE ${SHADER_VARIANT_RESULT})message(FATAL_ERROR "Failed to generate shader variant build targets for "${variants_filename})endif()string(JSON VARIANT_COUNT LENGTH ${SHADER_VARIANTS} variants)message(NOTICE "Generating "${VARIANT_COUNT}" shader variants from "${variants_filename})# Adjust count as loop index goes from 0 to NMATH(EXPR VARIANT_COUNT "${VARIANT_COUNT} - 1")foreach(VARIANT_INDEX RANGE ${VARIANT_COUNT})string(JSON CURRENT_INTPUT_FILENAME GET ${SHADER_VARIANTS} variants ${VARIANT_INDEX} input)string(JSON CURRENT_OUTPUT_FILENAME GET ${SHADER_VARIANTS} variants ${VARIANT_INDEX} output)string(JSON CURRENT_DEFINES GET ${SHADER_VARIANTS} variants ${VARIANT_INDEX} defines)set(SHADER_TARGET_NAME "${target}_${CURRENT_OUTPUT_FILENAME}")CompileShader(${SHADER_TARGET_NAME}${CURRENT_INTPUT_FILENAME}${CURRENT_OUTPUT_FILENAME}${CURRENT_DEFINES})endforeach(VARIANT_INDEX RANGE ${VARIANT_COUNT})endfunction()
Here, CompileShader() call is another helper function that just invokes the glslangValidator GLSL->SPIR-V compiler with the specified options.
This nicely takes care of generating all of the required shader variants that will be compiled with correct dependencies on the source GLSL files. To ensure that the targets get updated if the input JSON configuration file changes, we can add the following snippet to the above function:
# Re-run cmake configure step if the variants file changesset_property( DIRECTORY
APPEND
PROPERTY CMAKE_CONFIGURE_DEPENDS${variants_filename})
Now, if we edit the JSON configuration file that contains the options, CMake will automatically re-run and generate the targets.
On the C++ runtime side of things, we have some logic to construct the appropriate shader file name for the compiled SPIR-V shader matching the options needed by whatever model we are rendering.
In the future, we may make this part more reusable by making it read in the same JSON configuration file used to create the shader variants.
Wrapping Up
So, going back to where we started: how does all of this tie into your PC's spending an hour compiling shaders when we have shown here how to compile them at application build time?
It all goes back to SPIR-V's just being a bytecode intermediate representation. Before the GPU can execute these shaders, it needs to do a final compilation step to convert the SPIR-V to actual machine code. In a modern graphics API, this is done when we create a so-called "graphics pipeline." At this point, we have to specify pretty much all GPU state, which then gets baked into a binary blob along with the shader code by the driver. This binary blob is both GPU-vendor and driver-version specific. So, it cannot be built at application build time but, rather, has to be done on the actual machine on which it will execute.
The first time you run such a game or other application, it will often loop through all of the shader variants and compile a graphics pipeline for each one. These then get cached to disk for use on subsequent runs. If you change your GPU or (more likely) the driver version, then this cache might get invalidated and you'd have to sit through this process once again.
For systems with known hardware and drivers, this whole process can be performed as part of the build step. This is why consoles such as the PlayStation 5 do not have to do this lengthy shader compiling step, while we wait there and watch.
There is some work going on in Khronos at present, in the shape of VK_ext_shader_object, to try to get back to a more dynamic-shader friendly way of doing things, in which the driver takes care of much of this compiling and caching for us. As with all things in computer science though, it will be a trade-off.
Thank you for reading about what turned out to be a nice little excursion of simplifying a problem by changing it from recursive to linear and learning about converting between numbers of different bases.
If you would like to learn more about modern 3D graphics or get some help on your own projects, then please get in touch.
Dr Sean Harmer is a senior software engineer at KDAB where he heads up our UK office and also leads the 3D R&D team. He has been developing with C++ and Qt since 1998 and is Qt 3D Maintainer and lead developer in the Qt Project. Sean has broad experience and a keen interest in scientific visualization and animation in OpenGL and Qt. He holds a PhD in Astrophysics along with a Masters in Mathematics and Astrophysics.
Our hands-on Modern C++ training courses are designed to quickly familiarize newcomers with the language. They also update professional C++ developers on the latest changes in the language and standard library introduced in recent C++ editions.
1 Comment
27 - Apr - 2023
Andy Maloney
Thanks for the great writeup Sean!