Skip to content

Clock and timer fixes from a review of every timer call site - #403

Open
RyeMutt wants to merge 33 commits into
developfrom
rye/clock-fixes
Open

RyeMutt wants to merge 33 commits into
developfrom
rye/clock-fixes

Conversation

@RyeMutt

@RyeMutt RyeMutt commented Oct 4, 2026

Copy link
Copy Markdown
Member

Summary

Fixes from a review of every clock and timer call site in the tree, plus three probe and signal-handler fixes that landed on the same branch.

The expiry trap

LLFrameTimer::setTimerExpirySec counted from the timer's last reset, while LLTimer's counts from now, and LLTimer::start()/reset() clear the expiry. Between them:

  • Triple-click line selection in ALTextView never worked.
  • The XUI Studio reread ran on every edit instead of once the typing stopped.
  • The avatar picker's throttle ran every draw.
  • AIS update timers started expired.
  • A failed folder fetch backed off from the fetch's start, not from the failure.

The event poll's "response arrived too early" guard called the static uptime getter through an instance, so it was dead after ten seconds of uptime. Linden's code has the same bug. Fast 499/5xx replies now take the error path with its backoff, and fifteen in a row force a disconnect, as the code intends.

The API underneath is fixed too:

  • LLFrameTimer expiries count from now. Every existing caller was read first, and none changes behaviour.
  • A paused or stopped LLFrameTimer keeps its elapsed time, instead of reading back its absolute start time.
  • LLTimer::resetWithExpiry is added.
  • The static uptime getters are renamed getUptimeSeconds.

Precision and units

  • The group cache LRU and Nearby's recent-arrival sort keep epoch times as F64. As F32 they resolved 128 s, so the group cache grew past its cap.
  • Scene Load Statistics reads the avatar time in milliseconds. It was 1000× too small.
  • A saved raw image is kept for its keep time after its last use, not only during the first N seconds of the session.
  • The geometry budget is capped at 5 ms a frame. The first frame after a long teleport could spend a second on it.

Threads and stats

  • Threaded file pickers no longer write the frame clock or reset the keyboard from their worker thread.
  • Frame stats no longer stop for the rest of the session after the first modal picker.
  • Session telemetry (sim_fps, foreground fps) counts from the scene-load start.
  • LLLeap's error drain no longer spins forever when the global timer doesn't exist.
  • A deadman timer built during static initialisation gets its horizon.

Server time

  • Event reminders, parcel access and ban times, timed bans, the display-name lockout and chat-log dates compare against server-corrected time.
  • The region's day cycle runs on the server's clock.

Smaller fixes

  • A failed experience lookup expires after its retry delay, not around 2080.
  • A ping answered within one message-time sample records its real time.
  • A conversation stamped in the future isn't purged as the oldest.
  • Pose undo coalescing measures on the steady clock.
  • Pending profile requests expire on the frame clock.
  • An inspector's fade doesn't restart when the mouse leaves it.
  • Live files are checked on every event-timer tick. Before, they could take up to twice the refresh period.

Also on this branch

  • 88f684d250: fatal signals restore the default handlers and re-raise, instead of calling LLApp::setError() from inside the handler. Linux and macOS only; checked by reading, and it compiles on Windows.
  • 155bfbd209, d165e55ba3: reflection probes are orphaned when their owner lets go of them, and a probe update stops once its probe is no longer relevant. Compiled on Windows; runtime verification is owed.

Testing

  • Built RelWithDebInfo on Windows. 375/375 ctest at fa9a934c9d, before the last three commits.

  • New tests:

    • altextview test 78 (triple-click);
    • llframetimer tests 4–6 (expiry from now, stop(), reset() while paused);
    • lltimer_test.

    Each new test was run against the old code to confirm it fails.

  • The clock and timer commits were verified in the viewer on Windows.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 42 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 76cc66b2-a4cf-4711-a7a1-e20c326a8451
📥 Commits

Reviewing files that changed from the base of the PR and between 3e4b866 and 09a85a9.

📒 Files selected for processing (83)
  • indra/llaudio/llaudioengine.cpp
  • indra/llcommon/CMakeLists.txt
  • indra/llcommon/llapp.cpp
  • indra/llcommon/lldeadmantimer.cpp
  • indra/llcommon/llframetimer.cpp
  • indra/llcommon/llframetimer.h
  • indra/llcommon/llleap.cpp
  • indra/llcommon/lllivefile.cpp
  • indra/llcommon/lltimer.h
  • indra/llcommon/tests/llframetimer_test.cpp
  • indra/llcommon/tests/lltimer_test.cpp
  • indra/llmessage/llcircuit.cpp
  • indra/llmessage/llexperiencecache.cpp
  • indra/llui/altextview.cpp
  • indra/llui/tests/altextview_test.cpp
  • indra/newview/alfloaterexploresounds.cpp
  • indra/newview/alfloaterlightbox.cpp
  • indra/newview/alfloaterregiontracker.cpp
  • indra/newview/alfloaterxuistudio.cpp
  • indra/newview/fsjointpose.cpp
  • indra/newview/fsjointpose.h
  • indra/newview/llagent.cpp
  • indra/newview/llaisapi.cpp
  • indra/newview/llappviewer.cpp
  • indra/newview/llavatarpropertiesprocessor.cpp
  • indra/newview/llavatarpropertiesprocessor.h
  • indra/newview/llchathistory.cpp
  • indra/newview/llcommandhandler.cpp
  • indra/newview/llconversationlog.cpp
  • indra/newview/llconversationmodel.cpp
  • indra/newview/llconversationmodel.h
  • indra/newview/lldebugmessagebox.cpp
  • indra/newview/lldirpicker.cpp
  • indra/newview/lldrawpoolwater.cpp
  • indra/newview/lldrawpoolwlsky.cpp
  • indra/newview/llenvironment.cpp
  • indra/newview/lleventnotifier.cpp
  • indra/newview/lleventpoll.cpp
  • indra/newview/llfilepicker.cpp
  • indra/newview/llflexibleobject.cpp
  • indra/newview/llfloateravatarpicker.cpp
  • indra/newview/llfloaterbanduration.cpp
  • indra/newview/llfloaterdisplayname.cpp
  • indra/newview/llfloaterimnearbychatlistener.cpp
  • indra/newview/llfloaterland.cpp
  • indra/newview/llfloaterworldmap.cpp
  • indra/newview/llgroupmgr.cpp
  • indra/newview/llgroupmgr.h
  • indra/newview/llhexeditor.cpp
  • indra/newview/llinspect.cpp
  • indra/newview/lllogchat.cpp
  • indra/newview/llpanelenvironment.cpp
  • indra/newview/llpanelpeople.cpp
  • indra/newview/llpanelplaceprofile.cpp
  • indra/newview/llpanelprofile.cpp
  • indra/newview/llrecentpeople.cpp
  • indra/newview/llrecentpeople.h
  • indra/newview/llreflectionmap.cpp
  • indra/newview/llreflectionmap.h
  • indra/newview/llreflectionmapmanager.cpp
  • indra/newview/llreflectionmapmanager.h
  • indra/newview/llselectmgr.cpp
  • indra/newview/llspatialpartition.cpp
  • indra/newview/llstartup.cpp
  • indra/newview/lltoolbarview.cpp
  • indra/newview/llviewerdisplay.cpp
  • indra/newview/llviewerinventory.cpp
  • indra/newview/llviewermessage.cpp
  • indra/newview/llviewerobject.cpp
  • indra/newview/llviewerobjectlist.cpp
  • indra/newview/llviewerregion.cpp
  • indra/newview/llviewerstats.cpp
  • indra/newview/llviewertexture.cpp
  • indra/newview/llvisualeffect.cpp
  • indra/newview/llvisualeffect.h
  • indra/newview/llvoavatar.cpp
  • indra/newview/llvoicevisualizer.cpp
  • indra/newview/llvoicevisualizer.h
  • indra/newview/llvovolume.cpp
  • indra/newview/llworldmap.cpp
  • indra/newview/llworldmapview.cpp
  • indra/newview/rlvlocks.cpp
  • indra/newview/rlvlocks.h

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bae39149-b4bf-47ef-a47a-e52508ac116d
📥 Commits

Reviewing files that changed from the base of the PR and between d165e55 and 3e4b866.

📒 Files selected for processing (1)
  • indra/llui/altextview.cpp

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Improved timing consistency across animations, chat and sound history, notifications, refreshes, and other timed features.
    • Corrected timer behavior so stopping preserves elapsed time, restarting begins cleanly, and expiry intervals are measured from when they are set.
    • Improved reflection-probe cleanup and handling of future-dated conversations.
    • Updated fatal-signal handling to log signals when enabled and re-raise them.
    • Capped per-frame geometry updates at 5 ms to limit their impact on frame processing.
    • Corrected time and rendering measurements, including avatar GPU render time and saved-image cleanup.

Walkthrough

The pull request updates timer APIs and behavior, changes call sites to use application uptime or corrected time, and revises reflection-probe cleanup. It also changes fatal-signal handling and several runtime timing and state paths.

Changes

Timing and Runtime Behavior

Layer / File(s) Summary
Timer contracts and behavior
indra/llcommon/lltimer.h, indra/llcommon/llframetimer.*, indra/llcommon/lldeadmantimer.cpp, indra/llcommon/tests/*timer_test.cpp, indra/llcommon/CMakeLists.txt, indra/llcommon/llleap.cpp, indra/llcommon/lllivefile.cpp, indra/llui/altextview.cpp, indra/newview/llaisapi.cpp, indra/newview/llviewerinventory.cpp, indra/newview/llfloateravatarpicker.cpp, indra/newview/alfloaterxuistudio.cpp
Timer APIs distinguish application uptime from timer elapsed time. Frame-timer start, stop, reset, and expiry behavior changes, and resetWithExpiry() is added. Tests are added or expanded. Related call sites use the updated timer operations.
Uptime-based timing call sites
indra/llaudio/llaudioengine.cpp, indra/llui/altextview.cpp, indra/newview/*
Audio, UI, and viewer call sites use uptime for timestamps and time-based behavior, including animation, throttling, refresh intervals, and runtime reporting. The voice visualizer uses the global frame timer instead of its private timer.
Clock and timestamp corrections
indra/llmessage/llcircuit.cpp, indra/llmessage/llexperiencecache.cpp, indra/newview/llenvironment.cpp, indra/newview/llconversationlog.cpp, indra/newview/llgroupmgr.*, indra/newview/llrecentpeople.*, indra/newview/llavatarpropertiesprocessor.*, indra/newview/llviewertexture.cpp, indra/newview/llviewerstats.cpp, indra/newview/fsjointpose.*, indra/newview/llfloaterland.cpp, indra/newview/lllogchat.cpp, indra/newview/llpanelenvironment.cpp, indra/newview/llpanelprofile.cpp, indra/newview/llfloaterbanduration.cpp, indra/newview/llfloaterdisplayname.cpp, indra/newview/lleventnotifier.cpp
Several calculations use corrected time. Selected timestamp storage and comparisons retain greater precision. Other changes adjust ping duration, retry-expiration storage, future conversation timestamps, steady-clock intervals, GPU render-time conversion, and saved-image retention checks.
Reflection-probe detachment and updates
indra/newview/llreflectionmap.*, indra/newview/llreflectionmapmanager.*, indra/newview/llspatialpartition.cpp, indra/newview/llviewerobject.cpp, indra/newview/llviewerregion.cpp, indra/newview/llvovolume.cpp
Reflection probes can be marked orphaned and detached from their owners. The manager rejects orphaned probes as relevant and abandons an in-progress update when a probe is deleted or becomes irrelevant.
Signal handling and runtime paths
indra/llcommon/llapp.cpp, indra/newview/lldirpicker.cpp, indra/newview/llfilepicker.cpp, indra/newview/llstartup.cpp, indra/newview/llviewerdisplay.cpp, indra/newview/llviewerobjectlist.cpp, indra/newview/llinspect.cpp
Fatal-signal handling restores default handlers and re-raises the signal. Other changes update Windows picker frame-time handling, startup and object-list state, inspection timing, and the geometry-update budget.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 3e4b8

The timer and runtime changes align timestamps and expiry behavior with their stated clock contracts. No concrete user-facing failure is established by the supplied current-head evidence, so no identified risk blocks merging.

Architecture Summary

Architecture risk: 🔵 Low · up to 3e4b8

The change affects 1 system.

Changed systems: indra

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — indra (service) was modified; 83 changed files map to changed impact.

Before / after behavior

  • observed — Modified behavior in indra/llaudio/llaudioengine.cpp: logSoundPlay now stores the start time from getUptimeSeconds() instead of getElapsedSeconds().
  • observed — Modified behavior in indra/llaudio/llaudioengine.cpp: logSoundStop now stores the stop time from getUptimeSeconds() instead of getElapsedSeconds().
  • observed — Modified behavior in indra/llcommon/CMakeLists.txt: Adds lltimer to the llcommon test targets.
  • observed — Modified behavior in indra/llcommon/CMakeLists.txt: Reformats the GCC-only workqueue_test.cpp compile-option setting across multiple lines; its condition, file, and option are unchanged.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.46% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 97 functions across 50 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the broad clock and timer fixes, which are the primary change in the pull request.
Description check ✅ Passed The description gives detailed change context and testing results. It does not include the template’s Related Issues section, checklist, or Additional Notes section, but the main description and testi…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks the ticking hand,
And hops where uptime marks the sand.
Probes detach, then timers start,
Clocks keep their place in every part.
The frames move on, the logs align,
A carrot waits at closing time.
“All clear,” says Bun, “the clocks are fine!”

Comment @coderabbitai help to get the list of available commands.

@RyeMutt
RyeMutt force-pushed the rye/clock-fixes branch 2 times, most recently from 3e4b866 to 09a85a9 Compare October 5, 2026 03:36
RyeMutt and others added 20 commits October 4, 2026 23:36
…arts

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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
RyeMutt and others added 13 commits October 4, 2026 23:36
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant