From dddce2b568e31497f84deede43fcb6ae76cb0ae2 Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Sun, 14 Sep 2025 20:47:28 +0100 Subject: [PATCH 01/34] FIRE-35794: First pass at restoring pose state Updated save version: now saves all the playing poses and their times, making 'diffs' much more useful. Loading attempts to replay the pose at that time; making several tries if needed. These poses restore the 'base' rotation state (position needs work). --- indra/newview/CMakeLists.txt | 2 + indra/newview/fsfloaterposer.cpp | 102 ++++++- indra/newview/fsfloaterposer.h | 28 +- indra/newview/fsjointpose.cpp | 70 ++++- indra/newview/fsjointpose.h | 155 ++++++++++- indra/newview/fsposeranimator.cpp | 115 +++++++- indra/newview/fsposeranimator.h | 93 +++++-- indra/newview/fsposestate.cpp | 249 ++++++++++++++++++ indra/newview/fsposestate.h | 158 +++++++++++ indra/newview/fsposingmotion.cpp | 110 +++++++- indra/newview/fsposingmotion.h | 61 ++++- .../skins/default/xui/en/floater_fs_poser.xml | 13 +- 12 files changed, 1091 insertions(+), 65 deletions(-) create mode 100644 indra/newview/fsposestate.cpp create mode 100644 indra/newview/fsposestate.h diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index ae16fda157..ee296bb93f 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -167,6 +167,7 @@ set(viewer_SOURCE_FILES fsparticipantlist.cpp fspose.cpp fsposeranimator.cpp + fsposestate.cpp fsposingmotion.cpp fsprimfeedauth.cpp fsradar.cpp @@ -1007,6 +1008,7 @@ set(viewer_HEADER_FILES fsparticipantlist.h fspose.h fsposeranimator.h + fsposestate.h fsposingmotion.h fsprimfeedauth.h fsradar.h diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 7594f57960..f5104ea2d5 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -45,6 +45,7 @@ #include "llvoavatarself.h" #include "llinventoryfunctions.h" #include "lltoolcomp.h" +#include "llloadingindicator.h" namespace { @@ -110,6 +111,8 @@ FSFloaterPoser::FSFloaterPoser(const LLSD& key) : LLFloater(key) mCommitCallbackRegistrar.add("Poser.CommitSpinner", [this](LLUICtrl* spinner, const LLSD& data) { onCommitSpinner(spinner, data); }); mCommitCallbackRegistrar.add("Poser.CommitSlider", [this](LLUICtrl* slider, const LLSD& data) { onCommitSlider(slider, data); }); mCommitCallbackRegistrar.add("Poser.Symmetrize", [this](LLUICtrl*, const LLSD& data) { onClickSymmetrize(data); }); + + mLoadPoseTimer = new FSLoadPoseTimer(boost::bind(&FSFloaterPoser::timedReload, this)); } bool FSFloaterPoser::postBuild() @@ -253,7 +256,6 @@ void FSFloaterPoser::onOpen(const LLSD& key) LLFloater::onOpen(key); } - void FSFloaterPoser::onFocusReceived() { LLEditMenuHandler::gEditMenuHandler = this; @@ -302,6 +304,7 @@ void FSFloaterPoser::onClose(bool app_quitting) } disableVisualManipulators(); + delete mLoadPoseTimer; LLFloater::onClose(app_quitting); } @@ -512,9 +515,11 @@ bool FSFloaterPoser::savePoseToXml(LLVOAvatar* avatar, const std::string& poseFi { bool savingDiff = !mPoserAnimator.allBaseRotationsAreZero(avatar); LLSD record; - record["version"]["value"] = (S32)6; + record["version"]["value"] = (S32)7; record["startFromTeePose"]["value"] = !savingDiff; + mPoserAnimator.savePosingState(avatar, &record); + LLVector3 rotation, position, scale, zeroVector; bool baseRotationIsZero; @@ -523,12 +528,15 @@ bool FSFloaterPoser::savePoseToXml(LLVOAvatar* avatar, const std::string& poseFi std::string bone_name = pj.jointName(); bool posingThisJoint = mPoserAnimator.isPosingAvatarJoint(avatar, pj); bool jointRotLocked = mPoserAnimator.getRotationIsWorldLocked(avatar, pj); + bool jointRotMirrored = mPoserAnimator.getRotationIsMirrored(avatar, pj); - record[bone_name] = bone_name; - record[bone_name]["enabled"] = posingThisJoint; + record[bone_name] = bone_name; + record[bone_name]["enabled"] = posingThisJoint; if (!posingThisJoint) continue; + record[bone_name]["mirrored"] = jointRotMirrored; + if (!mPoserAnimator.tryGetJointSaveVectors(avatar, pj, &rotation, &position, &scale, &baseRotationIsZero)) continue; @@ -563,7 +571,6 @@ bool FSFloaterPoser::savePoseToXml(LLVOAvatar* avatar, const std::string& poseFi return false; } - return true; } @@ -663,6 +670,8 @@ void FSFloaterPoser::onClickRecaptureSelectedBones() if (!mPoserAnimator.isPosingAvatar(avatar)) return; + mPoserAnimator.updatePosingState(avatar, selectedJoints); + for (auto item : selectedJoints) { bool currentlyPosing = mPoserAnimator.isPosingAvatarJoint(avatar, *item); @@ -892,14 +901,39 @@ void FSFloaterPoser::onPoseMenuAction(const LLSD& param) else if (loadStyle == "selective_rot") loadType = SELECTIVE_ROT; + mLoadPoseTimer->tryLoading(poseName, loadType); +} + +void FSFloaterPoser::timedReload() +{ + if (!mLoadPoseTimer) + return; + LLVOAvatar* avatar = getUiSelectedAvatar(); if (!avatar) return; - loadPoseFromXml(avatar, poseName, loadType); - onJointTabSelect(); - refreshJointScrollListMembers(); - setSavePosesButtonText(!mPoserAnimator.allBaseRotationsAreZero(avatar)); + if (loadPoseFromXml(avatar, mLoadPoseTimer->getPosePath(), mLoadPoseTimer->getLoadMethod())) + { + setLoadingProgress(false); + onJointTabSelect(); + refreshJointScrollListMembers(); + setSavePosesButtonText(!mPoserAnimator.allBaseRotationsAreZero(avatar)); + } + else + { + setLoadingProgress(true); + } +} + +void FSFloaterPoser::setLoadingProgress(bool started) +{ + LLLoadingIndicator* mLoadingIndicator = findChild("progress_indicator"); + if (!mLoadingIndicator) + return; + + mLoadingIndicator->setVisible(started); + started ? mLoadingIndicator->start() : mLoadingIndicator->stop(); } void FSFloaterPoser::onClickLoadLeftHandPose() @@ -1020,14 +1054,15 @@ bool FSFloaterPoser::poseFileStartsFromTeePose(const std::string& poseFileName) return false; } -void FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& poseFileName, E_LoadPoseMethods loadMethod) +bool FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& poseFileName, E_LoadPoseMethods loadMethod) { + bool loadSuccess = false; std::string pathname = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY); if (!gDirUtilp->fileExists(pathname)) - return; + return loadSuccess; if (!mPoserAnimator.isPosingAvatar(avatar)) - return; + return loadSuccess; std::string fullPath = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, POSE_SAVE_SUBDIRECTORY, poseFileName + POSE_INTERNAL_FORMAT_FILE_EXT); @@ -1049,20 +1084,22 @@ void FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& pose bool enabled; bool setJointBaseRotationToZero; bool worldLocked; + bool mirroredJoint; S32 version = 0; bool startFromZeroRot = true; infile.open(fullPath); if (!infile.is_open()) - return; + return loadSuccess; + loadSuccess = true; while (!infile.eof()) { S32 lineCount = LLSDSerialize::fromXML(pose, infile); if (lineCount == LLSDParser::PARSE_FAILURE) { LL_WARNS("Posing") << "Failed to parse file: " << poseFileName << LL_ENDL; - return; + return loadSuccess; } for (LLSD::map_const_iterator itr = pose.beginMap(); itr != pose.endMap(); ++itr) @@ -1127,13 +1164,22 @@ void FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& pose worldLocked = control_map.has("worldLocked") ? control_map["worldLocked"].asBoolean() : false; mPoserAnimator.setRotationIsWorldLocked(avatar, *poserJoint, worldLocked); + + mirroredJoint = control_map.has("mirrored") ? control_map["mirrored"].asBoolean() : false; + mPoserAnimator.setRotationIsMirrored(avatar, *poserJoint, mirroredJoint); } + + if (version > 6) + loadSuccess = mPoserAnimator.loadPosingState(avatar, pose); } } catch ( const std::exception & e ) { + loadSuccess = false; LL_WARNS("Posing") << "Everything caught fire trying to load the pose: " << poseFileName << " exception: " << e.what() << LL_ENDL; } + + return loadSuccess; } void FSFloaterPoser::startPosingSelf() @@ -2720,3 +2766,31 @@ void FSFloaterPoser::onClickLockWorldRotBtn() refreshTextHighlightingOnJointScrollLists(); } + +FSLoadPoseTimer::FSLoadPoseTimer(FSLoadPoseTimer::callback_t callback) : LLEventTimer(0.5f), mCallback(callback) +{ +} + +bool FSLoadPoseTimer::tick() +{ + if (!mAttemptLoading) + return false; + + if (mLoadAttempts > 5) + return false; + + if (mCallback.empty()) + return false; + + mCallback(); + mLoadAttempts++; + return false; +} + +void FSLoadPoseTimer::tryLoading(std::string filePath, E_LoadPoseMethods loadMethod) +{ + mPoseFullPath = filePath; + mLoadType = loadMethod; + mLoadAttempts = 0; + mAttemptLoading = true; +} diff --git a/indra/newview/fsfloaterposer.h b/indra/newview/fsfloaterposer.h index 69c96a769c..79830826b5 100644 --- a/indra/newview/fsfloaterposer.h +++ b/indra/newview/fsfloaterposer.h @@ -39,6 +39,7 @@ class LLLineEditor; class LLScrollListCtrl; class LLSliderCtrl; class LLTabContainer; +class FSLoadPoseTimer; /// /// Describes how to load a pose file. @@ -228,11 +229,12 @@ public: bool savePoseToBvh(LLVOAvatar* avatar, const std::string& posePath); void onClickBrowsePoseCache(); void onPoseMenuAction(const LLSD& param); - void loadPoseFromXml(LLVOAvatar* avatar, const std::string& poseFileName, E_LoadPoseMethods loadMethod); + bool loadPoseFromXml(LLVOAvatar* avatar, const std::string& poseFileName, E_LoadPoseMethods loadMethod); bool poseFileStartsFromTeePose(const std::string& poseFileName); void setPoseSaveFileTextBoxToUiSelectedAvatarSaveFileName(); void setUiSelectedAvatarSaveFileName(const std::string& saveFileName); - bool confirmFileOverwrite(std::string fileName); + void timedReload(); + void setLoadingProgress(bool started); void startPosingSelf(); void stopPosingAllAvatars(); // visual manipulators control @@ -502,6 +504,8 @@ public: LLLineEditor* mPoseSaveNameEditor{ nullptr }; + FSLoadPoseTimer* mLoadPoseTimer; + LLPanel* mJointsParentPnl{ nullptr }; LLPanel* mTrackballPnl{ nullptr }; LLPanel* mPositionRotationPnl{ nullptr }; @@ -530,4 +534,24 @@ public: LLUICtrl* mScaleZSpnr{ nullptr }; }; +class FSLoadPoseTimer : public LLEventTimer +{ +public: + typedef boost::function callback_t; + + FSLoadPoseTimer(callback_t callback); + /*virtual*/ bool tick(); + + void tryLoading(std::string filePath, E_LoadPoseMethods loadMethod); + std::string getPosePath() { return mPoseFullPath; }; + E_LoadPoseMethods getLoadMethod() { return mLoadType; }; + +private: + callback_t mCallback; + bool mAttemptLoading = false; + E_LoadPoseMethods mLoadType = ROT_POS_AND_SCALES; + std::string mPoseFullPath; + int mLoadAttempts = 0; +}; + #endif diff --git a/indra/newview/fsjointpose.cpp b/indra/newview/fsjointpose.cpp index e1acbce4d0..b0e879545e 100644 --- a/indra/newview/fsjointpose.cpp +++ b/indra/newview/fsjointpose.cpp @@ -55,6 +55,7 @@ void FSJointPose::setPublicPosition(const LLVector3& pos) { addStateToUndo(FSJointState(mCurrentState)); mCurrentState.mPosition.set(pos); + mCurrentState.mLastChangeWasRotational = false; } void FSJointPose::setPublicRotation(bool zeroBase, const LLQuaternion& rot) @@ -65,17 +66,22 @@ void FSJointPose::setPublicRotation(bool zeroBase, const LLQuaternion& rot) zeroBaseRotation(true); mCurrentState.mRotation.set(rot); + mCurrentState.mLastChangeWasRotational = true; } void FSJointPose::setPublicScale(const LLVector3& scale) { addStateToUndo(FSJointState(mCurrentState)); mCurrentState.mScale.set(scale); + mCurrentState.mLastChangeWasRotational = false; } -void FSJointPose::undoLastChange() +bool FSJointPose::undoLastChange() { - mCurrentState = undoLastStateChange(FSJointState(mCurrentState)); + bool changeType = mCurrentState.mLastChangeWasRotational; + mCurrentState = undoLastStateChange(FSJointState(mCurrentState)); + + return changeType; } void FSJointPose::redoLastChange() @@ -87,6 +93,7 @@ void FSJointPose::resetJoint() { addStateToUndo(FSJointState(mCurrentState)); mCurrentState.resetJoint(); + mCurrentState.mLastChangeWasRotational = true; } void FSJointPose::addStateToUndo(FSJointState stateToAddToUndo) @@ -153,6 +160,7 @@ void FSJointPose::recaptureJoint() addStateToUndo(FSJointState(mCurrentState)); mCurrentState = FSJointState(joint); + mCurrentState.mLastChangeWasRotational = true; } LLQuaternion FSJointPose::recaptureJointAsDelta(bool zeroBase) @@ -162,9 +170,30 @@ LLQuaternion FSJointPose::recaptureJointAsDelta(bool zeroBase) return LLQuaternion::DEFAULT; addStateToUndo(FSJointState(mCurrentState)); + mCurrentState.mLastChangeWasRotational = true; return mCurrentState.updateFromJoint(joint, zeroBase); } +void FSJointPose::setBaseRotation(LLQuaternion rotation, LLJoint::JointPriority priority) +{ + mCurrentState.resetBaseRotation(rotation, priority); +} + +void FSJointPose::setBasePosition(LLVector3 position, LLJoint::JointPriority priority) +{ + mCurrentState.resetBasePosition(position, priority); +} + +void FSJointPose::setBaseScale(LLVector3 scale, LLJoint::JointPriority priority) +{ + mCurrentState.resetBaseScale(scale, priority); +} + +void FSJointPose::setJointPriority(LLJoint::JointPriority priority) +{ + mCurrentState.setPriority(priority); +} + void FSJointPose::swapRotationWith(FSJointPose* oppositeJoint) { if (!oppositeJoint) @@ -177,6 +206,18 @@ void FSJointPose::swapRotationWith(FSJointPose* oppositeJoint) oppositeJoint->mCurrentState.cloneRotationFrom(tempState); } +void FSJointPose::swapBaseRotationWith(FSJointPose* oppositeJoint) +{ + if (!oppositeJoint) + return; + if (mIsCollisionVolume) + return; + + auto tempState = FSJointState(mCurrentState); + mCurrentState.cloneBaseRotationFrom(oppositeJoint->mCurrentState); + oppositeJoint->mCurrentState.cloneBaseRotationFrom(tempState); +} + void FSJointPose::cloneRotationFrom(FSJointPose* fromJoint) { if (!fromJoint) @@ -184,6 +225,7 @@ void FSJointPose::cloneRotationFrom(FSJointPose* fromJoint) addStateToUndo(FSJointState(mCurrentState)); mCurrentState.cloneRotationFrom(fromJoint->mCurrentState); + mCurrentState.mLastChangeWasRotational = true; } void FSJointPose::mirrorRotationFrom(FSJointPose* fromJoint) @@ -208,6 +250,14 @@ void FSJointPose::reflectRotation() mCurrentState.reflectRotation(); } +void FSJointPose::reflectBaseRotation() +{ + if (mIsCollisionVolume) + return; + + mCurrentState.reflectBaseRotation(); +} + void FSJointPose::zeroBaseRotation(bool lockInBvh) { if (mIsCollisionVolume) @@ -244,20 +294,24 @@ bool FSJointPose::userHasSetBaseRotationToZero() const bool FSJointPose::getWorldRotationLockState() const { - if (mIsCollisionVolume) - return false; - return mCurrentState.mRotationIsWorldLocked; } void FSJointPose::setWorldRotationLockState(bool newState) { - if (mIsCollisionVolume) - return; - mCurrentState.mRotationIsWorldLocked = newState; } +bool FSJointPose::getRotationMirrorState() const +{ + return mCurrentState.mJointRotationIsMirrored; +} + +void FSJointPose::setRotationMirrorState(bool newState) +{ + mCurrentState.mJointRotationIsMirrored = newState; +} + bool FSJointPose::canPerformUndo() const { switch (mLastSetJointStates.size()) diff --git a/indra/newview/fsjointpose.h b/indra/newview/fsjointpose.h index e4b91f29b6..86ddd8522c 100644 --- a/indra/newview/fsjointpose.h +++ b/indra/newview/fsjointpose.h @@ -70,7 +70,8 @@ class FSJointPose /// /// Undoes the last position set, if any. /// - void undoLastChange(); + /// true if the change we un-did was rotational. + bool undoLastChange(); /// /// Undoes the last position set, if any. @@ -104,6 +105,11 @@ class FSJointPose /// void reflectRotation(); + /// + /// Reflects the base rotation of the represented joint left-right. + /// + void reflectBaseRotation(); + /// /// Sets the private rotation of the represented joint to zero. /// @@ -143,6 +149,11 @@ class FSJointPose /// void swapRotationWith(FSJointPose* oppositeJoint); + /// + /// Exchanges the base rotations between two joints. + /// + void swapBaseRotationWith(FSJointPose* oppositeJoint); + /// /// Clones the rotation to this from the supplied joint. /// @@ -165,6 +176,33 @@ class FSJointPose /// The rotation of the public difference between before and after recapture. LLQuaternion recaptureJointAsDelta(bool zeroBase); + /// + /// Sets the base rotation to the supplied rotation if the supplied priority is appropriate. + /// + /// The base rotation to set; zero is ignored. + /// The priority of the base rotation; only priority equal or higher than any prior sets have any effect. + void setBaseRotation(LLQuaternion rotation, LLJoint::JointPriority priority); + + /// + /// Sets the base position to the supplied position if the supplied priority is appropriate. + /// + /// The base position to set; zero is ignored. + /// The priority of the base rotation; only priority equal or higher than any prior sets have any effect. + void setBasePosition(LLVector3 position, LLJoint::JointPriority priority); + + /// + /// Sets the base scale to the supplied scale if the supplied priority is appropriate. + /// + /// The base scale to set; zero is ignored. + /// The priority of the base rotation; only priority equal or higher than any prior sets have any effect. + void setBaseScale(LLVector3 scale, LLJoint::JointPriority priority); + + /// + /// Sets the priority of the bone to the supplied value. + /// + /// The new priority of the base rotation. + void setJointPriority(LLJoint::JointPriority priority); + /// /// Clears the undo/redo deque. /// @@ -188,6 +226,18 @@ class FSJointPose /// The new state for the world-rotation lock. void setWorldRotationLockState(bool newState); + /// + /// Gets whether the rotation of a joint has been mirrored. + /// + /// True if the joint has been mirrored, otherwise false. + bool getRotationMirrorState() const; + + /// + /// Sets whether the rotation of a joint has been mirrored. + /// + /// The new state for the mirror. + void setRotationMirrorState(bool newState); + /// /// Reverts the position/rotation/scale to their values when the animation begun. /// This treatment is required for certain joints, particularly Collision Volumes and those bones not commonly animated by an AO. @@ -212,41 +262,64 @@ class FSJointPose mBaseRotation.set(joint->getRotation()); mBasePosition.set(joint->getPosition()); mBaseScale.set(joint->getScale()); + + mBasePositionFromAnimation.setZero(); + mBaseScaleFromAnimation.setZero(); } FSJointState() = default; LLQuaternion getTargetRotation() const { return mRotation * mBaseRotation; } - LLVector3 getTargetPosition() const { return mPosition + mBasePosition; } - LLVector3 getTargetScale() const { return mScale + mBaseScale; } + LLVector3 getTargetPosition() const { return mPosition + mBasePosition + mBasePositionFromAnimation; } + LLVector3 getTargetScale() const { return mScale + mBaseScale + mBaseScaleFromAnimation; } void reflectRotation() { - mBaseRotation.mQ[VX] *= -1; - mBaseRotation.mQ[VZ] *= -1; + reflectBaseRotation(); mRotation.mQ[VX] *= -1; mRotation.mQ[VZ] *= -1; + mJointRotationIsMirrored = !mJointRotationIsMirrored; + } + + void reflectBaseRotation() + { + mBaseRotation.mQ[VX] *= -1; + mBaseRotation.mQ[VZ] *= -1; } void cloneRotationFrom(FSJointState otherState) { - mBaseRotation.set(otherState.mBaseRotation); + cloneBaseRotationFrom(otherState); mRotation.set(otherState.mRotation); mUserSpecifiedBaseZero = otherState.mUserSpecifiedBaseZero; } + void cloneBaseRotationFrom(FSJointState otherState) + { + mBaseRotation.set(otherState.mBaseRotation); + } + bool baseRotationIsZero() const { return mBaseRotation == LLQuaternion::DEFAULT; } void resetJoint() { - mUserSpecifiedBaseZero = false; - mRotationIsWorldLocked = false; + mUserSpecifiedBaseZero = false; + mRotationIsWorldLocked = false; + mJointRotationIsMirrored = false; + mLastChangeWasRotational = true; mBaseRotation.set(mStartingRotation); mRotation.set(LLQuaternion::DEFAULT); mPosition.setZero(); mScale.setZero(); + mBasePositionFromAnimation.setZero(); + mBaseScaleFromAnimation.setZero(); } - void zeroBaseRotation() { mBaseRotation = LLQuaternion::DEFAULT; } + void zeroBaseRotation() + { + mBasePriority = LLJoint::LOW_PRIORITY; + mBaseRotation = LLQuaternion::DEFAULT; + mJointRotationIsMirrored = false; + } void revertJointToBase(LLJoint* joint) const { @@ -275,12 +348,50 @@ class FSJointPose } mRotation.set(newPublicRot); - mPosition.set(joint->getPosition() - mBasePosition); - mScale.set(joint->getScale() - mBaseScale); + mPosition.set(joint->getPosition() - mBasePosition - mBasePositionFromAnimation); + mScale.set(joint->getScale() - mBaseScale - mBaseScaleFromAnimation); return newPublicRot *= ~initalPublicRot; } + void resetBaseRotation(LLQuaternion rotation, LLJoint::JointPriority priority) + { + if (priority < mBasePriority) + return; + + if (rotation == LLQuaternion::DEFAULT) + return; + + LL_WARNS("Posing") << "Loaded rot: " << rotation << " at priority " << priority << LL_ENDL; + mBasePriority = priority; + mBaseRotation.set(rotation); + } + + void resetBasePosition(LLVector3 position, LLJoint::JointPriority priority) + { + if (priority < mBasePriority) + return; + + LL_WARNS("Posing") << "Loaded pos: " << position << " at priority " << priority << LL_ENDL; + mBasePriority = priority; + mBasePositionFromAnimation.set(position); + } + + void resetBaseScale(LLVector3 scale, LLJoint::JointPriority priority) + { + if (priority < mBasePriority) + return; + + if (scale.isExactlyZero()) + return; + + LL_WARNS("Posing") << "Loaded pos: " << scale << " at priority " << priority << LL_ENDL; + mBasePriority = priority; + mBaseScaleFromAnimation.set(scale); + } + + void setPriority(LLJoint::JointPriority priority) { mBasePriority = priority; } + private: FSJointState(FSJointState* state) { @@ -288,12 +399,18 @@ class FSJointPose mBaseRotation.set(state->mBaseRotation); mBasePosition.set(state->mBasePosition); mBaseScale.set(state->mBaseScale); + mBasePositionFromAnimation.set(state->mBasePositionFromAnimation); + mBaseScaleFromAnimation.set(state->mBaseScaleFromAnimation); mRotation.set(state->mRotation); mPosition.set(state->mPosition); mScale.set(state->mScale); - mUserSpecifiedBaseZero = state->mUserSpecifiedBaseZero; - mRotationIsWorldLocked = state->mRotationIsWorldLocked; + + mUserSpecifiedBaseZero = state->mUserSpecifiedBaseZero; + mRotationIsWorldLocked = state->mRotationIsWorldLocked; + mBasePriority = state->mBasePriority; + mJointRotationIsMirrored = state->mJointRotationIsMirrored; + mLastChangeWasRotational = state->mLastChangeWasRotational; } public: @@ -301,6 +418,15 @@ class FSJointPose LLVector3 mPosition; LLVector3 mScale; bool mRotationIsWorldLocked = false; + bool mLastChangeWasRotational = false; + + /// + /// Whether the joint has been mirrored. + /// + /// + /// Used when loading a diff; indicating that the base-rotations, once restored, need to be swapped. + /// + bool mJointRotationIsMirrored = false; /// /// A value indicating whether the user has explicitly set the base rotation to zero. @@ -317,7 +443,10 @@ class FSJointPose LLQuaternion mStartingRotation; LLQuaternion mBaseRotation; LLVector3 mBasePosition; + LLVector3 mBasePositionFromAnimation; LLVector3 mBaseScale; + LLVector3 mBaseScaleFromAnimation; + LLJoint::JointPriority mBasePriority = LLJoint::LOW_PRIORITY; }; private: diff --git a/indra/newview/fsposeranimator.cpp b/indra/newview/fsposeranimator.cpp index ba91cf356f..b310a064e1 100644 --- a/indra/newview/fsposeranimator.cpp +++ b/indra/newview/fsposeranimator.cpp @@ -94,8 +94,8 @@ void FSPoserAnimator::undoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint if (!jointPose) return; - jointPose->undoLastChange(); - undoOrRedoWorldLockedDescendants(joint, posingMotion, false); + if (jointPose->undoLastChange()) + undoOrRedoWorldLockedDescendants(joint, posingMotion, false); if (style == NONE || style == DELTAMODE) return; @@ -104,7 +104,8 @@ void FSPoserAnimator::undoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint if (!oppositeJointPose) return; - oppositeJointPose->undoLastChange(); + if (!oppositeJointPose->undoLastChange()) + return; auto oppositePoserJoint = getPoserJointByName(joint.mirrorJointName()); if (oppositePoserJoint) @@ -271,6 +272,38 @@ void FSPoserAnimator::setJointPosition(LLVOAvatar* avatar, const FSPoserJoint* j } } +bool FSPoserAnimator::getRotationIsMirrored(LLVOAvatar* avatar, const FSPoserJoint& joint) const +{ + if (!isAvatarSafeToUse(avatar)) + return false; + + FSPosingMotion* posingMotion = getPosingMotion(avatar); + if (!posingMotion) + return false; + + FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); + if (!jointPose) + return false; + + return jointPose->getRotationMirrorState(); +} + +void FSPoserAnimator::setRotationIsMirrored(LLVOAvatar* avatar, const FSPoserJoint& joint, bool newState) +{ + if (!isAvatarSafeToUse(avatar)) + return; + + FSPosingMotion* posingMotion = getPosingMotion(avatar); + if (!posingMotion) + return; + + FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName()); + if (!jointPose) + return; + + jointPose->setRotationMirrorState(newState); +} + bool FSPoserAnimator::getRotationIsWorldLocked(LLVOAvatar* avatar, const FSPoserJoint& joint) const { if (!isAvatarSafeToUse(avatar)) @@ -923,6 +956,56 @@ void FSPoserAnimator::loadJointScale(LLVOAvatar* avatar, const FSPoserJoint* joi jointPose->setPublicScale(scale); } +bool FSPoserAnimator::loadPosingState(LLVOAvatar* avatar, LLSD pose) +{ + if (!isAvatarSafeToUse(avatar)) + return false; + + mPosingState.purgeMotionStates(avatar); + mPosingState.restoreMotionStates(avatar, pose); + + FSPosingMotion* posingMotion = getPosingMotion(avatar); + if (!posingMotion) + return false; + + // TODO: do I need to zero all bases first to reset latent rotations? + bool loadSuccess = mPosingState.applyMotionStatesToPosingMotion(avatar, posingMotion); + if (loadSuccess) + applyJointMirrorToBaseRotations(posingMotion); + + return loadSuccess; +} + +void FSPoserAnimator::applyJointMirrorToBaseRotations(FSPosingMotion* posingMotion) +{ + for (size_t index = 0; index != PoserJoints.size(); ++index) + { + FSJointPose* jointPose = posingMotion->getJointPoseByJointName(PoserJoints[index].jointName()); + if (!jointPose) + continue; + + if (!jointPose->getRotationMirrorState()) + continue; + + if (PoserJoints[index].dontFlipOnMirror()) // we only flip one side. + continue; + + jointPose->reflectBaseRotation(); + FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(PoserJoints[index].mirrorJointName()); + + if (!oppositeJointPose) + continue; + + oppositeJointPose->reflectBaseRotation(); + jointPose->swapBaseRotationWith(oppositeJointPose); + } +} + +void FSPoserAnimator::savePosingState(LLVOAvatar* avatar, LLSD* saveRecord) +{ + mPosingState.writeMotionStates(avatar, saveRecord); +} + const FSPoserAnimator::FSPoserJoint* FSPoserAnimator::getPoserJointByName(const std::string& jointName) const { for (size_t index = 0; index != PoserJoints.size(); ++index) @@ -948,6 +1031,8 @@ bool FSPoserAnimator::tryPosingAvatar(LLVOAvatar* avatar) if (avatar->isSelf()) gAgent.stopFidget(); + mPosingState.captureMotionStates(avatar); + avatar->startDefaultMotions(); avatar->startMotion(posingMotion->motionId()); @@ -957,6 +1042,22 @@ bool FSPoserAnimator::tryPosingAvatar(LLVOAvatar* avatar) return false; } +void FSPoserAnimator::updatePosingState(LLVOAvatar* avatar, std::vector jointsRecaptured) +{ + if (!avatar) + return; + + FSPosingMotion* posingMotion = getPosingMotion(avatar); + if (!posingMotion) + return; + + std::string jointNamesRecaptured; + for (auto item : jointsRecaptured) + jointNamesRecaptured += item->jointName(); + + mPosingState.updateMotionStates(avatar, posingMotion, jointNamesRecaptured); +} + void FSPoserAnimator::stopPosingAvatar(LLVOAvatar *avatar) { if (!avatar || avatar->isDead()) @@ -966,6 +1067,7 @@ void FSPoserAnimator::stopPosingAvatar(LLVOAvatar *avatar) if (!posingMotion) return; + mPosingState.purgeMotionStates(avatar); avatar->stopMotion(posingMotion->motionId()); } @@ -1006,7 +1108,6 @@ FSPosingMotion* FSPoserAnimator::findOrCreatePosingMotion(LLVOAvatar* avatar) sAvatarIdToRegisteredAnimationId[avatar->getID()] = animationAssetId; return dynamic_cast(avatar->createMotion(animationAssetId)); - } bool FSPoserAnimator::isAvatarSafeToUse(LLVOAvatar* avatar) const @@ -1118,7 +1219,11 @@ void FSPoserAnimator::undoOrRedoJointOrFirstLockedChild(const FSPoserJoint& join if (jointPose->getWorldRotationLockState()) { - redo ? jointPose->redoLastChange() : jointPose->undoLastChange(); + if (redo) + jointPose->redoLastChange(); + else + jointPose->undoLastChange(); + return; } diff --git a/indra/newview/fsposeranimator.h b/indra/newview/fsposeranimator.h index 853b15d308..387e60da12 100644 --- a/indra/newview/fsposeranimator.h +++ b/indra/newview/fsposeranimator.h @@ -28,6 +28,7 @@ #define LL_FSPoserAnimator_H #include "fsposingmotion.h" +#include "fsposestate.h" #include "llvoavatar.h" /// @@ -197,27 +198,27 @@ public: /// const std::vector PoserJoints{ // head, torso, legs - { "mHead", "", BODY, { "mEyeLeft", "mEyeRight", "mFaceRoot", "mSkull" }, "0.000 0.076 0.000" }, - { "mNeck", "", BODY, { "mHead" }, "0.000 0.251 -0.010" }, - { "mPelvis", "", WHOLEAVATAR, { "mSpine1", "mHipLeft", "mHipRight", "mTail1", "mGroin", "mHindLimbsRoot" }, "0.000000 0.000000 0.000000" }, - { "mChest", "", BODY, { "mNeck", "mCollarLeft", "mCollarRight", "mWingsRoot" }, "0.000 0.205 -0.015" }, - { "mTorso", "", BODY, { "mSpine3" }, "0.000 0.084 0.000" }, - { "mCollarLeft", "mCollarRight", BODY, { "mShoulderLeft" }, "0.085 0.165 -0.021" }, - { "mShoulderLeft", "mShoulderRight", BODY, { "mElbowLeft" }, "0.079 0.000 0.000" }, - { "mElbowLeft", "mElbowRight", BODY, { "mWristLeft" }, "0.248 0.000 0.000" }, - { "mWristLeft", "mWristRight", BODY, { "mHandThumb1Left", "mHandIndex1Left", "mHandMiddle1Left", "mHandRing1Left", "mHandPinky1Left" }, "0.205 0.000 0.000" }, - { "mCollarRight", "mCollarLeft", BODY, { "mShoulderRight" }, "-0.085 0.165 -0.021", "", true }, - { "mShoulderRight", "mShoulderLeft", BODY, { "mElbowRight" }, "-0.079 0.000 0.000", "", true }, - { "mElbowRight", "mElbowLeft", BODY, { "mWristRight" }, "-0.248 0.000 0.000", "", true }, - { "mWristRight", "mWristLeft", BODY, { "mHandThumb1Right", "mHandIndex1Right", "mHandMiddle1Right", "mHandRing1Right", "mHandPinky1Right" }, "-0.205 0.000 0.000", "", true }, - { "mHipLeft", "mHipRight", BODY, { "mKneeLeft" }, "0.127 -0.041 0.034" }, - { "mKneeLeft", "mKneeRight", BODY, { "mAnkleLeft" }, "-0.046 -0.491 -0.001" }, - { "mAnkleLeft", "mAnkleRight", BODY, { "mFootLeft" }, "0.001 -0.468 -0.029" }, + { "mHead", "", BODY, { "mEyeLeft", "mEyeRight", "mFaceRoot", "mSkull", "HEAD" }, "0.000 0.076 0.000" }, + { "mNeck", "", BODY, { "mHead", "NECK" }, "0.000 0.251 -0.010" }, + { "mPelvis", "", WHOLEAVATAR, { "mSpine1", "mHipLeft", "mHipRight", "mTail1", "mGroin", "mHindLimbsRoot", "PELVIS", "BUTT" }, "0.000000 0.000000 0.000000" }, + { "mChest", "", BODY, { "mNeck", "mCollarLeft", "mCollarRight", "mWingsRoot", "CHEST", "LEFT_PEC", "RIGHT_PEC", "UPPER_BACK" }, "0.000 0.205 -0.015" }, + { "mTorso", "", BODY, { "mSpine3", "BELLY", "LEFT_HANDLE", "RIGHT_HANDLE", "LOWER_BACK" }, "0.000 0.084 0.000" }, + { "mCollarLeft", "mCollarRight", BODY, { "mShoulderLeft", "L_CLAVICLE" }, "0.085 0.165 -0.021" }, + { "mShoulderLeft", "mShoulderRight", BODY, { "mElbowLeft", "L_UPPER_ARM" }, "0.079 0.000 0.000" }, + { "mElbowLeft", "mElbowRight", BODY, { "mWristLeft", "L_LOWER_ARM" }, "0.248 0.000 0.000" }, + { "mWristLeft", "mWristRight", BODY, { "mHandThumb1Left", "mHandIndex1Left", "mHandMiddle1Left", "mHandRing1Left", "mHandPinky1Left", "L_HAND" }, "0.205 0.000 0.000" }, + { "mCollarRight", "mCollarLeft", BODY, { "mShoulderRight", "R_CLAVICLE" }, "-0.085 0.165 -0.021", "", true }, + { "mShoulderRight", "mShoulderLeft", BODY, { "mElbowRight", "R_UPPER_ARM" }, "-0.079 0.000 0.000", "", true }, + { "mElbowRight", "mElbowLeft", BODY, { "mWristRight", "R_LOWER_ARM" }, "-0.248 0.000 0.000", "", true }, + { "mWristRight", "mWristLeft", BODY, { "mHandThumb1Right", "mHandIndex1Right", "mHandMiddle1Right", "mHandRing1Right", "mHandPinky1Right", "R_HAND" }, "-0.205 0.000 0.000", "", true }, + { "mHipLeft", "mHipRight", BODY, { "mKneeLeft", "L_UPPER_LEG" }, "0.127 -0.041 0.034" }, + { "mKneeLeft", "mKneeRight", BODY, { "mAnkleLeft", "L_LOWER_LEG" }, "-0.046 -0.491 -0.001" }, + { "mAnkleLeft", "mAnkleRight", BODY, { "mFootLeft", "L_FOOT" }, "0.001 -0.468 -0.029" }, { "mFootLeft", "mFootRight", BODY, { "mToeLeft" }, "0.000 -0.061 0.112" }, { "mToeLeft", "mToeRight", BODY, {}, "0.000 0.000 0.109", "0.000 0.020 0.000" }, - { "mHipRight", "mHipLeft", BODY, { "mKneeRight" }, "-0.129 -0.041 0.034", "", true }, - { "mKneeRight", "mKneeLeft", BODY, { "mAnkleRight" }, "0.049 -0.491 -0.001", "", true }, - { "mAnkleRight", "mAnkleLeft", BODY, { "mFootRight" }, "0.000 -0.468 -0.029", "", true }, + { "mHipRight", "mHipLeft", BODY, { "mKneeRight", "R_UPPER_LEG" }, "-0.129 -0.041 0.034", "", true }, + { "mKneeRight", "mKneeLeft", BODY, { "mAnkleRight", "R_LOWER_LEG" }, "0.049 -0.491 -0.001", "", true }, + { "mAnkleRight", "mAnkleLeft", BODY, { "mFootRight", "R_FOOT" }, "0.000 -0.468 -0.029", "", true }, { "mFootRight", "mFootLeft", BODY, { "mToeRight" }, "0.000 -0.061 0.112", "", true }, { "mToeRight", "mToeLeft", BODY, {}, "0.000 0.000 0.109", "0.000 0.020 0.000", true }, @@ -617,6 +618,22 @@ public: /// The lock state to apply. void setRotationIsWorldLocked(LLVOAvatar* avatar, const FSPoserJoint& joint, bool newState); + /// + /// Gets whether the supplied joint for the supplied avatar has been mirrored. + /// + /// The avatar owning the supplied joint. + /// The joint to query. + /// True if the joint is maintaining a fixed-rotation in world, otherwise false. + bool getRotationIsMirrored(LLVOAvatar* avatar, const FSPoserJoint& joint) const; + + /// + /// Sets the mirrored status for supplied joint for the supplied avatar. + /// + /// The avatar owning the supplied joint. + /// The joint to query. + /// The mirror state to apply. + void setRotationIsMirrored(LLVOAvatar* avatar, const FSPoserJoint& joint, bool newState); + /// /// Determines if the kind of save to perform should be a 'delta' save, or a complete save. /// @@ -690,6 +707,42 @@ public: /// void loadJointScale(LLVOAvatar* avatar, const FSPoserJoint* joint, bool loadScaleAsDelta, LLVector3 scale); + /// + /// Loads the posing state (base rotations) to the supplied avatars posing-motion, from the supplied record. + /// + /// That avatar whose posing state should be loaded. + /// The record to read the posing state from. + /// True if the pose loaded successfully, otherwise false. + /// + /// When a save embeds animations that need to be restored at a certain time, + /// it can take several frames for the animation to be loaded and ready. + /// It may therefore be necessary to attempt this several times. + /// + bool loadPosingState(LLVOAvatar* avatar, LLSD pose); + + /// + /// Adds the posing state for the supplied avatar to the supplied record. + /// + /// That avatar whose posing state should be written. + /// The record to write the posing state to. + void savePosingState(LLVOAvatar* avatar, LLSD* saveRecord); + + /// + /// Purges and recaptures the pose state for the supplied avatar. + /// + /// The avatar whose pose state is to be recapture. + /// The joints which were recaptured. + void updatePosingState(LLVOAvatar* avatar, std::vector jointsRecaptured); + + /// + /// Traverses the joints and applies reversals to the base rotations if needed. + /// + /// The posing motion whose pose states require updating. + /// + /// Required after restoring a diff. The base rotations will be in their original arrangment. + /// + void applyJointMirrorToBaseRotations(FSPosingMotion* posingMotion); + private: /// /// Translates a rotation vector from the UI to a Quaternion for the bone. @@ -788,6 +841,8 @@ public: /// Is static, so the animationId is not lost between sessions (such as when the UI floater is closed and reopened). /// static std::map sAvatarIdToRegisteredAnimationId; + + FSPoseState mPosingState; }; #endif // LL_FSPoserAnimator_H diff --git a/indra/newview/fsposestate.cpp b/indra/newview/fsposestate.cpp new file mode 100644 index 0000000000..753e33bfa2 --- /dev/null +++ b/indra/newview/fsposestate.cpp @@ -0,0 +1,249 @@ +#include "fsposestate.h" +#include "llinventorymodel.h" // gInventory + +std::map> FSPoseState::sMotionStates; +std::map FSPoseState::sCaptureOrder; + +void FSPoseState::captureMotionStates(LLVOAvatar* avatar) +{ + if (!avatar) + return; + + sCaptureOrder[avatar->getID()] = 0; + + for (auto anim_it = avatar->mPlayingAnimations.begin(); anim_it != avatar->mPlayingAnimations.end(); ++anim_it) + { + LLKeyframeMotion* motion = dynamic_cast(avatar->findMotion(anim_it->first)); + if (!motion) + continue; + + fsMotionState newState; + newState.avatarId = avatar->getID(); + newState.motionId = anim_it->first; + newState.lastUpdateTime = motion->getLastUpdateTime(); + newState.captureOrder = 0; + + sMotionStates[avatar->getID()].push_back(newState); + } +} + +void FSPoseState::updateMotionStates(LLVOAvatar* avatar, FSPosingMotion* posingMotion, std::string jointNamesRecaptured) +{ + if (!avatar || !posingMotion) + return; + + sCaptureOrder[avatar->getID()]++; + + // if an animation for avatar is a subset of jointNamesRecaptured, delete it + // this happens on second/subsequent recaptures; the first recapture is no longer needed + for (auto it = sMotionStates[avatar->getID()].begin(); it != sMotionStates[avatar->getID()].end();) + { + bool avatarMatches = (*it).avatarId == avatar->getID(); + std::string joints = (*it).jointNamesAnimated; + bool recaptureMatches = !joints.empty() && !jointNamesRecaptured.empty() && jointNamesRecaptured.find(joints) != std::string::npos; + + if (avatarMatches && recaptureMatches) + it = sMotionStates[avatar->getID()].erase(it); + else + it++; + } + + for (auto anim_it = avatar->mPlayingAnimations.begin(); anim_it != avatar->mPlayingAnimations.end(); anim_it++) + { + LLKeyframeMotion* motion = dynamic_cast(avatar->findMotion(anim_it->first)); + if (!motion) + continue; + + if (!posingMotion->otherMotionAnimatesJoints(motion, jointNamesRecaptured)) + continue; + + bool foundMatch = false; + for (auto it = sMotionStates[avatar->getID()].begin(); it != sMotionStates[avatar->getID()].end(); it++) + { + bool avatarMatches = (*it).avatarId == avatar->getID(); + bool motionIdMatches = (*it).motionId == anim_it->first; + bool updateTimesMatch = (*it).lastUpdateTime == motion->getLastUpdateTime(); // consider when recapturing the same animation at different times for a subset of bones + + foundMatch = avatarMatches && motionIdMatches && updateTimesMatch; + if (foundMatch) + break; + } + + if (foundMatch) + continue; + + fsMotionState newState; + newState.avatarId = avatar->getID(); + newState.motionId = anim_it->first; + newState.lastUpdateTime = motion->getLastUpdateTime(); + newState.jointNamesAnimated = jointNamesRecaptured; + newState.captureOrder = sCaptureOrder[avatar->getID()]; + + sMotionStates[avatar->getID()].push_back(newState); + } +} + +void FSPoseState::purgeMotionStates(LLVOAvatar* avatar) +{ + if (!avatar) + return; + + std::vector::iterator it; + for (it = sMotionStates[avatar->getID()].begin(); it != sMotionStates[avatar->getID()].end();) + { + if ((*it).avatarId == avatar->getID()) + it = sMotionStates[avatar->getID()].erase(it); + else + it++; + } +} + +void FSPoseState::writeMotionStates(LLVOAvatar* avatar, LLSD* saveRecord) +{ + if (!avatar) + return; + + int animNumber = 0; + for (auto it = sMotionStates[avatar->getID()].begin(); it != sMotionStates[avatar->getID()].end(); ++it) + { + if (it->avatarId != avatar->getID()) + continue; + + std::string uniqueAnimId = "poseState" + std::to_string(animNumber++); + (*saveRecord)[uniqueAnimId]["animationId"] = it->motionId.asString(); + (*saveRecord)[uniqueAnimId]["lastUpdateTime"] = it->lastUpdateTime; + (*saveRecord)[uniqueAnimId]["jointNamesAnimated"] = it->jointNamesAnimated; + (*saveRecord)[uniqueAnimId]["captureOrder"] = it->captureOrder; + (*saveRecord)[uniqueAnimId]["playOrder"] = animNumber; + } +} + +void FSPoseState::restoreMotionStates(LLVOAvatar* avatar, LLSD pose) +{ + if (!avatar) + return; + + sCaptureOrder[avatar->getID()] = 0; + + for (auto itr = pose.beginMap(); itr != pose.endMap(); ++itr) + { + std::string const& name = itr->first; + LLSD const& control_map = itr->second; + + if (!name.starts_with("poseState")) + continue; + + fsMotionState newState; + newState.avatarId = avatar->getID(); + + if (control_map.has("animationId")) + { + std::string const name = control_map["animationId"].asString(); + LLUUID animId; + if (LLUUID::parseUUID(name, &animId)) + newState.motionId = animId; + } + + if (control_map.has("lastUpdateTime")) + newState.lastUpdateTime = (F32)control_map["lastUpdateTime"].asReal(); + + if (control_map.has("jointNamesAnimated")) + newState.jointNamesAnimated = control_map["jointNamesAnimated"].asString(); + + if (control_map.has("captureOrder")) + newState.captureOrder = control_map["captureOrder"].asInteger(); + + if (newState.captureOrder > sCaptureOrder[avatar->getID()]) + sCaptureOrder[avatar->getID()] = newState.captureOrder; + + sMotionStates[avatar->getID()].push_back(newState); + } +} + +bool FSPoseState::applyMotionStatesToPosingMotion(LLVOAvatar* avatar, FSPosingMotion* posingMotion) +{ + if (!avatar || !posingMotion) + return false; + + bool allMotionsApplied = true; + + std::sort(sMotionStates[avatar->getID()].begin(), sMotionStates[avatar->getID()].end(), compareByCaptureOrder()); + + int lastCaptureOrder = 0; + bool needPriorityReset = false; + for (auto it = sMotionStates[avatar->getID()].begin(); it != sMotionStates[avatar->getID()].end(); it++) + { + if (it->avatarId != avatar->getID()) + continue; + + needPriorityReset = it->captureOrder > lastCaptureOrder; + + if (it->motionApplied) + continue; + + if (!avatarCanUsePose(avatar, it->motionId)) + { + it->motionApplied = true; + continue; + } + + LLKeyframeMotion* kfm = dynamic_cast(avatar->findMotion(it->motionId)); + + if (kfm) + { + if (needPriorityReset) + { + lastCaptureOrder = it->captureOrder; + LL_WARNS("Posing") << "Resetting priority at cap order: " << lastCaptureOrder << LL_ENDL; + resetPriorityForCaptureOrder(avatar, posingMotion, lastCaptureOrder); + } + + it->motionApplied = posingMotion->loadOtherMotionToBaseOfThisMotion(kfm, it->lastUpdateTime, it->jointNamesAnimated); + } + else + { + avatar->startMotion(it->motionId); // only start if not a kfm; then wait until it casts as a kfm + avatar->stopMotion(it->motionId); // only stop if we have used it and we started it + } + + allMotionsApplied &= it->motionApplied; + } + + return allMotionsApplied; +} + +void FSPoseState::resetPriorityForCaptureOrder(LLVOAvatar* avatar, FSPosingMotion* posingMotion, int captureOrder) +{ + for (auto it = sMotionStates[avatar->getID()].begin(); it != sMotionStates[avatar->getID()].end(); it++) + { + if (it->jointNamesAnimated.empty()) + continue; + if (it->avatarId != avatar->getID()) + continue; + if (it->motionApplied) + continue; + if (it->captureOrder != captureOrder) + continue; + + LL_WARNS("Posing") << "Resetting priority for: " << it->jointNamesAnimated << LL_ENDL; + posingMotion->resetBonePriority(it->jointNamesAnimated); + } +} + +bool FSPoseState::avatarCanUsePose(LLVOAvatar* avatar, LLUUID motionId) +{ + if (!avatar) + return true; + + if (!motionId.notNull()) + return true; + + if (avatar != gAgentAvatarp) + return true; + + LLInventoryItem* item = gInventory.getItem(motionId); + if (!item) + return true; + + return item->getPermissions().getOwner() == avatar->getID(); +} diff --git a/indra/newview/fsposestate.h b/indra/newview/fsposestate.h new file mode 100644 index 0000000000..b992fb11cc --- /dev/null +++ b/indra/newview/fsposestate.h @@ -0,0 +1,158 @@ +/** + * @file fsposestate.h + * @brief a means to save and restore the instantaneous state of animations posing an avatar. + * + * $LicenseInfo:firstyear=2025&license=viewerlgpl$ + * Phoenix Firestorm Viewer Source Code + * Copyright (c) 2025 Angeldark Raymaker @ Second Life + * + * 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$ + */ + +#ifndef LL_FSPoseState_H +#define LL_FSPoseState_H + +#include "llvoavatar.h" +#include "fsposingmotion.h" + +class FSPoseState +{ +public: + FSPoseState() = default; + virtual ~FSPoseState() = default; + +public: + /// + /// Captures the current animations posing the supplied avatar and how long they have been playing. + /// + /// The avatar whose animations are to be captured. + /// + /// Only animations owned by the supplied avatar are documented. + /// + void captureMotionStates(LLVOAvatar* avatar); + + /// + /// Updates the stored list of animations posing the avatar. + /// + /// The avatar whose animations are to be captured. + /// The posing motion. + /// The names of the joints being recaptured. + void updateMotionStates(LLVOAvatar* avatar, FSPosingMotion* posingMotion, std::string jointNamesRecaptured); + + /// + /// Removes all current animation states for the supplied avatar. + /// + /// The avatar whose animations are to be purged. + void purgeMotionStates(LLVOAvatar* avatar); + + /// + /// Writes any documented poses for the supplied avatar to the supplied stream. + /// + /// The avatar whose animations may have been captured. + /// The record to add to. + void writeMotionStates(LLVOAvatar* avatar, LLSD* saveRecord); + + /// + /// Restores pose state(s) from the supplied record. + /// + /// The avatar whose animations may have been captured. + /// The posing motion. + /// The record to read from. + void restoreMotionStates(LLVOAvatar* avatar, LLSD pose); + + /// + /// Applies the motion states for the supplied avatar to the supplied motion. + /// + /// The avatar to apply the motion state(s) to. + /// The posing motion to apply the state(s) to. + /// True if all the motion states for the supplied avatar have been applied, otherwise false. + /// + /// In some ways this is like an AO: loading LLKeyframeMotions. + /// Once loaded, the LLKeyframeMotion is put at time fsMotionState.lastUpdateTime. + /// The joint-rotations for that LLKeyframeMotion are then restored to the base. + /// This examines sMotionStates for any avatarId matches; such as after a restoreMotionStates(...). + /// This could result in loading assets, thus a particular member of sMotionStates may take several attempts to load. + /// Motion(s) that the avatar does not have permissions for are not considered in the return boolean. + /// + bool applyMotionStatesToPosingMotion(LLVOAvatar* avatar, FSPosingMotion* posingMotion); + + void resetPriorityForCaptureOrder(LLVOAvatar* avatar, FSPosingMotion* posingMotion, int captureOrder); + +private: + /// + /// A class documenting the state of an animation for an avatar. + /// + class fsMotionState + { + public: + /// + /// The avatar ID this record is associated with. + /// + LLUUID avatarId; + + /// + /// The motion ID recorded animating the avatar ID. + /// + LLAssetID motionId; + + /// + /// The play-time the motionId had progressed until the motion was captured. + /// + F32 lastUpdateTime = 0.f; + + /// + /// Upon reloading, whether this record has been applied to the avatar. + /// + bool motionApplied = false; + + /// + /// When reloading, larger numbers are loaded last, nesting order and priority. + /// Represents recaptures. + /// + int captureOrder = 0; + + /// + /// When reloading, and if not-empty, the names of the bones this motionId should affect. + /// + std ::string jointNamesAnimated; + }; + + /// + /// Gets whether the supplied avatar has ownership of the supplied motion id. + /// + /// The avatar to query for ownership. + /// The motion to query for ownership. + /// True if the avatar has ownership of the motion, otherwise false. + bool avatarCanUsePose(LLVOAvatar* avatar, LLUUID motionId); + + struct compareByCaptureOrder + { + bool operator()(const fsMotionState& a, const fsMotionState& b) + { + if (a.captureOrder < b.captureOrder) + return true; // Ascending order + + return false; + } + }; + + static std::map > sMotionStates; + static std::map sCaptureOrder; +}; + +#endif // LL_FSPoseState_H diff --git a/indra/newview/fsposingmotion.cpp b/indra/newview/fsposingmotion.cpp index 15fc7d6bb8..2ee32f77d1 100644 --- a/indra/newview/fsposingmotion.cpp +++ b/indra/newview/fsposingmotion.cpp @@ -29,10 +29,11 @@ #include "fsposingmotion.h" #include "llcharacter.h" -FSPosingMotion::FSPosingMotion(const LLUUID &id) : LLMotion(id) +FSPosingMotion::FSPosingMotion(const LLUUID& id) : LLKeyframeMotion(id) { mName = "fs_poser_pose"; mMotionID = id; + mJointMotionList = &dummyMotionList; } LLMotion::LLMotionInitStatus FSPosingMotion::onInitialize(LLCharacter *character) @@ -269,6 +270,113 @@ void FSPosingMotion::setJointBvhLock(FSJointPose* joint, bool lockInBvh) joint->zeroBaseRotation(lockInBvh); } +bool FSPosingMotion::loadOtherMotionToBaseOfThisMotion(LLKeyframeMotion* motionToLoad, F32 timeToLoadAt, std::string selectedJointNames) +{ + FSPosingMotion* motionToLoadAsFsPosingMotion = static_cast(motionToLoad); + if (!motionToLoadAsFsPosingMotion) + return false; + + LLJoint::JointPriority priority = motionToLoad->getPriority(); + bool motionIsForAllJoints = selectedJointNames.empty(); + + LLQuaternion rot; + LLVector3 position, scale; + bool hasRotation = false, hasPosition = false, hasScale = false; + + for (auto poserJoint_iter = mJointPoses.begin(); poserJoint_iter != mJointPoses.end(); ++poserJoint_iter) + { + std::string jointName = poserJoint_iter->jointName(); + + bool motionIsForThisJoint = selectedJointNames.find(jointName) != std::string::npos; + if (!motionIsForAllJoints && !motionIsForThisJoint) + continue; + + hasRotation = hasPosition = hasScale = false; + motionToLoadAsFsPosingMotion->getJointStateAtTime(jointName, timeToLoadAt, &hasRotation, &rot, &hasPosition, &position, &hasScale, &scale); + + if (hasRotation) + poserJoint_iter->setBaseRotation(rot, priority); + + //if (hasPosition) + // poserJoint_iter->setBasePosition(position, priority); + + //if (hasScale) + // poserJoint_iter->setBaseScale(scale, priority); + } + + return true; +} + +void FSPosingMotion::getJointStateAtTime(std::string jointPoseName, F32 timeToLoadAt, + bool* hasRotation, LLQuaternion* jointRotation, + bool* hasPosition, LLVector3* jointPosition, + bool* hasScale, LLVector3* jointScale) +{ + if (this == NULL || mJointMotionList == nullptr) + return; + + for (U32 i = 0; i < mJointMotionList->getNumJointMotions(); i++) + { + JointMotion* jm = mJointMotionList->getJointMotion(i); + if (!boost::iequals(jointPoseName, jm->mJointName)) + continue; + + *hasRotation = (jm->mRotationCurve.mNumKeys > 0); + if (hasRotation) + jointRotation->set(jm->mRotationCurve.getValue(timeToLoadAt, mJointMotionList->mDuration)); + + *hasPosition = (jm->mPositionCurve.mNumKeys > 0); + if (hasPosition) + jointPosition->set(jm->mPositionCurve.getValue(timeToLoadAt, mJointMotionList->mDuration)); + + *hasScale = (jm->mScaleCurve.mNumKeys > 0); + if (hasScale) + jointScale->set(jm->mScaleCurve.getValue(timeToLoadAt, mJointMotionList->mDuration)); + + return; + } +} + +bool FSPosingMotion::otherMotionAnimatesJoints(LLKeyframeMotion* motionToQuery, std::string recapturedJointNames) +{ + FSPosingMotion* motionToLoadAsFsPosingMotion = static_cast(motionToQuery); + if (!motionToLoadAsFsPosingMotion) + return false; + + return motionToLoadAsFsPosingMotion->motionAnimatesJoints(recapturedJointNames); +} + +bool FSPosingMotion::motionAnimatesJoints(std::string recapturedJointNames) +{ + if (this == NULL || mJointMotionList == nullptr) + return false; + + for (U32 i = 0; i < mJointMotionList->getNumJointMotions(); i++) + { + JointMotion* jm = mJointMotionList->getJointMotion(i); + if (recapturedJointNames.find(jm->mJointName) == std::string::npos) + continue; + + if (jm->mRotationCurve.mNumKeys > 0) + return true; + } + + return false; +} + +void FSPosingMotion::resetBonePriority(std::string boneNamesToReset) +{ + if (boneNamesToReset.empty()) + return; + + for (auto poserJoint_iter = mJointPoses.begin(); poserJoint_iter != mJointPoses.end(); ++poserJoint_iter) + { + std::string jointName = poserJoint_iter->jointName(); + if (boneNamesToReset.find(jointName) != std::string::npos) + poserJoint_iter->setJointPriority(LLJoint::LOW_PRIORITY); + } +} + bool FSPosingMotion::vectorsNotQuiteEqual(LLVector3 v1, LLVector3 v2) const { if (vectorAxesAlmostEqual(v1.mV[VX], v2.mV[VX]) && diff --git a/indra/newview/fsposingmotion.h b/indra/newview/fsposingmotion.h index 7e7b8fd9f4..0f336b3ba8 100644 --- a/indra/newview/fsposingmotion.h +++ b/indra/newview/fsposingmotion.h @@ -32,6 +32,7 @@ //----------------------------------------------------------------------------- #include "llmotion.h" #include "fsjointpose.h" +#include "llkeyframemotion.h" #define MIN_REQUIRED_PIXEL_AREA_POSING 500.f @@ -39,10 +40,11 @@ // class FSPosingMotion //----------------------------------------------------------------------------- class FSPosingMotion : - public LLMotion + public LLKeyframeMotion { public: FSPosingMotion(const LLUUID &id); + FSPosingMotion(const LLKeyframeMotion& kfm) : LLKeyframeMotion{ kfm } { } virtual ~FSPosingMotion(){}; public: @@ -132,6 +134,58 @@ public: /// Whether the joint should be locked if exported to BVH. void setJointBvhLock(FSJointPose* joint, bool lockInBvh); + /// + /// Loads the rotations of the supplied motion at the supplied time to the base + /// + /// The motion whose joint rotations (etc) we want to copy to this. + /// The play-time the animation should be advanced to derive the correct joint state. + /// If only some of the joints should be animated by this motion, name them here. + /// + bool loadOtherMotionToBaseOfThisMotion(LLKeyframeMotion* motionToLoad, F32 timeToLoadAt, std::string selectedJointNames); + + /// + /// Tries to get the rotation, position and scale for the supplied joint name at the supplied time. + /// + /// The name of the joint. Example: "mPelvis". + /// The time to get the rotation at. + /// Output of whether the animation has a rotation for the supplied joint name. + /// The output rotation of the named joint. + /// Output of whether the animation has a position for the supplied joint name. + /// The output position of the named joint. + /// Output of whether the animation has a scale for the supplied joint name. + /// The output scale of the named joint. + /// + /// The most significant thing this method does is provide access to protected properties of some other LLPosingMotion. + /// Thus its most common usage would be to access those properties for an arbitrary animation 'from' the poser's instance of one of these. + /// + void getJointStateAtTime(std::string jointPoseName, F32 timeToLoadAt, bool* hasRotation, LLQuaternion* jointRotation, + bool* hasPosition, LLVector3* jointPosition, bool* hasScale, LLVector3* jointScale); + + /// + /// Resets the bone priority to zero for the joints named in the supplied string. + /// + /// The string containg bone names (like mPelvis). + void resetBonePriority(std::string boneNamesToReset); + + /// + /// Queries whether the supplied motion animates any of the joints named in the supplied string. + /// + /// The motion to query. + /// A string containing all of the joint names. + /// True if the motion animates any of the bones named, otherwise false. + bool otherMotionAnimatesJoints(LLKeyframeMotion* motionToQuery, std::string recapturedJointNames); + + /// + /// Queries whether the this motion animates any of the joints named in the supplied string. + /// + /// A string containing all of the joint names. + /// True if the motion animates any of the bones named, otherwise false. + /// + /// The most significant thing this method does is provide access to protected properties of an LLPosingMotion. + /// Thus its most common usage would be to access those properties for an arbitrary animation. + /// + bool motionAnimatesJoints(std::string recapturedJointNames); + private: /// /// The axial difference considered close enough to be the same. @@ -154,6 +208,11 @@ private: /// LLAssetID mMotionID; + /// + /// Constructor and usage requires this not be NULL. + /// + JointMotionList dummyMotionList; + /// /// The time constant, in seconds, we use for transitioning between one animation-state to another; this affects the 'damping' /// of motion between changes to a joint. 'Constant' in this context is not a reference to the language-idea of 'const' value. diff --git a/indra/newview/skins/default/xui/en/floater_fs_poser.xml b/indra/newview/skins/default/xui/en/floater_fs_poser.xml index a2d35e8384..f74b357403 100644 --- a/indra/newview/skins/default/xui/en/floater_fs_poser.xml +++ b/indra/newview/skins/default/xui/en/floater_fs_poser.xml @@ -1838,6 +1838,15 @@ width="430"> + name="load_poses_button" left_pad="1" top_delta="0" - width="95"/> + width="85"/> /// The avatar whose animations are to be captured. - /// - /// Only animations owned by the supplied avatar are documented. - /// void captureMotionStates(LLVOAvatar* avatar); /// @@ -54,6 +51,17 @@ public: /// The names of the joints being recaptured. void updateMotionStates(LLVOAvatar* avatar, FSPosingMotion* posingMotion, std::string jointNamesRecaptured); + /// + /// Add a new posing state, or updates the matching posing state with the supplied data. + /// + /// The avatar the posing state is intended for. + /// The ID of the animation. + /// The frame-time of the animation. + /// The names of the joints, if any, the animation should specifically be applied to. + /// The capture order. + /// True if the posing state was added or changed by the update data, otherwise false. + bool addOrUpdatePosingMotionState(LLVOAvatar* avatar, LLUUID animId, F32 updateTime, std::string jointNames, int captureOrder); + /// /// Removes all current animation states for the supplied avatar. /// @@ -98,11 +106,6 @@ private: class fsMotionState { public: - /// - /// The avatar ID this record is associated with. - /// - LLUUID avatarId; - /// /// The motion ID recorded animating the avatar ID. /// From 7e04a1e658cc8c37c2887a704645a0b453286064 Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Sun, 28 Sep 2025 12:46:44 +0100 Subject: [PATCH 06/34] FIRE-35794: Don't zero scale and pos deltas for diff reloads --- indra/newview/fsfloaterposer.cpp | 6 ++++-- indra/newview/fsposeranimator.cpp | 1 - indra/newview/fsposingmotion.cpp | 6 ------ 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 0ccd53c6c0..02202e34ee 100644 --- a/indra/newview/fsfloaterposer.cpp +++ b/indra/newview/fsfloaterposer.cpp @@ -518,7 +518,8 @@ bool FSFloaterPoser::savePoseToXml(LLVOAvatar* avatar, const std::string& poseFi record["version"]["value"] = (S32)7; record["startFromTeePose"]["value"] = !savingDiff; - mPoserAnimator.savePosingState(avatar, &record); + if (savingDiff) + mPoserAnimator.savePosingState(avatar, &record); LLVector3 rotation, position, scale, zeroVector; bool baseRotationIsZero; @@ -902,6 +903,7 @@ void FSFloaterPoser::onPoseMenuAction(const LLSD& param) loadType = SELECTIVE_ROT; mLoadPoseTimer->tryLoading(poseName, loadType); + setLoadingProgress(true); } void FSFloaterPoser::timedReload() @@ -1171,7 +1173,7 @@ bool FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& pose mPoserAnimator.setRotationIsMirrored(avatar, *poserJoint, mirroredJoint); } - if (version > 6) + if (version > 6 && !startFromZeroRot) loadSuccess = mPoserAnimator.loadPosingState(avatar, pose); } } diff --git a/indra/newview/fsposeranimator.cpp b/indra/newview/fsposeranimator.cpp index fe7953c0e5..067c4dbdd0 100644 --- a/indra/newview/fsposeranimator.cpp +++ b/indra/newview/fsposeranimator.cpp @@ -937,7 +937,6 @@ void FSPoserAnimator::loadJointPosition(LLVOAvatar* avatar, const FSPoserJoint* { jointPose->setJointPriority(LLJoint::LOW_PRIORITY); jointPose->setBasePosition(position, LLJoint::LOW_PRIORITY); - jointPose->setPublicPosition(LLVector3::zero); } } diff --git a/indra/newview/fsposingmotion.cpp b/indra/newview/fsposingmotion.cpp index 7f47c2b86d..9c4772d309 100644 --- a/indra/newview/fsposingmotion.cpp +++ b/indra/newview/fsposingmotion.cpp @@ -298,16 +298,10 @@ bool FSPosingMotion::loadOtherMotionToBaseOfThisMotion(LLKeyframeMotion* motionT poserJoint_iter->setBaseRotation(rot, priority); if (hasPosition) - { poserJoint_iter->setBasePosition(position, priority); - poserJoint_iter->setPublicPosition(LLVector3::zero); - } if (hasScale) - { poserJoint_iter->setBaseScale(scale, priority); - poserJoint_iter->setPublicScale(LLVector3::zero); - } } return true; From 0663773ab4ba6d0f48219eb6b7b5d2da53b8aa2d Mon Sep 17 00:00:00 2001 From: Angeldark Raymaker Date: Sun, 28 Sep 2025 13:52:58 +0100 Subject: [PATCH 07/34] FIRE-35794: Don't reload base if user set to zero for partial-bvh reloads --- indra/newview/fsjointpose.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/indra/newview/fsjointpose.h b/indra/newview/fsjointpose.h index 1c279f1d6b..f87856795f 100644 --- a/indra/newview/fsjointpose.h +++ b/indra/newview/fsjointpose.h @@ -351,6 +351,9 @@ class FSJointPose void resetBaseRotation(LLQuaternion rotation, LLJoint::JointPriority priority) { + if (mUserSpecifiedBaseZero) + return; + if (priority < mBasePriority) return; From 318c18be6a7606382dd8c1db65d3987eafb1cb09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:07:39 +0000 Subject: [PATCH 08/34] Bump actions/github-script from 7 to 8 Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/check-pr.yaml | 2 +- .github/workflows/tag-release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-pr.yaml b/.github/workflows/check-pr.yaml index a5cee9157c..08e907e83f 100644 --- a/.github/workflows/check-pr.yaml +++ b/.github/workflows/check-pr.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check PR description - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const description = context.payload.pull_request.body || ''; diff --git a/.github/workflows/tag-release.yaml b/.github/workflows/tag-release.yaml index 24ee2de794..2922065f99 100644 --- a/.github/workflows/tag-release.yaml +++ b/.github/workflows/tag-release.yaml @@ -35,7 +35,7 @@ jobs: echo NIGHTLY_DATE=${NIGHTLY_DATE} >> ${GITHUB_ENV} echo TAG_ID="$(echo ${{ github.sha }} | cut -c1-8)-${{ inputs.project || '${NIGHTLY_DATE}' }}" >> ${GITHUB_ENV} - name: Update Tag - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8 with: # use a real access token instead of GITHUB_TOKEN default. # required so that the results of this tag creation can trigger the build workflow From a46b0f4348304aabbdd7248c3e7dbd58d77cdd1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:07:42 +0000 Subject: [PATCH 09/34] Bump actions/labeler from 5 to 6 Bumps [actions/labeler](https://github.com/actions/labeler) from 5 to 6. - [Release notes](https://github.com/actions/labeler/releases) - [Commits](https://github.com/actions/labeler/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/labeler dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/label.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/label.yaml b/.github/workflows/label.yaml index a34c575680..218327ef47 100644 --- a/.github/workflows/label.yaml +++ b/.github/workflows/label.yaml @@ -9,7 +9,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v6 with: configuration-path: .github/labeler.yaml repo-token: "${{ secrets.GITHUB_TOKEN }}" From d956c4f1d537156aed41c24fbd34309592be7fc1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:07:45 +0000 Subject: [PATCH 10/34] Bump actions/stale from 9 to 10 Bumps [actions/stale](https://github.com/actions/stale) from 9 to 10. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/v9...v10) --- updated-dependencies: - dependency-name: actions/stale dependency-version: '10' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 85138c7bcd..edfe71b693 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -12,7 +12,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@v10 id: stale with: stale-pr-message: This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or it will be closed in 7 days From 0b8db937e72923eb756a405280d415906a7714d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:07:50 +0000 Subject: [PATCH 11/34] Bump actions/setup-python from 5 to 6 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- .github/workflows/build_viewer.yml | 2 +- .github/workflows/pre-commit.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ed0fb48c78..c5fd9621fc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -90,7 +90,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Setup python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.11" diff --git a/.github/workflows/build_viewer.yml b/.github/workflows/build_viewer.yml index f5b0f1f6a8..4d74d5ba3d 100644 --- a/.github/workflows/build_viewer.yml +++ b/.github/workflows/build_viewer.yml @@ -73,7 +73,7 @@ jobs: - name: Set up Python (normal case) if: matrix.container_image != 'ubuntu:22.04' id: py311_setup - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' check-latest: true diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 73ec1c32e9..8f942fa11b 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x - uses: pre-commit/action@v3.0.1 From b8bc108ea791583c9cb3ec188acaee5cb8a48ffd Mon Sep 17 00:00:00 2001 From: Hecklezz Date: Thu, 2 Oct 2025 01:23:36 +1000 Subject: [PATCH 12/34] Partial restore of texture pipeline changes to address texture discard issues --- indra/llrender/llimagegl.cpp | 5 +++++ indra/newview/lltexturecache.cpp | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index d859c9cfac..b1ba3b54c5 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -641,6 +641,11 @@ bool LLImageGL::setSize(S32 width, S32 height, S32 ncomponents, S32 discard_leve if(discard_level > 0) { mMaxDiscardLevel = llmax(mMaxDiscardLevel, (S8)discard_level); + // [FIRE-35361] RenderMaxTextureResolution caps texture resolution lower than intended + // 2K textures could set the mMaxDiscardLevel above MAX_DISCARD_LEVEL, which would + // cause them to not be down-scaled so they would get stuck at 0 discard all the time. + mMaxDiscardLevel = llmax(mMaxDiscardLevel, (S8)MAX_DISCARD_LEVEL); + // [FIRE-35361] } } else diff --git a/indra/newview/lltexturecache.cpp b/indra/newview/lltexturecache.cpp index 014e9311ac..80a9e73f96 100644 --- a/indra/newview/lltexturecache.cpp +++ b/indra/newview/lltexturecache.cpp @@ -2095,6 +2095,30 @@ LLPointer LLTextureCache::readFromFastCache(const LLUUID& id, S32& d } LLPointer raw = new LLImageRaw(data, head[0], head[1], head[2], true); + // + // This fixes the invalid discard values from being created which cause the decoder code to fail when trying to handle 6 and 7's which are above the MAX_DISCARD_LEVEL of 5 + // especially on load + // We will expand the 16x16 texture to the actual MAX_DISCARD_LEVEL texture size, it may be blurry until the user gets closer but 5 discard value should be objects far from the camera. + // So a 1024x1024 texture with a dicard of 6 will become 32x32 and a 2048x2048 texture with a discard of 7 will become a 64x64 texture. + if (discardlevel > MAX_DISCARD_LEVEL) + { + LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("FixBadDiscardLevel"); + + S32 w = head[0]; // Get the current width from the header (16) + S32 h = head[1]; // Get the current height from the header (16) + + // Expand the width and height by teh difference between the discard and MAX_DISCARD_LEVEL bit shifted to the left. (Expand power of 2 textures) + w <<= discardlevel - MAX_DISCARD_LEVEL; + h <<= discardlevel - MAX_DISCARD_LEVEL; + + // Set the discard level to the MAX_DISCARD_LEVEL + discardlevel = MAX_DISCARD_LEVEL; + + // Scale up the texture and scale the actual data, as we just created it above, it should be fine. + raw->scale(w, h, true); + } + // + return raw; } From 49caa5e179e807c2b0977847d0afa9a2223109d7 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 30 Sep 2025 22:44:40 +0300 Subject: [PATCH 13/34] #4758 Extra logging to track discard 6 Plus clamped some values and preventing decode from starting # Conflicts: # indra/newview/llviewertexture.cpp --- indra/llrender/llimagegl.cpp | 3 ++- indra/newview/lltexturefetch.cpp | 21 +++++++++++++++++---- indra/newview/llviewertexture.cpp | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index b1ba3b54c5..4f60946577 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -1548,7 +1548,7 @@ bool LLImageGL::createGLTexture(S32 discard_level, const LLImageRaw* imageraw, S if (discard_level < 0) { llassert(mCurrentDiscardLevel >= 0); - discard_level = mCurrentDiscardLevel; + discard_level = llmin(mCurrentDiscardLevel, MAX_DISCARD_LEVEL); } // Actual image width/height = raw image width/height * 2^discard_level @@ -1648,6 +1648,7 @@ bool LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, bool data_ discard_level = mCurrentDiscardLevel; } discard_level = llclamp(discard_level, 0, (S32)mMaxDiscardLevel); + discard_level = llmin(discard_level, MAX_DISCARD_LEVEL); if (main_thread // <--- always force creation of new_texname when not on main thread ... && !defer_copy // <--- ... or defer copy is set diff --git a/indra/newview/lltexturefetch.cpp b/indra/newview/lltexturefetch.cpp index 334a62c02c..f4f960b8eb 100644 --- a/indra/newview/lltexturefetch.cpp +++ b/indra/newview/lltexturefetch.cpp @@ -1898,10 +1898,10 @@ bool LLTextureFetchWorker::doWork(S32 param) mHttpReplyOffset = 0; mLoadedDiscard = mRequestedDiscard; - if (mLoadedDiscard < 0) + if (mLoadedDiscard < 0 || (mLoadedDiscard > MAX_DISCARD_LEVEL && mFormattedImage->getCodec() == IMG_CODEC_J2C)) { LL_WARNS(LOG_TXT) << mID << " mLoadedDiscard is " << mLoadedDiscard - << ", should be >=0" << LL_ENDL; + << ", should be >=0 and <=" << MAX_DISCARD_LEVEL << LL_ENDL; } setState(DECODE_IMAGE); if (mWriteToCacheState != NOT_WRITE) @@ -1963,14 +1963,27 @@ bool LLTextureFetchWorker::doWork(S32 param) LL_DEBUGS(LOG_TXT) << mID << " DECODE_IMAGE abort: mLoadedDiscard < 0" << LL_ENDL; return true; } + + llassert_always(mFormattedImage.notNull()); + S32 discard = mHaveAllData && mFormattedImage->getCodec() != IMG_CODEC_J2C ? 0 : mLoadedDiscard; + if (discard > MAX_DISCARD_LEVEL) // only warn for j2c + { + // We encode j2c with fixed amount of discard levels, + // Trying to decode beyound that will fail. + LL_WARNS(LOG_TXT) << "Decode entered with invalid discard. ID = " << mID << LL_ENDL; + + //abort, don't decode + setState(DONE); + LL_DEBUGS(LOG_TXT) << mID << " DECODE_IMAGE abort: mLoadedDiscard > MAX_DISCARD_LEVEL" << LL_ENDL; + return true; + } + mDecodeTimer.reset(); mRawImage = NULL; mAuxImage = NULL; - llassert_always(mFormattedImage.notNull()); // if we have the entire image data (and the image is not J2C), decode the full res image // DO NOT decode a higher res j2c than was requested. This is a waste of time and memory. - S32 discard = mHaveAllData && mFormattedImage->getCodec() != IMG_CODEC_J2C ? 0 : mLoadedDiscard; mDecoded = false; setState(DECODE_IMAGE_UPDATE); LL_DEBUGS(LOG_TXT) << mID << ": Decoding. Bytes: " << mFormattedImage->getDataSize() << " Discard: " << discard diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 2d58f624e3..9567f55efa 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -2188,7 +2188,7 @@ bool LLViewerFetchedTexture::updateFetch() static LLCachedControl sTextureDiscardLevel(gSavedSettings, "TextureDiscardLevel"); const U32 override_tex_discard_level = sTextureDiscardLevel(); // - if (override_tex_discard_level != 0) + if (override_tex_discard_level != 0 && override_tex_discard_level <= MAX_DISCARD_LEVEL) { desired_discard = override_tex_discard_level; } From a12e38911e96deb71ce809c32edc7647a1a7536b Mon Sep 17 00:00:00 2001 From: Hecklezz Date: Thu, 2 Oct 2025 02:26:29 +1000 Subject: [PATCH 14/34] Use llmin instead of llmax to correctly clamp mMaxDiscardLevel --- indra/llrender/llimagegl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 4f60946577..6a6eda37dc 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -644,7 +644,7 @@ bool LLImageGL::setSize(S32 width, S32 height, S32 ncomponents, S32 discard_leve // [FIRE-35361] RenderMaxTextureResolution caps texture resolution lower than intended // 2K textures could set the mMaxDiscardLevel above MAX_DISCARD_LEVEL, which would // cause them to not be down-scaled so they would get stuck at 0 discard all the time. - mMaxDiscardLevel = llmax(mMaxDiscardLevel, (S8)MAX_DISCARD_LEVEL); + mMaxDiscardLevel = llmin(mMaxDiscardLevel, (S8)MAX_DISCARD_LEVEL); // [FIRE-35361] } } From 77bdbf965f9f2d86bc4d9dfcf24a671fc9181e57 Mon Sep 17 00:00:00 2001 From: Beq Date: Wed, 1 Oct 2025 17:41:29 +0100 Subject: [PATCH 15/34] Take median over longer period to avoid out-of-bounds warning on gcc Not sure why this only complained today. --- indra/newview/lloutfitslist.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/lloutfitslist.cpp b/indra/newview/lloutfitslist.cpp index f81bc78ed0..246a9ab626 100644 --- a/indra/newview/lloutfitslist.cpp +++ b/indra/newview/lloutfitslist.cpp @@ -1171,7 +1171,7 @@ void LLOutfitListBase::onIdleRefreshList() F64 MAX_TIME = 0.05f; constexpr F64 min_time = 0.001f; constexpr F64 threshold_fps = 30.0; - const auto current_fps = LLTrace::get_frame_recording().getPeriodMedianPerSec(LLStatViewer::FPS, 1); + const auto current_fps = LLTrace::get_frame_recording().getPeriodMedianPerSec(LLStatViewer::FPS,10); if (current_fps < threshold_fps) { MAX_TIME = min_time + (current_fps / threshold_fps) * (MAX_TIME - min_time); From 21ec3ed509bd0143cd9aa487c01cbf34f42169fc Mon Sep 17 00:00:00 2001 From: Beq Date: Wed, 1 Oct 2025 17:42:24 +0100 Subject: [PATCH 16/34] 'this' can't be null so gcc rejects it --- indra/newview/fsposingmotion.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indra/newview/fsposingmotion.cpp b/indra/newview/fsposingmotion.cpp index 9c4772d309..4d9a51b8b3 100644 --- a/indra/newview/fsposingmotion.cpp +++ b/indra/newview/fsposingmotion.cpp @@ -312,7 +312,7 @@ void FSPosingMotion::getJointStateAtTime(std::string jointPoseName, F32 timeToLo bool* hasPosition, LLVector3* jointPosition, bool* hasScale, LLVector3* jointScale) { - if (this == NULL || mJointMotionList == nullptr) + if ( mJointMotionList == nullptr) return; for (U32 i = 0; i < mJointMotionList->getNumJointMotions(); i++) @@ -348,7 +348,7 @@ bool FSPosingMotion::otherMotionAnimatesJoints(LLKeyframeMotion* motionToQuery, bool FSPosingMotion::motionAnimatesJoints(std::string recapturedJointNames) { - if (this == NULL || mJointMotionList == nullptr) + if (mJointMotionList == nullptr) return false; for (U32 i = 0; i < mJointMotionList->getNumJointMotions(); i++) From 052d49786a8a19b9b0eb2b150a6412c5fabcdf53 Mon Sep 17 00:00:00 2001 From: Ansariel Date: Fri, 3 Oct 2025 15:49:56 +0200 Subject: [PATCH 17/34] FIRE-35986: Restore using subfolders for asset cache --- indra/llfilesystem/lldiskcache.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/indra/llfilesystem/lldiskcache.cpp b/indra/llfilesystem/lldiskcache.cpp index 2f96c1ded4..100b1a005f 100644 --- a/indra/llfilesystem/lldiskcache.cpp +++ b/indra/llfilesystem/lldiskcache.cpp @@ -328,7 +328,11 @@ void LLDiskCache::purge() const std::string LLDiskCache::metaDataToFilepath(const LLUUID& id, LLAssetType::EType at) { - return llformat("%s%s%s_%s_0.asset", sCacheDir.c_str(), gDirUtilp->getDirDelimiter().c_str(), CACHE_FILENAME_PREFIX.c_str(), id.asString().c_str()); + // Store assets in subfolders + //return llformat("%s%s%s_%s_0.asset", sCacheDir.c_str(), gDirUtilp->getDirDelimiter().c_str(), CACHE_FILENAME_PREFIX.c_str(), id.asString().c_str()); + char id_string[36]{}; + return llformat("%s%s%c%s%s_%s_0.asset", sCacheDir.c_str(), gDirUtilp->getDirDelimiter().c_str(), id.toStringFast(id_string)[0], gDirUtilp->getDirDelimiter().c_str(), CACHE_FILENAME_PREFIX.c_str(), id.asString().c_str()); + // } const std::string LLDiskCache::getCacheInfo() From 2ee66d2b6fc4d56c58fcf6871ba2632fd7bb4a4c Mon Sep 17 00:00:00 2001 From: PanteraPolnocy Date: Sat, 4 Oct 2025 18:41:21 +0200 Subject: [PATCH 18/34] FIRE-35770 Sounds added to the blacklist keep playing Partially reverts commit 9798fae3d24e3bdf13c678ef751f1d82843a720e pending improvements to the check --- indra/newview/llviewerobject.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index 615c19799d..396dd43130 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -6587,6 +6587,24 @@ void LLViewerObject::setAttachedSound(const LLUUID &audio_uuid, const LLUUID& ow return; } + // Asset blacklist + FSAssetBlacklist& blacklist = FSAssetBlacklist::instance(); + if (blacklist.isBlacklisted(audio_uuid, LLAssetType::AT_SOUND)) + { + return; + } + else if (isAttachment() && blacklist.isBlacklisted(owner_id, LLAssetType::AT_SOUND, FSAssetBlacklist::eBlacklistFlag::WORN)) + { + // Attachment sound + return; + } + else if (blacklist.isBlacklisted(owner_id, LLAssetType::AT_SOUND, FSAssetBlacklist::eBlacklistFlag::REZZED)) + { + // Rezzed object sound + return; + } + // + if (flags & LL_SOUND_FLAG_LOOP && mAudioSourcep && mAudioSourcep->isLoop() && mAudioSourcep->getCurrentData() && mAudioSourcep->getCurrentData()->getID() == audio_uuid) From 6f5d67fd1fce10d4bd88201aebcf3132888c13fc Mon Sep 17 00:00:00 2001 From: PanteraPolnocy Date: Sat, 4 Oct 2025 19:11:31 +0200 Subject: [PATCH 19/34] FIRE-35947 Hide the new 'options' and 'trash' icons at the top of outfits window Currently they do not work, at all - Firestorm uses context menus and icons at the bottom of the panel anyway Change can be reverted when they get some proper usage, right now they're a bit misleading --- indra/newview/llpaneloutfitsinventory.cpp | 5 ++++- indra/newview/llsidepanelappearance.cpp | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/indra/newview/llpaneloutfitsinventory.cpp b/indra/newview/llpaneloutfitsinventory.cpp index b6ae97e834..26a8aa8eea 100644 --- a/indra/newview/llpaneloutfitsinventory.cpp +++ b/indra/newview/llpaneloutfitsinventory.cpp @@ -429,7 +429,10 @@ void LLPanelOutfitsInventory::onTabChange() } if (mTrashMenuPanel) { - mTrashMenuPanel->setVisible(mActivePanel->getTrashMenuVisible()); + // FIRE-35947 Ensure the top menu buttons (gear/sort/trash) are only visible in the outfits panel + // mTrashMenuPanel->setVisible(mActivePanel->getTrashMenuVisible()); + mTrashMenuPanel->setVisible(false); + // } updateVerbs(); diff --git a/indra/newview/llsidepanelappearance.cpp b/indra/newview/llsidepanelappearance.cpp index 5aa420ff5c..5386766379 100644 --- a/indra/newview/llsidepanelappearance.cpp +++ b/indra/newview/llsidepanelappearance.cpp @@ -366,9 +366,9 @@ void LLSidepanelAppearance::toggleMyOutfitsPanel(bool visible, const std::string mCurrOutfitPanel->setVisible(visible); // FIRE-35947 Ensure the top menu buttons (gear/sort/trash) are only visible in the outfits panel - getChildView("options_gear_btn_panel")->setVisible(visible); + getChildView("options_gear_btn_panel")->setVisible(false); getChildView("options_sort_btn_panel")->setVisible(visible); - getChildView("trash_btn_panel")->setVisible(visible); + getChildView("trash_btn_panel")->setVisible(false); // if (visible) From f686fb54e5962e28e55323398084f1450f5befd4 Mon Sep 17 00:00:00 2001 From: minerjr Date: Mon, 6 Oct 2025 05:29:18 -0300 Subject: [PATCH 20/34] mFSAreaSearchActive not initialized in llagent.cpp The bool mFSAreaSearchActive was not initialized in the constructor and is undefined so it may or may not be false. This flag is used for the search feature and is used for setting a region flag to send all objects and not just ones in front of the camera. --- indra/newview/llagent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp index d7a8dc0a22..ce2e942e6b 100644 --- a/indra/newview/llagent.cpp +++ b/indra/newview/llagent.cpp @@ -509,7 +509,7 @@ LLAgent::LLAgent() : mMouselookModeInSignal(NULL), mMouselookModeOutSignal(NULL), - + mFSAreaSearchActive(false), // - Flag was not initialized mPhantom(false), restoreToWorld(false) { From 56cb655fbe20f8cc2fb2749c2b99aaffd5f6d683 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Wed, 1 Oct 2025 20:33:53 +0300 Subject: [PATCH 21/34] #4773 Crash on sub_image_lines (FS:TJ- Follow-up cherry-pick to fix out of bounds discard_levels) Just caught it and discard_level is somehow 7, which seems like it resulted in src going out of bounds, which crashed glTexSubImage2D --- indra/llrender/llimagegl.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 6a6eda37dc..469603bc5c 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -1101,6 +1101,8 @@ void sub_image_lines(U32 target, S32 miplevel, S32 x_offset, S32 y_offset, S32 w // full width texture, do 32 lines at a time for (U32 y_pos = y_offset; y_pos < y_offset_end; y_pos += batch_size) { + // If this keeps crashing, pass down data_size, looks like it is using + // imageraw->getData(); for data, but goes way over allocated size limit glTexSubImage2D(target, miplevel, x_offset, y_pos, width, batch_size, pixformat, pixtype, src); src += line_width * batch_size; } @@ -1110,6 +1112,8 @@ void sub_image_lines(U32 target, S32 miplevel, S32 x_offset, S32 y_offset, S32 w // partial width or strange height for (U32 y_pos = y_offset; y_pos < y_offset_end; y_pos += 1) { + // If this keeps crashing, pass down data_size, looks like it is using + // imageraw->getData(); for data, but goes way over allocated size limit glTexSubImage2D(target, miplevel, x_offset, y_pos, width, 1, pixformat, pixtype, src); src += line_width; } @@ -1548,8 +1552,9 @@ bool LLImageGL::createGLTexture(S32 discard_level, const LLImageRaw* imageraw, S if (discard_level < 0) { llassert(mCurrentDiscardLevel >= 0); - discard_level = llmin(mCurrentDiscardLevel, MAX_DISCARD_LEVEL); + discard_level = mCurrentDiscardLevel; } + discard_level = llmin(discard_level, MAX_DISCARD_LEVEL); // Actual image width/height = raw image width/height * 2^discard_level S32 raw_w = imageraw->getWidth() ; From 1e254bb4c7563efea3a211b2ef46acd29d0fd6d9 Mon Sep 17 00:00:00 2001 From: PanteraPolnocy Date: Wed, 8 Oct 2025 14:50:11 +0200 Subject: [PATCH 22/34] Add an option to disable reset buttons in camera floaters --- indra/newview/app_settings/settings.xml | 11 +++++++++++ indra/newview/lljoystickbutton.cpp | 13 +++++++++++++ .../skins/default/xui/en/panel_preferences_UI.xml | 9 +++++++++ .../skins/default/xui/pl/panel_preferences_UI.xml | 1 + 4 files changed, 34 insertions(+) diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index d7f957bb82..82c10cfc58 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -23397,6 +23397,17 @@ Change of this parameter will affect the layout of buttons in notification toast Value 0 + DisableCameraJoystickCenterReset + + Comment + Disable center reset on camera joysticks (bullseye) in camera controls. + Persist + 1 + Type + Boolean + Value + 0 + FSNetMapScripted Comment diff --git a/indra/newview/lljoystickbutton.cpp b/indra/newview/lljoystickbutton.cpp index 16867f88c7..54255ac37c 100644 --- a/indra/newview/lljoystickbutton.cpp +++ b/indra/newview/lljoystickbutton.cpp @@ -38,6 +38,7 @@ #include "llagent.h" #include "llagentcamera.h" #include "llviewercamera.h" +#include "llviewercontrol.h" // gSavedSettings #include "llviewertexture.h" #include "llviewertexturelist.h" #include "llviewerwindow.h" @@ -568,6 +569,12 @@ void LLJoystickCameraRotate::onHeldDown() void LLJoystickCameraRotate::resetJoystickCamera() { + // If user opted to disable center reset buttons, do not reset + if (gSavedSettings.getBOOL("DisableCameraJoystickCenterReset")) + { + return; + } + // gAgentCamera.resetCameraOrbit(); } @@ -735,6 +742,12 @@ void LLJoystickCameraTrack::onHeldDown() void LLJoystickCameraTrack::resetJoystickCamera() { + // If user opted to disable center reset buttons, do not reset + if (gSavedSettings.getBOOL("DisableCameraJoystickCenterReset")) + { + return; + } + // gAgentCamera.resetCameraPan(); } diff --git a/indra/newview/skins/default/xui/en/panel_preferences_UI.xml b/indra/newview/skins/default/xui/en/panel_preferences_UI.xml index d1d80688dd..df29700ae7 100644 --- a/indra/newview/skins/default/xui/en/panel_preferences_UI.xml +++ b/indra/newview/skins/default/xui/en/panel_preferences_UI.xml @@ -989,6 +989,15 @@ width="270" control_name="FSUseSmallCameraFloater" tool_tip="If enabled, the smaller, legacy camera window without camera preset controls will be used."/> + + Używaj osobnych okien dla: From 821adc997d93b6f0f2c968f7006bccf915b4c130 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:51:42 +0300 Subject: [PATCH 23/34] #4794 SceneLoadRearMaxRadiusFraction not stored as a fraction - Cherry-pick to fix issue FIRE-36003 Reported issue to LL GitHub and FS JIRA FIRE-36003 LL Issue: https://github.com/secondlife/viewer/issues/4794 Linked to FS issue and cherry picked to pull in fix. Inital Issue: When you look at the SceneLoadRearMaxRadiusFraction value stored in the settings.xml, you will notice that the value is set to 75.0 for a float value. And the comments mention it is suppose to be fraction. The code that uses it is for scaling the amount of the scene behind the visible camera is loaded. The issue is because the calculation no longer is divided by 100.0f, the draw distance is multiplied by the value and then clamped at the draw distance value. So it is effectively only ever the draw distance and not the default 75% of it. So a 256 draw distance is locked at 256 meters instead of 192. This causes more objects to load behind the user, especially very far objects which can cause more RAM and VRAM usage, especially on teleport. --- indra/newview/app_settings/settings.xml | 4 ++-- indra/newview/llvocache.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 82c10cfc58..61449e0baa 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -14011,13 +14011,13 @@ Change of this parameter will affect the layout of buttons in notification toast SceneLoadRearMaxRadiusFraction Comment - a percentage of draw distance beyond which all objects outside of view frustum will be unloaded, regardless of pixel threshold + a fraction of draw distance beyond which all objects outside of view frustum will be unloaded, regardless of pixel threshold Persist 1 Type F32 Value - 75.0 + 0.75 SceneLoadRearPixelThreshold diff --git a/indra/newview/llvocache.cpp b/indra/newview/llvocache.cpp index 0aa7371a1f..917842c71f 100644 --- a/indra/newview/llvocache.cpp +++ b/indra/newview/llvocache.cpp @@ -508,7 +508,7 @@ void LLVOCacheEntry::updateDebugSettings() sNearRadius = MIN_RADIUS + ((clamped_min_radius - MIN_RADIUS) * adjust_factor); // a percentage of draw distance beyond which all objects outside of view frustum will be unloaded, regardless of pixel threshold - static LLCachedControl rear_max_radius_frac(gSavedSettings,"SceneLoadRearMaxRadiusFraction"); + static LLCachedControl rear_max_radius_frac(gSavedSettings,"SceneLoadRearMaxRadiusFraction", .75f); const F32 min_radius_plus_one = sNearRadius + 1.f; const F32 max_radius = rear_max_radius_frac * gAgentCamera.mDrawDistance; const F32 clamped_max_radius = llclamp(max_radius, min_radius_plus_one, draw_radius); // [sNearRadius, mDrawDistance] From f0d5e27b397982276b319063a67ee56fea13ccfe Mon Sep 17 00:00:00 2001 From: Ansariel Date: Thu, 9 Oct 2025 19:28:29 +0200 Subject: [PATCH 24/34] Update German translation --- indra/newview/skins/default/xui/de/panel_preferences_UI.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/indra/newview/skins/default/xui/de/panel_preferences_UI.xml b/indra/newview/skins/default/xui/de/panel_preferences_UI.xml index a6332de47d..92e474c815 100644 --- a/indra/newview/skins/default/xui/de/panel_preferences_UI.xml +++ b/indra/newview/skins/default/xui/de/panel_preferences_UI.xml @@ -151,6 +151,7 @@ + Eigene Fenster verwenden für: From 6e2a86f6b910468f8da7b09e0881a9b7681f40b3 Mon Sep 17 00:00:00 2001 From: Maxim Nikolenko Date: Tue, 7 Oct 2025 17:25:50 +0300 Subject: [PATCH 25/34] #4772 fix group ownership detection for deeded objects (FS:TJ- Fixes issue FIRE-35983) --- indra/llinventory/llpermissions.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/indra/llinventory/llpermissions.cpp b/indra/llinventory/llpermissions.cpp index 84fde40a1f..03cd7771d4 100644 --- a/indra/llinventory/llpermissions.cpp +++ b/indra/llinventory/llpermissions.cpp @@ -774,6 +774,7 @@ void LLPermissions::importLLSD(const LLSD& sd_perm) } } + fixOwnership(); fix(); } From 7e84883026296d850add269f69b0070890415572 Mon Sep 17 00:00:00 2001 From: PanteraPolnocy Date: Thu, 9 Oct 2025 20:42:28 +0200 Subject: [PATCH 26/34] FIRE-36002 Update Japanese translation, by Logue Takacs --- .../skins/default/xui/ja/floater_post_process.xml | 2 +- indra/newview/skins/default/xui/ja/menu_viewer.xml | 14 +++++++------- .../newview/skins/default/xui/ja/notifications.xml | 12 ++++++------ .../skins/default/xui/ja/panel_preferences_UI.xml | 1 + indra/newview/skins/default/xui/ja/strings.xml | 8 ++++---- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/indra/newview/skins/default/xui/ja/floater_post_process.xml b/indra/newview/skins/default/xui/ja/floater_post_process.xml index b945b2b623..fd0109aea0 100644 --- a/indra/newview/skins/default/xui/ja/floater_post_process.xml +++ b/indra/newview/skins/default/xui/ja/floater_post_process.xml @@ -18,7 +18,7 @@ - + diff --git a/indra/newview/skins/default/xui/ja/menu_viewer.xml b/indra/newview/skins/default/xui/ja/menu_viewer.xml index 9b7ae6c415..71e772c0c1 100644 --- a/indra/newview/skins/default/xui/ja/menu_viewer.xml +++ b/indra/newview/skins/default/xui/ja/menu_viewer.xml @@ -50,9 +50,9 @@ - - - + + + @@ -141,11 +141,11 @@ - - - + + + - + diff --git a/indra/newview/skins/default/xui/ja/notifications.xml b/indra/newview/skins/default/xui/ja/notifications.xml index e5b0806a46..3aec4ce361 100644 --- a/indra/newview/skins/default/xui/ja/notifications.xml +++ b/indra/newview/skins/default/xui/ja/notifications.xml @@ -5515,14 +5515,14 @@ Firestorm Viewer、Phoenix Firestorm Viewer Project Inc.、またはそのチー 圧縮:[PACK_TIME]秒 [PSIZE]㎅ 解凍:[UNPACK_TIME]秒 [USIZE]㎅ - + [MESSAGE]