From 59aa9539c5eca206a1cb47c1bc4d888cbf6a75ca Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:07 -0400 Subject: [PATCH 01/33] LLLeap's error drain times itself, so it stops even before LLTimer starts onError pumps mainloop for up to two seconds so the error reaches the plugin. The deadline came from LLTimer::getElapsedSeconds(), which reads the global timer and returns 0 while it does not exist: before LLCommon::initClass and after cleanupClass. An error logged then made the deadline 2 and the clock 0, and the loop spun for as long as the plugin's stdin stayed full. A local LLTimer starts its own clock. Co-Authored-By: Claude Opus 5.5 --- indra/llcommon/llleap.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indra/llcommon/llleap.cpp b/indra/llcommon/llleap.cpp index 1614cc6e57f..6b3292c597e 100644 --- a/indra/llcommon/llleap.cpp +++ b/indra/llcommon/llleap.cpp @@ -442,8 +442,8 @@ class LLLeapImpl: public LLLeap LLProcess::WritePipe& childin(mChild->getWritePipe(LLProcess::STDIN)); LLEventPump& mainloop(LLEventPumps::instance().obtain("mainloop")); LLSD nop; - F64 until = (LLTimer::getElapsedSeconds() + 2).value(); - while (childin.size() && LLTimer::getElapsedSeconds() < until) + LLTimer waited; + while (childin.size() && waited.getElapsedTimeF64() < 2.0) { mainloop.post(nop); } From eb5dbf9e85e4907fc5d75c463f57434e2389179f Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:10 -0400 Subject: [PATCH 02/33] A deadman timer built during static initialisation has its horizon The horizon was converted to clock ticks in the initialiser list, using a frequency that is 0 until something first reads the clock. A static LLDeadmanTimer, such as LLMeshRepository's quiescent timer, constructed before that got a horizon of 0 and expired at once. The constructor now reads the frequency first, as LLTimer's does. Co-Authored-By: Claude Opus 5.5 --- indra/llcommon/lldeadmantimer.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/indra/llcommon/lldeadmantimer.cpp b/indra/llcommon/lldeadmantimer.cpp index 45c01eb84a7..1116c3ad28d 100644 --- a/indra/llcommon/lldeadmantimer.cpp +++ b/indra/llcommon/lldeadmantimer.cpp @@ -44,7 +44,7 @@ // true true Not allowed // LLDeadmanTimer::LLDeadmanTimer(F64 horizon, bool inc_cpu) - : mHorizon(time_type(llmax(horizon, F64(0.0)) * get_timer_info().mClockFrequency)), + : mHorizon(U64L(0)), mActive(false), // If true, a timer is running. mDone(false), // If true, timer has completed and can be read (once) mStarted(U64L(0)), @@ -56,7 +56,15 @@ LLDeadmanTimer::LLDeadmanTimer(F64 horizon, bool inc_cpu) mUEndCPU(LLProcInfo::time_type(U64L(0))), mSStartCPU(LLProcInfo::time_type(U64L(0))), mSEndCPU(LLProcInfo::time_type(U64L(0))) -{} +{ + // A timer constructed during static initialisation can come before + // anything has read the clock frequency, which would make the horizon 0. + if (!get_timer_info().mClockFrequency) + { + get_timer_info().update(); + } + mHorizon = time_type(llmax(horizon, F64(0.0)) * get_timer_info().mClockFrequency); +} // static From 5a027957d8c65feba482d8c44055786726e3c46f Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:12 -0400 Subject: [PATCH 03/33] A ping answered within one message-time sample records its real time When the reply arrived in the same message-time sample the ping was sent in, the elapsed time read 0. The code refreshed the sample to get a real time, then went on using the 0 it had already computed, so those pings were recorded as about 0 ms and pulled the averaged ping down. The elapsed time is now taken again from the refreshed sample. Co-Authored-By: Claude Opus 5.5 --- indra/llmessage/llcircuit.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/indra/llmessage/llcircuit.cpp b/indra/llmessage/llcircuit.cpp index 694896fb806..645c41371ef 100644 --- a/indra/llmessage/llcircuit.cpp +++ b/indra/llmessage/llcircuit.cpp @@ -1276,6 +1276,7 @@ void LLCircuitData::pingTimerStop(const U8 ping_id) // Ack, we got our ping response on the same frame! Sigh, let's get a real time otherwise // all of our ping calculations will be skewed. mt_secs = LLMessageSystem::getMessageTimeSeconds(true); + time = mt_secs - mPingTime; } mLastPingReceivedTime = mt_secs; From b9ae6b17ab7ad6b6f4a6f70cace3a4f0d7cbd107 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:14 -0400 Subject: [PATCH 04/33] A failed experience lookup expires after its retry delay, not around 2080 processExperience adds the current time to EXPIRES, because the server sends it as a delay. The error path built its placeholder rows with the current time already added, so the expiry came out at about twice the epoch. A failed lookup was never retried for the session, and an existing entry whose refresh failed was written to the cache file with that expiry and never refreshed in later sessions either. The error path now stores the delay, as the error_ids path does. Co-Authored-By: Claude Opus 5.5 --- indra/llmessage/llexperiencecache.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/indra/llmessage/llexperiencecache.cpp b/indra/llmessage/llexperiencecache.cpp index 07d11932bca..77eebbe930e 100644 --- a/indra/llmessage/llexperiencecache.cpp +++ b/indra/llmessage/llexperiencecache.cpp @@ -267,8 +267,6 @@ void LLExperienceCache::requestExperiencesCoro(LLCoreHttpUtil::HttpCoroutineAdap if (!status) { - F64 now = LLFrameTimer::getTotalSeconds(); - LLSD headers = httpResults[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_HEADERS]; // build dummy entries for the failed requests for (RequestQueue_t::const_iterator it = requests.begin(); it != requests.end(); ++it) @@ -279,7 +277,8 @@ void LLExperienceCache::requestExperiencesCoro(LLCoreHttpUtil::HttpCoroutineAdap { exp[PROPERTIES] = PROPERTY_INVALID; } - exp[EXPIRES] = now + LLExperienceCacheImpl::getErrorRetryDeltaTime(status, headers); + // A delta, as the server sends it: processExperience adds the time. + exp[EXPIRES] = LLExperienceCacheImpl::getErrorRetryDeltaTime(status, headers); exp[EXPERIENCE_ID] = *it; exp["key_type"] = EXPERIENCE_ID; exp["uuid"] = *it; From ebc96ec5b8b9aca03705d746f82e509943ee566d Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:17 -0400 Subject: [PATCH 05/33] Triple-click takes the line however long the text view has been open LLFrameTimer::setTimerExpirySec counts from the timer's last reset, and mTripleClick was never reset, so arming it after a double click set an expiry 0.3 s after the view was constructed. Once a view was older than that, every third press read as expired and triple-click line selection never worked. Arming now resets the timer with its expiry. The test lets the triple-click window pass after the view is made, then double-clicks and presses again; it fails with the old arming. Co-Authored-By: Claude Opus 5.5 --- indra/llui/altextview.cpp | 2 +- indra/llui/tests/altextview_test.cpp | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/indra/llui/altextview.cpp b/indra/llui/altextview.cpp index 1e03b6daf2c..c3384d6b538 100644 --- a/indra/llui/altextview.cpp +++ b/indra/llui/altextview.cpp @@ -5858,7 +5858,7 @@ bool ALTextView::sameClickSpot(S32 x, S32 y) const void ALTextView::armTripleClick() { - mTripleClick.setTimerExpirySec(TRIPLE_CLICK_INTERVAL); + mTripleClick.resetWithExpiry(TRIPLE_CLICK_INTERVAL); } bool ALTextView::handleScrollWheel(S32 x, S32 y, LLScrollDelta delta) diff --git a/indra/llui/tests/altextview_test.cpp b/indra/llui/tests/altextview_test.cpp index 39a26934d7c..1301c8073f1 100644 --- a/indra/llui/tests/altextview_test.cpp +++ b/indra/llui/tests/altextview_test.cpp @@ -2420,4 +2420,25 @@ namespace tut ensure("the other caret let go going back", !v.hasOtherSelections()); ensure("at the match before", v.selection().normalised() == spanRange(0, 0, 3)); } + + template<> template<> + void altextview_object::test<78>() + { + set_test_name("a third press where a double click took a word takes the line, however long the view has been open"); + ALTextView& v = make("hello world\nsecond"); + // Longer than the triple-click window passes between the view being + // made and the first click. + ms_sleep(400); + LLFrameTimer::updateFrameTime(); + S32 x, y; + pointOf(0, 8, x, y); + v.handleMouseDown(x, y, MASK_NONE); + v.handleMouseUp(x, y, MASK_NONE); + ensure("double", v.handleDoubleClick(x, y, MASK_NONE)); + ensure_equals("the word", v.selectedText(), std::string("world")); + v.handleMouseUp(x, y, MASK_NONE); + ensure("third", v.handleMouseDown(x, y, MASK_NONE)); + ensure("the line", v.selection().normalised() == ALTextRange(ALTextPos(0, 0), ALTextPos(1, 0))); + v.handleMouseUp(x, y, MASK_NONE); + } } From a94d5f5043a22d94279e2b8949d3f541de88133c Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:19 -0400 Subject: [PATCH 06/33] XUI Studio rereads once the typing stops, not on every edit documentRead set mRereadAt's expiry without resetting it, and an LLFrameTimer counts its expiry from its last reset, which for this timer was the floater's construction. Once the floater was 0.75 s old every edit was already past the expiry, so the lint, the tree suffixes and the findings ran again on the frame after each keystroke instead of once the typing paused. It now resets first, as the source-edit timer beside it does. Co-Authored-By: Claude Opus 5.5 --- indra/newview/alfloaterxuistudio.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/indra/newview/alfloaterxuistudio.cpp b/indra/newview/alfloaterxuistudio.cpp index 08b5fe0b0e5..5311359a970 100644 --- a/indra/newview/alfloaterxuistudio.cpp +++ b/indra/newview/alfloaterxuistudio.cpp @@ -4090,6 +4090,7 @@ void ALFloaterXUIStudio::documentRead(const std::string& status) fillHistory(); setStatus(status); mRereadPending = true; + mRereadAt.reset(); mRereadAt.setTimerExpirySec(REREAD_SECONDS); } From bbc434c604925399a59d7e948620ef4ee4e2e61a Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:21 -0400 Subject: [PATCH 07/33] The event poll's early-reply guard measures the request, not the uptime message_time.getElapsedSeconds() is LLTimer's static uptime, called through an instance, so the "response arrived too early" check compared the viewer's uptime against 10 seconds and was dead after the first ten seconds of a session. A 499 or 5xx that came straight back was treated as an ordinary empty poll: reposted at once, with no backoff, and the error count reset. It now measures the request, so a fast failure takes the error path and its backoff, as the code intends. Co-Authored-By: Claude Opus 5.5 --- indra/newview/lleventpoll.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indra/newview/lleventpoll.cpp b/indra/newview/lleventpoll.cpp index 836ff634297..09c99b319bb 100644 --- a/indra/newview/lleventpoll.cpp +++ b/indra/newview/lleventpoll.cpp @@ -208,12 +208,12 @@ namespace Details || status == LLCore::HttpStatus(HTTP_SERVICE_UNAVAILABLE) || status == LLCore::HttpStatus(HTTP_GATEWAY_TIME_OUT)) { - if (message_time.getElapsedSeconds() < MIN_SECONDS_PASSED) + if (message_time.getElapsedTimeF64() < MIN_SECONDS_PASSED) { // Server is supposed to hold request for 20 to 30 seconds. // If it didn't hold the request at least for 10s, treat as an error. LL_WARNS("LLEventPollImpl") << "Response arrived too early, status: " << status.toTerseString() - << ", time passed: " << message_time.getElapsedSeconds() << LL_ENDL; + << ", time passed: " << message_time.getElapsedTimeF64() << LL_ENDL; } else { From 46e64e78acf899b56a26021bed527a1e3713c695 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:23 -0400 Subject: [PATCH 08/33] AIS update timers start before their expiry is set LLTimer::start() resets the timer, and reset() clears the expiry, so setting the expiry and then starting left both the task timer and the batch timer already expired. The first checkTimeout() of every non-priority update suspended until the next frame. Starting first keeps the expiry. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llaisapi.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indra/newview/llaisapi.cpp b/indra/newview/llaisapi.cpp index 25b0dbc5505..d56698a836d 100644 --- a/indra/newview/llaisapi.cpp +++ b/indra/newview/llaisapi.cpp @@ -1074,14 +1074,14 @@ AISUpdate::AISUpdate(const LLSD& update, AISAPI::COMMAND_TYPE type, const LLSD& && type != AISAPI::UPDATEITEM && type != AISAPI::FETCHCOF && type != AISAPI::FETCHCATEGORYLINKS; - mTaskTimer.setTimerExpirySec(AIS_TASK_EXPIRY_SECONDS); mTaskTimer.start(); + mTaskTimer.setTimerExpirySec(AIS_TASK_EXPIRY_SECONDS); U32 current_frame = LLFrameTimer::getFrameCount(); if (sBatchFrameCount != current_frame) { - sBatchTimer.setTimerExpirySec(AIS_BATCH_EXPIRY_SECONDS); sBatchTimer.start(); + sBatchTimer.setTimerExpirySec(AIS_BATCH_EXPIRY_SECONDS); sBatchFrameCount = current_frame; } parseUpdate(update); From b0b39417f6fc355e5a3a534badee34a66bf084de Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:25 -0400 Subject: [PATCH 09/33] The avatar picker revalidates three times a second, not every draw draw() set the throttle's expiry and then called start(), and LLFrameTimer::start() resets the expiry to now, so the timer had expired again by the next frame and onList() ran on every draw. resetWithExpiry sets both at once. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llfloateravatarpicker.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/indra/newview/llfloateravatarpicker.cpp b/indra/newview/llfloateravatarpicker.cpp index f994d0e0f2d..2493c216646 100644 --- a/indra/newview/llfloateravatarpicker.cpp +++ b/indra/newview/llfloateravatarpicker.cpp @@ -396,11 +396,10 @@ void LLFloaterAvatarPicker::draw() static LLFrameTimer timer; if (timer.hasExpired()) { - timer.setTimerExpirySec(0.33f); // three times per second should be enough. + timer.resetWithExpiry(0.33f); // three times per second should be enough. // simulate list changes. onList(); - timer.start(); } LLFloater::draw(); From 2185e7eaad6875590acf47b8779460a9ce629062 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:27 -0400 Subject: [PATCH 10/33] A failed folder fetch backs off for 60 seconds from the failure setFetching(FETCH_FAILED) set the expiry without a reset, and an LLFrameTimer counts its expiry from its last reset, which was the start of the fetch. A fetch that failed after more than 60 seconds, and AIS times out at 180, had its back-off expire before it began, so the folder was fetched again at once. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llviewerinventory.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/llviewerinventory.cpp b/indra/newview/llviewerinventory.cpp index db046004290..3fffbd9c66e 100644 --- a/indra/newview/llviewerinventory.cpp +++ b/indra/newview/llviewerinventory.cpp @@ -768,7 +768,7 @@ void LLViewerInventoryCategory::setFetching(LLViewerInventoryCategory::EFetchTyp if (fetching == FETCH_FAILED) { const F32 FETCH_FAILURE_EXPIRY = 60.0f; - mDescendentsRequested.setTimerExpirySec(FETCH_FAILURE_EXPIRY); + mDescendentsRequested.resetWithExpiry(FETCH_FAILURE_EXPIRY); mFetching = fetching; } else if (fetching > mFetching) // allow a switch from normal to recursive From 95945cf4eda8926b749ac4facfde146f40e8fa40 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:29 -0400 Subject: [PATCH 11/33] The group cache keeps access times as F64 Access times were epoch seconds cast to F32, which at the current epoch resolves 128 seconds. The LRU could not order groups touched within the same 128 s, and because it only evicts a group strictly older than now, none touched in the current bucket could go: the eviction loop gave up and the cache grew past MAX_CACHED_GROUPS. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llgroupmgr.cpp | 6 +++--- indra/newview/llgroupmgr.h | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/indra/newview/llgroupmgr.cpp b/indra/newview/llgroupmgr.cpp index e363c9118e7..becf7b56bd8 100644 --- a/indra/newview/llgroupmgr.cpp +++ b/indra/newview/llgroupmgr.cpp @@ -229,7 +229,7 @@ LLGroupMgrGroupData::LLGroupMgrGroupData(const LLUUID& id) : mRoleMemberDataComplete(false), mGroupPropertiesDataComplete(false), mPendingRoleMemberRequest(false), - mAccessTime(0.0f), + mAccessTime(0.0), mPendingBanRequest(false) { mMemberVersion.generate(); @@ -237,7 +237,7 @@ LLGroupMgrGroupData::LLGroupMgrGroupData(const LLUUID& id) : void LLGroupMgrGroupData::setAccessed() { - mAccessTime = (F32)LLFrameTimer::getTotalSeconds(); + mAccessTime = LLFrameTimer::getTotalSeconds(); } bool LLGroupMgrGroupData::getRoleData(const LLUUID& role_id, LLRoleData& role_data) @@ -1555,7 +1555,7 @@ void LLGroupMgr::addGroup(LLGroupMgrGroupData* group_datap) { // LRU: Remove the oldest un-observed group from cache until group size is small enough - F32 oldest_access = (F32)LLFrameTimer::getTotalSeconds(); + F64 oldest_access = LLFrameTimer::getTotalSeconds(); group_map_t::iterator oldest_gi = mGroups.end(); for (group_map_t::iterator gi = mGroups.begin(); gi != mGroups.end(); ++gi ) diff --git a/indra/newview/llgroupmgr.h b/indra/newview/llgroupmgr.h index 21cf4a62b61..b34edda5139 100644 --- a/indra/newview/llgroupmgr.h +++ b/indra/newview/llgroupmgr.h @@ -265,7 +265,7 @@ friend class LLGroupMgr; bool isSingleMemberNotOwner(); - F32 getAccessTime() const { return mAccessTime; } + F64 getAccessTime() const { return mAccessTime; } void setAccessed(); const LLUUID& getMemberVersion() const { return mMemberVersion; } @@ -329,7 +329,7 @@ friend class LLGroupMgr; bool mGroupPropertiesDataComplete; bool mPendingRoleMemberRequest; - F32 mAccessTime; + F64 mAccessTime; // Generate a new ID every time mMembers LLUUID mMemberVersion; From 6e1f61c05a9b397a5c087c102a38218be11cdbab Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:31 -0400 Subject: [PATCH 12/33] Nearby's recent-arrival order keeps arrival times as F64 getArrivalTimeByID returned epoch seconds as F32, which resolves 128 seconds at the current epoch, so everyone who arrived within the same two minutes compared equal and the list fell back to sorting them by name. The map already held F64; the getter and the comparator now do too. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llpanelpeople.cpp | 4 ++-- indra/newview/llrecentpeople.cpp | 6 +++--- indra/newview/llrecentpeople.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/indra/newview/llpanelpeople.cpp b/indra/newview/llpanelpeople.cpp index 6800fe462ee..012703d9b3e 100644 --- a/indra/newview/llpanelpeople.cpp +++ b/indra/newview/llpanelpeople.cpp @@ -222,8 +222,8 @@ class LLAvatarItemRecentArrivalComparator : public LLAvatarItemNameComparator virtual bool doCompare(const LLAvatarListItem* item1, const LLAvatarListItem* item2) const { - F32 arr_time1 = LLRecentPeople::instance().getArrivalTimeByID(item1->getAvatarId()); - F32 arr_time2 = LLRecentPeople::instance().getArrivalTimeByID(item2->getAvatarId()); + F64 arr_time1 = LLRecentPeople::instance().getArrivalTimeByID(item1->getAvatarId()); + F64 arr_time2 = LLRecentPeople::instance().getArrivalTimeByID(item2->getAvatarId()); if (arr_time1 == arr_time2) { diff --git a/indra/newview/llrecentpeople.cpp b/indra/newview/llrecentpeople.cpp index d3599e00a81..f8e73367c28 100644 --- a/indra/newview/llrecentpeople.cpp +++ b/indra/newview/llrecentpeople.cpp @@ -126,15 +126,15 @@ void LLRecentPeople::updateAvatarsArrivalTime(uuid_vec_t& uuids) } } -F32 LLRecentPeople::getArrivalTimeByID(const LLUUID& id) +F64 LLRecentPeople::getArrivalTimeByID(const LLUUID& id) { id_to_time_map_t::const_iterator it = mAvatarsArrivalTime.find(id); if (it != mAvatarsArrivalTime.end()) { - return (F32)(it->second); + return it->second; } - return (F32)LLDate::now().secondsSinceEpoch(); + return LLDate::now().secondsSinceEpoch(); } bool LLRecentPeople::save() const diff --git a/indra/newview/llrecentpeople.h b/indra/newview/llrecentpeople.h index 2a41a563064..3db7c208ec6 100644 --- a/indra/newview/llrecentpeople.h +++ b/indra/newview/llrecentpeople.h @@ -125,7 +125,7 @@ class LLRecentPeople final : public LLSingleton, public LLOldEve void clearHistory(); void updateAvatarsArrivalTime(uuid_vec_t& uuids); - F32 getArrivalTimeByID(const LLUUID& id); + F64 getArrivalTimeByID(const LLUUID& id); private: From 7bca3b9ac139beb517fd495cc5788bbba3c23030 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:33 -0400 Subject: [PATCH 13/33] Scene Load Statistics reads the avatar time in milliseconds LLVOAvatar::getTotalGPURenderTime() is in milliseconds, but the floater converted it with us_to_raw, so the avatar share came out a thousand times too small and the scenery share, which subtracts it, too large. The auto-tuner reads the same value with ms_to_raw. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llviewerstats.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/llviewerstats.cpp b/indra/newview/llviewerstats.cpp index 733b41f8842..9f9c530df14 100644 --- a/indra/newview/llviewerstats.cpp +++ b/indra/newview/llviewerstats.cpp @@ -551,7 +551,7 @@ void update_statistics() auto tot_frame_time_raw = LLPerfStats::StatsRecorder::getSceneStat(LLPerfStats::StatType_t::RENDER_FRAME); // cumulative avatar time (includes idle processing, attachments and base av) - auto tot_avatar_time_raw = LLPerfStats::us_to_raw(LLVOAvatar::getTotalGPURenderTime()); + auto tot_avatar_time_raw = LLPerfStats::ms_to_raw(LLVOAvatar::getTotalGPURenderTime()); // cumulative avatar render specific time (a bit arbitrary as the processing is too.) // auto tot_av_idle_time_raw = LLPerfStats::StatsRecorder::getSum(AvType, LLPerfStats::StatType_t::RENDER_IDLE); // auto tot_avatar_render_time_raw = tot_avatar_time_raw - tot_av_idle_time_raw; From c6df099afae0537844a3b7340c8fedab33fb5b84 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:35 -0400 Subject: [PATCH 14/33] A saved raw image is kept for its keep time after its last use destroySavedRawImage compared the time of the last reference, an absolute time since startup, against the keep time, a duration. A keep time of 30 seconds, as the bump maps ask for, protected the image only during the first 30 seconds of the session. It now compares the time since the last reference. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llviewertexture.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 172aa650844..69ccfe0883f 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -2944,7 +2944,7 @@ void LLViewerFetchedTexture::readbackRawImage() void LLViewerFetchedTexture::destroySavedRawImage() { - if(mLastReferencedSavedRawImageTime < mKeptSavedRawImageTime) + if(getElapsedLastReferencedSavedRawImageTime() < mKeptSavedRawImageTime) { return; //keep the saved raw image. } From 9513d1565c267703ee7bce7361fa377f1054a5b2 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:37 -0400 Subject: [PATCH 15/33] Frame stats skip the frame a pause stalled, not every frame after it send_agent_pause() sets mWasPaused so the stalled frame is left out of the frame statistics, but nothing ever cleared it. After the first modal file or directory picker, frame time, jitter, the percentiles and the normalised variance stopped updating for the rest of the session. The flag is now cleared once the skipped frame has been passed over. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llviewerobjectlist.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/indra/newview/llviewerobjectlist.cpp b/indra/newview/llviewerobjectlist.cpp index ca1a6c852ff..cae961824e0 100644 --- a/indra/newview/llviewerobjectlist.cpp +++ b/indra/newview/llviewerobjectlist.cpp @@ -1073,6 +1073,7 @@ void LLViewerObjectList::update(LLAgent &agent) { LLViewerStats::getInstance()->updateFrameStats(time_diff); } + mWasPaused = false; /* // Debugging code for viewing orphans, and orphaned parents From 6178ee2dc0bdec9a268800d8107dadb2278906b3 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:39 -0400 Subject: [PATCH 16/33] The geometry budget is capped at 5 ms a frame createObjects got 5% of the last frame interval with no upper bound, unlike the decode budgets beside it. The interval is measured between object-list updates, which stop during a teleport, so the first frame after a 20 second teleport could spend a second creating objects. It now stops at 5 ms, the decode budget's ceiling. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llviewerdisplay.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 8db9092bfa4..66e09b299a7 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -762,7 +762,7 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) { LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("Update Geom"); - const F32 max_geom_update_time = 0.005f*10.f*gFrameIntervalSeconds.value(); // 50 ms/second update time + const F32 max_geom_update_time = llmin(0.005f*10.f*gFrameIntervalSeconds.value(), 0.005f); // 50 ms/second update time, max 5ms/frame gPipeline.createObjects(max_geom_update_time); gPipeline.processPartitionQ(); gPipeline.updateGeom(max_geom_update_time); From 8db48a1c8662fe371f939f9725571b47d9ef7f65 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:41 -0400 Subject: [PATCH 17/33] Threaded file pickers leave the frame clock to the main thread getMultipleOpenFiles, getSaveFile and getDir called LLFrameTimer::updateFrameTime() whatever the mode. On Windows they run on LLFilePickerThread and LLDirPickerThread with blocking off, so a worker thread rewrote the frame clock while the main thread was in the middle of a frame. Only the modal case stalls the app, and only it now updates the clock, as getOpenFile already did. Co-Authored-By: Claude Opus 5.5 --- indra/newview/lldirpicker.cpp | 4 ++-- indra/newview/llfilepicker.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/indra/newview/lldirpicker.cpp b/indra/newview/lldirpicker.cpp index fbd04b5f818..006f27c4a20 100644 --- a/indra/newview/lldirpicker.cpp +++ b/indra/newview/lldirpicker.cpp @@ -274,10 +274,10 @@ bool LLDirPicker::getDir(std::string* filename, bool blocking) if (blocking) { send_agent_resume(); + // Account for the fact that the app has been stalled. + LLFrameTimer::updateFrameTime(); } - // Account for the fact that the app has been stalled. - LLFrameTimer::updateFrameTime(); return success; } diff --git a/indra/newview/llfilepicker.cpp b/indra/newview/llfilepicker.cpp index d270364a451..4e5d46f6db0 100644 --- a/indra/newview/llfilepicker.cpp +++ b/indra/newview/llfilepicker.cpp @@ -784,10 +784,10 @@ bool LLFilePicker::getMultipleOpenFiles(ELoadFilter filter, bool blocking) if (blocking) { send_agent_resume(); + // Account for the fact that the app has been stalled. + LLFrameTimer::updateFrameTime(); } - // Account for the fact that the app has been stalled. - LLFrameTimer::updateFrameTime(); return success; } @@ -1046,10 +1046,10 @@ bool LLFilePicker::getSaveFile(ESaveFilter filter, const std::string& filename, if (blocking) { send_agent_resume(); + // Account for the fact that the app has been stalled. + LLFrameTimer::updateFrameTime(); } - // Account for the fact that the app has been stalled. - LLFrameTimer::updateFrameTime(); return success; } From aea08c6a4be4f871f982d14215bbc543b051b48e Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:44 -0400 Subject: [PATCH 18/33] Session telemetry counts from the scene-load start gRenderStartTime and gForegroundTime are reset when the scene starts loading, but what is divided by them was not: - sim_fps used a last time taken before the reset, so its first denominator was the time since the reset minus the time to the end of init, which can be zero or negative. - fps divided frames counted since the app started by foreground time since the reset, so it included every login-screen frame. - Resetting gForegroundTime while it was paused, which it is when the window is unfocused at that moment, stored an absolute time where the paused elapsed time belongs; the next unpause turned it into seconds since startup. The counters now reset with their timers, and the foreground timer is reset running and paused again if it was paused. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llstartup.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index c0c17db0630..f6833e0ddf5 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -2490,7 +2490,19 @@ bool idle_startup() LL_DEBUGS("SceneLoadTiming", "Start") << "Scene Load Started " << LL_ENDL; gRenderStartTime.reset(); + gSimLastTime = 0.f; + gSimFrames = (F32)gFrameCount; + // Resetting a paused timer stores an absolute time where its paused + // elapsed time belongs, so the reset happens running and the pause is + // put back after. + const bool foreground = gForegroundTime.getStarted(); + gForegroundTime.unpause(); gForegroundTime.reset(); + if (!foreground) + { + gForegroundTime.pause(); + } + gForegroundFrameCount = 0; // HACK: Inform simulator of window size. // Do this here so it's less likely to race with RegisterNewAgent. From 419218ee74fe54f933cead20afa0ccb40fba5bfd Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:46 -0400 Subject: [PATCH 19/33] Pose undo coalescing measures on the steady clock The 0.8 second window that merges rapid changes into one undo step was measured on system_clock, so a backward step of the wall clock put every change inside the window and no undo snapshots were taken until the clock caught up. Co-Authored-By: Claude Opus 5.5 --- indra/newview/fsjointpose.cpp | 2 +- indra/newview/fsjointpose.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/indra/newview/fsjointpose.cpp b/indra/newview/fsjointpose.cpp index 449317b3b94..70a6c39c480 100644 --- a/indra/newview/fsjointpose.cpp +++ b/indra/newview/fsjointpose.cpp @@ -107,7 +107,7 @@ void FSJointPose::addStateToUndo(const FSJointState& stateToAddToUndo) { mModifiedThisSession = true; - auto now = std::chrono::system_clock::now(); + auto now = std::chrono::steady_clock::now(); auto timeIntervalSinceLastChange = now - mTimeLastUpdatedCurrentState; mTimeLastUpdatedCurrentState = now; diff --git a/indra/newview/fsjointpose.h b/indra/newview/fsjointpose.h index 42cec2abedb..d54b78cbd1a 100644 --- a/indra/newview/fsjointpose.h +++ b/indra/newview/fsjointpose.h @@ -487,7 +487,7 @@ class FSJointPose std::deque mLastSetJointStates; size_t mUndoneJointStatesIndex = 0; - std::chrono::system_clock::time_point mTimeLastUpdatedCurrentState = std::chrono::system_clock::now(); + std::chrono::steady_clock::time_point mTimeLastUpdatedCurrentState = std::chrono::steady_clock::now(); FSJointState mCurrentState; From e080b75c3c21b09efa9aba08586bf1af5e9053f3 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:48 -0400 Subject: [PATCH 20/33] A pending profile request expires on the frame clock The five second pending window was kept in whole seconds of time(), the wall clock, so a request could expire up to a second early, and a backward clock step held every request for that profile as pending for the length of the step. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llavatarpropertiesprocessor.cpp | 9 ++++----- indra/newview/llavatarpropertiesprocessor.h | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/indra/newview/llavatarpropertiesprocessor.cpp b/indra/newview/llavatarpropertiesprocessor.cpp index dfa5979e032..df9ad13937d 100644 --- a/indra/newview/llavatarpropertiesprocessor.cpp +++ b/indra/newview/llavatarpropertiesprocessor.cpp @@ -751,9 +751,9 @@ bool LLAvatarPropertiesProcessor::isPendingRequest(const LLUUID& avatar_id, EAva if (it == mRequestTimestamps.end()) return false; // We found a request, check if it has timed out - U32 now = (U32)time(nullptr); - const U32 REQUEST_EXPIRE_SECS = 5; - U32 expires = it->second + REQUEST_EXPIRE_SECS; + F64 now = LLFrameTimer::getElapsedSeconds(); + const F64 REQUEST_EXPIRE_SECS = 5.0; + F64 expires = it->second + REQUEST_EXPIRE_SECS; // Request is still pending if it hasn't expired yet // *NOTE: Expired requests will accumulate in this map, but they are rare, @@ -765,9 +765,8 @@ bool LLAvatarPropertiesProcessor::isPendingRequest(const LLUUID& avatar_id, EAva void LLAvatarPropertiesProcessor::addPendingRequest(const LLUUID& avatar_id, EAvatarProcessorType type) { timestamp_map_t::key_type key = std::make_pair(avatar_id, type); - U32 now = (U32)time(nullptr); // Add or update existing (expired) request - mRequestTimestamps[ key ] = now; + mRequestTimestamps[ key ] = LLFrameTimer::getElapsedSeconds(); } void LLAvatarPropertiesProcessor::removePendingRequest(const LLUUID& avatar_id, EAvatarProcessorType type) diff --git a/indra/newview/llavatarpropertiesprocessor.h b/indra/newview/llavatarpropertiesprocessor.h index fb9ae0df62c..d877b1663df 100644 --- a/indra/newview/llavatarpropertiesprocessor.h +++ b/indra/newview/llavatarpropertiesprocessor.h @@ -357,7 +357,7 @@ class LLAvatarPropertiesProcessor // Maintain a timestamp for each request so a request that receives no reply // does not block future requests forever. // Map avatar_id+request_type -> U32 timestamp in seconds - typedef std::map< std::pair, U32> timestamp_map_t; + typedef std::map< std::pair, F64> timestamp_map_t; timestamp_map_t mRequestTimestamps; // Is returned by isHideAgeSupportedByServer() From 4120638829c42977d42f802ee479cbd527cfe847 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:50 -0400 Subject: [PATCH 21/33] A conversation stamped in the future is not purged as the oldest isOlderThan subtracted the stored time from now in unsigned seconds, so a conversation whose time was ahead of the corrected clock wrapped to a huge age and was purged from the log at login. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llconversationlog.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/indra/newview/llconversationlog.cpp b/indra/newview/llconversationlog.cpp index bfcc2e41825..210f925566c 100644 --- a/indra/newview/llconversationlog.cpp +++ b/indra/newview/llconversationlog.cpp @@ -142,6 +142,12 @@ const std::string LLConversation::createTimestamp(const U64Seconds& utc_time) bool LLConversation::isOlderThan(U32Days days) const { U64Seconds now(time_corrected()); + if (mTime >= now) + { + // Stamped ahead of this clock, so no age at all; the unsigned + // difference would make it the oldest there is. + return false; + } U32Days age = now - mTime; return age > days; From 9c10a3d74302c2bdee68493394a5c4c7a94c4965 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:52 -0400 Subject: [PATCH 22/33] Leaving an inspector after it lost focus does not restart its fade Losing focus stops the open timer and starts the fade; moving the mouse out then unpaused the stopped timer. unpause() turns a paused timer's stored elapsed time back into a start time, but a stopped one holds its start time instead, so the inspector read as long past its stay time and started the fade again. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llinspect.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/indra/newview/llinspect.cpp b/indra/newview/llinspect.cpp index bfef4860c40..7feec774eb4 100644 --- a/indra/newview/llinspect.cpp +++ b/indra/newview/llinspect.cpp @@ -122,7 +122,12 @@ bool LLInspect::handleToolTip(S32 x, S32 y, MASK mask) // virtual void LLInspect::onMouseLeave(S32 x, S32 y, MASK mask) { - mOpenTimer.unpause(); + // Once focus is lost the open timer is stopped, not paused, and the + // fade has begun; unpausing it would restart the fade. + if (!mCloseTimer.getStarted()) + { + mOpenTimer.unpause(); + } } bool LLInspect::childHasVisiblePopupMenu() From 6a8109751e1816e73d58e549ad6ad3c7b7717004 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:54 -0400 Subject: [PATCH 23/33] Server dates are compared against server-corrected time These compared a time the server stamped against this machine's clock, which is off by however far the local clock is wrong: - event reminders (lleventnotifier); - the remaining time of parcel access and ban entries (llfloaterland); - the expiry sent with a timed parcel ban (llfloaterbanduration); - the display-name change lockout (llfloaterdisplayname, llpanelprofile); - which chat-log date is today (lllogchat), which has to agree with the time_corrected() stamps it is matched against. All now use time_corrected(), the clock corrected to the server's at login. Co-Authored-By: Claude Opus 5.5 --- indra/newview/lleventnotifier.cpp | 2 +- indra/newview/llfloaterbanduration.cpp | 2 +- indra/newview/llfloaterdisplayname.cpp | 2 +- indra/newview/llfloaterland.cpp | 4 ++-- indra/newview/lllogchat.cpp | 2 +- indra/newview/llpanelprofile.cpp | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/indra/newview/lleventnotifier.cpp b/indra/newview/lleventnotifier.cpp index 02bfd0045aa..fdd1b5a37b5 100644 --- a/indra/newview/lleventnotifier.cpp +++ b/indra/newview/lleventnotifier.cpp @@ -134,7 +134,7 @@ void LLEventNotifier::update() // Check our notifications again and send out updates // if they happen. - F64 alert_time = LLDate::now().secondsSinceEpoch() + 5 * 60; + F64 alert_time = (F64)time_corrected() + 5 * 60; en_map::iterator iter; for (iter = mEventNotifications.begin(); iter != mEventNotifications.end();) diff --git a/indra/newview/llfloaterbanduration.cpp b/indra/newview/llfloaterbanduration.cpp index eb32e509011..fb944195454 100644 --- a/indra/newview/llfloaterbanduration.cpp +++ b/indra/newview/llfloaterbanduration.cpp @@ -82,7 +82,7 @@ void LLFloaterBanDuration::onClickBan() LLSpinCtrl* hours_spin = getChild("ban_hours"); if (hours_spin) { - time = (S32)(LLDate::now().secondsSinceEpoch() + (hours_spin->getValue().asInteger() * 3600)); + time = (S32)(time_corrected() + (hours_spin->getValue().asInteger() * 3600)); } } mSelectionCallback(mAvatar_ids, time); diff --git a/indra/newview/llfloaterdisplayname.cpp b/indra/newview/llfloaterdisplayname.cpp index a07b50323bd..0e5274768bb 100644 --- a/indra/newview/llfloaterdisplayname.cpp +++ b/indra/newview/llfloaterdisplayname.cpp @@ -73,7 +73,7 @@ void LLFloaterDisplayName::onOpen(const LLSD& key) LLAvatarName av_name; LLAvatarNameCache::get(gAgent.getID(), &av_name); - F64 now_secs = LLDate::now().secondsSinceEpoch(); + F64 now_secs = (F64)time_corrected(); mIsLockedOut = now_secs < av_name.mNextUpdate; if (mIsLockedOut) { diff --git a/indra/newview/llfloaterland.cpp b/indra/newview/llfloaterland.cpp index 3d1c36a500e..ed582667724 100644 --- a/indra/newview/llfloaterland.cpp +++ b/indra/newview/llfloaterland.cpp @@ -2500,7 +2500,7 @@ void LLPanelLandAccess::refresh() if (entry.mTime != 0) { LLStringUtil::format_map_t args; - S32 now = (S32)time(NULL); + S32 now = (S32)time_corrected(); S32 seconds = entry.mTime - now; if (seconds < 0) seconds = 0; prefix.assign(" ("); @@ -2549,7 +2549,7 @@ void LLPanelLandAccess::refresh() if (entry.mTime != 0) { LLStringUtil::format_map_t args; - S32 now = (S32)time(NULL); + S32 now = (S32)time_corrected(); seconds = entry.mTime - now; if (seconds < 0) seconds = 0; diff --git a/indra/newview/lllogchat.cpp b/indra/newview/lllogchat.cpp index 4bcf754c9ce..e2ca41010ef 100644 --- a/indra/newview/lllogchat.cpp +++ b/indra/newview/lllogchat.cpp @@ -140,7 +140,7 @@ class LLLogChatTimeScanner: public LLSingleton { typedef boost::date_time::local_adjustor pst; typedef boost::date_time::local_adjustor pdt; - time_t t_time = time(NULL); + time_t t_time = time_corrected(); ptime p_time = LLStringOps::getPacificDaylightTime() ? pdt::utc_to_local(from_time_t(t_time)) : pst::utc_to_local(from_time_t(t_time)); diff --git a/indra/newview/llpanelprofile.cpp b/indra/newview/llpanelprofile.cpp index d0ea3af03b7..1d4f1ae1b0c 100644 --- a/indra/newview/llpanelprofile.cpp +++ b/indra/newview/llpanelprofile.cpp @@ -1676,7 +1676,7 @@ void LLPanelProfileSecondLife::onAvatarNameCacheSetName(const LLUUID& agent_id, LL_INFOS("LegacyProfile") << "name-change now " << LLDate::now() << " next_update " << LLDate(av_name.mNextUpdate) << LL_ENDL; - F64 now_secs = LLDate::now().secondsSinceEpoch(); + F64 now_secs = (F64)time_corrected(); if (now_secs < av_name.mNextUpdate) { From c21a0504d6f38623b24808b55cadd60b3e4c0676 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 00:58:56 -0400 Subject: [PATCH 24/33] The region's day cycle runs on the server's clock The day cycle's position, its blend timing and the environment panel's apparent time were taken from this machine's clock, so a viewer whose clock was wrong showed a different time of day from the region and from everyone else. They now add gUTCOffset, the offset to the server's clock measured at login. The per-frame sites keep LLDate::now()'s sub-second precision; time_corrected() is whole seconds and would make the sky step once a second. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llenvironment.cpp | 7 ++++--- indra/newview/llpanelenvironment.cpp | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/indra/newview/llenvironment.cpp b/indra/newview/llenvironment.cpp index c72e6d09c74..14e4472262e 100644 --- a/indra/newview/llenvironment.cpp +++ b/indra/newview/llenvironment.cpp @@ -278,7 +278,7 @@ namespace LLSettingsBase::Seconds getAdjustedNow() const { - LLSettingsBase::Seconds now(LLDate::now().secondsSinceEpoch()); + LLSettingsBase::Seconds now(LLDate::now().secondsSinceEpoch() + gUTCOffset); return (now + mCycleOffset); } @@ -2924,7 +2924,8 @@ void LLEnvironment::DayInstance::setBlenders(const LLSettingsBlender::ptr_t &sky LLSettingsBase::TrackPosition LLEnvironment::DayInstance::getProgress() const { - LLSettingsBase::Seconds now(LLDate::now().secondsSinceEpoch()); + // The region's day runs on the server's clock, not this machine's. + LLSettingsBase::Seconds now(LLDate::now().secondsSinceEpoch() + gUTCOffset); now += mDayOffset; if ((mDayLength <= 0) || !mDayCycle) @@ -2940,7 +2941,7 @@ LLSettingsBase::TrackPosition LLEnvironment::DayInstance::secondsToKeyframe(LLSe void LLEnvironment::DayInstance::animate() { - LLSettingsBase::Seconds now(LLDate::now().secondsSinceEpoch()); + LLSettingsBase::Seconds now(LLDate::now().secondsSinceEpoch() + gUTCOffset); now += mDayOffset; diff --git a/indra/newview/llpanelenvironment.cpp b/indra/newview/llpanelenvironment.cpp index 831ad7827ac..74dab9dca70 100644 --- a/indra/newview/llpanelenvironment.cpp +++ b/indra/newview/llpanelenvironment.cpp @@ -932,7 +932,7 @@ void LLPanelEnvironmentInfo::udpateApparentTimeOfDay() } mLabelApparentTime->setVisible(true); - S32Seconds now((S32)LLDate::now().secondsSinceEpoch()); + S32Seconds now((S32)time_corrected()); now += mCurrentEnvironment->mDayOffset; From a132067a642d8e72516c999384bb77f2feaef774 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 01:02:46 -0400 Subject: [PATCH 25/33] The threaded save picker leaves the keyboard to the main thread getSaveFile reset gKeyboard after the dialog closed whatever the mode, and on Windows the non-modal case runs on LLFilePickerThread, so a worker thread rewrote the key state while the main thread was reading it. That reset is only needed when the dialog is modal and the main thread was blocked in it; threaded, the dialog taking focus already resets the keyboard on the main thread through handleFocusLost. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llfilepicker.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/indra/newview/llfilepicker.cpp b/indra/newview/llfilepicker.cpp index 4e5d46f6db0..f304c8c8d23 100644 --- a/indra/newview/llfilepicker.cpp +++ b/indra/newview/llfilepicker.cpp @@ -1040,11 +1040,13 @@ bool LLFilePicker::getSaveFile(ESaveFilter filter, const std::string& filename, { LOG_UNHANDLED_EXCEPTION(""); } - gKeyboard->resetKeys(); } if (blocking) { + // Only here on the main thread: threaded, the keyboard belongs to the + // main thread, and the dialog taking focus has already reset it there. + gKeyboard->resetKeys(); send_agent_resume(); // Account for the fact that the app has been stalled. LLFrameTimer::updateFrameTime(); From 248c10350671d70b455caa70c2e218561675f9dc Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 01:56:11 -0400 Subject: [PATCH 26/33] The static uptime getters are named for what they are LLTimer::getElapsedSeconds() and LLFrameTimer::getElapsedSeconds() are statics returning the application's uptime, but read like the elapsed time of the timer they are called on, and C++ lets them be called through one. The event poll's early-reply guard did exactly that and measured the uptime instead of its request. Both are now getUptimeSeconds(), and every caller was read again on the way. The debug infinite loop called the static through an instance too, and now prints its own timer's elapsed time. LLVoiceVisualizer kept an LLFrameTimer member only to call the static getTotalSeconds() through it; it calls the static directly and the member is gone. Co-Authored-By: Claude Opus 5.5 --- indra/llaudio/llaudioengine.cpp | 4 ++-- indra/llcommon/llframetimer.h | 6 +++--- indra/llcommon/lltimer.h | 5 +++-- indra/llui/altextview.cpp | 4 ++-- indra/newview/alfloaterexploresounds.cpp | 6 +++--- indra/newview/alfloaterlightbox.cpp | 4 ++-- indra/newview/alfloaterregiontracker.cpp | 4 ++-- indra/newview/llagent.cpp | 2 +- indra/newview/llappviewer.cpp | 4 ++-- indra/newview/llavatarpropertiesprocessor.cpp | 4 ++-- indra/newview/llchathistory.cpp | 4 ++-- indra/newview/llcommandhandler.cpp | 2 +- indra/newview/llconversationmodel.cpp | 2 +- indra/newview/llconversationmodel.h | 2 +- indra/newview/lldebugmessagebox.cpp | 2 +- indra/newview/lldrawpoolwater.cpp | 2 +- indra/newview/lldrawpoolwlsky.cpp | 2 +- indra/newview/llflexibleobject.cpp | 4 ++-- indra/newview/llfloaterimnearbychatlistener.cpp | 2 +- indra/newview/llfloaterland.cpp | 4 ++-- indra/newview/llfloaterworldmap.cpp | 2 +- indra/newview/llhexeditor.cpp | 2 +- indra/newview/llpanelplaceprofile.cpp | 4 ++-- indra/newview/llselectmgr.cpp | 2 +- indra/newview/lltoolbarview.cpp | 2 +- indra/newview/llviewermessage.cpp | 2 +- indra/newview/llviewerobject.cpp | 8 ++++---- indra/newview/llviewerobjectlist.cpp | 2 +- indra/newview/llviewerstats.cpp | 2 +- indra/newview/llvisualeffect.cpp | 6 +++--- indra/newview/llvisualeffect.h | 2 +- indra/newview/llvoavatar.cpp | 4 ++-- indra/newview/llvoicevisualizer.cpp | 10 ++++------ indra/newview/llvoicevisualizer.h | 1 - indra/newview/llworldmap.cpp | 2 +- indra/newview/llworldmapview.cpp | 2 +- indra/newview/rlvlocks.cpp | 4 ++-- indra/newview/rlvlocks.h | 4 ++-- 38 files changed, 64 insertions(+), 66 deletions(-) diff --git a/indra/llaudio/llaudioengine.cpp b/indra/llaudio/llaudioengine.cpp index 6716f65d05d..8457be23463 100644 --- a/indra/llaudio/llaudioengine.cpp +++ b/indra/llaudio/llaudioengine.cpp @@ -1419,7 +1419,7 @@ void LLAudioEngine::logSoundPlay(const LLUUID& id, LLVector3d position, S32 type item->mOwnerID = ownerid; item->mSourceID = sourceid; item->mPlaying = true; - item->mTimeStarted = LLTimer::getElapsedSeconds(); + item->mTimeStarted = LLTimer::getUptimeSeconds(); item->mTimeStopped = F64_MAX; item->mIsTrigger = is_trigger; item->mIsLooped = is_looped; @@ -1437,7 +1437,7 @@ void LLAudioEngine::logSoundStop(const LLUUID& id) { LLSoundHistoryItem& hist_item = *iter->second; hist_item.mPlaying = false; - hist_item.mTimeStopped = LLTimer::getElapsedSeconds(); + hist_item.mTimeStopped = LLTimer::getUptimeSeconds(); pruneSoundLog(); } } diff --git a/indra/llcommon/llframetimer.h b/indra/llcommon/llframetimer.h index ba4f075b571..2c674b159fc 100644 --- a/indra/llcommon/llframetimer.h +++ b/indra/llcommon/llframetimer.h @@ -42,10 +42,10 @@ class LL_COMMON_API LLFrameTimer LLFrameTimer() : mStartTime( sFrameTime ), mExpiry(0), mStarted(true) {} // Return the number of seconds since the start of this - // application instance. - static F64SecondsImplicit getElapsedSeconds() + // application instance, as of the current frame. Static: it is not this + // timer's elapsed time, which is getElapsedTimeF32(). + static F64SecondsImplicit getUptimeSeconds() { - // Loses msec precision after ~4.5 hours... return sFrameTime; } diff --git a/indra/llcommon/lltimer.h b/indra/llcommon/lltimer.h index d8893c25261..dad53a38be9 100644 --- a/indra/llcommon/lltimer.h +++ b/indra/llcommon/lltimer.h @@ -66,8 +66,9 @@ class LL_COMMON_API LLTimer static void cleanupClass(); // Return a high precision number of seconds since the start of - // this application instance. - static F64SecondsImplicit getElapsedSeconds() + // this application instance. Static: it is not this timer's elapsed + // time, which is getElapsedTimeF64(). + static F64SecondsImplicit getUptimeSeconds() { if (sTimer) { diff --git a/indra/llui/altextview.cpp b/indra/llui/altextview.cpp index c3384d6b538..5c53095138e 100644 --- a/indra/llui/altextview.cpp +++ b/indra/llui/altextview.cpp @@ -2313,7 +2313,7 @@ ALTextDocument::Edit ALTextView::edit(const ALTextRange& range_in, std::string_v return done; } const ALTextPos after = mDocument.clamp(done.endAfter()); - mUndo.record(done, before, after, LLTimer::getElapsedSeconds(), std::move(others)); + mUndo.record(done, before, after, LLTimer::getUptimeSeconds(), std::move(others)); placeCaret(after, false); return done; } @@ -2340,7 +2340,7 @@ ALTextDocument::Edit ALTextView::editMany(std::vector((LLTimer::getElapsedSeconds() - item.mTimeStopped) / 60.0)); + format_args["TIME"] = llformat("%.1f", static_cast((LLTimer::getUptimeSeconds() - item.mTimeStopped) / 60.0)); str_not_playing.setArgs(format_args); playing_column["value"] = str_not_playing.getString(); } @@ -450,7 +450,7 @@ void ALFloaterExploreSounds::handleStop() if (iter != sound_log.end()) { iter->second->mPlaying = false; - iter->second->mTimeStopped = LLTimer::getElapsedSeconds(); + iter->second->mTimeStopped = LLTimer::getUptimeSeconds(); } else { @@ -459,7 +459,7 @@ void ALFloaterExploreSounds::handleStop() if (histItem.mID == item.mID) { histItem.mPlaying = false; - histItem.mTimeStopped = LLTimer::getElapsedSeconds(); + histItem.mTimeStopped = LLTimer::getUptimeSeconds(); break; } } diff --git a/indra/newview/alfloaterlightbox.cpp b/indra/newview/alfloaterlightbox.cpp index e792578ec1a..685d45d956a 100644 --- a/indra/newview/alfloaterlightbox.cpp +++ b/indra/newview/alfloaterlightbox.cpp @@ -1017,7 +1017,7 @@ void ALFloaterLightBox::endColorSession(bool escaped) if (mRecordedKeys.count(key) && !llsd_equals(picked, mColorOriginal)) { ScopedHistoryGroup group(mHistory); - mHistory.record(key, mColorOriginal, picked, (F32)LLTimer::getElapsedSeconds().value()); + mHistory.record(key, mColorOriginal, picked, (F32)LLTimer::getUptimeSeconds().value()); } } @@ -1693,7 +1693,7 @@ void ALFloaterLightBox::onGradeSettingChanged(const std::string& name, const LLS // A monotonic clock is all the history wants: it compares two of these to // decide whether one drag is still in progress, and never reads the value // on its own. - mHistory.record(name, before, after, (F32)LLTimer::getElapsedSeconds().value()); + mHistory.record(name, before, after, (F32)LLTimer::getUptimeSeconds().value()); } bool ALFloaterLightBox::applyHistory(bool redo_direction) diff --git a/indra/newview/alfloaterregiontracker.cpp b/indra/newview/alfloaterregiontracker.cpp index 1045678c991..d541de99fd1 100644 --- a/indra/newview/alfloaterregiontracker.cpp +++ b/indra/newview/alfloaterregiontracker.cpp @@ -135,7 +135,7 @@ void ALFloaterRegionTracker::refresh() { maturity.value(info->getAccessIcon()); - info->updateAgentCount(LLTimer::getElapsedSeconds()); + info->updateAgentCount(LLTimer::getUptimeSeconds()); S32 agent_count = info->getAgentCount(); if (info->isDown()) { @@ -190,7 +190,7 @@ void ALFloaterRegionTracker::requestRegionData() const auto& name = it->first; if (LLSimInfo* info = LLWorldMap::getInstance()->simInfoFromName(name)) { - info->updateAgentCount(LLTimer::getElapsedSeconds()); + info->updateAgentCount(LLTimer::getUptimeSeconds()); } else { diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp index a921f6ccb7e..b5226eebcc7 100644 --- a/indra/newview/llagent.cpp +++ b/indra/newview/llagent.cpp @@ -3622,7 +3622,7 @@ LLColor4 LLAgent::getEffectColor() if(AlchemyRainbowEffects) { LLColor3 rainbow; - rainbow.setHSL(fmodf((F32)LLFrameTimer::getElapsedSeconds()/4.f, 1.f), 1.f, 0.5f); + rainbow.setHSL(fmodf((F32)LLFrameTimer::getUptimeSeconds()/4.f, 1.f), 1.f, 0.5f); effect_color.set(rainbow, 1.0f); } return effect_color; diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index a504e3ab341..310e629cbfd 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -649,7 +649,7 @@ LLPurgeDiskCacheThread* LLAppViewer::sPurgeDiskCacheThread = NULL; std::string getRuntime() { - return llformat("%.4f", (F32)LLTimer::getElapsedSeconds().value()); + return llformat("%.4f", (F32)LLTimer::getUptimeSeconds().value()); } LLAppViewer::LLAppViewer() @@ -6539,7 +6539,7 @@ void LLAppViewer::forceErrorInfiniteLoop() { if (timer_expiry.hasExpired()) { - LL_INFOS() << "Infinite loop time : " << timer_total.getElapsedSeconds() << LL_ENDL; + LL_INFOS() << "Infinite loop time : " << timer_total.getElapsedTimeF64() << LL_ENDL; timer_expiry.setTimerExpirySec(report_frequency); } } diff --git a/indra/newview/llavatarpropertiesprocessor.cpp b/indra/newview/llavatarpropertiesprocessor.cpp index df9ad13937d..c70ecaa591e 100644 --- a/indra/newview/llavatarpropertiesprocessor.cpp +++ b/indra/newview/llavatarpropertiesprocessor.cpp @@ -751,7 +751,7 @@ bool LLAvatarPropertiesProcessor::isPendingRequest(const LLUUID& avatar_id, EAva if (it == mRequestTimestamps.end()) return false; // We found a request, check if it has timed out - F64 now = LLFrameTimer::getElapsedSeconds(); + F64 now = LLFrameTimer::getUptimeSeconds(); const F64 REQUEST_EXPIRE_SECS = 5.0; F64 expires = it->second + REQUEST_EXPIRE_SECS; @@ -766,7 +766,7 @@ void LLAvatarPropertiesProcessor::addPendingRequest(const LLUUID& avatar_id, EAv { timestamp_map_t::key_type key = std::make_pair(avatar_id, type); // Add or update existing (expired) request - mRequestTimestamps[ key ] = LLFrameTimer::getElapsedSeconds(); + mRequestTimestamps[ key ] = LLFrameTimer::getUptimeSeconds(); } void LLAvatarPropertiesProcessor::removePendingRequest(const LLUUID& avatar_id, EAvatarProcessorType type) diff --git a/indra/newview/llchathistory.cpp b/indra/newview/llchathistory.cpp index 7c33a9258b0..ed59c18a731 100644 --- a/indra/newview/llchathistory.cpp +++ b/indra/newview/llchathistory.cpp @@ -201,7 +201,7 @@ class LLChatHistoryHeader final: public LLPanel if (mTime > 0) // have frame time { time_t current_time = time_corrected(); - time_t message_time = (time_t)(current_time - LLFrameTimer::getElapsedSeconds() + mTime); + time_t message_time = (time_t)(current_time - LLFrameTimer::getUptimeSeconds() + mTime); // Report abuse shouldn't use AM/PM, use 24-hour time time_string = "[" + LLTrans::getString("TimeMonth") + "]/[" @@ -503,7 +503,7 @@ class LLChatHistoryHeader final: public LLPanel if (mTime > 0) // have frame time { time_t current_time = time_corrected(); - time_t message_time = (time_t)(current_time - LLFrameTimer::getElapsedSeconds() + mTime); + time_t message_time = (time_t)(current_time - LLFrameTimer::getUptimeSeconds() + mTime); // Report abuse shouldn't use AM/PM, use 24-hour time time_string = "[" + LLTrans::getString("TimeMonth") + "]/[" diff --git a/indra/newview/llcommandhandler.cpp b/indra/newview/llcommandhandler.cpp index 4038ae8002b..20f930300a6 100644 --- a/indra/newview/llcommandhandler.cpp +++ b/indra/newview/llcommandhandler.cpp @@ -153,7 +153,7 @@ bool LLCommandHandlerRegistry::dispatch(const std::string& cmd, { break; } - cur_time = LLTimer::getElapsedSeconds(); + cur_time = LLTimer::getUptimeSeconds(); if (cur_time < last_throttle_time + THROTTLE_PERIOD) { // block request from external browser if it happened diff --git a/indra/newview/llconversationmodel.cpp b/indra/newview/llconversationmodel.cpp index 5d93c07f424..81118186623 100644 --- a/indra/newview/llconversationmodel.cpp +++ b/indra/newview/llconversationmodel.cpp @@ -422,7 +422,7 @@ void LLConversationItemSession::setParticipantIsModerator(const LLUUID& particip void LLConversationItemSession::setTimeNow(const LLUUID& participant_id) { - mLastActiveTime = LLFrameTimer::getElapsedSeconds(); + mLastActiveTime = LLFrameTimer::getUptimeSeconds(); mNeedsRefresh = true; LLConversationItemParticipant* participant = findParticipant(participant_id); if (participant) diff --git a/indra/newview/llconversationmodel.h b/indra/newview/llconversationmodel.h index 93598083c0a..234b1a28524 100644 --- a/indra/newview/llconversationmodel.h +++ b/indra/newview/llconversationmodel.h @@ -205,7 +205,7 @@ class LLConversationItemParticipant : public LLConversationItem bool isModerator() const { return mIsModerator; } void moderateVoice(bool mute_voice) { mIsModeratorMuted = mute_voice; } void setIsModerator(bool is_moderator) { mIsModerator = is_moderator; mNeedsRefresh = true; } - void setTimeNow() { mLastActiveTime = LLFrameTimer::getElapsedSeconds(); mNeedsRefresh = true; } + void setTimeNow() { mLastActiveTime = LLFrameTimer::getUptimeSeconds(); mNeedsRefresh = true; } void setDistance(F64 dist) { mDistToAgent = dist; mNeedsRefresh = true; } void buildContextMenu(LLMenuGL& menu, U32 flags); diff --git a/indra/newview/lldebugmessagebox.cpp b/indra/newview/lldebugmessagebox.cpp index 91ea36706e5..83173b97060 100644 --- a/indra/newview/lldebugmessagebox.cpp +++ b/indra/newview/lldebugmessagebox.cpp @@ -272,7 +272,7 @@ void LLDebugVarMessageBox::draw() { if (mSlider1) { - F32 animated_val = clamp_rescale(fmodf((F32)LLFrameTimer::getElapsedSeconds() / 5.f, 1.f), 0.f, 1.f, 0.f, mSlider1->getMaxValue()); + F32 animated_val = clamp_rescale(fmodf((F32)LLFrameTimer::getUptimeSeconds() / 5.f, 1.f), 0.f, 1.f, 0.f, mSlider1->getMaxValue()); mSlider1->setValue(animated_val); sliderChanged(LLSD()); if (mSlider2) diff --git a/indra/newview/lldrawpoolwater.cpp b/indra/newview/lldrawpoolwater.cpp index 6aa6e81b93d..ea2f33b846f 100644 --- a/indra/newview/lldrawpoolwater.cpp +++ b/indra/newview/lldrawpoolwater.cpp @@ -172,7 +172,7 @@ void LLDrawPoolWater::renderPostDeferred(S32 pass) has_normal_mips ? ALSamplers::AnisoWrap : ALSamplers::PointWrap; LLColor4 specular(sun_up ? psky->getSunlightColor() : psky->getMoonlightColor()); - F32 phase_time = (F32) LLFrameTimer::getElapsedSeconds() * 0.5f; + F32 phase_time = (F32) LLFrameTimer::getUptimeSeconds() * 0.5f; LLGLSLShader *shader = nullptr; // One pass, one of two shaders. Void water and region water share state. diff --git a/indra/newview/lldrawpoolwlsky.cpp b/indra/newview/lldrawpoolwlsky.cpp index 0b3c89dae96..6e924d3626c 100644 --- a/indra/newview/lldrawpoolwlsky.cpp +++ b/indra/newview/lldrawpoolwlsky.cpp @@ -264,7 +264,7 @@ void LLDrawPoolWLSky::renderStarsDeferred(const LLVector3& camPosLocal) const // typical session never hits the wrap boundary (where the non-periodic // noise would show a one-frame jump), and short enough that 32-bit float // precision stays good for the scaled time term in the shader. - sStarTime = (F32)fmod(LLFrameTimer::getElapsedSeconds() * 0.5, 86400.0); + sStarTime = (F32)fmod(LLFrameTimer::getUptimeSeconds() * 0.5, 86400.0); gDeferredStarProgram.uniform1f(LLShaderMgr::WATER_TIME, sStarTime); diff --git a/indra/newview/llflexibleobject.cpp b/indra/newview/llflexibleobject.cpp index d9d5768faf9..15576808ae8 100644 --- a/indra/newview/llflexibleobject.cpp +++ b/indra/newview/llflexibleobject.cpp @@ -94,7 +94,7 @@ void LLVolumeImplFlexible::updateClass() { LL_PROFILE_ZONE_SCOPED; - U64 virtual_frame_num = (U64)(LLTimer::getElapsedSeconds() / SEC_PER_FLEXI_FRAME); + U64 virtual_frame_num = (U64)(LLTimer::getUptimeSeconds() / SEC_PER_FLEXI_FRAME); for (std::vector::iterator iter = sInstanceList.begin(); iter != sInstanceList.end(); ++iter) @@ -362,7 +362,7 @@ void LLVolumeImplFlexible::doIdleUpdate() update_period = llclamp(update_period, 1U, 32U); // We control how fast flexies update, buy splitting updates among frames - U64 virtual_frame_num = (U64)(LLTimer::getElapsedSeconds() / SEC_PER_FLEXI_FRAME); + U64 virtual_frame_num = (U64)(LLTimer::getUptimeSeconds() / SEC_PER_FLEXI_FRAME); if (visible) { diff --git a/indra/newview/llfloaterimnearbychatlistener.cpp b/indra/newview/llfloaterimnearbychatlistener.cpp index b15a32ce40d..36df6a3a8ca 100644 --- a/indra/newview/llfloaterimnearbychatlistener.cpp +++ b/indra/newview/llfloaterimnearbychatlistener.cpp @@ -52,7 +52,7 @@ LLFloaterIMNearbyChatListener::LLFloaterIMNearbyChatListener() : // "sendChat" command void LLFloaterIMNearbyChatListener::sendChat(LLSD const& chat_data) { - F64 cur_time = LLTimer::getElapsedSeconds(); + F64 cur_time = LLTimer::getUptimeSeconds(); if (cur_time < mLastThrottleTime + CHAT_THROTTLE_PERIOD) { diff --git a/indra/newview/llfloaterland.cpp b/indra/newview/llfloaterland.cpp index ed582667724..ee303ed7285 100644 --- a/indra/newview/llfloaterland.cpp +++ b/indra/newview/llfloaterland.cpp @@ -3063,7 +3063,7 @@ void LLPanelLandCovenant::refresh() } if (mLastRegionID != region->getRegionID() - || mNextUpdateTime < LLTimer::getElapsedSeconds()) + || mNextUpdateTime < LLTimer::getUptimeSeconds()) { // Request Covenant Info // Note: LLPanelLandCovenant doesn't change Covenant's content and any @@ -3076,7 +3076,7 @@ void LLPanelLandCovenant::refresh() msg->sendReliable(region->getHost()); mLastRegionID = region->getRegionID(); - mNextUpdateTime = LLTimer::getElapsedSeconds() + COVENANT_REFRESH_TIME_SEC; + mNextUpdateTime = LLTimer::getUptimeSeconds() + COVENANT_REFRESH_TIME_SEC; } } diff --git a/indra/newview/llfloaterworldmap.cpp b/indra/newview/llfloaterworldmap.cpp index 44208ba930e..e1c2217fc30 100644 --- a/indra/newview/llfloaterworldmap.cpp +++ b/indra/newview/llfloaterworldmap.cpp @@ -641,7 +641,7 @@ void LLFloaterWorldMap::draw() { if (mCompletingRegionName != "") { - F64 seconds = LLTimer::getElapsedSeconds(); + F64 seconds = LLTimer::getUptimeSeconds(); double value = fmod(seconds, 2); value = 0.5 + 0.5*cos(value * F_PI); LLColor4 loading_color(0.0, F32(value/2), F32(value), 1.0); diff --git a/indra/newview/llhexeditor.cpp b/indra/newview/llhexeditor.cpp index 64ee84b5b70..181df954ac8 100644 --- a/indra/newview/llhexeditor.cpp +++ b/indra/newview/llhexeditor.cpp @@ -908,7 +908,7 @@ void LLHexEditor::draw() // Cursor - if(has_focus && !mHasSelection && (U32(LLTimer::getElapsedSeconds() * 2.0f) & 0x1)) + if(has_focus && !mHasSelection && (U32(LLTimer::getUptimeSeconds() * 2.0f) & 0x1)) { U32 cursor_line = mCursorPos / mColumns; if((cursor_line >= first_line) && (cursor_line <= last_line)) diff --git a/indra/newview/llpanelplaceprofile.cpp b/indra/newview/llpanelplaceprofile.cpp index 5dbd86a4b6b..7addc44792e 100644 --- a/indra/newview/llpanelplaceprofile.cpp +++ b/indra/newview/llpanelplaceprofile.cpp @@ -342,7 +342,7 @@ void LLPanelPlaceProfile::displaySelectedParcelInfo(LLParcel* parcel, return; if (mLastSelectedRegionID != region->getRegionID() - || mNextCovenantUpdateTime < LLTimer::getElapsedSeconds()) + || mNextCovenantUpdateTime < LLTimer::getUptimeSeconds()) { // send EstateCovenantInfo message // Note: LLPanelPlaceProfile doesn't change Covenant's content and any @@ -353,7 +353,7 @@ void LLPanelPlaceProfile::displaySelectedParcelInfo(LLParcel* parcel, msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID()); msg->addUUIDFast(_PREHASH_SessionID,gAgent.getSessionID()); msg->sendReliable(region->getHost()); - mNextCovenantUpdateTime = LLTimer::getElapsedSeconds() + COVENANT_REFRESH_TIME_SEC; + mNextCovenantUpdateTime = LLTimer::getUptimeSeconds() + COVENANT_REFRESH_TIME_SEC; } LLParcelData parcel_data; diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index db6dd78c294..665a607106a 100644 --- a/indra/newview/llselectmgr.cpp +++ b/indra/newview/llselectmgr.cpp @@ -7601,7 +7601,7 @@ void LLSelectNode::renderOneSilhouette(const LLColor4 &color) LLVector3 view_vector = LLViewerCamera::getInstance()->getOrigin() - objectp->getRenderPosition(); silhouette_thickness = view_vector.magVec() * LLSelectMgr::sHighlightThickness * (LLViewerCamera::getInstance()->getView() / LLViewerCamera::getInstance()->getDefaultFOV()); } - F32 animationTime = (F32)LLFrameTimer::getElapsedSeconds(); + F32 animationTime = (F32)LLFrameTimer::getUptimeSeconds(); F32 u_coord = fmod(animationTime * LLSelectMgr::sHighlightUAnim, 1.f); F32 v_coord = 1.f - fmod(animationTime * LLSelectMgr::sHighlightVAnim, 1.f); diff --git a/indra/newview/lltoolbarview.cpp b/indra/newview/lltoolbarview.cpp index 7940754d71a..2cb280250e3 100644 --- a/indra/newview/lltoolbarview.cpp +++ b/indra/newview/lltoolbarview.cpp @@ -829,7 +829,7 @@ void LLToolBarView::updateAutoHide() S32 mouse_y = 0; LLUI::getInstance()->getMousePositionLocal(this, &mouse_x, &mouse_y); - const F64 now = LLFrameTimer::getElapsedSeconds(); + const F64 now = LLFrameTimer::getUptimeSeconds(); const F32 interp = LLSmoothInterpolation::getInterpolant(TOOLBAR_AUTO_HIDE_TIME_CONSTANT); const bool forced_visible = toolbars_forced_visible(); diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp index 224c264b93d..ffd8e38d8be 100644 --- a/indra/newview/llviewermessage.cpp +++ b/indra/newview/llviewermessage.cpp @@ -2519,7 +2519,7 @@ void process_chat_from_simulator(LLMessageSystem *msg, void **user_data) msg->getU8Fast(_PREHASH_ChatData, _PREHASH_Audible, audible_temp); chat.mAudible = (EChatAudible)audible_temp; - chat.mTime = LLFrameTimer::getElapsedSeconds(); + chat.mTime = LLFrameTimer::getUptimeSeconds(); // IDEVO Correct for new-style "Resident" names if (chat.mSourceType == CHAT_SOURCE_AGENT) diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index 1d354ba6777..3e6fff926b9 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -335,7 +335,7 @@ LLViewerObject::LLViewerObject(const LLUUID &id, const LLPCode pcode, LLViewerRe // Both clocks need it: an object whose first update is rejected before the timestamps are // stamped otherwise measures its update age from the start of the session, which is instantly // past every phase-out threshold. - mLastInterpUpdateSecs = LLFrameTimer::getElapsedSeconds(); + mLastInterpUpdateSecs = LLFrameTimer::getUptimeSeconds(); mLastMessageUpdateSecs = mLastInterpUpdateSecs; mPositionRegion = LLVector3(0.f, 0.f, 0.f); @@ -568,7 +568,7 @@ void LLViewerObject::dump() const { LL_INFOS() << "Type: " << pCodeToString(mPrimitiveCode) << LL_ENDL; LL_INFOS() << "Drawable: " << (LLDrawable *)mDrawable << LL_ENDL; - LL_INFOS() << "Update Age: " << LLFrameTimer::getElapsedSeconds() - mLastMessageUpdateSecs << LL_ENDL; + LL_INFOS() << "Update Age: " << LLFrameTimer::getUptimeSeconds() - mLastMessageUpdateSecs << LL_ENDL; LL_INFOS() << "Parent: " << getParent() << LL_ENDL; LL_INFOS() << "ID: " << mID << LL_ENDL; @@ -2535,7 +2535,7 @@ U32 LLViewerObject::processUpdateMessage(LLMessageSystem *mesgsys, // much jumping and hopping around... // U32 ping_delay = mesgsys->mCircuitInfo.getPingDelay(); - const F64 update_time = LLFrameTimer::getElapsedSeconds(); + const F64 update_time = LLFrameTimer::getUptimeSeconds(); // Before the clocks move: how far the prediction had drifted by the time the real answer // arrived. This is the reading that settles the direction of the timestep correction in @@ -7255,7 +7255,7 @@ void LLViewerObject::updateRegion(LLViewerRegion *regionp) { // if (regionp) // { -// F64 now = LLFrameTimer::getElapsedSeconds(); +// F64 now = LLFrameTimer::getUptimeSeconds(); // LL_INFOS() << "Updating to region " << regionp->getName() // << ", ms since last update message: " << (F32)((now - mLastMessageUpdateSecs) * 1000.0) // << ", ms since last interpolation: " << (F32)((now - mLastInterpUpdateSecs) * 1000.0) diff --git a/indra/newview/llviewerobjectlist.cpp b/indra/newview/llviewerobjectlist.cpp index cae961824e0..fc3d044d37f 100644 --- a/indra/newview/llviewerobjectlist.cpp +++ b/indra/newview/llviewerobjectlist.cpp @@ -962,7 +962,7 @@ void LLViewerObjectList::update(LLAgent &agent) //clear avatar LOD change counter LLVOAvatar::sNumLODChangesThisFrame = 0; - const F64 frame_time = LLFrameTimer::getElapsedSeconds(); + const F64 frame_time = LLFrameTimer::getUptimeSeconds(); LLViewerObject *objectp = NULL; diff --git a/indra/newview/llviewerstats.cpp b/indra/newview/llviewerstats.cpp index 9f9c530df14..05cab6be660 100644 --- a/indra/newview/llviewerstats.cpp +++ b/indra/newview/llviewerstats.cpp @@ -660,7 +660,7 @@ void send_viewer_stats(bool include_preferences) time_t ltime; time(<ime); - F32 run_time = F32(LLFrameTimer::getElapsedSeconds()); + F32 run_time = F32(LLFrameTimer::getUptimeSeconds()); agent["start_time"] = S32(ltime - S32(run_time)); diff --git a/indra/newview/llvisualeffect.cpp b/indra/newview/llvisualeffect.cpp index 9d9e3f2950f..e19137f2fae 100644 --- a/indra/newview/llvisualeffect.cpp +++ b/indra/newview/llvisualeffect.cpp @@ -31,7 +31,7 @@ float LLTweenableValueLerp::get() { if (!m_CurValue) { - float curFactor = (F32)((LLTimer::getElapsedSeconds() - m_StartTime) / m_Duration); + float curFactor = (F32)((LLTimer::getUptimeSeconds() - m_StartTime) / m_Duration); if (curFactor < 1.0f) return lerp(m_StartValue, m_EndValue, curFactor); m_CurValue = m_EndValue; @@ -44,7 +44,7 @@ LLColor3 LLTweenableValueLerp::get() { if (!m_CurValue) { - float curFactor = (F32)((LLTimer::getElapsedSeconds() - m_StartTime) / m_Duration); + float curFactor = (F32)((LLTimer::getUptimeSeconds() - m_StartTime) / m_Duration); if (curFactor < 1.0f) return lerp(m_StartValue, m_EndValue, curFactor); m_CurValue = m_EndValue; @@ -57,7 +57,7 @@ LLVector4 LLTweenableValueLerp::get() { if (!m_CurValue) { - float curFactor = (F32)((LLTimer::getElapsedSeconds() - m_StartTime) / m_Duration); + float curFactor = (F32)((LLTimer::getUptimeSeconds() - m_StartTime) / m_Duration); if (curFactor < 1.0f) return lerp(m_StartValue, m_EndValue, curFactor); m_CurValue = m_EndValue; diff --git a/indra/newview/llvisualeffect.h b/indra/newview/llvisualeffect.h index 806d9dc6386..ddc4598e8ec 100644 --- a/indra/newview/llvisualeffect.h +++ b/indra/newview/llvisualeffect.h @@ -141,7 +141,7 @@ class LLTweenableValueLerp : public LLTweenableValue this->m_CurValue = boost::none; m_EndValue = endValue; - m_StartTime = LLTimer::getElapsedSeconds(); + m_StartTime = LLTimer::getUptimeSeconds(); m_Duration = duration; } diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index c2c2603a0eb..a87bbb331b1 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -4011,7 +4011,7 @@ void LLVOAvatar::idleUpdateNameTagText(bool new_name) for(; chat_iter != mChats.end(); ++chat_iter) { - F32 chat_fade_amt = llclamp((F32)((LLFrameTimer::getElapsedSeconds() - chat_iter->mTime) / CHAT_FADE_TIME), 0.f, 4.f); + F32 chat_fade_amt = llclamp((F32)((LLFrameTimer::getUptimeSeconds() - chat_iter->mTime) / CHAT_FADE_TIME), 0.f, 4.f); LLFontGL::StyleFlags style; switch(chat_iter->mChatType) { @@ -11342,7 +11342,7 @@ S32 LLVOAvatar::getUnbakedPixelAreaRank() // static, gets called once per frame from updateApparentAngles. void LLVOAvatar::cullAvatarsByPixelArea() { - F64 current_time = LLFrameTimer::getElapsedSeconds(); + F64 current_time = LLFrameTimer::getUptimeSeconds(); bool needs_resort = sAvatarCullNeedsUpdate || ((current_time - sLastCullUpdateTime) >= 1.0); if (needs_resort) diff --git a/indra/newview/llvoicevisualizer.cpp b/indra/newview/llvoicevisualizer.cpp index 00c33e4ca2f..8b1bda11fc5 100644 --- a/indra/newview/llvoicevisualizer.cpp +++ b/indra/newview/llvoicevisualizer.cpp @@ -96,7 +96,7 @@ F32 LLVoiceVisualizer::sAahPowerTransfersf = 0.0f; LLVoiceVisualizer::LLVoiceVisualizer( const U8 type ) : LLHUDEffect(type) { - mCurrentTime = mTimer.getTotalSeconds(); + mCurrentTime = LLFrameTimer::getTotalSeconds(); mPreviousTime = mCurrentTime; mStartTime = mCurrentTime; mVoiceSourceWorldPosition = LLVector3( 0.0f, 0.0f, 0.0f ); @@ -108,8 +108,6 @@ LLVoiceVisualizer::LLVoiceVisualizer( const U8 type ) mSoundSymbol.mActive = true; mSoundSymbol.mPosition = LLVector3( 0.0f, 0.0f, 0.0f ); - mTimer.reset(); - const char* sound_level_img[] = { "voice_meter_dot.j2c", @@ -173,7 +171,7 @@ void LLVoiceVisualizer::setVoiceEnabled( bool v ) //--------------------------------------------------- void LLVoiceVisualizer::setStartSpeaking() { - mStartTime = mTimer.getTotalSeconds(); + mStartTime = LLFrameTimer::getTotalSeconds(); mCurrentlySpeaking = true; mSoundSymbol.mActive = true; @@ -298,7 +296,7 @@ void LLVoiceVisualizer::lipSyncOohAah( F32& ooh, F32& aah ) } F32 transfer_aah = sAahPowerTransfer[transfer_index]; - F64 current_time = mTimer.getTotalSeconds(); + F64 current_time = LLFrameTimer::getTotalSeconds(); F64 elapsed_time = current_time - mStartTime; U32 elapsed_frames = (U32) (elapsed_time * sOohAahRate); U32 elapsed_oohs = elapsed_frames % sOohs; @@ -346,7 +344,7 @@ void LLVoiceVisualizer::render() if ( mSoundSymbol.mActive ) { mPreviousTime = mCurrentTime; - mCurrentTime = mTimer.getTotalSeconds(); + mCurrentTime = LLFrameTimer::getTotalSeconds(); //--------------------------------------------------------------- // set the sound symbol position over the source (avatar's head) diff --git a/indra/newview/llvoicevisualizer.h b/indra/newview/llvoicevisualizer.h index b788691a4fb..af3611a86a6 100644 --- a/indra/newview/llvoicevisualizer.h +++ b/indra/newview/llvoicevisualizer.h @@ -121,7 +121,6 @@ class LLVoiceVisualizer : public LLHUDEffect LLVector3 mPosition; }; - LLFrameTimer mTimer; // so I can ask the current time in seconds F64 mStartTime; // time in seconds when speaking started F64 mCurrentTime; // current time in seconds, captured every step F64 mPreviousTime; // copy of "current time" from last frame diff --git a/indra/newview/llworldmap.cpp b/indra/newview/llworldmap.cpp index 6248c831afa..67d9e4213fe 100644 --- a/indra/newview/llworldmap.cpp +++ b/indra/newview/llworldmap.cpp @@ -610,7 +610,7 @@ void LLWorldMap::updateRegions(S32 x0, S32 y0, S32 x1, S32 y1) // Expire blocks requested more than BLOCK_UPDATE_TIMER ago so that they are // re-requested below and their sim info refreshed. - const F64 time_now = LLTimer::getElapsedSeconds(); + const F64 time_now = LLTimer::getUptimeSeconds(); for (auto it = mMapBlockLastUpdateOffsets.begin(); it != mMapBlockLastUpdateOffsets.end();) { if ((time_now - it->second) > BLOCK_UPDATE_TIMER) diff --git a/indra/newview/llworldmapview.cpp b/indra/newview/llworldmapview.cpp index 5e60a17ed5b..c5323104d14 100644 --- a/indra/newview/llworldmapview.cpp +++ b/indra/newview/llworldmapview.cpp @@ -374,7 +374,7 @@ void LLWorldMapView::draw() LLTextureView::clearDebugImages(); - F64 current_time = LLTimer::getElapsedSeconds(); + F64 current_time = LLTimer::getUptimeSeconds(); mVisibleRegions.clear(); diff --git a/indra/newview/rlvlocks.cpp b/indra/newview/rlvlocks.cpp index c3c59a14dd4..8279421c6e2 100644 --- a/indra/newview/rlvlocks.cpp +++ b/indra/newview/rlvlocks.cpp @@ -717,7 +717,7 @@ void RlvAttachmentLockWatchdog::onSavedAssetIntoInventory(const LLUUID& idItem) if ( (!itAttach->second.fAssetSaved) && (idItem == itAttach->second.idItem) ) { LLAttachmentsMgr::instance().addAttachmentRequest(itAttach->second.idItem, itAttach->first, true, true); - itAttach->second.tsAttach = LLFrameTimer::getElapsedSeconds(); + itAttach->second.tsAttach = LLFrameTimer::getUptimeSeconds(); } } } @@ -726,7 +726,7 @@ void RlvAttachmentLockWatchdog::onSavedAssetIntoInventory(const LLUUID& idItem) bool RlvAttachmentLockWatchdog::onTimer() { // RELEASE-RLVa: [SL-2.0.0] This will need rewriting for "ENABLE_MULTIATTACHMENTS" - F64 tsCurrent = LLFrameTimer::getElapsedSeconds(); + F64 tsCurrent = LLFrameTimer::getUptimeSeconds(); // Garbage collect (failed) wear requests older than 60 seconds rlv_wear_map_t::iterator itWear = m_PendingWear.begin(); diff --git a/indra/newview/rlvlocks.h b/indra/newview/rlvlocks.h index e0e73b6352e..984d8566eda 100644 --- a/indra/newview/rlvlocks.h +++ b/indra/newview/rlvlocks.h @@ -177,7 +177,7 @@ class RlvAttachmentLockWatchdog : public LLSingleton struct RlvReattachInfo { RlvReattachInfo(const LLUUID& itemid) : idItem(itemid), fAssetSaved(false), tsAttach(0) - { tsDetach = LLFrameTimer::getElapsedSeconds(); } + { tsDetach = LLFrameTimer::getUptimeSeconds(); } LLUUID idItem; bool fAssetSaved; @@ -192,7 +192,7 @@ class RlvAttachmentLockWatchdog : public LLSingleton struct RlvWearInfo { RlvWearInfo(const LLUUID& itemid, ERlvWearMask wearaction) : idItem(itemid), eWearAction(wearaction) - { tsWear = LLFrameTimer::getElapsedSeconds(); } + { tsWear = LLFrameTimer::getUptimeSeconds(); } bool isAddLockedAttachPt(S32 idxAttachPt) const; void dumpInstance() const; From 469c03638e859b57f56bbdf628b7d8c9446cd2ec Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 01:56:50 -0400 Subject: [PATCH 27/33] LLFrameTimer expiries count from now, as LLTimer's do LLFrameTimer::setTimerExpirySec counted from the timer's last reset and LLTimer::setTimerExpirySec counts from now. Code that set an expiry on an LLFrameTimer without resetting it first got an expiry measured from whenever the timer was last reset, often its construction; that is where the broken triple-click, the XUI Studio reread, the avatar picker throttle and the inventory back-off came from. Every LLFrameTimer caller in the tree was read: each resets or starts the timer immediately before setting the expiry, so none changes. What changes is that the next caller to forget the reset gets the expiry it asked for. Co-Authored-By: Claude Opus 5.5 --- indra/llcommon/llframetimer.cpp | 2 +- indra/llcommon/llframetimer.h | 2 +- indra/llcommon/tests/llframetimer_test.cpp | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/indra/llcommon/llframetimer.cpp b/indra/llcommon/llframetimer.cpp index a0080b57bba..bf463056c3c 100644 --- a/indra/llcommon/llframetimer.cpp +++ b/indra/llcommon/llframetimer.cpp @@ -96,7 +96,7 @@ void LLFrameTimer::unpause() void LLFrameTimer::setTimerExpirySec(F32 expiration) { - mExpiry = expiration + mStartTime; + mExpiry = expiration + sFrameTime; } void LLFrameTimer::setExpiryAt(F64 seconds_since_epoch) diff --git a/indra/llcommon/llframetimer.h b/indra/llcommon/llframetimer.h index 2c674b159fc..b552b8f93d2 100644 --- a/indra/llcommon/llframetimer.h +++ b/indra/llcommon/llframetimer.h @@ -82,7 +82,7 @@ class LL_COMMON_API LLFrameTimer void resetWithExpiry(F32 expiration); void pause(); void unpause(); - void setTimerExpirySec(F32 expiration); + void setTimerExpirySec(F32 expiration); // Expires this long from now, as LLTimer's does void setExpiryAt(F64 seconds_since_epoch); bool checkExpirationAndReset(F32 expiration); F32 getElapsedTimeAndResetF32() { F32 t = F32(sFrameTime - mStartTime); reset(); return t; } diff --git a/indra/llcommon/tests/llframetimer_test.cpp b/indra/llcommon/tests/llframetimer_test.cpp index b9a8c91abf6..2402674e08c 100644 --- a/indra/llcommon/tests/llframetimer_test.cpp +++ b/indra/llcommon/tests/llframetimer_test.cpp @@ -112,10 +112,17 @@ namespace tut ensure("timer took too long to expire", iterations_until_expiration <= 10); } -/* template<> template<> void frametimer_object_t::test<4>() { + set_test_name("an expiry counts from now, not from the timer's last reset"); + LLFrameTimer timer; + // Longer than the expiry passes between the timer starting and the + // expiry being set. + ms_sleep(300); + LLFrameTimer::updateFrameTime(); + timer.setTimerExpirySec(0.2f); + ensure("not expired the moment it is set", !timer.hasExpired()); + ensure("the whole expiry still to run", timer.getTimeToExpireF32() > 0.19f); } -*/ } From d83cb8b5c3448b4d133b61ecd1bb6f42a0e392ae Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 01:56:52 -0400 Subject: [PATCH 28/33] A paused or stopped LLFrameTimer keeps its elapsed time A paused LLFrameTimer holds its elapsed time where a running one holds its start time. stop() and reset() did not keep to that: stop() only cleared the started flag, leaving the start time behind, and reset() wrote a start time whatever the state. A stopped timer, or one reset while paused, then read its absolute start time back as its elapsed time, and a later unpause() turned that into nonsense. stop() now freezes the time run as pause() does, reset() zeroes the elapsed time of a paused timer and leaves it paused, and start() marks the timer running before it resets. setExpiryAt, setAge and getElapsedTimeAndResetF32 keep to the same rule. The scene-load telemetry no longer has to unpause gForegroundTime around its reset. Co-Authored-By: Claude Opus 5.5 --- indra/llcommon/llframetimer.cpp | 12 ++++-- indra/llcommon/llframetimer.h | 4 +- indra/llcommon/tests/llframetimer_test.cpp | 48 ++++++++++++++++++++++ indra/newview/llstartup.cpp | 9 ---- 4 files changed, 58 insertions(+), 15 deletions(-) diff --git a/indra/llcommon/llframetimer.cpp b/indra/llcommon/llframetimer.cpp index bf463056c3c..476be89e2bd 100644 --- a/indra/llcommon/llframetimer.cpp +++ b/indra/llcommon/llframetimer.cpp @@ -51,18 +51,22 @@ void LLFrameTimer::updateFrameTime() void LLFrameTimer::start() { - reset(); mStarted = true; + reset(); } void LLFrameTimer::stop() { - mStarted = false; + // Frozen at the time run so far, as pause() leaves it: a stopped timer's + // elapsed time is otherwise read back as its absolute start time. + pause(); } void LLFrameTimer::reset() { - mStartTime = sFrameTime; + // A paused timer keeps its elapsed time where a running one keeps its + // start, so a reset while paused zeroes that rather than writing a start. + mStartTime = mStarted ? sFrameTime : 0.0; mExpiry = sFrameTime; } @@ -101,7 +105,7 @@ void LLFrameTimer::setTimerExpirySec(F32 expiration) void LLFrameTimer::setExpiryAt(F64 seconds_since_epoch) { - mStartTime = sFrameTime; + reset(); mExpiry = seconds_since_epoch - (USEC_TO_SEC_F64 * sStartTotalTime); } diff --git a/indra/llcommon/llframetimer.h b/indra/llcommon/llframetimer.h index b552b8f93d2..4be90a50637 100644 --- a/indra/llcommon/llframetimer.h +++ b/indra/llcommon/llframetimer.h @@ -85,9 +85,9 @@ class LL_COMMON_API LLFrameTimer void setTimerExpirySec(F32 expiration); // Expires this long from now, as LLTimer's does void setExpiryAt(F64 seconds_since_epoch); bool checkExpirationAndReset(F32 expiration); - F32 getElapsedTimeAndResetF32() { F32 t = F32(sFrameTime - mStartTime); reset(); return t; } + F32 getElapsedTimeAndResetF32() { F32 t = getElapsedTimeF32(); reset(); return t; } - void setAge(const F64 age) { mStartTime = sFrameTime - age; } + void setAge(const F64 age) { mStartTime = mStarted ? sFrameTime - age : age; } // ACCESSORS bool hasExpired() const { return (sFrameTime >= mExpiry); } diff --git a/indra/llcommon/tests/llframetimer_test.cpp b/indra/llcommon/tests/llframetimer_test.cpp index 2402674e08c..2af7be4fe59 100644 --- a/indra/llcommon/tests/llframetimer_test.cpp +++ b/indra/llcommon/tests/llframetimer_test.cpp @@ -125,4 +125,52 @@ namespace tut ensure("not expired the moment it is set", !timer.hasExpired()); ensure("the whole expiry still to run", timer.getTimeToExpireF32() > 0.19f); } + + template<> template<> + void frametimer_object_t::test<5>() + { + set_test_name("a stopped timer keeps the time it ran, and resumes from it"); + // Far enough from the start of the run that the absolute start time + // cannot pass for the time run. + ms_sleep(300); + LLFrameTimer::updateFrameTime(); + LLFrameTimer timer; + timer.start(); + ms_sleep(200); + LLFrameTimer::updateFrameTime(); + const F32 ran = timer.getElapsedTimeF32(); + timer.stop(); + ensure("stopped", !timer.getStarted()); + ensure_approximately_equals("the time it ran", timer.getElapsedTimeF32(), ran, 12); + ms_sleep(100); + LLFrameTimer::updateFrameTime(); + ensure_approximately_equals("not running while stopped", timer.getElapsedTimeF32(), ran, 12); + timer.unpause(); + ensure_approximately_equals("resumed from where it stopped", timer.getElapsedTimeF32(), ran, 12); + } + + template<> template<> + void frametimer_object_t::test<6>() + { + set_test_name("a reset while paused zeroes the time run and leaves it paused"); + ms_sleep(300); + LLFrameTimer::updateFrameTime(); + LLFrameTimer timer; + timer.start(); + ms_sleep(100); + LLFrameTimer::updateFrameTime(); + timer.pause(); + timer.reset(); + ensure("still paused", !timer.getStarted()); + ensure_approximately_equals("no time run", timer.getElapsedTimeF32(), 0.f, 12); + ms_sleep(100); + LLFrameTimer::updateFrameTime(); + ensure_approximately_equals("not running while paused", timer.getElapsedTimeF32(), 0.f, 12); + timer.unpause(); + ensure_approximately_equals("runs on from zero", timer.getElapsedTimeF32(), 0.f, 12); + timer.pause(); + timer.start(); + ensure("started", timer.getStarted()); + ensure_approximately_equals("start() runs from zero, paused or not", timer.getElapsedTimeF32(), 0.f, 12); + } } diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index f6833e0ddf5..f8d09951ecf 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -2492,16 +2492,7 @@ bool idle_startup() gRenderStartTime.reset(); gSimLastTime = 0.f; gSimFrames = (F32)gFrameCount; - // Resetting a paused timer stores an absolute time where its paused - // elapsed time belongs, so the reset happens running and the pause is - // put back after. - const bool foreground = gForegroundTime.getStarted(); - gForegroundTime.unpause(); gForegroundTime.reset(); - if (!foreground) - { - gForegroundTime.pause(); - } gForegroundFrameCount = 0; // HACK: Inform simulator of window size. From bd6bfc2603f3311f622b7f6672bf23765cd886f9 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 01:56:54 -0400 Subject: [PATCH 29/33] LLTimer has resetWithExpiry, and a test of what clears an expiry LLTimer::start() and reset() clear the expiry, so setting one and then starting the timer leaves it expired, which is how the AIS update timers went wrong. resetWithExpiry does both in the order that works, as LLFrameTimer's already did. The new test pins that a fresh timer reads expired, that resetWithExpiry keeps its expiry, that start() clears one, and that the uptime is the application's and not the timer's. Co-Authored-By: Claude Opus 5.5 --- indra/llcommon/CMakeLists.txt | 1 + indra/llcommon/lltimer.h | 3 +- indra/llcommon/tests/lltimer_test.cpp | 85 +++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 indra/llcommon/tests/lltimer_test.cpp diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 7e1f2f316ab..58e2790960b 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -340,6 +340,7 @@ if(AL_BUILD_TESTS) llstreamtools llstring llstring_utf + lltimer lltrace lltreeiterators llunits diff --git a/indra/llcommon/lltimer.h b/indra/llcommon/lltimer.h index dad53a38be9..869af9e57be 100644 --- a/indra/llcommon/lltimer.h +++ b/indra/llcommon/lltimer.h @@ -92,7 +92,8 @@ class LL_COMMON_API LLTimer void stop() { mStarted = false; } void reset(); // Resets the timer void setLastClockCount(U64 current_count); // Sets the timer so that the next elapsed call will be relative to this time - void setTimerExpirySec(F32SecondsImplicit expiration); + void setTimerExpirySec(F32SecondsImplicit expiration); // Expires this long from now + void resetWithExpiry(F32SecondsImplicit expiration) { reset(); setTimerExpirySec(expiration); } bool checkExpirationAndReset(F32 expiration); bool hasExpired() const; F32SecondsImplicit getElapsedTimeAndResetF32(); // Returns elapsed time in seconds with reset diff --git a/indra/llcommon/tests/lltimer_test.cpp b/indra/llcommon/tests/lltimer_test.cpp new file mode 100644 index 00000000000..3af0abd4053 --- /dev/null +++ b/indra/llcommon/tests/lltimer_test.cpp @@ -0,0 +1,85 @@ +/** + * @file lltimer_test.cpp + * @brief When an LLTimer's expiry is, and what resets it. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * 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 + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "../lltimer.h" + +#include "../test/lltut.h" + +namespace tut +{ + struct timer_data + { + // The global timer behind the uptime; an application makes it in + // LLCommon::initClass, which the test runner does not call. + timer_data() { LLTimer::initClass(); } + }; + typedef test_group timer_group; + typedef timer_group::object timer_object; + tut::timer_group timer_instance("LLTimer"); + + template<> template<> + void timer_object::test<1>() + { + set_test_name("a new timer reads expired until it is given an expiry"); + LLTimer timer; + ensure("expired", timer.hasExpired()); + timer.setTimerExpirySec(10.f); + ensure("not expired once given ten seconds", !timer.hasExpired()); + } + + template<> template<> + void timer_object::test<2>() + { + set_test_name("resetWithExpiry restarts the timer and keeps the expiry"); + LLTimer timer; + ms_sleep(50); + timer.resetWithExpiry(10.f); + ensure("not expired", !timer.hasExpired()); + ensure("the whole expiry to run", timer.getRemainingTimeF32() > 9.9f); + ensure("restarted", timer.getElapsedTimeF32() < 0.04f); + } + + template<> template<> + void timer_object::test<3>() + { + set_test_name("start() after an expiry clears it, which is why resetWithExpiry exists"); + LLTimer timer; + timer.setTimerExpirySec(10.f); + timer.start(); + ensure("start() cleared the expiry", timer.hasExpired()); + } + + template<> template<> + void timer_object::test<4>() + { + set_test_name("the uptime is the application's, not this timer's"); + LLTimer timer; + ms_sleep(50); + timer.reset(); + ensure("the timer was just reset", timer.getElapsedTimeF64() < 0.04); + ensure("the uptime runs from before the reset", LLTimer::getUptimeSeconds() > timer.getElapsedTimeF64()); + } +} From 78e9e2bd463923984874e4a7eb6052911e9592e5 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 01:56:56 -0400 Subject: [PATCH 30/33] Live files are checked on every event-timer tick A live file's event timer fires once its period has passed on a live clock, and the check it called then asked a frame-quantised timer whether the same period had passed. Frame time could fall short of the period by part of a frame, the check was skipped, and the file was next looked at a whole period later: up to twice the refresh period for logcontrol.xml, fonts.xml and externally edited notecards. The event timer now forces the check, since it has already waited the period. Co-Authored-By: Claude Opus 5.5 --- indra/llcommon/lllivefile.cpp | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/indra/llcommon/lllivefile.cpp b/indra/llcommon/lllivefile.cpp index 58de61a7e48..526aebd89e0 100644 --- a/indra/llcommon/lllivefile.cpp +++ b/indra/llcommon/lllivefile.cpp @@ -161,30 +161,16 @@ std::string LLLiveFile::filename() const return impl.mFilename; } -namespace -{ - class LiveFileEventTimer : public LLEventTimer - { - public: - LiveFileEventTimer(LLLiveFile& f, F32 refresh) - : LLEventTimer(refresh), mLiveFile(f) - { } - - bool tick() - { - mLiveFile.checkAndReload(); - return false; - } - - private: - LLLiveFile& mLiveFile; - }; - -} - void LLLiveFile::addToEventTimer() { - impl.mEventTimer = new LiveFileEventTimer(*this, impl.mRefreshPeriod); + // The event timer keeps the refresh period itself, so each tick checks. + // Asking the frame-quantised gate again could find a frame less than a + // period since the last check and skip one, doubling the period. + impl.mEventTimer = LLEventTimer::run_every(impl.mRefreshPeriod, [this]() + { + impl.mForceCheck = true; + checkAndReload(); + }); } void LLLiveFile::setRefreshPeriod(F32 seconds) From 9244d374f18fa597eb810724ba84128e34efc8ce Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 02:11:53 -0400 Subject: [PATCH 31/33] A fatal signal takes the process down where it happened Without a crash reporter the viewer installs its own handler, and it answered a fatal signal on the main thread by calling LLApp::setError() from inside the handler. That posts the status change, and its listeners close work queues and join thread pools: none of it safe in a signal handler, and after a fault the heap may already be damaged. Each worker the join wakes frees its malloc cache on the way out and aborts on that damage. A Linux crash in LLReflectionMap::syncToViewerObject came back as a worker's munmap_chunk() SIGABRT, with a third thread terminating on a fiber mutex lock_error inside a re-entered handler, and the fault that started it buried under both. Every fatal signal now restores the default handlers and re-raises, as helper threads already did. The isError() check for a second signal goes, since it locks a fiber mutex and the restored defaults already make a second signal fatal. So does the smackdown branch's setDefaultLevel call, which only quietened the shutdown that no longer runs, and the --disablecrashlogger branch folds in, having done exactly this already. The cost is the final "status: error" log line; none of the LLApp listeners reports crashes. The handler builds only on Linux and macOS and was checked by reading; llapp.cpp compiles on Windows. Co-Authored-By: Claude Opus 5.5 --- indra/llcommon/llapp.cpp | 75 +++++++++------------------------------- 1 file changed, 16 insertions(+), 59 deletions(-) diff --git a/indra/llcommon/llapp.cpp b/indra/llcommon/llapp.cpp index 6d4b7f5b9f1..1cad7b256da 100644 --- a/indra/llcommon/llapp.cpp +++ b/indra/llcommon/llapp.cpp @@ -38,13 +38,11 @@ #include "llcommon.h" -#include "llerrorcontrol.h" #include "llframetimer.h" #include "lllivefile.h" #include "llmemory.h" #include "llstl.h" // for DeletePointer() #include "llstring.h" -#include "llthread.h" // for on_main_thread() #include "lleventtimer.h" #include "stringize.h" #include "llcleanup.h" @@ -377,10 +375,9 @@ void LLApp::sendOutOfDiskSpaceNotification() #ifndef LL_WINDOWS // With a crash reporter in the process, the crash signals are its own: // crashpad on Linux and SentryCrash on macOS install their handlers after -// this runs, and a handler of ours underneath would run the app's shutdown -// from inside a crashed process once the reporter re-raises. The signals the -// app answers itself, for a graceful quit or to ignore, stay with it either -// way. +// this runs, and a handler of ours underneath would have nothing to add once +// the reporter re-raises. The signals the app answers itself, for a graceful +// quit or to ignore, stay with it either way. void setup_signals() { // @@ -484,8 +481,6 @@ void default_unix_signal_handler(int signum, siginfo_t *info, void *) { // Unix implementation of synchronous signal handler // This runs in the thread that threw the signal. - // We do the somewhat sketchy operation of blocking in here until the error handler - // has gracefully stopped the app. // FIXME(brad) - we are using this handler for asynchronous signals as well, so sLogInSignal is currently // disabled for safety. we need to find a way to selectively reenable it when it is safe. @@ -555,11 +550,6 @@ void default_unix_signal_handler(int signum, siginfo_t *info, void *) { LL_WARNS() << "Signal handler - Handling smackdown signal!" << LL_ENDL; } - else - { - // Don't log anything, even errors - this is because this signal could happen anywhere. - LLError::setDefaultLevel(LLError::LEVEL_NONE); - } // Change the signal that we reraise to SIGABRT, so we generate a core dump. signum = SIGABRT; @@ -567,54 +557,21 @@ void default_unix_signal_handler(int signum, siginfo_t *info, void *) if (LLApp::sLogInSignal) { - LL_WARNS() << "Signal handler - Handling fatal signal!" << LL_ENDL; - } - - if (LLApp::isError()) - { - // Received second fatal signal while handling first, just die right now - // Set the signal handlers back to default before handling the signal - this makes the next signal wipe out the app. - clear_signals(); - - if (LLApp::sLogInSignal) - { - LL_WARNS() << "Signal handler - Got another fatal signal while in the error handler, die now!" << LL_ENDL; - } - raise(signum); - return; + LL_WARNS() << "Signal handler - Fatal signal, passing it back to the operating system" << LL_ENDL; } - if (LLApp::sLogInSignal) - { - LL_WARNS() << "Signal handler - Flagging error status and waiting for shutdown" << LL_ENDL; - } - - if (LLApp::isCrashloggerDisabled()) // Don't gracefully handle any signal, crash and core for a gdb post mortem - { - clear_signals(); - LL_WARNS() << "Fatal signal received, not handling the crash here, passing back to operating system" << LL_ENDL; - raise(signum); - return; - } - - if (!on_main_thread()) - { - // A fatal signal on a helper thread cannot be handled gracefully: - // setError() posts events and joins thread pools, which deadlocks - // if the crashing thread is one the shutdown depends on, e.g. a - // libdispatch worker inside the macOS GL driver. Crash instead. - clear_signals(); - raise(signum); - return; - } - - // Flag status to ERROR - LLApp::setError(); - - if (LLApp::sLogInSignal) - { - LL_WARNS() << "Signal handler - App is stopped, reraising signal" << LL_ENDL; - } + // A fatal signal is never handled here, on any thread. LLApp::setError() + // posts the status change, and its listeners close work queues and join + // thread pools -- none of it safe inside a signal handler, least of all + // after a fault, when the heap may already be damaged. Each worker that + // exits frees its malloc cache on the way out and aborts on that damage, + // so the core records a worker's abort instead of the fault that started + // it; and a fault on a thread the shutdown waits for, such as a + // libdispatch worker inside the macOS GL driver, deadlocks outright. + // LLApp::isError() is no safer, since it locks a fiber mutex. + // + // Restoring the default handlers before re-raising also covers a second + // fatal signal: it takes the process down by default. clear_signals(); raise(signum); return; From 4d7b1b7e790a465822891991d9dad9a04a684d4e Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 02:11:55 -0400 Subject: [PATCH 32/33] A reflection probe its owner let go of stops counting as relevant LLReflectionMap::mGroup is a raw pointer, and ~LLSpatialGroup left it set. The manager keeps a probe until its next update finds nothing else holds it, and in that window autoAdjustOrigin dereferences mGroup and runs lineSegmentIntersect through it. Clearing the pointer alone would make such a probe look like a terrain probe, which is all a probe with neither a group nor a viewer object is, and a manual probe already did after markDead: relevant at RenderReflectionProbeLevel 2, so free to take a cube slot, join neighbour lists and draw an occlusion query in the pass before it was deleted. Every owner now calls LLReflectionMap::orphan() when it lets go -- the spatial group's destructor, LLViewerObject::markDead and its destructor, LLVOVolume when an object stops being a probe, and the region's destructor for its terrain probes. orphan() clears the pointer back and marks the probe, and isRelevant() rejects an orphan before it looks at which pointers are set. An object that stops being a probe now drops its probe's reference to it at once; that probe used to stay a live manual probe for one more pass. Compiled on Windows; runtime verification owed. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llreflectionmap.cpp | 12 ++++++++++++ indra/newview/llreflectionmap.h | 10 ++++++++++ indra/newview/llspatialpartition.cpp | 7 +++++++ indra/newview/llviewerobject.cpp | 4 ++-- indra/newview/llviewerregion.cpp | 9 +++++++++ indra/newview/llvovolume.cpp | 1 + 6 files changed, 41 insertions(+), 2 deletions(-) diff --git a/indra/newview/llreflectionmap.cpp b/indra/newview/llreflectionmap.cpp index 11dd05bcf09..a8a9f147d6e 100644 --- a/indra/newview/llreflectionmap.cpp +++ b/indra/newview/llreflectionmap.cpp @@ -213,6 +213,13 @@ void LLReflectionMap::syncToViewerObject() } } +void LLReflectionMap::orphan() +{ + mGroup = nullptr; + mViewerObject = nullptr; + mOrphaned = true; +} + bool LLReflectionMap::eclipses(const LLReflectionMap* other, F32 margin) const { if (!other || other == this || !mViewerObject || mViewerObject->isDead()) @@ -383,6 +390,11 @@ bool LLReflectionMap::isRelevant() const { static LLCachedControl RenderReflectionProbeLevel(gSavedSettings, "RenderReflectionProbeLevel", 3); + if (mOrphaned) + { // whatever it was registered for is gone; the manager deletes it on its next update + return false; + } + if (mViewerObject && RenderReflectionProbeLevel > 0) { // not an automatic probe return true; diff --git a/indra/newview/llreflectionmap.h b/indra/newview/llreflectionmap.h index 3c20a232077..6e7b50b8da1 100644 --- a/indra/newview/llreflectionmap.h +++ b/indra/newview/llreflectionmap.h @@ -70,6 +70,11 @@ class alignas(16) LLReflectionMap : public LLRefCount // sort order and influence volumes from these two values. void syncToViewerObject(); + // Called by the spatial group, viewer object or region this probe was registered for when it + // lets go of the probe. Clears any pointer back to it, and takes the probe out of relevance for + // the time the manager keeps it before deleting it. + void orphan(); + // True if this probe's influence volume has moved or resized enough since its neighbour // list was built that the list may no longer describe it. bool neighborsAreStale() const; @@ -125,6 +130,11 @@ class alignas(16) LLReflectionMap : public LLRefCount // longer affect any pixel (maintained by LLReflectionMapManager, consulted by isRelevant) bool mInsideManualProbe = false; + // whatever registered this probe has let go of it (see orphan). Without this a probe with + // neither a group nor a viewer object is indistinguishable from a live terrain probe, and + // would be treated as one until the manager deletes it. + bool mOrphaned = false; + // fade in parameter for this probe F32 mFadeIn = 0.f; diff --git a/indra/newview/llspatialpartition.cpp b/indra/newview/llspatialpartition.cpp index b6e80c42b71..a79e03831db 100644 --- a/indra/newview/llspatialpartition.cpp +++ b/indra/newview/llspatialpartition.cpp @@ -128,6 +128,13 @@ LLSpatialGroup::~LLSpatialGroup() sNodeCount--; + // unhook from the reflection probe, which the manager keeps until its next update + // notices nothing else holds it -- and autoAdjustOrigin dereferences mGroup until then + if (mReflectionProbe.notNull()) + { + mReflectionProbe->orphan(); + } + clearDrawMap(); } diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index 3e6fff926b9..29ae8526b09 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -356,7 +356,7 @@ LLViewerObject::~LLViewerObject() // unhook from reflection probe manager if (mReflectionProbe.notNull()) { - mReflectionProbe->mViewerObject = nullptr; + mReflectionProbe->orphan(); mReflectionProbe = nullptr; } @@ -537,7 +537,7 @@ void LLViewerObject::markDead() if (mReflectionProbe.notNull()) { - mReflectionProbe->mViewerObject = nullptr; + mReflectionProbe->orphan(); mReflectionProbe = nullptr; } diff --git a/indra/newview/llviewerregion.cpp b/indra/newview/llviewerregion.cpp index c603838b86c..4686de85cf4 100644 --- a/indra/newview/llviewerregion.cpp +++ b/indra/newview/llviewerregion.cpp @@ -741,6 +741,15 @@ LLViewerRegion::~LLViewerRegion() gObjectList.killObjects(this); } + // the reflection probe manager keeps this region's terrain probes until its next update + for (auto& probe : mReflectionMaps) + { + if (probe.notNull()) + { + probe->orphan(); + } + } + delete mImpl->mCompositionp; delete mParcelOverlay; delete mImpl->mLandp; diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp index f15182ea967..f02dffc38cc 100644 --- a/indra/newview/llvovolume.cpp +++ b/indra/newview/llvovolume.cpp @@ -4596,6 +4596,7 @@ void LLVOVolume::updateReflectionProbePtr() { if (mReflectionProbe.notNull()) { + mReflectionProbe->orphan(); mReflectionProbe = nullptr; } From 09a85a9ed777249c12b228f383a42a023266e2cb Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 4 Oct 2026 02:11:57 -0400 Subject: [PATCH 33/33] A probe update stops when its probe stops being relevant The probe being updated was finished whatever became of it, so each of its remaining passes rendered the whole scene for a probe the release pass was about to take the slot from, or, once orphaned, to delete. update() now abandons it first, by the rule the scheduling loop uses to skip a probe: the default probe is exempt and pausing does not count. That also covers coverage being lowered mid-update, where at level 0 a non-default probe could reach the llassert in updateProbeFace, and an automatic probe a manual probe swallows partway through. deleteProbe already abandoned an update when it deleted the probe being updated, but left mRadiancePass set. The radiance half runs second and is what marks a probe complete, so the next probe started on it, skipped its irradiance projection and was marked complete on whatever SH coefficients its slot's previous owner left behind. Both paths now go through abandonProbeUpdate(), which resets the probe, the face and the pass together. Compiled on Windows; runtime verification owed. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llreflectionmapmanager.cpp | 24 ++++++++++++++++++++++-- indra/newview/llreflectionmapmanager.h | 3 +++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/indra/newview/llreflectionmapmanager.cpp b/indra/newview/llreflectionmapmanager.cpp index 5bde77948c5..7ec2e97695b 100644 --- a/indra/newview/llreflectionmapmanager.cpp +++ b/indra/newview/llreflectionmapmanager.cpp @@ -401,6 +401,15 @@ void LLReflectionMapManager::update() LLReflectionMap* oldestProbe = nullptr; LLReflectionMap* oldestOccluded = nullptr; + // A probe that stopped being relevant partway through its twelve passes is dropped rather than + // finished, by the same rule the scheduling loop below uses to skip one. Every remaining pass + // renders the whole scene for a probe that is about to lose its cube slot in the release pass, + // or, when whatever registered it is gone, to be deleted outright. + if (mUpdatingProbe != nullptr && mUpdatingProbe != mDefaultProbe && !mUpdatingProbe->isRelevant()) + { + abandonProbeUpdate(); + } + if (mUpdatingProbe != nullptr) { did_update = true; @@ -911,8 +920,7 @@ void LLReflectionMapManager::deleteProbe(U32 i) } if (mUpdatingProbe == probe) { - mUpdatingProbe = nullptr; - mUpdatingFace = 0; + abandonProbeUpdate(); } // remove from any Neighbors lists @@ -927,6 +935,18 @@ void LLReflectionMapManager::deleteProbe(U32 i) mProbes.erase(mProbes.begin() + i); } +void LLReflectionMapManager::abandonProbeUpdate() +{ + mUpdatingProbe = nullptr; + mUpdatingFace = 0; + + // The irradiance half runs first and the radiance half is what marks a probe complete. Left + // set, this would start the next probe on its radiance half: it would skip the irradiance + // projection altogether and be marked complete on whatever SH coefficients its slot's + // previous owner left behind. + mRadiancePass = false; +} + void LLReflectionMapManager::doProbeUpdate() { diff --git a/indra/newview/llreflectionmapmanager.h b/indra/newview/llreflectionmapmanager.h index bb5627ef208..85719d9b94d 100644 --- a/indra/newview/llreflectionmapmanager.h +++ b/indra/newview/llreflectionmapmanager.h @@ -194,6 +194,9 @@ class alignas(16) LLReflectionMapManager // delete the probe with the given index in mProbes void deleteProbe(U32 i); + // stop generating mUpdatingProbe partway through, so the next probe starts from its first pass + void abandonProbeUpdate(); + // get a free cube index // returns -1 if allocation failed S32 allocateCubeIndex();