From ca9043a2678e2089ac92b6c43cfe964f76871757 Mon Sep 17 00:00:00 2001 From: Zanibar <91260002+taylnos@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:38:54 -0500 Subject: [PATCH 01/10] PBR faces draw the local bake while the avatar edits its appearance A PBR material slot naming a bake proxy (IMG_USE_BAKED_*) drew white in edit appearance mode, and normal, ORM and emissive slots fell to their defaults the same way. LLVOAvatar::getBakedTexture returns the local composite there, an LLViewerTexLayerSetBuffer, which is a dynamic texture. LLFetchedGLTFMaterial's slots hold only fetched textures, so updateTEMaterialTextures cast it away with a dynamic_cast to LLViewerFetchedTexture, and both bind paths drew the empty slot with their stand-in. Leaving the mode put the server bake, a fetched texture, back. The cast came from LL in 1a437cbedf9 and the slot type in de4c018499d. LLFetchedGLTFMaterial::mBakedTextures now holds what each bake slot resolved to, of any texture type, and updateTEMaterialTextures fetches nothing for those slots. getRenderTexture gives a slot's bake, else its fetched texture, and is what bind, pushGLTFBatchIndexed, the faces' texture channels and the selection outline read. The fetched slots keep their type for the terrain, preview and upload code, which never see a bake. refreshBakeTexture looked only at the face's own image, so a PBR face whose image named no bake was refreshed only when something else rebuilt it. It now resolves the bakes the face's render material names, and where one resolves to another texture calls changeTEMaterialTextures, which LLVOVolume follows with the rebuild changeTEImage asks for. Co-Authored-By: Claude Opus 5.5 --- indra/newview/alselectionoutline.cpp | 2 +- indra/newview/lldrawpool.cpp | 12 +++-- indra/newview/llfetchedgltfmaterial.cpp | 16 ++++--- indra/newview/llfetchedgltfmaterial.h | 24 ++++++++++ indra/newview/llviewerobject.cpp | 63 ++++++++++++++++++------- indra/newview/llviewerobject.h | 3 ++ indra/newview/llvovolume.cpp | 17 +++++-- indra/newview/llvovolume.h | 1 + 8 files changed, 105 insertions(+), 33 deletions(-) diff --git a/indra/newview/alselectionoutline.cpp b/indra/newview/alselectionoutline.cpp index 6ac7cc51ee..d4f237df87 100644 --- a/indra/newview/alselectionoutline.cpp +++ b/indra/newview/alselectionoutline.cpp @@ -885,7 +885,7 @@ ALSelectionOutline::FaceBinding ALSelectionOutline::bindFace(LLGLSLShader& progr // LLFetchedGLTFMaterial::bind in its place. if (!face->hasMedia() || !texture || texture->getType() != LLViewerTexture::MEDIA_TEXTURE) { - texture = gltf->mBaseColorTexture.get(); + texture = gltf->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR); } if (texture && texture_has_alpha(texture)) { diff --git a/indra/newview/lldrawpool.cpp b/indra/newview/lldrawpool.cpp index bec7df7d8d..b66fb5f6bd 100644 --- a/indra/newview/lldrawpool.cpp +++ b/indra/newview/lldrawpool.cpp @@ -1220,7 +1220,8 @@ void LLRenderPass::pushGLTFBatchIndexed(LLDrawInfo& params, eGLTFIndexedMaps map // vs data split LLFetchedGLTFMaterial::bind makes, and it has to match: a batched and // an unbatched copy of the same material must sample identically, or they shade // differently for no reason the content author can see. - LLViewerTexture* base = mat->mBaseColorTexture.notNull() ? mat->mBaseColorTexture.get() : LLViewerFetchedTexture::sWhiteImagep.get(); + LLViewerTexture* base = mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR); + base = base ? base : LLViewerFetchedTexture::sWhiteImagep.get(); gGL.getTextureSlot(s)->bindFast(base, ALSamplers::AnisoWrapSRGB); min_alpha[s] = (mat->mAlphaMode == LLGLTFMaterial::ALPHA_MODE_MASK) ? mat->mAlphaCutoff : -1.f; @@ -1236,7 +1237,8 @@ void LLRenderPass::pushGLTFBatchIndexed(LLDrawInfo& params, eGLTFIndexedMaps map } // emissive map/color/transform -- needed by both the glow and GBuffer passes - LLViewerTexture* em = mat->mEmissiveTexture.notNull() ? mat->mEmissiveTexture.get() : LLViewerFetchedTexture::sWhiteImagep.get(); + LLViewerTexture* em = mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE); + em = em ? em : LLViewerFetchedTexture::sWhiteImagep.get(); gGL.getTextureSlot(3 * N + s)->bindFast(em, ALSamplers::AnisoWrapSRGB); emissive[3 * s + 0] = mat->mEmissiveColor.mV[0]; @@ -1254,8 +1256,10 @@ void LLRenderPass::pushGLTFBatchIndexed(LLDrawInfo& params, eGLTFIndexedMaps map continue; } - LLViewerTexture* norm = (mat->mNormalTexture.notNull() && mat->mNormalTexture->getDiscardLevel() <= 4) ? mat->mNormalTexture.get() : LLViewerFetchedTexture::sFlatNormalImagep.get(); - LLViewerTexture* orm = mat->mMetallicRoughnessTexture.notNull() ? mat->mMetallicRoughnessTexture.get() : LLViewerFetchedTexture::sWhiteImagep.get(); + LLViewerTexture* norm = mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL); + norm = (norm && norm->getDiscardLevel() <= 4) ? norm : LLViewerFetchedTexture::sFlatNormalImagep.get(); + LLViewerTexture* orm = mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS); + orm = orm ? orm : LLViewerFetchedTexture::sWhiteImagep.get(); gGL.getTextureSlot(N + s)->bindFast(norm, ALSamplers::AnisoWrap); gGL.getTextureSlot(2 * N + s)->bindFast(orm, ALSamplers::AnisoWrap); diff --git a/indra/newview/llfetchedgltfmaterial.cpp b/indra/newview/llfetchedgltfmaterial.cpp index bd04cfbce4..74bd423533 100644 --- a/indra/newview/llfetchedgltfmaterial.cpp +++ b/indra/newview/llfetchedgltfmaterial.cpp @@ -57,6 +57,7 @@ LLFetchedGLTFMaterial& LLFetchedGLTFMaterial::operator=(const LLFetchedGLTFMater mNormalTexture = rhs.mNormalTexture; mMetallicRoughnessTexture = rhs.mMetallicRoughnessTexture; mEmissiveTexture = rhs.mEmissiveTexture; + std::copy(std::begin(rhs.mBakedTextures), std::end(rhs.mBakedTextures), std::begin(mBakedTextures)); return *this; } @@ -95,8 +96,8 @@ void LLFetchedGLTFMaterial::bind(LLViewerTexture* media_tex) LLGLSLShader* shader = LLGLSLShader::sCurBoundShaderPtr; // override emissive and base color textures with media tex if present - LLViewerTexture* baseColorTex = media_tex ? media_tex : mBaseColorTexture; - LLViewerTexture* emissiveTex = media_tex ? media_tex : mEmissiveTexture; + LLViewerTexture* baseColorTex = media_tex ? media_tex : getRenderTexture(GLTF_TEXTURE_INFO_BASE_COLOR); + LLViewerTexture* emissiveTex = media_tex ? media_tex : getRenderTexture(GLTF_TEXTURE_INFO_EMISSIVE); // sImpostorRender is checked alongside sShadowRender throughout this function because // generateImpostor sets BOTH, and only one of them means "depth-only". An impostor bake @@ -136,18 +137,20 @@ void LLFetchedGLTFMaterial::bind(LLViewerTexture* media_tex) if (!depth_only_pass) { - if (mNormalTexture.notNull() && mNormalTexture->getDiscardLevel() <= 4) + LLViewerTexture* normalTex = getRenderTexture(GLTF_TEXTURE_INFO_NORMAL); + if (normalTex != nullptr && normalTex->getDiscardLevel() <= 4) { - shader->bindTexture(LLShaderMgr::BUMP_MAP, mNormalTexture, GLTF_DATA_SAMPLER); + shader->bindTexture(LLShaderMgr::BUMP_MAP, normalTex, GLTF_DATA_SAMPLER); } else { shader->bindTexture(LLShaderMgr::BUMP_MAP, LLViewerFetchedTexture::sFlatNormalImagep, GLTF_DATA_SAMPLER); } - if (mMetallicRoughnessTexture.notNull()) + LLViewerTexture* metallicRoughnessTex = getRenderTexture(GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS); + if (metallicRoughnessTex != nullptr) { - shader->bindTexture(LLShaderMgr::SPECULAR_MAP, mMetallicRoughnessTexture, GLTF_DATA_SAMPLER); // PBR linear packed Occlusion, Roughness, Metal. + shader->bindTexture(LLShaderMgr::SPECULAR_MAP, metallicRoughnessTex, GLTF_DATA_SAMPLER); // PBR linear packed Occlusion, Roughness, Metal. } else { @@ -269,6 +272,7 @@ void LLFetchedGLTFMaterial::clearFetchedTextures() mNormalTexture = nullptr; mMetallicRoughnessTexture = nullptr; mEmissiveTexture = nullptr; + std::fill(std::begin(mBakedTextures), std::end(mBakedTextures), nullptr); } void LLFetchedGLTFMaterial::materialBegin() diff --git a/indra/newview/llfetchedgltfmaterial.h b/indra/newview/llfetchedgltfmaterial.h index 3530660603..b3a13b6d99 100644 --- a/indra/newview/llfetchedgltfmaterial.h +++ b/indra/newview/llfetchedgltfmaterial.h @@ -71,6 +71,30 @@ class LLFetchedGLTFMaterial: public LLGLTFMaterial LLPointer mNormalTexture; LLPointer mMetallicRoughnessTexture; LLPointer mEmissiveTexture; + + // What a slot naming a bake proxy (IMG_USE_BAKED_*) resolved to on the face this render + // material is drawn on (LLViewerObject::updateTEMaterialTextures, which fetches nothing for + // it): the server's bake, or the local composite while the avatar edits its appearance, which + // is no fetched texture and so has no place in the slots above. Null for any other slot. + LLPointer mBakedTextures[GLTF_TEXTURE_INFO_COUNT]; + + // The texture a pass draws a slot with: its bake, else its fetched texture + LLViewerTexture* getRenderTexture(TextureInfo slot) const + { + if (mBakedTextures[slot].notNull()) + { + return mBakedTextures[slot]; + } + switch (slot) + { + case GLTF_TEXTURE_INFO_BASE_COLOR: return mBaseColorTexture; + case GLTF_TEXTURE_INFO_NORMAL: return mNormalTexture; + case GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS: return mMetallicRoughnessTexture; + case GLTF_TEXTURE_INFO_EMISSIVE: return mEmissiveTexture; + default: return nullptr; + } + } + void clearFetchedTextures(); std::set mTextureEntires; diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index 4b527212aa..b46ccc393e 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -5477,22 +5477,18 @@ void LLViewerObject::updateTEMaterialTextures(U8 te) getTE(te)->setGLTFMaterial(nullptr); } - auto fetch_texture = [this](const LLUUID& id) + // A slot naming a bake proxy draws what it resolves to on this object's avatar + // (LLFetchedGLTFMaterial::mBakedTextures), and fetches nothing; any other fetches its asset + auto fetch_texture = [this, mat](LLGLTFMaterial::TextureInfo slot) { + const LLUUID& id = mat->mTextureId[slot]; + mat->mBakedTextures[slot] = getBakedTextureForMagicId(id); + LLViewerFetchedTexture* img = nullptr; - if (id.notNull()) + if (id.notNull() && mat->mBakedTextures[slot].isNull()) { - if (LLAvatarAppearanceDefines::LLAvatarAppearanceDictionary::isBakedImageId(id)) - { - // TODO -- fall back to LLTextureEntry::mGLTFRenderMaterial when overriding with baked texture - LLViewerTexture* viewerTexture = getBakedTextureForMagicId(id); - img = viewerTexture ? dynamic_cast(viewerTexture) : nullptr; - } - else - { - img = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE); - img->addTextureStats(64.f * 64.f, true); - } + img = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE); + img->addTextureStats(64.f * 64.f, true); } return img; @@ -5500,10 +5496,10 @@ void LLViewerObject::updateTEMaterialTextures(U8 te) if (mat != nullptr) { - mat->mBaseColorTexture = fetch_texture(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR]); - mat->mNormalTexture = fetch_texture(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL]); - mat->mMetallicRoughnessTexture = fetch_texture(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS]); - mat->mEmissiveTexture= fetch_texture(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE]); + mat->mBaseColorTexture = fetch_texture(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR); + mat->mNormalTexture = fetch_texture(LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL); + mat->mMetallicRoughnessTexture = fetch_texture(LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS); + mat->mEmissiveTexture = fetch_texture(LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE); } } @@ -5512,12 +5508,33 @@ void LLViewerObject::refreshBakeTexture() for (int face_index = 0; face_index < getNumTEs(); face_index++) { LLTextureEntry* tex_entry = getTE(face_index); - if (tex_entry && LLAvatarAppearanceDefines::LLAvatarAppearanceDictionary::isBakedImageId(tex_entry->getID())) + if (!tex_entry) + { + continue; + } + + if (LLAvatarAppearanceDefines::LLAvatarAppearanceDictionary::isBakedImageId(tex_entry->getID())) { const LLUUID& image_id = tex_entry->getID(); LLViewerTexture* bakedTexture = getBakedTextureForMagicId(image_id); changeTEImage(face_index, bakedTexture); } + + // A PBR face draws the bake proxies its render material names from what the material holds + // for them, resolved when the face was last built: resolve them again where one now resolves + // to another texture, whatever the face's own image names + const LLFetchedGLTFMaterial* mat = static_cast(tex_entry->getGLTFRenderMaterial()); + if (mat != nullptr) + { + for (U32 slot = 0; slot < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; ++slot) + { + if (getBakedTextureForMagicId(mat->mTextureId[slot]) != mat->mBakedTextures[slot].get()) + { + changeTEMaterialTextures(face_index); + break; + } + } + } } } @@ -5617,6 +5634,16 @@ void LLViewerObject::changeTEImage(S32 index, LLViewerTexture* new_image) mTEImages[index] = new_image ; } +//virtual +void LLViewerObject::changeTEMaterialTextures(S32 index) +{ + if (index < 0 || index >= getNumTEs()) + { + return; + } + updateTEMaterialTextures((U8)index); +} + void LLViewerObject::changeTENormalMap(S32 index, LLViewerTexture* new_image) { if(index < 0 || index >= getNumTEs()) diff --git a/indra/newview/llviewerobject.h b/indra/newview/llviewerobject.h index f51049f826..294ba1a3e1 100644 --- a/indra/newview/llviewerobject.h +++ b/indra/newview/llviewerobject.h @@ -399,6 +399,9 @@ class LLViewerObject virtual void changeTEImage(S32 index, LLViewerTexture* new_image) ; virtual void changeTENormalMap(S32 index, LLViewerTexture* new_image) ; virtual void changeTESpecularMap(S32 index, LLViewerTexture* new_image) ; + // Resolve a face's render material textures again (updateTEMaterialTextures), as when a bake + // proxy it names resolves to another texture + virtual void changeTEMaterialTextures(S32 index); LLViewerTexture *getTEImage(const U8 te) const; LLViewerTexture *getTENormalMap(const U8 te) const; LLViewerTexture *getTESpecularMap(const U8 te) const; diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp index 71fa8c240d..a7fd2366d7 100644 --- a/indra/newview/llvovolume.cpp +++ b/indra/newview/llvovolume.cpp @@ -2529,6 +2529,15 @@ void LLVOVolume::changeTEImage(S32 index, LLViewerTexture* imagep) } } +//virtual +void LLVOVolume::changeTEMaterialTextures(S32 index) +{ + LLViewerObject::changeTEMaterialTextures(index); + // the rebuild hands the faces' texture channels what the material now draws + gPipeline.markTextured(mDrawable); + mFaceMappingChanged = true; +} + void LLVOVolume::setTEImage(const U8 te, LLViewerTexture *imagep) { bool changed = (mTEImages[te] != imagep); @@ -6348,10 +6357,10 @@ void LLVolumeGeometryManager::rebuildGeom(LLSpatialGroup* group) facep->setTexture(LLRender::SPECULAR_MAP, nullptr); // let texture streaming system know about PBR textures - facep->setTexture(LLRender::BASECOLOR_MAP, gltf_mat->mBaseColorTexture); - facep->setTexture(LLRender::GLTF_NORMAL_MAP, gltf_mat->mNormalTexture); - facep->setTexture(LLRender::METALLIC_ROUGHNESS_MAP, gltf_mat->mMetallicRoughnessTexture); - facep->setTexture(LLRender::EMISSIVE_MAP, gltf_mat->mEmissiveTexture); + facep->setTexture(LLRender::BASECOLOR_MAP, gltf_mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR)); + facep->setTexture(LLRender::GLTF_NORMAL_MAP, gltf_mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL)); + facep->setTexture(LLRender::METALLIC_ROUGHNESS_MAP, gltf_mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS)); + facep->setTexture(LLRender::EMISSIVE_MAP, gltf_mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE)); } //ALWAYS null out vertex buffer on rebuild -- if the face lands in a render diff --git a/indra/newview/llvovolume.h b/indra/newview/llvovolume.h index da1bbd6e21..d1da564e41 100644 --- a/indra/newview/llvovolume.h +++ b/indra/newview/llvovolume.h @@ -214,6 +214,7 @@ class LLVOVolume : public LLViewerObject /*virtual*/ void setScale(const LLVector3 &scale, bool damped) override; /*virtual*/ void changeTEImage(S32 index, LLViewerTexture* new_image) override; + /*virtual*/ void changeTEMaterialTextures(S32 index) override; /*virtual*/ void setNumTEs(const U8 num_tes) override; /*virtual*/ void setTEImage(const U8 te, LLViewerTexture *imagep) override; /*virtual*/ S32 setTETexture(const U8 te, const LLUUID &uuid) override; From ce67354a60c9747a74e1d9e9fbade1d819e7c001 Mon Sep 17 00:00:00 2001 From: Zanibar <91260002+taylnos@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:10:06 -0500 Subject: [PATCH 02/10] Bake proxies in a PBR material hide the system body as an image's do An attachment face whose image is a bake proxy (IMG_USE_BAKED_*) hides the system body part that bake is for. One that names the proxy only in its PBR material did not: LLVOAvatar::updateMeshVisibility read each face's image ID alone, and only a change to an image asked for it again (LLViewerObject::updateAvatarMeshVisibility). A mesh body whose material named BAKED_UPPER drew over the system upper body unless its image named it too. LL's develop reads image IDs alone as well. LLVOAvatar::getBakesDrawnBy gives the bakes a face draws, from its image and every slot of its render material, and updateMeshVisibility gathers them in place of its two copies of eleven compares. The image's part is the same test, through assetIdToBakedTextureIndex. An overload of updateAvatarMeshVisibility takes a face and the render material it had before, and asks the avatar again where the bakes the two name differ: initRenderMaterial calls it, for a material that completes, an override or a face added, and so does setRenderMaterialID. It compares the bakes rather than asking whenever either names one, as the image overload does, because every texture entry update rebuilds a PBR face's render material, and an alpha HUD sends many at once. A material still fetching when a face took it named nothing then and fills in its textures in place, so setRenderMaterialID's completion callback asks again from nothing rather than from the material. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llviewerobject.cpp | 35 ++++++++++++++++-- indra/newview/llviewerobject.h | 2 ++ indra/newview/llvoavatar.cpp | 62 +++++++++++++++++--------------- indra/newview/llvoavatar.h | 9 +++++ 4 files changed, 78 insertions(+), 30 deletions(-) diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index b46ccc393e..08ac262be9 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -5417,6 +5417,22 @@ void LLViewerObject::updateAvatarMeshVisibility(const LLUUID& id, const LLUUID& } } +void LLViewerObject::updateAvatarMeshVisibility(U8 te, const LLGLTFMaterial* old_material) +{ + const LLTextureEntry* tep = getTE(te); + const LLGLTFMaterial* material = tep ? tep->getGLTFRenderMaterial() : nullptr; + if (material == old_material) + { + return; + } + + LLVOAvatar* avatar = getAvatar(); + if (avatar && LLVOAvatar::getBakesNamedBy(material) != LLVOAvatar::getBakesNamedBy(old_material)) + { + avatar->updateMeshVisibility(); + } +} + void LLViewerObject::setTE(const U8 te, const LLTextureEntry& texture_entry) { @@ -5919,7 +5935,12 @@ S32 LLViewerObject::initRenderMaterial(U8 te) if (override_material) { render_material->applyOverride(*override_material); } render_material->clearFetchedTextures(); } - return tep->setGLTFRenderMaterial(render_material); + + // held, as setting the new one may release it + const LLPointer old_material = tep->getGLTFRenderMaterial(); + const S32 changed = tep->setGLTFRenderMaterial(render_material); + updateAvatarMeshVisibility(te, old_material); + return changed; } S32 LLViewerObject::setTEGLTFMaterialOverride(U8 te, LLGLTFMaterial* override_mat) @@ -7820,6 +7841,8 @@ void LLViewerObject::setRenderMaterialID(S32 te_in, const LLUUID& id, bool updat for (S32 te = start_idx; te < end_idx; ++te) { LLTextureEntry* tep = getTE(te); + // held, as clearing the render material may release it + const LLPointer old_material = tep->getGLTFRenderMaterial(); // If local_origin=false (i.e. it's from the server), we know the // material has updated or been created, because extra params are @@ -7849,6 +7872,8 @@ void LLViewerObject::setRenderMaterialID(S32 te_in, const LLUUID& id, bool updat tep->setGLTFMaterial(new_material, !update_server); } + updateAvatarMeshVisibility((U8)te, old_material); + if (material_changed && new_material) { // Sometimes, the material may change out from underneath the overrides. @@ -7859,11 +7884,17 @@ void LLViewerObject::setRenderMaterialID(S32 te_in, const LLUUID& id, bool updat // Also, sometimes, the material has baked textures, which requires // a copy unique to this object. // Currently, we do not deduplicate render materials. - new_material->onMaterialComplete([obj_id = getID(), te]() + new_material->onMaterialComplete([obj_id = getID(), te, fetching = new_material->isFetching()]() { LLViewerObject* obj = gObjectList.findObject(obj_id); if (!obj) { return; } obj->initRenderMaterial(te); + if (fetching) + { + // the material named no bake when the face took it, above, and has filled in + // its textures since, so initRenderMaterial sees none of them change + obj->updateAvatarMeshVisibility((U8)te, nullptr); + } }); } } diff --git a/indra/newview/llviewerobject.h b/indra/newview/llviewerobject.h index 294ba1a3e1..8293c90c99 100644 --- a/indra/newview/llviewerobject.h +++ b/indra/newview/llviewerobject.h @@ -695,6 +695,8 @@ class LLViewerObject public: LLViewerTexture* getBakedTextureForMagicId(const LLUUID& id); void updateAvatarMeshVisibility(const LLUUID& id, const LLUUID& old_id); + // As above, for a face's PBR material: old_material is the render material it had before + void updateAvatarMeshVisibility(U8 te, const LLGLTFMaterial* old_material); void refreshBakeTexture(); public: static void unpackVector3(LLDataPackerBinaryBuffer* dp, LLVector3& value, std::string name); diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index 52e325ffbc..a0ce49cee7 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -9492,6 +9492,37 @@ void LLVOAvatar::debugColorizeSubMeshes(U32 i, const LLColor4& color) } } +// Add the bake a texture ID names, where it is a bake proxy (IMG_USE_BAKED_*) +static void add_named_bake(LLVOAvatar::bake_set_t& bakes, const LLUUID& id) +{ + const EBakedTextureIndex index = LLAvatarAppearanceDictionary::assetIdToBakedTextureIndex(id); + if (index != BAKED_NUM_INDICES) + { + bakes.set(index); + } +} + +// static +LLVOAvatar::bake_set_t LLVOAvatar::getBakesNamedBy(const LLGLTFMaterial* material) +{ + bake_set_t bakes; + if (material) + { + for (const LLUUID& id : material->mTextureId) + { + add_named_bake(bakes, id); + } + } + return bakes; +} + +// static +LLVOAvatar::bake_set_t LLVOAvatar::getBakesDrawnBy(const LLTextureEntry& te) +{ + bake_set_t bakes = getBakesNamedBy(te.getGLTFRenderMaterial()); + add_named_bake(bakes, te.getID()); + return bakes; +} //----------------------------------------------------------------------------- // updateMeshVisibility() @@ -9499,8 +9530,7 @@ void LLVOAvatar::debugColorizeSubMeshes(U32 i, const LLColor4& color) //----------------------------------------------------------------------------- void LLVOAvatar::updateMeshVisibility() { - bool bake_flag[BAKED_NUM_INDICES]; - memset(bake_flag, 0, BAKED_NUM_INDICES*sizeof(bool)); + bake_set_t bake_flag; if (getOverallAppearance() == AOA_NORMAL) { @@ -9516,19 +9546,7 @@ void LLVOAvatar::updateMeshVisibility() for (int face_index = 0; face_index < objectp->getNumTEs(); face_index++) { - LLTextureEntry* tex_entry = objectp->getTE(face_index); - const auto& tex_id = tex_entry->getID(); - bake_flag[BAKED_HEAD] |= (tex_id == IMG_USE_BAKED_HEAD); - bake_flag[BAKED_EYES] |= (tex_id == IMG_USE_BAKED_EYES); - bake_flag[BAKED_HAIR] |= (tex_id == IMG_USE_BAKED_HAIR); - bake_flag[BAKED_LOWER] |= (tex_id == IMG_USE_BAKED_LOWER); - bake_flag[BAKED_UPPER] |= (tex_id == IMG_USE_BAKED_UPPER); - bake_flag[BAKED_SKIRT] |= (tex_id == IMG_USE_BAKED_SKIRT); - bake_flag[BAKED_LEFT_ARM] |= (tex_id == IMG_USE_BAKED_LEFTARM); - bake_flag[BAKED_LEFT_LEG] |= (tex_id == IMG_USE_BAKED_LEFTLEG); - bake_flag[BAKED_AUX1] |= (tex_id == IMG_USE_BAKED_AUX1); - bake_flag[BAKED_AUX2] |= (tex_id == IMG_USE_BAKED_AUX2); - bake_flag[BAKED_AUX3] |= (tex_id == IMG_USE_BAKED_AUX3); + bake_flag |= getBakesDrawnBy(*objectp->getTE(face_index)); } for (const auto& objectchild : objectp->getChildren()) @@ -9538,19 +9556,7 @@ void LLVOAvatar::updateMeshVisibility() for (int face_index = 0; face_index < objectchild->getNumTEs(); face_index++) { - LLTextureEntry* tex_entry = objectchild->getTE(face_index); - const auto& tex_id = tex_entry->getID(); - bake_flag[BAKED_HEAD] |= (tex_id == IMG_USE_BAKED_HEAD); - bake_flag[BAKED_EYES] |= (tex_id == IMG_USE_BAKED_EYES); - bake_flag[BAKED_HAIR] |= (tex_id == IMG_USE_BAKED_HAIR); - bake_flag[BAKED_LOWER] |= (tex_id == IMG_USE_BAKED_LOWER); - bake_flag[BAKED_UPPER] |= (tex_id == IMG_USE_BAKED_UPPER); - bake_flag[BAKED_SKIRT] |= (tex_id == IMG_USE_BAKED_SKIRT); - bake_flag[BAKED_LEFT_ARM] |= (tex_id == IMG_USE_BAKED_LEFTARM); - bake_flag[BAKED_LEFT_LEG] |= (tex_id == IMG_USE_BAKED_LEFTLEG); - bake_flag[BAKED_AUX1] |= (tex_id == IMG_USE_BAKED_AUX1); - bake_flag[BAKED_AUX2] |= (tex_id == IMG_USE_BAKED_AUX2); - bake_flag[BAKED_AUX3] |= (tex_id == IMG_USE_BAKED_AUX3); + bake_flag |= getBakesDrawnBy(*objectchild->getTE(face_index)); } } } diff --git a/indra/newview/llvoavatar.h b/indra/newview/llvoavatar.h index 1688b23036..cfbc109379 100644 --- a/indra/newview/llvoavatar.h +++ b/indra/newview/llvoavatar.h @@ -28,6 +28,7 @@ #ifndef LL_VOAVATAR_H #define LL_VOAVATAR_H +#include #include #include #include @@ -942,6 +943,14 @@ class LLVOAvatar : void updateMeshVisibility(); LLViewerTexture* getBakedTexture(const U8 te); + // A set of bakes, by EBakedTextureIndex + typedef std::bitset bake_set_t; + // The bakes a face draws, by its image or any slot of its PBR material: an avatar wearing it + // hides the body parts they are baked for (updateMeshVisibility) + static bake_set_t getBakesDrawnBy(const LLTextureEntry& te); + // The bakes a PBR material's slots name + static bake_set_t getBakesNamedBy(const LLGLTFMaterial* material); + // Matrix palette cache entry class alignas(16) MatrixPaletteCache { From 8738946fe141c54b31309905187a9fd5fe90a98e Mon Sep 17 00:00:00 2001 From: Zanibar <91260002+taylnos@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:31:03 -0500 Subject: [PATCH 03/10] Refresh Textures refreshes the bake a PBR face draws object_texture_refresh took a PBR face's textures from its render material's texture IDs, while it takes the image from getTEImage, the texture drawn. For a slot naming a bake proxy (IMG_USE_BAKED_*) the ID is the proxy's own, so the bake the face draws was never refreshed, and destroy_texture, which made a fetched texture of any ID to clear it, started a download of the proxy that nothing draws. Each slot's ID now comes from LLFetchedGLTFMaterial::getRenderTexture, as the image's does from getTEImage. destroy_texture finds the texture in gTextureList rather than making one. An ID no fetched texture has, as the local composite's while the avatar edits its appearance, which getTEImage and getRenderTexture both give, has nothing to clear. Co-Authored-By: Claude Opus 5.5 --- indra/newview/alviewermenu.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/indra/newview/alviewermenu.cpp b/indra/newview/alviewermenu.cpp index a1b013cff7..4f38489a66 100644 --- a/indra/newview/alviewermenu.cpp +++ b/indra/newview/alviewermenu.cpp @@ -38,6 +38,7 @@ #include "llagentcamera.h" #include "llappviewer.h" #include "llavatarpropertiesprocessor.h" +#include "llfetchedgltfmaterial.h" #include "llhudobject.h" #include "llnotifications.h" #include "llnotificationsutil.h" @@ -48,6 +49,7 @@ #include "llviewerobject.h" #include "llviewerobjectlist.h" #include "llviewerregion.h" +#include "llviewertexturelist.h" #include "llvoavatar.h" #include "llvoavatarself.h" @@ -381,7 +383,9 @@ namespace if (id.isNull() || id == IMG_DEFAULT || id == IMG_TRANSPARENT|| id == "8dcd4a48-2d37-4909-9f78-f7a9eb4ef903") return; - LLViewerFetchedTexture* texture = LLViewerTextureManager::getFetchedTexture(id); + // Looked up, not created: an ID no fetched texture has, such as a dynamic texture's, has + // nothing to clear, and creating one would start a download nothing draws + LLViewerFetchedTexture* texture = gTextureList.findImage(id, TEX_LIST_STANDARD); if (texture) texture->clearFetchedResults(); LLAppViewer::getTextureCache()->removeFromCache(id); @@ -412,12 +416,17 @@ namespace faces_per_tex[spec_img->getID()].push_back(i); } - LLPointer mat = node->getObject()->getTE(i)->getGLTFRenderMaterial(); - if (mat.notNull()) + // What each slot draws, as getTEImage is for the image: a bake proxy's bake, not the + // proxy's own ID + const auto* mat = static_cast(node->getObject()->getTE(i)->getGLTFRenderMaterial()); + if (mat) { for (U32 j = 0; j < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; ++j) { - faces_per_tex[mat->mTextureId[j]].push_back(i); + if (const LLViewerTexture* tex = mat->getRenderTexture((LLGLTFMaterial::TextureInfo)j)) + { + faces_per_tex[tex->getID()].push_back(i); + } } } } From 03d9e9b6f36a02b14a1dd91855ba4538b1ad78a8 Mon Sep 17 00:00:00 2001 From: Zanibar <91260002+taylnos@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:38:17 -0500 Subject: [PATCH 04/10] A face that takes a material still fetching gets its own copy when it completes A new object's render material IDs are read before it has faces: LLVOVolume::processUpdateMessage sets the volume, which makes them, after the base class has unpacked the extra params, so setRenderMaterialIDs set none. Each face takes its material at its first rebuild, in the fallback in updateTEMaterialTextures, and where the material was still fetching there its completion only marked the object for update. initRenderMaterial saw the material's empty textures when the face took it and never ran again, so a material naming a bake proxy kept no copy of its own unless an override arrived. The face drew the material every object using it shares, and each rebuild resolved its own avatar's bake into it: two avatars wearing one body's material could show each other's skin. The system body part stayed shown too, as the visibility check runs from initRenderMaterial. The fallback came from LL in 2022. onRenderMaterialFetched does what setRenderMaterialID's completion did for a material that was fetching: initRenderMaterial, then the avatar's mesh visibility from nothing. Both completions call it. CodeRabbit found this on #533. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llviewerobject.cpp | 26 +++++++++++++++++++++----- indra/newview/llviewerobject.h | 4 ++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index 08ac262be9..d5181e7e3f 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -5473,11 +5473,12 @@ void LLViewerObject::updateTEMaterialTextures(U8 te) llassert(mat == nullptr || dynamic_cast(gGLTFMaterialList.getMaterial(mat_id)) != nullptr); if (mat->isFetching()) { // material is not loaded yet, rebuild draw info when the object finishes loading - mat->onMaterialComplete([id=getID()] + mat->onMaterialComplete([id=getID(), te] { LLViewerObject* obj = gObjectList.findObject(id); if (obj) { + obj->onRenderMaterialFetched(te); obj->markForUpdate(); } }); @@ -5943,6 +5944,20 @@ S32 LLViewerObject::initRenderMaterial(U8 te) return changed; } +void LLViewerObject::onRenderMaterialFetched(U8 te) +{ + const LLTextureEntry* tep = getTE(te); + if (!tep || !tep->getGLTFMaterial()) + { // the face lost its material while it fetched + return; + } + + initRenderMaterial(te); + // The material named no bake when the face took it and has filled in its textures since, so + // initRenderMaterial saw none of them change: look from nothing + updateAvatarMeshVisibility(te, nullptr); +} + S32 LLViewerObject::setTEGLTFMaterialOverride(U8 te, LLGLTFMaterial* override_mat) { LL_PROFILE_ZONE_SCOPED; @@ -7888,12 +7903,13 @@ void LLViewerObject::setRenderMaterialID(S32 te_in, const LLUUID& id, bool updat { LLViewerObject* obj = gObjectList.findObject(obj_id); if (!obj) { return; } - obj->initRenderMaterial(te); if (fetching) { - // the material named no bake when the face took it, above, and has filled in - // its textures since, so initRenderMaterial sees none of them change - obj->updateAvatarMeshVisibility((U8)te, nullptr); + obj->onRenderMaterialFetched((U8)te); + } + else + { + obj->initRenderMaterial(te); } }); } diff --git a/indra/newview/llviewerobject.h b/indra/newview/llviewerobject.h index 8293c90c99..ef3d865323 100644 --- a/indra/newview/llviewerobject.h +++ b/indra/newview/llviewerobject.h @@ -387,6 +387,10 @@ class LLViewerObject /*virtual*/ S32 setTEMaterialID(const U8 te, const LLMaterialID& pMaterialID); /*virtual*/ S32 setTEMaterialParams(const U8 te, const LLMaterialPtr pMaterialParams); S32 initRenderMaterial(const U8 te); + // The material face te took while it was still fetching has completed: give the face its own + // copy where it needs one (initRenderMaterial), so the bakes it names are resolved for this + // object's avatar and not into the material every object using it shares + void onRenderMaterialFetched(const U8 te); virtual S32 setTEGLTFMaterialOverride(U8 te, LLGLTFMaterial* mat); // Used by Materials update functions to properly kick off rebuilds From 6b6f8cd1b2b7969a3c18c0b6fe2d37fff2850e31 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 26 Oct 2026 09:30:38 -0400 Subject: [PATCH 05/10] Body hiding counts a PBR material's base colour, and none of a HUD's material A bake proxy (IMG_USE_BAKED_*) in any of a PBR material's four slots hid the system body part it is baked for, so a layer using IMG_USE_BAKED_UPPER as its emissive or ORM map hid the upper body. A legacy material's normal and specular maps hide nothing; only a face's image does, and the base colour is the slot that stands for it. updateMeshVisibility also counted the materials on the HUD attachment points, where getBakedTextureForMagicId resolves no bake, so a HUD naming one hid a body part it does not draw. ALBakeProxies holds the bake sets, apart from LLVOAvatar so that the viewer's tests reach them. drawnBy counts a face's image and its render material's base colour, and of a HUD's face its image alone, as Linden's viewer counts it. updateAvatarMeshVisibility asks nothing when a HUD's material changes. Found reviewing #533. Co-Authored-By: Claude Opus 5.5 --- indra/newview/CMakeLists.txt | 4 + indra/newview/albakeproxies.cpp | 57 +++++++++++ indra/newview/albakeproxies.h | 41 ++++++++ indra/newview/llviewerobject.cpp | 6 +- indra/newview/llvoavatar.cpp | 40 +------- indra/newview/llvoavatar.h | 9 -- indra/newview/tests/albakeproxies_test.cpp | 108 +++++++++++++++++++++ 7 files changed, 219 insertions(+), 46 deletions(-) create mode 100644 indra/newview/albakeproxies.cpp create mode 100644 indra/newview/albakeproxies.h create mode 100644 indra/newview/tests/albakeproxies_test.cpp diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 9fea885622..a378c9f799 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -135,6 +135,7 @@ if(AL_BUILD_VIEWER) alassetblocklist.cpp alavataractions.cpp alavatargroups.cpp + albakeproxies.cpp alchatautocomplete.cpp alchatbar.cpp alchatcommand.cpp @@ -1011,6 +1012,7 @@ if(AL_BUILD_VIEWER) alassetblocklist.h alavataractions.h alavatargroups.h + albakeproxies.h alchatautocomplete.h alchatbar.h alchatcommand.h @@ -2535,6 +2537,7 @@ if(AL_BUILD_TESTS) PROJECT ${test_project} SOURCES tests/alagentlocationformat_test.cpp + tests/albakeproxies_test.cpp tests/alchattimestamp_test.cpp tests/alcolorwheelmodel_test.cpp tests/alcrashreporter_test.cpp @@ -2609,6 +2612,7 @@ if(AL_BUILD_TESTS) tests/noise_test.cpp UNIT alagentlocationformat.cpp + albakeproxies.cpp alchattimestamp.cpp alcolorwheelmodel.cpp alcrashreporter.cpp diff --git a/indra/newview/albakeproxies.cpp b/indra/newview/albakeproxies.cpp new file mode 100644 index 0000000000..345a97d47d --- /dev/null +++ b/indra/newview/albakeproxies.cpp @@ -0,0 +1,57 @@ +/** + * @file albakeproxies.cpp + * @brief The bakes a face names through bake proxies (IMG_USE_BAKED_*) + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "linden_common.h" + +#include "albakeproxies.h" + +#include "llgltfmaterial.h" +#include "lltextureentry.h" + +using namespace LLAvatarAppearanceDefines; + +namespace +{ + // The bake a texture ID names, where it is a bake proxy + ALBakeProxies::bake_set_t named_by(const LLUUID& id) + { + ALBakeProxies::bake_set_t bakes; + const EBakedTextureIndex index = LLAvatarAppearanceDictionary::assetIdToBakedTextureIndex(id); + if (index != BAKED_NUM_INDICES) + { + bakes.set(index); + } + return bakes; + } +} + +namespace ALBakeProxies +{ + +bake_set_t drawnBy(const LLGLTFMaterial* material) +{ + return material ? named_by(material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR]) : bake_set_t(); +} + +bake_set_t drawnBy(const LLTextureEntry& te, bool hud) +{ + bake_set_t bakes = named_by(te.getID()); + if (!hud) + { + bakes |= drawnBy(te.getGLTFRenderMaterial()); + } + return bakes; +} + +} diff --git a/indra/newview/albakeproxies.h b/indra/newview/albakeproxies.h new file mode 100644 index 0000000000..52549fd3f8 --- /dev/null +++ b/indra/newview/albakeproxies.h @@ -0,0 +1,41 @@ +/** + * @file albakeproxies.h + * @brief The bakes a face names through bake proxies (IMG_USE_BAKED_*), whose system body parts an avatar + * wearing it hides. + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ +#ifndef AL_BAKEPROXIES_H +#define AL_BAKEPROXIES_H + +#include "llavatarappearancedefines.h" + +#include + +class LLGLTFMaterial; +class LLTextureEntry; + +namespace ALBakeProxies +{ + // A set of bakes, by EBakedTextureIndex + typedef std::bitset bake_set_t; + + // The bake a PBR material draws a body with: the one its base colour names, the slot that stands for a + // face's image. A bake in its other slots shades the surface without drawing a body + bake_set_t drawnBy(const LLGLTFMaterial* material); + + // The bakes a face draws a body with, whose system body parts an avatar wearing it hides + // (LLVOAvatar::updateMeshVisibility): its image's and its PBR material's. A HUD's face resolves no bake + // (LLViewerObject::getBakedTextureForMagicId): its image counts, as Linden's viewer counts it, and its + // material does not + bake_set_t drawnBy(const LLTextureEntry& te, bool hud); +} + +#endif // AL_BAKEPROXIES_H diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index d5181e7e3f..9cecb73437 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -54,6 +54,7 @@ #include "object_flags.h" #include "alassetblocklist.h" +#include "albakeproxies.h" #include "llaudiosourcevo.h" #include "llagent.h" #include "llagentcamera.h" @@ -5421,13 +5422,14 @@ void LLViewerObject::updateAvatarMeshVisibility(U8 te, const LLGLTFMaterial* old { const LLTextureEntry* tep = getTE(te); const LLGLTFMaterial* material = tep ? tep->getGLTFRenderMaterial() : nullptr; - if (material == old_material) + // a HUD's material draws no bake (ALBakeProxies::drawnBy) + if (material == old_material || isHUDAttachment()) { return; } LLVOAvatar* avatar = getAvatar(); - if (avatar && LLVOAvatar::getBakesNamedBy(material) != LLVOAvatar::getBakesNamedBy(old_material)) + if (avatar && ALBakeProxies::drawnBy(material) != ALBakeProxies::drawnBy(old_material)) { avatar->updateMeshVisibility(); } diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index a0ce49cee7..a36a92f0e6 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -38,6 +38,7 @@ #include "raytrace.h" #include "alavatargroups.h" +#include "albakeproxies.h" #include "alrenderisolation.h" #include "aoengine.h" #include "llagent.h" // Get state values from here @@ -9492,45 +9493,13 @@ void LLVOAvatar::debugColorizeSubMeshes(U32 i, const LLColor4& color) } } -// Add the bake a texture ID names, where it is a bake proxy (IMG_USE_BAKED_*) -static void add_named_bake(LLVOAvatar::bake_set_t& bakes, const LLUUID& id) -{ - const EBakedTextureIndex index = LLAvatarAppearanceDictionary::assetIdToBakedTextureIndex(id); - if (index != BAKED_NUM_INDICES) - { - bakes.set(index); - } -} - -// static -LLVOAvatar::bake_set_t LLVOAvatar::getBakesNamedBy(const LLGLTFMaterial* material) -{ - bake_set_t bakes; - if (material) - { - for (const LLUUID& id : material->mTextureId) - { - add_named_bake(bakes, id); - } - } - return bakes; -} - -// static -LLVOAvatar::bake_set_t LLVOAvatar::getBakesDrawnBy(const LLTextureEntry& te) -{ - bake_set_t bakes = getBakesNamedBy(te.getGLTFRenderMaterial()); - add_named_bake(bakes, te.getID()); - return bakes; -} - //----------------------------------------------------------------------------- // updateMeshVisibility() // Hide the mesh joints if attachments are using baked textures //----------------------------------------------------------------------------- void LLVOAvatar::updateMeshVisibility() { - bake_set_t bake_flag; + ALBakeProxies::bake_set_t bake_flag; if (getOverallAppearance() == AOA_NORMAL) { @@ -9539,6 +9508,7 @@ void LLVOAvatar::updateMeshVisibility() if (!attachment) continue; + const bool hud = attachment->getIsHUDAttachment(); for (const auto& objectp : attachment->mAttachedObjects) { if (objectp.isNull()) @@ -9546,7 +9516,7 @@ void LLVOAvatar::updateMeshVisibility() for (int face_index = 0; face_index < objectp->getNumTEs(); face_index++) { - bake_flag |= getBakesDrawnBy(*objectp->getTE(face_index)); + bake_flag |= ALBakeProxies::drawnBy(*objectp->getTE(face_index), hud); } for (const auto& objectchild : objectp->getChildren()) @@ -9556,7 +9526,7 @@ void LLVOAvatar::updateMeshVisibility() for (int face_index = 0; face_index < objectchild->getNumTEs(); face_index++) { - bake_flag |= getBakesDrawnBy(*objectchild->getTE(face_index)); + bake_flag |= ALBakeProxies::drawnBy(*objectchild->getTE(face_index), hud); } } } diff --git a/indra/newview/llvoavatar.h b/indra/newview/llvoavatar.h index cfbc109379..1688b23036 100644 --- a/indra/newview/llvoavatar.h +++ b/indra/newview/llvoavatar.h @@ -28,7 +28,6 @@ #ifndef LL_VOAVATAR_H #define LL_VOAVATAR_H -#include #include #include #include @@ -943,14 +942,6 @@ class LLVOAvatar : void updateMeshVisibility(); LLViewerTexture* getBakedTexture(const U8 te); - // A set of bakes, by EBakedTextureIndex - typedef std::bitset bake_set_t; - // The bakes a face draws, by its image or any slot of its PBR material: an avatar wearing it - // hides the body parts they are baked for (updateMeshVisibility) - static bake_set_t getBakesDrawnBy(const LLTextureEntry& te); - // The bakes a PBR material's slots name - static bake_set_t getBakesNamedBy(const LLGLTFMaterial* material); - // Matrix palette cache entry class alignas(16) MatrixPaletteCache { diff --git a/indra/newview/tests/albakeproxies_test.cpp b/indra/newview/tests/albakeproxies_test.cpp new file mode 100644 index 0000000000..d564a688f6 --- /dev/null +++ b/indra/newview/tests/albakeproxies_test.cpp @@ -0,0 +1,108 @@ +/** + * @file albakeproxies_test.cpp + * @brief Unit tests for the bakes a face names through bake proxies + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "linden_common.h" + +#include + +#include "../albakeproxies.h" + +#include "indra_constants.h" +#include "llgltfmaterial.h" +#include "lltextureentry.h" + +#include +#include + +namespace +{ + using namespace ALBakeProxies; + using namespace LLAvatarAppearanceDefines; + + // The set of the given bakes, as a number Catch prints + unsigned long bakes(std::initializer_list indices) + { + bake_set_t set; + for (EBakedTextureIndex index : indices) + { + set.set(index); + } + return set.to_ulong(); + } + + // A face and the PBR material it is drawn with + struct face_data + { + LLPointer mMaterial = new LLGLTFMaterial(); + LLTextureEntry mTE; + + face_data() : mTE(IMG_DEFAULT) { mTE.setGLTFMaterial(mMaterial); } + + LLUUID& slot(LLGLTFMaterial::TextureInfo info) { return mMaterial->mTextureId[info]; } + }; + + TEST_CASE("ALBakeProxies: every bake proxy names its own bake", "[ALBakeProxies]") + { + const std::pair proxies[] = { + { &IMG_USE_BAKED_HEAD, BAKED_HEAD }, { &IMG_USE_BAKED_UPPER, BAKED_UPPER }, + { &IMG_USE_BAKED_LOWER, BAKED_LOWER }, { &IMG_USE_BAKED_EYES, BAKED_EYES }, + { &IMG_USE_BAKED_SKIRT, BAKED_SKIRT }, { &IMG_USE_BAKED_HAIR, BAKED_HAIR }, + { &IMG_USE_BAKED_LEFTARM, BAKED_LEFT_ARM }, { &IMG_USE_BAKED_LEFTLEG, BAKED_LEFT_LEG }, + { &IMG_USE_BAKED_AUX1, BAKED_AUX1 }, { &IMG_USE_BAKED_AUX2, BAKED_AUX2 }, + { &IMG_USE_BAKED_AUX3, BAKED_AUX3 }, + }; + for (const auto& [id, index] : proxies) + { + LLTextureEntry te(*id); + CHECK(drawnBy(te, false).to_ulong() == bakes({ index })); + } + } + + TEST_CASE("ALBakeProxies: a face with no proxy draws no bake", "[ALBakeProxies]") + { + LLTextureEntry te(IMG_DEFAULT); + CHECK(drawnBy(te, false).none()); + CHECK(drawnBy(nullptr).none()); + } + + TEST_CASE_METHOD(face_data, "ALBakeProxies: a PBR material draws the bake its base colour names", "[ALBakeProxies]") + { + slot(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR) = IMG_USE_BAKED_UPPER; + CHECK(drawnBy(mMaterial).to_ulong() == bakes({ BAKED_UPPER })); + CHECK(drawnBy(mTE, false).to_ulong() == bakes({ BAKED_UPPER })); + + // the image's bake and the material's together + mTE.setID(IMG_USE_BAKED_HEAD); + CHECK(drawnBy(mTE, false).to_ulong() == bakes({ BAKED_HEAD, BAKED_UPPER })); + } + + TEST_CASE_METHOD(face_data, "ALBakeProxies: a bake in a material's other slots draws no body", "[ALBakeProxies]") + { + slot(LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL) = IMG_USE_BAKED_UPPER; + slot(LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS) = IMG_USE_BAKED_HEAD; + slot(LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE) = IMG_USE_BAKED_HAIR; + + CHECK(drawnBy(mMaterial).none()); + CHECK(drawnBy(mTE, false).none()); + } + + TEST_CASE_METHOD(face_data, "ALBakeProxies: a HUD's face counts its image and not its material", "[ALBakeProxies]") + { + mTE.setID(IMG_USE_BAKED_HEAD); + slot(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR) = IMG_USE_BAKED_UPPER; + + CHECK(drawnBy(mTE, true).to_ulong() == bakes({ BAKED_HEAD })); + CHECK(drawnBy(mTE, false).to_ulong() == bakes({ BAKED_HEAD, BAKED_UPPER })); + } +} From a1d931ad69840768e62138e4b04680709cd0c8e7 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 26 Oct 2026 09:30:48 -0400 Subject: [PATCH 06/10] A face that takes a loaded material in its first build hides the system body A new attachment's faces take their material in updateTEMaterialTextures' fallback, at their first build, after attachObject has asked for the body's visibility. Where the material was already loaded, as when a body is worn again within half a minute or another avatar nearby wears the same one, initRenderMaterial compared the face's copy with the material set on the face a line before, which names the same bakes, and asked nothing: the system body drew through a body whose PBR material named its bake until the next appearance message. The fallback now asks from nothing, as onRenderMaterialFetched does for a material still fetching. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llviewerobject.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index 9cecb73437..9d685d208b 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -5487,6 +5487,9 @@ void LLViewerObject::updateTEMaterialTextures(U8 te) } getTE(te)->setGLTFMaterial(mat); initRenderMaterial(te); + // initRenderMaterial compared the face's copy with the material just set on it, which names the + // same bakes: the face had no material before + updateAvatarMeshVisibility(te, nullptr); mat = (LLFetchedGLTFMaterial*) getTE(te)->getGLTFRenderMaterial(); llassert(mat == nullptr || dynamic_cast(getTE(te)->getGLTFRenderMaterial()) != nullptr); } From b71807bc8313bc3994331d937f1a322ddc7bf644 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 26 Oct 2026 09:30:51 -0400 Subject: [PATCH 07/10] An avatar hides its body parts once a frame, however many faces ask Each face that asked for updateMeshVisibility had it run there and then, and it walks every face of every attachment. When a material a body's faces share completes, each face's onRenderMaterialFetched asked in turn: a few hundred walks of a few thousand faces in one callback. A body's images do the same as its texture entries arrive. The faces now mark the avatar (dirtyMeshVisibility), and its idle update runs it once. The callers that attach, detach or change the avatar's appearance still run it straight away, which clears the mark. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llviewerobject.cpp | 4 ++-- indra/newview/llvoavatar.cpp | 7 +++++++ indra/newview/llvoavatar.h | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index 9d685d208b..4a0a1bcfc6 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -5414,7 +5414,7 @@ void LLViewerObject::updateAvatarMeshVisibility(const LLUUID& id, const LLUUID& LLVOAvatar* avatar = getAvatar(); if (avatar) { - avatar->updateMeshVisibility(); + avatar->dirtyMeshVisibility(); } } @@ -5431,7 +5431,7 @@ void LLViewerObject::updateAvatarMeshVisibility(U8 te, const LLGLTFMaterial* old LLVOAvatar* avatar = getAvatar(); if (avatar && ALBakeProxies::drawnBy(material) != ALBakeProxies::drawnBy(old_material)) { - avatar->updateMeshVisibility(); + avatar->dirtyMeshVisibility(); } } diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index a36a92f0e6..731c2c9bee 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -2855,6 +2855,11 @@ void LLVOAvatar::idleUpdate(LLAgent &agent, const F64 &time) return; } + if (mMeshVisibilityDirty) + { + updateMeshVisibility(); + } + readProfileQuery(); static LLCachedControl friends_only(gSavedSettings, "RenderAvatarFriendsOnly", false); @@ -9499,6 +9504,8 @@ void LLVOAvatar::debugColorizeSubMeshes(U32 i, const LLColor4& color) //----------------------------------------------------------------------------- void LLVOAvatar::updateMeshVisibility() { + mMeshVisibilityDirty = false; + ALBakeProxies::bake_set_t bake_flag; if (getOverallAppearance() == AOA_NORMAL) diff --git a/indra/newview/llvoavatar.h b/indra/newview/llvoavatar.h index 1688b23036..bca3f74a93 100644 --- a/indra/newview/llvoavatar.h +++ b/indra/newview/llvoavatar.h @@ -940,6 +940,9 @@ class LLVOAvatar : virtual void dirtyMesh(); // Dirty the avatar mesh void updateMeshData(); void updateMeshVisibility(); + // updateMeshVisibility at the avatar's next idle update, once however many of its attachments' faces + // ask before then: a material its body's faces share completes for all of them at once + void dirtyMeshVisibility() { mMeshVisibilityDirty = true; } LLViewerTexture* getBakedTexture(const U8 te); // Matrix palette cache entry @@ -989,6 +992,7 @@ class LLVOAvatar : LLViewerJoint* getViewerJoint(S32 idx); S32 mDirtyMesh; // 0 -- not dirty, 1 -- morphed, 2 -- LOD bool mMeshTexturesDirty; + bool mMeshVisibilityDirty = false; //-------------------------------------------------------------------- // Destroy invisible mesh From 7aaeae90f4ac674213a8e4e98a989fbb3e923165 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 26 Oct 2026 09:30:53 -0400 Subject: [PATCH 08/10] A face drawing a material that names a bake gets its own copy wherever it took it updateTEMaterialTextures resolves a material's bake proxies for the object's avatar into whatever the face draws, which is the material gGLTFMaterialList shares when the face has no copy of its own. The face that took a material still fetching now gets its copy when it completes, but a face can still reach a build drawing the shared material: a copied LLTextureEntry keeps no render material, and LLVOVolume::setNumTEs copies the last face into the new ones when it plays media. The bakes of the avatar built last were then drawn on every face drawing the material. A face drawing a material that names a bake now takes its copy there, before anything is resolved into it. ALBakeProxies::anyNamedBy is the test, which initRenderMaterial makes too. Co-Authored-By: Claude Opus 5.5 --- indra/newview/albakeproxies.cpp | 12 +++++++++++ indra/newview/albakeproxies.h | 8 ++++++-- indra/newview/llviewerobject.cpp | 23 +++++++++++----------- indra/newview/tests/albakeproxies_test.cpp | 5 +++++ 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/indra/newview/albakeproxies.cpp b/indra/newview/albakeproxies.cpp index 345a97d47d..53f9bcd2c9 100644 --- a/indra/newview/albakeproxies.cpp +++ b/indra/newview/albakeproxies.cpp @@ -39,6 +39,18 @@ namespace namespace ALBakeProxies { +bool anyNamedBy(const LLGLTFMaterial& material) +{ + for (const LLUUID& id : material.mTextureId) + { + if (LLAvatarAppearanceDictionary::isBakedImageId(id)) + { + return true; + } + } + return false; +} + bake_set_t drawnBy(const LLGLTFMaterial* material) { return material ? named_by(material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR]) : bake_set_t(); diff --git a/indra/newview/albakeproxies.h b/indra/newview/albakeproxies.h index 52549fd3f8..18c54b5871 100644 --- a/indra/newview/albakeproxies.h +++ b/indra/newview/albakeproxies.h @@ -1,7 +1,7 @@ /** * @file albakeproxies.h - * @brief The bakes a face names through bake proxies (IMG_USE_BAKED_*), whose system body parts an avatar - * wearing it hides. + * @brief The bakes a face names through bake proxies (IMG_USE_BAKED_*): those an avatar wearing it hides + * the system body parts of, and whether its PBR material resolves one for its own avatar. * * Copyright (c) 2026, Rye Mutt * @@ -27,6 +27,10 @@ namespace ALBakeProxies // A set of bakes, by EBakedTextureIndex typedef std::bitset bake_set_t; + // Whether any slot of a PBR material names a bake proxy, which the face drawing it resolves for its own + // object's avatar, and so draws from a copy of its own (LLViewerObject::initRenderMaterial) + bool anyNamedBy(const LLGLTFMaterial& material); + // The bake a PBR material draws a body with: the one its base colour names, the slot that stands for a // face's image. A bake in its other slots shades the surface without drawing a body bake_set_t drawnBy(const LLGLTFMaterial* material); diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index 4a0a1bcfc6..7c86bd66ef 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -5499,6 +5499,16 @@ void LLViewerObject::updateTEMaterialTextures(U8 te) getTE(te)->setGLTFMaterial(nullptr); } + // The bakes a material names are resolved for this object's avatar, into the face's own copy of the + // material (initRenderMaterial): resolved into the material itself, they would be drawn on every face it + // is on, whoever wears it. A face still drawing the material itself, as one whose texture entry was + // copied (a copied LLTextureEntry keeps no render material), gets its copy here + if (mat != nullptr && mat == getTE(te)->getGLTFMaterial() && ALBakeProxies::anyNamedBy(*mat)) + { + initRenderMaterial(te); + mat = (LLFetchedGLTFMaterial*) getTE(te)->getGLTFRenderMaterial(); + } + // A slot naming a bake proxy draws what it resolves to on this object's avatar // (LLFetchedGLTFMaterial::mBakedTextures), and fetches nothing; any other fetches its asset auto fetch_texture = [this, mat](LLGLTFMaterial::TextureInfo slot) @@ -5923,18 +5933,7 @@ S32 LLViewerObject::initRenderMaterial(U8 te) if (!base_material) { return 0; } const LLGLTFMaterial* override_material = tep->getGLTFMaterialOverride(); LLFetchedGLTFMaterial* render_material = nullptr; - bool need_render_material = override_material; - if (!need_render_material) - { - for (const LLUUID& texture_id : base_material->mTextureId) - { - if (LLAvatarAppearanceDefines::LLAvatarAppearanceDictionary::isBakedImageId(texture_id)) - { - need_render_material = true; - break; - } - } - } + const bool need_render_material = override_material || ALBakeProxies::anyNamedBy(*base_material); if (need_render_material) { render_material = new LLFetchedGLTFMaterial(*base_material); diff --git a/indra/newview/tests/albakeproxies_test.cpp b/indra/newview/tests/albakeproxies_test.cpp index d564a688f6..b76edad4db 100644 --- a/indra/newview/tests/albakeproxies_test.cpp +++ b/indra/newview/tests/albakeproxies_test.cpp @@ -74,6 +74,9 @@ namespace LLTextureEntry te(IMG_DEFAULT); CHECK(drawnBy(te, false).none()); CHECK(drawnBy(nullptr).none()); + + LLPointer material = new LLGLTFMaterial(); + CHECK_FALSE(anyNamedBy(*material)); } TEST_CASE_METHOD(face_data, "ALBakeProxies: a PBR material draws the bake its base colour names", "[ALBakeProxies]") @@ -95,6 +98,8 @@ namespace CHECK(drawnBy(mMaterial).none()); CHECK(drawnBy(mTE, false).none()); + // but each is resolved for the face's own avatar + CHECK(anyNamedBy(*mMaterial)); } TEST_CASE_METHOD(face_data, "ALBakeProxies: a HUD's face counts its image and not its material", "[ALBakeProxies]") From 1e5a7d7d962bc7510a53e63110719a61e839d569 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 26 Oct 2026 09:30:56 -0400 Subject: [PATCH 09/10] Refresh Textures refreshes the slots of a PBR copy not yet built object_texture_refresh refreshed a PBR slot only where getRenderTexture gave a texture. A copy initRenderMaterial makes holds none until the face's next build, as when an override arrives during one, and Refresh Textures then skipped every slot of the face. Such a slot refreshes its own ID, which is what the face will draw. For a bake proxy that clears only a fetched texture of the proxy, which a HUD or an animated object draws. Co-Authored-By: Claude Opus 5.5 --- indra/newview/alviewermenu.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/indra/newview/alviewermenu.cpp b/indra/newview/alviewermenu.cpp index 4f38489a66..bb69ca79e9 100644 --- a/indra/newview/alviewermenu.cpp +++ b/indra/newview/alviewermenu.cpp @@ -417,16 +417,15 @@ namespace } // What each slot draws, as getTEImage is for the image: a bake proxy's bake, not the - // proxy's own ID + // proxy's own ID. A copy of the material not yet built since it was made + // (initRenderMaterial) draws nothing yet, and its slots' own IDs are what it will draw const auto* mat = static_cast(node->getObject()->getTE(i)->getGLTFRenderMaterial()); if (mat) { for (U32 j = 0; j < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; ++j) { - if (const LLViewerTexture* tex = mat->getRenderTexture((LLGLTFMaterial::TextureInfo)j)) - { - faces_per_tex[tex->getID()].push_back(i); - } + const LLViewerTexture* tex = mat->getRenderTexture((LLGLTFMaterial::TextureInfo)j); + faces_per_tex[tex ? tex->getID() : mat->mTextureId[j]].push_back(i); } } } From 5cc8430be5711cb781e4457c284f01fb6ae84e65 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 26 Oct 2026 09:37:03 -0400 Subject: [PATCH 10/10] The spot shadow cache digests the bake a PBR caster is alpha tested by The spot shadow cache (#544) digests an alpha-tested GLTF caster's base colour from mBaseColorTexture, which holds nothing for a slot naming a bake proxy now that the bake is LLFetchedGLTFMaterial::mBakedTextures'. A caster masked by its bake kept its cached shadow when the bake changed, and while the avatar edits its appearance the local composite, a dynamic texture that marks the casters changing, went unseen. The digest reads getRenderTexture, the texture the shadow pass binds. Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 29471d814b..9099505c6c 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -13190,7 +13190,7 @@ namespace draw.add((U64)material->mDoubleSided); if (textured) { - add_shadow_texture(casters, draw, material->mBaseColorTexture.get()); + add_shadow_texture(casters, draw, material->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR)); draw.add((U64)material->mAlphaMode).add(material->mAlphaCutoff).add(material->mBaseColor.mV[VALPHA]); LLGLTFMaterial::TextureTransform::Pack packed; material->mTextureTransform[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR].getPacked(packed);