omni/extras/RtxSettings.h

File members: omni/extras/RtxSettings.h

// Copyright (c) 2020-2023, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto. Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//

#pragma once

#include "../../carb/dictionary/IDictionary.h"
#include "../../carb/logging/Log.h"
#include "../../carb/settings/ISettings.h"
#include "../../carb/settings/SettingsUtils.h"
#include "../../carb/tasking/TaskingUtils.h"
#include "StringHelpers.h"

#include <algorithm>
#include <sstream>
#include <string>
#include <unordered_map>
#include "md5.h"

namespace rtx
{

/*

Usage:

// init
globalOverrides = Settings::createContext();
vp0Context = Settings::createContext();

// update loop
{
    // Frame begin
    // clone global settings
    Settings::cloneToContext(vp0Context, nullptr);
    Settings::getIDictionary()->makeIntAtPath(vp0Context, "path/to/int", 10);
    // ...
    Settings::applyOverridesToContext(vp0Context, globalOverrides);

    // ...

    // will have all the overrides
    Settings::getIDictionary()->getAsInt(vp0Context, "path/to/int");
}

*/

constexpr char kDefaultSettingsPath[] = "/rtx-defaults";
constexpr char kSettingsPath[] = "/rtx";

constexpr char kDefaultPersistentSettingsPath[] = "/persistent-defaults";
constexpr char kPersistentSettingsPath[] = "/persistent";

constexpr char kTransientSettingsPath[] = "/rtx-transient";
constexpr char kFlagsSettingsPath[] = "/rtx-flags";

// A note on internal setting handling.
// Settings in /rtx-transient/internal and /rtx/internal are automatically mapped to entries in /rtx-transient/hashed
// and /rtx/hashed, respectively. We use the MD5 hash of the full internal setting string as the key inside the "hashed"
// scope. See the comments in InternalSettings.h for more details.

constexpr char kInternalSettingKey[] = "internal";
constexpr char kHashedSettingKey[] = "hashed";
constexpr const char* kInternalSettingRoots[] = {
    kSettingsPath,
    kTransientSettingsPath,
};

typedef uint32_t SettingFlags;

constexpr SettingFlags kSettingFlagNone = 0;
constexpr SettingFlags kSettingFlagTransient = 1 << 0;
constexpr SettingFlags kSettingFlagResetDisabled = 1 << 1;
constexpr SettingFlags kSettingFlagDefault = kSettingFlagNone;

constexpr size_t kHashedSettingPrefixMaxSize =
    carb_max(carb::countOf(kSettingsPath), carb::countOf(kTransientSettingsPath)) + 1 + carb::countOf(kHashedSettingKey);
constexpr size_t kHashedSettingCStringMaxLength = kHashedSettingPrefixMaxSize + MD5::kDigestStringSize + 1;

class RenderSettings
{
public:
    struct HashedSettingCString
    {
        char data[kHashedSettingCStringMaxLength];
    };

    static bool isPathRenderSettingsRoot(const char* path)
    {
        size_t rtxSettingsPathLen = CARB_COUNTOF(kSettingsPath) - 1;
        size_t pathLen = strlen(path);
        if (pathLen == rtxSettingsPathLen && strcmp(path, kSettingsPath) == 0)
        {
            return true;
        }

        return false;
    }

    static std::string getAssociatedDefaultsPath(const char* cPath)
    {
        std::string path(cPath);

        size_t pos = path.find_first_of("/", 1);

        std::string parentPath;
        std::string childPath;

        if (pos == std::string::npos)
        {
            parentPath = path;
        }
        else
        {
            parentPath = path.substr(0, pos);
            childPath = path.substr(pos);
        }

        return parentPath + "-defaults" + childPath;
    }

    template <typename KeyType, typename ValueType>
    static void stringToKeyTypeToValueTypeDictionary(const std::string& keyToValueDictStr,
                                                     std::unordered_map<KeyType, ValueType>& outputDict,
                                                     const char* debugStr)
    {
        // Convert from string to a dictionary mapping KeyType values to ValueType values
        //
        // Example syntax: 0:0; 1:1,2; 2:3,4; 3:5; 4:6; 5:6; 6:6; 7;6"
        // Example syntax: "AsphaltStandard:0; AsphaltWeathered:1; ConcreteRough:2"
        // I.e.: use semicolon (';') to separate dictionary entries from each other,
        //       use colon (':') to separate key from value and
        //       use comma (',') to separate entries in a list of values
        std::stringstream ss(keyToValueDictStr);
        static constexpr char kMappingSep = ';';
        static constexpr char kKeyValueSep = ':';

        // Split by ';' into key:value mappings
        std::vector<std::string> keyToValueStrVec = omni::extras::split(keyToValueDictStr, kMappingSep);
        for (auto& keyToValue : keyToValueStrVec)
        {
            // Split a mapping into its key and value
            std::vector<std::string> keyToValueStrVec = omni::extras::split(keyToValue, kKeyValueSep);

            if (keyToValueStrVec.size() == 2) // this is a key,value pair
            {
                // find an appropriate overloaded function to add a key:value pair to the dictionary
                addValueAtKey(keyToValueStrVec[0], keyToValueStrVec[1], outputDict, debugStr);
            }
        }
    }

    static inline bool hasSettingFlags(carb::settings::ISettings& settings, const char* settingPath)
    {
        std::string settingPathStr(kFlagsSettingsPath);
        settingPathStr += settingPath;
        return settings.getItemType(settingPathStr.c_str()) != carb::dictionary::ItemType::eCount;
    }

    static inline SettingFlags getSettingFlags(carb::settings::ISettings& settings, const char* settingPath)
    {
        std::string settingPathStr(kFlagsSettingsPath);
        settingPathStr += settingPath;
        return SettingFlags(settings.getAsInt(settingPathStr.c_str()));
    }

    static inline void setSettingFlags(carb::settings::ISettings& settings, const char* settingPath, SettingFlags flags)
    {
        std::string settingPathStr(kFlagsSettingsPath);
        settingPathStr += settingPath;
        settings.setInt64(settingPathStr.c_str(), flags);
    }

    static inline void removeSettingFlags(carb::settings::ISettings& settings, const char* settingPath)
    {
        std::string settingPathStr(kFlagsSettingsPath);
        settingPathStr += settingPath;
        settings.destroyItem(settingPathStr.c_str());
    }

    template <typename SettingType>
    static void setAndBackupDefaultSetting(const char* settingPath,
                                           SettingType value,
                                           SettingFlags flags = kSettingFlagDefault)
    {
        carb::settings::ISettings* settings = getISettings();
        settings->setDefault<SettingType>(settingPath, value);

        bool allowReset = (flags & (kSettingFlagTransient | kSettingFlagResetDisabled)) == 0;
        if (allowReset)
        {
            std::string defaultSettingPath = getAssociatedDefaultsPath(settingPath);
            if (!defaultSettingPath.empty())
            {
                std::string path(defaultSettingPath);
                if (path.substr(0, std::strlen(kPersistentSettingsPath)) == kPersistentSettingsPath)
                {
                    settings->set<SettingType>(defaultSettingPath.c_str(), value);
                }
                else
                {
                    settings->setDefault<SettingType>(
                        defaultSettingPath.c_str(), settings->get<SettingType>(settingPath));
                }
            }
        }
        // Set per setting flag (persistent, etc.)
        setSettingFlags(*settings, settingPath, flags);
    }

    static inline std::string getInternalSettingString(const char* s)
    {
        return std::string(getHashedSettingString(s).data);
    }

    // TODO: Make private? It looks like this function is just subscribed for change events in
    // setAndBackupPersistentDefaultSetting.
    static void updateSetting(const carb::dictionary::Item* changedItem,
                              carb::dictionary::ChangeEventType eventType,
                              void* userData)
    {
        CARB_UNUSED(eventType);
        if (!changedItem)
        {
            CARB_LOG_ERROR("Unexpected setting deletion");
        }

        const char* path = reinterpret_cast<const char*>(userData);
        carb::dictionary::IDictionary* dictionary = getIDictionary();
        carb::settings::ISettings* settings = getISettings();

        carb::dictionary::ItemType itemType = dictionary->getItemType(changedItem);
        if (itemType == carb::dictionary::ItemType::eString)
        {
            settings->setString(path, dictionary->getStringBuffer(changedItem));
        }
        else if (itemType == carb::dictionary::ItemType::eFloat)
        {
            settings->setFloat(path, dictionary->getAsFloat(changedItem));
        }
        else if (itemType == carb::dictionary::ItemType::eInt)
        {
            settings->setInt(path, dictionary->getAsInt(changedItem));
        }
        else if (itemType == carb::dictionary::ItemType::eBool)
        {
            settings->setBool(path, dictionary->getAsBool(changedItem));
        }
    }

    template <typename SettingType>
    static void setAndBackupPersistentDefaultSetting(const char* settingPath, SettingType value)
    {
        carb::settings::ISettings* settings = getISettings();

        std::string persistentPath = std::string("/persistent") + settingPath;
        settings->setDefault<SettingType>(persistentPath.c_str(), value);

        std::string defaultSettingPath = getAssociatedDefaultsPath(persistentPath.c_str());
        if (!defaultSettingPath.empty())
        {
            std::string path(defaultSettingPath);
            if (path.substr(0, std::strlen(kPersistentSettingsPath)) == kPersistentSettingsPath)
            {
                settings->set<SettingType>(defaultSettingPath.c_str(), value);
            }
            else
            {
                settings->set<SettingType>(
                    defaultSettingPath.c_str(), settings->get<SettingType>(persistentPath.c_str()));
            }
        }

        settings->subscribeToNodeChangeEvents(
            persistentPath.c_str(), RenderSettings::updateSetting, (void*)(settingPath));

        settings->set<SettingType>(settingPath, settings->get<SettingType>(persistentPath.c_str()));
    }

    template <typename SettingArrayType>
    static void setAndBackupDefaultSettingArray(const char* settingPath,
                                                const SettingArrayType* array,
                                                size_t arrayLength,
                                                SettingFlags flags = kSettingFlagDefault)
    {
        carb::settings::ISettings* settings = getISettings();
        settings->setDefaultArray<SettingArrayType>(settingPath, array, arrayLength);

        bool allowReset = (flags & (kSettingFlagTransient | kSettingFlagResetDisabled)) == 0;
        if (allowReset)
        {
            std::string defaultSettingPath = getAssociatedDefaultsPath(settingPath);
            if (!defaultSettingPath.empty())
            {
                settings->destroyItem(defaultSettingPath.c_str());
                size_t arrayLength = settings->getArrayLength(settingPath);
                for (size_t idx = 0; idx < arrayLength; ++idx)
                {
                    std::string elementPath = std::string(settingPath) + "/" + std::to_string(idx);
                    std::string defaultElementPath = std::string(defaultSettingPath) + "/" + std::to_string(idx);
                    settings->setDefault<SettingArrayType>(
                        defaultElementPath.c_str(), settings->get<SettingArrayType>(elementPath.c_str()));
                }
            }
        }
        // Set per setting flag (persistent, etc.)
        setSettingFlags(*settings, settingPath, flags);
    }

    static void resetSettingToDefault(const char* path)
    {
        carb::dictionary::IDictionary* dictionary = getIDictionary();
        carb::settings::ISettings* settings = getISettings();

        std::string defaultsPathStorage;
        defaultsPathStorage = getAssociatedDefaultsPath(path);

        const carb::dictionary::Item* srcItem = settings->getSettingsDictionary(defaultsPathStorage.c_str());
        carb::dictionary::ItemType srcItemType = dictionary->getItemType(srcItem);
        size_t srcItemArrayLength = dictionary->getArrayLength(srcItem);

        if ((srcItemType == carb::dictionary::ItemType::eDictionary) && (srcItemArrayLength == 0))
        {
            resetSectionToDefault(path);
            return;
        }

        switch (srcItemType)
        {
            case carb::dictionary::ItemType::eBool:
            {
                settings->set<bool>(path, dictionary->getAsBool(srcItem));
                break;
            }
            case carb::dictionary::ItemType::eInt:
            {
                settings->set<int32_t>(path, dictionary->getAsInt(srcItem));
                break;
            }
            case carb::dictionary::ItemType::eFloat:
            {
                settings->set<float>(path, dictionary->getAsFloat(srcItem));
                break;
            }
            case carb::dictionary::ItemType::eString:
            {
                settings->set<const char*>(path, dictionary->getStringBuffer(srcItem));
                break;
            }
            case carb::dictionary::ItemType::eDictionary:
            {
                if (srcItemArrayLength > 0)
                {
                    settings->update(path, srcItem, nullptr, carb::dictionary::kUpdateItemOverwriteOriginal, nullptr);
                }
                break;
            }
            default:
                break;
        }
    }

    static void resetSectionToDefault(const char* path)
    {
        carb::settings::ISettings* settings = getISettings();

        std::string defaultsPathStorage;
        const char* defaultsPath = nullptr;

        defaultsPathStorage = getAssociatedDefaultsPath(path);
        if (!defaultsPathStorage.empty())
        {
            defaultsPath = defaultsPathStorage.c_str();
        }

        if (defaultsPath)
        {
            // Do not delete existing original settings.
            // If the source item exists (rtx-default value), overwrite the destination (original rtx value).
            // Otherwise, leave them as-is. We want to keep original values until we find some default values in the
            // future. Plugins may load at a later time and we should not remove original values until we have some
            // default values.
            settings->update(path, settings->getSettingsDictionary(defaultsPath), nullptr,
                             carb::dictionary::kUpdateItemOverwriteOriginal, nullptr);
        }
        else
        {
            CARB_LOG_ERROR("%s: failed to resolve default paths", __func__);
        }
    }

    static carb::dictionary::Item* createContext()
    {
        return getIDictionary()->createItem(nullptr, "<rtx context>", carb::dictionary::ItemType::eDictionary);
    }

    static void destroyContext(carb::dictionary::Item* ctx)
    {
        getIDictionary()->destroyItem(ctx);
    }

    static void cloneToContext(carb::dictionary::Item* dstContext, carb::dictionary::Item* srcContext)
    {
        carb::dictionary::IDictionary* dict = getIDictionary();
        carb::settings::ISettings* settings = getISettings();

        dict->destroyItem(dstContext);
        if (srcContext)
        {
            // TODO: It is undefined behavior to use dstContext after destroyItem above
            dict->update(dstContext, "", srcContext, "", carb::dictionary::kUpdateItemOverwriteOriginal, nullptr);
        }
        else
        {
            dstContext = settings->createDictionaryFromSettings(kSettingsPath);
        }
    }

    static void applyOverridesToContext(carb::dictionary::Item* dstContext, carb::dictionary::Item* overridesContext)
    {
        if (!overridesContext)
        {
            CARB_LOG_ERROR("%s needs context to override from", __FUNCTION__);
        }
        carb::dictionary::IDictionary* dict = getIDictionary();

        dict->update(dstContext, "", overridesContext, "", carb::dictionary::kUpdateItemOverwriteOriginal, nullptr);
    }

    static carb::dictionary::IDictionary* getIDictionary()
    {
        return carb::getCachedInterface<carb::dictionary::IDictionary>();
    }

    static carb::settings::ISettings* getISettings()
    {
        return carb::getCachedInterface<carb::settings::ISettings>();
    }

    static void hashInternalRenderSettings()
    {
        auto settings = getISettings();
        auto hashRtxSetting = [&settings](const char* itemPath, uint32_t, void*) -> uint32_t {
            const carb::dictionary::ItemType itemType = settings->getItemType(itemPath);

            if (itemType == carb::dictionary::ItemType::eDictionary)
            {
                return 0;
            }

            const auto newHashedPath = getHashedSettingString(itemPath);

            switch (settings->getItemType(itemPath))
            {
                case carb::dictionary::ItemType::eBool:
                    settings->setBool(newHashedPath.data, settings->getAsBool(itemPath));
                    break;
                case carb::dictionary::ItemType::eFloat:
                    settings->setFloat(newHashedPath.data, settings->getAsFloat(itemPath));
                    break;
                case carb::dictionary::ItemType::eInt:
                    settings->setInt(newHashedPath.data, settings->getAsInt(itemPath));
                    break;
                case carb::dictionary::ItemType::eString:
                    settings->setString(newHashedPath.data, settings->getStringBuffer(itemPath));
                    break;
                case carb::dictionary::ItemType::eDictionary:
                    CARB_FALLTHROUGH;
                default:
                    CARB_ASSERT(0);
            }

            if (hasSettingFlags(*settings, itemPath))
            {
                setSettingFlags(*settings, newHashedPath.data, getSettingFlags(*settings, itemPath));
                removeSettingFlags(*settings, itemPath);
            }

            return 0;
        };

        carb::dictionary::IDictionary* dictionary = getIDictionary();

        for (const char* root : kInternalSettingRoots)
        {
            std::stringstream ss;
            ss << root << '/' << kInternalSettingKey;
            const std::string internalRoot = ss.str();

            carb::settings::walkSettings(dictionary, settings, carb::dictionary::WalkerMode::eSkipRoot,
                                         internalRoot.c_str(), 0, hashRtxSetting, nullptr);
            settings->destroyItem(internalRoot.c_str());
        }
    }

private:
    // Key : Value = uint32_t : std::vector<uint32_t>
    static void addValueAtKey(std::string key,
                              std::string value,
                              std::unordered_map<uint32_t, std::vector<uint32_t>>& outputDict,
                              const char* debugStr)
    {
        static const char kListElemSep = ',';
        int intKey = 0;
        if (!omni::extras::stringToInteger(key, intKey))
        {
            CARB_LOG_WARN("Non-integer value %s set in \"%s\"", key.c_str(), debugStr);
        }

        // Split the value into a list of values
        std::vector<std::string> intVecStrVec = omni::extras::split(value, kListElemSep);

        int32_t intVecValueEntry = 0;
        outputDict[intKey].clear();
        for (auto& intVecValueEntryStr : intVecStrVec)
        {
            if (omni::extras::stringToInteger(intVecValueEntryStr, intVecValueEntry))
            {
                outputDict[intKey].push_back(intVecValueEntry);
            }
            else
            {
                CARB_LOG_WARN("Non-integer value %s set in \"%s\"", intVecValueEntryStr.c_str(), debugStr);
            }
        }
    }
    // Key : Value = std::string : uint32_t
    static void addValueAtKey(std::string key,
                              std::string value,
                              std::unordered_map<std::string, uint32_t>& outputDict,
                              const char* debugStr)
    {
        int32_t outInt = 0;
        if (omni::extras::stringToInteger(value, outInt))
        {
            outputDict[key] = outInt;
        }
        else
        {
            CARB_LOG_WARN("Non-integer value %s set in \"%s\"", key.c_str(), debugStr);
        }
    }

    static HashedSettingCString getHashedSettingString(const char* settingStr)
    {
        constexpr size_t sizeOfRtxSettingsRoot = carb::countOf(kSettingsPath) - 1;
        const bool isRtxSettingsPath =
            strncmp(settingStr, kSettingsPath, sizeOfRtxSettingsRoot) == 0 && settingStr[sizeOfRtxSettingsRoot] == '/';
        // Default to kTransientSettingsPath for anything outside /rtx/.
        // We don't promise that anything works for paths not rooted in /rtx[-transient]/internal, but implement
        // reasonable behavior rather than crash or map all invalid settings to a null string. Improperly rooted strings
        // will work fine except that hashInternalRenderSettings() won't know to translate them.
        const char* hashedRoot = isRtxSettingsPath ? kSettingsPath : kTransientSettingsPath;

        return getHashedSettingString(
            MD5::getDigestString(MD5::run((const uint8_t*)settingStr, strlen(settingStr))), hashedRoot);
    }

    static HashedSettingCString getHashedSettingString(const MD5::DigestString& digestStr, const char* root)
    {
        HashedSettingCString result = {};

        char* s = std::copy_n(root, strlen(root), result.data);
        *s++ = '/';
        s = std::copy_n(kHashedSettingKey, carb::countOf(kHashedSettingKey) - 1, s);
        *s++ = '/';
        s = std::copy_n(digestStr.s, MD5::kDigestStringSize, s);
        *s = '\0';

        return result;
    }
};

inline bool enableAperture()
{
    // Enabling Aperture mode adds 2 ray types.
    return false;
}

inline bool enableMDLDistilledMtls()
{
    auto&& init = [] {
        carb::settings::ISettings* settings = carb::getCachedInterface<carb::settings::ISettings>();
        return settings->getAsBool("/rtx/mdltranslator/distillMaterial");
    };
    static bool result = init();
    return result;
}

inline bool enableAbiskoMode()
{
    auto&& init = [] {
        carb::settings::ISettings* settings = carb::getCachedInterface<carb::settings::ISettings>();
        const char* renderMode = settings->getStringBuffer("/rtx/rendermode");
        return strcmp(renderMode, "abisko") == 0;
    };
    static bool result = init();
    return result;
}

// Returns how many ray types we are going to use during the whole Kit rendering. This CAN'T be changed once the
// renderer is initialized
inline uint32_t getRayTypeCount()
{
    auto&& init = [] {
        uint32_t rayCount = 2; // MATERIAL_RAY and VISIBILITY_RAY for RTX are always available
        if (!rtx::enableAbiskoMode()) // Abisko only has two ray types
        {
            if (rtx::enableAperture())
            {
                rayCount += 2; // APERTURE_MATERIAL_RAY and APERTURE_VISIBILTY_RAY
            }

            if (rtx::enableMDLDistilledMtls())
            {
                rayCount += 1; // DISTILLED_MATERIAL_RAY
            }
        }
        return rayCount;
    };
    static uint32_t result = init();
    return result;
}

inline bool setDefaultBoolEx(carb::settings::ISettings* settings, const char* path, bool value)
{
    // Unfortunately need a write lock since we might be writing
    carb::dictionary::ScopedWrite writeLock(*carb::getCachedInterface<carb::dictionary::IDictionary>(),
                                            const_cast<carb::dictionary::Item*>(settings->getSettingsDictionary(path)));
    if (settings->getItemType(path) != carb::dictionary::ItemType::eCount)
        return false;
    settings->setBool(path, value);
    return true;
}

} // namespace rtx