From 93c320b7ebb8f8a9e98c0cc682822d41e109d5c0 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 3 Nov 2026 09:58:00 -0500 Subject: [PATCH] A face an object gains takes its own texture entry, material and override An object's texture entry was applied only to the faces it had as the entry arrived. A mesh has the faces of its placeholder shape until its asset loads, and each face it gained then was a copy of its last face: its texture entry, its render material, which LLViewerObject::setNumTEs also wrote into the render material parameter block for the face, and its glTF override. The entry named every face, and the block each face's material, and the copies stayed until another update brought them again. LLPrimitive now keeps the texture entry it last unpacked, as it arrived, and a face it gains takes the value that entry gives it. A refused or empty entry leaves the one kept as it was. A primitive that has unpacked none, as a client-only object, copies its last face as before. A face gained by an object that keeps an entry takes the render material the block names for it and the override the region holds for it, applied as a rebuild applies them (applyQueuedOverrides), and not the last face's override. setRenderMaterialID writes the block only where it names another material, so a face given no material where the block names none takes no entry: the simulator's render materials, applied to each face, had added an empty entry for every face they named nothing for. Faces removed go as before, and an update that changes the shape still applies its own entry after the faces change. The tests cover LLPrimitive. The render material and override of a face gained have no test, as newview_tests does not build LLViewerObject. Co-Authored-By: Claude Opus 5.5 --- indra/llprimitive/llprimitive.cpp | 79 ++++++++--- indra/llprimitive/llprimitive.h | 14 ++ .../tests/llprimitivetemessage_test.cpp | 126 ++++++++++++++++++ indra/newview/llviewerobject.cpp | 30 ++++- 4 files changed, 225 insertions(+), 24 deletions(-) diff --git a/indra/llprimitive/llprimitive.cpp b/indra/llprimitive/llprimitive.cpp index c4b4447a23..33f47f6096 100644 --- a/indra/llprimitive/llprimitive.cpp +++ b/indra/llprimitive/llprimitive.cpp @@ -262,7 +262,14 @@ LLTextureEntry* LLPrimitive::getTE(const U8 index) const //=============================================================== void LLPrimitive::setNumTEs(const U8 num_tes) { + const U8 old_num_tes = getNumTEs(); + // A face gained starts as a copy of the last, and takes the values of the + // entry last unpacked where there is one. mTextureList.setSize(num_tes); + if (num_tes > old_num_tes) + { + applyTEEntry(old_num_tes); + } } //=============================================================== @@ -1307,6 +1314,28 @@ S32 LLPrimitive::parseTEMessage(LLMessageSystem* mesgsys, char const* block_name namespace { + // Reads a message's TextureEntry field whole into packed; block_num < 0 + // reads block 0. + void read_te_field(LLMessageSystem* mesgsys, char const* block_name, const S32 block_num, boost::container::small_vector_base& packed) + { + S32 size; + if (block_num < 0) + { + size = mesgsys->getSizeFast(block_name, _PREHASH_TextureEntry); + } + else + { + size = mesgsys->getSizeFast(block_name, block_num, _PREHASH_TextureEntry); + } + + packed.clear(); + if (size > 0) + { + packed.resize(size); + mesgsys->getBinaryDataFast(block_name, _PREHASH_TextureEntry, packed.data(), 0, std::max(block_num, 0), size); + } + } + // Parses a packed TextureEntry field of any length into tec, for // face_count faces. S32 parse_te_entry(boost::container::small_vector_base& packed, LLTEContents& tec, U8 face_count) @@ -1367,23 +1396,8 @@ namespace // static S32 LLPrimitive::parseTEMessage(LLMessageSystem* mesgsys, char const* block_name, const S32 block_num, LLTEContents& tec, U8 face_count) { - S32 size; - if (block_num < 0) - { - size = mesgsys->getSizeFast(block_name, _PREHASH_TextureEntry); - } - else - { - size = mesgsys->getSizeFast(block_name, block_num, _PREHASH_TextureEntry); - } - te_bytes_t packed; - if (size > 0) - { - packed.resize(size); - // if block_num < 0 ask for block 0 - mesgsys->getBinaryDataFast(block_name, _PREHASH_TextureEntry, packed.data(), 0, std::max(block_num, 0), size); - } + read_te_field(mesgsys, block_name, block_num, packed); return parse_te_entry(packed, tec, face_count); } @@ -1408,11 +1422,16 @@ void LLPrimitive::unpackCompressedData(LLDataPacker& dp, U32 flags, boost::conta } S32 LLPrimitive::applyParsedTEMessage(LLTEContents& tec) +{ + return applyParsedTEFaces(tec, 0); +} + +S32 LLPrimitive::applyParsedTEFaces(LLTEContents& tec, U32 first_face) { S32 retval = 0; LLColor4 color; - for (U32 i = 0; i < tec.face_count; i++) + for (U32 i = first_face; i < tec.face_count; i++) { LLUUID& req_id = ((LLUUID*)tec.image_data)[i]; retval |= setTETexture(i, req_id); @@ -1440,11 +1459,9 @@ S32 LLPrimitive::applyParsedTEMessage(LLTEContents& tec) S32 LLPrimitive::unpackTEMessage(LLMessageSystem* mesgsys, char const* block_name, const S32 block_num) { - LLTEContents tec; - S32 retval = parseTEMessage(mesgsys, block_name, block_num, tec); - if (!retval) - return retval; - return applyParsedTEMessage(tec); + te_bytes_t packed; + read_te_field(mesgsys, block_name, block_num, packed); + return unpackTEEntry(packed); } S32 LLPrimitive::unpackTEMessage(LLDataPacker &dp) @@ -1463,11 +1480,29 @@ S32 LLPrimitive::unpackTEEntry(boost::container::small_vector_base& packed) LLTEContents tec; if (!parse_te_entry(packed, tec, getNumTEs())) { + // An entry refused, or none, leaves the one kept as it was. return 0; } + // Kept as it arrived, without the zero the parse ends it with. + mTEEntry.assign(packed.begin(), packed.end() - 1); return applyParsedTEMessage(tec); } +void LLPrimitive::applyTEEntry(U8 first_face) +{ + if (mTEEntry.empty()) + { + return; + } + + te_bytes_t packed(mTEEntry.begin(), mTEEntry.end()); + LLTEContents tec; + if (parse_te_entry(packed, tec, getNumTEs())) + { + applyParsedTEFaces(tec, first_face); + } +} + U8 LLPrimitive::getExpectedNumTEs() const { U8 expected_face_count = 0; diff --git a/indra/llprimitive/llprimitive.h b/indra/llprimitive/llprimitive.h index a1f79e0e55..a18558d25d 100644 --- a/indra/llprimitive/llprimitive.h +++ b/indra/llprimitive/llprimitive.h @@ -556,6 +556,10 @@ class LLPrimitive : public LLXform S32 applyParsedTEMessage(LLTEContents& tec); + // Whether the primitive keeps a texture entry it unpacked, whose values + // the faces it gains take (setNumTEs). + bool hasTEEntry() const { return !mTEEntry.empty(); } + #ifdef CHECK_FOR_FINITE inline void setPosition(const LLVector3& pos); inline void setPosition(const F32 x, const F32 y, const F32 z); @@ -634,6 +638,11 @@ class LLPrimitive : public LLXform private: void updateNumBumpmap(const U8 index, const U8 bump); + // Applies the faces of tec from first_face on. + S32 applyParsedTEFaces(LLTEContents& tec, U32 first_face); + // The faces from first_face on take the values mTEEntry gives them. + void applyTEEntry(U8 first_face); + protected: LLPCode mPrimitiveCode; // Primitive code LLVector3 mVelocity; // how fast are we moving? @@ -646,6 +655,11 @@ class LLPrimitive : public LLXform U8 mNumBumpmapTEs; // number of bumpmap TEs. U32 mMiscFlags; // home for misc bools + // The texture entry last unpacked, as it arrived. It gives a value for + // every face, so a face the primitive gains later, as a mesh's asset + // loads, takes its own value from it rather than the last face's. + std::vector mTEEntry; + public: static LLVolumeMgr* sVolumeManager; diff --git a/indra/llprimitive/tests/llprimitivetemessage_test.cpp b/indra/llprimitive/tests/llprimitivetemessage_test.cpp index 1cb2e67a81..4972a7fd93 100644 --- a/indra/llprimitive/tests/llprimitivetemessage_test.cpp +++ b/indra/llprimitive/tests/llprimitivetemessage_test.cpp @@ -163,4 +163,130 @@ namespace CHECK(copy.unpackTEMessage(reader) == TEM_INVALID); CHECK(packed_entry(copy) == before); } + + // Unpacks entry into prim through a data packer, as an update does. + S32 unpack_entry(LLPrimitive& prim, const std::vector& entry) + { + std::vector buffer = sized(entry); + LLDataPackerBinaryBuffer reader(buffer.data(), (S32)buffer.size()); + return prim.unpackTEMessage(reader); + } + + // A mesh has the faces of its placeholder shape until its asset loads, + // fewer than the entry it is sent names. + TEST_CASE("LLPrimitiveTEMessage: the faces a primitive gains take the values its entry gives them", "[LLPrimitiveTEMessage]") + { + LLPrimitive sent; + give_faces_their_own_entries(sent, 6); + const std::vector entry = packed_entry(sent); + + LLPrimitive prim; + prim.setNumTEs(4); + CHECK(unpack_entry(prim, entry) != TEM_INVALID); + REQUIRE(prim.hasTEEntry()); + + prim.setNumTEs(6); + REQUIRE(prim.getNumTEs() == 6); + CHECK(prim.getTE(4)->getID() == face_uuid(4, 1)); + CHECK(prim.getTE(5)->getID() == face_uuid(5, 1)); + CHECK(prim.getTE(5)->getMaterialID() == LLMaterialID(face_uuid(5, 100))); + CHECK(packed_entry(prim) == entry); + } + + TEST_CASE("LLPrimitiveTEMessage: faces removed and gained again take the values the entry gives them", "[LLPrimitiveTEMessage]") + { + LLPrimitive sent; + give_faces_their_own_entries(sent, 6); + const std::vector entry = packed_entry(sent); + + LLPrimitive prim; + prim.setNumTEs(6); + CHECK(unpack_entry(prim, entry) != TEM_INVALID); + + prim.setNumTEs(2); + REQUIRE(prim.getNumTEs() == 2); + CHECK(prim.getTE(1)->getID() == face_uuid(1, 1)); + + prim.setNumTEs(6); + CHECK(packed_entry(prim) == entry); + } + + TEST_CASE("LLPrimitiveTEMessage: the faces a primitive gains take the values of the newest entry", "[LLPrimitiveTEMessage]") + { + LLPrimitive sent; + give_faces_their_own_entries(sent, 6); + const std::vector first = packed_entry(sent); + sent.setTETexture(4, face_uuid(4, 50)); + sent.setTEColor(5, LLColor4(0.5f, 0.25f, 0.75f, 1.f)); + const std::vector second = packed_entry(sent); + REQUIRE(second != first); + + LLPrimitive prim; + prim.setNumTEs(4); + CHECK(unpack_entry(prim, first) != TEM_INVALID); + CHECK(unpack_entry(prim, second) != TEM_INVALID); + + prim.setNumTEs(6); + CHECK(prim.getTE(4)->getID() == face_uuid(4, 50)); + CHECK(packed_entry(prim) == second); + } + + // A refused entry changes no face, so the faces gained later take the + // values of the entry before it. + TEST_CASE("LLPrimitiveTEMessage: the faces a primitive gains after a refused entry take the entry before it", "[LLPrimitiveTEMessage]") + { + LLPrimitive sent; + give_faces_their_own_entries(sent, 6); + const std::vector entry = packed_entry(sent); + + LLPrimitive prim; + prim.setNumTEs(4); + CHECK(unpack_entry(prim, entry) != TEM_INVALID); + + // Cut inside the first field's default value, and empty: each + // changes nothing. + const std::vector malformed(entry.begin(), entry.begin() + 8); + CHECK(unpack_entry(prim, malformed) == TEM_CHANGE_NONE); + CHECK(unpack_entry(prim, {}) == TEM_CHANGE_NONE); + + prim.setNumTEs(6); + CHECK(packed_entry(prim) == entry); + } + + // An entry gives every face past the last it packs that face's value. + TEST_CASE("LLPrimitiveTEMessage: faces gained past those an entry packs take the value it gives every face", "[LLPrimitiveTEMessage]") + { + LLPrimitive sent; + give_faces_their_own_entries(sent, 4); + const std::vector entry = packed_entry(sent); + + LLPrimitive prim; + prim.setNumTEs(4); + CHECK(unpack_entry(prim, entry) != TEM_INVALID); + + prim.setNumTEs(6); + for (U8 face = 4; face < 6; ++face) + { + CAPTURE((int)face); + CHECK(*prim.getTE(face) == *prim.getTE(3)); + } + CHECK(prim.getTE(3)->getID() == face_uuid(3, 1)); + } + + // With no entry to take values from, a face gained is a copy of the + // last. + TEST_CASE("LLPrimitiveTEMessage: a primitive that unpacked no entry copies its last face into the faces it gains", "[LLPrimitiveTEMessage]") + { + LLPrimitive prim; + give_faces_their_own_entries(prim, 2); + REQUIRE_FALSE(prim.hasTEEntry()); + + prim.setNumTEs(4); + for (U8 face = 2; face < 4; ++face) + { + CAPTURE((int)face); + CHECK(*prim.getTE(face) == *prim.getTE(1)); + } + CHECK(prim.getTE(1)->getID() == face_uuid(1, 1)); + } } diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index 57d3e5bcbc..f1025c7d1c 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -5229,8 +5229,29 @@ void LLViewerObject::setNumTEs(const U8 num_tes) LLPrimitive::setNumTEs(num_tes); setChanged(TEXTURE); + if (hasTEEntry()) + { + // The faces gained took their values from the texture entry last + // unpacked. Each takes the render material the parameter block + // names for it, and the override the region holds for it, rather + // than the last face's. + if (original_tes < getNumTEs()) + { + for (int i = original_tes; i < getNumTEs(); ++i) + { + LLTextureEntry* tep = getTE(i); + const LLUUID id = getRenderMaterialID(i); + tep->setGLTFMaterialOverride(nullptr); + if (id.notNull() || tep->getGLTFMaterial()) + { + setRenderMaterialID(i, id, false, false); + } + } + gGLTFMaterialList.applyQueuedOverrides(this); + } + } // touch up GLTF materials - if (original_tes > 0) + else if (original_tes > 0) { for (int i = original_tes; i < getNumTEs(); ++i) { @@ -7908,7 +7929,12 @@ void LLViewerObject::setRenderMaterialID(S32 te_in, const LLUUID& id, bool updat { // update existing parameter block for (S32 te = start_idx; te < end_idx; ++te) { - param_block->setMaterial(te, id); + // Only where the block names another material: a face given no + // material where it names none takes no entry for it. + if (param_block->getMaterial(te) != id) + { + param_block->setMaterial(te, id); + } } }