/** * @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 #include "json/reader.h" #include "json/value.h" #include 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 // side - S32 index of texture entry // gltf_json - String 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 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 results; if (!object_override.mSides.empty()) { results.reserve(object_override.mSides.size()); // parse json std::map::const_iterator iter = object_override.mSides.begin(); std::map::const_iterator end = object_override.mSides.end(); while (iter != end) { LLPointer 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 results) // Callback to main thread { LLViewerObject * obj = gObjectList.findObject(object_override.mObjectId); if (results.size() > 0 ) { std::unordered_set 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 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; 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) || !obj->getTE(i)->getGLTFMaterial()) { // object doesn't have its base GLTF material yet, don't apply override (yet) return; } obj->setTEGLTFMaterialOverride(i, overrides[i]); if (obj->getTE(i)->isSelected()) { handle_gltf_override_message.doSelectionCallbacks(id, i); } } } 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 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 return_data_t; main_queue->postTo( general_queue, [id, asset_type, asset_data]() // Work done on general queue { std::vector 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") && asset["version"] == "1.0") { if (asset.has("type") && asset["type"].asString() == "GLTF 2.0") { 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 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); }