MDL SDK API nvidia_logo_transpbg.gif Up
Example Implementation of an MDL Browser
[Previous] [Up] [Next]

This example demonstrates how a material selection dialogue that emphasizes the package and module structure could look like. The mi::neuraylib::IMdl_discovery_api is used to get a holistic view of the modules that are installed on the system, independent of the of individually configured search paths.

New Topics

  • Caching of information on packages, modules, and materials
  • Illustration of how we believe MDL materials should be presented to the user

Detailed Description

Discovering modules and caching of information


When starting the application, the mi::neuraylib::IMdl_discovery_api is used the get the current package and module structure installed on the system. This includes the handling of admin and user space search paths as well as modules inside of MDL Archives. As demonstrated in the Discovery Example, this gives us a holistic tree of mi::neuraylib::IMdl_info objects with basic information on the modules and packages, e.g., simple and qualified name, the search path they have been found in and whether a module is located in an MDL Archive or not. To get information on materials or functions that are exported by individual modules, we need to load the modules and iterate over the exported elements. Since that can take a lot of time for large material libraries or libraries that are for instance stored on network drives, we try to minimize the loading time by storing the gathered information in a persistent cache file that can be loaded much faster.

The MDL browser contains an example implementation of such a cache that follows the MDL API design. Like the result of the mi::neuraylib::IMdl_discovery_api, it is also organized in a tree structure with materials and functions as leaf nodes, while modules and packages are represented by inner nodes. Each node in the tree can save various information in form of key-value-pairs. This allows us to store names, authors, descriptions or other data, mainly gathered from annotations, for each exported material or function. The example also contains an implementation of the IMdl_cache_serializer, which exports the cache to a xml file and imports it back again when the application is started the next time.

In the current implementation, all modules that are not already in the cache, are loaded and analyzed at the first start of the MDL Browser. This can take several seconds up to minutes, depending on the number of modules installed on the system. Modules that are present in the cache are checked for updates. Therefore, we store the file path and the time of the last modification in the cache. In case the module (or archive) has changed, we load and analyze the content again. We also check if the resolved file path provided by the mi::neuraylib::IMdl_discovery_api matches the one stored in the cache to detect if a module originates from a different search path. This can happen due to new shadowing modules or after resolving previously existing shadows.

Usage of the Package Explorer


On the left side of the MDL Browser window there is a Package Explorer that allows to
navigate the package and module structure. Every time the user clicks on a package it is expanded to show its content. At the same time, the main selection pane on the right is filtered to only show materials that are exported by modules of the current navigation level or deeper levels in the hierarchy. This way the user can narrow down the module he or she is looking for. To go up the hierarchy, there is a back button at the left edge of the screen and breadcrumbs at the top of the Package Explorer. Every time the user goes up, the filtering on the right side is updated to again show all materials for the current or deeper levels. Selecting a module in the Package Explorer, illustrated by a piece of a puzzle, the selection pane shows only materials that are exported by that module. Selecting the same module once more, or picking the last breadcrumb, removes this restriction again.

Please note, that always showing all possible materials in the selection pane might not scale arbitrarily. Furthermore, it would make sense to introduce pages, similar to many web shops, or to only show the materials that are exported by the modules of the current level -- and not deeper levels -- of the hierarchy.

Searching for materials based on the cached information


For reasonable large material libraries, the navigation of packages using only the Package Explorer is starting to get a tedious task, especially if the user already knows which material or at least which type of material he or she wants. Therefore, it is necessary to offer a search mechanism that allows to filter the set of materials to select from. Adding multiple search terms allows to further narrow down the list of candidates.

To realize such a search mechanism, a very basic inverse index structure is used. Each material or function acts as document that can be retrieved by the search and all the text information we store in the cache is treated as content of the individual documents. This content is split into single words and each word can be weighted based on the type of information, as a word appearing in the name might be more descriptive than a word in the description or in the author's name. During the construction of the index structure, each document is processed into a list of pairs containing all words and their relevance for the document. The index itself consists of a hash map that is indexed by the words retrieved from all documents. The values in the map are so called posting lists. Here, the document is added if it contains the word that is used as key. The element that is added as a posting list is again a pair. This time, a pointer to the document itself and the relevance of the current word.

The index is constructed at application startup, right after the cache was updated. In contrast to the cache however, the index is not stored persistently even though it would make sense for very large material libraries or more detailed information about the individual elements.

At runtime, the user can search for materials by inserting queries into the search bar at the top of the window. This query is split into single words following the same rules as during the document processing. Then, for each word of the query, a regex matching for all keys in the index structure is computed. In case of a match, the posting list is iterated and the documents along with their relevance are retrieved. If there is more than one search term, only documents that appear in all results are presented to the user and the product of their relevance is used for ranking the selectable materials. Adding a leading minus sign directly in front of a search term allows to negate a search result. Only elements that do not appear in result list for that term are excepted. So, searching for "metal -copper" will retrieve all materials that are made of metal but not of copper.

Presentation of selectable materials


Depending on the user's intention, we support him or her by sorting the results on the selection pane. The example provides three example sorting strategies: sorting by name, by modification date, and by search ranking, while all of them can be sorted ascending and descending. Besides the corresponding buttons below the search bar that can be used to configure the sorting, we automatically sort descending by search ranking when the user hits enter to submit a search query. Since hitting enter is optional, this is only added for convenience.

The selectable elements can be presented in a list or in a grid view. While the latter offers more possible selection candidates at once, the list view displays more information, i.e., the author and the description. However, both presentations provide more data when hovering over a small info icon.

Selecting a material shows a button to confirm the selection, which closes the browser and returns the qualified name back to the application, or in our case, to the console. Double-clicking a material can be used alternatively.

Shadowed modules


The mi::neuraylib::IMdl_discovery_api provides information about modules that are shadowed by modules in other search paths. The MDL Browser informs the user by a red exclamation mark on the module icon in the Package Explorer. Hovering over the icon uncovers the search path of the visible module, the one that can be loaded by the SDK, and the search paths of all modules that are shadows and thereby unavailable.

Running the example from the command line


The MDL Browser can be started with the command line options:

  • -c | --cache_rebuild
    Forces a rebuild of the cache file.
  • -h | --help
    Shows all options with a short explanation.
  • -k | --keep_open
    Reopens the browser after closing the window or selecting a material until the console is closed.
  • -p | --mdl_path <value>
    By default, the MDL Browser uses the two default search paths for the system. On the Windows platform this is the admin-space search path, e.g., C:\ProgramData\NVIDIA Corporation\mdl and the user-space search path %USERPROFILE%\Documents\mdl. By passing the command line option -p <value>, this default behavior can be changed. The option can appear multiple times.
  • -l | --locale <value>
    Enable localization of the material descriptions and other display names. The localization code (see ISO 639-1 standard) has to be provided as <value>. If the option is not set, the system locale is used by default.
  • --no_qt_mode
    Setting this flag illustrates the standalone use-case. For applications that are not based on Qt, a windows in form of a dialog is created. Without the flag, the plugin is used as QML module that provided new MDL controls.

Restrictions


The MDL Browser is a prototype that illustrates certain aspects user experience.

  • At the moment, the example only works on the Windows platform.
  • Only materials can be selected even though the cache already supports functions.

Example Source

The source code consists of a lot of source files in examples/mdl_sdk/mdl_browser. Below we show only the file containing the main() function.

Source Code Location: examples/mdl_sdk/mdl_browser/mdl_browser/mdl_browser.cpp

/******************************************************************************
* Copyright 2023 NVIDIA Corporation. All rights reserved.
*****************************************************************************/
//Defines the entry point for the console application.
// example helper
#include "mdl_sdk_helper.h"
#include "mdl_browser_command_line_options.h"
#include "example_shared.h"
// system
#include <iostream>
// Qt
#include <QtQuick>
#include <QtQuickControls2/QQuickStyle>
#include <QQmlApplicationEngine>
#include <QtSvg>
// MDL QtPlugin
#include <mdl_qt_plugin.h>
// illustrates the use of the mdl qt module in a Qt based application
int run_demo_qt_application(Mdl_qt_plugin_interface* plugin,
Mdl_browser_command_line_options& options)
{
// global Qt Setting that is used for the example
QQuickStyle::setStyle("Material");
#if defined(Q_OS_WIN)
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
// QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); // hangs in remote desktop mode
QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
#endif
// create a Qt application
int argc = 0;
QGuiApplication app(argc, nullptr);
QQmlApplicationEngine engine;
// create and pass the context
Mdl_qt_plugin_context plugin_context(neuray, transaction);
plugin_context.rebuild_module_cache = options.cache_rebuild;
plugin->set_context(&engine, &plugin_context);
// setup callbacks to get the result (not required)
plugin_context.get_mdl_browser_callbacks()->on_accepted = [](const char* value)
{
std::cout << "[info] Selection: " << value << "\n";
};
plugin_context.get_mdl_browser_callbacks()->on_rejected = []()
{
std::cout << "[info] Selection: nothing\n";
};
// load main window
app.setWindowIcon(QIcon(QString(":/mdlqtplugin/graphics/mdl_icon.svg")));
engine.load(":/Main.qml");
// Run the Application
int exit_code = app.exec();
if (exit_code != 0)
std::cerr << "[error] Qt application crashed.\n";
return exit_code;
}
// illustrates the standalone usage of the mdl browser in a non-Qt-based application
int run_non_qt_application(Mdl_qt_plugin_interface* plugin,
Mdl_browser_command_line_options& options)
{
do
{
// create and pass the context for the (next) invocation of the browser
Mdl_qt_plugin_context* plugin_context = new Mdl_qt_plugin_context(neuray, transaction);
plugin_context->rebuild_module_cache = options.cache_rebuild;
Mdl_qt_plguin_browser_handle selection_handle;
plugin->show_select_material_dialog(plugin_context, &selection_handle);
// ...
// do something else in your application
// ...
selection_handle.thread.join();
if (selection_handle.accepted)
{
// (only) output consumed by an application that uses this browser
// instead of implementing an own one
std::cout << selection_handle.result;
}
else
{
std::cout << "[info] nothing selected\n";
}
std::cout << std::flush;
delete plugin_context;
} while (options.keep_open);
exit_success();
}
int MAIN_UTF8(int argc, char* argv[])
{
// parse command line options of the example
Mdl_browser_command_line_options options(argc, argv);
// Access the MDL SDK
// every application that used the MDL SDK does this in some way
mi::base::Handle<mi::neuraylib::INeuray> neuray(load_mdl_sdk(options));
if (!neuray)
{
std::cout << "[MDL Browser] Failed to load the MDL SDK libraries.\n";
return -1;
}
// create a transaction that used for loading modules and building the cache
mi::base::Handle<mi::neuraylib::ITransaction> transaction(create_transaction(neuray.get()));
if (!transaction)
{
std::cout << "[MDL Browser] Failed to create a DB transaction.\n";
return -1;
}
// load and initialize the MDL Qt Plugin
Mdl_qt_plugin_interface* plugin = Mdl_qt_plugin_interface::load(
mi::examples::io::get_executable_folder().c_str());
if (!plugin)
{
std::cout << "[MDL Browser] Failed to load the MdlQtPlugin.\n";
return -1;
}
// depending on the use-case, the browser can be used from the as standalone windows in case
// of an application that is not using Qt so far. If the application is based on Qt, the
// browser can be included from the QML module.
int exit_code = -1;
if (options.no_qt_mode)
exit_code = run_non_qt_application(plugin, neuray.get(), transaction.get(), options);
else
exit_code = run_demo_qt_application(plugin, neuray.get(), transaction.get(), options);
// unload the plugin
plugin->unload();
// close the MDL SDK
transaction->commit();
transaction = nullptr;
// Shut down the MDL SDK
if (neuray->shutdown() != 0)
std::cerr << "Failed to shutdown the SDK.\n";
// Unload the MDL SDK
neuray = nullptr;
if (!mi::examples::mdl::unload())
std::cerr << "Failed to unload the SDK.\n";
return exit_code;
}
COMMANDLINE_TO_UTF8
Handle class template for interfaces, automatizing the lifetime control via reference counting.
Definition: handle.h:113
This is an object representing the MDL SDK library.
Definition: ineuray.h:44
virtual Sint32 shutdown(bool blocking=true)=0
Shuts down the library.
A transaction provides a consistent view on the database.
Definition: itransaction.h:81
virtual Sint32 commit()=0
Commits the transaction.
[Previous] [Up] [Next]