diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f4f59e794f..31bf2ecfb6 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" - name: Checkout build variables 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/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/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 }}" 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 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 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 diff --git a/indra/llfilesystem/lldiskcache.cpp b/indra/llfilesystem/lldiskcache.cpp index 7d9a9c1072..fa2214dcdf 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() diff --git a/indra/llimage/llimagej2c.cpp b/indra/llimage/llimagej2c.cpp index 1cb92d5c48..23ef3d74e8 100644 --- a/indra/llimage/llimagej2c.cpp +++ b/indra/llimage/llimagej2c.cpp @@ -276,29 +276,111 @@ S32 LLImageJ2C::calcDataSizeJ2C(S32 w, S32 h, S32 comp, S32 discard_level, F32 r // Estimate the number of layers. This is consistent with what's done for j2c encoding in LLImageJ2CKDU::encodeImpl(). constexpr S32 precision = 8; // assumed bitrate per component channel, might change in future for HDR support constexpr S32 max_components = 4; // assumed the file has four components; three color and alpha - // Use MAX_IMAGE_SIZE_DEFAULT (currently 2048) if either dimension is unknown (zero) - S32 width = (w > 0) ? w : 2048; - S32 height = (h > 0) ? h : 2048; - S32 max_dimension = llmax(width, height); // Find largest dimension - S32 block_area = MAX_BLOCK_SIZE * MAX_BLOCK_SIZE; // Calculated initial block area from established max block size (currently 64) - S32 max_layers = (S32)llmax(llround(log2f((float)max_dimension) - log2f((float)MAX_BLOCK_SIZE)), 4); // Find number of powers of two between extents and block size to a minimum of 4 - block_area *= llmax(max_layers, 1); // Adjust initial block area by max number of layers - S32 totalbytes = (S32) (MIN_LAYER_SIZE * max_components * precision); // Start estimation with a minimum reasonable size - S32 block_layers = 0; - while (block_layers <= max_layers) // Walk the layers + + // [FIRE-35987] slow textures due to overfetch in j2c header size estimation + // Preserve recent LL body for reference (see PRs #2406, #2525, #4018, #4020): + // // Use MAX_IMAGE_SIZE_DEFAULT (currently 2048) if either dimension is unknown (zero) + // S32 width = (w > 0) ? w : 2048; + // S32 height = (h > 0) ? h : 2048; + // S32 max_dimension = llmax(width, height); // Find largest dimension + // S32 block_area = MAX_BLOCK_SIZE * MAX_BLOCK_SIZE; // Calculated initial block area from established max block size (currently 64) + // S32 max_layers = (S32)llmax(llround(log2f((float)max_dimension) - log2f((float)MAX_BLOCK_SIZE)), 4); // Find number of powers of two between extents and block size to a minimum of 4 + // block_area *= llmax(max_layers, 1); // Adjust initial block area by max number of layers + // S32 totalbytes = (S32) (MIN_LAYER_SIZE * max_components * precision); // Start estimation with a minimum reasonable size + // S32 block_layers = 0; + // while (block_layers <= max_layers) // Walk the layers + // { + // if (block_layers <= (5 - discard_level)) // Walk backwards from discard 5 to required discard layer. + // totalbytes += (S32) (block_area * max_components * precision * rate); // Add each block layer reduced by assumed compression rate + // block_layers++; // Move to next layer + // block_area *= 4; // Increase block area by power of four + // } + // totalbytes /= 8; // to bytes + // totalbytes += calcHeaderSizeJ2C(); // header + // + // return totalbytes; + + // --- Use 7.1.11 basis with fixes implied by LL PRs --- + (void)comp; // retained for parity with the viewer signature + const S32 hard_cap = 12; // sanity cap + const S64 base_layer_area = static_cast(MAX_BLOCK_SIZE) * static_cast(MAX_BLOCK_SIZE); // 64x64 blocks at discard 5 + const S32 discard_layers = std::max(5 - discard_level, 0); + const double rate64 = static_cast(rate); + const S64 header_bytes = static_cast(calcHeaderSizeJ2C()); + const S64 bits_per_tile = static_cast(max_components) * static_cast(precision); + + // helper lambda: layer area to estimated bit budget + auto scaled_bits = [rate64, bits_per_tile](S64 layer_area) -> S64 { - if (block_layers <= (5 - discard_level)) // Walk backwards from discard 5 to required discard layer. - totalbytes += (S32) (block_area * max_components * precision * rate); // Add each block layer reduced by assumed compression rate - block_layers++; // Move to next layer - block_area *= 4; // Increase block area by power of four + const S64 layer_bits = layer_area * bits_per_tile; + return static_cast(std::llround(static_cast(layer_bits) * rate64)); + }; + + // If dimensions are unknown, provide a *reliable discard-5 estimate* without assuming a 2k texture. + // This lets the fetcher skip a header pass for d5 while avoiding the large overfetch seen previously. + S64 total_bits = 0; + if (w <= 0 || h <= 0) + { + total_bits = scaled_bits(base_layer_area); } + else + { + // Classic surface walk: start at 64x64 and grow by 4x until we cover the surface. + const S64 surface = static_cast(w) * static_cast(h); - totalbytes /= 8; // to bytes - totalbytes += calcHeaderSizeJ2C(); // header + S64 layer_area = base_layer_area; + S32 nb_layers = 1; - return totalbytes; + // First layer (7.1.11 did first-term outside loop). Keep it explicit for clarity. + total_bits = scaled_bits(layer_area); + + while (surface > layer_area && nb_layers < hard_cap) + { + if (nb_layers <= discard_layers) + { + total_bits += scaled_bits(layer_area); + } + ++nb_layers; + layer_area *= 4; + } + + // Allow extra layers for large (2k+) assets uploaded with 7–8 layers (from LL change justification) + // If the max dimension implies more pyramid steps than the surface loop used, extend the walk up to that. + // Note: this mostly affect long thin textures like 2048x256 as best I can tell. + { + const S32 max_dimension = std::max(w, h); + const float ratio = (max_dimension > 0) + ? static_cast(max_dimension) / static_cast(MAX_BLOCK_SIZE) + : 0.0f; + const S32 dimension_layers = (ratio > 0.0f) + ? std::max(static_cast(std::floor(std::log2(ratio))) + 1, 1) + : 1; + + if (dimension_layers > nb_layers) + { + S32 extra = std::min(dimension_layers, hard_cap) - nb_layers; + while (extra-- > 0) + { + if (nb_layers <= discard_layers) + { + total_bits += scaled_bits(layer_area); + } + ++nb_layers; + layer_area *= 4; + } + } + } + } + // Convert to bytes and add header. + S64 est = total_bits / 8; + est += header_bytes; + + est = llclamp(est, (S64)0, (S64)std::numeric_limits::max()); + return static_cast(est); + // } + S32 LLImageJ2C::calcHeaderSize() { return calcHeaderSizeJ2C(); 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(); } diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 8ddc8c5e71..5ed1f3b687 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -642,6 +642,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 = llmin(mMaxDiscardLevel, (S8)MAX_DISCARD_LEVEL); + // [FIRE-35361] } } else @@ -1097,6 +1102,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; } @@ -1106,6 +1113,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; } @@ -1546,6 +1555,7 @@ bool LLImageGL::createGLTexture(S32 discard_level, const LLImageRaw* imageraw, S llassert(mCurrentDiscardLevel >= 0); 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() ; @@ -1644,6 +1654,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/CMakeLists.txt b/indra/newview/CMakeLists.txt index a43c102ca7..3e2827daa5 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 @@ -1008,6 +1009,7 @@ set(viewer_HEADER_FILES fsparticipantlist.h fspose.h fsposeranimator.h + fsposestate.h fsposingmotion.h fsprimfeedauth.h fsradar.h diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index f22d2b5b84..41e05c58f8 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 @@ -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 @@ -26576,6 +26587,17 @@ Change of this parameter will affect the layout of buttons in notification toast Value 0 + FSSnapshotShowGuides + + Comment + If enabled, shows composition guides inside the snapshot frame. + Persist + 1 + Type + Boolean + Value + 0 + FSSnapshotFrameBorderColor Comment diff --git a/indra/newview/app_settings/shaders/class1/post/snapshotFrameF.glsl b/indra/newview/app_settings/shaders/class1/post/snapshotFrameF.glsl index 1e3576d19e..aed4eecc60 100644 --- a/indra/newview/app_settings/shaders/class1/post/snapshotFrameF.glsl +++ b/indra/newview/app_settings/shaders/class1/post/snapshotFrameF.glsl @@ -20,7 +20,7 @@ void main() vec4 frame_rect_px = vec4(frame_rect.x * screen_res.x, frame_rect.y * screen_res.y, frame_rect.z * screen_res.x, frame_rect.w * screen_res.y); vec4 border_rect_px = vec4( (frame_rect.x * screen_res.x) - border_thickness, - (frame_rect.y * screen_res.y) - border_thickness, + (frame_rect.y * screen_res.y) - border_thickness, (frame_rect.z * screen_res.x) + border_thickness, (frame_rect.w * screen_res.y) + border_thickness); @@ -30,7 +30,7 @@ void main() { // Simple box blur vec3 blur_color = vec3(0.0); - float blur_size = 2; + float blur_size = 2; int blur_samples = 9; for (int x = -1; x <= 1; ++x) @@ -44,15 +44,15 @@ void main() blur_color /= float(blur_samples); float gray = dot(blur_color, vec3(0.299, 0.587, 0.114)); - diff.rgb = vec3(gray); + diff.rgb = vec3(gray); } else { // Draw border around the snapshot frame - if ((tc.x >= border_rect_px.x && tc.x < frame_rect_px.x) || - (tc.x <= border_rect_px.z && tc.x > frame_rect_px.z) || - (tc.y >= border_rect_px.y && tc.y < frame_rect_px.y) || - (tc.y <= border_rect_px.w && tc.y > frame_rect_px.w)) + if ((tc.x >= border_rect_px.x && tc.x < frame_rect_px.x) || + (tc.x > frame_rect_px.z && tc.x <= border_rect_px.z) || + (tc.y >= border_rect_px.y && tc.y < frame_rect_px.y) || + (tc.y > frame_rect_px.w && tc.y <= border_rect_px.w)) { diff.rgb = mix(diff.rgb, border_color, 0.5); } diff --git a/indra/newview/fsfloaterposer.cpp b/indra/newview/fsfloaterposer.cpp index 7594f57960..02202e34ee 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,12 @@ 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; + if (savingDiff) + mPoserAnimator.savePosingState(avatar, &record); + LLVector3 rotation, position, scale, zeroVector; bool baseRotationIsZero; @@ -523,12 +529,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 +572,6 @@ bool FSFloaterPoser::savePoseToXml(LLVOAvatar* avatar, const std::string& poseFi return false; } - return true; } @@ -663,6 +671,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 +902,39 @@ void FSFloaterPoser::onPoseMenuAction(const LLSD& param) else if (loadStyle == "selective_rot") loadType = SELECTIVE_ROT; + mLoadPoseTimer->tryLoading(poseName, loadType); + setLoadingProgress(true); +} + +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())) + { + mLoadPoseTimer->completeLoading(); + onJointTabSelect(); + refreshJointScrollListMembers(); + setSavePosesButtonText(!mPoserAnimator.allBaseRotationsAreZero(avatar)); + } + + if (mLoadPoseTimer->loadCompleteOrFailed()) + setLoadingProgress(false); +} + +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 +1055,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 +1085,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) @@ -1108,32 +1146,44 @@ void FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& pose setJointBaseRotationToZero = startFromZeroRot; if (loadPositions && control_map.has("position")) - { vec3.setValue(control_map["position"]); - mPoserAnimator.loadJointPosition(avatar, poserJoint, loadPositionsAndScalesAsDeltas, vec3); - } + else + vec3.clear(); + + mPoserAnimator.loadJointPosition(avatar, poserJoint, loadPositionsAndScalesAsDeltas, vec3); if (loadRotations && control_map.has("rotation")) - { vec3.setValue(control_map["rotation"]); - mPoserAnimator.loadJointRotation(avatar, poserJoint, setJointBaseRotationToZero, vec3); - } + else + vec3.clear(); + + mPoserAnimator.loadJointRotation(avatar, poserJoint, setJointBaseRotationToZero, vec3); if (loadScales && control_map.has("scale")) - { vec3.setValue(control_map["scale"]); - mPoserAnimator.loadJointScale(avatar, poserJoint, loadPositionsAndScalesAsDeltas, vec3); - } + else + vec3.clear(); + + mPoserAnimator.loadJointScale(avatar, poserJoint, loadPositionsAndScalesAsDeltas, vec3); 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 && !startFromZeroRot) + 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() @@ -1777,6 +1827,9 @@ void FSFloaterPoser::setUiSelectedAvatarSaveFileName(const std::string& saveFile LLVOAvatar* FSFloaterPoser::getAvatarByUuid(const LLUUID& avatarToFind) const { + if (avatarToFind.isNull()) + return nullptr; + for (LLCharacter* character : LLCharacter::sInstances) { if (avatarToFind != character->getID()) @@ -2720,3 +2773,30 @@ void FSFloaterPoser::onClickLockWorldRotBtn() refreshTextHighlightingOnJointScrollLists(); } + +FSLoadPoseTimer::FSLoadPoseTimer(FSLoadPoseTimer::callback_t callback) : LLEventTimer(0.5f), mCallback(callback) +{ +} + +bool FSLoadPoseTimer::tick() +{ + if (!mAttemptLoading) + return false; + if (mCallback.empty()) + return false; + + if (mLoadAttempts >= mMaxLoadAttempts) + mAttemptLoading = false; + + mLoadAttempts++; + mCallback(); + 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..a88f690a4b 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,27 @@ 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); + bool loadCompleteOrFailed() const { return !mAttemptLoading && mLoadAttempts > 0; } + void completeLoading() { mAttemptLoading = false; } + std::string getPosePath() { return mPoseFullPath; }; + E_LoadPoseMethods getLoadMethod() const { return mLoadType; }; + +private: + callback_t mCallback; + bool mAttemptLoading = false; + E_LoadPoseMethods mLoadType = ROT_POS_AND_SCALES; + std::string mPoseFullPath; + int mLoadAttempts = 0; + const int mMaxLoadAttempts = 5; +}; + #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..f87856795f 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. @@ -221,32 +271,50 @@ class FSJointPose 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(); } - void zeroBaseRotation() { mBaseRotation = LLQuaternion::DEFAULT; } + void zeroBaseRotation() + { + mBasePriority = LLJoint::LOW_PRIORITY; + mBaseRotation = LLQuaternion::DEFAULT; + mJointRotationIsMirrored = false; + } void revertJointToBase(LLJoint* joint) const { @@ -281,6 +349,44 @@ class FSJointPose return newPublicRot *= ~initalPublicRot; } + void resetBaseRotation(LLQuaternion rotation, LLJoint::JointPriority priority) + { + if (mUserSpecifiedBaseZero) + return; + + if (priority < mBasePriority) + return; + + if (rotation == LLQuaternion::DEFAULT) + return; + + mBasePriority = priority; + mBaseRotation.set(rotation); + } + + void resetBasePosition(LLVector3 position, LLJoint::JointPriority priority) + { + if (priority < mBasePriority) + return; + + mBasePriority = priority; + mBasePosition.set(position); + } + + void resetBaseScale(LLVector3 scale, LLJoint::JointPriority priority) + { + if (priority < mBasePriority) + return; + + if (scale.isExactlyZero()) + return; + + mBasePriority = priority; + mBaseScale.set(scale); + } + + void setPriority(LLJoint::JointPriority priority) { mBasePriority = priority; } + private: FSJointState(FSJointState* state) { @@ -292,8 +398,12 @@ class FSJointPose 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 +411,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. @@ -318,6 +437,7 @@ class FSJointPose LLQuaternion mBaseRotation; LLVector3 mBasePosition; LLVector3 mBaseScale; + LLJoint::JointPriority mBasePriority = LLJoint::LOW_PRIORITY; }; private: diff --git a/indra/newview/fsposeranimator.cpp b/indra/newview/fsposeranimator.cpp index ba91cf356f..067c4dbdd0 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)) @@ -901,7 +934,10 @@ void FSPoserAnimator::loadJointPosition(LLVOAvatar* avatar, const FSPoserJoint* if (loadPositionAsDelta) jointPose->setPublicPosition(position); else - jointPose->setPublicPosition(position); + { + jointPose->setJointPriority(LLJoint::LOW_PRIORITY); + jointPose->setBasePosition(position, LLJoint::LOW_PRIORITY); + } } void FSPoserAnimator::loadJointScale(LLVOAvatar* avatar, const FSPoserJoint* joint, bool loadScaleAsDelta, LLVector3 scale) @@ -920,7 +956,60 @@ void FSPoserAnimator::loadJointScale(LLVOAvatar* avatar, const FSPoserJoint* joi if (loadScaleAsDelta) jointPose->setPublicScale(scale); else - jointPose->setPublicScale(scale); + { + jointPose->setJointPriority(LLJoint::LOW_PRIORITY); + jointPose->setBaseScale(scale, LLJoint::LOW_PRIORITY); + jointPose->setPublicScale(LLVector3::zero); + } +} + +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; + + 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 @@ -948,6 +1037,8 @@ bool FSPoserAnimator::tryPosingAvatar(LLVOAvatar* avatar) if (avatar->isSelf()) gAgent.stopFidget(); + mPosingState.captureMotionStates(avatar); + avatar->startDefaultMotions(); avatar->startMotion(posingMotion->motionId()); @@ -957,6 +1048,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 +1073,7 @@ void FSPoserAnimator::stopPosingAvatar(LLVOAvatar *avatar) if (!posingMotion) return; + mPosingState.purgeMotionStates(avatar); avatar->stopMotion(posingMotion->motionId()); } @@ -1006,7 +1114,6 @@ FSPosingMotion* FSPoserAnimator::findOrCreatePosingMotion(LLVOAvatar* avatar) sAvatarIdToRegisteredAnimationId[avatar->getID()] = animationAssetId; return dynamic_cast(avatar->createMotion(animationAssetId)); - } bool FSPoserAnimator::isAvatarSafeToUse(LLVOAvatar* avatar) const @@ -1118,7 +1225,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..880997a750 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,53 @@ 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); + + /// + /// 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 addOrUpdatePosingState(LLVOAvatar* avatar, LLUUID animId, F32 updateTime, std::string jointNames, int captureOrder); + + /// + /// 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 +852,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..ea94565d7d --- /dev/null +++ b/indra/newview/fsposestate.cpp @@ -0,0 +1,268 @@ +#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.motionId = anim_it->first; + newState.lastUpdateTime = motion->getLastUpdateTime(); + newState.captureOrder = 0; + newState.avatarOwnsPose = canSaveMotionId(avatar, anim_it->first); + + 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();) + { + std::string joints = (*it).jointNamesAnimated; + bool recaptureMatches = !joints.empty() && !jointNamesRecaptured.empty() && jointNamesRecaptured.find(joints) != std::string::npos; + + if (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 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 = motionIdMatches && updateTimesMatch; + if (foundMatch) + break; + } + + if (foundMatch) + continue; + + fsMotionState newState; + newState.motionId = anim_it->first; + newState.lastUpdateTime = motion->getLastUpdateTime(); + newState.jointNamesAnimated = jointNamesRecaptured; + newState.captureOrder = sCaptureOrder[avatar->getID()]; + newState.avatarOwnsPose = canSaveMotionId(avatar, anim_it->first); + + sMotionStates[avatar->getID()].push_back(newState); + } +} + +bool FSPoseState::addOrUpdatePosingMotionState(LLVOAvatar* avatar, LLUUID animId, F32 updateTime, std::string jointNames, int captureOrder) +{ + if (!avatar) + return false; + + bool foundMatch = false; + for (auto it = sMotionStates[avatar->getID()].begin(); it != sMotionStates[avatar->getID()].end(); it++) + { + bool motionIdMatches = (*it).motionId == animId; + bool updateTimesMatch = (*it).lastUpdateTime == updateTime; + bool jointNamesMatch = (*it).jointNamesAnimated == jointNames; + bool captureOrdersMatch = (*it).captureOrder == captureOrder; + + foundMatch = motionIdMatches && updateTimesMatch && jointNamesMatch && captureOrdersMatch; + if (foundMatch) + return false; + } + + fsMotionState newState; + newState.motionId = animId; + newState.lastUpdateTime = updateTime; + newState.jointNamesAnimated = jointNames; + newState.captureOrder = captureOrder; + newState.avatarOwnsPose = false; + + sMotionStates[avatar->getID()].push_back(newState); + return true; +} + +void FSPoseState::purgeMotionStates(LLVOAvatar* avatar) +{ + if (!avatar) + return; + + sMotionStates[avatar->getID()].clear(); +} + +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->avatarOwnsPose) + 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; + } +} + +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.avatarOwnsPose = true; + + 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++) + { + needPriorityReset = it->captureOrder > lastCaptureOrder; + + if (it->motionApplied) + continue; + + LLKeyframeMotion* kfm = dynamic_cast(avatar->findMotion(it->motionId)); + + if (kfm) + { + if (needPriorityReset) + { + lastCaptureOrder = it->captureOrder; + 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->motionApplied) + continue; + if (it->captureOrder != captureOrder) + continue; + + posingMotion->resetBonePriority(it->jointNamesAnimated); + } +} + +bool FSPoseState::canSaveMotionId(LLVOAvatar* avatar, LLAssetID motionId) +{ + if (!gAgentAvatarp || gAgentAvatarp.isNull()) + return false; + + // does the animation exist in inventory + LLInventoryItem* item = gInventory.getItem(motionId); + if (item && item->getPermissions().getOwner() == avatar->getID()) + return true; + + for (const auto& [anim_object_id, anim_anim_id] : gAgentAvatarp->mAnimationSources) + { + if (anim_anim_id != motionId) + continue; + + // is the item that started the anim in inventory + item = gInventory.getItem(anim_object_id); + if (item && item->getPermissions().getOwner() == avatar->getID()) + return true; + + // is the item that start the animation in-world + LLViewerObject* object = gObjectList.findObject(anim_object_id); + if (object && object->permYouOwner()) + return true; + + return false; + } + + return false; +} diff --git a/indra/newview/fsposestate.h b/indra/newview/fsposestate.h new file mode 100644 index 0000000000..e941e28d70 --- /dev/null +++ b/indra/newview/fsposestate.h @@ -0,0 +1,172 @@ +/** + * @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. + 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); + + /// + /// 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. + /// + /// 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); + +private: + /// + /// A class documenting the state of an animation for an avatar. + /// + class fsMotionState + { + public: + /// + /// 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; + + /// + /// Whether the avatar owns the pose, or the pose was loaded. + /// + bool avatarOwnsPose = false; + + /// + /// When reloading, larger numbers are loaded last, nesting order and priority. + /// This is used to represent recaptures, where joints could be animated with different poses. + /// + int captureOrder = 0; + + /// + /// When reloading, and if not-empty, the names of the bones this motionId should affect. + /// + std ::string jointNamesAnimated; + }; + + /// + /// Resets the priority for the named joints for the supplied posing motion at the supplied capture order. + /// + /// The avatar being posed by the motion. + /// The posing motion. + /// The order of the capture. + void resetPriorityForCaptureOrder(LLVOAvatar* avatar, FSPosingMotion* posingMotion, int captureOrder); + + /// + /// Gets whether the supplied avatar owns, and thus can save information about the supplied asset ID. + /// + /// The avatar to query ownership for. + /// The asset ID of the object. + /// True if the avatar owns the asset, otherwise false. + bool canSaveMotionId(LLVOAvatar* avatar, LLAssetID 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..4d9a51b8b3 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 ( 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 (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/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) { diff --git a/indra/newview/llinventorybridge.cpp b/indra/newview/llinventorybridge.cpp index f6033d6c04..e06a4dade7 100644 --- a/indra/newview/llinventorybridge.cpp +++ b/indra/newview/llinventorybridge.cpp @@ -9353,7 +9353,9 @@ void LLRecentItemsFolderBridge::buildContextMenu(LLMenuGL& menu, U32 flags) buildContextMenuOptions(flags, items, disabled_items); items.erase(std::remove(items.begin(), items.end(), std::string("New Folder")), items.end()); - items.erase(std::remove(items.begin(), items.end(), std::string("New folder from selected")), items.end()); + // [FIRE-35996] Restore allowing creating folder from selected on recent and favorites panels + //items.erase(std::remove(items.begin(), items.end(), std::string("New folder from selected")), items.end()); + // hide_context_entries(menu, items, disabled_items); } @@ -9398,7 +9400,9 @@ void LLFavoritesFolderBridge::buildContextMenu(LLMenuGL& menu, U32 flags) buildContextMenuOptions(flags, items, disabled_items); items.erase(std::remove(items.begin(), items.end(), std::string("New Folder")), items.end()); - items.erase(std::remove(items.begin(), items.end(), std::string("New folder from selected")), items.end()); + // [FIRE-35996] Restore allowing creating folder from selected on recent and favorites panels + //items.erase(std::remove(items.begin(), items.end(), std::string("New folder from selected")), items.end()); + // hide_context_entries(menu, items, disabled_items); } 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/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); 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) 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; } diff --git a/indra/newview/lltexturefetch.cpp b/indra/newview/lltexturefetch.cpp index efdcc5e8df..db5c52ba02 100644 --- a/indra/newview/lltexturefetch.cpp +++ b/indra/newview/lltexturefetch.cpp @@ -1899,10 +1899,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) @@ -1964,14 +1964,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/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) diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index bc19f5c04d..5036acbdaa 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -2229,7 +2229,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; } diff --git a/indra/newview/llvocache.cpp b/indra/newview/llvocache.cpp index fe8d590810..54f68f3a29 100644 --- a/indra/newview/llvocache.cpp +++ b/indra/newview/llvocache.cpp @@ -515,7 +515,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 * draw_radius; const F32 clamped_max_radius = llclamp(max_radius, min_radius_plus_one, draw_radius); // [sNearRadius, mDrawDistance] diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 0958db1880..6dbe59033d 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -8603,7 +8603,7 @@ void LLPipeline::renderFinalize() }; // // new shader for snapshot frame helper - if (renderSnapshotFrame(auxTargetBuffer, auxActiveBuffer)) + if (renderSnapshotFrame(auxActiveBuffer, auxTargetBuffer)) { std::swap(auxActiveBuffer, auxTargetBuffer); }; 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: 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"/>