Skip to content

Local Assets: client-side preview of local mesh, animation, texture, and material files#296

Merged
RyeMutt merged 58 commits into
developfrom
rye/local-mesh-preview
Jun 8, 2026
Merged

Local Assets: client-side preview of local mesh, animation, texture, and material files#296
RyeMutt merged 58 commits into
developfrom
rye/local-mesh-preview

Conversation

@RyeMutt

@RyeMutt RyeMutt commented Jun 7, 2026

Copy link
Copy Markdown
Member

Description

Adds a Local Assets system: load mesh (.dae / .gltf / .glb), animation
(.bvh / .anim), texture, and glTF material files straight from disk and preview them
live in-world — no upload, no asset-server round-trip. It's built for content creators
iterating on assets: edit the source file on disk and the in-world preview hot-reloads in
place, so you can see exactly how something will look before spending upload credits.

Everything is exposed through a single unified Local Assets floater with
Mesh / Animations / Textures / Materials / Spawned tabs (Add / Unload / Remove,
OS drag-and-drop, empty-state hints, tooltips, and per-account persistence of the
working set).

Capabilities

  • Mesh preview — load DAE/glTF/GLB from disk and spawn in-world as a faithful linkset
    that matches the upload result (multi-prim split for >8-face / multi-mesh files).
    Selectable/movable with full Build-floater integration. Live reload: save the source
    file and the preview hot-swaps in place, preserving your face edits.
  • Materials — a mesh's imported materials are routed through the existing local asset
    managers by source type: Blinn-Phong (.dae) diffuse → LLLocalBitmapMgr; glTF PBR
    (.gltf/.glb) → LLLocalGLTFMaterialMgr, applied per face as a render material.
    Content-identical materials are de-duplicated at import (a single-material atlas export no
    longer spawns a dialog per face). You can also apply any loaded local texture/material to
    the selected face(s).
  • Rigged meshes — wear/attach a rigged preview on your own avatar through the normal
    Attach menu (with attach-point choice). Skin/geometry are kept asset-faithful for a 1:1
    preview, without deforming the agent avatar or baking joint-position overrides.
  • Animesh — load and play a local animation on an animesh preview.
  • Upload — per-tab Upload buttons run the local file through the normal upload flow when
    you're ready to commit it.

Related Issues

  • No separate tracking issue — maintainer-driven feature (design/cadence tracked
    internally).

Issue Link: n/a


Checklist

  • I have provided a clear title and detailed description for this pull request.
  • If useful, I have included media such as screenshots and video to show off my changes.
  • I have tested the changes locally and verified they work as intended.
  • All new and existing tests pass.
  • Code follows the project's style guidelines.
  • Documentation has been updated if needed.
  • Any dependent changes have been merged and published in downstream modules.
  • I have reviewed the contributing guidelines.

Additional Notes

Architectural spine — client-only objects. Previews are flagged client-only on
LLViewerObject; every path that would talk to the simulator (TE updates, media-on-surface,
material apply/edit, etc.) is gated behind isLocalOnly() so a preview can never send sim
traffic. Most of the "gate … for client-only objects" commits enforce that single invariant;
Build-floater fields that would emit sim updates are disabled for previews. Reviewers should
focus here — it's the safety boundary the whole feature rests on.

Mesh-owned managed assets. Textures/materials a mesh imports are tagged mesh-owned so
they don't clutter the Textures/Materials tabs, aren't persisted, and are released when the
mesh reloads or unloads.

Lifecycle. Previews are torn down on region teardown and on viewer shutdown; Delete
removes the whole preview linkset; rigged previews detach cleanly on despawn.

Recent correctness fixes (top of branch).

  • Skewed normals on static rezzed previews — LLVolumeFace::mNormalizedScale now round-trips
    the actual normalization used at ingest.
  • An O(V²) skin-weight lookup that froze both the stock mesh uploader and local preview on
    load → replaced with a spatial-hash LLModel::JointWeightCache (benefits the upload path
    too, not just previews).
  • glTF material rendering on only one face on first load — render-material extra params are now
    marked in-use before the per-face apply loop, so every face persists through the next
    rebuild.
  • PNG 16→8-bit import now uses accurate quantization (scale_16) instead of bit-stripping.

Testing. Runtime-verified by the maintainer per change (the team's build → verify-in-viewer
→ commit cadence). No automated tests were added for this UI/preview feature.

Scope. 47 commits, ~54 files, +6688/−105 vs develop (branch is 5 commits behind develop
at time of opening). Two non-feature commits ride along on the branch base (#5786 uninitialized
override-matrix fix and a GLTF bind-matrix remapping fix); the rest is the Local Assets feature.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1889d06f-e913-4ffa-9a62-021434d83ed0

📥 Commits

Reviewing files that changed from the base of the PR and between e400efb and 0e47a38.

📒 Files selected for processing (1)
  • indra/newview/llviewershadermgr.cpp
💤 Files with no reviewable changes (1)
  • indra/newview/llviewershadermgr.cpp

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Local Assets floater: manage/preview local meshes, animations, textures, materials with tabs, drag‑drop, live reload, spawn/attach, apply/upload and context menus.
    • Client-side local previews: mesh, animation, texture/material previewing with playback, hot-swap, attach/detach and read-only “model-owned” handling.
    • UI/menu integration and picker refreshes for local assets.
  • Bug Fixes

    • Suppressed spurious XML parse error logs for tiny LLSD documents.
    • Corrected PNG 16→8-bit scaling behavior.
    • Faster/more accurate skin-weight lookups and preview weight summaries.
    • Use transparent.j2c for console background.
  • Tests

    • Added test to ensure no unexpected XML parse error log for single-line empty LLSD documents.

Walkthrough

Adds a Local Assets preview subsystem (meshes, animations, textures, materials) with per-account persistence, floater UI, async decode/spawn/hot-swap for meshes, local animation playback with live reload, local-only object handling across selection/transport/inspector, build wiring, and small parser/image/model fixes.

Changes

Local Assets Feature

Layer / File(s) Summary
Parser, image, and model infrastructure updates
indra/llcommon/*, indra/llimage/llpngwrapper.cpp, indra/llprimitive/*, indra/llui/llconsole.cpp
Gates an XML parse-start error log behind graceful-stop state and adds a test to avoid spurious logs; uses accurate 16→8 scaling when supported; adds LLModel JointWeightCache and uses it during weight emission; switches console background image id.
Build wiring and contracts
indra/newview/CMakeLists.txt, indra/newview/lllocalanim.h, indra/newview/lllocalmesh.h, indra/newview/lllocalassetpaths.h
Adds new viewer build entries and header contracts for local mesh, anim, and persisted-path managers and their timers/interfaces.
Local mesh core implementation
indra/newview/lllocalmesh.*, indra/newview/llmeshrepository.cpp, indra/newview/llmodelpreview.cpp
Async local mesh parsing, per-part LLLocalMesh/LLLocalMeshMgr lifecycle, spawn/attach/hot-swap/respawn, live-reload polling, and mesh-repo fast paths for local meshes; model-preview uses JointWeightCache.
Local animation core implementation
indra/newview/lllocalanim.*
Local anim decode/load, unit lifecycle, avatar play/stop, scroll-list feed, and heartbeat-driven live-reload + reapply.
Persistence and asset-manager extensions
indra/newview/lllocalassetpaths.*, indra/newview/lllocalbitmaps.*, indra/newview/lllocalgltfmaterials.*
Per-account local asset path persistence, mesh-joints flag, mesh-owned tracking, deduplication, aliasing, and units-changed callbacks and query APIs.
Floater UI and apply/picker integration
indra/newview/llfloaterlocalassets.*, indra/newview/lltexturectrl.*, indra/newview/llgltfmateriallist.cpp, indra/newview/llfloaterobjectweights.cpp
Implements Local Assets floater tabs (Spawned/Mesh/Anim/Textures/Materials), apply-to-face helpers, picker refresh hooks, material-queue bypasses for local-only objects, and weights UI adjustments for local-only roots.
Menus, resources, and registration
indra/newview/skins/*, indra/newview/app_settings/commands.xml, indra/newview/llviewerfloaterreg.cpp, indra/newview/llviewermenu.*
Adds floater/menu XML, icon/strings, command registration, menu actions for loading local mesh and local anim playback, and floater registration.
Local-only runtime and selection integration
many indra/newview/* files (object, selection, panels, attachments, RLV, media, lists)
Adds LViewerObject isLocalOnly flag, suppresses simulator-bound messages/updates for local-only previews, synthesizes local selection nodes, prevents mixing previews with sim objects in selections, and updates numerous panels/flows to disable sim-only controls for previews.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I nibbled code and wove a view,

Meshes hop, and animations too,
Local textures shimmer bright,
Floater opens — what a sight! ✨

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
indra/newview/llselectmgr.cpp (2)

1028-1070: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Whole-linkset deselect still emits ObjectDeselect for local previews.

The new guards cover deselectObjectOnly() and deselectAll(), but deselectObjectAndFamily() still marshals _PREHASH_ObjectDeselect whenever send_to_sim is true. Any root/linkset deselect path for a local preview will still send fake local IDs to the simulator.

Suggested fix
-    if (!send_to_sim) return;
+    if (!send_to_sim || isLocalPreviewObject(object)) return;

Also applies to: 1086-1096, 5091-5100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indra/newview/llselectmgr.cpp` around lines 1028 - 1070, The loop still sends
_PREHASH_ObjectDeselect for local-preview linksets when send_to_sim is true;
update the sending logic in the block that marshals ObjectDeselect (the loop
using msg, regionp, select_count and objects[i]->getLocalID()) to skip
building/sending that message for local-preview objects/regions—e.g. check
regionp or the object (via an existing isLocal/
isLocalPreview/isAgentAvatarProxy-style API) and continue without adding
ObjectData or calling sendReliable for those entries; ensure the same guard is
applied in the other similar blocks (lines around 1086-1096 and 5091-5100) so
deselectObjectAndFamily/deselectAll paths do not send fake local IDs to the
simulator.

1262-1276: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Local hover and box-select nodes still stay invalid.

requestObjectPropertiesFamily() now no-ops for local previews, but setHoverObject() and selectHighlightedObjects() still create bare LLSelectNodes and rely on that path or sendSelect() to fill mValid, permissions, name, and description. Local previews selected via hover/rectangle therefore never become fully editable unless they went through addAsIndividual() / addAsFamily().

Suggested fix
             LLSelectNode* nodep = new LLSelectNode(cur_objectp, false);
+            synthesizeLocalPreviewNode(nodep, cur_objectp);
             nodep->selectTE(face, true);
             mHoverObjects->addNodeAtEnd(nodep);
@@
         LLSelectNode* new_nodep = new LLSelectNode(*nodep);
+        synthesizeLocalPreviewNode(new_nodep, objectp);
         mSelectedObjects->addNode(new_nodep);

Also applies to: 1432-1445, 6153-6158

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indra/newview/llselectmgr.cpp` around lines 1262 - 1276, The hover/box-select
code creates bare LLSelectNode instances (in
setHoverObject()/selectHighlightedObjects() where
mHoverObjects->addNodeAtEnd(nodep) is used) but requestObjectPropertiesFamily()
is a no-op for local previews so mValid, permissions, name and description
remain unset; change the hover/rectangle selection paths to initialize nodes the
same way selections do by invoking the existing population routines (e.g. call
addAsIndividual()/addAsFamily() or reuse the logic from
sendSelect()/requestObjectPropertiesFamily() to fill mValid and metadata)
instead of creating bare LLSelectNode objects, ensuring nodes added to
mHoverObjects are fully populated for local previews.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@indra/llprimitive/llmodel.cpp`:
- Around line 1317-1352: The neighbor scan in
LLModel::JointWeightCache::influences currently selects the closest cached entry
within WELD_EPSILON, which changes legacy tie-breaking (getJointInfluences
historically returned the first matching mSkinWeights entry). Modify influences
so that during the 3x3x3 mCells scan you track the number of in-epsilon
candidates (e.g., increment a counter when d < best_dist or when d <=
WELD_EPSILON), and if more than one candidate is found, immediately return
mModel.getJointInfluences(pos); only return the cached weight_list when exactly
one in-epsilon match was found. Ensure you still use the same symbols (CellKey
base = cellKey(pos), mCells, best_dist, best, mModel.getJointInfluences) and
preserve the fallback behavior for zero matches.

In `@indra/newview/gltf/llgltfloader.cpp`:
- Around line 1701-1748: The current computeViewerBindMatrix computes a
world-space delta (bind_delta = gltf_joint_bind * inverse(gltf_joint_node)) so
parent bind/rest differences leak into children; instead compute the joint-local
delta by first ensuring the parent's converted viewer bind/rest is available
(call computeViewerBindMatrix recursively for the parent's gltf index using
converted_bind_matrices cache), compute the parent's GLTF rest and bind
(rotated) and derive the local GLTF delta relative to the parent (e.g.
local_bind_delta = inverse(parent_gltf_bind) * gltf_joint_bind or equivalently
local_bind_delta = gltf_joint_bind * inverse(gltf_parent_node)), then apply that
same local delta to the parent's viewer-converted bind/rest (viewer_parent_bind)
to produce viewer_joint_bind and store it in converted_bind_matrices; use
node_data.mParentIndex (or the joint parent field in JointNodeData),
rotated_bind_matrices, node_data.mGltfRestMatrix and
node_data.mOverrideRestMatrix, and the existing converted_bind_matrices cache to
implement the recursive local conversion.
- Around line 1687-1699: populateJointsFromSkin() currently uses
computeGltfToViewerSkeletonTransform() in the branch where inverseBindMatrices
are missing, which hardcodes coord_system_rotation and ignores mApplyXYRotation;
replace that usage so the generated inverse bind/rest matrices are produced by
calling the existing helper rotateGltfMatrixToViewerSpace(const glm::mat4&
gltf_matrix) instead (it already applies coord_system_rotation and the optional
coord_system_rotationxy when mApplyXYRotation is set), ensuring you update any
call sites in populateJointsFromSkin() that previously used
computeGltfToViewerSkeletonTransform() to call rotateGltfMatrixToViewerSpace()
and remove the hardcoded coord_system_rotation path.

In `@indra/newview/llfloaterlocalassets.cpp`:
- Around line 372-405: onRemoveBtn currently calls
LLLocalAssetPaths::removePath(...) and then
LLLocalGLTFMaterialMgr::delUnit(tracking_id) for each selected entry, but
delUnit(LLUUID) only removes a single material by tracking id which leaves other
material units from the same GLTF filename behind; to fix, when processing an
entry whose path refers to a GLTF (.gltf/.glb) gather all tracking IDs for that
path (use LLLocalGLTFMaterialMgr::getTrackingIDs(path, ids) or equivalent) and
call delUnit(...) for each tracking id (or add and call a new delFilename(path)
API) instead of deleting only the selected id, keeping removePath(), refresh(),
and other logic intact (update LLPanelLocalAssetBase::onRemoveBtn and use
LLLocalGLTFMaterialMgr methods to enumerate and delete all units for the
filename).

In `@indra/newview/llfloaterobjectweights.cpp`:
- Around line 129-140: The posted main-coro callback currently captures the
floater state directly and may touch widgets after the floater is closed or
after a newer refresh; change the capture to store a weak pointer/handle to the
floater (or widget container) and the current transaction id when calling
LLAppViewer::instance()->postToMainCoro, then inside the lambda immediately lock
the weak handle and verify the floater is still valid and that the transaction
id matches the latest known id before updating mSelectedDownloadWeight,
mSelectedPhysicsWeight, mSelectedServerWeight, mSelectedDisplayWeight or calling
toggleWeightsLoadingIndicators(false); if either check fails, early-return
without mutating UI. Ensure the transaction id is updated by refresh() and
compared here so stale responses are ignored.

In `@indra/newview/lllocalanim.cpp`:
- Around line 107-110: The .anim branch currently assigns raw bytes into
out_keyframe without validating them; instead, run the same
deserialize/validation logic used by playOnAvatar() (the path that
constructs/loads a Keyframe/motion used in playOnAvatar() and checked later in
reapplyToAvatar()) before committing to out_keyframe or mAnims, and if
deserialization fails keep the existing payload (do not overwrite
out_keyframe/mAnims); update the code in the .anim handling block (and the
similar code around lines 348-365) to attempt to deserialize/construct the
animation/motion object, only move/assign data on success, and log/return
failure on error so doUpdates()/reapplyToAvatar() never sees a corrupted
payload.
- Around line 87-105: decodeFile() currently allocates and reads the entire file
into data before verifying the file extension and enforcing a maximum size,
which allows unsupported or huge files to cause OOM; fix by first obtaining the
extension (use gDirUtilp->getExtension and LLStringUtil::toLower) and
immediately rejecting unsupported types, then check file_size against a
configured MAX_ANIM_FILE_SIZE constant (and verify infile.size(ec) succeeded and
ec is clear) before allocating the std::vector<U8> or calling infile.read; if
either check fails, log with LL_WARNS("LocalAnim") and return false without
buffering.

In `@indra/newview/lllocalgltfmaterials.cpp`:
- Around line 406-413: The loop is returning an ID that ignores the mesh
ownership guard by calling getUnitID(filename, 0); update the fast-path to
return the matching unit's actual ID by passing the correct mesh ownership flag
(e.g. call getUnitID(filename, mesh_owned) or getUnitID(filename,
unit->isMeshOwned())) so outID refers to the same ownership class as the matched
LLPointer<LLLocalGLTFMaterial> in mMaterialList.

In `@indra/newview/lllocalmesh.cpp`:
- Around line 1065-1072: Deleting a mesh unit currently calls
LLLocalBitmapMgr::delUnit(bid) and LLLocalGLTFMaterialMgr::delUnit(mid) for
every ID in unit->mOwnedBitmaps/mOwnedMaterials unconditionally, which will
remove shared imports used by other loaded units; instead, ensure you only
remove an import when no other unit still references it—either check a shared
reference-count or query the manager for remaining owners before calling delUnit
(or add a manager API like hasOtherOwners(id, unit) or decrementRef(id) that
returns zero); update the deletion loop to consult that check for each bid/mid
and only call delUnit when the import is truly unused.

In `@indra/newview/llmeshrepository.cpp`:
- Around line 4408-4412: The current guard using sys_volume->isMeshAssetLoaded()
prevents subsequent in-place reloads from updating cached faces; modify the
logic in the block around sys_volume, local_volume, isMeshAssetLoaded,
copyVolumeFaces and setMeshAssetLoaded so local meshes are refreshed on reloads
— either remove the boolean gate and unconditionally call
sys_volume->copyVolumeFaces(local_volume) (and then setMeshAssetLoaded(true)),
or augment the guard to compare a reload/revision token (e.g. assetRevision or
lastReloadTimestamp) on sys_volume against the source asset and recopy when they
differ, then update that token alongside setMeshAssetLoaded.
- Around line 4398-4421: The code currently uses getVolumeForWorldID() success
to decide locality, which lets a missing/decoding local preview fall through to
the remote fetch path; instead, first test mesh locality via LLLocalMeshMgr
(guarded by LLLocalMeshMgr::instanceExists()), and if the UUID is known-local
but getVolumeForWorldID(mesh_params.getSculptID()) returns null, do the local
failure path (do NOT call LLPrimitive::getVolumeManager()->refVolume(...) or
start a remote fetch), notify the viewer object via the local-mesh failure
notifier used in this module (e.g., the same vobj notification path used for
local handling), and return new_lod; only when not-local should you continue to
refVolume()/isMeshAssetLoaded()/copyVolumeFaces()/unrefVolume() and the normal
remote-fetch logic.

In `@indra/newview/llpanelcontents.cpp`:
- Around line 135-150: When handling object selection in llpanelcontents.cpp,
the code disables mFilterEditor and mPanelInventoryObject inside the
isLocalOnly() branch but never re-enables them for non-local selections; update
the normal (non-local-only) path to explicitly call
mFilterEditor->setEnabled(true) and mPanelInventoryObject->setEnabled(true) (and
re-enable any other controls you disabled, e.g., "button new script"/"button
permissions" if appropriate) so these UI elements are restored after leaving a
local-only selection; apply the same fix for the other occurrence around the
188-193 region where the filter is disabled.

In `@indra/newview/llselectmgr.cpp`:
- Around line 165-176: synthesizeLocalPreviewNode() currently only snapshots
mSavedTextures so local preview nodes never record GLTF material state; update
synthesizeLocalPreviewNode() to also capture and store the GLTF material IDs and
override materials (populate mSavedGLTFMaterialIds and
mSavedGLTFOverrideMaterials) the same way saveSelectedObjectTextures() does
before calling nodep->saveTextures(texture_ids), so
selectionRevertGLTFMaterials() has data to restore after local material edits.

In `@indra/newview/llstartup.cpp`:
- Around line 1204-1208: The code calls
LLLocalAssetPaths::getInstance()->loadAndWatch() during STATE_LOGIN_CLEANUP but
loadAndWatch() is guarded by mWatching so it never reloads per-account data; add
a reset step before reloading: either implement a new LLLocalAssetPaths method
(e.g. resetForNewAccount or clearAndStopWatching) that clears mPaths,
stops/unregisters any watchers and sets mWatching = false, or expose a small
clearWatching() + clearPaths() pair; then call that reset method from the
STATE_LOGIN_CLEANUP path (before invoking loadAndWatch) so on login retry or
account switch the local_assets.xml is re-read and onUnitsChanged() won’t write
into the previous account’s paths.

In `@indra/newview/lltexturectrl.cpp`:
- Around line 663-670: The refresh callbacks set via mLocalBitmapsChangedConn
and mLocalMaterialsChangedConn call refreshLocalList() which clears UI selection
but leaves the picker's mImageAssetID and Select enabled, leading
commitCallback() to potentially apply a stale PICKER_UNKNOWN ID; before calling
refreshLocalList() capture the current local tracking ID (the ID used for local
selection), then after refreshLocalList() check the refreshed list for that
ID—if present reselect it in the list control, otherwise clear mImageAssetID and
disable the local Select state so commitCallback() cannot use a deleted asset;
update the lambdas registered with
LLLocalBitmapMgr::getInstance()->setUnitsChangedCallback and
LLLocalGLTFMaterialMgr::getInstance()->setUnitsChangedCallback to perform this
capture-and-revalidate around refreshLocalList().

In `@indra/newview/llviewermenu.cpp`:
- Around line 7723-7727: The local-only preview early-return bypasses RLVa
restrictions: ensure the same RLVa policy checks that gate normal attach/detach
are applied before calling LLLocalMeshMgr::getInstance()->attachPreviewToAvatar
for selectedObject->isLocalOnly(); either move the entire local-only branch to
after the existing RLVa guard blocks or replicate the guard logic (the same
checks used around the non-local attach/detach paths) immediately before
invoking attachPreviewToAvatar and before calling setObjectSelection(NULL), and
do the same fix for the analogous block at the other occurrence (the 8019-8022
equivalent).

In `@indra/newview/llviewerobject.cpp`:
- Around line 3659-3660: updateInventory() returns early for local-only objects
but updateMaterialInventory() preemptively inserts the asset UUID into
mPendingInventoryItemsIDs, leaving stale pending IDs that make subsequent
isAssetInInventory() checks incorrectly short-circuit; modify
updateMaterialInventory()/the code path that adds to mPendingInventoryItemsIDs
so it only inserts the asset ID after confirming the object is not local-only
(call isLocalOnly() first) or remove the UUID from mPendingInventoryItemsIDs
immediately when updateInventory() returns early, ensuring
mPendingInventoryItemsIDs is only populated for non-local objects.

---

Outside diff comments:
In `@indra/newview/llselectmgr.cpp`:
- Around line 1028-1070: The loop still sends _PREHASH_ObjectDeselect for
local-preview linksets when send_to_sim is true; update the sending logic in the
block that marshals ObjectDeselect (the loop using msg, regionp, select_count
and objects[i]->getLocalID()) to skip building/sending that message for
local-preview objects/regions—e.g. check regionp or the object (via an existing
isLocal/ isLocalPreview/isAgentAvatarProxy-style API) and continue without
adding ObjectData or calling sendReliable for those entries; ensure the same
guard is applied in the other similar blocks (lines around 1086-1096 and
5091-5100) so deselectObjectAndFamily/deselectAll paths do not send fake local
IDs to the simulator.
- Around line 1262-1276: The hover/box-select code creates bare LLSelectNode
instances (in setHoverObject()/selectHighlightedObjects() where
mHoverObjects->addNodeAtEnd(nodep) is used) but requestObjectPropertiesFamily()
is a no-op for local previews so mValid, permissions, name and description
remain unset; change the hover/rectangle selection paths to initialize nodes the
same way selections do by invoking the existing population routines (e.g. call
addAsIndividual()/addAsFamily() or reuse the logic from
sendSelect()/requestObjectPropertiesFamily() to fill mValid and metadata)
instead of creating bare LLSelectNode objects, ensuring nodes added to
mHoverObjects are fully populated for local previews.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8e69a87b-9e4b-4b5b-a1e0-f2926e1517eb

📥 Commits

Reviewing files that changed from the base of the PR and between 9eb69de and fd158b9.

📒 Files selected for processing (53)
  • indra/llcommon/llsdserialize_xml.cpp
  • indra/llcommon/tests/llsdserialize_test.cpp
  • indra/llimage/llpngwrapper.cpp
  • indra/llprimitive/llmodel.cpp
  • indra/llprimitive/llmodel.h
  • indra/llui/llconsole.cpp
  • indra/newview/CMakeLists.txt
  • indra/newview/gltf/llgltfloader.cpp
  • indra/newview/gltf/llgltfloader.h
  • indra/newview/llfloaterlocalassets.cpp
  • indra/newview/llfloaterlocalassets.h
  • indra/newview/llfloaterobjectweights.cpp
  • indra/newview/llgltfmateriallist.cpp
  • indra/newview/lllocalanim.cpp
  • indra/newview/lllocalanim.h
  • indra/newview/lllocalassetpaths.cpp
  • indra/newview/lllocalassetpaths.h
  • indra/newview/lllocalbitmaps.cpp
  • indra/newview/lllocalbitmaps.h
  • indra/newview/lllocalgltfmaterials.cpp
  • indra/newview/lllocalgltfmaterials.h
  • indra/newview/lllocalmesh.cpp
  • indra/newview/lllocalmesh.h
  • indra/newview/llmeshrepository.cpp
  • indra/newview/llmodelpreview.cpp
  • indra/newview/llpanelcontents.cpp
  • indra/newview/llpanelface.cpp
  • indra/newview/llpanelobject.cpp
  • indra/newview/llpanelpermissions.cpp
  • indra/newview/llpanelvolume.cpp
  • indra/newview/llselectmgr.cpp
  • indra/newview/llstartup.cpp
  • indra/newview/lltexturectrl.cpp
  • indra/newview/lltexturectrl.h
  • indra/newview/llviewerfloaterreg.cpp
  • indra/newview/llviewerjointattachment.cpp
  • indra/newview/llviewermenu.cpp
  • indra/newview/llviewermenu.h
  • indra/newview/llviewerobject.cpp
  • indra/newview/llviewerobject.h
  • indra/newview/llviewerobjectlist.cpp
  • indra/newview/llviewerwindow.cpp
  • indra/newview/llvoavatarself.cpp
  • indra/newview/llvovolume.cpp
  • indra/newview/llvovolume.h
  • indra/newview/rlvlocks.cpp
  • indra/newview/skins/default/xui/en/floater_local_assets.xml
  • indra/newview/skins/default/xui/en/menu_local_mesh.xml
  • indra/newview/skins/default/xui/en/menu_object.xml
  • indra/newview/skins/default/xui/en/menu_viewer.xml
  • indra/newview/skins/default/xui/en/panel_local_asset_list.xml
  • indra/newview/skins/default/xui/en/panel_local_spawned.xml
  • indra/newview/skins/default/xui/en/strings.xml

Comment thread indra/llprimitive/llmodel.cpp
Comment thread indra/newview/gltf/llgltfloader.cpp
Comment thread indra/newview/gltf/llgltfloader.cpp
Comment thread indra/newview/llfloaterlocalassets.cpp
Comment thread indra/newview/llfloaterobjectweights.cpp
Comment thread indra/newview/llselectmgr.cpp
Comment thread indra/newview/llstartup.cpp
Comment thread indra/newview/lltexturectrl.cpp Outdated
Comment thread indra/newview/llviewermenu.cpp Outdated
Comment thread indra/newview/llviewerobject.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
indra/newview/lllocalanim.cpp (1)

376-383: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only advance mLastModified after a successful reload.

Line 376 updates anim.mLastModified before decodeFile() succeeds. If the timer polls while the editor is mid-save, decodeFile() fails, the old bytes are kept, and later polls skip the now-complete file because that same mtime was already consumed. That leaves hot-reload stale until the next edit.

Suggested fix
-        anim.mLastModified = mtime;
-
         std::vector<U8> fresh;
         if (!decodeFile(anim.mFilename, fresh))
         {
-            continue; // keep last good data; retry on the next mtime change
+            continue; // keep last good data; retry this version on the next poll
         }
+        anim.mLastModified = mtime;
         anim.mData = std::move(fresh);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indra/newview/lllocalanim.cpp` around lines 376 - 383, The code updates
anim.mLastModified before attempting decodeFile(anim.mFilename, fresh), which
can cause a failed decode (e.g., mid-save) to leave mLastModified advanced and
prevent retry; change the flow in the reload loop so that you call
decodeFile(...) first, only on success assign anim.mData = std::move(fresh) and
then set anim.mLastModified = mtime (i.e., move the update of anim.mLastModified
to after successful decode and data assignment), keeping existing behavior of
continuing on failure.
♻️ Duplicate comments (1)
indra/newview/lltexturectrl.cpp (1)

1565-1585: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Reselect the row if the asset still exists after refresh.

The logic correctly drops mImageAssetID when the unit is deleted, but it doesn't preserve the selection (and re-highlight the row in mLocalScrollCtrl) when the unit is still valid. After refreshLocalList() rebuilds the list, a valid tracking_id should be located in the new list and reselected so the UI doesn't lose the user's choice unexpectedly.

This partially addresses the past review concern, which you confirmed as a follow-up. The current code drops invalid selections (good), but doesn't reselect valid ones (still missing).

💡 Suggested reselection flow
 void LLFloaterTexturePicker::onLocalAssetsChanged()
 {
-    // The refresh may be triggered by another picker / the Local Assets floater
-    // removing the very local asset this picker has selected. If we're on the Local
-    // tab and the selected row's unit no longer resolves, its world id is dangling --
-    // drop it so a later commit can't apply a deleted local texture/material. Check
-    // before refreshLocalList() rebuilds (and clears) the list.
-    bool drop_selection = false;
+    // Capture the tracking_id before refresh so we can either reselect it or drop it.
+    LLUUID old_tracking_id;
+    bool was_selected = false;
     if (mModeSelector && mModeSelector->getValue().asInteger() == PICKER_LOCAL && mLocalScrollCtrl)
     {
         if (LLScrollListItem* sel = mLocalScrollCtrl->getFirstSelected())
         {
             const LLSD data = sel->getValue();
-            const LLUUID tracking_id = data["id"].asUUID();
-            const LLUUID world_id = (data["type"].asInteger() == LLAssetType::AT_MATERIAL)
-                ? LLLocalGLTFMaterialMgr::getInstance()->getWorldID(tracking_id)
-                : LLLocalBitmapMgr::getInstance()->getWorldID(tracking_id);
-            drop_selection = world_id.isNull();
+            old_tracking_id = data["id"].asUUID();
+            was_selected = true;
         }
     }
 
     refreshLocalList();
 
-    if (drop_selection)
+    // Attempt to reselect the same tracking_id if it still exists after refresh.
+    if (was_selected)
     {
-        mImageAssetID.setNull();
+        bool found = false;
+        std::vector<LLScrollListItem*> all_items = mLocalScrollCtrl->getAllData();
+        for (LLScrollListItem* item : all_items)
+        {
+            if (item && item->getValue()["id"].asUUID() == old_tracking_id)
+            {
+                mLocalScrollCtrl->selectItem(item);
+                found = true;
+                break;
+            }
+        }
+        if (!found)
+        {
+            // Asset was removed; clear the selection and disable Select button.
+            mImageAssetID.setNull();
+        }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indra/newview/lltexturectrl.cpp` around lines 1565 - 1585, Before calling
refreshLocalList(), capture the selected tracking_id (from sel->getValue()["id"]
and computed world_id) and whether drop_selection is true; after
refreshLocalList(), if drop_selection is false search mLocalScrollCtrl's items
for an entry whose value["id"] or world_id matches the previously captured
tracking_id/world_id and programmatically reselect that row (use the existing
scroll list selection API on mLocalScrollCtrl) so the UI remains highlighted and
mImageAssetID remains unchanged; if no matching item is found then clear
mImageAssetID as currently done. Reference: mModeSelector, mLocalScrollCtrl,
refreshLocalList(), tracking_id, world_id, mImageAssetID.
🧹 Nitpick comments (1)
indra/newview/lltexturectrl.cpp (1)

1566-1566: ⚡ Quick win

Use the named constant PICKER_LOCAL instead of the magic number.

The hardcoded 1 is brittle. If the LLPickerSource enum order changes, this breaks silently. Use PICKER_LOCAL (as done elsewhere in this file, e.g., line 1472).

🔧 Suggested fix
-    if (mModeSelector && mModeSelector->getValue().asInteger() == 1 /* Local */ && mLocalScrollCtrl)
+    if (mModeSelector && mModeSelector->getValue().asInteger() == PICKER_LOCAL && mLocalScrollCtrl)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indra/newview/lltexturectrl.cpp` at line 1566, Replace the magic literal 1
used to check the mode with the named enum constant PICKER_LOCAL to avoid
brittle behavior if the LLPickerSource enum order changes; specifically update
the condition that checks mModeSelector->getValue().asInteger() in the block
that also references mLocalScrollCtrl so it compares against PICKER_LOCAL (as
used elsewhere in this file), ensuring consistency with other uses like the one
near line 1472.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@indra/newview/lltexturectrl.cpp`:
- Around line 1570-1574: The code reads LLSD data = sel->getValue() then
directly calls data["id"].asUUID() and data["type"].asInteger(); add defensive
checks to verify data.has("id") and data.has("type") (and that
data["id"].isUUID() / data["type"].isInteger() or at least non-null) before
calling asUUID()/asInteger(), and bail out or use a safe default if checks fail;
use these validated values when computing tracking_id and when deciding between
LLAssetType::AT_MATERIAL to call
LLLocalGLTFMaterialMgr::getInstance()->getWorldID(tracking_id) or
LLLocalBitmapMgr::getInstance()->getWorldID(tracking_id).

In `@indra/newview/llviewermenu.cpp`:
- Around line 7740-7744: The local-only branch drops the mReplace flag so
Replace vs Add semantics are lost; update the code path to forward the user's
choice to the local preview handler by passing mReplace into
LLLocalMeshMgr::getInstance()->attachPreviewToAvatar (or by calling a
replace-specific local method if available) and handle occupied-attachment logic
accordingly so the preview honors mReplace when attaching to an occupied index;
refer to selectedObject, index and mReplace to locate and modify the call and
adjust LLLocalMeshMgr API if needed.

---

Outside diff comments:
In `@indra/newview/lllocalanim.cpp`:
- Around line 376-383: The code updates anim.mLastModified before attempting
decodeFile(anim.mFilename, fresh), which can cause a failed decode (e.g.,
mid-save) to leave mLastModified advanced and prevent retry; change the flow in
the reload loop so that you call decodeFile(...) first, only on success assign
anim.mData = std::move(fresh) and then set anim.mLastModified = mtime (i.e.,
move the update of anim.mLastModified to after successful decode and data
assignment), keeping existing behavior of continuing on failure.

---

Duplicate comments:
In `@indra/newview/lltexturectrl.cpp`:
- Around line 1565-1585: Before calling refreshLocalList(), capture the selected
tracking_id (from sel->getValue()["id"] and computed world_id) and whether
drop_selection is true; after refreshLocalList(), if drop_selection is false
search mLocalScrollCtrl's items for an entry whose value["id"] or world_id
matches the previously captured tracking_id/world_id and programmatically
reselect that row (use the existing scroll list selection API on
mLocalScrollCtrl) so the UI remains highlighted and mImageAssetID remains
unchanged; if no matching item is found then clear mImageAssetID as currently
done. Reference: mModeSelector, mLocalScrollCtrl, refreshLocalList(),
tracking_id, world_id, mImageAssetID.

---

Nitpick comments:
In `@indra/newview/lltexturectrl.cpp`:
- Line 1566: Replace the magic literal 1 used to check the mode with the named
enum constant PICKER_LOCAL to avoid brittle behavior if the LLPickerSource enum
order changes; specifically update the condition that checks
mModeSelector->getValue().asInteger() in the block that also references
mLocalScrollCtrl so it compares against PICKER_LOCAL (as used elsewhere in this
file), ensuring consistency with other uses like the one near line 1472.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b708c4e5-7060-4f90-b437-6a05fa8ae46a

📥 Commits

Reviewing files that changed from the base of the PR and between bc08dcb and 1b91c26.

⛔ Files ignored due to path filters (1)
  • indra/newview/skins/default/textures/toolbar_icons/local_assets.png is excluded by !**/*.png
📒 Files selected for processing (13)
  • indra/newview/app_settings/commands.xml
  • indra/newview/llfloaterlocalassets.cpp
  • indra/newview/lllocalanim.cpp
  • indra/newview/lllocalassetpaths.cpp
  • indra/newview/lllocalassetpaths.h
  • indra/newview/llmeshrepository.cpp
  • indra/newview/llselectmgr.cpp
  • indra/newview/lltexturectrl.cpp
  • indra/newview/lltexturectrl.h
  • indra/newview/llviewermenu.cpp
  • indra/newview/skins/default/textures/textures.xml
  • indra/newview/skins/default/xui/en/menu_viewer.xml
  • indra/newview/skins/default/xui/en/strings.xml
✅ Files skipped from review due to trivial changes (1)
  • indra/newview/skins/default/textures/textures.xml
🚧 Files skipped from review as they are similar to previous changes (6)
  • indra/newview/lltexturectrl.h
  • indra/newview/lllocalassetpaths.h
  • indra/newview/lllocalassetpaths.cpp
  • indra/newview/llmeshrepository.cpp
  • indra/newview/llselectmgr.cpp
  • indra/newview/llfloaterlocalassets.cpp

Comment thread indra/newview/lltexturectrl.cpp Outdated
Comment thread indra/newview/llviewermenu.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@indra/newview/llfloaterobjectweights.cpp`:
- Around line 317-324: The all-local-preview branch incorrectly forces
mSelectedDisplayWeight->setText(zero), hiding client-side computable render
cost; instead, keep toggleWeightsLoadingIndicators(false) and set
mSelectedDisplayWeight from the client-side render-cost computation used
elsewhere (i.e., call the existing routine that computes display/render weight
for the current selection rather than using zero) or skip overriding
mSelectedDisplayWeight so the normal client-side update can populate it; leave
the other weights zeroed as-is and ensure you only replace the single
mSelectedDisplayWeight->setText(zero) line.

In `@indra/newview/llviewerobjectlist.cpp`:
- Around line 1614-1622: The code queues local-only preview objects into
mStalePhysicsFlags and then posts their IDs to GetObjectPhysicsData in
fetchPhisicsFlagsCoro(), causing unresolved simulator lookups for fake UUIDs;
mirror the short-circuit used in updateObjectCost(): when updating physics flags
(in updatePhysicsFlags()) and before building/sending idList in
fetchPhisicsFlagsCoro(), skip any object where object->isLocalOnly() is true
(remove or filter out those IDs from mStalePhysicsFlags/idList) so only
simulator-backed objects are sent to GetObjectPhysicsData.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0d0fc3cf-10e2-4bca-8917-edaccb859413

📥 Commits

Reviewing files that changed from the base of the PR and between b05a259 and b25aac6.

📒 Files selected for processing (7)
  • indra/newview/llfloaterlocalassets.cpp
  • indra/newview/llfloaterobjectweights.cpp
  • indra/newview/lllocalmesh.cpp
  • indra/newview/lllocalmesh.h
  • indra/newview/llviewermenu.cpp
  • indra/newview/llviewerobjectlist.cpp
  • indra/newview/skins/default/xui/en/notifications.xml
🚧 Files skipped from review as they are similar to previous changes (4)
  • indra/newview/lllocalmesh.h
  • indra/newview/llviewermenu.cpp
  • indra/newview/llfloaterlocalassets.cpp
  • indra/newview/lllocalmesh.cpp

Comment thread indra/newview/llfloaterobjectweights.cpp Outdated
Comment thread indra/newview/llviewerobjectlist.cpp
RyeMutt added a commit that referenced this pull request Jun 8, 2026
CodeRabbit on PR #296:

- Object Weights: the all-client-only fallback zeroed every weight, but the display/render weight is computed client-side. Keep it accurate via getSelectedObjectRenderCost(); only the sim-fetched download/physics/server weights are zeroed.

- updatePhysicsFlags(): skip client-only (and null) objects too, mirroring updateObjectCost(), so local-preview fake UUIDs are not POSTed to the GetObjectPhysicsData cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@RyeMutt RyeMutt force-pushed the rye/local-mesh-preview branch from b25aac6 to 4e49d7f Compare June 8, 2026 07:55
RyeMutt added a commit that referenced this pull request Jun 8, 2026
CodeRabbit on PR #296:

- Object Weights: the all-client-only fallback zeroed every weight, but the display/render weight is computed client-side. Keep it accurate via getSelectedObjectRenderCost(); only the sim-fetched download/physics/server weights are zeroed.

- updatePhysicsFlags(): skip client-only (and null) objects too, mirroring updateObjectCost(), so local-preview fake UUIDs are not POSTed to the GetObjectPhysicsData cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@RyeMutt RyeMutt force-pushed the rye/local-mesh-preview branch from 4e49d7f to 7a36891 Compare June 8, 2026 08:01

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@indra/newview/lllocalanim.cpp`:
- Around line 147-150: The current code uses an empty joint_alias_map when
isAgentAvatarValid() is false and then immediately records the BVH unit as
current via loadAnim(), which prevents a later re-decode once avatar aliases
become available; change the logic in the block that assigns joint_alias_map
(and the subsequent loadAnim() commit path) to defer committing the mtime or
mark the unit dirty for a one-shot re-decode when gAgentAvatarp becomes valid:
keep using gAgentAvatarp->getJointAliases() when isAgentAvatarValid() is true,
but when false do not serialize an aliasless decode—either skip recording the
file's current mtime or set a “needs_alias_redecode”/dirty flag on the unit so
loadAnim() will re-run decode once isAgentAvatarValid() becomes true and joint
aliases are available.
- Around line 343-358: doUpdates() currently clears the cached keyframe and
advances mLastModified/mData before calling reapplyToAvatar(), but
reapplyToAvatar() calls av->stopMotion()/av->removeMotion() and may fail during
createMotion()/deserialize(), leaving the preview blank and mLastModified
consumed; change the flow in reapplyToAvatar() (or the caller doUpdates()) so
that the state commit (updating mLastModified/mData and clearing the cached
keyframe) only happens after a successful swap: attempt to
createMotion()/deserialize()/startMotion() first (using the newly loaded bytes),
and only on success update mLastModified/mData/clear cache, otherwise leave
mLastModified unchanged (or restore it) so the next heartbeat will retry; adjust
logic around LLKeyframeMotion* motionp, av->createMotion, motionp->deserialize
and av->startMotion to implement this transactional swap behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 61e03ed1-8c00-4eeb-82e5-93d1335c9e37

📥 Commits

Reviewing files that changed from the base of the PR and between b05a259 and 7a36891.

⛔ Files ignored due to path filters (1)
  • indra/newview/skins/default/textures/toolbar_icons/local_assets.png is excluded by !**/*.png
📒 Files selected for processing (54)
  • indra/llcommon/llsdserialize_xml.cpp
  • indra/llcommon/tests/llsdserialize_test.cpp
  • indra/llimage/llpngwrapper.cpp
  • indra/llprimitive/llmodel.cpp
  • indra/llprimitive/llmodel.h
  • indra/llui/llconsole.cpp
  • indra/newview/CMakeLists.txt
  • indra/newview/app_settings/commands.xml
  • indra/newview/llfloaterlocalassets.cpp
  • indra/newview/llfloaterlocalassets.h
  • indra/newview/llfloaterobjectweights.cpp
  • indra/newview/llgltfmateriallist.cpp
  • indra/newview/lllocalanim.cpp
  • indra/newview/lllocalanim.h
  • indra/newview/lllocalassetpaths.cpp
  • indra/newview/lllocalassetpaths.h
  • indra/newview/lllocalbitmaps.cpp
  • indra/newview/lllocalbitmaps.h
  • indra/newview/lllocalgltfmaterials.cpp
  • indra/newview/lllocalgltfmaterials.h
  • indra/newview/lllocalmesh.cpp
  • indra/newview/lllocalmesh.h
  • indra/newview/llmeshrepository.cpp
  • indra/newview/llmodelpreview.cpp
  • indra/newview/llpanelcontents.cpp
  • indra/newview/llpanelface.cpp
  • indra/newview/llpanelobject.cpp
  • indra/newview/llpanelpermissions.cpp
  • indra/newview/llpanelvolume.cpp
  • indra/newview/llselectmgr.cpp
  • indra/newview/llstartup.cpp
  • indra/newview/lltexturectrl.cpp
  • indra/newview/lltexturectrl.h
  • indra/newview/llviewerfloaterreg.cpp
  • indra/newview/llviewerjointattachment.cpp
  • indra/newview/llviewermenu.cpp
  • indra/newview/llviewermenu.h
  • indra/newview/llviewerobject.cpp
  • indra/newview/llviewerobject.h
  • indra/newview/llviewerobjectlist.cpp
  • indra/newview/llviewerwindow.cpp
  • indra/newview/llvoavatarself.cpp
  • indra/newview/llvovolume.cpp
  • indra/newview/llvovolume.h
  • indra/newview/rlvlocks.cpp
  • indra/newview/skins/default/textures/textures.xml
  • indra/newview/skins/default/xui/en/floater_local_assets.xml
  • indra/newview/skins/default/xui/en/menu_local_mesh.xml
  • indra/newview/skins/default/xui/en/menu_object.xml
  • indra/newview/skins/default/xui/en/menu_viewer.xml
  • indra/newview/skins/default/xui/en/notifications.xml
  • indra/newview/skins/default/xui/en/panel_local_asset_list.xml
  • indra/newview/skins/default/xui/en/panel_local_spawned.xml
  • indra/newview/skins/default/xui/en/strings.xml
✅ Files skipped from review due to trivial changes (3)
  • indra/newview/skins/default/xui/en/floater_local_assets.xml
  • indra/newview/CMakeLists.txt
  • indra/newview/skins/default/textures/textures.xml
🚧 Files skipped from review as they are similar to previous changes (42)
  • indra/newview/llpanelobject.cpp
  • indra/newview/llpanelpermissions.cpp
  • indra/newview/rlvlocks.cpp
  • indra/newview/llfloaterlocalassets.h
  • indra/newview/app_settings/commands.xml
  • indra/llimage/llpngwrapper.cpp
  • indra/newview/llviewerjointattachment.cpp
  • indra/newview/lltexturectrl.h
  • indra/newview/llpanelcontents.cpp
  • indra/newview/skins/default/xui/en/menu_local_mesh.xml
  • indra/newview/llviewermenu.h
  • indra/newview/llvovolume.h
  • indra/newview/skins/default/xui/en/panel_local_spawned.xml
  • indra/llprimitive/llmodel.h
  • indra/newview/llviewerfloaterreg.cpp
  • indra/newview/lllocalassetpaths.h
  • indra/newview/llvovolume.cpp
  • indra/newview/llmodelpreview.cpp
  • indra/newview/skins/default/xui/en/menu_viewer.xml
  • indra/newview/llpanelvolume.cpp
  • indra/newview/skins/default/xui/en/menu_object.xml
  • indra/newview/llpanelface.cpp
  • indra/newview/skins/default/xui/en/panel_local_asset_list.xml
  • indra/newview/llgltfmateriallist.cpp
  • indra/llcommon/tests/llsdserialize_test.cpp
  • indra/newview/skins/default/xui/en/strings.xml
  • indra/newview/llviewerwindow.cpp
  • indra/newview/llstartup.cpp
  • indra/newview/lltexturectrl.cpp
  • indra/newview/lllocalgltfmaterials.cpp
  • indra/newview/lllocalgltfmaterials.h
  • indra/newview/lllocalbitmaps.h
  • indra/newview/llvoavatarself.cpp
  • indra/newview/lllocalanim.h
  • indra/newview/lllocalmesh.h
  • indra/newview/llviewerobject.cpp
  • indra/newview/llviewermenu.cpp
  • indra/newview/llselectmgr.cpp
  • indra/newview/llfloaterlocalassets.cpp
  • indra/newview/lllocalassetpaths.cpp
  • indra/newview/lllocalbitmaps.cpp
  • indra/newview/lllocalmesh.cpp

Comment thread indra/newview/lllocalanim.cpp
Comment thread indra/newview/lllocalanim.cpp
RyeMutt added a commit that referenced this pull request Jun 8, 2026
CodeRabbit on PR #296:

doUpdates(): commit the new mtime only after a successful live reapply. reapplyToAvatar() tears the playing motion down before the replacement deserializes, so a transient failure used to blank the preview with the reload already consumed (never retried). It now returns success and the mtime is left for the next heartbeat to retry on failure.

BVH/login: a .bvh restored before the agent avatar exists has no joint aliases. decodeFile() now flags that case and loadAnim()/doUpdates() leave the mtime uncommitted, so it re-decodes with aliases once the avatar is ready instead of locking in a mismapped decode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RyeMutt and others added 12 commits June 8, 2026 06:42
A "Local Mesh" feature -- the mesh analog of Local Bitmap. Load a Collada
(.dae) or glTF (.gltf/.glb) file from disk using the same loaders as the
mesh upload path, decode it to in-memory geometry (+ skin), assign it a
client-only "world" UUID, and render it in-world without uploading to the
asset server.

- LLLocalMesh / LLLocalMeshMgr (lllocalmesh.h/.cpp): parse a model file on
  the model-loader worker thread and, on the main-thread callback, assemble
  the high-LOD geometry into a single LLVolume (served for every LOD) plus
  an LLMeshSkinInfo if rigged. Units are tracked by tracking ID / world
  UUID. The loader self-deletes (LLModelLoader::loadModelCallback), so only
  the per-load context is freed. The registry is created at
  LLMeshRepository::init().

- LLMeshRepository injection: loadMesh / getActualMeshLOD / hasHeader /
  hasSkinInfo / getSkinInfo short-circuit local world IDs to serve the
  decoded volume/skin instead of issuing an asset fetch (guarded by
  instanceExists() for shutdown safety).

- spawnInWorld: create a client-only LL_PCODE_VOLUME via createObjectViewer,
  mark it as a mesh (PARAMS_SCULPT) with a forced LOD so the repository
  injection serves the geometry with the correct face count, give each face
  a default texture, and register it with the render pipeline. Spawned
  objects are tracked and torn down with their unit. Not selectable yet.

- Develop > Load Local Mesh...: file-pick a model and spawn it in-world
  (temporary trigger ahead of the dedicated UI floater).

- LLVOVolume::setLOD(): force a specific LOD on a client-only volume.

Follow-ups: build-tool selection/manipulation, file textures/materials,
live reload, rigged-to-avatar, animesh, and the UI floater.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…M2c)

The client-only preview can now be selected and moved/rotated/scaled with the
standard build tools, with zero server traffic.

- spawnInWorld marks it selectable (mbCanSelect) and sets full owner
  permission flags (FLAGS_OBJECT_YOU_OWNER/MODIFY/MOVE/COPY/TRANSFER) so
  permYouOwner/permModify/permMove enable manipulation.
- LLLocalMeshMgr::isLocalPreview(obj) identifies spawned preview objects.
- LLSelectMgr suppresses all server I/O for them: ObjectSelect (direct and
  sendSelect), ObjectDeselect (direct and deselectAll/ForStandingUp),
  RequestObjectPropertiesFamily, and MultipleObjectUpdate (transform sends).
  Sends are only skipped when the selection is entirely previews, so mixed
  selections still send for real objects.
- addAsIndividual synthesizes a valid, agent-owned LLSelectNode for previews
  (no sim ObjectProperties reply arrives) so the build floater treats them as
  editable.

The manip tools apply transforms locally during drag as before; with the
commit sends suppressed and no server echo, the local transform sticks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Right-clicking/hovering the preview crashed in LLVolume::genTangents during
raycast: loaded mesh assets are required to carry tangents, but the assembled
volume (copied from the GLTF loader's LLModel, which has none) was flagged
setMeshAssetLoaded(true) without them, so picking dereferenced absent tangents.

Run cacheOptimize(true) after assembling the faces -- the same call
unpackVolumeFaces() makes for a real mesh asset -- to optimize the index
buffer and generate tangents before marking the asset loaded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(M2c fix)

Selecting a preview left the build floater blank. The build-tool select path
(selectObjectAndFamily -> addAsFamily) creates select nodes without the
synthesis I'd added in addAsIndividual, and no sim ObjectProperties reply ever
arrives to set node->mValid, so the floater's panels treated the selection as
invalid and showed nothing.

Add a shared synthesizeLocalPreviewNode() that fills in a valid, agent-owned,
fully-permissive node (as processObjectProperties would) and call it from both
addAsFamily and addAsIndividual. selectObjectAndFamily already calls
dialog_refresh_all() afterward, so the floater repopulates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rect (M2c fix)

Both the DAE and glTF loaders normalize each model into a unit box and record
its size, the same convention the upload path relies on. assembleFromScene then
bakes the per-instance node transforms while merging faces, which puts the merged
result back into authored world space -- for the Khronos Duck that is ~100 units
and well off-origin. The spawned object was placed with a 1x1x1 prim scale, so the
pivot/gizmo sat away from the geometry, the selection box did not match, and the
build-tool size read 1m.

Re-normalize the merged faces into a unit box centred at the origin (mirroring
LLModel::normalizeVolumeFaces: positions, normals, extents, texcoord extents) and
keep the recovered size as the prim scale. The preview now centres on its pivot
and reports its authored dimensions. Rigged meshes are left in skin/bind space --
the skeleton, not the object transform, drives them (handled at rigged-attach).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A 3s LLEventTimer (mirroring LLLocalBitmapTimer) polls each loaded unit's source
file mtime. On a change it kicks the same async re-parse used for the initial
load; the geometry swap happens back in the load callback.

To swap under a stable reference without fighting the volume cache (loadMesh's
injection is guarded on !isMeshAssetLoaded(), so reusing the world id would be a
no-op), the unit mints a fresh world id on a successful reload and re-points its
spawned objects at it -- setVolume sees the changed sculpt id, drops the stale
skin, and re-runs the mesh path against the new geometry. The old volume is
unref'd, so there is no per-reload cache growth. This is the same new-id approach
local bitmaps use (replaceIDs).

Robustness:
- ingestScene() assembles into locals and commits only on success, so a failed
  reload (corrupt/locked file, empty scene) keeps showing the last good mesh.
- the parsed mtime is captured at kickoff and stamped on success; a version that
  fails to parse is remembered and not retried until the file changes again.
- reload preserves the object's transform (position/rotation/scale) -- only the
  mesh data updates, like a local-bitmap texture swap. Face-count changes are
  handled (TEs are re-derived from the new volume).
- the timer runs only while at least one unit is loaded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Loading a rigged mesh deformed the user's own avatar. The model loaders resolve
rig joints via a lookup callback, and the DAE loader writes the model's
joint-position overrides straight onto the returned LLJoint
(lldaeloader.cpp: pJoint->addAttachmentPosOverride(...)). Our callback returned
gAgentAvatarp's live joints, so those overrides landed on the agent.

Fix: resolve joints against a dedicated, never-rendered UI avatar
(CO_FLAG_UI_AVATAR), exactly as LLModelPreview does for the upload floater. The
manager lazily creates one preview avatar and uses it for the joint alias map,
the joint lookup callback, and the glTF rest skeleton; loader override writes now
land there instead of on the agent. The avatar is markDead'd when the manager is
torn down.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The manager holds LLPointers to the client-only objects it rezzes and to the
joint-resolution preview avatar -- both live in gObjectList. Left to singleton
teardown, those references would outlive the viewer object list and could
markDead objects against already-gone regions.

- Add LLLocalMeshMgr::cleanup() (idempotent): markDead + release all spawned
  objects and the preview avatar, and stop the reload timer. The unit destructor
  now routes through it.
- Call it from LLViewerObjectList::killAllObjects() so these objects are released
  while the object list and regions are still valid (markDead is idempotent, so
  the kill loop re-marking them is fine).
- Tear the manager down in LLMeshRepository::shutdown() (deleteSingleton),
  symmetric with its creation in LLMeshRepository::init(); this frees the decoded
  units after killAllObjects has already released the objects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…upload result

A file with more than 8 faces (or multiple mesh nodes) was merged into one volume
and clipped at 8 faces, dropping the rest. An actual upload of the same file
splits it into one mesh per LLModel (<= 8 faces each, the loader already does this
via trimVolumeFacesToSize) and rezzes them as a linkset. Reproduce that exactly.

LLLocalMesh now holds a vector of LLLocalMeshPart -- one per LLModel -- each with
its own world id, normalized geometry (unit box + authored scale, as the upload
path does), and an offset giving its position within the model. ingestScene builds
the parts (no merge, no cap) and computes each part's offset from the combined
model centre.

spawnInWorld creates one client-only prim per part and links them into a single
linkset rooted at the first prim: addChild() sets the parent, and setDrawableParent
parents the drawable (a client-only linkset must do this itself -- server linksets
get it in processUpdateMessage). Each prim is placed at the model centre + its
offset; the whole linkset drops in front of the agent and selects/moves as one,
like a rezzed upload. Live reload now replaces the linkset in place, preserving the
root's position and rotation.

The repository injection resolves per-part world ids: getVolumeForWorldID /
getSkinInfoForWorldID / isLocal walk all units' parts, replacing the old
single-world-id unit lookups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Delete key/menu routes through LLSelectMgr::selectDelete (and selectForceDelete)
which send DeRezObject/ObjectDelete to the sim. Local previews are client-only, so
the sim has nothing to delete and the object just stayed.

Intercept both paths: when the selection is entirely local previews
(selectionAllLocalPreview), despawn them locally instead. LLLocalMeshMgr gains
deletePreviewObject(), which maps a selected prim (linkset root or child) back to
its unit and delUnit()s it -- tearing down the whole linkset and dropping the
loaded unit so a later file save does not respawn it (live reload stops too).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The selection/delete gating asked LLLocalMeshMgr whether an object was a local
preview, which scanned mSpawnedObjects on every check. Replace that with an O(1)
LLViewerObject::isLocalOnly() flag -- a general "client-only object, no simulator
counterpart" property -- set on each preview prim at spawn.

llselectmgr's isLocalPreviewObject() now just reads the flag (no manager call, no
instanceExists), and LLLocalMeshMgr::isLocalPreview() is removed. The manager
still owns mSpawnedObjects for despawn/delete bookkeeping.

This only addresses the object-keyed lookups. The repository injection
(isLocal/getVolumeForWorldID/getSkinInfoForWorldID) is keyed by mesh world id, not
by object, so it stays manager-side; those scans are over the tiny set of loaded
units and can be made O(1) with a world-id map later if it ever matters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RyeMutt and others added 24 commits June 8, 2026 06:42
Every interactive control now has a hint to ease discovery: the shared
panel's add / select / attach combo + button / animation target combo /
play / stop gain tool_tips (unload/remove/upload/joint-positions already
had them), and the repurposed spawn_btn slot sets its tooltip in code
(Rez on Mesh, Apply to Face on Textures/Materials). The Rezzed tab already
had tool_tips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A mesh's imported textures/materials (mesh-owned) were only appended to
mOwnedBitmaps/mOwnedMaterials and never pruned, so editing a mesh to drop a
material left its local texture/material lingering until the mesh was
removed.

ingestScene now rebuilds the owned sets fresh each parse: registerOwnedBitmap
/ importGLTFMaterials record every mesh-owned unit the parse references (via
the new LLLocalBitmapMgr/LLLocalGLTFMaterialMgr isMeshOwned(tracking_id)),
and on commit the units the previous parse used but this one dropped are
delUnit'd. A failed parse drops only the units it newly created, keeping the
last-good geometry.

Guarded against shared imports: LLLocalMeshMgr::isImportOwnedByOther() keeps
a unit alive if a sibling mesh (sharing the same texture/material file) still
references it, so a reload can't delUnit it out from under another mesh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ce is picked

The Textures and GLTF Materials tabs' apply button now applies to every face of
the selected object by default, or only the face(s) chosen with the Select Face
tool. Previously a whole-object selection with no per-face mask could apply to
nothing.

llfloaterlocalassets.cpp: before applying, mark all TEs on any selected object
that has no individual face selection, so the existing applyToTEs /
selectionSetImage / setRenderMaterialID paths cover the whole object. Objects
with a Select-Face pick are left untouched. The button stays disabled unless an
asset row and an in-world object are both selected.

Relabel the button "Apply to Face" -> "Apply to Selected" and update its tooltip
to match (panel_local_asset_list.xml).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sim sends

synthesizeLocalPreviewNode now shows the selected sub-mesh's own name (the
model/instance label, falling back to the file's short name) and the source file
path, instead of the generic "(local mesh preview)". spawnLinkset rezzes one
prim per part, so each child prim resolves to its part by index.

- lllocalmesh: store the sub-mesh name on LLLocalMeshPart at ingest (instance
  label, else model name); add LLLocalMeshMgr::getPreviewDisplay(obj) to resolve
  a preview prim (root or child) to its display name + source file path.
- llselectmgr: sendListToRegions short-circuits when the whole selection is a
  local preview, so a client-only object never emits any sim object message
  (name/description/permissions/sale/group/owner/click-action/category/...).
  Position/TE/inventory send paths were already isLocalOnly-guarded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Client-only local mesh previews have no server-side identity, so the sim-backed
build-floater fields don't apply (and their edits are already short-circuited via
sendListToRegions/sendMultipleUpdate). Disable them when the selected object is
isLocalOnly(); name/description still show (read-only). Position/size/rotation
and texture/material stay editable so previews remain useful.

- llpanelpermissions: name, description, set group, deed, share-with-group,
  allow-everyone move/copy, next-owner modify/copy/transfer, for sale + price +
  sale type, search, click action.
- llpanelcontents: New Script, Permissions, task-inventory panel, filter editor.
- llpanelobject: Physical, Temporary, Phantom.
- llpanelvolume: physics shape type, gravity, friction, density, restitution,
  and the prim Material combo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…views

Client-only local mesh previews have no sim counterpart, so the PBR material
paths must neither round-trip to the simulator nor depend on the sim's override
echo to render.

- llgltfmateriallist: short-circuit queueModify and all queueApply overloads for
  isLocalOnly() objects (the ModifyMaterialParams cap round-trip). Render state is
  applied locally by the callers (setRenderMaterialID is already isLocalOnly-
  guarded and sets up the render-material param block).
- llpanelface: route the GLTF override edits (PBR sliders + planar align) through
  a new apply_gltf_override() that, for local previews, applies the override
  directly via setTEGLTFMaterialOverride (which re-renders) instead of
  queueModify -- so edits show without the sim echo a preview never receives.

Texture picker and Blinn-Phong paths were already safe (guarded sendTEUpdate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-swap

A live reload (source-file change) or joint-position toggle re-runs
applyPartGeometry, which resets every face to the file's imported material --
wiping textures/materials/glow/etc. the user applied to the in-world preview.
Snapshot each prim's per-face render state before the swap and restore it after.

Captured/restored per face: diffuse texture, color, glow, bump/shiny/fullbright,
texture transforms, the Blinn-Phong material (normal/specular), and the glTF
render material id + override. Restored via client-only setters
(update_server=false), so no sim traffic.

Note: this preserves the current face state, so a material change made in the
source file won't override an already-applied face on reload (preserve wins).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reduce 16-bit-per-channel PNGs with png_set_scale_16 (round(v/257)) instead of png_set_strip_16's biased low-byte truncation (v>>8), which darkened images by up to ~1 LSB and discarded low-byte precision. Guarded by PNG_READ_SCALE_16_TO_8_SUPPORTED with strip_16 fallback. Affects all PNG decode paths; no dithering, so normal/data maps are unaffected beyond correct rounding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The static ingest path normalizes geometry twice -- once in the model
loader (which records each face's mNormalizedScale) and again in
ingestScene via transformFace()+normalizeFaces() to bake the instance
transform for in-world placement. The second pass never updated
mNormalizedScale, so a rotated or non-uniformly scaled instance transform
left it inconsistent with the actual unit-box geometry.

cacheOptimize() uses mNormalizedScale to reconstruct the mesh in its
un-normalized frame for tangent generation and then scales back, running
normals through normalize(normalize(n / S_stale) * S_stale). That is not
identity for a non-uniform model (e.g. a tall, narrow body), so rezzed
previews rendered with visibly skewed normals and tangents versus the
uploaded copy.

Set each face's mNormalizedScale to part.mScale (the size that
un-normalizes this part's geometry) before building the volume. Exact
round-trip for the static path; a no-op for rigged, which already matches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…local preview

LLModel::getJointInfluences() linearly scans the model's entire skin-weight
map, and three places call it once per vertex -- writeModel() (upload + the
uploader's load-time rebuildUploadData), LLModelPreview::genBuffers() (preview
vertex buffers), and the local-mesh preview's populateFaceWeights(). That makes
the rigged weight pass O(V^2): a dense body (~40k verts) is ~1.6e9 distance
checks on the main thread, freezing the viewer for seconds on load even though
parsing already runs on a background thread.

Add LLModel::JointWeightCache: a spatial hash over mSkinWeights (cell size and
match radius = the existing 1e-5 weld epsilon), built once before a per-vertex
loop, giving O(1) lookups -> O(V) overall. A position with no key within epsilon
falls back to getJointInfluences() (preserving its exact-find / closest-point
path), so results are byte-identical -- safe for the upload serialization path.

Wired into all three call sites; the local preview's earlier ad-hoc WeightLocator
is replaced by the shared class.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A glTF/glb mesh imported into a local preview only showed its material on a
single face until the source was manually re-applied or reloaded. Two issues:

1. Apply order. applyPartGeometry() set each face's render material in a loop
   but only marked the render-material extra param in use *afterward*.
   LLViewerObject::setRenderMaterialID() only persists an id into the param
   block when getRenderMaterialParams() returns it, and that returns null until
   the param is in use -- so each call created a throwaway block and only the
   last face's id survived. The next rebuild's updateTEMaterialTextures() then
   cleared every TE whose param-block id was null, blanking all but one face.
   Fix: mark the param in use before the per-face apply so all ids accumulate.
   Also size the TEs from the part's decoded face count (part.mNumFaces) rather
   than the not-yet-realized viewer volume, so every face is covered at spawn.

2. Duplicate materials. Exporters routinely split one atlas-textured material
   into one material per face (distinct names, distinct texture objects pointing
   at the same image). These now deduplicate at import by content signature
   (factors + per-slot source image), so such a mesh imports as a single local
   material instead of N -- no Materials-tab clutter, no repeated load/verify
   dialogs, and one shared texture. Each original face-binding name is kept as an
   alias so faces still resolve to the surviving unit; getTrackingIDs() makes the
   mesh-owned tracking safe across the index gaps dedup leaves behind.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The never-rendered preview avatar exists only to resolve joints and absorb a model's joint-position overrides, but it was running ANIM_AGENT_STAND, which poses the arms. A loader that reconstructs inverse binds from the joints' world matrices then captures that posed skeleton instead of the canonical bind pose, skewing the arm binds. Add a run_stand_anim flag (default off) so the skeleton stays at rest unless a caller actually renders the avatar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LLPanelLocalAssetBase and LLPanelLocalSpawned override LLView::refresh()
but didn't mark it override, while their sibling postBuild()/draw() do.
MSVC accepts this; Clang fails it under -Werror,-Winconsistent-missing-override,
breaking the macOS and Linux CI builds. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Targeted fixes from the PR review pass:

- llmodel JointWeightCache::influences: defer to getJointInfluences() unless
  there's exactly one in-epsilon match, so the cache reproduces the legacy
  "first weld-epsilon match" tie-break instead of picking the closest.
- lllocalanim decodeFile: reject unsupported extensions and cap file size
  BEFORE buffering, so a wrong/huge pick can't allocate unbounded memory.
- lllocalgltfmaterials addUnitInternal: return the matched unit's tracking id
  (not getUnitID(file,0), which ignores user-vs-mesh-owned).
- lllocalmesh delUnit: only release mesh-owned imports no other loaded mesh
  still references (imports are deduped by file and shared across units).
- llpanelcontents getState: re-enable the contents filter on the normal path
  so it isn't left disabled after a local-only selection.
- llviewerobject updateMaterialInventory: early-out for local-only objects so
  the asset isn't queued in mPendingInventoryItemsIDs and left stuck pending.
- llfloaterobjectweights onWeightsUpdate: guard the posted main-coro callback
  with a floater handle to avoid touching widgets after the floater closes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-ups from the review pass, runtime-verified:

- llmeshrepository: decide local-mesh locality by registry membership
  (LLLocalMeshMgr::isLocal), not decoded-volume presence -- a local-only id
  never falls through to the simulator fetch, even mid hot-reload; recopy the
  faces unconditionally so a reload isn't pinned to stale cached geometry.
- llselectmgr: snapshot GLTF render-material ids + overrides for local previews
  so a material-picker cancel can revert; guard deselectObjectAndFamily against
  sending fake local ids; populate local-preview select nodes on hover/box-select.
- lllocalanim: validate .anim bytes deserialize before accepting them, so a bad
  hot-reload keeps the last good motion instead of blanking the preview.
- lllocalassetpaths: re-read the per-account saved set on every login so a
  relog / account switch doesn't carry the previous account's paths.
- lltexturectrl: drop a local picker selection whose asset was removed elsewhere.
- llviewermenu: apply RLVa attach/detach locks to local-preview attach/detach.
- llfloaterlocalassets: removing a multi-material glTF row now unloads all of
  the file's material units, not just the selected one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- menu_viewer: move "Local Assets..." to the top of the Build menu with a
  separator beneath it (previously buried in the Me menu).
- commands.xml: add a "local_assets" command (toybox-available) that toggles
  the floater, so it can be dragged onto a toolbar.
- textures.xml + toolbar_icons/local_assets.png: an 18x18 folder icon for it.
- strings.xml: Command_LocalAssets_Label / Command_LocalAssets_Tooltip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- lltexturectrl onLocalAssetsChanged: use the PICKER_LOCAL constant, guard the
  scroll row's LLSD (id/type) before access, and reselect the row when its unit
  survives the refresh -- only drop the picker selection when the unit was
  actually removed elsewhere.
- lllocalanim doUpdates: advance mLastModified only after a successful decode, so
  a poll that catches a file mid-save retries it instead of skipping the
  now-complete file (completes the keep-last-good-on-failure behavior).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The attach menu serves both the Replace and Add actions, but the local-only
preview branch dropped the choice. Thread it through: attachPreviewToAvatar()
takes a `replace` flag, and when set it first detaches any OTHER local preview
already worn on the target attachment point (client-only and per-point -- it
never touches the user's real attachments) before attaching the new one.
llviewermenu passes mReplace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Static (non-rigged) previews baked the instance/scene transform into the
geometry, then re-normalized -- which skews normals and generates tangents in
the wrong frame for a non-uniform or sheared node transform, so a rezzed preview
didn't match the uploaded copy. (The uploader keeps the mesh model-local and
applies the instance transform as the prim's transform via decomposeMeshMatrix.)

Mirror the uploader: keep the loader's model-local geometry for static units too
(so cacheOptimize reconstructs the authored frame for tangent generation exactly
as the download path does -> normals AND tangents match a real upload), and
decompose each instance transform into the preview object's scale / rotation /
position (dropping shear like decomposeMeshMatrix). Spawn applies the per-part
rotation + position; the re-rez path saves the user's rotation delta so the
intrinsic rotation isn't double-applied. Removes the now-dead bake/normalize
helpers and the model-centre math. Rigged previews are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes in the Apply-to-Selected path for local previews:

1. Apply hit only the first face(s) on the first click, then all faces on the second. applyToTEs()/selectionSetImage() clamp to llmin(getNumTEs, getNumFaces), and a freshly (re)built local-mesh volume drawable face count lags its TE count by a frame. Iterate getNumTEs() directly (like LLToolDragAndDrop::dropTextureAllFaces), honoring an explicit Select-Face subset.

2. A GLTF material applied to a multi-face preview stuck only on the last face. setRenderMaterialID() creates a throwaway render-material param block (in_use=false) until the block is in use, so each per-face call replaced the previous one. Mark the param in use before the per-face loop for client-only objects, mirroring applyPartGeometry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A mesh exported without UV coordinates cannot be textured: the loaders zero-fill the missing texcoords, so the diffuse/material maps and the Select-Face overlay sample a single texel and the surface renders white. Detect a UV-less unit during ingestScene and warn once on initial load (not on reload, to avoid spam during live editing) with a non-intrusive notifytip, instead of leaving a silent white preview.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Client-only preview objects have no sim counterpart, so costing their fake UUIDs never resolves and re-fires on every read. LLViewerObjectList::updateObjectCost() now skips client-only (and null) objects, covering getObjectCost/getLinksetCost/getPhysicsCost/getLinksetPhysicsCost and the getSelected* aggregators -- they return the 0 default, the correct land impact for something not rezzed on the region. The Object Weights floater skips client-only roots in the ResourceCostSelected fetch and shows zero weights for an all-local selection instead of a perpetual loading spinner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CodeRabbit on PR #296:

- Object Weights: the all-client-only fallback zeroed every weight, but the display/render weight is computed client-side. Keep it accurate via getSelectedObjectRenderCost(); only the sim-fetched download/physics/server weights are zeroed.

- updatePhysicsFlags(): skip client-only (and null) objects too, mirroring updateObjectCost(), so local-preview fake UUIDs are not POSTed to the GetObjectPhysicsData cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CodeRabbit on PR #296:

doUpdates(): commit the new mtime only after a successful live reapply. reapplyToAvatar() tears the playing motion down before the replacement deserializes, so a transient failure used to blank the preview with the reload already consumed (never retried). It now returns success and the mtime is left for the next heartbeat to retry on failure.

BVH/login: a .bvh restored before the agent avatar exists has no joint aliases. decodeFile() now flags that case and loadAnim()/doUpdates() leave the mtime uncommitted, so it re-decodes with aliases once the avatar is ready instead of locking in a mismapped decode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@RyeMutt RyeMutt force-pushed the rye/local-mesh-preview branch from 9274d61 to e400efb Compare June 8, 2026 10:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
indra/newview/llfloaterlocalassets.cpp (1)

403-423: ⚡ Quick win

Deduplicate remove work by backing path.

Selecting multiple rows from the same .gltf/.glb file unloads every unit on the first iteration, then later iterations fall back to entry.second and call delUnit() again on IDs that were already removed. Collapsing selected to one operation per entry.first keeps remove semantics one-shot per file and avoids redundant manager mutations/signals.

Proposed change
-    for (const auto& entry : selected)
+    std::set<std::string> handled_paths;
+    std::set<LLUUID> handled_ids;
+    for (const auto& entry : selected)
     {
+        if (!entry.first.empty() && !handled_paths.insert(entry.first).second)
+        {
+            continue;
+        }
         if (!entry.first.empty())
         {
             LLLocalAssetPaths::getInstance()->removePath(assetType(), entry.first); // forget the path
         }
         // Unload every decoded unit backing this row. For a multi-material glTF file
@@
-        if (ids.empty() && entry.second.notNull())
+        if (ids.empty() && entry.second.notNull() && handled_ids.insert(entry.second).second)
         {
             ids.push_back(entry.second);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indra/newview/llfloaterlocalassets.cpp` around lines 403 - 423, The loop over
selected rows calls removePath(unitsForPath(...)) and delUnit(...) repeatedly
for the same backing path/IDs when multiple rows point to the same .gltf/.glb;
collapse work by tracking already-processed backing paths (entry.first) so you
only call LLLocalAssetPaths::getInstance()->removePath(assetType(), path) and
resolve unitsForPath(path, ids) once per unique path, and ensure delUnit(id) is
invoked only once per decoded unit (e.g., skip paths already processed or
deduplicate IDs before calling delUnit); update the loop that iterates selected
and the logic around entry.first, entry.second, unitsForPath, removePath and
delUnit accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@indra/newview/llfloaterlocalassets.cpp`:
- Around line 403-423: The loop over selected rows calls
removePath(unitsForPath(...)) and delUnit(...) repeatedly for the same backing
path/IDs when multiple rows point to the same .gltf/.glb; collapse work by
tracking already-processed backing paths (entry.first) so you only call
LLLocalAssetPaths::getInstance()->removePath(assetType(), path) and resolve
unitsForPath(path, ids) once per unique path, and ensure delUnit(id) is invoked
only once per decoded unit (e.g., skip paths already processed or deduplicate
IDs before calling delUnit); update the loop that iterates selected and the
logic around entry.first, entry.second, unitsForPath, removePath and delUnit
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 509a2abf-66bc-47b1-b617-f411b4a8aed4

📥 Commits

Reviewing files that changed from the base of the PR and between 9274d61 and e400efb.

⛔ Files ignored due to path filters (1)
  • indra/newview/skins/default/textures/toolbar_icons/local_assets.png is excluded by !**/*.png
📒 Files selected for processing (54)
  • indra/llcommon/llsdserialize_xml.cpp
  • indra/llcommon/tests/llsdserialize_test.cpp
  • indra/llimage/llpngwrapper.cpp
  • indra/llprimitive/llmodel.cpp
  • indra/llprimitive/llmodel.h
  • indra/llui/llconsole.cpp
  • indra/newview/CMakeLists.txt
  • indra/newview/app_settings/commands.xml
  • indra/newview/llfloaterlocalassets.cpp
  • indra/newview/llfloaterlocalassets.h
  • indra/newview/llfloaterobjectweights.cpp
  • indra/newview/llgltfmateriallist.cpp
  • indra/newview/lllocalanim.cpp
  • indra/newview/lllocalanim.h
  • indra/newview/lllocalassetpaths.cpp
  • indra/newview/lllocalassetpaths.h
  • indra/newview/lllocalbitmaps.cpp
  • indra/newview/lllocalbitmaps.h
  • indra/newview/lllocalgltfmaterials.cpp
  • indra/newview/lllocalgltfmaterials.h
  • indra/newview/lllocalmesh.cpp
  • indra/newview/lllocalmesh.h
  • indra/newview/llmeshrepository.cpp
  • indra/newview/llmodelpreview.cpp
  • indra/newview/llpanelcontents.cpp
  • indra/newview/llpanelface.cpp
  • indra/newview/llpanelobject.cpp
  • indra/newview/llpanelpermissions.cpp
  • indra/newview/llpanelvolume.cpp
  • indra/newview/llselectmgr.cpp
  • indra/newview/llstartup.cpp
  • indra/newview/lltexturectrl.cpp
  • indra/newview/lltexturectrl.h
  • indra/newview/llviewerfloaterreg.cpp
  • indra/newview/llviewerjointattachment.cpp
  • indra/newview/llviewermenu.cpp
  • indra/newview/llviewermenu.h
  • indra/newview/llviewerobject.cpp
  • indra/newview/llviewerobject.h
  • indra/newview/llviewerobjectlist.cpp
  • indra/newview/llviewerwindow.cpp
  • indra/newview/llvoavatarself.cpp
  • indra/newview/llvovolume.cpp
  • indra/newview/llvovolume.h
  • indra/newview/rlvlocks.cpp
  • indra/newview/skins/default/textures/textures.xml
  • indra/newview/skins/default/xui/en/floater_local_assets.xml
  • indra/newview/skins/default/xui/en/menu_local_mesh.xml
  • indra/newview/skins/default/xui/en/menu_object.xml
  • indra/newview/skins/default/xui/en/menu_viewer.xml
  • indra/newview/skins/default/xui/en/notifications.xml
  • indra/newview/skins/default/xui/en/panel_local_asset_list.xml
  • indra/newview/skins/default/xui/en/panel_local_spawned.xml
  • indra/newview/skins/default/xui/en/strings.xml
✅ Files skipped from review due to trivial changes (4)
  • indra/newview/skins/default/textures/textures.xml
  • indra/newview/CMakeLists.txt
  • indra/newview/skins/default/xui/en/strings.xml
  • indra/newview/llstartup.cpp
🚧 Files skipped from review as they are similar to previous changes (49)
  • indra/llui/llconsole.cpp
  • indra/newview/app_settings/commands.xml
  • indra/newview/skins/default/xui/en/panel_local_spawned.xml
  • indra/newview/llpanelvolume.cpp
  • indra/newview/llvovolume.h
  • indra/newview/skins/default/xui/en/menu_object.xml
  • indra/newview/llviewerfloaterreg.cpp
  • indra/newview/llviewermenu.h
  • indra/newview/llfloaterlocalassets.h
  • indra/newview/llmodelpreview.cpp
  • indra/newview/rlvlocks.cpp
  • indra/llprimitive/llmodel.h
  • indra/newview/skins/default/xui/en/menu_local_mesh.xml
  • indra/newview/skins/default/xui/en/menu_viewer.xml
  • indra/newview/llvovolume.cpp
  • indra/newview/skins/default/xui/en/floater_local_assets.xml
  • indra/newview/lllocalassetpaths.h
  • indra/newview/llpanelcontents.cpp
  • indra/newview/skins/default/xui/en/notifications.xml
  • indra/newview/llviewerobject.h
  • indra/newview/llfloaterobjectweights.cpp
  • indra/llimage/llpngwrapper.cpp
  • indra/newview/skins/default/xui/en/panel_local_asset_list.xml
  • indra/newview/llviewerobjectlist.cpp
  • indra/newview/llpanelface.cpp
  • indra/newview/llgltfmateriallist.cpp
  • indra/llcommon/llsdserialize_xml.cpp
  • indra/newview/lltexturectrl.cpp
  • indra/newview/lllocalassetpaths.cpp
  • indra/newview/llvoavatarself.cpp
  • indra/newview/llviewerwindow.cpp
  • indra/newview/llpanelobject.cpp
  • indra/newview/llpanelpermissions.cpp
  • indra/llcommon/tests/llsdserialize_test.cpp
  • indra/newview/llviewerjointattachment.cpp
  • indra/newview/lllocalanim.h
  • indra/newview/llviewerobject.cpp
  • indra/newview/lllocalgltfmaterials.h
  • indra/newview/lltexturectrl.h
  • indra/newview/lllocalmesh.h
  • indra/newview/lllocalgltfmaterials.cpp
  • indra/newview/llviewermenu.cpp
  • indra/newview/llmeshrepository.cpp
  • indra/llprimitive/llmodel.cpp
  • indra/newview/lllocalbitmaps.cpp
  • indra/newview/lllocalbitmaps.h
  • indra/newview/llselectmgr.cpp
  • indra/newview/lllocalanim.cpp
  • indra/newview/lllocalmesh.cpp

@RyeMutt RyeMutt merged commit ef5bc5d into develop Jun 8, 2026
17 checks passed
@RyeMutt RyeMutt deleted the rye/local-mesh-preview branch June 8, 2026 22:42
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