Spinning linksets (llTargetOmega): links drift out of sync / snap back to their unspun pose #418

Open
opened 2026-10-05 09:51:53 +00:00 by sky_line · 0 comments
Owner

A spinning linkset (almost certainly llTargetOmega) gets its links out of sync: individual links occasionally appear to reset to their 0 / unspun position and then spin out of phase with the rest of the linkset. Reported by the owner, 2026-10-05.

How target omega is drawn in Vulkan (code)

llcleaninstancestore.cpp, composeOwnedPose (~70-165), the "GPU TARGET OMEGA" block:

  • LL spins on the CPU every frame (applyAngularVelocity: rot = rot * dQ, accumulated in mAngularVelocityRot). VK instead factors the accumulated spin out of the pose (base = getRotation() * ~getAngularVelocityRot()) and gives the GPU the axis, pivot and phase. The vertex shader applies the spin with phase cam_time.w - t0, where t0 = LLWorldClock::getTime() - spinner->getRotTime().
  • Children of a spinning root use the root's axis, pivot and rot time, composed per child instance.
  • Comment ~170: "The spin phase t0 is only replaced when the axis changes".

LL side, llviewerobject.cpp ~2400-2424 (processUpdateMessage rotation path): when a new rotation arrives from the server, resetRot() zeroes mAngularVelocityRot and the rot time, then setRotation(new_rot * mAngularVelocityRot). When only the angular velocity changes, resetRotTime() (or resetRot() for physical objects).

Likely cause

Each link is a separate instance, re-composed only when that instance is visited (ingest sweep / dirty / mover refresh). A link's spin phase depends on its spinner's getRotTime() and accumulated rotation, which change when the server sends a rotation or omega update (resetRot / resetRotTime):

  1. The phase isn't refreshed when the rot time resets but the axis stays the same. The base pose changes (accumulated spin back to identity), but t0 keeps the old phase. That link jumps (looks like a "reset to 0"), and then it's offset from the others.
  2. Children are refreshed at a different time from the root. When the root gets resetRot, child instances (which use the root's base and rot time) aren't re-composed in the same frame. They keep the old t0 and base until their own sweep visit, so they drift out of phase with the root and each other.
  3. Per-link server updates: a child that receives its own rotation update gets recomposed with the root's current state while siblings keep older state, so the linkset ends up with mixed phases.
  4. Also worth checking: whether isOmegaSpinner flips around the threshold (> 0.00001) for slow spins, switching a link between the CPU and GPU paths.

Fix direction

  • Treat any change in the spinner's getRotTime() or accumulated-rotation reset as a spin-record change, not only an axis change, so t0 and the base are rewritten together.
  • When a spinning root's spin state changes, re-compose all its child instances in the same frame (mark the whole linkset dirty), so every link shares one t0 and base.
  • Optionally store the spin record per linkset (the root's) and have child instances reference it instead of copying it, so they can't drift.
  • Add a debug log: per spinning linkset, how many links have a t0 that differs from the root's.

Repro

  • Rez a linkset with a few child prims, and run llTargetOmega(<0,0,1>, 1.0, 1.0) in the root.
  • Watch for a while; then poke it (edit or move it, call llSetRot from a script, or change the omega periodically).
  • Links should stay rigidly together.
  • Compare with Firestorm (CPU spin) as the reference.
A spinning linkset (almost certainly `llTargetOmega`) gets its **links out of sync**: individual links occasionally appear to **reset to their 0 / unspun position** and then spin out of phase with the rest of the linkset. Reported by the owner, 2026-10-05. ## How target omega is drawn in Vulkan (code) `llcleaninstancestore.cpp`, `composeOwnedPose` (~70-165), the "GPU TARGET OMEGA" block: - LL spins on the CPU every frame (`applyAngularVelocity`: `rot = rot * dQ`, accumulated in `mAngularVelocityRot`). VK instead **factors the accumulated spin out** of the pose (`base = getRotation() * ~getAngularVelocityRot()`) and gives the GPU the axis, pivot and phase. The vertex shader applies the spin with phase `cam_time.w - t0`, where `t0 = LLWorldClock::getTime() - spinner->getRotTime()`. - **Children of a spinning root** use the root's axis, pivot and rot time, composed per child instance. - Comment ~170: "The spin phase t0 is only replaced when the axis changes". LL side, `llviewerobject.cpp` ~2400-2424 (`processUpdateMessage` rotation path): when a new rotation arrives from the server, `resetRot()` zeroes `mAngularVelocityRot` and the rot time, then `setRotation(new_rot * mAngularVelocityRot)`. When only the angular velocity changes, `resetRotTime()` (or `resetRot()` for physical objects). ## Likely cause Each link is a **separate instance**, re-composed only when *that* instance is visited (ingest sweep / dirty / mover refresh). A link's spin phase depends on **its spinner's** `getRotTime()` and accumulated rotation, which change when the server sends a rotation or omega update (`resetRot` / `resetRotTime`): 1. **The phase isn't refreshed when the rot time resets but the axis stays the same.** The base pose changes (accumulated spin back to identity), but `t0` keeps the old phase. That link jumps (looks like a "reset to 0"), and then it's offset from the others. 2. **Children are refreshed at a different time from the root.** When the *root* gets `resetRot`, child instances (which use the root's base and rot time) aren't re-composed in the same frame. They keep the old `t0` and base until their own sweep visit, so they drift out of phase with the root and each other. 3. **Per-link server updates:** a child that receives its own rotation update gets recomposed with the root's current state while siblings keep older state, so the linkset ends up with mixed phases. 4. Also worth checking: whether `isOmegaSpinner` flips around the threshold (`> 0.00001`) for slow spins, switching a link between the CPU and GPU paths. ## Fix direction - Treat **any change in the spinner's `getRotTime()` or accumulated-rotation reset** as a spin-record change, not only an axis change, so `t0` and the base are rewritten together. - When a spinning **root**'s spin state changes, **re-compose all its child instances in the same frame** (mark the whole linkset dirty), so every link shares one `t0` and base. - Optionally store the spin record **per linkset** (the root's) and have child instances reference it instead of copying it, so they can't drift. - Add a debug log: per spinning linkset, how many links have a `t0` that differs from the root's. ## Repro - Rez a linkset with a few child prims, and run `llTargetOmega(<0,0,1>, 1.0, 1.0)` in the root. - Watch for a while; then poke it (edit or move it, call llSetRot from a script, or change the omega periodically). - Links should stay rigidly together. - Compare with Firestorm (CPU spin) as the reference.
sky_line added this to the Slipstream project 2026-10-06 15:04:13 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sky_line/Slipstream#418
No description provided.