phoenix-firestorm/indra/newview/llgltfmateriallist.cpp

743 lines
22 KiB
C++

/**
* @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 <boost/iostreams/device/array.hpp>
#include <boost/iostreams/stream.hpp>
#include <unordered_set>
LLGLTFMaterialList gGLTFMaterialList;
LLGLTFMaterialList::modify_queue_t LLGLTFMaterialList::sModifyQueue;
LLGLTFMaterialList::apply_queue_t LLGLTFMaterialList::sApplyQueue;
LLSD LLGLTFMaterialList::sUpdates;
const size_t MAX_TASK_UPDATES = 255;
#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
{
LOG_CLASS(LLGLTFMaterialOverrideDispatchHandler);
public:
LLGLTFMaterialOverrideDispatchHandler() = default;
~LLGLTFMaterialOverrideDispatchHandler() = default;
void addCallback(void(*callback)(const LLUUID& object_id, S32 side))
{
mSelectionCallbacks.push_back(callback);
}
void doSelectionCallbacks(const LLUUID& object_id, S32 side)
{
for (auto& callback : mSelectionCallbacks)
{
callback(object_id, side);
}
}
private:
std::vector<void(*)(const LLUUID& object_id, S32 side)> mSelectionCallbacks;
};
namespace
{
LLGLTFMaterialOverrideDispatchHandler handle_gltf_override_message;
}
void LLGLTFMaterialList::applyOverrideMessage(LLMessageSystem* msg, const std::string& data_in)
{
std::istringstream str(data_in);
LLSD data;
LLSDSerialize::fromNotation(data, str, data_in.length());
const LLHost& host = msg->getSender();
LLViewerRegion* region = LLWorld::instance().getRegion(host);
llassert(region);
if (region)
{
U32 local_id = data.get("id").asInteger();
LLUUID id;
gObjectList.getUUIDFromLocal(id, local_id, host.getAddress(), host.getPort());
LLViewerObject* obj = gObjectList.findObject(id);
// NOTE: obj may be null if the viewer hasn't heard about the object yet, cache update in any case
if (obj && gShowObjectUpdates)
{ // display a cyan blip for override updates when "Show Updates to Objects" enabled
LLColor4 color(0.f, 1.f, 1.f, 1.f);
gPipeline.addDebugBlip(obj->getPositionAgent(), color);
}
const LLSD& tes = data["te"];
const LLSD& od = data["od"];
constexpr U32 MAX_TES = 45;
bool has_te[MAX_TES] = { false };
if (tes.isArray()) // NOTE: if no "te" array exists, this is a malformed message (null out all overrides will come in as an empty te array)
{
LLGLTFOverrideCacheEntry cache;
cache.mLocalId = local_id;
cache.mObjectId = id;
cache.mRegionHandle = region->getHandle();
auto count = llmin(tes.size(), MAX_TES);
for (size_t i = 0; i < count; ++i)
{
LLGLTFMaterial* mat = new LLGLTFMaterial(); // setTEGLTFMaterialOverride and cache will take ownership
mat->applyOverrideLLSD(od[i]);
S32 te = tes[i].asInteger();
has_te[te] = true;
cache.mSides[te] = od[i];
cache.mGLTFMaterial[te] = mat;
if (obj)
{
obj->setTEGLTFMaterialOverride(te, mat);
if (obj->getTE(te) && obj->getTE(te)->isSelected())
{
handle_gltf_override_message.doSelectionCallbacks(id, te);
}
}
}
if (obj)
{ // null out overrides on TEs that shouldn't have them
U32 count = llmin(obj->getNumTEs(), MAX_TES);
for (U32 i = 0; i < count; ++i)
{
LLTextureEntry* te = obj->getTE(i);
if (!has_te[i] && te && te->getGLTFMaterialOverride())
{
obj->setTEGLTFMaterialOverride(i, nullptr);
handle_gltf_override_message.doSelectionCallbacks(id, i);
}
}
}
region->cacheFullUpdateGLTFOverride(cache);
LL_DEBUGS("GLTF") << "GLTF Material Override: " << cache.mObjectId << " " << cache.mLocalId << " " << cache.mRegionHandle << " (sides:" << (cache.mSides.size()) << ")" << LL_ENDL;
}
}
}
void LLGLTFMaterialList::queueOverrideUpdate(const LLUUID& id, S32 side, LLGLTFMaterial* override_data)
{
#if 0
override_list_t& overrides = mQueuedOverrides[id];
if (overrides.size() < side + 1)
{
overrides.resize(side + 1);
}
overrides[side] = override_data;
#endif
}
void LLGLTFMaterialList::applyQueuedOverrides(LLViewerObject* obj)
{
LL_PROFILE_ZONE_SCOPED;
llassert(obj);
#if 0
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);
}
#else
// the override cache is the authoritarian source of the most recent override data
LLViewerRegion* regionp = obj->getRegion();
if (regionp)
{
regionp->applyCacheMiscExtras(obj);
}
#endif
}
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::queueApply(const LLViewerObject* obj, S32 side, const LLUUID& asset_id, const LLGLTFMaterial* material_override)
{
if (asset_id.isNull() || material_override == nullptr)
{
queueApply(obj, side, asset_id);
}
else
{
LLGLTFMaterial* material = new LLGLTFMaterial(*material_override);
sApplyQueue.push_back({ obj->getID(), side, asset_id, material });
}
if (sUpdates.size() >= MAX_TASK_UPDATES)
{
LLCoros::instance().launch("modifyMaterialCoro",
std::bind(&LLGLTFMaterialList::modifyMaterialCoro,
gAgent.getRegionCapability("ModifyMaterialParams"),
sUpdates,
std::shared_ptr<CallbackHolder>(nullptr)));
sUpdates = LLSD::emptyArray();
}
}
void LLGLTFMaterialList::queueUpdate(const LLSD& data)
{
llassert(is_valid_update(data));
if (!sUpdates.isArray())
{
sUpdates = LLSD::emptyArray();
}
sUpdates[sUpdates.size()] = data;
if (sUpdates.size() >= MAX_TASK_UPDATES)
{
LLCoros::instance().launch("modifyMaterialCoro",
std::bind(&LLGLTFMaterialList::modifyMaterialCoro,
gAgent.getRegionCapability("ModifyMaterialParams"),
sUpdates,
std::shared_ptr<CallbackHolder>(nullptr)));
sUpdates = LLSD::emptyArray();
}
}
void LLGLTFMaterialList::flushUpdates(void(*done_callback)(bool))
{
std::shared_ptr<CallbackHolder> callback_holder;
if (done_callback)
{
callback_holder = std::make_shared<CallbackHolder>(done_callback);
}
while (!sModifyQueue.empty() || !sApplyQueue.empty())
{
flushUpdatesOnce(callback_holder);
}
}
void LLGLTFMaterialList::flushUpdatesOnce(std::shared_ptr<CallbackHolder> callback_holder)
{
LLSD& data = sUpdates;
size_t i = data.size();
while (!sModifyQueue.empty() && i < MAX_TASK_UPDATES)
{
ModifyMaterialData& e = sModifyQueue.front();
#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.pop_front();
}
while (!sApplyQueue.empty() && i < MAX_TASK_UPDATES)
{
ApplyMaterialAssetData& e = sApplyQueue.front();
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.pop_front();
}
#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,
callback_holder));
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(false);
delete asset_data;
}
else
{
LL::WorkQueue::ptr_t main_queue = LL::WorkQueue::getInstance("mainloop");
LL::WorkQueue::ptr_t general_queue = LL::WorkQueue::getInstance("General");
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], static_cast<S32>(buffer.size()));
}
{
LL_PROFILE_ZONE_NAMED("gltf deserialize asset");
LLSD asset;
// read file into buffer
boost::iostreams::stream<boost::iostreams::array_source> str(buffer.data(), 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(), static_cast<U32>(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(true);
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::modifyMaterialCoro(std::string cap_url, LLSD overrides, std::shared_ptr<CallbackHolder> callback_holder)
{
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 (callback_holder)
{
// Set to false even if something went through
// since at the moment it get used to refresh UI
// if update failed
callback_holder->mSuccess &= success;
}
}