diff --git a/indra/llcharacter/llcharacter.cpp b/indra/llcharacter/llcharacter.cpp index 24dde1760c..4e8f4eca04 100644 --- a/indra/llcharacter/llcharacter.cpp +++ b/indra/llcharacter/llcharacter.cpp @@ -109,6 +109,14 @@ void LLCharacter::removeMotion( const LLUUID& id ) mMotionController.removeMotion(id); } +//----------------------------------------------------------------------------- +// purgeMotionInstances() +//----------------------------------------------------------------------------- +void LLCharacter::purgeMotionInstances( const LLUUID& id ) +{ + mMotionController.purgeMotionInstances(id); +} + //----------------------------------------------------------------------------- // findMotion() //----------------------------------------------------------------------------- diff --git a/indra/llcharacter/llcharacter.h b/indra/llcharacter/llcharacter.h index b0c721344d..7019802a32 100644 --- a/indra/llcharacter/llcharacter.h +++ b/indra/llcharacter/llcharacter.h @@ -133,6 +133,11 @@ class LLCharacter void removeMotion( const LLUUID& id ); + // removes ALL instances of a motion -- the canonical one AND any deprecated + // duplicates still easing out -- for when the motion's backing keyframe + // data is about to be destroyed (see LLMotionController::purgeMotionInstances) + void purgeMotionInstances( const LLUUID& id ); + // returns an instance of a registered motion, creating one if necessary LLMotion* createMotion( const LLUUID &id ); diff --git a/indra/llcharacter/llmotioncontroller.cpp b/indra/llcharacter/llmotioncontroller.cpp index 03ad676891..c07ff1359f 100644 --- a/indra/llcharacter/llmotioncontroller.cpp +++ b/indra/llcharacter/llmotioncontroller.cpp @@ -335,6 +335,27 @@ void LLMotionController::removeMotionInstance(LLMotion* motionp) } } +//----------------------------------------------------------------------------- +// purgeMotionInstances() +//----------------------------------------------------------------------------- +void LLMotionController::purgeMotionInstances(const LLUUID& id) +{ + removeMotion(id); + for (motion_set_t::iterator iter = mDeprecatedMotions.begin(); + iter != mDeprecatedMotions.end(); ) + { + motion_set_t::iterator cur_iter = iter++; + LLMotion* motionp = *cur_iter; + if (motionp->getID() == id) + { + // The same complete excision deactivateMotionInstance() applies to + // deprecated motions; removeMotionInstance() deactivates if needed. + removeMotionInstance(motionp); // does not touch mDeprecatedMotions + mDeprecatedMotions.erase(cur_iter); + } + } +} + //----------------------------------------------------------------------------- // createMotion() //----------------------------------------------------------------------------- diff --git a/indra/llcharacter/llmotioncontroller.h b/indra/llcharacter/llmotioncontroller.h index c2cb174821..b1862d8ff4 100644 --- a/indra/llcharacter/llmotioncontroller.h +++ b/indra/llcharacter/llmotioncontroller.h @@ -122,6 +122,15 @@ class LLMotionController // returns true if successful bool stopMotionLocally( const LLUUID &id, bool stop_immediate ); + // immediately deactivates and deletes EVERY instance of a motion id -- + // the canonical instance and any deprecated duplicates still easing out. + // For use when the motion's backing data is about to be destroyed (e.g. a + // locally previewed animation whose globally cached keyframe data is purged + // on live reload/removal): removeMotion() only reaches the canonical + // instance, and a deprecated instance left easing out keeps dereferencing + // the freed data every frame. + void purgeMotionInstances( const LLUUID& id ); + // Move motions from loading to loaded void updateLoadingMotions(); diff --git a/indra/llprimitive/llmodelloader.cpp b/indra/llprimitive/llmodelloader.cpp index 4c0cbda205..638cb765f5 100644 --- a/indra/llprimitive/llmodelloader.cpp +++ b/indra/llprimitive/llmodelloader.cpp @@ -32,6 +32,7 @@ #include "llsdserialize.h" #include "lljoint.h" #include "llcallbacklist.h" +#include "workqueue.h" #include "llmatrix4a.h" #include @@ -192,9 +193,24 @@ void LLModelLoader::run() setLoadState(ERROR_PARSING); } - // todo: we are inside of a thread, push this into main thread worker, - // not into doOnIdleOneTime that laks tread safety - doOnIdleOneTime(boost::bind(&LLModelLoader::loadModelCallback,this)); + // Hand completion back to the main thread. run() executes on the loader's + // worker thread, and the LLCallbackList behind doOnIdleOneTime() is not + // thread-safe -- post through the main loop's WorkQueue instead. + LL::WorkQueue::ptr_t main_queue = LL::WorkQueue::getInstance("mainloop"); + if (main_queue) + { + main_queue->post(boost::bind(&LLModelLoader::loadModelCallback, this)); + } + else + { + // No main-loop queue registered. The viewer always has one (a global, + // gMainloopWork), so this branch can only run in a binary with no main + // loop concurrently using LLCallbackList -- where the idle list is the + // only delivery left. loadModelCallback() cannot be invoked inline + // here: it blocks until this thread is stopped and then deletes the + // loader, which would deadlock run(). + doOnIdleOneTime(boost::bind(&LLModelLoader::loadModelCallback, this)); + } } // static diff --git a/indra/newview/llfloaterlocalassets.cpp b/indra/newview/llfloaterlocalassets.cpp index 0d28ca6a1b..a925ff85ba 100644 --- a/indra/newview/llfloaterlocalassets.cpp +++ b/indra/newview/llfloaterlocalassets.cpp @@ -402,16 +402,16 @@ void LLPanelLocalAssetBase::onRemoveBtn() for (const auto& entry : selected) { - if (!entry.first.empty()) - { - LLLocalAssetPaths::getInstance()->removePath(assetType(), entry.first); // forget the path - } - // Unload every decoded unit backing this row. For a multi-material glTF file - // that's all of the file's material units, not just the selected row. + // Unload every decoded unit backing this row FIRST and forget the path + // only once they are all gone. delUnit() fires the manager signals + // synchronously, and the add-only LLLocalAssetPaths::onUnitsChanged() + // re-records any path a still-loaded unit reports -- removePath() up + // front would be undone by the very first delUnit (mesh teardown, or a + // multi-material glTF file's surviving sibling units). std::vector ids; if (!entry.first.empty()) { - unitsForPath(entry.first, ids); + unitsForPath(entry.first, ids); // all of the file's units, not just the row } if (ids.empty() && entry.second.notNull()) { @@ -421,6 +421,10 @@ void LLPanelLocalAssetBase::onRemoveBtn() { delUnit(id); // unload the decoded unit (fires the manager signal) } + if (!entry.first.empty()) + { + LLLocalAssetPaths::getInstance()->removePath(assetType(), entry.first); // forget the path + } } refresh(); // removePath() alone (undecoded rows) doesn't fire a manager signal } @@ -723,11 +727,15 @@ void LLPanelLocalMesh::onRez() doSpawn(id); // always rez a new copy return; } - // Undecoded: load it and rez once it finishes (addAndSpawn handles the async load). + // Undecoded: load it and rez once it finishes (addAndSpawn handles the async + // load). Decode with the joint-position flag the artist saved for this file, + // like loadPath -- defaulting it would also make onUnitsChanged erase the + // saved flag. const std::string path = getSelectedPath(); if (!path.empty()) { - LLLocalMeshMgr::getInstance()->addAndSpawn(std::vector(1, path)); + LLLocalMeshMgr::getInstance()->addAndSpawn(std::vector(1, path), + LLLocalAssetPaths::getInstance()->getMeshJoints(path)); } } @@ -740,11 +748,12 @@ void LLPanelLocalMesh::onAttach() return; } // Undecoded row: load it and attach once it finishes loading (mirrors how Rez - // handles an undecoded row via addAndSpawn). + // handles an undecoded row via addAndSpawn), honoring the saved joint flag. const std::string path = getSelectedPath(); if (!path.empty()) { - LLLocalMeshMgr::getInstance()->addAndAttach(path, getComboAttachPoint()); + LLLocalMeshMgr::getInstance()->addAndAttach(path, getComboAttachPoint(), + LLLocalAssetPaths::getInstance()->getMeshJoints(path)); } } @@ -837,9 +846,13 @@ void LLPanelLocalMesh::doRemove(const LLUUID& tracking_id) { if (tracking_id.notNull()) { - LLLocalAssetPaths::getInstance()->removePath(LLLocalAssetPaths::TYPE_MESH, - pathForUnit(tracking_id)); + // Resolve the path before the unit dies, but forget it only AFTER + // delUnit: the units-changed listeners fire during teardown and the + // add-only LLLocalAssetPaths::onUnitsChanged would re-record a path + // removed up front (see onRemoveBtn). + const std::string path = pathForUnit(tracking_id); delUnit(tracking_id); // units-changed signal -> refresh() + LLLocalAssetPaths::getInstance()->removePath(LLLocalAssetPaths::TYPE_MESH, path); } } @@ -1217,7 +1230,9 @@ class LLPanelLocalTexture final : public LLPanelLocalApplyAsset } LLUUID unitForPath(const std::string& path) const override { - return LLLocalBitmapMgr::getInstance()->getUnitID(path); + // User units only: a mesh-owned import of the same file is a distinct, + // read-only unit this tab must neither claim as loaded nor delete. + return LLLocalBitmapMgr::getInstance()->getUnitID(path, /*mesh_owned=*/false); } std::string pathForUnit(const LLUUID& tracking_id) const override { @@ -1271,8 +1286,9 @@ class LLPanelLocalMaterial final : public LLPanelLocalApplyAsset } LLUUID unitForPath(const std::string& path) const override { - // A file holds >= 1 material; treat it as loaded if its first material is. - return LLLocalGLTFMaterialMgr::getInstance()->getUnitID(path, 0); + // A file holds >= 1 material; treat it as loaded if its first USER material + // is (a mesh-owned import of the same file is a distinct, read-only set). + return LLLocalGLTFMaterialMgr::getInstance()->getUnitID(path, 0, /*mesh_owned=*/false); } std::string pathForUnit(const LLUUID& tracking_id) const override { @@ -1283,8 +1299,10 @@ class LLPanelLocalMaterial final : public LLPanelLocalApplyAsset } void unitsForPath(const std::string& path, std::vector& out) const override { - // One .gltf/.glb can decode to several material units; remove them all. - LLLocalGLTFMaterialMgr::getInstance()->getTrackingIDs(path, out); + // One .gltf/.glb can decode to several material units; act on all of the + // USER's. Mesh-owned imports of the same file belong to a loaded mesh and + // must not be deleted from this tab. + LLLocalGLTFMaterialMgr::getInstance()->getTrackingIDs(path, out, /*mesh_owned=*/false); } std::string iconName() const override { diff --git a/indra/newview/lllocalanim.cpp b/indra/newview/lllocalanim.cpp index 95f9074e36..b3aa4bfe6b 100644 --- a/indra/newview/lllocalanim.cpp +++ b/indra/newview/lllocalanim.cpp @@ -26,6 +26,7 @@ #include "lllocalanim.h" +#include "fsyspath.h" // UTF-8-safe std::filesystem paths on Windows #include "llbvhloader.h" #include "llcharacter.h" // LLCharacter::sInstances (resolve avatar by id) #include "lldatapacker.h" @@ -210,7 +211,7 @@ LLUUID LLLocalAnimMgr::loadAnim(const std::string& filename) if (!alias_deferred) { std::error_code ec; - anim.mLastModified = std::filesystem::last_write_time(filename, ec); + anim.mLastModified = std::filesystem::last_write_time(fsyspath(filename), ec); } const size_t bytes = anim.mData.size(); @@ -253,16 +254,15 @@ void LLLocalAnimMgr::delUnit(LLUUID tracking_id) return; } - // Stop it wherever it's playing, purge the cached motion, and drop the play map. + // Drop the play-map entries, then stop and delete the motion EVERYWHERE + // before freeing its cached keyframe data. The sweep covers instances + // mPlaying no longer tracks -- replaced/stopped ones still easing out and + // deprecated duplicates -- which would otherwise keep dereferencing the + // freed JointMotionList every frame. for (auto pit = mPlaying.begin(); pit != mPlaying.end(); ) { if (pit->second == tracking_id) { - if (LLVOAvatar* av = resolve_avatar(pit->first)) - { - av->stopMotion(tracking_id, true); - av->removeMotion(tracking_id); - } pit = mPlaying.erase(pit); } else @@ -270,6 +270,13 @@ void LLLocalAnimMgr::delUnit(LLUUID tracking_id) ++pit; } } + for (LLCharacter* character : LLCharacter::sInstances) + { + if (character) + { + character->purgeMotionInstances(tracking_id); + } + } LLKeyframeDataCache::removeKeyframeData(tracking_id); mAnims.erase(iter); @@ -354,24 +361,27 @@ bool LLLocalAnimMgr::reapplyToAvatar(const LLUUID& av_id, const LLUUID& anim_id) return false; } - // Purge the stale parsed motion instance so createMotion() yields a fresh one - // that deserializes the new bytes. The id is keyed per-avatar, so the old motion - // must go before a new one can take its place. - av->stopMotion(anim_id, true); - av->removeMotion(anim_id); - + // The caller (doUpdates) already purged the stale motion instances and the + // cached keyframe data, so createMotion() yields a fresh motion. The first + // avatar's deserialize() rebuilds and globally re-caches the new data; + // later avatars must NOT deserialize again -- every deserialize builds + // another JointMotionList and addKeyframeData() overwrites the cache slot + // without freeing it, leaking one list per extra avatar per reload. LLKeyframeMotion* motionp = dynamic_cast(av->createMotion(anim_id)); if (!motionp) { return false; } - LLDataPackerBinaryBuffer dp(iter->second.mData.data(), (S32)iter->second.mData.size()); - if (motionp->deserialize(dp, anim_id, false)) + if (!LLKeyframeDataCache::getKeyframeData(anim_id)) { - av->startMotion(anim_id); - return true; + LLDataPackerBinaryBuffer dp(iter->second.mData.data(), (S32)iter->second.mData.size()); + if (!motionp->deserialize(dp, anim_id, false)) + { + return false; + } } - return false; + av->startMotion(anim_id); + return true; } void LLLocalAnimMgr::doUpdates() @@ -385,7 +395,7 @@ void LLLocalAnimMgr::doUpdates() LocalAnim& anim = entry.second; std::error_code ec; - const auto mtime = std::filesystem::last_write_time(anim.mFilename, ec); + const auto mtime = std::filesystem::last_write_time(fsyspath(anim.mFilename), ec); if (ec || mtime == anim.mLastModified) { continue; @@ -398,16 +408,44 @@ void LLLocalAnimMgr::doUpdates() } anim.mData = std::move(fresh); - // Refresh the cached keyframe data so the next play uses the new bytes, - // and re-apply live to any avatar currently playing this id. - LLKeyframeDataCache::removeKeyframeData(id); - bool reapply_ok = true; - for (const auto& play : mPlaying) + // Collect the avatars to re-apply to, pruning entries whose avatar is + // gone -- counting a dead avatar as a reapply failure would leave the + // mtime unconsumed and re-parse + restart the anim on every live avatar + // every heartbeat, forever. + std::vector replaying; + for (auto pit = mPlaying.begin(); pit != mPlaying.end(); ) { - if (play.second == id) + if (pit->second == id) { - reapply_ok = reapplyToAvatar(play.first, id) && reapply_ok; + if (!resolve_avatar(pit->first)) + { + pit = mPlaying.erase(pit); + continue; + } + replaying.push_back(pit->first); } + ++pit; + } + + // Stop and delete every live instance BEFORE freeing the cached keyframe + // data: stopping dereferences the JointMotionList (setStopTime, constraint + // teardown), and instances mPlaying no longer tracks (replaced/stopped + // ones still easing out, deprecated duplicates) would keep reading the + // freed data every frame. + for (LLCharacter* character : LLCharacter::sInstances) + { + if (character) + { + character->purgeMotionInstances(id); + } + } + LLKeyframeDataCache::removeKeyframeData(id); + + // Re-apply live to any avatar currently playing this id. + bool reapply_ok = true; + for (const LLUUID& av_id : replaying) + { + reapply_ok = reapplyToAvatar(av_id, id) && reapply_ok; } // Consume the mtime only once the swap is fully live AND the decode used joint diff --git a/indra/newview/lllocalassetpaths.cpp b/indra/newview/lllocalassetpaths.cpp index 53fb163c16..bd9c8dacb9 100644 --- a/indra/newview/lllocalassetpaths.cpp +++ b/indra/newview/lllocalassetpaths.cpp @@ -94,15 +94,29 @@ void LLLocalAssetPaths::writeToDisk() const { return; } - llofstream out(path.c_str()); - if (out.is_open()) + // Write to a temp file and rename it over the old one (std::filesystem::rename + // replaces in one step) so a crash mid-write can't leave a truncated file -- + // reloadForAccount() would silently normalize that to an empty working set. + const std::string tmp_path = path + ".tmp"; + llofstream out(tmp_path.c_str()); + if (!out.is_open()) { - LLSDSerialize::toXML(mPaths, out); - out.close(); + LL_WARNS("LocalAssets") << "Can't write local asset list: " << tmp_path << LL_ENDL; + return; + } + LLSDSerialize::toXML(mPaths, out); + out.close(); + if (out.fail()) + { + LL_WARNS("LocalAssets") << "Failed writing local asset list: " << tmp_path << LL_ENDL; + LLFile::remove(tmp_path); + return; } - else + if (LLFile::rename(tmp_path, path) != 0) { - LL_WARNS("LocalAssets") << "Can't write local asset list: " << path << LL_ENDL; + // rename already warned; don't leave the temp file behind, and keep the + // old (still intact) file as the persisted state + LLFile::remove(tmp_path); } } diff --git a/indra/newview/lllocalbitmaps.cpp b/indra/newview/lllocalbitmaps.cpp index 94215b0a53..ae5bb7b237 100644 --- a/indra/newview/lllocalbitmaps.cpp +++ b/indra/newview/lllocalbitmaps.cpp @@ -1163,12 +1163,12 @@ LLUUID LLLocalBitmapMgr::addUnitInternal(const std::string& filename, bool mesh_ return LLUUID::null; } -LLUUID LLLocalBitmapMgr::getUnitID(const std::string& filename) +LLUUID LLLocalBitmapMgr::getUnitID(const std::string& filename, bool mesh_owned) { for (local_list_iter itBitmap = mBitmapList.begin(); mBitmapList.end() != itBitmap; ++itBitmap) { LLLocalBitmap* unit = *itBitmap; - if (filename == unit->getFilename()) + if (filename == unit->getFilename() && unit->isMeshOwned() == mesh_owned) { return unit->getTrackingID(); } diff --git a/indra/newview/lllocalbitmaps.h b/indra/newview/lllocalbitmaps.h index f92bba78c7..9ac0e86413 100644 --- a/indra/newview/lllocalbitmaps.h +++ b/indra/newview/lllocalbitmaps.h @@ -146,7 +146,11 @@ class LLLocalBitmapMgr : public LLSingleton LLUUID addUnit(const std::string& filename); // Add a unit imported by a local mesh: hidden from the Textures tab + persistence. LLUUID addUnit(const std::string& filename, bool mesh_owned); - LLUUID getUnitID(const std::string& filename); + // Resolve a file to its unit of ONE ownership class. A user texture and a + // mesh-owned import of the same file are distinct units (see addUnitInternal); + // an ownership-blind first-match could hand the Textures tab a mesh's hidden + // import to delete (or vice versa), so callers must say which side they want. + LLUUID getUnitID(const std::string& filename, bool mesh_owned); void delUnit(LLUUID tracking_id); bool checkTextureDimensions(std::string filename); diff --git a/indra/newview/lllocalgltfmaterials.cpp b/indra/newview/lllocalgltfmaterials.cpp index 5d378f931c..682fa2456f 100644 --- a/indra/newview/lllocalgltfmaterials.cpp +++ b/indra/newview/lllocalgltfmaterials.cpp @@ -35,6 +35,7 @@ #include "llgltfmateriallist.h" #include "llimage.h" #include "llinventoryicon.h" +#include "lllocalmesh.h" // rebindFaceMaterials after a live-edit regroup #include "llmaterialmgr.h" #include "llnotificationsutil.h" #include "llscrolllistctrl.h" @@ -243,10 +244,16 @@ bool LLLocalGLTFMaterial::loadMaterial() material_name); } - mMaterialName = material_name; // for matching a mesh face's binding name - if (!material_name.empty()) + if (decode_successful) { - mShortName = gDirUtilp->getBaseFileName(filename_lc, true) + " (" + material_name + ")"; + // Only commit the binding name on success: a failed re-decode (e.g. + // the file locked mid-save, retried later) must not blank the name a + // mesh re-bind (getWorldIDsByName) matches faces against. + mMaterialName = material_name; // for matching a mesh face's binding name + if (!material_name.empty()) + { + mShortName = gDirUtilp->getBaseFileName(filename_lc, true) + " (" + material_name + ")"; + } } break; @@ -532,14 +539,15 @@ std::vector LLLocalGLTFMaterialMgr::getFilenames() const return out; } -LLUUID LLLocalGLTFMaterialMgr::getUnitID(const std::string& filename, S32 index) +LLUUID LLLocalGLTFMaterialMgr::getUnitID(const std::string& filename, S32 index, bool mesh_owned) { if (!mMaterialList.empty()) { for (local_list_iter itBitmap = mMaterialList.begin(); mMaterialList.end() != itBitmap; ++itBitmap) { LLLocalGLTFMaterial* unit = *itBitmap; - if (filename == unit->getFilename() && index == unit->getIndexInFile()) + if (filename == unit->getFilename() && index == unit->getIndexInFile() + && unit->isMeshOwned() == mesh_owned) { return unit->getTrackingID(); } @@ -548,11 +556,11 @@ LLUUID LLLocalGLTFMaterialMgr::getUnitID(const std::string& filename, S32 index) return LLUUID::null; } -void LLLocalGLTFMaterialMgr::getTrackingIDs(const std::string& filename, std::vector& out) +void LLLocalGLTFMaterialMgr::getTrackingIDs(const std::string& filename, std::vector& out, bool mesh_owned) { for (const LLPointer& unit : mMaterialList) { - if (unit->getFilename() == filename) + if (unit->getFilename() == filename && unit->isMeshOwned() == mesh_owned) { out.push_back(unit->getTrackingID()); } @@ -616,12 +624,12 @@ void LLLocalGLTFMaterialMgr::getFilenameAndIndex(LLUUID tracking_id, std::string } } -void LLLocalGLTFMaterialMgr::getWorldIDsByName(const std::string& filename, std::map& out) +void LLLocalGLTFMaterialMgr::getWorldIDsByName(const std::string& filename, std::map& out, bool mesh_owned) { for (local_list_iter iter = mMaterialList.begin(); iter != mMaterialList.end(); iter++) { LLLocalGLTFMaterial* unit = *iter; - if (unit->getFilename() != filename) + if (unit->getFilename() != filename || unit->isMeshOwned() != mesh_owned) { continue; } @@ -695,11 +703,142 @@ void LLLocalGLTFMaterialMgr::doUpdates() // preventing theoretical overlap in cases with huge number of loaded images. mTimer.stopTimer(); + // Per-unit content refresh; collect which (file, ownership class) groups + // actually re-decoded so the structural pass below runs once per group. + std::vector> changed; for (local_list_iter iter = mMaterialList.begin(); iter != mMaterialList.end(); iter++) { - (*iter)->updateSelf(); + if ((*iter)->updateSelf()) + { + std::pair key((*iter)->getFilename(), (*iter)->isMeshOwned()); + if (std::find(changed.begin(), changed.end(), key) == changed.end()) + { + changed.push_back(key); + } + } + } + + // A live edit can change a file's material STRUCTURE, not just content: + // re-derive its units/aliases, and when that moved the name -> id mapping, + // re-point any local mesh bound to the file (faces bind by name only at + // ingest, so a diverged formerly-deduplicated material would otherwise keep + // rendering the stale canonical unit forever). + bool structure_changed = false; + for (const auto& entry : changed) + { + if (regroupFileMaterials(entry.first, entry.second)) + { + structure_changed = true; + LLLocalMeshMgr::getInstance()->rebindFaceMaterials(entry.first); + } + } + if (structure_changed) + { + mUnitsChangedSignal(); // regroup can add units -> refresh the lists } mTimer.startTimer(); } +bool LLLocalGLTFMaterialMgr::regroupFileMaterials(const std::string& filename, bool mesh_owned) +{ + std::map before; + getWorldIDsByName(filename, before, mesh_owned); + + tinygltf::Model model; + if (!LLTinyGLTFHelper::loadModel(filename, model)) + { + return false; // unreadable mid-save; the per-unit retry path covers it + } + const S32 count = (S32)model.materials.size(); + if (count <= 0) + { + return false; + } + + // This file's units of this ownership class, by material index. + std::map by_index; + for (const LLPointer& unit : mMaterialList) + { + if (unit->getFilename() == filename && unit->isMeshOwned() == mesh_owned) + { + by_index[unit->getIndexInFile()] = unit.get(); + } + } + if (by_index.empty()) + { + return false; + } + + // Units whose material index no longer exists: a successful parse is truth + // (a mesh reload replaces geometry wholesale, same idea), so drop them. + // Collect first -- delUnit mutates mMaterialList and fires the signal. + std::vector dead; + for (auto it = by_index.begin(); it != by_index.end();) + { + if (it->first >= count) + { + dead.push_back(it->second->getTrackingID()); + it = by_index.erase(it); + } + else + { + ++it; + } + } + + // Signature per index, and signature -> canonical unit over the unit-backed + // indices (lowest index wins, matching import order). + std::vector sigs; + sigs.reserve(count); + for (S32 i = 0; i < count; ++i) + { + sigs.push_back(gltfMaterialSignature(model, i)); + } + std::map canonical; + for (const auto& entry : by_index) + { + canonical.emplace(sigs[entry.first], entry.second); + } + + // Rebuild every alias from scratch. An index with no unit either still + // matches an existing unit's content (alias it there) or has diverged / + // been newly added (give it its own unit now). + for (const auto& entry : by_index) + { + entry.second->clearAliasNames(); + } + for (S32 i = 0; i < count; ++i) + { + if (by_index.count(i)) + { + continue; // has its own unit + } + const std::string& nm = model.materials[i].name; + const std::string name = nm.empty() ? ("mat" + std::to_string(i)) : nm; + std::map::iterator canon_it = canonical.find(sigs[i]); + if (canon_it != canonical.end()) + { + canon_it->second->addAliasName(name); + continue; + } + LLPointer unit = new LLLocalGLTFMaterial(filename, i); + if (unit->updateSelf()) + { + unit->setMeshOwned(mesh_owned); + mMaterialList.emplace_back(unit); + by_index[i] = unit.get(); + canonical.emplace(sigs[i], unit.get()); + } + } + + for (const LLUUID& id : dead) + { + delUnit(id); + } + + std::map after; + getWorldIDsByName(filename, after, mesh_owned); + return before != after; +} + diff --git a/indra/newview/lllocalgltfmaterials.h b/indra/newview/lllocalgltfmaterials.h index 7ec1309e64..ab1f7ab85c 100644 --- a/indra/newview/lllocalgltfmaterials.h +++ b/indra/newview/lllocalgltfmaterials.h @@ -58,6 +58,7 @@ class LLLocalGLTFMaterial : public LLFetchedGLTFMaterial // map (see LLLocalGLTFMaterialMgr::getWorldIDsByName). const std::vector& getAliasNames() const { return mAliasNames; } void addAliasName(const std::string& name) { mAliasNames.push_back(name); } + void clearAliasNames() { mAliasNames.clear(); } // rebuilt by a live-edit regroup // Imported by a local mesh for its own faces: hidden from the Materials tab + // cross-session persistence (it reappears when the mesh re-decodes). bool isMeshOwned() const { return mIsMeshOwned; } @@ -122,13 +123,25 @@ class LLLocalGLTFMaterialMgr : public LLSingleton S32 addUnit(const std::string& filename, bool mesh_owned); protected: S32 addUnitInternal(const std::string& filename, LLUUID& outID, bool mesh_owned = false); // file can hold multiple materials + // Re-derive a file's unit/alias structure after a live edit: import-time dedup + // collapses content-identical materials onto one unit (the other binding names + // become aliases), and an edit can split such a group, merge one, or add/remove + // materials outright. Surviving units keep their tracking/world ids; diverged or + // new indices get fresh units; removed indices drop theirs; every alias is + // rebuilt. Returns true when the file's name -> world id mapping changed (the + // caller re-binds local-mesh faces off that). + bool regroupFileMaterials(const std::string& filename, bool mesh_owned); public: void delUnit(LLUUID tracking_id); - LLUUID getUnitID(const std::string& filename, S32 index = 0); - // Tracking ids of every unit loaded from a file, in list order. Unlike walking - // getUnitID(file, 0..N), this is safe across the index gaps that import-time - // deduplication leaves in getIndexInFile(). - void getTrackingIDs(const std::string& filename, std::vector& out); + // The lookups below resolve within ONE ownership class: a user-loaded set and a + // mesh-owned import of the same file are distinct units (see addUnitInternal), + // and an ownership-blind match could hand the Materials tab a mesh's hidden + // imports to delete (or bind a mesh's faces to the user's deletable copies). + LLUUID getUnitID(const std::string& filename, S32 index, bool mesh_owned); + // Tracking ids of every unit of the class loaded from a file, in list order. + // Unlike walking getUnitID(file, 0..N), this is safe across the index gaps + // that import-time deduplication leaves in getIndexInFile(). + void getTrackingIDs(const std::string& filename, std::vector& out, bool mesh_owned); LLUUID getWorldID(LLUUID tracking_id); bool isMeshOwned(const LLUUID& tracking_id) const; // imported by a local mesh @@ -136,7 +149,7 @@ class LLLocalGLTFMaterialMgr : public LLSingleton void getFilenameAndIndex(LLUUID tracking_id, std::string &filename, S32 &index); // Map each of a file's materials by name (empty name -> "mat", matching the // model loader's face bindings) to its world id, for texturing a local mesh. - void getWorldIDsByName(const std::string& filename, std::map& out); + void getWorldIDsByName(const std::string& filename, std::map& out, bool mesh_owned); std::vector getFilenames() const; // distinct loaded files (persistence) void feedScrollList(LLScrollListCtrl* ctrl); diff --git a/indra/newview/lllocalmesh.cpp b/indra/newview/lllocalmesh.cpp index 5a815f6d3b..f5c041cb31 100644 --- a/indra/newview/lllocalmesh.cpp +++ b/indra/newview/lllocalmesh.cpp @@ -76,15 +76,19 @@ namespace // owns the by-reference joint maps the loader holds for the duration of the // parse, so they outlive the (possibly deleted) LLLocalMesh. The load // callback frees it. The unit is looked up by tracking ID, so a unit - // deleted mid-load can't be dereferenced. + // deleted mid-load can't be dereferenced. The preview avatar is held by + // LLPointer: the loader thread resolves (and the DAE loader writes to) + // joints on it mid-parse, while a teleport/logout can markDead() and drop + // the manager's reference at any time -- the ref keeps the avatar's memory + // (and its joints) valid until the context is freed on the main thread. struct LoadContext { - LLUUID mTrackingID; - LLModelLoader* mLoader = nullptr; - JointTransformMap mJointTransformMap; - JointNameSet mJointsFromNode; - U32 mLoadState = LLModelLoader::STARTING; - LLVOAvatar* mAvatar = nullptr; // preview skeleton for joint lookup (never the agent) + LLUUID mTrackingID; + LLModelLoader* mLoader = nullptr; + JointTransformMap mJointTransformMap; + JointNameSet mJointsFromNode; + U32 mLoadState = LLModelLoader::STARTING; + LLPointer mAvatar; // preview skeleton for joint lookup (never the agent) }; // Build the joint alias map the loaders use to recognise rig joints, @@ -411,9 +415,14 @@ namespace LLUUID LLLocalMesh::registerOwnedBitmap(const std::string& filename, std::vector& owned) { LLLocalBitmapMgr* mgr = LLLocalBitmapMgr::getInstance(); - // Reuse an existing unit for this file (another face of this mesh, a prior - // reload, or one the user already loaded) so we don't duplicate it. - LLUUID tracking_id = mgr->getUnitID(filename); + // Reuse an existing unit for this file -- a prior import (another face of this + // mesh or an earlier reload) first, else a copy the user already loaded -- so + // we don't duplicate it. + LLUUID tracking_id = mgr->getUnitID(filename, /*mesh_owned=*/true); + if (tracking_id.isNull()) + { + tracking_id = mgr->getUnitID(filename, /*mesh_owned=*/false); + } if (tracking_id.isNull()) { tracking_id = mgr->addUnit(filename, /*mesh_owned=*/true); @@ -436,29 +445,26 @@ void LLLocalMesh::importGLTFMaterials(std::map& out_by_name // and doesn't count.) Import-time dedup can leave gaps in per-file material // indices, so enumerate units by tracking id rather than walking indices. std::vector tids; - mgr->getTrackingIDs(mFilename, tids); - bool has_mesh_owned = false; - for (const LLUUID& tid : tids) - { - if (mgr->isMeshOwned(tid)) { has_mesh_owned = true; break; } - } - if (!has_mesh_owned) + mgr->getTrackingIDs(mFilename, tids, /*mesh_owned=*/true); + if (tids.empty()) { mgr->addUnit(mFilename, /*mesh_owned=*/true); - tids.clear(); - mgr->getTrackingIDs(mFilename, tids); + mgr->getTrackingIDs(mFilename, tids, /*mesh_owned=*/true); } // Keep this parse's mesh-owned units referenced (a user-loaded copy is left // untracked -- the user owns it; we must not release it on reload/delete). for (const LLUUID& tid : tids) { - if (mgr->isMeshOwned(tid)) - { - track_owned(owned, tid); - } + track_owned(owned, tid); + } + // Bind faces to the mesh's own imports; the user's deletable copies are only a + // fallback for the corner where the mesh-owned import failed to load. + mgr->getWorldIDsByName(mFilename, out_by_name, /*mesh_owned=*/true); + if (out_by_name.empty()) + { + mgr->getWorldIDsByName(mFilename, out_by_name, /*mesh_owned=*/false); } - mgr->getWorldIDsByName(mFilename, out_by_name); } bool LLLocalMesh::ingestScene(LLModelLoader::scene& scene) @@ -590,15 +596,21 @@ bool LLLocalMesh::ingestScene(LLModelLoader::scene& scene) } } } - else if (mFormat == FMT_GLTF && !gltf_mat_by_name.empty()) + else if (mFormat == FMT_GLTF) { for (S32 fi = 0; fi < (S32)faces.size() && fi < (S32)mdl->mMaterialList.size(); ++fi) { + LLLocalMeshFaceMaterial& fm = part.mFaceMaterials[fi]; + // Keep the binding name even when it doesn't resolve right now: + // a live edit of the material file regroups its units (see + // LLLocalGLTFMaterialMgr), and rebindFaceMaterials() re-resolves + // faces by this name. + fm.mMaterialName = mdl->mMaterialList[fi]; std::map::const_iterator it = - gltf_mat_by_name.find(mdl->mMaterialList[fi]); + gltf_mat_by_name.find(fm.mMaterialName); if (it != gltf_mat_by_name.end()) { - part.mFaceMaterials[fi].mRenderMaterialID = it->second; + fm.mRenderMaterialID = it->second; } } } @@ -754,7 +766,11 @@ bool LLLocalMesh::forceReload() // Re-parse the current file regardless of mtime so a changed build option (e.g. // the joint-position toggle) takes effect. Reuses the reload path: onLoadResult() // rebuilds the geometry and refreshes the in-world linkset if one exists. - if (mReloading || !gDirUtilp->fileExists(mFilename)) + // Skip while the initial parse is still in flight: ingestScene() reads the + // current build options when that parse lands, so the change is picked up + // anyway -- and a second concurrent loader would cross result states with + // the first (a late failure would even delUnit a unit that loaded fine). + if (mReloading || mState == ST_LOADING || !gDirUtilp->fileExists(mFilename)) { return false; } @@ -852,6 +868,72 @@ void LLLocalMeshMgr::despawnObjectsInRegion(LLViewerRegion* regionp) } } +void LLLocalMeshMgr::rebindFaceMaterials(const std::string& filename) +{ + const LLUUID tracking_id = getUnitID(filename); + LLLocalMesh* unit = tracking_id.notNull() ? getUnit(tracking_id) : nullptr; + if (!unit || !unit->getValid()) + { + return; // no loaded mesh binds to this file + } + + // The new name -> world id mapping, with the same mesh-owned-first/user- + // fallback preference the ingest-time bind uses. + LLLocalGLTFMaterialMgr* mgr = LLLocalGLTFMaterialMgr::getInstance(); + std::map by_name; + mgr->getWorldIDsByName(filename, by_name, /*mesh_owned=*/true); + if (by_name.empty()) + { + mgr->getWorldIDsByName(filename, by_name, /*mesh_owned=*/false); + } + + for (size_t p = 0; p < unit->mParts.size(); ++p) + { + LLLocalMeshPart& part = unit->mParts[p]; + for (size_t f = 0; f < part.mFaceMaterials.size(); ++f) + { + LLLocalMeshFaceMaterial& fm = part.mFaceMaterials[f]; + if (fm.mMaterialName.empty()) + { + continue; // not a glTF-bound face + } + std::map::const_iterator it = by_name.find(fm.mMaterialName); + const LLUUID new_id = (it != by_name.end()) ? it->second : LLUUID(); + if (new_id == fm.mRenderMaterialID) + { + continue; + } + const LLUUID old_id = fm.mRenderMaterialID; + fm.mRenderMaterialID = new_id; // future hot-swaps diff against the new import + + for (auto& entry : mSpawnedCopies) + { + const SpawnedCopy& copy = entry.second; + if (copy.mTrackingID != tracking_id || p >= copy.mPrims.size()) + { + continue; + } + LLViewerObject* o = copy.mPrims[p].get(); + LLVOVolume* vol = (o && !o->isDead()) ? dynamic_cast(o) : nullptr; + if (!vol || f >= (size_t)vol->getNumTEs()) + { + continue; + } + if (vol->getRenderMaterialID((U8)f) != old_id) + { + continue; // the user re-materialed this face in-world; leave it + } + if (new_id.notNull()) + { + vol->setHasRenderMaterialParams(true); // client-only: no server echo sets it + } + vol->setRenderMaterialID((S32)f, new_id, false, true); + vol->markForUpdate(); + } + } + } +} + LLVOAvatar* LLLocalMeshMgr::getPreviewAvatar(bool run_stand_anim) { if ((mPreviewAvatar.isNull() || mPreviewAvatar->isDead()) && gAgent.getRegion()) @@ -933,57 +1015,61 @@ LLUUID LLLocalMeshMgr::addUnitInternal(const std::string& filename, bool include void LLLocalMeshMgr::delUnit(LLUUID tracking_id) { - for (local_list_iter iter = mMeshList.begin(); iter != mMeshList.end(); ) + // Pull the unit out of the list BEFORE any teardown that fires signals + // (despawnUnit fires mUnitsChangedSignal; releasing owned imports fires the + // bitmap/material managers' signals). Listeners run synchronously and must + // never see the dying unit still listed -- the add-only + // LLLocalAssetPaths::onUnitsChanged, for one, would re-record its path from + // getFilenames() and undo a Remove. + LLLocalMesh* unit = nullptr; + for (local_list_iter iter = mMeshList.begin(); iter != mMeshList.end(); ++iter) { - LLLocalMesh* unit = *iter; - if (unit->getTrackingID() == tracking_id) + if ((*iter)->getTrackingID() == tracking_id) { - despawnUnit(tracking_id); - // Release the mesh-owned local bitmaps + materials imported for this - // unit -- but only the ones no OTHER loaded mesh still references. - // Imports are deduplicated by file, so two meshes using the same - // texture/material share a tracking id; releasing it unconditionally - // would strip it from the other mesh too. - auto shared_with_other = [&](const LLUUID& import_id) -> bool + unit = *iter; + mMeshList.erase(iter); + break; + } + } + if (unit) + { + despawnUnit(tracking_id); + // Release the mesh-owned local bitmaps + materials imported for this + // unit -- but only the ones no OTHER loaded mesh still references. + // Imports are deduplicated by file, so two meshes using the same + // texture/material share a tracking id; releasing it unconditionally + // would strip it from the other mesh too. (The dying unit is already + // delisted, so mMeshList holds only the others.) + auto shared_with_other = [&](const LLUUID& import_id) -> bool + { + for (const LLLocalMesh* other : mMeshList) { - for (const LLLocalMesh* other : mMeshList) + for (const LLUUID& bid : other->mOwnedBitmaps) { - if (other == unit) - { - continue; - } - for (const LLUUID& bid : other->mOwnedBitmaps) - { - if (bid == import_id) return true; - } - for (const LLUUID& mid : other->mOwnedMaterials) - { - if (mid == import_id) return true; - } + if (bid == import_id) return true; } - return false; - }; - for (const LLUUID& bid : unit->mOwnedBitmaps) - { - if (!shared_with_other(bid)) + for (const LLUUID& mid : other->mOwnedMaterials) { - LLLocalBitmapMgr::getInstance()->delUnit(bid); + if (mid == import_id) return true; } } - for (const LLUUID& mid : unit->mOwnedMaterials) + return false; + }; + for (const LLUUID& bid : unit->mOwnedBitmaps) + { + if (!shared_with_other(bid)) { - if (!shared_with_other(mid)) - { - LLLocalGLTFMaterialMgr::getInstance()->delUnit(mid); - } + LLLocalBitmapMgr::getInstance()->delUnit(bid); } - iter = mMeshList.erase(iter); - delete unit; } - else + for (const LLUUID& mid : unit->mOwnedMaterials) { - ++iter; + if (!shared_with_other(mid)) + { + LLLocalGLTFMaterialMgr::getInstance()->delUnit(mid); + } } + delete unit; } if (mMeshList.empty()) @@ -1402,6 +1488,33 @@ LLViewerObject* LLLocalMeshMgr::spawnInWorld(const LLUUID& tracking_id) return root; } +LLViewerObject* LLLocalMeshMgr::duplicatePreview(LLViewerObject* obj, const LLVector3& offset) +{ + LLViewerObject* root = findRootForObject(obj); + if (!root || root->isDead() || root->isAttachment()) + { + return nullptr; // worn copies don't duplicate, matching the sim path + } + auto it = mSpawnedCopies.find(instanceForObject(root)); + if (it == mSpawnedCopies.end()) + { + return nullptr; + } + const LLUUID tracking_id = it->second.mTrackingID; + const LLLocalMesh* unit = getUnit(tracking_id); + if (!unit || !unit->getValid() || unit->getParts().empty()) + { + return nullptr; + } + // Same transform capture as respawnInstancesInPlace(): spawnLinkset() composes + // the root part's intrinsic rotation itself, so pass the USER delta, not the + // full world rotation. + const LLQuaternion user_delta = ~unit->getParts().front().mRotation * root->getRotation(); + LLUUID instance_id; + instance_id.generate(); + return spawnLinkset(tracking_id, instance_id, root->getPositionAgent() + offset, user_delta, false, 0); +} + LLViewerObject* LLLocalMeshMgr::spawnLinkset(const LLUUID& tracking_id, const LLUUID& instance_id, const LLVector3& base, const LLQuaternion& root_rot, bool attach, S32 attach_point) @@ -1550,47 +1663,80 @@ void LLLocalMeshMgr::respawnInstancesInPlace(const LLUUID& tracking_id) } } -namespace +// Hot-swap diff-restore state: each spawned prim's live face state, captured +// BEFORE the reload's ingestScene commits (ingest releases dropped imports, +// whose teardown already swaps live faces to IMG_DEFAULT -- a capture taken +// after that would record the reset as if the user had applied it), plus the +// OLD parts' imported materials to diff user edits against. +struct LLLocalMeshPreSwapSnapshot { -// A face's user-visible render state, snapshotted so user edits survive a hot-swap: -// applyPartGeometry re-applies the file's imported materials and resets every face, -// which would otherwise wipe textures/materials/glow/etc. the user applied to the -// in-world preview. -struct PreservedFace -{ - LLTextureEntry te; // diffuse id, color, glow, bump/shiny/fullbright, transforms, Blinn-Phong material, glTF override - LLUUID render_mat_id; // glTF render material id (kept in the param block, not the TE) + struct Face + { + LLTextureEntry mTE; // diffuse id, color, glow, bump/shiny/fullbright, transforms, Blinn-Phong material, glTF override + LLUUID mRenderMatID; // glTF render material id (kept in the param block, not the TE) + }; + typedef std::vector PrimFaces; + + std::map> mCaptured; // per copy (instance id), per prim + std::vector> mOldImported; // per part: the old file's materials }; -std::vector capturePreservedFaces(LLVOVolume* vol) +namespace { - std::vector out; +// Re-apply the user's in-world face edits on top of the freshly applied file +// materials. applyPartGeometry() has just written the NEW file's imported state +// (texture/color/fullbright/render material); for those fields a captured value +// is restored only when it differs from what the OLD file had imported -- i.e. +// only when the user actually changed it. Restoring them wholesale overwrote +// every file-level material edit with pre-reload state, so material changes in +// the source file never showed on spawned copies. Fields no import can author +// (glow, texture transforms, bump/shiny, Blinn-Phong maps, glTF overrides) are +// user state, restored as-is. +void restorePreservedFaces(LLVOVolume* vol, + const LLLocalMeshPreSwapSnapshot::PrimFaces& saved, + const std::vector* old_imported) +{ + LLLocalBitmapMgr* bitmap_mgr = LLLocalBitmapMgr::getInstance(); const U8 n = vol->getNumTEs(); - out.reserve(n); - for (U8 i = 0; i < n; ++i) + bool any_render_mat = false; + for (U8 i = 0; i < n && i < (U8)saved.size(); ++i) { - PreservedFace pf; - if (const LLTextureEntry* tep = vol->getTE(i)) + const LLTextureEntry& te = saved[i].mTE; + + // What the OLD file had applied to this face, with applyPartGeometry()'s + // fallbacks -- captured values equal to these are imports, not user edits. + const LLLocalMeshFaceMaterial* old_fm = + (old_imported && i < (U8)old_imported->size()) ? &(*old_imported)[i] : nullptr; + const LLUUID old_tex = (old_fm && old_fm->mDiffuseID.notNull()) ? old_fm->mDiffuseID : IMG_DEFAULT; + const LLColor4 old_color = old_fm ? old_fm->mDiffuseColor : LLColor4::white; + const bool old_fullbright = old_fm && old_fm->mFullbright; + const LLUUID old_render_mat = old_fm ? old_fm->mRenderMaterialID : LLUUID(); + + // Local-bitmap world ids rotate on the texture's own live reload; compare + // tracking ids too so that churn doesn't read as a user edit. + bool tex_is_user_edit = te.getID() != old_tex; + if (tex_is_user_edit) { - pf.te = *tep; + const LLUUID cap_track = bitmap_mgr->getTrackingID(te.getID()); + tex_is_user_edit = cap_track.isNull() || cap_track != bitmap_mgr->getTrackingID(old_tex); + } + if (tex_is_user_edit) + { + vol->setTETexture(i, te.getID()); + } + if (te.getColor() != old_color) + { + vol->setTEColor(i, te.getColor()); + } + if (((bool)te.getFullbright()) != old_fullbright) + { + vol->setTEFullbright(i, te.getFullbright()); } - pf.render_mat_id = vol->getRenderMaterialID(i); - out.push_back(pf); - } - return out; -} -void restorePreservedFaces(LLVOVolume* vol, const std::vector& saved) -{ - const U8 n = vol->getNumTEs(); - bool any_render_mat = false; - for (U8 i = 0; i < n && i < (U8)saved.size(); ++i) - { - const LLTextureEntry& te = saved[i].te; - vol->setTETexture(i, te.getID()); - vol->setTEColor(i, te.getColor()); + // User-only state: no import path authors these. vol->setTEGlow(i, te.getGlow()); - vol->setTEBumpShinyFullbright(i, te.getBumpShinyFullbright()); + vol->setTEBumpmap(i, te.getBumpmap()); + vol->setTEShiny(i, te.getShiny()); F32 ss = 1.f, st = 1.f, os = 0.f, ot = 0.f; te.getScale(&ss, &st); te.getOffset(&os, &ot); @@ -1601,17 +1747,20 @@ void restorePreservedFaces(LLVOVolume* vol, const std::vector& sa { vol->setTEMaterialParams(i, te.getMaterialParams()); // Blinn-Phong normal/specular } - if (saved[i].render_mat_id.notNull()) + + if (saved[i].mRenderMatID.notNull() && saved[i].mRenderMatID != old_render_mat) { - // glTF PBR material (+ override); client-only, so update_server=false. - vol->setRenderMaterialID((S32)i, saved[i].render_mat_id, false, true); - if (const LLGLTFMaterial* ov = te.getGLTFMaterialOverride()) - { - LLPointer ovp = new LLGLTFMaterial(*ov); - vol->setTEGLTFMaterialOverride(i, ovp); - } + // The user applied this material over the import; keep it showing. + // Client-only, so update_server=false. + vol->setRenderMaterialID((S32)i, saved[i].mRenderMatID, false, true); any_render_mat = true; } + if (const LLGLTFMaterial* ov = te.getGLTFMaterialOverride()) + { + // Overrides are user edits (imports never author them). + LLPointer ovp = new LLGLTFMaterial(*ov); + vol->setTEGLTFMaterialOverride(i, ovp); + } } if (any_render_mat) { @@ -1621,7 +1770,48 @@ void restorePreservedFaces(LLVOVolume* vol, const std::vector& sa } } // namespace -bool LLLocalMeshMgr::hotSwapInWorld(const LLUUID& tracking_id) +void LLLocalMeshMgr::capturePreSwap(const LLUUID& tracking_id, const LLLocalMesh& unit, + LLLocalMeshPreSwapSnapshot& out) const +{ + for (const LLLocalMeshPart& part : unit.getParts()) + { + out.mOldImported.push_back(part.mFaceMaterials); + } + for (const auto& entry : mSpawnedCopies) + { + const SpawnedCopy& copy = entry.second; + if (copy.mTrackingID != tracking_id) + { + continue; + } + std::vector prims; + prims.reserve(copy.mPrims.size()); + for (const LLPointer& p : copy.mPrims) + { + LLLocalMeshPreSwapSnapshot::PrimFaces faces; + LLVOVolume* vol = (p.notNull() && !p->isDead()) ? dynamic_cast(p.get()) : nullptr; + if (vol) + { + const U8 n = vol->getNumTEs(); + faces.reserve(n); + for (U8 i = 0; i < n; ++i) + { + LLLocalMeshPreSwapSnapshot::Face f; + if (const LLTextureEntry* tep = vol->getTE(i)) + { + f.mTE = *tep; + } + f.mRenderMatID = vol->getRenderMaterialID(i); + faces.push_back(f); + } + } + prims.push_back(std::move(faces)); + } + out.mCaptured.emplace(entry.first, std::move(prims)); + } +} + +bool LLLocalMeshMgr::hotSwapInWorld(const LLUUID& tracking_id, const LLLocalMeshPreSwapSnapshot& pre_swap) { LLLocalMesh* unit = getUnit(tracking_id); if (!unit || !unit->getValid()) @@ -1672,12 +1862,17 @@ bool LLLocalMeshMgr::hotSwapInWorld(const LLUUID& tracking_id) return false; // a dead/unexpected prim -> let the caller re-spawn } vol->setScale(parts[i].mScale, false); - // Preserve user-applied face params (diffuse/normal/specular/glow/color/ - // render material/transforms) across the swap -- applyPartGeometry resets - // every face to the file's imported material. - std::vector preserved = capturePreservedFaces(vol); + // Apply the new file's geometry + materials, then diff-restore the + // user's face edits over them (see restorePreservedFaces). The live + // state was captured pre-ingest in onLoadResult. applyPartGeometry(vol, parts[i]); - restorePreservedFaces(vol, preserved); + auto cap_it = pre_swap.mCaptured.find(entry.first); + if (cap_it != pre_swap.mCaptured.end() && i < cap_it->second.size()) + { + const std::vector* old_imported = + (i < pre_swap.mOldImported.size()) ? &pre_swap.mOldImported[i] : nullptr; + restorePreservedFaces(vol, cap_it->second[i], old_imported); + } } } @@ -1918,6 +2113,17 @@ void LLLocalMeshMgr::onLoadResult(const LLUUID& tracking_id, LLModelLoader::scen const bool reloading = unit->isReloading(); const bool parse_ok = (load_state < LLModelLoader::ERROR_PARSING) && !scene.empty(); + + // Snapshot the spawned copies' live face state and the old parts' imported + // materials BEFORE ingestScene commits: the hot-swap diff-restore needs both + // to tell user edits from file changes, and ingest's release of dropped + // imports already resets live faces as the dying units tear down. + LLLocalMeshPreSwapSnapshot pre_swap; + if (reloading && parse_ok) + { + capturePreSwap(tracking_id, *unit, pre_swap); + } + const bool assembled = parse_ok && unit->ingestScene(scene); if (!parse_ok) @@ -1937,7 +2143,7 @@ void LLLocalMeshMgr::onLoadResult(const LLUUID& tracking_id, LLModelLoader::scen // if the preview was worn). Not spawned -> just keep the rebuilt data. if (getSpawnedRoot(tracking_id)) { - if (!hotSwapInWorld(tracking_id)) + if (!hotSwapInWorld(tracking_id, pre_swap)) { respawnInstancesInPlace(tracking_id); // prim count changed -> re-rez each copy } @@ -2016,7 +2222,7 @@ void LLLocalMeshMgr::doUpdates() } } -void LLLocalMeshMgr::addAndSpawn(const std::vector& filenames) +void LLLocalMeshMgr::addAndSpawn(const std::vector& filenames, bool include_joints) { for (const std::string& filename : filenames) { @@ -2024,7 +2230,7 @@ void LLLocalMeshMgr::addAndSpawn(const std::vector& filenames) { continue; } - const LLUUID tracking_id = addUnit(filename); + const LLUUID tracking_id = addUnit(filename, include_joints); if (tracking_id.notNull()) { if (LLLocalMesh* unit = getUnit(tracking_id)) @@ -2035,13 +2241,13 @@ void LLLocalMeshMgr::addAndSpawn(const std::vector& filenames) } } -void LLLocalMeshMgr::addAndAttach(const std::string& filename, S32 attach_point) +void LLLocalMeshMgr::addAndAttach(const std::string& filename, S32 attach_point, bool include_joints) { if (filename.empty()) { return; } - const LLUUID tracking_id = addUnit(filename); // dedups: existing unit if already loaded + const LLUUID tracking_id = addUnit(filename, include_joints); // dedups: existing unit if already loaded LLLocalMesh* unit = tracking_id.notNull() ? getUnit(tracking_id) : nullptr; if (!unit) { diff --git a/indra/newview/lllocalmesh.h b/indra/newview/lllocalmesh.h index 43a3fbff02..09adb58999 100644 --- a/indra/newview/lllocalmesh.h +++ b/indra/newview/lllocalmesh.h @@ -60,6 +60,7 @@ class LLViewerRegion; class LLVOAvatar; class LLVOVolume; class LLVolume; +struct LLLocalMeshPreSwapSnapshot; // hot-swap diff-restore state (lllocalmesh.cpp) // One uploadable sub-mesh of a local mesh file: a single LLModel's geometry, // normalized to a unit box (as the upload path does), plus where it sits within @@ -72,10 +73,11 @@ class LLVolume; // world id applied as the face's render material. struct LLLocalMeshFaceMaterial { - LLUUID mDiffuseID; // local-bitmap world id (Blinn-Phong), or null - LLColor4 mDiffuseColor = LLColor4::white; - bool mFullbright = false; - LLUUID mRenderMaterialID; // local-gltf world id (glTF) -> face render material, or null + LLUUID mDiffuseID; // local-bitmap world id (Blinn-Phong), or null + LLColor4 mDiffuseColor = LLColor4::white; + bool mFullbright = false; + LLUUID mRenderMaterialID; // local-gltf world id (glTF) -> face render material, or null + std::string mMaterialName; // glTF binding name, to re-resolve after a material-file regroup }; struct LLLocalMeshPart @@ -265,11 +267,18 @@ class LLLocalMeshMgr : public LLSingleton // agent. A unit can be rezzed any number of times; each copy is independent with // its own instance id. Returns the new copy's linkset root. LLViewerObject* spawnInWorld(const LLUUID& tracking_id); + // Rez a NEW copy of the preview that owns `obj`, at the source copy's transform + // plus `offset` -- the local-object route for the build tools' Duplicate + // (Ctrl+D / shift-drag-copy). Worn copies don't duplicate, matching the sim + // path. Returns the new copy's linkset root, or null. + LLViewerObject* duplicatePreview(LLViewerObject* obj, const LLVector3& offset); // Convenience: load each file and spawn it in-world once it finishes loading. - void addAndSpawn(const std::vector& filenames); + // include_joints applies to any file that needs a fresh decode (pass the + // persisted joint-position flag for saved rows, like loadPath does). + void addAndSpawn(const std::vector& filenames, bool include_joints = false); // Load a file (if needed) and, once decoded, spawn it and wear it at the given // attachment point -- so Attach works on a not-yet-decoded unit. - void addAndAttach(const std::string& filename, S32 attach_point); + void addAndAttach(const std::string& filename, S32 attach_point, bool include_joints = false); // The root of (any) one rezzed copy of a unit, or null if it has none -- handy for // "is this unit in-world?" checks. Use getSpawnedInstances() to act per copy. @@ -330,6 +339,13 @@ class LLLocalMeshMgr : public LLSingleton // re-creates them in the current region. void despawnObjectsInRegion(LLViewerRegion* regionp); + // A local material file's name -> world id mapping changed (live-edit regroup + // in LLLocalGLTFMaterialMgr): re-resolve this mesh file's stored face + // materials by their binding names and re-point spawned faces at the new ids. + // Faces the user re-materialed in-world are left alone -- only faces still + // showing the previously imported id move (the hot-swap diff-restore rule). + void rebindFaceMaterials(const std::string& filename); + private: LLUUID addUnitInternal(const std::string& filename, bool include_joints = false); void despawnUnit(const LLUUID& tracking_id); @@ -337,8 +353,16 @@ class LLLocalMeshMgr : public LLSingleton // Reload every spawned copy of a unit in place by pointing each existing prim at // the freshly decoded geometry (new sculpt id) instead of despawning -- so // attachment, selection and transform are preserved and there's no flicker. + // pre_swap carries the state captured before ingestScene committed; the + // diff-restore re-applies only the user's face edits over the new file state. // Returns false if the prim count changed (caller falls back to a re-spawn). - bool hotSwapInWorld(const LLUUID& tracking_id); + bool hotSwapInWorld(const LLUUID& tracking_id, const LLLocalMeshPreSwapSnapshot& pre_swap); + // Snapshot, BEFORE a reload's ingestScene commits, everything the hot-swap + // diff-restore needs: each spawned prim's live face state and the old parts' + // imported materials (ingest releases dropped imports, whose teardown already + // resets live faces -- a later capture would mistake that for user state). + void capturePreSwap(const LLUUID& tracking_id, const LLLocalMesh& unit, + LLLocalMeshPreSwapSnapshot& out) const; // Re-rez every existing copy of a unit at its current transform/attachment, used // when a reload changes the prim count so hot-swap can't swap 1:1. void respawnInstancesInPlace(const LLUUID& tracking_id); diff --git a/indra/newview/llmeshrepository.cpp b/indra/newview/llmeshrepository.cpp index 14ca3d947c..81691b65d4 100644 --- a/indra/newview/llmeshrepository.cpp +++ b/indra/newview/llmeshrepository.cpp @@ -4604,10 +4604,10 @@ void LLMeshRepository::notifyLoadedMeshes() { //// Clean up dead skin info //U64Bytes skinbytes(0); + std::vector culled_ids; for (auto iter = mSkinMap.begin(), ender = mSkinMap.end(); iter != ender;) { auto copy_iter = iter++; - LLUUID id = copy_iter->first; //skinbytes += U64Bytes(sizeof(LLMeshSkinInfo)); //skinbytes += U64Bytes(copy_iter->second->mJointNames.size() * sizeof(std::string)); @@ -4615,16 +4615,28 @@ void LLMeshRepository::notifyLoadedMeshes() //skinbytes += U64Bytes(copy_iter->second->mJointNames.size() * sizeof(LLMatrix4a)); //skinbytes += U64Bytes(copy_iter->second->mJointNames.size() * sizeof(LLMatrix4)); + // The repo thread's mirror is erased only for skins actually evicted + // here: erasing it for every iterated skin emptied the mirror within + // one tick of arrival -- defeating its per-joint bounding-box use for + // any volume loading later than that -- and posted one work item per + // cached skin every tick. if (copy_iter->second->getNumRefs() == 1) { + culled_ids.push_back(copy_iter->first); mSkinMap.erase(copy_iter); } + } + if (!culled_ids.empty()) + { // erase from background thread - mThread->mWorkQueue.post([=, this]() + mThread->mWorkQueue.post([ids = std::move(culled_ids), this]() { LLMutexLock skin_lock(mThread->mSkinMapMutex); - mThread->mSkinMap.erase(id); + for (const LLUUID& id : ids) + { + mThread->mSkinMap.erase(id); + } }); } //LL_INFOS() << "Skin info cache elements:" << mSkinMap.size() << " Memory: " << U64Kilobytes(skinbytes) << LL_ENDL; @@ -4968,6 +4980,14 @@ void LLMeshRepository::fetchPhysicsShape(const LLUUID& mesh_id) { LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK; //LL_RECORD_BLOCK_TIME(FTM_MESH_FETCH); + // Local mesh previews have no server-side decomposition; don't queue a repo + // request that can never resolve (the id would sit in mLoadingPhysicsShapes + // forever, retrying). + if (LLLocalMeshMgr::instanceExists() && LLLocalMeshMgr::getInstance()->isLocal(mesh_id)) + { + return; + } + if (mesh_id.notNull()) { LLModel::Decomposition* decomp = NULL; @@ -4998,6 +5018,14 @@ LLModel::Decomposition* LLMeshRepository::getDecomposition(const LLUUID& mesh_id LLModel::Decomposition* ret = NULL; + // Local mesh previews have no server-side decomposition; don't queue a repo + // request that can never resolve (the id would sit in mLoadingDecompositions + // forever, retrying). + if (LLLocalMeshMgr::instanceExists() && LLLocalMeshMgr::getInstance()->isLocal(mesh_id)) + { + return ret; + } + if (mesh_id.notNull()) { decomposition_map::iterator iter = mDecompositionMap.find(mesh_id); @@ -5046,6 +5074,13 @@ bool LLMeshRepository::hasPhysicsShape(const LLUUID& mesh_id) return false; } + // Local mesh previews have no server physics shape (and must not trigger the + // decomposition fetch below). + if (LLLocalMeshMgr::instanceExists() && LLLocalMeshMgr::getInstance()->isLocal(mesh_id)) + { + return false; + } + if (mThread->hasPhysicsShapeInHeader(mesh_id)) { return true; diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index 402b6c9202..c35130d22a 100644 --- a/indra/newview/llselectmgr.cpp +++ b/indra/newview/llselectmgr.cpp @@ -4704,6 +4704,52 @@ void LLSelectMgr::selectDuplicate(const LLVector3& offset, bool select_copy) return; } } + + // Client-only local previews: duplicate locally. The ObjectDuplicate send + // below is swallowed for an all-local selection (sendListToRegions gate), + // which would otherwise make Ctrl+D / shift-drag-copy a silent no-op. + if (selectionAllLocalPreview(mSelectedObjects)) + { + // Snapshot the source roots first: spawning fires the manager's + // units-changed signal, whose listeners must not invalidate this walk. + std::vector> src_roots; + for (LLObjectSelection::root_iterator it = getSelection()->root_begin(); + it != getSelection()->root_end(); ++it) + { + src_roots.emplace_back((*it)->getObject()); + } + std::vector new_roots; + for (const LLPointer& src : src_roots) + { + if (LLViewerObject* new_root = LLLocalMeshMgr::getInstance()->duplicatePreview(src.get(), offset)) + { + new_roots.push_back(new_root); + } + } + if (select_copy && !new_roots.empty()) + { + deselectAll(); + for (LLViewerObject* new_root : new_roots) + { + selectObjectAndFamily(new_root); + } + } + else if (!select_copy) + { + // Mirror the sim path's bookkeeping below so repeatDuplicate() + // recognizes these as duplicated and chains the copy/move sequence. + for (LLObjectSelection::root_iterator it = getSelection()->root_begin(); + it != getSelection()->root_end(); ++it) + { + LLSelectNode* node = *it; + node->mDuplicated = true; + node->mDuplicatePos = node->getObject()->getPositionGlobal(); + node->mDuplicateRot = node->getObject()->getRotation(); + } + } + return; + } + LLDuplicateData data; data.offset = offset; @@ -4759,12 +4805,32 @@ void LLSelectMgr::repeatDuplicate() } // duplicate objects in place - LLDuplicateData data; + if (selectionAllLocalPreview(mSelectedObjects)) + { + // Client-only local previews: copy each in place locally -- the gated + // ObjectDuplicate send below is swallowed for an all-local selection + // (see selectDuplicate). The shared move-by-delta loop below then + // advances the selection exactly like the sim path. + std::vector> src_roots; + for (LLObjectSelection::root_iterator iter = getSelection()->root_begin(); + iter != getSelection()->root_end(); ++iter) + { + src_roots.emplace_back((*iter)->getObject()); + } + for (const LLPointer& src : src_roots) + { + LLLocalMeshMgr::getInstance()->duplicatePreview(src.get(), LLVector3::zero); + } + } + else + { + LLDuplicateData data; - data.offset = LLVector3::zero; - data.flags = 0x0; + data.offset = LLVector3::zero; + data.flags = 0x0; - sendListToRegions("ObjectDuplicate", packDuplicateHeader, packDuplicate, logNoOp, &data, SEND_ONLY_ROOTS); + sendListToRegions("ObjectDuplicate", packDuplicateHeader, packDuplicate, logNoOp, &data, SEND_ONLY_ROOTS); + } // move current selection based on delta from duplication position and update duplication position for (LLObjectSelection::root_iterator iter = getSelection()->root_begin(); @@ -5942,7 +6008,9 @@ void LLSelectMgr::sendListToRegions(LLObjectSelectionHandle selected_handle, // Client-only local mesh previews never talk to the simulator. Short-circuit // every object message marshalled through here (name/description/permissions/ // sale/group/owner/click-action/category/shape/flags/...). Local derez, - // duplicate and attach are handled by LLLocalMeshMgr before reaching this path. + // duplicate and attach are intercepted upstream (selectDelete, selectDuplicate + // and the attach menu route to LLLocalMeshMgr); anything else marshalled here + // for an all-local selection is intentionally dropped. if (selectionAllLocalPreview(selected_handle)) { return; @@ -8039,6 +8107,21 @@ bool LLSelectMgr::canSelectObject(LLViewerObject* object, bool ignore_select_own return false; } + // Don't allow mixing client-only local mesh previews with real (sim) objects + // in a single selection -- a mixed selection escapes the all-local gating and + // would send the previews' (fake) local IDs to the sim. Checked ahead of + // mForceSelection so temp/right-click (forced) selects can't sneak a mix in; + // the selection stays homogeneous, so the first selected object is + // representative. + if (mSelectedObjects->getObjectCount() > 0) + { + LLViewerObject* selected = mSelectedObjects->getFirstObject(); + if (selected && selected->isLocalOnly() != object->isLocalOnly()) + { + return false; + } + } + if (mForceSelection) { return true; @@ -8067,19 +8150,6 @@ bool LLSelectMgr::canSelectObject(LLViewerObject* object, bool ignore_select_own ESelectType selection_type = getSelectTypeForObject(object); if (mSelectedObjects->getObjectCount() > 0 && mSelectedObjects->mSelectType != selection_type) return false; - // Don't allow mixing client-only local mesh previews with real (sim) objects - // in a single selection -- a mixed selection escapes the all-local gating and - // would send the previews' (fake) local IDs to the sim. The selection is kept - // homogeneous by this check, so the first selected object is representative. - if (mSelectedObjects->getObjectCount() > 0) - { - LLViewerObject* selected = mSelectedObjects->getFirstObject(); - if (selected && selected->isLocalOnly() != object->isLocalOnly()) - { - return false; - } - } - return true; } @@ -9039,6 +9109,13 @@ void LLSelectMgr::sendSelectionMove() return; } + // Client-only local previews are moved purely locally (this is the + // joystick/spacenav build-move path); there is no sim object to update. + if (selectionAllLocalPreview(mSelectedObjects)) + { + return; + } + //saveSelectedObjectTransform(SELECT_ACTION_TYPE_PICK); U32 update_type = UPD_POSITION | UPD_ROTATION; diff --git a/indra/newview/llspatialpartition.h b/indra/newview/llspatialpartition.h index 1cb35c391c..ff90725981 100644 --- a/indra/newview/llspatialpartition.h +++ b/indra/newview/llspatialpartition.h @@ -102,7 +102,14 @@ class alignas(16) LLDrawInfo final : public LLRefCount const LLMatrix4* mModelMatrix = nullptr; LLPointer mAvatar = nullptr; - LLMeshSkinInfo* mSkinInfo = nullptr; + // Owning, like mAvatar above: the mesh repo culls skins whose only + // remaining reference is its own map (and a local-mesh reload drops the + // unit/volume references mid-frame), so the draw call must hold its skin + // alive itself for the per-frame matrix-palette upload. LLFace::mSkinInfo + // stays a raw mirror (like its mAvatar): faces and their group's draw + // infos are (re)built together, so a face's skin is always pinned by its + // volume or by the co-generated draw-info references here. + LLPointer mSkinInfo; // Material pointer here is likely for debugging only and are immaterial (zing!) LLPointer mMaterial; diff --git a/indra/newview/lltexturectrl.cpp b/indra/newview/lltexturectrl.cpp index ca96c56054..3874f0b155 100644 --- a/indra/newview/lltexturectrl.cpp +++ b/indra/newview/lltexturectrl.cpp @@ -1200,33 +1200,36 @@ void LLFloaterTexturePicker::onBtnAdd(void* userdata) void LLFloaterTexturePicker::onBtnRemove(void* userdata) { LLFloaterTexturePicker* self = (LLFloaterTexturePicker*) userdata; - std::vector selected_items = self->mLocalScrollCtrl->getAllSelected(); - if (!selected_items.empty()) + // Snapshot the selected units' values BEFORE mutating the managers: delUnit() + // fires the units-changed signal synchronously, which re-enters this picker + // via onLocalAssetsChanged() -> refreshLocalList() -> clearRows() and frees + // every LLScrollListItem a pointer snapshot would still be iterating. + std::vector> to_remove; + for (LLScrollListItem* list_item : self->mLocalScrollCtrl->getAllSelected()) { - - for(std::vector::iterator iter = selected_items.begin(); - iter != selected_items.end(); iter++) + if (list_item) { - LLScrollListItem* list_item = *iter; - if (list_item) + const LLSD data = list_item->getValue(); + if (data["mesh_owned"].asBoolean()) { - LLSD data = list_item->getValue(); - if (data["mesh_owned"].asBoolean()) - { - continue; // model-loaded: read-only, owned by its mesh - } - LLUUID tracking_id = data["id"]; - S32 asset_type = data["type"].asInteger(); + continue; // model-loaded: read-only, owned by its mesh + } + to_remove.emplace_back(data["id"].asUUID(), data["type"].asInteger()); + } + } - if (LLAssetType::AT_MATERIAL == asset_type) - { - LLLocalGLTFMaterialMgr::getInstance()->delUnit(tracking_id); - } - else - { - LLLocalBitmapMgr::getInstance()->delUnit(tracking_id); - } + if (!to_remove.empty()) + { + for (const auto& [tracking_id, asset_type] : to_remove) + { + if (LLAssetType::AT_MATERIAL == asset_type) + { + LLLocalGLTFMaterialMgr::getInstance()->delUnit(tracking_id); + } + else + { + LLLocalBitmapMgr::getInstance()->delUnit(tracking_id); } } @@ -1588,8 +1591,22 @@ void LLFloaterTexturePicker::onLocalAssetsChanged() { if (still_valid) { - // Keep the user's selection highlighted across the rebuild. - mLocalScrollCtrl->setSelectedByValue(sel_value, true); + // Keep the user's selection highlighted across the rebuild. The rows + // carry map-valued LLSD, which setSelectedByValue() compares via + // asString() -- a map stringifies to "", so it would just highlight + // the first row. Match the unit id + type manually instead. + const LLUUID sel_id = sel_value["id"].asUUID(); + const S32 sel_type = sel_value["type"].asInteger(); + std::vector items = mLocalScrollCtrl->getAllData(); + for (size_t i = 0; i < items.size(); ++i) + { + const LLSD v = items[i]->getValue(); + if (v.has("id") && v["id"].asUUID() == sel_id && v["type"].asInteger() == sel_type) + { + mLocalScrollCtrl->selectNthItem((S32)i); + break; + } + } } else { diff --git a/indra/newview/lltoolgrab.cpp b/indra/newview/lltoolgrab.cpp index a23cf70795..44bf95c9ee 100644 --- a/indra/newview/lltoolgrab.cpp +++ b/indra/newview/lltoolgrab.cpp @@ -339,8 +339,9 @@ bool LLToolGrabBase::handleObjectHit(const LLPickInfo& info) void LLToolGrabBase::startSpin() { LLViewerObject* objectp = mGrabPick.getObject(); - if (!objectp) + if (!objectp || objectp->isLocalOnly()) { + // Client-only local previews have no sim object to spin. return; } mSpinGrabbing = true; @@ -365,7 +366,7 @@ void LLToolGrabBase::stopSpin() mSpinGrabbing = false; LLViewerObject* objectp = mGrabPick.getObject(); - if (!objectp) + if (!objectp || objectp->isLocalOnly()) { return; } @@ -598,16 +599,22 @@ void LLToolGrabBase::handleHoverActive(S32 x, S32 y, MASK mask) mSpinRotation = mSpinRotation * rotation_around_vertical; mSpinRotation = mSpinRotation * rotation_around_left; - // TODO: Throttle these - LLMessageSystem *msg = gMessageSystem; - msg->newMessageFast(_PREHASH_ObjectSpinUpdate); - msg->nextBlockFast(_PREHASH_AgentData); - msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID() ); - msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID()); - msg->nextBlockFast(_PREHASH_ObjectData); - msg->addUUIDFast(_PREHASH_ObjectID, objectp->getID() ); - msg->addQuatFast(_PREHASH_Rotation, mSpinRotation ); - msg->sendMessage( objectp->getRegion()->getHost() ); + // Client-only local previews have no sim object to spin. The toggle + // above re-sets mSpinGrabbing from the mask every hover, so + // startSpin()'s early-out alone doesn't keep us out of this block. + if (!objectp->isLocalOnly()) + { + // TODO: Throttle these + LLMessageSystem *msg = gMessageSystem; + msg->newMessageFast(_PREHASH_ObjectSpinUpdate); + msg->nextBlockFast(_PREHASH_AgentData); + msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID() ); + msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID()); + msg->nextBlockFast(_PREHASH_ObjectData); + msg->addUUIDFast(_PREHASH_ObjectID, objectp->getID() ); + msg->addQuatFast(_PREHASH_Rotation, mSpinRotation ); + msg->sendMessage( objectp->getRegion()->getHost() ); + } } else { @@ -715,8 +722,10 @@ void LLToolGrabBase::handleHoverActive(S32 x, S32 y, MASK mask) } } - // Don't move above top of screen or below bottom - if ((grab_center_gl.mY < gViewerWindow->getWorldViewHeightScaled() - 6) + // Don't move above top of screen or below bottom; client-only local + // previews have no sim object to update. + if (!objectp->isLocalOnly() + && (grab_center_gl.mY < gViewerWindow->getWorldViewHeightScaled() - 6) && (grab_center_gl.mY > 24)) { // Transmit update to simulator @@ -889,7 +898,7 @@ void LLToolGrabBase::handleHoverNonPhysical(S32 x, S32 y, MASK mask) changed_since_last_update = true; } - if (changed_since_last_update) + if (changed_since_last_update && !objectp->isLocalOnly()) { LLMessageSystem *msg = gMessageSystem; msg->newMessageFast(_PREHASH_ObjectGrabUpdate); @@ -1173,7 +1182,8 @@ LLVector3d LLToolGrabBase::getGrabPointGlobal() void send_ObjectGrab_message(LLViewerObject* object, const LLPickInfo & pick, const LLVector3 &grab_offset) { - if (!object) return; + // Client-only local previews have no sim object; localid 0 means nothing there. + if (!object || object->isLocalOnly()) return; LLMessageSystem *msg = gMessageSystem; @@ -1210,7 +1220,7 @@ void send_ObjectGrab_message(LLViewerObject* object, const LLPickInfo & pick, co void send_ObjectDeGrab_message(LLViewerObject* object, const LLPickInfo & pick) { - if (!object) return; + if (!object || object->isLocalOnly()) return; LLMessageSystem *msg = gMessageSystem; diff --git a/indra/newview/llviewerjointattachment.cpp b/indra/newview/llviewerjointattachment.cpp index 4a625a4e71..4d3b6d614c 100644 --- a/indra/newview/llviewerjointattachment.cpp +++ b/indra/newview/llviewerjointattachment.cpp @@ -184,11 +184,13 @@ bool LLViewerJointAttachment::addObject(LLViewerObject* object) // Two instances of the same inventory item attached -- // Request detach, and kill the object in the meantime. - // (Skip for client-only objects: a local mesh preview has no inventory item, - // so its id is null and the dedup would markDead an unrelated null-id local - // attachment and send it an ObjectDetach the sim can't act on.) + // (Skip when EITHER side is a client-only object: a local mesh preview has + // no inventory item, so its null id would cross-match a real null-item-id + // attachment -- e.g. a temporary attachment -- in both directions, killing + // an unrelated object and sending an ObjectDetach the sim can't act on.) // [SL:KB] - Patch: Appearance-PhantomAttach | Checked: Catznip-5.0 - if (LLViewerObject* pAttachObj = (!object->isLocalOnly()) ? getAttachedObject(object->getAttachmentItemID()) : nullptr) + LLViewerObject* pAttachObj = (!object->isLocalOnly()) ? getAttachedObject(object->getAttachmentItemID()) : nullptr; + if (pAttachObj && !pAttachObj->isLocalOnly()) { LL_INFOS() << "(same object re-attached)" << LL_ENDL; pAttachObj->markDead(); diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index 60968232d2..a1fc41cbc0 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -1584,7 +1584,7 @@ LLWindowCallbacks::DragNDropResult LLViewerWindow::handleDragNDropFile(LLWindow if (LLAssetType::AT_TEXTURE == mDragItems.front().first->getType()) { - LLUUID idLocalBitmap = LLLocalBitmapMgr::instance().getUnitID(strFilename); + LLUUID idLocalBitmap = LLLocalBitmapMgr::instance().getUnitID(strFilename, false); if (idLocalBitmap.isNull()) { idLocalBitmap = LLLocalBitmapMgr::instance().addUnit(strFilename); @@ -1598,7 +1598,7 @@ LLWindowCallbacks::DragNDropResult LLViewerWindow::handleDragNDropFile(LLWindow } else if (LLAssetType::AT_MATERIAL == mDragItems.front().first->getType()) { - LLUUID isLocalMat = LLLocalGLTFMaterialMgr::instance().getUnitID(strFilename, 0); + LLUUID isLocalMat = LLLocalGLTFMaterialMgr::instance().getUnitID(strFilename, 0, false); if (isLocalMat.isNull()) { LLLocalGLTFMaterialMgr::instance().addUnit(strFilename, isLocalMat);