diff options
Diffstat (limited to 'indra/newview/llgltfmateriallist.cpp')
-rw-r--r-- | indra/newview/llgltfmateriallist.cpp | 789 |
1 files changed, 789 insertions, 0 deletions
diff --git a/indra/newview/llgltfmateriallist.cpp b/indra/newview/llgltfmateriallist.cpp new file mode 100644 index 0000000000..9c78e48cab --- /dev/null +++ b/indra/newview/llgltfmateriallist.cpp @@ -0,0 +1,789 @@ +/** + * @file llgltfmateriallist.cpp + * + * $LicenseInfo:firstyear=2022&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2022, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llgltfmateriallist.h" + +#include "llagent.h" +#include "llassetstorage.h" +#include "lldispatcher.h" +#include "llfetchedgltfmaterial.h" +#include "llfilesystem.h" +#include "llsdserialize.h" +#include "lltinygltfhelper.h" +#include "llviewercontrol.h" +#include "llviewergenericmessage.h" +#include "llviewerobjectlist.h" +#include "llviewerregion.h" +#include "llviewerstats.h" +#include "llcorehttputil.h" +#include "llagent.h" +#include "llvocache.h" +#include "llworld.h" + +#include "tinygltf/tiny_gltf.h" +#include <strstream> + +#include "json/reader.h" +#include "json/value.h" + +#include <unordered_set> + +LLGLTFMaterialList gGLTFMaterialList; + +LLGLTFMaterialList::modify_queue_t LLGLTFMaterialList::sModifyQueue; +LLGLTFMaterialList::apply_queue_t LLGLTFMaterialList::sApplyQueue; +LLSD LLGLTFMaterialList::sUpdates; + +const LLUUID LLGLTFMaterialList::BLANK_MATERIAL_ASSET_ID("968cbad0-4dad-d64e-71b5-72bf13ad051a"); + +#ifdef SHOW_ASSERT +// return true if given data is (probably) valid update message for ModifyMaterialParams capability +static bool is_valid_update(const LLSD& data) +{ + llassert(data.isMap()); + + U32 count = 0; + + if (data.has("object_id")) + { + if (!data["object_id"].isUUID()) + { + LL_WARNS() << "object_id is not a UUID" << LL_ENDL; + return false; + } + ++count; + } + else + { + LL_WARNS() << "Missing required parameter: object_id" << LL_ENDL; + return false; + } + + if (data.has("side")) + { + if (!data["side"].isInteger()) + { + LL_WARNS() << "side is not an integer" << LL_ENDL; + return false; + } + + if (data["side"].asInteger() < -1) + { + LL_WARNS() << "side is invalid" << LL_ENDL; + } + ++count; + } + else + { + LL_WARNS() << "Missing required parameter: side" << LL_ENDL; + return false; + } + + if (data.has("gltf_json")) + { + if (!data["gltf_json"].isString()) + { + LL_WARNS() << "gltf_json is not a string" << LL_ENDL; + return false; + } + ++count; + } + + if (data.has("asset_id")) + { + if (!data["asset_id"].isUUID()) + { + LL_WARNS() << "asset_id is not a UUID" << LL_ENDL; + return false; + } + ++count; + } + + if (count < 3) + { + LL_WARNS() << "Only specified object_id and side, update won't actually change anything and is just noise" << LL_ENDL; + return false; + } + + if (data.size() != count) + { + LL_WARNS() << "update data contains unrecognized parameters" << LL_ENDL; + return false; + } + + return true; +} +#endif + +class LLGLTFMaterialOverrideDispatchHandler : public LLDispatchHandler +{ + LOG_CLASS(LLGLTFMaterialOverrideDispatchHandler); +public: + LLGLTFMaterialOverrideDispatchHandler() = default; + ~LLGLTFMaterialOverrideDispatchHandler() override = default; + + void addCallback(void(*callback)(const LLUUID& object_id, S32 side)) + { + mSelectionCallbacks.push_back(callback); + } + + bool operator()(const LLDispatcher* dispatcher, const std::string& key, const LLUUID& invoice, const sparam_t& strings) override + { + LL_PROFILE_ZONE_SCOPED; + // receive override data from simulator via LargeGenericMessage + // message should have: + // object_id - UUID of LLViewerObject + // sides - array of S32 indices of texture entries + // gltf_json - array of corresponding Strings of GLTF json for override data + + + LLSD message; + + sparam_t::const_iterator it = strings.begin(); + if (it != strings.end()) + { + const std::string& llsdRaw = *it++; + std::istringstream llsdData(llsdRaw); + if (!LLSDSerialize::deserialize(message, llsdData, llsdRaw.length())) + { + LL_WARNS() << "LLGLTFMaterialOverrideDispatchHandler: Attempted to read parameter data into LLSD but failed:" << llsdRaw << LL_ENDL; + } + } + else + { + // malformed message, nothing we can do to handle it + LL_DEBUGS("GLTF") << "Empty message" << LL_ENDL; + return false; + } + + LLGLTFOverrideCacheEntry object_override; + if (!object_override.fromLLSD(message)) + { + // malformed message, nothing we can do to handle it + LL_DEBUGS("GLTF") << "Message without id:" << message << LL_ENDL; + return false; + } + + // Cache the data + { + LL_DEBUGS("GLTF") << "material overrides cache" << LL_ENDL; + + // default to main region if message doesn't specify + LLViewerRegion * region = gAgent.getRegion();; + + if (object_override.mHasRegionHandle) + { + // TODO start requiring this once server sends this for all messages + region = LLWorld::instance().getRegionFromHandle(object_override.mRegionHandle); + } + + if (region) + { + region->cacheFullUpdateGLTFOverride(object_override); + } + else + { + LL_WARNS("GLTF") << "could not access region for material overrides message cache, region_handle: " << LL_ENDL; + } + } + applyData(object_override); + return true; + } + + void doSelectionCallbacks(const LLUUID& object_id, S32 side) + { + for (auto& callback : mSelectionCallbacks) + { + callback(object_id, side); + } + } + + void applyData(const LLGLTFOverrideCacheEntry &object_override) + { + // Parse the data + + LL::WorkQueue::ptr_t main_queue = LL::WorkQueue::getInstance("mainloop"); + LL::WorkQueue::ptr_t general_queue = LL::WorkQueue::getInstance("General"); + + struct ReturnData + { + public: + LLPointer<LLGLTFMaterial> mMaterial; + S32 mSide; + bool mSuccess; + }; + + // fromJson() is performance heavy offload to a thread. + main_queue->postTo( + general_queue, + [object_override]() // Work done on general queue + { + std::vector<ReturnData> results; + + if (!object_override.mSides.empty()) + { + results.reserve(object_override.mSides.size()); + // parse json + std::map<S32, std::string>::const_iterator iter = object_override.mSides.begin(); + std::map<S32, std::string>::const_iterator end = object_override.mSides.end(); + while (iter != end) + { + LLPointer<LLGLTFMaterial> override_data = new LLGLTFMaterial(); + std::string warn_msg, error_msg; + + bool success = override_data->fromJSON(iter->second, warn_msg, error_msg); + + ReturnData result; + result.mSuccess = success; + result.mSide = iter->first; + + if (success) + { + result.mMaterial = override_data; + } + else + { + LL_WARNS("GLTF") << "failed to parse GLTF override data. errors: " << error_msg << " | warnings: " << warn_msg << LL_ENDL; + } + + results.push_back(result); + iter++; + } + } + return results; + }, + [object_override, this](std::vector<ReturnData> results) // Callback to main thread + { + + LLViewerObject * obj = gObjectList.findObject(object_override.mObjectId); + + if (results.size() > 0 ) + { + std::unordered_set<S32> side_set; + + for (int i = 0; i < results.size(); ++i) + { + if (results[i].mSuccess) + { + // flag this side to not be nulled out later + side_set.insert(results[i].mSide); + + if (!obj || !obj->setTEGLTFMaterialOverride(results[i].mSide, results[i].mMaterial)) + { + // object not ready to receive override data, queue for later + gGLTFMaterialList.queueOverrideUpdate(object_override.mObjectId, results[i].mSide, results[i].mMaterial); + } + else if (obj && obj->getTE(results[i].mSide) && obj->getTE(results[i].mSide)->isSelected()) + { + doSelectionCallbacks(object_override.mObjectId, results[i].mSide); + } + } + else + { + // unblock material editor + if (obj && obj->getTE(results[i].mSide) && obj->getTE(results[i].mSide)->isSelected()) + { + doSelectionCallbacks(object_override.mObjectId, results[i].mSide); + } + } + } + + if (obj && side_set.size() != obj->getNumTEs()) + { // object exists and at least one texture entry needs to have its override data nulled out + for (int i = 0; i < obj->getNumTEs(); ++i) + { + if (side_set.find(i) == side_set.end()) + { + obj->setTEGLTFMaterialOverride(i, nullptr); + if (obj->getTE(i) && obj->getTE(i)->isSelected()) + { + doSelectionCallbacks(object_override.mObjectId, i); + } + } + } + } + } + else if (obj) + { // override list was empty or an error occurred, null out all overrides for this object + for (int i = 0; i < obj->getNumTEs(); ++i) + { + obj->setTEGLTFMaterialOverride(i, nullptr); + if (obj->getTE(i) && obj->getTE(i)->isSelected()) + { + doSelectionCallbacks(obj->getID(), i); + } + } + } + }); + } + +private: + + std::vector<void(*)(const LLUUID& object_id, S32 side)> mSelectionCallbacks; +}; + +namespace +{ + LLGLTFMaterialOverrideDispatchHandler handle_gltf_override_message; +} + +void LLGLTFMaterialList::queueOverrideUpdate(const LLUUID& id, S32 side, LLGLTFMaterial* override_data) +{ + override_list_t& overrides = mQueuedOverrides[id]; + + if (overrides.size() < side + 1) + { + overrides.resize(side + 1); + } + + overrides[side] = override_data; +} + +void LLGLTFMaterialList::applyQueuedOverrides(LLViewerObject* obj) +{ + LL_PROFILE_ZONE_SCOPED; + + llassert(obj); + const LLUUID& id = obj->getID(); + auto iter = mQueuedOverrides.find(id); + + if (iter != mQueuedOverrides.end()) + { + override_list_t& overrides = iter->second; + for (int i = 0; i < overrides.size(); ++i) + { + if (overrides[i].notNull()) + { + if (!obj->getTE(i)) + { // object is incomplete + return; + } + + if (!obj->getTE(i)->getGLTFMaterial()) + { + // doesn't have its base GLTF material yet, don't apply override(yet) + return; + } + + S32 status = obj->setTEGLTFMaterialOverride(i, overrides[i]); + if (status == TEM_CHANGE_NONE) + { + // can't apply this yet, since failure to change the material override + // probably means the base material is still being fetched. leave in + // the queue for later + //obj->setDebugText("early out 3"); + return; + } + + if (obj->getTE(i)->isSelected()) + { + handle_gltf_override_message.doSelectionCallbacks(id, i); + } + // success! + overrides[i] = nullptr; + } + } + + mQueuedOverrides.erase(iter); + } +} + +void LLGLTFMaterialList::queueModify(const LLViewerObject* obj, S32 side, const LLGLTFMaterial* mat) +{ + if (obj && obj->getRenderMaterialID(side).notNull()) + { + if (mat == nullptr) + { + sModifyQueue.push_back({ obj->getID(), side, LLGLTFMaterial(), false }); + } + else + { + sModifyQueue.push_back({ obj->getID(), side, *mat, true }); + } + } +} + +void LLGLTFMaterialList::queueApply(const LLViewerObject* obj, S32 side, const LLUUID& asset_id) +{ + const LLGLTFMaterial* material_override = obj->getTE(side)->getGLTFMaterialOverride(); + if (material_override) + { + LLGLTFMaterial* cleared_override = new LLGLTFMaterial(*material_override); + cleared_override->setBaseMaterial(); + sApplyQueue.push_back({ obj->getID(), side, asset_id, cleared_override }); + } + else + { + sApplyQueue.push_back({ obj->getID(), side, asset_id, nullptr }); + } +} + +void LLGLTFMaterialList::queueUpdate(const LLSD& data) +{ + llassert(is_valid_update(data)); + + if (!sUpdates.isArray()) + { + sUpdates = LLSD::emptyArray(); + } + + sUpdates[sUpdates.size()] = data; +} + +void LLGLTFMaterialList::flushUpdates(void(*done_callback)(bool)) +{ + LLSD& data = sUpdates; + + S32 i = data.size(); + + for (ModifyMaterialData& e : sModifyQueue) + { +#ifdef SHOW_ASSERT + // validate object has a material id + LLViewerObject* obj = gObjectList.findObject(e.object_id); + llassert(obj && obj->getRenderMaterialID(e.side).notNull()); +#endif + + data[i]["object_id"] = e.object_id; + data[i]["side"] = e.side; + + if (e.has_override) + { + data[i]["gltf_json"] = e.override_data.asJSON(); + } + else + { + // Clear all overrides + data[i]["gltf_json"] = ""; + } + + llassert(is_valid_update(data[i])); + ++i; + } + sModifyQueue.clear(); + + for (auto& e : sApplyQueue) + { + data[i]["object_id"] = e.object_id; + data[i]["side"] = e.side; + data[i]["asset_id"] = e.asset_id; + if (e.override_data) + { + data[i]["gltf_json"] = e.override_data->asJSON(); + } + else + { + // Clear all overrides + data[i]["gltf_json"] = ""; + } + + llassert(is_valid_update(data[i])); + ++i; + } + sApplyQueue.clear(); + +#if 0 // debug output of data being sent to capability + std::stringstream str; + LLSDSerialize::serialize(data, str, LLSDSerialize::LLSD_NOTATION, LLSDFormatter::OPTIONS_PRETTY); + LL_INFOS() << "\n" << str.str() << LL_ENDL; +#endif + + if (sUpdates.size() > 0) + { + LLCoros::instance().launch("modifyMaterialCoro", + std::bind(&LLGLTFMaterialList::modifyMaterialCoro, + gAgent.getRegionCapability("ModifyMaterialParams"), + sUpdates, + done_callback)); + + sUpdates = LLSD::emptyArray(); + } +} + +void LLGLTFMaterialList::addSelectionUpdateCallback(void(*update_callback)(const LLUUID& object_id, S32 side)) +{ + handle_gltf_override_message.addCallback(update_callback); +} + +class AssetLoadUserData +{ +public: + AssetLoadUserData() {} + tinygltf::Model mModelIn; + LLPointer<LLFetchedGLTFMaterial> mMaterial; +}; + +void LLGLTFMaterialList::onAssetLoadComplete(const LLUUID& id, LLAssetType::EType asset_type, void* user_data, S32 status, LLExtStat ext_status) +{ + LL_PROFILE_ZONE_NAMED("gltf asset callback"); + AssetLoadUserData* asset_data = (AssetLoadUserData*)user_data; + + if (status != LL_ERR_NOERR) + { + LL_WARNS("GLTF") << "Error getting material asset data: " << LLAssetStorage::getErrorString(status) << " (" << status << ")" << LL_ENDL; + asset_data->mMaterial->materialComplete(); + delete asset_data; + } + else + { + + LL::WorkQueue::ptr_t main_queue = LL::WorkQueue::getInstance("mainloop"); + LL::WorkQueue::ptr_t general_queue = LL::WorkQueue::getInstance("General"); + + typedef std::pair<U32, tinygltf::Model> return_data_t; + + main_queue->postTo( + general_queue, + [id, asset_type, asset_data]() // Work done on general queue + { + std::vector<char> buffer; + { + LL_PROFILE_ZONE_NAMED("gltf read asset"); + LLFileSystem file(id, asset_type, LLFileSystem::READ); + auto size = file.getSize(); + if (!size) + { + return false; + } + + buffer.resize(size); + file.read((U8*)&buffer[0], buffer.size()); + } + + { + LL_PROFILE_ZONE_NAMED("gltf deserialize asset"); + + LLSD asset; + + // read file into buffer + std::istrstream str(&buffer[0], buffer.size()); + + if (LLSDSerialize::deserialize(asset, str, buffer.size())) + { + if (asset.has("version") && LLGLTFMaterial::isAcceptedVersion(asset["version"].asString())) + { + if (asset.has("type") && asset["type"].asString() == LLGLTFMaterial::ASSET_TYPE) + { + if (asset.has("data") && asset["data"].isString()) + { + std::string data = asset["data"]; + + std::string warn_msg, error_msg; + + LL_PROFILE_ZONE_SCOPED; + tinygltf::TinyGLTF gltf; + + if (!gltf.LoadASCIIFromString(&asset_data->mModelIn, &error_msg, &warn_msg, data.c_str(), data.length(), "")) + { + LL_WARNS("GLTF") << "Failed to decode material asset: " + << LL_NEWLINE + << warn_msg + << LL_NEWLINE + << error_msg + << LL_ENDL; + return false; + } + return true; + } + } + } + } + else + { + LL_WARNS("GLTF") << "Failed to deserialize material LLSD" << LL_ENDL; + } + } + + return false; + }, + [id, asset_data](bool result) // Callback to main thread + { + + if (result) + { + asset_data->mMaterial->setFromModel(asset_data->mModelIn, 0/*only one index*/); + } + else + { + LL_DEBUGS("GLTF") << "Failed to get material " << id << LL_ENDL; + } + + asset_data->mMaterial->materialComplete(); + + delete asset_data; + }); + } +} + +LLFetchedGLTFMaterial* LLGLTFMaterialList::getMaterial(const LLUUID& id) +{ + LL_PROFILE_ZONE_SCOPED; + uuid_mat_map_t::iterator iter = mList.find(id); + if (iter == mList.end()) + { + LL_PROFILE_ZONE_NAMED("gltf fetch") + LLFetchedGLTFMaterial* mat = new LLFetchedGLTFMaterial(); + mList[id] = mat; + + if (!mat->mFetching) + { + mat->materialBegin(); + + AssetLoadUserData *user_data = new AssetLoadUserData(); + user_data->mMaterial = mat; + + gAssetStorage->getAssetData(id, LLAssetType::AT_MATERIAL, onAssetLoadComplete, (void*)user_data); + } + + return mat; + } + + return iter->second; +} + +void LLGLTFMaterialList::addMaterial(const LLUUID& id, LLFetchedGLTFMaterial* material) +{ + mList[id] = material; +} + +void LLGLTFMaterialList::removeMaterial(const LLUUID& id) +{ + mList.erase(id); +} + +void LLGLTFMaterialList::flushMaterials() +{ + // Similar variant to what textures use + static const S32 MIN_UPDATE_COUNT = gSavedSettings.getS32("TextureFetchUpdateMinCount"); // default: 32 + //update MIN_UPDATE_COUNT or 5% of materials, whichever is greater + U32 update_count = llmax((U32)MIN_UPDATE_COUNT, (U32)mList.size() / 20); + update_count = llmin(update_count, (U32)mList.size()); + + const F64 MAX_INACTIVE_TIME = 30.f; + F64 cur_time = LLTimer::getTotalSeconds(); + + // advance iter one past the last key we updated + uuid_mat_map_t::iterator iter = mList.find(mLastUpdateKey); + if (iter != mList.end()) { + ++iter; + } + + while (update_count-- > 0) + { + if (iter == mList.end()) + { + iter = mList.begin(); + } + + LLPointer<LLFetchedGLTFMaterial> material = iter->second; + if (material->getNumRefs() == 2) // this one plus one from the list + { + + if (!material->mActive + && cur_time > material->mExpectedFlusTime) + { + iter = mList.erase(iter); + } + else + { + if (material->mActive) + { + material->mExpectedFlusTime = cur_time + MAX_INACTIVE_TIME; + material->mActive = false; + } + ++iter; + } + } + else + { + material->mActive = true; + ++iter; + } + } + + if (iter != mList.end()) + { + mLastUpdateKey = iter->first; + } + else + { + mLastUpdateKey.setNull(); + } + + { + using namespace LLStatViewer; + sample(NUM_MATERIALS, mList.size()); + } +} + +// static +void LLGLTFMaterialList::registerCallbacks() +{ + gGenericDispatcher.addHandler("GLTFMaterialOverride", &handle_gltf_override_message); +} + +// static +void LLGLTFMaterialList::modifyMaterialCoro(std::string cap_url, LLSD overrides, void(*done_callback)(bool) ) +{ + LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID); + LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t + httpAdapter(new LLCoreHttpUtil::HttpCoroutineAdapter("modifyMaterialCoro", httpPolicy)); + LLCore::HttpRequest::ptr_t httpRequest(new LLCore::HttpRequest); + LLCore::HttpOptions::ptr_t httpOpts(new LLCore::HttpOptions); + LLCore::HttpHeaders::ptr_t httpHeaders; + + httpOpts->setFollowRedirects(true); + + LL_DEBUGS("GLTF") << "Applying override via ModifyMaterialParams cap: " << overrides << LL_ENDL; + + LLSD result = httpAdapter->postAndSuspend(httpRequest, cap_url, overrides, httpOpts, httpHeaders); + + LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; + LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); + + bool success = true; + if (!status) + { + LL_WARNS("GLTF") << "Failed to modify material." << LL_ENDL; + success = false; + } + else if (!result["success"].asBoolean()) + { + LL_WARNS("GLTF") << "Failed to modify material: " << result["message"] << LL_ENDL; + success = false; + } + + if (done_callback) + { + done_callback(success); + } +} + +void LLGLTFMaterialList::loadCacheOverrides(const LLGLTFOverrideCacheEntry& override) +{ + handle_gltf_override_message.applyData(override); +} |